svc-infra 0.1.629__py3-none-any.whl → 0.1.630__py3-none-any.whl

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.

Potentially problematic release.


This version of svc-infra might be problematic. Click here for more details.

@@ -0,0 +1,147 @@
1
+ # Timeouts & Resource Limits
2
+
3
+ This guide covers request/handler timeouts, outbound HTTP client timeouts, database statement timeouts, job/webhook delivery timeouts, and graceful shutdown. It explains defaults, configuration, wiring, and recommended tuning by environment.
4
+
5
+ ## Why timeouts?
6
+
7
+ - Protects your service from slowloris uploads and hanging requests
8
+ - Limits blast radius of slow downstreams (HTTP, DB, webhooks)
9
+ - Enables predictable backpressure and faster recovery during incidents
10
+
11
+ ## Configuration overview
12
+
13
+ The library exposes simple environment variables with sensible defaults. Use floats for second values unless noted.
14
+
15
+ - REQUEST_BODY_TIMEOUT_SECONDS (int)
16
+ - Default: prod=15, nonprod=30
17
+ - Purpose: Abort slow request body reads (slowloris defense)
18
+ - REQUEST_TIMEOUT_SECONDS (int)
19
+ - Default: prod=30, nonprod=15
20
+ - Purpose: Cap overall handler execution time
21
+ - HTTP_CLIENT_TIMEOUT_SECONDS (float)
22
+ - Default: 10.0
23
+ - Purpose: Default timeout for outbound httpx clients created via helpers
24
+ - DB_STATEMENT_TIMEOUT_MS (int)
25
+ - Default: unset (disabled)
26
+ - Purpose: Per-transaction statement timeout (Postgres via SET LOCAL)
27
+ - JOB_DEFAULT_TIMEOUT_SECONDS (float)
28
+ - Default: unset (disabled)
29
+ - Purpose: Caps per-job handler runtime in the in-process jobs runner
30
+ - WEBHOOK_DELIVERY_TIMEOUT_SECONDS (float)
31
+ - Default: falls back to HTTP client default (10.0)
32
+ - Purpose: Timeout for webhook delivery HTTP calls
33
+ - SHUTDOWN_GRACE_PERIOD_SECONDS (float)
34
+ - Default: prod=20.0, nonprod=5.0
35
+ - Purpose: Wait time for in-flight requests to drain on shutdown
36
+
37
+ See ADR-0010 for design rationale: `src/svc_infra/docs/adr/0010-timeouts-and-resource-limits.md`.
38
+
39
+ ## Request/handler timeouts (FastAPI)
40
+
41
+ Two middlewares enforce timeouts inside your ASGI app:
42
+
43
+ - BodyReadTimeoutMiddleware
44
+ - Enforces a per-chunk timeout while reading the incoming request body.
45
+ - If reads stall beyond the timeout, responds with 408 application/problem+json.
46
+ - Module: `svc_infra.api.fastapi.middleware.timeout.BodyReadTimeoutMiddleware`
47
+ - HandlerTimeoutMiddleware
48
+ - Caps overall request handler execution time using asyncio.wait_for.
49
+ - If exceeded, responds with 504 application/problem+json.
50
+ - Module: `svc_infra.api.fastapi.middleware.timeout.HandlerTimeoutMiddleware`
51
+
52
+ Example wiring:
53
+
54
+ ```python
55
+ from fastapi import FastAPI
56
+ from svc_infra.api.fastapi.middleware.timeout import (
57
+ BodyReadTimeoutMiddleware,
58
+ HandlerTimeoutMiddleware,
59
+ )
60
+
61
+ app = FastAPI()
62
+
63
+ # Abort slow uploads (slowloris) after 15s in prod / 30s nonprod by default
64
+ app.add_middleware(BodyReadTimeoutMiddleware) # or timeout_seconds=20
65
+
66
+ # Cap total handler time (e.g., 30s in prod by default)
67
+ app.add_middleware(HandlerTimeoutMiddleware) # or timeout_seconds=25
68
+ ```
69
+
70
+ HTTP semantics:
71
+
72
+ - Body timeout → 408 Request Timeout (Problem+JSON) with fields: type, title, status, detail, instance, trace_id
73
+ - Handler timeout → 504 Gateway Timeout (Problem+JSON) with fields: type, title, status, detail, instance, trace_id
74
+
75
+ ## Outbound HTTP client timeouts (httpx)
76
+
77
+ Use the provided helpers to create httpx clients with the default timeout (driven by HTTP_CLIENT_TIMEOUT_SECONDS).
78
+
79
+ - Module: `svc_infra.http.client`
80
+ - `get_default_timeout_seconds()` → float
81
+ - `make_timeout(seconds=None) -> httpx.Timeout`
82
+ - `new_httpx_client(timeout_seconds=None, ...) -> httpx.Client`
83
+ - `new_async_httpx_client(timeout_seconds=None, ...) -> httpx.AsyncClient`
84
+
85
+ Error mapping:
86
+
87
+ - `httpx.TimeoutException` is mapped to 504 Gateway Timeout with Problem+JSON by default when `register_error_handlers(app)` is used.
88
+ - Module: `svc_infra.api.fastapi.middleware.errors.handlers.register_error_handlers`
89
+
90
+ ## Database statement timeouts (SQLAlchemy / Postgres)
91
+
92
+ If `DB_STATEMENT_TIMEOUT_MS` is set and Postgres is used, a per-transaction `SET LOCAL statement_timeout = :ms` is executed for sessions yielded by the built-in dependency.
93
+
94
+ - Module: `svc_infra.api.fastapi.db.sql.session.get_session`
95
+ - Non-Postgres dialects (e.g., SQLite) ignore this gracefully.
96
+
97
+ ## Jobs and webhooks
98
+
99
+ - Jobs runner
100
+ - Env: `JOB_DEFAULT_TIMEOUT_SECONDS`
101
+ - Module: `svc_infra.jobs.worker.process_one` — wraps job handler with `asyncio.wait_for()` when configured.
102
+ - Webhook delivery
103
+ - Env: `WEBHOOK_DELIVERY_TIMEOUT_SECONDS` (falls back to HTTP client default when unset)
104
+ - Module: `svc_infra.jobs.builtins.webhook_delivery.make_webhook_handler` — uses `new_async_httpx_client` with derived timeout.
105
+
106
+ ## Graceful shutdown
107
+
108
+ Install graceful shutdown to wait for in-flight requests (up to a grace period) during application shutdown.
109
+
110
+ - Module: `svc_infra.api.fastapi.middleware.graceful_shutdown.install_graceful_shutdown`
111
+ - Env: `SHUTDOWN_GRACE_PERIOD_SECONDS` (prod=20.0, nonprod=5.0 by default)
112
+
113
+ ```python
114
+ from svc_infra.api.fastapi.middleware.graceful_shutdown import install_graceful_shutdown
115
+
116
+ install_graceful_shutdown(app) # or grace_seconds=30.0
117
+ ```
118
+
119
+ ## Tuning recommendations
120
+
121
+ - Production
122
+ - REQUEST_BODY_TIMEOUT_SECONDS: 10–20s (shorter for public APIs)
123
+ - REQUEST_TIMEOUT_SECONDS: 20–30s (align with upstream proxy/gateway timeouts)
124
+ - HTTP_CLIENT_TIMEOUT_SECONDS: 3–10s (favor quick failover with retries)
125
+ - DB_STATEMENT_TIMEOUT_MS: set per-route/transaction if queries are constrained
126
+ - SHUTDOWN_GRACE_PERIOD_SECONDS: 20–60s depending on peak latencies
127
+ - Staging/Dev
128
+ - Relax timeouts slightly to reduce test flakiness (defaults already reflect this)
129
+ - Gateways/Proxies
130
+ - Ensure upstream (e.g., NGINX, ALB) timeouts exceed app’s body timeout and are aligned with handler timeout to avoid double timeouts.
131
+
132
+ ## Testing and acceptance
133
+
134
+ - Unit tests cover body read timeout, handler timeout, outbound timeout mapping, and a smoke check for DB statement timeout.
135
+ - Acceptance tests:
136
+ - A2-04: slow handler → 504 Problem
137
+ - A2-05: slow body → 408 Problem or 413 (size) as applicable
138
+ - A2-06: outbound httpx timeout → 504 Problem
139
+
140
+ ## Troubleshooting
141
+
142
+ - Seeing 200 instead of 408 for slow uploads under some servers?
143
+ - Some servers buffer the entire body before invoking the app. The BodyReadTimeoutMiddleware greedily drains with per-chunk timeouts and replays to reliably detect slowloris. Ensure HTTP/1.1 parsing with a streaming-capable server implementation (e.g., uvicorn+httptools) in acceptance tests.
144
+ - Outbound timeouts not mapped to Problem?
145
+ - Ensure `register_error_handlers(app)` is installed so `httpx.TimeoutException` returns a 504 Problem.
146
+ - Statement timeout ignored on SQLite?
147
+ - Expected. Non-Postgres dialects skip `SET LOCAL` safely.
@@ -0,0 +1,13 @@
1
+ from .client import (
2
+ get_default_timeout_seconds,
3
+ make_timeout,
4
+ new_async_httpx_client,
5
+ new_httpx_client,
6
+ )
7
+
8
+ __all__ = [
9
+ "get_default_timeout_seconds",
10
+ "new_httpx_client",
11
+ "new_async_httpx_client",
12
+ "make_timeout",
13
+ ]
@@ -0,0 +1,64 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from typing import Any, Dict, Optional
5
+
6
+ import httpx
7
+
8
+ from svc_infra.app.env import pick
9
+
10
+
11
+ def _parse_float_env(name: str, default: float) -> float:
12
+ raw = os.getenv(name)
13
+ if raw is None or raw == "":
14
+ return default
15
+ try:
16
+ return float(raw)
17
+ except ValueError:
18
+ return default
19
+
20
+
21
+ def get_default_timeout_seconds() -> float:
22
+ """Return default outbound HTTP client timeout in seconds.
23
+
24
+ Env var: HTTP_CLIENT_TIMEOUT_SECONDS (float)
25
+ Defaults: 10.0 seconds for all envs unless overridden; tweakable via pick() if needed.
26
+ """
27
+ default = pick(prod=10.0, nonprod=10.0)
28
+ return _parse_float_env("HTTP_CLIENT_TIMEOUT_SECONDS", default)
29
+
30
+
31
+ def make_timeout(seconds: float | None = None) -> httpx.Timeout:
32
+ s = seconds if seconds is not None else get_default_timeout_seconds()
33
+ # Apply same timeout for connect/read/write/pool for simplicity
34
+ return httpx.Timeout(timeout=s)
35
+
36
+
37
+ def new_httpx_client(
38
+ *,
39
+ timeout_seconds: Optional[float] = None,
40
+ headers: Optional[Dict[str, str]] = None,
41
+ base_url: Optional[str] = None,
42
+ **kwargs: Any,
43
+ ) -> httpx.Client:
44
+ """Create a sync httpx Client with default timeout and optional headers/base_url.
45
+
46
+ Callers can override timeout_seconds; remaining kwargs are forwarded to httpx.Client.
47
+ """
48
+ timeout = make_timeout(timeout_seconds)
49
+ return httpx.Client(timeout=timeout, headers=headers, base_url=base_url, **kwargs)
50
+
51
+
52
+ def new_async_httpx_client(
53
+ *,
54
+ timeout_seconds: Optional[float] = None,
55
+ headers: Optional[Dict[str, str]] = None,
56
+ base_url: Optional[str] = None,
57
+ **kwargs: Any,
58
+ ) -> httpx.AsyncClient:
59
+ """Create an async httpx AsyncClient with default timeout and optional headers/base_url.
60
+
61
+ Callers can override timeout_seconds; remaining kwargs are forwarded to httpx.AsyncClient.
62
+ """
63
+ timeout = make_timeout(timeout_seconds)
64
+ return httpx.AsyncClient(timeout=timeout, headers=headers, base_url=base_url, **kwargs)
@@ -1,9 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
- import httpx
3
+ import os
4
4
 
