librus-python-api 1.3.0__py3-none-any.whl → 1.4.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.
@@ -44,6 +44,7 @@ from librus_python_api.models import (
44
44
  DetailField,
45
45
  DetailFieldKey,
46
46
  FinalGrades,
47
+ FormativeGrade,
47
48
  FrequencyMeasure,
48
49
  GatewayAttendance,
49
50
  GatewayAttendanceRecord,
@@ -135,7 +136,7 @@ from librus_python_api.models import (
135
136
  from librus_python_api.sending import SendAttempt
136
137
  from librus_python_api.service import AccountClient, LibrusService
137
138
 
138
- __version__ = "1.3.0"
139
+ __version__ = "1.4.0"
139
140
 
140
141
  __all__ = [
141
142
  "ArchiveAbsences",
@@ -251,6 +252,7 @@ __all__ = [
251
252
  "DescriptiveGrade",
252
253
  "DescriptiveGradeSummary",
253
254
  "FinalGrades",
255
+ "FormativeGrade",
254
256
  "GradeKind",
255
257
  "GradeRecords",
256
258
  "Grades",
@@ -721,6 +721,18 @@ ATTENDANCE_MAX_LESSONS = 4096
721
721
  ATTENDANCE_MAX_SUBJECTS = 1024
722
722
  # Collection recognition and detail retrieval share this central path family.
723
723
  ATTENDANCE_DETAIL_PATH_PREFIX = "/przegladaj_nb/szczegoly/"
724
+ # Recognition only: formative detail links are classified, never requested.
725
+ FORMATIVE_DETAIL_PATH_PREFIX = "/przegladaj_oceny/szczegoly/ksztaltujace/"
726
+ FORMATIVE_HEADERS = (
727
+ "przedmiot",
728
+ "ocena kształtująca",
729
+ "kategoria",
730
+ "okres",
731
+ "data",
732
+ "typ",
733
+ )
734
+ FORMATIVE_TEMPLATE_ID = "000000"
735
+ FORMATIVE_MAX_TEXT_LENGTH = 65536
724
736
  ATTENDANCE_DETAIL_MAX_FIELDS = 32
725
737
  ATTENDANCE_METADATA_CACHE_SIZE = 256
726
738
  ATTENDANCE_RESULT_CACHE_SIZE = 64
@@ -0,0 +1,173 @@
1
+ """The grades page's formative-assessment table ("Oceny kształtujące").
2
+
3
+ Authored from an owner-authorized structural observation; fixtures are
4
+ synthetic. The table sits on the page `grades()` already reads, so no request
5
+ is added. Its subject may be the pseudo-subject "KARTA SPOSTRZEŻEŃ". Unknown
6
+ markup is unsupported; contradictions inside the known layout are parse errors.
7
+ """
8
+
9
+ import re
10
+ from collections.abc import Callable
11
+ from typing import Literal, NoReturn, cast
12
+
13
+ from lxml import html
14
+
15
+ from librus_python_api import markup
16
+ from librus_python_api.config import (
17
+ FORMATIVE_DETAIL_PATH_PREFIX,
18
+ FORMATIVE_HEADERS,
19
+ FORMATIVE_MAX_TEXT_LENGTH,
20
+ FORMATIVE_TEMPLATE_ID,
21
+ GRADE_MAX_RECORDS,
22
+ )
23
+ from librus_python_api.exceptions import ErrorKind, LibrusError
24
+ from librus_python_api.models import FormativeGrade
25
+
26
+ _SPAN = re.compile(r"[1-9][0-9]{0,5}")
27
+
28
+
29
+ def _fail(kind: ErrorKind) -> NoReturn:
30
+ raise LibrusError(kind)
31
+
32
+
33
+ def is_formative_table(table: html.HtmlElement) -> bool:
34
+ # Cheap structural gates first: other tables' header text is never read.
35
+ if not {"stretch", "decorated"} <= set(table.get("class", "").split()):
36
+ return False
37
+ heads = [row for row in markup.rows(table) if markup.in_header(row)]
38
+ if len(heads) != 1:
39
+ return False
40
+ cells = markup.cells(heads[0])
41
+ return len(cells) == len(FORMATIVE_HEADERS) and [
42
+ markup.text(cell).casefold() for cell in cells
43
+ ] == list(FORMATIVE_HEADERS)
44
+
45
+
46
+ def _single(cell: html.HtmlElement) -> None:
47
+ if cell.get("colspan", "1") != "1" or cell.get("rowspan", "1") != "1":
48
+ _fail(ErrorKind.UNSUPPORTED_CAPABILITY)
49
+
50
+
51
+ def _plain(cell: html.HtmlElement) -> str:
52
+ _single(cell)
53
+ if any(isinstance(node.tag, str) for node in cell.iterdescendants()):
54
+ _fail(ErrorKind.UNSUPPORTED_CAPABILITY)
55
+ return markup.text(cell)
56
+
57
+
58
+ def _link(cell: html.HtmlElement) -> tuple[str, str]:
59
+ """The detail ID and full text of the assessment cell.
60
+
61
+ Observed: one link whose optional first child is an empty coloured
62
+ `span.grade-box` marker, followed by the text. Anything else is new markup.
63
+ """
64
+ _single(cell)
65
+ anchors = [node for node in cell if isinstance(node.tag, str)]
66
+ if len(anchors) != 1 or anchors[0].tag != "a":
67
+ _fail(ErrorKind.UNSUPPORTED_CAPABILITY)
68
+ anchor = anchors[0]
69
+ children = [node for node in anchor.iterdescendants() if isinstance(node.tag, str)]
70
+ if children:
71
+ marker = children[0]
72
+ if (
73
+ len(children) != 1
74
+ or marker.getparent() is not anchor
75
+ or anchor.index(marker) != 0
76
+ or marker.tag != "span"
77
+ or "grade-box" not in marker.get("class", "").split()
78
+ or markup.text(marker)
79
+ ):
80
+ _fail(ErrorKind.UNSUPPORTED_CAPABILITY)
81
+ identifier = markup.detail_id(anchor.get("href"), FORMATIVE_DETAIL_PATH_PREFIX)
82
+ if identifier is None:
83
+ _fail(ErrorKind.UNSUPPORTED_CAPABILITY)
84
+ return identifier, markup.text(anchor, FORMATIVE_MAX_TEXT_LENGTH)
85
+
86
+
87
+ def _template(row: html.HtmlElement) -> bool:
88
+ """A hidden, empty template entry (ID 000000) rendered for scripts.
89
+
90
+ Not observed inside this table (the page's observed template is an ordinary
91
+ grade link in a separate hidden table); kept narrow on purpose.
92
+ """
93
+ hidden = "display:none" in row.get("style", "").replace(" ", "").casefold()
94
+ links = [
95
+ markup.detail_id(anchor.get("href"), FORMATIVE_DETAIL_PATH_PREFIX)
96
+ for anchor in row.iter("a")
97
+ ]
98
+ if not hidden or links != [FORMATIVE_TEMPLATE_ID]:
99
+ return False
100
+ if any(markup.text(anchor) for anchor in row.iter("a")):
101
+ _fail(ErrorKind.PARSE)
102
+ return True
103
+
104
+
105
+ def _item(subject: str, cells: list[html.HtmlElement]) -> FormativeGrade:
106
+ if len(cells) != 5:
107
+ _fail(ErrorKind.UNSUPPORTED_CAPABILITY)
108
+ identifier, text = _link(cells[0])
109
+ category = _plain(cells[1])
110
+ period = _plain(cells[2])
111
+ day = markup.civil_date(_plain(cells[3]))
112
+ kind = _plain(cells[4])
113
+ if not text or period not in ("1", "2") or identifier == FORMATIVE_TEMPLATE_ID:
114
+ _fail(ErrorKind.PARSE)
115
+ return FormativeGrade(
116
+ subject,
117
+ text,
118
+ category,
119
+ cast(Literal[1, 2], int(period)),
120
+ day,
121
+ kind,
122
+ identifier,
123
+ )
124
+
125
+
126
+ def read_formative_table(
127
+ table: html.HtmlElement, check_size: Callable[[int], None]
128
+ ) -> tuple[FormativeGrade, ...]:
129
+ """Rows in page order; a subject TH may span the rows of its group."""
130
+ if any(nested is not table for nested in table.iter("table")):
131
+ _fail(ErrorKind.PARSE)
132
+ items: list[FormativeGrade] = []
133
+ seen: set[str] = set()
134
+ subject, remaining = "", 0
135
+ for row in markup.rows(table):
136
+ if markup.in_header(row):
137
+ continue
138
+ cells = markup.cells(row)
139
+ if next(row.iterancestors("tfoot"), None) is not None:
140
+ if len(cells) != 1 or markup.text(cells[0]):
141
+ _fail(ErrorKind.PARSE)
142
+ continue
143
+ if _template(row):
144
+ continue
145
+ if remaining == 0:
146
+ if not cells or cells[0].tag != "th":
147
+ _fail(ErrorKind.UNSUPPORTED_CAPABILITY)
148
+ head = cells[0]
149
+ span = head.get("rowspan", "1")
150
+ if head.get("colspan", "1") != "1" or not _SPAN.fullmatch(span):
151
+ _fail(ErrorKind.UNSUPPORTED_CAPABILITY)
152
+ remaining = int(span)
153
+ if remaining > GRADE_MAX_RECORDS:
154
+ _fail(ErrorKind.LIMIT)
155
+ if any(isinstance(node.tag, str) for node in head.iterdescendants()):
156
+ _fail(ErrorKind.UNSUPPORTED_CAPABILITY)
157
+ subject = markup.text(head)
158
+ if not subject:
159
+ _fail(ErrorKind.PARSE)
160
+ cells = cells[1:]
161
+ elif cells and cells[0].tag == "th":
162
+ # A new subject before the spanning group ended.
163
+ _fail(ErrorKind.PARSE)
164
+ remaining -= 1
165
+ item = _item(subject, cells)
166
+ if item.detail_id in seen:
167
+ _fail(ErrorKind.PARSE)
168
+ seen.add(item.detail_id)
169
+ items.append(item)
170
+ check_size(len(items))
171
+ if remaining:
172
+ _fail(ErrorKind.PARSE)
173
+ return tuple(items)
@@ -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
+ FORMATIVE_DETAIL_PATH_PREFIX,
11
12
  GRADE_AVERAGE_HEADERS,
12
13
  GRADE_BODY_PREFIX_COLUMNS,
13
14
  GRADE_CURRENT_HEADER,
@@ -23,11 +24,13 @@ from librus_python_api.config import (
23
24
  GRADE_PUBLICATION_TEACHER_LABEL,
24
25
  )
25
26
  from librus_python_api.exceptions import ErrorKind, LibrusError
27
+ from librus_python_api.formative_grades import is_formative_table, read_formative_table
26
28
  from librus_python_api.grade_parsers import _locate, _subject_rows, _summary
27
29
  from librus_python_api.models import (
28
30
  Availability,
29
31
  DescriptiveGrade,
30
32
  DescriptiveGradeSummary,
33
+ FormativeGrade,
31
34
  GradeKind,
32
35
  GradeRecords,
33
36
  GradeSummaryValue,
@@ -138,14 +141,22 @@ class _Collection:
138
141
  subjects: set[tuple[bool, str]] = field(default_factory=set, repr=False)
139
142
  consumed: set[html.HtmlElement] = field(default_factory=set, repr=False)
140
143
  publication_rows: set[html.HtmlElement] = field(default_factory=set, repr=False)
144
+ formative: int = 0
141
145
 
142
146
  def check_size(self) -> None:
143
147
  if (
144
- len(self.numeric) + len(self.descriptive) + len(self.descriptive_summaries)
148
+ len(self.numeric)
149
+ + len(self.descriptive)
150
+ + len(self.descriptive_summaries)
151
+ + self.formative
145
152
  > GRADE_MAX_RECORDS
146
153
  ):
147
154
  raise LibrusError(ErrorKind.LIMIT)
148
155
 
156
+ def count_formative(self, count: int) -> None:
157
+ self.formative = count
158
+ self.check_size()
159
+
149
160
  def subject(self, value: str, *, descriptive: bool = False) -> None:
150
161
  key = (descriptive, value)
151
162
  if not value or key in self.subjects:
@@ -400,7 +411,14 @@ def parse_grade_records(body: bytes) -> GradeRecords:
400
411
  numeric_tables.append(table)
401
412
  if len(numeric_tables) > 1:
402
413
  raise LibrusError(ErrorKind.PARSE)
414
+ formative_tables = [table for table in tables if is_formative_table(table)]
415
+ if len(formative_tables) > 1:
416
+ raise LibrusError(ErrorKind.PARSE)
417
+ formative: tuple[FormativeGrade, ...] = ()
403
418
  for table in tables:
419
+ if table in formative_tables:
420
+ formative = read_formative_table(table, collection.count_formative)
421
+ continue
404
422
  _read_publications(table, collection)
405
423
  if table in numeric_tables:
406
424
  _read_numeric_table(table, collection)
@@ -434,6 +452,15 @@ def parse_grade_records(body: bytes) -> GradeRecords:
434
452
  ):
435
453
  continue
436
454
  raise LibrusError(ErrorKind.UNSUPPORTED_CAPABILITY)
455
+ for anchor in document.iter("a"):
456
+ # A formative link outside both known tables is a new layout.
457
+ if markup.detail_id(
458
+ anchor.get("href"), FORMATIVE_DETAIL_PATH_PREFIX
459
+ ) and not any(
460
+ parent in numeric_tables or parent in formative_tables
461
+ for parent in anchor.iterancestors("table")
462
+ ):
463
+ raise LibrusError(ErrorKind.UNSUPPORTED_CAPABILITY)
437
464
  if not collection.subjects and not collection.descriptive:
438
465
  raise LibrusError(ErrorKind.PARSE)
439
466
  for descriptive, subject in sorted(collection.subjects):
@@ -451,4 +478,5 @@ def parse_grade_records(body: bytes) -> GradeRecords:
451
478
  tuple(collection.descriptive),
452
479
  tuple(collection.averages),
453
480
  tuple(collection.descriptive_summaries),
481
+ formative,
454
482
  )
@@ -131,8 +131,19 @@ def attendance_detail_id(element: html.HtmlElement) -> str | None:
131
131
  )
132
132
  if match is None:
133
133
  return None
134
+ return detail_id(match[2], ATTENDANCE_DETAIL_PATH_PREFIX)
135
+
136
+
137
+ def detail_id(link: str | None, prefix: str) -> str | None:
138
+ """The numeric ID of a relative or same-origin Synergia detail link.
139
+
140
+ Recognition only: the link is never followed. Queries, fragments, other
141
+ origins, credentials and non-digit IDs are not detail links.
142
+ """
143
+ if link is None or len(link) > GRADE_MAX_METADATA_LENGTH:
144
+ return None
134
145
  try:
135
- target = urlsplit(match[2])
146
+ target = urlsplit(link)
136
147
  except ValueError:
137
148
  return None
138
149
  if target.query or target.fragment or target.username or target.password:
@@ -141,7 +152,7 @@ def attendance_detail_id(element: html.HtmlElement) -> str | None:
141
152
  expected = urlsplit(UPSTREAM_ORIGINS["synergia"])
142
153
  if (target.scheme, target.netloc) != (expected.scheme, expected.netloc):
143
154
  return None
144
- if not target.path.startswith(ATTENDANCE_DETAIL_PATH_PREFIX):
155
+ if not target.path.startswith(prefix):
145
156
  return None
146
- identifier = target.path[len(ATTENDANCE_DETAIL_PATH_PREFIX) :]
157
+ identifier = target.path[len(prefix) :]
147
158
  return identifier if re.fullmatch(r"[0-9]{1,64}", identifier) else None
@@ -1019,6 +1019,19 @@ class NumericGrade:
1019
1019
  metadata: tuple[tuple[str, str], ...] = field(repr=False)
1020
1020
  kind: GradeKind = GradeKind.CURRENT
1021
1021
 
1022
+ @property
1023
+ def formative_id(self) -> str | None:
1024
+ """The formative-assessment ID when `href` is a formative detail link.
1025
+
1026
+ Computed, not a field: canonical notification IDs hash the record's
1027
+ fields, so stored identities stay unchanged.
1028
+ """
1029
+ # Lazy import: config imports this module.
1030
+ from librus_python_api.config import FORMATIVE_DETAIL_PATH_PREFIX
1031
+ from librus_python_api.markup import detail_id
1032
+
1033
+ return detail_id(self.href, FORMATIVE_DETAIL_PATH_PREFIX)
1034
+
1022
1035
 
1023
1036
  @dataclass(frozen=True, slots=True)
1024
1037
  class DescriptiveGrade:
@@ -1032,6 +1045,19 @@ class DescriptiveGrade:
1032
1045
  kind: GradeKind = GradeKind.CURRENT
1033
1046
  href: str | None = field(default=None, repr=False)
1034
1047
 
1048
+ @property
1049
+ def formative_id(self) -> str | None:
1050
+ """The formative-assessment ID when `href` is a formative detail link.
1051
+
1052
+ Computed, not a field: canonical notification IDs hash the record's
1053
+ fields, so stored identities stay unchanged.
1054
+ """
1055
+ # Lazy import: config imports this module.
1056
+ from librus_python_api.config import FORMATIVE_DETAIL_PATH_PREFIX
1057
+ from librus_python_api.markup import detail_id
1058
+
1059
+ return detail_id(self.href, FORMATIVE_DETAIL_PATH_PREFIX)
1060
+
1035
1061
 
1036
1062
  @dataclass(frozen=True, slots=True)
1037
1063
  class SchoolAverage:
@@ -1051,6 +1077,22 @@ class DescriptiveGradeSummary:
1051
1077
  raw: str = field(repr=False)
1052
1078
 
1053
1079
 
1080
+ @dataclass(frozen=True, slots=True)
1081
+ class FormativeGrade:
1082
+ """One row of the grades page's formative-assessment ("Oceny kształtujące")
1083
+ table. "KARTA SPOSTRZEŻEŃ" (observation card) appears as a subject value.
1084
+ The same item may also appear in the grid with a matching `formative_id`.
1085
+ """
1086
+
1087
+ subject: str = field(repr=False)
1088
+ text: str = field(repr=False)
1089
+ category: str = field(repr=False)
1090
+ semester: Literal[1, 2]
1091
+ day: date = field(repr=False)
1092
+ assessment_type: str = field(repr=False)
1093
+ detail_id: str = field(repr=False)
1094
+
1095
+
1054
1096
  @dataclass(frozen=True, slots=True)
1055
1097
  class GradeRecords:
1056
1098
  numeric: tuple[NumericGrade, ...] = field(repr=False)
@@ -1059,6 +1101,7 @@ class GradeRecords:
1059
1101
  descriptive_summaries: tuple[DescriptiveGradeSummary, ...] = field(
1060
1102
  default=(), repr=False
1061
1103
  )
1104
+ formative: tuple[FormativeGrade, ...] = field(default=(), repr=False)
1062
1105
 
1063
1106
 
1064
1107
  @dataclass(frozen=True, slots=True)
@@ -1078,6 +1121,7 @@ class GradeWindow:
1078
1121
  descriptive: tuple[DescriptiveGrade, ...] = field(repr=False)
1079
1122
  observation: Observation
1080
1123
  view: GradeView = GradeView.ALL
1124
+ formative: tuple[FormativeGrade, ...] = field(default=(), repr=False)
1081
1125
 
1082
1126
 
1083
1127
  @dataclass(frozen=True, slots=True)
@@ -1307,6 +1307,11 @@ class AccountClient:
1307
1307
  ),
1308
1308
  result.observation,
1309
1309
  result.view,
1310
+ tuple(
1311
+ g
1312
+ for g in result.records.formative
1313
+ if (start is None or start <= g.day) and (end is None or g.day <= end)
1314
+ ),
1310
1315
  )
