easyrunner-cli 0.0.8.dev126__tar.gz → 0.0.8.dev128__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 (178) hide show
  1. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/PKG-INFO +1 -1
  2. easyrunner_cli-0.0.8.dev128/easyrunner/source/services/__init__.py +28 -0
  3. easyrunner_cli-0.0.8.dev128/easyrunner/source/services/app_service.py +168 -0
  4. easyrunner_cli-0.0.8.dev128/easyrunner/source/services/deployment_service.py +170 -0
  5. easyrunner_cli-0.0.8.dev128/easyrunner/source/services/license_service.py +76 -0
  6. easyrunner_cli-0.0.8.dev128/easyrunner/source/services/server_service.py +172 -0
  7. easyrunner_cli-0.0.8.dev128/easyrunner/source/services/service_result.py +48 -0
  8. easyrunner_cli-0.0.8.dev128/easyrunner/tests/test_services.py +495 -0
  9. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/pyproject.toml +1 -1
  10. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/README.md +0 -0
  11. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/.server-config/infra/authelia/configuration.template.yml +0 -0
  12. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/.server-config/infra/authelia/configuration.yml +0 -0
  13. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/.server-config/infra/authelia/local-dns-setup.sh +0 -0
  14. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/.server-config/infra/authelia/notification.txt +0 -0
  15. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/.server-config/infra/authelia/users_database.yaml +0 -0
  16. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/.server-config/infra/caddy/Caddyfile +0 -0
  17. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/.server-config/infra/caddy/Caddyfile with authcrunch +0 -0
  18. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/.server-config/infra/caddy/Caddyfile with ldap +0 -0
  19. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/.server-config/infra/docker-compose/.env.dev +0 -0
  20. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/.server-config/infra/docker-compose/.env.prod +0 -0
  21. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/.server-config/infra/docker-compose/docker-compose-host.yaml +0 -0
  22. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/.templates/docker-compose/docker-compose-app.yaml +0 -0
  23. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/.templates/docker-compose/docker-compose-helloworld.yaml +0 -0
  24. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/README.md +0 -0
  25. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/__init__.py +0 -0
  26. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/easyrunner_build.py +0 -0
  27. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/poetry.lock +0 -0
  28. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/pyproject.toml +0 -0
  29. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/pytest.ini +0 -0
  30. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/cloud_providers/__init__.py +0 -0
  31. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/cloud_providers/cloud_provider_base.py +0 -0
  32. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/cloud_providers/cloud_providers.py +0 -0
  33. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/cloud_providers/hetzner_provider.py +0 -0
  34. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/command_executor.py +0 -0
  35. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/command_executor_local.py +0 -0
  36. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/__init__.py +0 -0
  37. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/base/__init__.py +0 -0
  38. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/base/archive_commands.py +0 -0
  39. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/base/caddy_api_curl_commands.py +0 -0
  40. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/base/caddy_commands.py +0 -0
  41. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/base/command_base.py +0 -0
  42. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/base/curl_commands.py +0 -0
  43. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/base/dir_commands.py +0 -0
  44. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/base/docker_compose_commands.py +0 -0
  45. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/base/file_commands.py +0 -0
  46. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/base/git_commands.py +0 -0
  47. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/base/ip_tables_commands.py +0 -0
  48. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/base/ip_tables_persistent_commands.py +0 -0
  49. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/base/null_command.py +0 -0
  50. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/base/os_package_manager_commands.py +0 -0
  51. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/base/podman_commands.py +0 -0
  52. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/base/ssh_agent_commands.py +0 -0
  53. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/base/ssh_keygen_commands.py +0 -0
  54. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/base/systemctl_commands.py +0 -0
  55. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/base/user_commands.py +0 -0
  56. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/base/utility_commands.py +0 -0
  57. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/runnable_command_string.py +0 -0
  58. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/ubuntu/__init__.py +0 -0
  59. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/ubuntu/archive_commands_ubuntu.py +0 -0
  60. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/ubuntu/caddy_api_curl_commands_ubuntu.py +0 -0
  61. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/ubuntu/caddy_commands_container_ubuntu.py +0 -0
  62. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/ubuntu/curl_commands_ubuntu.py +0 -0
  63. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/ubuntu/dir_commands_ubuntu.py +0 -0
  64. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/ubuntu/docker_compose_commands_ubuntu.py +0 -0
  65. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/ubuntu/file_commands_ubuntu.py +0 -0
  66. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/ubuntu/git_commands_ubuntu.py +0 -0
  67. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/ubuntu/ip_tables_commands_ubuntu.py +0 -0
  68. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/ubuntu/ip_tables_persistent_commands_ubuntu.py +0 -0
  69. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/ubuntu/os_package_manager_commands_ubuntu.py +0 -0
  70. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/ubuntu/podman_commands_ubuntu.py +0 -0
  71. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/ubuntu/ssh_agent_commands_ubuntu.py +0 -0
  72. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/ubuntu/ssh_keygen_commands_ubuntu.py +0 -0
  73. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/ubuntu/systemctl_commands_ubuntu.py +0 -0
  74. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/ubuntu/user_commands_ubuntu.py +0 -0
  75. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/ubuntu/utility_commands_ubuntu.py +0 -0
  76. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/format_utils.py +0 -0
  77. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/http_client.py +0 -0
  78. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/integrations/__init__.py +0 -0
  79. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/integrations/cloudflare/__init__.py +0 -0
  80. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/integrations/cloudflare/cloudflare_api_client.py +0 -0
  81. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/integrations/cloudflare/cloudflare_api_config.py +0 -0
  82. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/integrations/cloudflare/cloudflare_api_token_manager.py +0 -0
  83. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/integrations/github/__init__.py +0 -0
  84. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/integrations/github/github_device_flow.py +0 -0
  85. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/integrations/github/github_oauth_config.py +0 -0
  86. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/integrations/github/github_token_manager.py +0 -0
  87. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/integrations/hetzner/__init__.py +0 -0
  88. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/integrations/hetzner/hetzner_api_key_manager.py +0 -0
  89. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/known_host_ssh_keys.py +0 -0
  90. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/cloud_resources/__init__.py +0 -0
  91. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/cloud_resources/cloud_firewall_base.py +0 -0
  92. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/cloud_resources/cloud_resource_api_base.py +0 -0
  93. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/cloud_resources/cloud_resource_pulumi_base.py +0 -0
  94. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/cloud_resources/cloud_virtual_machine_base.py +0 -0
  95. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/cloud_resources/github/github_api_client.py +0 -0
  96. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/cloud_resources/github/github_api_client_dtos.py +0 -0
  97. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/cloud_resources/github/github_repo.py +0 -0
  98. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/cloud_resources/hetzner/__init__.py +0 -0
  99. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/cloud_resources/hetzner/hetzner_firewall.py +0 -0
  100. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/cloud_resources/hetzner/hetzner_firewall_rule.py +0 -0
  101. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/cloud_resources/hetzner/hetzner_resource_factory.py +0 -0
  102. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/cloud_resources/hetzner/hetzner_stack.py +0 -0
  103. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/cloud_resources/hetzner/hetzner_virtual_machine.py +0 -0
  104. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/os_resources/__init__.py +0 -0
  105. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/os_resources/caddy.py +0 -0
  106. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/os_resources/directory.py +0 -0
  107. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/os_resources/docker_compose.py +0 -0
  108. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/os_resources/file.py +0 -0
  109. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/os_resources/git_repo.py +0 -0
  110. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/os_resources/host_server_ubuntu.py +0 -0
  111. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/os_resources/ip_tables.py +0 -0
  112. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/os_resources/os_package_manager.py +0 -0
  113. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/os_resources/os_resource_base.py +0 -0
  114. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/os_resources/podman.py +0 -0
  115. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/os_resources/podman_network.py +0 -0
  116. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/os_resources/ssh_agent.py +0 -0
  117. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/os_resources/systemd_service.py +0 -0
  118. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/os_resources/user.py +0 -0
  119. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/resource_base.py +0 -0
  120. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/web_security_scanner.py +0 -0
  121. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/ssh.py +0 -0
  122. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/ssh_key.py +0 -0
  123. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/store/__init__.py +0 -0
  124. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/store/data_models/__init__.py +0 -0
  125. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/store/data_models/app.py +0 -0
  126. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/store/data_models/database_dto_base.py +0 -0
  127. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/store/data_models/server.py +0 -0
  128. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/store/db_config.py +0 -0
  129. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/store/db_ctx.py +0 -0
  130. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/store/easyrunner_store.py +0 -0
  131. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/store/json_encoder.py +0 -0
  132. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/store/object_id.py +0 -0
  133. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/store/secret_store.py +0 -0
  134. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/store/uuid7.py +0 -0
  135. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/tool_paths.py +0 -0
  136. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/types/__init__.py +0 -0
  137. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/types/caddy/caddy_config.py +0 -0
  138. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/types/caddy/caddy_site.py +0 -0
  139. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/types/compose_project/__init__.py +0 -0
  140. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/types/compose_project/compose_network.py +0 -0
  141. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/types/compose_project/compose_project.py +0 -0
  142. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/types/compose_project/compose_service.py +0 -0
  143. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/types/compose_project/compose_volume.py +0 -0
  144. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/types/cpu_arch_types.py +0 -0
  145. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/types/dir_info.py +0 -0
  146. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/types/dto_base.py +0 -0
  147. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/types/exec_result.py +0 -0
  148. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/types/file_info.py +0 -0
  149. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/types/json.py +0 -0
  150. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/types/jsonobject_to_dataclass.py +0 -0
  151. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/types/os_type.py +0 -0
  152. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/types/podman_network_driver.py +0 -0
  153. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/types/security_scan_result.py +0 -0
  154. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/types/ssh_key_type.py +0 -0
  155. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/types/vm_config.py +0 -0
  156. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/tests/__init__.py +0 -0
  157. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/tests/conftest.py +0 -0
  158. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/tests/test_compose_to_quadlet.py +0 -0
  159. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/tests/test_hello_world.py +0 -0
  160. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/tests/test_secret_store.py +0 -0
  161. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/tests/test_user_resource.py +0 -0
  162. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/source/__init__.py +0 -0
  163. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/source/app_sub_command.py +0 -0
  164. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/source/infrastructure_deps.py +0 -0
  165. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/source/integrations/__init__.py +0 -0
  166. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/source/integrations/cloudflare/__init__.py +0 -0
  167. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/source/integrations/github/__init__.py +0 -0
  168. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/source/integrations/hetzner/__init__.py +0 -0
  169. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/source/integrations/secret_store/__init__.py +0 -0
  170. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/source/integrations/secret_store/keyring_secret_store.py +0 -0
  171. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/source/integrations/secret_store/secret_store.py +0 -0
  172. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/source/license_sub_command.py +0 -0
  173. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/source/licensing/__init__.py +0 -0
  174. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/source/licensing/license_manager.py +0 -0
  175. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/source/link_sub_command.py +0 -0
  176. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/source/main.py +0 -0
  177. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/source/servers_sub_command.py +0 -0
  178. {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/source/ssh_config.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: easyrunner_cli
3
- Version: 0.0.8.dev126
3
+ Version: 0.0.8.dev128
4
4
  Summary: EasyRunner CLI.
5
5
  License: Proprietary
6
6
  Author: Janaka Abeywardhana
@@ -0,0 +1,28 @@
1
+ """Service layer for business logic orchestration.
2
+
3
+ Services are the bridge between interfaces (CLI, Web API, Web UI) and core logic.
4
+ They encapsulate business rules, validation, and orchestration without being
5
+ tied to any specific interface.
6
+
7
+ Key principles:
8
+ - Pure business logic (testable without I/O)
9
+ - Return structured results (not UI formatting)
10
+ - Can be consumed by CLI, Web API, and Web UI equally
11
+ - No presentation logic (no Rich tables, JSON formatting, etc.)
12
+ """
13
+
14
+ from .app_service import AppService
15
+ from .deployment_service import DeploymentService
16
+ from .license_service import LicenseService
17
+ from .server_service import ServerService
18
+ from .service_result import (
19
+ ServiceResult,
20
+ )
21
+
22
+ __all__ = [
23
+ "ServerService",
24
+ "AppService",
25
+ "DeploymentService",
26
+ "LicenseService",
27
+ "ServiceResult",
28
+ ]
@@ -0,0 +1,168 @@
1
+ """AppService: Business logic for application management operations.
2
+
3
+ Handles:
4
+ - App validation and persistence
5
+ - App deployment orchestration
6
+ - App status checks
7
+ """
8
+
9
+ import logging
10
+ from typing import List
11
+
12
+ from ..store import EasyRunnerStore
13
+ from ..store.data_models.app import App
14
+ from .service_result import ServiceResult
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class AppService:
20
+ """Service for app management business logic."""
21
+
22
+ def __init__(self, store: EasyRunnerStore):
23
+ """Initialize service with store dependency.
24
+
25
+ Args:
26
+ store: EasyRunnerStore instance for persistence
27
+ """
28
+ self.store = store
29
+
30
+ def get_app(self, server_name: str, app_name: str) -> ServiceResult[App]:
31
+ """Get an app from a server.
32
+
33
+ Args:
34
+ server_name: Name of the server
35
+ app_name: Name of the app
36
+
37
+ Returns:
38
+ ServiceResult with App on success, or error if not found
39
+ """
40
+ try:
41
+ # Get server
42
+ server = self.store.get_server_by_name(name=server_name)
43
+ if not server:
44
+ logger.debug(f"Server not found: {server_name}")
45
+ return ServiceResult.error(
46
+ message=f"Server '{server_name}' not found",
47
+ error_code="SERVER_NOT_FOUND",
48
+ )
49
+
50
+ # Get app from server
51
+ app = next((a for a in server.apps if a.name == app_name), None)
52
+ if not app:
53
+ logger.debug(f"App not found: {app_name} on {server_name}")
54
+ return ServiceResult.error(
55
+ message=f"App '{app_name}' not found on server '{server_name}'",
56
+ error_code="APP_NOT_FOUND",
57
+ )
58
+
59
+ return ServiceResult.ok(
60
+ message=f"App '{app_name}' retrieved",
61
+ data=app,
62
+ )
63
+
64
+ except Exception as e:
65
+ logger.error(f"Error getting app: {str(e)}", exc_info=True)
66
+ return ServiceResult.error(
67
+ message=f"Failed to get app: {str(e)}",
68
+ error_code="INTERNAL_ERROR",
69
+ )
70
+
71
+ def list_apps(self, server_name: str) -> ServiceResult[List[App]]:
72
+ """List all apps on a server.
73
+
74
+ Args:
75
+ server_name: Name of the server
76
+
77
+ Returns:
78
+ ServiceResult with list of apps
79
+ """
80
+ try:
81
+ # Get server
82
+ server = self.store.get_server_by_name(name=server_name)
83
+ if not server:
84
+ logger.debug(f"Server not found: {server_name}")
85
+ return ServiceResult.error(
86
+ message=f"Server '{server_name}' not found",
87
+ error_code="SERVER_NOT_FOUND",
88
+ )
89
+
90
+ apps = server.apps if server.apps else []
91
+ return ServiceResult.ok(
92
+ message=f"Retrieved {len(apps)} app(s)",
93
+ data=apps,
94
+ )
95
+
96
+ except Exception as e:
97
+ logger.error(f"Error listing apps: {str(e)}", exc_info=True)
98
+ return ServiceResult.error(
99
+ message=f"Failed to list apps: {str(e)}",
100
+ error_code="INTERNAL_ERROR",
101
+ )
102
+
103
+ def validate_app_for_deployment(
104
+ self, server_name: str, app_name: str
105
+ ) -> ServiceResult[App]:
106
+ """Validate that an app is ready for deployment.
107
+
108
+ Business validation:
109
+ - App exists
110
+ - Server exists
111
+ - App has custom domain set (required for deployment)
112
+ - App has repo URL set
113
+
114
+ Args:
115
+ server_name: Name of the server
116
+ app_name: Name of the app
117
+
118
+ Returns:
119
+ ServiceResult with App if valid, error otherwise
120
+ """
121
+ try:
122
+ # Get the app (also validates server exists)
123
+ app_result = self.get_app(server_name=server_name, app_name=app_name)
124
+ if not app_result.success:
125
+ return app_result
126
+
127
+ app = app_result.data
128
+ if not app:
129
+ # Should not happen if app_result.success is True, but guard anyway
130
+ return ServiceResult.error(
131
+ message=f"App '{app_name}' data is missing",
132
+ error_code="APP_DATA_MISSING",
133
+ )
134
+
135
+ # Business rule: custom domain required
136
+ if not app.custom_domain:
137
+ logger.warning(
138
+ f"App validation failed - no custom domain: {app_name} on {server_name}"
139
+ )
140
+ return ServiceResult.error(
141
+ message=f"App '{app_name}' must have a custom domain set before deployment",
142
+ error_code="MISSING_CUSTOM_DOMAIN",
143
+ details={"app_name": app_name, "server_name": server_name},
144
+ )
145
+
146
+ # Business rule: repo URL required
147
+ if not app.repo_url or str(app.repo_url).strip() == "":
148
+ logger.warning(
149
+ f"App validation failed - no repo URL: {app_name} on {server_name}"
150
+ )
151
+ return ServiceResult.error(
152
+ message=f"App '{app_name}' must have a repository URL set before deployment",
153
+ error_code="MISSING_REPO_URL",
154
+ details={"app_name": app_name, "server_name": server_name},
155
+ )
156
+
157
+ logger.info(f"App validation passed: {app_name} on {server_name}")
158
+ return ServiceResult.ok(
159
+ message=f"App '{app_name}' is ready for deployment",
160
+ data=app,
161
+ )
162
+
163
+ except Exception as e:
164
+ logger.error(f"Error validating app: {str(e)}", exc_info=True)
165
+ return ServiceResult.error(
166
+ message=f"Failed to validate app: {str(e)}",
167
+ error_code="INTERNAL_ERROR",
168
+ )
@@ -0,0 +1,170 @@
1
+ """DeploymentService: Orchestrates multi-step app deployment flows.
2
+
3
+ Handles:
4
+ - Deployment validation and prerequisites
5
+ - SSH connection setup
6
+ - Coordinating resource operations
7
+ - Error handling and rollback
8
+ """
9
+
10
+ import logging
11
+ from typing import Optional
12
+
13
+ from ..command_executor import CommandExecutor
14
+ from ..integrations.github import GitHubTokenManager
15
+ from ..resources.os_resources import HostServerUbuntu
16
+ from ..ssh import Ssh
17
+ from ..store import EasyRunnerStore, SecretStore
18
+ from .app_service import AppService
19
+ from .service_result import ServiceResult
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ class DeploymentService:
25
+ """Service for orchestrating app deployments."""
26
+
27
+ def __init__(self, store: EasyRunnerStore, secret_store: SecretStore):
28
+ """Initialize service with store and secret store dependencies.
29
+
30
+ Args:
31
+ store: EasyRunnerStore instance for persistence
32
+ secret_store: SecretStore instance for secure token storage
33
+ """
34
+ self.store = store
35
+ self.secret_store = secret_store
36
+ self.app_service = AppService(store=store)
37
+ self.token_manager = GitHubTokenManager(secret_store=secret_store)
38
+
39
+ def deploy_app(
40
+ self,
41
+ server_name: str,
42
+ app_name: str,
43
+ ssh_username: str,
44
+ ssh_key_path: str,
45
+ github_token: Optional[str] = None,
46
+ debug: bool = False,
47
+ silent: bool = False,
48
+ ) -> ServiceResult[dict]:
49
+ """Deploy an app to a server.
50
+
51
+ This is the main orchestration method. It:
52
+ 1. Validates the app is ready for deployment
53
+ 2. Retrieves GitHub token if needed
54
+ 3. Establishes SSH connection
55
+ 4. Delegates to HostServerUbuntu resource for actual deployment
56
+ 5. Handles errors and provides feedback
57
+
58
+ Args:
59
+ server_name: Name of the server to deploy to
60
+ app_name: Name of the app to deploy
61
+ ssh_username: SSH username for connection
62
+ ssh_key_path: Path to SSH private key
63
+ github_token: Optional GitHub token (retrieved from store if not provided)
64
+ debug: Enable debug logging
65
+ silent: Suppress non-error output
66
+
67
+ Returns:
68
+ ServiceResult with deployment details on success, or error information
69
+ """
70
+ try:
71
+ # Step 1: Validate app is ready for deployment
72
+ validation_result = self.app_service.validate_app_for_deployment(
73
+ server_name=server_name, app_name=app_name
74
+ )
75
+ if not validation_result.success:
76
+ logger.warning(f"App validation failed: {validation_result.message}")
77
+ return ServiceResult.error(
78
+ message=validation_result.message,
79
+ error_code=validation_result.error_code,
80
+ )
81
+
82
+ app = validation_result.data
83
+ if not app:
84
+ # Should not happen if validation_result.success is True, but guard anyway
85
+ return ServiceResult.error(
86
+ message=f"App '{app_name}' data is missing",
87
+ error_code="APP_DATA_MISSING",
88
+ )
89
+
90
+ # Step 2: Get GitHub token if not provided
91
+ if github_token is None:
92
+ try:
93
+ github_token = self.token_manager.get_token()
94
+ except Exception as e:
95
+ logger.error(f"Failed to retrieve GitHub token: {str(e)}")
96
+ return ServiceResult.error(
97
+ message="GitHub token not found. Authenticate with GitHub first using 'er link github'.",
98
+ error_code="GITHUB_TOKEN_NOT_FOUND",
99
+ )
100
+
101
+ if not github_token:
102
+ logger.error("No GitHub token available")
103
+ return ServiceResult.error(
104
+ message="No GitHub token configured. Please authenticate with GitHub.",
105
+ error_code="GITHUB_TOKEN_NOT_CONFIGURED",
106
+ )
107
+
108
+ # Step 3: Get server info to get hostname/IP
109
+ server_result = self.app_service.get_app(
110
+ server_name=server_name, app_name=app_name
111
+ )
112
+ if not server_result.success:
113
+ logger.error(f"Server not found: {server_name}")
114
+ return ServiceResult.error(
115
+ message=f"Server '{server_name}' not found",
116
+ error_code="SERVER_NOT_FOUND",
117
+ )
118
+
119
+ server = self.store.get_server_by_name(name=server_name)
120
+ if not server:
121
+ logger.error(f"Server '{server_name}' is missing.")
122
+ return ServiceResult.error(
123
+ message=f"Server '{server_name}' is missing.",
124
+ error_code="SERVER_NOT_FOUND",
125
+ )
126
+
127
+ # Step 4: Establish SSH connection and deploy
128
+ logger.info(
129
+ f"Starting deployment: {app_name} to {server_name} via SSH as {ssh_username}"
130
+ )
131
+
132
+ with Ssh(
133
+ hostname_or_ipv4=server.hostname_or_ip,
134
+ username=ssh_username,
135
+ key_filename=ssh_key_path,
136
+ ) as ssh_client:
137
+ executor = CommandExecutor(ssh_client=ssh_client)
138
+ host_server = HostServerUbuntu(
139
+ easyrunner_username=ssh_username,
140
+ executor=executor,
141
+ debug=debug,
142
+ silent=silent,
143
+ )
144
+
145
+ # Delegate to resource for actual deployment
146
+ logger.info(f"Executing deployment flow for {app_name}")
147
+ host_server.deploy_app_flow_a(
148
+ repo_url=app.repo_url, # type: ignore
149
+ custom_app_domain_name=app.custom_domain, # type: ignore
150
+ github_access_token=github_token,
151
+ )
152
+
153
+ logger.info(f"Deployment completed: {app_name} on {server_name}")
154
+ return ServiceResult.ok(
155
+ message=f"App '{app_name}' deployed successfully to '{server_name}'",
156
+ data={
157
+ "app_name": app_name,
158
+ "server_name": server_name,
159
+ "custom_domain": app.custom_domain,
160
+ },
161
+ )
162
+
163
+ except Exception as e:
164
+ error_msg = f"Deployment failed: {str(e)}"
165
+ logger.error(error_msg, exc_info=True)
166
+ return ServiceResult.error(
167
+ message=error_msg,
168
+ error_code="DEPLOYMENT_FAILED",
169
+ details={"exception_type": type(e).__name__},
170
+ )
@@ -0,0 +1,76 @@
1
+ """LicenseService: Business logic for license checks and quota enforcement.
2
+
3
+ Handles:
4
+ - Server limit enforcement
5
+ - App limit enforcement
6
+ - License expiration checks
7
+ """
8
+
9
+ import logging
10
+
11
+ from ..store import EasyRunnerStore
12
+ from .service_result import ServiceResult
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class LicenseService:
18
+ """Service for license-related business logic."""
19
+
20
+ def __init__(self, store: EasyRunnerStore):
21
+ """Initialize service with store dependency.
22
+
23
+ Args:
24
+ store: EasyRunnerStore instance for persistence
25
+ """
26
+ self.store = store
27
+
28
+ def can_add_server(self) -> ServiceResult[None]:
29
+ """Check if a new server can be added based on license limits.
30
+
31
+ Returns:
32
+ ServiceResult indicating if server can be added
33
+ """
34
+ try:
35
+ # TODO: Integrate with actual LicenseManager when it's refactored to core
36
+ # For now, just return True (no license checks)
37
+ logger.debug("License check: can_add_server - not yet integrated")
38
+ return ServiceResult.ok(
39
+ message="Server can be added (TODO: integrate with LicenseManager)",
40
+ )
41
+
42
+ except Exception as e:
43
+ logger.error(f"Error checking server quota: {str(e)}", exc_info=True)
44
+ # Be permissive on errors (don't block operations)
45
+ return ServiceResult.ok(
46
+ message="Server can be added (error in license check, permissive mode)",
47
+ )
48
+
49
+ def can_add_app(self, server_name: str) -> ServiceResult[None]:
50
+ """Check if a new app can be added to a server based on license limits.
51
+
52
+ Args:
53
+ server_name: Name of the server
54
+
55
+ Returns:
56
+ ServiceResult indicating if app can be added
57
+ """
58
+ try:
59
+ server = self.store.get_server_by_name(name=server_name)
60
+ if not server:
61
+ return ServiceResult.error(
62
+ message=f"Server '{server_name}' not found",
63
+ error_code="SERVER_NOT_FOUND",
64
+ )
65
+
66
+ # TODO: Check license limits for apps per server
67
+ logger.debug(f"License check: can_add_app on {server_name}")
68
+ return ServiceResult.ok(
69
+ message="App can be added (TODO: check license limits)",
70
+ )
71
+
72
+ except Exception as e:
73
+ logger.error(f"Error checking app quota: {str(e)}", exc_info=True)
74
+ return ServiceResult.ok(
75
+ message="App can be added (error in license check, permissive mode)",
76
+ )
@@ -0,0 +1,172 @@
1
+ """ServerService: Business logic for server management operations.
2
+
3
+ Handles:
4
+ - Server addition with validation (uniqueness, etc.)
5
+ - Server retrieval and listing
6
+ - Server deletion
7
+ - Server setup verification
8
+ """
9
+
10
+ import logging
11
+ from typing import List
12
+
13
+ from ..store import EasyRunnerStore
14
+ from ..store.data_models.server import Server
15
+ from .service_result import ServiceResult
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class ServerService:
21
+ """Service for server management business logic."""
22
+
23
+ def __init__(self, store: EasyRunnerStore):
24
+ """Initialize service with store dependency.
25
+
26
+ Args:
27
+ store: EasyRunnerStore instance for persistence
28
+ """
29
+ self.store = store
30
+
31
+ def add_server(
32
+ self, name: str, hostname_or_ip: str
33
+ ) -> ServiceResult[Server]:
34
+ """Add a server to the store with validation.
35
+
36
+ Business validation:
37
+ - Server name must be unique
38
+ - Hostname/IP must be unique
39
+
40
+ Args:
41
+ name: Friendly name for the server
42
+ hostname_or_ip: Hostname or IP address
43
+
44
+ Returns:
45
+ ServiceResult with created Server on success, or error message on failure
46
+ """
47
+ try:
48
+ # Validation: check name uniqueness
49
+ existing_by_name = self.store.get_server_by_name(name=name)
50
+ if existing_by_name:
51
+ logger.warning(f"Attempt to add server with duplicate name: {name}")
52
+ return ServiceResult.error(
53
+ message=f"Server with name '{name}' already exists",
54
+ error_code="DUPLICATE_NAME",
55
+ details={"existing_server": existing_by_name.name},
56
+ )
57
+
58
+ # Validation: check hostname/IP uniqueness
59
+ existing_by_addr = self.store.get_server_by_hostname_or_ip(
60
+ hostname_or_ip=hostname_or_ip
61
+ )
62
+ if existing_by_addr:
63
+ logger.warning(
64
+ f"Attempt to add server with duplicate address: {hostname_or_ip}"
65
+ )
66
+ return ServiceResult.error(
67
+ message=f"Server with address '{hostname_or_ip}' already exists",
68
+ error_code="DUPLICATE_ADDRESS",
69
+ details={"existing_server": existing_by_addr.name},
70
+ )
71
+
72
+ # Create and persist server
73
+ server = Server(name=name, hostname_or_ip=hostname_or_ip)
74
+ self.store.add_server(server=server)
75
+
76
+ logger.info(f"Server added: {name} ({hostname_or_ip})")
77
+ return ServiceResult.ok(
78
+ message=f"Server '{name}' added successfully",
79
+ data=server,
80
+ )
81
+
82
+ except Exception as e:
83
+ logger.error(f"Error adding server: {str(e)}", exc_info=True)
84
+ return ServiceResult.error(
85
+ message=f"Failed to add server: {str(e)}",
86
+ error_code="INTERNAL_ERROR",
87
+ )
88
+
89
+ def get_server(self, name: str) -> ServiceResult[Server]:
90
+ """Get a server by name.
91
+
92
+ Args:
93
+ name: Server name
94
+
95
+ Returns:
96
+ ServiceResult with Server on success, or error if not found
97
+ """
98
+ try:
99
+ server = self.store.get_server_by_name(name=name)
100
+ if not server:
101
+ logger.debug(f"Server not found: {name}")
102
+ return ServiceResult.error(
103
+ message=f"Server '{name}' not found",
104
+ error_code="NOT_FOUND",
105
+ )
106
+
107
+ return ServiceResult.ok(
108
+ message=f"Server '{name}' retrieved",
109
+ data=server,
110
+ )
111
+
112
+ except Exception as e:
113
+ logger.error(f"Error getting server: {str(e)}", exc_info=True)
114
+ return ServiceResult.error(
115
+ message=f"Failed to get server: {str(e)}",
116
+ error_code="INTERNAL_ERROR",
117
+ )
118
+
119
+ def list_servers(self) -> ServiceResult[List[Server]]:
120
+ """List all servers.
121
+
122
+ Returns:
123
+ ServiceResult with list of servers
124
+ """
125
+ try:
126
+ servers = self.store.list_servers()
127
+ return ServiceResult.ok(
128
+ message=f"Retrieved {len(servers)} server(s)",
129
+ data=servers,
130
+ )
131
+
132
+ except Exception as e:
133
+ logger.error(f"Error listing servers: {str(e)}", exc_info=True)
134
+ return ServiceResult.error(
135
+ message=f"Failed to list servers: {str(e)}",
136
+ error_code="INTERNAL_ERROR",
137
+ )
138
+
139
+ def delete_server(self, name: str) -> ServiceResult[None]:
140
+ """Delete a server by name.
141
+
142
+ Args:
143
+ name: Server name
144
+
145
+ Returns:
146
+ ServiceResult indicating success or failure
147
+ """
148
+ try:
149
+ # Get the server first to validate it exists
150
+ server_result = self.get_server(name)
151
+ if not server_result.success:
152
+ return ServiceResult.error(
153
+ message=server_result.message,
154
+ error_code=server_result.error_code,
155
+ )
156
+
157
+ # Delete from store using server id
158
+ server = server_result.data
159
+ if server:
160
+ self.store.remove_server(server_id=server.id)
161
+
162
+ logger.info(f"Server deleted: {name}")
163
+ return ServiceResult.ok(
164
+ message=f"Server '{name}' deleted successfully",
165
+ )
166
+
167
+ except Exception as e:
168
+ logger.error(f"Error deleting server: {str(e)}", exc_info=True)
169
+ return ServiceResult.error(
170
+ message=f"Failed to delete server: {str(e)}",
171
+ error_code="INTERNAL_ERROR",
172
+ )
@@ -0,0 +1,48 @@
1
+ """Service result types for structured error/success handling.
2
+
3
+ Services return result objects instead of raising exceptions or returning raw values.
4
+ This enables callers (CLI, Web API, etc.) to handle success and failure uniformly.
5
+ """
6
+
7
+ from dataclasses import dataclass
8
+ from typing import Any, Generic, Optional, TypeVar
9
+
10
+ T = TypeVar("T")
11
+
12
+
13
+ @dataclass
14
+ class ServiceResult(Generic[T]):
15
+ """Base result type for all service operations."""
16
+
17
+ success: bool
18
+ message: str
19
+ data: Optional[T] = None
20
+ error_code: Optional[str] = None
21
+ details: Optional[dict[str, Any]] = None
22
+
23
+ @classmethod
24
+ def ok(cls, message: str = "Operation successful", data: Optional[T] = None, details: Optional[dict] = None) -> "ServiceResult[T]":
25
+ """Create a successful result."""
26
+ return cls(
27
+ success=True,
28
+ message=message,
29
+ data=data,
30
+ details=details,
31
+ )
32
+
33
+ @classmethod
34
+ def error(cls, message: str, error_code: Optional[str] = None, details: Optional[dict] = None) -> "ServiceResult[T]":
35
+ """Create an error result."""
36
+ return cls(
37
+ success=False,
38
+ message=message,
39
+ error_code=error_code,
40
+ details=details,
41
+ )
42
+
43
+
44
+ # Use ServiceResult[T] directly for server add, app deploy, and license check operations.
45
+ # Example:
46
+ # result: ServiceResult[Server]
47
+ # result: ServiceResult[AppDeployment]
48
+ # result: ServiceResult[LicenseInfo]