csrd-utils 0.3.82__tar.gz → 0.3.86__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 (174) hide show
  1. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/PKG-INFO +3 -1
  2. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/pyproject.toml +3 -1
  3. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/__main__.py +192 -0
  4. csrd_utils-0.3.86/src/csrd_utils/v2/AGENTS.md +349 -0
  5. csrd_utils-0.3.86/src/csrd_utils/v2/__init__.py +1 -0
  6. csrd_utils-0.3.86/src/csrd_utils/v2/compose/__init__.py +93 -0
  7. csrd_utils-0.3.86/src/csrd_utils/v2/compose/git.py +17 -0
  8. csrd_utils-0.3.86/src/csrd_utils/v2/compose/infra.py +257 -0
  9. csrd_utils-0.3.86/src/csrd_utils/v2/compose/loader.py +38 -0
  10. csrd_utils-0.3.86/src/csrd_utils/v2/compose/operations.py +126 -0
  11. csrd_utils-0.3.86/src/csrd_utils/v2/compose/presets.py +58 -0
  12. csrd_utils-0.3.86/src/csrd_utils/v2/compose/renderer.py +279 -0
  13. csrd_utils-0.3.86/src/csrd_utils/v2/compose/scaffolder.py +89 -0
  14. csrd_utils-0.3.86/src/csrd_utils/v2/compose/yaml_editor.py +134 -0
  15. csrd_utils-0.3.86/src/csrd_utils/v2/generate/__init__.py +5 -0
  16. csrd_utils-0.3.86/src/csrd_utils/v2/generate/handlers.py +705 -0
  17. csrd_utils-0.3.86/src/csrd_utils/v2/generate/helpers.py +647 -0
  18. csrd_utils-0.3.86/src/csrd_utils/v2/generate/menu.py +170 -0
  19. csrd_utils-0.3.86/src/csrd_utils/v2/models/__init__.py +38 -0
  20. csrd_utils-0.3.86/src/csrd_utils/v2/models/base.py +10 -0
  21. csrd_utils-0.3.86/src/csrd_utils/v2/models/spec.py +101 -0
  22. csrd_utils-0.3.86/src/csrd_utils/v2/models/types.py +6 -0
  23. csrd_utils-0.3.86/src/csrd_utils/v2/templates/service/cookiecutter.json +7 -0
  24. csrd_utils-0.3.86/src/csrd_utils/v2/templates/service/{{cookiecutter.__service_name_snake}}/Dockerfile +12 -0
  25. csrd_utils-0.3.86/src/csrd_utils/v2/templates/service/{{cookiecutter.__service_name_snake}}/README.md +21 -0
  26. csrd_utils-0.3.86/src/csrd_utils/v2/templates/service/{{cookiecutter.__service_name_snake}}/__init__.py +28 -0
  27. csrd_utils-0.3.86/src/csrd_utils/v2/templates/service/{{cookiecutter.__service_name_snake}}/requirements.txt +4 -0
  28. csrd_utils-0.3.86/src/csrd_utils/v2/templates/service/{{cookiecutter.__service_name_snake}}/settings.py +12 -0
  29. csrd_utils-0.3.86/src/csrd_utils/v2/templates/service/{{cookiecutter.__service_name_snake}}/tests/__init__.py +0 -0
  30. csrd_utils-0.3.86/src/csrd_utils/v2/templates/service/{{cookiecutter.__service_name_snake}}/tests/conftest.py +12 -0
  31. csrd_utils-0.3.86/src/csrd_utils/v2/templates/service/{{cookiecutter.__service_name_snake}}/tests/test_health.py +7 -0
  32. csrd_utils-0.3.86/src/csrd_utils/v2/templates/service/{{cookiecutter.__service_name_snake}}/views/__init__.py +9 -0
  33. csrd_utils-0.3.86/src/csrd_utils/v2/templates/service/{{cookiecutter.__service_name_snake}}/views/health_view.py +8 -0
  34. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/.gitignore +0 -0
  35. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/README.md +0 -0
  36. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/__init__.py +0 -0
  37. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/audit.py +0 -0
  38. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/augmentor.py +0 -0
  39. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/cluster.py +0 -0
  40. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/doctor.py +0 -0
  41. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/features/caching/conftest.fragment.py +0 -0
  42. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/features/caching/docker-compose.fragment.yaml +0 -0
  43. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/features/caching/manifest.yaml +0 -0
  44. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/features/caching/requirements.fragment +0 -0
  45. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/features/caching/settings.fragment.py +0 -0
  46. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/features/caching/src/app/cache.py +0 -0
  47. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/features/caching/tests/test_cache.py +0 -0
  48. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/features/logging/conftest.fragment.py +0 -0
  49. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/features/logging/manifest.yaml +0 -0
  50. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/features/logging/requirements.fragment +0 -0
  51. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/features/logging/settings.fragment.py +0 -0
  52. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/features/logging/src/app/middleware/logging.py +0 -0
  53. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/features/logging/tests/test_logging_middleware.py +0 -0
  54. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/features/metrics/conftest.fragment.py +0 -0
  55. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/features/metrics/docker-compose.fragment.yaml +0 -0
  56. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/features/metrics/manifest.yaml +0 -0
  57. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/features/metrics/requirements.fragment +0 -0
  58. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/features/metrics/settings.fragment.py +0 -0
  59. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/features/metrics/src/app/middleware/metrics.py +0 -0
  60. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/features/metrics/tests/test_metrics_middleware.py +0 -0
  61. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/features/tracing/conftest.fragment.py +0 -0
  62. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/features/tracing/manifest.yaml +0 -0
  63. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/features/tracing/requirements.fragment +0 -0
  64. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/features/tracing/settings.fragment.py +0 -0
  65. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/features/tracing/src/app/middleware/tracing.py +0 -0
  66. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/features/tracing/tests/test_tracing_middleware.py +0 -0
  67. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/features/workers/conftest.fragment.py +0 -0
  68. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/features/workers/docker-compose.fragment.yaml +0 -0
  69. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/features/workers/manifest.yaml +0 -0
  70. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/features/workers/requirements.fragment +0 -0
  71. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/features/workers/settings.fragment.py +0 -0
  72. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/features/workers/src/app/workers/__init__.py +0 -0
  73. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/features/workers/src/app/workers/celery_app.py +0 -0
  74. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/features/workers/src/app/workers/tasks.py +0 -0
  75. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/features/workers/tests/test_workers.py +0 -0
  76. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/fragments.py +0 -0
  77. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/generator.py +0 -0
  78. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/resources.py +0 -0
  79. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/cookiecutter-service/README.md +0 -0
  80. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/cookiecutter-service/cookiecutter.json +0 -0
  81. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/cookiecutter-service/hooks/post_gen_project.py +0 -0
  82. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/.cookiecutterignore +0 -0
  83. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/.dockerignore +0 -0
  84. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/Dockerfile +0 -0
  85. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/README.md +0 -0
  86. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/__init__.py +0 -0
  87. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/cache.py +0 -0
  88. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/delegates/__init__.py +0 -0
  89. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/delegates/upstreams.py +0 -0
  90. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/dependencies/__init__.py +0 -0
  91. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/dependencies/auth_service.py +0 -0
  92. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/dependencies/delegates.py +0 -0
  93. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/dependencies/item_repository.py +0 -0
  94. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/dependencies/worker_broker.py +0 -0
  95. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/docker-compose.yml +0 -0
  96. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/middleware/__init__.py +0 -0
  97. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/middleware/logging.py +0 -0
  98. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/repositories/__init__.py +0 -0
  99. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/repositories/item_repository.py +0 -0
  100. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/requirements.txt +0 -0
  101. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/settings.py +0 -0
  102. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/__init__.py +0 -0
  103. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/acceptance/__init__.py +0 -0
  104. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/acceptance/test_logging_middleware.py +0 -0
  105. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/conftest.py +0 -0
  106. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/test_items.py +0 -0
  107. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/unit/__init__.py +0 -0
  108. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/unit/test_cache.py +0 -0
  109. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/unit/test_database_settings.py +0 -0
  110. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/unit/test_delegates.py +0 -0
  111. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/unit/test_settings_contracts.py +0 -0
  112. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/unit/test_worker_broker.py +0 -0
  113. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/views/__init__.py +0 -0
  114. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/views/unversioned/__init__.py +0 -0
  115. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/views/unversioned/health_view.py +0 -0
  116. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/views/unversioned/items_view.py +0 -0
  117. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/workers/__init__.py +0 -0
  118. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/workers/tasks.py +0 -0
  119. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-app-service/cookiecutter.json +0 -0
  120. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/.dockerignore +0 -0
  121. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/Dockerfile +0 -0
  122. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/README.md +0 -0
  123. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/__init__.py +0 -0
  124. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/delegates/__init__.py +0 -0
  125. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/delegates/auth_delegate.py +0 -0
  126. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/delegates/identity_delegate.py +0 -0
  127. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/dependencies/__init__.py +0 -0
  128. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/dependencies/auth_passthrough.py +0 -0
  129. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/dependencies/auth_service.py +0 -0
  130. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/dependencies/item_repository.py +0 -0
  131. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/models/__init__.py +0 -0
  132. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/models/auth_service.py +0 -0
  133. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/models/items.py +0 -0
  134. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/repositories/__init__.py +0 -0
  135. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/repositories/item_repository.py +0 -0
  136. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/requirements.txt +0 -0
  137. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/settings.py +0 -0
  138. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/views/__init__.py +0 -0
  139. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/views/unversioned/__init__.py +0 -0
  140. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/views/unversioned/auth_service_view.py +0 -0
  141. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/views/unversioned/health_view.py +0 -0
  142. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/views/unversioned/items_view.py +0 -0
  143. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-auth-service/cookiecutter.json +0 -0
  144. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/.dockerignore +0 -0
  145. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/Dockerfile +0 -0
  146. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/README.md +0 -0
  147. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/__init__.py +0 -0
  148. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/dependencies/__init__.py +0 -0
  149. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/models/__init__.py +0 -0
  150. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/models/auth.py +0 -0
  151. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/repositories/__init__.py +0 -0
  152. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/repositories/token_repository.py +0 -0
  153. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/requirements.txt +0 -0
  154. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/settings.py +0 -0
  155. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/views/__init__.py +0 -0
  156. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/views/unversioned/__init__.py +0 -0
  157. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/views/unversioned/auth_view.py +0 -0
  158. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/views/unversioned/health_view.py +0 -0
  159. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-identity-service/cookiecutter.json +0 -0
  160. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/.dockerignore +0 -0
  161. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/Dockerfile +0 -0
  162. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/README.md +0 -0
  163. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/__init__.py +0 -0
  164. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/dependencies/__init__.py +0 -0
  165. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/models/__init__.py +0 -0
  166. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/models/users.py +0 -0
  167. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/repositories/__init__.py +0 -0
  168. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/repositories/user_repository.py +0 -0
  169. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/requirements.txt +0 -0
  170. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/settings.py +0 -0
  171. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/views/__init__.py +0 -0
  172. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/views/unversioned/__init__.py +0 -0
  173. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/views/unversioned/health_view.py +0 -0
  174. {csrd_utils-0.3.82 → csrd_utils-0.3.86}/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/views/unversioned/users_view.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: csrd-utils
