qubership-pipelines-common-library 0.1.4__tar.gz → 0.1.6__tar.gz

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 (29) hide show
  1. {qubership_pipelines_common_library-0.1.4 → qubership_pipelines_common_library-0.1.6}/PKG-INFO +8 -1
  2. {qubership_pipelines_common_library-0.1.4 → qubership_pipelines_common_library-0.1.6}/README.md +8 -0
  3. {qubership_pipelines_common_library-0.1.4 → qubership_pipelines_common_library-0.1.6}/pyproject.toml +1 -1
  4. {qubership_pipelines_common_library-0.1.4 → qubership_pipelines_common_library-0.1.6}/qubership_pipelines_common_library/v1/git_client.py +2 -1
  5. {qubership_pipelines_common_library-0.1.4 → qubership_pipelines_common_library-0.1.6}/qubership_pipelines_common_library/v1/github_client.py +95 -16
  6. {qubership_pipelines_common_library-0.1.4 → qubership_pipelines_common_library-0.1.6}/qubership_pipelines_common_library/v1/utils/utils_string.py +5 -1
  7. {qubership_pipelines_common_library-0.1.4 → qubership_pipelines_common_library-0.1.6}/LICENSE +0 -0
  8. {qubership_pipelines_common_library-0.1.4 → qubership_pipelines_common_library-0.1.6}/qubership_pipelines_common_library/__init__.py +0 -0
  9. {qubership_pipelines_common_library-0.1.4 → qubership_pipelines_common_library-0.1.6}/qubership_pipelines_common_library/v1/__init__.py +0 -0
  10. {qubership_pipelines_common_library-0.1.4 → qubership_pipelines_common_library-0.1.6}/qubership_pipelines_common_library/v1/artifactory_client.py +0 -0
  11. {qubership_pipelines_common_library-0.1.4 → qubership_pipelines_common_library-0.1.6}/qubership_pipelines_common_library/v1/execution/__init__.py +0 -0
  12. {qubership_pipelines_common_library-0.1.4 → qubership_pipelines_common_library-0.1.6}/qubership_pipelines_common_library/v1/execution/exec_command.py +0 -0
  13. {qubership_pipelines_common_library-0.1.4 → qubership_pipelines_common_library-0.1.6}/qubership_pipelines_common_library/v1/execution/exec_context.py +0 -0
  14. {qubership_pipelines_common_library-0.1.4 → qubership_pipelines_common_library-0.1.6}/qubership_pipelines_common_library/v1/execution/exec_context_file.py +0 -0
  15. {qubership_pipelines_common_library-0.1.4 → qubership_pipelines_common_library-0.1.6}/qubership_pipelines_common_library/v1/execution/exec_info.py +0 -0
  16. {qubership_pipelines_common_library-0.1.4 → qubership_pipelines_common_library-0.1.6}/qubership_pipelines_common_library/v1/execution/exec_logger.py +0 -0
  17. {qubership_pipelines_common_library-0.1.4 → qubership_pipelines_common_library-0.1.6}/qubership_pipelines_common_library/v1/gitlab_client.py +0 -0
  18. {qubership_pipelines_common_library-0.1.4 → qubership_pipelines_common_library-0.1.6}/qubership_pipelines_common_library/v1/jenkins_client.py +0 -0
  19. {qubership_pipelines_common_library-0.1.4 → qubership_pipelines_common_library-0.1.6}/qubership_pipelines_common_library/v1/kube_client.py +0 -0
  20. {qubership_pipelines_common_library-0.1.4 → qubership_pipelines_common_library-0.1.6}/qubership_pipelines_common_library/v1/log_client.py +0 -0
  21. {qubership_pipelines_common_library-0.1.4 → qubership_pipelines_common_library-0.1.6}/qubership_pipelines_common_library/v1/minio_client.py +0 -0
  22. {qubership_pipelines_common_library-0.1.4 → qubership_pipelines_common_library-0.1.6}/qubership_pipelines_common_library/v1/utils/__init__.py +0 -0
  23. {qubership_pipelines_common_library-0.1.4 → qubership_pipelines_common_library-0.1.6}/qubership_pipelines_common_library/v1/utils/rest.py +0 -0
  24. {qubership_pipelines_common_library-0.1.4 → qubership_pipelines_common_library-0.1.6}/qubership_pipelines_common_library/v1/utils/utils.py +0 -0
  25. {qubership_pipelines_common_library-0.1.4 → qubership_pipelines_common_library-0.1.6}/qubership_pipelines_common_library/v1/utils/utils_context.py +0 -0
  26. {qubership_pipelines_common_library-0.1.4 → qubership_pipelines_common_library-0.1.6}/qubership_pipelines_common_library/v1/utils/utils_dictionary.py +0 -0
  27. {qubership_pipelines_common_library-0.1.4 → qubership_pipelines_common_library-0.1.6}/qubership_pipelines_common_library/v1/utils/utils_file.py +0 -0
  28. {qubership_pipelines_common_library-0.1.4 → qubership_pipelines_common_library-0.1.6}/qubership_pipelines_common_library/v1/utils/utils_json.py +0 -0
  29. {qubership_pipelines_common_library-0.1.4 → qubership_pipelines_common_library-0.1.6}/qubership_pipelines_common_library/v1/webex_client.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: qubership-pipelines-common-library
