Facility: 023093

Buffalo Mountain View Storage

Stale Data Warning: This facility has not been successfully scraped in 26 days (threshold: 3 days). Data may be outdated.
Facility Information active
Facility ID
023093
Name
Buffalo Mountain View Storage
URL
https://www.buffalomountainviewstorage.com/units-and-size-guide
Address
N/A
Platform
custom_facility_023093
Parser File
src/parsers/custom/facility_023093_parser.py
Last Scraped
2026-03-27 14:01:24.692455
Created
2026-03-06 23:45:35.865957
Updated
2026-03-27 14:01:24.718855
Parser & Healing Diagnosis working
Parser Status
✓ Working
Status Reason
N/A
Last Healing Attempt
Not attempted
Parser Source (src/parsers/custom/facility_023093_parser.py)
"""Parser for Mountain View Mini Storage of Buffalo, WY.

This is a Squarespace site that lists units as accordion items.
Each accordion item has a title with the size (e.g. "10 x 10") and
a description containing the price (e.g. "$50/month").

The "Indoor covered parking" item is a special case: the title has no
dimensions but the description body lists two sub-sizes with prices
inline ("10x15 $70/month" and "10x25 $90/month").

The "Uncovered outdoor parking" item has no price (call for pricing)
and is skipped.
"""

from __future__ import annotations

import re

from bs4 import BeautifulSoup

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


class Facility023093Parser(BaseParser):
    """Extract storage units from Mountain View Mini Storage of Buffalo, WY."""

    platform = "custom_facility_023093"

    # Matches title sizes like "10 x 10", "10 x 25", "8 x 20 Con Ex"
    _TITLE_SIZE_RE = re.compile(r"(\d+)\s*[xX]\s*(\d+)", re.IGNORECASE)

    # Matches "$50/month" or "$50/mo" anywhere in text
    _PRICE_RE = re.compile(r"\$([\d,]+(?:\.\d+)?)\s*/\s*mo(?:nth)?", re.IGNORECASE)

    # Matches inline size+price pairs in description like "10x15 $70/month"
    _INLINE_UNIT_RE = re.compile(
        r"(\d+)\s*[xX]\s*(\d+)\s+\$([\d,]+(?:\.\d+)?)\s*/\s*mo(?:nth)?",
        re.IGNORECASE,
    )

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

        accordion_items = soup.find_all("li", class_="accordion-item")

        for item in accordion_items:
            title_el = item.find("span", class_="accordion-item__title")
            desc_el = item.find("div", class_="accordion-item__description")

            if not title_el:
                continue

            title_text = title_el.get_text(strip=True)
            desc_text = desc_el.get_text(separator=" ", strip=True) if desc_el else ""

            title_size_match = self._TITLE_SIZE_RE.search(title_text)

            if title_size_match:
                # Standard unit: size is in the title, price is in the description
                width = float(title_size_match.group(1))
                length = float(title_size_match.group(2))

                # Build a clean size label; include any suffix (e.g. "Con Ex")
                suffix_text = title_text[title_size_match.end():].strip()
                size_label = f"{int(width)} x {int(length)}"
                if suffix_text:
                    size_label = f"{size_label} {suffix_text}"

                price = None
                price_match = self._PRICE_RE.search(desc_text)
                if price_match:
                    price = self.normalize_price(f"${price_match.group(1)}")

                unit = UnitResult(
                    size=size_label,
                    price=price,
                    description=desc_text,
                    metadata={"width": width, "length": length, "sqft": width * length},
                )

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

            elif "covered parking" in title_text.lower():
                # Special case: "Indoor covered parking" — two sub-sizes in description
                for inline_match in self._INLINE_UNIT_RE.finditer(desc_text):
                    width = float(inline_match.group(1))
                    length = float(inline_match.group(2))
                    price = self.normalize_price(f"${inline_match.group(3)}")

                    unit = UnitResult(
                        size=f"{int(width)} x {int(length)}",
                        price=price,
                        description=f"Indoor covered parking — {int(width)}x{int(length)}",
                        metadata={
                            "width": width,
                            "length": length,
                            "sqft": width * length,
                            "amenities": ["indoor", "covered parking"],
                        },
                    )
                    result.units.append(unit)

            # "Uncovered outdoor parking" has no price, skip it

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

        return result

Scrape Runs (6)

Run #801 Details

Status
exported
Parser Used
Facility023093Parser
Platform Detected
table_layout
Units Found
7
Stage Reached
exported
Timestamp
2026-03-21 18:54:50.412583
Timing
Stage Duration
Fetch3659ms
Detect73ms
Parse28ms
Export9ms

Snapshot: 023093_20260321T185454Z.html · Show Snapshot · Open in New Tab

Parsed Units (7)

10 x 10

$50.00/mo

10 x 12

$65.00/mo

10 x 15

$70.00/mo

10 x 25

$90.00/mo

8 x 20 Con Ex

$80.00/mo

10 x 15

$70.00/mo

10 x 25

$90.00/mo

All Failures for this Facility (1)

fetch DatatypeMismatch unknown unknown permanent Run #32 | 2026-03-09 20:49:49.324489

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

← Back to dashboard