quadkit-cli 0.0.41__tar.gz → 0.0.43__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.
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/PKG-INFO +4 -4
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/README.md +3 -3
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/pyproject.toml +1 -1
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/commands/config.py +11 -2
- quadkit_cli-0.0.43/src/quadkit/cli/commands/db_commands_data.py +327 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/commands/new.py +2 -2
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/contributors/core.py +1 -1
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/generators/provider.py +2 -2
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/layout.py +55 -4
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/presets.py +2 -2
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/template.py +5 -3
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/scaffold/module_files.py +18 -1
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/scaffold/specs.py +6 -4
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/commands/test_config.py +52 -9
- quadkit_cli-0.0.43/tests/unit/commands/test_events_cmd.py +58 -0
- quadkit_cli-0.0.43/tests/unit/test_cli_contribution_system__testclicontributorsubprovider.py +478 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_db.py +94 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_gen_provider.py +2 -2
- quadkit_cli-0.0.43/tests/unit/test_generator_option_help.py +65 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_generator_registry.py +1 -1
- quadkit_cli-0.0.43/tests/unit/test_health_checks.py +312 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_layout.py +2 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_project_layout.py +1 -1
- quadkit_cli-0.0.43/tests/unit/test_scaffold_and_validators.py +314 -0
- quadkit_cli-0.0.43/tests/unit/test_storage_driver_generator.py +46 -0
- quadkit_cli-0.0.41/src/quadkit/cli/commands/db_commands_data.py +0 -215
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/.gitignore +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/LICENSE +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/NOTICE +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/example.yaml +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/__init__.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/assembly/__init__.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/assembly/assembler.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/commands/__init__.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/commands/add.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/commands/config_utils.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/commands/contrib.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/commands/db.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/commands/db_bootstrap.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/commands/db_commands_migrations.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/commands/db_commands_ops.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/commands/dev.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/commands/events.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/commands/gen.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/commands/init.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/commands/inspect.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/commands/meta.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/commands/project.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/commands/py.typed +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/commands/run.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/commands/shell.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/commands/system.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/config.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/constants.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/contributors/__init__.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/contributors/base.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/contributors/discovery.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/contributors/registry.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/contributors/runtime.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/decorators.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/di/__init__.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/di/provider.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/di/sub_providers/__init__.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/di/sub_providers/contributor.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/events.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/exceptions.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/generators/__init__.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/generators/base.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/generators/field_parser.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/generators/test.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/hooks.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/lib/__init__.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/lib/config_loader.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/lib/entry_point.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/lib/naming.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/lib/templates.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/module.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/output/__init__.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/output/manager.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/package_names.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/protocols.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/py.typed +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/__init__.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/_lazy_exports.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/backend_base.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/command.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/completion.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/config.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/database.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/database_backends.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/environment.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/formatter.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/generator.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/health.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/health_checks/__init__.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/health_checks/base.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/health_checks/deps.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/health_checks/project.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/health_checks/providers.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/health_checks/python_env.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/health_checks/tools.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/health_checks/vcs_docker.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/hook.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/inspect.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/migration.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/provider.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/provider_catalog.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/provider_registry.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/secrets.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/serializer.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/server.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/task.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/telemetry.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/version.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/watcher.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/runtime/__init__.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/runtime/context.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/runtime/error_handler.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/runtime/main.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/scaffold/__init__.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/scaffold/app_files.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/scaffold/render.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/scaffold/support_files.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/scaffold_package.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/templates/provider.py.jinja2 +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/templates/py.typed +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/templates/test_unit.py.jinja2 +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/types.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/py.typed +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/conftest.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/integration/__init__.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/integration/test_cli_lifecycle.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/test_package_structure_p1.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/__init__.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/_test_cli_contribution_system_support.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/commands/__init__.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/commands/test_config_happy_paths.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/commands/test_contrib.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/commands/test_inspect.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/commands/test_meta.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/commands/test_run.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/commands/test_shell.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/commands/test_system.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/di/test_config_alignment.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/lib/test_templates.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/registry/__init__.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/registry/test_completion.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/registry/test_database.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/registry/test_environment.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/registry/test_formatter.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/registry/test_health.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/registry/test_hook.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/registry/test_inspect_registry.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/registry/test_migration.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/registry/test_presets.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/registry/test_secrets.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/registry/test_serializer.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/registry/test_task.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/registry/test_telemetry.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/registry/test_version.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/registry/test_watcher.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_base_contributor.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_cli_constants.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_cli_context.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_cli_contribution_system.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_cli_errors.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_cli_exceptions.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_cli_import_isolation.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_cli_module.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_cli_types.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_config_async.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_config_loader.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_config_save.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_config_validate.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_contributor_registry.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_contributor_runtime.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_core_contributor.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_decorators.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_dev_command.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_error_handler.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_events.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_exception_handling.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_field_parser.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_gen.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_gen_controller.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_generator_base.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_generators_contract.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_hooks.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_init_command.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_main.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_new.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_output.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_output_json.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_project.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_provider_registry.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_round40_hardening.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_run_detection.py +0 -0
- {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_server_registry.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: quadkit-cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.43
|
|
4
4
|
Summary: Command-line interface for Quadkit Framework - Project scaffolding, code generation, and development tools
|
|
5
5
|
Project-URL: Homepage, https://dbtinoy-.github.io/quadkit/
|
|
6
6
|
Project-URL: Repository, https://github.com/dbtinoy-/quadkit
|
|
@@ -167,10 +167,10 @@ review release notes on upgrade. Report vulnerabilities privately per
|
|
|
167
167
|
|
|
168
168
|
## Stability
|
|
169
169
|
|
|
170
|
-
Version `0.0.
|
|
170
|
+
Version `0.0.43` in the `0.x` series, released in lockstep with the
|
|
171
171
|
other four distributions; APIs may change between minor versions until
|
|
172
|
-
1.0 — pin an exact version (`quadkit-cli==0.0.
|
|
173
|
-
(`>=0.0.
|
|
172
|
+
1.0 — pin an exact version (`quadkit-cli==0.0.43`) or a tight range
|
|
173
|
+
(`>=0.0.43,<0.1.0`). Full policy:
|
|
174
174
|
[stability and compatibility](https://dbtinoy-.github.io/quadkit/reference/stability/).
|
|
175
175
|
|
|
176
176
|
## Links
|
|
@@ -111,10 +111,10 @@ review release notes on upgrade. Report vulnerabilities privately per
|
|
|
111
111
|
|
|
112
112
|
## Stability
|
|
113
113
|
|
|
114
|
-
Version `0.0.
|
|
114
|
+
Version `0.0.43` in the `0.x` series, released in lockstep with the
|
|
115
115
|
other four distributions; APIs may change between minor versions until
|
|
116
|
-
1.0 — pin an exact version (`quadkit-cli==0.0.
|
|
117
|
-
(`>=0.0.
|
|
116
|
+
1.0 — pin an exact version (`quadkit-cli==0.0.43`) or a tight range
|
|
117
|
+
(`>=0.0.43,<0.1.0`). Full policy:
|
|
118
118
|
[stability and compatibility](https://dbtinoy-.github.io/quadkit/reference/stability/).
|
|
119
119
|
|
|
120
120
|
## Links
|
|
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
|
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "quadkit-cli"
|
|
9
|
-
version = "0.0.
|
|
9
|
+
version = "0.0.43"
|
|
10
10
|
description = "Command-line interface for Quadkit Framework - Project scaffolding, code generation, and development tools"
|
|
11
11
|
readme = "README.md"
|
|
12
12
|
requires-python = ">=3.11"
|
|
@@ -78,6 +78,9 @@ def show(
|
|
|
78
78
|
config_path = find_config()
|
|
79
79
|
config_data = await load_config_yaml_async(config_path)
|
|
80
80
|
except (ConfigNotFoundError, FileNotFoundError):
|
|
81
|
+
# Both are reachable here: find_config() raises the former, and
|
|
82
|
+
# the try also wraps the read, which can lose the file to a
|
|
83
|
+
# race after the walk.
|
|
81
84
|
out.error(
|
|
82
85
|
"No application.yaml found in current directory or any parent.",
|
|
83
86
|
hint="Run `quadkit init` to create one.",
|
|
@@ -342,7 +345,10 @@ def env_cmd(
|
|
|
342
345
|
async def _run() -> None:
|
|
343
346
|
try:
|
|
344
347
|
config_path = find_config()
|
|
345
|
-
except
|
|
348
|
+
except ConfigNotFoundError:
|
|
349
|
+
# find_config() raises ConfigNotFoundError (a CliError), never
|
|
350
|
+
# FileNotFoundError: it walks candidates with ``.is_file()``. The
|
|
351
|
+
# old arm therefore never ran, and this message never printed.
|
|
346
352
|
out.error("No application.yaml found")
|
|
347
353
|
raise typer.Exit(1) from None
|
|
348
354
|
|
|
@@ -458,7 +464,10 @@ def env_example(
|
|
|
458
464
|
async def _run() -> None:
|
|
459
465
|
try:
|
|
460
466
|
config_path = find_config()
|
|
461
|
-
except
|
|
467
|
+
except ConfigNotFoundError:
|
|
468
|
+
# find_config() raises ConfigNotFoundError (a CliError), never
|
|
469
|
+
# FileNotFoundError: it walks candidates with ``.is_file()``. The
|
|
470
|
+
# old arm therefore never ran, and this message never printed.
|
|
462
471
|
out.error("No application.yaml found")
|
|
463
472
|
raise typer.Exit(1) from None
|
|
464
473
|
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"""Seed, reset, and schema-setup commands for the db command group."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import importlib
|
|
7
|
+
import importlib.util
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import sys
|
|
10
|
+
from typing import Annotated
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
|
|
14
|
+
from quadkit.cli.commands import db_bootstrap
|
|
15
|
+
from quadkit.cli.layout import read_project_layout, resolve_output_dir
|
|
16
|
+
from quadkit.cli.output import OutputManager
|
|
17
|
+
from quadkit.cli.runtime import handle_errors
|
|
18
|
+
from quadkit.contracts.cli.contributions import (
|
|
19
|
+
SchemaSetupContribution,
|
|
20
|
+
SchemaSetupOutcome,
|
|
21
|
+
SchemaSetupResult,
|
|
22
|
+
)
|
|
23
|
+
from quadkit.contracts.data.sql.database import DatabaseProviderProtocol
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _seeder_sources() -> list[tuple[Path, str | None]]:
|
|
27
|
+
"""Every directory this project keeps seeders in, and how to import each.
|
|
28
|
+
|
|
29
|
+
Resolution order:
|
|
30
|
+
|
|
31
|
+
1. the canonical ``seeders`` component — where ``quadkit gen seeder``
|
|
32
|
+
writes and where a generated application keeps the seeders its
|
|
33
|
+
persistence provider boots;
|
|
34
|
+
2. **one per bounded context** — ``src/<app>/modules/<slug>/seeders``.
|
|
35
|
+
A seeder seeds one entity, so it follows that entity into its module
|
|
36
|
+
(OQ-L1). Before that, every seeder in a modular project shared one
|
|
37
|
+
``shared/seeders`` directory and a single lookup found them all;
|
|
38
|
+
scanning the modules is what keeps ``db seed`` complete now that they
|
|
39
|
+
are spread out. Missing this is how the command would silently seed
|
|
40
|
+
nothing in exactly the projects that need it most;
|
|
41
|
+
3. the project-root ``seeds/`` directory — what a project generated
|
|
42
|
+
before the ``seeders`` row landed has, and what a hand-written
|
|
43
|
+
collection looks like. Those files sit outside any package, so they
|
|
44
|
+
stay path-loaded, exactly as this command always loaded them.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
``(directory, module_prefix)`` pairs: the directory to read, and the
|
|
48
|
+
dotted package its modules belong to when it lives inside ``src/`` —
|
|
49
|
+
``None`` when it does not, which is the signal to load by path.
|
|
50
|
+
"""
|
|
51
|
+
layout = read_project_layout()
|
|
52
|
+
found: list[tuple[Path, str | None]] = []
|
|
53
|
+
|
|
54
|
+
canonical = Path(resolve_output_dir("src/seeders", app_package=layout.app_package))
|
|
55
|
+
if canonical.is_dir():
|
|
56
|
+
found.append((canonical, _module_prefix(canonical)))
|
|
57
|
+
|
|
58
|
+
# The app package's `modules/` tree, if this project has bounded contexts.
|
|
59
|
+
app_root = canonical.parent if canonical.name == "seeders" else None
|
|
60
|
+
modules_root = (app_root / "modules") if app_root else None
|
|
61
|
+
if modules_root and modules_root.is_dir():
|
|
62
|
+
for module_dir in sorted(modules_root.iterdir()):
|
|
63
|
+
seeders = module_dir / "seeders"
|
|
64
|
+
if seeders.is_dir():
|
|
65
|
+
found.append((seeders, _module_prefix(seeders)))
|
|
66
|
+
|
|
67
|
+
if found:
|
|
68
|
+
return found
|
|
69
|
+
# The legacy root directory is returned whether or not it exists: the
|
|
70
|
+
# callers report "no seeds directory found" from it, so handing back an
|
|
71
|
+
# absent path is how that message still gets made.
|
|
72
|
+
return [(Path("seeds"), None)]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _seeder_source() -> tuple[Path, str | None]:
|
|
76
|
+
"""The first seeder directory, for callers that only report existence."""
|
|
77
|
+
return _seeder_sources()[0]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _module_prefix(directory: Path) -> str | None:
|
|
81
|
+
"""Dotted package name for *directory*, when it is inside ``src/``."""
|
|
82
|
+
parts = directory.parts
|
|
83
|
+
if "src" not in parts:
|
|
84
|
+
return None
|
|
85
|
+
after_src = parts[parts.index("src") + 1 :]
|
|
86
|
+
return ".".join(after_src) if after_src else None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _make_importable(directory: Path) -> None:
|
|
90
|
+
"""Put the package root of *directory* on ``sys.path``.
|
|
91
|
+
|
|
92
|
+
Seeders import the application's own packages (``from my_app.shared...``),
|
|
93
|
+
and the CLI is not run with the project's ``src`` on the path the way the
|
|
94
|
+
project's own entry point is.
|
|
95
|
+
"""
|
|
96
|
+
parts = directory.parts
|
|
97
|
+
if "src" not in parts:
|
|
98
|
+
return
|
|
99
|
+
root = Path(*parts[: parts.index("src") + 1])
|
|
100
|
+
resolved = str(root.resolve())
|
|
101
|
+
if resolved not in sys.path:
|
|
102
|
+
sys.path.insert(0, resolved)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _seed_files(directory: Path) -> list[Path]:
|
|
106
|
+
"""Seeders to run, in filename order.
|
|
107
|
+
|
|
108
|
+
``__init__.py`` is skipped: seeders below ``src/`` live in a package, and
|
|
109
|
+
the package marker is not a seeder -- running it would report "No 'run'
|
|
110
|
+
function found" for a file the user never wrote.
|
|
111
|
+
"""
|
|
112
|
+
return [
|
|
113
|
+
path for path in sorted(directory.glob("*.py")) if path.name != "__init__.py"
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
async def _run_seeder(
|
|
118
|
+
out: OutputManager,
|
|
119
|
+
provider: DatabaseProviderProtocol,
|
|
120
|
+
seed_file: Path,
|
|
121
|
+
module_prefix: str | None,
|
|
122
|
+
) -> None:
|
|
123
|
+
"""Execute one seeder file.
|
|
124
|
+
|
|
125
|
+
A seeder inside the application package is *imported*: it is an ordinary
|
|
126
|
+
module there, and importing is what the generated application itself does
|
|
127
|
+
when it boots its seeders. Anything else is loaded from its path, which is
|
|
128
|
+
the only thing that works for a file outside ``src/``.
|
|
129
|
+
"""
|
|
130
|
+
out.info(f"Running seeder: {seed_file}")
|
|
131
|
+
|
|
132
|
+
if module_prefix is not None:
|
|
133
|
+
_make_importable(seed_file)
|
|
134
|
+
module = importlib.import_module(f"{module_prefix}.{seed_file.stem}")
|
|
135
|
+
else:
|
|
136
|
+
spec = importlib.util.spec_from_file_location("seed_module", seed_file)
|
|
137
|
+
if spec is None or spec.loader is None:
|
|
138
|
+
out.warning(f"Could not load seeder: {seed_file}")
|
|
139
|
+
return
|
|
140
|
+
module = importlib.util.module_from_spec(spec)
|
|
141
|
+
spec.loader.exec_module(module)
|
|
142
|
+
|
|
143
|
+
if not hasattr(module, "run"):
|
|
144
|
+
out.warning(f"No 'run' function found in {seed_file}")
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
if asyncio.iscoroutinefunction(module.run):
|
|
148
|
+
await module.run(provider)
|
|
149
|
+
else:
|
|
150
|
+
module.run(provider)
|
|
151
|
+
out.success(f"Seeder {seed_file} completed.")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@handle_errors
|
|
155
|
+
def seed(
|
|
156
|
+
file: str | None = typer.Argument(None, help="Specific seed file to run"),
|
|
157
|
+
env: str | None = typer.Option("--env", "-e", help="Environment name"),
|
|
158
|
+
) -> None:
|
|
159
|
+
"""Run database seeders."""
|
|
160
|
+
out = OutputManager()
|
|
161
|
+
|
|
162
|
+
async def _run() -> None:
|
|
163
|
+
manager = await db_bootstrap.get_migration_manager()
|
|
164
|
+
await manager.provider.boot()
|
|
165
|
+
try:
|
|
166
|
+
sources = _seeder_sources()
|
|
167
|
+
seeds_dir = sources[0][0]
|
|
168
|
+
if not any(d.exists() for d, _ in sources) and not file:
|
|
169
|
+
out.warning("No seeds directory found.")
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
# Annotated: the first branch appends ``(path, None)`` and the
|
|
173
|
+
# second rebuilds with a module prefix, so inference from the
|
|
174
|
+
# empty list would pin the tuple's second element to ``None``.
|
|
175
|
+
files_to_run: list[tuple[Path, str | None]] = []
|
|
176
|
+
if file:
|
|
177
|
+
# An explicit file is run as given, by path: the user named a
|
|
178
|
+
# file, not a module.
|
|
179
|
+
files_to_run.append((Path(file), None))
|
|
180
|
+
else:
|
|
181
|
+
files_to_run = [
|
|
182
|
+
(seed_file, prefix)
|
|
183
|
+
for directory, prefix in _seeder_sources()
|
|
184
|
+
for seed_file in _seed_files(directory)
|
|
185
|
+
]
|
|
186
|
+
|
|
187
|
+
for seed_file, prefix in files_to_run:
|
|
188
|
+
await _run_seeder(out, manager.provider, seed_file, prefix)
|
|
189
|
+
|
|
190
|
+
except Exception as e: # noqa: BLE001 — any seeder failure is a CLI error
|
|
191
|
+
out.error(f"Seeder failed: {e}")
|
|
192
|
+
raise typer.Exit(1) from None
|
|
193
|
+
finally:
|
|
194
|
+
await manager.provider.shutdown()
|
|
195
|
+
|
|
196
|
+
asyncio.run(_run())
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@handle_errors
|
|
200
|
+
def reset(
|
|
201
|
+
seed: bool = typer.Option(False, "--seed", help="Run seeders after reset"),
|
|
202
|
+
force: bool = typer.Option(
|
|
203
|
+
False,
|
|
204
|
+
"--force",
|
|
205
|
+
"-f",
|
|
206
|
+
help="Force reset without confirmation",
|
|
207
|
+
),
|
|
208
|
+
env: str | None = typer.Option("--env", "-e", help="Environment name"),
|
|
209
|
+
) -> None:
|
|
210
|
+
"""Drop and recreate the database."""
|
|
211
|
+
out = OutputManager()
|
|
212
|
+
if not force:
|
|
213
|
+
confirm = typer.confirm(
|
|
214
|
+
"Are you sure you want to reset the database? This will delete ALL data.",
|
|
215
|
+
)
|
|
216
|
+
if not confirm:
|
|
217
|
+
raise typer.Abort
|
|
218
|
+
|
|
219
|
+
async def _run() -> None:
|
|
220
|
+
manager = await db_bootstrap.get_migration_manager()
|
|
221
|
+
await manager.provider.boot()
|
|
222
|
+
try:
|
|
223
|
+
out.info("Resetting database...")
|
|
224
|
+
|
|
225
|
+
if "sqlite" in str(manager.provider.url):
|
|
226
|
+
res = await manager.provider.execute_query(
|
|
227
|
+
"SELECT name FROM sqlite_master WHERE type='table'",
|
|
228
|
+
)
|
|
229
|
+
tables = [row["name"] for row in res.rows]
|
|
230
|
+
for table in tables:
|
|
231
|
+
if table != "sqlite_sequence":
|
|
232
|
+
out.print(f"[dim]Dropping table: {table}[/dim]")
|
|
233
|
+
await manager.provider.execute(f"DROP TABLE {table}")
|
|
234
|
+
else:
|
|
235
|
+
out.warning(
|
|
236
|
+
"Reset is only fully optimized for SQLite currently.",
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
out.success("Database cleared.")
|
|
240
|
+
|
|
241
|
+
await manager.initialize_migration_table()
|
|
242
|
+
applied = await manager.apply_pending_migrations()
|
|
243
|
+
if applied:
|
|
244
|
+
for v in applied:
|
|
245
|
+
out.success(f"Applied {v}")
|
|
246
|
+
else:
|
|
247
|
+
out.info("No pending migrations.")
|
|
248
|
+
|
|
249
|
+
if seed:
|
|
250
|
+
sources = _seeder_sources()
|
|
251
|
+
if any(d.exists() for d, _ in sources):
|
|
252
|
+
for directory, prefix in sources:
|
|
253
|
+
for seed_file in _seed_files(directory):
|
|
254
|
+
await _run_seeder(out, manager.provider, seed_file, prefix)
|
|
255
|
+
else:
|
|
256
|
+
out.warning("No seeds directory found, skipping seed step.")
|
|
257
|
+
|
|
258
|
+
except Exception as e: # noqa: BLE001 — any reset failure is a CLI error
|
|
259
|
+
out.error(f"Reset failed: {e}")
|
|
260
|
+
raise typer.Exit(1) from None
|
|
261
|
+
finally:
|
|
262
|
+
await manager.provider.shutdown()
|
|
263
|
+
|
|
264
|
+
asyncio.run(_run())
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@handle_errors
|
|
268
|
+
def setup(
|
|
269
|
+
package: Annotated[
|
|
270
|
+
str | None,
|
|
271
|
+
typer.Option(
|
|
272
|
+
"--package", help="Only run setup for this package's contributions."
|
|
273
|
+
),
|
|
274
|
+
] = None,
|
|
275
|
+
) -> None:
|
|
276
|
+
"""Run schema setup for all installed packages that need database tables."""
|
|
277
|
+
asyncio.run(_run_setup(package))
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
async def _run_setup(package: str | None) -> None:
|
|
281
|
+
from quadkit.cli.contributors.runtime import ContributorRuntime
|
|
282
|
+
|
|
283
|
+
out = OutputManager()
|
|
284
|
+
runtime = ContributorRuntime.from_entry_points()
|
|
285
|
+
contributions = runtime.schema_setups
|
|
286
|
+
if package:
|
|
287
|
+
contributions = [c for c in contributions if c.name.split(".")[0] == package]
|
|
288
|
+
|
|
289
|
+
if not contributions:
|
|
290
|
+
out.info("Nothing to do — no schema setup contributions discovered.")
|
|
291
|
+
return
|
|
292
|
+
|
|
293
|
+
db, provider = await db_bootstrap._bootstrap_db_provider()
|
|
294
|
+
|
|
295
|
+
failed = False
|
|
296
|
+
try:
|
|
297
|
+
for contribution in contributions:
|
|
298
|
+
outcome = await _run_one_schema_setup(contribution, db)
|
|
299
|
+
if outcome.status == SchemaSetupResult.CREATED:
|
|
300
|
+
out.success(f"{contribution.name}: created")
|
|
301
|
+
elif outcome.status == SchemaSetupResult.ALREADY_PRESENT:
|
|
302
|
+
out.info(f"{contribution.name}: already present")
|
|
303
|
+
else:
|
|
304
|
+
out.error(f"{contribution.name}: failed — {outcome.message}")
|
|
305
|
+
failed = True
|
|
306
|
+
finally:
|
|
307
|
+
await provider.shutdown()
|
|
308
|
+
|
|
309
|
+
if failed:
|
|
310
|
+
raise typer.Exit(1)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
async def _run_one_schema_setup(
|
|
314
|
+
contribution: SchemaSetupContribution,
|
|
315
|
+
db: DatabaseProviderProtocol,
|
|
316
|
+
) -> SchemaSetupOutcome:
|
|
317
|
+
try:
|
|
318
|
+
module_path, _, fn_name = contribution.setup_fn_path.partition(":")
|
|
319
|
+
mod = importlib.import_module(module_path)
|
|
320
|
+
ensure_fn = getattr(mod, fn_name)
|
|
321
|
+
outcome: SchemaSetupOutcome = await ensure_fn(db)
|
|
322
|
+
return outcome
|
|
323
|
+
except Exception as exc:
|
|
324
|
+
return SchemaSetupOutcome(status=SchemaSetupResult.FAILED, message=str(exc))
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
__all__ = ["reset", "seed", "setup"]
|
|
@@ -32,7 +32,7 @@ def _valid_name(value: str, kind: str) -> str:
|
|
|
32
32
|
def main(
|
|
33
33
|
name: str = typer.Argument(..., help="Name of the project"),
|
|
34
34
|
template: str = typer.Option(
|
|
35
|
-
"
|
|
35
|
+
"api",
|
|
36
36
|
"--template",
|
|
37
37
|
"-t",
|
|
38
38
|
help=(
|
|
@@ -58,7 +58,7 @@ def main(
|
|
|
58
58
|
|
|
59
59
|
selected_template = typer.prompt(
|
|
60
60
|
"Select a template",
|
|
61
|
-
default="
|
|
61
|
+
default="api",
|
|
62
62
|
type=click.Choice(template_names()),
|
|
63
63
|
)
|
|
64
64
|
|
|
@@ -20,9 +20,9 @@ class ProviderGenerator(GeneratorBase):
|
|
|
20
20
|
|
|
21
21
|
name = "provider"
|
|
22
22
|
description = "Generate provider"
|
|
23
|
-
default_output_dir = "src/
|
|
23
|
+
default_output_dir = "src/di"
|
|
24
24
|
|
|
25
|
-
def __init__(self, output_dir: str | Path = "src/
|
|
25
|
+
def __init__(self, output_dir: str | Path = "src/di") -> None:
|
|
26
26
|
super().__init__(output_dir=output_dir)
|
|
27
27
|
|
|
28
28
|
def generate(
|
|
@@ -64,17 +64,48 @@ class ComponentDir:
|
|
|
64
64
|
COMPONENTS: tuple[ComponentDir, ...] = (
|
|
65
65
|
ComponentDir("admin/actions", "admin/actions", False, ("admin_action",)),
|
|
66
66
|
ComponentDir("admin/resources", "admin/resources", False, ("admin_resource",)),
|
|
67
|
-
|
|
67
|
+
# Per-entity attachment: an audit log is written for one entity's table,
|
|
68
|
+
# so it belongs wherever that entity does (the rule `validators` states
|
|
69
|
+
# below). Cross-cutting `shared/` put it where every context could see a
|
|
70
|
+
# file only one of them owns.
|
|
71
|
+
ComponentDir("audit", "audit", False, ("audited",)),
|
|
72
|
+
# ``auth``, ``contracts`` and ``validators`` carry no generators on
|
|
73
|
+
# purpose: no registered generator writes there yet (the CLI ships
|
|
74
|
+
# ``provider`` and ``test``; subsystems contribute the rest), and a
|
|
75
|
+
# ``generators`` entry is the map's authorisation for a generator's
|
|
76
|
+
# declared output directory, so it should name a generator that exists.
|
|
77
|
+
# The rows are the map's answer for *placement and scope*, which is what
|
|
78
|
+
# the builder needed to delete its own copy of them
|
|
79
|
+
# (docs/upstream/2026-09-02-component-rows.md).
|
|
80
|
+
# API-key issuance and verification. Cross-cutting by definition: one
|
|
81
|
+
# key store, checked by every bounded context.
|
|
82
|
+
ComponentDir("auth", "auth", True, ()),
|
|
68
83
|
ComponentDir("clients", "clients", False, ("api_client",)),
|
|
69
84
|
ComponentDir("commands", "commands", False, ("command",)),
|
|
70
85
|
ComponentDir("consumers", "consumers", False, ("message_consumer",)),
|
|
86
|
+
# Request/response schemas shared across controllers. They are the
|
|
87
|
+
# interface between a context and its callers, so they cannot be
|
|
88
|
+
# module-local without every consumer reaching across a boundary.
|
|
89
|
+
ComponentDir("contracts", "contracts", True, ()),
|
|
71
90
|
ComponentDir("controllers", "controllers", False, ("controller",)),
|
|
91
|
+
# Cross-cutting DI providers. Every quadkit subsystem keeps its own
|
|
92
|
+
# provider at ``<subsystem>/di/provider.py`` (30 of them, no
|
|
93
|
+
# exceptions), so a generated provider reads like the framework it
|
|
94
|
+
# is built on. The ``providers`` row below is kept for projects
|
|
95
|
+
# that already generated into it.
|
|
96
|
+
ComponentDir("di", "di", True, ("provider",)),
|
|
72
97
|
ComponentDir("domains", "domains", False, ("domain",)),
|
|
73
98
|
ComponentDir("errors", "errors", True, ("error",)),
|
|
74
99
|
ComponentDir("events", "events", False, ("event",)),
|
|
75
100
|
ComponentDir("features", "features", True, ("feature_flag",)),
|
|
76
101
|
ComponentDir("filters", "filters", True, ("exception_filter", "filter")),
|
|
77
|
-
|
|
102
|
+
# Cross-cutting, like ``filters`` above: ``@require_auth()`` decorates
|
|
103
|
+
# controllers in every bounded context, and the palette already scopes
|
|
104
|
+
# ``auth``/``role`` as shared kinds. While this row said module-local,
|
|
105
|
+
# the two answers met at generation time and a modular app containing
|
|
106
|
+
# an Auth Guard could not be generated at all (playground TASK-6 /
|
|
107
|
+
# MODULAR-3).
|
|
108
|
+
ComponentDir("guards", "guards", True, ("auth_guard", "guard")),
|
|
78
109
|
ComponentDir("handlers", "handlers", False, ("event_handler",)),
|
|
79
110
|
ComponentDir("health", "health", True, ("health",)),
|
|
80
111
|
ComponentDir("interceptors", "interceptors", True, ("interceptor",)),
|
|
@@ -85,7 +116,9 @@ COMPONENTS: tuple[ComponentDir, ...] = (
|
|
|
85
116
|
ComponentDir("pipelines", "pipelines", False, ("pipeline",)),
|
|
86
117
|
ComponentDir("policies", "policies", False, ("auth_policy",)),
|
|
87
118
|
ComponentDir("projections", "projections", False, ("projection",)),
|
|
88
|
-
|
|
119
|
+
# Superseded by the ``di`` row above; kept (with no generators) so a
|
|
120
|
+
# project that already has ``shared/providers/`` keeps resolving.
|
|
121
|
+
ComponentDir("providers", "providers", True, ()),
|
|
89
122
|
ComponentDir("queries", "queries", False, ("query",)),
|
|
90
123
|
ComponentDir(
|
|
91
124
|
"repositories",
|
|
@@ -96,11 +129,25 @@ COMPONENTS: tuple[ComponentDir, ...] = (
|
|
|
96
129
|
ComponentDir("sagas", "sagas", False, ("saga", "saga_step")),
|
|
97
130
|
ComponentDir("schema", "schema", True, ("graphql",)),
|
|
98
131
|
ComponentDir("schema/dataloaders", "schema/dataloaders", True, ("dataloader",)),
|
|
99
|
-
|
|
132
|
+
# Per-entity attachment: a search index indexes one entity's fields.
|
|
133
|
+
ComponentDir("search", "search", False, ("search_index",)),
|
|
134
|
+
# Generated seeders live inside the application package so they are
|
|
135
|
+
# importable: a project boots them from its persistence provider, and
|
|
136
|
+
# `quadkit db seed` imports the same modules. ``seeds`` (project root)
|
|
137
|
+
# stays resolvable in ROOT_DIRS below, because that is where every
|
|
138
|
+
# project generated before 2026-09-23 kept them and where the command
|
|
139
|
+
# still looks second.
|
|
140
|
+
# Per-entity attachment: a seeder seeds exactly one entity, so it follows
|
|
141
|
+
# that entity into its module. `quadkit db seed` discovers seeders through
|
|
142
|
+
# the layout rather than a fixed path, so it finds them either way.
|
|
143
|
+
ComponentDir("seeders", "seeders", False, ("seeder",)),
|
|
100
144
|
ComponentDir("services", "services", False, ("service",)),
|
|
101
145
|
ComponentDir("storage/backends", "storage/backends", True, ("storage_driver",)),
|
|
102
146
|
ComponentDir("tasks", "tasks", False, ("task",)),
|
|
103
147
|
ComponentDir("tenancy", "tenancy", True, ("tenant_resolver",)),
|
|
148
|
+
# Per-entity rules. They know an entity's fields, so they belong
|
|
149
|
+
# wherever that entity belongs: module-local.
|
|
150
|
+
ComponentDir("validators", "validators", False, ()),
|
|
104
151
|
ComponentDir(
|
|
105
152
|
"vector/collections", "vector/collections", True, ("vector_collection",)
|
|
106
153
|
),
|
|
@@ -112,6 +159,10 @@ COMPONENTS: tuple[ComponentDir, ...] = (
|
|
|
112
159
|
# Root-level output directories of the generator SDK (no ``src/`` prefix).
|
|
113
160
|
ROOT_DIRS: tuple[tuple[str, tuple[str, ...]], ...] = (
|
|
114
161
|
("migrations/versions", ("migration",)),
|
|
162
|
+
# ``seeds`` is legacy-but-supported: the ``seeders`` component above is
|
|
163
|
+
# where the generator writes now, while this row keeps ``quadkit gen
|
|
164
|
+
# seeder --output seeds`` and projects generated before 2026-09-23
|
|
165
|
+
# resolvable. ``quadkit db seed`` looks here second.
|
|
115
166
|
("seeds", ("seeder",)),
|
|
116
167
|
("tests/unit", ("test",)),
|
|
117
168
|
)
|
|
@@ -99,9 +99,9 @@ class FullStackPreset(PresetDefinition):
|
|
|
99
99
|
"""Full-stack application preset."""
|
|
100
100
|
|
|
101
101
|
name = "fullstack"
|
|
102
|
-
description = "Complete web application
|
|
102
|
+
description = "Complete web application: auth, cache, storage, events, tasks, HTTP, monitoring"
|
|
103
103
|
packages = packages_for(
|
|
104
|
-
"web", "sql", "auth", "
|
|
104
|
+
"web", "sql", "auth", "cache", "storage", "events", "tasks", "http", "monitor"
|
|
105
105
|
)
|
|
106
106
|
config = {
|
|
107
107
|
"web": {"host": "0.0.0.0", "port": 8000}, # noqa: S104 — preset template default
|
|
@@ -109,8 +109,10 @@ class FullStackTemplate(ProjectTemplate):
|
|
|
109
109
|
"""Full-stack application template."""
|
|
110
110
|
|
|
111
111
|
name = "fullstack"
|
|
112
|
-
description = "Full application
|
|
113
|
-
packages = packages_for(
|
|
112
|
+
description = "Full application: auth, cache, storage, events, tasks, HTTP, monitoring"
|
|
113
|
+
packages = packages_for(
|
|
114
|
+
"web", "sql", "auth", "cache", "storage", "events", "tasks", "http", "monitor"
|
|
115
|
+
)
|
|
114
116
|
|
|
115
117
|
def get_config(self) -> TemplateConfig:
|
|
116
118
|
return TemplateConfig(
|
|
@@ -154,7 +156,7 @@ class TemplateRegistry:
|
|
|
154
156
|
optional = (
|
|
155
157
|
(GraphQLTemplate, ("sql", "graphql")),
|
|
156
158
|
(WorkerTemplate, ("sql", "tasks", "queue")),
|
|
157
|
-
(FullStackTemplate, ("web", "sql", "auth", "
|
|
159
|
+
(FullStackTemplate, ("web", "sql", "auth", "cache", "storage", "events", "tasks", "http", "monitor")),
|
|
158
160
|
)
|
|
159
161
|
for entry, features in optional:
|
|
160
162
|
if all(provider_available(feature) for feature in features):
|
|
@@ -17,7 +17,17 @@ from quadkit.cli.scaffold.specs import ProjectTemplateSpec
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def _package_line(package: str, generators: tuple[str, ...]) -> str:
|
|
20
|
+
"""Docstring for a pre-created package.
|
|
21
|
+
|
|
22
|
+
A shared row can exist with no generator behind it (``contracts``,
|
|
23
|
+
``auth``): the map answers *where* the package lives, and the code that
|
|
24
|
+
fills it is written by hand or by a tool outside the CLI. Saying
|
|
25
|
+
"Writers: (quadkit gen)" in that case would advertise a command that
|
|
26
|
+
does not exist.
|
|
27
|
+
"""
|
|
20
28
|
names = ", ".join(generators)
|
|
29
|
+
if not names:
|
|
30
|
+
return f'"""{package} package."""\n'
|
|
21
31
|
return f'"""{package} package.\n\nWriters: {names} (quadkit gen)."""\n'
|
|
22
32
|
|
|
23
33
|
|
|
@@ -32,7 +42,14 @@ def _component_files(package_name: str) -> dict[str, str]:
|
|
|
32
42
|
"""
|
|
33
43
|
files: dict[str, str] = {}
|
|
34
44
|
for component in COMPONENTS:
|
|
35
|
-
|
|
45
|
+
# A shared row with no generator behind it is a *resolution* row, not
|
|
46
|
+
# a package the scaffold creates: `providers` exists so a project
|
|
47
|
+
# generated into it before the `di` row landed keeps resolving, and
|
|
48
|
+
# `auth`/`contracts` exist because the map is where a project's
|
|
49
|
+
# cross-cutting packages are declared -- nothing in the CLI writes
|
|
50
|
+
# there, so pre-creating them would put an unexplained empty package
|
|
51
|
+
# in every fresh project and change the scaffold for existing users.
|
|
52
|
+
if component.shared and component.generators:
|
|
36
53
|
path = f"src/{package_name}/shared/{component.modular}"
|
|
37
54
|
files[f"{path}/__init__.py"] = _package_line(
|
|
38
55
|
f"{component.modular} (shared)", component.generators
|
|
@@ -121,18 +121,20 @@ _TEMPLATE_SPECS: dict[str, ProjectTemplateSpec] = {
|
|
|
121
121
|
),
|
|
122
122
|
"full": _spec(
|
|
123
123
|
"full",
|
|
124
|
-
"Full application
|
|
124
|
+
"Full application: auth, cache, storage, events, tasks, HTTP, monitoring, and SQL",
|
|
125
125
|
_dependencies(
|
|
126
126
|
"web",
|
|
127
127
|
"sql",
|
|
128
128
|
"auth",
|
|
129
|
-
"admin",
|
|
130
|
-
"features",
|
|
131
129
|
"cache",
|
|
130
|
+
"storage",
|
|
131
|
+
"events",
|
|
132
132
|
"tasks",
|
|
133
|
+
"http",
|
|
134
|
+
"monitor",
|
|
133
135
|
tools=("uvicorn",),
|
|
134
136
|
),
|
|
135
|
-
frozenset({"web", "sql", "auth", "
|
|
137
|
+
frozenset({"web", "sql", "auth", "cache", "storage", "events", "tasks", "http", "monitor"}),
|
|
136
138
|
(
|
|
137
139
|
(
|
|
138
140
|
"Auth ships with dev secret keys; replace QK_AUTH__SECRET_KEY "
|