w6w 0.2.1__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.
- w6w-0.2.1/LICENSE +21 -0
- w6w-0.2.1/PKG-INFO +350 -0
- w6w-0.2.1/README.md +322 -0
- w6w-0.2.1/pyproject.toml +71 -0
- w6w-0.2.1/setup.cfg +4 -0
- w6w-0.2.1/src/w6w/__init__.py +131 -0
- w6w-0.2.1/src/w6w/_config.py +236 -0
- w6w-0.2.1/src/w6w/_env.py +84 -0
- w6w-0.2.1/src/w6w/_http.py +377 -0
- w6w-0.2.1/src/w6w/_vars.py +236 -0
- w6w-0.2.1/src/w6w/_version.py +15 -0
- w6w-0.2.1/src/w6w/client.py +210 -0
- w6w-0.2.1/src/w6w/connections.py +109 -0
- w6w-0.2.1/src/w6w/documents.py +303 -0
- w6w-0.2.1/src/w6w/errors.py +119 -0
- w6w-0.2.1/src/w6w/me.py +103 -0
- w6w-0.2.1/src/w6w/py.typed +1 -0
- w6w-0.2.1/src/w6w/run.py +138 -0
- w6w-0.2.1/src/w6w/types.py +912 -0
- w6w-0.2.1/src/w6w/workflows.py +260 -0
- w6w-0.2.1/src/w6w.egg-info/PKG-INFO +350 -0
- w6w-0.2.1/src/w6w.egg-info/SOURCES.txt +33 -0
- w6w-0.2.1/src/w6w.egg-info/dependency_links.txt +1 -0
- w6w-0.2.1/src/w6w.egg-info/requires.txt +3 -0
- w6w-0.2.1/src/w6w.egg-info/top_level.txt +1 -0
- w6w-0.2.1/tests/test_config.py +356 -0
- w6w-0.2.1/tests/test_discovery.py +424 -0
- w6w-0.2.1/tests/test_documents.py +444 -0
- w6w-0.2.1/tests/test_http.py +582 -0
- w6w-0.2.1/tests/test_me.py +319 -0
- w6w-0.2.1/tests/test_run.py +281 -0
- w6w-0.2.1/tests/test_surface.py +323 -0
- w6w-0.2.1/tests/test_vars.py +427 -0
- w6w-0.2.1/tests/test_version.py +133 -0
- w6w-0.2.1/tests/test_workflows_run.py +345 -0
w6w-0.2.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 w6w-io
|
|
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.
|
w6w-0.2.1/PKG-INFO
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: w6w
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Typed, dependency-free Python client for the w6w workflow API.
|
|
5
|
+
Author: w6w
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/w6w-io/w6w-wrappers/tree/main/python
|
|
8
|
+
Project-URL: Repository, https://github.com/w6w-io/w6w-wrappers
|
|
9
|
+
Project-URL: Issues, https://github.com/w6w-io/w6w-wrappers/issues
|
|
10
|
+
Keywords: w6w,workflow,automation,api,client,sdk
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: coverage>=7.0; extra == "dev"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# w6w
|
|
30
|
+
|
|
31
|
+
Typed Python client for the w6w workflow API.
|
|
32
|
+
|
|
33
|
+
It is a thin wrapper: transport, auth, error mapping. Documents, variables,
|
|
34
|
+
connections, workflows and runs — the same surface the Node SDK and the `w6w`
|
|
35
|
+
CLI expose, at the same version. All three wrappers implement one shared
|
|
36
|
+
contract and release together, so `w6w==X` gives you the same operations as
|
|
37
|
+
`@w6w/sdk@X`.
|
|
38
|
+
|
|
39
|
+
**Status: pre-release.** The full surface is in — `me`, `run`,
|
|
40
|
+
`connections.list`, `workflows.list` / `workflows.run`, and the complete
|
|
41
|
+
`documents.*` and `vars.*` CRUD. Below `1.0.0`, breaking changes may arrive in a
|
|
42
|
+
minor bump — that grace ends at `1.0.0`.
|
|
43
|
+
|
|
44
|
+
MIT licensed — see [LICENSE](./LICENSE).
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install w6w
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Python 3.9 or newer.
|
|
53
|
+
|
|
54
|
+
## Zero runtime dependencies
|
|
55
|
+
|
|
56
|
+
`dependencies` in `pyproject.toml` is empty and stays empty. HTTP goes through
|
|
57
|
+
`urllib.request` from the standard library — no `requests`, no `httpx`, no
|
|
58
|
+
`aiohttp`.
|
|
59
|
+
|
|
60
|
+
Two reasons, in order of importance:
|
|
61
|
+
|
|
62
|
+
1. **A client library should never join a dependency fight.** Installing this
|
|
63
|
+
package cannot conflict with the versions you have already pinned.
|
|
64
|
+
2. **It is the only honest choice for how this package is developed.** The
|
|
65
|
+
environment it is built in has no package installer available at all, so a
|
|
66
|
+
third-party HTTP or test dependency would make the package untestable by the
|
|
67
|
+
people writing it.
|
|
68
|
+
|
|
69
|
+
The package is fully annotated and ships a [PEP 561](https://peps.python.org/pep-0561/)
|
|
70
|
+
`py.typed` marker, so mypy and pyright see the types without a stub package.
|
|
71
|
+
|
|
72
|
+
## Usage
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from w6w import W6wClient, ApiError
|
|
76
|
+
|
|
77
|
+
client = W6wClient() # from the environment
|
|
78
|
+
# …or explicitly, which always wins over the environment:
|
|
79
|
+
client = W6wClient(base_url="https://api.example.com", token="tok_…")
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
identity = client.me() # who am I, and what answered?
|
|
83
|
+
except ApiError as err:
|
|
84
|
+
print(err.status, err.code, err.message)
|
|
85
|
+
|
|
86
|
+
# What can I run? `run` is addressed by a conn_… / fn_… / ep_… / wf_… id, and
|
|
87
|
+
# these two operations are how you discover one.
|
|
88
|
+
connections = client.connections.list()
|
|
89
|
+
workflows = client.workflows.list()
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Run anything addressable by a URN
|
|
93
|
+
|
|
94
|
+
`run` returns a `kind`-tagged envelope — the parsed body, exactly as it arrived.
|
|
95
|
+
Discriminate it before reading a field, because the arms use different field
|
|
96
|
+
names on purpose (`value` vs `output` vs `runId` + `status`):
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
import w6w
|
|
100
|
+
|
|
101
|
+
env = client.run(
|
|
102
|
+
connections[0].id, # "conn_…"
|
|
103
|
+
action="send_message", # which of the app's actions to invoke
|
|
104
|
+
payload={"channel": "#general", "text": "Hello from w6w"},
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if w6w.is_action_run(env):
|
|
108
|
+
print(env["value"]) # the action's return value
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
A `fn_…` or `ep_…` id runs the same way with no `action`; a `wf_…` id is
|
|
112
|
+
enqueued and comes back `202` with `runId` and `status` — which is success, not
|
|
113
|
+
an error. A `kind` this release has never heard of is handed back verbatim
|
|
114
|
+
rather than raised, so a newer server cannot break an installed client.
|
|
115
|
+
|
|
116
|
+
### Run a workflow, and optionally wait
|
|
117
|
+
|
|
118
|
+
`workflows.run` is the typed path, and the only one that can wait for the result
|
|
119
|
+
**server-side** — this package contains no polling loop of its own:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
run = client.workflows.run(workflows[0].id, wait=True, variables={"customerId": "cus_1"})
|
|
123
|
+
|
|
124
|
+
# A failed run is DATA, not a raised error. `terminal` says whether it finished.
|
|
125
|
+
if run.terminal and run.status == "failed":
|
|
126
|
+
print(run.error)
|
|
127
|
+
elif run.terminal:
|
|
128
|
+
print(run.output)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Documents and vars
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
doc = client.documents.create("welcome", "# Hi", format="markdown")
|
|
135
|
+
client.documents.update(doc.id, content="# Hello")
|
|
136
|
+
client.documents.delete(doc.id)
|
|
137
|
+
|
|
138
|
+
v = client.vars.create("sender_email", "string", "a@b.c")
|
|
139
|
+
client.vars.update(v.id, value="c@d.e")
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Documents are project-scoped (every operation takes an optional `project`);
|
|
143
|
+
**variables are not** — they are scoped by tenant/subject only, so no `vars.*`
|
|
144
|
+
signature has a `project` argument. The asymmetry is the server's, and this
|
|
145
|
+
package documents it rather than papering over it with an argument the server
|
|
146
|
+
ignores.
|
|
147
|
+
|
|
148
|
+
`update` distinguishes *leave this field alone* from *set this field to null*:
|
|
149
|
+
every patchable parameter defaults to `w6w.UNSET` (send nothing), and passing
|
|
150
|
+
`None` explicitly sends JSON `null`.
|
|
151
|
+
|
|
152
|
+
### Reaching an endpoint this version does not model
|
|
153
|
+
|
|
154
|
+
`client.request` is public and returns `(status, body)` — the status comes back
|
|
155
|
+
because a `202` is a normal outcome on this API, not an error. When any part of
|
|
156
|
+
a path comes from the caller, build it with `w6w.path` rather than by
|
|
157
|
+
concatenation; it percent-encodes every interpolated value:
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from w6w import path
|
|
161
|
+
|
|
162
|
+
client.request("GET", path("/documents/by-key/{key}", key="notes/2026"))
|
|
163
|
+
# -> GET https://api.example.com/documents/by-key/notes%2F2026
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
That is not defensive ceremony. Document keys are user-chosen and the server
|
|
167
|
+
accepts almost anything, so an unencoded `"notes/2026"` becomes two path
|
|
168
|
+
segments — a **different route**, answered with a `200` and no error anywhere.
|
|
169
|
+
|
|
170
|
+
## Configuration
|
|
171
|
+
|
|
172
|
+
Two environment variables, read once when a client is constructed:
|
|
173
|
+
|
|
174
|
+
| Variable | Meaning |
|
|
175
|
+
|---|---|
|
|
176
|
+
| `W6W_BASE_URL` | The **origin** of your w6w API — e.g. `https://api.example.com`. The API is served at the root of that host, so the client appends nothing. There is no default. |
|
|
177
|
+
| `W6W_TOKEN` | Your API token. Sent as `Authorization: Bearer <token>` on every request. |
|
|
178
|
+
|
|
179
|
+
These variables are part of the published contract and are identical across all
|
|
180
|
+
three wrappers.
|
|
181
|
+
|
|
182
|
+
**Explicit constructor arguments always win over the environment.**
|
|
183
|
+
`W6wClient(base_url=…, token=…)` overrides `W6W_BASE_URL` / `W6W_TOKEN`, and an
|
|
184
|
+
argument you pass explicitly — including an empty string — is never quietly
|
|
185
|
+
replaced by an environment variable behind your back.
|
|
186
|
+
|
|
187
|
+
Credentials are per-client **instance state**, never module globals: two clients
|
|
188
|
+
in one process can point at two servers with two tokens and not interfere.
|
|
189
|
+
|
|
190
|
+
### `W6W_BASE_URL` is an origin
|
|
191
|
+
|
|
192
|
+
The API is served at the **root** of its own host — `https://api.example.com/vars`,
|
|
193
|
+
not `…/api/vars` — so nothing is appended. Trailing slashes never matter, and a
|
|
194
|
+
path you configure is preserved verbatim, because it is indistinguishable from a
|
|
195
|
+
real gateway prefix:
|
|
196
|
+
|
|
197
|
+
| You set | The client uses |
|
|
198
|
+
|---|---|
|
|
199
|
+
| `https://api.example.com` | `https://api.example.com` |
|
|
200
|
+
| `https://api.example.com/` | `https://api.example.com` |
|
|
201
|
+
| `https://api.example.com///` | `https://api.example.com` |
|
|
202
|
+
| `https://api.example.com/gw` | `https://api.example.com/gw` |
|
|
203
|
+
|
|
204
|
+
> **Breaking in `0.2.0`.** The client used to append `/api`. If your
|
|
205
|
+
> `W6W_BASE_URL` ends in `/api` because of that, **drop the suffix** — a stale
|
|
206
|
+
> one now 404s on every call. It is not stripped for you: a configured path is
|
|
207
|
+
> exactly how a deployment behind a gateway prefix is addressed.
|
|
208
|
+
|
|
209
|
+
**A blank value is not a base URL.** An environment variable that is set but
|
|
210
|
+
empty or whitespace-only is treated as **absent**: `W6W_BASE_URL=`,
|
|
211
|
+
`W6W_BASE_URL=" "` and an unset `W6W_BASE_URL` all behave identically and all
|
|
212
|
+
raise the same `ConfigError` naming the variable. (`export W6W_BASE_URL=`, a
|
|
213
|
+
Dockerfile `ENV` with no build arg, and a CI variable whose template did not
|
|
214
|
+
interpolate are the common cases, and they look unset in every log line.) The
|
|
215
|
+
same rule applies to `W6W_TOKEN`.
|
|
216
|
+
|
|
217
|
+
**It must be absolute.** `W6W_BASE_URL=/foo` or `W6W_BASE_URL=api.example.com`
|
|
218
|
+
raises a `ConfigError` naming the variable, at construction — not a confusing
|
|
219
|
+
"the server may be down" on the first request. Scheme and host, `http` or
|
|
220
|
+
`https`.
|
|
221
|
+
|
|
222
|
+
A client with no token can still be **constructed** — the error surfaces on the
|
|
223
|
+
first request instead, so offline `--help`-style uses work. A client with no
|
|
224
|
+
base URL raises immediately, at construction.
|
|
225
|
+
|
|
226
|
+
**A token is validated as a header value.** A token containing a carriage
|
|
227
|
+
return, newline or other control character raises a `ConfigError` before any
|
|
228
|
+
request is made, because CR/LF in a header value is header injection — and the
|
|
229
|
+
usual way this happens is entirely innocent: `W6W_TOKEN=$(cat token.txt)` keeps
|
|
230
|
+
the file's trailing newline. The message names the offending codepoint and
|
|
231
|
+
position and never echoes the token.
|
|
232
|
+
|
|
233
|
+
## Errors
|
|
234
|
+
|
|
235
|
+
Two exception types, and the split is deliberate:
|
|
236
|
+
|
|
237
|
+
| Type | Means |
|
|
238
|
+
|---|---|
|
|
239
|
+
| `ConfigError` | The client was never in a position to make a request — no base URL, or no token. No HTTP exchange happened. |
|
|
240
|
+
| `ApiError` | The server answered, or could not be reached. Carries `status`, `code`, `message` and `raw` (the parsed body, kept because error bodies carry fields the message drops). |
|
|
241
|
+
|
|
242
|
+
`ApiError` has exactly three shapes: `status=0` / `code="network_error"` for a
|
|
243
|
+
transport failure, `code="bad_response"` for a non-JSON body on a failing status
|
|
244
|
+
(a proxy's HTML error page), and the server's own `code` / `message` for an error
|
|
245
|
+
envelope. Classify by `status` and by a code *prefix* (`unknown_*` → 404,
|
|
246
|
+
`invalid_*` → 400, `*_exists` → 409) rather than by an exhaustive list of codes:
|
|
247
|
+
the server mints codes freely.
|
|
248
|
+
|
|
249
|
+
A `424` is an app or upstream **execute-phase** failure, not a transport fault
|
|
250
|
+
and not a server error — it is a 4xx on purpose. It reaches you with its code and
|
|
251
|
+
body intact.
|
|
252
|
+
|
|
253
|
+
## Tests
|
|
254
|
+
|
|
255
|
+
The suite runs from a source checkout with **no installation step and no test
|
|
256
|
+
runner to install** — `unittest` from the standard library:
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
PYTHONPATH=src python3 -m unittest discover -s tests -t . -v
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Run it from the repository root. A single file:
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
PYTHONPATH=src python3 -m unittest tests.test_version -v
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
No test reaches the network. Client behaviour is exercised against an injected
|
|
269
|
+
transport seam, never a live server — conformance against a running server is a
|
|
270
|
+
separate, environment-dependent step and is not part of this suite.
|
|
271
|
+
|
|
272
|
+
`tests/test_surface.py` is the exception in kind rather than in method: it reads
|
|
273
|
+
the shared `endpoints.json` sitting **beside** this repository and asserts that
|
|
274
|
+
every contracted operation is reachable under the symbol the contract names it —
|
|
275
|
+
and that no namespace has grown one the contract does not have. It never
|
|
276
|
+
vendors a copy, and it fails naming the path it looked for rather than skipping,
|
|
277
|
+
because a guard that passes quietly when its input is missing is worse than no
|
|
278
|
+
guard.
|
|
279
|
+
|
|
280
|
+
### Why coverage is measured in CI and not locally
|
|
281
|
+
|
|
282
|
+
Coverage is gated at **90 %**, enforced by CI, and there is deliberately no
|
|
283
|
+
local coverage command.
|
|
284
|
+
|
|
285
|
+
`coverage` is the one development-only tool this repo uses, and it is a
|
|
286
|
+
third-party package. The environment this wrapper is developed in has **no
|
|
287
|
+
package installer available** (`python3 -m pip` and `python3 -m ensurepip` both
|
|
288
|
+
fail there), so `coverage` cannot be installed alongside the code it would
|
|
289
|
+
measure. Rather than publish a local gate that nobody can actually run, the
|
|
290
|
+
number is enforced where the tool does exist:
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
pip install -e '.[dev]'
|
|
294
|
+
coverage run -m unittest discover -s tests -t .
|
|
295
|
+
coverage report # fail_under = 90, configured in pyproject.toml
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
The local bar is different and, for the thing that actually matters, stricter:
|
|
299
|
+
**the suite runs with no install, every operation is exercised, and every error
|
|
300
|
+
path has a test.** A percentage catches a whole operation going untested; it
|
|
301
|
+
does not catch an operation tested badly. If you are adding an operation, the
|
|
302
|
+
list above is your gate, not the number.
|
|
303
|
+
|
|
304
|
+
## The surface
|
|
305
|
+
|
|
306
|
+
Which operations exist is not decided in this repository. It is defined by the
|
|
307
|
+
shared machine-readable contract **`endpoints.json`**, which all three w6w
|
|
308
|
+
wrappers implement and which each one's conformance test reads directly — the
|
|
309
|
+
same file, never a vendored copy. An operation missing from a wrapper, or one it
|
|
310
|
+
has grown alone, is a failing test rather than a preference.
|
|
311
|
+
|
|
312
|
+
Some operations may be marked `planned` in that contract: they are implemented
|
|
313
|
+
and unit-tested here, but the corresponding server route is not live yet, so
|
|
314
|
+
calling one against a server today returns `404`. The marker records **server**
|
|
315
|
+
readiness, not wrapper completeness.
|
|
316
|
+
|
|
317
|
+
## Versioning
|
|
318
|
+
|
|
319
|
+
The version appears in three places — `src/w6w/_version.py`, `pyproject.toml`,
|
|
320
|
+
and the wrappers' shared `VERSION` contract that sits beside this repository —
|
|
321
|
+
and `tests/test_version.py` fails if any of them disagree. All three are
|
|
322
|
+
written from the shared file at release time; none is ever bumped on its own.
|
|
323
|
+
That is the mechanism behind "one surface, one version, released together".
|
|
324
|
+
|
|
325
|
+
## Contributing
|
|
326
|
+
|
|
327
|
+
Conventions worth knowing before the first patch:
|
|
328
|
+
|
|
329
|
+
- **`src/` layout.** Tests import the package via `PYTHONPATH=src` (or an
|
|
330
|
+
install), never the working directory, so what is tested is what ships.
|
|
331
|
+
- **The barrel rule.** Every public symbol is re-exported from `w6w/__init__.py`
|
|
332
|
+
and listed in `__all__`. Modules with a leading underscore are private
|
|
333
|
+
implementation detail and may move.
|
|
334
|
+
- **Type annotations use `typing.Optional` / `Dict` / `List` form**, so they
|
|
335
|
+
stay valid on Python 3.9 even where they are evaluated at runtime; every
|
|
336
|
+
module also starts with `from __future__ import annotations`.
|
|
337
|
+
- **Exactly one module reads the environment** — `src/w6w/_env.py`. `os.environ`
|
|
338
|
+
and `os.getenv` appear there and nowhere else; every other module receives
|
|
339
|
+
resolved configuration as a value, so no operation can acquire a hidden
|
|
340
|
+
dependency on ambient process state.
|
|
341
|
+
- **Every caller-supplied value in a URL goes through `w6w.path`.** One
|
|
342
|
+
primitive at the transport seam, never a `quote()` at each call site — "every
|
|
343
|
+
call site remembered to encode" is not a property anyone can verify later.
|
|
344
|
+
- **Stdlib only, in tests too.** No file in this repository imports a
|
|
345
|
+
third-party module. The transport is constructor-injected, so no test opens a
|
|
346
|
+
socket.
|
|
347
|
+
- **The surface is not decided here.** Which operations exist, and what each is
|
|
348
|
+
called in each language, is pinned by the shared wrapper contract. Adding an
|
|
349
|
+
operation to this client alone would break the promise the three wrappers
|
|
350
|
+
make together.
|