commons-metrics 0.0.16__py3-none-any.whl → 0.0.18__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.
@@ -6,4 +6,4 @@ from .github_api_client import GitHubAPIClient
6
6
  from .azure_devops_client import AzureDevOpsClient
7
7
 
8
8
  __all__ = ['Util', 'DatabaseConnection', 'ComponentRepository', 'UpdateDesignSystemComponents', 'GitHubAPIClient', 'AzureDevOpsClient']
9
- __version__ = '0.0.16'
9
+ __version__ = '0.0.18'
@@ -0,0 +1,71 @@
1
+ import requests
2
+ import urllib3
3
+
4
+ from lib.commons_metrics.commons_metrics.s3_file_manager import S3FileManager
5
+
6
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
7
+
8
+
9
+ def load_cache_or_fetch(path: str, fetch_fn, clear_cache: bool = False):
10
+ """
11
+ Load data from a JSON cache stored in S3 using S3FileManager,
12
+ or fetch the data and update the cache.
13
+
14
+ Args:
15
+ path (str): Cache key relative to "cache/", e.g. "logs/123.json"
16
+ fetch_fn (Callable): Function returning JSON-serializable data
17
+ clear_cache (bool): If True, delete existing cache before fetching
18
+ Returns:
19
+ Any: Cached or freshly fetched data
20
+ """
21
+ s3 = S3FileManager()
22
+ key = f"cache/{path}"
23
+ if clear_cache:
24
+ try:
25
+ s3.s3.delete_object(Bucket=s3.bucket, Key=key)
26
+ except Exception:
27
+ pass
28
+ data = s3.load_json(key)
29
+
30
+ if data:
31
+ return data
32
+
33
+ data = fetch_fn()
34
+ s3.save_json(data, key)
35
+
36
+ return data
37
+
38
+
39
+ def get_data_from_api(url: str, headers: dict, auth_api) -> list:
40
+ """
41
+ Sends a GET request to the specified API and returns the JSON response if successful.
42
+
43
+ Args:
44
+ url (str): The API endpoint URL.
45
+ headers (dict): HTTP headers for the request.
46
+ auth_api: Authentication object for the request.
47
+ Returns:
48
+ list or dict: JSON response from the API if successful, otherwise an empty list.
49
+ """
50
+ response = requests.get(url, headers=headers, auth=auth_api, verify=False)
51
+ if response.ok:
52
+ return response.json()
53
+ return []
54
+
55
+
56
+ def post_data_to_api(url: str, headers: dict, body: dict, auth_api) -> list:
57
+ """
58
+ Sends a POST request to the specified API with the given body and returns the JSON response if successful.
59
+
60
+ Args:
61
+ url (str): The API endpoint URL.
62
+ headers (dict): HTTP headers for the request.
63
+ body (dict): Data to send in the POST request.
64
+ auth_api: Authentication object for the request.
65
+ Returns:
66
+ list or dict: JSON response from the API if successful, otherwise an empty list.
67
+ """
68
+ response = requests.post(url, headers=headers, auth=auth_api, data=body, verify=False)
69
+ if response.ok:
70
+ return response.json()
71
+ return []
@@ -0,0 +1,18 @@
1
+ from lib.commons_metrics.commons_metrics import DatabaseConnection, Util, ComponentRepository
2
+
3
+
4
+ def get_connection_database_from_secret(secret_name: str, logger: str, aws_region: str) -> ComponentRepository:
5
+ """
6
+ Retrieve connection database from AWS secrets manager
7
+ """
8
+ secret_json = Util.get_secret_aws(secret_name, logger, aws_region)
9
+ db_connection = DatabaseConnection()
10
+ db_connection.connect({
11
+ 'host': secret_json["host"],
12
+ 'port': secret_json["port"],
13
+ 'dbname': secret_json["dbname"],
14
+ 'username': secret_json["username"],
15
+ 'password': secret_json["password"]
16
+ })
17
+
18
+ return ComponentRepository(db_connection)
@@ -0,0 +1,66 @@
1
+ from datetime import datetime
2
+ from typing import List, Dict
3
+
4
+ from dateutil import parser
5
+
6
+
7
+ def parse_datetime(date_str: str) -> datetime:
8
+ """
9
+ Converts a string in ISO 8601 format to a datetime object.
10
+ If the string is empty or None, it returns the current date and time with the time zone.
11
+
12
+ Args:
13
+ date_str(str): Date in ISO 8601 format (e.g., "2025-12-02T10:00:00+00:00").
14
+ Returns:
15
+ datetime: Datetime object corresponding to the string, or the current date if empty.
16
+ """
17
+ if not date_str:
18
+ return datetime.now().astimezone()
19
+ return parser.isoparse(date_str)
20
+
21
+
22
+ def get_hours_difference_from_strings(start_date: str, end_date: str) -> float:
23
+ """
24
+ Calculate the difference in hours between two dates given as ISO 8601 strings.
25
+
26
+ Args:
27
+ start_date(str): Start date in ISO 8601 format.
28
+ end_date(str): End date in ISO 8601 format.
29
+ Returns:
30
+ float: Difference in hours (can be negative if end_date < start_date).
31
+ """
32
+ start = parse_datetime(start_date)
33
+ end = parse_datetime(end_date)
34
+ return get_hours_difference(start, end)
35
+
36
+
37
+ def get_hours_difference(start_date: datetime, end_date: datetime) -> float:
38
+ """
39
+ Calculate the difference in hours between two datetime objects.
40
+
41
+ Args:
42
+ start_date(datetime): Start date.
43
+ end_date(datetime): End date.
44
+ Returns:
45
+ float: Difference in hours (can be negative if end_date < start_date).
46
+ """
47
+ return (end_date - start_date).total_seconds() / 3600
48
+
49
+
50
+ def sort_by_date(list_dicts: List[Dict], date_attribute_name: str) -> List[Dict]:
51
+ """
52
+ Sorts a list of dictionaries by a specified date attribute in descending order.
53
+ The method uses the `parse_datetime` function to convert date strings into datetime objects
54
+ for accurate sorting.
55
+
56
+ Args:
57
+ list_dicts (List[Dict]): A list of dictionaries containing date attributes.
58
+ date_attribute_name (str): The key name of the date attribute in each dictionary.
59
+ Returns:
60
+ List[Dict]: A new list of dictionaries sorted by the given date attribute in descending order.
61
+ """
62
+ return sorted(
63
+ list_dicts,
64
+ key=lambda x: parse_datetime(x.get(date_attribute_name)),
65
+ reverse=True
66
+ )
@@ -0,0 +1,104 @@
1
+ import os
2
+ import json
3
+ import sys
4
+ from typing import List
5
+ import boto3
6
+ from awsglue.utils import getResolvedOptions
7
+ from botocore.exceptions import ClientError
8
+
9
+ args = getResolvedOptions(sys.argv, ['bucket', 'aws_region'])
10
+
11
+ class S3FileManager:
12
+ def __init__(self):
13
+ self.bucket = args['bucket']
14
+ if not self.bucket:
15
+ raise ValueError("Environment variable BUCKET is required")
16
+
17
+ region = args['aws_region']
18
+
19
+ client_params = {
20
+ "service_name": "s3",
21
+ "region_name": region,
22
+ }
23
+
24
+ self.s3 = boto3.client(**client_params)
25
+
26
+
27
+ def load_json(self, key: str) -> dict:
28
+ """
29
+ Loads JSON data from a file.
30
+
31
+ Args:
32
+ key (str): Key or Path to the JSON file.
33
+ Returns:
34
+ dict: Parsed JSON data as a dictionary.
35
+ Returns an empty dictionary if the file does not exist,
36
+ if the JSON is invalid, or if an unexpected error occurs.
37
+ """
38
+ try:
39
+ response = self.s3.get_object(Bucket=self.bucket, Key=key)
40
+ content = response["Body"].read().decode("utf-8")
41
+ return json.loads(content)
42
+ except ClientError as e:
43
+ pass
44
+ except json.JSONDecodeError:
45
+ print(f"[ERROR] Invalid JSON in S3 object: {key}")
46
+ except Exception as e:
47
+ print(f"[ERROR] Unexpected error: {e}")
48
+ return {}
49
+
50
+
51
+ def save_json(self, data: dict, key: str) -> None:
52
+ """
53
+ Saves a dictionary as a JSON file at the specified key or path.
54
+ Creates directories if they do not exist.
55
+
56
+ Args:
57
+ data (dict): Data to save.
58
+ key (str): Key or Path where the JSON file will be stored.
59
+ """
60
+ try:
61
+ self.s3.put_object(
62
+ Bucket=self.bucket,
63
+ Key=key,
64
+ Body=json.dumps(data, ensure_ascii=False, indent=4),
65
+ ContentType="application/json",
66
+ )
67
+ except Exception as e:
68
+ print(f"[ERROR] Saving JSON to S3: {e}")
69
+
70
+
71
+ def list_files(self, prefix: str, extension: str = ".json") -> List[str]:
72
+ """
73
+ Return a sorted list of files in the given folder that match the provided extension.
74
+ This function:
75
+ - Collects files pattern: `*{extension}`.
76
+ - Sorts results in ascending order by filename (lexicographic).
77
+ - Returns an empty list when no files match.
78
+ Parameters
79
+ prefix: str
80
+ Absolute or relative path to the target folder.
81
+ extension : str, optional
82
+ File extension to match (default: ".json"). The value is appended directly to
83
+ the glob pattern `*{extension}`; for reliable matching use a leading dot,
84
+ e.g., ".json", ".txt".
85
+ Returns
86
+ List[pathlib.Path]
87
+ A list of `Path` objects representing matching files. If the folder does not
88
+ exist or no files match, returns an empty list.
89
+ """
90
+ try:
91
+ response = self.s3.list_objects_v2(Bucket=self.bucket, Prefix=prefix)
92
+
93
+ if "Contents" not in response:
94
+ return []
95
+
96
+ return sorted(
97
+ obj["Key"]
98
+ for obj in response["Contents"]
99
+ if obj["Key"].endswith(extension)
100
+ )
101
+
102
+ except Exception as e:
103
+ print(f"[ERROR] Listing files in S3: {e}")
104
+ return []
@@ -0,0 +1,48 @@
1
+ import html
2
+ import json
3
+ import re
4
+
5
+
6
+ def compact_data(data: dict) -> str:
7
+ """
8
+ Removes keys with empty values (None, "", [], {}) from a dictionary
9
+ and returns a compact JSON string without unnecessary spaces.
10
+
11
+ Args:
12
+ data(dict): Original dictionary.
13
+ Returns:
14
+ str: Compact JSON representation of the filtered dictionary.
15
+ """
16
+ data = {k: v for k, v in data.items() if v not in (None, "", [], {})}
17
+ return json.dumps(data, separators=(",", ":"))
18
+
19
+ def clear_text(text: str) -> str:
20
+ """
21
+ Cleans text by removing HTML tags, Markdown links,
22
+ decoding HTML characters, removing escaped quotes, and
23
+ normalizing spaces.
24
+
25
+ Args:
26
+ text (str): Original text.
27
+ Returns:
28
+ str: Cleaned and simplified text.
29
+ """
30
+ if not text:
31
+ return ""
32
+
33
+ # Remove HTML tags
34
+ text = re.sub(r'<[^>]+>', '', text)
35
+
36
+ # Replace Markdown links [Text](URLxt
37
+ text = re.sub(r'\[([^\]]+)\]\([^)]+\)', r'\1', text)
38
+
39
+ # Decode HTML characters (&nbsp;, &lt;, etc.)
40
+ text = html.unescape(text)
41
+
42
+ # Remove escaped quotes
43
+ text = text.replace('\\"', '"').replace("\\'", "'")
44
+
45
+ # Remove line breaks, tabs, and multiple spaces
46
+ text = re.sub(r'\s+', ' ', text).strip()
47
+
48
+ return text
@@ -0,0 +1,187 @@
1
+ import re
2
+
3
+ from typing import Optional
4
+
5
+
6
+ def extract_issue_number(pr_body: str):
7
+ """
8
+ Extracts an issue number from a pull request body text.
9
+ Looks for a pattern like '#123' preceded by whitespace.
10
+
11
+ Args:
12
+ pr_body (str): The pull request body text.
13
+ Returns:
14
+ Optional[int]: The extracted issue number as an integer, or None if not found.
15
+ """
16
+ match = re.search(r"\s+#(\d+)", pr_body or "", re.IGNORECASE)
17
+ return int(match.group(1)) if match else None
18
+
19
+
20
+ def get_code(text: str) -> Optional[str]:
21
+ """
22
+ Extracts a code matching the pattern 'AW1234567' or 'NU1234567' from a string.
23
+ The code consists of two uppercase letters followed by seven digits.
24
+
25
+ Args:
26
+ text (str): The input string.
27
+ Returns:
28
+ Optional[str]: The extracted code or None if not found.
29
+ """
30
+ for tok in text.split('_'):
31
+ if re.fullmatch(r'[A-Z]{2}\d{7}', tok):
32
+ return tok
33
+ return None
34
+
35
+
36
+ def get_component_name(text: str) -> Optional[str]:
37
+ """
38
+ Extracts a component name from a string based on underscore-separated parts.
39
+ If the last part is 'dxp', returns the two preceding parts joined by underscore.
40
+ Otherwise, returns the last two parts.
41
+
42
+ Args:
43
+ text (str): The input string.
44
+ Returns:
45
+ Optional[str]: The component name or None if not enough parts.
46
+ """
47
+ parts = [p for p in text.strip('_').split('_') if p]
48
+ if len(parts) >= 2:
49
+ if parts[-1].lower() == "dxp":
50
+ return f"{parts[-3]}_{parts[-2]}"
51
+ return '_'.join(parts[-2:])
52
+ return None
53
+
54
+
55
+ def get_component_name_from_image(image: str, release_name: str) -> Optional[str]:
56
+ """
57
+ Extracts the component name from an image string.
58
+ If extraction fails, falls back to using release_name.
59
+
60
+ Args:
61
+ image (str): The image string (e.g., 'repo/component:tag').
62
+ release_name (str): The fallback release name.
63
+ Returns:
64
+ Optional[str]: The component name.
65
+ """
66
+ try:
67
+ tag = image.split('/')[-1]
68
+ repository_name = tag.split(':')[0]
69
+ return repository_name
70
+ except Exception:
71
+ return get_component_name(release_name)
72
+
73
+
74
+ def collect_all_variables(json_data, txt_variable_groups):
75
+ """
76
+ Collects all variables from a nested JSON structure.
77
+ Searches for keys named 'variables' and merges them into a single dictionary.
78
+
79
+ Args:
80
+ json_data (dict or list): The JSON data.
81
+ txt_variable_groups (str): The key name for variable groups.
82
+ Returns:
83
+ dict: A dictionary of all variables found.
84
+ """
85
+ all_variables = {}
86
+
87
+ def loop_through_json(data):
88
+ if isinstance(data, dict):
89
+ for key, value in data.items():
90
+ if key == 'variables':
91
+ all_variables.update(value)
92
+ elif key == txt_variable_groups:
93
+ if isinstance(value, list):
94
+ for group in value:
95
+ if isinstance(group, dict) and 'variables' in group:
96
+ all_variables.update(group['variables'])
97
+ else:
98
+ loop_through_json(value)
99
+ elif isinstance(data, list):
100
+ for item in data:
101
+ loop_through_json(item)
102
+
103
+ loop_through_json(json_data)
104
+ return all_variables
105
+
106
+
107
+ def resolve_value(value: str, all_variables: dict, visited=None) -> str:
108
+ """
109
+ Resolves variable references in a string recursively.
110
+ Variables are referenced using the format $(VAR_NAME).
111
+
112
+ Args:
113
+ value (str): The string containing variable references.
114
+ all_variables (dict): Dictionary of variables and their values.
115
+ visited (set): Set of visited variables to detect cycles.
116
+ Returns:
117
+ str: The resolved string with all references replaced.
118
+ """
119
+ if visited is None:
120
+ visited = set()
121
+
122
+ pattern = re.compile(r'\$\(([^)]+)\)')
123
+ while True:
124
+ matches = pattern.findall(value)
125
+ if not matches:
126
+ break
127
+ for match in matches:
128
+ if match in visited:
129
+ return f'$(CYCLE:{match})'
130
+ visited.add(match)
131
+ replacement = all_variables.get(match, {}).get('value', '')
132
+ resolved = resolve_value(replacement, all_variables, visited.copy())
133
+ value = value.replace(f'$({match})', resolved)
134
+ return value
135
+
136
+
137
+ def search_in_json(search_value: str, search_type: str, json_data, is_json_from_azure: bool = False) -> Optional[str]:
138
+ """
139
+ Searches for a variable in a nested JSON structure by key or value.
140
+ Resolves references if found.
141
+
142
+ Args:
143
+ search_value (str): The value to search for.
144
+ search_type (str): 'clave' to search by key, 'valor' to search by value.
145
+ json_data (dict or list): The JSON data.
146
+ is_json_from_azure (bool): Whether the JSON is from Azure (changes key names).
147
+ Returns:
148
+ Optional[str]: The resolved value if found, otherwise None.
149
+ """
150
+ txt_variable_groups = 'variableGroups' if is_json_from_azure else 'variable_groups'
151
+ search_value = search_value.lower()
152
+ all_variables = collect_all_variables(json_data, txt_variable_groups)
153
+
154
+ result_search = all_variables.get(search_value, {}).get('value', '')
155
+ if result_search and '$(' not in result_search:
156
+ return result_search
157
+
158
+ def recursive_search(data):
159
+ if isinstance(data, dict):
160
+ for key, value in data.items():
161
+ if key in ['variables', txt_variable_groups]:
162
+ if isinstance(value, dict):
163
+ for var_key, var_value in value.items():
164
+ if search_type == 'clave' and search_value == var_key.lower():
165
+ return resolve_value(var_value.get('value', ''), all_variables)
166
+ elif search_type == 'valor' and search_value == var_value.get('value', '').lower():
167
+ return resolve_value(var_value.get('value', ''), all_variables)
168
+ elif isinstance(value, list):
169
+ for item in value:
170
+ if isinstance(item, dict) and 'variables' in item:
171
+ for var_key, var_value in item['variables'].items():
172
+ if search_type == 'clave' and search_value == var_key.lower():
173
+ return resolve_value(var_value.get('value', ''), all_variables)
174
+ elif search_type == 'valor' and search_value == var_value.get('value', '').lower():
175
+ return resolve_value(var_value.get('value', ''), all_variables)
176
+ else:
177
+ result = recursive_search(value)
178
+ if result:
179
+ return result
180
+ elif isinstance(data, list):
181
+ for item in data:
182
+ result = recursive_search(item)
183
+ if result:
184
+ return result
185
+ return None
186
+
187
+ return recursive_search(json_data)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: commons_metrics
3
- Version: 0.0.16
3
+ Version: 0.0.18
4
4
  Summary: A simple library for basic statistical calculations
