|
3 | 3 | from typing import ( |
4 | 4 | Any, |
5 | 5 | List, |
| 6 | + Mapping, |
6 | 7 | NewType, |
7 | 8 | Optional, |
8 | 9 | Set, |
9 | 10 | Tuple, |
10 | | - Union, |
11 | 11 | ) |
12 | 12 |
|
13 | 13 | Record = NewType("Record", dict[str, Any]) |
14 | 14 | Version = NewType("Version", str) |
15 | 15 |
|
16 | 16 |
|
17 | | -# TODO: generic UUID[T] in Python 3.12 |
| 17 | +# NB. Ideally we'd have a generic UUID[T], but the semantics don't change |
| 18 | +# before mypy 1.12, which is incompatible with our use elsewhere of sqlmypy. |
| 19 | +ReplyUUID = NewType("ReplyUUID", str) |
18 | 20 | SourceUUID = NewType("SourceUUID", str) |
19 | 21 | ItemUUID = NewType("ItemUUID", str) |
20 | 22 | JournalistUUID = NewType("JournalistUUID", str) |
@@ -62,36 +64,71 @@ class Index: |
62 | 64 |
|
63 | 65 |
|
64 | 66 | @dataclass |
65 | | -class SourceTarget: |
66 | | - source_uuid: SourceUUID |
| 67 | +class Target: |
| 68 | + """Base class for `<Resource>Target` dataclasses, to make their union usable |
| 69 | + at runtime. Subclass at least with: |
| 70 | +
|
| 71 | + <resource>_uuid: <Resource>UUID |
| 72 | +
|
| 73 | + """ |
| 74 | + |
67 | 75 | version: Version |
68 | 76 |
|
69 | 77 |
|
70 | 78 | @dataclass |
71 | | -class ItemTarget: |
| 79 | +class SourceTarget(Target): |
| 80 | + source_uuid: SourceUUID |
| 81 | + |
| 82 | + |
| 83 | +@dataclass |
| 84 | +class ItemTarget(Target): |
72 | 85 | item_uuid: ItemUUID |
73 | | - version: Version |
| 86 | + |
| 87 | + |
| 88 | +@dataclass |
| 89 | +class EventData: |
| 90 | + """ |
| 91 | + Base class for `<EventType>Data dataclasses, to make their union usable at runtime. |
| 92 | + For non-empty events, subclass and add to `EVENT_DATA_TYPES`. |
| 93 | + """ |
| 94 | + |
| 95 | + |
| 96 | +@dataclass |
| 97 | +class ReplySentData(EventData): |
| 98 | + uuid: ReplyUUID |
| 99 | + reply: str |
| 100 | + |
| 101 | + |
| 102 | +EVENT_DATA_TYPES = {EventType.REPLY_SENT: ReplySentData} |
74 | 103 |
|
75 | 104 |
|
76 | 105 | @dataclass |
77 | 106 | class Event: |
78 | 107 | id: EventID |
79 | | - target: Union[SourceTarget, ItemTarget] |
| 108 | + target: Target | Mapping |
80 | 109 | type: EventType |
81 | | - data: dict[str, Any] = field(default_factory=dict) |
| 110 | + data: Optional[EventData | Mapping] = None |
82 | 111 |
|
83 | 112 | def __post_init__(self) -> None: |
84 | 113 | if not isinstance(self.type, EventType): |
85 | 114 | self.type = EventType(self.type) # strict enum |
86 | 115 |
|
87 | | - if isinstance(self.target, dict): |
| 116 | + if not isinstance(self.target, Target): |
88 | 117 | if "source_uuid" in self.target: |
89 | 118 | self.target = SourceTarget(**self.target) |
90 | 119 | elif "item_uuid" in self.target: |
91 | 120 | self.target = ItemTarget(**self.target) |
92 | 121 | else: |
93 | 122 | raise TypeError(f"invalid event target: {self.target}") |
94 | 123 |
|
| 124 | + if not isinstance(self.data, EventData) and self.data and self.type in EVENT_DATA_TYPES: |
| 125 | + try: |
| 126 | + self.data = EVENT_DATA_TYPES[self.type](**self.data) |
| 127 | + except TypeError: |
| 128 | + raise TypeError(f"invalid event data for type {self.type}") |
| 129 | + else: |
| 130 | + self.data = None |
| 131 | + |
95 | 132 |
|
96 | 133 | @dataclass |
97 | 134 | class EventResult: |
|
0 commit comments