svc-infra 0.1.613__py3-none-any.whl → 0.1.615__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.

@@ -46,6 +46,7 @@ def problem_response(
46
46
  code: str | None = None,
47
47
  errors: list[dict] | None = None,
48
48
  trace_id: str | None = None,
49
+ headers: dict[str, str] | None = None,
49
50
  ) -> Response:
50
51
  body: Dict[str, Any] = {
51
52
  "type": type_uri,
@@ -62,7 +63,7 @@ def problem_response(
62
63
  body["errors"] = errors
63
64
  if trace_id:
64
65
  body["trace_id"] = trace_id
65
- return JSONResponse(status_code=status, content=body, media_type=PROBLEM_MT)
66
+ return JSONResponse(status_code=status, content=body, media_type=PROBLEM_MT, headers=headers)
66
67
 
67
68
 
68
69
  def register_error_handlers(app):
@@ -104,14 +105,25 @@ def register_error_handlers(app):
104
105
  @app.exception_handler(HTTPException)
105
106
  async def handle_http_exception(request: Request, exc: HTTPException):
106
107
  trace_id = _trace_id_from_request(request)
107
- title = {401: "Unauthorized", 403: "Forbidden", 404: "Not Found"}.get(
108
- exc.status_code, "Error"
109
- )
108
+ title = {
109
+ 401: "Unauthorized",
110
+ 403: "Forbidden",
111
+ 404: "Not Found",
112
+ 429: "Too Many Requests",
113
+ }.get(exc.status_code, "Error")
110
114
  detail = (
111
115
  exc.detail
112
116
  if not IS_PROD or exc.status_code < 500
113
117
  else "Something went wrong. Please contact support."
114
118
  )
119
+ # Preserve headers set on the exception (e.g., Retry-After for rate limits)
120
+ hdrs: dict[str, str] | None = None
121
+ try:
122
+ if getattr(exc, "headers", None):
123
+ # FastAPI/Starlette exceptions store headers as a dict[str, str]
124
+ hdrs = dict(getattr(exc, "headers")) # type: ignore[arg-type]
125
+ except Exception:
126
+ hdrs = None
115
127
  return problem_response(
116
128
  status=exc.status_code,
117
129
  title=title,
@@ -119,19 +131,29 @@ def register_error_handlers(app):
119
131
  code=title.replace(" ", "_").upper(),
120
132
  instance=str(request.url),
121
133
  trace_id=trace_id,
134
+ headers=hdrs,
122
135
  )
123
136
 
124
137
  @app.exception_handler(StarletteHTTPException)
125
138
  async def handle_starlette_http_exception(request: Request, exc: StarletteHTTPException):
126
139
  trace_id = _trace_id_from_request(request)
127
- title = {401: "Unauthorized", 403: "Forbidden", 404: "Not Found"}.get(
128
- exc.status_code, "Error"
129
- )
140
+ title = {
141
+ 401: "Unauthorized",
142
+ 403: "Forbidden",
143
+ 404: "Not Found",
144
+ 429: "Too Many Requests",
145
+ }.get(exc.status_code, "Error")
130
146
  detail = (
131
147
  exc.detail
132
148
  if not IS_PROD or exc.status_code < 500
133
149
  else "Something went wrong. Please contact support."
134
150
  )
151
+ hdrs: dict[str, str] | None = None
152
+ try:
153
+ if getattr(exc, "headers", None):
154
+ hdrs = dict(getattr(exc, "headers")) # type: ignore[arg-type]
155
+ except Exception:
156
+ hdrs = None
135
157
  return problem_response(
136
158
  status=exc.status_code,
137
159
  title=title,
@@ -139,6 +161,7 @@ def register_error_handlers(app):
139
161
  code=title.replace(" ", "_").upper(),
140
162
  instance=str(request.url),
141
163
  trace_id=trace_id,
164
+ headers=hdrs,
142
165
  )
143
166
 
144
167
  @app.exception_handler(IntegrityError)
svc_infra/cli/__init__.py CHANGED
@@ -6,6 +6,7 @@ from svc_infra.cli.cmds import (
6
6
  _HELP,
7
7
  jobs_app,
8
8
  register_alembic,
9
+ register_docs,
9
10
  register_dx,
10
11
  register_mongo,
11
12
  register_mongo_scaffold,
@@ -40,6 +41,9 @@ app.add_typer(jobs_app, name="jobs")
40
41
  # -- sdk commands ---
41
42
  register_sdk(app)
42
43
 
44
+ # -- docs commands ---
45
+ register_docs(app)
46
+
43
47
 
44
48
  def main():
45
49
  app()
@@ -5,6 +5,7 @@ from svc_infra.cli.cmds.db.nosql.mongo.mongo_scaffold_cmds import (
5
5
  from svc_infra.cli.cmds.db.sql.alembic_cmds import register as register_alembic
6
6
  from svc_infra.cli.cmds.db.sql.sql_export_cmds import register as register_sql_export
7
7
  from svc_infra.cli.cmds.db.sql.sql_scaffold_cmds import register as register_sql_scaffold
8
+ from svc_infra.cli.cmds.docs.docs_cmds import register as register_docs
8
9
  from svc_infra.cli.cmds.dx import register_dx
9
10
  from svc_infra.cli.cmds.jobs.jobs_cmds import app as jobs_app
10
11
  from svc_infra.cli.cmds.obs.obs_cmds import register as register_obs
@@ -22,5 +23,6 @@ __all__ = [
22
23
  "jobs_app",
23
24
  "register_sdk",
24
25
  "register_dx",
26
+ "register_docs",
25
27
  "_HELP",
26
28
  ]
@@ -0,0 +1,69 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Dict, List, Tuple
5
+
6
+ import typer
7
+
8
+ from svc_infra.app.root import resolve_project_root
9
+
10
+
11
+ def _discover_docs(root: Path) -> List[Tuple[str, Path]]:
12
+ """Return a list of (topic, file_path) for top-level Markdown files under docs/.
13
+
14
+ Topic is the filename stem (e.g. security.md -> "security").
15
+ """
16
+ docs_dir = root / "docs"
17
+ topics: List[Tuple[str, Path]] = []
18
+ if not docs_dir.exists() or not docs_dir.is_dir():
19
+ return topics
20
+ for p in sorted(docs_dir.glob("*.md")):
21
+ if p.is_file():
22
+ topics.append((p.stem.replace(" ", "-"), p))
23
+ return topics
24
+
25
+
26
+ def register(app: typer.Typer) -> None:
27
+ """Register the `docs` command group and dynamic topic subcommands."""
28
+
29
+ root = resolve_project_root()
30
+ discovered = _discover_docs(root)
31
+
32
+ # Build help text listing available topics
33
+ if discovered:
34
+ topic_names = ", ".join(name for name, _ in discovered)
35
+ docs_help = (
36
+ f"Show docs from the repository's docs/ directory.\n\nAvailable topics: {topic_names}"
37
+ )
38
+ else:
39
+ docs_help = "Show docs from the repository's docs/ directory.\n\nNo topics discovered."
40
+
41
+ docs_app = typer.Typer(no_args_is_help=True, help=docs_help, add_completion=False)
42
+
43
+ @docs_app.command("list", help="List available documentation topics")
44
+ def list_topics() -> None:
45
+ for name, path in discovered:
46
+ typer.echo(f"{name}\t{path.relative_to(root)}")
47
+
48
+ # Freeze mapping for use in dynamic commands and generic fallback
49
+ topic_map: Dict[str, Path] = {name: path for name, path in discovered}
50
+
51
+ def _make_topic_cmd(topic: str, file_path: Path):
52
+ @docs_app.command(name=topic, help=f"Show docs for topic: {topic}")
53
+ def _show_topic() -> None: # noqa: WPS430 (nested function OK for closure)
54
+ content = file_path.read_text(encoding="utf-8", errors="replace")
55
+ typer.echo(content)
56
+
57
+ for name, path in discovered:
58
+ _make_topic_cmd(name, path)
59
+
60
+ # Optional generic fallback: allow `svc-infra docs --topic <name>`
61
+ @docs_app.callback(invoke_without_command=True)
62
+ def _maybe_show_topic(topic: str = typer.Option(None, "--topic", help="Topic to show")) -> None: # type: ignore[no-redef]
63
+ if topic:
64
+ p = topic_map.get(topic)
65
+ if not p:
66
+ raise typer.BadParameter(f"Unknown topic: {topic}")
67
+ typer.echo(p.read_text(encoding="utf-8", errors="replace"))
68
+
69
+ 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.613
3
+ Version: 0.1.615
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
@@ -76,7 +76,7 @@ svc_infra/api/fastapi/middleware/debug.py,sha256=H3jBKvdPkr2KHUEMGnqWBPZ0tG6Fgw-
76
76
  svc_infra/api/fastapi/middleware/errors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
77
77
  svc_infra/api/fastapi/middleware/errors/catchall.py,sha256=TG0W71UCDbfgLNdIaIv6mBSwZA_etMp5GquwKcAwYbI,1842
78
78
  svc_infra/api/fastapi/middleware/errors/exceptions.py,sha256=857_bdMgQugf8rb7U6ZaTZV3aiFTfBzFaUg80YUfAYE,475
79
- svc_infra/api/fastapi/middleware/errors/handlers.py,sha256=9Em_Z6PXTm9gM9ulEYwY02DqRuNxz6LLh8z5CMheruY,7128
79
+ svc_infra/api/fastapi/middleware/errors/handlers.py,sha256=pQMVs5n627vcKkDFEaUzx5wCYeUcU-h6acWzh27RIEQ,7993
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
@@ -126,9 +126,9 @@ svc_infra/cache/resources.py,sha256=BhvPAZvCQ-fitUdniGEOOE4g1ZvljdCA_R5pR8WfJz4,
126
126
  svc_infra/cache/tags.py,sha256=9URw4BRlnb4QFAYpDI36fMms6642xq4TeV9jqsEjzE8,2625
127
127
  svc_infra/cache/ttl.py,sha256=_lWvNx1CTE4RcFEOUYkADd7_k4I13SLmtK0AMRUq2OM,1945
128
128
  svc_infra/cache/utils.py,sha256=-LWr5IiJCNm3pwaoeCVlxNknnO2ChNKFcAGlFU98kjg,4856
129
- svc_infra/cli/__init__.py,sha256=Bdmx-qMHwPINg1S6nIsU7f4Qfrg8QmDiIQ11BblEuL4,864
129
+ svc_infra/cli/__init__.py,sha256=enGeMhxOjfeClic51C4QB2Car3DDZD3A9P9R_8YfYHQ,926
130
130
  svc_infra/cli/__main__.py,sha256=5BjNuyet8AY-POwoF5rGt722rHQ7tJ0Vf0UFUfzzi-I,58
131
- svc_infra/cli/cmds/__init__.py,sha256=MqXFdhTyLHua-c0bJGm0O5kFKsS-TXrA48PJy5u5zFU,958
131
+ svc_infra/cli/cmds/__init__.py,sha256=xKVXpMP_fD7jfmYonxWxh5LKHUQiuIFaJgkpqtkPt-M,1051
132
132
  svc_infra/cli/cmds/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
133
133
  svc_infra/cli/cmds/db/nosql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
134
134
  svc_infra/cli/cmds/db/nosql/mongo/README.md,sha256=0u3XLeoBd0XQzXwwfEiFISMIij11TJ9iOGzrysBvsFk,1788
@@ -139,6 +139,7 @@ svc_infra/cli/cmds/db/sql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJW
139
139
  svc_infra/cli/cmds/db/sql/alembic_cmds.py,sha256=kkAu8sfBLWbb9ApMS95b7b_c6GifqvPaRsO7K8icMVI,9649
140
140
  svc_infra/cli/cmds/db/sql/sql_export_cmds.py,sha256=6MxoQO-9upoXg0cl1RHIqz96yXFVGidiBYp_ewhB0E0,2700
141
141
  svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py,sha256=eNTCqHXOxgl9H3WTbGVn9BHXYwCpjIEJsDqhEFdrYMM,4613
142
+ svc_infra/cli/cmds/docs/docs_cmds.py,sha256=3GCoRKjnRr_re__7TuelDbcaouJawHlz0AphYyI0DIE,2575
142
143
  svc_infra/cli/cmds/dx/__init__.py,sha256=wQtl3-kOgoESlpVkjl3YFtqkOnQSIvVsOdutiaZFejM,197
143
144
  svc_infra/cli/cmds/dx/dx_cmds.py,sha256=XTKUJzS3UIYn6h3CHzDEWKYJaWn0TzGiUCq3OeW27E0,3326
144
145
  svc_infra/cli/cmds/help.py,sha256=wGfZFMYaR2ZPwW2JwKDU7M3m4AtdCd8GRQ412AmEBUM,758
@@ -292,7 +293,7 @@ svc_infra/webhooks/fastapi.py,sha256=BCNvGNxukf6dC2a4i-6en-PrjBGV19YvCWOot5lXWsA
292
293
  svc_infra/webhooks/router.py,sha256=6JvAVPMEth_xxHX-IsIOcyMgHX7g1H0OVxVXKLuMp9w,1596
293
294
  svc_infra/webhooks/service.py,sha256=hWgiJRXKBwKunJOx91C7EcLUkotDtD3Xp0RT6vj2IC0,1797
294
295
  svc_infra/webhooks/signing.py,sha256=NCwdZzmravUe7HVIK_uXK0qqf12FG-_MVsgPvOw6lsM,784
295
- svc_infra-0.1.613.dist-info/METADATA,sha256=3sERXZccSqWjWt03Ceu0z12Q36BWFahfFlaiV6yom0U,8106
296
- svc_infra-0.1.613.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
297
- svc_infra-0.1.613.dist-info/entry_points.txt,sha256=6x_nZOsjvn6hRZsMgZLgTasaCSKCgAjsGhACe_CiP0U,48
298
- svc_infra-0.1.613.dist-info/RECORD,,
296
+ svc_infra-0.1.615.dist-info/METADATA,sha256=VFmBWPFEQHoLsz2vTyHO2DMDkN9C70YlI3zAJolFyJg,8106
297
+ svc_infra-0.1.615.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
298
+ svc_infra-0.1.615.dist-info/entry_points.txt,sha256=6x_nZOsjvn6hRZsMgZLgTasaCSKCgAjsGhACe_CiP0U,48
299
+ svc_infra-0.1.615.dist-info/RECORD,,