5
5
  Author: Bancolombia
6
6
  Author-email: omar.david.pino@email.com
@@ -0,0 +1,19 @@
1
+ commons_metrics/__init__.py,sha256=zLT4kmogcGARPf9DuuBv_r72fwAdvy5ff9y3CNAOnyk,434
2
+ commons_metrics/azure_devops_client.py,sha256=sD130ggzZWUsNqkBVNrLH80_AN00CxQ4preP9sBdzHM,16778
3
+ commons_metrics/cache_manager.py,sha256=ARZBqUCGzpNeQuU2ixd8gu_kqW4PYujnN8Ve9EdIG00,2249
4
+ commons_metrics/commons_repos_client.py,sha256=SYIe1fFXM3qlc_G154AmorHScPS3rTsztKqxy3dD28w,4150
5
+ commons_metrics/connection_database.py,sha256=JvmEuhnnaE3MyQJfN4nXZzVuXB-hfWUAbJq4DEKVtO8,680
6
+ commons_metrics/database.py,sha256=570TtLZ9psNzvIp75UFLYph34cKVEz6eGJgxXyRyjW4,1285
7
+ commons_metrics/date_utils.py,sha256=owKtefTWQ6zT-a3ssurPlphjfjGyxtwrZ_EuFkrPhf0,2265
8
+ commons_metrics/github_api_client.py,sha256=yPLsqn_KvQDIXcNp7Ma8qQwWy8svpXNLpDX69vwL9BU,11465
9
+ commons_metrics/repositories.py,sha256=4hSA51Ft1qcZvHtNta_QKSkHIe78JmsVlKcihzkBcb4,9476
10
+ commons_metrics/s3_file_manager.py,sha256=qyviGsLKuNWeEPtHpITfbL62RmPeuf5CZTzHBnAUS_U,3489
11
+ commons_metrics/text_simplifier.py,sha256=U0oVy2tpOOin5MbfK-9R3euy2P_SkR0b8chto-qy0_M,1257
12
+ commons_metrics/update_design_components.py,sha256=QpY0GCCCMjdYOZ7b8oNigU9iTpiGx91CYsyWwN8WVDA,7660
13
+ commons_metrics/util.py,sha256=98zuynalXumQRh-BB0Bcjyoh6vS2BTOUM8tVgr7iS9Q,1225
14
+ commons_metrics/variable_finder.py,sha256=fTy4njbvZcwBgRDc3MbKejUJkgJS2Mj1ByzRtvS9uV8,7101
15
+ commons_metrics-0.0.18.dist-info/licenses/LICENSE,sha256=jsHZ2Sh1wCL74HC25pDDGXCyQ0xgsTAy62FvEnehKIg,1067
16
+ commons_metrics-0.0.18.dist-info/METADATA,sha256=bRiZoXClNrqzTcwgjQI9ozHnd5mLofczOH9lv3Xk4AE,402
17
+ commons_metrics-0.0.18.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
18
+ commons_metrics-0.0.18.dist-info/top_level.txt,sha256=lheUN-3OKdU3A8Tg8Y-1IEB_9i_vVRA0g_FOiUsTQz8,16
19
+ commons_metrics-0.0.18.dist-info/RECORD,,
@@ -1,13 +0,0 @@
1
- commons_metrics/__init__.py,sha256=Ka0Uux56IH7Uikg7fh2aAUVRgJl82pulWscsWfi-LO4,434
2
- commons_metrics/azure_devops_client.py,sha256=sD130ggzZWUsNqkBVNrLH80_AN00CxQ4preP9sBdzHM,16778
3
- commons_metrics/commons_repos_client.py,sha256=SYIe1fFXM3qlc_G154AmorHScPS3rTsztKqxy3dD28w,4150
4
- commons_metrics/database.py,sha256=570TtLZ9psNzvIp75UFLYph34cKVEz6eGJgxXyRyjW4,1285
5
- commons_metrics/github_api_client.py,sha256=yPLsqn_KvQDIXcNp7Ma8qQwWy8svpXNLpDX69vwL9BU,11465
6
- commons_metrics/repositories.py,sha256=4hSA51Ft1qcZvHtNta_QKSkHIe78JmsVlKcihzkBcb4,9476
7
- commons_metrics/update_design_components.py,sha256=QpY0GCCCMjdYOZ7b8oNigU9iTpiGx91CYsyWwN8WVDA,7660
8
- commons_metrics/util.py,sha256=98zuynalXumQRh-BB0Bcjyoh6vS2BTOUM8tVgr7iS9Q,1225
9
- commons_metrics-0.0.16.dist-info/licenses/LICENSE,sha256=jsHZ2Sh1wCL74HC25pDDGXCyQ0xgsTAy62FvEnehKIg,1067
10
- commons_metrics-0.0.16.dist-info/METADATA,sha256=AoaNTjvaQG_q_tS7icKlYlFIZEuAn8aeL0-mERjZEOk,402
11
- commons_metrics-0.0.16.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
12
- commons_metrics-0.0.16.dist-info/top_level.txt,sha256=lheUN-3OKdU3A8Tg8Y-1IEB_9i_vVRA0g_FOiUsTQz8,16
13
- commons_metrics-0.0.16.dist-info/RECORD,,