5
5
  from svc_infra.db.inbox import InboxStore
6
6
  from svc_infra.db.outbox import OutboxStore
7
+ from svc_infra.http import get_default_timeout_seconds, new_async_httpx_client
7
8
  from svc_infra.jobs.queue import Job
8
9
  from svc_infra.webhooks.signing import sign
9
10
 
@@ -65,7 +66,18 @@ def make_webhook_handler(
65
66
  version = delivery_payload.get("version")
66
67
  if version is not None:
67
68
  headers["X-Payload-Version"] = str(version)
68
- async with httpx.AsyncClient(timeout=10) as client:
69
+ # Derive timeout: dedicated WEBHOOK_DELIVERY_TIMEOUT_SECONDS or default HTTP client timeout
70
+ timeout_seconds = None
71
+ env_timeout = os.getenv("WEBHOOK_DELIVERY_TIMEOUT_SECONDS")
72
+ if env_timeout:
73
+ try:
74
+ timeout_seconds = float(env_timeout)
75
+ except ValueError:
76
+ timeout_seconds = get_default_timeout_seconds()
77
+ else:
78
+ timeout_seconds = get_default_timeout_seconds()
79
+
80
+ async with new_async_httpx_client(timeout_seconds=timeout_seconds) as client:
69
81
  resp = await client.post(url, json=delivery_payload, headers=headers)
70
82
  if 200 <= resp.status_code < 300:
71
83
  # record delivery and mark processed
@@ -0,0 +1,75 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import contextlib
5
+ from typing import Awaitable, Callable, Optional
6
+
7
+ from .queue import JobQueue
8
+
9
+ ProcessFunc = Callable[[object], Awaitable[None]]
10
+
11
+
12
+ class WorkerRunner:
13
+ """Cooperative worker loop with graceful stop.
14
+
15
+ - start(): begin polling the queue and processing jobs
16
+ - stop(grace_seconds): signal stop, wait up to grace for current job to finish
17
+ """
18
+
19
+ def __init__(self, queue: JobQueue, handler: ProcessFunc, *, poll_interval: float = 0.25):
20
+ self._queue = queue
21
+ self._handler = handler
22
+ self._poll_interval = poll_interval
23
+ self._task: Optional[asyncio.Task] = None
24
+ self._stopping = asyncio.Event()
25
+ self._inflight: Optional[asyncio.Task] = None
26
+
27
+ async def _loop(self) -> None:
28
+ try:
29
+ while not self._stopping.is_set():
30
+ job = self._queue.reserve_next()
31
+ if not job:
32
+ await asyncio.sleep(self._poll_interval)
33
+ continue
34
+
35
+ # Process one job; track in-flight task for stop()
36
+ async def _run():
37
+ try:
38
+ await self._handler(job)
39
+ except Exception as exc: # pragma: no cover
40
+ self._queue.fail(job.id, error=str(exc))
41
+ return
42
+ self._queue.ack(job.id)
43
+
44
+ self._inflight = asyncio.create_task(_run())
45
+ try:
46
+ await self._inflight
47
+ finally:
48
+ self._inflight = None
49
+ finally:
50
+ # exiting loop
51
+ pass
52
+
53
+ def start(self) -> asyncio.Task:
54
+ if self._task is None or self._task.done():
55
+ self._task = asyncio.create_task(self._loop())
56
+ return self._task
57
+
58
+ async def stop(self, *, grace_seconds: float = 10.0) -> None:
59
+ self._stopping.set()
60
+ # Wait for in-flight job to complete, up to grace
61
+ if self._inflight is not None and not self._inflight.done():
62
+ try:
63
+ await asyncio.wait_for(self._inflight, timeout=grace_seconds)
64
+ except asyncio.TimeoutError:
65
+ # Give up; job will be retried if your queue supports visibility timeouts
66
+ pass
67
+ # Finally, wait for loop to exit (should be quick since stopping is set)
68
+ if self._task is not None:
69
+ try:
70
+ await asyncio.wait_for(self._task, timeout=max(0.1, self._poll_interval + 0.1))
71
+ except asyncio.TimeoutError:
72
+ # Cancel as a last resort
73
+ self._task.cancel()
74
+ with contextlib.suppress(Exception):
75
+ await self._task
svc_infra/jobs/worker.py CHANGED
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import asyncio
4
+ import os
3
5
  from typing import Awaitable, Callable
4
6
 
5
7
  from .queue import Job, JobQueue
@@ -7,6 +9,16 @@ from .queue import Job, JobQueue
7
9
  ProcessFunc = Callable[[Job], Awaitable[None]]
8
10
 
9
11
 
12
+ def _get_job_timeout_seconds() -> float | None:
13
+ raw = os.getenv("JOB_DEFAULT_TIMEOUT_SECONDS")
14
+ if not raw:
15
+ return None
16
+ try:
17
+ return float(raw)
18
+ except ValueError:
19
+ return None
20
+
21
+
10
22
  async def process_one(queue: JobQueue, handler: ProcessFunc) -> bool:
11
23
  """Reserve a job, process with handler, ack on success or fail with backoff.
12
24
 
@@ -16,7 +28,11 @@ async def process_one(queue: JobQueue, handler: ProcessFunc) -> bool:
16
28
  if not job:
17
29
  return False
18
30
  try:
19
- await handler(job)
31
+ timeout = _get_job_timeout_seconds()
32
+ if timeout and timeout > 0:
33
+ await asyncio.wait_for(handler(job), timeout=timeout)
34
+ else:
35
+ await handler(job)
20
36
  except Exception as exc: # pragma: no cover - exercise in tests by raising
21
37
  queue.fail(job.id, error=str(exc))
22
38
  return True
@@ -5,7 +5,7 @@ import time
5
5
  from dataclasses import dataclass
6
6
  from typing import Dict, Optional
7
7
 
8
- import httpx
8
+ from svc_infra.http import new_httpx_client
9
9
 
10
10
 
11
11
  def sha1_hex(data: str) -> str:
@@ -39,7 +39,11 @@ class HIBPClient:
39
39
  self.timeout = timeout
40
40
  self.user_agent = user_agent
41
41
  self._cache: Dict[str, CacheEntry] = {}
42
- self._http = httpx.Client(timeout=self.timeout, headers={"User-Agent": self.user_agent})
42
+ # Use central factory for consistent defaults; retain explicit timeout override
43
+ self._http = new_httpx_client(
44
+ timeout_seconds=self.timeout,
45
+ headers={"User-Agent": self.user_agent},
46
+ )
43
47
 
44
48
  def _get_cached(self, prefix: str) -> Optional[str]:
45
49
  now = time.time()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: svc-infra
3
- Version: 0.1.629
3
+ Version: 0.1.630
4
4
  Summary: Infrastructure for building and deploying prod-ready services
5
5
  License: MIT
6
6
  Keywords: fastapi,sqlalchemy,alembic,auth,infra,async,pydantic
@@ -38,6 +38,8 @@ svc_infra/api/fastapi/auth/security.py,sha256=FU_XlaXHO1jocUbxeMOX3w2GWkR5ZXAcWb
38
38
  svc_infra/api/fastapi/auth/sender.py,sha256=7a47HXuP0JLR4NlFQVb3TpoQHOPYybKPJ06C2fMJaec,1811
39
39
  svc_infra/api/fastapi/auth/settings.py,sha256=H6jF9EOZOBbRJX0JugvirLMylJw64KK_32osGkJYRB8,3470
40
40
  svc_infra/api/fastapi/auth/state.py,sha256=_FAGOG36EPFAl2VbB6fdsFX0W9JNIdWN9X1_OVMz4Q8,1070
41
+ svc_infra/api/fastapi/billing/router.py,sha256=vy9Zsf49f3c5Ni9xnBxUVmfgWhNBbkCWiZDLNxl8uOg,2132
42
+ svc_infra/api/fastapi/billing/setup.py,sha256=N4_yvNdvCt2l2OUmREUdUAClGwLUvCwqN1IHgY99ZMI,621
41
43
  svc_infra/api/fastapi/cache/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
42
44
  svc_infra/api/fastapi/cache/add.py,sha256=CX4TWsxrZ2EnN_TjVDOhQf382k8aUj2CXTG1GaZ5jU0,338
43
45
  svc_infra/api/fastapi/db/__init__.py,sha256=ixNtgx8afhMDnLMEFis672r3t5yVC-Bt8VkTX15S95A,355
@@ -52,7 +54,7 @@ svc_infra/api/fastapi/db/sql/__init__.py,sha256=R9P1Vy2Uqf9gFISxChMhO9tOGciIjEym
52
54
  svc_infra/api/fastapi/db/sql/add.py,sha256=xGZnbGnP9PQtWvr5vuXA3_PXzTKldPM_cD3L0uztAvo,4768
53
55
  svc_infra/api/fastapi/db/sql/crud_router.py,sha256=H7212YK4BsmuuS5E-HWKnRXzgc4RQxVSFWT6wzb4IXQ,11575
54
56
  svc_infra/api/fastapi/db/sql/health.py,sha256=ELLgQerooHHnvZRhGueSAc4QJsb3C4RojUGIu_U-hA4,792
55
- svc_infra/api/fastapi/db/sql/session.py,sha256=DUBqKTRJAX4fqRz9B-w9eD9SpzZ8EUS862-GsjCL3ts,1869
57
+ svc_infra/api/fastapi/db/sql/session.py,sha256=5MpwHignl2OmlgWY-SOU7TQ7phJ58enOf2u1nBMZH5w,2567
56
58
  svc_infra/api/fastapi/db/sql/users.py,sha256=68HGJgYVTEjKJm4-DPPC8-6nwXJoCukmgrYIIOHEUjs,5346
57
59
  svc_infra/api/fastapi/dependencies/ratelimit.py,sha256=DiOC-MJfqTtSydM6RAaeAsiXXL_6oZQoBLvRSpdWzs4,3794
58
60
  svc_infra/api/fastapi/docs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -76,14 +78,16 @@ svc_infra/api/fastapi/middleware/debug.py,sha256=H3jBKvdPkr2KHUEMGnqWBPZ0tG6Fgw-
76
78
  svc_infra/api/fastapi/middleware/errors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
77
79
  svc_infra/api/fastapi/middleware/errors/catchall.py,sha256=TG0W71UCDbfgLNdIaIv6mBSwZA_etMp5GquwKcAwYbI,1842
78
80
  svc_infra/api/fastapi/middleware/errors/exceptions.py,sha256=857_bdMgQugf8rb7U6ZaTZV3aiFTfBzFaUg80YUfAYE,475
79
- svc_infra/api/fastapi/middleware/errors/handlers.py,sha256=pQMVs5n627vcKkDFEaUzx5wCYeUcU-h6acWzh27RIEQ,7993
81
+ svc_infra/api/fastapi/middleware/errors/handlers.py,sha256=mxPaYMoK5JaF3w4t8gE0X8jqt9wzyxVxpDYgXMp8H9U,8606
82
+ svc_infra/api/fastapi/middleware/graceful_shutdown.py,sha256=U0s0IQHSyjOcxzdLSB_as1pAHobjFA68zbNciejI6GU,2883
80
83
  svc_infra/api/fastapi/middleware/idempotency.py,sha256=vnBQgMWzJVaF8oWgfw2ATjEKCyQifDeGPUc9z1N7ebE,5051
81
84
  svc_infra/api/fastapi/middleware/idempotency_store.py,sha256=BQN_Cq_jf_cuZRhze4EF5v0lOMQXpUWoRo7CsSTprug,5528
82
85
  svc_infra/api/fastapi/middleware/optimistic_lock.py,sha256=9lOMBI4VNIVndXnrMmgSq4qeR7xPjNR1H9d1F71M5S8,1271
83
86
  svc_infra/api/fastapi/middleware/ratelimit.py,sha256=Zw55_vlSVz4aqwr7gZ1P53HHZMO6fYUUQ7TXBzjEbw8,5014
84
- svc_infra/api/fastapi/middleware/ratelimit_store.py,sha256=LmJR8-kkW42rzOjls9lG1SBtCKjVY7L2Y_bNKHNY3-A,2553
87
+ svc_infra/api/fastapi/middleware/ratelimit_store.py,sha256=qJqkDi_iPrWIUZJzkhaYFcgyD1fCNDKFMa_wN53UPSQ,2796
85
88
  svc_infra/api/fastapi/middleware/request_id.py,sha256=Iru7ypTdK_n76lwziEGDWoVF4FKS0Ps1PMASYmzK8ek,768
86
89
  svc_infra/api/fastapi/middleware/request_size_limit.py,sha256=AcGqaB-F7Tbhg-at7ViT4Bpifst34jFneDBlUBjgo5I,1248
90
+ svc_infra/api/fastapi/middleware/timeout.py,sha256=U24EELSQy-oqBMRt-YZXdksGah5tvZP9iMpMvjzgjaU,5334
87
91
  svc_infra/api/fastapi/openapi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
88
92
  svc_infra/api/fastapi/openapi/apply.py,sha256=VAwRfcYSLCSKIpO1dp9okG1MXvkZuciU41jrSSuvUpI,1697
89
93
  svc_infra/api/fastapi/openapi/conventions.py,sha256=e6gUsFyfEGvz3KkUimjAWMfF7_fonMJ3IoGvQZjpvfs,7171
@@ -101,7 +105,7 @@ svc_infra/api/fastapi/paths/prefix.py,sha256=I1xEt1lKDlOPn2Oa-y_1B-Q8JCwAPpu3_F_
101
105
  svc_infra/api/fastapi/paths/user.py,sha256=z8xv_A3dPhG366ezJU1c839oHbU3tEg06rbx3HUUuL0,323
102
106
  svc_infra/api/fastapi/routers/__init__.py,sha256=pbyrfVZzrMFgX11K47TvTS94yN0q-t-BdrVrGG9QyDk,9163
103
107
  svc_infra/api/fastapi/routers/ping.py,sha256=71DGaklMAkCL8g_NHdQk9LXG7C_cVem1p8qtapGtnBk,576
104
- svc_infra/api/fastapi/setup.py,sha256=zm7mgyhTZVFNbrRaCerBN8fDfglpWStAYLMZHfVmEjU,9574
108
+ svc_infra/api/fastapi/setup.py,sha256=L3BasWjBWwdSBsR2rSbHK1ccI0VNO1haL3k-bE6uLxk,10053
105
109
  svc_infra/api/fastapi/tenancy/add.py,sha256=47lhWoHjuqbgSckJBTNGz-Mo5R6W7SQ0hQDiOGC0HU0,509
106
110
  svc_infra/api/fastapi/tenancy/context.py,sha256=nYnUKKP88Xy5mZCY7B7ob9PTDAnaKIhrj7pN6s-lBqg,3308
107
111
  svc_infra/app/README.md,sha256=MqnKTVjE9txaqlcl41wAv34hWhNfc_fJXCqCTsEWNdk,5070
@@ -113,7 +117,11 @@ svc_infra/app/logging/filter.py,sha256=lPHUoCFoTP5AfwORCvwVe5z2Kltb0MwAXANwrcBAI
113
117
  svc_infra/app/logging/formats.py,sha256=65eHUMWj7aD1RG7lCYIBSkFa_B748TutrZsta0OS_-8,4657
114
118
  svc_infra/app/root.py,sha256=344EWBMJCduwzJ1BBo0yGAu15TkryuvOW4qBZ6Gk-8w,1635
115
119
  svc_infra/billing/__init__.py,sha256=AdVxgBWibsz0xWk-Z91B7HecA-EhPMSRrXWIYPBgtMA,365
120
+ svc_infra/billing/async_service.py,sha256=afJR3vqkKFm3pIrYURR0E3xUmnRcxIx2TWfeDlLV2js,4680
121
+ svc_infra/billing/jobs.py,sha256=HY1bgGCZ3JYF_C5eu539S7_Vy3ifOnq3LvloET5NWcQ,8031
116
122
  svc_infra/billing/models.py,sha256=bnCGPKfnK__6x0f0bwKYQsG2GwXjJFi3YRXnq5JYs7c,6083
123
+ svc_infra/billing/quotas.py,sha256=hreWT1ZI4f7uckAA19wlIC6JqgwBWJYrmaGsp0uqa1M,3469
124
+ svc_infra/billing/schemas.py,sha256=fjONpVnI4s4lwlcY8EBr6qHCA5GQ7vRVrxObkoECQJ8,898
117
125
  svc_infra/billing/service.py,sha256=3SDpPA3NF2lMYiOP4U99sgXpZAXaauexBfZQmYE2kvU,3727
118
126
  svc_infra/bundled_docs/README.md,sha256=FqTieL4ADODxTnig8yehV2KdHX9bASDega52bjp5n70,338
119
127
  svc_infra/bundled_docs/__init__.py,sha256=8_jF4fM-3Wf6j_mE4000_9AHcJ3tYZXO9hJY-pBEepM,63
@@ -224,10 +232,12 @@ svc_infra/docs/adr/0004-tenancy-model.md,sha256=ZaJesiWqVggrRLbTXCIyyaVNiDDjl0NW
224
232
  svc_infra/docs/adr/0005-data-lifecycle.md,sha256=XLFu2I0d_6Oc7e-MOy9UE_UuCkVHCvhWy8rUlVBeAcE,4897
225
233
  svc_infra/docs/adr/0006-ops-slos-and-metrics.md,sha256=Qd17l0RKGXczLs2AKJewCAxr2g0SP7BpcLLViGknJnE,2453
226
234
  svc_infra/docs/adr/0007-docs-and-sdks.md,sha256=uQ-q-5omaOXPL5tW5q0_1FE9P_OmO9aDHXqWSGme3eg,4481
227
- svc_infra/docs/adr/0008-billing-primitives.md,sha256=trqzGWsyk_QXNtVRHXcTayEV4Sx1Zjhis93bCnsqDvo,5544
235
+ svc_infra/docs/adr/0008-billing-primitives.md,sha256=6em0RYeDAQScN7oSZfD_XslzrzIZZ-qykROJixCcEQs,8479
228
236
  svc_infra/docs/adr/0009-acceptance-harness.md,sha256=jDmoWn2uJTeK28YZo75YR1ym6NdgcmPOlMfupZlCCBs,2146
237
+ svc_infra/docs/adr/0010-timeouts-and-resource-limits.md,sha256=tpOTjncKJAjTsDN8jSUOTNqEKHfhVcfooxfW0nnbnro,2815
229
238
  svc_infra/docs/api.md,sha256=AlPL9kBS6_dM0NrOteDQ9WqalSfKf_p9_zdy1CtGJdU,2384
230
239
  svc_infra/docs/auth.md,sha256=PRl9G4UW78cT_7c4koVh5NDlheNAr02CpJT2YFbEXto,1333
240
+ svc_infra/docs/billing.md,sha256=MArKbKhzFwMLaOMABNDRtT_2D0zGgyFZ2r54o-99v68,7884
231
241
  svc_infra/docs/cache.md,sha256=XUsJ8p6QFBWTNuqata1HJjB6accdPANipdoNAkKirqs,673
232
242
  svc_infra/docs/cli.md,sha256=w5og4SWrLyizlJAJiFgcWu2jDSc1Wj3NCYqzbbvg8VE,1702
233
243
  svc_infra/docs/contributing.md,sha256=a0PhmzCLFw8S3odxFbI3p5_FOiPMZLrxk15Ujrk7ao4,1175
@@ -239,23 +249,27 @@ svc_infra/docs/getting-started.md,sha256=B1ns6Zm_LOGtncuBafxVb2yGHSqRkUsncytJw6R
239
249
  svc_infra/docs/idempotency.md,sha256=jvemdVY4g6xoHW38OAZIS5JxA3SdK8a0iAayx18kIbk,3890
240
250
  svc_infra/docs/jobs.md,sha256=iyDPo6oo7fJdBZ9e6Qt9x_kX4sX7_TUUdY89CFiZ61I,1586
241
251
  svc_infra/docs/observability.md,sha256=qzu44CSmGwjdLkBj0TQ1LBMmXd7BO2hmJSdByZlBXck,1021
242
- svc_infra/docs/ops.md,sha256=to47s3Z66-wQ4WvwEnD3xMrcmvKf-lowBqWqS-2n2HQ,1306
243
- svc_infra/docs/rate-limiting.md,sha256=W_96ch1dIjD9NP9Ma45UgsNTqDh6wlMbhJgAN3Uc4do,3983
252
+ svc_infra/docs/ops.md,sha256=Tvqg5qvJ2RqLWbHAUlH3JPgK8yn-lNpyHb-lIpYUakg,1508
253
+ svc_infra/docs/rate-limiting.md,sha256=j1df-D9t5sJtv6HVaATfSxQrv5S8zLTo2N9S_3HHzkM,4156
244
254
  svc_infra/docs/repo-review.md,sha256=REz2zT1LXURQUB9yZiBn9ICXHkpdpBSJCRTp-AKH190,8153
245
255
  svc_infra/docs/security.md,sha256=tjr5IlBYu47Z1qs2ZHQE-Low1ZOQDtZWoKjmaNXalG4,6121
246
256
  svc_infra/docs/tenancy.md,sha256=k7mOvIQWZF1b3-CETyE8UsIPdhGjNNa9Q4j5AXAqQrQ,2023
257
+ svc_infra/docs/timeouts-and-resource-limits.md,sha256=M3AoKQlCmMvgXAGOoBWeVQ-0Fwm0t5Zz0dYUCf54Mbo,6559
247
258
  svc_infra/docs/webhooks.md,sha256=b0F2vnkxOWdYWwCBAo9i39xdi11nbgEtl05JE3e0lrE,3671
248
259
  svc_infra/dx/add.py,sha256=FAnLGP0BPm_q_VCEcpUwfj-b0mEse988chh9DHeS7GU,1474
249
260
  svc_infra/dx/changelog.py,sha256=9SD29ZzKzbGTA6kHQXiPLtb7uueL1wrRiiLE2qMzz8o,1941
250
261
  svc_infra/dx/checks.py,sha256=R6YqRvpKPr9zQgif4QVx2_Zl4s9YjehSkAvwlxK46lI,2267
262
+ svc_infra/http/__init__.py,sha256=K79-aGyq_JdsxhyxispQlnygvf9LhU0_NJQcFYlWB9I,249
263
+ svc_infra/http/client.py,sha256=EezhoBqgJYfbRvX5fWNiMB3DhWq96fV3ZC7nDvUi9dQ,2025
251
264
  svc_infra/jobs/builtins/outbox_processor.py,sha256=VZoehNyjdaV_MmV74WMcbZR6z9E3VFMtZC-pxEwK0x0,1247
252
- svc_infra/jobs/builtins/webhook_delivery.py,sha256=z_cl6YKwnduGjGaB8ZoUpKhFcEAhUZqqBma8v2FO1so,2982
265
+ svc_infra/jobs/builtins/webhook_delivery.py,sha256=ID0V1r0OgNRlvh8zU_DQaXeZtAMQGaaTTUvqUzZG5JQ,3547
253
266
  svc_infra/jobs/easy.py,sha256=eix-OxWeE3vdkY3GGNoYM0GAyOxc928SpiSzMkr9k0A,977
254
267
  svc_infra/jobs/loader.py,sha256=LFO6gOacj6rT698vkDg0YfcHDRTue4zus3Nl9QrS5R0,1164
255
268
  svc_infra/jobs/queue.py,sha256=KNpYU_za8B7mmmWY6eWDohSRYy7VIKHyWAGD1qkXUOw,2816
256
269
  svc_infra/jobs/redis_queue.py,sha256=wgmWKslF1dkYscJe49UgUX7gwEuGyOUWEb0-pn82I3g,7543
270
+ svc_infra/jobs/runner.py,sha256=Cxs1pvcGxK2GCY5r7V6DGRgxadKrIX2tXPGIK6tEx98,2746
257
271
  svc_infra/jobs/scheduler.py,sha256=dTUEEyEuTVHNmJT8wPdMu4YjnTN7R_YW67gtCKpqC7M,1180
258
- svc_infra/jobs/worker.py,sha256=T2A575_mnieJHPOYU_FseubLA_HQf9pB4CkRgzRJBHU,694
272
+ svc_infra/jobs/worker.py,sha256=_Xfnhot3WOpDJNtCO5mSds-10sN1Ye2ltDBo_WZ2VpY,1099
259
273
  svc_infra/mcp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
260
274
  svc_infra/mcp/svc_infra_mcp.py,sha256=ELX4jUOrHa55asCu7q4jhDvfB9EFPNwogqX3fnSa-z0,3489
261
275
  svc_infra/obs/README.md,sha256=pmd6AyFZW3GCCi0sr3uTHrPj5KgAI8rrXw8QPkrf1R8,8021
@@ -309,7 +323,7 @@ svc_infra/security/add.py,sha256=VrGPDUSXnGX3caNXi4BbqNllSQ5YGsZzBtKESWXDJzA,611
309
323
  svc_infra/security/audit.py,sha256=r_OrXAz5uIa2o5nVD-8lsWqzggRGDKfp2sWd8URlz-E,4355
310
324
  svc_infra/security/audit_service.py,sha256=Xd5V7Iz6PS4YpxmLyJypnaqr8poaaleKwAI2uFF7y1A,2351
311
325
  svc_infra/security/headers.py,sha256=1VkAe-IWzPYfHxeZAnv9QwtzD7fP9NBTJw3mflW17bQ,1461
312
- svc_infra/security/hibp.py,sha256=VrPoz2vjHEC3AjC4f-IyRWMx43qN3Xpb5pYoSQqQSAE,2728
326
+ svc_infra/security/hibp.py,sha256=dTJXEdUNxLpxS5Eq8vUuWUdzDXh_b3_OjmvlR5jPHvQ,2894
313
327
  svc_infra/security/jwt_rotation.py,sha256=VXPRQeSCoJEl2kQnIJZELWIS0-rGUZrxCdH4Fr47vig,1767
314
328
  svc_infra/security/lockout.py,sha256=KdKN9FWejuzHRKS9jXzi_f3-lNF6QZyiEDBXCej0LSY,2804
315
329
  svc_infra/security/models.py,sha256=US5jxgeZf7C_tWW3QZRj5RTuRZE_yS6RHZBEK0ea9tA,9535
@@ -325,7 +339,7 @@ svc_infra/webhooks/fastapi.py,sha256=BCNvGNxukf6dC2a4i-6en-PrjBGV19YvCWOot5lXWsA
325
339
  svc_infra/webhooks/router.py,sha256=6JvAVPMEth_xxHX-IsIOcyMgHX7g1H0OVxVXKLuMp9w,1596
326
340
  svc_infra/webhooks/service.py,sha256=hh-rw0otc00vipZ998XaV5mHsk0IDGYqon0FnhaGr60,2229
327
341
  svc_infra/webhooks/signing.py,sha256=NCwdZzmravUe7HVIK_uXK0qqf12FG-_MVsgPvOw6lsM,784
328
- svc_infra-0.1.629.dist-info/METADATA,sha256=er59ifizb7n0wnHT4qtUIMkCC1l5kx5yol0WXlyw-cw,8748
329
- svc_infra-0.1.629.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
330
- svc_infra-0.1.629.dist-info/entry_points.txt,sha256=6x_nZOsjvn6hRZsMgZLgTasaCSKCgAjsGhACe_CiP0U,48
331
- svc_infra-0.1.629.dist-info/RECORD,,
342
+ svc_infra-0.1.630.dist-info/METADATA,sha256=lr5YAY1xGvRrngGSPgH2cfCOLACfM-YFLAR21lCX_NY,8748
343
+ svc_infra-0.1.630.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
344
+ svc_infra-0.1.630.dist-info/entry_points.txt,sha256=6x_nZOsjvn6hRZsMgZLgTasaCSKCgAjsGhACe_CiP0U,48
345
+ svc_infra-0.1.630.dist-info/RECORD,,