thestage 0.6.6__py3-none-any.whl → 0.6.8__py3-none-any.whl

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 (194) hide show
  1. thestage/__init__.py +1 -1
  2. thestage/cli_command_helper.py +2 -2
  3. thestage/config/__init__.py +1 -1
  4. thestage/{services → config/business}/app_config_service.py +3 -4
  5. thestage/{services/config_provider → config/business}/config_provider.py +6 -5
  6. thestage/config/{config_storage.py → business/config_storage.py} +1 -1
  7. thestage/{services → config/business}/validation_service.py +9 -10
  8. thestage/{controllers/config_controller.py → config/communication/config_command.py} +7 -7
  9. thestage/{services/connect → connect/business}/connect_service.py +25 -22
  10. thestage/{services → connect/business}/remote_server_service.py +4 -5
  11. thestage/connect/communication/connect_api_client.py +84 -0
  12. thestage/{services/clients/thestage_api/dtos/ssh_key_controller → connect/dto}/add_ssh_key_to_user_response.py +0 -1
  13. thestage/{services/clients/thestage_api/dtos/ssh_key_controller → connect/dto}/add_ssh_public_key_to_instance_response.py +1 -4
  14. thestage/{services/clients/thestage_api/dtos/base_controller → connect/dto}/connect_resolve_response.py +1 -1
  15. thestage/controllers/base_controller.py +2 -2
  16. thestage/debug_main.dist.py +16 -14
  17. thestage/{services/container → docker_container/business}/container_service.py +120 -40
  18. thestage/{services/container → docker_container/business}/mapper/container_mapper.py +3 -3
  19. thestage/docker_container/communication/__init__.py +0 -0
  20. thestage/{controllers/container_controller.py → docker_container/communication/docker_command.py} +27 -86
  21. thestage/docker_container/communication/docker_container_api_client.py +99 -0
  22. thestage/docker_container/dto/__init__.py +0 -0
  23. thestage/docker_container/dto/container_action_request.py +11 -0
  24. thestage/{services/clients/thestage_api/dtos → docker_container/dto}/container_response.py +4 -4
  25. thestage/{services/clients/thestage_api/dtos/docker_container_controller → docker_container/dto}/docker_container_list_response.py +3 -5
  26. thestage/docker_container/dto/enum/__init__.py +0 -0
  27. thestage/git/__init__.py +0 -0
  28. thestage/git/business/__init__.py +0 -0
  29. thestage/git/communication/__init__.py +0 -0
  30. thestage/{services/clients/git → git/communication}/git_client.py +5 -5
  31. thestage/global_dto/__init__.py +0 -0
  32. thestage/global_dto/enums/__init__.py +0 -0
  33. thestage/helpers/error_handler.py +5 -5
  34. thestage/helpers/logger/app_logger.py +1 -3
  35. thestage/i18n/en_GB/messages.po +14 -14
  36. thestage/inference_model/__init__.py +0 -0
  37. thestage/inference_model/business/__init__.py +0 -0
  38. thestage/inference_model/business/inference_model_service.py +281 -0
  39. thestage/inference_model/business/mapper/__init__.py +0 -0
  40. thestage/{services/project/mapper/project_inference_simulator_model_mapper.py → inference_model/business/mapper/inference_model_mapper.py} +5 -5
  41. thestage/inference_model/communication/__init__.py +0 -0
  42. thestage/inference_model/communication/inference_model_api_client.py +139 -0
  43. thestage/inference_model/communication/inference_model_command.py +246 -0
  44. thestage/inference_model/dto/__init__.py +0 -0
  45. thestage/{services/clients/thestage_api/dtos/inference_controller → inference_model/dto}/deploy_inference_model_to_instance_request.py +0 -2
  46. thestage/{services/clients/thestage_api/dtos/inference_controller → inference_model/dto}/deploy_inference_model_to_instance_response.py +2 -1
  47. thestage/inference_model/dto/enum/__init__.py +0 -0
  48. thestage/{services/project/dto/inference_simulator_model_dto.py → inference_model/dto/inference_model.py} +1 -1
  49. thestage/{entities/project_inference_simulator_model.py → inference_model/dto/inference_model_entity.py} +2 -2
  50. thestage/{services/clients/thestage_api/dtos/inference_controller → inference_model/dto}/inference_simulator_model_list_for_project_response.py +2 -3
  51. thestage/{services/clients/thestage_api/dtos/project_controller/project_push_inference_simulator_model_request.py → inference_model/dto/push_inference_simulator_model_request.py} +1 -1
  52. thestage/{services/clients/thestage_api/dtos/project_controller/project_push_inference_simulator_model_response.py → inference_model/dto/push_inference_simulator_model_response.py} +1 -1
  53. thestage/inference_simulator/__init__.py +0 -0
  54. thestage/inference_simulator/business/__init__.py +0 -0
  55. thestage/inference_simulator/business/inference_simulator_service.py +338 -0
  56. thestage/inference_simulator/business/mapper/__init__.py +0 -0
  57. thestage/{services/project/mapper/project_inference_simulator_mapper.py → inference_simulator/business/mapper/inference_simulator_mapper.py} +5 -5
  58. thestage/inference_simulator/communication/__init__.py +0 -0
  59. thestage/inference_simulator/communication/inference_simulator_api_client.py +114 -0
  60. thestage/inference_simulator/communication/inference_simulator_command.py +347 -0
  61. thestage/inference_simulator/dto/__init__.py +0 -0
  62. thestage/inference_simulator/dto/enum/__init__.py +0 -0
  63. thestage/{services/clients/thestage_api/dtos/inference_controller → inference_simulator/dto}/get_inference_simulator_request.py +1 -1
  64. thestage/inference_simulator/dto/get_inference_simulator_response.py +12 -0
  65. thestage/{services/project/dto/inference_simulator_dto.py → inference_simulator/dto/inference_simulator.py} +1 -1
  66. thestage/{entities/project_inference_simulator.py → inference_simulator/dto/inference_simulator_entity.py} +1 -1
  67. thestage/{services/clients/thestage_api/dtos/inference_controller → inference_simulator/dto}/inference_simulator_list_response.py +2 -2
  68. thestage/{services/clients/thestage_api/dtos/project_controller/project_start_inference_simulator_request.py → inference_simulator/dto/start_inference_simulator_request.py} +1 -1
  69. thestage/inference_simulator/dto/start_inference_simulator_response.py +10 -0
  70. thestage/instance/__init__.py +0 -0
  71. thestage/instance/business/__init__.py +0 -0
  72. thestage/{services/instance → instance/business}/instance_service.py +26 -27
  73. thestage/instance/business/mapper/__init__.py +0 -0
  74. thestage/{services/instance/mapper/instance_mapper.py → instance/business/mapper/rented_instance_mapper.py} +3 -3
  75. thestage/{services/instance/mapper/selfhosted_mapper.py → instance/business/mapper/selfhosted_instance_mapper.py} +5 -7
  76. thestage/instance/communication/__init__.py +0 -0
  77. thestage/instance/communication/instance_api_client.py +150 -0
  78. thestage/{controllers/instance_controller.py → instance/communication/instance_command.py} +9 -9
  79. thestage/instance/dto/__init__.py +0 -0
  80. thestage/instance/dto/enum/__init__.py +0 -0
  81. thestage/{services/clients/thestage_api/dtos → instance/dto}/instance_detected_gpus.py +1 -2
  82. thestage/{services/clients/thestage_api/dtos → instance/dto}/instance_rented_response.py +2 -2
  83. thestage/{services/clients/thestage_api/dtos → instance/dto}/selfhosted_instance_response.py +2 -3
  84. thestage/logging/__init__.py +0 -0
  85. thestage/logging/business/__init__.py +0 -0
  86. thestage/{services/logging → logging/business}/logging_service.py +45 -36
  87. thestage/logging/communication/__init__.py +0 -0
  88. thestage/logging/communication/logging_api_client.py +63 -0
  89. thestage/logging/dto/__init__.py +0 -0
  90. thestage/{services/clients/thestage_api/dtos/logging_controller → logging/dto}/log_polling_response.py +2 -2
  91. thestage/{services/clients/thestage_api/dtos/logging_controller → logging/dto}/user_logs_query_response.py +2 -2
  92. thestage/main.py +48 -9
  93. thestage/project/__init__.py +0 -0
  94. thestage/project/business/__init__.py +0 -0
  95. thestage/project/business/project_service.py +480 -0
  96. thestage/project/communication/__init__.py +0 -0
  97. thestage/project/communication/project_api_client.py +46 -0
  98. thestage/project/communication/project_command.py +284 -0
  99. thestage/project/dto/__init__.py +0 -0
  100. thestage/{services/project → project}/dto/project_config.py +1 -2
  101. thestage/services/clients/thestage_api/core/api_client_core.py +1 -1
  102. thestage/services/clients/thestage_api/dtos/entity_filter_request.py +1 -1
  103. thestage/services/clients/thestage_api/dtos/sftp_path_helper.py +1 -1
  104. thestage/services/filesystem_service.py +2 -2
  105. thestage/services/service_factory.py +130 -43
  106. thestage/task/__init__.py +0 -0
  107. thestage/task/business/__init__.py +0 -0
  108. thestage/task/business/mapper/__init__.py +0 -0
  109. thestage/{services/project/mapper/project_task_mapper.py → task/business/mapper/task_mapper.py} +5 -5
  110. thestage/task/business/task_service.py +304 -0
  111. thestage/task/communication/__init__.py +0 -0
  112. thestage/task/communication/task_api_client.py +122 -0
  113. thestage/task/communication/task_command.py +212 -0
  114. thestage/task/dto/__init__.py +0 -0
  115. thestage/task/dto/enum/__init__.py +0 -0
  116. thestage/{services/clients/thestage_api/dtos/task_controller/task_list_for_project_response.py → task/dto/list_for_project_response.py} +2 -2
  117. thestage/{services/clients/thestage_api/dtos/project_controller/project_run_task_request.py → task/dto/run_task_request.py} +1 -1
  118. thestage/task/dto/run_task_response.py +13 -0
  119. thestage/{services/task/dto/task_dto.py → task/dto/task.py} +1 -4
  120. thestage/{entities/project_task.py → task/dto/task_entity.py} +1 -1
  121. thestage/{services/clients/thestage_api/dtos/task_controller/task_view_response.py → task/dto/view_response.py} +2 -2
  122. {thestage-0.6.6.dist-info → thestage-0.6.8.dist-info}/METADATA +2 -1
  123. thestage-0.6.8.dist-info/RECORD +219 -0
  124. {thestage-0.6.6.dist-info → thestage-0.6.8.dist-info}/WHEEL +1 -1
  125. thestage/controllers/project_controller.py +0 -1058
  126. thestage/services/clients/thestage_api/api_client.py +0 -753
  127. thestage/services/clients/thestage_api/dtos/container_param_request.py +0 -11
  128. thestage/services/clients/thestage_api/dtos/inference_controller/get_inference_simulator_response.py +0 -13
  129. thestage/services/clients/thestage_api/dtos/project_controller/project_run_task_response.py +0 -10
  130. thestage/services/clients/thestage_api/dtos/project_controller/project_start_inference_simulator_response.py +0 -10
  131. thestage/services/clients/thestage_api/dtos/user_controller/user_profile.py +0 -12
  132. thestage/services/project/project_service.py +0 -1283
  133. thestage-0.6.6.dist-info/RECORD +0 -167
  134. /thestage/{entities → color_scheme}/__init__.py +0 -0
  135. /thestage/{entities/enums → config/business}/__init__.py +0 -0
  136. /thestage/{services/clients/git → config/communication}/__init__.py +0 -0
  137. /thestage/{services/clients/thestage_api/dtos/enums → config/dto}/__init__.py +0 -0
  138. /thestage/{services/core_files → config/dto}/config_entity.py +0 -0
  139. /thestage/{services/connect → config}/dto/remote_server_config.py +0 -0
  140. /thestage/{services/config_provider → connect}/__init__.py +0 -0
  141. /thestage/{services/container → connect/business}/__init__.py +0 -0
  142. /thestage/{services/container/mapper → connect/communication}/__init__.py +0 -0
  143. /thestage/{services/instance → connect/dto}/__init__.py +0 -0
  144. /thestage/{services/clients/thestage_api/dtos/ssh_key_controller → connect/dto}/add_ssh_key_to_user_request.py +0 -0
  145. /thestage/{services/clients/thestage_api/dtos/ssh_key_controller → connect/dto}/add_ssh_public_key_to_instance_request.py +0 -0
  146. /thestage/{services/clients/thestage_api/dtos/ssh_key_controller → connect/dto}/is_user_has_public_ssh_key_request.py +0 -0
  147. /thestage/{services/clients/thestage_api/dtos/ssh_key_controller → connect/dto}/is_user_has_public_ssh_key_response.py +0 -0
  148. /thestage/{services/instance/mapper → docker_container}/__init__.py +0 -0
  149. /thestage/{services/project → docker_container/business}/__init__.py +0 -0
  150. /thestage/{services/project → docker_container/business}/mapper/__init__.py +0 -0
  151. /thestage/{entities/container.py → docker_container/dto/container_entity.py} +0 -0
  152. /thestage/{services/clients/thestage_api/dtos/docker_container_controller → docker_container/dto}/docker_container_list_request.py +0 -0
  153. /thestage/{services/clients/thestage_api/dtos → docker_container/dto}/docker_container_mapping.py +0 -0
  154. /thestage/{services/clients/thestage_api/dtos/enums → docker_container/dto/enum}/container_pending_action.py +0 -0
  155. /thestage/{services/clients/thestage_api/dtos/enums → docker_container/dto/enum}/container_status.py +0 -0
  156. /thestage/{services/logging/exception → exceptions}/log_polling_exception.py +0 -0
  157. /thestage/git/{ProgressPrinter.py → business/ProgressPrinter.py} +0 -0
  158. /thestage/{entities → global_dto}/enums/order_direction_type.py +0 -0
  159. /thestage/{entities → global_dto}/enums/shell_type.py +0 -0
  160. /thestage/{entities → global_dto}/enums/tail_output_type.py +0 -0
  161. /thestage/{entities → global_dto}/enums/yes_no_response.py +0 -0
  162. /thestage/{entities → global_dto}/file_item.py +0 -0
  163. /thestage/{services/clients/thestage_api/dtos/inference_controller → inference_model/dto}/deploy_inference_model_to_sagemaker_request.py +0 -0
  164. /thestage/{services/clients/thestage_api/dtos/inference_controller → inference_model/dto}/deploy_inference_model_to_sagemaker_response.py +0 -0
  165. /thestage/{services/clients/thestage_api/dtos/enums → inference_model/dto/enum}/inference_model_status.py +0 -0
  166. /thestage/{services/clients/thestage_api/dtos/inference_controller → inference_model/dto}/inference_simulator_model_list_for_project_request.py +0 -0
  167. /thestage/{services/clients/thestage_api/dtos → inference_model/dto}/inference_simulator_model_response.py +0 -0
  168. /thestage/{services/clients/thestage_api/dtos/enums → inference_simulator/dto/enum}/inference_simulator_status.py +0 -0
  169. /thestage/{services/clients/thestage_api/dtos/inference_controller → inference_simulator/dto}/inference_simulator_list_request.py +0 -0
  170. /thestage/{services/clients/thestage_api/dtos → inference_simulator/dto}/inference_simulator_response.py +0 -0
  171. /thestage/{services/clients/thestage_api/dtos/enums → instance/dto/enum}/cpu_type.py +0 -0
  172. /thestage/{services/clients/thestage_api/dtos/enums → instance/dto/enum}/gpu_name.py +0 -0
  173. /thestage/{services/clients/thestage_api/dtos/enums → instance/dto/enum}/instance_rented_status.py +0 -0
  174. /thestage/{services/clients/thestage_api/dtos/enums → instance/dto/enum}/provider_name.py +0 -0
  175. /thestage/{services/clients/thestage_api/dtos/enums → instance/dto/enum}/selfhosted_status.py +0 -0
  176. /thestage/{entities → instance/dto}/rented_instance.py +0 -0
  177. /thestage/{entities → instance/dto}/self_hosted_instance.py +0 -0
  178. /thestage/{services/logging → logging}/byte_print_style.py +0 -0
  179. /thestage/{services/clients/thestage_api/dtos/logging_controller → logging/dto}/docker_container_log_stream_request.py +0 -0
  180. /thestage/{services/logging → logging}/dto/log_message.py +0 -0
  181. /thestage/{services/clients/thestage_api/dtos/logging_controller → logging/dto}/log_polling_request.py +0 -0
  182. /thestage/{services/logging → logging}/dto/log_type.py +0 -0
  183. /thestage/{services/clients/thestage_api/dtos/logging_controller → logging/dto}/task_log_stream_request.py +0 -0
  184. /thestage/{services/clients/thestage_api/dtos/logging_controller → logging/dto}/user_logs_query_request.py +0 -0
  185. /thestage/{services/logging → logging}/logging_constants.py +0 -0
  186. /thestage/{services/clients/thestage_api/dtos/project_controller/project_get_deploy_ssh_key_request.py → project/dto/get_deploy_ssh_key_request.py} +0 -0
  187. /thestage/{services/clients/thestage_api/dtos/project_controller/project_get_deploy_ssh_key_response.py → project/dto/get_deploy_ssh_key_response.py} +0 -0
  188. /thestage/{services/clients/thestage_api/dtos → project/dto}/project_response.py +0 -0
  189. /thestage/{services/clients/thestage_api/dtos/enums → task/dto/enum}/task_execution_status.py +0 -0
  190. /thestage/{services/clients/thestage_api/dtos/enums → task/dto/enum}/task_status.py +0 -0
  191. /thestage/{services/clients/thestage_api/dtos/task_controller/task_list_for_project_request.py → task/dto/list_for_project_request.py} +0 -0
  192. /thestage/{services/clients/thestage_api/dtos/task_controller/task_status_localized_map_response.py → task/dto/status_localized_map_response.py} +0 -0
  193. {thestage-0.6.6.dist-info → thestage-0.6.8.dist-info}/entry_points.txt +0 -0
  194. {thestage-0.6.6.dist-info → thestage-0.6.8.dist-info}/licenses/LICENSE.txt +0 -0
