Facility: 022914

Hilltop Storage Casper

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 18 (-100.0% change, delta: -18).
Facility Information active
Facility ID
022914
Name
Hilltop Storage Casper
URL
https://www.hilltopstoragecasper.com/unit-sizes/
Address
N/A
Platform
custom_facility_022914
Parser File
src/parsers/custom/facility_022914_parser.py
Last Scraped
2026-03-27 14:01:20.915522
Created
2026-03-06 23:45:35.865957
Updated
2026-03-27 14:01:20.943776
Parser & Healing Diagnosis working
Parser Status
✓ Working
Status Reason
N/A
Last Healing Attempt
Not attempted
Parser Source (src/parsers/custom/facility_022914_parser.py)
"""Parser for Hilltop Storage LLC (Casper, WY) - facility 022914.

This is a Beacon CMS site that lists unit sizes in an HTML table with
"Please Call For Availability" as the rate for every unit. No numeric prices
are published on the page.
"""

from __future__ import annotations

from bs4 import BeautifulSoup

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


class Facility022914Parser(BaseParser):
    """Extract storage units from Hilltop Storage LLC unit-sizes page.

    The page contains a single ``<table class="tg">`` with two columns:
    ``Unit Size`` and ``Monthly Rate``. All rates display "Please Call For
    Availability", so ``price`` is left as ``None`` and the raw rate text is
    stored in ``description``.
    """

    platform = "custom_facility_022914"

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

        # Locate the pricing table - it carries class "tg"
        table = soup.find("table", class_="tg")
        if not table:
            result.warnings.append("Pricing table (.tg) not found on page")
            return result

        rows = table.find_all("tr")
        if not rows:
            result.warnings.append("No rows found in pricing table")
            return result

        # Skip header row (contains <th> elements)
        data_rows = [r for r in rows if r.find("td")]

        for row in data_rows:
            cells = row.find_all("td")
            if len(cells) < 2:
                continue

            size_raw = cells[0].get_text(strip=True)
            rate_raw = cells[1].get_text(strip=True)

            if not size_raw:
                continue

            # Normalize the size string into dimensions
            width, length, sqft = self.normalize_size(size_raw)

            # Build a clean size label
            if width is not None and length is not None:
                size_label = f"{int(width)}x{int(length)}"
            else:
                # Preserve unusual formats (e.g. "7ft6inx10", "10x20, 12ft tall")
                size_label = size_raw

            # Detect if this is a "call for price" row
            call_for_price = "call" in rate_raw.lower() or "availability" in rate_raw.lower()

            unit = UnitResult(
                size=size_label,
                description=rate_raw if call_for_price else None,
                price=self.normalize_price(rate_raw) if not call_for_price else None,
                metadata={
                    "width": width,
                    "length": length,
                    "sqft": sqft,
                    "size_raw": size_raw,
                    "rate_raw": rate_raw,
                },
            )
            result.units.append(unit)

        if not result.units:
            result.warnings.append("Table found but no unit rows extracted")

        return result

Scrape Runs (7)

Run #14 Details

Status
failed
Parser Used
N/A
Platform Detected
N/A
Units Found
0
Stage Reached
fetch
Timestamp
2026-03-07 01:42:13.112214

Failures (1)

fetch DatatypeMismatch unknown unknown permanent

column "success" is of type boolean but expression is of type integer LINE 3: ... VALUES ('022914', 14, '022914_20260307T014215Z.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 ('022914', 14, '022914_20260307T014215Z.html', 0)
                                                                     ^
HINT:  You will need to rewrite or cast the expression.

All Failures for this Facility (1)

fetch DatatypeMismatch unknown unknown permanent Run #14 | 2026-03-07 01:42:15.857152

column "success" is of type boolean but expression is of type integer LINE 3: ... VALUES ('022914', 14, '022914_20260307T014215Z.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 ('022914', 14, '022914_20260307T014215Z.html', 0)
                                                                     ^
HINT:  You will need to rewrite or cast the expression.

← Back to dashboard