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.
@@ -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.0.2"
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",
@@ -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
- "notification_counts",
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, page: int, page_size: int
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
- return {"page": str(page), "limit": str(page_size)}
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 {"modern_messages_received", "modern_messages_sent"}:
894
- if set(query) != {"page", "limit"} or any(
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
- MessageFolder.RECEIVED, int(query["page"]), int(query["limit"])
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 = [
@@ -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(data: Any, folder: MessageFolder, account: str) -> ModernMessageSummary:
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(folder, identifier(data.get("messageId")), account),
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, folder: MessageFolder, page: int, page_size: int, account: str
114
- ) -> tuple[tuple[ModernMessageSummary, ...], int, str]:
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
- if data.get("archivingInProgress", False) is not False:
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(folder, 1, page_size)
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(folder, cursor, page_size, max_pages, limit, account)
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)
@@ -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, folder: MessageFolder, page: int, page_size: int, budget: RequestBudget
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: Literal["modern_messages_received", "modern_messages_sent"] = (
595
- "modern_messages_received"
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(folder, page, page_size),
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
- operation: Literal["modern_messages_received", "modern_messages_sent"] = (
631
- "modern_messages_received"
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(folder, page, page_size, budget)
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), fetch, budget, max_age_seconds
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, cursor, page_size, max_pages, limit, self._alias
658
- )
659
- operation: Literal["modern_messages_received", "modern_messages_sent"] = (
660
- "modern_messages_received"
661
- if folder is MessageFolder.RECEIVED
662
- else "modern_messages_sent"
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
- lambda page: self._modern_message_page(folder, page, page_size, budget),
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
- (operation, "batch", cursor, page_size, max_pages, limit),
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
- "notification_counts", budget, parse_notification_counts
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",), fetch, budget, max_age_seconds
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()
@@ -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
- "modern_school_recipients",
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.0.2
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.0.2
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.0.2-py3-none-any.whl
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=I8X5pBHeufBLGrfKdr38wG2Gh7o4HJH3dn8ARU1BfuU,5657
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=VL6FHsKh_R9Sp4pTUXgWo2_UciXV7wwtlJd3-TkoOpA,49136
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=aGPst3eV44w4YB-0HbmskumDz-KAHekTf6J6_CHaRbE,33826
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=nRrmIHb7RfV49xjUTBlDcgtxOdmXEpp4F1L0IvcGQxw,16118
29
- librus_python_api/modern_messages.py,sha256=9gysqjUkQGtMKmnCL7boRzNDnAhtL5ibAJ2rkNZWCec,9694
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=tPDO0MlbhD6j0PHIZAHGpAVPvCMq2bLLK1HXGR7QQdc,86428
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=6S5wwBiRkUfKmi6w2t8msxlTSzCo-IbIBE13FeBc7Jw,35357
45
- librus_python_api-1.0.2.dist-info/METADATA,sha256=unP0aaTk4Vg3SZEaHT03KmG-IS3LFX_luQapUFrMn8g,11546
46
- librus_python_api-1.0.2.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
47
- librus_python_api-1.0.2.dist-info/licenses/LICENSE,sha256=IuerLb3cnZCsaB23znydjjvKqP-LmSJd3syKa9NGUts,1071
48
- librus_python_api-1.0.2.dist-info/RECORD,,
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,,