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,436 +1,436 @@
1
- import datetime
2
-
3
- import os
4
-
5
- from pathlib import Path
6
- from typing import Optional, List
7
-
8
- import git
9
- import typer
10
- from git import Remote, Repo, GitCommandError, Commit
11
- from gitdb.exc import BadName
12
- from rich import print
13
-
14
- from thestage.color_scheme.color_scheme import ColorScheme
15
- from thestage.config import THESTAGE_CONFIG_DIR
16
- from thestage.exceptions.git_access_exception import GitAccessException
17
- from thestage.git.ProgressPrinter import ProgressPrinter
18
- from thestage.services.filesystem_service import FileSystemService
19
-
20
-
21
- class GitLocalClient:
22
- __base_name_remote: str = 'origin'
23
- __base_name_local: str = 'main'
24
- __git_ignore_thestage_line: str = f'/{THESTAGE_CONFIG_DIR}/'
25
-
26
- __special_main_branches = ['main', 'master']
27
-
28
- __base_git_url: str = 'https://github.com/'
29
-
30
- def __init__(
31
- self,
32
- file_system_service: FileSystemService,
33
- ):
34
- self.__file_system_service = file_system_service
35
-
36
- # todo delete this fuckery
37
- def __get_repo(self, path: str) -> Repo:
38
- return git.Repo(path)
39
-
40
- def is_present_local_git(self, path: str) -> bool:
41
- git_path = self.__file_system_service.get_path(path)
42
- if not git_path.exists():
43
- return False
44
-
45
- git_path = git_path.joinpath('.git')
46
- if not git_path.exists():
47
- return False
48
-
49
- result = git.repo.base.is_git_dir(git_path)
50
- return result
51
-
52
- def get_remote(self, path: str) -> Optional[List[Remote]]:
53
- is_git_repo = self.is_present_local_git(path=path)
54
- if is_git_repo:
55
- repo = git.Repo(path)
56
- remotes: Optional[List[Remote]] = list(repo.remotes) if repo.remotes else []
57
- return remotes
58
- return None
59
-
60
- def has_remote(self, path: str) -> bool:
61
- remotes: Optional[List[Remote]] = self.get_remote(path)
62
- return True if remotes is not None and len(remotes) > 0 else False
63
-
64
- def has_changes_with_untracked(self, path: str) -> bool:
65
- repo = self.__get_repo(path=path)
66
- return repo.is_dirty(untracked_files=True)
67
-
68
- def init_repository(
69
- self,
70
- path: str,
71
- ) -> Optional[Repo]:
72
-
73
- repo = git.Repo.init(path)
74
- if repo:
75
- # default git name master, rename to main - sync wih github
76
- repo.git.branch("-M", self.__base_name_local)
77
- return repo
78
-
79
- def add_remote_to_repo(
80
- self,
81
- path: str,
82
- remote_url: str,
83
- remote_name: str,
84
- ) -> bool:
85
- repo = self.__get_repo(path=path)
86
- remotes: List[Remote] = repo.remotes
87
- not_present = True
88
- if remotes:
89
- item = list(filter(lambda x: x.name == remote_name, remotes))
90
- if len(item) > 0:
91
- not_present = False
92
-
93
- if not_present:
94
- remote: Remote = repo.create_remote(
95
- name=self.__base_name_remote,
96
- url=remote_url,
97
- )
98
- if remote:
99
- return True
100
- else:
101
- return False
102
- else:
103
- return True
104
-
105
- def git_fetch(self, path: str, deploy_key_path: str):
106
- repo = self.__get_repo(path=path)
107
- git_ssh_cmd = 'ssh -F /dev/null -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -i %s' % deploy_key_path
108
-
109
- with repo.git.custom_environment(GIT_SSH_COMMAND=git_ssh_cmd):
110
- remote: Remote = repo.remote(self.__base_name_remote)
111
- if remote:
112
- progress = ProgressPrinter()
113
- try:
114
- remote.fetch(progress=progress)
115
- except GitCommandError as ex:
116
- for line in progress.allDroppedLines():
117
- # returning the whole output if failed - so that user have any idea what's going on
118
- print(f'>> {line}')
119
- raise ex
120
-
121
-
122
- def git_pull(self, path: str, deploy_key_path: str):
123
- repo = self.__get_repo(path=path)
124
- git_ssh_cmd = 'ssh -F /dev/null -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -i %s' % deploy_key_path
125
-
126
- with repo.git.custom_environment(GIT_SSH_COMMAND=git_ssh_cmd):
127
- local_branch = self.__base_name_local
128
- if repo.active_branch.name:
129
- local_branch = repo.active_branch.name
130
-
131
- origin = repo.remote(self.__base_name_remote)
132
-
133
- if origin:
134
- progress = ProgressPrinter()
135
- try:
136
- origin.pull(refspec=local_branch, progress=progress)
137
- typer.echo(f"Pulled remote changes to branch '{local_branch}'")
138
- except GitCommandError as ex:
139
- for line in progress.allDroppedLines():
140
- # returning the whole output if failed - so that user have any idea what's going on
141
- print(f'>> {line}')
142
- raise ex
143
-
144
- def find_main_branch_name(self, path: str) -> Optional[str]:
145
- repo = self.__get_repo(path=path)
146
- if repo:
147
- for branch in [head.name for head in repo.heads]:
148
- if branch in self.__special_main_branches:
149
- return branch
150
- return None
151
-
152
- def get_active_branch_name(self, path: str) -> Optional[str]:
153
- repo = self.__get_repo(path=path)
154
- if repo:
155
- if repo.head.is_detached:
156
- return None
157
- return repo.active_branch.name
158
- return None
159
-
160
- def git_checkout_to_branch(self, path: str, branch: str):
161
- repo = self.__get_repo(path=path)
162
- if repo:
163
- repo.git.checkout(branch.strip())
164
-
165
- def git_checkout_to_commit(self, path: str, commit_hash: str = None) -> bool:
166
- repo = self.__get_repo(path=path)
167
- if repo:
168
- if is_commit_exists(repo, commit_hash):
169
- repo.git.checkout(commit_hash.strip())
170
- return True
171
- else:
172
- typer.echo(f"Could not checkout to commit {commit_hash} - reference not found in repository")
173
- return False
174
-
175
- def build_http_repo_url(self, git_path: str) -> str:
176
- start_path_pos = git_path.find(":")
177
- pre_url = git_path[start_path_pos + 1:]
178
- url = pre_url.replace('.git', '')
179
- return self.__base_git_url + url
180
-
181
- def clone(self, url: str, path: str, deploy_key_path: str) -> Optional[Repo]:
182
- try:
183
- git_ssh_cmd = 'ssh -F /dev/null -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -i %s' % deploy_key_path
184
- return Repo.clone_from(url=url, to_path=path, env={"GIT_SSH_COMMAND": git_ssh_cmd})
185
- except GitCommandError as base_ex:
186
- msg = base_ex.stderr
187
- if msg and 'Repository not found' in msg and 'correct access rights' in msg:
188
- raise GitAccessException(
189
- message='You dont have access to repository, or repository not found.',
190
- url=self.build_http_repo_url(git_path=url),
191
- dop_message=msg,
192
- )
193
- else:
194
- raise base_ex
195
-
196
- def commit_local_changes(
197
- self,
198
- path: str,
199
- name: Optional[str] = None,
200
- ) -> Optional[str]:
201
- repo = self.__get_repo(path=path)
202
- if repo.head.is_detached:
203
- line_color = ColorScheme.GIT_HEADLESS
204
- print(f'[{line_color}]Committing in detached head state at {repo.head.commit.hexsha}[/{line_color}]')
205
- commit_name = name if name else f"Auto commit {str(datetime.datetime.now().date())}"
206
- commit = repo.git.commit('--allow-empty', '-m', commit_name, )
207
- return commit
208
-
209
- def push_changes(
210
- self,
211
- path: str,
212
- deploy_key_path: str
213
- ):
214
- repo = self.__get_repo(path=path)
215
- git_ssh_cmd = 'ssh -F /dev/null -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -i %s' % deploy_key_path
216
-
217
- with repo.git.custom_environment(GIT_SSH_COMMAND=git_ssh_cmd):
218
- origin = repo.remote(self.__base_name_remote)
219
- if origin:
220
- progress = ProgressPrinter()
221
- # repo.git.push(origin.name, repo.active_branch.name)
222
- try:
223
- origin.push(refspec=repo.active_branch.name, progress=progress).raise_if_error()
224
- except GitCommandError as ex:
225
- for line in progress.allDroppedLines():
226
- # returning the whole output if failed - so that user have any idea what's going on
227
- print(f'>> {line}')
228
- raise ex
229
-
230
- def get_current_commit(self, path: str,) -> Optional[Commit]:
231
- repo = self.__get_repo(path=path)
232
- if repo:
233
- return repo.head.commit
234
- else:
235
- return None
236
-
237
- def get_commit_by_hash(self, path: str, commit_hash: str) -> Optional[Commit]:
238
- repo = self.__get_repo(path=path)
239
- if repo:
240
- try:
241
- return repo.commit(commit_hash)
242
- except BadName as ex:
243
- return None
244
- else:
245
- return None
246
-
247
- def _get_gitignore_path(self, path: str) -> Path:
248
- git_path = self.__file_system_service.get_path(path)
249
- return git_path.joinpath('.gitignore')
250
-
251
- def git_add_by_path(self, repo_path: str, file_path: str):
252
- repo = self.__get_repo(path=repo_path)
253
- if repo:
254
- abs_path = os.path.join(repo_path, file_path)
255
- if os.path.isfile(abs_path):
256
- repo.index.add([file_path])
257
- else:
258
- repo.index.remove([file_path])
259
-
260
- def git_add_all(self, repo_path: str):
261
- repo = self.__get_repo(path=repo_path)
262
- if repo:
263
- repo.git.add(all=True)
264
-
265
- def git_diff_stat(self, repo_path: str) -> str:
266
- repo = self.__get_repo(path=repo_path)
267
- if repo:
268
- try:
269
- diff: str = repo.git.diff("--cached", "--stat")
270
- if diff:
271
- return diff.splitlines()[-1]
272
- else:
273
- return None
274
- except ValueError as e:
275
- return str(e)
276
-
277
- def add_files_with_size_limit_or_warn(
278
- self,
279
- repo_path: str,
280
- files_to_add: Optional[str] = None,
281
- max_file_size: int = 500 * 1024, # default 500KB
282
- ) -> bool:
283
- """
284
- Adds to git only files <= max_file_size.
285
- If files_to_add is not provided, adds all changed/new files except those that are too large.
286
- Returns True if files were added, False if operation was aborted due to large files.
287
- """
288
- if files_to_add:
289
- return self.add_files_by_path(repo_path=repo_path, files_to_add=files_to_add, max_file_size=max_file_size)
290
-
291
- return self.add_all_files(repo_path=repo_path, max_file_size=max_file_size)
292
-
293
-
294
- def add_files_by_path(
295
- self,
296
- repo_path: str,
297
- files_to_add: str,
298
- max_file_size: int = 500 * 1024, # default 500KB
299
- ) -> bool:
300
- repo = self.__get_repo(path=repo_path)
301
-
302
- if files_to_add.strip().endswith(","):
303
- space_warning = f"[{ColorScheme.WARNING.value}][WARNING] Use only commas to separate files, without spaces[/{ColorScheme.WARNING.value}]"
304
- print(space_warning)
305
- return False
306
-
307
- files = [f.strip() for f in files_to_add.split(",") if f.strip()]
308
-
309
- rejected_file_paths = []
310
- missing_file_paths = []
311
-
312
- if repo.head.is_valid():
313
- staged_files = [item.a_path for item in repo.index.diff('HEAD')]
314
- else:
315
- staged_files = []
316
-
317
- deleted_files = [item.a_path for item in repo.index.diff(None) if item.change_type == 'D']
318
-
319
- for file_path in files:
320
- if file_path not in deleted_files and file_path not in staged_files:
321
- abs_path = os.path.join(repo_path, file_path)
322
-
323
- if not os.path.isfile(abs_path):
324
- missing_file_paths.append(file_path)
325
- elif os.path.getsize(abs_path) > max_file_size:
326
- rejected_file_paths.append(file_path)
327
-
328
- if missing_file_paths:
329
- not_found_file_warning = f"[{ColorScheme.WARNING.value}][WARNING] The following files do not exist and cannot be added: {', '.join(missing_file_paths)}[{ColorScheme.WARNING.value}]"
330
- print(not_found_file_warning)
331
-
332
- if rejected_file_paths:
333
- size_kb = max_file_size // 1024
334
- wrong_size_warning = f"[{ColorScheme.WARNING.value}][WARNING] The following files exceed {size_kb}KB and cannot be added: {', '.join(rejected_file_paths)}[{ColorScheme.WARNING.value}]"
335
- print(wrong_size_warning)
336
-
337
- if rejected_file_paths or missing_file_paths:
338
- return False
339
-
340
- for file_path in files:
341
- if file_path not in staged_files:
342
- self.git_add_by_path(repo_path=repo_path, file_path=file_path)
343
-
344
- return True
345
-
346
- def add_all_files(
347
- self,
348
- repo_path: str,
349
- max_file_size: int = 500 * 1024, # default 500KB
350
- ) -> bool:
351
- repo = self.__get_repo(path=repo_path)
352
-
353
- files = [item.a_path for item in repo.index.diff(None)] + repo.untracked_files
354
-
355
- rejected_file_paths = []
356
- deleted_files = [item.a_path for item in repo.index.diff(None) if item.change_type == 'D']
357
-
358
- for file_path in files:
359
- abs_path = os.path.join(repo_path, file_path)
360
- if file_path not in deleted_files and os.path.getsize(abs_path) > max_file_size:
361
- rejected_file_paths.append(file_path)
362
-
363
- if rejected_file_paths:
364
- size_kb = max_file_size // 1024
365
- wrong_size_warning = f"[{ColorScheme.WARNING.value}][WARNING] The following files exceed {size_kb}KB and cannot be added: {', '.join(rejected_file_paths)}[{ColorScheme.WARNING.value}]"
366
- print(wrong_size_warning)
367
- return False
368
-
369
- self.git_add_all(repo_path=repo_path)
370
-
371
- return True
372
-
373
- def init_gitignore(self, path: str):
374
- gitignore_path = self._get_gitignore_path(path=path)
375
- if not gitignore_path.exists():
376
- self.__file_system_service.create_if_not_exists_file(gitignore_path)
377
- self.git_add_by_path(repo_path=path, file_path=str(gitignore_path))
378
-
379
- is_present_tsr = self.__file_system_service.find_line_in_text_file(file=str(gitignore_path),
380
- find=self.__git_ignore_thestage_line)
381
- if not is_present_tsr:
382
- self.__file_system_service.add_line_to_text_file(file=str(gitignore_path),
383
- new_line=self.__git_ignore_thestage_line)
384
-
385
- def is_head_detached(self, path: str) -> bool:
386
- repo = self.__get_repo(path=path)
387
- if repo:
388
- return repo.head.is_detached
389
-
390
- def reset_hard(self, path: str, deploy_key_path: str, reset_to_origin: bool):
391
- repo = self.__get_repo(path=path)
392
- if repo:
393
- git_ssh_cmd = 'ssh -F /dev/null -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -i %s' % deploy_key_path
394
- with repo.git.custom_environment(GIT_SSH_COMMAND=git_ssh_cmd):
395
- if reset_to_origin:
396
- repo.git.reset('--hard', f'origin/{repo.active_branch.name}')
397
- typer.echo(f'Branch "{repo.active_branch.name}" is now synced to its remote counterpart')
398
- else:
399
- typer.echo('simple branch reset is not implemented')
400
-
401
- # refers to a "headless commit" where something was committed while in detached head state and head is pointing at that commit
402
- def is_head_committed_in_headless_state(self, path: str) -> bool:
403
- repo = self.__get_repo(path=path)
404
- if repo:
405
- commit = repo.head.commit
406
- for branch in repo.heads:
407
- for commit_item in repo.iter_commits(branch):
408
- if commit_item.hexsha == commit.hexsha:
409
- return False
410
- return True
411
-
412
- def is_branch_exists(self, path: str, branch_name: str) -> bool:
413
- repo = self.__get_repo(path=path)
414
- if repo:
415
- for branch in repo.heads:
416
- if branch.name == branch_name:
417
- return True
418
-
419
- for ref in repo.remotes.origin.refs:
420
- if ref.remote_head == branch_name:
421
- typer.echo(f'Found remote branch "{branch_name}"')
422
- return True
423
- return False
424
-
425
- def checkout_to_new_branch(self, path: str, branch_name: str):
426
- repo = self.__get_repo(path=path)
427
- if repo:
428
- repo.git.checkout("-b", branch_name)
429
-
430
-
431
- def is_commit_exists(repo, commit_sha) -> bool:
432
- try:
433
- repo.commit(commit_sha)
434
- return True
435
- except ValueError:
436
- return False
1
+ import datetime
2
+
3
+ import os
4
+
5
+ from pathlib import Path
6
+ from typing import Optional, List
7
+
8
+ import git
9
+ import typer
10
+ from git import Remote, Repo, GitCommandError, Commit
11
+ from gitdb.exc import BadName
12
+ from rich import print
13
+
14
+ from thestage.color_scheme.color_scheme import ColorScheme
15
+ from thestage.config import THESTAGE_CONFIG_DIR
16
+ from thestage.exceptions.git_access_exception import GitAccessException
17
+ from thestage.git.ProgressPrinter import ProgressPrinter
18
+ from thestage.services.filesystem_service import FileSystemService
19
+
20
+
21
+ class GitLocalClient:
22
+ __base_name_remote: str = 'origin'
23
+ __base_name_local: str = 'main'
24
+ __git_ignore_thestage_line: str = f'/{THESTAGE_CONFIG_DIR}/'
25
+
26
+ __special_main_branches = ['main', 'master']
27
+
28
+ __base_git_url: str = 'https://github.com/'
29
+
30
+ def __init__(
31
+ self,
32
+ file_system_service: FileSystemService,
33
+ ):
34
+ self.__file_system_service = file_system_service
35
+
36
+ # todo delete this fuckery
37
+ def __get_repo(self, path: str) -> Repo:
38
+ return git.Repo(path)
39
+
40
+ def is_present_local_git(self, path: str) -> bool:
41
+ git_path = self.__file_system_service.get_path(path)
42
+ if not git_path.exists():
43
+ return False
44
+
45
+ git_path = git_path.joinpath('.git')
46
+ if not git_path.exists():
47
+ return False
48
+
49
+ result = git.repo.base.is_git_dir(git_path)
50
+ return result
51
+
52
+ def get_remote(self, path: str) -> Optional[List[Remote]]:
53
+ is_git_repo = self.is_present_local_git(path=path)
54
+ if is_git_repo:
55
+ repo = git.Repo(path)
56
+ remotes: Optional[List[Remote]] = list(repo.remotes) if repo.remotes else []
57
+ return remotes
58
+ return None
59
+
60
+ def has_remote(self, path: str) -> bool:
61
+ remotes: Optional[List[Remote]] = self.get_remote(path)
62
+ return True if remotes is not None and len(remotes) > 0 else False
63
+
64
+ def has_changes_with_untracked(self, path: str) -> bool:
65
+ repo = self.__get_repo(path=path)
66
+ return repo.is_dirty(untracked_files=True)
67
+
68
+ def init_repository(
69
+ self,
70
+ path: str,
71
+ ) -> Optional[Repo]:
72
+
73
+ repo = git.Repo.init(path)
74
+ if repo:
75
+ # default git name master, rename to main - sync wih github
76
+ repo.git.branch("-M", self.__base_name_local)
77
+ return repo
78
+
79
+ def add_remote_to_repo(
80
+ self,
81
+ path: str,
82
+ remote_url: str,
83
+ remote_name: str,
84
+ ) -> bool:
85
+ repo = self.__get_repo(path=path)
86
+ remotes: List[Remote] = repo.remotes
87
+ not_present = True
88
+ if remotes:
89
+ item = list(filter(lambda x: x.name == remote_name, remotes))
90
+ if len(item) > 0:
91
+ not_present = False
92
+
93
+ if not_present:
94
+ remote: Remote = repo.create_remote(
95
+ name=self.__base_name_remote,
96
+ url=remote_url,
97
+ )
98
+ if remote:
99
+ return True
100
+ else:
101
+ return False
102
+ else:
103
+ return True
104
+
105
+ def git_fetch(self, path: str, deploy_key_path: str):
106
+ repo = self.__get_repo(path=path)
107
+ git_ssh_cmd = 'ssh -F /dev/null -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -i %s' % deploy_key_path
108
+
109
+ with repo.git.custom_environment(GIT_SSH_COMMAND=git_ssh_cmd):
110
+ remote: Remote = repo.remote(self.__base_name_remote)
111
+ if remote:
112
+ progress = ProgressPrinter()
113
+ try:
114
+ remote.fetch(progress=progress)
115
+ except GitCommandError as ex:
116
+ for line in progress.allDroppedLines():
117
+ # returning the whole output if failed - so that user have any idea what's going on
118
+ print(f'>> {line}')
119
+ raise ex
120
+
121
+
122
+ def git_pull(self, path: str, deploy_key_path: str):
123
+ repo = self.__get_repo(path=path)
124
+ git_ssh_cmd = 'ssh -F /dev/null -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -i %s' % deploy_key_path
125
+
126
+ with repo.git.custom_environment(GIT_SSH_COMMAND=git_ssh_cmd):
127
+ local_branch = self.__base_name_local
128
+ if repo.active_branch.name:
129
+ local_branch = repo.active_branch.name
130
+
131
+ origin = repo.remote(self.__base_name_remote)
132
+
133
+ if origin:
134
+ progress = ProgressPrinter()
135
+ try:
136
+ origin.pull(refspec=local_branch, progress=progress)
137
+ typer.echo(f"Pulled remote changes to branch '{local_branch}'")
138
+ except GitCommandError as ex:
139
+ for line in progress.allDroppedLines():
140
+ # returning the whole output if failed - so that user have any idea what's going on
141
+ print(f'>> {line}')
142
+ raise ex
143
+
144
+ def find_main_branch_name(self, path: str) -> Optional[str]:
145
+ repo = self.__get_repo(path=path)
146
+ if repo:
147
+ for branch in [head.name for head in repo.heads]:
148
+ if branch in self.__special_main_branches:
149
+ return branch
150
+ return None
151
+
152
+ def get_active_branch_name(self, path: str) -> Optional[str]:
153
+ repo = self.__get_repo(path=path)
154
+ if repo:
155
+ if repo.head.is_detached:
156
+ return None
157
+ return repo.active_branch.name
158
+ return None
159
+
160
+ def git_checkout_to_branch(self, path: str, branch: str):
161
+ repo = self.__get_repo(path=path)
162
+ if repo:
163
+ repo.git.checkout(branch.strip())
164
+
165
+ def git_checkout_to_commit(self, path: str, commit_hash: str = None) -> bool:
166
+ repo = self.__get_repo(path=path)
167
+ if repo:
168
+ if is_commit_exists(repo, commit_hash):
169
+ repo.git.checkout(commit_hash.strip())
170
+ return True
171
+ else:
172
+ typer.echo(f"Could not checkout to commit {commit_hash} - reference not found in repository")
173
+ return False
174
+
175
+ def build_http_repo_url(self, git_path: str) -> str:
176
+ start_path_pos = git_path.find(":")
177
+ pre_url = git_path[start_path_pos + 1:]
178
+ url = pre_url.replace('.git', '')
179
+ return self.__base_git_url + url
180
+
181
+ def clone(self, url: str, path: str, deploy_key_path: str) -> Optional[Repo]:
182
+ try:
183
+ git_ssh_cmd = 'ssh -F /dev/null -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -i %s' % deploy_key_path
184
+ return Repo.clone_from(url=url, to_path=path, env={"GIT_SSH_COMMAND": git_ssh_cmd})
185
+ except GitCommandError as base_ex:
186
+ msg = base_ex.stderr
187
+ if msg and 'Repository not found' in msg and 'correct access rights' in msg:
188
+ raise GitAccessException(
189
+ message='You dont have access to repository, or repository not found.',
190
+ url=self.build_http_repo_url(git_path=url),
191
+ dop_message=msg,
192
+ )
193
+ else:
194
+ raise base_ex
195
+
196
+ def commit_local_changes(
197
+ self,
198
+ path: str,
199
+ name: Optional[str] = None,
200
+ ) -> Optional[str]:
201
+ repo = self.__get_repo(path=path)
202
+ if repo.head.is_detached:
203
+ line_color = ColorScheme.GIT_HEADLESS
204
+ print(f'[{line_color}]Committing in detached head state at {repo.head.commit.hexsha}[/{line_color}]')
205
+ commit_name = name if name else f"Auto commit {str(datetime.datetime.now().date())}"
206
+ commit = repo.git.commit('--allow-empty', '-m', commit_name, )
207
+ return commit
208
+
209
+ def push_changes(
210
+ self,
211
+ path: str,
212
+ deploy_key_path: str
213
+ ):
214
+ repo = self.__get_repo(path=path)
215
+ git_ssh_cmd = 'ssh -F /dev/null -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -i %s' % deploy_key_path
216
+
217
+ with repo.git.custom_environment(GIT_SSH_COMMAND=git_ssh_cmd):
218
+ origin = repo.remote(self.__base_name_remote)
219
+ if origin:
220
+ progress = ProgressPrinter()
221
+ # repo.git.push(origin.name, repo.active_branch.name)
222
+ try:
223
+ origin.push(refspec=repo.active_branch.name, progress=progress).raise_if_error()
224
+ except GitCommandError as ex:
225
+ for line in progress.allDroppedLines():
226
+ # returning the whole output if failed - so that user have any idea what's going on
227
+ print(f'>> {line}')
228
+ raise ex
229
+
230
+ def get_current_commit(self, path: str,) -> Optional[Commit]:
231
+ repo = self.__get_repo(path=path)
232
+ if repo:
233
+ return repo.head.commit
234
+ else:
235
+ return None
236
+
237
+ def get_commit_by_hash(self, path: str, commit_hash: str) -> Optional[Commit]:
238
+ repo = self.__get_repo(path=path)
239
+ if repo:
240
+ try:
241
+ return repo.commit(commit_hash)
242
+ except BadName as ex:
243
+ return None
244
+ else:
245
+ return None
246
+
247
+ def _get_gitignore_path(self, path: str) -> Path:
248
+ git_path = self.__file_system_service.get_path(path)
249
+ return git_path.joinpath('.gitignore')
250
+
251
+ def git_add_by_path(self, repo_path: str, file_path: str):
252
+ repo = self.__get_repo(path=repo_path)
253
+ if repo:
254
+ abs_path = os.path.join(repo_path, file_path)
255
+ if os.path.isfile(abs_path):
256
+ repo.index.add([file_path])
257
+ else:
258
+ repo.index.remove([file_path])
259
+
260
+ def git_add_all(self, repo_path: str):
261
+ repo = self.__get_repo(path=repo_path)
262
+ if repo:
263
+ repo.git.add(all=True)
264
+
265
+ def git_diff_stat(self, repo_path: str) -> str:
266
+ repo = self.__get_repo(path=repo_path)
267
+ if repo:
268
+ try:
269
+ diff: str = repo.git.diff("--cached", "--stat")
270
+ if diff:
271
+ return diff.splitlines()[-1]
272
+ else:
273
+ return None
274
+ except ValueError as e:
275
+ return str(e)
276
+
277
+ def add_files_with_size_limit_or_warn(
278
+ self,
279
+ repo_path: str,
280
+ files_to_add: Optional[str] = None,
281
+ max_file_size: int = 500 * 1024, # default 500KB
282
+ ) -> bool:
283
+ """
284
+ Adds to git only files <= max_file_size.
285
+ If files_to_add is not provided, adds all changed/new files except those that are too large.
286
+ Returns True if files were added, False if operation was aborted due to large files.
287
+ """
288
+ if files_to_add:
289
+ return self.add_files_by_path(repo_path=repo_path, files_to_add=files_to_add, max_file_size=max_file_size)
290
+
291
+ return self.add_all_files(repo_path=repo_path, max_file_size=max_file_size)
292
+
293
+
294
+ def add_files_by_path(
295
+ self,
296
+ repo_path: str,
297
+ files_to_add: str,
298
+ max_file_size: int = 500 * 1024, # default 500KB
299
+ ) -> bool:
300
+ repo = self.__get_repo(path=repo_path)
301
+
302
+ if files_to_add.strip().endswith(","):
303
+ space_warning = f"[{ColorScheme.WARNING.value}][WARNING] Use only commas to separate files, without spaces[/{ColorScheme.WARNING.value}]"
304
+ print(space_warning)
305
+ return False
306
+
307
+ files = [f.strip() for f in files_to_add.split(",") if f.strip()]
308
+
309
+ rejected_file_paths = []
310
+ missing_file_paths = []
311
+
312
+ if repo.head.is_valid():
313
+ staged_files = [item.a_path for item in repo.index.diff('HEAD')]
314
+ else:
315
+ staged_files = []
316
+
317
+ deleted_files = [item.a_path for item in repo.index.diff(None) if item.change_type == 'D']
318
+
319
+ for file_path in files:
320
+ if file_path not in deleted_files and file_path not in staged_files:
321
+ abs_path = os.path.join(repo_path, file_path)
322
+
323
+ if not os.path.isfile(abs_path):
324
+ missing_file_paths.append(file_path)
325
+ elif os.path.getsize(abs_path) > max_file_size:
326
+ rejected_file_paths.append(file_path)
327
+
328
+ if missing_file_paths:
329
+ not_found_file_warning = f"[{ColorScheme.WARNING.value}][WARNING] The following files do not exist and cannot be added: {', '.join(missing_file_paths)}[{ColorScheme.WARNING.value}]"
330
+ print(not_found_file_warning)
331
+
332
+ if rejected_file_paths:
333
+ size_kb = max_file_size // 1024
334
+ wrong_size_warning = f"[{ColorScheme.WARNING.value}][WARNING] The following files exceed {size_kb}KB and cannot be added: {', '.join(rejected_file_paths)}[{ColorScheme.WARNING.value}]"
335
+ print(wrong_size_warning)
336
+
337
+ if rejected_file_paths or missing_file_paths:
338
+ return False
339
+
340
+ for file_path in files:
341
+ if file_path not in staged_files:
342
+ self.git_add_by_path(repo_path=repo_path, file_path=file_path)
343
+
344
+ return True
345
+
346
+ def add_all_files(
347
+ self,
348
+ repo_path: str,
349
+ max_file_size: int = 500 * 1024, # default 500KB
350
+ ) -> bool:
351
+ repo = self.__get_repo(path=repo_path)
352
+
353
+ files = [item.a_path for item in repo.index.diff(None)] + repo.untracked_files
354
+
355
+ rejected_file_paths = []
356
+ deleted_files = [item.a_path for item in repo.index.diff(None) if item.change_type == 'D']
357
+
358
+ for file_path in files:
359
+ abs_path = os.path.join(repo_path, file_path)
360
+ if file_path not in deleted_files and os.path.getsize(abs_path) > max_file_size:
361
+ rejected_file_paths.append(file_path)
362
+
363
+ if rejected_file_paths:
364
+ size_kb = max_file_size // 1024
365
+ wrong_size_warning = f"[{ColorScheme.WARNING.value}][WARNING] The following files exceed {size_kb}KB and cannot be added: {', '.join(rejected_file_paths)}[{ColorScheme.WARNING.value}]"
366
+ print(wrong_size_warning)
367
+ return False
368
+
369
+ self.git_add_all(repo_path=repo_path)
370
+
371
+ return True
372
+
373
+ def init_gitignore(self, path: str):
374
+ gitignore_path = self._get_gitignore_path(path=path)
375
+ if not gitignore_path.exists():
376
+ self.__file_system_service.create_if_not_exists_file(gitignore_path)
377
+ self.git_add_by_path(repo_path=path, file_path=str(gitignore_path))
378
+
379
+ is_present_tsr = self.__file_system_service.find_line_in_text_file(file=str(gitignore_path),
380
+ find=self.__git_ignore_thestage_line)
381
+ if not is_present_tsr:
382
+ self.__file_system_service.add_line_to_text_file(file=str(gitignore_path),
383
+ new_line=self.__git_ignore_thestage_line)
384
+
385
+ def is_head_detached(self, path: str) -> bool:
386
+ repo = self.__get_repo(path=path)
387
+ if repo:
388
+ return repo.head.is_detached
389
+
390
+ def reset_hard(self, path: str, deploy_key_path: str, reset_to_origin: bool):
391
+ repo = self.__get_repo(path=path)
392
+ if repo:
393
+ git_ssh_cmd = 'ssh -F /dev/null -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -i %s' % deploy_key_path
394
+ with repo.git.custom_environment(GIT_SSH_COMMAND=git_ssh_cmd):
395
+ if reset_to_origin:
396
+ repo.git.reset('--hard', f'origin/{repo.active_branch.name}')
397
+ typer.echo(f'Branch "{repo.active_branch.name}" is now synced to its remote counterpart')
398
+ else:
399
+ typer.echo('simple branch reset is not implemented')
400
+
401
+ # refers to a "headless commit" where something was committed while in detached head state and head is pointing at that commit
402
+ def is_head_committed_in_headless_state(self, path: str) -> bool:
403
+ repo = self.__get_repo(path=path)
404
+ if repo:
405
+ commit = repo.head.commit
406
+ for branch in repo.heads:
407
+ for commit_item in repo.iter_commits(branch):
408
+ if commit_item.hexsha == commit.hexsha:
409
+ return False
410
+ return True
411
+
412
+ def is_branch_exists(self, path: str, branch_name: str) -> bool:
413
+ repo = self.__get_repo(path=path)
414
+ if repo:
415
+ for branch in repo.heads:
416
+ if branch.name == branch_name:
417
+ return True
418
+
419
+ for ref in repo.remotes.origin.refs:
420
+ if ref.remote_head == branch_name:
421
+ typer.echo(f'Found remote branch "{branch_name}"')
422
+ return True
423
+ return False
424
+
425
+ def checkout_to_new_branch(self, path: str, branch_name: str):
426
+ repo = self.__get_repo(path=path)
427
+ if repo:
428
+ repo.git.checkout("-b", branch_name)
429
+
430
+
431
+ def is_commit_exists(repo, commit_sha) -> bool:
432
+ try:
433
+ repo.commit(commit_sha)
434
+ return True
435
+ except ValueError:
436
+ return False