Facility: 090745

U-Haul Casper WY (S. 82609)

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
090745
Name
U-Haul Casper WY (S. 82609)
URL
https://www.uhaul.com/Locations/Self-Storage-near-Casper-WY-82609/792076/
Address
N/A
Platform
custom_facility_090745
Parser File
src/parsers/custom/facility_090745_parser.py
Last Scraped
2026-03-23 03:21:16.500292
Created
2026-03-06 23:45:35.865957
Updated
2026-03-23 03:21:16.513125
Parser & Healing Diagnosis working
Parser Status
✓ Working
Status Reason
N/A
Last Healing Attempt
Not attempted
Parser Source (src/parsers/custom/facility_090745_parser.py)
"""Parser for U-Haul Storage of Casper, WY facility.

U-Haul location pages render storage units inside ``<ul class="uhjs-unit-list">``
elements, one per size tier (small, medium, large, etc.). Each unit is an
``<li class="divider">`` row containing:

- Size in ``<span class="nowrap">`` inside an ``<h4>`` (e.g. "10' x 10' x 8'")
- Label/tier name in the ``<h4>`` text before the ``<span>`` (e.g. "Medium")
- Monthly price in ``<b class="text-xl">``  (displayed price)
- Scarcity in ``<span class="text-callout text-bold">`` (e.g. "2 Units Left!")
- Amenities as ``<li>`` text items: "Drive Up", "No Climate", "Outside Level", etc.
"""

from __future__ import annotations

import re

from bs4 import BeautifulSoup

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


class Facility090745Parser(BaseParser):
    """Extract storage units from U-Haul Storage of Casper (uhjs-unit-list layout)."""

    platform = "custom_facility_090745"

    # Matches dimensions like "10' x 10' x 8'" or "10' x 20'"
    _SIZE_RE = re.compile(
        r"(\d+(?:\.\d+)?)['\u2019\u2032]?\s*[xX\u00d7]\s*(\d+(?:\.\d+)?)['\u2019\u2032]?",
        re.IGNORECASE,
    )

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

        # All unit rows live inside ul.uhjs-unit-list elements
        unit_lists = soup.select("ul.uhjs-unit-list")

        for ul in unit_lists:
            items = ul.select("li.divider")
            for item in items:
                unit = UnitResult()

                # Size: "10' x 10' x 8'" inside span.nowrap within h4
                size_el = item.select_one("h4 span.nowrap")
                if size_el:
                    raw_size = size_el.get_text(strip=True)
                    unit.size = raw_size
                    m = self._SIZE_RE.search(raw_size)
                    if m:
                        width = float(m.group(1))
                        length = float(m.group(2))
                        unit.metadata = {"width": width, "length": length, "sqft": width * length}

                # Description: tier label from h4 text (e.g. "Medium", "Large")
                h4_el = item.select_one("h4")
                if h4_el:
                    # Remove the size span text to get just the tier label
                    tier_text = h4_el.get_text(separator=" ", strip=True)
                    if size_el:
                        tier_text = tier_text.replace(size_el.get_text(strip=True), "").strip()
                    if tier_text:
                        unit.description = tier_text

                # Price: b.text-xl contains the monthly rate
                price_el = item.select_one("b.text-xl")
                if price_el:
                    unit.price = self.normalize_price(price_el.get_text(strip=True))

                # Scarcity: span.text-callout.text-bold (e.g. "2 Units Left!")
                scarcity_el = item.select_one("span.text-callout.text-bold")
                if scarcity_el:
                    unit.scarcity = scarcity_el.get_text(strip=True)

                # Amenities: collect short bare <li> text items (Drive Up, No Climate, etc.)
                amenities: list[str] = []
                for li in item.find_all("li"):
                    li_text = li.get_text(strip=True)
                    # Exclude tab titles and long benefit descriptions
                    if li_text and len(li_text) < 40 and not li.get("class"):
                        amenities.append(li_text)
                if amenities and unit.metadata is not None:
                    unit.metadata["amenities"] = amenities
                elif amenities:
                    unit.metadata = {"amenities": amenities}

                if unit.size or unit.price:
                    result.units.append(unit)

        if not result.units:
            result.warnings.append("No unit rows found in uhjs-unit-list elements")

        return result

Scrape Runs (5)

Run #1495 Details

Status
exported
Parser Used
Facility090745Parser
Platform Detected
storageunitsoftware
Units Found
1
Stage Reached
exported
Timestamp
2026-03-23 03:21:08.462329
Timing
Stage Duration
Fetch7764ms
Detect159ms
Parse95ms
Export6ms

Snapshot: 090745_20260323T032116Z.html · Show Snapshot · Open in New Tab

Parsed Units (1)

15' x 20' x 8'

$179.95/mo
1 Unit Left!

All Failures for this Facility (1)

parse _WarningAsException scraper no_units_extracted warning Run #N/A | 2026-03-21 19:14:33.933891

No units extracted for 090745

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

← Back to dashboard