tts-data-utils 0.7.3__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.
- tts_data_utils/__init__.py +0 -0
- tts_data_utils/core/__init__.py +0 -0
- tts_data_utils/core/container_history.py +79 -0
- tts_data_utils/core/data_container.py +2102 -0
- tts_data_utils/core/data_item.py +487 -0
- tts_data_utils/core/diff.py +155 -0
- tts_data_utils/core/generic.py +78 -0
- tts_data_utils/core/lorem_utils.py +206 -0
- tts_data_utils/core/visual_diff.py +148 -0
- tts_data_utils/invulnerable_data_manager/__init__.py +0 -0
- tts_data_utils/invulnerable_data_manager/batch.py +176 -0
- tts_data_utils/invulnerable_data_manager/invulnerable_data_manager.py +171 -0
- tts_data_utils/invulnerable_data_manager/utilities.py +151 -0
- tts_data_utils/multimission/__init__.py +0 -0
- tts_data_utils/multimission/alarms.py +109 -0
- tts_data_utils/multimission/eha.py +253 -0
- tts_data_utils/multimission/evr.py +340 -0
- tts_data_utils/multimission/evr_gaps.py +148 -0
- tts_data_utils/multimission/expected_lad.py +161 -0
- tts_data_utils/multimission/planning_rule.py +106 -0
- tts_data_utils/multimission/taco.py +53 -0
- tts_data_utils/test/__init__.py +0 -0
- tts_data_utils/test/core/__init__.py +0 -0
- tts_data_utils/test/core/test_data_container.py +403 -0
- tts_data_utils/test/core/test_data_item.py +92 -0
- tts_data_utils/test/core/test_diff.py +173 -0
- tts_data_utils/test/core/test_files/diff/diff_dict/actual_diff.csv +5135 -0
- tts_data_utils/test/core/test_files/diff/diff_dict/expected_diff.csv +5135 -0
- tts_data_utils/test/core/test_files/diff/diff_dict/left.csv +637 -0
- tts_data_utils/test/core/test_files/diff/diff_dict/right.csv +637 -0
- tts_data_utils/test/core/test_files/diff/diff_list/actual_diff.csv +5135 -0
- tts_data_utils/test/core/test_files/diff/diff_list/expected_diff.csv +5135 -0
- tts_data_utils/test/core/test_files/diff/diff_list/left.csv +637 -0
- tts_data_utils/test/core/test_files/diff/diff_list/right.csv +637 -0
- tts_data_utils/test/core/test_files/diff/diff_list_size/actual_diff.csv +33 -0
- tts_data_utils/test/core/test_files/diff/diff_list_size/expected_diff.csv +33 -0
- tts_data_utils/test/core/test_files/diff/diff_list_size/left.csv +637 -0
- tts_data_utils/test/core/test_files/diff/diff_list_size/right.csv +637 -0
- tts_data_utils/test/core/test_files/diff/diff_simple_types/actual_diff.csv +17894 -0
- tts_data_utils/test/core/test_files/diff/diff_simple_types/expected_diff.csv +17894 -0
- tts_data_utils/test/core/test_files/diff/diff_simple_types/left.csv +637 -0
- tts_data_utils/test/core/test_files/diff/diff_simple_types/right.csv +637 -0
- tts_data_utils/test/core/test_files/diff/diff_type/actual_diff.csv +5135 -0
- tts_data_utils/test/core/test_files/diff/diff_type/expected_diff.csv +5135 -0
- tts_data_utils/test/core/test_files/diff/diff_type/left.csv +637 -0
- tts_data_utils/test/core/test_files/diff/diff_type/right.csv +637 -0
- tts_data_utils/test/core/test_files/diff/same_not_same/actual_diff.csv +5135 -0
- tts_data_utils/test/core/test_files/visdiff/adjacent_insert_delete.html +1581 -0
- tts_data_utils/test/core/test_files/visdiff/insert_and_delete.html +1581 -0
- tts_data_utils/test/core/test_files/visdiff/no_diff.html +1581 -0
- tts_data_utils/test/core/test_files/visdiff/overlapping_insert_delete.html +1541 -0
- tts_data_utils/test/core/test_files/visdiff/replace.html +1581 -0
- tts_data_utils/test/core/test_files/visdiff/replace_with_delete.html +1581 -0
- tts_data_utils/test/core/test_files/visdiff/simple_delete.html +1581 -0
- tts_data_utils/test/core/test_files/visdiff/simple_insert.html +1581 -0
- tts_data_utils/test/core/test_files/visdiff/stressing_case.html +2061 -0
- tts_data_utils/test/core/test_filters.py +319 -0
- tts_data_utils/test/core/test_generic_container.py +22 -0
- tts_data_utils/test/core/test_history.py +44 -0
- tts_data_utils/test/core/test_visdiff.py +193 -0
- tts_data_utils/test/invulnerable_data_manager/test_invulnerable_data_manager.py +150 -0
- tts_data_utils/test/multimission/__init__.py +0 -0
- tts_data_utils/test/multimission/evr/__init__.py +0 -0
- tts_data_utils/test/multimission/evr/test_evr.py +29 -0
- tts_data_utils/test/multimission/evr/test_files/actual_evr_gaps.csv +12 -0
- tts_data_utils/test/multimission/evr/test_files/evrs_with_gaps.csv +30 -0
- tts_data_utils/test/multimission/evr/test_files/expected_evr_gaps.csv +12 -0
- tts_data_utils/util.py +104 -0
- tts_data_utils-0.7.3.dist-info/METADATA +81 -0
- tts_data_utils-0.7.3.dist-info/RECORD +72 -0
- tts_data_utils-0.7.3.dist-info/WHEEL +5 -0
- tts_data_utils-0.7.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#Python Imports
|
|
2
|
+
import pdb
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
#JPL Imports
|
|
7
|
+
from jpl_time import Time
|
|
8
|
+
from jpl_time.jpl_time import TimeConversionError
|
|
9
|
+
|
|
10
|
+
#This Library Imports
|
|
11
|
+
from tts_data_utils.core.data_container import DataContainer
|
|
12
|
+
from tts_data_utils.core.data_item import DataItem
|
|
13
|
+
|
|
14
|
+
class GenericItem(DataItem):
|
|
15
|
+
"""
|
|
16
|
+
DataItem to go with GenericContainer
|
|
17
|
+
|
|
18
|
+
No validation requirements on these items.
|
|
19
|
+
"""
|
|
20
|
+
#these are the fields that will be validated against these
|
|
21
|
+
#data types. Other fields are allowed
|
|
22
|
+
|
|
23
|
+
DICT_VALID_KEYS = []
|
|
24
|
+
|
|
25
|
+
TIME_FORMATS = {}
|
|
26
|
+
NAME = 'Generic Data Item'
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def default_html_row_style(self):
|
|
31
|
+
return {}
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def time(self):
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def time_str(self):
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
class GenericContainer(DataContainer):
|
|
42
|
+
"""
|
|
43
|
+
Container provided for hooking into DataContainer infrastructure for which there is
|
|
44
|
+
no extended class.
|
|
45
|
+
|
|
46
|
+
Usually this class shouldn't be used in production, but is nice to have in development,
|
|
47
|
+
especially when prototyping behavior for data types for which you are not certain you
|
|
48
|
+
will want to eventually define an extension.
|
|
49
|
+
|
|
50
|
+
Some behavior will be missing without data validation defined, and no default styles
|
|
51
|
+
for html or text repr will be available.
|
|
52
|
+
|
|
53
|
+
For parameters, see DataContainer
|
|
54
|
+
"""
|
|
55
|
+
NAME = 'Generic Items'
|
|
56
|
+
DATA_ITEM_CLS = GenericItem
|
|
57
|
+
|
|
58
|
+
def __init__(self, raw_data=None, name=None, **kwargs):
|
|
59
|
+
super().__init__(raw_data=raw_data, **kwargs)
|
|
60
|
+
self.name = 'Generic Container' if name is None else name
|
|
61
|
+
|
|
62
|
+
# Check if we have records to determine default columns
|
|
63
|
+
if self.records:
|
|
64
|
+
# Use the merged values from the first record instead of raw_data
|
|
65
|
+
self._repr_cols = list(self.records[0].values.keys())
|
|
66
|
+
self._csv_cols = self._repr_cols
|
|
67
|
+
|
|
68
|
+
def _impl_init(self):
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def repr_cols(self):
|
|
73
|
+
return self._repr_cols
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def default_time_label(self):
|
|
77
|
+
return
|
|
78
|
+
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utilities for generating lorem ipsum dummy data for DataContainer instances.
|
|
3
|
+
|
|
4
|
+
This module provides functions to generate realistic dummy data based on
|
|
5
|
+
the structure defined in a DataItem class.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import random
|
|
9
|
+
from datetime import datetime, timedelta
|
|
10
|
+
import string
|
|
11
|
+
import uuid
|
|
12
|
+
|
|
13
|
+
# Lorem ipsum words for generating text
|
|
14
|
+
LOREM_WORDS = [
|
|
15
|
+
"lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing", "elit",
|
|
16
|
+
"sed", "do", "eiusmod", "tempor", "incididunt", "ut", "labore", "et", "dolore",
|
|
17
|
+
"magna", "aliqua", "enim", "ad", "minim", "veniam", "quis", "nostrud", "exercitation",
|
|
18
|
+
"ullamco", "laboris", "nisi", "aliquip", "ex", "ea", "commodo", "consequat",
|
|
19
|
+
"duis", "aute", "irure", "reprehenderit", "voluptate", "velit", "esse", "cillum",
|
|
20
|
+
"dolore", "fugiat", "nulla", "pariatur", "excepteur", "sint", "occaecat", "cupidatat",
|
|
21
|
+
"non", "proident", "sunt", "culpa", "qui", "officia", "deserunt", "mollit", "anim",
|
|
22
|
+
"id", "est", "laborum"
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
# Common status values
|
|
26
|
+
STATUS_VALUES = ["pending", "in_progress", "completed", "failed", "success", "unknown", "error"]
|
|
27
|
+
|
|
28
|
+
# Common severity levels
|
|
29
|
+
SEVERITY_LEVELS = ["info", "debug", "warning", "error", "critical"]
|
|
30
|
+
|
|
31
|
+
def generate_lorem_text(min_words=3, max_words=10):
|
|
32
|
+
"""Generate lorem ipsum text with a random number of words."""
|
|
33
|
+
|
|
34
|
+
# nosec B311: Standard random is fine for placeholder text (not cryptographic)
|
|
35
|
+
num_words = random.randint(min_words, max_words) #nosec
|
|
36
|
+
text = " ".join(random.sample(LOREM_WORDS, k=min(num_words, len(LOREM_WORDS)))) #nosec
|
|
37
|
+
return text.capitalize()
|
|
38
|
+
|
|
39
|
+
def generate_value_for_type(type_spec):
|
|
40
|
+
"""
|
|
41
|
+
Generate a random value that matches the given type specification.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
type_spec: A type or tuple of types (from DICT_VALID_KEYS)
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
A random value of the appropriate type
|
|
48
|
+
"""
|
|
49
|
+
# If type_spec is not a tuple, make it one for consistent handling
|
|
50
|
+
if not isinstance(type_spec, tuple):
|
|
51
|
+
types = (type_spec,)
|
|
52
|
+
else:
|
|
53
|
+
types = type_spec
|
|
54
|
+
|
|
55
|
+
# If None is an acceptable type, occasionally return None
|
|
56
|
+
if None in types and random.random() < 0.1: #nosec
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
# Filter out None from types for actual value generation
|
|
60
|
+
valid_types = [t for t in types if t is not None]
|
|
61
|
+
|
|
62
|
+
# If no valid types remain, return None
|
|
63
|
+
if not valid_types:
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
# Choose a random type from the valid options
|
|
67
|
+
chosen_type = random.choice(valid_types) #nosec
|
|
68
|
+
|
|
69
|
+
# Generate a value based on the chosen type
|
|
70
|
+
if chosen_type == str:
|
|
71
|
+
# For strings, generate different formats based on field name patterns
|
|
72
|
+
return generate_lorem_text(2, 8)
|
|
73
|
+
elif chosen_type == int:
|
|
74
|
+
return random.randint(0, 10000) #nosec
|
|
75
|
+
elif chosen_type == float:
|
|
76
|
+
return round(random.uniform(0, 100), 2) #nosec
|
|
77
|
+
elif chosen_type == bool:
|
|
78
|
+
return random.choice([True, False]) #nosec
|
|
79
|
+
elif chosen_type == datetime:
|
|
80
|
+
# Generate a random datetime within the last year
|
|
81
|
+
days_ago = random.randint(0, 365) #nosec
|
|
82
|
+
return datetime.now().replace(microsecond=0) - timedelta(days=days_ago)
|
|
83
|
+
elif chosen_type == dict:
|
|
84
|
+
# Generate a simple dictionary with 1-3 key-value pairs
|
|
85
|
+
num_items = random.randint(1, 3) #nosec
|
|
86
|
+
return {f"key{i}": generate_lorem_text(1, 3) for i in range(num_items)}
|
|
87
|
+
elif chosen_type == list:
|
|
88
|
+
# Generate a list with 1-5 string items
|
|
89
|
+
num_items = random.randint(1, 5) #nosec
|
|
90
|
+
return [generate_lorem_text(1, 2) for _ in range(num_items)]
|
|
91
|
+
else:
|
|
92
|
+
# For any other types, return a string representation
|
|
93
|
+
return str(chosen_type.__name__)
|
|
94
|
+
|
|
95
|
+
def generate_smart_value(key, type_spec):
|
|
96
|
+
"""
|
|
97
|
+
Generate a value for a specific key, using naming conventions to create more realistic data.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
key: The field name
|
|
101
|
+
type_spec: The type specification
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
A value appropriate for the field name and type
|
|
105
|
+
"""
|
|
106
|
+
# Handle common field name patterns
|
|
107
|
+
key_lower = key.lower()
|
|
108
|
+
|
|
109
|
+
# Handle None in type_spec
|
|
110
|
+
if isinstance(type_spec, tuple) and None in type_spec and random.random() < 0.1: #nosec
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
# ID fields
|
|
114
|
+
if key_lower in ('id', 'channelid', 'eventid', 'sessionid') or key_lower.endswith('id'):
|
|
115
|
+
if isinstance(type_spec, tuple):
|
|
116
|
+
if str in type_spec:
|
|
117
|
+
return f"ID-{random.randint(1000, 9999)}" #nosec
|
|
118
|
+
elif int in type_spec:
|
|
119
|
+
return random.randint(1000, 9999) #nosec
|
|
120
|
+
elif type_spec == int:
|
|
121
|
+
return random.randint(1000, 9999) #nosec
|
|
122
|
+
elif type_spec == str:
|
|
123
|
+
return f"ID-{random.randint(1000, 9999)}" #nosec
|
|
124
|
+
|
|
125
|
+
# Name fields
|
|
126
|
+
if key_lower == 'name' or key_lower.endswith('name'):
|
|
127
|
+
return generate_lorem_text(1, 3)
|
|
128
|
+
|
|
129
|
+
# Message fields
|
|
130
|
+
if 'message' in key_lower:
|
|
131
|
+
return generate_lorem_text(5, 15)
|
|
132
|
+
|
|
133
|
+
# Status fields
|
|
134
|
+
if 'status' in key_lower:
|
|
135
|
+
return random.choice(STATUS_VALUES) #nosec
|
|
136
|
+
|
|
137
|
+
# Level or severity fields
|
|
138
|
+
if 'level' in key_lower or 'severity' in key_lower:
|
|
139
|
+
return random.choice(SEVERITY_LEVELS) #nosec
|
|
140
|
+
|
|
141
|
+
# Time fields
|
|
142
|
+
if any(time_word in key_lower for time_word in ['time', 'date', 'scet', 'ert', 'lst', 'rct']):
|
|
143
|
+
if isinstance(type_spec, tuple):
|
|
144
|
+
if datetime in type_spec:
|
|
145
|
+
days_ago = random.randint(0, 365) #nosec
|
|
146
|
+
return datetime.now().replace(microsecond=0) - timedelta(days=days_ago)
|
|
147
|
+
elif str in type_spec:
|
|
148
|
+
# Return an ISO format date string
|
|
149
|
+
days_ago = random.randint(0, 365) #nosec
|
|
150
|
+
dt = datetime.now() - timedelta(days=days_ago)
|
|
151
|
+
return dt.isoformat()
|
|
152
|
+
elif type_spec == datetime:
|
|
153
|
+
days_ago = random.randint(0, 365) #nosec
|
|
154
|
+
return datetime.now().replace(microsecond=0) - timedelta(days=days_ago)
|
|
155
|
+
elif type_spec == str:
|
|
156
|
+
days_ago = random.randint(0, 365) #nosec
|
|
157
|
+
dt = datetime.now() - timedelta(days=days_ago)
|
|
158
|
+
return dt.isoformat()
|
|
159
|
+
|
|
160
|
+
# Clock fields
|
|
161
|
+
if 'sclk' in key_lower:
|
|
162
|
+
return random.uniform(1000000, 9999999) #nosec
|
|
163
|
+
|
|
164
|
+
# Boolean fields
|
|
165
|
+
if any(bool_word in key_lower for bool_word in ['is_', 'has_', 'can_', 'should_', 'fromsse', 'realtime']):
|
|
166
|
+
return random.choice([True, False]) #nosec
|
|
167
|
+
|
|
168
|
+
# Fallback to generic value generation
|
|
169
|
+
return generate_value_for_type(type_spec)
|
|
170
|
+
|
|
171
|
+
def generate_lorem_data_for_item(data_item_cls, num_records=10):
|
|
172
|
+
"""
|
|
173
|
+
Generate lorem ipsum data for a specific DataItem class.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
data_item_cls: The DataItem class to generate data for
|
|
177
|
+
num_records: Number of records to generate
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
List of dictionaries with dummy data matching the DataItem structure
|
|
181
|
+
"""
|
|
182
|
+
data = []
|
|
183
|
+
|
|
184
|
+
# If the class has no DICT_VALID_KEYS, generate generic data
|
|
185
|
+
if not hasattr(data_item_cls, 'DICT_VALID_KEYS') or not data_item_cls.DICT_VALID_KEYS:
|
|
186
|
+
for i in range(num_records):
|
|
187
|
+
record = {
|
|
188
|
+
"id": i + 1,
|
|
189
|
+
"title": generate_lorem_text(2, 5),
|
|
190
|
+
"description": generate_lorem_text(10, 20),
|
|
191
|
+
"timestamp": datetime.now() - timedelta(days=random.randint(0, 365)), #nosec
|
|
192
|
+
"value": round(random.uniform(0, 100), 2), #nosec
|
|
193
|
+
"count": random.randint(1, 1000), #nosec
|
|
194
|
+
"status": random.choice(STATUS_VALUES) #nosec
|
|
195
|
+
}
|
|
196
|
+
data.append(record)
|
|
197
|
+
return data
|
|
198
|
+
|
|
199
|
+
# Generate data based on DICT_VALID_KEYS
|
|
200
|
+
for i in range(num_records):
|
|
201
|
+
record = {}
|
|
202
|
+
for key, type_spec in data_item_cls.DICT_VALID_KEYS:
|
|
203
|
+
record[key] = generate_smart_value(key, type_spec)
|
|
204
|
+
data.append(record)
|
|
205
|
+
|
|
206
|
+
return data
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#Python Imports
|
|
2
|
+
import pdb
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
#JPL Imports
|
|
7
|
+
from jpl_time import Time
|
|
8
|
+
from jpl_time.jpl_time import TimeConversionError
|
|
9
|
+
|
|
10
|
+
#Teamtool Studio Imports
|
|
11
|
+
# None
|
|
12
|
+
|
|
13
|
+
#This Library Imports
|
|
14
|
+
from tts_data_utils.core.data_container import DataContainer
|
|
15
|
+
from tts_data_utils.core.data_item import DataItem
|
|
16
|
+
|
|
17
|
+
class VisualDiffItem(DataItem):
|
|
18
|
+
"""
|
|
19
|
+
A specialized data record designed for side-by-side comparison.
|
|
20
|
+
|
|
21
|
+
**The Concept:**
|
|
22
|
+
When comparing two versions of a dataset (a "Diff"), we don't just care about the
|
|
23
|
+
raw values—we care about the *state* of each row relative to its counterpart.
|
|
24
|
+
Is it new? Was it deleted? Was it modified?
|
|
25
|
+
|
|
26
|
+
A `VisualDiffItem` carries the data values along with metadata (prefixed with `_visdiff`)
|
|
27
|
+
that tells a renderer exactly how to style the row (e.g., green for an insert,
|
|
28
|
+
red for a delete) to make differences immediately obvious to a human reviewer.
|
|
29
|
+
|
|
30
|
+
**Styling Logic:**
|
|
31
|
+
* **Row Level:** The entire background color is determined by the `_visdiff_match` status.
|
|
32
|
+
* **Cell Level:** Individual cells are highlighted if their specific key exists in
|
|
33
|
+
the `_mismatched_keys` list.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
PALETTE = {
|
|
37
|
+
'green': {'background-color': '#95FB95', 'color': '#333333'},
|
|
38
|
+
'blue': {'background-color': '#ADC4DF', 'color': '#333333'},
|
|
39
|
+
'grey': {'background-color': '#BDBEBD', 'color': '#BDBEBD'},
|
|
40
|
+
'red': {'background-color': '#FF614A', 'color': '#333333'},
|
|
41
|
+
'white': {'background-color': '#FFFFF', 'color': '#333333'},
|
|
42
|
+
'error': {'background-color': '#000000', 'color': '#FF614A'}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
MATCH_STATUS_COLORS = {
|
|
47
|
+
'insert': PALETTE['green'],
|
|
48
|
+
'replace': PALETTE['blue'],
|
|
49
|
+
'delete': PALETTE['red'],
|
|
50
|
+
'equal': PALETTE['white'],
|
|
51
|
+
'empty_from_delete': PALETTE['grey'],
|
|
52
|
+
'empty_from_insert': PALETTE['grey'],
|
|
53
|
+
'unmatched': PALETTE['red'],
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
DICT_VALID_KEYS = [
|
|
57
|
+
('_visdiff_index', (int, None)),
|
|
58
|
+
('_visdiff_match', (str, None)),
|
|
59
|
+
('_mismatched_keys', list)
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
TIME_FORMATS = {
|
|
63
|
+
'Time': '%Y-%jT%H:%M:%S.%f'
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
NAME = 'Generic Data Item'
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def default_html_row_style(self):
|
|
71
|
+
"""
|
|
72
|
+
Returns the CSS style dictionary for the table row based on the match status.
|
|
73
|
+
"""
|
|
74
|
+
return self.MATCH_STATUS_COLORS[self['_visdiff_match']]
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def default_html_cell_styles(self):
|
|
78
|
+
"""
|
|
79
|
+
Returns a mapping of column keys to CSS styles, highlighting specific
|
|
80
|
+
mismatched cells.
|
|
81
|
+
"""
|
|
82
|
+
return {k: {'background-color': '#218FFF'} if k in self['_mismatched_keys'] else {} for k in self.values.keys()}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def time(self):
|
|
87
|
+
"""
|
|
88
|
+
Returns the native time object for the item.
|
|
89
|
+
"""
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def time_str(self):
|
|
94
|
+
"""
|
|
95
|
+
Returns a formatted string representation of the item's timestamp.
|
|
96
|
+
"""
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
class VisualDiffContainer(DataContainer):
|
|
100
|
+
"""
|
|
101
|
+
A collection of VisualDiffItems, representing a full comparison report.
|
|
102
|
+
|
|
103
|
+
**Concept:**
|
|
104
|
+
The `VisualDiffContainer` acts as the manager for a set of diffed records. It
|
|
105
|
+
distinguishes between "Display" columns (what the user needs to see) and
|
|
106
|
+
"Metadata" columns (the internal flags used to calculate differences).
|
|
107
|
+
|
|
108
|
+
**Usage Note:**
|
|
109
|
+
By default, any column starting with an underscore (`_`) is treated as internal
|
|
110
|
+
metadata and is hidden from the standard `repr_cols` view, though it remains
|
|
111
|
+
accessible for CSV exports and logic processing.
|
|
112
|
+
|
|
113
|
+
:param raw_data: A list of dictionaries representing the diffed rows.
|
|
114
|
+
:type raw_data: list[dict]
|
|
115
|
+
:param name: The name of the container/report. Defaults to 'Generic Container'.
|
|
116
|
+
:type name: str, optional
|
|
117
|
+
:param kwargs: Additional arguments passed to the parent `DataContainer`.
|
|
118
|
+
"""
|
|
119
|
+
NAME = 'Visual Diff Container'
|
|
120
|
+
DATA_ITEM_CLS = VisualDiffItem
|
|
121
|
+
|
|
122
|
+
def __init__(self, raw_data=None, name=None, **kwargs):
|
|
123
|
+
#TO DO; Clean this up. Shouldn't be checking for a string like this
|
|
124
|
+
#and need to clean up at a higher level
|
|
125
|
+
super().__init__(raw_data=raw_data, **kwargs)
|
|
126
|
+
self.name = 'Generic Container' if name is None else name
|
|
127
|
+
self._repr_cols = [x for x, _ in raw_data[0].items() if not x.startswith('_')]
|
|
128
|
+
self._csv_cols = [x for x, _ in raw_data[0].items()]
|
|
129
|
+
|
|
130
|
+
def _impl_init(self):
|
|
131
|
+
"""
|
|
132
|
+
Internal initialization hook for custom setup logic.
|
|
133
|
+
"""
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def repr_cols(self):
|
|
138
|
+
"""
|
|
139
|
+
The list of columns intended for visual display (excludes internal metadata).
|
|
140
|
+
"""
|
|
141
|
+
return self._repr_cols
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def default_time_label(self):
|
|
145
|
+
"""
|
|
146
|
+
The primary time-based column label used for sorting or indexing.
|
|
147
|
+
"""
|
|
148
|
+
return
|
|
File without changes
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
class Batcher(ABC):
|
|
4
|
+
"""
|
|
5
|
+
An abstract factory for organizing raw data into logical processing groups.
|
|
6
|
+
|
|
7
|
+
**The Concept:**
|
|
8
|
+
Think of a `Batcher` as a sorting machine at a postal office. It takes a giant
|
|
9
|
+
unsorted bin of mail (`source_batch`) and uses specific internal rules to
|
|
10
|
+
place that mail into smaller, specialized bags (`batches`) based on
|
|
11
|
+
destination or priority (e.g., 'daily_maintenance' or 'system_updates').
|
|
12
|
+
|
|
13
|
+
**How it works:**
|
|
14
|
+
1. The `InvulnerableDataManager` provides the `source_batch`.
|
|
15
|
+
2. The `Batcher` checks `REQUIRED_DATA` to ensure it has what it needs.
|
|
16
|
+
3. The internal `_make_batches()` method runs the sorting logic.
|
|
17
|
+
4. Each resulting `Batch` is stored in the `self.batches` list.
|
|
18
|
+
|
|
19
|
+
:param source_batch: The pool of data to be sorted.
|
|
20
|
+
:type source_batch: AllDataBatch
|
|
21
|
+
"""
|
|
22
|
+
@classmethod
|
|
23
|
+
@property
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def NAME(cls):
|
|
26
|
+
"""Name of the data batch type, e.g. 'mobility_backbone' or 'daily_maintenance'"""
|
|
27
|
+
raise NotImplementedError
|
|
28
|
+
|
|
29
|
+
REQUIRED_DATA = []
|
|
30
|
+
|
|
31
|
+
def __init__(self, source_batch):
|
|
32
|
+
self.source_batch = source_batch
|
|
33
|
+
|
|
34
|
+
self.batches = []
|
|
35
|
+
self._make_batches()
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def _make_batches(self):
|
|
39
|
+
"""
|
|
40
|
+
Subclasses must implement this to define how the source data
|
|
41
|
+
is split into individual Batch objects.
|
|
42
|
+
"""
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
def _add_batch(self, new_batch):
|
|
46
|
+
"""
|
|
47
|
+
Internal helper to append a completed batch to the registry.
|
|
48
|
+
"""
|
|
49
|
+
self.batches.append(new_batch)
|
|
50
|
+
|
|
51
|
+
class Batch:
|
|
52
|
+
"""
|
|
53
|
+
A labeled container for a subset of data items.
|
|
54
|
+
|
|
55
|
+
**The Concept:**
|
|
56
|
+
A `Batch` is a logical "slice" of data. Unlike a standard `DataContainer`,
|
|
57
|
+
a Batch can hold multiple types of data containers simultaneously, grouped
|
|
58
|
+
by a common theme (like a specific time window or a shared asset ID).
|
|
59
|
+
|
|
60
|
+
**Data Tagging:**
|
|
61
|
+
If `tag_data` is True, every `DataItem` added to this batch is "stamped"
|
|
62
|
+
with a reference back to this batch. This allows individual data rows to
|
|
63
|
+
"know" which logical groups they belong to later in the pipeline.
|
|
64
|
+
|
|
65
|
+
:param name: Human-readable name for the batch.
|
|
66
|
+
:type name: str
|
|
67
|
+
:param tag_data: If True, calls `.tag_with_batch()` on all ingested items.
|
|
68
|
+
:type tag_data: bool
|
|
69
|
+
"""
|
|
70
|
+
def __init__(self, name, tag_data=True):
|
|
71
|
+
# Inputs
|
|
72
|
+
self.NAME = name
|
|
73
|
+
self.tag_data = tag_data
|
|
74
|
+
# Data storage
|
|
75
|
+
self._data_map = {}
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def data_map(self):
|
|
79
|
+
"""The internal dictionary mapping container names to DataContainers."""
|
|
80
|
+
return self._data_map
|
|
81
|
+
|
|
82
|
+
def has_data(self, name):
|
|
83
|
+
"""Check if a specific data container exists in this batch."""
|
|
84
|
+
return name in self._data_map
|
|
85
|
+
|
|
86
|
+
def set_data(self, **data_map):
|
|
87
|
+
"""
|
|
88
|
+
Bulk-add data containers to the batch via keyword arguments.
|
|
89
|
+
"""
|
|
90
|
+
for name, data in data_map.items():
|
|
91
|
+
self.set_data_one(name, data)
|
|
92
|
+
|
|
93
|
+
def set_data_one(self, name, data):
|
|
94
|
+
"""
|
|
95
|
+
Add a single named DataContainer to the batch.
|
|
96
|
+
|
|
97
|
+
:raises ValueError: If a container with the same name is already present.
|
|
98
|
+
"""
|
|
99
|
+
if self.has_data(name):
|
|
100
|
+
raise ValueError(f'Attempt to set data {name} in batch {self.NAME} twice')
|
|
101
|
+
self._data_map[name] = data
|
|
102
|
+
# Tag the data with this batch if desired
|
|
103
|
+
if self.tag_data:
|
|
104
|
+
for data_item in data:
|
|
105
|
+
data_item.tag_with_batch(self)
|
|
106
|
+
|
|
107
|
+
def get_data(self, name):
|
|
108
|
+
"""Retrieve a DataContainer by name from the internal map."""
|
|
109
|
+
return self._data_map.get(name)
|
|
110
|
+
|
|
111
|
+
def subdivide_on_condition(self, sub_f):
|
|
112
|
+
"""
|
|
113
|
+
Creates a new data map by applying a filter function to every
|
|
114
|
+
container in the batch.
|
|
115
|
+
|
|
116
|
+
:param sub_f: A function that returns a boolean for each DataItem.
|
|
117
|
+
:type sub_f: function
|
|
118
|
+
:return: A dictionary of filtered DataContainers.
|
|
119
|
+
:rtype: dict
|
|
120
|
+
"""
|
|
121
|
+
new_data_map = {}
|
|
122
|
+
for name, data in self.data_map.items():
|
|
123
|
+
new_data_map[name] = data.subdivide_f(sub_f)
|
|
124
|
+
return new_data_map
|
|
125
|
+
|
|
126
|
+
def subdivide_on_time(self, start_time=None, end_time=None):
|
|
127
|
+
"""
|
|
128
|
+
A specialized subdivision helper for temporal windowing.
|
|
129
|
+
|
|
130
|
+
**Usage Note:**
|
|
131
|
+
This relies on the `DataItem.time` property being properly implemented
|
|
132
|
+
in the underlying data objects.
|
|
133
|
+
|
|
134
|
+
:param start_time: The beginning of the window (inclusive).
|
|
135
|
+
:param end_time: The end of the window (inclusive).
|
|
136
|
+
:type start_time, end_time: datetime, optional
|
|
137
|
+
"""
|
|
138
|
+
if start_time is None and end_time is None:
|
|
139
|
+
raise ValueError(f'Attempting to subdivide batch {self.NAME} on time with no time arguments')
|
|
140
|
+
if start_time is None:
|
|
141
|
+
sub_f = lambda x: x.time <= end_time
|
|
142
|
+
elif end_time is None:
|
|
143
|
+
sub_f = lambda x: start_time <= x.time
|
|
144
|
+
else:
|
|
145
|
+
sub_f = lambda x: start_time <= x.time <= end_time
|
|
146
|
+
return self.subdivide_on_condition(sub_f)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class AllDataBatch(Batch):
|
|
150
|
+
"""
|
|
151
|
+
A specialized Batch that serves as the global "Master List" for all input data.
|
|
152
|
+
|
|
153
|
+
**Concept:**
|
|
154
|
+
This batch is initialized with `tag_data=False` because it represents the
|
|
155
|
+
raw, un-categorized state of the system. It is the "Source of Truth" from
|
|
156
|
+
which all other Batchers draw their data.
|
|
157
|
+
"""
|
|
158
|
+
def __init__(self):
|
|
159
|
+
super().__init__('All Data', tag_data=False)
|
|
160
|
+
|
|
161
|
+
class UntaggedBatch(Batch):
|
|
162
|
+
"""
|
|
163
|
+
A "Lost and Found" batch for data that escaped categorization.
|
|
164
|
+
|
|
165
|
+
**Concept:**
|
|
166
|
+
After all Batchers have run, the `UntaggedBatch` scans the `AllDataBatch`
|
|
167
|
+
to find any `DataItem` that does not have any batch tags associated with it.
|
|
168
|
+
This is a critical auditing tool to ensure no data "fell through the cracks"
|
|
169
|
+
of the defined sorting logic.
|
|
170
|
+
|
|
171
|
+
:param all_data_batch: The master data batch to scan for untagged items.
|
|
172
|
+
:type all_data_batch: AllDataBatch
|
|
173
|
+
"""
|
|
174
|
+
def __init__(self, all_data_batch):
|
|
175
|
+
super().__init__('Untagged Data', tag_data=False)
|
|
176
|
+
self.set_data(**all_data_batch.subdivide_on_condition(lambda x: not x.any_batches()))
|