ipulse-shared-core-ftredge 2.39__tar.gz → 2.51__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (32) hide show
  1. {ipulse_shared_core_ftredge-2.39/src/ipulse_shared_core_ftredge.egg-info → ipulse_shared_core_ftredge-2.51}/PKG-INFO +1 -1
  2. {ipulse_shared_core_ftredge-2.39 → ipulse_shared_core_ftredge-2.51}/setup.py +2 -1
  3. {ipulse_shared_core_ftredge-2.39 → ipulse_shared_core_ftredge-2.51}/src/ipulse_shared_core_ftredge/__init__.py +3 -5
  4. {ipulse_shared_core_ftredge-2.39 → ipulse_shared_core_ftredge-2.51}/src/ipulse_shared_core_ftredge/enums/__init__.py +2 -1
  5. {ipulse_shared_core_ftredge-2.39 → ipulse_shared_core_ftredge-2.51}/src/ipulse_shared_core_ftredge/enums/enums_common_utils.py +43 -13
  6. {ipulse_shared_core_ftredge-2.39 → ipulse_shared_core_ftredge-2.51}/src/ipulse_shared_core_ftredge/enums/enums_data_eng.py +18 -19
  7. ipulse_shared_core_ftredge-2.51/src/ipulse_shared_core_ftredge/utils_common.py +369 -0
  8. {ipulse_shared_core_ftredge-2.39 → ipulse_shared_core_ftredge-2.51}/src/ipulse_shared_core_ftredge/utils_gcp.py +106 -30
  9. {ipulse_shared_core_ftredge-2.39 → ipulse_shared_core_ftredge-2.51}/src/ipulse_shared_core_ftredge/utils_templates_and_schemas.py +32 -30
  10. {ipulse_shared_core_ftredge-2.39 → ipulse_shared_core_ftredge-2.51/src/ipulse_shared_core_ftredge.egg-info}/PKG-INFO +1 -1
  11. ipulse_shared_core_ftredge-2.39/src/ipulse_shared_core_ftredge/utils_common.py +0 -24
  12. {ipulse_shared_core_ftredge-2.39 → ipulse_shared_core_ftredge-2.51}/LICENCE +0 -0
  13. {ipulse_shared_core_ftredge-2.39 → ipulse_shared_core_ftredge-2.51}/README.md +0 -0
  14. {ipulse_shared_core_ftredge-2.39 → ipulse_shared_core_ftredge-2.51}/pyproject.toml +0 -0
  15. {ipulse_shared_core_ftredge-2.39 → ipulse_shared_core_ftredge-2.51}/setup.cfg +0 -0
  16. {ipulse_shared_core_ftredge-2.39 → ipulse_shared_core_ftredge-2.51}/src/ipulse_shared_core_ftredge/enums/enums_module_fincore.py +0 -0
  17. {ipulse_shared_core_ftredge-2.39 → ipulse_shared_core_ftredge-2.51}/src/ipulse_shared_core_ftredge/enums/enums_modules.py +0 -0
  18. {ipulse_shared_core_ftredge-2.39 → ipulse_shared_core_ftredge-2.51}/src/ipulse_shared_core_ftredge/models/__init__.py +0 -0
  19. {ipulse_shared_core_ftredge-2.39 → ipulse_shared_core_ftredge-2.51}/src/ipulse_shared_core_ftredge/models/audit_log_firestore.py +0 -0
  20. {ipulse_shared_core_ftredge-2.39 → ipulse_shared_core_ftredge-2.51}/src/ipulse_shared_core_ftredge/models/organisation.py +0 -0
  21. {ipulse_shared_core_ftredge-2.39 → ipulse_shared_core_ftredge-2.51}/src/ipulse_shared_core_ftredge/models/pulse_enums.py +0 -0
  22. {ipulse_shared_core_ftredge-2.39 → ipulse_shared_core_ftredge-2.51}/src/ipulse_shared_core_ftredge/models/resource_catalog_item.py +0 -0
  23. {ipulse_shared_core_ftredge-2.39 → ipulse_shared_core_ftredge-2.51}/src/ipulse_shared_core_ftredge/models/user_auth.py +0 -0
  24. {ipulse_shared_core_ftredge-2.39 → ipulse_shared_core_ftredge-2.51}/src/ipulse_shared_core_ftredge/models/user_profile.py +0 -0
  25. {ipulse_shared_core_ftredge-2.39 → ipulse_shared_core_ftredge-2.51}/src/ipulse_shared_core_ftredge/models/user_profile_update.py +0 -0
  26. {ipulse_shared_core_ftredge-2.39 → ipulse_shared_core_ftredge-2.51}/src/ipulse_shared_core_ftredge/models/user_status.py +0 -0
  27. {ipulse_shared_core_ftredge-2.39 → ipulse_shared_core_ftredge-2.51}/src/ipulse_shared_core_ftredge/tests/__init__.py +0 -0
  28. {ipulse_shared_core_ftredge-2.39 → ipulse_shared_core_ftredge-2.51}/src/ipulse_shared_core_ftredge/tests/test.py +0 -0
  29. {ipulse_shared_core_ftredge-2.39 → ipulse_shared_core_ftredge-2.51}/src/ipulse_shared_core_ftredge.egg-info/SOURCES.txt +0 -0
  30. {ipulse_shared_core_ftredge-2.39 → ipulse_shared_core_ftredge-2.51}/src/ipulse_shared_core_ftredge.egg-info/dependency_links.txt +0 -0
  31. {ipulse_shared_core_ftredge-2.39 → ipulse_shared_core_ftredge-2.51}/src/ipulse_shared_core_ftredge.egg-info/requires.txt +0 -0
  32. {ipulse_shared_core_ftredge-2.39 → ipulse_shared_core_ftredge-2.51}/src/ipulse_shared_core_ftredge.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ipulse_shared_core_ftredge