3
- Version: 0.3.82
3
+ Version: 0.3.86
4
4
  Summary: CLI utilities for csrd service generation and feature augmentation
5
5
  Project-URL: Repository, https://github.com/csrd-api/fastapi-common
6
6
  Project-URL: Documentation, https://github.com/csrd-api/fastapi-common/tree/main/packages/utils
@@ -8,7 +8,9 @@ Project-URL: Changelog, https://github.com/csrd-api/fastapi-common/blob/main/CHA
8
8
  License: MIT
9
9
  Requires-Python: >=3.12
10
10
  Requires-Dist: cookiecutter<3,>=2.6
11
+ Requires-Dist: pydantic<3,>=2.6
11
12
  Requires-Dist: pyyaml<7,>=6
13
+ Requires-Dist: ruamel-yaml<1,>=0.18
12
14
  Description-Content-Type: text/markdown
13
15
 
14
16
  # csrd-utils
@@ -1,12 +1,14 @@
1
1
  [project]
2
2
  name = "csrd-utils"
3
- version = "0.3.82"
3
+ version = "0.3.86"
4
4
  description = "CLI utilities for csrd service generation and feature augmentation"
5
5
  license = { text = "MIT" }
6
6
  requires-python = ">=3.12"
7
7
  dependencies = [
8
8
  "cookiecutter>=2.6,<3",
9
9
  "PyYAML>=6,<7",
10
+ "pydantic>=2.6,<3",
11
+ "ruamel.yaml>=0.18,<1",
10
12
  ]
