librus-python-api 1.0.3__py3-none-any.whl → 1.3.0__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 +31 -1
- librus_python_api/config.py +159 -5
- librus_python_api/models.py +154 -0
- librus_python_api/modern_mailbox.py +137 -9
- librus_python_api/modern_messages.py +23 -0
- librus_python_api/school_year_archive.py +432 -0
- librus_python_api/service.py +244 -25
- librus_python_api/transport.py +3 -6
- {librus_python_api-1.0.3.dist-info → librus_python_api-1.3.0.dist-info}/METADATA +15 -7
- {librus_python_api-1.0.3.dist-info → librus_python_api-1.3.0.dist-info}/RECORD +12 -11
- {librus_python_api-1.0.3.dist-info → librus_python_api-1.3.0.dist-info}/WHEEL +0 -0
- {librus_python_api-1.0.3.dist-info → librus_python_api-1.3.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -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)
|