thestage 0.6.5__py3-none-any.whl → 0.6.7__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 (75) hide show
  1. thestage/__init__.py +1 -1
  2. thestage/color_scheme/color_scheme.py +1 -0
  3. thestage/controllers/base_controller.py +4 -3
  4. thestage/controllers/config_controller.py +16 -4
  5. thestage/controllers/container_controller.py +151 -106
  6. thestage/controllers/instance_controller.py +35 -9
  7. thestage/controllers/project_controller.py +335 -89
  8. thestage/entities/container.py +5 -3
  9. thestage/entities/project_inference_simulator.py +2 -1
  10. thestage/entities/project_inference_simulator_model.py +2 -2
  11. thestage/entities/project_task.py +2 -3
  12. thestage/entities/rented_instance.py +2 -2
  13. thestage/entities/self_hosted_instance.py +2 -2
  14. thestage/helpers/error_handler.py +1 -1
  15. thestage/main.py +1 -1
  16. thestage/services/clients/git/git_client.py +1 -1
  17. thestage/services/clients/thestage_api/api_client.py +142 -109
  18. thestage/services/clients/thestage_api/dtos/base_controller/connect_resolve_response.py +22 -0
  19. thestage/services/clients/thestage_api/dtos/container_param_request.py +1 -1
  20. thestage/services/clients/thestage_api/dtos/container_response.py +1 -21
  21. thestage/services/clients/thestage_api/dtos/docker_container_controller/docker_container_list_request.py +2 -1
  22. thestage/services/clients/thestage_api/dtos/inference_controller/deploy_inference_model_to_instance_request.py +5 -1
  23. thestage/services/clients/thestage_api/dtos/inference_controller/deploy_inference_model_to_instance_response.py +2 -1
  24. thestage/services/clients/thestage_api/dtos/inference_controller/deploy_inference_model_to_sagemaker_request.py +1 -0
  25. thestage/services/clients/thestage_api/dtos/inference_controller/get_inference_simulator_request.py +2 -1
  26. thestage/services/clients/thestage_api/dtos/inference_controller/{inference_simulator_list_for_project_request.py → inference_simulator_list_request.py} +3 -2
  27. thestage/services/clients/thestage_api/dtos/inference_controller/{inference_simulator_list_for_project_response.py → inference_simulator_list_response.py} +1 -1
  28. thestage/services/clients/thestage_api/dtos/inference_controller/inference_simulator_model_list_for_project_request.py +2 -1
  29. thestage/services/clients/thestage_api/dtos/instance_rented_response.py +4 -37
  30. thestage/services/clients/thestage_api/dtos/logging_controller/log_polling_request.py +3 -3
  31. thestage/services/clients/thestage_api/dtos/logging_controller/user_logs_query_request.py +3 -11
  32. thestage/services/clients/thestage_api/dtos/project_controller/project_get_deploy_ssh_key_request.py +1 -0
  33. thestage/services/clients/thestage_api/dtos/project_controller/project_push_inference_simulator_model_request.py +2 -1
  34. thestage/services/clients/thestage_api/dtos/project_controller/project_run_task_request.py +2 -4
  35. thestage/services/clients/thestage_api/dtos/project_controller/project_run_task_response.py +3 -0
  36. thestage/services/clients/thestage_api/dtos/project_controller/project_start_inference_simulator_request.py +5 -3
  37. thestage/services/clients/thestage_api/dtos/project_response.py +3 -15
  38. thestage/services/clients/thestage_api/dtos/selfhosted_instance_response.py +2 -20
  39. thestage/services/clients/thestage_api/dtos/ssh_key_controller/add_ssh_key_to_user_response.py +1 -1
  40. thestage/services/clients/thestage_api/dtos/ssh_key_controller/add_ssh_public_key_to_instance_request.py +4 -2
  41. thestage/services/clients/thestage_api/dtos/ssh_key_controller/is_user_has_public_ssh_key_response.py +1 -1
  42. thestage/services/clients/thestage_api/dtos/task_controller/task_list_for_project_request.py +4 -1
  43. thestage/services/clients/thestage_api/dtos/task_controller/task_view_response.py +0 -2
  44. thestage/services/config_provider/config_provider.py +2 -2
  45. thestage/services/connect/connect_service.py +77 -74
  46. thestage/services/container/container_service.py +120 -41
  47. thestage/services/container/mapper/container_mapper.py +2 -1
  48. thestage/services/instance/instance_service.py +13 -20
  49. thestage/services/instance/mapper/instance_mapper.py +1 -3
  50. thestage/services/instance/mapper/selfhosted_mapper.py +3 -4
  51. thestage/services/logging/logging_service.py +45 -48
  52. thestage/services/project/dto/inference_simulator_dto.py +1 -10
  53. thestage/services/project/dto/inference_simulator_model_dto.py +2 -10
  54. thestage/services/project/dto/project_config.py +2 -2
  55. thestage/services/project/mapper/project_inference_simulator_mapper.py +1 -0
  56. thestage/services/project/mapper/project_inference_simulator_model_mapper.py +2 -2
  57. thestage/services/project/mapper/project_task_mapper.py +2 -3
  58. thestage/services/project/project_service.py +174 -140
  59. thestage/services/remote_server_service.py +1 -0
  60. thestage/services/task/dto/task_dto.py +3 -23
  61. {thestage-0.6.5.dist-info → thestage-0.6.7.dist-info}/METADATA +4 -2
  62. {thestage-0.6.5.dist-info → thestage-0.6.7.dist-info}/RECORD +65 -74
  63. {thestage-0.6.5.dist-info → thestage-0.6.7.dist-info}/WHEEL +1 -1
  64. thestage/services/clients/thestage_api/dtos/cloud_provider_region.py +0 -19
  65. thestage/services/clients/thestage_api/dtos/docker_container_assigned_device.py +0 -10
  66. thestage/services/clients/thestage_api/dtos/enums/currency_type.py +0 -10
  67. thestage/services/clients/thestage_api/dtos/enums/daemon_status.py +0 -9
  68. thestage/services/clients/thestage_api/dtos/enums/disk_type.py +0 -7
  69. thestage/services/clients/thestage_api/dtos/enums/drive_type.py +0 -7
  70. thestage/services/clients/thestage_api/dtos/enums/instance_type.py +0 -7
  71. thestage/services/clients/thestage_api/dtos/enums/location_region.py +0 -11
  72. thestage/services/clients/thestage_api/dtos/enums/power_status.py +0 -10
  73. thestage/services/clients/thestage_api/dtos/price_definition.py +0 -14
  74. {thestage-0.6.5.dist-info → thestage-0.6.7.dist-info}/entry_points.txt +0 -0
  75. {thestage-0.6.5.dist-info → thestage-0.6.7.dist-info/licenses}/LICENSE.txt +0 -0
