librus-python-api 1.5.0__tar.gz → 1.6.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.5.0 → librus_python_api-1.6.1}/API.md +12 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/CHANGELOG.md +23 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/PKG-INFO +6 -6
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/README.md +5 -5
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/pyproject.toml +2 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/__init__.py +5 -1
- librus_python_api-1.6.1/src/librus_python_api/class_free_days.py +73 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/config.py +18 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/exceptions.py +7 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/markup.py +4 -2
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/models.py +19 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/school_reads.py +18 -3
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/service.py +42 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/.gitignore +0 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/LICENSE +0 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/SECURITY.md +0 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/_notification_bootstrap.py +0 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/_notification_codec.py +0 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/_storage.py +0 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/_windows_filesystem.py +0 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/announcements.py +0 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/attachment_routes.py +0 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/attachments.py +0 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/attendance.py +0 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/attendance_frequency.py +0 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/budget.py +0 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/checkpoint.py +0 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/completed_lessons.py +0 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/detail_fields.py +0 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/diagnostics.py +0 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/files.py +0 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/formative_grades.py +0 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/grade_parsers.py +0 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/grade_records.py +0 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/homework_range.py +0 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/lifecycle.py +0 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/message_content.py +0 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/messages.py +0 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/modern_body.py +0 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/modern_mailbox.py +0 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/modern_messages.py +0 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/notification_models.py +0 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/notification_persistence.py +0 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/notification_workflow.py +0 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/notifications.py +0 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/parsers.py +0 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/parsing.py +0 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/persistence.py +0 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/py.typed +0 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/recipients.py +0 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/scheduler.py +0 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/school_year_archive.py +0 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/sending.py +0 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/timetable.py +0 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/transport.py +0 -0
- {librus_python_api-1.5.0 → librus_python_api-1.6.1}/uv.lock +0 -0
|
@@ -261,6 +261,17 @@ section returns `()`. See [formative grades](contracts/grades.md#formative-grade
|
|
|
261
261
|
|
|
262
262
|
## Timetable
|
|
263
263
|
|
|
264
|
+
### Class free days
|
|
265
|
+
|
|
266
|
+
`class_free_days(*, budget=None, max_age_seconds=0.0)` returns frozen
|
|
267
|
+
`ClassFreeDays` with `identity`, `items` and `observation`. Each `ClassFreeDay`
|
|
268
|
+
contains `identifier`, `class_id`, `type_id`, civil `date_from`/`date_to` and
|
|
269
|
+
optional paired `lesson_no_from`/`lesson_no_to`. One bounded read-only GET;
|
|
270
|
+
reference URLs are not followed and type labels are not inferred. An explicit
|
|
271
|
+
empty collection returns `items=()`. See [contract](contracts/class-free-days.md).
|
|
272
|
+
|
|
273
|
+
### Weekly timetable
|
|
274
|
+
|
|
264
275
|
`timetable(monday)` takes a `date` that is a Monday and returns `Timetable` with
|
|
265
276
|
seven `TimetableDay`s. Each `TimetablePeriod` has a number, a local-time
|
|
266
277
|
`interval`, its `lessons` (subject and the combined teacher/classroom text),
|
|
@@ -1083,6 +1094,7 @@ text. `error_for(kind)` builds one.
|
|
|
1083
1094
|
| `SessionExpiredError` | Proven expiry that could not be recovered |
|
|
1084
1095
|
| `AccessDeniedError` | Denied, or an unexpected redirect or identity |
|
|
1085
1096
|
| `ViewDisabledError` | The school administrator disabled this view |
|
|
1097
|
+
| `ModuleUnavailableError` | The school's Synergia variant does not include this module; exact same-origin unavailable redirect or fixed page notice, without follow/retry ([contract](contracts/module-unavailable.md)) |
|
|
1086
1098
|
| `UnsupportedCapabilityError` | A recognized but unsupported layout or content |
|
|
1087
1099
|
| `ParseError` | The page or JSON does not match the expected structure |
|
|
1088
1100
|
| `StaleCursorError` | A previously valid continuation no longer matches the current sequence; restart explicitly |
|
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.6.1 (2026-10-12) - Sunday grade dates and calendar closures
|
|
4
|
+
|
|
5
|
+
- Accept both observed Sunday tooltip labels, `nd.` and `ndz.`, while retaining
|
|
6
|
+
the original grade metadata and strict rejection of unknown date suffixes.
|
|
7
|
+
- Preserve calendar closure events whose handler targets the separate closure
|
|
8
|
+
details page. Such events retain their text and date with `reference=None`;
|
|
9
|
+
they are never misrouted to ordinary agenda details or fetched automatically.
|
|
10
|
+
- Add regressions based on guarded structure-only observations with independently
|
|
11
|
+
authored text and IDs. Unknown closure handlers still fail closed.
|
|
12
|
+
|
|
13
|
+
## 1.6.0 (2026-10-09) - Class free days and capability outcomes
|
|
14
|
+
|
|
15
|
+
Compatible minor release with a new typed module-unavailable outcome. Consumers
|
|
16
|
+
matching error kinds exhaustively should handle `module_unavailable`.
|
|
17
|
+
|
|
18
|
+
- Add the bounded, account-isolated `class_free_days()` gateway read, frozen
|
|
19
|
+
`ClassFreeDays`/`ClassFreeDay` results and a guarded weekly check.
|
|
20
|
+
- Recognize the exact module-unavailable redirect and page notice as
|
|
21
|
+
`ModuleUnavailableError`, separate from disabled views and parser failures.
|
|
22
|
+
- Add an independent live-check freshness job to the dependency-drift workflow.
|
|
23
|
+
- Add reproducible targeted guard mutations and representative loopback load
|
|
24
|
+
measurements with scoped numerical thresholds. See [quality evidence](contracts/quality-evidence.md).
|
|
25
|
+
|
|
3
26
|
## 1.5.0 (2026-10-08) - Sent receipts with a recipient class column
|
|
4
27
|
|
|
5
28
|
Compatible minor release. Existing fields, results and the two-column receipt
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: librus-python-api
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.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,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.
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
**Status:** `1.6.1` fixes Sunday grade-date labels and calendar closure events.
|
|
46
|
+
Class free days, typed capability outcomes and the live-check freshness watchdog
|
|
47
|
+
remain available. 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.
|
|
57
|
+
python -m pip install librus-python-api==1.6.1
|
|
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.
|
|
73
|
+
python -m pip install ./dist/librus_python_api-1.6.1-py3-none-any.whl
|
|
74
74
|
```
|
|
75
75
|
|
|
76
76
|
No CLI or background process is installed: import the library in your own program.
|
|
@@ -16,9 +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.
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
**Status:** `1.6.1` fixes Sunday grade-date labels and calendar closure events.
|
|
20
|
+
Class free days, typed capability outcomes and the live-check freshness watchdog
|
|
21
|
+
remain available. See [verification](VERIFICATION.md).
|
|
22
22
|
The stable 1.x API does not guarantee every school layout. School features depend
|
|
23
23
|
on what each account can access. See the [compatibility policy](API.md#compatibility-policy).
|
|
24
24
|
See [limitations](#supported-features-and-limitations) below.
|
|
@@ -28,7 +28,7 @@ See [limitations](#supported-features-and-limitations) below.
|
|
|
28
28
|
Install the exact release from PyPI after its gated publication completes:
|
|
29
29
|
|
|
30
30
|
```sh
|
|
31
|
-
python -m pip install librus-python-api==1.
|
|
31
|
+
python -m pip install librus-python-api==1.6.1
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
Pin the exact version qualified by your application. Before publication, use a
|
|
@@ -44,7 +44,7 @@ python -m pip install .
|
|
|
44
44
|
To install a locally built wheel instead:
|
|
45
45
|
|
|
46
46
|
```sh
|
|
47
|
-
python -m pip install ./dist/librus_python_api-1.
|
|
47
|
+
python -m pip install ./dist/librus_python_api-1.6.1-py3-none-any.whl
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
No CLI or background process is installed: import the library in your own program.
|
|
@@ -56,6 +56,8 @@ include = ["/src", "/pyproject.toml", "/uv.lock", "/README.md", "/SECURITY.md",
|
|
|
56
56
|
testpaths = ["tests"]
|
|
57
57
|
pythonpath = ["."]
|
|
58
58
|
addopts = "--import-mode=importlib -ra -m 'not performance'"
|
|
59
|
+
# Dump every thread's stack if one test runs this long, so a hang names itself.
|
|
60
|
+
faulthandler_timeout = 300
|
|
59
61
|
markers = [
|
|
60
62
|
"performance: opt-in hardware-sensitive resource measurements",
|
|
61
63
|
]
|
|
@@ -35,6 +35,8 @@ from librus_python_api.models import (
|
|
|
35
35
|
AttendanceView,
|
|
36
36
|
AttendanceWindow,
|
|
37
37
|
Availability,
|
|
38
|
+
ClassFreeDay,
|
|
39
|
+
ClassFreeDays,
|
|
38
40
|
CompletedLesson,
|
|
39
41
|
CompletedLessons,
|
|
40
42
|
CompletedLessonsCursor,
|
|
@@ -136,7 +138,7 @@ from librus_python_api.models import (
|
|
|
136
138
|
from librus_python_api.sending import SendAttempt
|
|
137
139
|
from librus_python_api.service import AccountClient, LibrusService
|
|
138
140
|
|
|
139
|
-
__version__ = "1.
|
|
141
|
+
__version__ = "1.6.1"
|
|
140
142
|
|
|
141
143
|
__all__ = [
|
|
142
144
|
"ArchiveAbsences",
|
|
@@ -216,6 +218,8 @@ __all__ = [
|
|
|
216
218
|
"AccountClient",
|
|
217
219
|
"AccountCredentials",
|
|
218
220
|
"Availability",
|
|
221
|
+
"ClassFreeDay",
|
|
222
|
+
"ClassFreeDays",
|
|
219
223
|
"Attendance",
|
|
220
224
|
"Announcement",
|
|
221
225
|
"Agenda",
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Bounded gateway class free days from independently observed JSON structure."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from datetime import date
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from librus_python_api.config import CLASS_FREE_DAYS_MAX_RECORDS
|
|
8
|
+
from librus_python_api.exceptions import ErrorKind, LibrusError
|
|
9
|
+
from librus_python_api.models import ClassFreeDay
|
|
10
|
+
from librus_python_api.parsers import decode_json
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _identifier(value: Any) -> str:
|
|
14
|
+
if type(value) is not int or value < 0 or len(str(value)) > 64:
|
|
15
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
16
|
+
return str(value)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _reference(value: Any) -> str:
|
|
20
|
+
if not isinstance(value, dict):
|
|
21
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
22
|
+
return _identifier(value.get("Id"))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _day(value: Any) -> date:
|
|
26
|
+
result = None
|
|
27
|
+
if type(value) is str and re.fullmatch(r"[0-9]{4}-[0-9]{2}-[0-9]{2}", value):
|
|
28
|
+
try:
|
|
29
|
+
result = date.fromisoformat(value)
|
|
30
|
+
except ValueError:
|
|
31
|
+
pass
|
|
32
|
+
if result is None:
|
|
33
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
34
|
+
return result
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def parse_class_free_days(body: bytes) -> tuple[ClassFreeDay, ...]:
|
|
38
|
+
data = decode_json(body)
|
|
39
|
+
if not isinstance(data, dict) or not isinstance(data.get("ClassFreeDays"), list):
|
|
40
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
41
|
+
rows = data["ClassFreeDays"]
|
|
42
|
+
if len(rows) > CLASS_FREE_DAYS_MAX_RECORDS:
|
|
43
|
+
raise LibrusError(ErrorKind.LIMIT)
|
|
44
|
+
items: list[ClassFreeDay] = []
|
|
45
|
+
seen: set[str] = set()
|
|
46
|
+
for row in rows:
|
|
47
|
+
if not isinstance(row, dict):
|
|
48
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
49
|
+
identifier = _identifier(row.get("Id"))
|
|
50
|
+
start, end = _day(row.get("DateFrom")), _day(row.get("DateTo"))
|
|
51
|
+
first, last = row.get("LessonNoFrom"), row.get("LessonNoTo")
|
|
52
|
+
if "LessonNoFrom" in row or "LessonNoTo" in row:
|
|
53
|
+
if (
|
|
54
|
+
type(first) is not int
|
|
55
|
+
or type(last) is not int
|
|
56
|
+
or not 0 <= first <= last <= 99
|
|
57
|
+
):
|
|
58
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
59
|
+
if identifier in seen or start > end:
|
|
60
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
61
|
+
seen.add(identifier)
|
|
62
|
+
items.append(
|
|
63
|
+
ClassFreeDay(
|
|
64
|
+
identifier,
|
|
65
|
+
_reference(row.get("Class")),
|
|
66
|
+
_reference(row.get("Type")),
|
|
67
|
+
start,
|
|
68
|
+
end,
|
|
69
|
+
first,
|
|
70
|
+
last,
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
return tuple(items)
|
|
@@ -449,6 +449,14 @@ ENDPOINTS: Mapping[str, Endpoint] = MappingProxyType(
|
|
|
449
449
|
True,
|
|
450
450
|
Evidence.INDEPENDENTLY_OBSERVED,
|
|
451
451
|
),
|
|
452
|
+
Endpoint(
|
|
453
|
+
"class_free_days",
|
|
454
|
+
"GET",
|
|
455
|
+
"/gateway/api/2.0/ClassFreeDays",
|
|
456
|
+
SideEffect.NONE,
|
|
457
|
+
True,
|
|
458
|
+
Evidence.INDEPENDENTLY_OBSERVED,
|
|
459
|
+
),
|
|
452
460
|
Endpoint(
|
|
453
461
|
"gateway_attendance",
|
|
454
462
|
"GET",
|
|
@@ -852,21 +860,31 @@ HOMEWORK_MARK_DONE_HANDLER = (
|
|
|
852
860
|
r"\s*showConfirmQuestion\(\s*[0-9]{1,64}\s*,\s*[0-9]{1,64}\s*\);?\s*"
|
|
853
861
|
)
|
|
854
862
|
WEEKDAY_LABELS = ("pon.", "wt.", "śr.", "czw.", "pt.", "sob.", "ndz.")
|
|
863
|
+
# Grade tooltips also use the short Sunday display label.
|
|
864
|
+
CIVIL_DATE_WEEKDAY_LABELS = frozenset((*WEEKDAY_LABELS, "nd."))
|
|
855
865
|
# Observed page-level notices shown instead of the requested content.
|
|
856
866
|
PAGE_NOTICES = MappingProxyType(
|
|
857
867
|
{
|
|
868
|
+
(
|
|
869
|
+
"Ten moduł nie jest dostępny w wykorzystywanym przez szkołę wariancie "
|
|
870
|
+
"rozwiązania LIBRUS Synergia."
|
|
871
|
+
): ErrorKind.MODULE_UNAVAILABLE,
|
|
858
872
|
"Ten widok został wyłączony przez administratora szkoły.": (
|
|
859
873
|
ErrorKind.VIEW_DISABLED
|
|
860
874
|
),
|
|
861
875
|
"Wybrano nieprawidłowy zakres daty.": ErrorKind.INVALID_INPUT,
|
|
862
876
|
}
|
|
863
877
|
)
|
|
878
|
+
MODULE_UNAVAILABLE_PATH = "/modul_niedostepny"
|
|
879
|
+
CLASS_FREE_DAYS_MAX_RECORDS = 4096
|
|
864
880
|
COMPLETED_LESSONS_MAX_WINDOW_DAYS = 371
|
|
865
881
|
COMPLETED_LESSONS_MAX_PAGE_COUNT = 1000
|
|
866
882
|
COMPLETED_LESSONS_MAX_PAGE_ITEMS = 256
|
|
867
883
|
COMPLETED_LESSONS_MAX_BATCH_PAGES = 8
|
|
868
884
|
COMPLETED_LESSONS_MAX_BATCH_ITEMS = 256
|
|
869
885
|
AGENDA_DETAIL_PATH_PREFIX = "/terminarz/szczegoly/"
|
|
886
|
+
# Recognition only: closure details are not ordinary agenda-detail references.
|
|
887
|
+
AGENDA_CLOSURE_DETAIL_PATH_PREFIX = "/terminarz/szczegoly_wolne/"
|
|
870
888
|
HOMEWORK_DETAIL_PATH_PREFIX = "/moje_zadania/podglad/"
|
|
871
889
|
MESSAGE_MAX_PAGE_COUNT = 1000
|
|
872
890
|
MESSAGE_MAX_PAGE_ITEMS = 50
|
|
@@ -14,6 +14,7 @@ __all__ = [
|
|
|
14
14
|
"LibrusError",
|
|
15
15
|
"LimitError",
|
|
16
16
|
"MaintenanceError",
|
|
17
|
+
"ModuleUnavailableError",
|
|
17
18
|
"OperationTimeoutError",
|
|
18
19
|
"ParseError",
|
|
19
20
|
"SessionExpiredError",
|
|
@@ -36,6 +37,7 @@ class ErrorKind(StrEnum):
|
|
|
36
37
|
ACCESS_DENIED = "access_denied"
|
|
37
38
|
UNSUPPORTED_CAPABILITY = "unsupported_capability"
|
|
38
39
|
VIEW_DISABLED = "view_disabled"
|
|
40
|
+
MODULE_UNAVAILABLE = "module_unavailable"
|
|
39
41
|
THROTTLED = "throttled"
|
|
40
42
|
MAINTENANCE = "maintenance"
|
|
41
43
|
CONNECTION = "connection"
|
|
@@ -101,6 +103,10 @@ class ViewDisabledError(LibrusError):
|
|
|
101
103
|
"""The school administrator has switched this Synergia view off."""
|
|
102
104
|
|
|
103
105
|
|
|
106
|
+
class ModuleUnavailableError(LibrusError):
|
|
107
|
+
"""The school's Synergia product variant does not include this module."""
|
|
108
|
+
|
|
109
|
+
|
|
104
110
|
class ThrottledError(LibrusError):
|
|
105
111
|
pass
|
|
106
112
|
|
|
@@ -154,6 +160,7 @@ _ERROR_TYPES: dict[ErrorKind, type[LibrusError]] = {
|
|
|
154
160
|
ErrorKind.ACCESS_DENIED: AccessDeniedError,
|
|
155
161
|
ErrorKind.UNSUPPORTED_CAPABILITY: UnsupportedCapabilityError,
|
|
156
162
|
ErrorKind.VIEW_DISABLED: ViewDisabledError,
|
|
163
|
+
ErrorKind.MODULE_UNAVAILABLE: ModuleUnavailableError,
|
|
157
164
|
ErrorKind.THROTTLED: ThrottledError,
|
|
158
165
|
ErrorKind.MAINTENANCE: MaintenanceError,
|
|
159
166
|
ErrorKind.CONNECTION: ConnectionError,
|
|
@@ -9,11 +9,11 @@ from lxml import etree, html
|
|
|
9
9
|
|
|
10
10
|
from librus_python_api.config import (
|
|
11
11
|
ATTENDANCE_DETAIL_PATH_PREFIX,
|
|
12
|
+
CIVIL_DATE_WEEKDAY_LABELS,
|
|
12
13
|
GRADE_MAX_COLUMNS,
|
|
13
14
|
GRADE_MAX_METADATA_FIELDS,
|
|
14
15
|
GRADE_MAX_METADATA_LENGTH,
|
|
15
16
|
UPSTREAM_ORIGINS,
|
|
16
|
-
WEEKDAY_LABELS,
|
|
17
17
|
)
|
|
18
18
|
from librus_python_api.exceptions import ErrorKind, LibrusError
|
|
19
19
|
from librus_python_api.parsers import parse_html_document
|
|
@@ -109,7 +109,9 @@ def tooltip_fields(element: html.HtmlElement) -> dict[str, str]:
|
|
|
109
109
|
def civil_date(value: str | None) -> date:
|
|
110
110
|
"""An ISO date, optionally followed by its weekday label in parentheses."""
|
|
111
111
|
match = re.fullmatch(r"([0-9]{4}-[0-9]{2}-[0-9]{2})(?: \(([^()]+)\))?", value or "")
|
|
112
|
-
if match is None or (
|
|
112
|
+
if match is None or (
|
|
113
|
+
match[2] is not None and match[2] not in CIVIL_DATE_WEEKDAY_LABELS
|
|
114
|
+
):
|
|
113
115
|
raise LibrusError(ErrorKind.PARSE)
|
|
114
116
|
try:
|
|
115
117
|
return date.fromisoformat(match[1])
|
|
@@ -16,6 +16,7 @@ from librus_python_api.exceptions import ErrorKind
|
|
|
16
16
|
|
|
17
17
|
type OperationName = Literal[
|
|
18
18
|
"identity",
|
|
19
|
+
"class_free_days",
|
|
19
20
|
"student_information",
|
|
20
21
|
"final_grades",
|
|
21
22
|
"grades",
|
|
@@ -632,6 +633,24 @@ class CompletedLessons:
|
|
|
632
633
|
observation: "Observation"
|
|
633
634
|
|
|
634
635
|
|
|
636
|
+
@dataclass(frozen=True, slots=True)
|
|
637
|
+
class ClassFreeDay:
|
|
638
|
+
identifier: str = field(repr=False)
|
|
639
|
+
class_id: str = field(repr=False)
|
|
640
|
+
type_id: str = field(repr=False)
|
|
641
|
+
date_from: date = field(repr=False)
|
|
642
|
+
date_to: date = field(repr=False)
|
|
643
|
+
lesson_no_from: int | None = None
|
|
644
|
+
lesson_no_to: int | None = None
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
@dataclass(frozen=True, slots=True)
|
|
648
|
+
class ClassFreeDays:
|
|
649
|
+
identity: "Identity" = field(repr=False)
|
|
650
|
+
items: tuple[ClassFreeDay, ...] = field(repr=False)
|
|
651
|
+
observation: "Observation"
|
|
652
|
+
|
|
653
|
+
|
|
635
654
|
class GradeKind(StrEnum):
|
|
636
655
|
CURRENT = "current"
|
|
637
656
|
PERIOD = "period"
|
|
@@ -9,6 +9,7 @@ from lxml import html
|
|
|
9
9
|
|
|
10
10
|
from librus_python_api import markup
|
|
11
11
|
from librus_python_api.config import (
|
|
12
|
+
AGENDA_CLOSURE_DETAIL_PATH_PREFIX,
|
|
12
13
|
AGENDA_DESCRIPTION_LABEL,
|
|
13
14
|
AGENDA_DETAIL_PATH_PREFIX,
|
|
14
15
|
AGENDA_TOOLTIP_LABELS,
|
|
@@ -42,13 +43,14 @@ def school_reference(
|
|
|
42
43
|
) -> SchoolReference | None:
|
|
43
44
|
"""Find the single detail link.
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
mark-done button;
|
|
46
|
+
Closure handlers stay inert without an ordinary agenda-detail reference.
|
|
47
|
+
Homework rows also carry a mark-done button; handlers are never followed.
|
|
47
48
|
"""
|
|
48
49
|
prefix = (
|
|
49
50
|
AGENDA_DETAIL_PATH_PREFIX if kind == "agenda" else HOMEWORK_DETAIL_PATH_PREFIX
|
|
50
51
|
)
|
|
51
52
|
found: set[str] = set()
|
|
53
|
+
closure = False
|
|
52
54
|
for node in nodes:
|
|
53
55
|
if not isinstance(node.tag, str):
|
|
54
56
|
continue
|
|
@@ -57,6 +59,19 @@ def school_reference(
|
|
|
57
59
|
raise LibrusError(ErrorKind.LIMIT)
|
|
58
60
|
if not action:
|
|
59
61
|
continue
|
|
62
|
+
if (
|
|
63
|
+
kind == "agenda"
|
|
64
|
+
and node is nodes[0]
|
|
65
|
+
and node.tag == "td"
|
|
66
|
+
and re.fullmatch(
|
|
67
|
+
r"location\.href='"
|
|
68
|
+
+ re.escape(AGENDA_CLOSURE_DETAIL_PATH_PREFIX)
|
|
69
|
+
+ r"[0-9]{1,64}'",
|
|
70
|
+
action,
|
|
71
|
+
)
|
|
72
|
+
):
|
|
73
|
+
closure = True
|
|
74
|
+
continue
|
|
60
75
|
if kind == "homework" and prefix not in action:
|
|
61
76
|
# Only the observed mark-done button is inert; anything else is new.
|
|
62
77
|
if re.fullmatch(HOMEWORK_MARK_DONE_HANDLER, action):
|
|
@@ -66,7 +81,7 @@ def school_reference(
|
|
|
66
81
|
if len(paths) != 1:
|
|
67
82
|
raise LibrusError(ErrorKind.UNSUPPORTED_CAPABILITY)
|
|
68
83
|
found.add(paths[0].removeprefix(prefix))
|
|
69
|
-
if len(found) > 1:
|
|
84
|
+
if len(found) > 1 or (closure and found):
|
|
70
85
|
raise LibrusError(ErrorKind.PARSE)
|
|
71
86
|
return SchoolReference(kind, next(iter(found)), account) if found else None
|
|
72
87
|
|
|
@@ -27,6 +27,7 @@ from librus_python_api.attendance_frequency import (
|
|
|
27
27
|
)
|
|
28
28
|
from librus_python_api.budget import RequestBudget
|
|
29
29
|
from librus_python_api.checkpoint import CHECKPOINT_SERVICE, validate_checkpoint
|
|
30
|
+
from librus_python_api.class_free_days import parse_class_free_days
|
|
30
31
|
from librus_python_api.completed_lessons import (
|
|
31
32
|
parse_completed_lessons,
|
|
32
33
|
validate_selection,
|
|
@@ -42,6 +43,7 @@ from librus_python_api.config import (
|
|
|
42
43
|
GRADE_MAX_WINDOW_DAYS,
|
|
43
44
|
MESSAGE_MAX_CURSOR_IDS,
|
|
44
45
|
MODERN_RECIPIENT_OPERATIONS,
|
|
46
|
+
MODULE_UNAVAILABLE_PATH,
|
|
45
47
|
NOTIFICATION_COUNTS_ENDPOINT,
|
|
46
48
|
SCHEDULE_RESPONSE_VERSION,
|
|
47
49
|
SESSION_COOKIE,
|
|
@@ -83,6 +85,7 @@ from librus_python_api.models import (
|
|
|
83
85
|
AttendanceFrequency,
|
|
84
86
|
AttendanceView,
|
|
85
87
|
AttendanceWindow,
|
|
88
|
+
ClassFreeDays,
|
|
86
89
|
CompletedLesson,
|
|
87
90
|
CompletedLessons,
|
|
88
91
|
CompletedLessonsCursor,
|
|
@@ -1314,6 +1317,24 @@ class AccountClient:
|
|
|
1314
1317
|
),
|
|
1315
1318
|
)
|
|
1316
1319
|
|
|
1320
|
+
async def class_free_days(
|
|
1321
|
+
self,
|
|
1322
|
+
*,
|
|
1323
|
+
budget: RequestBudget | None = None,
|
|
1324
|
+
max_age_seconds: float = 0.0,
|
|
1325
|
+
) -> ClassFreeDays:
|
|
1326
|
+
"""Read one bounded collection; dates are civil and references stay opaque."""
|
|
1327
|
+
|
|
1328
|
+
async def fetch(budget: RequestBudget, _: bool) -> ClassFreeDays:
|
|
1329
|
+
items = await self._page(
|
|
1330
|
+
"class_free_days", budget, parse_class_free_days, content_type=JSON
|
|
1331
|
+
)
|
|
1332
|
+
return ClassFreeDays(
|
|
1333
|
+
self._session_identity(), items, self._observation("class_free_days")
|
|
1334
|
+
)
|
|
1335
|
+
|
|
1336
|
+
return await self._read(("class_free_days",), fetch, budget, max_age_seconds)
|
|
1337
|
+
|
|
1317
1338
|
async def attendance(
|
|
1318
1339
|
self,
|
|
1319
1340
|
*,
|
|
@@ -2438,6 +2459,8 @@ class AccountClient:
|
|
|
2438
2459
|
self, response: TransportResponse, content_type: str
|
|
2439
2460
|
) -> None:
|
|
2440
2461
|
if 300 <= response.status < 400:
|
|
2462
|
+
if self._is_module_unavailable_redirect(response):
|
|
2463
|
+
raise LibrusError(ErrorKind.MODULE_UNAVAILABLE)
|
|
2441
2464
|
# A redirect proves expiry only when it targets a login route.
|
|
2442
2465
|
# Other redirects must not trigger a new credential submission.
|
|
2443
2466
|
if self._is_login_redirect(response):
|
|
@@ -2446,6 +2469,25 @@ class AccountClient:
|
|
|
2446
2469
|
if response.status != 200 or _media_type(response) != content_type:
|
|
2447
2470
|
raise LibrusError(ErrorKind.PARSE)
|
|
2448
2471
|
|
|
2472
|
+
def _is_module_unavailable_redirect(self, response: TransportResponse) -> bool:
|
|
2473
|
+
location = response.headers.get("location", "")
|
|
2474
|
+
if not location or len(location) > 4096:
|
|
2475
|
+
return False
|
|
2476
|
+
try:
|
|
2477
|
+
target = urlsplit(urljoin(response.url, location))
|
|
2478
|
+
source = urlsplit(response.url)
|
|
2479
|
+
except ValueError:
|
|
2480
|
+
return False
|
|
2481
|
+
expected = urlsplit(self._service._connection.origin(ENDPOINTS["identity"]))
|
|
2482
|
+
return (
|
|
2483
|
+
(source.scheme, source.netloc) == (expected.scheme, expected.netloc)
|
|
2484
|
+
and (target.scheme, target.netloc) == (expected.scheme, expected.netloc)
|
|
2485
|
+
and not (
|
|
2486
|
+
target.username or target.password or target.query or target.fragment
|
|
2487
|
+
)
|
|
2488
|
+
and target.path == MODULE_UNAVAILABLE_PATH
|
|
2489
|
+
)
|
|
2490
|
+
|
|
2449
2491
|
def _is_login_redirect(self, response: TransportResponse) -> bool:
|
|
2450
2492
|
location = response.headers.get("location", "")
|
|
2451
2493
|
try:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/_notification_bootstrap.py
RENAMED
|
File without changes
|
{librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/_notification_codec.py
RENAMED
|
File without changes
|
|
File without changes
|
{librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/_windows_filesystem.py
RENAMED
|
File without changes
|
|
File without changes
|
{librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/attachment_routes.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/attendance_frequency.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/completed_lessons.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/formative_grades.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/message_content.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/modern_messages.py
RENAMED
|
File without changes
|
{librus_python_api-1.5.0 → librus_python_api-1.6.1}/src/librus_python_api/notification_models.py
RENAMED
|
File without changes
|
|
File without changes
|
{librus_python_api-1.5.0 → librus_python_api-1.6.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
|
{librus_python_api-1.5.0 → librus_python_api-1.6.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
|