11
13
  readme = "README.md"
12
14
 
@@ -3,6 +3,7 @@
3
3
  import argparse
4
4
  import json
5
5
  import shutil
6
+ import sys
6
7
  from contextlib import suppress
7
8
  from importlib.metadata import version
8
9
  from pathlib import Path
@@ -24,6 +25,9 @@ from .cluster import (
24
25
  from .doctor import run_doctor
25
26
  from .generator import generate_platform_service, generate_service
26
27
  from .resources import features_path
28
+ from .v2.compose import apply as v2_compose_apply
29
+ from .v2.compose import validate as v2_compose_validate
30
+ from .v2.generate import run_generate_menu as run_v2_generate_menu
27
31
 
28
32
 
29
33
  class GenerateServiceArgs(TypedDict):
@@ -57,6 +61,7 @@ class GenerateWorkspaceArgs(TypedDict):
57
61
  output_dir: Path
58
62
  overwrite_if_exists: bool
59
63
  generate: str
64
+ git_init: bool
60
65
 
61
66
 
62
67
  class ExtractWorkspaceServiceArgs(TypedDict):
@@ -416,12 +421,14 @@ def _collect_new_workspace_config(args: argparse.Namespace) -> GenerateWorkspace
416
421
  generate = args.generate or _prompt_choice(
417
422
  "Generate in workspace now", ["none", "service", "cluster"], "service"
418
423
  )
424
+ git_init = _prompt_bool("Initialize git repository", True)
419
425
  return GenerateWorkspaceArgs(
420
426
  {
421
427
  "name": name,
422
428
  "output_dir": output_dir,
423
429
  "overwrite_if_exists": args.force,
424
430
  "generate": generate,
431
+ "git_init": git_init,
425
432
  }
426
433
  )
427
434
 
@@ -438,6 +445,10 @@ def _create_workspace(config: GenerateWorkspaceArgs) -> Path:
438
445
  )
439
446
 
440
447
  _write_workspace_marker(workspace_root)
448
+
449
+ if config.get("git_init", False):
450
+ _git_init(workspace_root)
451
+
441
452
  return workspace_root
442
453
 
443
454
 
@@ -577,6 +588,20 @@ def _write_workspace_marker(path: Path) -> None:
577
588
  (path / WORKSPACE_MARKER).write_text("workspace: csrd\n", encoding="utf-8")
578
589
 
579
590
 
591
+ def _git_init(path: Path) -> None:
592
+ """Run ``git init`` inside *path* if not already a git repository."""
593
+ import subprocess
594
+
595
+ if (path / ".git").exists():
596
+ return
597
+ subprocess.run(
598
+ ["git", "init"],
599
+ cwd=path,
600
+ check=True,
601
+ capture_output=True,
602
+ )
603
+
604
+
580
605
  def _is_workspace(path: Path) -> bool:
581
606
  return (path / WORKSPACE_MARKER).is_file()
582
607
 
@@ -1199,6 +1224,76 @@ def _build_parser() -> argparse.ArgumentParser:
1199
1224
  completion_sub = completion.add_subparsers(dest="completion_shell", required=True)
1200
1225
  completion_sub.add_parser("bash", help="Print bash completion script")
1201
1226
 
