langgraph-checkpoint-postgres 3.1.0a3__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.0a3 → langgraph_checkpoint_postgres-3.1.0a4}/PKG-INFO +2 -2
  2. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.0a4}/langgraph/checkpoint/postgres/__init__.py +100 -45
  3. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.0a4}/langgraph/checkpoint/postgres/aio.py +82 -44
  4. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.0a4}/langgraph/checkpoint/postgres/base.py +243 -103
  5. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.0a4}/pyproject.toml +2 -2
  6. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.0a4}/uv.lock +86 -69
  7. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.0a4}/.gitignore +0 -0
  8. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.0a4}/LICENSE +0 -0
  9. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.0a4}/Makefile +0 -0
  10. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.0a4}/README.md +0 -0
  11. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.0a4}/langgraph/checkpoint/postgres/_ainternal.py +0 -0
  12. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.0a4}/langgraph/checkpoint/postgres/_internal.py +0 -0
  13. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.0a4}/langgraph/checkpoint/postgres/py.typed +0 -0
  14. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.0a4}/langgraph/checkpoint/postgres/shallow.py +0 -0
  15. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.0a4}/langgraph/store/postgres/__init__.py +0 -0
  16. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.0a4}/langgraph/store/postgres/aio.py +0 -0
  17. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.0a4}/langgraph/store/postgres/base.py +0 -0
  18. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.0a4}/langgraph/store/postgres/py.typed +0 -0
  19. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.0a4}/tests/__init__.py +0 -0
  20. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.0a4}/tests/compose-postgres.yml +0 -0
  21. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.0a4}/tests/conftest.py +0 -0
  22. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.0a4}/tests/embed_test_utils.py +0 -0
  23. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.0a4}/tests/test_async.py +0 -0
  24. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.0a4}/tests/test_async_store.py +0 -0
  25. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.0a4}/tests/test_store.py +0 -0
  26. {langgraph_checkpoint_postgres-3.1.0a3 → 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.0a3
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.0a3
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,19 +2,18 @@ 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
  )
@@ -27,10 +26,10 @@ from psycopg_pool import ConnectionPool
27
26
 
28
27
  from langgraph.checkpoint.postgres import _internal
29
28
  from langgraph.checkpoint.postgres.base import (
30
- SELECT_DELTA_STAGE1_SQL,
31
- SELECT_DELTA_STAGE2_SQL,
29
+ _DELTA_PAGE_SIZE,
32
30
  BasePostgresSaver,
33
- _DeltaStage1Row,
31
+ _build_delta_stage1_sql,
32
+ _build_delta_stage2_sql,
34
33
  _DeltaStage2Row,
35
34
  )
36
35
  from langgraph.checkpoint.postgres.shallow import ShallowPostgresSaver
@@ -311,9 +310,7 @@ class PostgresSaver(BasePostgresSaver):
311
310
  # others are stored in blobs table
312
311
  blob_values = {}
313
312
  for k, v in checkpoint["channel_values"].items():
314
- if v is DELTA_SENTINEL:
315
- copy["channel_values"].pop(k)
316
- elif isinstance(v, _DeltaSnapshot):
313
+ if isinstance(v, _DeltaSnapshot):
317
314
  blob_values[k] = copy["channel_values"].pop(k)
318
315
  copy["channel_values"][k] = True
319
316
  elif v is None or isinstance(v, (str, int, float, bool)):
@@ -444,53 +441,111 @@ class PostgresSaver(BasePostgresSaver):
444
441
  with conn.cursor(binary=True, row_factory=dict_row) as cur:
445
442
  yield cur
446
443
 
447
- def _get_channel_writes_history(
448
- self, config: RunnableConfig, channel: str
449
- ) -> _ChannelWritesHistory:
450
- """Fast-path override of `BaseCheckpointSaver._get_channel_writes_history`.
451
-
452
- Two-stage query: stage 1 scans checkpoint metadata to walk the parent
453
- chain and locate the nearest snapshot; stage 2 fetches only the
454
- chain-limited writes and single seed blob.
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.
455
463
  """
464
+ if not channels:
465
+ return {}
466
+ channels = list(channels)
456
467
  thread_id = config["configurable"]["thread_id"]
457
468
  checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
458
469
  checkpoint_id = get_checkpoint_id(config)
