librus-python-api 1.6.2__tar.gz → 1.6.4__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 (56) hide show
  1. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/API.md +2 -2
  2. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/CHANGELOG.md +42 -0
  3. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/PKG-INFO +8 -8
  4. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/README.md +6 -6
  5. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/SECURITY.md +11 -1
  6. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/pyproject.toml +1 -1
  7. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/__init__.py +1 -1
  8. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/modern_body.py +33 -6
  9. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/uv.lock +35 -35
  10. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/.gitignore +0 -0
  11. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/LICENSE +0 -0
  12. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/_notification_bootstrap.py +0 -0
  13. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/_notification_codec.py +0 -0
  14. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/_storage.py +0 -0
  15. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/_windows_filesystem.py +0 -0
  16. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/announcements.py +0 -0
  17. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/attachment_routes.py +0 -0
  18. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/attachments.py +0 -0
  19. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/attendance.py +0 -0
  20. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/attendance_frequency.py +0 -0
  21. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/budget.py +0 -0
  22. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/checkpoint.py +0 -0
  23. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/class_free_days.py +0 -0
  24. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/completed_lessons.py +0 -0
  25. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/config.py +0 -0
  26. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/detail_fields.py +0 -0
  27. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/diagnostics.py +0 -0
  28. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/exceptions.py +0 -0
  29. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/files.py +0 -0
  30. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/formative_grades.py +0 -0
  31. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/grade_parsers.py +0 -0
  32. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/grade_records.py +0 -0
  33. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/homework_range.py +0 -0
  34. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/lifecycle.py +0 -0
  35. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/markup.py +0 -0
  36. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/message_content.py +0 -0
  37. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/messages.py +0 -0
  38. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/models.py +0 -0
  39. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/modern_mailbox.py +0 -0
  40. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/modern_messages.py +0 -0
  41. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/notification_models.py +0 -0
  42. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/notification_persistence.py +0 -0
  43. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/notification_workflow.py +0 -0
  44. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/notifications.py +0 -0
  45. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/parsers.py +0 -0
  46. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/parsing.py +0 -0
  47. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/persistence.py +0 -0
  48. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/py.typed +0 -0
  49. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/recipients.py +0 -0
  50. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/scheduler.py +0 -0
  51. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/school_reads.py +0 -0
  52. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/school_year_archive.py +0 -0
  53. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/sending.py +0 -0
  54. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/service.py +0 -0
  55. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/timetable.py +0 -0
  56. {librus_python_api-1.6.2 → librus_python_api-1.6.4}/src/librus_python_api/transport.py +0 -0
@@ -657,7 +657,7 @@ creates state, reads old MCP files or starts background recovery.
657
657
  files and unsafe POSIX permissions or Windows ACLs fail closed, never reset state.
658
658
  No automatic retry, reconciliation, deletion of consumed history or polling.
659
659
  - Defaults: 8 admitted storage workers and 8 send workflows, 256 total records,
660
- 32 unused previews, 300-second expiry and 0.1-second SQLite busy timeout.
660
+ 32 unused previews, 300-second expiry and 1-second SQLite busy timeout.
661
661
  Configured maxima: 64 each, 4,096 records, 256 previews, 300 seconds and 5 seconds.
662
662
  Storage workers are serialized per store; full queues/contention fail immediately
663
663
  or after the bounded busy interval. The final outcome save after a claim waits
@@ -760,7 +760,7 @@ async with NotificationStore(Path("/absolute/private/notification-state")) as st
760
760
  state without HTTP. Pending batches do not mark IDs seen. Defaults are bounded:
761
761
  16 contexts, 8 workers/workflows, 32 raw/reservation records, 16 MiB checkpoint
762
762
  bytes, 4 MiB total seen-state bytes, 4,096 IDs/category, 500 items/1 MiB per batch,
763
- 500 events/128 KiB value JSON per replay slice, 0.1 s busy timeout, and 5 s
763
+ 500 events/128 KiB value JSON per replay slice, 1 s busy timeout, and 5 s
764
764
  `final_busy_timeout_seconds` for checkpoint, staging and acknowledgement saves
765
765
  that follow upstream work, since all contexts share one write lock. The full
766
766
  envelope is capped at 4 MiB encoded body plus 4 MiB metadata; database main file