1227
+ v2 = sub.add_parser("v2", help="Preview next-gen generator and compose workflow")
1228
+ v2_sub = v2.add_subparsers(dest="v2_command")
1229
+
1230
+ v2_generate = v2_sub.add_parser("generate", help="Open interactive v2 generator menu")
1231
+ v2_generate.add_argument(
1232
+ "target",
1233
+ nargs="?",
1234
+ choices=["workspace", "preset", "add-service", "add-infra", "remove-infra", "empty"],
1235
+ help="Direct generator target (workspace, preset, add-service, add-infra, remove-infra)",
1236
+ )
1237
+ v2_generate.add_argument(
1238
+ "--name",
1239
+ dest="generate_name",
1240
+ help="Workspace or service name",
1241
+ )
1242
+ v2_generate.add_argument(
1243
+ "--git-init",
1244
+ action="store_true",
1245
+ default=False,
1246
+ help="Initialize a git repository in the generated workspace",
1247
+ )
1248
+ v2_generate.add_argument(
1249
+ "--features",
1250
+ dest="features",
1251
+ nargs="*",
1252
+ choices=["database", "caching", "messaging"],
1253
+ default=None,
1254
+ help="Service capabilities to wire (for add-service)",
1255
+ )
1256
+ v2_generate.add_argument(
1257
+ "--port",
1258
+ dest="port",
1259
+ type=int,
1260
+ default=None,
1261
+ help="Service port (for add-service)",
1262
+ )
1263
+ v2_generate.add_argument(
1264
+ "--infra-type",
1265
+ dest="infra_type",
1266
+ choices=["postgres", "mariadb", "sqlite", "redis", "rabbitmq"],
1267
+ default=None,
1268
+ help="Infrastructure type (for add-infra)",
1269
+ )
1270
+
1271
+ v2_compose = v2_sub.add_parser("compose", help="Compose v2 operations")
1272
+ v2_compose_sub = v2_compose.add_subparsers(dest="v2_compose_command")
1273
+
1274
+ v2_compose_validate = v2_compose_sub.add_parser("validate", help="Validate csrd-compose.yaml")
1275
+ v2_compose_validate.add_argument(
1276
+ "--output", type=Path, default=Path.cwd(), help="Workspace root"
1277
+ )
1278
+ v2_compose_validate.add_argument(
1279
+ "--json", action="store_true", help="Print validated compose spec as JSON"
1280
+ )
1281
+
1282
+ v2_compose_plan = v2_compose_sub.add_parser("plan", help="Preview resolved compose baseline")
1283
+ v2_compose_plan.add_argument("--output", type=Path, default=Path.cwd(), help="Workspace root")
1284
+ v2_compose_plan.add_argument("--json", action="store_true", help="Print plan payload as JSON")
1285
+
1286
+ v2_compose_apply = v2_compose_sub.add_parser(
1287
+ "apply", help="Render baseline workspace artifacts"
1288
+ )
1289
+ v2_compose_apply.add_argument("--output", type=Path, default=Path.cwd(), help="Workspace root")
1290
+ v2_compose_apply.add_argument(
1291
+ "--git-init",
1292
+ action="store_true",
1293
+ default=False,
1294
+ help="Initialize a git repository in the workspace",
1295
+ )
1296
+
1202
1297
  return parser
1203
1298
 
1204
1299
 
@@ -1323,9 +1418,106 @@ def _run_audit(service: Path | None, as_json: bool) -> int:
1323
1418
 
1324
1419
 
1325
1420
  def main() -> int:
1421
+ # Shorthand: `csrd v2 compose <path>` => `csrd v2 compose apply --output <path>`
1422
+ raw_argv = sys.argv[1:]
1423
+ if len(raw_argv) >= 3 and raw_argv[0] == "v2" and raw_argv[1] == "compose":
1424
+ third = raw_argv[2]
1425
+ if third not in {"validate", "plan", "apply", "-h", "--help"} and not third.startswith("-"):
1426
+ shorthand_dir = Path(third).resolve()
1427
+ has_compose_spec = (shorthand_dir / "csrd-compose.yaml").is_file() or (
1428
+ shorthand_dir / "csrd-compose.yml"
1429
+ ).is_file()
1430
+ if not has_compose_spec:
1431
+ print(f"ERROR: Compose shorthand requires csrd-compose.yaml in: {shorthand_dir}")
1432
+ print(
1433
+ "Hint: create a spec first (csrd v2 generate empty --name <name>) "
1434
+ "or use explicit apply: csrd v2 compose apply --output <path>"
1435
+ )
1436
+ return 1
1437
+ sys.argv = [
1438
+ sys.argv[0],
1439
+ "v2",
1440
+ "compose",
1441
+ "apply",
1442
+ "--output",
1443
+ third,
1444
+ *raw_argv[3:],
1445
+ ]
1446
+
1326
1447
  parser = _build_parser()
1327
1448
  args = parser.parse_args()
1328
1449
 
