librus-python-api 1.0.3__tar.gz → 1.3.0__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 (54) hide show
  1. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/API.md +76 -6
  2. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/CHANGELOG.md +64 -0
  3. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/PKG-INFO +15 -7
  4. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/README.md +14 -6
  5. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/__init__.py +31 -1
  6. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/config.py +159 -5
  7. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/models.py +154 -0
  8. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/modern_mailbox.py +137 -9
  9. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/modern_messages.py +23 -0
  10. librus_python_api-1.3.0/src/librus_python_api/school_year_archive.py +432 -0
  11. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/service.py +244 -25
  12. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/transport.py +3 -6
  13. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/uv.lock +57 -57
  14. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/.gitignore +0 -0
  15. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/LICENSE +0 -0
  16. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/SECURITY.md +0 -0
  17. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/pyproject.toml +0 -0
  18. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/_notification_bootstrap.py +0 -0
  19. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/_notification_codec.py +0 -0
  20. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/_storage.py +0 -0
  21. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/_windows_filesystem.py +0 -0
  22. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/announcements.py +0 -0
  23. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/attachment_routes.py +0 -0
  24. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/attachments.py +0 -0
  25. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/attendance.py +0 -0
  26. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/attendance_frequency.py +0 -0
  27. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/budget.py +0 -0
  28. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/checkpoint.py +0 -0
  29. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/completed_lessons.py +0 -0
  30. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/detail_fields.py +0 -0
  31. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/diagnostics.py +0 -0
  32. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/exceptions.py +0 -0
  33. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/files.py +0 -0
  34. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/grade_parsers.py +0 -0
  35. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/grade_records.py +0 -0
  36. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/homework_range.py +0 -0
  37. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/lifecycle.py +0 -0
  38. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/markup.py +0 -0
  39. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/message_content.py +0 -0
  40. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/messages.py +0 -0
  41. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/modern_body.py +0 -0
  42. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/notification_models.py +0 -0
  43. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/notification_persistence.py +0 -0
  44. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/notification_workflow.py +0 -0
  45. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/notifications.py +0 -0
  46. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/parsers.py +0 -0
  47. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/parsing.py +0 -0
  48. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/persistence.py +0 -0
  49. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/py.typed +0 -0
  50. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/recipients.py +0 -0
  51. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/scheduler.py +0 -0
  52. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/school_reads.py +0 -0
  53. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/sending.py +0 -0
  54. {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/timetable.py +0 -0
@@ -191,7 +191,45 @@ source)` with an aware UTC timestamp.
191
191
  selection. It reuses only that view's cache; averages and undated summaries
192
192
  remain outside the window.
193
193
 
194
- The observation card ("Karta spostrzeżeń") shown on some grades pages is not
194
+ ### School-year archive (1.3.0)
195
+
196
+ `school_year_archive(*, budget=None, max_age_seconds=0.0)` returns
197
+ `SchoolYearArchive(identity, years, achievements, observation)`. It performs one
198
+ read-only GET for all earlier years, with the same per-login cache, coalescing,
199
+ budgets and safe-read session recovery as `announcements()`. There is no year
200
+ selection or chart request. This is separate from the modern message archive.
201
+
202
+ - `years` contains `ArchiveYear` in page order: `class_name`, `school_year`,
203
+ `first_year`, `subjects`, `descriptive`, two raw behaviour fields and `absences`.
204
+ - Each `ArchiveSubject(subject, marks)` has `ArchiveMarks(first_semester,
205
+ second_semester, year_end)`. Empty text and `-` remain distinct. `year_end` is
206
+ not asserted to equal `SubjectGradeSummary.annual`.
207
+ - `ArchiveDescriptive(label, text)` keeps whole-year text, with explicit BR
208
+ boundaries as newlines. Source whitespace is normalized.
209
+ - `behaviour_first_semester` and `behaviour_second_semester_and_year_end` retain
210
+ positional text. Populated behaviour semantics have not been observed.
211
+ - `ArchiveAbsences(unexcused, excused, late)` contains `ArchiveCounts` with the
212
+ same three period names. Counts are rendered integers, never summed or repaired.
213
+ - `achievements` contains `ArchiveAchievement(day, class_name, category, text)`;
214
+ `day` is a civil `date`. A header-only achievements table yields an empty tuple.
215
+
216
+ All archive types are root exports. School text and record containers are omitted
217
+ from reprs; the page's student-name header does not enter archive records.
218
+ Limits: 16 unique consecutive school-year pairs, 128 subject rows, 32 descriptive
219
+ rows, 256 achievements, 1024 characters per short field, 65536 per long text
220
+ cell and 262144 total rendered characters, plus the shared transport/tree bounds.
221
+
222
+ The observed `Brak danych` information notice returns empty `years` and
223
+ `achievements`. Missing tables without that explicit marker still raise `PARSE`;
224
+ an empty notice combined with data tables is rejected. Repeated school years
225
+ (including a class-change layout) are ambiguous and raise `PARSE`. Unknown
226
+ geometry/markup raises `UNSUPPORTED_CAPABILITY`; bounds raise `LIMIT`, never
227
+ partial success. Consumers must handle these states before exposing the read.
228
+ See [archive evidence and limitations](contracts/grades.md#school-year-archive-130).
229
+
230
+ The observed grade-detail page added no fields beyond the list tooltip, so
231
+ there is no separate per-grade detail request. This finding is limited to the
232
+ observed layout. The observation card ("Karta spostrzeżeń") shown on some grades pages is not
195
233
  read yet.
196
234
 
197
235
  ## Attendance
@@ -399,13 +437,45 @@ redirects, fallback, post-send lookup or implicit reauthentication. Cancel/shutd
399
437
  propagate normally with an inspectable outcome after joined cleanup, as below.
400
438
  Modern list/content reads do not reconcile durable UNKNOWN send history.
401
439
 
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.
440
+ - `modern_messages_page(folder=RECEIVED, *, page=1, page_size=10, archived=False,
441
+ budget=None, max_age_seconds=0)` returns `ModernMessagesPage`. Pages are
442
+ one-based. This GET returns summaries only; sent read status can be unknown.
405
443
  - `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.
