redeploy 0.2.54__tar.gz → 0.2.56__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 (224) hide show
  1. {redeploy-0.2.54 → redeploy-0.2.56}/PKG-INFO +6 -6
  2. {redeploy-0.2.54 → redeploy-0.2.56}/README.md +5 -5
  3. {redeploy-0.2.54 → redeploy-0.2.56}/pyproject.toml +1 -1
  4. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/__init__.py +1 -1
  5. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/heal/runner.py +1 -0
  6. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/iac/docker_compose.py +74 -49
  7. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/iac/parsers/compose.py +21 -13
  8. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_heal.py +5 -5
  9. redeploy-0.2.56/redeploy/tests/test_heal_decider.py +91 -0
  10. redeploy-0.2.56/redeploy/tests/test_iac_compose_contract.py +181 -0
  11. redeploy-0.2.56/redeploy/tests/test_iac_docker_compose_contract.py +172 -0
  12. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy.egg-info/PKG-INFO +6 -6
  13. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy.egg-info/SOURCES.txt +5 -0
  14. redeploy-0.2.56/tests/test_heal_decider.py +63 -0
  15. redeploy-0.2.56/tests/test_heal_loop_detector.py +47 -0
  16. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/apply/__init__.py +0 -0
  17. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/apply/exceptions.py +0 -0
  18. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/apply/executor.py +0 -0
  19. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/apply/handlers.py +0 -0
  20. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/apply/progress.py +0 -0
  21. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/apply/rollback.py +0 -0
  22. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/apply/state.py +0 -0
  23. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/apply/state_apply.py +0 -0
  24. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/apply/utils/__init__.py +0 -0
  25. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/apply/utils/run_container_build.py +0 -0
  26. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/audit.py +0 -0
  27. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/blueprint/__init__.py +0 -0
  28. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/blueprint/extractor.py +0 -0
  29. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/blueprint/generators/__init__.py +0 -0
  30. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/blueprint/generators/docker_compose.py +0 -0
  31. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/blueprint/generators/migration.py +0 -0
  32. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/blueprint/sources/__init__.py +0 -0
  33. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/blueprint/sources/compose.py +0 -0
  34. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/blueprint/sources/hardware.py +0 -0
  35. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/blueprint/sources/infra.py +0 -0
  36. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/blueprint/sources/migration.py +0 -0
  37. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/__init__.py +0 -0
  38. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/__init__.py +0 -0
  39. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/audit.py +0 -0
  40. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/blueprint.py +0 -0
  41. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/bump_fix.py +0 -0
  42. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/detect.py +0 -0
  43. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/device_map.py +0 -0
  44. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/device_map_renderers.py +0 -0
  45. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/devices.py +0 -0
  46. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/diagnose.py +0 -0
  47. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/diff.py +0 -0
  48. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/exec_.py +0 -0
  49. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/export.py +0 -0
  50. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/hardware.py +0 -0
  51. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/import_.py +0 -0
  52. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/init.py +0 -0
  53. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/inspect.py +0 -0
  54. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/mcp_cmd.py +0 -0
  55. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/patterns.py +0 -0
  56. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/plan_apply.py +0 -0
  57. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/plugin.py +0 -0
  58. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/probe.py +0 -0
  59. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/prompt_cmd.py +0 -0
  60. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/push.py +0 -0
  61. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/state.py +0 -0
  62. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/status.py +0 -0
  63. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/target.py +0 -0
  64. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/version/__init__.py +0 -0
  65. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/version/commands.py +0 -0
  66. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/version/helpers.py +0 -0
  67. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/version/monorepo.py +0 -0
  68. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/version/release.py +0 -0
  69. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/version/scanner.py +0 -0
  70. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/workflow.py +0 -0
  71. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/core.py +0 -0
  72. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/display.py +0 -0
  73. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/query.py +0 -0
  74. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli.py +0 -0
  75. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/config_apply/__init__.py +0 -0
  76. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/config_apply/applier.py +0 -0
  77. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/config_apply/loader.py +0 -0
  78. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/data_sync.py +0 -0
  79. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/detect/__init__.py +0 -0
  80. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/detect/builtin/__init__.py +0 -0
  81. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/detect/detector.py +0 -0
  82. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/detect/hardware.py +0 -0
  83. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/detect/hardware_rules.py +0 -0
  84. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/detect/probes.py +0 -0
  85. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/detect/remote.py +0 -0
  86. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/detect/templates.py +0 -0
  87. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/detect/workflow.py +0 -0
  88. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/discovery.py +0 -0
  89. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/dsl/__init__.py +0 -0
  90. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/dsl/loader.py +0 -0
  91. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/dsl/parser.py +0 -0
  92. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/dsl_python/__init__.py +0 -0
  93. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/dsl_python/context.py +0 -0
  94. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/dsl_python/decorators.py +0 -0
  95. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/dsl_python/docker_steps.py +0 -0
  96. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/dsl_python/exceptions.py +0 -0
  97. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/dsl_python/runner.py +0 -0
  98. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/dsl_python/steps.py +0 -0
  99. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/fleet.py +0 -0
  100. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/hardware/__init__.py +0 -0
  101. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/hardware/config_txt.py +0 -0
  102. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/hardware/data/__init__.py +0 -0
  103. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/hardware/data/hyperpixel.py +0 -0
  104. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/hardware/data/official.py +0 -0
  105. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/hardware/data/waveshare.py +0 -0
  106. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/hardware/fixes.py +0 -0
  107. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/hardware/kiosk/__init__.py +0 -0
  108. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/hardware/kiosk/autostart.py +0 -0
  109. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/hardware/kiosk/browsers.py +0 -0
  110. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/hardware/kiosk/compositors.py +0 -0
  111. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/hardware/kiosk/output_profiles.py +0 -0
  112. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/hardware/panels.py +0 -0
  113. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/hardware/raspi_config.py +0 -0
  114. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/heal/__init__.py +0 -0
  115. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/heal/decider.py +0 -0
  116. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/heal/hint_provider.py +0 -0
  117. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/heal/log_writer.py +0 -0
  118. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/heal/loop_detector.py +0 -0
  119. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/heal.py +0 -0
  120. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/iac/__init__.py +0 -0
  121. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/iac/base.py +0 -0
  122. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/iac/config_hints.py +0 -0
  123. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/iac/parsers/__init__.py +0 -0
  124. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/iac/registry.py +0 -0
  125. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/integrations/__init__.py +0 -0
  126. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/integrations/op3_bridge.py +0 -0
  127. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/markpact/__init__.py +0 -0
  128. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/markpact/compiler.py +0 -0
  129. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/markpact/models.py +0 -0
  130. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/markpact/parser.py +0 -0
  131. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/mcp_server.py +0 -0
  132. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/models.py +0 -0
  133. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/observe.py +0 -0
  134. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/parse.py +0 -0
  135. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/patterns.py +0 -0
  136. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/plan/__init__.py +0 -0
  137. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/plan/planner.py +0 -0
  138. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/plugins/__init__.py +0 -0
  139. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/plugins/builtin/__init__.py +0 -0
  140. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/plugins/builtin/browser_reload.py +0 -0
  141. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/plugins/builtin/hardware_diagnostic.py +0 -0
  142. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/plugins/builtin/notify.py +0 -0
  143. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/plugins/builtin/process_control.py +0 -0
  144. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/plugins/builtin/systemd_reload.py +0 -0
  145. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/schema.py +0 -0
  146. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/spec_loader.py +0 -0
  147. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/ssh.py +0 -0
  148. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/steps/__init__.py +0 -0
  149. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/steps/builtins.py +0 -0
  150. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/steps/kiosk.py +0 -0
  151. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/__init__.py +0 -0
  152. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_audit.py +0 -0
  153. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_cli.py +0 -0
  154. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_data_sync.py +0 -0
  155. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_discovery.py +0 -0
  156. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_dsl.py +0 -0
  157. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_examples.py +0 -0
  158. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_executor.py +0 -0
  159. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_fleet.py +0 -0
  160. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_git_integration.py +0 -0
  161. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_hooks.py +0 -0
  162. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_iac.py +0 -0
  163. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_manifest.py +0 -0
  164. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_markpact_compiler.py +0 -0
  165. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_markpact_parser.py +0 -0
  166. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_models.py +0 -0
  167. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_observe.py +0 -0
  168. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_parse.py +0 -0
  169. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_patterns.py +0 -0
  170. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_planner_edge.py +0 -0
  171. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_plugins.py +0 -0
  172. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_probes.py +0 -0
  173. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_public_api.py +0 -0
  174. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_resume.py +0 -0
  175. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_spec_loader.py +0 -0
  176. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_ssh.py +0 -0
  177. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_templates.py +0 -0
  178. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_verify.py +0 -0
  179. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_version.py +0 -0
  180. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_version_changelog.py +0 -0
  181. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_version_cli.py +0 -0
  182. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_version_manifest.py +0 -0
  183. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_version_sources.py +0 -0
  184. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_workflow.py +0 -0
  185. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/verify.py +0 -0
  186. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/version/__init__.py +0 -0
  187. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/version/bump.py +0 -0
  188. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/version/changelog.py +0 -0
  189. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/version/commits.py +0 -0
  190. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/version/diff.py +0 -0
  191. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/version/git_integration.py +0 -0
  192. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/version/git_transaction.py +0 -0
  193. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/version/manifest.py +0 -0
  194. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/version/sources/__init__.py +0 -0
  195. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/version/sources/base.py +0 -0
  196. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/version/sources/json_.py +0 -0
  197. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/version/sources/plain.py +0 -0
  198. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/version/sources/regex.py +0 -0
  199. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/version/sources/toml_.py +0 -0
  200. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/version/sources/yaml_.py +0 -0
  201. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/version/transaction.py +0 -0
  202. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/version.py +0 -0
  203. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy.egg-info/dependency_links.txt +0 -0
  204. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy.egg-info/entry_points.txt +0 -0
  205. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy.egg-info/requires.txt +0 -0
  206. {redeploy-0.2.54 → redeploy-0.2.56}/redeploy.egg-info/top_level.txt +0 -0
  207. {redeploy-0.2.54 → redeploy-0.2.56}/setup.cfg +0 -0
  208. {redeploy-0.2.54 → redeploy-0.2.56}/tests/test_blueprint_sources.py +0 -0
  209. {redeploy-0.2.54 → redeploy-0.2.56}/tests/test_blueprint_sources_compose.py +0 -0
  210. {redeploy-0.2.54 → redeploy-0.2.56}/tests/test_cli_query.py +0 -0
  211. {redeploy-0.2.54 → redeploy-0.2.56}/tests/test_example_argocd_flux_plugin.py +0 -0
  212. {redeploy-0.2.54 → redeploy-0.2.56}/tests/test_example_gitops_ci_plugin.py +0 -0
  213. {redeploy-0.2.54 → redeploy-0.2.56}/tests/test_example_helm_kustomize_plugin.py +0 -0
  214. {redeploy-0.2.54 → redeploy-0.2.56}/tests/test_hardware_config.py +0 -0
  215. {redeploy-0.2.54 → redeploy-0.2.56}/tests/test_hardware_kiosk.py +0 -0
  216. {redeploy-0.2.54 → redeploy-0.2.56}/tests/test_hardware_rules.py +0 -0
  217. {redeploy-0.2.54 → redeploy-0.2.56}/tests/test_iac.py +0 -0
  218. {redeploy-0.2.54 → redeploy-0.2.56}/tests/test_iac_config_hints.py +0 -0
  219. {redeploy-0.2.54 → redeploy-0.2.56}/tests/test_iac_plugin_loading.py +0 -0
  220. {redeploy-0.2.54 → redeploy-0.2.56}/tests/test_import_plugin_template.py +0 -0
  221. {redeploy-0.2.54 → redeploy-0.2.56}/tests/test_parse.py +0 -0
  222. {redeploy-0.2.54 → redeploy-0.2.56}/tests/test_prompt_llm.py +0 -0
  223. {redeploy-0.2.54 → redeploy-0.2.56}/tests/test_redeploy.py +0 -0
  224. {redeploy-0.2.54 → redeploy-0.2.56}/tests/test_steps_kiosk.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redeploy