1450
+ if args.command == "v2":
1451
+ if getattr(args, "v2_command", None) == "generate":
1452
+ return run_v2_generate_menu(
1453
+ target=getattr(args, "target", None),
1454
+ name=getattr(args, "generate_name", None),
1455
+ git_init=getattr(args, "git_init", False),
1456
+ features=getattr(args, "features", None),
1457
+ port=getattr(args, "port", None),
1458
+ infra_type=getattr(args, "infra_type", None),
1459
+ )
1460
+
1461
+ if getattr(args, "v2_command", None) != "compose":
1462
+ parser.print_help()
1463
+ return 0
1464
+
1465
+ compose_command = getattr(args, "v2_compose_command", None)
1466
+ if compose_command is None:
1467
+ parser.print_help()
1468
+ return 0
1469
+
1470
+ output_dir = Path(args.output).resolve()
1471
+ try:
1472
+ if compose_command == "apply":
1473
+ generated_root = v2_compose_apply(
1474
+ output_dir, git_init=getattr(args, "git_init", False)
1475
+ )
1476
+ print(f"Rendered compose workspace at: {generated_root}")
1477
+ return 0
1478
+
1479
+ if compose_command == "validate":
1480
+ spec = v2_compose_validate(output_dir)
1481
+ if args.json:
1482
+ print(json.dumps(spec.model_dump(mode="python"), indent=2, sort_keys=True))
1483
+ else:
1484
+ print(f"Compose spec valid: {output_dir / 'csrd-compose.yaml'}")
1485
+ return 0
1486
+
1487
+ if compose_command == "plan":
1488
+ spec = v2_compose_validate(output_dir)
1489
+ payload = {
1490
+ "workspace": spec.workspace.name,
1491
+ "version": spec.version,
1492
+ "services": len(spec.services),
1493
+ "infra": len(spec.infra),
1494
+ "presets": [preset.name for preset in spec.presets],
1495
+ "overrides_keys": sorted(spec.overrides.keys()),
1496
+ }
1497
+ if args.json:
1498
+ print(json.dumps(payload, indent=2, sort_keys=True))
1499
+ else:
1500
+ print("Compose v2 plan")
1501
+ print(f"- workspace: {payload['workspace']}")
1502
+ print(f"- version: {payload['version']}")
1503
+ print(f"- services: {payload['services']}")
1504
+ print(f"- infra: {payload['infra']}")
1505
+ presets = payload["presets"]
1506
+ overrides_keys = payload["overrides_keys"]
1507
+ assert isinstance(presets, list)
1508
+ assert isinstance(overrides_keys, list)
1509
+ print(f"- presets: {', '.join(presets) if presets else '<none>'}")
1510
+ print(
1511
+ f"- overrides: {', '.join(overrides_keys) if overrides_keys else '<none>'}"
1512
+ )
1513
+ return 0
1514
+ except ValueError as exc:
1515
+ print(f"ERROR: {exc}")
1516
+ return 1
1517
+
1518
+ parser.print_help()
1519
+ return 0
1520
+
1329
1521
  if args.command == "new" and args.new_command == "service":
1330
1522
  try:
1331
1523
  service_config = _collect_new_service_config(args)
