cardog 2.1.0__tar.gz → 2.2.0__tar.gz

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.
Files changed (81) hide show
  1. {cardog-2.1.0 → cardog-2.2.0}/CHANGELOG.md +30 -0
  2. {cardog-2.1.0 → cardog-2.2.0}/PKG-INFO +26 -16
  3. {cardog-2.1.0 → cardog-2.2.0}/README.md +25 -15
  4. {cardog-2.1.0 → cardog-2.2.0}/openapi/openapi.v2.json +1165 -231
  5. {cardog-2.1.0 → cardog-2.2.0}/pyproject.toml +1 -1
  6. {cardog-2.1.0 → cardog-2.2.0}/scripts/generate.py +28 -7
  7. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/__init__.py +8 -2
  8. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/_base_client.py +23 -0
  9. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/_client.py +1 -1
  10. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/_constants.py +1 -1
  11. cardog-2.2.0/src/cardog/_credits.py +74 -0
  12. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/_exceptions.py +10 -2
  13. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/v2/__init__.py +5 -0
  14. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/v2/entities.py +8 -2
  15. cardog-2.2.0/src/cardog/v2/feeds.py +107 -0
  16. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/v2/instruments.py +8 -2
  17. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/v2/quotes.py +8 -2
  18. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/v2/recalls.py +8 -2
  19. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/v2/types.py +124 -1
  20. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/v2/vin.py +8 -2
  21. {cardog-2.1.0 → cardog-2.2.0}/tests/conftest.py +1 -1
  22. {cardog-2.1.0 → cardog-2.2.0}/tests/test_client.py +15 -0
  23. {cardog-2.1.0 → cardog-2.2.0}/tests/test_contracts_async.py +4 -4
  24. {cardog-2.1.0 → cardog-2.2.0}/tests/test_contracts_vin.py +4 -4
  25. {cardog-2.1.0 → cardog-2.2.0}/tests/v2/spec_fixtures.py +1 -1
  26. cardog-2.2.0/tests/v2/test_v2_credits.py +125 -0
  27. {cardog-2.1.0 → cardog-2.2.0}/tests/v2/test_v2_live_smoke.py +3 -3
  28. {cardog-2.1.0 → cardog-2.2.0}/tests/v2/test_v2_urls.py +16 -7
  29. {cardog-2.1.0 → cardog-2.2.0}/.gitignore +0 -0
  30. {cardog-2.1.0 → cardog-2.2.0}/Makefile +0 -0
  31. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/_async_client.py +0 -0
  32. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/py.typed +0 -0
  33. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/resources/__init__.py +0 -0
  34. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/resources/charging.py +0 -0
  35. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/resources/complaints.py +0 -0
  36. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/resources/efficiency.py +0 -0
  37. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/resources/fuel.py +0 -0
  38. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/resources/history.py +0 -0
  39. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/resources/listings.py +0 -0
  40. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/resources/locations.py +0 -0
  41. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/resources/market.py +0 -0
  42. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/resources/recalls.py +0 -0
  43. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/resources/research.py +0 -0
  44. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/resources/safety.py +0 -0
  45. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/resources/vin.py +0 -0
  46. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/types/__init__.py +0 -0
  47. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/types/charging.py +0 -0
  48. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/types/common.py +0 -0
  49. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/types/complaints.py +0 -0
  50. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/types/efficiency.py +0 -0
  51. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/types/fuel.py +0 -0
  52. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/types/history.py +0 -0
  53. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/types/listings.py +0 -0
  54. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/types/locations.py +0 -0
  55. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/types/market.py +0 -0
  56. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/types/recalls.py +0 -0
  57. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/types/research.py +0 -0
  58. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/types/safety.py +0 -0
  59. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/types/vin.py +0 -0
  60. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/v2/_support.py +0 -0
  61. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/v2/charging.py +0 -0
  62. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/v2/feedback.py +0 -0
  63. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/v2/listings.py +0 -0
  64. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/v2/platform.py +0 -0
  65. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/v2/safety.py +0 -0
  66. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/v2/specs.py +0 -0
  67. {cardog-2.1.0 → cardog-2.2.0}/src/cardog/v2/tape.py +0 -0
  68. {cardog-2.1.0 → cardog-2.2.0}/tests/__init__.py +0 -0
  69. {cardog-2.1.0 → cardog-2.2.0}/tests/test_contracts_charging.py +0 -0
  70. {cardog-2.1.0 → cardog-2.2.0}/tests/test_contracts_complaints.py +0 -0
  71. {cardog-2.1.0 → cardog-2.2.0}/tests/test_contracts_efficiency.py +0 -0
  72. {cardog-2.1.0 → cardog-2.2.0}/tests/test_contracts_fuel.py +0 -0
  73. {cardog-2.1.0 → cardog-2.2.0}/tests/test_contracts_listings.py +0 -0
  74. {cardog-2.1.0 → cardog-2.2.0}/tests/test_contracts_locations.py +0 -0
  75. {cardog-2.1.0 → cardog-2.2.0}/tests/test_contracts_market.py +0 -0
  76. {cardog-2.1.0 → cardog-2.2.0}/tests/test_contracts_recalls.py +0 -0
  77. {cardog-2.1.0 → cardog-2.2.0}/tests/test_contracts_research.py +0 -0
  78. {cardog-2.1.0 → cardog-2.2.0}/tests/test_contracts_safety.py +0 -0
  79. {cardog-2.1.0 → cardog-2.2.0}/tests/v2/__init__.py +0 -0
  80. {cardog-2.1.0 → cardog-2.2.0}/tests/v2/test_v2_async.py +0 -0
  81. {cardog-2.1.0 → cardog-2.2.0}/tests/v2/test_v2_errors.py +0 -0
