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,237 +1,237 @@
1
- import typer
2
-
3
- import json
4
- import os
5
- from pathlib import Path
6
- from typing import Optional, Dict, Any
7
-
8
- from thestage.cli_command import CliCommand, CliCommandAvailability, ALWAYS_AVAILABLE_COMMANDS
9
- from thestage.services.clients.thestage_api.dtos.validate_token_response import ValidateTokenResponse
10
- from thestage.services.core_files.config_entity import ConfigEntity
11
- from thestage.helpers.ssh_util import parse_private_key
12
- from thestage.services.connect.dto.remote_server_config import RemoteServerConfig
13
- from thestage.services.filesystem_service import FileSystemService
14
- from thestage.services.project.dto.project_config import ProjectConfig
15
- from thestage.config import THESTAGE_CONFIG_DIR, THESTAGE_CONFIG_FILE, THESTAGE_AUTH_TOKEN, THESTAGE_API_URL, \
16
- config_storage
17
-
18
-
19
- class ConfigProvider:
20
- _file_system_service = FileSystemService
21
-
22
-
23
- def __init__(
24
- self,
25
- file_system_service: FileSystemService,
26
- ):
27
- self._file_system_service = file_system_service
28
-
29
- global_config_path = self.get_global_config_path()
30
- if not global_config_path.exists():
31
- self._file_system_service.create_if_not_exists_dir(global_config_path)
32
-
33
- global_config_file = self.get_global_config_file()
34
- self._file_system_service.create_if_not_exists_file(global_config_file)
35
-
36
-
37
- # must be called right after the app is started
38
- def build_config(self) -> ConfigEntity:
39
- config_values = {}
40
-
41
- config_from_env = {}
42
- config_from_env['main'] = {}
43
- if THESTAGE_AUTH_TOKEN:
44
- config_from_env['main']['thestage_auth_token'] = THESTAGE_AUTH_TOKEN
45
- if THESTAGE_API_URL:
46
- config_from_env['main']['thestage_api_url'] = THESTAGE_API_URL
47
-
48
- if config_from_env:
49
- self.__update_config_values_dict(values_to_update=config_values, new_values=config_from_env)
50
-
51
- config_from_file = self._file_system_service.read_config_file(self.get_global_config_file())
52
- if config_from_file:
53
- self.__update_config_values_dict(values_to_update=config_values, new_values=config_from_file)
54
-
55
- config = ConfigEntity.model_validate(config_values)
56
- config.runtime.config_global_path = str(self.get_global_config_path())
57
-
58
- config_storage.APP_CONFIG = config
59
-
60
- return config
61
-
62
-
63
- def get_config(self) -> ConfigEntity:
64
- if not config_storage.APP_CONFIG:
65
- raise Exception('Config storage was not initialized')
66
- return config_storage.APP_CONFIG
67
-
68
-
69
- def save_project_config(self, project_config: ProjectConfig):
70
- project_data_dirpath = self.__get_project_config_path(with_file=False)
71
- self._file_system_service.create_if_not_exists_dir(project_data_dirpath)
72
-
73
- project_data_filepath = self.__get_project_config_path(with_file=True)
74
- self._file_system_service.create_if_not_exists_file(project_data_filepath)
75
-
76
- self.__save_config_file(data=project_config.model_dump(), file_path=project_data_filepath)
77
-
78
-
79
- def save_project_deploy_ssh_key(self, deploy_ssh_key: str, project_slug: str, project_id: int) -> str:
80
- deploy_key_dirpath = self.get_global_config_path().joinpath('project_deploy_keys')
81
- self._file_system_service.create_if_not_exists_dir(deploy_key_dirpath)
82
-
83
- deploy_key_filepath = deploy_key_dirpath.joinpath(f'project_deploy_key_{project_id}_{project_slug}')
84
- self._file_system_service.create_if_not_exists_file(deploy_key_filepath)
85
-
86
- text_file = open(deploy_key_filepath, "w")
87
- text_file.write(deploy_ssh_key)
88
- text_file.close()
89
- os.chmod(deploy_key_filepath, 0o600)
90
-
91
- return str(deploy_key_filepath)
92
-
93
-
94
- def read_project_config(self) -> Optional[ProjectConfig]:
95
- project_data_dirpath = self.__get_project_config_path()
96
- if not project_data_dirpath.exists():
97
- return None
98
-
99
- project_data_filepath = self.__get_project_config_path(with_file=True)
100
- if not project_data_filepath.exists():
101
- return None
102
-
103
- config_data = self._file_system_service.read_config_file(project_data_filepath) if project_data_filepath and project_data_filepath.exists() else {}
104
- return ProjectConfig.model_validate(config_data)
105
-
106
-
107
- def __get_project_config_path(self, with_file: bool = False) -> Path:
108
- if with_file:
109
- return Path(self.get_config().runtime.working_directory).joinpath(THESTAGE_CONFIG_DIR).joinpath('project.json')
110
- else:
111
- return Path(self.get_config().runtime.working_directory).joinpath(THESTAGE_CONFIG_DIR)
112
-
113
-
114
- def __get_remote_server_config_path(self) -> Path:
115
- return self.get_global_config_path().joinpath('remote_server_config.json')
116
-
117
-
118
- def update_remote_server_config_entry(self, ip_address: str, ssh_key_path: Optional[Path]):
119
- # double checking if key is parseable (fails if key is encrypted)
120
- # see remote_server_service for similar check
121
- if ssh_key_path:
122
- pkey = parse_private_key(str(ssh_key_path))
123
-
124
- if pkey is None:
125
- typer.echo("Could not identify provided private key (expected RSA, ECDSA, ED25519)")
126
- raise typer.Exit(1)
127
-
128
- config = self.__read_remote_server_config()
129
- remote_server_config_filepath = self.__get_remote_server_config_path()
130
- if config:
131
- if not config.ip_address_to_ssh_key_map:
132
- config.ip_address_to_ssh_key_map = {}
133
-
134
- existing_path = config.ip_address_to_ssh_key_map.get(ip_address)
135
- if ssh_key_path and existing_path != str(ssh_key_path):
136
- config.ip_address_to_ssh_key_map.update({ip_address: str(ssh_key_path)})
137
- typer.echo(f'Updated ssh key for {ip_address}: {ssh_key_path}')
138
-
139
- if not ssh_key_path and existing_path:
140
- typer.echo(f"Private key at path {existing_path} was not found")
141
- config.ip_address_to_ssh_key_map.pop(ip_address, None)
142
- else:
143
- self._file_system_service.create_if_not_exists_file(remote_server_config_filepath)
144
- config = RemoteServerConfig(ip_address_to_ssh_key_map={ip_address: str(ssh_key_path)})
145
-
146
- self.__save_config_file(data=config.model_dump(), file_path=remote_server_config_filepath)
147
-
148
- return str(remote_server_config_filepath)
149
-
150
-
151
- def __read_remote_server_config(self) -> Optional[RemoteServerConfig]:
152
- config_filepath = self.__get_remote_server_config_path()
153
- if not config_filepath.is_file():
154
- return None
155
-
156
- config_data = self._file_system_service.read_config_file(config_filepath) if config_filepath and config_filepath.exists() else {}
157
- return RemoteServerConfig.model_validate(config_data)
158
-
159
-
160
- def get_valid_private_key_path_by_ip_address(self, ip_address: str) -> Optional[str]:
161
- remote_server_config = self.__read_remote_server_config()
162
- if remote_server_config and remote_server_config.ip_address_to_ssh_key_map:
163
- private_key_path = remote_server_config.ip_address_to_ssh_key_map.get(ip_address)
164
- if private_key_path and Path(private_key_path).is_file():
165
- return private_key_path
166
- elif private_key_path:
167
- self.update_remote_server_config_entry(ip_address, None)
168
- return None
169
-
170
-
171
- @staticmethod
172
- def __update_config_values_dict(values_to_update: Dict, new_values: Dict):
173
- if 'main' in new_values:
174
- if 'main' in values_to_update:
175
- values_to_update['main'].update(new_values['main'])
176
- else:
177
- values_to_update['main'] = new_values['main']
178
-
179
- @staticmethod
180
- def __save_config_file(data: Dict, file_path: Path):
181
- with open(file_path, 'w') as configfile:
182
- json.dump(data, configfile, indent=1)
183
-
184
-
185
- def save_config(self):
186
- config = self.get_config()
187
- global_config_file = self.get_global_config_file()
188
- data: Dict[str, Any] = self._file_system_service.read_config_file(global_config_file)
189
- data.update(config.model_dump(exclude_none=True, by_alias=True, exclude={'runtime', 'RUNTIME', 'daemon', 'DAEMON'}))
190
- self.__save_config_file(data=data, file_path=global_config_file)
191
-
192
-
193
- def clear_config(self, ):
194
- global_config_path = self.get_global_config_path()
195
- if global_config_path.exists():
196
- self._file_system_service.remove_folder(str(global_config_path))
197
-
198
- os.unsetenv('THESTAGE_CONFIG_DIR')
199
- os.unsetenv('THESTAGE_CONFIG_FILE')
200
- os.unsetenv('THESTAGE_CLI_ENV')
201
- os.unsetenv('THESTAGE_API_URL')
202
- os.unsetenv('THESTAGE_LOG_FILE')
203
- os.unsetenv('THESTAGE_AUTH_TOKEN')
204
-
205
-
206
- def update_config(self, updated_config: ConfigEntity):
207
- config_storage.APP_CONFIG = updated_config
208
-
209
- def get_global_config_path(self) -> Path:
210
- home_dir = self._file_system_service.get_home_path()
211
- return home_dir.joinpath(THESTAGE_CONFIG_DIR)
212
-
213
- def get_global_config_file(self) -> Path:
214
- return self.get_global_config_path().joinpath(THESTAGE_CONFIG_FILE)
215
-
216
-
217
- def update_allowed_commands_and_is_token_valid(self, validate_token_response: ValidateTokenResponse):
218
- config = self.get_config()
219
- for cli_command_item in CliCommand:
220
- is_command_available = validate_token_response.allowedCliCommands.get(cli_command_item)
221
- command_availability = CliCommandAvailability.DEPRECATED
222
-
223
- if cli_command_item in ALWAYS_AVAILABLE_COMMANDS:
224
- command_availability = CliCommandAvailability.ALLOWED
225
- else:
226
- if is_command_available is True:
227
- command_availability = CliCommandAvailability.ALLOWED
228
- if is_command_available is False:
229
- command_availability = CliCommandAvailability.RESTRICTED
230
- config.runtime.allowed_commands.update({cli_command_item: command_availability})
231
- config.runtime.is_token_valid = validate_token_response.is_success
232
- self.update_config(config)
233
-
234
-
235
- def is_cli_command_allowed(self, cli_command: CliCommand) -> bool:
236
- config = self.get_config()
237
- return True if config.runtime.allowed_commands.get(cli_command) == CliCommandAvailability.ALLOWED else False
1
+ import typer
2
+
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+ from typing import Optional, Dict, Any
7
+
8
+ from thestage.cli_command import CliCommand, CliCommandAvailability, ALWAYS_AVAILABLE_COMMANDS
9
+ from thestage.services.clients.thestage_api.dtos.validate_token_response import ValidateTokenResponse
10
+ from thestage.services.core_files.config_entity import ConfigEntity
11
+ from thestage.helpers.ssh_util import parse_private_key
12
+ from thestage.services.connect.dto.remote_server_config import RemoteServerConfig
13
+ from thestage.services.filesystem_service import FileSystemService
14
+ from thestage.services.project.dto.project_config import ProjectConfig
15
+ from thestage.config import THESTAGE_CONFIG_DIR, THESTAGE_CONFIG_FILE, THESTAGE_AUTH_TOKEN, THESTAGE_API_URL, \
16
+ config_storage
17
+
18
+
19
+ class ConfigProvider:
20
+ _file_system_service = FileSystemService
21
+
22
+
23
+ def __init__(
24
+ self,
25
+ file_system_service: FileSystemService,
26
+ ):
27
+ self._file_system_service = file_system_service
28
+
29
+ global_config_path = self.get_global_config_path()
30
+ if not global_config_path.exists():
31
+ self._file_system_service.create_if_not_exists_dir(global_config_path)
32
+
33
+ global_config_file = self.get_global_config_file()
34
+ self._file_system_service.create_if_not_exists_file(global_config_file)
35
+
36
+
37
+ # must be called right after the app is started
38
+ def build_config(self) -> ConfigEntity:
39
+ config_values = {}
40
+
41
+ config_from_env = {}
42
+ config_from_env['main'] = {}
43
+ if THESTAGE_AUTH_TOKEN:
44
+ config_from_env['main']['thestage_auth_token'] = THESTAGE_AUTH_TOKEN
45
+ if THESTAGE_API_URL:
46
+ config_from_env['main']['thestage_api_url'] = THESTAGE_API_URL
47
+
48
+ if config_from_env:
49
+ self.__update_config_values_dict(values_to_update=config_values, new_values=config_from_env)
50
+
51
+ config_from_file = self._file_system_service.read_config_file(self.get_global_config_file())
52
+ if config_from_file:
53
+ self.__update_config_values_dict(values_to_update=config_values, new_values=config_from_file)
54
+
55
+ config = ConfigEntity.model_validate(config_values)
56
+ config.runtime.config_global_path = str(self.get_global_config_path())
57
+
58
+ config_storage.APP_CONFIG = config
59
+
60
+ return config
61
+
62
+
63
+ def get_config(self) -> ConfigEntity:
64
+ if not config_storage.APP_CONFIG:
65
+ raise Exception('Config storage was not initialized')
66
+ return config_storage.APP_CONFIG
67
+
68
+
69
+ def save_project_config(self, project_config: ProjectConfig):
70
+ project_data_dirpath = self.__get_project_config_path(with_file=False)
71
+ self._file_system_service.create_if_not_exists_dir(project_data_dirpath)
72
+
73
+ project_data_filepath = self.__get_project_config_path(with_file=True)
74
+ self._file_system_service.create_if_not_exists_file(project_data_filepath)
75
+
76
+ self.__save_config_file(data=project_config.model_dump(), file_path=project_data_filepath)
77
+
78
+
79
+ def save_project_deploy_ssh_key(self, deploy_ssh_key: str, project_slug: str, project_id: int) -> str:
80
+ deploy_key_dirpath = self.get_global_config_path().joinpath('project_deploy_keys')
81
+ self._file_system_service.create_if_not_exists_dir(deploy_key_dirpath)
82
+
83
+ deploy_key_filepath = deploy_key_dirpath.joinpath(f'project_deploy_key_{project_id}_{project_slug}')
84
+ self._file_system_service.create_if_not_exists_file(deploy_key_filepath)
85
+
86
+ text_file = open(deploy_key_filepath, "w")
87
+ text_file.write(deploy_ssh_key)
88
+ text_file.close()
89
+ os.chmod(deploy_key_filepath, 0o600)
90
+
91
+ return str(deploy_key_filepath)
92
+
93
+
94
+ def read_project_config(self) -> Optional[ProjectConfig]:
95
+ project_data_dirpath = self.__get_project_config_path()
96
+ if not project_data_dirpath.exists():
97
+ return None
98
+
99
+ project_data_filepath = self.__get_project_config_path(with_file=True)
100
+ if not project_data_filepath.exists():
101
+ return None
102
+
103
+ config_data = self._file_system_service.read_config_file(project_data_filepath) if project_data_filepath and project_data_filepath.exists() else {}
104
+ return ProjectConfig.model_validate(config_data)
105
+
106
+
107
+ def __get_project_config_path(self, with_file: bool = False) -> Path:
108
+ if with_file:
109
+ return Path(self.get_config().runtime.working_directory).joinpath(THESTAGE_CONFIG_DIR).joinpath('project.json')
110
+ else:
111
+ return Path(self.get_config().runtime.working_directory).joinpath(THESTAGE_CONFIG_DIR)
112
+
113
+
114
+ def __get_remote_server_config_path(self) -> Path:
115
+ return self.get_global_config_path().joinpath('remote_server_config.json')
116
+
117
+
118
+ def update_remote_server_config_entry(self, ip_address: str, ssh_key_path: Optional[Path]):
119
+ # double checking if key is parseable (fails if key is encrypted)
120
+ # see remote_server_service for similar check
121
+ if ssh_key_path:
122
+ pkey = parse_private_key(str(ssh_key_path))
123
+
124
+ if pkey is None:
125
+ typer.echo("Could not identify provided private key (expected RSA, ECDSA, ED25519)")
126
+ raise typer.Exit(1)
127
+
128
+ config = self.__read_remote_server_config()
129
+ remote_server_config_filepath = self.__get_remote_server_config_path()
130
+ if config:
131
+ if not config.ip_address_to_ssh_key_map:
132
+ config.ip_address_to_ssh_key_map = {}
133
+
134
+ existing_path = config.ip_address_to_ssh_key_map.get(ip_address)
135
+ if ssh_key_path and existing_path != str(ssh_key_path):
136
+ config.ip_address_to_ssh_key_map.update({ip_address: str(ssh_key_path)})
137
+ typer.echo(f'Updated ssh key for {ip_address}: {ssh_key_path}')
138
+
139
+ if not ssh_key_path and existing_path:
140
+ typer.echo(f"Private key at path {existing_path} was not found")
141
+ config.ip_address_to_ssh_key_map.pop(ip_address, None)
142
+ else:
143
+ self._file_system_service.create_if_not_exists_file(remote_server_config_filepath)
144
+ config = RemoteServerConfig(ip_address_to_ssh_key_map={ip_address: str(ssh_key_path)})
145
+
146
+ self.__save_config_file(data=config.model_dump(), file_path=remote_server_config_filepath)
147
+
148
+ return str(remote_server_config_filepath)
149
+
150
+
151
+ def __read_remote_server_config(self) -> Optional[RemoteServerConfig]:
152
+ config_filepath = self.__get_remote_server_config_path()
153
+ if not config_filepath.is_file():
154
+ return None
155
+
156
+ config_data = self._file_system_service.read_config_file(config_filepath) if config_filepath and config_filepath.exists() else {}
157
+ return RemoteServerConfig.model_validate(config_data)
158
+
159
+
160
+ def get_valid_private_key_path_by_ip_address(self, ip_address: str) -> Optional[str]:
161
+ remote_server_config = self.__read_remote_server_config()
162
+ if remote_server_config and remote_server_config.ip_address_to_ssh_key_map:
163
+ private_key_path = remote_server_config.ip_address_to_ssh_key_map.get(ip_address)
164
+ if private_key_path and Path(private_key_path).is_file():
165
+ return private_key_path
166
+ elif private_key_path:
167
+ self.update_remote_server_config_entry(ip_address, None)
168
+ return None
169
+
170
+
171
+ @staticmethod
172
+ def __update_config_values_dict(values_to_update: Dict, new_values: Dict):
173
+ if 'main' in new_values:
174
+ if 'main' in values_to_update:
175
+ values_to_update['main'].update(new_values['main'])
176
+ else:
177
+ values_to_update['main'] = new_values['main']
178
+
179
+ @staticmethod
180
+ def __save_config_file(data: Dict, file_path: Path):
181
+ with open(file_path, 'w') as configfile:
182
+ json.dump(data, configfile, indent=1)
183
+
184
+
185
+ def save_config(self):
186
+ config = self.get_config()
187
+ global_config_file = self.get_global_config_file()
188
+ data: Dict[str, Any] = self._file_system_service.read_config_file(global_config_file)
189
+ data.update(config.model_dump(exclude_none=True, by_alias=True, exclude={'runtime', 'RUNTIME', 'daemon', 'DAEMON'}))
190
+ self.__save_config_file(data=data, file_path=global_config_file)
191
+
192
+
193
+ def clear_config(self, ):
194
+ global_config_path = self.get_global_config_path()
195
+ if global_config_path.exists():
196
+ self._file_system_service.remove_folder(str(global_config_path))
197
+
198
+ os.unsetenv('THESTAGE_CONFIG_DIR')
199
+ os.unsetenv('THESTAGE_CONFIG_FILE')
200
+ os.unsetenv('THESTAGE_CLI_ENV')
201
+ os.unsetenv('THESTAGE_API_URL')
202
+ os.unsetenv('THESTAGE_LOG_FILE')
203
+ os.unsetenv('THESTAGE_AUTH_TOKEN')
204
+
205
+
206
+ def update_config(self, updated_config: ConfigEntity):
207
+ config_storage.APP_CONFIG = updated_config
208
+
209
+ def get_global_config_path(self) -> Path:
210
+ home_dir = self._file_system_service.get_home_path()
211
+ return home_dir.joinpath(THESTAGE_CONFIG_DIR)
212
+
213
+ def get_global_config_file(self) -> Path:
214
+ return self.get_global_config_path().joinpath(THESTAGE_CONFIG_FILE)
215
+
216
+
217
+ def update_allowed_commands_and_is_token_valid(self, validate_token_response: ValidateTokenResponse):
218
+ config = self.get_config()
219
+ for cli_command_item in CliCommand:
220
+ is_command_available = validate_token_response.allowedCliCommands.get(cli_command_item)
221
+ command_availability = CliCommandAvailability.DEPRECATED
222
+
223
+ if cli_command_item in ALWAYS_AVAILABLE_COMMANDS:
224
+ command_availability = CliCommandAvailability.ALLOWED
225
+ else:
226
+ if is_command_available is True:
227
+ command_availability = CliCommandAvailability.ALLOWED
228
+ if is_command_available is False:
229
+ command_availability = CliCommandAvailability.RESTRICTED
230
+ config.runtime.allowed_commands.update({cli_command_item: command_availability})
231
+ config.runtime.is_token_valid = validate_token_response.is_success
232
+ self.update_config(config)
233
+
234
+
235
+ def is_cli_command_allowed(self, cli_command: CliCommand) -> bool:
236
+ config = self.get_config()
237
+ return True if config.runtime.allowed_commands.get(cli_command) == CliCommandAvailability.ALLOWED else False