ipulse-shared-core-ftredge 2.50__py3-none-any.whl → 2.52__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.
- ipulse_shared_core_ftredge/__init__.py +3 -3
- ipulse_shared_core_ftredge/enums/__init__.py +2 -1
- ipulse_shared_core_ftredge/enums/enums_common_utils.py +26 -7
- ipulse_shared_core_ftredge/utils_common.py +262 -300
- ipulse_shared_core_ftredge/utils_gcp.py +28 -34
- ipulse_shared_core_ftredge/utils_templates_and_schemas.py +21 -21
- {ipulse_shared_core_ftredge-2.50.dist-info → ipulse_shared_core_ftredge-2.52.dist-info}/METADATA +1 -1
- {ipulse_shared_core_ftredge-2.50.dist-info → ipulse_shared_core_ftredge-2.52.dist-info}/RECORD +11 -11
- {ipulse_shared_core_ftredge-2.50.dist-info → ipulse_shared_core_ftredge-2.52.dist-info}/WHEEL +1 -1
- {ipulse_shared_core_ftredge-2.50.dist-info → ipulse_shared_core_ftredge-2.52.dist-info}/LICENCE +0 -0
- {ipulse_shared_core_ftredge-2.50.dist-info → ipulse_shared_core_ftredge-2.52.dist-info}/top_level.txt +0 -0
|
@@ -2,12 +2,12 @@ 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,
|
|
5
|
+
write_csv_to_gcs, write_json_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 (Notice,
|
|
8
|
+
from .utils_common import (Notice, NoticesManager)
|
|
9
9
|
|
|
10
|
-
from .enums import (
|
|
10
|
+
from .enums import (NoticeManagerCategory, NoticeLevel, Unit, Frequency,
|
|
11
11
|
Module, SubModule, BaseDataCategory,
|
|
12
12
|
FinCoreCategory, FincCoreSubCategory,
|
|
13
13
|
FinCoreRecordsCategory, ExchangeOrPublisher,
|
|
@@ -5,27 +5,35 @@
|
|
|
5
5
|
|
|
6
6
|
from enum import Enum
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
|
|
9
|
+
class NoticeManagerCategory(Enum):
|
|
10
|
+
NOTICES = "notices"
|
|
11
|
+
WARN_ERRS = "warn_errs"
|
|
12
|
+
SUCCESSES = "successes"
|
|
13
|
+
class NoticeLevel(Enum):
|
|
9
14
|
"""
|
|
10
|
-
Standardized
|
|
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
|
-
|
|
16
|
-
|
|
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
28
|
WARNING_REVIEW_RECOMMENDED = 402 # Action recommended to prevent potential future issues
|
|
21
29
|
WARNING_FIX_RECOMMENDED = 403 # Action recommended to prevent potential future issues
|
|
22
30
|
WARNING_FIX_REQUIRED = 404 # Action required, pipeline can likely continue
|
|
23
31
|
|
|
32
|
+
ERROR = 500 # General error, no immediate action required
|
|
24
33
|
# Errors indicate a problem that disrupts normal pipeline execution:
|
|
25
|
-
ERROR_EXCEPTION_REDO =
|
|
26
|
-
ERROR_CUSTOM_REDO =
|
|
34
|
+
ERROR_EXCEPTION_REDO = 501
|
|
35
|
+
ERROR_CUSTOM_REDO = 502 # Temporary error, automatic retry likely to succeed
|
|
27
36
|
|
|
28
|
-
|
|
29
37
|
ERROR_EXCEPTION_INVESTIGATE = 601 # Exception occured after some data was likely persisted (e.g., to GCS or BQ)
|
|
30
38
|
ERROR_CUSTOM_INVESTIGATE= 602
|
|
31
39
|
ERROR_EXCEPTION_PERSTISTANCE = 603 # Exception occured after data was persisted (e.g., to GCS or BQ)
|
|
@@ -37,6 +45,17 @@ class NoticeSeverity(Enum):
|
|
|
37
45
|
|
|
38
46
|
UNKNOWN=1001 # Unknown error, should not be used in normal operation
|
|
39
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
|
+
|
|
58
|
+
|
|
40
59
|
### Exception during full exection, partially saved
|
|
41
60
|
# Exception during ensemble pipeline; modifications collected in local object , nothing persisted
|
|
42
61
|
# Exception during ensemble pipeline; modifications persisted , metadata failed
|
|
@@ -2,116 +2,148 @@
|
|
|
2
2
|
# pylint: disable=missing-function-docstring
|
|
3
3
|
# pylint: disable=logging-fstring-interpolation
|
|
4
4
|
# pylint: disable=line-too-long
|
|
5
|
+
# pylint: disable=missing-class-docstring
|
|
5
6
|
import traceback
|
|
6
7
|
import json
|
|
7
|
-
import
|
|
8
|
-
import time
|
|
8
|
+
import uuid
|
|
9
9
|
from datetime import datetime, timezone
|
|
10
10
|
from contextlib import contextmanager
|
|
11
11
|
from typing import List
|
|
12
|
-
from
|
|
13
|
-
from ipulse_shared_core_ftredge.
|
|
14
|
-
|
|
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
|
|
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()
|
|
29
|
-
|
|
30
|
-
# Prepare the base notice dictionary with all fields
|
|
31
|
-
notice = {
|
|
32
|
-
"severity_code": severity.value,
|
|
33
|
-
"severity_name": severity.name,
|
|
34
|
-
"subject": subject,
|
|
35
|
-
"message": message,
|
|
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
|
|
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_json_to_gcs
|
|
67
15
|
|
|
68
16
|
|
|
69
17
|
# ["data_import","data_quality", "data_processing","data_general","data_persistance","metadata_quality", "metadata_processing", "metadata_persistance","metadata_general"]
|
|
70
18
|
|
|
71
19
|
class Notice:
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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):
|
|
75
25
|
if e is not None:
|
|
76
26
|
e_type = type(e).__name__ if e_type is None else e_type
|
|
77
27
|
e_message = str(e) if e_message is None else e_message
|
|
78
28
|
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
29
|
elif e_traceback is None and (e_type or e_message):
|
|
81
30
|
e_traceback = traceback.format_exc()
|
|
82
31
|
|
|
83
|
-
self.
|
|
84
|
-
self.severity = severity
|
|
32
|
+
self.level = level
|
|
85
33
|
self.subject = subject
|
|
86
|
-
self.
|
|
87
|
-
self.
|
|
34
|
+
self.description = description
|
|
35
|
+
self._start_context = start_context
|
|
36
|
+
self._context = context
|
|
37
|
+
self.notice_manager_id = notice_manager_id
|
|
88
38
|
self.exception_type = e_type
|
|
89
39
|
self.exception_message = e_message
|
|
90
|
-
self.exception_traceback = e_traceback
|
|
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
|
|
91
103
|
|
|
92
104
|
def to_dict(self):
|
|
93
105
|
return {
|
|
106
|
+
"start_context": self.start_context,
|
|
94
107
|
"context": self.context,
|
|
95
|
-
"
|
|
96
|
-
"
|
|
108
|
+
"level_code": self.level.value,
|
|
109
|
+
"level_name": self.level.name,
|
|
97
110
|
"subject": self.subject,
|
|
98
|
-
"
|
|
111
|
+
"description": self.description,
|
|
99
112
|
"exception_type": self.exception_type,
|
|
100
113
|
"exception_message": self.exception_message,
|
|
101
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
|
|
102
118
|
}
|
|
103
119
|
|
|
104
120
|
class NoticesManager:
|
|
105
|
-
ERROR_CODE_START_VALUE =
|
|
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._id = str(uuid.uuid4())
|
|
127
|
+
self._notices = []
|
|
128
|
+
self._early_stop = False
|
|
129
|
+
self._errors_count = 0
|
|
130
|
+
self._warnings_count = 0
|
|
131
|
+
self._successes_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
|
|
106
143
|
|
|
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
144
|
|
|
113
145
|
@contextmanager
|
|
114
|
-
def
|
|
146
|
+
def context(self, context):
|
|
115
147
|
self.push_context(context)
|
|
116
148
|
try:
|
|
117
149
|
yield
|
|
@@ -119,245 +151,155 @@ class NoticesManager:
|
|
|
119
151
|
self.pop_context()
|
|
120
152
|
|
|
121
153
|
def push_context(self, context):
|
|
122
|
-
self.
|
|
154
|
+
self._context_stack.append(context)
|
|
123
155
|
|
|
124
156
|
def pop_context(self):
|
|
125
|
-
if self.
|
|
126
|
-
self.
|
|
157
|
+
if self._context_stack:
|
|
158
|
+
self._context_stack.pop()
|
|
127
159
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if context_substring in notice["context"]
|
|
132
|
-
]
|
|
160
|
+
@property
|
|
161
|
+
def current_context(self):
|
|
162
|
+
return " >> ".join(self._context_stack)
|
|
133
163
|
|
|
134
|
-
|
|
135
|
-
|
|
164
|
+
@property
|
|
165
|
+
def start_context(self):
|
|
166
|
+
return self._start_context
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def id(self):
|
|
170
|
+
return self._id
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def early_stop(self):
|
|
174
|
+
return self._early_stop
|
|
175
|
+
|
|
176
|
+
def set_early_stop(self, max_errors_tolerance:int, create_error_notice=True,pop_context=False):
|
|
177
|
+
self.early_stop = True
|
|
178
|
+
if create_error_notice:
|
|
179
|
+
if pop_context:
|
|
180
|
+
self.pop_context()
|
|
181
|
+
self.add_notice(Notice(level=NoticeLevel.ERROR,
|
|
182
|
+
subject="EARLY_STOP",
|
|
183
|
+
description=f"Total MAX_ERRORS_TOLERANCE of {max_errors_tolerance} has been reached."))
|
|
184
|
+
|
|
185
|
+
def reset_early_stop(self):
|
|
186
|
+
self._early_stop = False
|
|
187
|
+
|
|
188
|
+
def get_early_stop(self):
|
|
189
|
+
return self._early_stop
|
|
136
190
|
|
|
137
|
-
def get_all_notices(self):
|
|
138
|
-
return self.notices
|
|
139
191
|
def add_notice(self, notice: Notice):
|
|
140
|
-
notice.
|
|
192
|
+
if (self._category == NoticeManagerCategory.SUCCESSES.value and notice.level != NoticeLevel.SUCCESS) or \
|
|
193
|
+
(self._category == NoticeManagerCategory.WARN_ERRS.value and notice.level.value < self.WARNING_CODE_START_VALUE):
|
|
194
|
+
raise ValueError(f"Invalid notice level {notice.level.name} for category {self._category}")
|
|
195
|
+
notice.start_context = self.start_context
|
|
196
|
+
notice.context = self.current_context
|
|
197
|
+
notice.notice_manager_id = self.id
|
|
141
198
|
notice_dict = notice.to_dict()
|
|
142
|
-
self.
|
|
199
|
+
self._notices.append(notice_dict)
|
|
143
200
|
self._update_counts(notice_dict)
|
|
144
201
|
|
|
202
|
+
if self._logger:
|
|
203
|
+
if notice.level.value >= self.WARNING_CODE_START_VALUE:
|
|
204
|
+
self._logger.log_struct(notice_dict, severity="WARNING")
|
|
205
|
+
else:
|
|
206
|
+
self._logger.log_struct(notice_dict, severity="INFO")
|
|
207
|
+
|
|
145
208
|
def add_notices(self, notices: List[Notice]):
|
|
146
209
|
for notice in notices:
|
|
147
|
-
|
|
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)
|
|
210
|
+
self.add_notice(notice)
|
|
157
211
|
|
|
158
|
-
def
|
|
159
|
-
self.
|
|
160
|
-
self.
|
|
161
|
-
self.
|
|
212
|
+
def clear_notices_and_counts(self):
|
|
213
|
+
self._notices = []
|
|
214
|
+
self._errors_count = 0
|
|
215
|
+
self._warnings_count = 0
|
|
216
|
+
self._successes_count = 0
|
|
217
|
+
self._level_counts = {level.name: 0 for level in NoticeLevel}
|
|
162
218
|
|
|
163
|
-
def
|
|
164
|
-
|
|
219
|
+
def clear_notices(self):
|
|
220
|
+
self._notices = []
|
|
165
221
|
|
|
166
|
-
def
|
|
167
|
-
return self.
|
|
222
|
+
def get_all_notices(self):
|
|
223
|
+
return self._notices
|
|
168
224
|
|
|
169
|
-
def
|
|
170
|
-
return self.
|
|
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)
|
|
225
|
+
def get_notices_for_level(self, level: NoticeLevel):
|
|
226
|
+
return [notice for notice in self._notices if notice["level_code"] == level.value]
|
|
180
227
|
|
|
181
|
-
def
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
)
|
|
228
|
+
def get_notices_by_str_in_context(self, context_substring: str):
|
|
229
|
+
return [
|
|
230
|
+
notice for notice in self._notices
|
|
231
|
+
if context_substring in notice["context"]
|
|
232
|
+
]
|
|
212
233
|
|
|
213
|
-
def
|
|
214
|
-
|
|
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)
|
|
234
|
+
def contains_errors(self):
|
|
235
|
+
return self._errors_count > 0
|
|
254
236
|
|
|
255
|
-
|
|
256
|
-
|
|
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)
|
|
237
|
+
def count_errors(self):
|
|
238
|
+
return self._errors_count
|
|
269
239
|
|
|
270
|
-
def
|
|
271
|
-
|
|
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
|
|
240
|
+
def contains_warnings_or_errors(self):
|
|
241
|
+
return self._warnings_count > 0 or self._errors_count > 0
|
|
279
242
|
|
|
243
|
+
def count_warnings_and_errors(self):
|
|
244
|
+
return self._warnings_count + self._errors_count
|
|
280
245
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
self.context = context
|
|
284
|
-
self.subject = subject
|
|
285
|
-
self.timestamp = datetime.now(timezone.utc).isoformat()
|
|
286
|
-
self.description = description
|
|
246
|
+
def count_warnings(self):
|
|
247
|
+
return self._warnings_count
|
|
287
248
|
|
|
288
|
-
def
|
|
289
|
-
return
|
|
290
|
-
"context": self.context or "",
|
|
291
|
-
"subject": self.subject,
|
|
292
|
-
"timestamp": self.timestamp,
|
|
293
|
-
"description": self.description or ""
|
|
294
|
-
}
|
|
249
|
+
def count_successes(self):
|
|
250
|
+
return self._successes_count
|
|
295
251
|
|
|
252
|
+
def count_all_notices(self):
|
|
253
|
+
return len(self._notices)
|
|
296
254
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
self.successlogs = []
|
|
300
|
-
self.context_stack = []
|
|
255
|
+
def count_notices_by_level(self, level: NoticeLevel):
|
|
256
|
+
return self._level_counts.get(level.name, 0)
|
|
301
257
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
258
|
+
def _count_notices(self, context_substring: str, exact_match=False, level_code_min=None, level_code_max=None):
|
|
259
|
+
return sum(
|
|
260
|
+
1 for notice in self._notices
|
|
261
|
+
if (notice["context"] == context_substring if exact_match else context_substring in notice["context"]) and
|
|
262
|
+
(level_code_min is None or notice["level_code"] >= level_code_min) and
|
|
263
|
+
(level_code_max is None or notice["level_code"] <= level_code_max)
|
|
264
|
+
)
|
|
309
265
|
|
|
310
|
-
def
|
|
311
|
-
self.
|
|
266
|
+
def count_notices_for_current_context(self):
|
|
267
|
+
return self._count_notices(self.current_context, exact_match=True)
|
|
312
268
|
|
|
313
|
-
def
|
|
314
|
-
|
|
315
|
-
self.context_stack.pop()
|
|
269
|
+
def count_notices_for_current_and_nested_contexts(self):
|
|
270
|
+
return self._count_notices(self.current_context)
|
|
316
271
|
|
|
317
|
-
def
|
|
318
|
-
return
|
|
272
|
+
def count_notices_by_level_for_current_context(self, level: NoticeLevel):
|
|
273
|
+
return self._count_notices(self.current_context, exact_match=True, level_code_min=level.value, level_code_max=level.value)
|
|
319
274
|
|
|
320
|
-
def
|
|
321
|
-
return self.
|
|
275
|
+
def count_notices_by_level_for_current_and_nested_contexts(self, level: NoticeLevel):
|
|
276
|
+
return self._count_notices(self.current_context, level_code_min=level.value, level_code_max=level.value)
|
|
322
277
|
|
|
323
|
-
def
|
|
324
|
-
|
|
325
|
-
successlog_dict = successlog.to_dict()
|
|
326
|
-
self.successlogs.append(successlog_dict)
|
|
278
|
+
def count_errors_for_current_context(self):
|
|
279
|
+
return self._count_notices(self.current_context, exact_match=True, level_code_min=self.ERROR_CODE_START_VALUE)
|
|
327
280
|
|
|
328
|
-
def
|
|
329
|
-
|
|
330
|
-
successlog.context = self.get_current_context()
|
|
331
|
-
successlog_dict = successlog.to_dict()
|
|
332
|
-
self.successlogs.append(successlog_dict)
|
|
281
|
+
def count_errors_for_current_and_nested_contexts(self):
|
|
282
|
+
return self._count_notices(self.current_context, level_code_min=self.ERROR_CODE_START_VALUE)
|
|
333
283
|
|
|
334
|
-
def
|
|
335
|
-
|
|
336
|
-
if successlog_dict in self.successlogs:
|
|
337
|
-
self.successlogs.remove(successlog_dict)
|
|
284
|
+
def count_warnings_and_errors_for_current_context(self):
|
|
285
|
+
return self._count_notices(self.current_context, exact_match=True, level_code_min=self.WARNING_CODE_START_VALUE)
|
|
338
286
|
|
|
339
|
-
def
|
|
340
|
-
self.
|
|
287
|
+
def count_warnings_and_errors_for_current_and_nested_contexts(self):
|
|
288
|
+
return self._count_notices(self.current_context, level_code_min=self.WARNING_CODE_START_VALUE)
|
|
341
289
|
|
|
342
|
-
def
|
|
343
|
-
return
|
|
290
|
+
def count_warnings_for_current_context(self):
|
|
291
|
+
return self._count_notices(self.current_context, exact_match=True, level_code_min=self.WARNING_CODE_START_VALUE, level_code_max=self.ERROR_CODE_START_VALUE - 1)
|
|
344
292
|
|
|
345
|
-
def
|
|
346
|
-
current_context = self.
|
|
347
|
-
return sum(
|
|
348
|
-
1 for successlog in self.successlogs
|
|
349
|
-
if successlog["context"] == current_context
|
|
350
|
-
)
|
|
293
|
+
def count_warnings_for_current_and_nested_contexts(self):
|
|
294
|
+
return self._count_notices(self.current_context, level_code_min=self.WARNING_CODE_START_VALUE, level_code_max=self.ERROR_CODE_START_VALUE - 1)
|
|
351
295
|
|
|
352
|
-
def
|
|
353
|
-
current_context = self.
|
|
354
|
-
return sum(
|
|
355
|
-
1 for successlog in self.successlogs
|
|
356
|
-
if current_context in successlog["context"]
|
|
357
|
-
)
|
|
296
|
+
def count_successes_for_current_context(self):
|
|
297
|
+
return self._count_notices(self.current_context, exact_match=True, level_code_min=self.SUCCESS_CODE_START_VALUE, level_code_max=self.SUCCESS_CODE_START_VALUE)
|
|
358
298
|
|
|
299
|
+
def count_successes_for_current_and_nested_contexts(self):
|
|
300
|
+
return self._count_notices(self.current_context, level_code_min=self.SUCCESS_CODE_START_VALUE, level_code_max=self.SUCCESS_CODE_START_VALUE)
|
|
359
301
|
|
|
360
|
-
def
|
|
302
|
+
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):
|
|
361
303
|
def log_message(message):
|
|
362
304
|
if logger:
|
|
363
305
|
logger.info(message)
|
|
@@ -366,50 +308,70 @@ class SuccessLogManager:
|
|
|
366
308
|
if logger:
|
|
367
309
|
logger.error(message, exc_info=exc_info)
|
|
368
310
|
|
|
311
|
+
if not file_prefix:
|
|
312
|
+
file_prefix = self._category
|
|
369
313
|
if not file_name:
|
|
370
314
|
timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
|
|
371
315
|
if top_level_context:
|
|
372
|
-
file_name = f"
|
|
316
|
+
file_name = f"{file_prefix}_{timestamp}_{top_level_context}_len{len(self._notices)}.json"
|
|
373
317
|
else:
|
|
374
|
-
file_name = f"
|
|
318
|
+
file_name = f"{file_prefix}_{timestamp}_len{len(self._notices)}.json"
|
|
375
319
|
|
|
376
|
-
|
|
377
|
-
local_path=None
|
|
320
|
+
result=None
|
|
378
321
|
try:
|
|
379
|
-
|
|
322
|
+
result= write_json_to_gcs(
|
|
380
323
|
bucket_name=bucket_name,
|
|
381
324
|
storage_client=storage_client,
|
|
382
|
-
data=self.
|
|
325
|
+
data=self._notices,
|
|
383
326
|
file_name=file_name,
|
|
384
327
|
save_locally=save_locally,
|
|
385
328
|
local_path=local_path,
|
|
386
329
|
logger=logger,
|
|
387
|
-
max_retries=max_retries
|
|
330
|
+
max_retries=max_retries,
|
|
331
|
+
overwrite=True
|
|
388
332
|
)
|
|
389
|
-
log_message(f"
|
|
333
|
+
log_message(f"{file_prefix} successfully saved (ovewritten={result.get("ovewritten")}) to GCS at {result.get("gcs_path")} and locally at {result.get("local_path")}.")
|
|
390
334
|
except Exception as e:
|
|
391
|
-
log_error(f"Failed to
|
|
335
|
+
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)}")
|
|
392
336
|
|
|
393
|
-
return
|
|
394
|
-
|
|
395
|
-
def
|
|
337
|
+
return result
|
|
338
|
+
|
|
339
|
+
def import_notices_from_json(self, json_or_file, logger=None):
|
|
396
340
|
def log_message(message):
|
|
397
341
|
if logger:
|
|
398
342
|
logger.info(message)
|
|
399
|
-
else:
|
|
400
|
-
print(message)
|
|
401
343
|
|
|
402
|
-
def
|
|
344
|
+
def log_warning(message, exc_info=False):
|
|
403
345
|
if logger:
|
|
404
|
-
logger.
|
|
405
|
-
|
|
406
|
-
print(message)
|
|
346
|
+
logger.warning(message, exc_info=exc_info)
|
|
347
|
+
|
|
407
348
|
try:
|
|
408
349
|
if isinstance(json_or_file, str): # Load from string
|
|
409
|
-
|
|
350
|
+
imported_notices = json.loads(json_or_file)
|
|
410
351
|
elif hasattr(json_or_file, 'read'): # Load from file-like object
|
|
411
|
-
|
|
412
|
-
self.
|
|
413
|
-
log_message("Successfully imported
|
|
352
|
+
imported_notices = json.load(json_or_file)
|
|
353
|
+
self.add_notices(imported_notices)
|
|
354
|
+
log_message("Successfully imported notices from json.")
|
|
414
355
|
except Exception as e:
|
|
415
|
-
|
|
356
|
+
log_warning(f"Failed to import notices from json: {type(e).__name__} - {str(e)}", exc_info=True)
|
|
357
|
+
|
|
358
|
+
def _update_counts(self, notice, remove=False):
|
|
359
|
+
level_code = notice["level_code"]
|
|
360
|
+
level_name = notice["level_name"]
|
|
361
|
+
|
|
362
|
+
if remove:
|
|
363
|
+
if level_code >= self.ERROR_CODE_START_VALUE:
|
|
364
|
+
self._errors_count -= 1
|
|
365
|
+
elif level_code >= self.WARNING_CODE_START_VALUE:
|
|
366
|
+
self._warnings_count -= 1
|
|
367
|
+
elif level_code >= self.SUCCESS_CODE_START_VALUE:
|
|
368
|
+
self._successes_count -= 1
|
|
369
|
+
self._level_counts[level_name] -= 1
|
|
370
|
+
else:
|
|
371
|
+
if level_code >= self.ERROR_CODE_START_VALUE:
|
|
372
|
+
self._errors_count += 1
|
|
373
|
+
elif level_code >= self.WARNING_CODE_START_VALUE:
|
|
374
|
+
self._warnings_count += 1
|
|
375
|
+
elif level_code == self.SUCCESS_CODE_START_VALUE:
|
|
376
|
+
self._successes_count += 1
|
|
377
|
+
self._level_counts[level_name] += 1
|
|
@@ -7,7 +7,6 @@ from io import StringIO
|
|
|
7
7
|
import logging
|
|
8
8
|
import os
|
|
9
9
|
import time
|
|
10
|
-
from datetime import datetime, timezone
|
|
11
10
|
import traceback
|
|
12
11
|
from google.cloud import error_reporting, logging as cloud_logging
|
|
13
12
|
from google.api_core.exceptions import NotFound
|
|
@@ -123,47 +122,32 @@ def read_csv_from_gcs(bucket_name, file_name, storage_client, logger):
|
|
|
123
122
|
|
|
124
123
|
|
|
125
124
|
|
|
126
|
-
def
|
|
127
|
-
save_locally=False, local_path=None, logger=None, max_retries=3):
|
|
125
|
+
def write_json_to_gcs(bucket_name, storage_client, data, file_name=None,
|
|
126
|
+
save_locally=False, local_path=None, logger=None, max_retries=3, overwrite=True):
|
|
128
127
|
"""Saves data to Google Cloud Storage and optionally locally.
|
|
129
128
|
|
|
130
129
|
This function attempts to upload data to GCS. If the upload fails after
|
|
131
130
|
retries and `save_locally` is True or `local_path` is provided, it attempts
|
|
132
131
|
to save the data locally.
|
|
133
132
|
|
|
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
133
|
Returns:
|
|
145
|
-
|
|
146
|
-
|
|
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.
|
|
134
|
+
dict: A dictionary containing the GCS path (or None if upload failed),
|
|
135
|
+
the local path (or None if not saved locally), and a boolean indicating if the file was overwritten.
|
|
153
136
|
"""
|
|
154
137
|
|
|
155
138
|
def log_message(message):
|
|
156
139
|
if logger:
|
|
157
140
|
logger.info(message)
|
|
158
141
|
|
|
159
|
-
def log_error(message,
|
|
142
|
+
def log_error(message,exc_info=False):
|
|
160
143
|
if logger:
|
|
161
144
|
logger.error(message, exc_info=exc_info)
|
|
162
145
|
|
|
163
146
|
attempts = 0
|
|
164
147
|
success = False
|
|
165
|
-
|
|
148
|
+
gcs_path = None
|
|
166
149
|
local_path_final = None
|
|
150
|
+
overwritten = False
|
|
167
151
|
gcs_upload_exception = None # Store potential GCS exception
|
|
168
152
|
|
|
169
153
|
if isinstance(data, (list, dict)):
|
|
@@ -177,17 +161,25 @@ def write_data_to_gcs(bucket_name, storage_client, data, file_name=None,
|
|
|
177
161
|
try:
|
|
178
162
|
bucket = storage_client.bucket(bucket_name)
|
|
179
163
|
blob = bucket.blob(file_name)
|
|
164
|
+
|
|
165
|
+
# Check if the file exists and if we should overwrite it
|
|
166
|
+
if blob.exists():
|
|
167
|
+
if not overwrite:
|
|
168
|
+
raise FileExistsError(f"File {file_name} already exists in bucket {bucket_name} and overwrite is set to False.")
|
|
169
|
+
else:
|
|
170
|
+
overwritten = True
|
|
171
|
+
|
|
180
172
|
blob.upload_from_string(data_str, content_type='application/json')
|
|
181
|
-
|
|
182
|
-
log_message(f"Successfully saved file to GCS {
|
|
173
|
+
gcs_path = f"gs://{bucket_name}/{file_name}"
|
|
174
|
+
log_message(f"Successfully saved file to GCS {gcs_path}.")
|
|
183
175
|
success = True
|
|
184
176
|
except Exception as e:
|
|
185
|
-
gcs_upload_exception = e
|
|
177
|
+
gcs_upload_exception = e
|
|
186
178
|
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
179
|
if attempts < max_retries:
|
|
190
180
|
time.sleep(2 ** attempts)
|
|
181
|
+
else:
|
|
182
|
+
log_error(f"Failed to write {file_name} to GCS bucket {bucket_name} after {max_retries} attempts: {e}")
|
|
191
183
|
|
|
192
184
|
if not success and (save_locally or local_path):
|
|
193
185
|
try:
|
|
@@ -199,14 +191,16 @@ def write_data_to_gcs(bucket_name, storage_client, data, file_name=None,
|
|
|
199
191
|
f.write(data_str)
|
|
200
192
|
log_message(f"Saved {file_name} locally at {local_path_final}.")
|
|
201
193
|
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
|
|
194
|
+
log_error(f"Failed to write {file_name} locally: {local_e}", exc_info=True)
|
|
205
195
|
|
|
206
|
-
|
|
207
|
-
|
|
196
|
+
if gcs_upload_exception is not None:
|
|
197
|
+
raise gcs_upload_exception # Propagate without nesting
|
|
208
198
|
|
|
209
|
-
return
|
|
199
|
+
return {
|
|
200
|
+
"gcs_path": gcs_path,
|
|
201
|
+
"local_path": local_path_final,
|
|
202
|
+
"overwritten": overwritten
|
|
203
|
+
}
|
|
210
204
|
|
|
211
205
|
|
|
212
206
|
def write_csv_to_gcs(bucket_name, file_name, data, storage_client, logger,log_info_verbose=True):
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import datetime
|
|
7
7
|
from google.cloud import bigquery
|
|
8
|
-
from ipulse_shared_core_ftredge.enums.enums_common_utils import
|
|
8
|
+
from ipulse_shared_core_ftredge.enums.enums_common_utils import NoticeLevel
|
|
9
9
|
from ipulse_shared_core_ftredge.utils_common import Notice
|
|
10
10
|
|
|
11
11
|
|
|
@@ -61,9 +61,9 @@ def update_check_with_schema_template(updates, schema, dt_ts_to_str=True, check_
|
|
|
61
61
|
valid_updates[field_name] = value
|
|
62
62
|
|
|
63
63
|
elif mode == "REQUIRED":
|
|
64
|
-
notice=Notice(
|
|
64
|
+
notice=Notice(level=NoticeLevel.WARNING_FIX_REQUIRED,
|
|
65
65
|
subject=field_name,
|
|
66
|
-
|
|
66
|
+
description=f"Required field '{field_name}' is missing in the updates.")
|
|
67
67
|
|
|
68
68
|
notices.append(notice)
|
|
69
69
|
|
|
@@ -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, Notice(
|
|
85
|
+
return None, Notice(level=NoticeLevel.WARNING_FIX_REQUIRED,
|
|
86
86
|
subject=field_name,
|
|
87
|
-
|
|
87
|
+
description=f"Expected a DATE in YYYY-MM-DD format but got {value}.")
|
|
88
88
|
else:
|
|
89
|
-
return None, Notice(
|
|
89
|
+
return None, Notice(level=NoticeLevel.WARNING_FIX_REQUIRED,
|
|
90
90
|
subject=field_name,
|
|
91
|
-
|
|
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,21 +104,21 @@ 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, Notice(
|
|
107
|
+
return None, Notice(level=NoticeLevel.WARNING_FIX_REQUIRED,
|
|
108
108
|
subject=field_name,
|
|
109
|
-
|
|
109
|
+
description= f"Expected ISO format TIMESTAMP but got {value}.")
|
|
110
110
|
else:
|
|
111
|
-
return None, Notice(
|
|
111
|
+
return None, Notice(level=NoticeLevel.WARNING_FIX_REQUIRED,
|
|
112
112
|
subject=field_name,
|
|
113
|
-
|
|
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], Notice(
|
|
119
|
+
return value[:max_length], Notice(level=NoticeLevel.WARNING_FIX_RECOMMENDED,
|
|
120
120
|
subject= field_name,
|
|
121
|
-
|
|
121
|
+
description= f"Field exceeds max length: {len(value)}/{max_length}. Truncating.")
|
|
122
122
|
|
|
123
123
|
return value, None
|
|
124
124
|
|
|
@@ -126,27 +126,27 @@ def check_and_truncate_length(field_name, value, max_length):
|
|
|
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), Notice(
|
|
129
|
+
return str(value), Notice(level=NoticeLevel.WARNING_REVIEW_RECOMMENDED,
|
|
130
130
|
subject=field_name,
|
|
131
|
-
|
|
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, Notice(
|
|
137
|
+
return None, Notice(level=NoticeLevel.WARNING_FIX_REQUIRED,
|
|
138
138
|
subject= field_name,
|
|
139
|
-
|
|
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, Notice(
|
|
144
|
+
return None, Notice(level=NoticeLevel.WARNING_FIX_REQUIRED,
|
|
145
145
|
subject=field_name,
|
|
146
|
-
|
|
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), Notice(
|
|
148
|
+
return bool(value), Notice(level=NoticeLevel.WARNING_REVIEW_RECOMMENDED,
|
|
149
149
|
subject=field_name,
|
|
150
|
-
|
|
150
|
+
description=f"Expected BOOL, but got {value}. Converting as {bool(value)}.")
|
|
151
151
|
|
|
152
152
|
return value, None
|
{ipulse_shared_core_ftredge-2.50.dist-info → ipulse_shared_core_ftredge-2.52.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: ipulse_shared_core_ftredge
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.52
|
|
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
|
{ipulse_shared_core_ftredge-2.50.dist-info → ipulse_shared_core_ftredge-2.52.dist-info}/RECORD
RENAMED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
ipulse_shared_core_ftredge/__init__.py,sha256=
|
|
2
|
-
ipulse_shared_core_ftredge/utils_common.py,sha256=
|
|
3
|
-
ipulse_shared_core_ftredge/utils_gcp.py,sha256=
|
|
4
|
-
ipulse_shared_core_ftredge/utils_templates_and_schemas.py,sha256=
|
|
5
|
-
ipulse_shared_core_ftredge/enums/__init__.py,sha256=
|
|
6
|
-
ipulse_shared_core_ftredge/enums/enums_common_utils.py,sha256=
|
|
1
|
+
ipulse_shared_core_ftredge/__init__.py,sha256=g3PRhnLbDFPVAFPIpL6tQgiEJFbPGPmeleHoxPNaQzE,879
|
|
2
|
+
ipulse_shared_core_ftredge/utils_common.py,sha256=AT8R7BfEbOvMztgxQKv6d6jwGfTCJMO5FDz9idTSE4A,15564
|
|
3
|
+
ipulse_shared_core_ftredge/utils_gcp.py,sha256=KZSuugt-746lfSAlNi8ks1llxxPzTskmtZwnm18SnhQ,8873
|
|
4
|
+
ipulse_shared_core_ftredge/utils_templates_and_schemas.py,sha256=OriQHxM4AU6T3kGwwhjRdMt3ZYGmMJe0B5PLcHyzgXk,7084
|
|
5
|
+
ipulse_shared_core_ftredge/enums/__init__.py,sha256=Pg8LUhBb7PJAHULoM13TrFEzG9wgCmw-ZuOdN3Rw6Og,853
|
|
6
|
+
ipulse_shared_core_ftredge/enums/enums_common_utils.py,sha256=oW0zhmJZfeYycVXWDCede1_Vaa0Q-KClp_KOK4kzIj8,5261
|
|
7
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
|
|
@@ -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.
|
|
22
|
-
ipulse_shared_core_ftredge-2.
|
|
23
|
-
ipulse_shared_core_ftredge-2.
|
|
24
|
-
ipulse_shared_core_ftredge-2.
|
|
25
|
-
ipulse_shared_core_ftredge-2.
|
|
21
|
+
ipulse_shared_core_ftredge-2.52.dist-info/LICENCE,sha256=YBtYAXNqCCOo9Mr2hfkbSPAM9CeAr2j1VZBSwQTrNwE,1060
|
|
22
|
+
ipulse_shared_core_ftredge-2.52.dist-info/METADATA,sha256=TKMk8quvkJXQbkQmBUcANVJfow831f3dHL2QGqwU9IM,561
|
|
23
|
+
ipulse_shared_core_ftredge-2.52.dist-info/WHEEL,sha256=rWxmBtp7hEUqVLOnTaDOPpR-cZpCDkzhhcBce-Zyd5k,91
|
|
24
|
+
ipulse_shared_core_ftredge-2.52.dist-info/top_level.txt,sha256=8sgYrptpexkA_6_HyGvho26cVFH9kmtGvaK8tHbsGHk,27
|
|
25
|
+
ipulse_shared_core_ftredge-2.52.dist-info/RECORD,,
|
{ipulse_shared_core_ftredge-2.50.dist-info → ipulse_shared_core_ftredge-2.52.dist-info}/LICENCE
RENAMED
|
File without changes
|
|
File without changes
|