Skip to content

Commit 324b528

Browse files
authored
Merge pull request #7681 from freedomofpress/typedef-event-data
feat(`Event`): typedef `Event.data` per `Event.type`
2 parents 408a4f1 + 415f076 commit 324b528

File tree

2 files changed

+49
-10
lines changed

2 files changed

+49
-10
lines changed

securedrop/journalist_app/api2/events.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from dataclasses import asdict
2+
13
from db import db
24
from journalist_app import utils
35
from journalist_app.api2.shared import json_version, save_reply
@@ -119,7 +121,7 @@ def handle_reply_sent(event: Event) -> EventResult:
119121
),
120122
)
121123

122-
reply = save_reply(source, event.data)
124+
reply = save_reply(source, asdict(event.data))
123125
db.session.refresh(source)
124126

125127
return EventResult(

securedrop/journalist_app/api2/types.py

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,20 @@
33
from typing import (
44
Any,
55
List,
6+
Mapping,
67
NewType,
78
Optional,
89
Set,
910
Tuple,
10-
Union,
1111
)
1212

1313
Record = NewType("Record", dict[str, Any])
1414
Version = NewType("Version", str)
1515

1616

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)
1820
SourceUUID = NewType("SourceUUID", str)
1921
ItemUUID = NewType("ItemUUID", str)
2022
JournalistUUID = NewType("JournalistUUID", str)
@@ -62,36 +64,71 @@ class Index:
6264

6365

6466
@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+
6775
version: Version
6876

6977

7078
@dataclass
71-
class ItemTarget:
79+
class SourceTarget(Target):
80+
source_uuid: SourceUUID
81+
82+
83+
@dataclass
84+
class ItemTarget(Target):
7285
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}
74103

75104

76105
@dataclass
77106
class Event:
78107
id: EventID
79-
target: Union[SourceTarget, ItemTarget]
108+
target: Target | Mapping
80109
type: EventType
81-
data: dict[str, Any] = field(default_factory=dict)
110+
data: Optional[EventData | Mapping] = None
82111

83112
def __post_init__(self) -> None:
84113
if not isinstance(self.type, EventType):
85114
self.type = EventType(self.type) # strict enum
86115

87-
if isinstance(self.target, dict):
116+
if not isinstance(self.target, Target):
88117
if "source_uuid" in self.target:
89118
self.target = SourceTarget(**self.target)
90119
elif "item_uuid" in self.target:
91120
self.target = ItemTarget(**self.target)
92121
else:
93122
raise TypeError(f"invalid event target: {self.target}")
94123

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+
95132

96133
@dataclass
97134
class EventResult:

0 commit comments

Comments
 (0)