langgraph-checkpoint-postgres 3.1.0a1__tar.gz → 3.1.0a4__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 (26) hide show
  1. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a4}/PKG-INFO +2 -2
  2. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a4}/langgraph/checkpoint/postgres/__init__.py +108 -37
  3. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a4}/langgraph/checkpoint/postgres/aio.py +90 -34
  4. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a4}/langgraph/checkpoint/postgres/base.py +257 -112
  5. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a4}/pyproject.toml +2 -2
  6. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a4}/uv.lock +86 -69
  7. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a4}/.gitignore +0 -0
  8. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a4}/LICENSE +0 -0
  9. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a4}/Makefile +0 -0
  10. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a4}/README.md +0 -0
  11. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a4}/langgraph/checkpoint/postgres/_ainternal.py +0 -0
  12. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a4}/langgraph/checkpoint/postgres/_internal.py +0 -0
  13. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a4}/langgraph/checkpoint/postgres/py.typed +0 -0
  14. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a4}/langgraph/checkpoint/postgres/shallow.py +0 -0
  15. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a4}/langgraph/store/postgres/__init__.py +0 -0
  16. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a4}/langgraph/store/postgres/aio.py +0 -0
  17. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a4}/langgraph/store/postgres/base.py +0 -0
  18. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a4}/langgraph/store/postgres/py.typed +0 -0
  19. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a4}/tests/__init__.py +0 -0
  20. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a4}/tests/compose-postgres.yml +0 -0
  21. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a4}/tests/conftest.py +0 -0
  22. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a4}/tests/embed_test_utils.py +0 -0
  23. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a4}/tests/test_async.py +0 -0
  24. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a4}/tests/test_async_store.py +0 -0
  25. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a4}/tests/test_store.py +0 -0
  26. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a4}/tests/test_sync.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langgraph-checkpoint-postgres
3
- Version: 3.1.0a1
3
+ Version: 3.1.0a4
4
4
  Summary: Library with a Postgres implementation of LangGraph checkpoint saver.
5
5
  Project-URL: Source, https://github.com/langchain-ai/langgraph/tree/main/libs/checkpoint-postgres
6
6
  Project-URL: Twitter, https://x.com/langchain_oss
@@ -9,7 +9,7 @@ Project-URL: Reddit, https://www.reddit.com/r/LangChain/
9
9
  License-Expression: MIT
10
10
  License-File: LICENSE
11
11
  Requires-Python: >=3.10
12
- Requires-Dist: langgraph-checkpoint<5.0.0,>=4.1.0a1
12
+ Requires-Dist: langgraph-checkpoint<5.0.0,>=4.1.0a4
13
13
  Requires-Dist: orjson>=3.11.5
14
14
  Requires-Dist: psycopg-pool>=3.2.0
15
15
  Requires-Dist: psycopg>=3.2.0
@@ -2,23 +2,23 @@ from __future__ import annotations
2
2
 
3
3
  import threading
4
4
  from collections import defaultdict
5
- from collections.abc import Iterator, Sequence
5
+ from collections.abc import Iterator, Mapping, Sequence
6
6
  from contextlib import contextmanager
7
7
  from typing import Any, cast
8
8
 
9
9
  from langchain_core.runnables import RunnableConfig
10
10
  from langgraph.checkpoint.base import (
11
- DELTA_SENTINEL,
12
11
  WRITES_IDX_MAP,
13
12
  ChannelVersions,
14
13
  Checkpoint,
15
14
  CheckpointMetadata,
16
15
  CheckpointTuple,
17
- _ChannelWritesHistory,
16
+ DeltaChannelHistory,
18
17
  get_checkpoint_id,
19
18
  get_serializable_checkpoint_metadata,
20
19
  )
21
20
  from langgraph.checkpoint.serde.base import SerializerProtocol
21
+ from langgraph.checkpoint.serde.types import _DeltaSnapshot
22
22
  from psycopg import Capabilities, Connection, Cursor, Pipeline
23
23
  from psycopg.rows import DictRow, dict_row
24
24
  from psycopg.types.json import Jsonb
@@ -26,9 +26,11 @@ from psycopg_pool import ConnectionPool
26
26
 
27
27
  from langgraph.checkpoint.postgres import _internal
28
28
  from langgraph.checkpoint.postgres.base import (
29
- SELECT_DELTA_COMBINED_SQL,
29
+ _DELTA_PAGE_SIZE,
30
30
  BasePostgresSaver,
31
- _DeltaCombinedRow,
31
+ _build_delta_stage1_sql,
32
+ _build_delta_stage2_sql,
33
+ _DeltaStage2Row,
32
34
  )
33
35
  from langgraph.checkpoint.postgres.shallow import ShallowPostgresSaver
34
36
 
@@ -308,7 +310,10 @@ class PostgresSaver(BasePostgresSaver):
308
310
  # others are stored in blobs table
309
311
  blob_values = {}
310
312
  for k, v in checkpoint["channel_values"].items():
311
- if v is None or isinstance(v, (str, int, float, bool)):
313
+ if isinstance(v, _DeltaSnapshot):
314
+ blob_values[k] = copy["channel_values"].pop(k)
315
+ copy["channel_values"][k] = True
316
+ elif v is None or isinstance(v, (str, int, float, bool)):
312
317
  pass
313
318
  else:
314
319
  blob_values[k] = copy["channel_values"].pop(k)
@@ -436,46 +441,112 @@ class PostgresSaver(BasePostgresSaver):
436
441
  with conn.cursor(binary=True, row_factory=dict_row) as cur:
437
442
  yield cur
438
443
 
439
- def _get_channel_writes_history(
440
- self, config: RunnableConfig, channel: str
441
- ) -> _ChannelWritesHistory:
442
- """Fast-path override of `BaseCheckpointSaver._get_channel_writes_history`.
443
-
444
- One combined UNION ALL query (`SELECT_DELTA_COMBINED_SQL`) fetches rows
445
- from `checkpoints`, `checkpoint_writes`, and `checkpoint_blobs` in a
446
- single roundtrip; the ancestor walk runs in Python.
444
+ def get_delta_channel_history(
445
+ self, *, config: RunnableConfig, channels: Sequence[str]
446
+ ) -> Mapping[str, DeltaChannelHistory]:
447
+ """Fast-path override of `BaseCheckpointSaver.get_delta_channel_history`.
448
+
449
+ Two-stage query, both stages cover ALL requested channels:
450
+
451
+ * Stage 1 (paged): dynamic SELECT over `checkpoints` with K parallel
452
+ JSONB key lookups (one column pair per channel) — no subquery, no
453
+ aggregation. Pages newest-first by `checkpoint_id` with a cursor;
454
+ page size is `_DELTA_PAGE_SIZE`. Stops paging when every channel
455
+ has found its seed or the chain is exhausted.
456
+
457
+ * Stage 2 (per-channel UNION ALL): one branch per channel reading
458
+ `checkpoint_writes` filtered to that channel's specific
459
+ `chain_cids`, plus one branch per channel that has a seed reading
460
+ `checkpoint_blobs` for that channel + version. Avoids the
461
+ over-fetch of a single `channel = ANY(channels)` filter when
462
+ channels have different chain depths.
447
463
  """
464
+ if not channels:
465
+ return {}
466
+ channels = list(channels)
448
467
  thread_id = config["configurable"]["thread_id"]
449
468
  checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
450
469
  checkpoint_id = get_checkpoint_id(config)
451
470
  if checkpoint_id is None:
452
- # Caller didn't specify a target — resolve to the latest
453
- # checkpoint on the thread. `get_tuple` without `checkpoint_id`
454
- # returns the newest; its config carries the resolved id.
455
471
  target = self.get_tuple(config)
456
472
  if target is None:
457
- return _ChannelWritesHistory(seed=DELTA_SENTINEL, writes=[])
473
+ return {ch: {"writes": []} for ch in channels}
458
474
  checkpoint_id = target.config["configurable"]["checkpoint_id"]
475
+
476
+ # Stage 1: paged K-JSONB-lookup scan, walking the parent chain in
477
+ # Python after each page. Stops as soon as every channel has its seed.
478
+ stage1_sql = _build_delta_stage1_sql(channels, paged=True)
479
+ parent_of: dict[str, str | None] = {}
480
+ ver_by_i_by_cid: list[dict[str, str | None]] = [{} for _ in channels]
481
+ hs_by_i_by_cid: list[dict[str, bool]] = [{} for _ in channels]
482
+ chain_by_ch: dict[str, list[str]] = {ch: [] for ch in channels}
483
+ seed_ver_by_ch: dict[str, str | None] = {ch: None for ch in channels}
484
+ walk_cursor_by_ch: dict[str, str | None] = {}
485
+ seeded: set[str] = set()
486
+ cursor: str | None = None
487
+
459
488
  with self._cursor() as cur:
460
- cur.execute(
461
- SELECT_DELTA_COMBINED_SQL,
462
- (
463
- channel,
464
- thread_id,
465
- checkpoint_ns,
466
- thread_id,
467
- checkpoint_ns,
468
- channel,
469
- thread_id,
470
- checkpoint_ns,
471
- channel,
472
- ),
473
- )
474
- rows = cur.fetchall()
475
- return self._build_delta_channel_writes_history(
476
- channel=channel,
477
- target_id=checkpoint_id,
478
- rows=cast("list[_DeltaCombinedRow]", rows),
489
+ while True:
490
+ stage1_params: list[Any] = []
491
+ for ch in channels:
492
+ stage1_params.extend([ch, ch])
493
+ stage1_params.extend(
494
+ [thread_id, checkpoint_ns, cursor, cursor, _DELTA_PAGE_SIZE]
495
+ )
496
+ cur.execute(stage1_sql, stage1_params)
497
+ page = cur.fetchall()
498
+ if not page:
499
+ break
500
+ oldest = self._ingest_stage1_page(
501
+ cast("list[Mapping[str, Any]]", page),
502
+ channels,
503
+ parent_of,
504
+ ver_by_i_by_cid,
505
+ hs_by_i_by_cid,
506
+ )
507
+ self._try_advance_walks(
508
+ checkpoint_id,
509
+ channels,
510
+ parent_of,
511
+ ver_by_i_by_cid,
512
+ hs_by_i_by_cid,
513
+ chain_by_ch,
514
+ seed_ver_by_ch,
515
+ walk_cursor_by_ch,
516
+ seeded,
517
+ )
518
+ # Stop if every channel is seeded, or the page was short
519
+ # (chain exhausted — no more rows to fetch).
520
+ if len(seeded) == len(channels) or len(page) < _DELTA_PAGE_SIZE:
521
+ break
522
+ cursor = oldest
523
+
524
+ # Stage 2: per-channel UNION ALL — one writes branch per channel
525
+ # with non-empty chain, plus one blob branch per seeded channel.
526
+ channels_with_chain = [ch for ch in channels if chain_by_ch[ch]]
527
+ channels_with_seed = [ch for ch in channels if seed_ver_by_ch[ch] is not None]
528
+ stage2_sql = _build_delta_stage2_sql(
529
+ channels_with_chain=channels_with_chain,
530
+ channels_with_seed=channels_with_seed,
531
+ )
532
+
533
+ if stage2_sql:
534
+ stage2_params: list[Any] = []
535
+ for ch in channels_with_chain:
536
+ stage2_params.extend([thread_id, checkpoint_ns, ch, chain_by_ch[ch]])
537
+ for ch in channels_with_seed:
538
+ stage2_params.extend([thread_id, checkpoint_ns, ch, seed_ver_by_ch[ch]])
539
+ with self._cursor() as cur:
540
+ cur.execute(stage2_sql, stage2_params)
541
+ stage2_rows = cur.fetchall()
542
+ else:
543
+ stage2_rows = []
544
+
545
+ return self._build_delta_channels_writes_history(
546
+ channels=channels,
547
+ chain_by_ch=chain_by_ch,
548
+ seed_ver_by_ch=seed_ver_by_ch,
549
+ stage2_rows=cast("list[_DeltaStage2Row]", stage2_rows),
479
550
  )
480
551
 
481
552
  def _load_checkpoint_tuple(self, value: DictRow) -> CheckpointTuple:
@@ -2,23 +2,23 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  from collections import defaultdict
5
- from collections.abc import AsyncIterator, Iterator, Sequence
5
+ from collections.abc import AsyncIterator, Iterator, Mapping, Sequence
6
6
  from contextlib import asynccontextmanager
7
7
  from typing import Any, cast
8
8
 
9
9
  from langchain_core.runnables import RunnableConfig
10
10
  from langgraph.checkpoint.base import (
11
- DELTA_SENTINEL,
12
11
  WRITES_IDX_MAP,
13
12
  ChannelVersions,
14
13
  Checkpoint,
15
14
  CheckpointMetadata,
16
15
  CheckpointTuple,
17
- _ChannelWritesHistory,
16
+ DeltaChannelHistory,
18
17
  get_checkpoint_id,
19
18
  get_serializable_checkpoint_metadata,
20
19
  )
21
20
  from langgraph.checkpoint.serde.base import SerializerProtocol
21
+ from langgraph.checkpoint.serde.types import _DeltaSnapshot
22
22
  from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
23
23
  from psycopg.rows import DictRow, dict_row
24
24
  from psycopg.types.json import Jsonb
@@ -26,9 +26,11 @@ from psycopg_pool import AsyncConnectionPool
26
26
 
27
27
  from langgraph.checkpoint.postgres import _ainternal
28
28
  from langgraph.checkpoint.postgres.base import (
29
- SELECT_DELTA_COMBINED_SQL,
29
+ _DELTA_PAGE_SIZE,
30
30
  BasePostgresSaver,
31
- _DeltaCombinedRow,
31
+ _build_delta_stage1_sql,
32
+ _build_delta_stage2_sql,
33
+ _DeltaStage2Row,
32
34
  )
33
35
  from langgraph.checkpoint.postgres.shallow import AsyncShallowPostgresSaver
34
36
 
@@ -267,7 +269,10 @@ class AsyncPostgresSaver(BasePostgresSaver):
267
269
  # others are stored in blobs table
268
270
  blob_values = {}
269
271
  for k, v in checkpoint["channel_values"].items():
270
- if v is None or isinstance(v, (str, int, float, bool)):
272
+ if isinstance(v, _DeltaSnapshot):
273
+ blob_values[k] = copy["channel_values"].pop(k)
274
+ copy["channel_values"][k] = True
275
+ elif v is None or isinstance(v, (str, int, float, bool)):
271
276
  pass
272
277
  else:
273
278
  blob_values[k] = copy["channel_values"].pop(k)
@@ -397,44 +402,95 @@ class AsyncPostgresSaver(BasePostgresSaver):
397
402
  async with conn.cursor(binary=True, row_factory=dict_row) as cur:
398
403
  yield cur
399
404
 
400
- async def _aget_channel_writes_history(
401
- self, config: RunnableConfig, channel: str
402
- ) -> _ChannelWritesHistory:
403
- """Fast-path override of `BaseCheckpointSaver._aget_channel_writes_history`.
405
+ async def aget_delta_channel_history(
406
+ self, *, config: RunnableConfig, channels: Sequence[str]
407
+ ) -> Mapping[str, DeltaChannelHistory]:
408
+ """Fast-path override of `BaseCheckpointSaver.aget_delta_channel_history`.
404
409
 
405
- One combined UNION ALL query (`SELECT_DELTA_COMBINED_SQL`) fetches rows
406
- from `checkpoints`, `checkpoint_writes`, and `checkpoint_blobs` in a
407
- single roundtrip; rows are assembled by the shared pure helper on
408
- `BasePostgresSaver`.
410
+ See `PostgresSaver.get_delta_channel_history` for design notes; this is
411
+ the async equivalent with internal stage-1 paging and per-channel
412
+ UNION ALL stage-2.
409
413
  """
