thestage 0.6.2__py3-none-any.whl → 0.6.4__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 (177) hide show
  1. thestage/.env +4 -5
  2. thestage/__init__.py +3 -3
  3. thestage/__main__.py +9 -9
  4. thestage/cli_command.py +56 -56
  5. thestage/cli_command_helper.py +51 -51
  6. thestage/color_scheme/color_scheme.py +7 -7
  7. thestage/config/__init__.py +18 -18
  8. thestage/config/config_storage.py +5 -5
  9. thestage/config/env_base.py +7 -7
  10. thestage/controllers/__init__.py +0 -0
  11. thestage/controllers/base_controller.py +67 -67
  12. thestage/controllers/config_controller.py +137 -137
  13. thestage/controllers/container_controller.py +389 -389
  14. thestage/controllers/instance_controller.py +183 -183
  15. thestage/controllers/project_controller.py +810 -810
  16. thestage/controllers/utils_controller.py +32 -32
  17. thestage/debug_main.dist.py +28 -28
  18. thestage/entities/__init__.py +0 -0
  19. thestage/entities/container.py +17 -17
  20. thestage/entities/enums/__init__.py +0 -0
  21. thestage/entities/enums/order_direction_type.py +6 -6
  22. thestage/entities/enums/shell_type.py +7 -7
  23. thestage/entities/enums/tail_output_type.py +6 -6
  24. thestage/entities/enums/yes_no_response.py +7 -7
  25. thestage/entities/file_item.py +27 -27
  26. thestage/entities/project_inference_simulator.py +18 -18
  27. thestage/entities/project_inference_simulator_model.py +17 -17
  28. thestage/entities/project_task.py +19 -19
  29. thestage/entities/rented_instance.py +19 -19
  30. thestage/entities/self_hosted_instance.py +18 -18
  31. thestage/exceptions/__init__.py +0 -0
  32. thestage/exceptions/auth_exception.py +6 -6
  33. thestage/exceptions/base_exception.py +13 -13
  34. thestage/exceptions/business_logic_exception.py +6 -6
  35. thestage/exceptions/config_exception.py +6 -6
  36. thestage/exceptions/file_system_exception.py +6 -6
  37. thestage/exceptions/git_access_exception.py +17 -17
  38. thestage/exceptions/remote_server_exception.py +24 -24
  39. thestage/git/ProgressPrinter.py +22 -22
  40. thestage/helpers/__init__.py +0 -0
  41. thestage/helpers/error_handler.py +115 -115
  42. thestage/helpers/exception_hook.py +14 -14
  43. thestage/helpers/logger/__init__.py +0 -0
  44. thestage/helpers/logger/app_logger.py +50 -50
  45. thestage/helpers/ssh_util.py +38 -38
  46. thestage/i18n/en_GB/messages.po +947 -947
  47. thestage/i18n/translation.py +9 -9
  48. thestage/main.py +36 -36
  49. thestage/services/.env +6 -6
  50. thestage/services/__init__.py +0 -0
  51. thestage/services/abstract_mapper.py +9 -9
  52. thestage/services/abstract_service.py +87 -87
  53. thestage/services/app_config_service.py +52 -52
  54. thestage/services/clients/__init__.py +0 -0
  55. thestage/services/clients/git/__init__.py +0 -0
  56. thestage/services/clients/git/git_client.py +436 -436
  57. thestage/services/clients/thestage_api/__init__.py +0 -0
  58. thestage/services/clients/thestage_api/api_client.py +718 -718
  59. thestage/services/clients/thestage_api/core/api_client_core.py +108 -108
  60. thestage/services/clients/thestage_api/core/http_client_exception.py +12 -12
  61. thestage/services/clients/thestage_api/dtos/__init__.py +0 -0
  62. thestage/services/clients/thestage_api/dtos/base_response.py +13 -13
  63. thestage/services/clients/thestage_api/dtos/cloud_provider_region.py +19 -19
  64. thestage/services/clients/thestage_api/dtos/container_param_request.py +11 -11
  65. thestage/services/clients/thestage_api/dtos/container_response.py +67 -67
  66. thestage/services/clients/thestage_api/dtos/docker_container_assigned_device.py +10 -10
  67. thestage/services/clients/thestage_api/dtos/docker_container_controller/docker_container_list_request.py +13 -13
  68. thestage/services/clients/thestage_api/dtos/docker_container_controller/docker_container_list_response.py +13 -13
  69. thestage/services/clients/thestage_api/dtos/docker_container_mapping.py +10 -10
  70. thestage/services/clients/thestage_api/dtos/entity_filter_request.py +14 -14
  71. thestage/services/clients/thestage_api/dtos/enums/__init__.py +0 -0
  72. thestage/services/clients/thestage_api/dtos/enums/container_pending_action.py +10 -10
  73. thestage/services/clients/thestage_api/dtos/enums/container_status.py +17 -17
  74. thestage/services/clients/thestage_api/dtos/enums/cpu_type.py +8 -8
  75. thestage/services/clients/thestage_api/dtos/enums/currency_type.py +10 -10
  76. thestage/services/clients/thestage_api/dtos/enums/daemon_status.py +9 -9
  77. thestage/services/clients/thestage_api/dtos/enums/disk_type.py +7 -7
  78. thestage/services/clients/thestage_api/dtos/enums/drive_type.py +7 -7
  79. thestage/services/clients/thestage_api/dtos/enums/gpu_name.py +8 -8
  80. thestage/services/clients/thestage_api/dtos/enums/inference_model_status.py +9 -9
  81. thestage/services/clients/thestage_api/dtos/enums/inference_simulator_status.py +15 -15
  82. thestage/services/clients/thestage_api/dtos/enums/instance_rented_status.py +17 -17
  83. thestage/services/clients/thestage_api/dtos/enums/instance_type.py +7 -7
  84. thestage/services/clients/thestage_api/dtos/enums/location_region.py +11 -11
  85. thestage/services/clients/thestage_api/dtos/enums/power_status.py +10 -10
  86. thestage/services/clients/thestage_api/dtos/enums/provider_name.py +11 -11
  87. thestage/services/clients/thestage_api/dtos/enums/selfhosted_status.py +10 -10
  88. thestage/services/clients/thestage_api/dtos/enums/task_execution_status.py +12 -12
  89. thestage/services/clients/thestage_api/dtos/enums/task_status.py +12 -12
  90. thestage/services/clients/thestage_api/dtos/frontend_status.py +10 -10
  91. thestage/services/clients/thestage_api/dtos/inference_controller/deploy_inference_model_to_instance_request.py +13 -13
  92. thestage/services/clients/thestage_api/dtos/inference_controller/deploy_inference_model_to_instance_response.py +13 -13
  93. thestage/services/clients/thestage_api/dtos/inference_controller/deploy_inference_model_to_sagemaker_request.py +12 -12
  94. thestage/services/clients/thestage_api/dtos/inference_controller/deploy_inference_model_to_sagemaker_response.py +12 -12
  95. thestage/services/clients/thestage_api/dtos/inference_controller/get_inference_simulator_request.py +10 -10
  96. thestage/services/clients/thestage_api/dtos/inference_controller/get_inference_simulator_response.py +13 -13
  97. thestage/services/clients/thestage_api/dtos/inference_controller/inference_simulator_list_for_project_request.py +14 -14
  98. thestage/services/clients/thestage_api/dtos/inference_controller/inference_simulator_list_for_project_response.py +12 -12
  99. thestage/services/clients/thestage_api/dtos/inference_controller/inference_simulator_model_list_for_project_request.py +12 -12
  100. thestage/services/clients/thestage_api/dtos/inference_controller/inference_simulator_model_list_for_project_response.py +13 -13
  101. thestage/services/clients/thestage_api/dtos/inference_simulator_model_response.py +11 -11
  102. thestage/services/clients/thestage_api/dtos/inference_simulator_response.py +11 -11
  103. thestage/services/clients/thestage_api/dtos/installed_service.py +17 -17
  104. thestage/services/clients/thestage_api/dtos/instance_detected_gpus.py +20 -20
  105. thestage/services/clients/thestage_api/dtos/instance_rented_response.py +71 -71
  106. thestage/services/clients/thestage_api/dtos/logging_controller/docker_container_log_stream_request.py +7 -7
  107. thestage/services/clients/thestage_api/dtos/logging_controller/log_polling_request.py +13 -13
  108. thestage/services/clients/thestage_api/dtos/logging_controller/log_polling_response.py +14 -14
  109. thestage/services/clients/thestage_api/dtos/logging_controller/task_log_stream_request.py +7 -7
  110. thestage/services/clients/thestage_api/dtos/logging_controller/user_logs_query_request.py +21 -21
  111. thestage/services/clients/thestage_api/dtos/logging_controller/user_logs_query_response.py +14 -14
  112. thestage/services/clients/thestage_api/dtos/paginated_entity_list.py +11 -11
  113. thestage/services/clients/thestage_api/dtos/pagination_data.py +10 -10
  114. thestage/services/clients/thestage_api/dtos/price_definition.py +14 -14
  115. thestage/services/clients/thestage_api/dtos/project_controller/project_get_deploy_ssh_key_request.py +7 -7
  116. thestage/services/clients/thestage_api/dtos/project_controller/project_get_deploy_ssh_key_response.py +10 -10
  117. thestage/services/clients/thestage_api/dtos/project_controller/project_push_inference_simulator_model_request.py +8 -8
  118. thestage/services/clients/thestage_api/dtos/project_controller/project_push_inference_simulator_model_response.py +6 -6
  119. thestage/services/clients/thestage_api/dtos/project_controller/project_run_task_request.py +15 -15
  120. thestage/services/clients/thestage_api/dtos/project_controller/project_run_task_response.py +10 -10
  121. thestage/services/clients/thestage_api/dtos/project_controller/project_start_inference_simulator_request.py +13 -13
  122. thestage/services/clients/thestage_api/dtos/project_controller/project_start_inference_simulator_response.py +10 -10
  123. thestage/services/clients/thestage_api/dtos/project_response.py +32 -32
  124. thestage/services/clients/thestage_api/dtos/selfhosted_instance_response.py +56 -56
  125. thestage/services/clients/thestage_api/dtos/sftp_path_helper.py +13 -13
  126. thestage/services/clients/thestage_api/dtos/ssh_key_controller/add_ssh_key_to_user_request.py +8 -8
  127. thestage/services/clients/thestage_api/dtos/ssh_key_controller/add_ssh_key_to_user_response.py +11 -11
  128. thestage/services/clients/thestage_api/dtos/ssh_key_controller/add_ssh_public_key_to_instance_request.py +8 -8
  129. thestage/services/clients/thestage_api/dtos/ssh_key_controller/add_ssh_public_key_to_instance_response.py +11 -11
  130. thestage/services/clients/thestage_api/dtos/ssh_key_controller/is_user_has_public_ssh_key_request.py +7 -7
  131. thestage/services/clients/thestage_api/dtos/ssh_key_controller/is_user_has_public_ssh_key_response.py +12 -12
  132. thestage/services/clients/thestage_api/dtos/task_controller/task_list_for_project_request.py +10 -10
  133. thestage/services/clients/thestage_api/dtos/task_controller/task_list_for_project_response.py +12 -12
  134. thestage/services/clients/thestage_api/dtos/task_controller/task_status_localized_map_response.py +9 -9
  135. thestage/services/clients/thestage_api/dtos/task_controller/task_view_response.py +12 -12
  136. thestage/services/clients/thestage_api/dtos/user_controller/user_profile.py +12 -12
  137. thestage/services/clients/thestage_api/dtos/validate_token_response.py +11 -11
  138. thestage/services/config_provider/__init__.py +0 -0
  139. thestage/services/config_provider/config_provider.py +237 -237
  140. thestage/services/connect/connect_service.py +193 -196
  141. thestage/services/connect/dto/remote_server_config.py +9 -9
  142. thestage/services/container/__init__.py +0 -0
  143. thestage/services/container/container_service.py +374 -374
  144. thestage/services/container/mapper/__init__.py +0 -0
  145. thestage/services/container/mapper/container_mapper.py +30 -30
  146. thestage/services/core_files/config_entity.py +26 -26
  147. thestage/services/filesystem_service.py +133 -133
  148. thestage/services/instance/__init__.py +0 -0
  149. thestage/services/instance/instance_service.py +303 -303
  150. thestage/services/instance/mapper/__init__.py +0 -0
  151. thestage/services/instance/mapper/instance_mapper.py +24 -24
  152. thestage/services/instance/mapper/selfhosted_mapper.py +33 -33
  153. thestage/services/logging/byte_print_style.py +5 -5
  154. thestage/services/logging/dto/log_message.py +15 -15
  155. thestage/services/logging/dto/log_type.py +6 -6
  156. thestage/services/logging/exception/log_polling_exception.py +6 -6
  157. thestage/services/logging/logging_constants.py +3 -3
  158. thestage/services/logging/logging_service.py +367 -367
  159. thestage/services/project/__init__.py +0 -0
  160. thestage/services/project/dto/inference_simulator_dto.py +22 -22
  161. thestage/services/project/dto/inference_simulator_model_dto.py +20 -20
  162. thestage/services/project/dto/project_config.py +14 -14
  163. thestage/services/project/mapper/__init__.py +0 -0
  164. thestage/services/project/mapper/project_inference_simulator_mapper.py +21 -21
  165. thestage/services/project/mapper/project_inference_simulator_model_mapper.py +21 -21
  166. thestage/services/project/mapper/project_task_mapper.py +22 -22
  167. thestage/services/project/project_service.py +1253 -1253
  168. thestage/services/remote_server_service.py +609 -609
  169. thestage/services/service_factory.py +97 -97
  170. thestage/services/task/dto/task_dto.py +40 -40
  171. thestage/services/validation_service.py +61 -61
  172. {thestage-0.6.2.dist-info → thestage-0.6.4.dist-info}/LICENSE.txt +12 -12
  173. {thestage-0.6.2.dist-info → thestage-0.6.4.dist-info}/METADATA +3 -2
  174. thestage-0.6.4.dist-info/RECORD +176 -0
  175. {thestage-0.6.2.dist-info → thestage-0.6.4.dist-info}/WHEEL +1 -1
  176. thestage-0.6.2.dist-info/RECORD +0 -176
  177. {thestage-0.6.2.dist-info → thestage-0.6.4.dist-info}/entry_points.txt +0 -0
