librus-python-api 1.0.3__py3-none-any.whl → 1.3.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.
@@ -16,6 +16,13 @@ from librus_python_api.models import (
16
16
  AgendaEvent,
17
17
  Announcement,
18
18
  Announcements,
19
+ ArchiveAbsences,
20
+ ArchiveAchievement,
21
+ ArchiveCounts,
22
+ ArchiveDescriptive,
23
+ ArchiveMarks,
24
+ ArchiveSubject,
25
+ ArchiveYear,
19
26
  AttachmentHeaders,
20
27
  AttachmentMetadata,
21
28
  Attendance,
@@ -65,6 +72,9 @@ from librus_python_api.models import (
65
72
  MessageTimestamp,
66
73
  MessagingBackend,
67
74
  ModernAccountData,
75
+ ModernCorrespondent,
76
+ ModernCorrespondentReference,
77
+ ModernCorrespondents,
68
78
  ModernIdentity,
69
79
  ModernMessageAttachment,
70
80
  ModernMessageAttachmentReference,
@@ -82,6 +92,10 @@ from librus_python_api.models import (
82
92
  ModernRecipientTypeReference,
83
93
  ModernRecipientTypes,
84
94
  ModernSendSubmission,
95
+ ModernTeacherSubject,
96
+ ModernTeacherSubjects,
97
+ ModernUnreadCounts,
98
+ ModernUnreadFolders,
85
99
  NotificationCategory,
86
100
  NotificationCount,
87
101
  NotificationCounts,
@@ -103,6 +117,7 @@ from librus_python_api.models import (
103
117
  SchoolAverage,
104
118
  SchoolDetail,
105
119
  SchoolReference,
120
+ SchoolYearArchive,
106
121
  SendResult,
107
122
  SendStatus,
108
123
  SendSubmission,
@@ -120,9 +135,17 @@ from librus_python_api.models import (
120
135
  from librus_python_api.sending import SendAttempt
121
136
  from librus_python_api.service import AccountClient, LibrusService
122
137
 
123
- __version__ = "1.0.3"
138
+ __version__ = "1.3.0"
124
139
 
125
140
  __all__ = [
141
+ "ArchiveAbsences",
142
+ "ArchiveAchievement",
143
+ "ArchiveCounts",
144
+ "ArchiveDescriptive",
145
+ "ArchiveMarks",
146
+ "ArchiveSubject",
147
+ "ArchiveYear",
148
+ "SchoolYearArchive",
126
149
  "AccountContext",
127
150
  "MessagingBackend",
128
151
  "ModernAccountData",
@@ -135,6 +158,13 @@ __all__ = [
135
158
  "ModernMessageRecipientReceipt",
136
159
  "ModernMessages",
137
160
  "ModernMessagesCursor",
161
+ "ModernUnreadCounts",
162
+ "ModernCorrespondent",
163
+ "ModernCorrespondentReference",
164
+ "ModernCorrespondents",
165
+ "ModernTeacherSubject",
166
+ "ModernTeacherSubjects",
167
+ "ModernUnreadFolders",
138
168
  "ModernMessagesPage",
139
169
  "ModernMessageSummary",
140
170
  "ModernRecipient",
@@ -207,6 +207,60 @@ ENDPOINTS: Mapping[str, Endpoint] = MappingProxyType(
207
207
  Evidence.INDEPENDENTLY_OBSERVED,
208
208
  "messages",
209
209
  ),
210
+ Endpoint(
211
+ "modern_archive_messages_received",
212
+ "GET",
213
+ "/api/archive/inbox/messages",
214
+ SideEffect.NONE,
215
+ False,
216
+ Evidence.INDEPENDENTLY_OBSERVED,
217
+ "messages",
218
+ ),
219
+ Endpoint(
220
+ "modern_archive_messages_sent",
221
+ "GET",
222
+ "/api/archive/outbox/messages",
223
+ SideEffect.NONE,
224
+ False,
225
+ Evidence.INDEPENDENTLY_OBSERVED,
226
+ "messages",
227
+ ),
228
+ Endpoint(
229
+ "modern_unread_counts",
230
+ "GET",
231
+ "/api/inbox/unreadMessagesCount",
232
+ SideEffect.NONE,
233
+ False,
234
+ Evidence.INDEPENDENTLY_OBSERVED,
235
+ "messages",
236
+ ),
237
+ Endpoint(
238
+ "modern_senders",
239
+ "GET",
240
+ "/api/inbox/messages/senders",
241
+ SideEffect.NONE,
242
+ False,
243
+ Evidence.INDEPENDENTLY_OBSERVED,
244
+ "messages",
245
+ ),
246
+ Endpoint(
247
+ "modern_receivers",
248
+ "GET",
249
+ "/api/outbox/messages/receivers",
250
+ SideEffect.NONE,
251
+ False,
252
+ Evidence.INDEPENDENTLY_OBSERVED,
253
+ "messages",
254
+ ),
255
+ Endpoint(
256
+ "modern_teacher_subjects",
257
+ "GET",
258
+ "/api/receivers/student-subjects",
259
+ SideEffect.NONE,
260
+ False,
261
+ Evidence.INDEPENDENTLY_OBSERVED,
262
+ "messages",
263
+ ),
210
264
  Endpoint(
211
265
  "modern_content_received",
212
266
  "GET",
@@ -451,6 +505,14 @@ ENDPOINTS: Mapping[str, Endpoint] = MappingProxyType(
451
505
  True,
452
506
  Evidence.INDEPENDENTLY_OBSERVED,
453
507
  ),
508
+ Endpoint(
509
+ "school_year_archive",
510
+ "GET",
511
+ "/archiwum",
512
+ SideEffect.NONE,
513
+ True,
514
+ Evidence.INDEPENDENTLY_OBSERVED,
515
+ ),
454
516
  Endpoint(
455
517
  "agenda",
456
518
  "POST",
@@ -668,6 +730,35 @@ ANNOUNCEMENT_MAX_ITEMS = 256
668
730
  ANNOUNCEMENT_MAX_FIELD_LENGTH = 1024
669
731
  ANNOUNCEMENT_MAX_CONTENT_LENGTH = 65536
670
732
  ANNOUNCEMENT_MAX_TOTAL_TEXT_LENGTH = 262144
733
+
734
+ # School-year archive (/archiwum): labels as rendered upstream, including the
735
+ # absence spellings. Matching is case-insensitive and exact otherwise.
736
+ ARCHIVE_TABLE_CLASSES = frozenset({"decorated"})
737
+ ARCHIVE_ACHIEVEMENT_TABLE_CLASSES = frozenset({"decorated", "big", "center"})
738
+ ARCHIVE_YEAR_PATTERN = (
739
+ r"Klasa: (?P<class_name>\S(?:.*\S)?) Rok: (?P<first>[0-9]{4})/(?P<second>[0-9]{4})"
740
+ )
741
+ ARCHIVE_PERIOD_LABELS = ("okres 1", "okres 2", "koniec roku")
742
+ ARCHIVE_BEHAVIOUR_HEADING = "zachowanie"
743
+ ARCHIVE_ABSENCES_HEADING = "nieobecności"
744
+ ARCHIVE_ABSENCE_LABELS = MappingProxyType(
745
+ {
746
+ "nieusprawiedlione": "unexcused",
747
+ "usprawiedlione": "excused",
748
+ "spóźnienia": "late",
749
+ }
750
+ )
751
+ ARCHIVE_ACHIEVEMENT_HEADERS = ("data", "klasa", "kategoria", "osiągnięcie")
752
+ ARCHIVE_YEAR_HEADER_TAGS = frozenset({"span", "b"})
753
+ ARCHIVE_MAX_YEARS = 16
754
+ ARCHIVE_MAX_SUBJECTS = 128
755
+ ARCHIVE_MAX_DESCRIPTIVE = 32
756
+ ARCHIVE_MAX_ACHIEVEMENTS = 256
757
+ ARCHIVE_MAX_TEXT_LENGTH = 65536
758
+ ARCHIVE_MAX_TOTAL_TEXT_LENGTH = 262144
759
+ ARCHIVE_MAX_YEAR_HEADER_LENGTH = 1024 + len("Klasa: Rok: 0000/0000")
760
+ ARCHIVE_EMPTY_TITLE = "brak danych"
761
+ ARCHIVE_EMPTY_BOX_CLASSES = frozenset({"warning-box", "information", "medium"})
671
762
  ANNOUNCEMENT_TABLE_CLASSES = frozenset(
672
763
  {"decorated", "big", "center", "printable", "margin-top"}
673
764
  )
@@ -878,17 +969,67 @@ def modern_directory_query(
878
969
 
879
970
 
880
971
  def modern_mailbox_query(
881
- folder: MessageFolder, page: int, page_size: int
972
+ folder: MessageFolder,
973
+ page: int,
974
+ page_size: int,
975
+ *,
976
+ correspondent: str | None = None,
977
+ unread_only: bool = False,
882
978
  ) -> dict[str, str]:
979
+ """Page selection plus the web app's own sender/receiver/unread filters."""
883
980
  if (
884
981
  not isinstance(folder, MessageFolder)
885
982
  or type(page) is not int
886
983
  or not 1 <= page <= 1000
887
984
  or type(page_size) is not int
888
985
  or not 1 <= page_size <= 50
986
+ or type(unread_only) is not bool
987
+ or (unread_only and folder is not MessageFolder.RECEIVED)
988
+ or (
989
+ correspondent is not None
990
+ and (
991
+ type(correspondent) is not str
992
+ or re.fullmatch(r"[0-9]{1,64}", correspondent) is None
993
+ )
994
+ )
889
995
  ):
890
996
  raise LibrusError(ErrorKind.INVALID_INPUT)
891
- return {"page": str(page), "limit": str(page_size)}
997
+ query = {}
998
+ if correspondent is not None:
999
+ query[MODERN_CORRESPONDENT_FILTERS[folder]] = correspondent
1000
+ if unread_only:
1001
+ query["unreadOnly"] = "1"
1002
+ return query | {"page": str(page), "limit": str(page_size)}
1003
+
1004
+
1005
+ MODERN_MAILBOX_OPERATIONS = frozenset(
1006
+ {
1007
+ "modern_messages_received",
1008
+ "modern_messages_sent",
1009
+ "modern_archive_messages_received",
1010
+ "modern_archive_messages_sent",
1011
+ }
1012
+ )
1013
+ MODERN_CORRESPONDENT_FILTERS = MappingProxyType(
1014
+ {MessageFolder.RECEIVED: "senderId", MessageFolder.SENT: "receiverId"}
1015
+ )
1016
+ # Filters observed in the web app for the current mailbox only.
1017
+ MODERN_MAILBOX_FILTERS = MappingProxyType(
1018
+ {
1019
+ "modern_messages_received": frozenset({"senderId", "unreadOnly"}),
1020
+ "modern_messages_sent": frozenset({"receiverId"}),
1021
+ }
1022
+ )
1023
+ MODERN_UNREAD_COUNT_FIELDS = (
1024
+ "inbox",
1025
+ "notes",
1026
+ "alerts",
1027
+ "substitutions",
1028
+ "absences",
1029
+ "justifications",
1030
+ "trash",
1031
+ )
1032
+ MODERN_MAX_UNREAD_COUNT = 1000000
892
1033
 
893
1034
 
894
1035
  def validate_modern_query(operation: str, query: Mapping[str, str]) -> None:
@@ -897,14 +1038,27 @@ def validate_modern_query(operation: str, query: Mapping[str, str]) -> None:
897
1038
  type(v) is not str for v in query.values()
898
1039
  ):
899
1040
  raise LibrusError(ErrorKind.INVALID_INPUT)
900
- if operation in {"modern_messages_received", "modern_messages_sent"}:
901
- if set(query) != {"page", "limit"} or any(
1041
+ if operation in MODERN_MAILBOX_OPERATIONS:
1042
+ filters = MODERN_MAILBOX_FILTERS.get(operation, frozenset())
1043
+ if not {"page", "limit"} <= set(query) <= {"page", "limit"} | filters or any(
902
1044
  re.fullmatch(r"[1-9][0-9]{0,3}", query[name]) is None
903
1045
  for name in ("page", "limit")
904
1046
  ):
905
1047
  raise LibrusError(ErrorKind.INVALID_INPUT)
1048
+ folder = (
1049
+ MessageFolder.SENT
1050
+ if operation == "modern_messages_sent"
1051
+ else MessageFolder.RECEIVED
1052
+ )
1053
+ unread = query.get("unreadOnly")
1054
+ if unread not in (None, "1"):
1055
+ raise LibrusError(ErrorKind.INVALID_INPUT)
906
1056
  modern_mailbox_query(
907
- MessageFolder.RECEIVED, int(query["page"]), int(query["limit"])
1057
+ folder,
1058
+ int(query["page"]),
1059
+ int(query["limit"]),
1060
+ correspondent=query.get(MODERN_CORRESPONDENT_FILTERS[folder]),
1061
+ unread_only=unread == "1",
908
1062
  )
909
1063
  return
910
1064
  allowed = [
@@ -38,6 +38,7 @@ type OperationName = Literal[
38
38
  "attachment_download",
39
39
  "recipient_groups",
40
40
  "recipients",
41
+ "school_year_archive",
41
42
  "notification_counts",
42
43
  "consume_schedule_events",
43
44
  "decode_schedule_events",
@@ -48,6 +49,12 @@ type OperationName = Literal[
48
49
  "modern_send_message",
49
50
  "modern_school_recipients",
50
51
  "modern_class_parents",
52
+ "modern_archive_messages_received",
53
+ "modern_archive_messages_sent",
54
+ "modern_unread_counts",
55
+ "modern_senders",
56
+ "modern_receivers",
57
+ "modern_teacher_subjects",
51
58
  "modern_messages_received",
52
59
  "modern_messages_sent",
53
60
  "modern_content_received",
@@ -393,6 +400,46 @@ class ModernMessageReference:
393
400
  folder: MessageFolder
394
401
  identifier: str = field(repr=False)
395
402
  account: str = field(repr=False)
403
+ # Listed from the archive mailbox. Archived content opens are not supported.
404
+ archived: bool = False
405
+
406
+
407
+ @dataclass(frozen=True, slots=True)
408
+ class ModernCorrespondentReference:
409
+ """A sender (received folder) or receiver (sent folder) to filter a list by."""
410
+
411
+ folder: MessageFolder
412
+ identifier: str = field(repr=False)
413
+ account: str = field(repr=False)
414
+
415
+
416
+ @dataclass(frozen=True, slots=True)
417
+ class ModernCorrespondent:
418
+ reference: ModernCorrespondentReference = field(repr=False)
419
+ first_name: str = field(repr=False)
420
+ last_name: str = field(repr=False)
421
+
422
+
423
+ @dataclass(frozen=True, slots=True)
424
+ class ModernCorrespondents:
425
+ identity: "Identity" = field(repr=False)
426
+ folder: MessageFolder
427
+ items: tuple[ModernCorrespondent, ...] = field(repr=False)
428
+ observation: "Observation"
429
+
430
+
431
+ @dataclass(frozen=True, slots=True)
432
+ class ModernTeacherSubject:
433
+ # The teacher's modern account ID, as a decimal string.
434
+ teacher_identifier: str = field(repr=False)
435
+ subject: str = field(repr=False)
436
+
437
+
438
+ @dataclass(frozen=True, slots=True)
439
+ class ModernTeacherSubjects:
440
+ identity: "Identity" = field(repr=False)
441
+ items: tuple[ModernTeacherSubject, ...] = field(repr=False)
442
+ observation: "Observation"
396
443
 
397
444
 
398
445
  @dataclass(frozen=True, slots=True)
@@ -417,6 +464,11 @@ class ModernMessagesPage:
417
464
  items: tuple[ModernMessageSummary, ...] = field(repr=False)
418
465
  fingerprint: str = field(repr=False)
419
466
  observation: "Observation"
467
+ archived: bool = False
468
+ correspondent: ModernCorrespondentReference | None = field(default=None, repr=False)
469
+ unread_only: bool = False
470
+ # Archive listings only: the upstream archivingInProgress status flag.
471
+ archiving_in_progress: bool | None = None
420
472
 
421
473
 
422
474
  @dataclass(frozen=True, slots=True)
@@ -429,6 +481,9 @@ class ModernMessagesCursor:
429
481
  total_count: int
430
482
  fingerprint: str = field(repr=False)
431
483
  seen_ids: tuple[str, ...] = field(repr=False)
484
+ archived: bool = False
485
+ correspondent: str | None = field(default=None, repr=False)
486
+ unread_only: bool = False
432
487
 
433
488
 
434
489
  @dataclass(frozen=True, slots=True)
@@ -441,6 +496,32 @@ class ModernMessages:
441
496
  next_cursor: ModernMessagesCursor | None = field(repr=False)
442
497
  truncation_reason: Literal["item_limit", "page_limit"] | None
443
498
  observation: "Observation"
499
+ archived: bool = False
500
+ correspondent: ModernCorrespondentReference | None = field(default=None, repr=False)
501
+ unread_only: bool = False
502
+ # Archive listings only: the upstream archivingInProgress status flag.
503
+ archiving_in_progress: bool | None = None
504
+
505
+
506
+ @dataclass(frozen=True, slots=True)
507
+ class ModernUnreadFolders:
508
+ """Unread counters the modern mailbox shows for one mailbox section."""
509
+
510
+ inbox: int
511
+ notes: int
512
+ alerts: int
513
+ substitutions: int
514
+ absences: int
515
+ justifications: int
516
+ trash: int
517
+
518
+
519
+ @dataclass(frozen=True, slots=True)
520
+ class ModernUnreadCounts:
521
+ identity: "Identity" = field(repr=False)
522
+ current: ModernUnreadFolders
523
+ archive: ModernUnreadFolders
524
+ observation: "Observation"
444
525
 
445
526
 
446
527
  @dataclass(frozen=True, slots=True)
@@ -698,6 +779,79 @@ class Announcements:
698
779
  observation: Observation
699
780
 
700
781
 
782
+ @dataclass(frozen=True, slots=True)
783
+ class ArchiveCounts:
784
+ """Counts per period as rendered; the year-end value is not checked as a sum."""
785
+
786
+ first_semester: int
787
+ second_semester: int
788
+ year_end: int
789
+
790
+
791
+ @dataclass(frozen=True, slots=True)
792
+ class ArchiveAbsences:
793
+ unexcused: ArchiveCounts
794
+ excused: ArchiveCounts
795
+ late: ArchiveCounts
796
+
797
+
798
+ @dataclass(frozen=True, slots=True)
799
+ class ArchiveMarks:
800
+ """Rendered marks; "-" and empty text are preserved, never invented.
801
+
802
+ `year_end` is the archive's year-end column, not asserted to equal the
803
+ published annual grade of `SubjectGradeSummary`.
804
+ """
805
+
806
+ first_semester: str = field(repr=False)
807
+ second_semester: str = field(repr=False)
808
+ year_end: str = field(repr=False)
809
+
810
+
811
+ @dataclass(frozen=True, slots=True)
812
+ class ArchiveSubject:
813
+ subject: str = field(repr=False)
814
+ marks: ArchiveMarks = field(repr=False)
815
+
816
+
817
+ @dataclass(frozen=True, slots=True)
818
+ class ArchiveDescriptive:
819
+ label: str = field(repr=False)
820
+ text: str = field(repr=False)
821
+
822
+
823
+ @dataclass(frozen=True, slots=True)
824
+ class ArchiveYear:
825
+ """One earlier school year. Behaviour cells are raw positional text: the
826
+ first-semester column and the cell spanning second semester and year end;
827
+ their populated meaning is not established."""
828
+
829
+ class_name: str = field(repr=False)
830
+ school_year: str = field(repr=False)
831
+ first_year: int = field(repr=False)
832
+ subjects: tuple[ArchiveSubject, ...] = field(repr=False)
833
+ descriptive: tuple[ArchiveDescriptive, ...] = field(repr=False)
834
+ behaviour_first_semester: str = field(repr=False)
835
+ behaviour_second_semester_and_year_end: str = field(repr=False)
836
+ absences: ArchiveAbsences = field(repr=False)
837
+
838
+
839
+ @dataclass(frozen=True, slots=True)
840
+ class ArchiveAchievement:
841
+ day: date = field(repr=False)
842
+ class_name: str = field(repr=False)
843
+ category: str = field(repr=False)
844
+ text: str = field(repr=False)
845
+
846
+
847
+ @dataclass(frozen=True, slots=True)
848
+ class SchoolYearArchive:
849
+ identity: Identity
850
+ years: tuple[ArchiveYear, ...] = field(repr=False)
851
+ achievements: tuple[ArchiveAchievement, ...] = field(repr=False)
852
+ observation: Observation
853
+
854
+
701
855
  @dataclass(frozen=True, slots=True)
702
856
  class AgendaEvent:
703
857
  day: date = field(repr=False)