@@ -8,6 +8,36 @@ pre-release it tracks. 3.0.0, published to PyPI on 2026-09-28, broke that
8
8
  rule and is yanked: `pip install cardog` no longer resolves to it, and a
9
9
  pin on `==3.0.0` still installs. Everything it carried is in 2.1.0.
10
10
 
11
+ ## [2.2.0] - 2026-09-30
12
+
13
+ Tracks API 2.0.0-rc.6. The SDK ergonomics batch (CRDG-3286). Nothing on the
14
+ wire changes; nothing existing stops working.
15
+
16
+ ### Added
17
+
18
+ - `client.last_credits`: the `X-Credits-*` headers of the last metered
19
+ response, parsed into a `Credits` (`rate`, `remaining`, `allowance`,
20
+ `reset`, `source`), on both `Cardog` and `AsyncCardog`. Errors included: a
21
+ 402 states the budget too. `None` until a metered response arrives, and an
22
+ unmetered response (pricing, a signed link) leaves the last set alone. The
23
+ credits contract says the price of your next call is in the headers of your
24
+ last one; budget-aware callers no longer drop to raw httpx for it.
25
+ - `APIError.message` (and so `CardogAPIError.message`): the API's message on
26
+ its own. `str(err)` is unchanged, `[status] (code) message`, so
27
+ `f"{err.code}: {err.message}"` prints the code once.
28
+ - The sibling names the TypeScript client uses, on the same groups:
29
+ `client.v2.entities.get_by_ref`, `client.v2.instruments.get_by_ref`,
30
+ `client.v2.quotes.get_by_ref`, `client.v2.recalls.get_by_ref` and
31
+ `client.v2.vin.get_by_vin` (for `getByRef` / `getByVin`). The former `get`
32
+ on each group stays callable as an alias.
33
+
34
+ ### Changed
35
+
36
+ - The documented example VIN is `5YJSA1E28MF420053` everywhere (README,
37
+ docstrings, tests). The previous `5YJSA1E26MF420053` fails the check digit
38
+ at position 9; the decoder tolerated it, but an integrator validating check
39
+ digits first rejected our own example.
40
+
11
41
  ## [2.1.0] - 2026-09-30
12
42
 
13
43
  Tracks API 2.0.0-rc.6. Carries what 3.0.0 (yanked) and the unpublished
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cardog
3
- Version: 2.1.0
3
+ Version: 2.2.0
4
4
  Summary: Python SDK for the Cardog API — Canadian VIN decode, vehicle specs, live listings, market quotes, and Transport Canada + NHTSA recalls.
5
5
  Project-URL: Homepage, https://cardog.app
6
6
  Project-URL: Documentation, https://cardog.app/docs
@@ -51,9 +51,9 @@ client = Cardog(api_key=os.environ["CARDOG_API_KEY"])
51
51
  result = client.v2.entities.resolve("2021 Civic")
52
52
  ref = result.best.ref # "model-year:honda/civic/2021"
