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.

@@ -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, object]:
24
- topics: Dict[str, object] = {}
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
- import importlib.resources as ir
27
-
28
- pkg_docs = ir.files("svc_infra.bundled_docs")
29
- for res in pkg_docs.iterdir():
30
- if res.name.endswith(".md"):
31
- topics[Path(res.name).stem.replace(" ", "-")] = res
32
- except Exception:
33
- pass
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() if not fs else {}
69
- names.extend(fs.keys())
70
- names.extend(pkg.keys())
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
- if not fs:
94
- pkg = _discover_pkg_topics()
95
- if name in pkg:
96
- res = pkg[name]
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
- content = getattr(res, "read_text", None)
104
- if callable(content):
105
- text = content(encoding="utf-8", errors="replace")
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
- return _show_pkg
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() if not fs else {}
166
- for name, path in fs.items():
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
- for name in sorted(pkg.keys()):
173
- typer.echo(f"{name}\t(bundled)")
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
- if not fs:
185
- pkg = _discover_pkg_topics()
186
- if topic in pkg:
187
- try:
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")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: svc-infra
3
- Version: 0.1.616
3
+ Version: 0.1.618
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
@@ -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=8A_J6JdU-cs9QYqP6Ufbp4vDkiH-H6CsZwege1nqf24,3855
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=uRoClC68-FRLGkHyTYds3h2xKE_4STzkudB6T72DiKo,7611
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.616.dist-info/METADATA,sha256=8UIJLzGOQ3LgK55oyA9V_dGAg_i33C5wou1tkDajCzM,8106
300
- svc_infra-0.1.616.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
301
- svc_infra-0.1.616.dist-info/entry_points.txt,sha256=6x_nZOsjvn6hRZsMgZLgTasaCSKCgAjsGhACe_CiP0U,48
302
- svc_infra-0.1.616.dist-info/RECORD,,
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,,