Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 80 additions & 1 deletion src/visor/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,80 @@
# Public exports added in implementation step 12 (after _client and _pagination exist).
from visor.exceptions import (
AuthError,
ForbiddenError,
NotFoundError,
PaymentRequiredError,
RateLimitError,
ValidationError,
VisorAPIError,
VisorError,
VisorTransportError,
)
from visor.models._base import (
BBox,
DealerRef,
Pagination,
VehicleBuild,
VehicleOption,
VehicleRecord,
)
from visor.models.dealers import (
DealerAddress,
DealerDetail,
DealerFilter,
DealersPage,
DealerSummary,
)
from visor.models.facets import FacetBucket, FacetsData, FacetsFilter, FacetsResponse
from visor.models.listings import (
ListingDetail,
ListingsFilter,
ListingSnapshot,
ListingsPage,
ListingSummary,
)
from visor.models.usage import UsageMeta, UsageRecord, UsageSummary, UsageTotals
from visor.models.vins import VinDetail

__all__ = [
# exceptions
"AuthError",
"ForbiddenError",
"NotFoundError",
"PaymentRequiredError",
"RateLimitError",
"ValidationError",
"VisorAPIError",
"VisorError",
"VisorTransportError",
# shared base
"BBox",
"DealerRef",
"Pagination",
"VehicleBuild",
"VehicleOption",
"VehicleRecord",
# dealers
"DealerAddress",
"DealerDetail",
"DealerFilter",
"DealersPage",
"DealerSummary",
# facets
"FacetBucket",
"FacetsData",
"FacetsFilter",
"FacetsResponse",
# listings
"ListingDetail",
"ListingsFilter",
"ListingsPage",
"ListingSnapshot",
"ListingSummary",
# usage
"UsageMeta",
"UsageRecord",
"UsageSummary",
"UsageTotals",
# vins
"VinDetail",
]
82 changes: 82 additions & 0 deletions src/visor/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from visor.models._base import (
BBox,
DealerRef,
InventoryMode,
Pagination,
PriceHistoryEntry,
SortOrder,
VehicleBuild,
VehicleOption,
VehicleRecord,
VisorRequestModel,
VisorResponseModel,
)
from visor.models.dealers import (
DealerAddress,
DealerDetail,
DealerFilter,
DealersPage,
DealerSummary,
)
from visor.models.facets import (
FacetBucket,
FacetsData,
FacetsFilter,
FacetsMeta,
FacetsResponse,
FieldStats,
RangeBucket,
RangeFacet,
)
from visor.models.listings import (
ListingDetail,
ListingsFilter,
ListingSnapshot,
ListingsPage,
ListingSummary,
)
from visor.models.usage import UsageMeta, UsageRecord, UsageSummary, UsageTotals
from visor.models.vins import VinDetail

__all__ = [
# base
"BBox",
"DealerRef",
"InventoryMode",
"Pagination",
"PriceHistoryEntry",
"SortOrder",
"VehicleBuild",
"VehicleOption",
"VehicleRecord",
"VisorRequestModel",
"VisorResponseModel",
# dealers
"DealerAddress",
"DealerDetail",
"DealerFilter",
"DealersPage",
"DealerSummary",
# facets
"FacetBucket",
"FacetsData",
"FacetsMeta",
"FacetsFilter",
"FacetsResponse",
"FieldStats",
"RangeBucket",
"RangeFacet",
# listings
"ListingDetail",
"ListingsFilter",
"ListingsPage",
"ListingSnapshot",
"ListingSummary",
# usage
"UsageMeta",
"UsageRecord",
"UsageSummary",
"UsageTotals",
# vins
"VinDetail",
]
81 changes: 81 additions & 0 deletions src/visor/models/dealers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
from typing import Literal

from pydantic import Field, field_validator

from visor.models._base import Pagination, VisorRequestModel, VisorResponseModel


class DealerFilter(VisorRequestModel):
limit: int = 50
offset: int = 0
dealer_id: list[str] | None = None
state: list[str] | None = None
country: str | None = None
type: Literal["franchise", "independent"] | None = None
make: list[str] | None = None
q: str | None = None

