Facility: 076452
Morningstar Storage
- Facility ID
- 076452
- Name
- Morningstar Storage
- URL
- https://www.morningstarstorage.com/location/379/tx/houston/eado/
- Address
- 812 Live Oak St, Houston, TX 77003
- Platform
- custom_facility_076452
- Parser File
- src/parsers/custom/facility_076452_parser.py
- Last Scraped
- 2026-03-27 13:41:54.016221
- Created
- 2026-03-23 02:35:08.816820
- Updated
- 2026-03-27 13:41:54.049360
- Parser Status
- ✓ Working
- Status Reason
- N/A
- Last Healing Attempt
- Not attempted
Parser Source (src/parsers/custom/facility_076452_parser.py)
"""Parser for Morningstar Storage facility 076452 (CANDEE platform)."""
from __future__ import annotations
import json
import re
from bs4 import BeautifulSoup
from src.parsers.base import BaseParser, ParseResult, UnitResult
class Facility076452Parser(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_076452"
_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 #1582 Details
- Status
- exported
- Parser Used
- Facility076452Parser
- Platform Detected
- table_layout
- Units Found
- 25
- Stage Reached
- exported
- Timestamp
- 2026-03-27 13:41:45.665750
Timing
| Stage | Duration |
|---|---|
| Fetch | 8128ms |
| Detect | 108ms |
| Parse | 51ms |
| Export | 24ms |
Snapshot: 076452_20260327T134153Z.html · Show Snapshot · Open in New Tab
Parsed Units (25)
5x5
$63.00/mo
5x5
$37.00/mo
5x5
$18.00/mo
8.5x5
$23.00/mo
5x10
$41.00/mo
5x10
$74.00/mo
7x10
$59.00/mo
10x10
$67.00/mo
10x10
$112.00/mo
10x15
$112.00/mo
10x15
$149.00/mo
10x17
$119.00/mo
17x10
$119.00/mo
10x20
$142.00/mo
10x20
$172.00/mo
20x10
$134.00/mo
25x10
$187.00/mo
10x30
$247.00/mo
10x30
$269.00/mo
15x25
$277.00/mo
13x28
$194.00/mo
30x15
$337.00/mo
10x40
$299.00/mo
20x20
$299.00/mo
20x15
$248.00/mo
All Failures for this Facility (1)
parse
_WarningAsException
scraper
no_units_extracted
warning
Run #N/A | 2026-03-27 13:41:47.404337
No units extracted for 076452
Stack trace
src.reporting.failure_reporter._WarningAsException: No units extracted for 076452