workwire 0.0.1a1__tar.gz → 0.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.
@@ -0,0 +1,27 @@
1
+ # Changelog
2
+
3
+ ## [0.2.0](https://github.com/workwire/workwire-python/compare/v0.1.0...v0.2.0) (2026-09-04)
4
+
5
+
6
+ ### Features
7
+
8
+ * honor WORKWIRE_BASE_URL alongside the base_url argument ([#8](https://github.com/workwire/workwire-python/issues/8)) ([41f8982](https://github.com/workwire/workwire-python/commit/41f898267d1352a7353869819eb6bdf59f35ec21))
9
+
10
+
11
+ ### Documentation
12
+
13
+ * note the SDK pins no API version header — none exists, by design ([#11](https://github.com/workwire/workwire-python/issues/11)) ([420756e](https://github.com/workwire/workwire-python/commit/420756ea94d55ee19ffede18edd7eaccec8d5d95))
14
+
15
+ ## 0.1.0 (2026-08-31)
16
+
17
+
18
+ ### Features
19
+
20
+ * retry the 409 idempotency_in_flight after backoff (amended ruling) ([04008a0](https://github.com/workwire/workwire-python/commit/04008a0728d1f5f3d177534898d93960570ddea8))
21
+ * v1 SDK — projects, wires, partnerships on the served contract ([#1](https://github.com/workwire/workwire-python/pull/1))
22
+
23
+
24
+ ### Bug Fixes
25
+
26
+ * apply CodeRabbit review round 1 (6/6 accepted) ([4120c8c](https://github.com/workwire/workwire-python/commit/4120c8cde13833821a516e664735d75717e47e81))
27
+ * derive __version__ from installed metadata — pyproject is the only version literal ([#3](https://github.com/workwire/workwire-python/pull/3))
workwire-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Workwire Inc.
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.
@@ -0,0 +1,236 @@
1
+ Metadata-Version: 2.5
2
+ Name: workwire
3
+ Version: 0.2.0
4
+ Summary: Official Python SDK for the Workwire API
5
+ Project-URL: Homepage, https://github.com/workwire/workwire-python
6
+ Project-URL: Changelog, https://github.com/workwire/workwire-python/blob/main/CHANGELOG.md
7
+ Project-URL: Issues, https://github.com/workwire/workwire-python/issues
8
+ Author-email: Workwire <support@workwire.com>
9
+ License: MIT License
10
+
11
+ Copyright (c) 2026 Workwire Inc.
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
30
+ License-File: LICENSE
31
+ Classifier: Development Status :: 3 - Alpha
32
+ Classifier: Intended Audience :: Developers
33
+ Classifier: License :: OSI Approved :: MIT License
34
+ Classifier: Operating System :: OS Independent
35
+ Classifier: Programming Language :: Python :: 3
36
+ Classifier: Programming Language :: Python :: 3.10
37
+ Classifier: Programming Language :: Python :: 3.11
38
+ Classifier: Programming Language :: Python :: 3.12
39
+ Classifier: Programming Language :: Python :: 3.13
40
+ Classifier: Typing :: Typed
41
+ Requires-Python: >=3.10
42
+ Requires-Dist: httpx<1,>=0.24
43
+ Description-Content-Type: text/markdown
44
+
45
+ # Workwire Python SDK
46
+
47
+ The official Python library for the [Workwire](https://workwire.com) API.
48
+ Create projects, compose and send Wires, and initiate partnerships from your
49
+ own systems — no HTTP calls to write, no idempotency keys to invent, no error
50
+ envelopes to parse, no 429s to handle.
51
+
52
+ ## Install
53
+
54
+ ```bash
55
+ pip install workwire
56
+ ```
57
+
58
+ Until the first stable PyPI release you can also install straight from
59
+ GitHub:
60
+
61
+ ```bash
62
+ pip install git+https://github.com/workwire/workwire-python.git
63
+ ```
64
+
65
+ Python 3.10+. The only runtime dependency is `httpx`.
66
+
67
+ ## Quickstart
68
+
69
+ ```python
70
+ import workwire
71
+
72
+ client = workwire.Workwire() # reads WORKWIRE_API_KEY from the environment
73
+
74
+ project = client.projects.create(name="Elm St. Townhomes")
75
+
76
+ # The partnership with your sub — create one (see below) or pick an
77
+ # established one from client.partnerships.list().
78
+ partnership = client.partnerships.create(sub_org_id="...")
79
+
80
+ wire = client.wires.create(
81
+ project_id=project.id,
82
+ title="Drywall — Bldg A",
83
+ parties=[
84
+ workwire.Party.partnered(
85
+ partnership_id=partnership.id,
86
+ trade="Drywall",
87
+ scope_text="Hang, tape, and finish all interior drywall.",
88
+ contract_value="125000.00",
89
+ ),
90
+ ],
91
+ )
92
+
93
+ client.wires.go_live(wire.id) # emails any cold-invited subs
94
+
95
+ for p in client.partnerships.list().auto_paging_iter():
96
+ print(p.id, p.status)
97
+ ```
98
+
99
+ A party can also cold-invite a sub who isn't on Workwire yet — the
100
+ invitation is minted and emailed at `go_live`:
101
+
102
+ ```python
103
+ workwire.Party.cold_invite(
104
+ recipient_email="estimator@acmedrywall.com",
105
+ recipient_org_name="Acme Drywall",
106
+ trade="Drywall",
107
+ scope_text="North wing.",
108
+ contract_value="80000.00",
109
+ )
110
+ ```
111
+
112
+ The counterparty's acceptance always happens between people in the Workwire
113
+ app — there is deliberately no API call for the other side of the handshake.
114
+
115
+ The quickstart is runnable as a script:
116
+ [`examples/quickstart.py`](https://github.com/workwire/workwire-python/blob/main/examples/quickstart.py)
117
+ drafts the wire above against a partnership you name, and only sends it
118
+ with `--go-live`.
119
+
120
+ ## Authentication
121
+
122
+ An org admin issues an API key in the Workwire dashboard under **Settings →
123
+ API keys**; the secret (`wwk_live_…`) is shown exactly once. Put it in your
124
+ secret manager and export it:
125
+
126
+ ```bash
127
+ export WORKWIRE_API_KEY="wwk_live_..."
128
+ ```
129
+
130
+ `workwire.Workwire()` reads that variable, or takes `api_key=` explicitly.
131
+ The SDK never loads `.env` files itself — if you use one, load it with your
132
+ framework or `python-dotenv` before constructing the client. The client's
133
+ `repr()` and the SDK's errors never contain the key.
134
+
135
+ The base URL works the same way: an explicit `base_url=` argument wins,
136
+ otherwise the `WORKWIRE_BASE_URL` environment variable, otherwise production
137
+ (`https://api.workwire.com`). Point it at a local backend or a staging
138
+ environment without touching code:
139
+
140
+ ```bash
141
+ export WORKWIRE_BASE_URL="http://localhost:8081"
142
+ ```
143
+
144
+ There is no API version to configure: Workwire versions its API in the URL
145
+ path only (`/v1`), with no version header — the SDK sends no API version
146
+ and has none to pin, by design. (Pinning the `workwire` *package* version
147
+ is a separate matter — see Versioning and stability below.)
148
+
149
+ ## Errors
150
+
151
+ Every API failure raises a typed exception from `workwire.errors`, carrying
152
+ the stable machine `code`, human `message`, `details`, HTTP `status`, and the
153
+ `request_id` to quote to support:
154
+
155
+ ```python
156
+ from workwire import errors
157
+
158
+ try:
159
+ client.wires.create(...)
160
+ except errors.ConflictError as e:
161
+ if e.code == "partnership_not_established":
162
+ print("partnership is", e.details["current_state"])
163
+ except errors.InvalidRequestError as e:
164
+ print("fix these fields:", e.fields)
165
+ except errors.RateLimitError as e:
166
+ print(f"over the {e.scope} budget, retry in {e.retry_after}s")
167
+ ```
168
+
169
+ The hierarchy: `WorkwireError` → `APIConnectionError` (network),
170
+ `AuthenticationError` (401), `PermissionError` / `ForbiddenScopeError` (403,
171
+ with `.required_scope`), `NotFoundError` (404 — also another org's
172
+ resources; existence is never confirmed), `InvalidRequestError` (400/413/422,
173
+ with `.fields`), `ConflictError` (409, with `IdempotencyError` for the
174
+ idempotency codes), `RateLimitError` (429), `APIError` (5xx). Key on
175
+ `e.code`, never on the message text.
176
+
177
+ ## Retries and idempotency
178
+
179
+ Every `POST` automatically carries an `Idempotency-Key` (a fresh UUID, or
180
+ pass `idempotency_key=` to control replay across your own process restarts).
181
+ That makes retries safe, so the SDK retries for you: connection errors,
182
+ timeouts, 429s (honoring `Retry-After`), 5xx responses, and the one
183
+ retryable 409 — `idempotency_in_flight`, where the API asks you to retry
184
+ after backoff because the original attempt is still running. All with
185
+ exponential backoff, up to `max_retries` (default 2, configurable on the
186
+ client). Every retry resends the identical bytes under the same key, which
187
+ is exactly what the server's replay detection requires. No other 4xx is
188
+ ever retried.
189
+
190
+ ```python
191
+ client = workwire.Workwire(timeout=10, max_retries=3)
192
+ client.projects.create(name="X", idempotency_key="my-op-42", timeout=60)
193
+ ```
194
+
195
+ ## Pagination
196
+
197
+ `list()` returns one page: `.data`, `.next_cursor`, `.has_more`. Walk
198
+ everything with the iterator — it follows cursors until the server says done:
199
+
200
+ ```python
201
+ for wire in client.wires.list(state="live").auto_paging_iter():
202
+ ...
203
+ ```
204
+
205
+ ## Responses
206
+
207
+ Responses are typed objects with attribute access, `.to_dict()`, and
208
+ `.request_id`. Fields the API adds in the future are preserved and
209
+ reachable, never dropped.
210
+
211
+ ## Raw escape hatch
212
+
213
+ Anything the resource classes don't wrap yet:
214
+
215
+ ```python
216
+ client.request("POST", "/v1/some/new/endpoint", json_body={...})
217
+ ```
218
+
219
+ Same idempotency, retries, and typed errors.
220
+
221
+ ## Versioning and stability
222
+
223
+ The SDK follows semver; the API is versioned in the URL (`/v1/`) and
224
+ additive changes ship without breaking. See
225
+ [CHANGELOG.md](CHANGELOG.md).
226
+
227
+ **Release history note:** the `0.0.1a1` release on PyPI was a yanked
228
+ name-reservation placeholder, not an SDK release. Real release history
229
+ starts at `0.1.0`.
230
+
231
+ **Pre-1.0 note:** while the version is below 1.0, minor releases (`0.x` →
232
+ `0.y`) may include breaking changes to the SDK's Python surface — pin
233
+ `workwire>=0.1,<0.2` (or an exact version) in anything you deploy. The
234
+ wire-level API contract itself is the served OpenAPI document and does not
235
+ break within `/v1`; new response fields may appear at any time, and the SDK
236
+ preserves them rather than dropping them.
@@ -0,0 +1,192 @@
1
+ # Workwire Python SDK
2
+
3
+ The official Python library for the [Workwire](https://workwire.com) API.
4
+ Create projects, compose and send Wires, and initiate partnerships from your
5
+ own systems — no HTTP calls to write, no idempotency keys to invent, no error
6
+ envelopes to parse, no 429s to handle.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ pip install workwire
12
+ ```
13
+
14
+ Until the first stable PyPI release you can also install straight from
15
+ GitHub:
16
+
17
+ ```bash
18
+ pip install git+https://github.com/workwire/workwire-python.git
19
+ ```
20
+
21
+ Python 3.10+. The only runtime dependency is `httpx`.
22
+
23
+ ## Quickstart
24
+
25
+ ```python
26
+ import workwire
27
+
28
+ client = workwire.Workwire() # reads WORKWIRE_API_KEY from the environment
29
+
30
+ project = client.projects.create(name="Elm St. Townhomes")
31
+
32
+ # The partnership with your sub — create one (see below) or pick an
33
+ # established one from client.partnerships.list().
34
+ partnership = client.partnerships.create(sub_org_id="...")
35
+
36
+ wire = client.wires.create(
37
+ project_id=project.id,
38
+ title="Drywall — Bldg A",
39
+ parties=[
40
+ workwire.Party.partnered(
41
+ partnership_id=partnership.id,
42
+ trade="Drywall",
43
+ scope_text="Hang, tape, and finish all interior drywall.",
44
+ contract_value="125000.00",
45
+ ),
46
+ ],
47
+ )
48
+
49
+ client.wires.go_live(wire.id) # emails any cold-invited subs
50
+
51
+ for p in client.partnerships.list().auto_paging_iter():
52
+ print(p.id, p.status)
53
+ ```
54
+
55
+ A party can also cold-invite a sub who isn't on Workwire yet — the
56
+ invitation is minted and emailed at `go_live`:
57
+
58
+ ```python
59
+ workwire.Party.cold_invite(
60
+ recipient_email="estimator@acmedrywall.com",
61
+ recipient_org_name="Acme Drywall",
62
+ trade="Drywall",
63
+ scope_text="North wing.",
64
+ contract_value="80000.00",
65
+ )
66
+ ```
67
+
68
+ The counterparty's acceptance always happens between people in the Workwire
69
+ app — there is deliberately no API call for the other side of the handshake.
70
+
71
+ The quickstart is runnable as a script:
72
+ [`examples/quickstart.py`](https://github.com/workwire/workwire-python/blob/main/examples/quickstart.py)
73
+ drafts the wire above against a partnership you name, and only sends it
74
+ with `--go-live`.
75
+
76
+ ## Authentication
77
+
78
+ An org admin issues an API key in the Workwire dashboard under **Settings →
79
+ API keys**; the secret (`wwk_live_…`) is shown exactly once. Put it in your
80
+ secret manager and export it:
81
+
82
+ ```bash
83
+ export WORKWIRE_API_KEY="wwk_live_..."
84
+ ```
85
+
86
+ `workwire.Workwire()` reads that variable, or takes `api_key=` explicitly.
87
+ The SDK never loads `.env` files itself — if you use one, load it with your
88
+ framework or `python-dotenv` before constructing the client. The client's
89
+ `repr()` and the SDK's errors never contain the key.
90
+
91
+ The base URL works the same way: an explicit `base_url=` argument wins,
92
+ otherwise the `WORKWIRE_BASE_URL` environment variable, otherwise production
93
+ (`https://api.workwire.com`). Point it at a local backend or a staging
94
+ environment without touching code:
95
+
96
+ ```bash
97
+ export WORKWIRE_BASE_URL="http://localhost:8081"
98
+ ```
99
+
100
+ There is no API version to configure: Workwire versions its API in the URL
101
+ path only (`/v1`), with no version header — the SDK sends no API version
102
+ and has none to pin, by design. (Pinning the `workwire` *package* version
103
+ is a separate matter — see Versioning and stability below.)
104
+
105
+ ## Errors
106
+
107
+ Every API failure raises a typed exception from `workwire.errors`, carrying
108
+ the stable machine `code`, human `message`, `details`, HTTP `status`, and the
109
+ `request_id` to quote to support:
110
+
111
+ ```python
112
+ from workwire import errors
113
+
114
+ try:
115
+ client.wires.create(...)
116
+ except errors.ConflictError as e:
117
+ if e.code == "partnership_not_established":
118
+ print("partnership is", e.details["current_state"])
119
+ except errors.InvalidRequestError as e:
120
+ print("fix these fields:", e.fields)
121
+ except errors.RateLimitError as e:
122
+ print(f"over the {e.scope} budget, retry in {e.retry_after}s")
123
+ ```
124
+
125
+ The hierarchy: `WorkwireError` → `APIConnectionError` (network),
126
+ `AuthenticationError` (401), `PermissionError` / `ForbiddenScopeError` (403,
127
+ with `.required_scope`), `NotFoundError` (404 — also another org's
128
+ resources; existence is never confirmed), `InvalidRequestError` (400/413/422,
129
+ with `.fields`), `ConflictError` (409, with `IdempotencyError` for the
130
+ idempotency codes), `RateLimitError` (429), `APIError` (5xx). Key on
131
+ `e.code`, never on the message text.
132
+
133
+ ## Retries and idempotency
134
+
135
+ Every `POST` automatically carries an `Idempotency-Key` (a fresh UUID, or
136
+ pass `idempotency_key=` to control replay across your own process restarts).
137
+ That makes retries safe, so the SDK retries for you: connection errors,
138
+ timeouts, 429s (honoring `Retry-After`), 5xx responses, and the one
139
+ retryable 409 — `idempotency_in_flight`, where the API asks you to retry
140
+ after backoff because the original attempt is still running. All with
141
+ exponential backoff, up to `max_retries` (default 2, configurable on the
142
+ client). Every retry resends the identical bytes under the same key, which
143
+ is exactly what the server's replay detection requires. No other 4xx is
144
+ ever retried.
145
+
146
+ ```python
147
+ client = workwire.Workwire(timeout=10, max_retries=3)
148
+ client.projects.create(name="X", idempotency_key="my-op-42", timeout=60)
149
+ ```
150
+
151
+ ## Pagination
152
+
153
+ `list()` returns one page: `.data`, `.next_cursor`, `.has_more`. Walk
154
+ everything with the iterator — it follows cursors until the server says done:
155
+
156
+ ```python
157
+ for wire in client.wires.list(state="live").auto_paging_iter():
158
+ ...
159
+ ```
160
+
161
+ ## Responses
162
+
163
+ Responses are typed objects with attribute access, `.to_dict()`, and
164
+ `.request_id`. Fields the API adds in the future are preserved and
165
+ reachable, never dropped.
166
+
167
+ ## Raw escape hatch
168
+
169
+ Anything the resource classes don't wrap yet:
170
+
171
+ ```python
172
+ client.request("POST", "/v1/some/new/endpoint", json_body={...})
173
+ ```
174
+
175
+ Same idempotency, retries, and typed errors.
176
+
177
+ ## Versioning and stability
178
+
179
+ The SDK follows semver; the API is versioned in the URL (`/v1/`) and
180
+ additive changes ship without breaking. See
181
+ [CHANGELOG.md](CHANGELOG.md).
182
+
183
+ **Release history note:** the `0.0.1a1` release on PyPI was a yanked
184
+ name-reservation placeholder, not an SDK release. Real release history
185
+ starts at `0.1.0`.
186
+
187
+ **Pre-1.0 note:** while the version is below 1.0, minor releases (`0.x` →
188
+ `0.y`) may include breaking changes to the SDK's Python surface — pin
189
+ `workwire>=0.1,<0.2` (or an exact version) in anything you deploy. The
190
+ wire-level API contract itself is the served OpenAPI document and does not
191
+ break within `/v1`; new response fields may appear at any time, and the SDK
192
+ preserves them rather than dropping them.
@@ -0,0 +1,23 @@
1
+ # Security
2
+
3
+ ## Reporting
4
+
5
+ Email security@workwire.com. Please do not open public issues for security
6
+ reports.
7
+
8
+ ## API key handling
9
+
10
+ Workwire API keys (`wwk_live_…`) are org-scoped bearer secrets. The SDK's
11
+ posture, which your integration should mirror:
12
+
13
+ - The key is read from `api_key=` or `WORKWIRE_API_KEY` and sent only as the
14
+ `Authorization` header to your configured `base_url`.
15
+ - The key never appears in `repr()`, exceptions, logs, or telemetry emitted
16
+ by this SDK, and the SDK never writes it to disk.
17
+ - The SDK does not load `.env` files; your application owns its environment
18
+ loading.
19
+ - Keys are shown once at creation in the Workwire dashboard and are revoked
20
+ there; revocation takes effect on the key's next request.
21
+
22
+ If a key may have been exposed, revoke it in **Settings → API keys** and
23
+ issue a new one.
@@ -0,0 +1,67 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "workwire"
7
+ version = "0.2.0"
8
+ description = "Official Python SDK for the Workwire API"
9
+ readme = "README.md"
10
+ license = { file = "LICENSE" }
11
+ authors = [{ name = "Workwire", email = "support@workwire.com" }]
12
+ requires-python = ">=3.10"
13
+ dependencies = ["httpx>=0.24,<1"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Typing :: Typed",
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/workwire/workwire-python"
29
+ Changelog = "https://github.com/workwire/workwire-python/blob/main/CHANGELOG.md"
30
+ Issues = "https://github.com/workwire/workwire-python/issues"
31
+
32
+ [dependency-groups]
33
+ dev = [
34
+ "pytest>=8",
35
+ "ruff>=0.6",
36
+ "pyyaml>=6",
37
+ "bcrypt>=4", # scripts/local_contract_smoke.py key minting only
38
+ ]
39
+
40
+ [tool.hatch.build.targets.wheel]
41
+ packages = ["src/workwire"]
42
+
43
+ # The published sdist is the library, not the workshop: no repo plumbing,
44
+ # specs, tests, or agent docs (the publish.yml launch checklist's "trim the
45
+ # sdist" item). The wheel is package-only by construction above.
46
+ [tool.hatch.build.targets.sdist]
47
+ only-include = ["src", "README.md", "LICENSE", "CHANGELOG.md", "SECURITY.md", "pyproject.toml"]
48
+
49
+ [tool.ruff]
50
+ line-length = 96
51
+ src = ["src", "tests", "scripts"]
52
+
53
+ [tool.ruff.lint]
54
+ select = ["E", "F", "W", "I", "UP", "B", "S"]
55
+ ignore = [
56
+ "S101", # asserts are fine in tests and as internal invariants
57
+ ]
58
+
59
+ [tool.ruff.lint.per-file-ignores]
60
+ "tests/*" = ["S105", "S106"] # fake keys in tests are not leaked secrets
61
+ # Dev-only smoke harness: psql runs with a fixed argv; values reach SQL
62
+ # only as psql -v variables (:'name' quoting), never spliced into the text.
63
+ "scripts/local_contract_smoke.py" = ["S603", "S607"]
64
+
65
+ [tool.pytest.ini_options]
66
+ testpaths = ["tests"]
67
+ addopts = "-q"