459
470
  if checkpoint_id is None:
460
471
  target = self.get_tuple(config)
461
472
  if target is None:
462
- return _ChannelWritesHistory(seed=DELTA_SENTINEL, writes=[])
473
+ return {ch: {"writes": []} for ch in channels}
463
474
  checkpoint_id = target.config["configurable"]["checkpoint_id"]
464
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
+
465
488
  with self._cursor() as cur:
466
- cur.execute(
467
- SELECT_DELTA_STAGE1_SQL,
468
- (channel, channel, thread_id, checkpoint_ns),
469
- )
470
- stage1_rows = cur.fetchall()
471
- chain_cids, seed_version = self._walk_stage1(
472
- cast("list[_DeltaStage1Row]", stage1_rows), checkpoint_id
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,
473
531
  )
474
- seed_versions = [seed_version] if seed_version else []
475
- with self._cursor() as cur:
476
- cur.execute(
477
- SELECT_DELTA_STAGE2_SQL,
478
- (
479
- thread_id,
480
- checkpoint_ns,
481
- channel,
482
- chain_cids,
483
- thread_id,
484
- checkpoint_ns,
485
- channel,
486
- seed_versions,
487
- ),
488
- )
489
- stage2_rows = cur.fetchall()
490
- return self._build_delta_channel_writes_history(
491
- channel=channel,
492
- chain_cids=chain_cids,
493
- seed_version=seed_version,
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,
494
549
  stage2_rows=cast("list[_DeltaStage2Row]", stage2_rows),
495
550
  )
496
551
 
@@ -2,19 +2,18 @@ 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
  )
@@ -27,10 +26,10 @@ from psycopg_pool import AsyncConnectionPool
27
26
 
28
27
  from langgraph.checkpoint.postgres import _ainternal
29
28
  from langgraph.checkpoint.postgres.base import (
30
- SELECT_DELTA_STAGE1_SQL,
31
- SELECT_DELTA_STAGE2_SQL,
29
+ _DELTA_PAGE_SIZE,
32
30
  BasePostgresSaver,
33
- _DeltaStage1Row,
31
+ _build_delta_stage1_sql,
32
+ _build_delta_stage2_sql,
34
33
  _DeltaStage2Row,
35
34
  )
36
35
  from langgraph.checkpoint.postgres.shallow import AsyncShallowPostgresSaver
@@ -270,9 +269,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
270
269
  # others are stored in blobs table
271
270
  blob_values = {}
272
271
  for k, v in checkpoint["channel_values"].items():
273
- if v is DELTA_SENTINEL:
274
- copy["channel_values"].pop(k)
275
- elif isinstance(v, _DeltaSnapshot):
272
+ if isinstance(v, _DeltaSnapshot):
276
273
  blob_values[k] = copy["channel_values"].pop(k)
277
274
  copy["channel_values"][k] = True
278
275
  elif v is None or isinstance(v, (str, int, float, bool)):
@@ -405,53 +402,94 @@ class AsyncPostgresSaver(BasePostgresSaver):
405
402
  async with conn.cursor(binary=True, row_factory=dict_row) as cur:
406
403
  yield cur
407
404
 
408
- async def _aget_channel_writes_history(
409
- self, config: RunnableConfig, channel: str
410
- ) -> _ChannelWritesHistory:
411
- """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`.
412
409
 
413
- Two-stage query: stage 1 scans checkpoint metadata to walk the parent
414
- chain and locate the nearest snapshot; stage 2 fetches only the
415
- chain-limited writes and single seed blob.
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.
416
413
  """
414
+ if not channels:
415
+ return {}
416
+ channels = list(channels)
417
417
  thread_id = config["configurable"]["thread_id"]
418
418
  checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
419
419
  checkpoint_id = get_checkpoint_id(config)
420
420
  if checkpoint_id is None:
421
421
  target = await self.aget_tuple(config)
422
422
  if target is None:
423
- return _ChannelWritesHistory(seed=DELTA_SENTINEL, writes=[])
423
+ return {ch: {"writes": []} for ch in channels}
424
424
  checkpoint_id = target.config["configurable"]["checkpoint_id"]
425
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
+
426
436
  async with self._cursor() as cur:
