librus-python-api 1.6.1__tar.gz → 1.6.3__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.3}/CHANGELOG.md +47 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/PKG-INFO +8 -8
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/README.md +6 -6
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/pyproject.toml +2 -2
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/__init__.py +1 -1
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/completed_lessons.py +85 -13
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/config.py +6 -1
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/detail_fields.py +0 -1
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/formative_grades.py +0 -1
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/grade_records.py +53 -3
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/notification_persistence.py +0 -1
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/persistence.py +1 -1
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/transport.py +91 -102
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/uv.lock +224 -151
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/.gitignore +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/API.md +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/LICENSE +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/SECURITY.md +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/_notification_bootstrap.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/_notification_codec.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/_storage.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/_windows_filesystem.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/announcements.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/attachment_routes.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/attachments.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/attendance.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/attendance_frequency.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/budget.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/checkpoint.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/class_free_days.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/diagnostics.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/exceptions.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/files.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/grade_parsers.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/homework_range.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/lifecycle.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/markup.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/message_content.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/messages.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/models.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/modern_body.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/modern_mailbox.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/modern_messages.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/notification_models.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/notification_workflow.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/notifications.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/parsers.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/parsing.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/py.typed +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/recipients.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/scheduler.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/school_reads.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/school_year_archive.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/sending.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/service.py +0 -0
- {librus_python_api-1.6.1 → librus_python_api-1.6.3}/src/librus_python_api/timetable.py +0 -0
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.6.3 (2026-10-22) - Dependency security update
|
|
4
|
+
|
|
5
|
+
Raises dependency floors and tooling pins past published advisories. Public
|
|
6
|
+
types, signatures and parser behaviour are unchanged.
|
|
7
|
+
|
|
8
|
+
- Runtime: require `aiohttp>=3.14.4`. Earlier 3.14 releases are affected by
|
|
9
|
+
GHSA-494j-54xm-rwp3 (a Secure cookie in the shared cookie jar could be sent
|
|
10
|
+
over plain HTTP) and GHSA-gx76-c99x-2c96 (a subdomain could strip the
|
|
11
|
+
host-only flag from a parent-domain cookie), both relevant to the client
|
|
12
|
+
cookie jar, plus server-side advisories that this client does not reach. The
|
|
13
|
+
lock already used 3.14.5; the floor stops a resolver from choosing an affected
|
|
14
|
+
release.
|
|
15
|
+
- Development lock: certifi 2026.10.19 drops the compromised emSign ECC Root CA
|
|
16
|
+
G3 (GHSA-6q48-6c7w-9gw2), and virtualenv 21.14.12 fixes activation-script and
|
|
17
|
+
path quoting advisories. Neither is a runtime dependency.
|
|
18
|
+
- CI and release workflows pin uv 0.13.5 with per-platform checksums. uv 0.12.7
|
|
19
|
+
trusted the same compromised root (GHSA-hq85-5cpj-mr62) and could write
|
|
20
|
+
outside the install prefix on Windows (GHSA-2cv4-cqwr-gwf7). The lockfile
|
|
21
|
+
format revision moves from 3 to 5 with no resolution change.
|
|
22
|
+
|
|
23
|
+
## 1.6.2 (2026-10-19) - School layout variants and behaviour points
|
|
24
|
+
|
|
25
|
+
Fixes for completed-lesson and grade pages that 1.6.1 rejected with
|
|
26
|
+
`ParseError`. The layouts were observed offline in owner-provided pages and one
|
|
27
|
+
issue report; the guarded live gate for them is pending. Public types and
|
|
28
|
+
signatures are unchanged.
|
|
29
|
+
|
|
30
|
+
- Completed lessons: accept the item range after the page count
|
|
31
|
+
(`Strona 1 z 3 Pozycje 1..20 z 47`) and require it to match the parsed rows;
|
|
32
|
+
ignore the `decorated filters` date-filter table; locate the optional
|
|
33
|
+
`e-Tablica` column by its header label (nonempty cells raise
|
|
34
|
+
`UnsupportedCapabilityError`); skip a blank full-width closing row.
|
|
35
|
+
- Completed lessons: strip whitespace and line breaks around the subject/teacher
|
|
36
|
+
comma, so `<b>Subject</b>, <br>Teacher` no longer yields the whole cell as the
|
|
37
|
+
subject with no teacher.
|
|
38
|
+
- Grades: recognize behaviour points in the `Zachowanie` row (a starting-points
|
|
39
|
+
or running-total tooltip, and point entries linking to behaviour details).
|
|
40
|
+
They are validated and not returned as grades, so the whole grade read no
|
|
41
|
+
longer fails on them (#76). Other content beside the points tooltip still
|
|
42
|
+
fails.
|
|
43
|
+
- Internal: share aiohttp session setup and message send dispatch in the
|
|
44
|
+
transport, remove an unused recipient constant, orphaned 0.4.x qualification
|
|
45
|
+
scripts and dead test-support code, and correct stale status sentences in the
|
|
46
|
+
contracts. Behaviour is unchanged.
|
|
47
|
+
- Development dependencies and pinned GitHub Actions are updated; runtime
|
|
48
|
+
dependency ranges are unchanged.
|
|
49
|
+
|
|
3
50
|
## 1.6.1 (2026-10-12) - Sunday grade dates and calendar closures
|
|
4
51
|
|
|
5
52
|
- 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.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
|
|
@@ -16,7 +16,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.14
|
|
17
17
|
Classifier: Typing :: Typed
|
|
18
18
|
Requires-Python: >=3.13
|
|
19
|
-
Requires-Dist: aiohttp<4,>=3.14.
|
|
19
|
+
Requires-Dist: aiohttp<4,>=3.14.4
|
|
20
20
|
Requires-Dist: lxml<7,>=6.1
|
|
21
21
|
Requires-Dist: pydantic<3,>=2.12
|
|
22
22
|
Requires-Dist: pywin32<400,>=312; sys_platform == 'win32'
|
|
@@ -42,8 +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.6.
|
|
46
|
-
|
|
45
|
+
**Status:** `1.6.3` requires an aiohttp release without known advisories and
|
|
46
|
+
keeps the 1.6.2 completed-lesson and grade layouts, including behaviour points,
|
|
47
|
+
which are validated and not reported as grades. 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
|
|
49
50
|
on what each account can access. See the [compatibility policy](API.md#compatibility-policy).
|
|
@@ -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.3
|
|
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.3-py3-none-any.whl
|
|
74
75
|
```
|
|
75
76
|
|
|
76
77
|
No CLI or background process is installed: import the library in your own program.
|
|
@@ -249,7 +250,7 @@ Completed lessons may be disabled by the school. Behaviour notes are not
|
|
|
249
250
|
implemented. Some recipient, archive and receipt layouts remain
|
|
250
251
|
unqualified; backend acceptance is not proof of delivery. Tests cover supported
|
|
251
252
|
contracts, not every school or role. Detailed coverage is in the
|
|
252
|
-
[verification log][verification]
|
|
253
|
+
[verification log][verification]; open work is tracked in [issues][issues].
|
|
253
254
|
|
|
254
255
|
## Development and support
|
|
255
256
|
|
|
@@ -264,7 +265,6 @@ MIT licensed. See [LICENSE][license].
|
|
|
264
265
|
[api]: https://github.com/krzysztofbury/librus-python-api/blob/main/API.md
|
|
265
266
|
[upgrade]: https://github.com/krzysztofbury/librus-python-api/blob/main/contracts/account-context.md
|
|
266
267
|
[verification]: https://github.com/krzysztofbury/librus-python-api/blob/main/VERIFICATION.md
|
|
267
|
-
[roadmap]: https://github.com/krzysztofbury/librus-python-api/blob/main/TODO.md
|
|
268
268
|
[contributing]: https://github.com/krzysztofbury/librus-python-api/blob/main/CONTRIBUTING.md
|
|
269
269
|
[issues]: https://github.com/krzysztofbury/librus-python-api/issues
|
|
270
270
|
[security]: https://github.com/krzysztofbury/librus-python-api/blob/main/SECURITY.md
|
|
@@ -16,8 +16,9 @@ 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.
|
|
20
|
-
|
|
19
|
+
**Status:** `1.6.3` requires an aiohttp release without known advisories and
|
|
20
|
+
keeps the 1.6.2 completed-lesson and grade layouts, including behaviour points,
|
|
21
|
+
which are validated and not reported as grades. 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
|
|
23
24
|
on what each account can access. See the [compatibility policy](API.md#compatibility-policy).
|
|
@@ -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.3
|
|
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.3-py3-none-any.whl
|
|
48
49
|
```
|
|
49
50
|
|
|
50
51
|
No CLI or background process is installed: import the library in your own program.
|
|
@@ -223,7 +224,7 @@ Completed lessons may be disabled by the school. Behaviour notes are not
|
|
|
223
224
|
implemented. Some recipient, archive and receipt layouts remain
|
|
224
225
|
unqualified; backend acceptance is not proof of delivery. Tests cover supported
|
|
225
226
|
contracts, not every school or role. Detailed coverage is in the
|
|
226
|
-
[verification log][verification]
|
|
227
|
+
[verification log][verification]; open work is tracked in [issues][issues].
|
|
227
228
|
|
|
228
229
|
## Development and support
|
|
229
230
|
|
|
@@ -238,7 +239,6 @@ MIT licensed. See [LICENSE][license].
|
|
|
238
239
|
[api]: https://github.com/krzysztofbury/librus-python-api/blob/main/API.md
|
|
239
240
|
[upgrade]: https://github.com/krzysztofbury/librus-python-api/blob/main/contracts/account-context.md
|
|
240
241
|
[verification]: https://github.com/krzysztofbury/librus-python-api/blob/main/VERIFICATION.md
|
|
241
|
-
[roadmap]: https://github.com/krzysztofbury/librus-python-api/blob/main/TODO.md
|
|
242
242
|
[contributing]: https://github.com/krzysztofbury/librus-python-api/blob/main/CONTRIBUTING.md
|
|
243
243
|
[issues]: https://github.com/krzysztofbury/librus-python-api/issues
|
|
244
244
|
[security]: https://github.com/krzysztofbury/librus-python-api/blob/main/SECURITY.md
|
|
@@ -21,7 +21,7 @@ classifiers = [
|
|
|
21
21
|
"Programming Language :: Python :: 3.14",
|
|
22
22
|
"Typing :: Typed",
|
|
23
23
|
]
|
|
24
|
-
dependencies = ["aiohttp>=3.14.
|
|
24
|
+
dependencies = ["aiohttp>=3.14.4,<4", "lxml>=6.1,<7", "pydantic>=2.12,<3", "yarl>=1.20,<2", "pywin32>=312,<400; sys_platform == 'win32'", "tzdata>=2026.5; sys_platform == 'win32'"]
|
|
25
25
|
|
|
26
26
|
[project.urls]
|
|
27
27
|
Repository = "https://github.com/krzysztofbury/librus-python-api"
|
|
@@ -35,7 +35,7 @@ dev = [
|
|
|
35
35
|
"openapi-spec-validator>=0.7,<0.10",
|
|
36
36
|
"pytest>=9,<10",
|
|
37
37
|
"pyyaml>=6,<7",
|
|
38
|
-
"ruff>=0.15,<0.
|
|
38
|
+
"ruff>=0.15,<0.18",
|
|
39
39
|
"types-pyyaml>=6,<7",
|
|
40
40
|
"types-lxml>=2026.2.16,<2027",
|
|
41
41
|
"types-pywin32>=312,<400",
|
|
@@ -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.3"
|
|
142
142
|
|
|
143
143
|
__all__ = [
|
|
144
144
|
"ArchiveAbsences",
|
{librus_python_api-1.6.1 → librus_python_api-1.6.3}/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.3}/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
|
|