Facility: 011634

ACMS Storage

Stale Data Warning: This facility has not been successfully scraped in 30 days (threshold: 3 days). Data may be outdated.
Facility Information active
Facility ID
011634
Name
ACMS Storage
URL
http://www.acmsstorage.com/
Address
N/A
Platform
custom_facility_011634
Parser File
src/parsers/custom/facility_011634_parser.py
Last Scraped
2026-03-23 03:18:18.309576
Created
2026-03-06 23:45:35.865957
Updated
2026-03-23 03:18:18.309576
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_011634_parser.py)
"""Parser for ACMS Storage (Sheridan, WY) — Storable/Storedge platform.

The site is a React SPA (Storable Voyager) that embeds unit data in a
``window.__APOLLO_STATE__`` JSON blob. Unit groups are keyed as
``UnitGroup:<uuid>`` with dimensions encoded in the ``name`` field as
``WxLx0 - $streetPrice - facilityCode - [unitIds]`` and the current
listed price in the ``price`` field.
"""

from __future__ import annotations

import json
import re

from bs4 import BeautifulSoup

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


class Facility011634Parser(BaseParser):
    """Extract storage units from ACMS Storage via Apollo state JSON.

    The page embeds a ``window.__APOLLO_STATE__`` JSON object that contains
    ``UnitGroup`` entries.  Each entry has:

    - ``name``: ``"WxLx0 - $<street_price> - <code> - [<ids>]"``
    - ``price``: current listed price (float)
    - ``type``: unit type string, e.g. ``"Self Storage"``, ``"Lot Parking"``
    """

    platform = "custom_facility_011634"

    # Parses "10x15x0 - $95.00 - ..." → width=10, length=15, street_price=95.00
    _NAME_RE = re.compile(
        r"^(?P<width>\d+)x(?P<length>\d+)x\d+\s*-\s*\$(?P<street_price>[\d.]+)"
    )

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

        # Locate the Apollo state script
        apollo_data = self._extract_apollo_state(soup)
        if apollo_data is None:
            result.warnings.append("window.__APOLLO_STATE__ not found in page")
            return result

        unit_groups = [
            v for k, v in apollo_data.items()
            if k.startswith("UnitGroup:") and isinstance(v, dict)
        ]

        if not unit_groups:
            result.warnings.append("No UnitGroup entries found in Apollo state")
            return result

        for group in unit_groups:
            unit = self._parse_unit_group(group)
            if unit is not None:
                result.units.append(unit)

        if not result.units:
            result.warnings.append("UnitGroup entries found but none could be parsed")

        return result

    # ------------------------------------------------------------------
    # Helpers
    # ------------------------------------------------------------------

    def _extract_apollo_state(self, soup: BeautifulSoup) -> dict | None:
        """Return the parsed ``__APOLLO_STATE__`` dict or ``None``."""
        for script in soup.find_all("script"):
            text = script.string or ""
            if "__APOLLO_STATE__" not in text:
                continue
            try:
                idx = text.index("{")
                decoder = json.JSONDecoder()
                data, _ = decoder.raw_decode(text, idx)
                return data
            except (ValueError, json.JSONDecodeError):
                continue
        return None

    def _parse_unit_group(self, group: dict) -> UnitResult | None:
        """Convert a single UnitGroup dict into a ``UnitResult``."""
        name = group.get("name", "")
        listed_price = group.get("price")
        unit_type = group.get("type", "")

        m = self._NAME_RE.match(name)
        if not m:
            return None

        width = float(m.group("width"))
        length = float(m.group("length"))
        street_price = float(m.group("street_price"))

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

        unit = UnitResult(
            size=size,
            description=f"{size} {unit_type}".strip(),
            price=float(listed_price) if listed_price is not None else None,
            sale_price=street_price if street_price != float(listed_price or 0) else None,
            metadata={
                "width": width,
                "length": length,
                "sqft": width * length,
                "unit_type": unit_type,
            },
        )

        return unit

Scrape Runs (5)

Run #46 Details

Status
exported
Parser Used
Facility011634Parser
Platform Detected
storageunitsoftware
Units Found
0
Stage Reached
exported
Timestamp
2026-03-13 21:12:34.137441
Timing
Stage Duration
Fetch5483ms
Detect22ms
Parse9ms
Export17ms

Snapshot: 011634_20260313T211239Z.html · Show Snapshot · Open in New Tab

No units found in this run.

All Failures for this Facility (5)

parse _WarningAsException scraper no_units_extracted warning Run #N/A | 2026-03-23 03:18:18.305867

No units extracted for 011634

Stack trace
src.reporting.failure_reporter._WarningAsException: No units extracted for 011634
parse _WarningAsException scraper no_units_extracted warning Run #N/A | 2026-03-21 19:11:11.221323

No units extracted for 011634

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

No units extracted for 011634

Stack trace
src.reporting.failure_reporter._WarningAsException: No units extracted for 011634
parse _WarningAsException scraper no_units_extracted warning Run #N/A | 2026-03-14 01:05:25.905634

No units extracted for 011634

Stack trace
src.reporting.failure_reporter._WarningAsException: No units extracted for 011634
parse _WarningAsException scraper no_units_extracted warning Run #N/A | 2026-03-13 21:12:39.695954

No units extracted for 011634

Stack trace
src.reporting.failure_reporter._WarningAsException: No units extracted for 011634

← Back to dashboard