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

@@ -2,10 +2,10 @@ from .models import (Organisation, UserAuth, UserProfile,
2
2
  UserStatus, UserProfileUpdate, pulse_enums)
3
3
  from .utils_gcp import (setup_gcp_logger_and_error_report,
4
4
  read_csv_from_gcs, read_json_from_gcs,
5
- write_csv_to_gcs, write_json_to_gcs)
5
+ write_csv_to_gcs, write_data_to_gcs)
6
6
  from .utils_templates_and_schemas import (create_bigquery_schema_from_json,
7
7
  update_check_with_schema_template)
8
- from .utils_common import (create_custom_notice, create_exception_notice)
8
+ from .utils_common import (Notice, NoticeSeverity, NoticesManager,SuccessLog, SuccessLogManager)
9
9
 
10
10
  from .enums import (NoticeSeverity, Unit, Frequency,
11
11
  Module, SubModule, BaseDataCategory,
@@ -13,5 +13,3 @@ from .enums import (NoticeSeverity, Unit, Frequency,
13
13
  FinCoreRecordsCategory, ExchangeOrPublisher,
14
14
  SourcingPipelineType, SourcingTriggerType,
15
15
  DWEvent, DWEventTriggerType)
16
-
17
-
@@ -17,20 +17,31 @@ class NoticeSeverity(Enum):
17
17
 
18
18
  # Warnings indicate potential issues that might require attention:
19
19
  WARNING_NO_ACTION = 401 # Minor issue or Unexpected Behavior, no immediate action required (can be logged frequently)
20
- WARNING_ACTION_RECOMMENDED = 402 # Action recommended to prevent potential future issues
21
- WARNING_ACTION_REQUIRED = 403 # Action required, pipeline can likely continue
20
+ WARNING_REVIEW_RECOMMENDED = 402 # Action recommended to prevent potential future issues
21
+ WARNING_FIX_RECOMMENDED = 403 # Action recommended to prevent potential future issues
22
+ WARNING_FIX_REQUIRED = 404 # Action required, pipeline can likely continue
22
23
 
23
24
  # Errors indicate a problem that disrupts normal pipeline execution:
24
- ERROR_TRANSIENT_RETRY = 501 # Temporary error, automatic retry likely to succeed
25
- ERROR_DATA_ISSUE_ISOLATED = 502 # Error likely caused by data issues, manual intervention likely needed
26
- ERROR_DATA_ISSUE_WITH_DEPENDENCIES = 503 # Error likely in code/configuration, investigation required
27
- ERROR_CONFIG_OR_CODE_ISSUE = 504 # Error likely in code/configuration, investigation required
28
- ERROR_UNKNOWN_EXCEPTION = 505
25
+ ERROR_EXCEPTION_REDO = 502
26
+ ERROR_CUSTOM_REDO = 503 # Temporary error, automatic retry likely to succeed
27
+
28
+
29
+ ERROR_EXCEPTION_INVESTIGATE = 601 # Exception occured after some data was likely persisted (e.g., to GCS or BQ)
30
+ ERROR_CUSTOM_INVESTIGATE= 602
31
+ ERROR_EXCEPTION_PERSTISTANCE = 603 # Exception occured after data was persisted (e.g., to GCS or BQ)
32
+ ERROR_CUSTOM_PERSTISTANCE = 604
29
33
 
30
34
  # Critical errors indicate severe failures requiring immediate attention:
31
- CRITICAL_SYSTEM_FAILURE = 601 # System-level failure (e.g., infrastructure), requires immediate action
32
- CRITICAL_PIPELINE_FAILURE = 602 # Complete pipeline failure, requires investigation and potential rollback
35
+ CRITICAL_SYSTEM_FAILURE = 701 # System-level failure (e.g., infrastructure), requires immediate action
36
+ CRITICAL_PIPELINE_FAILURE = 702 # Complete pipeline failure, requires investigation and potential rollback
37
+
38
+ UNKNOWN=1001 # Unknown error, should not be used in normal operation
33
39
 
40
+ ### Exception during full exection, partially saved
41
+ # Exception during ensemble pipeline; modifications collected in local object , nothing persisted
42
+ # Exception during ensemble pipeline; modifications persisted , metadata failed
43
+ # Exception during ensemble pipeline; modifications persisted , metadata persisted
44
+ # Exception during ensemble pipeline; modifications persisted , metadata persisted
34
45
 
35
46
 
36
47
  class Unit(Enum):
@@ -20,25 +20,24 @@ class SourcingPipelineType(Enum):
20
20
  CLOUD_GET_API_INMEMORY = "cloud_get_api_inmemory"
21
21
 
22
22
  class DWEventTriggerType(Enum):
23
- GCS_BUCKET_UPLOAD = "gcs_bucket_upload"
23
+ GCS_UPLOAD_TRIGGER_CF = "gcs_upload_trigger_cf"
24
+ HTTP_TRIGGER_CF_FOR_GCS_FILE = "http_trigger_cf_for_gcs_file"
25
+ PUBSUB_TRIGGER_CF_FOR_GCS_FILE = "pubsub_trigger_cf_for_gcs_file"
26
+ LOCAL_SCRIPT_FOR_GCS_FILE = "local_script_for_gcs_file"
24
27
  INSIDE_SOURCING_FUNCTION = "inside_sourcing_function"
25
- HTTP_FUNC_TO_GCS = "http_func_to_gcs"
26
- LOCAL_FROM_GCS_FILE = "local_from_gcs_file"
27
- MANUAL_FROM_LOCAL_FILE = "manual_from_local_file"
28
- PUBSUBC_TOPIC = "pubsubc_topic"
29
28
 
30
29
  class DWEvent(Enum):
31
- INSERT_NOREPLACE_1A_NT = "insert_noreplace_1a_nt"
32
- MERGE_NOREPLACE_NA_1T = "merge_noreplace_na_1t"
33
- MERGE_NOREPLACE_NA_NT = "merge_noreplace_na_nt"
34
- INSERT_NOREPLACE_1A_1T = "insert_noreplace_1a_1t"
35
- MERGE_NOREPLACE_1A_NT = "merge_noreplace_1a_nt"
36
- INSERT_REPLACE_1A_1T = "insert_replace_1a_1t"
37
- INSERT_REPLACE_1A_NT = "insert_replace_1a_nt"
38
- MERGE_REPLACE_NA_NT = "merge_replace_na_nt"
39
- MERGE_REPLACE_1A_NT = "merge_replace_1a_nt"
40
- MERGE_REPLACE_NA_1T = "merge_replace_na_1t"
41
- DELETE_1A_1T = "delete_1a_1t"
42
- DELETE_1A_NT = "delete_1a_nt"
43
- DELETE_NA_1T = "delete_na_1t"
44
- DELETE_NA_NT = "delete_na_nt"
30
+ INSERT_NOREPLACE_1O_NT = "insert_noreplace_1o_nt"
31
+ MERGE_NOREPLACE_NO_1T = "merge_noreplace_no_1t"
32
+ MERGE_NOREPLACE_NO_NT = "merge_noreplace_no_nt"
33
+ INSERT_NOREPLACE_1O_1T = "insert_noreplace_1o_1t"
34
+ MERGE_NOREPLACE_1O_NT = "merge_noreplace_1o_nt"
35
+ INSERT_REPLACE_1O_1T = "insert_replace_1o_1t"
36
+ INSERT_REPLACE_1O_NT = "insert_replace_1o_nt"
37
+ MERGE_REPLACE_NO_NT = "merge_replace_no_nt"
38
+ MERGE_REPLACE_1O_NT = "merge_replace_1o_nt"
39
+ MERGE_REPLACE_NO_1T = "merge_replace_no_1t"
40
+ DELETE_1O_1T = "delete_1o_1t"
41
+ DELETE_1O_NT = "delete_1o_nt"
42
+ DELETE_NO_1T = "delete_no_1t"
43
+ DELETE_NO_NT = "delete_no_nt"
@@ -2,23 +2,414 @@
2
2
  # pylint: disable=missing-function-docstring
3
3
  # pylint: disable=logging-fstring-interpolation
4
4
  # pylint: disable=line-too-long
5
+ import traceback
6
+ import json
7
+ import os
8
+ import time
9
+ from datetime import datetime, timezone
10
+ from contextlib import contextmanager
11
+ from typing import List
12
+ from ipulse_shared_core_ftredge.enums.enums_common_utils import NoticeSeverity
13
+ from ipulse_shared_core_ftredge.utils_gcp import write_data_to_gcs
5
14
 
6
- def create_custom_notice(severity, subject, message):
7
- return {
8
- "severity_code": severity.value,
9
- "severity_name": severity.name,
10
- "subject": subject,
11
- "message": message
12
- }
15
+ def create_notice(severity, e=None, e_type=None, e_message=None, e_traceback=None, subject=None, message=None,context=None):
16
+ # Validate input: ensure severity is provided, use a default if not
17
+ if severity is None:
18
+ severity = NoticeSeverity.UNKNOWN # Assume Severity.UNKNOWN is a default fallback
13
19
 
20
+ # If an exception object is provided, use it to extract details
21
+ if e is not None:
22
+ e_type = type(e).__name__ if e_type is None else e_type
23
+ e_message = str(e) if e_message is None else e_message
24
+ e_traceback = traceback.format_exc() if e_traceback is None else e_traceback
25
+ else:
26
+ # Calculate traceback if not provided and if exception details are partially present
27
+ if e_traceback is None and (e_type or e_message):
28
+ e_traceback = traceback.format_exc()
14
29
 
15
- def create_exception_notice(severity, exception_code,exception_message, exception_traceback=None, subject=None, message=None):
16
- return {
30
+ # Prepare the base notice dictionary with all fields
31
+ notice = {
17
32
  "severity_code": severity.value,
18
33
  "severity_name": severity.name,
19
34
  "subject": subject,
20
35
  "message": message,
21
- "exception_code": exception_code,
22
- "exception_message": exception_message,
23
- "exception_traceback": exception_traceback
24
- }
36
+ "exception_code": e_type,
37
+ "exception_message": e_message,
38
+ "exception_traceback": e_traceback or None, # Ensure field is present even if traceback isn't calculated
39
+ "context": context or ""
40
+ }
41
+ return notice
42
+
43
+
44
+
45
+
46
+ def merge_notices_dicts(dict1, dict2):
47
+ """
48
+ Merge two dictionaries of lists, combining lists for overlapping keys.
49
+
50
+ Parameters:
51
+ dict1 (dict): The first dictionary of lists.
52
+ dict2 (dict): The second dictionary of lists.
53
+
54
+ Returns:
55
+ dict: A new dictionary with combined lists for overlapping keys.
56
+ """
57
+ merged_dict = {}
58
+
59
+ # Get all unique keys from both dictionaries
60
+ all_keys = set(dict1) | set(dict2)
61
+
62
+ for key in all_keys:
63
+ # Combine lists from both dictionaries for each key
64
+ merged_dict[key] = dict1.get(key, []) + dict2.get(key, [])
65
+
66
+ return merged_dict
67
+
68
+
69
+ # ["data_import","data_quality", "data_processing","data_general","data_persistance","metadata_quality", "metadata_processing", "metadata_persistance","metadata_general"]
70
+
71
+ class Notice:
72
+ def __init__(self, severity: NoticeSeverity, e: Exception = None, e_type: str = None, e_message: str = None, e_traceback: str = None, subject: str = None, message: str = None, context: str = None):
73
+
74
+ # If an exception object is provided, use it to extract details
75
+ if e is not None:
76
+ e_type = type(e).__name__ if e_type is None else e_type
77
+ e_message = str(e) if e_message is None else e_message
78
+ e_traceback = traceback.format_exc() if e_traceback is None else e_traceback
79
+ # If exception details are provided but not from an exception object
80
+ elif e_traceback is None and (e_type or e_message):
81
+ e_traceback = traceback.format_exc()
82
+
83
+ self.timestamp = datetime.now(timezone.utc).isoformat()
84
+ self.severity = severity
85
+ self.subject = subject
86
+ self.message = message
87
+ self.context = context
88
+ self.exception_type = e_type
89
+ self.exception_message = e_message
90
+ self.exception_traceback = e_traceback
91
+
92
+ def to_dict(self):
93
+ return {
94
+ "context": self.context,
95
+ "severity_code": self.severity.value,
96
+ "severity_name": self.severity.name,
97
+ "subject": self.subject,
98
+ "message": self.message,
99
+ "exception_type": self.exception_type,
100
+ "exception_message": self.exception_message,
101
+ "exception_traceback": self.exception_traceback,
102
+ }
103
+
104
+ class NoticesManager:
105
+ ERROR_CODE_START_VALUE = 500
106
+
107
+ def __init__(self):
108
+ self.notices = []
109
+ self.error_count = 0
110
+ self.severity_counts = {severity.name: 0 for severity in NoticeSeverity}
111
+ self.context_stack = []
112
+
113
+ @contextmanager
114
+ def notice_context(self, context):
115
+ self.push_context(context)
116
+ try:
117
+ yield
118
+ finally:
119
+ self.pop_context()
120
+
121
+ def push_context(self, context):
122
+ self.context_stack.append(context)
123
+
124
+ def pop_context(self):
125
+ if self.context_stack:
126
+ self.context_stack.pop()
127
+
128
+ def get_notices_by_context(self, context_substring: str):
129
+ return [
130
+ notice for notice in self.notices
131
+ if context_substring in notice["context"]
132
+ ]
133
+
134
+ def get_current_context(self):
135
+ return " >> ".join(self.context_stack)
136
+
137
+ def get_all_notices(self):
138
+ return self.notices
139
+ def add_notice(self, notice: Notice):
140
+ notice.context = self.get_current_context()
141
+ notice_dict = notice.to_dict()
142
+ self.notices.append(notice_dict)
143
+ self._update_counts(notice_dict)
144
+
145
+ def add_notices(self, notices: List[Notice]):
146
+ for notice in notices:
147
+ notice.context = self.get_current_context()
148
+ notice_dict = notice.to_dict()
149
+ self.notices.append(notice_dict)
150
+ self._update_counts(notice_dict)
151
+
152
+ def remove_notice(self, notice: Notice):
153
+ notice_dict = notice.to_dict()
154
+ if notice_dict in self.notices:
155
+ self.notices.remove(notice_dict)
156
+ self._update_counts(notice_dict, remove=True)
157
+
158
+ def clear_notices(self):
159
+ self.notices = []
160
+ self.error_count = 0
161
+ self.severity_counts = {severity.name: 0 for severity in NoticeSeverity}
162
+
163
+ def contains_errors(self):
164
+ return self.error_count > 0
165
+
166
+ def count_errors(self):
167
+ return self.error_count
168
+
169
+ def count_notices_by_severity(self, severity: NoticeSeverity):
170
+ return self.severity_counts.get(severity.name, 0)
171
+
172
+ def count_errors_for_current_context(self):
173
+ current_context = self.get_current_context()
174
+ return sum(
175
+ 1 for notice in self.notices
176
+ if notice["context"] == current_context and notice["severity_code"] >= self.ERROR_CODE_START_VALUE
177
+ )
178
+ def count_all_notices(self):
179
+ return len(self.notices)
180
+
181
+ def count_notices_for_current_context(self):
182
+ current_context = self.get_current_context()
183
+ return sum(
184
+ 1 for notice in self.notices
185
+ if notice["context"] == current_context
186
+ )
187
+
188
+ def count_notices_by_severity_for_current_context(self, severity: NoticeSeverity):
189
+ current_context = self.get_current_context()
190
+ return sum(
191
+ 1 for notice in self.notices
192
+ if notice["context"] == current_context and notice["severity_code"] == severity.value
193
+ )
194
+ def count_notices_for_current_and_nested_contexts(self):
195
+ current_context = self.get_current_context()
196
+ return sum(
197
+ 1 for notice in self.notices
198
+ if current_context in notice["context"]
199
+ )
200
+ def count_errors_for_current_and_nested_contexts(self):
201
+ current_context = self.get_current_context()
202
+ return sum(
203
+ 1 for notice in self.notices
204
+ if current_context in notice["context"] and notice["severity_code"] >= self.ERROR_CODE_START_VALUE
205
+ )
206
+ def count_notices_by_severity_for_current_and_nested_contexts(self, severity: NoticeSeverity):
207
+ current_context = self.get_current_context()
208
+ return sum(
209
+ 1 for notice in self.notices
210
+ if current_context in notice["context"] and notice["severity_code"] == severity.value
211
+ )
212
+
213
+ def export_notices_to_gcs_file(self, bucket_name, storage_client, file_name=None, top_level_context=None, save_locally=False, local_path=None, logger=None, max_retries=2):
214
+ def log_message(message):
215
+ if logger:
216
+ logger.info(message)
217
+
218
+ def log_error(message, exc_info=False):
219
+ if logger:
220
+ logger.error(message, exc_info=exc_info)
221
+
222
+ if not file_name:
223
+ timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
224
+ if top_level_context:
225
+ file_name = f"notices_{timestamp}_{top_level_context}_len{len(self.notices)}.json"
226
+ else:
227
+ file_name = f"notices_{timestamp}_len{len(self.notices)}.json"
228
+
229
+ cloud_path = None # Initialize cloud_path here
230
+ local_path = None # Initialize local_path here
231
+ try:
232
+ cloud_path, local_path = write_data_to_gcs(
233
+ bucket_name=bucket_name,
234
+ storage_client=storage_client,
235
+ data=self.notices,
236
+ file_name=file_name,
237
+ save_locally=save_locally,
238
+ local_path=local_path,
239
+ logger=logger,
240
+ max_retries=max_retries
241
+ )
242
+ log_message(f"Notices successfully saved to GCS at {cloud_path} and locally at {local_path}.")
243
+ except Exception as e:
244
+ log_error(f"Failed to export notices: {type(e).__name__} - {str(e)}", exc_info=True)
245
+
246
+ return cloud_path , local_path
247
+
248
+ def import_notices_from_json(self, json_or_file, logger=None):
249
+ def log_message(message):
250
+ if logger:
251
+ logger.info(message)
252
+ else:
253
+ print(message)
254
+
255
+ def log_error(message, exc_info=False):
256
+ if logger:
257
+ logger.error(message, exc_info=exc_info)
258
+ else:
259
+ print(message)
260
+ try:
261
+ if isinstance(json_or_file, str): # Load from string
262
+ imported_notices = json.loads(json_or_file)
263
+ elif hasattr(json_or_file, 'read'): # Load from file-like object
264
+ imported_notices = json.load(json_or_file)
265
+ self.add_notice(imported_notices)
266
+ log_message("Successfully imported notices from json.")
267
+ except Exception as e:
268
+ log_error(f"Failed to import notices from json: {type(e).__name__} - {str(e)}", exc_info=True)
269
+
270
+ def _update_counts(self, notice, remove=False):
271
+ if remove:
272
+ if notice["severity_code"] >= self.ERROR_CODE_START_VALUE:
273
+ self.error_count -= 1
274
+ self.severity_counts[notice["severity_name"]] -= 1
275
+ else:
276
+ if notice["severity_code"] >= self.ERROR_CODE_START_VALUE:
277
+ self.error_count += 1
278
+ self.severity_counts[notice["severity_name"]] += 1
279
+
280
+
281
+ class SuccessLog:
282
+ def __init__(self, subject:str, description:str=None, context:str=None):
283
+ self.context = context
284
+ self.subject = subject
285
+ self.timestamp = datetime.now(timezone.utc).isoformat()
286
+ self.description = description
287
+
288
+ def to_dict(self):
289
+ return {
290
+ "context": self.context or "",
291
+ "subject": self.subject,
292
+ "timestamp": self.timestamp,
293
+ "description": self.description or ""
294
+ }
295
+
296
+
297
+ class SuccessLogManager:
298
+ def __init__(self):
299
+ self.successlogs = []
300
+ self.context_stack = []
301
+
302
+ @contextmanager
303
+ def successlog_context(self, context):
304
+ self.push_context(context)
305
+ try:
306
+ yield
307
+ finally:
308
+ self.pop_context()
309
+
310
+ def push_context(self, context):
311
+ self.context_stack.append(context)
312
+
313
+ def pop_context(self):
314
+ if self.context_stack:
315
+ self.context_stack.pop()
316
+
317
+ def get_current_context(self):
318
+ return " >> ".join(self.context_stack)
319
+
320
+ def get_all_successlogs(self):
321
+ return self.successlogs
322
+
323
+ def add_successlog(self, successlog: SuccessLog):
324
+ successlog.context = self.get_current_context()
325
+ successlog_dict = successlog.to_dict()
326
+ self.successlogs.append(successlog_dict)
327
+
328
+ def add_successlogs(self, successlogs: List[SuccessLog]):
329
+ for successlog in successlogs:
330
+ successlog.context = self.get_current_context()
331
+ successlog_dict = successlog.to_dict()
332
+ self.successlogs.append(successlog_dict)
333
+
334
+ def remove_successlog(self, successlog: SuccessLog):
335
+ successlog_dict = successlog.to_dict()
336
+ if successlog_dict in self.successlogs:
337
+ self.successlogs.remove(successlog_dict)
338
+
339
+ def clear_successlogs(self):
340
+ self.successlogs = []
341
+
342
+ def count_all_successlogs(self):
343
+ return len(self.successlogs)
344
+
345
+ def count_successlogs_for_current_context(self):
346
+ current_context = self.get_current_context()
347
+ return sum(
348
+ 1 for successlog in self.successlogs
349
+ if successlog["context"] == current_context
350
+ )
351
+
352
+ def count_successlogs_for_current_and_nested_contexts(self):
353
+ current_context = self.get_current_context()
354
+ return sum(
355
+ 1 for successlog in self.successlogs
356
+ if current_context in successlog["context"]
357
+ )
358
+
359
+
360
+ def export_successlogs_to_gcs_file(self, bucket_name, storage_client, file_name=None, top_level_context=None, save_locally=False, local_path=None, logger=None, max_retries=3):
361
+ def log_message(message):
362
+ if logger:
363
+ logger.info(message)
364
+
365
+ def log_error(message, exc_info=False):
366
+ if logger:
367
+ logger.error(message, exc_info=exc_info)
368
+
369
+ if not file_name:
370
+ timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
371
+ if top_level_context:
372
+ file_name = f"successlogs_{timestamp}_{top_level_context}_len{len(self.successlogs)}.json"
373
+ else:
374
+ file_name = f"successlogs_{timestamp}_len{len(self.successlogs)}.json"
375
+
376
+ cloud_path=None
377
+ local_path=None
378
+ try:
379
+ cloud_path, local_path = write_data_to_gcs(
380
+ bucket_name=bucket_name,
381
+ storage_client=storage_client,
382
+ data=self.successlogs,
383
+ file_name=file_name,
384
+ save_locally=save_locally,
385
+ local_path=local_path,
386
+ logger=logger,
387
+ max_retries=max_retries
388
+ )
389
+ log_message(f"Success logs successfully saved to GCS at {cloud_path} and locally at {local_path}.")
390
+ except Exception as e:
391
+ log_error(f"Failed to export success logs: {type(e).__name__} - {str(e)}", exc_info=True)
392
+
393
+ return cloud_path, local_path
394
+
395
+ def import_successlogs_from_json(self, json_or_file, logger=None):
396
+ def log_message(message):
397
+ if logger:
398
+ logger.info(message)
399
+ else:
400
+ print(message)
401
+
402
+ def log_error(message, exc_info=False):
403
+ if logger:
404
+ logger.error(message, exc_info=exc_info)
405
+ else:
406
+ print(message)
407
+ try:
408
+ if isinstance(json_or_file, str): # Load from string
409
+ imported_success_logs = json.loads(json_or_file)
410
+ elif hasattr(json_or_file, 'read'): # Load from file-like object
411
+ imported_success_logs = json.load(json_or_file)
412
+ self.add_successlog(imported_success_logs)
413
+ log_message("Successfully imported success logs from json.")
414
+ except Exception as e:
415
+ log_error(f"Failed to import success logs from json: {type(e).__name__} - {str(e)}", exc_info=True)
@@ -1,8 +1,13 @@
1
+ # pylint: disable=missing-module-docstring
2
+ # pylint: disable=missing-function-docstring
3
+ # pylint: disable=missing-class-docstring
1
4
  import json
2
5
  import csv
3
6
  from io import StringIO
4
7
  import logging
5
8
  import os
9
+ import time
10
+ from datetime import datetime, timezone
6
11
  import traceback
7
12
  from google.cloud import error_reporting, logging as cloud_logging
8
13
  from google.api_core.exceptions import NotFound
@@ -22,7 +27,7 @@ from google.api_core.exceptions import NotFound
22
27
  ## TODO Fix the issue with POST 0B Nan.... printed in Cloud Logging , which is referring to posting to Cloud Logging probably.
23
28
  ENV = os.getenv('ENV', 'LOCAL').strip("'")
24
29
 
25
- def setup_gcp_logger_and_error_report(logger_name):
30
+ def setup_gcp_logger_and_error_report(logger_name,level=logging.INFO, use_cloud_logging=True):
26
31
  """Sets up a logger with Error Reporting and Cloud Logging handlers.
27
32
 
28
33
  Args:
@@ -52,26 +57,26 @@ def setup_gcp_logger_and_error_report(logger_name):
52
57
  self.handleError(record)
53
58
 
54
59
  logger = logging.getLogger(logger_name)
55
- logger.setLevel(logging.INFO)
56
-
57
- # Create Error Reporting handler
58
- error_reporting_handler = ErrorReportingHandler()
59
-
60
- # Create Google Cloud Logging handler
61
- cloud_logging_client = cloud_logging.Client()
62
- cloud_logging_handler = cloud_logging_client.get_default_handler()
63
-
64
- # Add handlers to the logger
65
- logger.addHandler(error_reporting_handler)
66
- logger.addHandler(cloud_logging_handler)
60
+ logger.setLevel(level)
67
61
 
68
62
  # Add a console handler for local development
69
- if ENV == "LOCAL":
63
+ if ENV == "LOCAL" or not use_cloud_logging:
70
64
  formatter = logging.Formatter('%(levelname)s : %(name)s : %(asctime)s : %(message)s')
71
65
  console_handler = logging.StreamHandler()
72
66
  console_handler.setFormatter(formatter)
73
67
  logger.addHandler(console_handler)
74
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)
75
80
  return logger
76
81
  ############################################################################
77
82
 
@@ -116,21 +121,93 @@ def read_csv_from_gcs(bucket_name, file_name, storage_client, logger):
116
121
  logger.error(f"An unexpected error occurred: {e}", exc_info=True)
117
122
  return None
118
123
 
119
- def write_json_to_gcs(bucket_name, file_name, data, stor_client, logger, log_info_verbose=True):
120
- """ Helper function to write a JSON file to Google Cloud Storage """
121
- try:
122
- bucket = stor_client.bucket(bucket_name)
123
- blob = bucket.blob(file_name)
124
- # Check if data is already a JSON string
125
- if isinstance(data, str):
126
- data_string = data
127
- else:
128
- data_string = json.dumps(data)
129
- blob.upload_from_string(data_string, content_type='application/json')
130
- if log_info_verbose:
131
- logger.info(f"Successfully wrote JSON to {file_name} in bucket {bucket_name}.")
132
- except Exception as e:
133
- logger.error(f"An unexpected error occurred while writing JSON to GCS: {e}", exc_info=True)
124
+
125
+
126
+ def write_data_to_gcs(bucket_name, storage_client, data, file_name=None,
127
+ save_locally=False, local_path=None, logger=None, max_retries=3):
128
+ """Saves data to Google Cloud Storage and optionally locally.
129
+
130
+ This function attempts to upload data to GCS. If the upload fails after
131
+ retries and `save_locally` is True or `local_path` is provided, it attempts
132
+ to save the data locally.
133
+
134
+ Args:
135
+ bucket_name (str): Name of the GCS bucket.
136
+ storage_client (google.cloud.storage.Client): GCS client object.
137
+ data (list, dict, or str): Data to be saved.
138
+ file_name (str, optional): File name for GCS and local. Defaults to None.
139
+ save_locally (bool, optional): Save locally if GCS fails. Defaults to False.
140
+ local_path (str, optional): Local directory to save. Defaults to None.
141
+ logger (logging.Logger, optional): Logger for messages. Defaults to None.
142
+ max_retries (int, optional): Number of GCS upload retries. Defaults to 3.
143
+
144
+ Returns:
145
+ tuple: A tuple containing the GCS path (or None if upload failed) and
146
+ the local path (or None if not saved locally).
147
+
148
+ Raises:
149
+ ValueError: If data is not a list, dict, or str.
150
+ Exception: If GCS upload fails after retries and local saving fails or
151
+ is not requested. If GCS upload fails after retries and
152
+ local saving is requested but unsuccessful.
153
+ """
154
+
155
+ def log_message(message):
156
+ if logger:
157
+ logger.info(message)
158
+
159
+ def log_error(message, exc_info=False):
160
+ if logger:
161
+ logger.error(message, exc_info=exc_info)
162
+
163
+ attempts = 0
164
+ success = False
165
+ cloud_path = None
166
+ local_path_final = None
167
+ gcs_upload_exception = None # Store potential GCS exception
168
+
169
+ if isinstance(data, (list, dict)):
170
+ data_str = json.dumps(data, indent=2)
171
+ elif isinstance(data, str):
172
+ data_str = data
173
+ else:
174
+ raise ValueError("Unsupported data type. It should be a list, dict, or str.")
175
+
176
+ while attempts < max_retries and not success:
177
+ try:
178
+ bucket = storage_client.bucket(bucket_name)
179
+ blob = bucket.blob(file_name)
180
+ blob.upload_from_string(data_str, content_type='application/json')
181
+ cloud_path = f"{bucket_name}/{file_name}"
182
+ log_message(f"Successfully saved file to GCS {cloud_path}.")
183
+ success = True
184
+ except Exception as e:
185
+ gcs_upload_exception = e
186
+ attempts += 1
187
+ log_error(f"Attempt {attempts} - Failed to write {file_name} "
188
+ f"to GCS bucket '{bucket_name}': {e}") # Log with full traceback
189
+ if attempts < max_retries:
190
+ time.sleep(2 ** attempts)
191
+
192
+ if not success and (save_locally or local_path):
193
+ try:
194
+ if not local_path:
195
+ local_path_final = os.path.join("/tmp", file_name)
196
+ else:
197
+ local_path_final = os.path.join(local_path, file_name)
198
+ with open(local_path_final, 'w', encoding='utf-8') as f:
199
+ f.write(data_str)
200
+ log_message(f"Saved {file_name} locally at {local_path_final}.")
201
+ except Exception as local_e:
202
+ log_error(f"Failed to write {file_name} locally: {local_e}",exc_info=True)
203
+
204
+ # If GCS upload failed, raise a single exception here
205
+
206
+ if gcs_upload_exception:
207
+ raise gcs_upload_exception from None # Propagate without nesting
208
+
209
+ return cloud_path, local_path_final
210
+
134
211
 
135
212
  def write_csv_to_gcs(bucket_name, file_name, data, storage_client, logger,log_info_verbose=True):
136
213
  """ Helper function to write a CSV file to Google Cloud Storage """
@@ -151,4 +228,4 @@ def write_csv_to_gcs(bucket_name, file_name, data, storage_client, logger,log_in
151
228
  except ValueError as e:
152
229
  logger.error(f"ValueError: {e}")
153
230
  except Exception as e:
154
- logger.error(f"An unexpected error occurred while writing CSV to GCS: {e}", exc_info=True)
231
+ logger.error(f"An unexpected error occurred while writing CSV to GCS: {e}", exc_info=True)
@@ -6,7 +6,7 @@
6
6
  import datetime
7
7
  from google.cloud import bigquery
8
8
  from ipulse_shared_core_ftredge.enums.enums_common_utils import NoticeSeverity
9
- from ipulse_shared_core_ftredge.utils_common import create_custom_notice
9
+ from ipulse_shared_core_ftredge.utils_common import Notice
10
10
 
11
11
 
12
12
  def create_bigquery_schema_from_json(json_schema):
@@ -23,14 +23,14 @@ def update_check_with_schema_template(updates, schema, dt_ts_to_str=True, check_
23
23
 
24
24
  """Ensure Update dict corresponds to the config schema, ensuring proper formats and lengths."""
25
25
  valid_updates = {}
26
- notices=[] ### THIS IS TO AVOID LOGGING A WARNING RANDOMLY , INSTEAD GROUPPING FOR A GIVEN RUN
26
+ notices=[] ### THIS IS TO AVOID LOGGING A WARNING RANDOMLY, INSTEAD GROUPPING FOR A GIVEN RUN
27
27
 
28
28
  # Process updates to conform to the schema
29
29
  for field in schema:
30
30
  field_name = field["name"]
31
31
  field_type = field["type"]
32
32
  mode = field["mode"]
33
-
33
+
34
34
  # Initialize notice to None at the start of each field processing
35
35
  notice = None
36
36
 
@@ -38,7 +38,7 @@ def update_check_with_schema_template(updates, schema, dt_ts_to_str=True, check_
38
38
  value = updates[field_name]
39
39
 
40
40
  # Handle date and timestamp formatting
41
-
41
+
42
42
  # Validate and potentially convert date and timestamp fields
43
43
  if field_type == "DATE":
44
44
  value, notice = handle_date_fields(field_name, value, dt_ts_to_str)
@@ -55,18 +55,18 @@ def update_check_with_schema_template(updates, schema, dt_ts_to_str=True, check_
55
55
  value,notice = check_and_truncate_length(field_name, value, field["max_length"])
56
56
  if notice:
57
57
  notices.append(notice)
58
-
58
+
59
59
  # Only add to the dictionary if value is not None or the field is required
60
60
  if value is not None or mode == "REQUIRED":
61
61
  valid_updates[field_name] = value
62
62
 
63
63
  elif mode == "REQUIRED":
64
- notice=create_custom_notice(severity=NoticeSeverity.WARNING_ACTION_REQUIRED,
64
+ notice=Notice(severity=NoticeSeverity.WARNING_FIX_REQUIRED,
65
65
  subject=field_name,
66
66
  message=f"Required field '{field_name}' is missing in the updates.")
67
67
 
68
68
  notices.append(notice)
69
-
69
+
70
70
  return valid_updates, notices
71
71
 
72
72
  def handle_date_fields(field_name, value, dt_ts_to_str):
@@ -82,11 +82,11 @@ def handle_date_fields(field_name, value, dt_ts_to_str):
82
82
  return value, None
83
83
  return parsed_date, None
84
84
  except ValueError:
85
- return None, create_custom_notice(severity=NoticeSeverity.WARNING_ACTION_REQUIRED,
85
+ return None, Notice(severity=NoticeSeverity.WARNING_FIX_REQUIRED,
86
86
  subject=field_name,
87
87
  message=f"Expected a DATE in YYYY-MM-DD format but got {value}.")
88
88
  else:
89
- return None, create_custom_notice(severity=NoticeSeverity.WARNING_ACTION_REQUIRED,
89
+ return None, Notice(severity=NoticeSeverity.WARNING_FIX_REQUIRED,
90
90
  subject=field_name,
91
91
  message= f"Expected a DATE or YYYY-MM-DD str format but got {value} of type {type(value).__name__}.")
92
92
 
@@ -104,11 +104,11 @@ def handle_timestamp_fields(field_name, value, dt_ts_to_str):
104
104
  return value, None
105
105
  return parsed_datetime, None
106
106
  except ValueError:
107
- return None, create_custom_notice(severity=NoticeSeverity.WARNING_ACTION_REQUIRED,
107
+ return None, Notice(severity=NoticeSeverity.WARNING_FIX_REQUIRED,
108
108
  subject=field_name,
109
109
  message= f"Expected ISO format TIMESTAMP but got {value}.")
110
110
  else:
111
- return None, create_custom_notice(severity=NoticeSeverity.WARNING_ACTION_REQUIRED,
111
+ return None, Notice(severity=NoticeSeverity.WARNING_FIX_REQUIRED,
112
112
  subject=field_name,
113
113
  message= f"Expected ISO format TIMESTAMP but got {value} of type {type(value).__name__}.")
114
114
 
@@ -116,17 +116,17 @@ def handle_timestamp_fields(field_name, value, dt_ts_to_str):
116
116
  def check_and_truncate_length(field_name, value, max_length):
117
117
  """Checks and truncates the length of string fields if they exceed the max length."""
118
118
  if isinstance(value, str) and len(value) > max_length:
119
- return value[:max_length], create_custom_notice(severity=NoticeSeverity.WARNING_ACTION_RECOMMENDED,
119
+ return value[:max_length], Notice(severity=NoticeSeverity.WARNING_FIX_RECOMMENDED,
120
120
  subject= field_name,
121
121
  message= f"Field exceeds max length: {len(value)}/{max_length}. Truncating.")
122
-
122
+
123
123
  return value, None
124
124
 
125
125
 
126
126
 
127
127
  def handle_type_conversion(field_type, field_name, value):
128
128
  if field_type == "STRING" and not isinstance(value, str):
129
- return str(value), create_custom_notice(severity=NoticeSeverity.WARNING_ACTION_REQUIRED,
129
+ return str(value), Notice(severity=NoticeSeverity.WARNING_REVIEW_RECOMMENDED,
130
130
  subject=field_name,
131
131
  message= f"Expected STRING but got {value} of type {type(value).__name__}.")
132
132
 
@@ -134,17 +134,19 @@ def handle_type_conversion(field_type, field_name, value):
134
134
  try:
135
135
  return int(value), None
136
136
  except ValueError:
137
- return None, create_custom_notice(severity=NoticeSeverity.WARNING_ACTION_REQUIRED,
137
+ return None, Notice(severity=NoticeSeverity.WARNING_FIX_REQUIRED,
138
138
  subject= field_name,
139
139
  message=f"Expected INTEGER, but got {value} of type {type(value).__name__}.")
140
140
  if field_type == "FLOAT64" and not isinstance(value, float):
141
141
  try:
142
142
  return float(value), None
143
143
  except ValueError:
144
- return None, create_custom_notice(severity=NoticeSeverity.WARNING_ACTION_REQUIRED,
145
- subject=field_name,
146
- message=f"Expected FLOAT, but got {value} of type {type(value).__name__}.")
144
+ return None, Notice(severity=NoticeSeverity.WARNING_FIX_REQUIRED,
145
+ subject=field_name,
146
+ message=f"Expected FLOAT, but got {value} of type {type(value).__name__}.")
147
147
  if field_type == "BOOL" and not isinstance(value, bool):
148
- return bool(value), None
149
-
150
- return value, None
148
+ return bool(value), Notice(severity=NoticeSeverity.WARNING_REVIEW_RECOMMENDED,
149
+ subject=field_name,
150
+ message=f"Expected BOOL, but got {value}. Converting as {bool(value)}.")
151
+
152
+ return value, None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ipulse_shared_core_ftredge
3
- Version: 2.39
3
+ Version: 2.50
4
4
  Summary: Shared Core models and Logger util for the Pulse platform project. Using AI for financial advisory and investment management.
5
5
  Home-page: https://github.com/TheFutureEdge/ipulse_shared_core
6
6
  Author: Russlan Ramdowar
@@ -1,10 +1,10 @@
1
- ipulse_shared_core_ftredge/__init__.py,sha256=87fWkJqrx0FMqOlzxclR6vBRJPisz3SbwhCWbeeP4kk,902
2
- ipulse_shared_core_ftredge/utils_common.py,sha256=Fs8-guNuz_UJd4p7Vh0bFDzxyE2YCVaj4bxJQynOLpk,803
3
- ipulse_shared_core_ftredge/utils_gcp.py,sha256=E8TvZ05fTjNr-VQXxSZNCiqT9PwPhtqeKOifIGhb2sg,6289
4
- ipulse_shared_core_ftredge/utils_templates_and_schemas.py,sha256=l70p5NV6R4pNEkqdSz0WOy8RPdsR8BqrP-qlZ7Jjep8,7075
1
+ ipulse_shared_core_ftredge/__init__.py,sha256=jAtj9nifLy5nOgpVuc5JeAQAkyF8a_5IzWHKexFZT2Q,904
2
+ ipulse_shared_core_ftredge/utils_common.py,sha256=w038BKQZqqndM-9hYT-tZ0opw3QbVgBemJxug8d6HK0,15565
3
+ ipulse_shared_core_ftredge/utils_gcp.py,sha256=OuR0tMGDSn69kGWhrDP1URuItrtGLu6T3vQlQS65IMk,9389
4
+ ipulse_shared_core_ftredge/utils_templates_and_schemas.py,sha256=67WaefoBXFiRKoYKCo3XcThODJyW7YoSWhsZOj1nTlo,7107
5
5
  ipulse_shared_core_ftredge/enums/__init__.py,sha256=PjxJiUConI2TuaG_Ushe2BaFVjBDw1rbq1E9Vt9nXvE,801
6
- ipulse_shared_core_ftredge/enums/enums_common_utils.py,sha256=ukChcm8R2QwlK4NAfM291mWryCAZvfg7bMpdScQQfok,4360
7
- ipulse_shared_core_ftredge/enums/enums_data_eng.py,sha256=A8_uiGGacd-_AZP09Zft-DX3rF8aVqqFzFak8s4MsfY,1801
6
+ ipulse_shared_core_ftredge/enums/enums_common_utils.py,sha256=aUH5SJOInEvVA_jtKwaBYNc1_5E3ud5IWCVSEiK1cCI,4895
7
+ ipulse_shared_core_ftredge/enums/enums_data_eng.py,sha256=2i6Qo6Yi_j_O9xxnOD6QA-r0Cv7mWAUaKUx907XMRio,1825
8
8
  ipulse_shared_core_ftredge/enums/enums_module_fincore.py,sha256=MuqQg249clrWUOBb1S-iPsoOldN2_F3ohRQizbjhwG0,1374
9
9
  ipulse_shared_core_ftredge/enums/enums_modules.py,sha256=AyXUoNmR75DZLaEHi3snV6LngR25LeZRqzrLDaAupbY,1244
10
10
  ipulse_shared_core_ftredge/models/__init__.py,sha256=gE22Gzhil0RYQa7YLtdtT44_AsWqklcDfRtgLAQc1dI,200
@@ -18,8 +18,8 @@ ipulse_shared_core_ftredge/models/user_profile_update.py,sha256=oKK0XsQDKkgDvjFP
18
18
  ipulse_shared_core_ftredge/models/user_status.py,sha256=8TyRd8tBK9_xb0MPKbI5pn9-lX7ovKbeiuWYYPtIOiw,3202
19
19
  ipulse_shared_core_ftredge/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
20
  ipulse_shared_core_ftredge/tests/test.py,sha256=0lS8HP5Quo_BqNoscU40qOH9aJRaa1Pfam5VUBmdld8,682
21
- ipulse_shared_core_ftredge-2.39.dist-info/LICENCE,sha256=YBtYAXNqCCOo9Mr2hfkbSPAM9CeAr2j1VZBSwQTrNwE,1060
22
- ipulse_shared_core_ftredge-2.39.dist-info/METADATA,sha256=I99qJdttOu8XaJhp1dCaYAuJw-17QlN57kJtSghhtpM,561
23
- ipulse_shared_core_ftredge-2.39.dist-info/WHEEL,sha256=cpQTJ5IWu9CdaPViMhC9YzF8gZuS5-vlfoFihTBC86A,91
24
- ipulse_shared_core_ftredge-2.39.dist-info/top_level.txt,sha256=8sgYrptpexkA_6_HyGvho26cVFH9kmtGvaK8tHbsGHk,27
25
- ipulse_shared_core_ftredge-2.39.dist-info/RECORD,,
21
+ ipulse_shared_core_ftredge-2.50.dist-info/LICENCE,sha256=YBtYAXNqCCOo9Mr2hfkbSPAM9CeAr2j1VZBSwQTrNwE,1060
22
+ ipulse_shared_core_ftredge-2.50.dist-info/METADATA,sha256=Y00Y547YKmK5-I0zlA522urqyihif5GUVgCG5yAXZIg,561
23
+ ipulse_shared_core_ftredge-2.50.dist-info/WHEEL,sha256=Z4pYXqR_rTB7OWNDYFOm1qRk0RX6GFP2o8LgvP453Hk,91
24
+ ipulse_shared_core_ftredge-2.50.dist-info/top_level.txt,sha256=8sgYrptpexkA_6_HyGvho26cVFH9kmtGvaK8tHbsGHk,27
25
+ ipulse_shared_core_ftredge-2.50.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (70.1.0)
2
+ Generator: setuptools (70.3.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5