444
+ limit=128, archived=False, budget=None, max_age_seconds=0)` returns
445
+ `ModernMessages` with explicit truncation, duplicate counts and
446
+ account/folder/mailbox/page-size-bound continuation.
408
447
  Cursor drift and later-page errors never return partial output.
448
+ - `archived=True` (1.1.0) lists the archive mailbox instead of the current one.
449
+ Pages, collections, cursors and references carry `archived`; a cursor never
450
+ continues the other mailbox. Archived content opens raise
451
+ `UNSUPPORTED_CAPABILITY` because no archived content route or mark-read effect
452
+ is established. Archive pages and collections report the upstream
453
+ `archivingInProgress` status as `archiving_in_progress` (`None` for the
454
+ current mailbox). Live archives report `true` while listing normally, so it
455
+ is informational; before 1.2.1 it wrongly made every archive read
456
+ `UNSUPPORTED_CAPABILITY`.
457
+ - `modern_correspondents(folder=RECEIVED, *, budget=None, max_age_seconds=0)`
458
+ (1.2.0) returns `ModernCorrespondents`: the senders (received) or receivers
459
+ (sent) the web app offers in its filter panel. Each `ModernCorrespondent` has
460
+ `first_name`, `last_name` (one may be empty, never both) and a login-bound
461
+ `ModernCorrespondentReference`.
462
+ - `modern_messages_page()` and `modern_messages()` accept `correspondent=` (a
463
+ reference for the same folder and login) and, for the received folder,
464
+ `unread_only=True` (1.2.0). They send the web app's own `senderId`,
465
+ `receiverId` and `unreadOnly` filters. Results and cursors record the filter,
466
+ and a cursor never continues a differently filtered list. Filters on the
467
+ archive mailbox raise `UNSUPPORTED_CAPABILITY`; they were not observed there.
468
+ - `modern_teacher_subjects(*, budget=None, max_age_seconds=0)` (1.2.0) returns
469
+ `ModernTeacherSubjects`: `ModernTeacherSubject(teacher_identifier, subject)`
470
+ pairs, one per subject a teacher teaches the student. Identical repeated rows
471
+ collapse to one (1.2.1). The identifier is the
472
+ teacher's modern account ID; it is informational and not a recipient reference.
473
+ - `modern_unread_counts(*, budget=None, max_age_seconds=0)` (1.1.0) returns
474
+ `ModernUnreadCounts` with `current` and `archive` `ModernUnreadFolders`:
475
+ `inbox`, `notes`, `alerts`, `substitutions`, `absences`, `justifications` and
476
+ `trash`. It opens no message. A missing or invalid counter fails with `PARSE`;
477
+ unknown extra counters are ignored. Populated counter semantics beyond the
478
+ observed field names are not independently established.
409
479
  Page size is 1-50, page number 1-1,000, `max_pages` 1-8, `limit` 1-256 and
410
480
  cursor history at most 2,000 IDs. These are library bounds, not upstream maxima.
411
481
  - `modern_message_content(reference, *, allow_mark_read=False, budget=None,
@@ -1,5 +1,69 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.3.0 (2026-10-07) - School-year archive read
4
+
5
+ Compatible minor release. Existing calls and results are unchanged.
6
+
7
+ - Add `AccountClient.school_year_archive()` and frozen per-year records for
8
+ subject marks, descriptive text, raw behaviour cells, absence counts and
9
+ dated achievements. One read-only GET covers all earlier years without year
10
+ selection, chart requests or fetching grade-detail links.
11
+ - Apply the shared account-isolated cache, coalescing, recovery and budgets.
12
+ Reject ambiguous tables, unknown markup and exceeded bounds without partial
13
+ output. Preserve empty marks and school-provided year-end values.
14
+ - Add guarded weekly archive coverage with redacted year counts. Older pinned
15
+ libraries without the method skip this check. Installed live checks passed
16
+ on two parent logins, covering populated and explicit empty archives with
17
+ unchanged notification counters. Broader school-layout support is unqualified.
18
+ - Recognize the observed `Brak danych` empty notice and the malformed HTML
19
+ chart container after the achievements footer. Nested tabular data still fails.
20
+ - Record the #40 grade-detail finding: the observed page added no fields beyond
21
+ the list tooltip, so no separate grade-detail read is introduced.
22
+
23
+ ## 1.2.1 (2026-10-06) - Live fixes for archive listing and teacher subjects
24
+
25
+ Compatible patch release found by the new pre-publication live check. 1.1.0 and
26
+ 1.2.0 were tagged but not published to PyPI because that check failed.
27
+
28
+ - List the modern archive mailbox. Live archive pages report
29
+ `archivingInProgress: true` while listing normally, and 1.1.0/1.2.0 treated
30
+ that as unsupported, so every archive read failed. Archive pages and
31
+ collections now report it as `archiving_in_progress`; the current mailbox
32
+ still refuses the flag.
33
+ - Collapse identical repeated rows in `modern_teacher_subjects()` instead of
34
+ failing with `PARSE`.
35
+
36
+ ## 1.2.0 (2026-10-06) - Modern correspondent filters and teacher subjects
37
+
38
+ Compatible minor release. Existing calls and results are unchanged; new fields
39
+ have defaults.
40
+
41
+ - Add `modern_correspondents(folder)`, listing the senders (received) or
42
+ receivers (sent) the modern mailbox can be filtered by.
43
+ - Filter `modern_messages_page()` and `modern_messages()` by `correspondent=`
44
+ and, for received messages, `unread_only=True`, using the web app's own
45
+ query filters. Pages, collections and cursors record the filter; a cursor
46
+ never continues a differently filtered list. The archive mailbox does not
47
+ accept filters.
48
+ - Add `modern_teacher_subjects()`, pairing teachers' modern account IDs with
49
+ the subjects they teach the student.
50
+
51
+ ## 1.1.0 (2026-10-06) - Modern archive mailbox and unread counts
52
+
53
+ Compatible minor release. Existing calls and results are unchanged; new fields
54
+ have defaults.
55
+
56
+ - List the modern archive mailbox (earlier school years) with
57
+ `modern_messages_page(..., archived=True)` and `modern_messages(...,
58
+ archived=True)`. Pages, collections, cursors and message references gain an
59
+ `archived` field (default `False`), and a cursor never continues the other
60
+ mailbox. Opening an archived message raises `UNSUPPORTED_CAPABILITY` until an
61
+ archived content route is established.
62
+ - Add `modern_unread_counts()`, returning `ModernUnreadCounts` with current and
63
+ archive `ModernUnreadFolders` counters (inbox, notes, alerts, substitutions,
64
+ absences, justifications, trash) from the modern mailbox's own counter route.
65
+ It opens no message.
66
+
3
67
  ## 1.0.3 (2026-10-06) - Notification counts for parent logins
4
68
 
5
69
  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.3
3
+ Version: 1.3.0
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
@@ -26,6 +26,8 @@ Description-Content-Type: text/markdown
26
26
 
27
27
  # librus-python-api
28
28
 
29
+ [![Weekly live check](https://github.com/krzysztofbury/librus-python-api/actions/workflows/live-check.yml/badge.svg?branch=main)](https://github.com/krzysztofbury/librus-python-api/actions/workflows/live-check.yml)
30
+
29
31
  Read grades, attendance, homework, timetables and messages from **Librus Synergia**
30
32
  in Python. This independent, asynchronous client handles login, session recovery,
31
33
  pagination and request limits, returning typed Python objects rather than HTML.
@@ -40,9 +42,10 @@ private ACLs. Windows installations include `pywin32` and `tzdata` automatically
40
42
  Network paths, reparse-point paths and unsafe storage permissions fail closed.
41
43
  See [platform requirements](contracts/persistence.md#windows-disk-boundary-25).
42
44
 
43
- **Status:** `1.0.x` freezes the documented native library API. It does not claim
44
- completed MCP cutover, production-state migration or universal live-school
45
- compatibility. Those consumer/live gates remain separate. School features depend
45
+ **Status:** `1.3.0` adds a read-only school-year archive.
46
+ Installed live checks passed for populated and empty
47
+ archives on two parent logins; see [verification](VERIFICATION.md).
48
+ The stable 1.x API does not guarantee every school layout. School features depend
46
49
  on what each account can access. See the [compatibility policy](API.md#compatibility-policy).
47
50
  See [limitations](#supported-features-and-limitations) below.
48
51
 
@@ -51,7 +54,7 @@ See [limitations](#supported-features-and-limitations) below.
51
54
  Install the exact release from PyPI after its gated publication completes:
52
55
 
53
56
  ```sh
54
- python -m pip install librus-python-api==1.0.3
57
+ python -m pip install librus-python-api==1.3.0
55
58
  ```
56
59
 
57
60
  Pin the exact version qualified by your application. Before publication, use a
@@ -67,7 +70,7 @@ python -m pip install .
67
70
  To install a locally built wheel instead:
68
71
 
69
72
  ```sh
70
- python -m pip install ./dist/librus_python_api-1.0.3-py3-none-any.whl
73
+ python -m pip install ./dist/librus_python_api-1.3.0-py3-none-any.whl
71
74
  ```
72
75
 
73
76
  No CLI or background process is installed: import the library in your own program.
@@ -148,6 +151,11 @@ attendance = await client.attendance_window(today.replace(day=1), today)
148
151
 
149
152
  # Permit reuse of this account's cached result for up to 60 seconds.
150
153
  announcements = await client.announcements(max_age_seconds=60)
154
+
155
+ # Earlier school years, separate from the message archive (1.3.0).
156
+ archive = await client.school_year_archive()
157
+ for year in archive.years:
158
+ print(year.school_year, year.subjects)
151
159
  ```
152
160
 
153
161
  Collections expose named record tuples, for example `homework.items`, rather than
@@ -233,7 +241,7 @@ removed in 0.7.
233
241
 
234
242
  | Area | Available |
235
243
  | --- | --- |
236
- | School data | Profile, grades, attendance, timetable, announcements, agenda, homework and completed lessons |
244
+ | School data | Profile, grades, school-year archive, attendance, timetable, announcements, agenda, homework and completed lessons |
237
245
  | Communication | Legacy/modern message lists and content, recipient discovery, bounded attachment streams and explicit sending |
238
246
  | Application workflows | Shared multi-account limits, caching, notification checkpoints, optional durable send/notification stores |
239
247
 
@@ -1,5 +1,7 @@
1
1
  # librus-python-api
2
2
 
3
+ [![Weekly live check](https://github.com/krzysztofbury/librus-python-api/actions/workflows/live-check.yml/badge.svg?branch=main)](https://github.com/krzysztofbury/librus-python-api/actions/workflows/live-check.yml)
4
+
3
5
  Read grades, attendance, homework, timetables and messages from **Librus Synergia**
4
6
  in Python. This independent, asynchronous client handles login, session recovery,
5
7
  pagination and request limits, returning typed Python objects rather than HTML.
@@ -14,9 +16,10 @@ private ACLs. Windows installations include `pywin32` and `tzdata` automatically
14
16
  Network paths, reparse-point paths and unsafe storage permissions fail closed.
15
17
  See [platform requirements](contracts/persistence.md#windows-disk-boundary-25).
16
18
 
17
- **Status:** `1.0.x` freezes the documented native library API. It does not claim
18
- completed MCP cutover, production-state migration or universal live-school
19
- compatibility. Those consumer/live gates remain separate. School features depend
19
+ **Status:** `1.3.0` adds a read-only school-year archive.
20
+ Installed live checks passed for populated and empty
21
+ archives on two parent logins; see [verification](VERIFICATION.md).
22
+ The stable 1.x API does not guarantee every school layout. School features depend
20
23
  on what each account can access. See the [compatibility policy](API.md#compatibility-policy).
21
24
  See [limitations](#supported-features-and-limitations) below.
22
25
 
@@ -25,7 +28,7 @@ See [limitations](#supported-features-and-limitations) below.
25
28
  Install the exact release from PyPI after its gated publication completes:
26
29
 
27
30
  ```sh
28
- python -m pip install librus-python-api==1.0.3
31
+ python -m pip install librus-python-api==1.3.0
29
32
  ```
30
33
 
31
34
  Pin the exact version qualified by your application. Before publication, use a
@@ -41,7 +44,7 @@ python -m pip install .
41
44
  To install a locally built wheel instead:
42
45
 
43
46
  ```sh
44
- python -m pip install ./dist/librus_python_api-1.0.3-py3-none-any.whl
47
+ python -m pip install ./dist/librus_python_api-1.3.0-py3-none-any.whl
45
48
  ```
46
49
 
47
50
  No CLI or background process is installed: import the library in your own program.
@@ -122,6 +125,11 @@ attendance = await client.attendance_window(today.replace(day=1), today)
122
125
 
123
126
  # Permit reuse of this account's cached result for up to 60 seconds.
124
127
  announcements = await client.announcements(max_age_seconds=60)
128
+
129
+ # Earlier school years, separate from the message archive (1.3.0).
130
+ archive = await client.school_year_archive()
131
+ for year in archive.years:
132
+ print(year.school_year, year.subjects)
125
133
  ```
126
134
 
127
135
  Collections expose named record tuples, for example `homework.items`, rather than
@@ -207,7 +215,7 @@ removed in 0.7.
207
215
 
208
216
  | Area | Available |
209
217
  | --- | --- |
210
- | School data | Profile, grades, attendance, timetable, announcements, agenda, homework and completed lessons |
218
+ | School data | Profile, grades, school-year archive, attendance, timetable, announcements, agenda, homework and completed lessons |
211
219
  | Communication | Legacy/modern message lists and content, recipient discovery, bounded attachment streams and explicit sending |
212
220
  | Application workflows | Shared multi-account limits, caching, notification checkpoints, optional durable send/notification stores |
213
221
 
@@ -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 = [