Facility: 112029

New Frontier 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
112029
Name
New Frontier Self Storage
URL
https://www.newfrontierselfstorage.com/locations/laramie-wy-82070
Address
N/A
Platform
custom_facility_112029
Parser File
src/parsers/custom/facility_112029_parser.py
Last Scraped
2026-03-23 03:13:41.116762
Created
2026-03-06 23:45:35.865957
Updated
2026-03-23 03:13:41.124796
Parser & Healing Diagnosis working
Parser Status
✓ Working
Status Reason
N/A
Last Healing Attempt
Not attempted
Parser Source (src/parsers/custom/facility_112029_parser.py)
"""Parser for New Frontier Self Storage - Laramie, WY facility.

This is a CCStorage/Dragon-themed site with structured unit cards.
Each card contains dimensions, a sale price (price-normal), a regular
price (price-discount/strikethrough), a promotion, scarcity badge,
and unit type indicator.
"""

from __future__ import annotations

from bs4 import BeautifulSoup

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


class Facility112029Parser(BaseParser):
    """Extract storage units from New Frontier Self Storage (Laramie, WY).

    Unit cards use CSS classes:
      - .dimensions          — size text e.g. "5' x 5'"
      - .price-normal        — discounted/sale price e.g. "$30"
      - .price-discount      — regular (strikethrough) price e.g. "$61"
      - .unit-recurring      — promotion text e.g. "75% Off 1st Month"
      - .badge-danger span   — scarcity badge e.g. "2 Units Left"
      - .unit-type-name      — unit type e.g. "Drive-up", "Inside"
    """

    platform = "custom_facility_112029"

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

        containers = soup.find_all(
            "div", class_=lambda c: c and "unit-card" in c
        )

        for container in containers:
            unit = UnitResult()

            # Size / dimensions
            dims_el = container.find("span", class_="dimensions")
            if dims_el:
                size_text = dims_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}

            # Sale price (the discounted "price-normal" displayed prominently)
            sale_el = container.find(
                "div", class_=lambda c: c and "price-normal" in c
            )
            if sale_el:
                unit.sale_price = self.normalize_price(sale_el.get_text(strip=True))

            # Regular price (strikethrough "price-discount")
            regular_el = container.find(
                "div", class_=lambda c: c and "price-discount" in c
            )
            if regular_el:
                unit.price = self.normalize_price(regular_el.get_text(strip=True))

            # Promotion text
            promo_el = container.find("div", class_="unit-recurring")
            if promo_el:
                unit.promotion = promo_el.get_text(strip=True)

            # Scarcity badge (badge-danger indicates limited availability)
            scarcity_badge = container.find(
                "span", class_=lambda c: c and "badge-danger" in c
            )
            if scarcity_badge:
                unit.scarcity = scarcity_badge.get_text(strip=True)

            # Unit type — store in metadata alongside dimensions
            type_el = container.find(
                "div", class_=lambda c: c and "unit-type-name" in c
            )
            if type_el:
                unit_type = type_el.get_text(strip=True)
                if unit.metadata is None:
                    unit.metadata = {}
                unit.metadata["unit_type"] = unit_type
                unit.description = unit_type

            if unit.size or unit.price or unit.sale_price:
                result.units.append(unit)

        if not result.units:
            result.warnings.append("No units found on page")

        return result

Scrape Runs (8)

Run #453 Details

Status
exported
Parser Used
Facility112029Parser
Platform Detected
ccstorage
Units Found
6
Stage Reached
exported
Timestamp
2026-03-14 16:45:18.158710
Timing
Stage Duration
Fetch5113ms
Detect46ms
Parse35ms
Export14ms

Snapshot: 112029_20260314T164523Z.html · Show Snapshot · Open in New Tab

Parsed Units (6)

5' x 5'

$60.00/mo
Street: $64.00
Last Unit

10' x 5'

$49.00/mo
Street: $86.00

5' x 10'

$49.00/mo
Street: $86.00

10' x 10'

$61.00/mo
Street: $132.00

10' x 15'

$79.00/mo
Street: $162.00

20' x 10'

$119.00/mo
Street: $192.00

All Failures for this Facility (1)

fetch DatatypeMismatch unknown unknown permanent Run #13 | 2026-03-07 01:42:13.089607

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

← Back to dashboard