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.
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/API.md +76 -6
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/CHANGELOG.md +64 -0
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/PKG-INFO +15 -7
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/README.md +14 -6
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/__init__.py +31 -1
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/config.py +159 -5
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/models.py +154 -0
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/modern_mailbox.py +137 -9
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/modern_messages.py +23 -0
- librus_python_api-1.3.0/src/librus_python_api/school_year_archive.py +432 -0
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/service.py +244 -25
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/transport.py +3 -6
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/uv.lock +57 -57
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/.gitignore +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/LICENSE +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/SECURITY.md +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/pyproject.toml +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/_notification_bootstrap.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/_notification_codec.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/_storage.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/_windows_filesystem.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/announcements.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/attachment_routes.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/attachments.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/attendance.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/attendance_frequency.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/budget.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/checkpoint.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/completed_lessons.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/detail_fields.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/diagnostics.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/exceptions.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/files.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/grade_parsers.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/grade_records.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/homework_range.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/lifecycle.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/markup.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/message_content.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/messages.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/modern_body.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/notification_models.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/notification_persistence.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/notification_workflow.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/notifications.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/parsers.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/parsing.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/persistence.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/py.typed +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/recipients.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/scheduler.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/school_reads.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.3.0}/src/librus_python_api/sending.py +0 -0
- {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
|
-
|
|
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,
|
|
403
|
-
max_age_seconds=0)` returns `ModernMessagesPage`. Pages are
|
|
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
|
|
407
|
-
truncation, duplicate counts and
|
|
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
|
+
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
|
+
[](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
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
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
|
|
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
|
+
[](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
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
901
|
-
|
|
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
|
-
|
|
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 = [
|