@@ -0,0 +1,304 @@
1
+ from typing import Optional
2
+
3
+ import click
4
+ import typer
5
+ from git import Commit
6
+ from rich import print
7
+
8
+ from thestage.color_scheme.color_scheme import ColorScheme
9
+ from thestage.config.business.config_provider import ConfigProvider
10
+ from thestage.docker_container.communication.docker_container_api_client import DockerContainerApiClient
11
+ from thestage.docker_container.dto.container_response import DockerContainerDto
12
+ from thestage.git.communication.git_client import GitLocalClient
13
+ from thestage.global_dto.enums.yes_no_response import YesOrNoResponse
14
+ from thestage.helpers.error_handler import error_handler
15
+ from thestage.i18n.translation import __
16
+ from thestage.project.business.project_service import ProjectService
17
+ from thestage.project.dto.project_config import ProjectConfig
18
+ from thestage.services.abstract_service import AbstractService
19
+ from thestage.services.clients.thestage_api.dtos.paginated_entity_list import PaginatedEntityList
20
+ from thestage.services.filesystem_service import FileSystemService
21
+ from thestage.task.business.mapper.task_mapper import TaskMapper
22
+ from thestage.task.communication.task_api_client import TaskApiClient
23
+ from thestage.task.dto.run_task_response import RunTaskResponse
24
+ from thestage.task.dto.task import Task
25
+ from thestage.task.dto.task_entity import TaskEntity
26
+
27
+
28
+ class TaskService(AbstractService):
29
+ def __init__(
30
+ self,
31
+ docker_container_api_client: DockerContainerApiClient,
32
+ task_api_client: TaskApiClient,
33
+ config_provider: ConfigProvider,
34
+ git_local_client: GitLocalClient,
35
+ file_system_service: FileSystemService,
36
+ project_service: ProjectService,
37
+ ):
38
+ self.__docker_container_api_client = docker_container_api_client
39
+ self.__task_api_client = task_api_client
40
+ self.__config_provider = config_provider
41
+ self.__git_local_client = git_local_client
42
+ self.__file_system_service = file_system_service
43
+ self.__project_service = project_service
44
+
45
+
46
+ @error_handler()
47
+ def project_run_task(
48
+ self,
49
+ run_command: str,
50
+ docker_container_slug: str,
51
+ docker_container_public_id: str,
52
+ task_title: Optional[str] = None,
53
+ commit_hash: Optional[str] = None,
54
+ files_to_add: Optional[str] = None,
55
+ is_skip_auto_commit: Optional[bool] = False,
56
+ ) -> Optional[Task]:
57
+ config = self.__config_provider.get_config()
58
+ project_config: ProjectConfig = self.__project_service.get_fixed_project_config()
59
+ if not project_config:
60
+ typer.echo(__("No project found at the path: %path%. Initialize or clone a project first.",
61
+ {"path": config.runtime.working_directory}))
62
+ raise typer.Exit(1)
63
+
64
+ if not docker_container_public_id and not docker_container_slug and not project_config.default_container_public_id:
65
+ typer.echo(__('Docker container ID or name is required'))
66
+ raise typer.Exit(1)
67
+
68
+ final_container_public_id = docker_container_public_id
69
+ final_container_slug = docker_container_slug
70
+ if not final_container_public_id and not final_container_slug:
71
+ final_container_public_id = project_config.default_container_public_id
72
+ typer.echo(
73
+ f"Using default docker container for this project: '{project_config.default_container_public_id}'")
74
+
75
+ container: DockerContainerDto = self.__docker_container_api_client.get_container(
76
+ container_slug=final_container_slug,
77
+ container_public_id=final_container_public_id
78
+ )
79
+
80
+ if container is None:
81
+ if final_container_slug:
82
+ typer.echo(f"Could not find container with name '{final_container_slug}'")
83
+ if final_container_public_id:
84
+ typer.echo(f"Could not find container with ID '{final_container_public_id}'")
85
+ if project_config.default_container_public_id == final_container_public_id:
86
+ project_config.default_container_public_id = None
87
+ project_config.prompt_for_default_container = True
88
+ self.__config_provider.save_project_config(project_config=project_config)
89
+ typer.echo(f"Default container settings were reset")
90
+ raise typer.Exit(1)
91
+
92
+ if container.project.public_id != project_config.public_id:
93
+ typer.echo(
94
+ f"Provided container '{container.public_id}' is not related to project '{project_config.public_id}'")
95
+ raise typer.Exit(1)
96
+
97
+ if (project_config.prompt_for_default_container is None or project_config.prompt_for_default_container) and (
98
+ docker_container_slug or docker_container_public_id) and (
99
+ project_config.default_container_public_id != container.public_id):
100
+ set_default_container_answer: str = typer.prompt(
101
+ text=f"Would you like to set '{docker_container_slug}' as a default container for this project installation?",
102
+ show_choices=True,
103
+ default=YesOrNoResponse.YES.value,
104
+ type=click.Choice([r.value for r in YesOrNoResponse]),
105
+ show_default=True,
106
+ )
107
+ project_config.prompt_for_default_container = False
108
+ if set_default_container_answer == YesOrNoResponse.YES.value:
109
+ project_config.default_container_public_id = container.public_id
110
+
111
+ self.__config_provider.save_project_config(project_config=project_config)
112
+
113
+ has_wrong_args = files_to_add and commit_hash or is_skip_auto_commit and commit_hash or files_to_add and is_skip_auto_commit
114
+
115
+ if has_wrong_args:
116
+ warning_msg = f"[{ColorScheme.WARNING.value}][WARNING] You can provide only one of the following arguments: --commit-hash, --files-add, --skip-autocommit[{ColorScheme.WARNING.value}]"
117
+ print(warning_msg)
118
+ raise typer.Exit(1)
119
+
120
+ if not is_skip_auto_commit and not commit_hash:
121
+ is_git_folder = self.__git_local_client.is_present_local_git(path=config.runtime.working_directory)
122
+ if not is_git_folder:
123
+ typer.echo("Error: Working directory is not a git repository")
124
+ raise typer.Exit(1)
125
+
126
+ is_commit_allowed: bool = True
127
+ has_changes = self.__git_local_client.has_changes_with_untracked(
128
+ path=config.runtime.working_directory,
129
+ )
130
+
131
+ if self.__git_local_client.is_head_detached(path=config.runtime.working_directory):
132
+ is_commit_allowed = False
133
+ print(f"[{ColorScheme.GIT_HEADLESS.value}]HEAD is detached[{ColorScheme.GIT_HEADLESS.value}]")
134
+
135
+ is_headless_commits_present = self.__git_local_client.is_head_committed_in_headless_state(
136
+ path=config.runtime.working_directory)
137
+ if is_headless_commits_present:
138
+ print(
139
+ f"[{ColorScheme.GIT_HEADLESS.value}]Current commit created in detached HEAD state. Cannot use it to run the task. Consider using 'project checkout' command to return to a valid reference.[{ColorScheme.GIT_HEADLESS.value}]")
140
+ raise typer.Exit(1)
141
+
142
+ if has_changes:
143
+ print(
144
+ f"[{ColorScheme.GIT_HEADLESS.value}]Local changes detected in detached head state. They will not impact the task execution.[{ColorScheme.GIT_HEADLESS.value}]")
145
+ response: YesOrNoResponse = typer.prompt(
146
+ text=__('Continue?'),
147
+ show_choices=True,
148
+ default=YesOrNoResponse.YES.value,
149
+ type=click.Choice([r.value for r in YesOrNoResponse]),
150
+ show_default=True,
151
+ )
152
+ if response == YesOrNoResponse.NO:
153
+ raise typer.Exit(0)
154
+
155
+ if is_commit_allowed:
156
+ if not self.__git_local_client.add_files_with_size_limit_or_warn(config.runtime.working_directory,
157
+ files_to_add):
158
+ warning_msg = f"[{ColorScheme.WARNING.value}][WARNING] Task was not started [{ColorScheme.WARNING.value}]"
159
+ print(warning_msg)
160
+ raise typer.Exit(1)
161
+
162
+ diff_stat = self.__git_local_client.git_diff_stat(repo_path=config.runtime.working_directory)
163
+
164
+ if has_changes and diff_stat:
165
+ branch_name = self.__git_local_client.get_active_branch_name(config.runtime.working_directory)
166
+
167
+ typer.echo(__('Active branch [%branch_name%] has uncommitted changes: %diff_stat_bottomline%', {
168
+ 'diff_stat_bottomline': diff_stat,
169
+ 'branch_name': branch_name,
170
+ }))
171
+
172
+ response: str = typer.prompt(
173
+ text=__('Commit changes?'),
174
+ show_choices=True,
175
+ default=YesOrNoResponse.YES.value,
176
+ type=click.Choice([r.value for r in YesOrNoResponse]),
177
+ show_default=True,
178
+ )
179
+ if response == YesOrNoResponse.NO.value:
180
+ typer.echo("Cannot run task with uncommitted changes - aborting")
181
+ raise typer.Exit(0)
182
+
183
+ commit_name = typer.prompt(
184
+ text=__('Please provide commit message'),
185
+ show_choices=False,
186
+ type=str,
187
+ show_default=False,
188
+ )
189
+
190
+ if commit_name:
191
+ commit_result = self.__git_local_client.commit_local_changes(
192
+ path=config.runtime.working_directory,
193
+ name=commit_name
194
+ )
195
+
196
+ if commit_result:
197
+ # in docs not Commit object, on real - str
198
+ if isinstance(commit_result, str):
199
+ typer.echo(commit_result)
200
+ else:
201
+ typer.echo(__('Commit message cannot be empty'))
202
+ raise typer.Exit(0)
203
+ else:
204
+ pass
205
+ # possible to push new empty branch - only that there's a wrong place to do so
206
+
207
+ self.__git_local_client.push_changes(
208
+ path=config.runtime.working_directory,
209
+ deploy_key_path=project_config.deploy_key_path
210
+ )
211
+ typer.echo(__("Pushed changes to remote repository"))
212
+
213
+ if not commit_hash:
214
+ commit = self.__git_local_client.get_current_commit(path=config.runtime.working_directory)
215
+ if not commit or not isinstance(commit, Commit):
216
+ print('[red]Error: No current commit found in the local repository[/red]')
217
+ raise typer.Exit(0)
218
+ commit_hash = commit.hexsha
219
+ else:
220
+ commit = self.__git_local_client.get_commit_by_hash(path=config.runtime.working_directory,
221
+ commit_hash=commit_hash)
222
+ if not commit or not isinstance(commit, Commit):
223
+ print(f'[red]Error: commit \'{commit_hash}\' was not found in the local repository[/red]')
224
+ raise typer.Exit(0)
225
+
226
+ if not task_title:
227
+ task_title = commit.message.strip() if commit.message else f'Task_{commit_hash}'
228
+ if not commit.message:
229
+ typer.echo(f'Commit message is empty. Task title is set to "{task_title}"')
230
+
231
+ run_task_response: RunTaskResponse = self.__task_api_client.execute_project_task(
232
+ project_public_id=project_config.public_id,
233
+ docker_container_public_id=container.public_id,
234
+ run_command=run_command,
235
+ commit_hash=commit_hash,
236
+ task_title=task_title,
237
+ )
238
+ if run_task_response:
239
+ if run_task_response.message:
240
+ print(f"[{ColorScheme.WARNING.value}]{run_task_response.message}[{ColorScheme.WARNING.value}]")
241
+ if run_task_response.is_success and run_task_response.task:
242
+ typer.echo(f"Task '{run_task_response.task.title}' has been scheduled successfully. Task ID: {run_task_response.task.public_id}")
243
+ if run_task_response.tasksInQueue:
244
+ typer.echo(f"There are tasks in queue ahead of this new task:")
245
+ for queued_task_item in run_task_response.tasksInQueue:
246
+ typer.echo(f"{queued_task_item.public_id} - {queued_task_item.frontend_status.status_translation}")
247
+ return run_task_response.task
248
+ else:
249
+ typer.echo(f'The task failed with an error: {run_task_response.message}')
250
+ raise typer.Exit(1)
251
+ else:
252
+ typer.echo("The task failed with an error")
253
+ raise typer.Exit(1)
254
+
255
+ @error_handler()
256
+ def cancel_task(self, task_public_id: str):
257
+ cancel_result = self.__task_api_client.cancel_task(
258
+ task_public_id=task_public_id,
259
+ )
260
+
261
+ if cancel_result.is_success:
262
+ typer.echo(f'Task {task_public_id} has been canceled')
263
+ else:
264
+ typer.echo(f'Task {task_public_id} could not be canceled: {cancel_result.message}')
265
+
266
+ def print_task_list(self, project_public_id: Optional[str], project_slug: Optional[str], row, page):
267
+ if not project_slug and not project_public_id:
268
+ project_config: ProjectConfig = self.__config_provider.read_project_config()
269
+ if not project_config:
270
+ typer.echo(
271
+ __("Provide the project unique ID or run this command from within an initialized project directory"))
272
+ raise typer.Exit(1)
273
+ project_public_id = project_config.public_id
274
+
275
+ self.print(
276
+ func_get_data=self.get_project_task_list,
277
+ func_special_params={
278
+ 'project_public_id': project_public_id,
279
+ 'project_slug': project_slug,
280
+ },
281
+ mapper=TaskMapper(),
282
+ headers=list(map(lambda x: x.alias, TaskEntity.model_fields.values())),
283
+ row=row,
284
+ page=page,
285
+ max_col_width=[100, 100, 100, 100, 100, 100, 100, 100],
286
+ show_index="never",
287
+ )
288
+
289
+ @error_handler()
290
+ def get_project_task_list(
291
+ self,
292
+ project_public_id: Optional[str],
293
+ project_slug: Optional[str],
294
+ row: int = 5,
295
+ page: int = 1,
296
+ ) -> PaginatedEntityList[Task]:
297
+ data: Optional[PaginatedEntityList[Task]] = self.__task_api_client.get_task_list_for_project(
298
+ project_public_id=project_public_id,
299
+ project_slug=project_slug,
300
+ page=page,
301
+ limit=row,
302
+ )
303
+
304
+ return data
File without changes
@@ -0,0 +1,122 @@
1
+ from typing import Optional, List, Dict
2
+
3
+ from thestage.config.business.config_provider import ConfigProvider
4
+ from thestage.global_dto.enums.order_direction_type import OrderDirectionType
5
+ from thestage.services.clients.thestage_api.core.api_client_core import TheStageApiClientCore
6
+ from thestage.services.clients.thestage_api.dtos.entity_filter_request import EntityFilterRequest
7
+ from thestage.services.clients.thestage_api.dtos.base_response import TheStageBaseResponse
8
+ from thestage.services.clients.thestage_api.dtos.paginated_entity_list import PaginatedEntityList
9
+ from thestage.task.dto.list_for_project_request import TaskListForProjectRequest
10
+ from thestage.task.dto.list_for_project_response import TaskListForProjectResponse
11
+ from thestage.task.dto.run_task_request import RunTaskRequest
12
+ from thestage.task.dto.run_task_response import RunTaskResponse
13
+ from thestage.task.dto.status_localized_map_response import TaskStatusLocalizedMapResponse
14
+ from thestage.task.dto.view_response import TaskViewResponse
15
+ from thestage.task.dto.task import Task
16
+
17
+
18
+ class TaskApiClient(TheStageApiClientCore):
19
+ def __init__(self, config_provider: ConfigProvider):
20
+ super().__init__(url=config_provider.get_config().main.thestage_api_url)
21
+ self.__config_provider = config_provider
22
+
23
+ def get_task(
24
+ self,
25
+ task_public_id: str,
26
+ ) -> Optional[TaskViewResponse]:
27
+ data = {
28
+ "taskPublicId": task_public_id,
29
+ }
30
+
31
+ response = self._request(
32
+ method='POST',
33
+ url='/user-api/v2/task/view',
34
+ data=data,
35
+ token=self.__config_provider.get_config().main.thestage_auth_token,
36
+ )
37
+
38
+ result = TaskViewResponse.model_validate(response) if response else None
39
+ return result if result and result.is_success else None
40
+
41
+ def get_task_list_for_project(
42
+ self,
43
+ project_public_id: Optional[str],
44
+ project_slug: Optional[str],
45
+ page: int = 1,
46
+ limit: int = 10,
47
+ ) -> Optional[PaginatedEntityList[Task]]:
48
+ request = TaskListForProjectRequest(
49
+ projectPublicId=project_public_id,
50
+ projectSlug=project_slug,
51
+ entityFilterRequest=EntityFilterRequest(
52
+ orderByField="createdAt",
53
+ orderByDirection=OrderDirectionType.DESC,
54
+ page=page,
55
+ limit=limit,
56
+ ),
57
+ )
58
+
59
+ response = self._request(
60
+ method='POST',
61
+ url='/user-api/v2/task/list',
62
+ data=request.model_dump(),
63
+ token=self.__config_provider.get_config().main.thestage_auth_token,
64
+ )
65
+
66
+ result = TaskListForProjectResponse.model_validate(response) if response else None
67
+ return result.tasks if result and result.is_success else None
68
+
69
+ def get_task_localized_status_map(self) -> Optional[Dict[str, str]]:
70
+ response = self._request(
71
+ method='POST',
72
+ url='/user-api/v1/task/status-localized-mapping',
73
+ data=None,
74
+ token=self.__config_provider.get_config().main.thestage_auth_token,
75
+ )
76
+
77
+ data = TaskStatusLocalizedMapResponse.model_validate(response) if response else None
78
+
79
+ return data.taskStatusMap if data else None
80
+
81
+ def cancel_task(
82
+ self,
83
+ task_public_id: str,
84
+ ) -> Optional[TheStageBaseResponse]:
85
+ data = {
86
+ "taskPublicId": task_public_id,
87
+ }
88
+
89
+ response = self._request(
90
+ method='POST',
91
+ url='/user-api/v2/task/cancel',
92
+ data=data,
93
+ token=self.__config_provider.get_config().main.thestage_auth_token,
94
+ )
95
+
96
+ result = TheStageBaseResponse.model_validate(response) if response else None
97
+ return result if result else None
98
+
99
+ def execute_project_task(
100
+ self,
101
+ project_public_id: str,
102
+ run_command: str,
103
+ task_title: str,
104
+ docker_container_public_id: str,
105
+ commit_hash: Optional[str] = None,
106
+ ) -> Optional[RunTaskResponse]:
107
+ request = RunTaskRequest(
108
+ projectPublicId=project_public_id,
109
+ dockerContainerPublicId=docker_container_public_id,
110
+ commitHash=commit_hash,
111
+ runCommand=run_command,
112
+ taskTitle=task_title,
113
+ )
114
+
115
+ response = self._request(
116
+ method='POST',
117
+ url='/user-api/v2/task/execute',
118
+ data=request.model_dump(),
119
+ token=self.__config_provider.get_config().main.thestage_auth_token,
120
+ )
121
+
122
+ return RunTaskResponse.model_validate(response) if response else None
@@ -0,0 +1,212 @@
1
+ from typing import Optional, List
2
+
3
+ import typer
4
+ from typing_extensions import Annotated
5
+
6
+ from thestage.cli_command import CliCommand
7
+ from thestage.cli_command_helper import get_command_metadata, check_command_permission
8
+ from thestage.controllers.utils_controller import validate_config_and_get_service_factory, get_current_directory
9
+ from thestage.helpers.logger.app_logger import app_logger
10
+ from thestage.i18n.translation import __
11
+ from thestage.logging.business.logging_service import LoggingService
12
+ from thestage.task.dto.task import Task
13
+
14
+ app = typer.Typer(no_args_is_help=True, help=__("Manage project tasks"))
15
+ runner_app = typer.Typer(help="Project Runner")
16
+
17
+ @runner_app.command(name='run', no_args_is_help=True, help=__("Run a task within the project. By default, it uses the latest commit from the main branch and streams real-time task logs."), **get_command_metadata(CliCommand.PROJECT_RUN))
18
+ def run(
19
+ command: Annotated[List[str], typer.Argument(
20
+ help=__("Command to run (required)"),
21
+ )],
22
+ commit_hash: Optional[str] = typer.Option(
23
+ None,
24
+ '--commit-hash',
25
+ '-hash',
26
+ help=__("Commit hash to use. By default, the current HEAD commit is used."),
27
+ is_eager=False,
28
+ ),
29
+ docker_container_public_id: Optional[str] = typer.Option(
30
+ None,
31
+ '--container-id',
32
+ '-cid',
33
+ help=__("Docker container ID"),
34
+ is_eager=False,
35
+ ),
36
+ docker_container_slug: Optional[str] = typer.Option(
37
+ None,
38
+ '--container-name',
39
+ '-cn',
40
+ help=__("Docker container name"),
41
+ is_eager=False,
42
+ ),
43
+ working_directory: Optional[str] = typer.Option(
44
+ None,
45
+ "--working-directory",
46
+ "-wd",
47
+ help=__("Full path to working directory"),
48
+ show_default=False,
49
+ is_eager=False,
50
+ ),
51
+ enable_log_stream: Optional[bool] = typer.Option(
52
+ True,
53
+ " /--no-logs",
54
+ " /-nl",
55
+ help=__("Disable real-time log streaming"),
56
+ is_eager=False,
57
+ ),
58
+ task_title: Optional[str] = typer.Option(
59
+ None,
60
+ "--title",
61
+ "-t",
62
+ help=__("Provide a custom task title. Git commit message is used by default."),
63
+ is_eager=False,
64
+ ),
65
+ files_to_add: Optional[str] = typer.Option(
66
+ None,
67
+ "--files-add",
68
+ "-fa",
69
+ help=__("Files to add to the commit. You can add files by their relative path from the working directory with a comma as a separator."),
70
+ is_eager=False,
71
+ ),
72
+ is_skip_auto_commit: Optional[bool] = typer.Option(
73
+ False,
74
+ "--skip-autocommit",
75
+ "-sa",
76
+ help=__("Skip automatic commit of the changes"),
77
+ is_eager=False,
78
+ ),
79
+ ):
80
+ command_name = CliCommand.PROJECT_RUN
81
+ app_logger.info(f'Running {command_name} from {get_current_directory()}')
82
+ check_command_permission(command_name)
83
+
84
+ if sum(v is not None for v in [docker_container_public_id, docker_container_slug]) != 1:
85
+ typer.echo("Provide a single identifier for the container - name or ID.")
86
+ raise typer.Exit(1)
87
+
88
+ if not command:
89
+ typer.echo(__('Command is required'))
90
+ raise typer.Exit(1)
91
+
92
+ service_factory = validate_config_and_get_service_factory(working_directory=working_directory)
93
+ task_service = service_factory.get_task_service()
94
+
95
+ task: Optional[Task] = task_service.project_run_task(
96
+ run_command=" ".join(command),
97
+ commit_hash=commit_hash,
98
+ docker_container_public_id=docker_container_public_id,
99
+ docker_container_slug=docker_container_slug,
100
+ task_title=task_title,
101
+ files_to_add=files_to_add,
102
+ is_skip_auto_commit=is_skip_auto_commit,
103
+ )
104
+
105
+ if enable_log_stream:
106
+ logging_service: LoggingService = service_factory.get_logging_service()
107
+ logging_service.stream_task_logs_with_controls(task_public_id=task.public_id)
108
+
109
+ raise typer.Exit(0)
110
+
111
+
112
+ @app.command(name='cancel', no_args_is_help=True, help=__("Cancel a task by ID"), **get_command_metadata(CliCommand.PROJECT_TASK_CANCEL))
113
+ def cancel_task(
114
+ task_id: Annotated[str, typer.Argument(
115
+ help=__("Task ID (required)"),
116
+ )],
117
+ ):
118
+ command_name = CliCommand.PROJECT_TASK_CANCEL
119
+ app_logger.info(f'Running {command_name} from {get_current_directory()}')
120
+ check_command_permission(command_name)
121
+
122
+ if not task_id:
123
+ typer.echo('Task ID is required')
124
+ raise typer.Exit(1)
125
+
126
+ service_factory = validate_config_and_get_service_factory()
127
+ task_service = service_factory.get_task_service()
128
+
129
+ task_service.cancel_task(
130
+ task_public_id=task_id
131
+ )
132
+
133
+ raise typer.Exit(0)
134
+
135
+
136
+ @app.command("ls", help=__("List tasks"), **get_command_metadata(CliCommand.PROJECT_TASK_LS))
137
+ def list_runs(
138
+ project_public_id: Optional[str] = typer.Option(
139
+ None,
140
+ '--project-id',
141
+ '-pid',
142
+ help=__("Project ID. By default, project info is taken from the current directory"),
143
+ is_eager=False,
144
+ ),
145
+ project_slug: Optional[str] = typer.Option(
146
+ None,
147
+ '--project-name',
148
+ '-pn',
149
+ help=__("Project name. By default, project info is taken from the current directory"),
150
+ is_eager=False,
151
+ ),
152
+ row: int = typer.Option(
153
+ 5,
154
+ '--row',
155
+ '-r',
156
+ help=__("Set number of rows displayed per page"),
157
+ is_eager=False,
158
+ ),
159
+ page: int = typer.Option(
160
+ 1,
161
+ '--page',
162
+ '-p',
163
+ help=__("Set starting page for displaying output"),
164
+ is_eager=False,
165
+ ),
166
+ ):
167
+ command_name = CliCommand.PROJECT_TASK_LS
168
+ app_logger.info(f'Running {command_name} from {get_current_directory()}')
169
+ check_command_permission(command_name)
170
+
171
+ if sum(v is not None for v in [project_public_id, project_slug]) > 1:
172
+ typer.echo("Provide a single identifier for project - ID or name.")
173
+ raise typer.Exit(1)
174
+
175
+ service_factory = validate_config_and_get_service_factory()
176
+ task_service = service_factory.get_task_service()
177
+
178
+ task_service.print_task_list(project_public_id=project_public_id, project_slug=project_slug, row=row, page=page)
179
+
180
+ typer.echo(__("Tasks listing complete"))
181
+ raise typer.Exit(0)
182
+
183
+
184
+ @app.command(name="logs", no_args_is_help=True, help=__("Stream real-time task logs or view last logs for a task"), **get_command_metadata(CliCommand.PROJECT_TASK_LOGS))
185
+ def task_logs(
186
+ task_id: Optional[str] = typer.Argument(help=__("Task ID"),),
187
+ logs_number: Optional[int] = typer.Option(
188
+ None,
189
+ '--number',
190
+ '-n',
191
+ help=__("Display a number of latest log entries. No real-time stream if provided."),
192
+ is_eager=False,
193
+ ),
194
+ ):
195
+ command_name = CliCommand.PROJECT_TASK_LOGS
196
+ app_logger.info(f'Running {command_name} from {get_current_directory()}')
197
+ check_command_permission(command_name)
198
+
199
+ if not task_id:
200
+ typer.echo(__('Task ID is required'))
201
+ raise typer.Exit(1)
202
+
203
+ service_factory = validate_config_and_get_service_factory()
204
+ logging_service: LoggingService = service_factory.get_logging_service()
205
+
206
+ if logs_number is None:
207
+ logging_service.stream_task_logs_with_controls(task_public_id=task_id)
208
+ else:
209
+ logging_service.print_last_task_logs(task_public_id=task_id, logs_number=logs_number)
210
+
211
+ app_logger.info(f'Task logs - end')
212
+ raise typer.Exit(0)
File without changes
File without changes
@@ -3,10 +3,10 @@ from pydantic import Field, ConfigDict
3
3
 
