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

@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import importlib.util
4
4
  import os
5
+ import sys
5
6
  from importlib.metadata import PackageNotFoundError, distribution
6
7
  from pathlib import Path
7
8
  from typing import Dict, List
@@ -10,7 +11,15 @@ import click
10
11
  import typer
11
12
  from typer.core import TyperGroup
12
13
 
13
- from svc_infra.app.root import resolve_project_root
14
+
15
+ def _norm(name: str) -> str:
16
+ """Normalize a topic name for stable CLI commands.
17
+
18
+ - Lowercase
19
+ - Replace spaces and underscores with hyphens
20
+ - Strip leading/trailing whitespace
21
+ """
22
+ return name.strip().lower().replace(" ", "-").replace("_", "-")
14
23
 
15
24
 
16
25
  def _discover_fs_topics(docs_dir: Path) -> Dict[str, Path]:
@@ -18,7 +27,7 @@ def _discover_fs_topics(docs_dir: Path) -> Dict[str, Path]:
18
27
  if docs_dir.exists() and docs_dir.is_dir():
19
28
  for p in sorted(docs_dir.glob("*.md")):
20
29
  if p.is_file():
21
- topics[p.stem.replace(" ", "-")] = p
30
+ topics[_norm(p.stem)] = p
22
31
  return topics
23
32
 
24
33
 
@@ -46,7 +55,7 @@ def _discover_pkg_topics() -> Dict[str, Path]:
46
55
  s = str(f)
47
56
  if not s.startswith("docs/") or not s.endswith(".md"):
48
57
  continue
49
- topic_name = Path(s).stem.replace(" ", "-")
58
+ topic_name = _norm(Path(s).stem)
50
59
  try:
51
60
  abs_path = Path(dist.locate_file(f))
52
61
  if abs_path.exists() and abs_path.is_file():
@@ -55,60 +64,145 @@ def _discover_pkg_topics() -> Dict[str, Path]:
55
64
  # Best effort; continue to next
56
65
  continue
57
66
 
58
- # 2) Fallback: site-packages sibling 'docs/' directory
67
+ # 2) Fallback: site-packages sibling 'docs/' directory (and repo-root docs in editable installs)
59
68
  try:
60
69
  spec = importlib.util.find_spec("svc_infra")
61
70
  if spec and spec.submodule_search_locations:
62
71
  pkg_dir = Path(next(iter(spec.submodule_search_locations)))
