Facility: 027618

Antelope Meadows 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
027618
Name
Antelope Meadows Storage
URL
http://www.antelopemeadowsstorage.com/
Address
N/A
Platform
custom_facility_027618
Parser File
src/parsers/custom/facility_027618_parser.py
Last Scraped
2026-03-23 03:18:34.017217
Created
2026-03-06 23:45:35.865957
Updated
2026-03-23 03:18:34.017217
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_027618_parser.py)
"""Parser for Antelope Meadows Mini Storage (Laramie, WY).

This site uses the Storedge/Voyager platform. Unit pricing is embedded in a
``window.__APOLLO_STATE__`` JSON block on the facility detail page
(e.g. ``/379-w-shields-st-laramie-wy-82072``).  Each ``UnitGroup`` entry
contains a ``name`` (dimensions like ``"10x15"``), a ``type``
(``"Self Storage"`` or ``"Parking"``), and a ``price``.

The main homepage does NOT contain unit data — the fetcher must follow the
``/Unit Prices`` navigation link to the facility page.
"""

from __future__ import annotations

import json
import re

from bs4 import BeautifulSoup

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


class Facility027618Parser(BaseParser):
    """Extract storage units from Antelope Meadows Mini Storage (Storedge platform).

    Reads ``window.__APOLLO_STATE__`` embedded JSON and extracts all
    ``UnitGroup`` objects.  Duplicate entries (same name, type, and price)
    are collapsed to a single record.
    """

    platform = "custom_facility_027618"

    # Matches the APOLLO_STATE assignment; the JSON ends at the next
    # ``window.__`` assignment on the same script block.
    _APOLLO_RE = re.compile(r"window\.__APOLLO_STATE__\s*=\s*(\{.*?)(?:,window\.__|\Z)", re.DOTALL)

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

        apollo_data = self._extract_apollo_state(soup)
        if apollo_data is None:
            result.warnings.append("window.__APOLLO_STATE__ not found or not parseable")
            return result

        seen: set[tuple] = set()

        for key, value in apollo_data.items():
            if not key.startswith("UnitGroup:"):
                continue

            name = value.get("name", "")
            unit_type = value.get("type", "")
            price_raw = value.get("price")

            if not name:
                continue

            dedup_key = (name, unit_type, price_raw)
            if dedup_key in seen:
                continue
            seen.add(dedup_key)

            unit = UnitResult()

            # Normalise display size
            size_text = name.strip()
            unit.size = size_text

            # Parse dimensions from names like "10x15" or "12x35"
            w, ln, sq = self.normalize_size(size_text)
            meta: dict = {}
            if w is not None:
                meta = {"width": w, "length": ln, "sqft": sq}

            # Capture unit type (Self Storage vs Parking)
            if unit_type:
                meta["unit_type"] = unit_type

            if meta:
                unit.metadata = meta

            # Price is already a number in the JSON
            if price_raw is not None:
                try:
                    unit.price = float(price_raw)
                except (TypeError, ValueError):
                    unit.price = self.normalize_price(str(price_raw))

            # Description combines size and type
            unit.description = f"{size_text} {unit_type}".strip()

            if unit.size or unit.price:
                result.units.append(unit)

        if not result.units:
            result.warnings.append(
                "No UnitGroup entries found in Apollo state — "
                "ensure the snapshot is from the facility detail page, not the homepage"
            )

        return result

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

    def _extract_apollo_state(self, soup: BeautifulSoup) -> dict | None:
        """Return the parsed __APOLLO_STATE__ dict, or None on failure."""
        for script in soup.find_all("script"):
            text = script.string or ""
            if "__APOLLO_STATE__" not in text:
                continue

            match = self._APOLLO_RE.search(text)
            if not match:
                continue

            json_text = match.group(1)
            try:
                return json.loads(json_text)
            except json.JSONDecodeError:
                # Try stripping a trailing comma if present
                cleaned = json_text.rstrip().rstrip(",")
                try:
                    return json.loads(cleaned)
                except json.JSONDecodeError:
                    return None

        return None

Scrape Runs (5)

Run #1471 Details

Status
exported
Parser Used
Facility027618Parser
Platform Detected
storageunitsoftware
Units Found
0
Stage Reached
exported
Timestamp
2026-03-23 03:18:30.150656
Timing
Stage Duration
Fetch3800ms
Detect28ms
Parse12ms
Export3ms

Snapshot: 027618_20260323T031833Z.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:34.008857

No units extracted for 027618

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

No units extracted for 027618

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

No units extracted for 027618

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

No units extracted for 027618

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

No units extracted for 027618

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

← Back to dashboard