librus-python-api 1.0.2__tar.gz → 1.2.1__tar.gz

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 (53) hide show
  1. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/API.md +37 -5
  2. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/CHANGELOG.md +59 -0
  3. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/PKG-INFO +3 -3
  4. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/README.md +2 -2
  5. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/__init__.py +15 -1
  6. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/config.py +130 -6
  7. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/models.py +80 -0
  8. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/modern_mailbox.py +137 -9
  9. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/modern_messages.py +23 -0
  10. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/service.py +225 -27
  11. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/transport.py +3 -6
  12. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/.gitignore +0 -0
  13. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/LICENSE +0 -0
  14. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/SECURITY.md +0 -0
  15. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/pyproject.toml +0 -0
  16. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/_notification_bootstrap.py +0 -0
  17. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/_notification_codec.py +0 -0
  18. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/_storage.py +0 -0
  19. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/_windows_filesystem.py +0 -0
  20. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/announcements.py +0 -0
  21. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/attachment_routes.py +0 -0
  22. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/attachments.py +0 -0
  23. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/attendance.py +0 -0
  24. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/attendance_frequency.py +0 -0
  25. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/budget.py +0 -0
  26. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/checkpoint.py +0 -0
  27. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/completed_lessons.py +0 -0
  28. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/detail_fields.py +0 -0
  29. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/diagnostics.py +0 -0
  30. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/exceptions.py +0 -0
  31. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/files.py +0 -0
  32. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/grade_parsers.py +0 -0
  33. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/grade_records.py +0 -0
  34. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/homework_range.py +0 -0
  35. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/lifecycle.py +0 -0
  36. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/markup.py +0 -0
  37. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/message_content.py +0 -0
  38. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/messages.py +0 -0
  39. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/modern_body.py +0 -0
  40. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/notification_models.py +0 -0
  41. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/notification_persistence.py +0 -0
  42. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/notification_workflow.py +0 -0
  43. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/notifications.py +0 -0
  44. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/parsers.py +0 -0
  45. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/parsing.py +0 -0
  46. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/persistence.py +0 -0
  47. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/py.typed +0 -0
  48. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/recipients.py +0 -0
  49. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/scheduler.py +0 -0
  50. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/school_reads.py +0 -0
  51. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/sending.py +0 -0
  52. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/src/librus_python_api/timetable.py +0 -0
  53. {librus_python_api-1.0.2 → librus_python_api-1.2.1}/uv.lock +0 -0
@@ -399,13 +399,45 @@ redirects, fallback, post-send lookup or implicit reauthentication. Cancel/shutd
399
399
  propagate normally with an inspectable outcome after joined cleanup, as below.
400
400
  Modern list/content reads do not reconcile durable UNKNOWN send history.
401
401
 
