Facility: 112073
Volunteer Storage - Maryville 2
- Facility ID
- 112073
- Name
- Volunteer Storage - Maryville 2
- URL
- https://ospreystorage.com/storage-locations/tennessee/maryville/volunteer-storage-maryville-2/?utm_source=google&utm_medium=organic&utm_campaign=rentstorage
- Address
- 2924 E Lamar Alexander Pkwy, Maryville, TN 37804, USA, Maryville, Tennessee 37804
- Platform
- custom_facility_112073
- Parser File
- src/parsers/custom/facility_112073_parser.py
- Last Scraped
- 2026-03-27 13:45:57.968094
- Created
- 2026-03-20 23:32:48.933261
- Updated
- 2026-03-27 13:45:58.003196
- Parser Status
- ✓ Working
- Status Reason
- N/A
- Last Healing Attempt
- Not attempted
Parser Source (src/parsers/custom/facility_112073_parser.py)
"""Parser for Volunteer Storage — Maryville 2, TN.
The page uses the Osprey Storage / StorEdge "vapor-unit-table" WordPress
plugin which loads unit data via client-side API. The server-rendered HTML
contains only loading skeletons, but the page embeds a JSON-LD ``@graph``
block with a ``SelfStorage`` entity whose ``hasOfferCatalog`` lists all
unit groups as ``Product`` objects.
This facility has storage units and vehicle parking spaces. The ``name``
field encodes dimensions and category (e.g. "10x10 - Uncovered Parking",
"8x10 - Small").
"""
from __future__ import annotations
import json
import re
from bs4 import BeautifulSoup
from src.parsers.base import BaseParser, ParseResult, UnitResult
_SIZE_RE = re.compile(r"(\d+(?:\.\d+)?)\s*[xX]\s*(\d+(?:\.\d+)?)")
class Facility112073Parser(BaseParser):
"""Extract storage units from Volunteer Storage Maryville 2 (JSON-LD)."""
platform = "custom_facility_112073"
def parse(self, html: str, url: str = "") -> ParseResult:
soup = BeautifulSoup(html, "lxml")
result = ParseResult(platform=self.platform, parser_name=self.__class__.__name__)
products = self._find_products(soup)
if not products:
result.warnings.append("No SelfStorage hasOfferCatalog found in JSON-LD")
return result
seen: set[tuple[str, float | None]] = set()
for product in products:
offers = product.get("offers", {})
raw_price = offers.get("price")
# Skip products with no price
if raw_price is None:
continue
price: float | None = None
try:
price = float(raw_price)
except (TypeError, ValueError):
price = self.normalize_price(str(raw_price))
name = product.get("name", "")
availability = offers.get("availability", "")
# Parse size from name like "10x10 - Uncovered Parking"
m = _SIZE_RE.search(name)
if not m:
continue
width = float(m.group(1))
length = float(m.group(2))
size = f"{m.group(1)}x{m.group(2)}"
key = (size, price)
if key in seen:
continue
seen.add(key)
# Determine category from name suffix
category = ""
if " - " in name:
category = name.split(" - ", 1)[1].strip()
is_available = "OutOfStock" not in availability
metadata: dict = {
"width": width,
"length": length,
"sqft": width * length,
}
if category:
metadata["category"] = category
if not is_available:
metadata["available"] = False
unit = UnitResult(
size=size,
price=price,
description=name,
url=url,
metadata=metadata,
)
result.units.append(unit)
if not result.units:
result.warnings.append("No valid units found in JSON-LD")
return result
@staticmethod
def _find_products(soup: BeautifulSoup) -> list[dict]:
"""Locate Product items from SelfStorage JSON-LD."""
for script in soup.find_all("script", type="application/ld+json"):
raw = script.string or ""
if not raw.strip():
continue
try:
data = json.loads(raw)
except (json.JSONDecodeError, ValueError):
continue
# Check @graph for SelfStorage entity
if isinstance(data, dict) and "@graph" in data:
for item in data["@graph"]:
if not isinstance(item, dict):
continue
item_type = item.get("@type", "")
if isinstance(item_type, list):
item_type = " ".join(item_type)
if "SelfStorage" in str(item_type):
catalog = item.get("hasOfferCatalog", {})
products = catalog.get("itemListElement", [])
if products:
return products
# Direct SelfStorage at top level
if isinstance(data, dict):
item_type = data.get("@type", "")
if isinstance(item_type, list):
item_type = " ".join(item_type)
if "SelfStorage" in str(item_type):
catalog = data.get("hasOfferCatalog", {})
products = catalog.get("itemListElement", [])
if products:
return products
return []
Scrape Runs (4)
-
exported Run #16832026-03-27 13:45:50.427697 | 23 units | Facility112073Parser | View Data →
-
exported Run #16822026-03-27 13:45:49.747139 | 23 units | Facility112073Parser | View Data →
-
exported Run #11032026-03-23 02:46:23.016500 | 23 units | Facility112073Parser | View Data →
-
exported Run #6092026-03-21 18:36:36.095130 | 23 units | Facility112073Parser | View Data →
Run #609 Details
- Status
- exported
- Parser Used
- Facility112073Parser
- Platform Detected
- storageunitsoftware
- Units Found
- 23
- Stage Reached
- exported
- Timestamp
- 2026-03-21 18:36:36.095130
Timing
| Stage | Duration |
|---|---|
| Fetch | 6325ms |
| Detect | 179ms |
| Parse | 203ms |
| Export | 22ms |
Snapshot: 112073_20260321T183642Z.html · Show Snapshot · Open in New Tab
Parsed Units (23)
10x10
$64.00/mo
16x20
$102.00/mo
18x19
$109.00/mo
10x40
$84.00/mo
8x10
$147.00/mo
9x10
$162.00/mo
8x12
$164.00/mo
10x10
$155.00/mo
8x13
$169.00/mo
8x15
$174.00/mo
10x12
$184.00/mo
10x16
$217.00/mo
12x14
$230.00/mo
13x16
$112.00/mo
12x21
$149.00/mo
13x22
$120.00/mo
16x28
$192.00/mo
20x24
$199.00/mo
20x26
$199.00/mo
17x31
$182.00/mo
20x30
$224.00/mo
20x35
$302.00/mo
30x33
$294.00/mo