librus-python-api 1.6.3__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.3 → librus_python_api-1.6.4}/API.md +2 -2
  2. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/CHANGELOG.md +22 -0
  3. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/PKG-INFO +7 -6
  4. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/README.md +6 -5
  5. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/SECURITY.md +11 -1
  6. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/__init__.py +1 -1
  7. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/modern_body.py +33 -6
  8. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/uv.lock +30 -30
  9. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/.gitignore +0 -0
  10. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/LICENSE +0 -0
  11. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/pyproject.toml +0 -0
  12. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/_notification_bootstrap.py +0 -0
  13. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/_notification_codec.py +0 -0
  14. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/_storage.py +0 -0
  15. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/_windows_filesystem.py +0 -0
  16. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/announcements.py +0 -0
  17. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/attachment_routes.py +0 -0
  18. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/attachments.py +0 -0
  19. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/attendance.py +0 -0
  20. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/attendance_frequency.py +0 -0
  21. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/budget.py +0 -0
  22. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/checkpoint.py +0 -0
  23. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/class_free_days.py +0 -0
  24. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/completed_lessons.py +0 -0
  25. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/config.py +0 -0
  26. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/detail_fields.py +0 -0
  27. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/diagnostics.py +0 -0
  28. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/exceptions.py +0 -0
  29. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/files.py +0 -0
  30. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/formative_grades.py +0 -0
  31. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/grade_parsers.py +0 -0
  32. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/grade_records.py +0 -0
  33. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/homework_range.py +0 -0
  34. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/lifecycle.py +0 -0
  35. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/markup.py +0 -0
  36. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/message_content.py +0 -0
  37. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/messages.py +0 -0
  38. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/models.py +0 -0
  39. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/modern_mailbox.py +0 -0
  40. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/modern_messages.py +0 -0
  41. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/notification_models.py +0 -0
  42. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/notification_persistence.py +0 -0
  43. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/notification_workflow.py +0 -0
  44. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/notifications.py +0 -0
  45. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/parsers.py +0 -0
  46. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/parsing.py +0 -0
  47. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/persistence.py +0 -0
  48. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/py.typed +0 -0
  49. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/recipients.py +0 -0
  50. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/scheduler.py +0 -0
  51. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/school_reads.py +0 -0
  52. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/school_year_archive.py +0 -0
  53. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/sending.py +0 -0
  54. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/service.py +0 -0
  55. {librus_python_api-1.6.3 → librus_python_api-1.6.4}/src/librus_python_api/timetable.py +0 -0
  56. {librus_python_api-1.6.3 → 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,27 @@
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
+
3
25
  ## 1.6.3 (2026-10-22) - Dependency security update
4
26
 
5
27
  Raises dependency floors and tooling pins past published advisories. Public
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: librus-python-api
3
- Version: 1.6.3
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
@@ -42,9 +42,10 @@ private ACLs. Windows installations include `pywin32` and `tzdata` automatically
42
42
  Network paths, reparse-point paths and unsafe storage permissions fail closed.
43
43
  See [platform requirements](contracts/persistence.md#windows-disk-boundary-25).
44
44
 
45
- **Status:** `1.6.3` requires an aiohttp release without known advisories and
46
- keeps the 1.6.2 completed-lesson and grade layouts, including behaviour points,
47
- which are validated and not reported as grades. Class free days, typed capability outcomes and the live-check freshness watchdog
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.
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
50
51
  on what each account can access. See the [compatibility policy](API.md#compatibility-policy).
@@ -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.3
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.3-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.
@@ -16,9 +16,10 @@ 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.3` requires an aiohttp release without known advisories and
20
- keeps the 1.6.2 completed-lesson and grade layouts, including behaviour points,
21
- which are validated and not reported as grades. Class free days, typed capability outcomes and the live-check freshness watchdog
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.
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
24
25
  on what each account can access. See the [compatibility policy](API.md#compatibility-policy).
@@ -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.3
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.3-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.
@@ -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.
@@ -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.3"
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,
@@ -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]]
@@ -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.12"
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/c9/2c/1a69b3346931e9a7db9cc00cc0fa847c843a85fa7cf52a598f57261c68df/virtualenv-21.14.12.tar.gz", hash = "sha256:bbcfb78e36c22ad70c6917c4aecb5e63477d3c138e9f88c946f320e31859c76a", size = 5562800, upload-time = "2026-10-21T01:58:51.724Z" }
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/a0/92/f3b8af2d36ddec4dc61dc25655dc010e419ff456cb4ca0e720c8112f1047/virtualenv-21.14.12-py3-none-any.whl", hash = "sha256:7820aeaef23f1f59a96458edfc7f9d3deac937155718de15f1c9a64471e15551", size = 5556824, upload-time = "2026-10-21T01:58:48.505Z" },
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]]