1311
1316
 
1312
1317
  async def attendance(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: librus-python-api
3
- Version: 1.3.0
3
+ Version: 1.4.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.3.0` adds a read-only school-year archive.
46
- Installed live checks passed for populated and empty
47
- archives on two parent logins; see [verification](VERIFICATION.md).
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).
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.3.0
57
+ python -m pip install librus-python-api==1.4.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.3.0-py3-none-any.whl
73
+ python -m pip install ./dist/librus_python_api-1.4.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.
@@ -245,8 +245,8 @@ removed in 0.7.
245
245
  | Communication | Legacy/modern message lists and content, recipient discovery, bounded attachment streams and explicit sending |
246
246
  | Application workflows | Shared multi-account limits, caching, notification checkpoints, optional durable send/notification stores |
247
247
 
248
- Completed lessons may be disabled by the school. Behaviour notes and observation
249
- cards are not implemented. Some recipient, archive and receipt layouts remain
248
+ Completed lessons may be disabled by the school. Behaviour notes are not
249
+ implemented. Some recipient, archive and receipt layouts remain
250
250
  unqualified; backend acceptance is not proof of delivery. Tests cover supported
251
251
  contracts, not every school or role. Detailed coverage is in the
252
252
  [verification log][verification] and [roadmap][roadmap].
@@ -1,4 +1,4 @@
1
- librus_python_api/__init__.py,sha256=XnV4fql56WvZHQuk3zrUHlQ4mtK0272dM07QFKyINU4,6393
1
+ librus_python_api/__init__.py,sha256=Ka6_hAV-fnh3QrjAPyqhWPvaG2eh5w8VT8a5aB_RBNU,6435
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,19 +11,20 @@ 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=8DckId5C13QH9KPelZwPfsci8upoqb_SJBtq7XviPPY,54628
14
+ librus_python_api/config.py,sha256=zhE2-v3FehMvvsbxMtK1J6iliOp8jYwb4g-e-rFCMEQ,54968
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
18
18
  librus_python_api/files.py,sha256=pQBsIJyfTOZDz83ZB-5fcd-nPMcxR1HJ155h4zTHmsA,9223
19
+ librus_python_api/formative_grades.py,sha256=-F-d1rnFVAIGwg1mN0cryCIBR76OW3v2L4mCWUZ3M7Q,6227
19
20
  librus_python_api/grade_parsers.py,sha256=91Kr1-TArzqO-b94ipeIR0a98OV7tRCZQdBWsd9QLZ0,6141
20
- librus_python_api/grade_records.py,sha256=HvGHQ8Fyx_1Ne5k6N8cBeTqrDCzM5P6saj8eijDbRA4,16793
21
+ librus_python_api/grade_records.py,sha256=K0ZzmU1KsA8Jo1MXh30mStApgrigaOKk5ldoBnwRrH8,17901
21
22
  librus_python_api/homework_range.py,sha256=erYfvS6PhHU5XhnR6LkiLNy677hGVflY3UAuHOs-YUw,1663
22
23
  librus_python_api/lifecycle.py,sha256=XzA5CEdZFo7iL5IMUrawnRvf2m9BFe0OseP-fpQFaAE,856
23
- librus_python_api/markup.py,sha256=GZk7MmBBGB5SIVY_VngmQxQErI3eGHOpJs3mFVlMR3M,5298
24
+ librus_python_api/markup.py,sha256=mjCZf6KJTWUz2vjfXbJ4zM17hnev7a5AFIOW3ip3gsE,5677
24
25
  librus_python_api/message_content.py,sha256=sUPuqpjuavqDyn6QGM5aPH4mpoQV62bDLepSSQ2AsUE,8686
25
26
  librus_python_api/messages.py,sha256=K9F8XVs1SojfFpdxxLV8gx9E0g81YxEHjigDzLHmDCg,10230
26
- librus_python_api/models.py,sha256=wD4Wg7Jq0dYU2YA8FVztz0EguueN2cue0V0dbvxxJkw,38548
27
+ librus_python_api/models.py,sha256=wWMw2TXmeRWP3yJLSDmdVDYsbXRIztm3mEhCYPH1gco,40310
27
28
  librus_python_api/modern_body.py,sha256=3L4P_cIrHdIxZqiinFSQrDMhG6iklT1GoA-VEWMJUiI,2682
28
29
  librus_python_api/modern_mailbox.py,sha256=2jF9UnJZ5AF6dip-GrMcv_z2kSLpp87dIpVeXk9jMzA,20600
29
30
  librus_python_api/modern_messages.py,sha256=qt2CXtrxCZQ-jTt0aQCD6ZaPAJQZXf0BxmvacPo9B0w,10653
@@ -40,10 +41,10 @@ librus_python_api/scheduler.py,sha256=3JplYxqvQdFz3aKXcoQUJHE9s3FPw_xRhQ2CPNLuQm
40
41
  librus_python_api/school_reads.py,sha256=PCnZ4lv44j-68FgJpQiMteaPSyfhbX8_3CUyE6zhFcQ,14760
41
42
  librus_python_api/school_year_archive.py,sha256=D0A1ywMKgoYgvi-d70__SFreMGvMb_72870Akkbb_m8,15492
42
43
  librus_python_api/sending.py,sha256=Nvvk2r-6D6Us53a2fH5GW9PcA6bWEfuOvy6c3M5QVcE,4549
43
- librus_python_api/service.py,sha256=0nu0XEVEnPFZ6gaeDN7Ao9WPmkDfuYbz8nW_Eg-y78I,94275
44
+ librus_python_api/service.py,sha256=yjdKc4-psMDlIHVLud-WA2YDXafKPuTT-hLdVmdWsIM,94464
44
45
  librus_python_api/timetable.py,sha256=tX1QK9fQABxzfLaHHqTe8e6rAgdOkCoTOfAHdPwNmLE,9836
45
46
  librus_python_api/transport.py,sha256=QnYXnrv1lW4qToYoTv6HyZw-Lt4rDc-_SMTJpMp6_bc,35297
46
- librus_python_api-1.3.0.dist-info/METADATA,sha256=7LcWlfUprMbkEQZbGWQHxj_p6mhrieVWk30-hAaClQ0,11990
47
- librus_python_api-1.3.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
48
- librus_python_api-1.3.0.dist-info/licenses/LICENSE,sha256=IuerLb3cnZCsaB23znydjjvKqP-LmSJd3syKa9NGUts,1071
49
- librus_python_api-1.3.0.dist-info/RECORD,,
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,,