@@ -3,21 +3,18 @@ import typer
3
3
 
4
4
  from thestage.cli_command import CliCommand
5
5
  from thestage.cli_command_helper import check_command_permission
6
+ from thestage.color_scheme.color_scheme import ColorScheme
6
7
  from thestage.i18n.translation import __
7
8
  from thestage.services.clients.thestage_api.core.http_client_exception import HttpClientException
8
- from thestage.services.clients.thestage_api.dtos.enums.container_status import DockerContainerStatus
9
- from thestage.services.clients.thestage_api.dtos.enums.selfhosted_status import SelfhostedBusinessStatus
10
9
  from thestage.services.clients.thestage_api.dtos.enums.instance_rented_status import InstanceRentedBusinessStatus
11
10
  from thestage.services.abstract_service import AbstractService
12
11
  from thestage.helpers.error_handler import error_handler
13
12
  from thestage.services.clients.thestage_api.api_client import TheStageApiClient
14
- from thestage.services.clients.thestage_api.dtos.enums.task_status import TaskStatus
15
13
  from thestage.services.clients.thestage_api.dtos.instance_rented_response import InstanceRentedDto
16
14
  from thestage.services.container.container_service import ContainerService
17
15
  from thestage.services.instance.instance_service import InstanceService
18
16
  from thestage.services.logging.logging_service import LoggingService
19
- from thestage.services.task.dto.task_dto import TaskDto
20
- from thestage.helpers.logger.app_logger import app_logger
17
+ from rich import print
21
18
 
22
19
 
23
20
  class ConnectService(AbstractService):
@@ -44,113 +41,119 @@ class ConnectService(AbstractService):
44
41
  @error_handler()
45
42
  def connect_to_entity(
46
43
  self,
47
- uid: str,
44
+ input_entity_identifier: str,
48
45
  username: Optional[str],
49
46
  private_key_path: Optional[str],
50
47
  ):
