svc-infra 0.1.620__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.

@@ -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
@@ -12,26 +13,77 @@ from typer.core import TyperGroup
12
13
 
13
14
  from svc_infra.app.root import resolve_project_root
14
15
 
16
+ # ---------- small helpers ----------
15
17
 
16
- def _discover_fs_topics(docs_dir: Path) -> Dict[str, Path]:
18
+
19
+ def _norm(name: str) -> str:
20
+ return name.strip().lower().replace(" ", "-").replace("_", "-")
21
+
22
+
23
+ def _md_topics_in(dirpath: Path) -> Dict[str, Path]:
17
24
  topics: Dict[str, Path] = {}
18
- if docs_dir.exists() and docs_dir.is_dir():
19
- for p in sorted(docs_dir.glob("*.md")):
25
+ if dirpath.exists() and dirpath.is_dir():
26
+ for p in sorted(dirpath.glob("*.md")):
20
27
  if p.is_file():
21
- topics[p.stem.replace(" ", "-")] = p
28
+ topics[_norm(p.stem)] = p
22
29
  return topics
23
30
 
24
31
 
25
- def _discover_pkg_topics() -> Dict[str, Path]:
26
- """Discover docs packaged under 'docs/' in the installed distribution.
32
+ # ---------- where could docs live after install? ----------
27
33
 
28
- Works in external projects without a local docs/ by inspecting the wheel
29
- metadata and, as a fallback, searching for a top-level docs/ next to the
30
- installed package directory in site-packages.
34
+
35
+ def _candidate_docs_dirs(ctx: click.Context) -> List[Path]:
31
36
  """
32
- topics: Dict[str, Path] = {}
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
33
58
 
34
- # 1) Prefer distribution metadata (RECORD) for both hyphen/underscore names
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
35
87
  dist = None
36
88
  for name in ("svc-infra", "svc_infra"):
37
89
  try:
@@ -40,113 +92,95 @@ def _discover_pkg_topics() -> Dict[str, Path]:
40
92
  except PackageNotFoundError:
41
93
  dist = None
42
94
 
43
- if dist is not None:
44
- files = getattr(dist, "files", None) or []
45
- for f in files:
46
- s = str(f)
47
- if not s.startswith("docs/") or not s.endswith(".md"):
48
- continue
49
- topic_name = Path(s).stem.replace(" ", "-")
50
- try:
51
- abs_path = Path(dist.locate_file(f))
52
- if abs_path.exists() and abs_path.is_file():
53
- topics[topic_name] = abs_path
54
- except Exception:
55
- # 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():
56
106
  continue
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[:] = []
118
+ continue
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)
57
129
 
58
- # 2) Fallback: site-packages sibling 'docs/' directory
59
- try:
60
- spec = importlib.util.find_spec("svc_infra")
61
- if spec and spec.submodule_search_locations:
62
- 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)
68
- except Exception:
69
- # Optional fallback only
70
- pass
71
-
72
- return topics
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
73
140
 
74
141
 
75
- 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
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
153
+ return topics
87
154
 
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
155
 
95
- # Project docs
96
- root = resolve_project_root()
97
- proj_docs = root / "docs"
98
- if proj_docs.exists():
99
- return proj_docs
100
- return None
156
+ # ---------- Typer group ----------
101
157
 
102
158
 
103
159
  class DocsGroup(TyperGroup):
104
160
  def list_commands(self, ctx: click.Context) -> List[str]:
105
- 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
- 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])
112
- # Deduplicate and sort
113
- return sorted({*names})
161
+ topics = _discover_topics(ctx)
162
+ return sorted(topics.keys())
114
163
 
115
164
  def get_command(self, ctx: click.Context, name: str) -> click.Command | None:
116
- # Built-ins first (e.g., list, show)
117
165
  cmd = super().get_command(ctx, name)
118
166
  if cmd is not None:
119
167
  return cmd
120
168
 
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
134
- pkg = _discover_pkg_topics()
135
- if name in pkg:
136
- file_path = pkg[name]
169
+ key = _norm(name)
170
+ topics = _discover_topics(ctx)
171
+ if key in topics:
172
+ file_path = topics[key]
137
173
 
138
174
  @click.command(name=name)
139
- def _show_pkg() -> None:
175
+ def _show() -> None:
140
176
  click.echo(file_path.read_text(encoding="utf-8", errors="replace"))
141
177
 
142
- return _show_pkg
178
+ return _show
143
179
 
144
180
  return None
145
181
 
146
182
 
147
183
  def register(app: typer.Typer) -> None:
148
- """Register the `docs` command group with dynamic topic subcommands."""
149
-
150
184
  docs_app = typer.Typer(no_args_is_help=True, add_completion=False, cls=DocsGroup)
151
185
 
152
186
  @docs_app.callback(invoke_without_command=True)
@@ -154,21 +188,15 @@ def register(app: typer.Typer) -> None:
154
188
  docs_dir: Path | None = typer.Option(
155
189
  None,
156
190
  "--docs-dir",
157
- help="Path to a docs directory to read from (overrides env/project root)",
191
+ help="Path to a docs directory to read from (overrides packaged docs)",
158
192
  ),
159
193
  topic: str | None = typer.Option(None, "--topic", help="Topic to show directly"),
160
194
  ) -> None:
161
- """Support --docs-dir and --topic at group level."""
162
195
  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
- pkg = _discover_pkg_topics()
170
- if topic in pkg:
171
- 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"))
172
200
  raise typer.Exit(code=0)
173
201
  raise typer.BadParameter(f"Unknown topic: {topic}")
174
202
 
@@ -176,37 +204,21 @@ def register(app: typer.Typer) -> None:
176
204
  def list_topics() -> None:
177
205
  ctx = click.get_current_context()
178
206
  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
- pkg = _discover_pkg_topics()
207
+ topics = _discover_topics(ctx)
182
208
 
183
- # Print FS topics first (project/env/option), then packaged topics not shadowed by FS
184
- def _print(name: str, path: Path) -> None:
209
+ for name, path in topics.items():
185
210
  try:
186
211
  rel = path.relative_to(root)
187
212
  typer.echo(f"{name}\t{rel}")
188
213
  except Exception:
189
- # For packaged topics, path will be site-packages absolute path
190
214
  typer.echo(f"{name}\t{path}")
191
215
 
192
- for name, path in fs.items():
193
- _print(name, path)
194
- for name, path in pkg.items():
195
- if name not in fs:
196
- _print(name, path)
197
-
198
- # Also support a generic "show" command
199
216
  @docs_app.command("show", help="Show docs for a topic (alternative to dynamic subcommand)")
200
217
  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
- pkg = _discover_pkg_topics()
208
- if topic in pkg:
209
- 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"))
210
222
  return
211
223
  raise typer.BadParameter(f"Unknown topic: {topic}")
212
224
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: svc-infra
3
- Version: 0.1.620
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=vRDhddel_X9MCfCVRvV7_Ns7sB3awfwSVWV0HqKLRKg,7653
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.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.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,,