|
7 | 7 | from enum import Enum as PyEnum |
8 | 8 | from functools import lru_cache |
9 | 9 | from pprint import pformat |
10 | | -from typing import Any, Deque, Dict, List, Tuple, Union |
| 10 | +from typing import Any, Deque, Dict, List, Optional, Tuple, Union |
11 | 11 |
|
12 | 12 | import csp |
13 | 13 | import duckdb |
@@ -75,6 +75,16 @@ def disable_duckdb_state() -> None: |
75 | 75 | _USE_DUCKDB_STATE = False |
76 | 76 |
|
77 | 77 |
|
| 78 | +def _resolve_keyby_attr(record: Any, path: str) -> Any: |
| 79 | + """Resolve a (possibly dotted) attribute path on ``record``, returning ``None`` if any segment is missing.""" |
| 80 | + obj = record |
| 81 | + for segment in path.split("."): |
| 82 | + if obj is None: |
| 83 | + return None |
| 84 | + obj = getattr(obj, segment, None) |
| 85 | + return obj |
| 86 | + |
| 87 | + |
78 | 88 | class StateType(CoreEnum): |
79 | 89 | UNKNOWN = 0 |
80 | 90 | DEFAULT = 1 |
@@ -149,7 +159,7 @@ def insert(self, record: Any) -> None: |
149 | 159 |
|
150 | 160 | for subkey in self._keyby: |
151 | 161 | # extract the key from the record |
152 | | - subkey_to_use = getattr(record, subkey, None) |
| 162 | + subkey_to_use = _resolve_keyby_attr(record, subkey) |
153 | 163 |
|
154 | 164 | if subkey == self._keyby[-1]: |
155 | 165 | # Put the element there if last |
@@ -408,7 +418,7 @@ def insert(self, record: Struct) -> None: |
408 | 418 | obj_id = None |
409 | 419 | for subkey in self._keyby: |
410 | 420 | # extract the key from the record |
411 | | - subkey_to_use = getattr(record, subkey, None) |
| 421 | + subkey_to_use = _resolve_keyby_attr(record, subkey) |
412 | 422 |
|
413 | 423 | if subkey == self._keyby[-1]: |
414 | 424 | if subkey_to_use not in place.keys(): |
@@ -527,23 +537,45 @@ def get_duckdb_schema_struct(cls: Struct) -> Tuple[Dict, bool]: |
527 | 537 | return (new_type_info, use_duckdb) |
528 | 538 |
|
529 | 539 |
|
530 | | -# NOTE: NEVER access State object directly, always access through the __class_getitem__ API |
| 540 | +# NOTE: For runtime state instances, always use the State[<typ>] API. |
| 541 | +# State() called directly (with no parameterized typ) is the annotation |
| 542 | +# marker form used in `Annotated[ts[X], State(keyby=..., indexer=..., alias=...)]`. |
531 | 543 | class State(BaseState): |
532 | | - def __init__(self, keyby: Union[Tuple[str, ...], str] = ("id",)) -> None: |
533 | | - """Switch case between different state specializations based on the type of the records""" |
| 544 | + # Annotation metadata. Set on every instance; consumed by ChannelsMetaclass |
| 545 | + # when this State is found in a field's Annotated metadata. |
| 546 | + _meta_keyby: Union[Tuple[str, ...], str] = ("id",) |
| 547 | + _meta_indexer: Optional[Union[str, int]] = None |
| 548 | + _meta_alias: Optional[str] = None |
| 549 | + |
| 550 | + def __init__( |
| 551 | + self, |
| 552 | + keyby: Union[Tuple[str, ...], str] = ("id",), |
| 553 | + indexer: Optional[Union[str, int]] = None, |
| 554 | + alias: Optional[str] = None, |
| 555 | + ) -> None: |
| 556 | + """Initialize a State. |
| 557 | +
|
| 558 | + When ``self._typ`` is set (via ``State[T](...)``), this constructs a runtime |
| 559 | + state collection and dispatches to the appropriate backend. Otherwise the |
| 560 | + instance is treated as an annotation marker and only retains its metadata. |
| 561 | + """ |
| 562 | + # Always retain annotation metadata. |
| 563 | + self._meta_keyby = keyby |
| 564 | + self._meta_indexer = indexer |
| 565 | + self._meta_alias = alias |
| 566 | + |
| 567 | + typ = getattr(self, "_typ", None) |
| 568 | + if typ is None: |
| 569 | + # Annotation-marker form; no runtime backend needed. |
| 570 | + return |
534 | 571 |
|
535 | 572 | global _USE_DUCKDB_STATE |
536 | | - try: |
537 | | - typ = self._typ |
538 | | - if _USE_DUCKDB_STATE and isinstance(typ, type) and issubclass(typ, Struct): |
539 | | - schema, use_duckdb = get_duckdb_schema_struct(typ) |
540 | | - if use_duckdb: |
541 | | - self._state_impl = DuckDBState(typ, keyby, schema) |
542 | | - self._state_type = StateType.DUCKDB |
543 | | - return |
544 | | - except AttributeError: |
545 | | - log.warning("Do not create object directly from State use the State[<typ>] API instead for performance reasons") |
546 | | - log.warning("Using DefaultStateClass") |
| 573 | + if _USE_DUCKDB_STATE and isinstance(typ, type) and issubclass(typ, Struct): |
| 574 | + schema, use_duckdb = get_duckdb_schema_struct(typ) |
| 575 | + if use_duckdb: |
| 576 | + self._state_impl = DuckDBState(typ, keyby, schema) |
| 577 | + self._state_type = StateType.DUCKDB |
| 578 | + return |
547 | 579 | self._state_impl = DefaultState(keyby) |
548 | 580 | self._state_type = StateType.DEFAULT |
549 | 581 |
|
|
0 commit comments