414
+ if not channels:
415
+ return {}
416
+ channels = list(channels)
410
417
  thread_id = config["configurable"]["thread_id"]
411
418
  checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
412
419
  checkpoint_id = get_checkpoint_id(config)
413
420
  if checkpoint_id is None:
414
421
  target = await self.aget_tuple(config)
415
422
  if target is None:
416
- return _ChannelWritesHistory(seed=DELTA_SENTINEL, writes=[])
423
+ return {ch: {"writes": []} for ch in channels}
417
424
  checkpoint_id = target.config["configurable"]["checkpoint_id"]
425
+
426
+ stage1_sql = _build_delta_stage1_sql(channels, paged=True)
427
+ parent_of: dict[str, str | None] = {}
428
+ ver_by_i_by_cid: list[dict[str, str | None]] = [{} for _ in channels]
429
+ hs_by_i_by_cid: list[dict[str, bool]] = [{} for _ in channels]
430
+ chain_by_ch: dict[str, list[str]] = {ch: [] for ch in channels}
431
+ seed_ver_by_ch: dict[str, str | None] = {ch: None for ch in channels}
432
+ walk_cursor_by_ch: dict[str, str | None] = {}
433
+ seeded: set[str] = set()
434
+ cursor: str | None = None
435
+
418
436
  async with self._cursor() as cur:
