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.
Files changed (198) hide show
  1. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/PKG-INFO +4 -4
  2. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/README.md +3 -3
  3. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/pyproject.toml +1 -1
  4. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/commands/config.py +11 -2
  5. quadkit_cli-0.0.43/src/quadkit/cli/commands/db_commands_data.py +327 -0
  6. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/commands/new.py +2 -2
  7. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/contributors/core.py +1 -1
  8. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/generators/provider.py +2 -2
  9. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/layout.py +55 -4
  10. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/presets.py +2 -2
  11. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/template.py +5 -3
  12. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/scaffold/module_files.py +18 -1
  13. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/scaffold/specs.py +6 -4
  14. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/commands/test_config.py +52 -9
  15. quadkit_cli-0.0.43/tests/unit/commands/test_events_cmd.py +58 -0
  16. quadkit_cli-0.0.43/tests/unit/test_cli_contribution_system__testclicontributorsubprovider.py +478 -0
  17. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_db.py +94 -0
  18. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_gen_provider.py +2 -2
  19. quadkit_cli-0.0.43/tests/unit/test_generator_option_help.py +65 -0
  20. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_generator_registry.py +1 -1
  21. quadkit_cli-0.0.43/tests/unit/test_health_checks.py +312 -0
  22. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_layout.py +2 -0
  23. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_project_layout.py +1 -1
  24. quadkit_cli-0.0.43/tests/unit/test_scaffold_and_validators.py +314 -0
  25. quadkit_cli-0.0.43/tests/unit/test_storage_driver_generator.py +46 -0
  26. quadkit_cli-0.0.41/src/quadkit/cli/commands/db_commands_data.py +0 -215
  27. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/.gitignore +0 -0
  28. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/LICENSE +0 -0
  29. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/NOTICE +0 -0
  30. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/example.yaml +0 -0
  31. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/__init__.py +0 -0
  32. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/assembly/__init__.py +0 -0
  33. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/assembly/assembler.py +0 -0
  34. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/commands/__init__.py +0 -0
  35. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/commands/add.py +0 -0
  36. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/commands/config_utils.py +0 -0
  37. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/commands/contrib.py +0 -0
  38. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/commands/db.py +0 -0
  39. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/commands/db_bootstrap.py +0 -0
  40. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/commands/db_commands_migrations.py +0 -0
  41. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/commands/db_commands_ops.py +0 -0
  42. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/commands/dev.py +0 -0
  43. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/commands/events.py +0 -0
  44. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/commands/gen.py +0 -0
  45. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/commands/init.py +0 -0
  46. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/commands/inspect.py +0 -0
  47. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/commands/meta.py +0 -0
  48. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/commands/project.py +0 -0
  49. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/commands/py.typed +0 -0
  50. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/commands/run.py +0 -0
  51. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/commands/shell.py +0 -0
  52. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/commands/system.py +0 -0
  53. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/config.py +0 -0
  54. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/constants.py +0 -0
  55. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/contributors/__init__.py +0 -0
  56. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/contributors/base.py +0 -0
  57. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/contributors/discovery.py +0 -0
  58. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/contributors/registry.py +0 -0
  59. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/contributors/runtime.py +0 -0
  60. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/decorators.py +0 -0
  61. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/di/__init__.py +0 -0
  62. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/di/provider.py +0 -0
  63. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/di/sub_providers/__init__.py +0 -0
  64. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/di/sub_providers/contributor.py +0 -0
  65. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/events.py +0 -0
  66. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/exceptions.py +0 -0
  67. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/generators/__init__.py +0 -0
  68. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/generators/base.py +0 -0
  69. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/generators/field_parser.py +0 -0
  70. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/generators/test.py +0 -0
  71. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/hooks.py +0 -0
  72. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/lib/__init__.py +0 -0
  73. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/lib/config_loader.py +0 -0
  74. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/lib/entry_point.py +0 -0
  75. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/lib/naming.py +0 -0
  76. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/lib/templates.py +0 -0
  77. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/module.py +0 -0
  78. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/output/__init__.py +0 -0
  79. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/output/manager.py +0 -0
  80. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/package_names.py +0 -0
  81. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/protocols.py +0 -0
  82. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/py.typed +0 -0
  83. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/__init__.py +0 -0
  84. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/_lazy_exports.py +0 -0
  85. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/backend_base.py +0 -0
  86. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/command.py +0 -0
  87. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/completion.py +0 -0
  88. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/config.py +0 -0
  89. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/database.py +0 -0
  90. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/database_backends.py +0 -0
  91. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/environment.py +0 -0
  92. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/formatter.py +0 -0
  93. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/generator.py +0 -0
  94. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/health.py +0 -0
  95. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/health_checks/__init__.py +0 -0
  96. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/health_checks/base.py +0 -0
  97. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/health_checks/deps.py +0 -0
  98. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/health_checks/project.py +0 -0
  99. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/health_checks/providers.py +0 -0
  100. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/health_checks/python_env.py +0 -0
  101. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/health_checks/tools.py +0 -0
  102. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/health_checks/vcs_docker.py +0 -0
  103. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/hook.py +0 -0
  104. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/inspect.py +0 -0
  105. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/migration.py +0 -0
  106. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/provider.py +0 -0
  107. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/provider_catalog.py +0 -0
  108. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/provider_registry.py +0 -0
  109. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/secrets.py +0 -0
  110. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/serializer.py +0 -0
  111. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/server.py +0 -0
  112. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/task.py +0 -0
  113. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/telemetry.py +0 -0
  114. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/version.py +0 -0
  115. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/registry/watcher.py +0 -0
  116. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/runtime/__init__.py +0 -0
  117. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/runtime/context.py +0 -0
  118. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/runtime/error_handler.py +0 -0
  119. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/runtime/main.py +0 -0
  120. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/scaffold/__init__.py +0 -0
  121. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/scaffold/app_files.py +0 -0
  122. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/scaffold/render.py +0 -0
  123. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/scaffold/support_files.py +0 -0
  124. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/scaffold_package.py +0 -0
  125. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/templates/provider.py.jinja2 +0 -0
  126. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/templates/py.typed +0 -0
  127. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/templates/test_unit.py.jinja2 +0 -0
  128. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/cli/types.py +0 -0
  129. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/src/quadkit/py.typed +0 -0
  130. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/conftest.py +0 -0
  131. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/integration/__init__.py +0 -0
  132. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/integration/test_cli_lifecycle.py +0 -0
  133. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/test_package_structure_p1.py +0 -0
  134. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/__init__.py +0 -0
  135. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/_test_cli_contribution_system_support.py +0 -0
  136. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/commands/__init__.py +0 -0
  137. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/commands/test_config_happy_paths.py +0 -0
  138. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/commands/test_contrib.py +0 -0
  139. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/commands/test_inspect.py +0 -0
  140. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/commands/test_meta.py +0 -0
  141. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/commands/test_run.py +0 -0
  142. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/commands/test_shell.py +0 -0
  143. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/commands/test_system.py +0 -0
  144. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/di/test_config_alignment.py +0 -0
  145. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/lib/test_templates.py +0 -0
  146. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/registry/__init__.py +0 -0
  147. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/registry/test_completion.py +0 -0
  148. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/registry/test_database.py +0 -0
  149. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/registry/test_environment.py +0 -0
  150. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/registry/test_formatter.py +0 -0
  151. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/registry/test_health.py +0 -0
  152. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/registry/test_hook.py +0 -0
  153. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/registry/test_inspect_registry.py +0 -0
  154. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/registry/test_migration.py +0 -0
  155. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/registry/test_presets.py +0 -0
  156. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/registry/test_secrets.py +0 -0
  157. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/registry/test_serializer.py +0 -0
  158. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/registry/test_task.py +0 -0
  159. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/registry/test_telemetry.py +0 -0
  160. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/registry/test_version.py +0 -0
  161. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/registry/test_watcher.py +0 -0
  162. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_base_contributor.py +0 -0
  163. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_cli_constants.py +0 -0
  164. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_cli_context.py +0 -0
  165. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_cli_contribution_system.py +0 -0
  166. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_cli_errors.py +0 -0
  167. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_cli_exceptions.py +0 -0
  168. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_cli_import_isolation.py +0 -0
  169. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_cli_module.py +0 -0
  170. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_cli_types.py +0 -0
  171. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_config_async.py +0 -0
  172. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_config_loader.py +0 -0
  173. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_config_save.py +0 -0
  174. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_config_validate.py +0 -0
  175. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_contributor_registry.py +0 -0
  176. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_contributor_runtime.py +0 -0
  177. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_core_contributor.py +0 -0
  178. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_decorators.py +0 -0
  179. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_dev_command.py +0 -0
  180. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_error_handler.py +0 -0
  181. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_events.py +0 -0
  182. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_exception_handling.py +0 -0
  183. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_field_parser.py +0 -0
  184. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_gen.py +0 -0
  185. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_gen_controller.py +0 -0
  186. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_generator_base.py +0 -0
  187. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_generators_contract.py +0 -0
  188. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_hooks.py +0 -0
  189. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_init_command.py +0 -0
  190. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_main.py +0 -0
  191. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_new.py +0 -0
  192. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_output.py +0 -0
  193. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_output_json.py +0 -0
  194. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_project.py +0 -0
  195. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_provider_registry.py +0 -0
  196. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_round40_hardening.py +0 -0
  197. {quadkit_cli-0.0.41 → quadkit_cli-0.0.43}/tests/unit/test_run_detection.py +0 -0
  198. {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.41
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.3` in the `0.x` series, released in lockstep with the
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.3`) or a tight range
173
- (`>=0.0.3,<0.1.0`). Full policy:
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.3` in the `0.x` series, released in lockstep with the
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.3`) or a tight range
117
- (`>=0.0.3,<0.1.0`). Full policy:
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.41"
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 (ConfigNotFoundError, FileNotFoundError):
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 (ConfigNotFoundError, FileNotFoundError):
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
- "web-api",
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="web-api",
61
+ default="api",
62
62
  type=click.Choice(template_names()),
