thestage 0.5.37__py3-none-any.whl → 0.5.39__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.
- thestage/__init__.py +1 -1
- thestage/controllers/config_controller.py +3 -4
- thestage/controllers/container_controller.py +12 -16
- thestage/controllers/project_controller.py +145 -3
- thestage/controllers/utils_controller.py +2 -3
- thestage/entities/file_item.py +27 -0
- thestage/entities/project_inference_simulator_model.py +1 -0
- thestage/exceptions/file_system_exception.py +6 -0
- thestage/helpers/error_handler.py +2 -2
- thestage/helpers/logger/app_logger.py +3 -4
- thestage/services/abstract_service.py +1 -2
- thestage/services/app_config_service.py +2 -3
- thestage/services/clients/git/git_client.py +3 -3
- thestage/services/clients/thestage_api/api_client.py +58 -61
- thestage/services/clients/thestage_api/core/api_client_abstract.py +91 -0
- thestage/services/clients/thestage_api/core/api_client_core.py +25 -0
- thestage/services/clients/thestage_api/core/http_client_exception.py +12 -0
- thestage/services/clients/thestage_api/dtos/inference_controller/deploy_inference_model_to_instance_request.py +14 -0
- thestage/services/clients/thestage_api/dtos/inference_controller/deploy_inference_model_to_instance_response.py +13 -0
- thestage/services/clients/thestage_api/dtos/inference_controller/deploy_inference_model_to_sagemaker_request.py +12 -0
- thestage/services/clients/thestage_api/dtos/inference_controller/deploy_inference_model_to_sagemaker_response.py +12 -0
- thestage/services/clients/thestage_api/dtos/logging_controller/log_polling_request.py +1 -1
- thestage/services/clients/thestage_api/dtos/sftp_path_helper.py +3 -2
- thestage/services/config_provider/config_provider.py +98 -44
- thestage/services/connect/connect_service.py +1 -1
- thestage/services/container/container_service.py +2 -8
- thestage/services/core_files/config_entity.py +25 -0
- thestage/services/filesystem_service.py +115 -0
- thestage/services/instance/instance_service.py +1 -2
- thestage/services/logging/logging_service.py +76 -95
- thestage/services/project/dto/inference_simulator_model_dto.py +1 -0
- thestage/services/project/mapper/project_inference_simulator_model_mapper.py +1 -0
- thestage/services/project/project_service.py +184 -7
- thestage/services/remote_server_service.py +3 -3
- thestage/services/service_factory.py +1 -2
- thestage/services/validation_service.py +26 -10
- {thestage-0.5.37.dist-info → thestage-0.5.39.dist-info}/METADATA +2 -2
- {thestage-0.5.37.dist-info → thestage-0.5.39.dist-info}/RECORD +41 -30
- {thestage-0.5.37.dist-info → thestage-0.5.39.dist-info}/LICENSE.txt +0 -0
- {thestage-0.5.37.dist-info → thestage-0.5.39.dist-info}/WHEEL +0 -0
- {thestage-0.5.37.dist-info → thestage-0.5.39.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from typing import Optional, List, Tuple
|
|
2
|
+
|
|
3
|
+
from thestage.services.clients.thestage_api.core.api_client_abstract import TheStageApiClientAbstract
|
|
4
|
+
from thestage.services.clients.thestage_api.dtos.base_response import TheStageBaseResponse
|
|
5
|
+
from thestage import __version__
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TheStageApiClientCore(TheStageApiClientAbstract):
|
|
9
|
+
|
|
10
|
+
def __init__(self, timeout: int = 90, url: Optional[str] = None):
|
|
11
|
+
super(TheStageApiClientCore, self).__init__(timeout=timeout, url=url)
|
|
12
|
+
|
|
13
|
+
def validate_token(self, token: str) -> bool:
|
|
14
|
+
data = {
|
|
15
|
+
"userApiToken": token,
|
|
16
|
+
"cliVersion": __version__,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
response = self._request(
|
|
20
|
+
method='POST',
|
|
21
|
+
url='/user-api/v1/validate-token',
|
|
22
|
+
data=data,
|
|
23
|
+
)
|
|
24
|
+
result = TheStageBaseResponse.model_validate(response) if response else None
|
|
25
|
+
return result.is_success if result else False
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from thestage.exceptions.base_exception import BaseAbstractException
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class HttpClientException(BaseAbstractException):
|
|
5
|
+
_status_code: int = 0
|
|
6
|
+
|
|
7
|
+
def __init__(self, message: str, status_code: int, ):
|
|
8
|
+
super(HttpClientException, self).__init__(message=message)
|
|
9
|
+
self._status_code = status_code
|
|
10
|
+
|
|
11
|
+
def get_status_code(self) -> int:
|
|
12
|
+
return self._status_code
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from typing import Optional, List
|
|
2
|
+
|
|
3
|
+
from pydantic import Field, ConfigDict, BaseModel
|
|
4
|
+
|
|
5
|
+
from thestage.services.clients.thestage_api.dtos.entity_filter_request import EntityFilterRequest
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DeployInferenceModelToInstanceRequest(BaseModel):
|
|
9
|
+
model_config = ConfigDict(use_enum_values=True)
|
|
10
|
+
|
|
11
|
+
modelSlug: Optional[str] = Field(None, alias='modelSlug')
|
|
12
|
+
instanceRentedSlug: Optional[str] = Field(None, alias='instanceRentedSlug')
|
|
13
|
+
selfhostedInstanceSlug: Optional[str] = Field(None, alias='selfhostedInstanceSlug')
|
|
14
|
+
inferenceSimulatorSlug: Optional[str] = Field(None, alias='inferenceSimulatorSlug')
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from pydantic import Field, ConfigDict
|
|
4
|
+
|
|
5
|
+
from thestage.services.clients.thestage_api.dtos.base_response import TheStageBaseResponse
|
|
6
|
+
from thestage.services.project.dto.inference_simulator_model_dto import InferenceSimulatorModelDto
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DeployInferenceModelToInstanceResponse(TheStageBaseResponse):
|
|
10
|
+
model_config = ConfigDict(use_enum_values=True)
|
|
11
|
+
inferenceSimulatorModel: Optional[InferenceSimulatorModelDto] = Field(None, alias='inferenceSimulatorModel')
|
|
12
|
+
|
|
13
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from typing import Optional, List
|
|
2
|
+
|
|
3
|
+
from pydantic import Field, ConfigDict, BaseModel
|
|
4
|
+
|
|
5
|
+
from thestage.services.clients.thestage_api.dtos.entity_filter_request import EntityFilterRequest
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DeployInferenceModelToSagemakerRequest(BaseModel):
|
|
9
|
+
model_config = ConfigDict(use_enum_values=True)
|
|
10
|
+
|
|
11
|
+
modelSlug: Optional[str] = Field(None, alias='modelSlug')
|
|
12
|
+
arn: Optional[str] = Field(None, alias='arn')
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from pydantic import Field, ConfigDict
|
|
2
|
+
|
|
3
|
+
from thestage.services.clients.thestage_api.dtos.base_response import TheStageBaseResponse
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DeployInferenceModelToSagemakerResponse(TheStageBaseResponse):
|
|
7
|
+
model_config = ConfigDict(use_enum_values=True)
|
|
8
|
+
modelId: str = Field(None, alias='modelId')
|
|
9
|
+
ecrImageUrl: str = Field(None, alias='ecrImageUrl')
|
|
10
|
+
s3ArtifactsUrl: str = Field(None, alias='s3ArtifactsUrl')
|
|
11
|
+
|
|
12
|
+
|
|
@@ -9,5 +9,5 @@ class LogPollingRequest(BaseModel):
|
|
|
9
9
|
taskId: Optional[int] = Field(None, alias='taskId')
|
|
10
10
|
inferenceSimulatorId: Optional[int] = Field(None, alias='inferenceSimulatorId')
|
|
11
11
|
lastLogId: Optional[str] = Field(None, alias='lastLogId')
|
|
12
|
-
dockerContainerId: Optional[
|
|
12
|
+
dockerContainerId: Optional[int] = Field(None, alias='dockerContainerId')
|
|
13
13
|
lastLogTimestamp: Optional[str] = Field(None, alias='lastLogTimestamp')
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from typing import Optional, List
|
|
2
2
|
|
|
3
|
-
from pydantic import
|
|
4
|
-
|
|
3
|
+
from pydantic import Field
|
|
4
|
+
|
|
5
|
+
from thestage.entities.file_item import FileItemEntity
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
class SftpFileItemEntity(FileItemEntity):
|
|
@@ -1,30 +1,56 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
|
|
3
|
+
import json
|
|
1
4
|
import os
|
|
5
|
+
from json import JSONDecodeError
|
|
2
6
|
from pathlib import Path
|
|
3
|
-
from typing import Optional, Dict
|
|
7
|
+
from typing import Optional, Dict, Any
|
|
4
8
|
|
|
5
|
-
import
|
|
6
|
-
from
|
|
7
|
-
from thestage_core.services.config_provider.config_provider import ConfigProviderCore
|
|
8
|
-
|
|
9
|
-
from thestage.config import THESTAGE_AUTH_TOKEN, THESTAGE_API_URL
|
|
9
|
+
from thestage.exceptions.file_system_exception import FileSystemException
|
|
10
|
+
from thestage.services.core_files.config_entity import ConfigEntity
|
|
10
11
|
from thestage.helpers.ssh_util import parse_private_key
|
|
11
12
|
from thestage.services.connect.dto.remote_server_config import RemoteServerConfig
|
|
13
|
+
from thestage.services.filesystem_service import FileSystemServiceCore
|
|
12
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
|
+
|
|
17
|
+
class ConfigProvider():
|
|
18
|
+
# path to current physical subject we work with (project / etc) - might be passed explicitly
|
|
19
|
+
_local_path: Optional[Path] = None
|
|
20
|
+
_local_config_path: Optional[Path] = None
|
|
21
|
+
_global_config_path: Optional[Path] = None
|
|
22
|
+
_global_config_file: Optional[Path] = None
|
|
23
|
+
_file_system_service = FileSystemServiceCore
|
|
13
24
|
|
|
14
25
|
|
|
15
|
-
class ConfigProvider(ConfigProviderCore):
|
|
16
26
|
def __init__(
|
|
17
27
|
self,
|
|
18
|
-
local_path: str,
|
|
28
|
+
local_path: Optional[str] = None,
|
|
19
29
|
):
|
|
20
|
-
|
|
21
|
-
|
|
30
|
+
self._file_system_service = FileSystemServiceCore()
|
|
31
|
+
if local_path:
|
|
32
|
+
self._local_path = self._file_system_service.get_path(directory=local_path, auto_create=False)
|
|
33
|
+
|
|
34
|
+
config_folder_name = self._file_system_service.get_path(f"{THESTAGE_CONFIG_DIR}", False)
|
|
35
|
+
self._local_config_path = self._local_path.joinpath(config_folder_name)
|
|
36
|
+
|
|
37
|
+
home_dir = self._file_system_service.get_home_path()
|
|
38
|
+
self._global_config_path = home_dir.joinpath(config_folder_name)
|
|
39
|
+
|
|
40
|
+
if self._global_config_path:
|
|
41
|
+
if not self._global_config_path.exists():
|
|
42
|
+
self._file_system_service.create_if_not_exists_dir(self._global_config_path)
|
|
43
|
+
|
|
44
|
+
self._global_config_file = self._global_config_path.joinpath(
|
|
45
|
+
self._file_system_service.get_path(f"{THESTAGE_CONFIG_FILE}", False)
|
|
22
46
|
)
|
|
47
|
+
if not self._global_config_file.exists():
|
|
48
|
+
self._file_system_service.create_if_not_exists_file(self._global_config_file)
|
|
49
|
+
|
|
23
50
|
|
|
24
|
-
def get_full_config(self
|
|
51
|
+
def get_full_config(self) -> ConfigEntity:
|
|
25
52
|
config_values = {}
|
|
26
53
|
|
|
27
|
-
# read env data
|
|
28
54
|
config_from_env = {}
|
|
29
55
|
config_from_env['main'] = {}
|
|
30
56
|
if THESTAGE_AUTH_TOKEN:
|
|
@@ -35,8 +61,7 @@ class ConfigProvider(ConfigProviderCore):
|
|
|
35
61
|
if config_from_env:
|
|
36
62
|
self.__update_config_values_dict(values_to_update=config_values, new_values=config_from_env)
|
|
37
63
|
|
|
38
|
-
|
|
39
|
-
config_from_file = self._read_config_file(self._global_config_file)
|
|
64
|
+
config_from_file = self.__read_config_file(self._global_config_file)
|
|
40
65
|
if config_from_file:
|
|
41
66
|
self.__update_config_values_dict(values_to_update=config_values, new_values=config_from_file)
|
|
42
67
|
|
|
@@ -47,27 +72,25 @@ class ConfigProvider(ConfigProviderCore):
|
|
|
47
72
|
if self._global_config_path and not config.runtime.config_global_path:
|
|
48
73
|
config.runtime.config_global_path = str(self._global_config_path)
|
|
49
74
|
|
|
50
|
-
if check_daemon:
|
|
51
|
-
self.check_for_daemon(config=config)
|
|
52
|
-
|
|
53
75
|
return config
|
|
54
76
|
|
|
77
|
+
|
|
55
78
|
def save_project_config(self, project_config: ProjectConfig):
|
|
56
|
-
self.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
79
|
+
project_data_dirpath = self.__get_project_config_path()
|
|
80
|
+
if not project_data_dirpath.exists():
|
|
81
|
+
self._file_system_service.create_if_not_exists_dir(project_data_dirpath)
|
|
82
|
+
|
|
83
|
+
project_data_filepath = self.__get_project_config_path(with_file=True)
|
|
84
|
+
if not project_data_filepath.exists():
|
|
85
|
+
self._file_system_service.create_if_not_exists_file(project_data_filepath)
|
|
60
86
|
|
|
61
|
-
def save_remote_server_config(self, remote_server_config: RemoteServerConfig):
|
|
62
|
-
self.__create_empty_project_config_if_missing()
|
|
63
|
-
# self.read_project_config()
|
|
64
87
|
project_config_path = self.__get_project_config_path(with_file=True)
|
|
65
|
-
self.
|
|
88
|
+
self.__save_config_file(data=project_config.model_dump(), file_path=project_config_path)
|
|
66
89
|
|
|
67
90
|
|
|
68
91
|
def save_project_deploy_ssh_key(self, deploy_ssh_key: str, project_slug: str, project_id: int) -> str:
|
|
69
92
|
deploy_key_dirpath = self._global_config_path.joinpath('project_deploy_keys')
|
|
70
|
-
self._file_system_service.
|
|
93
|
+
self._file_system_service.create_if_not_exists_dir(deploy_key_dirpath)
|
|
71
94
|
|
|
72
95
|
deploy_key_filepath = deploy_key_dirpath.joinpath(f'project_deploy_key_{project_id}_{project_slug}')
|
|
73
96
|
self._file_system_service.create_if_not_exists_file(deploy_key_filepath)
|
|
@@ -79,30 +102,20 @@ class ConfigProvider(ConfigProviderCore):
|
|
|
79
102
|
|
|
80
103
|
return str(deploy_key_filepath)
|
|
81
104
|
|
|
105
|
+
|
|
82
106
|
def read_project_config(self) -> Optional[ProjectConfig]:
|
|
83
107
|
project_data_dirpath = self.__get_project_config_path()
|
|
84
108
|
if not project_data_dirpath.exists():
|
|
85
109
|
return None
|
|
86
|
-
# self._file_system_service.create_if_not_exists(project_data_dirpath)
|
|
87
110
|
|
|
88
111
|
project_data_filepath = self.__get_project_config_path(with_file=True)
|
|
89
112
|
if not project_data_filepath.exists():
|
|
90
113
|
return None
|
|
91
114
|
|
|
92
|
-
config_data = self.
|
|
115
|
+
config_data = self.__read_config_file(project_data_filepath) if project_data_filepath and project_data_filepath.exists() else {}
|
|
93
116
|
return ProjectConfig.model_validate(config_data)
|
|
94
117
|
|
|
95
118
|
|
|
96
|
-
def __create_empty_project_config_if_missing(self):
|
|
97
|
-
project_data_dirpath = self.__get_project_config_path()
|
|
98
|
-
if not project_data_dirpath.exists():
|
|
99
|
-
self._file_system_service.create_if_not_exists(project_data_dirpath)
|
|
100
|
-
|
|
101
|
-
project_data_filepath = self.__get_project_config_path(with_file=True)
|
|
102
|
-
if not project_data_filepath.exists():
|
|
103
|
-
self._file_system_service.create_if_not_exists_file(project_data_filepath)
|
|
104
|
-
|
|
105
|
-
|
|
106
119
|
def __get_project_config_path(self, with_file: bool = False) -> Path:
|
|
107
120
|
if with_file:
|
|
108
121
|
return self._local_config_path.joinpath('project.json')
|
|
@@ -124,7 +137,7 @@ class ConfigProvider(ConfigProviderCore):
|
|
|
124
137
|
typer.echo("Could not identify provided private key (expected RSA, ECDSA, ED25519)")
|
|
125
138
|
raise typer.Exit(1)
|
|
126
139
|
|
|
127
|
-
config = self.
|
|
140
|
+
config = self.__read_remote_server_config()
|
|
128
141
|
remote_server_config_filepath = self.__get_remote_server_config_path()
|
|
129
142
|
if config:
|
|
130
143
|
if not config.ip_address_to_ssh_key_map:
|
|
@@ -142,21 +155,22 @@ class ConfigProvider(ConfigProviderCore):
|
|
|
142
155
|
self._file_system_service.create_if_not_exists_file(remote_server_config_filepath)
|
|
143
156
|
config = RemoteServerConfig(ip_address_to_ssh_key_map={ip_address: str(ssh_key_path)})
|
|
144
157
|
|
|
145
|
-
self.
|
|
158
|
+
self.__save_config_file(data=config.model_dump(), file_path=remote_server_config_filepath)
|
|
146
159
|
|
|
147
160
|
return str(remote_server_config_filepath)
|
|
148
161
|
|
|
149
162
|
|
|
150
|
-
def
|
|
163
|
+
def __read_remote_server_config(self) -> Optional[RemoteServerConfig]:
|
|
151
164
|
config_filepath = self.__get_remote_server_config_path()
|
|
152
165
|
if not config_filepath.is_file():
|
|
153
166
|
return None
|
|
154
167
|
|
|
155
|
-
config_data = self.
|
|
168
|
+
config_data = self.__read_config_file(config_filepath) if config_filepath and config_filepath.exists() else {}
|
|
156
169
|
return RemoteServerConfig.model_validate(config_data)
|
|
157
170
|
|
|
171
|
+
|
|
158
172
|
def get_valid_private_key_path_by_ip_address(self, ip_address: str) -> Optional[str]:
|
|
159
|
-
remote_server_config = self.
|
|
173
|
+
remote_server_config = self.__read_remote_server_config()
|
|
160
174
|
if remote_server_config and remote_server_config.ip_address_to_ssh_key_map:
|
|
161
175
|
private_key_path = remote_server_config.ip_address_to_ssh_key_map.get(ip_address)
|
|
162
176
|
if private_key_path and Path(private_key_path).is_file():
|
|
@@ -165,10 +179,50 @@ class ConfigProvider(ConfigProviderCore):
|
|
|
165
179
|
self.update_remote_server_config_entry(ip_address, None)
|
|
166
180
|
return None
|
|
167
181
|
|
|
182
|
+
|
|
168
183
|
@staticmethod
|
|
169
184
|
def __update_config_values_dict(values_to_update: Dict, new_values: Dict):
|
|
170
185
|
if 'main' in new_values:
|
|
171
186
|
if 'main' in values_to_update:
|
|
172
187
|
values_to_update['main'].update(new_values['main'])
|
|
173
188
|
else:
|
|
174
|
-
values_to_update['main'] = new_values['main']
|
|
189
|
+
values_to_update['main'] = new_values['main']
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def __read_config_file(self, path: Path) -> Dict[str, Any]:
|
|
193
|
+
result = {}
|
|
194
|
+
try:
|
|
195
|
+
if path and path.exists():
|
|
196
|
+
with path.open("r") as file:
|
|
197
|
+
try:
|
|
198
|
+
if os.stat(path).st_size != 0:
|
|
199
|
+
result = json.load(file)
|
|
200
|
+
except JSONDecodeError:
|
|
201
|
+
pass
|
|
202
|
+
except OSError:
|
|
203
|
+
raise FileSystemException(f"Could not open config file: {path}")
|
|
204
|
+
return result
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@staticmethod
|
|
208
|
+
def __save_config_file(data: Dict, file_path: Path):
|
|
209
|
+
with open(file_path, 'w') as configfile:
|
|
210
|
+
json.dump(data, configfile, indent=1)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def save_config(self, config: ConfigEntity):
|
|
214
|
+
data: Dict[str, Any] = self.__read_config_file(self._global_config_file)
|
|
215
|
+
data.update(config.model_dump(exclude_none=True, by_alias=True, exclude={'runtime', 'RUNTIME', 'daemon', 'DAEMON'}))
|
|
216
|
+
self.__save_config_file(data=data, file_path=self._global_config_file)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def clear_config(self, ):
|
|
220
|
+
if self._global_config_path and self._global_config_path.exists():
|
|
221
|
+
self._file_system_service.remove_folder(str(self._global_config_path))
|
|
222
|
+
|
|
223
|
+
os.unsetenv('THESTAGE_CONFIG_DIR')
|
|
224
|
+
os.unsetenv('THESTAGE_CONFIG_FILE')
|
|
225
|
+
os.unsetenv('THESTAGE_CLI_ENV')
|
|
226
|
+
os.unsetenv('THESTAGE_API_URL')
|
|
227
|
+
os.unsetenv('THESTAGE_LOG_FILE')
|
|
228
|
+
os.unsetenv('THESTAGE_AUTH_TOKEN')
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from typing import Optional
|
|
2
2
|
import typer
|
|
3
|
-
from thestage_core.exceptions.http_error_exception import HttpClientException
|
|
4
3
|
|
|
5
4
|
from thestage.i18n.translation import __
|
|
5
|
+
from thestage.services.clients.thestage_api.core.http_client_exception import HttpClientException
|
|
6
6
|
from thestage.services.clients.thestage_api.dtos.enums.container_status import DockerContainerStatus
|
|
7
7
|
from thestage.services.clients.thestage_api.dtos.enums.selfhosted_status import SelfhostedBusinessStatus
|
|
8
8
|
from thestage.services.clients.thestage_api.dtos.enums.instance_rented_status import InstanceRentedBusinessStatus
|
|
@@ -1,22 +1,16 @@
|
|
|
1
|
-
import uuid
|
|
2
1
|
from pathlib import Path
|
|
3
2
|
from typing import List, Tuple, Optional, Dict
|
|
4
3
|
|
|
5
4
|
import typer
|
|
6
|
-
from
|
|
7
|
-
from thestage_core.entities.file_item import FileItemEntity
|
|
8
|
-
from thestage_core.services.filesystem_service import FileSystemServiceCore
|
|
9
|
-
|
|
5
|
+
from thestage.services.core_files.config_entity import ConfigEntity
|
|
10
6
|
from thestage.entities.container import DockerContainerEntity
|
|
11
|
-
from thestage.helpers.logger.app_logger import app_logger
|
|
12
7
|
from thestage.services.clients.thestage_api.dtos.container_param_request import DockerContainerActionRequestDto
|
|
13
8
|
from thestage.services.clients.thestage_api.dtos.enums.container_pending_action import DockerContainerAction
|
|
14
9
|
from thestage.services.clients.thestage_api.dtos.enums.container_status import DockerContainerStatus
|
|
15
10
|
from thestage.entities.enums.shell_type import ShellType
|
|
16
11
|
from thestage.services.clients.thestage_api.dtos.paginated_entity_list import PaginatedEntityList
|
|
17
|
-
from thestage.services.clients.thestage_api.dtos.project_response import ProjectDto
|
|
18
|
-
from thestage.services.clients.thestage_api.dtos.sftp_path_helper import SftpFileItemEntity
|
|
19
12
|
from thestage.services.container.mapper.container_mapper import ContainerMapper
|
|
13
|
+
from thestage.services.filesystem_service import FileSystemServiceCore
|
|
20
14
|
from thestage.services.remote_server_service import RemoteServerService
|
|
21
15
|
from thestage.i18n.translation import __
|
|
22
16
|
from thestage.services.abstract_service import AbstractService
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
# saved to file
|
|
6
|
+
class MainConfigEntity(BaseModel):
|
|
7
|
+
thestage_auth_token: Optional[str] = Field(None, alias='thestage_auth_token')
|
|
8
|
+
thestage_api_url: Optional[str] = Field(None, alias='thestage_api_url')
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DaemonConfigEntity(BaseModel):
|
|
12
|
+
daemon_token: Optional[str] = Field(None, alias='daemon_token')
|
|
13
|
+
backend_api_url: Optional[str] = Field(None, alias='backend_api_url')
|
|
14
|
+
|
|
15
|
+
# not saved to file
|
|
16
|
+
class RuntimeConfigEntity(BaseModel):
|
|
17
|
+
working_directory: Optional[str] = Field(None, alias='working_directory')
|
|
18
|
+
config_global_path: Optional[str] = Field(None, alias='config_global_path')
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ConfigEntity(BaseModel):
|
|
22
|
+
main: MainConfigEntity = Field(default_factory=MainConfigEntity, alias='main')
|
|
23
|
+
runtime: RuntimeConfigEntity = Field(default_factory=RuntimeConfigEntity, alias="runtime") # TODO merge with main
|
|
24
|
+
daemon: DaemonConfigEntity = Field(default_factory=DaemonConfigEntity, alias="daemon") # TODO this should not be in core package
|
|
25
|
+
start_on_daemon: bool = Field(False, alias='start_on_daemon') # TODO this should not be in core package
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional, List
|
|
5
|
+
|
|
6
|
+
from thestage.entities.file_item import FileItemEntity
|
|
7
|
+
from thestage.exceptions.file_system_exception import FileSystemException
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FileSystemServiceCore:
|
|
11
|
+
|
|
12
|
+
def get_ssh_path(self) -> Optional[Path]:
|
|
13
|
+
home_path = self.get_home_path()
|
|
14
|
+
ssh_path = home_path.joinpath('.ssh')
|
|
15
|
+
if not ssh_path.exists():
|
|
16
|
+
raise FileSystemException(f"Path does not exist: {ssh_path}")
|
|
17
|
+
return ssh_path
|
|
18
|
+
|
|
19
|
+
def get_home_path(self) -> Optional[Path]:
|
|
20
|
+
try:
|
|
21
|
+
return Path.home()
|
|
22
|
+
except RuntimeError | OSError as ex1:
|
|
23
|
+
raise FileSystemException("Error getting user home path") from ex1
|
|
24
|
+
|
|
25
|
+
def create_if_not_exists_dir(self, path: Path) -> Path:
|
|
26
|
+
if not path.exists():
|
|
27
|
+
try:
|
|
28
|
+
path.mkdir(exist_ok=True, parents=True)
|
|
29
|
+
except FileNotFoundError as ex1:
|
|
30
|
+
raise FileSystemException(message=f"FileNotFoundError (dir): {path}") from ex1
|
|
31
|
+
except OSError as ex2:
|
|
32
|
+
raise FileSystemException(message=f"Could not create directory: {path}") from ex2
|
|
33
|
+
return path
|
|
34
|
+
|
|
35
|
+
def create_if_not_exists_file(self, path: Path) -> Path:
|
|
36
|
+
if not path.exists():
|
|
37
|
+
try:
|
|
38
|
+
path.touch(exist_ok=True)
|
|
39
|
+
except FileNotFoundError as ex1:
|
|
40
|
+
raise FileSystemException(message=f"FileNotFoundError (file): {path}") from ex1
|
|
41
|
+
except OSError as ex2:
|
|
42
|
+
raise FileSystemException(message=f"Could not create file: {path}") from ex2
|
|
43
|
+
return path
|
|
44
|
+
|
|
45
|
+
def get_path(self, directory: str, auto_create: bool = True) -> Path:
|
|
46
|
+
path = Path(directory)
|
|
47
|
+
if auto_create:
|
|
48
|
+
self.create_if_not_exists_dir(path)
|
|
49
|
+
return path
|
|
50
|
+
|
|
51
|
+
def is_folder_empty(self, folder: str, auto_create: bool) -> bool:
|
|
52
|
+
path = self.get_path(folder, auto_create)
|
|
53
|
+
if not path.exists():
|
|
54
|
+
return True
|
|
55
|
+
if not path.is_dir():
|
|
56
|
+
raise FileSystemException(message=f"Expected directory but found a file: {path}")
|
|
57
|
+
objects = os.listdir(path)
|
|
58
|
+
if len(objects) == 0:
|
|
59
|
+
return True
|
|
60
|
+
else:
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
def is_folder_exists(self, folder: str, auto_create: bool = True) -> bool:
|
|
64
|
+
path = self.get_path(folder, auto_create=auto_create)
|
|
65
|
+
if path.exists():
|
|
66
|
+
return True
|
|
67
|
+
else:
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
def find_line_in_text_file(self, file: str, find: str) -> bool:
|
|
71
|
+
path = self.get_path(file, auto_create=False)
|
|
72
|
+
if path and path.exists():
|
|
73
|
+
with open(path, 'r') as file:
|
|
74
|
+
for line in file.readlines():
|
|
75
|
+
if (find + "\n") == line:
|
|
76
|
+
return True
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
def add_line_to_text_file(self, file: str, new_line: str):
|
|
80
|
+
path = self.get_path(file, auto_create=False)
|
|
81
|
+
if path and path.exists():
|
|
82
|
+
with open(path, 'a') as file:
|
|
83
|
+
file.write(new_line)
|
|
84
|
+
file.write('\n')
|
|
85
|
+
|
|
86
|
+
def check_if_path_exist(self, file: str) -> bool:
|
|
87
|
+
path = self.get_path(file, auto_create=False)
|
|
88
|
+
if path.exists():
|
|
89
|
+
return True
|
|
90
|
+
else:
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
def get_path_items(self, folder: str) -> List[FileItemEntity]:
|
|
94
|
+
path = self.get_path(folder, auto_create=False)
|
|
95
|
+
path_items = []
|
|
96
|
+
if not path.exists():
|
|
97
|
+
return path_items
|
|
98
|
+
|
|
99
|
+
parent = FileItemEntity.build_from_path(path=path)
|
|
100
|
+
path_items.append(parent)
|
|
101
|
+
if path.is_dir():
|
|
102
|
+
objects = os.listdir(path)
|
|
103
|
+
if objects:
|
|
104
|
+
for item in objects:
|
|
105
|
+
elem = path.joinpath(item)
|
|
106
|
+
if elem.is_dir():
|
|
107
|
+
parent.children.extend(self.get_path_items(folder=str(elem)))
|
|
108
|
+
else:
|
|
109
|
+
parent.children.append(FileItemEntity.build_from_path(path=elem))
|
|
110
|
+
return path_items
|
|
111
|
+
|
|
112
|
+
def remove_folder(self, path: str):
|
|
113
|
+
real_path = self.get_path(directory=path, auto_create=False)
|
|
114
|
+
if real_path and real_path.exists():
|
|
115
|
+
shutil.rmtree(real_path)
|
|
@@ -2,8 +2,7 @@ from pathlib import Path
|
|
|
2
2
|
from typing import List, Optional, Dict
|
|
3
3
|
|
|
4
4
|
import typer
|
|
5
|
-
from
|
|
6
|
-
|
|
5
|
+
from thestage.services.core_files.config_entity import ConfigEntity
|
|
7
6
|
from thestage.i18n.translation import __
|
|
8
7
|
from thestage.services.clients.thestage_api.dtos.enums.selfhosted_status import SelfhostedBusinessStatus
|
|
9
8
|
from thestage.services.clients.thestage_api.dtos.enums.instance_rented_status import InstanceRentedBusinessStatus
|