@@ -1,5 +1,47 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.6.4 (2026-10-26) - Message body rendering security fix
4
+
5
+ Fixes a denial of service in message body rendering. Public types, signatures
6
+ and parser behaviour are unchanged.
7
+
8
+ - Security (GHSA-c9qq-qqr2-hf23, CWE-1333): `modern_message_content` decided
9
+ whether a body is an XML `Message` wrapper with a regular expression that
10
+ backtracked exponentially over a run of XML comments or processing
11
+ instructions with no wrapper after it. A body of a few hundred bytes, which any
12
+ Librus user can send, blocked the calling thread for minutes; that includes
13
+ every client that renders received messages, such as librus-mcp. A single
14
+ linear scan now makes the same decision: it accepts exactly the inputs the
15
+ expression accepted, so routing between the XML and HTML readers is
16
+ unchanged. All releases up to 1.6.3 are affected.
17
+ - Development lock: virtualenv 21.14.13 fixes CVE-2026-115586, pre-commit 4.7.0
18
+ and ruff 0.17.1. None is a runtime dependency.
19
+ - CI and release workflows use setup-uv 10.3.2 and pypi-publish 1.14.3. The
20
+ publish action up to 1.14.2 trusted the revoked emSign root CA in the
21
+ Sigstore chain used for Trusted Publishing (GHSA-5xjx-3w67-w9x9).
22
+ - SECURITY.md adds GitHub private vulnerability reporting beside email and lists
23
+ the repository protections; Dependabot runs on a fixed weekly schedule.
24
+
25
+ ## 1.6.3 (2026-10-22) - Dependency security update
26
+
27
+ Raises dependency floors and tooling pins past published advisories. Public
28
+ types, signatures and parser behaviour are unchanged.
29
+
30
+ - Runtime: require `aiohttp>=3.14.4`. Earlier 3.14 releases are affected by
31
+ GHSA-494j-54xm-rwp3 (a Secure cookie in the shared cookie jar could be sent
32
+ over plain HTTP) and GHSA-gx76-c99x-2c96 (a subdomain could strip the
33
+ host-only flag from a parent-domain cookie), both relevant to the client
34
+ cookie jar, plus server-side advisories that this client does not reach. The
35
+ lock already used 3.14.5; the floor stops a resolver from choosing an affected
36
+ release.
37
+ - Development lock: certifi 2026.10.19 drops the compromised emSign ECC Root CA
38
+ G3 (GHSA-6q48-6c7w-9gw2), and virtualenv 21.14.12 fixes activation-script and
39
+ path quoting advisories. Neither is a runtime dependency.
40
+ - CI and release workflows pin uv 0.13.5 with per-platform checksums. uv 0.12.7
41
+ trusted the same compromised root (GHSA-hq85-5cpj-mr62) and could write
42
+ outside the install prefix on Windows (GHSA-2cv4-cqwr-gwf7). The lockfile
43
+ format revision moves from 3 to 5 with no resolution change.
44
+
3
45
  ## 1.6.2 (2026-10-19) - School layout variants and behaviour points
4
46
 
5
47
  Fixes for completed-lesson and grade pages that 1.6.1 rejected with
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: librus-python-api
3
- Version: 1.6.2
3
+ Version: 1.6.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
@@ -16,7 +16,7 @@ Classifier: Programming Language :: Python :: 3.13
16
16
  Classifier: Programming Language :: Python :: 3.14
17
17
  Classifier: Typing :: Typed
18
18
  Requires-Python: >=3.13
19
- Requires-Dist: aiohttp<4,>=3.14.2
19
+ Requires-Dist: aiohttp<4,>=3.14.4
20
20
  Requires-Dist: lxml<7,>=6.1
21
21
  Requires-Dist: pydantic<3,>=2.12
22
22
  Requires-Dist: pywin32<400,>=312; sys_platform == 'win32'
@@ -42,8 +42,9 @@ private ACLs. Windows installations include `pywin32` and `tzdata` automatically
42
42
  Network paths, reparse-point paths and unsafe storage permissions fail closed.