63
63
  )
64
64
 
@@ -13,7 +13,7 @@ _GENERATOR_SPECS: tuple[tuple[str, str, str, str], ...] = (
13
13
  "provider",
14
14
  "Generate provider",
15
15
  "quadkit.cli.generators.provider:ProviderGenerator",
16
- "src/providers",
16
+ "src/di",
17
17
  ),
18
18
  (
19
19
  "test",
@@ -20,9 +20,9 @@ class ProviderGenerator(GeneratorBase):
20
20
 
21
21
  name = "provider"
22
22
  description = "Generate provider"
23
- default_output_dir = "src/providers"
23
+ default_output_dir = "src/di"
24
24
 
25
- def __init__(self, output_dir: str | Path = "src/providers") -> None:
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
- ComponentDir("audit", "audit", True, ("audited",)),
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
- ComponentDir("guards", "guards", False, ("auth_guard", "guard")),
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
- ComponentDir("providers", "providers", True, ("provider",)),
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
- ComponentDir("search", "search", True, ("search_index",)),
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 with database, auth, cache, and messaging"
102
+ description = "Complete web application: auth, cache, storage, events, tasks, HTTP, monitoring"
103
103
  packages = packages_for(
104
- "web", "sql", "auth", "admin", "cache", "tasks", "monitoring"
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 with auth, admin, and more"
113
- packages = packages_for("web", "sql", "auth", "admin", "cache", "tasks", "monitor")
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", "admin", "cache", "tasks")),
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
- if component.shared:
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 with auth, admin, feature flags, cache, tasks, and SQL",
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", "admin", "features", "cache", "tasks"}),
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 "