Facility: 038686
Stor-It Caldwell
- 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 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)
-
exported Run #15032026-03-23 03:21:56.262213 | Facility038686Parser
-
exported Run #10102026-03-21 19:15:20.648283 | Facility038686Parser
-
exported Run #5632026-03-14 16:56:44.598796 | Facility038686Parser
-
exported Run #1752026-03-14 05:03:46.648119 | Facility038686Parser
-
exported Run #952026-03-14 01:02:37.782077 | Facility038686Parser
Run #1010 Details
- Status
- exported
- Parser Used
- Facility038686Parser
- Platform Detected
- unknown
- Units Found
- 0
- Stage Reached
- exported
- Timestamp
- 2026-03-21 19:15:20.648283
Timing
| Stage | Duration |
|---|---|
| Fetch | 1940ms |
| Detect | 2ms |
| Parse | 2ms |
| Export | 3ms |
Snapshot: 038686_20260321T191522Z.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