Minimal utilities to build scrapeable API models on top of Pydantic with simple caching and async HTTP.
Project description
pydantic-scrapeable-api-model
Minimal utilities to build scrapeable API models on top of Pydantic with simple on-disk caching and async HTTP.
Relies on pydantic-cacheable-model for JSON-based caching.
Install
pip install pydantic-scrapeable-api-model
Usage
Quickstart
from __future__ import annotations
import asyncio
from typing import Any
from pydantic_scrapeable_api_model import (
CacheKey,
ScrapeableApiModel,
DetailField,
CustomScrapeField,
)
class MyAPI(ScrapeableApiModel):
BASE_URL = "https://api.example.com"
list_endpoint = "/items"
id: CacheKey[int] # required, unique key (or create a custom `id` property)
name: str
# lazily-scraped field (filled by `scrape_detail` or a custom method)
description: DetailField[str] = CustomScrapeField("fetch_description")
@property
def detail_endpoint(self) -> str | None:
return f"/items/{self.id}"
# Optional: fetch a field yourself instead of relying on detail_endpoint
async def fetch_description(self) -> str:
resp = await self.request(
id=f"item-{self.id}-desc",
url=self._build_url(f"/items/{self.id}/description"),
headers={"Accept": "application/json"},
)
if not resp:
return ""
return resp.json()["description"]
async def main() -> None:
# Scrape list + detail for all models of MyAPI
await MyAPI.scrape_list(check_api=True, use_cache=True)
# Work with cached data later
items = list(MyAPI.load_all_cached())
first = items[0]
print(first.model_dump())
asyncio.run(main())
Fields annotated with DetailField begin as placeholders and are populated
only after scrape_detail runs (via .scrape_list() by default, .scrape_detail(), or a
custom getter). Pass scrape_details=False to scrape_list to skip detail scraping. Use
CustomScrapeField("method_name") to register an async method that returns the
field's value during scrape_detail. These methods are validated to exist and to
return the same type as the field they populate.
Run All Subclasses
from pydantic_scrapeable_api_model import CacheKey, ScrapeableApiModel
# Define a base that sets the host
class Base(ScrapeableApiModel):
BASE_URL = "https://api.example.com"
class Users(Base):
list_endpoint = "/users"
id: CacheKey[int]
username: str
class Posts(Base):
list_endpoint = "/posts"
id: CacheKey[int]
title: str
# Discover and run all children concurrently
asyncio.run(Base.run(use_cache=True, check_api=True))
Absolute Endpoints (no BASE_URL)
from pydantic_scrapeable_api_model import CacheKey, ScrapeableApiModel
class ExternalFeed(ScrapeableApiModel):
BASE_URL = "" # allowed when list_endpoint is absolute
list_endpoint = "https://example.org/feed.json"
id: CacheKey[int]
title: str
# Works because the endpoint is absolute
asyncio.run(ExternalFeed.scrape_list(check_api=True, use_cache=True, scrape_details=False))
Cached Access Helpers
# Load everything from cache
cached_items = list(MyAPI.load_all_cached())
# Fetch one by key (fallback to API when allowed)
item = asyncio.run(MyAPI.get(cache_key="123", check_api=True))
Configure Logging
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s - %(message)s",
)
# Library logs under module name
logging.getLogger("pydantic_scrapeable_api_model").setLevel(logging.INFO)
Custom Response Mapping
Override response_to_models() to adapt to non-trivial API shapes:
from typing import Sequence
import httpx
class MyWrappedAPI(MyAPI):
# Server wraps results like: {"data": {"items": [ ... ]}}
@classmethod
def response_to_models(cls, resp: httpx.Response) -> Sequence["MyWrappedAPI"]:
payload = resp.json()
items = payload.get("data", {}).get("items", [])
return [cls(**row) for row in items]
API Notes
MyModel.scrape_list(check_api=True|False|"/override", use_cache=True|False, scrape_details=True|False) -> list[MyModel]
MyModel.run(use_cache=True, check_api=True) -> None # runs all subclasses
MyModel.get(cache_key=..., check_api=False) -> Optional[MyModel]
MyModel.load_all_cached() -> Iterable[MyModel]
instance.scrape_detail(use_cache=True) -> None
instance.model_dump() -> dict (unscraped fields omitted)
MyModel.response_to_models(resp) -> Sequence[MyModel] # customize parsing
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pydantic_scrapeable_api_model-1.0.0.tar.gz.
File metadata
- Download URL: pydantic_scrapeable_api_model-1.0.0.tar.gz
- Upload date:
- Size: 7.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.8.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fa30501f92e3585946ad82c6b055aab9048173ac8a44421294d93a69bcae6ce4
|
|
| MD5 |
778e92cc94a6ec5a45a491cb2ef6c3a0
|
|
| BLAKE2b-256 |
76e895d29e25d519d7840cca68935578d3033f56f4c6569de4a8dc899d8f52d8
|
File details
Details for the file pydantic_scrapeable_api_model-1.0.0-py3-none-any.whl.
File metadata
- Download URL: pydantic_scrapeable_api_model-1.0.0-py3-none-any.whl
- Upload date:
- Size: 8.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.8.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fef792c5c43cab0cff7c341698631ac37fd53756df5d58c84965326591989b3e
|
|
| MD5 |
ad2c46333063c1e5db31087ef183f9fc
|
|
| BLAKE2b-256 |
ddea967ec07b97dff2d109fd4a8a0ec1b28600c005bb8270ab9580ac278f14da
|