librus-python-api 1.8.3__py3-none-any.whl → 1.8.4__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.
@@ -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.3"
4
+ __version__ = "1.8.4"
5
5
 
6
6
  from librus_python_api.attachments import AttachmentStream, ModernAttachmentStream
7
7
  from librus_python_api.budget import RequestBudget
@@ -721,8 +721,12 @@ GRADE_MAX_VALUE_LENGTH = 1024
721
721
  GRADE_MERGED_SUBJECTS = frozenset({"Zachowanie"})
722
722
  GRADE_INLINE_DETAIL_LABEL = "Ocena"
723
723
  # Tooltip fields whose free text may continue on unlabelled lines. A teacher's
724
- # multi-line grade comment was observed on 2026-10-27.
724
+ # multi-line grade comment was observed on 2026-10-27. A formative mirror's
725
+ # assessment text after `Ocena` also spans lines (2026-11-08, markup.py).
725
726
  TOOLTIP_MULTILINE_LABELS = frozenset({"Komentarz"})
727
+ # Labels a tooltip may repeat. A grade correcting two earlier grades lists one
728
+ # `Poprawa oceny` line per corrected grade in the week view (2026-11-08).
729
+ TOOLTIP_REPEATABLE_LABELS = frozenset({"Poprawa oceny"})
726
730
  # Tooltip labels observed on grade, attendance and point pages. Diagnostics
727
731
  # show only these by name; any other label is masked as teacher-entered text.
