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