Facility: 019998

Sheridan Storage

Stale Data Warning: This facility has not been successfully scraped in 26 days (threshold: 3 days). Data may be outdated.
⚠ Unit Count Anomaly (Critical): Current run has 0 units, expected baseline is 6 (-100.0% change, delta: -6).
Facility Information active
Facility ID
019998
Name
Sheridan Storage
URL
https://www.sheridanstorage.net/2501-allen-ave-sheridan-wy-82801
Address
N/A
Platform
custom_facility_019998
Parser File
src/parsers/custom/facility_019998_parser.py
Last Scraped
2026-03-27 14:01:10.079710
Created
2026-03-06 23:45:35.865957
Updated
2026-03-27 14:01:10.079710
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_019998_parser.py)
"""Parser for Interstate Storage - Sheridan, WY (facility 019998).

This site uses the StorEdge/storageunitsoftware platform but with
obfuscated CSS class names that differ from the standard parser selectors.
The unit data is structured in a Bootstrap table with:
  - Size in ._2MVARJZtZDaHzpMVh0UAfo
  - Description type text in a plain <div> inside the second <td>
  - Price in ._3V2N9w-4bQvYJ9lUWBSkHt
"""

from __future__ import annotations

from bs4 import BeautifulSoup

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


class Facility019998Parser(BaseParser):
    """Extract storage units from Interstate Storage - Sheridan, WY.

    The page renders a Bootstrap table with obfuscated class names.
    Each <tr> in the <tbody> represents one unit type with size, description,
    and price columns.
    """

    platform = "custom_facility_019998"

    # CSS class for the size text element inside the first <td>
    _SIZE_CLASS = "_2MVARJZtZDaHzpMVh0UAfo"

    # CSS class for the price element inside the third <td>
    _PRICE_CLASS = "_3V2N9w-4bQvYJ9lUWBSkHt"

    # The unit table is identified by having both "table" and a specific class
    _TABLE_CLASS = "_2QREMkZenGaFOjxHn8CDhU"

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

        # Locate the unit pricing table
        table = soup.find("table", class_=self._TABLE_CLASS)
        if not table:
            result.warnings.append("Unit pricing table not found")
            return result

        tbody = table.find("tbody")
        if not tbody:
            result.warnings.append("Table body not found")
            return result

        rows = tbody.find_all("tr")
        for row in rows:
            tds = row.find_all("td")
            if len(tds) < 3:
                continue

            unit = UnitResult()

            # Extract size from first column
            size_el = tds[0].find(class_=self._SIZE_CLASS)
            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}

            # Extract description from the second column (unit type text)
            desc_td = tds[1]
            # The type label sits in a plain <div> alongside an icon <div>
            # Grab all direct text-bearing divs and pick the non-empty one
            desc_text_parts = []
            for div in desc_td.find_all("div", recursive=True):
                text = div.get_text(strip=True)
                if text and not div.find("div"):
                    desc_text_parts.append(text)
            if desc_text_parts:
                unit.description = " | ".join(dict.fromkeys(desc_text_parts))

            # Extract price from the third column
            price_el = tds[2].find(class_=self._PRICE_CLASS)
            if price_el:
                unit.price = self.normalize_price(price_el.get_text(strip=True))

            # Only add if we have at least a size or a price
            if unit.size or unit.price:
                result.units.append(unit)

        if not result.units:
            result.warnings.append("No unit rows extracted from table")

        return result

Scrape Runs (6)

Run #2048 Details

Status
exported
Parser Used
Facility019998Parser
Platform Detected
storageunitsoftware
Units Found
0
Stage Reached
exported
Timestamp
2026-03-27 14:01:03.904915
Timing
Stage Duration
Fetch4804ms
Detect32ms
Parse17ms
Export15ms

Snapshot: 019998_20260327T140108Z.html · Show Snapshot · Open in New Tab

No units found in this run.

All Failures for this Facility (3)

parse _WarningAsException scraper no_units_extracted warning Run #N/A | 2026-03-27 14:01:10.060761

No units extracted for 019998

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

No units extracted for 019998

Stack trace
src.reporting.failure_reporter._WarningAsException: No units extracted for 019998
fetch DatatypeMismatch unknown unknown permanent Run #23 | 2026-03-09 20:49:19.460657

column "success" is of type boolean but expression is of type integer LINE 3: ... VALUES ('019998', 23, '019998_20260309T204919Z.html', 0) ^ HINT: You will need to rewrite or cast the expression.

Stack trace
Traceback (most recent call last):
  File "/app/src/pipeline.py", line 329, in _process_facility
    manifest_id = storage.insert_snapshot_manifest(
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/src/db/pg_backend.py", line 615, in insert_snapshot_manifest
    row = self._execute_returning(
          ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/src/db/pg_backend.py", line 54, in _execute_returning
    cur.execute(sql, params)
  File "/app/.venv/lib/python3.11/site-packages/psycopg2/extras.py", line 236, in execute
    return super().execute(query, vars)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
psycopg2.errors.DatatypeMismatch: column "success" is of type boolean but expression is of type integer
LINE 3: ...    VALUES ('019998', 23, '019998_20260309T204919Z.html', 0)
                                                                     ^
HINT:  You will need to rewrite or cast the expression.

← Back to dashboard