Facility: 111169

Compass Self Storage

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
111169
Name
Compass Self Storage
URL
https://www.compassselfstorage.com/self-storage/tx/spring/aldine-westfield/
Address
25528 Aldine Westfield Rd, Spring, TX 77373, USA, Spring, Texas 77373
Platform
custom_facility_111169
Parser File
src/parsers/custom/facility_111169_parser.py
Last Scraped
2026-03-27 13:45:01.438229
Created
2026-03-20 23:32:48.933261
Updated
2026-03-27 13:45:01.480995
Parser & Healing Diagnosis working
Parser Status
✓ Working
Status Reason
N/A
Last Healing Attempt
Not attempted
Parser Source (src/parsers/custom/facility_111169_parser.py)
"""Parser for Compass Self Storage — Spring, TX (Aldine-Westfield).

The page uses the StorEdge "vapor-unit-table" WordPress plugin which loads
unit data via a client-side API call. The server-rendered HTML contains only
loading skeletons, but the page embeds a JSON-LD ``@graph`` block with a
``SelfStorage`` entity whose ``hasOfferCatalog`` lists all unit groups as
``Product`` objects.

Products with ``price: 0`` are filtered out (these represent unit types where
pricing is not published). The ``name`` field encodes dimensions and category
(e.g. "10x20 - Large").
"""

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 Facility111169Parser(BaseParser):
    """Extract storage units from Compass Self Storage Spring TX (JSON-LD)."""

    platform = "custom_facility_111169"

    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")

            # Skip products with no price or zero price
            if raw_price is None or raw_price == 0:
                continue

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

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

            # Parse size from name like "10x20 - Large"
            m = _SIZE_RE.search(name)
            if not m:
                continue

            width = float(m.group(1))
            length = float(m.group(2))
            size = f"{m.group(1)}x{m.group(2)}"

            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 not is_available:
                metadata["available"] = False

            unit = UnitResult(
                size=size,
                price=price,
                description=name,
                url=url,
                metadata=metadata,
            )
            result.units.append(unit)

        if not result.units:
            result.warnings.append("No units with valid prices 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

            # 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

            # 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

        return []

Scrape Runs (4)

Run #1665 Details

Status
exported
Parser Used
Facility111169Parser
Platform Detected
table_layout
Units Found
34
Stage Reached
exported
Timestamp
2026-03-27 13:44:53.974803
Timing
Stage Duration
Fetch7202ms
Detect142ms
Parse56ms
Export26ms

Snapshot: 111169_20260327T134501Z.html · Show Snapshot · Open in New Tab

Parsed Units (34)

5x7.5

$25.00/mo

5x10

$15.00/mo

5x10

$30.00/mo

5x10

$16.00/mo

5x10

$32.00/mo

5x10

$39.00/mo

7.5x10

$54.00/mo

10x10

$20.00/mo

10x10

$21.00/mo

10x10

$22.00/mo

10x10

$30.00/mo

10x10

$31.00/mo

10x10

$33.00/mo

10x10

$69.00/mo

12.5x10

$94.00/mo

10x15

$40.00/mo

10x15

$41.00/mo

10x15

$44.00/mo

10x15

$189.00/mo

10x20

$45.00/mo

10x20

$47.00/mo

10x20

$49.00/mo

10x20

$52.00/mo

10x20

$55.00/mo

10x20

$57.00/mo

10x20

$60.00/mo

10x30

$209.00/mo

10x30

$239.00/mo

10x40

$129.00/mo

13x45

$699.00/mo

7.5x5

$25.00/mo

12.5x10

$104.00/mo

10x25

$189.00/mo

10x25

$229.00/mo

← Back to dashboard