Facility: 14

A Safe Self 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
14
Name
A Safe Self Storage
URL
https://www.asafeselfstorage.com/sizes.html
Address
N/A
Platform
custom_facility_14
Parser File
src/parsers/custom/facility_14_parser.py
Last Scraped
2026-03-23 03:14:04.068220
Created
2026-03-14 16:21:53.706708
Updated
2026-03-23 03:14:04.076797
Parser & Healing Diagnosis working
Parser Status
✓ Working
Status Reason
N/A
Last Healing Attempt
Not attempted
Parser Source (src/parsers/custom/facility_14_parser.py)
"""Parser for A Safe Self Storage."""

from __future__ import annotations

import re

from bs4 import BeautifulSoup

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


class Facility14Parser(BaseParser):
    """Extract storage units from A Safe Self Storage.

    Uses an AccordionWidget where each panel tab has size text
    (e.g. "5 x 5 . 25 sq ft") and the panel content has
    "Rent = $55.00" / "Monthly Payment = $58.30".
    """

    platform = "custom_facility_14"

    _RENT_RE = re.compile(r"Rent\s*=\s*\$([\d,]+(?:\.\d+)?)", re.IGNORECASE)

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

        panels = soup.select(".AccordionPanel")
        if not panels:
            result.warnings.append("No AccordionPanel elements found")
            return result

        for panel in panels:
            tab = panel.select_one(".AccordionPanelTab")
            content = panel.select_one(".AccordionPanelContent")

            if not tab or not content:
                continue

            size_text = tab.get_text(strip=True)
            content_text = content.get_text(separator=" ", strip=True)

            unit = UnitResult()
            unit.size = size_text
            unit.description = f"{size_text} | {content_text}"
            w, ln, sq = self.normalize_size(size_text)
            if w is not None:
                unit.metadata = {"width": w, "length": ln, "sqft": sq}

            rent_match = self._RENT_RE.search(content_text)
            if rent_match:
                unit.price = self.normalize_price(rent_match.group(1))

            if (unit.metadata and "width" in unit.metadata) or unit.price:
                result.units.append(unit)

        if not result.units:
            result.warnings.append("Accordion panels found but no units extracted")

        return result

Scrape Runs (3)

Run #909 Details

Status
exported
Parser Used
Facility14Parser
Platform Detected
unknown
Units Found
8
Stage Reached
exported
Timestamp
2026-03-21 19:06:40.557042
Timing
Stage Duration
Fetch2343ms
Detect252ms
Parse45ms
Export7ms

Snapshot: 14_20260321T190642Z.html · Show Snapshot · Open in New Tab

Parsed Units (8)

5 x 5 • 25 sq ft

$55.00/mo

5 x 10 • 50 sq ft

$75.00/mo

5 x 15 • 75 sq ft

$85.00/mo

10 x 10 • 100 sq ft

$110.00/mo

10 x 15 • 150 sq ft

$130.00/mo

10 x 20 • 200 sq ft

$150.00/mo

10 x 25 • 250 sq ft

$180.00/mo

10 x 30 • 300 sq ft

$200.00/mo

← Back to dashboard