Facility: 038927

South Shepherd Storage

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
038927
Name
South Shepherd Storage
URL
https://southshepherdstorage.ccstorage.com/find_units
Address
N/A
Platform
custom_facility_038927
Parser File
src/parsers/custom/facility_038927_parser.py
Last Scraped
2026-03-23 03:19:30.841029
Created
2026-03-06 23:45:35.865957
Updated
2026-03-23 03:19:30.852573
Parser & Healing Diagnosis working
Parser Status
✓ Working
Status Reason
N/A
Last Healing Attempt
Not attempted
Parser Source (src/parsers/custom/facility_038927_parser.py)
"""Parser for South Shepherd Storage facility.

This is a CCStoarge-based SPA that renders unit type cards inside a
``<turbo-frame>`` element.  Each card contains a size label, an amenities
section, and a pricing table that lists separate Card and Cash rates.
Availability is signalled by the CTA button text ("Rent Now" vs
"Join waiting list") and an optional "No Units Available" badge.
"""

from __future__ import annotations

from bs4 import BeautifulSoup

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


class Facility038927Parser(BaseParser):
    """Extract storage units from South Shepherd Storage.

    Unit cards live inside ``<div data-test-type="sut_...">`` elements rendered
    inside a ``<turbo-frame>`` block.  Each card exposes:

    - Size: ``<p class="text-xl font-bold">`` (e.g. "10x10")
    - Amenities: listed below an "Amenities" heading (or "None listed")
    - Prices: one or more ``<dl>`` pairs with ``<dt>`` label (Card / Cash)
      and ``<dd>`` amount
    - Availability: "No Units Available" div present when sold out
    """

    platform = "custom_facility_038927"

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

        # Unit cards are divs with a data-test-type attribute (sut_...)
        containers = soup.select("div[data-test-type]")

        for container in containers:
            unit = UnitResult(url=url)

            # --- Size ---
            size_el = container.select_one("p.text-xl.font-bold")
            if size_el:
                size_text = size_el.get_text(strip=True)
                unit.size = size_text
                w, ln, sq = self.normalize_size(size_text)
                if w is not None:
                    unit.metadata = {"width": w, "length": ln, "sqft": sq}

            # --- Amenities ---
            amenities_text: str | None = None
            amenities_heading = container.find(
                lambda tag: tag.name == "p" and tag.get_text(strip=True) == "Amenities"
            )
            if amenities_heading:
                # The amenity list is the next sibling <p> or element
                sibling = amenities_heading.find_next_sibling()
                if sibling:
                    amenities_text = sibling.get_text(strip=True)
                    if amenities_text and amenities_text.lower() != "none listed":
                        if unit.metadata is None:
                            unit.metadata = {}
                        unit.metadata["amenities"] = amenities_text

            # --- Prices: iterate all <dl> pairs ---
            cash_price: float | None = None
            card_price: float | None = None
            for dl in container.select("dl"):
                dt = dl.select_one("dt")
                dd = dl.select_one("dd")
                if not dt or not dd:
                    continue
                label = dt.get_text(strip=True).lower()
                value = self.normalize_price(dd.get_text(strip=True))
                if label == "cash":
                    cash_price = value
                elif label == "card":
                    card_price = value

            # Use Cash as the primary price, Card as an alternative
            unit.price = cash_price
            if card_price is not None:
                if unit.metadata is None:
                    unit.metadata = {}
                unit.metadata["card_price"] = card_price

            # --- Availability ---
            no_units_el = container.find(
                lambda tag: tag.name == "div"
                and "No Units Available" in tag.get_text()
            )
            if no_units_el:
                unit.scarcity = "No Units Available"
            else:
                # Check for "Rent Now" button indicating availability
                rent_now = container.find(
                    lambda tag: tag.name in ("a", "button")
                    and "Rent Now" in tag.get_text()
                )
                if rent_now:
                    unit.scarcity = "Available"

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

        if not result.units:
            result.warnings.append("No unit cards found on page")

        return result

Scrape Runs (5)

Run #144 Details

Status
exported
Parser Used
Facility038927Parser
Platform Detected
ccstorage
Units Found
16
Stage Reached
exported
Timestamp
2026-03-14 04:58:06.852955
Timing
Stage Duration
Fetch3567ms
Detect0ms
Parse34ms
Export6ms

Snapshot: 038927_20260314T045810Z.html · Show Snapshot · Open in New Tab

Parsed Units (16)

10x10

$59.00/mo
No Units Available

10x15

$71.00/mo
No Units Available

10x20

$85.00/mo
No Units Available

10x25

$103.00/mo
No Units Available

10x30

$122.00/mo
No Units Available

10x40

$156.00/mo
No Units Available

10x7

$48.00/mo
Available

10x8

$50.00/mo
No Units Available

11x10

$59.00/mo
No Units Available

11x15

$71.00/mo
No Units Available

11x25

$103.00/mo
No Units Available

11x30

$152.00/mo
No Units Available

11x40

$156.00/mo
No Units Available

5x7

$30.00/mo
No Units Available

5x8

$30.00/mo
No Units Available

8x20 RV Out Door Parking

$33.00/mo
No Units Available

← Back to dashboard