63
- candidate = pkg_dir.parent / "docs"
64
- if candidate.exists() and candidate.is_dir():
65
- for p in sorted(candidate.glob("*.md")):
66
- if p.is_file():
67
- topics.setdefault(p.stem.replace(" ", "-"), p)
72
+ candidates = [
73
+ pkg_dir.parent / "docs", # site-packages/docs OR src/docs
74
+ pkg_dir / "docs", # site-packages/svc_infra/docs OR src/svc_infra/docs
75
+ pkg_dir.parent.parent
76
+ / "docs", # repo-root/docs when running editable from repo (src/svc_infra → ../../docs)
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
68
86
  except Exception:
69
87
  # Optional fallback only
70
88
  pass
71
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():
175
+ continue
176
+ base_parts = len(base.parts)
177
+ for root, dirs, files in os.walk(base):
178
+ root_path = Path(root)
179
+ # Limit search depth to avoid expensive scans
180
+ if len(root_path.parts) - base_parts > 4:
181
+ # prune
182
+ dirs[:] = []
183
+ continue
184
+ if root_path.name == "docs":
185
+ for p in sorted(root_path.glob("*.md")):
186
+ if p.is_file():
187
+ topics.setdefault(_norm(p.stem), p)
188
+ # do not break; there might be multiple doc dirs
189
+ except Exception:
190
+ pass
191
+
72
192
  return topics
73
193
 
74
194
 
75
195
  def _resolve_docs_dir(ctx: click.Context) -> Path | None:
76
- # CLI option takes precedence; walk up parent contexts because Typer
77
- # executes subcommands in child contexts that do not inherit params.
78
- current: click.Context | None = ctx
79
- while current is not None:
80
- docs_dir_opt = (current.params or {}).get("docs_dir")
81
- if docs_dir_opt:
82
- path = docs_dir_opt if isinstance(docs_dir_opt, Path) else Path(docs_dir_opt)
83
- path = path.expanduser()
84
- if path.exists():
85
- return path
86
- current = current.parent
87
-
88
- # Env var next
89
- env_dir = os.getenv("SVC_INFRA_DOCS_DIR")
90
- if env_dir:
91
- p = Path(env_dir).expanduser()
92
- if p.exists():
93
- return p
94
-
95
- # Project docs
96
- root = resolve_project_root()
97
- proj_docs = root / "docs"
98
- if proj_docs.exists():
99
- return proj_docs
196
+ # Deprecated: we no longer read docs from arbitrary paths or env.
197
+ # All docs are sourced from the packaged svc-infra distribution only.
100
198
  return None
101
199
 
102
200
 
103
201
  class DocsGroup(TyperGroup):
104
202
  def list_commands(self, ctx: click.Context) -> List[str]:
105
203
  names: List[str] = list(super().list_commands(ctx) or [])
106
- dir_to_use = _resolve_docs_dir(ctx)
107
- fs = _discover_fs_topics(dir_to_use) if dir_to_use else {}
108
204
  pkg = _discover_pkg_topics()
109
- # FS topics win on conflicts; add both for visibility
110
- names.extend([k for k in fs.keys()])
111
- names.extend([k for k in pkg.keys() if k not in fs])
205
+ names.extend([k for k in pkg.keys()])
112
206
  # Deduplicate and sort
113
207
  return sorted({*names})
114
208
 
@@ -118,19 +212,7 @@ class DocsGroup(TyperGroup):
118
212
  if cmd is not None:
119
213
  return cmd
120
214
 
121
- # Dynamic topic resolution from FS
122
- dir_to_use = _resolve_docs_dir(ctx)
123
- fs = _discover_fs_topics(dir_to_use) if dir_to_use else {}
124
- if name in fs:
125
- file_path = fs[name]
126
-
127
- @click.command(name=name)
128
- def _show_fs() -> None:
129
- click.echo(file_path.read_text(encoding="utf-8", errors="replace"))
130
-
131
- return _show_fs
132
-
133
- # Packaged fallback
215
+ # Packaged topics only
134
216
  pkg = _discover_pkg_topics()
135
217
  if name in pkg:
136
218
  file_path = pkg[name]
@@ -151,21 +233,10 @@ def register(app: typer.Typer) -> None:
151
233
 
152
234
  @docs_app.callback(invoke_without_command=True)
153
235
  def _docs_options(
154
- docs_dir: Path | None = typer.Option(
155
- None,
156
- "--docs-dir",
157
- help="Path to a docs directory to read from (overrides env/project root)",
158
- ),
159
236
  topic: str | None = typer.Option(None, "--topic", help="Topic to show directly"),
160
237
  ) -> None:
161
- """Support --docs-dir and --topic at group level."""
238
+ """Support --topic at group level (packaged docs only)."""
162
239
  if topic:
163
- ctx = click.get_current_context()
164
- dir_to_use = _resolve_docs_dir(ctx)
165
- fs = _discover_fs_topics(dir_to_use) if dir_to_use else {}
166
- if topic in fs:
167
- typer.echo(fs[topic].read_text(encoding="utf-8", errors="replace"))
168
- raise typer.Exit(code=0)
169
240
  pkg = _discover_pkg_topics()
170
241
  if topic in pkg:
171
242
  typer.echo(pkg[topic].read_text(encoding="utf-8", errors="replace"))
@@ -174,36 +245,18 @@ def register(app: typer.Typer) -> None:
174
245
 
175
246
  @docs_app.command("list", help="List available documentation topics")
176
247
  def list_topics() -> None:
177
- ctx = click.get_current_context()
178
- root = resolve_project_root()
179
- dir_to_use = _resolve_docs_dir(ctx)
180
- fs = _discover_fs_topics(dir_to_use) if dir_to_use else {}
181
248
  pkg = _discover_pkg_topics()
182
249
 
183
- # Print FS topics first (project/env/option), then packaged topics not shadowed by FS
250
+ # Print packaged topics only
184
251
  def _print(name: str, path: Path) -> None:
185
- try:
186
- rel = path.relative_to(root)
187
- typer.echo(f"{name}\t{rel}")
188
- except Exception:
189
- # For packaged topics, path will be site-packages absolute path
190
- typer.echo(f"{name}\t{path}")
252
+ typer.echo(f"{name}\t{path}")
191
253
 
192
- for name, path in fs.items():
193
- _print(name, path)
194
254
  for name, path in pkg.items():
195
- if name not in fs:
196
- _print(name, path)
255
+ _print(name, path)
197
256
 
198
257
  # Also support a generic "show" command
199
258
  @docs_app.command("show", help="Show docs for a topic (alternative to dynamic subcommand)")
200
259
  def show(topic: str) -> None:
201
- ctx = click.get_current_context()
202
- dir_to_use = _resolve_docs_dir(ctx)
203
- fs = _discover_fs_topics(dir_to_use) if dir_to_use else {}
204
- if topic in fs:
205
- typer.echo(fs[topic].read_text(encoding="utf-8", errors="replace"))
206
- return
207
260
  pkg = _discover_pkg_topics()
208
261
  if topic in pkg:
209
262
  typer.echo(pkg[topic].read_text(encoding="utf-8", errors="replace"))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: svc-infra
3
- Version: 0.1.620
3
+ Version: 0.1.621
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
@@ -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=vRDhddel_X9MCfCVRvV7_Ns7sB3awfwSVWV0HqKLRKg,7653
145
+ svc_infra/cli/cmds/docs/docs_cmds.py,sha256=fbdnTEfzx3HNNXK3zL5fa_Lk3FowzHBcfS_VHQah7VQ,10720
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.620.dist-info/METADATA,sha256=Ii8wzmysVqUARGfBUES-2TQIMzhyeAGvGimkSTMIpYg,8106
300
- svc_infra-0.1.620.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
301
- svc_infra-0.1.620.dist-info/entry_points.txt,sha256=6x_nZOsjvn6hRZsMgZLgTasaCSKCgAjsGhACe_CiP0U,48
302
- svc_infra-0.1.620.dist-info/RECORD,,
299
+ svc_infra-0.1.621.dist-info/METADATA,sha256=QD-ETuukvgwtsMr9N5ylyIOBUgPW6yms4kDLJbFN1H4,8106
300
+ svc_infra-0.1.621.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
301
+ svc_infra-0.1.621.dist-info/entry_points.txt,sha256=6x_nZOsjvn6hRZsMgZLgTasaCSKCgAjsGhACe_CiP0U,48
302
+ svc_infra-0.1.621.dist-info/RECORD,,