|
3 | 3 | """ |
4 | 4 |
|
5 | 5 | import re |
| 6 | +from copy import deepcopy |
| 7 | +from typing import Any, Dict, Iterable, Tuple |
6 | 8 |
|
7 | 9 |
|
8 | 10 | def determine_value_type(value: str): |
@@ -203,3 +205,84 @@ def parse_json_filter(filter_str: str) -> tuple: |
203 | 205 | raise ValueError(f"Invalid numeric value '{value}' for operator {operator}") |
204 | 206 |
|
205 | 207 | return pipeline_name, version, field_path, operator, value |
| 208 | + |
| 209 | + |
| 210 | +DEFAULT_NOTE_KEY_TYPE = "string" |
| 211 | + |
| 212 | + |
| 213 | +def _extract_note_key_type(value: Any) -> str: |
| 214 | + """Derive the type string from a note_keys entry.""" |
| 215 | + if isinstance(value, dict): |
| 216 | + for candidate in ("type", "value_type", "data_type", "datatype"): |
| 217 | + candidate_value = value.get(candidate) |
| 218 | + if candidate_value is not None: |
| 219 | + return str(candidate_value) |
| 220 | + return DEFAULT_NOTE_KEY_TYPE |
| 221 | + if value is None: |
| 222 | + return DEFAULT_NOTE_KEY_TYPE |
| 223 | + return str(value) |
| 224 | + |
| 225 | + |
| 226 | +def _hydrate_note_key_entry( |
| 227 | + key: str, value: Any, original_index: int |
| 228 | +) -> Tuple[str, Dict[str, Any], int, int]: |
| 229 | + """ |
| 230 | + Build a normalized representation of a note_key entry while delaying order assignment. |
| 231 | +
|
| 232 | + Returns a tuple of: |
| 233 | + - key name |
| 234 | + - entry dict (deep copied when applicable) |
| 235 | + - provided order (or None) |
| 236 | + - original index (for stability when order is missing) |
| 237 | + """ |
| 238 | + if isinstance(value, dict): |
| 239 | + entry = deepcopy(value) |
| 240 | + provided_order = entry.get("order") |
| 241 | + else: |
| 242 | + entry = {} |
| 243 | + provided_order = None |
| 244 | + |
| 245 | + entry["type"] = _extract_note_key_type(value) |
| 246 | + if provided_order is not None: |
| 247 | + try: |
| 248 | + provided_order = int(provided_order) |
| 249 | + except (ValueError, TypeError): |
| 250 | + provided_order = None |
| 251 | + |
| 252 | + return key, entry, provided_order, original_index |
| 253 | + |
| 254 | + |
| 255 | +def normalize_note_keys(note_keys: Any) -> Dict[str, Dict[str, Any]]: |
| 256 | + """ |
| 257 | + Ensure note_keys are stored as an ordered mapping of key -> metadata, where |
| 258 | + metadata includes a ``type`` and zero-based ``order`` attribute. |
| 259 | +
|
| 260 | + The returned mapping preserves any additional attributes present on each |
| 261 | + entry and reindexes orders sequentially starting at 0. When no explicit |
| 262 | + order is supplied, the original insertion order is used. |
| 263 | + """ |
| 264 | + if not isinstance(note_keys, dict) or not note_keys: |
| 265 | + return {} |
| 266 | + |
| 267 | + hydrated_entries: Iterable[Tuple[str, Dict[str, Any], int, int]] = [ |
| 268 | + _hydrate_note_key_entry(key, value, original_index) |
| 269 | + for original_index, (key, value) in enumerate(note_keys.items()) |
| 270 | + ] |
| 271 | + |
| 272 | + sorted_entries = sorted( |
| 273 | + hydrated_entries, |
| 274 | + key=lambda item: ( |
| 275 | + item[2] is None, |
| 276 | + item[2] if item[2] is not None else item[3], |
| 277 | + item[0], |
| 278 | + ), |
| 279 | + ) |
| 280 | + |
| 281 | + normalized: Dict[str, Dict[str, Any]] = {} |
| 282 | + for new_order, (key, entry, _provided_order, _original_index) in enumerate( |
| 283 | + sorted_entries |
| 284 | + ): |
| 285 | + entry["order"] = new_order |
| 286 | + normalized[key] = entry |
| 287 | + |
| 288 | + return normalized |
0 commit comments