53
53
 
54
- identity = client.v2.vin.get("2HGFC2F53MH500001")
54
+ identity = client.v2.vin.get_by_vin("2HGFC2F53MH500001")
55
55
  recalls = client.v2.vin.recalls("2HGFC2F53MH500001")
56
- quote = client.v2.quotes.get(ref)
56
+ quote = client.v2.quotes.get_by_ref(ref)
57
57
  ```
58
58
 
59
59
  Canadian VIN decode coverage is 99.77%. Types and methods are generated from the same OpenAPI contract the API validates against — this SDK and the TypeScript one are siblings, not translations.
@@ -78,7 +78,7 @@ best = resolved.best # None when nothing clears the confidence floor
78
78
  print(best.ref) # "model:tesla/model-y"
79
79
 
80
80
  # 2. Dereference a ref: node + parents/children + counts + links
81
- detail = client.v2.entities.get(best.ref)
81
+ detail = client.v2.entities.get_by_ref(best.ref)
82
82
  print(detail.name, detail.counts)
83
83
 
84
84
  # 3. Query with refs — never fuzzy, never guessed
@@ -99,18 +99,18 @@ for listing in results.listings:
99
99
 
100
100
  ```python
101
101
  # VIN → graph identity card (refs, grains, links)
102
- identity = client.v2.vin.get("5YJSA1E26MF420053")
102
+ identity = client.v2.vin.get_by_vin("5YJSA1E28MF420053")
103
103
  print(identity.make, identity.model, identity.refs.model_year)
104
104
 
105
105
  # Batch decode (metered per VIN, max 1000) — one bad VIN fails its own row, never the batch
106
- batch = client.v2.vin.batch(["5YJSA1E26MF420053", "1HGCV1F34LA045661"])
106
+ batch = client.v2.vin.batch(["5YJSA1E28MF420053", "1HGCV1F34LA045661"])
107
107
 
108
108
  # Recalls affecting a VIN (Transport Canada + NHTSA fused)
109
- recalls = client.v2.recalls.vin("5YJSA1E26MF420053")
109
+ recalls = client.v2.recalls.vin("5YJSA1E28MF420053")
110
110
 
111
111
  # VIN → market instrument bridge, quotes, tape
112
- instrument = client.v2.vin.instrument("5YJSA1E26MF420053")
113
- quote = client.v2.quotes.get("model-year:toyota/rav4/2021")
112
+ instrument = client.v2.vin.instrument("5YJSA1E28MF420053")
113
+ quote = client.v2.quotes.get_by_ref("model-year:toyota/rav4/2021")
114
114
  bars = client.v2.tape.history("model-year:toyota/rav4/2021", window="3m")
