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.

@@ -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
- def _norm(name: str) -> str:
16
- """Normalize a topic name for stable CLI commands.
16
+ # ---------- small helpers ----------
17
17
 
18
- - Lowercase
19
- - Replace spaces and underscores with hyphens
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 _discover_fs_topics(docs_dir: Path) -> Dict[str, Path]:
23
+ def _md_topics_in(dirpath: Path) -> Dict[str, Path]:
26
24
  topics: Dict[str, Path] = {}
27
- if docs_dir.exists() and docs_dir.is_dir():
28
- for p in sorted(docs_dir.glob("*.md")):
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
- def _discover_pkg_topics() -> Dict[str, Path]:
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
- # 1) Prefer distribution metadata (RECORD) for both hyphen/underscore names
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
- files = getattr(dist, "files", None) or []
54
- for f in files:
55
- s = str(f)
56
- if not s.startswith("docs/") or not s.endswith(".md"):
57
- continue
58
- topic_name = _norm(Path(s).stem)
59
- try:
60
- abs_path = Path(dist.locate_file(f))
61
- if abs_path.exists() and abs_path.is_file():
62
- topics[topic_name] = abs_path
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
- # 2) Fallback: site-packages sibling 'docs/' directory (and repo-root docs in editable installs)
68
- try:
69
- spec = importlib.util.find_spec("svc_infra")
70
- if spec and spec.submodule_search_locations:
71
- pkg_dir = Path(next(iter(spec.submodule_search_locations)))
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
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
- 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
-
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
- def _resolve_docs_dir(ctx: click.Context) -> Path | None:
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
- names: List[str] = list(super().list_commands(ctx) or [])
204
- pkg = _discover_pkg_topics()
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
- # Packaged topics only
216
- pkg = _discover_pkg_topics()
217
- if name in pkg:
218
- file_path = pkg[name]
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 _show_pkg() -> None:
175
+ def _show() -> None:
222
176
  click.echo(file_path.read_text(encoding="utf-8", errors="replace"))
223
177
 
224
- return _show_pkg
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
- pkg = _discover_pkg_topics()
241
- if topic in pkg:
242
- typer.echo(pkg[topic].read_text(encoding="utf-8", errors="replace"))
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
- pkg = _discover_pkg_topics()
249
-
250
- # Print packaged topics only
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 pkg.items():
255
- _print(name, path)
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
- pkg = _discover_pkg_topics()
261
- if topic in pkg:
262
- typer.echo(pkg[topic].read_text(encoding="utf-8", errors="replace"))
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: svc-infra
3
- Version: 0.1.621
3
+ Version: 0.1.622
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=fbdnTEfzx3HNNXK3zL5fa_Lk3FowzHBcfS_VHQah7VQ,10720
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.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,,
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,,