Facility: 080337

Safeguard Storage Idaho

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
080337
Name
Safeguard Storage Idaho
URL
https://www.safeguardstorageidaho.com/
Address
N/A
Platform
custom_facility_080337
Parser File
src/parsers/custom/facility_080337_parser.py
Last Scraped
2026-03-23 03:21:55.631443
Created
2026-03-06 23:45:35.865957
Updated
2026-03-23 03:21:55.631443
Parser & Healing Diagnosis needs_fix
Parser Status
⚠ Needs Fix
Status Reason
Parser returned 0 units
Last Healing Attempt
Not attempted
Parser Source (src/parsers/custom/facility_080337_parser.py)
"""Parser for Safeguard Storage Idaho (storedge platform).

The facility page embeds a schema.org JSON-LD script tag containing a
``@graph`` array of ``Product`` objects.  Each product's ``description``
field encodes the unit dimensions and price in the format:

    ``{width}x{length}x{height} - ${price} - {unit_id}``

e.g. ``"10x10x9 - $95.00 - 34633"``

The unit pricing page is at the facility slug path, not the homepage.
"""

from __future__ import annotations

import json
import re

from bs4 import BeautifulSoup

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

# Matches: "10x10x9 - $95.00 - 34633"  (height is optional context only)
_DESC_RE = re.compile(
    r"(\d+(?:\.\d+)?)\s*[xX]\s*(\d+(?:\.\d+)?)"  # widthxlength
    r"(?:\s*[xX]\s*\d+(?:\.\d+)?)?"               # optional height
    r"\s*-\s*\$([\d,]+(?:\.\d+)?)",               # price
)


class Facility080337Parser(BaseParser):
    """Extract storage units from Safeguard Storage Idaho (storedge SPA).

    Unit data is embedded in a schema.org JSON-LD ``<script>`` block as
    ``Product`` objects whose ``description`` field carries dimensions and
    price.  The facility page URL is:
        https://www.safeguardstorageidaho.com/2101-n-middleton-rd-nampa-id-83651
    """

    platform = "custom_facility_080337"

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

        # Find schema.org JSON-LD script containing @graph with Products
        for script in soup.find_all("script", type="application/ld+json"):
            text = script.get_text(strip=True)
            if '"@graph"' not in text or "Product" not in text:
                continue

            try:
                data = json.loads(text)
            except json.JSONDecodeError:
                continue

            products = data.get("@graph", [])
            for product in products:
                if product.get("@type") != "Product":
                    continue

                description = product.get("description", "")
                offer = product.get("offers", {})

                # Extract price directly from offers (most reliable)
                raw_price = offer.get("price")
                price: float | None = None
                if raw_price is not None:
                    try:
                        price = float(raw_price)
                    except (TypeError, ValueError):
                        price = None

                # Parse dimensions from description field
                m = _DESC_RE.search(description)
                if not m:
                    continue

                width = float(m.group(1))
                length = float(m.group(2))
                sqft = width * length

                # Fall back to price in description string if offer price missing
                if price is None:
                    try:
                        price = float(m.group(3).replace(",", ""))
                    except (TypeError, ValueError):
                        price = None

                size_str = f"{int(width)}' x {int(length)}'"

                unit = UnitResult(
                    size=size_str,
                    price=price,
                    description=description.strip(),
                    url=url,
                    metadata={
                        "width": width,
                        "length": length,
                        "sqft": sqft,
                    },
                )
                result.units.append(unit)

            # Stop after the first matching script block
            if result.units:
                break

        if not result.units:
            result.warnings.append(
                "No units found — ensure snapshot is from the facility pricing page "
                "(e.g. /2101-n-middleton-rd-nampa-id-83651), not the homepage"
            )

        return result

Scrape Runs (5)

Run #1502 Details

Status
exported
Parser Used
Facility080337Parser
Platform Detected
storageunitsoftware
Units Found
0
Stage Reached
exported
Timestamp
2026-03-23 03:21:51.978225
Timing
Stage Duration
Fetch3559ms
Detect50ms
Parse23ms
Export2ms

Snapshot: 080337_20260323T032155Z.html · Show Snapshot · Open in New Tab

No units found in this run.

All Failures for this Facility (5)

parse _WarningAsException scraper no_units_extracted warning Run #N/A | 2026-03-23 03:21:55.628168

No units extracted for 080337

Stack trace
src.reporting.failure_reporter._WarningAsException: No units extracted for 080337
parse _WarningAsException scraper no_units_extracted warning Run #N/A | 2026-03-21 19:15:19.784121

No units extracted for 080337

Stack trace
src.reporting.failure_reporter._WarningAsException: No units extracted for 080337
parse _WarningAsException scraper no_units_extracted warning Run #N/A | 2026-03-14 16:56:44.204827

No units extracted for 080337

Stack trace
src.reporting.failure_reporter._WarningAsException: No units extracted for 080337
parse _WarningAsException scraper no_units_extracted warning Run #N/A | 2026-03-14 05:03:43.111009

No units extracted for 080337

Stack trace
src.reporting.failure_reporter._WarningAsException: No units extracted for 080337
parse _WarningAsException scraper no_units_extracted warning Run #N/A | 2026-03-14 01:02:34.106936

No units extracted for 080337

Stack trace
src.reporting.failure_reporter._WarningAsException: No units extracted for 080337

← Back to dashboard