Facility: 086007

Walla Walla Self Storage - Eastgate

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
086007
Name
Walla Walla Self Storage - Eastgate
URL
https://www.wallawallaselfstorage.com/eastgate-self-storage/
Address
N/A
Platform
custom_facility_086007
Parser File
src/parsers/custom/facility_086007_parser.py
Last Scraped
2026-03-23 03:22:18.621772
Created
2026-03-06 23:45:35.865957
Updated
2026-03-23 03:22:18.627723
Parser & Healing Diagnosis working
Parser Status
✓ Working
Status Reason
N/A
Last Healing Attempt
Not attempted
Parser Source (src/parsers/custom/facility_086007_parser.py)
"""Parser for Eastgate Self Storage (Walla Walla, WA) facility 086007.

The site uses the StorEdge/storedge platform. Unit listings appear in
``<div id="NxM-Storage">`` blocks, each containing a title, a green
sale price, a crossed-out regular price, amenity bullets and an
optional red promotion / scarcity badge.
"""

from __future__ import annotations

import re

from bs4 import BeautifulSoup

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

# Matches IDs like "5x5-Storage", "10x20-Storage", "12x24-Storage"
_UNIT_ID_RE = re.compile(r"^\d+x\d+-Storage$", re.IGNORECASE)

# Scarcity indicator in the red promo badge
_LAST_UNIT_RE = re.compile(r"last\s+unit", re.IGNORECASE)


class Facility086007Parser(BaseParser):
    """Extract storage units from Eastgate Self Storage (facility 086007).

    Each unit group is wrapped in a ``<div id="NxM-Storage">`` block
    and contains:
    - A bold title div with the size label (e.g. "5x5 Storage")
    - A green ``<span>`` with the sale / starting-at price
    - A struck-through div (``.sutv2-cross-out``) with the regular price
    - A ``<ul>`` with amenity bullets; the first ``<li>`` may be a red
      promotion badge that includes scarcity language ("Last Unit …")
    """

    platform = "custom_facility_086007"

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

        unit_divs = soup.find_all("div", id=_UNIT_ID_RE)
        if not unit_divs:
            result.warnings.append("No unit blocks found (expected <div id='NxM-Storage'>)")
            return result

        for block in unit_divs:
            # --- size ---
            title_div = block.find("div", class_=lambda c: c and "font-bold" in c and "text-xl" in c)
            raw_title = title_div.get_text(strip=True) if title_div else block.get("id", "")
            width, length, sqft = self.normalize_size(raw_title)
            if width is not None and length is not None:
                size = f"{int(width)}x{int(length)}"
            else:
                # Fall back to the block id (e.g. "5x5-Storage" → "5x5")
                size = block.get("id", "").replace("-Storage", "").replace("-storage", "")

            # --- sale price (green bold span) ---
            sale_span = block.find("span", class_=lambda c: c and "text-wcgreen" in c)
            sale_price = self.normalize_price(sale_span.get_text(strip=True)) if sale_span else None

            # --- regular price (struck-through) ---
            crossout_div = block.find("div", class_=lambda c: c and "sutv2-cross-out" in c)
            regular_price = self.normalize_price(crossout_div.get_text(strip=True)) if crossout_div else None

            # --- amenities & promotion ---
            amenities: list[str] = []
            promotion: str | None = None
            scarcity: str | None = None
            for li in block.find_all("li"):
                li_text = li.get_text(strip=True)
                li_classes = " ".join(li.get("class", []))
                if "text-wcred" in li_classes:
                    # Red badge = promotion / scarcity text
                    promotion = li_text
                    if _LAST_UNIT_RE.search(li_text):
                        scarcity = "Last unit available"
                else:
                    amenities.append(li_text)

            # Build description from amenities list
            description = "; ".join(amenities) if amenities else None

            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 amenities:
                metadata["amenities"] = amenities

            unit = UnitResult(
                size=size,
                description=description,
                price=regular_price,
                sale_price=sale_price,
                promotion=promotion,
                scarcity=scarcity,
                url=url,
                metadata=metadata if metadata else None,
            )
            result.units.append(unit)

        return result

Scrape Runs (5)

Run #179 Details

Status
exported
Parser Used
Facility086007Parser
Platform Detected
storageunitsoftware
Units Found
6
Stage Reached
exported
Timestamp
2026-03-14 05:04:08.472839
Timing
Stage Duration
Fetch4644ms
Detect32ms
Parse16ms
Export8ms

Snapshot: 086007_20260314T050413Z.html · Show Snapshot · Open in New Tab

Parsed Units (6)

5x5

$12.00/mo
Street: $24.00
Last unit available

10x10

$25.00/mo
Street: $50.00

10x15

$27.00/mo
Street: $54.00

10x20

$42.00/mo
Street: $84.00

12x24

$69.00/mo
Street: $137.00

5x10

$128.00/mo

← Back to dashboard