3
- Version: 0.2.54
3
+ Version: 0.2.56
4
4
  Summary: Infrastructure migration toolkit: detect → plan → apply
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -32,17 +32,17 @@ Requires-Dist: mcp>=1.0; extra == "mcp"
32
32
 
33
33
  ## AI Cost Tracking
34
34
 
35
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.54-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
36
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-28.3h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
35
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.56-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
36
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-28.6h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
37
37
 
38
- - 🤖 **LLM usage:** $7.5000 (74 commits)
39
- - 👤 **Human dev:** ~$2829 (28.3h @ $100/h, 30min dedup)
38
+ - 🤖 **LLM usage:** $7.5000 (76 commits)
39
+ - 👤 **Human dev:** ~$2862 (28.6h @ $100/h, 30min dedup)
40
40
 
41
41
  Generated on 2026-04-21 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
42
42
 
43
43
  ---
44
44
 
45
- ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.54-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
45
+ ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.56-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
46
46
 
47
47
  Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
48
48
 
@@ -3,17 +3,17 @@
3
3
 
4
4
  ## AI Cost Tracking
5
5
 
6
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.54-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-28.3h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
6
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.56-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-28.6h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $7.5000 (74 commits)
10
- - 👤 **Human dev:** ~$2829 (28.3h @ $100/h, 30min dedup)
9
+ - 🤖 **LLM usage:** $7.5000 (76 commits)
10
+ - 👤 **Human dev:** ~$2862 (28.6h @ $100/h, 30min dedup)
11
11
 
