librus-python-api 1.2.1__tar.gz → 1.3.0__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.2.1 → librus_python_api-1.3.0}/API.md +39 -1
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/CHANGELOG.md +20 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/PKG-INFO +15 -7
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/README.md +14 -6
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/__init__.py +17 -1
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/config.py +37 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/models.py +74 -0
- librus_python_api-1.3.0/src/librus_python_api/school_year_archive.py +432 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/service.py +26 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/uv.lock +57 -57
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/.gitignore +0 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/LICENSE +0 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/SECURITY.md +0 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/pyproject.toml +0 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/_notification_bootstrap.py +0 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/_notification_codec.py +0 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/_storage.py +0 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/_windows_filesystem.py +0 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/announcements.py +0 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/attachment_routes.py +0 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/attachments.py +0 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/attendance.py +0 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/attendance_frequency.py +0 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/budget.py +0 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/checkpoint.py +0 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/completed_lessons.py +0 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/detail_fields.py +0 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/diagnostics.py +0 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/exceptions.py +0 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/files.py +0 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/grade_parsers.py +0 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/grade_records.py +0 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/homework_range.py +0 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/lifecycle.py +0 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/markup.py +0 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/message_content.py +0 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/messages.py +0 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/modern_body.py +0 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/modern_mailbox.py +0 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/modern_messages.py +0 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/notification_models.py +0 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/notification_persistence.py +0 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/notification_workflow.py +0 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/notifications.py +0 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/parsers.py +0 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/parsing.py +0 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/persistence.py +0 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/py.typed +0 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/recipients.py +0 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/scheduler.py +0 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/school_reads.py +0 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/sending.py +0 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/timetable.py +0 -0
- {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/transport.py +0 -0
|
@@ -191,7 +191,45 @@ source)` with an aware UTC timestamp.
|
|
|
191
191
|
selection. It reuses only that view's cache; averages and undated summaries
|
|
192
192
|
remain outside the window.
|
|
193
193
|
|
|
194
|
-
|
|
194
|
+
### School-year archive (1.3.0)
|
|
195
|
+
|
|
196
|
+
`school_year_archive(*, budget=None, max_age_seconds=0.0)` returns
|
|
197
|
+
`SchoolYearArchive(identity, years, achievements, observation)`. It performs one
|
|
198
|
+
read-only GET for all earlier years, with the same per-login cache, coalescing,
|
|
199
|
+
budgets and safe-read session recovery as `announcements()`. There is no year
|
|
200
|
+
selection or chart request. This is separate from the modern message archive.
|
|
201
|
+
|
|
202
|
+
- `years` contains `ArchiveYear` in page order: `class_name`, `school_year`,
|
|
203
|
+
`first_year`, `subjects`, `descriptive`, two raw behaviour fields and `absences`.
|
|
204
|
+
- Each `ArchiveSubject(subject, marks)` has `ArchiveMarks(first_semester,
|
|
205
|
+
second_semester, year_end)`. Empty text and `-` remain distinct. `year_end` is
|
|
206
|
+
not asserted to equal `SubjectGradeSummary.annual`.
|
|
207
|
+
- `ArchiveDescriptive(label, text)` keeps whole-year text, with explicit BR
|
|
208
|
+
boundaries as newlines. Source whitespace is normalized.
|
|
209
|
+
- `behaviour_first_semester` and `behaviour_second_semester_and_year_end` retain
|
|
210
|
+
positional text. Populated behaviour semantics have not been observed.
|
|
211
|
+
- `ArchiveAbsences(unexcused, excused, late)` contains `ArchiveCounts` with the
|
|
212
|
+
same three period names. Counts are rendered integers, never summed or repaired.
|
|
213
|
+
- `achievements` contains `ArchiveAchievement(day, class_name, category, text)`;
|
|
214
|
+
`day` is a civil `date`. A header-only achievements table yields an empty tuple.
|
|
215
|
+
|
|
216
|
+
All archive types are root exports. School text and record containers are omitted
|
|
217
|
+
from reprs; the page's student-name header does not enter archive records.
|
|
218
|
+
Limits: 16 unique consecutive school-year pairs, 128 subject rows, 32 descriptive
|
|
219
|
+
rows, 256 achievements, 1024 characters per short field, 65536 per long text
|
|
220
|
+
cell and 262144 total rendered characters, plus the shared transport/tree bounds.
|
|
221
|
+
|
|
222
|
+
The observed `Brak danych` information notice returns empty `years` and
|
|
223
|
+
`achievements`. Missing tables without that explicit marker still raise `PARSE`;
|
|
224
|
+
an empty notice combined with data tables is rejected. Repeated school years
|
|
225
|
+
(including a class-change layout) are ambiguous and raise `PARSE`. Unknown
|
|
226
|
+
geometry/markup raises `UNSUPPORTED_CAPABILITY`; bounds raise `LIMIT`, never
|
|
227
|
+
partial success. Consumers must handle these states before exposing the read.
|
|
228
|
+
See [archive evidence and limitations](contracts/grades.md#school-year-archive-130).
|
|
229
|
+
|
|
230
|
+
The observed grade-detail page added no fields beyond the list tooltip, so
|
|
231
|
+
there is no separate per-grade detail request. This finding is limited to the
|
|
232
|
+
observed layout. The observation card ("Karta spostrzeżeń") shown on some grades pages is not
|
|
195
233
|
read yet.
|
|
196
234
|
|
|
197
235
|
## Attendance
|
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.3.0 (2026-10-07) - School-year archive read
|
|
4
|
+
|
|
5
|
+
Compatible minor release. Existing calls and results are unchanged.
|
|
6
|
+
|
|
7
|
+
- Add `AccountClient.school_year_archive()` and frozen per-year records for
|
|
8
|
+
subject marks, descriptive text, raw behaviour cells, absence counts and
|
|
9
|
+
dated achievements. One read-only GET covers all earlier years without year
|
|
10
|
+
selection, chart requests or fetching grade-detail links.
|
|
11
|
+
- Apply the shared account-isolated cache, coalescing, recovery and budgets.
|
|
12
|
+
Reject ambiguous tables, unknown markup and exceeded bounds without partial
|
|
13
|
+
output. Preserve empty marks and school-provided year-end values.
|
|
14
|
+
- Add guarded weekly archive coverage with redacted year counts. Older pinned
|
|
15
|
+
libraries without the method skip this check. Installed live checks passed
|
|
16
|
+
on two parent logins, covering populated and explicit empty archives with
|
|
17
|
+
unchanged notification counters. Broader school-layout support is unqualified.
|
|
18
|
+
- Recognize the observed `Brak danych` empty notice and the malformed HTML
|
|
19
|
+
chart container after the achievements footer. Nested tabular data still fails.
|
|
20
|
+
- Record the #40 grade-detail finding: the observed page added no fields beyond
|
|
21
|
+
the list tooltip, so no separate grade-detail read is introduced.
|
|
22
|
+
|
|
3
23
|
## 1.2.1 (2026-10-06) - Live fixes for archive listing and teacher subjects
|
|
4
24
|
|
|
5
25
|
Compatible patch release found by the new pre-publication live check. 1.1.0 and
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: librus-python-api
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
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
|
|
@@ -26,6 +26,8 @@ Description-Content-Type: text/markdown
|
|
|
26
26
|
|
|
27
27
|
# librus-python-api
|
|
28
28
|
|
|
29
|
+
[](https://github.com/krzysztofbury/librus-python-api/actions/workflows/live-check.yml)
|
|
30
|
+
|
|
29
31
|
Read grades, attendance, homework, timetables and messages from **Librus Synergia**
|
|
30
32
|
in Python. This independent, asynchronous client handles login, session recovery,
|
|
31
33
|
pagination and request limits, returning typed Python objects rather than HTML.
|
|
@@ -40,9 +42,10 @@ private ACLs. Windows installations include `pywin32` and `tzdata` automatically
|
|
|
40
42
|
Network paths, reparse-point paths and unsafe storage permissions fail closed.
|
|
41
43
|
See [platform requirements](contracts/persistence.md#windows-disk-boundary-25).
|
|
42
44
|
|
|
43
|
-
**Status:** `1.0
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
**Status:** `1.3.0` adds a read-only school-year archive.
|
|
46
|
+
Installed live checks passed for populated and empty
|
|
47
|
+
archives on two parent logins; see [verification](VERIFICATION.md).
|
|
48
|
+
The stable 1.x API does not guarantee every school layout. School features depend
|
|
46
49
|
on what each account can access. See the [compatibility policy](API.md#compatibility-policy).
|
|
47
50
|
See [limitations](#supported-features-and-limitations) below.
|
|
48
51
|
|
|
@@ -51,7 +54,7 @@ See [limitations](#supported-features-and-limitations) below.
|
|
|
51
54
|
Install the exact release from PyPI after its gated publication completes:
|
|
52
55
|
|
|
53
56
|
```sh
|
|
54
|
-
python -m pip install librus-python-api==1.
|
|
57
|
+
python -m pip install librus-python-api==1.3.0
|
|
55
58
|
```
|
|
56
59
|
|
|
57
60
|
Pin the exact version qualified by your application. Before publication, use a
|
|
@@ -67,7 +70,7 @@ python -m pip install .
|
|
|
67
70
|
To install a locally built wheel instead:
|
|
68
71
|
|
|
69
72
|
```sh
|
|
70
|
-
python -m pip install ./dist/librus_python_api-1.
|
|
73
|
+
python -m pip install ./dist/librus_python_api-1.3.0-py3-none-any.whl
|
|
71
74
|
```
|
|
72
75
|
|
|
73
76
|
No CLI or background process is installed: import the library in your own program.
|
|
@@ -148,6 +151,11 @@ attendance = await client.attendance_window(today.replace(day=1), today)
|
|
|
148
151
|
|
|
149
152
|
# Permit reuse of this account's cached result for up to 60 seconds.
|
|
150
153
|
announcements = await client.announcements(max_age_seconds=60)
|
|
154
|
+
|
|
155
|
+
# Earlier school years, separate from the message archive (1.3.0).
|
|
156
|
+
archive = await client.school_year_archive()
|
|
157
|
+
for year in archive.years:
|
|
158
|
+
print(year.school_year, year.subjects)
|
|
151
159
|
```
|
|
152
160
|
|
|
153
161
|
Collections expose named record tuples, for example `homework.items`, rather than
|
|
@@ -233,7 +241,7 @@ removed in 0.7.
|
|
|
233
241
|
|
|
234
242
|
| Area | Available |
|
|
235
243
|
| --- | --- |
|
|
236
|
-
| School data | Profile, grades, attendance, timetable, announcements, agenda, homework and completed lessons |
|
|
244
|
+
| School data | Profile, grades, school-year archive, attendance, timetable, announcements, agenda, homework and completed lessons |
|
|
237
245
|
| Communication | Legacy/modern message lists and content, recipient discovery, bounded attachment streams and explicit sending |
|
|
238
246
|
| Application workflows | Shared multi-account limits, caching, notification checkpoints, optional durable send/notification stores |
|
|
239
247
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# librus-python-api
|
|
2
2
|
|
|
3
|
+
[](https://github.com/krzysztofbury/librus-python-api/actions/workflows/live-check.yml)
|
|
4
|
+
|
|
3
5
|
Read grades, attendance, homework, timetables and messages from **Librus Synergia**
|
|
4
6
|
in Python. This independent, asynchronous client handles login, session recovery,
|
|
5
7
|
pagination and request limits, returning typed Python objects rather than HTML.
|
|
@@ -14,9 +16,10 @@ private ACLs. Windows installations include `pywin32` and `tzdata` automatically
|
|
|
14
16
|
Network paths, reparse-point paths and unsafe storage permissions fail closed.
|
|
15
17
|
See [platform requirements](contracts/persistence.md#windows-disk-boundary-25).
|
|
16
18
|
|
|
17
|
-
**Status:** `1.0
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
**Status:** `1.3.0` adds a read-only school-year archive.
|
|
20
|
+
Installed live checks passed for populated and empty
|
|
21
|
+
archives on two parent logins; see [verification](VERIFICATION.md).
|
|
22
|
+
The stable 1.x API does not guarantee every school layout. School features depend
|
|
20
23
|
on what each account can access. See the [compatibility policy](API.md#compatibility-policy).
|
|
21
24
|
See [limitations](#supported-features-and-limitations) below.
|
|
22
25
|
|
|
@@ -25,7 +28,7 @@ See [limitations](#supported-features-and-limitations) below.
|
|
|
25
28
|
Install the exact release from PyPI after its gated publication completes:
|
|
26
29
|
|
|
27
30
|
```sh
|
|
28
|
-
python -m pip install librus-python-api==1.
|
|
31
|
+
python -m pip install librus-python-api==1.3.0
|
|
29
32
|
```
|
|
30
33
|
|
|
31
34
|
Pin the exact version qualified by your application. Before publication, use a
|
|
@@ -41,7 +44,7 @@ python -m pip install .
|
|
|
41
44
|
To install a locally built wheel instead:
|
|
42
45
|
|
|
43
46
|
```sh
|
|
44
|
-
python -m pip install ./dist/librus_python_api-1.
|
|
47
|
+
python -m pip install ./dist/librus_python_api-1.3.0-py3-none-any.whl
|
|
45
48
|
```
|
|
46
49
|
|
|
47
50
|
No CLI or background process is installed: import the library in your own program.
|
|
@@ -122,6 +125,11 @@ attendance = await client.attendance_window(today.replace(day=1), today)
|
|
|
122
125
|
|
|
123
126
|
# Permit reuse of this account's cached result for up to 60 seconds.
|
|
124
127
|
announcements = await client.announcements(max_age_seconds=60)
|
|
128
|
+
|
|
129
|
+
# Earlier school years, separate from the message archive (1.3.0).
|
|
130
|
+
archive = await client.school_year_archive()
|
|
131
|
+
for year in archive.years:
|
|
132
|
+
print(year.school_year, year.subjects)
|
|
125
133
|
```
|
|
126
134
|
|
|
127
135
|
Collections expose named record tuples, for example `homework.items`, rather than
|
|
@@ -207,7 +215,7 @@ removed in 0.7.
|
|
|
207
215
|
|
|
208
216
|
| Area | Available |
|
|
209
217
|
| --- | --- |
|
|
210
|
-
| School data | Profile, grades, attendance, timetable, announcements, agenda, homework and completed lessons |
|
|
218
|
+
| School data | Profile, grades, school-year archive, attendance, timetable, announcements, agenda, homework and completed lessons |
|
|
211
219
|
| Communication | Legacy/modern message lists and content, recipient discovery, bounded attachment streams and explicit sending |
|
|
212
220
|
| Application workflows | Shared multi-account limits, caching, notification checkpoints, optional durable send/notification stores |
|
|
213
221
|
|
|
@@ -16,6 +16,13 @@ from librus_python_api.models import (
|
|
|
16
16
|
AgendaEvent,
|
|
17
17
|
Announcement,
|
|
18
18
|
Announcements,
|
|
19
|
+
ArchiveAbsences,
|
|
20
|
+
ArchiveAchievement,
|
|
21
|
+
ArchiveCounts,
|
|
22
|
+
ArchiveDescriptive,
|
|
23
|
+
ArchiveMarks,
|
|
24
|
+
ArchiveSubject,
|
|
25
|
+
ArchiveYear,
|
|
19
26
|
AttachmentHeaders,
|
|
20
27
|
AttachmentMetadata,
|
|
21
28
|
Attendance,
|
|
@@ -110,6 +117,7 @@ from librus_python_api.models import (
|
|
|
110
117
|
SchoolAverage,
|
|
111
118
|
SchoolDetail,
|
|
112
119
|
SchoolReference,
|
|
120
|
+
SchoolYearArchive,
|
|
113
121
|
SendResult,
|
|
114
122
|
SendStatus,
|
|
115
123
|
SendSubmission,
|
|
@@ -127,9 +135,17 @@ from librus_python_api.models import (
|
|
|
127
135
|
from librus_python_api.sending import SendAttempt
|
|
128
136
|
from librus_python_api.service import AccountClient, LibrusService
|
|
129
137
|
|
|
130
|
-
__version__ = "1.
|
|
138
|
+
__version__ = "1.3.0"
|
|
131
139
|
|
|
132
140
|
__all__ = [
|
|
141
|
+
"ArchiveAbsences",
|
|
142
|
+
"ArchiveAchievement",
|
|
143
|
+
"ArchiveCounts",
|
|
144
|
+
"ArchiveDescriptive",
|
|
145
|
+
"ArchiveMarks",
|
|
146
|
+
"ArchiveSubject",
|
|
147
|
+
"ArchiveYear",
|
|
148
|
+
"SchoolYearArchive",
|
|
133
149
|
"AccountContext",
|
|
134
150
|
"MessagingBackend",
|
|
135
151
|
"ModernAccountData",
|
|
@@ -505,6 +505,14 @@ ENDPOINTS: Mapping[str, Endpoint] = MappingProxyType(
|
|
|
505
505
|
True,
|
|
506
506
|
Evidence.INDEPENDENTLY_OBSERVED,
|
|
507
507
|
),
|
|
508
|
+
Endpoint(
|
|
509
|
+
"school_year_archive",
|
|
510
|
+
"GET",
|
|
511
|
+
"/archiwum",
|
|
512
|
+
SideEffect.NONE,
|
|
513
|
+
True,
|
|
514
|
+
Evidence.INDEPENDENTLY_OBSERVED,
|
|
515
|
+
),
|
|
508
516
|
Endpoint(
|
|
509
517
|
"agenda",
|
|
510
518
|
"POST",
|
|
@@ -722,6 +730,35 @@ ANNOUNCEMENT_MAX_ITEMS = 256
|
|
|
722
730
|
ANNOUNCEMENT_MAX_FIELD_LENGTH = 1024
|
|
723
731
|
ANNOUNCEMENT_MAX_CONTENT_LENGTH = 65536
|
|
724
732
|
ANNOUNCEMENT_MAX_TOTAL_TEXT_LENGTH = 262144
|
|
733
|
+
|
|
734
|
+
# School-year archive (/archiwum): labels as rendered upstream, including the
|
|
735
|
+
# absence spellings. Matching is case-insensitive and exact otherwise.
|
|
736
|
+
ARCHIVE_TABLE_CLASSES = frozenset({"decorated"})
|
|
737
|
+
ARCHIVE_ACHIEVEMENT_TABLE_CLASSES = frozenset({"decorated", "big", "center"})
|
|
738
|
+
ARCHIVE_YEAR_PATTERN = (
|
|
739
|
+
r"Klasa: (?P<class_name>\S(?:.*\S)?) Rok: (?P<first>[0-9]{4})/(?P<second>[0-9]{4})"
|
|
740
|
+
)
|
|
741
|
+
ARCHIVE_PERIOD_LABELS = ("okres 1", "okres 2", "koniec roku")
|
|
742
|
+
ARCHIVE_BEHAVIOUR_HEADING = "zachowanie"
|
|
743
|
+
ARCHIVE_ABSENCES_HEADING = "nieobecności"
|
|
744
|
+
ARCHIVE_ABSENCE_LABELS = MappingProxyType(
|
|
745
|
+
{
|
|
746
|
+
"nieusprawiedlione": "unexcused",
|
|
747
|
+
"usprawiedlione": "excused",
|
|
748
|
+
"spóźnienia": "late",
|
|
749
|
+
}
|
|
750
|
+
)
|
|
751
|
+
ARCHIVE_ACHIEVEMENT_HEADERS = ("data", "klasa", "kategoria", "osiągnięcie")
|
|
752
|
+
ARCHIVE_YEAR_HEADER_TAGS = frozenset({"span", "b"})
|
|
753
|
+
ARCHIVE_MAX_YEARS = 16
|
|
754
|
+
ARCHIVE_MAX_SUBJECTS = 128
|
|
755
|
+
ARCHIVE_MAX_DESCRIPTIVE = 32
|
|
756
|
+
ARCHIVE_MAX_ACHIEVEMENTS = 256
|
|
757
|
+
ARCHIVE_MAX_TEXT_LENGTH = 65536
|
|
758
|
+
ARCHIVE_MAX_TOTAL_TEXT_LENGTH = 262144
|
|
759
|
+
ARCHIVE_MAX_YEAR_HEADER_LENGTH = 1024 + len("Klasa: Rok: 0000/0000")
|
|
760
|
+
ARCHIVE_EMPTY_TITLE = "brak danych"
|
|
761
|
+
ARCHIVE_EMPTY_BOX_CLASSES = frozenset({"warning-box", "information", "medium"})
|
|
725
762
|
ANNOUNCEMENT_TABLE_CLASSES = frozenset(
|
|
726
763
|
{"decorated", "big", "center", "printable", "margin-top"}
|
|
727
764
|
)
|
|
@@ -38,6 +38,7 @@ type OperationName = Literal[
|
|
|
38
38
|
"attachment_download",
|
|
39
39
|
"recipient_groups",
|
|
40
40
|
"recipients",
|
|
41
|
+
"school_year_archive",
|
|
41
42
|
"notification_counts",
|
|
42
43
|
"consume_schedule_events",
|
|
43
44
|
"decode_schedule_events",
|
|
@@ -778,6 +779,79 @@ class Announcements:
|
|
|
778
779
|
observation: Observation
|
|
779
780
|
|
|
780
781
|
|
|
782
|
+
@dataclass(frozen=True, slots=True)
|
|
783
|
+
class ArchiveCounts:
|
|
784
|
+
"""Counts per period as rendered; the year-end value is not checked as a sum."""
|
|
785
|
+
|
|
786
|
+
first_semester: int
|
|
787
|
+
second_semester: int
|
|
788
|
+
year_end: int
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
@dataclass(frozen=True, slots=True)
|
|
792
|
+
class ArchiveAbsences:
|
|
793
|
+
unexcused: ArchiveCounts
|
|
794
|
+
excused: ArchiveCounts
|
|
795
|
+
late: ArchiveCounts
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
@dataclass(frozen=True, slots=True)
|
|
799
|
+
class ArchiveMarks:
|
|
800
|
+
"""Rendered marks; "-" and empty text are preserved, never invented.
|
|
801
|
+
|
|
802
|
+
`year_end` is the archive's year-end column, not asserted to equal the
|
|
803
|
+
published annual grade of `SubjectGradeSummary`.
|
|
804
|
+
"""
|
|
805
|
+
|
|
806
|
+
first_semester: str = field(repr=False)
|
|
807
|
+
second_semester: str = field(repr=False)
|
|
808
|
+
year_end: str = field(repr=False)
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
@dataclass(frozen=True, slots=True)
|
|
812
|
+
class ArchiveSubject:
|
|
813
|
+
subject: str = field(repr=False)
|
|
814
|
+
marks: ArchiveMarks = field(repr=False)
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
@dataclass(frozen=True, slots=True)
|
|
818
|
+
class ArchiveDescriptive:
|
|
819
|
+
label: str = field(repr=False)
|
|
820
|
+
text: str = field(repr=False)
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
@dataclass(frozen=True, slots=True)
|
|
824
|
+
class ArchiveYear:
|
|
825
|
+
"""One earlier school year. Behaviour cells are raw positional text: the
|
|
826
|
+
first-semester column and the cell spanning second semester and year end;
|
|
827
|
+
their populated meaning is not established."""
|
|
828
|
+
|
|
829
|
+
class_name: str = field(repr=False)
|
|
830
|
+
school_year: str = field(repr=False)
|
|
831
|
+
first_year: int = field(repr=False)
|
|
832
|
+
subjects: tuple[ArchiveSubject, ...] = field(repr=False)
|
|
833
|
+
descriptive: tuple[ArchiveDescriptive, ...] = field(repr=False)
|
|
834
|
+
behaviour_first_semester: str = field(repr=False)
|
|
835
|
+
behaviour_second_semester_and_year_end: str = field(repr=False)
|
|
836
|
+
absences: ArchiveAbsences = field(repr=False)
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
@dataclass(frozen=True, slots=True)
|
|
840
|
+
class ArchiveAchievement:
|
|
841
|
+
day: date = field(repr=False)
|
|
842
|
+
class_name: str = field(repr=False)
|
|
843
|
+
category: str = field(repr=False)
|
|
844
|
+
text: str = field(repr=False)
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
@dataclass(frozen=True, slots=True)
|
|
848
|
+
class SchoolYearArchive:
|
|
849
|
+
identity: Identity
|
|
850
|
+
years: tuple[ArchiveYear, ...] = field(repr=False)
|
|
851
|
+
achievements: tuple[ArchiveAchievement, ...] = field(repr=False)
|
|
852
|
+
observation: Observation
|
|
853
|
+
|
|
854
|
+
|
|
781
855
|
@dataclass(frozen=True, slots=True)
|
|
782
856
|
class AgendaEvent:
|
|
783
857
|
day: date = field(repr=False)
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
"""The school-year archive page (`/archiwum`), authored from an owner-authorized
|
|
2
|
+
structural observation; every fixture is synthetic.
|
|
3
|
+
|
|
4
|
+
One table holds every earlier school year: a year header row, a period row,
|
|
5
|
+
grade rows (subject marks or full-year descriptive text), then exactly one
|
|
6
|
+
behaviour section, one absences section and an empty footer, in that order.
|
|
7
|
+
A separate table lists achievements. Unknown layouts are unsupported;
|
|
8
|
+
contradictions inside the known layout are parse errors. The student's name in
|
|
9
|
+
the page header and the chart scripts are never read.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
from typing import NoReturn
|
|
14
|
+
|
|
15
|
+
from lxml import html
|
|
16
|
+
|
|
17
|
+
from librus_python_api import markup
|
|
18
|
+
from librus_python_api.config import (
|
|
19
|
+
ARCHIVE_ABSENCE_LABELS,
|
|
20
|
+
ARCHIVE_ABSENCES_HEADING,
|
|
21
|
+
ARCHIVE_ACHIEVEMENT_HEADERS,
|
|
22
|
+
ARCHIVE_ACHIEVEMENT_TABLE_CLASSES,
|
|
23
|
+
ARCHIVE_BEHAVIOUR_HEADING,
|
|
24
|
+
ARCHIVE_EMPTY_BOX_CLASSES,
|
|
25
|
+
ARCHIVE_EMPTY_TITLE,
|
|
26
|
+
ARCHIVE_MAX_ACHIEVEMENTS,
|
|
27
|
+
ARCHIVE_MAX_DESCRIPTIVE,
|
|
28
|
+
ARCHIVE_MAX_SUBJECTS,
|
|
29
|
+
ARCHIVE_MAX_TEXT_LENGTH,
|
|
30
|
+
ARCHIVE_MAX_TOTAL_TEXT_LENGTH,
|
|
31
|
+
ARCHIVE_MAX_YEAR_HEADER_LENGTH,
|
|
32
|
+
ARCHIVE_MAX_YEARS,
|
|
33
|
+
ARCHIVE_PERIOD_LABELS,
|
|
34
|
+
ARCHIVE_TABLE_CLASSES,
|
|
35
|
+
ARCHIVE_YEAR_HEADER_TAGS,
|
|
36
|
+
ARCHIVE_YEAR_PATTERN,
|
|
37
|
+
)
|
|
38
|
+
from librus_python_api.exceptions import ErrorKind, LibrusError
|
|
39
|
+
from librus_python_api.models import (
|
|
40
|
+
ArchiveAbsences,
|
|
41
|
+
ArchiveAchievement,
|
|
42
|
+
ArchiveCounts,
|
|
43
|
+
ArchiveDescriptive,
|
|
44
|
+
ArchiveMarks,
|
|
45
|
+
ArchiveSubject,
|
|
46
|
+
ArchiveYear,
|
|
47
|
+
)
|
|
48
|
+
from librus_python_api.parsers import parse_page
|
|
49
|
+
|
|
50
|
+
_YEAR = re.compile(ARCHIVE_YEAR_PATTERN)
|
|
51
|
+
_COUNT = re.compile(r"[0-9]{1,6}")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _fail(kind: ErrorKind) -> NoReturn:
|
|
55
|
+
raise LibrusError(kind)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class _Text:
|
|
59
|
+
"""Rendered cell text under one total budget for the whole page."""
|
|
60
|
+
|
|
61
|
+
def __init__(self) -> None:
|
|
62
|
+
self.used = 0
|
|
63
|
+
|
|
64
|
+
def __call__(
|
|
65
|
+
self,
|
|
66
|
+
cell: html.HtmlElement,
|
|
67
|
+
*,
|
|
68
|
+
multiline: bool = False,
|
|
69
|
+
limit: int = markup.FIELD_LENGTH,
|
|
70
|
+
) -> str:
|
|
71
|
+
value = markup.text(cell, limit, multiline=multiline)
|
|
72
|
+
self.used += len(value)
|
|
73
|
+
if self.used > ARCHIVE_MAX_TOTAL_TEXT_LENGTH:
|
|
74
|
+
_fail(ErrorKind.LIMIT)
|
|
75
|
+
return value
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _plain(cell: html.HtmlElement, allowed: frozenset[str] = frozenset()) -> None:
|
|
79
|
+
"""Only the observed child vocabulary; other markup is a new layout."""
|
|
80
|
+
for node in cell.iterdescendants():
|
|
81
|
+
if isinstance(node.tag, str) and node.tag not in allowed:
|
|
82
|
+
_fail(ErrorKind.UNSUPPORTED_CAPABILITY)
|
|
83
|
+
for node in cell.iter():
|
|
84
|
+
if any(name == "title" or name.startswith("on") for name in node.attrib):
|
|
85
|
+
_fail(ErrorKind.UNSUPPORTED_CAPABILITY)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _td(cells: list[html.HtmlElement]) -> None:
|
|
89
|
+
if any(cell.tag != "td" for cell in cells):
|
|
90
|
+
_fail(ErrorKind.UNSUPPORTED_CAPABILITY)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _classes(table: html.HtmlElement) -> set[str]:
|
|
94
|
+
return set(table.get("class", "").split())
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _first_cells(table: html.HtmlElement) -> list[html.HtmlElement]:
|
|
98
|
+
first = next(markup.rows(table), None)
|
|
99
|
+
return [] if first is None else markup.cells(first)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _is_archive(table: html.HtmlElement) -> bool:
|
|
103
|
+
cells = _first_cells(table)
|
|
104
|
+
return (
|
|
105
|
+
len(cells) >= 2
|
|
106
|
+
and markup.text(cells[0]) == ""
|
|
107
|
+
and all(
|
|
108
|
+
_YEAR.fullmatch(markup.text(cell, ARCHIVE_MAX_YEAR_HEADER_LENGTH))
|
|
109
|
+
for cell in cells[1:]
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _is_achievement_list(table: html.HtmlElement) -> bool:
|
|
115
|
+
cells = _first_cells(table)
|
|
116
|
+
return [
|
|
117
|
+
markup.text(cell, ARCHIVE_MAX_YEAR_HEADER_LENGTH).casefold() for cell in cells
|
|
118
|
+
] == list(ARCHIVE_ACHIEVEMENT_HEADERS)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _in_footer(row: html.HtmlElement) -> bool:
|
|
122
|
+
return next(row.iterancestors("tfoot"), None) is not None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _years(header: html.HtmlElement, text: _Text) -> list[tuple[str, str, int]]:
|
|
126
|
+
cells = markup.cells(header)
|
|
127
|
+
_td(cells)
|
|
128
|
+
_plain(cells[0])
|
|
129
|
+
if len(cells) - 1 > ARCHIVE_MAX_YEARS:
|
|
130
|
+
_fail(ErrorKind.LIMIT)
|
|
131
|
+
if markup.colspan(cells[0]) != 1:
|
|
132
|
+
_fail(ErrorKind.UNSUPPORTED_CAPABILITY)
|
|
133
|
+
years: list[tuple[str, str, int]] = []
|
|
134
|
+
for cell in cells[1:]:
|
|
135
|
+
if markup.colspan(cell) != 3:
|
|
136
|
+
_fail(ErrorKind.UNSUPPORTED_CAPABILITY)
|
|
137
|
+
_plain(cell, ARCHIVE_YEAR_HEADER_TAGS)
|
|
138
|
+
match = _YEAR.fullmatch(text(cell, limit=ARCHIVE_MAX_YEAR_HEADER_LENGTH))
|
|
139
|
+
if match is None:
|
|
140
|
+
_fail(ErrorKind.PARSE)
|
|
141
|
+
first, second = int(match["first"]), int(match["second"])
|
|
142
|
+
if len(match["class_name"]) > markup.FIELD_LENGTH:
|
|
143
|
+
_fail(ErrorKind.LIMIT)
|
|
144
|
+
if second != first + 1 or any(first == seen for _, _, seen in years):
|
|
145
|
+
_fail(ErrorKind.PARSE)
|
|
146
|
+
years.append((match["class_name"], f"{first}/{second}", first))
|
|
147
|
+
return years
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _check_periods(row: html.HtmlElement, count: int, text: _Text) -> None:
|
|
151
|
+
cells = markup.cells(row)
|
|
152
|
+
_td(cells)
|
|
153
|
+
for cell in cells:
|
|
154
|
+
_plain(cell)
|
|
155
|
+
if (
|
|
156
|
+
len(cells) != 1 + 3 * count
|
|
157
|
+
or any(markup.colspan(cell) != 1 for cell in cells)
|
|
158
|
+
or text(cells[0])
|
|
159
|
+
or [text(cell).casefold() for cell in cells[1:]]
|
|
160
|
+
!= list(ARCHIVE_PERIOD_LABELS) * count
|
|
161
|
+
):
|
|
162
|
+
_fail(ErrorKind.UNSUPPORTED_CAPABILITY)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class _Body:
|
|
166
|
+
"""The section order: grades, behaviour, absences, footer."""
|
|
167
|
+
|
|
168
|
+
def __init__(self, count: int, text: _Text) -> None:
|
|
169
|
+
self.count = count
|
|
170
|
+
self.text = text
|
|
171
|
+
self.state = "grades"
|
|
172
|
+
self.labels: set[str] = set()
|
|
173
|
+
self.subjects: list[tuple[str, list[ArchiveMarks]]] = []
|
|
174
|
+
self.descriptive: list[tuple[str, list[str]]] = []
|
|
175
|
+
self.behaviour: list[tuple[str, str]] = []
|
|
176
|
+
self.absences: dict[str, list[ArchiveCounts]] = {}
|
|
177
|
+
|
|
178
|
+
def read(self, row: html.HtmlElement) -> None:
|
|
179
|
+
cells = markup.cells(row)
|
|
180
|
+
spans = [markup.colspan(cell) for cell in cells]
|
|
181
|
+
full = [1 + 3 * self.count]
|
|
182
|
+
heading = spans == full and "bolded" in row.get("class", "").split()
|
|
183
|
+
if self.state == "grades":
|
|
184
|
+
if heading:
|
|
185
|
+
self._heading(cells, ARCHIVE_BEHAVIOUR_HEADING, "behaviour")
|
|
186
|
+
else:
|
|
187
|
+
self._grade(cells, spans)
|
|
188
|
+
elif self.state == "behaviour":
|
|
189
|
+
self._behaviour(cells, spans, heading)
|
|
190
|
+
elif self.state == "absences_heading":
|
|
191
|
+
if not heading:
|
|
192
|
+
_fail(ErrorKind.UNSUPPORTED_CAPABILITY)
|
|
193
|
+
self._heading(cells, ARCHIVE_ABSENCES_HEADING, "absences")
|
|
194
|
+
elif self.state == "absences":
|
|
195
|
+
if spans == full and not heading:
|
|
196
|
+
self._footer(cells)
|
|
197
|
+
else:
|
|
198
|
+
self._absence(cells, spans)
|
|
199
|
+
else:
|
|
200
|
+
_fail(ErrorKind.PARSE)
|
|
201
|
+
|
|
202
|
+
def _heading(self, cells: list[html.HtmlElement], label: str, state: str) -> None:
|
|
203
|
+
_td(cells)
|
|
204
|
+
_plain(cells[0])
|
|
205
|
+
if self.text(cells[0]).casefold() != label:
|
|
206
|
+
_fail(ErrorKind.UNSUPPORTED_CAPABILITY)
|
|
207
|
+
self.state = state
|
|
208
|
+
|
|
209
|
+
def _label(self, cells: list[html.HtmlElement]) -> str:
|
|
210
|
+
if not cells or cells[0].tag != "th":
|
|
211
|
+
_fail(ErrorKind.UNSUPPORTED_CAPABILITY)
|
|
212
|
+
_plain(cells[0])
|
|
213
|
+
return self.text(cells[0])
|
|
214
|
+
|
|
215
|
+
def _values(
|
|
216
|
+
self,
|
|
217
|
+
cells: list[html.HtmlElement],
|
|
218
|
+
*,
|
|
219
|
+
multiline: bool = False,
|
|
220
|
+
limit: int = markup.FIELD_LENGTH,
|
|
221
|
+
) -> list[str]:
|
|
222
|
+
_td(cells)
|
|
223
|
+
values = []
|
|
224
|
+
for cell in cells:
|
|
225
|
+
_plain(cell, frozenset({"br"}) if multiline else frozenset())
|
|
226
|
+
values.append(self.text(cell, multiline=multiline, limit=limit))
|
|
227
|
+
return values
|
|
228
|
+
|
|
229
|
+
def _grade(self, cells: list[html.HtmlElement], spans: list[int]) -> None:
|
|
230
|
+
label = self._label(cells)
|
|
231
|
+
if not label:
|
|
232
|
+
_fail(ErrorKind.UNSUPPORTED_CAPABILITY)
|
|
233
|
+
if spans == [1] * (1 + 3 * self.count):
|
|
234
|
+
values = self._values(cells[1:])
|
|
235
|
+
marks = [
|
|
236
|
+
ArchiveMarks(*values[3 * i : 3 * i + 3]) for i in range(self.count)
|
|
237
|
+
]
|
|
238
|
+
self.subjects.append((label, marks))
|
|
239
|
+
limit, size = ARCHIVE_MAX_SUBJECTS, len(self.subjects)
|
|
240
|
+
elif spans == [1] + [3] * self.count:
|
|
241
|
+
texts = self._values(
|
|
242
|
+
cells[1:], multiline=True, limit=ARCHIVE_MAX_TEXT_LENGTH
|
|
243
|
+
)
|
|
244
|
+
self.descriptive.append((label, texts))
|
|
245
|
+
limit, size = ARCHIVE_MAX_DESCRIPTIVE, len(self.descriptive)
|
|
246
|
+
else:
|
|
247
|
+
_fail(ErrorKind.UNSUPPORTED_CAPABILITY)
|
|
248
|
+
if label in self.labels:
|
|
249
|
+
_fail(ErrorKind.PARSE)
|
|
250
|
+
self.labels.add(label)
|
|
251
|
+
if size > limit:
|
|
252
|
+
_fail(ErrorKind.LIMIT)
|
|
253
|
+
|
|
254
|
+
def _behaviour(
|
|
255
|
+
self, cells: list[html.HtmlElement], spans: list[int], heading: bool
|
|
256
|
+
) -> None:
|
|
257
|
+
if heading or spans != [1] + [1, 2] * self.count or self._label(cells):
|
|
258
|
+
_fail(ErrorKind.UNSUPPORTED_CAPABILITY)
|
|
259
|
+
values = self._values(cells[1:])
|
|
260
|
+
self.behaviour = [(values[2 * i], values[2 * i + 1]) for i in range(self.count)]
|
|
261
|
+
self.state = "absences_heading"
|
|
262
|
+
|
|
263
|
+
def _absence(self, cells: list[html.HtmlElement], spans: list[int]) -> None:
|
|
264
|
+
field = ARCHIVE_ABSENCE_LABELS.get(self._label(cells).casefold())
|
|
265
|
+
if field is None or spans != [1] * (1 + 3 * self.count):
|
|
266
|
+
_fail(ErrorKind.UNSUPPORTED_CAPABILITY)
|
|
267
|
+
if field in self.absences:
|
|
268
|
+
_fail(ErrorKind.PARSE)
|
|
269
|
+
values = self._values(cells[1:])
|
|
270
|
+
if not all(_COUNT.fullmatch(value) for value in values):
|
|
271
|
+
_fail(ErrorKind.PARSE)
|
|
272
|
+
numbers = [int(value) for value in values]
|
|
273
|
+
self.absences[field] = [
|
|
274
|
+
ArchiveCounts(*numbers[3 * i : 3 * i + 3]) for i in range(self.count)
|
|
275
|
+
]
|
|
276
|
+
|
|
277
|
+
def _footer(self, cells: list[html.HtmlElement]) -> None:
|
|
278
|
+
_td(cells)
|
|
279
|
+
_plain(cells[0])
|
|
280
|
+
if self.text(cells[0]) or len(self.absences) != len(ARCHIVE_ABSENCE_LABELS):
|
|
281
|
+
_fail(ErrorKind.PARSE)
|
|
282
|
+
self.state = "end"
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _archive(table: html.HtmlElement, text: _Text) -> tuple[ArchiveYear, ...]:
|
|
286
|
+
rows = list(markup.rows(table))
|
|
287
|
+
if len(rows) < 2:
|
|
288
|
+
_fail(ErrorKind.PARSE)
|
|
289
|
+
years = _years(rows[0], text)
|
|
290
|
+
_check_periods(rows[1], len(years), text)
|
|
291
|
+
body = _Body(len(years), text)
|
|
292
|
+
for row in rows[2:]:
|
|
293
|
+
body.read(row)
|
|
294
|
+
if body.state != "end":
|
|
295
|
+
_fail(ErrorKind.PARSE)
|
|
296
|
+
return tuple(
|
|
297
|
+
ArchiveYear(
|
|
298
|
+
class_name=class_name,
|
|
299
|
+
school_year=school_year,
|
|
300
|
+
first_year=first_year,
|
|
301
|
+
subjects=tuple(
|
|
302
|
+
ArchiveSubject(label, marks[i]) for label, marks in body.subjects
|
|
303
|
+
),
|
|
304
|
+
descriptive=tuple(
|
|
305
|
+
ArchiveDescriptive(label, texts[i]) for label, texts in body.descriptive
|
|
306
|
+
),
|
|
307
|
+
behaviour_first_semester=body.behaviour[i][0],
|
|
308
|
+
behaviour_second_semester_and_year_end=body.behaviour[i][1],
|
|
309
|
+
absences=ArchiveAbsences(
|
|
310
|
+
unexcused=body.absences["unexcused"][i],
|
|
311
|
+
excused=body.absences["excused"][i],
|
|
312
|
+
late=body.absences["late"][i],
|
|
313
|
+
),
|
|
314
|
+
)
|
|
315
|
+
for i, (class_name, school_year, first_year) in enumerate(years)
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _achievements(
|
|
320
|
+
table: html.HtmlElement, text: _Text
|
|
321
|
+
) -> tuple[ArchiveAchievement, ...]:
|
|
322
|
+
items: list[ArchiveAchievement] = []
|
|
323
|
+
rows = list(markup.rows(table))
|
|
324
|
+
headers = markup.cells(rows[0])
|
|
325
|
+
_td(headers)
|
|
326
|
+
for cell in headers:
|
|
327
|
+
_plain(cell)
|
|
328
|
+
if markup.colspan(cell) != 1:
|
|
329
|
+
_fail(ErrorKind.UNSUPPORTED_CAPABILITY)
|
|
330
|
+
text(cell)
|
|
331
|
+
finished = False
|
|
332
|
+
for row in rows[1:]:
|
|
333
|
+
if finished:
|
|
334
|
+
_fail(ErrorKind.PARSE)
|
|
335
|
+
cells = markup.cells(row)
|
|
336
|
+
_td(cells)
|
|
337
|
+
if _in_footer(row):
|
|
338
|
+
if len(cells) != 1 or text(cells[0]):
|
|
339
|
+
_fail(ErrorKind.PARSE)
|
|
340
|
+
_plain(cells[0])
|
|
341
|
+
if markup.colspan(cells[0]) != 4:
|
|
342
|
+
_fail(ErrorKind.UNSUPPORTED_CAPABILITY)
|
|
343
|
+
finished = True
|
|
344
|
+
continue
|
|
345
|
+
if len(cells) != 4 or any(markup.colspan(cell) != 1 for cell in cells):
|
|
346
|
+
_fail(ErrorKind.UNSUPPORTED_CAPABILITY)
|
|
347
|
+
for i, cell in enumerate(cells):
|
|
348
|
+
_plain(cell, frozenset({"br"}) if i == 3 else frozenset())
|
|
349
|
+
items.append(
|
|
350
|
+
ArchiveAchievement(
|
|
351
|
+
day=markup.civil_date(text(cells[0])),
|
|
352
|
+
class_name=text(cells[1]),
|
|
353
|
+
category=text(cells[2]),
|
|
354
|
+
text=text(cells[3], multiline=True, limit=ARCHIVE_MAX_TEXT_LENGTH),
|
|
355
|
+
)
|
|
356
|
+
)
|
|
357
|
+
if len(items) > ARCHIVE_MAX_ACHIEVEMENTS:
|
|
358
|
+
_fail(ErrorKind.LIMIT)
|
|
359
|
+
return tuple(items)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _explicit_empty(document: html.HtmlElement) -> bool:
|
|
363
|
+
notices = []
|
|
364
|
+
for box in document.iter("div"):
|
|
365
|
+
if not ARCHIVE_EMPTY_BOX_CLASSES <= _classes(box):
|
|
366
|
+
continue
|
|
367
|
+
parent = box.getparent()
|
|
368
|
+
if parent is None or "container-background" not in _classes(parent):
|
|
369
|
+
continue
|
|
370
|
+
for head in box:
|
|
371
|
+
if head.tag != "div" or "warning-head" not in _classes(head):
|
|
372
|
+
continue
|
|
373
|
+
for title in head:
|
|
374
|
+
if title.tag == "span" and "warning-title" in _classes(title):
|
|
375
|
+
_plain(title)
|
|
376
|
+
if markup.text(title).casefold() != ARCHIVE_EMPTY_TITLE:
|
|
377
|
+
_fail(ErrorKind.UNSUPPORTED_CAPABILITY)
|
|
378
|
+
notices.append(title)
|
|
379
|
+
if len(notices) > 1:
|
|
380
|
+
_fail(ErrorKind.PARSE)
|
|
381
|
+
if notices and any("decorated" in _classes(t) for t in document.iter("table")):
|
|
382
|
+
_fail(ErrorKind.PARSE)
|
|
383
|
+
return bool(notices)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _check_nesting(table: html.HtmlElement, *, chart_trailer: bool = False) -> None:
|
|
387
|
+
if next(table.iterancestors("table"), None) is not None:
|
|
388
|
+
_fail(ErrorKind.PARSE)
|
|
389
|
+
nested = [node for node in table.iter("table") if node is not table]
|
|
390
|
+
if not nested:
|
|
391
|
+
return
|
|
392
|
+
# Live HTML nests the post-footer charts in a stray opening table. Permit
|
|
393
|
+
# that single rowless trailing container, never nested tabular data.
|
|
394
|
+
if not chart_trailer or len(nested) != 1:
|
|
395
|
+
_fail(ErrorKind.PARSE)
|
|
396
|
+
trailer = nested[0]
|
|
397
|
+
previous = trailer.getprevious()
|
|
398
|
+
if (
|
|
399
|
+
trailer.getparent() is not table
|
|
400
|
+
or trailer.getnext() is not None
|
|
401
|
+
or previous is None
|
|
402
|
+
or previous.tag != "tfoot"
|
|
403
|
+
or trailer.attrib
|
|
404
|
+
or any(
|
|
405
|
+
node.tag in {"tr", "td", "th", "thead", "tbody", "tfoot"}
|
|
406
|
+
for node in trailer.iterdescendants()
|
|
407
|
+
)
|
|
408
|
+
):
|
|
409
|
+
_fail(ErrorKind.PARSE)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def parse_school_year_archive(
|
|
413
|
+
body: bytes,
|
|
414
|
+
) -> tuple[tuple[ArchiveYear, ...], tuple[ArchiveAchievement, ...]]:
|
|
415
|
+
document = parse_page(body)
|
|
416
|
+
archives = [t for t in document.iter("table") if _is_archive(t)]
|
|
417
|
+
lists = [t for t in document.iter("table") if _is_achievement_list(t)]
|
|
418
|
+
if _explicit_empty(document):
|
|
419
|
+
if archives or lists:
|
|
420
|
+
_fail(ErrorKind.PARSE)
|
|
421
|
+
return (), ()
|
|
422
|
+
if len(archives) != 1 or len(lists) != 1:
|
|
423
|
+
_fail(ErrorKind.PARSE)
|
|
424
|
+
archive, achievements = archives[0], lists[0]
|
|
425
|
+
_check_nesting(archive)
|
|
426
|
+
_check_nesting(achievements, chart_trailer=True)
|
|
427
|
+
if not ARCHIVE_TABLE_CLASSES <= _classes(archive) or not (
|
|
428
|
+
ARCHIVE_ACHIEVEMENT_TABLE_CLASSES <= _classes(achievements)
|
|
429
|
+
):
|
|
430
|
+
_fail(ErrorKind.UNSUPPORTED_CAPABILITY)
|
|
431
|
+
text = _Text()
|
|
432
|
+
return _archive(archive, text), _achievements(achievements, text)
|
|
@@ -138,6 +138,7 @@ from librus_python_api.models import (
|
|
|
138
138
|
SchedulerSnapshot,
|
|
139
139
|
SchoolDetail,
|
|
140
140
|
SchoolReference,
|
|
141
|
+
SchoolYearArchive,
|
|
141
142
|
SendResult,
|
|
142
143
|
SendSubmission,
|
|
143
144
|
StudentInformation,
|
|
@@ -196,6 +197,7 @@ from librus_python_api.school_reads import (
|
|
|
196
197
|
parse_homework,
|
|
197
198
|
parse_school_detail,
|
|
198
199
|
)
|
|
200
|
+
from librus_python_api.school_year_archive import parse_school_year_archive
|
|
199
201
|
from librus_python_api.sending import SendAttempt, parse_send_acknowledgement
|
|
200
202
|
from librus_python_api.timetable import parse_timetable
|
|
201
203
|
from librus_python_api.transport import (
|
|
@@ -1486,6 +1488,30 @@ class AccountClient:
|
|
|
1486
1488
|
|
|
1487
1489
|
return await self._read(("announcements",), fetch, budget, max_age_seconds)
|
|
1488
1490
|
|
|
1491
|
+
async def school_year_archive(
|
|
1492
|
+
self, *, budget: RequestBudget | None = None, max_age_seconds: float = 0.0
|
|
1493
|
+
) -> SchoolYearArchive:
|
|
1494
|
+
"""Read every earlier school year's summary and the achievements list.
|
|
1495
|
+
|
|
1496
|
+
One GET with no side effect; the page's student name and charts are
|
|
1497
|
+
never read. A layout other than the observed one is unsupported.
|
|
1498
|
+
"""
|
|
1499
|
+
|
|
1500
|
+
async def fetch(budget: RequestBudget, _: bool) -> SchoolYearArchive:
|
|
1501
|
+
years, achievements = await self._page(
|
|
1502
|
+
"school_year_archive", budget, parse_school_year_archive
|
|
1503
|
+
)
|
|
1504
|
+
return SchoolYearArchive(
|
|
1505
|
+
self._session_identity(),
|
|
1506
|
+
years,
|
|
1507
|
+
achievements,
|
|
1508
|
+
self._observation("school_year_archive"),
|
|
1509
|
+
)
|
|
1510
|
+
|
|
1511
|
+
return await self._read(
|
|
1512
|
+
("school_year_archive",), fetch, budget, max_age_seconds
|
|
1513
|
+
)
|
|
1514
|
+
|
|
1489
1515
|
async def agenda(
|
|
1490
1516
|
self,
|
|
1491
1517
|
year: int,
|
|
@@ -1233,7 +1233,7 @@ wheels = [
|
|
|
1233
1233
|
|
|
1234
1234
|
[[package]]
|
|
1235
1235
|
name = "mypy"
|
|
1236
|
-
version = "2.
|
|
1236
|
+
version = "2.4.0"
|
|
1237
1237
|
source = { registry = "https://pypi.org/simple" }
|
|
1238
1238
|
dependencies = [
|
|
1239
1239
|
{ name = "ast-serialize" },
|
|
@@ -1242,41 +1242,41 @@ dependencies = [
|
|
|
1242
1242
|
{ name = "pathspec" },
|
|
1243
1243
|
{ name = "typing-extensions" },
|
|
1244
1244
|
]
|
|
1245
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
1246
|
-
wheels = [
|
|
1247
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1248
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1249
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1250
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1251
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1252
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1253
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1254
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1255
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1256
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1257
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1258
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1259
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1260
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1261
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1262
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1263
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1264
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1265
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1266
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1267
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1268
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1269
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1270
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1271
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1272
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1273
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1274
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1275
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1276
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1277
|
-
{ url = "https://files.pythonhosted.org/packages/c0/
|
|
1278
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1279
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1245
|
+
sdist = { url = "https://files.pythonhosted.org/packages/34/4e/64300736cf0a0373a27b94a91b664ee7382e36f77b0621bae6381da3e180/mypy-2.4.0.tar.gz", hash = "sha256:77bdaebd452f43fcfc4cc3ba94352a3ea537cd01e3f2d0879f48673d2ec00d6e", size = 4064425, upload-time = "2026-10-01T20:40:39.229Z" }
|
|
1246
|
+
wheels = [
|
|
1247
|
+
{ url = "https://files.pythonhosted.org/packages/44/f2/eb15183c97c69d7cbfac990a6efd33a19ecfd97dab9e714c742fa78a784f/mypy-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7da85fbcff6dac1abcc636707bed38b45598131fb7a605d9719c70b5cc733af8", size = 14213395, upload-time = "2026-10-01T20:40:21.178Z" },
|
|
1248
|
+
{ url = "https://files.pythonhosted.org/packages/8c/b5/ba91b6ff65e4d6b6ff53b2b3b3ac5f1babf0c7c27d0b43a0196b1c967926/mypy-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4209da39d85cf240f762af622d8180fcdfcb4727d021f44ade62d613a1a43324", size = 14373009, upload-time = "2026-10-01T20:39:58.86Z" },
|
|
1249
|
+
{ url = "https://files.pythonhosted.org/packages/d5/c4/484275efc935c0003e55e4e8a33e4b8e99528ee956c12256ece4708f903f/mypy-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6be721bd4bd57576193653b75b4af3461c9d0bf7dd8b528f782e9be210dc75bb", size = 15465557, upload-time = "2026-10-01T20:39:01.794Z" },
|
|
1250
|
+
{ url = "https://files.pythonhosted.org/packages/50/30/66eb6fdd0875e3c9025a02f0bb0ea2e524b74274b658c37fde0068c4939d/mypy-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1fde197ae65be856a034a91b70ed747a16562ca69577785f06c661548424bf1", size = 15728673, upload-time = "2026-10-01T20:38:43.724Z" },
|
|
1251
|
+
{ url = "https://files.pythonhosted.org/packages/58/bc/2aa98fd7f49c42dba8e9c065886fadffdd00f3c654cff3f2a7103a797ac8/mypy-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:295ecf2e57542cd836ca537486951289678c8c7d1ee6ad74ebe29b2168a003cf", size = 11389803, upload-time = "2026-10-01T20:39:37.547Z" },
|
|
1252
|
+
{ url = "https://files.pythonhosted.org/packages/49/41/17b60df2d946792ef6af43b89351f5c9ddabc69053206b2304945572a744/mypy-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:bc378bdad4e9f12b5bd96466083d1e71acf00594ec9c7b2bdb5e02816f77f303", size = 10951060, upload-time = "2026-10-01T20:39:04.015Z" },
|
|
1253
|
+
{ url = "https://files.pythonhosted.org/packages/e0/66/924be0b653372ed31ad5c48e26044cc00840e591943a56e61621cf05b60e/mypy-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:058165f564ccf559c68c70fec2091fca5891110480210c22594635e3f6683437", size = 14234230, upload-time = "2026-10-01T20:39:44.535Z" },
|
|
1254
|
+
{ url = "https://files.pythonhosted.org/packages/56/39/c4f176880a4177123576de6cec6309feea8f42fca2bf2f6584e88054f656/mypy-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fa247e02b505a45a2775f69df38d360d197e3790bc60f717595db9eda358b6e", size = 14392944, upload-time = "2026-10-01T20:40:05.78Z" },
|
|
1255
|
+
{ url = "https://files.pythonhosted.org/packages/bc/1c/26e16977e25ef2494a74f8ffc872a76ec2c3aa43c3156057cbdf88f352c5/mypy-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20e9a5cd875837520c43db98dea0b6d0c2197833d95c30127d8f570fb9b1f00b", size = 15441463, upload-time = "2026-10-01T20:40:32.412Z" },
|
|
1256
|
+
{ url = "https://files.pythonhosted.org/packages/31/9c/9e4b049f0ecefbfd6817ca2a16eea55dd74a07950268edc6cffd29ccfe98/mypy-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9f03a7828cca2b0adcd6662aee8f2711ff8830e1027641fdea3ab0b787483566", size = 15705406, upload-time = "2026-10-01T20:40:34.844Z" },
|
|
1257
|
+
{ url = "https://files.pythonhosted.org/packages/4a/5e/e861b5f6c5ef9ee6cd24683aed1edecf82a26dbd536049f9b7850b586267/mypy-2.4.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:9279488933040b638c0ab739084c0ca100efeea6db581bf5d7628d8e89de53fe", size = 7897523, upload-time = "2026-10-01T20:39:39.861Z" },
|
|
1258
|
+
{ url = "https://files.pythonhosted.org/packages/3a/87/61ffee58b25a956532a6006fae84918a2de849ed25f397492b2e815fe7b3/mypy-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:2106b55105ba5ea9be4f53a24517fc5fa927ff1585edc9bc1a975abb72caef89", size = 11576345, upload-time = "2026-10-01T20:39:41.553Z" },
|
|
1259
|
+
{ url = "https://files.pythonhosted.org/packages/a1/88/a331c20698971c2ce8d1c30f317fd61b5be13a85b1f053e12dfa22ac6568/mypy-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:528c8744b8b5e3ecb8774f86af38d2376216816e9908317ad055f3c9c2d74799", size = 11172062, upload-time = "2026-10-01T20:38:59.534Z" },
|
|
1260
|
+
{ url = "https://files.pythonhosted.org/packages/47/c0/4f7daa73270dced8e86c6f4a911c68082d03a84e6c1ec9bd916028d66131/mypy-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0bb95cf34899e4619c61ab0a8667804e139e580b30d5df12af2102dfe44d0c97", size = 15711208, upload-time = "2026-10-01T20:39:30.597Z" },
|
|
1261
|
+
{ url = "https://files.pythonhosted.org/packages/2f/05/f1afa303c678be24cf7a266d38fb24b3de4599a024c2f9ba0d5905a3efa3/mypy-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86d616fe84c6eab8026f8c50ab5bcb90db780d2ccd233d971e34e92bede9b359", size = 15935442, upload-time = "2026-10-01T20:39:22.781Z" },
|
|
1262
|
+
{ url = "https://files.pythonhosted.org/packages/9d/d6/6a1a45459b63716e0d035f4892a926d3054f0a8cbfa02551d228a9946083/mypy-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3f86fd1313dd69d013e265f1fdcd12ea7a9d606f9875b2a3db946cd334555f3", size = 17240044, upload-time = "2026-10-01T20:39:32.95Z" },
|
|
1263
|
+
{ url = "https://files.pythonhosted.org/packages/87/85/ae33bee66c13f98d421964d87bf0888be941063bc75c1204a4cf142cf1cc/mypy-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:720434d48542ecfe84d32d287b727569d3fc8f5769acd39051130e490a5c295c", size = 17545856, upload-time = "2026-10-01T20:39:08.827Z" },
|
|
1264
|
+
{ url = "https://files.pythonhosted.org/packages/df/de/eeff209b65c3e267818d79333bb9f058da6e4e73761461bd94748a2eb628/mypy-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a6e851b82c0661f69f1630fc16172c68787a6a9cf0991e7c6437d60976cdcd76", size = 12369342, upload-time = "2026-10-01T20:40:10.516Z" },
|
|
1265
|
+
{ url = "https://files.pythonhosted.org/packages/2b/43/e62d8d5c1dd737aa248968302ff7d6eabf997c3301773ed8bcb64932ca77/mypy-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:3bd0e340f0ebe65c548210f53be3fd8192e83964760caf0c28bef368e68b0d37", size = 11849099, upload-time = "2026-10-01T20:39:51.834Z" },
|
|
1266
|
+
{ url = "https://files.pythonhosted.org/packages/2e/5f/335b8980055118dc131355155883fb676bb2161a0d97e08658e479c776fb/mypy-2.4.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:afa89837d9be67e0cadfa33bca3bb7efdda98c3b07e74dc3b635ebfb1c8a926a", size = 14241302, upload-time = "2026-10-01T20:39:13.227Z" },
|
|
1267
|
+
{ url = "https://files.pythonhosted.org/packages/18/37/1482fdc49332b145828912b15f16eee0c4ca70a8bce0f6514ece79b14680/mypy-2.4.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb443e81057896132d3642d6be219e6efd158691ac7883e3ba8fcb469865f05d", size = 14408200, upload-time = "2026-10-01T20:38:52.576Z" },
|
|
1268
|
+
{ url = "https://files.pythonhosted.org/packages/04/09/dce2e8f6c1b31053c430ef6963f6f7a38ccb49b90c5e01e37ed0129b7d5a/mypy-2.4.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7f38f57d344f8b6accb40e01c3d83cfc590498231724d16c07ffb7940f157818", size = 15444409, upload-time = "2026-10-01T20:39:18.1Z" },
|
|
1269
|
+
{ url = "https://files.pythonhosted.org/packages/43/c5/91b68306da4cd280cb15be35dbb5ffc343cabd67c4b121b6625bf2d13177/mypy-2.4.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e76172710bd4e5eeae061abfd68347e5264632e02778be61784671ae3a2132f5", size = 15731186, upload-time = "2026-10-01T20:40:23.426Z" },
|
|
1270
|
+
{ url = "https://files.pythonhosted.org/packages/64/71/2d0340182a8f27352fb1e25531355951108b506097c433937e9eec452ffd/mypy-2.4.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:f83353e47ab520bf6fd4df8f5897d9fe081211f2fbc4b7d37736a3e3c166cbcf", size = 7897838, upload-time = "2026-10-01T20:38:50.779Z" },
|
|
1271
|
+
{ url = "https://files.pythonhosted.org/packages/3e/08/32703c117e134c02efa2a82705bb40cee91eaedea10b6530e20eba651b1f/mypy-2.4.0-cp315-cp315-win_amd64.whl", hash = "sha256:970b221ed5842213d98e3c480c08f795ace4b1f81fb21e1b126bd0476bce1c34", size = 11575700, upload-time = "2026-10-01T20:39:53.982Z" },
|
|
1272
|
+
{ url = "https://files.pythonhosted.org/packages/ab/08/08bb269feafdaad031046ee2d771528a64467e6a180f962afc28c4a3ccd8/mypy-2.4.0-cp315-cp315-win_arm64.whl", hash = "sha256:502b94b0b331f7dafe32fd6b151797ddbb4f32385b362e722c783a025e5954a3", size = 11172908, upload-time = "2026-10-01T20:40:12.91Z" },
|
|
1273
|
+
{ url = "https://files.pythonhosted.org/packages/fb/3f/c5c92626006ca92c7686adfbece47fc6a0daa0e54753952a9ad13ce0561c/mypy-2.4.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c9de622fd397495695d0598ddc789222bfcfec9d7c9ec3a1e385c855e3bc5e01", size = 15705970, upload-time = "2026-10-01T20:40:17.79Z" },
|
|
1274
|
+
{ url = "https://files.pythonhosted.org/packages/10/f3/863365f7997a76a5a1dd42d7902ab05afa4124e8419089cbca1ce2554db1/mypy-2.4.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f459f0b4f0596d9d51fe7716b404b35287b99e77da98a7af90a65dd5fd61141", size = 15942234, upload-time = "2026-10-01T20:40:37.243Z" },
|
|
1275
|
+
{ url = "https://files.pythonhosted.org/packages/f6/30/2f45b1f425a2c95dbe1a3f4d076bfd42b76e9615dba5906230703b14e4eb/mypy-2.4.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9b028548b3af480e2b1ed8df14ccaac86f99c9f600d1580770ab7ba3dcd40f0", size = 17218364, upload-time = "2026-10-01T20:39:06.497Z" },
|
|
1276
|
+
{ url = "https://files.pythonhosted.org/packages/43/8b/5b2bbfc69e84800b78fa2dba16d995b003f93b573558e412c5490d6e6c37/mypy-2.4.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:cb734b2668c1f40d07ce093bbeb4407e9527c67901627b0e1679825be3f09975", size = 17557934, upload-time = "2026-10-01T20:38:46.077Z" },
|
|
1277
|
+
{ url = "https://files.pythonhosted.org/packages/df/c0/1a5dc601c22041a7bbfe1ff71cf097fbbd4fb49930867c6789baf5102106/mypy-2.4.0-cp315-cp315t-win_amd64.whl", hash = "sha256:172e30b8fea631fe310f0c665477f52d9ea40bb4e99e0c81dc30118563b13710", size = 12350955, upload-time = "2026-10-01T20:40:01.52Z" },
|
|
1278
|
+
{ url = "https://files.pythonhosted.org/packages/1c/bc/697e9e26fc2a86c094ad67ee1e419971b7f01ded66ee66231b09e9f3e12e/mypy-2.4.0-cp315-cp315t-win_arm64.whl", hash = "sha256:5786ef987b3767e51aaa53f20aec104c0252b42ecda7aef8e8b4cbae279b05c5", size = 11842738, upload-time = "2026-10-01T20:40:30.026Z" },
|
|
1279
|
+
{ url = "https://files.pythonhosted.org/packages/81/12/46ae8670c98a3cd0286ca5645c2f918f8f6be65edfed81b916010619f668/mypy-2.4.0-py3-none-any.whl", hash = "sha256:d01c5d26a352acc6d5cf3128225477e1e8465e8d3029d4c345807fbf7f3cf093", size = 2800734, upload-time = "2026-10-01T20:39:26.837Z" },
|
|
1280
1280
|
]
|
|
1281
1281
|
|
|
1282
1282
|
[[package]]
|
|
@@ -1977,27 +1977,27 @@ wheels = [
|
|
|
1977
1977
|
|
|
1978
1978
|
[[package]]
|
|
1979
1979
|
name = "ruff"
|
|
1980
|
-
version = "0.16.
|
|
1981
|
-
source = { registry = "https://pypi.org/simple" }
|
|
1982
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
1983
|
-
wheels = [
|
|
1984
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1985
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1986
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1987
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1988
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1989
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1990
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1991
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1992
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1993
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1994
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1995
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1996
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1997
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1998
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1999
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
2000
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1980
|
+
version = "0.16.10"
|
|
1981
|
+
source = { registry = "https://pypi.org/simple" }
|
|
1982
|
+
sdist = { url = "https://files.pythonhosted.org/packages/c4/49/23802c45f093eb14bde54b141d2b2f058edfa63a06db7beed047308cc08f/ruff-0.16.10.tar.gz", hash = "sha256:eff4728c4eaae93f0955cd264d24b2ab348e74bf59986ccf282ba6dc16b3b017", size = 4958724, upload-time = "2026-10-01T18:03:21.697Z" }
|
|
1983
|
+
wheels = [
|
|
1984
|
+
{ url = "https://files.pythonhosted.org/packages/2f/21/ebce22e1d90cdb2cd691026b9c6e9bec6499f0b396089481755b5d49efff/ruff-0.16.10-py3-none-linux_armv6l.whl", hash = "sha256:488b0fe3f3574210e5cf80d9f59b9e3ab17a127a8155de3f307b392589cfb511", size = 10095557, upload-time = "2026-10-01T18:02:36.072Z" },
|
|
1985
|
+
{ url = "https://files.pythonhosted.org/packages/cb/98/a54de85876a8b2612bfa0d84c7b9abfb39c6a3354aee7800b09c1649b9e3/ruff-0.16.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e748ff95c934c4e978783b8e687bc174e7bd84e8ad24e3243e1ecfcda5e0282d", size = 10340577, upload-time = "2026-10-01T18:02:39.258Z" },
|
|
1986
|
+
{ url = "https://files.pythonhosted.org/packages/9f/16/1a5a4a2657effe4806110f2b907313802f1367fcdbb29e8122766e407fab/ruff-0.16.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3031a4a2e8e7b8a46f70be45f198c35a11ece509a94b80334d8d397a33c67550", size = 9774282, upload-time = "2026-10-01T18:02:41.79Z" },
|
|
1987
|
+
{ url = "https://files.pythonhosted.org/packages/6e/fb/470085af734da396e68cd80fb0a3e7c109a459716ae59588c0f6fab8a17d/ruff-0.16.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:494401c86df4c4c25f69b9419605d944467ee98c42fb6ad405ef4fa40b8fb67d", size = 9920895, upload-time = "2026-10-01T18:02:44.458Z" },
|
|
1988
|
+
{ url = "https://files.pythonhosted.org/packages/57/de/f10cffe4f88a37ea6615ec460f01bf76f0bc1477737a35d9ee61a630a78b/ruff-0.16.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d203abc0ff2b773ee33d00ab8df0bb67046fbc7c07b119332f08b9b345cf8221", size = 9902707, upload-time = "2026-10-01T18:02:47.041Z" },
|
|
1989
|
+
{ url = "https://files.pythonhosted.org/packages/ff/44/3fdcedf83ae60ef837dd239170e480dce606a9142cfa2db911afdf855fd9/ruff-0.16.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd83d1235a5258d318477bc5b576303974cbdef5df0c01a1bff14efcc23bd12a", size = 10619287, upload-time = "2026-10-01T18:02:49.485Z" },
|
|
1990
|
+
{ url = "https://files.pythonhosted.org/packages/c1/62/02e76a5574002153618eafbb70e159468addc72a5d2c488e65cbb4639d2d/ruff-0.16.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc2610fb269fa56dd8a68669ae470fa6272902668c0fc2ebc3aa112b2633d5b8", size = 11339942, upload-time = "2026-10-01T18:02:52.008Z" },
|
|
1991
|
+
{ url = "https://files.pythonhosted.org/packages/6b/c4/cde27d47ad8d4126c587e608c67c46e7feac11763f606d261a1bc8a489e6/ruff-0.16.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e70175e29cc94c26ea296c80e470180b744b7419026898e58f520c6ab32578e", size = 10934316, upload-time = "2026-10-01T18:02:54.811Z" },
|
|
1992
|
+
{ url = "https://files.pythonhosted.org/packages/e4/03/17234145f302a645a123e8c3bb2411ecf4669fbc350de3b0d803230a1729/ruff-0.16.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f33f43a864a8483eebd160e713336c8bab02c934feaff0a33cf5ccb41546d09a", size = 10387968, upload-time = "2026-10-01T18:02:57.497Z" },
|
|
1993
|
+
{ url = "https://files.pythonhosted.org/packages/71/29/2493af60240ee7644b38a4b821f5f9c3b5a4fa3178770fe1d0c685217395/ruff-0.16.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1dfc6f0088149fb6a362c1c446bcbb3fd2157b3852fe2fa68409276eab9ad9b3", size = 10537906, upload-time = "2026-10-01T18:03:00.006Z" },
|
|
1994
|
+
{ url = "https://files.pythonhosted.org/packages/2e/9a/f56b28f3b143bb9e518c34fb89ab191b626bca8b70dbf8af0d2e2d473572/ruff-0.16.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6553498afc35f580f036030795810b9e6bcea31604b0fd9e8d352795473042e3", size = 10012938, upload-time = "2026-10-01T18:03:03.006Z" },
|
|
1995
|
+
{ url = "https://files.pythonhosted.org/packages/c2/c4/fca37362848ea4d4d80712df13632e645e7c7cfb1bdedf140699ac7a090b/ruff-0.16.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a3b8471dea115d37f123882be852bed13403746d3a76c11de4a19ec5f9ff5a03", size = 9897945, upload-time = "2026-10-01T18:03:05.818Z" },
|
|
1996
|
+
{ url = "https://files.pythonhosted.org/packages/7d/c7/e0bc57664d6e0c61fd22f620af260fe9662d7e1ddb320ea7f160e76ef165/ruff-0.16.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:92e59a70bcbd9d3a5483656da906ec28edfdacfce00afd99edb8b4e9d15644be", size = 10333134, upload-time = "2026-10-01T18:03:08.25Z" },
|
|
1997
|
+
{ url = "https://files.pythonhosted.org/packages/21/aa/5c9f3b68737c0e4a33a1db5dfab44f7b786d96ca91a7233112ddab1e6dc2/ruff-0.16.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7ae7375f803b5520dc9f546bed7e3a0acb70b91812e9bb4b19927de22f25b77d", size = 10741702, upload-time = "2026-10-01T18:03:10.775Z" },
|
|
1998
|
+
{ url = "https://files.pythonhosted.org/packages/78/fa/0f9c2020dc316c53d983be011720b8157cc052b97e3f4dfa7db6d0f880a6/ruff-0.16.10-py3-none-win32.whl", hash = "sha256:2a12e01cb9156c10c466f63b46eaae5ecea28dfbd21b5836353ae498e7d1349a", size = 10139176, upload-time = "2026-10-01T18:03:13.267Z" },
|
|
1999
|
+
{ url = "https://files.pythonhosted.org/packages/99/29/cfb0df9448d4d4ad48c2de029ada9ebd71baa6da983a6c77ee6c6cd0fe82/ruff-0.16.10-py3-none-win_amd64.whl", hash = "sha256:97f2015c92aa97105b0eab19eb5d224884399281cfc5da86a92db4ab5e7fb2ca", size = 10584734, upload-time = "2026-10-01T18:03:16.006Z" },
|
|
2000
|
+
{ url = "https://files.pythonhosted.org/packages/fc/05/c16957eb287c3fc062e032619a25868d93d408a844b725bcf514f7a378ff/ruff-0.16.10-py3-none-win_arm64.whl", hash = "sha256:25a65fe998c4e6861ec079ada5826a2fc605e6cbccbe9dcd7fac1f54e791621b", size = 10366440, upload-time = "2026-10-01T18:03:19.04Z" },
|
|
2001
2001
|
]
|
|
2002
2002
|
|
|
2003
2003
|
[[package]]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/_notification_bootstrap.py
RENAMED
|
File without changes
|
{librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/_notification_codec.py
RENAMED
|
File without changes
|
|
File without changes
|
{librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/_windows_filesystem.py
RENAMED
|
File without changes
|
|
File without changes
|
{librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/attachment_routes.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/attendance_frequency.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/completed_lessons.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.2.1 → librus_python_api-1.3.0}/src/librus_python_api/message_content.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/modern_messages.py
RENAMED
|
File without changes
|
{librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/notification_models.py
RENAMED
|
File without changes
|
|
File without changes
|
{librus_python_api-1.2.1 → librus_python_api-1.3.0}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|