ipulse-shared-core-ftredge 2.57__py3-none-any.whl → 3.1.1__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.

Potentially problematic release.


This version of ipulse-shared-core-ftredge might be problematic. Click here for more details.

Files changed (24) hide show
  1. ipulse_shared_core_ftredge/__init__.py +9 -18
  2. ipulse_shared_core_ftredge/models/__init__.py +0 -1
  3. ipulse_shared_core_ftredge/models/organisation.py +61 -55
  4. ipulse_shared_core_ftredge/models/resource_catalog_item.py +97 -171
  5. ipulse_shared_core_ftredge/utils/__init__.py +3 -0
  6. ipulse_shared_core_ftredge/utils/utils_common.py +10 -0
  7. {ipulse_shared_core_ftredge-2.57.dist-info → ipulse_shared_core_ftredge-3.1.1.dist-info}/METADATA +5 -7
  8. ipulse_shared_core_ftredge-3.1.1.dist-info/RECORD +15 -0
  9. {ipulse_shared_core_ftredge-2.57.dist-info → ipulse_shared_core_ftredge-3.1.1.dist-info}/WHEEL +1 -1
  10. ipulse_shared_core_ftredge/enums/__init__.py +0 -29
  11. ipulse_shared_core_ftredge/enums/enums_common_utils.py +0 -177
  12. ipulse_shared_core_ftredge/enums/enums_data_eng.py +0 -44
  13. ipulse_shared_core_ftredge/enums/enums_module_fincore.py +0 -58
  14. ipulse_shared_core_ftredge/enums/enums_modules.py +0 -33
  15. ipulse_shared_core_ftredge/models/audit_log_firestore.py +0 -12
  16. ipulse_shared_core_ftredge/models/pulse_enums.py +0 -196
  17. ipulse_shared_core_ftredge/utils_custom_logs.py +0 -201
  18. ipulse_shared_core_ftredge/utils_gcp.py +0 -314
  19. ipulse_shared_core_ftredge/utils_gcp_for_pipelines.py +0 -201
  20. ipulse_shared_core_ftredge/utils_pipelinemon.py +0 -362
  21. ipulse_shared_core_ftredge/utils_templates_and_schemas.py +0 -153
  22. ipulse_shared_core_ftredge-2.57.dist-info/RECORD +0 -25
  23. {ipulse_shared_core_ftredge-2.57.dist-info → ipulse_shared_core_ftredge-3.1.1.dist-info}/LICENCE +0 -0
  24. {ipulse_shared_core_ftredge-2.57.dist-info → ipulse_shared_core_ftredge-3.1.1.dist-info}/top_level.txt +0 -0
