librus-python-api 1.0.3__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.
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/API.md +37 -5
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/CHANGELOG.md +44 -0
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/PKG-INFO +3 -3
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/README.md +2 -2
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/__init__.py +15 -1
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/config.py +122 -5
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/models.py +80 -0
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/modern_mailbox.py +137 -9
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/modern_messages.py +23 -0
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/service.py +218 -25
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/transport.py +3 -6
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/.gitignore +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/LICENSE +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/SECURITY.md +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/pyproject.toml +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/_notification_bootstrap.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/_notification_codec.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/_storage.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/_windows_filesystem.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/announcements.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/attachment_routes.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/attachments.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/attendance.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/attendance_frequency.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/budget.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/checkpoint.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/completed_lessons.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/detail_fields.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/diagnostics.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/exceptions.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/files.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/grade_parsers.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/grade_records.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/homework_range.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/lifecycle.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/markup.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/message_content.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/messages.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/modern_body.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/notification_models.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/notification_persistence.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/notification_workflow.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/notifications.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/parsers.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/parsing.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/persistence.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/py.typed +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/recipients.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/scheduler.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/school_reads.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/sending.py +0 -0
- {librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/timetable.py +0 -0
- {librus_python_api-1.0.3 → 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,
|
|
403
|
-
max_age_seconds=0)` returns `ModernMessagesPage`. Pages are
|
|
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
|
|
407
|
-
truncation, duplicate counts and
|
|
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,49 @@
|
|
|
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
|
+
|
|
3
47
|
## 1.0.3 (2026-10-06) - Notification counts for parent logins
|
|
4
48
|
|
|
5
49
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|
|
@@ -878,17 +932,67 @@ def modern_directory_query(
|
|
|
878
932
|
|
|
879
933
|
|
|
880
934
|
def modern_mailbox_query(
|
|
881
|
-
folder: MessageFolder,
|
|
935
|
+
folder: MessageFolder,
|
|
936
|
+
page: int,
|
|
937
|
+
page_size: int,
|
|
938
|
+
*,
|
|
939
|
+
correspondent: str | None = None,
|
|
940
|
+
unread_only: bool = False,
|
|
882
941
|
) -> dict[str, str]:
|
|
942
|
+
"""Page selection plus the web app's own sender/receiver/unread filters."""
|
|
883
943
|
if (
|
|
884
944
|
not isinstance(folder, MessageFolder)
|
|
885
945
|
or type(page) is not int
|
|
886
946
|
or not 1 <= page <= 1000
|
|
887
947
|
or type(page_size) is not int
|
|
888
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
|
+
)
|
|
889
958
|
):
|
|
890
959
|
raise LibrusError(ErrorKind.INVALID_INPUT)
|
|
891
|
-
|
|
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
|
|
892
996
|
|
|
893
997
|
|
|
894
998
|
def validate_modern_query(operation: str, query: Mapping[str, str]) -> None:
|
|
@@ -897,14 +1001,27 @@ def validate_modern_query(operation: str, query: Mapping[str, str]) -> None:
|
|
|
897
1001
|
type(v) is not str for v in query.values()
|
|
898
1002
|
):
|
|
899
1003
|
raise LibrusError(ErrorKind.INVALID_INPUT)
|
|
900
|
-
if operation in
|
|
901
|
-
|
|
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(
|
|
902
1007
|
re.fullmatch(r"[1-9][0-9]{0,3}", query[name]) is None
|
|
903
1008
|
for name in ("page", "limit")
|
|
904
1009
|
):
|
|
905
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)
|
|
906
1019
|
modern_mailbox_query(
|
|
907
|
-
|
|
1020
|
+
folder,
|
|
1021
|
+
int(query["page"]),
|
|
1022
|
+
int(query["limit"]),
|
|
1023
|
+
correspondent=query.get(MODERN_CORRESPONDENT_FILTERS[folder]),
|
|
1024
|
+
unread_only=unread == "1",
|
|
908
1025
|
)
|
|
909
1026
|
return
|
|
910
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)
|
|
@@ -14,11 +14,17 @@ from librus_python_api.config import (
|
|
|
14
14
|
MESSAGE_MAX_CURSOR_IDS,
|
|
15
15
|
MESSAGE_MAX_FIELD_LENGTH,
|
|
16
16
|
MESSAGE_MAX_TOTAL_TEXT_LENGTH,
|
|
17
|
+
MODERN_MAX_LABEL,
|
|
18
|
+
MODERN_MAX_RECIPIENTS,
|
|
19
|
+
MODERN_MAX_UNREAD_COUNT,
|
|
20
|
+
MODERN_UNREAD_COUNT_FIELDS,
|
|
17
21
|
modern_mailbox_query,
|
|
18
22
|
)
|
|
19
23
|
from librus_python_api.exceptions import ErrorKind, LibrusError
|
|
20
24
|
from librus_python_api.models import (
|
|
21
25
|
MessageFolder,
|
|
26
|
+
ModernCorrespondent,
|
|
27
|
+
ModernCorrespondentReference,
|
|
22
28
|
ModernMessageAttachment,
|
|
23
29
|
ModernMessageAttachmentReference,
|
|
24
30
|
ModernMessageRecipientReceipt,
|
|
@@ -26,6 +32,7 @@ from librus_python_api.models import (
|
|
|
26
32
|
ModernMessagesCursor,
|
|
27
33
|
ModernMessagesPage,
|
|
28
34
|
ModernMessageSummary,
|
|
35
|
+
ModernUnreadFolders,
|
|
29
36
|
)
|
|
30
37
|
from librus_python_api.modern_body import render_body
|
|
31
38
|
from librus_python_api.parsers import decode_json
|
|
@@ -48,6 +55,7 @@ def validate_reference(reference: ModernMessageReference, account: str) -> None:
|
|
|
48
55
|
not isinstance(reference, ModernMessageReference)
|
|
49
56
|
or not isinstance(reference.folder, MessageFolder)
|
|
50
57
|
or reference.account != account
|
|
58
|
+
or type(reference.archived) is not bool
|
|
51
59
|
or type(reference.identifier) is not str
|
|
52
60
|
or re.fullmatch(r"[0-9]{1,64}", reference.identifier) is None
|
|
53
61
|
):
|
|
@@ -75,7 +83,9 @@ def _date(value: Any) -> datetime:
|
|
|
75
83
|
raise LibrusError(ErrorKind.PARSE)
|
|
76
84
|
|
|
77
85
|
|
|
78
|
-
def _summary(
|
|
86
|
+
def _summary(
|
|
87
|
+
data: Any, folder: MessageFolder, account: str, archived: bool
|
|
88
|
+
) -> ModernMessageSummary:
|
|
79
89
|
if not isinstance(data, dict):
|
|
80
90
|
raise LibrusError(ErrorKind.PARSE)
|
|
81
91
|
correspondent = (
|
|
@@ -98,7 +108,9 @@ def _summary(data: Any, folder: MessageFolder, account: str) -> ModernMessageSum
|
|
|
98
108
|
read = data.get("readDate")
|
|
99
109
|
read_at = None if read in (None, "") else _date(read)
|
|
100
110
|
return ModernMessageSummary(
|
|
101
|
-
ModernMessageReference(
|
|
111
|
+
ModernMessageReference(
|
|
112
|
+
folder, identifier(data.get("messageId")), account, archived
|
|
113
|
+
),
|
|
102
114
|
name,
|
|
103
115
|
subject,
|
|
104
116
|
_date(stamp),
|
|
@@ -110,13 +122,23 @@ def _summary(data: Any, folder: MessageFolder, account: str) -> ModernMessageSum
|
|
|
110
122
|
|
|
111
123
|
|
|
112
124
|
def parse_page(
|
|
113
|
-
body: bytes,
|
|
114
|
-
|
|
125
|
+
body: bytes,
|
|
126
|
+
folder: MessageFolder,
|
|
127
|
+
page: int,
|
|
128
|
+
page_size: int,
|
|
129
|
+
account: str,
|
|
130
|
+
archived: bool = False,
|
|
131
|
+
) -> tuple[tuple[ModernMessageSummary, ...], int, str, bool | None]:
|
|
115
132
|
modern_mailbox_query(folder, page, page_size)
|
|
116
133
|
data = decode_json(body)
|
|
117
134
|
if not isinstance(data, dict) or not {"data", "total"} <= set(data):
|
|
118
135
|
raise LibrusError(ErrorKind.PARSE)
|
|
119
|
-
|
|
136
|
+
in_progress = data.get("archivingInProgress", False)
|
|
137
|
+
if type(in_progress) is not bool:
|
|
138
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
139
|
+
# Live archive pages report true while listing normally; it is a status
|
|
140
|
+
# flag there. The current mailbox has never shown it.
|
|
141
|
+
if in_progress and not archived:
|
|
120
142
|
raise LibrusError(ErrorKind.UNSUPPORTED_CAPABILITY)
|
|
121
143
|
entries, total = data["data"], data["total"]
|
|
122
144
|
if (
|
|
@@ -130,7 +152,7 @@ def parse_page(
|
|
|
130
152
|
expected = min(page_size, max(0, total - (page - 1) * page_size))
|
|
131
153
|
if len(entries) != expected:
|
|
132
154
|
raise LibrusError(ErrorKind.PARSE)
|
|
133
|
-
items = tuple(_summary(item, folder, account) for item in entries)
|
|
155
|
+
items = tuple(_summary(item, folder, account, archived) for item in entries)
|
|
134
156
|
if len({item.reference.identifier for item in items}) != len(items):
|
|
135
157
|
raise LibrusError(ErrorKind.PARSE)
|
|
136
158
|
if (
|
|
@@ -155,7 +177,7 @@ def parse_page(
|
|
|
155
177
|
)
|
|
156
178
|
).encode()
|
|
157
179
|
).hexdigest()
|
|
158
|
-
return items, total, fingerprint
|
|
180
|
+
return items, total, fingerprint, in_progress if archived else None
|
|
159
181
|
|
|
160
182
|
|
|
161
183
|
@dataclass(frozen=True, slots=True, repr=False)
|
|
@@ -280,6 +302,7 @@ def parse_content(body: bytes, reference: ModernMessageReference) -> ParsedConte
|
|
|
280
302
|
content | {"isAnyFileAttached": has_attachment},
|
|
281
303
|
reference.folder,
|
|
282
304
|
reference.account,
|
|
305
|
+
reference.archived,
|
|
283
306
|
)
|
|
284
307
|
if summary.reference != reference:
|
|
285
308
|
raise LibrusError(ErrorKind.PARSE)
|
|
@@ -326,8 +349,15 @@ def validate_selection(
|
|
|
326
349
|
max_pages: int,
|
|
327
350
|
limit: int,
|
|
328
351
|
account: str,
|
|
352
|
+
archived: bool = False,
|
|
353
|
+
correspondent: str | None = None,
|
|
354
|
+
unread_only: bool = False,
|
|
329
355
|
) -> None:
|
|
330
|
-
modern_mailbox_query(
|
|
356
|
+
modern_mailbox_query(
|
|
357
|
+
folder, 1, page_size, correspondent=correspondent, unread_only=unread_only
|
|
358
|
+
)
|
|
359
|
+
if type(archived) is not bool:
|
|
360
|
+
raise LibrusError(ErrorKind.INVALID_INPUT)
|
|
331
361
|
if type(max_pages) is not int or not 1 <= max_pages <= MESSAGE_MAX_BATCH_PAGES:
|
|
332
362
|
raise LibrusError(ErrorKind.INVALID_INPUT)
|
|
333
363
|
if type(limit) is not int or not 1 <= limit <= MESSAGE_MAX_BATCH_ITEMS:
|
|
@@ -340,6 +370,9 @@ def validate_selection(
|
|
|
340
370
|
if (
|
|
341
371
|
cursor.account != account
|
|
342
372
|
or cursor.folder is not folder
|
|
373
|
+
or cursor.archived is not archived
|
|
374
|
+
or cursor.correspondent != correspondent
|
|
375
|
+
or cursor.unread_only is not unread_only
|
|
343
376
|
or cursor.page_size != page_size
|
|
344
377
|
or type(cursor.offset) is not int
|
|
345
378
|
or not 0 <= cursor.offset < page_size
|
|
@@ -367,6 +400,9 @@ async def collect(
|
|
|
367
400
|
page_size: int,
|
|
368
401
|
max_pages: int,
|
|
369
402
|
limit: int,
|
|
403
|
+
archived: bool = False,
|
|
404
|
+
correspondent: str | None = None,
|
|
405
|
+
unread_only: bool = False,
|
|
370
406
|
) -> tuple[
|
|
371
407
|
tuple[ModernMessageSummary, ...],
|
|
372
408
|
int,
|
|
@@ -374,7 +410,17 @@ async def collect(
|
|
|
374
410
|
ModernMessagesCursor | None,
|
|
375
411
|
Literal["item_limit", "page_limit"] | None,
|
|
376
412
|
]:
|
|
377
|
-
validate_selection(
|
|
413
|
+
validate_selection(
|
|
414
|
+
folder,
|
|
415
|
+
cursor,
|
|
416
|
+
page_size,
|
|
417
|
+
max_pages,
|
|
418
|
+
limit,
|
|
419
|
+
account,
|
|
420
|
+
archived,
|
|
421
|
+
correspondent,
|
|
422
|
+
unread_only,
|
|
423
|
+
)
|
|
378
424
|
page = cursor.page if cursor else 1
|
|
379
425
|
offset = cursor.offset if cursor else 0
|
|
380
426
|
seen = list(cursor.seen_ids) if cursor else []
|
|
@@ -427,6 +473,9 @@ async def collect(
|
|
|
427
473
|
total,
|
|
428
474
|
result.fingerprint,
|
|
429
475
|
tuple(seen),
|
|
476
|
+
archived,
|
|
477
|
+
correspondent,
|
|
478
|
+
unread_only,
|
|
430
479
|
),
|
|
431
480
|
"item_limit",
|
|
432
481
|
)
|
|
@@ -452,8 +501,87 @@ async def collect(
|
|
|
452
501
|
total,
|
|
453
502
|
result.fingerprint,
|
|
454
503
|
tuple(seen),
|
|
504
|
+
archived,
|
|
505
|
+
correspondent,
|
|
506
|
+
unread_only,
|
|
455
507
|
),
|
|
456
508
|
"item_limit" if len(items) >= limit else "page_limit",
|
|
457
509
|
)
|
|
458
510
|
page, offset = page + 1, 0
|
|
459
511
|
raise AssertionError("Bounded collector must terminate")
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def _unread_folders(data: dict[str, Any], prefix: str) -> ModernUnreadFolders:
|
|
515
|
+
values = []
|
|
516
|
+
for name in MODERN_UNREAD_COUNT_FIELDS:
|
|
517
|
+
key = prefix + name[0].upper() + name[1:] if prefix else name
|
|
518
|
+
value = data.get(key)
|
|
519
|
+
if type(value) is not int or value < 0:
|
|
520
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
521
|
+
if value > MODERN_MAX_UNREAD_COUNT:
|
|
522
|
+
raise LibrusError(ErrorKind.LIMIT)
|
|
523
|
+
values.append(value)
|
|
524
|
+
return ModernUnreadFolders(*values)
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def parse_unread_counts(
|
|
528
|
+
body: bytes,
|
|
529
|
+
) -> tuple[ModernUnreadFolders, ModernUnreadFolders]:
|
|
530
|
+
"""Current and archive counters; unknown extra counters stay inert."""
|
|
531
|
+
data = decode_json(body)
|
|
532
|
+
counts = data.get("data") if isinstance(data, dict) else None
|
|
533
|
+
if not isinstance(counts, dict):
|
|
534
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
535
|
+
return _unread_folders(counts, ""), _unread_folders(counts, "archive")
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def validate_correspondent(
|
|
539
|
+
reference: ModernCorrespondentReference | None, folder: MessageFolder, account: str
|
|
540
|
+
) -> str | None:
|
|
541
|
+
"""The filter value for a correspondent listed for this folder and login."""
|
|
542
|
+
if reference is None:
|
|
543
|
+
return None
|
|
544
|
+
if (
|
|
545
|
+
not isinstance(reference, ModernCorrespondentReference)
|
|
546
|
+
or reference.folder is not folder
|
|
547
|
+
or reference.account != account
|
|
548
|
+
or type(reference.identifier) is not str
|
|
549
|
+
or re.fullmatch(r"[0-9]{1,64}", reference.identifier) is None
|
|
550
|
+
):
|
|
551
|
+
raise LibrusError(ErrorKind.INVALID_INPUT)
|
|
552
|
+
return reference.identifier
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def _name(value: Any) -> str:
|
|
556
|
+
if type(value) is not str or len(value) > MODERN_MAX_LABEL or "\x00" in value:
|
|
557
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
558
|
+
return value
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def parse_correspondents(
|
|
562
|
+
body: bytes, folder: MessageFolder, account: str
|
|
563
|
+
) -> tuple[ModernCorrespondent, ...]:
|
|
564
|
+
"""People the folder's messages came from (received) or went to (sent)."""
|
|
565
|
+
role = "sender" if folder is MessageFolder.RECEIVED else "receiver"
|
|
566
|
+
data = decode_json(body)
|
|
567
|
+
entries = data.get("data") if isinstance(data, dict) else None
|
|
568
|
+
if not isinstance(entries, list):
|
|
569
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
570
|
+
if len(entries) > MODERN_MAX_RECIPIENTS:
|
|
571
|
+
raise LibrusError(ErrorKind.LIMIT)
|
|
572
|
+
items = []
|
|
573
|
+
for entry in entries:
|
|
574
|
+
if not isinstance(entry, dict):
|
|
575
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
576
|
+
first = _name(entry.get(f"{role}FirstName"))
|
|
577
|
+
last = _name(entry.get(f"{role}LastName"))
|
|
578
|
+
# Institutional senders can carry a single name part; never both empty.
|
|
579
|
+
if not (first.strip() or last.strip()):
|
|
580
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
581
|
+
reference = ModernCorrespondentReference(
|
|
582
|
+
folder, identifier(entry.get(f"{role}Id")), account
|
|
583
|
+
)
|
|
584
|
+
items.append(ModernCorrespondent(reference, first, last))
|
|
585
|
+
if len({item.reference.identifier for item in items}) != len(items):
|
|
586
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
587
|
+
return tuple(items)
|
{librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/modern_messages.py
RENAMED
|
@@ -23,6 +23,7 @@ from librus_python_api.models import (
|
|
|
23
23
|
ModernRecipientReference,
|
|
24
24
|
ModernRecipientType,
|
|
25
25
|
ModernRecipientTypeReference,
|
|
26
|
+
ModernTeacherSubject,
|
|
26
27
|
SendStatus,
|
|
27
28
|
)
|
|
28
29
|
from librus_python_api.parsers import decode_json
|
|
@@ -274,3 +275,25 @@ def parse_modern_send_response(body: bytes, status: int) -> SendStatus:
|
|
|
274
275
|
):
|
|
275
276
|
return SendStatus.REJECTED
|
|
276
277
|
raise LibrusError(ErrorKind.UNKNOWN_DELIVERY)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def parse_teacher_subjects(body: bytes) -> tuple[ModernTeacherSubject, ...]:
|
|
281
|
+
"""Teacher account IDs paired with the subjects they teach the student."""
|
|
282
|
+
entries = _object(decode_json(body)).get("data")
|
|
283
|
+
if not isinstance(entries, list):
|
|
284
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
285
|
+
if len(entries) > MODERN_MAX_RECIPIENTS:
|
|
286
|
+
raise LibrusError(ErrorKind.LIMIT)
|
|
287
|
+
items = []
|
|
288
|
+
for entry in entries:
|
|
289
|
+
value = _object(entry)
|
|
290
|
+
raw = value.get("teacherIdentifier")
|
|
291
|
+
if type(raw) is int and 0 < raw < 10**64:
|
|
292
|
+
raw = str(raw)
|
|
293
|
+
item = ModernTeacherSubject(_identifier(raw), _label(value.get("subject")))
|
|
294
|
+
# Live lists can repeat an identical pair; the copy adds nothing.
|
|
295
|
+
if item not in items:
|
|
296
|
+
items.append(item)
|
|
297
|
+
if sum(len(item.subject) for item in items) > MODERN_MAX_TOTAL_TEXT:
|
|
298
|
+
raise LibrusError(ErrorKind.LIMIT)
|
|
299
|
+
return tuple(items)
|
|
@@ -107,6 +107,8 @@ from librus_python_api.models import (
|
|
|
107
107
|
MessagesPage,
|
|
108
108
|
MessageSummary,
|
|
109
109
|
ModernAccountData,
|
|
110
|
+
ModernCorrespondentReference,
|
|
111
|
+
ModernCorrespondents,
|
|
110
112
|
ModernIdentity,
|
|
111
113
|
ModernMessageAttachmentReference,
|
|
112
114
|
ModernMessageContent,
|
|
@@ -119,6 +121,8 @@ from librus_python_api.models import (
|
|
|
119
121
|
ModernRecipientTypeReference,
|
|
120
122
|
ModernRecipientTypes,
|
|
121
123
|
ModernSendSubmission,
|
|
124
|
+
ModernTeacherSubjects,
|
|
125
|
+
ModernUnreadCounts,
|
|
122
126
|
NotificationCounts,
|
|
123
127
|
Observation,
|
|
124
128
|
OperationName,
|
|
@@ -146,7 +150,16 @@ from librus_python_api.modern_mailbox import collect as collect_modern_messages
|
|
|
146
150
|
from librus_python_api.modern_mailbox import (
|
|
147
151
|
parse_content as parse_modern_message_content,
|
|
148
152
|
)
|
|
153
|
+
from librus_python_api.modern_mailbox import (
|
|
154
|
+
parse_correspondents as parse_modern_correspondents,
|
|
155
|
+
)
|
|
149
156
|
from librus_python_api.modern_mailbox import parse_page as parse_modern_message_page
|
|
157
|
+
from librus_python_api.modern_mailbox import (
|
|
158
|
+
parse_unread_counts as parse_modern_unread_counts,
|
|
159
|
+
)
|
|
160
|
+
from librus_python_api.modern_mailbox import (
|
|
161
|
+
validate_correspondent as validate_modern_correspondent,
|
|
162
|
+
)
|
|
150
163
|
from librus_python_api.modern_mailbox import (
|
|
151
164
|
validate_reference as validate_modern_message_reference,
|
|
152
165
|
)
|
|
@@ -160,6 +173,9 @@ from librus_python_api.modern_messages import (
|
|
|
160
173
|
parse_modern_types,
|
|
161
174
|
validate_modern_type,
|
|
162
175
|
)
|
|
176
|
+
from librus_python_api.modern_messages import (
|
|
177
|
+
parse_teacher_subjects as parse_modern_teacher_subjects,
|
|
178
|
+
)
|
|
163
179
|
from librus_python_api.notifications import (
|
|
164
180
|
decode_payload,
|
|
165
181
|
parse_notification_counts,
|
|
@@ -590,21 +606,32 @@ class AccountClient:
|
|
|
590
606
|
)
|
|
591
607
|
|
|
592
608
|
async def _modern_message_page(
|
|
593
|
-
self,
|
|
609
|
+
self,
|
|
610
|
+
folder: MessageFolder,
|
|
611
|
+
page: int,
|
|
612
|
+
page_size: int,
|
|
613
|
+
budget: RequestBudget,
|
|
614
|
+
archived: bool,
|
|
615
|
+
correspondent: ModernCorrespondentReference | None = None,
|
|
616
|
+
unread_only: bool = False,
|
|
594
617
|
) -> ModernMessagesPage:
|
|
595
|
-
operation
|
|
596
|
-
|
|
597
|
-
if folder is MessageFolder.RECEIVED
|
|
598
|
-
else "modern_messages_sent"
|
|
599
|
-
)
|
|
600
|
-
items, total, fingerprint = await self._page(
|
|
618
|
+
operation = _modern_mailbox_operation(folder, archived)
|
|
619
|
+
items, total, fingerprint, in_progress = await self._page(
|
|
601
620
|
operation,
|
|
602
621
|
budget,
|
|
603
622
|
lambda body: parse_modern_message_page(
|
|
604
|
-
body, folder, page, page_size, self._alias
|
|
623
|
+
body, folder, page, page_size, self._alias, archived
|
|
605
624
|
),
|
|
606
625
|
content_type=JSON,
|
|
607
|
-
query=modern_mailbox_query(
|
|
626
|
+
query=modern_mailbox_query(
|
|
627
|
+
folder,
|
|
628
|
+
page,
|
|
629
|
+
page_size,
|
|
630
|
+
correspondent=None
|
|
631
|
+
if correspondent is None
|
|
632
|
+
else correspondent.identifier,
|
|
633
|
+
unread_only=unread_only,
|
|
634
|
+
),
|
|
608
635
|
)
|
|
609
636
|
return ModernMessagesPage(
|
|
610
637
|
self._session_identity(),
|
|
@@ -615,31 +642,62 @@ class AccountClient:
|
|
|
615
642
|
items,
|
|
616
643
|
fingerprint,
|
|
617
644
|
self._observation(operation),
|
|
645
|
+
archived,
|
|
646
|
+
correspondent,
|
|
647
|
+
unread_only,
|
|
648
|
+
in_progress,
|
|
618
649
|
)
|
|
619
650
|
|
|
651
|
+
def _modern_filter(
|
|
652
|
+
self,
|
|
653
|
+
folder: MessageFolder,
|
|
654
|
+
archived: bool,
|
|
655
|
+
correspondent: ModernCorrespondentReference | None,
|
|
656
|
+
unread_only: bool,
|
|
657
|
+
) -> str | None:
|
|
658
|
+
"""Validate list filters; they are observed for the current mailbox only."""
|
|
659
|
+
value = validate_modern_correspondent(correspondent, folder, self._alias)
|
|
660
|
+
if type(archived) is not bool or type(unread_only) is not bool:
|
|
661
|
+
raise LibrusError(ErrorKind.INVALID_INPUT)
|
|
662
|
+
if unread_only and folder is not MessageFolder.RECEIVED:
|
|
663
|
+
raise LibrusError(ErrorKind.INVALID_INPUT)
|
|
664
|
+
if archived and (value is not None or unread_only):
|
|
665
|
+
raise LibrusError(ErrorKind.UNSUPPORTED_CAPABILITY)
|
|
666
|
+
return value
|
|
667
|
+
|
|
620
668
|
async def modern_messages_page(
|
|
621
669
|
self,
|
|
622
670
|
folder: MessageFolder = MessageFolder.RECEIVED,
|
|
623
671
|
*,
|
|
624
672
|
page: int = 1,
|
|
625
673
|
page_size: int = 10,
|
|
674
|
+
archived: bool = False,
|
|
675
|
+
correspondent: ModernCorrespondentReference | None = None,
|
|
676
|
+
unread_only: bool = False,
|
|
626
677
|
budget: RequestBudget | None = None,
|
|
627
678
|
max_age_seconds: float = 0.0,
|
|
628
679
|
) -> ModernMessagesPage:
|
|
629
|
-
"""One one-based ordinary modern page, without any content open.
|
|
680
|
+
"""One one-based ordinary modern page, without any content open.
|
|
681
|
+
|
|
682
|
+
``archived`` lists the archive mailbox (earlier school years) instead.
|
|
683
|
+
``correspondent`` (from ``modern_correspondents``) and ``unread_only``
|
|
684
|
+
(received folder) filter the current mailbox as the web app does.
|
|
685
|
+
"""
|
|
630
686
|
modern_mailbox_query(folder, page, page_size)
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
if folder is MessageFolder.RECEIVED
|
|
634
|
-
else "modern_messages_sent"
|
|
635
|
-
)
|
|
687
|
+
filter_id = self._modern_filter(folder, archived, correspondent, unread_only)
|
|
688
|
+
operation = _modern_mailbox_operation(folder, archived)
|
|
636
689
|
|
|
637
690
|
async def fetch(budget: RequestBudget, _: bool) -> ModernMessagesPage:
|
|
638
691
|
await self._modern_ready(budget)
|
|
639
|
-
return await self._modern_message_page(
|
|
692
|
+
return await self._modern_message_page(
|
|
693
|
+
folder, page, page_size, budget, archived, correspondent, unread_only
|
|
694
|
+
)
|
|
640
695
|
|
|
641
696
|
return await self._read(
|
|
642
|
-
(operation, "page", page, page_size
|
|
697
|
+
(operation, "page", page, page_size, filter_id, unread_only),
|
|
698
|
+
fetch,
|
|
699
|
+
budget,
|
|
700
|
+
max_age_seconds,
|
|
643
701
|
)
|
|
644
702
|
|
|
645
703
|
async def modern_messages(
|
|
@@ -650,29 +708,55 @@ class AccountClient:
|
|
|
650
708
|
page_size: int = 50,
|
|
651
709
|
max_pages: int = 4,
|
|
652
710
|
limit: int = 128,
|
|
711
|
+
archived: bool = False,
|
|
712
|
+
correspondent: ModernCorrespondentReference | None = None,
|
|
713
|
+
unread_only: bool = False,
|
|
653
714
|
budget: RequestBudget | None = None,
|
|
654
715
|
max_age_seconds: float = 0.0,
|
|
655
716
|
) -> ModernMessages:
|
|
656
717
|
"""Bounded modern collection with selection-bound continuation."""
|
|
718
|
+
filter_id = self._modern_filter(folder, archived, correspondent, unread_only)
|
|
657
719
|
validate_modern_message_selection(
|
|
658
|
-
folder,
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
720
|
+
folder,
|
|
721
|
+
cursor,
|
|
722
|
+
page_size,
|
|
723
|
+
max_pages,
|
|
724
|
+
limit,
|
|
725
|
+
self._alias,
|
|
726
|
+
archived,
|
|
727
|
+
filter_id,
|
|
728
|
+
unread_only,
|
|
664
729
|
)
|
|
730
|
+
operation = _modern_mailbox_operation(folder, archived)
|
|
665
731
|
|
|
666
732
|
async def fetch(budget: RequestBudget, _: bool) -> ModernMessages:
|
|
667
733
|
await self._modern_ready(budget)
|
|
734
|
+
flags: list[bool | None] = []
|
|
735
|
+
|
|
736
|
+
async def page_of(page: int) -> ModernMessagesPage:
|
|
737
|
+
result = await self._modern_message_page(
|
|
738
|
+
folder,
|
|
739
|
+
page,
|
|
740
|
+
page_size,
|
|
741
|
+
budget,
|
|
742
|
+
archived,
|
|
743
|
+
correspondent,
|
|
744
|
+
unread_only,
|
|
745
|
+
)
|
|
746
|
+
flags.append(result.archiving_in_progress)
|
|
747
|
+
return result
|
|
748
|
+
|
|
668
749
|
result = await collect_modern_messages(
|
|
669
|
-
|
|
750
|
+
page_of,
|
|
670
751
|
folder,
|
|
671
752
|
self._alias,
|
|
672
753
|
cursor,
|
|
673
754
|
page_size,
|
|
674
755
|
max_pages,
|
|
675
756
|
limit,
|
|
757
|
+
archived,
|
|
758
|
+
filter_id,
|
|
759
|
+
unread_only,
|
|
676
760
|
)
|
|
677
761
|
items, pages, duplicates, continuation, reason = result
|
|
678
762
|
return ModernMessages(
|
|
@@ -684,15 +768,106 @@ class AccountClient:
|
|
|
684
768
|
continuation,
|
|
685
769
|
reason,
|
|
686
770
|
self._observation(operation),
|
|
771
|
+
archived,
|
|
772
|
+
correspondent,
|
|
773
|
+
unread_only,
|
|
774
|
+
any(flags) if archived else None,
|
|
687
775
|
)
|
|
688
776
|
|
|
689
777
|
return await self._read(
|
|
690
|
-
(
|
|
778
|
+
(
|
|
779
|
+
operation,
|
|
780
|
+
"batch",
|
|
781
|
+
cursor,
|
|
782
|
+
page_size,
|
|
783
|
+
max_pages,
|
|
784
|
+
limit,
|
|
785
|
+
filter_id,
|
|
786
|
+
unread_only,
|
|
787
|
+
),
|
|
691
788
|
fetch,
|
|
692
789
|
budget,
|
|
693
790
|
max_age_seconds,
|
|
694
791
|
)
|
|
695
792
|
|
|
793
|
+
async def modern_correspondents(
|
|
794
|
+
self,
|
|
795
|
+
folder: MessageFolder = MessageFolder.RECEIVED,
|
|
796
|
+
*,
|
|
797
|
+
budget: RequestBudget | None = None,
|
|
798
|
+
max_age_seconds: float = 0.0,
|
|
799
|
+
) -> ModernCorrespondents:
|
|
800
|
+
"""Senders (received) or receivers (sent) the current mailbox can filter by."""
|
|
801
|
+
if not isinstance(folder, MessageFolder):
|
|
802
|
+
raise LibrusError(ErrorKind.INVALID_INPUT)
|
|
803
|
+
operation: Literal["modern_senders", "modern_receivers"] = (
|
|
804
|
+
"modern_senders" if folder is MessageFolder.RECEIVED else "modern_receivers"
|
|
805
|
+
)
|
|
806
|
+
|
|
807
|
+
async def fetch(budget: RequestBudget, _: bool) -> ModernCorrespondents:
|
|
808
|
+
await self._modern_ready(budget)
|
|
809
|
+
items = await self._page(
|
|
810
|
+
operation,
|
|
811
|
+
budget,
|
|
812
|
+
lambda body: parse_modern_correspondents(body, folder, self._alias),
|
|
813
|
+
content_type=JSON,
|
|
814
|
+
)
|
|
815
|
+
return ModernCorrespondents(
|
|
816
|
+
self._session_identity(),
|
|
817
|
+
folder,
|
|
818
|
+
items,
|
|
819
|
+
self._observation(operation),
|
|
820
|
+
)
|
|
821
|
+
|
|
822
|
+
return await self._read((operation,), fetch, budget, max_age_seconds)
|
|
823
|
+
|
|
824
|
+
async def modern_teacher_subjects(
|
|
825
|
+
self, *, budget: RequestBudget | None = None, max_age_seconds: float = 0.0
|
|
826
|
+
) -> ModernTeacherSubjects:
|
|
827
|
+
"""Teachers' modern account IDs with the subjects they teach the student."""
|
|
828
|
+
|
|
829
|
+
async def fetch(budget: RequestBudget, _: bool) -> ModernTeacherSubjects:
|
|
830
|
+
await self._modern_ready(budget)
|
|
831
|
+
items = await self._page(
|
|
832
|
+
"modern_teacher_subjects",
|
|
833
|
+
budget,
|
|
834
|
+
parse_modern_teacher_subjects,
|
|
835
|
+
content_type=JSON,
|
|
836
|
+
)
|
|
837
|
+
return ModernTeacherSubjects(
|
|
838
|
+
self._session_identity(),
|
|
839
|
+
items,
|
|
840
|
+
self._observation("modern_teacher_subjects"),
|
|
841
|
+
)
|
|
842
|
+
|
|
843
|
+
return await self._read(
|
|
844
|
+
("modern_teacher_subjects",), fetch, budget, max_age_seconds
|
|
845
|
+
)
|
|
846
|
+
|
|
847
|
+
async def modern_unread_counts(
|
|
848
|
+
self, *, budget: RequestBudget | None = None, max_age_seconds: float = 0.0
|
|
849
|
+
) -> ModernUnreadCounts:
|
|
850
|
+
"""Unread counters of the modern mailbox, without opening any message."""
|
|
851
|
+
|
|
852
|
+
async def fetch(budget: RequestBudget, _: bool) -> ModernUnreadCounts:
|
|
853
|
+
await self._modern_ready(budget)
|
|
854
|
+
current, archive = await self._page(
|
|
855
|
+
"modern_unread_counts",
|
|
856
|
+
budget,
|
|
857
|
+
parse_modern_unread_counts,
|
|
858
|
+
content_type=JSON,
|
|
859
|
+
)
|
|
860
|
+
return ModernUnreadCounts(
|
|
861
|
+
self._session_identity(),
|
|
862
|
+
current,
|
|
863
|
+
archive,
|
|
864
|
+
self._observation("modern_unread_counts"),
|
|
865
|
+
)
|
|
866
|
+
|
|
867
|
+
return await self._read(
|
|
868
|
+
("modern_unread_counts",), fetch, budget, max_age_seconds
|
|
869
|
+
)
|
|
870
|
+
|
|
696
871
|
async def modern_message_content(
|
|
697
872
|
self,
|
|
698
873
|
reference: ModernMessageReference,
|
|
@@ -706,6 +881,9 @@ class AccountClient:
|
|
|
706
881
|
reference.folder is MessageFolder.RECEIVED and not allow_mark_read
|
|
707
882
|
):
|
|
708
883
|
raise LibrusError(ErrorKind.INVALID_INPUT)
|
|
884
|
+
if reference.archived:
|
|
885
|
+
# No archived content route or mark-read effect is established yet.
|
|
886
|
+
raise LibrusError(ErrorKind.UNSUPPORTED_CAPABILITY)
|
|
709
887
|
operation: Literal["modern_content_received", "modern_content_sent"] = (
|
|
710
888
|
"modern_content_received"
|
|
711
889
|
if reference.folder is MessageFolder.RECEIVED
|
|
@@ -2336,5 +2514,20 @@ class AccountClient:
|
|
|
2336
2514
|
return response
|
|
2337
2515
|
|
|
2338
2516
|
|
|
2517
|
+
def _modern_mailbox_operation(
|
|
2518
|
+
folder: MessageFolder, archived: bool
|
|
2519
|
+
) -> Literal[
|
|
2520
|
+
"modern_messages_received",
|
|
2521
|
+
"modern_messages_sent",
|
|
2522
|
+
"modern_archive_messages_received",
|
|
2523
|
+
"modern_archive_messages_sent",
|
|
2524
|
+
]:
|
|
2525
|
+
if folder is MessageFolder.RECEIVED:
|
|
2526
|
+
if archived:
|
|
2527
|
+
return "modern_archive_messages_received"
|
|
2528
|
+
return "modern_messages_received"
|
|
2529
|
+
return "modern_archive_messages_sent" if archived else "modern_messages_sent"
|
|
2530
|
+
|
|
2531
|
+
|
|
2339
2532
|
def _media_type(response: TransportResponse) -> str:
|
|
2340
2533
|
return response.headers.get("content-type", "").partition(";")[0].strip().lower()
|
|
@@ -34,6 +34,7 @@ from librus_python_api.config import (
|
|
|
34
34
|
MESSAGE_PAGE_FIELDS,
|
|
35
35
|
MODERN_DIRECTORY_QUERIES,
|
|
36
36
|
MODERN_HANDOFF_PATTERN,
|
|
37
|
+
MODERN_MAILBOX_OPERATIONS,
|
|
37
38
|
MODERN_TERMINAL_PATHS,
|
|
38
39
|
OAUTH_QUERY,
|
|
39
40
|
USER_AGENT,
|
|
@@ -297,12 +298,8 @@ class AiohttpTransport:
|
|
|
297
298
|
url = str(URL(url).with_query(MODERN_DIRECTORY_QUERIES[endpoint_id]))
|
|
298
299
|
if (
|
|
299
300
|
endpoint_id
|
|
300
|
-
in {
|
|
301
|
-
|
|
302
|
-
"modern_class_parents",
|
|
303
|
-
"modern_messages_received",
|
|
304
|
-
"modern_messages_sent",
|
|
305
|
-
}
|
|
301
|
+
in {"modern_school_recipients", "modern_class_parents"}
|
|
302
|
+
| MODERN_MAILBOX_OPERATIONS
|
|
306
303
|
and query is None
|
|
307
304
|
):
|
|
308
305
|
raise LibrusError(ErrorKind.INVALID_INPUT)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/_notification_bootstrap.py
RENAMED
|
File without changes
|
{librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/_notification_codec.py
RENAMED
|
File without changes
|
|
File without changes
|
{librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/_windows_filesystem.py
RENAMED
|
File without changes
|
|
File without changes
|
{librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/attachment_routes.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/attendance_frequency.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/completed_lessons.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/message_content.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/notification_models.py
RENAMED
|
File without changes
|
|
File without changes
|
{librus_python_api-1.0.3 → librus_python_api-1.2.1}/src/librus_python_api/notification_workflow.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|