Skip to content

Commit f19f612

Browse files
committed
chore: adopt ruff 0.16.0
ruff 0.16.0 stabilized CPY001 (missing-copyright-notice) out of preview, so `select = ["ALL"]` now picks it up. Ignore it, matching modern-di. 0.16.0 also formats Python code blocks inside Markdown; reformat the 51 affected file(s). No .py file changed.
1 parent 2c72d63 commit f19f612

52 files changed

Lines changed: 387 additions & 242 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

architecture/errors.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
`StatusError` and all its 4xx/5xx subclasses are constructed with a **single positional `response: httpx2.Response`**. Subclasses do not override `__init__`. All fields are available via `exc.response.*` (status code, headers, content, request, etc.).
44

55
```python
6-
raise NotFoundError(response) # correct
7-
exc.response.status_code # 404
8-
exc.response.request.url # URL of the failed request
6+
raise NotFoundError(response) # correct
7+
exc.response.status_code # 404
8+
exc.response.request.url # URL of the failed request
99
```
1010

1111
`__repr__` and the `str()` summary redact URL userinfo (`user:pass@`) and mask the values of known-sensitive query and fragment parameters (e.g. `token`, `api_key`, `secret`) to avoid leaking credentials in tracebacks.

docs/decoders.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,7 @@ class CsvDecoder:
7474
(row_type,) = typing.get_args(model)
7575
field_types = {f.name: f.type for f in dataclasses.fields(row_type)}
7676
reader = csv.DictReader(io.StringIO(content.decode("utf-8")))
77-
return [
78-
row_type(**{name: field_types[name](value) for name, value in row.items()})
79-
for row in reader
80-
]
77+
return [row_type(**{name: field_types[name](value) for name, value in row.items()}) for row in reader]
8178
```
8279

8380
`can_decode` is total and never raises: a non-`list` model, a bare `list`, or `list[int]` all fall through to `False`. `decode` coerces each CSV cell with its field's type (CSV values arrive as strings) — a real decoder would handle optionals, dates, and missing columns; this is where your domain logic goes. Wire it ahead of the built-ins so it gets first refusal on `list[...]` models while pydantic still handles everything else:

docs/errors.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -106,14 +106,14 @@ async def fetch(client: AsyncClient, user_id: int) -> dict | None:
106106
For any `StatusError` subclass, the raw `httpx2.Response` is on `exc.response`:
107107

108108
```python
109-
exc.response.status_code # 404
110-
exc.response.headers # httpx2.Headers — case-insensitive
111-
exc.response.content # raw bytes
112-
exc.response.text # decoded body
113-
exc.response.json() # parsed JSON (raises if not JSON)
114-
exc.response.request # the failing httpx2.Request
115-
exc.response.request.url # the failing URL (httpx2.URL)
116-
exc.response.request.method # the HTTP method
109+
exc.response.status_code # 404
110+
exc.response.headers # httpx2.Headers — case-insensitive
111+
exc.response.content # raw bytes
112+
exc.response.text # decoded body
113+
exc.response.json() # parsed JSON (raises if not JSON)
114+
exc.response.request # the failing httpx2.Request
115+
exc.response.request.url # the failing URL (httpx2.URL)
116+
exc.response.request.method # the HTTP method
117117
```
118118

119119
**Security note:** `__repr__` and the exception's summary message strip `user:pass@` userinfo and mask the values of known-sensitive query and URL-fragment parameters (`api_key`, `apikey`, `access_token`, `refresh_token`, `token`, `secret`, `client_secret`, `password`, `passwd`, `pwd`, `auth`, `authorization`, `sig`, `signature`, `key`, `private_key`, `session`, `sessionid`, `x-api-key`) as `REDACTED`, preserving the keys. Query values under other names are **not** masked, so still avoid putting non-standard secrets in query strings. Note that request *headers* (`Authorization`, `Cookie`, etc.) are never redacted — see `exc.response.request.headers` above.

docs/index.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,13 @@ import asyncio
4242

4343
from httpware import AsyncClient
4444

45+
4546
async def main() -> None:
4647
async with AsyncClient(base_url="https://jsonplaceholder.typicode.com") as client:
4748
response = await client.get("/users/1")
4849
print(response.json())
4950

51+
5052
asyncio.run(main())
5153
```
5254

@@ -102,7 +104,7 @@ async def main() -> None:
102104
base_url="https://api.example.com",
103105
middleware=[
104106
AsyncBulkhead(max_concurrent=10), # cap total in-flight
105-
AsyncRetry(), # default: 3 attempts, full-jitter backoff
107+
AsyncRetry(), # default: 3 attempts, full-jitter backoff
106108
],
107109
) as client:
108110
user = await client.get("/users/1", response_model=User)

