librus-python-api 1.8.2__py3-none-any.whl → 1.8.3__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.
@@ -1,7 +1,7 @@
1
1
  """Independent bounded async client. General live compatibility is unqualified."""
2
2
 
3
3
  # Before the imports: config reads it for the User-Agent.
4
- __version__ = "1.8.2"
4
+ __version__ = "1.8.3"
5
5
 
6
6
  from librus_python_api.attachments import AttachmentStream, ModernAttachmentStream
7
7
  from librus_python_api.budget import RequestBudget
@@ -11,6 +11,7 @@ from librus_python_api.config import (
11
11
  ATTENDANCE_EMPTY_MARKERS,
12
12
  ATTENDANCE_MAX_RECORDS,
13
13
  ATTENDANCE_SEMESTER_LABELS,
14
+ ATTENDANCE_TOOLTIP_NOTICES,
14
15
  GRADE_MAX_METADATA_LENGTH,
15
16
  GRADE_MAX_VALUE_LENGTH,
16
17
  )
@@ -29,6 +30,17 @@ def _entry(element: html.HtmlElement, semester: Literal[1, 2]) -> AttendanceReco
29
30
  title = element.get("title", "")
30
31
  if len(title) > GRADE_MAX_METADATA_LENGTH:
31
32
  raise LibrusError(ErrorKind.LIMIT)
33
+ # A known Librus notice is a whole line of its own; other unlabelled lines
34
+ # still fail in tooltip_fields.
35
+ notices: list[tuple[str, str]] = []
36
+ lines = []
37
+ for line in re.split(r"<br\s*/?>|\n", title, flags=re.I):
38
+ notice = ATTENDANCE_TOOLTIP_NOTICES.get(markup.tooltip_line(line))
39
+ if notice is None or notice in notices:
40
+ lines.append(line)
41
+ else:
42
+ notices.append(notice)
43
+ title = "<br>".join(lines)
32
44
  holder = html.Element("span")
33
45
  holder.set("title", re.sub(r"</b>\s*(?=<b[ >])", "</b>\n", title, flags=re.I))
34
46
  fields = markup.tooltip_fields(holder)
@@ -57,7 +69,7 @@ def _entry(element: html.HtmlElement, semester: Literal[1, 2]) -> AttendanceReco
57
69
  fields.get("Temat zajęć"),
58
70
  fields.get("Lekcja"),
59
71
  markup.attendance_detail_id(element),
60
- tuple(fields.items()),
72
+ (*fields.items(), *notices),
61
73
  )
62
74
 
63
75
 
@@ -787,6 +787,7 @@ GRADE_PERIOD_HEADERS = MappingProxyType(
787
787
  # semester cells and its script loads the grades from the gateway routes above.
788
788
  DESCRIPTIVE_TABLE_CLASS = "newSchemaGradesByStudentTable"
789
789
  DESCRIPTIVE_ROW_CLASS = "studentRow"
790
+ DESCRIPTIVE_DETAILS_ROW_CLASS = "studentGradesDetailsRow"
790
791
  # Gateway account and student identifiers are opaque strings, unlike every
791
792
  # other `{id}` route, which takes a numeric ID.
792
793
  DESCRIPTIVE_IDENTIFIER = re.compile(r"[A-Za-z0-9_-]{1,128}")
@@ -838,6 +839,17 @@ ATTENDANCE_SEMESTER_LABELS = MappingProxyType(
838
839
  }
839
840
  )
840
841
  ATTENDANCE_EMPTY_MARKERS = frozenset({"", "-", "Brak nieobecności"})
842
+ # Fixed unlabelled tooltip lines Librus appends to an attendance entry, and the
843
+ # metadata pair each becomes. Observed live on 2026-11-06 on two logins: an
844
+ # absence excused through the e-Usprawiedliwienia module.
845
+ ATTENDANCE_TOOLTIP_NOTICES = MappingProxyType(
846
+ {
847
+ "Usprawiedliwienie dodane za pomocą modułu e-Usprawiedliwienia": (
848
+ "Usprawiedliwienie",
849
+ "dodane za pomocą modułu e-Usprawiedliwienia",
850
+ )
851
+ }
852
+ )
841
853
  ATTENDANCE_MAX_RECORDS = 2048
