librus-python-api 1.0.0rc1__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.
- librus_python_api/__init__.py +243 -0
- librus_python_api/_notification_codec.py +310 -0
- librus_python_api/_storage.py +367 -0
- librus_python_api/announcements.py +159 -0
- librus_python_api/attachment_routes.py +114 -0
- librus_python_api/attachments.py +297 -0
- librus_python_api/attendance.py +182 -0
- librus_python_api/attendance_frequency.py +112 -0
- librus_python_api/budget.py +79 -0
- librus_python_api/checkpoint.py +61 -0
- librus_python_api/completed_lessons.py +216 -0
- librus_python_api/config.py +1410 -0
- librus_python_api/detail_fields.py +50 -0
- librus_python_api/diagnostics.py +25 -0
- librus_python_api/exceptions.py +172 -0
- librus_python_api/files.py +156 -0
- librus_python_api/grade_parsers.py +169 -0
- librus_python_api/grade_records.py +454 -0
- librus_python_api/homework_range.py +41 -0
- librus_python_api/lifecycle.py +24 -0
- librus_python_api/markup.py +147 -0
- librus_python_api/message_content.py +230 -0
- librus_python_api/messages.py +288 -0
- librus_python_api/models.py +1160 -0
- librus_python_api/modern_body.py +75 -0
- librus_python_api/modern_mailbox.py +459 -0
- librus_python_api/modern_messages.py +276 -0
- librus_python_api/notification_models.py +67 -0
- librus_python_api/notification_persistence.py +1044 -0
- librus_python_api/notification_workflow.py +303 -0
- librus_python_api/notifications.py +216 -0
- librus_python_api/parsers.py +232 -0
- librus_python_api/parsing.py +49 -0
- librus_python_api/persistence.py +405 -0
- librus_python_api/py.typed +0 -0
- librus_python_api/recipients.py +271 -0
- librus_python_api/scheduler.py +287 -0
- librus_python_api/school_reads.py +400 -0
- librus_python_api/sending.py +125 -0
- librus_python_api/service.py +2285 -0
- librus_python_api/timetable.py +261 -0
- librus_python_api/transport.py +956 -0
- librus_python_api-1.0.0rc1.dist-info/METADATA +254 -0
- librus_python_api-1.0.0rc1.dist-info/RECORD +46 -0
- librus_python_api-1.0.0rc1.dist-info/WHEEL +4 -0
- librus_python_api-1.0.0rc1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""Independent bounded async client. General live compatibility is unqualified."""
|
|
2
|
+
|
|
3
|
+
from librus_python_api.attachments import AttachmentStream, ModernAttachmentStream
|
|
4
|
+
from librus_python_api.budget import RequestBudget
|
|
5
|
+
from librus_python_api.config import (
|
|
6
|
+
AccountCredentials,
|
|
7
|
+
ConnectionSettings,
|
|
8
|
+
OperationLimits,
|
|
9
|
+
SchedulerLimits,
|
|
10
|
+
TransportLimits,
|
|
11
|
+
)
|
|
12
|
+
from librus_python_api.models import (
|
|
13
|
+
AccountContext,
|
|
14
|
+
Agenda,
|
|
15
|
+
AgendaDay,
|
|
16
|
+
AgendaEvent,
|
|
17
|
+
Announcement,
|
|
18
|
+
Announcements,
|
|
19
|
+
AttachmentHeaders,
|
|
20
|
+
AttachmentMetadata,
|
|
21
|
+
Attendance,
|
|
22
|
+
AttendanceDetail,
|
|
23
|
+
AttendanceDetailContent,
|
|
24
|
+
AttendanceFrequency,
|
|
25
|
+
AttendanceKind,
|
|
26
|
+
AttendanceRecord,
|
|
27
|
+
AttendanceRecords,
|
|
28
|
+
AttendanceView,
|
|
29
|
+
AttendanceWindow,
|
|
30
|
+
Availability,
|
|
31
|
+
CompletedLesson,
|
|
32
|
+
CompletedLessons,
|
|
33
|
+
CompletedLessonsCursor,
|
|
34
|
+
CompletedLessonsPage,
|
|
35
|
+
DescriptiveGrade,
|
|
36
|
+
DescriptiveGradeSummary,
|
|
37
|
+
DetailField,
|
|
38
|
+
DetailFieldKey,
|
|
39
|
+
FinalGrades,
|
|
40
|
+
FrequencyMeasure,
|
|
41
|
+
GatewayAttendance,
|
|
42
|
+
GatewayAttendanceRecord,
|
|
43
|
+
GradeKind,
|
|
44
|
+
GradeRecords,
|
|
45
|
+
Grades,
|
|
46
|
+
GradeSummaryValue,
|
|
47
|
+
GradeView,
|
|
48
|
+
GradeWindow,
|
|
49
|
+
Homework,
|
|
50
|
+
HomeworkItem,
|
|
51
|
+
HomeworkRangeRequest,
|
|
52
|
+
Identity,
|
|
53
|
+
LuckyNumber,
|
|
54
|
+
MessageAttachment,
|
|
55
|
+
MessageAttachmentReference,
|
|
56
|
+
MessageContent,
|
|
57
|
+
MessageContentData,
|
|
58
|
+
MessageFolder,
|
|
59
|
+
MessageRecipientReceipt,
|
|
60
|
+
MessageReference,
|
|
61
|
+
Messages,
|
|
62
|
+
MessagesCursor,
|
|
63
|
+
MessagesPage,
|
|
64
|
+
MessageSummary,
|
|
65
|
+
MessageTimestamp,
|
|
66
|
+
MessagingBackend,
|
|
67
|
+
ModernAccountData,
|
|
68
|
+
ModernIdentity,
|
|
69
|
+
ModernMessageAttachment,
|
|
70
|
+
ModernMessageAttachmentReference,
|
|
71
|
+
ModernMessageContent,
|
|
72
|
+
ModernMessageRecipientReceipt,
|
|
73
|
+
ModernMessageReference,
|
|
74
|
+
ModernMessages,
|
|
75
|
+
ModernMessagesCursor,
|
|
76
|
+
ModernMessagesPage,
|
|
77
|
+
ModernMessageSummary,
|
|
78
|
+
ModernRecipient,
|
|
79
|
+
ModernRecipientReference,
|
|
80
|
+
ModernRecipients,
|
|
81
|
+
ModernRecipientType,
|
|
82
|
+
ModernRecipientTypeReference,
|
|
83
|
+
ModernRecipientTypes,
|
|
84
|
+
ModernSendSubmission,
|
|
85
|
+
NotificationCategory,
|
|
86
|
+
NotificationCount,
|
|
87
|
+
NotificationCounts,
|
|
88
|
+
NumericGrade,
|
|
89
|
+
Observation,
|
|
90
|
+
Person,
|
|
91
|
+
RecentScheduleEvent,
|
|
92
|
+
Recipient,
|
|
93
|
+
RecipientGroup,
|
|
94
|
+
RecipientGroupChoice,
|
|
95
|
+
RecipientGroupChoices,
|
|
96
|
+
RecipientGroupReference,
|
|
97
|
+
RecipientGroups,
|
|
98
|
+
RecipientReference,
|
|
99
|
+
Recipients,
|
|
100
|
+
ScheduleEventResponse,
|
|
101
|
+
ScheduleEvents,
|
|
102
|
+
ScheduleEventWire,
|
|
103
|
+
SchoolAverage,
|
|
104
|
+
SchoolDetail,
|
|
105
|
+
SchoolReference,
|
|
106
|
+
SendResult,
|
|
107
|
+
SendStatus,
|
|
108
|
+
SendSubmission,
|
|
109
|
+
StudentInformation,
|
|
110
|
+
SubjectFrequencies,
|
|
111
|
+
SubjectFrequency,
|
|
112
|
+
SubjectGradeSummary,
|
|
113
|
+
Timetable,
|
|
114
|
+
TimetableChange,
|
|
115
|
+
TimetableDay,
|
|
116
|
+
TimetableInterval,
|
|
117
|
+
TimetableLesson,
|
|
118
|
+
TimetablePeriod,
|
|
119
|
+
)
|
|
120
|
+
from librus_python_api.sending import SendAttempt
|
|
121
|
+
from librus_python_api.service import AccountClient, LibrusService
|
|
122
|
+
|
|
123
|
+
__version__ = "1.0.0rc1"
|
|
124
|
+
|
|
125
|
+
__all__ = [
|
|
126
|
+
"AccountContext",
|
|
127
|
+
"MessagingBackend",
|
|
128
|
+
"ModernAccountData",
|
|
129
|
+
"ModernIdentity",
|
|
130
|
+
"ModernMessageAttachment",
|
|
131
|
+
"ModernAttachmentStream",
|
|
132
|
+
"ModernMessageAttachmentReference",
|
|
133
|
+
"ModernMessageContent",
|
|
134
|
+
"ModernMessageReference",
|
|
135
|
+
"ModernMessageRecipientReceipt",
|
|
136
|
+
"ModernMessages",
|
|
137
|
+
"ModernMessagesCursor",
|
|
138
|
+
"ModernMessagesPage",
|
|
139
|
+
"ModernMessageSummary",
|
|
140
|
+
"ModernRecipient",
|
|
141
|
+
"ModernRecipientReference",
|
|
142
|
+
"ModernRecipientType",
|
|
143
|
+
"ModernRecipientTypeReference",
|
|
144
|
+
"ModernRecipientTypes",
|
|
145
|
+
"ModernRecipients",
|
|
146
|
+
"ModernSendSubmission",
|
|
147
|
+
"SendAttempt",
|
|
148
|
+
"SendResult",
|
|
149
|
+
"SendStatus",
|
|
150
|
+
"SendSubmission",
|
|
151
|
+
"RecipientGroupChoice",
|
|
152
|
+
"RecipientGroupChoices",
|
|
153
|
+
"MessageRecipientReceipt",
|
|
154
|
+
"NotificationCategory",
|
|
155
|
+
"NotificationCount",
|
|
156
|
+
"NotificationCounts",
|
|
157
|
+
"ScheduleEventWire",
|
|
158
|
+
"ScheduleEventResponse",
|
|
159
|
+
"RecentScheduleEvent",
|
|
160
|
+
"ScheduleEvents",
|
|
161
|
+
"AttachmentStream",
|
|
162
|
+
"AttachmentMetadata",
|
|
163
|
+
"AttachmentHeaders",
|
|
164
|
+
"MessageAttachment",
|
|
165
|
+
"MessageAttachmentReference",
|
|
166
|
+
"MessageContent",
|
|
167
|
+
"MessageContentData",
|
|
168
|
+
"Recipient",
|
|
169
|
+
"RecipientGroup",
|
|
170
|
+
"RecipientGroupReference",
|
|
171
|
+
"RecipientGroups",
|
|
172
|
+
"RecipientReference",
|
|
173
|
+
"Recipients",
|
|
174
|
+
"MessageFolder",
|
|
175
|
+
"MessageReference",
|
|
176
|
+
"MessageSummary",
|
|
177
|
+
"MessageTimestamp",
|
|
178
|
+
"Messages",
|
|
179
|
+
"MessagesCursor",
|
|
180
|
+
"MessagesPage",
|
|
181
|
+
"CompletedLesson",
|
|
182
|
+
"CompletedLessons",
|
|
183
|
+
"CompletedLessonsCursor",
|
|
184
|
+
"CompletedLessonsPage",
|
|
185
|
+
"AccountClient",
|
|
186
|
+
"AccountCredentials",
|
|
187
|
+
"Availability",
|
|
188
|
+
"Attendance",
|
|
189
|
+
"Announcement",
|
|
190
|
+
"Agenda",
|
|
191
|
+
"AgendaDay",
|
|
192
|
+
"AgendaEvent",
|
|
193
|
+
"Homework",
|
|
194
|
+
"HomeworkItem",
|
|
195
|
+
"HomeworkRangeRequest",
|
|
196
|
+
"SchoolDetail",
|
|
197
|
+
"DetailField",
|
|
198
|
+
"DetailFieldKey",
|
|
199
|
+
"SchoolReference",
|
|
200
|
+
"Announcements",
|
|
201
|
+
"AttendanceDetail",
|
|
202
|
+
"AttendanceDetailContent",
|
|
203
|
+
"AttendanceKind",
|
|
204
|
+
"AttendanceFrequency",
|
|
205
|
+
"FrequencyMeasure",
|
|
206
|
+
"GatewayAttendance",
|
|
207
|
+
"GatewayAttendanceRecord",
|
|
208
|
+
"SubjectFrequency",
|
|
209
|
+
"SubjectFrequencies",
|
|
210
|
+
"Timetable",
|
|
211
|
+
"TimetableChange",
|
|
212
|
+
"TimetableDay",
|
|
213
|
+
"TimetableInterval",
|
|
214
|
+
"TimetableLesson",
|
|
215
|
+
"TimetablePeriod",
|
|
216
|
+
"AttendanceRecord",
|
|
217
|
+
"AttendanceRecords",
|
|
218
|
+
"AttendanceView",
|
|
219
|
+
"AttendanceWindow",
|
|
220
|
+
"ConnectionSettings",
|
|
221
|
+
"DescriptiveGrade",
|
|
222
|
+
"DescriptiveGradeSummary",
|
|
223
|
+
"FinalGrades",
|
|
224
|
+
"GradeKind",
|
|
225
|
+
"GradeRecords",
|
|
226
|
+
"Grades",
|
|
227
|
+
"GradeSummaryValue",
|
|
228
|
+
"GradeView",
|
|
229
|
+
"GradeWindow",
|
|
230
|
+
"Identity",
|
|
231
|
+
"LibrusService",
|
|
232
|
+
"LuckyNumber",
|
|
233
|
+
"NumericGrade",
|
|
234
|
+
"Observation",
|
|
235
|
+
"OperationLimits",
|
|
236
|
+
"Person",
|
|
237
|
+
"RequestBudget",
|
|
238
|
+
"SchedulerLimits",
|
|
239
|
+
"SchoolAverage",
|
|
240
|
+
"StudentInformation",
|
|
241
|
+
"SubjectGradeSummary",
|
|
242
|
+
"TransportLimits",
|
|
243
|
+
]
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""Bounded original serialization of typed native records and raw envelopes."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import asdict, dataclass, fields
|
|
7
|
+
from datetime import date, datetime
|
|
8
|
+
from functools import lru_cache
|
|
9
|
+
from typing import Any, cast
|
|
10
|
+
|
|
11
|
+
from pydantic import TypeAdapter, ValidationError
|
|
12
|
+
|
|
13
|
+
from librus_python_api.exceptions import ErrorKind, LibrusError
|
|
14
|
+
from librus_python_api.models import (
|
|
15
|
+
Announcement,
|
|
16
|
+
AttendanceRecord,
|
|
17
|
+
DescriptiveGrade,
|
|
18
|
+
HomeworkItem,
|
|
19
|
+
Identity,
|
|
20
|
+
MessageSummary,
|
|
21
|
+
NotificationCategory,
|
|
22
|
+
NumericGrade,
|
|
23
|
+
Observation,
|
|
24
|
+
RecentScheduleEvent,
|
|
25
|
+
ScheduleEventResponse,
|
|
26
|
+
ScheduleEventWire,
|
|
27
|
+
)
|
|
28
|
+
from librus_python_api.notification_models import (
|
|
29
|
+
NotificationBatch,
|
|
30
|
+
NotificationItem,
|
|
31
|
+
NotificationSeen,
|
|
32
|
+
NotificationState,
|
|
33
|
+
NotificationValue,
|
|
34
|
+
)
|
|
35
|
+
from librus_python_api.notifications import validate_response
|
|
36
|
+
|
|
37
|
+
HEX = re.compile(r"[0-9a-f]{64}")
|
|
38
|
+
WIRE_BYTES = 4 * 1024 * 1024
|
|
39
|
+
META_BYTES = 4 * 1024 * 1024
|
|
40
|
+
ARCHIVE_BYTES = 48 * 1024 * 1024
|
|
41
|
+
_VALUE_TYPES: dict[str, type[Any]] = {
|
|
42
|
+
cls.__name__: cls
|
|
43
|
+
for cls in (
|
|
44
|
+
NumericGrade,
|
|
45
|
+
DescriptiveGrade,
|
|
46
|
+
AttendanceRecord,
|
|
47
|
+
MessageSummary,
|
|
48
|
+
Announcement,
|
|
49
|
+
HomeworkItem,
|
|
50
|
+
RecentScheduleEvent,
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
_CATEGORY_TYPES: dict[NotificationCategory, tuple[type[Any], ...]] = {
|
|
54
|
+
NotificationCategory.GRADES: (NumericGrade, DescriptiveGrade),
|
|
55
|
+
NotificationCategory.ATTENDANCE: (AttendanceRecord,),
|
|
56
|
+
NotificationCategory.MESSAGES: (MessageSummary,),
|
|
57
|
+
NotificationCategory.ANNOUNCEMENTS: (Announcement,),
|
|
58
|
+
NotificationCategory.HOMEWORK: (HomeworkItem,),
|
|
59
|
+
NotificationCategory.AGENDA: (RecentScheduleEvent,),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _dates(value: Any) -> str:
|
|
64
|
+
if isinstance(value, (date, datetime)):
|
|
65
|
+
return value.isoformat()
|
|
66
|
+
raise ValueError("Unsupported value")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def dump(value: Any, maximum: int) -> bytes:
|
|
70
|
+
failed = False
|
|
71
|
+
payload = b""
|
|
72
|
+
try:
|
|
73
|
+
payload = json.dumps(
|
|
74
|
+
value,
|
|
75
|
+
default=_dates,
|
|
76
|
+
ensure_ascii=False,
|
|
77
|
+
sort_keys=True,
|
|
78
|
+
separators=(",", ":"),
|
|
79
|
+
allow_nan=False,
|
|
80
|
+
).encode("utf-8")
|
|
81
|
+
except (TypeError, ValueError, UnicodeError, RecursionError):
|
|
82
|
+
failed = True
|
|
83
|
+
if failed:
|
|
84
|
+
raise LibrusError(ErrorKind.INVALID_INPUT)
|
|
85
|
+
if len(payload) > maximum:
|
|
86
|
+
raise LibrusError(ErrorKind.LIMIT)
|
|
87
|
+
return payload
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _pairs(values: list[tuple[str, Any]]) -> dict[str, Any]:
|
|
91
|
+
result: dict[str, Any] = {}
|
|
92
|
+
for key, value in values:
|
|
93
|
+
if key in result:
|
|
94
|
+
raise ValueError("Duplicate key")
|
|
95
|
+
result[key] = value
|
|
96
|
+
return result
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def load(payload: bytes, maximum: int) -> Any:
|
|
100
|
+
if type(payload) is not bytes or len(payload) > maximum:
|
|
101
|
+
raise LibrusError(ErrorKind.LIMIT)
|
|
102
|
+
failed = False
|
|
103
|
+
result: Any = None
|
|
104
|
+
try:
|
|
105
|
+
result = json.loads(payload, object_pairs_hook=_pairs)
|
|
106
|
+
except (ValueError, UnicodeError, RecursionError):
|
|
107
|
+
failed = True
|
|
108
|
+
if failed:
|
|
109
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
110
|
+
return result
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@lru_cache(maxsize=16)
|
|
114
|
+
def _adapter(cls: type[Any]) -> TypeAdapter[Any]:
|
|
115
|
+
return TypeAdapter(cls)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def typed[T](cls: type[T], value: Any, maximum: int = META_BYTES) -> T:
|
|
119
|
+
if type(value) is not dict or set(value) != {
|
|
120
|
+
f.name for f in fields(cast(Any, cls))
|
|
121
|
+
}:
|
|
122
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
123
|
+
adapter = _adapter(cast(Any, cls))
|
|
124
|
+
failed = False
|
|
125
|
+
result: T | None = None
|
|
126
|
+
try:
|
|
127
|
+
result = cast(T, adapter.validate_json(dump(value, maximum), strict=True))
|
|
128
|
+
except (ValidationError, ValueError, RecursionError):
|
|
129
|
+
failed = True
|
|
130
|
+
if failed:
|
|
131
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
132
|
+
assert result is not None
|
|
133
|
+
return result
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def canonical_notification_id(
|
|
137
|
+
category: NotificationCategory, value: NotificationValue
|
|
138
|
+
) -> str:
|
|
139
|
+
if (
|
|
140
|
+
not isinstance(category, NotificationCategory)
|
|
141
|
+
or type(value) not in _CATEGORY_TYPES[category]
|
|
142
|
+
):
|
|
143
|
+
raise LibrusError(ErrorKind.INVALID_INPUT)
|
|
144
|
+
if isinstance(value, RecentScheduleEvent):
|
|
145
|
+
# Independently specified three-field canonical identity, including data.
|
|
146
|
+
canonical: Any = asdict(value)
|
|
147
|
+
elif isinstance(value, MessageSummary):
|
|
148
|
+
canonical = [
|
|
149
|
+
1,
|
|
150
|
+
category.value,
|
|
151
|
+
value.reference.folder.value,
|
|
152
|
+
value.reference.identifier,
|
|
153
|
+
]
|
|
154
|
+
elif isinstance(value, Announcement):
|
|
155
|
+
canonical = [1, category.value, value.reference]
|
|
156
|
+
elif isinstance(value, HomeworkItem) and value.reference is not None:
|
|
157
|
+
canonical = [1, category.value, value.reference.identifier]
|
|
158
|
+
elif isinstance(value, AttendanceRecord) and value.detail_id is not None:
|
|
159
|
+
canonical = [1, category.value, value.detail_id]
|
|
160
|
+
else:
|
|
161
|
+
canonical = [1, category.value, type(value).__name__, asdict(value)]
|
|
162
|
+
return hashlib.sha256(dump(canonical, META_BYTES)).hexdigest()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def empty_state() -> NotificationState:
|
|
166
|
+
return NotificationState(
|
|
167
|
+
False,
|
|
168
|
+
tuple(NotificationSeen(category, ()) for category in NotificationCategory),
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def state_bytes(state: NotificationState, per_category: int, maximum: int) -> bytes:
|
|
173
|
+
restored = typed(NotificationState, asdict(state))
|
|
174
|
+
if tuple(s.category for s in restored.seen) != tuple(NotificationCategory):
|
|
175
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
176
|
+
for seen in restored.seen:
|
|
177
|
+
if len(seen.identifiers) > per_category:
|
|
178
|
+
raise LibrusError(ErrorKind.LIMIT)
|
|
179
|
+
if len(set(seen.identifiers)) != len(seen.identifiers) or any(
|
|
180
|
+
HEX.fullmatch(v) is None for v in seen.identifiers
|
|
181
|
+
):
|
|
182
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
183
|
+
if not restored.initialized and any(seen.identifiers for seen in restored.seen):
|
|
184
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
185
|
+
return dump(asdict(restored), maximum)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def restore_state(payload: bytes, per_category: int, maximum: int) -> NotificationState:
|
|
189
|
+
state = typed(NotificationState, load(payload, maximum))
|
|
190
|
+
state_bytes(state, per_category, maximum)
|
|
191
|
+
return state
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@dataclass(frozen=True, slots=True)
|
|
195
|
+
class _EnvelopeMetadata:
|
|
196
|
+
version: int
|
|
197
|
+
identity: Identity
|
|
198
|
+
observation: Observation
|
|
199
|
+
content_type: str | None
|
|
200
|
+
content_codings: tuple[str, ...]
|
|
201
|
+
transfer_codings: tuple[str, ...]
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def encode_envelope(response: ScheduleEventResponse, alias: str) -> tuple[bytes, bytes]:
|
|
205
|
+
validate_response(response, alias, WIRE_BYTES)
|
|
206
|
+
wire = response.wire
|
|
207
|
+
metadata = _EnvelopeMetadata(
|
|
208
|
+
response.version,
|
|
209
|
+
response.identity,
|
|
210
|
+
response.observation,
|
|
211
|
+
wire.content_type,
|
|
212
|
+
wire.content_codings,
|
|
213
|
+
wire.transfer_codings,
|
|
214
|
+
)
|
|
215
|
+
payload = dump(asdict(metadata), META_BYTES)
|
|
216
|
+
restore_envelope(payload, wire.body, alias)
|
|
217
|
+
return payload, wire.body
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def restore_envelope(metadata: bytes, body: bytes, alias: str) -> ScheduleEventResponse:
|
|
221
|
+
record = typed(_EnvelopeMetadata, load(metadata, META_BYTES))
|
|
222
|
+
response = ScheduleEventResponse(
|
|
223
|
+
record.version,
|
|
224
|
+
record.identity,
|
|
225
|
+
ScheduleEventWire(
|
|
226
|
+
body, record.content_type, record.content_codings, record.transfer_codings
|
|
227
|
+
),
|
|
228
|
+
record.observation,
|
|
229
|
+
)
|
|
230
|
+
validate_response(response, alias, WIRE_BYTES)
|
|
231
|
+
for observation in (record.observation, record.identity.observation):
|
|
232
|
+
if (
|
|
233
|
+
observation.observed_at.tzinfo is None
|
|
234
|
+
or not 0 <= observation.session_generation <= 10**12
|
|
235
|
+
):
|
|
236
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
237
|
+
return response
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def encode_batch(batch: NotificationBatch, maximum: int) -> bytes:
|
|
241
|
+
payload = asdict(batch)
|
|
242
|
+
payload["items"] = [
|
|
243
|
+
{"kind": type(item.value).__name__, "item": asdict(item)}
|
|
244
|
+
for item in batch.items
|
|
245
|
+
]
|
|
246
|
+
return dump(payload, maximum)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def restore_batch(payload: bytes, maximum: int) -> NotificationBatch:
|
|
250
|
+
record = load(payload, maximum)
|
|
251
|
+
if (
|
|
252
|
+
type(record) is not dict
|
|
253
|
+
or set(record) != {f.name for f in fields(NotificationBatch)}
|
|
254
|
+
or type(record["items"]) is not list
|
|
255
|
+
):
|
|
256
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
257
|
+
items = []
|
|
258
|
+
for entry in record["items"]:
|
|
259
|
+
if (
|
|
260
|
+
type(entry) is not dict
|
|
261
|
+
or set(entry) != {"kind", "item"}
|
|
262
|
+
or type(entry["kind"]) is not str
|
|
263
|
+
or entry["kind"] not in _VALUE_TYPES
|
|
264
|
+
):
|
|
265
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
266
|
+
item = entry["item"]
|
|
267
|
+
if type(item) is not dict or set(item) != {
|
|
268
|
+
f.name for f in fields(NotificationItem)
|
|
269
|
+
}:
|
|
270
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
271
|
+
value = typed(_VALUE_TYPES[entry["kind"]], item["value"])
|
|
272
|
+
# Validate union contents separately, then reconstruct the original type.
|
|
273
|
+
parsed = typed(NotificationItem, item)
|
|
274
|
+
if type(value) not in _CATEGORY_TYPES[
|
|
275
|
+
parsed.category
|
|
276
|
+
] or parsed.identifier != canonical_notification_id(parsed.category, value):
|
|
277
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
278
|
+
items.append(
|
|
279
|
+
NotificationItem(
|
|
280
|
+
parsed.category,
|
|
281
|
+
parsed.identifier,
|
|
282
|
+
value,
|
|
283
|
+
parsed.identity,
|
|
284
|
+
parsed.observation,
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
record["items"] = []
|
|
288
|
+
batch = typed(NotificationBatch, record, maximum)
|
|
289
|
+
if (
|
|
290
|
+
HEX.fullmatch(batch.receipt) is None
|
|
291
|
+
or HEX.fullmatch(batch.context.identifier) is None
|
|
292
|
+
or not batch.categories
|
|
293
|
+
or len(set(batch.categories)) != len(batch.categories)
|
|
294
|
+
):
|
|
295
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
296
|
+
if any(
|
|
297
|
+
item.category not in batch.categories
|
|
298
|
+
or item.observation.account != batch.context.alias
|
|
299
|
+
or item.identity.observation.account != batch.context.alias
|
|
300
|
+
for item in items
|
|
301
|
+
):
|
|
302
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
303
|
+
return NotificationBatch(
|
|
304
|
+
batch.receipt,
|
|
305
|
+
batch.context,
|
|
306
|
+
batch.first_run,
|
|
307
|
+
batch.categories,
|
|
308
|
+
tuple(items),
|
|
309
|
+
batch.has_more_schedule,
|
|
310
|
+
)
|