simple-module-cli 0.0.41__tar.gz → 0.0.42__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.41 → simple_module_cli-0.0.42}/PKG-INFO +1 -1
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/pyproject.toml +1 -1
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/cli.py +17 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/docker_assets.py +1 -1
- simple_module_cli-0.0.42/simple_module_cli/migration_recipes/mail_settings.py +48 -0
- simple_module_cli-0.0.42/simple_module_cli/migration_recipes/users_tokens.py +34 -0
- simple_module_cli-0.0.42/simple_module_cli/migrations_cmd.py +66 -0
- simple_module_cli-0.0.42/simple_module_cli/package_update.py +189 -0
- simple_module_cli-0.0.42/simple_module_cli/package_update_deps.py +153 -0
- simple_module_cli-0.0.42/simple_module_cli/package_update_npm.py +126 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/skills/simple-module-cli/SKILL.md +21 -3
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/docker/compose-base-postgres.yml.tpl +2 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/docker/compose-base-sqlite.yml.tpl +1 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/docker/compose-tasks-postgres.yml.tpl +4 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/docker/dockerignore +11 -1
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/docker/host-flat.Dockerfile +3 -2
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/docker/host.Dockerfile +4 -2
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/docker/worker.Dockerfile.tpl +3 -1
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/README.md.tpl +18 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/conftest.py +14 -0
- simple_module_cli-0.0.42/tests/test_cli_docker_persistence_secrets.py +94 -0
- simple_module_cli-0.0.42/tests/test_cli_package_update_groups.py +74 -0
- simple_module_cli-0.0.42/tests/test_cli_package_update_npm.py +220 -0
- simple_module_cli-0.0.42/tests/test_cli_prepare_data_migrations.py +185 -0
- simple_module_cli-0.0.42/tests/test_cli_prepare_data_postgres.py +89 -0
- simple_module_cli-0.0.41/simple_module_cli/package_update.py +0 -262
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/.gitignore +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/LICENSE +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/README.md +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/__init__.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/_add_install.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/_add_sources.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/_app_project_manifests.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/_env.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/_local_env.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/_module_build.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/_module_host.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/_templating.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/add_cmd.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/app_project.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/case.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/catalog.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/docker_cmd.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/framework_repo.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/git_source.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/module_cmd.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/new.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/openapi_cmd.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/openapi_types.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/pins.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/plugins.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/pypi.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/pyproject_edit.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/recipes.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/requirements.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/scaffolding.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/skills/README.md +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/skills/simple-module-conventions/SKILL.md +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/skills/simple-module-creating/SKILL.md +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/skills/simple-module-database/SKILL.md +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/skills/simple-module-doctor/SKILL.md +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/skills/simple-module-inertia-pages/SKILL.md +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/skills/simple-module-locales/SKILL.md +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/skills/simple-module-migrations/SKILL.md +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/skills/simple-module-registries/SKILL.md +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/skills/simple-module-testing/SKILL.md +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/skills_cmd.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/docker/Makefile.snippet +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/docker/compose-tasks-sqlite.yml.tpl +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/docker/compose-worker.yml.tpl +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/.env.example +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/.gitignore +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/Makefile +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/_optional/background_tasks/Makefile.snippet +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/_optional/background_tasks/run_worker.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/alembic.ini +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/client_app/app.tsx +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/client_app/main.tsx +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/client_app/module-assets.ts +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/client_app/package.json.tpl +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/client_app/pages/Error.tsx +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/client_app/pages/Landing.tsx +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/client_app/pages.ts +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/client_app/styles.css +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/client_app/tsconfig.json +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/client_app/vite.config.ts +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/client_app/vite.dev-url.ts +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/main.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/migrations/env.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/migrations/script.py.mako +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/migrations/versions/.gitkeep +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/pyproject.toml.tpl +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/routes.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/templates/index.html +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/.github/workflows/ci.yml +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/.github/workflows/publish.yml.tpl +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/.gitignore +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/README.md.tpl +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/__PACKAGE__/__init__.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/__PACKAGE__/constants.py.tpl +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/__PACKAGE__/contracts/__init__.py.tpl +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/__PACKAGE__/contracts/schemas.py.tpl +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/__PACKAGE__/deps.py.tpl +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/__PACKAGE__/endpoints/__init__.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/__PACKAGE__/endpoints/api.py.tpl +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/__PACKAGE__/endpoints/views.py.tpl +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/__PACKAGE__/locales/en.json.tpl +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/__PACKAGE__/models.py.tpl +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/__PACKAGE__/module.py.tpl +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/__PACKAGE__/pages/Browse.tsx.tpl +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/__PACKAGE__/pages/Create.tsx.tpl +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/__PACKAGE__/pages/Edit.tsx.tpl +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/__PACKAGE__/pages/api.generated.ts.tpl +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/__PACKAGE__/pages/api.ts.tpl +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/__PACKAGE__/py.typed +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/__PACKAGE__/service.py.tpl +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/__PACKAGE__/services.py.tpl +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/__PACKAGE__/settings.py.tpl +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/_optional/framework_repo/LICENSE +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/_optional/framework_repo/README.md.tpl +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/_optional/framework_repo/__PACKAGE__/pages/Browse.tsx.tpl +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/_optional/framework_repo/__PACKAGE__/pages/Create.tsx.tpl +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/_optional/framework_repo/__PACKAGE__/pages/Edit.tsx.tpl +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/_optional/framework_repo/pyproject.toml.tpl +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/_optional/standalone/package.json.tpl +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/_optional/standalone/tsconfig.json.tpl +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/package.json.tpl +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/pyproject.toml.tpl +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/tests/test___PACKAGE__.py.tpl +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/tsconfig.json.tpl +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/workspace/.env.example +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/workspace/.gitignore +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/workspace/Makefile +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/workspace/README.md.tpl +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/workspace/package.json.tpl +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/templates/workspace/pyproject.toml.tpl +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/update_cmd.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/uv_workspace.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/simple_module_cli/wizard.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_build_packaging.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_case.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_cli_add_cmd.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_cli_add_spec_parse.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_cli_add_workspace_member.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_cli_asset_prerequisites.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_cli_catalog.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_cli_catalog_depends_on.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_cli_docker_assets.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_cli_docker_workers.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_cli_gen_types_scaffold.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_cli_git_scan.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_cli_new.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_cli_new_dest_tolerance.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_cli_new_git_module.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_cli_new_migration_bootstrap.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_cli_new_regressions.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_cli_new_scaffold_layout.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_cli_openapi_export.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_cli_openapi_gen_types.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_cli_package_update.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_cli_package_update_pins.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_cli_pyproject_edit.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_cli_recipes.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_cli_update_sources.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_cli_vite_port_env.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_cli_wizard.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_cli_wizard_git.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_create_module_ci_skip.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_env_helper.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_framework_repo_wiring.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_module_cmd_build.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_module_cmd_e2e.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_module_cmd_verify.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_module_css_assets.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_module_css_build.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_module_css_subdir_sources.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_module_npm_aliases.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_module_pages_manifest.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_no_framework_deps.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_plugin_discovery.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_scaffold_app_tsx_spa_links.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_scaffold_rollback.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_scaffolded_host_setup_wizard.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_scaffolding_framework_repo.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_scaffolding_host.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_scaffolding_module.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_scaffolding_module_js.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_skills_cmd.py +0 -0
- {simple_module_cli-0.0.41 → simple_module_cli-0.0.42}/tests/test_template_requires_framework.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: simple_module_cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.42
|
|
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
|
|
@@ -23,6 +23,7 @@ import typer
|
|
|
23
23
|
from simple_module_cli.add_cmd import add_module, run_add
|
|
24
24
|
from simple_module_cli.case import to_kebab_case
|
|
25
25
|
from simple_module_cli.docker_cmd import docker_app
|
|
26
|
+
from simple_module_cli.migrations_cmd import migrations_app
|
|
26
27
|
from simple_module_cli.module_cmd import module_app
|
|
27
28
|
from simple_module_cli.new import new_project
|
|
28
29
|
from simple_module_cli.openapi_cmd import openapi_app
|
|
@@ -49,6 +50,22 @@ app.add_typer(docker_app, name="docker")
|
|
|
49
50
|
app.add_typer(skills_app, name="skills")
|
|
50
51
|
app.add_typer(module_app, name="module")
|
|
51
52
|
app.add_typer(openapi_app, name="openapi")
|
|
53
|
+
app.add_typer(migrations_app, name="migrations")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@app.command("doctor")
|
|
57
|
+
def doctor() -> None:
|
|
58
|
+
"""Check installed modules and their database compatibility."""
|
|
59
|
+
try:
|
|
60
|
+
from simple_module_core.__main__ import main as run_doctor
|
|
61
|
+
except ImportError:
|
|
62
|
+
# The CLI installs on its own (to scaffold); doctor needs a project's framework.
|
|
63
|
+
typer.echo(
|
|
64
|
+
"ERROR: smpy doctor needs simple_module_core; run it inside a project.", err=True
|
|
65
|
+
)
|
|
66
|
+
raise typer.Exit(1) from None
|
|
67
|
+
|
|
68
|
+
raise typer.Exit(run_doctor())
|
|
52
69
|
|
|
53
70
|
|
|
54
71
|
@app.command("create-host")
|
|
@@ -78,7 +78,7 @@ def scaffold_docker_assets(target: Path, ctx: ScaffoldCtx, *, flat: bool = False
|
|
|
78
78
|
.replace(_PG_DB_TOKEN, pg_db)
|
|
79
79
|
.replace(_APP_EXTRA_ENV_TOKEN, _APP_BROKER_ENV if with_tasks else "")
|
|
80
80
|
)
|
|
81
|
-
volumes = ["pgdata"] if db == "postgres" else ["appdata"]
|
|
81
|
+
volumes = ["appdata", "pgdata"] if db == "postgres" else ["appdata"]
|
|
82
82
|
if with_tasks:
|
|
83
83
|
compose += _read(templates / f"compose-tasks-{db}.yml.tpl").replace(_PG_DB_TOKEN, pg_db)
|
|
84
84
|
volumes.append("redisdata")
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Frozen system mail-settings move from d7b3a9e5c1f2 (#711).
|
|
2
|
+
|
|
3
|
+
An existing target value wins; the obsolete source row is removed.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sqlalchemy as sa
|
|
7
|
+
from alembic import op
|
|
8
|
+
|
|
9
|
+
_TABLE = "settings_setting"
|
|
10
|
+
_SCOPE_SYSTEM = "system"
|
|
11
|
+
_SYSTEM_SCOPE_ID = ""
|
|
12
|
+
|
|
13
|
+
_KEY_PAIRS = (
|
|
14
|
+
("users.mailer", "mail.backend"),
|
|
15
|
+
*((f"users.smtp_{f}", f"mail.smtp_{f}") for f in ("host", "port", "username", "password")),
|
|
16
|
+
("users.smtp_from", "mail.smtp_from"),
|
|
17
|
+
("users.smtp_tls", "mail.smtp_tls"),
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _rename(old: str, new: str) -> None:
|
|
22
|
+
scope = {"scope": _SCOPE_SYSTEM, "scope_id": _SYSTEM_SCOPE_ID, "old": old, "new": new}
|
|
23
|
+
bind = op.get_bind()
|
|
24
|
+
bind.execute(
|
|
25
|
+
sa.text(
|
|
26
|
+
f"DELETE FROM {_TABLE} WHERE scope = :scope AND scope_id = :scope_id AND key = :old"
|
|
27
|
+
f" AND EXISTS (SELECT 1 FROM {_TABLE} AS t WHERE t.scope = :scope"
|
|
28
|
+
" AND t.scope_id = :scope_id AND t.key = :new)"
|
|
29
|
+
),
|
|
30
|
+
scope,
|
|
31
|
+
)
|
|
32
|
+
bind.execute(
|
|
33
|
+
sa.text(
|
|
34
|
+
f"UPDATE {_TABLE} SET key = :new"
|
|
35
|
+
" WHERE scope = :scope AND scope_id = :scope_id AND key = :old"
|
|
36
|
+
),
|
|
37
|
+
scope,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def upgrade() -> None:
|
|
42
|
+
for old, new in _KEY_PAIRS:
|
|
43
|
+
_rename(old, new)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def downgrade() -> None:
|
|
47
|
+
for old, new in _KEY_PAIRS:
|
|
48
|
+
_rename(new, old)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Frozen token-storage upgrade from a626b1c8d023 (#711).
|
|
2
|
+
|
|
3
|
+
Upgrade and downgrade invalidate all access/refresh credentials. Stop all old
|
|
4
|
+
application workers before upgrading; clients must sign in again.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sqlalchemy as sa
|
|
8
|
+
from alembic import op
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _clear_tokens() -> None:
|
|
12
|
+
for table in ("users_refresh_token", "users_access_token"):
|
|
13
|
+
op.execute(sa.table(table).delete())
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def upgrade() -> None:
|
|
17
|
+
_clear_tokens()
|
|
18
|
+
with op.batch_alter_table("users_refresh_token") as batch:
|
|
19
|
+
batch.alter_column(
|
|
20
|
+
"token", existing_type=sa.Uuid(), type_=sa.String(64), postgresql_using="token::text"
|
|
21
|
+
)
|
|
22
|
+
with op.batch_alter_table("users_access_token") as batch:
|
|
23
|
+
batch.alter_column("token", existing_type=sa.String(43), type_=sa.String(64))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def downgrade() -> None:
|
|
27
|
+
# Digests cannot be restored to UUIDs or bearer secrets.
|
|
28
|
+
_clear_tokens()
|
|
29
|
+
with op.batch_alter_table("users_refresh_token") as batch:
|
|
30
|
+
batch.alter_column(
|
|
31
|
+
"token", existing_type=sa.String(64), type_=sa.Uuid(), postgresql_using="token::uuid"
|
|
32
|
+
)
|
|
33
|
+
with op.batch_alter_table("users_access_token") as batch:
|
|
34
|
+
batch.alter_column("token", existing_type=sa.String(64), type_=sa.String(43))
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Prepare frozen framework upgrades in a consuming host's own Alembic graph."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from importlib.resources import files
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Annotated
|
|
9
|
+
from uuid import uuid4
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
|
|
13
|
+
migrations_app = typer.Typer(help="Prepare host-owned framework upgrade revisions.")
|
|
14
|
+
_RECIPES = {
|
|
15
|
+
"mail.settings_namespace_v1": ("mail", "mail_settings.py"),
|
|
16
|
+
"users.hashed_bearer_tokens_v1": ("users", "users_tokens.py"),
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@migrations_app.command("prepare")
|
|
21
|
+
def prepare(
|
|
22
|
+
upgrade_id: Annotated[str, typer.Argument(help="Stable framework upgrade id.")],
|
|
23
|
+
head: Annotated[str, typer.Option("--head", help="Explicit host head or label@head.")],
|
|
24
|
+
config: Annotated[Path | None, typer.Option("--config", help="Host alembic.ini.")] = None,
|
|
25
|
+
) -> None:
|
|
26
|
+
"""Write a self-contained revision; never connect to or migrate the database."""
|
|
27
|
+
if upgrade_id not in _RECIPES:
|
|
28
|
+
raise typer.BadParameter(f"Unknown upgrade {upgrade_id!r}; choose {', '.join(_RECIPES)}")
|
|
29
|
+
try:
|
|
30
|
+
from alembic.config import Config
|
|
31
|
+
from alembic.script import ScriptDirectory
|
|
32
|
+
from alembic.util.exc import CommandError
|
|
33
|
+
except ImportError as exc:
|
|
34
|
+
raise typer.BadParameter(
|
|
35
|
+
"Run this command in the host environment with Alembic installed"
|
|
36
|
+
) from exc
|
|
37
|
+
|
|
38
|
+
root = Path(os.environ.get("SM_PROJECT_ROOT", Path.cwd()))
|
|
39
|
+
if config is None:
|
|
40
|
+
config = root / "alembic.ini"
|
|
41
|
+
if not config.is_file():
|
|
42
|
+
config = root / "host/alembic.ini"
|
|
43
|
+
if not config.is_file():
|
|
44
|
+
raise typer.BadParameter(f"Host Alembic config not found: {config}")
|
|
45
|
+
try:
|
|
46
|
+
script = ScriptDirectory.from_config(Config(str(config)))
|
|
47
|
+
parent = script.get_revision(head)
|
|
48
|
+
if parent is None or not parent.is_head:
|
|
49
|
+
raise typer.BadParameter("--head must resolve to one existing host head")
|
|
50
|
+
label, recipe = _RECIPES[upgrade_id]
|
|
51
|
+
existing = {name for rev in script.walk_revisions() for name in rev.branch_labels}
|
|
52
|
+
except CommandError as exc:
|
|
53
|
+
raise typer.BadParameter(str(exc)) from exc
|
|
54
|
+
|
|
55
|
+
# Frozen source is copied, so future package changes cannot change this revision.
|
|
56
|
+
source = files("simple_module_cli").joinpath("migration_recipes", recipe).read_text()
|
|
57
|
+
revision = uuid4().hex[:12]
|
|
58
|
+
branch_labels = None if label in existing else (label,)
|
|
59
|
+
identifiers = (
|
|
60
|
+
f"\nrevision = {revision!r}\ndown_revision = {parent.revision!r}\n"
|
|
61
|
+
f"branch_labels = {branch_labels!r}\ndepends_on = None\n"
|
|
62
|
+
)
|
|
63
|
+
path = Path(script.versions) / f"{revision}_{upgrade_id.replace('.', '_')}.py"
|
|
64
|
+
with path.open("x") as output:
|
|
65
|
+
output.write(source + identifiers)
|
|
66
|
+
typer.echo(f"Prepared {upgrade_id}: {path}. Review it, then run alembic upgrade heads.")
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""``smpy package-update`` — bump simple_module_* deps to latest PyPI versions.
|
|
2
|
+
|
|
3
|
+
Walks the project's ``pyproject.toml`` (and any ``[tool.uv.workspace]`` members),
|
|
4
|
+
finds every dependency whose distribution name starts with ``simple_module_`` /
|
|
5
|
+
``simple-module-``, queries PyPI for the latest non-yanked release, and points
|
|
6
|
+
the constraint at it. Includes optional dependencies and dependency groups.
|
|
7
|
+
Matching npm packages use the selected core release when published on npm.
|
|
8
|
+
|
|
9
|
+
The rewrite preserves the pin style you wrote: ``==0.0.32`` becomes
|
|
10
|
+
``==0.0.33``, ``>=0.0.32`` becomes ``>=0.0.33``, and an upper bound that
|
|
11
|
+
excludes the latest release skips the dependency instead of being dropped.
|
|
12
|
+
``--loosen`` restores the older behaviour of rewriting every constraint to
|
|
13
|
+
``name>=<latest>``.
|
|
14
|
+
|
|
15
|
+
Dependencies whose ``[tool.uv.sources]`` entry points at a workspace member, a
|
|
16
|
+
local path, a git ref, or a URL are left untouched — those aren't installed
|
|
17
|
+
from PyPI.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Annotated
|
|
25
|
+
|
|
26
|
+
import tomlkit
|
|
27
|
+
import typer
|
|
28
|
+
|
|
29
|
+
from simple_module_cli.package_update_deps import (
|
|
30
|
+
Change,
|
|
31
|
+
Skip,
|
|
32
|
+
collect_sm_deps,
|
|
33
|
+
local_sources,
|
|
34
|
+
normalize,
|
|
35
|
+
process_file,
|
|
36
|
+
source_map,
|
|
37
|
+
workspace_member_dirs,
|
|
38
|
+
)
|
|
39
|
+
from simple_module_cli.package_update_npm import load_manifests, plan_npm
|
|
40
|
+
from simple_module_cli.pypi import Fetcher, default_fetcher, fetch_latest
|
|
41
|
+
|
|
42
|
+
__all__ = ["package_update", "run_update"]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
_CORE = "simple-module-core"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _npm_target(cache: dict[str, str | None], skips: list[Skip]) -> tuple[str | None, str | None]:
|
|
49
|
+
"""The core version npm pins follow, or why there is none.
|
|
50
|
+
|
|
51
|
+
npm pins move in lockstep with the Python core pin, so when this run left
|
|
52
|
+
that pin alone (an upper bound, a local source) the npm pins stay too.
|
|
53
|
+
"""
|
|
54
|
+
for skip in skips:
|
|
55
|
+
if normalize(skip.package) == _CORE:
|
|
56
|
+
return None, f"simple_module_core not updated ({skip.reason})"
|
|
57
|
+
latest = next((v for n, v in cache.items() if normalize(n) == _CORE), None)
|
|
58
|
+
return latest, None if latest else "simple_module_core not found on PyPI"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _print_summary(changes: list[Change], skips: list[Skip], dry_run: bool) -> None:
|
|
62
|
+
if not changes and not skips:
|
|
63
|
+
typer.echo("No simple_module_* or @simple-module-py/* dependencies found.")
|
|
64
|
+
return
|
|
65
|
+
by_file: dict[Path, list[str]] = {}
|
|
66
|
+
for c in changes:
|
|
67
|
+
by_file.setdefault(c.file, []).append(f" {c.package}: {c.old} → {c.new}")
|
|
68
|
+
for s in skips:
|
|
69
|
+
by_file.setdefault(s.file, []).append(f" {s.package}: skipped ({s.reason})")
|
|
70
|
+
for file, lines in by_file.items():
|
|
71
|
+
typer.echo(f"\n{file}")
|
|
72
|
+
for line in lines:
|
|
73
|
+
typer.echo(line)
|
|
74
|
+
if changes:
|
|
75
|
+
verb = "Would update" if dry_run else "Updated"
|
|
76
|
+
file_count = len({c.file for c in changes})
|
|
77
|
+
typer.echo(f"\n{verb} {len(changes)} dependency(ies) across {file_count} file(s).")
|
|
78
|
+
if not dry_run:
|
|
79
|
+
if any(c.file.suffix == ".toml" for c in changes):
|
|
80
|
+
typer.echo("Run `uv sync` to apply Python changes.")
|
|
81
|
+
for directory in sorted(
|
|
82
|
+
{c.file.parent for c in changes if c.file.name == "package.json"}
|
|
83
|
+
):
|
|
84
|
+
typer.echo(f"Run `npm install` in {directory} to apply npm changes.")
|
|
85
|
+
elif skips:
|
|
86
|
+
typer.echo("\nNo updates applied.")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def run_update(
|
|
90
|
+
path: Path,
|
|
91
|
+
*,
|
|
92
|
+
dry_run: bool = False,
|
|
93
|
+
include_pre: bool = False,
|
|
94
|
+
loosen: bool = False,
|
|
95
|
+
fetcher: Fetcher | None = None,
|
|
96
|
+
) -> None:
|
|
97
|
+
"""Programmatic entry point — separated from the Typer command for testing."""
|
|
98
|
+
path = path.resolve()
|
|
99
|
+
root = path if path.name == "pyproject.toml" else path / "pyproject.toml"
|
|
100
|
+
if not root.is_file():
|
|
101
|
+
typer.echo(f"ERROR: {root} not found.", err=True)
|
|
102
|
+
raise typer.Exit(code=1)
|
|
103
|
+
|
|
104
|
+
fetch = fetcher or default_fetcher
|
|
105
|
+
root_doc = tomlkit.parse(root.read_bytes().decode("utf-8"))
|
|
106
|
+
files: list[tuple[Path, tomlkit.TOMLDocument]] = [(root, root_doc)]
|
|
107
|
+
for member in workspace_member_dirs(root, root_doc):
|
|
108
|
+
if member == root:
|
|
109
|
+
continue
|
|
110
|
+
files.append((member, tomlkit.parse(member.read_bytes().decode("utf-8"))))
|
|
111
|
+
|
|
112
|
+
npm_manifests = load_manifests([file.parent for file, _ in files])
|
|
113
|
+
inherited = source_map(root_doc)
|
|
114
|
+
locals_by_file = {file: local_sources(doc, inherited) for file, doc in files}
|
|
115
|
+
|
|
116
|
+
# Pre-fetch all unique sm packages in parallel — PyPI calls dominate runtime.
|
|
117
|
+
names = {n for file, doc in files for n in collect_sm_deps(doc, locals_by_file[file])}
|
|
118
|
+
if any(manifest.packages for manifest in npm_manifests) and not any(
|
|
119
|
+
normalize(n) == _CORE for n in names
|
|
120
|
+
):
|
|
121
|
+
names.add("simple_module_core")
|
|
122
|
+
unique_names = sorted(names)
|
|
123
|
+
cache: dict[str, str | None] = {}
|
|
124
|
+
if unique_names:
|
|
125
|
+
with ThreadPoolExecutor(max_workers=min(8, len(unique_names))) as pool:
|
|
126
|
+
results = pool.map(
|
|
127
|
+
lambda n: (n, fetch_latest(n, include_pre=include_pre, fetcher=fetch)),
|
|
128
|
+
unique_names,
|
|
129
|
+
)
|
|
130
|
+
cache = dict(results)
|
|
131
|
+
|
|
132
|
+
all_changes: list[Change] = []
|
|
133
|
+
all_skips: list[Skip] = []
|
|
134
|
+
pending: list[tuple[Path, str]] = []
|
|
135
|
+
|
|
136
|
+
for file, doc in files:
|
|
137
|
+
changes, skips, new_doc = process_file(
|
|
138
|
+
file, doc, cache=cache, loosen=loosen, local=locals_by_file[file]
|
|
139
|
+
)
|
|
140
|
+
all_changes.extend(changes)
|
|
141
|
+
all_skips.extend(skips)
|
|
142
|
+
if new_doc is not None:
|
|
143
|
+
pending.append((file, tomlkit.dumps(new_doc)))
|
|
144
|
+
|
|
145
|
+
target, blocked = _npm_target(cache, all_skips)
|
|
146
|
+
changes, skips, npm_pending = plan_npm(npm_manifests, target, fetch, blocked=blocked)
|
|
147
|
+
all_changes.extend(changes)
|
|
148
|
+
all_skips.extend(skips)
|
|
149
|
+
pending.extend(npm_pending)
|
|
150
|
+
|
|
151
|
+
if not dry_run:
|
|
152
|
+
for file, content in pending:
|
|
153
|
+
# newline="" keeps each file's own line endings (CRLF must not become CRCRLF).
|
|
154
|
+
file.write_text(content, encoding="utf-8", newline="")
|
|
155
|
+
|
|
156
|
+
_print_summary(all_changes, all_skips, dry_run)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def package_update(
|
|
160
|
+
path: Annotated[
|
|
161
|
+
Path,
|
|
162
|
+
typer.Option("--path", help="Project root or pyproject.toml. Defaults to cwd."),
|
|
163
|
+
] = Path(),
|
|
164
|
+
dry_run: Annotated[
|
|
165
|
+
bool,
|
|
166
|
+
typer.Option("--dry-run", help="Show planned changes without writing."),
|
|
167
|
+
] = False,
|
|
168
|
+
include_pre: Annotated[
|
|
169
|
+
bool,
|
|
170
|
+
typer.Option("--include-pre", help="Include pre-release versions."),
|
|
171
|
+
] = False,
|
|
172
|
+
loosen: Annotated[
|
|
173
|
+
bool,
|
|
174
|
+
typer.Option(
|
|
175
|
+
"--loosen",
|
|
176
|
+
help="Rewrite Python constraints to `>=<latest>` instead of keeping their operator.",
|
|
177
|
+
),
|
|
178
|
+
] = False,
|
|
179
|
+
) -> None:
|
|
180
|
+
"""Update simple_module_* dependencies and lockstep @simple-module-py/* pins.
|
|
181
|
+
|
|
182
|
+
Each constraint keeps the operator it already has — `==0.0.32` becomes
|
|
183
|
+
`==0.0.33`, `>=0.0.32` becomes `>=0.0.33` — so bumping versions never
|
|
184
|
+
silently changes a project's pinning policy. A dependency whose upper
|
|
185
|
+
bound excludes the latest release is reported and left alone. Pass
|
|
186
|
+
`--loosen` to rewrite Python constraints to `>=<latest>` instead.
|
|
187
|
+
npm pins keep exact, caret or tilde style and skip unpublished core versions.
|
|
188
|
+
"""
|
|
189
|
+
run_update(path, dry_run=dry_run, include_pre=include_pre, loosen=loosen)
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Python manifest discovery and dependency planning for package-update."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import tomlkit
|
|
11
|
+
from tomlkit.items import Array
|
|
12
|
+
|
|
13
|
+
from simple_module_cli.requirements import parse_requirement, rewrite_requirement
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class Change:
|
|
18
|
+
file: Path
|
|
19
|
+
package: str
|
|
20
|
+
old: str
|
|
21
|
+
new: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class Skip:
|
|
26
|
+
file: Path
|
|
27
|
+
package: str
|
|
28
|
+
reason: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def normalize(name: str) -> str:
|
|
32
|
+
return re.sub(r"[-_.]+", "-", name).lower()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _is_sm_package(name: str) -> bool:
|
|
36
|
+
return normalize(name).startswith("simple-module-")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _dep_name(spec: str) -> str | None:
|
|
40
|
+
"""Extract the distribution name from a PEP 508 requirement string."""
|
|
41
|
+
parsed = parse_requirement(spec)
|
|
42
|
+
return parsed.name if parsed else None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _is_local_source(source: Any) -> bool:
|
|
46
|
+
if isinstance(source, (list, Array)):
|
|
47
|
+
return any(_is_local_source(item) for item in source)
|
|
48
|
+
if not isinstance(source, dict):
|
|
49
|
+
return False
|
|
50
|
+
if source.get("workspace") is True:
|
|
51
|
+
return True
|
|
52
|
+
return any(key in source for key in ("path", "git", "url"))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _get_uv_section(doc: tomlkit.TOMLDocument, key: str) -> dict[str, Any] | None:
|
|
56
|
+
tool = doc.get("tool")
|
|
57
|
+
if not isinstance(tool, dict):
|
|
58
|
+
return None
|
|
59
|
+
uv = tool.get("uv")
|
|
60
|
+
if not isinstance(uv, dict):
|
|
61
|
+
return None
|
|
62
|
+
section = uv.get(key)
|
|
63
|
+
return section if isinstance(section, dict) else None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def workspace_member_dirs(root_pyproject: Path, doc: tomlkit.TOMLDocument) -> list[Path]:
|
|
67
|
+
workspace = _get_uv_section(doc, "workspace")
|
|
68
|
+
if workspace is None:
|
|
69
|
+
return []
|
|
70
|
+
members = workspace.get("members") or []
|
|
71
|
+
base = root_pyproject.parent
|
|
72
|
+
out: list[Path] = []
|
|
73
|
+
for pattern in members:
|
|
74
|
+
for match in sorted(base.glob(str(pattern))):
|
|
75
|
+
if (match / "pyproject.toml").is_file():
|
|
76
|
+
out.append(match / "pyproject.toml")
|
|
77
|
+
return list(dict.fromkeys(p.resolve() for p in out))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def source_map(doc: tomlkit.TOMLDocument) -> dict[str, Any]:
|
|
81
|
+
return {normalize(name): src for name, src in (_get_uv_section(doc, "sources") or {}).items()}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def local_sources(doc: tomlkit.TOMLDocument, inherited: dict[str, Any]) -> set[str]:
|
|
85
|
+
sources = {**inherited, **source_map(doc)}
|
|
86
|
+
return {name for name, source in sources.items() if _is_local_source(source)}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _dependency_arrays(doc: tomlkit.TOMLDocument):
|
|
90
|
+
project = doc.get("project", {})
|
|
91
|
+
if isinstance(project, dict):
|
|
92
|
+
deps = project.get("dependencies")
|
|
93
|
+
if isinstance(deps, (list, Array)):
|
|
94
|
+
yield deps
|
|
95
|
+
optional = project.get("optional-dependencies", {})
|
|
96
|
+
if isinstance(optional, dict):
|
|
97
|
+
for deps in optional.values():
|
|
98
|
+
if isinstance(deps, (list, Array)):
|
|
99
|
+
yield deps
|
|
100
|
+
groups = doc.get("dependency-groups", {})
|
|
101
|
+
if isinstance(groups, dict):
|
|
102
|
+
for deps in groups.values():
|
|
103
|
+
if isinstance(deps, (list, Array)):
|
|
104
|
+
yield deps
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def collect_sm_deps(doc: tomlkit.TOMLDocument, local: set[str]) -> list[str]:
|
|
108
|
+
return [
|
|
109
|
+
name
|
|
110
|
+
for deps in _dependency_arrays(doc)
|
|
111
|
+
for raw in deps
|
|
112
|
+
if isinstance(raw, str)
|
|
113
|
+
and (name := _dep_name(raw))
|
|
114
|
+
and _is_sm_package(name)
|
|
115
|
+
and normalize(name) not in local
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def process_file(
|
|
120
|
+
path: Path,
|
|
121
|
+
doc: tomlkit.TOMLDocument,
|
|
122
|
+
*,
|
|
123
|
+
cache: dict[str, str | None],
|
|
124
|
+
loosen: bool = False,
|
|
125
|
+
local: set[str],
|
|
126
|
+
) -> tuple[list[Change], list[Skip], tomlkit.TOMLDocument | None]:
|
|
127
|
+
changes: list[Change] = []
|
|
128
|
+
skips: list[Skip] = []
|
|
129
|
+
|
|
130
|
+
for deps in _dependency_arrays(doc):
|
|
131
|
+
for idx, raw in enumerate(deps):
|
|
132
|
+
if not isinstance(raw, str):
|
|
133
|
+
continue
|
|
134
|
+
dep_str = raw
|
|
135
|
+
name = _dep_name(dep_str)
|
|
136
|
+
if not name or not _is_sm_package(name):
|
|
137
|
+
continue
|
|
138
|
+
if normalize(name) in local:
|
|
139
|
+
skips.append(Skip(path, name, "workspace/local source"))
|
|
140
|
+
continue
|
|
141
|
+
latest = cache.get(name)
|
|
142
|
+
if latest is None:
|
|
143
|
+
skips.append(Skip(path, name, "not found on PyPI"))
|
|
144
|
+
continue
|
|
145
|
+
result = rewrite_requirement(dep_str, latest, loosen=loosen)
|
|
146
|
+
if result.spec is None:
|
|
147
|
+
if result.reason:
|
|
148
|
+
skips.append(Skip(path, name, result.reason))
|
|
149
|
+
continue
|
|
150
|
+
deps[idx] = result.spec
|
|
151
|
+
changes.append(Change(path, name, dep_str.strip(), result.spec))
|
|
152
|
+
|
|
153
|
+
return changes, skips, doc if changes else None
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Plan lockstep npm updates without installing packages or writing files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
from urllib.parse import quote
|
|
11
|
+
|
|
12
|
+
from simple_module_cli.package_update_deps import Change, Skip
|
|
13
|
+
from simple_module_cli.pypi import Fetcher
|
|
14
|
+
|
|
15
|
+
_SECTIONS = ("dependencies", "devDependencies", "optionalDependencies", "peerDependencies")
|
|
16
|
+
_PREFIX = "@simple-module-py/"
|
|
17
|
+
_REGISTRY = "https://registry.npmjs.org/"
|
|
18
|
+
_SEMVER = re.compile(
|
|
19
|
+
r"(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)"
|
|
20
|
+
r"(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?"
|
|
21
|
+
r"(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _version_key(version: str) -> tuple | None:
|
|
26
|
+
match = _SEMVER.fullmatch(version)
|
|
27
|
+
if not match:
|
|
28
|
+
return None
|
|
29
|
+
major, minor, patch, pre = match.groups()
|
|
30
|
+
identifiers = (pre or "").split(".") if pre else []
|
|
31
|
+
if any(part.isdigit() and len(part) > 1 and part.startswith("0") for part in identifiers):
|
|
32
|
+
return None
|
|
33
|
+
suffix = tuple((0, int(p)) if p.isdigit() else (1, p) for p in identifiers)
|
|
34
|
+
return (int(major), int(minor), int(patch), pre is None, suffix)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class Manifest:
|
|
39
|
+
path: Path
|
|
40
|
+
original: str
|
|
41
|
+
doc: dict[str, Any]
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def packages(self) -> set[str]:
|
|
45
|
+
return {
|
|
46
|
+
name
|
|
47
|
+
for section in _SECTIONS
|
|
48
|
+
for name in self.doc.get(section, {})
|
|
49
|
+
if name.startswith(_PREFIX)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def load_manifests(directories: list[Path]) -> list[Manifest]:
|
|
54
|
+
manifests = []
|
|
55
|
+
paths = dict.fromkeys(
|
|
56
|
+
path.resolve()
|
|
57
|
+
for directory in directories
|
|
58
|
+
for path in (directory / "package.json", directory / "client_app/package.json")
|
|
59
|
+
if path.is_file()
|
|
60
|
+
)
|
|
61
|
+
for path in paths:
|
|
62
|
+
original = path.read_bytes().decode("utf-8")
|
|
63
|
+
doc = json.loads(original)
|
|
64
|
+
if not isinstance(doc, dict):
|
|
65
|
+
raise ValueError(f"{path}: expected a JSON object")
|
|
66
|
+
for section in _SECTIONS:
|
|
67
|
+
if section in doc and not isinstance(doc[section], dict):
|
|
68
|
+
raise ValueError(f"{path}: {section} must be an object")
|
|
69
|
+
manifests.append(Manifest(path, original, doc))
|
|
70
|
+
return manifests
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _render(manifest: Manifest) -> str:
|
|
74
|
+
match = re.search(r"(?:\r?\n)([ \t]+)\S", manifest.original)
|
|
75
|
+
indent = match[1] if match else None
|
|
76
|
+
newline = "\r\n" if "\r\n" in manifest.original else "\n"
|
|
77
|
+
rendered = json.dumps(manifest.doc, indent=indent, ensure_ascii=False)
|
|
78
|
+
if newline != "\n":
|
|
79
|
+
rendered = rendered.replace("\n", newline)
|
|
80
|
+
return rendered + (newline if manifest.original.endswith("\n") else "")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def plan_npm(
|
|
84
|
+
manifests: list[Manifest], target: str | None, fetcher: Fetcher, *, blocked: str | None = None
|
|
85
|
+
) -> tuple[list[Change], list[Skip], list[tuple[Path, str]]]:
|
|
86
|
+
changes: list[Change] = []
|
|
87
|
+
skips: list[Skip] = []
|
|
88
|
+
pending: list[tuple[Path, str]] = []
|
|
89
|
+
available: dict[str, bool] = {}
|
|
90
|
+
target_key = _version_key(target) if target else None
|
|
91
|
+
for manifest in manifests:
|
|
92
|
+
changed = False
|
|
93
|
+
for section in _SECTIONS:
|
|
94
|
+
for name, pin in manifest.doc.get(section, {}).items():
|
|
95
|
+
if not name.startswith(_PREFIX):
|
|
96
|
+
continue
|
|
97
|
+
style = pin[:1] if isinstance(pin, str) and pin[:1] in ("^", "~") else ""
|
|
98
|
+
current = _version_key(pin[len(style) :]) if isinstance(pin, str) else None
|
|
99
|
+
reason = None
|
|
100
|
+
if current is None:
|
|
101
|
+
reason = f"unsupported npm reference {pin!r}"
|
|
102
|
+
elif target_key is None:
|
|
103
|
+
reason = blocked or f"no npm-compatible simple_module_core target ({target})"
|
|
104
|
+
elif current >= target_key:
|
|
105
|
+
continue
|
|
106
|
+
else:
|
|
107
|
+
if name not in available:
|
|
108
|
+
try:
|
|
109
|
+
data = fetcher(_REGISTRY + quote(name, safe="@"))
|
|
110
|
+
available[name] = isinstance(data, dict) and target in (
|
|
111
|
+
data.get("versions") or {}
|
|
112
|
+
)
|
|
113
|
+
except (OSError, ValueError): # HTTP/URL/timeout, bad JSON
|
|
114
|
+
available[name] = False
|
|
115
|
+
if not available[name]:
|
|
116
|
+
reason = f"npm release {target} unavailable"
|
|
117
|
+
if reason:
|
|
118
|
+
skips.append(Skip(manifest.path, name, reason))
|
|
119
|
+
continue
|
|
120
|
+
new = style + str(target)
|
|
121
|
+
manifest.doc[section][name] = new
|
|
122
|
+
changes.append(Change(manifest.path, name, pin, new))
|
|
123
|
+
changed = True
|
|
124
|
+
if changed:
|
|
125
|
+
pending.append((manifest.path, _render(manifest)))
|
|
126
|
+
return changes, skips, pending
|