51
- try:
52
- instance_selfhosted = self.__thestage_api_client.get_selfhosted_instance(instance_slug=uid)
53
- except HttpClientException as e:
54
- app_logger.warn(f"get_selfhosted_instance: code {e.get_status_code()}")
55
- instance_selfhosted = None
56
-
57
- try:
58
- instance_rented = self.__thestage_api_client.get_rented_instance(instance_slug=uid)
59
- except HttpClientException as e:
60
- app_logger.warn(f"get_rented_instance: code {e.get_status_code()}")
61
- instance_rented = None
62
-
63
- try:
64
- container = self.__thestage_api_client.get_container(container_slug=uid,)
65
- except HttpClientException as e:
66
- app_logger.warn(f"get_container: code {e.get_status_code()}")
67
- container = None
68
-
69
- task: Optional[TaskDto] = None
70
- if uid.isdigit():
71
- try:
72
- task_view_response = self.__thestage_api_client.get_task(task_id=int(uid))
73
- except HttpClientException as e:
74
- app_logger.warn(f"get_task error: code {e.get_status_code()}")
75
- task_view_response = None
76
- if task_view_response and task_view_response.task:
77
- task = task_view_response.task
78
-
79
- rented_exists = int(instance_rented is not None and instance_rented.frontend_status.status_key == InstanceRentedBusinessStatus.ONLINE)
80
- selfhosted_exists = int(instance_selfhosted is not None)
81
- container_exists = int(container is not None)
82
- task_exists = int(task is not None)
83
-
84
- rented_presence = int(rented_exists and instance_rented.frontend_status.status_key == InstanceRentedBusinessStatus.ONLINE)
85
- selfhosted_presence = int(selfhosted_exists and instance_selfhosted.frontend_status.status_key == SelfhostedBusinessStatus.RUNNING)
86
- container_presence = int(container_exists and (container.frontend_status.status_key == DockerContainerStatus.RUNNING or container.frontend_status.status_key == DockerContainerStatus.BUSY))
87
- task_presence = int(task_exists and task.frontend_status.status_key in [TaskStatus.RUNNING, TaskStatus.SCHEDULED])
88
-
89
- if rented_exists:
90
- typer.echo(__("Found a rented instance with the provided UID in status: '%rented_status%'", {"rented_status": instance_rented.frontend_status.status_translation}))
91
-
92
- if selfhosted_exists:
93
- typer.echo(__("Found a self-hosted instance with the provided UID in status: '%selfhosted_status%'", {"selfhosted_status": instance_selfhosted.frontend_status.status_translation}))
94
-
95
- if container_exists:
96
- typer.echo(__("Found a docker container with the provided UID in status: '%container_status%'", {"container_status": container.frontend_status.status_translation}))
97
-
98
- if task_exists:
99
- typer.echo(__("Found a task with the provided ID in status: '%task_status%'", {"task_status": task.frontend_status.status_translation}))
100
-
101
- if (rented_presence + selfhosted_presence + container_presence + task_presence) > 1:
48
+ resolved_options = self.__thestage_api_client.resolve_user_input(entity_identifier=input_entity_identifier)
49
+ entities_available_for_connect_count = 0
50
+ task_presence = False
51
+ container_presence = False
52
+ rented_presence = False
53
+ selfhosted_presence = False
54
+ resolved_entity_public_id = None
55
+
56
+ if resolved_options.taskMatchData:
57
+ for task_item in resolved_options.taskMatchData:
58
+ message = f"Found a task with with matching {task_item.matchedField} in status: '{task_item.frontendStatus.status_translation}' (ID: {task_item.publicId})"
59
+ line_color = ColorScheme.SUCCESS.value if task_item.canConnect else 'default'
60
+ print(f"[{line_color}]{message}[{line_color}]")
61
+ if task_item.canConnect:
62
+ task_presence = True
63
+ resolved_entity_public_id = task_item.publicId
64
+ entities_available_for_connect_count += 1
65
+
66
+ if resolved_options.dockerContainerMatchData:
67
+ for container_item in resolved_options.dockerContainerMatchData:
68
+ message = f"Found a container with matching {container_item.matchedField} in status: '{container_item.frontendStatus.status_translation}' (ID: {container_item.publicId})"
69
+ line_color = ColorScheme.SUCCESS.value if container_item.canConnect else 'default'
70
+ print(f"[{line_color}]{message}[{line_color}]")
71
+ if container_item.canConnect:
72
+ container_presence = True
73
+ resolved_entity_public_id = container_item.publicId
74
+ entities_available_for_connect_count += 1
75
+
76
+ if resolved_options.instanceRentedMatchData:
77
+ for instance_rented_item in resolved_options.instanceRentedMatchData:
78
+ message = f"Found a rented instance with matching {instance_rented_item.matchedField} in status: '{instance_rented_item.frontendStatus.status_translation}' (ID: {instance_rented_item.publicId})"
79
+ line_color = ColorScheme.SUCCESS.value if instance_rented_item.canConnect else 'default'
80
+ print(f"[{line_color}]{message}[{line_color}]")
81
+
82
+ if instance_rented_item.canConnect:
83
+ rented_presence = True
84
+ resolved_entity_public_id = instance_rented_item.publicId
85
+ entities_available_for_connect_count += 1
86
+
87
+ if resolved_options.selfhostedInstanceMatchData:
88
+ for selfhosted_item in resolved_options.selfhostedInstanceMatchData:
89
+ message = f"Found a self-hosted instance with matching {selfhosted_item.matchedField} in status: '{selfhosted_item.frontendStatus.status_translation}' (ID: {selfhosted_item.publicId})"
90
+ line_color = ColorScheme.SUCCESS.value if selfhosted_item.canConnect else 'default'
91
+ print(f"[{line_color}]{message}[{line_color}]")
92
+
93
+ if selfhosted_item.canConnect:
94
+ selfhosted_presence = True
95
+ resolved_entity_public_id = selfhosted_item.publicId
96
+ entities_available_for_connect_count += 1
97
+
98
+ if entities_available_for_connect_count > 1:
102
99
  typer.echo("Provided identifier caused ambiguity")
