redeploy 0.2.57__tar.gz → 0.2.59__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 (238) hide show
  1. {redeploy-0.2.57 → redeploy-0.2.59}/PKG-INFO +6 -6
  2. {redeploy-0.2.57 → redeploy-0.2.59}/README.md +5 -5
  3. {redeploy-0.2.57 → redeploy-0.2.59}/pyproject.toml +1 -1
  4. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/__init__.py +1 -1
  5. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/cli/commands/device_map_renderers.py +1 -1
  6. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/cli/commands/plan_apply.py +2 -2
  7. redeploy-0.2.59/redeploy/cli/commands/version/utils/__init__.py +8 -0
  8. redeploy-0.2.59/redeploy/cli/commands/version/utils/changelog_config.py +27 -0
  9. redeploy-0.2.59/redeploy/cli/commands/version/utils/git_config.py +27 -0
  10. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/cli/core.py +1 -1
  11. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/cli/display.py +1 -1
  12. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/detect/workflow.py +1 -1
  13. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/iac/docker_compose.py +37 -4
  14. redeploy-0.2.59/redeploy/models/__init__.py +121 -0
  15. redeploy-0.2.59/redeploy/models/blueprint.py +95 -0
  16. redeploy-0.2.59/redeploy/models/devices.py +170 -0
  17. redeploy-0.2.59/redeploy/models/hardware.py +117 -0
  18. redeploy-0.2.59/redeploy/models/infra.py +77 -0
  19. redeploy-0.2.59/redeploy/models/manifest.py +151 -0
  20. redeploy-0.2.59/redeploy/models/pipeline.py +35 -0
  21. redeploy-0.2.59/redeploy/models/plan.py +83 -0
  22. redeploy-0.2.59/redeploy/models/spec.py +182 -0
  23. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/plan/planner.py +1 -1
  24. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy.egg-info/PKG-INFO +6 -6
  25. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy.egg-info/SOURCES.txt +12 -1
  26. redeploy-0.2.57/redeploy/models.py +0 -1096
  27. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/apply/__init__.py +0 -0
  28. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/apply/exceptions.py +0 -0
  29. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/apply/executor.py +0 -0
  30. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/apply/handlers.py +0 -0
  31. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/apply/progress.py +0 -0
  32. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/apply/rollback.py +0 -0
  33. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/apply/state.py +0 -0
  34. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/apply/state_apply.py +0 -0
  35. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/apply/utils/__init__.py +0 -0
  36. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/apply/utils/run_container_build.py +0 -0
  37. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/audit.py +0 -0
  38. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/blueprint/__init__.py +0 -0
  39. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/blueprint/extractor.py +0 -0
  40. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/blueprint/generators/__init__.py +0 -0
  41. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/blueprint/generators/docker_compose.py +0 -0
  42. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/blueprint/generators/migration.py +0 -0
  43. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/blueprint/sources/__init__.py +0 -0
  44. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/blueprint/sources/compose.py +0 -0
  45. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/blueprint/sources/hardware.py +0 -0
  46. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/blueprint/sources/infra.py +0 -0
  47. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/blueprint/sources/migration.py +0 -0
  48. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/cli/__init__.py +0 -0
  49. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/cli/commands/__init__.py +0 -0
  50. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/cli/commands/audit.py +0 -0
  51. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/cli/commands/blueprint.py +0 -0
  52. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/cli/commands/bump_fix.py +0 -0
  53. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/cli/commands/detect.py +0 -0
  54. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/cli/commands/device_map.py +0 -0
  55. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/cli/commands/devices.py +0 -0
  56. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/cli/commands/diagnose.py +0 -0
  57. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/cli/commands/diff.py +0 -0
  58. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/cli/commands/exec_.py +0 -0
  59. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/cli/commands/export.py +0 -0
  60. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/cli/commands/hardware.py +0 -0
  61. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/cli/commands/import_.py +0 -0
  62. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/cli/commands/init.py +0 -0
  63. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/cli/commands/inspect.py +0 -0
  64. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/cli/commands/mcp_cmd.py +0 -0
  65. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/cli/commands/patterns.py +0 -0
  66. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/cli/commands/plugin.py +0 -0
  67. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/cli/commands/probe.py +0 -0
  68. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/cli/commands/prompt_cmd.py +0 -0
  69. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/cli/commands/push.py +0 -0
  70. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/cli/commands/state.py +0 -0
  71. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/cli/commands/status.py +0 -0
  72. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/cli/commands/target.py +0 -0
  73. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/cli/commands/version/__init__.py +0 -0
  74. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/cli/commands/version/commands.py +0 -0
  75. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/cli/commands/version/helpers.py +0 -0
  76. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/cli/commands/version/monorepo.py +0 -0
  77. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/cli/commands/version/release.py +0 -0
  78. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/cli/commands/version/scanner.py +0 -0
  79. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/cli/commands/workflow.py +0 -0
  80. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/cli/query.py +0 -0
  81. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/cli.py +0 -0
  82. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/config_apply/__init__.py +0 -0
  83. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/config_apply/applier.py +0 -0
  84. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/config_apply/loader.py +0 -0
  85. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/data_sync.py +0 -0
  86. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/detect/__init__.py +0 -0
  87. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/detect/builtin/__init__.py +0 -0
  88. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/detect/detector.py +0 -0
  89. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/detect/hardware.py +0 -0
  90. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/detect/hardware_rules.py +0 -0
  91. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/detect/probes.py +0 -0
  92. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/detect/remote.py +0 -0
  93. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/detect/templates.py +0 -0
  94. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/discovery.py +0 -0
  95. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/dsl/__init__.py +0 -0
  96. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/dsl/loader.py +0 -0
  97. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/dsl/parser.py +0 -0
  98. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/dsl_python/__init__.py +0 -0
  99. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/dsl_python/context.py +0 -0
  100. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/dsl_python/decorators.py +0 -0
  101. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/dsl_python/docker_steps.py +0 -0
  102. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/dsl_python/exceptions.py +0 -0
  103. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/dsl_python/runner.py +0 -0
  104. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/dsl_python/steps.py +0 -0
  105. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/fleet.py +0 -0
  106. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/hardware/__init__.py +0 -0
  107. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/hardware/config_txt.py +0 -0
  108. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/hardware/data/__init__.py +0 -0
  109. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/hardware/data/hyperpixel.py +0 -0
  110. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/hardware/data/official.py +0 -0
  111. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/hardware/data/waveshare.py +0 -0
  112. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/hardware/fixes.py +0 -0
  113. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/hardware/kiosk/__init__.py +0 -0
  114. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/hardware/kiosk/autostart.py +0 -0
  115. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/hardware/kiosk/browsers.py +0 -0
  116. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/hardware/kiosk/compositors.py +0 -0
  117. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/hardware/kiosk/output_profiles.py +0 -0
  118. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/hardware/panels.py +0 -0
  119. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/hardware/raspi_config.py +0 -0
  120. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/heal/__init__.py +0 -0
  121. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/heal/decider.py +0 -0
  122. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/heal/hint_provider.py +0 -0
  123. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/heal/log_writer.py +0 -0
  124. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/heal/loop_detector.py +0 -0
  125. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/heal/runner.py +0 -0
  126. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/heal.py +0 -0
  127. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/iac/__init__.py +0 -0
  128. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/iac/base.py +0 -0
  129. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/iac/config_hints.py +0 -0
  130. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/iac/parsers/__init__.py +0 -0
  131. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/iac/parsers/compose.py +0 -0
  132. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/iac/registry.py +0 -0
  133. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/integrations/__init__.py +0 -0
  134. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/integrations/op3_bridge.py +0 -0
  135. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/markpact/__init__.py +0 -0
  136. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/markpact/compiler.py +0 -0
  137. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/markpact/models.py +0 -0
  138. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/markpact/parser.py +0 -0
  139. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/mcp_server.py +0 -0
  140. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/models/enums.py +0 -0
  141. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/models/persisted.py +0 -0
  142. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/observe.py +0 -0
  143. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/parse.py +0 -0
  144. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/patterns.py +0 -0
  145. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/plan/__init__.py +0 -0
  146. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/plugins/__init__.py +0 -0
  147. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/plugins/builtin/__init__.py +0 -0
  148. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/plugins/builtin/browser_reload.py +0 -0
  149. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/plugins/builtin/hardware_diagnostic.py +0 -0
  150. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/plugins/builtin/notify.py +0 -0
  151. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/plugins/builtin/process_control.py +0 -0
  152. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/plugins/builtin/systemd_reload.py +0 -0
  153. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/schema.py +0 -0
  154. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/spec_loader.py +0 -0
  155. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/ssh.py +0 -0
  156. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/steps/__init__.py +0 -0
  157. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/steps/builtins.py +0 -0
  158. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/steps/kiosk.py +0 -0
  159. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/tests/__init__.py +0 -0
  160. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/tests/test_audit.py +0 -0
  161. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/tests/test_cli.py +0 -0
  162. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/tests/test_data_sync.py +0 -0
  163. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/tests/test_discovery.py +0 -0
  164. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/tests/test_dsl.py +0 -0
  165. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/tests/test_examples.py +0 -0
  166. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/tests/test_executor.py +0 -0
  167. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/tests/test_fleet.py +0 -0
  168. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/tests/test_git_integration.py +0 -0
  169. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/tests/test_heal.py +0 -0
  170. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/tests/test_heal_decider.py +0 -0
  171. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/tests/test_hooks.py +0 -0
  172. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/tests/test_iac.py +0 -0
  173. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/tests/test_iac_compose_contract.py +0 -0
  174. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/tests/test_iac_docker_compose_contract.py +0 -0
  175. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/tests/test_manifest.py +0 -0
  176. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/tests/test_markpact_compiler.py +0 -0
  177. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/tests/test_markpact_parser.py +0 -0
  178. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/tests/test_models.py +0 -0
  179. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/tests/test_observe.py +0 -0
  180. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/tests/test_parse.py +0 -0
  181. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/tests/test_patterns.py +0 -0
  182. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/tests/test_planner_edge.py +0 -0
  183. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/tests/test_plugins.py +0 -0
  184. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/tests/test_probes.py +0 -0
  185. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/tests/test_public_api.py +0 -0
  186. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/tests/test_resume.py +0 -0
  187. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/tests/test_spec_loader.py +0 -0
  188. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/tests/test_ssh.py +0 -0
  189. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/tests/test_templates.py +0 -0
  190. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/tests/test_verify.py +0 -0
  191. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/tests/test_version.py +0 -0
  192. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/tests/test_version_changelog.py +0 -0
  193. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/tests/test_version_cli.py +0 -0
  194. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/tests/test_version_manifest.py +0 -0
  195. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/tests/test_version_sources.py +0 -0
  196. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/tests/test_workflow.py +0 -0
  197. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/verify.py +0 -0
  198. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/version/__init__.py +0 -0
  199. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/version/bump.py +0 -0
  200. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/version/changelog.py +0 -0
  201. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/version/commits.py +0 -0
  202. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/version/diff.py +0 -0
  203. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/version/git_integration.py +0 -0
  204. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/version/git_transaction.py +0 -0
  205. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/version/manifest.py +0 -0
  206. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/version/sources/__init__.py +0 -0
  207. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/version/sources/base.py +0 -0
  208. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/version/sources/json_.py +0 -0
  209. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/version/sources/plain.py +0 -0
  210. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/version/sources/regex.py +0 -0
  211. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/version/sources/toml_.py +0 -0
  212. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/version/sources/yaml_.py +0 -0
  213. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/version/transaction.py +0 -0
  214. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy/version.py +0 -0
  215. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy.egg-info/dependency_links.txt +0 -0
  216. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy.egg-info/entry_points.txt +0 -0
  217. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy.egg-info/requires.txt +0 -0
  218. {redeploy-0.2.57 → redeploy-0.2.59}/redeploy.egg-info/top_level.txt +0 -0
  219. {redeploy-0.2.57 → redeploy-0.2.59}/setup.cfg +0 -0
  220. {redeploy-0.2.57 → redeploy-0.2.59}/tests/test_blueprint_sources.py +0 -0
  221. {redeploy-0.2.57 → redeploy-0.2.59}/tests/test_blueprint_sources_compose.py +0 -0
  222. {redeploy-0.2.57 → redeploy-0.2.59}/tests/test_cli_query.py +0 -0
  223. {redeploy-0.2.57 → redeploy-0.2.59}/tests/test_example_argocd_flux_plugin.py +0 -0
  224. {redeploy-0.2.57 → redeploy-0.2.59}/tests/test_example_gitops_ci_plugin.py +0 -0
  225. {redeploy-0.2.57 → redeploy-0.2.59}/tests/test_example_helm_kustomize_plugin.py +0 -0
  226. {redeploy-0.2.57 → redeploy-0.2.59}/tests/test_hardware_config.py +0 -0
  227. {redeploy-0.2.57 → redeploy-0.2.59}/tests/test_hardware_kiosk.py +0 -0
  228. {redeploy-0.2.57 → redeploy-0.2.59}/tests/test_hardware_rules.py +0 -0
  229. {redeploy-0.2.57 → redeploy-0.2.59}/tests/test_heal_decider.py +0 -0
  230. {redeploy-0.2.57 → redeploy-0.2.59}/tests/test_heal_loop_detector.py +0 -0
  231. {redeploy-0.2.57 → redeploy-0.2.59}/tests/test_iac.py +0 -0
  232. {redeploy-0.2.57 → redeploy-0.2.59}/tests/test_iac_config_hints.py +0 -0
  233. {redeploy-0.2.57 → redeploy-0.2.59}/tests/test_iac_plugin_loading.py +0 -0
  234. {redeploy-0.2.57 → redeploy-0.2.59}/tests/test_import_plugin_template.py +0 -0
  235. {redeploy-0.2.57 → redeploy-0.2.59}/tests/test_parse.py +0 -0
  236. {redeploy-0.2.57 → redeploy-0.2.59}/tests/test_prompt_llm.py +0 -0
  237. {redeploy-0.2.57 → redeploy-0.2.59}/tests/test_redeploy.py +0 -0
  238. {redeploy-0.2.57 → redeploy-0.2.59}/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.57
