Facility: 101861
Mod Storage
- Facility ID
- 101861
- Name
- Mod Storage
- URL
- https://www.modstorage.com/storage-units/wyoming/laramie/skyline-road
- Address
- N/A
- Platform
- custom_facility_101861
- Parser File
- src/parsers/custom/facility_101861_parser.py
- Last Scraped
- 2026-03-23 03:20:46.299171
- Created
- 2026-03-06 23:45:35.865957
- Updated
- 2026-03-23 03:20:46.308768
- Parser Status
- ✓ Working
- Status Reason
- N/A
- Last Healing Attempt
- Not attempted
Parser Source (src/parsers/custom/facility_101861_parser.py)
"""Parser for modSTORAGE — Laramie, WY (facility 101861).
This is a React/Next.js SPA site (modstorage.com) that renders unit cards as
mobile-optimised card elements. Each unit is a ``div.flex.md:hidden.flex-col``
card containing size, amenity type, promotional pricing ($0 first month), the
ongoing monthly price, the regular (crossed-out) price, and optional scarcity.
"""
from __future__ import annotations
import re
from bs4 import BeautifulSoup, Tag
from src.parsers.base import BaseParser, ParseResult, UnitResult
class Facility101861Parser(BaseParser):
"""Extract storage units from modSTORAGE Laramie WY.
Page structure (per unit card):
<div class="flex md:hidden flex-col p-4 gap-3">
<!-- size -->
<span class="text-xl font-bold">5' x 5'</span>
<!-- amenity/type -->
<span class="text-xs text-muted-foreground">Drive Up</span>
<!-- optional scarcity badge, e.g. "2 left" or "Only 2 left" -->
<!-- promotional label, e.g. "Totally FREE Move In" -->
<p class="text-[11px] text-primary font-medium ...">Totally FREE Move In</p>
<!-- first-month price (often $0.00) -->
<span class="text-2xl font-bold text-primary">$0.00</span>
<!-- "then $XX.XX/mo" ongoing price -->
<span class="text-sm text-muted-foreground">then $19.99/mo</span>
<!-- regular/strikethrough price -->
<span class="text-sm line-through ...">$34.99</span>
</div>
Pricing mapping:
``price`` = regular (strikethrough) price — the non-promotional rate
``sale_price`` = ongoing monthly rate after promotion (``then $XX/mo``)
``promotion`` = first-month promo text (e.g. "Totally FREE Move In – $0.00 1st mo")
"""
platform = "custom_facility_101861"
# Matches "then $XX.XX/mo" in muted spans
_THEN_PRICE_RE = re.compile(r"then\s+\$([\d,]+(?:\.\d+)?)\s*/\s*mo", re.IGNORECASE)
# Matches plain scarcity text such as "2 left" or "Only 2 left"
_SCARCITY_RE = re.compile(r"(?:only\s+)?(\d+)\s+left", re.IGNORECASE)
def parse(self, html: str, url: str = "") -> ParseResult:
soup = BeautifulSoup(html, "lxml")
result = ParseResult(platform=self.platform, parser_name=self.__class__.__name__)
# Target the mobile unit cards rendered by the React SPA.
# The desktop variant (div.md:flex.hidden) duplicates the same data, so
# we parse only the mobile cards to avoid double-counting.
cards = soup.find_all(
"div",
class_=lambda c: c and "md:hidden" in c and "flex-col" in c and "p-4" in c,
)
if not cards:
result.warnings.append("No unit cards found — page structure may have changed")
return result
for card in cards:
unit = self._parse_card(card, url)
if unit is not None:
result.units.append(unit)
if not result.units:
result.warnings.append(f"Cards found ({len(cards)}) but no units extracted")
return result
def _parse_card(self, card: Tag, url: str) -> UnitResult | None:
"""Extract a single UnitResult from a unit card element."""
# --- Size ---
size_span = card.find("span", class_=lambda c: c and "text-xl" in c and "font-bold" in c)
if not size_span:
return None
raw_size = size_span.get_text(strip=True)
width, length, sqft = self.normalize_size(raw_size)
if width is None:
return None
size = f"{int(width)}' x {int(length)}'"
# --- Amenity / unit type ---
amenity_span = card.find(
"span",
class_=lambda c: c and "text-xs" in c and "text-muted-foreground" in c,
)
amenity = amenity_span.get_text(strip=True) if amenity_span else None
# --- Scarcity ---
# The scarcity text may appear in various inline spans; scan all text.
card_text = card.get_text(separator=" ", strip=True)
scarcity_match = self._SCARCITY_RE.search(card_text)
scarcity = scarcity_match.group(0).strip() if scarcity_match else None
# --- First-month promotional price (e.g. "$0.00 1st mo") ---
promo_label_el = card.find("p", class_=lambda c: c and "text-primary" in c and "font-medium" in c)
promo_label = promo_label_el.get_text(strip=True) if promo_label_el else None
first_price_span = card.find(
"span",
class_=lambda c: c and "text-2xl" in c and "font-bold" in c and "text-primary" in c,
)
first_price_text = first_price_span.get_text(strip=True) if first_price_span else None
first_price = self.normalize_price(first_price_text) if first_price_text else None
# Build promotion string
promotion: str | None = None
if promo_label and first_price is not None:
promotion = f"{promo_label} – {first_price_text} 1st mo"
elif promo_label:
promotion = promo_label
# --- Ongoing monthly price (sale_price) ---
# Appears in a "text-sm text-muted-foreground" span containing "then $..."
then_spans = card.find_all(
"span",
class_=lambda c: c and "text-sm" in c and "text-muted-foreground" in c,
)
sale_price: float | None = None
for span in then_spans:
m = self._THEN_PRICE_RE.search(span.get_text(strip=True))
if m:
sale_price = self.normalize_price(m.group(1))
break
# If no "then" span exists the first_price IS the ongoing price
if sale_price is None and first_price is not None and first_price > 0:
sale_price = first_price
promotion = None # no special promo in this case
# --- Regular / strikethrough price ---
strike_span = card.find("span", class_=lambda c: c and "line-through" in c)
regular_price: float | None = None
if strike_span:
regular_price = self.normalize_price(strike_span.get_text(strip=True))
return UnitResult(
size=size,
description=amenity,
price=regular_price,
sale_price=sale_price,
promotion=promotion,
scarcity=scarcity,
url=url,
metadata={
"width": width,
"length": length,
"sqft": sqft,
"amenity": amenity,
},
)
Scrape Runs (5)
-
exported Run #14912026-03-23 03:20:39.870409 | 18 units | Facility101861Parser | View Data →
-
exported Run #9982026-03-21 19:13:54.829593 | 18 units | Facility101861Parser | View Data →
-
exported Run #5512026-03-14 16:55:32.863143 | 19 units | Facility101861Parser | View Data →
-
exported Run #1552026-03-14 04:59:29.133024 | 19 units | Facility101861Parser | View Data →
-
exported Run #782026-03-14 01:01:04.337806 | 19 units | Facility101861Parser | View Data →
Run #551 Details
- Status
- exported
- Parser Used
- Facility101861Parser
- Platform Detected
- table_layout
- Units Found
- 19
- Stage Reached
- exported
- Timestamp
- 2026-03-14 16:55:32.863143
Timing
| Stage | Duration |
|---|---|
| Fetch | 5769ms |
| Detect | 42ms |
| Parse | 19ms |
| Export | 14ms |
Snapshot: 101861_20260314T165538Z.html · Show Snapshot · Open in New Tab
Parsed Units (19)
5' x 5'
$19.99/mo
Street: $34.99
8' x 25'
$19.99/mo
Street: $34.99
5' x 5'
$24.99/mo
Street: $39.99
2 left
5' x 10'
$39.99/mo
Street: $69.99
10' x 5'
$41.99/mo
Street: $72.99
5' x 15'
$45.99/mo
Street: $79.99
10' x 10'
$49.99/mo
Street: $84.99
8' x 45'
$54.99/mo
Street: $94.99
8' x 20'
$59.99/mo
Street: $104.99
10' x 15'
$59.99/mo
Street: $104.99
10' x 15'
$64.99/mo
Street: $114.99
1 left
10' x 20'
$79.99/mo
Street: $139.99
10' x 20'
$89.99/mo
Street: $159.99
10' x 20'
$94.99/mo
Street: $169.99
1 left
10' x 25'
$124.99/mo
Street: $194.99
3 left
10' x 30'
$149.99/mo
Street: $234.99
2 left
20' x 20'
$199.99/mo
Street: $314.99
1 left
20' x 30'
$399.99/mo
Street: $554.99
1 left
20' x 50'
$999.99/mo
Street: $1394.99
1 left