qubership-pipelines-common-library 2.0.1__py3-none-any.whl → 2.0.3__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.
@@ -1,14 +1,11 @@
1
1
  import logging
2
+ import os
2
3
  import re
3
-
4
+ import sys
4
5
  import click
5
- from rich import box
6
- from rich.logging import RichHandler
7
- from rich.panel import Panel
8
6
 
9
7
  from qubership_pipelines_common_library.v1.execution.exec_logger import ExecutionLogger
10
- from qubership_pipelines_common_library.v1.utils.utils_logging import rich_console, ExtendedReprHighlighter, \
11
- LevelColorFilter
8
+ from qubership_pipelines_common_library.v1.utils.utils_string import UtilsString
12
9
 
13
10
  DEFAULT_CONTEXT_FILE_PATH = 'context.yaml'
14
11
 
@@ -37,26 +34,34 @@ def utils_cli(func):
37
34
 
38
35
  def _configure_global_logger(global_logger: logging.Logger, log_level: str):
39
36
  """Configure the global logger with a specific log level and formatter."""
37
+ log_level_value = getattr(logging, log_level.upper(), logging.INFO)
40
38
  global_logger.setLevel(logging.DEBUG)
41
39
  if global_logger.hasHandlers():
42
40
  global_logger.handlers.clear()
43
41
  global_logger.propagate = True
44
- rich_handler = RichHandler(
45
- console=rich_console,
46
- show_time=False,
47
- show_level=False,
48
- show_path=False,
49
- enable_link_path=False,
50
- rich_tracebacks=True,
51
- tracebacks_show_locals=False,
52
- markup=True,
53
- highlighter=ExtendedReprHighlighter(),
54
- )
55
- rich_handler.addFilter(LevelColorFilter())
56
- rich_handler.setFormatter(logging.Formatter(ExecutionLogger.LEVELNAME_COLORED_FORMAT))
57
- log_level_value = getattr(logging, log_level.upper(), logging.INFO)
58
- rich_handler.setLevel(log_level_value)
59
- global_logger.addHandler(rich_handler)
42
+ if UtilsString.convert_to_bool(os.getenv('NO_RICH', False)):
43
+ stdout_handler = logging.StreamHandler(sys.stdout)
44
+ stdout_handler.setLevel(log_level_value)
45
+ stdout_handler.setFormatter(logging.Formatter(ExecutionLogger.DEFAULT_FORMAT))
46
+ global_logger.addHandler(stdout_handler)
47
+ else:
48
+ from rich.logging import RichHandler
49
+ from qubership_pipelines_common_library.v1.utils.utils_logging import rich_console, ExtendedReprHighlighter, LevelColorFilter
50
+ rich_handler = RichHandler(
51
+ console=rich_console,
52
+ show_time=False,
53
+ show_level=False,
54
+ show_path=False,
55
+ enable_link_path=False,
56
+ rich_tracebacks=True,
57
+ tracebacks_show_locals=False,
58
+ markup=True,
59
+ highlighter=ExtendedReprHighlighter(),
60
+ )
61
+ rich_handler.addFilter(LevelColorFilter())
62
+ rich_handler.setFormatter(logging.Formatter(ExecutionLogger.LEVELNAME_COLORED_FORMAT))
63
+ rich_handler.setLevel(log_level_value)
64
+ global_logger.addHandler(rich_handler)
60
65
 
61
66
 
62
67
  def _print_command_name():
@@ -67,10 +72,16 @@ def _print_command_name():
67
72
  logging.getLogger().warning("Can't find command name.")
68
73
  command_name = ""
69
74
 
70
- command_panel = Panel(f"command_name = {command_name}", expand=False, padding=(0, 1), box=box.ROUNDED)
71
- rich_console.print()
72
- rich_console.print(command_panel)
73
- rich_console.print()
75
+ if UtilsString.convert_to_bool(os.getenv('NO_RICH', False)):
76
+ logging.info(f"command_name = {command_name}")
77
+ else:
78
+ from rich import box
79
+ from rich.panel import Panel
80
+ from qubership_pipelines_common_library.v1.utils.utils_logging import rich_console
81
+ command_panel = Panel(f"command_name = {command_name}", expand=False, padding=(0, 1), box=box.ROUNDED)
82
+ rich_console.print()
83
+ rich_console.print(command_panel)
84
+ rich_console.print()
74
85
 
75
86
 
76
87
  def _transform_kwargs(kwargs):
@@ -18,7 +18,6 @@ soft_theme = Theme({
18
18
  "repr.str": "rgb(140,180,140)",
19
19
  "repr.tag_name": "rgb(200,170,220)",
20
20
  "repr.tag_value": "rgb(170,200,220)",
21
-
22
21
  "repr.time": "rgb(160,190,220) italic",
23
22
  })
24
23
 
