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.
Files changed (72) hide show
  1. tts_data_utils/__init__.py +0 -0
  2. tts_data_utils/core/__init__.py +0 -0
  3. tts_data_utils/core/container_history.py +79 -0
  4. tts_data_utils/core/data_container.py +2102 -0
  5. tts_data_utils/core/data_item.py +487 -0
  6. tts_data_utils/core/diff.py +155 -0
  7. tts_data_utils/core/generic.py +78 -0
  8. tts_data_utils/core/lorem_utils.py +206 -0
  9. tts_data_utils/core/visual_diff.py +148 -0
  10. tts_data_utils/invulnerable_data_manager/__init__.py +0 -0
  11. tts_data_utils/invulnerable_data_manager/batch.py +176 -0
  12. tts_data_utils/invulnerable_data_manager/invulnerable_data_manager.py +171 -0
  13. tts_data_utils/invulnerable_data_manager/utilities.py +151 -0
  14. tts_data_utils/multimission/__init__.py +0 -0
  15. tts_data_utils/multimission/alarms.py +109 -0
  16. tts_data_utils/multimission/eha.py +253 -0
  17. tts_data_utils/multimission/evr.py +340 -0
  18. tts_data_utils/multimission/evr_gaps.py +148 -0
  19. tts_data_utils/multimission/expected_lad.py +161 -0
  20. tts_data_utils/multimission/planning_rule.py +106 -0
  21. tts_data_utils/multimission/taco.py +53 -0
  22. tts_data_utils/test/__init__.py +0 -0
  23. tts_data_utils/test/core/__init__.py +0 -0
  24. tts_data_utils/test/core/test_data_container.py +403 -0
  25. tts_data_utils/test/core/test_data_item.py +92 -0
  26. tts_data_utils/test/core/test_diff.py +173 -0
  27. tts_data_utils/test/core/test_files/diff/diff_dict/actual_diff.csv +5135 -0
  28. tts_data_utils/test/core/test_files/diff/diff_dict/expected_diff.csv +5135 -0
  29. tts_data_utils/test/core/test_files/diff/diff_dict/left.csv +637 -0
  30. tts_data_utils/test/core/test_files/diff/diff_dict/right.csv +637 -0
  31. tts_data_utils/test/core/test_files/diff/diff_list/actual_diff.csv +5135 -0
  32. tts_data_utils/test/core/test_files/diff/diff_list/expected_diff.csv +5135 -0
  33. tts_data_utils/test/core/test_files/diff/diff_list/left.csv +637 -0
  34. tts_data_utils/test/core/test_files/diff/diff_list/right.csv +637 -0
  35. tts_data_utils/test/core/test_files/diff/diff_list_size/actual_diff.csv +33 -0
  36. tts_data_utils/test/core/test_files/diff/diff_list_size/expected_diff.csv +33 -0
  37. tts_data_utils/test/core/test_files/diff/diff_list_size/left.csv +637 -0
  38. tts_data_utils/test/core/test_files/diff/diff_list_size/right.csv +637 -0
  39. tts_data_utils/test/core/test_files/diff/diff_simple_types/actual_diff.csv +17894 -0
  40. tts_data_utils/test/core/test_files/diff/diff_simple_types/expected_diff.csv +17894 -0
  41. tts_data_utils/test/core/test_files/diff/diff_simple_types/left.csv +637 -0
  42. tts_data_utils/test/core/test_files/diff/diff_simple_types/right.csv +637 -0
  43. tts_data_utils/test/core/test_files/diff/diff_type/actual_diff.csv +5135 -0
  44. tts_data_utils/test/core/test_files/diff/diff_type/expected_diff.csv +5135 -0
  45. tts_data_utils/test/core/test_files/diff/diff_type/left.csv +637 -0
  46. tts_data_utils/test/core/test_files/diff/diff_type/right.csv +637 -0
  47. tts_data_utils/test/core/test_files/diff/same_not_same/actual_diff.csv +5135 -0
  48. tts_data_utils/test/core/test_files/visdiff/adjacent_insert_delete.html +1581 -0
  49. tts_data_utils/test/core/test_files/visdiff/insert_and_delete.html +1581 -0
  50. tts_data_utils/test/core/test_files/visdiff/no_diff.html +1581 -0
  51. tts_data_utils/test/core/test_files/visdiff/overlapping_insert_delete.html +1541 -0
  52. tts_data_utils/test/core/test_files/visdiff/replace.html +1581 -0
  53. tts_data_utils/test/core/test_files/visdiff/replace_with_delete.html +1581 -0
  54. tts_data_utils/test/core/test_files/visdiff/simple_delete.html +1581 -0
  55. tts_data_utils/test/core/test_files/visdiff/simple_insert.html +1581 -0
  56. tts_data_utils/test/core/test_files/visdiff/stressing_case.html +2061 -0
  57. tts_data_utils/test/core/test_filters.py +319 -0
  58. tts_data_utils/test/core/test_generic_container.py +22 -0
  59. tts_data_utils/test/core/test_history.py +44 -0
  60. tts_data_utils/test/core/test_visdiff.py +193 -0
  61. tts_data_utils/test/invulnerable_data_manager/test_invulnerable_data_manager.py +150 -0
  62. tts_data_utils/test/multimission/__init__.py +0 -0
  63. tts_data_utils/test/multimission/evr/__init__.py +0 -0
  64. tts_data_utils/test/multimission/evr/test_evr.py +29 -0
  65. tts_data_utils/test/multimission/evr/test_files/actual_evr_gaps.csv +12 -0
  66. tts_data_utils/test/multimission/evr/test_files/evrs_with_gaps.csv +30 -0
  67. tts_data_utils/test/multimission/evr/test_files/expected_evr_gaps.csv +12 -0
  68. tts_data_utils/util.py +104 -0
  69. tts_data_utils-0.7.3.dist-info/METADATA +81 -0
  70. tts_data_utils-0.7.3.dist-info/RECORD +72 -0
  71. tts_data_utils-0.7.3.dist-info/WHEEL +5 -0
  72. 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()))