csrd-utils 0.3.86__tar.gz → 0.3.89__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 (196) hide show
  1. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/PKG-INFO +1 -1
  2. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/pyproject.toml +3 -2
  3. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/v2/compose/__init__.py +24 -1
  4. csrd_utils-0.3.89/src/csrd_utils/v2/compose/augments.py +166 -0
  5. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/v2/compose/infra.py +0 -2
  6. csrd_utils-0.3.89/src/csrd_utils/v2/compose/operations.py +358 -0
  7. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/v2/compose/presets.py +15 -11
  8. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/v2/compose/renderer.py +122 -3
  9. csrd_utils-0.3.89/src/csrd_utils/v2/compose/scaffolder.py +224 -0
  10. csrd_utils-0.3.89/src/csrd_utils/v2/compose/service_renderers.py +288 -0
  11. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/v2/compose/yaml_editor.py +0 -2
  12. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/v2/generate/handlers.py +0 -2
  13. csrd_utils-0.3.89/src/csrd_utils/v2/generate/helpers.py +71 -0
  14. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/v2/generate/menu.py +2 -3
  15. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/v2/models/__init__.py +8 -3
  16. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/v2/models/spec.py +20 -3
  17. csrd_utils-0.3.89/src/csrd_utils/v2/models/types.py +8 -0
  18. csrd_utils-0.3.89/src/csrd_utils/v2/templates/augments/db-config/dependencies/db.py +21 -0
  19. csrd_utils-0.3.89/src/csrd_utils/v2/templates/augments/db-config/migrations.py +21 -0
  20. csrd_utils-0.3.89/src/csrd_utils/v2/templates/augments/jwt-auth-consumer/dependencies/auth.py +24 -0
  21. csrd_utils-0.3.89/src/csrd_utils/v2/templates/augments/jwt-auth-provider/dependencies/db.py +31 -0
  22. csrd_utils-0.3.89/src/csrd_utils/v2/templates/augments/jwt-auth-provider/migrations.py +52 -0
  23. csrd_utils-0.3.89/src/csrd_utils/v2/templates/augments/jwt-auth-provider/repositories/user_repository.py +108 -0
  24. csrd_utils-0.3.89/src/csrd_utils/v2/templates/augments/jwt-auth-provider/views/auth_view.py +121 -0
  25. csrd_utils-0.3.89/src/csrd_utils/v2/templates/augments/jwt-auth-provider/views/jwks_view.py +27 -0
  26. csrd_utils-0.3.89/src/csrd_utils/v2/templates/augments/jwt-auth-provider/views/users_view.py +77 -0
  27. csrd_utils-0.3.89/src/tui_wizard/__init__.py +45 -0
  28. csrd_utils-0.3.89/src/tui_wizard/exceptions.py +5 -0
  29. csrd_utils-0.3.89/src/tui_wizard/menu.py +128 -0
  30. csrd_utils-0.3.89/src/tui_wizard/models.py +34 -0
  31. csrd_utils-0.3.89/src/tui_wizard/prompts.py +311 -0
  32. csrd_utils-0.3.89/src/tui_wizard/terminal.py +74 -0
  33. csrd_utils-0.3.89/src/tui_wizard/wizard.py +67 -0
  34. csrd_utils-0.3.86/src/csrd_utils/v2/compose/operations.py +0 -126
  35. csrd_utils-0.3.86/src/csrd_utils/v2/compose/scaffolder.py +0 -89
  36. csrd_utils-0.3.86/src/csrd_utils/v2/generate/helpers.py +0 -647
  37. csrd_utils-0.3.86/src/csrd_utils/v2/models/types.py +0 -6
  38. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/.gitignore +0 -0
  39. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/README.md +0 -0
  40. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/__init__.py +0 -0
  41. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/__main__.py +0 -0
  42. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/audit.py +0 -0
  43. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/augmentor.py +0 -0
  44. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/cluster.py +0 -0
  45. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/doctor.py +0 -0
  46. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/features/caching/conftest.fragment.py +0 -0
  47. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/features/caching/docker-compose.fragment.yaml +0 -0
  48. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/features/caching/manifest.yaml +0 -0
  49. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/features/caching/requirements.fragment +0 -0
  50. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/features/caching/settings.fragment.py +0 -0
  51. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/features/caching/src/app/cache.py +0 -0
  52. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/features/caching/tests/test_cache.py +0 -0
  53. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/features/logging/conftest.fragment.py +0 -0
  54. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/features/logging/manifest.yaml +0 -0
  55. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/features/logging/requirements.fragment +0 -0
  56. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/features/logging/settings.fragment.py +0 -0
  57. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/features/logging/src/app/middleware/logging.py +0 -0
  58. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/features/logging/tests/test_logging_middleware.py +0 -0
  59. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/features/metrics/conftest.fragment.py +0 -0
  60. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/features/metrics/docker-compose.fragment.yaml +0 -0
  61. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/features/metrics/manifest.yaml +0 -0
  62. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/features/metrics/requirements.fragment +0 -0
  63. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/features/metrics/settings.fragment.py +0 -0
  64. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/features/metrics/src/app/middleware/metrics.py +0 -0
  65. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/features/metrics/tests/test_metrics_middleware.py +0 -0
  66. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/features/tracing/conftest.fragment.py +0 -0
  67. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/features/tracing/manifest.yaml +0 -0
  68. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/features/tracing/requirements.fragment +0 -0
  69. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/features/tracing/settings.fragment.py +0 -0
  70. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/features/tracing/src/app/middleware/tracing.py +0 -0
  71. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/features/tracing/tests/test_tracing_middleware.py +0 -0
  72. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/features/workers/conftest.fragment.py +0 -0
  73. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/features/workers/docker-compose.fragment.yaml +0 -0
  74. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/features/workers/manifest.yaml +0 -0
  75. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/features/workers/requirements.fragment +0 -0
  76. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/features/workers/settings.fragment.py +0 -0
  77. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/features/workers/src/app/workers/__init__.py +0 -0
  78. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/features/workers/src/app/workers/celery_app.py +0 -0
  79. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/features/workers/src/app/workers/tasks.py +0 -0
  80. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/features/workers/tests/test_workers.py +0 -0
  81. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/fragments.py +0 -0
  82. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/generator.py +0 -0
  83. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/resources.py +0 -0
  84. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/cookiecutter-service/README.md +0 -0
  85. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/cookiecutter-service/cookiecutter.json +0 -0
  86. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/cookiecutter-service/hooks/post_gen_project.py +0 -0
  87. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/.cookiecutterignore +0 -0
  88. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/.dockerignore +0 -0
  89. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/Dockerfile +0 -0
  90. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/README.md +0 -0
  91. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/__init__.py +0 -0
  92. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/cache.py +0 -0
  93. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/delegates/__init__.py +0 -0
  94. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/delegates/upstreams.py +0 -0
  95. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/dependencies/__init__.py +0 -0
  96. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/dependencies/auth_service.py +0 -0
  97. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/dependencies/delegates.py +0 -0
  98. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/dependencies/item_repository.py +0 -0
  99. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/dependencies/worker_broker.py +0 -0
  100. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/docker-compose.yml +0 -0
  101. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/middleware/__init__.py +0 -0
  102. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/middleware/logging.py +0 -0
  103. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/repositories/__init__.py +0 -0
  104. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/repositories/item_repository.py +0 -0
  105. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/requirements.txt +0 -0
  106. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/settings.py +0 -0
  107. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/__init__.py +0 -0
  108. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/acceptance/__init__.py +0 -0
  109. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/acceptance/test_logging_middleware.py +0 -0
  110. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/conftest.py +0 -0
  111. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/test_items.py +0 -0
  112. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/unit/__init__.py +0 -0
  113. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/unit/test_cache.py +0 -0
  114. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/unit/test_database_settings.py +0 -0
  115. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/unit/test_delegates.py +0 -0
  116. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/unit/test_settings_contracts.py +0 -0
  117. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/unit/test_worker_broker.py +0 -0
  118. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/views/__init__.py +0 -0
  119. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/views/unversioned/__init__.py +0 -0
  120. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/views/unversioned/health_view.py +0 -0
  121. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/views/unversioned/items_view.py +0 -0
  122. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/workers/__init__.py +0 -0
  123. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/workers/tasks.py +0 -0
  124. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-app-service/cookiecutter.json +0 -0
  125. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/.dockerignore +0 -0
  126. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/Dockerfile +0 -0
  127. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/README.md +0 -0
  128. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/__init__.py +0 -0
  129. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/delegates/__init__.py +0 -0
  130. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/delegates/auth_delegate.py +0 -0
  131. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/delegates/identity_delegate.py +0 -0
  132. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/dependencies/__init__.py +0 -0
  133. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/dependencies/auth_passthrough.py +0 -0
  134. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/dependencies/auth_service.py +0 -0
  135. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/dependencies/item_repository.py +0 -0
  136. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/models/__init__.py +0 -0
  137. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/models/auth_service.py +0 -0
  138. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/models/items.py +0 -0
  139. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/repositories/__init__.py +0 -0
  140. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/repositories/item_repository.py +0 -0
  141. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/requirements.txt +0 -0
  142. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/settings.py +0 -0
  143. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/views/__init__.py +0 -0
  144. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/views/unversioned/__init__.py +0 -0
  145. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/views/unversioned/auth_service_view.py +0 -0
  146. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/views/unversioned/health_view.py +0 -0
  147. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-app-service/{{cookiecutter.__service_name_snake}}/views/unversioned/items_view.py +0 -0
  148. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-auth-service/cookiecutter.json +0 -0
  149. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/.dockerignore +0 -0
  150. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/Dockerfile +0 -0
  151. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/README.md +0 -0
  152. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/__init__.py +0 -0
  153. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/dependencies/__init__.py +0 -0
  154. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/models/__init__.py +0 -0
  155. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/models/auth.py +0 -0
  156. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/repositories/__init__.py +0 -0
  157. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/repositories/token_repository.py +0 -0
  158. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/requirements.txt +0 -0
  159. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/settings.py +0 -0
  160. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/views/__init__.py +0 -0
  161. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/views/unversioned/__init__.py +0 -0
  162. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/views/unversioned/auth_view.py +0 -0
  163. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/views/unversioned/health_view.py +0 -0
  164. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-identity-service/cookiecutter.json +0 -0
  165. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/.dockerignore +0 -0
  166. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/Dockerfile +0 -0
  167. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/README.md +0 -0
  168. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/__init__.py +0 -0
  169. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/dependencies/__init__.py +0 -0
  170. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/models/__init__.py +0 -0
  171. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/models/users.py +0 -0
  172. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/repositories/__init__.py +0 -0
  173. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/repositories/user_repository.py +0 -0
  174. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/requirements.txt +0 -0
  175. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/settings.py +0 -0
  176. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/views/__init__.py +0 -0
  177. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/views/unversioned/__init__.py +0 -0
  178. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/views/unversioned/health_view.py +0 -0
  179. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/views/unversioned/users_view.py +0 -0
  180. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/v2/AGENTS.md +0 -0
  181. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/v2/__init__.py +0 -0
  182. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/v2/compose/git.py +0 -0
  183. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/v2/compose/loader.py +0 -0
  184. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/v2/generate/__init__.py +0 -0
  185. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/v2/models/base.py +0 -0
  186. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/v2/templates/service/cookiecutter.json +0 -0
  187. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/v2/templates/service/{{cookiecutter.__service_name_snake}}/Dockerfile +0 -0
  188. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/v2/templates/service/{{cookiecutter.__service_name_snake}}/README.md +0 -0
  189. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/v2/templates/service/{{cookiecutter.__service_name_snake}}/__init__.py +0 -0
  190. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/v2/templates/service/{{cookiecutter.__service_name_snake}}/requirements.txt +0 -0
  191. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/v2/templates/service/{{cookiecutter.__service_name_snake}}/settings.py +0 -0
  192. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/v2/templates/service/{{cookiecutter.__service_name_snake}}/tests/__init__.py +0 -0
  193. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/v2/templates/service/{{cookiecutter.__service_name_snake}}/tests/conftest.py +0 -0
  194. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/v2/templates/service/{{cookiecutter.__service_name_snake}}/tests/test_health.py +0 -0
  195. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/v2/templates/service/{{cookiecutter.__service_name_snake}}/views/__init__.py +0 -0
  196. {csrd_utils-0.3.86 → csrd_utils-0.3.89}/src/csrd_utils/v2/templates/service/{{cookiecutter.__service_name_snake}}/views/health_view.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: csrd-utils