@@ -0,0 +1,349 @@
1
+ # Menu System & Compose Design Rules
2
+
3
+ This file captures the design principles of the `v2/generate` menu system
4
+ and the `v2/compose` rendering pipeline.
5
+ Consult it for every change that adds, removes, or modifies menus, menu items,
6
+ prompts, interactive selection flows, or compose/renderer behavior.
7
+
8
+ ## Scope
9
+
10
+ - Applies to `packages/utils/src/csrd_utils/v2/**`.
11
+ - Also follow `packages/utils/AGENTS.md` and the top-level `AGENTS.md`.
12
+
13
+ ## Drop-in replacement intent
14
+
15
+ The `v2` module is intended to **fully replace** the bare `csrd` command.
16
+ Once promoted, the `v2/` directory becomes the top-level implementation and
17
+ the `v2` namespace disappears.
18
+
19
+ **Naming rule**: Do not use "v2" in any module name, class name, function name,
20
+ variable name, constant, test name, CLI label, or user-facing string inside this
21
+ package. The only place "v2" appears is the directory name itself, which will be
22
+ removed at promotion time. Names should read as if `v2/` does not exist —
23
+ e.g. `compose`, `generate`, `ComposeSpec`, `render_workspace`, not
24
+ `v2_compose`, `V2ComposeSpec`, `render_v2_workspace`.
25
+
26
+ ## Architecture overview
27
+
28
+ ### Generate layer (interactive / CLI)
29
+
30
+ The menu system is split into three layers:
31
+
32
+ | Layer | File | Responsibility |
33
+ |---|---|---|
34
+ | **Primitives** | `helpers.py` | `MenuItem` dataclass, rendering, TTY/non-TTY selection, prompt helpers (`prompt_text`, `prompt_yes_no`, `prompt_single_select`, `prompt_multi_select`). |
35
+ | **Orchestration** | `menu.py` | Declarative item lists (`IN_WORKSPACE_ITEMS`, `_build_no_workspace_items`), context predicates, direct-mode dispatch, and the public `run_generate_menu` entry-point. |
36
+ | **Handlers** | `handlers.py` | Business-logic callbacks invoked by menu items. Each returns an `int` exit code. Shared finalization via `_finalize_workspace`. |
37
+
38
+ Changes must respect this separation. Do not put rendering logic in handlers
39
+ or business logic in helpers.
40
+
41
+ ### Compose layer (deterministic rendering)
42
+
43
+ The compose pipeline is split into focused modules:
44
+
45
+ | Module | Responsibility |
46
+ |---|---|
47
+ | `models/spec.py` | Pydantic models: `ComposeSpec`, `ServiceNode`, `InfraNode`, `PresetDefinition`, constants (`INFRA_DATABASES`, `INFRA_CATEGORIES`, etc.) |
48
+ | `compose/infra.py` | `InfraDescriptor` registry — single source of truth for all infra container defs, service env wiring, depends_on, and env example lines |
49
+ | `compose/operations.py` | Workspace mutations: `add_service`, `add_infra`, `remove_infra`, `available_infra`, `configured_infra`, `apply`, `validate` |
50
+ | `compose/renderer.py` | Pure `spec → file content` transforms: docker-compose.yml, .env.example, README, .gitignore, pyproject.toml, workspace marker |
51
+ | `compose/scaffolder.py` | Cookiecutter template → workspace filesystem layout (idempotent) |
52
+ | `compose/loader.py` | Load/save `csrd-compose.yaml` |
53
+ | `compose/presets.py` | Preset definitions and category helpers |
54
+ | `compose/yaml_editor.py` | YAML serialization utilities |
55
+ | `compose/__init__.py` | Re-exports only — no business logic |
56
+
57
+ ## Non-negotiable design rules
58
+
59
+ ### 1. Deterministic rendering
60
+
61
+ Every render function must be a **pure transform from spec to output**.
62
+ Same `ComposeSpec` → same files, byte-for-byte.
63
+
64
+ - **No randomness**: no UUIDs, timestamps, random ports, or non-deterministic ordering.
65
+ - **No environment reads**: renderers must not read env vars, `$HOME`, `os.getcwd()`, or any external state.
66
+ - **Stable iteration**: `spec.infra` and `spec.services` are ordered lists (not sets). Compose services appear in insertion order.
67
+ - **Stable YAML**: `dumps_yaml` uses `default_flow_style=False` for consistent formatting.
68
+ - **Test proof**: `test_render_workspace_is_idempotent` asserts two consecutive renders produce byte-identical output.
69
+
70
+ ### 2. Declarative spec as single source of truth
71
+
72
+ `csrd-compose.yaml` (`ComposeSpec`) is the **only** source of truth for a workspace.
73
+
74
+ - Renderers never inspect generated files to decide what to generate.
75
+ - Data flow is one-directional: `user input → operations (mutate spec) → renderer (spec → files)`.
76
+ - No feedback loops: the renderer does not read `docker-compose.yml` or scaffolded source to inform its output.
77
+
78
+ ### 3. Data-driven infra registry (DRY / Open-Closed)
79
+
80
+ All infra knowledge lives in `INFRA_REGISTRY` (`infra.py`) as frozen `InfraDescriptor` instances.
81
+
82
+ - **Adding a new infra type** requires only adding a new `InfraDescriptor` entry to the registry.
83
+ - Do **not** scatter infra-specific if/elif branches across renderer, env example, or service wiring code.
84
+ - The renderer queries the registry generically via `desc.service_env`, `desc.depends_on`, `desc.env_example_lines`.
85
+ - Exception: sqlite is special-cased in `_wire_feature` because it needs per-service volumes with no shared container. This is documented inline.
86
+
87
+ ### 4. Single Responsibility (SRP)
88
+
89
+ - `compose/__init__.py` — re-exports only, zero business logic.
90
+ - `operations.py` — spec mutations (add/remove on disk).
91
+ - `renderer.py` — spec → file content (no I/O decisions, no spec mutations).
92
+ - `scaffolder.py` — cookiecutter execution and filesystem rearrangement.
93
+ - `infra.py` — infra data and lookup helpers.
94
+ - `handlers.py` — interactive/direct CLI handlers. Shared finalization via `_finalize_workspace`.
95
+
96
+ Do not mix responsibilities across these modules.
97
+
98
+ ### 5. DRY: No duplicate definitions
99
+
100
+ - **Infra wiring** (service env, depends_on, env example) is defined once in `InfraDescriptor`, not repeated in renderer, env builder, or handlers.
101
+ - **Workspace finalization** (create dir → add infra → add services → render) is in `_finalize_workspace`, shared by both `create_workspace` and `create_workspace_from_preset`.
102
+ - **DB detection** is in `infra.detect_configured_db`, not duplicated in renderer and handlers.
103
+ - **Model types** are defined in `models/spec.py`, re-exported through `models/__init__.py` and `compose/__init__.py` — no shim modules or alias files.
104
+
105
+ ### 6. Idempotent scaffolding
106
+
107
+ `scaffold_service` skips silently if `src/<service-name>/` already exists.
108
+ User edits to scaffolded files are never overwritten on re-render.
109
+
110
+ - `.env` is created only if missing (user customizations preserved).
111
+ - All other workspace files (compose, env example, pyproject, README, gitignore) are overwritten on every render (they are derived from spec).
112
+
113
+ ### 7. Workspace filesystem contract
114
+
115
+ ```
116
+ <ws-root>/
117
+ csrd-compose.yaml ← spec (source of truth)
118
+ docker-compose.yml ← rendered from spec
119
+ .env.example ← rendered from spec
120
+ .env ← created once, never overwritten
121
+ pyproject.toml ← rendered from spec (pytest config)
122
+ README.md ← rendered from spec
123
+ .gitignore ← rendered from spec
124
+ .csrd-workspace ← marker file
125
+ Dockerfile.<service-name> ← one per service, at root
126
+ src/<service-name>/ ← service source (kebab-case dir)
127
+ tests/unit/<service_snake>/ ← unit tests (snake_case dir)
128
+ tests/acceptance/<service_snake>/ ← acceptance stubs (snake_case dir)
129
+ ```
130
+
131
+ - Service source dirs use **kebab-case** (matching `service.name`).
132
+ - Test dirs use **snake_case** (valid Python package names).
133
+ - Dockerfiles use kebab-case service name for the suffix, snake_case for the Python module reference in CMD.
134
+
135
+ ### 8. Consistent model paradigm
136
+
137
+ All models use **Pydantic `BaseModel`** (from `models/base.py`).
138
+ Do not use `@dataclass` for spec or preset models.
139
+ Use `model_config = ConfigDict(frozen=True)` for immutable models (e.g. `PresetDefinition`).
140
+
141
+ Exception: `InfraDescriptor` in `infra.py` uses `@dataclass(frozen=True)` because it is a
142
+ compose-internal data structure, not a serializable spec model.
143
+
144
+ ### 9. Exception semantics
145
+
146
+ - `FileNotFoundError` — missing spec file (`loader.py`).
147
+ - `ValueError` — invalid spec content, duplicate service/infra, unconfigured infra removal.
148
+ - `KeyError` — unknown infra type in registry lookup.
149
+
150
+ Do not use generic `Exception` or mismatched exception types.
151
+
152
+ ### 10. Declarative item definitions
153
+
154
+ Menu items are declared as static `list[MenuItem]` constants (or built by a
155
+ simple factory when runtime data like `default_name` is needed).
156
+
157
+ - **Do not** build menus with imperative if/else chains.
158
+ - Use the `visible` predicate on `MenuItem` to hide items based on context.
159
+ The `filter_visible()` helper evaluates these at menu-build time.
160
+
161
+ ### 11. Dual-mode: interactive + direct dispatch
162
+
163
+ Every interactive flow must have a non-interactive (direct) counterpart
164
+ invokable via CLI flags (`target`, `--name`, `--database`, etc.).
165
+
166
+ - `run_generate_menu` routes direct targets first, then falls through to the
167
+ interactive menu. Keep this order: direct dispatch → interactive.
168
+ - Direct handlers are named `*_direct`, interactive ones `*_interactive`.
169
+
170
+ ### 12. TTY / non-TTY graceful degradation
171
+
172
+ All interactive primitives must work in both TTY and non-TTY environments:
173
+
174
+ - **TTY path**: arrow-key navigation via `cbreak_mode()`, `>` cursor marker.
175
+ - **Non-TTY path**: numbered list with text input fallback.
176
+ - Gate on `sys.stdin.isatty() and sys.stdout.isatty()` — never assume a terminal.
177
+ - `cbreak_mode()` yields `None` when `termios` is unavailable; always handle that.
178
+ - Escape key detection uses `select.select()` with a 50 ms timeout via
179
+ `_read_escape_seq()` to distinguish standalone Escape from arrow-key
180
+ prefixes (`\x1b[A`/`\x1b[B`).
181
+
182
+ ### 13. Cancel, interrupt, and back-navigation
183
+
184
+ **Ctrl-C (KeyboardInterrupt)**:
185
+ - Must **propagate** from all prompt primitives — they must NOT swallow it.
186
+ - Only the outermost handler or `run_menu` catches it.
187
+ - Handlers print `"Cancelled."` (wrapped in `contextlib.suppress`) and
188
+ return exit code `1`.
189
+ - Swallowing at the primitive level causes fall-through — this is a bug.
190
+
191
+ **Escape / back (GoBack)**:
192
+ - `GoBack` is a lightweight exception raised by `prompt_single_select` and
193
+ `prompt_multi_select` when the user presses Escape (TTY) or types `0` /
194
+ `back` (non-TTY).
195
+ - In **nested flows** (e.g. `_prompt_infra_categories`), catch `GoBack`
196
+ from sub-selects and loop back to the parent prompt.
197
+ - In **top-level handlers**, catch `GoBack` and skip the step (empty selection).
198
+ - In **`run_menu`**, Escape = cancel (same as selecting the cancel item).
199
+ - Visual hints: `"Esc back"` (TTY), `"0) back"` (non-TTY).
200
+
201
+ **EOF (EOFError)**:
202
+ - Return quietly with the default/empty value from primitives.
203
+ - `run_menu` returns `0`.
204
+
205
+ ### 14. Exit code contract
206
+
207
+ All handlers and menu runners return an `int` exit code:
208
+
209
+ | Code | Meaning |
210
+ |---|---|
211
+ | `0` | Success or clean cancel |
212
+ | `1` | Error or user interrupt |
213
+
214
+ Do not raise exceptions to signal user-facing outcomes; print and return a code.
215
+
216
+ ### 15. Prompt conventions
217
+
218
+ - `prompt_text(question, default=...)` — returns default on empty input or EOF.
219
+ In TTY mode uses `_tty_line_input` (cbreak-mode character-by-character
220
+ reading) so that Escape raises `GoBack` and Ctrl-C raises
221
+ `KeyboardInterrupt`. Non-TTY falls back to `input()`.
222
+ - `prompt_yes_no(question, default=True)` — same TTY/non-TTY split. First
223
+ prompt defaults yes; "add another?" prompts default no.
224
+ - `prompt_single_select` — auto-selects when only one option exists. Escape
225
+ raises `GoBack`.
226
+ - `prompt_multi_select` — spacebar toggles, Enter confirms; non-TTY accepts
227
+ comma-separated numbers, `all`, or `back`/`0`. Escape raises `GoBack`.
228
+
229
+ All four prompts support Escape for back-navigation. Do not invent new prompt
230
+ primitives without adding them to `helpers.py` first.
231
+
232
+ ### 16. Composable wizard flows
233
+
234
+ Multi-step interactive flows **must** use `run_wizard()` from `helpers.py`
235
+ with a list of `WizardStep` callables. Each step is a function that receives
236
+ a shared `state: dict[str, object]` and either returns normally (advance) or
237
+ raises `GoBack` (go back). The wizard handles all back-navigation and
238
+ interrupt logic automatically.
239
+
240
+ ```python
241
+ # ✅ Correct — composable steps, no control flow boilerplate
242
+ steps: list[WizardStep] = [
243
+ _step_workspace_name,
244
+ _step_infra_categories,
245
+ _step_services,
246
+ _step_git_init,
247
+ ]
248
+ state = run_wizard(steps)
249
+ ```
250
+
251
+ ```python
252
+ # ❌ Prohibited — hand-coded if/elif step dispatch
253
+ while step <= 3:
254
+ if step == 0:
255
+ ...
256
+ elif step == 1:
257
+ ...
258
+ ```
259
+
260
+ Rules:
261
+ - **Never** use raw `while step <= N` / `if step == 0` dispatch in handlers.
262
+ - Each step must be an independent `_step_*` function in `handlers.py`.
263
+ - Steps that appear in more than one wizard flow must be shared (e.g.
264
+ `_step_workspace_name`, `_step_git_init`).
265
+ - Steps communicate exclusively through the shared state dict.
266
+ - `GoBack` at step 0 = cancel (exit code 0).
267
+ - `KeyboardInterrupt` at any step = abort (exit code 1).
268
+
269
+ Back-navigation semantics:
270
+ ```
271
+ Step 0: name ← Escape here = cancel (exit code 0)
272
+ Step 1: infra ← Escape here = back to step 0
273
+ Step 2: services ← Escape here = back to step 1
274
+ Step 3: git init ← Escape here = back to step 2
275
+ ```
276
+
277
+
278
+ ### 17. Naming & normalization
279
+
280
+ - Service names: `normalize_service_name()` → kebab-case, appends `-service`.
281
+ - Workspace names: `resolve_workspace_name()` → collapse whitespace, reject
282
+ path separators.
283
+ - Always print a `"Normalized to: ..."` message when the name changes.
284
+
285
+ ### 18. Category-based infra selection
286
+
287
+ Infrastructure is organized by category (`database`, `messaging`, `caching`)
288
+ defined in `models/spec.py` via `INFRA_CATEGORIES`.
289
+
290
+ - Step 1: multi-select categories.
291
+ - Step 2: for each category, single-select the specific type (auto-select if
292
+ only one member).
293
+ - Already-configured types are excluded from the available set.
294
+
295
+ Do not bypass the category system with flat infra-type lists.
296
+
297
+ ### 19. Context awareness
298
+
299
+ Menus adapt based on workspace presence:
300
+
301
+ - **Inside workspace** (`csrd-compose.yaml` exists): show `add service`,
302
+ `add infra` (if slots remain), `remove infra` (if any configured), `cancel`.
303
+ - **Outside workspace**: show `custom`, `preset`, `cancel`.
304
+
305
+ Context predicates (`_has_workspace`, `_has_available_infra`,
306
+ `_has_configured_infra`) must be pure filesystem checks — no global state,
307
+ no caching.
308
+
309
+ Alias uniqueness: each alias must be unique within a menu list. Do not reuse
310
+ a shortcut alias across items in the same list (e.g. `"c"` for both `custom`
311
+ and `cancel`).
312
+
313
+ ### 20. No runtime auto-discovery
314
+
315
+ Menu items, infra types, and preset definitions are statically registered.
316
+ Do not add plugin loaders, dynamic module scanning, or entry-point discovery.
317
+ This aligns with the top-level `AGENTS.md` cookiecutter guardrail.
318
+
319
+ ## Audit checklist
320
+
321
+ When asked to "audit the menus or compose system", verify each of the following:
322
+
323
+ ### Menu audit
324
+ 1. Every `MenuItem` has a `label`, at least two `aliases` (full name + shortcut), and a handler.
325
+ 2. Every menu list ends with a `cancel` item.
326
+ 3. `filter_visible()` is called before passing items to `run_menu()`.
327
+ 4. All interactive flows have a `*_direct` counterpart wired in `run_generate_menu`.
328
+ 5. TTY and non-TTY paths are both exercised (check for `isatty` gates).
329
+ 6. `KeyboardInterrupt` and `EOFError` are caught in every prompt/menu call.
330
+ 7. Exit codes follow the `0`/`1` contract.
331
+ 8. Infra selection uses the category flow, not a flat list.
332
+ 9. Names are normalized and the user is informed of changes.
333
+ 10. No new prompt primitives exist outside `helpers.py`.
334
+ 11. The three-layer separation (helpers → menu → handlers) is preserved.
335
+ 12. Tests in `tests/test_cli.py` and `tests/test_v2_generate*.py` cover new/changed items.
336
+ 13. All multi-step interactive flows use `run_wizard()` with a list of `WizardStep` callables — no raw `while step <= N` / `if step == 0` dispatch exists in handlers.
337
+ 14. No step logic is duplicated across wizard flows — shared steps (`_step_workspace_name`, `_step_git_init`, etc.) are extracted to `_step_*` functions.
338
+
339
+ ### Compose / renderer audit
340
+ 15. All render functions are pure `spec → string` transforms with no side effects, no env reads, no filesystem inspection.
341
+ 16. `test_render_workspace_is_idempotent` passes — two consecutive renders produce identical output.
342
+ 17. Infra wiring uses `INFRA_REGISTRY` descriptors — no hardcoded if/elif branches (except the documented sqlite special case).
343
+ 18. `.env` is created-if-missing, all other files are overwritten from spec.
344
+ 19. `compose/__init__.py` contains only re-exports — no business logic.
345
+ 20. `operations.py` functions load → mutate → save spec — no rendering.
346
+ 21. Workspace finalization uses `_finalize_workspace` — no duplicated create/add/render sequences in handlers.
347
+ 22. `detect_configured_db` is defined once in `infra.py` — not duplicated.
348
+ 23. Test dirs use snake_case, source dirs use kebab-case, Dockerfiles use kebab-case suffix.
349
+ 24. No shim modules or alias re-export files exist (pre-1.0 refactor policy).
@@ -0,0 +1 @@
1
+ """Namespace package for next-gen csrd compose and generate flows."""