3
+ Version: 0.2.59
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.57-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.9h-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.59-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-29.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
37
37
 
38
- - 🤖 **LLM usage:** $7.5000 (77 commits)
39
- - 👤 **Human dev:** ~$2886 (28.9h @ $100/h, 30min dedup)
38
+ - 🤖 **LLM usage:** $7.5000 (79 commits)
39
+ - 👤 **Human dev:** ~$2897 (29.0h @ $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.57-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.59-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.57-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.9h-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.59-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-29.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $7.5000 (77 commits)
10
- - 👤 **Human dev:** ~$2886 (28.9h @ $100/h, 30min dedup)
9
+ - 🤖 **LLM usage:** $7.5000 (79 commits)
10
+ - 👤 **Human dev:** ~$2897 (29.0h @ $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.57-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.59-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.57"
7
+ version = "0.2.59"
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.57"
10
+ __version__ = "0.2.59"
11
11
 
12
12
  # ── Core models ───────────────────────────────────────────────────────────────
13
13
  from .models import ( # noqa: F401
@@ -147,7 +147,7 @@ def render_rich(console: Console, dm: "DeviceMap") -> None:
147
147
  ]
148
148
  rt_str = " ".join(r for r in runtimes if r) or "—"
149
149
  console.print(
150
- f"\n[bold]Infra[/bold] strategy=[cyan]{infra.detected_strategy.value}[/cyan]"
150
+ f"\n[bold]Infra[/bold] strategy=[cyan]{infra.detected_strategy}[/cyan]"
151
151
  f" version={infra.current_version or '?'} {rt_str}"
152
152
  )
153
153
 
@@ -186,7 +186,7 @@ def migrate(ctx, host, app, domain, target, strategy, target_version,
186
186
  state = d.run()
187
187
  d.save(state, Path(infra_out))
188
188
  console.print(
189
- f" Strategy: {state.detected_strategy.value} "
189
+ f" Strategy: {state.detected_strategy} "
190
190
  f" Version: {state.current_version or '?'} "
191
191
  f" Conflicts: {len(state.conflicts)}"
192
192
  )
@@ -412,7 +412,7 @@ def _perform_live_detect(console, spec):
412
412
  )
413
413
  state = d.run()
414
414
  console.print(
415
- f" detected: {state.detected_strategy.value} "
415
+ f" detected: {state.detected_strategy} "
416
416
  f"version={state.current_version or '?'} "
417
417
  f"conflicts={len(state.conflicts)}"
418
418
  )
@@ -0,0 +1,8 @@
1
+ """Shared version-command utilities."""
2
+ from .git_config import resolve_package_release_git_config
3
+ from .changelog_config import resolve_package_release_changelog_config
4
+
5
+ __all__ = [
6
+ "resolve_package_release_git_config",
7
+ "resolve_package_release_changelog_config",
8
+ ]
@@ -0,0 +1,27 @@
1
+ """Resolve changelog config for a package release."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+
7
+ def resolve_package_release_changelog_config(
8
+ manifest_model: Any,
9
+ package_name: str | None,
10
+ *,
11
+ allow_root_fallback: bool,
12
+ ) -> Any:
13
+ """Return the changelog config for *package_name* with optional root fallback."""
14
+ if not package_name:
15
+ return manifest_model.changelog
16
+
17
+ package_manifest = manifest_model.get_package(package_name)
18
+ if package_manifest is None:
19
+ raise ValueError(f"Package '{package_name}' not found in manifest")
20
+
21
+ if package_manifest.changelog is not None:
22
+ return package_manifest.changelog
23
+
24
+ if allow_root_fallback:
25
+ return manifest_model.changelog
26
+
27
+ return None
@@ -0,0 +1,27 @@
1
+ """Resolve git-config for a package release."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+
7
+ def resolve_package_release_git_config(
8
+ manifest_model: Any,
9
+ package_name: str | None,
10
+ *,
11
+ allow_root_fallback: bool,
12
+ ) -> Any:
13
+ """Return the git config for *package_name* with optional root fallback."""
14
+ if not package_name:
15
+ return manifest_model.git
16
+
17
+ package_manifest = manifest_model.get_package(package_name)
18
+ if package_manifest is None:
19
+ raise ValueError(f"Package '{package_name}' not found in manifest")
20
+
21
+ if package_manifest.git is not None:
22
+ return package_manifest.git
23
+
24
+ if allow_root_fallback:
25
+ return manifest_model.git
26
+
27
+ return None
@@ -126,7 +126,7 @@ def run_detect_for_spec(
126
126
  d = Detector(host=spec.source.host, app=spec.source.app, domain=spec.source.domain)
127
127
  state = d.run()
128
128
  console.print(
129
- f" detected: {state.detected_strategy.value} "
129
+ f" detected: {state.detected_strategy} "
130
130
  f"version={state.current_version or '?'} "
131
131
  f"conflicts={len(state.conflicts)}"
132
132
  )
@@ -36,7 +36,7 @@ def print_infrastructure_summary(console: "Console", state, host: str) -> None:
36
36
  t.add_column("key", style="dim")
37
37
  t.add_column("value")
38
38
  t.add_row("App", state.app)
39
- t.add_row("Strategy (detected)", state.detected_strategy.value)
39
+ t.add_row("Strategy (detected)", state.detected_strategy)
40
40
  t.add_row("Version", state.current_version or "unknown")
41
41
  t.add_row("Docker", state.runtime.docker or "—")
42
42
  t.add_row("k3s", state.runtime.k3s or "—")
@@ -398,7 +398,7 @@ class DetectionWorkflow:
398
398
  host_res = self._deep_detect(host_res)
399
399
  if host_res.state:
400
400
  logger.info(
401
- f"[workflow] strategy={host_res.state.detected_strategy.value} "
401
+ f"[workflow] strategy={host_res.state.detected_strategy} "
402
402
  f"ports={sorted(host_res.state.ports.keys())} "
403
403
  f"conflicts={len(host_res.state.conflicts)}"
404
404
  )
@@ -399,6 +399,40 @@ class DockerComposeParser:
399
399
  def _parse_service_command(self, cfg: dict) -> Optional[str]:
400
400
  return str(cfg["command"]) if cfg.get("command") else None
401
401
 
402
+ def _build_service_info(
403
+ self,
404
+ *,
405
+ name: str,
406
+ cfg: dict,
407
+ image: Optional[str],
408
+ ports: list[PortInfo],
409
+ volumes: list[VolumeInfo],
410
+ env: dict[str, str],
411
+ env_files: list[str],
412
+ svc_networks: list[str],
413
+ depends_on: list[str],
414
+ healthcheck: Optional[str],
415
+ build_context: Optional[str],
416
+ replicas: int,
417
+ labels: dict[str, str],
418
+ ) -> ServiceInfo:
419
+ return ServiceInfo(
420
+ name=name,
421
+ image=image,
422
+ ports=ports,
423
+ volumes=volumes,
424
+ env=env,
425
+ env_files=env_files,
426
+ networks=svc_networks,
427
+ depends_on=depends_on,
428
+ healthcheck=healthcheck,
429
+ restart=cfg.get("restart"),
430
+ command=self._parse_service_command(cfg),
431
+ build_context=build_context,
432
+ replicas=replicas,
433
+ labels=labels,
434
+ )
435
+
402
436
  def _parse_service(self, name: str, cfg: dict,
403
437
  env_context: dict[str, str],
404
438
  spec: ParsedSpec) -> ServiceInfo:
@@ -413,18 +447,17 @@ class DockerComposeParser:
413
447
  replicas = self._parse_service_replicas(cfg)
414
448
  labels = self._parse_service_labels(cfg)
415
449
 
416
- return ServiceInfo(
450
+ return self._build_service_info(
417
451
  name=name,
452
+ cfg=cfg,
418
453
  image=image,
419
454
  ports=ports,
420
455
  volumes=volumes,
421
456
  env=env,
422
457
  env_files=env_files,
423
- networks=svc_networks,
458
+ svc_networks=svc_networks,
424
459
  depends_on=depends_on,
425
460
  healthcheck=healthcheck,
426
- restart=cfg.get("restart"),
427
- command=self._parse_service_command(cfg),
428
461
  build_context=build_context,
429
462
  replicas=replicas,
430
463
  labels=labels,
@@ -0,0 +1,121 @@
1
+ """redeploy.models — shared data models (backward-compat re-export hub).
2
+
3
+ This package was refactored from the monolithic ``redeploy.models`` module
4
+ (R3, 0.2.53). All public names remain importable from the top-level
5
+ package for backward compatibility.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ # ── core ─────────────────────────────────────────────────────────────────────
10
+ from .enums import (
11
+ ConflictSeverity,
12
+ DeployStrategy,
13
+ StepAction,
14
+ StepStatus,
15
+ _STRATEGY_ALIASES,
16
+ )
17
+ from .persisted import PersistedModel
18
+
19
+ # ── hardware ─────────────────────────────────────────────────────────────────
20
+ from .hardware import (
21
+ BacklightInfo,
22
+ DrmOutput,
23
+ HardwareDiagnostic,
24
+ HardwareInfo,
25
+ I2CBusInfo,
26
+ )
27
+
28
+ # ── infra state ─────────────────────────────────────────────────────────────
29
+ from .infra import (
30
+ AppHealthInfo,
31
+ ConflictInfo,
32
+ InfraState,
33
+ PortInfo,
34
+ RuntimeInfo,
35
+ ServiceInfo,
36
+ )
37
+
38
+ # ── pipeline ─────────────────────────────────────────────────────────────────
39
+ from .pipeline import Hook, PipelinePhase
40
+
41
+ # ── spec & target ───────────────────────────────────────────────────────────
42
+ from .spec import (
43
+ InfraSpec,
44
+ MigrationSpec,
45
+ TargetConfig,
46
+ _migrate_legacy_post_deploy,
47
+ )
48
+
49
+ # ── plan ─────────────────────────────────────────────────────────────────────
50
+ from .plan import MigrationPlan, MigrationStep
51
+
52
+ # ── manifest ─────────────────────────────────────────────────────────────────
53
+ from .manifest import EnvironmentConfig, ProjectManifest
54
+
55
+ # ── blueprint ────────────────────────────────────────────────────────────────
56
+ from .blueprint import (
57
+ BlueprintSource,
58
+ DeviceBlueprint,
59
+ HardwareRequirements,
60
+ ServicePort,
61
+ ServiceSpec,
62
+ VolumeMount,
63
+ )
64
+
65
+ # ── device registry / map ───────────────────────────────────────────────────
66
+ from .devices import (
67
+ DeployRecord,
68
+ DeviceMap,
69
+ DeviceRegistry,
70
+ KnownDevice,
71
+ )
72
+
73
+ __all__ = [
74
+ # enums
75
+ "ConflictSeverity",
76
+ "DeployStrategy",
77
+ "StepAction",
78
+ "StepStatus",
79
+ "_STRATEGY_ALIASES",
80
+ # persisted
81
+ "PersistedModel",
82
+ # hardware
83
+ "BacklightInfo",
84
+ "DrmOutput",
85
+ "HardwareDiagnostic",
86
+ "HardwareInfo",
87
+ "I2CBusInfo",
88
+ # infra
89
+ "AppHealthInfo",
90
+ "ConflictInfo",
91
+ "InfraState",
92
+ "PortInfo",
93
+ "RuntimeInfo",
94
+ "ServiceInfo",
95
+ # pipeline
96
+ "Hook",
97
+ "PipelinePhase",
98
+ # spec
99
+ "InfraSpec",
100
+ "MigrationSpec",
101
+ "TargetConfig",
102
+ "_migrate_legacy_post_deploy",
103
+ # plan
104
+ "MigrationPlan",
105
+ "MigrationStep",
106
+ # manifest
107
+ "EnvironmentConfig",
108
+ "ProjectManifest",
109
+ # blueprint
110
+ "BlueprintSource",
111
+ "DeviceBlueprint",
112
+ "HardwareRequirements",
113
+ "ServicePort",
114
+ "ServiceSpec",
115
+ "VolumeMount",
116
+ # devices
117
+ "DeployRecord",
118
+ "DeviceMap",
119
+ "DeviceRegistry",
120
+ "KnownDevice",
121
+ ]
@@ -0,0 +1,95 @@
1
+ """Device blueprint models — portable deployment recipes."""
2
+ from __future__ import annotations
3
+
4
+ from datetime import datetime, timezone
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ from pydantic import BaseModel, Field
9
+
10
+ from .hardware import HardwareInfo
11
+ from .persisted import PersistedModel
12
+
13
+
14
+ _DEFAULT_BLUEPRINT_DIR = Path.home() / ".config" / "redeploy" / "blueprints"
15
+
16
+
17
+ class ServicePort(BaseModel):
18
+ host: int
19
+ container: int
20
+ protocol: str = "tcp"
21
+
22
+
23
+ class VolumeMount(BaseModel):
24
+ host: str
25
+ container: str
26
+ read_only: bool = False
27
+
28
+
29
+ class ServiceSpec(BaseModel):
30
+ name: str
31
+ image: str
32
+ platform: str = ""
33
+ ports: list[ServicePort] = Field(default_factory=list)
34
+ volumes: list[VolumeMount] = Field(default_factory=list)
35
+ env: dict[str, str] = Field(default_factory=dict)
36
+ command: Optional[str] = None
37
+ healthcheck: Optional[str] = None
38
+ depends_on: list[str] = Field(default_factory=list)
39
+ labels: dict[str, str] = Field(default_factory=dict)
40
+ restart: str = "unless-stopped"
41
+ network_mode: Optional[str] = None
42
+ privileged: bool = False
43
+ source_ref: Optional[str] = None
44
+
45
+
46
+ class HardwareRequirements(BaseModel):
47
+ arch: str = "linux/arm64"
48
+ min_ram_mb: int = 0
49
+ min_disk_gb: int = 0
50
+ display_type: Optional[str] = None
51
+ display_resolution: Optional[str] = None
52
+ i2c_required: bool = False
53
+ gpio_required: bool = False
54
+ features: list[str] = Field(default_factory=list)
55
+
56
+
57
+ class BlueprintSource(BaseModel):
58
+ device_id: Optional[str] = None
59
+ device_name: Optional[str] = None
60
+ migration_file: Optional[str] = None
61
+ markpact_files: list[str] = Field(default_factory=list)
62
+ compose_files: list[str] = Field(default_factory=list)
63
+ extracted_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
64
+
65
+
66
+ class DeviceBlueprint(PersistedModel):
67
+ """Self-contained, portable deployment recipe."""
68
+ name: str
69
+ version: str = "0.0.0"
70
+ description: str = ""
71
+ tags: list[str] = Field(default_factory=list)
72
+ created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
73
+
74
+ services: list[ServiceSpec] = Field(default_factory=list)
75
+ hardware: HardwareRequirements = Field(default_factory=HardwareRequirements)
76
+ app_url: Optional[str] = None
77
+ deploy_strategy: str = "podman_quadlet"
78
+ env_file: Optional[str] = None
79
+ notes: str = ""
80
+ source: BlueprintSource = Field(default_factory=BlueprintSource)
81
+
82
+ def service(self, name: str) -> Optional[ServiceSpec]:
83
+ return next((s for s in self.services if s.name == name), None)
84
+
85
+ def save(self, path: Optional[Path] = None) -> Path:
86
+ p = path or _DEFAULT_BLUEPRINT_DIR / f"{self.name}.blueprint.yaml"
87
+ p.parent.mkdir(parents=True, exist_ok=True)
88
+ p.write_text(self.to_yaml())
89
+ return p
90
+
91
+ @classmethod
92
+ def list_saved(cls) -> list[Path]:
93
+ if not _DEFAULT_BLUEPRINT_DIR.exists():
94
+ return []
95
+ return sorted(_DEFAULT_BLUEPRINT_DIR.glob("*.blueprint.yaml"))
@@ -0,0 +1,170 @@
1
+ """Device registry and device-map models."""
2
+ from __future__ import annotations
3
+
4
+ from datetime import datetime, timezone
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ import yaml
9
+ from pydantic import BaseModel, Field, PrivateAttr
10
+
11
+ from .hardware import HardwareInfo
12
+ from .infra import InfraState
13
+ from .persisted import PersistedModel
14
+
15
+
16
+ _DEFAULT_DEVICE_MAP_DIR = Path.home() / ".config" / "redeploy" / "device-maps"
17
+
18
+
19
+ class DeployRecord(BaseModel):
20
+ """Single deployment event recorded for a device."""
21
+ timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
22
+ spec_name: str = ""
23
+ from_strategy: str = ""
24
+ to_strategy: str = ""
25
+ version: str = ""
26
+ ok: bool = True
27
+ steps_total: int = 0
28
+ steps_ok: int = 0
29
+ note: str = ""
30
+
31
+
32
+ class KnownDevice(BaseModel):
33
+ """Device known to redeploy — persisted in ~/.config/redeploy/devices.yaml."""
34
+ id: str
35
+ host: str = ""
36
+ name: str = ""
37
+ tags: list[str] = Field(default_factory=list)
38
+ strategy: str = "docker_full"
39
+ app: str = ""
40
+ domain: str = ""
41
+ remote_dir: str = ""
42
+ ssh_port: int = 22
43
+ ssh_key: Optional[str] = None
44
+
45
+ ip: str = ""
46
+ mac: str = ""
47
+ hostname: str = ""
48
+ ssh_user: str = ""
49
+ last_seen: Optional[datetime] = None
50
+ last_ssh_ok: Optional[datetime] = None
51
+ source: str = "manual"
52
+
53
+ hardware: Optional[HardwareInfo] = None
54
+ deploys: list[DeployRecord] = Field(default_factory=list)
55
+
56
+ @property
57
+ def last_deploy(self) -> Optional[DeployRecord]:
58
+ return self.deploys[-1] if self.deploys else None
59
+
60
+ @property
61
+ def is_reachable(self) -> bool:
62
+ if self.last_seen is None:
63
+ return False
64
+ return (datetime.now(timezone.utc) - self.last_seen).total_seconds() < 300
65
+
66
+ def record_deploy(self, record: DeployRecord) -> None:
67
+ self.deploys.append(record)
68
+ self.deploys = self.deploys[-50:]
69
+
70
+
71
+ class DeviceMap(PersistedModel):
72
+ """Full, persisted snapshot of a device: identity + InfraState + HardwareInfo."""
73
+ id: str
74
+ host: str = ""
75
+ name: str = ""
76
+ tags: list[str] = Field(default_factory=list)
77
+ scanned_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
78
+
79
+ hardware: Optional[HardwareInfo] = None
80
+ infra: Optional[InfraState] = None
81
+ issues: list[dict] = Field(default_factory=list)
82
+
83
+ @property
84
+ def has_errors(self) -> bool:
85
+ return any(i.get("severity") in ("error", "critical") for i in self.issues)
86
+
87
+ @property
88
+ def display_summary(self) -> str:
89
+ if not self.hardware:
90
+ return "no hardware data"
91
+ hw = self.hardware
92
+ dsi = next((o for o in hw.drm_outputs if "DSI" in o.name), None)
93
+ if dsi:
94
+ mode = dsi.modes[0] if dsi.modes else "?"
95
+ bl = hw.backlights[0].brightness if hw.backlights else "?"
96
+ return f"DSI {dsi.connector} {dsi.status} {mode} backlight={bl}"
97
+ return "no DSI"
98
+
99
+ def save(self, path: Optional[Path] = None) -> Path:
100
+ p = path or _DEFAULT_DEVICE_MAP_DIR / f"{self.id.replace('/', '_').replace('@', '_at_')}.yaml"
101
+ p.parent.mkdir(parents=True, exist_ok=True)
102
+ p.write_text(self.to_yaml())
103
+ return p
104
+
105
+ @classmethod
106
+ def load_for(cls, device_id: str) -> Optional[DeviceMap]:
107
+ p = _DEFAULT_DEVICE_MAP_DIR / f"{device_id.replace('/', '_').replace('@', '_at_')}.yaml"
108
+ if p.exists():
109
+ return cls.load(p)
110
+ return None
111
+
112
+ @classmethod
113
+ def list_saved(cls) -> list[Path]:
114
+ if not _DEFAULT_DEVICE_MAP_DIR.exists():
115
+ return []
116
+ return sorted(_DEFAULT_DEVICE_MAP_DIR.glob("*.yaml"))
117
+
118
+
119
+ class DeviceRegistry(BaseModel):
120
+ """Persistent device registry — stored at ~/.config/redeploy/devices.yaml."""
121
+ devices: list[KnownDevice] = Field(default_factory=list)
122
+ _path: Optional[Path] = PrivateAttr(default=None)
123
+
124
+ def get(self, device_id: str) -> Optional[KnownDevice]:
125
+ return next((d for d in self.devices if d.id == device_id), None)
126
+
127
+ def upsert(self, device: KnownDevice) -> None:
128
+ for i, d in enumerate(self.devices):
129
+ if d.id == device.id:
130
+ self.devices[i] = device
131
+ return
132
+ self.devices.append(device)
133
+
134
+ def remove(self, device_id: str) -> bool:
135
+ before = len(self.devices)
136
+ self.devices = [d for d in self.devices if d.id != device_id]
137
+ return len(self.devices) < before
138
+
139
+ def by_tag(self, tag: str) -> list[KnownDevice]:
140
+ return [d for d in self.devices if tag in d.tags]
141
+
142
+ def by_strategy(self, strategy: str) -> list[KnownDevice]:
143
+ return [d for d in self.devices if d.strategy == strategy]
144
+
145
+ def reachable(self) -> list[KnownDevice]:
146
+ return [d for d in self.devices if d.is_reachable]
147
+
148
+ @classmethod
149
+ def default_path(cls) -> Path:
150
+ return Path.home() / ".config" / "redeploy" / "devices.yaml"
151
+
152
+ @classmethod
153
+ def load(cls, path: Optional[Path] = None) -> DeviceRegistry:
154
+ p = path or cls.default_path()
155
+ if not p.exists():
156
+ return cls()
157
+ raw = yaml.safe_load(p.read_text()) or {}
158
+ devices = [KnownDevice(**d) for d in raw.get("devices", [])]
159
+ reg = cls(devices=devices)
160
+ reg._path = p
161
+ return reg
162
+
163
+ def save(self, path: Optional[Path] = None) -> None:
164
+ p = path or self._path or self.default_path()
165
+ p.parent.mkdir(parents=True, exist_ok=True)
166
+ data = {"devices": [d.model_dump(mode="json") for d in self.devices]}
167
+ tmp = p.with_suffix(".tmp")
168
+ tmp.write_text(yaml.dump(data, allow_unicode=True, sort_keys=False))
169
+ tmp.chmod(0o600)
170
+ tmp.replace(p)