Facility: 090738

ABC Storage LLC

Stale Data Warning: This facility has not been successfully scraped in 26 days (threshold: 3 days). Data may be outdated.
Facility Information active
Facility ID
090738
Name
ABC Storage LLC
URL
https://www.abcstoragellc.com/
Address
N/A
Platform
custom_facility_090738
Parser File
src/parsers/custom/facility_090738_parser.py
Last Scraped
2026-03-27 14:04:21.579533
Created
2026-03-06 23:45:35.865957
Updated
2026-03-27 14:04:21.579533
Parser & Healing Diagnosis needs_fix
Parser Status
⚠ Needs Fix
Status Reason
Parser returned 0 units
Last Healing Attempt
Not attempted
Parser Source (src/parsers/custom/facility_090738_parser.py)
"""Parser for ABC Mini-Storage (facility 090738).

This site (abcstoragellc.com) is a brochure-style WordPress/ThemeSuite site
that does not publish pricing online. Unit sizes are listed on the
/unit-sizing/ page inside <article> elements grouped by category
(Small / Medium / Large). Each category has an <h2 class="tsI_sh"> heading
and <li> items containing dimension strings like "5' x 5'".

Because no prices are available the parser records size and category
description only; price fields are left as None.
"""

from __future__ import annotations

import re

from bs4 import BeautifulSoup

from src.parsers.base import BaseParser, ParseResult, UnitResult

# Matches dimension strings such as "5' x 5'", "7'x 10'", "10' x 20'"
_SIZE_RE = re.compile(
    r"(\d+(?:\.\d+)?)\s*['\u2019\u2032]?\s*[xX\u00d7]\s*(\d+(?:\.\d+)?)\s*['\u2019\u2032]?",
)

# Extracts the square-footage bracket from headings like "Small (0-50 sq. ft.)"
_CATEGORY_RE = re.compile(r"^(Small|Medium|Large)\s*\(([^)]+)\)", re.IGNORECASE)


class Facility090738Parser(BaseParser):
    """Extract storage unit sizes from ABC Mini-Storage unit-sizing page.

    No pricing data is published on this site; price fields will be None.
    Sizes and category descriptions are extracted from the /unit-sizing/ page.
    """

    platform = "custom_facility_090738"

    def parse(self, html: str, url: str = "") -> ParseResult:
        soup = BeautifulSoup(html, "lxml")
        result = ParseResult(platform=self.platform, parser_name=self.__class__.__name__)

        # Find all article containers that hold size groups
        articles = soup.find_all("article", class_=re.compile(r"\btsI\b"))

        if not articles:
            # Fallback: search the whole page for li items with dimension text
            articles = [soup]

        for article in articles:
            # Extract category heading e.g. "Small (0-50 sq. ft.)"
            heading_tag = article.find(class_="tsI_sh")
            category = heading_tag.get_text(strip=True) if heading_tag else ""

            # Extract category description from the paragraph inside tsI_dsc
            desc_tag = article.find(class_="tsI_dsc")
            category_desc = ""
            if desc_tag:
                p = desc_tag.find("p")
                if p:
                    category_desc = p.get_text(separator=" ", strip=True)

            # Extract each <li> containing a dimension string
            li_items = article.find_all("li")
            for li in li_items:
                li_text = li.get_text(strip=True)
                m = _SIZE_RE.search(li_text)
                if not m:
                    continue

                width = float(m.group(1))
                length = float(m.group(2))
                sqft = width * length

                size_label = f"{int(width)}' x {int(length)}'"

                unit = UnitResult(
                    size=size_label,
                    description=category_desc if category_desc else None,
                    price=None,
                    metadata={
                        "width": width,
                        "length": length,
                        "sqft": sqft,
                        "category": category,
                    },
                )
                result.units.append(unit)

        if not result.units:
            result.warnings.append(
                "No unit size entries found. "
                "Check that the snapshot is from the /unit-sizing/ page."
            )
        else:
            result.warnings.append(
                "No pricing data available on this site. "
                "Prices must be obtained by calling 307-266-2080."
            )

        return result

Scrape Runs (7)

Run #12 Details

Status
started
Parser Used
N/A
Platform Detected
N/A
Units Found
0
Stage Reached
started
Timestamp
2026-03-07 01:05:47.916473

All Failures for this Facility (6)

parse _WarningAsException scraper no_units_extracted warning Run #N/A | 2026-03-27 14:04:21.559923

No units extracted for 090738

Stack trace
src.reporting.failure_reporter._WarningAsException: No units extracted for 090738
parse _WarningAsException scraper no_units_extracted warning Run #N/A | 2026-03-27 14:04:21.553830

No units extracted for 090738

Stack trace
src.reporting.failure_reporter._WarningAsException: No units extracted for 090738
parse _WarningAsException scraper no_units_extracted warning Run #N/A | 2026-03-23 03:06:40.060588

No units extracted for 090738

Stack trace
src.reporting.failure_reporter._WarningAsException: No units extracted for 090738
parse _WarningAsException scraper no_units_extracted warning Run #N/A | 2026-03-21 18:58:40.165066

No units extracted for 090738

Stack trace
src.reporting.failure_reporter._WarningAsException: No units extracted for 090738
parse _WarningAsException scraper no_units_extracted warning Run #N/A | 2026-03-14 16:38:19.315337

No units extracted for 090738

Stack trace
src.reporting.failure_reporter._WarningAsException: No units extracted for 090738
fetch DatatypeMismatch unknown unknown permanent Run #22 | 2026-03-07 01:42:41.097915

column "success" is of type boolean but expression is of type integer LINE 3: ... VALUES ('090738', 22, '090738_20260307T014241Z.html', 0) ^ HINT: You will need to rewrite or cast the expression.

Stack trace
Traceback (most recent call last):
  File "/app/src/pipeline.py", line 329, in _process_facility
    manifest_id = storage.insert_snapshot_manifest(
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/src/db/pg_backend.py", line 615, in insert_snapshot_manifest
    row = self._execute_returning(
          ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/src/db/pg_backend.py", line 54, in _execute_returning
    cur.execute(sql, params)
  File "/app/.venv/lib/python3.11/site-packages/psycopg2/extras.py", line 236, in execute
    return super().execute(query, vars)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
psycopg2.errors.DatatypeMismatch: column "success" is of type boolean but expression is of type integer
LINE 3: ...    VALUES ('090738', 22, '090738_20260307T014241Z.html', 0)
                                                                     ^
HINT:  You will need to rewrite or cast the expression.

← Back to dashboard