3
- Version: 0.3.86
3
+ Version: 0.3.89
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "csrd-utils"
3
- version = "0.3.86"
3
+ version = "0.3.89"
4
4
  description = "CLI utilities for csrd service generation and feature augmentation"
5
5
  license = { text = "MIT" }
6
6
  requires-python = ">=3.12"
@@ -26,11 +26,12 @@ requires = ["hatchling"]
26
26
  build-backend = "hatchling.build"
27
27
 
28
28
  [tool.hatch.build.targets.wheel]
29
- packages = ["src/csrd_utils"]
29
+ packages = ["src/csrd_utils", "src/tui_wizard"]
30
30
 
31
31
  [tool.hatch.build.targets.sdist]
32
32
  include = [
33
33
  "src/csrd_utils/**",
34
+ "src/tui_wizard/**",
34
35
  "README.md",
35
36
  "pyproject.toml",
36
37
  ]
@@ -8,7 +8,15 @@ from ..models import (
8
8
  ComposeSpec,
9
9
  InfraNode,
10
10
  PresetDefinition,
11
+ ServiceAugment,
11
12
  ServiceNode,
13
+ WorkspaceAugment,
14
+ )
15
+ from .augments import (
16
+ AUGMENT_REGISTRY,
17
+ AugmentDescriptor,
18
+ augment_for,
19
+ augments_for_scope,
12
20
  )