File without changes
@@ -0,0 +1,100 @@
1
+ from qubership_pipelines_common_library.v1.execution.exec_command import ExecutionCommand
2
+ from qubership_pipelines_common_library.v2.jira.jira_client import JiraClient, AuthType
3
+ from qubership_pipelines_common_library.v2.jira.jira_utils import JiraUtils
4
+
5
+
6
+ class JiraAddTicketComment(ExecutionCommand):
7
+ """
8
+ Adds comment to JIRA ticket and retrieves latest comments.
9
+
10
+ Input Parameters Structure (this structure is expected inside "input_params.params" block):
11
+ ```
12
+ {
13
+ "ticket": {
14
+ "id": "BUG-567", # REQUIRED: Ticket ID
15
+ "comment": "your comment body", # REQUIRED: Comment body
16
+ "latest_comments_count": 50, # OPTIONAL: Number of latest comments to fetch
17
+ },
18
+ "retry_timeout_seconds": 180, # OPTIONAL: Timeout for JIRA client operations in seconds (default: 180)
19
+ "retry_wait_seconds": 1, # OPTIONAL: Wait interval between retries in seconds (default: 1)
20
+ }
21
+ ```
22
+
23
+ Systems Configuration (expected in "systems.jira" block):
24
+ ```
25
+ {
26
+ "url": "https://your_cloud_jira.atlassian.net", # REQUIRED: JIRA server URL
27
+ "username": "your_username_or_email", # REQUIRED: JIRA user login or email
28
+ "password": "<your_token>", # REQUIRED: JIRA user token
29
+ "auth_type": "basic" # OPTIONAL: 'basic' or 'bearer'
30
+ }
31
+ ```
32
+
33
+ Command name: "jira-add-ticket-comment"
34
+ """
35
+
36
+ RETRY_TIMEOUT_SECONDS = 180 # default value, how many seconds to try
37
+ RETRY_WAIT_SECONDS = 1 # default value, how many seconds between tries
38
+ LATEST_COMMENTS_COUNT = 50 # default value, max amount of comments in response
39
+
40
+ def _validate(self):
41
+ names = [
42
+ "paths.input.params",
43
+ "paths.output.params",
44
+ "systems.jira.url",
45
+ "systems.jira.username",
46
+ "systems.jira.password",
47
+ "params.ticket.id",
48
+ "params.ticket.comment",
49
+ ]
50
+ if not self.context.validate(names):
51
+ return False
52
+
53
+ self.retry_timeout_seconds = int(self.context.input_param_get("params.retry_timeout_seconds", self.RETRY_TIMEOUT_SECONDS))
54
+ self.retry_wait_seconds = int(self.context.input_param_get("params.retry_wait_seconds", self.RETRY_WAIT_SECONDS))
55
+
56
+ self.jira_url = self.context.input_param_get("systems.jira.url").rstrip('/')
57
+ self.jira_username = self.context.input_param_get("systems.jira.username")
58
+ self.jira_password = self.context.input_param_get("systems.jira.password")
59
+ self.auth_type = self.context.input_param_get("systems.jira.auth_type", AuthType.BASIC)
60
+
61
+ self.ticket_key = self.context.input_param_get("params.ticket.id")
62
+ self.ticket_comment = self.context.input_param_get("params.ticket.comment")
63
+ self.latest_comments_count = int(self.context.input_param_get("params.ticket.latest_comments_count", self.LATEST_COMMENTS_COUNT))
64
+ return True
65
+
66
+ def _execute(self):
67
+ self.context.logger.info("Running jira-add-ticket-comment")
68
+ self.jira_client = JiraClient.create_jira_client(
69
+ self.jira_url, self.jira_username, self.jira_password, self.auth_type,
70
+ self.retry_timeout_seconds, self.retry_wait_seconds,
71
+ )
72
+
73
+ if self.ticket_comment:
74
+ JiraUtils.add_ticket_comment(self)
75
+
76
+ total_comments = 0
77
+ parsed_latest_comments = []
78
+ try:
79
+ self.latest_comments = self.jira_client.get_latest_ticket_comments(self.ticket_key, max_results=self.latest_comments_count)
80
+ total_comments = len(self.latest_comments)
81
+ parsed_latest_comments = [self._parse_comment(comment) for comment in self.latest_comments]
82
+ except Exception as e:
83
+ self.context.logger.warning(f"Can't get latest ticket comments. Response exception: {str(e)}")
84
+
85
+ self.context.output_param_set("params.ticket.id", self.ticket_key)
86
+ self.context.output_param_set("params.ticket.url", f"{self.jira_url}/browse/{self.ticket_key}")
87
+ self.context.output_param_set("params.ticket.total_comments", total_comments)
88
+ self.context.output_param_set("params.ticket.latest_comments", parsed_latest_comments)
89
+ self.context.output_params_save()
90
+ self.context.logger.info("Add ticket comment request executed. See output params for details")
91
+
92
+ @staticmethod
93
+ def _parse_comment(comment):
94
+ return {
95
+ "body": comment.get("body"),
96
+ "created": comment.get("created"),
97
+ "updated": comment.get("updated"),
98
+ "author": JiraClient.serialize_person_ref(comment.get("author", {})),
99
+ "updateAuthor": JiraClient.serialize_person_ref(comment.get("updateAuthor", {})),
100
+ }
@@ -0,0 +1,215 @@
1
+ import os
2
+ import json
3
+ import logging
4
+ import requests
5
+
6
+ from enum import StrEnum
7
+ from typing import Any
8
+ from requests import Response
9
+ from requests.auth import HTTPBasicAuth
10
+ from qubership_pipelines_common_library.v2.utils.retry_decorator import RetryDecorator
11
+
12
+
13
+ class AuthType(StrEnum):
14
+ BASIC = 'basic'
15
+ BEARER = 'bearer'
16
+
17
+
18
+ class JiraClient:
19
+
20
+ DEFAULT_FIELD_NAMES_FILTER = [
21
+ "fixVersions",
22
+ "resolution",
23
+ "priority",
24
+ "labels",
25
+ "versions",
26
+ "assignee",
27
+ "status",
28
+ "components",
29
+ "creator",
30
+ "reporter",
31
+ "issuetype",
32
+ "project",
33
+ "resolutiondate",
34
+ "created",
35
+ "updated",
36
+ "description",
37
+ "summary",
38
+ "customfield_10014", # Found in
39
+ ]
40
+
41
+ API_VERSION = "2"
42
+
43
+ @classmethod
44
+ @RetryDecorator(condition_func=lambda client: client is not None)
45
+ def create_jira_client(cls, host: str, user: str, password: str, auth_type: str,
46
+ retry_timeout_seconds: int = 180, retry_wait_seconds: int = 1):
47
+ return cls(host, user, password, auth_type)
48
+
49
+ def __init__(self, host: str, user: str, password: str, auth_type: str = AuthType.BASIC):
50
+ self.host = host.rstrip("/")
51
+ self.user = user
52
+ self.password = password
53
+ self.session = requests.Session()
54
+ self.session.verify = os.getenv("PYTHONHTTPSVERIFY", "1") != "0"
55
+ if auth_type.lower() == AuthType.BEARER:
56
+ self.session.headers.update({"Authorization": f"Bearer {password}"})
57
+ else:
58
+ self.session.auth = HTTPBasicAuth(user, password)
59
+ self.headers = {"Accept": "application/json", "Content-Type": "application/json"}
60
+ self.session.headers.update(self.headers)
61
+ self.logger = logging.getLogger()
62
+ try:
63
+ self.server_info = self.get_server_info()
64
+ self.deployment_type = self.server_info.get("deploymentType", "Server")
65
+ except Exception as e:
66
+ self.logger.info(f"Could not get Jira instance version, assuming 'Server': {e}")
67
+ self.deployment_type = "Server"
68
+ self.logger.info(f"Jira Client configured for {self.host}, deployment type: {self.deployment_type}")
69
+
70
+ @property
71
+ def _is_cloud(self) -> bool:
72
+ return self.deployment_type == "Cloud"
73
+
74
+ def get_server_info(self) -> dict[str, Any]:
75
+ retry = 0
76
+ j = self._get_json("serverInfo")
77
+ while not j and retry < 3:
78
+ retry += 1
79
+ j = self._get_json("serverInfo")
80
+ return j
81
+
82
+ @RetryDecorator(condition_func=lambda response: response is not None and (response.ok or response.status_code == 400)) # do not retry BadRequest
83
+ def add_ticket_comment(self, ticket_id: str, comment: str, retry_timeout_seconds: int = 180, retry_wait_seconds: int = 1) -> Response:
84
+ response = self.session.post(
85
+ url=f"{self.host}/rest/api/{self.API_VERSION}/issue/{ticket_id}/comment",
86
+ data=json.dumps({"body": comment})
87
+ )
88
+ self.logger.debug(f"Add ticket (id={ticket_id}) comment response: status_code = {response.status_code}, body = {response.text}")
89
+ return response
90
+
91
+ def get_latest_ticket_comments(self, ticket_id: str, max_results: int = 50) -> list:
92
+ response = self.session.get(f"{self.host}/rest/api/{self.API_VERSION}/issue/{ticket_id}/comment?maxResults={max_results}&orderBy=-created")
93
+ self.logger.debug(f"Get ticket (id={ticket_id}) comments response: status_code = {response.status_code}, body = {response.text}")
94
+ response.raise_for_status()
95
+ return response.json().get("comments", [])
96
+
97
+ @RetryDecorator(condition_func=lambda response: response is not None and (response.ok or response.status_code == 400))
98
+ def create_ticket(self, ticket_fields: dict, retry_timeout_seconds: int = 180, retry_wait_seconds: int = 1) -> Response:
99
+ body = {"fields": ticket_fields}
100
+ response = self.session.post(f"{self.host}/rest/api/{self.API_VERSION}/issue", data=json.dumps(body))
101
+ self.logger.debug(f"Create ticket response: status_code = {response.status_code}, body = {response.text}")
102
+ return response
103
+
104
+ def get_createmeta_fields(self, project_key: str, issue_type_name: str) -> dict:
105
+ response = self.session.get(f"{self.host}/rest/api/{self.API_VERSION}/issue/createmeta/{project_key}/issuetypes?maxResults=100")
106
+ self.logger.debug(f"Get createmeta for project (id={project_key}) response: status_code = {response.status_code}, body = {response.text}")
107
+ if not response.ok:
108
+ self.logger.warning(f"Can't get issuetypes by projectKey = {project_key}. Response status = {response.status_code}")
109
+ return {}
110
+
111
+ issue_types = response.json().get("issueTypes" if self._is_cloud else "values", [])
112
+ issue_type_ids = [issue_type.get("id") for issue_type in issue_types if issue_type.get("name") == issue_type_name]
113
+ if not issue_type_ids:
114
+ self.logger.warning(f"Can't find issue type id by issue_type_name = {issue_type_name}")
115
+ return {}
116
+
117
+ response = self.session.get(f"{self.host}/rest/api/{self.API_VERSION}/issue/createmeta/{project_key}/issuetypes/{issue_type_ids[0]}?maxResults=100")
118
+ self.logger.debug(f"Get createmeta for issuetype (id={issue_type_ids[0]}) response: status_code = {response.status_code}, body = {response.text}")
119
+ if not response.ok:
120
+ self.logger.warning(f"Can't get createmeta by projectKey = {project_key} and issue_type_id = {issue_type_ids[0]}. Response status = {response.status_code}")
121
+ return {}
122
+
123
+ fields = response.json().get("fields" if self._is_cloud else "values", [])
124
+ return {field["fieldId"]: field for field in fields}
125
+
126
+ def get_ticket_fields(self, ticket_id: str, field_names_filter: list) -> dict:
127
+ response = self.session.get(f"{self.host}/rest/api/{self.API_VERSION}/issue/{ticket_id}?fields={','.join(field_names_filter)}")
128
+ self.logger.debug(f"Get ticket fields (id={ticket_id}) response: status_code = {response.status_code}, body = {response.text}")
129
+ if not response.ok:
130
+ self.logger.warning(f"Can't get ticket info by ticket_id = {ticket_id}. Response status = {response.status_code}")
131
+ return {}
132
+ return self._transform_ticket_fields(response.json().get("fields", {}))
133
+
134
+ def get_editmeta_fields(self, ticket_id: str) -> dict:
135
+ response = self.session.get(f"{self.host}/rest/api/{self.API_VERSION}/issue/{ticket_id}/editmeta")
136
+ self.logger.debug(f"Get editmeta for ticket (id={ticket_id}) response: status_code = {response.status_code}, body = {response.text}")
137
+ if not response.ok:
138
+ self.logger.warning(f"Can't get ticket {ticket_id} editmeta. Response status = {response.status_code}")
139
+ return {}
140
+ return response.json().get("fields", {})
141
+
142
+ @RetryDecorator(condition_func=lambda response: response is not None and (response.ok or response.status_code == 400))
143
+ def update_ticket(self, ticket_id: str, ticket_fields: dict,
144
+ retry_timeout_seconds: int = 180, retry_wait_seconds: int = 1) -> Response:
145
+ body = {"fields": ticket_fields}
146
+ response = self.session.put(f"{self.host}/rest/api/{self.API_VERSION}/issue/{ticket_id}", data=json.dumps(body))
147
+ self.logger.debug(f"Update ticket (id={ticket_id}) response: status_code = {response.status_code}, body = {response.text}")
148
+ return response
149
+
150
+ def get_ticket_transitions(self, ticket_id: str):
151
+ response = self.session.get(f"{self.host}/rest/api/{self.API_VERSION}/issue/{ticket_id}/transitions?expand=transitions.fields")
152
+ self.logger.debug(f"Get ticket (id={ticket_id}) transitions response: status_code = {response.status_code}, body = {response.text}")
153
+ if not response.ok:
154
+ self.logger.warning(f"Ticket {ticket_id} transitions are not found in response. Response status = {response.status_code}")
155
+ return []
156
+ return response.json().get("transitions", [])
157
+
158
+ def find_applicable_transition(self, transitions: list, status_name: str, transition_name: str = ""):
159
+ transitions_by_next_status = [transition for transition in transitions if status_name.lower() == transition.get("to", {}).get("name", "").lower()]
160
+ if not transitions_by_next_status:
161
+ self.logger.error(f"Status '{status_name}' is not found in transitions.")
162
+ return None
163
+
164
+ if len(transitions_by_next_status) > 1:
165
+ self.logger.warning(f"Found more than one transition to status '{status_name}'")
166
+ if transition_name:
167
+ transitions_by_name = [transition for transition in transitions_by_next_status if transition_name.lower() == transition.get("name", "").lower()]
168
+ if transitions_by_name:
169
+ return transitions_by_name[0]
170
+ else:
171
+ self.logger.warning(f"Transition '{transition_name}' is not found in transitions. Will use first found transition by status '{status_name}'.")
172
+
173
+ return transitions_by_next_status[0]
174
+
175
+ def perform_ticket_transition(self, ticket_id: str, transition_id: str, transition_fields):
176
+ body = {"transition": {"id": transition_id}}
177
+ if transition_fields:
178
+ body["fields"] = transition_fields
179
+ response = self.session.post(f"{self.host}/rest/api/{self.API_VERSION}/issue/{ticket_id}/transitions", data=json.dumps(body))
180
+ self.logger.debug(f"Perform transition for ticket (id={ticket_id}) response: status_code = {response.status_code}, body = {response.text}")
181
+ return response
182
+
183
+ def _get_json(self, path: str, params: dict[str, Any] | None = None, use_post: bool = False) -> dict | list:
184
+ url = f"{self.host}/rest/api/{self.API_VERSION}/{path}"
185
+ response = (
186
+ self.session.post(url, data=json.dumps(params))
187
+ if use_post
188
+ else self.session.get(url, params=params)
189
+ )
190
+ return response.json()
191
+
192
+ @staticmethod
193
+ def filter_ticket_fields(ticket_fields: dict, meta_fields_filter: dict) -> dict:
194
+ return {field_key: field_value for field_key, field_value in ticket_fields.items() if field_key in meta_fields_filter.keys()}
195
+
196
+ @staticmethod
197
+ def _transform_ticket_fields(ticket_fields_json: dict):
198
+ filtered_fields = {}
199
+ for k, v in ticket_fields_json.items():
200
+ if isinstance(v, dict) and "emailAddress" in v:
201
+ filtered_fields[k] = JiraClient.serialize_person_ref(v)
202
+ else:
203
+ filtered_fields[k] = v
204
+ return filtered_fields
205
+
206
+ @staticmethod
207
+ def serialize_person_ref(person: dict) -> dict | None:
208
+ if person:
209
+ return {
210
+ "displayName": person.get("displayName", None),
211
+ "emailAddress": person.get("emailAddress", None),
212
+ "name": person.get("name", None),
213
+ "key": person.get("key", None),
214
+ }
215
+ return None
@@ -0,0 +1,126 @@
1
+ import re
2
+
3
+ from qubership_pipelines_common_library.v1.execution.exec_command import ExecutionCommand
4
+ from qubership_pipelines_common_library.v2.jira.jira_client import JiraClient, AuthType
5
+ from qubership_pipelines_common_library.v2.jira.jira_utils import JiraUtils
6
+
7
+
8
+ class JiraCreateTicket(ExecutionCommand):
9
+ """
10
+ Creates new issue/ticket in JIRA project.
11
+
12
+ Input Parameters Structure (this structure is expected inside "input_params.params" block):
13
+ ```
14
+ {
15
+ "ticket": {
16
+ "fields: { # REQUIRED: Dict structure that will be used as ticket-creation-body, without transformations
17
+ "project": {"key": "<YOUR_PROJECT_KEY>"}, # REQUIRED: Project Key
18
+ "issuetype": {"name": "Bug"}, # REQUIRED: Issue type name
19
+ "priority": {"name": "High"}, # OPTIONAL: Other ticket fields with different formats, depending on your Project configuration
20
+ "duedate": "2030-02-20", # OPTIONAL: Text-value fields need no dict wrappers
21
+ "summary": "[SOME_LABEL] Ticket Subject",
22
+ "description": "Ticket body",
23
+ "components": [{"name":"COMPONENT NAME"}],
24
+ "labels": ["Test_Label1"],
25
+ },
26
+ "comment": "your comment body", # OPTIONAL: Comment to add to created ticket
27
+ "field_names_filter": "summary,issuetype,creator,status", # OPTIONAL: Comma-separated names of fields to extract from created ticket to output params
28
+ },
29
+ "retry_timeout_seconds": 180, # OPTIONAL: Timeout for JIRA client operations in seconds (default: 180)
30
+ "retry_wait_seconds": 1, # OPTIONAL: Wait interval between retries in seconds (default: 1)
31
+ }
32
+ ```
33
+
34
+ Systems Configuration (expected in "systems.jira" block):
35
+ ```
36
+ {
37
+ "url": "https://your_cloud_jira.atlassian.net", # REQUIRED: JIRA server URL
38
+ "username": "your_username_or_email", # REQUIRED: JIRA user login or email
39
+ "password": "<your_token>", # REQUIRED: JIRA user token
40
+ "auth_type": "basic" # OPTIONAL: 'basic' or 'bearer'
41
+ }
42
+ ```
43
+
44
+ Command name: "jira-create-ticket"
45
+ """
46
+ RETRY_TIMEOUT_SECONDS = 180 # default value, how many seconds to try
47
+ RETRY_WAIT_SECONDS = 1 # default value, how many seconds between tries
48
+
49
+ def _validate(self):
50
+ names = [
51
+ "paths.input.params",
52
+ "paths.output.params",
53
+ "systems.jira.url",
54
+ "systems.jira.username",
55
+ "systems.jira.password",
56
+ "params.ticket.fields",
57
+ ]
58
+ if not self.context.validate(names):
59
+ return False
60
+
61
+ self.retry_timeout_seconds = int(self.context.input_param_get("params.retry_timeout_seconds", self.RETRY_TIMEOUT_SECONDS))
62
+ self.retry_wait_seconds = int(self.context.input_param_get("params.retry_wait_seconds", self.RETRY_WAIT_SECONDS))
63
+
64
+ self.jira_url = self.context.input_param_get("systems.jira.url").rstrip('/')
65
+ self.jira_username = self.context.input_param_get("systems.jira.username")
66
+ self.jira_password = self.context.input_param_get("systems.jira.password")
67
+ self.auth_type = self.context.input_param_get("systems.jira.auth_type", AuthType.BASIC)
68
+
69
+ self.ticket_comment = self.context.input_param_get("params.ticket.comment")
70
+ self.ticket_fields = self.context.input_param_get("params.ticket.fields", {})
71
+ self.project_key = self.ticket_fields.get('project', {}).get('key')
72
+ self.issue_type_name = self.ticket_fields.get('issuetype', {}).get('name')
73
+
74
+ if not self.project_key or not self.issue_type_name:
75
+ self.context.logger.error("Can't find project.key and/or issuetype.name in input parameters")
76
+ return False
77
+
78
+ if not self._validate_mandatory_ticket_fields(self.ticket_fields):
79
+ return False
80
+
81
+ if field_names_filter := self.context.input_param_get("params.ticket.field_names_filter"):
82
+ self.field_names_filter = [x.strip() for x in re.split(r'[,;]+', field_names_filter)]
83
+ else:
84
+ self.field_names_filter = JiraClient.DEFAULT_FIELD_NAMES_FILTER
85
+
86
+ return True
87
+
88
+ def _validate_mandatory_ticket_fields(self, ticket_fields):
89
+ valid = True
90
+ for field_key in ["project", "issuetype", "summary"]:
91
+ if field_key not in ticket_fields:
92
+ valid = False
93
+ self.context.logger.error(f"Parameter '{field_key}' is mandatory but not found in ticket params map")
94
+ return valid
95
+
96
+ def _execute(self):
97
+ self.context.logger.info("Running jira-create-ticket")
98
+ self.context.logger.info(f"Creating ticket in project {self.project_key}, type {self.issue_type_name}")
99
+ self.jira_client = JiraClient.create_jira_client(
100
+ self.jira_url, self.jira_username, self.jira_password, self.auth_type,
101
+ self.retry_timeout_seconds, self.retry_wait_seconds,
102
+ )
103
+
104
+ createmeta_fields = self.jira_client.get_createmeta_fields(self.project_key, self.issue_type_name)
105
+ self.filtered_ticket_fields = JiraClient.filter_ticket_fields(self.ticket_fields, createmeta_fields)
106
+ self.context.logger.debug(f"Filtered ticket fields: {self.filtered_ticket_fields}")
107
+
108
+ create_ticket_response = self.jira_client.create_ticket(self.filtered_ticket_fields,
109
+ retry_timeout_seconds=self.retry_timeout_seconds,
110
+ retry_wait_seconds=self.retry_wait_seconds)
111
+ if not create_ticket_response.ok:
112
+ self._exit(False, f"Can't create ticket. Response status: {create_ticket_response.status_code}")
113
+ self.ticket_key = create_ticket_response.json().get('key')
114
+ self.context.logger.info(f"Ticket created successfully: {self.ticket_key}")
115
+
116
+ if self.ticket_comment:
117
+ JiraUtils.add_ticket_comment(self)
118
+
119
+ self.context.output_param_set("params.ticket.id", self.ticket_key)
120
+ self.context.output_param_set("params.ticket.url", f"{self.jira_url}/browse/{self.ticket_key}")
121
+
122
+ filtered_response_fields = self.jira_client.get_ticket_fields(self.ticket_key, self.field_names_filter)
123
+ self.context.output_param_set("params.ticket.fields", filtered_response_fields)
124
+
125
+ self.context.output_params_save()
126
+ self.context.logger.info("JIRA ticket creation completed successfully")
@@ -0,0 +1,139 @@
1
+ import re
2
+
3
+ from qubership_pipelines_common_library.v1.execution.exec_command import ExecutionCommand
4
+ from qubership_pipelines_common_library.v2.jira.jira_client import JiraClient, AuthType
5
+ from qubership_pipelines_common_library.v2.jira.jira_utils import JiraUtils
6
+
7
+
8
+ class JiraUpdateTicket(ExecutionCommand):
9
+ """
10
+ Updates ticket fields and transitions status.
11
+
12
+ Input Parameters Structure (this structure is expected inside "input_params.params" block):
13
+ ```
14
+ {
15
+ "ticket": {
16
+ "fields: { # REQUIRED: Dict structure that will be used as ticket-update-body, without transformations
17
+ "status": {"name": "Done"}, # OPTIONAL: Next status name
18
+ "transition": {"name": "From Review to Done"}, # OPTIONAL: Transition name
19
+ "priority": {"name": "High"}, # OPTIONAL: Other ticket fields with different formats, depending on your Project configuration
20
+ "duedate": "2030-02-20", # OPTIONAL: Text-value fields need no dict wrappers
21
+ "description": "Ticket body",
22
+ "labels": ["Test_Label1"],
23
+ },
24
+ "id": "BUG-567", # REQUIRED: Ticket ID
25
+ "comment": "your comment body", # OPTIONAL: Comment to add to created ticket
26
+ "field_names_filter": "summary,issuetype,creator,status", # OPTIONAL: Comma-separated names of fields to extract from created ticket to output params
27
+ },
28
+ "retry_timeout_seconds": 180, # OPTIONAL: Timeout for JIRA client operations in seconds (default: 180)
29
+ "retry_wait_seconds": 1, # OPTIONAL: Wait interval between retries in seconds (default: 1)
30
+ }
31
+ ```
32
+
33
+ Systems Configuration (expected in "systems.jira" block):
34
+ ```
35
+ {
36
+ "url": "https://your_cloud_jira.atlassian.net", # REQUIRED: JIRA server URL
37
+ "username": "your_username_or_email", # REQUIRED: JIRA user login or email
38
+ "password": "<your_token>", # REQUIRED: JIRA user token
39
+ "auth_type": "basic" # OPTIONAL: 'basic' or 'bearer'
40
+ }
41
+ ```
42
+
43
+ Command name: "jira-update-ticket"
44
+ """
45
+ RETRY_TIMEOUT_SECONDS = 180 # default value, how many seconds to try
46
+ RETRY_WAIT_SECONDS = 1 # default value, how many seconds between tries
47
+
48
+ def _validate(self):
49
+ names = [
50
+ "paths.input.params",
51
+ "paths.output.params",
52
+ "systems.jira.url",
53
+ "systems.jira.username",
54
+ "systems.jira.password",
55
+ "params.ticket.id",
56
+ "params.ticket.fields",
57
+ ]
58
+ if not self.context.validate(names):
59
+ return False
60
+
61
+ self.retry_timeout_seconds = int(self.context.input_param_get("params.retry_timeout_seconds", self.RETRY_TIMEOUT_SECONDS))
62
+ self.retry_wait_seconds = int(self.context.input_param_get("params.retry_wait_seconds", self.RETRY_WAIT_SECONDS))
63
+
64
+ self.jira_url = self.context.input_param_get("systems.jira.url").rstrip('/')
65
+ self.jira_username = self.context.input_param_get("systems.jira.username")
66
+ self.jira_password = self.context.input_param_get("systems.jira.password")
67
+ self.auth_type = self.context.input_param_get("systems.jira.auth_type", AuthType.BASIC)
68
+
69
+ self.ticket_key = self.context.input_param_get("params.ticket.id")
70
+ self.ticket_comment = self.context.input_param_get("params.ticket.comment")
71
+ self.ticket_fields = self.context.input_param_get("params.ticket.fields")
72
+ self.next_status_name = self.ticket_fields.pop('status', {}).get('name')
73
+ self.transition_name = self.ticket_fields.pop('transition', {}).get('name')
74
+
75
+ if field_names_filter := self.context.input_param_get("params.ticket.field_names_filter"):
76
+ self.field_names_filter = [x.strip() for x in re.split(r'[,;]+', field_names_filter)]
77
+ else:
78
+ self.field_names_filter = JiraClient.DEFAULT_FIELD_NAMES_FILTER
79
+
80
+ return True
81
+
82
+ def _execute(self):
83
+ self.context.logger.info("Running jira-update-ticket")
84
+ self.context.logger.info(f"Updating ticket {self.ticket_key}...")
85
+ self.jira_client = JiraClient.create_jira_client(
86
+ self.jira_url, self.jira_username, self.jira_password, self.auth_type,
87
+ self.retry_timeout_seconds, self.retry_wait_seconds,
88
+ )
89
+
90
+ editmeta_fields = self.jira_client.get_editmeta_fields(self.ticket_key)
91
+ self.filtered_ticket_fields = JiraClient.filter_ticket_fields(self.ticket_fields, editmeta_fields)
92
+ self.context.logger.debug(f"Filtered ticket fields: {self.filtered_ticket_fields}")
93
+
94
+ update_ticket_response = self.jira_client.update_ticket(
95
+ self.ticket_key, self.filtered_ticket_fields,
96
+ retry_timeout_seconds=self.retry_timeout_seconds, retry_wait_seconds=self.retry_wait_seconds
97
+ )
98
+ if not update_ticket_response.ok:
99
+ self._exit(False, f"Can't update ticket. Response status: {update_ticket_response.status_code}")
100
+
101
+ if self.next_status_name:
102
+ self.context.logger.info(f"Updating ticket status to '{self.next_status_name}'...")
103
+ self._perform_status_transition()
104
+
105
+ if self.ticket_comment:
106
+ JiraUtils.add_ticket_comment(self)
107
+
108
+ self.context.output_param_set("params.ticket.id", self.ticket_key)
109
+ self.context.output_param_set("params.ticket.url", f"{self.jira_url}/browse/{self.ticket_key}")
110
+
111
+ filtered_response_fields = self.jira_client.get_ticket_fields(self.ticket_key, self.field_names_filter)
112
+ self.context.output_param_set("params.ticket.fields", filtered_response_fields)
113
+
114
+ self.context.output_params_save()
115
+ self.context.logger.info("Update ticket request executed. See output params for details")
116
+
117
+ def _perform_status_transition(self):
118
+
119
+ ticket_current_status = self.jira_client.get_ticket_fields(self.ticket_key, ["status"]).get("status", {}).get("name")
120
+ if str(self.next_status_name).strip().lower() == str(ticket_current_status).strip().lower():
121
+ self.context.logger.info(f"Ticket {self.ticket_key} already has '{self.next_status_name}' status. Skipping status transition.")
122
+
123
+ else:
124
+ transitions = self.jira_client.get_ticket_transitions(self.ticket_key)
125
+ if not transitions:
126
+ self._exit(False, f"Can't find ticket {self.ticket_key} transitions.")
127
+
128
+ transition = self.jira_client.find_applicable_transition(
129
+ transitions, self.next_status_name, self.transition_name
130
+ )
131
+ if transition is None:
132
+ self._exit(False, f"Can't find transition with next status '{self.next_status_name}'.")
133
+
134
+ ticket_transition_fields = JiraClient.filter_ticket_fields(self.ticket_fields, transition.get("fields"))
135
+ transition_response = self.jira_client.perform_ticket_transition(
136
+ self.ticket_key, transition.get("id"), ticket_transition_fields
137
+ )
138
+ if not transition_response.ok:
139
+ self._exit(False, f"Can't perform ticket status transition. Response status: {transition_response.status_code}")
@@ -0,0 +1,19 @@
1
+ from qubership_pipelines_common_library.v1.execution.exec_command import ExecutionCommand
2
+
3
+
4
+ class JiraUtils:
5
+
6
+ @staticmethod
7
+ def add_ticket_comment(command: ExecutionCommand):
8
+ try:
9
+ command.context.logger.info(f"Adding comment to ticket {command.ticket_key}...")
10
+ add_ticket_comment_response = command.jira_client.add_ticket_comment(
11
+ command.ticket_key, command.ticket_comment,
12
+ retry_timeout_seconds=command.retry_timeout_seconds,
13
+ retry_wait_seconds=command.retry_wait_seconds
14
+ )
15
+ if not add_ticket_comment_response.ok:
16
+ command._exit(False, f"Can't add ticket comment. Response status: {add_ticket_comment_response.status_code}")
17
+ command.context.logger.info(f"Comment added to ticket {command.ticket_key}")
18
+ except Exception as e:
19
+ command._exit(False, f"Can't add ticket comment. Response exception: {str(e)}")
@@ -0,0 +1,150 @@
1
+ import smtplib
2
+ import socket
3
+ import ssl
4
+
5
+ from email.utils import formatdate
6
+ from email.mime.multipart import MIMEMultipart
7
+ from email.mime.text import MIMEText
8
+ from qubership_pipelines_common_library.v1.execution.exec_command import ExecutionCommand
9
+ from qubership_pipelines_common_library.v1.utils.utils_string import UtilsString
10
+
11
+
12
+ class SendEmail(ExecutionCommand):
13
+ """
14
+ This command sends email notification with optional attachments.
15
+
16
+ Input Parameters Structure (this structure is expected inside "input_params.params" block):
17
+ ```
18
+ {
19
+ "email_subject": "Report for 01.01.2026", # REQUIRED: E-mail subject
20
+ "email_body": "Following jobs were completed: ...", # REQUIRED: E-mail message
21
+ "email_recipients": "user1@qubership.org,user2@qubership.org", # REQUIRED: Comma-separated list of recipients
22
+ "email_body_type": "plain", # OPTIONAL: Either "plain" or "html
23
+ "attachments": { # OPTIONAL: Dict with attachments
24
+ "unique_attachment_key": {
25
+ "name": "HTML_report.html", # REQUIRED: File name used for attachment
26
+ "content": "<html>...</html>", # REQUIRED: Text content put inside attachment
27
+ "mime_type": "text/html",
28
+ },
29
+ "another_attachment_key": {...}
30
+ }
31
+ }
32
+ ```
33
+
34
+ Systems Configuration (expected in "systems.email" block):
35
+ ```
36
+ {
37
+ "server": "your.mail.server.org", # REQUIRED: E-mail host server
38
+ "port": "3025" # REQUIRED: E-mail port
39
+ "user": "your@email.bot" # REQUIRED: E-mail user
40
+ "password": "<email_password>" # OPTIONAL: E-mail password
41
+ "use_ssl": "False" # OPTIONAL: SMTP connection will use SSL mode (default: False)
42
+ "use_tls": "False" # OPTIONAL: SMTP connection will use TLS mode (default: False)
43
+ "verify": "False" # OPTIONAL: SSL Certificate verification (default: False)
44
+ "timeout_seconds": "60" # OPTIONAL: SMTP connection timeout in seconds (default: 60)
45
+ }
46
+ ```
47
+
48
+ Command name: "send-email"
49
+ """
50
+
51
+ EMAIL_BODY_TYPE_PLAIN = "plain"
52
+ EMAIL_BODY_TYPE_HTML = "html"
53
+ EMAIL_BODY_TYPES = [EMAIL_BODY_TYPE_PLAIN, EMAIL_BODY_TYPE_HTML]
54
+ WAIT_TIMEOUT = 60
55
+
56
+ def _validate(self):
57
+ required_params = [
58
+ "paths.input.params",
59
+ "systems.email.server",
60
+ "systems.email.port",
61
+ "systems.email.user",
62
+ "params.email_subject",
63
+ "params.email_body",
64
+ "params.email_recipients",
65
+ ]
66
+ if not self.context.validate(required_params):
67
+ return False
68
+
69
+ self.email_server = self.context.input_param_get("systems.email.server")
70
+ self.email_port = self.context.input_param_get("systems.email.port")
71
+ self.email_user = self.context.input_param_get("systems.email.user")
72
+ self.email_password = self.context.input_param_get("systems.email.password")
73
+ self.email_password = self.context.input_param_get("systems.email.password")
74
+ self.timeout_seconds = max(1, int(self.context.input_param_get("systems.email.timeout_seconds", self.WAIT_TIMEOUT)))
75
+ self.use_ssl = UtilsString.convert_to_bool(self.context.input_param_get("systems.email.use_ssl", False))
76
+ self.use_tls = UtilsString.convert_to_bool(self.context.input_param_get("systems.email.use_tls", False))
77
+ self.verify = UtilsString.convert_to_bool(self.context.input_param_get("systems.email.verify", False))
78
+
79
+ self.email_subject = self.context.input_param_get("params.email_subject")
80
+ self.email_body = self.context.input_param_get("params.email_body")
81
+ self.email_recipients = [x.strip() for x in self.context.input_param_get("params.email_recipients").split(",")]
82
+
83
+ self.email_body_type = self.context.input_param_get("params.email_body_type", SendEmail.EMAIL_BODY_TYPE_PLAIN)
84
+ if self.email_body_type not in SendEmail.EMAIL_BODY_TYPES:
85
+ self.context.logger.error(f"Incorrect email_body_type value: {self.email_body_type}. Only '{SendEmail.EMAIL_BODY_TYPES}' are supported")
86
+ return False
87
+
88
+ self.attachments = []
89
+ for key, data in self.context.input_param_get("params.attachments", {}).items():
90
+ if not data.get("content") or not data.get("name"):
91
+ self.context.logger.error(f"Attachment with key [{key}] is missing content and/or name!")
92
+ return False
93
+ self.attachments.append({
94
+ "name": data.get("name"),
95
+ "mime_type": data.get("mime_type", "text/plain"),
96
+ "content": data.get("content")
97
+ })
98
+
99
+ return True
100
+
101
+ def _execute(self):
102
+ try:
103
+ msg = MIMEMultipart()
104
+ msg["From"] = self.email_user
105
+ msg["To"] = ", ".join(self.email_recipients)
106
+ msg["Subject"] = self.email_subject
107
+ msg["Date"] = formatdate(localtime=True)
108
+ msg.attach(MIMEText(self.email_body, self.email_body_type))
109
+
110
+ if self.attachments:
111
+ for attachment in self.attachments:
112
+ part = MIMEText(attachment["content"], attachment["mime_type"])
113
+ part.add_header('Content-Disposition', f'attachment; filename="{attachment["name"]}"')
114
+ msg.attach(part)
115
+
116
+ self.context.logger.debug(f"Connecting to SMTP server: {self.email_server}:{self.email_port}"
117
+ f", using SSL: {self.use_ssl}, TLS: {self.use_tls}, verify: {self.verify}")
118
+ ssl_context = ssl.create_default_context() if self.verify else ssl._create_unverified_context()
119
+ if self.use_ssl:
120
+ smtp = smtplib.SMTP_SSL(self.email_server, self.email_port, timeout=self.timeout_seconds, context=ssl_context)
121
+ else:
122
+ smtp = smtplib.SMTP(self.email_server, self.email_port, timeout=self.timeout_seconds)
123
+
124
+ try:
125
+ if self.use_tls and not self.use_ssl:
126
+ smtp.starttls(context=ssl_context)
127
+
128
+ if self.email_password:
129
+ self.context.logger.debug("Authenticating with server...")
130
+ smtp.login(self.email_user, self.email_password)
131
+
132
+ self.context.logger.debug("Sending email...")
133
+ smtp.send_message(msg)
134
+ self.context.logger.info(f"Email sent successfully to {len(self.email_recipients)} recipients: {self.email_recipients}")
135
+ finally:
136
+ try:
137
+ smtp.quit()
138
+ except Exception:
139
+ pass
140
+
141
+ except socket.gaierror as e:
142
+ self._exit(False, f"Invalid SMTP server. Cannot resolve hostname '{self.email_server}': {str(e)}")
143
+ except socket.timeout as e:
144
+ self._exit(False, f"Connection timeout to SMTP server: {str(e)}")
145
+ except smtplib.SMTPAuthenticationError as e:
146
+ self._exit(False, f"SMTP authentication failed: {str(e)}")
147
+ except smtplib.SMTPRecipientsRefused as e:
148
+ self._exit(False, f"Email recipients rejected - verify email addresses are valid and domain exists: {str(e)}. SMTP may reject domains like: @gmail.com and etc.")
149
+ except Exception as e:
150
+ self._exit(False, f"Failed to send email: {str(e)}")
@@ -0,0 +1,131 @@
1
+ import tempfile
2
+ import os
3
+ import traceback
4
+
5
+ from qubership_pipelines_common_library.v1.execution.exec_command import ExecutionCommand
6
+ from qubership_pipelines_common_library.v1.webex_client import WebexClient
7
+ from webexpythonsdk.exceptions import ApiError
8
+ from requests.exceptions import ProxyError
9
+
10
+
11
+ class SendWebexMessage(ExecutionCommand):
12
+ """
13
+ This command sends Webex message with optional attachments.
14
+
15
+ Input Parameters Structure (this structure is expected inside "input_params.params" block):
16
+ ```
17
+ {
18
+ "webex_message": "Hello, world!", # REQUIRED: Text message (Markdown format is supported)
19
+ "parent_id": "1234321", # OPTIONAL: The parent message to reply to
20
+ "attachments": { # OPTIONAL: Dict with attachments
21
+ "unique_attachment_key": {
22
+ "name": "HTML_report.html", # REQUIRED: File name used for attachment
23
+ "content": "<html>...</html>", # REQUIRED: Text content put inside attachment
24
+ "mime_type": "text/html",
25
+ },
26
+ "another_attachment_key": {...}
27
+ }
28
+ }
29
+ ```
30
+
31
+ Systems Configuration (expected in "systems.webex" block):
32
+ ```
33
+ {
34
+ "room_id": "...Y2lzY29zc...", # REQUIRED: Webex unique room_id where message will be posted
35
+ "token": "your_bot_account_token" # REQUIRED: Bot/Service account token that will be used to send message
36
+ "proxy": "https://127.0.0.1" # OPTIONAL: Host to be used as a webex-proxy
37
+ }
38
+ ```
39
+
40
+ Output Parameters:
41
+ - params.message_id: Received `message_id` of sent message
42
+ - params.attachment_message_ids: dict of `attachment_name` -> `message_id`
43
+
44
+ Command name: "send-webex-message"
45
+ """
46
+
47
+ def _validate(self):
48
+ required_params = [
49
+ "paths.input.params",
50
+ "paths.output.params",
51
+ "systems.webex.room_id",
52
+ "systems.webex.token",
53
+ "params.webex_message",
54
+ ]
55
+ if not self.context.validate(required_params):
56
+ return False
57
+
58
+ self.webex_room_id = self.context.input_param_get("systems.webex.room_id")
59
+ self.webex_token = self.context.input_param_get("systems.webex.token")
60
+ self.webex_proxy = self.context.input_param_get("systems.webex.proxy")
61
+ self.webex_message = self.context.input_param_get("params.webex_message")
62
+ self.webex_parent_id = self.context.input_param_get("params.parent_id", "")
63
+
64
+ self.attachments = []
65
+ for key, data in self.context.input_param_get("params.attachments", {}).items():
66
+ if not data.get("content") or not data.get("name"):
67
+ self.context.logger.error(f"Attachment with key [{key}] is missing content and/or name!")
68
+ return False
69
+ self.attachments.append({
70
+ "name": data.get("name"),
71
+ "mime_type": data.get("mime_type"),
72
+ "content": data.get("content")
73
+ })
74
+
75
+ self.webex_client = WebexClient(
76
+ bot_token=self.webex_token,
77
+ proxies={"https": self.webex_proxy} if self.webex_proxy else None
78
+ )
79
+ return True
80
+
81
+ def _execute(self):
82
+ try:
83
+ response = self.webex_client.send_message(
84
+ room_id=self.webex_room_id,
85
+ parent_id=self.webex_parent_id,
86
+ markdown=self.webex_message
87
+ )
88
+ message_id = response.id if response else None
89
+ self.context.output_param_set("params.message_id", message_id)
90
+
91
+ if self.attachments:
92
+ attachment_message_ids = {}
93
+ for attachment in self.attachments:
94
+ attachment_message_ids[attachment["name"]] = self._send_attachment(attachment)
95
+ self.context.output_param_set("params.attachment_message_ids", attachment_message_ids)
96
+
97
+ self.context.output_params_save()
98
+ self.context.logger.info("Webex message sent successfully.")
99
+
100
+ except ApiError as e:
101
+ self.context.logger.debug("Full traceback: %s", traceback.format_exc())
102
+ error_str = str(e)
103
+ if "404" in error_str or "Not Found" in error_str:
104
+ error_msg = f"Webex room not found - verify room_id '{self.webex_room_id}' exists. {error_str}"
105
+ elif "401" in error_str or "Unauthorized" in error_str:
106
+ error_msg = f"Webex authentication failed - verify token is valid: {error_str}"
107
+ else:
108
+ error_msg = f"Webex API error: {error_str}"
109
+ self._exit(False, error_msg)
110
+
111
+ except ProxyError as e:
112
+ self.context.logger.debug("Full traceback: %s", traceback.format_exc())
113
+ self._exit(False, f"Invalid proxy: {str(e)}")
114
+
115
+ except Exception as e:
116
+ self._exit(False, f"Failed to send Webex message: {e}\n{traceback.format_exc()}")
117
+
118
+ def _send_attachment(self, attachment: dict):
119
+ temp_dir = tempfile.gettempdir()
120
+ temp_file_path = os.path.join(temp_dir, attachment["name"])
121
+ with open(temp_file_path, "w") as temp_file:
122
+ temp_file.write(attachment["content"])
123
+ temp_file_path = temp_file.name
124
+ try:
125
+ attachment_response = self.webex_client.send_message(room_id=self.webex_room_id,
126
+ parent_id=self.webex_parent_id,
127
+ markdown=attachment["name"],
128
+ attachment_path=temp_file_path)
129
+ return attachment_response.id if attachment_response else None
130
+ finally:
131
+ os.remove(temp_file_path)
@@ -36,6 +36,8 @@ class CryptoUtils:
36
36
  }
