Facility: 038686

Stor-It Caldwell

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
038686
Name
Stor-It Caldwell
URL
https://www.stor-it.com/location/USA/ID/Caldwell/stor-it-caldwell/
Address
N/A
Platform
custom_facility_038686
Parser File
src/parsers/custom/facility_038686_parser.py
Last Scraped
2026-03-23 03:21:58.159578
Created
2026-03-06 23:45:35.865957
Updated
2026-03-23 03:21:58.159578
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_038686_parser.py)
"""Parser for Stor-It Self Storage - Caldwell (facility 038686).

This facility uses the Candee platform (WordPress plugin). Unit data is loaded
via an AJAX call and embedded in ``data-unit-info`` JSON attributes on
``.unitsList.lineItem.unitMasterData`` elements. The snapshot stored on disk
is the AJAX response HTML fragment (not the full page).
"""

from __future__ import annotations

import json
import logging

from bs4 import BeautifulSoup

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

logger = logging.getLogger(__name__)


class Facility038686Parser(BaseParser):
    """Extract storage units from Stor-It Self Storage - Caldwell (Candee platform).

    Each unit is represented by a ``<div class="unitsList lineItem unitMasterData">``
    element whose ``data-unit-info`` attribute contains a JSON object with all
    relevant fields: ``rental_name``, ``price``, ``strike_through``, ``width``,
    ``length``, ``unit_type``, ``inside``, ``available``, ``total_available``,
    ``discount_name``, and ``discounts``.
    """

    platform = "custom_facility_038686"

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

        unit_items = soup.select(".unitsList.lineItem.unitMasterData")
        if not unit_items:
            result.warnings.append("No Candee unit items found (expected .unitsList.lineItem.unitMasterData)")
            return result

        for item in unit_items:
            unit_info_raw = item.get("data-unit-info", "")
            if not unit_info_raw:
                continue

            try:
                info = json.loads(unit_info_raw)
            except json.JSONDecodeError as exc:
                logger.debug("Failed to parse data-unit-info: %s", exc)
                result.warnings.append(f"Could not parse data-unit-info JSON: {exc}")
                continue

            rental_name = info.get("rental_name", "")
            unit_type = info.get("unit_type", "")
            width = info.get("width")
            length = info.get("length")
            price_raw = info.get("price")
            strike_through_raw = info.get("strike_through")
            discount_name = info.get("discount_name")
            discounts = info.get("discounts", [])
            total_available = info.get("total_available")
            inside = info.get("inside")

            # Build size string
            if width and length:
                size = f"{int(width)}' x {int(length)}'"
                sqft = float(width) * float(length)
            else:
                # Fall back to parsing the rental_name
                w, l, sqft = self.normalize_size(rental_name)
                if w and l:
                    size = f"{int(w)}' x {int(l)}'"
                    width, length = w, l
                else:
                    size = rental_name
                    sqft = None

            # Prices
            price: float | None = None
            sale_price: float | None = None

            if price_raw is not None:
                try:
                    price_val = float(str(price_raw).replace(",", ""))
                except (ValueError, TypeError):
                    price_val = None
            else:
                price_val = None

            # strike_through holds the original (higher) price when a discount applies
            if strike_through_raw and str(strike_through_raw) not in ("0", ""):
                try:
                    strike_val = float(str(strike_through_raw).replace(",", ""))
                except (ValueError, TypeError):
                    strike_val = None
                if strike_val:
                    price = strike_val
                    sale_price = price_val
                else:
                    price = price_val
            else:
                price = price_val

            # Promotion text from first discount
            promotion: str | None = None
            if discounts:
                first_discount = discounts[0]
                promo_name = first_discount.get("name") or discount_name
                if promo_name:
                    promotion = promo_name
            elif discount_name:
                promotion = discount_name

            # Scarcity
            scarcity: str | None = None
            if total_available is not None:
                try:
                    avail_count = int(total_available)
                    if 0 < avail_count <= 3:
                        scarcity = f"Only {avail_count} left"
                except (ValueError, TypeError):
                    pass

            # Description: use rental_name for full context including unit type
            description = rental_name if rental_name else unit_type

            # Amenity metadata
            is_climate = "climate" in unit_type.lower() or "climate" in rental_name.lower()
            is_parking = "parking" in unit_type.lower()
            is_interior = bool(inside) or "interior" in unit_type.lower()

            metadata: dict = {
                "unit_type": unit_type,
                "facility_id": info.get("rental_id"),
                "prop_id": item.get("data-facility"),
                "is_parking": is_parking,
                "is_climate_controlled": is_climate,
                "is_interior": is_interior,
            }
            if width and length:
                metadata["width"] = width
                metadata["length"] = length
            if sqft is not None:
                metadata["sqft"] = sqft

            result.units.append(
                UnitResult(
                    size=size,
                    description=description,
                    price=price,
                    sale_price=sale_price,
                    promotion=promotion,
                    scarcity=scarcity,
                    url=url or None,
                    metadata=metadata,
                )
            )

        if not result.units:
            result.warnings.append("No units could be extracted from Candee unit items")

        return result

Scrape Runs (5)

Run #563 Details

Status
exported
Parser Used
Facility038686Parser
Platform Detected
table_layout
Units Found
0
Stage Reached
exported
Timestamp
2026-03-14 16:56:44.598796
Timing
Stage Duration
Fetch7466ms
Detect251ms
Parse123ms
Export12ms

Snapshot: 038686_20260314T165652Z.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:21:58.148948

No units extracted for 038686

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

No units extracted for 038686

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

No units extracted for 038686

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

No units extracted for 038686

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

No units extracted for 038686

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

← Back to dashboard