Facility: 004216

Gypsum Mini 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
004216
Name
Gypsum Mini Storage
URL
https://www.gypsumministorage.com/115-park-street-po-box-878-gypsum-co-81637
Address
115 Park St, Gypsum, CO 81637, USA, Gypsum, Colorado 81637
Platform
custom_facility_004216
Parser File
src/parsers/custom/facility_004216_parser.py
Last Scraped
2026-03-27 13:58:00.288155
Created
2026-03-14 16:21:53.706708
Updated
2026-03-27 13:58:00.320144
Parser & Healing Diagnosis working
Parser Status
✓ Working
Status Reason
N/A
Last Healing Attempt
Not attempted
Parser Source (src/parsers/custom/facility_004216_parser.py)
"""Parser for Gypsum Mini Storage (StorEdge / Storable platform)."""

from __future__ import annotations

import re

from bs4 import BeautifulSoup, Tag

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


class Facility004216Parser(BaseParser):
    """Extract storage units from Gypsum Mini Storage.

    Site uses the Storable/StorEdge rental center with Material UI card
    components.  Each unit is rendered inside a ``MuiCard-root`` div with:

    * An ``<h5>`` (or similar heading) containing the size, e.g. "5' x 5' Unit"
    * A ``MuiTypography-h6`` element with the current/discounted monthly price
    * An optional ``MuiTypography-caption`` span with the original
      (struck-through) price
    * A ``MuiTypography-body1`` paragraph with the unit category
    """

    platform = "custom_facility_004216"

    _SIZE_RE = re.compile(
        r"(\d+)[′'']?\s*[xX×]\s*(\d+)[′'']?"
    )

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

        cards = soup.find_all("div", class_="MuiCard-root")
        seen: set[str] = set()

        for card in cards:
            if not isinstance(card, Tag):
                continue

            # --- size from heading ---
            heading = card.find(["h3", "h4", "h5", "h6"])
            if heading is None:
                continue
            heading_text = heading.get_text(strip=True)
            m = self._SIZE_RE.search(heading_text)
            if not m:
                continue

            size_raw = f"{m.group(1)}' x {m.group(2)}'"
            if size_raw in seen:
                continue
            seen.add(size_raw)

            # --- prices ---
            # Discounted / current price lives in an h6.MuiTypography-h6
            price_el = card.find(
                "h6",
                class_=lambda c: c and "MuiTypography-h6" in c,
            )
            # Fallback: subtitle1 (contains "$X /mo")
            if price_el is None:
                price_el = card.find(
                    "h6",
                    class_=lambda c: c and "MuiTypography-subtitle1" in c,
                )
            price = self.normalize_price(price_el.get_text(strip=True)) if price_el else None

            # Original / strike-through price (caption)
            strike_el = card.find(
                "span",
                class_=lambda c: c and "MuiTypography-caption" in c,
            )
            original_price = (
                self.normalize_price(strike_el.get_text(strip=True))
                if strike_el
                else None
            )

            # --- category ---
            cat_el = card.find(
                "p",
                class_=lambda c: c and "MuiTypography-body1" in c,
            )
            category = cat_el.get_text(strip=True) if cat_el else None

            # --- build unit ---
            unit = UnitResult()
            unit.size = size_raw
            w, ln, sq = self.normalize_size(size_raw)
            metadata: dict[str, object] = {}
            if w is not None:
                metadata.update(width=w, length=ln, sqft=sq)
            if original_price is not None:
                metadata["original_price"] = original_price
            if category:
                metadata["category"] = category
            if metadata:
                unit.metadata = metadata

            unit.price = price
            unit.description = heading_text
            result.units.append(unit)

        if not result.units:
            result.warnings.append("No MuiCard-root unit cards found")

        return result

Scrape Runs (5)

Run #756 Details

Status
exported
Parser Used
Facility004216Parser
Platform Detected
storageunitsoftware
Units Found
4
Stage Reached
exported
Timestamp
2026-03-21 18:51:06.762013
Timing
Stage Duration
Fetch4755ms
Detect79ms
Parse24ms
Export4ms

Snapshot: 004216_20260321T185111Z.html · Show Snapshot · Open in New Tab

Parsed Units (4)

5' x 5'

$60.00/mo

5' x 10'

$115.00/mo

10' x 10'

$165.00/mo

10' x 20'

$275.00/mo

← Back to dashboard