@@ -1,201 +0,0 @@
1
-
2
- # pylint: disable=missing-module-docstring
3
- # pylint: disable=missing-function-docstring
4
- # pylint: disable=logging-fstring-interpolation
5
- # pylint: disable=line-too-long
6
- # pylint: disable=missing-class-docstring
7
- # pylint: disable=broad-exception-caught
8
- import traceback
9
- import json
10
- from datetime import datetime, timezone
11
- from typing import List
12
- from ipulse_shared_core_ftredge.enums.enums_common_utils import LogLevel, LogStatus
13
-
14
-
15
- class ContextLog:
16
-
17
- def __init__(self, level: LogLevel, base_context: str = None, collector_id: str = None,
18
- context: str = None, description: str = None,
19
- e: Exception = None, e_type: str = None, e_message: str = None, e_traceback: str = None,
20
- log_status: LogStatus = LogStatus.OPEN, subject: str = None, systems_impacted: List[str] = None,
21
- ):
22
-
23
- if e is not None:
24
- e_type = type(e).__name__ if e_type is None else e_type
25
- e_message = str(e) if e_message is None else e_message
26
- e_traceback = traceback.format_exc() if e_traceback is None else e_traceback
27
- elif e_traceback is None and (e_type or e_message):
28
- e_traceback = traceback.format_exc()
29
-
30
- self.level = level
31
- self.subject = subject
32
- self.description = description
33
- self._base_context = base_context
34
- self._context = context
35
- self._systems_impacted = systems_impacted if systems_impacted else []
36
- self.collector_id = collector_id
37
- self.exception_type = e_type
38
- self.exception_message = e_message
39
- self.exception_traceback = e_traceback
40
- self.log_status = log_status
41
- self.timestamp = datetime.now(timezone.utc).isoformat()
42
-
43
- @property
44
- def base_context(self):
45
- return self._base_context
46
-
47
- @base_context.setter
48
- def base_context(self, value):
49
- self._base_context = value
50
-
51
- @property
52
- def context(self):
53
- return self._context
54
-
55
- @context.setter
56
- def context(self, value):
57
- self._context = value
58
-
59
- @property
60
- def systems_impacted(self):
61
- return self._systems_impacted
62
-
63
- @systems_impacted.setter
64
- def systems_impacted(self, list_of_si: List[str]):
65
- self._systems_impacted = list_of_si
66
-
67
- def add_system_impacted(self, system_impacted: str):
68
- if self._systems_impacted is None:
69
- self._systems_impacted = []
70
- self._systems_impacted.append(system_impacted)
71
-
72
- def remove_system_impacted(self, system_impacted: str):
73
- if self._systems_impacted is not None:
74
- self._systems_impacted.remove(system_impacted)
75
-
76
- def clear_systems_impacted(self):
77
- self._systems_impacted = []
78
-
79
- def _format_traceback(self, e_traceback, e_message, max_field_len:int, max_traceback_lines:int):
80
- if not e_traceback or e_traceback == 'None\n':
81
- return None
82
-
83
- traceback_lines = e_traceback.splitlines()
84
-
85
- # Check if the traceback is within the limits
86
- if len(traceback_lines) <= max_traceback_lines and len(e_traceback) <= max_field_len:
87
- return e_traceback
88
-
89
- # Remove lines that are part of the exception message if they are present in traceback
90
- message_lines = e_message.splitlines() if e_message else []
91
- if message_lines:
92
- for message_line in message_lines:
93
- if message_line in traceback_lines:
94
- traceback_lines.remove(message_line)
95
-
96
- # Filter out lines from third-party libraries (like site-packages)
97
- filtered_lines = [line for line in traceback_lines if "site-packages" not in line]
98
-
99
- # If filtering results in too few lines, revert to original traceback
100
- if len(filtered_lines) < 2:
101
- filtered_lines = traceback_lines
102
-
103
- # Combine standalone bracket lines with previous or next lines
104
- combined_lines = []
105
- for line in filtered_lines:
106
- if line.strip() in {"(", ")", "{", "}", "[", "]"} and combined_lines:
107
- combined_lines[-1] += " " + line.strip()
108
- else:
109
- combined_lines.append(line)
110
-
111
- # Ensure the number of lines doesn't exceed MAX_TRACEBACK_LINES
112
- if len(combined_lines) > max_traceback_lines:
113
- keep_lines_start = min(max_traceback_lines // 2, len(combined_lines))
114
- keep_lines_end = min(max_traceback_lines // 2, len(combined_lines) - keep_lines_start)
115
- combined_lines = (
116
- combined_lines[:keep_lines_start] +
117
- ['... (truncated) ...'] +
118
- combined_lines[-keep_lines_end:]
119
- )
120
-
121
- formatted_traceback = '\n'.join(combined_lines)
122
-
123
- # Ensure the total length doesn't exceed MAX_TRACEBACK_LENGTH
124
- if len(formatted_traceback) > max_field_len:
125
- truncated_length = max_field_len - len('... (truncated) ...')
126
- half_truncated_length = truncated_length // 2
127
- formatted_traceback = (
128
- formatted_traceback[:half_truncated_length] +
129
- '\n... (truncated) ...\n' +
130
- formatted_traceback[-half_truncated_length:]
131
- )
132
- return formatted_traceback
133
-
134
- def to_dict(self, max_field_len:int =10000, size_limit:float=256 * 1024 * 0.80,max_traceback_lines:int = 30):
135
- size_limit = int(size_limit) # Ensure size_limit is an integer
136
-
137
- # Unified list of all fields
138
- systems_impacted_str = f"{len(self.systems_impacted)} system(s): " + " ,,, ".join(self.systems_impacted) if self.systems_impacted else None
139
- fields = [
140
- ("log_status", str(self.log_status.name)),
141
- ("level_code", self.level.value),
142
- ("level_name", str(self.level.name)),
143
- ("base_context", str(self.base_context)),
144
- ("timestamp", str(self.timestamp)),
145
- ("collector_id", str(self.collector_id)),
146
- ("systems_impacted", systems_impacted_str),
147
- ("context", str(self.context)), # special sizing rules apply to it
148
- ("subject", str(self.subject)),
149
- ("description", str(self.description)),
150
- ("exception_type", str(self.exception_type)),
151
- ("exception_message", str(self.exception_message)),
152
- ("exception_traceback", str(self._format_traceback(self.exception_traceback,self.exception_message, max_field_len, max_traceback_lines)))
153
- ]
154
-
155
- # Function to calculate the byte size of a JSON-encoded field
156
- def field_size(key, value):
157
- return len(json.dumps({key: value}).encode('utf-8'))
158
-
159
- # Function to truncate a value based on its type
160
- # Function to truncate a value based on its type
161
- def truncate_value(value, max_size):
162
- if isinstance(value, str):
163
- half_size = max_size // 2
164
- return value[:half_size] + '...' + value[-(max_size - half_size - 3):]
165
- return value
166
-
167
- # Ensure no field exceeds max_field_len
168
- for i, (key, value) in enumerate(fields):
169
- if isinstance(value, str) and len(value) > max_field_len:
170
- fields[i] = (key, truncate_value(value, max_field_len))
171
-
172
- # Ensure total size of the dict doesn't exceed size_limit
173
- total_size = sum(field_size(key, value) for key, value in fields)
174
- log_dict = {}
175
- truncated = False
176
-
177
- if total_size > size_limit:
178
- truncated = True
179
- remaining_size = size_limit
180
- remaining_fields = len(fields)
181
-
182
- for key, value in fields:
183
- if remaining_fields > 0:
184
- max_size_per_field = remaining_size // remaining_fields
185
- else:
186
- max_size_per_field = 0
187
-
188
- field_sz = field_size(key, value)
189
- if field_sz > max_size_per_field:
190
- value = truncate_value(value, max_size_per_field)
191
- field_sz = field_size(key, value)
192
-
193
- log_dict[key] = value
194
- remaining_size -= field_sz
195
- remaining_fields -= 1
196
- else:
197
- log_dict = dict(fields)
198
-
199
- log_dict['trunc'] = truncated
200
-
201
- return log_dict
@@ -1,314 +0,0 @@
1
- # pylint: disable=missing-module-docstring
2
- # pylint: disable=missing-function-docstring
3
- # pylint: disable=missing-class-docstring
4
- # pylint: disable=broad-exception-caught
5
- # pylint: disable=line-too-long
6
- # pylint: disable=unused-variable
7
- import json
8
- import csv
9
- from io import StringIO
10
- import logging
11
- import os
12
- import time
13
- import traceback
14
- from google.cloud import error_reporting, logging as cloud_logging
15
- from google.api_core.exceptions import NotFound
16
-
17
- ############################################################################
18
- ##################### SETTING UP LOGGER ##########################
19
-
20
- ####DEPCREACATED: THIS APPROACH WAS GOOD, BUT ERRORS WERE NOT REPORTED TO ERROR REPORTING
21
- # logging.basicConfig(level=logging.INFO)
22
- # logging_client = google.cloud.logging.Client()
23
- # logging_client.setup_logging()
24
- ###################################
25
-
26
-
27
- ##### THIS APPROACH IS USED NOW ########
28
- ENV = os.getenv('ENV', 'LOCAL').strip("'")
29
-
30
- def setup_gcp_logger_and_error_report(logger_name,level=logging.INFO, use_cloud_logging=True):
31
- """Sets up a logger with Error Reporting and Cloud Logging handlers.
32
-
33
- Args:
34
- logger_name: The name of the logger.
35
-
36
- Returns:
37
- logging.Logger: The configured logger instance.
38
- """
39
-
40
- class ErrorReportingHandler(logging.Handler):
41
- def __init__(self, level=logging.ERROR):
42
- super().__init__(level)
43
- self.error_client = error_reporting.Client()
44
- self.propagate = True
45
-
46
- def emit(self, record):
47
- try:
48
- if record.levelno >= logging.ERROR:
49
- message = self.format(record)
50
- if record.exc_info:
51
- message += "\n" + ''.join(traceback.format_exception(*record.exc_info))
52
- if hasattr(record, 'pathname') and hasattr(record, 'lineno'):
53
- message += f"\nFile: {record.pathname}, Line: {record.lineno}"
54
- self.error_client.report(message)
55
- except Exception as e:
56
- # Ensure no exceptions are raised during logging
57
- self.handleError(record)
58
-
59
- logger = logging.getLogger(logger_name)
60
- logger.setLevel(level)
61
-
62
- # Add a console handler for local development
63
- if ENV == "LOCAL" or not use_cloud_logging:
64
- formatter = logging.Formatter('%(levelname)s : %(name)s : %(asctime)s : %(message)s')
65
- console_handler = logging.StreamHandler()
66
- console_handler.setFormatter(formatter)
67
- logger.addHandler(console_handler)
68
-
69
- if use_cloud_logging:
70
- # Create Error Reporting handler
71
- error_reporting_handler = ErrorReportingHandler()
72
-
73
- # Create Google Cloud Logging handler
74
- cloud_logging_client = cloud_logging.Client()
75
- cloud_logging_handler = cloud_logging_client.get_default_handler()
76
-
77
- # Add handlers to the logger
78
- logger.addHandler(error_reporting_handler)
79
- logger.addHandler(cloud_logging_handler)
80
- return logger
81
- ############################################################################
82
-
83
-
84
- ############################################################################
85
- ##################### GOOGLE CLOUD STORAGE UTILS ##########################
86
-
87
- def read_json_from_gcs(bucket_name, file_name, stor_client, logger):
88
- """ Helper function to read a JSON file from Google Cloud Storage """
89
- try:
90
- bucket = stor_client.bucket(bucket_name)
91
- blob = bucket.blob(file_name)
92
- data_string = blob.download_as_text()
93
- data = json.loads(data_string)
94
- return data
95
- except NotFound:
96
- logger.error(f"Error: The file {file_name} was not found in the bucket {bucket_name}.")
97
- return None
98
- except json.JSONDecodeError:
99
- logger.error(f"Error: The file {file_name} could not be decoded as JSON.")
100
- return None
101
- except Exception as e:
102
- logger.error(f"An unexpected error occurred: {e}", exc_info=True)
103
- return None
104
-
105
- def read_csv_from_gcs(bucket_name, file_name, storage_client, logger):
106
- """ Helper function to read a CSV file from Google Cloud Storage """
107
- try:
108
- bucket = storage_client.bucket(bucket_name)
109
- blob = bucket.blob(file_name)
110
- data_string = blob.download_as_text()
111
- data_file = StringIO(data_string)
112
- reader = csv.DictReader(data_file)
113
- return list(reader)
114
- except NotFound:
115
- logger.error(f"Error: The file {file_name} was not found in the bucket {bucket_name}.")
116
- return None
117
- except csv.Error:
118
- logger.error(f"Error: The file {file_name} could not be read as CSV.")
119
- return None
120
- except Exception as e:
121
- logger.error(f"An unexpected error occurred: {e}", exc_info=True)
122
- return None
123
-
124
-
125
-
126
- def write_json_to_gcs( storage_client, data, bucket_name, file_name,
127
- file_exists_if_starts_with_prefix=None, overwrite_if_exists=False, increment_if_exists=False,
128
- save_locally=False, local_path=None, max_retries=2, max_deletable_files=1, logger=None):
129
- """Saves data to Google Cloud Storage and optionally locally.
130
-
131
- This function attempts to upload data to GCS.
132
- - If the upload fails after retries and `save_locally` is True or `local_path` is provided, it attempts to save the data locally.
133
- - It handles file name conflicts based on these rules:
134
- - If `overwrite_if_exists` is True:
135
- - If `file_exists_if_contains_substr` is provided, ANY existing file containing the substring is deleted, and the new file is saved with the provided `file_name`.
136
- - If `file_exists_if_contains_substr` is None, and a file with the exact `file_name` exists, it's overwritten.
137
- - If `increment_if_exists` is True:
138
- - If `file_exists_if_contains_substr` is provided, a new file with an incremented version is created ONLY if a file with the EXACT `file_name` exists.
139
- - If `file_exists_if_contains_substr` is None, a new file with an incremented version is created if a file with the exact `file_name` exists.
140
-
141
- -If both overwrite_if_exists and increment_if_exists are provided as Ture, an exception will be raised.
142
- """
143
-
144
- def log_message(message):
145
- if logger:
146
- logger.info(message)
147
-
148
- def log_error(message, exc_info=False):
149
- if logger:
150
- logger.error(message, exc_info=exc_info)
151
-
152
- def log_warning(message):
153
- if logger:
154
- logger.warning(message)
155
-
156
- # Input validation
157
- if overwrite_if_exists and increment_if_exists:
158
- raise ValueError("Both 'overwrite_if_exists' and 'increment_if_exists' cannot be True simultaneously.")
159
- if not isinstance(data, (list, dict, str)):
160
- raise ValueError("Unsupported data type. Data must be a list, dict, or str.")
161
- if max_deletable_files > 10:
162
- raise ValueError("max_deletable_files should be less than 10 for safety. For more use another method.")
163
-
164
- # Prepare data
165
- if isinstance(data, (list, dict)):
166
- data_str = json.dumps(data, indent=2)
167
- else:
168
- data_str = data
169
-
170
- bucket = storage_client.bucket(bucket_name)
171
- base_file_name, ext = os.path.splitext(file_name)
172
- increment = 0
173
- attempts = 0
174
- success = False
175
-
176
- # GCS-related metadata
177
- gcs_path = None
178
- gcs_file_overwritten = False
179
- gcs_file_already_exists = False
180
- gcs_file_saved_with_increment = False
181
- gcs_file_exists_checked_on_name = file_name
182
- gcs_deleted_files=[]
183
-
184
- # GCS upload exception
185
- gcs_upload_exception = None
186
-
187
- # Local file path
188
- local_path_final = None
189
-
190
- try:
191
- # --- Overwrite Logic ---
192
- if overwrite_if_exists:
193
- if file_exists_if_starts_with_prefix:
194
- gcs_file_exists_checked_on_name = file_exists_if_starts_with_prefix
195
- blobs_to_delete = list(bucket.list_blobs(prefix=file_exists_if_starts_with_prefix))
196
- if len(blobs_to_delete) > max_deletable_files:
197
- raise Exception(f"Error: Attempt to delete {len(blobs_to_delete)} matched files, but limit is {max_deletable_files}.")
198
- if blobs_to_delete:
199
- log_message(f"Deleting files containing '{file_exists_if_starts_with_prefix}' for overwrite.")
200
- for blob in blobs_to_delete:
201
- blob.delete()
202
- gcs_deleted_files.append(blob.name)
203
- log_message(f"Deleted: gs://{bucket_name}/{blob.name}")
204
- gcs_file_overwritten = True
205
- else:
206
- blob = bucket.blob(file_name)
207
- if blob.exists():
208
- gcs_file_already_exists = True
209
- gcs_path = f"gs://{bucket_name}/{file_name}"
210
- log_message(f"File '{file_name}' already exists. Overwriting.")
211
- blob.delete() # Delete the existing blob
212
- gcs_deleted_files.append(blob.name)
213
- gcs_file_overwritten = True
214
-
215
- # --- Increment Logic ---
216
- elif increment_if_exists:
217
- gcs_file_exists_checked_on_name = file_name # We only increment if the exact name exists
218
- while bucket.blob(file_name).exists():
219
- gcs_file_already_exists = True
220
- increment += 1
221
- file_name = f"{base_file_name}_v{increment}{ext}"
222
- gcs_file_saved_with_increment = True
223
- log_warning(f"File already exists. Using incremented name: {file_name}")
224
-
225
- # --- GCS Upload ---
226
- if overwrite_if_exists or increment_if_exists: # Only upload if either overwrite or increment is True
227
- while attempts < max_retries and not success:
228
- try:
229
- blob = bucket.blob(file_name) # Use the potentially updated file_name
230
- blob.upload_from_string(data_str, content_type='application/json')
231
- gcs_path = f"gs://{bucket_name}/{file_name}"
232
- log_message(f"Successfully saved file to GCS: {gcs_path}")
233
- success = True
234
- except Exception as e:
235
- gcs_upload_exception=e
236
- attempts += 1
237
- if attempts < max_retries:
238
- log_warning(f"Attempt {attempts} to upload to GCS failed. Retrying...")
239
- time.sleep(2 ** attempts)
240
- else:
241
- log_error(f"Failed to write '{file_name}' to GCS bucket '{bucket_name}' after {max_retries} attempts: {e}", exc_info=True)
242
- if save_locally or local_path:
243
- log_message(f"Attempting to save '{file_name}' locally due to GCS upload failure.")
244
- except Exception as e:
245
- log_error(f"Error during GCS operations: {e}", exc_info=True)
246
- gcs_upload_exception = e
247
-
248
- # --- Save Locally ---
249
- write_out=False
250
- if not success or save_locally or local_path:
251
- try:
252
- local_path=local_path if local_path else "/tmp"
253
- local_path_final = os.path.join(local_path, file_name)
254
-
255
- if os.path.exists(local_path_final):
256
- if increment_if_exists:
257
- increment = 0
258
- while os.path.exists(local_path_final):
259
- increment += 1
260
- local_path_final = os.path.join(local_path, f"{base_file_name}_v{increment}{ext}")
261
- log_warning(f"Local file already exists. Using incremented name: {local_path_final}")
262
- write_out=True
263
- elif overwrite_if_exists:
264
- write_out=True
265
- log_message(f"File {file_name} already exists locally at {local_path_final}. Overwriting: {overwrite_if_exists}")
266
- else:
267
- log_message(f"File {file_name} already exists locally at {local_path_final} and overwrite is set to False. Skipping save.")
268
- write_out=False
269
- else:
270
- write_out=True
271
-
272
- if write_out:
273
- with open(local_path_final, 'w', encoding='utf-8') as f:
274
- f.write(data_str)
275
- log_message(f"Saved {file_name} locally at {local_path_final}. Overwritten: {overwrite_if_exists}")
276
-
277
- except Exception as local_e:
278
- log_error(f"Failed to write {file_name} locally: {local_e}", exc_info=True)
279
-
280
- if gcs_upload_exception is not None:
281
- raise gcs_upload_exception # Propagate without nesting
282
-
283
- # --- Return Metadata ---
284
- return {
285
- "gcs_path": gcs_path if success else None, # Only set gcs_path if upload succeeded
286
- "local_path": local_path_final if write_out else None, # Only set local_path if saved locally
287
- "gcs_file_already_exists": gcs_file_already_exists,
288
- "gcs_file_exists_checked_on_name":gcs_file_exists_checked_on_name ,
289
- "gcs_file_overwritten": gcs_file_overwritten,
290
- "gcs_deleted_file_names": ",,,".join(gcs_deleted_files) if gcs_deleted_files else None,
291
- "gcs_file_saved_with_increment": gcs_file_saved_with_increment
292
- }
293
-
294
-
295
- def write_csv_to_gcs(bucket_name, file_name, data, storage_client, logger,log_info_verbose=True):
296
- """ Helper function to write a CSV file to Google Cloud Storage """
297
- try:
298
- bucket = storage_client.bucket(bucket_name)
299
- blob = bucket.blob(file_name)
300
- data_file = StringIO()
301
- if data and isinstance(data, list) and isinstance(data[0], dict):
302
- fieldnames = data[0].keys()
303
- writer = csv.DictWriter(data_file, fieldnames=fieldnames)
304
- writer.writeheader()
305
- writer.writerows(data)
306
- else:
307
- raise ValueError("Data should be a list of dictionaries")
308
- blob.upload_from_string(data_file.getvalue(), content_type='text/csv')
309
- if log_info_verbose:
310
- logger.info(f"Successfully wrote CSV to {file_name} in bucket {bucket_name}.")
311
- except ValueError as e:
312
- logger.error(f"ValueError: {e}")
313
- except Exception as e:
314
- logger.error(f"An unexpected error occurred while writing CSV to GCS: {e}", exc_info=True)