librus-python-api 1.0.0__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_bootstrap.py +124 -0
- librus_python_api/_notification_codec.py +396 -0
- librus_python_api/_storage.py +403 -0
- librus_python_api/_windows_filesystem.py +390 -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 +242 -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 +145 -0
- librus_python_api/notification_persistence.py +1243 -0
- librus_python_api/notification_workflow.py +337 -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 +419 -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.0.dist-info/METADATA +262 -0
- librus_python_api-1.0.0.dist-info/RECORD +48 -0
- librus_python_api-1.0.0.dist-info/WHEEL +4 -0
- librus_python_api-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1160 @@
|
|
|
1
|
+
"""Shared transport, wire-validation, domain, and diagnostic data models.
|
|
2
|
+
|
|
3
|
+
Fields are validated at the parser boundary. Explicit serialization is the
|
|
4
|
+
consumer's responsibility. Domain result reprs omit personal data, including aliases.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from datetime import date, datetime, time
|
|
10
|
+
from enum import StrEnum
|
|
11
|
+
from typing import Annotated, Any, Literal
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, ConfigDict, Field, SecretStr, field_validator
|
|
14
|
+
|
|
15
|
+
from librus_python_api.exceptions import ErrorKind
|
|
16
|
+
|
|
17
|
+
type OperationName = Literal[
|
|
18
|
+
"identity",
|
|
19
|
+
"student_information",
|
|
20
|
+
"final_grades",
|
|
21
|
+
"grades",
|
|
22
|
+
"attendance",
|
|
23
|
+
"attendance_detail",
|
|
24
|
+
"gateway_attendance",
|
|
25
|
+
"subject_frequency",
|
|
26
|
+
"timetable",
|
|
27
|
+
"announcements",
|
|
28
|
+
"agenda",
|
|
29
|
+
"agenda_detail",
|
|
30
|
+
"homework",
|
|
31
|
+
"homework_detail",
|
|
32
|
+
"completed_lessons",
|
|
33
|
+
"messages_received",
|
|
34
|
+
"messages_sent",
|
|
35
|
+
"message_content_received",
|
|
36
|
+
"message_content_sent",
|
|
37
|
+
"attachment_resolve",
|
|
38
|
+
"attachment_download",
|
|
39
|
+
"recipient_groups",
|
|
40
|
+
"recipients",
|
|
41
|
+
"notification_counts",
|
|
42
|
+
"consume_schedule_events",
|
|
43
|
+
"decode_schedule_events",
|
|
44
|
+
"send_message",
|
|
45
|
+
"modern_identity",
|
|
46
|
+
"modern_recipient_types",
|
|
47
|
+
"modern_recipients",
|
|
48
|
+
"modern_send_message",
|
|
49
|
+
"modern_school_recipients",
|
|
50
|
+
"modern_class_parents",
|
|
51
|
+
"modern_messages_received",
|
|
52
|
+
"modern_messages_sent",
|
|
53
|
+
"modern_content_received",
|
|
54
|
+
"modern_content_sent",
|
|
55
|
+
"modern_attachment_resolve",
|
|
56
|
+
"modern_archive_attachment_resolve",
|
|
57
|
+
"modern_attachment_download",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True, slots=True)
|
|
62
|
+
class TransportResponse:
|
|
63
|
+
status: int
|
|
64
|
+
body: bytes = field(repr=False)
|
|
65
|
+
url: str = field(repr=False)
|
|
66
|
+
headers: Mapping[str, str] = field(repr=False)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True, slots=True)
|
|
70
|
+
class SchedulerSnapshot:
|
|
71
|
+
active: int
|
|
72
|
+
queued: int
|
|
73
|
+
requests_dispatched: int
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(frozen=True, slots=True)
|
|
77
|
+
class DiagnosticEvent:
|
|
78
|
+
operation: OperationName
|
|
79
|
+
outcome: ErrorKind | Literal["ok", "cancelled"]
|
|
80
|
+
elapsed_seconds: float
|
|
81
|
+
budget_requests_dispatched: int
|
|
82
|
+
budget_response_bytes: int
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(frozen=True, slots=True)
|
|
86
|
+
class LoginSubmission:
|
|
87
|
+
login: SecretStr = field(repr=False)
|
|
88
|
+
password: SecretStr = field(repr=False)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class GradeView(StrEnum):
|
|
92
|
+
ALL = "all"
|
|
93
|
+
WEEK = "week"
|
|
94
|
+
LAST_LOGIN = "last_login"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class AttendanceView(StrEnum):
|
|
98
|
+
ALL = "all"
|
|
99
|
+
WEEK = "week"
|
|
100
|
+
LAST_LOGIN = "last_login"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class MessageFolder(StrEnum):
|
|
104
|
+
RECEIVED = "received"
|
|
105
|
+
SENT = "sent"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class SendStatus(StrEnum):
|
|
109
|
+
NOT_DISPATCHED = "not_dispatched"
|
|
110
|
+
UNKNOWN = "unknown"
|
|
111
|
+
ACCEPTED = "accepted"
|
|
112
|
+
REJECTED = "rejected"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class MessagingBackend(StrEnum):
|
|
116
|
+
LEGACY = "legacy"
|
|
117
|
+
MODERN = "modern"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclass(frozen=True, slots=True)
|
|
121
|
+
class SendSubmission:
|
|
122
|
+
recipients: tuple["RecipientReference", ...] = field(repr=False)
|
|
123
|
+
subject: str = field(repr=False)
|
|
124
|
+
body: str = field(repr=False)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass(frozen=True, slots=True)
|
|
128
|
+
class SendResult:
|
|
129
|
+
status: SendStatus
|
|
130
|
+
reason: ErrorKind | Literal["cancelled"] | None = None
|
|
131
|
+
identity: "Identity | None" = field(default=None, repr=False)
|
|
132
|
+
observation: "Observation | None" = field(default=None, repr=False)
|
|
133
|
+
backend: MessagingBackend = MessagingBackend.LEGACY
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class NotificationCategory(StrEnum):
|
|
137
|
+
GRADES = "grades"
|
|
138
|
+
ATTENDANCE = "attendance"
|
|
139
|
+
MESSAGES = "messages"
|
|
140
|
+
ANNOUNCEMENTS = "announcements"
|
|
141
|
+
AGENDA = "agenda"
|
|
142
|
+
HOMEWORK = "homework"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@dataclass(frozen=True, slots=True)
|
|
146
|
+
class MessageReference:
|
|
147
|
+
folder: MessageFolder
|
|
148
|
+
identifier: str = field(repr=False)
|
|
149
|
+
account: str = field(repr=False)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@dataclass(frozen=True, slots=True)
|
|
153
|
+
class MessageTimestamp:
|
|
154
|
+
"""School wall time, not a guessed UTC instant or DST fold."""
|
|
155
|
+
|
|
156
|
+
local: datetime = field(repr=False)
|
|
157
|
+
raw: str = field(repr=False)
|
|
158
|
+
timezone: Literal["Europe/Warsaw"] = "Europe/Warsaw"
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@dataclass(frozen=True, slots=True)
|
|
162
|
+
class MessageSummary:
|
|
163
|
+
reference: MessageReference = field(repr=False)
|
|
164
|
+
correspondent: str = field(repr=False)
|
|
165
|
+
subject: str = field(repr=False)
|
|
166
|
+
timestamp: MessageTimestamp = field(repr=False)
|
|
167
|
+
unread: bool | None
|
|
168
|
+
has_attachment: bool
|
|
169
|
+
recipient_read_status: str | None = field(repr=False)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@dataclass(frozen=True, slots=True)
|
|
173
|
+
class MessageAttachmentReference:
|
|
174
|
+
message: MessageReference = field(repr=False)
|
|
175
|
+
identifier: str = field(repr=False)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@dataclass(frozen=True, slots=True)
|
|
179
|
+
class MessageAttachment:
|
|
180
|
+
reference: MessageAttachmentReference = field(repr=False)
|
|
181
|
+
filename: str = field(repr=False)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@dataclass(frozen=True, slots=True)
|
|
185
|
+
class AttachmentHeaders:
|
|
186
|
+
content_type: str | None = field(repr=False)
|
|
187
|
+
content_length: int | None
|
|
188
|
+
content_disposition: str | None = field(repr=False)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@dataclass(frozen=True, slots=True)
|
|
192
|
+
class AttachmentMetadata:
|
|
193
|
+
identity: "Identity" = field(repr=False)
|
|
194
|
+
reference: "MessageAttachmentReference | ModernMessageAttachmentReference" = field(
|
|
195
|
+
repr=False
|
|
196
|
+
)
|
|
197
|
+
headers: AttachmentHeaders = field(repr=False)
|
|
198
|
+
observation: "Observation"
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@dataclass(frozen=True, slots=True)
|
|
202
|
+
class MessageRecipientReceipt:
|
|
203
|
+
recipient: str = field(repr=False)
|
|
204
|
+
raw_status: str = field(repr=False)
|
|
205
|
+
read_timestamp: MessageTimestamp | None = field(repr=False)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@dataclass(frozen=True, slots=True)
|
|
209
|
+
class MessageContentData:
|
|
210
|
+
reference: MessageReference = field(repr=False)
|
|
211
|
+
correspondent: str | None = field(repr=False)
|
|
212
|
+
subject: str = field(repr=False)
|
|
213
|
+
timestamp: MessageTimestamp = field(repr=False)
|
|
214
|
+
read_timestamp: MessageTimestamp | None = field(repr=False)
|
|
215
|
+
text: str = field(repr=False)
|
|
216
|
+
attachments: tuple[MessageAttachment, ...] = field(repr=False)
|
|
217
|
+
recipient_receipts: tuple[MessageRecipientReceipt, ...] = field(
|
|
218
|
+
default=(), repr=False
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@dataclass(frozen=True, slots=True)
|
|
223
|
+
class MessageContent:
|
|
224
|
+
identity: "Identity" = field(repr=False)
|
|
225
|
+
content: MessageContentData = field(repr=False)
|
|
226
|
+
may_mark_read: bool
|
|
227
|
+
observation: "Observation"
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@dataclass(frozen=True, slots=True)
|
|
231
|
+
class MessagesCursor:
|
|
232
|
+
account: str = field(repr=False)
|
|
233
|
+
folder: MessageFolder
|
|
234
|
+
page: int
|
|
235
|
+
offset: int
|
|
236
|
+
page_count: int
|
|
237
|
+
fingerprint: str = field(repr=False)
|
|
238
|
+
seen_ids: tuple[str, ...] = field(repr=False)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@dataclass(frozen=True, slots=True)
|
|
242
|
+
class MessagesPage:
|
|
243
|
+
identity: "Identity" = field(repr=False)
|
|
244
|
+
folder: MessageFolder
|
|
245
|
+
page: int
|
|
246
|
+
page_count: int
|
|
247
|
+
items: tuple[MessageSummary, ...] = field(repr=False)
|
|
248
|
+
fingerprint: str = field(repr=False)
|
|
249
|
+
observation: "Observation"
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@dataclass(frozen=True, slots=True)
|
|
253
|
+
class Messages:
|
|
254
|
+
identity: "Identity" = field(repr=False)
|
|
255
|
+
folder: MessageFolder
|
|
256
|
+
items: tuple[MessageSummary, ...] = field(repr=False)
|
|
257
|
+
pages_fetched: int
|
|
258
|
+
duplicates_skipped: int
|
|
259
|
+
next_cursor: MessagesCursor | None = field(repr=False)
|
|
260
|
+
truncation_reason: Literal["item_limit", "page_limit"] | None
|
|
261
|
+
observation: "Observation"
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@dataclass(frozen=True, slots=True)
|
|
265
|
+
class RecipientGroupReference:
|
|
266
|
+
identifier: str = field(repr=False)
|
|
267
|
+
account: str = field(repr=False)
|
|
268
|
+
selection_id: str = field(default="0", repr=False)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@dataclass(frozen=True, slots=True)
|
|
272
|
+
class RecipientGroup:
|
|
273
|
+
reference: RecipientGroupReference = field(repr=False)
|
|
274
|
+
label: str = field(repr=False)
|
|
275
|
+
available: bool
|
|
276
|
+
lookup_supported: bool
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
@dataclass(frozen=True, slots=True)
|
|
280
|
+
class RecipientReference:
|
|
281
|
+
identifier: str = field(repr=False)
|
|
282
|
+
account: str = field(repr=False)
|
|
283
|
+
group_type: str = field(repr=False)
|
|
284
|
+
selection_id: str = field(default="0", repr=False)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
@dataclass(frozen=True, slots=True)
|
|
288
|
+
class Recipient:
|
|
289
|
+
reference: RecipientReference = field(repr=False)
|
|
290
|
+
label: str | None = field(repr=False)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@dataclass(frozen=True, slots=True)
|
|
294
|
+
class RecipientGroupChoice:
|
|
295
|
+
reference: RecipientGroupReference = field(repr=False)
|
|
296
|
+
label: str = field(repr=False)
|
|
297
|
+
available: bool
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
@dataclass(frozen=True, slots=True)
|
|
301
|
+
class RecipientGroupChoices:
|
|
302
|
+
identity: "Identity" = field(repr=False)
|
|
303
|
+
group: RecipientGroupReference = field(repr=False)
|
|
304
|
+
items: tuple[RecipientGroupChoice, ...] = field(repr=False)
|
|
305
|
+
observation: "Observation"
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@dataclass(frozen=True, slots=True)
|
|
309
|
+
class RecipientGroups:
|
|
310
|
+
identity: "Identity" = field(repr=False)
|
|
311
|
+
groups: tuple[RecipientGroup, ...] = field(repr=False)
|
|
312
|
+
observation: "Observation"
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
@dataclass(frozen=True, slots=True)
|
|
316
|
+
class Recipients:
|
|
317
|
+
identity: "Identity" = field(repr=False)
|
|
318
|
+
group: RecipientGroupReference = field(repr=False)
|
|
319
|
+
items: tuple[Recipient, ...] = field(repr=False)
|
|
320
|
+
observation: "Observation"
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
@dataclass(frozen=True, slots=True)
|
|
324
|
+
class ModernRecipientTypeReference:
|
|
325
|
+
identifier: str = field(repr=False)
|
|
326
|
+
account: str = field(repr=False)
|
|
327
|
+
include_virtual: bool = False
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
@dataclass(frozen=True, slots=True)
|
|
331
|
+
class ModernRecipientType:
|
|
332
|
+
reference: ModernRecipientTypeReference = field(repr=False)
|
|
333
|
+
label: str = field(repr=False)
|
|
334
|
+
lookup_supported: bool
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
@dataclass(frozen=True, slots=True)
|
|
338
|
+
class ModernRecipientReference:
|
|
339
|
+
account_id: str = field(repr=False)
|
|
340
|
+
user_id: str = field(repr=False)
|
|
341
|
+
account: str = field(repr=False)
|
|
342
|
+
recipient_type: str = field(repr=False)
|
|
343
|
+
class_label: str = field(repr=False)
|
|
344
|
+
include_virtual: bool = False
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
@dataclass(frozen=True, slots=True)
|
|
348
|
+
class ModernRecipient:
|
|
349
|
+
reference: ModernRecipientReference = field(repr=False)
|
|
350
|
+
label: str = field(repr=False)
|
|
351
|
+
availability_status_json: str | None = field(default=None, repr=False)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
@dataclass(frozen=True, slots=True)
|
|
355
|
+
class ModernAccountData:
|
|
356
|
+
account_id: str = field(repr=False)
|
|
357
|
+
group_id: str
|
|
358
|
+
first_name: str = field(repr=False)
|
|
359
|
+
last_name: str = field(repr=False)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
@dataclass(frozen=True, slots=True)
|
|
363
|
+
class ModernIdentity:
|
|
364
|
+
identity: "Identity" = field(repr=False)
|
|
365
|
+
account: ModernAccountData = field(repr=False)
|
|
366
|
+
observation: "Observation"
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
@dataclass(frozen=True, slots=True)
|
|
370
|
+
class ModernRecipientTypes:
|
|
371
|
+
identity: "Identity" = field(repr=False)
|
|
372
|
+
items: tuple[ModernRecipientType, ...] = field(repr=False)
|
|
373
|
+
observation: "Observation"
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
@dataclass(frozen=True, slots=True)
|
|
377
|
+
class ModernRecipients:
|
|
378
|
+
identity: "Identity" = field(repr=False)
|
|
379
|
+
recipient_type: ModernRecipientTypeReference = field(repr=False)
|
|
380
|
+
items: tuple[ModernRecipient, ...] = field(repr=False)
|
|
381
|
+
observation: "Observation"
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
@dataclass(frozen=True, slots=True)
|
|
385
|
+
class ModernSendSubmission:
|
|
386
|
+
recipients: tuple[ModernRecipientReference, ...] = field(repr=False)
|
|
387
|
+
subject: str = field(repr=False)
|
|
388
|
+
body: str = field(repr=False)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
@dataclass(frozen=True, slots=True)
|
|
392
|
+
class ModernMessageReference:
|
|
393
|
+
folder: MessageFolder
|
|
394
|
+
identifier: str = field(repr=False)
|
|
395
|
+
account: str = field(repr=False)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
@dataclass(frozen=True, slots=True)
|
|
399
|
+
class ModernMessageSummary:
|
|
400
|
+
reference: ModernMessageReference = field(repr=False)
|
|
401
|
+
correspondent: str = field(repr=False)
|
|
402
|
+
subject: str = field(repr=False)
|
|
403
|
+
sent_at: datetime = field(repr=False)
|
|
404
|
+
raw_sent_at: str = field(repr=False)
|
|
405
|
+
read_at: datetime | None = field(repr=False)
|
|
406
|
+
unread: bool | None
|
|
407
|
+
has_attachment: bool
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
@dataclass(frozen=True, slots=True)
|
|
411
|
+
class ModernMessagesPage:
|
|
412
|
+
identity: "Identity" = field(repr=False)
|
|
413
|
+
folder: MessageFolder
|
|
414
|
+
page: int
|
|
415
|
+
page_size: int
|
|
416
|
+
total_count: int
|
|
417
|
+
items: tuple[ModernMessageSummary, ...] = field(repr=False)
|
|
418
|
+
fingerprint: str = field(repr=False)
|
|
419
|
+
observation: "Observation"
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
@dataclass(frozen=True, slots=True)
|
|
423
|
+
class ModernMessagesCursor:
|
|
424
|
+
account: str = field(repr=False)
|
|
425
|
+
folder: MessageFolder
|
|
426
|
+
page: int
|
|
427
|
+
offset: int
|
|
428
|
+
page_size: int
|
|
429
|
+
total_count: int
|
|
430
|
+
fingerprint: str = field(repr=False)
|
|
431
|
+
seen_ids: tuple[str, ...] = field(repr=False)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
@dataclass(frozen=True, slots=True)
|
|
435
|
+
class ModernMessages:
|
|
436
|
+
identity: "Identity" = field(repr=False)
|
|
437
|
+
folder: MessageFolder
|
|
438
|
+
items: tuple[ModernMessageSummary, ...] = field(repr=False)
|
|
439
|
+
pages_fetched: int
|
|
440
|
+
duplicates_skipped: int
|
|
441
|
+
next_cursor: ModernMessagesCursor | None = field(repr=False)
|
|
442
|
+
truncation_reason: Literal["item_limit", "page_limit"] | None
|
|
443
|
+
observation: "Observation"
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
@dataclass(frozen=True, slots=True)
|
|
447
|
+
class ModernMessageAttachmentReference:
|
|
448
|
+
message: ModernMessageReference = field(repr=False)
|
|
449
|
+
identifier: str = field(repr=False)
|
|
450
|
+
archived: bool = False
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
@dataclass(frozen=True, slots=True)
|
|
454
|
+
class ModernMessageAttachment:
|
|
455
|
+
reference: ModernMessageAttachmentReference = field(repr=False)
|
|
456
|
+
filename: str = field(repr=False)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
@dataclass(frozen=True, slots=True)
|
|
460
|
+
class ModernMessageRecipientReceipt:
|
|
461
|
+
recipient_id: str = field(repr=False)
|
|
462
|
+
name: str = field(repr=False)
|
|
463
|
+
channel: Literal["to", "cc", "bcc"]
|
|
464
|
+
read: bool | None
|
|
465
|
+
read_at: datetime | None = field(repr=False)
|
|
466
|
+
# The observed readed field is not an independent delivery acknowledgement.
|
|
467
|
+
delivered: None = None
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
@dataclass(frozen=True, slots=True)
|
|
471
|
+
class ModernMessageContent:
|
|
472
|
+
identity: "Identity" = field(repr=False)
|
|
473
|
+
summary: ModernMessageSummary = field(repr=False)
|
|
474
|
+
text: str = field(repr=False)
|
|
475
|
+
attachments: tuple[ModernMessageAttachment, ...] = field(repr=False)
|
|
476
|
+
may_mark_read: bool
|
|
477
|
+
observation: "Observation"
|
|
478
|
+
recipient_receipts: tuple[ModernMessageRecipientReceipt, ...] = field(
|
|
479
|
+
default=(), repr=False
|
|
480
|
+
)
|
|
481
|
+
recipient_count: int | None = None
|
|
482
|
+
read_count: int | None = None
|
|
483
|
+
receipt_source: Literal["receivers", "individualRecipients"] | None = None
|
|
484
|
+
archived: bool = False
|
|
485
|
+
withdrawn: bool = False
|
|
486
|
+
original_subject: str | None = field(default=None, repr=False)
|
|
487
|
+
original_text: str | None = field(default=None, repr=False)
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
@dataclass(frozen=True, slots=True)
|
|
491
|
+
class CompletedLessonsCursor:
|
|
492
|
+
account: str = field(repr=False)
|
|
493
|
+
start: date = field(repr=False)
|
|
494
|
+
end: date = field(repr=False)
|
|
495
|
+
page: int
|
|
496
|
+
offset: int
|
|
497
|
+
page_count: int
|
|
498
|
+
fingerprint: str = field(repr=False)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
@dataclass(frozen=True, slots=True)
|
|
502
|
+
class SchoolReference:
|
|
503
|
+
kind: Literal["agenda", "homework"]
|
|
504
|
+
identifier: str = field(repr=False)
|
|
505
|
+
account: str = field(repr=False)
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
# A credential submission keeps secrets out of reprs; other POST forms are
|
|
509
|
+
# built only by the fixed form functions in config.
|
|
510
|
+
type RequestForm = LoginSubmission | Mapping[str, str] | None
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
@dataclass(frozen=True, slots=True)
|
|
514
|
+
class CompletedLesson:
|
|
515
|
+
day: date = field(repr=False)
|
|
516
|
+
raw_day: str = field(repr=False)
|
|
517
|
+
weekday: str = field(repr=False)
|
|
518
|
+
lesson_number: int | None
|
|
519
|
+
raw_lesson_number: str = field(repr=False)
|
|
520
|
+
subject: str = field(repr=False)
|
|
521
|
+
teacher: str | None = field(repr=False)
|
|
522
|
+
subject_teacher_text: str = field(repr=False)
|
|
523
|
+
topic: str = field(repr=False)
|
|
524
|
+
z_value: str = field(repr=False)
|
|
525
|
+
attendance_symbol: str = field(repr=False)
|
|
526
|
+
attendance_detail_id: str | None = field(repr=False)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
@dataclass(frozen=True, slots=True)
|
|
530
|
+
class CompletedLessonsPage:
|
|
531
|
+
identity: "Identity" = field(repr=False)
|
|
532
|
+
start: date = field(repr=False)
|
|
533
|
+
end: date = field(repr=False)
|
|
534
|
+
page: int
|
|
535
|
+
page_count: int
|
|
536
|
+
items: tuple[CompletedLesson, ...] = field(repr=False)
|
|
537
|
+
fingerprint: str = field(repr=False)
|
|
538
|
+
observation: "Observation"
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
@dataclass(frozen=True, slots=True)
|
|
542
|
+
class CompletedLessons:
|
|
543
|
+
identity: "Identity" = field(repr=False)
|
|
544
|
+
start: date = field(repr=False)
|
|
545
|
+
end: date = field(repr=False)
|
|
546
|
+
items: tuple[CompletedLesson, ...] = field(repr=False)
|
|
547
|
+
pages_fetched: int
|
|
548
|
+
next_cursor: CompletedLessonsCursor | None = field(repr=False)
|
|
549
|
+
observation: "Observation"
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
class GradeKind(StrEnum):
|
|
553
|
+
CURRENT = "current"
|
|
554
|
+
PERIOD = "period"
|
|
555
|
+
PREDICTED_PERIOD = "predicted_period"
|
|
556
|
+
ANNUAL = "annual"
|
|
557
|
+
PREDICTED_ANNUAL = "predicted_annual"
|
|
558
|
+
PUBLICATION = "publication"
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
class Availability(StrEnum):
|
|
562
|
+
AVAILABLE = "available"
|
|
563
|
+
UNAVAILABLE = "unavailable"
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
@dataclass(frozen=True, slots=True)
|
|
567
|
+
class Person:
|
|
568
|
+
id: str = field(repr=False)
|
|
569
|
+
first_name: str | None = field(default=None, repr=False)
|
|
570
|
+
last_name: str | None = field(default=None, repr=False)
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
@dataclass(frozen=True, slots=True)
|
|
574
|
+
class Observation:
|
|
575
|
+
account: str = field(repr=False)
|
|
576
|
+
observed_at: datetime
|
|
577
|
+
session_generation: int
|
|
578
|
+
source: str
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
@dataclass(frozen=True, slots=True)
|
|
582
|
+
class Identity:
|
|
583
|
+
owner: Person
|
|
584
|
+
student: Person
|
|
585
|
+
observation: Observation
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
@dataclass(frozen=True, slots=True)
|
|
589
|
+
class AccountContext:
|
|
590
|
+
"""Application-keyed login pseudonym, not credentials or an authority token."""
|
|
591
|
+
|
|
592
|
+
alias: str = field(repr=False)
|
|
593
|
+
identifier: str = field(repr=False)
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
@dataclass(frozen=True, slots=True)
|
|
597
|
+
class NotificationCount:
|
|
598
|
+
category: NotificationCategory
|
|
599
|
+
label: str = field(repr=False)
|
|
600
|
+
count: int
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
@dataclass(frozen=True, slots=True)
|
|
604
|
+
class NotificationCounts:
|
|
605
|
+
identity: Identity = field(repr=False)
|
|
606
|
+
items: tuple[NotificationCount, ...] = field(repr=False)
|
|
607
|
+
observation: Observation
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
@dataclass(frozen=True, slots=True)
|
|
611
|
+
class ScheduleEventWire:
|
|
612
|
+
"""HTTP payload bytes with transfer framing removed, before content decoding."""
|
|
613
|
+
|
|
614
|
+
body: bytes = field(repr=False)
|
|
615
|
+
content_type: str | None = field(repr=False)
|
|
616
|
+
content_codings: tuple[str, ...] = field(repr=False)
|
|
617
|
+
transfer_codings: tuple[str, ...] = field(repr=False)
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
@dataclass(frozen=True, slots=True)
|
|
621
|
+
class ScheduleEventResponse:
|
|
622
|
+
version: int
|
|
623
|
+
identity: Identity = field(repr=False)
|
|
624
|
+
wire: ScheduleEventWire = field(repr=False)
|
|
625
|
+
observation: Observation
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
@dataclass(frozen=True, slots=True)
|
|
629
|
+
class RecentScheduleEvent:
|
|
630
|
+
date_added: str = field(repr=False)
|
|
631
|
+
type: str = field(repr=False)
|
|
632
|
+
data: str = field(repr=False)
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
@dataclass(frozen=True, slots=True)
|
|
636
|
+
class ScheduleEvents:
|
|
637
|
+
identity: Identity = field(repr=False)
|
|
638
|
+
items: tuple[RecentScheduleEvent, ...] = field(repr=False)
|
|
639
|
+
observation: Observation
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
@dataclass(frozen=True, slots=True)
|
|
643
|
+
class TimetableInterval:
|
|
644
|
+
starts_at: time
|
|
645
|
+
ends_at: time
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
@dataclass(frozen=True, slots=True)
|
|
649
|
+
class TimetableLesson:
|
|
650
|
+
subject: str = field(repr=False)
|
|
651
|
+
teacher_and_classroom: str | None = field(repr=False)
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
@dataclass(frozen=True, slots=True)
|
|
655
|
+
class TimetableChange:
|
|
656
|
+
label: str = field(repr=False)
|
|
657
|
+
metadata: tuple[tuple[str, str], ...] = field(repr=False)
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
@dataclass(frozen=True, slots=True)
|
|
661
|
+
class TimetablePeriod:
|
|
662
|
+
number: int
|
|
663
|
+
interval: TimetableInterval
|
|
664
|
+
lessons: tuple[TimetableLesson, ...] = field(repr=False)
|
|
665
|
+
changes: tuple[TimetableChange, ...] = field(repr=False)
|
|
666
|
+
next_recess: TimetableInterval | None
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
@dataclass(frozen=True, slots=True)
|
|
670
|
+
class TimetableDay:
|
|
671
|
+
day: date = field(repr=False)
|
|
672
|
+
periods: tuple[TimetablePeriod, ...] = field(repr=False)
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
@dataclass(frozen=True, slots=True)
|
|
676
|
+
class Timetable:
|
|
677
|
+
identity: Identity
|
|
678
|
+
monday: date = field(repr=False)
|
|
679
|
+
days: tuple[TimetableDay, ...] = field(repr=False)
|
|
680
|
+
observation: Observation
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
@dataclass(frozen=True, slots=True)
|
|
684
|
+
class Announcement:
|
|
685
|
+
# A content-addressed account-scoped reference, never an upstream resource ID.
|
|
686
|
+
reference: str = field(repr=False)
|
|
687
|
+
title: str = field(repr=False)
|
|
688
|
+
author: str = field(repr=False)
|
|
689
|
+
date_text: str = field(repr=False)
|
|
690
|
+
published_on: date = field(repr=False)
|
|
691
|
+
content: str = field(repr=False)
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
@dataclass(frozen=True, slots=True)
|
|
695
|
+
class Announcements:
|
|
696
|
+
identity: Identity
|
|
697
|
+
items: tuple[Announcement, ...] = field(repr=False)
|
|
698
|
+
observation: Observation
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
@dataclass(frozen=True, slots=True)
|
|
702
|
+
class AgendaEvent:
|
|
703
|
+
day: date = field(repr=False)
|
|
704
|
+
title: str = field(repr=False)
|
|
705
|
+
subject: str | None = field(repr=False)
|
|
706
|
+
text: str = field(repr=False)
|
|
707
|
+
lesson_number: int | None
|
|
708
|
+
at_time: time | None
|
|
709
|
+
metadata_text: str = field(repr=False)
|
|
710
|
+
metadata: tuple[tuple[str, str], ...] = field(repr=False)
|
|
711
|
+
metadata_notes: tuple[str, ...] = field(repr=False)
|
|
712
|
+
reference: SchoolReference | None = field(repr=False)
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
@dataclass(frozen=True, slots=True)
|
|
716
|
+
class AgendaDay:
|
|
717
|
+
day: date = field(repr=False)
|
|
718
|
+
events: tuple[AgendaEvent, ...] = field(repr=False)
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
@dataclass(frozen=True, slots=True)
|
|
722
|
+
class Agenda:
|
|
723
|
+
identity: Identity
|
|
724
|
+
year: int
|
|
725
|
+
month: int
|
|
726
|
+
days: tuple[AgendaDay, ...] = field(repr=False)
|
|
727
|
+
observation: Observation
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
@dataclass(frozen=True, slots=True)
|
|
731
|
+
class HomeworkItem:
|
|
732
|
+
subject: str = field(repr=False)
|
|
733
|
+
teacher: str = field(repr=False)
|
|
734
|
+
topic: str = field(repr=False)
|
|
735
|
+
category: str = field(repr=False)
|
|
736
|
+
assigned_on: date = field(repr=False)
|
|
737
|
+
due_on: date = field(repr=False)
|
|
738
|
+
# Raw solution-upload status; None when the school layout has no column.
|
|
739
|
+
submission_status: str | None = field(repr=False)
|
|
740
|
+
marked_done_at: datetime | None = field(repr=False)
|
|
741
|
+
reference: SchoolReference | None = field(repr=False)
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
@dataclass(frozen=True, slots=True)
|
|
745
|
+
class HomeworkRangeRequest:
|
|
746
|
+
"""Explicit bounded selection, validated by AccountClient before any I/O."""
|
|
747
|
+
|
|
748
|
+
start: date
|
|
749
|
+
end: date
|
|
750
|
+
max_windows: int = 13
|
|
751
|
+
max_items: int = 4096
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
@dataclass(frozen=True, slots=True)
|
|
755
|
+
class Homework:
|
|
756
|
+
identity: Identity
|
|
757
|
+
start: date = field(repr=False)
|
|
758
|
+
end: date = field(repr=False)
|
|
759
|
+
items: tuple[HomeworkItem, ...] = field(repr=False)
|
|
760
|
+
observation: Observation
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
type DetailFieldKey = Literal[
|
|
764
|
+
"date",
|
|
765
|
+
"lesson_number",
|
|
766
|
+
"teacher",
|
|
767
|
+
"category",
|
|
768
|
+
"subject",
|
|
769
|
+
"room",
|
|
770
|
+
"description",
|
|
771
|
+
"published_at",
|
|
772
|
+
"topic",
|
|
773
|
+
"due_at",
|
|
774
|
+
"content",
|
|
775
|
+
]
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
@dataclass(frozen=True, slots=True)
|
|
779
|
+
class DetailField:
|
|
780
|
+
"""Stable semantic key with bounded displayed text, not a parsed scalar."""
|
|
781
|
+
|
|
782
|
+
key: DetailFieldKey | None
|
|
783
|
+
raw_label: str = field(repr=False)
|
|
784
|
+
value: str = field(repr=False)
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
@dataclass(frozen=True, slots=True)
|
|
788
|
+
class SchoolDetail:
|
|
789
|
+
identity: Identity
|
|
790
|
+
reference: SchoolReference = field(repr=False)
|
|
791
|
+
title: str | None = field(repr=False)
|
|
792
|
+
fields: tuple[tuple[str, str], ...] = field(repr=False)
|
|
793
|
+
notes: tuple[str, ...] = field(repr=False)
|
|
794
|
+
observation: Observation
|
|
795
|
+
normalized_fields: tuple[DetailField, ...] = field(
|
|
796
|
+
default=(), repr=False, compare=False
|
|
797
|
+
)
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
@dataclass(frozen=True, slots=True)
|
|
801
|
+
class LuckyNumber:
|
|
802
|
+
availability: Availability
|
|
803
|
+
number: int | None = field(default=None, repr=False)
|
|
804
|
+
day: date | None = None
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
@dataclass(frozen=True, slots=True)
|
|
808
|
+
class StudentInformation:
|
|
809
|
+
identity: Identity
|
|
810
|
+
name: str = field(repr=False)
|
|
811
|
+
class_name: str = field(repr=False)
|
|
812
|
+
register_number: int = field(repr=False)
|
|
813
|
+
tutor: str = field(repr=False)
|
|
814
|
+
school: str = field(repr=False)
|
|
815
|
+
lucky_number: LuckyNumber
|
|
816
|
+
observation: Observation
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
@dataclass(frozen=True, slots=True)
|
|
820
|
+
class GradeSummaryValue:
|
|
821
|
+
"""Column presence, separate from its raw school-provided value.
|
|
822
|
+
|
|
823
|
+
Unavailable columns have raw=None. Available columns preserve empty strings,
|
|
824
|
+
unassigned markers such as '-', symbols, and descriptive values without
|
|
825
|
+
inventing numeric conversions or dates.
|
|
826
|
+
"""
|
|
827
|
+
|
|
828
|
+
availability: Availability
|
|
829
|
+
raw: str | None = field(repr=False)
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
@dataclass(frozen=True, slots=True)
|
|
833
|
+
class SubjectGradeSummary:
|
|
834
|
+
subject: str = field(repr=False)
|
|
835
|
+
midterm: GradeSummaryValue
|
|
836
|
+
predicted_annual: GradeSummaryValue
|
|
837
|
+
annual: GradeSummaryValue
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
@dataclass(frozen=True, slots=True)
|
|
841
|
+
class FinalGrades:
|
|
842
|
+
identity: Identity
|
|
843
|
+
items: tuple[SubjectGradeSummary, ...] = field(repr=False)
|
|
844
|
+
observation: Observation
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
@dataclass(frozen=True, slots=True)
|
|
848
|
+
class NumericGrade:
|
|
849
|
+
"""A school grade symbol, not an invented numeric conversion.
|
|
850
|
+
|
|
851
|
+
Missing count/weight metadata is unknown, not False or zero. Links are inert
|
|
852
|
+
upstream strings, never an authorized transport destination.
|
|
853
|
+
"""
|
|
854
|
+
|
|
855
|
+
subject: str = field(repr=False)
|
|
856
|
+
raw: str = field(repr=False)
|
|
857
|
+
day: date = field(repr=False)
|
|
858
|
+
semester: Literal[0, 1, 2]
|
|
859
|
+
counts_toward_average: bool | None = field(repr=False)
|
|
860
|
+
weight: int | None = field(repr=False)
|
|
861
|
+
category: str | None = field(repr=False)
|
|
862
|
+
teacher: str | None = field(repr=False)
|
|
863
|
+
comment: str | None = field(repr=False)
|
|
864
|
+
href: str | None = field(repr=False)
|
|
865
|
+
metadata: tuple[tuple[str, str], ...] = field(repr=False)
|
|
866
|
+
kind: GradeKind = GradeKind.CURRENT
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
@dataclass(frozen=True, slots=True)
|
|
870
|
+
class DescriptiveGrade:
|
|
871
|
+
subject: str = field(repr=False)
|
|
872
|
+
raw: str = field(repr=False)
|
|
873
|
+
day: date = field(repr=False)
|
|
874
|
+
semester: Literal[0, 1, 2] | None
|
|
875
|
+
teacher: str | None = field(repr=False)
|
|
876
|
+
comment: str | None = field(repr=False)
|
|
877
|
+
metadata: tuple[tuple[str, str], ...] = field(repr=False)
|
|
878
|
+
kind: GradeKind = GradeKind.CURRENT
|
|
879
|
+
href: str | None = field(default=None, repr=False)
|
|
880
|
+
|
|
881
|
+
|
|
882
|
+
@dataclass(frozen=True, slots=True)
|
|
883
|
+
class SchoolAverage:
|
|
884
|
+
"""School-provided text only; semester zero means the annual column."""
|
|
885
|
+
|
|
886
|
+
subject: str = field(repr=False)
|
|
887
|
+
semester: Literal[0, 1, 2]
|
|
888
|
+
value: GradeSummaryValue
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
@dataclass(frozen=True, slots=True)
|
|
892
|
+
class DescriptiveGradeSummary:
|
|
893
|
+
"""Undated semester text, never eligible for a dated grade window."""
|
|
894
|
+
|
|
895
|
+
subject: str = field(repr=False)
|
|
896
|
+
semester: Literal[1, 2]
|
|
897
|
+
raw: str = field(repr=False)
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
@dataclass(frozen=True, slots=True)
|
|
901
|
+
class GradeRecords:
|
|
902
|
+
numeric: tuple[NumericGrade, ...] = field(repr=False)
|
|
903
|
+
descriptive: tuple[DescriptiveGrade, ...] = field(repr=False)
|
|
904
|
+
averages: tuple[SchoolAverage, ...] = field(repr=False)
|
|
905
|
+
descriptive_summaries: tuple[DescriptiveGradeSummary, ...] = field(
|
|
906
|
+
default=(), repr=False
|
|
907
|
+
)
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
@dataclass(frozen=True, slots=True)
|
|
911
|
+
class Grades:
|
|
912
|
+
identity: Identity
|
|
913
|
+
records: GradeRecords
|
|
914
|
+
observation: Observation
|
|
915
|
+
view: GradeView = GradeView.ALL
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
@dataclass(frozen=True, slots=True)
|
|
919
|
+
class GradeWindow:
|
|
920
|
+
identity: Identity
|
|
921
|
+
start: date | None
|
|
922
|
+
end: date | None
|
|
923
|
+
numeric: tuple[NumericGrade, ...] = field(repr=False)
|
|
924
|
+
descriptive: tuple[DescriptiveGrade, ...] = field(repr=False)
|
|
925
|
+
observation: Observation
|
|
926
|
+
view: GradeView = GradeView.ALL
|
|
927
|
+
|
|
928
|
+
|
|
929
|
+
@dataclass(frozen=True, slots=True)
|
|
930
|
+
class AttendanceRecord:
|
|
931
|
+
symbol: str = field(repr=False)
|
|
932
|
+
day: date = field(repr=False)
|
|
933
|
+
semester: Literal[1, 2]
|
|
934
|
+
attendance_type: str | None = field(repr=False)
|
|
935
|
+
teacher: str | None = field(repr=False)
|
|
936
|
+
period: int | None = field(repr=False)
|
|
937
|
+
excursion: bool | None = field(repr=False)
|
|
938
|
+
topic: str | None = field(repr=False)
|
|
939
|
+
subject: str | None = field(repr=False)
|
|
940
|
+
detail_id: str | None = field(repr=False)
|
|
941
|
+
metadata: tuple[tuple[str, str], ...] = field(repr=False)
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
@dataclass(frozen=True, slots=True)
|
|
945
|
+
class AttendanceRecords:
|
|
946
|
+
items: tuple[AttendanceRecord, ...] = field(repr=False)
|
|
947
|
+
semesters: tuple[Literal[1, 2], ...]
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
@dataclass(frozen=True, slots=True)
|
|
951
|
+
class Attendance:
|
|
952
|
+
identity: Identity
|
|
953
|
+
items: tuple[AttendanceRecord, ...] = field(repr=False)
|
|
954
|
+
semesters: tuple[Literal[1, 2], ...]
|
|
955
|
+
observation: Observation
|
|
956
|
+
view: AttendanceView = AttendanceView.ALL
|
|
957
|
+
|
|
958
|
+
|
|
959
|
+
@dataclass(frozen=True, slots=True)
|
|
960
|
+
class AttendanceWindow:
|
|
961
|
+
identity: Identity
|
|
962
|
+
start: date | None
|
|
963
|
+
end: date | None
|
|
964
|
+
items: tuple[AttendanceRecord, ...] = field(repr=False)
|
|
965
|
+
observation: Observation
|
|
966
|
+
view: AttendanceView = AttendanceView.ALL
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
@dataclass(frozen=True, slots=True)
|
|
970
|
+
class AttendanceDetailContent:
|
|
971
|
+
fields: tuple[tuple[str, str], ...] = field(repr=False)
|
|
972
|
+
notes: tuple[str, ...] = field(repr=False)
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
@dataclass(frozen=True, slots=True)
|
|
976
|
+
class AttendanceDetail:
|
|
977
|
+
identity: Identity
|
|
978
|
+
detail_id: str = field(repr=False)
|
|
979
|
+
fields: tuple[tuple[str, str], ...] = field(repr=False)
|
|
980
|
+
notes: tuple[str, ...] = field(repr=False)
|
|
981
|
+
observation: Observation
|
|
982
|
+
normalized_fields: tuple[DetailField, ...] = field(
|
|
983
|
+
default=(), repr=False, compare=False
|
|
984
|
+
)
|
|
985
|
+
|
|
986
|
+
|
|
987
|
+
class AttendanceKind(StrEnum):
|
|
988
|
+
ABSENCE = "absence"
|
|
989
|
+
LATE = "late"
|
|
990
|
+
EXCUSED = "excused"
|
|
991
|
+
EXEMPTION = "exemption"
|
|
992
|
+
PRESENT = "present"
|
|
993
|
+
EXCURSION = "excursion"
|
|
994
|
+
CONTEST = "contest"
|
|
995
|
+
TRAINING = "training"
|
|
996
|
+
UNKNOWN = "unknown"
|
|
997
|
+
|
|
998
|
+
|
|
999
|
+
@dataclass(frozen=True, slots=True)
|
|
1000
|
+
class GatewayAttendanceRecord:
|
|
1001
|
+
identifier: str | None = field(repr=False)
|
|
1002
|
+
day: date = field(repr=False)
|
|
1003
|
+
semester: Literal[1, 2]
|
|
1004
|
+
type_id: str = field(repr=False)
|
|
1005
|
+
kind: AttendanceKind
|
|
1006
|
+
lesson_id: str = field(repr=False)
|
|
1007
|
+
period: int | None
|
|
1008
|
+
|
|
1009
|
+
|
|
1010
|
+
@dataclass(frozen=True, slots=True)
|
|
1011
|
+
class GatewayAttendance:
|
|
1012
|
+
identity: Identity
|
|
1013
|
+
items: tuple[GatewayAttendanceRecord, ...] = field(repr=False)
|
|
1014
|
+
observation: Observation
|
|
1015
|
+
|
|
1016
|
+
|
|
1017
|
+
@dataclass(frozen=True, slots=True)
|
|
1018
|
+
class FrequencyMeasure:
|
|
1019
|
+
attended_count: int
|
|
1020
|
+
total_count: int
|
|
1021
|
+
excluded_count: int
|
|
1022
|
+
unknown_count: int
|
|
1023
|
+
ratio: float | None
|
|
1024
|
+
policy: Literal["overall", "subject"]
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
@dataclass(frozen=True, slots=True)
|
|
1028
|
+
class AttendanceFrequency:
|
|
1029
|
+
identity: Identity
|
|
1030
|
+
first_semester: FrequencyMeasure
|
|
1031
|
+
second_semester: FrequencyMeasure
|
|
1032
|
+
overall: FrequencyMeasure
|
|
1033
|
+
observation: Observation
|
|
1034
|
+
|
|
1035
|
+
|
|
1036
|
+
@dataclass(frozen=True, slots=True)
|
|
1037
|
+
class SubjectFrequency:
|
|
1038
|
+
subject_id: str = field(repr=False)
|
|
1039
|
+
subject: str = field(repr=False)
|
|
1040
|
+
frequency: FrequencyMeasure
|
|
1041
|
+
|
|
1042
|
+
|
|
1043
|
+
@dataclass(frozen=True, slots=True)
|
|
1044
|
+
class SubjectFrequencies:
|
|
1045
|
+
identity: Identity
|
|
1046
|
+
items: tuple[SubjectFrequency, ...] = field(repr=False)
|
|
1047
|
+
start: date | None
|
|
1048
|
+
end: date | None
|
|
1049
|
+
observation: Observation
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
class _NumericReferenceWire(BaseModel):
|
|
1053
|
+
model_config = ConfigDict(strict=True, extra="ignore", hide_input_in_errors=True)
|
|
1054
|
+
Id: Annotated[str, Field(pattern=r"^[0-9]{1,64}$")]
|
|
1055
|
+
|
|
1056
|
+
@field_validator("Id", mode="before")
|
|
1057
|
+
@classmethod
|
|
1058
|
+
def normalize_id(cls, value: Any) -> Any:
|
|
1059
|
+
return str(value) if type(value) is int and 0 <= value < 10**64 else value
|
|
1060
|
+
|
|
1061
|
+
|
|
1062
|
+
class _AttendanceWire(BaseModel):
|
|
1063
|
+
model_config = ConfigDict(strict=True, extra="ignore", hide_input_in_errors=True)
|
|
1064
|
+
Id: Annotated[str | None, Field(pattern=r"^[0-9]{1,64}$")] = None
|
|
1065
|
+
Date: Annotated[str, Field(pattern=r"^[0-9]{4}-[0-9]{2}-[0-9]{2}$")]
|
|
1066
|
+
Semester: Literal[1, 2]
|
|
1067
|
+
Type: _NumericReferenceWire
|
|
1068
|
+
Lesson: _NumericReferenceWire
|
|
1069
|
+
LessonNo: Annotated[int | None, Field(ge=0, le=99)] = None
|
|
1070
|
+
|
|
1071
|
+
@field_validator("Semester", mode="before")
|
|
1072
|
+
@classmethod
|
|
1073
|
+
def reject_boolean_semester(cls, value: Any) -> Any:
|
|
1074
|
+
if type(value) is not int:
|
|
1075
|
+
raise ValueError("Invalid semester type")
|
|
1076
|
+
return value
|
|
1077
|
+
|
|
1078
|
+
@field_validator("Id", mode="before")
|
|
1079
|
+
@classmethod
|
|
1080
|
+
def normalize_id(cls, value: Any) -> Any:
|
|
1081
|
+
return _NumericReferenceWire.normalize_id(value)
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
class _AttendanceEnvelopeWire(BaseModel):
|
|
1085
|
+
model_config = ConfigDict(strict=True, extra="ignore", hide_input_in_errors=True)
|
|
1086
|
+
Attendances: Annotated[list[_AttendanceWire], Field(max_length=2048)]
|
|
1087
|
+
|
|
1088
|
+
|
|
1089
|
+
class _LessonWire(_NumericReferenceWire):
|
|
1090
|
+
Subject: _NumericReferenceWire
|
|
1091
|
+
|
|
1092
|
+
|
|
1093
|
+
class _LessonEnvelopeWire(BaseModel):
|
|
1094
|
+
model_config = ConfigDict(strict=True, extra="ignore", hide_input_in_errors=True)
|
|
1095
|
+
Lesson: _LessonWire
|
|
1096
|
+
|
|
1097
|
+
|
|
1098
|
+
class _SubjectWire(_NumericReferenceWire):
|
|
1099
|
+
Name: Annotated[str, Field(min_length=1, max_length=1024)]
|
|
1100
|
+
|
|
1101
|
+
|
|
1102
|
+
class _SubjectEnvelopeWire(BaseModel):
|
|
1103
|
+
model_config = ConfigDict(strict=True, extra="ignore", hide_input_in_errors=True)
|
|
1104
|
+
Subject: _SubjectWire
|
|
1105
|
+
|
|
1106
|
+
|
|
1107
|
+
@dataclass(frozen=True, slots=True)
|
|
1108
|
+
class ProfileFields:
|
|
1109
|
+
name: str = field(repr=False)
|
|
1110
|
+
class_name: str = field(repr=False)
|
|
1111
|
+
register_number: int = field(repr=False)
|
|
1112
|
+
tutor: str = field(repr=False)
|
|
1113
|
+
school: str = field(repr=False)
|
|
1114
|
+
lucky_number: LuckyNumber
|
|
1115
|
+
|
|
1116
|
+
|
|
1117
|
+
class _PersonWire(BaseModel):
|
|
1118
|
+
model_config = ConfigDict(strict=True, extra="ignore", hide_input_in_errors=True)
|
|
1119
|
+
Id: Annotated[str, Field(pattern=r"^[A-Za-z0-9_-]{1,64}$")]
|
|
1120
|
+
FirstName: Annotated[str | None, Field(max_length=256)] = None
|
|
1121
|
+
LastName: Annotated[str | None, Field(max_length=256)] = None
|
|
1122
|
+
|
|
1123
|
+
@field_validator("Id", mode="before")
|
|
1124
|
+
@classmethod
|
|
1125
|
+
def normalize_id(cls, value: Any) -> Any:
|
|
1126
|
+
if type(value) is int and 0 <= value < 10**64:
|
|
1127
|
+
return str(value)
|
|
1128
|
+
return value
|
|
1129
|
+
|
|
1130
|
+
|
|
1131
|
+
class _AccountWire(_PersonWire):
|
|
1132
|
+
UserId: Annotated[str | None, Field(pattern=r"^[A-Za-z0-9_-]{1,64}$")] = None
|
|
1133
|
+
|
|
1134
|
+
@field_validator("UserId", mode="before")
|
|
1135
|
+
@classmethod
|
|
1136
|
+
def normalize_user_id(cls, value: Any) -> Any:
|
|
1137
|
+
return cls.normalize_id(value)
|
|
1138
|
+
|
|
1139
|
+
|
|
1140
|
+
class _UserWire(BaseModel):
|
|
1141
|
+
model_config = ConfigDict(strict=True, extra="ignore", hide_input_in_errors=True)
|
|
1142
|
+
Id: Annotated[str | None, Field(pattern=r"^[A-Za-z0-9_-]{1,64}$")] = None
|
|
1143
|
+
FirstName: Annotated[str | None, Field(max_length=256)] = None
|
|
1144
|
+
LastName: Annotated[str | None, Field(max_length=256)] = None
|
|
1145
|
+
|
|
1146
|
+
@field_validator("Id", mode="before")
|
|
1147
|
+
@classmethod
|
|
1148
|
+
def normalize_id(cls, value: Any) -> Any:
|
|
1149
|
+
return _PersonWire.normalize_id(value)
|
|
1150
|
+
|
|
1151
|
+
|
|
1152
|
+
class _MeWire(BaseModel):
|
|
1153
|
+
model_config = ConfigDict(strict=True, extra="ignore", hide_input_in_errors=True)
|
|
1154
|
+
Account: _AccountWire
|
|
1155
|
+
User: _UserWire
|
|
1156
|
+
|
|
1157
|
+
|
|
1158
|
+
class _EnvelopeWire(BaseModel):
|
|
1159
|
+
model_config = ConfigDict(strict=True, extra="ignore", hide_input_in_errors=True)
|
|
1160
|
+
Me: _MeWire
|