13
21
  from .infra import (
14
22
  INFRA_REGISTRY,
@@ -29,10 +37,14 @@ from .loader import (
29
37
  from .operations import (
30
38
  add_infra,
31
39
  add_service,
40
+ add_service_augment,
41
+ add_workspace_augment,
32
42
  apply,
33
43
  available_infra,
44
+ available_service_augments,
34
45
  configured_infra,
35
46
  remove_infra,
47
+ remove_service_augment,
36
48
  validate,
37
49
  )
38
50
  from .presets import (
@@ -41,7 +53,7 @@ from .presets import (
41
53
  members_for_category,
42
54
  )
43
55
  from .renderer import render_workspace
44
- from .scaffolder import scaffold_service, scaffold_services
56
+ from .scaffolder import scaffold_augment, scaffold_service, scaffold_services
45
57
  from .yaml_editor import (
46
58
  deep_merge,
47
59
  dumps_yaml,
@@ -52,6 +64,7 @@ from .yaml_editor import (
52
64
  )
53
65
 
54
66
  __all__ = [
67
+ "AUGMENT_REGISTRY",
55
68
  "INFRA_ALL_TYPES",
56
69
  "INFRA_CATEGORIES",
57
70
  "INFRA_DATABASES",
@@ -59,15 +72,23 @@ __all__ = [
59
72
  "INFRA_RENDERERS",
60
73
  "PRESET_REGISTRY",
61
74
  "SPEC_FILENAME",
75
+ "AugmentDescriptor",
62
76
  "ComposeSpec",
63
77
  "InfraDescriptor",
64
78
  "InfraNode",
65
79
  "PresetDefinition",
80
+ "ServiceAugment",
66
81
  "ServiceNode",
82
+ "WorkspaceAugment",
67
83
  "add_infra",
68
84
  "add_service",
85
+ "add_service_augment",
86
+ "add_workspace_augment",
69
87
  "apply",
88
+ "augment_for",
89
+ "augments_for_scope",
70
90
  "available_infra",
91
+ "available_service_augments",
71
92
  "configured_infra",
72
93
  "deep_merge",
73
94
  "default_spec",
@@ -81,10 +102,12 @@ __all__ = [
81
102
  "load_yaml",
82
103
  "members_for_category",
83
104
  "remove_infra",
105
+ "remove_service_augment",
84
106
  "render_infra",
85
107
  "render_workspace",
86
108
  "save_spec",
87
109
  "save_yaml",
110
+ "scaffold_augment",
88
111
  "scaffold_service",
89
112
  "scaffold_services",
90
113
  "set_key",
@@ -0,0 +1,166 @@
1
+ """Augment descriptors for application-level service capabilities.
2
+
3
+ Each augment is described by an ``AugmentDescriptor`` dataclass that
4
+ encapsulates everything the renderer and scaffolder need: prerequisite
5
+ checks, compose wiring, package dependencies, settings fields, and
6
+ template references. Adding a new augment requires only adding a new
7
+ descriptor to ``AUGMENT_REGISTRY``.
8
+
9
+ Mirrors the ``InfraDescriptor`` pattern in ``infra.py``.
10
+ """
11
+
12
+ from dataclasses import dataclass, field
13
+ from typing import Literal
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class AugmentDescriptor:
18
+ """Complete description of an augment capability.
19
+
20
+ Parameters
21
+ ----------
22
+ name
23
+ Unique augment identifier used in specs and registry lookups.
24
+ scope
25
+ ``"workspace"`` for cluster-wide facts, ``"service"`` for
26
+ per-service capabilities.
27
+ description
28
+ Human-readable description shown in menus and docs.
29
+ requires_infra
30
+ Infra feature categories that must be present in the spec
31
+ (e.g. ``["database"]``).
32
+ requires_workspace_augment
33
+ A workspace-scope augment that must exist for this augment to
34
+ be valid (e.g. ``"jwt-auth-provider"``).
35
+ requires_service_in_spec
36
+ A service role that must exist in the spec.
37
+ compose_env
38
+ Static compose environment variables contributed by this augment.
39
+ compose_depends_on
40
+ Static compose ``depends_on`` entries.
41
+ packages
42
+ Python package names added to the service's ``requirements.txt``.
43
+ settings_fields
44
+ Static settings fields (name → default value). Dynamic fields
45
+ are resolved in the renderer.
46
+ template_dir
47
+ Relative name under ``v2/templates/augments/`` for scaffolded
48
+ files. ``None`` for workspace-scope augments that don't scaffold.
49
+ conflicts_with
50
+ Augment names that cannot coexist with this one.
51
+ needs_target_selection
52
+ Whether the wizard should prompt the user to pick target
53
+ services from the spec (e.g. delegate targets).
54
+ """
55
+
56
+ name: str
57
+ scope: Literal["workspace", "service"]
58
+ description: str
59
+
60
+ # Prerequisites
61
+ requires_infra: list[str] = field(default_factory=list)
62
+ requires_workspace_augment: str | None = None
63
+ requires_service_in_spec: str | None = None
64
+
65
+ # What it contributes to compose (static values only)
66
+ compose_env: dict[str, str] = field(default_factory=dict)
67
+ compose_depends_on: list[str] = field(default_factory=list)
68
+
69
+ # What it contributes to scaffolded code
70
+ packages: list[str] = field(default_factory=list)
71
+ settings_fields: dict[str, str] = field(default_factory=dict)
72
+ template_dir: str | None = None
73
+
74
+ # Conflict prevention
75
+ conflicts_with: list[str] = field(default_factory=list)
76
+
77
+ # Does this augment resolve values dynamically from the spec?
78
+ needs_target_selection: bool = False
79
+
80
+ # For workspace-scope augments: scaffold template files into the
81
+ # service with this role. ``None`` means no scaffolding.
82
+ applies_to_role: str | None = None
83
+
84
+
85
+ # ---------------------------------------------------------------------------
86
+ # Registry
87
+ # ---------------------------------------------------------------------------
88
+
89
+ AUGMENT_REGISTRY: dict[str, AugmentDescriptor] = {
90
+ # ── Workspace-scope ────────────────────────────────────────────
91
+ "jwt-auth-provider": AugmentDescriptor(
92
+ name="jwt-auth-provider",
93
+ scope="workspace",
94
+ description="Cluster provides JWT authentication via an auth service",
95
+ requires_infra=["database"],
96
+ packages=[
97
+ "csrd-auth",
98
+ "csrd-models",
99
+ "csrd-repository",
100
+ "csrd-migration",
101
+ "PyJWT[crypto]",
102
+ ],
103
+ template_dir="jwt-auth-provider",
104
+ applies_to_role="auth",
105
+ ),
106
+ # ── Service-scope ──────────────────────────────────────────────
107
+ "db-config": AugmentDescriptor(
108
+ name="db-config",
109
+ scope="service",
110
+ description="Database repository with migrations and compose wiring",
111
+ requires_infra=["database"],
112
+ packages=["csrd-repository", "csrd-migration"],
113
+ # settings_fields resolved dynamically per database type in renderer
114
+ template_dir="db-config",
115
+ ),
116
+ "delegate-config": AugmentDescriptor(
117
+ name="delegate-config",
118
+ scope="service",
119
+ description="Inter-service delegation with delegate classes",
120
+ packages=["csrd-delegate", "csrd-models"],
121
+ template_dir="delegate-config",
122
+ needs_target_selection=True,
123
+ # compose_env and settings_fields are dynamic per target
124
+ ),
125
+ "jwt-auth-consumer": AugmentDescriptor(
126
+ name="jwt-auth-consumer",
127
+ scope="service",
128
+ description="JWT token validation using cluster auth service",
129
+ requires_workspace_augment="jwt-auth-provider",
130
+ packages=["csrd-auth", "csrd-models"],
131
+ template_dir="jwt-auth-consumer",
132
+ # compose_env, compose_depends_on, settings_fields resolved from spec
133
+ ),
134
+ "context-logging": AugmentDescriptor(
135
+ name="context-logging",
136
+ scope="service",
137
+ description="Request context middleware and structured logging",
138
+ packages=["csrd-logging", "csrd-context"],
139
+ template_dir="context-logging",
140
+ ),
141
+ "service-layer": AugmentDescriptor(
142
+ name="service-layer",
143
+ scope="service",
144
+ description="BaseService + ServiceError hierarchy + exception handler",
145
+ packages=["csrd-service", "csrd-models"],
146
+ template_dir="service-layer",
147
+ ),
148
+ }
149
+
150
+
151
+ # ---------------------------------------------------------------------------
152
+ # Helpers
153
+ # ---------------------------------------------------------------------------
154
+
155
+
156
+ def augment_for(name: str) -> AugmentDescriptor:
157
+ """Look up an augment descriptor by name.
158
+
159
+ Raises ``KeyError`` if the name is not registered.
160
+ """
161
+ return AUGMENT_REGISTRY[name]
162
+
163
+
164
+ def augments_for_scope(scope: Literal["workspace", "service"]) -> list[AugmentDescriptor]:
165
+ """Return all augment descriptors matching the given scope."""
166
+ return [d for d in AUGMENT_REGISTRY.values() if d.scope == scope]
@@ -7,8 +7,6 @@ volumes, service environment wiring, depends_on entries, and
7
7
  a new descriptor to ``INFRA_REGISTRY``.
8
8
  """
9
9
 
10
- from __future__ import annotations
11
-
12
10
  from dataclasses import dataclass, field
13
11
  from typing import Any
14
12
 
@@ -0,0 +1,358 @@
1
+ """Workspace mutation operations: add/remove services and infra.
2
+
3
+ These functions load, mutate, and persist the compose spec. They are
4
+ the canonical way to change a workspace's ``csrd-compose.yaml`` from
5
+ library code or CLI handlers.
6
+ """
7
+
8
+ from pathlib import Path
9
+
10
+ from ..models import (
11
+ INFRA_ALL_TYPES,
12
+ INFRA_DATABASES,
13
+ ComposeSpec,
14
+ InfraNode,
15
+ ServiceAugment,
16
+ ServiceNode,
17
+ WorkspaceAugment,
18
+ )
19
+ from .augments import AUGMENT_REGISTRY, augment_for
20
+ from .infra import INFRA_REGISTRY
21
+ from .loader import load_spec, save_spec, spec_file_path
22
+ from .renderer import render_workspace
23
+
24
+
25
+ def apply(output_dir: Path, *, git_init: bool = False) -> Path:
26
+ """Apply baseline empty-workspace rendering into *output_dir*."""
27
+
28
+ return render_workspace(output_dir, git_init=git_init)
29
+
30
+
31
+ def validate(output_dir: Path) -> ComposeSpec:
32
+ """Validate and return the canonical compose spec for a workspace."""
33
+
34
+ return load_spec(spec_file_path(output_dir))
35
+
36
+
37
+ def add_service(output_dir: Path, service: ServiceNode) -> ComposeSpec:
38
+ """Append a service to the workspace spec and persist to disk.
39
+
40
+ Raises ``ValueError`` if *service.name* already exists.
41
+ Returns the updated spec.
42
+ """
43
+
44
+ spec_path = spec_file_path(output_dir)
45
+ spec = load_spec(spec_path)
46
+
47
+ existing_names = {s.name for s in spec.services}
48
+ if service.name in existing_names:
49
+ raise ValueError(f"Service '{service.name}' already exists in workspace spec")
50
+
51
+ spec.services.append(service)
52
+ save_spec(spec, spec_path)
53
+ return spec
54
+
55
+
56
+ def add_infra(output_dir: Path, infra: InfraNode) -> ComposeSpec:
57
+ """Add or replace an infrastructure entry in the workspace spec.
58
+
59
+ Database types are mutually exclusive — adding one replaces any existing
60
+ database entry. Other infra types (redis, rabbitmq) are singletons that
61
+ cannot be duplicated.
62
+
63
+ Returns the updated spec.
64
+ """
65
+
66
+ spec_path = spec_file_path(output_dir)
67
+ spec = load_spec(spec_path)
68
+
69
+ existing_types = {i.type for i in spec.infra}
70
+
71
+ if infra.type in INFRA_DATABASES:
72
+ # Remove any existing database entry (mutually exclusive)
73
+ spec.infra = [i for i in spec.infra if i.type not in INFRA_DATABASES]
74
+ elif infra.type in existing_types:
75
+ raise ValueError(f"Infra '{infra.type}' already exists in workspace spec")
76
+
77
+ spec.infra.append(infra)
78
+ save_spec(spec, spec_path)
79
+ return spec
80
+
81
+
82
+ def remove_infra(output_dir: Path, infra_type: str) -> ComposeSpec:
83
+ """Remove an infrastructure entry from the workspace spec.
84
+
85
+ Raises ``ValueError`` if *infra_type* is not currently configured.
86
+ Returns the updated spec.
87
+ """
88
+
89
+ spec_path = spec_file_path(output_dir)
90
+ spec = load_spec(spec_path)
91
+
92
+ existing_types = {i.type for i in spec.infra}
93
+ if infra_type not in existing_types:
94
+ raise ValueError(f"Infra '{infra_type}' is not configured in workspace spec")
95
+
96
+ spec.infra = [i for i in spec.infra if i.type != infra_type]
97
+ save_spec(spec, spec_path)
98
+ return spec
99
+
100
+
101
+ def available_infra(workspace_dir: Path) -> list[str]:
102
+ """Return infra types not yet present in the workspace spec.
103
+
104
+ Database types are mutually exclusive — if any database is already
105
+ configured, all database options are excluded.
106
+ """
107
+
108
+ spec_path = spec_file_path(workspace_dir)
109
+ if not spec_path.is_file():
110
+ return sorted(INFRA_ALL_TYPES)
111
+
112
+ spec = load_spec(spec_path)
113
+ existing = {i.type for i in spec.infra}
114
+
115
+ available: list[str] = []
116
+ has_db = bool(existing & INFRA_DATABASES)
117
+ for infra_type in sorted(INFRA_ALL_TYPES):
118
+ if infra_type in existing:
119
+ continue
120
+ if has_db and infra_type in INFRA_DATABASES:
121
+ continue
122
+ available.append(infra_type)
123
+ return available
124
+
125
+
126
+ def configured_infra(workspace_dir: Path) -> list[str]:
127
+ """Return infra types currently configured in the workspace spec."""
128
+
129
+ spec_path = spec_file_path(workspace_dir)
130
+ if not spec_path.is_file():
131
+ return []
132
+
133
+ spec = load_spec(spec_path)
134
+ return sorted(i.type for i in spec.infra)
135
+
136
+
137
+ # ---------------------------------------------------------------------------
138
+ # Augment operations
139
+ # ---------------------------------------------------------------------------
140
+
141
+
142
+ def _infra_features_in_spec(spec: ComposeSpec) -> set[str]:
143
+ """Return the set of infra feature categories present in the spec."""
144
+ features: set[str] = set()
145
+ for infra_node in spec.infra:
146
+ desc = INFRA_REGISTRY.get(infra_node.type)
147
+ if desc is not None:
148
+ features.add(desc.feature)
149
+ return features
150
+
151
+
152
+ def _validate_augment_prerequisites(
153
+ augment_name: str,
154
+ spec: ComposeSpec,
155
+ *,
156
+ service_name: str | None = None,
157
+ ) -> None:
158
+ """Validate that all prerequisites for an augment are met.
159
+
160
+ Raises ``ValueError`` with a descriptive message on failure.
161
+ """
162
+ desc = augment_for(augment_name)
163
+
164
+ # Check required infra features
165
+ if desc.requires_infra:
166
+ spec_features = _infra_features_in_spec(spec)
167
+ missing = set(desc.requires_infra) - spec_features
168
+ if missing:
169
+ raise ValueError(
170
+ f"Augment '{augment_name}' requires infra features "
171
+ f"{sorted(missing)}, but they are not configured. "
172
+ f"Add the required infra first."
173
+ )
174
+
175
+ # Check required workspace augment
176
+ if desc.requires_workspace_augment:
177
+ ws_augment_names = {a.name for a in spec.workspace.augments}
178
+ if desc.requires_workspace_augment not in ws_augment_names:
179
+ raise ValueError(
180
+ f"Augment '{augment_name}' requires workspace augment "
181
+ f"'{desc.requires_workspace_augment}', but it is not present. "
182
+ f"Add the workspace augment first."
183
+ )
184
+
185
+ # Check required service role in spec
186
+ if desc.requires_service_in_spec:
187
+ roles = {s.role for s in spec.services}
188
+ if desc.requires_service_in_spec not in roles:
189
+ raise ValueError(
190
+ f"Augment '{augment_name}' requires a service with role "
191
+ f"'{desc.requires_service_in_spec}' in the spec."
192
+ )
193
+
194
+ # Check conflicts (only relevant for service-scope augments)
195
+ if service_name is not None and desc.conflicts_with:
196
+ svc = next((s for s in spec.services if s.name == service_name), None)
197
+ if svc is not None:
198
+ existing = {a.name for a in svc.augments}
199
+ conflicts = set(desc.conflicts_with) & existing
200
+ if conflicts:
201
+ raise ValueError(
202
+ f"Augment '{augment_name}' conflicts with "
203
+ f"{sorted(conflicts)} on service '{service_name}'."
204
+ )
205
+
206
+
207
+ def add_workspace_augment(
208
+ output_dir: Path,
209
+ augment_name: str,
210
+ options: dict[str, str | list[str]] | None = None,
211
+ ) -> ComposeSpec:
212
+ """Add a workspace-scoped augment to the spec.
213
+
214
+ Validates that the augment exists in the registry and has scope
215
+ ``"workspace"``. Raises ``ValueError`` on validation failure.
216
+ Returns the updated spec.
217
+ """
218
+ spec_path = spec_file_path(output_dir)
219
+ spec = load_spec(spec_path)
220
+
221
+ # Validate augment exists and has correct scope
222
+ try:
223
+ desc = augment_for(augment_name)
224
+ except KeyError:
225
+ raise ValueError(f"Unknown augment '{augment_name}'") from None
226
+
227
+ if desc.scope != "workspace":
228
+ raise ValueError(
229
+ f"Augment '{augment_name}' has scope '{desc.scope}', "
230
+ f"not 'workspace'. Use add_service_augment() instead."
231
+ )
232
+
233
+ # Check for duplicates
234
+ existing = {a.name for a in spec.workspace.augments}
235
+ if augment_name in existing:
236
+ raise ValueError(f"Workspace augment '{augment_name}' is already present.")
237
+
238
+ _validate_augment_prerequisites(augment_name, spec)
239
+
240
+ spec.workspace.augments.append(WorkspaceAugment(name=augment_name, options=options or {}))
241
+ save_spec(spec, spec_path)
242
+ return spec
243
+
244
+
245
+ def add_service_augment(
246
+ output_dir: Path,
247
+ service_name: str,
248
+ augment_name: str,
249
+ options: dict[str, str | list[str]] | None = None,
250
+ ) -> ComposeSpec:
251
+ """Add a service-scoped augment to a specific service in the spec.
252
+
253
+ Validates prerequisites, scope, duplicates, and conflicts.
254
+ Raises ``ValueError`` on validation failure.
255
+ Returns the updated spec.
256
+ """
257
+ spec_path = spec_file_path(output_dir)
258
+ spec = load_spec(spec_path)
259
+
260
+ # Validate augment exists and has correct scope
261
+ try:
262
+ desc = augment_for(augment_name)
263
+ except KeyError:
264
+ raise ValueError(f"Unknown augment '{augment_name}'") from None
265
+
266
+ if desc.scope != "service":
267
+ raise ValueError(
268
+ f"Augment '{augment_name}' has scope '{desc.scope}', "
269
+ f"not 'service'. Use add_workspace_augment() instead."
270
+ )
271
+
272
+ # Find the target service
273
+ svc = next((s for s in spec.services if s.name == service_name), None)
274
+ if svc is None:
275
+ raise ValueError(f"Service '{service_name}' not found in workspace spec.")
276
+
277
+ # Check for duplicates
278
+ existing = {a.name for a in svc.augments}
279
+ if augment_name in existing:
280
+ raise ValueError(
281
+ f"Augment '{augment_name}' is already present on service '{service_name}'."
282
+ )
283
+
284
+ # Validate target selection requirement
285
+ if desc.needs_target_selection:
286
+ targets = (options or {}).get("targets", [])
287
+ if not targets:
288
+ raise ValueError(
289
+ f"Augment '{augment_name}' requires target selection. "
290
+ f'Pass options={{"targets": [...]}}.'
291
+ )
292
+
293
+ _validate_augment_prerequisites(augment_name, spec, service_name=service_name)
294
+
295
+ svc.augments.append(ServiceAugment(name=augment_name, options=options or {}))
296
+ save_spec(spec, spec_path)
297
+ return spec
298
+
299
+
300
+ def remove_service_augment(
301
+ output_dir: Path,
302
+ service_name: str,
303
+ augment_name: str,
304
+ ) -> ComposeSpec:
305
+ """Remove a service-scoped augment from a service.
306
+
307
+ Only updates the spec — scaffolded files are NOT removed (they may
308
+ contain user edits). Returns the updated spec.
309
+ Raises ``ValueError`` if the augment is not present.
310
+ """
311
+ spec_path = spec_file_path(output_dir)
312
+ spec = load_spec(spec_path)
313
+
314
+ svc = next((s for s in spec.services if s.name == service_name), None)
315
+ if svc is None:
316
+ raise ValueError(f"Service '{service_name}' not found in workspace spec.")
317
+
318
+ original_count = len(svc.augments)
319
+ svc.augments = [a for a in svc.augments if a.name != augment_name]
320
+
321
+ if len(svc.augments) == original_count:
322
+ raise ValueError(f"Augment '{augment_name}' is not present on service '{service_name}'.")
323
+
324
+ save_spec(spec, spec_path)
325
+ return spec
326
+
327
+
328
+ def available_service_augments(
329
+ output_dir: Path,
330
+ service_name: str,
331
+ ) -> list[str]:
332
+ """Return augment names that can be added to a service.
333
+
334
+ Filters out augments already present, those with unmet prerequisites,
335
+ and those with conflicts.
336
+ """
337
+ spec_path = spec_file_path(output_dir)
338
+ spec = load_spec(spec_path)
339
+
340
+ svc = next((s for s in spec.services if s.name == service_name), None)
341
+ if svc is None:
342
+ return []
343
+
344
+ existing = {a.name for a in svc.augments}
345
+ available: list[str] = []
346
+
347
+ for name, desc in sorted(AUGMENT_REGISTRY.items()):
348
+ if desc.scope != "service":
349
+ continue
350
+ if name in existing:
351
+ continue
352
+ try:
353
+ _validate_augment_prerequisites(name, spec, service_name=service_name)
354
+ except ValueError:
355
+ continue
356
+ available.append(name)
357
+
358
+ return available