419
- await cur.execute(
420
- SELECT_DELTA_COMBINED_SQL,
421
- (
422
- channel,
423
- thread_id,
424
- checkpoint_ns,
425
- thread_id,
426
- checkpoint_ns,
427
- channel,
428
- thread_id,
429
- checkpoint_ns,
430
- channel,
431
- ),
432
- )
433
- rows = await cur.fetchall()
434
- return self._build_delta_channel_writes_history(
435
- channel=channel,
436
- target_id=checkpoint_id,
437
- rows=cast("list[_DeltaCombinedRow]", rows),
437
+ while True:
438
+ stage1_params: list[Any] = []
439
+ for ch in channels:
440
+ stage1_params.extend([ch, ch])
441
+ stage1_params.extend(
442
+ [thread_id, checkpoint_ns, cursor, cursor, _DELTA_PAGE_SIZE]
443
+ )
444
+ await cur.execute(stage1_sql, stage1_params)
445
+ page = await cur.fetchall()
446
+ if not page:
447
+ break
448
+ oldest = self._ingest_stage1_page(
449
+ cast("list[Mapping[str, Any]]", page),
450
+ channels,
451
+ parent_of,
452
+ ver_by_i_by_cid,
453
+ hs_by_i_by_cid,
454
+ )
455
+ self._try_advance_walks(
456
+ checkpoint_id,
457
+ channels,
458
+ parent_of,
459
+ ver_by_i_by_cid,
460
+ hs_by_i_by_cid,
461
+ chain_by_ch,
462
+ seed_ver_by_ch,
463
+ walk_cursor_by_ch,
464
+ seeded,
465
+ )
466
+ if len(seeded) == len(channels) or len(page) < _DELTA_PAGE_SIZE:
467
+ break
468
+ cursor = oldest
469
+
470
+ channels_with_chain = [ch for ch in channels if chain_by_ch[ch]]
471
+ channels_with_seed = [ch for ch in channels if seed_ver_by_ch[ch] is not None]
472
+ stage2_sql = _build_delta_stage2_sql(
473
+ channels_with_chain=channels_with_chain,
474
+ channels_with_seed=channels_with_seed,
475
+ )
476
+
477
+ if stage2_sql:
478
+ stage2_params: list[Any] = []
479
+ for ch in channels_with_chain:
480
+ stage2_params.extend([thread_id, checkpoint_ns, ch, chain_by_ch[ch]])
481
+ for ch in channels_with_seed:
482
+ stage2_params.extend([thread_id, checkpoint_ns, ch, seed_ver_by_ch[ch]])
483
+ async with self._cursor() as cur:
484
+ await cur.execute(stage2_sql, stage2_params)
485
+ stage2_rows = await cur.fetchall()
486
+ else:
487
+ stage2_rows = []
488
+
489
+ return self._build_delta_channels_writes_history(
490
+ channels=channels,
491
+ chain_by_ch=chain_by_ch,
492
+ seed_ver_by_ch=seed_ver_by_ch,
493
+ stage2_rows=cast("list[_DeltaStage2Row]", stage2_rows),
438
494
  )
439
495
 
440
496
  async def _load_checkpoint_tuple(self, value: DictRow) -> CheckpointTuple:
@@ -2,23 +2,26 @@ from __future__ import annotations
2
2
 
3
3
  import random
4
4
  import warnings
5
- from collections.abc import Sequence
5
+ from collections.abc import Mapping, Sequence
6
6
  from importlib.metadata import version as get_version
7
7
  from typing import Any, TypedDict, cast
8
8
 
9
9
  from langchain_core.runnables import RunnableConfig
10
10
  from langgraph.checkpoint.base import (
11
- DELTA_SENTINEL,
12
11
  WRITES_IDX_MAP,
13
12
  BaseCheckpointSaver,
14
13
  ChannelVersions,
14
+ DeltaChannelHistory,
15
15
  PendingWrite,
16
- _ChannelWritesHistory,
17
16
  get_checkpoint_id,
18
17
  )
19
18
  from langgraph.checkpoint.serde.types import TASKS
20
19
  from psycopg.types.json import Jsonb
21
20
 
21
+ # Page size for stage-1 paged scan in `get_delta_channel_history`. Internal
22
+ # constant — exposing this as a kwarg is left as a follow-up.
23
+ _DELTA_PAGE_SIZE = 1024
24
+
22
25
  MetadataInput = dict[str, Any] | None
23
26
 
24
27
  try:
@@ -156,60 +159,139 @@ INSERT_CHECKPOINT_WRITES_SQL = """
156
159
  """
157
160
 
158
161
 
159
- class _DeltaCombinedRow(TypedDict, total=False):
160
- """One row from `SELECT_DELTA_COMBINED_SQL` (a UNION ALL of three tables).
161
-
162
- Every row carries `_kind` ("p" / "w" / "b") plus whichever columns are
163
- relevant for that kind; irrelevant columns are NULL and typed as `None`.
164
- """
162
+ class _DeltaStage2Row(TypedDict, total=False):
163
+ """One row from `_build_delta_stage2_sql` (a UNION ALL of writes and blobs)."""
165
164
 
166
- _kind: str # always present: "p", "w", or "b"
167
- # checkpoint row ("p")
168
- checkpoint_id: str | None
169
- parent_checkpoint_id: str | None
170
- ver: str | None
171
- # write / blob rows ("w", "b")
165
+ _kind: str # "w" or "b"
166
+ checkpoint_id: str | None # "w" rows only
167
+ channel: str | None # set on both "w" and "b" rows
172
168
  type: str | None
173
169
  blob: bytes | None
174
- # write row only ("w")
175
- task_id: str | None
176
- idx: int | None
177
- # blob row only ("b")
178
- version: str | None
170
+ task_id: str | None # "w" rows only
171
+ idx: int | None # "w" rows only
172
+ version: str | None # "b" rows only
179
173
 
180
174
 
181
- # DeltaChannel reconstruction: one UNION ALL query fetches checkpoints,
182
- # writes, and blobs for `channel` in one roundtrip; the ancestor walk runs
183
- # in Python in `_build_delta_channel_writes_history`.
175
+ # Multi-channel two-stage DeltaChannel reconstruction.
184
176
  #
185
- # Parameter order: (channel, thread_id, checkpoint_ns,
186
- # thread_id, checkpoint_ns, channel,
187
- # thread_id, checkpoint_ns, channel)
188
- SELECT_DELTA_COMBINED_SQL = """
189
- SELECT 'p'::text AS _kind,
190
- checkpoint_id,
191
- parent_checkpoint_id,
192
- checkpoint -> 'channel_versions' ->> %s AS ver,
193
- NULL::text AS type,
194
- NULL::bytea AS blob,
195
- NULL::text AS task_id,
196
- NULL::int AS idx,
197
- NULL::text AS version
198
- FROM checkpoints
199
- WHERE thread_id = %s AND checkpoint_ns = %s
200
- UNION ALL
201
- SELECT 'w',
202
- checkpoint_id, NULL, NULL,
203
- type, blob, task_id, idx, NULL
204
- FROM checkpoint_writes
205
- WHERE thread_id = %s AND checkpoint_ns = %s AND channel = %s
206
- UNION ALL
207
- SELECT 'b',
208
- NULL, NULL, NULL,
209
- type, blob, NULL, NULL, version
210
- FROM checkpoint_blobs
211
- WHERE thread_id = %s AND checkpoint_ns = %s AND channel = %s
212
- """
177
+ # Stage 1 scans checkpoint metadata (no blob bytes) and emits one row per
178
+ # checkpoint with K parallel JSONB key lookups (one column pair per
179
+ # requested delta channel: ver_i / hs_i). No subqueries, no aggregation.
180
+ # Python walks the parent chain once across all channels.
181
+ #
182
+ # Stage 2 fetches all writes and the seed blobs for ALL channels in a
183
+ # single roundtrip via `channel = ANY(%s)` and chain/seed-version
184
+ # filtering.
185
+ #
186
+ # Empirical comparison vs an alternative "ship full channel_versions /
187
+ # channel_values JSONB and let Python pick" form (1000 checkpoints,
188
+ # 8 total channels in graph, 3 delta channels requested):
189
+ #
190
+ # Postgres execution: A=0.24ms vs B=0.38ms (both negligible)
191
+ # End-to-end latency: A=6.83ms vs B=2.28ms (B is 3.0x faster)
192
+ # Wire payload: A=836KB vs B=330KB (61% smaller)
193
+ # Buffer hits: identical (167 blocks)
194
+ #
195
+ # B (this dynamic-columns design) wins because it avoids JSONB
196
+ # serialization on the wire and JSONB-to-dict deserialization in
197
+ # psycopg. Even at K=8 (8 delta channels = 16 dynamic columns), B
198
+ # still beats A end-to-end (4.2ms vs 6.8ms).
199
+
200
+
201
+ def _build_delta_stage1_sql(channels: Sequence[str], *, paged: bool) -> str:
202
+ """Build stage 1 SQL with 2K parallel JSONB key lookups.
203
+
204
+ For channels=["messages", "files"] (with `paged=True`) the result is::
205
+
206
+ SELECT checkpoint_id, parent_checkpoint_id,
207
+ checkpoint -> 'channel_versions' ->> %s AS ver_0,
208
+ (checkpoint -> 'channel_values' -> %s) IS NOT NULL AS hs_0,
209
+ checkpoint -> 'channel_versions' ->> %s AS ver_1,
210
+ (checkpoint -> 'channel_values' -> %s) IS NOT NULL AS hs_1
211
+ FROM checkpoints
212
+ WHERE thread_id = %s AND checkpoint_ns = %s
213
+ AND (%s::text IS NULL OR checkpoint_id < %s)
214
+ ORDER BY checkpoint_id DESC
215
+ LIMIT %s
216
+
217
+ Channel names are passed as `%s` parameters (safe from SQL injection).
218
+ Only the column aliases `ver_i` / `hs_i` are interpolated into the
219
+ SQL string (i is bounded by len(channels) and uses safe identifiers).
220
+
221
+ Caller must extend params with `[ch_0, ch_0, ch_1, ch_1, ...,
222
+ thread_id, ns, cursor, cursor, page_size]` when `paged=True`.
223
+
224
+ When `paged=False`, the WHERE has no cursor predicate and there's no
225
+ LIMIT/ORDER BY — kept as a non-public helper for tests/diagnostics.
226
+ """
227
+ cols = []
228
+ for i in range(len(channels)):
229
+ cols.append(
230
+ f"checkpoint -> 'channel_versions' ->> %s AS ver_{i}, "
231
+ f"(checkpoint -> 'channel_values' -> %s) IS NOT NULL AS hs_{i}"
232
+ )
233
+ sql = (
234
+ "SELECT checkpoint_id, parent_checkpoint_id, "
235
+ + ", ".join(cols)
236
+ + " FROM checkpoints WHERE thread_id = %s AND checkpoint_ns = %s"
237
+ )
238
+ if paged:
239
+ sql += (
240
+ " AND (%s::text IS NULL OR checkpoint_id < %s)"
241
+ " ORDER BY checkpoint_id DESC LIMIT %s"
242
+ )
243
+ return sql
244
+
245
+
246
+ def _build_delta_stage2_sql(
247
+ *,
248
+ channels_with_chain: Sequence[str],
249
+ channels_with_seed: Sequence[str],
250
+ ) -> str:
251
+ """Build stage 2 SQL as a per-channel UNION ALL.
252
+
253
+ For each channel with a non-empty chain, emit one branch reading
254
+ `checkpoint_writes` for that specific channel + chain_cids. For each
255
+ channel with a seed_version, emit one branch reading `checkpoint_blobs`
256
+ for that channel + version. This avoids the over-fetch of the prior
257
+ `channel = ANY(channels) AND checkpoint_id = ANY(union)` form when
258
+ channels have different chain depths.
259
+
260
+ The caller must pass parameters in matching order:
261
+
262
+ for ch in channels_with_chain:
263
+ params += [thread_id, checkpoint_ns, ch, chain_cids[ch]]
264
+ for ch in channels_with_seed:
265
+ params += [thread_id, checkpoint_ns, ch, seed_version[ch]]
266
+
267
+ Returns an empty SQL string if both channel lists are empty (caller
268
+ must skip executing in that case).
269
+ """
270
+ branches: list[str] = []
271
+ for _ in channels_with_chain:
272
+ branches.append(
273
+ "SELECT 'w'::text AS _kind, "
274
+ "checkpoint_id, channel, "
275
+ "type, blob, task_id, idx, NULL::text AS version "
276
+ "FROM checkpoint_writes "
277
+ "WHERE thread_id = %s AND checkpoint_ns = %s AND channel = %s "
278
+ "AND checkpoint_id = ANY(%s)"
279
+ )
280
+ for _ in channels_with_seed:
281
+ branches.append(
282
+ "SELECT 'b'::text, NULL, channel, "
283
+ "type, blob, NULL, NULL, version "
284
+ "FROM checkpoint_blobs "
285
+ "WHERE thread_id = %s AND checkpoint_ns = %s AND channel = %s "
286
+ "AND version = %s"
287
+ )
288
+ return " UNION ALL ".join(branches)
289
+
290
+
291
+ # Stage 1 rows are dynamic-shape dicts: {checkpoint_id, parent_checkpoint_id,
292
+ # ver_0, hs_0, ver_1, hs_1, ...}. Walking is parameterized by the channel
293
+ # list to map indices back to channel names — no static TypedDict here.
294
+ # `dict[str, Any]` is the practical signature.
213
295
 
214
296
 
215
297
  class BasePostgresSaver(BaseCheckpointSaver[str]):
@@ -254,82 +336,145 @@ class BasePostgresSaver(BaseCheckpointSaver[str]):
254
336
  if t.decode() != "empty"
255
337
  }
256
338
 
