librus-python-api 1.7.0__tar.gz → 1.7.1__tar.gz
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.
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/CHANGELOG.md +18 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/PKG-INFO +5 -5
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/README.md +4 -4
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/__init__.py +1 -1
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/config.py +3 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/formative_grades.py +6 -1
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/grade_parsers.py +28 -8
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/grade_records.py +17 -11
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/markup.py +11 -1
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/.gitignore +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/API.md +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/LICENSE +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/SECURITY.md +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/pyproject.toml +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/_notification_bootstrap.py +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/_notification_codec.py +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/_storage.py +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/_windows_filesystem.py +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/announcements.py +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/attachment_routes.py +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/attachments.py +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/attendance.py +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/attendance_frequency.py +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/budget.py +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/checkpoint.py +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/class_free_days.py +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/completed_lessons.py +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/detail_fields.py +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/diagnostics.py +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/exceptions.py +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/files.py +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/homework_range.py +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/lifecycle.py +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/message_content.py +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/messages.py +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/models.py +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/modern_body.py +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/modern_mailbox.py +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/modern_messages.py +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/notification_models.py +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/notification_persistence.py +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/notification_workflow.py +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/notifications.py +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/parsers.py +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/parsing.py +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/persistence.py +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/py.typed +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/recipients.py +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/scheduler.py +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/school_reads.py +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/school_year_archive.py +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/sending.py +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/service.py +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/timetable.py +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/transport.py +0 -0
- {librus_python_api-1.7.0 → librus_python_api-1.7.1}/uv.lock +0 -0
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.7.1 (2026-10-27) - Observed point-grade page and multi-line comments
|
|
4
|
+
|
|
5
|
+
Fixes for layouts 1.7.0 rejected with `ParseError`, established from an
|
|
6
|
+
affected school's real point-grade page and a live grade read. Public types
|
|
7
|
+
and signatures are unchanged.
|
|
8
|
+
|
|
9
|
+
- Grades: a teacher's multi-line `Komentarz` continues on unlabelled tooltip
|
|
10
|
+
lines and is kept with its line breaks. Every grade read of an account with
|
|
11
|
+
such a comment failed in all releases up to 1.7.0; any other unlabelled
|
|
12
|
+
tooltip line still fails.
|
|
13
|
+
- Point grades: a formative entry among the point entries is accepted as the
|
|
14
|
+
formative mirror it is; the empty footer row wider than the table is
|
|
15
|
+
skipped; titles that break their own label (`Ocena śródroczna <br> z
|
|
16
|
+
pierwszego okresu`) name the period columns, so point midterm grades are
|
|
17
|
+
read. These rules apply to point tables only.
|
|
18
|
+
- Formative assessments: assessment text may contain line breaks. 1.7.0 and
|
|
19
|
+
earlier failed such a table with `UnsupportedCapabilityError`.
|
|
20
|
+
|
|
3
21
|
## 1.7.0 (2026-10-27) - Point grades
|
|
4
22
|
|
|
5
23
|
Reads the "Oceny punktowe" table that point-grade schools show on the grades
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: librus-python-api
|
|
3
|
-
Version: 1.7.
|
|
3
|
+
Version: 1.7.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
|
|
@@ -42,8 +42,8 @@ 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.7.
|
|
46
|
-
|
|
45
|
+
**Status:** `1.7.1` reads point grades ("Oceny punktowe") as an affected
|
|
46
|
+
school's page shows them, and multi-line grade comments. It keeps the 1.6.4 linear-time message body
|
|
47
47
|
rendering and the 1.6.2 completed-lesson and grade layouts, including behaviour
|
|
48
48
|
points, which are validated and not reported as grades.
|
|
49
49
|
Class free days, typed capability outcomes and the live-check freshness watchdog
|
|
@@ -57,7 +57,7 @@ See [limitations](#supported-features-and-limitations) below.
|
|
|
57
57
|
Install the exact release from PyPI after its gated publication completes:
|
|
58
58
|
|
|
59
59
|
```sh
|
|
60
|
-
python -m pip install librus-python-api==1.7.
|
|
60
|
+
python -m pip install librus-python-api==1.7.1
|
|
61
61
|
```
|
|
62
62
|
|
|
63
63
|
Pin the exact version qualified by your application. Before publication, use a
|
|
@@ -73,7 +73,7 @@ python -m pip install .
|
|
|
73
73
|
To install a locally built wheel instead:
|
|
74
74
|
|
|
75
75
|
```sh
|
|
76
|
-
python -m pip install ./dist/librus_python_api-1.7.
|
|
76
|
+
python -m pip install ./dist/librus_python_api-1.7.1-py3-none-any.whl
|
|
77
77
|
```
|
|
78
78
|
|
|
79
79
|
No CLI or background process is installed: import the library in your own program.
|
|
@@ -16,8 +16,8 @@ private ACLs. Windows installations include `pywin32` and `tzdata` automatically
|
|
|
16
16
|
Network paths, reparse-point paths and unsafe storage permissions fail closed.
|
|
17
17
|
See [platform requirements](contracts/persistence.md#windows-disk-boundary-25).
|
|
18
18
|
|
|
19
|
-
**Status:** `1.7.
|
|
20
|
-
|
|
19
|
+
**Status:** `1.7.1` reads point grades ("Oceny punktowe") as an affected
|
|
20
|
+
school's page shows them, and multi-line grade comments. It keeps the 1.6.4 linear-time message body
|
|
21
21
|
rendering and the 1.6.2 completed-lesson and grade layouts, including behaviour
|
|
22
22
|
points, which are validated and not reported as grades.
|
|
23
23
|
Class free days, typed capability outcomes and the live-check freshness watchdog
|
|
@@ -31,7 +31,7 @@ See [limitations](#supported-features-and-limitations) below.
|
|
|
31
31
|
Install the exact release from PyPI after its gated publication completes:
|
|
32
32
|
|
|
33
33
|
```sh
|
|
34
|
-
python -m pip install librus-python-api==1.7.
|
|
34
|
+
python -m pip install librus-python-api==1.7.1
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
Pin the exact version qualified by your application. Before publication, use a
|
|
@@ -47,7 +47,7 @@ python -m pip install .
|
|
|
47
47
|
To install a locally built wheel instead:
|
|
48
48
|
|
|
49
49
|
```sh
|
|
50
|
-
python -m pip install ./dist/librus_python_api-1.7.
|
|
50
|
+
python -m pip install ./dist/librus_python_api-1.7.1-py3-none-any.whl
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
No CLI or background process is installed: import the library in your own program.
|
|
@@ -138,7 +138,7 @@ from librus_python_api.models import (
|
|
|
138
138
|
from librus_python_api.sending import SendAttempt
|
|
139
139
|
from librus_python_api.service import AccountClient, LibrusService
|
|
140
140
|
|
|
141
|
-
__version__ = "1.7.
|
|
141
|
+
__version__ = "1.7.1"
|
|
142
142
|
|
|
143
143
|
__all__ = [
|
|
144
144
|
"ArchiveAbsences",
|
|
@@ -659,6 +659,9 @@ GRADE_MAX_SUBJECTS = 128
|
|
|
659
659
|
GRADE_MAX_VALUE_LENGTH = 1024
|
|
660
660
|
GRADE_MERGED_SUBJECTS = frozenset({"Zachowanie"})
|
|
661
661
|
GRADE_INLINE_DETAIL_LABEL = "Ocena"
|
|
662
|
+
# Tooltip fields whose free text may continue on unlabelled lines. A teacher's
|
|
663
|
+
# multi-line grade comment was observed on 2026-10-27.
|
|
664
|
+
TOOLTIP_MULTILINE_LABELS = frozenset({"Komentarz"})
|
|
662
665
|
# A point-grade ("Oceny punktowe") table marks each subject row's expander image
|
|
663
666
|
# with this ID; ordinary grade rows use `przedmioty_<id>_node`.
|
|
664
667
|
GRADE_POINT_ROW_NODE = re.compile(r"przedmioty_OP_[0-9A-Za-z_]{1,64}_node")
|
{librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/formative_grades.py
RENAMED
|
@@ -58,7 +58,8 @@ def _link(cell: html.HtmlElement) -> tuple[str, str]:
|
|
|
58
58
|
"""The detail ID and full text of the assessment cell.
|
|
59
59
|
|
|
60
60
|
Observed: one link whose optional first child is an empty coloured
|
|
61
|
-
`span.grade-box` marker, followed by the text
|
|
61
|
+
`span.grade-box` marker, followed by the text, which may contain empty
|
|
62
|
+
line breaks (2026-10-27). Anything else is new markup.
|
|
62
63
|
"""
|
|
63
64
|
_single(cell)
|
|
64
65
|
anchors = [node for node in cell if isinstance(node.tag, str)]
|
|
@@ -66,6 +67,10 @@ def _link(cell: html.HtmlElement) -> tuple[str, str]:
|
|
|
66
67
|
_fail(ErrorKind.UNSUPPORTED_CAPABILITY)
|
|
67
68
|
anchor = anchors[0]
|
|
68
69
|
children = [node for node in anchor.iterdescendants() if isinstance(node.tag, str)]
|
|
70
|
+
breaks = [node for node in children if node.tag == "br"]
|
|
71
|
+
if any(node.getparent() is not anchor or len(node) for node in breaks):
|
|
72
|
+
_fail(ErrorKind.UNSUPPORTED_CAPABILITY)
|
|
73
|
+
children = [node for node in children if node.tag != "br"]
|
|
69
74
|
if children:
|
|
70
75
|
marker = children[0]
|
|
71
76
|
if (
|
|
@@ -27,22 +27,34 @@ from librus_python_api.models import (
|
|
|
27
27
|
from librus_python_api.parsers import parse_page
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
def
|
|
30
|
+
def title_label(title: str, *, points: bool = False) -> str:
|
|
31
|
+
"""A header title's label: its first line, or for a point table all of it.
|
|
32
|
+
|
|
33
|
+
Ordinary titles append an explanation after a line break. Point tables
|
|
34
|
+
break the label itself ("Ocena śródroczna <br> z pierwszego okresu").
|
|
35
|
+
"""
|
|
36
|
+
if points:
|
|
37
|
+
return " ".join(re.sub(r"<br\s*/?>", " ", title, flags=re.I).split())
|
|
38
|
+
return " ".join(re.split(r"<br\s*/?>", title, flags=re.I)[0].split())
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _summary_field(cell: html.HtmlElement, *, points: bool = False) -> str | None:
|
|
31
42
|
title = cell.get("title", "")
|
|
32
43
|
if len(title) > GRADE_MAX_VALUE_LENGTH:
|
|
33
44
|
raise LibrusError(ErrorKind.LIMIT)
|
|
34
|
-
|
|
35
|
-
return GRADE_SUMMARY_HEADERS.get(label)
|
|
45
|
+
return GRADE_SUMMARY_HEADERS.get(title_label(title, points=points))
|
|
36
46
|
|
|
37
47
|
|
|
38
|
-
def _header(
|
|
48
|
+
def _header(
|
|
49
|
+
row: html.HtmlElement, *, points: bool = False
|
|
50
|
+
) -> tuple[dict[str, int], int]:
|
|
39
51
|
fields: dict[str, int] = {}
|
|
40
52
|
position = GRADE_BODY_PREFIX_COLUMNS
|
|
41
53
|
for cell in markup.cells(row):
|
|
42
54
|
span = markup.colspan(cell)
|
|
43
55
|
if position + span > GRADE_MAX_COLUMNS:
|
|
44
56
|
raise LibrusError(ErrorKind.LIMIT)
|
|
45
|
-
field = _summary_field(cell)
|
|
57
|
+
field = _summary_field(cell, points=points)
|
|
46
58
|
if field is not None:
|
|
47
59
|
if field in fields:
|
|
48
60
|
raise LibrusError(ErrorKind.PARSE)
|
|
@@ -86,7 +98,7 @@ def point_layout(table: html.HtmlElement) -> tuple[dict[str, int], int]:
|
|
|
86
98
|
]
|
|
87
99
|
if len(headers) != 1:
|
|
88
100
|
raise LibrusError(ErrorKind.PARSE)
|
|
89
|
-
return _header(headers[0])
|
|
101
|
+
return _header(headers[0], points=True)
|
|
90
102
|
|
|
91
103
|
|
|
92
104
|
def _candidates(
|
|
@@ -181,7 +193,7 @@ def _summary(
|
|
|
181
193
|
|
|
182
194
|
|
|
183
195
|
def _subject_rows(
|
|
184
|
-
table: html.HtmlElement, width: int
|
|
196
|
+
table: html.HtmlElement, width: int, *, points: bool = False
|
|
185
197
|
) -> Iterator[list[html.HtmlElement]]:
|
|
186
198
|
for row in markup.rows(table):
|
|
187
199
|
if next(row.iterancestors("thead"), None) is not None:
|
|
@@ -189,6 +201,14 @@ def _subject_rows(
|
|
|
189
201
|
cells = markup.cells(row)
|
|
190
202
|
if not cells:
|
|
191
203
|
raise LibrusError(ErrorKind.PARSE)
|
|
204
|
+
# The observed point table closes with an empty footer cell wider than
|
|
205
|
+
# the table; only an empty footer row is skipped.
|
|
206
|
+
if (
|
|
207
|
+
points
|
|
208
|
+
and next(row.iterancestors("tfoot"), None) is not None
|
|
209
|
+
and not any(markup.text(cell) for cell in cells)
|
|
210
|
+
):
|
|
211
|
+
continue
|
|
192
212
|
if list(row.iter("table")):
|
|
193
213
|
if _outside_nested_tables(row):
|
|
194
214
|
raise LibrusError(ErrorKind.PARSE)
|
|
@@ -252,7 +272,7 @@ def parse_final_grades(body: bytes) -> tuple[SubjectGradeSummary, ...]:
|
|
|
252
272
|
items: dict[str, SubjectGradeSummary] = {}
|
|
253
273
|
for table, fields, width in sources:
|
|
254
274
|
family_subjects: set[str] = set()
|
|
255
|
-
for cells in _subject_rows(table, width):
|
|
275
|
+
for cells in _subject_rows(table, width, points=table in point_tables):
|
|
256
276
|
item = _summary(cells, fields, width)
|
|
257
277
|
if item.subject in family_subjects:
|
|
258
278
|
raise LibrusError(ErrorKind.PARSE)
|
|
@@ -36,6 +36,7 @@ from librus_python_api.grade_parsers import (
|
|
|
36
36
|
_summary,
|
|
37
37
|
is_point_table,
|
|
38
38
|
point_layout,
|
|
39
|
+
title_label,
|
|
39
40
|
)
|
|
40
41
|
from librus_python_api.models import (
|
|
41
42
|
Availability,
|
|
@@ -51,8 +52,8 @@ from librus_python_api.models import (
|
|
|
51
52
|
from librus_python_api.parsers import parse_page
|
|
52
53
|
|
|
53
54
|
|
|
54
|
-
def _label(title: str) -> str:
|
|
55
|
-
return
|
|
55
|
+
def _label(title: str, *, points: bool = False) -> str:
|
|
56
|
+
return title_label(title, points=points)
|
|
56
57
|
|
|
57
58
|
|
|
58
59
|
def _layout(
|
|
@@ -74,7 +75,9 @@ def _layout(
|
|
|
74
75
|
if markup.colspan(cell) != 1:
|
|
75
76
|
raise LibrusError(ErrorKind.PARSE)
|
|
76
77
|
current.append(position)
|
|
77
|
-
period = GRADE_AVERAGE_HEADERS.get(
|
|
78
|
+
period = GRADE_AVERAGE_HEADERS.get(
|
|
79
|
+
_label(cell.get("title", ""), points=points)
|
|
80
|
+
)
|
|
78
81
|
result = GRADE_POINT_RESULT_HEADER.fullmatch(markup.text(cell))
|
|
79
82
|
if points and period is None and result is not None:
|
|
80
83
|
period = GRADE_POINT_RESULT_PERIODS[result.group(1)]
|
|
@@ -333,7 +336,9 @@ def _read_descriptive_row(
|
|
|
333
336
|
raise LibrusError(ErrorKind.UNSUPPORTED_CAPABILITY)
|
|
334
337
|
|
|
335
338
|
|
|
336
|
-
def _period_columns(
|
|
339
|
+
def _period_columns(
|
|
340
|
+
table: html.HtmlElement, *, points: bool = False
|
|
341
|
+
) -> dict[tuple[int, GradeKind], int]:
|
|
337
342
|
result: dict[tuple[int, GradeKind], int] = {}
|
|
338
343
|
for row in markup.rows(table):
|
|
339
344
|
cells = markup.cells(row)
|
|
@@ -343,7 +348,7 @@ def _period_columns(table: html.HtmlElement) -> dict[tuple[int, GradeKind], int]
|
|
|
343
348
|
continue
|
|
344
349
|
position = GRADE_BODY_PREFIX_COLUMNS
|
|
345
350
|
for cell in cells:
|
|
346
|
-
label = _label(cell.get("title", ""))
|
|
351
|
+
label = _label(cell.get("title", ""), points=points)
|
|
347
352
|
period = GRADE_PERIOD_HEADERS.get(label)
|
|
348
353
|
kind = GradeKind.ANNUAL if period == 0 else GradeKind.PERIOD
|
|
349
354
|
if label == GRADE_PREDICTED_ANNUAL_HEADER:
|
|
@@ -371,11 +376,11 @@ def _read_numeric_table(
|
|
|
371
376
|
else:
|
|
372
377
|
_, fields, width = _locate(table)
|
|
373
378
|
current, means = _layout(table, points=points)
|
|
374
|
-
periods = _period_columns(table)
|
|
379
|
+
periods = _period_columns(table, points=points)
|
|
375
380
|
averages = collection.point_results if points else collection.averages
|
|
376
381
|
# Period grades shown by both tables are one record, kept from the first.
|
|
377
382
|
prior = set(collection.numeric) if points else set()
|
|
378
|
-
for cells in _subject_rows(table, width):
|
|
383
|
+
for cells in _subject_rows(table, width, points=points):
|
|
379
384
|
if cells[0].getparent() in collection.publication_rows:
|
|
380
385
|
continue
|
|
381
386
|
if points:
|
|
@@ -412,9 +417,10 @@ def _read_numeric_table(
|
|
|
412
417
|
columns[index], subject, cast(Literal[1, 2], period), collection
|
|
413
418
|
)
|
|
414
419
|
# A point entry without its detail link would pass as a
|
|
415
|
-
# grade symbol; an unseen link layout fails instead.
|
|
420
|
+
# grade symbol; an unseen link layout fails instead. The
|
|
421
|
+
# observed point table also shows formative entries.
|
|
416
422
|
if points and any(
|
|
417
|
-
record.point_grade_id is None
|
|
423
|
+
record.point_grade_id is None and record.formative_id is None
|
|
418
424
|
for record in collection.numeric[start:]
|
|
419
425
|
):
|
|
420
426
|
raise LibrusError(ErrorKind.PARSE)
|
|
@@ -558,11 +564,11 @@ def parse_grade_records(body: bytes) -> GradeRecords:
|
|
|
558
564
|
continue
|
|
559
565
|
raise LibrusError(ErrorKind.UNSUPPORTED_CAPABILITY)
|
|
560
566
|
for anchor in document.iter("a"):
|
|
561
|
-
# A formative link outside
|
|
567
|
+
# A formative link outside the known tables is a new layout.
|
|
562
568
|
if markup.detail_id(
|
|
563
569
|
anchor.get("href"), FORMATIVE_DETAIL_PATH_PREFIX
|
|
564
570
|
) and not any(
|
|
565
|
-
parent in
|
|
571
|
+
parent in grade_tables or parent in formative_tables
|
|
566
572
|
for parent in anchor.iterancestors("table")
|
|
567
573
|
):
|
|
568
574
|
raise LibrusError(ErrorKind.UNSUPPORTED_CAPABILITY)
|
|
@@ -13,6 +13,7 @@ from librus_python_api.config import (
|
|
|
13
13
|
GRADE_MAX_COLUMNS,
|
|
14
14
|
GRADE_MAX_METADATA_FIELDS,
|
|
15
15
|
GRADE_MAX_METADATA_LENGTH,
|
|
16
|
+
TOOLTIP_MULTILINE_LABELS,
|
|
16
17
|
UPSTREAM_ORIGINS,
|
|
17
18
|
)
|
|
18
19
|
from librus_python_api.exceptions import ErrorKind, LibrusError
|
|
@@ -81,11 +82,16 @@ def colspan(cell: html.HtmlElement) -> int:
|
|
|
81
82
|
|
|
82
83
|
|
|
83
84
|
def tooltip_fields(element: html.HtmlElement) -> dict[str, str]:
|
|
84
|
-
"""Parse a 'Label: value<br>' title attribute into unique labelled fields.
|
|
85
|
+
"""Parse a 'Label: value<br>' title attribute into unique labelled fields.
|
|
86
|
+
|
|
87
|
+
A free-text field may span lines; an unlabelled line after it continues
|
|
88
|
+
its value. Any other unlabelled line is an unknown layout.
|
|
89
|
+
"""
|
|
85
90
|
title = element.get("title", "")
|
|
86
91
|
if len(title) > GRADE_MAX_METADATA_LENGTH:
|
|
87
92
|
raise LibrusError(ErrorKind.LIMIT)
|
|
88
93
|
result: dict[str, str] = {}
|
|
94
|
+
last: str | None = None
|
|
89
95
|
chunks = re.split(r"<br\s*/?>|\n", title, flags=re.I)
|
|
90
96
|
if len(chunks) > GRADE_MAX_METADATA_FIELDS:
|
|
91
97
|
raise LibrusError(ErrorKind.LIMIT)
|
|
@@ -100,9 +106,13 @@ def tooltip_fields(element: html.HtmlElement) -> dict[str, str]:
|
|
|
100
106
|
)
|
|
101
107
|
key, separator, value = rendered.partition(":")
|
|
102
108
|
key, value = key.strip(), value.strip()
|
|
109
|
+
if not separator and last in TOOLTIP_MULTILINE_LABELS:
|
|
110
|
+
result[last] = f"{result[last]}\n{rendered}".strip()
|
|
111
|
+
continue
|
|
103
112
|
if not separator or not key or key in result:
|
|
104
113
|
raise LibrusError(ErrorKind.PARSE)
|
|
105
114
|
result[key] = value
|
|
115
|
+
last = key
|
|
106
116
|
return result
|
|
107
117
|
|
|
108
118
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/_notification_bootstrap.py
RENAMED
|
File without changes
|
{librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/_notification_codec.py
RENAMED
|
File without changes
|
|
File without changes
|
{librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/_windows_filesystem.py
RENAMED
|
File without changes
|
|
File without changes
|
{librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/attachment_routes.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/attendance_frequency.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/class_free_days.py
RENAMED
|
File without changes
|
{librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/completed_lessons.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/message_content.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/modern_messages.py
RENAMED
|
File without changes
|
{librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/notification_models.py
RENAMED
|
File without changes
|
|
File without changes
|
{librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/notification_workflow.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{librus_python_api-1.7.0 → librus_python_api-1.7.1}/src/librus_python_api/school_year_archive.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|