librus-python-api 1.4.0__py3-none-any.whl → 1.6.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.
@@ -35,6 +35,8 @@ from librus_python_api.models import (
35
35
  AttendanceView,
36
36
  AttendanceWindow,
37
37
  Availability,
38
+ ClassFreeDay,
39
+ ClassFreeDays,
38
40
  CompletedLesson,
39
41
  CompletedLessons,
40
42
  CompletedLessonsCursor,
@@ -136,7 +138,7 @@ from librus_python_api.models import (
136
138
  from librus_python_api.sending import SendAttempt
137
139
  from librus_python_api.service import AccountClient, LibrusService
138
140
 
139
- __version__ = "1.4.0"
141
+ __version__ = "1.6.0"
140
142
 
141
143
  __all__ = [
142
144
  "ArchiveAbsences",
@@ -216,6 +218,8 @@ __all__ = [
216
218
  "AccountClient",
217
219
  "AccountCredentials",
218
220
  "Availability",
221
+ "ClassFreeDay",
222
+ "ClassFreeDays",
219
223
  "Attendance",
220
224
  "Announcement",
221
225
  "Agenda",
@@ -0,0 +1,73 @@
1
+ """Bounded gateway class free days from independently observed JSON structure."""
2
+
3
+ import re
4
+ from datetime import date
5
+ from typing import Any
6
+
7
+ from librus_python_api.config import CLASS_FREE_DAYS_MAX_RECORDS
8
+ from librus_python_api.exceptions import ErrorKind, LibrusError
9
+ from librus_python_api.models import ClassFreeDay
10
+ from librus_python_api.parsers import decode_json
11
+
12
+
13
+ def _identifier(value: Any) -> str:
14
+ if type(value) is not int or value < 0 or len(str(value)) > 64:
15
+ raise LibrusError(ErrorKind.PARSE)
16
+ return str(value)
17
+
18
+
19
+ def _reference(value: Any) -> str:
20
+ if not isinstance(value, dict):
21
+ raise LibrusError(ErrorKind.PARSE)
22
+ return _identifier(value.get("Id"))
23
+
24
+
25
+ def _day(value: Any) -> date:
26
+ result = None
27
+ if type(value) is str and re.fullmatch(r"[0-9]{4}-[0-9]{2}-[0-9]{2}", value):
28
+ try:
29
+ result = date.fromisoformat(value)
30
+ except ValueError:
31
+ pass
32
+ if result is None:
33
+ raise LibrusError(ErrorKind.PARSE)
34
+ return result
35
+
36
+
37
+ def parse_class_free_days(body: bytes) -> tuple[ClassFreeDay, ...]:
38
+ data = decode_json(body)
39
+ if not isinstance(data, dict) or not isinstance(data.get("ClassFreeDays"), list):
40
+ raise LibrusError(ErrorKind.PARSE)
41
+ rows = data["ClassFreeDays"]
42
+ if len(rows) > CLASS_FREE_DAYS_MAX_RECORDS:
43
+ raise LibrusError(ErrorKind.LIMIT)
44
+ items: list[ClassFreeDay] = []
45
+ seen: set[str] = set()
46
+ for row in rows:
47
+ if not isinstance(row, dict):
48
+ raise LibrusError(ErrorKind.PARSE)
49
+ identifier = _identifier(row.get("Id"))
50
+ start, end = _day(row.get("DateFrom")), _day(row.get("DateTo"))
51
+ first, last = row.get("LessonNoFrom"), row.get("LessonNoTo")
52
+ if "LessonNoFrom" in row or "LessonNoTo" in row:
53
+ if (
54
+ type(first) is not int
55
+ or type(last) is not int
56
+ or not 0 <= first <= last <= 99
57
+ ):
58
+ raise LibrusError(ErrorKind.PARSE)
59
+ if identifier in seen or start > end:
60
+ raise LibrusError(ErrorKind.PARSE)
61
+ seen.add(identifier)
62
+ items.append(
63
+ ClassFreeDay(
64
+ identifier,
65
+ _reference(row.get("Class")),
66
+ _reference(row.get("Type")),
67
+ start,
68
+ end,
69
+ first,
70
+ last,
71
+ )
72
+ )
73
+ return tuple(items)
@@ -449,6 +449,14 @@ ENDPOINTS: Mapping[str, Endpoint] = MappingProxyType(
449
449
  True,
450
450
  Evidence.INDEPENDENTLY_OBSERVED,
451
451
  ),
452
+ Endpoint(
453
+ "class_free_days",
454
+ "GET",
455
+ "/gateway/api/2.0/ClassFreeDays",
456
+ SideEffect.NONE,
457
+ True,
458
+ Evidence.INDEPENDENTLY_OBSERVED,
459
+ ),
452
460
  Endpoint(
453
461
  "gateway_attendance",
454
462
  "GET",
@@ -855,12 +863,18 @@ WEEKDAY_LABELS = ("pon.", "wt.", "śr.", "czw.", "pt.", "sob.", "ndz.")
855
863
  # Observed page-level notices shown instead of the requested content.
856
864
  PAGE_NOTICES = MappingProxyType(
857
865
  {
866
+ (
867
+ "Ten moduł nie jest dostępny w wykorzystywanym przez szkołę wariancie "
868
+ "rozwiązania LIBRUS Synergia."
869
+ ): ErrorKind.MODULE_UNAVAILABLE,
858
870
  "Ten widok został wyłączony przez administratora szkoły.": (
859
871
  ErrorKind.VIEW_DISABLED
860
872
  ),
861
873
  "Wybrano nieprawidłowy zakres daty.": ErrorKind.INVALID_INPUT,
862
874
  }
863
875
  )
876
+ MODULE_UNAVAILABLE_PATH = "/modul_niedostepny"
877
+ CLASS_FREE_DAYS_MAX_RECORDS = 4096
864
878
  COMPLETED_LESSONS_MAX_WINDOW_DAYS = 371
865
879
  COMPLETED_LESSONS_MAX_PAGE_COUNT = 1000
866
880
  COMPLETED_LESSONS_MAX_PAGE_ITEMS = 256
@@ -14,6 +14,7 @@ __all__ = [
14
14
  "LibrusError",
15
15
  "LimitError",
16
16
  "MaintenanceError",
17
+ "ModuleUnavailableError",
17
18
  "OperationTimeoutError",
18
19
  "ParseError",
19
20
  "SessionExpiredError",
@@ -36,6 +37,7 @@ class ErrorKind(StrEnum):
36
37
  ACCESS_DENIED = "access_denied"
37
38
  UNSUPPORTED_CAPABILITY = "unsupported_capability"
38
39
  VIEW_DISABLED = "view_disabled"
40
+ MODULE_UNAVAILABLE = "module_unavailable"
39
41
  THROTTLED = "throttled"
40
42
  MAINTENANCE = "maintenance"
41
43
  CONNECTION = "connection"
@@ -101,6 +103,10 @@ class ViewDisabledError(LibrusError):
101
103
  """The school administrator has switched this Synergia view off."""
102
104
 
103
105
 
106
+ class ModuleUnavailableError(LibrusError):
107
+ """The school's Synergia product variant does not include this module."""
108
+
109
+
104
110
  class ThrottledError(LibrusError):
105
111
  pass
106
112
 
@@ -154,6 +160,7 @@ _ERROR_TYPES: dict[ErrorKind, type[LibrusError]] = {
154
160
  ErrorKind.ACCESS_DENIED: AccessDeniedError,
155
161
  ErrorKind.UNSUPPORTED_CAPABILITY: UnsupportedCapabilityError,
156
162
  ErrorKind.VIEW_DISABLED: ViewDisabledError,
163
+ ErrorKind.MODULE_UNAVAILABLE: ModuleUnavailableError,
157
164
  ErrorKind.THROTTLED: ThrottledError,
158
165
  ErrorKind.MAINTENANCE: MaintenanceError,
159
166
  ErrorKind.CONNECTION: ConnectionError,
@@ -57,26 +57,33 @@ def _recipient_receipts(
57
57
  raise LibrusError(ErrorKind.UNSUPPORTED_CAPABILITY)
58
58
  result = []
59
59
  total = 0
60
+ # Rows are recipient/status, or recipient/class/status when the page adds
61
+ # the class-label column; one table never mixes the two.
62
+ width = len(cells(table_rows[1]))
60
63
  for row in table_rows[1:]:
61
64
  columns = cells(row)
62
- if len(columns) != 2 or any(
63
- c.tag != "td"
64
- or c.get("colspan", "1") != "1"
65
- or c.get("rowspan", "1") != "1"
66
- or next(c.iterdescendants("table"), None) is not None
67
- for c in columns
65
+ if (
66
+ len(columns) != width
67
+ or width not in (2, 3)
68
+ or any(
69
+ c.tag != "td"
70
+ or c.get("colspan", "1") != "1"
71
+ or c.get("rowspan", "1") != "1"
72
+ or next(c.iterdescendants("table"), None) is not None
73
+ for c in columns
74
+ )
68
75
  ):
69
76
  raise LibrusError(ErrorKind.UNSUPPORTED_CAPABILITY)
70
- recipient, status = [
71
- text(c, MESSAGE_MAX_FIELD_LENGTH, multiline=True) for c in columns
72
- ]
77
+ values = [text(c, MESSAGE_MAX_FIELD_LENGTH, multiline=True) for c in columns]
78
+ recipient, status = values[0], values[-1]
73
79
  if not recipient or not status:
74
80
  raise LibrusError(ErrorKind.PARSE)
75
- total += len(recipient) + len(status)
81
+ total += sum(map(len, values))
76
82
  if total > MESSAGE_MAX_RECEIPT_TEXT_LENGTH:
77
83
  raise LibrusError(ErrorKind.LIMIT)
78
84
  stamp = None if status == "NIE" else _timestamp(status)
79
- result.append(MessageRecipientReceipt(recipient, status, stamp))
85
+ label = values[1] if width == 3 and values[1] else None
86
+ result.append(MessageRecipientReceipt(recipient, status, stamp, label))
80
87
  return tuple(result)
81
88
 
82
89
 
@@ -16,6 +16,7 @@ from librus_python_api.exceptions import ErrorKind
16
16
 
17
17
  type OperationName = Literal[
18
18
  "identity",
19
+ "class_free_days",
19
20
  "student_information",
20
21
  "final_grades",
21
22
  "grades",
@@ -210,6 +211,8 @@ class MessageRecipientReceipt:
210
211
  recipient: str = field(repr=False)
211
212
  raw_status: str = field(repr=False)
212
213
  read_timestamp: MessageTimestamp | None = field(repr=False)
214
+ # Displayed class label of the optional middle column; None without it.
215
+ recipient_class: str | None = field(default=None, repr=False)
213
216
 
214
217
 
215
218
  @dataclass(frozen=True, slots=True)
@@ -630,6 +633,24 @@ class CompletedLessons:
630
633
  observation: "Observation"
631
634
 
632
635
 
636
+ @dataclass(frozen=True, slots=True)
637
+ class ClassFreeDay:
638
+ identifier: str = field(repr=False)
639
+ class_id: str = field(repr=False)
640
+ type_id: str = field(repr=False)
641
+ date_from: date = field(repr=False)
642
+ date_to: date = field(repr=False)
643
+ lesson_no_from: int | None = None
644
+ lesson_no_to: int | None = None
645
+
646
+
647
+ @dataclass(frozen=True, slots=True)
648
+ class ClassFreeDays:
649
+ identity: "Identity" = field(repr=False)
650
+ items: tuple[ClassFreeDay, ...] = field(repr=False)
651
+ observation: "Observation"
652
+
653
+
633
654
  class GradeKind(StrEnum):
634
655
  CURRENT = "current"
635
656
  PERIOD = "period"
@@ -27,6 +27,7 @@ from librus_python_api.attendance_frequency import (
27
27
  )
28
28
  from librus_python_api.budget import RequestBudget
29
29
  from librus_python_api.checkpoint import CHECKPOINT_SERVICE, validate_checkpoint
30
+ from librus_python_api.class_free_days import parse_class_free_days
30
31
  from librus_python_api.completed_lessons import (
31
32
  parse_completed_lessons,
32
33
  validate_selection,
@@ -42,6 +43,7 @@ from librus_python_api.config import (
42
43
  GRADE_MAX_WINDOW_DAYS,
43
44
  MESSAGE_MAX_CURSOR_IDS,
44
45
  MODERN_RECIPIENT_OPERATIONS,
46
+ MODULE_UNAVAILABLE_PATH,
45
47
  NOTIFICATION_COUNTS_ENDPOINT,
46
48
  SCHEDULE_RESPONSE_VERSION,
47
49
  SESSION_COOKIE,
@@ -83,6 +85,7 @@ from librus_python_api.models import (
83
85
  AttendanceFrequency,
84
86
  AttendanceView,
85
87
  AttendanceWindow,
88
+ ClassFreeDays,
86
89
  CompletedLesson,
87
90
  CompletedLessons,
88
91
  CompletedLessonsCursor,
@@ -1314,6 +1317,24 @@ class AccountClient:
1314
1317
  ),
1315
1318
  )
1316
1319
 
1320
+ async def class_free_days(
1321
+ self,
1322
+ *,
1323
+ budget: RequestBudget | None = None,
1324
+ max_age_seconds: float = 0.0,
1325
+ ) -> ClassFreeDays:
1326
+ """Read one bounded collection; dates are civil and references stay opaque."""
1327
+
1328
+ async def fetch(budget: RequestBudget, _: bool) -> ClassFreeDays:
1329
+ items = await self._page(
1330
+ "class_free_days", budget, parse_class_free_days, content_type=JSON
1331
+ )
1332
+ return ClassFreeDays(
1333
+ self._session_identity(), items, self._observation("class_free_days")
1334
+ )
1335
+
1336
+ return await self._read(("class_free_days",), fetch, budget, max_age_seconds)
1337
+
1317
1338
  async def attendance(
1318
1339
  self,
1319
1340
  *,
@@ -2438,6 +2459,8 @@ class AccountClient:
2438
2459
  self, response: TransportResponse, content_type: str
2439
2460
  ) -> None:
2440
2461
  if 300 <= response.status < 400:
2462
+ if self._is_module_unavailable_redirect(response):
2463
+ raise LibrusError(ErrorKind.MODULE_UNAVAILABLE)
2441
2464
  # A redirect proves expiry only when it targets a login route.
2442
2465
  # Other redirects must not trigger a new credential submission.
2443
2466
  if self._is_login_redirect(response):
@@ -2446,6 +2469,25 @@ class AccountClient:
2446
2469
  if response.status != 200 or _media_type(response) != content_type:
2447
2470
  raise LibrusError(ErrorKind.PARSE)
2448
2471
 
2472
+ def _is_module_unavailable_redirect(self, response: TransportResponse) -> bool:
2473
+ location = response.headers.get("location", "")
2474
+ if not location or len(location) > 4096:
2475
+ return False
2476
+ try:
2477
+ target = urlsplit(urljoin(response.url, location))
2478
+ source = urlsplit(response.url)
2479
+ except ValueError:
2480
+ return False
2481
+ expected = urlsplit(self._service._connection.origin(ENDPOINTS["identity"]))
2482
+ return (
2483
+ (source.scheme, source.netloc) == (expected.scheme, expected.netloc)
2484
+ and (target.scheme, target.netloc) == (expected.scheme, expected.netloc)
2485
+ and not (
2486
+ target.username or target.password or target.query or target.fragment
2487
+ )
2488
+ and target.path == MODULE_UNAVAILABLE_PATH
2489
+ )
2490
+
2449
2491
  def _is_login_redirect(self, response: TransportResponse) -> bool:
2450
2492
  location = response.headers.get("location", "")
2451
2493
  try:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: librus-python-api
3
- Version: 1.4.0
3
+ Version: 1.6.0
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
@@ -42,9 +42,9 @@ private ACLs. Windows installations include `pywin32` and `tzdata` automatically
42
42
  Network paths, reparse-point paths and unsafe storage permissions fail closed.
43
43
  See [platform requirements](contracts/persistence.md#windows-disk-boundary-25).
44
44
 
45
- **Status:** `1.4.0` adds formative assessments, including the observation card,
46
- to grade results. The 1.3.0 archive read passed installed live checks for
47
- populated and empty archives on two parent logins; see [verification](VERIFICATION.md).
45
+ **Status:** `1.6.0` adds class free days and a typed module-unavailable outcome,
46
+ plus an independent live-check freshness watchdog and targeted mutation/load
47
+ evidence. See [verification](VERIFICATION.md).
48
48
  The stable 1.x API does not guarantee every school layout. School features depend
49
49
  on what each account can access. See the [compatibility policy](API.md#compatibility-policy).
50
50
  See [limitations](#supported-features-and-limitations) below.
@@ -54,7 +54,7 @@ See [limitations](#supported-features-and-limitations) below.
54
54
  Install the exact release from PyPI after its gated publication completes:
55
55
 
56
56
  ```sh
57
- python -m pip install librus-python-api==1.4.0
57
+ python -m pip install librus-python-api==1.6.0
58
58
  ```
59
59
 
60
60
  Pin the exact version qualified by your application. Before publication, use a
@@ -70,7 +70,7 @@ python -m pip install .
70
70
  To install a locally built wheel instead:
71
71
 
72
72
  ```sh
73
- python -m pip install ./dist/librus_python_api-1.4.0-py3-none-any.whl
73
+ python -m pip install ./dist/librus_python_api-1.6.0-py3-none-any.whl
74
74
  ```
75
75
 
76
76
  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=Ka6_hAV-fnh3QrjAPyqhWPvaG2eh5w8VT8a5aB_RBNU,6435
1
+ librus_python_api/__init__.py,sha256=DhWaUkE_sGZgqB1aWZh5ZUtyjfIeMJl1KhUPV5WpzLA,6513
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
@@ -10,11 +10,12 @@ librus_python_api/attendance.py,sha256=HeFAfnIUn4N__yp1zr635Dve55S2jmwtkeIy_6mV5
10
10
  librus_python_api/attendance_frequency.py,sha256=SBhED5zuyRIHWdw4urFyTJYykb6EYB7eL4dU3mif1EY,5309
11
11
  librus_python_api/budget.py,sha256=8fvqT7eIxyVX2P2OBG27XsYHk_HbtSS4lzuecYpHtgs,2878
12
12
  librus_python_api/checkpoint.py,sha256=QBmG9GtpyaGRaFq-FXuGA3yR1BjuMFN9V3RTvvi_FNQ,1930
13
+ librus_python_api/class_free_days.py,sha256=IwoCgr-9ZgEYYQ6717s4cDpyFzbosHMzESMabXJLGqA,2422
13
14
  librus_python_api/completed_lessons.py,sha256=sR2oLSGb2q1580l-2upCBv9sQfIcHi7CQfWr1K0EFXI,7635
14
- librus_python_api/config.py,sha256=zhE2-v3FehMvvsbxMtK1J6iliOp8jYwb4g-e-rFCMEQ,54968
15
+ librus_python_api/config.py,sha256=A5UNZ0oc8Oxt13y0To1REdQLWPgLrH-VRRpcuwodElc,55482
15
16
  librus_python_api/detail_fields.py,sha256=K10Stx_ehDTttkUI7-G7a2Ko9exYEu5NJpR-3sBnQ60,1789
16
17
  librus_python_api/diagnostics.py,sha256=2l7wq6RA3it-Gwj-JRLcw5qbI37fl3xII6GZA2mZj0o,856
17
- librus_python_api/exceptions.py,sha256=yWWoo0xyrQAk1d13HOJZnQBsq4ulh_P0hRSI6ZiNQOE,4622
18
+ librus_python_api/exceptions.py,sha256=adVcJuzfB8wGrUlF2SfiyIlE9ZZyTAvGdWRtQIjsA3I,4879
18
19
  librus_python_api/files.py,sha256=pQBsIJyfTOZDz83ZB-5fcd-nPMcxR1HJ155h4zTHmsA,9223
19
20
  librus_python_api/formative_grades.py,sha256=-F-d1rnFVAIGwg1mN0cryCIBR76OW3v2L4mCWUZ3M7Q,6227
20
21
  librus_python_api/grade_parsers.py,sha256=91Kr1-TArzqO-b94ipeIR0a98OV7tRCZQdBWsd9QLZ0,6141
@@ -22,9 +23,9 @@ librus_python_api/grade_records.py,sha256=K0ZzmU1KsA8Jo1MXh30mStApgrigaOKk5ldoBn
22
23
  librus_python_api/homework_range.py,sha256=erYfvS6PhHU5XhnR6LkiLNy677hGVflY3UAuHOs-YUw,1663
23
24
  librus_python_api/lifecycle.py,sha256=XzA5CEdZFo7iL5IMUrawnRvf2m9BFe0OseP-fpQFaAE,856
24
25
  librus_python_api/markup.py,sha256=mjCZf6KJTWUz2vjfXbJ4zM17hnev7a5AFIOW3ip3gsE,5677
25
- librus_python_api/message_content.py,sha256=sUPuqpjuavqDyn6QGM5aPH4mpoQV62bDLepSSQ2AsUE,8686
26
+ librus_python_api/message_content.py,sha256=v_qoEAoaYS0_9MQBaj57i5XnC7pzv_HHWefgTt4CNyM,9043
26
27
  librus_python_api/messages.py,sha256=K9F8XVs1SojfFpdxxLV8gx9E0g81YxEHjigDzLHmDCg,10230
27
- librus_python_api/models.py,sha256=wWMw2TXmeRWP3yJLSDmdVDYsbXRIztm3mEhCYPH1gco,40310
28
+ librus_python_api/models.py,sha256=Z6eV-ftjaN8YGUW8SntVTLjGCqe1EWRpd0P1Va8EwDs,40991
28
29
  librus_python_api/modern_body.py,sha256=3L4P_cIrHdIxZqiinFSQrDMhG6iklT1GoA-VEWMJUiI,2682
29
30
  librus_python_api/modern_mailbox.py,sha256=2jF9UnJZ5AF6dip-GrMcv_z2kSLpp87dIpVeXk9jMzA,20600
30
31
  librus_python_api/modern_messages.py,sha256=qt2CXtrxCZQ-jTt0aQCD6ZaPAJQZXf0BxmvacPo9B0w,10653
@@ -41,10 +42,10 @@ librus_python_api/scheduler.py,sha256=3JplYxqvQdFz3aKXcoQUJHE9s3FPw_xRhQ2CPNLuQm
41
42
  librus_python_api/school_reads.py,sha256=PCnZ4lv44j-68FgJpQiMteaPSyfhbX8_3CUyE6zhFcQ,14760
42
43
  librus_python_api/school_year_archive.py,sha256=D0A1ywMKgoYgvi-d70__SFreMGvMb_72870Akkbb_m8,15492
43
44
  librus_python_api/sending.py,sha256=Nvvk2r-6D6Us53a2fH5GW9PcA6bWEfuOvy6c3M5QVcE,4549
44
- librus_python_api/service.py,sha256=yjdKc4-psMDlIHVLud-WA2YDXafKPuTT-hLdVmdWsIM,94464
45
+ librus_python_api/service.py,sha256=7ioLx-8C-I0Qla8YUyu5UqhjBPlcUvEFpQSYH-9yjzs,96232
45
46
  librus_python_api/timetable.py,sha256=tX1QK9fQABxzfLaHHqTe8e6rAgdOkCoTOfAHdPwNmLE,9836
46
47
  librus_python_api/transport.py,sha256=QnYXnrv1lW4qToYoTv6HyZw-Lt4rDc-_SMTJpMp6_bc,35297
47
- librus_python_api-1.4.0.dist-info/METADATA,sha256=VKqlisucgyKpgIO7aGudJPX8-gKpm5mB3ZEm_59jJzE,12031
48
- librus_python_api-1.4.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
49
- librus_python_api-1.4.0.dist-info/licenses/LICENSE,sha256=IuerLb3cnZCsaB23znydjjvKqP-LmSJd3syKa9NGUts,1071
50
- librus_python_api-1.4.0.dist-info/RECORD,,
48
+ librus_python_api-1.6.0.dist-info/METADATA,sha256=O_HfTBq306xfvGOiGLwpWeWdHb2G9m_2c2PN9HmY4VY,11994
49
+ librus_python_api-1.6.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
50
+ librus_python_api-1.6.0.dist-info/licenses/LICENSE,sha256=IuerLb3cnZCsaB23znydjjvKqP-LmSJd3syKa9NGUts,1071
51
+ librus_python_api-1.6.0.dist-info/RECORD,,