visor-python 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- visor/__init__.py +95 -0
- visor/_client.py +588 -0
- visor/_pagination.py +109 -0
- visor/_transport.py +130 -0
- visor/exceptions.py +72 -0
- visor/models/__init__.py +82 -0
- visor/models/_base.py +353 -0
- visor/models/dealers.py +81 -0
- visor/models/facets.py +142 -0
- visor/models/listings.py +205 -0
- visor/models/usage.py +30 -0
- visor/models/vins.py +9 -0
- visor/py.typed +0 -0
- visor_python-0.1.0.dist-info/METADATA +250 -0
- visor_python-0.1.0.dist-info/RECORD +17 -0
- visor_python-0.1.0.dist-info/WHEEL +4 -0
- visor_python-0.1.0.dist-info/licenses/LICENSE +21 -0
visor/models/listings.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
from datetime import date
|
|
2
|
+
from typing import Literal
|
|
3
|
+
|
|
4
|
+
from pydantic import Field, field_validator
|
|
5
|
+
|
|
6
|
+
from visor.models._base import (
|
|
7
|
+
DealerRef,
|
|
8
|
+
ListingsFilterBase,
|
|
9
|
+
Pagination,
|
|
10
|
+
PriceHistoryEntry,
|
|
11
|
+
SortOrder,
|
|
12
|
+
VehicleOption,
|
|
13
|
+
VehicleRecord,
|
|
14
|
+
VisorResponseModel,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
LISTING_FIELDS = {
|
|
18
|
+
"default",
|
|
19
|
+
"id",
|
|
20
|
+
"vin",
|
|
21
|
+
"year",
|
|
22
|
+
"make",
|
|
23
|
+
"model",
|
|
24
|
+
"trim",
|
|
25
|
+
"version",
|
|
26
|
+
"body_type",
|
|
27
|
+
"drivetrain",
|
|
28
|
+
"fuel_type",
|
|
29
|
+
"powertrain_type",
|
|
30
|
+
"transmission",
|
|
31
|
+
"engine",
|
|
32
|
+
"cylinders",
|
|
33
|
+
"doors",
|
|
34
|
+
"seating_capacity",
|
|
35
|
+
"exterior_color",
|
|
36
|
+
"interior_color",
|
|
37
|
+
"base_exterior_color",
|
|
38
|
+
"base_interior_color",
|
|
39
|
+
"msrp",
|
|
40
|
+
"discount_from_msrp",
|
|
41
|
+
"price",
|
|
42
|
+
"miles",
|
|
43
|
+
"days_on_market",
|
|
44
|
+
"status",
|
|
45
|
+
"inventory_status",
|
|
46
|
+
"inventory_type",
|
|
47
|
+
"stock_number",
|
|
48
|
+
"vdp_url",
|
|
49
|
+
"sold_date",
|
|
50
|
+
"dealer_id",
|
|
51
|
+
"dealer_name",
|
|
52
|
+
"dealer_type",
|
|
53
|
+
"city",
|
|
54
|
+
"state",
|
|
55
|
+
"latitude",
|
|
56
|
+
"longitude",
|
|
57
|
+
"distance_miles",
|
|
58
|
+
"photo_urls",
|
|
59
|
+
"features",
|
|
60
|
+
"options_packages",
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ListingsFilter(ListingsFilterBase):
|
|
65
|
+
limit: int = 50
|
|
66
|
+
offset: int = 0
|
|
67
|
+
sort: SortOrder = SortOrder.DAYS_ON_MARKET
|
|
68
|
+
fields: list[str] | None = None
|
|
69
|
+
include: list[Literal["price_history", "options"]] | None = None
|
|
70
|
+
|
|
71
|
+
@field_validator("limit")
|
|
72
|
+
@classmethod
|
|
73
|
+
def _limit_max(cls, v: int) -> int:
|
|
74
|
+
if v > 100:
|
|
75
|
+
raise ValueError("limit maximum is 100")
|
|
76
|
+
return v
|
|
77
|
+
|
|
78
|
+
@field_validator("fields")
|
|
79
|
+
@classmethod
|
|
80
|
+
def _validate_fields(cls, v: list[str] | None) -> list[str] | None:
|
|
81
|
+
if v is not None:
|
|
82
|
+
unknown = set(v) - LISTING_FIELDS
|
|
83
|
+
if unknown:
|
|
84
|
+
raise ValueError(f"unknown fields: {unknown}")
|
|
85
|
+
return v
|
|
86
|
+
|
|
87
|
+
def to_params(self) -> dict[str, str]:
|
|
88
|
+
params = super().to_params()
|
|
89
|
+
params["limit"] = str(self.limit)
|
|
90
|
+
params["offset"] = str(self.offset)
|
|
91
|
+
params["sort"] = self.sort.value
|
|
92
|
+
if self.fields:
|
|
93
|
+
params["fields"] = ",".join(self.fields)
|
|
94
|
+
if self.include:
|
|
95
|
+
params["include"] = ",".join(self.include)
|
|
96
|
+
return params
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
# Response models
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class ListingSummary(VisorResponseModel):
|
|
105
|
+
"""Returned by filter_listings() and dealer_inventory().
|
|
106
|
+
|
|
107
|
+
id and vin are always present; the API returns them regardless of fields
|
|
108
|
+
projection. All other fields are optional because the caller controls which
|
|
109
|
+
fields are returned via ListingsFilter.fields.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
id: str
|
|
113
|
+
vin: str
|
|
114
|
+
year: int | None = None
|
|
115
|
+
make: str | None = None
|
|
116
|
+
model: str | None = None
|
|
117
|
+
trim: str | None = None
|
|
118
|
+
version: str | None = None
|
|
119
|
+
body_type: str | None = None
|
|
120
|
+
drivetrain: str | None = None
|
|
121
|
+
fuel_type: str | None = None
|
|
122
|
+
powertrain_type: str | None = None
|
|
123
|
+
transmission: str | None = None
|
|
124
|
+
engine: str | None = None
|
|
125
|
+
cylinders: int | None = None
|
|
126
|
+
doors: int | None = None
|
|
127
|
+
seating_capacity: int | None = None
|
|
128
|
+
exterior_color: str | None = None
|
|
129
|
+
interior_color: str | None = None
|
|
130
|
+
base_exterior_color: str | None = None
|
|
131
|
+
base_interior_color: str | None = None
|
|
132
|
+
msrp: int | None = None
|
|
133
|
+
discount_from_msrp: int | None = None
|
|
134
|
+
price: int | None = None
|
|
135
|
+
miles: int | None = None
|
|
136
|
+
days_on_market: int | None = None
|
|
137
|
+
status: str | None = None
|
|
138
|
+
inventory_status: str | None = None
|
|
139
|
+
inventory_type: str | None = None
|
|
140
|
+
stock_number: str | None = None
|
|
141
|
+
vdp_url: str | None = None
|
|
142
|
+
sold_date: date | None = None
|
|
143
|
+
dealer_id: str | None = None
|
|
144
|
+
dealer_name: str | None = None
|
|
145
|
+
dealer_type: str | None = None
|
|
146
|
+
city: str | None = None
|
|
147
|
+
state: str | None = None
|
|
148
|
+
latitude: float | None = None
|
|
149
|
+
longitude: float | None = None
|
|
150
|
+
distance_miles: float | None = None
|
|
151
|
+
photo_urls: list[str] = Field(default_factory=list)
|
|
152
|
+
features: list[str] = Field(default_factory=list)
|
|
153
|
+
options_packages: list[str] = Field(default_factory=list)
|
|
154
|
+
price_history: list[PriceHistoryEntry] = Field(default_factory=list)
|
|
155
|
+
options: list[VehicleOption] = Field(default_factory=list)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class ListingDetail(VisorResponseModel):
|
|
159
|
+
"""Returned by get_listing() — always fully populated."""
|
|
160
|
+
|
|
161
|
+
id: str
|
|
162
|
+
vin: str
|
|
163
|
+
status: str
|
|
164
|
+
price: int | None = None
|
|
165
|
+
miles: int | None = None
|
|
166
|
+
inventory_type: str
|
|
167
|
+
stock_number: str | None = None
|
|
168
|
+
vdp_url: str | None = None
|
|
169
|
+
vhr_url: str | None = None
|
|
170
|
+
photo_urls: list[str] = Field(default_factory=list)
|
|
171
|
+
photo_url_primary: str | None = None
|
|
172
|
+
inventory_date: date | None = None
|
|
173
|
+
sold_date: date | None = None
|
|
174
|
+
last_checked_at: str | None = None
|
|
175
|
+
dealer: DealerRef
|
|
176
|
+
vehicle: VehicleRecord
|
|
177
|
+
price_history: list[PriceHistoryEntry] | None = None
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class ListingSnapshot(VisorResponseModel):
|
|
181
|
+
"""Embedded listing inside VinDetail.latest_listing.
|
|
182
|
+
|
|
183
|
+
Differs from ListingDetail: no top-level vin/status/vehicle fields.
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
id: str
|
|
187
|
+
price: int | None = None
|
|
188
|
+
miles: int | None = None
|
|
189
|
+
inventory_type: str
|
|
190
|
+
stock_number: str | None = None
|
|
191
|
+
vdp_url: str | None = None
|
|
192
|
+
vhr_url: str | None = None
|
|
193
|
+
photo_urls: list[str] = Field(default_factory=list)
|
|
194
|
+
photo_url_primary: str | None = None
|
|
195
|
+
inventory_date: date | None = None
|
|
196
|
+
sold_date: date | None = None
|
|
197
|
+
last_checked_at: str | None = None
|
|
198
|
+
dealer: DealerRef
|
|
199
|
+
price_history: list[PriceHistoryEntry] | None = None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class ListingsPage(VisorResponseModel):
|
|
203
|
+
data: list[ListingSummary]
|
|
204
|
+
pagination: Pagination
|
|
205
|
+
meta: dict[str, object] = Field(default_factory=dict)
|
visor/models/usage.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from datetime import date
|
|
2
|
+
|
|
3
|
+
from visor.models._base import VisorResponseModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class UsageRecord(VisorResponseModel):
|
|
7
|
+
date: date
|
|
8
|
+
metering_class: str
|
|
9
|
+
requests: int
|
|
10
|
+
charged_micros: int
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class UsageTotals(VisorResponseModel):
|
|
14
|
+
requests: int
|
|
15
|
+
charged_micros: int
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class UsageMeta(VisorResponseModel):
|
|
19
|
+
start_date: date
|
|
20
|
+
end_date: date
|
|
21
|
+
interval: str
|
|
22
|
+
currency: str
|
|
23
|
+
source: str
|
|
24
|
+
freshness: str
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class UsageSummary(VisorResponseModel):
|
|
28
|
+
data: list[UsageRecord]
|
|
29
|
+
totals: UsageTotals
|
|
30
|
+
meta: UsageMeta
|
visor/models/vins.py
ADDED
visor/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: visor-python
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the Visor Public API
|
|
5
|
+
Project-URL: Homepage, https://visor.vin
|
|
6
|
+
Project-URL: Repository, https://github.com/whitewalls86/visor-python
|
|
7
|
+
Project-URL: Issues, https://github.com/whitewalls86/visor-python/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/whitewalls86/visor-python/blob/master/CHANGELOG.md
|
|
9
|
+
Author-email: Andrew Miller <miller.andrew.preston@gmail.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: api,automotive,car,inventory,listings,sdk,vehicle,visor
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: httpx>=0.27
|
|
24
|
+
Requires-Dist: pydantic>=2.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: build>=1.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
28
|
+
Requires-Dist: pre-commit>=3.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
33
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
34
|
+
Requires-Dist: twine>=5.0; extra == 'dev'
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# visor-python
|
|
38
|
+
|
|
39
|
+
[](https://github.com/whitewalls86/visor-python/actions/workflows/ci.yml)
|
|
40
|
+
|
|
41
|
+
**visor-python** is an unofficial community Python SDK for the [Visor Public API](https://api.visor.vin) — a vehicle inventory search platform covering new, used, and certified pre-owned listings from dealers across the US. It provides a thin, fully-typed wrapper around the REST API with sync and async clients, Pydantic response models, and auto-pagination helpers.
|
|
42
|
+
|
|
43
|
+
> **Disclaimer:** This is an unofficial community SDK and is not affiliated with or endorsed by Visor (Currents Systems Inc.).
|
|
44
|
+
|
|
45
|
+
> **Pre-1.0 notice:** This package is in initial development (`0.x`). Minor version bumps may include breaking changes. Pin to a specific minor version in production and review the [CHANGELOG](CHANGELOG.md) before upgrading.
|
|
46
|
+
|
|
47
|
+
## Install
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install visor-python
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Requires Python 3.10+ and no non-standard runtime dependencies beyond `httpx` and `pydantic`.
|
|
54
|
+
|
|
55
|
+
## Quick start
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from visor import VisorClient, ListingsFilter, iter_listings
|
|
59
|
+
|
|
60
|
+
with VisorClient() as client: # reads VISOR_API_KEY from env
|
|
61
|
+
# Search for used Toyota Tacomas in Texas under $40k
|
|
62
|
+
page = client.filter_listings(
|
|
63
|
+
ListingsFilter(
|
|
64
|
+
make=["Toyota"],
|
|
65
|
+
model=["Tacoma"],
|
|
66
|
+
inventory_type=["used"],
|
|
67
|
+
state=["TX"],
|
|
68
|
+
max_price=40_000,
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
for listing in page.data:
|
|
72
|
+
price = f"${listing.price:,}" if listing.price is not None else "N/A"
|
|
73
|
+
print(f"{listing.year} {listing.make} {listing.model} — {price}")
|
|
74
|
+
|
|
75
|
+
# Look up a specific VIN
|
|
76
|
+
vin = client.lookup_vin("4T1DAACKXTU765422", include=["price_history"])
|
|
77
|
+
msrp = f"${vin.build.combined_msrp:,}" if vin.build.combined_msrp is not None else "N/A"
|
|
78
|
+
print(msrp)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Geo filtering
|
|
82
|
+
|
|
83
|
+
Pass a `postal_code` and `radius` (miles) to search near a location:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
page = client.filter_listings(
|
|
87
|
+
ListingsFilter(
|
|
88
|
+
make=["Honda"],
|
|
89
|
+
model=["CR-V"],
|
|
90
|
+
postal_code="90210",
|
|
91
|
+
radius=50,
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`radius` requires exactly one of `postal_code` or `latitude`/`longitude`. Passing `radius` alone (or with neither) raises `ValueError` before any network call.
|
|
97
|
+
|
|
98
|
+
### Paginating all results
|
|
99
|
+
|
|
100
|
+
`iter_listings` (sync) and `paginate_listings` (async) iterate every page automatically:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from visor import VisorClient, ListingsFilter, iter_listings
|
|
104
|
+
|
|
105
|
+
with VisorClient() as client:
|
|
106
|
+
for listing in iter_listings(
|
|
107
|
+
client,
|
|
108
|
+
ListingsFilter(make=["Ford"], state=["TX"]),
|
|
109
|
+
):
|
|
110
|
+
print(listing.vin, listing.price)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
For dealers, use `iter_dealers` / `paginate_dealers` in the same way.
|
|
114
|
+
|
|
115
|
+
`client.filter_listings(...)` returns a single page (`ListingsPage`). Use the
|
|
116
|
+
`iter_*` / `paginate_*` helpers when you need all results.
|
|
117
|
+
|
|
118
|
+
### Async
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
import asyncio
|
|
122
|
+
from visor import AsyncVisorClient, ListingsFilter, paginate_listings
|
|
123
|
+
|
|
124
|
+
async def main() -> None:
|
|
125
|
+
async with AsyncVisorClient() as client:
|
|
126
|
+
# Single page
|
|
127
|
+
page = await client.filter_listings(
|
|
128
|
+
ListingsFilter(make=["Toyota"], state=["TX"], max_price=40_000)
|
|
129
|
+
)
|
|
130
|
+
for listing in page.data:
|
|
131
|
+
price = f"${listing.price:,}" if listing.price is not None else "N/A"
|
|
132
|
+
print(listing.vin, price)
|
|
133
|
+
|
|
134
|
+
# All pages
|
|
135
|
+
async for listing in paginate_listings(
|
|
136
|
+
client,
|
|
137
|
+
ListingsFilter(make=["Toyota"], state=["TX"]),
|
|
138
|
+
):
|
|
139
|
+
print(listing.vin)
|
|
140
|
+
|
|
141
|
+
asyncio.run(main())
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Configuration
|
|
145
|
+
|
|
146
|
+
### API key
|
|
147
|
+
|
|
148
|
+
Pass your key explicitly or export `VISOR_API_KEY` before running:
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
client = VisorClient(api_key="vsr_live_...")
|
|
152
|
+
# or
|
|
153
|
+
# export VISOR_API_KEY=vsr_live_...
|
|
154
|
+
client = VisorClient()
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
You need your own Visor API key — see [api.visor.vin](https://api.visor.vin) for details. Use of the API is governed by Visor's API terms; you are responsible for complying with them.
|
|
158
|
+
|
|
159
|
+
### Timeout
|
|
160
|
+
|
|
161
|
+
Default request timeout is 30 seconds. Override at construction time:
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
client = VisorClient(timeout=10.0)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Base URL (advanced)
|
|
168
|
+
|
|
169
|
+
`base_url` defaults to the production API. Override it for local testing or staging:
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
client = VisorClient(base_url="http://localhost:8080")
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Key concepts
|
|
176
|
+
|
|
177
|
+
### `ListingsFilter` is shared
|
|
178
|
+
|
|
179
|
+
`ListingsFilter` is accepted by both `filter_listings()` and `dealer_inventory()`. Build one filter object and reuse it across both methods.
|
|
180
|
+
|
|
181
|
+
### `fields` is response projection, not filtering
|
|
182
|
+
|
|
183
|
+
`ListingsFilter.fields` controls which fields the API returns — it does not filter which listings match. Example:
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
filter = ListingsFilter(
|
|
187
|
+
make=["Toyota"],
|
|
188
|
+
fields=["vin", "price", "miles"],
|
|
189
|
+
)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
`id` and `vin` are always returned by the API regardless of the `fields` projection.
|
|
193
|
+
|
|
194
|
+
### Responses are Pydantic models
|
|
195
|
+
|
|
196
|
+
All responses — `ListingsPage`, `ListingDetail`, `VinDetail`, etc. — are Pydantic v2 models. Access fields as attributes and use standard Pydantic methods (`.model_dump()`, `.model_json_schema()`, etc.) as needed.
|
|
197
|
+
|
|
198
|
+
## Error handling
|
|
199
|
+
|
|
200
|
+
All methods raise typed exceptions from `visor.exceptions`. The SDK does not retry automatically — `RateLimitError.retry_after` gives you the hint to build your own retry logic.
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
import time
|
|
204
|
+
from visor import VisorClient, ListingsFilter, RateLimitError, VisorAPIError
|
|
205
|
+
|
|
206
|
+
def fetch_with_backoff(client: VisorClient, f: ListingsFilter) -> object:
|
|
207
|
+
for attempt in range(4):
|
|
208
|
+
try:
|
|
209
|
+
return client.filter_listings(f)
|
|
210
|
+
except RateLimitError as e:
|
|
211
|
+
wait = e.retry_after if e.retry_after is not None else 2 ** attempt
|
|
212
|
+
print(f"Rate limited — waiting {wait}s")
|
|
213
|
+
time.sleep(wait)
|
|
214
|
+
except VisorAPIError as e:
|
|
215
|
+
raise # surface non-rate-limit errors immediately
|
|
216
|
+
raise RuntimeError("Exhausted retries")
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Exception hierarchy:
|
|
220
|
+
|
|
221
|
+
| Exception | When |
|
|
222
|
+
|---|---|
|
|
223
|
+
| `VisorAPIError` | Base for all API errors; has `.status_code` |
|
|
224
|
+
| `AuthError` | 401 — invalid or missing API key |
|
|
225
|
+
| `NotFoundError` | 404 — resource does not exist |
|
|
226
|
+
| `RateLimitError` | 429 — includes `.retry_after` (seconds, or `None`) |
|
|
227
|
+
|
|
228
|
+
## Debugging
|
|
229
|
+
|
|
230
|
+
**Inspect the exception** — `VisorAPIError` carries `.status_code` and a message from the API.
|
|
231
|
+
|
|
232
|
+
**Check `retry_after`** — for `RateLimitError`, `.retry_after` is the number of seconds to wait (or `None` if the API did not provide a value).
|
|
233
|
+
|
|
234
|
+
**Request-level logging** — visor-python uses `httpx` internally. Enable httpx logging to see raw requests and responses:
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
import logging
|
|
238
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
239
|
+
logging.getLogger("httpx").setLevel(logging.DEBUG)
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Community
|
|
243
|
+
|
|
244
|
+
- [CONTRIBUTING.md](CONTRIBUTING.md) — how to contribute
|
|
245
|
+
- [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) — community standards
|
|
246
|
+
- [GitHub Issues](https://github.com/whitewalls86/visor-python/issues) — bug reports and feature requests
|
|
247
|
+
|
|
248
|
+
## License
|
|
249
|
+
|
|
250
|
+
MIT
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
visor/__init__.py,sha256=ZwBV7OIrapXUndumE4_9E0PgLhAiPpLRPqlvycLka3k,1955
|
|
2
|
+
visor/_client.py,sha256=rKqp0OOFE-oKtu5QOhGeXqjvOw5bh-M80uaX3ZmJlpg,23995
|
|
3
|
+
visor/_pagination.py,sha256=uwTP5GwjYNphF2TXyBn_9XBF2yJodL9GHqMrrKbySCw,3561
|
|
4
|
+
visor/_transport.py,sha256=coJb9ZTcADTH_QGF9u-PBU0B2LUUs8LLfUVabnVZtzU,3956
|
|
5
|
+
visor/exceptions.py,sha256=Fk7zpkNeiingXqCep9abhP0FmVxa5DZNqDVgvlj7GoE,2313
|
|
6
|
+
visor/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
visor/models/__init__.py,sha256=R2pOfwsLSTxIrJyehqVvhwXcEdpc77_iKHjh4hpUan8,1560
|
|
8
|
+
visor/models/_base.py,sha256=pelgGdQK7LeA5AsX_TUrD_ymKKxU2C1T3yXhcvuddxs,11903
|
|
9
|
+
visor/models/dealers.py,sha256=ravIxjhWxpRq5aCyJ0kIGbqoxjoiBpk-AX2IhiPAqpc,2178
|
|
10
|
+
visor/models/facets.py,sha256=Kwi4XvMebow0o5SEyY17u_0PGsy1eJbLtGERpNoEke0,3712
|
|
11
|
+
visor/models/listings.py,sha256=gwOC1q4z9BdIjKWOF8CvHZUD--7j2gD4Fz9R9SZ8Zss,5716
|
|
12
|
+
visor/models/usage.py,sha256=tZCnQo3AVNllcmpLRW-g0lsN0bIvtUB8EY08nFl8XaY,546
|
|
13
|
+
visor/models/vins.py,sha256=IsKKRJurGHHUuKEQIU8finljalPKT0I94iVzpKm0-kw,256
|
|
14
|
+
visor_python-0.1.0.dist-info/METADATA,sha256=o4t-J2GseWoNYng1r0_U32R1VDFBhgDDNomtyKQQuic,8511
|
|
15
|
+
visor_python-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
16
|
+
visor_python-0.1.0.dist-info/licenses/LICENSE,sha256=VyWYhOWf272yziUZJBZwHP54JIp4PG7TQp2GphWiWs4,1082
|
|
17
|
+
visor_python-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 visor-python contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|