37
37
  elif isinstance(data, list):
38
38
  return [CryptoUtils.mask_values(item, path) for item in data]
39
+ elif data is None or data == '':
40
+ return ""
39
41
  else:
40
42
  return "[MASKED]"
41
43
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qubership-pipelines-common-library
3
- Version: 2.0.1
3
+ Version: 2.0.3
4
4
  Summary: Qubership Pipelines common library
5
5
  License: Apache-2.0
6
6
  License-File: LICENSE
@@ -20,7 +20,7 @@ Requires-Dist: google-cloud-artifact-registry (>=1.16.1,<2.0.0)
20
20
  Requires-Dist: http-exceptions (>=0.2.10,<0.3.0)
21
21
  Requires-Dist: kubernetes (>=34.1.0,<35.0.0)
22
22
  Requires-Dist: minio (>=7.2.12,<8.0.0)
23
- Requires-Dist: python-gitlab (>=4.13.0,<5.0.0)
23
+ Requires-Dist: python-gitlab (>=4.13.0,<6.0.0)
24
24
  Requires-Dist: python-jenkins (>=1.8.2,<2.0.0)
25
25
  Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
26
26
  Requires-Dist: requests (>=2.32.3,<3.0.0)
@@ -19,12 +19,12 @@ qubership_pipelines_common_library/v1/utils/__init__.py,sha256=QczIlSYNOtXMuMWSz
19
19
  qubership_pipelines_common_library/v1/utils/rest.py,sha256=OfYGPE2tM-vDUg__ytZKfpCeGVCDePSjXDHK61A2KDM,3069
