Facility: 022914
Hilltop Storage Casper
- 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 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)
-
exported Run #20552026-03-27 14:01:18.054172 | 18 units | Facility022914Parser | View Data →
-
exported Run #20542026-03-27 14:01:16.894177 | 18 units | Facility022914Parser | View Data →
-
exported Run #12932026-03-23 03:03:09.202279 | 18 units | Facility022914Parser | View Data →
-
exported Run #8002026-03-21 18:54:46.470868 | 18 units | Facility022914Parser | View Data →
-
exported Run #3492026-03-14 16:35:20.192364 | 18 units | Facility022914Parser | View Data →
-
failed Run #142026-03-07 01:42:13.112214 | 1 failure(s)
-
started Run #42026-03-07 01:05:24.376707
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.