728
732
  TOOLTIP_LABELS = frozenset(
@@ -744,6 +748,7 @@ TOOLTIP_LABELS = frozenset(
744
748
  "Nauczyciel",
745
749
  "Obowiązek wyk. zadania",
746
750
  "Ocena",
751
+ "Poprawa oceny",
747
752
  "Rodzaj",
748
753
  "Suma punktów",
749
754
  "Temat zajęć",
@@ -783,8 +788,9 @@ GRADE_PERIOD_HEADERS = MappingProxyType(
783
788
  "Ocena roczna": 0,
784
789
  }
785
790
  )
786
- # New-schema descriptive grading: the page renders this table with empty
787
- # semester cells and its script loads the grades from the gateway routes above.
791
+ # New-schema descriptive grading: the page renders this table either with empty
792
+ # `gradesCell` semester cells, which its script fills from the gateway routes
793
+ # above, or with bare semester text, which needs no gateway read.
788
794
  DESCRIPTIVE_TABLE_CLASS = "newSchemaGradesByStudentTable"
789
795
  DESCRIPTIVE_ROW_CLASS = "studentRow"
790
796
  DESCRIPTIVE_DETAILS_ROW_CLASS = "studentGradesDetailsRow"
@@ -15,6 +15,7 @@ from librus_python_api.config import (
15
15
  GRADE_BODY_PREFIX_COLUMNS,
16
16
  GRADE_CURRENT_HEADER,
17
17
  GRADE_EMPTY_MARKERS,
18
+ GRADE_INLINE_DETAIL_LABEL,
18
19
  GRADE_MAX_METADATA_LENGTH,
19
20
  GRADE_MAX_RECORDS,
20
21
  GRADE_MAX_SUBJECTS,
@@ -28,6 +29,7 @@ from librus_python_api.config import (
28
29
  GRADE_PUBLICATION_PERIOD_LABELS,
29
30
  GRADE_PUBLICATION_TEACHER_LABEL,
30
31
  TOOLTIP_LABELS,
32
+ TOOLTIP_MULTILINE_LABELS,
31
33
  )
32
34
  from librus_python_api.exceptions import ErrorKind, LibrusError
33
35
  from librus_python_api.formative_grades import is_formative_table, read_formative_table
@@ -99,7 +101,16 @@ def _numeric(
99
101
  semester: Literal[0, 1, 2],
100
102
  kind: GradeKind = GradeKind.CURRENT,
101
103
  ) -> NumericGrade:
102
- metadata = markup.tooltip_fields(element)
104
+ if markup.detail_id(element.get("href"), FORMATIVE_DETAIL_PATH_PREFIX) is None:
105
+ metadata = markup.tooltip_fields(element)
106
+ else:
107
+ # A formative mirror's assessment text after `Ocena` is free text that
108
+ # may span lines and hold colons (observed live 2026-11-08).
109
+ metadata = markup.tooltip_fields(
110
+ element,
111
+ multiline=TOOLTIP_MULTILINE_LABELS | {GRADE_INLINE_DETAIL_LABEL},
112
+ unknown_multiline=True,
113
+ )
103
114
  counts = metadata.get("Licz do średniej")
104
115
  if counts is not None and counts.casefold() not in ("tak", "nie"):
105
116
  raise LibrusError(ErrorKind.PARSE)
@@ -31,7 +31,7 @@ NOTICE = (
31
31
 
32
32
 
33
33
  def _grades(body: bytes) -> dict[str, int]:
34
- # The all view also checks the new-schema table, as `grades()` does.
34
+ # Every grade view also checks the new-schema table, as `grades()` does.
35
35
  records, table = parse_grade_page(body)
36
36
  return {
37
37
  "numeric": sum(1 for g in records.numeric if g.point_grade_id is None),
@@ -13,7 +13,9 @@ from librus_python_api.config import (
13
13
  GRADE_MAX_COLUMNS,
14
14
  GRADE_MAX_METADATA_FIELDS,
15
15
  GRADE_MAX_METADATA_LENGTH,
16
+ TOOLTIP_LABELS,
16
17
  TOOLTIP_MULTILINE_LABELS,
18
+ TOOLTIP_REPEATABLE_LABELS,
17
19
  UPSTREAM_ORIGINS,
18
20
  )
19
21
  from librus_python_api.exceptions import ErrorKind, LibrusError
@@ -82,14 +84,27 @@ def colspan(cell: html.HtmlElement) -> int:
82
84
 
83
85
 
84
86
  def tooltip_fields(
85
- element: html.HtmlElement, heading: list[str] | None = None
87
+ element: html.HtmlElement,
88
+ heading: list[str] | None = None,
89
+ *,
90
+ multiline: frozenset[str] | None = None,
91
+ unknown_multiline: bool = False,
86
92
  ) -> dict[str, str]:
87
93
  """Parse a 'Label: value<br>' title attribute into unique labelled fields.
88
94
 
89
95
  A free-text field may span lines; an unlabelled line after it continues
90
96
  its value. Any other unlabelled line is an unknown layout. With `heading`,
91
97
  the first line is taken whole, colon included, and appended to it.
98
+ `multiline` names the free-text labels (default `Komentarz`); with
99
+ `unknown_multiline`, a field whose label is not a known one (teacher text
100
+ holding a colon) is free text too, and inside such free text a line
101
+ starting with a known label is text unless the tooltip's closing run of
102
+ known-label lines begins there. Otherwise a known label such as `Data`
103
+ never continues. A repeatable label (`Poprawa oceny`) adds its value as
104
+ one more line of the same field.
92
105
  """
106
+ if multiline is None:
107
+ multiline = TOOLTIP_MULTILINE_LABELS
93
108
  title = element.get("title", "")
94
109
  if len(title) > GRADE_MAX_METADATA_LENGTH:
95
110
  raise LibrusError(ErrorKind.LIMIT)
@@ -98,18 +113,34 @@ def tooltip_fields(
98
113
  chunks = re.split(r"<br\s*/?>|\n", title, flags=re.I)
99
114
  if len(chunks) > GRADE_MAX_METADATA_FIELDS:
100
115
  raise LibrusError(ErrorKind.LIMIT)
101
- for chunk in chunks:
102
- if not chunk.strip():
103
- continue
104
- rendered = tooltip_line(chunk)
116
+ lines = [tooltip_line(chunk) for chunk in chunks if chunk.strip()]
117
+ # Start of the closing run of known-label lines, such as Kategoria, Data.
118
+ closing = len(lines)
119
+ while closing and _known_label(lines[closing - 1]):
120
+ closing -= 1
121
+ for index, rendered in enumerate(lines):
105
122
  if heading is not None and not heading:
106
123
  heading.append(rendered)
107
124
  continue
108
125
  key, separator, value = rendered.partition(":")
109
126
  key, value = key.strip(), value.strip()
110
- if not separator and last in TOOLTIP_MULTILINE_LABELS:
127
+ if (
128
+ last is not None
129
+ and (
130
+ last in multiline or (unknown_multiline and last not in TOOLTIP_LABELS)
131
+ )
132
+ and (
133
+ not separator
134
+ or (unknown_multiline and index < closing and key in TOOLTIP_LABELS)
135
+ )
136
+ ):
111
137
  result[last] = f"{result[last]}\n{rendered}".strip()
112
138
  continue
139
+ if separator and key in result and key in TOOLTIP_REPEATABLE_LABELS:
140
+ # Each repeat keeps its value, in order, as one more line.
141
+ result[key] = f"{result[key]}\n{value}"
142
+ last = key
143
+ continue
113
144
  if not separator or not key or key in result:
114
145
  raise LibrusError(ErrorKind.PARSE)
115
146
  result[key] = value
@@ -117,6 +148,11 @@ def tooltip_fields(
117
148
  return result
118
149
 
119
150
 
151
+ def _known_label(line: str) -> bool:
152
+ key, separator, _ = line.partition(":")
153
+ return bool(separator) and key.strip() in TOOLTIP_LABELS
154
+
155
+
120
156
  def tooltip_line(chunk: str) -> str:
121
157
  """One tooltip line's text, whitespace normalized."""
122
158
  # Tooltips are HTML inside an attribute; parse markup only where present.
@@ -130,7 +130,8 @@ class NotificationWorkflow:
130
130
  if uncertain:
131
131
  raise LibrusError(ErrorKind.CHECKPOINT)
132
132
  if not consume:
133
- raise LibrusError(ErrorKind.INVALID_INPUT)
133
+ # Nothing is read before the caller agrees to consume new events.
134
+ raise LibrusError(ErrorKind.CONSENT_REQUIRED)
134
135
  state = await store._io(lambda: store._read_state(context.identifier))
135
136
  ordinary = await self._ordinary(categories, window, budget)
136
137
  # Validate whole ordinary results and state capacity before consuming.
@@ -89,7 +89,6 @@ from librus_python_api.gateway_grades import (
89
89
  parse_user_info,
90
90
  )
91
91
  from librus_python_api.grade_parsers import parse_final_grades
92
- from librus_python_api.grade_records import parse_grade_records
93
92
  from librus_python_api.homework_range import HomeworkAccumulator
94
93
  from librus_python_api.lifecycle import join_owned
95
94
  from librus_python_api.message_content import parse_message_content, validate_reference
@@ -1084,11 +1083,11 @@ class AccountClient:
1084
1083
  Callback success acknowledges consumer-owned durable storage. Callback
1085
1084
  failure/timeout means acknowledgement unknown, never automatic replay.
1086
1085
  """
1087
- if (
1088
- type(allow_consume_events) is not bool
1089
- or not allow_consume_events
1090
- or self._consuming_schedule
1091
- ):
1086
+ if type(allow_consume_events) is not bool:
1087
+ raise LibrusError(ErrorKind.INVALID_INPUT)
1088
+ if not allow_consume_events:
1089
+ raise LibrusError(ErrorKind.CONSENT_REQUIRED)
1090
+ if self._consuming_schedule:
1092
1091
  raise LibrusError(ErrorKind.INVALID_INPUT)
1093
1092
  validate_checkpoint(checkpoint, checkpoint_timeout_seconds)
1094
1093
  self._consuming_schedule = True
@@ -1324,24 +1323,22 @@ class AccountClient:
1324
1323
  """Select a grade view with one view-changing POST.
1325
1324
 
1326
1325
  Changes the selected grade filter, not school records. A cache hit makes
1327
- no POST. Missing inline weight/count data remains unknown. In the all
1328
- view, a page with the new-schema descriptive table adds that table's
1326
+ no POST. Missing inline weight/count data remains unknown.
1327
+
1328
+ Every view checks a new-schema descriptive table the same way. Its
1329
+ semester cells either are empty script hooks or hold bare text, which
1330
+ becomes undated summaries with no extra request; any other layout
1331
+ fails the read. Only the all view with empty hooks adds that table's
1329
1332
  grades from the gateway: three more reads, five when there are grades.
1330
- Other views do not request them.
1331
1333
  """
1332
1334
  form = grade_view_form(view)
1333
1335
 
1334
1336
  async def fetch(budget: RequestBudget, _: bool) -> Grades:
1335
- if view is not GradeView.ALL:
1336
- records = await self._page(
1337
- "grades", budget, parse_grade_records, form=form
1338
- )
1339
- else:
1340
- records, table = await self._page(
1341
- "grades", budget, parse_grade_page, form=form
1342
- )
1343
- if table is not None and not table.filled:
1344
- records = await self._gateway_descriptive(records, table, budget)
1337
+ records, table = await self._page(
1338
+ "grades", budget, parse_grade_page, form=form
1339
+ )
1340
+ if view is GradeView.ALL and table is not None and not table.filled:
1341
+ records = await self._gateway_descriptive(records, table, budget)
1345
1342
  return Grades(
1346
1343
  self._session_identity(), records, self._observation("grades"), view
1347
1344
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: librus-python-api
3
- Version: 1.8.3
3
+ Version: 1.8.4
4
4
  Summary: Independent bounded async Librus Synergia client
5
5
  Project-URL: Repository, https://github.com/krzysztofbury/librus-python-api
6
6
  Project-URL: Issues, https://github.com/krzysztofbury/librus-python-api/issues
@@ -42,7 +42,10 @@ private ACLs. Windows installations include `pywin32` and `tzdata` automatically
42
42
  Network paths, reparse-point paths and unsafe storage permissions fail closed.
43
43
  See [platform requirements](contracts/persistence.md#windows-disk-boundary-25).
44
44
 
45
- **Status:** `1.8.3` reads behaviour grade symbols, filled descriptive
45
+ **Status:** `1.8.4` reads multi-line formative assessment text and repeated
46
+ corrections in grade tooltips, checks the new-schema grade table in every grade view and reports
47
+ missing calendar-consumption consent as `ConsentRequiredError`.
48
+ `1.8.3` reads behaviour grade symbols, filled descriptive
46
49
  grade tables, the "Aktualnie brak ogłoszeń" page and absences excused
47
50
  through e-Usprawiedliwienia. `1.8.2` keeps the modern mailbox session across a cancelled read
48
51
  or a reached limit and sends its own version in the `User-Agent`. `1.8.1` recovers a session Synergia no longer knows (it answers
@@ -66,7 +69,7 @@ See [limitations](#supported-features-and-limitations) below.
66
69
  Install the exact release from PyPI after its gated publication completes:
67
70
 
68
71
  ```sh
69
- python -m pip install librus-python-api==1.8.3
72
+ python -m pip install librus-python-api==1.8.4
70
73
  ```
71
74
 
72
75
  Pin the exact version qualified by your application. Before publication, use a
@@ -82,7 +85,7 @@ python -m pip install .
82
85
  To install a locally built wheel instead:
83
86
 
84
87
  ```sh
85
- python -m pip install ./dist/librus_python_api-1.8.3-py3-none-any.whl
88
+ python -m pip install ./dist/librus_python_api-1.8.4-py3-none-any.whl
86
89
  ```
87
90
 
88
91
  No console script or background process is installed: import the library in your
@@ -1,4 +1,4 @@
1
- librus_python_api/__init__.py,sha256=Ts6HZg9LRMp-MLMbjajwEdNlxVsmW8TnnGKbnzytP1k,6571
1
+ librus_python_api/__init__.py,sha256=A9OaGlvvc-wAqzAp7925LUh9EnmcEx_WncmUXoaZFWU,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
@@ -12,7 +12,7 @@ librus_python_api/budget.py,sha256=8fvqT7eIxyVX2P2OBG27XsYHk_HbtSS4lzuecYpHtgs,2
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=Sz6b5lWllzsKSbCbfGKqN6z-CZfwbCt9MkD2y-OG9CU,62019
15
+ librus_python_api/config.py,sha256=rAZK4YfQbhAftSj56tkLJ52DrrhWHzYoSz-ZdwjKUKM,62420
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
@@ -20,11 +20,11 @@ librus_python_api/files.py,sha256=pQBsIJyfTOZDz83ZB-5fcd-nPMcxR1HJ155h4zTHmsA,92
20
20
  librus_python_api/formative_grades.py,sha256=VykwtGvViZVOJMwPH3Re1RTDCHMr1_m9_xHjZyaXDIQ,6452
21
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=IT6A5_09Zu5h03g_p3pHVNepFCMoWag0WIJKg4A9MlQ,24978
23
+ librus_python_api/grade_records.py,sha256=xxQ8HrlIMXPtp8oEd3-TUewBhIIBc6Qu7V9Pu2ASxqk,25472
24
24
  librus_python_api/homework_range.py,sha256=erYfvS6PhHU5XhnR6LkiLNy677hGVflY3UAuHOs-YUw,1663
25
- librus_python_api/inspect_page.py,sha256=W5M21v0DlFw3eaqzwGG6hsI6fkTZVQTwkO2x-NNk4y0,5490
25
+ librus_python_api/inspect_page.py,sha256=zSo9SZVWz2POUWC0mNBnwhgHk5arrEOs3l209cxkPSg,5494
26
26
  librus_python_api/lifecycle.py,sha256=XzA5CEdZFo7iL5IMUrawnRvf2m9BFe0OseP-fpQFaAE,856
27
- librus_python_api/markup.py,sha256=AS0hfJS83lnOUpyfgoNHwUKf8MKBR3WN3p_4aeZ7BOk,6408
27
+ librus_python_api/markup.py,sha256=TzGt8pRed2n--BpxFtLY0eq1wzdOhvKhehS50uPBi1o,7935
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
@@ -33,7 +33,7 @@ librus_python_api/modern_mailbox.py,sha256=2jF9UnJZ5AF6dip-GrMcv_z2kSLpp87dIpVeX
33
33
  librus_python_api/modern_messages.py,sha256=qt2CXtrxCZQ-jTt0aQCD6ZaPAJQZXf0BxmvacPo9B0w,10653
34
34
  librus_python_api/notification_models.py,sha256=4eLTAE9JzHhveoaWtZQwGBolmb87QUP5kOSW7Jjt7J0,4199
35
35
  librus_python_api/notification_persistence.py,sha256=8GYPbCVD0AecrC4IUXFkHkhNS-uGXkXzsuKxreTIk9I,49454
36
- librus_python_api/notification_workflow.py,sha256=MsBJdwn6J1UUGlz3Ldzi7WEBz5Qsama7eGmZvUQj25g,12924
36
+ librus_python_api/notification_workflow.py,sha256=XpWNGWbRsqtpTJinPsw65bjnnEnNpm-qzn0QPxz43Xg,13009
37
37
  librus_python_api/notifications.py,sha256=VbA8pqNG_19yYo9y7IqpGRmfEb4qDcvA5gBb1syGJKs,7806
38
38
  librus_python_api/parsers.py,sha256=8sdfPsOZYHNYVZ4rwX_de5Y5rPl0umoQPon3uflQFlY,9178
39
39
  librus_python_api/parsing.py,sha256=H71srw5yx-RFrAKZFgj-QFX-DIXZmhnGrDTmJhazCB8,1838
@@ -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=GaavbI6117fcbVuWDzZXDbbgQUczZXXFVNKEMXsFfaw,103063
47
+ librus_python_api/service.py,sha256=A24_70Doqk0mh9fAOxOUSNMtuPG-7qaDvUMlBSE8Lrw,103073
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.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,,
50
+ librus_python_api-1.8.4.dist-info/METADATA,sha256=M-xBnjR60MNy2jfQSjQsmwDPnwWUNVnIhaoi8dXRSe4,14586
51
+ librus_python_api-1.8.4.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
52
+ librus_python_api-1.8.4.dist-info/licenses/LICENSE,sha256=IuerLb3cnZCsaB23znydjjvKqP-LmSJd3syKa9NGUts,1071
53
+ librus_python_api-1.8.4.dist-info/RECORD,,