103
100
  typer.echo("Consider running a dedicated command to connect to the entity you need")
104
101
  raise typer.Exit(code=1)
105
102
 
106
- if (rented_presence + selfhosted_presence + container_presence + task_presence) == 0:
103
+ if entities_available_for_connect_count == 0:
107
104
  typer.echo("There is nothing to connect to with the provided identifier")
108
105
  raise typer.Exit(code=1)
109
106
 
110
107
  if rented_presence:
111
108
  check_command_permission(CliCommand.INSTANCE_RENTED_CONNECT)
112
- typer.echo("Connecting to rented instance...")
109
+ typer.echo(f"Connecting to rented instance '{resolved_entity_public_id}'...")
113
110
  self.__instance_service.connect_to_rented_instance(
114
- instance_rented_slug=uid,
111
+ instance_rented_public_id=resolved_entity_public_id,
112
+ instance_rented_slug=None,
115
113
  input_ssh_key_path=private_key_path
116
114
  )
117
115
 
118
116
  if container_presence:
119
117
  check_command_permission(CliCommand.CONTAINER_CONNECT)
120
- typer.echo("Connecting to docker container...")
118
+ typer.echo(f"Connecting to docker container '{resolved_entity_public_id}'...")
121
119
  self.__container_service.connect_to_container(
122
- container_uid=uid,
120
+ container_public_id=resolved_entity_public_id,
121
+ container_slug=None,
123
122
  username=username,
124
123
  input_ssh_key_path=private_key_path
125
124
  )
126
125
 
127
126
  if selfhosted_presence:
128
127
  check_command_permission(CliCommand.INSTANCE_SELF_HOSTED_CONNECT)
129
- typer.echo("Connecting to self-hosted instance...")
128
+ typer.echo(f"Connecting to self-hosted instance '{resolved_entity_public_id}'...")
130
129
 
131
130
  self.__instance_service.connect_to_selfhosted_instance(
132
- selfhosted_instance_slug=uid,
131
+ selfhosted_instance_public_id=resolved_entity_public_id,
132
+ selfhosted_instance_slug=None,
133
133
  username=username,
134
134
  input_ssh_key_path=private_key_path
135
135
  )
136
136
 
137
137
  if task_presence:
138
- typer.echo(__("Connecting to task..."))
139
- self.__logging_service.stream_task_logs_with_controls(task_id=int(uid))
138
+ typer.echo(f"Connecting to task '{resolved_entity_public_id}'...")
139
+ self.__logging_service.stream_task_logs_with_controls(task_public_id=resolved_entity_public_id)
140
140
 
141
141
 
142
142
  @error_handler()
143
- def upload_ssh_key(self, public_key_contents: str, instance_slug: Optional[str]):
143
+ def upload_ssh_key(self, public_key_contents: str, instance_public_id: Optional[str], instance_slug: Optional[str]):
144
144
  instance_rented: Optional[InstanceRentedDto] = None
145
- if instance_slug:
145
+ if instance_slug or instance_public_id:
146
146
  try:
147
- instance_rented = self.__thestage_api_client.get_rented_instance(instance_slug=instance_slug)
147
+ instance_rented = self.__thestage_api_client.get_rented_instance(
148
+ instance_public_id=instance_public_id,
149
+ instance_slug=instance_slug
150
+ )
148
151
  except HttpClientException as e:
149
152
  instance_rented = None
150
153
 
151
154
  # if no instances found - exit 1
152
155
  if instance_rented is None:
153
- typer.echo(f"No rented instance found with matching unique ID '{instance_slug}'")
156
+ typer.echo(f"No rented instance found with matching identifier")
154
157
  raise typer.Exit(1)
155
158
 
156
159
  note_to_send: Optional[str] = None
@@ -159,7 +162,7 @@ class ConnectService(AbstractService):
159
162
  public_key=public_key_contents
160
163
  )
161
164
 
162
- ssh_key_pair_id = is_user_already_has_key_response.sshKeyPairId
165
+ ssh_key_pair_public_id = is_user_already_has_key_response.sshKeyPairPublicId
163
166
  is_adding_key_to_user = not is_user_already_has_key_response.isUserHasPublicKey
164
167
 
165
168
  if is_adding_key_to_user and not note_to_send:
@@ -179,12 +182,12 @@ class ConnectService(AbstractService):
179
182
  note=note_to_send
180
183
  )