43
43
  See [platform requirements](contracts/persistence.md#windows-disk-boundary-25).
44
44
 
45
- **Status:** `1.6.2` accepts more completed-lesson and grade layouts, including
46
- behaviour points, which are validated and not reported as grades.
45
+ **Status:** `1.6.4` renders message bodies in linear time, so a crafted body can
46
+ no longer stall a client, and keeps the 1.6.2 completed-lesson and grade layouts,
47
+ including behaviour points, which are validated and not reported as grades.
47
48
  Class free days, typed capability outcomes and the live-check freshness watchdog
48
49
  remain available. See [verification](VERIFICATION.md).
49
50
  The stable 1.x API does not guarantee every school layout. School features depend
@@ -55,7 +56,7 @@ See [limitations](#supported-features-and-limitations) below.
55
56
  Install the exact release from PyPI after its gated publication completes:
56
57
 
57
58
  ```sh
58
- python -m pip install librus-python-api==1.6.2
59
+ python -m pip install librus-python-api==1.6.4
59
60
  ```
60
61
 
61
62
  Pin the exact version qualified by your application. Before publication, use a
@@ -71,7 +72,7 @@ python -m pip install .
71
72
  To install a locally built wheel instead:
72
73
 
73
74
  ```sh
74
- python -m pip install ./dist/librus_python_api-1.6.2-py3-none-any.whl
75
+ python -m pip install ./dist/librus_python_api-1.6.4-py3-none-any.whl
75
76
  ```
76
77
 
77
78
  No CLI or background process is installed: import the library in your own program.
@@ -250,7 +251,7 @@ Completed lessons may be disabled by the school. Behaviour notes are not
250
251
  implemented. Some recipient, archive and receipt layouts remain
251
252
  unqualified; backend acceptance is not proof of delivery. Tests cover supported
252
253
  contracts, not every school or role. Detailed coverage is in the
253
- [verification log][verification] and [roadmap][roadmap].
254
+ [verification log][verification]; open work is tracked in [issues][issues].
254
255
 
255
256
  ## Development and support
256
257
 
@@ -265,7 +266,6 @@ MIT licensed. See [LICENSE][license].
265
266
  [api]: https://github.com/krzysztofbury/librus-python-api/blob/main/API.md
266
267
  [upgrade]: https://github.com/krzysztofbury/librus-python-api/blob/main/contracts/account-context.md
267
268
  [verification]: https://github.com/krzysztofbury/librus-python-api/blob/main/VERIFICATION.md
268
- [roadmap]: https://github.com/krzysztofbury/librus-python-api/blob/main/TODO.md
269
269
  [contributing]: https://github.com/krzysztofbury/librus-python-api/blob/main/CONTRIBUTING.md
270
270
  [issues]: https://github.com/krzysztofbury/librus-python-api/issues
271
271
  [security]: https://github.com/krzysztofbury/librus-python-api/blob/main/SECURITY.md
@@ -16,8 +16,9 @@ private ACLs. Windows installations include `pywin32` and `tzdata` automatically
16
16
  Network paths, reparse-point paths and unsafe storage permissions fail closed.
17
17
  See [platform requirements](contracts/persistence.md#windows-disk-boundary-25).
18
18
 
19
- **Status:** `1.6.2` accepts more completed-lesson and grade layouts, including
20
- behaviour points, which are validated and not reported as grades.
19
+ **Status:** `1.6.4` renders message bodies in linear time, so a crafted body can
20
+ no longer stall a client, and keeps the 1.6.2 completed-lesson and grade layouts,
21
+ including behaviour points, which are validated and not reported as grades.
21
22
  Class free days, typed capability outcomes and the live-check freshness watchdog
22
23
  remain available. See [verification](VERIFICATION.md).
23
24
  The stable 1.x API does not guarantee every school layout. School features depend
@@ -29,7 +30,7 @@ See [limitations](#supported-features-and-limitations) below.
29
30
  Install the exact release from PyPI after its gated publication completes:
30
31
 
31
32
  ```sh
32
- python -m pip install librus-python-api==1.6.2
33
+ python -m pip install librus-python-api==1.6.4
33
34
  ```
34
35
 
35
36
  Pin the exact version qualified by your application. Before publication, use a
@@ -45,7 +46,7 @@ python -m pip install .
45
46
  To install a locally built wheel instead:
46
47
 
47
48
  ```sh
48
- python -m pip install ./dist/librus_python_api-1.6.2-py3-none-any.whl
49
+ python -m pip install ./dist/librus_python_api-1.6.4-py3-none-any.whl
49
50
  ```
50
51
 
51
52
  No CLI or background process is installed: import the library in your own program.
@@ -224,7 +225,7 @@ Completed lessons may be disabled by the school. Behaviour notes are not
224
225
  implemented. Some recipient, archive and receipt layouts remain
225
226
  unqualified; backend acceptance is not proof of delivery. Tests cover supported
226
227
  contracts, not every school or role. Detailed coverage is in the
227
- [verification log][verification] and [roadmap][roadmap].
228
+ [verification log][verification]; open work is tracked in [issues][issues].
228
229
 
229
230
  ## Development and support
230
231
 
@@ -239,7 +240,6 @@ MIT licensed. See [LICENSE][license].
239
240
  [api]: https://github.com/krzysztofbury/librus-python-api/blob/main/API.md
240
241
  [upgrade]: https://github.com/krzysztofbury/librus-python-api/blob/main/contracts/account-context.md
241
242
  [verification]: https://github.com/krzysztofbury/librus-python-api/blob/main/VERIFICATION.md
242
- [roadmap]: https://github.com/krzysztofbury/librus-python-api/blob/main/TODO.md
243
243
  [contributing]: https://github.com/krzysztofbury/librus-python-api/blob/main/CONTRIBUTING.md
244
244
  [issues]: https://github.com/krzysztofbury/librus-python-api/issues
245
245
  [security]: https://github.com/krzysztofbury/librus-python-api/blob/main/SECURITY.md
@@ -1,7 +1,9 @@
1
1
  # Security
2
2
 
3
3
  Please do not report vulnerabilities or expose school data in public issues.
4
- Send security reports privately to [contact@datacraze.io](mailto:contact@datacraze.io).
4
+ Report vulnerabilities privately through the repository's **Security** tab
5
+ (**Report a vulnerability**) or by email to
6
+ [contact@datacraze.io](mailto:contact@datacraze.io).
5
7
 
6
8
  Include the affected version or commit, steps to reproduce, expected and
7
9
  observed behavior, and impact. Redact credentials, context keys, cookies, tokens, and any
@@ -11,3 +13,11 @@ The [client API security review](contracts/api-security-review.md) maps the
11
13
  outbound-client threat boundary and existing controls to Snyk's OWASP API guidance.
12
14
  It also records consumer authorization/serialization responsibilities and local
13
15
  filesystem/persistence limits; it is not a third-party security certification.
16
+
17
+ ## Repository protections
18
+
19
+ The default branch requires a pull request and the CI checks, and refuses force
20
+ pushes and deletion. Secret scanning with push protection, Dependabot alerts and
21
+ security updates, CodeQL default setup and private vulnerability reporting are
22
+ enabled. Workflows default to a read-only token and need approval before running
23
+ for outside contributors.
@@ -21,7 +21,7 @@ classifiers = [
21
21
  "Programming Language :: Python :: 3.14",
22
22
  "Typing :: Typed",
23
23
  ]
24
- dependencies = ["aiohttp>=3.14.2,<4", "lxml>=6.1,<7", "pydantic>=2.12,<3", "yarl>=1.20,<2", "pywin32>=312,<400; sys_platform == 'win32'", "tzdata>=2026.5; sys_platform == 'win32'"]
24
+ dependencies = ["aiohttp>=3.14.4,<4", "lxml>=6.1,<7", "pydantic>=2.12,<3", "yarl>=1.20,<2", "pywin32>=312,<400; sys_platform == 'win32'", "tzdata>=2026.5; sys_platform == 'win32'"]
25
25
 
26
26
  [project.urls]
27
27
  Repository = "https://github.com/krzysztofbury/librus-python-api"
@@ -138,7 +138,7 @@ from librus_python_api.models import (
138
138
  from librus_python_api.sending import SendAttempt
139
139
  from librus_python_api.service import AccountClient, LibrusService
140
140
 
141
- __version__ = "1.6.2"
141
+ __version__ = "1.6.4"
142
142
 
143
143
  __all__ = [
144
144
  "ArchiveAbsences",
@@ -11,11 +11,8 @@ from librus_python_api.exceptions import ErrorKind, LibrusError
11
11
  from librus_python_api.markup import text
12
12
  from librus_python_api.parsers import parse_html_document
13
13
 
14
- _XML_PREFIX = re.compile(
15
- r"\s*(?:(?:<!--.*?-->|<\?(?!xml\b).*?\?>)\s*)*"
16
- r"(?:<\?xml\b|<Message(?=[\s/>]))",
17
- re.DOTALL,
18
- )
14
+ _XML_START = re.compile(r"<\?xml\b|<Message(?=[\s/>])")
15
+ _XML_DECLARATION = re.compile(r"<\?xml\b")
19
16
 
20
17
 
21
18
  def render_body(encoded: str) -> str:
@@ -34,7 +31,7 @@ def render_body(encoded: str) -> str:
34
31
  decoded = decoded.removeprefix("\ufeff")
35
32
  # XML comments/PIs and a UTF-8 BOM must not route a Message wrapper through
36
33
  # the permissive HTML renderer, losing CDATA or bypassing Content checks.
37
- if _XML_PREFIX.match(decoded):
34
+ if _xml_prefixed(decoded):
38
35
  decoded = _xml_content(decoded)
39
36
  if not decoded:
40
37
  return ""
@@ -47,6 +44,36 @@ def render_body(encoded: str) -> str:
47
44
  )
48
45
 
49
46
 
47
+ def _xml_prefixed(decoded: str) -> bool:
48
+ """Whether whitespace, comments and non-declaration PIs precede an XML start.
49
+
50
+ A comment or PI may end at any later terminator, as the lazy pattern this
51
+ replaces allowed, but one left-to-right pass decides it: once an item has
52
+ opened, every later terminator closes it. A regex backtracks exponentially
53
+ over a run of items with no XML start after it.
54
+ """
55
+ starts = bytearray(len(decoded) + 1)
56
+ starts[0] = 1
57
+ comment_from = pi_from = len(decoded) + 1
58
+ for index, char in enumerate(decoded):
59
+ if starts[index]:
60
+ if char.isspace():
61
+ starts[index + 1] = 1
62
+ elif _XML_START.match(decoded, index):
63
+ return True
64
+ elif decoded.startswith("<!--", index):
65
+ comment_from = min(comment_from, index + 4)
66
+ elif decoded.startswith("<?", index) and not _XML_DECLARATION.match(
67
+ decoded, index
68
+ ):
69
+ pi_from = min(pi_from, index + 2)
70
+ if index >= comment_from and decoded.startswith("-->", index):
71
+ starts[index + 3] = 1
72
+ if index >= pi_from and decoded.startswith("?>", index):
73
+ starts[index + 2] = 1
74
+ return False
75
+
76
+
50
77
  def _xml_content(decoded: str) -> str:
51
78
  parser = etree.XMLParser(
52
79
  no_network=True,
@@ -1,5 +1,5 @@
1
1
  version = 1
2
- revision = 3
2
+ revision = 5
3
3
  requires-python = ">=3.13"
4
4
  resolution-markers = [
5
5
  "python_full_version >= '3.15'",
@@ -275,11 +275,11 @@ filecache = [
275
275
 
276
276
  [[package]]
277
277
  name = "certifi"
278
- version = "2026.7.22"
278
+ version = "2026.10.19"
279
279
  source = { registry = "https://pypi.org/simple" }
280
- sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" }
280
+ sdist = { url = "https://files.pythonhosted.org/packages/a6/bd/1436c50cc46699de0230fbaacc1d1de7bfec413176099949e9d5112e88f2/certifi-2026.10.19.tar.gz", hash = "sha256:035c1e85572b141bf1c3a3eff0e33a8eb74917522f2559075ff8d2fea780a1ae", size = 137598, upload-time = "2026-10-20T02:24:16.604Z" }
281
281
  wheels = [
282
- { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" },
282
+ { url = "https://files.pythonhosted.org/packages/c4/51/29046e48f693c54e5b7412216d447a1d8362c9653148d98c02cdf65fa733/certifi-2026.10.19-py3-none-any.whl", hash = "sha256:42d35340cc5b9ba78e818ddbff0c5b1c2b779de448a340fd1b989f5fa8670e1b", size = 136464, upload-time = "2026-10-20T02:24:15.072Z" },
283
283
  ]
284
284
 
285
285
  [[package]]
@@ -553,11 +553,11 @@ wheels = [
553
553
 
554
554
  [[package]]
555
555
  name = "filelock"
556
- version = "4.0.7"
556
+ version = "4.2.4"
557
557
  source = { registry = "https://pypi.org/simple" }
558
- sdist = { url = "https://files.pythonhosted.org/packages/35/f5/14097cca69f53794270d8c7970b48321636302affe3154c7e0ba114eeff9/filelock-4.0.7.tar.gz", hash = "sha256:da5915714a70b55d167fdc7e251ad91302b0a36816fb574dfafae8f4f2c9bb21", size = 248489, upload-time = "2026-09-29T23:04:05.995Z" }
558
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/28/3893324bcadf8f89b1616637468d766af3f625008c0735bf704fdfa86617/filelock-4.2.4.tar.gz", hash = "sha256:c6d8663535e1650ee430507b4ffbac7b464dd64b7647ea0112a9f406499d163d", size = 652444, upload-time = "2026-10-19T01:57:51.001Z" }
559
559
  wheels = [
560
- { url = "https://files.pythonhosted.org/packages/06/cf/1731c0dd182ee35f80a39e63afb3d03056ea0dd561b7498932b10b2e34d9/filelock-4.0.7-py3-none-any.whl", hash = "sha256:a93c4d93269b339a6af4848342c7e940d0f9928ad95eff64764699e5f1bf8a6a", size = 109242, upload-time = "2026-09-29T23:04:04.696Z" },
560
+ { url = "https://files.pythonhosted.org/packages/e0/1b/5e74289a1878e666bb4b65fe5805a04cafcac2aa5f519ef1ead67ef91c56/filelock-4.2.4-py3-none-any.whl", hash = "sha256:301241438c57936ef93f4243a94f2559022268bfcf9b375a9a72eb0f8db8358e", size = 162475, upload-time = "2026-10-19T01:57:49.419Z" },
561
561
  ]
562
562
 
563
563
  [[package]]
@@ -931,7 +931,7 @@ dev = [
931
931
 
932
932
  [package.metadata]
933
933
  requires-dist = [
934
- { name = "aiohttp", specifier = ">=3.14.2,<4" },
934
+ { name = "aiohttp", specifier = ">=3.14.4,<4" },
935
935
  { name = "lxml", specifier = ">=6.1,<7" },
936
936
  { name = "pydantic", specifier = ">=2.12,<3" },
937
937
  { name = "pywin32", marker = "sys_platform == 'win32'", specifier = ">=312,<400" },
@@ -1518,7 +1518,7 @@ wheels = [
1518
1518
 
1519
1519
  [[package]]
1520
1520
  name = "pre-commit"
1521
- version = "4.6.2"
1521
+ version = "4.7.0"
1522
1522
  source = { registry = "https://pypi.org/simple" }
1523
1523
  dependencies = [
1524
1524
  { name = "cfgv" },
@@ -1527,9 +1527,9 @@ dependencies = [
1527
1527
  { name = "pyyaml" },
1528
1528
  { name = "virtualenv" },
1529
1529
  ]
1530
- sdist = { url = "https://files.pythonhosted.org/packages/74/89/1f3e8e1fc3e97de0fa963495832f581f025f29471602a309e48808244292/pre_commit-4.6.2.tar.gz", hash = "sha256:8f5d7bfb021ecdbcd9d49d89847082dd24172ccde534390081a679ad046e2441", size = 198670, upload-time = "2026-08-10T22:07:18.421Z" }
1530
+ sdist = { url = "https://files.pythonhosted.org/packages/3a/c3/6965c2690ddce885ec75121572d065a5b94c5a5d8d3e657927ecda9fd715/pre_commit-4.7.0.tar.gz", hash = "sha256:1f3deee12b914c8fa184bd2b5953391c2e4f36763e403d4a23dc9983fa7c6e86", size = 198120, upload-time = "2026-10-12T20:45:40.32Z" }
1531
1531
  wheels = [
1532
- { url = "https://files.pythonhosted.org/packages/45/e2/bbb7129c9e7999a6b8ee9cca3b66486c25c423ab5a75f34071798b74ce94/pre_commit-4.6.2-py2.py3-none-any.whl", hash = "sha256:e2dde9a75d3bce11bd3831c26d134df00a2803c1d818be6a0383c3dcda25dc4e", size = 226202, upload-time = "2026-08-10T22:07:16.942Z" },
1532
+ { url = "https://files.pythonhosted.org/packages/eb/83/ce0a196e4f7b20b11ce3aa1fc72fb973f64b7628d51c1b8aebad37fb7f81/pre_commit-4.7.0-py2.py3-none-any.whl", hash = "sha256:2e229038ad3656081b70c27913157e4b636ef306f643f024a8a8e3f0f7b2f877", size = 226261, upload-time = "2026-10-12T20:45:38.858Z" },
1533
1533
  ]
1534
1534
 
1535
1535
  [[package]]
@@ -2050,27 +2050,27 @@ wheels = [
2050
2050
 
2051
2051
  [[package]]
2052
2052
  name = "ruff"
2053
- version = "0.17.0"
2054
- source = { registry = "https://pypi.org/simple" }
2055
- sdist = { url = "https://files.pythonhosted.org/packages/e9/a7/70debb024dfacda67b8e560cc7511f52b34b9348a8cc8c1ec23036dcd51d/ruff-0.17.0.tar.gz", hash = "sha256:5cd03240d8208a557c2a9655a5cb07ebe36aa6bb35065f97d48c1f6adef5a322", size = 5320554, upload-time = "2026-10-09T19:47:29.248Z" }
2056
- wheels = [
2057
- { url = "https://files.pythonhosted.org/packages/bc/f8/ee5ab9da6089eae2a33e6008b01deb1eda19992c1c8e10661e98cee1640f/ruff-0.17.0-py3-none-linux_armv6l.whl", hash = "sha256:0e271826af9a20d18c6cfae8c51e82959167c24859686ddd3eb9a7f0842ce81e", size = 10148907, upload-time = "2026-10-09T19:46:38.695Z" },
2058
- { url = "https://files.pythonhosted.org/packages/9f/d9/2f81fb5a9d580afbb11b1c8ff915233a11f2a1b27405d7991f183c5e1976/ruff-0.17.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5f0ca4a40f81403689c04f12966e22f44e329ae362072d8f1587b7bda87f603b", size = 10224208, upload-time = "2026-10-09T19:46:41.711Z" },
2059
- { url = "https://files.pythonhosted.org/packages/a7/20/643f3c8f75594f937b2bf74801241c56a2e2b8e139d24dff8b66b28cdd7f/ruff-0.17.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cbf7149e0927dc3295d5d64679a4765576eef71b00782b2ae969ef82274d6bb9", size = 9273324, upload-time = "2026-10-09T19:46:44.323Z" },
2060
- { url = "https://files.pythonhosted.org/packages/ec/91/627700b233d367736cb274f1bd0b47d1f2b12f68878192812bd875adadc3/ruff-0.17.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13ee90156522998c3037059d8f66885c8adeeaf7643bdce2caceee196ecd23e0", size = 9953190, upload-time = "2026-10-09T19:46:47.155Z" },
2061
- { url = "https://files.pythonhosted.org/packages/cd/92/91f7b5ed39490f89d6cbf56e1f543c383667a725efa8e2c2dee0f01f5591/ruff-0.17.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3d8e4a002a94cd9d0dc48b51dc69d807a172b5b9bf2b668e656424dc5b55ead1", size = 9961746, upload-time = "2026-10-09T19:46:50.098Z" },
2062
- { url = "https://files.pythonhosted.org/packages/87/c5/7310f9fc63ce11ff6394edbd5e85433dfb0e14c9fbf6ccc97f1538491bc7/ruff-0.17.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c0b8a60c06a218c337e1161638d34757f83449243e2db161483ddf948e53ad14", size = 10625483, upload-time = "2026-10-09T19:46:53.379Z" },
2063
- { url = "https://files.pythonhosted.org/packages/a5/8d/97443f0dca4a03a0bc7629fd396fd494a1cb6666121e38c5075acb217d8f/ruff-0.17.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a330178bdffc4205dbf3bda11d93e059e388fd6546f8cdd304501a9160363c0d", size = 11350129, upload-time = "2026-10-09T19:46:56.486Z" },
2064
- { url = "https://files.pythonhosted.org/packages/9c/0a/c525efd9777be4b6b012e6969a3012648468e7e6c4b3e5b46af69f46e8eb/ruff-0.17.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7bb08489e234876fa2da67ae3ea938e9a2156da80293e0e4365abd6973d98329", size = 10906223, upload-time = "2026-10-09T19:47:00.263Z" },
2065
- { url = "https://files.pythonhosted.org/packages/2c/3c/4a01195d93420cad1175bedad13a515dc8a56f95a6e39789b92e582682f5/ruff-0.17.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc73e7c133e82d55b5f15897b2a442d72c0cb4a0c886c46801ce3c247150b60c", size = 10386314, upload-time = "2026-10-09T19:47:03.057Z" },
2066
- { url = "https://files.pythonhosted.org/packages/c7/72/1a3951665485a921f6375f91e754a1854d5a645d41acc3642668064ff64d/ruff-0.17.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:db4f74c533403ab70fe4007873f6ae0c9f94a8b03158cf48d78788e47cdbe399", size = 10565464, upload-time = "2026-10-09T19:47:05.831Z" },
2067
- { url = "https://files.pythonhosted.org/packages/90/8c/539b4d8c082f57e18db8ae2be85a460d77861c79dcd5798e32b536a6a06f/ruff-0.17.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3d8cc360e666d1914e47b0777c6906d70cf18891a55532bd0a16844195d70859", size = 10079468, upload-time = "2026-10-09T19:47:08.617Z" },
2068
- { url = "https://files.pythonhosted.org/packages/e0/b8/84286966db79434e8c26b585b0a0f6897cb3ab1c51a4aa4df10c28488b62/ruff-0.17.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d66de796b726c4801e05fa99a2a8d7a780e107be222486c304ab61765561e866", size = 9958556, upload-time = "2026-10-09T19:47:11.324Z" },
2069
- { url = "https://files.pythonhosted.org/packages/69/50/27b6eed27b83fcdd5bfa0d52b83231e29094754374698da404d094487ae3/ruff-0.17.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c3f268baf004aea944f040623327119527ea231af15f7fb7890e82cea0679589", size = 10352173, upload-time = "2026-10-09T19:47:14.188Z" },
2070
- { url = "https://files.pythonhosted.org/packages/2a/fa/955399fd13044cd827862044117d784a59e3196f6cce7424908ac9a7f914/ruff-0.17.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:864b6c1acb6b0bccf94b5a3938a1531fd09aaca5e5659a2e7bf0f3cf2a685540", size = 10791761, upload-time = "2026-10-09T19:47:16.931Z" },
2071
- { url = "https://files.pythonhosted.org/packages/ae/bf/024e01e1f6aec87768696725e648ed5b438941341eea8f8100beb681961f/ruff-0.17.0-py3-none-win32.whl", hash = "sha256:5e50aa5b84decd9fe5b0bb0e6f71c3b592f1767ed09faa4b7207d933961e35cd", size = 9904112, upload-time = "2026-10-09T19:47:19.75Z" },
2072
- { url = "https://files.pythonhosted.org/packages/cc/77/1ee73df41dcc8d1cdb686ee4bc46ea29704ea175feb6b95c78420f631ab8/ruff-0.17.0-py3-none-win_amd64.whl", hash = "sha256:8ab76bcda86dfd28e13776cb5de3c7bcdcf1ae3d37ed761113d1a5a415dc134c", size = 10115585, upload-time = "2026-10-09T19:47:22.698Z" },
2073
- { url = "https://files.pythonhosted.org/packages/fd/71/eb4f0ccc844aece56963e8578df9c95d4c00f580547d52329d4035d3af18/ruff-0.17.0-py3-none-win_arm64.whl", hash = "sha256:c154c73ff43f9854395e24cac507af13078962e53d2b511605058d22af1fdb88", size = 9872035, upload-time = "2026-10-09T19:47:26.306Z" },
2053
+ version = "0.17.1"
2054
+ source = { registry = "https://pypi.org/simple" }
2055
+ sdist = { url = "https://files.pythonhosted.org/packages/f9/e7/e6d5749983b020e6cb1055abec0a6f361a8e94f0184cdcf56d4760995f02/ruff-0.17.1.tar.gz", hash = "sha256:5bb796c5112e9fb9527f2ef2faa0a130d6312d19de92bcfaddd70dbb2d41020f", size = 5323349, upload-time = "2026-10-15T15:23:18.64Z" }
2056
+ wheels = [
2057
+ { url = "https://files.pythonhosted.org/packages/2e/78/90c46ca6250709f7c37e397b60d06061adee8d0f7d13959ed6a8e49b14fd/ruff-0.17.1-py3-none-linux_armv6l.whl", hash = "sha256:1796c41eafec66f4635125dd1e074a7aa27b8d20c187d484360acf11d247fc4a", size = 10147528, upload-time = "2026-10-15T15:22:33.667Z" },
2058
+ { url = "https://files.pythonhosted.org/packages/aa/d2/a2e28bc35b083ae51fa07dfba5280a667c8b01ecaee7802fc8849dcd5c46/ruff-0.17.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8eadb09a788010be29c895418bdfa513e4e1c65e64b94e5b19e4cd381f6cc7fb", size = 10225974, upload-time = "2026-10-15T15:22:36.328Z" },
2059
+ { url = "https://files.pythonhosted.org/packages/8e/71/7a9029a81c8206ee01a98f6cfeeb56e6c095af86857581a37045c46d7e94/ruff-0.17.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:29b454f18ca3eeaf37fbf140841dcf27d36109e29dee9aae8acecb68c26c6f74", size = 9270361, upload-time = "2026-10-15T15:22:38.651Z" },
2060
+ { url = "https://files.pythonhosted.org/packages/c0/8c/e075e9c917cb0575297880c96768304e881240171c870cbd8d9b7b54bec2/ruff-0.17.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:872e03dd231744d6c5ec5c8af6be52b71323fc6ee096e4c8b34d9f844ee3b978", size = 9938300, upload-time = "2026-10-15T15:22:41.284Z" },
2061
+ { url = "https://files.pythonhosted.org/packages/87/27/0b4bb926a49de2383f161272b4d863571a4c7ecdbf595a6bc58813a13367/ruff-0.17.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4c0d6cb5f8fbac77eec9af49f4fc0eaf856b05545e372b0694d24b4e76b1c640", size = 9948464, upload-time = "2026-10-15T15:22:43.681Z" },
2062
+ { url = "https://files.pythonhosted.org/packages/14/27/518d57e0e02aecb8ea4acac6b57b51a53173d25fa734952bfdd7d531e88e/ruff-0.17.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78beced4a3c94dfedbee4b999b95b6a27b35820fdc0afbacc6d37a8da26c1cd1", size = 10638639, upload-time = "2026-10-15T15:22:46.081Z" },
2063
+ { url = "https://files.pythonhosted.org/packages/01/82/d89a4e498ade59e6cee52d67f8da569019372212b87468c1fdeae884601c/ruff-0.17.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7feefc63921202894f0e158b0e32f45a5950d70a7fb693ccb9264816c25b1c05", size = 11337238, upload-time = "2026-10-15T15:22:48.894Z" },
2064
+ { url = "https://files.pythonhosted.org/packages/a2/7c/3b0fdef9352656317c977086fe1d802902406e4f2a0d8578da90c0e6d1c5/ruff-0.17.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:684154861a819bf1f433669d2a0ce9c3e54677c65b8ebb866cc2a7ec86345785", size = 10896327, upload-time = "2026-10-15T15:22:51.45Z" },
2065
+ { url = "https://files.pythonhosted.org/packages/62/86/5566290219708ce2928f3a29c4f843a1185b341ec6111c4631a33c5ceced/ruff-0.17.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6ef98e51de8ef38082f90653ceeb84944bd7673783eea13e1647c5b9b064189", size = 10350811, upload-time = "2026-10-15T15:22:54.401Z" },
2066
+ { url = "https://files.pythonhosted.org/packages/ad/d2/ca8462dabb3c413990d49703ba306c30c862195a75448d080fa8fcce9775/ruff-0.17.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:8f9aa5b5bd5fa1937803ea609c8f153b8de39bf3e0e149897ae34b0c5b52f467", size = 10569994, upload-time = "2026-10-15T15:22:57.142Z" },
2067
+ { url = "https://files.pythonhosted.org/packages/e3/ad/dd1864c1f1db17c8cfb297ce70b352388e6977620f66f7182c52855407df/ruff-0.17.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:80e53b0e5faa3c5667f035daaa032893ae42b7cfe708c886d39ed131e54688ab", size = 10059672, upload-time = "2026-10-15T15:23:00.084Z" },
2068
+ { url = "https://files.pythonhosted.org/packages/65/45/3f56cf1d2168c2165cdd5ac6f9668aee66a4cfdd4caf2b2cc5ccb2b1d568/ruff-0.17.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:056c5d5fa0ac790051ae165d7035c7bf956d02de2db557f2e9b31d877a505fca", size = 9947803, upload-time = "2026-10-15T15:23:03.132Z" },
2069
+ { url = "https://files.pythonhosted.org/packages/aa/4b/5457cb2be4c1d1ea5e919558131a0f197a4a454a0530d9e30820e8206e4c/ruff-0.17.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bd8f5a55b10f371642eacd0e8665a42a8b97da15c9a7187c79e697b77b5e0ce4", size = 10351807, upload-time = "2026-10-15T15:23:05.628Z" },
2070
+ { url = "https://files.pythonhosted.org/packages/49/fb/db15b4f8fa24a1e36b99e1401f42ef6df16ee299a48670d5d262b771e7d7/ruff-0.17.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ce05e6f3973611d9550a600e04bb531061e368baba31610eb57c1509a8f21a0a", size = 10765716, upload-time = "2026-10-15T15:23:08.092Z" },
2071
+ { url = "https://files.pythonhosted.org/packages/0a/e8/1e186578076aff7a80735f625005fde678412ab609cd323ed182eb7d6b87/ruff-0.17.1-py3-none-win32.whl", hash = "sha256:1de0030a8aa78c65c5fecb84b2e5c390ac684c0452875a02afc7c083d49a496a", size = 9895093, upload-time = "2026-10-15T15:23:10.582Z" },
2072
+ { url = "https://files.pythonhosted.org/packages/6a/2e/da3d9589bae8e8b58f3a48bd5bc0b980232fb14a83bf02c3be7660f87c1b/ruff-0.17.1-py3-none-win_amd64.whl", hash = "sha256:5df76f16580fab58d0f6c98a3ac254a4abceb7b2489b8b678335869cff77b3d7", size = 10097955, upload-time = "2026-10-15T15:23:13.309Z" },
2073
+ { url = "https://files.pythonhosted.org/packages/df/c9/0f2a4dea06a5e12c1191ee3d96b1e1233797d0d5e18f97a2b909a95a9836/ruff-0.17.1-py3-none-win_arm64.whl", hash = "sha256:42400b9a6ff8515ff27796f07f92e0195be011c86e81e381606f58ea5502fef4", size = 9853542, upload-time = "2026-10-15T15:23:16.05Z" },
2074
2074
  ]
2075
2075
 
2076
2076
  [[package]]
@@ -2291,7 +2291,7 @@ wheels = [
2291
2291
 
2292
2292
  [[package]]
2293
2293
  name = "virtualenv"
2294
- version = "21.14.1"
2294
+ version = "21.14.13"
2295
2295
  source = { registry = "https://pypi.org/simple" }
2296
2296
  dependencies = [
2297
2297
  { name = "distlib" },
@@ -2300,9 +2300,9 @@ dependencies = [
2300
2300
  { name = "platformdirs" },
2301
2301
  { name = "python-discovery" },
2302
2302
  ]
2303
- sdist = { url = "https://files.pythonhosted.org/packages/50/67/b5d37693e5e666b68100db8fe34f00db28279330db55f739a3c799ff2449/virtualenv-21.14.1.tar.gz", hash = "sha256:719b189804e66678017d9f63bbfc590c44f6b96ab4829513806394f9b375929c", size = 5462838, upload-time = "2026-09-29T23:33:33.991Z" }
2303
+ sdist = { url = "https://files.pythonhosted.org/packages/70/aa/bff50daee0488e16d611258a9b6c5a1d0c9e128f8ca424e9f4e1c0444ef2/virtualenv-21.14.13.tar.gz", hash = "sha256:de5307ba4b9a0001d9c8ea02832d38f63ca432880f416c1bface61c4323fd905", size = 5570301, upload-time = "2026-10-22T20:10:16.952Z" }
2304
2304
  wheels = [
2305
- { url = "https://files.pythonhosted.org/packages/85/a8/22be2d14ef0d7b1e9e7dc1d839a85073fc20917be1747d6f1347d1cfc8c3/virtualenv-21.14.1-py3-none-any.whl", hash = "sha256:6fd04089fc0dc33549e7abdff70fc3b63d4e15799f2dbf3281f80d13b9fce522", size = 5486534, upload-time = "2026-09-29T23:33:32.333Z" },
2305
+ { url = "https://files.pythonhosted.org/packages/bb/a9/c10d8a44a29fb94a18b43fdf11f38caca53ae7c9406f11a72b1cb8f624f1/virtualenv-21.14.13-py3-none-any.whl", hash = "sha256:5fbb04138a4861580b78ba9110d529a26bc48fcc93c8ec36bc0202eeed96a788", size = 5557094, upload-time = "2026-10-22T20:10:15.096Z" },
2306
2306
  ]
2307
2307
 
2308
2308
  [[package]]