Facility: 027618
Antelope Meadows Storage
- 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 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)
-
exported Run #14712026-03-23 03:18:30.150656 | Facility027618Parser
-
exported Run #9782026-03-21 19:11:23.761848 | Facility027618Parser
-
exported Run #5312026-03-14 16:53:59.816159 | Facility027618Parser
-
exported Run #1332026-03-14 01:05:40.989408 | Facility027618Parser
-
exported Run #522026-03-13 21:12:52.753598 | Facility027618Parser
Run #52 Details
- Status
- exported
- Parser Used
- Facility027618Parser
- Platform Detected
- storageunitsoftware
- Units Found
- 0
- Stage Reached
- exported
- Timestamp
- 2026-03-13 21:12:52.753598
Timing
| Stage | Duration |
|---|---|
| Fetch | 2537ms |
| Detect | 15ms |
| Parse | 7ms |
| Export | 11ms |
Snapshot: 027618_20260313T211255Z.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