svc-infra 0.1.614__py3-none-any.whl → 0.1.616__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,5 @@
1
+ # Bundled Docs
2
+
3
+ This directory contains a minimal set of Markdown files that the `svc-infra docs` CLI can fall back to when the project running the CLI doesn't have a local `docs/` directory.
4
+
5
+ You can add more topics here as needed; each `*.md` file becomes a topic named after its stem (e.g., `getting-started.md` -> `getting-started`).
@@ -0,0 +1 @@
1
+ # Bundled docs package for zip-safe importlib.resources access
@@ -0,0 +1,6 @@
1
+ # Getting Started
2
+
3
+ Welcome to svc-infra docs. Use `svc-infra docs list` to see topics.
4
+
5
+ - This content is bundled with the package.
6
+ - If your project doesn't have a local `docs/` folder, you'll still see this.
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,203 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Dict, List
6
+
7
+ import click
8
+ import typer
9
+ from typer.core import TyperGroup
10
+
11
+ from svc_infra.app.root import resolve_project_root
12
+
13
+
14
+ def _discover_fs_topics(docs_dir: Path) -> Dict[str, Path]:
15
+ topics: Dict[str, Path] = {}
16
+ if docs_dir.exists() and docs_dir.is_dir():
17
+ for p in sorted(docs_dir.glob("*.md")):
18
+ if p.is_file():
19
+ topics[p.stem.replace(" ", "-")] = p
20
+ return topics
21
+
22
+
23
+ def _discover_pkg_topics() -> Dict[str, object]:
24
+ topics: Dict[str, object] = {}
25
+ 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
34
+ return topics
35
+
36
+
37
+ def _resolve_docs_dir(ctx: click.Context) -> Path | None:
38
+ # CLI option takes precedence; walk up parent contexts because Typer
39
+ # executes subcommands in child contexts that do not inherit params.
40
+ current: click.Context | None = ctx
41
+ while current is not None:
42
+ docs_dir = (current.params or {}).get("docs_dir")
43
+ if docs_dir:
44
+ path = docs_dir if isinstance(docs_dir, Path) else Path(docs_dir)
45
+ path = path.expanduser()
46
+ if path.exists():
47
+ return path
48
+ current = current.parent
49
+ # Env var next
50
+ env_dir = os.getenv("SVC_INFRA_DOCS_DIR")
51
+ if env_dir:
52
+ p = Path(env_dir).expanduser()
53
+ if p.exists():
54
+ return p
55
+ # Project docs
56
+ root = resolve_project_root()
57
+ proj_docs = root / "docs"
58
+ if proj_docs.exists():
59
+ return proj_docs
60
+ return None
61
+
62
+
63
+ class DocsGroup(TyperGroup):
64
+ def list_commands(self, ctx: click.Context) -> List[str]:
65
+ names: List[str] = list(super().list_commands(ctx) or [])
66
+ dir_to_use = _resolve_docs_dir(ctx)
67
+ 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())
71
+ # Deduplicate and sort
72
+ uniq = sorted({*names})
73
+ return uniq
74
+
75
+ def get_command(self, ctx: click.Context, name: str) -> click.Command | None:
76
+ # Built-ins first (e.g., list)
77
+ cmd = super().get_command(ctx, name)
78
+ if cmd is not None:
79
+ return cmd
80
+
81
+ # Dynamic topic resolution
82
+ dir_to_use = _resolve_docs_dir(ctx)
83
+ fs = _discover_fs_topics(dir_to_use) if dir_to_use else {}
84
+ if name in fs:
85
+ file_path = fs[name]
86
+
87
+ @click.command(name=name)
88
+ def _show_fs() -> None:
89
+ click.echo(file_path.read_text(encoding="utf-8", errors="replace"))
90
+
91
+ return _show_fs
92
+
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
102
+
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}")
112
+
113
+ return _show_pkg
114
+
115
+ return None
116
+
117
+
118
+ def register(app: typer.Typer) -> None:
119
+ """Register the `docs` command group with dynamic topic subcommands."""
120
+
121
+ docs_app = typer.Typer(no_args_is_help=True, add_completion=False, cls=DocsGroup)
122
+
123
+ @docs_app.callback(invoke_without_command=True)
124
+ def _docs_options(
125
+ docs_dir: Path | None = typer.Option(
126
+ None,
127
+ "--docs-dir",
128
+ help="Path to a docs directory to read from (overrides env/project root)",
129
+ ),
130
+ topic: str | None = typer.Option(None, "--topic", help="Topic to show directly"),
131
+ ) -> None:
132
+ """Support --docs-dir and --topic at group level."""
133
+ if topic:
134
+ ctx = click.get_current_context()
135
+ dir_to_use = _resolve_docs_dir(ctx)
136
+ fs = _discover_fs_topics(dir_to_use) if dir_to_use else {}
137
+ if topic in fs:
138
+ typer.echo(fs[topic].read_text(encoding="utf-8", errors="replace"))
139
+ 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
+ raise typer.BadParameter(f"Unknown topic: {topic}")
158
+
159
+ @docs_app.command("list", help="List available documentation topics")
160
+ def list_topics() -> None:
161
+ ctx = click.get_current_context()
162
+ root = resolve_project_root()
163
+ dir_to_use = _resolve_docs_dir(ctx)
164
+ 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():
167
+ try:
168
+ rel = path.relative_to(root)
169
+ typer.echo(f"{name}\t{rel}")
170
+ except Exception:
171
+ typer.echo(f"{name}\t{path}")
172
+ for name in sorted(pkg.keys()):
173
+ typer.echo(f"{name}\t(bundled)")
174
+
175
+ # Also support a generic "show" command
176
+ @docs_app.command("show", help="Show docs for a topic (alternative to dynamic subcommand)")
177
+ def show(topic: str) -> None:
178
+ ctx = click.get_current_context()
179
+ dir_to_use = _resolve_docs_dir(ctx)
180
+ fs = _discover_fs_topics(dir_to_use) if dir_to_use else {}
181
+ if topic in fs:
182
+ typer.echo(fs[topic].read_text(encoding="utf-8", errors="replace"))
183
+ 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}")
201
+ raise typer.BadParameter(f"Unknown topic: {topic}")
202
+
203
+ 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.614
3
+ Version: 0.1.616
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
@@ -115,6 +115,9 @@ svc_infra/app/root.py,sha256=344EWBMJCduwzJ1BBo0yGAu15TkryuvOW4qBZ6Gk-8w,1635
115
115
  svc_infra/billing/__init__.py,sha256=AdVxgBWibsz0xWk-Z91B7HecA-EhPMSRrXWIYPBgtMA,365
