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 ADDED
@@ -0,0 +1,95 @@
1
+ from visor._client import AsyncVisorClient, VisorClient
2
+ from visor._pagination import (
3
+ iter_dealers,
4
+ iter_listings,
5
+ paginate_dealers,
6
+ paginate_listings,
7
+ )
8
+ from visor.exceptions import (
9
+ AuthError,
10
+ ForbiddenError,
11
+ NotFoundError,
12
+ PaymentRequiredError,
13
+ RateLimitError,
14
+ ValidationError,
15
+ VisorAPIError,
16
+ VisorError,
17
+ VisorTransportError,
18
+ )
19
+ from visor.models._base import (
20
+ BBox,
21
+ DealerRef,
22
+ Pagination,
23
+ VehicleBuild,
24
+ VehicleOption,
25
+ VehicleRecord,
26
+ )
27
+ from visor.models.dealers import (
28
+ DealerAddress,
29
+ DealerDetail,
30
+ DealerFilter,
31
+ DealersPage,
32
+ DealerSummary,
33
+ )
34
+ from visor.models.facets import FacetBucket, FacetsData, FacetsFilter, FacetsResponse
35
+ from visor.models.listings import (
36
+ ListingDetail,
37
+ ListingsFilter,
38
+ ListingSnapshot,
39
+ ListingsPage,
40
+ ListingSummary,
41
+ )
42
+ from visor.models.usage import UsageMeta, UsageRecord, UsageSummary, UsageTotals
43
+ from visor.models.vins import VinDetail
44
+
45
+ __all__ = [
46
+ # client
47
+ "AsyncVisorClient",
48
+ "VisorClient",
49
+ # pagination
50
+ "paginate_listings",
51
+ "paginate_dealers",
52
+ "iter_listings",
53
+ "iter_dealers",
54
+ # exceptions
55
+ "AuthError",
56
+ "ForbiddenError",
57
+ "NotFoundError",
58
+ "PaymentRequiredError",
59
+ "RateLimitError",
60
+ "ValidationError",
61
+ "VisorAPIError",
62
+ "VisorError",
63
+ "VisorTransportError",
64
+ # shared base
65
+ "BBox",
66
+ "DealerRef",
67
+ "Pagination",
68
+ "VehicleBuild",
69
+ "VehicleOption",
70
+ "VehicleRecord",
71
+ # dealers
72
+ "DealerAddress",
73
+ "DealerDetail",
74
+ "DealerFilter",
75
+ "DealersPage",
76
+ "DealerSummary",
77
+ # facets
78
+ "FacetBucket",
79
+ "FacetsData",
80
+ "FacetsFilter",
81
+ "FacetsResponse",
82
+ # listings
83
+ "ListingDetail",
84
+ "ListingsFilter",
85
+ "ListingsPage",
86
+ "ListingSnapshot",
87
+ "ListingSummary",
88
+ # usage
89
+ "UsageMeta",
90
+ "UsageRecord",
91
+ "UsageSummary",
92
+ "UsageTotals",
93
+ # vins
94
+ "VinDetail",
95
+ ]
visor/_client.py ADDED
@@ -0,0 +1,588 @@
1
+ import os
2
+ from datetime import date
3
+ from typing import Literal
4
+
5
+ from visor._transport import DEFAULT_BASE_URL, AsyncVisorTransport, SyncVisorTransport
6
+ from visor.models.dealers import DealerDetail, DealerFilter, DealersPage
7
+ from visor.models.facets import FacetsFilter, FacetsResponse
8
+ from visor.models.listings import ListingDetail, ListingsFilter, ListingsPage
9
+ from visor.models.usage import UsageSummary
10
+ from visor.models.vins import VinDetail
11
+
12
+
13
+ class AsyncVisorClient:
14
+ """Async client for the Visor Public API.
15
+
16
+ All methods are coroutines and must be awaited. Use as an async context
17
+ manager to ensure the underlying HTTP connection pool is closed:
18
+
19
+ async with AsyncVisorClient() as client:
20
+ page = await client.filter_listings(...)
21
+
22
+ Args:
23
+ api_key: Visor API key. Defaults to the ``VISOR_API_KEY`` environment
24
+ variable.
25
+ timeout: Request timeout in seconds. Defaults to 30.
26
+ base_url: API base URL. Override for local testing or staging.
27
+
28
+ Raises:
29
+ ValueError: If no API key is provided or found in the environment.
30
+ """
31
+
32
+ def __init__(
33
+ self,
34
+ api_key: str | None = None,
35
+ timeout: float = 30.0,
36
+ base_url: str = DEFAULT_BASE_URL,
37
+ ) -> None:
38
+ key = api_key or os.environ.get("VISOR_API_KEY")
39
+ if not key:
40
+ raise ValueError(
41
+ "api_key is required. Pass it directly or set VISOR_API_KEY."
42
+ )
43
+ self._transport = AsyncVisorTransport(key, base_url=base_url, timeout=timeout)
44
+
45
+ async def __aenter__(self) -> "AsyncVisorClient":
46
+ return self
47
+
48
+ async def __aexit__(self, *args: object) -> None:
49
+ await self.aclose()
50
+
51
+ async def aclose(self) -> None:
52
+ """Close the underlying HTTP client and release connections."""
53
+ await self._transport.aclose()
54
+
55
+ # ------------------------------------------------------------------ #
56
+ # Inventory #
57
+ # ------------------------------------------------------------------ #
58
+
59
+ async def filter_listings(
60
+ self, filter: ListingsFilter | None = None
61
+ ) -> ListingsPage:
62
+ """Return a single page of listings matching the given filter.
63
+
64
+ To iterate all results across pages, use :func:`visor.paginate_listings`
65
+ instead.
66
+
67
+ Args:
68
+ filter: Search and pagination criteria. Defaults to an empty filter
69
+ (first page, no constraints).
70
+
71
+ Returns:
72
+ A :class:`~visor.models.listings.ListingsPage` containing the
73
+ matched listings and pagination metadata.
74
+
75
+ Raises:
76
+ AuthError: Invalid or missing API key (HTTP 401).
77
+ ForbiddenError: Key lacks access to this resource (HTTP 403).
78
+ RateLimitError: Rate limit exceeded; check ``.retry_after``.
79
+ VisorAPIError: Any other API-level error.
80
+ VisorTransportError: Network-level failure (timeout, connection).
81
+ """
82
+ params = (filter or ListingsFilter()).to_params()
83
+ data = await self._transport.get("/listings", params)
84
+ return ListingsPage.model_validate(data)
85
+
86
+ async def get_listing(
87
+ self,
88
+ listing_id: str,
89
+ include: list[Literal["price_history", "options"]] | None = None,
90
+ ) -> ListingDetail:
91
+ """Return full detail for a single listing by its ID.
92
+
93
+ Args:
94
+ listing_id: The unique listing identifier.
95
+ include: Optional extra sections to embed in the response.
96
+ ``"price_history"`` adds historical price records;
97
+ ``"options"`` adds option/package details.
98
+
99
+ Returns:
100
+ A :class:`~visor.models.listings.ListingDetail` for the requested
101
+ listing.
102
+
103
+ Raises:
104
+ NotFoundError: No listing with that ID exists (HTTP 404).
105
+ AuthError: Invalid or missing API key (HTTP 401).
106
+ ForbiddenError: Key lacks access to this resource (HTTP 403).
107
+ RateLimitError: Rate limit exceeded; check ``.retry_after``.
108
+ VisorAPIError: Any other API-level error.
109
+ VisorTransportError: Network-level failure.
110
+ """
111
+ params: dict[str, str] = {}
112
+ if include:
113
+ params["include"] = ",".join(include)
114
+ data = await self._transport.get(f"/listings/{listing_id}", params)
115
+ return ListingDetail.model_validate(data["data"])
116
+
117
+ async def lookup_vin(
118
+ self,
119
+ vin: str,
120
+ include: list[Literal["price_history", "options"]] | None = None,
121
+ ) -> VinDetail:
122
+ """Return build and listing information for a VIN.
123
+
124
+ The returned :class:`~visor.models.vins.VinDetail` always includes
125
+ ``vin``, ``status``, and ``build``. The ``latest_listing`` field is
126
+ ``None`` when no active or recent listing exists for the VIN.
127
+
128
+ Args:
129
+ vin: 17-character Vehicle Identification Number.
130
+ include: Optional extra sections to embed. ``"price_history"``
131
+ adds historical price records; ``"options"`` adds option
132
+ details on the latest listing.
133
+
134
+ Returns:
135
+ A :class:`~visor.models.vins.VinDetail` for the requested VIN.
136
+
137
+ Raises:
138
+ NotFoundError: VIN not found in the Visor database (HTTP 404).
139
+ AuthError: Invalid or missing API key (HTTP 401).
140
+ ForbiddenError: Key lacks access to this resource (HTTP 403).
141
+ RateLimitError: Rate limit exceeded; check ``.retry_after``.
142
+ VisorAPIError: Any other API-level error.
143
+ VisorTransportError: Network-level failure.
144
+ """
145
+ params: dict[str, str] = {}
146
+ if include:
147
+ params["include"] = ",".join(include)
148
+ data = await self._transport.get(f"/vins/{vin}", params)
149
+ return VinDetail.model_validate(data["data"])
150
+
151
+ async def filter_facets(self, filter: FacetsFilter) -> FacetsResponse:
152
+ """Return facet aggregations for the given filter.
153
+
154
+ Facets summarize available field values and ranges across all listings
155
+ matching the filter, useful for building search-UI refinement panels.
156
+
157
+ Args:
158
+ filter: Facet query criteria, including which facets to compute.
159
+
160
+ Returns:
161
+ A :class:`~visor.models.facets.FacetsResponse` with aggregation
162
+ data for each requested facet.
163
+
164
+ Raises:
165
+ AuthError: Invalid or missing API key (HTTP 401).
166
+ ForbiddenError: Key lacks access to this resource (HTTP 403).
167
+ RateLimitError: Rate limit exceeded; check ``.retry_after``.
168
+ VisorAPIError: Any other API-level error.
169
+ VisorTransportError: Network-level failure.
170
+ """
171
+ data = await self._transport.get("/facets", filter.to_params())
172
+ return FacetsResponse.model_validate(data)
173
+
174
+ # ------------------------------------------------------------------ #
175
+ # Dealers #
176
+ # ------------------------------------------------------------------ #
177
+
178
+ async def search_dealers(self, filter: DealerFilter | None = None) -> DealersPage:
179
+ """Return a single page of dealers matching the given filter.
180
+
181
+ To iterate all dealers across pages, use :func:`visor.paginate_dealers`
182
+ instead.
183
+
184
+ Args:
185
+ filter: Search criteria and pagination parameters. Defaults to an
186
+ empty filter (first page, no constraints).
187
+
188
+ Returns:
189
+ A :class:`~visor.models.dealers.DealersPage` containing matched
190
+ dealers and pagination metadata.
191
+
192
+ Raises:
193
+ AuthError: Invalid or missing API key (HTTP 401).
194
+ ForbiddenError: Key lacks access to this resource (HTTP 403).
195
+ RateLimitError: Rate limit exceeded; check ``.retry_after``.
196
+ VisorAPIError: Any other API-level error.
197
+ VisorTransportError: Network-level failure.
198
+ """
199
+ params = (filter or DealerFilter()).to_params()
200
+ data = await self._transport.get("/dealers", params)
201
+ return DealersPage.model_validate(data)
202
+
203
+ async def get_dealer(self, dealer_id: str) -> DealerDetail:
204
+ """Return full detail for a single dealer by its ID.
205
+
206
+ Args:
207
+ dealer_id: The unique dealer identifier.
208
+
209
+ Returns:
210
+ A :class:`~visor.models.dealers.DealerDetail` for the requested
211
+ dealer.
212
+
213
+ Raises:
214
+ NotFoundError: No dealer with that ID exists (HTTP 404).
215
+ AuthError: Invalid or missing API key (HTTP 401).
216
+ ForbiddenError: Key lacks access to this resource (HTTP 403).
217
+ RateLimitError: Rate limit exceeded; check ``.retry_after``.
218
+ VisorAPIError: Any other API-level error.
219
+ VisorTransportError: Network-level failure.
220
+ """
221
+ data = await self._transport.get(f"/dealers/{dealer_id}")
222
+ return DealerDetail.model_validate(data["data"])
223
+
224
+ async def dealer_inventory(
225
+ self,
226
+ dealer_id: str,
227
+ filter: ListingsFilter | None = None,
228
+ ) -> ListingsPage:
229
+ """Return a single page of inventory for a specific dealer.
230
+
231
+ Accepts the same :class:`~visor.models.listings.ListingsFilter` shape
232
+ as :meth:`filter_listings`, so you can reuse a filter object across
233
+ both methods. Call this method in a loop advancing ``filter.offset`` to
234
+ paginate through all inventory pages.
235
+
236
+ Args:
237
+ dealer_id: The unique dealer identifier.
238
+ filter: Optional search and pagination criteria.
239
+
240
+ Returns:
241
+ A :class:`~visor.models.listings.ListingsPage` of the dealer's
242
+ inventory.
243
+
244
+ Raises:
245
+ NotFoundError: No dealer with that ID exists (HTTP 404).
246
+ AuthError: Invalid or missing API key (HTTP 401).
247
+ ForbiddenError: Key lacks access to this resource (HTTP 403).
248
+ RateLimitError: Rate limit exceeded; check ``.retry_after``.
249
+ VisorAPIError: Any other API-level error.
250
+ VisorTransportError: Network-level failure.
251
+ """
252
+ params = (filter or ListingsFilter()).to_params()
253
+ data = await self._transport.get(f"/dealers/{dealer_id}/listings", params)
254
+ return ListingsPage.model_validate(data)
255
+
256
+ # ------------------------------------------------------------------ #
257
+ # Usage #
258
+ # ------------------------------------------------------------------ #
259
+
260
+ async def get_usage(
261
+ self,
262
+ start_date: date | None = None,
263
+ end_date: date | None = None,
264
+ metering_class: list[str] | None = None,
265
+ ) -> UsageSummary:
266
+ """Return API usage statistics for your account.
267
+
268
+ All parameters are optional. Omitting date bounds returns the full
269
+ available history. Omitting ``metering_class`` returns all endpoint
270
+ categories.
271
+
272
+ Args:
273
+ start_date: Start of the reporting window (inclusive).
274
+ end_date: End of the reporting window (inclusive).
275
+ metering_class: Endpoint categories to include, e.g.
276
+ ``["listings", "dealers"]``. Returns all categories when
277
+ omitted.
278
+
279
+ Returns:
280
+ A :class:`~visor.models.usage.UsageSummary` with request counts
281
+ and quota details.
282
+
283
+ Raises:
284
+ AuthError: Invalid or missing API key (HTTP 401).
285
+ ForbiddenError: Key lacks access to this resource (HTTP 403).
286
+ RateLimitError: Rate limit exceeded; check ``.retry_after``.
287
+ VisorAPIError: Any other API-level error.
288
+ VisorTransportError: Network-level failure.
289
+ """
290
+ params: dict[str, str] = {}
291
+ if start_date:
292
+ params["start_date"] = start_date.isoformat()
293
+ if end_date:
294
+ params["end_date"] = end_date.isoformat()
295
+ if metering_class:
296
+ params["metering_class"] = ",".join(metering_class)
297
+ data = await self._transport.get("/usage", params)
298
+ return UsageSummary.model_validate(data)
299
+
300
+
301
+ class VisorClient:
302
+ """Synchronous client for the Visor Public API.
303
+
304
+ All methods block until the HTTP response is received. The client owns an
305
+ internal :class:`httpx.Client` with a connection pool; it is **not**
306
+ thread-safe. If you share a single ``VisorClient`` across threads, you must
307
+ add external synchronization — or create one client per thread instead.
308
+
309
+ Use as a context manager to ensure the connection pool is released:
310
+
311
+ with VisorClient() as client:
312
+ page = client.filter_listings(...)
313
+
314
+ Args:
315
+ api_key: Visor API key. Defaults to the ``VISOR_API_KEY`` environment
316
+ variable.
317
+ timeout: Request timeout in seconds. Defaults to 30.
318
+ base_url: API base URL. Override for local testing or staging.
319
+
320
+ Raises:
321
+ ValueError: If no API key is provided or found in the environment.
322
+ """
323
+
324
+ def __init__(
325
+ self,
326
+ api_key: str | None = None,
327
+ timeout: float = 30.0,
328
+ base_url: str = DEFAULT_BASE_URL,
329
+ ) -> None:
330
+ key = api_key or os.environ.get("VISOR_API_KEY")
331
+ if not key:
332
+ raise ValueError(
333
+ "api_key is required. Pass it directly or set VISOR_API_KEY."
334
+ )
335
+ self._transport = SyncVisorTransport(key, base_url=base_url, timeout=timeout)
336
+
337
+ def __enter__(self) -> "VisorClient":
338
+ return self
339
+
340
+ def __exit__(self, *args: object) -> None:
341
+ self.close()
342
+
343
+ def close(self) -> None:
344
+ """Close the underlying HTTP client and release connections."""
345
+ self._transport.close()
346
+
347
+ # ------------------------------------------------------------------ #
348
+ # Inventory #
349
+ # ------------------------------------------------------------------ #
350
+
351
+ def filter_listings(self, filter: ListingsFilter | None = None) -> ListingsPage:
352
+ """Return a single page of listings matching the given filter.
353
+
354
+ To iterate all results across pages, use :func:`visor.iter_listings`
355
+ instead.
356
+
357
+ Args:
358
+ filter: Search and pagination criteria. Defaults to an empty filter
359
+ (first page, no constraints).
360
+
361
+ Returns:
362
+ A :class:`~visor.models.listings.ListingsPage` containing the
363
+ matched listings and pagination metadata.
364
+
365
+ Raises:
366
+ AuthError: Invalid or missing API key (HTTP 401).
367
+ ForbiddenError: Key lacks access to this resource (HTTP 403).
368
+ RateLimitError: Rate limit exceeded; check ``.retry_after``.
369
+ VisorAPIError: Any other API-level error.
370
+ VisorTransportError: Network-level failure (timeout, connection).
371
+ """
372
+ params = (filter or ListingsFilter()).to_params()
373
+ data = self._transport.get("/listings", params)
374
+ return ListingsPage.model_validate(data)
375
+
376
+ def get_listing(
377
+ self,
378
+ listing_id: str,
379
+ include: list[Literal["price_history", "options"]] | None = None,
380
+ ) -> ListingDetail:
381
+ """Return full detail for a single listing by its ID.
382
+
383
+ Args:
384
+ listing_id: The unique listing identifier.
385
+ include: Optional extra sections to embed in the response.
386
+ ``"price_history"`` adds historical price records;
387
+ ``"options"`` adds option/package details.
388
+
389
+ Returns:
390
+ A :class:`~visor.models.listings.ListingDetail` for the requested
391
+ listing.
392
+
393
+ Raises:
394
+ NotFoundError: No listing with that ID exists (HTTP 404).
395
+ AuthError: Invalid or missing API key (HTTP 401).
396
+ ForbiddenError: Key lacks access to this resource (HTTP 403).
397
+ RateLimitError: Rate limit exceeded; check ``.retry_after``.
398
+ VisorAPIError: Any other API-level error.
399
+ VisorTransportError: Network-level failure.
400
+ """
401
+ params: dict[str, str] = {}
402
+ if include:
403
+ params["include"] = ",".join(include)
404
+ data = self._transport.get(f"/listings/{listing_id}", params)
405
+ return ListingDetail.model_validate(data["data"])
406
+
407
+ def lookup_vin(
408
+ self,
409
+ vin: str,
410
+ include: list[Literal["price_history", "options"]] | None = None,
411
+ ) -> VinDetail:
412
+ """Return build and listing information for a VIN.
413
+
414
+ The returned :class:`~visor.models.vins.VinDetail` always includes
415
+ ``vin``, ``status``, and ``build``. The ``latest_listing`` field is
416
+ ``None`` when no active or recent listing exists for the VIN.
417
+
418
+ Args:
419
+ vin: 17-character Vehicle Identification Number.
420
+ include: Optional extra sections to embed. ``"price_history"``
421
+ adds historical price records; ``"options"`` adds option
422
+ details on the latest listing.
423
+
424
+ Returns:
425
+ A :class:`~visor.models.vins.VinDetail` for the requested VIN.
426
+
427
+ Raises:
428
+ NotFoundError: VIN not found in the Visor database (HTTP 404).
429
+ AuthError: Invalid or missing API key (HTTP 401).
430
+ ForbiddenError: Key lacks access to this resource (HTTP 403).
431
+ RateLimitError: Rate limit exceeded; check ``.retry_after``.
432
+ VisorAPIError: Any other API-level error.
433
+ VisorTransportError: Network-level failure.
434
+ """
435
+ params: dict[str, str] = {}
436
+ if include:
437
+ params["include"] = ",".join(include)
438
+ data = self._transport.get(f"/vins/{vin}", params)
439
+ return VinDetail.model_validate(data["data"])
440
+
441
+ def filter_facets(self, filter: FacetsFilter) -> FacetsResponse:
442
+ """Return facet aggregations for the given filter.
443
+
444
+ Facets summarize available field values and ranges across all listings
445
+ matching the filter, useful for building search-UI refinement panels.
446
+
447
+ Args:
448
+ filter: Facet query criteria, including which facets to compute.
449
+
450
+ Returns:
451
+ A :class:`~visor.models.facets.FacetsResponse` with aggregation
452
+ data for each requested facet.
453
+
454
+ Raises:
455
+ AuthError: Invalid or missing API key (HTTP 401).
456
+ ForbiddenError: Key lacks access to this resource (HTTP 403).
457
+ RateLimitError: Rate limit exceeded; check ``.retry_after``.
458
+ VisorAPIError: Any other API-level error.
459
+ VisorTransportError: Network-level failure.
460
+ """
461
+ data = self._transport.get("/facets", filter.to_params())
462
+ return FacetsResponse.model_validate(data)
463
+
464
+ # ------------------------------------------------------------------ #
465
+ # Dealers #
466
+ # ------------------------------------------------------------------ #
467
+
468
+ def search_dealers(self, filter: DealerFilter | None = None) -> DealersPage:
469
+ """Return a single page of dealers matching the given filter.
470
+
471
+ To iterate all dealers across pages, use :func:`visor.iter_dealers`
472
+ instead.
473
+
474
+ Args:
475
+ filter: Search criteria and pagination parameters. Defaults to an
476
+ empty filter (first page, no constraints).
477
+
478
+ Returns:
479
+ A :class:`~visor.models.dealers.DealersPage` containing matched
480
+ dealers and pagination metadata.
481
+
482
+ Raises:
483
+ AuthError: Invalid or missing API key (HTTP 401).
484
+ ForbiddenError: Key lacks access to this resource (HTTP 403).
485
+ RateLimitError: Rate limit exceeded; check ``.retry_after``.
486
+ VisorAPIError: Any other API-level error.
487
+ VisorTransportError: Network-level failure.
488
+ """
489
+ params = (filter or DealerFilter()).to_params()
490
+ data = self._transport.get("/dealers", params)
491
+ return DealersPage.model_validate(data)
492
+
493
+ def get_dealer(self, dealer_id: str) -> DealerDetail:
494
+ """Return full detail for a single dealer by its ID.
495
+
496
+ Args:
497
+ dealer_id: The unique dealer identifier.
498
+
499
+ Returns:
500
+ A :class:`~visor.models.dealers.DealerDetail` for the requested
501
+ dealer.
502
+
503
+ Raises:
504
+ NotFoundError: No dealer with that ID exists (HTTP 404).
505
+ AuthError: Invalid or missing API key (HTTP 401).
506
+ ForbiddenError: Key lacks access to this resource (HTTP 403).
507
+ RateLimitError: Rate limit exceeded; check ``.retry_after``.
508
+ VisorAPIError: Any other API-level error.
509
+ VisorTransportError: Network-level failure.
510
+ """
511
+ data = self._transport.get(f"/dealers/{dealer_id}")
512
+ return DealerDetail.model_validate(data["data"])
513
+
514
+ def dealer_inventory(
515
+ self,
516
+ dealer_id: str,
517
+ filter: ListingsFilter | None = None,
518
+ ) -> ListingsPage:
519
+ """Return a single page of inventory for a specific dealer.
520
+
521
+ Accepts the same :class:`~visor.models.listings.ListingsFilter` shape
522
+ as :meth:`filter_listings`, so you can reuse a filter object across
523
+ both methods. Call this method in a loop advancing ``filter.offset`` to
524
+ paginate through all inventory pages.
525
+
526
+ Args:
527
+ dealer_id: The unique dealer identifier.
528
+ filter: Optional search and pagination criteria.
529
+
530
+ Returns:
531
+ A :class:`~visor.models.listings.ListingsPage` of the dealer's
532
+ inventory.
533
+
534
+ Raises:
535
+ NotFoundError: No dealer with that ID exists (HTTP 404).
536
+ AuthError: Invalid or missing API key (HTTP 401).
537
+ ForbiddenError: Key lacks access to this resource (HTTP 403).
538
+ RateLimitError: Rate limit exceeded; check ``.retry_after``.
539
+ VisorAPIError: Any other API-level error.
540
+ VisorTransportError: Network-level failure.
541
+ """
542
+ params = (filter or ListingsFilter()).to_params()
543
+ data = self._transport.get(f"/dealers/{dealer_id}/listings", params)
544
+ return ListingsPage.model_validate(data)
545
+
546
+ # ------------------------------------------------------------------ #
547
+ # Usage #
548
+ # ------------------------------------------------------------------ #
549
+
550
+ def get_usage(
551
+ self,
552
+ start_date: date | None = None,
553
+ end_date: date | None = None,
554
+ metering_class: list[str] | None = None,
555
+ ) -> UsageSummary:
556
+ """Return API usage statistics for your account.
557
+
558
+ All parameters are optional. Omitting date bounds returns the full
559
+ available history. Omitting ``metering_class`` returns all endpoint
560
+ categories.
561
+
562
+ Args:
563
+ start_date: Start of the reporting window (inclusive).
564
+ end_date: End of the reporting window (inclusive).
565
+ metering_class: Endpoint categories to include, e.g.
566
+ ``["listings", "dealers"]``. Returns all categories when
567
+ omitted.
568
+
569
+ Returns:
570
+ A :class:`~visor.models.usage.UsageSummary` with request counts
571
+ and quota details.
572
+
573
+ Raises:
574
+ AuthError: Invalid or missing API key (HTTP 401).
575
+ ForbiddenError: Key lacks access to this resource (HTTP 403).
576
+ RateLimitError: Rate limit exceeded; check ``.retry_after``.
577
+ VisorAPIError: Any other API-level error.
578
+ VisorTransportError: Network-level failure.
579
+ """
580
+ params: dict[str, str] = {}
581
+ if start_date:
582
+ params["start_date"] = start_date.isoformat()
583
+ if end_date:
584
+ params["end_date"] = end_date.isoformat()
585
+ if metering_class:
586
+ params["metering_class"] = ",".join(metering_class)
587
+ data = self._transport.get("/usage", params)
588
+ return UsageSummary.model_validate(data)