Compare commits

..
Author SHA1 Message Date
Quanzheng LongandClaude Opus 4.8 8e77945f40 fix: preserve updated_channels shape in update_state delta path
The supersteps-based delta history walk requires update_state to write
counters_since_delta_snapshot metadata, but passing updated_channels to
create_checkpoint also overrode checkpoint["updated_channels"] (was None),
breaking deferred-node triggering on resume for non-delta graphs
(test_in_one_fan_out_state_graph_defer_node). Keep updated_channels at its
default so resume/trigger semantics are unchanged; still write the counter
metadata and snapshot delta channels that hit their cadence.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 10:59:45 -07:00
Quanzheng Long 728fb748be fix 2026-06-30 21:22:00 -07:00
148 changed files with 3766 additions and 5285 deletions
+1 -1
View File
@@ -37,7 +37,7 @@ jobs:
run:
working-directory: libs/cli
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Get changed files
id: changed-files
if: github.event_name != 'workflow_dispatch'
+1 -1
View File
@@ -31,7 +31,7 @@ jobs:
- "3.12"
name: "lint #${{ matrix.python-version }}"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Get changed files
id: changed-files
if: github.event_name != 'workflow_dispatch'
+2 -2
View File
@@ -23,7 +23,7 @@ jobs:
env:
HAS_LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY != '' }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Set up Python
uses: ./.github/actions/uv_setup
@@ -33,7 +33,7 @@ jobs:
working-directory: libs/sdk-py
- name: Login to Docker Hub
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
if: ${{ !github.event.pull_request.head.repo.fork }}
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
+2 -2
View File
@@ -25,7 +25,7 @@ jobs:
name: "test #${{ matrix.python-version }}"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Set up Python ${{ matrix.python-version }}
uses: ./.github/actions/uv_setup
with:
@@ -33,7 +33,7 @@ jobs:
cache-suffix: test-${{ inputs.working-directory }}
working-directory: ${{ inputs.working-directory }}
- name: Login to Docker Hub
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
if: ${{ !github.event.pull_request.head.repo.fork }}
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
+2 -2
View File
@@ -23,7 +23,7 @@ jobs:
working-directory: libs/langgraph
name: "test #${{ matrix.python-version }}"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Set up Python ${{ matrix.python-version }}
uses: ./.github/actions/uv_setup
with:
@@ -31,7 +31,7 @@ jobs:
cache-suffix: "test-langgraph"
working-directory: libs/langgraph
- name: Login to Docker Hub
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
if: ${{ !github.event.pull_request.head.repo.fork }}
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
+3 -3
View File
@@ -23,7 +23,7 @@ jobs:
version: ${{ steps.check-version.outputs.version }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: ./.github/actions/uv_setup
@@ -74,7 +74,7 @@ jobs:
id-token: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
@@ -82,7 +82,7 @@ jobs:
path: ${{ inputs.working-directory }}/dist/
- name: Publish to test PyPI
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1
with:
packages-dir: ${{ inputs.working-directory }}/dist/
verbose: true
+2 -2
View File
@@ -17,7 +17,7 @@ jobs:
run:
working-directory: libs/langgraph
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- run: SHA=$(git rev-parse HEAD) && echo "SHA=$SHA" >> $GITHUB_ENV
- name: Set up Python 3.11
uses: ./.github/actions/uv_setup
@@ -30,7 +30,7 @@ jobs:
- name: Run benchmarks
run: OUTPUT=out/benchmark-baseline.json make -s benchmark
- name: Save outputs
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
key: ${{ runner.os }}-benchmark-baseline-${{ env.SHA }}
path: |
+2 -2
View File
@@ -15,7 +15,7 @@ jobs:
run:
working-directory: libs/langgraph
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- id: files
name: Get changed files
uses: Ana06/get-changed-files@25f79e676e7ea1868813e21465014798211fad8c # v2.3.0
@@ -30,7 +30,7 @@ jobs:
- name: Install dependencies
run: uv sync --group test
- name: Download baseline
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
key: ${{ runner.os }}-benchmark-baseline
restore-keys: |
+5 -5
View File
@@ -30,8 +30,8 @@ jobs:
deps: ${{ steps.filter.outputs.deps || 'true' }}
sdk_py: ${{ steps.filter.outputs.sdk_py || 'true' }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4
if: github.event_name != 'workflow_dispatch'
id: filter
with:
@@ -112,9 +112,9 @@ jobs:
name: "Check SDK methods matching"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.11"
- name: Run check_sdk_methods script
@@ -130,7 +130,7 @@ jobs:
python-version:
- "3.13"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Set up Python ${{ matrix.python-version }}
uses: ./.github/actions/uv_setup
with:
+2 -2
View File
@@ -26,10 +26,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Setup Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: '3.11'
+6 -6
View File
@@ -25,7 +25,7 @@ jobs:
tag: ${{ steps.check-version.outputs.tag }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Set up Python
uses: ./.github/actions/uv_setup
@@ -87,7 +87,7 @@ jobs:
outputs:
release-body: ${{ steps.generate-release-body.outputs.release-body }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
repository: langchain-ai/langgraph
path: langgraph
@@ -158,7 +158,7 @@ jobs:
- test-pypi-publish
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
# We explicitly *don't* set up caching here. This ensures our tests are
# maximally sensitive to catching breakage.
@@ -262,7 +262,7 @@ jobs:
working-directory: ${{ inputs.working-directory }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Set up Python
uses: ./.github/actions/uv_setup
@@ -278,7 +278,7 @@ jobs:
path: ${{ inputs.working-directory }}/dist/
- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1
with:
packages-dir: ${{ inputs.working-directory }}/dist/
verbose: true
@@ -304,7 +304,7 @@ jobs:
working-directory: ${{ inputs.working-directory }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Set up Python
uses: ./.github/actions/uv_setup
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Set up uv
uses: ./.github/actions/uv_setup
@@ -54,6 +54,12 @@ async def build_delta_chain(
stored: list[RunnableConfig] = []
parent_cfg: RunnableConfig | None = None
# Track supersteps since the channel's last snapshot, mirroring Pregel's
# `counters_since_delta_snapshot` bookkeeping: every step increments the
# counter; a snapshot step resets it (and carries no counter entry, so
# the walk locates the seed exactly `supersteps` hops back).
supersteps_since_snapshot = 0
for step in range(total_steps):
config: RunnableConfig = {
"configurable": {
@@ -68,11 +74,19 @@ async def build_delta_chain(
channel_values: dict[str, Any] = {}
channel_versions: dict[str, int] = {}
supersteps_since_snapshot += 1
counters: dict[str, tuple[int, int]] = {}
if step in snapshot_set:
channel_values[channel] = _DeltaSnapshot(
write_value_fn(step),
)
channel_versions[channel] = step + 1
supersteps_since_snapshot = 0
else:
counters[channel] = (
supersteps_since_snapshot,
supersteps_since_snapshot,
)
cp = Checkpoint(
v=1,
@@ -84,9 +98,10 @@ async def build_delta_chain(
updated_channels=None,
)
new_versions = dict(channel_versions)
parent_cfg = await saver.aput(
config, cp, generate_metadata(step=step), new_versions
)
md = generate_metadata(step=step)
if counters:
md["counters_since_delta_snapshot"] = counters
parent_cfg = await saver.aput(config, cp, md, new_versions)
stored.append(parent_cfg)
# Write a pending write for non-snapshot steps so the walk has
@@ -6,12 +6,9 @@ import traceback
from collections.abc import Callable
from uuid import uuid4
from langgraph.checkpoint.base import BaseCheckpointSaver, Checkpoint
from langgraph.checkpoint.base.id import uuid6
from langgraph.checkpoint.serde.types import _DeltaSnapshot
from langgraph.checkpoint.base import BaseCheckpointSaver
from langgraph.checkpoint.conformance.spec._delta_fixtures import build_delta_chain
from langgraph.checkpoint.conformance.test_utils import generate_metadata
async def test_history_returns_writes_oldest_first(
@@ -51,6 +48,8 @@ async def test_history_seed_is_nearest_snapshot(
result = await saver.aget_delta_channel_history(config=head, channels=["ch"])
assert "seed" in result["ch"], "Expected seed from snapshot at step 3"
seed = result["ch"]["seed"]
from langgraph.checkpoint.serde.types import _DeltaSnapshot
actual_value = seed.value if isinstance(seed, _DeltaSnapshot) else seed
assert actual_value == 3, f"Expected seed value 3 (step 3), got {actual_value}"
writes = result["ch"]["writes"]
@@ -82,7 +81,16 @@ async def test_history_multi_channel(
tid = str(uuid4())
configs: list = []
parent_cfg = None
from langgraph.checkpoint.base import Checkpoint
from langgraph.checkpoint.base.id import uuid6
from langgraph.checkpoint.serde.types import _DeltaSnapshot
from langgraph.checkpoint.conformance.test_utils import generate_metadata
# Per-channel supersteps since last snapshot ("a" snapshots at step 1,
# "b" at step 3). Drives the per-channel walk depth from the head.
s_a = 0
s_b = 0
for step in range(5):
config = {"configurable": {"thread_id": tid, "checkpoint_ns": ""}}
if parent_cfg:
@@ -91,12 +99,21 @@ async def test_history_multi_channel(
]
cv: dict = {}
cvs: dict = {}
s_a += 1
s_b += 1
counters: dict = {}
if step == 1:
cv["a"] = _DeltaSnapshot("snap_a")
cvs["a"] = step + 1
s_a = 0
else:
counters["a"] = (s_a, s_a)
if step == 3:
cv["b"] = _DeltaSnapshot("snap_b")
cvs["b"] = step + 1
s_b = 0
else:
counters["b"] = (s_b, s_b)
cp = Checkpoint(
v=1,
id=str(uuid6(clock_seq=-1)),
@@ -106,7 +123,10 @@ async def test_history_multi_channel(
versions_seen={},
updated_channels=None,
)
parent_cfg = await saver.aput(config, cp, generate_metadata(step=step), cvs)
md = generate_metadata(step=step)
if counters:
md["counters_since_delta_snapshot"] = counters
parent_cfg = await saver.aput(config, cp, md, cvs)
configs.append(parent_cfg)
await saver.aput_writes(parent_cfg, [("a", step), ("b", step)], str(uuid4()))
@@ -144,7 +164,12 @@ async def test_history_walk_to_root_no_seed(
)
head = configs[-1]
result = await saver.aget_delta_channel_history(config=head, channels=["ch"])
# The head's supersteps counter (4) runs one hop past the real root —
# the snapshot checkpoint was never persisted (implicit empty baseline).
# No seed, and the full chain (steps 0..2) replays from empty.
assert "seed" not in result["ch"], f"Expected no seed, got {result['ch']}"
values = [w[2] for w in result["ch"]["writes"]]
assert values == [0, 1, 2], f"Expected full-chain writes [0,1,2], got {values}"
async def test_history_migration_plain_value_as_seed(
@@ -157,10 +182,19 @@ async def test_history_migration_plain_value_as_seed(
channel_values[ch] (not a _DeltaSnapshot). The walk should treat it as the
seed and terminate there.
"""
from langgraph.checkpoint.base import Checkpoint
from langgraph.checkpoint.base.id import uuid6
from langgraph.checkpoint.conformance.test_utils import generate_metadata
tid = str(uuid4())
configs: list = []
parent_cfg = None
# The channel was a non-delta channel through step 1 (plain value, no
# delta counter), then migrated to DeltaChannel at step 2. Supersteps
# count from the migration boundary: step 2 -> 1, step 3 -> 2, so the
# head (step 3) walks 2 hops back to the plain-value seed at step 1.
for step in range(4):
config = {"configurable": {"thread_id": tid, "checkpoint_ns": ""}}
if parent_cfg:
@@ -173,6 +207,9 @@ async def test_history_migration_plain_value_as_seed(
if step == 1:
cv["ch"] = [10, 20, 30]
cvs["ch"] = step + 1
md = generate_metadata(step=step)
if step >= 2:
md["counters_since_delta_snapshot"] = {"ch": (step - 1, step - 1)}
cp = Checkpoint(
v=1,
id=str(uuid6(clock_seq=-1)),
@@ -182,7 +219,7 @@ async def test_history_migration_plain_value_as_seed(
versions_seen={},
updated_channels=None,
)
parent_cfg = await saver.aput(config, cp, generate_metadata(step=step), cvs)
parent_cfg = await saver.aput(config, cp, md, cvs)
configs.append(parent_cfg)
if step != 1:
await saver.aput_writes(parent_cfg, [("ch", step)], str(uuid4()))
@@ -199,35 +236,26 @@ async def test_history_migration_plain_value_as_seed(
assert values == [2], f"Expected [2], got {values}"
async def test_history_seed_ancestor_own_writes_are_replayed(
async def test_history_migration_skips_seed_checkpoint_writes(
saver: BaseCheckpointSaver,
) -> None:
"""Writes stored AT the seed ancestor must be included in `writes`.
"""A migrated plain-value seed already incorporates its own checkpoint's
writes, so those writes must NOT be re-emitted in the history.
A stored value is the state ENTERING its checkpoint; the writes stored
under that same checkpoint are what produced its child and are therefore
NOT subsumed by it. Only writes at ancestors OLDER than the seed are
subsumed, and the walk terminates before reaching them.
This holds for plain-value seeds (migration from a pre-delta channel type)
exactly as it does for `_DeltaSnapshot` seeds. Skipping the seed
ancestor's own writes silently drops the first post-migration write.
This is the discriminating case for non-additive reducers (e.g. an
even-only filter): re-applying the seed checkpoint's writes on top of
the seed would corrupt the reconstructed value. The saver contract is
to skip them — only writes strictly after the seed are returned.
"""
from langgraph.checkpoint.base import Checkpoint
from langgraph.checkpoint.base.id import uuid6
from langgraph.checkpoint.conformance.test_utils import generate_metadata
tid = str(uuid4())
configs: list = []
parent_cfg = None
# Each step's write is labelled by the role it plays, so the assertion
# below reads directly rather than by step index.
writes_by_step = {
0: "older-than-seed", # subsumed by the value stored at step 1
1: "at-seed", # the seed's own write, produced step 2
2: "after-seed", # delta-era write on the path to the head
3: "pending-at-head", # pending for the next step, never replayed
}
# Steps 0 and 1 store a plain value; 1 is the nearest, so it is the seed.
values_by_step = {0: [10], 1: [10, 20]}
for step in range(4):
config = {"configurable": {"thread_id": tid, "checkpoint_ns": ""}}
if parent_cfg:
@@ -236,9 +264,13 @@ async def test_history_seed_ancestor_own_writes_are_replayed(
]
cv: dict = {}
cvs: dict = {}
if step in values_by_step:
cv["ch"] = values_by_step[step]
# Step 1: pre-delta plain accumulated value AND its own writes.
if step == 1:
cv["ch"] = [2, 4]
cvs["ch"] = step + 1
md = generate_metadata(step=step)
if step >= 2:
md["counters_since_delta_snapshot"] = {"ch": (step - 1, step - 1)}
cp = Checkpoint(
v=1,
id=str(uuid6(clock_seq=-1)),
@@ -248,23 +280,21 @@ async def test_history_seed_ancestor_own_writes_are_replayed(
versions_seen={},
updated_channels=None,
)
parent_cfg = await saver.aput(config, cp, generate_metadata(step=step), cvs)
parent_cfg = await saver.aput(config, cp, md, cvs)
configs.append(parent_cfg)
await saver.aput_writes(
parent_cfg, [("ch", writes_by_step[step])], str(uuid4())
)
# The seed checkpoint (step 1) carries writes that the plain value
# already subsumes; later steps carry post-migration delta writes.
write_value = 99 if step == 1 else step * 10
await saver.aput_writes(parent_cfg, [("ch", write_value)], str(uuid4()))
head = configs[-1]
result = await saver.aget_delta_channel_history(config=head, channels=["ch"])
assert "seed" in result["ch"], "Expected seed from plain value at step 1"
assert result["ch"]["seed"] == [10, 20], (
f"Expected nearest plain value [10, 20], got {result['ch']['seed']}"
assert result["ch"].get("seed") == [2, 4], (
f"Expected plain seed [2, 4], got {result['ch'].get('seed')}"
)
values = [w[2] for w in result["ch"]["writes"]]
assert values == ["at-seed", "after-seed"], (
f'Expected ["at-seed", "after-seed"], got {values}'
)
# Seed checkpoint's own write (99) is skipped; step 2's write (20) kept.
assert values == [20], f"Expected only post-seed writes [20], got {values}"
ALL_DELTA_CHANNEL_HISTORY_TESTS = [
@@ -275,7 +305,7 @@ ALL_DELTA_CHANNEL_HISTORY_TESTS = [
test_history_empty_channels_returns_empty,
test_history_walk_to_root_no_seed,
test_history_migration_plain_value_as_seed,
test_history_seed_ancestor_own_writes_are_replayed,
test_history_migration_skips_seed_checkpoint_writes,
]
@@ -58,14 +58,8 @@ lint.select = [
"UP", # pyupgrade
"B", # flake8-bugbear
"I", # isort
"PLC0415", # import-outside-top-level
"RUF100", # unused noqa directive
]
lint.ignore = ["E501", "B008"]
# PLC0415 (import-outside-top-level) is enforced in tests only. Library code
# still has deferred imports that have not been reviewed, so it stays exempt
# for now.
lint.per-file-ignores = { "langgraph/**" = ["PLC0415"] }
target-version = "py310"
[tool.uv.sources]
+48 -48
View File
@@ -247,7 +247,7 @@ wheels = [
[[package]]
name = "langchain-core"
version = "1.5.2"
version = "1.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jsonpatch" },
@@ -260,26 +260,26 @@ dependencies = [
{ name = "typing-extensions" },
{ name = "uuid-utils" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6e/58/3ad53096eee1e07728e8219ded30ae308b0f2b8b7b26b8ebb6371917c0f5/langchain_core-1.5.2.tar.gz", hash = "sha256:2d13ab35b42eec63d4669a483776b8cdd778ee764107149369fb369d84c08c41", size = 972322, upload-time = "2026-07-28T16:38:37.977Z" }
sdist = { url = "https://files.pythonhosted.org/packages/59/de/679a53472c25860837e32c0442c962fa86e95317a36460e2c9d5c91b17c2/langchain_core-1.4.0.tar.gz", hash = "sha256:1dc341eed802ed9c117c0df3923c991e5e9e226571e5725c194eeb5bd93d1a7f", size = 920260, upload-time = "2026-05-11T18:42:35.919Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ca/e4/024e402a65f5ee2eb6ae9667b5ffc5f08776cb4e712f5a11ee1997d56083/langchain_core-1.5.2-py3-none-any.whl", hash = "sha256:a687dd7c3b22c6c1294e1c1eeb61fb6f3a308e6015a1d75b576b3836ad5b5aed", size = 561643, upload-time = "2026-07-28T16:38:36.38Z" },
{ url = "https://files.pythonhosted.org/packages/0f/1a/86c38c27b81913a1c6c12448cab55defb5a1097c7dc9a4cea83f55477a2d/langchain_core-1.4.0-py3-none-any.whl", hash = "sha256:23cbbdb46e38ddd1dd5247e6167e96013eae74bea4c5949c550809970a9e565c", size = 548120, upload-time = "2026-05-11T18:42:33.992Z" },
]
[[package]]
name = "langchain-protocol"
version = "0.0.18"
version = "0.0.15"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d2/59/b5959aea96faa9146e2e49a7a22882b3528c62efafe9a6a95beab30c2305/langchain_protocol-0.0.18.tar.gz", hash = "sha256:ec3e11782f1ed0c9db38e5a9ed01b0e7a0d3fba406faa8aef6594b73c56a63e6", size = 6150, upload-time = "2026-06-18T17:08:26.959Z" }
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" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/99/2e/d82db9eec13ad0f72e7aaad5c4bc730ab111934fdc83c85523206eb9b0a0/langchain_protocol-0.0.18-py3-none-any.whl", hash = "sha256:70b53a86fbf9cedc863555effe44da192ab02d556ddbf2cf95b8873adcf41b5a", size = 7221, upload-time = "2026-06-18T17:08:25.996Z" },
{ 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" },
]
[[package]]
name = "langgraph-checkpoint"
version = "4.2.0"
version = "4.1.1"
source = { editable = "../checkpoint" }
dependencies = [
{ name = "langchain-core" },
@@ -686,7 +686,7 @@ wheels = [
[[package]]
name = "pytest"
version = "9.1.1"
version = "9.0.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
@@ -697,9 +697,9 @@ dependencies = [
{ name = "pygments" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
]
[[package]]
@@ -809,27 +809,27 @@ wheels = [
[[package]]
name = "ruff"
version = "0.16.0"
version = "0.15.15"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4d/94/1e5e4967626faf12fa56999cd6222dff6992ceb086ad7945756baf70c7a7/ruff-0.16.0.tar.gz", hash = "sha256:e460aafd5495ec89efaa6ced2e4a9a581116451e1c88b9d37ef497e0f8e93982", size = 4790557, upload-time = "2026-07-23T19:11:30.981Z" }
sdist = { url = "https://files.pythonhosted.org/packages/84/6f/a76f7d96e5c962f5b69cee865e49c15c1116897c01990faa8a57edb62e7f/ruff-0.15.15.tar.gz", hash = "sha256:b8dff018130b46d8e5bf0f926ef6b60cf871d6d5ae45fc9334e09632daa741d6", size = 4706985, upload-time = "2026-05-28T14:16:57.784Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4b/81/1c8818fee7ce1a04cd7d1b3172e0a8f8e4f1dc4feb7fc390e16daa8af323/ruff-0.16.0-py3-none-linux_armv6l.whl", hash = "sha256:e5115729eb08c585e5121978ba5d5b60caeae394ce21b9fb5e6cd33a1c6c9b1e", size = 10754633, upload-time = "2026-07-23T19:10:46.415Z" },
{ url = "https://files.pythonhosted.org/packages/23/df/beaf59c09d68db84304d555f188b276a77132a5d5b0b67a5c762aa143628/ruff-0.16.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3c954b1d580bfa035b41654f7858cc7e71d5fc3ac5b723dd62bd9133830ed522", size = 10969164, upload-time = "2026-07-23T19:10:50.271Z" },
{ url = "https://files.pythonhosted.org/packages/42/ce/741cd197496a1abbf51352710fd15ed995d2a2be87189c1da26a450d6e83/ruff-0.16.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e01c21d10eb1b29f47b7454e1f4056db9a3f0260c646aa88457c610291db9f81", size = 10488846, upload-time = "2026-07-23T19:10:52.639Z" },
{ url = "https://files.pythonhosted.org/packages/52/2a/a2db8e88cade358f5cdcb05674a917751074109315d014eb6352d9a893f7/ruff-0.16.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e364e5ed22ed8dc05082fd78e35308618260907ac2d3c1d637b2e682415b6c9", size = 10889729, upload-time = "2026-07-23T19:10:54.89Z" },
{ url = "https://files.pythonhosted.org/packages/42/65/62a771694ebd63029dc953e27dbad40e1588bd4860ff9fe881018fddaa49/ruff-0.16.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d327b8fc113a1d4421a04f3839d3752057c8dd1ee320223a6f3f52d04ada462a", size = 10568275, upload-time = "2026-07-23T19:10:56.993Z" },
{ url = "https://files.pythonhosted.org/packages/3f/e2/ced249fe8af5f086c5c58cc21cc3356d50f32f7401c5df87050c999620a7/ruff-0.16.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b50c55e263103586b3dcf5f73d479eb8cb5fdb6098fec59a62891dab653717", size = 11385112, upload-time = "2026-07-23T19:10:59.615Z" },
{ url = "https://files.pythonhosted.org/packages/87/0b/05154977a8fd69eeb6c103271f55403bfd8711f5c0f8ed07489d95a504e7/ruff-0.16.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ff4a79ce3ec0172f3241943835de1c4cb4e2dcd07f0f8c2d02603dbbbee4b17", size = 12207008, upload-time = "2026-07-23T19:11:02.154Z" },
{ url = "https://files.pythonhosted.org/packages/fb/29/98225831a3a1eab0e02f4acc6ca6559a98611dcc68b6965ff4b7234627c1/ruff-0.16.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e95c448fca1fb2a18372a9440926c5a6ee789639bb975c72e7ae6d0b04218ab4", size = 11650842, upload-time = "2026-07-23T19:11:04.557Z" },
{ url = "https://files.pythonhosted.org/packages/91/66/6bd3cf90500653d55dc0ffc8507aa8300bd49d0214b2e8cb4d3fef2943ba/ruff-0.16.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f11a8d11010301d0a398a2fdef67691feca7294da6aef55e2150e8fa2cd520b", size = 11400718, upload-time = "2026-07-23T19:11:09.233Z" },
{ url = "https://files.pythonhosted.org/packages/8e/a2/a54eb4eae05d66364050a5d3b8a9c5ef88196531b3cbe7109d873f87f819/ruff-0.16.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:48044c678e9cb8698246c99b14aaccfa6601dea7379eb48a6f8f73f7a6d86cd0", size = 11426177, upload-time = "2026-07-23T19:11:11.994Z" },
{ url = "https://files.pythonhosted.org/packages/1a/be/16e3eea4b2a478a496919f5e36f17c4559e54620bd3bbac5d6affa068006/ruff-0.16.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7aa0959bad8eb8bef50340154fc9b58678dae31fa4293afa38b44b6e552c0213", size = 10856126, upload-time = "2026-07-23T19:11:14.221Z" },
{ url = "https://files.pythonhosted.org/packages/a2/84/252eb8b868a16eec7257c14f504f77537e734b2d69c762e639e588e304a3/ruff-0.16.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:28ea2b7df8ebf7f9da6b7d47b230ab48f387c0a29be3b474c4d0740e197bb9af", size = 10571208, upload-time = "2026-07-23T19:11:16.378Z" },
{ url = "https://files.pythonhosted.org/packages/21/09/817a482f542f7570cbb4554b26e896610c7114f539b1d9e2d2145bf6bef6/ruff-0.16.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:33a3dfac8c35f81498dea9181bccc2f4c4bc8f1521a1dd9406e77643e0f0fb09", size = 11063329, upload-time = "2026-07-23T19:11:19.173Z" },
{ url = "https://files.pythonhosted.org/packages/2e/23/9403c180ca1cb9b1f7335f5c3e5305c09d49ea5b345196682a36028bde4a/ruff-0.16.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a5237a0bda500d30d81b8e07a6973a5cbc772864cbf746ae2f4e8a2e01c9f4ed", size = 11489751, upload-time = "2026-07-23T19:11:21.74Z" },
{ url = "https://files.pythonhosted.org/packages/b2/1d/1b2ef7bcde851c78d7f17f1cca13fd6dc695fc4b3d6197941e72cae5b132/ruff-0.16.0-py3-none-win32.whl", hash = "sha256:7fab76fa065c873f41ff744347c6e77bcc3dfec4bcc754dc26b63d23c0f7f5fb", size = 10785885, upload-time = "2026-07-23T19:11:23.947Z" },
{ url = "https://files.pythonhosted.org/packages/b2/a3/d5e4ef7a56be3f928ffb90b94c25ba7d3cb9c7fe0736aeaaedf361770712/ruff-0.16.0-py3-none-win_amd64.whl", hash = "sha256:429c117f022bf481fabd9d551e7a3952b24c65e6ef44337ea09d90bebef14472", size = 11923141, upload-time = "2026-07-23T19:11:26.409Z" },
{ url = "https://files.pythonhosted.org/packages/cb/9a/8415f2657cbe200f41a4531ccededf135505a92d4a012229121f885b26f9/ruff-0.16.0-py3-none-win_arm64.whl", hash = "sha256:14296fedcd2705c77ab8235439278bbb38f285cf7da5528b00b3e330c3d4872d", size = 11273407, upload-time = "2026-07-23T19:11:28.705Z" },
{ url = "https://files.pythonhosted.org/packages/fa/9d/3a45c05b8ab04b4705989de70a79008e27c8003296a0feaee9edc18dd7e9/ruff-0.15.15-py3-none-linux_armv6l.whl", hash = "sha256:cf93e5388f412e1b108b1f8b34a6e036b70fe8aff89393befad96fe48670311b", size = 10710652, upload-time = "2026-05-28T14:16:06.701Z" },
{ url = "https://files.pythonhosted.org/packages/05/66/da974431624bf3b49f6ee1f9543c02d929ff1cba78b0d5a79c38cf21f744/ruff-0.15.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac5a646d1f6a7dadd5d50842dae2c1f9862ac887ef5d1b1375e02def791fde6e", size = 11096615, upload-time = "2026-05-28T14:16:23.313Z" },
{ url = "https://files.pythonhosted.org/packages/8c/09/7443452e5d290230a712103f2fdceeef7184f3ec99a2bd01c8be78aaceb5/ruff-0.15.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:77d955a431430c66f72dd94e379ad38a16daea3d25094872ac4edf9e797be530", size = 10436683, upload-time = "2026-05-28T14:16:40.974Z" },
{ url = "https://files.pythonhosted.org/packages/53/01/d330c26a57fa4f3943a14424904027428315b700fe4d14a84bb123a649e5/ruff-0.15.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7614ee79c69788cf6cedd568069ade9cecc22a1ad20494efe8d0c9ebb4b622d4", size = 10769064, upload-time = "2026-05-28T14:16:28.905Z" },
{ url = "https://files.pythonhosted.org/packages/1d/85/cc8770f8bdff541b1da8392d1634141fe4a0e3f4ee596605959b7906c27f/ruff-0.15.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3cdb1679e06a1f6b47bc384714ae96f6e2fb65ca441eb78c43d2ca554176ce1f", size = 10511987, upload-time = "2026-05-28T14:16:43.732Z" },
{ url = "https://files.pythonhosted.org/packages/7c/29/8c190c1472b63013583ba391f3342036e02010544c1270455ed8e519bdf3/ruff-0.15.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2728b93d7b23a603ea2c0ac6eb73d760bd38ec9de35f35fb41e18f7a3fee7622", size = 11275100, upload-time = "2026-05-28T14:16:55.244Z" },
{ url = "https://files.pythonhosted.org/packages/9f/6b/7e145ce2cc8e63d6834eca03d83a0e18d121def5c69f91b4cf4011ed4879/ruff-0.15.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be582fcc0db438902c7792b08d6ddf6c9b9e21addaa10092c2c741cfb09e5a45", size = 12176903, upload-time = "2026-05-28T14:16:14.368Z" },
{ url = "https://files.pythonhosted.org/packages/80/a3/d5974637f68e451f7fadf015cf3101d1cd7d8ba5027cffe0b9e3826ebe6b/ruff-0.15.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7aa77465b8ecaf1a27bea098d696f7fed5e1eccbd10b321b682d6de586ae5627", size = 11404550, upload-time = "2026-05-28T14:16:20.138Z" },
{ url = "https://files.pythonhosted.org/packages/fe/1c/e6e5e568f22be4fb05d6244234aba384c06b451252453b821e1a529263cf/ruff-0.15.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48decfa11d740de4889de623be1463308346312f2409a56e24aa280c86162dc4", size = 11382027, upload-time = "2026-05-28T14:16:46.615Z" },
{ url = "https://files.pythonhosted.org/packages/1d/01/170921b49fcd2e8858825593f91cf7146c3e40a5c3e6df763e4bb0484dde/ruff-0.15.15-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a5015088452ca0081387063649ec67f06d3d1d6b8b936a1f836b5e9657ecd48c", size = 11366041, upload-time = "2026-05-28T14:16:26.247Z" },
{ url = "https://files.pythonhosted.org/packages/87/54/a7bad711d7de93254e15e06a4c375b89a03d18de45d3e5dcc86a4472fb1a/ruff-0.15.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5294aab6356c81600fcdea3a62bb1b924dfd5e91767c12318d3f68f86af57cd", size = 10741795, upload-time = "2026-05-28T14:16:17.11Z" },
{ url = "https://files.pythonhosted.org/packages/c9/31/38c075963668f8b41c6914ee0f6f318727fbe30ab9145cb29e6df464c5fa/ruff-0.15.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:db5bd4d802415cca656dc1616070b725952d6ae95eb5d4831e49fbd94a38f75f", size = 10511117, upload-time = "2026-05-28T14:16:31.767Z" },
{ url = "https://files.pythonhosted.org/packages/9d/96/6ff689e1f7e375d1d97075eca022f74c2bab59554a432fe4d2e6f091986a/ruff-0.15.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:587a6278ed42059191c1a466e490bd7930fb50bd2e255398bc29616c895a61cb", size = 10994867, upload-time = "2026-05-28T14:16:35.149Z" },
{ url = "https://files.pythonhosted.org/packages/c3/c2/5dce0ab9f92a8d534fa62b9bf9caca3eddb8c1a81b616f5e195ada4f0d6e/ruff-0.15.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:df0c1c084f5f4be9812f61518a45c440d3c30d69ce4bf6c5270e66d38338f02a", size = 11482101, upload-time = "2026-05-28T14:16:49.598Z" },
{ url = "https://files.pythonhosted.org/packages/b1/c0/1003b60edd697c649faf61f1a34094b1abb38fb3d1181e3f895781250a08/ruff-0.15.15-py3-none-win32.whl", hash = "sha256:29428ea79694afbe756d45fd59b36f22b6b020dc0443cf7de0173046236964b9", size = 10716774, upload-time = "2026-05-28T14:16:52.337Z" },
{ url = "https://files.pythonhosted.org/packages/02/a8/1269eddd6945a06c23f055ef7848886e37cf9d6a8bebb386a3115f01470c/ruff-0.15.15-py3-none-win_amd64.whl", hash = "sha256:8df0323902e15e24bc4bf246da830573d3cf3352bd0b9a164eab335d111ff4a4", size = 11868463, upload-time = "2026-05-28T14:16:11.333Z" },
{ url = "https://files.pythonhosted.org/packages/4e/b2/920464c907b191e37469d477a1aa8bc048b8f36c4c1610dfa4ab87b39e18/ruff-0.15.15-py3-none-win_arm64.whl", hash = "sha256:3c8ceca6792f38196b8f589bc92eccd03eef286602da92e5dc05cc42ef6441b7", size = 11138498, upload-time = "2026-05-28T14:16:38.425Z" },
]
[[package]]
@@ -897,27 +897,27 @@ wheels = [
[[package]]
name = "ty"
version = "0.0.64"
version = "0.0.43"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8e/aa/14c9965d3b173105692473897cc89c34cd91241368b2044e43167e1c17ff/ty-0.0.64.tar.gz", hash = "sha256:d12ddbb05f15158bb518af619378b385486450def95fb06f8ab98037febe9f2c", size = 6350966, upload-time = "2026-07-27T18:32:45.403Z" }
sdist = { url = "https://files.pythonhosted.org/packages/0d/37/4ec04de0659b93be37d956dfceca13b1ecab9c959f28d8a1d5e514603f36/ty-0.0.43.tar.gz", hash = "sha256:ea4cff50548f2a1877e848d3abe9e293cde8ab94757a7eb93fc0d4013f98be8e", size = 5798429, upload-time = "2026-06-04T00:52:10.013Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ae/4c/c54937e4ff3fa7b34a99ea3387ec766bf0ad98dc8df8d792e89b388e658e/ty-0.0.64-py3-none-linux_armv6l.whl", hash = "sha256:3830a6675ab43635ced1c4c557f380ac4a49e9414e03975e4e4e8db644c64944", size = 12118357, upload-time = "2026-07-27T18:32:08.702Z" },
{ url = "https://files.pythonhosted.org/packages/ce/aa/a839ee2bc78e943d079e6abe199a97b4eeffb7e5c9a57326d69de452186a/ty-0.0.64-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3ff07d7bc32a2135f58afe57393789a32ea2fed1a66216129a8e535e76043903", size = 11790882, upload-time = "2026-07-27T18:32:10.997Z" },
{ url = "https://files.pythonhosted.org/packages/4c/de/19f14357888a7198438926303753cf749428e3d62e8980ff1e9a72a78402/ty-0.0.64-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4f6d1c7f897cca05d12bacbf1435150d5ffa496099515aa6ed303c8b29e1d0bb", size = 11317394, upload-time = "2026-07-27T18:32:13.162Z" },
{ url = "https://files.pythonhosted.org/packages/08/2f/f54462300535ab99b551eda733177be2eef5dbc2997d3fdb357c4ddd760a/ty-0.0.64-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:138d6c37ad4bf8583aa7a9b29d90954151d7856f9910a03eae3eb34b34c57215", size = 11863042, upload-time = "2026-07-27T18:32:15.307Z" },
{ url = "https://files.pythonhosted.org/packages/5e/95/dbecf745520ebe8bd7b02fc55eee6441c9be312ebf6addce605eb52740dd/ty-0.0.64-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ed9719d1b7b66fb8efe073d860208a44c40af1f6cd5c2364aa9b323a1e579b4", size = 11910730, upload-time = "2026-07-27T18:32:17.467Z" },
{ url = "https://files.pythonhosted.org/packages/3b/26/12cfd40028e51ceed7b3cb645281c61c02eb64ff9fb0c09231d65c30ff25/ty-0.0.64-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d68b23e5169e2137b5f1de7169ab0cebecab6c8eda374c34c1f6394308f58242", size = 12631936, upload-time = "2026-07-27T18:32:19.533Z" },
{ url = "https://files.pythonhosted.org/packages/a0/d0/65ffc2b0a686347193c6f98e9421a7fc2a96fc3cd0b1001cf7cf284baff9/ty-0.0.64-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f41cb07d89d32626fcaf3ed4d262778fcb28b2628b6ed1e172cd7b18820668d8", size = 13171049, upload-time = "2026-07-27T18:32:22.026Z" },
{ url = "https://files.pythonhosted.org/packages/f6/a4/975a5961842dcd6fa60f0770a102c0bba7509da909ab652919bfcdcd4fc7/ty-0.0.64-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5f24e1504ab9e212f92356b82fe088fdeb3a39f9a2f4ff25e505d2e1d0db9056", size = 12826438, upload-time = "2026-07-27T18:32:24.178Z" },
{ url = "https://files.pythonhosted.org/packages/af/ef/dfb9b7f9bcc032d3b540b0d1f55f532a336e2fb41b1bd539c05ae81a151a/ty-0.0.64-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86db830cb914bb33bb8247b66ccea58de4496c2391bd42658aba744b439f3290", size = 12440880, upload-time = "2026-07-27T18:32:26.341Z" },
{ url = "https://files.pythonhosted.org/packages/ee/d0/bedac20505e8a8f5501ad73d7d15d8e421a563fef59993909a23036929a4/ty-0.0.64-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:652ee3d6d03bea76cd2fe8949c78bb5970394ebd7c5fd270e9518c0dee1b931d", size = 12782439, upload-time = "2026-07-27T18:32:28.642Z" },
{ url = "https://files.pythonhosted.org/packages/79/d5/795733f13ceff1378f08b3de0c49d0f518df220ed856b0dfac869f3b7c81/ty-0.0.64-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4838768295774a86e95f9ec5633e739d016adbbb69dcbdc62f3549578a11f624", size = 11814821, upload-time = "2026-07-27T18:32:30.632Z" },
{ url = "https://files.pythonhosted.org/packages/52/3e/9d99cd1e1831003434f508ed9f258a56543194afc3bbe051eba2545fa676/ty-0.0.64-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5a3d700669868599edf39ce5125196682f15a8880cc8c669bc2a83b99984d99b", size = 11928678, upload-time = "2026-07-27T18:32:32.888Z" },
{ url = "https://files.pythonhosted.org/packages/b8/3d/448f49a3503fb119a34348a5714bb92001f252fadbbb12d645f2e744b557/ty-0.0.64-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b161f0a82a8e2f2432db3bf7702b4d3924fa9486ba0014f6710a160fc157df0d", size = 12202249, upload-time = "2026-07-27T18:32:34.905Z" },
{ url = "https://files.pythonhosted.org/packages/dd/9b/75768e562cec990d189dc05807ae72890b20ffbd1e1f43597bb98db63c60/ty-0.0.64-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:39b9dd42908df47c2dc57dda87e656fab97097ffd2618474bbdea986af0d6a9d", size = 12548817, upload-time = "2026-07-27T18:32:36.995Z" },
{ url = "https://files.pythonhosted.org/packages/34/89/44cc276ea6ca0245495014758ffeadde1798fb0b5840c9cf36d8d2ed3250/ty-0.0.64-py3-none-win32.whl", hash = "sha256:d0676ab0e0935795e5843baa28dd5e366dc343d7c0945a996df9eab8e0644885", size = 11545474, upload-time = "2026-07-27T18:32:39.389Z" },
{ url = "https://files.pythonhosted.org/packages/01/7e/d1c8a871a38d17c8f168b9a6975f6247f7660f8334e517656a5e4b4a4858/ty-0.0.64-py3-none-win_amd64.whl", hash = "sha256:dcb9bd31f54097e362b776c26ab4564d4564cdd1355cb883481167a26c03cc3f", size = 12542987, upload-time = "2026-07-27T18:32:41.412Z" },
{ url = "https://files.pythonhosted.org/packages/35/4d/6d18640d0204cacd69abbaca95ad6a34c6d7e9169e9051d0117b17b827ec/ty-0.0.64-py3-none-win_arm64.whl", hash = "sha256:82cc34c1ad9a8feb6059aef193bebcecc656e548f6fab3d518bcd8b57d198d39", size = 11899263, upload-time = "2026-07-27T18:32:43.366Z" },
{ url = "https://files.pythonhosted.org/packages/db/74/1916026a78f20019a2f03adbd6fb4430ddb7ce1e52c2e17a90856a6d192e/ty-0.0.43-py3-none-linux_armv6l.whl", hash = "sha256:3bf70f5446480562bf6c9f639df4b5cb60716b8f8d1a6b8e5811d5c7eccd8bf2", size = 11598153, upload-time = "2026-06-04T00:52:20.646Z" },
{ url = "https://files.pythonhosted.org/packages/b9/af/58bb0089d2635216c8fa6612dd486a3f986d0ab1c46a41527ab95e57f0e3/ty-0.0.43-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7184741f8b15425a1bc64b950ad005cb353573288ac0e8a04f5481ceb3832596", size = 11357811, upload-time = "2026-06-04T00:52:24.683Z" },
{ url = "https://files.pythonhosted.org/packages/d6/9c/32c6b14f3feddf87b59c7a50709e2b3da408258f2f583f05575f77bc8f7b/ty-0.0.43-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8c306379ca9a35f6ae5270fe9bda7af4b46d91822725a2586d78c8b9b5493b62", size = 10772024, upload-time = "2026-06-04T00:52:14.312Z" },
{ url = "https://files.pythonhosted.org/packages/09/fa/98aa4a74bd00cd5efc424923cd1daffbf1e40a0338041cafb203379d746f/ty-0.0.43-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d624b884c9c1fd244ad2a5f026364e7162a22b3f537025941ada2e363e676414", size = 11291034, upload-time = "2026-06-04T00:52:37.249Z" },
{ url = "https://files.pythonhosted.org/packages/b5/db/4de086c38ce96dcada2bd451f43171d2c237f96d8ed19a1ea8fe51bb8ef4/ty-0.0.43-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:281fc4c00fbc196045141faa085055bddc58846b04a2800204701415a1b9c6aa", size = 11364724, upload-time = "2026-06-04T00:52:33.138Z" },
{ url = "https://files.pythonhosted.org/packages/b0/d3/e3cd8e3233a6fd8362a49aa025b79e9f40151a2a86d811ace154c6eb7445/ty-0.0.43-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f57d6cc28de89024b48d1788e4758c05299d5749d4a51c02e71ac655ec23d9a5", size = 11890555, upload-time = "2026-06-04T00:52:22.711Z" },
{ url = "https://files.pythonhosted.org/packages/80/7b/6f46d444e8241606bbde098df3dca93f2ec0b834a42055db85ee7d33646f/ty-0.0.43-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0a1d6ad6c5e7792c7eac0a01e550f2c2004462e01a64a91ea1636aba6fef6e71", size = 12450968, upload-time = "2026-06-04T00:52:28.94Z" },
{ url = "https://files.pythonhosted.org/packages/4a/e1/79fbe51f2e4b9d8347f2013cd7ed0b63f3b499038c02dc0357e9b28a3a47/ty-0.0.43-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:66d474395d7635fb618bdbb58b4e3360259a2056d0a5621b82754b9da2cd8a04", size = 12064187, upload-time = "2026-06-04T00:52:12.039Z" },
{ url = "https://files.pythonhosted.org/packages/9b/3f/c758a3a8df5b90d331f2b60c8f16021ee64d75e78f99d67cc4efc9bf5f4b/ty-0.0.43-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2663a0003a8b60fb98db7f6f6e673df80b21d0fe3a9868a26fb06b4e049b6fc4", size = 11943208, upload-time = "2026-06-04T00:52:31.14Z" },
{ url = "https://files.pythonhosted.org/packages/54/5f/f516442749cf1b45ca6720a5d41df2738a486ed9ace774c03d515db89084/ty-0.0.43-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:d5a6c352d374d889189d5ec82b54b26a5885f769f7b7787f7f875500dcb8673e", size = 12143572, upload-time = "2026-06-04T00:52:18.457Z" },
{ url = "https://files.pythonhosted.org/packages/b7/bf/0d83c7f43bf4c10f3678bfe7d938e51c445298c7b923f155c5204730c2df/ty-0.0.43-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e7dbbeedfad3ca250d74fcc355fa9ab6b38d2a17f22d6304f615716939dbbb27", size = 11279355, upload-time = "2026-06-04T00:52:26.726Z" },
{ url = "https://files.pythonhosted.org/packages/3e/de/a6c978bef6d9e949f79f4782d9e4ee4df0893713e73b055d84c1a5116b9a/ty-0.0.43-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:24b18a0273ee46154996cfcfa27438f851f440c925587ec200df6f98dffe67d3", size = 11408412, upload-time = "2026-06-04T00:52:35.282Z" },
{ url = "https://files.pythonhosted.org/packages/ec/b1/d13857c23867f0f76b92e38e5841c64ca5e76dc5d4bf27f52cb81d8ab685/ty-0.0.43-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2ef681951520d692b7e9c0b5e56aacf4f98ccae47cf6ffccaf2c7b6b33dc226e", size = 11541709, upload-time = "2026-06-04T00:52:16.451Z" },
{ url = "https://files.pythonhosted.org/packages/7c/f1/cd6afc6f6a687e238bf5e12189f7920e81a0bdef6c3dba4c784ef140f7d9/ty-0.0.43-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2af105de7437143aa4676b28016b5bee661aaaa4eff52be5867fb25119641ceb", size = 12041266, upload-time = "2026-06-04T00:52:43.541Z" },
{ url = "https://files.pythonhosted.org/packages/bd/ba/51ca7c3335da2b8d0a3e477fa4986be9f4a53b05bfab862967d8d2e6ca60/ty-0.0.43-py3-none-win32.whl", hash = "sha256:e4773115b0d6486ee30f1657fc8bdffe7e3a3f5300ab77ef2495da6e83e4694f", size = 10858724, upload-time = "2026-06-04T00:52:07.843Z" },
{ url = "https://files.pythonhosted.org/packages/9f/29/5d80453e5f7c520145fa058851da87230dbd7ca761a7675447a9fe504e0b/ty-0.0.43-py3-none-win_amd64.whl", hash = "sha256:48d3545094a4ae6395492c7e6ac90550fce969e0ed2815fbf8c5da9756676b7d", size = 11976157, upload-time = "2026-06-04T00:52:41.438Z" },
{ url = "https://files.pythonhosted.org/packages/dc/ed/befe5a543e5b95e754ed38ee95239e44efda9bc5f578db4ac1bc8dd758d6/ty-0.0.43-py3-none-win_arm64.whl", hash = "sha256:740ca33d7f75f655a4e7d475bc42dfb825c13219bb073fad30fcc04d35790c74", size = 11308680, upload-time = "2026-06-04T00:52:39.233Z" },
]
[[package]]
+32 -8
View File
@@ -67,12 +67,24 @@ with PostgresSaver.from_conn_string(DB_URI) as checkpointer:
"v": 4,
"ts": "2024-07-31T20:14:19.804150+00:00",
"id": "1ef4f797-8335-6428-8001-8a1503f9b875",
"channel_values": {"my_key": "meow", "node": "node"},
"channel_versions": {"__start__": 2, "my_key": 3, "start:node": 3, "node": 3},
"channel_values": {
"my_key": "meow",
"node": "node"
},
"channel_versions": {
"__start__": 2,
"my_key": 3,
"start:node": 3,
"node": 3
},
"versions_seen": {
"__input__": {},
"__start__": {"__start__": 1},
"node": {"start:node": 2},
"__start__": {
"__start__": 1
},
"node": {
"start:node": 2
}
},
}
@@ -96,12 +108,24 @@ async with AsyncPostgresSaver.from_conn_string(DB_URI) as checkpointer:
"v": 4,
"ts": "2024-07-31T20:14:19.804150+00:00",
"id": "1ef4f797-8335-6428-8001-8a1503f9b875",
"channel_values": {"my_key": "meow", "node": "node"},
"channel_versions": {"__start__": 2, "my_key": 3, "start:node": 3, "node": 3},
"channel_values": {
"my_key": "meow",
"node": "node"
},
"channel_versions": {
"__start__": 2,
"my_key": 3,
"start:node": 3,
"node": 3
},
"versions_seen": {
"__input__": {},
"__start__": {"__start__": 1},
"node": {"start:node": 2},
"__start__": {
"__start__": 1
},
"node": {
"start:node": 2
}
},
}
@@ -14,6 +14,7 @@ from langgraph.checkpoint.base import (
CheckpointMetadata,
CheckpointTuple,
DeltaChannelHistory,
_parse_supersteps_since_last_snapshot_by_channel,
get_checkpoint_id,
get_serializable_checkpoint_metadata,
)
@@ -28,9 +29,11 @@ from langgraph.checkpoint.postgres import _internal
from langgraph.checkpoint.postgres.base import (
_DELTA_PAGE_SIZE,
BasePostgresSaver,
_build_delta_stage1_sql,
_build_delta_stage2_sql,
_advance_shared_chain,
_build_delta_fetch_sql,
_build_delta_walk_sql,
_DeltaStage2Row,
_ingest_walk_page,
)
from langgraph.checkpoint.postgres.shallow import ShallowPostgresSaver
@@ -223,7 +226,7 @@ class PostgresSaver(BasePostgresSaver):
>>> checkpoint_tuple = memory.get_tuple(config)
>>> print(checkpoint_tuple)
CheckpointTuple(...)
"""
""" # noqa
thread_id = config["configurable"]["thread_id"]
checkpoint_id = get_checkpoint_id(config)
checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
@@ -446,114 +449,137 @@ class PostgresSaver(BasePostgresSaver):
) -> Mapping[str, DeltaChannelHistory]:
"""Fast-path override of `BaseCheckpointSaver.get_delta_channel_history`.
Two-stage query, both stages cover ALL requested channels:
Reconstructs each delta channel's state for the target checkpoint by
walking the parent chain `supersteps` hops (read from the target's
`counters_since_delta_snapshot` metadata) to its seed snapshot, then
collecting the writes between the seed and the target.
* Stage 1 (paged): dynamic SELECT over `checkpoints` with K parallel
JSONB key lookups (one column pair per channel) — no subquery, no
aggregation. Pages newest-first by `checkpoint_id` with a cursor;
page size is `_DELTA_PAGE_SIZE`. Stops paging when every channel
has found its seed or the chain is exhausted.
Two passes (see the `# Multi-channel two-pass` comment in `base.py`):
* Stage 2 (per-channel UNION ALL): one branch per channel reading
`checkpoint_writes` filtered to that channel's specific
`chain_cids`, plus one branch per channel that has a seed reading
`checkpoint_blobs` for that channel + version. Avoids the
over-fetch of a single `channel = ANY(channels)` filter when
channels have different chain depths.
* WALK (paged): dynamic SELECT over `checkpoints` with K parallel
JSONB lookups for `channel_versions[ch]` (the seed blob version),
following `parent_checkpoint_id` newest-first. Page size is
`_DELTA_PAGE_SIZE`; stops once the shared chain reaches the deepest
requested `supersteps`, the root is reached, or the chain is
exhausted.
* FETCH (per-channel UNION ALL): one branch per channel reading
`checkpoint_writes` for its `chain_cids`, plus one branch per
channel that located a seed reading `checkpoint_blobs` at the
seed's version.
"""
if not channels:
return {}
channels = list(channels)
thread_id = config["configurable"]["thread_id"]
if not thread_id:
raise ValueError("empty thread ID")
checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
checkpoint_id = get_checkpoint_id(config)
if checkpoint_id is None:
target = self.get_tuple(config)
if target is None:
return {ch: {"writes": []} for ch in channels}
checkpoint_id = target.config["configurable"]["checkpoint_id"]
# Stage 1: paged K-JSONB-lookup scan, walking the parent chain in
# Python after each page. Stops as soon as every channel has its seed.
stage1_sql = _build_delta_stage1_sql(channels, paged=True)
# Resolve the target checkpoint id + its metadata (for supersteps).
checkpoint_id = get_checkpoint_id(config)
with self._cursor() as cur:
if checkpoint_id is None:
cur.execute(
"SELECT checkpoint_id, metadata FROM checkpoints "
"WHERE thread_id = %s AND checkpoint_ns = %s "
"ORDER BY checkpoint_id DESC LIMIT 1",
(thread_id, checkpoint_ns),
)
else:
cur.execute(
"SELECT checkpoint_id, metadata FROM checkpoints "
"WHERE thread_id = %s AND checkpoint_ns = %s "
"AND checkpoint_id = %s",
(thread_id, checkpoint_ns, checkpoint_id),
)
target_row = cur.fetchone()
if target_row is None:
return {ch: {"writes": []} for ch in channels}
target_id = cast(str, target_row["checkpoint_id"])
supersteps_by_ch = _parse_supersteps_since_last_snapshot_by_channel(
target_row["metadata"] or {}, channels
)
max_supersteps = max(supersteps_by_ch.values(), default=0)
# WALK: page the parent chain, bounded by the deepest supersteps.
walk_sql = _build_delta_walk_sql(channels)
parent_of: dict[str, str | None] = {}
ver_by_i_by_cid: list[dict[str, str | None]] = [{} for _ in channels]
hb_by_i_by_cid: list[dict[str, bool]] = [{} for _ in channels]
inline_by_i_by_cid: list[dict[str, Any]] = [{} for _ in channels]
chain_by_ch: dict[str, list[str]] = {ch: [] for ch in channels}
seed_ver_by_ch: dict[str, str | None] = {ch: None for ch in channels}
seed_inline_by_ch: dict[str, Any] = {}
walk_cursor_by_ch: dict[str, str | None] = {}
seeded: set[str] = set()
shared_cpid_chain: list[str] = []
has_reached_root = False
cursor: str | None = None
with self._cursor() as cur:
while True:
stage1_params: list[Any] = []
for ch in channels:
# ver_i, blob channel, blob version, inline_i
stage1_params.extend([ch, ch, ch, ch])
stage1_params.extend(
[thread_id, checkpoint_ns, cursor, cursor, _DELTA_PAGE_SIZE]
)
cur.execute(stage1_sql, stage1_params)
while max_supersteps > 0:
walk_params: list[Any] = [
*channels,
thread_id,
checkpoint_ns,
cursor,
cursor,
_DELTA_PAGE_SIZE,
]
with self._cursor() as cur:
cur.execute(walk_sql, walk_params)
page = cur.fetchall()
if not page:
break
oldest = self._ingest_stage1_page(
cast("list[Mapping[str, Any]]", page),
channels,
parent_of,
ver_by_i_by_cid,
hb_by_i_by_cid,
inline_by_i_by_cid,
)
self._try_advance_walks(
checkpoint_id,
channels,
parent_of,
ver_by_i_by_cid,
hb_by_i_by_cid,
inline_by_i_by_cid,
chain_by_ch,
seed_ver_by_ch,
seed_inline_by_ch,
walk_cursor_by_ch,
seeded,
)
# Stop if every channel is seeded, or the page was short
# (chain exhausted — no more rows to fetch).
if len(seeded) == len(channels) or len(page) < _DELTA_PAGE_SIZE:
break
cursor = oldest
if not page:
break
oldest = _ingest_walk_page(
cast("list[Mapping[str, Any]]", page),
channels,
parent_of,
ver_by_i_by_cid,
)
has_reached_root = _advance_shared_chain(
target_id, parent_of, shared_cpid_chain, max_supersteps
)
if (
has_reached_root
or len(shared_cpid_chain) >= max_supersteps
or len(page) < _DELTA_PAGE_SIZE
):
break
cursor = oldest
# Stage 2: per-channel UNION ALL — one writes branch per channel
# with non-empty chain, plus one blob branch per seeded channel.
channels_with_chain = [ch for ch in channels if chain_by_ch[ch]]
chained_cpid_by_ch, seed_cpid_by_ch, seed_ver_by_ch = (
self._resolve_delta_chains(
channels,
supersteps_by_ch,
shared_cpid_chain,
ver_by_i_by_cid,
has_reached_root,
thread_id,
)
)
# FETCH: per-channel UNION ALL — writes branch per chained channel,
# blob branch per seeded channel.
channels_with_chain = [ch for ch in channels if chained_cpid_by_ch[ch]]
channels_with_seed = [ch for ch in channels if seed_ver_by_ch[ch] is not None]
stage2_sql = _build_delta_stage2_sql(
fetch_sql = _build_delta_fetch_sql(
channels_with_chain=channels_with_chain,
channels_with_seed=channels_with_seed,
)
if stage2_sql:
stage2_params: list[Any] = []
if fetch_sql:
fetch_params: list[Any] = []
for ch in channels_with_chain:
stage2_params.extend([thread_id, checkpoint_ns, ch, chain_by_ch[ch]])
fetch_params.extend(
[thread_id, checkpoint_ns, ch, chained_cpid_by_ch[ch]]
)
for ch in channels_with_seed:
stage2_params.extend([thread_id, checkpoint_ns, ch, seed_ver_by_ch[ch]])
fetch_params.extend([thread_id, checkpoint_ns, ch, seed_ver_by_ch[ch]])
with self._cursor() as cur:
cur.execute(stage2_sql, stage2_params)
stage2_rows = cur.fetchall()
cur.execute(fetch_sql, fetch_params)
fetch_rows = cur.fetchall()
else:
stage2_rows = []
fetch_rows = []
return self._build_delta_channels_writes_history(
return self._assemble_delta_history(
channels=channels,
chain_by_ch=chain_by_ch,
chained_cpid_by_ch=chained_cpid_by_ch,
seed_cpid_by_ch=seed_cpid_by_ch,
seed_ver_by_ch=seed_ver_by_ch,
seed_inline_by_ch=seed_inline_by_ch,
stage2_rows=cast("list[_DeltaStage2Row]", stage2_rows),
fetch_rows=cast("list[_DeltaStage2Row]", fetch_rows),
)
def _load_checkpoint_tuple(self, value: DictRow) -> CheckpointTuple:
@@ -14,6 +14,7 @@ from langgraph.checkpoint.base import (
CheckpointMetadata,
CheckpointTuple,
DeltaChannelHistory,
_parse_supersteps_since_last_snapshot_by_channel,
get_checkpoint_id,
get_serializable_checkpoint_metadata,
)
@@ -28,9 +29,11 @@ from langgraph.checkpoint.postgres import _ainternal
from langgraph.checkpoint.postgres.base import (
_DELTA_PAGE_SIZE,
BasePostgresSaver,
_build_delta_stage1_sql,
_build_delta_stage2_sql,
_advance_shared_chain,
_build_delta_fetch_sql,
_build_delta_walk_sql,
_DeltaStage2Row,
_ingest_walk_page,
)
from langgraph.checkpoint.postgres.shallow import AsyncShallowPostgresSaver
@@ -408,96 +411,122 @@ class AsyncPostgresSaver(BasePostgresSaver):
"""Fast-path override of `BaseCheckpointSaver.aget_delta_channel_history`.
See `PostgresSaver.get_delta_channel_history` for design notes; this is
the async equivalent with internal stage-1 paging and per-channel
UNION ALL stage-2.
the async equivalent: paged supersteps-bounded WALK + per-channel
UNION ALL FETCH.
"""
if not channels:
return {}
channels = list(channels)
thread_id = config["configurable"]["thread_id"]
if not thread_id:
raise ValueError("empty thread ID")
checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
checkpoint_id = get_checkpoint_id(config)
if checkpoint_id is None:
target = await self.aget_tuple(config)
if target is None:
return {ch: {"writes": []} for ch in channels}
checkpoint_id = target.config["configurable"]["checkpoint_id"]
stage1_sql = _build_delta_stage1_sql(channels, paged=True)
# Resolve the target checkpoint id + its metadata (for supersteps).
checkpoint_id = get_checkpoint_id(config)
async with self._cursor() as cur:
if checkpoint_id is None:
await cur.execute(
"SELECT checkpoint_id, metadata FROM checkpoints "
"WHERE thread_id = %s AND checkpoint_ns = %s "
"ORDER BY checkpoint_id DESC LIMIT 1",
(thread_id, checkpoint_ns),
)
else:
await cur.execute(
"SELECT checkpoint_id, metadata FROM checkpoints "
"WHERE thread_id = %s AND checkpoint_ns = %s "
"AND checkpoint_id = %s",
(thread_id, checkpoint_ns, checkpoint_id),
)
target_row = await cur.fetchone()
if target_row is None:
return {ch: {"writes": []} for ch in channels}
target_id = cast(str, target_row["checkpoint_id"])
supersteps_by_ch = _parse_supersteps_since_last_snapshot_by_channel(
target_row["metadata"] or {}, channels
)
max_supersteps = max(supersteps_by_ch.values(), default=0)
# WALK: page the parent chain, bounded by the deepest supersteps.
walk_sql = _build_delta_walk_sql(channels)
parent_of: dict[str, str | None] = {}
ver_by_i_by_cid: list[dict[str, str | None]] = [{} for _ in channels]
hb_by_i_by_cid: list[dict[str, bool]] = [{} for _ in channels]
inline_by_i_by_cid: list[dict[str, Any]] = [{} for _ in channels]
chain_by_ch: dict[str, list[str]] = {ch: [] for ch in channels}
seed_ver_by_ch: dict[str, str | None] = {ch: None for ch in channels}
seed_inline_by_ch: dict[str, Any] = {}
walk_cursor_by_ch: dict[str, str | None] = {}
seeded: set[str] = set()
shared_cpid_chain: list[str] = []
has_reached_root = False
cursor: str | None = None
async with self._cursor() as cur:
while True:
stage1_params: list[Any] = []
for ch in channels:
# ver_i, blob channel, blob version, inline_i
stage1_params.extend([ch, ch, ch, ch])
stage1_params.extend(
[thread_id, checkpoint_ns, cursor, cursor, _DELTA_PAGE_SIZE]
)
await cur.execute(stage1_sql, stage1_params)
while max_supersteps > 0:
walk_params: list[Any] = [
*channels,
thread_id,
checkpoint_ns,
cursor,
cursor,
_DELTA_PAGE_SIZE,
]
async with self._cursor() as cur:
await cur.execute(walk_sql, walk_params)
page = await cur.fetchall()
if not page:
break
oldest = self._ingest_stage1_page(
cast("list[Mapping[str, Any]]", page),
channels,
parent_of,
ver_by_i_by_cid,
hb_by_i_by_cid,
inline_by_i_by_cid,
)
self._try_advance_walks(
checkpoint_id,
channels,
parent_of,
ver_by_i_by_cid,
hb_by_i_by_cid,
inline_by_i_by_cid,
chain_by_ch,
seed_ver_by_ch,
seed_inline_by_ch,
walk_cursor_by_ch,
seeded,
)
if len(seeded) == len(channels) or len(page) < _DELTA_PAGE_SIZE:
break
cursor = oldest
if not page:
break
oldest = _ingest_walk_page(
cast("list[Mapping[str, Any]]", page),
channels,
parent_of,
ver_by_i_by_cid,
)
has_reached_root = _advance_shared_chain(
target_id, parent_of, shared_cpid_chain, max_supersteps
)
if (
has_reached_root
or len(shared_cpid_chain) >= max_supersteps
or len(page) < _DELTA_PAGE_SIZE
):
break
cursor = oldest
channels_with_chain = [ch for ch in channels if chain_by_ch[ch]]
chained_cpid_by_ch, seed_cpid_by_ch, seed_ver_by_ch = (
self._resolve_delta_chains(
channels,
supersteps_by_ch,
shared_cpid_chain,
ver_by_i_by_cid,
has_reached_root,
thread_id,
)
)
# FETCH: per-channel UNION ALL — writes branch per chained channel,
# blob branch per seeded channel.
channels_with_chain = [ch for ch in channels if chained_cpid_by_ch[ch]]
channels_with_seed = [ch for ch in channels if seed_ver_by_ch[ch] is not None]
stage2_sql = _build_delta_stage2_sql(
fetch_sql = _build_delta_fetch_sql(
channels_with_chain=channels_with_chain,
channels_with_seed=channels_with_seed,
)
if stage2_sql:
stage2_params: list[Any] = []
if fetch_sql:
fetch_params: list[Any] = []
for ch in channels_with_chain:
stage2_params.extend([thread_id, checkpoint_ns, ch, chain_by_ch[ch]])
fetch_params.extend(
[thread_id, checkpoint_ns, ch, chained_cpid_by_ch[ch]]
)
for ch in channels_with_seed:
stage2_params.extend([thread_id, checkpoint_ns, ch, seed_ver_by_ch[ch]])
fetch_params.extend([thread_id, checkpoint_ns, ch, seed_ver_by_ch[ch]])
async with self._cursor() as cur:
await cur.execute(stage2_sql, stage2_params)
stage2_rows = await cur.fetchall()
await cur.execute(fetch_sql, fetch_params)
fetch_rows = await cur.fetchall()
else:
stage2_rows = []
fetch_rows = []
return self._build_delta_channels_writes_history(
return self._assemble_delta_history(
channels=channels,
chain_by_ch=chain_by_ch,
chained_cpid_by_ch=chained_cpid_by_ch,
seed_cpid_by_ch=seed_cpid_by_ch,
seed_ver_by_ch=seed_ver_by_ch,
seed_inline_by_ch=seed_inline_by_ch,
stage2_rows=cast("list[_DeltaStage2Row]", stage2_rows),
fetch_rows=cast("list[_DeltaStage2Row]", fetch_rows),
)
async def _load_checkpoint_tuple(self, value: DictRow) -> CheckpointTuple:
@@ -580,7 +609,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
while True:
try:
yield asyncio.run_coroutine_threadsafe(
anext(aiter_), # type: ignore[arg-type]
anext(aiter_), # type: ignore[arg-type] # noqa: F821
self.loop,
).result()
except StopAsyncIteration:
@@ -1,5 +1,6 @@
from __future__ import annotations
import logging
import random
import warnings
from collections.abc import Mapping, Sequence
@@ -15,10 +16,12 @@ from langgraph.checkpoint.base import (
PendingWrite,
get_checkpoint_id,
)
from langgraph.checkpoint.serde.types import TASKS
from langgraph.checkpoint.serde.types import TASKS, _DeltaSnapshot
from psycopg.types.json import Jsonb
# Page size for stage-1 paged scan in `get_delta_channel_history`. Internal
logger = logging.getLogger(__name__)
# Page size for the paged WALK scan in `get_delta_channel_history`. Internal
# constant — exposing this as a kwarg is left as a follow-up.
_DELTA_PAGE_SIZE = 1024
@@ -160,7 +163,7 @@ INSERT_CHECKPOINT_WRITES_SQL = """
class _DeltaStage2Row(TypedDict, total=False):
"""One row from `_build_delta_stage2_sql` (a UNION ALL of writes and blobs)."""
"""One row from `_build_delta_fetch_sql` (a UNION ALL of writes and blobs)."""
_kind: str # "w" or "b"
checkpoint_id: str | None # "w" rows only
@@ -172,135 +175,97 @@ class _DeltaStage2Row(TypedDict, total=False):
version: str | None # "b" rows only
# Multi-channel two-stage DeltaChannel reconstruction.
# Multi-channel two-pass DeltaChannel reconstruction.
#
# Stage 1 scans checkpoint metadata (no blob bytes) and emits one row per
# checkpoint with K parallel JSONB key lookups (one column pair per
# requested delta channel: ver_i / hs_i). No subqueries, no aggregation.
# Python walks the parent chain once across all channels.
# A `DeltaChannel` does not store its full value at every checkpoint — it
# stores periodic full-value *snapshots* and accumulates intermediate
# *writes* between snapshots. To rebuild a channel's value at a target
# checkpoint we need:
#
# Stage 2 fetches all writes and the seed blobs for ALL channels in a
# single roundtrip via `channel = ANY(%s)` and chain/seed-version
# filtering.
# - the **seed** — the most recent snapshot at-or-before the target
# (a single blob row in `checkpoint_blobs`); and
# - the **chain writes** — every write for this channel committed
# between that snapshot and the target, in order
# (rows in `checkpoint_writes`).
#
# Empirical comparison vs an alternative "ship full channel_versions /
# channel_values JSONB and let Python pick" form (1000 checkpoints,
# 8 total channels in graph, 3 delta channels requested):
# Two passes, in order:
#
# Postgres execution: A=0.24ms vs B=0.38ms (both negligible)
# End-to-end latency: A=6.83ms vs B=2.28ms (B is 3.0x faster)
# Wire payload: A=836KB vs B=330KB (61% smaller)
# Buffer hits: identical (167 blocks)
# 1. WALK — scan checkpoint metadata only (no blob bytes). For each
# requested channel we read `channel_versions[ch]` (the seed's blob
# version pointer). Then follow `parent_checkpoint_id` from the
# target backwards in pages of `_DELTA_PAGE_SIZE` rows.
#
# B (this dynamic-columns design) wins because it avoids JSONB
# serialization on the wire and JSONB-to-dict deserialization in
# psycopg. Even at K=8 (8 delta channels = 16 dynamic columns), B
# still beats A end-to-end (4.2ms vs 6.8ms).
# Walk depth is driven by the *supersteps since last snapshot*
# counter — `metadata.counters_since_delta_snapshot[ch][1]` — read
# from the target checkpoint. A channel's seed snapshot sits exactly
# `supersteps` hops back along the parent chain; walking by the
# counter (rather than scanning `channel_values` for the snapshot
# marker) is the only reliable way to locate seeds that aren't a
# `_DeltaSnapshot` sentinel — e.g. legacy plain-value blobs left by a
# thread that migrated from a non-delta channel, which `put` stores
# out of the inline `channel_values` map.
#
# 2. FETCH — given each channel's chained checkpoint ids and seed
# version from WALK, pull only the rows we need: writes for those
# exact checkpoint_ids and the seed blob at that exact version. One
# roundtrip, per-channel UNION ALL — no over-fetch.
#
# Walking the *parent chain* (not `list(before=...)`) matters: forked
# threads have multiple branches, and only on-path ancestors contribute.
def _build_delta_stage1_sql(channels: Sequence[str], *, paged: bool) -> str:
"""Build stage 1 SQL with K parallel version lookups + seed probes.
def _build_delta_walk_sql(channels: Sequence[str]) -> str:
"""Build the paged WALK SQL — scans checkpoint metadata only.
For channels=["messages", "files"] (with `paged=True`) the result is::
Emits one row per checkpoint with K parallel JSONB key lookups (one
`ver_i` column per requested channel — the channel's blob version, the
pointer we dereference in FETCH if this checkpoint is the seed). No
blob bytes; the result set fits a paged `LIMIT` cleanly.
For channels=["messages", "files"] the result is::
SELECT checkpoint_id, parent_checkpoint_id,
checkpoint -> 'channel_versions' ->> %s AS ver_0,
EXISTS (SELECT 1 FROM checkpoint_blobs b0
WHERE b0.thread_id = checkpoints.thread_id
AND b0.checkpoint_ns = checkpoints.checkpoint_ns
AND b0.channel = %s
AND b0.version = checkpoint -> 'channel_versions' ->> %s
AND b0.type <> 'empty') AS hb_0,
checkpoint -> 'channel_values' -> %s AS inline_0,
checkpoint -> 'channel_versions' ->> %s AS ver_1,
EXISTS (...) AS hb_1,
checkpoint -> 'channel_values' -> %s AS inline_1
checkpoint -> 'channel_versions' ->> %s AS ver_1
FROM checkpoints
WHERE thread_id = %s AND checkpoint_ns = %s
AND (%s::text IS NULL OR checkpoint_id < %s)
ORDER BY checkpoint_id DESC
LIMIT %s
A stored value for a channel lives in one of two places, because `put`
splits them:
* **blob** — non-primitive values (and `_DeltaSnapshot`) are moved to
`checkpoint_blobs`. `hb_i` ("has blob") probes for one. The probe hits
that table's primary key `(thread_id, checkpoint_ns, channel, version)`
exactly, so it is an index lookup per row per channel.
* **inline** — `None`, `str`, `int`, `float` and `bool` stay in the
checkpoint's own `channel_values` and get no blob row at all. `inline_i`
returns that value.
Testing only for a key in `channel_values` (the previous approach) missed
blob-stored plain values, since `put` leaves an inline marker there for
`_DeltaSnapshot` but not for a plain value — which is what a thread
migrated from a pre-delta channel type leaves behind. Probing only the
blobs table would conversely miss inline primitives. Both are needed, and
the caller treats "either present" as the seed.
`hb_i` also disambiguates the two: for a `_DeltaSnapshot`, `inline_i` is the
literal `true` marker rather than the value, so a blob must win over an
inline reading whenever one exists. That ordering is what makes a genuine
inline `true` (a bool channel) distinguishable from the marker.
The `type <> 'empty'` predicate mirrors the check stage 2 already applies
when resolving the seed blob. `put` does not currently produce `empty` rows
on this path — `blob_versions` is filtered to keys present in
`channel_values`, so `_dump_blobs`' empty branch is unreachable from it —
but without the predicate the two stages could disagree: stage 1 would
terminate the walk on a row stage 2 then discards, yielding no seed *and* a
truncated write chain, which is the failure this function exists to avoid.
Channel names are passed as `%s` parameters (safe from SQL injection).
Only the column aliases `ver_i` / `hb_i` / `inline_i` and the subquery alias
`b{i}` are interpolated into the SQL string (i is bounded by len(channels)
and uses safe identifiers).
Only the column aliases `ver_i` are interpolated into the SQL string
(i is bounded by len(channels) and uses safe identifiers).
Caller must extend params with `[ch_0 x4, ch_1 x4, ..., thread_id, ns,
cursor, cursor, page_size]` when `paged=True` — four per channel: the
version lookup, the blob's channel, the version the blob must match, and the
inline lookup.
When `paged=False`, the WHERE has no cursor predicate and there's no
LIMIT/ORDER BY — kept as a non-public helper for tests/diagnostics.
Caller must extend params with `[ch_0, ch_1, ..., thread_id, ns,
cursor, cursor, page_size]`. The `cursor` is the smallest
`checkpoint_id` from the previous page (or `None` on the first page);
`(%s::text IS NULL OR ...)` makes the first-page `WHERE` a no-op.
"""
cols = []
for i in range(len(channels)):
cols.append(
f"checkpoint -> 'channel_versions' ->> %s AS ver_{i}, "
f"EXISTS (SELECT 1 FROM checkpoint_blobs b{i} "
f"WHERE b{i}.thread_id = checkpoints.thread_id "
f"AND b{i}.checkpoint_ns = checkpoints.checkpoint_ns "
f"AND b{i}.channel = %s "
f"AND b{i}.version = checkpoint -> 'channel_versions' ->> %s "
f"AND b{i}.type <> 'empty') AS hb_{i}, "
f"checkpoint -> 'channel_values' -> %s AS inline_{i}"
)
sql = (
cols = [
f"checkpoint -> 'channel_versions' ->> %s AS ver_{i}"
for i in range(len(channels))
]
return (
"SELECT checkpoint_id, parent_checkpoint_id, "
+ ", ".join(cols)
+ " FROM checkpoints WHERE thread_id = %s AND checkpoint_ns = %s"
" AND (%s::text IS NULL OR checkpoint_id < %s)"
" ORDER BY checkpoint_id DESC LIMIT %s"
)
if paged:
sql += (
" AND (%s::text IS NULL OR checkpoint_id < %s)"
" ORDER BY checkpoint_id DESC LIMIT %s"
)
return sql
def _build_delta_stage2_sql(
def _build_delta_fetch_sql(
*,
channels_with_chain: Sequence[str],
channels_with_seed: Sequence[str],
) -> str:
"""Build stage 2 SQL as a per-channel UNION ALL.
"""Build the FETCH SQL as a per-channel UNION ALL.
For each channel with a non-empty chain, emit one branch reading
`checkpoint_writes` for that specific channel + chain_cids. For each
channel with a seed_version, emit one branch reading `checkpoint_blobs`
for that channel + version. This avoids the over-fetch of the prior
for that channel + version. This avoids the over-fetch of a single
`channel = ANY(channels) AND checkpoint_id = ANY(union)` form when
channels have different chain depths.
@@ -316,6 +281,7 @@ def _build_delta_stage2_sql(
"""
branches: list[str] = []
for _ in channels_with_chain:
# NOTE: no ORDER BY on this branch — writes are sorted in assembly.
branches.append(
"SELECT 'w'::text AS _kind, "
"checkpoint_id, channel, "
@@ -335,10 +301,59 @@ def _build_delta_stage2_sql(
return " UNION ALL ".join(branches)
# Stage 1 rows are dynamic-shape dicts: {checkpoint_id, parent_checkpoint_id,
# ver_0, hs_0, ver_1, hs_1, ...}. Walking is parameterized by the channel
# list to map indices back to channel names — no static TypedDict here.
# `dict[str, Any]` is the practical signature.
def _ingest_walk_page(
page_rows: Sequence[Mapping[str, Any]],
channels: Sequence[str],
parent_of: dict[str, str | None],
ver_by_i_by_cid: list[dict[str, str | None]],
) -> str | None:
"""Fold one WALK page into `parent_of` + per-channel `ver_by_cid`.
Returns the oldest checkpoint_id seen on this page (smallest, since
pages come back DESC). Caller uses it as the cursor for the next page
(`AND checkpoint_id < cursor`).
"""
oldest: str | None = None
for r in page_rows:
cid = cast(str, r["checkpoint_id"])
parent_of[cid] = cast("str | None", r["parent_checkpoint_id"])
for i in range(len(channels)):
ver_by_i_by_cid[i][cid] = cast("str | None", r.get(f"ver_{i}"))
# Rows are DESC; the last one is the smallest cid in the page.
oldest = cid
return oldest
def _advance_shared_chain(
target_id: str,
parent_of: Mapping[str, str | None],
shared_cpid_chain: list[str],
max_supersteps: int,
) -> bool:
"""Extend the shared parent chain as far as ingested pages allow.
The chain holds ancestors of the target, newest first: `chain[0]` is
the target's parent, `chain[1]` its grandparent, and so on. A single
chain is shared across all channels and grown to the maximum requested
depth; each channel later slices `chain[:supersteps]`.
Stops when:
- the chain reaches `max_supersteps` hops, OR
- the root is reached (parent is None) — returns True, OR
- the next ancestor's cid isn't in `parent_of` yet (waits for the
next page).
Returns True iff the root was reached.
"""
while len(shared_cpid_chain) < max_supersteps:
top_of_chain = shared_cpid_chain[-1] if shared_cpid_chain else target_id
if top_of_chain not in parent_of:
return False # wait for the next page
parent = parent_of[top_of_chain]
if parent is None:
return True # hit the root
shared_cpid_chain.append(parent)
return False
class BasePostgresSaver(BaseCheckpointSaver[str]):
@@ -383,126 +398,92 @@ class BasePostgresSaver(BaseCheckpointSaver[str]):
if t.decode() != "empty"
}
@staticmethod
def _ingest_stage1_page(
stage1_rows: Sequence[Mapping[str, Any]],
def _resolve_delta_chains(
self,
channels: Sequence[str],
parent_of: dict[str, str | None],
ver_by_i_by_cid: list[dict[str, str | None]],
hb_by_i_by_cid: list[dict[str, bool]],
inline_by_i_by_cid: list[dict[str, Any]],
) -> str | None:
"""Fold one stage-1 page into the running walk-state mappings.
Returns the oldest checkpoint_id seen on this page (smallest, since
pages come back DESC). Caller uses it as the cursor for the next
page (`AND checkpoint_id < cursor`).
"""
oldest: str | None = None
for r in stage1_rows:
cid = cast(str, r["checkpoint_id"])
parent_of[cid] = cast("str | None", r["parent_checkpoint_id"])
for i in range(len(channels)):
ver_by_i_by_cid[i][cid] = cast("str | None", r.get(f"ver_{i}"))
hb_by_i_by_cid[i][cid] = bool(r.get(f"hb_{i}"))
inline_by_i_by_cid[i][cid] = r.get(f"inline_{i}")
# Rows are DESC; the last one is the smallest cid in the page.
oldest = cid
return oldest
@staticmethod
def _try_advance_walks(
target_id: str,
channels: Sequence[str],
parent_of: Mapping[str, str | None],
supersteps_by_ch: Mapping[str, int],
shared_cpid_chain: Sequence[str],
ver_by_i_by_cid: Sequence[Mapping[str, str | None]],
hb_by_i_by_cid: Sequence[Mapping[str, bool]],
inline_by_i_by_cid: Sequence[Mapping[str, Any]],
chain_by_ch: dict[str, list[str]],
seed_ver_by_ch: dict[str, str | None],
seed_inline_by_ch: dict[str, Any],
walk_cursor_by_ch: dict[str, str | None],
seeded: set[str],
) -> None:
"""Advance each not-yet-seeded channel's walk as far as possible.
has_reached_root: bool,
thread_id: str,
) -> tuple[
dict[str, list[str]],
dict[str, str | None],
dict[str, str | None],
]:
"""Slice the shared parent chain into per-channel chain/seed mappings.
Uses the partial `parent_of` map accumulated so far. A walk stops
either because:
(a) it found a stored value for its channel — a blob or an inline
primitive (channel becomes seeded),
(b) it reached a real root (parent_of[cid] is None — fully
materialized at this point), or
(c) the next ancestor cid isn't in `parent_of` yet (waiting for
a later page; the cursor stays put).
For each channel the seed snapshot sits `supersteps` hops back, so
the seed checkpoint is `shared_cpid_chain[supersteps - 1]` and the
chain is `shared_cpid_chain[:supersteps]` (newest first).
Mutates `chain_by_ch`, `seed_ver_by_ch`, `seed_inline_by_ch`,
`walk_cursor_by_ch`, and `seeded` in place.
When the chain is shorter than `supersteps` but the walk reached the
root, the persisted chain is "compressed" relative to the logical
superstep count — either because intermediate supersteps were never
persisted (`durability="exit"`) or because the thread never produced
a snapshot at all. In both cases the seed candidate is the oldest
persisted checkpoint (`shared_cpid_chain[-1]`): FETCH loads its blob,
and assembly keeps it only if non-empty (a real snapshot or migrated
value) — otherwise it omits `seed` and replays the full chain on an
empty baseline.
"""
chained_cpid_by_ch: dict[str, list[str]] = {ch: [] for ch in channels}
seed_cpid_by_ch: dict[str, str | None] = {ch: None for ch in channels}
seed_ver_by_ch: dict[str, str | None] = {ch: None for ch in channels}
for i, ch in enumerate(channels):
if ch in seeded:
bound = supersteps_by_ch.get(ch, 0)
if bound <= 0:
continue
# First-time entry: cursor starts at the target's parent.
if ch not in walk_cursor_by_ch:
walk_cursor_by_ch[ch] = parent_of.get(target_id)
cur_cid = walk_cursor_by_ch[ch]
ch_chain = chain_by_ch[ch]
hb_i = hb_by_i_by_cid[i]
inline_i = inline_by_i_by_cid[i]
ver_i = ver_by_i_by_cid[i]
while cur_cid is not None:
if cur_cid not in parent_of:
# Need more pages to continue this walk.
break
ch_chain.append(cur_cid)
has_blob = hb_i.get(cur_cid, False)
inline = inline_i.get(cur_cid)
if has_blob or inline is not None:
# A blob wins: for a `_DeltaSnapshot` the inline reading is
# the `true` marker, not the value.
seed_ver_by_ch[ch] = ver_i.get(cur_cid)
if not has_blob:
seed_inline_by_ch[ch] = inline
seeded.add(ch)
cur_cid = None
break
cur_cid = parent_of[cur_cid]
walk_cursor_by_ch[ch] = cur_cid
if len(shared_cpid_chain) >= bound:
seed_depth = bound
elif has_reached_root:
seed_depth = len(shared_cpid_chain)
else:
logger.warning(
"cannot find seed snapshot for delta channel "
"(thread_id=%s, channel=%s)",
thread_id,
ch,
)
continue
if seed_depth <= 0:
continue
chained_cpid_by_ch[ch] = list(shared_cpid_chain[:seed_depth])
seed_cpid_by_ch[ch] = shared_cpid_chain[seed_depth - 1]
seed_ver_by_ch[ch] = ver_by_i_by_cid[i].get(seed_cpid_by_ch[ch])
return chained_cpid_by_ch, seed_cpid_by_ch, seed_ver_by_ch
def _build_delta_channels_writes_history(
def _assemble_delta_history(
self,
*,
channels: Sequence[str],
chain_by_ch: Mapping[str, list[str]],
chained_cpid_by_ch: Mapping[str, Sequence[str]],
seed_cpid_by_ch: Mapping[str, str | None],
seed_ver_by_ch: Mapping[str, str | None],
seed_inline_by_ch: Mapping[str, Any],
stage2_rows: Sequence[_DeltaStage2Row],
fetch_rows: Sequence[_DeltaStage2Row],
) -> dict[str, DeltaChannelHistory]:
"""Demux stage 2 rows per channel; produce per-channel histories.
"""Demux FETCH rows per channel and produce per-channel histories.
stage2_rows carry `channel` on every row. We build per-channel
`writes_by_cid` and per-channel `seed_blob` dicts, then assemble
a `DeltaChannelHistory` per requested channel.
`fetch_rows` carry `channel` on every row. Write rows (`_kind = 'w'`)
are bucketed per channel per checkpoint; seed-blob rows
(`_kind = 'b'`) give each channel its snapshot value.
A seed comes from the blobs table when the walk found one there, and
otherwise from `seed_inline_by_ch` — `put` keeps `None`, `str`, `int`,
`float` and `bool` values in the checkpoint's own `channel_values` with
no blob row, so those never appear in `stage2_rows`.
The `seed` key is omitted when the walk reached root without finding a
stored value, or when the seed blob is sentinel "empty" — in both cases
the consumer treats absence as "start empty".
The seed checkpoint's own writes are replayed on top of a
`_DeltaSnapshot` seed (the snapshot is the value *prior* to its own
writes), but skipped for a migrated plain-value seed (a legacy
non-delta blob already incorporates those writes). The `seed` key is
omitted when no seed was located or the blob is the "empty"
tombstone — the consumer treats absence as "start empty".
"""
# writes_by_ch_by_cid[channel][cid] = list of (type, blob, task_id, idx)
writes_by_ch_by_cid: dict[str, dict[str, list[tuple[str, bytes, str, int]]]] = {
ch: {} for ch in channels
}
# seed_blob_by_ver[(channel, version)] = (type, blob)
seed_blob_by_ver: dict[tuple[str, str], tuple[str, bytes]] = {}
seed_blob_by_ch: dict[str, tuple[str, bytes]] = {}
for r in stage2_rows:
for r in fetch_rows:
ch = cast(str, r["channel"])
kind = r["_kind"]
if kind == "w":
if r["_kind"] == "w":
cid = cast(str, r["checkpoint_id"])
writes_by_ch_by_cid.setdefault(ch, {}).setdefault(cid, []).append(
cast(
@@ -510,39 +491,39 @@ class BasePostgresSaver(BaseCheckpointSaver[str]):
(r["type"], r["blob"], r["task_id"], r["idx"]),
)
)
else: # kind == "b"
ver = cast(str, r["version"])
seed_blob_by_ver[(ch, ver)] = cast(
"tuple[str, bytes]", (r["type"], r["blob"])
)
else: # _kind == "b" — the seed blob for this channel.
seed_blob_by_ch[ch] = cast("tuple[str, bytes]", (r["type"], r["blob"]))
# Sort writes per (channel, cid) newest-first by (task_id, idx)
# Within a checkpoint, writes apply oldest→newest by (task_id, idx).
for cid_map in writes_by_ch_by_cid.values():
for ws in cid_map.values():
ws.sort(key=lambda w: (w[2], w[3]), reverse=True)
ws.sort(key=lambda w: (w[2], w[3]))
result: dict[str, DeltaChannelHistory] = {}
for ch in channels:
chain_cids = chain_by_ch.get(ch, [])
seed_version = seed_ver_by_ch.get(ch)
entry: DeltaChannelHistory = {"writes": []}
skip_seed_checkpoint_writes = False
seed_blob = seed_blob_by_ch.get(ch)
if seed_blob is not None and seed_blob[0] != "empty":
seed_value = self.serde.loads_typed(seed_blob)
entry["seed"] = seed_value
# A migrated (non-delta) seed already includes the writes on
# its own checkpoint; a `_DeltaSnapshot` does not.
skip_seed_checkpoint_writes = not isinstance(seed_value, _DeltaSnapshot)
collected: list[PendingWrite] = []
cid_writes = writes_by_ch_by_cid.get(ch, {})
for cid in chain_cids:
for type_tag, write_blob, task_id, _idx in cid_writes.get(cid, []):
val = self.serde.loads_typed((type_tag, write_blob))
collected.append((task_id, ch, val))
collected.reverse()
entry: DeltaChannelHistory = {"writes": collected}
if seed_version is not None:
blob = seed_blob_by_ver.get((ch, seed_version))
if blob is not None and blob[0] != "empty":
entry["seed"] = self.serde.loads_typed(blob)
elif ch in seed_inline_by_ch:
# Inline primitive: stored in the checkpoint, not the blobs
# table, so stage 2 never returned a row for it.
entry["seed"] = seed_inline_by_ch[ch]
if cid_writes:
collected: list[PendingWrite] = []
seed_cpid = seed_cpid_by_ch.get(ch)
# Chain is newest→oldest; replay oldest→newest.
for cid in reversed(chained_cpid_by_ch.get(ch, [])):
if skip_seed_checkpoint_writes and cid == seed_cpid:
continue
for type_tag, write_blob, task_id, _idx in cid_writes.get(cid, []):
val = self.serde.loads_typed((type_tag, write_blob))
collected.append((task_id, ch, val))
entry["writes"] = collected
result[ch] = entry
return result
@@ -334,7 +334,7 @@ class ShallowPostgresSaver(BasePostgresSaver):
>>> checkpoint_tuple = memory.get_tuple(config)
>>> print(checkpoint_tuple)
CheckpointTuple(...)
"""
""" # noqa
thread_id = config["configurable"]["thread_id"]
checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
args = (thread_id, checkpoint_ns)
@@ -885,7 +885,7 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
while True:
try:
yield asyncio.run_coroutine_threadsafe(
anext(aiter_), # type: ignore[arg-type]
anext(aiter_), # type: ignore[arg-type] # noqa: F821
self.loop,
).result()
except StopAsyncIteration:
@@ -4,10 +4,9 @@ import asyncio
import concurrent.futures
import json
import logging
import re
import threading
from collections import defaultdict
from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
from collections.abc import Callable, Iterable, Iterator, Sequence
from contextlib import contextmanager
from datetime import datetime
from typing import (
@@ -239,12 +238,6 @@ class BasePostgresStore(Generic[C]):
conn: C
_deserializer: Callable[[bytes | orjson.Fragment], dict[str, Any]] | None
index_config: PostgresIndexConfig | None
ttl_config: TTLConfig | None
@property
def _omit_expired(self) -> bool:
"""Whether expired-but-unswept rows should be filtered from reads."""
return bool(self.ttl_config and self.ttl_config.get("omit_expired"))
def _get_batch_GET_ops_queries(
self,
@@ -265,17 +258,12 @@ class BasePostgresStore(Generic[C]):
namespace_groups[op.namespace].append((idx, op.key))
refresh_ttls[op.namespace].append(op.refresh_ttl)
omit_expired = self._omit_expired
expiry_clause = (
"AND (s.expires_at IS NULL OR s.expires_at > NOW())" if omit_expired else ""
)
results = []
for namespace, items in namespace_groups.items():
_, keys = zip(*items, strict=False)
this_refresh_ttls = refresh_ttls[namespace]
query = f"""
query = """
WITH passed_in AS (
SELECT unnest(%s::text[]) AS key,
unnest(%s::bool[]) AS do_refresh
@@ -288,14 +276,12 @@ class BasePostgresStore(Generic[C]):
AND s.key = p.key
AND p.do_refresh = TRUE
AND s.ttl_minutes IS NOT NULL
{expiry_clause}
RETURNING s.key
)
SELECT s.key, s.value, s.created_at, s.updated_at
FROM store s
JOIN passed_in p ON s.key = p.key
WHERE s.prefix = %s
{expiry_clause}
"""
ns_text = _namespace_to_text(namespace)
params = (
@@ -354,7 +340,7 @@ class BasePostgresStore(Generic[C]):
(
_namespace_to_text(op.namespace),
op.key,
Jsonb(dict(cast(Mapping[str, Any], op.value))),
Jsonb(cast(dict, op.value)),
)
)
if op.ttl is not None:
@@ -436,13 +422,6 @@ class BasePostgresStore(Generic[C]):
- embedding_requests: list of (original_index_in_search_ops, text_query)
"""
omit_expired = self._omit_expired
search_expiry_clause = (
"AND (store.expires_at IS NULL OR store.expires_at > NOW())"
if omit_expired
else ""
)
queries = []
embedding_requests = []
for idx, (_, op) in enumerate(search_ops):
@@ -464,9 +443,8 @@ class BasePostgresStore(Generic[C]):
ns_condition = "TRUE"
ns_param: Sequence[str] | None = None
if op.namespace_prefix:
ns_condition, ns_param = _namespace_prefix_condition(
op.namespace_prefix
)
ns_condition = "store.prefix LIKE %s"
ns_param = (f"{_namespace_to_text(op.namespace_prefix)}%",)
else:
ns_param = ()
@@ -513,7 +491,7 @@ class BasePostgresStore(Generic[C]):
{score_operator} AS neg_score
FROM store
JOIN store_vectors sv ON store.prefix = sv.prefix AND store.key = sv.key
WHERE {ns_condition} {extra_filters} {search_expiry_clause}
WHERE {ns_condition} {extra_filters}
ORDER BY {score_operator} ASC
LIMIT %s
"""
@@ -549,7 +527,7 @@ class BasePostgresStore(Generic[C]):
base_query = f"""
SELECT store.prefix, store.key, store.value, store.created_at, store.updated_at, NULL AS score
FROM store
WHERE {ns_condition} {extra_filters} {search_expiry_clause}
WHERE {ns_condition} {extra_filters}
ORDER BY store.updated_at DESC
LIMIT %s
OFFSET %s
@@ -613,23 +591,18 @@ class BasePostgresStore(Generic[C]):
"""
params: list[Any] = [op.max_depth, op.max_depth]
omit_expired = self._omit_expired
conditions = []
if omit_expired:
conditions.append("(expires_at IS NULL OR expires_at > NOW())")
if op.match_conditions:
for condition in op.match_conditions:
if condition.match_type in ("prefix", "suffix"):
if not condition.path:
# An empty path constrains nothing; skipping keeps it a
# no-op rather than emitting a pattern that matches no
# namespace at all.
continue
conditions.append("prefix ~ %s")
if condition.match_type == "prefix":
conditions.append("prefix LIKE %s")
params.append(
_namespace_match_pattern(
condition.path, condition.match_type
)
f"{_namespace_to_text(condition.path, handle_wildcards=True)}%"
)
elif condition.match_type == "suffix":
conditions.append("prefix LIKE %s")
params.append(
f"%{_namespace_to_text(condition.path, handle_wildcards=True)}"
)
else:
logger.warning(
@@ -1275,59 +1248,15 @@ def _get_index_params(store: Any) -> tuple[str, dict[str, Any]]:
return kind, sanitized
def _namespace_to_text(namespace: tuple[str, ...]) -> str:
def _namespace_to_text(
namespace: tuple[str, ...], handle_wildcards: bool = False
) -> str:
"""Convert namespace tuple to text string."""
if handle_wildcards:
namespace = tuple("%" if val == "*" else val for val in namespace)
return ".".join(namespace)
def _escape_like_literal(text: str) -> str:
"""Escape LIKE metacharacters so `text` is matched literally.
Namespace labels may contain `_` and `%`, which would otherwise act as
wildcards: `("user_1",)` would match `("userX1",)`. Backslash is escaped
first so it cannot escape the following character. Requires an explicit
`ESCAPE '\\'` clause on the pattern.
"""
return text.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
def _namespace_prefix_condition(namespace_prefix: tuple[str, ...]) -> tuple[str, tuple]:
"""Build the SQL scoping a search to a namespace and its descendants.
Matches the namespace exactly or requires the `.` separator before any
remainder, so a prefix of `("foo",)` does not also match `("foobar",)`.
Both arms stay index-friendly: equality on the `(prefix, key)` primary key,
the anchored LIKE on the `prefix text_pattern_ops` index.
Only the LIKE arm is escaped -- equality does not interpret metacharacters,
so escaping it would stop `("user_1",)` from matching itself.
"""
path = _namespace_to_text(namespace_prefix)
condition = r"(store.prefix = %s OR store.prefix LIKE %s ESCAPE '\')"
return condition, (path, f"{_escape_like_literal(path)}.%")
def _namespace_match_pattern(path: tuple[str, ...], match_type: str) -> str:
"""Build a POSIX regex matching the dot-joined prefix on whole segments.
Needed because `LIKE` cannot express "any character except the separator".
Matches how `InMemoryStore` compares namespaces element-wise.
`*` matches exactly one segment. Prefix matches stay open-ended but must end
on a separator; suffix matches anchor at the end and begin on one.
Examples:
prefix ("uid", "*", "alice") -> ^uid\\.[^.]+\\.alice(\\.|\\Z)
suffix ("alice",) -> (^|\\.)alice\\Z
"""
segments = ("[^.]+" if part == "*" else re.escape(part) for part in path)
body = r"\.".join(segments)
if match_type == "suffix":
return rf"(^|\.){body}\Z"
return rf"^{body}(\.|\Z)"
def _row_to_item(
namespace: tuple[str, ...],
row: Row,
+1 -5
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "langgraph-checkpoint-postgres"
version = "3.1.2"
version = "3.1.0"
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
authors = []
requires-python = ">=3.10"
@@ -32,7 +32,6 @@ test = [
"pytest-mock",
"psycopg[binary]",
"langgraph-checkpoint",
"langgraph-checkpoint-conformance",
"pytest-watcher",
]
lint = [
@@ -50,7 +49,6 @@ default-groups = ['dev']
[tool.uv.sources]
langgraph-checkpoint = { path = "../checkpoint", editable = true }
langgraph-checkpoint-conformance = { path = "../checkpoint-conformance", editable = true }
[tool.hatch.build.targets.wheel]
include = ["langgraph"]
@@ -66,8 +64,6 @@ lint.select = [
"UP", # pyupgrade
"B", # flake8-bugbear
"I", # isort
"PLC0415", # import-outside-top-level
"RUF100", # unused noqa directive
"UP", # pyupgrade
]
lint.ignore = ["E501", "B008"]
+6 -8
View File
@@ -380,15 +380,13 @@ async def test_delta_channel_chain_reconstruction(saver_name: str) -> None:
"langgraph.channels.delta", reason="langgraph core not installed"
)
# Deferred on purpose: langgraph core is not a test dependency of this
# package, so these must stay behind the importorskip above.
from typing import Annotated # noqa: PLC0415
from typing import Annotated
from langchain_core.messages import AIMessage, HumanMessage # noqa: PLC0415
from langgraph.channels.delta import DeltaChannel # noqa: PLC0415
from langgraph.graph import START, StateGraph # noqa: PLC0415
from langgraph.graph.message import _messages_delta_reducer # noqa: PLC0415
from typing_extensions import TypedDict # noqa: PLC0415
from langchain_core.messages import AIMessage, HumanMessage
from langgraph.channels.delta import DeltaChannel
from langgraph.graph import START, StateGraph
from langgraph.graph.message import _messages_delta_reducer
from typing_extensions import TypedDict
class State(TypedDict):
messages: Annotated[list, DeltaChannel(_messages_delta_reducer)]
@@ -739,135 +739,3 @@ async def test_store_ttl(store):
# Now has been (TTL_SECONDS-2)*2 > TTL_SECONDS + TTL_SECONDS/2
results = await store.asearch(ns, query="bar", refresh_ttl=False)
assert len(results) == 0
async def _aexpire_now(
store: AsyncPostgresStore, ns: tuple[str, ...], key: str
) -> None:
"""Backdate a row's expires_at into the past without deleting it (unswept)."""
async with store._cursor() as cur:
await cur.execute(
"UPDATE store SET expires_at = NOW() - INTERVAL '1 minute' "
"WHERE prefix = %s AND key = %s",
(".".join(ns), key),
)
async def _arow_exists(
store: AsyncPostgresStore, ns: tuple[str, ...], key: str
) -> bool:
async with store._cursor() as cur:
await cur.execute(
"SELECT COUNT(*) AS n FROM store WHERE prefix = %s AND key = %s",
(".".join(ns), key),
)
return (await cur.fetchone())["n"] == 1
async def _astored_expires_at(store: AsyncPostgresStore, ns: tuple[str, ...], key: str):
async with store._cursor() as cur:
await cur.execute(
"SELECT expires_at FROM store WHERE prefix = %s AND key = %s",
(".".join(ns), key),
)
return (await cur.fetchone())["expires_at"]
async def test_omit_expired_filters_read_paths(store: AsyncPostgresStore) -> None:
await store.stop_ttl_sweeper() # deterministic: no background deletion
store.ttl_config["omit_expired"] = True
expired_ns = ("omit", "expired")
control_ns = ("omit", "control")
await store.aput(expired_ns, "e", {"data": "gone"}, ttl=TTL_MINUTES)
await store.aput(control_ns, "c", {"data": "keep"}, ttl=None)
await _aexpire_now(store, expired_ns, "e")
# The row is expired but physically still present (unswept).
assert await _arow_exists(store, expired_ns, "e")
# aget omits it; the never-expiring control is still returned.
assert await store.aget(expired_ns, "e") is None
assert await store.aget(control_ns, "c") is not None
# asearch omits it but returns the control.
assert await store.asearch(expired_ns) == []
assert [i.key for i in await store.asearch(control_ns)] == ["c"]
# alist_namespaces drops the expired-only namespace, keeps the control.
namespaces = await store.alist_namespaces(prefix=("omit",))
assert expired_ns not in namespaces
assert control_ns in namespaces
@pytest.mark.parametrize("omit", [None, False], ids=["default", "explicit-false"])
async def test_omit_expired_disabled_preserves_expired_rows(
store: AsyncPostgresStore, omit
) -> None:
await store.stop_ttl_sweeper()
if omit is not None:
store.ttl_config["omit_expired"] = omit
ns = ("keep",)
await store.aput(ns, "k", {"data": "still-here"}, ttl=TTL_MINUTES)
await _aexpire_now(store, ns, "k")
assert await store.aget(ns, "k", refresh_ttl=False) is not None
assert [i.key for i in await store.asearch(ns, refresh_ttl=False)] == ["k"]
assert ns in await store.alist_namespaces(prefix=("keep",))
async def test_omit_expired_refresh_ttl_only_refreshes_live_rows(
store: AsyncPostgresStore,
) -> None:
await store.stop_ttl_sweeper()
store.ttl_config["omit_expired"] = True
ns = ("refresh",)
await store.aput(ns, "expired", {"n": 0}, ttl=TTL_MINUTES)
await store.aput(ns, "live_get", {"n": 1}, ttl=TTL_MINUTES)
await store.aput(ns, "live_search", {"n": 2}, ttl=TTL_MINUTES)
await _aexpire_now(store, ns, "expired")
expired_before = await _astored_expires_at(store, ns, "expired")
get_before = await _astored_expires_at(store, ns, "live_get")
search_before = await _astored_expires_at(store, ns, "live_search")
# refresh_ttl=True must NOT resurrect the expired row (via aget or asearch)...
assert await store.aget(ns, "expired", refresh_ttl=True) is None
live_keys = [i.key for i in await store.asearch(ns, refresh_ttl=True)]
assert "expired" not in live_keys
assert await _astored_expires_at(store, ns, "expired") == expired_before
# ...but must still extend the live rows that were read.
assert await store.aget(ns, "live_get", refresh_ttl=True) is not None
assert await _astored_expires_at(store, ns, "live_get") > get_before
assert await _astored_expires_at(store, ns, "live_search") > search_before
async def test_omit_expired_search_pagination(store: AsyncPostgresStore) -> None:
await store.stop_ttl_sweeper()
store.ttl_config["omit_expired"] = True
ns = ("page",)
for k in ("a", "b", "c"):
await store.aput(ns, k, {"k": k}, ttl=TTL_MINUTES)
await store.aput(ns, "expired", {"k": "x"}, ttl=TTL_MINUTES)
await _aexpire_now(store, ns, "expired")
seconds_ago = {"a": 1, "expired": 2, "b": 3, "c": 4}
# updated_at DESC orders these a, expired, b, c, so the expired row sits inside
# the first limit=2 window. Correct (pre-LIMIT) filtering yields live pages
# [a, b] then [c]; post-LIMIT filtering would underfill page 1 to just [a].
async with store._cursor() as cur:
for key, secs in seconds_ago.items():
await cur.execute(
"UPDATE store SET updated_at = NOW() - (%s * INTERVAL '1 second') "
"WHERE prefix = %s AND key = %s",
(secs, ".".join(ns), key),
)
page1 = await store.asearch(ns, limit=2, offset=0)
page2 = await store.asearch(ns, limit=2, offset=2)
assert [i.key for i in page1] == ["a", "b"]
assert [i.key for i in page2] == ["c"]
@@ -1,30 +0,0 @@
"""Run delta-channel conformance capabilities against AsyncPostgresSaver."""
from __future__ import annotations
import pytest
from langgraph.checkpoint.conformance import validate
from langgraph.checkpoint.conformance.initializer import checkpointer_test
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
from tests.conftest import DEFAULT_URI
@pytest.mark.asyncio
async def test_delta_channel_conformance():
@checkpointer_test(name="AsyncPostgresSaver")
async def postgres_saver():
async with AsyncPostgresSaver.from_conn_string(DEFAULT_URI) as saver:
await saver.setup()
yield saver
report = await validate(
postgres_saver,
capabilities={
"delta_channel_history",
},
)
for cap, result in report.results.items():
if result.passed is False:
details = "\n".join(result.failures or [])
pytest.fail(f"Capability {cap} failed:\n{details}")
@@ -1,204 +0,0 @@
"""Seed detection for `DeltaChannel` histories on Postgres.
`put` splits stored values in two: primitives stay inline in the checkpoint's
`channel_values`, everything else moves to `checkpoint_blobs`. Only
`_DeltaSnapshot` leaves an inline marker behind when it moves, so the stage-1
walk has to check both places — a blob probe alone misses inline primitives, and
an inline-key check alone missed blob-stored plain values, which is what a thread
migrated from a pre-delta channel type leaves behind. See #8534.
"""
from __future__ import annotations
from typing import Any
from uuid import uuid4
import pytest
from langgraph.checkpoint.base import Checkpoint, empty_checkpoint
from langgraph.checkpoint.base.id import uuid6
from langgraph.checkpoint.serde.types import _DeltaSnapshot
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
from tests.conftest import DEFAULT_URI
CHANNEL = "items"
async def _build_chain(saver: AsyncPostgresSaver, seed_value: Any) -> tuple[str, dict]:
"""Store `seed_value` at step 1, then two steps that store nothing.
Every step carries a write so the walk has something to collect.
Returns `(thread_id, head_config)`.
"""
thread_id = str(uuid4())
parent: dict | None = None
for step in range(4):
config: dict = {"configurable": {"thread_id": thread_id, "checkpoint_ns": ""}}
if parent is not None:
config["configurable"]["checkpoint_id"] = parent["configurable"][
"checkpoint_id"
]
cp: Checkpoint = empty_checkpoint()
cp["id"] = str(uuid6(clock_seq=step))
new_versions: dict[str, Any] = {}
if step == 1:
cp["channel_values"][CHANNEL] = seed_value
cp["channel_versions"][CHANNEL] = "v1"
new_versions[CHANNEL] = "v1"
else:
cp["channel_versions"][CHANNEL] = f"v{step}"
parent = await saver.aput(
config, cp, {"source": "loop", "step": step, "parents": {}}, new_versions
)
await saver.aput_writes(parent, [(CHANNEL, f"w{step}")], str(uuid4()))
assert parent is not None
return thread_id, parent
@pytest.mark.asyncio
async def test_plain_value_seed_is_found() -> None:
"""A pre-delta plain value must be located as the seed.
Before #8534 the walk ran to the root and returned no seed, which happens
to reconstruct correctly for additive reducers while costing an
O(thread length) replay on every read.
"""
async with AsyncPostgresSaver.from_conn_string(DEFAULT_URI) as saver:
await saver.setup()
_, head = await _build_chain(saver, [10, 20])
result = await saver.aget_delta_channel_history(config=head, channels=[CHANNEL])
entry = result[CHANNEL]
assert entry.get("seed") == [10, 20], (
f"expected the plain value as seed, got {entry.get('seed', '<missing>')}"
)
# Only the writes between the seed and the head's parent replay: step 1
# (the seed's own) and step 2. Step 0 is older than the seed, step 3 is
# pending at the head.
assert [w[2] for w in entry["writes"]] == ["w1", "w2"]
@pytest.mark.asyncio
async def test_delta_snapshot_seed_is_found() -> None:
"""The `_DeltaSnapshot` path keeps working, so both seed kinds agree."""
async with AsyncPostgresSaver.from_conn_string(DEFAULT_URI) as saver:
await saver.setup()
_, head = await _build_chain(saver, _DeltaSnapshot([10, 20]))
result = await saver.aget_delta_channel_history(config=head, channels=[CHANNEL])
entry = result[CHANNEL]
seed = entry.get("seed")
assert isinstance(seed, _DeltaSnapshot), f"expected a snapshot, got {seed!r}"
assert seed.value == [10, 20]
assert [w[2] for w in entry["writes"]] == ["w1", "w2"]
@pytest.mark.asyncio
async def test_version_bump_without_a_value_does_not_hide_an_older_seed() -> None:
"""A delta-era step bumps `channel_versions` without storing a value, so no
blob exists for that version. The probe must report no seed there and keep
walking rather than stopping at a version it cannot resolve.
Step 0 holds the real value; step 1 bumps the version with nothing stored.
Walking back from the head has to pass step 1 to reach step 0.
"""
async with AsyncPostgresSaver.from_conn_string(DEFAULT_URI) as saver:
await saver.setup()
thread_id = str(uuid4())
parent: dict | None = None
for step in range(4):
config: dict = {
"configurable": {"thread_id": thread_id, "checkpoint_ns": ""}
}
if parent is not None:
config["configurable"]["checkpoint_id"] = parent["configurable"][
"checkpoint_id"
]
cp: Checkpoint = empty_checkpoint()
cp["id"] = str(uuid6(clock_seq=step))
new_versions: dict[str, Any] = {}
if step == 0:
cp["channel_values"][CHANNEL] = [10, 20]
cp["channel_versions"][CHANNEL] = "v0"
new_versions[CHANNEL] = "v0"
elif step == 1:
# Version bumped, value absent -> no blob row written.
cp["channel_versions"][CHANNEL] = "v1"
new_versions[CHANNEL] = "v1"
else:
cp["channel_versions"][CHANNEL] = "v1"
parent = await saver.aput(
config,
cp,
{"source": "loop", "step": step, "parents": {}},
new_versions,
)
await saver.aput_writes(parent, [(CHANNEL, f"w{step}")], str(uuid4()))
assert parent is not None
result = await saver.aget_delta_channel_history(
config=parent, channels=[CHANNEL]
)
entry = result[CHANNEL]
assert entry.get("seed") == [10, 20], (
"the walk stopped at the empty blob instead of reaching the real "
f"value at step 0; got {entry.get('seed', '<missing>')}"
)
assert [w[2] for w in entry["writes"]] == ["w0", "w1", "w2"]
@pytest.mark.asyncio
async def test_inline_primitive_seed_is_found() -> None:
"""`put` keeps `None`, `str`, `int`, `float` and `bool` in the checkpoint's
own `channel_values` with no blob row, so a blob probe alone cannot see
them. Stage 1 reads the inline value too and uses it when there is no blob.
"""
async with AsyncPostgresSaver.from_conn_string(DEFAULT_URI) as saver:
await saver.setup()
for seed_value in (42, "x", 3.5, None):
_, head = await _build_chain(saver, seed_value)
entry = (
await saver.aget_delta_channel_history(config=head, channels=[CHANNEL])
)[CHANNEL]
if seed_value is None:
# A JSON null is indistinguishable from "no value stored", so
# the walk keeps going; replay from empty is the correct result.
assert "seed" not in entry
else:
assert entry.get("seed") == seed_value, (
f"inline {type(seed_value).__name__} seed not found: "
f"{entry.get('seed', '<missing>')!r}"
)
assert [w[2] for w in entry["writes"]] == ["w1", "w2"]
@pytest.mark.asyncio
async def test_inline_true_is_not_read_as_a_snapshot_marker() -> None:
"""`put` inlines a literal `true` in `channel_values` as the marker for a
`_DeltaSnapshot`, which is also what a genuine `bool` channel holding
`True` looks like. A blob exists only in the snapshot case, so preferring
the blob keeps the two apart.
"""
async with AsyncPostgresSaver.from_conn_string(DEFAULT_URI) as saver:
await saver.setup()
_, head = await _build_chain(saver, True)
entry = (
await saver.aget_delta_channel_history(config=head, channels=[CHANNEL])
)[CHANNEL]
assert entry.get("seed") is True, (
f"a real inline True must survive, got {entry.get('seed', '<missing>')!r}"
)
_, snap_head = await _build_chain(saver, _DeltaSnapshot(True))
snap_entry = (
await saver.aget_delta_channel_history(config=snap_head, channels=[CHANNEL])
)[CHANNEL]
seed = snap_entry.get("seed")
assert isinstance(seed, _DeltaSnapshot), (
f"the marker must resolve to the blob, not inline true; got {seed!r}"
)
assert seed.value is True
@@ -20,10 +20,6 @@ from langgraph.store.base import (
from psycopg import Connection
from langgraph.store.postgres import PostgresStore
from langgraph.store.postgres.base import (
_escape_like_literal,
_namespace_match_pattern,
)
from tests.conftest import (
DEFAULT_URI,
VECTOR_TYPES,
@@ -330,127 +326,6 @@ def test_list_namespaces(store) -> None:
store.delete(namespace, "dummy")
def test_escape_like_literal() -> None:
assert _escape_like_literal("users.alice") == "users.alice"
assert _escape_like_literal("user_1") == r"user\_1"
assert _escape_like_literal("100%") == r"100\%"
assert _escape_like_literal("a\\b") == "a\\\\b"
assert _escape_like_literal("") == ""
def test_namespace_match_pattern() -> None:
assert _namespace_match_pattern(("foo",), "prefix") == r"^foo(\.|\Z)"
assert _namespace_match_pattern(("uid", "users"), "prefix") == r"^uid\.users(\.|\Z)"
assert (
_namespace_match_pattern(("uid", "*", "alice"), "prefix")
== r"^uid\.[^.]+\.alice(\.|\Z)"
)
assert _namespace_match_pattern(("alice",), "suffix") == r"(^|\.)alice\Z"
# Regex metacharacters in a label are quoted, not interpreted.
pattern = _namespace_match_pattern(("a.b+c",), "prefix")
assert re.match(pattern, "a.b+c.child")
assert not re.match(pattern, "axbbbc")
def test_search_namespace_segment_boundary(store) -> None:
"""Prefix scoping must stop at namespace segment boundaries.
Namespaces are stored dot-joined, so matching the raw text also returns
siblings sharing leading characters. Callers isolate tenants by namespace,
so prefix-shaped ids (1 vs 12) would cross-read.
"""
for namespace in [
("foo",),
("foo", "child"),
("foo", "child", "deep"),
("foobar",),
("foobar", "baz"),
("foo2",),
]:
store.put(namespace, "k", {"v": 1})
def _namespaces(prefix: tuple[str, ...]) -> set[tuple[str, ...]]:
return {item.namespace for item in store.search(prefix, limit=100)}
assert _namespaces(("foo",)) == {
("foo",),
("foo", "child"),
("foo", "child", "deep"),
}
# The sibling scope is independent, not merely narrower.
assert _namespaces(("foobar",)) == {("foobar",), ("foobar", "baz")}
assert _namespaces(("foo", "child")) == {("foo", "child"), ("foo", "child", "deep")}
assert _namespaces(("foo2",)) == {("foo2",)}
assert _namespaces(("fo",)) == set()
def test_search_empty_prefix_is_unconstrained(store) -> None:
"""An empty prefix constrains nothing and must return every namespace."""
for namespace in [("a",), ("b", "c"), ("d", "e", "f")]:
store.put(namespace, "k", {"v": 1})
assert {item.namespace for item in store.search((), limit=100)} == {
("a",),
("b", "c"),
("d", "e", "f"),
}
def test_search_namespace_like_metacharacters(store) -> None:
"""`_` and `%` are legal namespace labels, not LIKE wildcards."""
for namespace in [
("user_1",),
("user_1", "child"),
("userX1",),
("a%b",),
("axxb",),
]:
store.put(namespace, "k", {"v": 1})
def _namespaces(prefix: tuple[str, ...]) -> set[tuple[str, ...]]:
return {item.namespace for item in store.search(prefix, limit=100)}
# Also asserts the namespace still matches itself, which catches escaping
# the equality arm by mistake.
assert _namespaces(("user_1",)) == {("user_1",), ("user_1", "child")}
assert _namespaces(("a%b",)) == {("a%b",)}
def test_list_namespaces_segment_boundary(store) -> None:
for namespace in [
("foo",),
("foo", "child"),
("foobar",),
("foobar", "baz"),
("uid", "users", "alice"),
("uid", "users", "malice"),
("uid", "a", "b", "alice"),
]:
store.put(namespace, "k", {"v": 1})
assert set(store.list_namespaces(prefix=["foo"], limit=100)) == {
("foo",),
("foo", "child"),
}
# Suffix must align to a segment: "malice" does not end with the "alice"
# segment.
assert set(store.list_namespaces(suffix=["alice"], limit=100)) == {
("uid", "users", "alice"),
("uid", "a", "b", "alice"),
}
# "*" spans exactly one segment.
assert set(store.list_namespaces(prefix=["uid", "*", "alice"], limit=100)) == {
("uid", "users", "alice"),
}
# Prefix matching stays open-ended across depth.
assert set(store.list_namespaces(prefix=["uid"], limit=100)) == {
("uid", "users", "alice"),
("uid", "users", "malice"),
("uid", "a", "b", "alice"),
}
def test_search(store) -> None:
# Create test data
test_data = [
@@ -988,133 +863,6 @@ def test_store_ttl(store):
assert len(res) == 0
def _expire_now(store: PostgresStore, ns: tuple[str, ...], key: str) -> None:
"""Backdate a row's expires_at into the past without deleting it (unswept)."""
with store._cursor() as cur:
cur.execute(
"UPDATE store SET expires_at = NOW() - INTERVAL '1 minute' "
"WHERE prefix = %s AND key = %s",
(".".join(ns), key),
)
def _row_exists(store: PostgresStore, ns: tuple[str, ...], key: str) -> bool:
with store._cursor() as cur:
cur.execute(
"SELECT COUNT(*) AS n FROM store WHERE prefix = %s AND key = %s",
(".".join(ns), key),
)
return cur.fetchone()["n"] == 1
def _stored_expires_at(store: PostgresStore, ns: tuple[str, ...], key: str):
with store._cursor() as cur:
cur.execute(
"SELECT expires_at FROM store WHERE prefix = %s AND key = %s",
(".".join(ns), key),
)
return cur.fetchone()["expires_at"]
def test_omit_expired_filters_read_paths(store: PostgresStore) -> None:
store.stop_ttl_sweeper() # deterministic: no background deletion
store.ttl_config["omit_expired"] = True
expired_ns = ("omit", "expired")
control_ns = ("omit", "control")
store.put(expired_ns, "e", {"data": "gone"}, ttl=TTL_MINUTES)
store.put(control_ns, "c", {"data": "keep"}, ttl=None)
_expire_now(store, expired_ns, "e")
# The row is expired but physically still present (unswept).
assert _row_exists(store, expired_ns, "e")
# get omits it; the never-expiring control is still returned.
assert store.get(expired_ns, "e") is None
assert store.get(control_ns, "c") is not None
# search omits it but returns the control.
assert store.search(expired_ns) == []
assert [i.key for i in store.search(control_ns)] == ["c"]
# list_namespaces drops the expired-only namespace, keeps the control.
namespaces = store.list_namespaces(prefix=("omit",))
assert expired_ns not in namespaces
assert control_ns in namespaces
@pytest.mark.parametrize("omit", [None, False], ids=["default", "explicit-false"])
def test_omit_expired_disabled_preserves_expired_rows(
store: PostgresStore, omit
) -> None:
store.stop_ttl_sweeper()
if omit is not None:
store.ttl_config["omit_expired"] = omit
ns = ("keep",)
store.put(ns, "k", {"data": "still-here"}, ttl=TTL_MINUTES)
_expire_now(store, ns, "k")
assert store.get(ns, "k", refresh_ttl=False) is not None
assert [i.key for i in store.search(ns, refresh_ttl=False)] == ["k"]
assert ns in store.list_namespaces(prefix=("keep",))
def test_omit_expired_refresh_ttl_only_refreshes_live_rows(
store: PostgresStore,
) -> None:
store.stop_ttl_sweeper()
store.ttl_config["omit_expired"] = True
ns = ("refresh",)
store.put(ns, "expired", {"n": 0}, ttl=TTL_MINUTES)
store.put(ns, "live_get", {"n": 1}, ttl=TTL_MINUTES)
store.put(ns, "live_search", {"n": 2}, ttl=TTL_MINUTES)
_expire_now(store, ns, "expired")
expired_before = _stored_expires_at(store, ns, "expired")
get_before = _stored_expires_at(store, ns, "live_get")
search_before = _stored_expires_at(store, ns, "live_search")
# refresh_ttl=True must NOT resurrect the expired row (via get or search)...
assert store.get(ns, "expired", refresh_ttl=True) is None
assert "expired" not in [i.key for i in store.search(ns, refresh_ttl=True)]
assert _stored_expires_at(store, ns, "expired") == expired_before
# ...but must still extend the live rows that were read.
assert store.get(ns, "live_get", refresh_ttl=True) is not None
assert _stored_expires_at(store, ns, "live_get") > get_before
assert _stored_expires_at(store, ns, "live_search") > search_before
def test_omit_expired_search_pagination(store: PostgresStore) -> None:
store.stop_ttl_sweeper()
store.ttl_config["omit_expired"] = True
ns = ("page",)
for k in ("a", "b", "c"):
store.put(ns, k, {"k": k}, ttl=TTL_MINUTES)
store.put(ns, "expired", {"k": "x"}, ttl=TTL_MINUTES)
_expire_now(store, ns, "expired")
seconds_ago = {"a": 1, "expired": 2, "b": 3, "c": 4}
# updated_at DESC orders these a, expired, b, c, so the expired row sits inside
# the first limit=2 window. Correct (pre-LIMIT) filtering yields live pages
# [a, b] then [c]; post-LIMIT filtering would underfill page 1 to just [a].
with store._cursor() as cur:
for key, secs in seconds_ago.items():
cur.execute(
"UPDATE store SET updated_at = NOW() - (%s * INTERVAL '1 second') "
"WHERE prefix = %s AND key = %s",
(secs, ".".join(ns), key),
)
page1 = store.search(ns, limit=2, offset=0)
page2 = store.search(ns, limit=2, offset=2)
assert [i.key for i in page1] == ["a", "b"]
assert [i.key for i in page2] == ["c"]
@pytest.mark.parametrize(
"vector_type,distance_type",
[
@@ -1151,16 +899,3 @@ def test_non_ascii(
assert result3[0].key == "3"
assert result4[0].key == "4"
assert result5[0].key == "5"
def test_namespace_labels_with_trailing_newline(store) -> None:
"""Labels may contain newlines, and must not match a differently-named label."""
store.put(("users", "alice"), "k", {"v": 1})
store.put(("users", "alice\n"), "k", {"v": 2})
assert set(store.list_namespaces(suffix=["alice"], limit=100)) == {
("users", "alice"),
}
assert set(store.list_namespaces(prefix=["users", "alice"], limit=100)) == {
("users", "alice"),
}
+56 -87
View File
@@ -17,16 +17,16 @@ wheels = [
[[package]]
name = "anyio"
version = "4.14.2"
version = "4.13.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
{ name = "idna" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" }
sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" },
{ url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
]
[[package]]
@@ -138,11 +138,11 @@ wheels = [
[[package]]
name = "codespell"
version = "2.4.3"
version = "2.4.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/80/19/45e941380f69c042b43423513d201e6592346f992394347f5e7174c31407/codespell-2.4.3.tar.gz", hash = "sha256:cbe085e331227b37bb86ef8bddd08dc768c704ee9a07ca869852c093fa2793e2", size = 352773, upload-time = "2026-07-15T11:51:54.159Z" }
sdist = { url = "https://files.pythonhosted.org/packages/2d/9d/1d0903dff693160f893ca6abcabad545088e7a2ee0a6deae7c24e958be69/codespell-2.4.2.tar.gz", hash = "sha256:3c33be9ae34543807f088aeb4832dfad8cb2dae38da61cac0a7045dd376cfdf3", size = 352058, upload-time = "2026-03-05T18:10:42.936Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8b/bf/bdb951d34eb169140b546f44be9ec4525d1acefb9eb5572071f5492b19fc/codespell-2.4.3-py3-none-any.whl", hash = "sha256:af2505b335e8573dbd2d384d1c4ef498f4006f4ba2d6fceca01e55b91f52628a", size = 340736, upload-time = "2026-07-15T11:51:52.925Z" },
{ url = "https://files.pythonhosted.org/packages/42/a1/52fa05533e95fe45bcc09bcf8a503874b1c08f221a4e35608017e0938f55/codespell-2.4.2-py3-none-any.whl", hash = "sha256:97e0c1060cf46bd1d5db89a936c98db8c2b804e1fdd4b5c645e82a1ec6b1f886", size = 353715, upload-time = "2026-03-05T18:10:41.398Z" },
]
[[package]]
@@ -159,7 +159,7 @@ name = "exceptiongroup"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
wheels = [
@@ -244,7 +244,7 @@ wheels = [
[[package]]
name = "langchain-core"
version = "1.5.2"
version = "1.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jsonpatch" },
@@ -257,26 +257,26 @@ dependencies = [
{ name = "typing-extensions" },
{ name = "uuid-utils" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6e/58/3ad53096eee1e07728e8219ded30ae308b0f2b8b7b26b8ebb6371917c0f5/langchain_core-1.5.2.tar.gz", hash = "sha256:2d13ab35b42eec63d4669a483776b8cdd778ee764107149369fb369d84c08c41", size = 972322, upload-time = "2026-07-28T16:38:37.977Z" }
sdist = { url = "https://files.pythonhosted.org/packages/59/de/679a53472c25860837e32c0442c962fa86e95317a36460e2c9d5c91b17c2/langchain_core-1.4.0.tar.gz", hash = "sha256:1dc341eed802ed9c117c0df3923c991e5e9e226571e5725c194eeb5bd93d1a7f", size = 920260, upload-time = "2026-05-11T18:42:35.919Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ca/e4/024e402a65f5ee2eb6ae9667b5ffc5f08776cb4e712f5a11ee1997d56083/langchain_core-1.5.2-py3-none-any.whl", hash = "sha256:a687dd7c3b22c6c1294e1c1eeb61fb6f3a308e6015a1d75b576b3836ad5b5aed", size = 561643, upload-time = "2026-07-28T16:38:36.38Z" },
{ url = "https://files.pythonhosted.org/packages/0f/1a/86c38c27b81913a1c6c12448cab55defb5a1097c7dc9a4cea83f55477a2d/langchain_core-1.4.0-py3-none-any.whl", hash = "sha256:23cbbdb46e38ddd1dd5247e6167e96013eae74bea4c5949c550809970a9e565c", size = 548120, upload-time = "2026-05-11T18:42:33.992Z" },
]
[[package]]
name = "langchain-protocol"
version = "0.0.18"
version = "0.0.15"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d2/59/b5959aea96faa9146e2e49a7a22882b3528c62efafe9a6a95beab30c2305/langchain_protocol-0.0.18.tar.gz", hash = "sha256:ec3e11782f1ed0c9db38e5a9ed01b0e7a0d3fba406faa8aef6594b73c56a63e6", size = 6150, upload-time = "2026-06-18T17:08:26.959Z" }
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" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/99/2e/d82db9eec13ad0f72e7aaad5c4bc730ab111934fdc83c85523206eb9b0a0/langchain_protocol-0.0.18-py3-none-any.whl", hash = "sha256:70b53a86fbf9cedc863555effe44da192ab02d556ddbf2cf95b8873adcf41b5a", size = 7221, upload-time = "2026-06-18T17:08:25.996Z" },
{ 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" },
]
[[package]]
name = "langgraph-checkpoint"
version = "4.2.0"
version = "4.1.1"
source = { editable = "../checkpoint" }
dependencies = [
{ name = "langchain-core" },
@@ -322,36 +322,9 @@ test = [
{ name = "redis" },
]
[[package]]
name = "langgraph-checkpoint-conformance"
version = "0.0.2"
source = { editable = "../checkpoint-conformance" }
dependencies = [
{ name = "langgraph-checkpoint" },
]
[package.metadata]
requires-dist = [{ name = "langgraph-checkpoint", editable = "../checkpoint" }]
[package.metadata.requires-dev]
dev = [
{ name = "pytest" },
{ name = "pytest-asyncio" },
{ name = "ruff" },
{ name = "ty" },
]
lint = [
{ name = "ruff" },
{ name = "ty" },
]
test = [
{ name = "pytest" },
{ name = "pytest-asyncio" },
]
[[package]]
name = "langgraph-checkpoint-postgres"
version = "3.1.2"
version = "3.1.0"
source = { editable = "." }
dependencies = [
{ name = "langgraph-checkpoint" },
@@ -365,7 +338,6 @@ dev = [
{ name = "anyio" },
{ name = "codespell" },
{ name = "langgraph-checkpoint" },
{ name = "langgraph-checkpoint-conformance" },
{ name = "psycopg", extra = ["binary"] },
{ name = "pytest" },
{ name = "pytest-asyncio" },
@@ -382,7 +354,6 @@ lint = [
test = [
{ name = "anyio" },
{ name = "langgraph-checkpoint" },
{ name = "langgraph-checkpoint-conformance" },
{ name = "psycopg", extra = ["binary"] },
{ name = "pytest" },
{ name = "pytest-asyncio" },
@@ -403,7 +374,6 @@ dev = [
{ name = "anyio" },
{ name = "codespell" },
{ name = "langgraph-checkpoint", editable = "../checkpoint" },
{ name = "langgraph-checkpoint-conformance", editable = "../checkpoint-conformance" },
{ name = "psycopg", extras = ["binary"] },
{ name = "pytest" },
{ name = "pytest-asyncio" },
@@ -420,7 +390,6 @@ lint = [
test = [
{ name = "anyio" },
{ name = "langgraph-checkpoint", editable = "../checkpoint" },
{ name = "langgraph-checkpoint-conformance", editable = "../checkpoint-conformance" },
{ name = "psycopg", extras = ["binary"] },
{ name = "pytest" },
{ name = "pytest-asyncio" },
@@ -840,7 +809,7 @@ wheels = [
[[package]]
name = "pytest"
version = "9.1.1"
version = "9.0.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
@@ -851,9 +820,9 @@ dependencies = [
{ name = "pygments" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
]
[[package]]
@@ -988,27 +957,27 @@ wheels = [
[[package]]
name = "ruff"
version = "0.16.0"
version = "0.15.15"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4d/94/1e5e4967626faf12fa56999cd6222dff6992ceb086ad7945756baf70c7a7/ruff-0.16.0.tar.gz", hash = "sha256:e460aafd5495ec89efaa6ced2e4a9a581116451e1c88b9d37ef497e0f8e93982", size = 4790557, upload-time = "2026-07-23T19:11:30.981Z" }
sdist = { url = "https://files.pythonhosted.org/packages/84/6f/a76f7d96e5c962f5b69cee865e49c15c1116897c01990faa8a57edb62e7f/ruff-0.15.15.tar.gz", hash = "sha256:b8dff018130b46d8e5bf0f926ef6b60cf871d6d5ae45fc9334e09632daa741d6", size = 4706985, upload-time = "2026-05-28T14:16:57.784Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4b/81/1c8818fee7ce1a04cd7d1b3172e0a8f8e4f1dc4feb7fc390e16daa8af323/ruff-0.16.0-py3-none-linux_armv6l.whl", hash = "sha256:e5115729eb08c585e5121978ba5d5b60caeae394ce21b9fb5e6cd33a1c6c9b1e", size = 10754633, upload-time = "2026-07-23T19:10:46.415Z" },
{ url = "https://files.pythonhosted.org/packages/23/df/beaf59c09d68db84304d555f188b276a77132a5d5b0b67a5c762aa143628/ruff-0.16.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3c954b1d580bfa035b41654f7858cc7e71d5fc3ac5b723dd62bd9133830ed522", size = 10969164, upload-time = "2026-07-23T19:10:50.271Z" },
{ url = "https://files.pythonhosted.org/packages/42/ce/741cd197496a1abbf51352710fd15ed995d2a2be87189c1da26a450d6e83/ruff-0.16.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e01c21d10eb1b29f47b7454e1f4056db9a3f0260c646aa88457c610291db9f81", size = 10488846, upload-time = "2026-07-23T19:10:52.639Z" },
{ url = "https://files.pythonhosted.org/packages/52/2a/a2db8e88cade358f5cdcb05674a917751074109315d014eb6352d9a893f7/ruff-0.16.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e364e5ed22ed8dc05082fd78e35308618260907ac2d3c1d637b2e682415b6c9", size = 10889729, upload-time = "2026-07-23T19:10:54.89Z" },
{ url = "https://files.pythonhosted.org/packages/42/65/62a771694ebd63029dc953e27dbad40e1588bd4860ff9fe881018fddaa49/ruff-0.16.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d327b8fc113a1d4421a04f3839d3752057c8dd1ee320223a6f3f52d04ada462a", size = 10568275, upload-time = "2026-07-23T19:10:56.993Z" },
{ url = "https://files.pythonhosted.org/packages/3f/e2/ced249fe8af5f086c5c58cc21cc3356d50f32f7401c5df87050c999620a7/ruff-0.16.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b50c55e263103586b3dcf5f73d479eb8cb5fdb6098fec59a62891dab653717", size = 11385112, upload-time = "2026-07-23T19:10:59.615Z" },
{ url = "https://files.pythonhosted.org/packages/87/0b/05154977a8fd69eeb6c103271f55403bfd8711f5c0f8ed07489d95a504e7/ruff-0.16.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ff4a79ce3ec0172f3241943835de1c4cb4e2dcd07f0f8c2d02603dbbbee4b17", size = 12207008, upload-time = "2026-07-23T19:11:02.154Z" },
{ url = "https://files.pythonhosted.org/packages/fb/29/98225831a3a1eab0e02f4acc6ca6559a98611dcc68b6965ff4b7234627c1/ruff-0.16.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e95c448fca1fb2a18372a9440926c5a6ee789639bb975c72e7ae6d0b04218ab4", size = 11650842, upload-time = "2026-07-23T19:11:04.557Z" },
{ url = "https://files.pythonhosted.org/packages/91/66/6bd3cf90500653d55dc0ffc8507aa8300bd49d0214b2e8cb4d3fef2943ba/ruff-0.16.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f11a8d11010301d0a398a2fdef67691feca7294da6aef55e2150e8fa2cd520b", size = 11400718, upload-time = "2026-07-23T19:11:09.233Z" },
{ url = "https://files.pythonhosted.org/packages/8e/a2/a54eb4eae05d66364050a5d3b8a9c5ef88196531b3cbe7109d873f87f819/ruff-0.16.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:48044c678e9cb8698246c99b14aaccfa6601dea7379eb48a6f8f73f7a6d86cd0", size = 11426177, upload-time = "2026-07-23T19:11:11.994Z" },
{ url = "https://files.pythonhosted.org/packages/1a/be/16e3eea4b2a478a496919f5e36f17c4559e54620bd3bbac5d6affa068006/ruff-0.16.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7aa0959bad8eb8bef50340154fc9b58678dae31fa4293afa38b44b6e552c0213", size = 10856126, upload-time = "2026-07-23T19:11:14.221Z" },
{ url = "https://files.pythonhosted.org/packages/a2/84/252eb8b868a16eec7257c14f504f77537e734b2d69c762e639e588e304a3/ruff-0.16.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:28ea2b7df8ebf7f9da6b7d47b230ab48f387c0a29be3b474c4d0740e197bb9af", size = 10571208, upload-time = "2026-07-23T19:11:16.378Z" },
{ url = "https://files.pythonhosted.org/packages/21/09/817a482f542f7570cbb4554b26e896610c7114f539b1d9e2d2145bf6bef6/ruff-0.16.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:33a3dfac8c35f81498dea9181bccc2f4c4bc8f1521a1dd9406e77643e0f0fb09", size = 11063329, upload-time = "2026-07-23T19:11:19.173Z" },
{ url = "https://files.pythonhosted.org/packages/2e/23/9403c180ca1cb9b1f7335f5c3e5305c09d49ea5b345196682a36028bde4a/ruff-0.16.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a5237a0bda500d30d81b8e07a6973a5cbc772864cbf746ae2f4e8a2e01c9f4ed", size = 11489751, upload-time = "2026-07-23T19:11:21.74Z" },
{ url = "https://files.pythonhosted.org/packages/b2/1d/1b2ef7bcde851c78d7f17f1cca13fd6dc695fc4b3d6197941e72cae5b132/ruff-0.16.0-py3-none-win32.whl", hash = "sha256:7fab76fa065c873f41ff744347c6e77bcc3dfec4bcc754dc26b63d23c0f7f5fb", size = 10785885, upload-time = "2026-07-23T19:11:23.947Z" },
{ url = "https://files.pythonhosted.org/packages/b2/a3/d5e4ef7a56be3f928ffb90b94c25ba7d3cb9c7fe0736aeaaedf361770712/ruff-0.16.0-py3-none-win_amd64.whl", hash = "sha256:429c117f022bf481fabd9d551e7a3952b24c65e6ef44337ea09d90bebef14472", size = 11923141, upload-time = "2026-07-23T19:11:26.409Z" },
{ url = "https://files.pythonhosted.org/packages/cb/9a/8415f2657cbe200f41a4531ccededf135505a92d4a012229121f885b26f9/ruff-0.16.0-py3-none-win_arm64.whl", hash = "sha256:14296fedcd2705c77ab8235439278bbb38f285cf7da5528b00b3e330c3d4872d", size = 11273407, upload-time = "2026-07-23T19:11:28.705Z" },
{ url = "https://files.pythonhosted.org/packages/fa/9d/3a45c05b8ab04b4705989de70a79008e27c8003296a0feaee9edc18dd7e9/ruff-0.15.15-py3-none-linux_armv6l.whl", hash = "sha256:cf93e5388f412e1b108b1f8b34a6e036b70fe8aff89393befad96fe48670311b", size = 10710652, upload-time = "2026-05-28T14:16:06.701Z" },
{ url = "https://files.pythonhosted.org/packages/05/66/da974431624bf3b49f6ee1f9543c02d929ff1cba78b0d5a79c38cf21f744/ruff-0.15.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac5a646d1f6a7dadd5d50842dae2c1f9862ac887ef5d1b1375e02def791fde6e", size = 11096615, upload-time = "2026-05-28T14:16:23.313Z" },
{ url = "https://files.pythonhosted.org/packages/8c/09/7443452e5d290230a712103f2fdceeef7184f3ec99a2bd01c8be78aaceb5/ruff-0.15.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:77d955a431430c66f72dd94e379ad38a16daea3d25094872ac4edf9e797be530", size = 10436683, upload-time = "2026-05-28T14:16:40.974Z" },
{ url = "https://files.pythonhosted.org/packages/53/01/d330c26a57fa4f3943a14424904027428315b700fe4d14a84bb123a649e5/ruff-0.15.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7614ee79c69788cf6cedd568069ade9cecc22a1ad20494efe8d0c9ebb4b622d4", size = 10769064, upload-time = "2026-05-28T14:16:28.905Z" },
{ url = "https://files.pythonhosted.org/packages/1d/85/cc8770f8bdff541b1da8392d1634141fe4a0e3f4ee596605959b7906c27f/ruff-0.15.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3cdb1679e06a1f6b47bc384714ae96f6e2fb65ca441eb78c43d2ca554176ce1f", size = 10511987, upload-time = "2026-05-28T14:16:43.732Z" },
{ url = "https://files.pythonhosted.org/packages/7c/29/8c190c1472b63013583ba391f3342036e02010544c1270455ed8e519bdf3/ruff-0.15.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2728b93d7b23a603ea2c0ac6eb73d760bd38ec9de35f35fb41e18f7a3fee7622", size = 11275100, upload-time = "2026-05-28T14:16:55.244Z" },
{ url = "https://files.pythonhosted.org/packages/9f/6b/7e145ce2cc8e63d6834eca03d83a0e18d121def5c69f91b4cf4011ed4879/ruff-0.15.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be582fcc0db438902c7792b08d6ddf6c9b9e21addaa10092c2c741cfb09e5a45", size = 12176903, upload-time = "2026-05-28T14:16:14.368Z" },
{ url = "https://files.pythonhosted.org/packages/80/a3/d5974637f68e451f7fadf015cf3101d1cd7d8ba5027cffe0b9e3826ebe6b/ruff-0.15.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7aa77465b8ecaf1a27bea098d696f7fed5e1eccbd10b321b682d6de586ae5627", size = 11404550, upload-time = "2026-05-28T14:16:20.138Z" },
{ url = "https://files.pythonhosted.org/packages/fe/1c/e6e5e568f22be4fb05d6244234aba384c06b451252453b821e1a529263cf/ruff-0.15.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48decfa11d740de4889de623be1463308346312f2409a56e24aa280c86162dc4", size = 11382027, upload-time = "2026-05-28T14:16:46.615Z" },
{ url = "https://files.pythonhosted.org/packages/1d/01/170921b49fcd2e8858825593f91cf7146c3e40a5c3e6df763e4bb0484dde/ruff-0.15.15-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a5015088452ca0081387063649ec67f06d3d1d6b8b936a1f836b5e9657ecd48c", size = 11366041, upload-time = "2026-05-28T14:16:26.247Z" },
{ url = "https://files.pythonhosted.org/packages/87/54/a7bad711d7de93254e15e06a4c375b89a03d18de45d3e5dcc86a4472fb1a/ruff-0.15.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5294aab6356c81600fcdea3a62bb1b924dfd5e91767c12318d3f68f86af57cd", size = 10741795, upload-time = "2026-05-28T14:16:17.11Z" },
{ url = "https://files.pythonhosted.org/packages/c9/31/38c075963668f8b41c6914ee0f6f318727fbe30ab9145cb29e6df464c5fa/ruff-0.15.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:db5bd4d802415cca656dc1616070b725952d6ae95eb5d4831e49fbd94a38f75f", size = 10511117, upload-time = "2026-05-28T14:16:31.767Z" },
{ url = "https://files.pythonhosted.org/packages/9d/96/6ff689e1f7e375d1d97075eca022f74c2bab59554a432fe4d2e6f091986a/ruff-0.15.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:587a6278ed42059191c1a466e490bd7930fb50bd2e255398bc29616c895a61cb", size = 10994867, upload-time = "2026-05-28T14:16:35.149Z" },
{ url = "https://files.pythonhosted.org/packages/c3/c2/5dce0ab9f92a8d534fa62b9bf9caca3eddb8c1a81b616f5e195ada4f0d6e/ruff-0.15.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:df0c1c084f5f4be9812f61518a45c440d3c30d69ce4bf6c5270e66d38338f02a", size = 11482101, upload-time = "2026-05-28T14:16:49.598Z" },
{ url = "https://files.pythonhosted.org/packages/b1/c0/1003b60edd697c649faf61f1a34094b1abb38fb3d1181e3f895781250a08/ruff-0.15.15-py3-none-win32.whl", hash = "sha256:29428ea79694afbe756d45fd59b36f22b6b020dc0443cf7de0173046236964b9", size = 10716774, upload-time = "2026-05-28T14:16:52.337Z" },
{ url = "https://files.pythonhosted.org/packages/02/a8/1269eddd6945a06c23f055ef7848886e37cf9d6a8bebb386a3115f01470c/ruff-0.15.15-py3-none-win_amd64.whl", hash = "sha256:8df0323902e15e24bc4bf246da830573d3cf3352bd0b9a164eab335d111ff4a4", size = 11868463, upload-time = "2026-05-28T14:16:11.333Z" },
{ url = "https://files.pythonhosted.org/packages/4e/b2/920464c907b191e37469d477a1aa8bc048b8f36c4c1610dfa4ab87b39e18/ruff-0.15.15-py3-none-win_arm64.whl", hash = "sha256:3c8ceca6792f38196b8f589bc92eccd03eef286602da92e5dc05cc42ef6441b7", size = 11138498, upload-time = "2026-05-28T14:16:38.425Z" },
]
[[package]]
@@ -1076,27 +1045,27 @@ wheels = [
[[package]]
name = "ty"
version = "0.0.64"
version = "0.0.43"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8e/aa/14c9965d3b173105692473897cc89c34cd91241368b2044e43167e1c17ff/ty-0.0.64.tar.gz", hash = "sha256:d12ddbb05f15158bb518af619378b385486450def95fb06f8ab98037febe9f2c", size = 6350966, upload-time = "2026-07-27T18:32:45.403Z" }
sdist = { url = "https://files.pythonhosted.org/packages/0d/37/4ec04de0659b93be37d956dfceca13b1ecab9c959f28d8a1d5e514603f36/ty-0.0.43.tar.gz", hash = "sha256:ea4cff50548f2a1877e848d3abe9e293cde8ab94757a7eb93fc0d4013f98be8e", size = 5798429, upload-time = "2026-06-04T00:52:10.013Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ae/4c/c54937e4ff3fa7b34a99ea3387ec766bf0ad98dc8df8d792e89b388e658e/ty-0.0.64-py3-none-linux_armv6l.whl", hash = "sha256:3830a6675ab43635ced1c4c557f380ac4a49e9414e03975e4e4e8db644c64944", size = 12118357, upload-time = "2026-07-27T18:32:08.702Z" },
{ url = "https://files.pythonhosted.org/packages/ce/aa/a839ee2bc78e943d079e6abe199a97b4eeffb7e5c9a57326d69de452186a/ty-0.0.64-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3ff07d7bc32a2135f58afe57393789a32ea2fed1a66216129a8e535e76043903", size = 11790882, upload-time = "2026-07-27T18:32:10.997Z" },
{ url = "https://files.pythonhosted.org/packages/4c/de/19f14357888a7198438926303753cf749428e3d62e8980ff1e9a72a78402/ty-0.0.64-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4f6d1c7f897cca05d12bacbf1435150d5ffa496099515aa6ed303c8b29e1d0bb", size = 11317394, upload-time = "2026-07-27T18:32:13.162Z" },
{ url = "https://files.pythonhosted.org/packages/08/2f/f54462300535ab99b551eda733177be2eef5dbc2997d3fdb357c4ddd760a/ty-0.0.64-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:138d6c37ad4bf8583aa7a9b29d90954151d7856f9910a03eae3eb34b34c57215", size = 11863042, upload-time = "2026-07-27T18:32:15.307Z" },
{ url = "https://files.pythonhosted.org/packages/5e/95/dbecf745520ebe8bd7b02fc55eee6441c9be312ebf6addce605eb52740dd/ty-0.0.64-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ed9719d1b7b66fb8efe073d860208a44c40af1f6cd5c2364aa9b323a1e579b4", size = 11910730, upload-time = "2026-07-27T18:32:17.467Z" },
{ url = "https://files.pythonhosted.org/packages/3b/26/12cfd40028e51ceed7b3cb645281c61c02eb64ff9fb0c09231d65c30ff25/ty-0.0.64-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d68b23e5169e2137b5f1de7169ab0cebecab6c8eda374c34c1f6394308f58242", size = 12631936, upload-time = "2026-07-27T18:32:19.533Z" },
{ url = "https://files.pythonhosted.org/packages/a0/d0/65ffc2b0a686347193c6f98e9421a7fc2a96fc3cd0b1001cf7cf284baff9/ty-0.0.64-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f41cb07d89d32626fcaf3ed4d262778fcb28b2628b6ed1e172cd7b18820668d8", size = 13171049, upload-time = "2026-07-27T18:32:22.026Z" },
{ url = "https://files.pythonhosted.org/packages/f6/a4/975a5961842dcd6fa60f0770a102c0bba7509da909ab652919bfcdcd4fc7/ty-0.0.64-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5f24e1504ab9e212f92356b82fe088fdeb3a39f9a2f4ff25e505d2e1d0db9056", size = 12826438, upload-time = "2026-07-27T18:32:24.178Z" },
{ url = "https://files.pythonhosted.org/packages/af/ef/dfb9b7f9bcc032d3b540b0d1f55f532a336e2fb41b1bd539c05ae81a151a/ty-0.0.64-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86db830cb914bb33bb8247b66ccea58de4496c2391bd42658aba744b439f3290", size = 12440880, upload-time = "2026-07-27T18:32:26.341Z" },
{ url = "https://files.pythonhosted.org/packages/ee/d0/bedac20505e8a8f5501ad73d7d15d8e421a563fef59993909a23036929a4/ty-0.0.64-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:652ee3d6d03bea76cd2fe8949c78bb5970394ebd7c5fd270e9518c0dee1b931d", size = 12782439, upload-time = "2026-07-27T18:32:28.642Z" },
{ url = "https://files.pythonhosted.org/packages/79/d5/795733f13ceff1378f08b3de0c49d0f518df220ed856b0dfac869f3b7c81/ty-0.0.64-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4838768295774a86e95f9ec5633e739d016adbbb69dcbdc62f3549578a11f624", size = 11814821, upload-time = "2026-07-27T18:32:30.632Z" },
{ url = "https://files.pythonhosted.org/packages/52/3e/9d99cd1e1831003434f508ed9f258a56543194afc3bbe051eba2545fa676/ty-0.0.64-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5a3d700669868599edf39ce5125196682f15a8880cc8c669bc2a83b99984d99b", size = 11928678, upload-time = "2026-07-27T18:32:32.888Z" },
{ url = "https://files.pythonhosted.org/packages/b8/3d/448f49a3503fb119a34348a5714bb92001f252fadbbb12d645f2e744b557/ty-0.0.64-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b161f0a82a8e2f2432db3bf7702b4d3924fa9486ba0014f6710a160fc157df0d", size = 12202249, upload-time = "2026-07-27T18:32:34.905Z" },
{ url = "https://files.pythonhosted.org/packages/dd/9b/75768e562cec990d189dc05807ae72890b20ffbd1e1f43597bb98db63c60/ty-0.0.64-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:39b9dd42908df47c2dc57dda87e656fab97097ffd2618474bbdea986af0d6a9d", size = 12548817, upload-time = "2026-07-27T18:32:36.995Z" },
{ url = "https://files.pythonhosted.org/packages/34/89/44cc276ea6ca0245495014758ffeadde1798fb0b5840c9cf36d8d2ed3250/ty-0.0.64-py3-none-win32.whl", hash = "sha256:d0676ab0e0935795e5843baa28dd5e366dc343d7c0945a996df9eab8e0644885", size = 11545474, upload-time = "2026-07-27T18:32:39.389Z" },
{ url = "https://files.pythonhosted.org/packages/01/7e/d1c8a871a38d17c8f168b9a6975f6247f7660f8334e517656a5e4b4a4858/ty-0.0.64-py3-none-win_amd64.whl", hash = "sha256:dcb9bd31f54097e362b776c26ab4564d4564cdd1355cb883481167a26c03cc3f", size = 12542987, upload-time = "2026-07-27T18:32:41.412Z" },
{ url = "https://files.pythonhosted.org/packages/35/4d/6d18640d0204cacd69abbaca95ad6a34c6d7e9169e9051d0117b17b827ec/ty-0.0.64-py3-none-win_arm64.whl", hash = "sha256:82cc34c1ad9a8feb6059aef193bebcecc656e548f6fab3d518bcd8b57d198d39", size = 11899263, upload-time = "2026-07-27T18:32:43.366Z" },
{ url = "https://files.pythonhosted.org/packages/db/74/1916026a78f20019a2f03adbd6fb4430ddb7ce1e52c2e17a90856a6d192e/ty-0.0.43-py3-none-linux_armv6l.whl", hash = "sha256:3bf70f5446480562bf6c9f639df4b5cb60716b8f8d1a6b8e5811d5c7eccd8bf2", size = 11598153, upload-time = "2026-06-04T00:52:20.646Z" },
{ url = "https://files.pythonhosted.org/packages/b9/af/58bb0089d2635216c8fa6612dd486a3f986d0ab1c46a41527ab95e57f0e3/ty-0.0.43-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7184741f8b15425a1bc64b950ad005cb353573288ac0e8a04f5481ceb3832596", size = 11357811, upload-time = "2026-06-04T00:52:24.683Z" },
{ url = "https://files.pythonhosted.org/packages/d6/9c/32c6b14f3feddf87b59c7a50709e2b3da408258f2f583f05575f77bc8f7b/ty-0.0.43-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8c306379ca9a35f6ae5270fe9bda7af4b46d91822725a2586d78c8b9b5493b62", size = 10772024, upload-time = "2026-06-04T00:52:14.312Z" },
{ url = "https://files.pythonhosted.org/packages/09/fa/98aa4a74bd00cd5efc424923cd1daffbf1e40a0338041cafb203379d746f/ty-0.0.43-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d624b884c9c1fd244ad2a5f026364e7162a22b3f537025941ada2e363e676414", size = 11291034, upload-time = "2026-06-04T00:52:37.249Z" },
{ url = "https://files.pythonhosted.org/packages/b5/db/4de086c38ce96dcada2bd451f43171d2c237f96d8ed19a1ea8fe51bb8ef4/ty-0.0.43-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:281fc4c00fbc196045141faa085055bddc58846b04a2800204701415a1b9c6aa", size = 11364724, upload-time = "2026-06-04T00:52:33.138Z" },
{ url = "https://files.pythonhosted.org/packages/b0/d3/e3cd8e3233a6fd8362a49aa025b79e9f40151a2a86d811ace154c6eb7445/ty-0.0.43-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f57d6cc28de89024b48d1788e4758c05299d5749d4a51c02e71ac655ec23d9a5", size = 11890555, upload-time = "2026-06-04T00:52:22.711Z" },
{ url = "https://files.pythonhosted.org/packages/80/7b/6f46d444e8241606bbde098df3dca93f2ec0b834a42055db85ee7d33646f/ty-0.0.43-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0a1d6ad6c5e7792c7eac0a01e550f2c2004462e01a64a91ea1636aba6fef6e71", size = 12450968, upload-time = "2026-06-04T00:52:28.94Z" },
{ url = "https://files.pythonhosted.org/packages/4a/e1/79fbe51f2e4b9d8347f2013cd7ed0b63f3b499038c02dc0357e9b28a3a47/ty-0.0.43-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:66d474395d7635fb618bdbb58b4e3360259a2056d0a5621b82754b9da2cd8a04", size = 12064187, upload-time = "2026-06-04T00:52:12.039Z" },
{ url = "https://files.pythonhosted.org/packages/9b/3f/c758a3a8df5b90d331f2b60c8f16021ee64d75e78f99d67cc4efc9bf5f4b/ty-0.0.43-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2663a0003a8b60fb98db7f6f6e673df80b21d0fe3a9868a26fb06b4e049b6fc4", size = 11943208, upload-time = "2026-06-04T00:52:31.14Z" },
{ url = "https://files.pythonhosted.org/packages/54/5f/f516442749cf1b45ca6720a5d41df2738a486ed9ace774c03d515db89084/ty-0.0.43-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:d5a6c352d374d889189d5ec82b54b26a5885f769f7b7787f7f875500dcb8673e", size = 12143572, upload-time = "2026-06-04T00:52:18.457Z" },
{ url = "https://files.pythonhosted.org/packages/b7/bf/0d83c7f43bf4c10f3678bfe7d938e51c445298c7b923f155c5204730c2df/ty-0.0.43-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e7dbbeedfad3ca250d74fcc355fa9ab6b38d2a17f22d6304f615716939dbbb27", size = 11279355, upload-time = "2026-06-04T00:52:26.726Z" },
{ url = "https://files.pythonhosted.org/packages/3e/de/a6c978bef6d9e949f79f4782d9e4ee4df0893713e73b055d84c1a5116b9a/ty-0.0.43-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:24b18a0273ee46154996cfcfa27438f851f440c925587ec200df6f98dffe67d3", size = 11408412, upload-time = "2026-06-04T00:52:35.282Z" },
{ url = "https://files.pythonhosted.org/packages/ec/b1/d13857c23867f0f76b92e38e5841c64ca5e76dc5d4bf27f52cb81d8ab685/ty-0.0.43-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2ef681951520d692b7e9c0b5e56aacf4f98ccae47cf6ffccaf2c7b6b33dc226e", size = 11541709, upload-time = "2026-06-04T00:52:16.451Z" },
{ url = "https://files.pythonhosted.org/packages/7c/f1/cd6afc6f6a687e238bf5e12189f7920e81a0bdef6c3dba4c784ef140f7d9/ty-0.0.43-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2af105de7437143aa4676b28016b5bee661aaaa4eff52be5867fb25119641ceb", size = 12041266, upload-time = "2026-06-04T00:52:43.541Z" },
{ url = "https://files.pythonhosted.org/packages/bd/ba/51ca7c3335da2b8d0a3e477fa4986be9f4a53b05bfab862967d8d2e6ca60/ty-0.0.43-py3-none-win32.whl", hash = "sha256:e4773115b0d6486ee30f1657fc8bdffe7e3a3f5300ab77ef2495da6e83e4694f", size = 10858724, upload-time = "2026-06-04T00:52:07.843Z" },
{ url = "https://files.pythonhosted.org/packages/9f/29/5d80453e5f7c520145fa058851da87230dbd7ca761a7675447a9fe504e0b/ty-0.0.43-py3-none-win_amd64.whl", hash = "sha256:48d3545094a4ae6395492c7e6ac90550fce969e0ed2815fbf8c5da9756676b7d", size = 11976157, upload-time = "2026-06-04T00:52:41.438Z" },
{ url = "https://files.pythonhosted.org/packages/dc/ed/befe5a543e5b95e754ed38ee95239e44efda9bc5f578db4ac1bc8dd758d6/ty-0.0.43-py3-none-win_arm64.whl", hash = "sha256:740ca33d7f75f655a4e7d475bc42dfb825c13219bb073fad30fcc04d35790c74", size = 11308680, upload-time = "2026-06-04T00:52:39.233Z" },
]
[[package]]
@@ -24,10 +24,12 @@ from langgraph.checkpoint.base import (
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
from langgraph.checkpoint.sqlite._delta import (
DELTA_STAGE1_SQL,
DELTA_WALK_SQL,
build_delta_channels_writes_history,
build_delta_stage2_sql,
step_walk_with_row,
build_delta_writes_fetch_sql,
parse_supersteps_since_last_snapshot_by_channel,
resolve_delta_chains,
step_walk_supersteps,
)
from langgraph.checkpoint.sqlite.utils import search_where
@@ -77,7 +79,7 @@ class SqliteSaver(BaseCheckpointSaver[str]):
>>> result = graph.invoke(3, config)
>>> graph.get_state(config)
StateSnapshot(values=4, next=(), config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '0c62ca34-ac19-445d-bbb0-5b4984975b2a'}}, parent_config=None)
"""
""" # noqa
conn: sqlite3.Connection
is_setup: bool
@@ -222,7 +224,7 @@ class SqliteSaver(BaseCheckpointSaver[str]):
>>> checkpoint_tuple = memory.get_tuple(config)
>>> print(checkpoint_tuple)
CheckpointTuple(...)
"""
""" # noqa
checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
with self.cursor(transaction=False) as cur:
# find the latest checkpoint for the thread_id
@@ -505,68 +507,106 @@ class SqliteSaver(BaseCheckpointSaver[str]):
) -> Mapping[str, DeltaChannelHistory]:
"""Fast-path override of `BaseCheckpointSaver.get_delta_channel_history`.
Two-stage query:
Walks the parent chain `supersteps` hops (read from the target's
`counters_since_delta_snapshot` metadata) to each channel's seed
snapshot, then collects the writes between the seed and the target.
* Stage 1 (paged): newest-first slice of `checkpoints` returning
* WALK: stream a newest-first slice of `checkpoints` returning
`(checkpoint_id, parent_checkpoint_id, type, checkpoint)` per
ancestor. Sqlite has no JSONB, so we ship the full serialized
checkpoint blob and inspect `channel_values` in Python. Pages
newest-first by `checkpoint_id` with a `< cursor` predicate;
page size is `DELTA_PAGE_SIZE`. Stops paging when every channel
has found its seed or the chain is exhausted.
ancestor. Sqlite has no JSONB, so we deserialize only the seed
checkpoints to read their inline `channel_values`. Stops once the
shared chain reaches the deepest requested `supersteps` or the
root is reached.
* Stage 2 (per-channel UNION ALL): one branch per channel reading
`writes` filtered to that channel's specific `chain_cids`. No
separate seed-blob fetch — sqlite stores `channel_values` inline
in the checkpoint blob, so seeds come back from stage 1.
* FETCH (per-channel UNION ALL): one branch per channel reading
`writes` filtered to that channel's `chain_cids`. No separate
seed-blob fetch — sqlite stores `channel_values` inline.
"""
if not channels:
return {}
channels = list(channels)
thread_id = str(config["configurable"]["thread_id"])
if not thread_id:
raise ValueError("empty thread ID")
checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
checkpoint_id = get_checkpoint_id(config)
if checkpoint_id is None:
target = self.get_tuple(config)
if target is None:
return {ch: {"writes": []} for ch in channels}
checkpoint_id = target.config["configurable"]["checkpoint_id"]
chain_by_ch: dict[str, list[str]] = {ch: [] for ch in channels}
seed_val_by_ch: dict[str, Any] = {}
# Resolve the target checkpoint id + its metadata (for supersteps).
checkpoint_id = get_checkpoint_id(config)
with self.cursor(transaction=False) as cur:
if checkpoint_id is None:
cur.execute(
"SELECT checkpoint_id, metadata FROM checkpoints "
"WHERE thread_id = ? AND checkpoint_ns = ? "
"ORDER BY checkpoint_id DESC LIMIT 1",
(thread_id, checkpoint_ns),
)
else:
cur.execute(
"SELECT checkpoint_id, metadata FROM checkpoints "
"WHERE thread_id = ? AND checkpoint_ns = ? "
"AND checkpoint_id = ?",
(thread_id, checkpoint_ns, checkpoint_id),
)
target_row = cur.fetchone()
if target_row is None:
return {ch: {"writes": []} for ch in channels}
target_id = str(target_row[0])
metadata = json.loads(target_row[1]) if target_row[1] is not None else {}
supersteps_by_ch = parse_supersteps_since_last_snapshot_by_channel(
metadata, channels
)
max_supersteps = max(supersteps_by_ch.values(), default=0)
needed_depths = set(supersteps_by_ch.values())
shared_cpid_chain: list[str] = []
walk_state: dict[str, Any] = {}
seeded: set[str] = set()
seed_values_by_depth: dict[int, dict[str, Any]] = {}
with self.cursor(transaction=False) as cur:
cur.execute(DELTA_STAGE1_SQL, (thread_id, checkpoint_ns, checkpoint_id))
for row in cur:
cid, parent_cid, type_tag, blob = row
if step_walk_with_row(
cid=cid,
parent_cid=parent_cid,
type_tag=type_tag,
blob=blob,
target_id=checkpoint_id,
serde=self.serde,
chain_by_ch=chain_by_ch,
seed_val_by_ch=seed_val_by_ch,
walk_state=walk_state,
seeded=seeded,
channels=channels,
):
break
if max_supersteps > 0:
cur.execute(DELTA_WALK_SQL, (thread_id, checkpoint_ns, target_id))
for row in cur:
cid, parent_cid, type_tag, blob = row
if step_walk_supersteps(
cid=cid,
parent_cid=parent_cid,
type_tag=type_tag,
blob=blob,
target_id=target_id,
serde=self.serde,
shared_cpid_chain=shared_cpid_chain,
walk_state=walk_state,
max_supersteps=max_supersteps,
needed_depths=needed_depths,
seed_values_by_depth=seed_values_by_depth,
channels=channels,
):
break
channels_with_chain = [ch for ch in channels if chain_by_ch[ch]]
stage2_sql = build_delta_stage2_sql(
chain_lens=[len(chain_by_ch[ch]) for ch in channels_with_chain],
(
chained_cpid_by_ch,
seed_cpid_by_ch,
seed_value_by_ch,
) = resolve_delta_chains(
channels=channels,
supersteps_by_ch=supersteps_by_ch,
shared_cpid_chain=shared_cpid_chain,
seed_values_by_depth=seed_values_by_depth,
has_reached_root=bool(walk_state.get("reached_root")),
thread_id=thread_id,
)
if stage2_sql:
stage2_params: list[Any] = []
channels_with_chain = [ch for ch in channels if chained_cpid_by_ch[ch]]
fetch_sql = build_delta_writes_fetch_sql(
chain_lens=[len(chained_cpid_by_ch[ch]) for ch in channels_with_chain],
)
if fetch_sql:
fetch_params: list[Any] = []
for ch in channels_with_chain:
stage2_params.extend(
[thread_id, checkpoint_ns, ch, *chain_by_ch[ch]]
fetch_params.extend(
[thread_id, checkpoint_ns, ch, *chained_cpid_by_ch[ch]]
)
cur.execute(stage2_sql, stage2_params)
cur.execute(fetch_sql, fetch_params)
stage2_rows = cast(
"list[tuple[str, str, str, int, str, bytes]]", cur.fetchall()
)
@@ -575,9 +615,9 @@ class SqliteSaver(BaseCheckpointSaver[str]):
return build_delta_channels_writes_history(
channels=channels,
chain_by_ch=chain_by_ch,
seed_val_by_ch=seed_val_by_ch,
seeded=seeded,
chained_cpid_by_ch=chained_cpid_by_ch,
seed_cpid_by_ch=seed_cpid_by_ch,
seed_value_by_ch=seed_value_by_ch,
stage2_rows=stage2_rows,
serde=self.serde,
)
@@ -1,37 +1,50 @@
"""Shared helpers for `get_delta_channel_history` on sqlite savers.
Mirrors the two-stage shape of `BasePostgresSaver` (ancestor walk +
per-channel UNION ALL writes fetch), but adapted for sqlite's
constraints. The structural differences:
Mirrors the supersteps-based two-pass shape of `BasePostgresSaver`
(ancestor walk bounded by `counters_since_delta_snapshot` + per-channel
UNION ALL writes fetch), adapted for sqlite's constraints:
* No JSONB — to inspect `channel_values` for a checkpoint we must
deserialize the full blob. Stage 1 streams the cursor row-by-row and
deserializes only the rows the merged walk visits, freeing each blob
before advancing.
deserialize the full blob. The WALK streams the cursor row-by-row and
deserializes only the seed checkpoints (the ones at a channel's
`supersteps` depth), freeing each blob before advancing.
* No separate blob table — `channel_values` lives inline in the
checkpoint, so seeds come back from stage 1 with no second fetch.
* Single merged walk (not K independent walks): each visited cid is
deserialized exactly once, regardless of how many channels are still
seeking their seed.
checkpoint, so seeds come back from the WALK with no second fetch.
* Single shared parent-chain walk: each requested channel slices the
same chain to its own `supersteps` depth.
The streaming design keeps peak in-flight memory at roughly one
deserialized checkpoint at a time, instead of holding the entire
ancestor chain's worth of raw blobs as a `fetchall()`-materialized list.
Walk depth is driven by the *supersteps since last snapshot* counter,
not by scanning `channel_values` for the snapshot marker — the only
reliable way to locate seeds that aren't a `_DeltaSnapshot` sentinel
(e.g. legacy plain-value blobs from a thread migrated off a non-delta
channel).
"""
from __future__ import annotations
import logging
from collections.abc import Mapping, Sequence
from typing import Any
from langgraph.checkpoint.base import DeltaChannelHistory, PendingWrite
from langgraph.checkpoint.base import (
DeltaChannelHistory,
PendingWrite,
_parse_supersteps_since_last_snapshot_by_channel,
)
from langgraph.checkpoint.serde.types import _DeltaSnapshot
# Stage 1 streams ancestors of `target_cid` newest-first. The `<=`
logger = logging.getLogger(__name__)
# Re-exported under the package-local name used by the sqlite savers.
parse_supersteps_since_last_snapshot_by_channel = (
_parse_supersteps_since_last_snapshot_by_channel
)
# The WALK streams ancestors of `target_id` newest-first. The `<=`
# predicate keeps target itself in the stream so we can read its
# `parent_checkpoint_id` from the first row without a separate lookup;
# the caller skips target's own writes/seed (matches the
# `BaseCheckpointSaver` contract).
DELTA_STAGE1_SQL = (
# `parent_checkpoint_id` from the first matching row without a separate
# lookup; target's own writes/seed are not part of the contract.
DELTA_WALK_SQL = (
"SELECT checkpoint_id, parent_checkpoint_id, type, checkpoint "
"FROM checkpoints "
"WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id <= ? "
@@ -39,12 +52,12 @@ DELTA_STAGE1_SQL = (
)
def build_delta_stage2_sql(*, chain_lens: Sequence[int]) -> str:
"""Stage-2 per-channel UNION ALL fetching writes from `writes`.
def build_delta_writes_fetch_sql(*, chain_lens: Sequence[int]) -> str:
"""Per-channel UNION ALL fetching writes from `writes`.
One branch per channel with a non-empty chain. Each branch inlines its
own `IN (?, ?, ...)` placeholder list because sqlite has no array-bind
equivalent of postgres's `= ANY(%s)`. Caller passes parameters in
equivalent of postgres's `= ANY(?)`. Caller passes parameters in
matching order: `[thread_id, checkpoint_ns, channel, *chain_cids]` per
branch.
@@ -65,7 +78,7 @@ def build_delta_stage2_sql(*, chain_lens: Sequence[int]) -> str:
return " UNION ALL ".join(branches)
def step_walk_with_row(
def step_walk_supersteps(
*,
cid: str,
parent_cid: str | None,
@@ -73,75 +86,142 @@ def step_walk_with_row(
blob: bytes,
target_id: str,
serde: Any,
chain_by_ch: dict[str, list[str]],
seed_val_by_ch: dict[str, Any],
shared_cpid_chain: list[str],
walk_state: dict[str, Any],
seeded: set[str],
max_supersteps: int,
needed_depths: set[int],
seed_values_by_depth: dict[int, dict[str, Any]],
channels: Sequence[str],
) -> bool:
"""Process one streamed stage-1 row in the merged ancestor walk.
"""Process one streamed WALK row, extending the shared parent chain.
The cursor returns (cid, parent_cid, type, blob) rows in
`checkpoint_id` DESC order starting at target. The first row is
target itself; we read its parent_cid to seed the walk and otherwise
skip it (target's own writes/seed are not part of the contract).
The cursor returns `(cid, parent_cid, type, checkpoint)` rows in
`checkpoint_id` DESC order starting at target. The first row is target
itself; we read its `parent_cid` to seed the walk and skip it (target's
own writes/seed are not part of the contract). Off-path rows (a sibling
branch on the same thread) advance the cursor without doing work.
For each subsequent row, if `cid` matches the walk's current
position, we deserialize the blob, append the cid to every
not-yet-seeded channel's chain, and check `channel_values` for
seeds. The deserialized checkpoint is dropped before advancing — no
cross-row cache, so peak in-flight is one deserialized checkpoint.
For each on-path ancestor we append its cid to `shared_cpid_chain`
(newest first). When the ancestor sits at a depth some channel needs as
its seed (`len(chain)` ∈ `needed_depths`), we deserialize it once and
record its `channel_values` for the requested channels. The
deserialized checkpoint is dropped immediately — peak in-flight is one
deserialized checkpoint.
Off-path rows (different branch on the same thread) advance the
cursor without doing any work.
Returns True when every requested channel is seeded — the caller
can stop iterating and close the cursor.
Sets `walk_state["reached_root"]` when an ancestor has no parent.
Returns True when the walk can stop: chain reached `max_supersteps`, or
the root was reached.
"""
if "started" not in walk_state:
if cid == target_id:
walk_state["started"] = True
walk_state["cur_cid"] = parent_cid
walk_state["active"] = {ch for ch in channels if ch not in seeded}
if parent_cid is None:
walk_state["reached_root"] = True
return True
# Not target yet (or target not present): keep streaming.
return False
active: set[str] = walk_state["active"]
if not active:
if len(shared_cpid_chain) >= max_supersteps:
return True
if cid != walk_state["cur_cid"]:
# Off-path row from a sibling branch — skip without deserializing.
return False
for ch in active:
chain_by_ch[ch].append(cid)
ckpt = serde.loads_typed((type_tag, blob))
channel_values: Mapping[str, Any] = ckpt.get("channel_values") or {}
for ch in [ch for ch in active if ch in channel_values]:
seed_val_by_ch[ch] = channel_values[ch]
seeded.add(ch)
active.discard(ch)
del ckpt, channel_values
shared_cpid_chain.append(cid)
depth = len(shared_cpid_chain) # 1-indexed position along the chain
# Capture channel_values at any depth a channel may use as its seed: the
# exact `supersteps` depths, plus the root-most checkpoint (the seed
# candidate when the chain is shorter than `supersteps`).
if depth in needed_depths or parent_cid is None:
ckpt = serde.loads_typed((type_tag, blob))
channel_values: Mapping[str, Any] = ckpt.get("channel_values") or {}
seed_values_by_depth[depth] = {
ch: channel_values[ch] for ch in channels if ch in channel_values
}
del ckpt, channel_values
if parent_cid is None:
walk_state["reached_root"] = True
return True
walk_state["cur_cid"] = parent_cid
return not active
return len(shared_cpid_chain) >= max_supersteps
def resolve_delta_chains(
*,
channels: Sequence[str],
supersteps_by_ch: Mapping[str, int],
shared_cpid_chain: Sequence[str],
seed_values_by_depth: Mapping[int, Mapping[str, Any]],
has_reached_root: bool,
thread_id: str,
) -> tuple[
dict[str, list[str]],
dict[str, str | None],
dict[str, Any],
]:
"""Slice the shared parent chain into per-channel chain/seed mappings.
For each channel the seed snapshot sits `supersteps` hops back: the seed
checkpoint is `shared_cpid_chain[supersteps - 1]` and the chain is
`shared_cpid_chain[:supersteps]` (newest first), with the seed's inline
`channel_values[ch]` captured during the walk.
When the chain is shorter than `supersteps` but the walk reached the
root, the persisted chain is "compressed" relative to the logical
superstep count (`durability="exit"`, or a thread that never
snapshotted). The seed candidate is then the oldest persisted checkpoint
(`shared_cpid_chain[-1]`); if its `channel_values[ch]` is absent the
channel has no seed and replays the full chain on an empty baseline.
"""
chained_cpid_by_ch: dict[str, list[str]] = {ch: [] for ch in channels}
seed_cpid_by_ch: dict[str, str | None] = {ch: None for ch in channels}
seed_value_by_ch: dict[str, Any] = {}
for ch in channels:
bound = supersteps_by_ch.get(ch, 0)
if bound <= 0:
continue
if len(shared_cpid_chain) >= bound:
seed_depth = bound
elif has_reached_root:
seed_depth = len(shared_cpid_chain)
else:
logger.warning(
"cannot find seed snapshot for delta channel "
"(thread_id=%s, channel=%s)",
thread_id,
ch,
)
continue
if seed_depth <= 0:
continue
chained_cpid_by_ch[ch] = list(shared_cpid_chain[:seed_depth])
seed_cpid_by_ch[ch] = shared_cpid_chain[seed_depth - 1]
seed_vals = seed_values_by_depth.get(seed_depth, {})
if ch in seed_vals:
seed_value_by_ch[ch] = seed_vals[ch]
return chained_cpid_by_ch, seed_cpid_by_ch, seed_value_by_ch
def build_delta_channels_writes_history(
*,
channels: Sequence[str],
chain_by_ch: Mapping[str, list[str]],
seed_val_by_ch: Mapping[str, Any],
seeded: set[str],
chained_cpid_by_ch: Mapping[str, Sequence[str]],
seed_cpid_by_ch: Mapping[str, str | None],
seed_value_by_ch: Mapping[str, Any],
stage2_rows: Sequence[tuple[str, str, str, int, str, bytes]],
serde: Any,
) -> dict[str, DeltaChannelHistory]:
"""Demux stage-2 rows per channel; produce per-channel histories.
"""Demux writes rows per channel; produce per-channel histories.
Stage-2 rows are `(checkpoint_id, channel, task_id, idx, type, value)`.
`stage2_rows` are `(checkpoint_id, channel, task_id, idx, type, value)`.
Final write order is oldest→newest globally and `(task_id, idx)` within
a checkpoint, matching the contract on `DeltaChannelHistory.writes`.
`seed` is omitted when the walk reached a true root with no snapshot
found (channel never entered `seeded`); consumers treat absence as
"start empty".
The seed checkpoint's own writes are replayed on top of a
`_DeltaSnapshot` seed (the snapshot is the value *prior* to its own
writes), but skipped for a migrated plain-value seed (a legacy
non-delta blob already incorporates those writes). `seed` is omitted
when no seed was located (implicit empty baseline); consumers treat
absence as "start empty".
"""
writes_by_ch_by_cid: dict[str, dict[str, list[tuple[str, bytes, str, int]]]] = {
ch: {} for ch in channels
@@ -156,17 +236,26 @@ def build_delta_channels_writes_history(
result: dict[str, DeltaChannelHistory] = {}
for ch in channels:
chain_cids = chain_by_ch.get(ch, [])
entry: DeltaChannelHistory = {"writes": []}
skip_seed_checkpoint_writes = False
if ch in seed_value_by_ch:
seed_value = seed_value_by_ch[ch]
entry["seed"] = seed_value
skip_seed_checkpoint_writes = not isinstance(seed_value, _DeltaSnapshot)
cid_writes = writes_by_ch_by_cid.get(ch, {})
collected: list[PendingWrite] = []
# Chain is newest-first; iterate oldest-first for the public order.
for cid in reversed(chain_cids):
for type_tag, value_blob, task_id, _idx in cid_writes.get(cid, []):
collected.append(
(task_id, ch, serde.loads_typed((type_tag, value_blob)))
)
entry: DeltaChannelHistory = {"writes": collected}
if ch in seeded:
entry["seed"] = seed_val_by_ch[ch]
if cid_writes:
collected: list[PendingWrite] = []
seed_cpid = seed_cpid_by_ch.get(ch)
# Chain is newest→oldest; replay oldest→newest.
for cid in reversed(list(chained_cpid_by_ch.get(ch, []))):
if skip_seed_checkpoint_writes and cid == seed_cpid:
continue
for type_tag, value_blob, task_id, _idx in cid_writes.get(cid, []):
collected.append(
(task_id, ch, serde.loads_typed((type_tag, value_blob)))
)
entry["writes"] = collected
result[ch] = entry
return result
@@ -25,10 +25,12 @@ from langgraph.checkpoint.base import (
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
from langgraph.checkpoint.sqlite._delta import (
DELTA_STAGE1_SQL,
DELTA_WALK_SQL,
build_delta_channels_writes_history,
build_delta_stage2_sql,
step_walk_with_row,
build_delta_writes_fetch_sql,
parse_supersteps_since_last_snapshot_by_channel,
resolve_delta_chains,
step_walk_supersteps,
)
from langgraph.checkpoint.sqlite.utils import search_where
@@ -212,7 +214,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
while True:
try:
yield asyncio.run_coroutine_threadsafe(
anext(aiter_), # type: ignore[arg-type]
anext(aiter_), # type: ignore[arg-type] # noqa: F821
self.loop,
).result()
except StopAsyncIteration:
@@ -625,61 +627,98 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
"""Fast-path override of `BaseCheckpointSaver.aget_delta_channel_history`.
See `SqliteSaver.get_delta_channel_history` for design notes; this
is the async equivalent using `aiosqlite` cursors. Stage 1 pages
the parent chain newest-first and Python-deserializes each
checkpoint blob to find per-channel snapshots; stage 2 fetches
only the relevant writes via per-channel UNION ALL.
is the async equivalent using `aiosqlite` cursors. The WALK streams
the parent chain newest-first, bounded by the target's
`counters_since_delta_snapshot` supersteps, and deserializes only
seed checkpoints; FETCH pulls the relevant writes via per-channel
UNION ALL.
"""
if not channels:
return {}
channels = list(channels)
await self.setup()
thread_id = str(config["configurable"]["thread_id"])
if not thread_id:
raise ValueError("empty thread ID")
checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
checkpoint_id = get_checkpoint_id(config)
if checkpoint_id is None:
target = await self.aget_tuple(config)
if target is None:
return {ch: {"writes": []} for ch in channels}
checkpoint_id = target.config["configurable"]["checkpoint_id"]
chain_by_ch: dict[str, list[str]] = {ch: [] for ch in channels}
seed_val_by_ch: dict[str, Any] = {}
# Resolve the target checkpoint id + its metadata (for supersteps).
checkpoint_id = get_checkpoint_id(config)
async with self.lock, self.conn.cursor() as cur:
if checkpoint_id is None:
await cur.execute(
"SELECT checkpoint_id, metadata FROM checkpoints "
"WHERE thread_id = ? AND checkpoint_ns = ? "
"ORDER BY checkpoint_id DESC LIMIT 1",
(thread_id, checkpoint_ns),
)
else:
await cur.execute(
"SELECT checkpoint_id, metadata FROM checkpoints "
"WHERE thread_id = ? AND checkpoint_ns = ? "
"AND checkpoint_id = ?",
(thread_id, checkpoint_ns, checkpoint_id),
)
target_row = await cur.fetchone()
if target_row is None:
return {ch: {"writes": []} for ch in channels}
target_id = str(target_row[0])
metadata = json.loads(target_row[1]) if target_row[1] is not None else {}
supersteps_by_ch = parse_supersteps_since_last_snapshot_by_channel(
metadata, channels
)
max_supersteps = max(supersteps_by_ch.values(), default=0)
needed_depths = set(supersteps_by_ch.values())
shared_cpid_chain: list[str] = []
walk_state: dict[str, Any] = {}
seeded: set[str] = set()
seed_values_by_depth: dict[int, dict[str, Any]] = {}
async with self.lock, self.conn.cursor() as cur:
await cur.execute(
DELTA_STAGE1_SQL, (thread_id, checkpoint_ns, checkpoint_id)
)
async for row in cur:
cid, parent_cid, type_tag, blob = row
if step_walk_with_row(
cid=cid,
parent_cid=parent_cid,
type_tag=type_tag,
blob=blob,
target_id=checkpoint_id,
serde=self.serde,
chain_by_ch=chain_by_ch,
seed_val_by_ch=seed_val_by_ch,
walk_state=walk_state,
seeded=seeded,
channels=channels,
):
break
if max_supersteps > 0:
await cur.execute(DELTA_WALK_SQL, (thread_id, checkpoint_ns, target_id))
async for row in cur:
cid, parent_cid, type_tag, blob = row
if step_walk_supersteps(
cid=cid,
parent_cid=parent_cid,
type_tag=type_tag,
blob=blob,
target_id=target_id,
serde=self.serde,
shared_cpid_chain=shared_cpid_chain,
walk_state=walk_state,
max_supersteps=max_supersteps,
needed_depths=needed_depths,
seed_values_by_depth=seed_values_by_depth,
channels=channels,
):
break
channels_with_chain = [ch for ch in channels if chain_by_ch[ch]]
stage2_sql = build_delta_stage2_sql(
chain_lens=[len(chain_by_ch[ch]) for ch in channels_with_chain],
(
chained_cpid_by_ch,
seed_cpid_by_ch,
seed_value_by_ch,
) = resolve_delta_chains(
channels=channels,
supersteps_by_ch=supersteps_by_ch,
shared_cpid_chain=shared_cpid_chain,
seed_values_by_depth=seed_values_by_depth,
has_reached_root=bool(walk_state.get("reached_root")),
thread_id=thread_id,
)
if stage2_sql:
stage2_params: list[Any] = []
channels_with_chain = [ch for ch in channels if chained_cpid_by_ch[ch]]
fetch_sql = build_delta_writes_fetch_sql(
chain_lens=[len(chained_cpid_by_ch[ch]) for ch in channels_with_chain],
)
if fetch_sql:
fetch_params: list[Any] = []
for ch in channels_with_chain:
stage2_params.extend(
[thread_id, checkpoint_ns, ch, *chain_by_ch[ch]]
fetch_params.extend(
[thread_id, checkpoint_ns, ch, *chained_cpid_by_ch[ch]]
)
await cur.execute(stage2_sql, stage2_params)
await cur.execute(fetch_sql, fetch_params)
stage2_rows = cast(
"list[tuple[str, str, str, int, str, bytes]]",
await cur.fetchall(),
@@ -689,9 +728,9 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
return build_delta_channels_writes_history(
channels=channels,
chain_by_ch=chain_by_ch,
seed_val_by_ch=seed_val_by_ch,
seeded=seeded,
chained_cpid_by_ch=chained_cpid_by_ch,
seed_cpid_by_ch=seed_cpid_by_ch,
seed_value_by_ch=seed_value_by_ch,
stage2_rows=stage2_rows,
serde=self.serde,
)
@@ -24,13 +24,11 @@ from langgraph.store.base.batch import AsyncBatchedBaseStore
from langgraph.store.sqlite.base import (
_PLACEHOLDER,
NS_MATCH_FUNCTION,
BaseSqliteStore,
SqliteIndexConfig,
_decode_ns_text,
_ensure_index_config,
_group_ops,
_namespace_match,
_row_to_item,
_row_to_search_item,
)
@@ -152,13 +150,6 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
if self.is_setup:
return
# list_namespaces needs segment-aware matching, which SQLite cannot
# express in LIKE or GLOB. Registered here rather than in __init__
# because aiosqlite's create_function is a coroutine.
await self.conn.create_function(
NS_MATCH_FUNCTION, 2, _namespace_match, deterministic=True
)
# Create migrations table if it doesn't exist
await self.conn.execute(
"""
@@ -7,7 +7,7 @@ import re
import sqlite3
import threading
from collections import defaultdict
from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
from collections.abc import Callable, Iterable, Iterator, Sequence
from contextlib import contextmanager
from typing import Any, Literal, NamedTuple, cast
@@ -93,74 +93,12 @@ class SqliteIndexConfig(IndexConfig):
pass
NS_MATCH_FUNCTION = "_langgraph_namespace_match"
"""SQLite user function backing segment-aware namespace matching.
Registered under a private name rather than overriding `REGEXP`, so a caller's
own `REGEXP` is left untouched.
"""
def _namespace_match(prefix: str | None, pattern: str) -> int:
"""Backing implementation of `NS_MATCH_FUNCTION`."""
if prefix is None:
return 0
return 1 if re.search(pattern, prefix) else 0
def _escape_glob_literal(text: str) -> str:
"""Escape GLOB metacharacters so `text` is matched literally.
GLOB has no `ESCAPE` clause, so metacharacters are wrapped in a character
class instead. `]` is literal outside a class and needs no escaping.
"""
return text.replace("[", "[[]").replace("*", "[*]").replace("?", "[?]")
def _namespace_prefix_condition(
namespace_prefix: tuple[str, ...], column: str = "prefix"
) -> tuple[str, tuple[str, ...]]:
"""Build the SQL scoping a search to a namespace and its descendants.
Matches the namespace exactly or requires the `.` separator before any
remainder, so a prefix of `("foo",)` does not also match `("foobar",)`.
Uses GLOB rather than LIKE because SQLite's LIKE is case-insensitive for
ASCII, which would match `("FOO",)` for a prefix of `("foo",)` even though
`get`/`put`/`delete` compare with `=` and treat those as distinct.
An empty prefix is unconstrained and matches every namespace.
"""
if not namespace_prefix:
return "TRUE", ()
path = _namespace_to_text(namespace_prefix)
condition = f"({column} = ? OR {column} GLOB ?)"
return condition, (path, f"{_escape_glob_literal(path)}.*")
def _namespace_match_pattern(path: tuple[str, ...], match_type: str) -> str:
"""Build a regex matching the dot-joined prefix on whole namespace segments.
Needed because neither LIKE nor GLOB can express "any character except the
separator": GLOB has character classes but no quantifier, so `[^.]*` still
crosses `.`. Matches how `InMemoryStore` compares namespaces element-wise.
`*` matches exactly one segment. Prefix matches stay open-ended but must end
on a separator; suffix matches anchor at the end and begin on one.
Examples:
prefix ("uid", "*", "alice") -> ^uid\\.[^.]+\\.alice(\\.|\\Z)
suffix ("alice",) -> (^|\\.)alice\\Z
"""
segments = ("[^.]+" if part == "*" else re.escape(part) for part in path)
body = r"\.".join(segments)
if match_type == "suffix":
return rf"(^|\.){body}\Z"
return rf"^{body}(\.|\Z)"
def _namespace_to_text(namespace: tuple[str, ...]) -> str:
def _namespace_to_text(
namespace: tuple[str, ...], handle_wildcards: bool = False
) -> str:
"""Convert namespace tuple to text string."""
if handle_wildcards:
namespace = tuple("%" if val == "*" else val for val in namespace)
return ".".join(namespace)
@@ -387,7 +325,7 @@ class BaseSqliteStore:
[
_namespace_to_text(op.namespace),
op.key,
orjson.dumps(dict(cast(Mapping[str, Any], op.value))),
orjson.dumps(cast(dict, op.value)),
expires_at,
op.ttl,
]
@@ -523,11 +461,8 @@ class BaseSqliteStore:
else " AND " + " AND ".join(filter_conditions)
)
if op.namespace_prefix:
ns_condition, ns_args_tuple = _namespace_prefix_condition(
op.namespace_prefix, column="s.prefix"
)
prefix_filter_str = f"WHERE {ns_condition} {filter_str} "
ns_args: Sequence = ns_args_tuple
prefix_filter_str = f"WHERE s.prefix LIKE ? {filter_str} "
ns_args: Sequence = (f"{_namespace_to_text(op.namespace_prefix)}%",)
else:
ns_args = ()
if filter_str:
@@ -568,15 +503,12 @@ class BaseSqliteStore:
]
# Regular search branch (no vector search)
else:
ns_condition, ns_args_tuple = _namespace_prefix_condition(
op.namespace_prefix
)
base_query = f"""
base_query = """
SELECT prefix, key, value, created_at, updated_at, expires_at, ttl_minutes, NULL as score
FROM store
WHERE {ns_condition}
WHERE prefix LIKE ?
"""
params = list(ns_args_tuple)
params = [f"{_namespace_to_text(op.namespace_prefix)}%"]
if filter_conditions:
params.extend(filter_params)
@@ -617,28 +549,16 @@ class BaseSqliteStore:
if op.match_conditions:
for cond in op.match_conditions:
if cond.match_type in ("prefix", "suffix"):
if not cond.path:
# An empty path constrains nothing; skipping keeps it a
# no-op rather than emitting a pattern that matches no
# namespace at all.
continue
if cond.match_type == "prefix" and "*" not in cond.path:
# Equivalent to the anchored pattern, but SQLite can
# satisfy `=` and a trailing-wildcard GLOB from
# store_prefix_idx. The user function is opaque to the
# planner, so it would scan every row and call back
# into Python for each one.
condition, args = _namespace_prefix_condition(
tuple(cond.path)
)
where_clauses.append(condition)
params.extend(args)
else:
where_clauses.append(f"{NS_MATCH_FUNCTION}(prefix, ?) = 1")
params.append(
_namespace_match_pattern(cond.path, cond.match_type)
)
if cond.match_type == "prefix":
where_clauses.append("prefix LIKE ?")
params.append(
f"{_namespace_to_text(cond.path, handle_wildcards=True)}%"
)
elif cond.match_type == "suffix":
where_clauses.append("prefix LIKE ?")
params.append(
f"%{_namespace_to_text(cond.path, handle_wildcards=True)}"
)
else:
logger.warning(
"Unknown match_type in list_namespaces: %s", cond.match_type
@@ -865,9 +785,6 @@ class SqliteStore(BaseSqliteStore, BaseStore):
super().__init__()
self._deserializer = deserializer
self.conn = conn
# Registered here rather than in from_conn_string so a caller-supplied
# connection also gets it.
conn.create_function(NS_MATCH_FUNCTION, 2, _namespace_match, deterministic=True)
self.lock = threading.Lock()
self.is_setup = False
self.index_config = index
+1 -5
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "langgraph-checkpoint-sqlite"
version = "3.1.1"
version = "3.1.0"
description = "Library with a SQLite implementation of LangGraph checkpoint saver."
authors = []
requires-python = ">=3.10"
@@ -30,7 +30,6 @@ test = [
"pytest-mock",
"pytest-watcher",
"langgraph-checkpoint",
"langgraph-checkpoint-conformance",
"pytest-retry>=1.7.0",
]
lint = [
@@ -48,7 +47,6 @@ default-groups = ['dev']
[tool.uv.sources]
langgraph-checkpoint = { path = "../checkpoint", editable = true }
langgraph-checkpoint-conformance = { path = "../checkpoint-conformance", editable = true }
[tool.hatch.build.targets.wheel]
include = ["langgraph"]
@@ -64,8 +62,6 @@ lint.select = [
"UP", # pyupgrade
"B", # flake8-bugbear
"I", # isort
"PLC0415", # import-outside-top-level
"RUF100", # unused noqa directive
"UP", # pyupgrade
]
lint.ignore = ["E501", "B008"]
@@ -716,32 +716,3 @@ async def test_search_items(
for ns in test_namespaces:
key = f"item_{ns[-1]}"
await store.adelete(ns, key)
async def test_async_namespace_segment_boundary(store: AsyncSqliteStore) -> None:
"""Segment-aware scoping on the async path.
Also covers that the namespace-match SQLite function is registered on the
async connection -- aiosqlite's create_function is a coroutine, so it is
registered in setup() rather than __init__.
"""
for namespace in [
("foo",),
("foo", "child"),
("foobar",),
("uid", "users", "alice"),
("uid", "users", "malice"),
("user_1",),
("userX1",),
]:
await store.aput(namespace, "k", {"v": 1})
found = {item.namespace for item in await store.asearch(("foo",), limit=100)}
assert found == {("foo",), ("foo", "child")}
found = {item.namespace for item in await store.asearch(("user_1",), limit=100)}
assert found == {("user_1",)}
assert set(await store.alist_namespaces(suffix=["alice"], limit=100)) == {
("uid", "users", "alice"),
}
@@ -3,14 +3,21 @@
from __future__ import annotations
import pytest
from langgraph.checkpoint.conformance import validate
from langgraph.checkpoint.conformance.initializer import checkpointer_test
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
pytest.importorskip(
"langgraph.checkpoint.conformance",
reason="langgraph-checkpoint-conformance not installed",
)
pytest.importorskip("aiosqlite", reason="aiosqlite not installed")
@pytest.mark.asyncio
async def test_delta_channel_conformance():
from langgraph.checkpoint.conformance import validate
from langgraph.checkpoint.conformance.initializer import checkpointer_test
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
@checkpointer_test(name="AsyncSqliteSaver")
async def sqlite_saver():
async with AsyncSqliteSaver.from_conn_string(":memory:") as saver:
@@ -29,13 +29,13 @@ pytest.importorskip("langgraph.channels.delta", reason="langgraph core not insta
pytest.importorskip("langgraph.channels.binop", reason="langgraph core not installed")
pytest.importorskip("langgraph.graph", reason="langgraph core not installed")
from langgraph.channels.binop import BinaryOperatorAggregate # type: ignore[import-untyped] # noqa: I001
from langgraph.channels.delta import DeltaChannel # type: ignore[import-untyped]
from langgraph.graph import END, START, StateGraph # type: ignore[import-untyped]
from typing_extensions import TypedDict
from langgraph.channels.binop import BinaryOperatorAggregate # type: ignore[import-untyped] # noqa: E402,I001
from langgraph.channels.delta import DeltaChannel # type: ignore[import-untyped] # noqa: E402
from langgraph.graph import END, START, StateGraph # type: ignore[import-untyped] # noqa: E402
from typing_extensions import TypedDict # noqa: E402
from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
from langgraph.checkpoint.sqlite import SqliteSaver # noqa: E402
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver # noqa: E402
pytestmark = pytest.mark.anyio
@@ -32,13 +32,13 @@ from langchain_core.runnables import RunnableConfig
pytest.importorskip("langgraph.channels.delta", reason="langgraph core not installed")
pytest.importorskip("langgraph.graph", reason="langgraph core not installed")
from langgraph.channels.delta import DeltaChannel # type: ignore[import-untyped] # noqa: I001
from langgraph.checkpoint.serde.types import _DeltaSnapshot
from langgraph.graph import END, START, StateGraph # type: ignore[import-untyped]
from typing_extensions import TypedDict
from langgraph.channels.delta import DeltaChannel # type: ignore[import-untyped] # noqa: E402,I001
from langgraph.checkpoint.serde.types import _DeltaSnapshot # noqa: E402
from langgraph.graph import END, START, StateGraph # type: ignore[import-untyped] # noqa: E402
from typing_extensions import TypedDict # noqa: E402
from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
from langgraph.checkpoint.sqlite import SqliteSaver # noqa: E402
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver # noqa: E402
pytestmark = pytest.mark.anyio
+10 -216
View File
@@ -1,11 +1,7 @@
import math
import os
import random
import re
import tempfile
import time
import uuid
from collections import Counter, defaultdict
from collections.abc import Generator, Iterable
from contextlib import contextmanager
from typing import Any, Literal, cast
@@ -22,13 +18,7 @@ from langgraph.store.base import (
)
from langgraph.store.sqlite import SqliteStore
from langgraph.store.sqlite.base import (
NS_MATCH_FUNCTION,
BaseSqliteStore,
SqliteIndexConfig,
_escape_glob_literal,
_namespace_match_pattern,
)
from langgraph.store.sqlite.base import SqliteIndexConfig
# Local embeddings implementation for testing vector search
@@ -37,6 +27,10 @@ class CharacterEmbeddings(Embeddings):
def __init__(self, dims: int = 50, seed: int = 42):
"""Initialize with embedding dimensions and random seed."""
import math
import random
from collections import defaultdict
self._rng = random.Random(seed)
self.dims = dims
# Create projection vector for each character lazily
@@ -48,6 +42,9 @@ class CharacterEmbeddings(Embeddings):
def _embed_one(self, text: str) -> list[float]:
"""Embed a single text."""
import math
from collections import Counter
counts = Counter(text)
total = sum(counts.values())
@@ -335,6 +332,8 @@ class TestSqliteStore:
# Test update
# Small delay to ensure the updated timestamp is different
import time
time.sleep(0.01)
updated_value = {"title": "Updated Document", "content": "Hello, Updated!"}
@@ -1230,208 +1229,3 @@ def test_non_ascii(
assert result3[0].key == "3"
assert result4[0].key == "4"
assert result5[0].key == "5"
def test_escape_glob_literal() -> None:
assert _escape_glob_literal("users.alice") == "users.alice"
# "_" and "%" are LIKE wildcards but literal in GLOB, so they are left alone.
assert _escape_glob_literal("user_1") == "user_1"
assert _escape_glob_literal("100%") == "100%"
assert _escape_glob_literal("a*b") == "a[*]b"
assert _escape_glob_literal("a?b") == "a[?]b"
assert _escape_glob_literal("a[b") == "a[[]b"
def test_namespace_match_pattern() -> None:
assert _namespace_match_pattern(("foo",), "prefix") == r"^foo(\.|\Z)"
assert (
_namespace_match_pattern(("uid", "*", "alice"), "prefix")
== r"^uid\.[^.]+\.alice(\.|\Z)"
)
assert _namespace_match_pattern(("alice",), "suffix") == r"(^|\.)alice\Z"
def test_search_namespace_segment_boundary(store: SqliteStore) -> None:
"""Prefix scoping must stop at namespace segment boundaries.
Namespaces are stored dot-joined, so matching the raw text also returns
siblings sharing leading characters.
"""
for namespace in [
("foo",),
("foo", "child"),
("foo", "child", "deep"),
("foobar",),
("foobar", "baz"),
("foo2",),
]:
store.put(namespace, "k", {"v": 1})
def _namespaces(prefix: tuple[str, ...]) -> set[tuple[str, ...]]:
return {item.namespace for item in store.search(prefix, limit=100)}
assert _namespaces(("foo",)) == {
("foo",),
("foo", "child"),
("foo", "child", "deep"),
}
# The sibling scope is independent, not merely narrower.
assert _namespaces(("foobar",)) == {("foobar",), ("foobar", "baz")}
assert _namespaces(("foo2",)) == {("foo2",)}
assert _namespaces(("fo",)) == set()
def test_search_namespace_wildcard_chars_are_literal(store: SqliteStore) -> None:
"""LIKE and GLOB metacharacters in labels must be matched literally."""
for namespace in [
("user_1",),
("user_1", "child"),
("userX1",),
("a%b",),
("axxb",),
("star*",),
("starX",),
]:
store.put(namespace, "k", {"v": 1})
def _namespaces(prefix: tuple[str, ...]) -> set[tuple[str, ...]]:
return {item.namespace for item in store.search(prefix, limit=100)}
# Also asserts each namespace still matches itself, which catches escaping
# the equality arm by mistake.
assert _namespaces(("user_1",)) == {("user_1",), ("user_1", "child")}
assert _namespaces(("a%b",)) == {("a%b",)}
assert _namespaces(("star*",)) == {("star*",)}
def test_search_namespace_is_case_sensitive(store: SqliteStore) -> None:
"""Search must agree with get/put, which compare namespaces with `=`.
SQLite's LIKE is case-insensitive for ASCII, so matching with it conflated
namespaces that every other operation treats as distinct.
"""
store.put(("Foo",), "k", {"v": "upper"})
store.put(("foo",), "k", {"v": "lower"})
assert {item.namespace for item in store.search(("foo",), limit=100)} == {("foo",)}
assert {item.namespace for item in store.search(("Foo",), limit=100)} == {("Foo",)}
def test_list_namespaces_segment_boundary(store: SqliteStore) -> None:
for namespace in [
("foo",),
("foo", "child"),
("foobar",),
("foobar", "baz"),
("uid", "users", "alice"),
("uid", "users", "malice"),
("uid", "a", "b", "alice"),
]:
store.put(namespace, "k", {"v": 1})
assert set(store.list_namespaces(prefix=["foo"], limit=100)) == {
("foo",),
("foo", "child"),
}
# Suffix must align to a segment: "malice" does not end with the "alice"
# segment.
assert set(store.list_namespaces(suffix=["alice"], limit=100)) == {
("uid", "users", "alice"),
("uid", "a", "b", "alice"),
}
# "*" spans exactly one segment.
assert set(store.list_namespaces(prefix=["uid", "*", "alice"], limit=100)) == {
("uid", "users", "alice"),
}
# Prefix matching stays open-ended across depth.
assert set(store.list_namespaces(prefix=["uid"], limit=100)) == {
("uid", "users", "alice"),
("uid", "users", "malice"),
("uid", "a", "b", "alice"),
}
def test_search_empty_prefix_is_unconstrained(store: SqliteStore) -> None:
"""An empty prefix constrains nothing and must return every namespace."""
for namespace in [("a",), ("b", "c"), ("d", "e", "f")]:
store.put(namespace, "k", {"v": 1})
assert {item.namespace for item in store.search((), limit=100)} == {
("a",),
("b", "c"),
("d", "e", "f"),
}
def test_namespace_labels_with_trailing_newline(store: SqliteStore) -> None:
"""Labels may contain newlines, and must not match a differently-named label.
Python's `$` also matches just before a trailing newline, so the patterns use
`\\Z` to anchor at the true end of the string.
"""
store.put(("users", "alice"), "k", {"v": 1})
store.put(("users", "alice\n"), "k", {"v": 2})
assert set(store.list_namespaces(suffix=["alice"], limit=100)) == {
("users", "alice"),
}
assert set(store.list_namespaces(prefix=["users", "alice"], limit=100)) == {
("users", "alice"),
}
def test_list_namespaces_prefix_uses_indexable_condition() -> None:
"""Plain prefixes must use the indexable condition, not the match function.
A user function is opaque to the query planner, so it scans every row and
calls back into Python for each one. Only suffix and wildcard paths, which
no SQLite operator can express, need it.
"""
store = BaseSqliteStore()
def where(match_type: str, path: tuple[str, ...]) -> str:
op = ListNamespacesOp(
match_conditions=(MatchCondition(match_type=match_type, path=path),),
max_depth=None,
limit=10,
offset=0,
)
query, _ = store._get_batch_list_namespaces_queries([(0, op)])[0]
return " ".join(query.split())
assert "GLOB" in where("prefix", ("uid", "users"))
assert NS_MATCH_FUNCTION not in where("prefix", ("uid", "users"))
# A label that merely contains "*" is not the wildcard.
assert "GLOB" in where("prefix", ("star*",))
# Wildcard and suffix cannot be expressed by GLOB, so they keep the function.
assert NS_MATCH_FUNCTION in where("prefix", ("uid", "*", "alice"))
assert NS_MATCH_FUNCTION in where("suffix", ("alice",))
def test_list_namespaces_metacharacter_labels(store: SqliteStore) -> None:
"""Metacharacters in labels are literal on both matching paths.
Plain prefixes take the `= OR GLOB` condition and wildcard/suffix paths take
the regex function, so escaping has to hold in two different syntaxes.
"""
pairs = [
("star*", "starX"),
("q?m", "qXm"),
("br[ack]et", "brXacXket"),
("user_1", "userX1"),
("a%b", "axxb"),
("plus+", "plusX"),
]
for label, decoy in pairs:
store.put((label,), "k", {"v": 1})
store.put((decoy,), "k", {"v": 1})
store.put((label, "child"), "k", {"v": 1})
for label, decoy in pairs:
found = set(store.list_namespaces(prefix=[label], limit=100))
assert found == {(label,), (label, "child")}
# The decoy differs only where the metacharacter would have matched.
assert (decoy,) not in found
assert set(store.list_namespaces(prefix=[label, "child"], limit=100)) == {
(label, "child"),
}
+53 -84
View File
@@ -147,11 +147,11 @@ wheels = [
[[package]]
name = "codespell"
version = "2.4.3"
version = "2.4.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/80/19/45e941380f69c042b43423513d201e6592346f992394347f5e7174c31407/codespell-2.4.3.tar.gz", hash = "sha256:cbe085e331227b37bb86ef8bddd08dc768c704ee9a07ca869852c093fa2793e2", size = 352773, upload-time = "2026-07-15T11:51:54.159Z" }
sdist = { url = "https://files.pythonhosted.org/packages/2d/9d/1d0903dff693160f893ca6abcabad545088e7a2ee0a6deae7c24e958be69/codespell-2.4.2.tar.gz", hash = "sha256:3c33be9ae34543807f088aeb4832dfad8cb2dae38da61cac0a7045dd376cfdf3", size = 352058, upload-time = "2026-03-05T18:10:42.936Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8b/bf/bdb951d34eb169140b546f44be9ec4525d1acefb9eb5572071f5492b19fc/codespell-2.4.3-py3-none-any.whl", hash = "sha256:af2505b335e8573dbd2d384d1c4ef498f4006f4ba2d6fceca01e55b91f52628a", size = 340736, upload-time = "2026-07-15T11:51:52.925Z" },
{ url = "https://files.pythonhosted.org/packages/42/a1/52fa05533e95fe45bcc09bcf8a503874b1c08f221a4e35608017e0938f55/codespell-2.4.2-py3-none-any.whl", hash = "sha256:97e0c1060cf46bd1d5db89a936c98db8c2b804e1fdd4b5c645e82a1ec6b1f886", size = 353715, upload-time = "2026-03-05T18:10:41.398Z" },
]
[[package]]
@@ -168,7 +168,7 @@ name = "exceptiongroup"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
wheels = [
@@ -253,7 +253,7 @@ wheels = [
[[package]]
name = "langchain-core"
version = "1.5.2"
version = "1.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jsonpatch" },
@@ -266,26 +266,26 @@ dependencies = [
{ name = "typing-extensions" },
{ name = "uuid-utils" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6e/58/3ad53096eee1e07728e8219ded30ae308b0f2b8b7b26b8ebb6371917c0f5/langchain_core-1.5.2.tar.gz", hash = "sha256:2d13ab35b42eec63d4669a483776b8cdd778ee764107149369fb369d84c08c41", size = 972322, upload-time = "2026-07-28T16:38:37.977Z" }
sdist = { url = "https://files.pythonhosted.org/packages/59/de/679a53472c25860837e32c0442c962fa86e95317a36460e2c9d5c91b17c2/langchain_core-1.4.0.tar.gz", hash = "sha256:1dc341eed802ed9c117c0df3923c991e5e9e226571e5725c194eeb5bd93d1a7f", size = 920260, upload-time = "2026-05-11T18:42:35.919Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ca/e4/024e402a65f5ee2eb6ae9667b5ffc5f08776cb4e712f5a11ee1997d56083/langchain_core-1.5.2-py3-none-any.whl", hash = "sha256:a687dd7c3b22c6c1294e1c1eeb61fb6f3a308e6015a1d75b576b3836ad5b5aed", size = 561643, upload-time = "2026-07-28T16:38:36.38Z" },
{ url = "https://files.pythonhosted.org/packages/0f/1a/86c38c27b81913a1c6c12448cab55defb5a1097c7dc9a4cea83f55477a2d/langchain_core-1.4.0-py3-none-any.whl", hash = "sha256:23cbbdb46e38ddd1dd5247e6167e96013eae74bea4c5949c550809970a9e565c", size = 548120, upload-time = "2026-05-11T18:42:33.992Z" },
]
[[package]]
name = "langchain-protocol"
version = "0.0.18"
version = "0.0.15"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d2/59/b5959aea96faa9146e2e49a7a22882b3528c62efafe9a6a95beab30c2305/langchain_protocol-0.0.18.tar.gz", hash = "sha256:ec3e11782f1ed0c9db38e5a9ed01b0e7a0d3fba406faa8aef6594b73c56a63e6", size = 6150, upload-time = "2026-06-18T17:08:26.959Z" }
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" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/99/2e/d82db9eec13ad0f72e7aaad5c4bc730ab111934fdc83c85523206eb9b0a0/langchain_protocol-0.0.18-py3-none-any.whl", hash = "sha256:70b53a86fbf9cedc863555effe44da192ab02d556ddbf2cf95b8873adcf41b5a", size = 7221, upload-time = "2026-06-18T17:08:25.996Z" },
{ 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" },
]
[[package]]
name = "langgraph-checkpoint"
version = "4.2.0"
version = "4.1.1"
source = { editable = "../checkpoint" }
dependencies = [
{ name = "langchain-core" },
@@ -331,36 +331,9 @@ test = [
{ name = "redis" },
]
[[package]]
name = "langgraph-checkpoint-conformance"
version = "0.0.2"
source = { editable = "../checkpoint-conformance" }
dependencies = [
{ name = "langgraph-checkpoint" },
]
[package.metadata]
requires-dist = [{ name = "langgraph-checkpoint", editable = "../checkpoint" }]
[package.metadata.requires-dev]
dev = [
{ name = "pytest" },
{ name = "pytest-asyncio" },
{ name = "ruff" },
{ name = "ty" },
]
lint = [
{ name = "ruff" },
{ name = "ty" },
]
test = [
{ name = "pytest" },
{ name = "pytest-asyncio" },
]
[[package]]
name = "langgraph-checkpoint-sqlite"
version = "3.1.1"
version = "3.1.0"
source = { editable = "." }
dependencies = [
{ name = "aiosqlite" },
@@ -372,7 +345,6 @@ dependencies = [
dev = [
{ name = "codespell" },
{ name = "langgraph-checkpoint" },
{ name = "langgraph-checkpoint-conformance" },
{ name = "pytest" },
{ name = "pytest-asyncio" },
{ name = "pytest-mock" },
@@ -388,7 +360,6 @@ lint = [
]
test = [
{ name = "langgraph-checkpoint" },
{ name = "langgraph-checkpoint-conformance" },
{ name = "pytest" },
{ name = "pytest-asyncio" },
{ name = "pytest-mock" },
@@ -407,7 +378,6 @@ requires-dist = [
dev = [
{ name = "codespell" },
{ name = "langgraph-checkpoint", editable = "../checkpoint" },
{ name = "langgraph-checkpoint-conformance", editable = "../checkpoint-conformance" },
{ name = "pytest" },
{ name = "pytest-asyncio" },
{ name = "pytest-mock" },
@@ -423,7 +393,6 @@ lint = [
]
test = [
{ name = "langgraph-checkpoint", editable = "../checkpoint" },
{ name = "langgraph-checkpoint-conformance", editable = "../checkpoint-conformance" },
{ name = "pytest" },
{ name = "pytest-asyncio" },
{ name = "pytest-mock" },
@@ -751,7 +720,7 @@ wheels = [
[[package]]
name = "pytest"
version = "9.1.1"
version = "9.0.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
@@ -762,9 +731,9 @@ dependencies = [
{ name = "pygments" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
]
[[package]]
@@ -911,27 +880,27 @@ wheels = [
[[package]]
name = "ruff"
version = "0.16.0"
version = "0.15.15"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4d/94/1e5e4967626faf12fa56999cd6222dff6992ceb086ad7945756baf70c7a7/ruff-0.16.0.tar.gz", hash = "sha256:e460aafd5495ec89efaa6ced2e4a9a581116451e1c88b9d37ef497e0f8e93982", size = 4790557, upload-time = "2026-07-23T19:11:30.981Z" }
sdist = { url = "https://files.pythonhosted.org/packages/84/6f/a76f7d96e5c962f5b69cee865e49c15c1116897c01990faa8a57edb62e7f/ruff-0.15.15.tar.gz", hash = "sha256:b8dff018130b46d8e5bf0f926ef6b60cf871d6d5ae45fc9334e09632daa741d6", size = 4706985, upload-time = "2026-05-28T14:16:57.784Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4b/81/1c8818fee7ce1a04cd7d1b3172e0a8f8e4f1dc4feb7fc390e16daa8af323/ruff-0.16.0-py3-none-linux_armv6l.whl", hash = "sha256:e5115729eb08c585e5121978ba5d5b60caeae394ce21b9fb5e6cd33a1c6c9b1e", size = 10754633, upload-time = "2026-07-23T19:10:46.415Z" },
{ url = "https://files.pythonhosted.org/packages/23/df/beaf59c09d68db84304d555f188b276a77132a5d5b0b67a5c762aa143628/ruff-0.16.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3c954b1d580bfa035b41654f7858cc7e71d5fc3ac5b723dd62bd9133830ed522", size = 10969164, upload-time = "2026-07-23T19:10:50.271Z" },
{ url = "https://files.pythonhosted.org/packages/42/ce/741cd197496a1abbf51352710fd15ed995d2a2be87189c1da26a450d6e83/ruff-0.16.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e01c21d10eb1b29f47b7454e1f4056db9a3f0260c646aa88457c610291db9f81", size = 10488846, upload-time = "2026-07-23T19:10:52.639Z" },
{ url = "https://files.pythonhosted.org/packages/52/2a/a2db8e88cade358f5cdcb05674a917751074109315d014eb6352d9a893f7/ruff-0.16.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e364e5ed22ed8dc05082fd78e35308618260907ac2d3c1d637b2e682415b6c9", size = 10889729, upload-time = "2026-07-23T19:10:54.89Z" },
{ url = "https://files.pythonhosted.org/packages/42/65/62a771694ebd63029dc953e27dbad40e1588bd4860ff9fe881018fddaa49/ruff-0.16.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d327b8fc113a1d4421a04f3839d3752057c8dd1ee320223a6f3f52d04ada462a", size = 10568275, upload-time = "2026-07-23T19:10:56.993Z" },
{ url = "https://files.pythonhosted.org/packages/3f/e2/ced249fe8af5f086c5c58cc21cc3356d50f32f7401c5df87050c999620a7/ruff-0.16.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b50c55e263103586b3dcf5f73d479eb8cb5fdb6098fec59a62891dab653717", size = 11385112, upload-time = "2026-07-23T19:10:59.615Z" },
{ url = "https://files.pythonhosted.org/packages/87/0b/05154977a8fd69eeb6c103271f55403bfd8711f5c0f8ed07489d95a504e7/ruff-0.16.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ff4a79ce3ec0172f3241943835de1c4cb4e2dcd07f0f8c2d02603dbbbee4b17", size = 12207008, upload-time = "2026-07-23T19:11:02.154Z" },
{ url = "https://files.pythonhosted.org/packages/fb/29/98225831a3a1eab0e02f4acc6ca6559a98611dcc68b6965ff4b7234627c1/ruff-0.16.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e95c448fca1fb2a18372a9440926c5a6ee789639bb975c72e7ae6d0b04218ab4", size = 11650842, upload-time = "2026-07-23T19:11:04.557Z" },
{ url = "https://files.pythonhosted.org/packages/91/66/6bd3cf90500653d55dc0ffc8507aa8300bd49d0214b2e8cb4d3fef2943ba/ruff-0.16.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f11a8d11010301d0a398a2fdef67691feca7294da6aef55e2150e8fa2cd520b", size = 11400718, upload-time = "2026-07-23T19:11:09.233Z" },
{ url = "https://files.pythonhosted.org/packages/8e/a2/a54eb4eae05d66364050a5d3b8a9c5ef88196531b3cbe7109d873f87f819/ruff-0.16.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:48044c678e9cb8698246c99b14aaccfa6601dea7379eb48a6f8f73f7a6d86cd0", size = 11426177, upload-time = "2026-07-23T19:11:11.994Z" },
{ url = "https://files.pythonhosted.org/packages/1a/be/16e3eea4b2a478a496919f5e36f17c4559e54620bd3bbac5d6affa068006/ruff-0.16.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7aa0959bad8eb8bef50340154fc9b58678dae31fa4293afa38b44b6e552c0213", size = 10856126, upload-time = "2026-07-23T19:11:14.221Z" },
{ url = "https://files.pythonhosted.org/packages/a2/84/252eb8b868a16eec7257c14f504f77537e734b2d69c762e639e588e304a3/ruff-0.16.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:28ea2b7df8ebf7f9da6b7d47b230ab48f387c0a29be3b474c4d0740e197bb9af", size = 10571208, upload-time = "2026-07-23T19:11:16.378Z" },
{ url = "https://files.pythonhosted.org/packages/21/09/817a482f542f7570cbb4554b26e896610c7114f539b1d9e2d2145bf6bef6/ruff-0.16.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:33a3dfac8c35f81498dea9181bccc2f4c4bc8f1521a1dd9406e77643e0f0fb09", size = 11063329, upload-time = "2026-07-23T19:11:19.173Z" },
{ url = "https://files.pythonhosted.org/packages/2e/23/9403c180ca1cb9b1f7335f5c3e5305c09d49ea5b345196682a36028bde4a/ruff-0.16.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a5237a0bda500d30d81b8e07a6973a5cbc772864cbf746ae2f4e8a2e01c9f4ed", size = 11489751, upload-time = "2026-07-23T19:11:21.74Z" },
{ url = "https://files.pythonhosted.org/packages/b2/1d/1b2ef7bcde851c78d7f17f1cca13fd6dc695fc4b3d6197941e72cae5b132/ruff-0.16.0-py3-none-win32.whl", hash = "sha256:7fab76fa065c873f41ff744347c6e77bcc3dfec4bcc754dc26b63d23c0f7f5fb", size = 10785885, upload-time = "2026-07-23T19:11:23.947Z" },
{ url = "https://files.pythonhosted.org/packages/b2/a3/d5e4ef7a56be3f928ffb90b94c25ba7d3cb9c7fe0736aeaaedf361770712/ruff-0.16.0-py3-none-win_amd64.whl", hash = "sha256:429c117f022bf481fabd9d551e7a3952b24c65e6ef44337ea09d90bebef14472", size = 11923141, upload-time = "2026-07-23T19:11:26.409Z" },
{ url = "https://files.pythonhosted.org/packages/cb/9a/8415f2657cbe200f41a4531ccededf135505a92d4a012229121f885b26f9/ruff-0.16.0-py3-none-win_arm64.whl", hash = "sha256:14296fedcd2705c77ab8235439278bbb38f285cf7da5528b00b3e330c3d4872d", size = 11273407, upload-time = "2026-07-23T19:11:28.705Z" },
{ url = "https://files.pythonhosted.org/packages/fa/9d/3a45c05b8ab04b4705989de70a79008e27c8003296a0feaee9edc18dd7e9/ruff-0.15.15-py3-none-linux_armv6l.whl", hash = "sha256:cf93e5388f412e1b108b1f8b34a6e036b70fe8aff89393befad96fe48670311b", size = 10710652, upload-time = "2026-05-28T14:16:06.701Z" },
{ url = "https://files.pythonhosted.org/packages/05/66/da974431624bf3b49f6ee1f9543c02d929ff1cba78b0d5a79c38cf21f744/ruff-0.15.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac5a646d1f6a7dadd5d50842dae2c1f9862ac887ef5d1b1375e02def791fde6e", size = 11096615, upload-time = "2026-05-28T14:16:23.313Z" },
{ url = "https://files.pythonhosted.org/packages/8c/09/7443452e5d290230a712103f2fdceeef7184f3ec99a2bd01c8be78aaceb5/ruff-0.15.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:77d955a431430c66f72dd94e379ad38a16daea3d25094872ac4edf9e797be530", size = 10436683, upload-time = "2026-05-28T14:16:40.974Z" },
{ url = "https://files.pythonhosted.org/packages/53/01/d330c26a57fa4f3943a14424904027428315b700fe4d14a84bb123a649e5/ruff-0.15.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7614ee79c69788cf6cedd568069ade9cecc22a1ad20494efe8d0c9ebb4b622d4", size = 10769064, upload-time = "2026-05-28T14:16:28.905Z" },
{ url = "https://files.pythonhosted.org/packages/1d/85/cc8770f8bdff541b1da8392d1634141fe4a0e3f4ee596605959b7906c27f/ruff-0.15.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3cdb1679e06a1f6b47bc384714ae96f6e2fb65ca441eb78c43d2ca554176ce1f", size = 10511987, upload-time = "2026-05-28T14:16:43.732Z" },
{ url = "https://files.pythonhosted.org/packages/7c/29/8c190c1472b63013583ba391f3342036e02010544c1270455ed8e519bdf3/ruff-0.15.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2728b93d7b23a603ea2c0ac6eb73d760bd38ec9de35f35fb41e18f7a3fee7622", size = 11275100, upload-time = "2026-05-28T14:16:55.244Z" },
{ url = "https://files.pythonhosted.org/packages/9f/6b/7e145ce2cc8e63d6834eca03d83a0e18d121def5c69f91b4cf4011ed4879/ruff-0.15.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be582fcc0db438902c7792b08d6ddf6c9b9e21addaa10092c2c741cfb09e5a45", size = 12176903, upload-time = "2026-05-28T14:16:14.368Z" },
{ url = "https://files.pythonhosted.org/packages/80/a3/d5974637f68e451f7fadf015cf3101d1cd7d8ba5027cffe0b9e3826ebe6b/ruff-0.15.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7aa77465b8ecaf1a27bea098d696f7fed5e1eccbd10b321b682d6de586ae5627", size = 11404550, upload-time = "2026-05-28T14:16:20.138Z" },
{ url = "https://files.pythonhosted.org/packages/fe/1c/e6e5e568f22be4fb05d6244234aba384c06b451252453b821e1a529263cf/ruff-0.15.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48decfa11d740de4889de623be1463308346312f2409a56e24aa280c86162dc4", size = 11382027, upload-time = "2026-05-28T14:16:46.615Z" },
{ url = "https://files.pythonhosted.org/packages/1d/01/170921b49fcd2e8858825593f91cf7146c3e40a5c3e6df763e4bb0484dde/ruff-0.15.15-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a5015088452ca0081387063649ec67f06d3d1d6b8b936a1f836b5e9657ecd48c", size = 11366041, upload-time = "2026-05-28T14:16:26.247Z" },
{ url = "https://files.pythonhosted.org/packages/87/54/a7bad711d7de93254e15e06a4c375b89a03d18de45d3e5dcc86a4472fb1a/ruff-0.15.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5294aab6356c81600fcdea3a62bb1b924dfd5e91767c12318d3f68f86af57cd", size = 10741795, upload-time = "2026-05-28T14:16:17.11Z" },
{ url = "https://files.pythonhosted.org/packages/c9/31/38c075963668f8b41c6914ee0f6f318727fbe30ab9145cb29e6df464c5fa/ruff-0.15.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:db5bd4d802415cca656dc1616070b725952d6ae95eb5d4831e49fbd94a38f75f", size = 10511117, upload-time = "2026-05-28T14:16:31.767Z" },
{ url = "https://files.pythonhosted.org/packages/9d/96/6ff689e1f7e375d1d97075eca022f74c2bab59554a432fe4d2e6f091986a/ruff-0.15.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:587a6278ed42059191c1a466e490bd7930fb50bd2e255398bc29616c895a61cb", size = 10994867, upload-time = "2026-05-28T14:16:35.149Z" },
{ url = "https://files.pythonhosted.org/packages/c3/c2/5dce0ab9f92a8d534fa62b9bf9caca3eddb8c1a81b616f5e195ada4f0d6e/ruff-0.15.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:df0c1c084f5f4be9812f61518a45c440d3c30d69ce4bf6c5270e66d38338f02a", size = 11482101, upload-time = "2026-05-28T14:16:49.598Z" },
{ url = "https://files.pythonhosted.org/packages/b1/c0/1003b60edd697c649faf61f1a34094b1abb38fb3d1181e3f895781250a08/ruff-0.15.15-py3-none-win32.whl", hash = "sha256:29428ea79694afbe756d45fd59b36f22b6b020dc0443cf7de0173046236964b9", size = 10716774, upload-time = "2026-05-28T14:16:52.337Z" },
{ url = "https://files.pythonhosted.org/packages/02/a8/1269eddd6945a06c23f055ef7848886e37cf9d6a8bebb386a3115f01470c/ruff-0.15.15-py3-none-win_amd64.whl", hash = "sha256:8df0323902e15e24bc4bf246da830573d3cf3352bd0b9a164eab335d111ff4a4", size = 11868463, upload-time = "2026-05-28T14:16:11.333Z" },
{ url = "https://files.pythonhosted.org/packages/4e/b2/920464c907b191e37469d477a1aa8bc048b8f36c4c1610dfa4ab87b39e18/ruff-0.15.15-py3-none-win_arm64.whl", hash = "sha256:3c8ceca6792f38196b8f589bc92eccd03eef286602da92e5dc05cc42ef6441b7", size = 11138498, upload-time = "2026-05-28T14:16:38.425Z" },
]
[[package]]
@@ -1011,27 +980,27 @@ wheels = [
[[package]]
name = "ty"
version = "0.0.64"
version = "0.0.43"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8e/aa/14c9965d3b173105692473897cc89c34cd91241368b2044e43167e1c17ff/ty-0.0.64.tar.gz", hash = "sha256:d12ddbb05f15158bb518af619378b385486450def95fb06f8ab98037febe9f2c", size = 6350966, upload-time = "2026-07-27T18:32:45.403Z" }
sdist = { url = "https://files.pythonhosted.org/packages/0d/37/4ec04de0659b93be37d956dfceca13b1ecab9c959f28d8a1d5e514603f36/ty-0.0.43.tar.gz", hash = "sha256:ea4cff50548f2a1877e848d3abe9e293cde8ab94757a7eb93fc0d4013f98be8e", size = 5798429, upload-time = "2026-06-04T00:52:10.013Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ae/4c/c54937e4ff3fa7b34a99ea3387ec766bf0ad98dc8df8d792e89b388e658e/ty-0.0.64-py3-none-linux_armv6l.whl", hash = "sha256:3830a6675ab43635ced1c4c557f380ac4a49e9414e03975e4e4e8db644c64944", size = 12118357, upload-time = "2026-07-27T18:32:08.702Z" },
{ url = "https://files.pythonhosted.org/packages/ce/aa/a839ee2bc78e943d079e6abe199a97b4eeffb7e5c9a57326d69de452186a/ty-0.0.64-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3ff07d7bc32a2135f58afe57393789a32ea2fed1a66216129a8e535e76043903", size = 11790882, upload-time = "2026-07-27T18:32:10.997Z" },
{ url = "https://files.pythonhosted.org/packages/4c/de/19f14357888a7198438926303753cf749428e3d62e8980ff1e9a72a78402/ty-0.0.64-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4f6d1c7f897cca05d12bacbf1435150d5ffa496099515aa6ed303c8b29e1d0bb", size = 11317394, upload-time = "2026-07-27T18:32:13.162Z" },
{ url = "https://files.pythonhosted.org/packages/08/2f/f54462300535ab99b551eda733177be2eef5dbc2997d3fdb357c4ddd760a/ty-0.0.64-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:138d6c37ad4bf8583aa7a9b29d90954151d7856f9910a03eae3eb34b34c57215", size = 11863042, upload-time = "2026-07-27T18:32:15.307Z" },
{ url = "https://files.pythonhosted.org/packages/5e/95/dbecf745520ebe8bd7b02fc55eee6441c9be312ebf6addce605eb52740dd/ty-0.0.64-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ed9719d1b7b66fb8efe073d860208a44c40af1f6cd5c2364aa9b323a1e579b4", size = 11910730, upload-time = "2026-07-27T18:32:17.467Z" },
{ url = "https://files.pythonhosted.org/packages/3b/26/12cfd40028e51ceed7b3cb645281c61c02eb64ff9fb0c09231d65c30ff25/ty-0.0.64-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d68b23e5169e2137b5f1de7169ab0cebecab6c8eda374c34c1f6394308f58242", size = 12631936, upload-time = "2026-07-27T18:32:19.533Z" },
{ url = "https://files.pythonhosted.org/packages/a0/d0/65ffc2b0a686347193c6f98e9421a7fc2a96fc3cd0b1001cf7cf284baff9/ty-0.0.64-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f41cb07d89d32626fcaf3ed4d262778fcb28b2628b6ed1e172cd7b18820668d8", size = 13171049, upload-time = "2026-07-27T18:32:22.026Z" },
{ url = "https://files.pythonhosted.org/packages/f6/a4/975a5961842dcd6fa60f0770a102c0bba7509da909ab652919bfcdcd4fc7/ty-0.0.64-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5f24e1504ab9e212f92356b82fe088fdeb3a39f9a2f4ff25e505d2e1d0db9056", size = 12826438, upload-time = "2026-07-27T18:32:24.178Z" },
{ url = "https://files.pythonhosted.org/packages/af/ef/dfb9b7f9bcc032d3b540b0d1f55f532a336e2fb41b1bd539c05ae81a151a/ty-0.0.64-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86db830cb914bb33bb8247b66ccea58de4496c2391bd42658aba744b439f3290", size = 12440880, upload-time = "2026-07-27T18:32:26.341Z" },
{ url = "https://files.pythonhosted.org/packages/ee/d0/bedac20505e8a8f5501ad73d7d15d8e421a563fef59993909a23036929a4/ty-0.0.64-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:652ee3d6d03bea76cd2fe8949c78bb5970394ebd7c5fd270e9518c0dee1b931d", size = 12782439, upload-time = "2026-07-27T18:32:28.642Z" },
{ url = "https://files.pythonhosted.org/packages/79/d5/795733f13ceff1378f08b3de0c49d0f518df220ed856b0dfac869f3b7c81/ty-0.0.64-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4838768295774a86e95f9ec5633e739d016adbbb69dcbdc62f3549578a11f624", size = 11814821, upload-time = "2026-07-27T18:32:30.632Z" },
{ url = "https://files.pythonhosted.org/packages/52/3e/9d99cd1e1831003434f508ed9f258a56543194afc3bbe051eba2545fa676/ty-0.0.64-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5a3d700669868599edf39ce5125196682f15a8880cc8c669bc2a83b99984d99b", size = 11928678, upload-time = "2026-07-27T18:32:32.888Z" },
{ url = "https://files.pythonhosted.org/packages/b8/3d/448f49a3503fb119a34348a5714bb92001f252fadbbb12d645f2e744b557/ty-0.0.64-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b161f0a82a8e2f2432db3bf7702b4d3924fa9486ba0014f6710a160fc157df0d", size = 12202249, upload-time = "2026-07-27T18:32:34.905Z" },
{ url = "https://files.pythonhosted.org/packages/dd/9b/75768e562cec990d189dc05807ae72890b20ffbd1e1f43597bb98db63c60/ty-0.0.64-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:39b9dd42908df47c2dc57dda87e656fab97097ffd2618474bbdea986af0d6a9d", size = 12548817, upload-time = "2026-07-27T18:32:36.995Z" },
{ url = "https://files.pythonhosted.org/packages/34/89/44cc276ea6ca0245495014758ffeadde1798fb0b5840c9cf36d8d2ed3250/ty-0.0.64-py3-none-win32.whl", hash = "sha256:d0676ab0e0935795e5843baa28dd5e366dc343d7c0945a996df9eab8e0644885", size = 11545474, upload-time = "2026-07-27T18:32:39.389Z" },
{ url = "https://files.pythonhosted.org/packages/01/7e/d1c8a871a38d17c8f168b9a6975f6247f7660f8334e517656a5e4b4a4858/ty-0.0.64-py3-none-win_amd64.whl", hash = "sha256:dcb9bd31f54097e362b776c26ab4564d4564cdd1355cb883481167a26c03cc3f", size = 12542987, upload-time = "2026-07-27T18:32:41.412Z" },
{ url = "https://files.pythonhosted.org/packages/35/4d/6d18640d0204cacd69abbaca95ad6a34c6d7e9169e9051d0117b17b827ec/ty-0.0.64-py3-none-win_arm64.whl", hash = "sha256:82cc34c1ad9a8feb6059aef193bebcecc656e548f6fab3d518bcd8b57d198d39", size = 11899263, upload-time = "2026-07-27T18:32:43.366Z" },
{ url = "https://files.pythonhosted.org/packages/db/74/1916026a78f20019a2f03adbd6fb4430ddb7ce1e52c2e17a90856a6d192e/ty-0.0.43-py3-none-linux_armv6l.whl", hash = "sha256:3bf70f5446480562bf6c9f639df4b5cb60716b8f8d1a6b8e5811d5c7eccd8bf2", size = 11598153, upload-time = "2026-06-04T00:52:20.646Z" },
{ url = "https://files.pythonhosted.org/packages/b9/af/58bb0089d2635216c8fa6612dd486a3f986d0ab1c46a41527ab95e57f0e3/ty-0.0.43-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7184741f8b15425a1bc64b950ad005cb353573288ac0e8a04f5481ceb3832596", size = 11357811, upload-time = "2026-06-04T00:52:24.683Z" },
{ url = "https://files.pythonhosted.org/packages/d6/9c/32c6b14f3feddf87b59c7a50709e2b3da408258f2f583f05575f77bc8f7b/ty-0.0.43-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8c306379ca9a35f6ae5270fe9bda7af4b46d91822725a2586d78c8b9b5493b62", size = 10772024, upload-time = "2026-06-04T00:52:14.312Z" },
{ url = "https://files.pythonhosted.org/packages/09/fa/98aa4a74bd00cd5efc424923cd1daffbf1e40a0338041cafb203379d746f/ty-0.0.43-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d624b884c9c1fd244ad2a5f026364e7162a22b3f537025941ada2e363e676414", size = 11291034, upload-time = "2026-06-04T00:52:37.249Z" },
{ url = "https://files.pythonhosted.org/packages/b5/db/4de086c38ce96dcada2bd451f43171d2c237f96d8ed19a1ea8fe51bb8ef4/ty-0.0.43-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:281fc4c00fbc196045141faa085055bddc58846b04a2800204701415a1b9c6aa", size = 11364724, upload-time = "2026-06-04T00:52:33.138Z" },
{ url = "https://files.pythonhosted.org/packages/b0/d3/e3cd8e3233a6fd8362a49aa025b79e9f40151a2a86d811ace154c6eb7445/ty-0.0.43-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f57d6cc28de89024b48d1788e4758c05299d5749d4a51c02e71ac655ec23d9a5", size = 11890555, upload-time = "2026-06-04T00:52:22.711Z" },
{ url = "https://files.pythonhosted.org/packages/80/7b/6f46d444e8241606bbde098df3dca93f2ec0b834a42055db85ee7d33646f/ty-0.0.43-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0a1d6ad6c5e7792c7eac0a01e550f2c2004462e01a64a91ea1636aba6fef6e71", size = 12450968, upload-time = "2026-06-04T00:52:28.94Z" },
{ url = "https://files.pythonhosted.org/packages/4a/e1/79fbe51f2e4b9d8347f2013cd7ed0b63f3b499038c02dc0357e9b28a3a47/ty-0.0.43-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:66d474395d7635fb618bdbb58b4e3360259a2056d0a5621b82754b9da2cd8a04", size = 12064187, upload-time = "2026-06-04T00:52:12.039Z" },
{ url = "https://files.pythonhosted.org/packages/9b/3f/c758a3a8df5b90d331f2b60c8f16021ee64d75e78f99d67cc4efc9bf5f4b/ty-0.0.43-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2663a0003a8b60fb98db7f6f6e673df80b21d0fe3a9868a26fb06b4e049b6fc4", size = 11943208, upload-time = "2026-06-04T00:52:31.14Z" },
{ url = "https://files.pythonhosted.org/packages/54/5f/f516442749cf1b45ca6720a5d41df2738a486ed9ace774c03d515db89084/ty-0.0.43-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:d5a6c352d374d889189d5ec82b54b26a5885f769f7b7787f7f875500dcb8673e", size = 12143572, upload-time = "2026-06-04T00:52:18.457Z" },
{ url = "https://files.pythonhosted.org/packages/b7/bf/0d83c7f43bf4c10f3678bfe7d938e51c445298c7b923f155c5204730c2df/ty-0.0.43-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e7dbbeedfad3ca250d74fcc355fa9ab6b38d2a17f22d6304f615716939dbbb27", size = 11279355, upload-time = "2026-06-04T00:52:26.726Z" },
{ url = "https://files.pythonhosted.org/packages/3e/de/a6c978bef6d9e949f79f4782d9e4ee4df0893713e73b055d84c1a5116b9a/ty-0.0.43-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:24b18a0273ee46154996cfcfa27438f851f440c925587ec200df6f98dffe67d3", size = 11408412, upload-time = "2026-06-04T00:52:35.282Z" },
{ url = "https://files.pythonhosted.org/packages/ec/b1/d13857c23867f0f76b92e38e5841c64ca5e76dc5d4bf27f52cb81d8ab685/ty-0.0.43-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2ef681951520d692b7e9c0b5e56aacf4f98ccae47cf6ffccaf2c7b6b33dc226e", size = 11541709, upload-time = "2026-06-04T00:52:16.451Z" },
{ url = "https://files.pythonhosted.org/packages/7c/f1/cd6afc6f6a687e238bf5e12189f7920e81a0bdef6c3dba4c784ef140f7d9/ty-0.0.43-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2af105de7437143aa4676b28016b5bee661aaaa4eff52be5867fb25119641ceb", size = 12041266, upload-time = "2026-06-04T00:52:43.541Z" },
{ url = "https://files.pythonhosted.org/packages/bd/ba/51ca7c3335da2b8d0a3e477fa4986be9f4a53b05bfab862967d8d2e6ca60/ty-0.0.43-py3-none-win32.whl", hash = "sha256:e4773115b0d6486ee30f1657fc8bdffe7e3a3f5300ab77ef2495da6e83e4694f", size = 10858724, upload-time = "2026-06-04T00:52:07.843Z" },
{ url = "https://files.pythonhosted.org/packages/9f/29/5d80453e5f7c520145fa058851da87230dbd7ca761a7675447a9fe504e0b/ty-0.0.43-py3-none-win_amd64.whl", hash = "sha256:48d3545094a4ae6395492c7e6ac90550fce969e0ed2815fbf8c5da9756676b7d", size = 11976157, upload-time = "2026-06-04T00:52:41.438Z" },
{ url = "https://files.pythonhosted.org/packages/dc/ed/befe5a543e5b95e754ed38ee95239e44efda9bc5f578db4ac1bc8dd758d6/ty-0.0.43-py3-none-win_arm64.whl", hash = "sha256:740ca33d7f75f655a4e7d475bc42dfb825c13219bb073fad30fcc04d35790c74", size = 11308680, upload-time = "2026-06-04T00:52:39.233Z" },
]
[[package]]
@@ -34,6 +34,37 @@ PendingWrite = tuple[str, str, Any]
logger = logging.getLogger(__name__)
def _parse_supersteps_since_last_snapshot_by_channel(
metadata: Mapping[str, Any],
channels: Sequence[str],
) -> dict[str, int]:
"""Per-channel supersteps-since-last-snapshot, parsed from metadata.
Reads `metadata.counters_since_delta_snapshot[ch]`, a `(updates,
supersteps)` pair, and returns `{ch: supersteps}` for channels whose
supersteps count is positive. Channels with no counter entry (just
snapshotted, or never written) are omitted their seed, if any, is the
target checkpoint itself and needs no ancestor walk.
Used to drive the ancestor-walk depth in `get_delta_channel_history`:
a channel's seed snapshot sits exactly `supersteps` hops back along the
parent chain.
"""
counters = metadata.get("counters_since_delta_snapshot") or {}
result: dict[str, int] = {}
for ch in channels:
entry = counters.get(ch)
if not isinstance(entry, (list, tuple)) or len(entry) < 2:
continue
try:
supersteps = int(entry[1])
except (TypeError, ValueError):
continue
if supersteps > 0:
result[ch] = supersteps
return result
# Marked as total=False to allow for future expansion.
class CheckpointMetadata(TypedDict, total=False):
"""Metadata associated with a checkpoint."""
@@ -619,34 +650,30 @@ class BaseCheckpointSaver(Generic[V]):
"""
if not channels:
return {}
collected_by_ch: dict[str, list[PendingWrite]] = {c: [] for c in channels}
seed_by_ch: dict[str, Any] = {}
remaining: set[str] = set(channels)
channels = list(channels)
target_tuple = self.get_tuple(config)
cursor_config: RunnableConfig | None = (
target_tuple.parent_config if target_tuple else None
if target_tuple is None:
return {ch: {"writes": []} for ch in channels}
supersteps_by_ch = _parse_supersteps_since_last_snapshot_by_channel(
target_tuple.metadata or {}, channels
)
while cursor_config is not None and remaining:
max_supersteps = max(supersteps_by_ch.values(), default=0)
chain: list[CheckpointTuple] = []
has_reached_root = False
cursor_config: RunnableConfig | None = target_tuple.parent_config
while cursor_config is not None and len(chain) < max_supersteps:
tup = self.get_tuple(cursor_config)
if tup is None:
break
if tup.pending_writes:
for write in reversed(tup.pending_writes):
ch = write[1]
if ch in remaining:
collected_by_ch[ch].append(write)
for ch in list(remaining):
if ch in tup.checkpoint["channel_values"]:
seed_by_ch[ch] = tup.checkpoint["channel_values"][ch]
remaining.discard(ch)
chain.append(tup)
if tup.parent_config is None:
has_reached_root = True
break
cursor_config = tup.parent_config
result: dict[str, DeltaChannelHistory] = {}
for ch in channels:
entry: DeltaChannelHistory = {"writes": list(reversed(collected_by_ch[ch]))}
if ch in seed_by_ch:
entry["seed"] = seed_by_ch[ch]
result[ch] = entry
return result
return self._assemble_default_delta_history(
channels, supersteps_by_ch, chain, has_reached_root, config
)
async def aget_delta_channel_history(
self, *, config: RunnableConfig, channels: Sequence[str]
@@ -660,32 +687,106 @@ class BaseCheckpointSaver(Generic[V]):
"""
if not channels:
return {}
collected_by_ch: dict[str, list[PendingWrite]] = {c: [] for c in channels}
seed_by_ch: dict[str, Any] = {}
remaining: set[str] = set(channels)
channels = list(channels)
target_tuple = await self.aget_tuple(config)
cursor_config: RunnableConfig | None = (
target_tuple.parent_config if target_tuple else None
if target_tuple is None:
return {ch: {"writes": []} for ch in channels}
supersteps_by_ch = _parse_supersteps_since_last_snapshot_by_channel(
target_tuple.metadata or {}, channels
)
while cursor_config is not None and remaining:
max_supersteps = max(supersteps_by_ch.values(), default=0)
chain: list[CheckpointTuple] = []
has_reached_root = False
cursor_config: RunnableConfig | None = target_tuple.parent_config
while cursor_config is not None and len(chain) < max_supersteps:
tup = await self.aget_tuple(cursor_config)
if tup is None:
break
if tup.pending_writes:
for write in reversed(tup.pending_writes):
ch = write[1]
if ch in remaining:
collected_by_ch[ch].append(write)
for ch in list(remaining):
if ch in tup.checkpoint["channel_values"]:
seed_by_ch[ch] = tup.checkpoint["channel_values"][ch]
remaining.discard(ch)
chain.append(tup)
if tup.parent_config is None:
has_reached_root = True
break
cursor_config = tup.parent_config
return self._assemble_default_delta_history(
channels, supersteps_by_ch, chain, has_reached_root, config
)
def _assemble_default_delta_history(
self,
channels: Sequence[str],
supersteps_by_ch: Mapping[str, int],
chain: Sequence[CheckpointTuple],
has_reached_root: bool,
config: RunnableConfig,
) -> dict[str, DeltaChannelHistory]:
"""Slice the walked parent chain into per-channel histories.
For each channel the seed snapshot sits `supersteps` hops back along
`chain` (newest first): the seed checkpoint is `chain[supersteps - 1]`
and its `channel_values[ch]` is the seed value. The seed checkpoint's
own writes are replayed on top of a `_DeltaSnapshot` seed but skipped
for a migrated plain-value seed (the legacy non-delta blob already
incorporates them).
When the chain is shorter than `supersteps` but the root was reached,
the persisted chain is "compressed" relative to the logical superstep
count (`durability="exit"`, or a thread that never snapshotted). The
seed candidate is then the oldest checkpoint (`chain[-1]`); if its
`channel_values[ch]` is absent the channel has no seed and replays
the full chain on an empty baseline.
"""
# Imported lazily to avoid a hard checkpoint→serde-types coupling at
# module import; only the delta surface needs the runtime check.
from langgraph.checkpoint.serde.types import _DeltaSnapshot
thread_id = config["configurable"].get("thread_id")
result: dict[str, DeltaChannelHistory] = {}
for ch in channels:
entry: DeltaChannelHistory = {"writes": list(reversed(collected_by_ch[ch]))}
if ch in seed_by_ch:
entry["seed"] = seed_by_ch[ch]
entry: DeltaChannelHistory = {"writes": []}
bound = supersteps_by_ch.get(ch, 0)
if bound <= 0:
result[ch] = entry
continue
if len(chain) >= bound:
seed_depth = bound
elif has_reached_root:
seed_depth = len(chain)
else:
logger.warning(
"cannot find seed snapshot for delta channel "
"(thread_id=%s, channel=%s)",
thread_id,
ch,
)
result[ch] = entry
continue
if seed_depth <= 0:
result[ch] = entry
continue
chain_slice: Sequence[CheckpointTuple] = chain[:seed_depth]
seed_tuple: CheckpointTuple | None = chain[seed_depth - 1]
skip_seed_checkpoint_writes = False
channel_values = seed_tuple.checkpoint["channel_values"]
if ch in channel_values:
seed_value = channel_values[ch]
entry["seed"] = seed_value
skip_seed_checkpoint_writes = not isinstance(seed_value, _DeltaSnapshot)
else:
# No stored value at the oldest checkpoint → empty baseline,
# so the chain's own writes are all replayed from empty.
seed_tuple = None
collected: list[PendingWrite] = []
# Chain is newest→oldest; replay oldest→newest.
for tup in reversed(list(chain_slice)):
if skip_seed_checkpoint_writes and tup is seed_tuple:
continue
for write in tup.pending_writes or []:
if write[1] == ch:
collected.append(write)
entry["writes"] = collected
result[ch] = entry
return result
@@ -23,6 +23,7 @@ from langgraph.checkpoint.base import (
DeltaChannelHistory,
PendingWrite,
SerializerProtocol,
_parse_supersteps_since_last_snapshot_by_channel,
get_checkpoint_id,
get_checkpoint_metadata,
)
@@ -144,81 +145,119 @@ class InMemorySaver(
) -> Mapping[str, DeltaChannelHistory]:
"""Override: walk the parent chain ONCE for all requested channels.
Each channel terminates independently at the nearest ancestor
whose stored blob is non-empty. Other channels keep walking until
they find their own terminator or hit the root.
Walk depth is driven by the target checkpoint's
`counters_since_delta_snapshot[ch]` supersteps counter: each
channel's seed snapshot sits exactly `supersteps` hops back along
the parent chain. This locates seeds reliably even when they aren't
a `_DeltaSnapshot` sentinel e.g. a legacy plain-value blob from a
thread migrated off a non-delta channel.
A blob is the value AT its ancestor, prior to the writes stored
under that same ancestor (those writes produce its child, which
is on the path to the target). This holds for `_DeltaSnapshot`
blobs and for pre-delta plain values alike, so the seed
ancestor's own writes are always collected. Writes at ancestors
older than the seed are subsumed by the seed value and are never
reached the walk terminates there.
Pre-delta plain-value seeds subsume their own checkpoint's pending
writes (the value already includes them), so those are skipped;
`_DeltaSnapshot` seeds do not (the snapshot is the value AT that
ancestor, prior to its own pending writes), so they are replayed.
When the chain reaches the root short of `supersteps`, the snapshot
was never persisted (implicit empty baseline) replay the full
chain with no seed.
"""
if not channels:
return {}
# Imported lazily to avoid a hard checkpoint→serde-types coupling at
# module import; only this override needs the runtime check.
from langgraph.checkpoint.serde.types import _DeltaSnapshot
channels = list(channels)
thread_id = config["configurable"]["thread_id"]
checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
checkpoint_id = config["configurable"].get("checkpoint_id", "")
ns_storage = self.storage.get(thread_id, {}).get(checkpoint_ns, {})
# Resolve the target checkpoint id + its metadata (for supersteps).
checkpoint_id = get_checkpoint_id(config)
if checkpoint_id is None:
checkpoint_id = next(reversed(ns_storage), None)
target_entry = ns_storage.get(checkpoint_id) if checkpoint_id else None
if target_entry is None:
return {ch: {"writes": []} for ch in channels}
metadata = self.serde.loads_typed(target_entry[1])
supersteps_by_ch = _parse_supersteps_since_last_snapshot_by_channel(
metadata or {}, channels
)
max_supersteps = max(supersteps_by_ch.values(), default=0)
# Walk the parent chain (newest first) up to the deepest supersteps.
chain: list[str] = []
target_entry = ns_storage.get(checkpoint_id)
current: str | None = target_entry[2] if target_entry is not None else None
while current is not None:
has_reached_root = False
current: str | None = target_entry[2]
while current is not None and len(chain) < max_supersteps:
entry = ns_storage.get(current)
if entry is None:
break
chain.append(current)
_, _, parent = entry
current = parent
collected_by_ch: dict[str, list[PendingWrite]] = {c: [] for c in channels}
seed_by_ch: dict[str, Any] = {}
remaining: set[str] = set(channels)
for cp_id in chain:
if not remaining:
parent = entry[2]
if parent is None:
has_reached_root = True
break
entry = ns_storage.get(cp_id)
ckpt = self.serde.loads_typed(entry[0]) if entry is not None else None
terminated_here: set[str] = set()
blob_value_by_ch: dict[str, Any] = {}
if ckpt is not None:
versions = ckpt.get("channel_versions", {})
for ch in remaining:
ver = versions.get(ch)
if ver is None:
continue
blob_entry = self.blobs.get((thread_id, checkpoint_ns, ch, ver))
if blob_entry is None or blob_entry[0] == "empty":
continue
blob_value_by_ch[ch] = self.serde.loads_typed(blob_entry)
terminated_here.add(ch)
step_writes = self.writes.get((thread_id, checkpoint_ns, cp_id), {})
for (_task_id, _idx), (tid, ch, serialized, _) in sorted(
step_writes.items(), reverse=True
):
if ch not in remaining:
continue
collected_by_ch[ch].append(
(tid, ch, self.serde.loads_typed(serialized))
)
for ch in terminated_here:
seed_by_ch[ch] = blob_value_by_ch[ch]
remaining.discard(ch)
current = parent
result: dict[str, DeltaChannelHistory] = {}
for ch in channels:
entry_h: DeltaChannelHistory = {
"writes": list(reversed(collected_by_ch[ch]))
}
if ch in seed_by_ch:
entry_h["seed"] = seed_by_ch[ch]
entry_h: DeltaChannelHistory = {"writes": []}
bound = supersteps_by_ch.get(ch, 0)
if bound <= 0:
result[ch] = entry_h
continue
if len(chain) >= bound:
seed_depth = bound
elif has_reached_root:
seed_depth = len(chain)
else:
logger.warning(
"cannot find seed snapshot for delta channel "
"(thread_id=%s, channel=%s)",
thread_id,
ch,
)
result[ch] = entry_h
continue
if seed_depth <= 0:
result[ch] = entry_h
continue
chain_slice: list[str] = chain[:seed_depth]
seed_cpid: str | None = chain[seed_depth - 1]
skip_seed_checkpoint_writes = False
seed_entry = ns_storage.get(seed_cpid)
if seed_entry is not None:
ckpt = self.serde.loads_typed(seed_entry[0])
ver = (ckpt.get("channel_versions") or {}).get(ch)
blob_entry = (
self.blobs.get((thread_id, checkpoint_ns, ch, ver))
if ver is not None
else None
)
if blob_entry is not None and blob_entry[0] != "empty":
seed_value = self.serde.loads_typed(blob_entry)
entry_h["seed"] = seed_value
skip_seed_checkpoint_writes = not isinstance(
seed_value, _DeltaSnapshot
)
else:
# No stored value at the oldest checkpoint → empty
# baseline; the chain's own writes replay from empty.
seed_cpid = None
collected: list[PendingWrite] = []
# Chain is newest→oldest; replay oldest→newest.
for cp_id in reversed(chain_slice):
if skip_seed_checkpoint_writes and cp_id == seed_cpid:
continue
step_writes = self.writes.get((thread_id, checkpoint_ns, cp_id), {})
for (_task_id, _idx), (tid, w_ch, serialized, _) in sorted(
step_writes.items()
):
if w_ch == ch:
collected.append((tid, ch, self.serde.loads_typed(serialized)))
entry_h["writes"] = collected
result[ch] = entry_h
return result
@@ -12,7 +12,7 @@ Core types:
from __future__ import annotations
from abc import ABC, abstractmethod
from collections.abc import Iterable, Mapping
from collections.abc import Iterable
from datetime import datetime
from typing import (
Any,
@@ -473,10 +473,10 @@ class PutOp(NamedTuple):
the full path would effectively be `"documents/user123/report1"`
"""
value: Mapping[str, Any] | None
value: dict[str, Any] | None
"""The data to store, or `None` to mark the item for deletion.
The value must be a mapping with string keys and JSON-serializable values.
The value must be a dictionary with string keys and JSON-serializable values.
Setting this to `None` signals that the item should be deleted.
Example:
@@ -552,14 +552,6 @@ class TTLConfig(TypedDict, total=False):
This can be overridden per-operation by explicitly setting `refresh_ttl`.
Defaults to `True` if not configured.
"""
omit_expired: bool
"""Whether to omit expired items from read operations.
If `True`, and if the store supports this option, expired items will not be
returned by `GET` or `SEARCH`, or included in namespace listings, even before
a TTL sweep deletes them.
Defaults to `False` if not configured.
"""
default_ttl: float | None
"""Default TTL (time-to-live) in minutes for new items.
@@ -857,7 +849,7 @@ class BaseStore(ABC):
self,
namespace: tuple[str, ...],
key: str,
value: Mapping[str, Any],
value: dict[str, Any],
index: Literal[False] | list[str] | None = None,
*,
ttl: float | None | NotProvided = NOT_PROVIDED,
@@ -869,7 +861,7 @@ class BaseStore(ABC):
Example: `("documents", "user123")`
key: Unique identifier within the namespace. Together with namespace forms
the complete path to the item.
value: Mapping containing the item's data. Must contain string keys
value: Dictionary containing the item's data. Must contain string keys
and JSON-serializable values.
index: Controls how the item's fields are indexed for search:
@@ -1110,7 +1102,7 @@ class BaseStore(ABC):
self,
namespace: tuple[str, ...],
key: str,
value: Mapping[str, Any],
value: dict[str, Any],
index: Literal[False] | list[str] | None = None,
*,
ttl: float | None | NotProvided = NOT_PROVIDED,
@@ -1122,7 +1114,7 @@ class BaseStore(ABC):
Example: `("documents", "user123")`
key: Unique identifier within the namespace. Together with namespace forms
the complete path to the item.
value: Mapping containing the item's data. Must contain string keys
value: Dictionary containing the item's data. Must contain string keys
and JSON-serializable values.
index: Controls how the item's fields are indexed for search:
@@ -5,7 +5,7 @@ from __future__ import annotations
import asyncio
import functools
import weakref
from collections.abc import Callable, Iterable, Mapping
from collections.abc import Callable, Iterable
from typing import Any, Literal, TypeVar
from langgraph.store.base import (
@@ -132,7 +132,7 @@ class AsyncBatchedBaseStore(BaseStore):
self,
namespace: tuple[str, ...],
key: str,
value: Mapping[str, Any],
value: dict[str, Any],
index: Literal[False] | list[str] | None = None,
*,
ttl: float | None | NotProvided = NOT_PROVIDED,
@@ -231,7 +231,7 @@ class AsyncBatchedBaseStore(BaseStore):
self,
namespace: tuple[str, ...],
key: str,
value: Mapping[str, Any],
value: dict[str, Any],
index: Literal[False] | list[str] | None = None,
*,
ttl: float | None | NotProvided = NOT_PROVIDED,
@@ -408,7 +408,7 @@ class InMemoryStore(BaseStore):
self._vectors[namespace].pop(key, None)
else:
self._data[namespace][key] = Item(
value=dict(op.value),
value=op.value,
key=key,
namespace=namespace,
created_at=datetime.now(timezone.utc),
+1 -7
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "langgraph-checkpoint"
version = "4.2.0"
version = "4.1.1"
description = "Library with base interfaces for LangGraph checkpoint savers."
authors = []
requires-python = ">=3.10"
@@ -59,15 +59,9 @@ lint.select = [
"UP", # pyupgrade
"B", # flake8-bugbear
"I", # isort
"PLC0415", # import-outside-top-level
"RUF100", # unused noqa directive
"UP", # pyupgrade
]
lint.ignore = ["E501", "B008"]
# PLC0415 (import-outside-top-level) is enforced in tests only. Library code
# still has deferred imports that have not been reviewed, so it stays exempt
# for now.
lint.per-file-ignores = { "langgraph/**" = ["PLC0415"] }
target-version = "py310"
[tool.ty.rules]
@@ -12,14 +12,10 @@ conformance = pytest.importorskip(
@pytest.mark.asyncio
async def test_delta_channel_conformance():
# Imported inside the test: the module-level importorskip above is what
# makes these safe, so they cannot move to the top of the file.
from langgraph.checkpoint.conformance import validate # noqa: PLC0415
from langgraph.checkpoint.conformance.initializer import ( # noqa: PLC0415
checkpointer_test,
)
from langgraph.checkpoint.conformance import validate
from langgraph.checkpoint.conformance.initializer import checkpointer_test
from langgraph.checkpoint.memory import InMemorySaver # noqa: PLC0415
from langgraph.checkpoint.memory import InMemorySaver
@checkpointer_test(name="InMemorySaver")
async def mem_saver():
+2
View File
@@ -307,6 +307,8 @@ class TestWithMsgpackAllowlistEncrypted:
def loads_typed(self, data: tuple[str, bytes]) -> None:
return None
from langgraph.checkpoint.serde.base import CipherProtocol
class DummyCipher(CipherProtocol):
def encrypt(self, plaintext: bytes) -> tuple[str, bytes]:
return "dummy", plaintext
+9 -4
View File
@@ -1,12 +1,9 @@
import dataclasses
import json
import logging
import os
import pathlib
import pickle
import re
import sys
import tempfile
import uuid
from collections import deque
from datetime import date, datetime, time, timezone
@@ -21,7 +18,7 @@ import ormsgpack
import pandas as pd
import pytest
from langchain_core.documents.base import Document
from langchain_core.messages import AIMessage, HumanMessage
from langchain_core.messages import HumanMessage
from pydantic import BaseModel, SecretStr
from pydantic.v1 import BaseModel as BaseModelV1
from pydantic.v1 import SecretStr as SecretStrV1
@@ -344,6 +341,7 @@ def test_lc2_json_safe_type_revives_without_allowlist() -> None:
constructor dicts. Resuming those threads must reconstruct proper BaseMessage objects
rather than returning raw dicts that cause MESSAGE_COERCION_FAILURE in add_messages.
"""
from langchain_core.messages import AIMessage
serde = JsonPlusSerializer() # default: _allowed_json_modules=None
@@ -412,6 +410,7 @@ def test_lc2_json_method_field_is_ignored() -> None:
to that method: the result is whatever ``AIMessage(*args, **kwargs)`` would
produce, which proves the default constructor ran instead of ``parse_raw``.
"""
from langchain_core.messages import AIMessage
serde = JsonPlusSerializer()
load = {
@@ -437,6 +436,7 @@ def test_lc2_json_method_field_is_ignored_for_allowlisted_types() -> None:
method dispatch as a side effect. Revival is restricted to the default
constructor regardless of how the class reached the revival path.
"""
from langchain_core.messages import AIMessage
serde = JsonPlusSerializer(
allowed_json_modules=[("langchain_core.messages.ai", "AIMessage")]
@@ -455,6 +455,7 @@ def test_lc2_json_method_field_is_ignored_for_allowlisted_types() -> None:
def test_lc2_json_safe_type_init_still_works() -> None:
"""SAFE-type lc=2 revival without a `method` field still constructs the class."""
from langchain_core.messages import AIMessage
serde = JsonPlusSerializer()
load = {
@@ -478,6 +479,7 @@ def test_lc2_json_legacy_pydantic_method_list_falls_back_to_default() -> None:
this shape continue to revive correctly as long as the default constructor
accepts the serialized kwargs.
"""
from langchain_core.messages import AIMessage
serde = JsonPlusSerializer()
load = {
@@ -549,6 +551,9 @@ def test_lc2_json_safe_type_pickle_payload_does_not_execute() -> None:
With method dispatch removed from `_revive_lc2`, the gadget bytes are never
passed to `parse_raw` and therefore never reach `pickle.loads`.
"""
import os
import pickle
import tempfile
marker = tempfile.NamedTemporaryFile(
prefix="lc2_block_proof_", suffix=".out", delete=False
+34 -51
View File
@@ -1,4 +1,3 @@
import asyncio
import logging
from typing import Any
@@ -352,9 +351,12 @@ class TestInMemorySaverDeltaChannel:
cp1["id"] = "cp1"
cp2 = empty_checkpoint()
cp2["id"] = "cp2"
# Target (cp2) carries the supersteps counter; no snapshot was ever
# taken, so the walk runs back to the root → no seed, empty baseline.
cp2_md = {"counters_since_delta_snapshot": {channel: [1, 2]}}
saver.storage[thread_id][ns] = {
"cp1": (serde.dumps_typed(cp1), serde.dumps_typed({}), None),
"cp2": (serde.dumps_typed(cp2), serde.dumps_typed({}), "cp1"),
"cp2": (serde.dumps_typed(cp2), serde.dumps_typed(cp2_md), "cp1"),
}
# Writes stored at cp1 produced the cp1 snapshot; part of history.
saver.writes[(thread_id, ns, "cp1")][("task1", 0)] = (
@@ -457,10 +459,15 @@ class TestBaseFallbackGetChannelWrites:
cp1["id"] = "00000000000000000000000000000002.0000000000000000"
cp2 = empty_checkpoint()
cp2["id"] = "00000000000000000000000000000003.0000000000000000"
# The target (cp2) carries `counters_since_delta_snapshot` — the
# supersteps-since-last-snapshot driving the ancestor-walk depth. No
# snapshot was ever taken, so the count runs back past the oldest
# persisted checkpoint (root) → implicit empty baseline, no seed.
cp2_md = {"counters_since_delta_snapshot": {channel: [2, 3]}}
saver.storage[thread_id][ns] = {
cp0["id"]: (serde.dumps_typed(cp0), serde.dumps_typed({}), None),
cp1["id"]: (serde.dumps_typed(cp1), serde.dumps_typed({}), cp0["id"]),
cp2["id"]: (serde.dumps_typed(cp2), serde.dumps_typed({}), cp1["id"]),
cp2["id"]: (serde.dumps_typed(cp2), serde.dumps_typed(cp2_md), cp1["id"]),
}
# Writes under cp0 produced cp1's state; writes under cp1 produced cp2's.
saver.writes[(thread_id, ns, cp0["id"])][("task1", 0)] = (
@@ -524,6 +531,7 @@ class TestBaseFallbackGetChannelWrites:
`threading.local()` guard would let whichever task set it first
short-circuit the other to `writes=[]`.
"""
import asyncio
saver, thread_id, ns = self._build_saver_with_chain()
@@ -577,19 +585,9 @@ class TestPreDeltaBlobTerminator:
"""
def _build_mixed_thread(self) -> tuple[InMemorySaver, str, str, str, str]:
"""Four-checkpoint chain spanning the migration boundary:
* `cp0` pre-delta ancestor OLDER than the seed. Its write
(`OLDER-WRITE`) is already folded into `cp1`'s stored value, so the
walk must terminate at `cp1` and never reach it.
* `cp1` pre-delta, blob `["A"]`. That value is the state ENTERING
`cp1`; the write stored under `cp1` (`PRE-DELTA-WRITE`) is what
produced `cp2` and is NOT subsumed by the blob.
* `cp2` delta-era, no stored value, write `B`.
* `cp3` target, delta-era, write `PENDING-AT-TARGET`.
Reconstructing at `cp3` must yield seed `["A"]` plus writes
`["PRE-DELTA-WRITE", "B"]`.
"""Three-checkpoint chain: cp1 (pre-delta, blob=[A]), cp2 (delta,
write=B), cp3 (delta, write=C). Reconstructing at cp3 must yield
seed=[A] + writes=[B, C].
Returns `(saver, thread_id, ns, channel, cp3_id)`.
"""
@@ -597,21 +595,16 @@ class TestPreDeltaBlobTerminator:
serde = JsonPlusSerializer()
thread_id, ns, channel = "t1", "", "messages"
v0 = "00000000000000000000000000000000.0"
v1 = "00000000000000000000000000000001.0"
v2 = "00000000000000000000000000000002.0"
v3 = "00000000000000000000000000000003.0"
# Pre-delta: cp0 and cp1 stored real blobs for the channel.
saver.blobs[(thread_id, ns, channel, v0)] = serde.dumps_typed([])
# Pre-delta: cp1 stored a real blob for the channel.
saver.blobs[(thread_id, ns, channel, v1)] = serde.dumps_typed(["A"])
# Delta-era: cp2 and cp3 store "empty"; real writes in checkpoint_writes.
saver.blobs[(thread_id, ns, channel, v2)] = ("empty", b"")
saver.blobs[(thread_id, ns, channel, v3)] = ("empty", b"")
cp0 = empty_checkpoint()
cp0["id"] = "cp0"
cp0["channel_versions"][channel] = v0
cp1 = empty_checkpoint()
cp1["id"] = "cp1"
cp1["channel_versions"][channel] = v1
@@ -622,25 +615,21 @@ class TestPreDeltaBlobTerminator:
cp3["id"] = "cp3"
cp3["channel_versions"][channel] = v3
# cp1 is the pre-delta (migration) seed; the channel became a
# DeltaChannel at cp2, so its supersteps counter starts there: cp2 -> 1,
# cp3 -> 2. The target (cp3) walks 2 hops back to the cp1 seed blob.
cp3_md = {"counters_since_delta_snapshot": {channel: [2, 2]}}
saver.storage[thread_id][ns] = {
"cp0": (serde.dumps_typed(cp0), serde.dumps_typed({}), None),
"cp1": (serde.dumps_typed(cp1), serde.dumps_typed({}), "cp0"),
"cp1": (serde.dumps_typed(cp1), serde.dumps_typed({}), None),
"cp2": (serde.dumps_typed(cp2), serde.dumps_typed({}), "cp1"),
"cp3": (serde.dumps_typed(cp3), serde.dumps_typed({}), "cp2"),
"cp3": (serde.dumps_typed(cp3), serde.dumps_typed(cp3_md), "cp2"),
}
# Write under cp0 is older than the seed — cp1's blob already folded
# it in, and the terminator must stop before reaching it.
saver.writes[(thread_id, ns, "cp0")][("task0", 0)] = (
# Write under cp1 would be from the pre-delta era and MUST be ignored
# (the blob already captures it). We add one and assert it is not
# folded into the reconstructed result.
saver.writes[(thread_id, ns, "cp1")][("task0", 0)] = (
"task0",
channel,
serde.dumps_typed("OLDER-WRITE"),
"",
)
# Write under cp1 postdates cp1's blob (it is what produced cp2, which
# stores no value of its own) and MUST be replayed.
saver.writes[(thread_id, ns, "cp1")][("task1", 0)] = (
"task1",
channel,
serde.dumps_typed("PRE-DELTA-WRITE"),
"",
)
@@ -674,19 +663,15 @@ class TestPreDeltaBlobTerminator:
# Seed came from the pre-delta blob at cp1.
assert result["seed"] == ["A"]
# The seed ancestor's own write and the delta-era write from cp2 both
# replay through the reducer on top of the seed, oldest first. cp3 is
# the target — its own write is pending for the NEXT step and must be
# excluded.
# Delta-era writes from cp2 replay through the reducer on top of seed.
# cp3 is the target — its own write is pending for the NEXT step and
# must be excluded.
values = [v for _, _, v in result["writes"]]
assert values == ["PRE-DELTA-WRITE", "B"]
assert values == ["B"]
def test_seed_bounds_walk_without_dropping_its_own_writes(self) -> None:
"""The seed terminator bounds the walk: writes at ancestors OLDER than
the seed are already folded into the seed value and must not be
replayed. The seed ancestor's own write is not one of them — it
postdates the stored value and produced the next checkpoint.
"""
def test_pre_delta_blob_terminates_walk_before_older_writes(self) -> None:
"""Writes stored at the pre-delta ancestor itself must not be replayed
(the blob subsumes them)."""
saver, thread_id, ns, channel, target = self._build_mixed_thread()
config: RunnableConfig = {
"configurable": {
@@ -701,9 +686,7 @@ class TestPreDeltaBlobTerminator:
]
values = [v for _, _, v in result["writes"]]
# Older than the seed — subsumed by cp1's blob, so the walk stops first.
assert "OLDER-WRITE" not in values
# Stored AT the seed ancestor — not subsumed, so it must be replayed.
assert "PRE-DELTA-WRITE" in values
# The pre-delta write under cp1 must not appear (the blob subsumes it).
assert "PRE-DELTA-WRITE" not in values
# And the pending write at the target is never folded in.
assert "PENDING-AT-TARGET" not in values
+54 -54
View File
@@ -149,11 +149,11 @@ wheels = [
[[package]]
name = "codespell"
version = "2.4.3"
version = "2.4.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/80/19/45e941380f69c042b43423513d201e6592346f992394347f5e7174c31407/codespell-2.4.3.tar.gz", hash = "sha256:cbe085e331227b37bb86ef8bddd08dc768c704ee9a07ca869852c093fa2793e2", size = 352773, upload-time = "2026-07-15T11:51:54.159Z" }
sdist = { url = "https://files.pythonhosted.org/packages/2d/9d/1d0903dff693160f893ca6abcabad545088e7a2ee0a6deae7c24e958be69/codespell-2.4.2.tar.gz", hash = "sha256:3c33be9ae34543807f088aeb4832dfad8cb2dae38da61cac0a7045dd376cfdf3", size = 352058, upload-time = "2026-03-05T18:10:42.936Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8b/bf/bdb951d34eb169140b546f44be9ec4525d1acefb9eb5572071f5492b19fc/codespell-2.4.3-py3-none-any.whl", hash = "sha256:af2505b335e8573dbd2d384d1c4ef498f4006f4ba2d6fceca01e55b91f52628a", size = 340736, upload-time = "2026-07-15T11:51:52.925Z" },
{ url = "https://files.pythonhosted.org/packages/42/a1/52fa05533e95fe45bcc09bcf8a503874b1c08f221a4e35608017e0938f55/codespell-2.4.2-py3-none-any.whl", hash = "sha256:97e0c1060cf46bd1d5db89a936c98db8c2b804e1fdd4b5c645e82a1ec6b1f886", size = 353715, upload-time = "2026-03-05T18:10:41.398Z" },
]
[[package]]
@@ -268,7 +268,7 @@ wheels = [
[[package]]
name = "langchain-core"
version = "1.5.2"
version = "1.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jsonpatch" },
@@ -281,26 +281,26 @@ dependencies = [
{ name = "typing-extensions" },
{ name = "uuid-utils" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6e/58/3ad53096eee1e07728e8219ded30ae308b0f2b8b7b26b8ebb6371917c0f5/langchain_core-1.5.2.tar.gz", hash = "sha256:2d13ab35b42eec63d4669a483776b8cdd778ee764107149369fb369d84c08c41", size = 972322, upload-time = "2026-07-28T16:38:37.977Z" }
sdist = { url = "https://files.pythonhosted.org/packages/59/de/679a53472c25860837e32c0442c962fa86e95317a36460e2c9d5c91b17c2/langchain_core-1.4.0.tar.gz", hash = "sha256:1dc341eed802ed9c117c0df3923c991e5e9e226571e5725c194eeb5bd93d1a7f", size = 920260, upload-time = "2026-05-11T18:42:35.919Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ca/e4/024e402a65f5ee2eb6ae9667b5ffc5f08776cb4e712f5a11ee1997d56083/langchain_core-1.5.2-py3-none-any.whl", hash = "sha256:a687dd7c3b22c6c1294e1c1eeb61fb6f3a308e6015a1d75b576b3836ad5b5aed", size = 561643, upload-time = "2026-07-28T16:38:36.38Z" },
{ url = "https://files.pythonhosted.org/packages/0f/1a/86c38c27b81913a1c6c12448cab55defb5a1097c7dc9a4cea83f55477a2d/langchain_core-1.4.0-py3-none-any.whl", hash = "sha256:23cbbdb46e38ddd1dd5247e6167e96013eae74bea4c5949c550809970a9e565c", size = 548120, upload-time = "2026-05-11T18:42:33.992Z" },
]
[[package]]
name = "langchain-protocol"
version = "0.0.18"
version = "0.0.15"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d2/59/b5959aea96faa9146e2e49a7a22882b3528c62efafe9a6a95beab30c2305/langchain_protocol-0.0.18.tar.gz", hash = "sha256:ec3e11782f1ed0c9db38e5a9ed01b0e7a0d3fba406faa8aef6594b73c56a63e6", size = 6150, upload-time = "2026-06-18T17:08:26.959Z" }
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" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/99/2e/d82db9eec13ad0f72e7aaad5c4bc730ab111934fdc83c85523206eb9b0a0/langchain_protocol-0.0.18-py3-none-any.whl", hash = "sha256:70b53a86fbf9cedc863555effe44da192ab02d556ddbf2cf95b8873adcf41b5a", size = 7221, upload-time = "2026-06-18T17:08:25.996Z" },
{ 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" },
]
[[package]]
name = "langgraph-checkpoint"
version = "4.2.0"
version = "4.1.1"
source = { editable = "." }
dependencies = [
{ name = "langchain-core" },
@@ -982,7 +982,7 @@ wheels = [
[[package]]
name = "pytest"
version = "9.1.1"
version = "9.0.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
@@ -993,9 +993,9 @@ dependencies = [
{ name = "pygments" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
]
[[package]]
@@ -1124,14 +1124,14 @@ wheels = [
[[package]]
name = "redis"
version = "8.0.1"
version = "8.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "async-timeout", marker = "python_full_version < '3.11.3'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cc/c3/928b290c2c0ca99ab96eea5b4ff8f30be8112b075301a7d3ba214a3c8c12/redis-8.0.1.tar.gz", hash = "sha256:afc5a7a2f5a084f5b1880dec548dd45be17db7e43c82a30d84f952aefb05cfb0", size = 5114170, upload-time = "2026-06-23T14:52:37.728Z" }
sdist = { url = "https://files.pythonhosted.org/packages/53/ae/ed461cca5780b5fc8b9fe8ca0ed98d89508645fb9d880c24cc42c087678f/redis-8.0.0.tar.gz", hash = "sha256:a00c5355432051ac14e593b8b197fc76c887ee12d55a0984f69328a1115fdc49", size = 5101591, upload-time = "2026-05-28T12:45:13.5Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fd/0a/c2345ebf1ebe70840ce3f6c6ee612f8fa749cfbd1b03069c53bf0c62aaad/redis-8.0.1-py3-none-any.whl", hash = "sha256:47daa35a058c23468d6437f17a8c76882cb316b838ef763036af99b96cedd743", size = 502406, upload-time = "2026-06-23T14:52:36.137Z" },
{ url = "https://files.pythonhosted.org/packages/27/e3/b519734372d305bd547534a9f32e4ce9f98552af753dce72cf3483a0ff0b/redis-8.0.0-py3-none-any.whl", hash = "sha256:c938c18338585009f0bc310f4c7e4e4b4d37639356c4ac072cedf3af570c8dc7", size = 499870, upload-time = "2026-05-28T12:45:11.697Z" },
]
[[package]]
@@ -1163,27 +1163,27 @@ wheels = [
[[package]]
name = "ruff"
version = "0.16.0"
version = "0.15.15"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4d/94/1e5e4967626faf12fa56999cd6222dff6992ceb086ad7945756baf70c7a7/ruff-0.16.0.tar.gz", hash = "sha256:e460aafd5495ec89efaa6ced2e4a9a581116451e1c88b9d37ef497e0f8e93982", size = 4790557, upload-time = "2026-07-23T19:11:30.981Z" }
sdist = { url = "https://files.pythonhosted.org/packages/84/6f/a76f7d96e5c962f5b69cee865e49c15c1116897c01990faa8a57edb62e7f/ruff-0.15.15.tar.gz", hash = "sha256:b8dff018130b46d8e5bf0f926ef6b60cf871d6d5ae45fc9334e09632daa741d6", size = 4706985, upload-time = "2026-05-28T14:16:57.784Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4b/81/1c8818fee7ce1a04cd7d1b3172e0a8f8e4f1dc4feb7fc390e16daa8af323/ruff-0.16.0-py3-none-linux_armv6l.whl", hash = "sha256:e5115729eb08c585e5121978ba5d5b60caeae394ce21b9fb5e6cd33a1c6c9b1e", size = 10754633, upload-time = "2026-07-23T19:10:46.415Z" },
{ url = "https://files.pythonhosted.org/packages/23/df/beaf59c09d68db84304d555f188b276a77132a5d5b0b67a5c762aa143628/ruff-0.16.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3c954b1d580bfa035b41654f7858cc7e71d5fc3ac5b723dd62bd9133830ed522", size = 10969164, upload-time = "2026-07-23T19:10:50.271Z" },
{ url = "https://files.pythonhosted.org/packages/42/ce/741cd197496a1abbf51352710fd15ed995d2a2be87189c1da26a450d6e83/ruff-0.16.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e01c21d10eb1b29f47b7454e1f4056db9a3f0260c646aa88457c610291db9f81", size = 10488846, upload-time = "2026-07-23T19:10:52.639Z" },
{ url = "https://files.pythonhosted.org/packages/52/2a/a2db8e88cade358f5cdcb05674a917751074109315d014eb6352d9a893f7/ruff-0.16.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e364e5ed22ed8dc05082fd78e35308618260907ac2d3c1d637b2e682415b6c9", size = 10889729, upload-time = "2026-07-23T19:10:54.89Z" },
{ url = "https://files.pythonhosted.org/packages/42/65/62a771694ebd63029dc953e27dbad40e1588bd4860ff9fe881018fddaa49/ruff-0.16.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d327b8fc113a1d4421a04f3839d3752057c8dd1ee320223a6f3f52d04ada462a", size = 10568275, upload-time = "2026-07-23T19:10:56.993Z" },
{ url = "https://files.pythonhosted.org/packages/3f/e2/ced249fe8af5f086c5c58cc21cc3356d50f32f7401c5df87050c999620a7/ruff-0.16.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b50c55e263103586b3dcf5f73d479eb8cb5fdb6098fec59a62891dab653717", size = 11385112, upload-time = "2026-07-23T19:10:59.615Z" },
{ url = "https://files.pythonhosted.org/packages/87/0b/05154977a8fd69eeb6c103271f55403bfd8711f5c0f8ed07489d95a504e7/ruff-0.16.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ff4a79ce3ec0172f3241943835de1c4cb4e2dcd07f0f8c2d02603dbbbee4b17", size = 12207008, upload-time = "2026-07-23T19:11:02.154Z" },
{ url = "https://files.pythonhosted.org/packages/fb/29/98225831a3a1eab0e02f4acc6ca6559a98611dcc68b6965ff4b7234627c1/ruff-0.16.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e95c448fca1fb2a18372a9440926c5a6ee789639bb975c72e7ae6d0b04218ab4", size = 11650842, upload-time = "2026-07-23T19:11:04.557Z" },
{ url = "https://files.pythonhosted.org/packages/91/66/6bd3cf90500653d55dc0ffc8507aa8300bd49d0214b2e8cb4d3fef2943ba/ruff-0.16.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f11a8d11010301d0a398a2fdef67691feca7294da6aef55e2150e8fa2cd520b", size = 11400718, upload-time = "2026-07-23T19:11:09.233Z" },
{ url = "https://files.pythonhosted.org/packages/8e/a2/a54eb4eae05d66364050a5d3b8a9c5ef88196531b3cbe7109d873f87f819/ruff-0.16.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:48044c678e9cb8698246c99b14aaccfa6601dea7379eb48a6f8f73f7a6d86cd0", size = 11426177, upload-time = "2026-07-23T19:11:11.994Z" },
{ url = "https://files.pythonhosted.org/packages/1a/be/16e3eea4b2a478a496919f5e36f17c4559e54620bd3bbac5d6affa068006/ruff-0.16.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7aa0959bad8eb8bef50340154fc9b58678dae31fa4293afa38b44b6e552c0213", size = 10856126, upload-time = "2026-07-23T19:11:14.221Z" },
{ url = "https://files.pythonhosted.org/packages/a2/84/252eb8b868a16eec7257c14f504f77537e734b2d69c762e639e588e304a3/ruff-0.16.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:28ea2b7df8ebf7f9da6b7d47b230ab48f387c0a29be3b474c4d0740e197bb9af", size = 10571208, upload-time = "2026-07-23T19:11:16.378Z" },
{ url = "https://files.pythonhosted.org/packages/21/09/817a482f542f7570cbb4554b26e896610c7114f539b1d9e2d2145bf6bef6/ruff-0.16.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:33a3dfac8c35f81498dea9181bccc2f4c4bc8f1521a1dd9406e77643e0f0fb09", size = 11063329, upload-time = "2026-07-23T19:11:19.173Z" },
{ url = "https://files.pythonhosted.org/packages/2e/23/9403c180ca1cb9b1f7335f5c3e5305c09d49ea5b345196682a36028bde4a/ruff-0.16.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a5237a0bda500d30d81b8e07a6973a5cbc772864cbf746ae2f4e8a2e01c9f4ed", size = 11489751, upload-time = "2026-07-23T19:11:21.74Z" },
{ url = "https://files.pythonhosted.org/packages/b2/1d/1b2ef7bcde851c78d7f17f1cca13fd6dc695fc4b3d6197941e72cae5b132/ruff-0.16.0-py3-none-win32.whl", hash = "sha256:7fab76fa065c873f41ff744347c6e77bcc3dfec4bcc754dc26b63d23c0f7f5fb", size = 10785885, upload-time = "2026-07-23T19:11:23.947Z" },
{ url = "https://files.pythonhosted.org/packages/b2/a3/d5e4ef7a56be3f928ffb90b94c25ba7d3cb9c7fe0736aeaaedf361770712/ruff-0.16.0-py3-none-win_amd64.whl", hash = "sha256:429c117f022bf481fabd9d551e7a3952b24c65e6ef44337ea09d90bebef14472", size = 11923141, upload-time = "2026-07-23T19:11:26.409Z" },
{ url = "https://files.pythonhosted.org/packages/cb/9a/8415f2657cbe200f41a4531ccededf135505a92d4a012229121f885b26f9/ruff-0.16.0-py3-none-win_arm64.whl", hash = "sha256:14296fedcd2705c77ab8235439278bbb38f285cf7da5528b00b3e330c3d4872d", size = 11273407, upload-time = "2026-07-23T19:11:28.705Z" },
{ url = "https://files.pythonhosted.org/packages/fa/9d/3a45c05b8ab04b4705989de70a79008e27c8003296a0feaee9edc18dd7e9/ruff-0.15.15-py3-none-linux_armv6l.whl", hash = "sha256:cf93e5388f412e1b108b1f8b34a6e036b70fe8aff89393befad96fe48670311b", size = 10710652, upload-time = "2026-05-28T14:16:06.701Z" },
{ url = "https://files.pythonhosted.org/packages/05/66/da974431624bf3b49f6ee1f9543c02d929ff1cba78b0d5a79c38cf21f744/ruff-0.15.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac5a646d1f6a7dadd5d50842dae2c1f9862ac887ef5d1b1375e02def791fde6e", size = 11096615, upload-time = "2026-05-28T14:16:23.313Z" },
{ url = "https://files.pythonhosted.org/packages/8c/09/7443452e5d290230a712103f2fdceeef7184f3ec99a2bd01c8be78aaceb5/ruff-0.15.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:77d955a431430c66f72dd94e379ad38a16daea3d25094872ac4edf9e797be530", size = 10436683, upload-time = "2026-05-28T14:16:40.974Z" },
{ url = "https://files.pythonhosted.org/packages/53/01/d330c26a57fa4f3943a14424904027428315b700fe4d14a84bb123a649e5/ruff-0.15.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7614ee79c69788cf6cedd568069ade9cecc22a1ad20494efe8d0c9ebb4b622d4", size = 10769064, upload-time = "2026-05-28T14:16:28.905Z" },
{ url = "https://files.pythonhosted.org/packages/1d/85/cc8770f8bdff541b1da8392d1634141fe4a0e3f4ee596605959b7906c27f/ruff-0.15.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3cdb1679e06a1f6b47bc384714ae96f6e2fb65ca441eb78c43d2ca554176ce1f", size = 10511987, upload-time = "2026-05-28T14:16:43.732Z" },
{ url = "https://files.pythonhosted.org/packages/7c/29/8c190c1472b63013583ba391f3342036e02010544c1270455ed8e519bdf3/ruff-0.15.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2728b93d7b23a603ea2c0ac6eb73d760bd38ec9de35f35fb41e18f7a3fee7622", size = 11275100, upload-time = "2026-05-28T14:16:55.244Z" },
{ url = "https://files.pythonhosted.org/packages/9f/6b/7e145ce2cc8e63d6834eca03d83a0e18d121def5c69f91b4cf4011ed4879/ruff-0.15.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be582fcc0db438902c7792b08d6ddf6c9b9e21addaa10092c2c741cfb09e5a45", size = 12176903, upload-time = "2026-05-28T14:16:14.368Z" },
{ url = "https://files.pythonhosted.org/packages/80/a3/d5974637f68e451f7fadf015cf3101d1cd7d8ba5027cffe0b9e3826ebe6b/ruff-0.15.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7aa77465b8ecaf1a27bea098d696f7fed5e1eccbd10b321b682d6de586ae5627", size = 11404550, upload-time = "2026-05-28T14:16:20.138Z" },
{ url = "https://files.pythonhosted.org/packages/fe/1c/e6e5e568f22be4fb05d6244234aba384c06b451252453b821e1a529263cf/ruff-0.15.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48decfa11d740de4889de623be1463308346312f2409a56e24aa280c86162dc4", size = 11382027, upload-time = "2026-05-28T14:16:46.615Z" },
{ url = "https://files.pythonhosted.org/packages/1d/01/170921b49fcd2e8858825593f91cf7146c3e40a5c3e6df763e4bb0484dde/ruff-0.15.15-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a5015088452ca0081387063649ec67f06d3d1d6b8b936a1f836b5e9657ecd48c", size = 11366041, upload-time = "2026-05-28T14:16:26.247Z" },
{ url = "https://files.pythonhosted.org/packages/87/54/a7bad711d7de93254e15e06a4c375b89a03d18de45d3e5dcc86a4472fb1a/ruff-0.15.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5294aab6356c81600fcdea3a62bb1b924dfd5e91767c12318d3f68f86af57cd", size = 10741795, upload-time = "2026-05-28T14:16:17.11Z" },
{ url = "https://files.pythonhosted.org/packages/c9/31/38c075963668f8b41c6914ee0f6f318727fbe30ab9145cb29e6df464c5fa/ruff-0.15.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:db5bd4d802415cca656dc1616070b725952d6ae95eb5d4831e49fbd94a38f75f", size = 10511117, upload-time = "2026-05-28T14:16:31.767Z" },
{ url = "https://files.pythonhosted.org/packages/9d/96/6ff689e1f7e375d1d97075eca022f74c2bab59554a432fe4d2e6f091986a/ruff-0.15.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:587a6278ed42059191c1a466e490bd7930fb50bd2e255398bc29616c895a61cb", size = 10994867, upload-time = "2026-05-28T14:16:35.149Z" },
{ url = "https://files.pythonhosted.org/packages/c3/c2/5dce0ab9f92a8d534fa62b9bf9caca3eddb8c1a81b616f5e195ada4f0d6e/ruff-0.15.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:df0c1c084f5f4be9812f61518a45c440d3c30d69ce4bf6c5270e66d38338f02a", size = 11482101, upload-time = "2026-05-28T14:16:49.598Z" },
{ url = "https://files.pythonhosted.org/packages/b1/c0/1003b60edd697c649faf61f1a34094b1abb38fb3d1181e3f895781250a08/ruff-0.15.15-py3-none-win32.whl", hash = "sha256:29428ea79694afbe756d45fd59b36f22b6b020dc0443cf7de0173046236964b9", size = 10716774, upload-time = "2026-05-28T14:16:52.337Z" },
{ url = "https://files.pythonhosted.org/packages/02/a8/1269eddd6945a06c23f055ef7848886e37cf9d6a8bebb386a3115f01470c/ruff-0.15.15-py3-none-win_amd64.whl", hash = "sha256:8df0323902e15e24bc4bf246da830573d3cf3352bd0b9a164eab335d111ff4a4", size = 11868463, upload-time = "2026-05-28T14:16:11.333Z" },
{ url = "https://files.pythonhosted.org/packages/4e/b2/920464c907b191e37469d477a1aa8bc048b8f36c4c1610dfa4ab87b39e18/ruff-0.15.15-py3-none-win_arm64.whl", hash = "sha256:3c8ceca6792f38196b8f589bc92eccd03eef286602da92e5dc05cc42ef6441b7", size = 11138498, upload-time = "2026-05-28T14:16:38.425Z" },
]
[[package]]
@@ -1260,27 +1260,27 @@ wheels = [
[[package]]
name = "ty"
version = "0.0.64"
version = "0.0.43"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8e/aa/14c9965d3b173105692473897cc89c34cd91241368b2044e43167e1c17ff/ty-0.0.64.tar.gz", hash = "sha256:d12ddbb05f15158bb518af619378b385486450def95fb06f8ab98037febe9f2c", size = 6350966, upload-time = "2026-07-27T18:32:45.403Z" }
sdist = { url = "https://files.pythonhosted.org/packages/0d/37/4ec04de0659b93be37d956dfceca13b1ecab9c959f28d8a1d5e514603f36/ty-0.0.43.tar.gz", hash = "sha256:ea4cff50548f2a1877e848d3abe9e293cde8ab94757a7eb93fc0d4013f98be8e", size = 5798429, upload-time = "2026-06-04T00:52:10.013Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ae/4c/c54937e4ff3fa7b34a99ea3387ec766bf0ad98dc8df8d792e89b388e658e/ty-0.0.64-py3-none-linux_armv6l.whl", hash = "sha256:3830a6675ab43635ced1c4c557f380ac4a49e9414e03975e4e4e8db644c64944", size = 12118357, upload-time = "2026-07-27T18:32:08.702Z" },
{ url = "https://files.pythonhosted.org/packages/ce/aa/a839ee2bc78e943d079e6abe199a97b4eeffb7e5c9a57326d69de452186a/ty-0.0.64-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3ff07d7bc32a2135f58afe57393789a32ea2fed1a66216129a8e535e76043903", size = 11790882, upload-time = "2026-07-27T18:32:10.997Z" },
{ url = "https://files.pythonhosted.org/packages/4c/de/19f14357888a7198438926303753cf749428e3d62e8980ff1e9a72a78402/ty-0.0.64-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4f6d1c7f897cca05d12bacbf1435150d5ffa496099515aa6ed303c8b29e1d0bb", size = 11317394, upload-time = "2026-07-27T18:32:13.162Z" },
{ url = "https://files.pythonhosted.org/packages/08/2f/f54462300535ab99b551eda733177be2eef5dbc2997d3fdb357c4ddd760a/ty-0.0.64-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:138d6c37ad4bf8583aa7a9b29d90954151d7856f9910a03eae3eb34b34c57215", size = 11863042, upload-time = "2026-07-27T18:32:15.307Z" },
{ url = "https://files.pythonhosted.org/packages/5e/95/dbecf745520ebe8bd7b02fc55eee6441c9be312ebf6addce605eb52740dd/ty-0.0.64-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ed9719d1b7b66fb8efe073d860208a44c40af1f6cd5c2364aa9b323a1e579b4", size = 11910730, upload-time = "2026-07-27T18:32:17.467Z" },
{ url = "https://files.pythonhosted.org/packages/3b/26/12cfd40028e51ceed7b3cb645281c61c02eb64ff9fb0c09231d65c30ff25/ty-0.0.64-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d68b23e5169e2137b5f1de7169ab0cebecab6c8eda374c34c1f6394308f58242", size = 12631936, upload-time = "2026-07-27T18:32:19.533Z" },
{ url = "https://files.pythonhosted.org/packages/a0/d0/65ffc2b0a686347193c6f98e9421a7fc2a96fc3cd0b1001cf7cf284baff9/ty-0.0.64-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f41cb07d89d32626fcaf3ed4d262778fcb28b2628b6ed1e172cd7b18820668d8", size = 13171049, upload-time = "2026-07-27T18:32:22.026Z" },
{ url = "https://files.pythonhosted.org/packages/f6/a4/975a5961842dcd6fa60f0770a102c0bba7509da909ab652919bfcdcd4fc7/ty-0.0.64-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5f24e1504ab9e212f92356b82fe088fdeb3a39f9a2f4ff25e505d2e1d0db9056", size = 12826438, upload-time = "2026-07-27T18:32:24.178Z" },
{ url = "https://files.pythonhosted.org/packages/af/ef/dfb9b7f9bcc032d3b540b0d1f55f532a336e2fb41b1bd539c05ae81a151a/ty-0.0.64-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86db830cb914bb33bb8247b66ccea58de4496c2391bd42658aba744b439f3290", size = 12440880, upload-time = "2026-07-27T18:32:26.341Z" },
{ url = "https://files.pythonhosted.org/packages/ee/d0/bedac20505e8a8f5501ad73d7d15d8e421a563fef59993909a23036929a4/ty-0.0.64-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:652ee3d6d03bea76cd2fe8949c78bb5970394ebd7c5fd270e9518c0dee1b931d", size = 12782439, upload-time = "2026-07-27T18:32:28.642Z" },
{ url = "https://files.pythonhosted.org/packages/79/d5/795733f13ceff1378f08b3de0c49d0f518df220ed856b0dfac869f3b7c81/ty-0.0.64-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4838768295774a86e95f9ec5633e739d016adbbb69dcbdc62f3549578a11f624", size = 11814821, upload-time = "2026-07-27T18:32:30.632Z" },
{ url = "https://files.pythonhosted.org/packages/52/3e/9d99cd1e1831003434f508ed9f258a56543194afc3bbe051eba2545fa676/ty-0.0.64-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5a3d700669868599edf39ce5125196682f15a8880cc8c669bc2a83b99984d99b", size = 11928678, upload-time = "2026-07-27T18:32:32.888Z" },
{ url = "https://files.pythonhosted.org/packages/b8/3d/448f49a3503fb119a34348a5714bb92001f252fadbbb12d645f2e744b557/ty-0.0.64-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b161f0a82a8e2f2432db3bf7702b4d3924fa9486ba0014f6710a160fc157df0d", size = 12202249, upload-time = "2026-07-27T18:32:34.905Z" },
{ url = "https://files.pythonhosted.org/packages/dd/9b/75768e562cec990d189dc05807ae72890b20ffbd1e1f43597bb98db63c60/ty-0.0.64-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:39b9dd42908df47c2dc57dda87e656fab97097ffd2618474bbdea986af0d6a9d", size = 12548817, upload-time = "2026-07-27T18:32:36.995Z" },
{ url = "https://files.pythonhosted.org/packages/34/89/44cc276ea6ca0245495014758ffeadde1798fb0b5840c9cf36d8d2ed3250/ty-0.0.64-py3-none-win32.whl", hash = "sha256:d0676ab0e0935795e5843baa28dd5e366dc343d7c0945a996df9eab8e0644885", size = 11545474, upload-time = "2026-07-27T18:32:39.389Z" },
{ url = "https://files.pythonhosted.org/packages/01/7e/d1c8a871a38d17c8f168b9a6975f6247f7660f8334e517656a5e4b4a4858/ty-0.0.64-py3-none-win_amd64.whl", hash = "sha256:dcb9bd31f54097e362b776c26ab4564d4564cdd1355cb883481167a26c03cc3f", size = 12542987, upload-time = "2026-07-27T18:32:41.412Z" },
{ url = "https://files.pythonhosted.org/packages/35/4d/6d18640d0204cacd69abbaca95ad6a34c6d7e9169e9051d0117b17b827ec/ty-0.0.64-py3-none-win_arm64.whl", hash = "sha256:82cc34c1ad9a8feb6059aef193bebcecc656e548f6fab3d518bcd8b57d198d39", size = 11899263, upload-time = "2026-07-27T18:32:43.366Z" },
{ url = "https://files.pythonhosted.org/packages/db/74/1916026a78f20019a2f03adbd6fb4430ddb7ce1e52c2e17a90856a6d192e/ty-0.0.43-py3-none-linux_armv6l.whl", hash = "sha256:3bf70f5446480562bf6c9f639df4b5cb60716b8f8d1a6b8e5811d5c7eccd8bf2", size = 11598153, upload-time = "2026-06-04T00:52:20.646Z" },
{ url = "https://files.pythonhosted.org/packages/b9/af/58bb0089d2635216c8fa6612dd486a3f986d0ab1c46a41527ab95e57f0e3/ty-0.0.43-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7184741f8b15425a1bc64b950ad005cb353573288ac0e8a04f5481ceb3832596", size = 11357811, upload-time = "2026-06-04T00:52:24.683Z" },
{ url = "https://files.pythonhosted.org/packages/d6/9c/32c6b14f3feddf87b59c7a50709e2b3da408258f2f583f05575f77bc8f7b/ty-0.0.43-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8c306379ca9a35f6ae5270fe9bda7af4b46d91822725a2586d78c8b9b5493b62", size = 10772024, upload-time = "2026-06-04T00:52:14.312Z" },
{ url = "https://files.pythonhosted.org/packages/09/fa/98aa4a74bd00cd5efc424923cd1daffbf1e40a0338041cafb203379d746f/ty-0.0.43-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d624b884c9c1fd244ad2a5f026364e7162a22b3f537025941ada2e363e676414", size = 11291034, upload-time = "2026-06-04T00:52:37.249Z" },
{ url = "https://files.pythonhosted.org/packages/b5/db/4de086c38ce96dcada2bd451f43171d2c237f96d8ed19a1ea8fe51bb8ef4/ty-0.0.43-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:281fc4c00fbc196045141faa085055bddc58846b04a2800204701415a1b9c6aa", size = 11364724, upload-time = "2026-06-04T00:52:33.138Z" },
{ url = "https://files.pythonhosted.org/packages/b0/d3/e3cd8e3233a6fd8362a49aa025b79e9f40151a2a86d811ace154c6eb7445/ty-0.0.43-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f57d6cc28de89024b48d1788e4758c05299d5749d4a51c02e71ac655ec23d9a5", size = 11890555, upload-time = "2026-06-04T00:52:22.711Z" },
{ url = "https://files.pythonhosted.org/packages/80/7b/6f46d444e8241606bbde098df3dca93f2ec0b834a42055db85ee7d33646f/ty-0.0.43-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0a1d6ad6c5e7792c7eac0a01e550f2c2004462e01a64a91ea1636aba6fef6e71", size = 12450968, upload-time = "2026-06-04T00:52:28.94Z" },
{ url = "https://files.pythonhosted.org/packages/4a/e1/79fbe51f2e4b9d8347f2013cd7ed0b63f3b499038c02dc0357e9b28a3a47/ty-0.0.43-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:66d474395d7635fb618bdbb58b4e3360259a2056d0a5621b82754b9da2cd8a04", size = 12064187, upload-time = "2026-06-04T00:52:12.039Z" },
{ url = "https://files.pythonhosted.org/packages/9b/3f/c758a3a8df5b90d331f2b60c8f16021ee64d75e78f99d67cc4efc9bf5f4b/ty-0.0.43-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2663a0003a8b60fb98db7f6f6e673df80b21d0fe3a9868a26fb06b4e049b6fc4", size = 11943208, upload-time = "2026-06-04T00:52:31.14Z" },
{ url = "https://files.pythonhosted.org/packages/54/5f/f516442749cf1b45ca6720a5d41df2738a486ed9ace774c03d515db89084/ty-0.0.43-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:d5a6c352d374d889189d5ec82b54b26a5885f769f7b7787f7f875500dcb8673e", size = 12143572, upload-time = "2026-06-04T00:52:18.457Z" },
{ url = "https://files.pythonhosted.org/packages/b7/bf/0d83c7f43bf4c10f3678bfe7d938e51c445298c7b923f155c5204730c2df/ty-0.0.43-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e7dbbeedfad3ca250d74fcc355fa9ab6b38d2a17f22d6304f615716939dbbb27", size = 11279355, upload-time = "2026-06-04T00:52:26.726Z" },
{ url = "https://files.pythonhosted.org/packages/3e/de/a6c978bef6d9e949f79f4782d9e4ee4df0893713e73b055d84c1a5116b9a/ty-0.0.43-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:24b18a0273ee46154996cfcfa27438f851f440c925587ec200df6f98dffe67d3", size = 11408412, upload-time = "2026-06-04T00:52:35.282Z" },
{ url = "https://files.pythonhosted.org/packages/ec/b1/d13857c23867f0f76b92e38e5841c64ca5e76dc5d4bf27f52cb81d8ab685/ty-0.0.43-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2ef681951520d692b7e9c0b5e56aacf4f98ccae47cf6ffccaf2c7b6b33dc226e", size = 11541709, upload-time = "2026-06-04T00:52:16.451Z" },
{ url = "https://files.pythonhosted.org/packages/7c/f1/cd6afc6f6a687e238bf5e12189f7920e81a0bdef6c3dba4c784ef140f7d9/ty-0.0.43-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2af105de7437143aa4676b28016b5bee661aaaa4eff52be5867fb25119641ceb", size = 12041266, upload-time = "2026-06-04T00:52:43.541Z" },
{ url = "https://files.pythonhosted.org/packages/bd/ba/51ca7c3335da2b8d0a3e477fa4986be9f4a53b05bfab862967d8d2e6ca60/ty-0.0.43-py3-none-win32.whl", hash = "sha256:e4773115b0d6486ee30f1657fc8bdffe7e3a3f5300ab77ef2495da6e83e4694f", size = 10858724, upload-time = "2026-06-04T00:52:07.843Z" },
{ url = "https://files.pythonhosted.org/packages/9f/29/5d80453e5f7c520145fa058851da87230dbd7ca761a7675447a9fe504e0b/ty-0.0.43-py3-none-win_amd64.whl", hash = "sha256:48d3545094a4ae6395492c7e6ac90550fce969e0ed2815fbf8c5da9756676b7d", size = 11976157, upload-time = "2026-06-04T00:52:41.438Z" },
{ url = "https://files.pythonhosted.org/packages/dc/ed/befe5a543e5b95e754ed38ee95239e44efda9bc5f578db4ac1bc8dd758d6/ty-0.0.43-py3-none-win_arm64.whl", hash = "sha256:740ca33d7f75f655a4e7d475bc42dfb825c13219bb073fad30fcc04d35790c74", size = 11308680, upload-time = "2026-06-04T00:52:39.233Z" },
]
[[package]]
+1 -1
View File
@@ -236,7 +236,7 @@ def swap_roles(state: InterviewState, name: str):
converted = []
for message in state["messages"]:
if isinstance(message, AIMessage) and message.name != name:
message = HumanMessage(**message.model_dump(exclude={"type"}))
message = HumanMessage(**message.dict(exclude={"type"}))
converted.append(message)
return {"messages": converted}
+9 -12
View File
@@ -21,28 +21,25 @@
"test:all": "yarn test && yarn test:int && yarn lint:langgraph"
},
"dependencies": {
"@langchain/core": "^1.2.4",
"@langchain/langgraph": "^1.4.8"
},
"resolutions": {
"@langchain/langgraph-checkpoint": "1.0.4"
"@langchain/core": "^1.1.48",
"@langchain/langgraph": "^1.3.3"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.6",
"@eslint/eslintrc": "^3.3.5",
"@eslint/js": "^10.0.1",
"@tsconfig/recommended": "^1.0.13",
"@types/jest": "^30.0.0",
"@typescript-eslint/eslint-plugin": "^8.65.0",
"@typescript-eslint/parser": "^8.65.0",
"@typescript-eslint/eslint-plugin": "^8.60.1",
"@typescript-eslint/parser": "^8.60.1",
"dotenv": "^17.4.2",
"eslint": "^10.8.0",
"eslint": "^10.4.1",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-no-instanceof": "^1.0.1",
"eslint-plugin-prettier": "^5.5.6",
"jest": "^30.4.2",
"prettier": "^3.9.6",
"ts-jest": "^29.4.12",
"typescript": "^7.0.2"
"prettier": "^3.8.3",
"ts-jest": "^29.4.11",
"typescript": "^6.0.3"
}
}
+134 -263
View File
@@ -412,10 +412,10 @@
debug "^4.3.1"
minimatch "^10.2.4"
"@eslint/config-helpers@^0.7.0":
version "0.7.0"
resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.7.0.tgz#09ee4aa07b73f059ec2d4c74bf4b2ff02b322377"
integrity sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==
"@eslint/config-helpers@^0.6.0":
version "0.6.0"
resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.6.0.tgz#ef9a36881d39dfd5dbeac22b0da997fabfb08b03"
integrity sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==
dependencies:
"@eslint/core" "^1.2.1"
@@ -426,10 +426,10 @@
dependencies:
"@types/json-schema" "^7.0.15"
"@eslint/eslintrc@^3.3.6":
version "3.3.6"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.3.6.tgz#d22bfd6b3a7d8e1f2c0b2f2e6de111b53ec6e13e"
integrity sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==
"@eslint/eslintrc@^3.3.5":
version "3.3.5"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.3.5.tgz#c131793cfc1a7b96f24a83e0a8bbd4b881558c60"
integrity sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==
dependencies:
ajv "^6.14.0"
debug "^4.3.2"
@@ -437,7 +437,7 @@
globals "^14.0.0"
ignore "^5.2.0"
import-fresh "^3.2.1"
js-yaml "^4.3.0"
js-yaml "^4.1.1"
minimatch "^3.1.5"
strip-json-comments "^3.1.1"
@@ -835,10 +835,10 @@
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
"@langchain/core@^1.2.4":
version "1.2.4"
resolved "https://registry.yarnpkg.com/@langchain/core/-/core-1.2.4.tgz#3916181e00f93b2e3b5801ab8273c07d144d75ce"
integrity sha512-GIrJktdsFPx8gM0C3VyikkeYGZAV7iRIzUJuS5tFatiKLExybou0LI0MuxH9kksN38quyfWdQDl20lIEAEVkVA==
"@langchain/core@^1.1.48":
version "1.1.48"
resolved "https://registry.yarnpkg.com/@langchain/core/-/core-1.1.48.tgz#dfe7f6a64b0e3e79e5357ed4b402c021ee315a7f"
integrity sha512-fQU6Guyb1pwc2fEplmA8FPbKfOMAofjnyJzExevro0FxEiuGHE18Ov/ZHmT9trWCDTZRI9eW1VIc6aChxV8pAQ==
dependencies:
"@cfworker/json-schema" "^4.0.2"
"@standard-schema/spec" "^1.1.0"
@@ -848,37 +848,39 @@
p-queue "^6.6.2"
zod "^3.25.76 || ^4"
"@langchain/langgraph-checkpoint@1.0.4", "@langchain/langgraph-checkpoint@^1.1.3":
"@langchain/langgraph-checkpoint@^1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-1.0.4.tgz#893aa7255b2fe810fdeb50281f6750a3b0a6fef7"
integrity sha512-1y5MgZ0gXXrtmoy56e3kaBChI3GwFPIKl27xkrHwN+VE/3iUsyr9gO3Jtp7kdKAe6diZGbcas5bdC/r0yUwTZA==
dependencies:
uuid "^14.0.0"
"@langchain/langgraph-sdk@~1.9.26":
version "1.9.28"
resolved "https://registry.yarnpkg.com/@langchain/langgraph-sdk/-/langgraph-sdk-1.9.28.tgz#1664cc7b6f2c1509ddffecf17d805a26e70a7e1a"
integrity sha512-4j3XuM0PvtmAbL8mPfBS99ez3+ytRfgbOpAR/nOeaejTRF3Q9dNw2QnaGLGng8wLPtGLoSj+SYgUOVxy9Bv9vg==
"@langchain/langgraph-sdk@~1.9.11":
version "1.9.11"
resolved "https://registry.yarnpkg.com/@langchain/langgraph-sdk/-/langgraph-sdk-1.9.11.tgz#d16f18b2c48e544436489172d86b3c78524eb679"
integrity sha512-mhadkZy4LQ97NJwvATiVIkSxVfOnauXNhrVHFgGnzyqr5zzPLS0VIKJW9xKT+pM8yLqW8Qj6+nPPNhwGUaxoRw==
dependencies:
"@langchain/protocol" "^0.0.18"
"@langchain/protocol" "^0.0.16"
"@types/json-schema" "^7.0.15"
p-queue "^9.0.1"
p-retry "^7.1.1"
uuid "^14.0.0"
"@langchain/langgraph@^1.4.8":
version "1.4.8"
resolved "https://registry.yarnpkg.com/@langchain/langgraph/-/langgraph-1.4.8.tgz#65ef5635b5554cd32f502a3808b92d8371998a99"
integrity sha512-DN1Np1XefdBEbp1qBKlt39cwoL743AAGpR5Ipja0gY2YbWvsoQnOTIrjnj/orSAhaUYsdTKS8VSWdFzsHZo6Ig==
"@langchain/langgraph@^1.3.3":
version "1.3.3"
resolved "https://registry.yarnpkg.com/@langchain/langgraph/-/langgraph-1.3.3.tgz#c7bb19029e37971116e07101be3d8fd4b6483d50"
integrity sha512-8xbpGUQNBcWua7ivT5vUvDnQ+6Qbt0JO8RisgXZ8guPXNqh8plGVvrODW68S4AlJbOYY2yi0ROKtrL/1yN3MBQ==
dependencies:
"@langchain/langgraph-checkpoint" "^1.1.3"
"@langchain/langgraph-sdk" "~1.9.26"
"@langchain/protocol" "^0.0.18"
"@langchain/langgraph-checkpoint" "^1.0.4"
"@langchain/langgraph-sdk" "~1.9.11"
"@langchain/protocol" "^0.0.16"
"@standard-schema/spec" "1.1.0"
uuid "^14.0.0"
"@langchain/protocol@^0.0.18":
version "0.0.18"
resolved "https://registry.yarnpkg.com/@langchain/protocol/-/protocol-0.0.18.tgz#6d96155e7263c958fbce6d4b7241b4725b72fbad"
integrity sha512-XW1egQtPfsGI41w2AMZNFZrUIwFSQHTjVMZs0OaTpCAvht/QLoaPN8FQcsysMVypOhupG28J29yOorrc70otBQ==
"@langchain/protocol@^0.0.16":
version "0.0.16"
resolved "https://registry.yarnpkg.com/@langchain/protocol/-/protocol-0.0.16.tgz#68b63d58feaf4f0acf77405cb8fb16f9c3f09b60"
integrity sha512-ws+J7MaHyhO5dG7f0vdyHQiUn9hoCnki0f3crJPa4MCTGzcRC39jYSCghyrGtBPYQnZbUQiGyRVpW3z3M8IpJg==
"@napi-rs/wasm-runtime@^0.2.11":
version "0.2.12"
@@ -1044,212 +1046,102 @@
dependencies:
"@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@^8.65.0":
version "8.65.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz#0a58df6fea8c0bf6b396f518077099bc8b762bb5"
integrity sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==
"@typescript-eslint/eslint-plugin@^8.60.1":
version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz#c1060bb8fa4be80624d3f3dec8dd9caca373af76"
integrity sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==
dependencies:
"@eslint-community/regexpp" "^4.12.2"
"@typescript-eslint/scope-manager" "8.65.0"
"@typescript-eslint/type-utils" "8.65.0"
"@typescript-eslint/utils" "8.65.0"
"@typescript-eslint/visitor-keys" "8.65.0"
"@typescript-eslint/scope-manager" "8.60.1"
"@typescript-eslint/type-utils" "8.60.1"
"@typescript-eslint/utils" "8.60.1"
"@typescript-eslint/visitor-keys" "8.60.1"
ignore "^7.0.5"
natural-compare "^1.4.0"
ts-api-utils "^2.5.0"
"@typescript-eslint/parser@^8.65.0":
version "8.65.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.65.0.tgz#5295c1058c0a1dd746ef28baaf9c0341dbdf03dc"
integrity sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==
"@typescript-eslint/parser@^8.60.1":
version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.60.1.tgz#a9d7f30850384d34b41f4687dd8944823c09e289"
integrity sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==
dependencies:
"@typescript-eslint/scope-manager" "8.65.0"
"@typescript-eslint/types" "8.65.0"
"@typescript-eslint/typescript-estree" "8.65.0"
"@typescript-eslint/visitor-keys" "8.65.0"
"@typescript-eslint/scope-manager" "8.60.1"
"@typescript-eslint/types" "8.60.1"
"@typescript-eslint/typescript-estree" "8.60.1"
"@typescript-eslint/visitor-keys" "8.60.1"
debug "^4.4.3"
"@typescript-eslint/project-service@8.65.0":
version "8.65.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.65.0.tgz#65fbbc9a1591abffaeab5513200f848271cb0aa5"
integrity sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==
"@typescript-eslint/project-service@8.60.1":
version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.60.1.tgz#eb29712f58d72c222fc727162e92f2ab4670971b"
integrity sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==
dependencies:
"@typescript-eslint/tsconfig-utils" "^8.65.0"
"@typescript-eslint/types" "^8.65.0"
"@typescript-eslint/tsconfig-utils" "^8.60.1"
"@typescript-eslint/types" "^8.60.1"
debug "^4.4.3"
"@typescript-eslint/scope-manager@8.65.0":
version "8.65.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz#9547202ce7e608e7b6283df585703b980a0ea70d"
integrity sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==
"@typescript-eslint/scope-manager@8.60.1":
version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.60.1.tgz#2f875962eaad0a0789cc3c36aea9b4ddeb2dd9c8"
integrity sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==
dependencies:
"@typescript-eslint/types" "8.65.0"
"@typescript-eslint/visitor-keys" "8.65.0"
"@typescript-eslint/types" "8.60.1"
"@typescript-eslint/visitor-keys" "8.60.1"
"@typescript-eslint/tsconfig-utils@8.65.0":
version "8.65.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz#36f168fcdbb1295f7446ff0379667f98c3cf1bf3"
integrity sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==
"@typescript-eslint/tsconfig-utils@8.60.1", "@typescript-eslint/tsconfig-utils@^8.60.1":
version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz#bee8b942a13679a878101c9c74577d732062ed93"
integrity sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==
"@typescript-eslint/tsconfig-utils@^8.65.0":
version "8.66.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.66.0.tgz#3a89066c507aa30541dc176804685b4b444e1e52"
integrity sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g==
"@typescript-eslint/type-utils@8.65.0":
version "8.65.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz#d316d7522d93cff4cd14f305e02f3df2d804f9c1"
integrity sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==
"@typescript-eslint/type-utils@8.60.1":
version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.60.1.tgz#1ae45f0f2a701354beea4a58c2161e40a5e3c379"
integrity sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==
dependencies:
"@typescript-eslint/types" "8.65.0"
"@typescript-eslint/typescript-estree" "8.65.0"
"@typescript-eslint/utils" "8.65.0"
"@typescript-eslint/types" "8.60.1"
"@typescript-eslint/typescript-estree" "8.60.1"
"@typescript-eslint/utils" "8.60.1"
debug "^4.4.3"
ts-api-utils "^2.5.0"
"@typescript-eslint/types@8.65.0":
version "8.65.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.65.0.tgz#3e86738416a777c8b8925ab46745f48ecf904c9f"
integrity sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==
"@typescript-eslint/types@8.60.1", "@typescript-eslint/types@^8.60.1":
version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.60.1.tgz#ccdc482ba9e17f9723a10ce240b5e67dad3046c4"
integrity sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==
"@typescript-eslint/types@^8.65.0":
version "8.66.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.66.0.tgz#3cacab94d3b564c1d48c56eb37b89f89a6d48479"
integrity sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==
"@typescript-eslint/typescript-estree@8.65.0":
version "8.65.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz#f1f514808f6aa713e2d678ae8ff592a65e1632af"
integrity sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==
"@typescript-eslint/typescript-estree@8.60.1":
version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz#016630b119228bf483ddc652703a6a038f3fdd74"
integrity sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==
dependencies:
"@typescript-eslint/project-service" "8.65.0"
"@typescript-eslint/tsconfig-utils" "8.65.0"
"@typescript-eslint/types" "8.65.0"
"@typescript-eslint/visitor-keys" "8.65.0"
"@typescript-eslint/project-service" "8.60.1"
"@typescript-eslint/tsconfig-utils" "8.60.1"
"@typescript-eslint/types" "8.60.1"
"@typescript-eslint/visitor-keys" "8.60.1"
debug "^4.4.3"
minimatch "^10.2.2"
semver "^7.7.3"
tinyglobby "^0.2.15"
ts-api-utils "^2.5.0"
"@typescript-eslint/utils@8.65.0":
version "8.65.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.65.0.tgz#afedd974a0c8deeef553b509df5800bafd615a72"
integrity sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==
"@typescript-eslint/utils@8.60.1":
version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.60.1.tgz#31cf566095602d9fe8ad91837d2eb520b8de762b"
integrity sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==
dependencies:
"@eslint-community/eslint-utils" "^4.9.1"
"@typescript-eslint/scope-manager" "8.65.0"
"@typescript-eslint/types" "8.65.0"
"@typescript-eslint/typescript-estree" "8.65.0"
"@typescript-eslint/scope-manager" "8.60.1"
"@typescript-eslint/types" "8.60.1"
"@typescript-eslint/typescript-estree" "8.60.1"
"@typescript-eslint/visitor-keys@8.65.0":
version "8.65.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz#e3704c13cb4a1c22454c1abf28ff4737e15018c6"
integrity sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==
"@typescript-eslint/visitor-keys@8.60.1":
version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.1.tgz#165d1d8901137b944efaf18f00ab5ecb57f06995"
integrity sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==
dependencies:
"@typescript-eslint/types" "8.65.0"
"@typescript-eslint/types" "8.60.1"
eslint-visitor-keys "^5.0.0"
"@typescript/typescript-aix-ppc64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz#cdc7ce81d60f1e09034960ddfb1fb880d7a776b6"
integrity sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==
"@typescript/typescript-darwin-arm64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz#a55fdfcfa58df58d27db2237cde6a5c1e35a7235"
integrity sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==
"@typescript/typescript-darwin-x64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz#38d1c9172800a91d707bec64d2a370a016634db4"
integrity sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==
"@typescript/typescript-freebsd-arm64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz#f1ff8810030b35d2b5be0db6a2dc650460ea94fa"
integrity sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==
"@typescript/typescript-freebsd-x64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz#3d86b03f353c5b1ba95162eb6ce35533bfc294bd"
integrity sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==
"@typescript/typescript-linux-arm64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz#d9334d96d6dac6ff85da9c865588948de939e91f"
integrity sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==
"@typescript/typescript-linux-arm@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz#ad94b41e1aee2a4dcc6a298c7b67c43345fde32e"
integrity sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==
"@typescript/typescript-linux-loong64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz#2965aee4fc873360139d893daafe6397a29138ad"
integrity sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==
"@typescript/typescript-linux-mips64el@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz#1a887a311bed3a833f80bfd4a9ed37c271936cf0"
integrity sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==
"@typescript/typescript-linux-ppc64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz#8b63c9b2f445b393eb4e43ec21da225dade3577d"
integrity sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==
"@typescript/typescript-linux-riscv64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz#b6e8a35c289b3ea97a92a41d461aaeed0d3b36e1"
integrity sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==
"@typescript/typescript-linux-s390x@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz#2ef96693be4861f6d17965427e5b009cbbed1a3e"
integrity sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==
"@typescript/typescript-linux-x64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz#73269cb0baba50aea0ca060445a6b88e583f1ce2"
integrity sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==
"@typescript/typescript-netbsd-arm64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz#3a3649f97fafa210b4e6e3798c15e06605c8a901"
integrity sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==
"@typescript/typescript-netbsd-x64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz#47ec59491a40c470d2807dc4d2b825528fd979ab"
integrity sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==
"@typescript/typescript-openbsd-arm64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz#796be8da0bd989d8a3fb96f2801e38a8365b4baf"
integrity sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==
"@typescript/typescript-openbsd-x64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz#d37fe2a729eb942c076c454ee7f1815faf7d560f"
integrity sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==
"@typescript/typescript-sunos-x64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz#aba8d3464c3565a7044789baba96916bd4ab2c88"
integrity sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==
"@typescript/typescript-win32-arm64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz#b9de50a17196383f62620b5f9d0a2f34ad3b60d7"
integrity sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==
"@typescript/typescript-win32-x64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz#cf3b7b0d6ce5635daca4c8e01c189cdcde47ec3c"
integrity sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==
"@ungap/structured-clone@^1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8"
@@ -1637,10 +1529,10 @@ brace-expansion@^2.0.2:
dependencies:
balanced-match "^1.0.0"
brace-expansion@^5.0.8:
version "5.0.9"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.9.tgz#7c72438809b5fa5babf54199a1f1c281a6984fcf"
integrity sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==
brace-expansion@^5.0.2:
version "5.0.2"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.2.tgz#b6c16d0791087af6c2bc463f52a8142046c06b6f"
integrity sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==
dependencies:
balanced-match "^4.0.2"
@@ -2294,15 +2186,15 @@ eslint-visitor-keys@^5.0.0, eslint-visitor-keys@^5.0.1:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz#9e3c9489697824d2d4ce3a8ad12628f91e9f59be"
integrity sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==
eslint@^10.8.0:
version "10.8.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-10.8.0.tgz#e6d19907a3f090a53a022261ba34c5ff6d04908b"
integrity sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==
eslint@^10.4.1:
version "10.4.1"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-10.4.1.tgz#f6640b176e0912246d9ddbf8fcfa5e8b7f02445a"
integrity sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw==
dependencies:
"@eslint-community/eslint-utils" "^4.8.0"
"@eslint-community/regexpp" "^4.12.2"
"@eslint/config-array" "^0.23.5"
"@eslint/config-helpers" "^0.7.0"
"@eslint/config-helpers" "^0.6.0"
"@eslint/core" "^1.2.1"
"@eslint/plugin-kit" "^0.7.2"
"@humanfs/node" "^0.16.6"
@@ -2326,7 +2218,7 @@ eslint@^10.8.0:
imurmurhash "^0.1.4"
is-glob "^4.0.0"
json-stable-stringify-without-jsonify "^1.0.1"
minimatch "^10.2.5"
minimatch "^10.2.4"
natural-compare "^1.4.0"
optionator "^0.9.3"
@@ -3651,10 +3543,10 @@ js-yaml@^3.13.1:
argparse "^1.0.7"
esprima "^4.0.0"
js-yaml@^4.3.0:
version "4.3.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.3.1.tgz#01216c001d67f48e2cd560d708c7af21090a3848"
integrity sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==
js-yaml@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b"
integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==
dependencies:
argparse "^2.0.1"
@@ -3800,12 +3692,12 @@ mimic-fn@^2.1.0:
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
minimatch@^10.2.2, minimatch@^10.2.4, minimatch@^10.2.5:
version "10.2.6"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.6.tgz#fd956bbe0b77241e9f15ac5dccb1c638060968ef"
integrity sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==
minimatch@^10.2.2, minimatch@^10.2.4:
version "10.2.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.4.tgz#465b3accbd0218b8281f5301e27cedc697f96fde"
integrity sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==
dependencies:
brace-expansion "^5.0.8"
brace-expansion "^5.0.2"
minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2, minimatch@^3.1.5:
version "3.1.5"
@@ -4151,10 +4043,10 @@ prettier-linter-helpers@^1.0.1:
dependencies:
fast-diff "^1.1.2"
prettier@^3.9.6:
version "3.9.6"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.9.6.tgz#b3ea5146515d40fc53f18aa63f74dfab1e10dbf6"
integrity sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==
prettier@^3.8.3:
version "3.8.3"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.8.3.tgz#560f2de55bf01b4c0503bc629d5df99b9a1d09b0"
integrity sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==
pretty-format@30.2.0, pretty-format@^30.0.0:
version "30.2.0"
@@ -4319,10 +4211,10 @@ semver@^6.3.1:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
semver@^7.5.3, semver@^7.5.4, semver@^7.7.2, semver@^7.7.3, semver@^7.8.5:
version "7.8.5"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.8.5.tgz#39b646037dd50c14fb451e7e4cac58ed8b863f69"
integrity sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==
semver@^7.5.3, semver@^7.5.4, semver@^7.7.2, semver@^7.7.3, semver@^7.8.0:
version "7.8.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.8.1.tgz#bf4970b5e70fda0686363cc18bfe8805d5ed957e"
integrity sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==
set-function-length@^1.2.1, set-function-length@^1.2.2:
version "1.2.2"
@@ -4655,10 +4547,10 @@ ts-api-utils@^2.5.0:
resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz#4acd4a155e22734990a5ed1fe9e97f113bcb37c1"
integrity sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==
ts-jest@^29.4.12:
version "29.4.12"
resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.4.12.tgz#c34cd91f02d364e9286d2c016843315fd0efff76"
integrity sha512-Ov6ClY53Fflh6BGAnY2DlTq1hYDrTycz2PVTXBWFW2CU+9zrEqAp9fWdGXl42EXO5RLSFAcAZ2JFKbP+zBTFfw==
ts-jest@^29.4.11:
version "29.4.11"
resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.4.11.tgz#42f5de21c37ccc01a580253afae6955abbf4d0b3"
integrity sha512-IrFl7l9AuB/qrNw5quqvAv/hmKMb8dhWOH4jQOGo0Oq8tCeo1O86/iTFG1FaRimgUkF13l4PcepO8ATFT6Ns4g==
dependencies:
bs-logger "^0.2.6"
fast-json-stable-stringify "^2.1.0"
@@ -4666,7 +4558,7 @@ ts-jest@^29.4.12:
json5 "^2.2.3"
lodash.memoize "^4.1.2"
make-error "^1.3.6"
semver "^7.8.5"
semver "^7.8.0"
type-fest "^4.41.0"
yargs-parser "^21.1.1"
@@ -4796,31 +4688,10 @@ typed-array-length@^1.0.7:
possible-typed-array-names "^1.0.0"
reflect.getprototypeof "^1.0.6"
typescript@^7.0.2:
version "7.0.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-7.0.2.tgz#9ec773d7954a8c182c17cc5bbd575aa28bc51582"
integrity sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==
optionalDependencies:
"@typescript/typescript-aix-ppc64" "7.0.2"
"@typescript/typescript-darwin-arm64" "7.0.2"
"@typescript/typescript-darwin-x64" "7.0.2"
"@typescript/typescript-freebsd-arm64" "7.0.2"
"@typescript/typescript-freebsd-x64" "7.0.2"
"@typescript/typescript-linux-arm" "7.0.2"
"@typescript/typescript-linux-arm64" "7.0.2"
"@typescript/typescript-linux-loong64" "7.0.2"
"@typescript/typescript-linux-mips64el" "7.0.2"
"@typescript/typescript-linux-ppc64" "7.0.2"
"@typescript/typescript-linux-riscv64" "7.0.2"
"@typescript/typescript-linux-s390x" "7.0.2"
"@typescript/typescript-linux-x64" "7.0.2"
"@typescript/typescript-netbsd-arm64" "7.0.2"
"@typescript/typescript-netbsd-x64" "7.0.2"
"@typescript/typescript-openbsd-arm64" "7.0.2"
"@typescript/typescript-openbsd-x64" "7.0.2"
"@typescript/typescript-sunos-x64" "7.0.2"
"@typescript/typescript-win32-arm64" "7.0.2"
"@typescript/typescript-win32-x64" "7.0.2"
typescript@^6.0.3:
version "6.0.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-6.0.3.tgz#90251dc007916e972786cb94d74d15b185577d21"
integrity sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==
uglify-js@^3.1.4:
version "3.19.3"
@@ -4895,9 +4766,9 @@ uri-js@^4.2.2:
punycode "^2.1.0"
uuid@^14.0.0:
version "14.0.1"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-14.0.1.tgz#8a5975b3e038902bfd169a10b5202f5ec0cf3faf"
integrity sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==
version "14.0.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-14.0.0.tgz#0af883220163d264ffe0c084f6b8a89b9666966d"
integrity sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==
v8-to-istanbul@^9.0.1:
version "9.3.0"
@@ -9,10 +9,10 @@
},
"dependencies": {
"@js-monorepo-example/shared": "*",
"@langchain/core": "^1.2.4",
"@langchain/langgraph": "^1.4.8"
"@langchain/core": "^1.1.48",
"@langchain/langgraph": "^1.3.3"
},
"devDependencies": {
"typescript": "^7.0.2"
"typescript": "^6.0.3"
}
}
@@ -9,6 +9,6 @@
"clean": "rm -rf dist"
},
"devDependencies": {
"typescript": "^7.0.2"
"typescript": "^6.0.3"
}
}
+7 -7
View File
@@ -17,18 +17,18 @@
"lint": "eslint 'apps/**/*.ts' 'libs/**/*.ts'"
},
"devDependencies": {
"turbo": "^2.10.8",
"typescript": "^7.0.2",
"turbo": "^2.9.16",
"typescript": "^6.0.3",
"@tsconfig/recommended": "^1.0.13",
"@eslint/eslintrc": "^3.3.6",
"@eslint/eslintrc": "^3.3.5",
"@eslint/js": "^10.0.1",
"eslint": "^10.8.0",
"eslint": "^10.4.1",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-no-instanceof": "^1.0.1",
"eslint-plugin-prettier": "^5.5.6",
"@typescript-eslint/eslint-plugin": "^8.65.0",
"@typescript-eslint/parser": "^8.65.0",
"prettier": "^3.9.6"
"@typescript-eslint/eslint-plugin": "^8.60.1",
"@typescript-eslint/parser": "^8.60.1",
"prettier": "^3.8.3"
}
}
+167 -289
View File
@@ -28,10 +28,10 @@
debug "^4.3.1"
minimatch "^10.2.4"
"@eslint/config-helpers@^0.7.0":
version "0.7.0"
resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.7.0.tgz#09ee4aa07b73f059ec2d4c74bf4b2ff02b322377"
integrity sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==
"@eslint/config-helpers@^0.6.0":
version "0.6.0"
resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.6.0.tgz#ef9a36881d39dfd5dbeac22b0da997fabfb08b03"
integrity sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==
dependencies:
"@eslint/core" "^1.2.1"
@@ -42,10 +42,10 @@
dependencies:
"@types/json-schema" "^7.0.15"
"@eslint/eslintrc@^3.3.6":
version "3.3.6"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.3.6.tgz#d22bfd6b3a7d8e1f2c0b2f2e6de111b53ec6e13e"
integrity sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==
"@eslint/eslintrc@^3.3.5":
version "3.3.5"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.3.5.tgz#c131793cfc1a7b96f24a83e0a8bbd4b881558c60"
integrity sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==
dependencies:
ajv "^6.14.0"
debug "^4.3.2"
@@ -53,7 +53,7 @@
globals "^14.0.0"
ignore "^5.2.0"
import-fresh "^3.2.1"
js-yaml "^4.3.0"
js-yaml "^4.1.1"
minimatch "^3.1.5"
strip-json-comments "^3.1.1"
@@ -103,10 +103,10 @@
resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-9.0.0.tgz#4d0a3f127058043bf2e7ee169eaf30ed901302f3"
integrity sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==
"@langchain/core@^1.2.4":
version "1.2.4"
resolved "https://registry.yarnpkg.com/@langchain/core/-/core-1.2.4.tgz#3916181e00f93b2e3b5801ab8273c07d144d75ce"
integrity sha512-GIrJktdsFPx8gM0C3VyikkeYGZAV7iRIzUJuS5tFatiKLExybou0LI0MuxH9kksN38quyfWdQDl20lIEAEVkVA==
"@langchain/core@^1.1.48":
version "1.1.48"
resolved "https://registry.yarnpkg.com/@langchain/core/-/core-1.1.48.tgz#dfe7f6a64b0e3e79e5357ed4b402c021ee315a7f"
integrity sha512-fQU6Guyb1pwc2fEplmA8FPbKfOMAofjnyJzExevro0FxEiuGHE18Ov/ZHmT9trWCDTZRI9eW1VIc6aChxV8pAQ==
dependencies:
"@cfworker/json-schema" "^4.0.2"
"@standard-schema/spec" "^1.1.0"
@@ -116,35 +116,39 @@
p-queue "^6.6.2"
zod "^3.25.76 || ^4"
"@langchain/langgraph-checkpoint@^1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-1.1.3.tgz#891f2f9a2e96cf2ab0920f9240f9ad1e0a86a4ab"
integrity sha512-wgzdQNeEsdw1e+4lvlj0tdq/RYR/k1vPin10g0ymGoehZDDgd9nvIllGXSXN4TFgF9sf5qQP/KTkOcLfeseIhA==
"@langchain/langgraph-sdk@~1.9.26":
version "1.9.28"
resolved "https://registry.yarnpkg.com/@langchain/langgraph-sdk/-/langgraph-sdk-1.9.28.tgz#1664cc7b6f2c1509ddffecf17d805a26e70a7e1a"
integrity sha512-4j3XuM0PvtmAbL8mPfBS99ez3+ytRfgbOpAR/nOeaejTRF3Q9dNw2QnaGLGng8wLPtGLoSj+SYgUOVxy9Bv9vg==
"@langchain/langgraph-checkpoint@^1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-1.0.4.tgz#893aa7255b2fe810fdeb50281f6750a3b0a6fef7"
integrity sha512-1y5MgZ0gXXrtmoy56e3kaBChI3GwFPIKl27xkrHwN+VE/3iUsyr9gO3Jtp7kdKAe6diZGbcas5bdC/r0yUwTZA==
dependencies:
"@langchain/protocol" "^0.0.18"
uuid "^14.0.0"
"@langchain/langgraph-sdk@~1.9.11":
version "1.9.11"
resolved "https://registry.yarnpkg.com/@langchain/langgraph-sdk/-/langgraph-sdk-1.9.11.tgz#d16f18b2c48e544436489172d86b3c78524eb679"
integrity sha512-mhadkZy4LQ97NJwvATiVIkSxVfOnauXNhrVHFgGnzyqr5zzPLS0VIKJW9xKT+pM8yLqW8Qj6+nPPNhwGUaxoRw==
dependencies:
"@langchain/protocol" "^0.0.16"
"@types/json-schema" "^7.0.15"
p-queue "^9.0.1"
p-retry "^7.1.1"
uuid "^14.0.0"
"@langchain/langgraph@^1.4.8":
version "1.4.8"
resolved "https://registry.yarnpkg.com/@langchain/langgraph/-/langgraph-1.4.8.tgz#65ef5635b5554cd32f502a3808b92d8371998a99"
integrity sha512-DN1Np1XefdBEbp1qBKlt39cwoL743AAGpR5Ipja0gY2YbWvsoQnOTIrjnj/orSAhaUYsdTKS8VSWdFzsHZo6Ig==
"@langchain/langgraph@^1.3.3":
version "1.3.3"
resolved "https://registry.yarnpkg.com/@langchain/langgraph/-/langgraph-1.3.3.tgz#c7bb19029e37971116e07101be3d8fd4b6483d50"
integrity sha512-8xbpGUQNBcWua7ivT5vUvDnQ+6Qbt0JO8RisgXZ8guPXNqh8plGVvrODW68S4AlJbOYY2yi0ROKtrL/1yN3MBQ==
dependencies:
"@langchain/langgraph-checkpoint" "^1.1.3"
"@langchain/langgraph-sdk" "~1.9.26"
"@langchain/protocol" "^0.0.18"
"@langchain/langgraph-checkpoint" "^1.0.4"
"@langchain/langgraph-sdk" "~1.9.11"
"@langchain/protocol" "^0.0.16"
"@standard-schema/spec" "1.1.0"
uuid "^14.0.0"
"@langchain/protocol@^0.0.18":
version "0.0.18"
resolved "https://registry.yarnpkg.com/@langchain/protocol/-/protocol-0.0.18.tgz#6d96155e7263c958fbce6d4b7241b4725b72fbad"
integrity sha512-XW1egQtPfsGI41w2AMZNFZrUIwFSQHTjVMZs0OaTpCAvht/QLoaPN8FQcsysMVypOhupG28J29yOorrc70otBQ==
"@langchain/protocol@^0.0.16":
version "0.0.16"
resolved "https://registry.yarnpkg.com/@langchain/protocol/-/protocol-0.0.16.tgz#68b63d58feaf4f0acf77405cb8fb16f9c3f09b60"
integrity sha512-ws+J7MaHyhO5dG7f0vdyHQiUn9hoCnki0f3crJPa4MCTGzcRC39jYSCghyrGtBPYQnZbUQiGyRVpW3z3M8IpJg==
"@pkgr/core@^0.3.6":
version "0.3.6"
@@ -166,35 +170,35 @@
resolved "https://registry.yarnpkg.com/@tsconfig/recommended/-/recommended-1.0.13.tgz#269fce3ad04ca70b93269ff44cca81b950f542da"
integrity sha512-sySRuBfMKyKO/j2ZAhR8kSembhjuPEV4Ra3AHtmWLq51+iGaudr45crPSzNC5b7/Ctrh9dfUpBuTlYrH6rM58Q==
"@turbo/darwin-64@2.10.8":
version "2.10.8"
resolved "https://registry.yarnpkg.com/@turbo/darwin-64/-/darwin-64-2.10.8.tgz#0ae7cde6b95b82b0a4fe406ffea64bdfcf68a0ee"
integrity sha512-po+7rfJfUnFXjWlcoN2RwhErgzCdRtBc1T26vYPcywHlggmCQiQe1uWaE4j+BibI2uY9/2pDoFzMN0rmSaPFOw==
"@turbo/darwin-64@2.9.16":
version "2.9.16"
resolved "https://registry.yarnpkg.com/@turbo/darwin-64/-/darwin-64-2.9.16.tgz#f4146eab98e020d658a9a5d7feb846acba653546"
integrity sha512-jLjApWTSNd7JZ5JaLYfelW1ytnGQOvB7ivl+2RD1xQvJTbi8I9gBjzcga7tDZVPyaxpl10YTfJt3BrYXR18KDw==
"@turbo/darwin-arm64@2.10.8":
version "2.10.8"
resolved "https://registry.yarnpkg.com/@turbo/darwin-arm64/-/darwin-arm64-2.10.8.tgz#18a9127c0a0d9c5ad7de49bdb27e7032f8946334"
integrity sha512-+zB2btDJ00lnPRuqOvpVvgl4x34k/djZQGZTTCfjn7JgNCl8QFY5Njo5+dqkY1g/+9gbbsnAvWm9CmJg9ebcXA==
"@turbo/darwin-arm64@2.9.16":
version "2.9.16"
resolved "https://registry.yarnpkg.com/@turbo/darwin-arm64/-/darwin-arm64-2.9.16.tgz#1d01ca232fc189045d6a4dba4741f462e0343b8c"
integrity sha512-YPgrn+5HIGzrx0O2a631SV4MBQUe4W/DafMFUuBVgaU32PW9/OTT0ehviF0QSxTXuRJlHvW2eUTemddF5/spmw==
"@turbo/linux-64@2.10.8":
version "2.10.8"
resolved "https://registry.yarnpkg.com/@turbo/linux-64/-/linux-64-2.10.8.tgz#1677672f5760d272c0a0608686fe8b8b29491fdb"
integrity sha512-K1dxqiVisyN7cViVsfQLs6xscQbYuI8aO2nbUhFURDACgEDfZRdP/b4CCxeosBJpcMfhYyiibWqJorCnvz9kKg==
"@turbo/linux-64@2.9.16":
version "2.9.16"
resolved "https://registry.yarnpkg.com/@turbo/linux-64/-/linux-64-2.9.16.tgz#8db630e6e5681a2ea48f7df1bca57bbd9bdf3e25"
integrity sha512-vAEf1H6l26lTpl9FJ/peQo1NUB8RC0sbEJJz5mPcUhHA2bPDup2x3CZPgo/bH8S4cUcBLm4FN3UHd5iUO2RAew==
"@turbo/linux-arm64@2.10.8":
version "2.10.8"
resolved "https://registry.yarnpkg.com/@turbo/linux-arm64/-/linux-arm64-2.10.8.tgz#dbdb5aef9e53bb4c88357dafc19f79138ec29253"
integrity sha512-Gi77ibVnrE1fEmvr+/wBD/yvRqhwp/RQuCp2+//lv1U1wNFFyVg0V7Wj8FG9FXPFAw5QHReo8rxc9+wBSDZjzA==
"@turbo/linux-arm64@2.9.16":
version "2.9.16"
resolved "https://registry.yarnpkg.com/@turbo/linux-arm64/-/linux-arm64-2.9.16.tgz#d1de82fc8075c4774b214dd2f5e8e2570bb03643"
integrity sha512-xDBLR2PZg4BrQOchfG6svgpv5FCNJ2TOtT2psLdEJcdKo1BH+pnPs9Xj6pvUjgfkHbuvBOfeE4R6tvxMoQKDHQ==
"@turbo/windows-64@2.10.8":
version "2.10.8"
resolved "https://registry.yarnpkg.com/@turbo/windows-64/-/windows-64-2.10.8.tgz#451c3b5421c9ce573c764b1efacae5ad4d1312ee"
integrity sha512-znnLO1haJPYTHoKMKwlAvlkjRiYbbhBzME6wIGaMd+fwir23U6jVd1ecaTWWi1fbnRVqxMfgDBKseQ/hLKb83g==
"@turbo/windows-64@2.9.16":
version "2.9.16"
resolved "https://registry.yarnpkg.com/@turbo/windows-64/-/windows-64-2.9.16.tgz#e369c51edd737978651c2f6b4ce49b582976d66f"
integrity sha512-NBAJnaUiGdgkSzQwUIdOvkCkcpTSu58G/sBGa0mvBtzfvFOOgrQwepKOOQ8cp6sWM6OcKDNFj2p1dsZA1OWjPg==
"@turbo/windows-arm64@2.10.8":
version "2.10.8"
resolved "https://registry.yarnpkg.com/@turbo/windows-arm64/-/windows-arm64-2.10.8.tgz#5661ca9b75713aece06d319493e48a366a4cd05c"
integrity sha512-VN30vh3b3Czh2WzYHNTfF1FE0YMZ5aHsLO8dBMGHJewA6792wX6iJR8ZxlzFW6WdOu0gEAKIvlYhfyT81Wkm4Q==
"@turbo/windows-arm64@2.9.16":
version "2.9.16"
resolved "https://registry.yarnpkg.com/@turbo/windows-arm64/-/windows-arm64-2.9.16.tgz#dbb2b2863e80eb9ca8da8bd8da63e2cc457e470c"
integrity sha512-Y7SJppD0Z8wjO3Ec0ZGd9KQ4Yv0BMnA8CIowj5Vp+OEVsosXDG2weK6/t1RRLfJmc2Ozrnd6y4DOgQys+mn3WQ==
"@types/esrecurse@^4.3.1":
version "4.3.1"
@@ -216,212 +220,102 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
"@typescript-eslint/eslint-plugin@^8.65.0":
version "8.65.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz#0a58df6fea8c0bf6b396f518077099bc8b762bb5"
integrity sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==
"@typescript-eslint/eslint-plugin@^8.60.1":
version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz#c1060bb8fa4be80624d3f3dec8dd9caca373af76"
integrity sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==
dependencies:
"@eslint-community/regexpp" "^4.12.2"
"@typescript-eslint/scope-manager" "8.65.0"
"@typescript-eslint/type-utils" "8.65.0"
"@typescript-eslint/utils" "8.65.0"
"@typescript-eslint/visitor-keys" "8.65.0"
"@typescript-eslint/scope-manager" "8.60.1"
"@typescript-eslint/type-utils" "8.60.1"
"@typescript-eslint/utils" "8.60.1"
"@typescript-eslint/visitor-keys" "8.60.1"
ignore "^7.0.5"
natural-compare "^1.4.0"
ts-api-utils "^2.5.0"
"@typescript-eslint/parser@^8.65.0":
version "8.65.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.65.0.tgz#5295c1058c0a1dd746ef28baaf9c0341dbdf03dc"
integrity sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==
"@typescript-eslint/parser@^8.60.1":
version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.60.1.tgz#a9d7f30850384d34b41f4687dd8944823c09e289"
integrity sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==
dependencies:
"@typescript-eslint/scope-manager" "8.65.0"
"@typescript-eslint/types" "8.65.0"
"@typescript-eslint/typescript-estree" "8.65.0"
"@typescript-eslint/visitor-keys" "8.65.0"
"@typescript-eslint/scope-manager" "8.60.1"
"@typescript-eslint/types" "8.60.1"
"@typescript-eslint/typescript-estree" "8.60.1"
"@typescript-eslint/visitor-keys" "8.60.1"
debug "^4.4.3"
"@typescript-eslint/project-service@8.65.0":
version "8.65.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.65.0.tgz#65fbbc9a1591abffaeab5513200f848271cb0aa5"
integrity sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==
"@typescript-eslint/project-service@8.60.1":
version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.60.1.tgz#eb29712f58d72c222fc727162e92f2ab4670971b"
integrity sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==
dependencies:
"@typescript-eslint/tsconfig-utils" "^8.65.0"
"@typescript-eslint/types" "^8.65.0"
"@typescript-eslint/tsconfig-utils" "^8.60.1"
"@typescript-eslint/types" "^8.60.1"
debug "^4.4.3"
"@typescript-eslint/scope-manager@8.65.0":
version "8.65.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz#9547202ce7e608e7b6283df585703b980a0ea70d"
integrity sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==
"@typescript-eslint/scope-manager@8.60.1":
version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.60.1.tgz#2f875962eaad0a0789cc3c36aea9b4ddeb2dd9c8"
integrity sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==
dependencies:
"@typescript-eslint/types" "8.65.0"
"@typescript-eslint/visitor-keys" "8.65.0"
"@typescript-eslint/types" "8.60.1"
"@typescript-eslint/visitor-keys" "8.60.1"
"@typescript-eslint/tsconfig-utils@8.65.0":
version "8.65.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz#36f168fcdbb1295f7446ff0379667f98c3cf1bf3"
integrity sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==
"@typescript-eslint/tsconfig-utils@8.60.1", "@typescript-eslint/tsconfig-utils@^8.60.1":
version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz#bee8b942a13679a878101c9c74577d732062ed93"
integrity sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==
"@typescript-eslint/tsconfig-utils@^8.65.0":
version "8.66.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.66.0.tgz#3a89066c507aa30541dc176804685b4b444e1e52"
integrity sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g==
"@typescript-eslint/type-utils@8.65.0":
version "8.65.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz#d316d7522d93cff4cd14f305e02f3df2d804f9c1"
integrity sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==
"@typescript-eslint/type-utils@8.60.1":
version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.60.1.tgz#1ae45f0f2a701354beea4a58c2161e40a5e3c379"
integrity sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==
dependencies:
"@typescript-eslint/types" "8.65.0"
"@typescript-eslint/typescript-estree" "8.65.0"
"@typescript-eslint/utils" "8.65.0"
"@typescript-eslint/types" "8.60.1"
"@typescript-eslint/typescript-estree" "8.60.1"
"@typescript-eslint/utils" "8.60.1"
debug "^4.4.3"
ts-api-utils "^2.5.0"
"@typescript-eslint/types@8.65.0":
version "8.65.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.65.0.tgz#3e86738416a777c8b8925ab46745f48ecf904c9f"
integrity sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==
"@typescript-eslint/types@8.60.1", "@typescript-eslint/types@^8.60.1":
version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.60.1.tgz#ccdc482ba9e17f9723a10ce240b5e67dad3046c4"
integrity sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==
"@typescript-eslint/types@^8.65.0":
version "8.66.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.66.0.tgz#3cacab94d3b564c1d48c56eb37b89f89a6d48479"
integrity sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==
"@typescript-eslint/typescript-estree@8.65.0":
version "8.65.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz#f1f514808f6aa713e2d678ae8ff592a65e1632af"
integrity sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==
"@typescript-eslint/typescript-estree@8.60.1":
version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz#016630b119228bf483ddc652703a6a038f3fdd74"
integrity sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==
dependencies:
"@typescript-eslint/project-service" "8.65.0"
"@typescript-eslint/tsconfig-utils" "8.65.0"
"@typescript-eslint/types" "8.65.0"
"@typescript-eslint/visitor-keys" "8.65.0"
"@typescript-eslint/project-service" "8.60.1"
"@typescript-eslint/tsconfig-utils" "8.60.1"
"@typescript-eslint/types" "8.60.1"
"@typescript-eslint/visitor-keys" "8.60.1"
debug "^4.4.3"
minimatch "^10.2.2"
semver "^7.7.3"
tinyglobby "^0.2.15"
ts-api-utils "^2.5.0"
"@typescript-eslint/utils@8.65.0":
version "8.65.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.65.0.tgz#afedd974a0c8deeef553b509df5800bafd615a72"
integrity sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==
"@typescript-eslint/utils@8.60.1":
version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.60.1.tgz#31cf566095602d9fe8ad91837d2eb520b8de762b"
integrity sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==
dependencies:
"@eslint-community/eslint-utils" "^4.9.1"
"@typescript-eslint/scope-manager" "8.65.0"
"@typescript-eslint/types" "8.65.0"
"@typescript-eslint/typescript-estree" "8.65.0"
"@typescript-eslint/scope-manager" "8.60.1"
"@typescript-eslint/types" "8.60.1"
"@typescript-eslint/typescript-estree" "8.60.1"
"@typescript-eslint/visitor-keys@8.65.0":
version "8.65.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz#e3704c13cb4a1c22454c1abf28ff4737e15018c6"
integrity sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==
"@typescript-eslint/visitor-keys@8.60.1":
version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.1.tgz#165d1d8901137b944efaf18f00ab5ecb57f06995"
integrity sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==
dependencies:
"@typescript-eslint/types" "8.65.0"
"@typescript-eslint/types" "8.60.1"
eslint-visitor-keys "^5.0.0"
"@typescript/typescript-aix-ppc64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz#cdc7ce81d60f1e09034960ddfb1fb880d7a776b6"
integrity sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==
"@typescript/typescript-darwin-arm64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz#a55fdfcfa58df58d27db2237cde6a5c1e35a7235"
integrity sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==
"@typescript/typescript-darwin-x64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz#38d1c9172800a91d707bec64d2a370a016634db4"
integrity sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==
"@typescript/typescript-freebsd-arm64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz#f1ff8810030b35d2b5be0db6a2dc650460ea94fa"
integrity sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==
"@typescript/typescript-freebsd-x64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz#3d86b03f353c5b1ba95162eb6ce35533bfc294bd"
integrity sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==
"@typescript/typescript-linux-arm64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz#d9334d96d6dac6ff85da9c865588948de939e91f"
integrity sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==
"@typescript/typescript-linux-arm@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz#ad94b41e1aee2a4dcc6a298c7b67c43345fde32e"
integrity sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==
"@typescript/typescript-linux-loong64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz#2965aee4fc873360139d893daafe6397a29138ad"
integrity sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==
"@typescript/typescript-linux-mips64el@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz#1a887a311bed3a833f80bfd4a9ed37c271936cf0"
integrity sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==
"@typescript/typescript-linux-ppc64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz#8b63c9b2f445b393eb4e43ec21da225dade3577d"
integrity sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==
"@typescript/typescript-linux-riscv64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz#b6e8a35c289b3ea97a92a41d461aaeed0d3b36e1"
integrity sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==
"@typescript/typescript-linux-s390x@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz#2ef96693be4861f6d17965427e5b009cbbed1a3e"
integrity sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==
"@typescript/typescript-linux-x64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz#73269cb0baba50aea0ca060445a6b88e583f1ce2"
integrity sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==
"@typescript/typescript-netbsd-arm64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz#3a3649f97fafa210b4e6e3798c15e06605c8a901"
integrity sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==
"@typescript/typescript-netbsd-x64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz#47ec59491a40c470d2807dc4d2b825528fd979ab"
integrity sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==
"@typescript/typescript-openbsd-arm64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz#796be8da0bd989d8a3fb96f2801e38a8365b4baf"
integrity sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==
"@typescript/typescript-openbsd-x64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz#d37fe2a729eb942c076c454ee7f1815faf7d560f"
integrity sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==
"@typescript/typescript-sunos-x64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz#aba8d3464c3565a7044789baba96916bd4ab2c88"
integrity sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==
"@typescript/typescript-win32-arm64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz#b9de50a17196383f62620b5f9d0a2f34ad3b60d7"
integrity sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==
"@typescript/typescript-win32-x64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz#cf3b7b0d6ce5635daca4c8e01c189cdcde47ec3c"
integrity sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==
acorn-jsx@^5.3.2:
version "5.3.2"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
@@ -557,10 +451,10 @@ brace-expansion@^1.1.7:
balanced-match "^1.0.0"
concat-map "0.0.1"
brace-expansion@^5.0.8:
version "5.0.9"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.9.tgz#7c72438809b5fa5babf54199a1f1c281a6984fcf"
integrity sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==
brace-expansion@^5.0.2:
version "5.0.2"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.2.tgz#b6c16d0791087af6c2bc463f52a8142046c06b6f"
integrity sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==
dependencies:
balanced-match "^4.0.2"
@@ -881,15 +775,15 @@ eslint-visitor-keys@^5.0.0, eslint-visitor-keys@^5.0.1:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz#9e3c9489697824d2d4ce3a8ad12628f91e9f59be"
integrity sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==
eslint@^10.8.0:
version "10.8.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-10.8.0.tgz#e6d19907a3f090a53a022261ba34c5ff6d04908b"
integrity sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==
eslint@^10.4.1:
version "10.4.1"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-10.4.1.tgz#f6640b176e0912246d9ddbf8fcfa5e8b7f02445a"
integrity sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw==
dependencies:
"@eslint-community/eslint-utils" "^4.8.0"
"@eslint-community/regexpp" "^4.12.2"
"@eslint/config-array" "^0.23.5"
"@eslint/config-helpers" "^0.7.0"
"@eslint/config-helpers" "^0.6.0"
"@eslint/core" "^1.2.1"
"@eslint/plugin-kit" "^0.7.2"
"@humanfs/node" "^0.16.6"
@@ -913,7 +807,7 @@ eslint@^10.8.0:
imurmurhash "^0.1.4"
is-glob "^4.0.0"
json-stable-stringify-without-jsonify "^1.0.1"
minimatch "^10.2.5"
minimatch "^10.2.4"
natural-compare "^1.4.0"
optionator "^0.9.3"
@@ -1385,10 +1279,10 @@ js-tiktoken@^1.0.12:
dependencies:
base64-js "^1.5.1"
js-yaml@^4.3.0:
version "4.3.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.3.1.tgz#01216c001d67f48e2cd560d708c7af21090a3848"
integrity sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==
js-yaml@^4.1.1:
version "4.2.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.2.0.tgz#2bd9e85682dd91bd469afb809d816043b3d49524"
integrity sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==
dependencies:
argparse "^2.0.1"
@@ -1448,12 +1342,12 @@ math-intrinsics@^1.1.0:
resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
minimatch@^10.2.2, minimatch@^10.2.4, minimatch@^10.2.5:
version "10.2.6"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.6.tgz#fd956bbe0b77241e9f15ac5dccb1c638060968ef"
integrity sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==
minimatch@^10.2.2, minimatch@^10.2.4:
version "10.2.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.4.tgz#465b3accbd0218b8281f5301e27cedc697f96fde"
integrity sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==
dependencies:
brace-expansion "^5.0.8"
brace-expansion "^5.0.2"
minimatch@^3.1.2, minimatch@^3.1.5:
version "3.1.5"
@@ -1652,10 +1546,10 @@ prettier-linter-helpers@^1.0.1:
dependencies:
fast-diff "^1.1.2"
prettier@^3.9.6:
version "3.9.6"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.9.6.tgz#b3ea5146515d40fc53f18aa63f74dfab1e10dbf6"
integrity sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==
prettier@^3.8.3:
version "3.8.3"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.8.3.tgz#560f2de55bf01b4c0503bc629d5df99b9a1d09b0"
integrity sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==
punycode@^2.1.0:
version "2.3.1"
@@ -1908,17 +1802,17 @@ tsconfig-paths@^3.15.0:
minimist "^1.2.6"
strip-bom "^3.0.0"
turbo@^2.10.8:
version "2.10.8"
resolved "https://registry.yarnpkg.com/turbo/-/turbo-2.10.8.tgz#09457cb7db79710586da455a29bccc05f15b1b72"
integrity sha512-9+8YX5QOkGXzZxcIykTHgaooRHGMWO+jfdyRK0o+rN0U7hBIig2MrJ8r/aNzIPDPhdA73SGb0O+tIztaModTMg==
turbo@^2.9.16:
version "2.9.16"
resolved "https://registry.yarnpkg.com/turbo/-/turbo-2.9.16.tgz#ed72ff679c2c54bde0e3a2e40107416181e203a4"
integrity sha512-NqgRQy6j6dPYcdSdv0q1g9QsZg7SWg87RERM8otw/1AtKU2yTFVClOM7cbwKzOonZr/Ek1blTBucw64L9H0Bwg==
optionalDependencies:
"@turbo/darwin-64" "2.10.8"
"@turbo/darwin-arm64" "2.10.8"
"@turbo/linux-64" "2.10.8"
"@turbo/linux-arm64" "2.10.8"
"@turbo/windows-64" "2.10.8"
"@turbo/windows-arm64" "2.10.8"
"@turbo/darwin-64" "2.9.16"
"@turbo/darwin-arm64" "2.9.16"
"@turbo/linux-64" "2.9.16"
"@turbo/linux-arm64" "2.9.16"
"@turbo/windows-64" "2.9.16"
"@turbo/windows-arm64" "2.9.16"
type-check@^0.4.0, type-check@~0.4.0:
version "0.4.0"
@@ -1972,31 +1866,10 @@ typed-array-length@^1.0.7:
possible-typed-array-names "^1.0.0"
reflect.getprototypeof "^1.0.6"
typescript@^7.0.2:
version "7.0.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-7.0.2.tgz#9ec773d7954a8c182c17cc5bbd575aa28bc51582"
integrity sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==
optionalDependencies:
"@typescript/typescript-aix-ppc64" "7.0.2"
"@typescript/typescript-darwin-arm64" "7.0.2"
"@typescript/typescript-darwin-x64" "7.0.2"
"@typescript/typescript-freebsd-arm64" "7.0.2"
"@typescript/typescript-freebsd-x64" "7.0.2"
"@typescript/typescript-linux-arm" "7.0.2"
"@typescript/typescript-linux-arm64" "7.0.2"
"@typescript/typescript-linux-loong64" "7.0.2"
"@typescript/typescript-linux-mips64el" "7.0.2"
"@typescript/typescript-linux-ppc64" "7.0.2"
"@typescript/typescript-linux-riscv64" "7.0.2"
"@typescript/typescript-linux-s390x" "7.0.2"
"@typescript/typescript-linux-x64" "7.0.2"
"@typescript/typescript-netbsd-arm64" "7.0.2"
"@typescript/typescript-netbsd-x64" "7.0.2"
"@typescript/typescript-openbsd-arm64" "7.0.2"
"@typescript/typescript-openbsd-x64" "7.0.2"
"@typescript/typescript-sunos-x64" "7.0.2"
"@typescript/typescript-win32-arm64" "7.0.2"
"@typescript/typescript-win32-x64" "7.0.2"
typescript@^6.0.3:
version "6.0.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-6.0.3.tgz#90251dc007916e972786cb94d74d15b185577d21"
integrity sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==
unbox-primitive@^1.1.0:
version "1.1.0"
@@ -2015,6 +1888,11 @@ uri-js@^4.2.2:
dependencies:
punycode "^2.1.0"
uuid@^14.0.0:
version "14.0.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-14.0.0.tgz#0af883220163d264ffe0c084f6b8a89b9666966d"
integrity sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==
which-boxed-primitive@^1.1.0, which-boxed-primitive@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz#d76ec27df7fa165f18d5808374a5fe23c29b176e"
+1 -1
View File
@@ -1 +1 @@
__version__ = "0.4.31"
__version__ = "0.4.30"
+4 -12
View File
@@ -1786,12 +1786,8 @@ def deploy_list(api_key: str | None, host_url: str | None, name_contains: str) -
client,
lambda c: c.list_deployments(name_contains=name_contains),
)
resources = response.get("resources") if isinstance(response, dict) else None
deployments = (
[item for item in resources if isinstance(item, dict)]
if isinstance(resources, list)
else []
)
resources = response.get("resources", []) if isinstance(response, dict) else []
deployments = [item for item in resources if isinstance(item, dict)]
if not deployments:
click.echo("No deployments found.")
return
@@ -1835,12 +1831,8 @@ def deploy_revisions_list(
client,
lambda c: c.list_revisions(deployment_id, limit=limit),
)
resources = response.get("resources") if isinstance(response, dict) else None
revisions = (
[item for item in resources if isinstance(item, dict)]
if isinstance(resources, list)
else []
)
resources = response.get("resources", []) if isinstance(response, dict) else []
revisions = [item for item in resources if isinstance(item, dict)]
if not revisions:
click.echo(f"No revisions found for deployment {deployment_id}.")
return
+1 -7
View File
@@ -23,7 +23,7 @@ dependencies = [
path = "langgraph_cli/__init__.py"
[project.optional-dependencies]
inmem = [
"langgraph-api>=0.5.35,<1.0.0 ; python_version >= '3.11'",
"langgraph-api>=0.5.35,<0.12.0 ; python_version >= '3.11'",
"langgraph-runtime-inmem>=0.7 ; python_version >= '3.11'",
]
@@ -72,15 +72,9 @@ lint.select = [
"UP", # pyupgrade
"B", # flake8-bugbear
"I", # isort
"PLC0415", # import-outside-top-level
"RUF100", # unused noqa directive
"UP", # pyupgrade
]
lint.ignore = ["E501", "B008"]
# PLC0415 (import-outside-top-level) is enforced in tests only. Library code
# still has deferred imports that have not been reviewed, so it stays exempt
# for now.
lint.per-file-ignores = { "langgraph_cli/**" = ["PLC0415"], "generate_schema.py" = ["PLC0415"] }
target-version = "py310"
[tool.ty.rules]
+6 -1
View File
@@ -11,7 +11,6 @@ from langgraph_cli.archive import (
_tar_filter,
create_archive,
)
from langgraph_cli.config import LocalDeps
# ---------------------------------------------------------------------------
# _tar_filter
@@ -199,6 +198,7 @@ class TestCreateArchive:
@patch("langgraph_cli.archive._assemble_local_deps")
def test_yields_archive_with_config(self, mock_deps, tmp_path):
from langgraph_cli.config import LocalDeps
config_file = self._make_project(tmp_path)
mock_deps.return_value = LocalDeps(
@@ -218,6 +218,7 @@ class TestCreateArchive:
@patch("langgraph_cli.archive._assemble_local_deps")
def test_excludes_pycache(self, mock_deps, tmp_path):
from langgraph_cli.config import LocalDeps
config_file = self._make_project(tmp_path)
mock_deps.return_value = LocalDeps(
@@ -231,6 +232,7 @@ class TestCreateArchive:
@patch("langgraph_cli.archive._assemble_local_deps")
def test_cleans_up_tmp_dir_on_normal_exit(self, mock_deps, tmp_path):
from langgraph_cli.config import LocalDeps
config_file = self._make_project(tmp_path)
mock_deps.return_value = LocalDeps(
@@ -245,6 +247,7 @@ class TestCreateArchive:
@patch("langgraph_cli.archive._assemble_local_deps")
def test_cleans_up_tmp_dir_on_exception(self, mock_deps, tmp_path):
from langgraph_cli.config import LocalDeps
config_file = self._make_project(tmp_path)
mock_deps.return_value = LocalDeps(
@@ -261,6 +264,7 @@ class TestCreateArchive:
@patch("langgraph_cli.archive._assemble_local_deps")
@patch("langgraph_cli.archive._MAX_SIZE", 10)
def test_raises_on_oversized_archive(self, mock_deps, tmp_path):
from langgraph_cli.config import LocalDeps
config_file = self._make_project(tmp_path)
mock_deps.return_value = LocalDeps(
@@ -274,6 +278,7 @@ class TestCreateArchive:
@patch("langgraph_cli.archive._assemble_local_deps")
def test_handles_extra_contexts(self, mock_deps, tmp_path):
"""Monorepo case: project + sibling dependency directory."""
from langgraph_cli.config import LocalDeps
project = tmp_path / "myproject"
project.mkdir()
@@ -347,6 +347,7 @@ class TestCallHostBackendWithOptionalTenant:
def test_workspace_prompt_blocked_by_no_input(self, monkeypatch):
"""With _no_input=True, 403 requiring workspace should raise ClickException."""
import langgraph_cli.deploy as deploy_mod
monkeypatch.setattr(deploy_mod, "_no_input", True)
@@ -514,6 +515,7 @@ class TestEmitterTextMode:
class TestCreateHostBackendClientNoInput:
def test_raises_when_no_api_key_and_no_input(self, monkeypatch, tmp_path):
import langgraph_cli.deploy as deploy_mod
monkeypatch.setattr(deploy_mod, "_no_input", True)
monkeypatch.delenv("LANGSMITH_API_KEY", raising=False)
@@ -528,6 +530,7 @@ class TestCreateHostBackendClientNoInput:
)
def test_succeeds_with_api_key_in_env(self, monkeypatch, tmp_path):
import langgraph_cli.deploy as deploy_mod
monkeypatch.setattr(deploy_mod, "_no_input", True)
monkeypatch.setenv("LANGSMITH_API_KEY", "lsv2_test")
+231 -267
View File
@@ -158,7 +158,7 @@ name = "blockbuster"
version = "1.5.26"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "forbiddenfruit", marker = "implementation_name == 'cpython'" },
{ name = "forbiddenfruit", marker = "python_full_version >= '3.11' and implementation_name == 'cpython'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/55/e0/dcbab602790a576b0b94108c07e2c048e5897df7cc83722a89582d733987/blockbuster-1.5.26.tar.gz", hash = "sha256:cc3ce8c70fa852a97ee3411155f31e4ad2665cd1c6c7d2f8bb1851dab61dc629", size = 36085, upload-time = "2025-12-05T10:43:47.735Z" }
wheels = [
@@ -363,14 +363,14 @@ wheels = [
[[package]]
name = "click"
version = "8.4.2"
version = "8.4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" }
sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" },
{ url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" },
]
[[package]]
@@ -384,11 +384,11 @@ wheels = [
[[package]]
name = "codespell"
version = "2.4.3"
version = "2.4.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/80/19/45e941380f69c042b43423513d201e6592346f992394347f5e7174c31407/codespell-2.4.3.tar.gz", hash = "sha256:cbe085e331227b37bb86ef8bddd08dc768c704ee9a07ca869852c093fa2793e2", size = 352773, upload-time = "2026-07-15T11:51:54.159Z" }
sdist = { url = "https://files.pythonhosted.org/packages/2d/9d/1d0903dff693160f893ca6abcabad545088e7a2ee0a6deae7c24e958be69/codespell-2.4.2.tar.gz", hash = "sha256:3c33be9ae34543807f088aeb4832dfad8cb2dae38da61cac0a7045dd376cfdf3", size = 352058, upload-time = "2026-03-05T18:10:42.936Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8b/bf/bdb951d34eb169140b546f44be9ec4525d1acefb9eb5572071f5492b19fc/codespell-2.4.3-py3-none-any.whl", hash = "sha256:af2505b335e8573dbd2d384d1c4ef498f4006f4ba2d6fceca01e55b91f52628a", size = 340736, upload-time = "2026-07-15T11:51:52.925Z" },
{ url = "https://files.pythonhosted.org/packages/42/a1/52fa05533e95fe45bcc09bcf8a503874b1c08f221a4e35608017e0938f55/codespell-2.4.2-py3-none-any.whl", hash = "sha256:97e0c1060cf46bd1d5db89a936c98db8c2b804e1fdd4b5c645e82a1ec6b1f886", size = 353715, upload-time = "2026-03-05T18:10:41.398Z" },
]
[[package]]
@@ -405,7 +405,7 @@ name = "croniter"
version = "6.2.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "python-dateutil" },
{ name = "python-dateutil", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/df/de/5832661ed55107b8a09af3f0a2e71e0957226a59eb1dcf0a445cce6daf20/croniter-6.2.2.tar.gz", hash = "sha256:ba60832a5ec8e12e51b8691c3309a113d1cf6526bdf1a48150ce8ec7a532d0ab", size = 113762, upload-time = "2026-03-15T08:43:48.112Z" }
wheels = [
@@ -414,59 +414,62 @@ wheels = [
[[package]]
name = "cryptography"
version = "50.0.0"
version = "48.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" }
sdist = { url = "https://files.pythonhosted.org/packages/12/45/870e7f4bef50e5f53b9f51d4428aee5290eedf58ba443f16b1ebb7ab8e66/cryptography-48.0.1.tar.gz", hash = "sha256:266f4ee051abb2f725b74ef8072b521ce1feacf685a3364fa6a6b45548db791a", size = 832989, upload-time = "2026-06-09T22:32:31.8Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c5/5c/59086b4aac5e879d38ddbcf74e4be7ade89cebc3eb199a55da998c3bb46a/cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03", size = 4001252, upload-time = "2026-07-31T14:23:33.331Z" },
{ url = "https://files.pythonhosted.org/packages/57/ef/8f2df13c7216bcad3e1c74e07f6e193d93e998e114f524a53877c9af27ad/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645", size = 4719554, upload-time = "2026-07-31T14:23:35.611Z" },
{ url = "https://files.pythonhosted.org/packages/d9/41/029086c34d91052fc3b88bcc8056f709a7c915c7a23b235a54eb800b1c97/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7", size = 4702130, upload-time = "2026-07-31T14:23:37.635Z" },
{ url = "https://files.pythonhosted.org/packages/7d/ff/b6ce0954962e7f7b969f850a883744197bb3910bdfd7b6da162eab7d9f68/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3", size = 4725244, upload-time = "2026-07-31T14:23:39.471Z" },
{ url = "https://files.pythonhosted.org/packages/06/1e/63a1027cb7fec360a182208e1b7767d5aa1fe57be3d6aa856e69a321edc0/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f", size = 5342265, upload-time = "2026-07-31T14:23:41.286Z" },
{ url = "https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae", size = 4734609, upload-time = "2026-07-31T14:23:43.139Z" },
{ url = "https://files.pythonhosted.org/packages/15/37/36a9c479bbe49acea2636c7fd3360d20f7b7e079c300352011c44850b181/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a", size = 4356517, upload-time = "2026-07-31T14:23:44.939Z" },
{ url = "https://files.pythonhosted.org/packages/32/98/8a151d64367204cbc63ec65d37502f1d9c53cf4bfc6ec3c532614dbec60d/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987", size = 4724529, upload-time = "2026-07-31T14:23:46.93Z" },
{ url = "https://files.pythonhosted.org/packages/22/f6/ec13b470172126464a86bf54d2294a46d29837fc51ba3e45d4047946fb5e/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169", size = 5299852, upload-time = "2026-07-31T14:23:48.851Z" },
{ url = "https://files.pythonhosted.org/packages/da/3a/f05e32c99d440c9bb891ea0e36c9091891e36be5a9a87ab2ee6ea20729f6/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f", size = 4734462, upload-time = "2026-07-31T14:23:50.861Z" },
{ url = "https://files.pythonhosted.org/packages/ca/dc/bd72b26be8953f80625f63151efd38eee71c76ca6cf591c08ff34615a79e/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105", size = 4852708, upload-time = "2026-07-31T14:23:52.715Z" },
{ url = "https://files.pythonhosted.org/packages/27/20/c930314a2ab476d15dec966ec87e2e9637bb02b06106b12c0396c57bb603/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef", size = 5004179, upload-time = "2026-07-31T14:23:54.887Z" },
{ url = "https://files.pythonhosted.org/packages/32/2e/c9db68a0c4bfa28e310707527c0ee3a2bd254104d2e02e68f368e197aa4c/cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30", size = 3840395, upload-time = "2026-07-31T14:23:56.677Z" },
{ url = "https://files.pythonhosted.org/packages/c3/fb/951032a3bf22a5697c83183fb6294a4843772947a70e616c57b3ff5f522e/cryptography-50.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41", size = 3989258, upload-time = "2026-07-31T14:23:58.881Z" },
{ url = "https://files.pythonhosted.org/packages/d4/67/91eb047e69c5e845f2f14b8a2e4a1aab0f283cb885531e9e22c8adb176bc/cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc", size = 4700648, upload-time = "2026-07-31T14:24:00.702Z" },
{ url = "https://files.pythonhosted.org/packages/30/82/85f0f7425c856b9f96459411eb12e74ef72df9caf6f8f15bf23a33ff131f/cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f", size = 4682442, upload-time = "2026-07-31T14:24:02.538Z" },
{ url = "https://files.pythonhosted.org/packages/1a/28/b555a365adff1cca2fbe7b9e487d68a40de6bc67ff2cb587473eb43de0e7/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3", size = 4707596, upload-time = "2026-07-31T14:24:04.394Z" },
{ url = "https://files.pythonhosted.org/packages/72/d8/f52538140cc719df62a01cf87d1c7142318d235817109d6f4054d7c352d6/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533", size = 5314552, upload-time = "2026-07-31T14:24:06.31Z" },
{ url = "https://files.pythonhosted.org/packages/38/14/6120e5bd7c5aa022ad15424ba4d5c5269d0d9448ed4d55e492ea91e3c1c4/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037", size = 4717113, upload-time = "2026-07-31T14:24:08.349Z" },
{ url = "https://files.pythonhosted.org/packages/fa/71/190bf38c3ee2e0f8efc9860ae100c9df4169742eef274b91e7aa1cb133b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f", size = 4338580, upload-time = "2026-07-31T14:24:10.227Z" },
{ url = "https://files.pythonhosted.org/packages/3a/63/504ccfbbe61fd8aa983f7f146399cdf034c72c2fc55f5b2dfdcdcdb20c99/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11", size = 4707038, upload-time = "2026-07-31T14:24:12.169Z" },
{ url = "https://files.pythonhosted.org/packages/01/77/2cf79bbfc4d12ca106437a6e170d6aaa01a373e93093118aaaef0e801bd4/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d", size = 5273110, upload-time = "2026-07-31T14:24:14.38Z" },
{ url = "https://files.pythonhosted.org/packages/e5/45/8aae2972c520145377ea3559a605a899bebe227bf070b33cdb445929a9b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c", size = 4716439, upload-time = "2026-07-31T14:24:16.415Z" },
{ url = "https://files.pythonhosted.org/packages/7b/20/4fe50b619a48c2525cc46e2dbc1ac490708d704be5d467bdaac6dc955682/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c", size = 4837383, upload-time = "2026-07-31T14:24:18.553Z" },
{ url = "https://files.pythonhosted.org/packages/92/91/3a31366e183343d3703f8995c095f5734676bd6938118047e50fcf279eb4/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95", size = 4985772, upload-time = "2026-07-31T14:24:20.385Z" },
{ url = "https://files.pythonhosted.org/packages/74/9a/02ffe35b2853d121689871eb5dce862092562b3a1ed5cc98f1aaed441506/cryptography-50.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269", size = 3816291, upload-time = "2026-07-31T14:24:22.125Z" },
{ url = "https://files.pythonhosted.org/packages/03/37/73d005be173aff344af30e9fd2a576575cb2391a7101d9cd3842e1fa8cce/cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07", size = 4036009, upload-time = "2026-07-31T14:24:24.122Z" },
{ url = "https://files.pythonhosted.org/packages/ff/c6/7a6202a534e32103a285b7834a120869557fe198d51d7cfe59754c8bda9c/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3", size = 4745252, upload-time = "2026-07-31T14:24:26.118Z" },
{ url = "https://files.pythonhosted.org/packages/85/4f/0fa8c2f4428198f15d9ff8d63400e27afbf94ce833f6108da1eb3753f945/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f", size = 4728939, upload-time = "2026-07-31T14:24:27.994Z" },
{ url = "https://files.pythonhosted.org/packages/d1/63/54dd723490ba2dc09b299682c10b38db38f159728bcaae8c591b8af2f22d/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5", size = 4748483, upload-time = "2026-07-31T14:24:30.254Z" },
{ url = "https://files.pythonhosted.org/packages/1d/dd/7c77d26285cc7f6991efce64a0f5b4f9383bfa5dd8c5033003eaf7db4cdb/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f", size = 5367599, upload-time = "2026-07-31T14:24:32.457Z" },
{ url = "https://files.pythonhosted.org/packages/46/c9/f60aed34c013f317f92817b6c171c2d22a78270fa41109bd4b08af26b194/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025", size = 4762647, upload-time = "2026-07-31T14:24:34.599Z" },
{ url = "https://files.pythonhosted.org/packages/be/f3/f9a0173b139372c3a48ed98154b45cc6b9de17c789d5ab552e621c293609/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a", size = 4385197, upload-time = "2026-07-31T14:24:36.647Z" },
{ url = "https://files.pythonhosted.org/packages/d8/36/83bb81f6e569bc38e1e4a7bc80f29b46bb9601920bc455fc8e888f5d5742/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b", size = 4748095, upload-time = "2026-07-31T14:24:39.493Z" },
{ url = "https://files.pythonhosted.org/packages/6b/16/d3008eff98c764979865834c3d386d4fd041b5f52e7f34fc29ac1a5eb515/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708", size = 5325948, upload-time = "2026-07-31T14:24:41.556Z" },
{ url = "https://files.pythonhosted.org/packages/9c/f8/d97f9603efda3888187bfdb893f26c41be4735c10631d05d284ee6b047c4/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47", size = 4762400, upload-time = "2026-07-31T14:24:43.636Z" },
{ url = "https://files.pythonhosted.org/packages/64/a2/4615c8f7d81a00b1d6e6afe19f694e1543582349fb5f4076f6cb5dc36485/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9", size = 4878208, upload-time = "2026-07-31T14:24:45.522Z" },
{ url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" },
{ url = "https://files.pythonhosted.org/packages/57/30/4a22984d4f1bdfb8c054f07a92bc176b97a3134cc1d6c4b3bffb1f3688b4/cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba", size = 3874135, upload-time = "2026-07-31T14:24:50.085Z" },
{ url = "https://files.pythonhosted.org/packages/9d/3e/e54cde8c01631a5a8226ccd617eab9e57fd5cfdad90f1a9e6bb570794631/cryptography-50.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5e34edd123674534acd70147f0ca331eaa2c74e6325fb2028c886aa26ba0b68c", size = 3963170, upload-time = "2026-07-31T14:24:51.968Z" },
{ url = "https://files.pythonhosted.org/packages/01/b6/0b9e125e90f3d2dcf599a218a899cda7326a3158cfa258723f0b398b08f6/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8eb5e1172eb569ea8a872796576e6a67c276351728b6455d5beb01242b027c6a", size = 4692441, upload-time = "2026-07-31T14:24:53.743Z" },
{ url = "https://files.pythonhosted.org/packages/53/c9/a5151588710785a96d7bc4de27d4cd62f263bbbcb203cfe29df537eb6505/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:910d11e1a385c654bf738bf3e6b8e6ed5de0f5610fcae2be9e5b398d8081d20e", size = 4699810, upload-time = "2026-07-31T14:24:55.746Z" },
{ url = "https://files.pythonhosted.org/packages/c7/1a/15b92b25eb6ce3089cd49377ae990a0f3ad485a510f968aed1f19dbdcdf2/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:62598a8a57f815db4c6259a4e97d857dab56697e7de8e8ab02352ab74da1995d", size = 4691924, upload-time = "2026-07-31T14:24:58.082Z" },
{ url = "https://files.pythonhosted.org/packages/62/15/219075012ab13e8905f3cd572204f4acb4b111df787104346b9bc0cea789/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:07479a1cb08219ab719147e742e76090c9c773321959bb94946fffdd397a6437", size = 4699593, upload-time = "2026-07-31T14:24:59.951Z" },
{ url = "https://files.pythonhosted.org/packages/8e/b5/c2c5fce26f0ee40d21bafe7f191d29a34b35a65ac4fe8a1191d1983612e9/cryptography-50.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c99c003e088647b8a5b7c145d6f78c335f6348332b62e142d411c4b63d1460b9", size = 3813796, upload-time = "2026-07-31T14:25:02.298Z" },
{ url = "https://files.pythonhosted.org/packages/1b/bc/ee4137cbbe105652c0ee4252792b78fc8e7afa4b8e61d9d5dc05a7f45731/cryptography-48.0.1-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:3e4a1a3232eef2e6c732827d5722db29a0cc8b27af2a4d865b094cf954be9ca1", size = 8008324, upload-time = "2026-06-09T22:31:00.702Z" },
{ url = "https://files.pythonhosted.org/packages/d5/85/6379d42181bfc713094f081360fc5784d6c816b599d45e7f082502d173ce/cryptography-48.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:32143b24adb918f078134e1e230f1eb8cc04886b92c28b5f0041aaf3e5699225", size = 4696243, upload-time = "2026-06-09T22:32:33.446Z" },
{ url = "https://files.pythonhosted.org/packages/9c/87/c85d147b53323c7eb4d850920c8901377323c2a0ff8d79c262d4fee89aa2/cryptography-48.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0d27a5696721ef7a672b8c810f6aded391058e0b9486e63e6d93baf765da691", size = 4713235, upload-time = "2026-06-09T22:31:40.141Z" },
{ url = "https://files.pythonhosted.org/packages/79/58/67cbf8cf1ee7c54b439ca07bbecf8362c07afc11a3724fea70f745784add/cryptography-48.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb86ce1af36fe65041b6db9a8bb064ee621a7e5fded0f80d475ec243477cd242", size = 4702323, upload-time = "2026-06-09T22:31:42.191Z" },
{ url = "https://files.pythonhosted.org/packages/89/c6/24266ac10c47f6cd2a865f4446062b466da1d1f10b27189eac00e61bf0c9/cryptography-48.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b024e784ad6c077ee0147b35ea9cbfc1e34e1fd4c1dcca214c2794d73a12df08", size = 5300085, upload-time = "2026-06-09T22:31:58.703Z" },
{ url = "https://files.pythonhosted.org/packages/d2/bb/cc4b78784f97efc8c5874c2a9743708d172be6663024b34a0467885ae0c8/cryptography-48.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3752f2dbc8f07a30aad2932c986cea495b03bb554887828225da104f732852b6", size = 4746137, upload-time = "2026-06-09T22:31:31.01Z" },
{ url = "https://files.pythonhosted.org/packages/1f/52/0c44de3f5267f8fbe8e835138017522a333436166e406f0db9b9e6e3033f/cryptography-48.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:bd81490cd5801d755cf97bb68ac191f14b708470b1c7cf4580f669b9c9264cd8", size = 4333867, upload-time = "2026-06-09T22:32:28.096Z" },
{ url = "https://files.pythonhosted.org/packages/9a/2e/772d7adbfa931537bc401640b7cac9976bff689bda187833e5d63b428e49/cryptography-48.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:66fd0771e7b9c6dcd44cf1120690d2338d16d72795cf40cae2786a39eba65429", size = 4701805, upload-time = "2026-06-09T22:31:38.284Z" },
{ url = "https://files.pythonhosted.org/packages/f8/a3/b06844f303873493c963caf581c04df31c7035e0c1b0f02c4814d319ec80/cryptography-48.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:3fd2ca57062b241c856670b073487d2e86c4637937ca5601e48f97bf8e11fc8f", size = 5258461, upload-time = "2026-06-09T22:31:04.187Z" },
{ url = "https://files.pythonhosted.org/packages/9f/13/8b765e2e12b07c74941caadb9d1c8fdc006c4dfbf2b8f2d610519758954d/cryptography-48.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:0ee6ea481db1ab889cba043ec1eda17bb9c1ea79db6722f779c3667f9f70322f", size = 4745488, upload-time = "2026-06-09T22:32:30.07Z" },
{ url = "https://files.pythonhosted.org/packages/2e/aa/48972bce55049b32a94f4907eda4d75fa385aad8a39506cc2fc72196ecf0/cryptography-48.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f2ceef93cb096aa3c4cc4b5c94ca6131f9196d28c64d6111533402a9b2054d41", size = 4830256, upload-time = "2026-06-09T22:31:43.868Z" },
{ url = "https://files.pythonhosted.org/packages/47/a2/e5079a032fb85cf6005046ca92bbd78b0c82dad2b5751ab8c311659da06f/cryptography-48.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bd3f92d76217892b15df84ca256c2c113d386fdda7a7d8691aeeced976507c6", size = 4979117, upload-time = "2026-06-09T22:31:05.845Z" },
{ url = "https://files.pythonhosted.org/packages/b7/a0/8f50cae9c74e718ed769d63ed5c74bd0ea830c9550a74629cebd1b9c7bc7/cryptography-48.0.1-cp311-abi3-win32.whl", hash = "sha256:b9a32b876490d66c8bcc9963ef220199569748434ab01a9d6aaeabf88e7f5158", size = 3304154, upload-time = "2026-06-09T22:32:16.845Z" },
{ url = "https://files.pythonhosted.org/packages/c5/69/0572c77dbace6fef72f33755bd52ea399c71367250d366237f8691826b9e/cryptography-48.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:39489bfca54c7a1f6b297efcd8bc608ab92d16c4ca631b0cad4da46724588b24", size = 3817138, upload-time = "2026-06-09T22:32:00.388Z" },
{ url = "https://files.pythonhosted.org/packages/42/06/3e768b4c3bc78201583fa35a0e18f640dd782ff41afba88f8545481a8874/cryptography-48.0.1-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:f817adc181390bd54f2f700107a7419040fb7c1bdf2fc26f36551a06a68c3345", size = 7989830, upload-time = "2026-06-09T22:31:07.8Z" },
{ url = "https://files.pythonhosted.org/packages/8a/13/6476736484b94041110c8340a3eb63962fea4975baea8cb4a512adb44d4d/cryptography-48.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d5d30989c6917b478b5817902e85fddaea2261efa8648383d965381ccb9e1ac4", size = 4689201, upload-time = "2026-06-09T22:31:09.745Z" },
{ url = "https://files.pythonhosted.org/packages/79/62/65a87f34d2a431546e2509b85d55e8c90df86d668f6731da64d538512ac2/cryptography-48.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:df637c05205ea7c1d7fbcbe54bbfea648a52951155f997af13d895d0ecc96991", size = 4702822, upload-time = "2026-06-09T22:32:24.409Z" },
{ url = "https://files.pythonhosted.org/packages/7f/59/810b5204b0a9b10f4b6bc06bd551a8b609803cd931806bc3b71884b225e5/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:869c3b8a53bfe27147832df48b32adadf558249d50e76cb3769d40e986b13265", size = 4694875, upload-time = "2026-06-09T22:32:08.737Z" },
{ url = "https://files.pythonhosted.org/packages/24/dc/d8ca05ffea724eec6d232ea6f18e74c269eb6bdfdcc9bfba689790d1325f/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:e361afba8918070d376df76f408a4f67fec0ee9cff81a99e48fe9a233ef59e17", size = 5290385, upload-time = "2026-06-09T22:31:15.212Z" },
{ url = "https://files.pythonhosted.org/packages/03/8c/3be6cb4da181f5bb6c19cf560c2359d60644a6b5fc5b57854e528f47b296/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:d069066deead00ac7f090be101be875a06855908f7ec004c27b8fefb4acfb411", size = 4737082, upload-time = "2026-06-09T22:32:22.66Z" },
{ url = "https://files.pythonhosted.org/packages/aa/f6/d5f60a5a1434dbfd949e227fd0065d194c7e6b6ac526b17f5c06152b8231/cryptography-48.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:09f73a725d582cef64b91281a322cd798d14a33b2b6f2b7ad9531dc336d84c02", size = 4325328, upload-time = "2026-06-09T22:32:10.777Z" },
{ url = "https://files.pythonhosted.org/packages/17/b7/ba75dd947a14b6ad907b01ae8f6b5b348cdd1b48142f0063dee9e20c1d9d/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:15254441469dd6bf027039453288e2072124f8b6603563f5d759e1c9b69273fa", size = 4694530, upload-time = "2026-06-09T22:31:53.105Z" },
{ url = "https://files.pythonhosted.org/packages/62/29/50d6b9e8aff12d8b67afaeb3569335e32dc83a5723e3bbded24fdac9f809/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:8ace4507d1e6533c125f4fac754f8bb8b6a74c08e92179dabd7e16571a3efbf3", size = 5245046, upload-time = "2026-06-09T22:31:25.774Z" },
{ url = "https://files.pythonhosted.org/packages/9f/04/618f4115cfc0add0838c82507aa18a346089428da8653ad38b3ff36f5cb3/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:b4e391975f038e66432328639620a4aff2d307513b004f1ca06d6225bced815c", size = 4736660, upload-time = "2026-06-09T22:32:12.676Z" },
{ url = "https://files.pythonhosted.org/packages/24/9c/06e062462a0de28a3b3911322eded4c16deb9f441b1b7575d3dc59488ab5/cryptography-48.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42fcd8e26fe555d9b3577a135f5091fefa0aa4e99129c23fb56787a1bd4ada72", size = 4822229, upload-time = "2026-06-09T22:31:17.062Z" },
{ url = "https://files.pythonhosted.org/packages/f4/be/0561971eaaee4b8a0e7d5113c536921063ab91aaf23278ac374eaf881e11/cryptography-48.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c1400da5e32a43253392277eac7490a60e497d810a63dd5608d71bbd7af507c9", size = 4966364, upload-time = "2026-06-09T22:31:32.842Z" },
{ url = "https://files.pythonhosted.org/packages/a4/27/728c77876f12b000820b69ae490f3c4083775e79e07827e9e60be07ad209/cryptography-48.0.1-cp314-cp314t-win32.whl", hash = "sha256:0df56b056bc17c1b7d6821dfa65216e62bd232d8ab05eb3db44e71d235651471", size = 3278498, upload-time = "2026-06-09T22:31:29.154Z" },
{ url = "https://files.pythonhosted.org/packages/06/e3/79a612c6d7b1e6ee0edd43633d53035bec2cfb78c82b76f7864f39e36f34/cryptography-48.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:9de21387aa95e2a895823d0745b430bed4f33503ba9ab5e0b5311f33e37d66d2", size = 3798790, upload-time = "2026-06-09T22:31:56.697Z" },
{ url = "https://files.pythonhosted.org/packages/ca/6c/00fa2a95997164c8b2072ce327c23d4ab20809ccc323ea5fab91e53a4bba/cryptography-48.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:4fdc69f8e4316bcf0c8c8ec1f26f285d12e8142d88d96c876a59a03be3f6ae67", size = 7987408, upload-time = "2026-06-09T22:32:20.777Z" },
{ url = "https://files.pythonhosted.org/packages/b0/d9/45f309a7e4e5f3f8f121d6d3be9e94024a7726ec598d6e08ae04edb2f04d/cryptography-48.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48fe40804d4caa2288f24e70ca8c64c42dd826da0ad7e4f1b41b2128d679e6c8", size = 4690196, upload-time = "2026-06-09T22:31:54.74Z" },
{ url = "https://files.pythonhosted.org/packages/5f/9f/a1bc8bcc798811b8527eb374bbccf30a3f3e806829d967118222bf1125eb/cryptography-48.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:86be3b1b0b6bf09482fb50a979c508d2950ed95f5621ec77f4e385962006b83a", size = 4696782, upload-time = "2026-06-09T22:31:45.615Z" },
{ url = "https://files.pythonhosted.org/packages/66/c2/81a4fb4e4373c500bb526bc337ac5719dd31dd15b970b84a238168c6aa08/cryptography-48.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4ab0a343c807bbcd90c971cd1ecf072937cd01847a9e002bef88fb47ac6be577", size = 4696618, upload-time = "2026-06-09T22:31:11.564Z" },
{ url = "https://files.pythonhosted.org/packages/e5/0b/aa68b221dde92d09cb29a024ede17550ee21e77a404e59fc093c82bb51e1/cryptography-48.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9621de99d2da096006b629979efd8ae7eb2d8b822488d0c89ee4000c306c59b1", size = 5289970, upload-time = "2026-06-09T22:31:20.368Z" },
{ url = "https://files.pythonhosted.org/packages/78/13/fba657f958d2af66ea959a4ba01212632089249d34af1ae48054136344d7/cryptography-48.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:88c852a0ae366e262e5a1744b685e6a433dc8788dd2a277e418bf4904203609d", size = 4731873, upload-time = "2026-06-09T22:31:22.253Z" },
{ url = "https://files.pythonhosted.org/packages/4c/4c/9a964756d24a26b3e34dfcb16f961b89838786e6700b635b0d1e3adff4b6/cryptography-48.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:43c5835e2cb98c8733d86f57d6fc879b613f5c3478607281c3e36daffc6dd8a6", size = 4330804, upload-time = "2026-06-09T22:31:36.56Z" },
{ url = "https://files.pythonhosted.org/packages/4b/0f/a10f3a6eb12950a10e3a874070283aa2dd5875b2bfd15fad8a3e17b3f13e/cryptography-48.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:fe0180af5bf9236518a087e35bf2d9a347d5f5f51e63c579d683ddff424e3d46", size = 4696217, upload-time = "2026-06-09T22:31:13.351Z" },
{ url = "https://files.pythonhosted.org/packages/f3/6f/5cd12f951165ea73ef85266775d97e4c763b2474ccfd816dd69d3a18d6f8/cryptography-48.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:b7a2d1a937a738a881737cec135a38bb61470589b17515b9f73f571d0ae10401", size = 5245252, upload-time = "2026-06-09T22:32:02.193Z" },
{ url = "https://files.pythonhosted.org/packages/68/ab/8aaa12e4516ec4464033ab79b6f3b592bd5a92102467c4ace8a0d970203f/cryptography-48.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b74ca3b8e5ecdd833bf6a002ca41b4793bb27fb8f1c06ffaf2643c9e9140e31b", size = 4731388, upload-time = "2026-06-09T22:32:04.019Z" },
{ url = "https://files.pythonhosted.org/packages/1b/24/50027ea4dca85ec1f40688f3c24fb32ccacd520583c9592c3cc95628e6fb/cryptography-48.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2c37f2461406063b417837f5f3daab668652acd82423efcd7f0a9f04be972de1", size = 4824186, upload-time = "2026-06-09T22:32:18.707Z" },
{ url = "https://files.pythonhosted.org/packages/52/41/04cb5eb17085ade6f50cc611fb657df6a0f5885350de8764ece89c050197/cryptography-48.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:86fe77abb1bd87afb251d4d02ada7ecf53a32cee9b67d976abb2e45a13297475", size = 4964539, upload-time = "2026-06-09T22:31:18.793Z" },
{ url = "https://files.pythonhosted.org/packages/36/bf/ed70785c496e89d7e73b7cda2d21f2447fd6d4e821714b8d04ff217fed92/cryptography-48.0.1-cp39-abi3-win32.whl", hash = "sha256:6b2c0c3e6ccf3ade7750f836ef3ee36eea250cc467d45c256895573ac08cc6f1", size = 3282307, upload-time = "2026-06-09T22:30:53.162Z" },
{ url = "https://files.pythonhosted.org/packages/b3/ff/371ea7d252656ee1eb6d83eeeef3d1d0c6baf1d6497687d081ea03814670/cryptography-48.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:9a49ca6c81417f6a5edb50375a60cccdd70fa0a91a5211829dbea74eba94d2ac", size = 3793408, upload-time = "2026-06-09T22:32:15.191Z" },
{ url = "https://files.pythonhosted.org/packages/a9/d3/eb4e394e587341fdad09a09101fa76478ead3a78b0ad63e55c22f0d75c02/cryptography-48.0.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:08a597acce1ff37f347400087776599e2348a3a8bc53b44120e463cd274efe4a", size = 3951747, upload-time = "2026-06-09T22:31:23.871Z" },
{ url = "https://files.pythonhosted.org/packages/e0/4a/3f43451b4f858bfceaaaffc649e6e787e8d4fb332a1d443af39ab02cc8f1/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:735824ec41b7f74a7c45fb1591349333e4c696cb6c044e5f46356e560143e4cd", size = 4641226, upload-time = "2026-06-09T22:31:02.532Z" },
{ url = "https://files.pythonhosted.org/packages/73/4e/855584c2c23b09e4ce2d3b9c30e983e679cd60b068c513c6bbdb91e11782/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:92a46e1d638daa264ba2971c0b0489c9409787943efae4d60ffda3d091ef832c", size = 4668958, upload-time = "2026-06-09T22:32:06.213Z" },
{ url = "https://files.pythonhosted.org/packages/42/3b/d35750e41d803d1e516fd6d6011f065424924da7af1748cef4cc9cb3ede1/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:7e234ac052af99f2700826a5c29ea99d9c1b1f80341cde62d11c8154dc8e0bd9", size = 4640793, upload-time = "2026-06-09T22:32:26.331Z" },
{ url = "https://files.pythonhosted.org/packages/ca/aa/cdb7181fe865285e87e96825aaab239400f1de0c3bfba9bd9769b79f1a92/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:33842cf0888951cef5bc7ac724ab844a42044c1727b967b7f8997289a0464f92", size = 4668505, upload-time = "2026-06-09T22:31:27.534Z" },
{ url = "https://files.pythonhosted.org/packages/5d/8c/ce3823c06c2804f194f9e64f0d67fa3f4094a39f2bb1a990cd03603af8fc/cryptography-48.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6184ca7b174f28d7c703f1290d4b297217c45355f77a98f67e9b7f14549ac54a", size = 3742204, upload-time = "2026-06-09T22:31:34.773Z" },
]
[[package]]
@@ -478,15 +481,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
]
[[package]]
name = "distro"
version = "1.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
]
[[package]]
name = "docopt"
version = "0.6.2"
@@ -498,7 +492,7 @@ name = "exceptiongroup"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
wheels = [
@@ -525,7 +519,7 @@ name = "googleapis-common-protos"
version = "1.74.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "protobuf" },
{ name = "protobuf", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/20/18/a746c8344152d368a5aac738d4c857012f2c5d1fd2eac7e17b647a7861bd/googleapis_common_protos-1.74.0.tar.gz", hash = "sha256:57971e4eeeba6aad1163c1f0fc88543f965bb49129b8bb55b2b7b26ecab084f1", size = 151254, upload-time = "2026-04-02T21:23:26.679Z" }
wheels = [
@@ -537,7 +531,7 @@ name = "grpcio"
version = "1.80.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
{ name = "typing-extensions", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b7/48/af6173dbca4454f4637a4678b67f52ca7e0c1ed7d5894d89d434fecede05/grpcio-1.80.0.tar.gz", hash = "sha256:29aca15edd0688c22ba01d7cc01cb000d72b2033f4a3c72a81a19b56fd143257", size = 12978905, upload-time = "2026-03-30T08:49:10.502Z" }
wheels = [
@@ -598,8 +592,8 @@ name = "grpcio-health-checking"
version = "1.80.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "grpcio" },
{ name = "protobuf" },
{ name = "grpcio", marker = "python_full_version >= '3.11'" },
{ name = "protobuf", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d1/a2/aa3cc47f19c03f8e5287b987317059753141a3af8f66b96d5a64b3be10b8/grpcio_health_checking-1.80.0.tar.gz", hash = "sha256:2cc5f08bc8b816b8655ab6f59c71450063ba20766d31e21a493e912e3560c8b1", size = 17117, upload-time = "2026-03-30T08:54:41.899Z" }
wheels = [
@@ -611,9 +605,9 @@ name = "grpcio-tools"
version = "1.80.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "grpcio" },
{ name = "protobuf" },
{ name = "setuptools" },
{ name = "grpcio", marker = "python_full_version >= '3.11'" },
{ name = "protobuf", marker = "python_full_version >= '3.11'" },
{ name = "setuptools", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/94/c8/1223f29c84a143ae9a56c084fc96894de0ba84b6e8d60a26241abd81d278/grpcio_tools-1.80.0.tar.gz", hash = "sha256:26052b19c6ce0dcf52d1024496aea3e2bdfa864159f06dc7b97b22d041a94b26", size = 6133212, upload-time = "2026-03-30T08:52:39.077Z" }
wheels = [
@@ -680,14 +674,13 @@ wheels = [
[[package]]
name = "hatch"
version = "1.17.1"
version = "1.16.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "backports-zstd", marker = "python_full_version < '3.14'" },
{ name = "click" },
{ name = "distro", marker = "sys_platform == 'linux'" },
{ name = "hatchling" },
{ name = "httpx2" },
{ name = "httpx" },
{ name = "hyperlink" },
{ name = "keyring" },
{ name = "packaging" },
@@ -703,9 +696,9 @@ dependencies = [
{ name = "uv" },
{ name = "virtualenv" },
]
sdist = { url = "https://files.pythonhosted.org/packages/01/5a/7cacce8d33a93e81cda9b4f8623ebf67b73859950af0062e1f2c527f4b34/hatch-1.17.1.tar.gz", hash = "sha256:e5f2f389ececd7e86cd86863e3f921a9024cd04ce6ef6b36d07cd9a6dc37c9cd", size = 5252686, upload-time = "2026-07-08T01:54:37.327Z" }
sdist = { url = "https://files.pythonhosted.org/packages/d2/02/ce9c4c439fa3f195b21b4b5bb18b44d1076297c86477ef7e3d2de6064ec3/hatch-1.16.5.tar.gz", hash = "sha256:57bdeeaa72577859ce37091a5449583875331c06f9cb6af9077947ad40b3a1de", size = 5220741, upload-time = "2026-02-27T18:45:31.21Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0d/0a/2b4e653186fc85061f0dfde43d602e7e93c08c0d75b23fa3577f9b3f83fd/hatch-1.17.1-py3-none-any.whl", hash = "sha256:cc6c06d302cfa785c35586ab4285c8c28a24b1744addb66344192302f6958083", size = 162405, upload-time = "2026-07-08T01:54:35.452Z" },
{ url = "https://files.pythonhosted.org/packages/e4/8a/11ae7e271870f0ad8fa0012e4265982bebe0fdc21766b161fb8b8fc3aefc/hatch-1.16.5-py3-none-any.whl", hash = "sha256:d9b8047f2cd10d3349eb6e8f278ad728a04f91495aace305c257d5c2747188fb", size = 141269, upload-time = "2026-02-27T18:45:29.573Z" },
]
[[package]]
@@ -737,19 +730,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
]
[[package]]
name = "httpcore2"
version = "2.5.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "h11" },
{ name = "truststore" },
]
sdist = { url = "https://files.pythonhosted.org/packages/47/06/5c12df521b5322fb1114a83d46911b2fbcb8855ddb3a635f11c01a214af5/httpcore2-2.5.0.tar.gz", hash = "sha256:88aa170137c17328d5ac44234f9fd10706466d5fb347f3edac4d39b91137b09d", size = 64808, upload-time = "2026-06-25T14:16:56.472Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c9/a1/7564199d1a8728fe737b0a72e5b3f8d92dfe085a74ddf7cdd83bce5f206d/httpcore2-2.5.0-py3-none-any.whl", hash = "sha256:5ce35188de461d31e8d000bfb8ef8bf22c6c16587a211e5571deaa5e9bdf842a", size = 80330, upload-time = "2026-06-25T14:16:53.634Z" },
]
[[package]]
name = "httptools"
version = "0.7.1"
@@ -803,22 +783,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
]
[[package]]
name = "httpx2"
version = "2.5.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "httpcore2" },
{ name = "idna" },
{ name = "truststore" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d0/e2/b5dedc0cf35aa65de5f541ccd30d2bc1fd7f1d43c9ab09f8ed9a7342317b/httpx2-2.5.0.tar.gz", hash = "sha256:e2df9cb4611021527ff8a675b1c320b610a2ec397acc8d6fe6e91df2d9b33c29", size = 83121, upload-time = "2026-06-25T14:16:57.491Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/31/22/859d8252dad9bc9adee34b52e62cde621ece07b042ccb2ab4da1be46695f/httpx2-2.5.0-py3-none-any.whl", hash = "sha256:3d2d4d9cf4b61f1a1f46a95947cfdb47e80cb56a2f91c6256ac8f58e4891df41", size = 76652, upload-time = "2026-06-25T14:16:55.23Z" },
]
[[package]]
name = "hyperlink"
version = "21.0.0"
@@ -833,11 +797,11 @@ wheels = [
[[package]]
name = "idna"
version = "3.18"
version = "3.15"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" }
sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
{ url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" },
]
[[package]]
@@ -911,7 +875,7 @@ name = "jsonpatch"
version = "1.33"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jsonpointer" },
{ name = "jsonpointer", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" }
wheels = [
@@ -985,15 +949,15 @@ name = "langchain-core"
version = "1.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jsonpatch" },
{ name = "langchain-protocol" },
{ name = "langsmith" },
{ name = "packaging" },
{ name = "pydantic" },
{ name = "pyyaml" },
{ name = "tenacity" },
{ name = "typing-extensions" },
{ name = "uuid-utils" },
{ name = "jsonpatch", marker = "python_full_version >= '3.11'" },
{ name = "langchain-protocol", marker = "python_full_version >= '3.11'" },
{ name = "langsmith", marker = "python_full_version >= '3.11'" },
{ name = "packaging", marker = "python_full_version >= '3.11'" },
{ name = "pydantic", marker = "python_full_version >= '3.11'" },
{ name = "pyyaml", marker = "python_full_version >= '3.11'" },
{ name = "tenacity", marker = "python_full_version >= '3.11'" },
{ name = "typing-extensions", marker = "python_full_version >= '3.11'" },
{ name = "uuid-utils", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/59/de/679a53472c25860837e32c0442c962fa86e95317a36460e2c9d5c91b17c2/langchain_core-1.4.0.tar.gz", hash = "sha256:1dc341eed802ed9c117c0df3923c991e5e9e226571e5725c194eeb5bd93d1a7f", size = 920260, upload-time = "2026-05-11T18:42:35.919Z" }
wheels = [
@@ -1005,7 +969,7 @@ name = "langchain-protocol"
version = "0.0.17"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
{ name = "typing-extensions", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/44/b3/4e2429876c7a35585618caa2b9f9089f7162a6b50562b614ad82ac11c17e/langchain_protocol-0.0.17.tar.gz", hash = "sha256:e7cbe58c205df4b4fd87dc6d5bb23f10e13b236d0e2e1b0b9d05bc2b648f3eea", size = 6026, upload-time = "2026-06-12T18:39:51.923Z" }
wheels = [
@@ -1017,12 +981,12 @@ name = "langgraph"
version = "1.2.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "langchain-core" },
{ name = "langgraph-checkpoint" },
{ name = "langgraph-prebuilt" },
{ name = "langgraph-sdk" },
{ name = "pydantic" },
{ name = "xxhash" },
{ name = "langchain-core", marker = "python_full_version >= '3.11'" },
{ name = "langgraph-checkpoint", marker = "python_full_version >= '3.11'" },
{ name = "langgraph-prebuilt", marker = "python_full_version >= '3.11'" },
{ name = "langgraph-sdk", marker = "python_full_version >= '3.11'" },
{ name = "pydantic", marker = "python_full_version >= '3.11'" },
{ name = "xxhash", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5c/55/e42f59b5e4cce75085f68abb77114c4d1152628157bd60c2587881bca559/langgraph-1.2.3.tar.gz", hash = "sha256:7f89cd5f0946fe29bd7ca048e2a84d3c14e7f652e38bb98e00f0ba8b7004b9d0", size = 718812, upload-time = "2026-06-01T18:55:54.458Z" }
wheels = [
@@ -1034,38 +998,38 @@ name = "langgraph-api"
version = "0.10.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cloudpickle" },
{ name = "cryptography" },
{ name = "grpcio" },
{ name = "grpcio-health-checking" },
{ name = "grpcio-tools" },
{ name = "httptools", marker = "sys_platform != 'win32'" },
{ name = "httpx" },
{ name = "jsonschema-rs" },
{ name = "langchain-core" },
{ name = "langchain-protocol" },
{ name = "langgraph" },
{ name = "langgraph-checkpoint" },
{ name = "langgraph-runtime-inmem" },
{ name = "langgraph-sdk" },
{ name = "langsmith", extra = ["otel"] },
{ name = "opentelemetry-api" },
{ name = "opentelemetry-exporter-otlp-proto-http" },
{ name = "opentelemetry-sdk" },
{ name = "orjson" },
{ name = "protobuf" },
{ name = "pyjwt" },
{ name = "sse-starlette" },
{ name = "starlette" },
{ name = "structlog" },
{ name = "tenacity" },
{ name = "truststore" },
{ name = "uuid-utils" },
{ name = "uvicorn" },
{ name = "uvloop", marker = "sys_platform != 'win32'" },
{ name = "watchfiles" },
{ name = "websockets" },
{ name = "zstandard" },
{ name = "cloudpickle", marker = "python_full_version >= '3.11'" },
{ name = "cryptography", marker = "python_full_version >= '3.11'" },
{ name = "grpcio", marker = "python_full_version >= '3.11'" },
{ name = "grpcio-health-checking", marker = "python_full_version >= '3.11'" },
{ name = "grpcio-tools", marker = "python_full_version >= '3.11'" },
{ name = "httptools", marker = "python_full_version >= '3.11' and sys_platform != 'win32'" },
{ name = "httpx", marker = "python_full_version >= '3.11'" },
{ name = "jsonschema-rs", marker = "python_full_version >= '3.11'" },
{ name = "langchain-core", marker = "python_full_version >= '3.11'" },
{ name = "langchain-protocol", marker = "python_full_version >= '3.11'" },
{ name = "langgraph", marker = "python_full_version >= '3.11'" },
{ name = "langgraph-checkpoint", marker = "python_full_version >= '3.11'" },
{ name = "langgraph-runtime-inmem", marker = "python_full_version >= '3.11'" },
{ name = "langgraph-sdk", marker = "python_full_version >= '3.11'" },
{ name = "langsmith", extra = ["otel"], marker = "python_full_version >= '3.11'" },
{ name = "opentelemetry-api", marker = "python_full_version >= '3.11'" },
{ name = "opentelemetry-exporter-otlp-proto-http", marker = "python_full_version >= '3.11'" },
{ name = "opentelemetry-sdk", marker = "python_full_version >= '3.11'" },
{ name = "orjson", marker = "python_full_version >= '3.11'" },
{ name = "protobuf", marker = "python_full_version >= '3.11'" },
{ name = "pyjwt", marker = "python_full_version >= '3.11'" },
{ name = "sse-starlette", marker = "python_full_version >= '3.11'" },
{ name = "starlette", marker = "python_full_version >= '3.11'" },
{ name = "structlog", marker = "python_full_version >= '3.11'" },
{ name = "tenacity", marker = "python_full_version >= '3.11'" },
{ name = "truststore", marker = "python_full_version >= '3.11'" },
{ name = "uuid-utils", marker = "python_full_version >= '3.11'" },
{ name = "uvicorn", marker = "python_full_version >= '3.11'" },
{ name = "uvloop", marker = "python_full_version >= '3.11' and sys_platform != 'win32'" },
{ name = "watchfiles", marker = "python_full_version >= '3.11'" },
{ name = "websockets", marker = "python_full_version >= '3.11'" },
{ name = "zstandard", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7d/17/a23ecdad3de7e48209bc8d534312ba83e332126ec60da9e6487461b02597/langgraph_api-0.10.0.tar.gz", hash = "sha256:f594a160857e2cd7fb2352bba585111177a4a394625ad0cf5898814754fb7bc1", size = 713903, upload-time = "2026-06-11T14:36:50.469Z" }
wheels = [
@@ -1077,8 +1041,8 @@ name = "langgraph-checkpoint"
version = "4.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "langchain-core" },
{ name = "ormsgpack" },
{ name = "langchain-core", marker = "python_full_version >= '3.11'" },
{ name = "ormsgpack", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/83/47/886af6f886f0bff2273164a45f008694e48a96ff3cd25ff0228f2aa9480e/langgraph_checkpoint-4.1.1.tar.gz", hash = "sha256:6c2bdb530c91f91d7d9c1bd100925d0fc4f498d418c17f3587d1526279482a25", size = 184020, upload-time = "2026-05-22T16:57:38.503Z" }
wheels = [
@@ -1132,7 +1096,7 @@ test = [
requires-dist = [
{ name = "click", specifier = ">=8.1.7" },
{ name = "httpx", specifier = ">=0.24.0" },
{ name = "langgraph-api", marker = "python_full_version >= '3.11' and extra == 'inmem'", specifier = ">=0.5.35,<1.0.0" },
{ name = "langgraph-api", marker = "python_full_version >= '3.11' and extra == 'inmem'", specifier = ">=0.5.35,<0.12.0" },
{ name = "langgraph-runtime-inmem", marker = "python_full_version >= '3.11' and extra == 'inmem'", specifier = ">=0.7" },
{ name = "langgraph-sdk", marker = "python_full_version >= '3.11'", specifier = ">=0.1.0" },
{ name = "pathspec", specifier = ">=0.11.0" },
@@ -1171,8 +1135,8 @@ name = "langgraph-prebuilt"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "langchain-core" },
{ name = "langgraph-checkpoint" },
{ name = "langchain-core", marker = "python_full_version >= '3.11'" },
{ name = "langgraph-checkpoint", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/29/66/ed9b93f56bc17ef22d551892f0ac2b225a97fe0fcf23a511b857f70d590b/langgraph_prebuilt-1.1.0.tar.gz", hash = "sha256:3c579cf6eed2d17f9c157c2d0fcaddcd8688524e7022d3b22b37a3bf4589d528", size = 178833, upload-time = "2026-05-12T03:37:49.332Z" }
wheels = [
@@ -1184,13 +1148,13 @@ name = "langgraph-runtime-inmem"
version = "0.30.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "blockbuster" },
{ name = "croniter" },
{ name = "langgraph" },
{ name = "langgraph-checkpoint" },
{ name = "sse-starlette" },
{ name = "starlette" },
{ name = "structlog" },
{ name = "blockbuster", marker = "python_full_version >= '3.11'" },
{ name = "croniter", marker = "python_full_version >= '3.11'" },
{ name = "langgraph", marker = "python_full_version >= '3.11'" },
{ name = "langgraph-checkpoint", marker = "python_full_version >= '3.11'" },
{ name = "sse-starlette", marker = "python_full_version >= '3.11'" },
{ name = "starlette", marker = "python_full_version >= '3.11'" },
{ name = "structlog", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/98/19/5d330e22e3ee2569742d3577b4ddfbb6b8e383f2edd9724b4a4b73735717/langgraph_runtime_inmem-0.30.0.tar.gz", hash = "sha256:31954e1de6cd43c284b425243d4faeae818f68b3daca4dbe4313e651b6f5e11d", size = 129940, upload-time = "2026-06-11T14:22:46.985Z" }
wheels = [
@@ -1202,11 +1166,11 @@ name = "langgraph-sdk"
version = "0.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
{ name = "langchain-core" },
{ name = "langchain-protocol" },
{ name = "orjson" },
{ name = "websockets" },
{ name = "httpx", marker = "python_full_version >= '3.11'" },
{ name = "langchain-core", marker = "python_full_version >= '3.11'" },
{ name = "langchain-protocol", marker = "python_full_version >= '3.11'" },
{ name = "orjson", marker = "python_full_version >= '3.11'" },
{ name = "websockets", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b4/2b/bd8ac26d4e97f6df88ef05ce5b6a38945a3903e1025d926f4752aa88aa97/langgraph_sdk-0.4.2.tar.gz", hash = "sha256:b88f0f5f6328ac0680d6790614a905b2bcfa257f2276dba4e38f0e86db0aa738", size = 348327, upload-time = "2026-06-01T17:51:19.856Z" }
wheels = [
@@ -1218,16 +1182,16 @@ name = "langsmith"
version = "0.8.18"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
{ name = "orjson", marker = "platform_python_implementation != 'PyPy'" },
{ name = "packaging" },
{ name = "pydantic" },
{ name = "requests" },
{ name = "requests-toolbelt" },
{ name = "uuid-utils" },
{ name = "websockets" },
{ name = "xxhash" },
{ name = "zstandard" },
{ name = "httpx", marker = "python_full_version >= '3.11'" },
{ name = "orjson", marker = "python_full_version >= '3.11' and platform_python_implementation != 'PyPy'" },
{ name = "packaging", marker = "python_full_version >= '3.11'" },
{ name = "pydantic", marker = "python_full_version >= '3.11'" },
{ name = "requests", marker = "python_full_version >= '3.11'" },
{ name = "requests-toolbelt", marker = "python_full_version >= '3.11'" },
{ name = "uuid-utils", marker = "python_full_version >= '3.11'" },
{ name = "websockets", marker = "python_full_version >= '3.11'" },
{ name = "xxhash", marker = "python_full_version >= '3.11'" },
{ name = "zstandard", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9a/d9/a6681aa9847bbbc5ec21abe20a5e233b94e5edcfe39624db607ac7e8ccb4/langsmith-0.8.18.tar.gz", hash = "sha256:32dde9c0e67e053e0fb738921fc8ced768af7b8fa83d7a0e3fd63597cf8776dd", size = 4526988, upload-time = "2026-06-19T13:12:17.123Z" }
wheels = [
@@ -1236,9 +1200,9 @@ wheels = [
[package.optional-dependencies]
otel = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-exporter-otlp-proto-http" },
{ name = "opentelemetry-sdk" },
{ name = "opentelemetry-api", marker = "python_full_version >= '3.11'" },
{ name = "opentelemetry-exporter-otlp-proto-http", marker = "python_full_version >= '3.11'" },
{ name = "opentelemetry-sdk", marker = "python_full_version >= '3.11'" },
]
[[package]]
@@ -1332,8 +1296,8 @@ name = "opentelemetry-api"
version = "1.41.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "importlib-metadata" },
{ name = "typing-extensions" },
{ name = "importlib-metadata", marker = "python_full_version >= '3.11'" },
{ name = "typing-extensions", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/47/8e/3778a7e87801d994869a9396b9fc2a289e5f9be91ff54a27d41eace494b0/opentelemetry_api-1.41.0.tar.gz", hash = "sha256:9421d911326ec12dee8bc933f7839090cad7a3f13fcfb0f9e82f8174dc003c09", size = 71416, upload-time = "2026-04-09T14:38:34.544Z" }
wheels = [
@@ -1345,7 +1309,7 @@ name = "opentelemetry-exporter-otlp-proto-common"
version = "1.41.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-proto" },
{ name = "opentelemetry-proto", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8c/28/e8eca94966fe9a1465f6094dc5ddc5398473682180279c94020bc23b4906/opentelemetry_exporter_otlp_proto_common-1.41.0.tar.gz", hash = "sha256:966bbce537e9edb166154779a7c4f8ab6b8654a03a28024aeaf1a3eacb07d6ee", size = 20411, upload-time = "2026-04-09T14:38:36.572Z" }
wheels = [
@@ -1357,13 +1321,13 @@ name = "opentelemetry-exporter-otlp-proto-http"
version = "1.41.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "googleapis-common-protos" },
{ name = "opentelemetry-api" },
{ name = "opentelemetry-exporter-otlp-proto-common" },
{ name = "opentelemetry-proto" },
{ name = "opentelemetry-sdk" },
{ name = "requests" },
{ name = "typing-extensions" },
{ name = "googleapis-common-protos", marker = "python_full_version >= '3.11'" },
{ name = "opentelemetry-api", marker = "python_full_version >= '3.11'" },
{ name = "opentelemetry-exporter-otlp-proto-common", marker = "python_full_version >= '3.11'" },
{ name = "opentelemetry-proto", marker = "python_full_version >= '3.11'" },
{ name = "opentelemetry-sdk", marker = "python_full_version >= '3.11'" },
{ name = "requests", marker = "python_full_version >= '3.11'" },
{ name = "typing-extensions", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/19/63/d9f43cd75f3fabb7e01148c89cfa9491fc18f6580a6764c554ff7c953c46/opentelemetry_exporter_otlp_proto_http-1.41.0.tar.gz", hash = "sha256:dcd6e0686f56277db4eecbadd5262124e8f2cc739cadbc3fae3d08a12c976cf5", size = 24139, upload-time = "2026-04-09T14:38:38.128Z" }
wheels = [
@@ -1375,7 +1339,7 @@ name = "opentelemetry-proto"
version = "1.41.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "protobuf" },
{ name = "protobuf", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e0/d9/08e3dc6156878713e8c811682bc76151f5fe1a3cb7f3abda3966fd56e71e/opentelemetry_proto-1.41.0.tar.gz", hash = "sha256:95d2e576f9fb1800473a3e4cfcca054295d06bdb869fda4dc9f4f779dc68f7b6", size = 45669, upload-time = "2026-04-09T14:38:45.978Z" }
wheels = [
@@ -1387,9 +1351,9 @@ name = "opentelemetry-sdk"
version = "1.41.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-semantic-conventions" },
{ name = "typing-extensions" },
{ name = "opentelemetry-api", marker = "python_full_version >= '3.11'" },
{ name = "opentelemetry-semantic-conventions", marker = "python_full_version >= '3.11'" },
{ name = "typing-extensions", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f8/0e/a586df1186f9f56b5a0879d52653effc40357b8e88fc50fe300038c3c08b/opentelemetry_sdk-1.41.0.tar.gz", hash = "sha256:7bddf3961131b318fc2d158947971a8e37e38b1cd23470cfb72b624e7cc108bd", size = 230181, upload-time = "2026-04-09T14:38:47.225Z" }
wheels = [
@@ -1401,8 +1365,8 @@ name = "opentelemetry-semantic-conventions"
version = "0.62b0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "typing-extensions" },
{ name = "opentelemetry-api", marker = "python_full_version >= '3.11'" },
{ name = "typing-extensions", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a3/b0/c14f723e86c049b7bf8ff431160d982519b97a7be2857ed2247377397a24/opentelemetry_semantic_conventions-0.62b0.tar.gz", hash = "sha256:cbfb3c8fc259575cf68a6e1b94083cc35adc4a6b06e8cf431efa0d62606c0097", size = 145753, upload-time = "2026-04-09T14:38:48.274Z" }
wheels = [
@@ -1632,10 +1596,10 @@ name = "pydantic"
version = "2.13.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
{ name = "pydantic-core" },
{ name = "typing-extensions" },
{ name = "typing-inspection" },
{ name = "annotated-types", marker = "python_full_version >= '3.11'" },
{ name = "pydantic-core", marker = "python_full_version >= '3.11'" },
{ name = "typing-extensions", marker = "python_full_version >= '3.11'" },
{ name = "typing-inspection", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/09/e5/06d23afac9973109d1e3c8ad38e1547a12e860610e327c05ee686827dc37/pydantic-2.13.2.tar.gz", hash = "sha256:b418196607e61081c3226dcd4f0672f2a194828abb9109e9cfb84026564df2d1", size = 843836, upload-time = "2026-04-17T09:31:59.636Z" }
wheels = [
@@ -1647,7 +1611,7 @@ name = "pydantic-core"
version = "2.46.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
{ name = "typing-extensions", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/43/bb/4742f05b739b2478459bb16fa8470549518c802e06ddcf3f106c5081315e/pydantic_core-2.46.2.tar.gz", hash = "sha256:37bb079f9ee3f1a519392b73fda2a96379b31f2013c6b467fe693e7f2987f596", size = 471269, upload-time = "2026-04-17T09:10:07.017Z" }
wheels = [
@@ -1787,7 +1751,7 @@ wheels = [
[[package]]
name = "pytest"
version = "9.1.1"
version = "9.0.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
@@ -1798,9 +1762,9 @@ dependencies = [
{ name = "pygments" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
]
[[package]]
@@ -1846,7 +1810,7 @@ name = "python-dateutil"
version = "2.9.0.post0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "six" },
{ name = "six", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
wheels = [
@@ -1953,10 +1917,10 @@ name = "requests"
version = "2.33.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
{ name = "certifi", marker = "python_full_version >= '3.11'" },
{ name = "charset-normalizer", marker = "python_full_version >= '3.11'" },
{ name = "idna", marker = "python_full_version >= '3.11'" },
{ name = "urllib3", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" }
wheels = [
@@ -1968,7 +1932,7 @@ name = "requests-toolbelt"
version = "1.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "requests" },
{ name = "requests", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" }
wheels = [
@@ -1990,27 +1954,27 @@ wheels = [
[[package]]
name = "ruff"
version = "0.16.0"
version = "0.15.15"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4d/94/1e5e4967626faf12fa56999cd6222dff6992ceb086ad7945756baf70c7a7/ruff-0.16.0.tar.gz", hash = "sha256:e460aafd5495ec89efaa6ced2e4a9a581116451e1c88b9d37ef497e0f8e93982", size = 4790557, upload-time = "2026-07-23T19:11:30.981Z" }
sdist = { url = "https://files.pythonhosted.org/packages/84/6f/a76f7d96e5c962f5b69cee865e49c15c1116897c01990faa8a57edb62e7f/ruff-0.15.15.tar.gz", hash = "sha256:b8dff018130b46d8e5bf0f926ef6b60cf871d6d5ae45fc9334e09632daa741d6", size = 4706985, upload-time = "2026-05-28T14:16:57.784Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4b/81/1c8818fee7ce1a04cd7d1b3172e0a8f8e4f1dc4feb7fc390e16daa8af323/ruff-0.16.0-py3-none-linux_armv6l.whl", hash = "sha256:e5115729eb08c585e5121978ba5d5b60caeae394ce21b9fb5e6cd33a1c6c9b1e", size = 10754633, upload-time = "2026-07-23T19:10:46.415Z" },
{ url = "https://files.pythonhosted.org/packages/23/df/beaf59c09d68db84304d555f188b276a77132a5d5b0b67a5c762aa143628/ruff-0.16.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3c954b1d580bfa035b41654f7858cc7e71d5fc3ac5b723dd62bd9133830ed522", size = 10969164, upload-time = "2026-07-23T19:10:50.271Z" },
{ url = "https://files.pythonhosted.org/packages/42/ce/741cd197496a1abbf51352710fd15ed995d2a2be87189c1da26a450d6e83/ruff-0.16.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e01c21d10eb1b29f47b7454e1f4056db9a3f0260c646aa88457c610291db9f81", size = 10488846, upload-time = "2026-07-23T19:10:52.639Z" },
{ url = "https://files.pythonhosted.org/packages/52/2a/a2db8e88cade358f5cdcb05674a917751074109315d014eb6352d9a893f7/ruff-0.16.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e364e5ed22ed8dc05082fd78e35308618260907ac2d3c1d637b2e682415b6c9", size = 10889729, upload-time = "2026-07-23T19:10:54.89Z" },
{ url = "https://files.pythonhosted.org/packages/42/65/62a771694ebd63029dc953e27dbad40e1588bd4860ff9fe881018fddaa49/ruff-0.16.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d327b8fc113a1d4421a04f3839d3752057c8dd1ee320223a6f3f52d04ada462a", size = 10568275, upload-time = "2026-07-23T19:10:56.993Z" },
{ url = "https://files.pythonhosted.org/packages/3f/e2/ced249fe8af5f086c5c58cc21cc3356d50f32f7401c5df87050c999620a7/ruff-0.16.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b50c55e263103586b3dcf5f73d479eb8cb5fdb6098fec59a62891dab653717", size = 11385112, upload-time = "2026-07-23T19:10:59.615Z" },
{ url = "https://files.pythonhosted.org/packages/87/0b/05154977a8fd69eeb6c103271f55403bfd8711f5c0f8ed07489d95a504e7/ruff-0.16.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ff4a79ce3ec0172f3241943835de1c4cb4e2dcd07f0f8c2d02603dbbbee4b17", size = 12207008, upload-time = "2026-07-23T19:11:02.154Z" },
{ url = "https://files.pythonhosted.org/packages/fb/29/98225831a3a1eab0e02f4acc6ca6559a98611dcc68b6965ff4b7234627c1/ruff-0.16.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e95c448fca1fb2a18372a9440926c5a6ee789639bb975c72e7ae6d0b04218ab4", size = 11650842, upload-time = "2026-07-23T19:11:04.557Z" },
{ url = "https://files.pythonhosted.org/packages/91/66/6bd3cf90500653d55dc0ffc8507aa8300bd49d0214b2e8cb4d3fef2943ba/ruff-0.16.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f11a8d11010301d0a398a2fdef67691feca7294da6aef55e2150e8fa2cd520b", size = 11400718, upload-time = "2026-07-23T19:11:09.233Z" },
{ url = "https://files.pythonhosted.org/packages/8e/a2/a54eb4eae05d66364050a5d3b8a9c5ef88196531b3cbe7109d873f87f819/ruff-0.16.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:48044c678e9cb8698246c99b14aaccfa6601dea7379eb48a6f8f73f7a6d86cd0", size = 11426177, upload-time = "2026-07-23T19:11:11.994Z" },
{ url = "https://files.pythonhosted.org/packages/1a/be/16e3eea4b2a478a496919f5e36f17c4559e54620bd3bbac5d6affa068006/ruff-0.16.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7aa0959bad8eb8bef50340154fc9b58678dae31fa4293afa38b44b6e552c0213", size = 10856126, upload-time = "2026-07-23T19:11:14.221Z" },
{ url = "https://files.pythonhosted.org/packages/a2/84/252eb8b868a16eec7257c14f504f77537e734b2d69c762e639e588e304a3/ruff-0.16.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:28ea2b7df8ebf7f9da6b7d47b230ab48f387c0a29be3b474c4d0740e197bb9af", size = 10571208, upload-time = "2026-07-23T19:11:16.378Z" },
{ url = "https://files.pythonhosted.org/packages/21/09/817a482f542f7570cbb4554b26e896610c7114f539b1d9e2d2145bf6bef6/ruff-0.16.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:33a3dfac8c35f81498dea9181bccc2f4c4bc8f1521a1dd9406e77643e0f0fb09", size = 11063329, upload-time = "2026-07-23T19:11:19.173Z" },
{ url = "https://files.pythonhosted.org/packages/2e/23/9403c180ca1cb9b1f7335f5c3e5305c09d49ea5b345196682a36028bde4a/ruff-0.16.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a5237a0bda500d30d81b8e07a6973a5cbc772864cbf746ae2f4e8a2e01c9f4ed", size = 11489751, upload-time = "2026-07-23T19:11:21.74Z" },
{ url = "https://files.pythonhosted.org/packages/b2/1d/1b2ef7bcde851c78d7f17f1cca13fd6dc695fc4b3d6197941e72cae5b132/ruff-0.16.0-py3-none-win32.whl", hash = "sha256:7fab76fa065c873f41ff744347c6e77bcc3dfec4bcc754dc26b63d23c0f7f5fb", size = 10785885, upload-time = "2026-07-23T19:11:23.947Z" },
{ url = "https://files.pythonhosted.org/packages/b2/a3/d5e4ef7a56be3f928ffb90b94c25ba7d3cb9c7fe0736aeaaedf361770712/ruff-0.16.0-py3-none-win_amd64.whl", hash = "sha256:429c117f022bf481fabd9d551e7a3952b24c65e6ef44337ea09d90bebef14472", size = 11923141, upload-time = "2026-07-23T19:11:26.409Z" },
{ url = "https://files.pythonhosted.org/packages/cb/9a/8415f2657cbe200f41a4531ccededf135505a92d4a012229121f885b26f9/ruff-0.16.0-py3-none-win_arm64.whl", hash = "sha256:14296fedcd2705c77ab8235439278bbb38f285cf7da5528b00b3e330c3d4872d", size = 11273407, upload-time = "2026-07-23T19:11:28.705Z" },
{ url = "https://files.pythonhosted.org/packages/fa/9d/3a45c05b8ab04b4705989de70a79008e27c8003296a0feaee9edc18dd7e9/ruff-0.15.15-py3-none-linux_armv6l.whl", hash = "sha256:cf93e5388f412e1b108b1f8b34a6e036b70fe8aff89393befad96fe48670311b", size = 10710652, upload-time = "2026-05-28T14:16:06.701Z" },
{ url = "https://files.pythonhosted.org/packages/05/66/da974431624bf3b49f6ee1f9543c02d929ff1cba78b0d5a79c38cf21f744/ruff-0.15.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac5a646d1f6a7dadd5d50842dae2c1f9862ac887ef5d1b1375e02def791fde6e", size = 11096615, upload-time = "2026-05-28T14:16:23.313Z" },
{ url = "https://files.pythonhosted.org/packages/8c/09/7443452e5d290230a712103f2fdceeef7184f3ec99a2bd01c8be78aaceb5/ruff-0.15.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:77d955a431430c66f72dd94e379ad38a16daea3d25094872ac4edf9e797be530", size = 10436683, upload-time = "2026-05-28T14:16:40.974Z" },
{ url = "https://files.pythonhosted.org/packages/53/01/d330c26a57fa4f3943a14424904027428315b700fe4d14a84bb123a649e5/ruff-0.15.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7614ee79c69788cf6cedd568069ade9cecc22a1ad20494efe8d0c9ebb4b622d4", size = 10769064, upload-time = "2026-05-28T14:16:28.905Z" },
{ url = "https://files.pythonhosted.org/packages/1d/85/cc8770f8bdff541b1da8392d1634141fe4a0e3f4ee596605959b7906c27f/ruff-0.15.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3cdb1679e06a1f6b47bc384714ae96f6e2fb65ca441eb78c43d2ca554176ce1f", size = 10511987, upload-time = "2026-05-28T14:16:43.732Z" },
{ url = "https://files.pythonhosted.org/packages/7c/29/8c190c1472b63013583ba391f3342036e02010544c1270455ed8e519bdf3/ruff-0.15.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2728b93d7b23a603ea2c0ac6eb73d760bd38ec9de35f35fb41e18f7a3fee7622", size = 11275100, upload-time = "2026-05-28T14:16:55.244Z" },
{ url = "https://files.pythonhosted.org/packages/9f/6b/7e145ce2cc8e63d6834eca03d83a0e18d121def5c69f91b4cf4011ed4879/ruff-0.15.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be582fcc0db438902c7792b08d6ddf6c9b9e21addaa10092c2c741cfb09e5a45", size = 12176903, upload-time = "2026-05-28T14:16:14.368Z" },
{ url = "https://files.pythonhosted.org/packages/80/a3/d5974637f68e451f7fadf015cf3101d1cd7d8ba5027cffe0b9e3826ebe6b/ruff-0.15.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7aa77465b8ecaf1a27bea098d696f7fed5e1eccbd10b321b682d6de586ae5627", size = 11404550, upload-time = "2026-05-28T14:16:20.138Z" },
{ url = "https://files.pythonhosted.org/packages/fe/1c/e6e5e568f22be4fb05d6244234aba384c06b451252453b821e1a529263cf/ruff-0.15.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48decfa11d740de4889de623be1463308346312f2409a56e24aa280c86162dc4", size = 11382027, upload-time = "2026-05-28T14:16:46.615Z" },
{ url = "https://files.pythonhosted.org/packages/1d/01/170921b49fcd2e8858825593f91cf7146c3e40a5c3e6df763e4bb0484dde/ruff-0.15.15-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a5015088452ca0081387063649ec67f06d3d1d6b8b936a1f836b5e9657ecd48c", size = 11366041, upload-time = "2026-05-28T14:16:26.247Z" },
{ url = "https://files.pythonhosted.org/packages/87/54/a7bad711d7de93254e15e06a4c375b89a03d18de45d3e5dcc86a4472fb1a/ruff-0.15.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5294aab6356c81600fcdea3a62bb1b924dfd5e91767c12318d3f68f86af57cd", size = 10741795, upload-time = "2026-05-28T14:16:17.11Z" },
{ url = "https://files.pythonhosted.org/packages/c9/31/38c075963668f8b41c6914ee0f6f318727fbe30ab9145cb29e6df464c5fa/ruff-0.15.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:db5bd4d802415cca656dc1616070b725952d6ae95eb5d4831e49fbd94a38f75f", size = 10511117, upload-time = "2026-05-28T14:16:31.767Z" },
{ url = "https://files.pythonhosted.org/packages/9d/96/6ff689e1f7e375d1d97075eca022f74c2bab59554a432fe4d2e6f091986a/ruff-0.15.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:587a6278ed42059191c1a466e490bd7930fb50bd2e255398bc29616c895a61cb", size = 10994867, upload-time = "2026-05-28T14:16:35.149Z" },
{ url = "https://files.pythonhosted.org/packages/c3/c2/5dce0ab9f92a8d534fa62b9bf9caca3eddb8c1a81b616f5e195ada4f0d6e/ruff-0.15.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:df0c1c084f5f4be9812f61518a45c440d3c30d69ce4bf6c5270e66d38338f02a", size = 11482101, upload-time = "2026-05-28T14:16:49.598Z" },
{ url = "https://files.pythonhosted.org/packages/b1/c0/1003b60edd697c649faf61f1a34094b1abb38fb3d1181e3f895781250a08/ruff-0.15.15-py3-none-win32.whl", hash = "sha256:29428ea79694afbe756d45fd59b36f22b6b020dc0443cf7de0173046236964b9", size = 10716774, upload-time = "2026-05-28T14:16:52.337Z" },
{ url = "https://files.pythonhosted.org/packages/02/a8/1269eddd6945a06c23f055ef7848886e37cf9d6a8bebb386a3115f01470c/ruff-0.15.15-py3-none-win_amd64.whl", hash = "sha256:8df0323902e15e24bc4bf246da830573d3cf3352bd0b9a164eab335d111ff4a4", size = 11868463, upload-time = "2026-05-28T14:16:11.333Z" },
{ url = "https://files.pythonhosted.org/packages/4e/b2/920464c907b191e37469d477a1aa8bc048b8f36c4c1610dfa4ab87b39e18/ruff-0.15.15-py3-none-win_arm64.whl", hash = "sha256:3c8ceca6792f38196b8f589bc92eccd03eef286602da92e5dc05cc42ef6441b7", size = 11138498, upload-time = "2026-05-28T14:16:38.425Z" },
]
[[package]]
@@ -2028,11 +1992,11 @@ wheels = [
[[package]]
name = "setuptools"
version = "83.0.0"
version = "82.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/34/26/f5d29e25ffdb535afef2d35cdb55b325298f96debd670da4c325e08d70f4/setuptools-83.0.0.tar.gz", hash = "sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef", size = 1154254, upload-time = "2026-07-04T15:31:22.699Z" }
sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5d/40/e1e72872c6354b306daef1703549e8e83b4d43cfea356311bf722a043752/setuptools-83.0.0-py3-none-any.whl", hash = "sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3", size = 1008090, upload-time = "2026-07-04T15:31:20.885Z" },
{ url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" },
]
[[package]]
@@ -2058,8 +2022,8 @@ name = "sse-starlette"
version = "3.3.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "starlette" },
{ name = "anyio", marker = "python_full_version >= '3.11'" },
{ name = "starlette", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/26/8c/f9290339ef6d79badbc010f067cd769d6601ec11a57d78569c683fb4dd87/sse_starlette-3.3.4.tar.gz", hash = "sha256:aaf92fc067af8a5427192895ac028e947b484ac01edbc3caf00e7e7137c7bef1", size = 32427, upload-time = "2026-03-29T09:00:23.307Z" }
wheels = [
@@ -2071,8 +2035,8 @@ name = "starlette"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
{ name = "anyio", marker = "python_full_version >= '3.11'" },
{ name = "typing-extensions", marker = "python_full_version >= '3.11' and python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" }
wheels = [
@@ -2189,27 +2153,27 @@ wheels = [
[[package]]
name = "ty"
version = "0.0.64"
version = "0.0.43"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8e/aa/14c9965d3b173105692473897cc89c34cd91241368b2044e43167e1c17ff/ty-0.0.64.tar.gz", hash = "sha256:d12ddbb05f15158bb518af619378b385486450def95fb06f8ab98037febe9f2c", size = 6350966, upload-time = "2026-07-27T18:32:45.403Z" }
sdist = { url = "https://files.pythonhosted.org/packages/0d/37/4ec04de0659b93be37d956dfceca13b1ecab9c959f28d8a1d5e514603f36/ty-0.0.43.tar.gz", hash = "sha256:ea4cff50548f2a1877e848d3abe9e293cde8ab94757a7eb93fc0d4013f98be8e", size = 5798429, upload-time = "2026-06-04T00:52:10.013Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ae/4c/c54937e4ff3fa7b34a99ea3387ec766bf0ad98dc8df8d792e89b388e658e/ty-0.0.64-py3-none-linux_armv6l.whl", hash = "sha256:3830a6675ab43635ced1c4c557f380ac4a49e9414e03975e4e4e8db644c64944", size = 12118357, upload-time = "2026-07-27T18:32:08.702Z" },
{ url = "https://files.pythonhosted.org/packages/ce/aa/a839ee2bc78e943d079e6abe199a97b4eeffb7e5c9a57326d69de452186a/ty-0.0.64-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3ff07d7bc32a2135f58afe57393789a32ea2fed1a66216129a8e535e76043903", size = 11790882, upload-time = "2026-07-27T18:32:10.997Z" },
{ url = "https://files.pythonhosted.org/packages/4c/de/19f14357888a7198438926303753cf749428e3d62e8980ff1e9a72a78402/ty-0.0.64-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4f6d1c7f897cca05d12bacbf1435150d5ffa496099515aa6ed303c8b29e1d0bb", size = 11317394, upload-time = "2026-07-27T18:32:13.162Z" },
{ url = "https://files.pythonhosted.org/packages/08/2f/f54462300535ab99b551eda733177be2eef5dbc2997d3fdb357c4ddd760a/ty-0.0.64-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:138d6c37ad4bf8583aa7a9b29d90954151d7856f9910a03eae3eb34b34c57215", size = 11863042, upload-time = "2026-07-27T18:32:15.307Z" },
{ url = "https://files.pythonhosted.org/packages/5e/95/dbecf745520ebe8bd7b02fc55eee6441c9be312ebf6addce605eb52740dd/ty-0.0.64-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ed9719d1b7b66fb8efe073d860208a44c40af1f6cd5c2364aa9b323a1e579b4", size = 11910730, upload-time = "2026-07-27T18:32:17.467Z" },
{ url = "https://files.pythonhosted.org/packages/3b/26/12cfd40028e51ceed7b3cb645281c61c02eb64ff9fb0c09231d65c30ff25/ty-0.0.64-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d68b23e5169e2137b5f1de7169ab0cebecab6c8eda374c34c1f6394308f58242", size = 12631936, upload-time = "2026-07-27T18:32:19.533Z" },
{ url = "https://files.pythonhosted.org/packages/a0/d0/65ffc2b0a686347193c6f98e9421a7fc2a96fc3cd0b1001cf7cf284baff9/ty-0.0.64-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f41cb07d89d32626fcaf3ed4d262778fcb28b2628b6ed1e172cd7b18820668d8", size = 13171049, upload-time = "2026-07-27T18:32:22.026Z" },
{ url = "https://files.pythonhosted.org/packages/f6/a4/975a5961842dcd6fa60f0770a102c0bba7509da909ab652919bfcdcd4fc7/ty-0.0.64-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5f24e1504ab9e212f92356b82fe088fdeb3a39f9a2f4ff25e505d2e1d0db9056", size = 12826438, upload-time = "2026-07-27T18:32:24.178Z" },
{ url = "https://files.pythonhosted.org/packages/af/ef/dfb9b7f9bcc032d3b540b0d1f55f532a336e2fb41b1bd539c05ae81a151a/ty-0.0.64-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86db830cb914bb33bb8247b66ccea58de4496c2391bd42658aba744b439f3290", size = 12440880, upload-time = "2026-07-27T18:32:26.341Z" },
{ url = "https://files.pythonhosted.org/packages/ee/d0/bedac20505e8a8f5501ad73d7d15d8e421a563fef59993909a23036929a4/ty-0.0.64-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:652ee3d6d03bea76cd2fe8949c78bb5970394ebd7c5fd270e9518c0dee1b931d", size = 12782439, upload-time = "2026-07-27T18:32:28.642Z" },
{ url = "https://files.pythonhosted.org/packages/79/d5/795733f13ceff1378f08b3de0c49d0f518df220ed856b0dfac869f3b7c81/ty-0.0.64-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4838768295774a86e95f9ec5633e739d016adbbb69dcbdc62f3549578a11f624", size = 11814821, upload-time = "2026-07-27T18:32:30.632Z" },
{ url = "https://files.pythonhosted.org/packages/52/3e/9d99cd1e1831003434f508ed9f258a56543194afc3bbe051eba2545fa676/ty-0.0.64-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5a3d700669868599edf39ce5125196682f15a8880cc8c669bc2a83b99984d99b", size = 11928678, upload-time = "2026-07-27T18:32:32.888Z" },
{ url = "https://files.pythonhosted.org/packages/b8/3d/448f49a3503fb119a34348a5714bb92001f252fadbbb12d645f2e744b557/ty-0.0.64-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b161f0a82a8e2f2432db3bf7702b4d3924fa9486ba0014f6710a160fc157df0d", size = 12202249, upload-time = "2026-07-27T18:32:34.905Z" },
{ url = "https://files.pythonhosted.org/packages/dd/9b/75768e562cec990d189dc05807ae72890b20ffbd1e1f43597bb98db63c60/ty-0.0.64-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:39b9dd42908df47c2dc57dda87e656fab97097ffd2618474bbdea986af0d6a9d", size = 12548817, upload-time = "2026-07-27T18:32:36.995Z" },
{ url = "https://files.pythonhosted.org/packages/34/89/44cc276ea6ca0245495014758ffeadde1798fb0b5840c9cf36d8d2ed3250/ty-0.0.64-py3-none-win32.whl", hash = "sha256:d0676ab0e0935795e5843baa28dd5e366dc343d7c0945a996df9eab8e0644885", size = 11545474, upload-time = "2026-07-27T18:32:39.389Z" },
{ url = "https://files.pythonhosted.org/packages/01/7e/d1c8a871a38d17c8f168b9a6975f6247f7660f8334e517656a5e4b4a4858/ty-0.0.64-py3-none-win_amd64.whl", hash = "sha256:dcb9bd31f54097e362b776c26ab4564d4564cdd1355cb883481167a26c03cc3f", size = 12542987, upload-time = "2026-07-27T18:32:41.412Z" },
{ url = "https://files.pythonhosted.org/packages/35/4d/6d18640d0204cacd69abbaca95ad6a34c6d7e9169e9051d0117b17b827ec/ty-0.0.64-py3-none-win_arm64.whl", hash = "sha256:82cc34c1ad9a8feb6059aef193bebcecc656e548f6fab3d518bcd8b57d198d39", size = 11899263, upload-time = "2026-07-27T18:32:43.366Z" },
{ url = "https://files.pythonhosted.org/packages/db/74/1916026a78f20019a2f03adbd6fb4430ddb7ce1e52c2e17a90856a6d192e/ty-0.0.43-py3-none-linux_armv6l.whl", hash = "sha256:3bf70f5446480562bf6c9f639df4b5cb60716b8f8d1a6b8e5811d5c7eccd8bf2", size = 11598153, upload-time = "2026-06-04T00:52:20.646Z" },
{ url = "https://files.pythonhosted.org/packages/b9/af/58bb0089d2635216c8fa6612dd486a3f986d0ab1c46a41527ab95e57f0e3/ty-0.0.43-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7184741f8b15425a1bc64b950ad005cb353573288ac0e8a04f5481ceb3832596", size = 11357811, upload-time = "2026-06-04T00:52:24.683Z" },
{ url = "https://files.pythonhosted.org/packages/d6/9c/32c6b14f3feddf87b59c7a50709e2b3da408258f2f583f05575f77bc8f7b/ty-0.0.43-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8c306379ca9a35f6ae5270fe9bda7af4b46d91822725a2586d78c8b9b5493b62", size = 10772024, upload-time = "2026-06-04T00:52:14.312Z" },
{ url = "https://files.pythonhosted.org/packages/09/fa/98aa4a74bd00cd5efc424923cd1daffbf1e40a0338041cafb203379d746f/ty-0.0.43-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d624b884c9c1fd244ad2a5f026364e7162a22b3f537025941ada2e363e676414", size = 11291034, upload-time = "2026-06-04T00:52:37.249Z" },
{ url = "https://files.pythonhosted.org/packages/b5/db/4de086c38ce96dcada2bd451f43171d2c237f96d8ed19a1ea8fe51bb8ef4/ty-0.0.43-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:281fc4c00fbc196045141faa085055bddc58846b04a2800204701415a1b9c6aa", size = 11364724, upload-time = "2026-06-04T00:52:33.138Z" },
{ url = "https://files.pythonhosted.org/packages/b0/d3/e3cd8e3233a6fd8362a49aa025b79e9f40151a2a86d811ace154c6eb7445/ty-0.0.43-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f57d6cc28de89024b48d1788e4758c05299d5749d4a51c02e71ac655ec23d9a5", size = 11890555, upload-time = "2026-06-04T00:52:22.711Z" },
{ url = "https://files.pythonhosted.org/packages/80/7b/6f46d444e8241606bbde098df3dca93f2ec0b834a42055db85ee7d33646f/ty-0.0.43-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0a1d6ad6c5e7792c7eac0a01e550f2c2004462e01a64a91ea1636aba6fef6e71", size = 12450968, upload-time = "2026-06-04T00:52:28.94Z" },
{ url = "https://files.pythonhosted.org/packages/4a/e1/79fbe51f2e4b9d8347f2013cd7ed0b63f3b499038c02dc0357e9b28a3a47/ty-0.0.43-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:66d474395d7635fb618bdbb58b4e3360259a2056d0a5621b82754b9da2cd8a04", size = 12064187, upload-time = "2026-06-04T00:52:12.039Z" },
{ url = "https://files.pythonhosted.org/packages/9b/3f/c758a3a8df5b90d331f2b60c8f16021ee64d75e78f99d67cc4efc9bf5f4b/ty-0.0.43-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2663a0003a8b60fb98db7f6f6e673df80b21d0fe3a9868a26fb06b4e049b6fc4", size = 11943208, upload-time = "2026-06-04T00:52:31.14Z" },
{ url = "https://files.pythonhosted.org/packages/54/5f/f516442749cf1b45ca6720a5d41df2738a486ed9ace774c03d515db89084/ty-0.0.43-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:d5a6c352d374d889189d5ec82b54b26a5885f769f7b7787f7f875500dcb8673e", size = 12143572, upload-time = "2026-06-04T00:52:18.457Z" },
{ url = "https://files.pythonhosted.org/packages/b7/bf/0d83c7f43bf4c10f3678bfe7d938e51c445298c7b923f155c5204730c2df/ty-0.0.43-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e7dbbeedfad3ca250d74fcc355fa9ab6b38d2a17f22d6304f615716939dbbb27", size = 11279355, upload-time = "2026-06-04T00:52:26.726Z" },
{ url = "https://files.pythonhosted.org/packages/3e/de/a6c978bef6d9e949f79f4782d9e4ee4df0893713e73b055d84c1a5116b9a/ty-0.0.43-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:24b18a0273ee46154996cfcfa27438f851f440c925587ec200df6f98dffe67d3", size = 11408412, upload-time = "2026-06-04T00:52:35.282Z" },
{ url = "https://files.pythonhosted.org/packages/ec/b1/d13857c23867f0f76b92e38e5841c64ca5e76dc5d4bf27f52cb81d8ab685/ty-0.0.43-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2ef681951520d692b7e9c0b5e56aacf4f98ccae47cf6ffccaf2c7b6b33dc226e", size = 11541709, upload-time = "2026-06-04T00:52:16.451Z" },
{ url = "https://files.pythonhosted.org/packages/7c/f1/cd6afc6f6a687e238bf5e12189f7920e81a0bdef6c3dba4c784ef140f7d9/ty-0.0.43-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2af105de7437143aa4676b28016b5bee661aaaa4eff52be5867fb25119641ceb", size = 12041266, upload-time = "2026-06-04T00:52:43.541Z" },
{ url = "https://files.pythonhosted.org/packages/bd/ba/51ca7c3335da2b8d0a3e477fa4986be9f4a53b05bfab862967d8d2e6ca60/ty-0.0.43-py3-none-win32.whl", hash = "sha256:e4773115b0d6486ee30f1657fc8bdffe7e3a3f5300ab77ef2495da6e83e4694f", size = 10858724, upload-time = "2026-06-04T00:52:07.843Z" },
{ url = "https://files.pythonhosted.org/packages/9f/29/5d80453e5f7c520145fa058851da87230dbd7ca761a7675447a9fe504e0b/ty-0.0.43-py3-none-win_amd64.whl", hash = "sha256:48d3545094a4ae6395492c7e6ac90550fce969e0ed2815fbf8c5da9756676b7d", size = 11976157, upload-time = "2026-06-04T00:52:41.438Z" },
{ url = "https://files.pythonhosted.org/packages/dc/ed/befe5a543e5b95e754ed38ee95239e44efda9bc5f578db4ac1bc8dd758d6/ty-0.0.43-py3-none-win_arm64.whl", hash = "sha256:740ca33d7f75f655a4e7d475bc42dfb825c13219bb073fad30fcc04d35790c74", size = 11308680, upload-time = "2026-06-04T00:52:39.233Z" },
]
[[package]]
@@ -2226,7 +2190,7 @@ name = "typing-inspection"
version = "0.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
{ name = "typing-extensions", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
wheels = [
@@ -2314,8 +2278,8 @@ name = "uvicorn"
version = "0.44.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "h11" },
{ name = "click", marker = "python_full_version >= '3.11'" },
{ name = "h11", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5e/da/6eee1ff8b6cbeed47eeb5229749168e81eb4b7b999a1a15a7176e51410c9/uvicorn-0.44.0.tar.gz", hash = "sha256:6c942071b68f07e178264b9152f1f16dfac5da85880c4ce06366a96d70d4f31e", size = 86947, upload-time = "2026-04-06T09:23:22.826Z" }
wheels = [
@@ -2419,7 +2383,7 @@ name = "watchfiles"
version = "1.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "anyio", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" }
wheels = [
@@ -68,7 +68,7 @@ def _create_root_model(
def schema(
cls: type[BaseModel],
by_alias: bool = True,
by_alias: bool = True, # noqa: FBT001,FBT002
ref_template: str = DEFAULT_REF_TEMPLATE,
) -> dict[str, Any]:
# Complains about schema not being defined in superclass
@@ -80,7 +80,7 @@ def _create_root_model(
def model_json_schema(
cls: type[BaseModel],
by_alias: bool = True,
by_alias: bool = True, # noqa: FBT001,FBT002
ref_template: str = DEFAULT_REF_TEMPLATE,
schema_generator: type[GenerateJsonSchema] = GenerateJsonSchema,
mode: JsonSchemaMode = "validation",
@@ -3,7 +3,6 @@ from __future__ import annotations
import asyncio
import enum
import inspect
import logging
import sys
import warnings
from collections.abc import (
@@ -64,26 +63,6 @@ try:
except ImportError:
_StreamingCallbackHandler = None # type: ignore
logger = logging.getLogger(__name__)
def _trace_payload(value: Any, transform: Callable[[Any], Any] | None) -> Any:
"""Return the payload to record on a run for `value`.
When `transform` is unset this is a passthrough, so unspecified nodes record exactly
as before. When set it always runs (regardless of tracing), but never affects
execution: if it raises, the untransformed value is recorded instead.
"""
if transform is None:
return value
try:
return transform(value)
except Exception:
logger.exception(
"trace input/output processor raised; recording untransformed payload"
)
return value
def _set_config_context(
config: RunnableConfig, run: Any = None
@@ -593,7 +572,6 @@ class RunnableSeq(Runnable):
*steps: RunnableLike,
name: str | None = None,
trace_inputs: Callable[[Any], Any] | None = None,
trace_outputs: Callable[[Any], Any] | None = None,
) -> None:
"""Create a new RunnableSeq.
@@ -619,7 +597,6 @@ class RunnableSeq(Runnable):
self.steps = steps_flat
self.name = name
self.trace_inputs = trace_inputs
self.trace_outputs = trace_outputs
def __or__(
self,
@@ -681,7 +658,7 @@ class RunnableSeq(Runnable):
# start the root run
run_manager = callback_manager.on_chain_start(
None,
_trace_payload(input, self.trace_inputs),
self.trace_inputs(input) if self.trace_inputs is not None else input,
name=config.get("run_name") or self.get_name(),
run_id=config.pop("run_id", None),
)
@@ -712,7 +689,7 @@ class RunnableSeq(Runnable):
run_manager.on_chain_error(e)
raise
else:
run_manager.on_chain_end(_trace_payload(input, self.trace_outputs))
run_manager.on_chain_end(input)
return input
async def ainvoke(
@@ -728,7 +705,7 @@ class RunnableSeq(Runnable):
# start the root run
run_manager = await callback_manager.on_chain_start(
None,
_trace_payload(input, self.trace_inputs),
self.trace_inputs(input) if self.trace_inputs is not None else input,
name=config.get("run_name") or self.get_name(),
run_id=config.pop("run_id", None),
)
@@ -765,7 +742,7 @@ class RunnableSeq(Runnable):
await run_manager.on_chain_error(e)
raise
else:
await run_manager.on_chain_end(_trace_payload(input, self.trace_outputs))
await run_manager.on_chain_end(input)
return input
def stream(
@@ -781,7 +758,7 @@ class RunnableSeq(Runnable):
# start the root run
run_manager = callback_manager.on_chain_start(
None,
_trace_payload(input, self.trace_inputs),
self.trace_inputs(input) if self.trace_inputs is not None else input,
name=config.get("run_name") or self.get_name(),
run_id=config.pop("run_id", None),
)
@@ -826,7 +803,7 @@ class RunnableSeq(Runnable):
run_manager.on_chain_error(e)
raise
else:
run_manager.on_chain_end(_trace_payload(output, self.trace_outputs))
run_manager.on_chain_end(output)
async def astream(
self,
@@ -841,7 +818,7 @@ class RunnableSeq(Runnable):
# start the root run
run_manager = await callback_manager.on_chain_start(
None,
_trace_payload(input, self.trace_inputs),
self.trace_inputs(input) if self.trace_inputs is not None else input,
name=config.get("run_name") or self.get_name(),
run_id=config.pop("run_id", None),
)
@@ -896,9 +873,7 @@ class RunnableSeq(Runnable):
await run_manager.on_chain_error(e)
raise
else:
await run_manager.on_chain_end(
_trace_payload(output, self.trace_outputs)
)
await run_manager.on_chain_end(output)
else:
try:
async with AsyncExitStack() as stack:
@@ -928,9 +903,7 @@ class RunnableSeq(Runnable):
await run_manager.on_chain_error(e)
raise
else:
await run_manager.on_chain_end(
_trace_payload(output, self.trace_outputs)
)
await run_manager.on_chain_end(output)
def _consume_iter(it: Iterator[Any]) -> Any:
+1 -1
View File
@@ -22,7 +22,7 @@ from pydantic import BaseModel
from typing_extensions import NotRequired, Required, is_typeddict
try:
from langgraph.checkpoint.serde._msgpack import (
from langgraph.checkpoint.serde._msgpack import ( # noqa: F401
STRICT_MSGPACK_ENABLED,
)
except ImportError:
+3 -17
View File
@@ -29,25 +29,11 @@ def _strip_extras(t): # type: ignore[no-untyped-def]
def _get_overwrite(value: Any) -> tuple[bool, Any]:
"""Inspects the given value and returns (is_overwrite, overwrite_value).
Recognises three forms:
* The typed `Overwrite` dataclass instance.
* The sentinel-keyed `{"__overwrite__": value}` dict form.
* The dataclass-erased `{"value": ..., "type": "__overwrite__"}` form that
results from JSON-serialising an `Overwrite` (e.g. an `orjson`-encoded
state update routed through the LangGraph API server). This keeps the
`Overwrite` semantics intact across JSON boundaries that strip dataclass
types.
"""
"""Inspects the given value and returns (is_overwrite, overwrite_value)."""
if isinstance(value, Overwrite):
return True, value.value
if isinstance(value, dict):
if len(value) == 1 and OVERWRITE in value:
return True, value[OVERWRITE]
if value.get("type") == OVERWRITE and "value" in value:
return True, value["value"]
if isinstance(value, dict) and len(value) == 1 and OVERWRITE in value:
return True, value[OVERWRITE]
return False, None
+3 -1
View File
@@ -172,11 +172,13 @@ class DeltaChannel(Generic[Value], BaseChannel[Any, Any, Any]):
overwrite_idx = i
if overwrite_idx is not None:
_, overwrite_value = _get_overwrite(values[overwrite_idx])
self.value = (
base = (
_copy.copy(overwrite_value)
if overwrite_value is not None
else self.typ()
)
remaining = [v for i, v in enumerate(values) if i != overwrite_idx]
self.value = self.reducer(base, remaining) if remaining else base
return True
base = self.typ() if self.value is MISSING else self.value
self.value = self.reducer(base, list(values))
+1 -1
View File
@@ -7,7 +7,7 @@ from typing import Any, Literal
from warnings import warn
# EmptyChannelError is re-exported from langgraph.channels.base
from langgraph.checkpoint.base import EmptyChannelError
from langgraph.checkpoint.base import EmptyChannelError # noqa: F401
from typing_extensions import deprecated
from langgraph.types import Command, Interrupt
+1 -9
View File
@@ -9,13 +9,7 @@ from langgraph.store.base import BaseStore
from langgraph._internal._typing import EMPTY_SEQ
from langgraph.runtime import Runtime
from langgraph.types import (
CachePolicy,
RetryPolicy,
StreamWriter,
TimeoutPolicy,
TracePolicy,
)
from langgraph.types import CachePolicy, RetryPolicy, StreamWriter, TimeoutPolicy
from langgraph.typing import ContextT, NodeInputT, NodeInputT_contra
@@ -99,5 +93,3 @@ class StateNodeSpec(Generic[NodeInputT, ContextT]):
ends: tuple[str, ...] | dict[str, str] | None = EMPTY_SEQ
defer: bool = False
timeout: TimeoutPolicy | None = None
trace_policy: TracePolicy | None = None
"""Optional policy controlling what this node records on its trace run."""
+1 -15
View File
@@ -85,7 +85,6 @@ from langgraph.types import (
RetryPolicy,
Send,
TimeoutPolicy,
TracePolicy,
ensure_valid_checkpointer,
)
from langgraph.typing import ContextT, InputT, NodeInputT, OutputT, StateT
@@ -385,7 +384,6 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
error_handler: StateNode[Any, ContextT] | None = None,
destinations: dict[str, str] | tuple[str, ...] | None = None,
timeout: float | timedelta | TimeoutPolicy | None = None,
trace_policy: TracePolicy | None = None,
**kwargs: Unpack[DeprecatedKwargs],
) -> Self:
"""Add a new node to the `StateGraph`, input schema is inferred as the state schema.
@@ -455,7 +453,6 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
error_handler: StateNode[Any, ContextT] | None = None,
destinations: dict[str, str] | tuple[str, ...] | None = None,
timeout: float | timedelta | TimeoutPolicy | None = None,
trace_policy: TracePolicy | None = None,
**kwargs: Unpack[DeprecatedKwargs],
) -> Self:
"""Add a new node to the `StateGraph` where input schema is specified.
@@ -530,7 +527,6 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
error_handler: StateNode[Any, ContextT] | None = None,
destinations: dict[str, str] | tuple[str, ...] | None = None,
timeout: float | timedelta | TimeoutPolicy | None = None,
trace_policy: TracePolicy | None = None,
**kwargs: Unpack[DeprecatedKwargs],
) -> Self:
"""Add a new node to the `StateGraph`, input schema is inferred as the state schema.
@@ -600,7 +596,6 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
error_handler: StateNode[Any, ContextT] | None = None,
destinations: dict[str, str] | tuple[str, ...] | None = None,
timeout: float | timedelta | TimeoutPolicy | None = None,
trace_policy: TracePolicy | None = None,
**kwargs: Unpack[DeprecatedKwargs],
) -> Self:
"""Add a new node to the `StateGraph`, input schema is specified.
@@ -677,7 +672,6 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
error_handler: StateNode[Any, ContextT] | None = None,
destinations: dict[str, str] | tuple[str, ...] | None = None,
timeout: float | timedelta | TimeoutPolicy | None = None,
trace_policy: TracePolicy | None = None,
**kwargs: Unpack[DeprecatedKwargs],
) -> Self:
"""Add a new node to the `StateGraph`.
@@ -697,10 +691,6 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
If a sequence is provided, the first matching policy will be applied.
cache_policy: The cache policy for the node.
error_handler: Optional node-level error handler callable for this node.
trace_policy: Optional policy controlling how this node's run is traced. Its
`process_inputs` callable transforms the node's input before it is
recorded (e.g. to omit or summarize large message history) without
changing the value passed to the node. Does not affect execution.
destinations: Destinations that indicate where a node can route to.
Useful for edgeless graphs with nodes that return `Command` objects.
@@ -890,7 +880,6 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
ends=ends,
defer=defer,
timeout=timeout,
trace_policy=trace_policy,
)
elif inferred_input_schema is not None:
self.nodes[node] = StateNodeSpec(
@@ -903,7 +892,6 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
ends=ends,
defer=defer,
timeout=timeout,
trace_policy=trace_policy,
)
else:
self.nodes[node] = StateNodeSpec[StateT, ContextT](
@@ -916,7 +904,6 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
ends=ends,
defer=defer,
timeout=timeout,
trace_policy=trace_policy,
)
input_schema = input_schema or inferred_input_schema
@@ -1008,7 +995,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
Without type hints on the `path` function's return value (e.g., `-> Literal["foo", "__end__"]:`)
or a path_map, the graph visualization assumes the edge could transition to any node in the graph.
"""
""" # noqa: E501
if self.compiled:
logger.warning(
"Adding an edge to a graph that has already been compiled. This will "
@@ -1543,7 +1530,6 @@ class CompiledStateGraph(
error_handler_node=node.error_handler_node,
bound=node.runnable, # type: ignore[arg-type]
timeout=node.timeout,
trace_policy=node.trace_policy,
)
else:
raise RuntimeError
+23 -68
View File
@@ -1,7 +1,7 @@
from __future__ import annotations
import uuid
from collections.abc import Callable, Iterable, Mapping
from collections.abc import Callable, Mapping
from datetime import datetime, timezone
from typing import Any, cast
@@ -14,7 +14,6 @@ from langgraph.checkpoint.base.id import uuid6
from langgraph.checkpoint.serde.types import _DeltaSnapshot
from langgraph._internal._config import DELTA_MAX_SUPERSTEPS_SINCE_SNAPSHOT
from langgraph._internal._constants import PUSH
from langgraph._internal._typing import MISSING
from langgraph.channels.base import BaseChannel
from langgraph.channels.delta import DeltaChannel
@@ -71,79 +70,40 @@ def delta_channels_to_snapshot(
return result
def get_updated_channels_from_tasks(
run_tasks: Iterable[Any],
) -> set[str]:
"""Channel names written by an update_state superstep (excluding PUSH)."""
return {c for task in run_tasks for c, _ in task.writes if c != PUSH}
def get_delta_channels_from_all_channels(
def advance_delta_counters(
channels: Mapping[str, BaseChannel],
) -> set[str]:
"""DeltaChannels to snapshot on the first update_state of a fresh thread."""
return {
k
for k, ch in channels.items()
if isinstance(ch, DeltaChannel) and ch.is_available()
}
def create_metadata_for_update_state_api(
channels: Mapping[str, BaseChannel],
updated_channels: set[str],
*,
prev_metadata: Mapping[str, Any] | None,
) -> dict[str, tuple[int, int]]:
"""Advance ``counters_since_delta_snapshot`` for update_state on a non-fresh thread.
updated_channels: set[str] | None,
) -> tuple[dict[str, tuple[int, int]], set[str]]:
"""Advance per-delta-channel `(updates, supersteps)` counters one step.
Mirrors the per-superstep counter bump in ``_loop._put_checkpoint``.
Mirrors the counter bookkeeping in `PregelLoop._put_checkpoint`: every
superstep bumps `supersteps` for all delta channels and `updates` for
those written this step, then channels that hit their snapshot cadence
are reset to `(0, 0)`.
Used by `update_state` (which creates a delta checkpoint outside the
main loop) so the written checkpoint carries a correct
`counters_since_delta_snapshot` without it, the supersteps-based
`get_delta_channel_history` walk can't locate the seed. Returns
`(new_counters, channels_to_snapshot)`; callers drop `(0, 0)` entries
before writing the metadata field.
"""
prev_counters = dict(
(prev_metadata or {}).get("counters_since_delta_snapshot") or {}
)
prev = dict((prev_metadata or {}).get("counters_since_delta_snapshot") or {})
updated = updated_channels or set()
new_counters: dict[str, tuple[int, int]] = {}
for ch_name, ch in channels.items():
for name, ch in channels.items():
if not isinstance(ch, DeltaChannel):
continue
u, s = prev_counters.get(ch_name, (0, 0))
u, s = prev.get(name, (0, 0))
s += 1
if ch_name in updated_channels:
if name in updated:
u += 1
new_counters[ch_name] = (u, s)
return new_counters
def create_checkpoint_plan_for_update_state_api(
channels: Mapping[str, BaseChannel],
updated_channels: set[str],
*,
step: int,
parents: dict[str, Any],
saved_metadata: Mapping[str, Any] | None,
is_fresh_thread: bool,
) -> tuple[set[str], dict[str, Any]]:
"""Return ``(channels_to_snapshot, metadata)`` for an update_state head."""
metadata: dict[str, Any] = {
"source": "update",
"step": step,
"parents": parents,
}
if is_fresh_thread:
return get_delta_channels_from_all_channels(channels), metadata
new_counters = create_metadata_for_update_state_api(
channels,
updated_channels,
prev_metadata=saved_metadata,
)
new_counters[name] = (u, s)
channels_to_snapshot = delta_channels_to_snapshot(channels, new_counters)
for k in channels_to_snapshot:
new_counters[k] = (0, 0)
non_zero = {k: v for k, v in new_counters.items() if v != (0, 0)}
if non_zero:
metadata["counters_since_delta_snapshot"] = non_zero
return channels_to_snapshot, metadata
return new_counters, channels_to_snapshot
def create_checkpoint(
@@ -178,11 +138,6 @@ def create_checkpoint(
continue
ch = channels[k]
if k in channels_to_snapshot:
# Callers force a full snapshot blob here: exit mode when a
# delta channel reaches its snapshot cadence, and update_state
# on a fresh thread (no ancestor to replay writes from). The
# manual version-bump below only applies to the exit-mode case.
#
# In exit mode, the snapshot decision is deferred to exit
# time (intermediate steps have do_checkpoint=False). The
# channel's count may have reached snapshot_frequency over
+1 -25
View File
@@ -70,7 +70,6 @@ from langgraph.callbacks import (
GraphResumeEvent,
)
from langgraph.channels.base import BaseChannel
from langgraph.channels.binop import _get_overwrite
from langgraph.channels.delta import DeltaChannel
from langgraph.channels.untracked_value import UntrackedValue
from langgraph.constants import TAG_HIDDEN
@@ -222,11 +221,6 @@ class PregelLoop:
# under the saver's `ORDER BY task_id, idx` sorting.
_exit_delta_writes: list[tuple[int, str, str, Any]] | None = None
# Delta channels that saw an Overwrite since the last checkpoint. These
# channels must snapshot after live update applies overwrite semantics so
# sparse replay starts from the same post-overwrite value.
_delta_channels_with_overwrite: set[str]
# The checkpoint_config that points at the parent loaded at `__enter__`
# (or the synthetic-empty checkpoint, on first run). We capture it
# eagerly because every `_put_checkpoint` advances `self.checkpoint_config`
@@ -683,11 +677,6 @@ class PregelLoop:
def after_tick(self) -> None:
# finish superstep
writes = [w for t in self.tasks.values() for w in t.writes]
self._delta_channels_with_overwrite.update(
ch
for ch, v in writes
if isinstance(self.specs.get(ch), DeltaChannel) and _get_overwrite(v)[0]
)
# all tasks have finished
self.updated_channels = apply_writes(
self.checkpoint,
@@ -991,11 +980,6 @@ class PregelLoop:
manager=None,
updated_channels=updated_channels,
)
self._delta_channels_with_overwrite.update(
c
for c, v in input_writes
if isinstance(self.specs.get(c), DeltaChannel) and _get_overwrite(v)[0]
)
# apply input writes
updated_channels = apply_writes(
self.checkpoint,
@@ -1136,7 +1120,6 @@ class PregelLoop:
# create new checkpoint
channels_to_snapshot = (
delta_channels_to_snapshot(self.channels, new_counters)
| self._delta_channels_with_overwrite
if do_checkpoint
else set()
)
@@ -1153,8 +1136,6 @@ class PregelLoop:
)
for k in channels_to_snapshot:
new_counters[k] = (0, 0)
if do_checkpoint:
self._delta_channels_with_overwrite.difference_update(channels_to_snapshot)
non_zero = {k: v for k, v in new_counters.items() if v != (0, 0)}
if non_zero:
self.checkpoint_metadata["counters_since_delta_snapshot"] = non_zero
@@ -1237,10 +1218,7 @@ class PregelLoop:
counters = dict(
self.checkpoint_metadata.get("counters_since_delta_snapshot") or {}
)
channels_to_snapshot = (
delta_channels_to_snapshot(self.channels, counters)
| self._delta_channels_with_overwrite
)
channels_to_snapshot = delta_channels_to_snapshot(self.channels, counters)
pending = [
(step, tid, ch, v)
@@ -1684,7 +1662,6 @@ class SyncPregelLoop(PregelLoop, AbstractContextManager):
)
self._delta_write_futs = []
self._error_handler_write_futs = []
self._delta_channels_with_overwrite = set()
self._exit_delta_writes = (
[] if self.durability == "exit" and self.checkpointer is not None else None
)
@@ -1942,7 +1919,6 @@ class AsyncPregelLoop(PregelLoop, AbstractAsyncContextManager):
)
self._delta_write_futs = []
self._error_handler_write_futs = []
self._delta_channels_with_overwrite = set()
self._exit_delta_writes = (
[] if self.durability == "exit" and self.checkpointer is not None else None
)
+3 -17
View File
@@ -16,7 +16,7 @@ from langgraph._internal._timeout import coerce_timeout_policy
from langgraph.pregel._utils import find_subgraph_pregel
from langgraph.pregel._write import ChannelWrite
from langgraph.pregel.protocol import PregelProtocol
from langgraph.types import CachePolicy, RetryPolicy, TimeoutPolicy, TracePolicy
from langgraph.types import CachePolicy, RetryPolicy, TimeoutPolicy
READ_TYPE = Callable[[str | Sequence[str], bool], Any | dict[str, Any]]
INPUT_CACHE_KEY_TYPE = tuple[Callable[..., Any], tuple[str, ...]]
@@ -138,9 +138,6 @@ class PregelNode:
metadata: Mapping[str, Any] | None
"""Metadata to attach to the node for tracing."""
trace_policy: TracePolicy | None
"""Optional policy controlling what this node records on its trace run."""
is_error_handler: bool
"""Whether this node is registered as an error handler node."""
@@ -159,7 +156,6 @@ class PregelNode:
writers: list[Runnable] | None = None,
tags: list[str] | None = None,
metadata: Mapping[str, Any] | None = None,
trace_policy: TracePolicy | None = None,
bound: Runnable[Any, Any] | None = None,
retry_policy: RetryPolicy | Sequence[RetryPolicy] | None = None,
cache_policy: CachePolicy | None = None,
@@ -181,7 +177,6 @@ class PregelNode:
self.timeout = coerce_timeout_policy(timeout)
self.tags = tags
self.metadata = metadata
self.trace_policy = trace_policy
self.is_error_handler = is_error_handler
self.error_handler_node = error_handler_node
if subgraphs is not None:
@@ -227,23 +222,14 @@ class PregelNode:
def node(self) -> Runnable[Any, Any] | None:
"""Get a runnable that combines `bound` and `writers`."""
writers = self.flat_writers
trace_inputs = self.trace_policy.process_inputs if self.trace_policy else None
trace_outputs = self.trace_policy.process_outputs if self.trace_policy else None
if self.bound is DEFAULT_BOUND and not writers:
return None
elif self.bound is DEFAULT_BOUND and len(writers) == 1:
return writers[0]
elif self.bound is DEFAULT_BOUND:
return RunnableSeq(
*writers, trace_inputs=trace_inputs, trace_outputs=trace_outputs
)
return RunnableSeq(*writers)
elif writers:
return RunnableSeq(
self.bound,
*writers,
trace_inputs=trace_inputs,
trace_outputs=trace_outputs,
)
return RunnableSeq(self.bound, *writers)
else:
return self.bound
+116 -50
View File
@@ -108,6 +108,7 @@ from langgraph.callbacks import (
get_sync_graph_callback_manager_for_config,
)
from langgraph.channels.base import BaseChannel
from langgraph.channels.delta import DeltaChannel
from langgraph.channels.topic import Topic
from langgraph.config import get_config
from langgraph.constants import END
@@ -129,12 +130,11 @@ from langgraph.pregel._algo import (
from langgraph.pregel._call import identifier
from langgraph.pregel._checkpoint import (
achannels_from_checkpoint,
advance_delta_counters,
channels_from_checkpoint,
copy_checkpoint,
create_checkpoint,
create_checkpoint_plan_for_update_state_api,
empty_checkpoint,
get_updated_channels_from_tasks,
)
from langgraph.pregel._draw import draw_graph
from langgraph.pregel._io import map_input, read_channels
@@ -1997,14 +1997,35 @@ class Pregel(
},
),
)
updated_channels = get_updated_channels_from_tasks(run_tasks)
if saved is not None:
for task_id, task in zip(run_task_ids, run_tasks):
channel_writes = [w for w in task.writes if w[0] != PUSH]
if channel_writes:
checkpointer.put_writes(
checkpoint_config, channel_writes, task_id
)
# save task writes
has_delta_writes = any(
isinstance(channels.get(c), DeltaChannel)
for task in run_tasks
for c, _ in task.writes
)
should_put_writes = saved is not None or has_delta_writes
if saved is None and has_delta_writes:
# If there is no previous checkpoint, we need to create a stub checkpoint
# so the first delta writes has a parent to anchor under.
# This is the model of DeltaChannel.
stub = empty_checkpoint()
checkpoint_config = checkpointer.put(
patch_configurable(
checkpoint_config, {CONFIG_KEY_CHECKPOINT_ID: None}
),
stub,
{"source": "update", "step": -1, "parents": {}},
{},
)
for task_id, task in zip(run_task_ids, run_tasks):
# channel writes are saved to current checkpoint
channel_writes = [w for w in task.writes if w[0] != PUSH]
if should_put_writes and channel_writes:
checkpointer.put_writes(checkpoint_config, channel_writes, task_id)
# apply to checkpoint and save
apply_writes(
checkpoint,
channels,
@@ -2012,35 +2033,46 @@ class Pregel(
checkpointer.get_next_version,
self.trigger_to_nodes,
)
channels_to_snapshot, checkpoint_metadata = (
create_checkpoint_plan_for_update_state_api(
channels,
updated_channels,
step=step + 1,
parents=saved.metadata.get("parents", {}) if saved else {},
saved_metadata=saved.metadata if saved else None,
is_fresh_thread=saved is None,
)
# Advance delta-channel snapshot counters for this manual super-
# step so the written checkpoint carries a correct
# `counters_since_delta_snapshot` (mirrors the main loop). Without
# it, the supersteps-based delta history walk can't find the seed.
updated_channel_names = {
c for task in run_tasks for c, _ in task.writes if c != PUSH
}
new_counters, channels_to_snapshot = advance_delta_counters(
channels,
saved.metadata if saved else None,
updated_channel_names,
)
# Keep `updated_channels` at its default (None) — matching the
# historical checkpoint shape here — so resume/trigger semantics
# (e.g. deferred nodes) are unaffected. `get_next_version` /
# `channels_to_snapshot` are no-ops unless a delta channel snapshots.
checkpoint = create_checkpoint(
checkpoint,
channels,
step + 1,
updated_channels=updated_channels if channels_to_snapshot else None,
get_next_version=checkpointer.get_next_version
if channels_to_snapshot
else None,
get_next_version=checkpointer.get_next_version,
channels_to_snapshot=channels_to_snapshot,
)
update_metadata: dict[str, Any] = {
"source": "update",
"step": step + 1,
"parents": saved.metadata.get("parents", {}) if saved else {},
}
if non_zero := {k: v for k, v in new_counters.items() if v != (0, 0)}:
update_metadata["counters_since_delta_snapshot"] = non_zero
next_config = checkpointer.put(
checkpoint_config,
checkpoint,
checkpoint_metadata,
update_metadata,
get_new_channel_versions(
checkpoint_previous_versions, checkpoint["channel_versions"]
),
)
for task_id, task in zip(run_task_ids, run_tasks):
# save push writes
if push_writes := [w for w in task.writes if w[0] == PUSH]:
checkpointer.put_writes(next_config, push_writes, task_id)
@@ -2457,14 +2489,36 @@ class Pregel(
},
),
)
updated_channels = get_updated_channels_from_tasks(run_tasks)
if saved is not None:
for task_id, task in zip(run_task_ids, run_tasks):
channel_writes = [w for w in task.writes if w[0] != PUSH]
if channel_writes:
await checkpointer.aput_writes(
checkpoint_config, channel_writes, task_id
)
# save task writes
has_delta_writes = any(
isinstance(channels.get(c), DeltaChannel)
for task in run_tasks
for c, _ in task.writes
)
should_put_writes = saved is not None or has_delta_writes
if saved is None and has_delta_writes:
# If there is no previous checkpoint, we need to create a stub checkpoint
# so the first delta writes has a parent to anchor under.
# This is the model of DeltaChannel.
stub = empty_checkpoint()
checkpoint_config = await checkpointer.aput(
patch_configurable(
checkpoint_config, {CONFIG_KEY_CHECKPOINT_ID: None}
),
stub,
{"source": "update", "step": -1, "parents": {}},
{},
)
for task_id, task in zip(run_task_ids, run_tasks):
# channel writes are saved to current checkpoint
channel_writes = [w for w in task.writes if w[0] != PUSH]
if should_put_writes and channel_writes:
await checkpointer.aput_writes(
checkpoint_config, channel_writes, task_id
)
# apply to checkpoint and save
apply_writes(
checkpoint,
channels,
@@ -2472,35 +2526,47 @@ class Pregel(
checkpointer.get_next_version,
self.trigger_to_nodes,
)
channels_to_snapshot, checkpoint_metadata = (
create_checkpoint_plan_for_update_state_api(
channels,
updated_channels,
step=step + 1,
parents=saved.metadata.get("parents", {}) if saved else {},
saved_metadata=saved.metadata if saved else None,
is_fresh_thread=saved is None,
)
# Advance delta-channel snapshot counters for this manual super-
# step so the written checkpoint carries a correct
# `counters_since_delta_snapshot` (mirrors the main loop). Without
# it, the supersteps-based delta history walk can't find the seed.
updated_channel_names = {
c for task in run_tasks for c, _ in task.writes if c != PUSH
}
new_counters, channels_to_snapshot = advance_delta_counters(
channels,
saved.metadata if saved else None,
updated_channel_names,
)
# Keep `updated_channels` at its default (None) — matching the
# historical checkpoint shape here — so resume/trigger semantics
# (e.g. deferred nodes) are unaffected. `get_next_version` /
# `channels_to_snapshot` are no-ops unless a delta channel snapshots.
checkpoint = create_checkpoint(
checkpoint,
channels,
step + 1,
updated_channels=updated_channels if channels_to_snapshot else None,
get_next_version=checkpointer.get_next_version
if channels_to_snapshot
else None,
get_next_version=checkpointer.get_next_version,
channels_to_snapshot=channels_to_snapshot,
)
update_metadata: dict[str, Any] = {
"source": "update",
"step": step + 1,
"parents": saved.metadata.get("parents", {}) if saved else {},
}
if non_zero := {k: v for k, v in new_counters.items() if v != (0, 0)}:
update_metadata["counters_since_delta_snapshot"] = non_zero
# save checkpoint, after applying writes
next_config = await checkpointer.aput(
checkpoint_config,
checkpoint,
checkpoint_metadata,
update_metadata,
get_new_channel_versions(
checkpoint_previous_versions, checkpoint["channel_versions"]
),
)
for task_id, task in zip(run_task_ids, run_tasks):
# save push writes
if push_writes := [w for w in task.writes if w[0] == PUSH]:
await checkpointer.aput_writes(next_config, push_writes, task_id)
return patch_checkpoint_map(next_config, saved.metadata if saved else None)
@@ -3512,7 +3578,7 @@ class Pregel(
control: RunControl | None = None,
transformers: Sequence[Callable[[tuple[str, ...]], Any]] | None = None,
**kwargs: Any,
) -> GraphRunStream:
) -> Any:
"""Internal v3 sync streaming implementation. Public entry: stream_events(version='v3').
Extra keyword arguments are forwarded to the underlying ``stream(...)``
@@ -3568,7 +3634,7 @@ class Pregel(
control: RunControl | None = None,
transformers: Sequence[Callable[[tuple[str, ...]], Any]] | None = None,
**kwargs: Any,
) -> AsyncGraphRunStream:
) -> Any:
"""Internal v3 async streaming implementation. Public entry: astream_events(version='v3').
Extra keyword arguments are forwarded to the underlying ``astream(...)``
@@ -3633,7 +3699,7 @@ class Pregel(
control: RunControl | None = None,
transformers: Sequence[Callable[[tuple[str, ...]], Any]] | None = None,
**kwargs: Any,
) -> GraphRunStream: ...
) -> Any: ...
def stream_events(
self,
@@ -3738,7 +3804,7 @@ class Pregel(
control: RunControl | None = None,
transformers: Sequence[Callable[[tuple[str, ...]], Any]] | None = None,
**kwargs: Any,
) -> Awaitable[AsyncGraphRunStream]: ...
) -> Awaitable[Any]: ...
def astream_events(
self,
+1 -27
View File
@@ -12,13 +12,7 @@ from langgraph.stream._mux import StreamMux
from langgraph.stream._types import ProtocolEvent
if TYPE_CHECKING:
from langchain_core.language_models.chat_model_stream import (
AsyncChatModelStream,
ChatModelStream,
)
from langgraph.stream.stream_channel import StreamChannel
from langgraph.stream.transformers import LifecyclePayload, SubgraphStatus
from langgraph.stream.transformers import SubgraphStatus
def _drive_until_done(pump: Callable[[], bool]) -> None:
@@ -54,16 +48,6 @@ class GraphRunStream:
experimental and may change.
"""
# Native projections always registered by `stream_events(version="v3")`.
# Attached dynamically by the `setattr` loop in `__init__`; declared here
# so type checkers see them. Opt-in native projections (`updates`,
# `custom`, `checkpoints`, `debug`, `tasks`) are only present when their
# transformer is registered, so they are reached via `extensions[...]`.
values: StreamChannel[dict[str, Any]]
messages: StreamChannel[ChatModelStream]
lifecycle: StreamChannel[LifecyclePayload]
subgraphs: StreamChannel[SubgraphRunStream]
def __init__(
self,
graph_iter: Iterator[Any] | None,
@@ -345,16 +329,6 @@ class AsyncGraphRunStream:
experimental and may change.
"""
# Native projections always registered by `astream_events(version="v3")`.
# Attached dynamically by the `setattr` loop in `__init__`; declared here
# so type checkers see them. Opt-in native projections (`updates`,
# `custom`, `checkpoints`, `debug`, `tasks`) are only present when their
# transformer is registered, so they are reached via `extensions[...]`.
values: StreamChannel[dict[str, Any]]
messages: StreamChannel[AsyncChatModelStream]
lifecycle: StreamChannel[LifecyclePayload]
subgraphs: StreamChannel[AsyncSubgraphRunStream]
def __init__(
self,
graph_aiter: AsyncIterator[Any] | None,
-46
View File
@@ -70,8 +70,6 @@ __all__ = (
"RetryPolicy",
"TimeoutPolicy",
"CachePolicy",
"TracePolicy",
"omit_payload",
"Interrupt",
"StateUpdate",
"PregelTask",
@@ -529,44 +527,6 @@ class CachePolicy(Generic[KeyFuncT]):
"""Time to live for the cache entry in seconds. If `None`, the entry never expires."""
@dataclass(**_DC_KWARGS)
class TracePolicy:
"""Configuration for how a node's run is traced.
Scope: this only transforms what the node's *own* run records. Child runs created
by a traced `bound` runnable and the root graph run are not affected. Plain
function nodes are traced with `trace=False`, so they have no such child runs.
Not intended to redact secrets. To redact inputs/outputs across all runs
(children included), use the LangSmith client's
`hide_inputs`/`hide_outputs`/`anonymizer` instead.
Each processor receives the node's raw input/output value (not a normalized
kwargs dict) and returns the value to record.
"""
process_inputs: Callable[[Any], Any] | None = None
"""Optional callable to transform the node's input before it is recorded on the
node's trace run. Can be used to omit or summarize large payloads
(e.g. message history). Not intended to affect the value passed to the node; avoid
mutating arguments in place."""
process_outputs: Callable[[Any], Any] | None = None
"""Optional callable to transform the node's output before it is recorded on the
node's trace run. Can be used to omit or summarize large payloads
(e.g. message history). Not intended to affect the value returned by the node; avoid
mutating arguments in place."""
def omit_payload(_value: Any) -> dict[str, Any]:
"""`TracePolicy` helper that records an empty payload, dropping the value entirely.
Use as `process_inputs` and/or `process_outputs` on a `TracePolicy` to keep a node's
span and its timing while omitting its inputs/outputs from the trace.
"""
return {}
_DEFAULT_INTERRUPT_ID = "placeholder-id"
@@ -1016,9 +976,3 @@ class Overwrite:
value: Any
"""The value to write directly to the channel, bypassing any reducer."""
type: Literal["__overwrite__"] = "__overwrite__"
"""Discriminator field. Lets the channel reducer recognise an `Overwrite`
even after its dataclass form is JSON-serialised and the typed instance
is lost (e.g. an `orjson`-encoded state update routed through the
LangGraph API server)."""
+2 -6
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "langgraph"
version = "1.2.11"
version = "1.2.6"
description = "Building stateful, multi-actor applications with LLMs"
authors = []
requires-python = ">=3.10"
@@ -89,12 +89,8 @@ langgraph-sdk = { path = "../sdk-py", editable = true }
langgraph-cli = { path = "../cli", editable = true }
[tool.ruff]
lint.select = [ "E", "F", "I", "PLC0415", "RUF100", "TID251", "UP" ]
lint.select = [ "E", "F", "I", "TID251", "UP" ]
lint.ignore = [ "E501" ]
# PLC0415 (import-outside-top-level) is enforced in tests only. Library code
# still has deferred imports that have not been reviewed, so it stays exempt
# for now.
lint.per-file-ignores = { "langgraph/**" = ["PLC0415"] }
line-length = 88
indent-width = 4
extend-include = ["*.ipynb"]
+2 -1
View File
@@ -1,6 +1,5 @@
import os
import tempfile
import time
from collections import defaultdict
from functools import partial
from typing import Any
@@ -74,6 +73,8 @@ class MemorySaverAssertImmutable(InMemorySaver):
new_versions: ChannelVersions,
) -> None:
if self.put_sleep:
import time
time.sleep(self.put_sleep)
# assert checkpoint hasn't been modified since last written
thread_id = config["configurable"]["thread_id"]
+1 -127
View File
@@ -2,16 +2,14 @@ import operator
from collections.abc import Sequence
from typing import Annotated
import orjson
import pytest
from langchain_core.messages import AIMessage, HumanMessage, RemoveMessage
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.serde.types import _DeltaSnapshot
from typing_extensions import NotRequired, TypedDict
from langgraph._internal._constants import OVERWRITE
from langgraph._internal._typing import MISSING
from langgraph.channels.binop import BinaryOperatorAggregate, _get_overwrite
from langgraph.channels.binop import BinaryOperatorAggregate
from langgraph.channels.delta import DeltaChannel
from langgraph.channels.last_value import LastValue
from langgraph.channels.topic import Topic
@@ -188,43 +186,6 @@ def test_delta_channel_overwrite() -> None:
assert ch.get()[0].content == "new"
def test_overwrite_dataclass_form_survives_json_roundtrip() -> None:
"""`Overwrite` serialised with `orjson` collapses to a plain dict but
must still be recognised as an overwrite by the channel reducer.
Without the `type` discriminator the dataclass-erased shape (`{"value":
...}`) is indistinguishable from a literal channel value, and downstream
reducers raise `MESSAGE_COERCION_FAILURE` (or similar) on read.
"""
ow = Overwrite(value=[HumanMessage(content="new", id="h2")])
erased = orjson.loads(orjson.dumps(ow, default=lambda o: o.model_dump()))
assert erased["type"] == OVERWRITE
is_overwrite, value = _get_overwrite(erased)
assert is_overwrite
assert isinstance(value, list)
assert value[0]["content"] == "new"
def test_overwrite_sentinel_dict_still_recognised() -> None:
"""The pre-existing `{"__overwrite__": value}` dict form continues to be
recognised. This is the canonical sentinel emitted by producers that do
not have an `Overwrite` dataclass available."""
is_overwrite, value = _get_overwrite({OVERWRITE: ["b"]})
assert is_overwrite
assert value == ["b"]
def test_overwrite_non_matching_dict_not_recognised() -> None:
"""Dicts that resemble the erased shape but do not carry the
`__overwrite__` discriminator must not be misclassified as overwrites."""
assert _get_overwrite({"value": ["b"]}) == (False, None)
assert _get_overwrite({"type": "human", "value": "hi"}) == (False, None)
def test_delta_channel_remove_message_and_replay() -> None:
"""RemoveMessage must round-trip correctly when writes are replayed."""
spec = DeltaChannel(_messages_delta_reducer, list)
@@ -436,93 +397,6 @@ def test_delta_channel_inmemory_saver_assembles_writes() -> None:
assert len(state.values["messages"]) == 4 # 2 human + 2 AI
def test_delta_channel_overwrite_superstep_snapshots() -> None:
def reducer(state: list[str], writes: Sequence[list[str]]) -> list[str]:
result = list(state)
for write in writes:
result.extend(write)
return result
class State(TypedDict):
items: Annotated[
list[str], DeltaChannel(reducer, list, snapshot_frequency=1000)
]
def node_a(state: State) -> dict:
return {"items": ["a"]}
def node_b(state: State) -> dict:
return {"items": Overwrite(["b"])}
def node_c(state: State) -> dict:
return {"items": ["c"]}
builder = StateGraph(State)
builder.add_node("node_a", node_a)
builder.add_node("node_b", node_b)
builder.add_node("node_c", node_c)
builder.add_edge(START, "node_a")
builder.add_edge("node_a", "node_b")
builder.add_edge("node_a", "node_c")
saver = InMemorySaver()
graph = builder.compile(checkpointer=saver)
config = {"configurable": {"thread_id": "overwrite-snapshot"}}
result = graph.invoke({"items": ["START"]}, config)
assert result == {"items": ["b"]}
saved = saver.get_tuple(config)
assert saved is not None
snapshot = saved.checkpoint["channel_values"].get("items")
assert isinstance(snapshot, _DeltaSnapshot)
assert snapshot.value == ["b"]
assert saved.metadata.get("counters_since_delta_snapshot", {}).get("items") is None
def test_delta_channel_replay_after_overwrite_snapshot() -> None:
def reducer(state: list[str], writes: Sequence[list[str]]) -> list[str]:
result = list(state)
for write in writes:
result.extend(write)
return result
class State(TypedDict):
items: Annotated[
list[str], DeltaChannel(reducer, list, snapshot_frequency=1000)
]
calls = 0
def node(state: State) -> dict:
nonlocal calls
calls += 1
if calls == 1:
return {"items": Overwrite(["reset"])}
return {"items": ["after"]}
builder = StateGraph(State)
builder.add_node("node", node)
builder.add_edge(START, "node")
saver = InMemorySaver()
graph = builder.compile(checkpointer=saver)
config = {"configurable": {"thread_id": "overwrite-replay"}}
assert graph.invoke({"items": ["before"]}, config) == {"items": ["reset"]}
first_saved = saver.get_tuple(config)
assert first_saved is not None
assert isinstance(
first_saved.checkpoint["channel_values"].get("items"), _DeltaSnapshot
)
assert graph.invoke({"items": []}, config) == {"items": ["reset", "after"]}
second_saved = saver.get_tuple(config)
assert second_saved is not None
assert "items" not in second_saved.checkpoint["channel_values"]
assert graph.get_state(config).values == {"items": ["reset", "after"]}
# ---------------------------------------------------------------------------
# DeltaChannel — dict reducer
# ---------------------------------------------------------------------------
+4 -4
View File
@@ -24,7 +24,7 @@ class _TrackingCallback(BaseCallbackHandler):
def __init__(self) -> None:
self.called = False
def on_chain_start(self, *args, **kwargs) -> None:
def on_chain_start(self, *args, **kwargs) -> None: # noqa: ANN002, ANN003
self.called = True
@@ -55,7 +55,7 @@ async def test_with_config_configurable_preserved_on_invoke() -> None:
builder = StateGraph(dict)
captured: dict = {}
def node(state, config):
def node(state, config): # noqa: ANN001
captured.update(config.get("configurable") or {})
return state
@@ -79,7 +79,7 @@ async def test_with_config_metadata_preserved_on_invoke() -> None:
builder = StateGraph(dict)
captured: dict = {}
def node(state, config):
def node(state, config): # noqa: ANN001
captured.update(config.get("metadata") or {})
return state
@@ -104,7 +104,7 @@ async def test_with_config_tags_preserved_on_invoke() -> None:
builder = StateGraph(dict)
captured: list = []
def node(state, config):
def node(state, config): # noqa: ANN001
captured.extend(config.get("tags") or [])
return state
@@ -220,7 +220,7 @@ def _checkpointers() -> list[tuple[str, Any]]:
result: list[tuple[str, Any]] = [("InMemory", None)]
if _POSTGRES_AVAILABLE:
try:
import psycopg # noqa: PLC0415
import psycopg
psycopg.connect(_POSTGRES_URI).close()
result.append(("Postgres", "postgres"))
@@ -616,117 +616,3 @@ async def test_add_messages_to_delta_migration_preserves_message_history_async()
assert [m.id for m in snap.values["messages"]] == ["h1", "a1"], (
f"async tip hydration mismatch: got {[m.id for m in snap.values['messages']]}"
)
# ---------------------------------------------------------------------------
# 8. First post-migration write, read back cold (regression for #8384)
#
# The migration boundary produces a checkpoint that carries BOTH a pre-delta
# plain-value blob AND the pending write that produced its (delta-era) child.
# That write is not subsumed by the blob — the blob is the value ENTERING that
# checkpoint. A saver whose ancestor walk skips the seed checkpoint's own
# writes silently drops the first post-migration write.
#
# The failure is invisible to the live `invoke` return value (computed
# in-memory before persistence), so these tests must assert on a COLD read.
# It is also invisible at `snapshot_frequency=1`, where every write is its own
# snapshot boundary and the walk never terminates on a plain value — hence the
# explicit default-frequency coverage.
# ---------------------------------------------------------------------------
def test_first_post_migration_write_survives_cold_read() -> None:
"""One non-snapshotting write after migrating a thread to `DeltaChannel`
must still be present when the state is read back from the checkpointer.
Regression for #8384: `invoke` returned the correct value while
`get_state` dropped the write permanently.
"""
checkpointer = InMemorySaver()
config = {"configurable": {"thread_id": "first-post-migration"}}
binop = _binop_graph(checkpointer)
binop.invoke({"items": ["a"]}, config)
delta = _delta_graph(checkpointer)
live = delta.invoke({"items": ["b"]}, config)
assert list(live["items"]) == ["a", "b"], "live invoke lost the write"
cold = delta.get_state(config)
assert list(cold.values["items"]) == ["a", "b"], (
"first post-migration write dropped on cold read: "
f"got {list(cold.values['items'])}"
)
async def test_first_post_migration_write_survives_cold_read_async() -> None:
"""Async variant of the #8384 regression."""
checkpointer = InMemorySaver()
config = {"configurable": {"thread_id": "first-post-migration-async"}}
binop = _binop_graph(checkpointer)
await binop.ainvoke({"items": ["a"]}, config)
delta = _delta_graph(checkpointer)
live = await delta.ainvoke({"items": ["b"]}, config)
assert list(live["items"]) == ["a", "b"], "live ainvoke lost the write"
cold = await delta.aget_state(config)
assert list(cold.values["items"]) == ["a", "b"], (
"first post-migration write dropped on cold read: "
f"got {list(cold.values['items'])}"
)
def test_post_migration_writes_match_base_saver_fallback() -> None:
"""Parity across the migration boundary WITH post-migration writes.
`test_base_saver_fallback_matches_optimized_override` only reads a
pre-migration chain, so the optimized override and the reference walk
never disagree there. Driving writes after the migration is what
separates them.
"""
def _run(saver: Any, thread: str) -> list[tuple[Any, list]]:
config = {"configurable": {"thread_id": thread}}
_drive(_binop_graph(saver), config, "u", 2)
delta = _delta_graph(saver)
_drive(delta, config, "d", 3)
return [
(s.next, list(s.values.get("items", [])))
for s in delta.get_state_history(config)
]
fast = _run(InMemorySaver(), "fast")
slow = _run(_ThirdPartyStyleSaver(), "slow")
assert fast == slow, (
"optimized override diverges from the base-saver fallback once "
f"post-migration writes exist; fast={fast}, slow={slow}"
)
# Guard the assertion above against both paths being wrong in the same way.
assert fast[0][1] == ["u0", "u1", "d0", "d1", "d2"], (
f"unexpected accumulated state: {fast[0][1]}"
)
def test_add_messages_migration_keeps_first_post_migration_message() -> None:
"""The `add_messages` -> `DeltaChannel` path is the one Deep Agents takes;
dropping the first post-migration write loses a real user message.
"""
checkpointer = InMemorySaver()
config = {"configurable": {"thread_id": "add-messages-first-write"}}
pre_graph = _add_messages_graph(checkpointer)
pre_graph.invoke({"messages": [HumanMessage(content="hello", id="h1")]}, config)
delta_graph = _delta_messages_graph(checkpointer)
delta_graph.invoke({"messages": [HumanMessage(content="second", id="h2")]}, config)
ids = [m.id for m in delta_graph.get_state(config).values["messages"]]
# h1 is the pre-migration seed, h2 the write that was being dropped; both
# have to survive, and in order.
assert ids == ["h1", "h2"], f"expected ['h1', 'h2'], got {ids}"
@@ -1,19 +1,20 @@
"""Tests for `update_state` / `aupdate_state` against `DeltaChannel`.
Regression suite for deepagents#3774 and Postgres read-path compatibility.
Fresh-thread ``update_state`` force-snapshots DeltaChannels (1.2.8). Non-fresh
``update_state`` persists ``checkpoint_writes`` on the parent, advances
``counters_since_delta_snapshot`` on the new head, and snapshots when a
channel reaches ``snapshot_frequency`` (mirroring normal run cadence).
Originally a regression suite for deepagents#3774 — `update_state` on a *fresh*
thread silently dropped the first write to a `DeltaChannel`-backed channel
because channel writes were only persisted when a previous checkpoint existed.
Fixed by lazily persisting an empty stub checkpoint on a fresh thread so the
first write has a parent to anchor under (mirrors the exit-mode lazy-stub
pattern in `_loop._put_exit_delta_writes`).
Coverage:
* fresh-thread regression: single ``update_state`` writes a message and reads back
* non-fresh thread: ``update_state`` after ``invoke``, after another ``update_state``,
and ``bulk_update_state`` with multiple per-superstep updates
* update-by-id end-to-end via ``update_state`` (DeltaChannel reducer semantics)
* fresh-thread head is snapshotted; non-fresh heads carry delta replay counters
* fresh-thread regression: single `update_state` writes a message and reads back
* non-fresh thread: `update_state` after `invoke`, after another `update_state`,
and `bulk_update_state` with multiple per-superstep updates
* update-by-id end-to-end via `update_state` (DeltaChannel reducer semantics)
* state-history chain shape on a fresh thread (lazy stub + update checkpoint
with correct parent linking)
"""
from typing import Annotated, Any
@@ -21,31 +22,22 @@ from typing import Annotated, Any
import pytest
from langchain_core.messages import HumanMessage
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.serde.types import _DeltaSnapshot
from typing_extensions import TypedDict
from langgraph.channels.delta import DeltaChannel
from langgraph.graph import START, StateGraph
from langgraph.graph.message import _messages_delta_reducer
from langgraph.types import StateUpdate
pytestmark = pytest.mark.anyio
def _build_graph(
checkpointer: InMemorySaver,
*,
two_nodes: bool = False,
snapshot_frequency: int = 1000,
) -> Any:
def _build_graph(checkpointer: InMemorySaver, *, two_nodes: bool = False) -> Any:
"""Compile a minimal DeltaChannel-backed `messages` graph.
`two_nodes=True` adds a second writer node so `bulk_update_state` can route
distinct updates to different `as_node` values within a single superstep.
"""
channel = DeltaChannel(
_messages_delta_reducer, snapshot_frequency=snapshot_frequency
)
channel = DeltaChannel(_messages_delta_reducer)
State = TypedDict("State", {"messages": Annotated[list, channel]}) # type: ignore[call-overload] # noqa: UP013
def model(state: dict) -> dict:
@@ -95,30 +87,14 @@ async def test_aupdate_state_fresh_thread_delta_channel() -> None:
assert [m.content for m in state.values["messages"]] == ["hello"]
def test_fresh_update_state_head_snapshots_delta_channel() -> None:
saver = InMemorySaver()
graph = _build_graph(saver)
config = {"configurable": {"thread_id": "fresh-head-snapshot"}}
graph.update_state(
config,
{"messages": [HumanMessage(content="hello", id="m1")]},
as_node="model",
)
head = saver.get_tuple(config)
assert head is not None
assert isinstance(head.checkpoint["channel_values"].get("messages"), _DeltaSnapshot)
assert head.metadata is not None
assert "counters_since_delta_snapshot" not in head.metadata
# ---------------------------------------------------------------------------
# Non-fresh thread: update_state after invoke
# ---------------------------------------------------------------------------
def test_update_state_after_invoke_delta_channel() -> None:
"""The non-fresh-thread path was already working before the fix; pin it
down so the lazy-stub change for fresh threads doesn't regress it."""
saver = InMemorySaver()
graph = _build_graph(saver)
config = {"configurable": {"thread_id": "after-invoke-sync"}}
@@ -134,12 +110,6 @@ def test_update_state_after_invoke_delta_channel() -> None:
assert [m.content for m in state.values["messages"]] == ["seed", "appended"]
assert [m.id for m in state.values["messages"]] == ["m1", "m2"]
head = saver.get_tuple(config)
assert head is not None
assert "messages" not in head.checkpoint["channel_values"]
assert head.metadata is not None
assert head.metadata["counters_since_delta_snapshot"]["messages"] == [2, 4]
async def test_aupdate_state_after_invoke_delta_channel() -> None:
saver = InMemorySaver()
@@ -163,6 +133,9 @@ async def test_aupdate_state_after_invoke_delta_channel() -> None:
def test_consecutive_update_states_delta_channel() -> None:
"""First update_state lazily persists a stub; the second sees a real
parent (`saved is not None`) and takes the original write path. Both
messages must round-trip in chronological order."""
saver = InMemorySaver()
graph = _build_graph(saver)
config = {"configurable": {"thread_id": "consecutive-sync"}}
@@ -182,39 +155,6 @@ def test_consecutive_update_states_delta_channel() -> None:
assert [m.content for m in state.values["messages"]] == ["first", "second"]
assert [m.id for m in state.values["messages"]] == ["m1", "m2"]
head = saver.get_tuple(config)
assert head is not None
assert "messages" not in head.checkpoint["channel_values"]
assert head.metadata is not None
assert head.metadata["counters_since_delta_snapshot"]["messages"] == [1, 1]
def test_update_state_snapshots_at_frequency() -> None:
"""Non-fresh update_state snapshots when counters reach snapshot_frequency."""
saver = InMemorySaver()
graph = _build_graph(saver, snapshot_frequency=1)
config = {"configurable": {"thread_id": "snapshot-at-freq"}}
graph.update_state(
config,
{"messages": [HumanMessage(content="first", id="m1")]},
as_node="model",
)
graph.update_state(
config,
{"messages": [HumanMessage(content="second", id="m2")]},
as_node="model",
)
state = graph.get_state(config)
assert [m.content for m in state.values["messages"]] == ["first", "second"]
head = saver.get_tuple(config)
assert head is not None
assert isinstance(head.checkpoint["channel_values"].get("messages"), _DeltaSnapshot)
assert head.metadata is not None
assert "counters_since_delta_snapshot" not in head.metadata
async def test_aconsecutive_update_states_delta_channel() -> None:
saver = InMemorySaver()
@@ -278,6 +218,7 @@ def test_bulk_update_state_multi_task_per_superstep_delta_channel() -> None:
different `StateUpdate`s targeting the same node otherwise both share
the deterministic interrupt-derived id and collide in the saver.
"""
from langgraph.types import StateUpdate
saver = InMemorySaver()
graph = _build_graph(saver)
@@ -311,14 +252,14 @@ def test_bulk_update_state_multi_task_per_superstep_delta_channel() -> None:
# ---------------------------------------------------------------------------
# Public-API observation of fresh-thread checkpoint shape
# Public-API observation of the lazy-stub mechanism
# ---------------------------------------------------------------------------
def test_state_history_chain_after_fresh_update_state_delta_channel() -> None:
"""A fresh-thread `update_state` should produce a single self-contained
checkpoint visible via `get_state_history`: step=0, `source='update'`,
no parent, with the DeltaChannel value snapshotted inline."""
"""A fresh-thread `update_state` should produce two checkpoints visible
via `get_state_history`: a stub (step=-1, no parent) and the update
(step=0, parent=stub). Both attributed `source='update'`."""
saver = InMemorySaver()
graph = _build_graph(saver)
config = {"configurable": {"thread_id": "history-chain"}}
@@ -329,12 +270,25 @@ def test_state_history_chain_after_fresh_update_state_delta_channel() -> None:
as_node="model",
)
# Newest first per `get_state_history` ordering.
history = list(graph.get_state_history(config))
assert len(history) == 1
assert len(history) == 2
update_snapshot, stub_snapshot = history
(update_snapshot,) = history
assert update_snapshot.metadata is not None
assert update_snapshot.metadata["source"] == "update"
assert update_snapshot.metadata["step"] == 0
assert update_snapshot.parent_config is None
assert [m.content for m in update_snapshot.values["messages"]] == ["hello"]
assert stub_snapshot.metadata is not None
assert stub_snapshot.metadata["source"] == "update"
assert stub_snapshot.metadata["step"] == -1
assert stub_snapshot.parent_config is None
# The update checkpoint's parent is the stub.
assert update_snapshot.parent_config is not None
assert (
update_snapshot.parent_config["configurable"]["checkpoint_id"]
== stub_snapshot.config["configurable"]["checkpoint_id"]
)
+4 -4
View File
@@ -88,13 +88,13 @@ def test_constants_deprecation() -> None:
LangGraphDeprecatedSinceV10,
match="Importing Send from langgraph.constants is deprecated. Please use 'from langgraph.types import Send' instead.",
):
from langgraph.constants import Send # noqa: PLC0415, F401
from langgraph.constants import Send # noqa: F401
with pytest.warns(
LangGraphDeprecatedSinceV10,
match="Importing Interrupt from langgraph.constants is deprecated. Please use 'from langgraph.types import Interrupt' instead.",
):
from langgraph.constants import Interrupt # noqa: PLC0415, F401
from langgraph.constants import Interrupt # noqa: F401
def test_pregel_types_deprecation() -> None:
@@ -102,7 +102,7 @@ def test_pregel_types_deprecation() -> None:
LangGraphDeprecatedSinceV10,
match="Importing from langgraph.pregel.types is deprecated. Please use 'from langgraph.types import ...' instead.",
):
from langgraph.pregel.types import StateSnapshot # noqa: PLC0415, F401
from langgraph.pregel.types import StateSnapshot # noqa: F401
def test_config_schema_deprecation() -> None:
@@ -195,7 +195,7 @@ def test_deprecated_import() -> None:
LangGraphDeprecatedSinceV10,
match="Importing PREVIOUS from langgraph.constants is deprecated. This constant is now private and should not be used directly.",
):
from langgraph.constants import PREVIOUS # noqa: PLC0415, F401
from langgraph.constants import PREVIOUS # noqa: F401
@pytest.mark.filterwarnings(
+1 -1
View File
@@ -13,7 +13,6 @@ from langgraph.callbacks import (
GraphCallbackHandler,
GraphInterruptEvent,
GraphResumeEvent,
_GraphCallbackManager,
)
from langgraph.graph import START, StateGraph
from langgraph.types import Command, Interrupt, interrupt
@@ -287,6 +286,7 @@ def test_non_graph_handler_via_add_handler_does_not_crash() -> None:
GraphCallbackHandler. They must be silently accepted graph lifecycle
events will simply not be dispatched to them.
"""
from langgraph.callbacks import _GraphCallbackManager
manager = _GraphCallbackManager()
plain_handler = _LangChainCustomEventHandler()
+45 -16
View File
@@ -2,26 +2,11 @@ import json
import operator
import re
import time
from copy import deepcopy
from dataclasses import replace
from typing import Annotated, Any, Literal, cast
import pytest
from langchain_core.callbacks import CallbackManagerForLLMRun
from langchain_core.language_models.fake import FakeStreamingListLLM
from langchain_core.language_models.fake_chat_models import (
FakeMessagesListChatModel,
)
from langchain_core.messages import (
AIMessage,
AnyMessage,
BaseMessage,
HumanMessage,
ToolCall,
ToolMessage,
)
from langchain_core.outputs import ChatGeneration, ChatResult
from langchain_core.prompts import PromptTemplate
from langchain_core.messages import AIMessage, AnyMessage, ToolCall
from langchain_core.runnables import RunnableConfig, RunnableMap, RunnablePick
from langchain_core.tools import tool
from langchain_core.version import VERSION as LANGCHAIN_CORE_VERSION
@@ -499,6 +484,9 @@ def test_conditional_state_graph(
snapshot: SnapshotAssertion,
sync_checkpointer: BaseCheckpointSaver,
) -> None:
from langchain_core.language_models.fake import FakeStreamingListLLM
from langchain_core.prompts import PromptTemplate
from langchain_core.tools import tool
class AgentState(TypedDict, total=False):
input: Annotated[str, UntrackedValue]
@@ -1273,6 +1261,8 @@ def test_conditional_state_graph(
def test_prebuilt_tool_chat(snapshot: SnapshotAssertion) -> None:
from langchain_core.messages import AIMessage, HumanMessage
from langchain_core.tools import tool
@tool()
def search_api(query: str) -> str:
@@ -1636,6 +1626,17 @@ def test_prebuilt_tool_chat(snapshot: SnapshotAssertion) -> None:
def test_state_graph_packets(
sync_checkpointer: BaseCheckpointSaver, mocker: MockerFixture
) -> None:
from langchain_core.language_models.fake_chat_models import (
FakeMessagesListChatModel,
)
from langchain_core.messages import (
AIMessage,
BaseMessage,
HumanMessage,
ToolCall,
ToolMessage,
)
from langchain_core.tools import tool
class AgentState(TypedDict):
messages: Annotated[list[BaseMessage], add_messages]
@@ -2380,6 +2381,15 @@ def test_message_graph(
deterministic_uuids: MockerFixture,
sync_checkpointer: BaseCheckpointSaver,
) -> None:
from copy import deepcopy
from langchain_core.callbacks import CallbackManagerForLLMRun
from langchain_core.language_models.fake_chat_models import (
FakeMessagesListChatModel,
)
from langchain_core.messages import AIMessage, BaseMessage, HumanMessage
from langchain_core.outputs import ChatGeneration, ChatResult
from langchain_core.tools import tool
class FakeFunctionChatModel(FakeMessagesListChatModel):
def bind_functions(self, functions: list):
@@ -3089,6 +3099,20 @@ def test_root_graph(
deterministic_uuids: MockerFixture,
sync_checkpointer: BaseCheckpointSaver,
) -> None:
from copy import deepcopy
from langchain_core.callbacks import CallbackManagerForLLMRun
from langchain_core.language_models.fake_chat_models import (
FakeMessagesListChatModel,
)
from langchain_core.messages import (
AIMessage,
BaseMessage,
HumanMessage,
ToolMessage,
)
from langchain_core.outputs import ChatGeneration, ChatResult
from langchain_core.tools import tool
class FakeFunctionChatModel(FakeMessagesListChatModel):
def bind_functions(self, functions: list):
@@ -5813,6 +5837,7 @@ def test_send_to_nested_graphs(sync_checkpointer: BaseCheckpointSaver) -> None:
def test_send_react_interrupt(
sync_checkpointer: BaseCheckpointSaver,
) -> None:
from langchain_core.messages import AIMessage, HumanMessage, ToolCall, ToolMessage
ai_message = AIMessage(
"",
@@ -6203,6 +6228,7 @@ def test_send_react_interrupt(
def test_send_react_interrupt_control(
sync_checkpointer: BaseCheckpointSaver, snapshot: SnapshotAssertion
) -> None:
from langchain_core.messages import AIMessage, HumanMessage, ToolCall, ToolMessage
ai_message = AIMessage(
"",
@@ -6429,6 +6455,9 @@ def test_send_react_interrupt_control(
def test_weather_subgraph(
sync_checkpointer: BaseCheckpointSaver, snapshot: SnapshotAssertion
) -> None:
from langchain_core.language_models.fake_chat_models import (
FakeMessagesListChatModel,
)
# setup subgraph
+29 -16
View File
@@ -9,22 +9,8 @@ from typing import (
)
import pytest
from langchain_core.agents import AgentAction, AgentFinish
from langchain_core.language_models.fake import FakeStreamingListLLM
from langchain_core.language_models.fake_chat_models import (
FakeMessagesListChatModel,
)
from langchain_core.messages import (
AIMessage,
AnyMessage,
BaseMessage,
HumanMessage,
ToolCall,
ToolMessage,
)
from langchain_core.prompts import PromptTemplate
from langchain_core.messages import AnyMessage, ToolCall
from langchain_core.runnables import RunnableConfig, RunnablePick
from langchain_core.tools import tool
from langchain_core.version import VERSION as LANGCHAIN_CORE_VERSION
from langgraph.checkpoint.base import BaseCheckpointSaver
from langgraph.prebuilt.chat_agent_executor import create_react_agent
@@ -36,7 +22,6 @@ from langgraph._internal._constants import PULL, PUSH
from langgraph.channels.last_value import LastValue
from langgraph.channels.untracked_value import UntrackedValue
from langgraph.constants import END, START
from langgraph.graph import MessagesState
from langgraph.graph.message import add_messages
from langgraph.graph.state import StateGraph
from langgraph.pregel import NodeBuilder, Pregel
@@ -494,6 +479,10 @@ async def test_fork_always_re_runs_nodes(
async def test_conditional_graph_state(async_checkpointer: BaseCheckpointSaver) -> None:
from langchain_core.agents import AgentAction, AgentFinish
from langchain_core.language_models.fake import FakeStreamingListLLM
from langchain_core.prompts import PromptTemplate
from langchain_core.tools import tool
class AgentState(TypedDict):
input: Annotated[str, UntrackedValue]
@@ -1028,6 +1017,8 @@ async def test_conditional_graph_state(async_checkpointer: BaseCheckpointSaver)
async def test_prebuilt_tool_chat() -> None:
from langchain_core.messages import AIMessage, HumanMessage
from langchain_core.tools import tool
model = FakeChatModel(
messages=[
@@ -1367,6 +1358,16 @@ async def test_prebuilt_tool_chat() -> None:
async def test_state_graph_packets(async_checkpointer: BaseCheckpointSaver) -> None:
from langchain_core.language_models.fake_chat_models import (
FakeMessagesListChatModel,
)
from langchain_core.messages import (
AIMessage,
BaseMessage,
HumanMessage,
ToolMessage,
)
from langchain_core.tools import tool
class AgentState(TypedDict):
messages: Annotated[list[BaseMessage], add_messages]
@@ -2071,6 +2072,11 @@ async def test_state_graph_packets(async_checkpointer: BaseCheckpointSaver) -> N
async def test_message_graph(async_checkpointer: BaseCheckpointSaver) -> None:
from langchain_core.language_models.fake_chat_models import (
FakeMessagesListChatModel,
)
from langchain_core.messages import AIMessage, HumanMessage
from langchain_core.tools import tool
class FakeFunctionChatModel(FakeMessagesListChatModel):
def bind_functions(self, functions: list):
@@ -3531,6 +3537,13 @@ async def test_send_to_nested_graphs(async_checkpointer: BaseCheckpointSaver) ->
async def test_weather_subgraph(
async_checkpointer: BaseCheckpointSaver,
) -> None:
from langchain_core.language_models.fake_chat_models import (
FakeMessagesListChatModel,
)
from langchain_core.messages import AIMessage, ToolCall
from langchain_core.tools import tool
from langgraph.graph import MessagesState
# setup subgraph
+27 -18
View File
@@ -4,39 +4,25 @@ import gc
import json
import logging
import operator
import random
import threading
import time
import uuid
from collections import Counter, defaultdict, deque
from collections import Counter, deque
from collections.abc import Sequence
from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass, field
from random import randrange
from typing import Annotated, Any, Literal, get_type_hints
from unittest.mock import patch
import pytest
from langchain_core.language_models import GenericFakeChatModel
from langchain_core.language_models.fake import FakeStreamingListLLM
from langchain_core.language_models.fake_chat_models import (
FakeMessagesListChatModel,
)
from langchain_core.messages import (
AIMessage,
AnyMessage,
BaseMessage,
HumanMessage,
RemoveMessage,
)
from langchain_core.prompts import ChatPromptTemplate, PromptTemplate
from langchain_core.messages import AIMessage, AnyMessage, HumanMessage, RemoveMessage
from langchain_core.runnables import (
RunnableConfig,
RunnableLambda,
RunnablePassthrough,
)
from langchain_core.runnables.graph import Edge
from langchain_core.tools import tool
from langchain_core.version import VERSION as LANGCHAIN_CORE_VERSION
from langgraph.cache.base import BaseCache
from langgraph.checkpoint.base import (
@@ -70,9 +56,8 @@ from langgraph.pregel import (
NodeBuilder,
Pregel,
)
from langgraph.pregel._loop import PregelLoop, SyncPregelLoop
from langgraph.pregel._loop import SyncPregelLoop
from langgraph.pregel._runner import PregelRunner
from langgraph.runtime import RunControl
from langgraph.types import (
CachePolicy,
Command,
@@ -140,6 +125,7 @@ def test_graph_validation() -> None:
def test_request_drain_allows_inflight_call_scheduling(
sync_checkpointer: BaseCheckpointSaver,
) -> None:
from langgraph.runtime import RunControl
@task
def child(x: int) -> int:
@@ -1783,6 +1769,9 @@ def test_conditional_state_graph_with_list_edge_inputs(snapshot: SnapshotAsserti
def test_state_graph_w_config_inherited_state_keys(snapshot: SnapshotAssertion) -> None:
from langchain_core.language_models.fake import FakeStreamingListLLM
from langchain_core.prompts import PromptTemplate
from langchain_core.tools import tool
class BaseState(TypedDict):
input: str
@@ -3780,6 +3769,12 @@ def test_checkpoint_metadata(sync_checkpointer: BaseCheckpointSaver) -> None:
previous checkpoint config for each step in the run.
"""
# set up test
from langchain_core.language_models.fake_chat_models import (
FakeMessagesListChatModel,
)
from langchain_core.messages import AIMessage, AnyMessage
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.tools import tool
# graph state
class BaseState(TypedDict):
@@ -3945,6 +3940,7 @@ def test_checkpoint_metadata(sync_checkpointer: BaseCheckpointSaver) -> None:
def test_remove_message_via_state_update(
sync_checkpointer: BaseCheckpointSaver,
) -> None:
from langchain_core.messages import AIMessage, HumanMessage, RemoveMessage
workflow = StateGraph(state_schema=Annotated[list[AnyMessage], add_messages]) # type: ignore[arg-type]
workflow.add_node(
@@ -3977,6 +3973,7 @@ def test_remove_message_via_state_update(
def test_remove_message_from_node():
from langchain_core.messages import AIMessage, HumanMessage, RemoveMessage
workflow = StateGraph(state_schema=Annotated[list[AnyMessage], add_messages]) # type: ignore[arg-type]
workflow.add_node(
@@ -4002,6 +3999,7 @@ def test_remove_message_from_node():
def test_xray_lance(snapshot: SnapshotAssertion):
from langchain_core.messages import AnyMessage, HumanMessage
class Analyst(BaseModel):
affiliation: str = Field(
@@ -4485,6 +4483,7 @@ def test_debug_subgraphs(
def test_debug_nested_subgraphs(
sync_checkpointer: BaseCheckpointSaver, durability: Durability
):
from collections import defaultdict
class State(TypedDict):
messages: Annotated[list[str], operator.add]
@@ -4744,6 +4743,8 @@ def test_runnable_passthrough_node_graph() -> None:
def test_parent_command(
sync_checkpointer: BaseCheckpointSaver, subgraph_persist: bool
) -> None:
from langchain_core.messages import BaseMessage
from langchain_core.tools import tool
@tool(return_direct=True)
def get_user_name() -> Command:
@@ -5163,6 +5164,7 @@ def test_command_with_static_breakpoints(
def test_multistep_plan(sync_checkpointer: BaseCheckpointSaver):
from langchain_core.messages import AnyMessage
class State(TypedDict, total=False):
plan: list[str | list[str]]
@@ -5908,6 +5910,9 @@ def test_no_redundant_put_writes_for_cached_task(
sync_checkpointer: BaseCheckpointSaver,
) -> None:
"""Cached @tasks on resume must not trigger redundant put_writes."""
from unittest.mock import patch
from langgraph.pregel._loop import PregelLoop
@task
def setup(x: int) -> int:
@@ -6970,6 +6975,7 @@ def test_configurable_propagates_to_stream_metadata() -> None:
def test_stream_mode_messages_command() -> None:
from langchain_core.messages import HumanMessage
def my_node(state):
return {"messages": HumanMessage(content="foo")}
@@ -7237,6 +7243,7 @@ def test_get_stream_writer() -> None:
def test_stream_messages_dedupe_inputs() -> None:
from langchain_core.messages import AIMessage
def call_model(state):
return {"messages": AIMessage("hi", id="1")}
@@ -7274,6 +7281,7 @@ def test_stream_messages_dedupe_inputs() -> None:
def test_stream_messages_dedupe_state(sync_checkpointer: BaseCheckpointSaver) -> None:
from langchain_core.messages import AIMessage
to_emit = [AIMessage("bye", id="1"), AIMessage("bye again", id="2")]
@@ -8245,6 +8253,7 @@ def test_get_graph_loop(snapshot: SnapshotAssertion) -> None:
def test_get_graph_self_loop(snapshot: SnapshotAssertion) -> None:
import random
subgraph_builder = StateGraph(MessagesState)
subgraph_builder.add_node("agent", lambda x: x)
+29 -18
View File
@@ -7,7 +7,7 @@ import operator
import random
import sys
import uuid
from collections import Counter, defaultdict, deque
from collections import Counter, deque
from dataclasses import replace
from time import perf_counter
from typing import (
@@ -21,20 +21,8 @@ from uuid import UUID
import pytest
from langchain_core.language_models import GenericFakeChatModel
from langchain_core.language_models.fake_chat_models import (
FakeMessagesListChatModel,
)
from langchain_core.messages import (
AIMessage,
AnyMessage,
BaseMessage,
HumanMessage,
ToolCall,
ToolMessage,
)
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.messages import HumanMessage
from langchain_core.runnables import RunnableConfig, RunnableLambda, RunnablePassthrough
from langchain_core.tools import tool
from langchain_core.utils.aiter import aclosing
from langchain_core.version import VERSION as LANGCHAIN_CORE_VERSION
from langgraph.cache.base import BaseCache
@@ -57,7 +45,6 @@ from typing_extensions import NotRequired, TypedDict
from langgraph._internal._constants import CONFIG_KEY_NODE_FINISHED, ERROR, PULL
from langgraph._internal._queue import AsyncQueue
from langgraph.channels.binop import BinaryOperatorAggregate
from langgraph.channels.delta import DeltaChannel
from langgraph.channels.last_value import LastValue
from langgraph.channels.topic import Topic
from langgraph.errors import (
@@ -68,11 +55,10 @@ from langgraph.errors import (
)
from langgraph.func import entrypoint, task
from langgraph.graph import END, START, StateGraph
from langgraph.graph.message import MessagesState, _messages_delta_reducer, add_messages
from langgraph.graph.message import MessagesState, add_messages
from langgraph.pregel import NodeBuilder, Pregel
from langgraph.pregel._loop import AsyncPregelLoop, PregelLoop
from langgraph.pregel._loop import AsyncPregelLoop
from langgraph.pregel._runner import PregelRunner
from langgraph.runtime import RunControl
from langgraph.types import (
CachePolicy,
Command,
@@ -236,6 +222,7 @@ async def test_checkpoint_errors() -> None:
async def test_request_drain_allows_inflight_acall_scheduling(
async_checkpointer: BaseCheckpointSaver,
) -> None:
from langgraph.runtime import RunControl
@task
async def child(x: int) -> int:
@@ -2881,6 +2868,7 @@ async def test_send_dedupe_on_resume(
async def test_send_react_interrupt(async_checkpointer: BaseCheckpointSaver) -> None:
from langchain_core.messages import AIMessage, HumanMessage, ToolCall, ToolMessage
ai_message = AIMessage(
"",
@@ -3271,6 +3259,7 @@ async def test_send_react_interrupt(async_checkpointer: BaseCheckpointSaver) ->
async def test_send_react_interrupt_control(
async_checkpointer: BaseCheckpointSaver, snapshot: SnapshotAssertion
) -> None:
from langchain_core.messages import AIMessage, HumanMessage, ToolCall, ToolMessage
ai_message = AIMessage(
"",
@@ -5549,6 +5538,12 @@ async def test_checkpoint_metadata(async_checkpointer: BaseCheckpointSaver) -> N
previous checkpoint config for each step in the run.
"""
# set up test
from langchain_core.language_models.fake_chat_models import (
FakeMessagesListChatModel,
)
from langchain_core.messages import AIMessage, AnyMessage
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.tools import tool
# graph state
class BaseState(TypedDict):
@@ -5949,6 +5944,7 @@ async def test_debug_subgraphs(
async def test_debug_nested_subgraphs(
async_checkpointer: BaseCheckpointSaver, durability: Durability
) -> None:
from collections import defaultdict
class State(TypedDict):
messages: Annotated[list[str], operator.add]
@@ -6065,6 +6061,8 @@ async def test_debug_nested_subgraphs(
async def test_parent_command(
async_checkpointer: BaseCheckpointSaver, subgraph_persist: bool
) -> None:
from langchain_core.messages import BaseMessage
from langchain_core.tools import tool
@tool(return_direct=True)
def get_user_name() -> Command:
@@ -6132,6 +6130,10 @@ async def test_parent_command(
async def test_delta_channel_durability_exit_stores_snapshot_async() -> None:
"""DeltaChannel must reload from an async durability='exit' checkpoint."""
from langchain_core.messages import AIMessage
from langgraph.channels.delta import DeltaChannel
from langgraph.graph.message import _messages_delta_reducer
class State(TypedDict):
messages: Annotated[list, DeltaChannel(_messages_delta_reducer)]
@@ -6418,6 +6420,7 @@ async def test_command_with_static_breakpoints(
async def test_multistep_plan(async_checkpointer: BaseCheckpointSaver) -> None:
from langchain_core.messages import AnyMessage
class State(TypedDict, total=False):
plan: list[str | list[str]]
@@ -6755,6 +6758,7 @@ async def test_multiple_interrupts_functional(
async_checkpointer: BaseCheckpointSaver,
) -> None:
"""Test multiple interrupts with functional API."""
from langgraph.func import entrypoint, task
counter = 0
@@ -7670,6 +7674,7 @@ async def test_configurable_propagates_to_stream_metadata() -> None:
async def test_stream_mode_messages_command() -> None:
from langchain_core.messages import HumanMessage
async def my_node(state):
return {"messages": HumanMessage(content="foo")}
@@ -7718,6 +7723,7 @@ async def test_stream_mode_messages_command() -> None:
async def test_stream_messages_dedupe_inputs() -> None:
from langchain_core.messages import AIMessage
async def call_model(state):
return {"messages": AIMessage("hi", id="1")}
@@ -7757,6 +7763,7 @@ async def test_stream_messages_dedupe_inputs() -> None:
async def test_stream_messages_dedupe_state(
async_checkpointer: BaseCheckpointSaver,
) -> None:
from langchain_core.messages import AIMessage
to_emit = [AIMessage("bye", id="1"), AIMessage("bye again", id="2")]
@@ -8135,6 +8142,9 @@ async def test_no_redundant_put_writes_for_cached_task(
async_checkpointer: BaseCheckpointSaver,
) -> None:
"""Cached @tasks on resume must not trigger redundant put_writes."""
from unittest.mock import patch
from langgraph.pregel._loop import PregelLoop
@task
async def setup(x: int) -> int:
@@ -8636,6 +8646,7 @@ async def test_batch_update_as_input(
async def test_draw_invalid():
from langchain_core.messages import BaseMessage
class AgentState(TypedDict):
messages: Annotated[list[BaseMessage], add_messages]
+4 -3
View File
@@ -4,13 +4,10 @@ import ipaddress
import pathlib
import re
import sys
import typing
import uuid
from enum import Enum
from typing import Annotated, Literal, Optional
import pydantic
import typing_extensions
from langgraph.checkpoint.base import BaseCheckpointSaver
from pydantic import (
BaseModel,
@@ -35,6 +32,10 @@ from tests.any_str import AnyStr
def test_is_supported_by_pydantic() -> None:
"""Test if types are supported by pydantic."""
import typing
import pydantic
import typing_extensions
class TypedDictExtensions(typing_extensions.TypedDict):
x: int
+5 -3
View File
@@ -10,14 +10,12 @@ from langchain_core.messages import AnyMessage, BaseMessage
from langchain_core.runnables import RunnableConfig
from langchain_core.runnables.graph import Edge as DrawableEdge
from langchain_core.runnables.graph import Node as DrawableNode
from langgraph.checkpoint.memory import InMemorySaver
from langgraph_sdk.client import get_client, get_sync_client
from langgraph_sdk.schema import StreamPart
from pydantic import BaseModel
from typing_extensions import TypedDict
from langgraph.errors import GraphInterrupt
from langgraph.graph import END, START, MessagesState, StateGraph, add_messages
from langgraph.graph import StateGraph, add_messages
from langgraph.pregel import Pregel
from langgraph.pregel.remote import RemoteGraph
from langgraph.types import Interrupt, StateSnapshot
@@ -1099,6 +1097,10 @@ def test_stream_context_base_model():
)
@pytest.mark.anyio
async def test_langgraph_cloud_integration():
from langgraph.checkpoint.memory import InMemorySaver
from langgraph_sdk.client import get_client, get_sync_client
from langgraph.graph import END, START, MessagesState, StateGraph
# create RemotePregel instance
client = get_client(url="http://localhost:8123")
+1 -1
View File
@@ -4,7 +4,6 @@ from unittest.mock import AsyncMock, MagicMock
import pytest
from langgraph.pregel import remote as remote_mod
from langgraph.pregel._remote_run_stream import (
_AsyncRemoteGraphRunStream,
_ChannelProjection,
@@ -578,6 +577,7 @@ def test_stream_events_v3_strips_checkpoint_keys_from_configurable():
def test_stream_events_v3_merges_tracing_headers_when_distributed_tracing(
monkeypatch,
):
from langgraph.pregel import remote as remote_mod
sync_client = MagicMock()
sync_client.threads.stream.return_value = MagicMock()
+5 -3
View File
@@ -11,9 +11,7 @@ from typing import Annotated, Any
from unittest.mock import Mock, patch
from uuid import uuid4
import httpx
import pytest
import requests
from langchain_core.callbacks import AsyncCallbackManagerForLLMRun, BaseCallbackHandler
from langchain_core.language_models.fake_chat_models import GenericFakeChatModel
from langchain_core.messages import AIMessage, AIMessageChunk, BaseMessage, HumanMessage
@@ -65,7 +63,6 @@ from langgraph.types import (
RetryPolicy,
Send,
TimeoutPolicy,
interrupt,
)
NEEDS_CONTEXTVARS = pytest.mark.skipif(
@@ -174,6 +171,8 @@ def test_checkpoint_ns_for_parent_command() -> None:
def test_should_retry_default_retry_on():
"""Test the default retry_on function."""
import httpx
import requests
# Create a RetryPolicy with default_retry_on
policy = RetryPolicy()
@@ -2199,6 +2198,7 @@ def test_graph_error_handler_does_not_swallow_interrupt_concurrent():
"""When a graph error handler is configured and a node calls interrupt()
concurrently with other nodes, the interrupt must still be raised not
silently swallowed."""
from langgraph.types import interrupt
class State(TypedDict):
foo: str
@@ -2587,6 +2587,8 @@ async def test_set_node_defaults_timeout():
.compile()
)
from langgraph.errors import NodeTimeoutError
with pytest.raises(NodeTimeoutError):
await graph.ainvoke({"foo": ""})
+3 -2
View File
@@ -6,11 +6,9 @@ from typing import Any
import pytest
from langgraph.checkpoint.memory import MemorySaver
from langgraph.store.memory import InMemoryStore
from pydantic import BaseModel, ValidationError
from typing_extensions import TypedDict
from langgraph._internal._constants import CONFIG_KEY_RUNTIME
from langgraph.errors import GraphDrained
from langgraph.graph import END, START, StateGraph
from langgraph.runtime import (
@@ -1179,6 +1177,9 @@ def test_foreign_object_in_runtime_slot_is_coerced() -> None:
`merge` when no per-run `context` is provided. `store` is resolved
separately, so it is not read off the foreign object in the coercion.
"""
from langgraph.store.memory import InMemoryStore
from langgraph._internal._constants import CONFIG_KEY_RUNTIME
store = InMemoryStore()
graph_level_context = {"source": "graph-level"}
+1 -1
View File
@@ -79,7 +79,7 @@ class DummyChannel:
def test_curated_core_allowlist_includes_messages() -> None:
try:
from langchain_core.messages import BaseMessage # noqa: PLC0415
from langchain_core.messages import BaseMessage
except Exception:
pytest.skip("langchain_core not available")
allowlist = curated_core_allowlist()
@@ -13,10 +13,8 @@ import operator
import time
from typing import Annotated, Any
from langgraph.checkpoint.memory import InMemorySaver
from typing_extensions import TypedDict
from langgraph.config import get_stream_writer
from langgraph.constants import END, START
from langgraph.graph import StateGraph
from langgraph.stream._mux import StreamMux
@@ -490,6 +488,7 @@ class _State(TypedDict):
def _my_node(state: _State) -> dict[str, Any]:
from langgraph.config import get_stream_writer
writer = get_stream_writer()
writer({"status": "working", "node": "my_node"})
@@ -607,6 +606,7 @@ def test_stream_events_v3_all_transformers_interleaved() -> None:
def test_stream_events_v3_all_transformers_with_checkpointer() -> None:
"""All transformers with a checkpointer — run.checkpoints populated."""
from langgraph.checkpoint.memory import InMemorySaver
builder = StateGraph(_State, input_schema=_State)
builder.add_node("my_node", _my_node)
@@ -645,6 +645,7 @@ def test_stream_events_v3_all_transformers_with_checkpointer() -> None:
def test_stream_events_v3_checkpoints_projection_opt_in() -> None:
"""run.checkpoints surfaces checkpoint data when opted in with a checkpointer."""
from langgraph.checkpoint.memory import InMemorySaver
builder = StateGraph(_State, input_schema=_State)
builder.add_node("my_node", _my_node)
@@ -12,10 +12,6 @@ from dataclasses import dataclass
from typing import Annotated, Any, TypeVar
import pytest
from langchain_core.language_models.chat_model_stream import (
AsyncChatModelStream,
ChatModelStream,
)
from langchain_core.messages import AIMessage, BaseMessage
from langgraph.checkpoint.memory import InMemorySaver
from pydantic import BaseModel, ValidationError
@@ -28,14 +24,6 @@ from langgraph.func import entrypoint
from langgraph.graph import StateGraph
from langgraph.graph.message import MessagesState
from langgraph.runtime import RunControl
from langgraph.stream import (
AsyncGraphRunStream,
AsyncSubgraphRunStream,
GraphRunStream,
LifecyclePayload,
StreamChannel,
SubgraphRunStream,
)
from langgraph.types import (
CheckpointPayload,
CheckpointStreamPart,
@@ -1190,32 +1178,3 @@ def _check_type_narrowing(part: StreamPart[_StateT, _OutputT]) -> None:
assert_type(part, DebugStreamPart[_StateT])
assert_type(part["data"], DebugPayload[_StateT])
assert_type(part["ns"], tuple[str, ...])
# --- v3 stream_events return / projection typing checks ---
# These functions are never called at runtime; `ty` validates the
# assert_type calls. They pin the public typing surface of
# stream_events(version="v3") / astream_events(version="v3"): the handle
# type and the always-registered native projections.
def _check_stream_events_v3_typing() -> None:
"""Compile-time checks for sync v3 typing — never called at runtime."""
graph = _make_simple_graph().compile()
run = graph.stream_events(_SIMPLE_INPUT, version="v3")
assert_type(run, GraphRunStream)
assert_type(run.values, StreamChannel[dict[str, Any]])
assert_type(run.messages, StreamChannel[ChatModelStream])
assert_type(run.lifecycle, StreamChannel[LifecyclePayload])
assert_type(run.subgraphs, StreamChannel[SubgraphRunStream])
async def _check_astream_events_v3_typing() -> None:
"""Compile-time checks for async v3 typing — never called at runtime."""
graph = _make_simple_graph().compile()
run = await graph.astream_events(_SIMPLE_INPUT, version="v3")
assert_type(run, AsyncGraphRunStream)
assert_type(run.values, StreamChannel[dict[str, Any]])
assert_type(run.messages, StreamChannel[AsyncChatModelStream])
assert_type(run.lifecycle, StreamChannel[LifecyclePayload])
assert_type(run.subgraphs, StreamChannel[AsyncSubgraphRunStream])

Some files were not shown because too many files have changed in this diff Show More