116
116
  svc_infra/billing/models.py,sha256=bnCGPKfnK__6x0f0bwKYQsG2GwXjJFi3YRXnq5JYs7c,6083
117
117
  svc_infra/billing/service.py,sha256=3SDpPA3NF2lMYiOP4U99sgXpZAXaauexBfZQmYE2kvU,3727
118
+ svc_infra/bundled_docs/README.md,sha256=FqTieL4ADODxTnig8yehV2KdHX9bASDega52bjp5n70,338
119
+ svc_infra/bundled_docs/__init__.py,sha256=8_jF4fM-3Wf6j_mE4000_9AHcJ3tYZXO9hJY-pBEepM,63
120
+ svc_infra/bundled_docs/getting-started.md,sha256=JaMOgRUK_ajaX4SCtiE3GrhQ81wMwng6y46t0032ftU,210
118
121
  svc_infra/cache/README.md,sha256=ZgIpmE0UVlGktp2nXUYv6FKJATCdkR_01v-GGxHN6Ao,10795
119
122
  svc_infra/cache/__init__.py,sha256=Fz3NS81jrY5sLikRhITCeHDT4MlOLcbMed5EjVecSAg,956
120
123
  svc_infra/cache/backend.py,sha256=-dbZ2qkhebzbKosQqgvBNb01A-2_jGt6_0WmJhPoHy8,4418
