vaquill 0.1.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.
- vaquill-0.1.0/.gitignore +17 -0
- vaquill-0.1.0/LICENSE +21 -0
- vaquill-0.1.0/PKG-INFO +195 -0
- vaquill-0.1.0/README.md +166 -0
- vaquill-0.1.0/docs/CLI.md +103 -0
- vaquill-0.1.0/docs/OPERATIONS.md +89 -0
- vaquill-0.1.0/examples/resolve_citation.py +26 -0
- vaquill-0.1.0/examples/search_and_paginate.py +29 -0
- vaquill-0.1.0/examples/watch_and_verify_webhook.py +42 -0
- vaquill-0.1.0/pyproject.toml +90 -0
- vaquill-0.1.0/src/vaquill/__init__.py +81 -0
- vaquill-0.1.0/src/vaquill/_base_client.py +578 -0
- vaquill-0.1.0/src/vaquill/_client.py +94 -0
- vaquill-0.1.0/src/vaquill/_config.py +22 -0
- vaquill-0.1.0/src/vaquill/_errors.py +207 -0
- vaquill-0.1.0/src/vaquill/_idempotency.py +23 -0
- vaquill-0.1.0/src/vaquill/_pagination.py +182 -0
- vaquill-0.1.0/src/vaquill/_response.py +154 -0
- vaquill-0.1.0/src/vaquill/_retry.py +74 -0
- vaquill-0.1.0/src/vaquill/_version.py +1 -0
- vaquill-0.1.0/src/vaquill/_webhooks.py +109 -0
- vaquill-0.1.0/src/vaquill/cli/__init__.py +5 -0
- vaquill-0.1.0/src/vaquill/cli/_main.py +628 -0
- vaquill-0.1.0/src/vaquill/cli/_render.py +61 -0
- vaquill-0.1.0/src/vaquill/py.typed +0 -0
- vaquill-0.1.0/src/vaquill/resources/__init__.py +29 -0
- vaquill-0.1.0/src/vaquill/resources/_async/__init__.py +1 -0
- vaquill-0.1.0/src/vaquill/resources/_async/boards.py +61 -0
- vaquill-0.1.0/src/vaquill/resources/_async/credits.py +19 -0
- vaquill-0.1.0/src/vaquill/resources/_async/pricing.py +29 -0
- vaquill-0.1.0/src/vaquill/resources/_async/session_laws.py +378 -0
- vaquill-0.1.0/src/vaquill/resources/_async/statutes.py +702 -0
- vaquill-0.1.0/src/vaquill/resources/_async/watches.py +252 -0
- vaquill-0.1.0/src/vaquill/resources/_base.py +97 -0
- vaquill-0.1.0/src/vaquill/resources/boards.py +60 -0
- vaquill-0.1.0/src/vaquill/resources/credits.py +18 -0
- vaquill-0.1.0/src/vaquill/resources/pricing.py +28 -0
- vaquill-0.1.0/src/vaquill/resources/session_laws.py +377 -0
- vaquill-0.1.0/src/vaquill/resources/statutes.py +691 -0
- vaquill-0.1.0/src/vaquill/resources/watches.py +251 -0
- vaquill-0.1.0/src/vaquill/types/__init__.py +434 -0
- vaquill-0.1.0/src/vaquill/types/_models.py +10219 -0
vaquill-0.1.0/.gitignore
ADDED
vaquill-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vaquill
|
|
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.
|
vaquill-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: vaquill
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the Vaquill US primary law Data API.
|
|
5
|
+
Project-URL: Homepage, https://www.vaquill.ai
|
|
6
|
+
Project-URL: Documentation, https://www.vaquill.ai/docs/api-guide/sdks/overview
|
|
7
|
+
Project-URL: Source, https://github.com/Vaquill-AI/vaquill-sdks/tree/main/python
|
|
8
|
+
Project-URL: Issues, https://github.com/Vaquill-AI/vaquill-sdks/issues
|
|
9
|
+
Author: Vaquill
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: api,cli,law,legal,legal-data,regulations,sdk,statutes,vaquill
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.11
|
|
26
|
+
Requires-Dist: httpx<1,>=0.27
|
|
27
|
+
Requires-Dist: pydantic<3,>=2
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# vaquill
|
|
31
|
+
|
|
32
|
+
Python SDK for the Vaquill US primary law Data API: statutes, regulations, court rules, constitutions, session laws and law-change alerts.
|
|
33
|
+
|
|
34
|
+
Server-side use only: an API key (`vq_key_...`) must never ship to a browser.
|
|
35
|
+
Python 3.11 or newer. Sync and async clients with the same methods.
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from vaquill import Vaquill
|
|
39
|
+
|
|
40
|
+
client = Vaquill() # reads VAQUILL_API_KEY; VAQUILL_BASE_URL overrides the host
|
|
41
|
+
|
|
42
|
+
# 2 credits. Even an unresolved citation is charged: the answer is what you pay for.
|
|
43
|
+
result = client.statutes.resolve("42 U.S.C. 1983")
|
|
44
|
+
if result.resolved and result.section:
|
|
45
|
+
body = client.statutes.get_section_body(result.section.act_id) # 6 credits
|
|
46
|
+
print(body.plain)
|
|
47
|
+
|
|
48
|
+
hits = client.statutes.search(
|
|
49
|
+
"statute of limitations for medical malpractice", state="ca"
|
|
50
|
+
) # 4 credits
|
|
51
|
+
for hit in hits.results:
|
|
52
|
+
print(hit.citation, hit.section_title)
|
|
53
|
+
print(hits.credits_consumed, client.credits_spent)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Async is the same with `AsyncVaquill` and `await`:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
import asyncio
|
|
60
|
+
from vaquill import AsyncVaquill
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
async def main() -> None:
|
|
64
|
+
async with AsyncVaquill() as client:
|
|
65
|
+
balance = await client.credits.balance()
|
|
66
|
+
print(balance.credits_remaining)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
asyncio.run(main())
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Namespaces
|
|
73
|
+
|
|
74
|
+
| Namespace | What it does |
|
|
75
|
+
| --- | --- |
|
|
76
|
+
| `client.statutes` | search, resolve, sections and their text, browse, changes, enactments, rulemaking, cross-references, citation extraction |
|
|
77
|
+
| `client.session_laws` | acts as enacted: search, list, record, text, versions, changes |
|
|
78
|
+
| `client.watches` | law-change alerts by webhook or email |
|
|
79
|
+
| `client.boards` | the sources a watch can follow |
|
|
80
|
+
| `client.credits` | `balance()` |
|
|
81
|
+
| `client.pricing` | `get()` the credit price of every endpoint |
|
|
82
|
+
|
|
83
|
+
All 36 operations, their method names and which ones cost credits are in [docs/OPERATIONS.md](docs/OPERATIONS.md).
|
|
84
|
+
Required arguments come first and may be positional; optional ones are keyword-only `snake_case` (sent to the API as camelCase).
|
|
85
|
+
|
|
86
|
+
## Errors
|
|
87
|
+
|
|
88
|
+
Every failure is a `VaquillError`. Status errors carry `status_code`, `request_id` and the parsed body.
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
import vaquill
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
client.statutes.resolve("42 U.S.C. 1983")
|
|
95
|
+
except vaquill.InsufficientCreditsError:
|
|
96
|
+
... # never retried; top up credits
|
|
97
|
+
except vaquill.RateLimitError as err:
|
|
98
|
+
print(err.retry_after_seconds) # seconds; the client already retried with backoff
|
|
99
|
+
except vaquill.APIStatusError as err:
|
|
100
|
+
print(err.status_code, err.request_id)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Pagination and the credit warning
|
|
104
|
+
|
|
105
|
+
`iter_*` helpers hide the offset, cursor and `sinceId`/`beforeId` styles behind one iterator.
|
|
106
|
+
**On a charged endpoint every page spends credits.** Iterating a whole result set can cost far more than one call, so always set `max_pages` or `max_items`.
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
pager = client.statutes.iter_search(
|
|
110
|
+
"negligence", state="ny", limit=10, max_pages=3
|
|
111
|
+
) # at most 12 credits
|
|
112
|
+
for hit in pager:
|
|
113
|
+
print(hit.citation)
|
|
114
|
+
print(pager.truncated) # True when the guard stopped it with more available
|
|
115
|
+
|
|
116
|
+
for page in client.statutes.iter_divisions("STATE", state="wa", max_items=100).pages():
|
|
117
|
+
... # whole pages, to control cost yourself
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Spend guard
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
client = Vaquill(
|
|
124
|
+
max_credits=500
|
|
125
|
+
) # charged calls raise SpendGuardExceeded once 500 credits are used
|
|
126
|
+
client.credits_spent # running total across this client and any with_options() copy
|
|
127
|
+
client.pricing.get() # free: the live price list
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Charged calls send an `Idempotency-Key` automatically and are only retried when that is safe.
|
|
131
|
+
Pass `idempotency_key="..."` to any call to supply your own.
|
|
132
|
+
|
|
133
|
+
## Raw responses
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
raw = client.with_raw_response.request(
|
|
137
|
+
"GET",
|
|
138
|
+
"/api/v1/us/statutes/resolve",
|
|
139
|
+
cast_to=dict,
|
|
140
|
+
params={"cite": "42 U.S.C. 1983"},
|
|
141
|
+
charged=True,
|
|
142
|
+
)
|
|
143
|
+
raw.request_id, raw.rate_limit.remaining, raw.credits_charged_this_call, raw.parse()
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`client.raw_request(method, path, ...)` reaches endpoints newer than this SDK and returns plain JSON.
|
|
147
|
+
|
|
148
|
+
## Webhooks
|
|
149
|
+
|
|
150
|
+
Verify the raw request body, never re-serialised JSON.
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from vaquill import verify_webhook, WebhookVerificationError
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
event = verify_webhook(raw_body, request_headers, WEBHOOK_SECRET, allow_legacy=False)
|
|
157
|
+
except WebhookVerificationError:
|
|
158
|
+
... # respond 400
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
A watch is created with `client.watches.create("state", state="wa", webhook_url=..., webhook_secret=...)`.
|
|
162
|
+
See [examples/](examples/) for runnable versions (`resolve_citation.py`, `search_and_paginate.py`, `watch_and_verify_webhook.py`); the test suite runs them against a fake server.
|
|
163
|
+
|
|
164
|
+
## Command line
|
|
165
|
+
|
|
166
|
+
Installing the package also installs a `vaquill` command (standard library only, no extra dependencies).
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
export VAQUILL_API_KEY=vq_key_...
|
|
170
|
+
vaquill resolve "42 U.S.C. 1983" # 2 credits
|
|
171
|
+
vaquill search "medical malpractice limitations" --state ca --limit 5 --json
|
|
172
|
+
vaquill section USC_42_1983 --body # 6 credits
|
|
173
|
+
vaquill credits # free
|
|
174
|
+
vaquill webhook verify --secret "$SECRET" --id "$ID" --timestamp "$TS" --signature "$SIG" < body.json
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Charged commands print `credits used: N` on stderr, so stdout stays pipeable.
|
|
178
|
+
Exit codes: 0 ok, 1 error, 2 usage, 3 auth, 4 no credits, 5 rate limited, 6 not found, 7 network.
|
|
179
|
+
Every command, flag and the JSON shape are in [docs/CLI.md](docs/CLI.md).
|
|
180
|
+
|
|
181
|
+
## Types
|
|
182
|
+
|
|
183
|
+
Responses are Pydantic v2 models in `vaquill.types`, generated from `openapi/vaquill-us.json` by `scripts/gen_models.py`.
|
|
184
|
+
Unknown fields are kept and unknown enum values never raise: a field typed `Status | str` holds the enum member for values this SDK knows and the plain string for new ones.
|
|
185
|
+
Every top-level response has `credits_consumed`, `request_id` and `idempotent_replayed`.
|
|
186
|
+
|
|
187
|
+
## Development
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
.venv/bin/python scripts/gen_models.py # regenerate src/vaquill/types after changing the spec
|
|
191
|
+
.venv/bin/python scripts/gen_async.py # regenerate src/vaquill/resources/_async after editing a sync resource
|
|
192
|
+
.venv/bin/python -m pytest -q
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
The sync resources in `src/vaquill/resources/` are the one hand-written definition; the async ones are derived from them mechanically and a test fails on drift.
|
vaquill-0.1.0/README.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# vaquill
|
|
2
|
+
|
|
3
|
+
Python SDK for the Vaquill US primary law Data API: statutes, regulations, court rules, constitutions, session laws and law-change alerts.
|
|
4
|
+
|
|
5
|
+
Server-side use only: an API key (`vq_key_...`) must never ship to a browser.
|
|
6
|
+
Python 3.11 or newer. Sync and async clients with the same methods.
|
|
7
|
+
|
|
8
|
+
```python
|
|
9
|
+
from vaquill import Vaquill
|
|
10
|
+
|
|
11
|
+
client = Vaquill() # reads VAQUILL_API_KEY; VAQUILL_BASE_URL overrides the host
|
|
12
|
+
|
|
13
|
+
# 2 credits. Even an unresolved citation is charged: the answer is what you pay for.
|
|
14
|
+
result = client.statutes.resolve("42 U.S.C. 1983")
|
|
15
|
+
if result.resolved and result.section:
|
|
16
|
+
body = client.statutes.get_section_body(result.section.act_id) # 6 credits
|
|
17
|
+
print(body.plain)
|
|
18
|
+
|
|
19
|
+
hits = client.statutes.search(
|
|
20
|
+
"statute of limitations for medical malpractice", state="ca"
|
|
21
|
+
) # 4 credits
|
|
22
|
+
for hit in hits.results:
|
|
23
|
+
print(hit.citation, hit.section_title)
|
|
24
|
+
print(hits.credits_consumed, client.credits_spent)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Async is the same with `AsyncVaquill` and `await`:
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
import asyncio
|
|
31
|
+
from vaquill import AsyncVaquill
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async def main() -> None:
|
|
35
|
+
async with AsyncVaquill() as client:
|
|
36
|
+
balance = await client.credits.balance()
|
|
37
|
+
print(balance.credits_remaining)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
asyncio.run(main())
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Namespaces
|
|
44
|
+
|
|
45
|
+
| Namespace | What it does |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| `client.statutes` | search, resolve, sections and their text, browse, changes, enactments, rulemaking, cross-references, citation extraction |
|
|
48
|
+
| `client.session_laws` | acts as enacted: search, list, record, text, versions, changes |
|
|
49
|
+
| `client.watches` | law-change alerts by webhook or email |
|
|
50
|
+
| `client.boards` | the sources a watch can follow |
|
|
51
|
+
| `client.credits` | `balance()` |
|
|
52
|
+
| `client.pricing` | `get()` the credit price of every endpoint |
|
|
53
|
+
|
|
54
|
+
All 36 operations, their method names and which ones cost credits are in [docs/OPERATIONS.md](docs/OPERATIONS.md).
|
|
55
|
+
Required arguments come first and may be positional; optional ones are keyword-only `snake_case` (sent to the API as camelCase).
|
|
56
|
+
|
|
57
|
+
## Errors
|
|
58
|
+
|
|
59
|
+
Every failure is a `VaquillError`. Status errors carry `status_code`, `request_id` and the parsed body.
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
import vaquill
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
client.statutes.resolve("42 U.S.C. 1983")
|
|
66
|
+
except vaquill.InsufficientCreditsError:
|
|
67
|
+
... # never retried; top up credits
|
|
68
|
+
except vaquill.RateLimitError as err:
|
|
69
|
+
print(err.retry_after_seconds) # seconds; the client already retried with backoff
|
|
70
|
+
except vaquill.APIStatusError as err:
|
|
71
|
+
print(err.status_code, err.request_id)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Pagination and the credit warning
|
|
75
|
+
|
|
76
|
+
`iter_*` helpers hide the offset, cursor and `sinceId`/`beforeId` styles behind one iterator.
|
|
77
|
+
**On a charged endpoint every page spends credits.** Iterating a whole result set can cost far more than one call, so always set `max_pages` or `max_items`.
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
pager = client.statutes.iter_search(
|
|
81
|
+
"negligence", state="ny", limit=10, max_pages=3
|
|
82
|
+
) # at most 12 credits
|
|
83
|
+
for hit in pager:
|
|
84
|
+
print(hit.citation)
|
|
85
|
+
print(pager.truncated) # True when the guard stopped it with more available
|
|
86
|
+
|
|
87
|
+
for page in client.statutes.iter_divisions("STATE", state="wa", max_items=100).pages():
|
|
88
|
+
... # whole pages, to control cost yourself
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Spend guard
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
client = Vaquill(
|
|
95
|
+
max_credits=500
|
|
96
|
+
) # charged calls raise SpendGuardExceeded once 500 credits are used
|
|
97
|
+
client.credits_spent # running total across this client and any with_options() copy
|
|
98
|
+
client.pricing.get() # free: the live price list
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Charged calls send an `Idempotency-Key` automatically and are only retried when that is safe.
|
|
102
|
+
Pass `idempotency_key="..."` to any call to supply your own.
|
|
103
|
+
|
|
104
|
+
## Raw responses
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
raw = client.with_raw_response.request(
|
|
108
|
+
"GET",
|
|
109
|
+
"/api/v1/us/statutes/resolve",
|
|
110
|
+
cast_to=dict,
|
|
111
|
+
params={"cite": "42 U.S.C. 1983"},
|
|
112
|
+
charged=True,
|
|
113
|
+
)
|
|
114
|
+
raw.request_id, raw.rate_limit.remaining, raw.credits_charged_this_call, raw.parse()
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`client.raw_request(method, path, ...)` reaches endpoints newer than this SDK and returns plain JSON.
|
|
118
|
+
|
|
119
|
+
## Webhooks
|
|
120
|
+
|
|
121
|
+
Verify the raw request body, never re-serialised JSON.
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from vaquill import verify_webhook, WebhookVerificationError
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
event = verify_webhook(raw_body, request_headers, WEBHOOK_SECRET, allow_legacy=False)
|
|
128
|
+
except WebhookVerificationError:
|
|
129
|
+
... # respond 400
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
A watch is created with `client.watches.create("state", state="wa", webhook_url=..., webhook_secret=...)`.
|
|
133
|
+
See [examples/](examples/) for runnable versions (`resolve_citation.py`, `search_and_paginate.py`, `watch_and_verify_webhook.py`); the test suite runs them against a fake server.
|
|
134
|
+
|
|
135
|
+
## Command line
|
|
136
|
+
|
|
137
|
+
Installing the package also installs a `vaquill` command (standard library only, no extra dependencies).
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
export VAQUILL_API_KEY=vq_key_...
|
|
141
|
+
vaquill resolve "42 U.S.C. 1983" # 2 credits
|
|
142
|
+
vaquill search "medical malpractice limitations" --state ca --limit 5 --json
|
|
143
|
+
vaquill section USC_42_1983 --body # 6 credits
|
|
144
|
+
vaquill credits # free
|
|
145
|
+
vaquill webhook verify --secret "$SECRET" --id "$ID" --timestamp "$TS" --signature "$SIG" < body.json
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Charged commands print `credits used: N` on stderr, so stdout stays pipeable.
|
|
149
|
+
Exit codes: 0 ok, 1 error, 2 usage, 3 auth, 4 no credits, 5 rate limited, 6 not found, 7 network.
|
|
150
|
+
Every command, flag and the JSON shape are in [docs/CLI.md](docs/CLI.md).
|
|
151
|
+
|
|
152
|
+
## Types
|
|
153
|
+
|
|
154
|
+
Responses are Pydantic v2 models in `vaquill.types`, generated from `openapi/vaquill-us.json` by `scripts/gen_models.py`.
|
|
155
|
+
Unknown fields are kept and unknown enum values never raise: a field typed `Status | str` holds the enum member for values this SDK knows and the plain string for new ones.
|
|
156
|
+
Every top-level response has `credits_consumed`, `request_id` and `idempotent_replayed`.
|
|
157
|
+
|
|
158
|
+
## Development
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
.venv/bin/python scripts/gen_models.py # regenerate src/vaquill/types after changing the spec
|
|
162
|
+
.venv/bin/python scripts/gen_async.py # regenerate src/vaquill/resources/_async after editing a sync resource
|
|
163
|
+
.venv/bin/python -m pytest -q
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The sync resources in `src/vaquill/resources/` are the one hand-written definition; the async ones are derived from them mechanically and a test fails on drift.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Command line
|
|
2
|
+
|
|
3
|
+
`vaquill` is installed with the package.
|
|
4
|
+
It uses only the standard library and the SDK client, so it adds no dependencies.
|
|
5
|
+
Run `vaquill COMMAND --help` for the flags of any command.
|
|
6
|
+
|
|
7
|
+
## Authentication and global options
|
|
8
|
+
|
|
9
|
+
The key comes from `--api-key` or the `VAQUILL_API_KEY` variable.
|
|
10
|
+
It is never stored on disk and never printed: error text and `--debug` logs have any `vq_key_` value replaced with `vq_key_[redacted]`.
|
|
11
|
+
|
|
12
|
+
| Option | Meaning |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| `--api-key KEY` | API key. Prefer the variable, because a flag is visible in the process list. |
|
|
15
|
+
| `--base-url URL` | API host. Defaults to `VAQUILL_BASE_URL`, then `https://api.vaquill.ai`. |
|
|
16
|
+
| `--timeout SECONDS` | Per-request timeout. |
|
|
17
|
+
| `--max-retries N` | Retries after the first attempt (default 2). |
|
|
18
|
+
| `--max-credits N` | Spend guard: a charged call is refused once N credits have been used in this run. |
|
|
19
|
+
| `--json` | Print the response as JSON on stdout. |
|
|
20
|
+
| `--no-color` | Never use colour. |
|
|
21
|
+
| `--debug` | Log requests to stderr and show a traceback on failure. |
|
|
22
|
+
|
|
23
|
+
Global options work before or after the command name.
|
|
24
|
+
|
|
25
|
+
## Commands
|
|
26
|
+
|
|
27
|
+
| Command | What it does | Cost |
|
|
28
|
+
| --- | --- | --- |
|
|
29
|
+
| `search QUERY [--corpus-type T ...] [--state S] [--limit N] [--offset N]` | Search statutes, regulations and rules | charged |
|
|
30
|
+
| `resolve CITATION` | Resolve a citation to its section | charged |
|
|
31
|
+
| `section ACT_ID [--body]` | A section's metadata, or its text with `--body` | charged |
|
|
32
|
+
| `coverage` | What the corpus holds per jurisdiction | free |
|
|
33
|
+
| `credits` | Your credit balance | free |
|
|
34
|
+
| `pricing` | The credit price of every endpoint | free |
|
|
35
|
+
| `watches list [--limit N] [--offset N]` | List watches | free |
|
|
36
|
+
| `watches create CORPUS_TYPE [--state S] [--channel C] [--webhook-url U] [--webhook-secret S] [--email E]` | Create a watch | free |
|
|
37
|
+
| `watches delete WATCH_ID` | Delete a watch | free |
|
|
38
|
+
| `watches changes WATCH_ID [--limit N] [--since-id N] [--before-id N] [--order asc\|desc]` | Changes a watch recorded | free |
|
|
39
|
+
| `watches deliveries WATCH_ID [--limit N]` | Recent deliveries and delivery health | free |
|
|
40
|
+
| `webhook verify --secret S --id I --timestamp T --signature SIG [--file F] [--tolerance N]` | Verify a Standard Webhooks signature offline | none |
|
|
41
|
+
| `version` | Print the SDK and Python versions | none |
|
|
42
|
+
| `api METHOD PATH [--query K=V ...] [--data JSON] [--limit N] [--charged]` | Call any endpoint and print the JSON | see below |
|
|
43
|
+
|
|
44
|
+
Prices are not repeated here: run `vaquill pricing` for the live table.
|
|
45
|
+
|
|
46
|
+
## Output
|
|
47
|
+
|
|
48
|
+
Human output is a compact table or a list of `key value` lines, clipped to the terminal width.
|
|
49
|
+
Colour is used only when stdout (or stderr, for messages) is a terminal and `NO_COLOR` is unset and `--no-color` is absent.
|
|
50
|
+
|
|
51
|
+
`--json` prints the response model dumped by alias, so the keys are the API's camelCase names (`creditsConsumed`, `actId`).
|
|
52
|
+
The shape is the same as the SDK's `model_dump(mode="json", by_alias=True)`.
|
|
53
|
+
`watches delete` prints `{"deleted": true, "id": "..."}`, `version` prints `{"version": ..., "python": ...}` and `webhook verify` prints `{"valid": true, "event": ...}`.
|
|
54
|
+
|
|
55
|
+
Messages never go to stdout.
|
|
56
|
+
Charged commands print `credits used: N` on stderr after the result, so `vaquill search ... --json | jq` stays clean.
|
|
57
|
+
|
|
58
|
+
## Pages and cost
|
|
59
|
+
|
|
60
|
+
The CLI never paginates or loops by itself.
|
|
61
|
+
`search`, `watches list` and `watches changes` make exactly one request.
|
|
62
|
+
When the server reports more results, a notice on stderr names the flag that continues (`--offset N`, `--before-id N`, `--since-id N`).
|
|
63
|
+
Each continued search call is charged again.
|
|
64
|
+
|
|
65
|
+
## The `api` command
|
|
66
|
+
|
|
67
|
+
`vaquill api GET /api/v1/watches --query limit=5` sends one request and prints the JSON body.
|
|
68
|
+
`--query` can repeat, and a repeated key becomes a repeated query parameter.
|
|
69
|
+
`--data` takes a JSON string, or `-` to read it from stdin.
|
|
70
|
+
`--limit N` sets the `limit` query parameter.
|
|
71
|
+
The CLI cannot know which paths are charged, so pass `--charged` for one that is.
|
|
72
|
+
That adds an idempotency key, applies `--max-credits` and counts the credits in `credits used`.
|
|
73
|
+
When the body says `hasMore`, a notice names the next page value if the body has one (`nextCursor` or `nextOffset`).
|
|
74
|
+
|
|
75
|
+
## Webhook verification
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
vaquill webhook verify --secret "$WEBHOOK_SECRET" \
|
|
79
|
+
--id "$WEBHOOK_ID" --timestamp "$WEBHOOK_TIMESTAMP" --signature "$WEBHOOK_SIGNATURE" \
|
|
80
|
+
--file body.json
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
It makes no network call and needs no API key.
|
|
84
|
+
The body must be the exact bytes received: a trailing newline added by `echo` makes the signature fail, so use `--file`, a redirect (`< body.json`) or `printf %s`.
|
|
85
|
+
`--secret` falls back to the `VAQUILL_WEBHOOK_SECRET` variable.
|
|
86
|
+
Timestamps more than `--tolerance` seconds (default 300) from the current time are rejected.
|
|
87
|
+
Only Standard Webhooks headers are accepted; the legacy header has no replay protection.
|
|
88
|
+
The signature is checked before the body is parsed, so a body that is not a Vaquill event still passes with `"event": null`.
|
|
89
|
+
|
|
90
|
+
## Exit codes
|
|
91
|
+
|
|
92
|
+
| Code | Meaning |
|
|
93
|
+
| --- | --- |
|
|
94
|
+
| 0 | Success |
|
|
95
|
+
| 1 | Any other error, including a failed webhook verification |
|
|
96
|
+
| 2 | Usage error (bad flag, unreadable file, invalid JSON) |
|
|
97
|
+
| 3 | Authentication: missing or malformed key, HTTP 401 or 403 |
|
|
98
|
+
| 4 | Insufficient credits (HTTP 402) or the `--max-credits` guard |
|
|
99
|
+
| 5 | Rate limited, including the daily cap (HTTP 429) |
|
|
100
|
+
| 6 | Not found (HTTP 404) |
|
|
101
|
+
| 7 | Network failure or timeout |
|
|
102
|
+
|
|
103
|
+
A failure prints one line to stderr, `error: MESSAGE (status N, code C, request id R)`, and no traceback unless `--debug` is set.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Operations
|
|
2
|
+
|
|
3
|
+
All 36 operations of the frozen contract (`openapi/vaquill-us.json`, `info.version` 1.0.0), the SDK method for each, and which ones spend credits.
|
|
4
|
+
Each method exists on `Vaquill` and, with `await`, on `AsyncVaquill`.
|
|
5
|
+
`tests/test_operations.py` fails if this table and the spec disagree.
|
|
6
|
+
|
|
7
|
+
## Charged rule
|
|
8
|
+
|
|
9
|
+
An operation is **charged** when the spec declares the `Idempotency-Key` header parameter on it.
|
|
10
|
+
That is 25 operations.
|
|
11
|
+
The SDK passes `charged=True` for exactly those: it sends an auto-generated `Idempotency-Key`, retries ambiguous failures safely, checks the `max_credits` spend guard before the call and adds the response's `creditsConsumed` to `client.credits_spent` after it.
|
|
12
|
+
The TypeScript SDK applies the same rule.
|
|
13
|
+
|
|
14
|
+
Free operations: `list_statutes_coverage`, `list_boards`, the watches operations except `get_watch_change_diff`, `get_credit_balance` and `get_pricing`.
|
|
15
|
+
Prices are not hard-coded here. Call `client.pricing.get()` (free) for the live matrix.
|
|
16
|
+
Some charged calls return `creditsConsumed: 0` (for example a body that is not held); read the figure from the response.
|
|
17
|
+
|
|
18
|
+
## Naming
|
|
19
|
+
|
|
20
|
+
The method name is the `operationId` with the namespace noun removed (`search_statutes` is `statutes.search`, `get_session_law_body` is `session_laws.get_body`).
|
|
21
|
+
Where the verb carries meaning it is kept (`get_section`, `get_sections_batch`).
|
|
22
|
+
`get_section_neighbors` is `statutes.neighbors` (the path is `/related`).
|
|
23
|
+
|
|
24
|
+
## Call shape
|
|
25
|
+
|
|
26
|
+
* Required parameters (path parameters first, then required query or body members) come first and may be passed by position.
|
|
27
|
+
* Optional parameters are keyword-only, in `snake_case`. The wire name is the API's camelCase (`title_number` is sent as `titleNumber`).
|
|
28
|
+
* Unset optional parameters are not sent, so the server default applies.
|
|
29
|
+
* Every method also accepts `timeout`, `max_retries`, `idempotency_key` and `headers` as keyword arguments.
|
|
30
|
+
* Array parameters take a list. Query arrays are sent as repeated keys.
|
|
31
|
+
|
|
32
|
+
## Methods
|
|
33
|
+
|
|
34
|
+
| operationId | SDK method | Route | Cost |
|
|
35
|
+
| --- | --- | --- | --- |
|
|
36
|
+
| `get_section_cited_by` | `client.statutes.cited_by` | GET `/us/statutes/section/{act_id}/cited-by` | charged |
|
|
37
|
+
| `count_statute_sections` | `client.statutes.count` | POST `/us/statutes/count` | charged |
|
|
38
|
+
| `list_statutes_coverage` | `client.statutes.coverage` | GET `/us/statutes/coverage` | free |
|
|
39
|
+
| `get_section_cross_state` | `client.statutes.cross_state` | GET `/us/statutes/section/{act_id}/cross-state` | charged |
|
|
40
|
+
| `get_section_definitions` | `client.statutes.definitions` | GET `/us/statutes/section/{act_id}/definitions` | charged |
|
|
41
|
+
| `list_statute_divisions` | `client.statutes.divisions` | GET `/us/statutes/divisions` | charged |
|
|
42
|
+
| `get_section_enactments` | `client.statutes.enactments` | GET `/us/statutes/section/{act_id}/enactments` | charged |
|
|
43
|
+
| `get_section_rulemaking` | `client.statutes.section_rulemaking` | GET `/us/statutes/section/{act_id}/rulemaking` | charged |
|
|
44
|
+
| `get_part_rulemaking` | `client.statutes.part_rulemaking` | GET `/us/statutes/part/{title}/{part}/rulemaking` | charged |
|
|
45
|
+
| `extract_statute_citations` | `client.statutes.extract_citations` | POST `/us/statutes/extract-citations` | charged |
|
|
46
|
+
| `get_section` | `client.statutes.get_section` | GET `/us/statutes/section/{act_id}` | charged |
|
|
47
|
+
| `get_section_body` | `client.statutes.get_section_body` | GET `/us/statutes/section/{act_id}/body` | charged |
|
|
48
|
+
| `get_sections_batch` | `client.statutes.get_sections_batch` | POST `/us/statutes/sections` | charged |
|
|
49
|
+
| `get_section_neighbors` | `client.statutes.neighbors` | GET `/us/statutes/section/{act_id}/related` | charged |
|
|
50
|
+
| `resolve_statute_citation` | `client.statutes.resolve` | GET `/us/statutes/resolve` | charged |
|
|
51
|
+
| `resolve_statute_citations_batch` | `client.statutes.resolve_batch` | POST `/us/statutes/resolve` | charged |
|
|
52
|
+
| `search_statutes` | `client.statutes.search` | POST `/us/statutes/search` | charged |
|
|
53
|
+
| `get_section_changes` | `client.statutes.section_changes` | GET `/us/statutes/section/{act_id}/changes` | charged |
|
|
54
|
+
| `get_session_law_changes` | `client.session_laws.changes` | GET `/us/session-laws/changes` | charged |
|
|
55
|
+
| `get_session_law` | `client.session_laws.get` | GET `/us/session-laws/{session_law_id}` | charged |
|
|
56
|
+
| `get_session_laws_batch` | `client.session_laws.get_batch` | POST `/us/session-laws/batch` | charged |
|
|
57
|
+
| `get_session_law_body` | `client.session_laws.get_body` | GET `/us/session-laws/{session_law_id}/body` | charged |
|
|
58
|
+
| `list_session_laws` | `client.session_laws.list` | GET `/us/session-laws/list` | charged |
|
|
59
|
+
| `search_session_laws` | `client.session_laws.search` | POST `/us/session-laws/search` | charged |
|
|
60
|
+
| `get_session_law_versions` | `client.session_laws.versions` | GET `/us/session-laws/{session_law_id}/versions` | charged |
|
|
61
|
+
| `get_watch_change_diff` | `client.watches.change_diff` | GET `/watches/{watch_id}/changes/{change_id}/diff` | charged |
|
|
62
|
+
| `list_watch_changes` | `client.watches.changes` | GET `/watches/{watch_id}/changes` | free |
|
|
63
|
+
| `create_watch` | `client.watches.create` | POST `/watches` | free |
|
|
64
|
+
| `delete_watch` | `client.watches.delete` | DELETE `/watches/{watch_id}` | free |
|
|
65
|
+
| `list_watch_deliveries` | `client.watches.deliveries` | GET `/watches/{watch_id}/deliveries` | free |
|
|
66
|
+
| `list_watches` | `client.watches.list` | GET `/watches` | free |
|
|
67
|
+
| `test_watch` | `client.watches.test` | POST `/watches/{watch_id}/test` | free |
|
|
68
|
+
| `update_watch` | `client.watches.update` | PATCH `/watches/{watch_id}` | free |
|
|
69
|
+
| `list_boards` | `client.boards.list` | GET `/boards` | free |
|
|
70
|
+
| `get_credit_balance` | `client.credits.balance` | GET `/credits/balance` | free |
|
|
71
|
+
| `get_pricing` | `client.pricing.get` | GET `/api-credits/pricing` | free |
|
|
72
|
+
|
|
73
|
+
## Auto-pagination
|
|
74
|
+
|
|
75
|
+
Each helper returns a `SyncPaginator` (or `AsyncPaginator`).
|
|
76
|
+
Iterate it for items, call `.pages()` for whole pages, read `.truncated` after iterating.
|
|
77
|
+
All accept `max_pages` and `max_items`. On a charged helper every page spends credits, so always set one of them.
|
|
78
|
+
|
|
79
|
+
| Helper | Wraps | Paging | Cost |
|
|
80
|
+
| --- | --- | --- | --- |
|
|
81
|
+
| `client.statutes.iter_search` | `search_statutes` | offset (`nextOffset`) | charged |
|
|
82
|
+
| `client.statutes.iter_divisions` | `list_statute_divisions` | cursor (`nextCursor`) | charged |
|
|
83
|
+
| `client.statutes.iter_section_changes` | `get_section_changes` | `beforeId` (newest first) or `sinceId` (`order="asc"`) | charged |
|
|
84
|
+
| `client.session_laws.iter_search` | `search_session_laws` | offset (`hasMore`) | charged |
|
|
85
|
+
| `client.session_laws.iter_list` | `list_session_laws` | cursor (`nextCursor`) | charged |
|
|
86
|
+
| `client.session_laws.iter_changes` | `get_session_law_changes` | `sinceId` | charged |
|
|
87
|
+
| `client.watches.iter_list` | `list_watches` | offset | free |
|
|
88
|
+
| `client.watches.iter_changes` | `list_watch_changes` | `beforeId` or `sinceId` (`order="asc"`) | free |
|
|
89
|
+
| `client.boards.iter_list` | `list_boards` | offset (`hasMore`) | free |
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Resolve a citation to its section, then read the section text.
|
|
2
|
+
|
|
3
|
+
VAQUILL_API_KEY=vq_key_... python examples/resolve_citation.py
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from vaquill import InsufficientCreditsError, Vaquill
|
|
9
|
+
from vaquill.types import StatuteResolveResponse
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main(client: Vaquill, cite: str = "42 U.S.C. 1983") -> StatuteResolveResponse | None:
|
|
13
|
+
try:
|
|
14
|
+
result = client.statutes.resolve(cite) # 2 credits, even when it does not resolve
|
|
15
|
+
except InsufficientCreditsError:
|
|
16
|
+
print("Out of credits. Top up in the developer dashboard.")
|
|
17
|
+
return None
|
|
18
|
+
print(f"{cite}: resolved={result.resolved} charged={result.credits_consumed} credits")
|
|
19
|
+
if result.resolved and result.section is not None and result.section.act_id:
|
|
20
|
+
body = client.statutes.get_section_body(result.section.act_id) # 6 credits
|
|
21
|
+
print((body.plain or body.content or "")[:200])
|
|
22
|
+
return result
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
if __name__ == "__main__":
|
|
26
|
+
main(Vaquill(max_credits=20))
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Search, then page through results with a cost guard.
|
|
2
|
+
|
|
3
|
+
Every page of a charged endpoint spends credits, so always bound the iteration.
|
|
4
|
+
|
|
5
|
+
VAQUILL_API_KEY=vq_key_... python examples/search_and_paginate.py
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from vaquill import Vaquill
|
|
11
|
+
from vaquill.types import StatuteResult
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main(client: Vaquill) -> list[StatuteResult]:
|
|
15
|
+
first = client.statutes.search("statute of limitations for medical malpractice", state="ca")
|
|
16
|
+
rows_on_page = len(first.results or [])
|
|
17
|
+
print(f"{rows_on_page} rows on page one, cost {first.credits_consumed} credits")
|
|
18
|
+
|
|
19
|
+
# 2 pages x 4 credits at most. `truncated` tells you more existed.
|
|
20
|
+
pager = client.statutes.iter_search(
|
|
21
|
+
"statute of limitations for medical malpractice", state="ca", limit=10, max_pages=2
|
|
22
|
+
)
|
|
23
|
+
rows = list(pager)
|
|
24
|
+
print(f"read {len(rows)} rows, truncated={pager.truncated}, spent={client.credits_spent}")
|
|
25
|
+
return rows
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
if __name__ == "__main__":
|
|
29
|
+
main(Vaquill(max_credits=50))
|