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.
@@ -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, write_data_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, NoticeSeverity, NoticesManager,SuccessLog, SuccessLogManager)
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,
@@ -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,27 +5,35 @@
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
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 = 502
26
- ERROR_CUSTOM_REDO = 503 # Temporary error, automatic retry likely to succeed
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 os
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 ipulse_shared_core_ftredge.enums.enums_common_utils import NoticeSeverity
13
- from ipulse_shared_core_ftredge.utils_gcp import write_data_to_gcs
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
- 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
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.timestamp = datetime.now(timezone.utc).isoformat()
84
- self.severity = severity
32
+ self.level = level
85
33
  self.subject = subject
86
- self.message = message
87
- self.context = context
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
- "severity_code": self.severity.value,
96
- "severity_name": self.severity.name,
108
+ "level_code": self.level.value,
109
+ "level_name": self.level.name,
97
110
  "subject": self.subject,
98
- "message": self.message,
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 = 500
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 notice_context(self, context):
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.context_stack.append(context)
154
+ self._context_stack.append(context)
123
155
 
124
156
  def pop_context(self):
125
- if self.context_stack:
126
- self.context_stack.pop()
157
+ if self._context_stack:
158
+ self._context_stack.pop()
127
159
 
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
- ]
160
+ @property
161
+ def current_context(self):
162
+ return " >> ".join(self._context_stack)
133
163
 
134
- def get_current_context(self):
135
- return " >> ".join(self.context_stack)
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.context = self.get_current_context()
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.notices.append(notice_dict)
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
- 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)
210
+ self.add_notice(notice)
157
211
 
158
- def clear_notices(self):
159
- self.notices = []
160
- self.error_count = 0
161
- self.severity_counts = {severity.name: 0 for severity in NoticeSeverity}
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 contains_errors(self):
164
- return self.error_count > 0
219
+ def clear_notices(self):
220
+ self._notices = []
165
221
 
166
- def count_errors(self):
167
- return self.error_count
222
+ def get_all_notices(self):
223
+ return self._notices
168
224
 
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)
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 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
- )
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 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)
234
+ def contains_errors(self):
235
+ return self._errors_count > 0
254
236
 
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)
237
+ def count_errors(self):
238
+ return self._errors_count
269
239
 
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
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
- 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
246
+ def count_warnings(self):
247
+ return self._warnings_count
287
248
 
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
- }
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
- class SuccessLogManager:
298
- def __init__(self):
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
- @contextmanager
303
- def successlog_context(self, context):
304
- self.push_context(context)
305
- try:
306
- yield
307
- finally:
308
- self.pop_context()
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 push_context(self, context):
311
- self.context_stack.append(context)
266
+ def count_notices_for_current_context(self):
267
+ return self._count_notices(self.current_context, exact_match=True)
312
268
 
313
- def pop_context(self):
314
- if self.context_stack:
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 get_current_context(self):
318
- return " >> ".join(self.context_stack)
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 get_all_successlogs(self):
321
- return self.successlogs
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 add_successlog(self, successlog: SuccessLog):
324
- successlog.context = self.get_current_context()
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 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)
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 remove_successlog(self, successlog: SuccessLog):
335
- successlog_dict = successlog.to_dict()
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 clear_successlogs(self):
340
- self.successlogs = []
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 count_all_successlogs(self):
343
- return len(self.successlogs)
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 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
- )
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 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
- )
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 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):
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"successlogs_{timestamp}_{top_level_context}_len{len(self.successlogs)}.json"
316
+ file_name = f"{file_prefix}_{timestamp}_{top_level_context}_len{len(self._notices)}.json"
373
317
  else:
374
- file_name = f"successlogs_{timestamp}_len{len(self.successlogs)}.json"
318
+ file_name = f"{file_prefix}_{timestamp}_len{len(self._notices)}.json"
375
319
 
376
- cloud_path=None
377
- local_path=None
320
+ result=None
378
321
  try:
379
- cloud_path, local_path = write_data_to_gcs(
322
+ result= write_json_to_gcs(
380
323
  bucket_name=bucket_name,
381
324
  storage_client=storage_client,
382
- data=self.successlogs,
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"Success logs successfully saved to GCS at {cloud_path} and locally at {local_path}.")
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 export success logs: {type(e).__name__} - {str(e)}", exc_info=True)
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 cloud_path, local_path
394
-
395
- def import_successlogs_from_json(self, json_or_file, logger=None):
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 log_error(message, exc_info=False):
344
+ def log_warning(message, exc_info=False):
403
345
  if logger:
404
- logger.error(message, exc_info=exc_info)
405
- else:
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
- imported_success_logs = json.loads(json_or_file)
350
+ imported_notices = json.loads(json_or_file)
410
351
  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.")
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
- log_error(f"Failed to import success logs from json: {type(e).__name__} - {str(e)}", exc_info=True)
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 write_data_to_gcs(bucket_name, storage_client, data, file_name=None,
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
- 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.
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, exc_info=False):
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
- cloud_path = None
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
- cloud_path = f"{bucket_name}/{file_name}"
182
- log_message(f"Successfully saved file to GCS {cloud_path}.")
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
- if gcs_upload_exception:
207
- raise gcs_upload_exception from None # Propagate without nesting
196
+ if gcs_upload_exception is not None:
197
+ raise gcs_upload_exception # Propagate without nesting
208
198
 
209
- return cloud_path, local_path_final
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 NoticeSeverity
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(severity=NoticeSeverity.WARNING_FIX_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
 
@@ -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(severity=NoticeSeverity.WARNING_FIX_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, Notice(severity=NoticeSeverity.WARNING_FIX_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,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(severity=NoticeSeverity.WARNING_FIX_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, Notice(severity=NoticeSeverity.WARNING_FIX_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], Notice(severity=NoticeSeverity.WARNING_FIX_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.")
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(severity=NoticeSeverity.WARNING_REVIEW_RECOMMENDED,
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, Notice(severity=NoticeSeverity.WARNING_FIX_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, Notice(severity=NoticeSeverity.WARNING_FIX_REQUIRED,
144
+ return None, Notice(level=NoticeLevel.WARNING_FIX_REQUIRED,
145
145
  subject=field_name,
146
- message=f"Expected FLOAT, but got {value} of type {type(value).__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), Notice(severity=NoticeSeverity.WARNING_REVIEW_RECOMMENDED,
148
+ return bool(value), Notice(level=NoticeLevel.WARNING_REVIEW_RECOMMENDED,
149
149
  subject=field_name,
150
- message=f"Expected BOOL, but got {value}. Converting as {bool(value)}.")
150
+ description=f"Expected BOOL, but got {value}. Converting as {bool(value)}.")
151
151
 
152
152
  return value, None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ipulse_shared_core_ftredge
3
- Version: 2.50
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
@@ -1,9 +1,9 @@
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
- ipulse_shared_core_ftredge/enums/__init__.py,sha256=PjxJiUConI2TuaG_Ushe2BaFVjBDw1rbq1E9Vt9nXvE,801
6
- ipulse_shared_core_ftredge/enums/enums_common_utils.py,sha256=aUH5SJOInEvVA_jtKwaBYNc1_5E3ud5IWCVSEiK1cCI,4895
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.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,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (70.3.0)
2
+ Generator: setuptools (71.0.4)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5