@@ -126,9 +129,9 @@ svc_infra/cache/resources.py,sha256=BhvPAZvCQ-fitUdniGEOOE4g1ZvljdCA_R5pR8WfJz4,
126
129
  svc_infra/cache/tags.py,sha256=9URw4BRlnb4QFAYpDI36fMms6642xq4TeV9jqsEjzE8,2625
127
130
  svc_infra/cache/ttl.py,sha256=_lWvNx1CTE4RcFEOUYkADd7_k4I13SLmtK0AMRUq2OM,1945
128
131
  svc_infra/cache/utils.py,sha256=-LWr5IiJCNm3pwaoeCVlxNknnO2ChNKFcAGlFU98kjg,4856
129
- svc_infra/cli/__init__.py,sha256=Bdmx-qMHwPINg1S6nIsU7f4Qfrg8QmDiIQ11BblEuL4,864
132
+ svc_infra/cli/__init__.py,sha256=enGeMhxOjfeClic51C4QB2Car3DDZD3A9P9R_8YfYHQ,926
130
133
  svc_infra/cli/__main__.py,sha256=5BjNuyet8AY-POwoF5rGt722rHQ7tJ0Vf0UFUfzzi-I,58
131
- svc_infra/cli/cmds/__init__.py,sha256=MqXFdhTyLHua-c0bJGm0O5kFKsS-TXrA48PJy5u5zFU,958
134
+ svc_infra/cli/cmds/__init__.py,sha256=xKVXpMP_fD7jfmYonxWxh5LKHUQiuIFaJgkpqtkPt-M,1051
132
135
  svc_infra/cli/cmds/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
133
136
  svc_infra/cli/cmds/db/nosql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
134
137
  svc_infra/cli/cmds/db/nosql/mongo/README.md,sha256=0u3XLeoBd0XQzXwwfEiFISMIij11TJ9iOGzrysBvsFk,1788
@@ -139,6 +142,7 @@ svc_infra/cli/cmds/db/sql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJW
139
142
  svc_infra/cli/cmds/db/sql/alembic_cmds.py,sha256=kkAu8sfBLWbb9ApMS95b7b_c6GifqvPaRsO7K8icMVI,9649
140
143
  svc_infra/cli/cmds/db/sql/sql_export_cmds.py,sha256=6MxoQO-9upoXg0cl1RHIqz96yXFVGidiBYp_ewhB0E0,2700
141
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
142
146
  svc_infra/cli/cmds/dx/__init__.py,sha256=wQtl3-kOgoESlpVkjl3YFtqkOnQSIvVsOdutiaZFejM,197
143
147
  svc_infra/cli/cmds/dx/dx_cmds.py,sha256=XTKUJzS3UIYn6h3CHzDEWKYJaWn0TzGiUCq3OeW27E0,3326
144
148
  svc_infra/cli/cmds/help.py,sha256=wGfZFMYaR2ZPwW2JwKDU7M3m4AtdCd8GRQ412AmEBUM,758
@@ -292,7 +296,7 @@ svc_infra/webhooks/fastapi.py,sha256=BCNvGNxukf6dC2a4i-6en-PrjBGV19YvCWOot5lXWsA
292
296
  svc_infra/webhooks/router.py,sha256=6JvAVPMEth_xxHX-IsIOcyMgHX7g1H0OVxVXKLuMp9w,1596
293
297
  svc_infra/webhooks/service.py,sha256=hWgiJRXKBwKunJOx91C7EcLUkotDtD3Xp0RT6vj2IC0,1797
294
298
  svc_infra/webhooks/signing.py,sha256=NCwdZzmravUe7HVIK_uXK0qqf12FG-_MVsgPvOw6lsM,784
295
- svc_infra-0.1.614.dist-info/METADATA,sha256=zY3M6rLvXJH2tE7oHwD07QN7zEMfD4yjVP0AbYrMV48,8106
296
- svc_infra-0.1.614.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
297
- svc_infra-0.1.614.dist-info/entry_points.txt,sha256=6x_nZOsjvn6hRZsMgZLgTasaCSKCgAjsGhACe_CiP0U,48
298
- svc_infra-0.1.614.dist-info/RECORD,,
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,,