svc-infra 0.1.621__py3-none-any.whl → 0.1.622__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/cli/cmds/docs/docs_cmds.py +147 -188
- {svc_infra-0.1.621.dist-info → svc_infra-0.1.622.dist-info}/METADATA +1 -1
- {svc_infra-0.1.621.dist-info → svc_infra-0.1.622.dist-info}/RECORD +5 -5
- {svc_infra-0.1.621.dist-info → svc_infra-0.1.622.dist-info}/WHEEL +0 -0
- {svc_infra-0.1.621.dist-info → svc_infra-0.1.622.dist-info}/entry_points.txt +0 -0
|
@@ -11,36 +11,79 @@ import click
|
|
|
11
11
|
import typer
|
|
12
12
|
from typer.core import TyperGroup
|
|
13
13
|
|
|
14
|
+
from svc_infra.app.root import resolve_project_root
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
"""Normalize a topic name for stable CLI commands.
|
|
16
|
+
# ---------- small helpers ----------
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
- Strip leading/trailing whitespace
|
|
21
|
-
"""
|
|
18
|
+
|
|
19
|
+
def _norm(name: str) -> str:
|
|
22
20
|
return name.strip().lower().replace(" ", "-").replace("_", "-")
|
|
23
21
|
|
|
24
22
|
|
|
25
|
-
def
|
|
23
|
+
def _md_topics_in(dirpath: Path) -> Dict[str, Path]:
|
|
26
24
|
topics: Dict[str, Path] = {}
|
|
27
|
-
if
|
|
28
|
-
for p in sorted(
|
|
25
|
+
if dirpath.exists() and dirpath.is_dir():
|
|
26
|
+
for p in sorted(dirpath.glob("*.md")):
|
|
29
27
|
if p.is_file():
|
|
30
28
|
topics[_norm(p.stem)] = p
|
|
31
29
|
return topics
|
|
32
30
|
|
|
33
31
|
|
|
34
|
-
|
|
35
|
-
"""Discover docs packaged under 'docs/' in the installed distribution.
|
|
32
|
+
# ---------- where could docs live after install? ----------
|
|
36
33
|
|
|
37
|
-
Works in external projects without a local docs/ by inspecting the wheel
|
|
38
|
-
metadata and, as a fallback, searching for a top-level docs/ next to the
|
|
39
|
-
installed package directory in site-packages.
|
|
40
|
-
"""
|
|
41
|
-
topics: Dict[str, Path] = {}
|
|
42
34
|
|
|
43
|
-
|
|
35
|
+
def _candidate_docs_dirs(ctx: click.Context) -> List[Path]:
|
|
36
|
+
"""
|
|
37
|
+
Return likely directories that contain the shipped docs (*.md), ordered by priority.
|
|
38
|
+
Covers:
|
|
39
|
+
1) explicit --docs-dir / env var
|
|
40
|
+
2) in-repo (editable install): <repo>/docs relative to src/svc_infra
|
|
41
|
+
3) wheel installs: <site-packages>/docs
|
|
42
|
+
4) wheel .data area: <site-packages>/{name}-{ver}.data/**/docs
|
|
43
|
+
"""
|
|
44
|
+
out: List[Path] = []
|
|
45
|
+
|
|
46
|
+
# 1) explicit override (--docs-dir or env)
|
|
47
|
+
# Walk up parent contexts to see Typer's group option.
|
|
48
|
+
cur: click.Context | None = ctx
|
|
49
|
+
while cur is not None:
|
|
50
|
+
docs_dir_opt = (cur.params or {}).get("docs_dir")
|
|
51
|
+
if docs_dir_opt:
|
|
52
|
+
p = docs_dir_opt if isinstance(docs_dir_opt, Path) else Path(docs_dir_opt)
|
|
53
|
+
p = p.expanduser()
|
|
54
|
+
if p.exists():
|
|
55
|
+
out.append(p)
|
|
56
|
+
return out # explicit override wins
|
|
57
|
+
cur = cur.parent
|
|
58
|
+
|
|
59
|
+
env_dir = os.getenv("SVC_INFRA_DOCS_DIR")
|
|
60
|
+
if env_dir:
|
|
61
|
+
p = Path(env_dir).expanduser()
|
|
62
|
+
if p.exists():
|
|
63
|
+
out.append(p)
|
|
64
|
+
return out # explicit override wins
|
|
65
|
+
|
|
66
|
+
# locate installed package dir: .../site-packages/svc_infra
|
|
67
|
+
pkg_dir: Path | None = None
|
|
68
|
+
spec = importlib.util.find_spec("svc_infra")
|
|
69
|
+
if spec and spec.submodule_search_locations:
|
|
70
|
+
pkg_dir = Path(next(iter(spec.submodule_search_locations)))
|
|
71
|
+
|
|
72
|
+
# 2) in-repo editable install: src/svc_infra -> ../../docs
|
|
73
|
+
if pkg_dir:
|
|
74
|
+
repo_root_docs = pkg_dir.parent.parent / "docs"
|
|
75
|
+
if repo_root_docs.exists():
|
|
76
|
+
out.append(repo_root_docs)
|
|
77
|
+
|
|
78
|
+
# 3) wheel installs often end up with a top-level site-packages/docs
|
|
79
|
+
top_level_docs = pkg_dir.parent / "docs"
|
|
80
|
+
if top_level_docs.exists():
|
|
81
|
+
out.append(top_level_docs)
|
|
82
|
+
|
|
83
|
+
# 4) wheel .data layout: <site-packages>/{dist-name}-{version}.data/**/docs
|
|
84
|
+
# This catches Poetry's include=docs/**/* paths installed by pip.
|
|
85
|
+
# We compute sibling candidates off site-packages base.
|
|
86
|
+
site_pkgs: Path | None = pkg_dir.parent if pkg_dir else None
|
|
44
87
|
dist = None
|
|
45
88
|
for name in ("svc-infra", "svc_infra"):
|
|
46
89
|
try:
|
|
@@ -49,217 +92,133 @@ def _discover_pkg_topics() -> Dict[str, Path]:
|
|
|
49
92
|
except PackageNotFoundError:
|
|
50
93
|
dist = None
|
|
51
94
|
|
|
52
|
-
if dist is not None:
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
except Exception:
|
|
64
|
-
# Best effort; continue to next
|
|
95
|
+
if site_pkgs and dist is not None:
|
|
96
|
+
# normalized dist name (hyphen/underscore forms both happen in practice)
|
|
97
|
+
dist_name = dist.metadata.get("Name", "svc-infra")
|
|
98
|
+
dist_ver = dist.version
|
|
99
|
+
data_candidates = [
|
|
100
|
+
site_pkgs / f"{dist_name}-{dist_ver}.data",
|
|
101
|
+
site_pkgs / f"{dist_name.replace('-', '_')}-{dist_ver}.data",
|
|
102
|
+
site_pkgs / f"{dist_name.replace('_', '-')}-{dist_ver}.data",
|
|
103
|
+
]
|
|
104
|
+
for data_dir in data_candidates:
|
|
105
|
+
if not data_dir.exists():
|
|
65
106
|
continue
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
]
|
|
78
|
-
for candidate in candidates:
|
|
79
|
-
if candidate.exists() and candidate.is_dir():
|
|
80
|
-
for p in sorted(candidate.glob("*.md")):
|
|
81
|
-
if p.is_file():
|
|
82
|
-
topics.setdefault(_norm(p.stem), p)
|
|
83
|
-
# If one candidate had docs, that's sufficient
|
|
84
|
-
if any(k for k in topics):
|
|
85
|
-
break
|
|
86
|
-
except Exception:
|
|
87
|
-
# Optional fallback only
|
|
88
|
-
pass
|
|
89
|
-
|
|
90
|
-
# 3) Last-resort: scan sys.path entries that look like site-/dist-packages for a top-level docs/
|
|
91
|
-
# directory containing markdown files. This covers non-standard installs/editable modes.
|
|
92
|
-
try:
|
|
93
|
-
if not topics:
|
|
94
|
-
for entry in sys.path:
|
|
95
|
-
try:
|
|
96
|
-
if not entry or ("site-packages" not in entry and "dist-packages" not in entry):
|
|
97
|
-
continue
|
|
98
|
-
docs_dir = Path(entry) / "docs"
|
|
99
|
-
if docs_dir.exists() and docs_dir.is_dir():
|
|
100
|
-
found = _discover_fs_topics(docs_dir)
|
|
101
|
-
if found:
|
|
102
|
-
# Merge but do not override anything already found
|
|
103
|
-
for k, v in found.items():
|
|
104
|
-
topics.setdefault(k, v)
|
|
105
|
-
# If we found one valid docs dir, it's enough
|
|
106
|
-
break
|
|
107
|
-
except Exception:
|
|
108
|
-
continue
|
|
109
|
-
except Exception:
|
|
110
|
-
pass
|
|
111
|
-
|
|
112
|
-
# 4) Parse dist-info/RECORD or egg-info/SOURCES.txt to enumerate docs if available
|
|
113
|
-
try:
|
|
114
|
-
if not topics:
|
|
115
|
-
spec = importlib.util.find_spec("svc_infra")
|
|
116
|
-
base_dir: Path | None = None
|
|
117
|
-
if spec and spec.submodule_search_locations:
|
|
118
|
-
base_dir = Path(next(iter(spec.submodule_search_locations))).parent
|
|
119
|
-
# Fallback to first site-packages on sys.path
|
|
120
|
-
if base_dir is None:
|
|
121
|
-
for entry in sys.path:
|
|
122
|
-
if entry and "site-packages" in entry:
|
|
123
|
-
base_dir = Path(entry)
|
|
124
|
-
break
|
|
125
|
-
if base_dir and base_dir.exists():
|
|
126
|
-
# Check for both hyphen and underscore dist-info names
|
|
127
|
-
candidates = list(base_dir.glob("svc_infra-*.dist-info")) + list(
|
|
128
|
-
base_dir.glob("svc-infra-*.dist-info")
|
|
129
|
-
)
|
|
130
|
-
for di in candidates:
|
|
131
|
-
record = di / "RECORD"
|
|
132
|
-
if record.exists():
|
|
133
|
-
try:
|
|
134
|
-
for line in record.read_text(
|
|
135
|
-
encoding="utf-8", errors="ignore"
|
|
136
|
-
).splitlines():
|
|
137
|
-
rel = line.split(",", 1)[0]
|
|
138
|
-
if rel.startswith("docs/") and rel.endswith(".md"):
|
|
139
|
-
abs_p = base_dir / rel
|
|
140
|
-
if abs_p.exists() and abs_p.is_file():
|
|
141
|
-
topics.setdefault(_norm(Path(rel).stem), abs_p)
|
|
142
|
-
except Exception:
|
|
143
|
-
continue
|
|
144
|
-
# egg-info fallback
|
|
145
|
-
if not topics:
|
|
146
|
-
egg_candidates = list(base_dir.glob("svc_infra-*.egg-info")) + list(
|
|
147
|
-
base_dir.glob("svc-infra-*.egg-info")
|
|
148
|
-
)
|
|
149
|
-
for ei in egg_candidates:
|
|
150
|
-
sources = ei / "SOURCES.txt"
|
|
151
|
-
if sources.exists():
|
|
152
|
-
try:
|
|
153
|
-
for rel in sources.read_text(
|
|
154
|
-
encoding="utf-8", errors="ignore"
|
|
155
|
-
).splitlines():
|
|
156
|
-
rel = rel.strip()
|
|
157
|
-
if rel.startswith("docs/") and rel.endswith(".md"):
|
|
158
|
-
abs_p = base_dir / rel
|
|
159
|
-
if abs_p.exists() and abs_p.is_file():
|
|
160
|
-
topics.setdefault(_norm(Path(rel).stem), abs_p)
|
|
161
|
-
except Exception:
|
|
162
|
-
continue
|
|
163
|
-
except Exception:
|
|
164
|
-
pass
|
|
165
|
-
|
|
166
|
-
# 5) Deep fallback: recursively search site-packages/dist-packages for any 'docs' folder
|
|
167
|
-
# containing markdown files (limited depth to keep overhead reasonable).
|
|
168
|
-
try:
|
|
169
|
-
if not topics:
|
|
170
|
-
for entry in sys.path:
|
|
171
|
-
if not entry or ("site-packages" not in entry and "dist-packages" not in entry):
|
|
172
|
-
continue
|
|
173
|
-
base = Path(entry)
|
|
174
|
-
if not base.exists() or not base.is_dir():
|
|
107
|
+
# common wheel data subfolders
|
|
108
|
+
for sub in ("purelib", "platlib", "data"):
|
|
109
|
+
d = data_dir / sub / "docs"
|
|
110
|
+
if d.exists():
|
|
111
|
+
out.append(d)
|
|
112
|
+
# fallback: search shallowly for any docs/ folder inside .data
|
|
113
|
+
for root, dirs, _files in os.walk(data_dir):
|
|
114
|
+
root_path = Path(root)
|
|
115
|
+
# limit depth (cheap)
|
|
116
|
+
if len(root_path.parts) - len(data_dir.parts) > 3:
|
|
117
|
+
dirs[:] = []
|
|
175
118
|
continue
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
119
|
+
if root_path.name == "docs":
|
|
120
|
+
out.append(root_path)
|
|
121
|
+
|
|
122
|
+
# 5) extremely defensive: scan sys.path entries that look like site-/dist-packages for top-level docs/
|
|
123
|
+
for entry in sys.path:
|
|
124
|
+
if not entry or ("site-packages" not in entry and "dist-packages" not in entry):
|
|
125
|
+
continue
|
|
126
|
+
p = Path(entry) / "docs"
|
|
127
|
+
if p.exists():
|
|
128
|
+
out.append(p)
|
|
129
|
+
|
|
130
|
+
# de-dup while preserving order
|
|
131
|
+
seen = set()
|
|
132
|
+
uniq: List[Path] = []
|
|
133
|
+
for p in out:
|
|
134
|
+
if p.exists():
|
|
135
|
+
key = str(p.resolve())
|
|
136
|
+
if key not in seen:
|
|
137
|
+
seen.add(key)
|
|
138
|
+
uniq.append(p)
|
|
139
|
+
return uniq
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _discover_topics(ctx: click.Context) -> Dict[str, Path]:
|
|
143
|
+
topics: Dict[str, Path] = {}
|
|
144
|
+
for d in _candidate_docs_dirs(ctx):
|
|
145
|
+
found = _md_topics_in(d)
|
|
146
|
+
# do not override earlier (higher-priority) sources
|
|
147
|
+
for k, v in found.items():
|
|
148
|
+
topics.setdefault(k, v)
|
|
149
|
+
if topics:
|
|
150
|
+
# one dir with content is enough for most setups
|
|
151
|
+
# (comment out this break if you *want* deep merging)
|
|
152
|
+
break
|
|
192
153
|
return topics
|
|
193
154
|
|
|
194
155
|
|
|
195
|
-
|
|
196
|
-
# Deprecated: we no longer read docs from arbitrary paths or env.
|
|
197
|
-
# All docs are sourced from the packaged svc-infra distribution only.
|
|
198
|
-
return None
|
|
156
|
+
# ---------- Typer group ----------
|
|
199
157
|
|
|
200
158
|
|
|
201
159
|
class DocsGroup(TyperGroup):
|
|
202
160
|
def list_commands(self, ctx: click.Context) -> List[str]:
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
names.extend([k for k in pkg.keys()])
|
|
206
|
-
# Deduplicate and sort
|
|
207
|
-
return sorted({*names})
|
|
161
|
+
topics = _discover_topics(ctx)
|
|
162
|
+
return sorted(topics.keys())
|
|
208
163
|
|
|
209
164
|
def get_command(self, ctx: click.Context, name: str) -> click.Command | None:
|
|
210
|
-
# Built-ins first (e.g., list, show)
|
|
211
165
|
cmd = super().get_command(ctx, name)
|
|
212
166
|
if cmd is not None:
|
|
213
167
|
return cmd
|
|
214
168
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if
|
|
218
|
-
file_path =
|
|
169
|
+
key = _norm(name)
|
|
170
|
+
topics = _discover_topics(ctx)
|
|
171
|
+
if key in topics:
|
|
172
|
+
file_path = topics[key]
|
|
219
173
|
|
|
220
174
|
@click.command(name=name)
|
|
221
|
-
def
|
|
175
|
+
def _show() -> None:
|
|
222
176
|
click.echo(file_path.read_text(encoding="utf-8", errors="replace"))
|
|
223
177
|
|
|
224
|
-
return
|
|
178
|
+
return _show
|
|
225
179
|
|
|
226
180
|
return None
|
|
227
181
|
|
|
228
182
|
|
|
229
183
|
def register(app: typer.Typer) -> None:
|
|
230
|
-
"""Register the `docs` command group with dynamic topic subcommands."""
|
|
231
|
-
|
|
232
184
|
docs_app = typer.Typer(no_args_is_help=True, add_completion=False, cls=DocsGroup)
|
|
233
185
|
|
|
234
186
|
@docs_app.callback(invoke_without_command=True)
|
|
235
187
|
def _docs_options(
|
|
188
|
+
docs_dir: Path | None = typer.Option(
|
|
189
|
+
None,
|
|
190
|
+
"--docs-dir",
|
|
191
|
+
help="Path to a docs directory to read from (overrides packaged docs)",
|
|
192
|
+
),
|
|
236
193
|
topic: str | None = typer.Option(None, "--topic", help="Topic to show directly"),
|
|
237
194
|
) -> None:
|
|
238
|
-
"""Support --topic at group level (packaged docs only)."""
|
|
239
195
|
if topic:
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
196
|
+
key = _norm(topic)
|
|
197
|
+
topics = _discover_topics(click.get_current_context())
|
|
198
|
+
if key in topics:
|
|
199
|
+
typer.echo(topics[key].read_text(encoding="utf-8", errors="replace"))
|
|
243
200
|
raise typer.Exit(code=0)
|
|
244
201
|
raise typer.BadParameter(f"Unknown topic: {topic}")
|
|
245
202
|
|
|
246
203
|
@docs_app.command("list", help="List available documentation topics")
|
|
247
204
|
def list_topics() -> None:
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
def _print(name: str, path: Path) -> None:
|
|
252
|
-
typer.echo(f"{name}\t{path}")
|
|
205
|
+
ctx = click.get_current_context()
|
|
206
|
+
root = resolve_project_root()
|
|
207
|
+
topics = _discover_topics(ctx)
|
|
253
208
|
|
|
254
|
-
for name, path in
|
|
255
|
-
|
|
209
|
+
for name, path in topics.items():
|
|
210
|
+
try:
|
|
211
|
+
rel = path.relative_to(root)
|
|
212
|
+
typer.echo(f"{name}\t{rel}")
|
|
213
|
+
except Exception:
|
|
214
|
+
typer.echo(f"{name}\t{path}")
|
|
256
215
|
|
|
257
|
-
# Also support a generic "show" command
|
|
258
216
|
@docs_app.command("show", help="Show docs for a topic (alternative to dynamic subcommand)")
|
|
259
217
|
def show(topic: str) -> None:
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
218
|
+
key = _norm(topic)
|
|
219
|
+
topics = _discover_topics(click.get_current_context())
|
|
220
|
+
if key in topics:
|
|
221
|
+
typer.echo(topics[key].read_text(encoding="utf-8", errors="replace"))
|
|
263
222
|
return
|
|
264
223
|
raise typer.BadParameter(f"Unknown topic: {topic}")
|
|
265
224
|
|
|
@@ -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=uCreHg69Zf6B5gbv9Dm39jCRk6q2KQy_05A-75IP0Fg,9650
|
|
143
143
|
svc_infra/cli/cmds/db/sql/sql_export_cmds.py,sha256=YpkguUJFeFApMphVkhOJllTi25ejlsQaJarMe6vJD54,2685
|
|
144
144
|
svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py,sha256=MKc_T_tY1Y_wQl7XTlq8GhYWMMI1q1_vcFZVPOEcNUg,4601
|
|
145
|
-
svc_infra/cli/cmds/docs/docs_cmds.py,sha256=
|
|
145
|
+
svc_infra/cli/cmds/docs/docs_cmds.py,sha256=zauAUG04yI3qbGXSyb5p_UhvvHvXSvirx1fuaA6HMOI,7816
|
|
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.622.dist-info/METADATA,sha256=GrIXBicqMBcCZRf97UzGyQwQJmCl_9eRKXJzfTfmvdE,8106
|
|
300
|
+
svc_infra-0.1.622.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
|
|
301
|
+
svc_infra-0.1.622.dist-info/entry_points.txt,sha256=6x_nZOsjvn6hRZsMgZLgTasaCSKCgAjsGhACe_CiP0U,48
|
|
302
|
+
svc_infra-0.1.622.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|