20
20
  qubership_pipelines_common_library/v1/utils/utils.py,sha256=aMyUrJqlnRcFSjNtiyKa5TDJ31m2ABpljeQdnil3A20,1866
21
21
  qubership_pipelines_common_library/v1/utils/utils_aws.py,sha256=BPPnHBzPPXPqFijtAiw16sTPu1tFZjS95GkSMX_HdjA,808
22
- qubership_pipelines_common_library/v1/utils/utils_cli.py,sha256=URmspvyL2kzRyoaXJbxsVEEcy9n4l9ezuvFYVW4W1hk,4188
22
+ qubership_pipelines_common_library/v1/utils/utils_cli.py,sha256=jkhBswmYqJHfszaYhWknCTxaBx0tEh42McFx5dP6EXQ,4931
23
23
  qubership_pipelines_common_library/v1/utils/utils_context.py,sha256=IlMFXGxS8zJw33Gu3SbOUcj88wquIkobBlWkdFbR7MA,3767
24
24
  qubership_pipelines_common_library/v1/utils/utils_dictionary.py,sha256=xe8ftnigzyCKWCZaUnI2Xu2ykYlOl1Nc9ATimXuCbgM,1366
25
25
  qubership_pipelines_common_library/v1/utils/utils_file.py,sha256=E4RhpeCRhUt-EQ_6pUz-QEKkH8JidJWwYxZmrAvpHBk,2905
