svc-infra 0.1.616__py3-none-any.whl → 0.1.618__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.
- svc_infra/api/fastapi/middleware/ratelimit.py +8 -0
- svc_infra/cli/cmds/docs/docs_cmds.py +55 -70
- {svc_infra-0.1.616.dist-info → svc_infra-0.1.618.dist-info}/METADATA +1 -1
- {svc_infra-0.1.616.dist-info → svc_infra-0.1.618.dist-info}/RECORD +6 -6
- {svc_infra-0.1.616.dist-info → svc_infra-0.1.618.dist-info}/WHEEL +0 -0
- {svc_infra-0.1.616.dist-info → svc_infra-0.1.618.dist-info}/entry_points.txt +0 -0
|
@@ -27,6 +27,10 @@ class SimpleRateLimitMiddleware(BaseHTTPMiddleware):
|
|
|
27
27
|
limit_resolver=None,
|
|
28
28
|
# If True, automatically scopes the bucket key by tenant id when available
|
|
29
29
|
scope_by_tenant: bool = False,
|
|
30
|
+
# When True, allows unresolved tenant IDs to fall back to an "X-Tenant-Id" header value.
|
|
31
|
+
# Disabled by default to avoid trusting arbitrary client-provided headers which could
|
|
32
|
+
# otherwise be used to evade per-tenant limits when authentication fails.
|
|
33
|
+
allow_untrusted_tenant_header: bool = False,
|
|
30
34
|
store: RateLimitStore | None = None,
|
|
31
35
|
):
|
|
32
36
|
super().__init__(app)
|
|
@@ -34,6 +38,7 @@ class SimpleRateLimitMiddleware(BaseHTTPMiddleware):
|
|
|
34
38
|
self.key_fn = key_fn or (lambda r: r.headers.get("X-API-Key") or r.client.host)
|
|
35
39
|
self._limit_resolver = limit_resolver
|
|
36
40
|
self.scope_by_tenant = scope_by_tenant
|
|
41
|
+
self._allow_untrusted_tenant_header = allow_untrusted_tenant_header
|
|
37
42
|
self.store = store or InMemoryRateLimitStore(limit=limit)
|
|
38
43
|
|
|
39
44
|
async def dispatch(self, request, call_next):
|
|
@@ -45,6 +50,9 @@ class SimpleRateLimitMiddleware(BaseHTTPMiddleware):
|
|
|
45
50
|
tenant_id = await _resolve_tenant_id(request)
|
|
46
51
|
except Exception:
|
|
47
52
|
tenant_id = None
|
|
53
|
+
# Fallback: read from header only if explicitly trusted
|
|
54
|
+
if not tenant_id and self._allow_untrusted_tenant_header:
|
|
55
|
+
tenant_id = request.headers.get("X-Tenant-Id") or request.headers.get("X-Tenant-ID")
|
|
48
56
|
|
|
49
57
|
key = self.key_fn(request)
|
|
50
58
|
if self.scope_by_tenant and tenant_id:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
+
from importlib.metadata import PackageNotFoundError, distribution
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from typing import Dict, List
|
|
6
7
|
|
|
@@ -20,17 +21,34 @@ def _discover_fs_topics(docs_dir: Path) -> Dict[str, Path]:
|
|
|
20
21
|
return topics
|
|
21
22
|
|
|
22
23
|
|
|
23
|
-
def _discover_pkg_topics() -> Dict[str,
|
|
24
|
-
|
|
24
|
+
def _discover_pkg_topics() -> Dict[str, Path]:
|
|
25
|
+
"""Discover docs packaged under 'docs/' in the installed distribution.
|
|
26
|
+
|
|
27
|
+
This lets 'svc-infra docs' work from external projects that don't have a
|
|
28
|
+
local docs/ directory by falling back to files shipped in the wheel.
|
|
29
|
+
"""
|
|
30
|
+
topics: Dict[str, Path] = {}
|
|
25
31
|
try:
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
dist = distribution("svc-infra")
|
|
33
|
+
except PackageNotFoundError:
|
|
34
|
+
return topics
|
|
35
|
+
|
|
36
|
+
files = getattr(dist, "files", None) or []
|
|
37
|
+
for f in files:
|
|
38
|
+
# f is a PackagePath; string form like 'docs/topic.md'
|
|
39
|
+
s = str(f)
|
|
40
|
+
if not s.startswith("docs/") or not s.endswith(".md"):
|
|
41
|
+
continue
|
|
42
|
+
name = Path(s).stem.replace(" ", "-")
|
|
43
|
+
try:
|
|
44
|
+
abs_path = dist.locate_file(f)
|
|
45
|
+
# Ensure it's a file before adding
|
|
46
|
+
abs_p = Path(abs_path)
|
|
47
|
+
if abs_p.exists() and abs_p.is_file():
|
|
48
|
+
topics[name] = abs_p
|
|
49
|
+
except Exception:
|
|
50
|
+
# best-effort; skip unreadable entries
|
|
51
|
+
continue
|
|
34
52
|
return topics
|
|
35
53
|
|
|
36
54
|
|
|
@@ -65,9 +83,10 @@ class DocsGroup(TyperGroup):
|
|
|
65
83
|
names: List[str] = list(super().list_commands(ctx) or [])
|
|
66
84
|
dir_to_use = _resolve_docs_dir(ctx)
|
|
67
85
|
fs = _discover_fs_topics(dir_to_use) if dir_to_use else {}
|
|
68
|
-
pkg = _discover_pkg_topics()
|
|
69
|
-
|
|
70
|
-
names.extend(
|
|
86
|
+
pkg = _discover_pkg_topics()
|
|
87
|
+
# FS topics win on conflicts; add both for visibility
|
|
88
|
+
names.extend([k for k in fs.keys()])
|
|
89
|
+
names.extend([k for k in pkg.keys() if k not in fs])
|
|
71
90
|
# Deduplicate and sort
|
|
72
91
|
uniq = sorted({*names})
|
|
73
92
|
return uniq
|
|
@@ -90,27 +109,16 @@ class DocsGroup(TyperGroup):
|
|
|
90
109
|
|
|
91
110
|
return _show_fs
|
|
92
111
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
@click.command(name=name)
|
|
99
|
-
def _show_pkg() -> None:
|
|
100
|
-
try:
|
|
101
|
-
import importlib.resources as ir
|
|
112
|
+
# Packaged fallback
|
|
113
|
+
pkg = _discover_pkg_topics()
|
|
114
|
+
if name in pkg:
|
|
115
|
+
file_path = pkg[name]
|
|
102
116
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
else:
|
|
107
|
-
with ir.as_file(res) as p:
|
|
108
|
-
text = Path(p).read_text(encoding="utf-8", errors="replace")
|
|
109
|
-
click.echo(text)
|
|
110
|
-
except Exception as e: # pragma: no cover
|
|
111
|
-
raise click.ClickException(f"Failed to load bundled doc: {e}")
|
|
117
|
+
@click.command(name=name)
|
|
118
|
+
def _show_pkg() -> None:
|
|
119
|
+
click.echo(file_path.read_text(encoding="utf-8", errors="replace"))
|
|
112
120
|
|
|
113
|
-
|
|
121
|
+
return _show_pkg
|
|
114
122
|
|
|
115
123
|
return None
|
|
116
124
|
|
|
@@ -137,23 +145,6 @@ def register(app: typer.Typer) -> None:
|
|
|
137
145
|
if topic in fs:
|
|
138
146
|
typer.echo(fs[topic].read_text(encoding="utf-8", errors="replace"))
|
|
139
147
|
raise typer.Exit(code=0)
|
|
140
|
-
if not fs:
|
|
141
|
-
pkg = _discover_pkg_topics()
|
|
142
|
-
if topic in pkg:
|
|
143
|
-
try:
|
|
144
|
-
import importlib.resources as ir
|
|
145
|
-
|
|
146
|
-
res = pkg[topic]
|
|
147
|
-
content = getattr(res, "read_text", None)
|
|
148
|
-
if callable(content):
|
|
149
|
-
text = content(encoding="utf-8", errors="replace")
|
|
150
|
-
else:
|
|
151
|
-
with ir.as_file(res) as p:
|
|
152
|
-
text = Path(p).read_text(encoding="utf-8", errors="replace")
|
|
153
|
-
typer.echo(text)
|
|
154
|
-
raise typer.Exit(code=0)
|
|
155
|
-
except Exception as e: # pragma: no cover
|
|
156
|
-
raise typer.BadParameter(f"Failed to load bundled topic '{topic}': {e}")
|
|
157
148
|
raise typer.BadParameter(f"Unknown topic: {topic}")
|
|
158
149
|
|
|
159
150
|
@docs_app.command("list", help="List available documentation topics")
|
|
@@ -162,15 +153,22 @@ def register(app: typer.Typer) -> None:
|
|
|
162
153
|
root = resolve_project_root()
|
|
163
154
|
dir_to_use = _resolve_docs_dir(ctx)
|
|
164
155
|
fs = _discover_fs_topics(dir_to_use) if dir_to_use else {}
|
|
165
|
-
pkg = _discover_pkg_topics()
|
|
166
|
-
|
|
156
|
+
pkg = _discover_pkg_topics()
|
|
157
|
+
|
|
158
|
+
# Print FS topics first (project/env/option), then packaged topics not shadowed by FS
|
|
159
|
+
def _print(name: str, path: Path) -> None:
|
|
167
160
|
try:
|
|
168
161
|
rel = path.relative_to(root)
|
|
169
162
|
typer.echo(f"{name}\t{rel}")
|
|
170
163
|
except Exception:
|
|
164
|
+
# For packaged topics, path will be site-packages absolute path
|
|
171
165
|
typer.echo(f"{name}\t{path}")
|
|
172
|
-
|
|
173
|
-
|
|
166
|
+
|
|
167
|
+
for name, path in fs.items():
|
|
168
|
+
_print(name, path)
|
|
169
|
+
for name, path in pkg.items():
|
|
170
|
+
if name not in fs:
|
|
171
|
+
_print(name, path)
|
|
174
172
|
|
|
175
173
|
# Also support a generic "show" command
|
|
176
174
|
@docs_app.command("show", help="Show docs for a topic (alternative to dynamic subcommand)")
|
|
@@ -181,23 +179,10 @@ def register(app: typer.Typer) -> None:
|
|
|
181
179
|
if topic in fs:
|
|
182
180
|
typer.echo(fs[topic].read_text(encoding="utf-8", errors="replace"))
|
|
183
181
|
return
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
import importlib.resources as ir
|
|
189
|
-
|
|
190
|
-
res = pkg[topic]
|
|
191
|
-
content = getattr(res, "read_text", None)
|
|
192
|
-
if callable(content):
|
|
193
|
-
text = content(encoding="utf-8", errors="replace")
|
|
194
|
-
else:
|
|
195
|
-
with ir.as_file(res) as p:
|
|
196
|
-
text = Path(p).read_text(encoding="utf-8", errors="replace")
|
|
197
|
-
typer.echo(text)
|
|
198
|
-
return
|
|
199
|
-
except Exception as e: # pragma: no cover
|
|
200
|
-
raise typer.BadParameter(f"Failed to load bundled topic '{topic}': {e}")
|
|
182
|
+
pkg = _discover_pkg_topics()
|
|
183
|
+
if topic in pkg:
|
|
184
|
+
typer.echo(pkg[topic].read_text(encoding="utf-8", errors="replace"))
|
|
185
|
+
return
|
|
201
186
|
raise typer.BadParameter(f"Unknown topic: {topic}")
|
|
202
187
|
|
|
203
188
|
app.add_typer(docs_app, name="docs")
|
|
@@ -80,7 +80,7 @@ svc_infra/api/fastapi/middleware/errors/handlers.py,sha256=pQMVs5n627vcKkDFEaUzx
|
|
|
80
80
|
svc_infra/api/fastapi/middleware/idempotency.py,sha256=vnBQgMWzJVaF8oWgfw2ATjEKCyQifDeGPUc9z1N7ebE,5051
|
|
81
81
|
svc_infra/api/fastapi/middleware/idempotency_store.py,sha256=BQN_Cq_jf_cuZRhze4EF5v0lOMQXpUWoRo7CsSTprug,5528
|
|
82
82
|
svc_infra/api/fastapi/middleware/optimistic_lock.py,sha256=9lOMBI4VNIVndXnrMmgSq4qeR7xPjNR1H9d1F71M5S8,1271
|
|
83
|
-
svc_infra/api/fastapi/middleware/ratelimit.py,sha256=
|
|
83
|
+
svc_infra/api/fastapi/middleware/ratelimit.py,sha256=f-nvsh3wqLMpDEmwPsKvVnOrM3cfC9AqQyl8eUqrUQM,4496
|
|
84
84
|
svc_infra/api/fastapi/middleware/ratelimit_store.py,sha256=LmJR8-kkW42rzOjls9lG1SBtCKjVY7L2Y_bNKHNY3-A,2553
|
|
85
85
|
svc_infra/api/fastapi/middleware/request_id.py,sha256=Iru7ypTdK_n76lwziEGDWoVF4FKS0Ps1PMASYmzK8ek,768
|
|
86
86
|
svc_infra/api/fastapi/middleware/request_size_limit.py,sha256=AcGqaB-F7Tbhg-at7ViT4Bpifst34jFneDBlUBjgo5I,1248
|
|
@@ -142,7 +142,7 @@ svc_infra/cli/cmds/db/sql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJW
|
|
|
142
142
|
svc_infra/cli/cmds/db/sql/alembic_cmds.py,sha256=kkAu8sfBLWbb9ApMS95b7b_c6GifqvPaRsO7K8icMVI,9649
|
|
143
143
|
svc_infra/cli/cmds/db/sql/sql_export_cmds.py,sha256=6MxoQO-9upoXg0cl1RHIqz96yXFVGidiBYp_ewhB0E0,2700
|
|
144
144
|
svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py,sha256=eNTCqHXOxgl9H3WTbGVn9BHXYwCpjIEJsDqhEFdrYMM,4613
|
|
145
|
-
svc_infra/cli/cmds/docs/docs_cmds.py,sha256=
|
|
145
|
+
svc_infra/cli/cmds/docs/docs_cmds.py,sha256=nvqxkqeYNSV79Eeq0Kv8PN1M8CbyS3lnrFNjHNlO53w,6661
|
|
146
146
|
svc_infra/cli/cmds/dx/__init__.py,sha256=wQtl3-kOgoESlpVkjl3YFtqkOnQSIvVsOdutiaZFejM,197
|
|
147
147
|
svc_infra/cli/cmds/dx/dx_cmds.py,sha256=XTKUJzS3UIYn6h3CHzDEWKYJaWn0TzGiUCq3OeW27E0,3326
|
|
148
148
|
svc_infra/cli/cmds/help.py,sha256=wGfZFMYaR2ZPwW2JwKDU7M3m4AtdCd8GRQ412AmEBUM,758
|
|
@@ -296,7 +296,7 @@ svc_infra/webhooks/fastapi.py,sha256=BCNvGNxukf6dC2a4i-6en-PrjBGV19YvCWOot5lXWsA
|
|
|
296
296
|
svc_infra/webhooks/router.py,sha256=6JvAVPMEth_xxHX-IsIOcyMgHX7g1H0OVxVXKLuMp9w,1596
|
|
297
297
|
svc_infra/webhooks/service.py,sha256=hWgiJRXKBwKunJOx91C7EcLUkotDtD3Xp0RT6vj2IC0,1797
|
|
298
298
|
svc_infra/webhooks/signing.py,sha256=NCwdZzmravUe7HVIK_uXK0qqf12FG-_MVsgPvOw6lsM,784
|
|
299
|
-
svc_infra-0.1.
|
|
300
|
-
svc_infra-0.1.
|
|
301
|
-
svc_infra-0.1.
|
|
302
|
-
svc_infra-0.1.
|
|
299
|
+
svc_infra-0.1.618.dist-info/METADATA,sha256=mH41WGLu0texy6G1rqK_cX5DmiJZFrhIN6kn6GgvLfE,8106
|
|
300
|
+
svc_infra-0.1.618.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
|
|
301
|
+
svc_infra-0.1.618.dist-info/entry_points.txt,sha256=6x_nZOsjvn6hRZsMgZLgTasaCSKCgAjsGhACe_CiP0U,48
|
|
302
|
+
svc_infra-0.1.618.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|