librus-python-api 1.0.2__py3-none-any.whl → 1.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- librus_python_api/__init__.py +15 -1
- librus_python_api/config.py +130 -6
- librus_python_api/models.py +80 -0
- librus_python_api/modern_mailbox.py +137 -9
- librus_python_api/modern_messages.py +23 -0
- librus_python_api/service.py +225 -27
- librus_python_api/transport.py +3 -6
- {librus_python_api-1.0.2.dist-info → librus_python_api-1.2.1.dist-info}/METADATA +3 -3
- {librus_python_api-1.0.2.dist-info → librus_python_api-1.2.1.dist-info}/RECORD +11 -11
- {librus_python_api-1.0.2.dist-info → librus_python_api-1.2.1.dist-info}/WHEEL +0 -0
- {librus_python_api-1.0.2.dist-info → librus_python_api-1.2.1.dist-info}/licenses/LICENSE +0 -0
librus_python_api/__init__.py
CHANGED
|
@@ -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",
|
librus_python_api/config.py
CHANGED
|
@@ -207,6 +207,60 @@ ENDPOINTS: Mapping[str, Endpoint] = MappingProxyType(
|
|
|
207
207
|
Evidence.INDEPENDENTLY_OBSERVED,
|
|
208
208
|
"messages",
|
|
209
209
|
),
|
|
210
|
+
Endpoint(
|
|
211
|
+
"modern_archive_messages_received",
|
|
212
|
+
"GET",
|
|
213
|
+
"/api/archive/inbox/messages",
|
|
214
|
+
SideEffect.NONE,
|
|
215
|
+
False,
|
|
216
|
+
Evidence.INDEPENDENTLY_OBSERVED,
|
|
217
|
+
"messages",
|
|
218
|
+
),
|
|
219
|
+
Endpoint(
|
|
220
|
+
"modern_archive_messages_sent",
|
|
221
|
+
"GET",
|
|
222
|
+
"/api/archive/outbox/messages",
|
|
223
|
+
SideEffect.NONE,
|
|
224
|
+
False,
|
|
225
|
+
Evidence.INDEPENDENTLY_OBSERVED,
|
|
226
|
+
"messages",
|
|
227
|
+
),
|
|
228
|
+
Endpoint(
|
|
229
|
+
"modern_unread_counts",
|
|
230
|
+
"GET",
|
|
231
|
+
"/api/inbox/unreadMessagesCount",
|
|
232
|
+
SideEffect.NONE,
|
|
233
|
+
False,
|
|
234
|
+
Evidence.INDEPENDENTLY_OBSERVED,
|
|
235
|
+
"messages",
|
|
236
|
+
),
|
|
237
|
+
Endpoint(
|
|
238
|
+
"modern_senders",
|
|
239
|
+
"GET",
|
|
240
|
+
"/api/inbox/messages/senders",
|
|
241
|
+
SideEffect.NONE,
|
|
242
|
+
False,
|
|
243
|
+
Evidence.INDEPENDENTLY_OBSERVED,
|
|
244
|
+
"messages",
|
|
245
|
+
),
|
|
246
|
+
Endpoint(
|
|
247
|
+
"modern_receivers",
|
|
248
|
+
"GET",
|
|
249
|
+
"/api/outbox/messages/receivers",
|
|
250
|
+
SideEffect.NONE,
|
|
251
|
+
False,
|
|
252
|
+
Evidence.INDEPENDENTLY_OBSERVED,
|
|
253
|
+
"messages",
|
|
254
|
+
),
|
|
255
|
+
Endpoint(
|
|
256
|
+
"modern_teacher_subjects",
|
|
257
|
+
"GET",
|
|
258
|
+
"/api/receivers/student-subjects",
|
|
259
|
+
SideEffect.NONE,
|
|
260
|
+
False,
|
|
261
|
+
Evidence.INDEPENDENTLY_OBSERVED,
|
|
262
|
+
"messages",
|
|
263
|
+
),
|
|
210
264
|
Endpoint(
|
|
211
265
|
"modern_content_received",
|
|
212
266
|
"GET",
|
|
@@ -337,8 +391,10 @@ ENDPOINTS: Mapping[str, Endpoint] = MappingProxyType(
|
|
|
337
391
|
False,
|
|
338
392
|
Evidence.SYNTHETIC_ONLY,
|
|
339
393
|
),
|
|
394
|
+
# Accepted only as a redirect target during login. Never requested
|
|
395
|
+
# directly: it answers parent logins with 403.
|
|
340
396
|
Endpoint(
|
|
341
|
-
"
|
|
397
|
+
"login_student_landing",
|
|
342
398
|
"GET",
|
|
343
399
|
"/uczen/index",
|
|
344
400
|
SideEffect.AUTHENTICATION,
|
|
@@ -695,6 +751,10 @@ CHECKPOINT_TIMEOUT_SECONDS = 5.0
|
|
|
695
751
|
CHECKPOINT_MAX_TIMEOUT_SECONDS = 30.0
|
|
696
752
|
SCHEDULE_RESPONSE_VERSION = 1
|
|
697
753
|
SCHEDULE_MAX_EVENTS = 1024
|
|
754
|
+
# Every Synergia page carries the counter menu. The information page is
|
|
755
|
+
# readable by student and parent logins alike; the student landing route
|
|
756
|
+
# (/uczen/index) answers parent logins with 403 and is not used.
|
|
757
|
+
NOTIFICATION_COUNTS_ENDPOINT = "student_information"
|
|
698
758
|
NOTIFICATION_MAX_MENU_ITEMS = 64
|
|
699
759
|
NOTIFICATION_MAX_COUNT = 1000000
|
|
700
760
|
NOTIFICATION_DESTINATIONS = MappingProxyType(
|
|
@@ -702,6 +762,7 @@ NOTIFICATION_DESTINATIONS = MappingProxyType(
|
|
|
702
762
|
"/przegladaj_oceny/uczen": "grades",
|
|
703
763
|
"/przegladaj_nb/uczen": "attendance",
|
|
704
764
|
"/wiadomosci": "messages",
|
|
765
|
+
"/wiadomosci3": "messages",
|
|
705
766
|
"/ogloszenia": "announcements",
|
|
706
767
|
"/terminarz": "agenda",
|
|
707
768
|
"/moje_zadania": "homework",
|
|
@@ -871,17 +932,67 @@ def modern_directory_query(
|
|
|
871
932
|
|
|
872
933
|
|
|
873
934
|
def modern_mailbox_query(
|
|
874
|
-
folder: MessageFolder,
|
|
935
|
+
folder: MessageFolder,
|
|
936
|
+
page: int,
|
|
937
|
+
page_size: int,
|
|
938
|
+
*,
|
|
939
|
+
correspondent: str | None = None,
|
|
940
|
+
unread_only: bool = False,
|
|
875
941
|
) -> dict[str, str]:
|
|
942
|
+
"""Page selection plus the web app's own sender/receiver/unread filters."""
|
|
876
943
|
if (
|
|
877
944
|
not isinstance(folder, MessageFolder)
|
|
878
945
|
or type(page) is not int
|
|
879
946
|
or not 1 <= page <= 1000
|
|
880
947
|
or type(page_size) is not int
|
|
881
948
|
or not 1 <= page_size <= 50
|
|
949
|
+
or type(unread_only) is not bool
|
|
950
|
+
or (unread_only and folder is not MessageFolder.RECEIVED)
|
|
951
|
+
or (
|
|
952
|
+
correspondent is not None
|
|
953
|
+
and (
|
|
954
|
+
type(correspondent) is not str
|
|
955
|
+
or re.fullmatch(r"[0-9]{1,64}", correspondent) is None
|
|
956
|
+
)
|
|
957
|
+
)
|
|
882
958
|
):
|
|
883
959
|
raise LibrusError(ErrorKind.INVALID_INPUT)
|
|
884
|
-
|
|
960
|
+
query = {}
|
|
961
|
+
if correspondent is not None:
|
|
962
|
+
query[MODERN_CORRESPONDENT_FILTERS[folder]] = correspondent
|
|
963
|
+
if unread_only:
|
|
964
|
+
query["unreadOnly"] = "1"
|
|
965
|
+
return query | {"page": str(page), "limit": str(page_size)}
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
MODERN_MAILBOX_OPERATIONS = frozenset(
|
|
969
|
+
{
|
|
970
|
+
"modern_messages_received",
|
|
971
|
+
"modern_messages_sent",
|
|
972
|
+
"modern_archive_messages_received",
|
|
973
|
+
"modern_archive_messages_sent",
|
|
974
|
+
}
|
|
975
|
+
)
|
|
976
|
+
MODERN_CORRESPONDENT_FILTERS = MappingProxyType(
|
|
977
|
+
{MessageFolder.RECEIVED: "senderId", MessageFolder.SENT: "receiverId"}
|
|
978
|
+
)
|
|
979
|
+
# Filters observed in the web app for the current mailbox only.
|
|
980
|
+
MODERN_MAILBOX_FILTERS = MappingProxyType(
|
|
981
|
+
{
|
|
982
|
+
"modern_messages_received": frozenset({"senderId", "unreadOnly"}),
|
|
983
|
+
"modern_messages_sent": frozenset({"receiverId"}),
|
|
984
|
+
}
|
|
985
|
+
)
|
|
986
|
+
MODERN_UNREAD_COUNT_FIELDS = (
|
|
987
|
+
"inbox",
|
|
988
|
+
"notes",
|
|
989
|
+
"alerts",
|
|
990
|
+
"substitutions",
|
|
991
|
+
"absences",
|
|
992
|
+
"justifications",
|
|
993
|
+
"trash",
|
|
994
|
+
)
|
|
995
|
+
MODERN_MAX_UNREAD_COUNT = 1000000
|
|
885
996
|
|
|
886
997
|
|
|
887
998
|
def validate_modern_query(operation: str, query: Mapping[str, str]) -> None:
|
|
@@ -890,14 +1001,27 @@ def validate_modern_query(operation: str, query: Mapping[str, str]) -> None:
|
|
|
890
1001
|
type(v) is not str for v in query.values()
|
|
891
1002
|
):
|
|
892
1003
|
raise LibrusError(ErrorKind.INVALID_INPUT)
|
|
893
|
-
if operation in
|
|
894
|
-
|
|
1004
|
+
if operation in MODERN_MAILBOX_OPERATIONS:
|
|
1005
|
+
filters = MODERN_MAILBOX_FILTERS.get(operation, frozenset())
|
|
1006
|
+
if not {"page", "limit"} <= set(query) <= {"page", "limit"} | filters or any(
|
|
895
1007
|
re.fullmatch(r"[1-9][0-9]{0,3}", query[name]) is None
|
|
896
1008
|
for name in ("page", "limit")
|
|
897
1009
|
):
|
|
898
1010
|
raise LibrusError(ErrorKind.INVALID_INPUT)
|
|
1011
|
+
folder = (
|
|
1012
|
+
MessageFolder.SENT
|
|
1013
|
+
if operation == "modern_messages_sent"
|
|
1014
|
+
else MessageFolder.RECEIVED
|
|
1015
|
+
)
|
|
1016
|
+
unread = query.get("unreadOnly")
|
|
1017
|
+
if unread not in (None, "1"):
|
|
1018
|
+
raise LibrusError(ErrorKind.INVALID_INPUT)
|
|
899
1019
|
modern_mailbox_query(
|
|
900
|
-
|
|
1020
|
+
folder,
|
|
1021
|
+
int(query["page"]),
|
|
1022
|
+
int(query["limit"]),
|
|
1023
|
+
correspondent=query.get(MODERN_CORRESPONDENT_FILTERS[folder]),
|
|
1024
|
+
unread_only=unread == "1",
|
|
901
1025
|
)
|
|
902
1026
|
return
|
|
903
1027
|
allowed = [
|
librus_python_api/models.py
CHANGED
|
@@ -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)
|
|
@@ -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)
|
librus_python_api/service.py
CHANGED
|
@@ -42,6 +42,7 @@ from librus_python_api.config import (
|
|
|
42
42
|
GRADE_MAX_WINDOW_DAYS,
|
|
43
43
|
MESSAGE_MAX_CURSOR_IDS,
|
|
44
44
|
MODERN_RECIPIENT_OPERATIONS,
|
|
45
|
+
NOTIFICATION_COUNTS_ENDPOINT,
|
|
45
46
|
SCHEDULE_RESPONSE_VERSION,
|
|
46
47
|
SESSION_COOKIE,
|
|
47
48
|
AccountCredentials,
|
|
@@ -106,6 +107,8 @@ from librus_python_api.models import (
|
|
|
106
107
|
MessagesPage,
|
|
107
108
|
MessageSummary,
|
|
108
109
|
ModernAccountData,
|
|
110
|
+
ModernCorrespondentReference,
|
|
111
|
+
ModernCorrespondents,
|
|
109
112
|
ModernIdentity,
|
|
110
113
|
ModernMessageAttachmentReference,
|
|
111
114
|
ModernMessageContent,
|
|
@@ -118,6 +121,8 @@ from librus_python_api.models import (
|
|
|
118
121
|
ModernRecipientTypeReference,
|
|
119
122
|
ModernRecipientTypes,
|
|
120
123
|
ModernSendSubmission,
|
|
124
|
+
ModernTeacherSubjects,
|
|
125
|
+
ModernUnreadCounts,
|
|
121
126
|
NotificationCounts,
|
|
122
127
|
Observation,
|
|
123
128
|
OperationName,
|
|
@@ -145,7 +150,16 @@ from librus_python_api.modern_mailbox import collect as collect_modern_messages
|
|
|
145
150
|
from librus_python_api.modern_mailbox import (
|
|
146
151
|
parse_content as parse_modern_message_content,
|
|
147
152
|
)
|
|
153
|
+
from librus_python_api.modern_mailbox import (
|
|
154
|
+
parse_correspondents as parse_modern_correspondents,
|
|
155
|
+
)
|
|
148
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
|
+
)
|
|
149
163
|
from librus_python_api.modern_mailbox import (
|
|
150
164
|
validate_reference as validate_modern_message_reference,
|
|
151
165
|
)
|
|
@@ -159,6 +173,9 @@ from librus_python_api.modern_messages import (
|
|
|
159
173
|
parse_modern_types,
|
|
160
174
|
validate_modern_type,
|
|
161
175
|
)
|
|
176
|
+
from librus_python_api.modern_messages import (
|
|
177
|
+
parse_teacher_subjects as parse_modern_teacher_subjects,
|
|
178
|
+
)
|
|
162
179
|
from librus_python_api.notifications import (
|
|
163
180
|
decode_payload,
|
|
164
181
|
parse_notification_counts,
|
|
@@ -589,21 +606,32 @@ class AccountClient:
|
|
|
589
606
|
)
|
|
590
607
|
|
|
591
608
|
async def _modern_message_page(
|
|
592
|
-
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,
|
|
593
617
|
) -> ModernMessagesPage:
|
|
594
|
-
operation
|
|
595
|
-
|
|
596
|
-
if folder is MessageFolder.RECEIVED
|
|
597
|
-
else "modern_messages_sent"
|
|
598
|
-
)
|
|
599
|
-
items, total, fingerprint = await self._page(
|
|
618
|
+
operation = _modern_mailbox_operation(folder, archived)
|
|
619
|
+
items, total, fingerprint, in_progress = await self._page(
|
|
600
620
|
operation,
|
|
601
621
|
budget,
|
|
602
622
|
lambda body: parse_modern_message_page(
|
|
603
|
-
body, folder, page, page_size, self._alias
|
|
623
|
+
body, folder, page, page_size, self._alias, archived
|
|
604
624
|
),
|
|
605
625
|
content_type=JSON,
|
|
606
|
-
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
|
+
),
|
|
607
635
|
)
|
|
608
636
|
return ModernMessagesPage(
|
|
609
637
|
self._session_identity(),
|
|
@@ -614,31 +642,62 @@ class AccountClient:
|
|
|
614
642
|
items,
|
|
615
643
|
fingerprint,
|
|
616
644
|
self._observation(operation),
|
|
645
|
+
archived,
|
|
646
|
+
correspondent,
|
|
647
|
+
unread_only,
|
|
648
|
+
in_progress,
|
|
617
649
|
)
|
|
618
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
|
+
|
|
619
668
|
async def modern_messages_page(
|
|
620
669
|
self,
|
|
621
670
|
folder: MessageFolder = MessageFolder.RECEIVED,
|
|
622
671
|
*,
|
|
623
672
|
page: int = 1,
|
|
624
673
|
page_size: int = 10,
|
|
674
|
+
archived: bool = False,
|
|
675
|
+
correspondent: ModernCorrespondentReference | None = None,
|
|
676
|
+
unread_only: bool = False,
|
|
625
677
|
budget: RequestBudget | None = None,
|
|
626
678
|
max_age_seconds: float = 0.0,
|
|
627
679
|
) -> ModernMessagesPage:
|
|
628
|
-
"""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
|
+
"""
|
|
629
686
|
modern_mailbox_query(folder, page, page_size)
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
if folder is MessageFolder.RECEIVED
|
|
633
|
-
else "modern_messages_sent"
|
|
634
|
-
)
|
|
687
|
+
filter_id = self._modern_filter(folder, archived, correspondent, unread_only)
|
|
688
|
+
operation = _modern_mailbox_operation(folder, archived)
|
|
635
689
|
|
|
636
690
|
async def fetch(budget: RequestBudget, _: bool) -> ModernMessagesPage:
|
|
637
691
|
await self._modern_ready(budget)
|
|
638
|
-
return await self._modern_message_page(
|
|
692
|
+
return await self._modern_message_page(
|
|
693
|
+
folder, page, page_size, budget, archived, correspondent, unread_only
|
|
694
|
+
)
|
|
639
695
|
|
|
640
696
|
return await self._read(
|
|
641
|
-
(operation, "page", page, page_size
|
|
697
|
+
(operation, "page", page, page_size, filter_id, unread_only),
|
|
698
|
+
fetch,
|
|
699
|
+
budget,
|
|
700
|
+
max_age_seconds,
|
|
642
701
|
)
|
|
643
702
|
|
|
644
703
|
async def modern_messages(
|
|
@@ -649,29 +708,55 @@ class AccountClient:
|
|
|
649
708
|
page_size: int = 50,
|
|
650
709
|
max_pages: int = 4,
|
|
651
710
|
limit: int = 128,
|
|
711
|
+
archived: bool = False,
|
|
712
|
+
correspondent: ModernCorrespondentReference | None = None,
|
|
713
|
+
unread_only: bool = False,
|
|
652
714
|
budget: RequestBudget | None = None,
|
|
653
715
|
max_age_seconds: float = 0.0,
|
|
654
716
|
) -> ModernMessages:
|
|
655
717
|
"""Bounded modern collection with selection-bound continuation."""
|
|
718
|
+
filter_id = self._modern_filter(folder, archived, correspondent, unread_only)
|
|
656
719
|
validate_modern_message_selection(
|
|
657
|
-
folder,
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
720
|
+
folder,
|
|
721
|
+
cursor,
|
|
722
|
+
page_size,
|
|
723
|
+
max_pages,
|
|
724
|
+
limit,
|
|
725
|
+
self._alias,
|
|
726
|
+
archived,
|
|
727
|
+
filter_id,
|
|
728
|
+
unread_only,
|
|
663
729
|
)
|
|
730
|
+
operation = _modern_mailbox_operation(folder, archived)
|
|
664
731
|
|
|
665
732
|
async def fetch(budget: RequestBudget, _: bool) -> ModernMessages:
|
|
666
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
|
+
|
|
667
749
|
result = await collect_modern_messages(
|
|
668
|
-
|
|
750
|
+
page_of,
|
|
669
751
|
folder,
|
|
670
752
|
self._alias,
|
|
671
753
|
cursor,
|
|
672
754
|
page_size,
|
|
673
755
|
max_pages,
|
|
674
756
|
limit,
|
|
757
|
+
archived,
|
|
758
|
+
filter_id,
|
|
759
|
+
unread_only,
|
|
675
760
|
)
|
|
676
761
|
items, pages, duplicates, continuation, reason = result
|
|
677
762
|
return ModernMessages(
|
|
@@ -683,15 +768,106 @@ class AccountClient:
|
|
|
683
768
|
continuation,
|
|
684
769
|
reason,
|
|
685
770
|
self._observation(operation),
|
|
771
|
+
archived,
|
|
772
|
+
correspondent,
|
|
773
|
+
unread_only,
|
|
774
|
+
any(flags) if archived else None,
|
|
686
775
|
)
|
|
687
776
|
|
|
688
777
|
return await self._read(
|
|
689
|
-
(
|
|
778
|
+
(
|
|
779
|
+
operation,
|
|
780
|
+
"batch",
|
|
781
|
+
cursor,
|
|
782
|
+
page_size,
|
|
783
|
+
max_pages,
|
|
784
|
+
limit,
|
|
785
|
+
filter_id,
|
|
786
|
+
unread_only,
|
|
787
|
+
),
|
|
690
788
|
fetch,
|
|
691
789
|
budget,
|
|
692
790
|
max_age_seconds,
|
|
693
791
|
)
|
|
694
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
|
+
|
|
695
871
|
async def modern_message_content(
|
|
696
872
|
self,
|
|
697
873
|
reference: ModernMessageReference,
|
|
@@ -705,6 +881,9 @@ class AccountClient:
|
|
|
705
881
|
reference.folder is MessageFolder.RECEIVED and not allow_mark_read
|
|
706
882
|
):
|
|
707
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)
|
|
708
887
|
operation: Literal["modern_content_received", "modern_content_sent"] = (
|
|
709
888
|
"modern_content_received"
|
|
710
889
|
if reference.folder is MessageFolder.RECEIVED
|
|
@@ -816,7 +995,7 @@ class AccountClient:
|
|
|
816
995
|
|
|
817
996
|
async def fetch(budget: RequestBudget, _: bool) -> NotificationCounts:
|
|
818
997
|
items = await self._page(
|
|
819
|
-
|
|
998
|
+
NOTIFICATION_COUNTS_ENDPOINT, budget, parse_notification_counts
|
|
820
999
|
)
|
|
821
1000
|
return NotificationCounts(
|
|
822
1001
|
self._session_identity(),
|
|
@@ -825,7 +1004,11 @@ class AccountClient:
|
|
|
825
1004
|
)
|
|
826
1005
|
|
|
827
1006
|
return await self._read(
|
|
828
|
-
("notification_counts",),
|
|
1007
|
+
("notification_counts",),
|
|
1008
|
+
fetch,
|
|
1009
|
+
budget,
|
|
1010
|
+
max_age_seconds,
|
|
1011
|
+
endpoint=NOTIFICATION_COUNTS_ENDPOINT,
|
|
829
1012
|
)
|
|
830
1013
|
|
|
831
1014
|
async def consume_schedule_events(
|
|
@@ -2331,5 +2514,20 @@ class AccountClient:
|
|
|
2331
2514
|
return response
|
|
2332
2515
|
|
|
2333
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
|
+
|
|
2334
2532
|
def _media_type(response: TransportResponse) -> str:
|
|
2335
2533
|
return response.headers.get("content-type", "").partition(";")[0].strip().lower()
|
librus_python_api/transport.py
CHANGED
|
@@ -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)
|
|
@@ -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.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
librus_python_api/__init__.py,sha256=
|
|
1
|
+
librus_python_api/__init__.py,sha256=cFnpyZw2vPzTFefw3pbhiP3f-CvFABI4rnGdpLye-LQ,6045
|
|
2
2
|
librus_python_api/_notification_bootstrap.py,sha256=18nzH0doXBNaZXCz-_ckIvRrlHTWV5Mm1t5m2-5eYGI,4669
|
|
3
3
|
librus_python_api/_notification_codec.py,sha256=hU6G3zGJ5fR8wfPC6fV2OqBKs-7XpT9eRnKO3FGsTzY,13451
|
|
4
4
|
librus_python_api/_storage.py,sha256=M_6ZAzN4zs7laOdKJBRH1I2rvy3pgqYgC_MZtsd2xtM,15893
|
|
@@ -11,7 +11,7 @@ librus_python_api/attendance_frequency.py,sha256=SBhED5zuyRIHWdw4urFyTJYykb6EYB7
|
|
|
11
11
|
librus_python_api/budget.py,sha256=8fvqT7eIxyVX2P2OBG27XsYHk_HbtSS4lzuecYpHtgs,2878
|
|
12
12
|
librus_python_api/checkpoint.py,sha256=QBmG9GtpyaGRaFq-FXuGA3yR1BjuMFN9V3RTvvi_FNQ,1930
|
|
13
13
|
librus_python_api/completed_lessons.py,sha256=sR2oLSGb2q1580l-2upCBv9sQfIcHi7CQfWr1K0EFXI,7635
|
|
14
|
-
librus_python_api/config.py,sha256=
|
|
14
|
+
librus_python_api/config.py,sha256=43tAalNG8_Em7Q-lXs1nmX-GKw7333CKpNAvJtGMg4E,53193
|
|
15
15
|
librus_python_api/detail_fields.py,sha256=K10Stx_ehDTttkUI7-G7a2Ko9exYEu5NJpR-3sBnQ60,1789
|
|
16
16
|
librus_python_api/diagnostics.py,sha256=2l7wq6RA3it-Gwj-JRLcw5qbI37fl3xII6GZA2mZj0o,856
|
|
17
17
|
librus_python_api/exceptions.py,sha256=yWWoo0xyrQAk1d13HOJZnQBsq4ulh_P0hRSI6ZiNQOE,4622
|
|
@@ -23,10 +23,10 @@ librus_python_api/lifecycle.py,sha256=XzA5CEdZFo7iL5IMUrawnRvf2m9BFe0OseP-fpQFaA
|
|
|
23
23
|
librus_python_api/markup.py,sha256=GZk7MmBBGB5SIVY_VngmQxQErI3eGHOpJs3mFVlMR3M,5298
|
|
24
24
|
librus_python_api/message_content.py,sha256=sUPuqpjuavqDyn6QGM5aPH4mpoQV62bDLepSSQ2AsUE,8686
|
|
25
25
|
librus_python_api/messages.py,sha256=K9F8XVs1SojfFpdxxLV8gx9E0g81YxEHjigDzLHmDCg,10230
|
|
26
|
-
librus_python_api/models.py,sha256=
|
|
26
|
+
librus_python_api/models.py,sha256=vqoQAL1IMgnOHRlNWxchMiOdkw7AnYDnyiYEQ2OAHko,36363
|
|
27
27
|
librus_python_api/modern_body.py,sha256=3L4P_cIrHdIxZqiinFSQrDMhG6iklT1GoA-VEWMJUiI,2682
|
|
28
|
-
librus_python_api/modern_mailbox.py,sha256=
|
|
29
|
-
librus_python_api/modern_messages.py,sha256=
|
|
28
|
+
librus_python_api/modern_mailbox.py,sha256=2jF9UnJZ5AF6dip-GrMcv_z2kSLpp87dIpVeXk9jMzA,20600
|
|
29
|
+
librus_python_api/modern_messages.py,sha256=qt2CXtrxCZQ-jTt0aQCD6ZaPAJQZXf0BxmvacPo9B0w,10653
|
|
30
30
|
librus_python_api/notification_models.py,sha256=4eLTAE9JzHhveoaWtZQwGBolmb87QUP5kOSW7Jjt7J0,4199
|
|
31
31
|
librus_python_api/notification_persistence.py,sha256=hXKmzAh0yNYG_n5CLlZOcoYOZdQnlrSeNtjjXiDmLm4,49532
|
|
32
32
|
librus_python_api/notification_workflow.py,sha256=MsBJdwn6J1UUGlz3Ldzi7WEBz5Qsama7eGmZvUQj25g,12924
|
|
@@ -39,10 +39,10 @@ librus_python_api/recipients.py,sha256=6GQCKo-Zjg3VFSg1J8YXcDsy_U3r6-MUS4VCvz0xd
|
|
|
39
39
|
librus_python_api/scheduler.py,sha256=3JplYxqvQdFz3aKXcoQUJHE9s3FPw_xRhQ2CPNLuQmA,10729
|
|
40
40
|
librus_python_api/school_reads.py,sha256=PCnZ4lv44j-68FgJpQiMteaPSyfhbX8_3CUyE6zhFcQ,14760
|
|
41
41
|
librus_python_api/sending.py,sha256=Nvvk2r-6D6Us53a2fH5GW9PcA6bWEfuOvy6c3M5QVcE,4549
|
|
42
|
-
librus_python_api/service.py,sha256=
|
|
42
|
+
librus_python_api/service.py,sha256=1hQkDjEIlOwF5_4aSAEkmb1iMdUU51bVyPTVc7BAKJU,93252
|
|
43
43
|
librus_python_api/timetable.py,sha256=tX1QK9fQABxzfLaHHqTe8e6rAgdOkCoTOfAHdPwNmLE,9836
|
|
44
|
-
librus_python_api/transport.py,sha256=
|
|
45
|
-
librus_python_api-1.
|
|
46
|
-
librus_python_api-1.
|
|
47
|
-
librus_python_api-1.
|
|
48
|
-
librus_python_api-1.
|
|
44
|
+
librus_python_api/transport.py,sha256=QnYXnrv1lW4qToYoTv6HyZw-Lt4rDc-_SMTJpMp6_bc,35297
|
|
45
|
+
librus_python_api-1.2.1.dist-info/METADATA,sha256=2EBX-fUeSn5fa9MG7Bgda1npQo-xnBbcgzhuFvnilJ0,11546
|
|
46
|
+
librus_python_api-1.2.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
47
|
+
librus_python_api-1.2.1.dist-info/licenses/LICENSE,sha256=IuerLb3cnZCsaB23znydjjvKqP-LmSJd3syKa9NGUts,1071
|
|
48
|
+
librus_python_api-1.2.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|