librus-python-api 1.0.3__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.3"
130
+ __version__ = "1.2.1"
124
131
 
125
132
  __all__ = [
126
133
  "AccountContext",
@@ -135,6 +142,13 @@ __all__ = [
135
142
  "ModernMessageRecipientReceipt",
136
143
  "ModernMessages",
137
144
  "ModernMessagesCursor",
145
+ "ModernUnreadCounts",
146
+ "ModernCorrespondent",
147
+ "ModernCorrespondentReference",
148
+ "ModernCorrespondents",
149
+ "ModernTeacherSubject",
150
+ "ModernTeacherSubjects",
151
+ "ModernUnreadFolders",
138
152
  "ModernMessagesPage",
139
153
  "ModernMessageSummary",
140
154
  "ModernRecipient",
@@ -207,6 +207,60 @@ ENDPOINTS: Mapping[str, Endpoint] = MappingProxyType(
207
207
  Evidence.INDEPENDENTLY_OBSERVED,
208
208
  "messages",
209
209
  ),
210
+ Endpoint(
211
+ "modern_archive_messages_received",
212
+ "GET",
213
+ "/api/archive/inbox/messages",
214
+ SideEffect.NONE,
215
+ False,
216
+ Evidence.INDEPENDENTLY_OBSERVED,
217
+ "messages",
218
+ ),
219
+ Endpoint(
220
+ "modern_archive_messages_sent",
221
+ "GET",
222
+ "/api/archive/outbox/messages",
223
+ SideEffect.NONE,
224
+ False,
225
+ Evidence.INDEPENDENTLY_OBSERVED,
226
+ "messages",
227
+ ),
228
+ Endpoint(
229
+ "modern_unread_counts",
230
+ "GET",
231
+ "/api/inbox/unreadMessagesCount",
232
+ SideEffect.NONE,
233
+ False,
234
+ Evidence.INDEPENDENTLY_OBSERVED,
235
+ "messages",
236
+ ),
237
+ Endpoint(
238
+ "modern_senders",
239
+ "GET",
240
+ "/api/inbox/messages/senders",
241
+ SideEffect.NONE,
242
+ False,
243
+ Evidence.INDEPENDENTLY_OBSERVED,
244
+ "messages",
245
+ ),
246
+ Endpoint(
247
+ "modern_receivers",
248
+ "GET",
249
+ "/api/outbox/messages/receivers",
250
+ SideEffect.NONE,
251
+ False,
252
+ Evidence.INDEPENDENTLY_OBSERVED,
253
+ "messages",
254
+ ),
255
+ Endpoint(
256
+ "modern_teacher_subjects",
257
+ "GET",
258
+ "/api/receivers/student-subjects",
259
+ SideEffect.NONE,
260
+ False,
261
+ Evidence.INDEPENDENTLY_OBSERVED,
262
+ "messages",
263
+ ),
210
264
  Endpoint(
211
265
  "modern_content_received",
212
266
  "GET",
@@ -878,17 +932,67 @@ def modern_directory_query(
878
932
 
879
933
 
880
934
  def modern_mailbox_query(
881
- folder: MessageFolder, 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,
882
941
  ) -> dict[str, str]:
942
+ """Page selection plus the web app's own sender/receiver/unread filters."""
883
943
  if (
884
944
  not isinstance(folder, MessageFolder)
885
945
  or type(page) is not int
886
946
  or not 1 <= page <= 1000
887
947
  or type(page_size) is not int
888
948
  or not 1 <= page_size <= 50
949
+ or type(unread_only) is not bool
950
+ or (unread_only and folder is not MessageFolder.RECEIVED)
951
+ or (
952
+ correspondent is not None
953
+ and (
954
+ type(correspondent) is not str
955
+ or re.fullmatch(r"[0-9]{1,64}", correspondent) is None
956
+ )
957
+ )
889
958
  ):
890
959
  raise LibrusError(ErrorKind.INVALID_INPUT)
891
- 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
892
996
 
893
997
 
894
998
  def validate_modern_query(operation: str, query: Mapping[str, str]) -> None:
@@ -897,14 +1001,27 @@ def validate_modern_query(operation: str, query: Mapping[str, str]) -> None:
897
1001
  type(v) is not str for v in query.values()
898
1002
  ):
899
1003
  raise LibrusError(ErrorKind.INVALID_INPUT)
900
- if operation in {"modern_messages_received", "modern_messages_sent"}:
901
- 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(
902
1007
  re.fullmatch(r"[1-9][0-9]{0,3}", query[name]) is None
903
1008
  for name in ("page", "limit")
904
1009
  ):
905
1010
  raise LibrusError(ErrorKind.INVALID_INPUT)
1011
+ folder = (
1012
+ MessageFolder.SENT
1013
+ if operation == "modern_messages_sent"
1014
+ else MessageFolder.RECEIVED
1015
+ )
1016
+ unread = query.get("unreadOnly")
1017
+ if unread not in (None, "1"):
1018
+ raise LibrusError(ErrorKind.INVALID_INPUT)
906
1019
  modern_mailbox_query(
907
- 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",
908
1025
  )
909
1026
  return
910
1027
  allowed = [
@@ -48,6 +48,12 @@ type OperationName = Literal[
48
48
  "modern_send_message",
49
49
  "modern_school_recipients",
50
50
  "modern_class_parents",
51
+ "modern_archive_messages_received",
52
+ "modern_archive_messages_sent",
53
+ "modern_unread_counts",
54
+ "modern_senders",
55
+ "modern_receivers",
56
+ "modern_teacher_subjects",
51
57
  "modern_messages_received",
52
58
  "modern_messages_sent",
53
59
  "modern_content_received",
@@ -393,6 +399,46 @@ class ModernMessageReference:
393
399
  folder: MessageFolder
394
400
  identifier: str = field(repr=False)
395
401
  account: str = field(repr=False)
402
+ # Listed from the archive mailbox. Archived content opens are not supported.
403
+ archived: bool = False
404
+
405
+
406
+ @dataclass(frozen=True, slots=True)
407
+ class ModernCorrespondentReference:
408
+ """A sender (received folder) or receiver (sent folder) to filter a list by."""
409
+
410
+ folder: MessageFolder
411
+ identifier: str = field(repr=False)
412
+ account: str = field(repr=False)
413
+
414
+
415
+ @dataclass(frozen=True, slots=True)
416
+ class ModernCorrespondent:
417
+ reference: ModernCorrespondentReference = field(repr=False)
418
+ first_name: str = field(repr=False)
419
+ last_name: str = field(repr=False)
420
+
421
+
422
+ @dataclass(frozen=True, slots=True)
423
+ class ModernCorrespondents:
424
+ identity: "Identity" = field(repr=False)
425
+ folder: MessageFolder
426
+ items: tuple[ModernCorrespondent, ...] = field(repr=False)
427
+ observation: "Observation"
428
+
429
+
430
+ @dataclass(frozen=True, slots=True)
431
+ class ModernTeacherSubject:
432
+ # The teacher's modern account ID, as a decimal string.
433
+ teacher_identifier: str = field(repr=False)
434
+ subject: str = field(repr=False)
435
+
436
+
437
+ @dataclass(frozen=True, slots=True)
438
+ class ModernTeacherSubjects:
439
+ identity: "Identity" = field(repr=False)
440
+ items: tuple[ModernTeacherSubject, ...] = field(repr=False)
441
+ observation: "Observation"
396
442
 
397
443
 
398
444
  @dataclass(frozen=True, slots=True)
@@ -417,6 +463,11 @@ class ModernMessagesPage:
417
463
  items: tuple[ModernMessageSummary, ...] = field(repr=False)
418
464
  fingerprint: str = field(repr=False)
419
465
  observation: "Observation"
466
+ archived: bool = False
467
+ correspondent: ModernCorrespondentReference | None = field(default=None, repr=False)
468
+ unread_only: bool = False
469
+ # Archive listings only: the upstream archivingInProgress status flag.
470
+ archiving_in_progress: bool | None = None
420
471
 
421
472
 
422
473
  @dataclass(frozen=True, slots=True)
@@ -429,6 +480,9 @@ class ModernMessagesCursor:
429
480
  total_count: int
430
481
  fingerprint: str = field(repr=False)
431
482
  seen_ids: tuple[str, ...] = field(repr=False)
483
+ archived: bool = False
484
+ correspondent: str | None = field(default=None, repr=False)
485
+ unread_only: bool = False
432
486
 
433
487
 
434
488
  @dataclass(frozen=True, slots=True)
@@ -441,6 +495,32 @@ class ModernMessages:
441
495
  next_cursor: ModernMessagesCursor | None = field(repr=False)
442
496
  truncation_reason: Literal["item_limit", "page_limit"] | None
443
497
  observation: "Observation"
498
+ archived: bool = False
499
+ correspondent: ModernCorrespondentReference | None = field(default=None, repr=False)
500
+ unread_only: bool = False
501
+ # Archive listings only: the upstream archivingInProgress status flag.
502
+ archiving_in_progress: bool | None = None
503
+
504
+
505
+ @dataclass(frozen=True, slots=True)
506
+ class ModernUnreadFolders:
507
+ """Unread counters the modern mailbox shows for one mailbox section."""
508
+
509
+ inbox: int
510
+ notes: int
511
+ alerts: int
512
+ substitutions: int
513
+ absences: int
514
+ justifications: int
515
+ trash: int
516
+
517
+
518
+ @dataclass(frozen=True, slots=True)
519
+ class ModernUnreadCounts:
520
+ identity: "Identity" = field(repr=False)
521
+ current: ModernUnreadFolders
522
+ archive: ModernUnreadFolders
523
+ observation: "Observation"
444
524
 
445
525
 
446
526
  @dataclass(frozen=True, slots=True)
@@ -14,11 +14,17 @@ from librus_python_api.config import (
14
14
  MESSAGE_MAX_CURSOR_IDS,
15
15
  MESSAGE_MAX_FIELD_LENGTH,
16
16
  MESSAGE_MAX_TOTAL_TEXT_LENGTH,
17
+ MODERN_MAX_LABEL,
18
+ MODERN_MAX_RECIPIENTS,
19
+ MODERN_MAX_UNREAD_COUNT,
20
+ MODERN_UNREAD_COUNT_FIELDS,
17
21
  modern_mailbox_query,
18
22
  )
19
23
  from librus_python_api.exceptions import ErrorKind, LibrusError
20
24
  from librus_python_api.models import (
21
25
  MessageFolder,
26
+ ModernCorrespondent,
27
+ ModernCorrespondentReference,
22
28
  ModernMessageAttachment,
23
29
  ModernMessageAttachmentReference,
24
30
  ModernMessageRecipientReceipt,
@@ -26,6 +32,7 @@ from librus_python_api.models import (
26
32
  ModernMessagesCursor,
27
33
  ModernMessagesPage,
28
34
  ModernMessageSummary,
35
+ ModernUnreadFolders,
29
36
  )
30
37
  from librus_python_api.modern_body import render_body
31
38
  from librus_python_api.parsers import decode_json
@@ -48,6 +55,7 @@ def validate_reference(reference: ModernMessageReference, account: str) -> None:
48
55
  not isinstance(reference, ModernMessageReference)
49
56
  or not isinstance(reference.folder, MessageFolder)
50
57
  or reference.account != account
58
+ or type(reference.archived) is not bool
51
59
  or type(reference.identifier) is not str
52
60
  or re.fullmatch(r"[0-9]{1,64}", reference.identifier) is None
53
61
  ):
@@ -75,7 +83,9 @@ def _date(value: Any) -> datetime:
75
83
  raise LibrusError(ErrorKind.PARSE)
76
84
 
77
85
 
78
- def _summary(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)
@@ -107,6 +107,8 @@ from librus_python_api.models import (
107
107
  MessagesPage,
108
108
  MessageSummary,
109
109
  ModernAccountData,
110
+ ModernCorrespondentReference,
111
+ ModernCorrespondents,
110
112
  ModernIdentity,
111
113
  ModernMessageAttachmentReference,
112
114
  ModernMessageContent,
@@ -119,6 +121,8 @@ from librus_python_api.models import (
119
121
  ModernRecipientTypeReference,
120
122
  ModernRecipientTypes,
121
123
  ModernSendSubmission,
124
+ ModernTeacherSubjects,
125
+ ModernUnreadCounts,
122
126
  NotificationCounts,
123
127
  Observation,
124
128
  OperationName,
@@ -146,7 +150,16 @@ from librus_python_api.modern_mailbox import collect as collect_modern_messages
146
150
  from librus_python_api.modern_mailbox import (
147
151
  parse_content as parse_modern_message_content,
148
152
  )
153
+ from librus_python_api.modern_mailbox import (
154
+ parse_correspondents as parse_modern_correspondents,
155
+ )
149
156
  from librus_python_api.modern_mailbox import parse_page as parse_modern_message_page
157
+ from librus_python_api.modern_mailbox import (
158
+ parse_unread_counts as parse_modern_unread_counts,
159
+ )
160
+ from librus_python_api.modern_mailbox import (
161
+ validate_correspondent as validate_modern_correspondent,
162
+ )
150
163
  from librus_python_api.modern_mailbox import (
151
164
  validate_reference as validate_modern_message_reference,
152
165
  )
@@ -160,6 +173,9 @@ from librus_python_api.modern_messages import (
160
173
  parse_modern_types,
161
174
  validate_modern_type,
162
175
  )
176
+ from librus_python_api.modern_messages import (
177
+ parse_teacher_subjects as parse_modern_teacher_subjects,
178
+ )
163
179
  from librus_python_api.notifications import (
164
180
  decode_payload,
165
181
  parse_notification_counts,
@@ -590,21 +606,32 @@ class AccountClient:
590
606
  )
591
607
 
592
608
  async def _modern_message_page(
593
- self, 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,
594
617
  ) -> ModernMessagesPage:
595
- operation: Literal["modern_messages_received", "modern_messages_sent"] = (
596
- "modern_messages_received"
597
- if folder is MessageFolder.RECEIVED
598
- else "modern_messages_sent"
599
- )
600
- items, total, fingerprint = await self._page(
618
+ operation = _modern_mailbox_operation(folder, archived)
619
+ items, total, fingerprint, in_progress = await self._page(
601
620
  operation,
602
621
  budget,
603
622
  lambda body: parse_modern_message_page(
604
- body, folder, page, page_size, self._alias
623
+ body, folder, page, page_size, self._alias, archived
605
624
  ),
606
625
  content_type=JSON,
607
- query=modern_mailbox_query(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
+ ),
608
635
  )
609
636
  return ModernMessagesPage(
610
637
  self._session_identity(),
@@ -615,31 +642,62 @@ class AccountClient:
615
642
  items,
616
643
  fingerprint,
617
644
  self._observation(operation),
645
+ archived,
646
+ correspondent,
647
+ unread_only,
648
+ in_progress,
618
649
  )
619
650
 
651
+ def _modern_filter(
652
+ self,
653
+ folder: MessageFolder,
654
+ archived: bool,
655
+ correspondent: ModernCorrespondentReference | None,
656
+ unread_only: bool,
657
+ ) -> str | None:
658
+ """Validate list filters; they are observed for the current mailbox only."""
659
+ value = validate_modern_correspondent(correspondent, folder, self._alias)
660
+ if type(archived) is not bool or type(unread_only) is not bool:
661
+ raise LibrusError(ErrorKind.INVALID_INPUT)
662
+ if unread_only and folder is not MessageFolder.RECEIVED:
663
+ raise LibrusError(ErrorKind.INVALID_INPUT)
664
+ if archived and (value is not None or unread_only):
665
+ raise LibrusError(ErrorKind.UNSUPPORTED_CAPABILITY)
666
+ return value
667
+
620
668
  async def modern_messages_page(
621
669
  self,
622
670
  folder: MessageFolder = MessageFolder.RECEIVED,
623
671
  *,
624
672
  page: int = 1,
625
673
  page_size: int = 10,
674
+ archived: bool = False,
675
+ correspondent: ModernCorrespondentReference | None = None,
676
+ unread_only: bool = False,
626
677
  budget: RequestBudget | None = None,
627
678
  max_age_seconds: float = 0.0,
628
679
  ) -> ModernMessagesPage:
629
- """One one-based ordinary modern page, without any content open."""
680
+ """One one-based ordinary modern page, without any content open.
681
+
682
+ ``archived`` lists the archive mailbox (earlier school years) instead.
683
+ ``correspondent`` (from ``modern_correspondents``) and ``unread_only``
684
+ (received folder) filter the current mailbox as the web app does.
685
+ """
630
686
  modern_mailbox_query(folder, page, page_size)
631
- operation: Literal["modern_messages_received", "modern_messages_sent"] = (
632
- "modern_messages_received"
633
- if folder is MessageFolder.RECEIVED
634
- else "modern_messages_sent"
635
- )
687
+ filter_id = self._modern_filter(folder, archived, correspondent, unread_only)
688
+ operation = _modern_mailbox_operation(folder, archived)
636
689
 
637
690
  async def fetch(budget: RequestBudget, _: bool) -> ModernMessagesPage:
638
691
  await self._modern_ready(budget)
639
- return await self._modern_message_page(folder, page, page_size, budget)
692
+ return await self._modern_message_page(
693
+ folder, page, page_size, budget, archived, correspondent, unread_only
694
+ )
640
695
 
641
696
  return await self._read(
642
- (operation, "page", page, page_size), fetch, budget, max_age_seconds
697
+ (operation, "page", page, page_size, filter_id, unread_only),
698
+ fetch,
699
+ budget,
700
+ max_age_seconds,
643
701
  )
644
702
 
645
703
  async def modern_messages(
@@ -650,29 +708,55 @@ class AccountClient:
650
708
  page_size: int = 50,
651
709
  max_pages: int = 4,
652
710
  limit: int = 128,
711
+ archived: bool = False,
712
+ correspondent: ModernCorrespondentReference | None = None,
713
+ unread_only: bool = False,
653
714
  budget: RequestBudget | None = None,
654
715
  max_age_seconds: float = 0.0,
655
716
  ) -> ModernMessages:
656
717
  """Bounded modern collection with selection-bound continuation."""
718
+ filter_id = self._modern_filter(folder, archived, correspondent, unread_only)
657
719
  validate_modern_message_selection(
658
- folder, cursor, page_size, max_pages, limit, self._alias
659
- )
660
- operation: Literal["modern_messages_received", "modern_messages_sent"] = (
661
- "modern_messages_received"
662
- if folder is MessageFolder.RECEIVED
663
- 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,
664
729
  )
730
+ operation = _modern_mailbox_operation(folder, archived)
665
731
 
666
732
  async def fetch(budget: RequestBudget, _: bool) -> ModernMessages:
667
733
  await self._modern_ready(budget)
734
+ flags: list[bool | None] = []
735
+
736
+ async def page_of(page: int) -> ModernMessagesPage:
737
+ result = await self._modern_message_page(
738
+ folder,
739
+ page,
740
+ page_size,
741
+ budget,
742
+ archived,
743
+ correspondent,
744
+ unread_only,
745
+ )
746
+ flags.append(result.archiving_in_progress)
747
+ return result
748
+
668
749
  result = await collect_modern_messages(
669
- lambda page: self._modern_message_page(folder, page, page_size, budget),
750
+ page_of,
670
751
  folder,
671
752
  self._alias,
672
753
  cursor,
673
754
  page_size,
674
755
  max_pages,
675
756
  limit,
757
+ archived,
758
+ filter_id,
759
+ unread_only,
676
760
  )
677
761
  items, pages, duplicates, continuation, reason = result
678
762
  return ModernMessages(
@@ -684,15 +768,106 @@ class AccountClient:
684
768
  continuation,
685
769
  reason,
686
770
  self._observation(operation),
771
+ archived,
772
+ correspondent,
773
+ unread_only,
774
+ any(flags) if archived else None,
687
775
  )
688
776
 
689
777
  return await self._read(
690
- (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
+ ),
691
788
  fetch,
692
789
  budget,
693
790
  max_age_seconds,
694
791
  )
695
792
 
793
+ async def modern_correspondents(
794
+ self,
795
+ folder: MessageFolder = MessageFolder.RECEIVED,
796
+ *,
797
+ budget: RequestBudget | None = None,
798
+ max_age_seconds: float = 0.0,
799
+ ) -> ModernCorrespondents:
800
+ """Senders (received) or receivers (sent) the current mailbox can filter by."""
801
+ if not isinstance(folder, MessageFolder):
802
+ raise LibrusError(ErrorKind.INVALID_INPUT)
803
+ operation: Literal["modern_senders", "modern_receivers"] = (
804
+ "modern_senders" if folder is MessageFolder.RECEIVED else "modern_receivers"
805
+ )
806
+
807
+ async def fetch(budget: RequestBudget, _: bool) -> ModernCorrespondents:
808
+ await self._modern_ready(budget)
809
+ items = await self._page(
810
+ operation,
811
+ budget,
812
+ lambda body: parse_modern_correspondents(body, folder, self._alias),
813
+ content_type=JSON,
814
+ )
815
+ return ModernCorrespondents(
816
+ self._session_identity(),
817
+ folder,
818
+ items,
819
+ self._observation(operation),
820
+ )
821
+
822
+ return await self._read((operation,), fetch, budget, max_age_seconds)
823
+
824
+ async def modern_teacher_subjects(
825
+ self, *, budget: RequestBudget | None = None, max_age_seconds: float = 0.0
826
+ ) -> ModernTeacherSubjects:
827
+ """Teachers' modern account IDs with the subjects they teach the student."""
828
+
829
+ async def fetch(budget: RequestBudget, _: bool) -> ModernTeacherSubjects:
830
+ await self._modern_ready(budget)
831
+ items = await self._page(
832
+ "modern_teacher_subjects",
833
+ budget,
834
+ parse_modern_teacher_subjects,
835
+ content_type=JSON,
836
+ )
837
+ return ModernTeacherSubjects(
838
+ self._session_identity(),
839
+ items,
840
+ self._observation("modern_teacher_subjects"),
841
+ )
842
+
843
+ return await self._read(
844
+ ("modern_teacher_subjects",), fetch, budget, max_age_seconds
845
+ )
846
+
847
+ async def modern_unread_counts(
848
+ self, *, budget: RequestBudget | None = None, max_age_seconds: float = 0.0
849
+ ) -> ModernUnreadCounts:
850
+ """Unread counters of the modern mailbox, without opening any message."""
851
+
852
+ async def fetch(budget: RequestBudget, _: bool) -> ModernUnreadCounts:
853
+ await self._modern_ready(budget)
854
+ current, archive = await self._page(
855
+ "modern_unread_counts",
856
+ budget,
857
+ parse_modern_unread_counts,
858
+ content_type=JSON,
859
+ )
860
+ return ModernUnreadCounts(
861
+ self._session_identity(),
862
+ current,
863
+ archive,
864
+ self._observation("modern_unread_counts"),
865
+ )
866
+
867
+ return await self._read(
868
+ ("modern_unread_counts",), fetch, budget, max_age_seconds
869
+ )
870
+
696
871
  async def modern_message_content(
697
872
  self,
698
873
  reference: ModernMessageReference,
@@ -706,6 +881,9 @@ class AccountClient:
706
881
  reference.folder is MessageFolder.RECEIVED and not allow_mark_read
707
882
  ):
708
883
  raise LibrusError(ErrorKind.INVALID_INPUT)
884
+ if reference.archived:
885
+ # No archived content route or mark-read effect is established yet.
886
+ raise LibrusError(ErrorKind.UNSUPPORTED_CAPABILITY)
709
887
  operation: Literal["modern_content_received", "modern_content_sent"] = (
710
888
  "modern_content_received"
711
889
  if reference.folder is MessageFolder.RECEIVED
@@ -2336,5 +2514,20 @@ class AccountClient:
2336
2514
  return response
2337
2515
 
2338
2516
 
2517
+ def _modern_mailbox_operation(
2518
+ folder: MessageFolder, archived: bool
2519
+ ) -> Literal[
2520
+ "modern_messages_received",
2521
+ "modern_messages_sent",
2522
+ "modern_archive_messages_received",
2523
+ "modern_archive_messages_sent",
2524
+ ]:
2525
+ if folder is MessageFolder.RECEIVED:
2526
+ if archived:
2527
+ return "modern_archive_messages_received"
2528
+ return "modern_messages_received"
2529
+ return "modern_archive_messages_sent" if archived else "modern_messages_sent"
2530
+
2531
+
2339
2532
  def _media_type(response: TransportResponse) -> str:
2340
2533
  return response.headers.get("content-type", "").partition(";")[0].strip().lower()
@@ -34,6 +34,7 @@ from librus_python_api.config import (
34
34
  MESSAGE_PAGE_FIELDS,
35
35
  MODERN_DIRECTORY_QUERIES,
36
36
  MODERN_HANDOFF_PATTERN,
37
+ MODERN_MAILBOX_OPERATIONS,
37
38
  MODERN_TERMINAL_PATHS,
38
39
  OAUTH_QUERY,
39
40
  USER_AGENT,
@@ -297,12 +298,8 @@ class AiohttpTransport:
297
298
  url = str(URL(url).with_query(MODERN_DIRECTORY_QUERIES[endpoint_id]))
298
299
  if (
299
300
  endpoint_id
300
- in {
301
- "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.3
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.3
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.3-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=6WrjdwyywtqZ9iRD7jGBvGjOdy7YAAUNfChbcT4jdQU,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=Tx104USrajcwJe9SxfJWK6ppeoSYqd88lC_PKsCJMYs,49575
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=tTjOC3nOWQL1O7QH-IvoSukJX7oNENmAN5xH8z-d8a0,86557
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.3.dist-info/METADATA,sha256=PbL-qr7wfo185OJS5JpIWHdli4xxck8uOtVRVuVHZYg,11546
46
- librus_python_api-1.0.3.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
47
- librus_python_api-1.0.3.dist-info/licenses/LICENSE,sha256=IuerLb3cnZCsaB23znydjjvKqP-LmSJd3syKa9NGUts,1071
48
- librus_python_api-1.0.3.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,,