842
854
  ATTENDANCE_MAX_WINDOW_DAYS = 371
843
855
  # Whole-school gateway collections resolve attendance metadata in two requests.
@@ -849,6 +861,8 @@ ATTENDANCE_DETAIL_PATH_PREFIX = "/przegladaj_nb/szczegoly/"
849
861
  FORMATIVE_DETAIL_PATH_PREFIX = "/przegladaj_oceny/szczegoly/ksztaltujace/"
850
862
  # Recognition only: behaviour-point detail links are classified, never requested.
851
863
  BEHAVIOUR_POINTS_DETAIL_PATH_PREFIX = "/przegladaj_oceny/zachowanie_szczegoly/"
864
+ # Tooltip labels of a behaviour grade symbol entry (#114, synthetic example).
865
+ BEHAVIOUR_GRADE_LABELS = frozenset({"Data wystawienia", "Dodał", "Komentarz"})
852
866
  # Recognition only: point-grade detail links are classified, never requested.
853
867
  POINT_GRADE_DETAIL_PATH_PREFIX = "/przegladaj_oceny_punktowe/szczegoly/"
854
868
  FORMATIVE_HEADERS = (
@@ -916,7 +930,10 @@ ANNOUNCEMENT_LABELS = MappingProxyType(
916
930
  ANNOUNCEMENT_EMPTY_CLASSES = frozenset(
917
931
  {"container", "border-red", "resizeable", "center"}
918
932
  )
919
- ANNOUNCEMENT_EMPTY_MARKERS = frozenset({"brak ogłoszeń", "nie ma ogłoszeń"})
933
+ # "Aktualnie brak ogłoszeń" reported in #120 on two accounts.
934
+ ANNOUNCEMENT_EMPTY_MARKERS = frozenset(
935
+ {"brak ogłoszeń", "nie ma ogłoszeń", "aktualnie brak ogłoszeń"}
936
+ )
920
937
  TIMETABLE_MAX_PERIODS = 32
921
938
  TIMETABLE_MAX_LESSONS_PER_SLOT = 16
922
939
  TIMETABLE_MAX_CHANGES_PER_SLOT = 16
@@ -6,6 +6,11 @@ grades from gateway JSON routes. Routes and field names come from public client
6
6
  code and the official page script. One authorized read on 2026-10-28 parsed four
7
7
  PartialGrades records on one school; pagination keys are still unknown, so every
8
8
  unexpected shape fails instead of producing a partial collection.
9
+
10
+ Another school renders the same table with plain text in its semester cells and
11
+ no script hooks (#115, from a reporter's synthetic example). The grade record
12
+ parser reads those cells like any descriptive row, so that table needs no
13
+ gateway read.
9
14
  """
10
15
 
11
16
  import re
@@ -20,6 +25,7 @@ from pydantic import BaseModel, ConfigDict, Field, ValidationError, field_valida
20
25
 
21
26
  from librus_python_api import markup
22
27
  from librus_python_api.config import (
28
+ DESCRIPTIVE_DETAILS_ROW_CLASS,
23
29
  DESCRIPTIVE_IDENTIFIER,
24
30
  DESCRIPTIVE_MAX_NAME_LENGTH,
25
31
  DESCRIPTIVE_MAX_SUBJECTS,
@@ -331,9 +337,13 @@ def parse_gateway_users(body: bytes) -> Mapping[str, str]:
331
337
 
332
338
  @dataclass(frozen=True, slots=True)
333
339
  class DescriptiveTable:
334
- """Numeric subject ID to the subject name the grades page shows."""
340
+ """Numeric subject ID to the subject name the grades page shows.
341
+
342
+ A filled table already holds its grades and has no gateway grades.
343
+ """
335
344
 
336
345
  subjects: Mapping[int, str] = field(repr=False)
346
+ filled: bool = False
337
347
 
338
348
 
339
349
  def _classes(element: html.HtmlElement) -> set[str]:
@@ -343,9 +353,11 @@ def _classes(element: html.HtmlElement) -> set[str]:
343
353
  def read_descriptive_table(document: html.HtmlElement) -> DescriptiveTable | None:
344
354
  """The new-schema subject table, or None when the page has none.
345
355
 
346
- Its rows hold an expander, the subject and two empty semester cells that
347
- the page script fills from the gateway. A cell the page already filled
348
- would duplicate the gateway records, so it is an unsupported layout.
356
+ Its rows hold an expander, the subject and two semester cells. Either
357
+ every row has empty `gradesCell` cells that the page script fills from
358
+ the gateway, or every row has plain cells the page filled itself, which
359
+ `read_grade_records` reads and validates. A script cell the page already
360
+ filled would duplicate the gateway records, so it is unsupported.
349
361
  """
350
362
  tables = [
351
363
  table
@@ -369,20 +381,33 @@ def read_descriptive_table(document: html.HtmlElement) -> DescriptiveTable | Non
369
381
  tables = populated
370
382
  subjects: dict[int, str] = {}
371
383
  students: set[str | None] = set()
384
+ layouts: set[bool] = set()
372
385
  for row in markup.rows(tables[0]):
373
386
  if DESCRIPTIVE_ROW_CLASS not in _classes(row):
374
387
  continue
375
388
  cells = markup.cells(row)
376
- if (
377
- len(cells) != 4
378
- or not {"micro", "center", "screen-only"} <= _classes(cells[0])
379
- or [cell.get("data-semester") for cell in cells[2:]] != ["1", "2"]
380
- or not all("gradesCell" in _classes(cell) for cell in cells[2:])
389
+ if len(cells) != 4 or not {"micro", "center", "screen-only"} <= _classes(
390
+ cells[0]
391
+ ):
392
+ raise LibrusError(ErrorKind.UNSUPPORTED_CAPABILITY)
393
+ semesters = cells[2:]
394
+ # A filled cell is bare text: no attributes and no markup, so a renamed
395
+ # or moved script hook never passes as a filled table.
396
+ filled = all(
397
+ not cell.attrib
398
+ and markup.text(cell)
399
+ and not any(isinstance(node.tag, str) for node in cell)
400
+ for cell in semesters
401
+ )
402
+ if not filled and (
403
+ [cell.get("data-semester") for cell in semesters] != ["1", "2"]
404
+ or not all("gradesCell" in _classes(cell) for cell in semesters)
381
405
  ):
382
406
  raise LibrusError(ErrorKind.UNSUPPORTED_CAPABILITY)
383
- if any(
407
+ layouts.add(filled)
408
+ if not filled and any(
384
409
  markup.text(cell) or any(isinstance(node.tag, str) for node in cell)
385
- for cell in cells[2:]
410
+ for cell in semesters
386
411
  ):
387
412
  raise LibrusError(ErrorKind.UNSUPPORTED_CAPABILITY)
388
413
  identifier = row.get("data-subject_id", "")
@@ -398,7 +423,15 @@ def read_descriptive_table(document: html.HtmlElement) -> DescriptiveTable | Non
398
423
  if len(students) > 1:
399
424
  # One page shows one student; rows of several students are unknown.
400
425
  raise LibrusError(ErrorKind.PARSE)
401
- return DescriptiveTable(MappingProxyType(subjects))
426
+ if len(layouts) > 1:
427
+ raise LibrusError(ErrorKind.UNSUPPORTED_CAPABILITY)
428
+ if layouts == {True} and any(
429
+ DESCRIPTIVE_DETAILS_ROW_CLASS in _classes(row) for row in markup.rows(tables[0])
430
+ ):
431
+ # Detail rows are what the page script fills from the gateway; a filled
432
+ # table that still has them may also have gateway grades.
433
+ raise LibrusError(ErrorKind.UNSUPPORTED_CAPABILITY)
434
+ return DescriptiveTable(MappingProxyType(subjects), layouts == {True})
402
435
 
403
436
 
404
437
  def parse_grade_page(body: bytes) -> tuple[GradeRecords, DescriptiveTable | None]:
@@ -8,6 +8,7 @@ from lxml import html
8
8
 
9
9
  from librus_python_api import markup
10
10
  from librus_python_api.config import (
11
+ BEHAVIOUR_GRADE_LABELS,
11
12
  BEHAVIOUR_POINTS_DETAIL_PATH_PREFIX,
12
13
  FORMATIVE_DETAIL_PATH_PREFIX,
13
14
  GRADE_AVERAGE_HEADERS,
@@ -26,6 +27,7 @@ from librus_python_api.config import (
26
27
  GRADE_PUBLICATION_DATE_LABEL,
27
28
  GRADE_PUBLICATION_PERIOD_LABELS,
28
29
  GRADE_PUBLICATION_TEACHER_LABEL,
30
+ TOOLTIP_LABELS,
29
31
  )
30
32
  from librus_python_api.exceptions import ErrorKind, LibrusError
31
33
  from librus_python_api.formative_grades import is_formative_table, read_formative_table
@@ -126,6 +128,51 @@ def _numeric(
126
128
  )
127
129
 
128
130
 
131
+ def _behaviour_grade(
132
+ element: html.HtmlElement,
133
+ subject: str,
134
+ semester: Literal[0, 1, 2],
135
+ kind: GradeKind = GradeKind.CURRENT,
136
+ ) -> NumericGrade:
137
+ """A behaviour entry with a grade symbol, such as `wz`, instead of points.
138
+
139
+ Its tooltip has the point entries' layout: an unlabelled first line, then
140
+ `Data wystawienia` and `Dodał`. The first line names the entry, as the
141
+ point category does, so it is the category. `Dodał` stays in metadata. A
142
+ label with digits, a point entry's range labels or any other label fails.
143
+ """
144
+ heading: list[str] = []
145
+ metadata = markup.tooltip_fields(element, heading)
146
+ raw = markup.text(element)
147
+ if (
148
+ not heading
149
+ # A labelled first line means the heading is missing.
150
+ or heading[0].partition(":")[0].strip() in TOOLTIP_LABELS
151
+ # Symbols carry no digits; points or a range are a point entry.
152
+ or not raw
153
+ or re.search(r"[0-9]", raw)
154
+ or not BEHAVIOUR_GRADE_LABELS >= metadata.keys()
155
+ ):
156
+ raise LibrusError(ErrorKind.PARSE)
157
+ href = element.get("href")
158
+ if href is not None and len(href) > GRADE_MAX_METADATA_LENGTH:
159
+ raise LibrusError(ErrorKind.LIMIT)
160
+ return NumericGrade(
161
+ subject,
162
+ raw,
163
+ markup.civil_date(metadata.get("Data wystawienia")),
164
+ semester,
165
+ None,
166
+ None,
167
+ heading[0],
168
+ None,
169
+ metadata.get("Komentarz"),
170
+ href,
171
+ tuple(metadata.items()),
172
+ kind,
173
+ )
174
+
175
+
129
176
  def _descriptive(
130
177
  box: html.HtmlElement,
131
178
  element: html.HtmlElement,
@@ -284,7 +331,21 @@ def _entries(
284
331
  if len(anchors) + len(descriptions) != 1:
285
332
  raise LibrusError(ErrorKind.PARSE)
286
333
  if anchors:
287
- record = _numeric(anchors[0], subject, period, kind)
334
+ behaviour = (
335
+ subject in GRADE_MERGED_SUBJECTS
336
+ and markup.detail_id(
337
+ anchors[0].get("href"), BEHAVIOUR_POINTS_DETAIL_PATH_PREFIX
338
+ )
339
+ is not None
340
+ )
341
+ if behaviour and descriptive:
342
+ # A descriptive record has no category for the first line.
343
+ raise LibrusError(ErrorKind.PARSE)
344
+ record = (
345
+ _behaviour_grade(anchors[0], subject, period, kind)
346
+ if behaviour
347
+ else _numeric(anchors[0], subject, period, kind)
348
+ )
288
349
  if descriptive:
289
350
  href = record.href
290
351
  if href is not None and re.sub(
@@ -540,6 +601,9 @@ def read_grade_records(document: html.HtmlElement) -> GradeRecords:
540
601
  _read_numeric_table(table, collection, points=True)
541
602
  else:
542
603
  for row in markup.rows(table):
604
+ if markup.in_header(row):
605
+ # The new-schema table's column labels, not a subject.
606
+ continue
543
607
  cells = markup.cells(row)
544
608
  if _descriptive_row(cells):
545
609
  _read_descriptive_row(cells, collection)
@@ -19,8 +19,8 @@ from librus_python_api import __version__
19
19
  from librus_python_api.config import TransportLimits
20
20
  from librus_python_api.diagnostics import describe_failure, failure_frames
21
21
  from librus_python_api.exceptions import LibrusError
22
+ from librus_python_api.gateway_grades import parse_grade_page
22
23
  from librus_python_api.grade_parsers import parse_final_grades
23
- from librus_python_api.grade_records import parse_grade_records
24
24
  from librus_python_api.models import Availability
25
25
  from librus_python_api.parsers import parse_profile
26
26
 
@@ -31,7 +31,8 @@ NOTICE = (
31
31
 
32
32
 
33
33
  def _grades(body: bytes) -> dict[str, int]:
34
- records = parse_grade_records(body)
34
+ # The all view also checks the new-schema table, as `grades()` does.
35
+ records, table = parse_grade_page(body)
35
36
  return {
36
37
  "numeric": sum(1 for g in records.numeric if g.point_grade_id is None),
37
38
  "point": sum(1 for g in records.numeric if g.point_grade_id is not None),
@@ -41,6 +42,8 @@ def _grades(body: bytes) -> dict[str, int]:
41
42
  "averages": len(records.averages),
42
43
  "point_results": len(records.point_results),
43
44
  "formative": len(records.formative),
45
+ "descriptive_table": int(table is not None),
46
+ "descriptive_table_filled": int(table is not None and table.filled),
44
47
  }
45
48
 
46
49
 
@@ -81,11 +81,14 @@ def colspan(cell: html.HtmlElement) -> int:
81
81
  return span
82
82
 
83
83
 
84
- def tooltip_fields(element: html.HtmlElement) -> dict[str, str]:
84
+ def tooltip_fields(
85
+ element: html.HtmlElement, heading: list[str] | None = None
86
+ ) -> dict[str, str]:
85
87
  """Parse a 'Label: value<br>' title attribute into unique labelled fields.
86
88
 
87
89
  A free-text field may span lines; an unlabelled line after it continues
88
- its value. Any other unlabelled line is an unknown layout.
90
+ its value. Any other unlabelled line is an unknown layout. With `heading`,
91
+ the first line is taken whole, colon included, and appended to it.
89
92
  """
90
93
  title = element.get("title", "")
91
94
  if len(title) > GRADE_MAX_METADATA_LENGTH:
@@ -98,12 +101,10 @@ def tooltip_fields(element: html.HtmlElement) -> dict[str, str]:
98
101
  for chunk in chunks:
99
102
  if not chunk.strip():
100
103
  continue
101
- # Tooltips are HTML inside an attribute; parse markup only where present.
102
- rendered = (
103
- text(parse_html_document(chunk.encode()))
104
- if "<" in chunk
105
- else " ".join(chunk.split())
106
- )
104
+ rendered = tooltip_line(chunk)
105
+ if heading is not None and not heading:
106
+ heading.append(rendered)
107
+ continue
107
108
  key, separator, value = rendered.partition(":")
108
109
  key, value = key.strip(), value.strip()
109
110
  if not separator and last in TOOLTIP_MULTILINE_LABELS:
@@ -116,6 +117,14 @@ def tooltip_fields(element: html.HtmlElement) -> dict[str, str]:
116
117
  return result
117
118
 
118
119
 
120
+ def tooltip_line(chunk: str) -> str:
121
+ """One tooltip line's text, whitespace normalized."""
122
+ # Tooltips are HTML inside an attribute; parse markup only where present.
123
+ if "<" in chunk:
124
+ return text(parse_html_document(chunk.encode()))
125
+ return " ".join(chunk.split())
126
+
127
+
119
128
  def civil_date(value: str | None) -> date:
120
129
  """An ISO date, optionally followed by its weekday label in parentheses."""
121
130
  match = re.fullmatch(r"([0-9]{4}-[0-9]{2}-[0-9]{2})(?: \(([^()]+)\))?", value or "")
@@ -394,6 +394,18 @@ def _require_dates(*days: date | None, max_days: int | None = None) -> None:
394
394
  raise LibrusError(ErrorKind.INVALID_INPUT)
395
395
 
396
396
 
397
+ def _check_record_count(records: GradeRecords, added: int) -> None:
398
+ if (
399
+ len(records.numeric)
400
+ + len(records.descriptive)
401
+ + len(records.descriptive_summaries)
402
+ + len(records.formative)
403
+ + added
404
+ > GRADE_MAX_RECORDS
405
+ ):
406
+ raise LibrusError(ErrorKind.LIMIT)
407
+
408
+
397
409
  def _cooldown_due(error: LibrusError) -> bool:
398
410
  """A denial, an unrecovered expiry, or a logged-out page that a session
399
411
  just signed in also got: signing in again at once cannot help."""
@@ -1328,7 +1340,7 @@ class AccountClient:
1328
1340
  records, table = await self._page(
1329
1341
  "grades", budget, parse_grade_page, form=form
1330
1342
  )
1331
- if table is not None:
1343
+ if table is not None and not table.filled:
1332
1344
  records = await self._gateway_descriptive(records, table, budget)
1333
1345
  return Grades(
1334
1346
  self._session_identity(), records, self._observation("grades"), view
@@ -2508,15 +2520,7 @@ class AccountClient:
2508
2520
  "descriptive_users", budget, parse_gateway_users, content_type=JSON
2509
2521
  )
2510
2522
  added = descriptive_grades(items, subjects, users, table)
2511
- if (
2512
- len(records.numeric)
2513
- + len(records.descriptive)
2514
- + len(records.descriptive_summaries)
2515
- + len(records.formative)
2516
- + len(added)
2517
- > GRADE_MAX_RECORDS
2518
- ):
2519
- raise LibrusError(ErrorKind.LIMIT)
2523
+ _check_record_count(records, len(added))
2520
2524
  return replace(records, descriptive=records.descriptive + added)
2521
2525
 
2522
2526
  async def _metadata_value(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: librus-python-api
3
- Version: 1.8.2
3
+ Version: 1.8.3
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,7 +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.8.2` keeps the modern mailbox session across a cancelled read
45
+ **Status:** `1.8.3` reads behaviour grade symbols, filled descriptive
46
+ grade tables, the "Aktualnie brak ogłoszeń" page and absences excused
47
+ through e-Usprawiedliwienia. `1.8.2` keeps the modern mailbox session across a cancelled read
46
48
  or a reached limit and sends its own version in the `User-Agent`. `1.8.1` recovers a session Synergia no longer knows (it answers
47
49
  with a "Brak dostępu" page, not a login redirect), reads the lucky number
48
50
  from the page header and summarizes a page that fails as a whole in
@@ -64,7 +66,7 @@ See [limitations](#supported-features-and-limitations) below.
64
66
  Install the exact release from PyPI after its gated publication completes:
65
67
 
66
68
  ```sh
67
- python -m pip install librus-python-api==1.8.2
69
+ python -m pip install librus-python-api==1.8.3
68
70
  ```
69
71
 
70
72
  Pin the exact version qualified by your application. Before publication, use a
@@ -80,7 +82,7 @@ python -m pip install .
80
82
  To install a locally built wheel instead:
81
83
 
82
84
  ```sh
83
- python -m pip install ./dist/librus_python_api-1.8.2-py3-none-any.whl
85
+ python -m pip install ./dist/librus_python_api-1.8.3-py3-none-any.whl
84
86
  ```
85
87
 
86
88
  No console script or background process is installed: import the library in your
@@ -1,4 +1,4 @@
1
- librus_python_api/__init__.py,sha256=CDi6pTko967g3RViVcZCIWhE-Lzof5VN6E4NkQJ4aUU,6571
1
+ librus_python_api/__init__.py,sha256=Ts6HZg9LRMp-MLMbjajwEdNlxVsmW8TnnGKbnzytP1k,6571
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
@@ -6,25 +6,25 @@ librus_python_api/_windows_filesystem.py,sha256=pQANIGkYrb4Q75gz5mdr5mqYhM_PFblW
6
6
  librus_python_api/announcements.py,sha256=13FoOR1sq7tt9w6aLf0vclcFKDqLUiIYaO5iYD2QVLA,5928
7
7
  librus_python_api/attachment_routes.py,sha256=uSg5wCQqHJiphF0KdI0GatQxZr5SLUx6xWQRsl_U4_U,4123
8
8
  librus_python_api/attachments.py,sha256=Iq-ERZWc3gYXu6Lcv6womT2EXCc-c1Q7r_Q6VyaCm2E,10818
9
- librus_python_api/attendance.py,sha256=HeFAfnIUn4N__yp1zr635Dve55S2jmwtkeIy_6mV5B4,6924
9
+ librus_python_api/attendance.py,sha256=Dq2IC8WERO-9x-LZIN5Nl0PTLYmL1Iq7BZtOz6nssOI,7428
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
13
  librus_python_api/class_free_days.py,sha256=IwoCgr-9ZgEYYQ6717s4cDpyFzbosHMzESMabXJLGqA,2422
14
14
  librus_python_api/completed_lessons.py,sha256=ngGfBD6K4B6dR4elPYvNRvp40Q-GlrhUsUKqBbYY1KE,10727
15
- librus_python_api/config.py,sha256=CBMwuuACfrB7TzTVkTP8QAuPz5hv43YOsbto81-uEJc,61248
15
+ librus_python_api/config.py,sha256=Sz6b5lWllzsKSbCbfGKqN6z-CZfwbCt9MkD2y-OG9CU,62019
16
16
  librus_python_api/detail_fields.py,sha256=vDDi5sV-e30-42SF0hwAUoldiZ5wvYtb1UlLt03lBHw,1708
17
17
  librus_python_api/diagnostics.py,sha256=5YbJB8J8RXMqMTmulcn5mJRF1vpi0Pvx00ETrXn9hUY,19036
18
18
  librus_python_api/exceptions.py,sha256=x8_z5UTNmuRINTK4buLYiKdYcFA2w6GZP9ZXOL-OnRU,5531
19
19
  librus_python_api/files.py,sha256=pQBsIJyfTOZDz83ZB-5fcd-nPMcxR1HJ155h4zTHmsA,9223
20
20
  librus_python_api/formative_grades.py,sha256=VykwtGvViZVOJMwPH3Re1RTDCHMr1_m9_xHjZyaXDIQ,6452
21
- librus_python_api/gateway_grades.py,sha256=T-G5KN0kpROi6xSGwAUba2qWDGNhg8TeGmQN140iLxc,16163
21
+ librus_python_api/gateway_grades.py,sha256=uaCCz743aPlymAa6rDfRb0w2JFC2hJ4yis0hbKdqTeM,17663
22
22
  librus_python_api/grade_parsers.py,sha256=ROfffkCP8yj5Q708TZdpp0PDTyVmoh6X7m0y2fyJcwg,11202
23
- librus_python_api/grade_records.py,sha256=YltZ_OQLsfxj0nvK8_xTuv8ifMOu2jJBnVn6TTQKzaY,22722
23
+ librus_python_api/grade_records.py,sha256=IT6A5_09Zu5h03g_p3pHVNepFCMoWag0WIJKg4A9MlQ,24978
24
24
  librus_python_api/homework_range.py,sha256=erYfvS6PhHU5XhnR6LkiLNy677hGVflY3UAuHOs-YUw,1663
25
- librus_python_api/inspect_page.py,sha256=AUm-n2Ewe4cQUnlFnhttcGK5B9shvFde_7t1JF7mEm0,5285
25
+ librus_python_api/inspect_page.py,sha256=W5M21v0DlFw3eaqzwGG6hsI6fkTZVQTwkO2x-NNk4y0,5490
26
26
  librus_python_api/lifecycle.py,sha256=XzA5CEdZFo7iL5IMUrawnRvf2m9BFe0OseP-fpQFaAE,856
27
- librus_python_api/markup.py,sha256=p97ZRPqhGYTEBbzJq_0j4j8AKihgQzJqTv__QUNm8Ws,6084
27
+ librus_python_api/markup.py,sha256=AS0hfJS83lnOUpyfgoNHwUKf8MKBR3WN3p_4aeZ7BOk,6408
28
28
  librus_python_api/message_content.py,sha256=v_qoEAoaYS0_9MQBaj57i5XnC7pzv_HHWefgTt4CNyM,9043
29
29
  librus_python_api/messages.py,sha256=K9F8XVs1SojfFpdxxLV8gx9E0g81YxEHjigDzLHmDCg,10230
30
30
  librus_python_api/models.py,sha256=IO-SYpqpQeuTej6kqNjBRXDbAbKITRZ1njRUyoEZ8t0,41951
@@ -44,10 +44,10 @@ librus_python_api/scheduler.py,sha256=3JplYxqvQdFz3aKXcoQUJHE9s3FPw_xRhQ2CPNLuQm
44
44
  librus_python_api/school_reads.py,sha256=YHta7DmhjS5vSa4Djq8pzp01-2jhVfGOo1Mnc738rV4,15209
45
45
  librus_python_api/school_year_archive.py,sha256=D0A1ywMKgoYgvi-d70__SFreMGvMb_72870Akkbb_m8,15492
46
46
  librus_python_api/sending.py,sha256=Nvvk2r-6D6Us53a2fH5GW9PcA6bWEfuOvy6c3M5QVcE,4549
47
- librus_python_api/service.py,sha256=34o-EQSuERhSHZAme-RDhVznhd_y1-GT0SPfduJ5LoM,102964
47
+ librus_python_api/service.py,sha256=GaavbI6117fcbVuWDzZXDbbgQUczZXXFVNKEMXsFfaw,103063
48
48
  librus_python_api/timetable.py,sha256=tX1QK9fQABxzfLaHHqTe8e6rAgdOkCoTOfAHdPwNmLE,9836
49
49
  librus_python_api/transport.py,sha256=yF-DnFy4xGYS_RWQ9161b9WJXOlYF8cFRgdjXmVVUIk,34988
50
- librus_python_api-1.8.2.dist-info/METADATA,sha256=78cqj_uSkFU-4NhnUV0FCj3U8-QmJRK51bvIzIRVask,14202
51
- librus_python_api-1.8.2.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
52
- librus_python_api-1.8.2.dist-info/licenses/LICENSE,sha256=IuerLb3cnZCsaB23znydjjvKqP-LmSJd3syKa9NGUts,1071
53
- librus_python_api-1.8.2.dist-info/RECORD,,
50
+ librus_python_api-1.8.3.dist-info/METADATA,sha256=PwGb0FBnKt-moy91gG0veDWgZZzMcEtymgI-BMFU75k,14361
51
+ librus_python_api-1.8.3.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
52
+ librus_python_api-1.8.3.dist-info/licenses/LICENSE,sha256=IuerLb3cnZCsaB23znydjjvKqP-LmSJd3syKa9NGUts,1071
53
+ librus_python_api-1.8.3.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.32.4
2
+ Generator: hatchling 1.33.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any