257
- def _build_delta_channel_writes_history(
339
+ @staticmethod
340
+ def _ingest_stage1_page(
341
+ stage1_rows: Sequence[Mapping[str, Any]],
342
+ channels: Sequence[str],
343
+ parent_of: dict[str, str | None],
344
+ ver_by_i_by_cid: list[dict[str, str | None]],
345
+ hs_by_i_by_cid: list[dict[str, bool]],
346
+ ) -> str | None:
347
+ """Fold one stage-1 page into the running walk-state mappings.
348
+
349
+ Returns the oldest checkpoint_id seen on this page (smallest, since
350
+ pages come back DESC). Caller uses it as the cursor for the next
351
+ page (`AND checkpoint_id < cursor`).
352
+ """
353
+ oldest: str | None = None
354
+ for r in stage1_rows:
355
+ cid = cast(str, r["checkpoint_id"])
356
+ parent_of[cid] = cast("str | None", r["parent_checkpoint_id"])
357
+ for i in range(len(channels)):
358
+ ver_by_i_by_cid[i][cid] = cast("str | None", r.get(f"ver_{i}"))
359
+ hs_by_i_by_cid[i][cid] = bool(r.get(f"hs_{i}"))
360
+ # Rows are DESC; the last one is the smallest cid in the page.
361
+ oldest = cid
362
+ return oldest
363
+
364
+ @staticmethod
365
+ def _try_advance_walks(
366
+ target_id: str,
367
+ channels: Sequence[str],
368
+ parent_of: Mapping[str, str | None],
369
+ ver_by_i_by_cid: Sequence[Mapping[str, str | None]],
370
+ hs_by_i_by_cid: Sequence[Mapping[str, bool]],
371
+ chain_by_ch: dict[str, list[str]],
372
+ seed_ver_by_ch: dict[str, str | None],
373
+ walk_cursor_by_ch: dict[str, str | None],
374
+ seeded: set[str],
375
+ ) -> None:
376
+ """Advance each not-yet-seeded channel's walk as far as possible.
377
+
378
+ Uses the partial `parent_of` map accumulated so far. A walk stops
379
+ either because:
380
+ (a) it found a snapshot for its channel (channel becomes seeded),
381
+ (b) it reached a real root (parent_of[cid] is None — fully
382
+ materialized at this point), or
383
+ (c) the next ancestor cid isn't in `parent_of` yet (waiting for
384
+ a later page; the cursor stays put).
385
+
386
+ Mutates `chain_by_ch`, `seed_ver_by_ch`, `walk_cursor_by_ch`, and
387
+ `seeded` in place.
388
+ """
389
+ for i, ch in enumerate(channels):
390
+ if ch in seeded:
391
+ continue
392
+ # First-time entry: cursor starts at the target's parent.
393
+ if ch not in walk_cursor_by_ch:
394
+ walk_cursor_by_ch[ch] = parent_of.get(target_id)
395
+ cur_cid = walk_cursor_by_ch[ch]
396
+ ch_chain = chain_by_ch[ch]
397
+ hs_i = hs_by_i_by_cid[i]
398
+ ver_i = ver_by_i_by_cid[i]
399
+ while cur_cid is not None:
400
+ if cur_cid not in parent_of:
401
+ # Need more pages to continue this walk.
402
+ break
403
+ ch_chain.append(cur_cid)
404
+ if hs_i.get(cur_cid, False):
405
+ seed_ver_by_ch[ch] = ver_i.get(cur_cid)
406
+ seeded.add(ch)
407
+ cur_cid = None
408
+ break
409
+ cur_cid = parent_of[cur_cid]
410
+ walk_cursor_by_ch[ch] = cur_cid
411
+
412
+ def _build_delta_channels_writes_history(
258
413
  self,
259
414
  *,
260
- channel: str,
261
- target_id: str,
262
- rows: Sequence[_DeltaCombinedRow],
263
- ) -> _ChannelWritesHistory:
264
- """Reconstruct one delta channel's history from the combined UNION ALL rows.
265
-
266
- Pure data transform shared by sync (`PostgresSaver`) and async
267
- (`AsyncPostgresSaver`); both paths run `SELECT_DELTA_COMBINED_SQL`
268
- and feed the tagged rows here.
269
-
270
- Walk is newest → oldest from the target's parent. A non-sentinel
271
- blob in `checkpoint_blobs` (a pre-delta snapshot) terminates the
272
- walk and is returned as the seed so replay starts from it.
273
-
274
- Writes stored at `target_id` itself are pending writes for the next
275
- step and are excluded — the walk begins at the target's parent.
415
+ channels: Sequence[str],
416
+ chain_by_ch: Mapping[str, list[str]],
417
+ seed_ver_by_ch: Mapping[str, str | None],
418
+ stage2_rows: Sequence[_DeltaStage2Row],
419
+ ) -> dict[str, DeltaChannelHistory]:
420
+ """Demux stage 2 rows per channel; produce per-channel histories.
421
+
422
+ stage2_rows carry `channel` on every row. We build per-channel
423
+ `writes_by_cid` and per-channel `seed_blob` dicts, then assemble
424
+ a `DeltaChannelHistory` per requested channel. The `seed` key is omitted
425
+ when the walk reached root with no snapshot found, or when the
426
+ seed blob is sentinel "empty" — in both cases the consumer treats
427
+ absence as "start empty".
276
428
  """
277
- parent_of: dict[str, str | None] = {}
278
- ver_of: dict[str, str | None] = {}
279
- writes_by_cid: dict[str, list[tuple[str, bytes, str, int]]] = {}
280
- blob_by_ver: dict[str, tuple[str, bytes]] = {}
429
+ # writes_by_ch_by_cid[channel][cid] = list of (type, blob, task_id, idx)
430
+ writes_by_ch_by_cid: dict[str, dict[str, list[tuple[str, bytes, str, int]]]] = {
431
+ ch: {} for ch in channels
432
+ }
433
+ # seed_blob_by_ver[(channel, version)] = (type, blob)
434
+ seed_blob_by_ver: dict[tuple[str, str], tuple[str, bytes]] = {}
281
435
 
282
- for r in rows:
436
+ for r in stage2_rows:
437
+ ch = cast(str, r["channel"])
283
438
  kind = r["_kind"]
284
- if kind == "p":
285
- cid = cast(str, r["checkpoint_id"])
286
- parent_of[cid] = r["parent_checkpoint_id"]
287
- ver_of[cid] = r["ver"]
288
- elif kind == "w":
439
+ if kind == "w":
289
440
  cid = cast(str, r["checkpoint_id"])
290
- writes_by_cid.setdefault(cid, []).append(
441
+ writes_by_ch_by_cid.setdefault(ch, {}).setdefault(cid, []).append(
291
442
  cast(
292
443
  "tuple[str, bytes, str, int]",
293
444
  (r["type"], r["blob"], r["task_id"], r["idx"]),
294
445
  )
295
446
  )
296
447
  else: # kind == "b"
297
- blob_by_ver[cast(str, r["version"])] = cast(
448
+ ver = cast(str, r["version"])
449
+ seed_blob_by_ver[(ch, ver)] = cast(
298
450
  "tuple[str, bytes]", (r["type"], r["blob"])
299
451
  )
300
452
 
301
- # newest write first per ancestor (task_id DESC, idx DESC)
302
- for ws in writes_by_cid.values():
303
- ws.sort(key=lambda w: (w[2], w[3]), reverse=True)
304
-
305
- ancestors: list[str] = []
306
- cur_cid: str | None = parent_of.get(target_id)
307
- while cur_cid is not None:
308
- ancestors.append(cur_cid)
309
- cur_cid = parent_of.get(cur_cid)
310
- if not ancestors:
311
- return _ChannelWritesHistory(seed=DELTA_SENTINEL, writes=[])
312
-
313
- collected: list[PendingWrite] = [] # newest first; reversed at the end
314
- for cid in ancestors:
315
- # Collect writes first — they encode the transition FROM this
316
- # ancestor's state to its child's and must be included even if
317
- # this ancestor is also the seed checkpoint.
318
- for type_tag, write_blob, task_id, _idx in writes_by_cid.get(cid, []):
319
- val = self.serde.loads_typed((type_tag, write_blob))
320
- collected.append((task_id, channel, val))
321
- # Then check seed terminator.
322
- ver = ver_of.get(cid)
323
- if ver is not None:
324
- seed_blob = blob_by_ver.get(ver)
325
- if seed_blob is not None and seed_blob[0] != "empty":
326
- blob_value = self.serde.loads_typed(seed_blob)
327
- if blob_value is not DELTA_SENTINEL:
328
- collected.reverse()
329
- return _ChannelWritesHistory(seed=blob_value, writes=collected)
330
-
331
- collected.reverse() # oldest → newest
332
- return _ChannelWritesHistory(seed=DELTA_SENTINEL, writes=collected)
453
+ # Sort writes per (channel, cid) newest-first by (task_id, idx)
454
+ for cid_map in writes_by_ch_by_cid.values():
455
+ for ws in cid_map.values():
456
+ ws.sort(key=lambda w: (w[2], w[3]), reverse=True)
457
+
458
+ result: dict[str, DeltaChannelHistory] = {}
459
+ for ch in channels:
460
+ chain_cids = chain_by_ch.get(ch, [])
461
+ seed_version = seed_ver_by_ch.get(ch)
462
+
463
+ collected: list[PendingWrite] = []
464
+ cid_writes = writes_by_ch_by_cid.get(ch, {})
465
+ for cid in chain_cids:
466
+ for type_tag, write_blob, task_id, _idx in cid_writes.get(cid, []):
467
+ val = self.serde.loads_typed((type_tag, write_blob))
468
+ collected.append((task_id, ch, val))
469
+ collected.reverse()
470
+
471
+ entry: DeltaChannelHistory = {"writes": collected}
472
+ if seed_version is not None:
473
+ blob = seed_blob_by_ver.get((ch, seed_version))
474
+ if blob is not None and blob[0] != "empty":
475
+ entry["seed"] = self.serde.loads_typed(blob)
476
+ result[ch] = entry
477
+ return result
333
478
 
334
479
  def _dump_blobs(
335
480
  self,
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "langgraph-checkpoint-postgres"
7
- version = "3.1.0a1"
7
+ version = "3.1.0a4"
8
8
  description = "Library with a Postgres implementation of LangGraph checkpoint saver."
9
9
  authors = []
10
10
  requires-python = ">=3.10"
@@ -12,7 +12,7 @@ readme = "README.md"
12
12
  license = "MIT"
13
13
  license-files = ['LICENSE']
14
14
  dependencies = [
15
- "langgraph-checkpoint>=4.1.0a1,<5.0.0",
15
+ "langgraph-checkpoint>=4.1.0a4,<5.0.0",
16
16
  "orjson>=3.11.5",
17
17
  "psycopg>=3.2.0",
18
18
  "psycopg-pool>=3.2.0",
@@ -1,6 +1,10 @@
1
1
  version = 1
2
2
  revision = 3
3
3
  requires-python = ">=3.10"
4
+ resolution-markers = [
5
+ "python_full_version >= '3.15'",
6
+ "python_full_version < '3.15'",
7
+ ]
4
8
 
5
9
  [[package]]
6
10
  name = "annotated-types"
@@ -240,10 +244,11 @@ wheels = [
240
244
 
241
245
  [[package]]
242
246
  name = "langchain-core"
243
- version = "1.2.28"
247
+ version = "1.3.2"
244
248
  source = { registry = "https://pypi.org/simple" }
245
249
  dependencies = [
246
250
  { name = "jsonpatch" },
251
+ { name = "langchain-protocol" },
247
252
  { name = "langsmith" },
248
253
  { name = "packaging" },
249
254
  { name = "pydantic" },
@@ -252,14 +257,26 @@ dependencies = [
252
257
  { name = "typing-extensions" },
253
258
  { name = "uuid-utils" },
254
259
  ]
255
- sdist = { url = "https://files.pythonhosted.org/packages/f8/a4/317a1a3ac1df33a64adb3670bf88bbe3b3d5baa274db6863a979db472897/langchain_core-1.2.28.tar.gz", hash = "sha256:271a3d8bd618f795fdeba112b0753980457fc90537c46a0c11998516a74dc2cb", size = 846119, upload-time = "2026-04-08T18:19:34.867Z" }
260
+ sdist = { url = "https://files.pythonhosted.org/packages/a8/03/7219502e8ca728d65eb44d7a3eb60239230742a70dbfc9241b9bfd61c4ab/langchain_core-1.3.2.tar.gz", hash = "sha256:fd7a50b2f28ba561fd9d7f5d2760bc9e06cf00cdf820a3ccafe88a94ffa8d5b7", size = 911813, upload-time = "2026-04-24T15:49:23.699Z" }
261
+ wheels = [
262
+ { url = "https://files.pythonhosted.org/packages/7d/d5/8fa4431007cbb7cfed7590f4d6a5dea3ad724f4174d248f6642ef5ce7d05/langchain_core-1.3.2-py3-none-any.whl", hash = "sha256:d44a66127f9f8db735bdfd0ab9661bccb47a97113cfd3f2d89c74864422b7274", size = 542390, upload-time = "2026-04-24T15:49:21.991Z" },
263
+ ]
264
+
265
+ [[package]]
266
+ name = "langchain-protocol"
267
+ version = "0.0.15"
268
+ source = { registry = "https://pypi.org/simple" }
269
+ dependencies = [
270
+ { name = "typing-extensions" },
271
+ ]
272
+ sdist = { url = "https://files.pythonhosted.org/packages/4f/24/9777489d6fbbee64af0c8f96d4f840239c408cf694f3394672807dafc490/langchain_protocol-0.0.15.tar.gz", hash = "sha256:9ab2d11ee73944754f10e037e717098d3a6796f0e58afa9cadda6154e7655ade", size = 5862, upload-time = "2026-05-01T22:30:04.748Z" }
256
273
  wheels = [
257
- { url = "https://files.pythonhosted.org/packages/a8/92/32f785f077c7e898da97064f113c73fbd9ad55d1e2169cf3a391b183dedb/langchain_core-1.2.28-py3-none-any.whl", hash = "sha256:80764232581eaf8057bcefa71dbf8adc1f6a28d257ebd8b95ba9b8b452e8c6ac", size = 508727, upload-time = "2026-04-08T18:19:32.823Z" },
274
+ { url = "https://files.pythonhosted.org/packages/1d/7a/9c97a7b9cbe4c5dc6a44cdb1545450c28f0c8ce89b9c1f0ee7fbad896263/langchain_protocol-0.0.15-py3-none-any.whl", hash = "sha256:461eb794358f83d5e42635a5797799ffec7b4702314e34edf73ac21e75d3ef79", size = 6982, upload-time = "2026-05-01T22:30:03.877Z" },
258
275
  ]
259
276
 
260
277
  [[package]]
261
278
  name = "langgraph-checkpoint"
262
- version = "4.1.0a1"
279
+ version = "4.1.0a4"
263
280
  source = { editable = "../checkpoint" }
264
281
  dependencies = [
265
282
  { name = "langchain-core" },
@@ -307,7 +324,7 @@ test = [
307
324
 
308
325
  [[package]]
309
326
  name = "langgraph-checkpoint-postgres"
310
- version = "3.1.0a1"
327
+ version = "3.1.0a4"
311
328
  source = { editable = "." }
312
329
  dependencies = [
313
330
  { name = "langgraph-checkpoint" },
@@ -487,7 +504,7 @@ wheels = [
487
504
 
488
505
  [[package]]
489
506
  name = "mypy"
490
- version = "1.20.0"
507
+ version = "1.20.2"
491
508
  source = { registry = "https://pypi.org/simple" }
492
509
  dependencies = [
493
510
  { name = "librt", marker = "platform_python_implementation != 'PyPy'" },
@@ -496,51 +513,51 @@ dependencies = [
496
513
  { name = "tomli", marker = "python_full_version < '3.11'" },
497
514
  { name = "typing-extensions" },
498
515
  ]
499
- sdist = { url = "https://files.pythonhosted.org/packages/f8/5c/b0089fe7fef0a994ae5ee07029ced0526082c6cfaaa4c10d40a10e33b097/mypy-1.20.0.tar.gz", hash = "sha256:eb96c84efcc33f0b5e0e04beacf00129dd963b67226b01c00b9dfc8affb464c3", size = 3815028, upload-time = "2026-03-31T16:55:14.959Z" }
516
+ sdist = { url = "https://files.pythonhosted.org/packages/04/af/e3d4b3e9ec91a0ff9aabfdb38692952acf49bbb899c2e4c29acb3a6da3ae/mypy-1.20.2.tar.gz", hash = "sha256:e8222c26daaafd9e8626dec58ae36029f82585890589576f769a650dd20fd665", size = 3817349, upload-time = "2026-04-21T17:12:28.473Z" }
500
517
  wheels = [
501
- { url = "https://files.pythonhosted.org/packages/4d/a2/a965c8c3fcd4fa8b84ba0d46606181b0d0a1d50f274c67877f3e9ed4882c/mypy-1.20.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d99f515f95fd03a90875fdb2cca12ff074aa04490db4d190905851bdf8a549a8", size = 14430138, upload-time = "2026-03-31T16:52:37.843Z" },
502
- { url = "https://files.pythonhosted.org/packages/53/6e/043477501deeb8eabbab7f1a2f6cac62cfb631806dc1d6862a04a7f5011b/mypy-1.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bd0212976dc57a5bfeede7c219e7cd66568a32c05c9129686dd487c059c1b88a", size = 13311282, upload-time = "2026-03-31T16:55:11.021Z" },
503
- { url = "https://files.pythonhosted.org/packages/65/aa/bd89b247b83128197a214f29f0632ff3c14f54d4cd70d144d157bd7d7d6e/mypy-1.20.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f8426d4d75d68714abc17a4292d922f6ba2cfb984b72c2278c437f6dae797865", size = 13750889, upload-time = "2026-03-31T16:52:02.909Z" },
504
- { url = "https://files.pythonhosted.org/packages/fa/9d/2860be7355c45247ccc0be1501c91176318964c2a137bd4743f58ce6200e/mypy-1.20.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02cca0761c75b42a20a2757ae58713276605eb29a08dd8a6e092aa347c4115ca", size = 14619788, upload-time = "2026-03-31T16:50:48.928Z" },
505
- { url = "https://files.pythonhosted.org/packages/75/7f/3ef3e360c91f3de120f205c8ce405e9caf9fc52ef14b65d37073e322c114/mypy-1.20.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b3a49064504be59e59da664c5e149edc1f26c67c4f8e8456f6ba6aba55033018", size = 14918849, upload-time = "2026-03-31T16:51:10.478Z" },
506
- { url = "https://files.pythonhosted.org/packages/ae/72/af970dfe167ef788df7c5e6109d2ed0229f164432ce828bc9741a4250e64/mypy-1.20.0-cp310-cp310-win_amd64.whl", hash = "sha256:ebea00201737ad4391142808ed16e875add5c17f676e0912b387739f84991e13", size = 10822007, upload-time = "2026-03-31T16:50:25.268Z" },
507
- { url = "https://files.pythonhosted.org/packages/93/94/ba9065c2ebe5421619aff684b793d953e438a8bfe31a320dd6d1e0706e81/mypy-1.20.0-cp310-cp310-win_arm64.whl", hash = "sha256:e80cf77847d0d3e6e3111b7b25db32a7f8762fd4b9a3a72ce53fe16a2863b281", size = 9756158, upload-time = "2026-03-31T16:48:36.213Z" },
508
- { url = "https://files.pythonhosted.org/packages/6e/1c/74cb1d9993236910286865679d1c616b136b2eae468493aa939431eda410/mypy-1.20.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4525e7010b1b38334516181c5b81e16180b8e149e6684cee5a727c78186b4e3b", size = 14343972, upload-time = "2026-03-31T16:49:04.887Z" },
509
- { url = "https://files.pythonhosted.org/packages/d5/0d/01399515eca280386e308cf57901e68d3a52af18691941b773b3380c1df8/mypy-1.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a17c5d0bdcca61ce24a35beb828a2d0d323d3fcf387d7512206888c900193367", size = 13225007, upload-time = "2026-03-31T16:50:08.151Z" },
510
- { url = "https://files.pythonhosted.org/packages/56/ac/b4ba5094fb2d7fe9d2037cd8d18bbe02bcf68fd22ab9ff013f55e57ba095/mypy-1.20.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f75ff57defcd0f1d6e006d721ccdec6c88d4f6a7816eb92f1c4890d979d9ee62", size = 13663752, upload-time = "2026-03-31T16:49:26.064Z" },
511
- { url = "https://files.pythonhosted.org/packages/db/a7/460678d3cf7da252d2288dad0c602294b6ec22a91932ec368cc11e44bb6e/mypy-1.20.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b503ab55a836136b619b5fc21c8803d810c5b87551af8600b72eecafb0059cb0", size = 14532265, upload-time = "2026-03-31T16:53:55.077Z" },
512
- { url = "https://files.pythonhosted.org/packages/a3/3e/051cca8166cf0438ae3ea80e0e7c030d7a8ab98dffc93f80a1aa3f23c1a2/mypy-1.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1973868d2adbb4584a3835780b27436f06d1dc606af5be09f187aaa25be1070f", size = 14768476, upload-time = "2026-03-31T16:50:34.587Z" },
513
- { url = "https://files.pythonhosted.org/packages/be/66/8e02ec184f852ed5c4abb805583305db475930854e09964b55e107cdcbc4/mypy-1.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:2fcedb16d456106e545b2bfd7ef9d24e70b38ec252d2a629823a4d07ebcdb69e", size = 10818226, upload-time = "2026-03-31T16:53:15.624Z" },
514
- { url = "https://files.pythonhosted.org/packages/13/4b/383ad1924b28f41e4879a74151e7a5451123330d45652da359f9183bcd45/mypy-1.20.0-cp311-cp311-win_arm64.whl", hash = "sha256:379edf079ce44ac8d2805bcf9b3dd7340d4f97aad3a5e0ebabbf9d125b84b442", size = 9750091, upload-time = "2026-03-31T16:54:12.162Z" },
515
- { url = "https://files.pythonhosted.org/packages/be/dd/3afa29b58c2e57c79116ed55d700721c3c3b15955e2b6251dd165d377c0e/mypy-1.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:002b613ae19f4ac7d18b7e168ffe1cb9013b37c57f7411984abbd3b817b0a214", size = 14509525, upload-time = "2026-03-31T16:55:01.824Z" },
516
- { url = "https://files.pythonhosted.org/packages/54/eb/227b516ab8cad9f2a13c5e7a98d28cd6aa75e9c83e82776ae6c1c4c046c7/mypy-1.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9336b5e6712f4adaf5afc3203a99a40b379049104349d747eb3e5a3aa23ac2e", size = 13326469, upload-time = "2026-03-31T16:51:41.23Z" },
517
- { url = "https://files.pythonhosted.org/packages/57/d4/1ddb799860c1b5ac6117ec307b965f65deeb47044395ff01ab793248a591/mypy-1.20.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f13b3e41bce9d257eded794c0f12878af3129d80aacd8a3ee0dee51f3a978651", size = 13705953, upload-time = "2026-03-31T16:48:55.69Z" },
518
- { url = "https://files.pythonhosted.org/packages/c5/b7/54a720f565a87b893182a2a393370289ae7149e4715859e10e1c05e49154/mypy-1.20.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9804c3ad27f78e54e58b32e7cb532d128b43dbfb9f3f9f06262b821a0f6bd3f5", size = 14710363, upload-time = "2026-03-31T16:53:26.948Z" },
519
- { url = "https://files.pythonhosted.org/packages/b2/2a/74810274848d061f8a8ea4ac23aaad43bd3d8c1882457999c2e568341c57/mypy-1.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:697f102c5c1d526bdd761a69f17c6070f9892eebcb94b1a5963d679288c09e78", size = 14947005, upload-time = "2026-03-31T16:50:17.591Z" },
520
- { url = "https://files.pythonhosted.org/packages/77/91/21b8ba75f958bcda75690951ce6fa6b7138b03471618959529d74b8544e2/mypy-1.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:0ecd63f75fdd30327e4ad8b5704bd6d91fc6c1b2e029f8ee14705e1207212489", size = 10880616, upload-time = "2026-03-31T16:52:19.986Z" },
521
- { url = "https://files.pythonhosted.org/packages/8a/15/3d8198ef97c1ca03aea010cce4f1d4f3bc5d9849e8c0140111ca2ead9fdd/mypy-1.20.0-cp312-cp312-win_arm64.whl", hash = "sha256:f194db59657c58593a3c47c6dfd7bad4ef4ac12dbc94d01b3a95521f78177e33", size = 9813091, upload-time = "2026-03-31T16:53:44.385Z" },
522
- { url = "https://files.pythonhosted.org/packages/d6/a7/f64ea7bd592fa431cb597418b6dec4a47f7d0c36325fec7ac67bc8402b94/mypy-1.20.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b20c8b0fd5877abdf402e79a3af987053de07e6fb208c18df6659f708b535134", size = 14485344, upload-time = "2026-03-31T16:49:16.78Z" },
523
- { url = "https://files.pythonhosted.org/packages/bb/72/8927d84cfc90c6abea6e96663576e2e417589347eb538749a464c4c218a0/mypy-1.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:367e5c993ba34d5054d11937d0485ad6dfc60ba760fa326c01090fc256adf15c", size = 13327400, upload-time = "2026-03-31T16:53:08.02Z" },
524
- { url = "https://files.pythonhosted.org/packages/ab/4a/11ab99f9afa41aa350178d24a7d2da17043228ea10f6456523f64b5a6cf6/mypy-1.20.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f799d9db89fc00446f03281f84a221e50018fc40113a3ba9864b132895619ebe", size = 13706384, upload-time = "2026-03-31T16:52:28.577Z" },
525
- { url = "https://files.pythonhosted.org/packages/42/79/694ca73979cfb3535ebfe78733844cd5aff2e63304f59bf90585110d975a/mypy-1.20.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:555658c611099455b2da507582ea20d2043dfdfe7f5ad0add472b1c6238b433f", size = 14700378, upload-time = "2026-03-31T16:48:45.527Z" },
526
- { url = "https://files.pythonhosted.org/packages/84/24/a022ccab3a46e3d2cdf2e0e260648633640eb396c7e75d5a42818a8d3971/mypy-1.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:efe8d70949c3023698c3fca1e94527e7e790a361ab8116f90d11221421cd8726", size = 14932170, upload-time = "2026-03-31T16:49:36.038Z" },
527
- { url = "https://files.pythonhosted.org/packages/d8/9b/549228d88f574d04117e736f55958bd4908f980f9f5700a07aeb85df005b/mypy-1.20.0-cp313-cp313-win_amd64.whl", hash = "sha256:f49590891d2c2f8a9de15614e32e459a794bcba84693c2394291a2038bbaaa69", size = 10888526, upload-time = "2026-03-31T16:50:59.827Z" },
528
- { url = "https://files.pythonhosted.org/packages/91/17/15095c0e54a8bc04d22d4ff06b2139d5f142c2e87520b4e39010c4862771/mypy-1.20.0-cp313-cp313-win_arm64.whl", hash = "sha256:76a70bf840495729be47510856b978f1b0ec7d08f257ca38c9d932720bf6b43e", size = 9816456, upload-time = "2026-03-31T16:49:59.537Z" },
529
- { url = "https://files.pythonhosted.org/packages/4e/0e/6ca4a84cbed9e62384bc0b2974c90395ece5ed672393e553996501625fc5/mypy-1.20.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0f42dfaab7ec1baff3b383ad7af562ab0de573c5f6edb44b2dab016082b89948", size = 14483331, upload-time = "2026-03-31T16:52:57.999Z" },
530
- { url = "https://files.pythonhosted.org/packages/7d/c5/5fe9d8a729dd9605064691816243ae6c49fde0bd28f6e5e17f6a24203c43/mypy-1.20.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:31b5dbb55293c1bd27c0fc813a0d2bb5ceef9d65ac5afa2e58f829dab7921fd5", size = 13342047, upload-time = "2026-03-31T16:54:21.555Z" },
531
- { url = "https://files.pythonhosted.org/packages/4c/33/e18bcfa338ca4e6b2771c85d4c5203e627d0c69d9de5c1a2cf2ba13320ba/mypy-1.20.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49d11c6f573a5a08f77fad13faff2139f6d0730ebed2cfa9b3d2702671dd7188", size = 13719585, upload-time = "2026-03-31T16:51:53.89Z" },
532
- { url = "https://files.pythonhosted.org/packages/6b/8d/93491ff7b79419edc7eabf95cb3b3f7490e2e574b2855c7c7e7394ff933f/mypy-1.20.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d3243c406773185144527f83be0e0aefc7bf4601b0b2b956665608bf7c98a83", size = 14685075, upload-time = "2026-03-31T16:54:04.464Z" },
533
- { url = "https://files.pythonhosted.org/packages/b5/9d/d924b38a4923f8d164bf2b4ec98bf13beaf6e10a5348b4b137eadae40a6e/mypy-1.20.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a79c1eba7ac4209f2d850f0edd0a2f8bba88cbfdfefe6fb76a19e9d4fe5e71a2", size = 14919141, upload-time = "2026-03-31T16:54:51.785Z" },
534
- { url = "https://files.pythonhosted.org/packages/59/98/1da9977016678c0b99d43afe52ed00bb3c1a0c4c995d3e6acca1a6ebb9b4/mypy-1.20.0-cp314-cp314-win_amd64.whl", hash = "sha256:00e047c74d3ec6e71a2eb88e9ea551a2edb90c21f993aefa9e0d2a898e0bb732", size = 11050925, upload-time = "2026-03-31T16:51:30.758Z" },
535
- { url = "https://files.pythonhosted.org/packages/5e/e3/ba0b7a3143e49a9c4f5967dde6ea4bf8e0b10ecbbcca69af84027160ee89/mypy-1.20.0-cp314-cp314-win_arm64.whl", hash = "sha256:931a7630bba591593dcf6e97224a21ff80fb357e7982628d25e3c618e7f598ef", size = 10001089, upload-time = "2026-03-31T16:49:43.632Z" },
536
- { url = "https://files.pythonhosted.org/packages/12/28/e617e67b3be9d213cda7277913269c874eb26472489f95d09d89765ce2d8/mypy-1.20.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:26c8b52627b6552f47ff11adb4e1509605f094e29815323e487fc0053ebe93d1", size = 15534710, upload-time = "2026-03-31T16:52:12.506Z" },
537
- { url = "https://files.pythonhosted.org/packages/6e/0c/3b5f2d3e45dc7169b811adce8451679d9430399d03b168f9b0489f43adaa/mypy-1.20.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:39362cdb4ba5f916e7976fccecaab1ba3a83e35f60fa68b64e9a70e221bb2436", size = 14393013, upload-time = "2026-03-31T16:54:41.186Z" },
538
- { url = "https://files.pythonhosted.org/packages/a3/49/edc8b0aa145cc09c1c74f7ce2858eead9329931dcbbb26e2ad40906daa4e/mypy-1.20.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34506397dbf40c15dc567635d18a21d33827e9ab29014fb83d292a8f4f8953b6", size = 15047240, upload-time = "2026-03-31T16:54:31.955Z" },
539
- { url = "https://files.pythonhosted.org/packages/42/37/a946bb416e37a57fa752b3100fd5ede0e28df94f92366d1716555d47c454/mypy-1.20.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:555493c44a4f5a1b58d611a43333e71a9981c6dbe26270377b6f8174126a0526", size = 15858565, upload-time = "2026-03-31T16:53:36.997Z" },
540
- { url = "https://files.pythonhosted.org/packages/2f/99/7690b5b5b552db1bd4ff362e4c0eb3107b98d680835e65823fbe888c8b78/mypy-1.20.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2721f0ce49cb74a38f00c50da67cb7d36317b5eda38877a49614dc018e91c787", size = 16087874, upload-time = "2026-03-31T16:52:48.313Z" },
541
- { url = "https://files.pythonhosted.org/packages/aa/76/53e893a498138066acd28192b77495c9357e5a58cc4be753182846b43315/mypy-1.20.0-cp314-cp314t-win_amd64.whl", hash = "sha256:47781555a7aa5fedcc2d16bcd72e0dc83eb272c10dd657f9fb3f9cc08e2e6abb", size = 12572380, upload-time = "2026-03-31T16:49:52.454Z" },
542
- { url = "https://files.pythonhosted.org/packages/76/9c/6dbdae21f01b7aacddc2c0bbf3c5557aa547827fdf271770fe1e521e7093/mypy-1.20.0-cp314-cp314t-win_arm64.whl", hash = "sha256:c70380fe5d64010f79fb863b9081c7004dd65225d2277333c219d93a10dad4dd", size = 10381174, upload-time = "2026-03-31T16:51:20.179Z" },
543
- { url = "https://files.pythonhosted.org/packages/21/66/4d734961ce167f0fd8380769b3b7c06dbdd6ff54c2190f3f2ecd22528158/mypy-1.20.0-py3-none-any.whl", hash = "sha256:a6e0641147cbfa7e4e94efdb95c2dab1aff8cfc159ded13e07f308ddccc8c48e", size = 2636365, upload-time = "2026-03-31T16:51:44.911Z" },
518
+ { url = "https://files.pythonhosted.org/packages/76/97/ce2502df2cecf2ef997b6c6527c4a223b92feb9e7b790cdc8dcd683f3a8a/mypy-1.20.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cf5a4db6dca263010e2c7bff081c89383c72d187ba2cf4c44759aac970e2f0c4", size = 14457059, upload-time = "2026-04-21T17:06:14.935Z" },
519
+ { url = "https://files.pythonhosted.org/packages/c9/34/417ee60b822cc80c0f3dc9f495ad7fd8dbb8d8b2cf4baf22d4046d25d01d/mypy-1.20.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7b0e817b518bff7facd7f85ea05b643ad8bdcce684cf29784987b0a7c8e1f997", size = 13346816, upload-time = "2026-04-21T17:10:41.433Z" },
520
+ { url = "https://files.pythonhosted.org/packages/4a/85/e20951978702df58379d0bcc2e8f7ccdca4e78cd7dc66dd3ddbf9b29d517/mypy-1.20.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97d7b9a485b40f8ca425460e89bf1da2814625b2da627c0dcc6aa46c92631d14", size = 13772593, upload-time = "2026-04-21T17:08:11.24Z" },
521
+ { url = "https://files.pythonhosted.org/packages/63/a5/5441a13259ec516c56fd5de0fd96a69a9590ae6c5e5d3e5174aa84b97973/mypy-1.20.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e1c12f6d2db3d78b909b5f77513c11eb7f2dd2782b96a3ab6dffc7d44575c99", size = 14656635, upload-time = "2026-04-21T17:09:54.042Z" },
522
+ { url = "https://files.pythonhosted.org/packages/3b/51/b89c69157c5e1f19fd125a65d991166a26906e7902f026f00feebbcfa2b9/mypy-1.20.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:89dce27e142d25ffbc154c1819383b69f2e9234dc4ed4766f42e0e8cb264ab5c", size = 14943278, upload-time = "2026-04-21T17:09:15.599Z" },
523
+ { url = "https://files.pythonhosted.org/packages/e9/44/6b0eeecfe96d7cce1d71c66b8e03cb304aa70ec11f1955dc1d6b46aca3c3/mypy-1.20.2-cp310-cp310-win_amd64.whl", hash = "sha256:f376e37f9bf2a946872fc5fd1199c99310748e3c26c7a26683f13f8bdb756cbd", size = 10851915, upload-time = "2026-04-21T17:06:03.5Z" },
524
+ { url = "https://files.pythonhosted.org/packages/3c/36/6593dc88545d75fb96416184be5392da5e2a8e8c2802a8597913e16ae25c/mypy-1.20.2-cp310-cp310-win_arm64.whl", hash = "sha256:6e2b469efd811707bc530fd1effef0f5d6eebcb7fe376affae69025da4b979a2", size = 9786676, upload-time = "2026-04-21T17:07:02.035Z" },
525
+ { url = "https://files.pythonhosted.org/packages/1f/4d/9ebeae211caccbdaddde7ed5e31dfcf57faac66be9b11deb1dc6526c8078/mypy-1.20.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4077797a273e56e8843d001e9dfe4ba10e33323d6ade647ff260e5cd97d9758c", size = 14371307, upload-time = "2026-04-21T17:08:56.442Z" },
526
+ { url = "https://files.pythonhosted.org/packages/95/d7/93473d34b61f04fac1aecc01368485c89c5c4af7a4b9a0cab5d77d04b63f/mypy-1.20.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cdecf62abcc4292500d7858aeae87a1f8f1150f4c4dd08fb0b336ee79b2a6df3", size = 13258917, upload-time = "2026-04-21T17:05:50.978Z" },
527
+ { url = "https://files.pythonhosted.org/packages/e2/30/3dd903e8bafb7b5f7bf87fcd58f8382086dea2aa19f0a7b357f21f63071b/mypy-1.20.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c566c3a88b6ece59b3d70f65bedef17304f48eb52ff040a6a18214e1917b3254", size = 13700516, upload-time = "2026-04-21T17:11:33.161Z" },
528
+ { url = "https://files.pythonhosted.org/packages/07/05/c61a140aba4c729ac7bc99ae26fc627c78a6e08f5b9dd319244ea71a3d7e/mypy-1.20.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0deb80d062b2479f2c87ae568f89845afc71d11bc41b04179e58165fd9f31e98", size = 14562889, upload-time = "2026-04-21T17:05:27.674Z" },
529
+ { url = "https://files.pythonhosted.org/packages/fd/87/da78243742ffa8a36d98c3010f0d829f93d5da4e6786f1a1a6f2ad616502/mypy-1.20.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bba9ad231e92a3e424b3e56b65aa17704993425bba97e302c832f9466bb85bac", size = 14803844, upload-time = "2026-04-21T17:10:06.2Z" },
530
+ { url = "https://files.pythonhosted.org/packages/37/52/10a1ddf91b40f843943a3c6db51e2df59c9e237f29d355e95eaab427461f/mypy-1.20.2-cp311-cp311-win_amd64.whl", hash = "sha256:baf593f2765fa3a6b1ef95807dbaa3d25b594f6a52adcc506a6b9cb115e1be67", size = 10846300, upload-time = "2026-04-21T17:12:23.886Z" },
531
+ { url = "https://files.pythonhosted.org/packages/20/02/f9a4415b664c53bd34d6709be59da303abcae986dc4ac847b402edb6fa1e/mypy-1.20.2-cp311-cp311-win_arm64.whl", hash = "sha256:20175a1c0f49863946ec20b7f63255768058ac4f07d2b9ded6a6b46cfb5a9100", size = 9779498, upload-time = "2026-04-21T17:09:23.695Z" },
532
+ { url = "https://files.pythonhosted.org/packages/71/4e/7560e4528db9e9b147e4c0f22660466bf30a0a1fe3d63d1b9d3b0fd354ee/mypy-1.20.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4dbfcf869f6b0517f70cf0030ba6ea1d6645e132337a7d5204a18d8d5636c02b", size = 14539393, upload-time = "2026-04-21T17:07:12.52Z" },
533
+ { url = "https://files.pythonhosted.org/packages/32/d9/34a5efed8124f5a9234f55ac6a4ced4201e2c5b81e1109c49ad23190ec8c/mypy-1.20.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b6481b228d072315b053210b01ac320e1be243dc17f9e5887ef167f23f5fae4", size = 13361642, upload-time = "2026-04-21T17:06:53.742Z" },
534
+ { url = "https://files.pythonhosted.org/packages/d1/14/eb377acf78c03c92d566a1510cda8137348215b5335085ef662ab82ecd3a/mypy-1.20.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34397cdced6b90b836e38182076049fdb41424322e0b0728c946b0939ebdf9f6", size = 13740347, upload-time = "2026-04-21T17:12:04.73Z" },
535
+ { url = "https://files.pythonhosted.org/packages/b9/94/7e4634a32b641aa1c112422eed1bbece61ee16205f674190e8b536f884de/mypy-1.20.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5da6976f20cae27059ea8d0c86e7cef3de720e04c4bb9ee18e3690fdb792066", size = 14734042, upload-time = "2026-04-21T17:07:43.16Z" },
536
+ { url = "https://files.pythonhosted.org/packages/7a/f3/f7e62395cb7f434541b4491a01149a4439e28ace4c0c632bbf5431e92d1f/mypy-1.20.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:56908d7e08318d39f85b1f0c6cfd47b0cac1a130da677630dac0de3e0623e102", size = 14964958, upload-time = "2026-04-21T17:11:00.665Z" },
537
+ { url = "https://files.pythonhosted.org/packages/3e/0d/47e3c3a0ec2a876e35aeac365df3cac7776c36bbd4ed18cc521e1b9d255b/mypy-1.20.2-cp312-cp312-win_amd64.whl", hash = "sha256:d52ad8d78522da1d308789df651ee5379088e77c76cb1994858d40a426b343b9", size = 10911340, upload-time = "2026-04-21T17:10:49.179Z" },
538
+ { url = "https://files.pythonhosted.org/packages/d6/b2/6c852d72e0ea8b01f49da817fb52539993cde327e7d010e0103dc12d0dac/mypy-1.20.2-cp312-cp312-win_arm64.whl", hash = "sha256:785b08db19c9f214dc37d65f7c165d19a30fcecb48abfa30f31b01b5acaabb58", size = 9833947, upload-time = "2026-04-21T17:09:05.267Z" },
539
+ { url = "https://files.pythonhosted.org/packages/5b/c4/b93812d3a192c9bcf5df405bd2f30277cd0e48106a14d1023c7f6ed6e39b/mypy-1.20.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:edfbfca868cdd6bd8d974a60f8a3682f5565d3f5c99b327640cedd24c4264026", size = 14524670, upload-time = "2026-04-21T17:10:30.737Z" },
540
+ { url = "https://files.pythonhosted.org/packages/f3/47/42c122501bff18eaf1e8f457f5c017933452d8acdc52918a9f59f6812955/mypy-1.20.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e2877a02380adfcdbc69071a0f74d6e9dbbf593c0dc9d174e1f223ffd5281943", size = 13336218, upload-time = "2026-04-21T17:08:44.069Z" },
541
+ { url = "https://files.pythonhosted.org/packages/92/8f/75bbc92f41725fbd585fb17b440b1119b576105df1013622983e18640a93/mypy-1.20.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7488448de6007cd5177c6cea0517ac33b4c0f5ee9b5e9f2be51ce75511a85517", size = 13724906, upload-time = "2026-04-21T17:08:01.02Z" },
542
+ { url = "https://files.pythonhosted.org/packages/a1/32/4c49da27a606167391ff0c39aa955707a00edc500572e562f7c36c08a71f/mypy-1.20.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb9c2fa06887e21d6a3a868762acb82aec34e2c6fd0174064f27c93ede68ad15", size = 14726046, upload-time = "2026-04-21T17:11:22.354Z" },
543
+ { url = "https://files.pythonhosted.org/packages/7f/fc/4e354a1bd70216359deb0c9c54847ee6b32ef78dfb09f5131ff99b494078/mypy-1.20.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d56a78b646f2e3daa865bc70cd5ec5a46c50045801ca8ff17a0c43abc97e3ee", size = 14955587, upload-time = "2026-04-21T17:12:16.033Z" },
544
+ { url = "https://files.pythonhosted.org/packages/62/b2/c0f2056e9eb8f08c62cafd9715e4584b89132bdc832fcf85d27d07b5f3e5/mypy-1.20.2-cp313-cp313-win_amd64.whl", hash = "sha256:2a4102b03bb7481d9a91a6da8d174740c9c8c4401024684b9ca3b7cc5e49852f", size = 10922681, upload-time = "2026-04-21T17:06:35.842Z" },
545
+ { url = "https://files.pythonhosted.org/packages/e5/14/065e333721f05de8ef683d0aa804c23026bcc287446b61cac657b902ccac/mypy-1.20.2-cp313-cp313-win_arm64.whl", hash = "sha256:a95a9248b0c6fd933a442c03c3b113c3b61320086b88e2c444676d3fd1ca3330", size = 9830560, upload-time = "2026-04-21T17:07:51.023Z" },
546
+ { url = "https://files.pythonhosted.org/packages/ae/d1/b4ec96b0ecc620a4443570c6e95c867903428cfcde4206518eafdd5880c3/mypy-1.20.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:419413398fe250aae057fd2fe50166b61077083c9b82754c341cf4fd73038f30", size = 14524561, upload-time = "2026-04-21T17:06:27.325Z" },
547
+ { url = "https://files.pythonhosted.org/packages/3a/63/d2c2ff4fa66bc49477d32dfa26e8a167ba803ea6a69c5efb416036909d30/mypy-1.20.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e73c07f23009962885c197ccb9b41356a30cc0e5a1d0c2ea8fd8fb1362d7f924", size = 13363883, upload-time = "2026-04-21T17:11:11.239Z" },
548
+ { url = "https://files.pythonhosted.org/packages/2a/56/983916806bf4eddeaaa2c9230903c3669c6718552a921154e1c5182c701f/mypy-1.20.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c64e5973df366b747646fc98da921f9d6eba9716d57d1db94a83c026a08e0fb", size = 13742945, upload-time = "2026-04-21T17:08:34.181Z" },
549
+ { url = "https://files.pythonhosted.org/packages/19/65/0cd9285ab010ee8214c83d67c6b49417c40d86ce46f1aa109457b5a9b8d7/mypy-1.20.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a65aa591af023864fd08a97da9974e919452cfe19cb146c8a5dc692626445dc", size = 14706163, upload-time = "2026-04-21T17:05:15.51Z" },
550
+ { url = "https://files.pythonhosted.org/packages/94/97/48ff3b297cafcc94d185243a9190836fb1b01c1b0918fff64e941e973cc9/mypy-1.20.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4fef51b01e638974a6e69885687e9bd40c8d1e09a6cd291cca0619625cf1f558", size = 14938677, upload-time = "2026-04-21T17:05:39.562Z" },
551
+ { url = "https://files.pythonhosted.org/packages/fd/a1/1b4233d255bdd0b38a1f284feeb1c143ca508c19184964e22f8d837ec851/mypy-1.20.2-cp314-cp314-win_amd64.whl", hash = "sha256:913485a03f1bcf5d279409a9d2b9ed565c151f61c09f29991e5faa14033da4c8", size = 11089322, upload-time = "2026-04-21T17:06:44.29Z" },
552
+ { url = "https://files.pythonhosted.org/packages/78/c2/ce7ee2ba36aeb954ba50f18fa25d9c1188578654b97d02a66a15b6f09531/mypy-1.20.2-cp314-cp314-win_arm64.whl", hash = "sha256:c3bae4f855d965b5453784300c12ffc63a548304ac7f99e55d4dc7c898673aa3", size = 10017775, upload-time = "2026-04-21T17:07:20.732Z" },
553
+ { url = "https://files.pythonhosted.org/packages/4e/a1/9d93a7d0b5859af0ead82b4888b46df6c8797e1bc5e1e262a08518c6d48e/mypy-1.20.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2de3dcea53babc1c3237a19002bc3d228ce1833278f093b8d619e06e7cc79609", size = 15549002, upload-time = "2026-04-21T17:08:23.107Z" },
554
+ { url = "https://files.pythonhosted.org/packages/00/d2/09a6a10ee1bf0008f6c144d9676f2ca6a12512151b4e0ad0ff6c4fac5337/mypy-1.20.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:52b176444e2e5054dfcbcb8c75b0b719865c96247b37407184bbfca5c353f2c2", size = 14401942, upload-time = "2026-04-21T17:07:31.837Z" },
555
+ { url = "https://files.pythonhosted.org/packages/57/da/9594b75c3c019e805250bed3583bdf4443ff9e6ef08f97e39ae308cb06f2/mypy-1.20.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:688c3312e5dadb573a2c69c82af3a298d43ecf9e6d264e0f95df960b5f6ac19c", size = 15041649, upload-time = "2026-04-21T17:09:34.653Z" },
556
+ { url = "https://files.pythonhosted.org/packages/97/77/f75a65c278e6e8eba2071f7f5a90481891053ecc39878cc444634d892abe/mypy-1.20.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29752dbbf8cc53f89f6ac096d363314333045c257c9c75cbd189ca2de0455744", size = 15864588, upload-time = "2026-04-21T17:11:44.936Z" },
557
+ { url = "https://files.pythonhosted.org/packages/d7/46/1a4e1c66e96c1a3246ddf5403d122ac9b0a8d2b7e65730b9d6533ba7a6d3/mypy-1.20.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:803203d2b6ea644982c644895c2f78b28d0e208bba7b27d9b921e0ec5eb207c6", size = 16093956, upload-time = "2026-04-21T17:10:17.683Z" },
558
+ { url = "https://files.pythonhosted.org/packages/5a/2c/78a8851264dec38cd736ca5b8bc9380674df0dd0be7792f538916157716c/mypy-1.20.2-cp314-cp314t-win_amd64.whl", hash = "sha256:9bcb8aa397ff0093c824182fd76a935a9ba7ad097fcbef80ae89bf6c1731d8ec", size = 12568661, upload-time = "2026-04-21T17:11:54.473Z" },
559
+ { url = "https://files.pythonhosted.org/packages/83/01/cd7318aa03493322ce275a0e14f4f52b8896335e4e79d4fb8153a7ad2b77/mypy-1.20.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e061b58443f1736f8a37c48978d7ab581636d6ab03e3d4f99e3fa90463bb9382", size = 10389240, upload-time = "2026-04-21T17:09:42.719Z" },
560
+ { url = "https://files.pythonhosted.org/packages/28/9a/f23c163e25b11074188251b0b5a0342625fc1cdb6af604757174fa9acc9b/mypy-1.20.2-py3-none-any.whl", hash = "sha256:a94c5a76ab46c5e6257c7972b6c8cff0574201ca7dc05647e33e795d78680563", size = 2637314, upload-time = "2026-04-21T17:05:54.5Z" },
544
561
  ]
545
562
 
546
563
  [[package]]
@@ -1099,27 +1116,27 @@ wheels = [
1099
1116
 
1100
1117
  [[package]]
1101
1118
  name = "ruff"
1102
- version = "0.15.8"
1119
+ version = "0.15.12"
1103
1120
  source = { registry = "https://pypi.org/simple" }
1104
- sdist = { url = "https://files.pythonhosted.org/packages/14/b0/73cf7550861e2b4824950b8b52eebdcc5adc792a00c514406556c5b80817/ruff-0.15.8.tar.gz", hash = "sha256:995f11f63597ee362130d1d5a327a87cb6f3f5eae3094c620bcc632329a4d26e", size = 4610921, upload-time = "2026-03-26T18:39:38.675Z" }
1121
+ sdist = { url = "https://files.pythonhosted.org/packages/99/43/3291f1cc9106f4c63bdce7a8d0df5047fe8422a75b091c16b5e9355e0b11/ruff-0.15.12.tar.gz", hash = "sha256:ecea26adb26b4232c0c2ca19ccbc0083a68344180bba2a600605538ce51a40a6", size = 4643852, upload-time = "2026-04-24T18:17:14.305Z" }
1105
1122
  wheels = [
1106
- { url = "https://files.pythonhosted.org/packages/4a/92/c445b0cd6da6e7ae51e954939cb69f97e008dbe750cfca89b8cedc081be7/ruff-0.15.8-py3-none-linux_armv6l.whl", hash = "sha256:cbe05adeba76d58162762d6b239c9056f1a15a55bd4b346cfd21e26cd6ad7bc7", size = 10527394, upload-time = "2026-03-26T18:39:41.566Z" },
1107
- { url = "https://files.pythonhosted.org/packages/eb/92/f1c662784d149ad1414cae450b082cf736430c12ca78367f20f5ed569d65/ruff-0.15.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d3e3d0b6ba8dca1b7ef9ab80a28e840a20070c4b62e56d675c24f366ef330570", size = 10905693, upload-time = "2026-03-26T18:39:30.364Z" },
1108
- { url = "https://files.pythonhosted.org/packages/ca/f2/7a631a8af6d88bcef997eb1bf87cc3da158294c57044aafd3e17030613de/ruff-0.15.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6ee3ae5c65a42f273f126686353f2e08ff29927b7b7e203b711514370d500de3", size = 10323044, upload-time = "2026-03-26T18:39:33.37Z" },
1109
- { url = "https://files.pythonhosted.org/packages/67/18/1bf38e20914a05e72ef3b9569b1d5c70a7ef26cd188d69e9ca8ef588d5bf/ruff-0.15.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdce027ada77baa448077ccc6ebb2fa9c3c62fd110d8659d601cf2f475858d94", size = 10629135, upload-time = "2026-03-26T18:39:44.142Z" },
1110
- { url = "https://files.pythonhosted.org/packages/d2/e9/138c150ff9af60556121623d41aba18b7b57d95ac032e177b6a53789d279/ruff-0.15.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12e617fc01a95e5821648a6df341d80456bd627bfab8a829f7cfc26a14a4b4a3", size = 10348041, upload-time = "2026-03-26T18:39:52.178Z" },
1111
- { url = "https://files.pythonhosted.org/packages/02/f1/5bfb9298d9c323f842c5ddeb85f1f10ef51516ac7a34ba446c9347d898df/ruff-0.15.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:432701303b26416d22ba696c39f2c6f12499b89093b61360abc34bcc9bf07762", size = 11121987, upload-time = "2026-03-26T18:39:55.195Z" },
1112
- { url = "https://files.pythonhosted.org/packages/10/11/6da2e538704e753c04e8d86b1fc55712fdbdcc266af1a1ece7a51fff0d10/ruff-0.15.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d910ae974b7a06a33a057cb87d2a10792a3b2b3b35e33d2699fdf63ec8f6b17a", size = 11951057, upload-time = "2026-03-26T18:39:19.18Z" },
1113
- { url = "https://files.pythonhosted.org/packages/83/f0/c9208c5fd5101bf87002fed774ff25a96eea313d305f1e5d5744698dc314/ruff-0.15.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2033f963c43949d51e6fdccd3946633c6b37c484f5f98c3035f49c27395a8ab8", size = 11464613, upload-time = "2026-03-26T18:40:06.301Z" },
1114
- { url = "https://files.pythonhosted.org/packages/f8/22/d7f2fabdba4fae9f3b570e5605d5eb4500dcb7b770d3217dca4428484b17/ruff-0.15.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f29b989a55572fb885b77464cf24af05500806ab4edf9a0fd8977f9759d85b1", size = 11257557, upload-time = "2026-03-26T18:39:57.972Z" },
1115
- { url = "https://files.pythonhosted.org/packages/71/8c/382a9620038cf6906446b23ce8632ab8c0811b8f9d3e764f58bedd0c9a6f/ruff-0.15.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:ac51d486bf457cdc985a412fb1801b2dfd1bd8838372fc55de64b1510eff4bec", size = 11169440, upload-time = "2026-03-26T18:39:22.205Z" },
1116
- { url = "https://files.pythonhosted.org/packages/4d/0d/0994c802a7eaaf99380085e4e40c845f8e32a562e20a38ec06174b52ef24/ruff-0.15.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c9861eb959edab053c10ad62c278835ee69ca527b6dcd72b47d5c1e5648964f6", size = 10605963, upload-time = "2026-03-26T18:39:46.682Z" },
1117
- { url = "https://files.pythonhosted.org/packages/19/aa/d624b86f5b0aad7cef6bbf9cd47a6a02dfdc4f72c92a337d724e39c9d14b/ruff-0.15.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8d9a5b8ea13f26ae90838afc33f91b547e61b794865374f114f349e9036835fb", size = 10357484, upload-time = "2026-03-26T18:39:49.176Z" },
1118
- { url = "https://files.pythonhosted.org/packages/35/c3/e0b7835d23001f7d999f3895c6b569927c4d39912286897f625736e1fd04/ruff-0.15.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c2a33a529fb3cbc23a7124b5c6ff121e4d6228029cba374777bd7649cc8598b8", size = 10830426, upload-time = "2026-03-26T18:40:03.702Z" },
1119
- { url = "https://files.pythonhosted.org/packages/f0/51/ab20b322f637b369383adc341d761eaaa0f0203d6b9a7421cd6e783d81b9/ruff-0.15.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:75e5cd06b1cf3f47a3996cfc999226b19aa92e7cce682dcd62f80d7035f98f49", size = 11345125, upload-time = "2026-03-26T18:39:27.799Z" },
1120
- { url = "https://files.pythonhosted.org/packages/37/e6/90b2b33419f59d0f2c4c8a48a4b74b460709a557e8e0064cf33ad894f983/ruff-0.15.8-py3-none-win32.whl", hash = "sha256:bc1f0a51254ba21767bfa9a8b5013ca8149dcf38092e6a9eb704d876de94dc34", size = 10571959, upload-time = "2026-03-26T18:39:36.117Z" },
1121
- { url = "https://files.pythonhosted.org/packages/1f/a2/ef467cb77099062317154c63f234b8a7baf7cb690b99af760c5b68b9ee7f/ruff-0.15.8-py3-none-win_amd64.whl", hash = "sha256:04f79eff02a72db209d47d665ba7ebcad609d8918a134f86cb13dd132159fc89", size = 11743893, upload-time = "2026-03-26T18:39:25.01Z" },
1122
- { url = "https://files.pythonhosted.org/packages/15/e2/77be4fff062fa78d9b2a4dea85d14785dac5f1d0c1fb58ed52331f0ebe28/ruff-0.15.8-py3-none-win_arm64.whl", hash = "sha256:cf891fa8e3bb430c0e7fac93851a5978fc99c8fa2c053b57b118972866f8e5f2", size = 11048175, upload-time = "2026-03-26T18:40:01.06Z" },
1123
+ { url = "https://files.pythonhosted.org/packages/c3/6e/e78ffb61d4686f3d96ba3df2c801161843746dcbcbb17a1e927d4829312b/ruff-0.15.12-py3-none-linux_armv6l.whl", hash = "sha256:f86f176e188e94d6bdbc09f09bfd9dc729059ad93d0e7390b5a73efe19f8861c", size = 10640713, upload-time = "2026-04-24T18:17:22.841Z" },
1124
+ { url = "https://files.pythonhosted.org/packages/ae/08/a317bc231fb9e7b93e4ef3089501e51922ff88d6936ce5cf870c4fe55419/ruff-0.15.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e3bcd123364c3770b8e1b7baaf343cc99a35f197c5c6e8af79015c666c423a6c", size = 11069267, upload-time = "2026-04-24T18:17:30.105Z" },
1125
+ { url = "https://files.pythonhosted.org/packages/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fe87510d000220aa1ed530d4448a7c696a0cae1213e5ec30e5874287b66557b5", size = 10397182, upload-time = "2026-04-24T18:17:07.177Z" },
1126
+ { url = "https://files.pythonhosted.org/packages/71/e0/3310fc6d1b5e1fdea22bf3b1b807c7e187b581021b0d7d4514cccdb5fb71/ruff-0.15.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84a1630093121375a3e2a95b4a6dc7b59e2b4ee76216e32d81aae550a832d002", size = 10758012, upload-time = "2026-04-24T18:16:55.759Z" },
1127
+ { url = "https://files.pythonhosted.org/packages/11/c1/a606911aee04c324ddaa883ae418f3569792fd3c4a10c50e0dd0a2311e1e/ruff-0.15.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fb129f40f114f089ebe0ca56c0d251cf2061b17651d464bb6478dc01e69f11f5", size = 10447479, upload-time = "2026-04-24T18:16:51.677Z" },
1128
+ { url = "https://files.pythonhosted.org/packages/9d/68/4201e8444f0894f21ab4aeeaee68aa4f10b51613514a20d80bd628d57e88/ruff-0.15.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0c862b172d695db7598426b8af465e7e9ac00a3ea2a3630ee67eb82e366aaa6", size = 11234040, upload-time = "2026-04-24T18:17:16.529Z" },
1129
+ { url = "https://files.pythonhosted.org/packages/34/ff/8a6d6cf4ccc23fd67060874e832c18919d1557a0611ebef03fdb01fff11e/ruff-0.15.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2849ea9f3484c3aca43a82f484210370319e7170df4dfe4843395ddf6c57bc33", size = 12087377, upload-time = "2026-04-24T18:17:04.944Z" },
1130
+ { url = "https://files.pythonhosted.org/packages/85/f6/c669cf73f5152f623d34e69866a46d5e6185816b19fcd5b6dd8a2d299922/ruff-0.15.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e77c7e51c07fe396826d5969a5b846d9cd4c402535835fb6e21ce8b28fef847", size = 11367784, upload-time = "2026-04-24T18:17:25.409Z" },
1131
+ { url = "https://files.pythonhosted.org/packages/e8/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b2f4f2f3b1026b5fb449b467d9264bf22067b600f7b6f41fc5958909f449d0", size = 11344088, upload-time = "2026-04-24T18:17:12.258Z" },
1132
+ { url = "https://files.pythonhosted.org/packages/c2/8d/49afab3645e31e12c590acb6d3b5b69d7aab5b81926dbaf7461f9441f37a/ruff-0.15.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9ba3b8f1afd7e2e43d8943e55f249e13f9682fde09711644a6e7290eb4f3e339", size = 11271770, upload-time = "2026-04-24T18:17:02.457Z" },
1133
+ { url = "https://files.pythonhosted.org/packages/46/06/33f41fe94403e2b755481cdfb9b7ef3e4e0ed031c4581124658d935d52b4/ruff-0.15.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e852ba9fdc890655e1d78f2df1499efbe0e54126bd405362154a75e2bde159c5", size = 10719355, upload-time = "2026-04-24T18:17:27.648Z" },
1134
+ { url = "https://files.pythonhosted.org/packages/0d/59/18aa4e014debbf559670e4048e39260a85c7fcee84acfd761ac01e7b8d35/ruff-0.15.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dd8aed930da53780d22fc70bdf84452c843cf64f8cb4eb38984319c24c5cd5fd", size = 10462758, upload-time = "2026-04-24T18:17:32.347Z" },
1135
+ { url = "https://files.pythonhosted.org/packages/25/e7/cc9f16fd0f3b5fddcbd7ec3d6ae30c8f3fde1047f32a4093a98d633c6570/ruff-0.15.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:01da3988d225628b709493d7dc67c3b9b12c0210016b08690ef9bd27970b262b", size = 10953498, upload-time = "2026-04-24T18:17:20.674Z" },
1136
+ { url = "https://files.pythonhosted.org/packages/72/7a/a9ba7f98c7a575978698f4230c5e8cc54bbc761af34f560818f933dafa0c/ruff-0.15.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9cae0f92bd5700d1213188b31cd3bdd2b315361296d10b96b8e2337d3d11f53e", size = 11447765, upload-time = "2026-04-24T18:17:09.755Z" },
1137
+ { url = "https://files.pythonhosted.org/packages/ea/f9/0ae446942c846b8266059ad8a30702a35afae55f5cdc54c5adf8d7afdc27/ruff-0.15.12-py3-none-win32.whl", hash = "sha256:d0185894e038d7043ba8fd6aee7499ece6462dc0ea9f1e260c7451807c714c20", size = 10657277, upload-time = "2026-04-24T18:17:18.591Z" },
1138
+ { url = "https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.whl", hash = "sha256:c87a162d61ab3adca47c03f7f717c68672edec7d1b5499e652331780fe74950d", size = 11837758, upload-time = "2026-04-24T18:17:00.113Z" },
1139
+ { url = "https://files.pythonhosted.org/packages/c0/98/6beb4b351e472e5f4c4613f7c35a5290b8be2497e183825310c4c3a3984b/ruff-0.15.12-py3-none-win_arm64.whl", hash = "sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f", size = 11120821, upload-time = "2026-04-24T18:16:57.979Z" },
1123
1140
  ]
1124
1141
 
1125
1142
  [[package]]