@@ -1,304 +1,304 @@
1
- from pathlib import Path
2
- from typing import List, Optional, Dict
3
-
4
- import typer
5
-
6
- from thestage.entities.rented_instance import RentedInstanceEntity
7
- from thestage.entities.self_hosted_instance import SelfHostedInstanceEntity
8
- from thestage.i18n.translation import __
9
- from thestage.services.clients.thestage_api.dtos.enums.selfhosted_status import SelfhostedBusinessStatus
10
- from thestage.services.clients.thestage_api.dtos.enums.instance_rented_status import InstanceRentedBusinessStatus
11
- from thestage.services.abstract_service import AbstractService
12
- from thestage.helpers.error_handler import error_handler
13
- from thestage.services.clients.thestage_api.api_client import TheStageApiClient
14
- from thestage.services.clients.thestage_api.dtos.instance_rented_response import InstanceRentedDto
15
- from thestage.services.clients.thestage_api.dtos.paginated_entity_list import PaginatedEntityList
16
- from thestage.services.clients.thestage_api.dtos.selfhosted_instance_response import SelfHostedInstanceDto
17
- from thestage.services.config_provider.config_provider import ConfigProvider
18
- from thestage.services.instance.mapper.instance_mapper import InstanceMapper
19
- from thestage.services.instance.mapper.selfhosted_mapper import SelfHostedMapper
20
- from thestage.services.remote_server_service import RemoteServerService
21
-
22
-
23
- class InstanceService(AbstractService):
24
-
25
- __thestage_api_client: TheStageApiClient = None
26
- __config_provider: ConfigProvider = None
27
-
28
- def __init__(
29
- self,
30
- thestage_api_client: TheStageApiClient,
31
- config_provider: ConfigProvider,
32
- remote_server_service: RemoteServerService,
33
- ):
34
- self.__thestage_api_client = thestage_api_client
35
- self.__remote_server_service = remote_server_service
36
- self.__config_provider = config_provider
37
-
38
- def get_rented_instance(
39
- self,
40
- instance_slug: str,
41
- ) -> Optional[InstanceRentedDto]:
42
- return self.__thestage_api_client.get_rented_instance(
43
- instance_slug=instance_slug,
44
- )
45
-
46
- def get_self_hosted_instance(
47
- self,
48
- instance_slug: str,
49
- ) -> Optional[SelfHostedInstanceDto]:
50
- return self.__thestage_api_client.get_selfhosted_instance(
51
- instance_slug=instance_slug,
52
- )
53
-
54
- @error_handler()
55
- def check_instance_status_to_connect(
56
- self,
57
- instance: InstanceRentedDto,
58
- ) -> InstanceRentedDto:
59
- if instance:
60
- if instance.frontend_status.status_key in [
61
- InstanceRentedBusinessStatus.IN_QUEUE.name,
62
- InstanceRentedBusinessStatus.CREATING.name,
63
- InstanceRentedBusinessStatus.REBOOTING.name,
64
- InstanceRentedBusinessStatus.STARTING.name,
65
- ]:
66
- typer.echo(__('Cannot connect to rented server instance: it is either in the process of being rented or rebooted'))
67
- raise typer.Exit(1)
68
- elif instance.frontend_status.status_key in [
69
- InstanceRentedBusinessStatus.TERMINATING.name,
70
- InstanceRentedBusinessStatus.RENTAL_ERROR.name,
71
- ]:
72
- typer.echo(__('Cannot connect to rented server instance: renting process failed'))
73
- raise typer.Exit(1)
74
- elif instance.frontend_status.status_key in [
75
- InstanceRentedBusinessStatus.STOPPED.name,
76
- InstanceRentedBusinessStatus.STOPPING.name,
77
- InstanceRentedBusinessStatus.DELETED.name,
78
- ]:
79
- typer.echo(__('Cannot connect to rented server instance: it is either stopped or has been deleted'))
80
- raise typer.Exit(1)
81
- elif instance.frontend_status.status_key in [
82
- InstanceRentedBusinessStatus.UNKNOWN.name,
83
- InstanceRentedBusinessStatus.ALL.name,
84
- ]:
85
- typer.echo(__('Cannot connect to rented server instance: instance status unknown'))
86
- raise typer.Exit(1)
87
-
88
- return instance
89
-
90
- @error_handler()
91
- def check_selfhosted_status_to_connect(
92
- self,
93
- instance: SelfHostedInstanceDto,
94
- ) -> SelfHostedInstanceDto:
95
- if instance:
96
- if instance.frontend_status.status_key in [
97
- SelfhostedBusinessStatus.AWAITING_CONFIGURATION.name,
98
- ]:
99
- typer.echo(__('Cannot connect to self-hosted instance: it is awaiting configuration'))
100
- raise typer.Exit(1)
101
- elif instance.frontend_status.status_key in [
102
- SelfhostedBusinessStatus.UNREACHABLE_DAEMON.name,
103
- SelfhostedBusinessStatus.DELETED.name,
104
- ]:
105
- typer.echo(__('Cannot connect to self-hosted instance: it may be turned off or unreachable'))
106
- raise typer.Exit(1)
107
- elif instance.frontend_status.status_key in [
108
- SelfhostedBusinessStatus.UNKNOWN.name,
109
- SelfhostedBusinessStatus.ALL.name,
110
- ]:
111
- typer.echo(__('Cannot connect to self-hosted instance: instance status unknown'))
112
- raise typer.Exit(1)
113
-
114
- return instance
115
-
116
- @error_handler()
117
- def connect_to_rented_instance(
118
- self,
119
- instance_rented_slug: str,
120
- input_ssh_key_path: Optional[str]
121
- ):
122
- instance = self.get_rented_instance(instance_slug=instance_rented_slug)
123
-
124
- if instance:
125
- self.check_instance_status_to_connect(
126
- instance=instance,
127
- )
128
-
129
- ssh_path_from_config: Optional[str] = None
130
- if not input_ssh_key_path:
131
- ssh_path_from_config = self.__config_provider.get_valid_private_key_path_by_ip_address(instance.ip_address)
132
- if ssh_path_from_config:
133
- typer.echo(f"Using configured ssh key for this instance: {ssh_path_from_config}")
134
-
135
- if not input_ssh_key_path and not ssh_path_from_config:
136
- typer.echo('Using SSH agent to connect to server instance')
137
-
138
- self.__remote_server_service.connect_to_instance(
139
- ip_address=instance.ip_address,
140
- username=instance.host_username,
141
- private_key_path=ssh_path_from_config or input_ssh_key_path
142
- )
143
-
144
- # cannot really detect how ssh connection was ended. capturing stderr using subprocess feels bad/unreliable.
145
- if input_ssh_key_path:
146
- self.__config_provider.update_remote_server_config_entry(ip_address=instance.ip_address, ssh_key_path=Path(input_ssh_key_path))
147
- else:
148
- typer.echo(__("Server instance not found: %instance_item%", {'instance_item': instance_rented_slug}))
149
-
150
-
151
- @error_handler()
152
- def connect_to_selfhosted_instance(
153
- self,
154
- selfhosted_instance_slug: str,
155
- username: str,
156
- input_ssh_key_path: Optional[str],
157
- ):
158
- if not username:
159
- username = 'root'
160
- typer.echo(__("No remote server username provided, using 'root' as username"))
161
-
162
- instance = self.get_self_hosted_instance(instance_slug=selfhosted_instance_slug)
163
-
164
- if instance:
165
- self.check_selfhosted_status_to_connect(
166
- instance=instance,
167
- )
168
-
169
- ssh_path_from_config: Optional[str] = None
170
- if not input_ssh_key_path:
171
- ssh_path_from_config = self.__config_provider.get_valid_private_key_path_by_ip_address(instance.ip_address)
172
- if ssh_path_from_config:
173
- typer.echo(f"Using configured ssh key for this instance: {ssh_path_from_config}")
174
-
175
- if not input_ssh_key_path and not ssh_path_from_config:
176
- typer.echo('Using SSH agent to connect to server instance')
177
-
178
- self.__remote_server_service.connect_to_instance(
179
- ip_address=instance.ip_address,
180
- username=username,
181
- private_key_path=ssh_path_from_config or input_ssh_key_path
182
- )
183
-
184
- if input_ssh_key_path:
185
- self.__config_provider.update_remote_server_config_entry(ip_address=instance.ip_address, ssh_key_path=Path(input_ssh_key_path))
186
- else:
187
- typer.echo(__("Server instance not found: %instance_item%", {'instance_item': selfhosted_instance_slug}))
188
-
189
-
190
- @error_handler()
191
- def get_rented_list(
192
- self,
193
- statuses: List[str],
194
- row: int = 5,
195
- page: int = 1,
196
- ) -> PaginatedEntityList[InstanceRentedDto]:
197
- data = self.__thestage_api_client.get_rented_instance_list(
198
- statuses=statuses,
199
- page=page,
200
- limit=row,
201
- )
202
-
203
- return data
204
-
205
- @error_handler()
206
- def get_self_hosted_list(
207
- self,
208
- statuses: List[str],
209
- row: int = 5,
210
- page: int = 1,
211
- ) -> PaginatedEntityList[SelfHostedInstanceDto]:
212
- data = self.__thestage_api_client.get_selfhosted_instance_list(
213
- statuses=statuses,
214
- page=page,
215
- limit=row,
216
- )
217
- return data
218
-
219
-
220
- @error_handler()
221
- def print_self_hosted_instance_list(self, statuses, row, page):
222
- selfhosted_instance_status_map = self.__thestage_api_client.get_selfhosted_business_status_map()
223
-
224
- if not statuses:
225
- statuses = ({key: selfhosted_instance_status_map[key] for key in [
226
- SelfhostedBusinessStatus.AWAITING_CONFIGURATION,
227
- SelfhostedBusinessStatus.RUNNING,
228
- SelfhostedBusinessStatus.UNREACHABLE_DAEMON,
229
- ]}).values()
230
-
231
- if "all" in statuses:
232
- statuses = selfhosted_instance_status_map.values()
233
-
234
- for input_status_item in statuses:
235
- if input_status_item not in selfhosted_instance_status_map.values():
236
- typer.echo(__("'%invalid_status%' is not one of %valid_statuses%", {
237
- 'invalid_status': input_status_item,
238
- 'valid_statuses': str(list(selfhosted_instance_status_map.values()))
239
- }))
240
- raise typer.Exit(1)
241
-
242
- typer.echo(__(
243
- "Listing self-hosted instances with the following statuses: %statuses%, to view all self-hosted instances, use --status all",
244
- placeholders={
245
- 'statuses': ', '.join([status_item for status_item in statuses])
246
- }))
247
-
248
- backend_statuses: List[str] = [key for key, value in selfhosted_instance_status_map.items() if value in statuses]
249
-
250
- self.print(
251
- func_get_data=self.get_self_hosted_list,
252
- func_special_params={
253
- 'statuses': backend_statuses,
254
- },
255
- mapper=SelfHostedMapper(),
256
- headers=list(map(lambda x: x.alias, SelfHostedInstanceEntity.model_fields.values())),
257
- row=row,
258
- page=page,
259
- show_index="never",
260
- )
261
-
262
-
263
- @error_handler()
264
- def print_rented_instance_list(self, statuses, row, page):
265
- instance_rented_status_map = self.__thestage_api_client.get_rented_business_status_map()
266
-
267
- if not statuses:
268
- statuses = ({key: instance_rented_status_map[key] for key in [
269
- InstanceRentedBusinessStatus.ONLINE,
270
- InstanceRentedBusinessStatus.CREATING,
271
- InstanceRentedBusinessStatus.TERMINATING,
272
- InstanceRentedBusinessStatus.REBOOTING,
273
- ]}).values()
274
-
275
- if "all" in statuses:
276
- statuses = instance_rented_status_map.values()
277
-
278
- for input_status_item in statuses:
279
- if input_status_item not in instance_rented_status_map.values():
280
- typer.echo(__("'%invalid_status%' is not one of %valid_statuses%", {
281
- 'invalid_status': input_status_item,
282
- 'valid_statuses': str(list(instance_rented_status_map.values()))
283
- }))
284
- raise typer.Exit(1)
285
-
286
- typer.echo(__(
287
- "Listing rented server instances with the following statuses: %statuses%, to view all rented server instances, use --status all",
288
- placeholders={
289
- 'statuses': ', '.join([status_item for status_item in statuses])
290
- }))
291
-
292
- backend_statuses: List[str] = [key for key, value in instance_rented_status_map.items() if value in statuses]
293
-
294
- self.print(
295
- func_get_data=self.get_rented_list,
296
- func_special_params={
297
- 'statuses': backend_statuses,
298
- },
299
- mapper=InstanceMapper(),
300
- headers=list(map(lambda x: x.alias, RentedInstanceEntity.model_fields.values())),
301
- row=row,
302
- page=page,
303
- show_index="never",
1
+ from pathlib import Path
2
+ from typing import List, Optional, Dict
3
+
4
+ import typer
5
+
6
+ from thestage.entities.rented_instance import RentedInstanceEntity
7
+ from thestage.entities.self_hosted_instance import SelfHostedInstanceEntity
8
+ from thestage.i18n.translation import __
9
+ from thestage.services.clients.thestage_api.dtos.enums.selfhosted_status import SelfhostedBusinessStatus
10
+ from thestage.services.clients.thestage_api.dtos.enums.instance_rented_status import InstanceRentedBusinessStatus
11
+ from thestage.services.abstract_service import AbstractService
12
+ from thestage.helpers.error_handler import error_handler
13
+ from thestage.services.clients.thestage_api.api_client import TheStageApiClient
14
+ from thestage.services.clients.thestage_api.dtos.instance_rented_response import InstanceRentedDto
15
+ from thestage.services.clients.thestage_api.dtos.paginated_entity_list import PaginatedEntityList
16
+ from thestage.services.clients.thestage_api.dtos.selfhosted_instance_response import SelfHostedInstanceDto
17
+ from thestage.services.config_provider.config_provider import ConfigProvider
18
+ from thestage.services.instance.mapper.instance_mapper import InstanceMapper
19
+ from thestage.services.instance.mapper.selfhosted_mapper import SelfHostedMapper
20
+ from thestage.services.remote_server_service import RemoteServerService
21
+
22
+
23
+ class InstanceService(AbstractService):
24
+
25
+ __thestage_api_client: TheStageApiClient = None
26
+ __config_provider: ConfigProvider = None
27
+
28
+ def __init__(
29
+ self,
30
+ thestage_api_client: TheStageApiClient,
31
+ config_provider: ConfigProvider,
32
+ remote_server_service: RemoteServerService,
33
+ ):
34
+ self.__thestage_api_client = thestage_api_client
35
+ self.__remote_server_service = remote_server_service
36
+ self.__config_provider = config_provider
37
+
38
+ def get_rented_instance(
39
+ self,
40
+ instance_slug: str,
41
+ ) -> Optional[InstanceRentedDto]:
42
+ return self.__thestage_api_client.get_rented_instance(
43
+ instance_slug=instance_slug,
44
+ )
45
+
46
+ def get_self_hosted_instance(
47
+ self,
48
+ instance_slug: str,
49
+ ) -> Optional[SelfHostedInstanceDto]:
50
+ return self.__thestage_api_client.get_selfhosted_instance(
51
+ instance_slug=instance_slug,
52
+ )
53
+
54
+ @error_handler()
55
+ def check_instance_status_to_connect(
56
+ self,
57
+ instance: InstanceRentedDto,
58
+ ) -> InstanceRentedDto:
59
+ if instance:
60
+ if instance.frontend_status.status_key in [
61
+ InstanceRentedBusinessStatus.IN_QUEUE.name,
62
+ InstanceRentedBusinessStatus.CREATING.name,
63
+ InstanceRentedBusinessStatus.REBOOTING.name,
64
+ InstanceRentedBusinessStatus.STARTING.name,
65
+ ]:
66
+ typer.echo(__('Cannot connect to rented server instance: it is either in the process of being rented or rebooted'))
67
+ raise typer.Exit(1)
68
+ elif instance.frontend_status.status_key in [
69
+ InstanceRentedBusinessStatus.TERMINATING.name,
70
+ InstanceRentedBusinessStatus.RENTAL_ERROR.name,
71
+ ]:
72
+ typer.echo(__('Cannot connect to rented server instance: renting process failed'))
73
+ raise typer.Exit(1)
74
+ elif instance.frontend_status.status_key in [
75
+ InstanceRentedBusinessStatus.STOPPED.name,
76
+ InstanceRentedBusinessStatus.STOPPING.name,
77
+ InstanceRentedBusinessStatus.DELETED.name,
78
+ ]:
79
+ typer.echo(__('Cannot connect to rented server instance: it is either stopped or has been deleted'))
80
+ raise typer.Exit(1)
81
+ elif instance.frontend_status.status_key in [
82
+ InstanceRentedBusinessStatus.UNKNOWN.name,
83
+ InstanceRentedBusinessStatus.ALL.name,
84
+ ]:
85
+ typer.echo(__('Cannot connect to rented server instance: instance status unknown'))
86
+ raise typer.Exit(1)
87
+
88
+ return instance
89
+
90
+ @error_handler()
91
+ def check_selfhosted_status_to_connect(
92
+ self,
93
+ instance: SelfHostedInstanceDto,
94
+ ) -> SelfHostedInstanceDto:
95
+ if instance:
96
+ if instance.frontend_status.status_key in [
97
+ SelfhostedBusinessStatus.AWAITING_CONFIGURATION.name,
98
+ ]:
99
+ typer.echo(__('Cannot connect to self-hosted instance: it is awaiting configuration'))
100
+ raise typer.Exit(1)
101
+ elif instance.frontend_status.status_key in [
102
+ SelfhostedBusinessStatus.UNREACHABLE_DAEMON.name,
103
+ SelfhostedBusinessStatus.DELETED.name,
104
+ ]:
105
+ typer.echo(__('Cannot connect to self-hosted instance: it may be turned off or unreachable'))
106
+ raise typer.Exit(1)
107
+ elif instance.frontend_status.status_key in [
108
+ SelfhostedBusinessStatus.UNKNOWN.name,
109
+ SelfhostedBusinessStatus.ALL.name,
110
+ ]:
111
+ typer.echo(__('Cannot connect to self-hosted instance: instance status unknown'))
112
+ raise typer.Exit(1)
113
+
114
+ return instance
115
+
116
+ @error_handler()
117
+ def connect_to_rented_instance(
118
+ self,
119
+ instance_rented_slug: str,
120
+ input_ssh_key_path: Optional[str]
121
+ ):
122
+ instance = self.get_rented_instance(instance_slug=instance_rented_slug)
123
+
124
+ if instance:
125
+ self.check_instance_status_to_connect(
126
+ instance=instance,
127
+ )
128
+
129
+ ssh_path_from_config: Optional[str] = None
130
+ if not input_ssh_key_path:
131
+ ssh_path_from_config = self.__config_provider.get_valid_private_key_path_by_ip_address(instance.ip_address)
132
+ if ssh_path_from_config:
133
+ typer.echo(f"Using configured ssh key for this instance: {ssh_path_from_config}")
134
+
135
+ if not input_ssh_key_path and not ssh_path_from_config:
136
+ typer.echo('Using SSH agent to connect to server instance')
137
+
138
+ self.__remote_server_service.connect_to_instance(
139
+ ip_address=instance.ip_address,
140
+ username=instance.host_username,
141
+ private_key_path=ssh_path_from_config or input_ssh_key_path
142
+ )
143
+
144
+ # cannot really detect how ssh connection was ended. capturing stderr using subprocess feels bad/unreliable.
145
+ if input_ssh_key_path:
146
+ self.__config_provider.update_remote_server_config_entry(ip_address=instance.ip_address, ssh_key_path=Path(input_ssh_key_path))
147
+ else:
148
+ typer.echo(__("Server instance not found: %instance_item%", {'instance_item': instance_rented_slug}))
149
+
150
+
151
+ @error_handler()
152
+ def connect_to_selfhosted_instance(
153
+ self,
154
+ selfhosted_instance_slug: str,
155
+ username: str,
156
+ input_ssh_key_path: Optional[str],
157
+ ):
158
+ if not username:
159
+ username = 'root'
160
+ typer.echo(__("No remote server username provided, using 'root' as username"))
161
+
162
+ instance = self.get_self_hosted_instance(instance_slug=selfhosted_instance_slug)
163
+
164
+ if instance:
165
+ self.check_selfhosted_status_to_connect(
166
+ instance=instance,
167
+ )
168
+
169
+ ssh_path_from_config: Optional[str] = None
170
+ if not input_ssh_key_path:
171
+ ssh_path_from_config = self.__config_provider.get_valid_private_key_path_by_ip_address(instance.ip_address)
172
+ if ssh_path_from_config:
173
+ typer.echo(f"Using configured ssh key for this instance: {ssh_path_from_config}")
174
+
175
+ if not input_ssh_key_path and not ssh_path_from_config:
176
+ typer.echo('Using SSH agent to connect to server instance')
177
+
178
+ self.__remote_server_service.connect_to_instance(
179
+ ip_address=instance.ip_address,
180
+ username=username,
181
+ private_key_path=ssh_path_from_config or input_ssh_key_path
182
+ )
183
+
184
+ if input_ssh_key_path:
185
+ self.__config_provider.update_remote_server_config_entry(ip_address=instance.ip_address, ssh_key_path=Path(input_ssh_key_path))
186
+ else:
187
+ typer.echo(__("Server instance not found: %instance_item%", {'instance_item': selfhosted_instance_slug}))
188
+
189
+
190
+ @error_handler()
191
+ def get_rented_list(
192
+ self,
193
+ statuses: List[str],
194
+ row: int = 5,
195
+ page: int = 1,
196
+ ) -> PaginatedEntityList[InstanceRentedDto]:
197
+ data = self.__thestage_api_client.get_rented_instance_list(
198
+ statuses=statuses,
199
+ page=page,
200
+ limit=row,
201
+ )
202
+
203
+ return data
204
+
205
+ @error_handler()
206
+ def get_self_hosted_list(
207
+ self,
208
+ statuses: List[str],
209
+ row: int = 5,
210
+ page: int = 1,
211
+ ) -> PaginatedEntityList[SelfHostedInstanceDto]:
212
+ data = self.__thestage_api_client.get_selfhosted_instance_list(
213
+ statuses=statuses,
214
+ page=page,
215
+ limit=row,
216
+ )
217
+ return data
218
+
219
+
220
+ @error_handler()
221
+ def print_self_hosted_instance_list(self, statuses, row, page):
222
+ selfhosted_instance_status_map = self.__thestage_api_client.get_selfhosted_business_status_map()
223
+
224
+ if not statuses:
225
+ statuses = ({key: selfhosted_instance_status_map[key] for key in [
226
+ SelfhostedBusinessStatus.AWAITING_CONFIGURATION,
227
+ SelfhostedBusinessStatus.RUNNING,
228
+ SelfhostedBusinessStatus.UNREACHABLE_DAEMON,
229
+ ]}).values()
230
+
231
+ if "all" in statuses:
232
+ statuses = selfhosted_instance_status_map.values()
233
+
234
+ for input_status_item in statuses:
235
+ if input_status_item not in selfhosted_instance_status_map.values():
236
+ typer.echo(__("'%invalid_status%' is not one of %valid_statuses%", {
237
+ 'invalid_status': input_status_item,
238
+ 'valid_statuses': str(list(selfhosted_instance_status_map.values()))
239
+ }))
240
+ raise typer.Exit(1)
241
+
242
+ typer.echo(__(
243
+ "Listing self-hosted instances with the following statuses: %statuses%, to view all self-hosted instances, use --status all",
244
+ placeholders={
245
+ 'statuses': ', '.join([status_item for status_item in statuses])
246
+ }))
247
+
248
+ backend_statuses: List[str] = [key for key, value in selfhosted_instance_status_map.items() if value in statuses]
249
+
250
+ self.print(
251
+ func_get_data=self.get_self_hosted_list,
252
+ func_special_params={
253
+ 'statuses': backend_statuses,
254
+ },
255
+ mapper=SelfHostedMapper(),
256
+ headers=list(map(lambda x: x.alias, SelfHostedInstanceEntity.model_fields.values())),
257
+ row=row,
258
+ page=page,
259
+ show_index="never",
260
+ )
261
+
262
+
263
+ @error_handler()
264
+ def print_rented_instance_list(self, statuses, row, page):
265
+ instance_rented_status_map = self.__thestage_api_client.get_rented_business_status_map()
266
+
267
+ if not statuses:
268
+ statuses = ({key: instance_rented_status_map[key] for key in [
269
+ InstanceRentedBusinessStatus.ONLINE,
270
+ InstanceRentedBusinessStatus.CREATING,
271
+ InstanceRentedBusinessStatus.TERMINATING,
272
+ InstanceRentedBusinessStatus.REBOOTING,
273
+ ]}).values()
274
+
275
+ if "all" in statuses:
276
+ statuses = instance_rented_status_map.values()
277
+
278
+ for input_status_item in statuses:
279
+ if input_status_item not in instance_rented_status_map.values():
280
+ typer.echo(__("'%invalid_status%' is not one of %valid_statuses%", {
281
+ 'invalid_status': input_status_item,
282
+ 'valid_statuses': str(list(instance_rented_status_map.values()))
283
+ }))
284
+ raise typer.Exit(1)
285
+
286
+ typer.echo(__(
287
+ "Listing rented server instances with the following statuses: %statuses%, to view all rented server instances, use --status all",
288
+ placeholders={
289
+ 'statuses': ', '.join([status_item for status_item in statuses])
290
+ }))
291
+
292
+ backend_statuses: List[str] = [key for key, value in instance_rented_status_map.items() if value in statuses]
293
+
294
+ self.print(
295
+ func_get_data=self.get_rented_list,
296
+ func_special_params={
297
+ 'statuses': backend_statuses,
298
+ },
299
+ mapper=InstanceMapper(),
300
+ headers=list(map(lambda x: x.alias, RentedInstanceEntity.model_fields.values())),
301
+ row=row,
302
+ page=page,
303
+ show_index="never",
304
304
  )
File without changes
@@ -1,24 +1,24 @@
1
- from typing import Optional
2
-
3
- from thestage.entities.rented_instance import RentedInstanceEntity
4
- from thestage.services.clients.thestage_api.dtos.instance_rented_response import InstanceRentedDto
5
- from thestage.services.abstract_mapper import AbstractMapper
6
-
7
-
8
- class InstanceMapper(AbstractMapper):
9
-
10
- def build_entity(self, item: InstanceRentedDto) -> Optional[RentedInstanceEntity]:
11
- if not item:
12
- return None
13
-
14
- return RentedInstanceEntity(
15
- slug=item.slug if item.slug else '',
16
- title=item.title if item.title else '',
17
- cpu_type=item.cpu_type if item.cpu_type else '',
18
- gpu_type=item.gpu_type if item.gpu_type else '',
19
- cpu_cores=str(item.cpu_cores) if item.cpu_cores else '',
20
- ip_address=item.ip_address if item.ip_address else '',
21
- status=item.frontend_status.status_translation if item.frontend_status else '',
22
- created_at=str(item.created_at.strftime("%Y-%m-%d %H:%M:%S")) if item.created_at else '',
23
- updated_at=str(item.updated_at.strftime("%Y-%m-%d %H:%M:%S")) if item.updated_at else '',
24
- )
1
+ from typing import Optional
2
+
3
+ from thestage.entities.rented_instance import RentedInstanceEntity
4
+ from thestage.services.clients.thestage_api.dtos.instance_rented_response import InstanceRentedDto
5
+ from thestage.services.abstract_mapper import AbstractMapper
6
+
7
+
8
+ class InstanceMapper(AbstractMapper):
9
+
10
+ def build_entity(self, item: InstanceRentedDto) -> Optional[RentedInstanceEntity]:
11
+ if not item:
12
+ return None
13
+
14
+ return RentedInstanceEntity(
15
+ slug=item.slug if item.slug else '',
16
+ title=item.title if item.title else '',
17
+ cpu_type=item.cpu_type if item.cpu_type else '',
18
+ gpu_type=item.gpu_type if item.gpu_type else '',
19
+ cpu_cores=str(item.cpu_cores) if item.cpu_cores else '',
20
+ ip_address=item.ip_address if item.ip_address else '',
21
+ status=item.frontend_status.status_translation if item.frontend_status else '',
22
+ created_at=str(item.created_at.strftime("%Y-%m-%d %H:%M:%S")) if item.created_at else '',
23
+ updated_at=str(item.updated_at.strftime("%Y-%m-%d %H:%M:%S")) if item.updated_at else '',
24
+ )