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
7 changes: 5 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `visor.models._base` — shared building blocks and `ListingsFilterBase`
- `visor.models.listings` — `ListingsFilter` and listing response models
- `visor.models.facets` — `FacetsFilter` and facet response models
- `AsyncVisorClient` — async HTTP client with context-manager support
- `VisorClient` — synchronous HTTP client with context-manager support
- Auto-pagination helpers: `paginate_listings`, `paginate_dealers` (async generators),
`iter_listings`, `iter_dealers` (sync iterators)
- Public export audit and test suite (`tests/test_exports.py`)

## Future Work

- HTTP client (`AsyncVisorClient`, `VisorClient`)
- Auto-pagination helpers
- Retry-with-backoff strategy
- MCP server wrapper
- Response caching
32 changes: 27 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ pip install visor-python
## Quick start

```python
from visor import VisorClient, ListingsFilter
from visor import VisorClient, ListingsFilter, iter_listings

with VisorClient() as client: # reads VISOR_API_KEY from env
# Search for used Toyota trucks in Texas under $40k
Expand All @@ -26,14 +26,15 @@ with VisorClient() as client: # reads VISOR_API_KEY from env
)
)
for listing in page.data:
print(f"{listing.year} {listing.make} {listing.model} — ${listing.price:,}")
price = f"${listing.price:,}" if listing.price is not None else "N/A"
print(f"{listing.year} {listing.make} {listing.model} — {price}")

# Look up a specific VIN
vin = client.lookup_vin("4T1DAACKXTU765422", include=["price_history"])
print(vin.build.combined_msrp)
msrp = f"${vin.build.combined_msrp:,}" if vin.build.combined_msrp is not None else "N/A"
print(msrp)

# Paginate all results
from visor import iter_listings
for listing in iter_listings(client, ListingsFilter(make=["Ford"], state=["TX"])):
print(listing.vin)
```
Expand All @@ -50,7 +51,8 @@ async def main():
ListingsFilter(make=["Toyota"], state=["TX"], max_price=40_000)
)
for listing in page.data:
print(listing.vin, listing.price)
price = f"${listing.price:,}" if listing.price is not None else "N/A"
print(listing.vin, price)

asyncio.run(main())
```
Expand All @@ -66,6 +68,26 @@ client = VisorClient(api_key="vsr_live_...")
client = VisorClient()
```

## Error handling

All methods raise typed exceptions from `visor.exceptions`. Rate limit responses
include a `retry_after` hint; the SDK does not retry automatically.

```python
from visor import VisorClient, ListingsFilter, RateLimitError, VisorAPIError

with VisorClient() as client:
try:
page = client.filter_listings(ListingsFilter(make=["Toyota"]))
except RateLimitError as e:
retry_after = f"{e.retry_after}s" if e.retry_after is not None else "later"
print(f"Rate limited — retry {retry_after}")
except VisorAPIError as e:
print(f"API error {e.status_code}: {e}")
```

All responses are Pydantic models.

## License

MIT
Loading