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.
Files changed (46) hide show
  1. librus_python_api/__init__.py +243 -0
  2. librus_python_api/_notification_codec.py +310 -0
  3. librus_python_api/_storage.py +367 -0
  4. librus_python_api/announcements.py +159 -0
  5. librus_python_api/attachment_routes.py +114 -0
  6. librus_python_api/attachments.py +297 -0
  7. librus_python_api/attendance.py +182 -0
  8. librus_python_api/attendance_frequency.py +112 -0
  9. librus_python_api/budget.py +79 -0
  10. librus_python_api/checkpoint.py +61 -0
  11. librus_python_api/completed_lessons.py +216 -0
  12. librus_python_api/config.py +1410 -0
  13. librus_python_api/detail_fields.py +50 -0
  14. librus_python_api/diagnostics.py +25 -0
  15. librus_python_api/exceptions.py +172 -0
  16. librus_python_api/files.py +156 -0
  17. librus_python_api/grade_parsers.py +169 -0
  18. librus_python_api/grade_records.py +454 -0
  19. librus_python_api/homework_range.py +41 -0
  20. librus_python_api/lifecycle.py +24 -0
  21. librus_python_api/markup.py +147 -0
  22. librus_python_api/message_content.py +230 -0
  23. librus_python_api/messages.py +288 -0
  24. librus_python_api/models.py +1160 -0
  25. librus_python_api/modern_body.py +75 -0
  26. librus_python_api/modern_mailbox.py +459 -0
  27. librus_python_api/modern_messages.py +276 -0
  28. librus_python_api/notification_models.py +67 -0
  29. librus_python_api/notification_persistence.py +1044 -0
  30. librus_python_api/notification_workflow.py +303 -0
  31. librus_python_api/notifications.py +216 -0
  32. librus_python_api/parsers.py +232 -0
  33. librus_python_api/parsing.py +49 -0
  34. librus_python_api/persistence.py +405 -0
  35. librus_python_api/py.typed +0 -0
  36. librus_python_api/recipients.py +271 -0
  37. librus_python_api/scheduler.py +287 -0
  38. librus_python_api/school_reads.py +400 -0
  39. librus_python_api/sending.py +125 -0
  40. librus_python_api/service.py +2285 -0
  41. librus_python_api/timetable.py +261 -0
  42. librus_python_api/transport.py +956 -0
  43. librus_python_api-1.0.0rc1.dist-info/METADATA +254 -0
  44. librus_python_api-1.0.0rc1.dist-info/RECORD +46 -0
  45. librus_python_api-1.0.0rc1.dist-info/WHEEL +4 -0
  46. librus_python_api-1.0.0rc1.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