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.
Files changed (54) hide show
  1. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/API.md +39 -1
  2. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/CHANGELOG.md +20 -0
  3. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/PKG-INFO +15 -7
  4. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/README.md +14 -6
  5. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/__init__.py +17 -1
  6. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/config.py +37 -0
  7. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/models.py +74 -0
  8. librus_python_api-1.3.0/src/librus_python_api/school_year_archive.py +432 -0
  9. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/service.py +26 -0
  10. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/uv.lock +57 -57
  11. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/.gitignore +0 -0
  12. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/LICENSE +0 -0
  13. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/SECURITY.md +0 -0
  14. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/pyproject.toml +0 -0
  15. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/_notification_bootstrap.py +0 -0
  16. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/_notification_codec.py +0 -0
  17. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/_storage.py +0 -0
  18. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/_windows_filesystem.py +0 -0
  19. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/announcements.py +0 -0
  20. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/attachment_routes.py +0 -0
  21. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/attachments.py +0 -0
  22. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/attendance.py +0 -0
  23. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/attendance_frequency.py +0 -0
  24. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/budget.py +0 -0
  25. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/checkpoint.py +0 -0
  26. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/completed_lessons.py +0 -0
  27. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/detail_fields.py +0 -0
  28. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/diagnostics.py +0 -0
  29. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/exceptions.py +0 -0
  30. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/files.py +0 -0
  31. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/grade_parsers.py +0 -0
  32. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/grade_records.py +0 -0
  33. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/homework_range.py +0 -0
  34. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/lifecycle.py +0 -0
  35. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/markup.py +0 -0
  36. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/message_content.py +0 -0
  37. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/messages.py +0 -0
  38. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/modern_body.py +0 -0
  39. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/modern_mailbox.py +0 -0
  40. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/modern_messages.py +0 -0
  41. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/notification_models.py +0 -0
  42. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/notification_persistence.py +0 -0
  43. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/notification_workflow.py +0 -0
  44. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/notifications.py +0 -0
  45. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/parsers.py +0 -0
  46. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/parsing.py +0 -0
  47. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/persistence.py +0 -0
  48. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/py.typed +0 -0
  49. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/recipients.py +0 -0
  50. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/scheduler.py +0 -0
  51. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/school_reads.py +0 -0
  52. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/sending.py +0 -0
  53. {librus_python_api-1.2.1 → librus_python_api-1.3.0}/src/librus_python_api/timetable.py +0 -0
  54. {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
- The observation card ("Karta spostrzeżeń") shown on some grades pages is not
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.2.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
+ [![Weekly live check](https://github.com/krzysztofbury/librus-python-api/actions/workflows/live-check.yml/badge.svg?branch=main)](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.x` freezes the documented native library API. It does not claim
44
- completed MCP cutover, production-state migration or universal live-school
45
- compatibility. Those consumer/live gates remain separate. School features depend
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.2.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.2.1-py3-none-any.whl
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
+ [![Weekly live check](https://github.com/krzysztofbury/librus-python-api/actions/workflows/live-check.yml/badge.svg?branch=main)](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.x` freezes the documented native library API. It does not claim
18
- completed MCP cutover, production-state migration or universal live-school
19
- compatibility. Those consumer/live gates remain separate. School features depend
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.2.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.2.1-py3-none-any.whl
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.2.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.3.1"
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/82/6a/878cc1097d4035f82bd516658d0c528d2a9955bc7b363afcbd0b07fea11b/mypy-2.3.1.tar.gz", hash = "sha256:47c1b1207258513a9d93495f69c8be9de73916186f0e52703e8c461b7a623419", size = 3992554, upload-time = "2026-08-15T03:03:38.549Z" }
1246
- wheels = [
1247
- { url = "https://files.pythonhosted.org/packages/de/cf/862010ee800ca9c2bd0c4c0dacf0f092e5411824a09b8f97ad4be8fe250e/mypy-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:114dff494000f18bd10d5d95d84b8567b26da60279ecbe838131841df20e635d", size = 13964542, upload-time = "2026-08-15T03:02:21.43Z" },
1248
- { url = "https://files.pythonhosted.org/packages/75/5a/3f3a2107b41e3e92e617e25daaee121413b91e9784bea733131ed4fecc5d/mypy-2.3.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8637731bb5eee3671eb2c3200827aa3564ed8a9309ecee4d1afe77e6d031bdb", size = 14168922, upload-time = "2026-08-15T03:03:00.351Z" },
1249
- { url = "https://files.pythonhosted.org/packages/8b/41/04dc4fe7e63d7820fa4eff272e95157d30cbea921388f3ab3fe77794cd0b/mypy-2.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c80fbc405ed8020f5ff3802dc18cf060197bcdd3fbdd6a26ef2fd34dfdd5226", size = 15244791, upload-time = "2026-08-15T03:02:31.089Z" },
1250
- { url = "https://files.pythonhosted.org/packages/96/fc/c3053b26b9054949285aa868cb6af8c10e7591541cacd79c5dcc06a1fcf9/mypy-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:84081f538ce27375045c02e3d7f81bd11d853400621ae245d87ce7b6c420ec74", size = 15501627, upload-time = "2026-08-15T03:03:34.128Z" },
1251
- { url = "https://files.pythonhosted.org/packages/70/4e/d77daab008bbc4e5001374d7928f4a260d28f0e6747af444fc4763f7a310/mypy-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:e9144ac16fde007096f9563eb2041b4433c2d705c4218edeb79e7e9d01035ee6", size = 11243961, upload-time = "2026-08-15T03:02:11.952Z" },
1252
- { url = "https://files.pythonhosted.org/packages/f0/f8/7eb68c136e4abd30569fe31ef2bfcb7eceae9952cab80017c04cd09f5d0c/mypy-2.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:77ad9529e67dca28e511f5cd5671436584ce91f6d3bac159a353158187b986ac", size = 10213219, upload-time = "2026-08-15T03:02:26.361Z" },
1253
- { url = "https://files.pythonhosted.org/packages/be/c4/42a49d44aeff804edf1b19acce0b49e8bd1a9c57dee9605dd8d980aa43d7/mypy-2.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:192abaedf75da1bc0b1cef104927e70ec49c1ef0031cc4825c7ee10a438ed24d", size = 13986778, upload-time = "2026-08-15T03:01:33.69Z" },
1254
- { url = "https://files.pythonhosted.org/packages/45/13/9331fd2dfed7194d66c5304072894a8be3e51e9deda6863c1eceaa35a43d/mypy-2.3.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf678dffd16efcda2c15cbd30e9ecc0081388e29ea23687a88e686ed92638dc3", size = 14188467, upload-time = "2026-08-15T03:02:40.554Z" },
1255
- { url = "https://files.pythonhosted.org/packages/78/f7/f4a34edab45667c5465855dc585a20e87978ffa8aee711445b7239d120c6/mypy-2.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e036f06b41630f4c8a1d48f9ac6aa26acc65f8be089973f5519da643318f03f", size = 15225538, upload-time = "2026-08-15T03:03:09.761Z" },
1256
- { url = "https://files.pythonhosted.org/packages/40/05/534b3590757bd05794f73e07f6666c2a77b8597ffed795c94ce570096aa0/mypy-2.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71af9c8a894e862b58e92abb08e53b05a384a1e5e5d6dc7cda59126211a53d82", size = 15480805, upload-time = "2026-08-15T03:01:41.134Z" },
1257
- { url = "https://files.pythonhosted.org/packages/55/da/bdfba852e2562f599624af5bb7d29e36b0b4f526f2b8bac85efe0dd1803d/mypy-2.3.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:3c80cd23d85368bdd9f37d5231dfd97d35bcbf5bf41af96ef3a9b078ad1957f9", size = 7761712, upload-time = "2026-08-15T03:02:36.008Z" },
1258
- { url = "https://files.pythonhosted.org/packages/98/31/60fc64a74cdba4f2a5d642d32317993e479163e1ac7d91b695e5d15e2264/mypy-2.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:4956f34d145e145562a0a0bf367f642bbc85c04ec2baf47ae015947c3169a85d", size = 11423968, upload-time = "2026-08-15T03:02:06.931Z" },
1259
- { url = "https://files.pythonhosted.org/packages/a9/23/eb5950b24cd26ba3b78f87707a275568d633c77dae8e61c9661be6055ca6/mypy-2.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:cfb12e360242d23d91f5e978d94f58ea66acf5804c4fb6f2f794a20d4cb1b595", size = 10399323, upload-time = "2026-08-15T03:02:33.671Z" },
1260
- { url = "https://files.pythonhosted.org/packages/82/c7/f80f4e46c0b9a00eb5f78a79d49dda8bdf56a5230f7257fb33e76be04da7/mypy-2.3.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5f1c50bb05b64e2026b52867e8d21106f01313c744a2c4ecc34c90d12e8d6e2", size = 15121308, upload-time = "2026-08-15T03:01:46.053Z" },
1261
- { url = "https://files.pythonhosted.org/packages/5d/74/9b04f17c7074cc5188f02fb63a2ca1d43fedf479e84fe3091c39061a1d7f/mypy-2.3.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:667196b352f4cf304ded4c10f90cfc179263a1acfb3cdcfa984bdfd340d498bc", size = 15536590, upload-time = "2026-08-15T03:01:35.941Z" },
1262
- { url = "https://files.pythonhosted.org/packages/26/04/c837ef6208e567774e2ed1f863f8ba6ec4817b1b6dd426315e5d559b6ec9/mypy-2.3.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9c53e395c12cad2c6d4b67d5da7c6057638a132d85c08b73646b18f802a0045", size = 16791074, upload-time = "2026-08-15T03:01:31.073Z" },
1263
- { url = "https://files.pythonhosted.org/packages/37/68/48730230afa45192d5bd429a6a2ff24a6f8dedda90fdf2b221792b54518f/mypy-2.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:18162b128c3f9c703cd35f5537446900b0d21a2549aa7a95d21380d2ef643fb0", size = 17069183, upload-time = "2026-08-15T03:02:28.566Z" },
1264
- { url = "https://files.pythonhosted.org/packages/1c/ea/ca23fc9c20eeda09a15c9cbcf50015d0e73f409f6ead059e42aa69a608ff/mypy-2.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:30c0477d4aab7b7f39c8397dc877f2c96b9fe5588ec379f372c56eb63d599f63", size = 12154679, upload-time = "2026-08-15T03:02:04.809Z" },
1265
- { url = "https://files.pythonhosted.org/packages/3b/67/8d982126034990869466f73b8db80dcb2234a7ac39b4dad093e047a79835/mypy-2.3.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6941ab3619377bc3f32ca02876b07d27f216f5201604b664d3937ea0fdd23bb4", size = 10969159, upload-time = "2026-08-15T03:02:38.152Z" },
1266
- { url = "https://files.pythonhosted.org/packages/ee/f7/41e7f2d8117fbc7a7587286162ffe2f688984b69c46ed63cf5f2e4fc3bae/mypy-2.3.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6f041a6de52c9217ca125e78ba0a335cb7fd98a1c0580978e49ab2b126f70b57", size = 13990694, upload-time = "2026-08-15T03:03:21.919Z" },
1267
- { url = "https://files.pythonhosted.org/packages/06/85/8f665811a0c8f3bf6fa1d9acd665ec2d97a2bcc453ae68dcd92340941cd6/mypy-2.3.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5159ae60f5dbc3a498af5ba8365505808ac8031bc63f9e00304ad545d40bdd9b", size = 14203518, upload-time = "2026-08-15T03:01:48.455Z" },
1268
- { url = "https://files.pythonhosted.org/packages/2d/82/91b866c8546b120bff83b73a439d90d2d63ef3aff113599e6b8e4d566848/mypy-2.3.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47a8a7a0a7f6f6e63995c0ac36fa0c07b127413fdc81f0439b7f3dccafd33561", size = 15220224, upload-time = "2026-08-15T03:01:23.577Z" },
1269
- { url = "https://files.pythonhosted.org/packages/c8/78/c226c99208ee40de7c768369fa533f933afa003dfdc606ff021450724e91/mypy-2.3.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2329c0501293d4e1f33bc15d04d6304d65a1cdda967ee93a05c1e681a3923133", size = 15501512, upload-time = "2026-08-15T03:02:09.453Z" },
1270
- { url = "https://files.pythonhosted.org/packages/a9/e7/7cfb3f106c393979f4cc37ad6c0586044d50401e3c35b0c003e4f3ba6bc9/mypy-2.3.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:bb26deed807bdb0457cf3e3f1cd7c4a1cf9d66864eaf1b4a61e06805d4c6b1f9", size = 7761913, upload-time = "2026-08-15T03:01:55.65Z" },
1271
- { url = "https://files.pythonhosted.org/packages/99/3c/52affefa273b97939a1f474ae4a349c8718635c15b941112dfab4291b0c1/mypy-2.3.1-cp315-cp315-win_amd64.whl", hash = "sha256:375d7013876a8233b2d05be185bfa09f689696cd999ce8b1cfe6acac5c80e8a3", size = 11422533, upload-time = "2026-08-15T03:03:24.101Z" },
1272
- { url = "https://files.pythonhosted.org/packages/2a/b7/75643e70c72a5b346d8a9b1543c967ea8824df2ee3fb7ccba652c272b7bb/mypy-2.3.1-cp315-cp315-win_arm64.whl", hash = "sha256:586b3612214cceabb3c0f588c97e7d1e535393f06a60e912e994f6b3ace97523", size = 10397931, upload-time = "2026-08-15T03:02:55.265Z" },
1273
- { url = "https://files.pythonhosted.org/packages/10/ce/53be21f2d4adfcd26f63f1184a13ed797015ab463853f117e2e11e4d726f/mypy-2.3.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:ef0c6335cda9d807f8193d8ff6204a72bc909fa9882aacbca14f43cdb7188306", size = 15118669, upload-time = "2026-08-15T03:02:51.479Z" },
1274
- { url = "https://files.pythonhosted.org/packages/62/43/20de757cd42989d291a17fad607742c4c74e875ce5cea00e5a5225020ac1/mypy-2.3.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e598c8c66401d26b150872154a286e6d484cf2789c3bb28a7556806298423021", size = 15545627, upload-time = "2026-08-15T03:03:05.132Z" },
1275
- { url = "https://files.pythonhosted.org/packages/7e/fc/092bdf77ad280eaf501422f0f3b966012b528076cc13e41a774861c907d1/mypy-2.3.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eda22fd4efa9dcd39331d1dede9b5b8b8a7fd69af07592e778433da98610d29e", size = 16764157, upload-time = "2026-08-15T03:02:23.958Z" },
1276
- { url = "https://files.pythonhosted.org/packages/94/5c/c94c4d62d909b07f552d0d9356d7acc943825558e602a64822ffa2231536/mypy-2.3.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:2a0ba2e57847849fb0d1fcdabb32786d223095ed8bc121dfe322bcdb3d9c46bc", size = 17073258, upload-time = "2026-08-15T03:02:14.573Z" },
1277
- { url = "https://files.pythonhosted.org/packages/c0/f7/511a88b89e478053c02d22039bb8f3ce4183efe8fd7a4f0a5910a8bb0a32/mypy-2.3.1-cp315-cp315t-win_amd64.whl", hash = "sha256:3f7e865dd51f235f60a2dbcd8728a1c095f5ca28f095d48a725b84cd935735c4", size = 12135505, upload-time = "2026-08-15T03:02:16.714Z" },
1278
- { url = "https://files.pythonhosted.org/packages/71/bf/02573b56964ecb0f7c644f915f53c325ae15c3faec521c5adf11599a32df/mypy-2.3.1-cp315-cp315t-win_arm64.whl", hash = "sha256:8ad80807dc3ab8ea978b1b2b6e4a657194ace1d4ef03e0e731aff1abd517da29", size = 10962647, upload-time = "2026-08-15T03:01:43.712Z" },
1279
- { url = "https://files.pythonhosted.org/packages/8e/41/9675c7a1e78edecfba0b79e587a52594c56e189368261dc7b3a7fffb9527/mypy-2.3.1-py3-none-any.whl", hash = "sha256:6ed5c7e3419083268e5c9258bd1c1ef91af44a9e89374dbcaf37b775716e72eb", size = 2754338, upload-time = "2026-08-15T03:02:53.4Z" },
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.9"
1981
- source = { registry = "https://pypi.org/simple" }
1982
- sdist = { url = "https://files.pythonhosted.org/packages/96/bf/c935ca98e73fe8ce65b87ef08a280c0c1e85295d569228c15e87d8fdfaf1/ruff-0.16.9.tar.gz", hash = "sha256:12b625c6cfba78d285d9f48eda5f053374f1e53cb10ef17342a383750db99161", size = 4948764, upload-time = "2026-09-24T20:37:49.416Z" }
1983
- wheels = [
1984
- { url = "https://files.pythonhosted.org/packages/0d/26/df51322b52ee1ada7eff2d071ea09d11d5c2d1dcc9f02594f5785c4d1635/ruff-0.16.9-py3-none-linux_armv6l.whl", hash = "sha256:95e6f022090368ab3b824c36276839c53b2adf1a3f4c09fefc33dfc400f6da96", size = 10082922, upload-time = "2026-09-24T20:37:13.045Z" },
1985
- { url = "https://files.pythonhosted.org/packages/a5/27/7bf51f5a7aa375e9f339280a303aab44ca75dc1525f1cdc5991761685b0f/ruff-0.16.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a5f27be168556594a86d2f415db0cf43f5291917849318f873c7e2791f7a8c67", size = 10236360, upload-time = "2026-09-24T20:37:16.049Z" },
1986
- { url = "https://files.pythonhosted.org/packages/b6/63/09659283f92f02dff45809da194a70da2f688d87c55d8875c4fae3536072/ruff-0.16.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1632eb1d6197f33bd00b1acbc5b71009e89a8895c158e2d2b03a834fac964ab6", size = 9892940, upload-time = "2026-09-24T20:37:17.957Z" },
1987
- { url = "https://files.pythonhosted.org/packages/24/58/98de1b72ec172f5f8f1731236fe21585b3998bf7dfe9fcc44ae9ba626012/ruff-0.16.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3f951b14d865d5952c89d40a5ca07e87abe24fa5453299878411e127748fb1c", size = 10032114, upload-time = "2026-09-24T20:37:19.942Z" },
1988
- { url = "https://files.pythonhosted.org/packages/c8/7d/f1e17c54ab59d4bad1dce8ee3e22a7a1d0ef4745240decacdcf3832b5bb2/ruff-0.16.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:447fc07e1573afff7cb02803462b12b6c8ece7cf10e2cd78565fa6d7a1c0bf8d", size = 9910227, upload-time = "2026-09-24T20:37:21.872Z" },
1989
- { url = "https://files.pythonhosted.org/packages/34/19/436f647a65075bbd3bab2668b3bdaa5120559b294694018cdcefabbbf30b/ruff-0.16.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a3e039a6a40ed976c491722b60e0ae4a4aa1a86057f540ee7a37a5d19ae9120", size = 10547484, upload-time = "2026-09-24T20:37:24.229Z" },
1990
- { url = "https://files.pythonhosted.org/packages/03/59/38430a6bc2f6d8095447ac39625cf8b6e9344a47e6c26225c8ba1bff3ffb/ruff-0.16.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4684dded7db60aa57cb118fa158630f5feade4af5782903b6053484bdf9bd129", size = 11412367, upload-time = "2026-09-24T20:37:26.307Z" },
1991
- { url = "https://files.pythonhosted.org/packages/c8/bd/bbb6d7fc7f208c8b8c50dd5dc8206e4cfdb1e7a8fb852606a3adf370c880/ruff-0.16.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d29c934357e45642fda2f34c0b1f4025b4a6c01e15e4bf0016879d60078a142c", size = 10869787, upload-time = "2026-09-24T20:37:28.35Z" },
1992
- { url = "https://files.pythonhosted.org/packages/bc/b8/9c543074918061abbefc3bd139bee22de35abedb00dde0f2d27288838962/ruff-0.16.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a21713e629d3e5bdb2f5c2def1cc7f04f47fa8e1a7eb0571b4a28e1da64bc728", size = 10406494, upload-time = "2026-09-24T20:37:30.624Z" },
1993
- { url = "https://files.pythonhosted.org/packages/35/7a/5a8851bd146e7ccf8fd4b003f6c75c11f8fbdb0e60673b45097986b6bf41/ruff-0.16.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:7baa24ef5fc8e77aa93879e1d3f43754a01ae488e869f1ae30cf431afd4d2452", size = 10590083, upload-time = "2026-09-24T20:37:32.439Z" },
1994
- { url = "https://files.pythonhosted.org/packages/87/f0/4c3467188f23f806960b46fa76575a7cd0514c9ba90562650b71efc96980/ruff-0.16.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a41aac6230aadfaa133bdfa1614488531ffa3e0837567ae04c0da2058a9c0f9e", size = 10119151, upload-time = "2026-09-24T20:37:34.581Z" },
1995
- { url = "https://files.pythonhosted.org/packages/15/34/5a4def5adea572ce6aea0bb64f21f928ee317b80e0db747d7979b01d7261/ruff-0.16.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c2529fb5896d49115b0e9aa8f887490b34bbe76baf879ec2264ac59406869ce7", size = 9911544, upload-time = "2026-09-24T20:37:36.796Z" },
1996
- { url = "https://files.pythonhosted.org/packages/62/5d/d15ebea7499eef9373318c0ee6ca127832927c6529731f6d48e18dce7ca9/ruff-0.16.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:41e3870277694177429b56406d65dfbdb2c2802c52b715edaf6a0b829c69d4ee", size = 10269884, upload-time = "2026-09-24T20:37:38.857Z" },
1997
- { url = "https://files.pythonhosted.org/packages/d1/56/c5d3cd119ded7a3c7aba0e961b69cb3df701c91662c98ad694467d060ce1/ruff-0.16.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8adbe4e58af167f767d7b2ba5e83c42e878350796cf78c2f5e14ab9903a92588", size = 10749366, upload-time = "2026-09-24T20:37:41.042Z" },
1998
- { url = "https://files.pythonhosted.org/packages/ac/fe/734ec7527029ac757ecf821f143c9f3fcf69149c21f53a044a899b430f5a/ruff-0.16.9-py3-none-win32.whl", hash = "sha256:0e1dbc2073624dee6618d41d0098690a7244654af746704b64759e12b6b6b385", size = 10152355, upload-time = "2026-09-24T20:37:43.025Z" },
1999
- { url = "https://files.pythonhosted.org/packages/14/21/26e4643629b3ebb44f0a06f9c9a53058d63d989415f63a9a3c28e2ee7f22/ruff-0.16.9-py3-none-win_amd64.whl", hash = "sha256:6bd40fec8cd4c8a3d4dd589bd8ad4e6320c13c29234159bfd959a40d529d597b", size = 10592965, upload-time = "2026-09-24T20:37:44.944Z" },
2000
- { url = "https://files.pythonhosted.org/packages/51/60/5fb1a39dbb5ae314d5f59bc7348a63c1d5c20f3cd83914c4b5cb0be31d2d/ruff-0.16.9-py3-none-win_arm64.whl", hash = "sha256:ed1a252039200f57a59eebc063b54beabea67bfbaaca0eeaa7f54b5fbcda2284", size = 10458649, upload-time = "2026-09-24T20:37:46.882Z" },
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]]