4
4
  from thestage.services.clients.thestage_api.dtos.base_response import TheStageBaseResponse
5
5
  from thestage.services.clients.thestage_api.dtos.paginated_entity_list import PaginatedEntityList
6
- from thestage.services.task.dto.task_dto import TaskDto
6
+ from thestage.task.dto.task import Task
7
7
 
8
8
 
9
9
  class TaskListForProjectResponse(TheStageBaseResponse):
10
10
  model_config = ConfigDict(use_enum_values=True)
11
11
 
12
- tasks: PaginatedEntityList[TaskDto] = Field(None, alias='tasks')
12
+ tasks: PaginatedEntityList[Task] = Field(None, alias='tasks')
@@ -3,7 +3,7 @@ from typing import Optional
3
3
  from pydantic import Field, ConfigDict, BaseModel
4
4
 
5
5
 
6
- class ProjectRunTaskRequest(BaseModel):
6
+ class RunTaskRequest(BaseModel):
7
7
  model_config = ConfigDict(use_enum_values=True)
8
8
 
9
9
  projectPublicId: str = Field(None, alias='projectPublicId')
@@ -0,0 +1,13 @@
1
+ from typing import Optional, List
2
+
3
+ from pydantic import Field, ConfigDict
4
+
5
+ from thestage.services.clients.thestage_api.dtos.base_response import TheStageBaseResponse
6
+ from thestage.task.dto.task import Task
7
+
8
+
9
+ class RunTaskResponse(TheStageBaseResponse):
10
+ model_config = ConfigDict(use_enum_values=True)
11
+
12
+ task: Task = Field(None, alias='task')
13
+ tasksInQueue: Optional[List[Task]] = Field(None, alias='tasksInQueue')