427
- await cur.execute(
428
- SELECT_DELTA_STAGE1_SQL,
429
- (channel, channel, thread_id, checkpoint_ns),
430
- )
431
- stage1_rows = await cur.fetchall()
432
- chain_cids, seed_version = self._walk_stage1(
433
- cast("list[_DeltaStage1Row]", stage1_rows), checkpoint_id
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,
434
475
  )
435
- seed_versions = [seed_version] if seed_version else []
436
- async with self._cursor() as cur:
437
- await cur.execute(
438
- SELECT_DELTA_STAGE2_SQL,
439
- (
440
- thread_id,
441
- checkpoint_ns,
442
- channel,
443
- chain_cids,
444
- thread_id,
445
- checkpoint_ns,
446
- channel,
447
- seed_versions,
448
- ),
449
- )
450
- stage2_rows = await cur.fetchall()
451
- return self._build_delta_channel_writes_history(
452
- channel=channel,
453
- chain_cids=chain_cids,
454
- seed_version=seed_version,
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,
455
493
  stage2_rows=cast("list[_DeltaStage2Row]", stage2_rows),
456
494
  )
457
495
 
@@ -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:
@@ -157,10 +160,11 @@ INSERT_CHECKPOINT_WRITES_SQL = """
157
160
 
158
161
 
159
162
  class _DeltaStage2Row(TypedDict, total=False):
160
- """One row from `SELECT_DELTA_STAGE2_SQL` (a UNION ALL of writes and blobs)."""
163
+ """One row from `_build_delta_stage2_sql` (a UNION ALL of writes and blobs)."""
161
164
 
162
165
  _kind: str # "w" or "b"
163
166
  checkpoint_id: str | None # "w" rows only
167
+ channel: str | None # set on both "w" and "b" rows
164
168
  type: str | None
165
169
  blob: bytes | None
166
170
  task_id: str | None # "w" rows only
@@ -168,48 +172,126 @@ class _DeltaStage2Row(TypedDict, total=False):
168
172
  version: str | None # "b" rows only
169
173
 
170
174
 
171
- # Two-stage DeltaChannel reconstruction. Stage 1 scans checkpoint
172
- # metadata (no blob bytes) to walk the parent chain and locate the
173
- # nearest snapshot marker. Stage 2 fetches only the chain-limited
174
- # writes and the single seed snapshot blob.
175
+ # Multi-channel two-stage DeltaChannel reconstruction.
175
176
  #
176
- # Parameter order:
177
- # stage1: (channel, channel, thread_id, checkpoint_ns)
178
- # stage2: (thread_id, checkpoint_ns, channel, chain_cids[],
179
- # thread_id, checkpoint_ns, channel, seed_versions[])
180
-
181
- SELECT_DELTA_STAGE1_SQL = """
182
- SELECT checkpoint_id,
183
- parent_checkpoint_id,
184
- checkpoint -> 'channel_versions' ->> %s AS ver,
185
- (checkpoint -> 'channel_values' -> %s) IS NOT NULL AS has_snapshot
186
- FROM checkpoints
187
- WHERE thread_id = %s AND checkpoint_ns = %s
188
- """
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).
189
199
 
190
- SELECT_DELTA_STAGE2_SQL = """
191
- SELECT 'w'::text AS _kind,
192
- checkpoint_id,
193
- type, blob, task_id, idx, NULL::text AS version
194
- FROM checkpoint_writes
195
- WHERE thread_id = %s AND checkpoint_ns = %s AND channel = %s
196
- AND checkpoint_id = ANY(%s)
197
- UNION ALL
198
- SELECT 'b', NULL,
199
- type, blob, NULL, NULL, version
200
- FROM checkpoint_blobs
201
- WHERE thread_id = %s AND checkpoint_ns = %s AND channel = %s
202
- AND version = ANY(%s)
203
- """
204
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
205
 
206
- class _DeltaStage1Row(TypedDict):
207
- """One row from `SELECT_DELTA_STAGE1_SQL`."""
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
208
216
 
209
- checkpoint_id: str
210
- parent_checkpoint_id: str | None
211
- ver: str | None
212
- has_snapshot: bool
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]):
@@ -255,86 +337,144 @@ class BasePostgresSaver(BaseCheckpointSaver[str]):
255
337
  }
256
338
 
257
339
  @staticmethod
258
- def _walk_stage1(
259
- stage1_rows: Sequence[_DeltaStage1Row],
260
- target_id: str,
261
- ) -> tuple[list[str], str | None]:
262
- """Walk the parent chain from stage 1 metadata rows.
263
-
264
- Returns (chain_cids, seed_version):
265
- chain_cids: ancestor checkpoint IDs from target's parent down to
266
- the seed (or root), in newest-first order.
267
- seed_version: the channel blob version at the nearest ancestor
268
- with has_snapshot=True, or None if pure delta.
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`).
269
352
  """
270
- parent_of: dict[str, str | None] = {}
271
- ver_of: dict[str, str | None] = {}
272
- snapshot_of: dict[str, bool] = {}
353
+ oldest: str | None = None
273
354
  for r in stage1_rows:
274
- cid = r["checkpoint_id"]
275
- parent_of[cid] = r["parent_checkpoint_id"]
276
- ver_of[cid] = r["ver"]
277
- snapshot_of[cid] = r["has_snapshot"]
278
-
279
- chain_cids: list[str] = []
280
- seed_version: str | None = None
281
- cur_cid: str | None = parent_of.get(target_id)
282
- while cur_cid is not None:
283
- chain_cids.append(cur_cid)
284
- if snapshot_of.get(cur_cid, False):
285
- seed_version = ver_of.get(cur_cid)
286
- break
287
- cur_cid = parent_of.get(cur_cid)
288
- return chain_cids, seed_version
289
-
290
- def _build_delta_channel_writes_history(
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(
291
413
  self,
292
414
  *,
293
- channel: str,
294
- chain_cids: list[str],
295
- seed_version: str | None,
415
+ channels: Sequence[str],
416
+ chain_by_ch: Mapping[str, list[str]],
417
+ seed_ver_by_ch: Mapping[str, str | None],
296
418
  stage2_rows: Sequence[_DeltaStage2Row],
297
- ) -> _ChannelWritesHistory:
298
- """Reconstruct delta channel history from two-stage query results.
299
-
300
- chain_cids are in newest-first order (target's parent first).
301
- stage2_rows contain only writes for chain_cids and the single
302
- seed blob at seed_version.
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".
303
428
  """
304
- writes_by_cid: dict[str, list[tuple[str, bytes, str, int]]] = {}
305
- seed_blob: tuple[str, bytes] | None = None
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]] = {}
306
435
 
307
436
  for r in stage2_rows:
437
+ ch = cast(str, r["channel"])
308
438
  kind = r["_kind"]
309
439
  if kind == "w":
310
440
  cid = cast(str, r["checkpoint_id"])
311
- writes_by_cid.setdefault(cid, []).append(
441
+ writes_by_ch_by_cid.setdefault(ch, {}).setdefault(cid, []).append(
312
442
  cast(
313
443
  "tuple[str, bytes, str, int]",
314
444
  (r["type"], r["blob"], r["task_id"], r["idx"]),
315
445
  )
316
446
  )
317
447
  else: # kind == "b"
318
- seed_blob = cast("tuple[str, bytes]", (r["type"], r["blob"]))
319
-
320
- for ws in writes_by_cid.values():
321
- ws.sort(key=lambda w: (w[2], w[3]), reverse=True)
322
-
323
- if not chain_cids:
324
- return _ChannelWritesHistory(seed=DELTA_SENTINEL, writes=[])
325
-
326
- collected: list[PendingWrite] = []
327
- for cid in chain_cids:
328
- for type_tag, write_blob, task_id, _idx in writes_by_cid.get(cid, []):
329
- val = self.serde.loads_typed((type_tag, write_blob))
330
- collected.append((task_id, channel, val))
331
-
332
- seed: Any = DELTA_SENTINEL
333
- if seed_blob is not None and seed_blob[0] != "empty":
334
- seed = self.serde.loads_typed(seed_blob)
448
+ ver = cast(str, r["version"])
449
+ seed_blob_by_ver[(ch, ver)] = cast(
450
+ "tuple[str, bytes]", (r["type"], r["blob"])
451
+ )
335
452
 
336
- collected.reverse()
337
- return _ChannelWritesHistory(seed=seed, 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
338
478
 
339
479
  def _dump_blobs(
340
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.0a3"
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.0a3,<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.0a3"
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.0a3"
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]]