26
26
  qubership_pipelines_common_library/v1/utils/utils_json.py,sha256=QczIlSYNOtXMuMWSznhV_BkXMM5KLn1wOogtlT2kcy0,598
27
- qubership_pipelines_common_library/v1/utils/utils_logging.py,sha256=2kPBjPyQfPOZ8_Zv4W-aBWBlZtN_mDCc3JWHzdW4BJo,1506
27
+ qubership_pipelines_common_library/v1/utils/utils_logging.py,sha256=rVkx1OnyL9WmvLvvYij9Fn9tM16geqsCzeeVwObPhRw,1505
28
28
  qubership_pipelines_common_library/v1/utils/utils_string.py,sha256=Phx5ZXPRjhjg9AaSPx6WLX9zQvwJH1txslfnG3jJ43w,993
29
29
  qubership_pipelines_common_library/v1/webex_client.py,sha256=JU_0NgLu_p6zgaUi-ixgZeFMlJaTAvXwrU1oA607Bv0,2997
30
30
  qubership_pipelines_common_library/v2/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -63,14 +63,23 @@ qubership_pipelines_common_library/v2/jenkins/jenkins_client.py,sha256=h-EWIBYHx
63
63
  qubership_pipelines_common_library/v2/jenkins/jenkins_pipeline_data_importer.py,sha256=zWNejXoAw1y25Jf6cl6f7UhwnJsyEK87hw7MDdoL0Mk,1572