181
184
  typer.echo(f"Public key '{note_to_send}' added to your profile")
182
- ssh_key_pair_id = add_ssh_key_to_user_response.sshKeyPairId
185
+ ssh_key_pair_public_id = add_ssh_key_to_user_response.sshKeyPairPublicId
183
186
 
184
187
  if instance_rented:
185
188
  self.__thestage_api_client.add_public_ssh_key_to_instance_rented(
186
- instance_rented_id=instance_rented.id,
187
- ssh_key_pair_id=ssh_key_pair_id
189
+ instance_rented_public_id=instance_rented.public_id,
190
+ ssh_key_pair_public_id=ssh_key_pair_public_id
188
191
  )
189
192
 
190
193
  if instance_rented.frontend_status.status_key != InstanceRentedBusinessStatus.ONLINE:
@@ -1,7 +1,11 @@
1
+ import re
1
2
  from pathlib import Path
2
3
  from typing import List, Tuple, Optional, Dict
4
+ from rich import print
3
5
 
4
6
  import typer
7
+
8
+ from thestage.color_scheme.color_scheme import ColorScheme
5
9
  from thestage.entities.container import DockerContainerEntity
6
10
  from thestage.services.clients.thestage_api.dtos.container_param_request import DockerContainerActionRequestDto
7
11
  from thestage.services.clients.thestage_api.dtos.enums.container_pending_action import DockerContainerAction
@@ -42,7 +46,8 @@ class ContainerService(AbstractService):
42
46
  self,
43
47
  row: int,
44
48
  page: int,
45
- project_uid: Optional[str],
49
+ project_public_id: Optional[str],
50
+ project_slug: Optional[str],
46
51
  statuses: List[str],
47
52
  ):
48
53
  container_status_map = self.__thestage_api_client.get_container_business_status_map()
@@ -72,22 +77,18 @@ class ContainerService(AbstractService):
72
77
 
73
78
  backend_statuses: List[str] = [key for key, value in container_status_map.items() if value in statuses]
74
79
 
75
- project_id: Optional[int] = None
76
- if project_uid:
77
- project = self.__thestage_api_client.get_project_by_slug(slug=project_uid)
78
- project_id = project.id
79
-
80
80
  self.print(
81
81
  func_get_data=self.get_list,
82
82
  func_special_params={
83
83
  'statuses': backend_statuses,
84
- 'project_id': project_id,
84
+ 'project_slug': project_slug,
85
+ 'project_public_id': project_public_id,
85
86
  },
86
87
  mapper=ContainerMapper(),
87
88
  headers=list(map(lambda x: x.alias, DockerContainerEntity.model_fields.values())),
88
89
  row=row,
89
90
  page=page,
90
- max_col_width=[15, 20, 25],
91
+ max_col_width=[35, 20, 25],
91
92
  show_index="never",
92
93
  )
93
94
 
@@ -98,26 +99,29 @@ class ContainerService(AbstractService):
98
99
  statuses: List[str],
99
100
  row: int = 5,
100
101
  page: int = 1,
101
- project_id: Optional[int] = None,
102
+ project_public_id: Optional[str] = None,
103
+ project_slug: Optional[str] = None,
102
104
  ) -> PaginatedEntityList[DockerContainerDto]:
103
105
 
104
106
  list = self.__thestage_api_client.get_container_list(
105
107
  statuses=statuses,
106
108
  page=page,
107
109
  limit=row,
108
- project_id=project_id,
110
+ project_public_id=project_public_id,
111
+ project_slug=project_slug
109
112
  )
110
113
 
111
114
  return list
112
115
 
116
+ # TODO delete this proxy method
113
117
  @error_handler()
114
118
  def get_container(
115
119
  self,
116
- container_id: Optional[int] = None,
120
+ container_public_id: Optional[str] = None,
117
121
  container_slug: Optional[str] = None,
118
122
  ) -> Optional[DockerContainerDto]:
119
123
  return self.__thestage_api_client.get_container(
120
- container_id=container_id,
124
+ container_public_id=container_public_id,
121
125
  container_slug=container_slug,
122
126
  )
123
127
 
@@ -150,7 +154,7 @@ class ContainerService(AbstractService):
150
154
  if private_key_path:
151
155
  typer.echo(f'Using configured private key for {ip_address}: {private_key_path}')
152
156
  else:
153
- typer.echo(f'Using SSH agent to connect to {ip_address}')
157
+ typer.echo(f'Using SSH agent to connect to {ip_address} as {username}')
154
158
  else:
155
159
  self.__config_provider.update_remote_server_config_entry(ip_address, Path(private_key_path))
156
160
  typer.echo(f'Updated private key path for {ip_address}: {private_key_path}')
@@ -160,16 +164,18 @@ class ContainerService(AbstractService):
160
164
  @error_handler()
