Facility: 111290

STORAGExperts at Chino Valley

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
111290
Name
STORAGExperts at Chino Valley
URL
https://www.storagexperts.net/locations/chino-valley-az-86323?reset
Address
1272 Az-89, Chino Valley, AZ 86323, USA, Chino Valley, Arizona 86323
Platform
custom_facility_111290
Parser File
src/parsers/custom/facility_111290_parser.py
Last Scraped
2026-03-27 13:45:17.636139
Created
2026-03-20 23:32:48.933261
Updated
2026-03-27 13:45:17.663111
Parser & Healing Diagnosis working
Parser Status
✓ Working
Status Reason
N/A
Last Healing Attempt
Not attempted
Parser Source (src/parsers/custom/facility_111290_parser.py)
"""Parser for STORAGExperts — Chino Valley, AZ.

The page uses the GoLocal platform with unit data embedded in a JSON-LD
``SelfStorage`` block (top-level, not in @graph). The ``hasOfferCatalog``
contains ``Product`` items with name (e.g. "5x5 - Small"), width/depth
dimensions, description (e.g. "Drive Up"), and offers with price and
availability.
"""

from __future__ import annotations

import json
import re

from bs4 import BeautifulSoup

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

_SIZE_RE = re.compile(r"(\d+(?:\.\d+)?)\s*[xX]\s*(\d+(?:\.\d+)?)")


class Facility111290Parser(BaseParser):
    """Extract storage units from STORAGExperts Chino Valley (JSON-LD)."""

    platform = "custom_facility_111290"

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

        products = self._find_products(soup)
        if not products:
            result.warnings.append("No SelfStorage hasOfferCatalog found in JSON-LD")
            return result

        seen: set[tuple[str, float | None]] = set()
        for product in products:
            offers = product.get("offers", {})
            raw_price = offers.get("price")

            price: float | None = None
            if raw_price is not None:
                try:
                    price = float(raw_price)
                except (TypeError, ValueError):
                    price = self.normalize_price(str(raw_price))

            name = product.get("name", "")
            description = product.get("description", "")
            availability = offers.get("availability", "")

            # Parse size from name like "5x10 - Small" or from width/depth fields
            width_str = product.get("width", "")
            depth_str = product.get("depth", "")

            width: float | None = None
            length: float | None = None

            # Try width/depth fields first (e.g. "5ft" -> 5)
            if width_str and depth_str:
                w_match = re.search(r"(\d+(?:\.\d+)?)", str(width_str))
                d_match = re.search(r"(\d+(?:\.\d+)?)", str(depth_str))
                if w_match and d_match:
                    width = float(w_match.group(1))
                    length = float(d_match.group(1))

            # Fallback to name pattern
            if width is None or length is None:
                m = _SIZE_RE.search(name)
                if m:
                    width = float(m.group(1))
                    length = float(m.group(2))

            if width is None or length is None:
                continue

            size = f"{width:g}x{length:g}"

            key = (size, price)
            if key in seen:
                continue
            seen.add(key)

            # Determine category from name suffix
            category = ""
            if " - " in name:
                category = name.split(" - ", 1)[1].strip()

            is_available = "OutOfStock" not in availability

            metadata: dict = {
                "width": width,
                "length": length,
                "sqft": width * length,
            }
            if category:
                metadata["category"] = category
            if description:
                metadata["unit_type"] = description
            if not is_available:
                metadata["available"] = False

            unit = UnitResult(
                size=size,
                price=price,
                description=f"{name} ({description})" if description else name,
                url=url,
                metadata=metadata,
            )
            result.units.append(unit)

        if not result.units:
            result.warnings.append("No valid units found in JSON-LD")

        return result

    @staticmethod
    def _find_products(soup: BeautifulSoup) -> list[dict]:
        """Locate Product items from SelfStorage JSON-LD."""
        for script in soup.find_all("script", type="application/ld+json"):
            raw = script.string or ""
            if not raw.strip():
                continue
            try:
                data = json.loads(raw)
            except (json.JSONDecodeError, ValueError):
                continue

            # Direct SelfStorage at top level
            if isinstance(data, dict):
                item_type = data.get("@type", "")
                if isinstance(item_type, list):
                    item_type = " ".join(item_type)
                if "SelfStorage" in str(item_type):
                    catalog = data.get("hasOfferCatalog", {})
                    products = catalog.get("itemListElement", [])
                    if products:
                        return products

            # Check @graph for SelfStorage entity
            if isinstance(data, dict) and "@graph" in data:
                for item in data["@graph"]:
                    if not isinstance(item, dict):
                        continue
                    item_type = item.get("@type", "")
                    if isinstance(item_type, list):
                        item_type = " ".join(item_type)
                    if "SelfStorage" in str(item_type):
                        catalog = item.get("hasOfferCatalog", {})
                        products = catalog.get("itemListElement", [])
                        if products:
                            return products

        return []

Scrape Runs (4)

Run #1672 Details

Status
exported
Parser Used
Facility111290Parser
Platform Detected
table_layout
Units Found
6
Stage Reached
exported
Timestamp
2026-03-27 13:45:12.701653
Timing
Stage Duration
Fetch4651ms
Detect84ms
Parse30ms
Export19ms

Snapshot: 111290_20260327T134517Z.html · Show Snapshot · Open in New Tab

Parsed Units (6)

5x5

$64.00/mo

5x10

$64.00/mo

10x10

$92.00/mo

10x15

$114.00/mo

10x20

$139.00/mo

10x30

$284.00/mo

← Back to dashboard