3
- Version: 2.39
3
+ Version: 2.51
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,8 +1,9 @@
1
+ # pylint: disable=import-error
1
2
  from setuptools import setup, find_packages
2
3
 
3
4
  setup(
4
5
  name='ipulse_shared_core_ftredge',
5
- version='2.39',
6
+ version='2.51',
6
7
  package_dir={'': 'src'}, # Specify the source directory
7
8
  packages=find_packages(where='src'), # Look for packages in 'src'
8
9
  install_requires=[
@@ -2,16 +2,14 @@ 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, NoticesManager)
9
9
 
10
- from .enums import (NoticeSeverity, Unit, Frequency,
10
+ from .enums import (NoticeManagerCategory, NoticeLevel, Unit, Frequency,
11
11
  Module, SubModule, BaseDataCategory,
12
12
  FinCoreCategory, FincCoreSubCategory,
13
13
  FinCoreRecordsCategory, ExchangeOrPublisher,
14
14
  SourcingPipelineType, SourcingTriggerType,
15
15
  DWEvent, DWEventTriggerType)
16
-
17
-
@@ -3,7 +3,8 @@
3
3
  # pylint: disable=missing-function-docstring
4
4
  # pylint: disable=missing-class-docstring
5
5
 
6
- from .enums_common_utils import (NoticeSeverity,
6
+ from .enums_common_utils import (NoticeLevel,
7
+ NoticeManagerCategory,
7
8
  Unit,
8
9
  Frequency)
9
10
 
@@ -5,32 +5,62 @@
5
5
 
6
6
  from enum import Enum
7
7
 
8
- class NoticeSeverity(Enum):
8
+
9
+ class NoticeManagerCategory(Enum):
10
+ NOTICES = "notices"
11
+ WARN_ERRS = "warn_errs"
12
+ SUCCESSES = "successes"
13
+ class NoticeLevel(Enum):
9
14
  """
10
- Standardized logging levels for data engineering pipelines,
15
+ Standardized notice levels for data engineering pipelines,
11
16
  designed for easy analysis and identification of manual
12
17
  intervention needs.
13
18
  """
14
19
  DEBUG = 100 # Detailed debug information (for development/troubleshooting)
15
- INFO = 200 # Normal pipeline execution information
16
- NOTICE = 300 # Events requiring attention, but not necessarily errors
20
+
21
+ INFO = 200
22
+
23
+ SUCCESS = 300 # Events requiring attention, but not necessarily errors
17
24
 
18
25
  # Warnings indicate potential issues that might require attention:
26
+ WARNING = 400 # General warning, no immediate action required
19
27
  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
28
+ WARNING_REVIEW_RECOMMENDED = 402 # Action recommended to prevent potential future issues
29
+ WARNING_FIX_RECOMMENDED = 403 # Action recommended to prevent potential future issues
30
+ WARNING_FIX_REQUIRED = 404 # Action required, pipeline can likely continue
22
31
 
32
+ ERROR = 500 # General error, no immediate action required
23
33
  # 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
34
+ ERROR_EXCEPTION_REDO = 501
35
+ ERROR_CUSTOM_REDO = 502 # Temporary error, automatic retry likely to succeed
36
+
37
+ ERROR_EXCEPTION_INVESTIGATE = 601 # Exception occured after some data was likely persisted (e.g., to GCS or BQ)
38
+ ERROR_CUSTOM_INVESTIGATE= 602
39
+ ERROR_EXCEPTION_PERSTISTANCE = 603 # Exception occured after data was persisted (e.g., to GCS or BQ)
40
+ ERROR_CUSTOM_PERSTISTANCE = 604
29
41
 
30
42
  # 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
43
+ CRITICAL_SYSTEM_FAILURE = 701 # System-level failure (e.g., infrastructure), requires immediate action
44
+ CRITICAL_PIPELINE_FAILURE = 702 # Complete pipeline failure, requires investigation and potential rollback
45
+
46
+ UNKNOWN=1001 # Unknown error, should not be used in normal operation
47
+
48
+
49
+ class NoticeStatus(Enum):
50
+ OPEN = "open"
51
+ ACKNOWLEDGED = "acknowledged"
52
+ IN_PROGRESS = "in_progress"
53
+ RESOLVED = "resolved"
54
+ IGNORED = "ignored"
55
+
56
+
57
+
33
58
 
59
+ ### Exception during full exection, partially saved
60
+ # Exception during ensemble pipeline; modifications collected in local object , nothing persisted
61
+ # Exception during ensemble pipeline; modifications persisted , metadata failed
62
+ # Exception during ensemble pipeline; modifications persisted , metadata persisted
63
+ # Exception during ensemble pipeline; modifications persisted , metadata persisted
34
64
 
35
65
 
36
66
  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"
@@ -0,0 +1,369 @@
1
+ # pylint: disable=missing-module-docstring
2
+ # pylint: disable=missing-function-docstring
3
+ # pylint: disable=logging-fstring-interpolation
4
+ # pylint: disable=line-too-long
5
+ # pylint: disable=missing-class-docstring
6
+ import traceback
7
+ import json
8
+ import uuid
9
+ from datetime import datetime, timezone
10
+ from contextlib import contextmanager
11
+ from typing import List
12
+ from google.cloud import logging as cloudlogging
13
+ from ipulse_shared_core_ftredge.enums.enums_common_utils import NoticeLevel, NoticeManagerCategory, NoticeStatus
14
+ from ipulse_shared_core_ftredge.utils_gcp import write_data_to_gcs
15
+
16
+
17
+ # ["data_import","data_quality", "data_processing","data_general","data_persistance","metadata_quality", "metadata_processing", "metadata_persistance","metadata_general"]
18
+
19
+ class Notice:
20
+ MAX_TRACEBACK_LINES = 14 # Define the maximum number of traceback lines to include
21
+ def __init__(self, level: NoticeLevel, start_context: str = None, notice_manager_id: str = None,
22
+ e: Exception = None, e_type: str = None, e_message: str = None, e_traceback: str = None,
23
+ subject: str = None, description: str = None, context: str = None,
24
+ notice_status: NoticeStatus = NoticeStatus.OPEN):
25
+ if e is not None:
26
+ e_type = type(e).__name__ if e_type is None else e_type
27
+ e_message = str(e) if e_message is None else e_message
28
+ e_traceback = traceback.format_exc() if e_traceback is None else e_traceback
29
+ elif e_traceback is None and (e_type or e_message):
30
+ e_traceback = traceback.format_exc()
31
+
32
+ self.level = level
33
+ self.subject = subject
34
+ self.description = description
35
+ self._start_context = start_context
36
+ self._context = context
37
+ self.notice_manager_id = notice_manager_id
38
+ self.exception_type = e_type
39
+ self.exception_message = e_message
40
+ self.exception_traceback = self._format_traceback(e_traceback,e_message)
41
+ self.notice_status = notice_status
42
+ self.timestamp = datetime.now(timezone.utc).isoformat()
43
+
44
+ def _format_traceback(self, e_traceback, e_message):
45
+ if not e_traceback or e_traceback == 'None\n':
46
+ return None
47
+
48
+ traceback_lines = e_traceback.splitlines()
49
+
50
+ # Remove lines that are part of the exception message if they are present in traceback
51
+ message_lines = e_message.splitlines() if e_message else []
52
+ if message_lines:
53
+ for message_line in message_lines:
54
+ if message_line in traceback_lines:
55
+ traceback_lines.remove(message_line)
56
+
57
+ # Filter out lines from third-party libraries (like site-packages)
58
+ filtered_lines = [line for line in traceback_lines if "site-packages" not in line]
59
+
60
+ # If filtering results in too few lines, revert to original traceback
61
+ if len(filtered_lines) < 2:
62
+ filtered_lines = traceback_lines
63
+
64
+ # Combine standalone bracket lines with previous or next lines
65
+ combined_lines = []
66
+ for line in filtered_lines:
67
+ if line.strip() in {"(", ")", "{", "}", "[", "]"} and combined_lines:
68
+ combined_lines[-1] += " " + line.strip()
69
+ else:
70
+ combined_lines.append(line)
71
+
72
+ # Determine the number of lines to keep from the start and end
73
+ keep_lines_start = min(self.MAX_TRACEBACK_LINES // 2, len(combined_lines))
74
+ keep_lines_end = min(self.MAX_TRACEBACK_LINES // 2, len(combined_lines) - keep_lines_start)
75
+
76
+ if len(combined_lines) > self.MAX_TRACEBACK_LINES:
77
+ # Include the first few and last few lines, and an indicator of truncation
78
+ formatted_traceback = '\n'.join(
79
+ combined_lines[:keep_lines_start] +
80
+ ['... (truncated) ...'] +
81
+ combined_lines[-keep_lines_end:]
82
+ )
83
+ else:
84
+ formatted_traceback = '\n'.join(combined_lines)
85
+
86
+ return formatted_traceback
87
+
88
+ @property
89
+ def start_context(self):
90
+ return self._start_context
91
+
92
+ @start_context.setter
93
+ def start_context(self, value):
94
+ self._start_context = value
95
+
96
+ @property
97
+ def context(self):
98
+ return self._context
99
+
100
+ @context.setter
101
+ def context(self, value):
102
+ self._context = value
103
+
104
+ def to_dict(self):
105
+ return {
106
+ "start_context": self.start_context,
107
+ "context": self.context,
108
+ "level_code": self.level.value,
109
+ "level_name": self.level.name,
110
+ "subject": self.subject,
111
+ "description": self.description,
112
+ "exception_type": self.exception_type,
113
+ "exception_message": self.exception_message,
114
+ "exception_traceback": self.exception_traceback,
115
+ "notice_status": self.notice_status.value,
116
+ "notice_manager_id": self.notice_manager_id,
117
+ "timestamp": self.timestamp
118
+ }
119
+
120
+ class NoticesManager:
121
+ ERROR_CODE_START_VALUE = NoticeLevel.ERROR.value
122
+ WARNING_CODE_START_VALUE = NoticeLevel.WARNING.value
123
+ SUCCESS_CODE_START_VALUE = NoticeLevel.SUCCESS.value
124
+
125
+ def __init__(self, start_context: str, category: NoticeManagerCategory = NoticeManagerCategory.NOTICES, logger_name=None):
126
+ self._notice_manager_id = str(uuid.uuid4())
127
+ self._notices = []
128
+ self._early_stop = False
129
+ self._error_count = 0
130
+ self._warning_count = 0
131
+ self._success_count = 0
132
+ self._level_counts = {level.name: 0 for level in NoticeLevel}
133
+ self._start_context = start_context
134
+ self._context_stack = []
135
+ self._category = category.value
136
+ self._logger = self._initialize_logger(logger_name)
137
+
138
+ def _initialize_logger(self, logger_name):
139
+ if logger_name:
140
+ logging_client = cloudlogging.Client()
141
+ return logging_client.logger(logger_name)
142
+ return None
143
+
144
+
145
+ @contextmanager
146
+ def context(self, context):
147
+ self.push_context(context)
148
+ try:
149
+ yield
150
+ finally:
151
+ self.pop_context()
152
+
153
+ def push_context(self, context):
154
+ self._context_stack.append(context)
155
+
156
+ def pop_context(self):
157
+ if self._context_stack:
158
+ self._context_stack.pop()
159
+
160
+ def get_current_context(self):
161
+ return " >> ".join(self._context_stack)
162
+
163
+ def get_start_context(self):
164
+ return self._start_context
165
+
166
+ def get_notice_manager_id(self):
167
+ return self._notice_manager_id
168
+
169
+ def set_early_stop(self, max_errors_tolerance:int, create_error_notice=True,pop_context=False):
170
+ self._early_stop = True
171
+ if create_error_notice:
172
+ if pop_context:
173
+ self.pop_context()
174
+ self.add_notice(Notice(level=NoticeLevel.ERROR,
175
+ subject="EARLY_STOP",
176
+ description=f"Total MAX_ERRORS_TOLERANCE of {max_errors_tolerance} has been reached."))
177
+ def reset_early_stop(self):
178
+ self._early_stop = False
179
+
180
+ def get_early_stop(self):
181
+ return self._early_stop
182
+
183
+ def add_notice(self, notice: Notice):
184
+ if (self._category == NoticeManagerCategory.SUCCESSES.value and notice.level != NoticeLevel.SUCCESS) or \
185
+ (self._category == NoticeManagerCategory.WARN_ERRS.value and notice.level.value < self.WARNING_CODE_START_VALUE):
186
+ raise ValueError(f"Invalid notice level {notice.level.name} for category {self._category}")
187
+ notice.start_context = self.get_start_context()
188
+ notice.context = self.get_current_context()
189
+ notice.notice_manager_id = self._notice_manager_id
190
+ notice_dict = notice.to_dict()
191
+ self._notices.append(notice_dict)
192
+ self._update_counts(notice_dict)
193
+
194
+ if self._logger:
195
+ if notice.level.value >= self.WARNING_CODE_START_VALUE:
196
+ self._logger.log_struct(notice_dict, severity="WARNING")
197
+ else:
198
+ self._logger.log_struct(notice_dict, severity="INFO")
199
+
200
+ def add_notices(self, notices: List[Notice]):
201
+ for notice in notices:
202
+ self.add_notice(notice)
203
+
204
+ def clear_notices_and_counts(self):
205
+ self._notices = []
206
+ self._error_count = 0
207
+ self._warning_count = 0
208
+ self._success_count = 0
209
+ self._level_counts = {level.name: 0 for level in NoticeLevel}
210
+
211
+ def clear_notices(self):
212
+ self._notices = []
213
+
214
+ def get_all_notices(self):
215
+ return self._notices
216
+
217
+ def get_notices_for_level(self, level: NoticeLevel):
218
+ return [notice for notice in self._notices if notice["level_code"] == level.value]
219
+
220
+ def get_notices_by_str_in_context(self, context_substring: str):
221
+ return [
222
+ notice for notice in self._notices
223
+ if context_substring in notice["context"]
224
+ ]
225
+
226
+ def contains_errors(self):
227
+ return self._error_count > 0
228
+
229
+ def count_errors(self):
230
+ return self._error_count
231
+
232
+ def contains_warnings_or_errors(self):
233
+ return self._warning_count > 0 or self._error_count > 0
234
+
235
+ def count_warnings_and_errors(self):
236
+ return self._warning_count + self._error_count
237
+
238
+ def count_warnings(self):
239
+ return self._warning_count
240
+
241
+ def count_successes(self):
242
+ return self._success_count
243
+
244
+ def count_all_notices(self):
245
+ return len(self._notices)
246
+
247
+ def count_notices_by_level(self, level: NoticeLevel):
248
+ return self._level_counts.get(level.name, 0)
249
+
250
+ def _count_notices(self, context_substring: str, exact_match=False, level_code_min=None, level_code_max=None):
251
+ return sum(
252
+ 1 for notice in self._notices
253
+ if (notice["context"] == context_substring if exact_match else context_substring in notice["context"]) and
254
+ (level_code_min is None or notice["level_code"] >= level_code_min) and
255
+ (level_code_max is None or notice["level_code"] <= level_code_max)
256
+ )
257
+
258
+ def count_notices_for_current_context(self):
259
+ return self._count_notices(self.get_current_context(), exact_match=True)
260
+
261
+ def count_notices_for_current_and_nested_contexts(self):
262
+ return self._count_notices(self.get_current_context())
263
+
264
+ def count_notices_by_level_for_current_context(self, level: NoticeLevel):
265
+ return self._count_notices(self.get_current_context(), exact_match=True, level_code_min=level.value, level_code_max=level.value)
266
+
267
+ def count_notices_by_level_for_current_and_nested_contexts(self, level: NoticeLevel):
268
+ return self._count_notices(self.get_current_context(), level_code_min=level.value, level_code_max=level.value)
269
+
270
+ def count_errors_for_current_context(self):
271
+ return self._count_notices(self.get_current_context(), exact_match=True, level_code_min=self.ERROR_CODE_START_VALUE)
272
+
273
+ def count_errors_for_current_and_nested_contexts(self):
274
+ return self._count_notices(self.get_current_context(), level_code_min=self.ERROR_CODE_START_VALUE)
275
+
276
+ def count_warnings_and_errors_for_current_context(self):
277
+ return self._count_notices(self.get_current_context(), exact_match=True, level_code_min=self.WARNING_CODE_START_VALUE)
278
+
279
+ def count_warnings_and_errors_for_current_and_nested_contexts(self):
280
+ return self._count_notices(self.get_current_context(), level_code_min=self.WARNING_CODE_START_VALUE)
281
+
282
+ def count_warnings_for_current_context(self):
283
+ return self._count_notices(self.get_current_context(), exact_match=True, level_code_min=self.WARNING_CODE_START_VALUE, level_code_max=self.ERROR_CODE_START_VALUE - 1)
284
+
285
+ def count_warnings_for_current_and_nested_contexts(self):
286
+ return self._count_notices(self.get_current_context(), level_code_min=self.WARNING_CODE_START_VALUE, level_code_max=self.ERROR_CODE_START_VALUE - 1)
287
+
288
+ def count_successes_for_current_context(self):
289
+ return self._count_notices(self.get_current_context(), exact_match=True, level_code_min=self.SUCCESS_CODE_START_VALUE, level_code_max=self.SUCCESS_CODE_START_VALUE)
290
+
291
+ def count_successes_for_current_and_nested_contexts(self):
292
+ return self._count_notices(self.get_current_context(), level_code_min=self.SUCCESS_CODE_START_VALUE, level_code_max=self.SUCCESS_CODE_START_VALUE)
293
+
294
+ def export_notices_to_gcs_file(self, bucket_name, storage_client, file_prefix=None, file_name=None, top_level_context=None, save_locally=False, local_path=None, logger=None, max_retries=2):
295
+ def log_message(message):
296
+ if logger:
297
+ logger.info(message)
298
+
299
+ def log_error(message, exc_info=False):
300
+ if logger:
301
+ logger.error(message, exc_info=exc_info)
302
+
303
+ if not file_prefix:
304
+ file_prefix = self._category
305
+ if not file_name:
306
+ timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
307
+ if top_level_context:
308
+ file_name = f"{file_prefix}_{timestamp}_{top_level_context}_len{len(self._notices)}.json"
309
+ else:
310
+ file_name = f"{file_prefix}_{timestamp}_len{len(self._notices)}.json"
311
+
312
+ cloud_path = None
313
+ local_path = None
314
+ try:
315
+ cloud_path, local_path = write_data_to_gcs(
316
+ bucket_name=bucket_name,
317
+ storage_client=storage_client,
318
+ data=self._notices,
319
+ file_name=file_name,
320
+ save_locally=save_locally,
321
+ local_path=local_path,
322
+ logger=logger,
323
+ max_retries=max_retries
324
+ )
325
+ log_message(f"{file_prefix} successfully saved to GCS at {cloud_path} and locally at {local_path}.")
326
+ except Exception as e:
327
+ log_error(f"Failed at export_notices_to_gcs_file for {file_prefix} for file {file_name} to bucket {bucket_name}: {type(e).__name__} - {str(e)}")
328
+
329
+ return cloud_path, local_path
330
+
331
+ def import_notices_from_json(self, json_or_file, logger=None):
332
+ def log_message(message):
333
+ if logger:
334
+ logger.info(message)
335
+
336
+ def log_warning(message, exc_info=False):
337
+ if logger:
338
+ logger.warning(message, exc_info=exc_info)
339
+
340
+ try:
341
+ if isinstance(json_or_file, str): # Load from string
342
+ imported_notices = json.loads(json_or_file)
343
+ elif hasattr(json_or_file, 'read'): # Load from file-like object
344
+ imported_notices = json.load(json_or_file)
345
+ self.add_notices(imported_notices)
346
+ log_message("Successfully imported notices from json.")
347
+ except Exception as e:
348
+ log_warning(f"Failed to import notices from json: {type(e).__name__} - {str(e)}", exc_info=True)
349
+
350
+ def _update_counts(self, notice, remove=False):
351
+ level_code = notice["level_code"]
352
+ level_name = notice["level_name"]
353
+
354
+ if remove:
355
+ if level_code >= self.ERROR_CODE_START_VALUE:
356
+ self._error_count -= 1
357
+ elif level_code >= self.WARNING_CODE_START_VALUE:
358
+ self._warning_count -= 1
359
+ elif level_code >= self.SUCCESS_CODE_START_VALUE:
360
+ self._success_count -= 1
361
+ self._level_counts[level_name] -= 1
362
+ else:
363
+ if level_code >= self.ERROR_CODE_START_VALUE:
364
+ self._error_count += 1
365
+ elif level_code >= self.WARNING_CODE_START_VALUE:
366
+ self._warning_count += 1
367
+ elif level_code == self.SUCCESS_CODE_START_VALUE:
368
+ self._success_count += 1
369
+ self._level_counts[level_name] += 1
@@ -1,8 +1,12 @@
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
6
10
  import traceback
7
11
  from google.cloud import error_reporting, logging as cloud_logging
8
12
  from google.api_core.exceptions import NotFound
@@ -22,7 +26,7 @@ from google.api_core.exceptions import NotFound
22
26
  ## TODO Fix the issue with POST 0B Nan.... printed in Cloud Logging , which is referring to posting to Cloud Logging probably.
23
27
  ENV = os.getenv('ENV', 'LOCAL').strip("'")
24
28
 
25
- def setup_gcp_logger_and_error_report(logger_name):
29
+ def setup_gcp_logger_and_error_report(logger_name,level=logging.INFO, use_cloud_logging=True):
26
30
  """Sets up a logger with Error Reporting and Cloud Logging handlers.
27
31
 
28
32
  Args:
@@ -52,26 +56,26 @@ def setup_gcp_logger_and_error_report(logger_name):
52
56
  self.handleError(record)
53
57
 
54
58
  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)
59
+ logger.setLevel(level)
67
60
 
68
61
  # Add a console handler for local development
69
- if ENV == "LOCAL":
62
+ if ENV == "LOCAL" or not use_cloud_logging:
70
63
  formatter = logging.Formatter('%(levelname)s : %(name)s : %(asctime)s : %(message)s')
71
64
  console_handler = logging.StreamHandler()
72
65
  console_handler.setFormatter(formatter)
73
66
  logger.addHandler(console_handler)
74
67
 
68
+ if use_cloud_logging:
69
+ # Create Error Reporting handler
70
+ error_reporting_handler = ErrorReportingHandler()
71
+
72
+ # Create Google Cloud Logging handler
73
+ cloud_logging_client = cloud_logging.Client()
74
+ cloud_logging_handler = cloud_logging_client.get_default_handler()
75
+
76
+ # Add handlers to the logger
77
+ logger.addHandler(error_reporting_handler)
78
+ logger.addHandler(cloud_logging_handler)
75
79
  return logger
76
80
  ############################################################################
77
81
 
@@ -116,21 +120,93 @@ def read_csv_from_gcs(bucket_name, file_name, storage_client, logger):
116
120
  logger.error(f"An unexpected error occurred: {e}", exc_info=True)
117
121
  return None
118
122
 
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)
123
+
124
+
125
+ def write_data_to_gcs(bucket_name, storage_client, data, file_name=None,
126
+ save_locally=False, local_path=None, logger=None, max_retries=3):
127
+ """Saves data to Google Cloud Storage and optionally locally.
128
+
129
+ This function attempts to upload data to GCS. If the upload fails after
130
+ retries and `save_locally` is True or `local_path` is provided, it attempts
131
+ to save the data locally.
132
+
133
+ Args:
134
+ bucket_name (str): Name of the GCS bucket.
135
+ storage_client (google.cloud.storage.Client): GCS client object.
136
+ data (list, dict, or str): Data to be saved.
137
+ file_name (str, optional): File name for GCS and local. Defaults to None.
138
+ save_locally (bool, optional): Save locally if GCS fails. Defaults to False.
139
+ local_path (str, optional): Local directory to save. Defaults to None.
140
+ logger (logging.Logger, optional): Logger for messages. Defaults to None.
141
+ max_retries (int, optional): Number of GCS upload retries. Defaults to 3.
142
+
143
+ Returns:
144
+ tuple: A tuple containing the GCS path (or None if upload failed) and
145
+ the local path (or None if not saved locally).
146
+
147
+ Raises:
148
+ ValueError: If data is not a list, dict, or str.
149
+ Exception: If GCS upload fails after retries and local saving fails or
150
+ is not requested. If GCS upload fails after retries and
151
+ local saving is requested but unsuccessful.
152
+ """
153
+
154
+ def log_message(message):
155
+ if logger:
156
+ logger.info(message)
157
+
158
+ def log_error(message, exc_info=False):
159
+ if logger:
160
+ logger.error(message, exc_info=exc_info)
161
+
162
+ attempts = 0
163
+ success = False
164
+ cloud_path = None
165
+ local_path_final = None
166
+ gcs_upload_exception = None # Store potential GCS exception
167
+
168
+ if isinstance(data, (list, dict)):
169
+ data_str = json.dumps(data, indent=2)
170
+ elif isinstance(data, str):
171
+ data_str = data
172
+ else:
173
+ raise ValueError("Unsupported data type. It should be a list, dict, or str.")
174
+
175
+ while attempts < max_retries and not success:
176
+ try:
177
+ bucket = storage_client.bucket(bucket_name)
178
+ blob = bucket.blob(file_name)
179
+ blob.upload_from_string(data_str, content_type='application/json')
180
+ cloud_path = f"{bucket_name}/{file_name}"
181
+ log_message(f"Successfully saved file to GCS {cloud_path}.")
182
+ success = True
183
+ except Exception as e:
184
+ gcs_upload_exception = e
185
+ attempts += 1
186
+ if attempts < max_retries:
187
+ time.sleep(2 ** attempts)
188
+ else:
189
+ log_error(f"Failed to write {file_name} to GCS bucket {bucket_name} after {max_retries} attempts :{e}")
190
+
191
+ if not success and (save_locally or local_path):
192
+ try:
193
+ if not local_path:
194
+ local_path_final = os.path.join("/tmp", file_name)
195
+ else:
196
+ local_path_final = os.path.join(local_path, file_name)
197
+ with open(local_path_final, 'w', encoding='utf-8') as f:
198
+ f.write(data_str)
199
+ log_message(f"Saved {file_name} locally at {local_path_final}.")
200
+ except Exception as local_e:
201
+ log_error(f"Failed to write {file_name} locally: {local_e}",exc_info=True)
202
+
203
+ # If GCS upload failed, raise a single exception here
204
+
205
+ if gcs_upload_exception is not None:
206
+ raise gcs_upload_exception # Propagate without nesting
207
+
208
+ return cloud_path, local_path_final
209
+
134
210
 
135
211
  def write_csv_to_gcs(bucket_name, file_name, data, storage_client, logger,log_info_verbose=True):
136
212
  """ Helper function to write a CSV file to Google Cloud Storage """
@@ -151,4 +227,4 @@ def write_csv_to_gcs(bucket_name, file_name, data, storage_client, logger,log_in
151
227
  except ValueError as e:
152
228
  logger.error(f"ValueError: {e}")
153
229
  except Exception as e:
154
- logger.error(f"An unexpected error occurred while writing CSV to GCS: {e}", exc_info=True)
230
+ logger.error(f"An unexpected error occurred while writing CSV to GCS: {e}", exc_info=True)
@@ -5,8 +5,8 @@
5
5
 
6
6
  import datetime
7
7
  from google.cloud import bigquery
8
- from ipulse_shared_core_ftredge.enums.enums_common_utils import NoticeSeverity
9
- from ipulse_shared_core_ftredge.utils_common import create_custom_notice
8
+ from ipulse_shared_core_ftredge.enums.enums_common_utils import NoticeLevel
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(level=NoticeLevel.WARNING_FIX_REQUIRED,
65
65
  subject=field_name,
66
- message=f"Required field '{field_name}' is missing in the updates.")
66
+ description=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,13 +82,13 @@ 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(level=NoticeLevel.WARNING_FIX_REQUIRED,
86
86
  subject=field_name,
87
- message=f"Expected a DATE in YYYY-MM-DD format but got {value}.")
87
+ description=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(level=NoticeLevel.WARNING_FIX_REQUIRED,
90
90
  subject=field_name,
91
- message= f"Expected a DATE or YYYY-MM-DD str format but got {value} of type {type(value).__name__}.")
91
+ description= f"Expected a DATE or YYYY-MM-DD str format but got {value} of type {type(value).__name__}.")
92
92
 
93
93
 
94
94
  def handle_timestamp_fields(field_name, value, dt_ts_to_str):
@@ -104,47 +104,49 @@ 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(level=NoticeLevel.WARNING_FIX_REQUIRED,
108
108
  subject=field_name,
109
- message= f"Expected ISO format TIMESTAMP but got {value}.")
109
+ description= 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(level=NoticeLevel.WARNING_FIX_REQUIRED,
112
112
  subject=field_name,
113
- message= f"Expected ISO format TIMESTAMP but got {value} of type {type(value).__name__}.")
113
+ description= f"Expected ISO format TIMESTAMP but got {value} of type {type(value).__name__}.")
114
114
 
115
115
 
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(level=NoticeLevel.WARNING_FIX_RECOMMENDED,
120
120
  subject= field_name,
121
- message= f"Field exceeds max length: {len(value)}/{max_length}. Truncating.")
122
-
121
+ description= f"Field exceeds max length: {len(value)}/{max_length}. Truncating.")
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(level=NoticeLevel.WARNING_REVIEW_RECOMMENDED,
130
130
  subject=field_name,
131
- message= f"Expected STRING but got {value} of type {type(value).__name__}.")
131
+ description= f"Expected STRING but got {value} of type {type(value).__name__}.")
132
132
 
133
133
  if field_type == "INT64" and not isinstance(value, int):
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(level=NoticeLevel.WARNING_FIX_REQUIRED,
138
138
  subject= field_name,
139
- message=f"Expected INTEGER, but got {value} of type {type(value).__name__}.")
139
+ description=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(level=NoticeLevel.WARNING_FIX_REQUIRED,
145
+ subject=field_name,
146
+ description=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(level=NoticeLevel.WARNING_REVIEW_RECOMMENDED,
149
+ subject=field_name,
150
+ description=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.51
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,24 +0,0 @@
1
- # pylint: disable=missing-module-docstring
2
- # pylint: disable=missing-function-docstring
3
- # pylint: disable=logging-fstring-interpolation
4
- # pylint: disable=line-too-long
5
-
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
- }
13
-
14
-
15
- def create_exception_notice(severity, exception_code,exception_message, exception_traceback=None, subject=None, message=None):
16
- return {
17
- "severity_code": severity.value,
18
- "severity_name": severity.name,
19
- "subject": subject,
20
- "message": message,
21
- "exception_code": exception_code,
22
- "exception_message": exception_message,
23
- "exception_traceback": exception_traceback
24
- }