Facility: 084967
Morningstar Storage
- Facility ID
- 084967
- Name
- Morningstar Storage
- URL
- https://www.morningstarstorage.com/location/377/tx/houston/the-heights/
- Address
- 4495 Katy Fwy, Houston, TX 77007
- Platform
- custom_facility_084967
- Parser File
- src/parsers/custom/facility_084967_parser.py
- Last Scraped
- 2026-03-27 13:42:03.767733
- Created
- 2026-03-23 02:35:08.816820
- Updated
- 2026-03-27 13:42:03.767733
- Parser Status
- ⚠ Needs Fix
- Status Reason
- Parser returned 0 units
- Last Healing Attempt
- Not attempted
Parser Source (src/parsers/custom/facility_084967_parser.py)
"""Parser for Morningstar Storage facility 084967 (CANDEE platform)."""
from __future__ import annotations
import json
import re
from bs4 import BeautifulSoup
from src.parsers.base import BaseParser, ParseResult, UnitResult
class Facility084967Parser(BaseParser):
"""Extract storage units from Morningstar Storage (CANDEE reservation system).
The CANDEE platform renders unit data in div.unitMasterData elements with
data-price (monthly rate) and a nested div.unitName containing the size
string (e.g. "5x5 Heated & Cooled Ground"). data-size holds the sq ft.
"""
platform = "custom_facility_084967"
_DIM_RE = re.compile(r"(\d+\.?\d*)\s*[xX×]\s*(\d+\.?\d*)")
def parse(self, html: str, url: str = "") -> ParseResult:
soup = BeautifulSoup(html, "lxml")
result = ParseResult(platform=self.platform, parser_name=self.__class__.__name__)
seen: set[tuple[str, str]] = set()
for unit_el in soup.find_all(class_="unitMasterData"):
price_raw = unit_el.get("data-price", "").strip()
price = self.normalize_price(price_raw)
if price is None:
continue
# unitName contains the display name e.g. "5x5 Heated & Cooled Ground"
name_el = unit_el.find(class_="unitName")
if not name_el:
continue
name_text = name_el.get_text(strip=True)
# Extract dimension from the name text
dim_m = self._DIM_RE.search(name_text)
if dim_m:
size_text = dim_m.group(0)
else:
# Fall back to sq ft if no WxL found
sqft_raw = unit_el.get("data-size", "")
size_text = f"{sqft_raw} sqft" if sqft_raw else name_text
key = (size_text, price_raw)
if key in seen:
continue
seen.add(key)
# Features from data-features JSON array
features_raw = unit_el.get("data-features", "")
desc: str | None = None
if features_raw:
try:
features_list = json.loads(features_raw)
# Deduplicate while preserving order
seen_feats: list[str] = []
for f in features_list:
if f not in seen_feats:
seen_feats.append(f)
desc = ", ".join(seen_feats)
except (json.JSONDecodeError, TypeError):
pass
unit = UnitResult(
size=size_text,
price=price,
description=desc,
)
w, ln, sq = self.normalize_size(size_text)
if w is not None:
unit.metadata = {"width": w, "length": ln, "sqft": sq}
elif unit_el.get("data-size"):
try:
unit.metadata = {"sqft": float(unit_el["data-size"])}
except ValueError:
pass
result.units.append(unit)
if not result.units:
result.warnings.append("No units found in CANDEE unitMasterData elements")
return result
Scrape Runs (3)
Run #1587 Details
- Status
- exported
- Parser Used
- Facility084967Parser
- Platform Detected
- table_layout
- Units Found
- 0
- Stage Reached
- exported
- Timestamp
- 2026-03-27 13:41:57.158544
Timing
| Stage | Duration |
|---|---|
| Fetch | 6473ms |
| Detect | 45ms |
| Parse | 19ms |
| Export | 13ms |
Snapshot: 084967_20260327T134203Z.html · Show Snapshot · Open in New Tab
No units found in this run.
All Failures for this Facility (2)
parse
_WarningAsException
scraper
no_units_extracted
warning
Run #N/A | 2026-03-27 13:42:03.750756
No units extracted for 084967
Stack trace
src.reporting.failure_reporter._WarningAsException: No units extracted for 084967
parse
_WarningAsException
scraper
no_units_extracted
warning
Run #N/A | 2026-03-27 13:42:01.683202
No units extracted for 084967
Stack trace
src.reporting.failure_reporter._WarningAsException: No units extracted for 084967