161
165
  def connect_to_container(
162
166
  self,
163
- container_uid: str,
167
+ container_public_id: Optional[str],
168
+ container_slug: Optional[str],
164
169
  username: Optional[str],
165
170
  input_ssh_key_path: Optional[str],
166
171
  ):
167
172
  container: Optional[DockerContainerDto] = self.get_container(
168
- container_slug=container_uid,
173
+ container_public_id=container_public_id,
174
+ container_slug=container_slug,
169
175
  )
170
176
 
171
177
  if not container:
172
- typer.echo(f"Container with UID '{container_uid}' not found")
178
+ typer.echo(f"Container not found")
173
179
  raise typer.Exit(1)
174
180
 
175
181
  self.check_if_container_running(
@@ -265,22 +271,49 @@ class ContainerService(AbstractService):
265
271
  @error_handler()
266
272
  def put_file_to_container(
267
273
  self,
268
- container: DockerContainerDto,
269
- src_path: str,
270
- copy_only_folder_contents: bool,
271
- destination_path: Optional[str] = None,
272
- username_param: Optional[str] = None,
274
+ source_path: str,
275
+ destination: str,
276
+ username_param: Optional[str],
273
277
  ):
274
- if not self.__file_system_service.check_if_path_exist(file=src_path):
275
- typer.echo(__("File not found at specified path"))
278
+ container_args = re.match(r"^([\w\W]+?):([\w\W]+)$", destination)
279
+ if container_args is None:
280
+ typer.echo(__('Container name and source file path are required as the second argument'))
281
+ typer.echo(__('Example: container_name:/path/to/file'))
276
282
  raise typer.Exit(1)
283
+ container_identifier = container_args.groups()[0]
284
+ destination_path = container_args.groups()[1].rstrip("/")
277
285
 
278
- username, ip_address, private_key_path = self.get_server_auth(
279
- container=container,
280
- username_param=username_param,
281
- private_key_path_override=None
286
+ if not container_identifier:
287
+ typer.echo('Container identifier (container_id_or_name) is required')
288
+ raise typer.Exit(1)
289
+
290
+ resolved_options = self.__thestage_api_client.resolve_user_input(entity_identifier=container_identifier)
291
+ container_public_id = None
292
+ valid_container_count = 0
293
+ for container_item in resolved_options.dockerContainerMatchData:
294
+ message = f"Found a container with matching {container_item.matchedField} in status: '{container_item.frontendStatus.status_translation}' (ID: {container_item.publicId})"
295
+ line_color = ColorScheme.SUCCESS.value if container_item.canDownloadUploadOnContainer else 'default'
296
+ print(f"[{line_color}]{message}[{line_color}]")
297
+ if container_item.canDownloadUploadOnContainer:
298
+ valid_container_count += 1
299
+ container_public_id = container_item.publicId
300
+
301
+ if valid_container_count != 1:
302
+ typer.echo(f"Failed to resolve the container by provided identifier, as total of {valid_container_count} containers are valid options")
303
+ raise typer.Exit(1)
304
+
305
+ container: Optional[DockerContainerDto] = self.__thestage_api_client.get_container(
306
+ container_public_id=container_public_id,
282
307
  )
283
308
 
309
+ if not container:
310
+ typer.echo(f"Unexpected error: container '{container_public_id}' not found")
311
+ raise typer.Exit(1)
312
+
313
+ if not self.__file_system_service.check_if_path_exist(file=source_path):
314
+ typer.echo(__("File not found at specified path"))
315
+ raise typer.Exit(1)
316
+
284
317
  if not container.mappings or not container.mappings.directory_mappings:
285
318
  typer.echo(__("Mapping folders not found"))
286
319
  raise typer.Exit(1)
@@ -294,10 +327,18 @@ class ContainerService(AbstractService):
294
327
  typer.echo(__("Cannot find matching container volume mapping for specified file path"))
295
328
  raise typer.Exit(1)
296
329
 
330
+ username, ip_address, private_key_path = self.get_server_auth(
331
+ container=container,
332
+ username_param=username_param,
333
+ private_key_path_override=None
334
+ )
335
+
336
+ copy_only_folder_contents = source_path.endswith("/")
337
+
297
338
  self.__remote_server_service.upload_data_to_container(
298
339
  ip_address=ip_address,
299
340
  username=username,
300
- src_path=src_path,
341
+ src_path=source_path,
301
342
  dest_path=destination_path,
302
343
  instance_path=instance_path,
303
344
  container_path=container_path,
@@ -308,31 +349,67 @@ class ContainerService(AbstractService):
308
349
  @error_handler()
309
350
  def get_file_from_container(
310
351
  self,
311
- container: DockerContainerDto,
312
- src_path: str,
313
- copy_only_folder_contents: bool,
314
- destination_path: Optional[str] = None,
352
+ source: str,
353
+ destination_path: str,
315
354
  username_param: Optional[str] = None,
316
355
  ):
317
- username, ip_address, private_key_path = self.get_server_auth(
318
- container=container,
319
- username_param=username_param,
320
- private_key_path_override=None,
356
+ container_args = re.match(r"^([\w\W]+?):([\w\W]+)$", source)
357
+
358
+ if container_args is None:
359
+ typer.echo(__('Container name and source directory path are required as the first argument'))
360
+ typer.echo(__('Example: container_name:/path/to/file'))
361
+ raise typer.Exit(1)
362
+ container_identifier = container_args.groups()[0]
363
+ source_path = container_args.groups()[1]
364
+
365
+ if not container_identifier:
366
+ typer.echo('Container identifier (container_id_or_name) is required')
367
+ raise typer.Exit(1)
368
+
369
+ resolved_options = self.__thestage_api_client.resolve_user_input(entity_identifier=container_identifier)
370
+ container_public_id = None
371
+ valid_container_count = 0
372
+ for container_item in resolved_options.dockerContainerMatchData:
373
+ message = f"Found a container with matching {container_item.matchedField} in status: '{container_item.frontendStatus.status_translation}' (ID: {container_item.publicId})"
374
+ line_color = ColorScheme.SUCCESS.value if container_item.canDownloadUploadOnContainer else 'default'
375
+ print(f"[{line_color}]{message}[{line_color}]")
376
+ if container_item.canDownloadUploadOnContainer:
377
+ valid_container_count += 1
378
+ container_public_id = container_item.publicId
379
+
380
+ if valid_container_count != 1:
381
+ typer.echo(f"Failed to resolve the container by provided identifier, as total of {valid_container_count} containers are valid options")
382
+ raise typer.Exit(1)
383
+
384
+ container: Optional[DockerContainerDto] = self.__thestage_api_client.get_container(
385
+ container_public_id=container_public_id,
321
386
  )
322
387
 
388
+ if not container:
389
+ typer.echo(f"Unexpected error: container '{container_public_id}' not found")
390
+ raise typer.Exit(1)
391
+
323
392
  if not container.mappings or not container.mappings.directory_mappings:
324
393
  typer.echo(__("Mapping folders not found"))
325
394
  raise typer.Exit(1)
326
395
 
327
396
  instance_path, container_path = self._get_new_path_from_mapping(
328
397
  directory_mapping=container.mappings.directory_mappings,
329
- destination_path=src_path,
398
+ destination_path=source_path,
330
399
  )
331
400
 
332
401
  if not instance_path and not container_path:
333
402
  typer.echo(__("Cannot find matching container volume mapping for specified file path"))
334
403
  raise typer.Exit(1)
335
404
 
405
+ username, ip_address, private_key_path = self.get_server_auth(
406
+ container=container,
407
+ username_param=username_param,
408
+ private_key_path_override=None,
409
+ )
410
+
411
+ copy_only_folder_contents=source_path.endswith("/")
412
+
336
413
  self.__remote_server_service.download_data_from_container(
337
414
  ip_address=ip_address,
338
415
  username=username,
@@ -346,14 +423,16 @@ class ContainerService(AbstractService):
346
423
  @error_handler()
347
424
  def request_docker_container_action(
348
425
  self,
349
- container_uid: str,
426
+ container_public_id: Optional[str],
427
+ container_slug: Optional[str],
350
428
  action: DockerContainerAction,
351
429
  ):
352
430
  container: Optional[DockerContainerDto] = self.get_container(
353
- container_slug=container_uid,
431
+ container_public_id=container_public_id,
432
+ container_slug=container_slug,
354
433
  )
355
434
  if not container:
356
- typer.echo(f"Container with unique ID '{container_uid}' not found")
435
+ typer.echo(f"Container not found")
357
436
  raise typer.Exit(1)
358
437
 
359
438
  if action == DockerContainerAction.START:
@@ -363,7 +442,7 @@ class ContainerService(AbstractService):
363
442
  self.check_if_container_running(container=container)
364
443
 
365
444
  request_params = DockerContainerActionRequestDto(
366
- dockerContainerId=container.id,
445
+ dockerContainerPublicId=container.public_id,
367
446
  action=action,
368
447
  )
369
448
  result = self.__thestage_api_client.container_action(
@@ -22,8 +22,9 @@ class ContainerMapper(AbstractMapper):
22
22
 
23
23
  return DockerContainerEntity(
24
24
  status=item.frontend_status.status_translation if item.frontend_status else '',
25
+ public_id=item.public_id or '',
25
26
  slug=item.slug or '',
26
- title=item.title or '',
27
+ project_slug=item.project.slug if item.project else '',
27
28
  instance_type=instance_type,
28
29
  instance_slug=instance_slug,
29
30
  docker_image=item.docker_image or '',
@@ -35,21 +35,6 @@ class InstanceService(AbstractService):
35
35
  self.__remote_server_service = remote_server_service
36
36
  self.__config_provider = config_provider
37
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
38
 
54
39
  @error_handler()
55
40
  def check_instance_status_to_connect(
@@ -116,10 +101,14 @@ class InstanceService(AbstractService):
116
101
  @error_handler()
117
102
  def connect_to_rented_instance(
118
103
  self,
119
- instance_rented_slug: str,
104
+ instance_rented_public_id: Optional[str],
105
+ instance_rented_slug: Optional[str],
120
106
  input_ssh_key_path: Optional[str]
121
107
  ):
122
- instance = self.get_rented_instance(instance_slug=instance_rented_slug)
108
+ instance = self.__thestage_api_client.get_rented_instance(
109
+ instance_public_id=instance_rented_public_id,
110
+ instance_slug=instance_rented_slug,
111
+ )
123
112
 
124
113
  if instance:
125
114
  self.check_instance_status_to_connect(
@@ -151,7 +140,8 @@ class InstanceService(AbstractService):
151
140
  @error_handler()
152
141
  def connect_to_selfhosted_instance(
153
142
  self,
154
- selfhosted_instance_slug: str,
143
+ selfhosted_instance_public_id: Optional[str],
144
+ selfhosted_instance_slug: Optional[str],
155
145
  username: str,
156
146
  input_ssh_key_path: Optional[str],
157
147
  ):
@@ -159,7 +149,10 @@ class InstanceService(AbstractService):
159
149
  username = 'root'
160
150
  typer.echo(__("No remote server username provided, using 'root' as username"))
161
151
 
162
- instance = self.get_self_hosted_instance(instance_slug=selfhosted_instance_slug)
152
+ instance = self.__thestage_api_client.get_selfhosted_instance(
153
+ instance_public_id=selfhosted_instance_public_id,
154
+ instance_slug=selfhosted_instance_slug,
155
+ )
163
156
 
164
157
  if instance:
165
158
  self.check_selfhosted_status_to_connect(
@@ -184,7 +177,7 @@ class InstanceService(AbstractService):
184
177
  if input_ssh_key_path:
185
178
  self.__config_provider.update_remote_server_config_entry(ip_address=instance.ip_address, ssh_key_path=Path(input_ssh_key_path))
186
179
  else:
187
- typer.echo(__("Server instance not found: %instance_item%", {'instance_item': selfhosted_instance_slug}))
180
+ typer.echo("Self-hosted instance not found")
188
181
 
189
182
 
190
183
  @error_handler()
@@ -13,12 +13,10 @@ class InstanceMapper(AbstractMapper):
13
13
 
14
14
  return RentedInstanceEntity(
15
15
  slug=item.slug if item.slug else '',
16
- title=item.title if item.title else '',
16
+ public_id=item.public_id if item.public_id else '',
17
17
  cpu_type=item.cpu_type if item.cpu_type else '',
18
18
  gpu_type=item.gpu_type if item.gpu_type else '',
19
19
  cpu_cores=str(item.cpu_cores) if item.cpu_cores else '',
20
20
  ip_address=item.ip_address if item.ip_address else '',
21
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
22
  )
@@ -3,6 +3,7 @@ from typing import Optional, Any, Tuple
3
3
 
4
4
  from thestage.entities.rented_instance import RentedInstanceEntity
5
5
  from thestage.entities.self_hosted_instance import SelfHostedInstanceEntity
6
+ from thestage.services.clients.thestage_api.dtos.enums.gpu_name import InstanceGpuType
6
7
  from thestage.services.clients.thestage_api.dtos.selfhosted_instance_response import SelfHostedInstanceDto
7
8
  from thestage.services.abstract_mapper import AbstractMapper
8
9
 
@@ -18,16 +19,14 @@ class SelfHostedMapper(AbstractMapper):
18
19
  gpus = [item.type.value for item in item.detected_gpus.gpus]
19
20
 
20
21
  if len(gpus) == 0:
21
- gpus = ['NO_GPU']
22
+ gpus = [InstanceGpuType.NO_GPU]
22
23
 
23
24
  return SelfHostedInstanceEntity(
24
25
  slug=item.slug,
25
- title=item.title,
26
+ public_id=item.public_id,
26
27
  cpu_type=item.cpu_type,
27
28
  cpu_cores=item.cpu_cores,
28
29
  gpu_type=', '.join(gpus),
29
30
  ip_address=item.ip_address,
30
31
  status=item.frontend_status.status_translation if item.frontend_status else None,
31
- created_at=item.created_at.strftime("%Y-%m-%d %H:%M:%S") if item.created_at else '',
32
- updated_at=item.updated_at.strftime("%Y-%m-%d %H:%M:%S") if item.updated_at else '',
33
32
  )