64
64
  qubership_pipelines_common_library/v2/jenkins/jenkins_run_pipeline_command.py,sha256=7ws_V2QO8QwpOKUtgl8PbcnhsUo_pHruue-if-79ee8,10136
65
65
  qubership_pipelines_common_library/v2/jenkins/safe_jenkins_client.py,sha256=PD1BaF4TbvzIy49g3EdxsliRyUp_zy6TFAuhBt29FEw,620
66
+ qubership_pipelines_common_library/v2/jira/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
67
+ qubership_pipelines_common_library/v2/jira/jira_add_ticket_comment_command.py,sha256=kHqrtYg519oVAAfH3iZJtlTWROduBAONplz2cv6qA5E,4844
68
+ qubership_pipelines_common_library/v2/jira/jira_client.py,sha256=wXWQyJiI0ZTFslEjcddLgLxPQUA5BcoulUbAFKF2Cio,11079
69
+ qubership_pipelines_common_library/v2/jira/jira_create_ticket_command.py,sha256=djPY_rH3rhjrhSNaC8SrDItBgGkGniUYXJXHe98pSi8,6741
70
+ qubership_pipelines_common_library/v2/jira/jira_update_ticket_command.py,sha256=JpkS-n-CepqwWEVV_mIP6XTGwQl4fk8KH8V6bBJrAmA,7453
71
+ qubership_pipelines_common_library/v2/jira/jira_utils.py,sha256=o6BLsPSKQjn-j0Ht8lGsF4_1PT9s7vgHvhMMxiMvt3Q,965
72
+ qubership_pipelines_common_library/v2/notifications/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
73
+ qubership_pipelines_common_library/v2/notifications/send_email_command.py,sha256=wlFdJaUyqROfVxBoSChdl4wjcxCJJ4t-vSQ-JCkjjd0,7665
74
+ qubership_pipelines_common_library/v2/notifications/send_webex_message_command.py,sha256=NuFVCijo0bnnL80mWvf_RknjgX76ZzIOuqdtynZPHRk,5819
66
75
  qubership_pipelines_common_library/v2/podman/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
