librus-python-api 1.8.2__py3-none-any.whl → 1.8.3__py3-none-any.whl
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/__init__.py +1 -1
- librus_python_api/attendance.py +13 -1
- librus_python_api/config.py +18 -1
- librus_python_api/gateway_grades.py +45 -12
- librus_python_api/grade_records.py +65 -1
- librus_python_api/inspect_page.py +5 -2
- librus_python_api/markup.py +17 -8
- librus_python_api/service.py +14 -10
- {librus_python_api-1.8.2.dist-info → librus_python_api-1.8.3.dist-info}/METADATA +6 -4
- {librus_python_api-1.8.2.dist-info → librus_python_api-1.8.3.dist-info}/RECORD +12 -12
- {librus_python_api-1.8.2.dist-info → librus_python_api-1.8.3.dist-info}/WHEEL +1 -1
- {librus_python_api-1.8.2.dist-info → librus_python_api-1.8.3.dist-info}/licenses/LICENSE +0 -0
librus_python_api/__init__.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Independent bounded async client. General live compatibility is unqualified."""
|
|
2
2
|
|
|
3
3
|
# Before the imports: config reads it for the User-Agent.
|
|
4
|
-
__version__ = "1.8.
|
|
4
|
+
__version__ = "1.8.3"
|
|
5
5
|
|
|
6
6
|
from librus_python_api.attachments import AttachmentStream, ModernAttachmentStream
|
|
7
7
|
from librus_python_api.budget import RequestBudget
|
librus_python_api/attendance.py
CHANGED
|
@@ -11,6 +11,7 @@ from librus_python_api.config import (
|
|
|
11
11
|
ATTENDANCE_EMPTY_MARKERS,
|
|
12
12
|
ATTENDANCE_MAX_RECORDS,
|
|
13
13
|
ATTENDANCE_SEMESTER_LABELS,
|
|
14
|
+
ATTENDANCE_TOOLTIP_NOTICES,
|
|
14
15
|
GRADE_MAX_METADATA_LENGTH,
|
|
15
16
|
GRADE_MAX_VALUE_LENGTH,
|
|
16
17
|
)
|
|
@@ -29,6 +30,17 @@ def _entry(element: html.HtmlElement, semester: Literal[1, 2]) -> AttendanceReco
|
|
|
29
30
|
title = element.get("title", "")
|
|
30
31
|
if len(title) > GRADE_MAX_METADATA_LENGTH:
|
|
31
32
|
raise LibrusError(ErrorKind.LIMIT)
|
|
33
|
+
# A known Librus notice is a whole line of its own; other unlabelled lines
|
|
34
|
+
# still fail in tooltip_fields.
|
|
35
|
+
notices: list[tuple[str, str]] = []
|
|
36
|
+
lines = []
|
|
37
|
+
for line in re.split(r"<br\s*/?>|\n", title, flags=re.I):
|
|
38
|
+
notice = ATTENDANCE_TOOLTIP_NOTICES.get(markup.tooltip_line(line))
|
|
39
|
+
if notice is None or notice in notices:
|
|
40
|
+
lines.append(line)
|
|
41
|
+
else:
|
|
42
|
+
notices.append(notice)
|
|
43
|
+
title = "<br>".join(lines)
|
|
32
44
|
holder = html.Element("span")
|
|
33
45
|
holder.set("title", re.sub(r"</b>\s*(?=<b[ >])", "</b>\n", title, flags=re.I))
|
|
34
46
|
fields = markup.tooltip_fields(holder)
|
|
@@ -57,7 +69,7 @@ def _entry(element: html.HtmlElement, semester: Literal[1, 2]) -> AttendanceReco
|
|
|
57
69
|
fields.get("Temat zajęć"),
|
|
58
70
|
fields.get("Lekcja"),
|
|
59
71
|
markup.attendance_detail_id(element),
|
|
60
|
-
|
|
72
|
+
(*fields.items(), *notices),
|
|
61
73
|
)
|
|
62
74
|
|
|
63
75
|
|
librus_python_api/config.py
CHANGED
|
@@ -787,6 +787,7 @@ GRADE_PERIOD_HEADERS = MappingProxyType(
|
|
|
787
787
|
# semester cells and its script loads the grades from the gateway routes above.
|
|
788
788
|
DESCRIPTIVE_TABLE_CLASS = "newSchemaGradesByStudentTable"
|
|
789
789
|
DESCRIPTIVE_ROW_CLASS = "studentRow"
|
|
790
|
+
DESCRIPTIVE_DETAILS_ROW_CLASS = "studentGradesDetailsRow"
|
|
790
791
|
# Gateway account and student identifiers are opaque strings, unlike every
|
|
791
792
|
# other `{id}` route, which takes a numeric ID.
|
|
792
793
|
DESCRIPTIVE_IDENTIFIER = re.compile(r"[A-Za-z0-9_-]{1,128}")
|
|
@@ -838,6 +839,17 @@ ATTENDANCE_SEMESTER_LABELS = MappingProxyType(
|
|
|
838
839
|
}
|
|
839
840
|
)
|
|
840
841
|
ATTENDANCE_EMPTY_MARKERS = frozenset({"", "-", "Brak nieobecności"})
|
|
842
|
+
# Fixed unlabelled tooltip lines Librus appends to an attendance entry, and the
|
|
843
|
+
# metadata pair each becomes. Observed live on 2026-11-06 on two logins: an
|
|
844
|
+
# absence excused through the e-Usprawiedliwienia module.
|
|
845
|
+
ATTENDANCE_TOOLTIP_NOTICES = MappingProxyType(
|
|
846
|
+
{
|
|
847
|
+
"Usprawiedliwienie dodane za pomocą modułu e-Usprawiedliwienia": (
|
|
848
|
+
"Usprawiedliwienie",
|
|
849
|
+
"dodane za pomocą modułu e-Usprawiedliwienia",
|
|
850
|
+
)
|
|
851
|
+
}
|
|
852
|
+
)
|
|
841
853
|
ATTENDANCE_MAX_RECORDS = 2048
|
|
842
854
|
ATTENDANCE_MAX_WINDOW_DAYS = 371
|
|
843
855
|
# Whole-school gateway collections resolve attendance metadata in two requests.
|
|
@@ -849,6 +861,8 @@ ATTENDANCE_DETAIL_PATH_PREFIX = "/przegladaj_nb/szczegoly/"
|
|
|
849
861
|
FORMATIVE_DETAIL_PATH_PREFIX = "/przegladaj_oceny/szczegoly/ksztaltujace/"
|
|
850
862
|
# Recognition only: behaviour-point detail links are classified, never requested.
|
|
851
863
|
BEHAVIOUR_POINTS_DETAIL_PATH_PREFIX = "/przegladaj_oceny/zachowanie_szczegoly/"
|
|
864
|
+
# Tooltip labels of a behaviour grade symbol entry (#114, synthetic example).
|
|
865
|
+
BEHAVIOUR_GRADE_LABELS = frozenset({"Data wystawienia", "Dodał", "Komentarz"})
|
|
852
866
|
# Recognition only: point-grade detail links are classified, never requested.
|
|
853
867
|
POINT_GRADE_DETAIL_PATH_PREFIX = "/przegladaj_oceny_punktowe/szczegoly/"
|
|
854
868
|
FORMATIVE_HEADERS = (
|
|
@@ -916,7 +930,10 @@ ANNOUNCEMENT_LABELS = MappingProxyType(
|
|
|
916
930
|
ANNOUNCEMENT_EMPTY_CLASSES = frozenset(
|
|
917
931
|
{"container", "border-red", "resizeable", "center"}
|
|
918
932
|
)
|
|
919
|
-
|
|
933
|
+
# "Aktualnie brak ogłoszeń" reported in #120 on two accounts.
|
|
934
|
+
ANNOUNCEMENT_EMPTY_MARKERS = frozenset(
|
|
935
|
+
{"brak ogłoszeń", "nie ma ogłoszeń", "aktualnie brak ogłoszeń"}
|
|
936
|
+
)
|
|
920
937
|
TIMETABLE_MAX_PERIODS = 32
|
|
921
938
|
TIMETABLE_MAX_LESSONS_PER_SLOT = 16
|
|
922
939
|
TIMETABLE_MAX_CHANGES_PER_SLOT = 16
|
|
@@ -6,6 +6,11 @@ grades from gateway JSON routes. Routes and field names come from public client
|
|
|
6
6
|
code and the official page script. One authorized read on 2026-10-28 parsed four
|
|
7
7
|
PartialGrades records on one school; pagination keys are still unknown, so every
|
|
8
8
|
unexpected shape fails instead of producing a partial collection.
|
|
9
|
+
|
|
10
|
+
Another school renders the same table with plain text in its semester cells and
|
|
11
|
+
no script hooks (#115, from a reporter's synthetic example). The grade record
|
|
12
|
+
parser reads those cells like any descriptive row, so that table needs no
|
|
13
|
+
gateway read.
|
|
9
14
|
"""
|
|
10
15
|
|
|
11
16
|
import re
|
|
@@ -20,6 +25,7 @@ from pydantic import BaseModel, ConfigDict, Field, ValidationError, field_valida
|
|
|
20
25
|
|
|
21
26
|
from librus_python_api import markup
|
|
22
27
|
from librus_python_api.config import (
|
|
28
|
+
DESCRIPTIVE_DETAILS_ROW_CLASS,
|
|
23
29
|
DESCRIPTIVE_IDENTIFIER,
|
|
24
30
|
DESCRIPTIVE_MAX_NAME_LENGTH,
|
|
25
31
|
DESCRIPTIVE_MAX_SUBJECTS,
|
|
@@ -331,9 +337,13 @@ def parse_gateway_users(body: bytes) -> Mapping[str, str]:
|
|
|
331
337
|
|
|
332
338
|
@dataclass(frozen=True, slots=True)
|
|
333
339
|
class DescriptiveTable:
|
|
334
|
-
"""Numeric subject ID to the subject name the grades page shows.
|
|
340
|
+
"""Numeric subject ID to the subject name the grades page shows.
|
|
341
|
+
|
|
342
|
+
A filled table already holds its grades and has no gateway grades.
|
|
343
|
+
"""
|
|
335
344
|
|
|
336
345
|
subjects: Mapping[int, str] = field(repr=False)
|
|
346
|
+
filled: bool = False
|
|
337
347
|
|
|
338
348
|
|
|
339
349
|
def _classes(element: html.HtmlElement) -> set[str]:
|
|
@@ -343,9 +353,11 @@ def _classes(element: html.HtmlElement) -> set[str]:
|
|
|
343
353
|
def read_descriptive_table(document: html.HtmlElement) -> DescriptiveTable | None:
|
|
344
354
|
"""The new-schema subject table, or None when the page has none.
|
|
345
355
|
|
|
346
|
-
Its rows hold an expander, the subject and two
|
|
347
|
-
|
|
348
|
-
|
|
356
|
+
Its rows hold an expander, the subject and two semester cells. Either
|
|
357
|
+
every row has empty `gradesCell` cells that the page script fills from
|
|
358
|
+
the gateway, or every row has plain cells the page filled itself, which
|
|
359
|
+
`read_grade_records` reads and validates. A script cell the page already
|
|
360
|
+
filled would duplicate the gateway records, so it is unsupported.
|
|
349
361
|
"""
|
|
350
362
|
tables = [
|
|
351
363
|
table
|
|
@@ -369,20 +381,33 @@ def read_descriptive_table(document: html.HtmlElement) -> DescriptiveTable | Non
|
|
|
369
381
|
tables = populated
|
|
370
382
|
subjects: dict[int, str] = {}
|
|
371
383
|
students: set[str | None] = set()
|
|
384
|
+
layouts: set[bool] = set()
|
|
372
385
|
for row in markup.rows(tables[0]):
|
|
373
386
|
if DESCRIPTIVE_ROW_CLASS not in _classes(row):
|
|
374
387
|
continue
|
|
375
388
|
cells = markup.cells(row)
|
|
376
|
-
if (
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
389
|
+
if len(cells) != 4 or not {"micro", "center", "screen-only"} <= _classes(
|
|
390
|
+
cells[0]
|
|
391
|
+
):
|
|
392
|
+
raise LibrusError(ErrorKind.UNSUPPORTED_CAPABILITY)
|
|
393
|
+
semesters = cells[2:]
|
|
394
|
+
# A filled cell is bare text: no attributes and no markup, so a renamed
|
|
395
|
+
# or moved script hook never passes as a filled table.
|
|
396
|
+
filled = all(
|
|
397
|
+
not cell.attrib
|
|
398
|
+
and markup.text(cell)
|
|
399
|
+
and not any(isinstance(node.tag, str) for node in cell)
|
|
400
|
+
for cell in semesters
|
|
401
|
+
)
|
|
402
|
+
if not filled and (
|
|
403
|
+
[cell.get("data-semester") for cell in semesters] != ["1", "2"]
|
|
404
|
+
or not all("gradesCell" in _classes(cell) for cell in semesters)
|
|
381
405
|
):
|
|
382
406
|
raise LibrusError(ErrorKind.UNSUPPORTED_CAPABILITY)
|
|
383
|
-
|
|
407
|
+
layouts.add(filled)
|
|
408
|
+
if not filled and any(
|
|
384
409
|
markup.text(cell) or any(isinstance(node.tag, str) for node in cell)
|
|
385
|
-
for cell in
|
|
410
|
+
for cell in semesters
|
|
386
411
|
):
|
|
387
412
|
raise LibrusError(ErrorKind.UNSUPPORTED_CAPABILITY)
|
|
388
413
|
identifier = row.get("data-subject_id", "")
|
|
@@ -398,7 +423,15 @@ def read_descriptive_table(document: html.HtmlElement) -> DescriptiveTable | Non
|
|
|
398
423
|
if len(students) > 1:
|
|
399
424
|
# One page shows one student; rows of several students are unknown.
|
|
400
425
|
raise LibrusError(ErrorKind.PARSE)
|
|
401
|
-
|
|
426
|
+
if len(layouts) > 1:
|
|
427
|
+
raise LibrusError(ErrorKind.UNSUPPORTED_CAPABILITY)
|
|
428
|
+
if layouts == {True} and any(
|
|
429
|
+
DESCRIPTIVE_DETAILS_ROW_CLASS in _classes(row) for row in markup.rows(tables[0])
|
|
430
|
+
):
|
|
431
|
+
# Detail rows are what the page script fills from the gateway; a filled
|
|
432
|
+
# table that still has them may also have gateway grades.
|
|
433
|
+
raise LibrusError(ErrorKind.UNSUPPORTED_CAPABILITY)
|
|
434
|
+
return DescriptiveTable(MappingProxyType(subjects), layouts == {True})
|
|
402
435
|
|
|
403
436
|
|
|
404
437
|
def parse_grade_page(body: bytes) -> tuple[GradeRecords, DescriptiveTable | None]:
|
|
@@ -8,6 +8,7 @@ from lxml import html
|
|
|
8
8
|
|
|
9
9
|
from librus_python_api import markup
|
|
10
10
|
from librus_python_api.config import (
|
|
11
|
+
BEHAVIOUR_GRADE_LABELS,
|
|
11
12
|
BEHAVIOUR_POINTS_DETAIL_PATH_PREFIX,
|
|
12
13
|
FORMATIVE_DETAIL_PATH_PREFIX,
|
|
13
14
|
GRADE_AVERAGE_HEADERS,
|
|
@@ -26,6 +27,7 @@ from librus_python_api.config import (
|
|
|
26
27
|
GRADE_PUBLICATION_DATE_LABEL,
|
|
27
28
|
GRADE_PUBLICATION_PERIOD_LABELS,
|
|
28
29
|
GRADE_PUBLICATION_TEACHER_LABEL,
|
|
30
|
+
TOOLTIP_LABELS,
|
|
29
31
|
)
|
|
30
32
|
from librus_python_api.exceptions import ErrorKind, LibrusError
|
|
31
33
|
from librus_python_api.formative_grades import is_formative_table, read_formative_table
|
|
@@ -126,6 +128,51 @@ def _numeric(
|
|
|
126
128
|
)
|
|
127
129
|
|
|
128
130
|
|
|
131
|
+
def _behaviour_grade(
|
|
132
|
+
element: html.HtmlElement,
|
|
133
|
+
subject: str,
|
|
134
|
+
semester: Literal[0, 1, 2],
|
|
135
|
+
kind: GradeKind = GradeKind.CURRENT,
|
|
136
|
+
) -> NumericGrade:
|
|
137
|
+
"""A behaviour entry with a grade symbol, such as `wz`, instead of points.
|
|
138
|
+
|
|
139
|
+
Its tooltip has the point entries' layout: an unlabelled first line, then
|
|
140
|
+
`Data wystawienia` and `Dodał`. The first line names the entry, as the
|
|
141
|
+
point category does, so it is the category. `Dodał` stays in metadata. A
|
|
142
|
+
label with digits, a point entry's range labels or any other label fails.
|
|
143
|
+
"""
|
|
144
|
+
heading: list[str] = []
|
|
145
|
+
metadata = markup.tooltip_fields(element, heading)
|
|
146
|
+
raw = markup.text(element)
|
|
147
|
+
if (
|
|
148
|
+
not heading
|
|
149
|
+
# A labelled first line means the heading is missing.
|
|
150
|
+
or heading[0].partition(":")[0].strip() in TOOLTIP_LABELS
|
|
151
|
+
# Symbols carry no digits; points or a range are a point entry.
|
|
152
|
+
or not raw
|
|
153
|
+
or re.search(r"[0-9]", raw)
|
|
154
|
+
or not BEHAVIOUR_GRADE_LABELS >= metadata.keys()
|
|
155
|
+
):
|
|
156
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
157
|
+
href = element.get("href")
|
|
158
|
+
if href is not None and len(href) > GRADE_MAX_METADATA_LENGTH:
|
|
159
|
+
raise LibrusError(ErrorKind.LIMIT)
|
|
160
|
+
return NumericGrade(
|
|
161
|
+
subject,
|
|
162
|
+
raw,
|
|
163
|
+
markup.civil_date(metadata.get("Data wystawienia")),
|
|
164
|
+
semester,
|
|
165
|
+
None,
|
|
166
|
+
None,
|
|
167
|
+
heading[0],
|
|
168
|
+
None,
|
|
169
|
+
metadata.get("Komentarz"),
|
|
170
|
+
href,
|
|
171
|
+
tuple(metadata.items()),
|
|
172
|
+
kind,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
129
176
|
def _descriptive(
|
|
130
177
|
box: html.HtmlElement,
|
|
131
178
|
element: html.HtmlElement,
|
|
@@ -284,7 +331,21 @@ def _entries(
|
|
|
284
331
|
if len(anchors) + len(descriptions) != 1:
|
|
285
332
|
raise LibrusError(ErrorKind.PARSE)
|
|
286
333
|
if anchors:
|
|
287
|
-
|
|
334
|
+
behaviour = (
|
|
335
|
+
subject in GRADE_MERGED_SUBJECTS
|
|
336
|
+
and markup.detail_id(
|
|
337
|
+
anchors[0].get("href"), BEHAVIOUR_POINTS_DETAIL_PATH_PREFIX
|
|
338
|
+
)
|
|
339
|
+
is not None
|
|
340
|
+
)
|
|
341
|
+
if behaviour and descriptive:
|
|
342
|
+
# A descriptive record has no category for the first line.
|
|
343
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
344
|
+
record = (
|
|
345
|
+
_behaviour_grade(anchors[0], subject, period, kind)
|
|
346
|
+
if behaviour
|
|
347
|
+
else _numeric(anchors[0], subject, period, kind)
|
|
348
|
+
)
|
|
288
349
|
if descriptive:
|
|
289
350
|
href = record.href
|
|
290
351
|
if href is not None and re.sub(
|
|
@@ -540,6 +601,9 @@ def read_grade_records(document: html.HtmlElement) -> GradeRecords:
|
|
|
540
601
|
_read_numeric_table(table, collection, points=True)
|
|
541
602
|
else:
|
|
542
603
|
for row in markup.rows(table):
|
|
604
|
+
if markup.in_header(row):
|
|
605
|
+
# The new-schema table's column labels, not a subject.
|
|
606
|
+
continue
|
|
543
607
|
cells = markup.cells(row)
|
|
544
608
|
if _descriptive_row(cells):
|
|
545
609
|
_read_descriptive_row(cells, collection)
|
|
@@ -19,8 +19,8 @@ from librus_python_api import __version__
|
|
|
19
19
|
from librus_python_api.config import TransportLimits
|
|
20
20
|
from librus_python_api.diagnostics import describe_failure, failure_frames
|
|
21
21
|
from librus_python_api.exceptions import LibrusError
|
|
22
|
+
from librus_python_api.gateway_grades import parse_grade_page
|
|
22
23
|
from librus_python_api.grade_parsers import parse_final_grades
|
|
23
|
-
from librus_python_api.grade_records import parse_grade_records
|
|
24
24
|
from librus_python_api.models import Availability
|
|
25
25
|
from librus_python_api.parsers import parse_profile
|
|
26
26
|
|
|
@@ -31,7 +31,8 @@ NOTICE = (
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
def _grades(body: bytes) -> dict[str, int]:
|
|
34
|
-
|
|
34
|
+
# The all view also checks the new-schema table, as `grades()` does.
|
|
35
|
+
records, table = parse_grade_page(body)
|
|
35
36
|
return {
|
|
36
37
|
"numeric": sum(1 for g in records.numeric if g.point_grade_id is None),
|
|
37
38
|
"point": sum(1 for g in records.numeric if g.point_grade_id is not None),
|
|
@@ -41,6 +42,8 @@ def _grades(body: bytes) -> dict[str, int]:
|
|
|
41
42
|
"averages": len(records.averages),
|
|
42
43
|
"point_results": len(records.point_results),
|
|
43
44
|
"formative": len(records.formative),
|
|
45
|
+
"descriptive_table": int(table is not None),
|
|
46
|
+
"descriptive_table_filled": int(table is not None and table.filled),
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
|
librus_python_api/markup.py
CHANGED
|
@@ -81,11 +81,14 @@ def colspan(cell: html.HtmlElement) -> int:
|
|
|
81
81
|
return span
|
|
82
82
|
|
|
83
83
|
|
|
84
|
-
def tooltip_fields(
|
|
84
|
+
def tooltip_fields(
|
|
85
|
+
element: html.HtmlElement, heading: list[str] | None = None
|
|
86
|
+
) -> dict[str, str]:
|
|
85
87
|
"""Parse a 'Label: value<br>' title attribute into unique labelled fields.
|
|
86
88
|
|
|
87
89
|
A free-text field may span lines; an unlabelled line after it continues
|
|
88
|
-
its value. Any other unlabelled line is an unknown layout.
|
|
90
|
+
its value. Any other unlabelled line is an unknown layout. With `heading`,
|
|
91
|
+
the first line is taken whole, colon included, and appended to it.
|
|
89
92
|
"""
|
|
90
93
|
title = element.get("title", "")
|
|
91
94
|
if len(title) > GRADE_MAX_METADATA_LENGTH:
|
|
@@ -98,12 +101,10 @@ def tooltip_fields(element: html.HtmlElement) -> dict[str, str]:
|
|
|
98
101
|
for chunk in chunks:
|
|
99
102
|
if not chunk.strip():
|
|
100
103
|
continue
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
else " ".join(chunk.split())
|
|
106
|
-
)
|
|
104
|
+
rendered = tooltip_line(chunk)
|
|
105
|
+
if heading is not None and not heading:
|
|
106
|
+
heading.append(rendered)
|
|
107
|
+
continue
|
|
107
108
|
key, separator, value = rendered.partition(":")
|
|
108
109
|
key, value = key.strip(), value.strip()
|
|
109
110
|
if not separator and last in TOOLTIP_MULTILINE_LABELS:
|
|
@@ -116,6 +117,14 @@ def tooltip_fields(element: html.HtmlElement) -> dict[str, str]:
|
|
|
116
117
|
return result
|
|
117
118
|
|
|
118
119
|
|
|
120
|
+
def tooltip_line(chunk: str) -> str:
|
|
121
|
+
"""One tooltip line's text, whitespace normalized."""
|
|
122
|
+
# Tooltips are HTML inside an attribute; parse markup only where present.
|
|
123
|
+
if "<" in chunk:
|
|
124
|
+
return text(parse_html_document(chunk.encode()))
|
|
125
|
+
return " ".join(chunk.split())
|
|
126
|
+
|
|
127
|
+
|
|
119
128
|
def civil_date(value: str | None) -> date:
|
|
120
129
|
"""An ISO date, optionally followed by its weekday label in parentheses."""
|
|
121
130
|
match = re.fullmatch(r"([0-9]{4}-[0-9]{2}-[0-9]{2})(?: \(([^()]+)\))?", value or "")
|
librus_python_api/service.py
CHANGED
|
@@ -394,6 +394,18 @@ def _require_dates(*days: date | None, max_days: int | None = None) -> None:
|
|
|
394
394
|
raise LibrusError(ErrorKind.INVALID_INPUT)
|
|
395
395
|
|
|
396
396
|
|
|
397
|
+
def _check_record_count(records: GradeRecords, added: int) -> None:
|
|
398
|
+
if (
|
|
399
|
+
len(records.numeric)
|
|
400
|
+
+ len(records.descriptive)
|
|
401
|
+
+ len(records.descriptive_summaries)
|
|
402
|
+
+ len(records.formative)
|
|
403
|
+
+ added
|
|
404
|
+
> GRADE_MAX_RECORDS
|
|
405
|
+
):
|
|
406
|
+
raise LibrusError(ErrorKind.LIMIT)
|
|
407
|
+
|
|
408
|
+
|
|
397
409
|
def _cooldown_due(error: LibrusError) -> bool:
|
|
398
410
|
"""A denial, an unrecovered expiry, or a logged-out page that a session
|
|
399
411
|
just signed in also got: signing in again at once cannot help."""
|
|
@@ -1328,7 +1340,7 @@ class AccountClient:
|
|
|
1328
1340
|
records, table = await self._page(
|
|
1329
1341
|
"grades", budget, parse_grade_page, form=form
|
|
1330
1342
|
)
|
|
1331
|
-
if table is not None:
|
|
1343
|
+
if table is not None and not table.filled:
|
|
1332
1344
|
records = await self._gateway_descriptive(records, table, budget)
|
|
1333
1345
|
return Grades(
|
|
1334
1346
|
self._session_identity(), records, self._observation("grades"), view
|
|
@@ -2508,15 +2520,7 @@ class AccountClient:
|
|
|
2508
2520
|
"descriptive_users", budget, parse_gateway_users, content_type=JSON
|
|
2509
2521
|
)
|
|
2510
2522
|
added = descriptive_grades(items, subjects, users, table)
|
|
2511
|
-
|
|
2512
|
-
len(records.numeric)
|
|
2513
|
-
+ len(records.descriptive)
|
|
2514
|
-
+ len(records.descriptive_summaries)
|
|
2515
|
-
+ len(records.formative)
|
|
2516
|
-
+ len(added)
|
|
2517
|
-
> GRADE_MAX_RECORDS
|
|
2518
|
-
):
|
|
2519
|
-
raise LibrusError(ErrorKind.LIMIT)
|
|
2523
|
+
_check_record_count(records, len(added))
|
|
2520
2524
|
return replace(records, descriptive=records.descriptive + added)
|
|
2521
2525
|
|
|
2522
2526
|
async def _metadata_value(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: librus-python-api
|
|
3
|
-
Version: 1.8.
|
|
3
|
+
Version: 1.8.3
|
|
4
4
|
Summary: Independent bounded async Librus Synergia client
|
|
5
5
|
Project-URL: Repository, https://github.com/krzysztofbury/librus-python-api
|
|
6
6
|
Project-URL: Issues, https://github.com/krzysztofbury/librus-python-api/issues
|
|
@@ -42,7 +42,9 @@ private ACLs. Windows installations include `pywin32` and `tzdata` automatically
|
|
|
42
42
|
Network paths, reparse-point paths and unsafe storage permissions fail closed.
|
|
43
43
|
See [platform requirements](contracts/persistence.md#windows-disk-boundary-25).
|
|
44
44
|
|
|
45
|
-
**Status:** `1.8.
|
|
45
|
+
**Status:** `1.8.3` reads behaviour grade symbols, filled descriptive
|
|
46
|
+
grade tables, the "Aktualnie brak ogłoszeń" page and absences excused
|
|
47
|
+
through e-Usprawiedliwienia. `1.8.2` keeps the modern mailbox session across a cancelled read
|
|
46
48
|
or a reached limit and sends its own version in the `User-Agent`. `1.8.1` recovers a session Synergia no longer knows (it answers
|
|
47
49
|
with a "Brak dostępu" page, not a login redirect), reads the lucky number
|
|
48
50
|
from the page header and summarizes a page that fails as a whole in
|
|
@@ -64,7 +66,7 @@ See [limitations](#supported-features-and-limitations) below.
|
|
|
64
66
|
Install the exact release from PyPI after its gated publication completes:
|
|
65
67
|
|
|
66
68
|
```sh
|
|
67
|
-
python -m pip install librus-python-api==1.8.
|
|
69
|
+
python -m pip install librus-python-api==1.8.3
|
|
68
70
|
```
|
|
69
71
|
|
|
70
72
|
Pin the exact version qualified by your application. Before publication, use a
|
|
@@ -80,7 +82,7 @@ python -m pip install .
|
|
|
80
82
|
To install a locally built wheel instead:
|
|
81
83
|
|
|
82
84
|
```sh
|
|
83
|
-
python -m pip install ./dist/librus_python_api-1.8.
|
|
85
|
+
python -m pip install ./dist/librus_python_api-1.8.3-py3-none-any.whl
|
|
84
86
|
```
|
|
85
87
|
|
|
86
88
|
No console script or background process is installed: import the library in your
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
librus_python_api/__init__.py,sha256=
|
|
1
|
+
librus_python_api/__init__.py,sha256=Ts6HZg9LRMp-MLMbjajwEdNlxVsmW8TnnGKbnzytP1k,6571
|
|
2
2
|
librus_python_api/_notification_bootstrap.py,sha256=18nzH0doXBNaZXCz-_ckIvRrlHTWV5Mm1t5m2-5eYGI,4669
|
|
3
3
|
librus_python_api/_notification_codec.py,sha256=hU6G3zGJ5fR8wfPC6fV2OqBKs-7XpT9eRnKO3FGsTzY,13451
|
|
4
4
|
librus_python_api/_storage.py,sha256=M_6ZAzN4zs7laOdKJBRH1I2rvy3pgqYgC_MZtsd2xtM,15893
|
|
@@ -6,25 +6,25 @@ librus_python_api/_windows_filesystem.py,sha256=pQANIGkYrb4Q75gz5mdr5mqYhM_PFblW
|
|
|
6
6
|
librus_python_api/announcements.py,sha256=13FoOR1sq7tt9w6aLf0vclcFKDqLUiIYaO5iYD2QVLA,5928
|
|
7
7
|
librus_python_api/attachment_routes.py,sha256=uSg5wCQqHJiphF0KdI0GatQxZr5SLUx6xWQRsl_U4_U,4123
|
|
8
8
|
librus_python_api/attachments.py,sha256=Iq-ERZWc3gYXu6Lcv6womT2EXCc-c1Q7r_Q6VyaCm2E,10818
|
|
9
|
-
librus_python_api/attendance.py,sha256=
|
|
9
|
+
librus_python_api/attendance.py,sha256=Dq2IC8WERO-9x-LZIN5Nl0PTLYmL1Iq7BZtOz6nssOI,7428
|
|
10
10
|
librus_python_api/attendance_frequency.py,sha256=SBhED5zuyRIHWdw4urFyTJYykb6EYB7eL4dU3mif1EY,5309
|
|
11
11
|
librus_python_api/budget.py,sha256=8fvqT7eIxyVX2P2OBG27XsYHk_HbtSS4lzuecYpHtgs,2878
|
|
12
12
|
librus_python_api/checkpoint.py,sha256=QBmG9GtpyaGRaFq-FXuGA3yR1BjuMFN9V3RTvvi_FNQ,1930
|
|
13
13
|
librus_python_api/class_free_days.py,sha256=IwoCgr-9ZgEYYQ6717s4cDpyFzbosHMzESMabXJLGqA,2422
|
|
14
14
|
librus_python_api/completed_lessons.py,sha256=ngGfBD6K4B6dR4elPYvNRvp40Q-GlrhUsUKqBbYY1KE,10727
|
|
15
|
-
librus_python_api/config.py,sha256=
|
|
15
|
+
librus_python_api/config.py,sha256=Sz6b5lWllzsKSbCbfGKqN6z-CZfwbCt9MkD2y-OG9CU,62019
|
|
16
16
|
librus_python_api/detail_fields.py,sha256=vDDi5sV-e30-42SF0hwAUoldiZ5wvYtb1UlLt03lBHw,1708
|
|
17
17
|
librus_python_api/diagnostics.py,sha256=5YbJB8J8RXMqMTmulcn5mJRF1vpi0Pvx00ETrXn9hUY,19036
|
|
18
18
|
librus_python_api/exceptions.py,sha256=x8_z5UTNmuRINTK4buLYiKdYcFA2w6GZP9ZXOL-OnRU,5531
|
|
19
19
|
librus_python_api/files.py,sha256=pQBsIJyfTOZDz83ZB-5fcd-nPMcxR1HJ155h4zTHmsA,9223
|
|
20
20
|
librus_python_api/formative_grades.py,sha256=VykwtGvViZVOJMwPH3Re1RTDCHMr1_m9_xHjZyaXDIQ,6452
|
|
21
|
-
librus_python_api/gateway_grades.py,sha256=
|
|
21
|
+
librus_python_api/gateway_grades.py,sha256=uaCCz743aPlymAa6rDfRb0w2JFC2hJ4yis0hbKdqTeM,17663
|
|
22
22
|
librus_python_api/grade_parsers.py,sha256=ROfffkCP8yj5Q708TZdpp0PDTyVmoh6X7m0y2fyJcwg,11202
|
|
23
|
-
librus_python_api/grade_records.py,sha256=
|
|
23
|
+
librus_python_api/grade_records.py,sha256=IT6A5_09Zu5h03g_p3pHVNepFCMoWag0WIJKg4A9MlQ,24978
|
|
24
24
|
librus_python_api/homework_range.py,sha256=erYfvS6PhHU5XhnR6LkiLNy677hGVflY3UAuHOs-YUw,1663
|
|
25
|
-
librus_python_api/inspect_page.py,sha256=
|
|
25
|
+
librus_python_api/inspect_page.py,sha256=W5M21v0DlFw3eaqzwGG6hsI6fkTZVQTwkO2x-NNk4y0,5490
|
|
26
26
|
librus_python_api/lifecycle.py,sha256=XzA5CEdZFo7iL5IMUrawnRvf2m9BFe0OseP-fpQFaAE,856
|
|
27
|
-
librus_python_api/markup.py,sha256=
|
|
27
|
+
librus_python_api/markup.py,sha256=AS0hfJS83lnOUpyfgoNHwUKf8MKBR3WN3p_4aeZ7BOk,6408
|
|
28
28
|
librus_python_api/message_content.py,sha256=v_qoEAoaYS0_9MQBaj57i5XnC7pzv_HHWefgTt4CNyM,9043
|
|
29
29
|
librus_python_api/messages.py,sha256=K9F8XVs1SojfFpdxxLV8gx9E0g81YxEHjigDzLHmDCg,10230
|
|
30
30
|
librus_python_api/models.py,sha256=IO-SYpqpQeuTej6kqNjBRXDbAbKITRZ1njRUyoEZ8t0,41951
|
|
@@ -44,10 +44,10 @@ librus_python_api/scheduler.py,sha256=3JplYxqvQdFz3aKXcoQUJHE9s3FPw_xRhQ2CPNLuQm
|
|
|
44
44
|
librus_python_api/school_reads.py,sha256=YHta7DmhjS5vSa4Djq8pzp01-2jhVfGOo1Mnc738rV4,15209
|
|
45
45
|
librus_python_api/school_year_archive.py,sha256=D0A1ywMKgoYgvi-d70__SFreMGvMb_72870Akkbb_m8,15492
|
|
46
46
|
librus_python_api/sending.py,sha256=Nvvk2r-6D6Us53a2fH5GW9PcA6bWEfuOvy6c3M5QVcE,4549
|
|
47
|
-
librus_python_api/service.py,sha256=
|
|
47
|
+
librus_python_api/service.py,sha256=GaavbI6117fcbVuWDzZXDbbgQUczZXXFVNKEMXsFfaw,103063
|
|
48
48
|
librus_python_api/timetable.py,sha256=tX1QK9fQABxzfLaHHqTe8e6rAgdOkCoTOfAHdPwNmLE,9836
|
|
49
49
|
librus_python_api/transport.py,sha256=yF-DnFy4xGYS_RWQ9161b9WJXOlYF8cFRgdjXmVVUIk,34988
|
|
50
|
-
librus_python_api-1.8.
|
|
51
|
-
librus_python_api-1.8.
|
|
52
|
-
librus_python_api-1.8.
|
|
53
|
-
librus_python_api-1.8.
|
|
50
|
+
librus_python_api-1.8.3.dist-info/METADATA,sha256=PwGb0FBnKt-moy91gG0veDWgZZzMcEtymgI-BMFU75k,14361
|
|
51
|
+
librus_python_api-1.8.3.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
52
|
+
librus_python_api-1.8.3.dist-info/licenses/LICENSE,sha256=IuerLb3cnZCsaB23znydjjvKqP-LmSJd3syKa9NGUts,1071
|
|
53
|
+
librus_python_api-1.8.3.dist-info/RECORD,,
|
|
File without changes
|