Facility: 034190
Storage Star - Sheridan
- Facility ID
- 034190
- Name
- Storage Star - Sheridan
- URL
- https://www.storagestar.com/storage-units/wyoming/sheridan/coffeen-avenue/
- Address
- N/A
- Platform
- custom_facility_034190
- Parser File
- src/parsers/custom/facility_034190_parser.py
- Last Scraped
- 2026-03-23 03:20:22.460234
- Created
- 2026-03-06 23:45:35.865957
- Updated
- 2026-03-23 03:20:22.468716
- Parser Status
- ✓ Working
- Status Reason
- N/A
- Last Healing Attempt
- Not attempted
Parser Source (src/parsers/custom/facility_034190_parser.py)
"""Parser for StorageStar Coffeen Avenue (Sheridan, WY) facility.
This site uses the Storagely platform. Unit rows are <tr> elements with
class names matching the pattern ``unit_NNNNN``. Pricing, dimensions, and
features are available in both structured HTML elements and a hidden
``<p class="tag_input">`` element with data attributes.
"""
from __future__ import annotations
import re
from bs4 import BeautifulSoup, Tag
from src.parsers.base import BaseParser, ParseResult, UnitResult
# Matches unit row class names like "unit_28978"
_UNIT_CLASS_RE = re.compile(r"^unit_\d+$")
# Matches dimension text like "5' WIDTH x 10' DEPTH" or "10' x 20'"
_DIM_RE = re.compile(
r"(\d+(?:\.\d+)?)\s*['\u2032]?\s*(?:WIDTH\s+)?[xX\u00d7]\s*(\d+(?:\.\d+)?)\s*['\u2032]?",
re.IGNORECASE,
)
def _extract_price(container: Tag | None) -> float | None:
"""Return the first numeric price found in a container element."""
if container is None:
return None
text = container.get_text(separator=" ", strip=True)
# Match patterns like "$60" or "$60.00" (ignoring "/month")
match = re.search(r"\$([\d,]+(?:\.\d+)?)", text)
if match:
return float(match.group(1).replace(",", ""))
return None
class Facility034190Parser(BaseParser):
"""Extract storage units from StorageStar Coffeen Ave (Sheridan, WY).
The page is rendered by the Storagely platform. Each unit is a ``<tr>``
element whose ``class`` list includes an ID-specific token like
``unit_29064``. Key data:
- Dimensions: ``<h2 class="widthHeight">`` — e.g. "5' WIDTH x 10' DEPTH"
- Unit type: ``<div class="unit-type-listing-name">``
- Web/sale price: ``<h3 class="actualMoPrice">``
- Standard price: ``<h3 class="withoutDiscntprice">``
- Promotion: ``<span class="offer__content">``
- Feature flags: ``<p class="tag_input" utype="..." climate="...">``
- Availability: ``data-listing_status`` attribute on the row
"""
platform = "custom_facility_034190"
def parse(self, html: str, url: str = "") -> ParseResult:
soup = BeautifulSoup(html, "lxml")
result = ParseResult(platform=self.platform, parser_name=self.__class__.__name__)
# Find all unit rows
unit_rows = soup.find_all(
lambda tag: any(_UNIT_CLASS_RE.match(c) for c in tag.get("class", []))
)
if not unit_rows:
result.warnings.append("No unit rows found matching pattern 'unit_NNNNN'")
return result
for row in unit_rows:
unit = self._parse_row(row, url)
if unit is not None:
result.units.append(unit)
if not result.units:
result.warnings.append(f"Found {len(unit_rows)} unit rows but extracted 0 units")
return result
def _parse_row(self, row: Tag, url: str) -> UnitResult | None:
"""Parse a single unit row into a UnitResult."""
# --- Availability ---
status = row.get("data-listing_status", "").strip().lower()
if status == "occupied":
return None
# --- Dimensions ---
h2 = row.find("h2", class_="widthHeight")
size_text = h2.get_text(separator=" ", strip=True) if h2 else ""
dim_match = _DIM_RE.search(size_text)
if dim_match:
width = float(dim_match.group(1))
length = float(dim_match.group(2))
size = f"{int(width)}' x {int(length)}'"
sqft = width * length
else:
size = size_text or None
width = length = sqft = None
# --- Unit type / description ---
unit_type_el = row.find(class_="unit-type-listing-name")
unit_type_raw = unit_type_el.get_text(separator=" ", strip=True) if unit_type_el else ""
# The element often has trailing unit name/number embedded — strip trailing digits
unit_type = re.sub(r"\s*\w*\d+\w*\s*$", "", unit_type_raw).strip()
# --- Prices ---
# actualMoPrice = web/sale rate
sale_price = _extract_price(row.find(class_="actualMoPrice"))
# withoutDiscntprice = standard rate (shown with strikethrough)
std_price = _extract_price(row.find(class_="withoutDiscntprice"))
# Assign price fields: price = standard rate, sale_price = web rate
price = std_price
if sale_price is not None and std_price is not None and sale_price < std_price:
# There is a genuine discount
pass # keep both as-is
elif sale_price is not None and std_price is None:
# Only one price; treat as the regular price
price = sale_price
sale_price = None
# --- Promotion ---
promo_el = row.find(class_="offer__content")
promotion = promo_el.get_text(strip=True) if promo_el else None
# --- Feature metadata from hidden tag_input element ---
tag_input = row.find("p", class_="tag_input")
utype = ""
climate = ""
if tag_input:
utype = tag_input.get("utype", "").strip()
climate = tag_input.get("climate", "").strip().rstrip(",")
is_climate_controlled = (
"climate" in utype.lower()
or "climate" in climate.lower()
or "heated" in climate.lower()
)
is_drive_up = "driveup" in utype.lower().replace("-", "").replace(" ", "")
is_parking = "parking" in utype.lower()
metadata: dict = {}
if width is not None:
metadata["width"] = width
if length is not None:
metadata["length"] = length
if sqft is not None:
metadata["sqft"] = sqft
if utype:
metadata["unit_type"] = utype
if climate:
metadata["climate"] = climate
if is_climate_controlled:
metadata["climate_controlled"] = True
if is_drive_up:
metadata["drive_up"] = True
if is_parking:
metadata["parking"] = True
description_parts = [p for p in [unit_type, climate] if p]
description = " | ".join(description_parts) if description_parts else unit_type or None
return UnitResult(
size=size,
description=description,
price=price,
sale_price=sale_price,
promotion=promotion,
url=url or None,
metadata=metadata if metadata else None,
)
Scrape Runs (5)
-
exported Run #14882026-03-23 03:20:06.982687 | 10 units | Facility034190Parser | View Data →
-
exported Run #9952026-03-21 19:13:17.782625 | 10 units | Facility034190Parser | View Data →
-
exported Run #5482026-03-14 16:55:13.452900 | Facility034190Parser
-
exported Run #1522026-03-14 04:58:59.369969 | 12 units | Facility034190Parser | View Data →
-
exported Run #752026-03-14 01:00:34.441518 | Facility034190Parser
Run #1488 Details
- Status
- exported
- Parser Used
- Facility034190Parser
- Platform Detected
- table_layout
- Units Found
- 10
- Stage Reached
- exported
- Timestamp
- 2026-03-23 03:20:06.982687
Timing
| Stage | Duration |
|---|---|
| Fetch | 15023ms |
| Detect | 283ms |
| Parse | 148ms |
| Export | 7ms |
Snapshot: 034190_20260323T032022Z.html · Show Snapshot · Open in New Tab
Parsed Units (10)
5' x 10'
$57.00/mo
Street: $95.00
5' x 10'
$82.00/mo
Street: $137.00
10' x 10'
$91.00/mo
Street: $152.00
10' x 15'
$96.00/mo
Street: $160.00
10' x 15'
$149.00/mo
Street: $248.00
10' x 20'
$98.00/mo
Street: $163.00
10' x 20'
$102.00/mo
Street: $170.00
10' x 30'
$64.00/mo
Street: $107.00
10' x 30'
$165.00/mo
Street: $275.00
10' x 32'
$220.00/mo
Street: $367.00
All Failures for this Facility (2)
parse
_WarningAsException
scraper
no_units_extracted
warning
Run #N/A | 2026-03-14 16:55:16.565913
No units extracted for 034190
Stack trace
src.reporting.failure_reporter._WarningAsException: No units extracted for 034190
parse
_WarningAsException
scraper
no_units_extracted
warning
Run #N/A | 2026-03-14 01:00:37.947804
No units extracted for 034190
Stack trace
src.reporting.failure_reporter._WarningAsException: No units extracted for 034190