115
115
  ```
116
116
 
@@ -146,7 +146,7 @@ from cardog import AsyncCardog
146
146
 
147
147
  async def main():
148
148
  client = AsyncCardog(api_key="your-api-key")
149
- identity = await client.v2.vin.get("5YJSA1E26MF420053")
149
+ identity = await client.v2.vin.get_by_vin("5YJSA1E28MF420053")
150
150
  results = await client.v2.listings.search(filters={"makes": ["make:tesla"]})
151
151
  await client.close()
152
152
 
@@ -157,18 +157,28 @@ asyncio.run(main())
157
157
 
158
158
  | Group | Methods |
159
159
  |-------|---------|
160
- | `client.v2.entities` | `browse(domain, ...)` · `resolve(q, ...)` · `get(ref)` |
161
- | `client.v2.vin` | `get(vin)` · `batch(vins)` · `recalls(vin)` · `listings(vin)` · `instrument(vin)` |
160
+ | `client.v2.entities` | `browse(domain, ...)` · `resolve(q, ...)` · `get_by_ref(ref)` |
161
+ | `client.v2.vin` | `get_by_vin(vin)` · `batch(vins)` · `recalls(vin)` · `listings(vin)` · `instrument(vin)` |
162
162
  | `client.v2.specs` | `catalog()` · `sheet(ref, trim=...)` |
163
163
  | `client.v2.listings` | `search(...)` · `count(...)` · `facets(...)` · `by_vin(vin)` · `by_id(id)` |
164
- | `client.v2.instruments` | `search(q=..., limit=...)` · `get(ref, window=...)` |
165
- | `client.v2.quotes` | `get(ref)` · `get_many(refs)` |
164
+ | `client.v2.instruments` | `search(q=..., limit=...)` · `get_by_ref(ref, window=...)` |
165
+ | `client.v2.quotes` | `get_by_ref(ref)` · `get_many(refs)` |
166
166
  | `client.v2.tape` | `live(limit=...)` · `history(ref, window=...)` |
167
- | `client.v2.recalls` | `vin(vin)` · `entity(ref)` · `feed()` · `stats()` · `get(ref)` |
167
+ | `client.v2.recalls` | `vin(vin)` · `entity(ref)` · `feed()` · `stats()` · `get_by_ref(ref)` |
168
168
  | `client.v2.safety` | `ratings(ref)` · `complaints(ref, page=..., limit=...)` |
169
169
  | platform meta | `client.v2.pricing()` · `client.v2.openapi()` (both unauthenticated) |
170
170
 
171
- The machine-readable rate card (`pricing()`) plus the `X-Credits-*` response headers let you budget mid-task; `openapi()` returns the same contract this SDK is generated from.
171
+ The machine-readable rate card (`pricing()`) plus the `X-Credits-*` response headers let you budget mid-task; `openapi()` returns the same contract this SDK is generated from. The client keeps the last set of those headers parsed on `client.last_credits`, errors included (a 402 states the budget too):
172
+
173
+ ```python
174
+ client.v2.vin.get_by_vin("5YJSA1E28MF420053")
175
+ credits = client.last_credits # a Credits, or None before any metered call
176
+ print(credits.rate, credits.remaining, credits.allowance, credits.reset, credits.source)
177
+ # rate: what that call cost · remaining: allowance headroom + pack balance (-1 = custom plan)
178
+ # reset: when the allowance rolls over · source: "allowance" | "pack" | "overage", None when nothing was debited
179
+ ```
180
+
181
+ Errors carry the envelope: `err.code` to branch on, `err.message` on its own, `err.hint` for what to do next. `str(err)` is the log line, `[status] (code) message`.
172
182
 
173
183
  ## v1 (legacy)
174
184
 
@@ -17,9 +17,9 @@ client = Cardog(api_key=os.environ["CARDOG_API_KEY"])
17
17
  result = client.v2.entities.resolve("2021 Civic")
18
18
  ref = result.best.ref # "model-year:honda/civic/2021"
19
19
 
20
- identity = client.v2.vin.get("2HGFC2F53MH500001")
20
+ identity = client.v2.vin.get_by_vin("2HGFC2F53MH500001")
21
21
  recalls = client.v2.vin.recalls("2HGFC2F53MH500001")
22
- quote = client.v2.quotes.get(ref)
22
+ quote = client.v2.quotes.get_by_ref(ref)
23
23
  ```
24
24
 
25
25
  Canadian VIN decode coverage is 99.77%. Types and methods are generated from the same OpenAPI contract the API validates against — this SDK and the TypeScript one are siblings, not translations.
@@ -44,7 +44,7 @@ best = resolved.best # None when nothing clears the confidence floor
44
44
  print(best.ref) # "model:tesla/model-y"
45
45
 
46
46
  # 2. Dereference a ref: node + parents/children + counts + links
47
- detail = client.v2.entities.get(best.ref)
47
+ detail = client.v2.entities.get_by_ref(best.ref)
48
48
  print(detail.name, detail.counts)
49
49
 
50
50
  # 3. Query with refs — never fuzzy, never guessed
@@ -65,18 +65,18 @@ for listing in results.listings:
65
65
 
66
66
  ```python
67
67
  # VIN → graph identity card (refs, grains, links)
68
- identity = client.v2.vin.get("5YJSA1E26MF420053")
68
+ identity = client.v2.vin.get_by_vin("5YJSA1E28MF420053")
69
69
  print(identity.make, identity.model, identity.refs.model_year)
70
70
 
71
71
  # Batch decode (metered per VIN, max 1000) — one bad VIN fails its own row, never the batch
72
- batch = client.v2.vin.batch(["5YJSA1E26MF420053", "1HGCV1F34LA045661"])
72
+ batch = client.v2.vin.batch(["5YJSA1E28MF420053", "1HGCV1F34LA045661"])
73
73
 
74
74
  # Recalls affecting a VIN (Transport Canada + NHTSA fused)
75
- recalls = client.v2.recalls.vin("5YJSA1E26MF420053")
75
+ recalls = client.v2.recalls.vin("5YJSA1E28MF420053")
76
76
 
77
77
  # VIN → market instrument bridge, quotes, tape
78
- instrument = client.v2.vin.instrument("5YJSA1E26MF420053")
79
- quote = client.v2.quotes.get("model-year:toyota/rav4/2021")
78
+ instrument = client.v2.vin.instrument("5YJSA1E28MF420053")
79
+ quote = client.v2.quotes.get_by_ref("model-year:toyota/rav4/2021")
80
80
  bars = client.v2.tape.history("model-year:toyota/rav4/2021", window="3m")
81
81
  ```