@field_validator("limit")
@classmethod
def _limit_max(cls, v: int) -> int:
if v > 100:
raise ValueError("limit maximum is 100")
return v

@field_validator("dealer_id")
@classmethod
def _dealer_id_max(cls, v: list[str] | None) -> list[str] | None:
if v is not None and len(v) > 100:
raise ValueError("dealer_id maximum is 100 entries")
return v

def to_params(self) -> dict[str, str]:
params: dict[str, str] = {
"limit": str(self.limit),
"offset": str(self.offset),
}
if self.dealer_id:
params["dealer_id"] = ",".join(self.dealer_id)
if self.state:
params["state"] = ",".join(self.state)
if self.country:
params["country"] = self.country
if self.type:
params["type"] = self.type
if self.make:
params["make"] = ",".join(self.make)
if self.q:
params["q"] = self.q
return params


class DealerAddress(VisorResponseModel):
line1: str | None = None
city: str
state: str
country: str


class DealerSummary(VisorResponseModel):
dealer_id: str
name: str
city: str
state: str
country: str
latitude: float | None = None
longitude: float | None = None
type: str
website: str | None = None
makes: list[str] = Field(default_factory=list)
listing_count: int


class DealerDetail(DealerSummary):
phone: str | None = None
address: DealerAddress | None = None


class DealersPage(VisorResponseModel):
data: list[DealerSummary]
pagination: Pagination
meta: dict[str, object] = Field(default_factory=dict)
24 changes: 23 additions & 1 deletion src/visor/models/facets.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Literal, TypeAlias

from pydantic import Field, field_validator
from pydantic import Field, field_validator, model_validator

from visor.models._base import ListingsFilterBase, VisorResponseModel

Expand Down Expand Up @@ -39,6 +39,10 @@

FacetSort: TypeAlias = Literal["count", "-count", "metric", "-metric"]

# Facets that return range histograms; all others are categorical (bucket lists).
RANGE_FACET_NAMES = {"price", "msrp", "miles", "days_on_market"}
CATEGORICAL_FACET_NAMES = FACET_NAMES - RANGE_FACET_NAMES


class FacetsFilter(ListingsFilterBase):
"""Filter for GET /v1/facets. No pagination or projection."""
Expand All @@ -56,6 +60,24 @@ def _validate_facets(cls, v: list[str]) -> list[str]:
raise ValueError(f"unknown facets: {unknown}")
return v

@field_validator("facet_value_limit")
@classmethod
def _facet_value_limit_max(cls, v: int | None) -> int | None:
if v is not None and v > 100:
raise ValueError("facet_value_limit maximum is 100")
return v

@model_validator(mode="after")
def _validate_metric_needs_categorical_facet(self) -> "FacetsFilter":
if self.metric is not None:
categorical = [f for f in self.facets if f in CATEGORICAL_FACET_NAMES]
if len(categorical) != 1:
raise ValueError(
"metric requires exactly one categorical facet; "
f"got {len(categorical)} ({categorical})"
)
return self

def to_params(self) -> dict[str, str]:
params = super().to_params()
params["facets"] = ",".join(self.facets)
Expand Down
30 changes: 30 additions & 0 deletions src/visor/models/usage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from datetime import date

from visor.models._base import VisorResponseModel


class UsageRecord(VisorResponseModel):
date: date
metering_class: str
requests: int
charged_micros: int


class UsageTotals(VisorResponseModel):
requests: int
charged_micros: int


class UsageMeta(VisorResponseModel):
start_date: date
end_date: date
interval: str
currency: str
source: str
freshness: str


class UsageSummary(VisorResponseModel):
data: list[UsageRecord]
totals: UsageTotals
meta: UsageMeta
9 changes: 9 additions & 0 deletions src/visor/models/vins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from visor.models._base import VehicleBuild, VisorResponseModel
from visor.models.listings import ListingSnapshot


class VinDetail(VisorResponseModel):
vin: str
status: str
build: VehicleBuild
latest_listing: ListingSnapshot | None = None
Loading
Loading