simple-module-cli 0.0.30__tar.gz → 0.0.32__tar.gz
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.
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/.gitignore +1 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/PKG-INFO +2 -2
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/pyproject.toml +1 -1
- simple_module_cli-0.0.32/simple_module_cli/add_cmd.py +267 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/cli.py +18 -0
- simple_module_cli-0.0.32/simple_module_cli/git_source.py +282 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/new.py +46 -6
- simple_module_cli-0.0.32/simple_module_cli/pyproject_edit.py +145 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/skills/simple-module-cli/SKILL.md +1 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/skills/simple-module-conventions/SKILL.md +15 -2
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/skills/simple-module-creating/SKILL.md +1 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/skills/simple-module-inertia-pages/SKILL.md +2 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/skills/simple-module-registries/SKILL.md +1 -1
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/host/README.md.tpl +2 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/host/alembic.ini +5 -1
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/host/client_app/app.tsx +6 -0
- simple_module_cli-0.0.32/simple_module_cli/templates/host/client_app/module-assets.ts +127 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/host/client_app/vite.config.ts +15 -90
- simple_module_cli-0.0.32/simple_module_cli/templates/host/client_app/vite.dev-url.ts +92 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/host/migrations/env.py +16 -2
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/workspace/Makefile +8 -2
- simple_module_cli-0.0.32/simple_module_cli/update_cmd.py +139 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/wizard.py +13 -2
- simple_module_cli-0.0.32/tests/conftest.py +119 -0
- simple_module_cli-0.0.32/tests/test_cli_add_cmd.py +168 -0
- simple_module_cli-0.0.32/tests/test_cli_add_spec_parse.py +91 -0
- simple_module_cli-0.0.32/tests/test_cli_git_scan.py +70 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/tests/test_cli_new.py +1 -1
- simple_module_cli-0.0.32/tests/test_cli_new_git_module.py +53 -0
- simple_module_cli-0.0.32/tests/test_cli_new_migration_bootstrap.py +85 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/tests/test_cli_new_regressions.py +0 -48
- simple_module_cli-0.0.32/tests/test_cli_pyproject_edit.py +94 -0
- simple_module_cli-0.0.32/tests/test_cli_update_sources.py +127 -0
- simple_module_cli-0.0.32/tests/test_cli_vite_port_env.py +48 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/tests/test_cli_wizard.py +7 -7
- simple_module_cli-0.0.32/tests/test_cli_wizard_git.py +39 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/tests/test_module_css_assets.py +52 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/tests/test_scaffolding_module.py +22 -1
- simple_module_cli-0.0.30/tests/conftest.py +0 -45
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/LICENSE +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/README.md +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/__init__.py +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/_env.py +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/_module_build.py +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/_module_host.py +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/_templating.py +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/app_project.py +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/case.py +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/catalog.py +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/docker_assets.py +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/module_cmd.py +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/package_update.py +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/pins.py +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/plugins.py +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/recipes.py +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/scaffolding.py +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/skills/README.md +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/skills/simple-module-database/SKILL.md +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/skills/simple-module-doctor/SKILL.md +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/skills/simple-module-locales/SKILL.md +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/skills/simple-module-migrations/SKILL.md +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/skills/simple-module-testing/SKILL.md +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/skills_cmd.py +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/docker/Makefile.snippet +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/docker/compose-base-postgres.yml.tpl +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/docker/compose-base-sqlite.yml.tpl +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/docker/compose-tasks-postgres.yml.tpl +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/docker/compose-tasks-sqlite.yml.tpl +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/docker/dockerignore +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/docker/host-flat.Dockerfile +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/docker/host.Dockerfile +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/host/.env.example +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/host/.gitignore +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/host/Makefile +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/host/_optional/background_tasks/Makefile.snippet +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/host/_optional/background_tasks/run_worker.py +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/host/client_app/main.tsx +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/host/client_app/package.json.tpl +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/host/client_app/pages/Error.tsx +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/host/client_app/pages/Landing.tsx +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/host/client_app/pages.ts +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/host/client_app/styles.css +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/host/client_app/tsconfig.json +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/host/main.py +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/host/migrations/script.py.mako +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/host/migrations/versions/.gitkeep +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/host/pyproject.toml.tpl +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/host/routes.py +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/host/templates/index.html +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/module/.github/workflows/ci.yml +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/module/.github/workflows/publish.yml.tpl +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/module/.gitignore +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/module/README.md.tpl +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/module/__PACKAGE__/__init__.py +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/module/__PACKAGE__/endpoints/__init__.py +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/module/__PACKAGE__/endpoints/api.py.tpl +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/module/__PACKAGE__/endpoints/views.py.tpl +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/module/__PACKAGE__/module.py.tpl +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/module/__PACKAGE__/pages/Index.tsx.tpl +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/module/__PACKAGE__/services.py.tpl +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/module/__PACKAGE__/settings.py.tpl +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/module/_optional/standalone/package.json.tpl +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/module/_optional/standalone/tsconfig.json.tpl +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/module/package.json.tpl +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/module/pyproject.toml.tpl +0 -0
- /simple_module_cli-0.0.30/simple_module_cli/templates/module/tests/test_module.py.tpl → /simple_module_cli-0.0.32/simple_module_cli/templates/module/tests/test___PACKAGE__.py.tpl +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/module/tsconfig.json.tpl +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/workspace/.env.example +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/workspace/.gitignore +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/workspace/README.md.tpl +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/workspace/package.json.tpl +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/simple_module_cli/templates/workspace/pyproject.toml.tpl +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/tests/test_build_packaging.py +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/tests/test_case.py +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/tests/test_cli_catalog.py +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/tests/test_cli_docker_assets.py +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/tests/test_cli_new_dest_tolerance.py +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/tests/test_cli_new_scaffold_layout.py +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/tests/test_cli_package_update.py +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/tests/test_cli_recipes.py +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/tests/test_create_module_ci_skip.py +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/tests/test_env_helper.py +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/tests/test_module_cmd_build.py +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/tests/test_module_cmd_e2e.py +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/tests/test_module_cmd_verify.py +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/tests/test_module_css_build.py +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/tests/test_module_npm_aliases.py +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/tests/test_module_pages_manifest.py +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/tests/test_no_framework_deps.py +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/tests/test_plugin_discovery.py +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/tests/test_scaffold_rollback.py +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/tests/test_scaffolding_host.py +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/tests/test_scaffolding_module_js.py +0 -0
- {simple_module_cli-0.0.30 → simple_module_cli-0.0.32}/tests/test_skills_cmd.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: simple_module_cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.32
|
|
4
4
|
Summary: Standalone scaffolder for the SimpleModule framework — `smpy new`, `smpy create-module`, plugin host.
|
|
5
5
|
Project-URL: Homepage, https://github.com/antosubash/simple_module_python
|
|
6
6
|
Project-URL: Repository, https://github.com/antosubash/simple_module_python
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""``smpy add`` — add a module from PyPI, any git repo, or a local path.
|
|
2
|
+
|
|
3
|
+
Git specs: ``git+<url>[@<tag|branch|sha>][#subdirectory=<dir>]``. The spec
|
|
4
|
+
is resolved first (ls-remote + shallow clone + entry-point scan); the host
|
|
5
|
+
pyproject is written only after resolution succeeds. A git spec without
|
|
6
|
+
``#subdirectory`` is scanned for module packages — one match is added
|
|
7
|
+
directly, several go through ``--module``/``--all`` or an interactive
|
|
8
|
+
picker. Then: ``uv sync`` → ``smpy host gen-pages`` → ``smpy host
|
|
9
|
+
sync-js-deps`` → entry-point verification, unless ``--no-sync``.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import subprocess
|
|
15
|
+
import tempfile
|
|
16
|
+
from collections.abc import Callable
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Annotated
|
|
19
|
+
|
|
20
|
+
import typer
|
|
21
|
+
|
|
22
|
+
from simple_module_cli.git_source import (
|
|
23
|
+
FoundModule,
|
|
24
|
+
SpecError,
|
|
25
|
+
classify_ref,
|
|
26
|
+
derive_range,
|
|
27
|
+
parse_add_spec,
|
|
28
|
+
scan_modules,
|
|
29
|
+
shallow_clone,
|
|
30
|
+
)
|
|
31
|
+
from simple_module_cli.pyproject_edit import (
|
|
32
|
+
has_git_sources,
|
|
33
|
+
load_pyproject,
|
|
34
|
+
save_pyproject,
|
|
35
|
+
write_dependency,
|
|
36
|
+
write_git_source,
|
|
37
|
+
write_path_source,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
__all__ = ["ExecRunner", "add_module", "post_install", "run_add"]
|
|
41
|
+
|
|
42
|
+
ExecRunner = Callable[[list[str], Path], int]
|
|
43
|
+
|
|
44
|
+
_SECURITY_NOTICE = (
|
|
45
|
+
"note: this installs and runs code from a URL you chose — review the "
|
|
46
|
+
"repository before booting the host."
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
_ENTRYPOINT_CHECK = (
|
|
50
|
+
"import sys\n"
|
|
51
|
+
"from importlib.metadata import distributions\n"
|
|
52
|
+
"target = sys.argv[1].replace('-', '_').lower()\n"
|
|
53
|
+
"for dist in distributions():\n"
|
|
54
|
+
" if any(ep.group == 'simple_module' for ep in dist.entry_points):\n"
|
|
55
|
+
" name = (dist.metadata['Name'] or '').replace('-', '_').lower()\n"
|
|
56
|
+
" if name == target:\n"
|
|
57
|
+
" sys.exit(0)\n"
|
|
58
|
+
"sys.exit(1)\n"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _default_exec_runner(cmd: list[str], cwd: Path) -> int:
|
|
63
|
+
return subprocess.run(cmd, cwd=cwd, check=False).returncode
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _fail(message: str) -> typer.Exit:
|
|
67
|
+
typer.echo(f"ERROR: {message}", err=True)
|
|
68
|
+
return typer.Exit(code=1)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _choose(
|
|
72
|
+
found: list[FoundModule],
|
|
73
|
+
*,
|
|
74
|
+
select: list[str] | None,
|
|
75
|
+
all_modules: bool,
|
|
76
|
+
assume_yes: bool,
|
|
77
|
+
) -> list[FoundModule]:
|
|
78
|
+
if len(found) == 1:
|
|
79
|
+
return found
|
|
80
|
+
if all_modules:
|
|
81
|
+
return found
|
|
82
|
+
if select:
|
|
83
|
+
wanted = {s.replace("-", "_").lower() for s in select}
|
|
84
|
+
chosen = [m for m in found if m.dist_name.replace("-", "_").lower() in wanted]
|
|
85
|
+
missing = wanted - {m.dist_name.replace("-", "_").lower() for m in chosen}
|
|
86
|
+
if missing:
|
|
87
|
+
raise _fail(f"module(s) not found in repo: {', '.join(sorted(missing))}")
|
|
88
|
+
return chosen
|
|
89
|
+
if assume_yes:
|
|
90
|
+
names = ", ".join(m.dist_name for m in found)
|
|
91
|
+
raise _fail(f"repo contains multiple modules ({names}); pick with --module a,b or --all")
|
|
92
|
+
return [
|
|
93
|
+
m
|
|
94
|
+
for m in found
|
|
95
|
+
if typer.confirm(f"Include {m.dist_name} ({m.subdirectory or 'repo root'})?")
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def post_install(
|
|
100
|
+
project_dir: Path,
|
|
101
|
+
dists: list[str],
|
|
102
|
+
models_dists: list[str],
|
|
103
|
+
exec_runner: ExecRunner,
|
|
104
|
+
) -> None:
|
|
105
|
+
"""uv sync → gen-pages → sync-js-deps → entry-point check → reminders."""
|
|
106
|
+
if exec_runner(["uv", "sync"], project_dir) != 0:
|
|
107
|
+
raise _fail("`uv sync` failed — pyproject was written; fix and re-run `uv sync`.")
|
|
108
|
+
if (project_dir / "client_app").is_dir():
|
|
109
|
+
exec_runner(["uv", "run", "smpy", "host", "gen-pages"], project_dir)
|
|
110
|
+
exec_runner(["uv", "run", "smpy", "host", "sync-js-deps"], project_dir)
|
|
111
|
+
for dist in dists:
|
|
112
|
+
code = exec_runner(["uv", "run", "python", "-c", _ENTRYPOINT_CHECK, dist], project_dir)
|
|
113
|
+
if code != 0:
|
|
114
|
+
raise _fail(
|
|
115
|
+
f"{dist} installed but exposes no [project.entry-points.simple_module] "
|
|
116
|
+
"entry point — it is not a SimpleModule module."
|
|
117
|
+
)
|
|
118
|
+
for dist in models_dists:
|
|
119
|
+
typer.echo(
|
|
120
|
+
f"{dist} ships database models — generate and apply the migration:\n"
|
|
121
|
+
f' make migration msg="add {dist}"\n make migrate'
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _add_pypi(doc, pyproject: Path, raw: str, no_sync: bool, exec_runner: ExecRunner) -> list[str]:
|
|
126
|
+
name = raw
|
|
127
|
+
for i, ch in enumerate(raw):
|
|
128
|
+
if ch in "<>=!~[; ":
|
|
129
|
+
name = raw[:i]
|
|
130
|
+
break
|
|
131
|
+
constraint = raw[len(name) :]
|
|
132
|
+
write_dependency(doc, name, constraint)
|
|
133
|
+
save_pyproject(pyproject, doc)
|
|
134
|
+
typer.echo(f"Added {name} (PyPI).")
|
|
135
|
+
if not no_sync:
|
|
136
|
+
post_install(pyproject.parent, [name], [], exec_runner)
|
|
137
|
+
return [name]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _add_path(
|
|
141
|
+
doc, pyproject: Path, spec_path: Path, no_sync: bool, exec_runner: ExecRunner
|
|
142
|
+
) -> list[str]:
|
|
143
|
+
root = spec_path if spec_path.is_absolute() else pyproject.parent / spec_path
|
|
144
|
+
found = [m for m in scan_modules(root) if m.subdirectory is None]
|
|
145
|
+
if not found:
|
|
146
|
+
raise _fail(f"{root} has no [project.entry-points.simple_module] entry point at its root")
|
|
147
|
+
mod = found[0]
|
|
148
|
+
write_dependency(doc, mod.dist_name, derive_range(mod.version))
|
|
149
|
+
write_path_source(doc, mod.dist_name, path=str(spec_path))
|
|
150
|
+
save_pyproject(pyproject, doc)
|
|
151
|
+
typer.echo(f"Added {mod.dist_name} (editable path {spec_path}).")
|
|
152
|
+
if not no_sync:
|
|
153
|
+
post_install(
|
|
154
|
+
pyproject.parent,
|
|
155
|
+
[mod.dist_name],
|
|
156
|
+
[mod.dist_name] if mod.ships_models else [],
|
|
157
|
+
exec_runner,
|
|
158
|
+
)
|
|
159
|
+
return [mod.dist_name]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def run_add(
|
|
163
|
+
spec: str,
|
|
164
|
+
*,
|
|
165
|
+
pyproject: Path,
|
|
166
|
+
select: list[str] | None = None,
|
|
167
|
+
all_modules: bool = False,
|
|
168
|
+
no_sync: bool = False,
|
|
169
|
+
assume_yes: bool = False,
|
|
170
|
+
git_runner=None,
|
|
171
|
+
exec_runner: ExecRunner | None = None,
|
|
172
|
+
) -> list[str]:
|
|
173
|
+
if not pyproject.is_file():
|
|
174
|
+
raise _fail(f"{pyproject} not found")
|
|
175
|
+
exec_runner = exec_runner or _default_exec_runner
|
|
176
|
+
try:
|
|
177
|
+
parsed = parse_add_spec(spec)
|
|
178
|
+
except SpecError as exc:
|
|
179
|
+
raise _fail(str(exc)) from exc
|
|
180
|
+
doc = load_pyproject(pyproject)
|
|
181
|
+
|
|
182
|
+
if parsed.kind == "pypi":
|
|
183
|
+
return _add_pypi(doc, pyproject, parsed.raw, no_sync, exec_runner)
|
|
184
|
+
if parsed.kind == "path":
|
|
185
|
+
assert parsed.path is not None
|
|
186
|
+
return _add_path(doc, pyproject, parsed.path, no_sync, exec_runner)
|
|
187
|
+
|
|
188
|
+
assert parsed.git is not None
|
|
189
|
+
git_kwargs = {"run": git_runner} if git_runner else {}
|
|
190
|
+
ref_info = classify_ref(parsed.git.url, parsed.git.ref, **git_kwargs)
|
|
191
|
+
with tempfile.TemporaryDirectory(prefix="smpy-add-") as tmp:
|
|
192
|
+
clone = shallow_clone(parsed.git.url, ref_info, Path(tmp) / "repo", **git_kwargs)
|
|
193
|
+
found = scan_modules(clone)
|
|
194
|
+
if parsed.git.subdirectory is not None:
|
|
195
|
+
found = [m for m in found if m.subdirectory == parsed.git.subdirectory]
|
|
196
|
+
if not found:
|
|
197
|
+
where = (
|
|
198
|
+
f"subdirectory {parsed.git.subdirectory!r} of {parsed.git.url}"
|
|
199
|
+
if parsed.git.subdirectory
|
|
200
|
+
else parsed.git.url
|
|
201
|
+
)
|
|
202
|
+
raise _fail(f"{where} has no package declaring [project.entry-points.simple_module]")
|
|
203
|
+
chosen = _choose(found, select=select, all_modules=all_modules, assume_yes=assume_yes)
|
|
204
|
+
if not chosen:
|
|
205
|
+
raise _fail("no modules selected")
|
|
206
|
+
|
|
207
|
+
if not has_git_sources(doc):
|
|
208
|
+
typer.echo(_SECURITY_NOTICE)
|
|
209
|
+
for mod in chosen:
|
|
210
|
+
write_dependency(doc, mod.dist_name, derive_range(mod.version))
|
|
211
|
+
write_git_source(
|
|
212
|
+
doc,
|
|
213
|
+
mod.dist_name,
|
|
214
|
+
url=parsed.git.url,
|
|
215
|
+
ref_info=ref_info,
|
|
216
|
+
subdirectory=mod.subdirectory,
|
|
217
|
+
)
|
|
218
|
+
pin = f"{ref_info.kind} {ref_info.value}" if ref_info.value else "default branch"
|
|
219
|
+
typer.echo(f"Added {mod.dist_name} {mod.version} from git ({pin}).")
|
|
220
|
+
if ref_info.kind == "branch":
|
|
221
|
+
typer.echo(
|
|
222
|
+
f" {mod.dist_name} tracks a branch — dev-mode pin; prefer a v* tag for releases."
|
|
223
|
+
)
|
|
224
|
+
save_pyproject(pyproject, doc)
|
|
225
|
+
if not no_sync:
|
|
226
|
+
post_install(
|
|
227
|
+
pyproject.parent,
|
|
228
|
+
[m.dist_name for m in chosen],
|
|
229
|
+
[m.dist_name for m in chosen if m.ships_models],
|
|
230
|
+
exec_runner,
|
|
231
|
+
)
|
|
232
|
+
return [m.dist_name for m in chosen]
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def add_module(
|
|
236
|
+
spec: Annotated[
|
|
237
|
+
str,
|
|
238
|
+
typer.Argument(help="PyPI requirement, git+URL[@ref][#subdirectory=dir], or local path."),
|
|
239
|
+
],
|
|
240
|
+
module: Annotated[
|
|
241
|
+
str,
|
|
242
|
+
typer.Option("--module", help="Comma-separated module dist names (multi-module repos)."),
|
|
243
|
+
] = "",
|
|
244
|
+
all_modules: Annotated[
|
|
245
|
+
bool, typer.Option("--all", help="Add every module found in the repo.")
|
|
246
|
+
] = False,
|
|
247
|
+
path: Annotated[
|
|
248
|
+
Path,
|
|
249
|
+
typer.Option("--path", help="Project root or pyproject.toml. Defaults to cwd."),
|
|
250
|
+
] = Path(),
|
|
251
|
+
no_sync: Annotated[
|
|
252
|
+
bool,
|
|
253
|
+
typer.Option("--no-sync", help="Write pyproject only; skip uv sync / gen-pages."),
|
|
254
|
+
] = False,
|
|
255
|
+
yes: Annotated[bool, typer.Option("--yes", "-y", help="Never prompt (fail instead).")] = False,
|
|
256
|
+
) -> None:
|
|
257
|
+
"""Add a module dependency from PyPI, a git repo, or a local path."""
|
|
258
|
+
pyproject = path if path.name == "pyproject.toml" else path / "pyproject.toml"
|
|
259
|
+
select = [m.strip() for m in module.split(",") if m.strip()]
|
|
260
|
+
run_add(
|
|
261
|
+
spec,
|
|
262
|
+
pyproject=pyproject,
|
|
263
|
+
select=select or None,
|
|
264
|
+
all_modules=all_modules,
|
|
265
|
+
no_sync=no_sync,
|
|
266
|
+
assume_yes=yes,
|
|
267
|
+
)
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Built-in commands:
|
|
4
4
|
smpy new
|
|
5
|
+
smpy add
|
|
6
|
+
smpy update
|
|
5
7
|
smpy create-host
|
|
6
8
|
smpy create-module
|
|
7
9
|
smpy skills add / list / update
|
|
@@ -17,6 +19,7 @@ from typing import Annotated
|
|
|
17
19
|
|
|
18
20
|
import typer
|
|
19
21
|
|
|
22
|
+
from simple_module_cli.add_cmd import add_module, run_add
|
|
20
23
|
from simple_module_cli.case import to_kebab_case
|
|
21
24
|
from simple_module_cli.module_cmd import module_app
|
|
22
25
|
from simple_module_cli.new import new_project
|
|
@@ -26,6 +29,7 @@ from simple_module_cli.scaffolding import create_host as _create_host
|
|
|
26
29
|
from simple_module_cli.scaffolding import create_module as _create_module
|
|
27
30
|
from simple_module_cli.scaffolding import is_inside_existing_repo, resolve_framework_version
|
|
28
31
|
from simple_module_cli.skills_cmd import app as skills_app
|
|
32
|
+
from simple_module_cli.update_cmd import update_modules
|
|
29
33
|
|
|
30
34
|
app = typer.Typer(
|
|
31
35
|
help="SimpleModule developer CLI.",
|
|
@@ -34,6 +38,8 @@ app = typer.Typer(
|
|
|
34
38
|
)
|
|
35
39
|
|
|
36
40
|
app.command("new")(new_project)
|
|
41
|
+
app.command("add")(add_module)
|
|
42
|
+
app.command("update")(update_modules)
|
|
37
43
|
app.command("package-update")(package_update)
|
|
38
44
|
app.add_typer(skills_app, name="skills")
|
|
39
45
|
app.add_typer(module_app, name="module")
|
|
@@ -53,6 +59,14 @@ def create_host(
|
|
|
53
59
|
help="Comma-separated module names to declare as deps (e.g. Auth,Dashboard).",
|
|
54
60
|
),
|
|
55
61
|
] = "",
|
|
62
|
+
git_module: Annotated[
|
|
63
|
+
list[str] | None,
|
|
64
|
+
typer.Option(
|
|
65
|
+
"--git-module",
|
|
66
|
+
help="git+URL[@ref][#subdirectory=dir] module source; repeatable. "
|
|
67
|
+
"Multi-module repos need an explicit #subdirectory here.",
|
|
68
|
+
),
|
|
69
|
+
] = None,
|
|
56
70
|
) -> None:
|
|
57
71
|
"""Scaffold a new SimpleModule host project at ./<NAME>."""
|
|
58
72
|
target = dest or Path.cwd() / name
|
|
@@ -73,6 +87,10 @@ def create_host(
|
|
|
73
87
|
typer.echo(f"ERROR: {exc}", err=True)
|
|
74
88
|
raise typer.Exit(code=1) from exc
|
|
75
89
|
|
|
90
|
+
# create-host is non-interactive: multi-module repos need #subdirectory.
|
|
91
|
+
for spec in git_module or []:
|
|
92
|
+
run_add(spec, pyproject=target / "pyproject.toml", no_sync=True, assume_yes=True)
|
|
93
|
+
|
|
76
94
|
typer.echo(f"Created host '{name}' at {target}")
|
|
77
95
|
if selected:
|
|
78
96
|
typer.echo(f"Declared modules: {', '.join(selected)}")
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""Parsing and git-side resolution for ``smpy add`` / ``smpy update``.
|
|
2
|
+
|
|
3
|
+
A git module source is a normal named dependency plus a ``[tool.uv.sources]``
|
|
4
|
+
redirect. This module owns everything before the host pyproject is written:
|
|
5
|
+
parsing the spec string, classifying ``@ref`` against the remote, shallow
|
|
6
|
+
cloning for metadata, and scanning a clone for module packages.
|
|
7
|
+
|
|
8
|
+
Stdlib + the ``git`` binary only: the CLI distribution depends on typer +
|
|
9
|
+
tomlkit alone (see test_no_framework_deps.py); clones are read with tomllib.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import subprocess
|
|
15
|
+
import tomllib
|
|
16
|
+
from collections.abc import Callable, Iterable
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Literal
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"FoundModule",
|
|
23
|
+
"GitAddSpec",
|
|
24
|
+
"ParsedSpec",
|
|
25
|
+
"RefInfo",
|
|
26
|
+
"SpecError",
|
|
27
|
+
"classify_ref",
|
|
28
|
+
"derive_range",
|
|
29
|
+
"list_remote_refs",
|
|
30
|
+
"parse_add_spec",
|
|
31
|
+
"pick_latest_tag",
|
|
32
|
+
"satisfies",
|
|
33
|
+
"scan_modules",
|
|
34
|
+
"shallow_clone",
|
|
35
|
+
"version_tuple",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
GitRunner = Callable[..., str]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class SpecError(ValueError):
|
|
42
|
+
"""Malformed or unsupported `smpy add` spec."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class GitAddSpec:
|
|
47
|
+
url: str
|
|
48
|
+
ref: str | None
|
|
49
|
+
subdirectory: str | None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class ParsedSpec:
|
|
54
|
+
kind: Literal["pypi", "git", "path"]
|
|
55
|
+
raw: str
|
|
56
|
+
git: GitAddSpec | None = None
|
|
57
|
+
path: Path | None = None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def parse_add_spec(spec: str) -> ParsedSpec:
|
|
61
|
+
raw = spec.strip()
|
|
62
|
+
if not raw:
|
|
63
|
+
raise SpecError("empty spec")
|
|
64
|
+
if raw.startswith("git+"):
|
|
65
|
+
return ParsedSpec("git", raw, git=_parse_git(raw))
|
|
66
|
+
if raw.startswith((".", "/", "~")):
|
|
67
|
+
return ParsedSpec("path", raw, path=Path(raw).expanduser())
|
|
68
|
+
if "://" in raw:
|
|
69
|
+
raise SpecError(f"unsupported URL {raw!r}: git sources must start with git+")
|
|
70
|
+
return ParsedSpec("pypi", raw)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _parse_git(raw: str) -> GitAddSpec:
|
|
74
|
+
body = raw[len("git+") :]
|
|
75
|
+
subdirectory: str | None = None
|
|
76
|
+
if "#" in body:
|
|
77
|
+
body, fragment = body.split("#", 1)
|
|
78
|
+
for part in fragment.split("&"):
|
|
79
|
+
if part.startswith("subdirectory="):
|
|
80
|
+
subdirectory = part[len("subdirectory=") :] or None
|
|
81
|
+
elif part:
|
|
82
|
+
raise SpecError(
|
|
83
|
+
f"unsupported fragment {part!r}: only subdirectory=<dir> is understood"
|
|
84
|
+
)
|
|
85
|
+
ref: str | None = None
|
|
86
|
+
head, sep, tail = body.rpartition("@")
|
|
87
|
+
# `@` also appears in URL userinfo (ssh://git@host/...). A ref candidate
|
|
88
|
+
# never contains "/" and what precedes it must still be a full URL.
|
|
89
|
+
if sep and tail and "/" not in tail and "://" in head:
|
|
90
|
+
body, ref = head, tail
|
|
91
|
+
if "://" not in body:
|
|
92
|
+
raise SpecError(f"cannot parse a git URL out of {raw!r}")
|
|
93
|
+
return GitAddSpec(url=body, ref=ref, subdirectory=subdirectory)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# --- version helpers (no `packaging` dep — CLI depends on typer + tomlkit only) ---
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def version_tuple(v: str) -> tuple[int, ...]:
|
|
100
|
+
parts: list[int] = []
|
|
101
|
+
for part in v.split("."):
|
|
102
|
+
digits = ""
|
|
103
|
+
for ch in part:
|
|
104
|
+
if ch.isdigit():
|
|
105
|
+
digits += ch
|
|
106
|
+
else:
|
|
107
|
+
break
|
|
108
|
+
parts.append(int(digits) if digits else 0)
|
|
109
|
+
return tuple(parts)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _cmp(a: str, b: str) -> int:
|
|
113
|
+
ta, tb = version_tuple(a), version_tuple(b)
|
|
114
|
+
width = max(len(ta), len(tb))
|
|
115
|
+
ta += (0,) * (width - len(ta))
|
|
116
|
+
tb += (0,) * (width - len(tb))
|
|
117
|
+
return (ta > tb) - (ta < tb)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
_OPS = ("<=", ">=", "==", "!=", "<", ">")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def satisfies(version: str, constraint: str) -> bool:
|
|
124
|
+
for clause in constraint.split(","):
|
|
125
|
+
clause = clause.strip()
|
|
126
|
+
if not clause:
|
|
127
|
+
continue
|
|
128
|
+
for op in _OPS:
|
|
129
|
+
if clause.startswith(op):
|
|
130
|
+
bound = clause[len(op) :].strip()
|
|
131
|
+
c = _cmp(version, bound)
|
|
132
|
+
ok = {
|
|
133
|
+
"<=": c <= 0,
|
|
134
|
+
">=": c >= 0,
|
|
135
|
+
"==": c == 0,
|
|
136
|
+
"!=": c != 0,
|
|
137
|
+
"<": c < 0,
|
|
138
|
+
">": c > 0,
|
|
139
|
+
}[op]
|
|
140
|
+
if not ok:
|
|
141
|
+
return False
|
|
142
|
+
break
|
|
143
|
+
else:
|
|
144
|
+
return False
|
|
145
|
+
return True
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def derive_range(version: str) -> str:
|
|
149
|
+
major = version_tuple(version)[0]
|
|
150
|
+
upper = "1.0" if major == 0 else f"{major + 1}.0"
|
|
151
|
+
return f">={version},<{upper}"
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def pick_latest_tag(tags: Iterable[str], constraint: str | None) -> str | None:
|
|
155
|
+
best: str | None = None
|
|
156
|
+
for tag in tags:
|
|
157
|
+
if not tag.startswith("v") or len(tag) < 2 or not tag[1].isdigit():
|
|
158
|
+
continue
|
|
159
|
+
ver = tag[1:]
|
|
160
|
+
if constraint and not satisfies(ver, constraint):
|
|
161
|
+
continue
|
|
162
|
+
if best is None or _cmp(ver, best[1:]) > 0:
|
|
163
|
+
best = tag
|
|
164
|
+
return best
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# --- remote resolution + clone scanning ---
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@dataclass(frozen=True)
|
|
171
|
+
class RefInfo:
|
|
172
|
+
kind: Literal["tag", "branch", "rev", "default"]
|
|
173
|
+
value: str | None
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@dataclass(frozen=True)
|
|
177
|
+
class FoundModule:
|
|
178
|
+
dist_name: str
|
|
179
|
+
version: str
|
|
180
|
+
subdirectory: str | None
|
|
181
|
+
ships_models: bool
|
|
182
|
+
framework_range: str | None
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _default_git_runner(args: list[str], cwd: Path | None = None) -> str:
|
|
186
|
+
result = subprocess.run(["git", *args], cwd=cwd, check=True, capture_output=True, text=True)
|
|
187
|
+
return result.stdout
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def list_remote_refs(
|
|
191
|
+
url: str, *, run: GitRunner = _default_git_runner
|
|
192
|
+
) -> tuple[set[str], set[str]]:
|
|
193
|
+
out = run(["ls-remote", "--tags", "--heads", url])
|
|
194
|
+
tags: set[str] = set()
|
|
195
|
+
branches: set[str] = set()
|
|
196
|
+
for line in out.splitlines():
|
|
197
|
+
parts = line.split("\t")
|
|
198
|
+
if len(parts) != 2:
|
|
199
|
+
continue
|
|
200
|
+
ref = parts[1].removesuffix("^{}")
|
|
201
|
+
if ref.startswith("refs/tags/"):
|
|
202
|
+
tags.add(ref.removeprefix("refs/tags/"))
|
|
203
|
+
elif ref.startswith("refs/heads/"):
|
|
204
|
+
branches.add(ref.removeprefix("refs/heads/"))
|
|
205
|
+
return tags, branches
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def classify_ref(url: str, ref: str | None, *, run: GitRunner = _default_git_runner) -> RefInfo:
|
|
209
|
+
if ref is None:
|
|
210
|
+
return RefInfo("default", None)
|
|
211
|
+
tags, branches = list_remote_refs(url, run=run)
|
|
212
|
+
if ref in tags:
|
|
213
|
+
return RefInfo("tag", ref)
|
|
214
|
+
if ref in branches:
|
|
215
|
+
return RefInfo("branch", ref)
|
|
216
|
+
return RefInfo("rev", ref)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def shallow_clone(
|
|
220
|
+
url: str, ref_info: RefInfo, dest: Path, *, run: GitRunner = _default_git_runner
|
|
221
|
+
) -> Path:
|
|
222
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
223
|
+
base = ["clone", "--depth", "1", "--quiet"]
|
|
224
|
+
if ref_info.kind in ("tag", "branch") and ref_info.value:
|
|
225
|
+
run([*base, "--branch", ref_info.value, url, str(dest)])
|
|
226
|
+
elif ref_info.kind == "rev" and ref_info.value:
|
|
227
|
+
# --branch can't take a SHA; fetch the rev into a fresh clone instead.
|
|
228
|
+
run([*base, url, str(dest)])
|
|
229
|
+
run(["fetch", "--depth", "1", "origin", ref_info.value], cwd=dest)
|
|
230
|
+
run(["checkout", "--quiet", "FETCH_HEAD"], cwd=dest)
|
|
231
|
+
else:
|
|
232
|
+
run([*base, url, str(dest)])
|
|
233
|
+
return dest
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
_SKIP_DIRS = {".git", "node_modules", ".venv", "__pycache__", "dist", "build"}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _framework_range(deps: list[str]) -> str | None:
|
|
240
|
+
for dep in deps:
|
|
241
|
+
text = str(dep).strip()
|
|
242
|
+
if text.replace("-", "_").startswith("simple_module_core"):
|
|
243
|
+
for i, ch in enumerate(text):
|
|
244
|
+
if ch in "<>=!~":
|
|
245
|
+
return text[i:].strip()
|
|
246
|
+
return None
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def scan_modules(repo_root: Path) -> list[FoundModule]:
|
|
250
|
+
"""Find every package in the clone declaring the simple_module entry point."""
|
|
251
|
+
candidates: list[Path] = []
|
|
252
|
+
for pattern in ("pyproject.toml", "*/pyproject.toml", "*/*/pyproject.toml"):
|
|
253
|
+
candidates.extend(sorted(repo_root.glob(pattern)))
|
|
254
|
+
found: list[FoundModule] = []
|
|
255
|
+
for pyproject in candidates:
|
|
256
|
+
rel_parts = pyproject.relative_to(repo_root).parts
|
|
257
|
+
if any(part in _SKIP_DIRS for part in rel_parts):
|
|
258
|
+
continue
|
|
259
|
+
try:
|
|
260
|
+
data = tomllib.loads(pyproject.read_text(encoding="utf-8"))
|
|
261
|
+
except (OSError, tomllib.TOMLDecodeError):
|
|
262
|
+
continue
|
|
263
|
+
project = data.get("project") or {}
|
|
264
|
+
entry_points = project.get("entry-points") or {}
|
|
265
|
+
if "simple_module" not in entry_points or "name" not in project:
|
|
266
|
+
continue
|
|
267
|
+
pkg_root = pyproject.parent
|
|
268
|
+
subdirectory = None if pkg_root == repo_root else "/".join(rel_parts[:-1])
|
|
269
|
+
ships_models = any(
|
|
270
|
+
not any(part in _SKIP_DIRS for part in p.relative_to(pkg_root).parts)
|
|
271
|
+
for p in pkg_root.glob("*/models.py")
|
|
272
|
+
)
|
|
273
|
+
found.append(
|
|
274
|
+
FoundModule(
|
|
275
|
+
dist_name=str(project["name"]),
|
|
276
|
+
version=str(project.get("version", "0")),
|
|
277
|
+
subdirectory=subdirectory,
|
|
278
|
+
ships_models=ships_models,
|
|
279
|
+
framework_range=_framework_range(project.get("dependencies") or []),
|
|
280
|
+
)
|
|
281
|
+
)
|
|
282
|
+
return found
|