67
76
  qubership_pipelines_common_library/v2/podman/podman_command.md,sha256=EKctWLIXZhoaxe579f-OgqFfjISk2UYgFD8JyzJeAVU,6500
68
77
  qubership_pipelines_common_library/v2/podman/podman_command.py,sha256=o78MYtieAD4rdOyRQEioQREhhw0e0Zqa1kNyygvOOSI,15541
69
78
  qubership_pipelines_common_library/v2/sops/sops_client.py,sha256=vB1czEqIQ6aRywkfvIQfqGZOTtPy39cnOExhfJbPw4s,4946
70
- qubership_pipelines_common_library/v2/utils/crypto_utils.py,sha256=zZ32IJY7WKzJEJNyZQVCPdWC4uujo6goR0MyzBAhn78,1504
79
+ qubership_pipelines_common_library/v2/utils/crypto_utils.py,sha256=FkVmRZVYZRNg0SzpO43cQc_PXJNSiQHlvJzxRGpVrI4,1567
71
80
  qubership_pipelines_common_library/v2/utils/extension_utils.py,sha256=-OyT6xrIg-PdHHy2Y712rbOAB6Q7WXTqGwP7oVne4k4,965
72
81
  qubership_pipelines_common_library/v2/utils/retry_decorator.py,sha256=ItYYL7zam4LrpQrsihis_n6aFCAa32ZAOKKSmxkaENM,4391
73
- qubership_pipelines_common_library-2.0.1.dist-info/METADATA,sha256=leOGiAkgcUaDCECQTxt3p0jzG1vOSno68GLK4UXPr7U,3063
74
- qubership_pipelines_common_library-2.0.1.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
75
- qubership_pipelines_common_library-2.0.1.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
76
- qubership_pipelines_common_library-2.0.1.dist-info/RECORD,,
82
+ qubership_pipelines_common_library-2.0.3.dist-info/METADATA,sha256=eGwWa2rTjnE2mv1U6lIp6W19-8FbwH7-T6qK9WYAPO8,3063
83
+ qubership_pipelines_common_library-2.0.3.dist-info/WHEEL,sha256=kJCRJT_g0adfAJzTx2GUMmS80rTJIVHRCfG0DQgLq3o,88
84
+ qubership_pipelines_common_library-2.0.3.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
85
+ qubership_pipelines_common_library-2.0.3.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.2.1
2
+ Generator: poetry-core 2.3.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any