402
- - `modern_messages_page(folder=RECEIVED, *, page=1, page_size=10, budget=None,
403
- max_age_seconds=0)` returns `ModernMessagesPage`. Pages are one-based. This GET
404
- returns summaries only; sent read status can be unknown.
402
+ - `modern_messages_page(folder=RECEIVED, *, page=1, page_size=10, archived=False,
403
+ budget=None, max_age_seconds=0)` returns `ModernMessagesPage`. Pages are
404
+ one-based. This GET returns summaries only; sent read status can be unknown.
405
405
  - `modern_messages(folder=RECEIVED, *, cursor=None, page_size=50, max_pages=4,
406
- limit=128, budget=None, max_age_seconds=0)` returns `ModernMessages` with explicit
407
- truncation, duplicate counts and account/folder/page-size-bound continuation.
406
+ limit=128, archived=False, budget=None, max_age_seconds=0)` returns
407
+ `ModernMessages` with explicit truncation, duplicate counts and
408
+ account/folder/mailbox/page-size-bound continuation.
408
409
  Cursor drift and later-page errors never return partial output.
410
+ - `archived=True` (1.1.0) lists the archive mailbox instead of the current one.
411
+ Pages, collections, cursors and references carry `archived`; a cursor never
412
+ continues the other mailbox. Archived content opens raise
413
+ `UNSUPPORTED_CAPABILITY` because no archived content route or mark-read effect
414
+ is established. Archive pages and collections report the upstream
415
+ `archivingInProgress` status as `archiving_in_progress` (`None` for the
416
+ current mailbox). Live archives report `true` while listing normally, so it
417
+ is informational; before 1.2.1 it wrongly made every archive read
418
+ `UNSUPPORTED_CAPABILITY`.
419
+ - `modern_correspondents(folder=RECEIVED, *, budget=None, max_age_seconds=0)`
420
+ (1.2.0) returns `ModernCorrespondents`: the senders (received) or receivers
421
+ (sent) the web app offers in its filter panel. Each `ModernCorrespondent` has
422
+ `first_name`, `last_name` (one may be empty, never both) and a login-bound
423
+ `ModernCorrespondentReference`.
424
+ - `modern_messages_page()` and `modern_messages()` accept `correspondent=` (a
425
+ reference for the same folder and login) and, for the received folder,
426
+ `unread_only=True` (1.2.0). They send the web app's own `senderId`,
427
+ `receiverId` and `unreadOnly` filters. Results and cursors record the filter,
428
+ and a cursor never continues a differently filtered list. Filters on the
429
+ archive mailbox raise `UNSUPPORTED_CAPABILITY`; they were not observed there.
430
+ - `modern_teacher_subjects(*, budget=None, max_age_seconds=0)` (1.2.0) returns
431
+ `ModernTeacherSubjects`: `ModernTeacherSubject(teacher_identifier, subject)`
432
+ pairs, one per subject a teacher teaches the student. Identical repeated rows
433
+ collapse to one (1.2.1). The identifier is the
434
+ teacher's modern account ID; it is informational and not a recipient reference.
435
+ - `modern_unread_counts(*, budget=None, max_age_seconds=0)` (1.1.0) returns
436
+ `ModernUnreadCounts` with `current` and `archive` `ModernUnreadFolders`:
437
+ `inbox`, `notes`, `alerts`, `substitutions`, `absences`, `justifications` and
438
+ `trash`. It opens no message. A missing or invalid counter fails with `PARSE`;
439
+ unknown extra counters are ignored. Populated counter semantics beyond the
440
+ observed field names are not independently established.
409
441
  Page size is 1-50, page number 1-1,000, `max_pages` 1-8, `limit` 1-256 and
410
442
  cursor history at most 2,000 IDs. These are library bounds, not upstream maxima.
411
443
  - `modern_message_content(reference, *, allow_mark_read=False, budget=None,
@@ -1,5 +1,64 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.2.1 (2026-10-06) - Live fixes for archive listing and teacher subjects
4
+
5
+ Compatible patch release found by the new pre-publication live check. 1.1.0 and
6
+ 1.2.0 were tagged but not published to PyPI because that check failed.
7
+
8
+ - List the modern archive mailbox. Live archive pages report
9
+ `archivingInProgress: true` while listing normally, and 1.1.0/1.2.0 treated
10
+ that as unsupported, so every archive read failed. Archive pages and
11
+ collections now report it as `archiving_in_progress`; the current mailbox
12
+ still refuses the flag.
13
+ - Collapse identical repeated rows in `modern_teacher_subjects()` instead of
14
+ failing with `PARSE`.
15
+
16
+ ## 1.2.0 (2026-10-06) - Modern correspondent filters and teacher subjects
17
+
18
+ Compatible minor release. Existing calls and results are unchanged; new fields
19
+ have defaults.
20
+
21
+ - Add `modern_correspondents(folder)`, listing the senders (received) or
22
+ receivers (sent) the modern mailbox can be filtered by.
23
+ - Filter `modern_messages_page()` and `modern_messages()` by `correspondent=`
24
+ and, for received messages, `unread_only=True`, using the web app's own
25
+ query filters. Pages, collections and cursors record the filter; a cursor
26
+ never continues a differently filtered list. The archive mailbox does not
27
+ accept filters.
28
+ - Add `modern_teacher_subjects()`, pairing teachers' modern account IDs with
29
+ the subjects they teach the student.
30
+
31
+ ## 1.1.0 (2026-10-06) - Modern archive mailbox and unread counts
32
+
33
+ Compatible minor release. Existing calls and results are unchanged; new fields
34
+ have defaults.
35
+
36
+ - List the modern archive mailbox (earlier school years) with
37
+ `modern_messages_page(..., archived=True)` and `modern_messages(...,
38
+ archived=True)`. Pages, collections, cursors and message references gain an
39
+ `archived` field (default `False`), and a cursor never continues the other
40
+ mailbox. Opening an archived message raises `UNSUPPORTED_CAPABILITY` until an
41
+ archived content route is established.
42
+ - Add `modern_unread_counts()`, returning `ModernUnreadCounts` with current and
43
+ archive `ModernUnreadFolders` counters (inbox, notes, alerts, substitutions,
44
+ absences, justifications, trash) from the modern mailbox's own counter route.
45
+ It opens no message.
46
+
47
+ ## 1.0.3 (2026-10-06) - Notification counts for parent logins
48
+
49
+ Compatible patch release. The public library API is unchanged.
50
+
51
+ - Read `notification_counts()` from the information page (`/informacja`), which
52
+ every login can open, instead of the student landing route `/uczen/index`.
53
+ That route returned 403 for a parent login in a live check, and the session
54
+ was unusable afterwards. The counters come from the menu shared by every
55
+ page, so the result is unchanged for student logins. The read is now
56
+ retry-safe: a proven session expiry is recovered once.
57
+ - Count the modern messages launcher link (`/wiadomosci3`) as the messages
58
+ category. Before, schools linking it showed no messages counter.
59
+ - Keep `/uczen/index` only as an accepted login redirect target. It is never
60
+ requested directly.
61
+
3
62
  ## 1.0.2 (2026-10-06) - Faster shared request policy
4
63
 
5
64
  Compatible patch release. The public library API is unchanged.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: librus-python-api
3
- Version: 1.0.2
3
+ Version: 1.2.1
4
4
  Summary: Independent bounded async Librus Synergia client
5
5
  Project-URL: Repository, https://github.com/krzysztofbury/librus-python-api
6
6
  Project-URL: Issues, https://github.com/krzysztofbury/librus-python-api/issues
@@ -51,7 +51,7 @@ See [limitations](#supported-features-and-limitations) below.
51
51
  Install the exact release from PyPI after its gated publication completes:
52
52
 
53
53
  ```sh
54
- python -m pip install librus-python-api==1.0.2
54
+ python -m pip install librus-python-api==1.2.1
55
55
  ```
56
56
 
57
57
  Pin the exact version qualified by your application. Before publication, use a
@@ -67,7 +67,7 @@ python -m pip install .
67
67
  To install a locally built wheel instead:
68
68
 
69
69
  ```sh
70
- python -m pip install ./dist/librus_python_api-1.0.2-py3-none-any.whl
70
+ python -m pip install ./dist/librus_python_api-1.2.1-py3-none-any.whl
71
71
  ```
72
72
 
73
73
  No CLI or background process is installed: import the library in your own program.
@@ -25,7 +25,7 @@ See [limitations](#supported-features-and-limitations) below.
25
25
  Install the exact release from PyPI after its gated publication completes:
26
26
 
27
27
  ```sh
28
- python -m pip install librus-python-api==1.0.2
28
+ python -m pip install librus-python-api==1.2.1
29
29
  ```
30
30
 
31
31
  Pin the exact version qualified by your application. Before publication, use a
@@ -41,7 +41,7 @@ python -m pip install .
41
41
  To install a locally built wheel instead:
42
42
 
43
43
  ```sh
44
- python -m pip install ./dist/librus_python_api-1.0.2-py3-none-any.whl
44
+ python -m pip install ./dist/librus_python_api-1.2.1-py3-none-any.whl
45
45
  ```
46
46
 
47
47
  No CLI or background process is installed: import the library in your own program.
@@ -65,6 +65,9 @@ from librus_python_api.models import (
65
65
  MessageTimestamp,
66
66
  MessagingBackend,
67
67
  ModernAccountData,
68
+ ModernCorrespondent,
69
+ ModernCorrespondentReference,
70
+ ModernCorrespondents,
68
71
  ModernIdentity,
69
72
  ModernMessageAttachment,
70
73
  ModernMessageAttachmentReference,
@@ -82,6 +85,10 @@ from librus_python_api.models import (
82
85
  ModernRecipientTypeReference,
83
86
  ModernRecipientTypes,
84
87
  ModernSendSubmission,
88
+ ModernTeacherSubject,
89
+ ModernTeacherSubjects,
90
+ ModernUnreadCounts,
91
+ ModernUnreadFolders,
85
92
  NotificationCategory,
86
93
  NotificationCount,
87
94
  NotificationCounts,
@@ -120,7 +127,7 @@ from librus_python_api.models import (
120
127
  from librus_python_api.sending import SendAttempt
121
128
  from librus_python_api.service import AccountClient, LibrusService
122
129
 
123
- __version__ = "1.0.2"
130
+ __version__ = "1.2.1"
124
131
 
125
132
  __all__ = [
126
133
  "AccountContext",
@@ -135,6 +142,13 @@ __all__ = [
135
142
  "ModernMessageRecipientReceipt",
136
143
  "ModernMessages",
137
144
  "ModernMessagesCursor",
145
+ "ModernUnreadCounts",
146
+ "ModernCorrespondent",
147
+ "ModernCorrespondentReference",
148
+ "ModernCorrespondents",
149
+ "ModernTeacherSubject",
150
+ "ModernTeacherSubjects",
151
+ "ModernUnreadFolders",
138
152
  "ModernMessagesPage",
139
153
  "ModernMessageSummary",
140
154
  "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",
@@ -337,8 +391,10 @@ ENDPOINTS: Mapping[str, Endpoint] = MappingProxyType(
337
391
  False,
338
392
  Evidence.SYNTHETIC_ONLY,
339
393
  ),
394
+ # Accepted only as a redirect target during login. Never requested
395
+ # directly: it answers parent logins with 403.
340
396
  Endpoint(
341
- "notification_counts",
397
+ "login_student_landing",
342
398
  "GET",
343
399
  "/uczen/index",
344
400
  SideEffect.AUTHENTICATION,
@@ -695,6 +751,10 @@ CHECKPOINT_TIMEOUT_SECONDS = 5.0
695
751
  CHECKPOINT_MAX_TIMEOUT_SECONDS = 30.0
696
752
  SCHEDULE_RESPONSE_VERSION = 1
697
753
  SCHEDULE_MAX_EVENTS = 1024
754
+ # Every Synergia page carries the counter menu. The information page is
755
+ # readable by student and parent logins alike; the student landing route
756
+ # (/uczen/index) answers parent logins with 403 and is not used.
757
+ NOTIFICATION_COUNTS_ENDPOINT = "student_information"
698
758
  NOTIFICATION_MAX_MENU_ITEMS = 64
699
759
  NOTIFICATION_MAX_COUNT = 1000000
700
760
  NOTIFICATION_DESTINATIONS = MappingProxyType(
@@ -702,6 +762,7 @@ NOTIFICATION_DESTINATIONS = MappingProxyType(
702
762
  "/przegladaj_oceny/uczen": "grades",
703
763
  "/przegladaj_nb/uczen": "attendance",
704
764
  "/wiadomosci": "messages",
765
+ "/wiadomosci3": "messages",
705
766
  "/ogloszenia": "announcements",
706
767
  "/terminarz": "agenda",
707
768
  "/moje_zadania": "homework",
@@ -871,17 +932,67 @@ def modern_directory_query(
871
932
 
872
933
 
873
934
  def modern_mailbox_query(
874
- folder: MessageFolder, page: int, page_size: int
935
+ folder: MessageFolder,
936
+ page: int,
937
+ page_size: int,
938
+ *,
939
+ correspondent: str | None = None,
940
+ unread_only: bool = False,
875
941
  ) -> dict[str, str]:
942
+ """Page selection plus the web app's own sender/receiver/unread filters."""
876
943
  if (
877
944
  not isinstance(folder, MessageFolder)
878
945
  or type(page) is not int
879
946
  or not 1 <= page <= 1000
880
947
  or type(page_size) is not int
881
948
  or not 1 <= page_size <= 50
949
+ or type(unread_only) is not bool
950
+ or (unread_only and folder is not MessageFolder.RECEIVED)
951
+ or (
952
+ correspondent is not None
953
+ and (
954
+ type(correspondent) is not str
955
+ or re.fullmatch(r"[0-9]{1,64}", correspondent) is None
956
+ )
957
+ )
882
958
  ):
883
959
  raise LibrusError(ErrorKind.INVALID_INPUT)
884
- return {"page": str(page), "limit": str(page_size)}
960
+ query = {}
961
+ if correspondent is not None:
962
+ query[MODERN_CORRESPONDENT_FILTERS[folder]] = correspondent
963
+ if unread_only:
964
+ query["unreadOnly"] = "1"
965
+ return query | {"page": str(page), "limit": str(page_size)}
966
+
967
+
968
+ MODERN_MAILBOX_OPERATIONS = frozenset(
969
+ {
970
+ "modern_messages_received",
971
+ "modern_messages_sent",
972
+ "modern_archive_messages_received",
973
+ "modern_archive_messages_sent",
974
+ }
975
+ )
976
+ MODERN_CORRESPONDENT_FILTERS = MappingProxyType(
977
+ {MessageFolder.RECEIVED: "senderId", MessageFolder.SENT: "receiverId"}
978
+ )
979
+ # Filters observed in the web app for the current mailbox only.
980
+ MODERN_MAILBOX_FILTERS = MappingProxyType(
981
+ {
982
+ "modern_messages_received": frozenset({"senderId", "unreadOnly"}),
983
+ "modern_messages_sent": frozenset({"receiverId"}),
984
+ }
985
+ )
986
+ MODERN_UNREAD_COUNT_FIELDS = (
987
+ "inbox",
988
+ "notes",
989
+ "alerts",
990
+ "substitutions",
991
+ "absences",
992
+ "justifications",
993
+ "trash",
994
+ )
995
+ MODERN_MAX_UNREAD_COUNT = 1000000
885
996
 
886
997
 
887
998
  def validate_modern_query(operation: str, query: Mapping[str, str]) -> None:
@@ -890,14 +1001,27 @@ def validate_modern_query(operation: str, query: Mapping[str, str]) -> None:
890
1001
  type(v) is not str for v in query.values()
891
1002
  ):
892
1003
  raise LibrusError(ErrorKind.INVALID_INPUT)
893
- if operation in {"modern_messages_received", "modern_messages_sent"}:
894
- if set(query) != {"page", "limit"} or any(
1004
+ if operation in MODERN_MAILBOX_OPERATIONS:
1005
+ filters = MODERN_MAILBOX_FILTERS.get(operation, frozenset())
1006
+ if not {"page", "limit"} <= set(query) <= {"page", "limit"} | filters or any(
895
1007
  re.fullmatch(r"[1-9][0-9]{0,3}", query[name]) is None
896
1008
  for name in ("page", "limit")
897
1009
  ):
898
1010
  raise LibrusError(ErrorKind.INVALID_INPUT)
1011
+ folder = (
1012
+ MessageFolder.SENT
1013
+ if operation == "modern_messages_sent"
1014
+ else MessageFolder.RECEIVED
1015
+ )
1016
+ unread = query.get("unreadOnly")
1017
+ if unread not in (None, "1"):
1018
+ raise LibrusError(ErrorKind.INVALID_INPUT)
899
1019
  modern_mailbox_query(
900
- MessageFolder.RECEIVED, int(query["page"]), int(query["limit"])
1020
+ folder,
1021
+ int(query["page"]),
1022
+ int(query["limit"]),
1023
+ correspondent=query.get(MODERN_CORRESPONDENT_FILTERS[folder]),
1024
+ unread_only=unread == "1",
901
1025
  )
902
1026
  return
903
1027
  allowed = [
@@ -48,6 +48,12 @@ type OperationName = Literal[
48
48
  "modern_send_message",
49
49
  "modern_school_recipients",
50
50
  "modern_class_parents",
51
+ "modern_archive_messages_received",
52
+ "modern_archive_messages_sent",
53
+ "modern_unread_counts",
54
+ "modern_senders",
55
+ "modern_receivers",
56
+ "modern_teacher_subjects",
51
57
  "modern_messages_received",
52
58
  "modern_messages_sent",
53
59
  "modern_content_received",
@@ -393,6 +399,46 @@ class ModernMessageReference:
393
399
  folder: MessageFolder
394
400
  identifier: str = field(repr=False)
395
401
  account: str = field(repr=False)
402
+ # Listed from the archive mailbox. Archived content opens are not supported.
403
+ archived: bool = False
404
+
405
+
406
+ @dataclass(frozen=True, slots=True)
407
+ class ModernCorrespondentReference:
408
+ """A sender (received folder) or receiver (sent folder) to filter a list by."""
409
+
410
+ folder: MessageFolder
411
+ identifier: str = field(repr=False)
412
+ account: str = field(repr=False)
413
+
414
+
415
+ @dataclass(frozen=True, slots=True)
416
+ class ModernCorrespondent:
417
+ reference: ModernCorrespondentReference = field(repr=False)
418
+ first_name: str = field(repr=False)
419
+ last_name: str = field(repr=False)
420
+
421
+
422
+ @dataclass(frozen=True, slots=True)
423
+ class ModernCorrespondents:
424
+ identity: "Identity" = field(repr=False)
425
+ folder: MessageFolder
426
+ items: tuple[ModernCorrespondent, ...] = field(repr=False)
427
+ observation: "Observation"
428
+
429
+
430
+ @dataclass(frozen=True, slots=True)
431
+ class ModernTeacherSubject:
432
+ # The teacher's modern account ID, as a decimal string.
433
+ teacher_identifier: str = field(repr=False)
434
+ subject: str = field(repr=False)
435
+
436
+
437
+ @dataclass(frozen=True, slots=True)
438
+ class ModernTeacherSubjects:
439
+ identity: "Identity" = field(repr=False)
440
+ items: tuple[ModernTeacherSubject, ...] = field(repr=False)
441
+ observation: "Observation"
396
442
 
397
443
 
398
444
  @dataclass(frozen=True, slots=True)
@@ -417,6 +463,11 @@ class ModernMessagesPage:
417
463
  items: tuple[ModernMessageSummary, ...] = field(repr=False)
418
464
  fingerprint: str = field(repr=False)
419
465
  observation: "Observation"
466
+ archived: bool = False
467
+ correspondent: ModernCorrespondentReference | None = field(default=None, repr=False)
468
+ unread_only: bool = False
469
+ # Archive listings only: the upstream archivingInProgress status flag.
470
+ archiving_in_progress: bool | None = None
420
471
 
421
472
 
422
473
  @dataclass(frozen=True, slots=True)
@@ -429,6 +480,9 @@ class ModernMessagesCursor:
429
480
  total_count: int
430
481
  fingerprint: str = field(repr=False)
431
482
  seen_ids: tuple[str, ...] = field(repr=False)
483
+ archived: bool = False
484
+ correspondent: str | None = field(default=None, repr=False)
485
+ unread_only: bool = False
432
486
 
433
487
 
434
488
  @dataclass(frozen=True, slots=True)
@@ -441,6 +495,32 @@ class ModernMessages:
441
495
  next_cursor: ModernMessagesCursor | None = field(repr=False)
442
496
  truncation_reason: Literal["item_limit", "page_limit"] | None
443
497
  observation: "Observation"
498
+ archived: bool = False
499
+ correspondent: ModernCorrespondentReference | None = field(default=None, repr=False)
500
+ unread_only: bool = False
501
+ # Archive listings only: the upstream archivingInProgress status flag.
502
+ archiving_in_progress: bool | None = None
503
+
504
+
505
+ @dataclass(frozen=True, slots=True)
506
+ class ModernUnreadFolders:
507
+ """Unread counters the modern mailbox shows for one mailbox section."""
508
+
509
+ inbox: int
510
+ notes: int
511
+ alerts: int
512
+ substitutions: int
513
+ absences: int
514
+ justifications: int
515
+ trash: int
516
+
517
+
518
+ @dataclass(frozen=True, slots=True)
519
+ class ModernUnreadCounts:
520
+ identity: "Identity" = field(repr=False)
521
+ current: ModernUnreadFolders
522
+ archive: ModernUnreadFolders
523
+ observation: "Observation"
444
524
 
445
525
 
446
526
  @dataclass(frozen=True, slots=True)