82
82
 
@@ -112,7 +112,7 @@ from cardog import AsyncCardog
112
112
 
113
113
  async def main():
114
114
  client = AsyncCardog(api_key="your-api-key")
115
- identity = await client.v2.vin.get("5YJSA1E26MF420053")
115
+ identity = await client.v2.vin.get_by_vin("5YJSA1E28MF420053")
116
116
  results = await client.v2.listings.search(filters={"makes": ["make:tesla"]})
117
117
  await client.close()
118
118
 
@@ -123,18 +123,28 @@ asyncio.run(main())
123
123
 
124
124
  | Group | Methods |
125
125
  |-------|---------|
126
- | `client.v2.entities` | `browse(domain, ...)` · `resolve(q, ...)` · `get(ref)` |
127
- | `client.v2.vin` | `get(vin)` · `batch(vins)` · `recalls(vin)` · `listings(vin)` · `instrument(vin)` |
126
+ | `client.v2.entities` | `browse(domain, ...)` · `resolve(q, ...)` · `get_by_ref(ref)` |
127
+ | `client.v2.vin` | `get_by_vin(vin)` · `batch(vins)` · `recalls(vin)` · `listings(vin)` · `instrument(vin)` |
128
128
  | `client.v2.specs` | `catalog()` · `sheet(ref, trim=...)` |
129
129
  | `client.v2.listings` | `search(...)` · `count(...)` · `facets(...)` · `by_vin(vin)` · `by_id(id)` |
130
- | `client.v2.instruments` | `search(q=..., limit=...)` · `get(ref, window=...)` |
131
- | `client.v2.quotes` | `get(ref)` · `get_many(refs)` |
130
+ | `client.v2.instruments` | `search(q=..., limit=...)` · `get_by_ref(ref, window=...)` |
131
+ | `client.v2.quotes` | `get_by_ref(ref)` · `get_many(refs)` |
132
132
  | `client.v2.tape` | `live(limit=...)` · `history(ref, window=...)` |
133
- | `client.v2.recalls` | `vin(vin)` · `entity(ref)` · `feed()` · `stats()` · `get(ref)` |
133
+ | `client.v2.recalls` | `vin(vin)` · `entity(ref)` · `feed()` · `stats()` · `get_by_ref(ref)` |
134
134
  | `client.v2.safety` | `ratings(ref)` · `complaints(ref, page=..., limit=...)` |
135
135
  | platform meta | `client.v2.pricing()` · `client.v2.openapi()` (both unauthenticated) |
136
136
 
137
- The machine-readable rate card (`pricing()`) plus the `X-Credits-*` response headers let you budget mid-task; `openapi()` returns the same contract this SDK is generated from.
137
+ The machine-readable rate card (`pricing()`) plus the `X-Credits-*` response headers let you budget mid-task; `openapi()` returns the same contract this SDK is generated from. The client keeps the last set of those headers parsed on `client.last_credits`, errors included (a 402 states the budget too):
138
+
139
+ ```python
140
+ client.v2.vin.get_by_vin("5YJSA1E28MF420053")
141
+ credits = client.last_credits # a Credits, or None before any metered call
142
+ print(credits.rate, credits.remaining, credits.allowance, credits.reset, credits.source)
143
+ # rate: what that call cost · remaining: allowance headroom + pack balance (-1 = custom plan)
144
+ # reset: when the allowance rolls over · source: "allowance" | "pack" | "overage", None when nothing was debited
145
+ ```
146
+
147
+ Errors carry the envelope: `err.code` to branch on, `err.message` on its own, `err.hint` for what to do next. `str(err)` is the log line, `[status] (code) message`.
138
148
 
139
149
  ## v1 (legacy)
140
150