12
12
  Generated on 2026-04-21 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
13
13
 
14
14
  ---
15
15
 
16
- ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.54-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
16
+ ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.56-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
17
17
 
18
18
  Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
19
19
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "redeploy"
7
- version = "0.2.54"
7
+ version = "0.2.56"
8
8
  description = "Infrastructure migration toolkit: detect → plan → apply"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -7,7 +7,7 @@ Public API (stable from 0.2.0, semver guaranteed)::
7
7
 
8
8
  Everything not listed in ``__all__`` is internal and may change without notice.
9
9
  """
10
- __version__ = "0.2.54"
10
+ __version__ = "0.2.56"
11
11
 
12
12
  # ── Core models ───────────────────────────────────────────────────────────────
13
13
  from .models import ( # noqa: F401
@@ -1,6 +1,7 @@
1
1
  """Self-healing runner — orchestrates detect → LLM fix → retry loop."""
2
2
  from __future__ import annotations
3
3
 
4
+ import re
4
5
  from pathlib import Path
5
6
  from typing import Optional
6
7
 
@@ -291,14 +291,12 @@ class DockerComposeParser:
291
291
  pass
292
292
  return env
293
293
 
294
- def _parse_service(self, name: str, cfg: dict,
295
- env_context: dict[str, str],
296
- spec: ParsedSpec) -> ServiceInfo:
297
- def resolve(v: object) -> str:
298
- return _resolve_var(str(v), env_context) if v is not None else ""
299
-
300
- # image / build
301
- image = resolve(cfg.get("image")) or None
294
+ def _resolve_image_and_build(
295
+ self,
296
+ cfg: dict,
297
+ env_context: dict[str, str],
298
+ ) -> tuple[Optional[str], Optional[str]]:
299
+ image = _resolve_var(str(cfg.get("image")), env_context) if cfg.get("image") is not None else None
302
300
  build = cfg.get("build")
303
301
  build_context: Optional[str] = None
304
302
  if build:
@@ -308,8 +306,15 @@ class DockerComposeParser:
308
306
  build_context = build.get("context", ".")
309
307
  if not image:
310
308
  image = f"<build:{build_context or '.'}>"
311
-
312
- # ports
309
+ return image, build_context
310
+
311
+ def _parse_service_ports(
312
+ self,
313
+ name: str,
314
+ cfg: dict,
315
+ env_context: dict[str, str],
316
+ spec: ParsedSpec,
317
+ ) -> list[PortInfo]:
313
318
  ports: list[PortInfo] = []
314
319
  for raw in (cfg.get("ports") or []):
315
320
  if isinstance(raw, str):
@@ -318,75 +323,95 @@ class DockerComposeParser:
318
323
  if p:
319
324
  ports.append(p)
320
325
  else:
321
- spec.add_warning(f"[{name}] Could not parse port: {raw!r}",
322
- severity="warn")
326
+ spec.add_warning(f"[{name}] Could not parse port: {raw!r}", severity="warn")
327
+ return ports
323
328
 
324
- # volumes
329
+ def _parse_service_volumes(self, cfg: dict) -> list[VolumeInfo]:
325
330
  volumes: list[VolumeInfo] = []
326
331
  for raw in (cfg.get("volumes") or []):
327
332
  v = _parse_volume(raw)
328
333
  if v:
329
334
  volumes.append(v)
335
+ return volumes
330
336
 
331
- # environment
337
+ def _parse_service_env(
338
+ self,
339
+ cfg: dict,
340
+ env_context: dict[str, str],
341
+ ) -> dict[str, str]:
332
342
  env = _env_dict(cfg.get("environment"))
333
- # resolve variables
334
- env = {k: _resolve_var(v, env_context) for k, v in env.items()}
343
+ return {k: _resolve_var(v, env_context) for k, v in env.items()}
335
344
 
336
- # env_file
337
- env_files: list[str] = []
345
+ def _parse_service_env_files(self, cfg: dict) -> list[str]:
338
346
  ef = cfg.get("env_file")
339
- if ef:
340
- if isinstance(ef, str):
341
- env_files = [ef]
342
- elif isinstance(ef, list):
343
- env_files = [str(x) for x in ef]
344
-
345
- # networks
347
+ if not ef:
348
+ return []
349
+ if isinstance(ef, str):
350
+ return [ef]
351
+ if isinstance(ef, list):
352
+ return [str(x) for x in ef]
353
+ return []
354
+
355
+ def _parse_service_networks(self, cfg: dict) -> list[str]:
346
356
  svc_nets = cfg.get("networks")
347
357
  if isinstance(svc_nets, dict):
348
- svc_networks = list(svc_nets.keys())
349
- elif isinstance(svc_nets, list):
350
- svc_networks = [str(n) for n in svc_nets]
351
- else:
352
- svc_networks = []
358
+ return list(svc_nets.keys())
359
+ if isinstance(svc_nets, list):
360
+ return [str(n) for n in svc_nets]
361
+ return []
353
362
 
354
- # depends_on
363
+ def _parse_service_depends_on(self, cfg: dict) -> list[str]:
355
364
  dep = cfg.get("depends_on")
356
365
  if isinstance(dep, list):
357
- depends_on = [str(d) for d in dep]
358
- elif isinstance(dep, dict):
359
- depends_on = list(dep.keys())
360
- else:
361
- depends_on = []
366
+ return [str(d) for d in dep]
367
+ if isinstance(dep, dict):
368
+ return list(dep.keys())
369
+ return []
362
370
 
363
- # healthcheck
371
+ def _parse_service_healthcheck(self, cfg: dict) -> Optional[str]:
364
372
  hc = cfg.get("healthcheck")
365
- healthcheck: Optional[str] = None
366
373
  if isinstance(hc, dict):
367
374
  test = hc.get("test")
368
375
  if isinstance(test, list):
369
376
  test = " ".join(str(t) for t in test)
370
- healthcheck = str(test) if test else None
377
+ return str(test) if test else None
378
+ return None
371
379
 
372
- # deploy.replicas
380
+ def _parse_service_replicas(self, cfg: dict) -> int:
373
381
  deploy = cfg.get("deploy") or {}
374
- replicas = 1
375
382
  if isinstance(deploy, dict):
376
- replicas = int(deploy.get("replicas", 1))
383
+ return int(deploy.get("replicas", 1))
384
+ return 1
377
385
 
378
- # labels
386
+ def _parse_service_labels(self, cfg: dict) -> dict[str, str]:
379
387
  labels_raw = cfg.get("labels")
380
388
  if isinstance(labels_raw, dict):
381
- labels = {str(k): str(v) for k, v in labels_raw.items()}
382
- elif isinstance(labels_raw, list):
383
- labels = {}
389
+ return {str(k): str(v) for k, v in labels_raw.items()}
390
+ if isinstance(labels_raw, list):
391
+ labels: dict[str, str] = {}
384
392
  for item in labels_raw:
385
393
  if "=" in str(item):
386
394
  k, v = str(item).split("=", 1)
387
395
  labels[k] = v
388
- else:
389
- labels = {}
396
+ return labels
397
+ return {}
398
+
399
+ def _parse_service_command(self, cfg: dict) -> Optional[str]:
400
+ return str(cfg["command"]) if cfg.get("command") else None
401
+
402
+ def _parse_service(self, name: str, cfg: dict,
403
+ env_context: dict[str, str],
404
+ spec: ParsedSpec) -> ServiceInfo:
405
+ image, build_context = self._resolve_image_and_build(cfg, env_context)
406
+ ports = self._parse_service_ports(name, cfg, env_context, spec)
407
+ volumes = self._parse_service_volumes(cfg)
408
+ env = self._parse_service_env(cfg, env_context)
409
+ env_files = self._parse_service_env_files(cfg)
410
+ svc_networks = self._parse_service_networks(cfg)
411
+ depends_on = self._parse_service_depends_on(cfg)
412
+ healthcheck = self._parse_service_healthcheck(cfg)
413
+ replicas = self._parse_service_replicas(cfg)
414
+ labels = self._parse_service_labels(cfg)
390
415
 
391
416
  return ServiceInfo(
392
417
  name=name,
@@ -399,7 +424,7 @@ class DockerComposeParser:
399
424
  depends_on=depends_on,
400
425
  healthcheck=healthcheck,
401
426
  restart=cfg.get("restart"),
402
- command=str(cfg["command"]) if cfg.get("command") else None,
427
+ command=self._parse_service_command(cfg),
403
428
  build_context=build_context,
404
429
  replicas=replicas,
405
430
  labels=labels,
@@ -113,13 +113,29 @@ class DockerComposeParser:
113
113
 
114
114
  # ── service parsing ───────────────────────────────────────────────────────
115
115
 
116
+ def _collect_service_secrets(self, d: dict, spec: ParsedSpec) -> None:
117
+ for secret in (d.get("secrets") or []):
118
+ secret_name = secret if isinstance(secret, str) else secret.get("source", "")
119
+ if secret_name and secret_name not in spec.secrets_referenced:
120
+ spec.secrets_referenced.append(secret_name)
121
+
122
+ def _merge_service_env_files(self, d: dict, svc: ServiceInfo, spec: ParsedSpec) -> None:
123
+ for ef in (d.get("env_file") or []):
124
+ ef = ef if isinstance(ef, str) else ef.get("path", "")
125
+ if ef and ef not in spec.env_files:
126
+ spec.env_files.append(ef)
127
+ if ef and ef not in svc.env_files:
128
+ svc.env_files.append(ef)
129
+
130
+ def _collect_service_image(self, svc: ServiceInfo, spec: ParsedSpec) -> None:
131
+ if svc.image and svc.image not in spec.images:
132
+ spec.images.append(svc.image)
133
+
116
134
  def _parse_service(self, name: str, d: dict, spec: ParsedSpec) -> ServiceInfo:
117
135
  svc = ServiceInfo(name=name)
118
136
 
119
137
  svc.image = d.get("image") or None
120
- if svc.image:
121
- if svc.image not in spec.images:
122
- spec.images.append(svc.image)
138
+ self._collect_service_image(svc, spec)
123
139
 
124
140
  svc.build_context = self._parse_build(d.get("build"))
125
141
  svc.command = self._parse_command(d.get("command"))
@@ -135,18 +151,10 @@ class DockerComposeParser:
135
151
  svc.healthcheck = self._parse_healthcheck(d.get("healthcheck"))
136
152
 
137
153
  # secrets referenced by service
138
- for secret in (d.get("secrets") or []):
139
- secret_name = secret if isinstance(secret, str) else secret.get("source", "")
140
- if secret_name and secret_name not in spec.secrets_referenced:
141
- spec.secrets_referenced.append(secret_name)
154
+ self._collect_service_secrets(d, spec)
142
155
 
143
156
  # env_files at service level
144
- for ef in (d.get("env_file") or []):
145
- ef = ef if isinstance(ef, str) else ef.get("path", "")
146
- if ef and ef not in spec.env_files:
147
- spec.env_files.append(ef)
148
- if ef and ef not in svc.env_files:
149
- svc.env_files.append(ef)
157
+ self._merge_service_env_files(d, svc, spec)
150
158
 
151
159
  return svc
152
160
 
@@ -60,11 +60,11 @@ def test_heal_runner_stops_on_repeating_hint_pattern(monkeypatch, tmp_path):
60
60
  ask_calls.append(True)
61
61
  return "```yaml\n- id: sync_env\n action: ssh_cmd\n description: \"manual\"\n```"
62
62
 
63
- monkeypatch.setattr("redeploy.heal.parse_failed_step", lambda *_a, **_k: ("sync_env", "boom"))
64
- monkeypatch.setattr("redeploy.heal.collect_diagnostics", lambda *_a, **_k: "ERROR: same")
65
- monkeypatch.setattr("redeploy.heal.ask_llm", _fake_ask)
66
- monkeypatch.setattr("redeploy.heal.apply_fix_to_spec", lambda *_a, **_k: False)
67
- monkeypatch.setattr("redeploy.heal.write_repair_log", lambda *_a, **_k: None)
63
+ monkeypatch.setattr("redeploy.heal.runner.parse_failed_step", lambda *_a, **_k: ("sync_env", "boom"))
64
+ monkeypatch.setattr("redeploy.heal.runner.collect_diagnostics", lambda *_a, **_k: "ERROR: same")
65
+ monkeypatch.setattr("redeploy.heal.runner.ask_llm", _fake_ask)
66
+ monkeypatch.setattr("redeploy.heal.runner.apply_fix_to_spec", lambda *_a, **_k: True)
67
+ monkeypatch.setattr("redeploy.heal.runner.write_repair_log", lambda *_a, **_k: None)
68
68
  monkeypatch.setattr(HealRunner, "_make_executor", _fake_make_executor)
69
69
  monkeypatch.setattr(HealRunner, "_reload_migration", lambda *_a, **_k: None)
70
70
 
@@ -0,0 +1,91 @@
1
+ from redeploy.heal.decider import Action, Decision, decide_after_failure, format_decision_message
2
+
3
+
4
+ def test_decide_abort_on_loop_detected():
5
+ decision = decide_after_failure(
6
+ attempt=1,
7
+ max_retries=3,
8
+ failed_step="sync_env",
9
+ loop_detected=True,
10
+ llm_error=False,
11
+ spec_patched=False,
12
+ )
13
+ assert decision == Decision(Action.ABORT, "Loop detected: same hint for 'sync_env' ≥ 3 times in a row")
14
+
15
+
16
+ def test_decide_abort_when_failed_step_unknown():
17
+ decision = decide_after_failure(
18
+ attempt=1,
19
+ max_retries=3,
20
+ failed_step=None,
21
+ loop_detected=False,
22
+ llm_error=False,
23
+ spec_patched=False,
24
+ )
25
+ assert decision == Decision(Action.ABORT, "Cannot identify failed step — giving up")
26
+
27
+
28
+ def test_decide_retry_on_transient_llm_error_before_limit():
29
+ decision = decide_after_failure(
30
+ attempt=1,
31
+ max_retries=3,
32
+ failed_step="sync_env",
33
+ loop_detected=False,
34
+ llm_error=True,
35
+ spec_patched=False,
36
+ )
37
+ assert decision == Decision(Action.RETRY, "LLM error — retrying may help if transient")
38
+
39
+
40
+ def test_decide_abort_on_llm_error_at_retry_limit():
41
+ decision = decide_after_failure(
42
+ attempt=3,
43
+ max_retries=3,
44
+ failed_step="sync_env",
45
+ loop_detected=False,
46
+ llm_error=True,
47
+ spec_patched=False,
48
+ )
49
+ assert decision == Decision(Action.ABORT, "LLM error — retrying may help if transient")
50
+
51
+
52
+ def test_decide_abort_when_retry_budget_exhausted():
53
+ decision = decide_after_failure(
54
+ attempt=3,
55
+ max_retries=3,
56
+ failed_step="sync_env",
57
+ loop_detected=False,
58
+ llm_error=False,
59
+ spec_patched=False,
60
+ )
61
+ assert decision == Decision(Action.ABORT, "Max retries (3) exhausted for 'sync_env'")
62
+
63
+
64
+ def test_decide_retry_when_spec_was_patched():
65
+ decision = decide_after_failure(
66
+ attempt=1,
67
+ max_retries=3,
68
+ failed_step="sync_env",
69
+ loop_detected=False,
70
+ llm_error=False,
71
+ spec_patched=True,
72
+ )
73
+ assert decision == Decision(Action.RETRY, "Spec patched — re-running migration")
74
+
75
+
76
+ def test_decide_skip_when_patch_not_applicable():
77
+ decision = decide_after_failure(
78
+ attempt=1,
79
+ max_retries=3,
80
+ failed_step="sync_env",
81
+ loop_detected=False,
82
+ llm_error=False,
83
+ spec_patched=False,
84
+ )
85
+ assert decision == Decision(Action.SKIP, "LLM did not produce an applicable patch — skipping step")
86
+
87
+
88
+ def test_format_decision_message_uses_action_and_reason():
89
+ msg = format_decision_message(Decision(Action.RETRY, "Spec patched"), "sync_env")
90
+ assert "RETRY" in msg
91
+ assert "Spec patched" in msg
@@ -0,0 +1,181 @@
1
+ from __future__ import annotations
2
+
3
+ import textwrap
4
+ from dataclasses import asdict
5
+
6
+ from redeploy.iac.parsers.compose import DockerComposeParser
7
+
8
+
9
+ def test_compose_parse_contract_golden(tmp_path):
10
+ compose = tmp_path / "docker-compose.yml"
11
+ compose.write_text(
12
+ textwrap.dedent(
13
+ """
14
+ version: "3.9"
15
+ env_file:
16
+ - .env.root
17
+ services:
18
+ web:
19
+ image: nginx:1.27
20
+ command: ["nginx", "-g", "daemon off;"]
21
+ restart: unless-stopped
22
+ deploy:
23
+ replicas: 2
24
+ ports:
25
+ - "127.0.0.1:8080:80/tcp"
26
+ - target: 443
27
+ published: 8443
28
+ protocol: tcp
29
+ volumes:
30
+ - ./html:/usr/share/nginx/html:ro
31
+ - type: volume
32
+ source: cache
33
+ target: /var/cache/nginx
34
+ networks: [frontend, backend]
35
+ environment:
36
+ APP_ENV: prod
37
+ FEATURE_FLAG: "1"
38
+ env_file:
39
+ - .env.web
40
+ depends_on:
41
+ db:
42
+ condition: service_healthy
43
+ labels:
44
+ - traefik.enable=true
45
+ healthcheck:
46
+ test: ["CMD", "curl", "-f", "http://localhost/health"]
47
+ secrets:
48
+ - source: tls_cert
49
+
50
+ db:
51
+ image: postgres:16
52
+ build:
53
+ context: ./db
54
+ command: postgres -c max_connections=300
55
+ ports: ["5432"]
56
+ volumes:
57
+ - pgdata:/var/lib/postgresql/data
58
+ networks: [backend]
59
+ environment:
60
+ - POSTGRES_DB=app
61
+ - POSTGRES_PASSWORD=secret
62
+ env_file:
63
+ - .env.db
64
+ depends_on: [redis]
65
+ labels:
66
+ com.example.role: db
67
+ healthcheck: pg_isready -U postgres
68
+ secrets:
69
+ - db_password
70
+
71
+ networks:
72
+ frontend: {}
73
+ backend: {}
74
+
75
+ volumes:
76
+ pgdata: {}
77
+ cache: {}
78
+
79
+ secrets:
80
+ tls_cert:
81
+ file: ./secrets/tls.crt
82
+ db_password:
83
+ file: ./secrets/db_password.txt
84
+ """
85
+ )
86
+ )
87
+
88
+ spec = DockerComposeParser().parse(compose)
89
+
90
+ actual = {
91
+ "services": [asdict(svc) for svc in spec.services],
92
+ "images": spec.images,
93
+ "networks": spec.networks,
94
+ "env_files": spec.env_files,
95
+ "secrets": spec.secrets_referenced,
96
+ "runtime_hints": spec.runtime_hints,
97
+ }
98
+
99
+ expected = {
100
+ "services": [
101
+ {
102
+ "name": "web",
103
+ "image": "nginx:1.27",
104
+ "ports": [
105
+ {
106
+ "container": 80,
107
+ "host": 8080,
108
+ "protocol": "tcp",
109
+ "host_ip": "127.0.0.1",
110
+ },
111
+ {
112
+ "container": 443,
113
+ "host": 8443,
114
+ "protocol": "tcp",
115
+ "host_ip": "0.0.0.0",
116
+ },
117
+ ],
118
+ "volumes": [
119
+ {
120
+ "target": "/usr/share/nginx/html",
121
+ "source": "./html",
122
+ "source_type": "volume",
123
+ "read_only": True,
124
+ },
125
+ {
126
+ "target": "/var/cache/nginx",
127
+ "source": "cache",
128
+ "source_type": "volume",
129
+ "read_only": False,
130
+ },
131
+ ],
132
+ "env": {"APP_ENV": "prod", "FEATURE_FLAG": "1"},
133
+ "env_files": [".env.web"],
134
+ "networks": ["frontend", "backend"],
135
+ "depends_on": ["db"],
136
+ "healthcheck": "curl -f http://localhost/health",
137
+ "restart": "unless-stopped",
138
+ "command": "nginx -g daemon off;",
139
+ "build_context": None,
140
+ "replicas": 2,
141
+ "labels": {"traefik.enable": "true"},
142
+ },
143
+ {
144
+ "name": "db",
145
+ "image": "postgres:16",
146
+ "ports": [
147
+ {
148
+ "container": 5432,
149
+ "host": None,
150
+ "protocol": "tcp",
151
+ "host_ip": "0.0.0.0",
152
+ }
153
+ ],
154
+ "volumes": [
155
+ {
156
+ "target": "/var/lib/postgresql/data",
157
+ "source": "pgdata",
158
+ "source_type": "volume",
159
+ "read_only": False,
160
+ }
161
+ ],
162
+ "env": {"POSTGRES_DB": "app", "POSTGRES_PASSWORD": "secret"},
163
+ "env_files": [".env.db"],
164
+ "networks": ["backend"],
165
+ "depends_on": ["redis"],
166
+ "healthcheck": "pg_isready -U postgres",
167
+ "restart": None,
168
+ "command": "postgres -c max_connections=300",
169
+ "build_context": "./db",
170
+ "replicas": 1,
171
+ "labels": {"com.example.role": "db"},
172
+ },
173
+ ],
174
+ "images": ["nginx:1.27", "postgres:16"],
175
+ "networks": ["frontend", "backend"],
176
+ "env_files": [".env.web", ".env.db", ".env.root"],
177
+ "secrets": ["tls_cert", "db_password"],
178
+ "runtime_hints": ["compose-version: 3.9"],
179
+ }
180
+
181
+ assert actual == expected