librus-python-api 1.8.3__tar.gz → 1.8.4__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.8.3 → librus_python_api-1.8.4}/API.md +31 -8
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/CHANGELOG.md +36 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/PKG-INFO +7 -4
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/README.md +6 -3
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/__init__.py +1 -1
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/config.py +9 -3
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/grade_records.py +12 -1
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/inspect_page.py +1 -1
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/markup.py +42 -6
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/notification_workflow.py +2 -1
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/service.py +16 -19
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/.gitignore +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/LICENSE +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/SECURITY.md +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/pyproject.toml +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/_notification_bootstrap.py +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/_notification_codec.py +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/_storage.py +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/_windows_filesystem.py +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/announcements.py +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/attachment_routes.py +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/attachments.py +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/attendance.py +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/attendance_frequency.py +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/budget.py +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/checkpoint.py +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/class_free_days.py +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/completed_lessons.py +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/detail_fields.py +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/diagnostics.py +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/exceptions.py +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/files.py +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/formative_grades.py +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/gateway_grades.py +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/grade_parsers.py +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/homework_range.py +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/lifecycle.py +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/message_content.py +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/messages.py +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/models.py +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/modern_body.py +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/modern_mailbox.py +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/modern_messages.py +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/notification_models.py +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/notification_persistence.py +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/notifications.py +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/parsers.py +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/parsing.py +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/persistence.py +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/py.typed +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/recipients.py +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/scheduler.py +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/school_reads.py +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/school_year_archive.py +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/sending.py +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/timetable.py +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/transport.py +0 -0
- {librus_python_api-1.8.3 → librus_python_api-1.8.4}/uv.lock +0 -0
|
@@ -287,12 +287,31 @@ such as `(0-100)`, not a grade symbol. The table's "Wyn." period results are
|
|
|
287
287
|
into `averages`), and a point table with titled summary columns adds its
|
|
288
288
|
subjects to `final_grades()`. See [point grades](contracts/grades.md#point-grades-170).
|
|
289
289
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
290
|
+
Behaviour grade symbols (1.8.3): an entry in the behaviour row with a grade
|
|
291
|
+
symbol such as `wz`, linking to a behaviour detail, is a `NumericGrade` in
|
|
292
|
+
`numeric`. As for point grades, `category` is the tooltip's unlabelled first
|
|
293
|
+
line (such as `wzorowe`), `day` is `Data wystawienia`, `teacher` stays `None`
|
|
294
|
+
and `Dodał` is kept only in `metadata`. See [grades](contracts/grades.md).
|
|
295
|
+
|
|
296
|
+
Schools on the new descriptive grading schema show a subject table
|
|
297
|
+
(`newSchemaGradesByStudentTable`) in one of two layouts. Every view checks it
|
|
298
|
+
the same way (from 1.8.4; before, only the `ALL` view did):
|
|
299
|
+
|
|
300
|
+
- Empty semester cells (`gradesCell` hooks) that the page script fills from
|
|
301
|
+
the gateway. In the `ALL` view `grades()` reads those grades from the
|
|
302
|
+
gateway, as described below. `WEEK` and `LAST_LOGIN` contain none of them.
|
|
303
|
+
- Semester cells holding bare text, such as `5` or `Brak ocen` (1.8.3). The
|
|
304
|
+
text becomes undated `descriptive_summaries` (`Brak ocen` is empty) and no
|
|
305
|
+
view makes a gateway request. Undated summaries are not notified.
|
|
306
|
+
- Anything else fails the read: blank cells without hooks, a cell with an
|
|
307
|
+
attribute or markup, both layouts in one table, a bare-text table that still
|
|
308
|
+
has `studentGradesDetailsRow` rows (`UnsupportedCapabilityError`), or rows
|
|
309
|
+
of more than one student (`ParseError`).
|
|
310
|
+
|
|
311
|
+
Gateway descriptive grades (1.8.0): with the empty layout, the `ALL` view
|
|
312
|
+
reads the grades from the gateway with the login's session and the call's
|
|
313
|
+
budget: three more requests, five when there are grades. They are
|
|
314
|
+
`DescriptiveGrade`s in `descriptive` (kind `CURRENT`, no
|
|
296
315
|
`href`), with the page's subject name, the scale value or else the content as
|
|
297
316
|
`raw`, the civil date, semester 1 or 2, the teacher's name when listed, the
|
|
298
317
|
comment and metadata pairs for the present fields only (`Obszar`, `Treść`,
|
|
@@ -804,7 +823,11 @@ async with NotificationStore(Path("/absolute/private/notification-state")) as st
|
|
|
804
823
|
announcements use the ordinary collection. No menu counts, message content,
|
|
805
824
|
detail calls, modern fallback or arbitrary historical catch-up is performed.
|
|
806
825
|
- New `AGENDA` consumption requires explicit `allow_consume_events=True` each
|
|
807
|
-
time.
|
|
826
|
+
time. Without it, and with no stored raw batch to replay, `poll()` and
|
|
827
|
+
`consume_schedule_events()` raise `ConsentRequiredError` before any request
|
|
828
|
+
(from 1.8.4; before, a plain `InvalidInputError`). Other invalid inputs,
|
|
829
|
+
such as repeated categories, stay `InvalidInputError`.
|
|
830
|
+
Complete encoded raw bytes and original metadata commit before parsing.
|
|
808
831
|
Existing raw checkpoints replay locally before another consume. Malformed raw
|
|
809
832
|
data remains stored and blocks another consume, even with fresh consent.
|
|
810
833
|
- `NotificationBatch(receipt, context, first_run, categories, items,
|
|
@@ -1162,7 +1185,7 @@ text. `error_for(kind)` builds one.
|
|
|
1162
1185
|
| Error | Meaning |
|
|
1163
1186
|
| --- | --- |
|
|
1164
1187
|
| `InvalidInputError` | Rejected before any request, or upstream rejected the selection |
|
|
1165
|
-
| `ConsentRequiredError` | A subclass of `InvalidInputError` (1.8.1): a call with an effect the user must agree to, such as opening a received message, was made without that consent; nothing was requested |
|
|
1188
|
+
| `ConsentRequiredError` | A subclass of `InvalidInputError` (1.8.1): a call with an effect the user must agree to, such as opening a received message, was made without that consent; nothing was requested. Since 1.8.4 also consuming new calendar events without `allow_consume_events=True` |
|
|
1166
1189
|
| `CredentialsRejectedError` | Login and password refused |
|
|
1167
1190
|
| `AccountActionRequiredError` | CAPTCHA, 2FA or another interactive step is required |
|
|
1168
1191
|
| `SessionExpiredError` | Proven expiry that could not be recovered |
|
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.8.4 (2026-11-08) - Multi-line formative tooltips, grade checks in every view, calendar consent
|
|
4
|
+
|
|
5
|
+
Signatures are unchanged.
|
|
6
|
+
|
|
7
|
+
- Grades: the week and last-login views run the same new-schema descriptive
|
|
8
|
+
table checks as the all view (#123). They used to return summaries or
|
|
9
|
+
nothing for a table the all view rejects: blank or classed semester cells,
|
|
10
|
+
a filled table with gateway detail rows, mixed layouts or rows of more than
|
|
11
|
+
one student. Notification polls read the last-login view, so such a page
|
|
12
|
+
now fails the poll instead of passing silently. Only the all view with
|
|
13
|
+
empty `gradesCell` hooks reads the gateway, as before.
|
|
14
|
+
- Grades: a formative entry in the grade grid whose tooltip carries a
|
|
15
|
+
multi-line assessment text after `Ocena` no longer fails every grade read of
|
|
16
|
+
the login with `ParseError`. Found on two of the owner's logins by the live
|
|
17
|
+
check for this release (2026-11-08); 1.8.3 fails the same way. Unlabelled
|
|
18
|
+
lines continue `Ocena`, or a teacher-written field, with newlines. Tooltips
|
|
19
|
+
that already parsed keep the same metadata, so notification identities do
|
|
20
|
+
not change; the same lines in an ordinary grade's tooltip still fail.
|
|
21
|
+
- Grades: a grade that corrects two earlier grades no longer fails the week
|
|
22
|
+
view with `ParseError`. Its tooltip lists one `Poprawa oceny` line per
|
|
23
|
+
corrected grade (seen live on 2026-11-08 in the week view of two owner
|
|
24
|
+
logins); the values are kept in order in one metadata pair, joined by a
|
|
25
|
+
newline. Other repeated labels still fail.
|
|
26
|
+
- Notifications (krzysztofbury/librus-mcp#91): polling `AGENDA` without
|
|
27
|
+
`allow_consume_events=True` when no stored raw batch can be replayed, and
|
|
28
|
+
`consume_schedule_events()` with
|
|
29
|
+
`allow_consume_events=False`, raise `ConsentRequiredError` instead of a
|
|
30
|
+
plain invalid input, so a caller can ask the user. Nothing is requested
|
|
31
|
+
either way. `except InvalidInputError` still catches it; code comparing
|
|
32
|
+
`kind` with `INVALID_INPUT` sees the new kind. Repeated categories and a
|
|
33
|
+
non-boolean flag stay `InvalidInputError`.
|
|
34
|
+
- Docs: `API.md`, the `grades()` docstring and `contracts/grades.md` describe
|
|
35
|
+
both new-schema table layouts, which views read the gateway, and behaviour
|
|
36
|
+
grade symbols (#124).
|
|
37
|
+
- The weekly live check also reads the week and last-login grade views.
|
|
38
|
+
|
|
3
39
|
## 1.8.3 (2026-11-06) - Reported grade, attendance and announcement layouts
|
|
4
40
|
|
|
5
41
|
Thank you, @wiktorw and @Witoso, for the reports and the careful synthetic
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: librus-python-api
|
|
3
|
-
Version: 1.8.
|
|
3
|
+
Version: 1.8.4
|
|
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,10 @@ 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.8.
|
|
45
|
+
**Status:** `1.8.4` reads multi-line formative assessment text and repeated
|
|
46
|
+
corrections in grade tooltips, checks the new-schema grade table in every grade view and reports
|
|
47
|
+
missing calendar-consumption consent as `ConsentRequiredError`.
|
|
48
|
+
`1.8.3` reads behaviour grade symbols, filled descriptive
|
|
46
49
|
grade tables, the "Aktualnie brak ogłoszeń" page and absences excused
|
|
47
50
|
through e-Usprawiedliwienia. `1.8.2` keeps the modern mailbox session across a cancelled read
|
|
48
51
|
or a reached limit and sends its own version in the `User-Agent`. `1.8.1` recovers a session Synergia no longer knows (it answers
|
|
@@ -66,7 +69,7 @@ See [limitations](#supported-features-and-limitations) below.
|
|
|
66
69
|
Install the exact release from PyPI after its gated publication completes:
|
|
67
70
|
|
|
68
71
|
```sh
|
|
69
|
-
python -m pip install librus-python-api==1.8.
|
|
72
|
+
python -m pip install librus-python-api==1.8.4
|
|
70
73
|
```
|
|
71
74
|
|
|
72
75
|
Pin the exact version qualified by your application. Before publication, use a
|
|
@@ -82,7 +85,7 @@ python -m pip install .
|
|
|
82
85
|
To install a locally built wheel instead:
|
|
83
86
|
|
|
84
87
|
```sh
|
|
85
|
-
python -m pip install ./dist/librus_python_api-1.8.
|
|
88
|
+
python -m pip install ./dist/librus_python_api-1.8.4-py3-none-any.whl
|
|
86
89
|
```
|
|
87
90
|
|
|
88
91
|
No console script or background process is installed: import the library in your
|
|
@@ -16,7 +16,10 @@ 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.8.
|
|
19
|
+
**Status:** `1.8.4` reads multi-line formative assessment text and repeated
|
|
20
|
+
corrections in grade tooltips, checks the new-schema grade table in every grade view and reports
|
|
21
|
+
missing calendar-consumption consent as `ConsentRequiredError`.
|
|
22
|
+
`1.8.3` reads behaviour grade symbols, filled descriptive
|
|
20
23
|
grade tables, the "Aktualnie brak ogłoszeń" page and absences excused
|
|
21
24
|
through e-Usprawiedliwienia. `1.8.2` keeps the modern mailbox session across a cancelled read
|
|
22
25
|
or a reached limit and sends its own version in the `User-Agent`. `1.8.1` recovers a session Synergia no longer knows (it answers
|
|
@@ -40,7 +43,7 @@ See [limitations](#supported-features-and-limitations) below.
|
|
|
40
43
|
Install the exact release from PyPI after its gated publication completes:
|
|
41
44
|
|
|
42
45
|
```sh
|
|
43
|
-
python -m pip install librus-python-api==1.8.
|
|
46
|
+
python -m pip install librus-python-api==1.8.4
|
|
44
47
|
```
|
|
45
48
|
|
|
46
49
|
Pin the exact version qualified by your application. Before publication, use a
|
|
@@ -56,7 +59,7 @@ python -m pip install .
|
|
|
56
59
|
To install a locally built wheel instead:
|
|
57
60
|
|
|
58
61
|
```sh
|
|
59
|
-
python -m pip install ./dist/librus_python_api-1.8.
|
|
62
|
+
python -m pip install ./dist/librus_python_api-1.8.4-py3-none-any.whl
|
|
60
63
|
```
|
|
61
64
|
|
|
62
65
|
No console script or background process is installed: import the library in your
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Independent bounded async client. General live compatibility is unqualified."""
|
|
2
2
|
|
|
3
3
|
# Before the imports: config reads it for the User-Agent.
|
|
4
|
-
__version__ = "1.8.
|
|
4
|
+
__version__ = "1.8.4"
|
|
5
5
|
|
|
6
6
|
from librus_python_api.attachments import AttachmentStream, ModernAttachmentStream
|
|
7
7
|
from librus_python_api.budget import RequestBudget
|
|
@@ -721,8 +721,12 @@ GRADE_MAX_VALUE_LENGTH = 1024
|
|
|
721
721
|
GRADE_MERGED_SUBJECTS = frozenset({"Zachowanie"})
|
|
722
722
|
GRADE_INLINE_DETAIL_LABEL = "Ocena"
|
|
723
723
|
# Tooltip fields whose free text may continue on unlabelled lines. A teacher's
|
|
724
|
-
# multi-line grade comment was observed on 2026-10-27.
|
|
724
|
+
# multi-line grade comment was observed on 2026-10-27. A formative mirror's
|
|
725
|
+
# assessment text after `Ocena` also spans lines (2026-11-08, markup.py).
|
|
725
726
|
TOOLTIP_MULTILINE_LABELS = frozenset({"Komentarz"})
|
|
727
|
+
# Labels a tooltip may repeat. A grade correcting two earlier grades lists one
|
|
728
|
+
# `Poprawa oceny` line per corrected grade in the week view (2026-11-08).
|
|
729
|
+
TOOLTIP_REPEATABLE_LABELS = frozenset({"Poprawa oceny"})
|
|
726
730
|
# Tooltip labels observed on grade, attendance and point pages. Diagnostics
|
|
727
731
|
# show only these by name; any other label is masked as teacher-entered text.
|
|
728
732
|
TOOLTIP_LABELS = frozenset(
|
|
@@ -744,6 +748,7 @@ TOOLTIP_LABELS = frozenset(
|
|
|
744
748
|
"Nauczyciel",
|
|
745
749
|
"Obowiązek wyk. zadania",
|
|
746
750
|
"Ocena",
|
|
751
|
+
"Poprawa oceny",
|
|
747
752
|
"Rodzaj",
|
|
748
753
|
"Suma punktów",
|
|
749
754
|
"Temat zajęć",
|
|
@@ -783,8 +788,9 @@ GRADE_PERIOD_HEADERS = MappingProxyType(
|
|
|
783
788
|
"Ocena roczna": 0,
|
|
784
789
|
}
|
|
785
790
|
)
|
|
786
|
-
# New-schema descriptive grading: the page renders this table with empty
|
|
787
|
-
# semester cells
|
|
791
|
+
# New-schema descriptive grading: the page renders this table either with empty
|
|
792
|
+
# `gradesCell` semester cells, which its script fills from the gateway routes
|
|
793
|
+
# above, or with bare semester text, which needs no gateway read.
|
|
788
794
|
DESCRIPTIVE_TABLE_CLASS = "newSchemaGradesByStudentTable"
|
|
789
795
|
DESCRIPTIVE_ROW_CLASS = "studentRow"
|
|
790
796
|
DESCRIPTIVE_DETAILS_ROW_CLASS = "studentGradesDetailsRow"
|
|
@@ -15,6 +15,7 @@ from librus_python_api.config import (
|
|
|
15
15
|
GRADE_BODY_PREFIX_COLUMNS,
|
|
16
16
|
GRADE_CURRENT_HEADER,
|
|
17
17
|
GRADE_EMPTY_MARKERS,
|
|
18
|
+
GRADE_INLINE_DETAIL_LABEL,
|
|
18
19
|
GRADE_MAX_METADATA_LENGTH,
|
|
19
20
|
GRADE_MAX_RECORDS,
|
|
20
21
|
GRADE_MAX_SUBJECTS,
|
|
@@ -28,6 +29,7 @@ from librus_python_api.config import (
|
|
|
28
29
|
GRADE_PUBLICATION_PERIOD_LABELS,
|
|
29
30
|
GRADE_PUBLICATION_TEACHER_LABEL,
|
|
30
31
|
TOOLTIP_LABELS,
|
|
32
|
+
TOOLTIP_MULTILINE_LABELS,
|
|
31
33
|
)
|
|
32
34
|
from librus_python_api.exceptions import ErrorKind, LibrusError
|
|
33
35
|
from librus_python_api.formative_grades import is_formative_table, read_formative_table
|
|
@@ -99,7 +101,16 @@ def _numeric(
|
|
|
99
101
|
semester: Literal[0, 1, 2],
|
|
100
102
|
kind: GradeKind = GradeKind.CURRENT,
|
|
101
103
|
) -> NumericGrade:
|
|
102
|
-
|
|
104
|
+
if markup.detail_id(element.get("href"), FORMATIVE_DETAIL_PATH_PREFIX) is None:
|
|
105
|
+
metadata = markup.tooltip_fields(element)
|
|
106
|
+
else:
|
|
107
|
+
# A formative mirror's assessment text after `Ocena` is free text that
|
|
108
|
+
# may span lines and hold colons (observed live 2026-11-08).
|
|
109
|
+
metadata = markup.tooltip_fields(
|
|
110
|
+
element,
|
|
111
|
+
multiline=TOOLTIP_MULTILINE_LABELS | {GRADE_INLINE_DETAIL_LABEL},
|
|
112
|
+
unknown_multiline=True,
|
|
113
|
+
)
|
|
103
114
|
counts = metadata.get("Licz do średniej")
|
|
104
115
|
if counts is not None and counts.casefold() not in ("tak", "nie"):
|
|
105
116
|
raise LibrusError(ErrorKind.PARSE)
|
|
@@ -31,7 +31,7 @@ NOTICE = (
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
def _grades(body: bytes) -> dict[str, int]:
|
|
34
|
-
#
|
|
34
|
+
# Every grade view also checks the new-schema table, as `grades()` does.
|
|
35
35
|
records, table = parse_grade_page(body)
|
|
36
36
|
return {
|
|
37
37
|
"numeric": sum(1 for g in records.numeric if g.point_grade_id is None),
|
|
@@ -13,7 +13,9 @@ 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_LABELS,
|
|
16
17
|
TOOLTIP_MULTILINE_LABELS,
|
|
18
|
+
TOOLTIP_REPEATABLE_LABELS,
|
|
17
19
|
UPSTREAM_ORIGINS,
|
|
18
20
|
)
|
|
19
21
|
from librus_python_api.exceptions import ErrorKind, LibrusError
|
|
@@ -82,14 +84,27 @@ def colspan(cell: html.HtmlElement) -> int:
|
|
|
82
84
|
|
|
83
85
|
|
|
84
86
|
def tooltip_fields(
|
|
85
|
-
element: html.HtmlElement,
|
|
87
|
+
element: html.HtmlElement,
|
|
88
|
+
heading: list[str] | None = None,
|
|
89
|
+
*,
|
|
90
|
+
multiline: frozenset[str] | None = None,
|
|
91
|
+
unknown_multiline: bool = False,
|
|
86
92
|
) -> dict[str, str]:
|
|
87
93
|
"""Parse a 'Label: value<br>' title attribute into unique labelled fields.
|
|
88
94
|
|
|
89
95
|
A free-text field may span lines; an unlabelled line after it continues
|
|
90
96
|
its value. Any other unlabelled line is an unknown layout. With `heading`,
|
|
91
97
|
the first line is taken whole, colon included, and appended to it.
|
|
98
|
+
`multiline` names the free-text labels (default `Komentarz`); with
|
|
99
|
+
`unknown_multiline`, a field whose label is not a known one (teacher text
|
|
100
|
+
holding a colon) is free text too, and inside such free text a line
|
|
101
|
+
starting with a known label is text unless the tooltip's closing run of
|
|
102
|
+
known-label lines begins there. Otherwise a known label such as `Data`
|
|
103
|
+
never continues. A repeatable label (`Poprawa oceny`) adds its value as
|
|
104
|
+
one more line of the same field.
|
|
92
105
|
"""
|
|
106
|
+
if multiline is None:
|
|
107
|
+
multiline = TOOLTIP_MULTILINE_LABELS
|
|
93
108
|
title = element.get("title", "")
|
|
94
109
|
if len(title) > GRADE_MAX_METADATA_LENGTH:
|
|
95
110
|
raise LibrusError(ErrorKind.LIMIT)
|
|
@@ -98,18 +113,34 @@ def tooltip_fields(
|
|
|
98
113
|
chunks = re.split(r"<br\s*/?>|\n", title, flags=re.I)
|
|
99
114
|
if len(chunks) > GRADE_MAX_METADATA_FIELDS:
|
|
100
115
|
raise LibrusError(ErrorKind.LIMIT)
|
|
101
|
-
for chunk in chunks
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
116
|
+
lines = [tooltip_line(chunk) for chunk in chunks if chunk.strip()]
|
|
117
|
+
# Start of the closing run of known-label lines, such as Kategoria, Data.
|
|
118
|
+
closing = len(lines)
|
|
119
|
+
while closing and _known_label(lines[closing - 1]):
|
|
120
|
+
closing -= 1
|
|
121
|
+
for index, rendered in enumerate(lines):
|
|
105
122
|
if heading is not None and not heading:
|
|
106
123
|
heading.append(rendered)
|
|
107
124
|
continue
|
|
108
125
|
key, separator, value = rendered.partition(":")
|
|
109
126
|
key, value = key.strip(), value.strip()
|
|
110
|
-
if
|
|
127
|
+
if (
|
|
128
|
+
last is not None
|
|
129
|
+
and (
|
|
130
|
+
last in multiline or (unknown_multiline and last not in TOOLTIP_LABELS)
|
|
131
|
+
)
|
|
132
|
+
and (
|
|
133
|
+
not separator
|
|
134
|
+
or (unknown_multiline and index < closing and key in TOOLTIP_LABELS)
|
|
135
|
+
)
|
|
136
|
+
):
|
|
111
137
|
result[last] = f"{result[last]}\n{rendered}".strip()
|
|
112
138
|
continue
|
|
139
|
+
if separator and key in result and key in TOOLTIP_REPEATABLE_LABELS:
|
|
140
|
+
# Each repeat keeps its value, in order, as one more line.
|
|
141
|
+
result[key] = f"{result[key]}\n{value}"
|
|
142
|
+
last = key
|
|
143
|
+
continue
|
|
113
144
|
if not separator or not key or key in result:
|
|
114
145
|
raise LibrusError(ErrorKind.PARSE)
|
|
115
146
|
result[key] = value
|
|
@@ -117,6 +148,11 @@ def tooltip_fields(
|
|
|
117
148
|
return result
|
|
118
149
|
|
|
119
150
|
|
|
151
|
+
def _known_label(line: str) -> bool:
|
|
152
|
+
key, separator, _ = line.partition(":")
|
|
153
|
+
return bool(separator) and key.strip() in TOOLTIP_LABELS
|
|
154
|
+
|
|
155
|
+
|
|
120
156
|
def tooltip_line(chunk: str) -> str:
|
|
121
157
|
"""One tooltip line's text, whitespace normalized."""
|
|
122
158
|
# Tooltips are HTML inside an attribute; parse markup only where present.
|
{librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/notification_workflow.py
RENAMED
|
@@ -130,7 +130,8 @@ class NotificationWorkflow:
|
|
|
130
130
|
if uncertain:
|
|
131
131
|
raise LibrusError(ErrorKind.CHECKPOINT)
|
|
132
132
|
if not consume:
|
|
133
|
-
|
|
133
|
+
# Nothing is read before the caller agrees to consume new events.
|
|
134
|
+
raise LibrusError(ErrorKind.CONSENT_REQUIRED)
|
|
134
135
|
state = await store._io(lambda: store._read_state(context.identifier))
|
|
135
136
|
ordinary = await self._ordinary(categories, window, budget)
|
|
136
137
|
# Validate whole ordinary results and state capacity before consuming.
|
|
@@ -89,7 +89,6 @@ from librus_python_api.gateway_grades import (
|
|
|
89
89
|
parse_user_info,
|
|
90
90
|
)
|
|
91
91
|
from librus_python_api.grade_parsers import parse_final_grades
|
|
92
|
-
from librus_python_api.grade_records import parse_grade_records
|
|
93
92
|
from librus_python_api.homework_range import HomeworkAccumulator
|
|
94
93
|
from librus_python_api.lifecycle import join_owned
|
|
95
94
|
from librus_python_api.message_content import parse_message_content, validate_reference
|
|
@@ -1084,11 +1083,11 @@ class AccountClient:
|
|
|
1084
1083
|
Callback success acknowledges consumer-owned durable storage. Callback
|
|
1085
1084
|
failure/timeout means acknowledgement unknown, never automatic replay.
|
|
1086
1085
|
"""
|
|
1087
|
-
if (
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1086
|
+
if type(allow_consume_events) is not bool:
|
|
1087
|
+
raise LibrusError(ErrorKind.INVALID_INPUT)
|
|
1088
|
+
if not allow_consume_events:
|
|
1089
|
+
raise LibrusError(ErrorKind.CONSENT_REQUIRED)
|
|
1090
|
+
if self._consuming_schedule:
|
|
1092
1091
|
raise LibrusError(ErrorKind.INVALID_INPUT)
|
|
1093
1092
|
validate_checkpoint(checkpoint, checkpoint_timeout_seconds)
|
|
1094
1093
|
self._consuming_schedule = True
|
|
@@ -1324,24 +1323,22 @@ class AccountClient:
|
|
|
1324
1323
|
"""Select a grade view with one view-changing POST.
|
|
1325
1324
|
|
|
1326
1325
|
Changes the selected grade filter, not school records. A cache hit makes
|
|
1327
|
-
no POST. Missing inline weight/count data remains unknown.
|
|
1328
|
-
|
|
1326
|
+
no POST. Missing inline weight/count data remains unknown.
|
|
1327
|
+
|
|
1328
|
+
Every view checks a new-schema descriptive table the same way. Its
|
|
1329
|
+
semester cells either are empty script hooks or hold bare text, which
|
|
1330
|
+
becomes undated summaries with no extra request; any other layout
|
|
1331
|
+
fails the read. Only the all view with empty hooks adds that table's
|
|
1329
1332
|
grades from the gateway: three more reads, five when there are grades.
|
|
1330
|
-
Other views do not request them.
|
|
1331
1333
|
"""
|
|
1332
1334
|
form = grade_view_form(view)
|
|
1333
1335
|
|
|
1334
1336
|
async def fetch(budget: RequestBudget, _: bool) -> Grades:
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
records, table = await self._page(
|
|
1341
|
-
"grades", budget, parse_grade_page, form=form
|
|
1342
|
-
)
|
|
1343
|
-
if table is not None and not table.filled:
|
|
1344
|
-
records = await self._gateway_descriptive(records, table, budget)
|
|
1337
|
+
records, table = await self._page(
|
|
1338
|
+
"grades", budget, parse_grade_page, form=form
|
|
1339
|
+
)
|
|
1340
|
+
if view is GradeView.ALL and table is not None and not table.filled:
|
|
1341
|
+
records = await self._gateway_descriptive(records, table, budget)
|
|
1345
1342
|
return Grades(
|
|
1346
1343
|
self._session_identity(), records, self._observation("grades"), view
|
|
1347
1344
|
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/_notification_bootstrap.py
RENAMED
|
File without changes
|
{librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/_notification_codec.py
RENAMED
|
File without changes
|
|
File without changes
|
{librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/_windows_filesystem.py
RENAMED
|
File without changes
|
|
File without changes
|
{librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/attachment_routes.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/attendance_frequency.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/class_free_days.py
RENAMED
|
File without changes
|
{librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/completed_lessons.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{librus_python_api-1.8.3 → librus_python_api-1.8.4}/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.8.3 → librus_python_api-1.8.4}/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.8.3 → librus_python_api-1.8.4}/src/librus_python_api/modern_messages.py
RENAMED
|
File without changes
|
{librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/notification_models.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
|
|
File without changes
|
{librus_python_api-1.8.3 → librus_python_api-1.8.4}/src/librus_python_api/school_year_archive.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|