librus-python-api 1.7.1__tar.gz → 1.8.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/API.md +93 -15
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/CHANGELOG.md +95 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/PKG-INFO +43 -6
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/README.md +42 -5
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/__init__.py +1 -1
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/config.py +104 -0
- librus_python_api-1.8.1/src/librus_python_api/diagnostics.py +546 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/exceptions.py +13 -0
- librus_python_api-1.8.1/src/librus_python_api/gateway_grades.py +458 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/grade_records.py +5 -1
- librus_python_api-1.8.1/src/librus_python_api/inspect_page.py +132 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/models.py +4 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/parsers.py +35 -5
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/service.py +174 -20
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/transport.py +20 -5
- librus_python_api-1.7.1/src/librus_python_api/diagnostics.py +0 -25
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/.gitignore +0 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/LICENSE +0 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/SECURITY.md +0 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/pyproject.toml +0 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/_notification_bootstrap.py +0 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/_notification_codec.py +0 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/_storage.py +0 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/_windows_filesystem.py +0 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/announcements.py +0 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/attachment_routes.py +0 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/attachments.py +0 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/attendance.py +0 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/attendance_frequency.py +0 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/budget.py +0 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/checkpoint.py +0 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/class_free_days.py +0 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/completed_lessons.py +0 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/detail_fields.py +0 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/files.py +0 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/formative_grades.py +0 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/grade_parsers.py +0 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/homework_range.py +0 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/lifecycle.py +0 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/markup.py +0 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/message_content.py +0 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/messages.py +0 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/modern_body.py +0 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/modern_mailbox.py +0 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/modern_messages.py +0 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/notification_models.py +0 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/notification_persistence.py +0 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/notification_workflow.py +0 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/notifications.py +0 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/parsing.py +0 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/persistence.py +0 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/py.typed +0 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/recipients.py +0 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/scheduler.py +0 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/school_reads.py +0 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/school_year_archive.py +0 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/sending.py +0 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/src/librus_python_api/timetable.py +0 -0
- {librus_python_api-1.7.1 → librus_python_api-1.8.1}/uv.lock +0 -0
|
@@ -148,11 +148,47 @@ refunded. Exhaustion raises `LimitError`; deadlines raise `OperationTimeoutError
|
|
|
148
148
|
allowlisted modern directory and mailbox selections only. Arbitrary query keys,
|
|
149
149
|
routes and modern-to-legacy reference reuse are rejected before dispatch.
|
|
150
150
|
- `diagnostic_sink`: receives `DiagnosticEvent(operation, outcome,
|
|
151
|
-
elapsed_seconds, budget_requests_dispatched, budget_response_bytes
|
|
152
|
-
`
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
151
|
+
elapsed_seconds, budget_requests_dispatched, budget_response_bytes, frames,
|
|
152
|
+
http_status)`. `http_status` (1.8.1) is the HTTP status of the operation's
|
|
153
|
+
last checked read response, or None; statuses the transport turns into
|
|
154
|
+
errors (4xx, 5xx) are not checked reads, and attachment streams carry none.
|
|
155
|
+
`frames` (1.8.0) holds the code locations of a `LibrusError` outcome, as below;
|
|
156
|
+
it is empty on success and for timeouts, cancellation and replaced
|
|
157
|
+
connection errors. `librus_python_api.diagnostics.logging_sink` emits allowlisted fields
|
|
158
|
+
through the standard `logging` logger named `librus_python_api`. The
|
|
159
|
+
application owns handlers and formatting; no logging dependency or global
|
|
160
|
+
configuration is added. Sink errors never affect results.
|
|
161
|
+
|
|
162
|
+
### Failure diagnostics (1.8.0)
|
|
163
|
+
|
|
164
|
+
Errors carry only their kind, so these helpers read where a failure happened
|
|
165
|
+
from the exception's own traceback. Neither raises.
|
|
166
|
+
|
|
167
|
+
- `diagnostics.failure_frames(error, limit=6, packages=("librus_python_api",))`
|
|
168
|
+
returns code locations such as `("grade_records:_numeric:100",
|
|
169
|
+
"markup:tooltip_fields:113")`, the original failure site last. It follows an
|
|
170
|
+
explicit cause or a context suppressed with `from None`, and skips helpers
|
|
171
|
+
that only raise. The strings are opaque and change between releases; report
|
|
172
|
+
them with the library version.
|
|
173
|
+
- `diagnostics.describe_failure(error)` returns the page structure around the
|
|
174
|
+
failure, or None: the HTML elements held by the innermost library frame that
|
|
175
|
+
holds any, with their path from the grade table, tags, safe class tokens,
|
|
176
|
+
known tooltip labels (`config.TOOLTIP_LABELS`), known detail-link families and
|
|
177
|
+
lengths. Text and attribute values are masked unless they equal a parser
|
|
178
|
+
constant, so a failing tooltip reads like `[[Data], [Waga], NO-COLON(len=17)]`.
|
|
179
|
+
A whole page held by the failing frame (1.8.1) is summarized after the other
|
|
180
|
+
elements: body text length, whether the Synergia menu and header are
|
|
181
|
+
present, title, headings, notices, forms and input types, then the first
|
|
182
|
+
tables row by row, with known profile labels and notices by name. Output is
|
|
183
|
+
bounded. It is meant for a human to review before sharing, not for
|
|
184
|
+
automatic upload.
|
|
185
|
+
- `python -m librus_python_api.inspect_page {grades,final-grades,profile} FILE`
|
|
186
|
+
parses a saved page offline (a whole page or a copied table) and prints
|
|
187
|
+
counts, or the failure kind, frames and structure. `profile` (1.8.1) reads the
|
|
188
|
+
information page. A source view saved as a text document (macOS TextEdit) is
|
|
189
|
+
named as such instead of being parsed. It makes no request and applies the
|
|
190
|
+
library's parse size limit. Exit status: 0 parsed, 1 failed, 2 unreadable
|
|
191
|
+
file.
|
|
156
192
|
|
|
157
193
|
All upstream routes and forms are fixed in `config.py` and documented in
|
|
158
194
|
[contracts/upstream.openapi.yaml](contracts/upstream.openapi.yaml). There is no
|
|
@@ -165,7 +201,8 @@ arbitrary-URL method.
|
|
|
165
201
|
reads it, so the first call costs no extra request.
|
|
166
202
|
- `student_information()` returns `StudentInformation` with name, class, register
|
|
167
203
|
number, tutor, school and `lucky_number`. `LuckyNumber.availability` is
|
|
168
|
-
`UNAVAILABLE` when the page has
|
|
204
|
+
`UNAVAILABLE` when the page has no lucky number, an empty marker or "?" in
|
|
205
|
+
it; no date is invented. Up to 1.8.0 it was always `UNAVAILABLE`.
|
|
169
206
|
|
|
170
207
|
Each result carries an `Observation(account, observed_at, session_generation,
|
|
171
208
|
source)` with an aware UTC timestamp.
|
|
@@ -178,7 +215,8 @@ source)` with an aware UTC timestamp.
|
|
|
178
215
|
`UNAVAILABLE` means the column is absent.
|
|
179
216
|
- `grades(view=GradeView.ALL)` returns `Grades(identity, records, observation,
|
|
180
217
|
view)`. Selecting a view (`ALL`, `WEEK`, `LAST_LOGIN`) is one POST that changes
|
|
181
|
-
the filter shown in that login's session; it is never replayed
|
|
218
|
+
the filter shown in that login's session; it is never replayed
|
|
219
|
+
(except once after Synergia's logged-out page, see [Errors](#errors)). `records` has:
|
|
182
220
|
- `numeric`: `NumericGrade` with subject, raw symbol, civil `day`, semester,
|
|
183
221
|
`kind` (`GradeKind`), and the optional count flag, weight, category, teacher,
|
|
184
222
|
comment and tooltip `metadata`. Unknown weight or count is `None`, not zero.
|
|
@@ -249,12 +287,32 @@ such as `(0-100)`, not a grade symbol. The table's "Wyn." period results are
|
|
|
249
287
|
into `averages`), and a point table with titled summary columns adds its
|
|
250
288
|
subjects to `final_grades()`. See [point grades](contracts/grades.md#point-grades-170).
|
|
251
289
|
|
|
290
|
+
Gateway descriptive grades (1.8.0) belong to schools on the new
|
|
291
|
+
descriptive grading schema, whose grades page shows a subject table with empty
|
|
292
|
+
semester cells that the page script fills from the gateway. In the `ALL` view
|
|
293
|
+
only, `grades()` then reads those grades from the gateway with the login's
|
|
294
|
+
session and the call's budget: three more requests, five when there are
|
|
295
|
+
grades. They are `DescriptiveGrade`s in `descriptive` (kind `CURRENT`, no
|
|
296
|
+
`href`), with the page's subject name, the scale value or else the content as
|
|
297
|
+
`raw`, the civil date, semester 1 or 2, the teacher's name when listed, the
|
|
298
|
+
comment and metadata pairs for the present fields only (`Obszar`, `Treść`,
|
|
299
|
+
`Dodał`, `Komentarz`, `Skala`). `grades_window()` filters them like other
|
|
300
|
+
dated records. `WEEK` and `LAST_LOGIN` do not include them, because the page
|
|
301
|
+
narrows those views by a field whose meaning is not established; notifications
|
|
302
|
+
therefore do not report them. A gateway error, an unknown subject, a grade with
|
|
303
|
+
no value, an unexpected shape or a pagination member that shows more items
|
|
304
|
+
fails the whole read instead of returning it without these grades; a refused
|
|
305
|
+
gateway route (403) is `UNSUPPORTED_CAPABILITY`, so it does not put the other
|
|
306
|
+
grade views into the access-denied cooldown. The routes and field names are
|
|
307
|
+
source-informed; a first authorized live read parsed populated records on one
|
|
308
|
+
school's login. See [gateway descriptive grades](contracts/grades.md#gateway-descriptive-grades-180-issue-73).
|
|
309
|
+
|
|
252
310
|
## Attendance
|
|
253
311
|
|
|
254
312
|
- `attendance(view=AttendanceView.ALL)` returns `Attendance` with
|
|
255
313
|
`AttendanceRecord`s (symbol, day, semester, type, teacher, period, excursion
|
|
256
314
|
flag, topic, subject, numeric `detail_id`, tooltip metadata) and the semesters
|
|
257
|
-
shown. The view POST is never replayed.
|
|
315
|
+
shown. The view POST is never replayed (except once after Synergia's logged-out page, see [Errors](#errors)).
|
|
258
316
|
- `attendance_window(start=None, end=None, view=AttendanceView.ALL)` filters the
|
|
259
317
|
selected collection, with the same optional-date/370-day-difference rules.
|
|
260
318
|
`AttendanceWindow.view` records the selection.
|
|
@@ -287,7 +345,8 @@ seven `TimetableDay`s. Each `TimetablePeriod` has a number, a local-time
|
|
|
287
345
|
`changes` and the reported `next_recess`. A `TimetableChange` keeps the notice
|
|
288
346
|
label (for example "zastępstwo") and its tooltip fields in order. The tooltip
|
|
289
347
|
anchor may wrap the notice or sit inside it. No status is inferred from the
|
|
290
|
-
label. The week selection is a POST that is never replayed
|
|
348
|
+
label. The week selection is a POST that is never replayed
|
|
349
|
+
(except once after Synergia's logged-out page, see [Errors](#errors)).
|
|
291
350
|
|
|
292
351
|
## Announcements
|
|
293
352
|
|
|
@@ -395,7 +454,8 @@ Each `MessageSummary` carries:
|
|
|
395
454
|
Each text field is bounded to 4,096 characters, total page text to 256 KiB,
|
|
396
455
|
in addition to shared body/parser/request limits. No body or attachment fetch,
|
|
397
456
|
mark-read, delete, send or read-once operation occurs. Pagination changes the
|
|
398
|
-
selected mailbox view, so its POST is never replayed, including after expiry
|
|
457
|
+
selected mailbox view, so its POST is never replayed, including after expiry
|
|
458
|
+
(except once after Synergia's logged-out page, see [Errors](#errors)).
|
|
399
459
|
Page and batch caches are separate and fresh by default.
|
|
400
460
|
|
|
401
461
|
In 0.4.11 a full 50-row page without pagination metadata is unsupported, not
|
|
@@ -892,7 +952,8 @@ be a `MessageReference` for the same login with a numeric ID and typed folder.
|
|
|
892
952
|
Foreign accounts, arbitrary URLs and injected IDs fail before authentication.
|
|
893
953
|
|
|
894
954
|
- Received opens require `allow_mark_read=True`, even for an already-read
|
|
895
|
-
selection or warm cache
|
|
955
|
+
selection or warm cache; without it they raise `ConsentRequiredError`
|
|
956
|
+
(`InvalidInputError` before 1.8.1) before any request. Opening may mark read before any response or parse
|
|
896
957
|
failure. Consent does not assert that upstream actually changed a read flag.
|
|
897
958
|
- `may_mark_read` describes the route, not delivery or mutation confirmation.
|
|
898
959
|
Sent opens do not require the opt-in. Neither folder is automatically replayed,
|
|
@@ -1025,7 +1086,7 @@ Limits: 32 group selectors, 2,048 recipients, 1,024 characters per label and
|
|
|
1025
1086
|
128 KiB total recipient text, plus shared transport/parser/body limits. Each
|
|
1026
1087
|
lookup has its own account/type/selection cache key and accepts the common budget and
|
|
1027
1088
|
freshness parameters. Fresh reads are default. Both selection operations are
|
|
1028
|
-
never replayed, even though group discovery uses GET. No send route, message
|
|
1089
|
+
never replayed (except once after Synergia's logged-out page, see [Errors](#errors)), even though group discovery uses GET. No send route, message
|
|
1029
1090
|
content, mark-read, attachment or read-once access occurs in recipient discovery.
|
|
1030
1091
|
|
|
1031
1092
|
The empty group-option selector is observed. Populated choices, nonzero dispatch,
|
|
@@ -1041,7 +1102,8 @@ grades, attendance, messages, announcements, agenda and homework. A shown catego
|
|
|
1041
1102
|
without a counter has zero; missing categories are not fabricated. Invalid or
|
|
1042
1103
|
duplicate counters fail. The menu may be a token-scoped snapshot, not a fresh poll
|
|
1043
1104
|
or the input for seen-state updates. This ordinary read accepts the common
|
|
1044
|
-
budget/freshness arguments and conservatively does not replay on expiry
|
|
1105
|
+
budget/freshness arguments and conservatively does not replay on expiry
|
|
1106
|
+
(except once after Synergia's logged-out page, see [Errors](#errors)).
|
|
1045
1107
|
|
|
1046
1108
|
```python
|
|
1047
1109
|
batch = await client.consume_schedule_events(
|
|
@@ -1098,6 +1160,7 @@ text. `error_for(kind)` builds one.
|
|
|
1098
1160
|
| Error | Meaning |
|
|
1099
1161
|
| --- | --- |
|
|
1100
1162
|
| `InvalidInputError` | Rejected before any request, or upstream rejected the selection |
|
|
1163
|
+
| `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 |
|
|
1101
1164
|
| `CredentialsRejectedError` | Login and password refused |
|
|
1102
1165
|
| `AccountActionRequiredError` | CAPTCHA, 2FA or another interactive step is required |
|
|
1103
1166
|
| `SessionExpiredError` | Proven expiry that could not be recovered |
|
|
@@ -1115,9 +1178,24 @@ text. `error_for(kind)` builds one.
|
|
|
1115
1178
|
|
|
1116
1179
|
Recovery policy:
|
|
1117
1180
|
|
|
1118
|
-
-
|
|
1181
|
+
- A 401 or a redirect to an approved login route proves expiry. So does
|
|
1182
|
+
(1.8.1) a Synergia page that fails to parse, has no signed-in menu and
|
|
1183
|
+
whose only heading is "Brak dostępu", unless a session that has just signed
|
|
1184
|
+
in gets it. Synergia answers every page read of a session
|
|
1185
|
+
it no longer knows that way, with status 200 (observed 2026-10-29); up to
|
|
1186
|
+
1.8.0 this failed as `ParseError` on every call until the client was
|
|
1187
|
+
recreated.
|
|
1119
1188
|
- Safe GET reads then log in once more and retry once, within the same budget.
|
|
1120
|
-
- View-selection POSTs are never replayed.
|
|
1189
|
+
- View-selection POSTs are never replayed after a 401 or login redirect. A
|
|
1190
|
+
read without an account effect answered with the logged-out page, POST
|
|
1191
|
+
included, is repeated once in a fresh session: that page shows the request
|
|
1192
|
+
was not applied to the account. The whole read is repeated, so a
|
|
1193
|
+
multi-page read posts its earlier view selections again (the same views,
|
|
1194
|
+
within the same budget). Opening a received message, which marks it read,
|
|
1195
|
+
is not repeated: it fails as `SessionExpiredError` and the next call signs
|
|
1196
|
+
in. If the fresh session gets the same page, or a session that has just
|
|
1197
|
+
signed in gets it, the read fails as `ParseError` and that operation cools
|
|
1198
|
+
down for 60 seconds, so a page that stays does not sign in on every call.
|
|
1121
1199
|
- A denial or unrecovered expiry starts a 60-second cooldown for that operation
|
|
1122
1200
|
on that login; a failed login starts one for the whole login.
|
|
1123
1201
|
- 429 and 503 pause the whole scheduler, honouring `Retry-After` up to 24 hours.
|
|
@@ -1,5 +1,100 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.8.1 (2026-10-29) - Session recovery, lucky number, consent error and diagnostics
|
|
4
|
+
|
|
5
|
+
Signatures are unchanged; one error kind is added.
|
|
6
|
+
|
|
7
|
+
- Session recovery: for a session it no longer knows, Synergia answers every
|
|
8
|
+
page read with status 200 and a "Brak dostępu" page instead of a login
|
|
9
|
+
redirect (observed 2026-10-29). Up to 1.8.0 every read then failed as
|
|
10
|
+
`ParseError`, and kept failing until the client was recreated: the
|
|
11
|
+
profile's `parse_profile` failure a tester reported. Such a page now counts
|
|
12
|
+
as expiry: a read without an account effect signs in again and is repeated
|
|
13
|
+
once, view-selection POSTs included, since the page shows the request was
|
|
14
|
+
not applied. Opening a received message (mark-read) is not repeated; it
|
|
15
|
+
fails as `SessionExpiredError` and the next call signs in. A session that
|
|
16
|
+
has just signed in and gets the page, or a repeat that gets it again, fails
|
|
17
|
+
as `ParseError` and cools down for 60 seconds, so a page that stays does
|
|
18
|
+
not sign in on every call. Verified live by invalidating the session
|
|
19
|
+
cookies: student information, notification counts, grades, timetable,
|
|
20
|
+
attendance, homework and legacy messages each recovered with one login.
|
|
21
|
+
- Student information: the lucky number is read from the page header
|
|
22
|
+
(`span.luckyNumber`). Up to 1.8.0 the parser looked for an element ID the
|
|
23
|
+
page does not use, so `lucky_number` was always `UNAVAILABLE`. An empty
|
|
24
|
+
marker or "?" (reported where no number is drawn; neither observed) stays
|
|
25
|
+
`UNAVAILABLE` instead of failing the read.
|
|
26
|
+
- `describe_failure` summarizes a whole page held by the failing frame, after
|
|
27
|
+
the elements nearest the failure: body text length, Synergia menu and
|
|
28
|
+
header presence, title, headings, notices, forms and input types, and the
|
|
29
|
+
first tables row by row, instead of the start of the page head. Profile
|
|
30
|
+
labels, the login owner's row labels and known page notices are shown by
|
|
31
|
+
name, so a login or error page is told apart from a changed layout.
|
|
32
|
+
- `ErrorKind.CONSENT_REQUIRED` and `ConsentRequiredError`, a subclass of
|
|
33
|
+
`InvalidInputError`: opening a received message without
|
|
34
|
+
`allow_mark_read=True` raises it instead of a plain invalid input, so a
|
|
35
|
+
caller can ask the user rather than treat the message as unreadable.
|
|
36
|
+
`except InvalidInputError` still catches it; code comparing `kind` with
|
|
37
|
+
`INVALID_INPUT` sees the new kind.
|
|
38
|
+
- `DiagnosticEvent.http_status`: the HTTP status of the operation's last
|
|
39
|
+
checked read response, also logged by `logging_sink`. A read that fails as
|
|
40
|
+
`PARSE` at the response check now says whether it was a 2xx other than 200
|
|
41
|
+
or a 200 of another media type.
|
|
42
|
+
- `inspect_page profile FILE` checks a saved information page. A page source
|
|
43
|
+
saved as a macOS TextEdit document is named as such.
|
|
44
|
+
|
|
45
|
+
## 1.8.0 (2026-10-28) - Failure diagnostics and gateway descriptive grades
|
|
46
|
+
|
|
47
|
+
Errors still carry only their kind; new helpers say where a failure happened
|
|
48
|
+
without page data. Schools on the new descriptive grading schema get their
|
|
49
|
+
gateway grades.
|
|
50
|
+
|
|
51
|
+
### Failure diagnostics
|
|
52
|
+
|
|
53
|
+
- `diagnostics.failure_frames(error)` returns the failure's code locations
|
|
54
|
+
(`module:function:line`, the original site last) from its traceback, through
|
|
55
|
+
the parser thread, re-raises and a context suppressed with `from None`.
|
|
56
|
+
- `diagnostics.describe_failure(error)` returns the page structure around the
|
|
57
|
+
failure: element path, tags, safe class tokens, known tooltip labels and
|
|
58
|
+
lengths, with every text and attribute value masked unless it equals a parser
|
|
59
|
+
constant. It names, for example, the unlabelled tooltip line that broke a
|
|
60
|
+
read.
|
|
61
|
+
- `python -m librus_python_api.inspect_page {grades,final-grades} FILE` parses a
|
|
62
|
+
saved page or copied table offline and prints counts or the failure report.
|
|
63
|
+
- `DiagnosticEvent.frames` carries the code locations to diagnostic sinks, and
|
|
64
|
+
`logging_sink` logs them.
|
|
65
|
+
- `config.TOOLTIP_LABELS` lists the tooltip labels diagnostics may show by name.
|
|
66
|
+
|
|
67
|
+
### Gateway descriptive grades
|
|
68
|
+
|
|
69
|
+
Reads the grades of schools on the new descriptive grading schema, whose
|
|
70
|
+
grades page renders a subject table with empty semester cells and loads the
|
|
71
|
+
grades from gateway JSON routes. Routes and field names come from public client
|
|
72
|
+
code and the official page script. A first authorized live read on 2026-10-28
|
|
73
|
+
parsed four populated records on one school's login and showed the page
|
|
74
|
+
layout below (issue #73).
|
|
75
|
+
|
|
76
|
+
- Grades: in the `ALL` view, a page with the new-schema table adds its gateway
|
|
77
|
+
grades to `GradeRecords.descriptive` as `DescriptiveGrade` records, which
|
|
78
|
+
grade windows filter by date. The read makes three more requests, or five
|
|
79
|
+
when there are grades, under the same session and budget. Public types and
|
|
80
|
+
signatures are unchanged and `DescriptiveGrade` has no new field.
|
|
81
|
+
- Grades: `WEEK` and `LAST_LOGIN` make no gateway request and do not include
|
|
82
|
+
these grades, so notifications do not report them yet.
|
|
83
|
+
- Grades: a gateway error, unknown subject, grade without a value, malformed
|
|
84
|
+
or oversized response, unknown table layout or a pagination member showing
|
|
85
|
+
more items fails the whole all-view read on such a page, instead of
|
|
86
|
+
returning it without these grades. Pages without the table are unchanged.
|
|
87
|
+
A refused gateway route (403) is `UnsupportedCapabilityError`, so it does
|
|
88
|
+
not put the week and last-login views into the access-denied cooldown.
|
|
89
|
+
- Upstream contract: five source-informed gateway operations; the two that
|
|
90
|
+
take an account identifier accept `[A-Za-z0-9_-]{1,128}`, every other
|
|
91
|
+
`{id}` route stays numeric.
|
|
92
|
+
- Live check: the weekly grade checks and `scripts/live_capture.py` declare
|
|
93
|
+
the five gateway routes.
|
|
94
|
+
- The page may hold an unclosed, row-less table of the same class before the
|
|
95
|
+
real one, and the real table has a period header row, hidden details rows
|
|
96
|
+
and a footer; only a table with subject rows of its own is read.
|
|
97
|
+
|
|
3
98
|
## 1.7.1 (2026-10-27) - Observed point-grade page and multi-line comments
|
|
4
99
|
|
|
5
100
|
Fixes for layouts 1.7.0 rejected with `ParseError`, established from an
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: librus-python-api
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.8.1
|
|
4
4
|
Summary: Independent bounded async Librus Synergia client
|
|
5
5
|
Project-URL: Repository, https://github.com/krzysztofbury/librus-python-api
|
|
6
6
|
Project-URL: Issues, https://github.com/krzysztofbury/librus-python-api/issues
|
|
@@ -42,8 +42,14 @@ 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
|
-
|
|
45
|
+
**Status:** `1.8.1` recovers a session Synergia no longer knows (it answers
|
|
46
|
+
with a "Brak dostępu" page, not a login redirect), reads the lucky number
|
|
47
|
+
from the page header and summarizes a page that fails as a whole in
|
|
48
|
+
diagnostics. `1.8.0` reports where a failure
|
|
49
|
+
happened (code locations and a redacted page structure, see
|
|
50
|
+
[failure diagnostics](API.md#failure-diagnostics-180)), reads gateway descriptive grades for schools on the new descriptive schema
|
|
51
|
+
(source-informed, first populated read live on one school) and keeps the 1.7.1 point
|
|
52
|
+
grades and multi-line grade comments. It keeps the 1.6.4 linear-time message body
|
|
47
53
|
rendering and the 1.6.2 completed-lesson and grade layouts, including behaviour
|
|
48
54
|
points, which are validated and not reported as grades.
|
|
49
55
|
Class free days, typed capability outcomes and the live-check freshness watchdog
|
|
@@ -57,7 +63,7 @@ See [limitations](#supported-features-and-limitations) below.
|
|
|
57
63
|
Install the exact release from PyPI after its gated publication completes:
|
|
58
64
|
|
|
59
65
|
```sh
|
|
60
|
-
python -m pip install librus-python-api==1.
|
|
66
|
+
python -m pip install librus-python-api==1.8.1
|
|
61
67
|
```
|
|
62
68
|
|
|
63
69
|
Pin the exact version qualified by your application. Before publication, use a
|
|
@@ -73,10 +79,12 @@ python -m pip install .
|
|
|
73
79
|
To install a locally built wheel instead:
|
|
74
80
|
|
|
75
81
|
```sh
|
|
76
|
-
python -m pip install ./dist/librus_python_api-1.
|
|
82
|
+
python -m pip install ./dist/librus_python_api-1.8.1-py3-none-any.whl
|
|
77
83
|
```
|
|
78
84
|
|
|
79
|
-
No
|
|
85
|
+
No console script or background process is installed: import the library in your
|
|
86
|
+
own program. `python -m librus_python_api.inspect_page` only parses a saved page
|
|
87
|
+
offline for a bug report.
|
|
80
88
|
|
|
81
89
|
## First request
|
|
82
90
|
|
|
@@ -254,6 +262,35 @@ unqualified; backend acceptance is not proof of delivery. Tests cover supported
|
|
|
254
262
|
contracts, not every school or role. Detailed coverage is in the
|
|
255
263
|
[verification log][verification]; open work is tracked in [issues][issues].
|
|
256
264
|
|
|
265
|
+
## Diagnosing failures
|
|
266
|
+
|
|
267
|
+
Errors carry only their kind. To see where a failure happened without any page
|
|
268
|
+
data, use the helpers in `librus_python_api.diagnostics` (see
|
|
269
|
+
[API.md](API.md#failure-diagnostics-180)):
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
from librus_python_api.diagnostics import describe_failure, failure_frames
|
|
273
|
+
from librus_python_api.exceptions import LibrusError
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
await client.grades()
|
|
277
|
+
except LibrusError as error:
|
|
278
|
+
print(
|
|
279
|
+
error.kind, failure_frames(error)
|
|
280
|
+
) # ('grade_records:_numeric:100', 'markup:tooltip_fields:113')
|
|
281
|
+
print(describe_failure(error)) # masked HTML structure around the failure
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
To check a saved page offline (a page source saved with Ctrl+U, or a copied
|
|
285
|
+
table), run `python -m librus_python_api.inspect_page grades page.html` (or
|
|
286
|
+
`final-grades`, or `profile` for the information page `/informacja`). Save the
|
|
287
|
+
page source itself: in Safari use File > Save As with format Page Source; in
|
|
288
|
+
Chrome on macOS open it with Option+Cmd+U, then Cmd+S. A source view copied
|
|
289
|
+
into TextEdit is a text document, which `inspect_page` rejects. It prints counts, or the failure's code locations and masked
|
|
290
|
+
structure, and makes no request. Pass a `diagnostic_sink` to `LibrusService`
|
|
291
|
+
to receive one `DiagnosticEvent` per operation, with `frames` on failure.
|
|
292
|
+
None of these print page text, values, logins or exception messages.
|
|
293
|
+
|
|
257
294
|
## Development and support
|
|
258
295
|
|
|
259
296
|
Use a repository checkout for tests and development tools; they are deliberately
|
|
@@ -16,8 +16,14 @@ 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
|
-
|
|
19
|
+
**Status:** `1.8.1` recovers a session Synergia no longer knows (it answers
|
|
20
|
+
with a "Brak dostępu" page, not a login redirect), reads the lucky number
|
|
21
|
+
from the page header and summarizes a page that fails as a whole in
|
|
22
|
+
diagnostics. `1.8.0` reports where a failure
|
|
23
|
+
happened (code locations and a redacted page structure, see
|
|
24
|
+
[failure diagnostics](API.md#failure-diagnostics-180)), reads gateway descriptive grades for schools on the new descriptive schema
|
|
25
|
+
(source-informed, first populated read live on one school) and keeps the 1.7.1 point
|
|
26
|
+
grades and multi-line grade comments. It keeps the 1.6.4 linear-time message body
|
|
21
27
|
rendering and the 1.6.2 completed-lesson and grade layouts, including behaviour
|
|
22
28
|
points, which are validated and not reported as grades.
|
|
23
29
|
Class free days, typed capability outcomes and the live-check freshness watchdog
|
|
@@ -31,7 +37,7 @@ See [limitations](#supported-features-and-limitations) below.
|
|
|
31
37
|
Install the exact release from PyPI after its gated publication completes:
|
|
32
38
|
|
|
33
39
|
```sh
|
|
34
|
-
python -m pip install librus-python-api==1.
|
|
40
|
+
python -m pip install librus-python-api==1.8.1
|
|
35
41
|
```
|
|
36
42
|
|
|
37
43
|
Pin the exact version qualified by your application. Before publication, use a
|
|
@@ -47,10 +53,12 @@ python -m pip install .
|
|
|
47
53
|
To install a locally built wheel instead:
|
|
48
54
|
|
|
49
55
|
```sh
|
|
50
|
-
python -m pip install ./dist/librus_python_api-1.
|
|
56
|
+
python -m pip install ./dist/librus_python_api-1.8.1-py3-none-any.whl
|
|
51
57
|
```
|
|
52
58
|
|
|
53
|
-
No
|
|
59
|
+
No console script or background process is installed: import the library in your
|
|
60
|
+
own program. `python -m librus_python_api.inspect_page` only parses a saved page
|
|
61
|
+
offline for a bug report.
|
|
54
62
|
|
|
55
63
|
## First request
|
|
56
64
|
|
|
@@ -228,6 +236,35 @@ unqualified; backend acceptance is not proof of delivery. Tests cover supported
|
|
|
228
236
|
contracts, not every school or role. Detailed coverage is in the
|
|
229
237
|
[verification log][verification]; open work is tracked in [issues][issues].
|
|
230
238
|
|
|
239
|
+
## Diagnosing failures
|
|
240
|
+
|
|
241
|
+
Errors carry only their kind. To see where a failure happened without any page
|
|
242
|
+
data, use the helpers in `librus_python_api.diagnostics` (see
|
|
243
|
+
[API.md](API.md#failure-diagnostics-180)):
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
from librus_python_api.diagnostics import describe_failure, failure_frames
|
|
247
|
+
from librus_python_api.exceptions import LibrusError
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
await client.grades()
|
|
251
|
+
except LibrusError as error:
|
|
252
|
+
print(
|
|
253
|
+
error.kind, failure_frames(error)
|
|
254
|
+
) # ('grade_records:_numeric:100', 'markup:tooltip_fields:113')
|
|
255
|
+
print(describe_failure(error)) # masked HTML structure around the failure
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
To check a saved page offline (a page source saved with Ctrl+U, or a copied
|
|
259
|
+
table), run `python -m librus_python_api.inspect_page grades page.html` (or
|
|
260
|
+
`final-grades`, or `profile` for the information page `/informacja`). Save the
|
|
261
|
+
page source itself: in Safari use File > Save As with format Page Source; in
|
|
262
|
+
Chrome on macOS open it with Option+Cmd+U, then Cmd+S. A source view copied
|
|
263
|
+
into TextEdit is a text document, which `inspect_page` rejects. It prints counts, or the failure's code locations and masked
|
|
264
|
+
structure, and makes no request. Pass a `diagnostic_sink` to `LibrusService`
|
|
265
|
+
to receive one `DiagnosticEvent` per operation, with `frames` on failure.
|
|
266
|
+
None of these print page text, values, logins or exception messages.
|
|
267
|
+
|
|
231
268
|
## Development and support
|
|
232
269
|
|
|
233
270
|
Use a repository checkout for tests and development tools; they are deliberately
|
|
@@ -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.
|
|
141
|
+
__version__ = "1.8.1"
|
|
142
142
|
|
|
143
143
|
__all__ = [
|
|
144
144
|
"ArchiveAbsences",
|
|
@@ -433,6 +433,51 @@ ENDPOINTS: Mapping[str, Endpoint] = MappingProxyType(
|
|
|
433
433
|
False,
|
|
434
434
|
Evidence.INDEPENDENTLY_OBSERVED,
|
|
435
435
|
),
|
|
436
|
+
# Gateway reads behind the grades page's new-schema descriptive
|
|
437
|
+
# table. Routes and field names come from public client code and the
|
|
438
|
+
# official page script, not from an observed populated response.
|
|
439
|
+
Endpoint(
|
|
440
|
+
"descriptive_token_info",
|
|
441
|
+
"GET",
|
|
442
|
+
"/gateway/api/2.0/Auth/TokenInfo",
|
|
443
|
+
SideEffect.NONE,
|
|
444
|
+
True,
|
|
445
|
+
Evidence.SOURCE_INFORMED,
|
|
446
|
+
),
|
|
447
|
+
Endpoint(
|
|
448
|
+
"descriptive_user_info",
|
|
449
|
+
"GET",
|
|
450
|
+
"/gateway/api/2.0/Auth/UserInfo/{id}",
|
|
451
|
+
SideEffect.NONE,
|
|
452
|
+
True,
|
|
453
|
+
Evidence.SOURCE_INFORMED,
|
|
454
|
+
),
|
|
455
|
+
# A read sent as POST with the fixed JSON body `{}`; it is never
|
|
456
|
+
# replayed, although it changes nothing that is known.
|
|
457
|
+
Endpoint(
|
|
458
|
+
"descriptive_partial_grades",
|
|
459
|
+
"POST",
|
|
460
|
+
"/gateway/api/2.0/Auth/DescriptiveGradingSystem/PartialGrades/Student/{id}",
|
|
461
|
+
SideEffect.NONE,
|
|
462
|
+
False,
|
|
463
|
+
Evidence.SOURCE_INFORMED,
|
|
464
|
+
),
|
|
465
|
+
Endpoint(
|
|
466
|
+
"descriptive_subjects",
|
|
467
|
+
"GET",
|
|
468
|
+
"/gateway/api/3.0/Auth/Subjects",
|
|
469
|
+
SideEffect.NONE,
|
|
470
|
+
True,
|
|
471
|
+
Evidence.SOURCE_INFORMED,
|
|
472
|
+
),
|
|
473
|
+
Endpoint(
|
|
474
|
+
"descriptive_users",
|
|
475
|
+
"GET",
|
|
476
|
+
"/gateway/api/2.0/Users",
|
|
477
|
+
SideEffect.NONE,
|
|
478
|
+
True,
|
|
479
|
+
Evidence.SOURCE_INFORMED,
|
|
480
|
+
),
|
|
436
481
|
Endpoint(
|
|
437
482
|
"attendance",
|
|
438
483
|
"POST",
|
|
@@ -643,6 +688,21 @@ PROFILE_LABELS = MappingProxyType(
|
|
|
643
688
|
"Szkoła": "school",
|
|
644
689
|
}
|
|
645
690
|
)
|
|
691
|
+
# Rows the information page lists for the login's own user after the
|
|
692
|
+
# student's rows (observed); never read, named by failure diagnostics.
|
|
693
|
+
PROFILE_OWNER_LABELS = ("Imię i nazwisko użytkownika", "Login", "Hasło")
|
|
694
|
+
# The page header shows the lucky number as `span.luckyNumber > b` (observed
|
|
695
|
+
# 2026-10-29). "?" there is reported where no number is drawn, for a school
|
|
696
|
+
# without lucky numbers and possibly on days without a draw; neither it nor
|
|
697
|
+
# an empty marker has been observed.
|
|
698
|
+
PROFILE_LUCKY_NUMBER_CLASS = "luckyNumber"
|
|
699
|
+
# For a session it no longer knows, Synergia answers every page read with 200
|
|
700
|
+
# and a page whose only heading is "Brak dostępu" (`h2.inside`), without the
|
|
701
|
+
# signed-in menu, not with a login redirect (observed 2026-10-29). A signed-in
|
|
702
|
+
# user's timetable page has no menu either, so the heading is required too.
|
|
703
|
+
LOGGED_IN_PAGE_MARKER = "graphic-menu"
|
|
704
|
+
LOGGED_OUT_HEADING = "Brak dostępu"
|
|
705
|
+
PROFILE_LUCKY_NUMBER_DISABLED = "?"
|
|
646
706
|
|
|
647
707
|
# HTML summary headers omit the two leading body cells: expander and subject.
|
|
648
708
|
# These labels/layout rules are source-informed requirements, not live evidence.
|
|
@@ -662,6 +722,35 @@ GRADE_INLINE_DETAIL_LABEL = "Ocena"
|
|
|
662
722
|
# Tooltip fields whose free text may continue on unlabelled lines. A teacher's
|
|
663
723
|
# multi-line grade comment was observed on 2026-10-27.
|
|
664
724
|
TOOLTIP_MULTILINE_LABELS = frozenset({"Komentarz"})
|
|
725
|
+
# Tooltip labels observed on grade, attendance and point pages. Diagnostics
|
|
726
|
+
# show only these by name; any other label is masked as teacher-entered text.
|
|
727
|
+
TOOLTIP_LABELS = frozenset(
|
|
728
|
+
{
|
|
729
|
+
"Czy wycieczka",
|
|
730
|
+
"Data",
|
|
731
|
+
"Data realizacji",
|
|
732
|
+
"Data wystawienia",
|
|
733
|
+
"Data zapowiedzi",
|
|
734
|
+
"Dodał",
|
|
735
|
+
"Godzina lekcyjna",
|
|
736
|
+
"Kategoria",
|
|
737
|
+
"Komentarz",
|
|
738
|
+
"Lekcja",
|
|
739
|
+
"Licz do puli",
|
|
740
|
+
"Licz do wyniku",
|
|
741
|
+
"Licz do średniej",
|
|
742
|
+
"Maksymalna ilość punktów",
|
|
743
|
+
"Nauczyciel",
|
|
744
|
+
"Obowiązek wyk. zadania",
|
|
745
|
+
"Ocena",
|
|
746
|
+
"Rodzaj",
|
|
747
|
+
"Suma punktów",
|
|
748
|
+
"Temat zajęć",
|
|
749
|
+
"Waga",
|
|
750
|
+
"Zakres do",
|
|
751
|
+
"Zakres od",
|
|
752
|
+
}
|
|
753
|
+
)
|
|
665
754
|
# A point-grade ("Oceny punktowe") table marks each subject row's expander image
|
|
666
755
|
# with this ID; ordinary grade rows use `przedmioty_<id>_node`.
|
|
667
756
|
GRADE_POINT_ROW_NODE = re.compile(r"przedmioty_OP_[0-9A-Za-z_]{1,64}_node")
|
|
@@ -693,6 +782,21 @@ GRADE_PERIOD_HEADERS = MappingProxyType(
|
|
|
693
782
|
"Ocena roczna": 0,
|
|
694
783
|
}
|
|
695
784
|
)
|
|
785
|
+
# New-schema descriptive grading: the page renders this table with empty
|
|
786
|
+
# semester cells and its script loads the grades from the gateway routes above.
|
|
787
|
+
DESCRIPTIVE_TABLE_CLASS = "newSchemaGradesByStudentTable"
|
|
788
|
+
DESCRIPTIVE_ROW_CLASS = "studentRow"
|
|
789
|
+
# Gateway account and student identifiers are opaque strings, unlike every
|
|
790
|
+
# other `{id}` route, which takes a numeric ID.
|
|
791
|
+
DESCRIPTIVE_IDENTIFIER = re.compile(r"[A-Za-z0-9_-]{1,128}")
|
|
792
|
+
DESCRIPTIVE_IDENTIFIER_OPERATIONS = frozenset(
|
|
793
|
+
{"descriptive_user_info", "descriptive_partial_grades"}
|
|
794
|
+
)
|
|
795
|
+
# The PartialGrades read is a POST whose only body is this fixed JSON object.
|
|
796
|
+
DESCRIPTIVE_PARTIAL_GRADES_BODY = b"{}"
|
|
797
|
+
DESCRIPTIVE_MAX_SUBJECTS = 1024
|
|
798
|
+
DESCRIPTIVE_MAX_USERS = 8192
|
|
799
|
+
DESCRIPTIVE_MAX_NAME_LENGTH = 1024
|
|
696
800
|
GRADE_PREDICTED_ANNUAL_HEADER = "Przewidywana ocena roczna"
|
|
697
801
|
GRADE_PREDICTED_PERIOD_HEADERS = MappingProxyType(
|
|
698
802
|
{
|