docs/recipes/link-header-pagination.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ async def main() -> None:
2424
response, tags = await client.send_with_response(request, response_model=list[Tag])
2525
for tag in tags:
2626
process(tag)
27-
url = next_link(response.headers.get("link")) # caller's parser
28-
params = None # next link carries query
27+
url = next_link(response.headers.get("link")) # caller's parser
28+
params = None # next link carries query
2929
```
3030

3131
`process` and `next_link` are caller-defined. Pick a Link-header parser that fits your project — there are several on PyPI, and the format is small enough to hand-roll.

docs/recipes/modern-di.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ class ServiceClients(Group):
6464
cache_settings=providers.CacheSettings(finalizer=AsyncClient.aclose),
6565
)
6666

67+
6768
# At Container(...) construction:
6869
# modern_di.exceptions.DuplicateProviderTypeError: Provider is duplicated by type
6970
# <class 'httpware.client.AsyncClient'>. To resolve this issue: ...

docs/recipes/phase-decorator-patterns.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ from httpware import async_before_request
4747

4848

4949
_CORRELATION_ID: contextvars.ContextVar[str | None] = contextvars.ContextVar(
50-
"correlation_id", default=None,
50+
"correlation_id",
51+
default=None,
5152
)
5253

5354

@@ -132,7 +133,8 @@ from httpware import async_on_error
132133

133134
@async_on_error
134135
async def fallback_on_network_error(
135-
request: httpx2.Request, exc: Exception,
136+
request: httpx2.Request,
137+
exc: Exception,
136138
) -> httpx2.Response | None:
137139
if isinstance(exc, NetworkError):
138140
return httpx2.Response(

docs/resilience.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ When `acquire_timeout` elapses without a slot opening, `AsyncBulkhead` raises `B
152152

153153
```python
154154
from httpware.middleware.resilience import AsyncCircuitBreaker # async
155-
from httpware.middleware.resilience import CircuitBreaker # sync
155+
from httpware.middleware.resilience import CircuitBreaker # sync
156156
```
157157

158158
Classic consecutive-failure circuit breaker. Counts failures and prevents requests from reaching a downstream that is known to be broken.
@@ -208,8 +208,8 @@ from httpware.middleware.resilience import AsyncCircuitBreaker
208208

209209
breaker = AsyncCircuitBreaker(
210210
failure_rate_threshold=0.5, # open at ≥50% failures
211-
window_seconds=30.0, # over a rolling 30s window
212-
minimum_calls=20, # but only once 20+ calls are observed
211+
window_seconds=30.0, # over a rolling 30s window
212+
minimum_calls=20, # but only once 20+ calls are observed
213213
)
214214
```
215215

planning/audits/2026-06-07-deep-audit.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,8 +539,10 @@ async def test_on_error_lets_cancelled_propagate() -> None:
539539
@async_on_error
540540
async def swallow_all(request, exc) -> httpx2.Response | None:
541541
raise AssertionError("should not catch CancelledError")
542+
542543
async def terminal(request):
543544
raise asyncio.CancelledError
545+
544546
dispatch = compose_async((swallow_all,), terminal)
545547
with pytest.raises(asyncio.CancelledError):
546548
await dispatch(_make_request())
@@ -556,7 +558,10 @@ Suggested direction: add `test_on_error_lets_keyboardinterrupt_propagate` (and o
556558
557559
```python
558560
"""Tests for the per-method API surface of AsyncClient."""
561+
559562
from httpware import AsyncClient, NotFoundError
563+
564+
560565
def _client_with_handler(handler, **kwargs) -> AsyncClient: ...
561566
async def test_get_returns_httpx2_response() -> None: ...
562567
```

planning/audits/2026-06-14-deep-audit.md

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,9 @@ The discover map labels the file "Hypothesis property-based tests for retry inte
235235

236236
```python
237237
async def test_total_attempts_never_exceeds_max_attempts(
238-
max_attempts: int, status: int, method: str,
238+
max_attempts: int,
239+
status: int,
240+
method: str,
239241
) -> None:
240242
...
241243
await client.request(method, "https://example.test/x")
@@ -270,8 +272,15 @@ CLAUDE.md and `architecture/errors.md` mandate that all `StatusError` subclasses
270272
```python
271273
def test_inheritance_tree() -> None:
272274
...
273-
for exc in (BadRequestError, UnauthorizedError, ForbiddenError, ForbiddenError,
274-
ConflictError, UnprocessableEntityError, RateLimitedError):
275+
for exc in (
276+
BadRequestError,
277+
UnauthorizedError,
278+
ForbiddenError,
279+
ForbiddenError,
280+
ConflictError,
281+
UnprocessableEntityError,
282+
RateLimitedError,
283+
):
275284
assert issubclass(exc, ClientStatusError), exc
276285
```
277286

@@ -334,6 +343,7 @@ def _is_streaming_body_async(value: object) -> bool:
334343
...
335344
return hasattr(value, "__aiter__")
336345

346+
337347
def _is_streaming_body_sync(value: object) -> bool:
338348
...
339349
return hasattr(value, "__iter__")
@@ -410,9 +420,7 @@ The test asserts `len(budget._deposits) == expected_deposits`, relying on a comm
410420

411421
```python
412422
expected_deposits = (_N_SYNC_THREADS * _N_OPS_PER_THREAD) + _N_ASYNC_TASKS
413-
assert len(budget._deposits) == expected_deposits, (
414-
f"expected {expected_deposits} deposits, got {len(budget._deposits)}"
415-
)
423+
assert len(budget._deposits) == expected_deposits, f"expected {expected_deposits} deposits, got {len(budget._deposits)}"
416424
```
417425

418426
Panel 2/3: code_reality, reproducer. Suggested direction: pin the injected clock so no real time elapses, making the no-purge assumption an enforced invariant rather than a fragile comment.

0 commit comments

Comments
 (0)