3
- Version: 0.1.4
3
+ Version: 0.1.6
4
4
  Summary: Qubership Pipelines common library
5
5
  Author: Igor Lebedev
6
6
  Author-email: lebedev.light@gmail.com
@@ -34,6 +34,8 @@ Library provides easy-to-use clients and wrappers for common devops services (e.
34
34
 
35
35
  Library is presented as a set of clients with predefined operations
36
36
 
37
+ Auto-generated reference (via mkdocs) is available on [this repo's GitHub Pages](https://netcracker.github.io/qubership-pipelines-common-python-library)
38
+
37
39
  ## Installation
38
40
 
39
41
  - Add the following section to your dependencies to add Qubership library as a dependency in your project:
@@ -58,3 +60,8 @@ You can install it via `pip`:
58
60
  pip install qubership-pipelines-common-library-py39
59
61
  ```
60
62
 
63
+ ## Sample implementation
64
+
65
+ Sample implementation of CLI commands using this library is available at [Quber CLI](https://github.com/LightlessOne/Quber-CLI?tab=readme-ov-file)
66
+
67
+ It includes reference python implementation along with [Development Guide](https://github.com/LightlessOne/Quber-CLI/blob/master/docs/development.md)
@@ -8,6 +8,8 @@ Library provides easy-to-use clients and wrappers for common devops services (e.
8
8
 
9
9
  Library is presented as a set of clients with predefined operations
10
10
 
11
+ Auto-generated reference (via mkdocs) is available on [this repo's GitHub Pages](https://netcracker.github.io/qubership-pipelines-common-python-library)
12
+
11
13
  ## Installation
12
14
 
13
15
  - Add the following section to your dependencies to add Qubership library as a dependency in your project:
@@ -31,3 +33,9 @@ You can install it via `pip`:
31
33
  ```bash
32
34
  pip install qubership-pipelines-common-library-py39
33
35
  ```
36
+
37
+ ## Sample implementation
38
+
39
+ Sample implementation of CLI commands using this library is available at [Quber CLI](https://github.com/LightlessOne/Quber-CLI?tab=readme-ov-file)
40
+
41
+ It includes reference python implementation along with [Development Guide](https://github.com/LightlessOne/Quber-CLI/blob/master/docs/development.md)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "qubership-pipelines-common-library"
3
- version = "0.1.4"
3
+ version = "0.1.6"
4
4
  description = "Qubership Pipelines common library"
5
5
  authors = ["Igor Lebedev <lebedev.light@gmail.com>"]
6
6
  readme = "README.md"
@@ -39,7 +39,7 @@ class GitClient:
39
39
  self.branch = None # last processed branch
40
40
  logging.info("Git Client configured for %s", self.host)
41
41
 
42
- def clone(self, repo_path: str, branch: str, temp_path: str):
42
+ def clone(self, repo_path: str, branch: str, temp_path: str, **kwargs):
43
43
  """"""
44
44
  repo_path = repo_path.lstrip("/").rstrip("/")
45
45
  if not repo_path:
@@ -56,6 +56,7 @@ class GitClient:
56
56
  self._gen_repo_auth_url(self.host, self.username, self.password, self.repo_path),
57
57
  temp_path,
58
58
  branch=branch,
59
+ **kwargs
59
60
  )
60
61
 
61
62
  def commit_and_push(self, commit_message: str):
@@ -12,13 +12,21 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
+ import json
15
16
  import logging
17
+ import tempfile
18
+ import uuid
19
+ import zipfile
16
20
  import requests
17
21
 
18
22
  from datetime import datetime, timezone
19
23
  from pathlib import Path
20
24
  from github import Github, Auth
21
25
  from time import sleep
26
+ from github.Artifact import Artifact
27
+ from github.PaginatedList import PaginatedList
28
+ from github.WorkflowRun import WorkflowRun
29
+
22
30
  from qubership_pipelines_common_library.v1.execution.exec_info import ExecutionInfo
23
31
 
24
32
 
@@ -39,6 +47,10 @@ class GithubClient:
39
47
  CONCLUSION_ACTION_REQUIRED = "action_required"
40
48
 
41
49
  BREAK_STATUS_LIST = [STATUS_COMPLETED, STATUS_FAILURE]
50
+ DISPATCH_PARAMS_LIMIT = 10
51
+ DEFAULT_UUID_ARTIFACT_NAME = "input_params"
52
+ DEFAULT_UUID_FILE_NAME = "input_params.json"
53
+ DEFAULT_UUID_PARAM_NAME = "workflow_run_uuid"
42
54
 
43
55
  def __init__(self, token: str = None, api_url: str = None, **kwargs):
44
56
  """
@@ -55,9 +67,23 @@ class GithubClient:
55
67
  logging.info("Github Client configured")
56
68
 
57
69
  def trigger_workflow(self, owner: str, repo_name: str, workflow_file_name: str, branch: str, pipeline_params: dict,
58
- timeout_seconds: float = 30.0, wait_seconds: float = 3.0):
59
- """ There's currently no reliable way to get ID of triggered workflow, dispatch is async and doesn't return anything
60
- As a workaround - we start looking for newly created runs of that workflow, filtering them as much as possible """
70
+ timeout_seconds: float = 30.0, wait_seconds: float = 3.0, find_via_uuid: bool = False,
71
+ uuid_param_name: str = DEFAULT_UUID_PARAM_NAME,
72
+ uuid_artifact_name: str = DEFAULT_UUID_ARTIFACT_NAME, uuid_file_name: str = DEFAULT_UUID_FILE_NAME):
73
+ """ There's currently no reliable way to get ID of triggered workflow, without adding explicit ID as an input parameter to each workflow, dispatch is async and doesn't return anything
74
+ This method supports two different ways to find and return started workflow:
75
+ Unreliable - where we start looking for newly created runs of that workflow, filtering them as much as possible (might return wrong run in a concurrent scenario)
76
+ Reliable:
77
+ you need to add specific explicit ID param to the workflow you are triggering (e.g. 'workflow_run_uuid'),
78
+ said workflow should have a step where it will save its input params,
79
+ and then you run this method with 'find_via_uuid = True'
80
+ """
81
+ if pipeline_params is None:
82
+ pipeline_params = {}
83
+ if find_via_uuid:
84
+ pipeline_params[uuid_param_name] = str(uuid.uuid4())
85
+ if len(pipeline_params) > GithubClient.DISPATCH_PARAMS_LIMIT:
86
+ logging.warning(f"Trying to dispatch workflow with more than {GithubClient.DISPATCH_PARAMS_LIMIT} pipeline_params, GitHub does not support it!")
61
87
  workflow = self.gh.get_repo(f"{owner}/{repo_name}", lazy=True).get_workflow(workflow_file_name)
62
88
  dispatch_time = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
63
89
  execution = ExecutionInfo()
@@ -65,14 +91,22 @@ class GithubClient:
65
91
  logging.info(f"Workflow Dispatch event for {workflow_file_name} is sent, workflow is created: {is_created}")
66
92
  if is_created:
67
93
  current_timeout = 0
94
+ already_checked_runs = []
68
95
  while current_timeout < timeout_seconds:
69
96
  runs_list = workflow.get_runs(event="workflow_dispatch", created=f">={dispatch_time}", branch=branch)
70
97
  if runs_list.totalCount > 0:
71
- created_run = runs_list.get_page(0).pop()
72
- logging.info(f"Pipeline successfully started at {created_run.html_url}")
73
- return execution.with_name(created_run.name).with_id(created_run.id) \
74
- .with_url(created_run.html_url).with_params(pipeline_params) \
75
- .start()
98
+ if find_via_uuid:
99
+ created_run = self._find_run_via_uuid_input_param(runs_list, already_checked_runs,
100
+ uuid_artifact_name, uuid_file_name,
101
+ uuid_param_name,
102
+ pipeline_params[uuid_param_name])
103
+ else:
104
+ created_run = runs_list.get_page(0).pop()
105
+ if created_run:
106
+ logging.info(f"Pipeline successfully started at {created_run.html_url}")
107
+ return execution.with_name(created_run.name).with_id(created_run.id) \
108
+ .with_url(created_run.html_url).with_params(pipeline_params) \
109
+ .start()
76
110
  current_timeout += wait_seconds
77
111
  logging.info(f"Waiting for triggered workflow run to start... Timeout {wait_seconds} seconds")
78
112
  sleep(wait_seconds)
@@ -143,15 +177,60 @@ class GithubClient:
143
177
  local_dir_path.mkdir(parents=True, exist_ok=True)
144
178
  run = self.gh.get_repo(repo_full_name).get_workflow_run(int(execution.get_id()))
145
179
  for artifact in run.get_artifacts():
146
- local_path = Path(local_dir_path, f"{artifact.name}.zip")
147
- (status, headers, _) = artifact.requester.requestBlob("GET", artifact.archive_download_url)
148
- if status != 302:
149
- logging.error(f"Unexpected status while downloading run artifact {artifact.name}: expected 302, got {status}")
180
+ self._save_artifact_to_dir(artifact, local_dir_path)
181
+
182
+ def get_workflow_run_input_params(self, run: WorkflowRun, artifact_name: str = DEFAULT_UUID_ARTIFACT_NAME,
183
+ file_name: str = DEFAULT_UUID_FILE_NAME):
184
+ """"""
185
+ for artifact in run.get_artifacts():
186
+ if artifact.name == artifact_name:
187
+ return self._get_input_params_from_artifact(artifact, file_name)
188
+ logging.info(f"Could not find input_params artifact for run {run.id}")
189
+ return {}
190
+
191
+ def _find_run_via_uuid_input_param(self, runs_list: PaginatedList[WorkflowRun], already_checked_runs: list,
192
+ uuid_artifact_name: str, uuid_file_name: str,
193
+ uuid_param_name: str, uuid_param_value: str):
194
+ for run in runs_list:
195
+ if run.id in already_checked_runs:
150
196
  continue
151
- response = requests.get(headers["location"])
152
- with local_path.open('wb') as f:
153
- logging.info(f"saving {local_path}...")
154
- f.write(response.content)
197
+ for artifact in run.get_artifacts():
198
+ if artifact.name == uuid_artifact_name:
199
+ if self._check_input_params_uuid(artifact, uuid_file_name, uuid_param_name, uuid_param_value):
200
+ logging.info(f"Found workflow run with expected UUID: {run.id} with {uuid_param_name}={uuid_param_value}")
201
+ return run
202
+ else:
203
+ already_checked_runs.append(run.id)
204
+ break
205
+ return None
206
+
207
+ def _check_input_params_uuid(self, artifact: Artifact, uuid_file_name: str, uuid_param_name: str, uuid_param_value: str):
208
+ try:
209
+ input_params = self._get_input_params_from_artifact(artifact, uuid_file_name)
210
+ return input_params.get(uuid_param_name) == uuid_param_value
211
+ except Exception as ex:
212
+ logging.error(f"Exception when downloading and checking artifact ({artifact.name}): {ex}")
213
+ return False
214
+
215
+ def _get_input_params_from_artifact(self, artifact: Artifact, file_name: str):
216
+ with tempfile.TemporaryDirectory() as temp_dirname:
217
+ artifact_path = self._save_artifact_to_dir(artifact, temp_dirname)
218
+ with zipfile.ZipFile(artifact_path) as zf:
219
+ zf.extractall(temp_dirname)
220
+ with open(Path(temp_dirname, file_name)) as input_params_file:
221
+ return json.load(input_params_file)
222
+
223
+ def _save_artifact_to_dir(self, artifact: Artifact, dirname):
224
+ local_path = Path(dirname, f"{artifact.name}.zip")
225
+ (status, headers, _) = artifact.requester.requestBlob("GET", artifact.archive_download_url)
226
+ if status != 302:
227
+ logging.error(f"Unexpected status while downloading run artifact {artifact.name}: expected 302, got {status}")
228
+ return None
229
+ response = requests.get(headers["location"])
230
+ with local_path.open('wb') as f:
231
+ logging.info(f"saving {local_path}...")
232
+ f.write(response.content)
233
+ return local_path
155
234
 
156
235
  def _map_status_and_conclusion(self, status: str, conclusion: str, default_status: str):
157
236
  logging.info(f"status: {status}, conclusion: {conclusion}")
@@ -21,4 +21,8 @@ class UtilsString:
21
21
  # split by newline, comma, or semicolon
22
22
  parts = re.split(r'[\n,;]', input_str)
23
23
  # trim whitespace from each part
24
- return [part.strip() for part in parts if part.strip()]
24
+ return [part.strip() for part in parts if part.strip()]
25
+
26
+ @staticmethod
27
+ def convert_to_bool(input_str):
28
+ return str(input_str).strip().lower() == "true"