librus-python-api 1.6.1__tar.gz → 1.6.2__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.6.1 → librus_python_api-1.6.2}/CHANGELOG.md +27 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/PKG-INFO +5 -4
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/README.md +4 -3
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/pyproject.toml +1 -1
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/__init__.py +1 -1
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/completed_lessons.py +85 -13
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/config.py +6 -1
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/detail_fields.py +0 -1
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/formative_grades.py +0 -1
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/grade_records.py +53 -3
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/notification_persistence.py +0 -1
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/persistence.py +1 -1
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/transport.py +91 -102
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/uv.lock +216 -143
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/.gitignore +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/API.md +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/LICENSE +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/SECURITY.md +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/_notification_bootstrap.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/_notification_codec.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/_storage.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/_windows_filesystem.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/announcements.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/attachment_routes.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/attachments.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/attendance.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/attendance_frequency.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/budget.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/checkpoint.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/class_free_days.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/diagnostics.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/exceptions.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/files.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/grade_parsers.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/homework_range.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/lifecycle.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/markup.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/message_content.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/messages.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/models.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/modern_body.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/modern_mailbox.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/modern_messages.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/notification_models.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/notification_workflow.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/notifications.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/parsers.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/parsing.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/py.typed +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/recipients.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/scheduler.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/school_reads.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/school_year_archive.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/sending.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/service.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/timetable.py +0 -0
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.6.2 (2026-10-19) - School layout variants and behaviour points
|
|
4
|
+
|
|
5
|
+
Fixes for completed-lesson and grade pages that 1.6.1 rejected with
|
|
6
|
+
`ParseError`. The layouts were observed offline in owner-provided pages and one
|
|
7
|
+
issue report; the guarded live gate for them is pending. Public types and
|
|
8
|
+
signatures are unchanged.
|
|
9
|
+
|
|
10
|
+
- Completed lessons: accept the item range after the page count
|
|
11
|
+
(`Strona 1 z 3 Pozycje 1..20 z 47`) and require it to match the parsed rows;
|
|
12
|
+
ignore the `decorated filters` date-filter table; locate the optional
|
|
13
|
+
`e-Tablica` column by its header label (nonempty cells raise
|
|
14
|
+
`UnsupportedCapabilityError`); skip a blank full-width closing row.
|
|
15
|
+
- Completed lessons: strip whitespace and line breaks around the subject/teacher
|
|
16
|
+
comma, so `<b>Subject</b>, <br>Teacher` no longer yields the whole cell as the
|
|
17
|
+
subject with no teacher.
|
|
18
|
+
- Grades: recognize behaviour points in the `Zachowanie` row (a starting-points
|
|
19
|
+
or running-total tooltip, and point entries linking to behaviour details).
|
|
20
|
+
They are validated and not returned as grades, so the whole grade read no
|
|
21
|
+
longer fails on them (#76). Other content beside the points tooltip still
|
|
22
|
+
fails.
|
|
23
|
+
- Internal: share aiohttp session setup and message send dispatch in the
|
|
24
|
+
transport, remove an unused recipient constant, orphaned 0.4.x qualification
|
|
25
|
+
scripts and dead test-support code, and correct stale status sentences in the
|
|
26
|
+
contracts. Behaviour is unchanged.
|
|
27
|
+
- Development dependencies and pinned GitHub Actions are updated; runtime
|
|
28
|
+
dependency ranges are unchanged.
|
|
29
|
+
|
|
3
30
|
## 1.6.1 (2026-10-12) - Sunday grade dates and calendar closures
|
|
4
31
|
|
|
5
32
|
- Accept both observed Sunday tooltip labels, `nd.` and `ndz.`, while retaining
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: librus-python-api
|
|
3
|
-
Version: 1.6.
|
|
3
|
+
Version: 1.6.2
|
|
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,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.6.
|
|
45
|
+
**Status:** `1.6.2` accepts more completed-lesson and grade layouts, including
|
|
46
|
+
behaviour points, which are validated and not reported as grades.
|
|
46
47
|
Class free days, typed capability outcomes and the live-check freshness watchdog
|
|
47
48
|
remain available. See [verification](VERIFICATION.md).
|
|
48
49
|
The stable 1.x API does not guarantee every school layout. School features depend
|
|
@@ -54,7 +55,7 @@ See [limitations](#supported-features-and-limitations) below.
|
|
|
54
55
|
Install the exact release from PyPI after its gated publication completes:
|
|
55
56
|
|
|
56
57
|
```sh
|
|
57
|
-
python -m pip install librus-python-api==1.6.
|
|
58
|
+
python -m pip install librus-python-api==1.6.2
|
|
58
59
|
```
|
|
59
60
|
|
|
60
61
|
Pin the exact version qualified by your application. Before publication, use a
|
|
@@ -70,7 +71,7 @@ python -m pip install .
|
|
|
70
71
|
To install a locally built wheel instead:
|
|
71
72
|
|
|
72
73
|
```sh
|
|
73
|
-
python -m pip install ./dist/librus_python_api-1.6.
|
|
74
|
+
python -m pip install ./dist/librus_python_api-1.6.2-py3-none-any.whl
|
|
74
75
|
```
|
|
75
76
|
|
|
76
77
|
No CLI or background process is installed: import the library in your own program.
|
|
@@ -16,7 +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.6.
|
|
19
|
+
**Status:** `1.6.2` accepts more completed-lesson and grade layouts, including
|
|
20
|
+
behaviour points, which are validated and not reported as grades.
|
|
20
21
|
Class free days, typed capability outcomes and the live-check freshness watchdog
|
|
21
22
|
remain available. See [verification](VERIFICATION.md).
|
|
22
23
|
The stable 1.x API does not guarantee every school layout. School features depend
|
|
@@ -28,7 +29,7 @@ See [limitations](#supported-features-and-limitations) below.
|
|
|
28
29
|
Install the exact release from PyPI after its gated publication completes:
|
|
29
30
|
|
|
30
31
|
```sh
|
|
31
|
-
python -m pip install librus-python-api==1.6.
|
|
32
|
+
python -m pip install librus-python-api==1.6.2
|
|
32
33
|
```
|
|
33
34
|
|
|
34
35
|
Pin the exact version qualified by your application. Before publication, use a
|
|
@@ -44,7 +45,7 @@ python -m pip install .
|
|
|
44
45
|
To install a locally built wheel instead:
|
|
45
46
|
|
|
46
47
|
```sh
|
|
47
|
-
python -m pip install ./dist/librus_python_api-1.6.
|
|
48
|
+
python -m pip install ./dist/librus_python_api-1.6.2-py3-none-any.whl
|
|
48
49
|
```
|
|
49
50
|
|
|
50
51
|
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.6.
|
|
141
|
+
__version__ = "1.6.2"
|
|
142
142
|
|
|
143
143
|
__all__ = [
|
|
144
144
|
"ArchiveAbsences",
|
{librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/completed_lessons.py
RENAMED
|
@@ -9,10 +9,12 @@ from lxml import html
|
|
|
9
9
|
|
|
10
10
|
from librus_python_api import markup
|
|
11
11
|
from librus_python_api.config import (
|
|
12
|
+
COMPLETED_LESSONS_COLUMNS,
|
|
12
13
|
COMPLETED_LESSONS_MAX_BATCH_ITEMS,
|
|
13
14
|
COMPLETED_LESSONS_MAX_BATCH_PAGES,
|
|
14
15
|
COMPLETED_LESSONS_MAX_PAGE_COUNT,
|
|
15
16
|
COMPLETED_LESSONS_MAX_PAGE_ITEMS,
|
|
17
|
+
COMPLETED_LESSONS_OPTIONAL_HEADERS,
|
|
16
18
|
SCHOOL_MAX_CONTENT_LENGTH,
|
|
17
19
|
SCHOOL_MAX_FIELD_LENGTH,
|
|
18
20
|
SCHOOL_MAX_TOTAL_TEXT_LENGTH,
|
|
@@ -65,7 +67,8 @@ def validate_selection(
|
|
|
65
67
|
completed_lessons_form(cursor.start, cursor.end, cursor.page)
|
|
66
68
|
|
|
67
69
|
|
|
68
|
-
def _pagination(document: html.HtmlElement, page: int) -> int:
|
|
70
|
+
def _pagination(document: html.HtmlElement, page: int) -> tuple[int, int | None]:
|
|
71
|
+
"""The page count and, when shown, the number of rows the page reports."""
|
|
69
72
|
containers = [
|
|
70
73
|
node
|
|
71
74
|
for node in document.iter("div")
|
|
@@ -74,14 +77,19 @@ def _pagination(document: html.HtmlElement, page: int) -> int:
|
|
|
74
77
|
if not containers:
|
|
75
78
|
if page != 0:
|
|
76
79
|
raise LibrusError(ErrorKind.PARSE)
|
|
77
|
-
return 1
|
|
80
|
+
return 1, None
|
|
78
81
|
if len(containers) != 1:
|
|
79
82
|
raise LibrusError(ErrorKind.PARSE)
|
|
80
83
|
spans = [node for node in containers[0] if node.tag == "span"]
|
|
81
84
|
if len(spans) != 1:
|
|
82
85
|
raise LibrusError(ErrorKind.PARSE)
|
|
83
86
|
value = markup.text(spans[0], SCHOOL_MAX_FIELD_LENGTH)
|
|
84
|
-
match = re.fullmatch(
|
|
87
|
+
match = re.fullmatch(
|
|
88
|
+
r"(?:Strona\s+)?([0-9]{1,4})\s+z\s+([0-9]{1,4})"
|
|
89
|
+
r"(?:\s+Pozycje\s+([0-9]{1,6})\.\.([0-9]{1,6})\s+z\s+([0-9]{1,6}))?",
|
|
90
|
+
value,
|
|
91
|
+
re.I,
|
|
92
|
+
)
|
|
85
93
|
if match is None:
|
|
86
94
|
raise LibrusError(ErrorKind.PARSE)
|
|
87
95
|
current, count = int(match[1]), int(match[2])
|
|
@@ -89,7 +97,53 @@ def _pagination(document: html.HtmlElement, page: int) -> int:
|
|
|
89
97
|
raise LibrusError(ErrorKind.LIMIT)
|
|
90
98
|
if not 1 <= current <= count or current != page + 1:
|
|
91
99
|
raise LibrusError(ErrorKind.PARSE)
|
|
92
|
-
|
|
100
|
+
if match[3] is None:
|
|
101
|
+
return count, None
|
|
102
|
+
# Some schools append the item range: "Pozycje first..last z total".
|
|
103
|
+
first, last, total = int(match[3]), int(match[4]), int(match[5])
|
|
104
|
+
if not 1 <= first <= last <= total:
|
|
105
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
106
|
+
return count, last - first + 1
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _optional_columns(table: html.HtmlElement) -> frozenset[int]:
|
|
110
|
+
"""Row positions of optional columns, located by their header labels."""
|
|
111
|
+
positions: set[int] = set()
|
|
112
|
+
for row in markup.rows(table):
|
|
113
|
+
if not markup.in_header(row):
|
|
114
|
+
continue
|
|
115
|
+
position = 0
|
|
116
|
+
labels = [
|
|
117
|
+
(cell, markup.text(cell, SCHOOL_MAX_FIELD_LENGTH))
|
|
118
|
+
for cell in markup.cells(row)
|
|
119
|
+
]
|
|
120
|
+
if not any(label in COMPLETED_LESSONS_OPTIONAL_HEADERS for _, label in labels):
|
|
121
|
+
continue
|
|
122
|
+
if positions:
|
|
123
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
124
|
+
for cell, label in labels:
|
|
125
|
+
span = markup.colspan(cell)
|
|
126
|
+
if label in COMPLETED_LESSONS_OPTIONAL_HEADERS:
|
|
127
|
+
if span != 1:
|
|
128
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
129
|
+
positions.add(position)
|
|
130
|
+
position += span
|
|
131
|
+
# A labelled header must describe exactly the row layout it extends.
|
|
132
|
+
if position != COMPLETED_LESSONS_COLUMNS + len(positions):
|
|
133
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
134
|
+
return frozenset(positions)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _closing_row(row: html.HtmlElement, width: int) -> bool:
|
|
138
|
+
"""A blank full-width row that closes some schools' tables, not a lesson."""
|
|
139
|
+
cells = markup.cells(row)
|
|
140
|
+
return (
|
|
141
|
+
len(cells) == 1
|
|
142
|
+
and cells[0].tag == "td"
|
|
143
|
+
and markup.colspan(cells[0]) == width
|
|
144
|
+
and not any(isinstance(node.tag, str) for node in cells[0])
|
|
145
|
+
and not markup.text(cells[0], SCHOOL_MAX_FIELD_LENGTH)
|
|
146
|
+
)
|
|
93
147
|
|
|
94
148
|
|
|
95
149
|
def _day(value: str) -> date:
|
|
@@ -109,15 +163,24 @@ def _day(value: str) -> date:
|
|
|
109
163
|
return result
|
|
110
164
|
|
|
111
165
|
|
|
112
|
-
def _lesson(
|
|
166
|
+
def _lesson(
|
|
167
|
+
row: html.HtmlElement, start: date, end: date, optional: frozenset[int]
|
|
168
|
+
) -> CompletedLesson:
|
|
113
169
|
cells = markup.cells(row)
|
|
114
|
-
if len(cells) !=
|
|
170
|
+
if len(cells) != COMPLETED_LESSONS_COLUMNS + len(optional) or any(
|
|
115
171
|
cell.tag != "td"
|
|
116
172
|
or cell.get("rowspan", "1") != "1"
|
|
117
173
|
or cell.get("colspan", "1") != "1"
|
|
118
174
|
for cell in cells
|
|
119
175
|
):
|
|
120
176
|
raise LibrusError(ErrorKind.PARSE)
|
|
177
|
+
for index in optional:
|
|
178
|
+
# Content in an optional column is not modelled; never drop it silently.
|
|
179
|
+
if any(isinstance(node.tag, str) for node in cells[index]) or markup.text(
|
|
180
|
+
cells[index], SCHOOL_MAX_FIELD_LENGTH
|
|
181
|
+
):
|
|
182
|
+
raise LibrusError(ErrorKind.UNSUPPORTED_CAPABILITY)
|
|
183
|
+
cells = [cell for index, cell in enumerate(cells) if index not in optional]
|
|
121
184
|
date_cells = [
|
|
122
185
|
c for c in cells if {"center", "small"} <= set(c.get("class", "").split())
|
|
123
186
|
]
|
|
@@ -140,7 +203,8 @@ def _lesson(row: html.HtmlElement, start: date, end: date) -> CompletedLesson:
|
|
|
140
203
|
raw_number, combined, topic, z_value, symbol = values
|
|
141
204
|
if raw_number not in {"", "-"} and re.fullmatch(r"[0-9]{1,2}", raw_number) is None:
|
|
142
205
|
raise LibrusError(ErrorKind.PARSE)
|
|
143
|
-
|
|
206
|
+
# Some schools render "<b>Subject</b>, <br>Teacher" or pad the comma.
|
|
207
|
+
names = [name.strip() for name in combined.split(",")]
|
|
144
208
|
subject, teacher = (names[0], names[1]) if len(names) == 2 else (combined, None)
|
|
145
209
|
if not subject or teacher == "":
|
|
146
210
|
raise LibrusError(ErrorKind.PARSE)
|
|
@@ -174,9 +238,13 @@ def parse_completed_lessons(
|
|
|
174
238
|
) -> tuple[tuple[CompletedLesson, ...], int, str]:
|
|
175
239
|
completed_lessons_form(start, end, page)
|
|
176
240
|
document = parse_page(body)
|
|
177
|
-
count = _pagination(document, page)
|
|
241
|
+
count, reported = _pagination(document, page)
|
|
242
|
+
# A date-filter form table shares the "decorated" class with the data table.
|
|
178
243
|
tables = [
|
|
179
|
-
t
|
|
244
|
+
t
|
|
245
|
+
for t in document.iter("table")
|
|
246
|
+
if "decorated" in (classes := t.get("class", "").split())
|
|
247
|
+
and "filters" not in classes
|
|
180
248
|
]
|
|
181
249
|
empty = [
|
|
182
250
|
n
|
|
@@ -190,10 +258,12 @@ def parse_completed_lessons(
|
|
|
190
258
|
if tables:
|
|
191
259
|
if list(tables[0].iterdescendants("table")):
|
|
192
260
|
raise LibrusError(ErrorKind.UNSUPPORTED_CAPABILITY)
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
261
|
+
optional = _optional_columns(tables[0])
|
|
262
|
+
lines = [row for row in markup.rows(tables[0]) if not markup.in_header(row)]
|
|
263
|
+
if lines and _closing_row(lines[-1], COMPLETED_LESSONS_COLUMNS + len(optional)):
|
|
264
|
+
lines.pop()
|
|
265
|
+
for row in lines:
|
|
266
|
+
item = _lesson(row, start, end, optional)
|
|
197
267
|
total += sum(len(v) for v in astuple(item) if isinstance(v, str))
|
|
198
268
|
items.append(item)
|
|
199
269
|
if (
|
|
@@ -201,6 +271,8 @@ def parse_completed_lessons(
|
|
|
201
271
|
or total > SCHOOL_MAX_TOTAL_TEXT_LENGTH
|
|
202
272
|
):
|
|
203
273
|
raise LibrusError(ErrorKind.LIMIT)
|
|
274
|
+
if reported is not None and reported != len(items):
|
|
275
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
204
276
|
if (items and empty) or (not items and (not empty or count != 1 or page != 0)):
|
|
205
277
|
raise LibrusError(ErrorKind.PARSE)
|
|
206
278
|
# An unrecognized notice next to an empty marker is not a valid empty page.
|
|
@@ -731,6 +731,8 @@ ATTENDANCE_MAX_SUBJECTS = 1024
|
|
|
731
731
|
ATTENDANCE_DETAIL_PATH_PREFIX = "/przegladaj_nb/szczegoly/"
|
|
732
732
|
# Recognition only: formative detail links are classified, never requested.
|
|
733
733
|
FORMATIVE_DETAIL_PATH_PREFIX = "/przegladaj_oceny/szczegoly/ksztaltujace/"
|
|
734
|
+
# Recognition only: behaviour-point detail links are classified, never requested.
|
|
735
|
+
BEHAVIOUR_POINTS_DETAIL_PATH_PREFIX = "/przegladaj_oceny/zachowanie_szczegoly/"
|
|
734
736
|
FORMATIVE_HEADERS = (
|
|
735
737
|
"przedmiot",
|
|
736
738
|
"ocena kształtująca",
|
|
@@ -882,6 +884,10 @@ COMPLETED_LESSONS_MAX_PAGE_COUNT = 1000
|
|
|
882
884
|
COMPLETED_LESSONS_MAX_PAGE_ITEMS = 256
|
|
883
885
|
COMPLETED_LESSONS_MAX_BATCH_PAGES = 8
|
|
884
886
|
COMPLETED_LESSONS_MAX_BATCH_ITEMS = 256
|
|
887
|
+
# Header labels of columns some schools add to the completed-lesson table. Their
|
|
888
|
+
# content is not modelled: such a column must be empty in every lesson row.
|
|
889
|
+
COMPLETED_LESSONS_OPTIONAL_HEADERS = frozenset({"e-Tablica"})
|
|
890
|
+
COMPLETED_LESSONS_COLUMNS = 7
|
|
885
891
|
AGENDA_DETAIL_PATH_PREFIX = "/terminarz/szczegoly/"
|
|
886
892
|
# Recognition only: closure details are not ordinary agenda-detail references.
|
|
887
893
|
AGENDA_CLOSURE_DETAIL_PATH_PREFIX = "/terminarz/szczegoly_wolne/"
|
|
@@ -931,7 +937,6 @@ MODERN_DIRECTORY_QUERIES = MappingProxyType(
|
|
|
931
937
|
"modern_recipients": (("receiverType", "parentsCouncil"),),
|
|
932
938
|
}
|
|
933
939
|
)
|
|
934
|
-
MODERN_SUPPORTED_RECIPIENT_TYPE = "parentsCouncil"
|
|
935
940
|
MODERN_RECIPIENT_OPERATIONS = MappingProxyType(
|
|
936
941
|
{
|
|
937
942
|
**{
|
|
@@ -34,7 +34,6 @@ def normalize_detail_fields(
|
|
|
34
34
|
fields: tuple[tuple[str, str], ...],
|
|
35
35
|
kind: Literal["agenda", "homework", "attendance"],
|
|
36
36
|
) -> tuple[DetailField, ...]:
|
|
37
|
-
"""Run after bounded HTML parsing, before publishing or caching a result."""
|
|
38
37
|
labels: set[str] = set()
|
|
39
38
|
keys: set[DetailFieldKey] = set()
|
|
40
39
|
result = []
|
{librus_python_api-1.6.1 → librus_python_api-1.6.2}/src/librus_python_api/formative_grades.py
RENAMED
|
@@ -31,7 +31,6 @@ def _fail(kind: ErrorKind) -> NoReturn:
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
def is_formative_table(table: html.HtmlElement) -> bool:
|
|
34
|
-
# Cheap structural gates first: other tables' header text is never read.
|
|
35
34
|
if not {"stretch", "decorated"} <= set(table.get("class", "").split()):
|
|
36
35
|
return False
|
|
37
36
|
heads = [row for row in markup.rows(table) if markup.in_header(row)]
|
|
@@ -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_POINTS_DETAIL_PATH_PREFIX,
|
|
11
12
|
FORMATIVE_DETAIL_PATH_PREFIX,
|
|
12
13
|
GRADE_AVERAGE_HEADERS,
|
|
13
14
|
GRADE_BODY_PREFIX_COLUMNS,
|
|
@@ -16,6 +17,7 @@ from librus_python_api.config import (
|
|
|
16
17
|
GRADE_MAX_METADATA_LENGTH,
|
|
17
18
|
GRADE_MAX_RECORDS,
|
|
18
19
|
GRADE_MAX_SUBJECTS,
|
|
20
|
+
GRADE_MERGED_SUBJECTS,
|
|
19
21
|
GRADE_PERIOD_HEADERS,
|
|
20
22
|
GRADE_PREDICTED_ANNUAL_HEADER,
|
|
21
23
|
GRADE_PREDICTED_PERIOD_HEADERS,
|
|
@@ -174,6 +176,50 @@ def _boxes(cell: html.HtmlElement) -> list[html.HtmlElement]:
|
|
|
174
176
|
]
|
|
175
177
|
|
|
176
178
|
|
|
179
|
+
def _behaviour_points(cell: html.HtmlElement, collection: _Collection) -> bool:
|
|
180
|
+
"""Recognize and validate a behaviour-points cell; points are not grades.
|
|
181
|
+
|
|
182
|
+
The cell holds one points tooltip (starting points or a running total; its
|
|
183
|
+
title is not interpreted) and point entries linking to behaviour details,
|
|
184
|
+
each with its own unlabelled tooltip. Any other content next to the points
|
|
185
|
+
tooltip fails instead of being skipped.
|
|
186
|
+
"""
|
|
187
|
+
tallies = [
|
|
188
|
+
node
|
|
189
|
+
for node in cell
|
|
190
|
+
if node.tag == "span" and "tooltip" in node.get("class", "").split()
|
|
191
|
+
]
|
|
192
|
+
if not tallies:
|
|
193
|
+
return False
|
|
194
|
+
elements = [node for node in cell if isinstance(node.tag, str)]
|
|
195
|
+
boxes = [node for node in elements if node not in tallies]
|
|
196
|
+
if (
|
|
197
|
+
len(tallies) != 1
|
|
198
|
+
or len(tallies[0])
|
|
199
|
+
or not re.fullmatch(r"-?[0-9]{1,4}", markup.text(tallies[0]))
|
|
200
|
+
or (cell.text or "").strip()
|
|
201
|
+
or any((node.tail or "").strip() for node in cell)
|
|
202
|
+
or _boxes(cell) != boxes
|
|
203
|
+
):
|
|
204
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
205
|
+
for box in boxes:
|
|
206
|
+
anchors = [node for node in box if isinstance(node.tag, str)]
|
|
207
|
+
if (
|
|
208
|
+
box.getparent() is not cell
|
|
209
|
+
or len(anchors) != 1
|
|
210
|
+
or anchors[0].tag != "a"
|
|
211
|
+
or len(anchors[0])
|
|
212
|
+
or markup.detail_id(
|
|
213
|
+
anchors[0].get("href"), BEHAVIOUR_POINTS_DETAIL_PATH_PREFIX
|
|
214
|
+
)
|
|
215
|
+
is None
|
|
216
|
+
or not re.fullmatch(r"[+-]?[0-9]{1,4}", markup.text(anchors[0]))
|
|
217
|
+
):
|
|
218
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
219
|
+
collection.consumed.add(box)
|
|
220
|
+
return True
|
|
221
|
+
|
|
222
|
+
|
|
177
223
|
def _entries(
|
|
178
224
|
cell: html.HtmlElement,
|
|
179
225
|
subject: str,
|
|
@@ -320,9 +366,13 @@ def _read_numeric_table(table: html.HtmlElement, collection: _Collection) -> Non
|
|
|
320
366
|
seen: set[html.HtmlElement] = set()
|
|
321
367
|
for period, index in enumerate(current, start=1):
|
|
322
368
|
if columns[index] not in seen:
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
369
|
+
if not (
|
|
370
|
+
subject in GRADE_MERGED_SUBJECTS
|
|
371
|
+
and _behaviour_points(columns[index], collection)
|
|
372
|
+
):
|
|
373
|
+
_entries(
|
|
374
|
+
columns[index], subject, cast(Literal[1, 2], period), collection
|
|
375
|
+
)
|
|
326
376
|
seen.add(columns[index])
|
|
327
377
|
for (period, kind), index in periods.items():
|
|
328
378
|
cell = columns[index]
|
|
@@ -144,7 +144,6 @@ def _seen_after(
|
|
|
144
144
|
class NotificationStore(_SQLiteStore):
|
|
145
145
|
"""Private native recovery store. No MCP files, auto migration or live access.
|
|
146
146
|
|
|
147
|
-
Context locks require POSIX flock. Unsupported platforms fail explicitly.
|
|
148
147
|
Raw checkpoints contain private account/content metadata, never HTTP cookies.
|
|
149
148
|
"""
|
|
150
149
|
|