Compare commits

...
Author SHA1 Message Date
Hugo Durand 81d5c4e2d6 fix(cli): treat a malformed listener id like an unknown one 2026-09-22 17:19:32 -04:00
Hugo Durand 7ae82d40e3 fix(cli): look up an explicit listener by id instead of searching a page 2026-09-22 17:16:36 -04:00
Hugo Durand 075e85925b merge: main into the listener placement branch 2026-09-22 16:43:08 -04:00
Hugo Durand 80b94edd75 refactor(cli): let the client own its endpoints and split resolving from announcing 2026-09-22 16:31:38 -04:00
Hugo Durand 4ea2701a29 fix(cli): refuse a malformed listener instead of dropping it 2026-09-22 16:29:33 -04:00
Hugo Durand 12e16f5fcb fix(cli): refuse to assume a name is free when the search page is full 2026-09-22 16:28:51 -04:00
Sreekara YachamaneniandGitHub 1211af45b1 feat(cli): Update langgraph deploy command to use agent_id and environment args (#9055)
- Accept agent_id and environment args for `lanngraph deploy`
  - Validate both arguments present or none
- If agent arguments present, make sure deployment_id and name are not
present
2026-09-22 16:28:36 -04:00
Hugo Durand 1c5be315e4 fix(cli): show the control plane's reason without the HTTP envelope 2026-09-22 16:27:39 -04:00
Hugo Durand e42ab5d589 fix(cli): point a managed build at --push-to in a listener workspace 2026-09-22 16:05:15 -04:00
Hugo Durand a4ed4dc356 refactor(cli): read the placement decision at its only call site 2026-09-22 15:47:45 -04:00
Hugo Durand 0566c60bc2 fix(cli): never match another deployment when the server ignores the name filter 2026-09-22 15:47:05 -04:00
Hugo Durand a3a2a2e33a refactor(cli): drop listeners without an id and name the source arguments 2026-09-22 15:28:20 -04:00
Hugo Durand f89bf66d84 fix(cli): refuse listener flags for an existing deployment before any call 2026-09-22 15:27:09 -04:00
Hugo Durand 904478cbec fix(cli): look up a deployment by exact name instead of a paged search 2026-09-22 15:25:28 -04:00
Hugo Durand beb3e9c1f1 fix(cli): look up listeners only when they can change the placement 2026-09-22 15:23:36 -04:00
Hugo Durand e1156979e3 feat(cli): announce the listener a deployment is placed on 2026-09-22 15:12:10 -04:00
Hugo Durand b809832ea3 feat(cli): refuse listener flags where placement cannot apply 2026-09-22 14:44:39 -04:00
Hugo Durand 16bc576b1b feat(cli): place new self-hosted deployments on a listener 2026-09-22 14:42:58 -04:00
Hugo Durand b7dd4b376a feat(cli): tell the managed control plane apart from a self-hosted one 2026-09-22 14:38:19 -04:00
Hugo Durand 0a3fc2dd38 feat(cli): resolve a listener placement from the workspace listeners 2026-09-22 14:37:37 -04:00
Hugo Durand 2ff8d6e950 feat(cli): read the workspace listeners from the control plane 2026-09-22 14:36:34 -04:00
Hugo Durand 04a78aafad refactor(cli): return resource lists from the control plane client 2026-09-22 14:35:31 -04:00
Randall HidajatGitHubHari Dhanushkodiopen-swe[bot] <open-swe@users.noreply.github.com>Hugo Durand
1afaca35a0 feat(cli): add --image-uri flag for self-hosted deployments (#8482)
Adds `--image-uri <uri>` to `langgraph deploy` so self-hosted LangSmith
customers can build, push, and deploy in one command without needing to
script the three steps manually.

When `--image-uri` is provided the CLI:
- Builds the image tagged to the provided URI (same Docker build path as
the local build flow)
- Pushes using whatever Docker credentials are already in the
environment (e.g. WIF, `aws ecr get-login-password`) — no auth handling
in the CLI
- PATCHes the deployment with `source_revision_config.image_uri` (no
`revision_source`, which the self-hosted control plane rejects for
`external_docker` deployments)

Also fixes two self-hosted-specific issues uncovered during testing:
- `LANGSMITH_ENDPOINT` is now used as a fallback when
`LANGGRAPH_HOST_URL` isn't set — the CLI strips the `/api/v1` path and
appends `/api-host` to reach the control plane
- The httpx client now builds full URLs via string concatenation rather
than relying on httpx base_url merging, which silently dropped the
`/api-host` path prefix when paths started with `/`
- The "Check status at" URL after a deploy now correctly points to the
self-hosted LangSmith UI instead of `smith.langchain.com`

**How did you verify your code works?**
Tested end-to-end against a self-hosted LangSmith instance using ECR as
the registry. `langgraph deploy --image-uri <ecr-uri>` successfully
built, pushed, and triggered a deployment revision. Confirmed the
existing SaaS flow (`langgraph deploy` without `--image-uri`) is
unaffected — the new flag is opt-in and the `LANGSMITH_ENDPOINT`
fallback only activates when `LANGGRAPH_HOST_URL` is unset and
`LANGSMITH_ENDPOINT` is present.

---------

Co-authored-by: Hari Dhanushkodi <hari-dhanushkodi@users.noreply.github.com>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Co-authored-by: Hugo Durand <hugo.durand@langchain.dev>
2026-09-22 11:50:02 -04:00
Elior Nataf LackritzandGitHub 49cce0ca85 release(sdk-py): 0.4.5 (#8988)
Bumps the Python SDK version from 0.4.4 to 0.4.5. Adds `response_schema`
to the `Interrupt` TypedDict (#8886).
2026-09-21 10:38:26 -04:00
Elior Nataf LackritzandGitHub 19273fa88b release(langgraph): 1.2.12 (#8987)
Bumps langgraph from 1.2.11 to 1.2.12. Ships `response_schema` on
`interrupt()` (#8886).
2026-09-21 10:38:21 -04:00
John KennedyGitHubopen-swe[bot] <open-swe@users.noreply.github.com>
ed384f3a12 fix(cli): remediate AnyIO vulnerabilities in example lockfiles (#9022)
- Upgrade AnyIO from 4.13.0 to 4.14.2 in both uv example lockfiles,
fixing GHSA-82r6-8w77-94w6 (TLS certificate spoofing) and
GHSA-5p39-cfhj-2xmp (process-pool hangs).
- Remove the orphaned examples Poetry lockfile left behind by the uv
migration; current example tooling does not consume it.
- Addresses all six currently open Dependabot alerts without changing
unrelated dependencies.

Made by [Open SWE](https://github.com/langchain-ai/open-swe) · [view
thread](https://openswe.vercel.app/agents/37d08f4f-9fe6-51be-adc9-d58aa9e6e010)
· openai:gpt-6-astra (medium)

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-09-20 12:50:31 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
aa742fb31e chore(deps): bump anyio from 4.14.2 to 4.15.1 in /libs/sdk-py (#8997)
Bumps [anyio](https://github.com/agronholm/anyio) from 4.14.2 to 4.15.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/agronholm/anyio/releases">anyio's
releases</a>.</em></p>
<blockquote>
<h2>4.15.1</h2>
<ul>
<li>Implemented a compatibility fix for supporting direct access of
<code>anyio.*</code> submodules from the main package even when those
submodules were not directly imported first (<!-- raw HTML omitted --><a
href="https://redirect.github.com/agronholm/anyio/issues/1311">#1311</a>
&lt;<a
href="https://redirect.github.com/agronholm/anyio/issues/1311%5C%3E">agronholm/anyio#1311</a><!--
raw HTML omitted -->)</li>
</ul>
<h2>4.15.0</h2>
<ul>
<li>
<p>Added support for the newer keyword-only arguments on
<code>anyio.Path</code> methods to match the standard library
<code>pathlib.Path</code>:</p>
<ul>
<li><code>follow_symlinks</code> on <code>exists()</code> (Python
3.12+)</li>
<li><code>follow_symlinks</code> on <code>is_dir()</code> (Python
3.13+)</li>
<li><code>follow_symlinks</code> on <code>is_file()</code> (Python
3.13+)</li>
<li><code>follow_symlinks</code> on <code>owner()</code> (Python
3.13+)</li>
<li><code>follow_symlinks</code> on <code>group()</code> (Python
3.13+)</li>
<li><code>newline</code> on <code>read_text()</code> (Python 3.13+)</li>
</ul>
<p>(<a
href="https://redirect.github.com/agronholm/anyio/pull/1286">#1286</a>,
<a
href="https://redirect.github.com/agronholm/anyio/pull/1293">#1293</a>;
PR by <a
href="https://github.com/jaideeppyne"><code>@​jaideeppyne</code></a>)</p>
</li>
<li>
<p>Added <code>amap</code>, <code>gather</code>, and
<code>as_completed</code> utility functions to simplify common patterns
(<a
href="https://redirect.github.com/agronholm/anyio/pull/1173">#1173</a>;
PR by <a
href="https://github.com/Graeme22"><code>@​Graeme22</code></a>)</p>
</li>
<li>
<p>Added <code>--anyio-mode</code> command-line option as an alternative
to the <code>anyio_mode</code> ini setting, and fix the pytest plugin's
auto mode detection to recognize the mode when set via either
mechanism(e.g: <code>pytest_asyncio</code>). (<a
href="https://redirect.github.com/agronholm/anyio/pull/1242">#1242</a>;
PR by <a
href="https://github.com/EmmanuelNiyonshuti"><code>@​EmmanuelNiyonshuti</code></a>)</p>
</li>
<li>
<p>Added the <code>anyio.Future</code> synchronization primitive which
behaves similar to <code>asyncio.Future</code>, allowing tasks to wait
for a value (or exception) from another task (<a
href="https://redirect.github.com/agronholm/anyio/pull/1146">#1146</a>;
PR by <a
href="https://github.com/Vizonex"><code>@​Vizonex</code></a>)</p>
</li>
<li>
<p>Added guidance for managing multiple memory object stream producers
and consumers with cloned streams (<a
href="https://redirect.github.com/agronholm/anyio/issues/330">#330</a>;
PR by <a
href="https://github.com/nightcityblade"><code>@​nightcityblade</code></a>)</p>
</li>
<li>
<p>Added <code>StapledObjectStream.send_nowait()</code> that delegates
to the underlying <code>ObjectSendStream</code>, if it implements it (<a
href="https://redirect.github.com/agronholm/anyio/pull/1241">#1241</a>;
PR by <a
href="https://github.com/davidbrochart"><code>@​davidbrochart</code></a>)</p>
</li>
<li>
<p>Added the <code>move_on_at()</code> and <code>fail_at()</code>
functions to complement <code>move_on_after()</code> and
<code>fail_after()</code></p>
</li>
<li>
<p>Changed the default name for a task spawned with
<code>TaskGroup.create_task(func())</code> to match the default task
name for the analogous task spawned with
<code>TaskGroup.start_soon(func)</code> or
<code>TaskGroup.start(func)</code> in more situations. Previously, the
default name of a <code>TaskGroup.create_task</code> task never included
the module name. (The default name for a task spawned with
<code>TaskGroup.start_soon</code> or <code>TaskGroup.start</code>
typically includes the module name.) (<a
href="https://redirect.github.com/agronholm/anyio/pull/1234">#1234</a>;
PR by <a
href="https://github.com/gschaffner"><code>@​gschaffner</code></a>)</p>
</li>
<li>
<p>Changed the <code>anyio</code> and <code>anyio.abc</code> modules to
lazily (much like <code>810</code>) import the necessary submodules.
This is done by parsing the AST of the module and building a lookup
table from the <code>if TYPE_CHECKING:</code> block. A fallback mode has
been provided for installations where the source code is unavailable
(e.g. PyInstaller). (<a
href="https://redirect.github.com/agronholm/anyio/pull/1169">#1169</a>)</p>
</li>
<li>
<p>Fixed free-threading compatibility issues arising from the fact that
on Python 3.14 free-threading builds, newly created threads inherit the
current context by default, causing AnyIO to behave erroneously in
relation to <code>start_blocking_portal()</code> and
<code>anyio.to_thread.run_sync()</code> (<a
href="https://redirect.github.com/agronholm/anyio/pull/1224">#1224</a>;
PR by <a
href="https://github.com/EmmanuelNiyonshuti"><code>@​EmmanuelNiyonshuti</code></a>)</p>
</li>
<li>
<p>Fixed <code>SpooledTemporaryFile.readinto()</code> and
<code>readinto1()</code> reading twice before rollover, so the
destination buffer was overwritten by the second read and the file
position advanced twice, silently losing data (<a
href="https://redirect.github.com/agronholm/anyio/pull/1215">#1215</a>;
PR by <a
href="https://github.com/c-tonneslan"><code>@​c-tonneslan</code></a>)</p>
</li>
<li>
<p>Added a <code>reason</code> parameter to <code>fail_after</code> (and
the new <code>fail_at</code>) allowing for added exception context when
raising <code>TimeoutError</code> (<a
href="https://redirect.github.com/agronholm/anyio/pull/1227">#1227</a>;
PR by <a
href="https://github.com/Graeme22"><code>@​Graeme22</code></a>)</p>
</li>
<li>
<p>Fixed the default <code>TaskHandle.name</code> missing part of the
task name for tasks started with <code>TaskGroup.start</code> on Trio
(<a
href="https://redirect.github.com/agronholm/anyio/issues/1231">#1231</a>;
PR by <a
href="https://github.com/gschaffner"><code>@​gschaffner</code></a>)</p>
</li>
<li>
<p>Fixed <code>anyio.run</code> leaking, or at least, delaying
collection of loop and root_task due to the root task being cached in a
<code>RunVar</code>. (<a
href="https://redirect.github.com/agronholm/anyio/issues/1203">#1203</a>;
PR by <a
href="https://github.com/tapetersen"><code>@​tapetersen</code></a>)</p>
</li>
<li>
<p>Fixed <code>anyio.Path.with_stem()</code> silently producing a wrong
path (e.g. <code>Path(&quot;.txt&quot;)</code>) instead of raising
<code>ValueError</code> when given an empty stem on a path with a
non-empty suffix, unlike <code>pathlib.PurePath.with_stem</code> (<a
href="https://redirect.github.com/agronholm/anyio/pull/1200">#1200</a>;
PR by <a
href="https://github.com/Sanjays2402"><code>@​Sanjays2402</code></a>)</p>
</li>
<li>
<p>Fixed <code>UNIXSocketStream.aclose()</code> raising
<code>asyncio.InvalidStateError</code> when a concurrent receive or send
operation had just been cancelled on the asyncio backend (<a
href="https://redirect.github.com/agronholm/anyio/issues/1267">#1267</a>;
PR by <a
href="https://github.com/alloutflo"><code>@​alloutflo</code></a>)</p>
</li>
<li>
<p>Fixed the pytest plugin importing the deprecated
<code>_pytest.python.CallSpec2</code> alias, which triggers
<code>PytestRemovedIn10Warning</code> on <code>pytest&gt;=9.2</code> and
crashes pytest at startup when <code>filterwarnings = error</code> is
configured (<a
href="https://redirect.github.com/agronholm/anyio/issues/1271">#1271</a>;
PR by <a
href="https://github.com/matthewfeickert"><code>@​matthewfeickert</code></a>)</p>
</li>
<li>
<p>Fixed an asyncio worker thread race that could raise
<code>RuntimeError</code> when the event loop closed between checking
its state and scheduling the worker result (<a
href="https://redirect.github.com/agronholm/anyio/issues/1265">#1265</a>;
PR by <a
href="https://github.com/hansu650"><code>@​hansu650</code></a>)</p>
</li>
<li>
<p>Fixed <code>CapacityLimiter</code> on the asyncio backend
over-granting tokens when <code>total_tokens</code> was raised while the
limiter was over-subscribed (<a
href="https://redirect.github.com/agronholm/anyio/pull/1223">#1223</a>;
PR by <a
href="https://github.com/zelinewang"><code>@​zelinewang</code></a>)</p>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/agronholm/anyio/commit/ffcd1542cd6d127980205f90a0100078849dd703"><code>ffcd154</code></a>
Bumped up the version</li>
<li><a
href="https://github.com/agronholm/anyio/commit/0ecf5ed98d294242509b043ebd1a0843e52d892f"><code>0ecf5ed</code></a>
Added a workaround for third party code accessing unimported submodules
(<a
href="https://redirect.github.com/agronholm/anyio/issues/1309">#1309</a>)</li>
<li><a
href="https://github.com/agronholm/anyio/commit/928366259543412a2deb1e2ba09ea45ffa92ef4f"><code>9283662</code></a>
Bumped up the version</li>
<li><a
href="https://github.com/agronholm/anyio/commit/d137692a90f76e4f71605e32ea5ca94cab3a539d"><code>d137692</code></a>
Improved the instructions for AI agents</li>
<li><a
href="https://github.com/agronholm/anyio/commit/033fc52b8fa8e90c5d0ef24b10b3860e974a6265"><code>033fc52</code></a>
Shield TemporaryDirectory cleanup from cancellation (<a
href="https://redirect.github.com/agronholm/anyio/issues/1304">#1304</a>)</li>
<li><a
href="https://github.com/agronholm/anyio/commit/942e9a6552cc10b5aaa779d84bfc8e2c3d5fcffc"><code>942e9a6</code></a>
[pre-commit.ci] pre-commit autoupdate (<a
href="https://redirect.github.com/agronholm/anyio/issues/1305">#1305</a>)</li>
<li><a
href="https://github.com/agronholm/anyio/commit/b825c3be7cb4ca1a8000b8065d4e147843deb704"><code>b825c3b</code></a>
Fixed pyproject.toml changes not triggering the test suite</li>
<li><a
href="https://github.com/agronholm/anyio/commit/9727dc504681e2986b5bc285de9571fb467539af"><code>9727dc5</code></a>
Fixed start inconsistencies between trio and asyncio (<a
href="https://redirect.github.com/agronholm/anyio/issues/1198">#1198</a>)</li>
<li><a
href="https://github.com/agronholm/anyio/commit/b05fe6d160a640355c201363cab286a7d2581da8"><code>b05fe6d</code></a>
Fixed wrong type in move_on_after (<a
href="https://redirect.github.com/agronholm/anyio/issues/1297">#1297</a>)</li>
<li><a
href="https://github.com/agronholm/anyio/commit/44d0c93cc20079acbf38ba4dbed5ab9df323f153"><code>44d0c93</code></a>
Fixed asyncio task group coroutine cleanup (<a
href="https://redirect.github.com/agronholm/anyio/issues/1275">#1275</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/agronholm/anyio/compare/4.14.2...4.15.1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=anyio&package-manager=uv&previous-version=4.14.2&new-version=4.15.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/langchain-ai/langgraph/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-18 23:31:43 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
b58044ac7a chore(deps): bump anyio from 4.13.0 to 4.14.2 in /libs/cli (#8998)
Bumps [anyio](https://github.com/agronholm/anyio) from 4.13.0 to 4.14.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/agronholm/anyio/releases">anyio's
releases</a>.</em></p>
<blockquote>
<h2>4.14.2</h2>
<ul>
<li>Changed <code>ByteReceiveStream.receive()</code> implementations to
raise a <code>ValueError</code> when <code>max_bytes</code> is not a
positive integer (<a
href="https://redirect.github.com/agronholm/anyio/pull/1191">#1191</a>)</li>
<li>Fixed <code>CapacityLimiter.total_tokens</code> rejecting
<code>float(&quot;inf&quot;)</code> when the limiter was instantiated
outside of an event loop. The adapter setter checked for infinity by
identity (<code>value is math.inf</code>), so only the exact
<code>math.inf</code> singleton was accepted, while every backend setter
(using <code>math.isinf()</code>) accepts any positive infinity (<a
href="https://redirect.github.com/agronholm/anyio/pull/1189">#1189</a>;
PR by <a
href="https://github.com/greymoth-jp"><code>@​greymoth-jp</code></a>).</li>
<li>Fixed <code>to_process.run_sync()</code> deadlocking when the worker
function writes enough data to <code>sys.stderr</code> to fill the
(undrained) pipe buffer. The worker process now redirects
<code>sys.stderr</code> to <code>os.devnull</code> as well, matching the
documented behavior</li>
<li>Fixed <code>TLSStream.wrap()</code> matching an internationalized
(unicode) host name against the peer certificate using IDNA 2003 (via
the standard library) instead of IDNA 2008, which could cause the host
name to be matched against the wrong certificate (<a
href="https://redirect.github.com/agronholm/anyio/pull/1208">#1208</a>)</li>
<li>Fixed <code>anyio.open_process()</code> (and
<code>run_process()</code>) ignoring the <code>extra_groups</code>
argument, as it mistakenly passed the value of the <code>group</code>
argument instead (<a
href="https://redirect.github.com/agronholm/anyio/pull/1209">#1209</a>)</li>
<li>Fixed <code>CapacityLimiter.acquire_nowait()</code> and
<code>CapacityLimiter.acquire_nowait_on_behalf_of()</code> raising
<code>trio.WouldBlock</code> instead of <code>anyio.WouldBlock</code> on
the <code>trio</code> backend when there are no tokens available (<a
href="https://redirect.github.com/agronholm/anyio/pull/1218">#1218</a>)</li>
<li>Fixed <code>CapacityLimiter</code> on the asyncio backend
over-granting tokens (<code>borrowed_tokens</code> exceeding
<code>total_tokens</code> and <code>available_tokens</code> going
negative) when a non-blocking acquire was made in the window between a
token being released and the notified waiter resuming. The freed token
is now reserved for the woken waiter right away, so the non-blocking
acquire correctly raises <code>WouldBlock</code> (<a
href="https://redirect.github.com/agronholm/anyio/issues/1170">#1170</a>;
PR by <a
href="https://github.com/gaoflow"><code>@​gaoflow</code></a>)</li>
<li>Fixed unnecessary CPU spin when delivering cancellation from
<code>CancelScope</code> on asyncio under certain conditions, including
improper cancel scope nesting (<a
href="https://redirect.github.com/agronholm/anyio/issues/1111">#1111</a>)</li>
</ul>
<h2>4.14.1</h2>
<ul>
<li>Fixed teardown of higher-scoped async fixtures failing on asyncio
with <code>RuntimeError: Attempted to exit cancel scope in a different
task than it was entered in</code> when an async test raise an outcome
exception (e.g., <code>pytest.skip()</code>,
<code>pytest.xfail()</code>, or <code>pytest.fail()</code>) (<a
href="https://redirect.github.com/agronholm/anyio/issues/1179">#1179</a>;
PR by <a
href="https://github.com/EmmanuelNiyonshuti"><code>@​EmmanuelNiyonshuti</code></a>)</li>
<li>Fixed <code>CapacityLimiter.total_tokens</code> rejecting a value of
<code>0</code> when the limiter was instantiated outside of an event
loop, contradicting the documented behavior of allowing 0 total tokens
(<a
href="https://redirect.github.com/agronholm/anyio/pull/1183">#1183</a>;
PR by <a
href="https://github.com/nyxst4ck"><code>@​nyxst4ck</code></a>)</li>
</ul>
<h2>4.14.0</h2>
<ul>
<li>
<p>Added support for Python 3.15</p>
</li>
<li>
<p>Added an asynchronous implementation of the <code>itertools</code>
module (<a
href="https://redirect.github.com/agronholm/anyio/issues/998">#998</a>;
PR by <a href="https://github.com/11kkw"><code>@​11kkw</code></a>)</p>
</li>
<li>
<p>Added the <code>local_port</code> parameter to
<code>connect_tcp()</code> to allow binding to a specific local port
before connecting (<a
href="https://redirect.github.com/agronholm/anyio/issues/1067">#1067</a>;
PR by <a
href="https://github.com/nullwiz"><code>@​nullwiz</code></a>)</p>
</li>
<li>
<p>Added support for custom capacity limiters in async path and file I/O
functions and classes</p>
</li>
<li>
<p>Added the <code>create_task()</code> task group method for easier
asyncio migration (returns a <code>TaskHandle</code>) (<a
href="https://redirect.github.com/agronholm/anyio/pull/1098">#1098</a>)</p>
</li>
<li>
<p>Changed <code>TaskGroup.start_soon()</code> to return a
<code>TaskHandle</code></p>
</li>
<li>
<p>Added an option for <code>TaskGroup.start()</code> to return a
<code>TaskHandle</code> (which then contains the start value in the
<code>start_value</code> property)</p>
</li>
<li>
<p>Added the <code>cancel()</code> convenience method to
<code>TaskGroup</code> as a shortcut for cancelling the task group's
cancel scope</p>
</li>
<li>
<p>Improved the error message when a known backend is not installed to
suggest the install command (<a
href="https://redirect.github.com/agronholm/anyio/pull/1115">#1115</a>;
PR by <a
href="https://github.com/EmmanuelNiyonshuti"><code>@​EmmanuelNiyonshuti</code></a>)</p>
</li>
<li>
<p>Improved <code>anyio.Path</code> to preserve subclass types by
returning <code>Self</code> in methods that return path objects (<a
href="https://redirect.github.com/agronholm/anyio/issues/1130">#1130</a>;
PR by <a
href="https://github.com/EmmanuelNiyonshuti"><code>@​EmmanuelNiyonshuti</code></a>)</p>
</li>
<li>
<p>Changed the parameter type annotation in
<code>anyio.Path.write_bytes()</code> to accept any
<code>ReadableBuffer</code>, thus allowing it to accept
<code>bytearray</code> and <code>memoryview</code> to match
<code>pathlib.Path.write_bytes()</code> (<a
href="https://redirect.github.com/agronholm/anyio/issues/1135">#1135</a>;
PR by <a href="https://github.com/SAY-5"><code>@​SAY-5</code></a>)</p>
</li>
<li>
<p>Changed several type annotations to only accept callables returning
coroutine-like objects instead of arbitrary awaitables:</p>
<ul>
<li><code>TaskGroup.start_soon()</code></li>
<li><code>TaskGroup.start()</code></li>
<li><code>anyio.from_thread.run()</code></li>
</ul>
<p>This reverts an earlier change from v3.7.0 which was made in error.
(<a
href="https://redirect.github.com/agronholm/anyio/pull/1153">#1153</a>)</p>
</li>
<li>
<p>Changed <code>anyio.run</code> to support callables returning
arbitrary awaitables at runtime on all backends. Previously, this only
worked on asyncio (<a
href="https://redirect.github.com/agronholm/anyio/pull/1171">#1171</a>;
PR by <a
href="https://github.com/gschaffner"><code>@​gschaffner</code></a>)</p>
</li>
<li>
<p>Changed several classes (and their subclasses) to have
<code>__slots__</code> (with <code>__weakref__</code>):</p>
<ul>
<li><code>anyio.CancelScope</code></li>
</ul>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/agronholm/anyio/commit/c384f99687c64c59ed8a11c3a0f11a2d57daff71"><code>c384f99</code></a>
Bumped up the version</li>
<li><a
href="https://github.com/agronholm/anyio/commit/dbba29d1ade7936f18fb71ba24aa92978673482a"><code>dbba29d</code></a>
Fixed 100% CPU spin on cancel scope misuse (<a
href="https://redirect.github.com/agronholm/anyio/issues/1217">#1217</a>)</li>
<li><a
href="https://github.com/agronholm/anyio/commit/6bbc6c33caabc13af5bc4256f745027cf8d5d7b8"><code>6bbc6c3</code></a>
Fix CapacityLimiter over-granting tokens on asyncio (<a
href="https://redirect.github.com/agronholm/anyio/issues/1172">#1172</a>)</li>
<li><a
href="https://github.com/agronholm/anyio/commit/6f82b2537cbbe98f3df3f295499056ab7de0b15b"><code>6f82b25</code></a>
Refactored TestTLSStream.test_receive_invalid_max_bytes() to be less
flaky</li>
<li><a
href="https://github.com/agronholm/anyio/commit/be24b0414f67f604bcbdd5ea3bcc56ab920d872e"><code>be24b04</code></a>
Relaxed timeouts to fix test flakiness</li>
<li><a
href="https://github.com/agronholm/anyio/commit/81135065749b4f60c06619b9caaf0a11871c1ddf"><code>8113506</code></a>
Fix test flakiness caused by slow callback duration logging</li>
<li><a
href="https://github.com/agronholm/anyio/commit/1e988b617b69588e33fecb75e36a9837245f562f"><code>1e988b6</code></a>
Fixed CapacityLimiter raising trio.WouldBlock instead of
anyio.WouldBlock (<a
href="https://redirect.github.com/agronholm/anyio/issues/1">#1</a>...</li>
<li><a
href="https://github.com/agronholm/anyio/commit/44713f345cd29dd4e7d76553c134543a1296cc62"><code>44713f3</code></a>
Pin setup-uv to a commit sha across downstream jobs (<a
href="https://redirect.github.com/agronholm/anyio/issues/1213">#1213</a>)</li>
<li><a
href="https://github.com/agronholm/anyio/commit/f1b7301c8264b0d2e8d24a5788fd29e93dea4040"><code>f1b7301</code></a>
Fixed stderr writes in a worker subprocess causing a deadlock (<a
href="https://redirect.github.com/agronholm/anyio/issues/1207">#1207</a>)</li>
<li><a
href="https://github.com/agronholm/anyio/commit/212be93c2cf2c841e753e95e5e2c543ee7feca90"><code>212be93</code></a>
Fix flaky test_tcp_listener_same_port using a hardcoded port (<a
href="https://redirect.github.com/agronholm/anyio/issues/1206">#1206</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/agronholm/anyio/compare/4.13.0...4.14.2">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=anyio&package-manager=uv&previous-version=4.13.0&new-version=4.14.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/langchain-ai/langgraph/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-18 23:29:43 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
daa514a988 chore(deps): bump anyio from 4.13.0 to 4.14.2 in /libs/checkpoint-conformance (#8996)
Bumps [anyio](https://github.com/agronholm/anyio) from 4.13.0 to 4.14.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/agronholm/anyio/releases">anyio's
releases</a>.</em></p>
<blockquote>
<h2>4.14.2</h2>
<ul>
<li>Changed <code>ByteReceiveStream.receive()</code> implementations to
raise a <code>ValueError</code> when <code>max_bytes</code> is not a
positive integer (<a
href="https://redirect.github.com/agronholm/anyio/pull/1191">#1191</a>)</li>
<li>Fixed <code>CapacityLimiter.total_tokens</code> rejecting
<code>float(&quot;inf&quot;)</code> when the limiter was instantiated
outside of an event loop. The adapter setter checked for infinity by
identity (<code>value is math.inf</code>), so only the exact
<code>math.inf</code> singleton was accepted, while every backend setter
(using <code>math.isinf()</code>) accepts any positive infinity (<a
href="https://redirect.github.com/agronholm/anyio/pull/1189">#1189</a>;
PR by <a
href="https://github.com/greymoth-jp"><code>@​greymoth-jp</code></a>).</li>
<li>Fixed <code>to_process.run_sync()</code> deadlocking when the worker
function writes enough data to <code>sys.stderr</code> to fill the
(undrained) pipe buffer. The worker process now redirects
<code>sys.stderr</code> to <code>os.devnull</code> as well, matching the
documented behavior</li>
<li>Fixed <code>TLSStream.wrap()</code> matching an internationalized
(unicode) host name against the peer certificate using IDNA 2003 (via
the standard library) instead of IDNA 2008, which could cause the host
name to be matched against the wrong certificate (<a
href="https://redirect.github.com/agronholm/anyio/pull/1208">#1208</a>)</li>
<li>Fixed <code>anyio.open_process()</code> (and
<code>run_process()</code>) ignoring the <code>extra_groups</code>
argument, as it mistakenly passed the value of the <code>group</code>
argument instead (<a
href="https://redirect.github.com/agronholm/anyio/pull/1209">#1209</a>)</li>
<li>Fixed <code>CapacityLimiter.acquire_nowait()</code> and
<code>CapacityLimiter.acquire_nowait_on_behalf_of()</code> raising
<code>trio.WouldBlock</code> instead of <code>anyio.WouldBlock</code> on
the <code>trio</code> backend when there are no tokens available (<a
href="https://redirect.github.com/agronholm/anyio/pull/1218">#1218</a>)</li>
<li>Fixed <code>CapacityLimiter</code> on the asyncio backend
over-granting tokens (<code>borrowed_tokens</code> exceeding
<code>total_tokens</code> and <code>available_tokens</code> going
negative) when a non-blocking acquire was made in the window between a
token being released and the notified waiter resuming. The freed token
is now reserved for the woken waiter right away, so the non-blocking
acquire correctly raises <code>WouldBlock</code> (<a
href="https://redirect.github.com/agronholm/anyio/issues/1170">#1170</a>;
PR by <a
href="https://github.com/gaoflow"><code>@​gaoflow</code></a>)</li>
<li>Fixed unnecessary CPU spin when delivering cancellation from
<code>CancelScope</code> on asyncio under certain conditions, including
improper cancel scope nesting (<a
href="https://redirect.github.com/agronholm/anyio/issues/1111">#1111</a>)</li>
</ul>
<h2>4.14.1</h2>
<ul>
<li>Fixed teardown of higher-scoped async fixtures failing on asyncio
with <code>RuntimeError: Attempted to exit cancel scope in a different
task than it was entered in</code> when an async test raise an outcome
exception (e.g., <code>pytest.skip()</code>,
<code>pytest.xfail()</code>, or <code>pytest.fail()</code>) (<a
href="https://redirect.github.com/agronholm/anyio/issues/1179">#1179</a>;
PR by <a
href="https://github.com/EmmanuelNiyonshuti"><code>@​EmmanuelNiyonshuti</code></a>)</li>
<li>Fixed <code>CapacityLimiter.total_tokens</code> rejecting a value of
<code>0</code> when the limiter was instantiated outside of an event
loop, contradicting the documented behavior of allowing 0 total tokens
(<a
href="https://redirect.github.com/agronholm/anyio/pull/1183">#1183</a>;
PR by <a
href="https://github.com/nyxst4ck"><code>@​nyxst4ck</code></a>)</li>
</ul>
<h2>4.14.0</h2>
<ul>
<li>
<p>Added support for Python 3.15</p>
</li>
<li>
<p>Added an asynchronous implementation of the <code>itertools</code>
module (<a
href="https://redirect.github.com/agronholm/anyio/issues/998">#998</a>;
PR by <a href="https://github.com/11kkw"><code>@​11kkw</code></a>)</p>
</li>
<li>
<p>Added the <code>local_port</code> parameter to
<code>connect_tcp()</code> to allow binding to a specific local port
before connecting (<a
href="https://redirect.github.com/agronholm/anyio/issues/1067">#1067</a>;
PR by <a
href="https://github.com/nullwiz"><code>@​nullwiz</code></a>)</p>
</li>
<li>
<p>Added support for custom capacity limiters in async path and file I/O
functions and classes</p>
</li>
<li>
<p>Added the <code>create_task()</code> task group method for easier
asyncio migration (returns a <code>TaskHandle</code>) (<a
href="https://redirect.github.com/agronholm/anyio/pull/1098">#1098</a>)</p>
</li>
<li>
<p>Changed <code>TaskGroup.start_soon()</code> to return a
<code>TaskHandle</code></p>
</li>
<li>
<p>Added an option for <code>TaskGroup.start()</code> to return a
<code>TaskHandle</code> (which then contains the start value in the
<code>start_value</code> property)</p>
</li>
<li>
<p>Added the <code>cancel()</code> convenience method to
<code>TaskGroup</code> as a shortcut for cancelling the task group's
cancel scope</p>
</li>
<li>
<p>Improved the error message when a known backend is not installed to
suggest the install command (<a
href="https://redirect.github.com/agronholm/anyio/pull/1115">#1115</a>;
PR by <a
href="https://github.com/EmmanuelNiyonshuti"><code>@​EmmanuelNiyonshuti</code></a>)</p>
</li>
<li>
<p>Improved <code>anyio.Path</code> to preserve subclass types by
returning <code>Self</code> in methods that return path objects (<a
href="https://redirect.github.com/agronholm/anyio/issues/1130">#1130</a>;
PR by <a
href="https://github.com/EmmanuelNiyonshuti"><code>@​EmmanuelNiyonshuti</code></a>)</p>
</li>
<li>
<p>Changed the parameter type annotation in
<code>anyio.Path.write_bytes()</code> to accept any
<code>ReadableBuffer</code>, thus allowing it to accept
<code>bytearray</code> and <code>memoryview</code> to match
<code>pathlib.Path.write_bytes()</code> (<a
href="https://redirect.github.com/agronholm/anyio/issues/1135">#1135</a>;
PR by <a href="https://github.com/SAY-5"><code>@​SAY-5</code></a>)</p>
</li>
<li>
<p>Changed several type annotations to only accept callables returning
coroutine-like objects instead of arbitrary awaitables:</p>
<ul>
<li><code>TaskGroup.start_soon()</code></li>
<li><code>TaskGroup.start()</code></li>
<li><code>anyio.from_thread.run()</code></li>
</ul>
<p>This reverts an earlier change from v3.7.0 which was made in error.
(<a
href="https://redirect.github.com/agronholm/anyio/pull/1153">#1153</a>)</p>
</li>
<li>
<p>Changed <code>anyio.run</code> to support callables returning
arbitrary awaitables at runtime on all backends. Previously, this only
worked on asyncio (<a
href="https://redirect.github.com/agronholm/anyio/pull/1171">#1171</a>;
PR by <a
href="https://github.com/gschaffner"><code>@​gschaffner</code></a>)</p>
</li>
<li>
<p>Changed several classes (and their subclasses) to have
<code>__slots__</code> (with <code>__weakref__</code>):</p>
<ul>
<li><code>anyio.CancelScope</code></li>
</ul>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/agronholm/anyio/commit/c384f99687c64c59ed8a11c3a0f11a2d57daff71"><code>c384f99</code></a>
Bumped up the version</li>
<li><a
href="https://github.com/agronholm/anyio/commit/dbba29d1ade7936f18fb71ba24aa92978673482a"><code>dbba29d</code></a>
Fixed 100% CPU spin on cancel scope misuse (<a
href="https://redirect.github.com/agronholm/anyio/issues/1217">#1217</a>)</li>
<li><a
href="https://github.com/agronholm/anyio/commit/6bbc6c33caabc13af5bc4256f745027cf8d5d7b8"><code>6bbc6c3</code></a>
Fix CapacityLimiter over-granting tokens on asyncio (<a
href="https://redirect.github.com/agronholm/anyio/issues/1172">#1172</a>)</li>
<li><a
href="https://github.com/agronholm/anyio/commit/6f82b2537cbbe98f3df3f295499056ab7de0b15b"><code>6f82b25</code></a>
Refactored TestTLSStream.test_receive_invalid_max_bytes() to be less
flaky</li>
<li><a
href="https://github.com/agronholm/anyio/commit/be24b0414f67f604bcbdd5ea3bcc56ab920d872e"><code>be24b04</code></a>
Relaxed timeouts to fix test flakiness</li>
<li><a
href="https://github.com/agronholm/anyio/commit/81135065749b4f60c06619b9caaf0a11871c1ddf"><code>8113506</code></a>
Fix test flakiness caused by slow callback duration logging</li>
<li><a
href="https://github.com/agronholm/anyio/commit/1e988b617b69588e33fecb75e36a9837245f562f"><code>1e988b6</code></a>
Fixed CapacityLimiter raising trio.WouldBlock instead of
anyio.WouldBlock (<a
href="https://redirect.github.com/agronholm/anyio/issues/1">#1</a>...</li>
<li><a
href="https://github.com/agronholm/anyio/commit/44713f345cd29dd4e7d76553c134543a1296cc62"><code>44713f3</code></a>
Pin setup-uv to a commit sha across downstream jobs (<a
href="https://redirect.github.com/agronholm/anyio/issues/1213">#1213</a>)</li>
<li><a
href="https://github.com/agronholm/anyio/commit/f1b7301c8264b0d2e8d24a5788fd29e93dea4040"><code>f1b7301</code></a>
Fixed stderr writes in a worker subprocess causing a deadlock (<a
href="https://redirect.github.com/agronholm/anyio/issues/1207">#1207</a>)</li>
<li><a
href="https://github.com/agronholm/anyio/commit/212be93c2cf2c841e753e95e5e2c543ee7feca90"><code>212be93</code></a>
Fix flaky test_tcp_listener_same_port using a hardcoded port (<a
href="https://redirect.github.com/agronholm/anyio/issues/1206">#1206</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/agronholm/anyio/compare/4.13.0...4.14.2">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=anyio&package-manager=uv&previous-version=4.13.0&new-version=4.14.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/langchain-ai/langgraph/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-18 15:36:37 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
022043a679 chore(deps): bump anyio from 4.12.1 to 4.14.2 in /libs/checkpoint (#8995)
Bumps [anyio](https://github.com/agronholm/anyio) from 4.12.1 to 4.14.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/agronholm/anyio/releases">anyio's
releases</a>.</em></p>
<blockquote>
<h2>4.14.2</h2>
<ul>
<li>Changed <code>ByteReceiveStream.receive()</code> implementations to
raise a <code>ValueError</code> when <code>max_bytes</code> is not a
positive integer (<a
href="https://redirect.github.com/agronholm/anyio/pull/1191">#1191</a>)</li>
<li>Fixed <code>CapacityLimiter.total_tokens</code> rejecting
<code>float(&quot;inf&quot;)</code> when the limiter was instantiated
outside of an event loop. The adapter setter checked for infinity by
identity (<code>value is math.inf</code>), so only the exact
<code>math.inf</code> singleton was accepted, while every backend setter
(using <code>math.isinf()</code>) accepts any positive infinity (<a
href="https://redirect.github.com/agronholm/anyio/pull/1189">#1189</a>;
PR by <a
href="https://github.com/greymoth-jp"><code>@​greymoth-jp</code></a>).</li>
<li>Fixed <code>to_process.run_sync()</code> deadlocking when the worker
function writes enough data to <code>sys.stderr</code> to fill the
(undrained) pipe buffer. The worker process now redirects
<code>sys.stderr</code> to <code>os.devnull</code> as well, matching the
documented behavior</li>
<li>Fixed <code>TLSStream.wrap()</code> matching an internationalized
(unicode) host name against the peer certificate using IDNA 2003 (via
the standard library) instead of IDNA 2008, which could cause the host
name to be matched against the wrong certificate (<a
href="https://redirect.github.com/agronholm/anyio/pull/1208">#1208</a>)</li>
<li>Fixed <code>anyio.open_process()</code> (and
<code>run_process()</code>) ignoring the <code>extra_groups</code>
argument, as it mistakenly passed the value of the <code>group</code>
argument instead (<a
href="https://redirect.github.com/agronholm/anyio/pull/1209">#1209</a>)</li>
<li>Fixed <code>CapacityLimiter.acquire_nowait()</code> and
<code>CapacityLimiter.acquire_nowait_on_behalf_of()</code> raising
<code>trio.WouldBlock</code> instead of <code>anyio.WouldBlock</code> on
the <code>trio</code> backend when there are no tokens available (<a
href="https://redirect.github.com/agronholm/anyio/pull/1218">#1218</a>)</li>
<li>Fixed <code>CapacityLimiter</code> on the asyncio backend
over-granting tokens (<code>borrowed_tokens</code> exceeding
<code>total_tokens</code> and <code>available_tokens</code> going
negative) when a non-blocking acquire was made in the window between a
token being released and the notified waiter resuming. The freed token
is now reserved for the woken waiter right away, so the non-blocking
acquire correctly raises <code>WouldBlock</code> (<a
href="https://redirect.github.com/agronholm/anyio/issues/1170">#1170</a>;
PR by <a
href="https://github.com/gaoflow"><code>@​gaoflow</code></a>)</li>
<li>Fixed unnecessary CPU spin when delivering cancellation from
<code>CancelScope</code> on asyncio under certain conditions, including
improper cancel scope nesting (<a
href="https://redirect.github.com/agronholm/anyio/issues/1111">#1111</a>)</li>
</ul>
<h2>4.14.1</h2>
<ul>
<li>Fixed teardown of higher-scoped async fixtures failing on asyncio
with <code>RuntimeError: Attempted to exit cancel scope in a different
task than it was entered in</code> when an async test raise an outcome
exception (e.g., <code>pytest.skip()</code>,
<code>pytest.xfail()</code>, or <code>pytest.fail()</code>) (<a
href="https://redirect.github.com/agronholm/anyio/issues/1179">#1179</a>;
PR by <a
href="https://github.com/EmmanuelNiyonshuti"><code>@​EmmanuelNiyonshuti</code></a>)</li>
<li>Fixed <code>CapacityLimiter.total_tokens</code> rejecting a value of
<code>0</code> when the limiter was instantiated outside of an event
loop, contradicting the documented behavior of allowing 0 total tokens
(<a
href="https://redirect.github.com/agronholm/anyio/pull/1183">#1183</a>;
PR by <a
href="https://github.com/nyxst4ck"><code>@​nyxst4ck</code></a>)</li>
</ul>
<h2>4.14.0</h2>
<ul>
<li>
<p>Added support for Python 3.15</p>
</li>
<li>
<p>Added an asynchronous implementation of the <code>itertools</code>
module (<a
href="https://redirect.github.com/agronholm/anyio/issues/998">#998</a>;
PR by <a href="https://github.com/11kkw"><code>@​11kkw</code></a>)</p>
</li>
<li>
<p>Added the <code>local_port</code> parameter to
<code>connect_tcp()</code> to allow binding to a specific local port
before connecting (<a
href="https://redirect.github.com/agronholm/anyio/issues/1067">#1067</a>;
PR by <a
href="https://github.com/nullwiz"><code>@​nullwiz</code></a>)</p>
</li>
<li>
<p>Added support for custom capacity limiters in async path and file I/O
functions and classes</p>
</li>
<li>
<p>Added the <code>create_task()</code> task group method for easier
asyncio migration (returns a <code>TaskHandle</code>) (<a
href="https://redirect.github.com/agronholm/anyio/pull/1098">#1098</a>)</p>
</li>
<li>
<p>Changed <code>TaskGroup.start_soon()</code> to return a
<code>TaskHandle</code></p>
</li>
<li>
<p>Added an option for <code>TaskGroup.start()</code> to return a
<code>TaskHandle</code> (which then contains the start value in the
<code>start_value</code> property)</p>
</li>
<li>
<p>Added the <code>cancel()</code> convenience method to
<code>TaskGroup</code> as a shortcut for cancelling the task group's
cancel scope</p>
</li>
<li>
<p>Improved the error message when a known backend is not installed to
suggest the install command (<a
href="https://redirect.github.com/agronholm/anyio/pull/1115">#1115</a>;
PR by <a
href="https://github.com/EmmanuelNiyonshuti"><code>@​EmmanuelNiyonshuti</code></a>)</p>
</li>
<li>
<p>Improved <code>anyio.Path</code> to preserve subclass types by
returning <code>Self</code> in methods that return path objects (<a
href="https://redirect.github.com/agronholm/anyio/issues/1130">#1130</a>;
PR by <a
href="https://github.com/EmmanuelNiyonshuti"><code>@​EmmanuelNiyonshuti</code></a>)</p>
</li>
<li>
<p>Changed the parameter type annotation in
<code>anyio.Path.write_bytes()</code> to accept any
<code>ReadableBuffer</code>, thus allowing it to accept
<code>bytearray</code> and <code>memoryview</code> to match
<code>pathlib.Path.write_bytes()</code> (<a
href="https://redirect.github.com/agronholm/anyio/issues/1135">#1135</a>;
PR by <a href="https://github.com/SAY-5"><code>@​SAY-5</code></a>)</p>
</li>
<li>
<p>Changed several type annotations to only accept callables returning
coroutine-like objects instead of arbitrary awaitables:</p>
<ul>
<li><code>TaskGroup.start_soon()</code></li>
<li><code>TaskGroup.start()</code></li>
<li><code>anyio.from_thread.run()</code></li>
</ul>
<p>This reverts an earlier change from v3.7.0 which was made in error.
(<a
href="https://redirect.github.com/agronholm/anyio/pull/1153">#1153</a>)</p>
</li>
<li>
<p>Changed <code>anyio.run</code> to support callables returning
arbitrary awaitables at runtime on all backends. Previously, this only
worked on asyncio (<a
href="https://redirect.github.com/agronholm/anyio/pull/1171">#1171</a>;
PR by <a
href="https://github.com/gschaffner"><code>@​gschaffner</code></a>)</p>
</li>
<li>
<p>Changed several classes (and their subclasses) to have
<code>__slots__</code> (with <code>__weakref__</code>):</p>
<ul>
<li><code>anyio.CancelScope</code></li>
</ul>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/agronholm/anyio/commit/c384f99687c64c59ed8a11c3a0f11a2d57daff71"><code>c384f99</code></a>
Bumped up the version</li>
<li><a
href="https://github.com/agronholm/anyio/commit/dbba29d1ade7936f18fb71ba24aa92978673482a"><code>dbba29d</code></a>
Fixed 100% CPU spin on cancel scope misuse (<a
href="https://redirect.github.com/agronholm/anyio/issues/1217">#1217</a>)</li>
<li><a
href="https://github.com/agronholm/anyio/commit/6bbc6c33caabc13af5bc4256f745027cf8d5d7b8"><code>6bbc6c3</code></a>
Fix CapacityLimiter over-granting tokens on asyncio (<a
href="https://redirect.github.com/agronholm/anyio/issues/1172">#1172</a>)</li>
<li><a
href="https://github.com/agronholm/anyio/commit/6f82b2537cbbe98f3df3f295499056ab7de0b15b"><code>6f82b25</code></a>
Refactored TestTLSStream.test_receive_invalid_max_bytes() to be less
flaky</li>
<li><a
href="https://github.com/agronholm/anyio/commit/be24b0414f67f604bcbdd5ea3bcc56ab920d872e"><code>be24b04</code></a>
Relaxed timeouts to fix test flakiness</li>
<li><a
href="https://github.com/agronholm/anyio/commit/81135065749b4f60c06619b9caaf0a11871c1ddf"><code>8113506</code></a>
Fix test flakiness caused by slow callback duration logging</li>
<li><a
href="https://github.com/agronholm/anyio/commit/1e988b617b69588e33fecb75e36a9837245f562f"><code>1e988b6</code></a>
Fixed CapacityLimiter raising trio.WouldBlock instead of
anyio.WouldBlock (<a
href="https://redirect.github.com/agronholm/anyio/issues/1">#1</a>...</li>
<li><a
href="https://github.com/agronholm/anyio/commit/44713f345cd29dd4e7d76553c134543a1296cc62"><code>44713f3</code></a>
Pin setup-uv to a commit sha across downstream jobs (<a
href="https://redirect.github.com/agronholm/anyio/issues/1213">#1213</a>)</li>
<li><a
href="https://github.com/agronholm/anyio/commit/f1b7301c8264b0d2e8d24a5788fd29e93dea4040"><code>f1b7301</code></a>
Fixed stderr writes in a worker subprocess causing a deadlock (<a
href="https://redirect.github.com/agronholm/anyio/issues/1207">#1207</a>)</li>
<li><a
href="https://github.com/agronholm/anyio/commit/212be93c2cf2c841e753e95e5e2c543ee7feca90"><code>212be93</code></a>
Fix flaky test_tcp_listener_same_port using a hardcoded port (<a
href="https://redirect.github.com/agronholm/anyio/issues/1206">#1206</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/agronholm/anyio/compare/4.12.1...4.14.2">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=anyio&package-manager=uv&previous-version=4.12.1&new-version=4.14.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/langchain-ai/langgraph/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-18 15:24:38 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
d7b99cc3ab chore(deps): bump anyio from 4.12.1 to 4.14.2 in /libs/sdk-py (#8994)
Bumps [anyio](https://github.com/agronholm/anyio) from 4.12.1 to 4.14.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/agronholm/anyio/releases">anyio's
releases</a>.</em></p>
<blockquote>
<h2>4.14.2</h2>
<ul>
<li>Changed <code>ByteReceiveStream.receive()</code> implementations to
raise a <code>ValueError</code> when <code>max_bytes</code> is not a
positive integer (<a
href="https://redirect.github.com/agronholm/anyio/pull/1191">#1191</a>)</li>
<li>Fixed <code>CapacityLimiter.total_tokens</code> rejecting
<code>float(&quot;inf&quot;)</code> when the limiter was instantiated
outside of an event loop. The adapter setter checked for infinity by
identity (<code>value is math.inf</code>), so only the exact
<code>math.inf</code> singleton was accepted, while every backend setter
(using <code>math.isinf()</code>) accepts any positive infinity (<a
href="https://redirect.github.com/agronholm/anyio/pull/1189">#1189</a>;
PR by <a
href="https://github.com/greymoth-jp"><code>@​greymoth-jp</code></a>).</li>
<li>Fixed <code>to_process.run_sync()</code> deadlocking when the worker
function writes enough data to <code>sys.stderr</code> to fill the
(undrained) pipe buffer. The worker process now redirects
<code>sys.stderr</code> to <code>os.devnull</code> as well, matching the
documented behavior</li>
<li>Fixed <code>TLSStream.wrap()</code> matching an internationalized
(unicode) host name against the peer certificate using IDNA 2003 (via
the standard library) instead of IDNA 2008, which could cause the host
name to be matched against the wrong certificate (<a
href="https://redirect.github.com/agronholm/anyio/pull/1208">#1208</a>)</li>
<li>Fixed <code>anyio.open_process()</code> (and
<code>run_process()</code>) ignoring the <code>extra_groups</code>
argument, as it mistakenly passed the value of the <code>group</code>
argument instead (<a
href="https://redirect.github.com/agronholm/anyio/pull/1209">#1209</a>)</li>
<li>Fixed <code>CapacityLimiter.acquire_nowait()</code> and
<code>CapacityLimiter.acquire_nowait_on_behalf_of()</code> raising
<code>trio.WouldBlock</code> instead of <code>anyio.WouldBlock</code> on
the <code>trio</code> backend when there are no tokens available (<a
href="https://redirect.github.com/agronholm/anyio/pull/1218">#1218</a>)</li>
<li>Fixed <code>CapacityLimiter</code> on the asyncio backend
over-granting tokens (<code>borrowed_tokens</code> exceeding
<code>total_tokens</code> and <code>available_tokens</code> going
negative) when a non-blocking acquire was made in the window between a
token being released and the notified waiter resuming. The freed token
is now reserved for the woken waiter right away, so the non-blocking
acquire correctly raises <code>WouldBlock</code> (<a
href="https://redirect.github.com/agronholm/anyio/issues/1170">#1170</a>;
PR by <a
href="https://github.com/gaoflow"><code>@​gaoflow</code></a>)</li>
<li>Fixed unnecessary CPU spin when delivering cancellation from
<code>CancelScope</code> on asyncio under certain conditions, including
improper cancel scope nesting (<a
href="https://redirect.github.com/agronholm/anyio/issues/1111">#1111</a>)</li>
</ul>
<h2>4.14.1</h2>
<ul>
<li>Fixed teardown of higher-scoped async fixtures failing on asyncio
with <code>RuntimeError: Attempted to exit cancel scope in a different
task than it was entered in</code> when an async test raise an outcome
exception (e.g., <code>pytest.skip()</code>,
<code>pytest.xfail()</code>, or <code>pytest.fail()</code>) (<a
href="https://redirect.github.com/agronholm/anyio/issues/1179">#1179</a>;
PR by <a
href="https://github.com/EmmanuelNiyonshuti"><code>@​EmmanuelNiyonshuti</code></a>)</li>
<li>Fixed <code>CapacityLimiter.total_tokens</code> rejecting a value of
<code>0</code> when the limiter was instantiated outside of an event
loop, contradicting the documented behavior of allowing 0 total tokens
(<a
href="https://redirect.github.com/agronholm/anyio/pull/1183">#1183</a>;
PR by <a
href="https://github.com/nyxst4ck"><code>@​nyxst4ck</code></a>)</li>
</ul>
<h2>4.14.0</h2>
<ul>
<li>
<p>Added support for Python 3.15</p>
</li>
<li>
<p>Added an asynchronous implementation of the <code>itertools</code>
module (<a
href="https://redirect.github.com/agronholm/anyio/issues/998">#998</a>;
PR by <a href="https://github.com/11kkw"><code>@​11kkw</code></a>)</p>
</li>
<li>
<p>Added the <code>local_port</code> parameter to
<code>connect_tcp()</code> to allow binding to a specific local port
before connecting (<a
href="https://redirect.github.com/agronholm/anyio/issues/1067">#1067</a>;
PR by <a
href="https://github.com/nullwiz"><code>@​nullwiz</code></a>)</p>
</li>
<li>
<p>Added support for custom capacity limiters in async path and file I/O
functions and classes</p>
</li>
<li>
<p>Added the <code>create_task()</code> task group method for easier
asyncio migration (returns a <code>TaskHandle</code>) (<a
href="https://redirect.github.com/agronholm/anyio/pull/1098">#1098</a>)</p>
</li>
<li>
<p>Changed <code>TaskGroup.start_soon()</code> to return a
<code>TaskHandle</code></p>
</li>
<li>
<p>Added an option for <code>TaskGroup.start()</code> to return a
<code>TaskHandle</code> (which then contains the start value in the
<code>start_value</code> property)</p>
</li>
<li>
<p>Added the <code>cancel()</code> convenience method to
<code>TaskGroup</code> as a shortcut for cancelling the task group's
cancel scope</p>
</li>
<li>
<p>Improved the error message when a known backend is not installed to
suggest the install command (<a
href="https://redirect.github.com/agronholm/anyio/pull/1115">#1115</a>;
PR by <a
href="https://github.com/EmmanuelNiyonshuti"><code>@​EmmanuelNiyonshuti</code></a>)</p>
</li>
<li>
<p>Improved <code>anyio.Path</code> to preserve subclass types by
returning <code>Self</code> in methods that return path objects (<a
href="https://redirect.github.com/agronholm/anyio/issues/1130">#1130</a>;
PR by <a
href="https://github.com/EmmanuelNiyonshuti"><code>@​EmmanuelNiyonshuti</code></a>)</p>
</li>
<li>
<p>Changed the parameter type annotation in
<code>anyio.Path.write_bytes()</code> to accept any
<code>ReadableBuffer</code>, thus allowing it to accept
<code>bytearray</code> and <code>memoryview</code> to match
<code>pathlib.Path.write_bytes()</code> (<a
href="https://redirect.github.com/agronholm/anyio/issues/1135">#1135</a>;
PR by <a href="https://github.com/SAY-5"><code>@​SAY-5</code></a>)</p>
</li>
<li>
<p>Changed several type annotations to only accept callables returning
coroutine-like objects instead of arbitrary awaitables:</p>
<ul>
<li><code>TaskGroup.start_soon()</code></li>
<li><code>TaskGroup.start()</code></li>
<li><code>anyio.from_thread.run()</code></li>
</ul>
<p>This reverts an earlier change from v3.7.0 which was made in error.
(<a
href="https://redirect.github.com/agronholm/anyio/pull/1153">#1153</a>)</p>
</li>
<li>
<p>Changed <code>anyio.run</code> to support callables returning
arbitrary awaitables at runtime on all backends. Previously, this only
worked on asyncio (<a
href="https://redirect.github.com/agronholm/anyio/pull/1171">#1171</a>;
PR by <a
href="https://github.com/gschaffner"><code>@​gschaffner</code></a>)</p>
</li>
<li>
<p>Changed several classes (and their subclasses) to have
<code>__slots__</code> (with <code>__weakref__</code>):</p>
<ul>
<li><code>anyio.CancelScope</code></li>
</ul>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/agronholm/anyio/commit/c384f99687c64c59ed8a11c3a0f11a2d57daff71"><code>c384f99</code></a>
Bumped up the version</li>
<li><a
href="https://github.com/agronholm/anyio/commit/dbba29d1ade7936f18fb71ba24aa92978673482a"><code>dbba29d</code></a>
Fixed 100% CPU spin on cancel scope misuse (<a
href="https://redirect.github.com/agronholm/anyio/issues/1217">#1217</a>)</li>
<li><a
href="https://github.com/agronholm/anyio/commit/6bbc6c33caabc13af5bc4256f745027cf8d5d7b8"><code>6bbc6c3</code></a>
Fix CapacityLimiter over-granting tokens on asyncio (<a
href="https://redirect.github.com/agronholm/anyio/issues/1172">#1172</a>)</li>
<li><a
href="https://github.com/agronholm/anyio/commit/6f82b2537cbbe98f3df3f295499056ab7de0b15b"><code>6f82b25</code></a>
Refactored TestTLSStream.test_receive_invalid_max_bytes() to be less
flaky</li>
<li><a
href="https://github.com/agronholm/anyio/commit/be24b0414f67f604bcbdd5ea3bcc56ab920d872e"><code>be24b04</code></a>
Relaxed timeouts to fix test flakiness</li>
<li><a
href="https://github.com/agronholm/anyio/commit/81135065749b4f60c06619b9caaf0a11871c1ddf"><code>8113506</code></a>
Fix test flakiness caused by slow callback duration logging</li>
<li><a
href="https://github.com/agronholm/anyio/commit/1e988b617b69588e33fecb75e36a9837245f562f"><code>1e988b6</code></a>
Fixed CapacityLimiter raising trio.WouldBlock instead of
anyio.WouldBlock (<a
href="https://redirect.github.com/agronholm/anyio/issues/1">#1</a>...</li>
<li><a
href="https://github.com/agronholm/anyio/commit/44713f345cd29dd4e7d76553c134543a1296cc62"><code>44713f3</code></a>
Pin setup-uv to a commit sha across downstream jobs (<a
href="https://redirect.github.com/agronholm/anyio/issues/1213">#1213</a>)</li>
<li><a
href="https://github.com/agronholm/anyio/commit/f1b7301c8264b0d2e8d24a5788fd29e93dea4040"><code>f1b7301</code></a>
Fixed stderr writes in a worker subprocess causing a deadlock (<a
href="https://redirect.github.com/agronholm/anyio/issues/1207">#1207</a>)</li>
<li><a
href="https://github.com/agronholm/anyio/commit/212be93c2cf2c841e753e95e5e2c543ee7feca90"><code>212be93</code></a>
Fix flaky test_tcp_listener_same_port using a hardcoded port (<a
href="https://redirect.github.com/agronholm/anyio/issues/1206">#1206</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/agronholm/anyio/compare/4.12.1...4.14.2">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=anyio&package-manager=uv&previous-version=4.12.1&new-version=4.14.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/langchain-ai/langgraph/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-18 15:24:34 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
b19edd783b chore(deps): bump anyio from 4.12.1 to 4.14.2 in /libs/checkpoint-sqlite (#8993)
Bumps [anyio](https://github.com/agronholm/anyio) from 4.12.1 to 4.14.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/agronholm/anyio/releases">anyio's
releases</a>.</em></p>
<blockquote>
<h2>4.14.2</h2>
<ul>
<li>Changed <code>ByteReceiveStream.receive()</code> implementations to
raise a <code>ValueError</code> when <code>max_bytes</code> is not a
positive integer (<a
href="https://redirect.github.com/agronholm/anyio/pull/1191">#1191</a>)</li>
<li>Fixed <code>CapacityLimiter.total_tokens</code> rejecting
<code>float(&quot;inf&quot;)</code> when the limiter was instantiated
outside of an event loop. The adapter setter checked for infinity by
identity (<code>value is math.inf</code>), so only the exact
<code>math.inf</code> singleton was accepted, while every backend setter
(using <code>math.isinf()</code>) accepts any positive infinity (<a
href="https://redirect.github.com/agronholm/anyio/pull/1189">#1189</a>;
PR by <a
href="https://github.com/greymoth-jp"><code>@​greymoth-jp</code></a>).</li>
<li>Fixed <code>to_process.run_sync()</code> deadlocking when the worker
function writes enough data to <code>sys.stderr</code> to fill the
(undrained) pipe buffer. The worker process now redirects
<code>sys.stderr</code> to <code>os.devnull</code> as well, matching the
documented behavior</li>
<li>Fixed <code>TLSStream.wrap()</code> matching an internationalized
(unicode) host name against the peer certificate using IDNA 2003 (via
the standard library) instead of IDNA 2008, which could cause the host
name to be matched against the wrong certificate (<a
href="https://redirect.github.com/agronholm/anyio/pull/1208">#1208</a>)</li>
<li>Fixed <code>anyio.open_process()</code> (and
<code>run_process()</code>) ignoring the <code>extra_groups</code>
argument, as it mistakenly passed the value of the <code>group</code>
argument instead (<a
href="https://redirect.github.com/agronholm/anyio/pull/1209">#1209</a>)</li>
<li>Fixed <code>CapacityLimiter.acquire_nowait()</code> and
<code>CapacityLimiter.acquire_nowait_on_behalf_of()</code> raising
<code>trio.WouldBlock</code> instead of <code>anyio.WouldBlock</code> on
the <code>trio</code> backend when there are no tokens available (<a
href="https://redirect.github.com/agronholm/anyio/pull/1218">#1218</a>)</li>
<li>Fixed <code>CapacityLimiter</code> on the asyncio backend
over-granting tokens (<code>borrowed_tokens</code> exceeding
<code>total_tokens</code> and <code>available_tokens</code> going
negative) when a non-blocking acquire was made in the window between a
token being released and the notified waiter resuming. The freed token
is now reserved for the woken waiter right away, so the non-blocking
acquire correctly raises <code>WouldBlock</code> (<a
href="https://redirect.github.com/agronholm/anyio/issues/1170">#1170</a>;
PR by <a
href="https://github.com/gaoflow"><code>@​gaoflow</code></a>)</li>
<li>Fixed unnecessary CPU spin when delivering cancellation from
<code>CancelScope</code> on asyncio under certain conditions, including
improper cancel scope nesting (<a
href="https://redirect.github.com/agronholm/anyio/issues/1111">#1111</a>)</li>
</ul>
<h2>4.14.1</h2>
<ul>
<li>Fixed teardown of higher-scoped async fixtures failing on asyncio
with <code>RuntimeError: Attempted to exit cancel scope in a different
task than it was entered in</code> when an async test raise an outcome
exception (e.g., <code>pytest.skip()</code>,
<code>pytest.xfail()</code>, or <code>pytest.fail()</code>) (<a
href="https://redirect.github.com/agronholm/anyio/issues/1179">#1179</a>;
PR by <a
href="https://github.com/EmmanuelNiyonshuti"><code>@​EmmanuelNiyonshuti</code></a>)</li>
<li>Fixed <code>CapacityLimiter.total_tokens</code> rejecting a value of
<code>0</code> when the limiter was instantiated outside of an event
loop, contradicting the documented behavior of allowing 0 total tokens
(<a
href="https://redirect.github.com/agronholm/anyio/pull/1183">#1183</a>;
PR by <a
href="https://github.com/nyxst4ck"><code>@​nyxst4ck</code></a>)</li>
</ul>
<h2>4.14.0</h2>
<ul>
<li>
<p>Added support for Python 3.15</p>
</li>
<li>
<p>Added an asynchronous implementation of the <code>itertools</code>
module (<a
href="https://redirect.github.com/agronholm/anyio/issues/998">#998</a>;
PR by <a href="https://github.com/11kkw"><code>@​11kkw</code></a>)</p>
</li>
<li>
<p>Added the <code>local_port</code> parameter to
<code>connect_tcp()</code> to allow binding to a specific local port
before connecting (<a
href="https://redirect.github.com/agronholm/anyio/issues/1067">#1067</a>;
PR by <a
href="https://github.com/nullwiz"><code>@​nullwiz</code></a>)</p>
</li>
<li>
<p>Added support for custom capacity limiters in async path and file I/O
functions and classes</p>
</li>
<li>
<p>Added the <code>create_task()</code> task group method for easier
asyncio migration (returns a <code>TaskHandle</code>) (<a
href="https://redirect.github.com/agronholm/anyio/pull/1098">#1098</a>)</p>
</li>
<li>
<p>Changed <code>TaskGroup.start_soon()</code> to return a
<code>TaskHandle</code></p>
</li>
<li>
<p>Added an option for <code>TaskGroup.start()</code> to return a
<code>TaskHandle</code> (which then contains the start value in the
<code>start_value</code> property)</p>
</li>
<li>
<p>Added the <code>cancel()</code> convenience method to
<code>TaskGroup</code> as a shortcut for cancelling the task group's
cancel scope</p>
</li>
<li>
<p>Improved the error message when a known backend is not installed to
suggest the install command (<a
href="https://redirect.github.com/agronholm/anyio/pull/1115">#1115</a>;
PR by <a
href="https://github.com/EmmanuelNiyonshuti"><code>@​EmmanuelNiyonshuti</code></a>)</p>
</li>
<li>
<p>Improved <code>anyio.Path</code> to preserve subclass types by
returning <code>Self</code> in methods that return path objects (<a
href="https://redirect.github.com/agronholm/anyio/issues/1130">#1130</a>;
PR by <a
href="https://github.com/EmmanuelNiyonshuti"><code>@​EmmanuelNiyonshuti</code></a>)</p>
</li>
<li>
<p>Changed the parameter type annotation in
<code>anyio.Path.write_bytes()</code> to accept any
<code>ReadableBuffer</code>, thus allowing it to accept
<code>bytearray</code> and <code>memoryview</code> to match
<code>pathlib.Path.write_bytes()</code> (<a
href="https://redirect.github.com/agronholm/anyio/issues/1135">#1135</a>;
PR by <a href="https://github.com/SAY-5"><code>@​SAY-5</code></a>)</p>
</li>
<li>
<p>Changed several type annotations to only accept callables returning
coroutine-like objects instead of arbitrary awaitables:</p>
<ul>
<li><code>TaskGroup.start_soon()</code></li>
<li><code>TaskGroup.start()</code></li>
<li><code>anyio.from_thread.run()</code></li>
</ul>
<p>This reverts an earlier change from v3.7.0 which was made in error.
(<a
href="https://redirect.github.com/agronholm/anyio/pull/1153">#1153</a>)</p>
</li>
<li>
<p>Changed <code>anyio.run</code> to support callables returning
arbitrary awaitables at runtime on all backends. Previously, this only
worked on asyncio (<a
href="https://redirect.github.com/agronholm/anyio/pull/1171">#1171</a>;
PR by <a
href="https://github.com/gschaffner"><code>@​gschaffner</code></a>)</p>
</li>
<li>
<p>Changed several classes (and their subclasses) to have
<code>__slots__</code> (with <code>__weakref__</code>):</p>
<ul>
<li><code>anyio.CancelScope</code></li>
</ul>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/agronholm/anyio/commit/c384f99687c64c59ed8a11c3a0f11a2d57daff71"><code>c384f99</code></a>
Bumped up the version</li>
<li><a
href="https://github.com/agronholm/anyio/commit/dbba29d1ade7936f18fb71ba24aa92978673482a"><code>dbba29d</code></a>
Fixed 100% CPU spin on cancel scope misuse (<a
href="https://redirect.github.com/agronholm/anyio/issues/1217">#1217</a>)</li>
<li><a
href="https://github.com/agronholm/anyio/commit/6bbc6c33caabc13af5bc4256f745027cf8d5d7b8"><code>6bbc6c3</code></a>
Fix CapacityLimiter over-granting tokens on asyncio (<a
href="https://redirect.github.com/agronholm/anyio/issues/1172">#1172</a>)</li>
<li><a
href="https://github.com/agronholm/anyio/commit/6f82b2537cbbe98f3df3f295499056ab7de0b15b"><code>6f82b25</code></a>
Refactored TestTLSStream.test_receive_invalid_max_bytes() to be less
flaky</li>
<li><a
href="https://github.com/agronholm/anyio/commit/be24b0414f67f604bcbdd5ea3bcc56ab920d872e"><code>be24b04</code></a>
Relaxed timeouts to fix test flakiness</li>
<li><a
href="https://github.com/agronholm/anyio/commit/81135065749b4f60c06619b9caaf0a11871c1ddf"><code>8113506</code></a>
Fix test flakiness caused by slow callback duration logging</li>
<li><a
href="https://github.com/agronholm/anyio/commit/1e988b617b69588e33fecb75e36a9837245f562f"><code>1e988b6</code></a>
Fixed CapacityLimiter raising trio.WouldBlock instead of
anyio.WouldBlock (<a
href="https://redirect.github.com/agronholm/anyio/issues/1">#1</a>...</li>
<li><a
href="https://github.com/agronholm/anyio/commit/44713f345cd29dd4e7d76553c134543a1296cc62"><code>44713f3</code></a>
Pin setup-uv to a commit sha across downstream jobs (<a
href="https://redirect.github.com/agronholm/anyio/issues/1213">#1213</a>)</li>
<li><a
href="https://github.com/agronholm/anyio/commit/f1b7301c8264b0d2e8d24a5788fd29e93dea4040"><code>f1b7301</code></a>
Fixed stderr writes in a worker subprocess causing a deadlock (<a
href="https://redirect.github.com/agronholm/anyio/issues/1207">#1207</a>)</li>
<li><a
href="https://github.com/agronholm/anyio/commit/212be93c2cf2c841e753e95e5e2c543ee7feca90"><code>212be93</code></a>
Fix flaky test_tcp_listener_same_port using a hardcoded port (<a
href="https://redirect.github.com/agronholm/anyio/issues/1206">#1206</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/agronholm/anyio/compare/4.12.1...4.14.2">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=anyio&package-manager=uv&previous-version=4.12.1&new-version=4.14.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/langchain-ai/langgraph/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-18 15:24:12 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c81c13533e chore(deps): bump soupsieve from 2.8.4 to 2.9 in /libs/langgraph (#8958)
Bumps [soupsieve](https://github.com/facelessuser/soupsieve) from 2.8.4
to 2.9.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/facelessuser/soupsieve/releases">soupsieve's
releases</a>.</em></p>
<blockquote>
<h2>2.9</h2>
<ul>
<li><strong>NEW</strong>: Drop Python 3.9 support.</li>
<li><strong>NEW</strong>: Lazy compile selector patterns to improve
initial import speed.</li>
<li><strong>FIX</strong>: Correct
<code>:nth-child</code>/<code>:nth-of-type</code> (and
<code>-last-</code> variants) for <code>An+B</code> values whose
sequence steps onto
index 0 or onto the last child (e.g. <code>:nth-child(2n-2)</code>,
<code>:nth-child(n-1)</code>, <code>:nth-child(n+5)</code>), which
previously
matched the wrong elements or nothing at all (<a
href="https://github.com/gaoflow"><code>@​gaoflow</code></a>).</li>
<li><strong>FIX</strong>: More efficient CSS ID matching (<a
href="https://github.com/kaimandalic"><code>@​kaimandalic</code></a>).</li>
<li><strong>FIX</strong>: Fix inefficient trimming of comments and white
space (<a
href="https://github.com/kaimandalic"><code>@​kaimandalic</code></a>).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/facelessuser/soupsieve/commit/8763f914472fc83652babda708bed5c8ef287004"><code>8763f91</code></a>
Format changelog message</li>
<li><a
href="https://github.com/facelessuser/soupsieve/commit/cf198fcddc9230f06ed39f974eba0ce076b85cda"><code>cf198fc</code></a>
Fix inefficient trimming of comments and white space</li>
<li><a
href="https://github.com/facelessuser/soupsieve/commit/ce44e4996e6632871c18cdd7a7fb641be8ef34ef"><code>ce44e49</code></a>
Merge commit from fork</li>
<li><a
href="https://github.com/facelessuser/soupsieve/commit/751c57b2c7e978e206b94b7dba17f8e2af392e19"><code>751c57b</code></a>
Fix :nth-child/:nth-of-type matching for An+B index boundaries (<a
href="https://redirect.github.com/facelessuser/soupsieve/issues/297">#297</a>)</li>
<li><a
href="https://github.com/facelessuser/soupsieve/commit/08e9ede4dcfafef860155319ef5eb9708e75d10b"><code>08e9ede</code></a>
Drop Python 3.9</li>
<li><a
href="https://github.com/facelessuser/soupsieve/commit/d6e68303a6c3e0e410530939b92955ba24a07a81"><code>d6e6830</code></a>
Rework selector mapping</li>
<li><a
href="https://github.com/facelessuser/soupsieve/commit/d2d1581fe275f89cb2e792589fed770aeb9e99b3"><code>d2d1581</code></a>
Utilize property for accessing lazy regular expression pattern</li>
<li><a
href="https://github.com/facelessuser/soupsieve/commit/b8701dec25c84a3910fd9a03222a3804fa119a1d"><code>b8701de</code></a>
Build patterns and regexes lazily in css_parser (<a
href="https://redirect.github.com/facelessuser/soupsieve/issues/296">#296</a>)</li>
<li>See full diff in <a
href="https://github.com/facelessuser/soupsieve/compare/2.8.4...2.9">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=soupsieve&package-manager=uv&previous-version=2.8.4&new-version=2.9)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/langchain-ai/langgraph/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-17 20:18:46 -07:00
Elior Nataf LackritzandGitHub 448a763779 feat(langgraph): add response_schema to interrupt() (#8886)
Adds an optional keyword-only `response_schema` to `interrupt()`,
carried on `Interrupt.response_schema` as JSON Schema so clients
(LangGraph Studio first) can render a typed form for the resume value
instead of a free-form JSON box. Default `None`, so nothing changes for
existing graphs.

- Accepts a JSON Schema dict (passed through, not validated) or a
Pydantic model / `TypedDict` / dataclass, converted with
`TypeAdapter.json_schema()`. For those, the resume value is validated
and the validated object is what `interrupt()` returns; an invalid value
raises from the node before it is committed, so the next resume works.
- Additive on the `stream_mode="debug"` payload: interrupt dicts gain
`response_schema` (it is a dataclass field, serialized with `asdict`).
- `libs/sdk-py`: mirrors the field on the `Interrupt` TypedDict as
`NotRequired`; the server omits the key when no schema was given.

JS counterpart: langchain-ai/langgraphjs#2824.

Verified: new tests parametrized across every checkpointer backend
(schema kinds, coercion, invalid resume) plus the full `libs/langgraph`
suite and downstream `prebuilt`, `cli`, `sdk-py`; round-tripped end to
end through the agent server with this branch installed.
2026-09-17 11:33:37 -04:00
Lauren Hirata SinghandGitHub 230927fb3a docs: publish the generated llms.txt instead of a hand-written copy (#8922)
## Why

`docs/llms.txt` is written by hand and was last touched in February, so
the index served at <https://langchain-ai.github.io/langgraph/llms.txt>
had drifted badly:

- 12 pages listed, against the 43 the docs site publishes.
- One entry, `/oss/python/langgraph/why-langgraph`, now 404s.

The deploy workflow only fires on `docs/**` pushes, so nothing brought
the file back in line as the docs site changed. It is served verbatim:
`generate_redirects.py` copies it into `docs/_site` via a `static_files`
list, and `deploy-redirects.yml` uploads that directory to Pages.

The docs site already generates exactly this index, and regenerates it
on every deploy:

```
https://docs.langchain.com/oss/python/langgraph/llms.txt
```

## What changed

`generate_redirects.py` now fetches that index at build time and writes
it into `_site`, so the published file cannot drift from the docs site.
The committed `docs/llms.txt` stays as a fallback only, refreshed here
to current content.

`deploy-redirects.yml` gains a weekly `schedule:` trigger, so docs
changes that never touch this repo still reach the deployed file. The
`permissions:` block is unchanged.

## Fetching remote content in CI

The fetched body is published on a public Pages site, so it is validated
before it is written:

- The URL is a hardcoded module constant, never built from input.
- Both that URL and the post-redirect `response.url` are checked against
an HTTPS-plus-single-host allowlist. `urlopen` follows redirects, so
checking only the request URL would not be enough.
- 30 second timeout, response capped at 1MB, decoded as UTF-8.
- The body must open with a markdown heading and contain a
docs.langchain.com link, which rejects an error page or a truncated
response.

Any failure returns `None` and falls back to the committed copy, so a
docs.langchain.com outage degrades to a stale file rather than a broken
deploy or a published error page.

## Verification

Ran `python docs/generate_redirects.py`: 294 redirect files, `llms.txt`
fetched, 43 entries.

Each rejection path was exercised against a stubbed `urlopen` and all
fall back to the committed file:

| Case | Result |
|---|---|
| Network error | falls back |
| Timeout | falls back |
| Redirect to another host | falls back |
| Body over 1MB | falls back |
| Invalid UTF-8 | falls back |
| HTML error page | falls back |
| Empty body | falls back |
| Valid index | published |

Allowlist rejects `http://docs.langchain.com/...`,
`https://evil.com/...`, and `https://docs.langchain.com.evil.com/...`.

## Note on ordering

The fallback committed here is labelled "LangGraph (Python)", which is
what the docs site will serve once langchain-ai/docs#6032 deploys. Until
then the fetched file reads "Open source (Python)". No action needed; it
self-corrects.

---

Written with Claude Code; I reviewed the diff and ran the verification
above.
2026-09-14 15:45:19 -04:00
John KennedyGitHublangsmith-fleet[bot] <langsmith-fleet[bot]@users.noreply.github.com>
e539ac122f chore(deps): fix vulnerable dev dependencies (#8449)
## Summary
Patch both `js-yaml` release lines in `libs/cli/js-examples` for
GHSA-2883-xcg3-v3hh: Jest's transitive copy to 3.15.2 and ESLint's to
4.3.2. Updates the existing fix rather than opening a duplicate; no
runtime dependencies added and no major-version overrides.

Addresses Dependabot alerts
[#398](https://github.com/langchain-ai/langgraph/security/dependabot/398)
and
[#397](https://github.com/langchain-ai/langgraph/security/dependabot/397).
These are real vulnerable versions in example development tooling; patch
rather than dismiss. Alerts remain open until this reaches `main` and
GitHub rescans.

## Verification
- [x] Yarn 1.22.22 regenerated the lockfile with lifecycle scripts
disabled; diff limited to the two js-yaml entries and scoped
resolutions.
- [x] `yarn install --frozen-lockfile --ignore-scripts --force
--non-interactive` in `libs/cli/js-examples`.
- [x] `yarn why js-yaml`: ESLint 4.3.2 and Jest/Istanbul 3.15.2.
- [x] Resolved versions checked against freshly retrieved GitHub
advisory patched versions for both alerts.
- [x] `yarn format:check` and `git diff --check`.
- [ ] Build fails in unchanged `tests/graph.int.test.ts:7`: `input` is
not a valid update property (also recorded in the earlier PR
verification).
- [ ] Unit-test script fails because it uses Jest's removed
`--testPathPattern` option; Jest requires `--testPathPatterns`.
- [ ] Lint fails because ESLint 10 requires `eslint.config.*`, which
this example lacks.

The build/test/lint configuration issues are outside this scoped
dependency patch and remain unresolved. No full test-pass claim.

---------

Co-authored-by: langsmith-fleet[bot] <langsmith-fleet[bot]@users.noreply.github.com>
2026-09-09 00:22:43 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>John Kennedy
ff4529380d chore(deps): bump httpx2 from 2.10.0 to 2.12.0 in /libs/cli (#8863)
Bumps [httpx2](https://github.com/pydantic/httpx2) from 2.10.0 to
2.12.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/pydantic/httpx2/releases">httpx2's
releases</a>.</em></p>
<blockquote>
<h2>v2.12.0</h2>
<h2>Highlights</h2>
<h3>🛡️ Bounded response decompression</h3>
<p><code>httpx2</code> now decodes <code>gzip</code>,
<code>deflate</code>, Brotli, and Zstandard responses incrementally.
Each decode step emits at most 1 MiB, so streaming a highly compressed
response no longer requires materializing an entire inflated network
chunk in memory (<a
href="https://redirect.github.com/pydantic/httpx2/pull/1126">pydantic/httpx2#1126</a>).</p>
<h3>📦 Shared Zstandard API</h3>
<p>Python 3.13 and earlier now use <code>backports.zstd</code>, which
provides the same bounded incremental decompression API as
<code>compression.zstd</code> on Python 3.14 and later (<a
href="https://redirect.github.com/pydantic/httpx2/pull/1146">pydantic/httpx2#1146</a>).</p>
<h2>httpx2</h2>
<h3>Changed</h3>
<ul>
<li>Use <code>backports.zstd</code> for Zstandard decoding on Python
3.13 and earlier by <a
href="https://github.com/Kludex"><code>@​Kludex</code></a> in <a
href="https://redirect.github.com/pydantic/httpx2/pull/1146">pydantic/httpx2#1146</a></li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Bound peak memory while streaming compressed responses and close
response streams when decoding fails by <a
href="https://github.com/Kludex"><code>@​Kludex</code></a> in <a
href="https://redirect.github.com/pydantic/httpx2/pull/1126">pydantic/httpx2#1126</a></li>
</ul>
<h2>httpcore2</h2>
<p>No changes since <code>2.11.0</code>. Version bumped to stay in
lockstep with <code>httpx2</code>.</p>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/pydantic/httpx2/compare/v2.11.0...v2.12.0">https://github.com/pydantic/httpx2/compare/v2.11.0...v2.12.0</a></p>
<h2>v2.11.0</h2>
<h2>Highlights</h2>
<h3>🌐 Public origin API</h3>
<p><code>httpx2</code> now includes an immutable and hashable
<code>Origin</code> value object, available through
<code>URL.origin</code>. It provides normalized scheme, host, and
effective port comparisons without including URL paths, queries,
fragments, or credentials (<a
href="https://redirect.github.com/pydantic/httpx2/pull/1134">pydantic/httpx2#1134</a>).</p>
<h3>🛠️ Request compatibility and validation</h3>
<ul>
<li>Explicit <code>Transfer-Encoding</code> headers now take precedence
over body-derived <code>Content-Length</code> headers (<a
href="https://redirect.github.com/pydantic/httpx2/pull/1137">pydantic/httpx2#1137</a>).</li>
<li>Deprecated status code aliases are available again (<a
href="https://redirect.github.com/pydantic/httpx2/pull/1135">pydantic/httpx2#1135</a>).</li>
<li>Multipart part headers are validated before serialization (<a
href="https://redirect.github.com/pydantic/httpx2/pull/1142">pydantic/httpx2#1142</a>).</li>
</ul>
<h2>httpx2</h2>
<h3>Added</h3>
<ul>
<li>Add the public <code>Origin</code> value object and
<code>URL.origin</code> property by <a
href="https://github.com/Kludex"><code>@​Kludex</code></a> in <a
href="https://redirect.github.com/pydantic/httpx2/pull/1134">pydantic/httpx2#1134</a></li>
</ul>
<h3>Changed</h3>
<ul>
<li>Require Brotli 1.2.0 or later for the <code>brotli</code> extra by
<a href="https://github.com/Kludex"><code>@​Kludex</code></a> in <a
href="https://redirect.github.com/pydantic/httpx2/pull/1141">pydantic/httpx2#1141</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/pydantic/httpx2/blob/main/src/httpx2/CHANGELOG.md">httpx2's
changelog</a>.</em></p>
<blockquote>
<h2>2.12.0 (August 18th, 2026)</h2>
<h3>Changed</h3>
<ul>
<li>Use <code>backports.zstd</code> for Zstandard decoding on Python
3.13 and earlier.
(<a
href="https://redirect.github.com/pydantic/httpx2/pull/1146">#1146</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Bound peak memory while streaming compressed responses and close
response streams when decoding fails.
(<a
href="https://redirect.github.com/pydantic/httpx2/pull/1126">#1126</a>)</li>
</ul>
<h2>2.11.0 (August 18th, 2026)</h2>
<h3>Added</h3>
<ul>
<li>Add the public <code>Origin</code> value object and
<code>URL.origin</code> property for normalized,
hashable origin comparisons. (<a
href="https://redirect.github.com/pydantic/httpx2/pull/1134">#1134</a>)</li>
</ul>
<h3>Changed</h3>
<ul>
<li>Require Brotli 1.2.0 or later for the <code>brotli</code> extra. (<a
href="https://redirect.github.com/pydantic/httpx2/pull/1141">#1141</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Restore deprecated status code aliases. (<a
href="https://redirect.github.com/pydantic/httpx2/pull/1135">#1135</a>)</li>
<li>Extract HTTP/2 release notes from changelog headings correctly. (<a
href="https://redirect.github.com/pydantic/httpx2/pull/1136">#1136</a>)</li>
<li>Respect explicit <code>Transfer-Encoding</code> headers and expose
buffered request body lengths to WSGI applications.
(<a
href="https://redirect.github.com/pydantic/httpx2/pull/1137">#1137</a>)</li>
<li>Validate multipart part header names and values before
serialization.
(<a
href="https://redirect.github.com/pydantic/httpx2/pull/1142">#1142</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/pydantic/httpx2/commit/71ae23be5448f859c2b4e21d9972ddfa7b8d759d"><code>71ae23b</code></a>
Version 2.12.0 (<a
href="https://redirect.github.com/pydantic/httpx2/issues/1147">#1147</a>)</li>
<li><a
href="https://github.com/pydantic/httpx2/commit/4fd0c70a3f207c618b145934792f791bccfb39f8"><code>4fd0c70</code></a>
Decode compressed response bodies incrementally (<a
href="https://redirect.github.com/pydantic/httpx2/issues/1126">#1126</a>)</li>
<li><a
href="https://github.com/pydantic/httpx2/commit/d588e528a11eb20323031ee571fadc668bb81b3f"><code>d588e52</code></a>
Use <code>backports.zstd</code> on Python 3.13 and earlier (<a
href="https://redirect.github.com/pydantic/httpx2/issues/1146">#1146</a>)</li>
<li><a
href="https://github.com/pydantic/httpx2/commit/344589da2a992f7e5a0c25c68cc78c25ec6d70bd"><code>344589d</code></a>
Version 2.11.0 (<a
href="https://redirect.github.com/pydantic/httpx2/issues/1143">#1143</a>)</li>
<li><a
href="https://github.com/pydantic/httpx2/commit/de96d810ee4e309d118982fe7084a46a2bcd600d"><code>de96d81</code></a>
Validate multipart part headers (<a
href="https://redirect.github.com/pydantic/httpx2/issues/1142">#1142</a>)</li>
<li><a
href="https://github.com/pydantic/httpx2/commit/51c32698ce029140cb69a26921d017e3edda80fa"><code>51c3269</code></a>
Require brotli 1.2.0 in the brotli extra (<a
href="https://redirect.github.com/pydantic/httpx2/issues/1141">#1141</a>)</li>
<li><a
href="https://github.com/pydantic/httpx2/commit/829b93a2393212996f613e635261f777d9ec6eab"><code>829b93a</code></a>
Respect explicit Transfer-Encoding headers (<a
href="https://redirect.github.com/pydantic/httpx2/issues/1137">#1137</a>)</li>
<li><a
href="https://github.com/pydantic/httpx2/commit/4fa6c8ee96fb79035c6820e4f44a10b6262d9c9f"><code>4fa6c8e</code></a>
Fix changelog extraction regex for H2 release headings (<a
href="https://redirect.github.com/pydantic/httpx2/issues/1136">#1136</a>)</li>
<li><a
href="https://github.com/pydantic/httpx2/commit/8a6f37063514ee2a2601607c20be72667fdfa3be"><code>8a6f370</code></a>
Restore deprecated status code aliases (<a
href="https://redirect.github.com/pydantic/httpx2/issues/1135">#1135</a>)</li>
<li><a
href="https://github.com/pydantic/httpx2/commit/d03f1ec6a1a323f951a8c21cd14f9c02f2d1e855"><code>d03f1ec</code></a>
Add public Origin API (<a
href="https://redirect.github.com/pydantic/httpx2/issues/1134">#1134</a>)</li>
<li>See full diff in <a
href="https://github.com/pydantic/httpx2/compare/v2.10.0...v2.12.0">compare
view</a></li>
</ul>
</details>
<br />

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Kennedy <65985482+jkennedyvz@users.noreply.github.com>
2026-09-09 06:32:13 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>John Kennedyopen-swe[bot] <open-swe@users.noreply.github.com>
c0b884e6d8 chore(deps): bump js-yaml from 4.3.1 to 4.3.2 in /libs/cli/js-monorepo-example (#8867)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.3.1 to 4.3.2.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/nodeca/js-yaml/blob/4.3.2/CHANGELOG.md">js-yaml's
changelog</a>.</em></p>
<blockquote>
<h2>4.3.2 - 2026-08-26</h2>
<h3>Changed</h3>
<ul>
<li>[backport] Hard-limit merge sequence size to 100.</li>
</ul>
<h3>Security</h3>
<ul>
<li>[backport] Count empty mappings in merge sequences toward
<code>maxTotalMergeKeys</code>
to limit CPU usage, <a
href="https://redirect.github.com/nodeca/js-yaml/issues/797">#797</a>.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/nodeca/js-yaml/commit/79ca68d90f333fbe6d9e42827527e62636200191"><code>79ca68d</code></a>
4.3.2 released</li>
<li><a
href="https://github.com/nodeca/js-yaml/commit/d90b6612a5a84385bdcb556c44578eac76dc0f6b"><code>d90b661</code></a>
Backport merge limits from v5.4.1</li>
<li>See full diff in <a
href="https://github.com/nodeca/js-yaml/compare/4.3.1...4.3.2">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=js-yaml&package-manager=npm_and_yarn&previous-version=4.3.1&new-version=4.3.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/langchain-ai/langgraph/network/alerts).

</details>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Kennedy <65985482+jkennedyvz@users.noreply.github.com>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-09-09 06:19:17 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
3299a1f563 chore(deps): bump httpcore2 from 2.5.0 to 2.10.0 in /libs/cli (#8864)
Bumps [httpcore2](https://github.com/pydantic/httpx2) from 2.5.0 to
2.10.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/pydantic/httpx2/releases">httpcore2's
releases</a>.</em></p>
<blockquote>
<h2>v2.10.0</h2>
<h2>Highlights</h2>
<h3>🚀 Performance and memory improvements</h3>
<ul>
<li>Sending large HTTP/2 request bodies no longer copies the body
quadratically - sending a 256 MiB body went from ~36s to under 0.1s (<a
href="https://redirect.github.com/pydantic/httpx2/pull/1127">pydantic/httpx2#1127</a>).</li>
<li>SSE parsing is up to 35x faster on highly fragmented streams (<a
href="https://redirect.github.com/pydantic/httpx2/pull/1117">pydantic/httpx2#1117</a>).</li>
<li>Cookie extraction is now skipped for responses without a
<code>Set-Cookie</code> header, making typical requests roughly 8%
faster (<a
href="https://redirect.github.com/pydantic/httpx2/pull/1107">pydantic/httpx2#1107</a>).</li>
</ul>
<h3>🕸️ WebAssembly / Emscripten support</h3>
<p><code>httpx2</code> now runs on Pyodide / Emscripten, using a
JavaScript <code>fetch</code>-based transport defined in
<code>httpx2-jsfetch</code> (<a
href="https://redirect.github.com/pydantic/httpx2/pull/1119">pydantic/httpx2#1119</a>,
<a
href="https://redirect.github.com/pydantic/httpx2/pull/1114">pydantic/httpx2#1114</a>).
Thanks <a
href="https://github.com/hoodmane"><code>@​hoodmane</code></a>!</p>
<h2>httpx2</h2>
<h3>Added</h3>
<ul>
<li>Add support for running on WebAssembly / Emscripten via Pyodide by
<a href="https://github.com/hoodmane"><code>@​hoodmane</code></a> in <a
href="https://redirect.github.com/pydantic/httpx2/pull/1119">pydantic/httpx2#1119</a></li>
<li>Add <code>max_event_size</code> to cap SSE event buffering by <a
href="https://github.com/Kludex"><code>@​Kludex</code></a> in <a
href="https://redirect.github.com/pydantic/httpx2/pull/1071">pydantic/httpx2#1071</a></li>
<li>Add RFC 9110 status code constants by <a
href="https://github.com/mbeijen"><code>@​mbeijen</code></a> in <a
href="https://redirect.github.com/pydantic/httpx2/pull/1069">pydantic/httpx2#1069</a></li>
<li>Add support for Python 3.15 by <a
href="https://github.com/hugovk"><code>@​hugovk</code></a> in <a
href="https://redirect.github.com/pydantic/httpx2/pull/1090">pydantic/httpx2#1090</a></li>
</ul>
<h3>Changed</h3>
<ul>
<li>Improve SSE chunk buffering performance by <a
href="https://github.com/Kludex"><code>@​Kludex</code></a> in <a
href="https://redirect.github.com/pydantic/httpx2/pull/1117">pydantic/httpx2#1117</a></li>
<li>Skip cookie extraction without <code>Set-Cookie</code> by <a
href="https://github.com/Kludex"><code>@​Kludex</code></a> in <a
href="https://redirect.github.com/pydantic/httpx2/pull/1107">pydantic/httpx2#1107</a></li>
<li>Return <code>str | None</code> instead of <code>Any</code> from
<code>Headers.get</code> by <a
href="https://github.com/ItsDrike"><code>@​ItsDrike</code></a> in <a
href="https://redirect.github.com/pydantic/httpx2/pull/1121">pydantic/httpx2#1121</a></li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Enforce WebSocket max message size across fragments by <a
href="https://github.com/Kludex"><code>@​Kludex</code></a> in <a
href="https://redirect.github.com/pydantic/httpx2/pull/1085">pydantic/httpx2#1085</a></li>
<li>Ignore unsolicited and duplicate Pong frames by <a
href="https://github.com/Kludex"><code>@​Kludex</code></a> in <a
href="https://redirect.github.com/pydantic/httpx2/pull/1122">pydantic/httpx2#1122</a></li>
</ul>
<h2>httpcore2</h2>
<h3>Added</h3>
<ul>
<li>Add support for Python 3.15 by <a
href="https://github.com/hugovk"><code>@​hugovk</code></a> in <a
href="https://redirect.github.com/pydantic/httpx2/pull/1090">pydantic/httpx2#1090</a></li>
</ul>
<h3>Changed</h3>
<ul>
<li>Avoid quadratic copying when sending large HTTP/2 request bodies by
<a href="https://github.com/Kludex"><code>@​Kludex</code></a> in <a
href="https://redirect.github.com/pydantic/httpx2/pull/1127">pydantic/httpx2#1127</a></li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Propagate the original exception instead of raising
<code>KeyError</code> when an HTTP/2 stream fails by <a
href="https://github.com/yhay81"><code>@​yhay81</code></a> in <a
href="https://redirect.github.com/pydantic/httpx2/pull/1093">pydantic/httpx2#1093</a></li>
<li>Start TLS for the <code>wss</code> scheme in SOCKS5 proxy
connections by <a
href="https://github.com/Kludex"><code>@​Kludex</code></a> in <a
href="https://redirect.github.com/pydantic/httpx2/pull/1104">pydantic/httpx2#1104</a></li>
</ul>
<h2>🙏 New Contributors</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/pydantic/httpx2/commit/a966320e75477b8c55ee78fdb8f57ead6a574cb0"><code>a966320</code></a>
Version 2.10.0 (<a
href="https://redirect.github.com/pydantic/httpx2/issues/1129">#1129</a>)</li>
<li><a
href="https://github.com/pydantic/httpx2/commit/586f968f5510aa4d3b7edd1f1b039684c42945ee"><code>586f968</code></a>
Avoid quadratic copying when sending large HTTP/2 request bodies (<a
href="https://redirect.github.com/pydantic/httpx2/issues/1127">#1127</a>)</li>
<li><a
href="https://github.com/pydantic/httpx2/commit/dee1d1ace3021404d0aa255957821eda97c94c43"><code>dee1d1a</code></a>
Return <code>str | None</code> instead of <code>Any</code> from
<code>Headers.get</code> (<a
href="https://redirect.github.com/pydantic/httpx2/issues/1121">#1121</a>)</li>
<li><a
href="https://github.com/pydantic/httpx2/commit/dec24ad57d1d337de7de23c011eb566a145e7b40"><code>dec24ad</code></a>
Use <code>httpx2-jsfetch</code> on Emscripten (<a
href="https://redirect.github.com/pydantic/httpx2/issues/1119">#1119</a>)</li>
<li><a
href="https://github.com/pydantic/httpx2/commit/454b8b2197f62d699ff3ad762917643926b4da77"><code>454b8b2</code></a>
Ignore unsolicited and duplicate Pong frames (<a
href="https://redirect.github.com/pydantic/httpx2/issues/1122">#1122</a>)</li>
<li><a
href="https://github.com/pydantic/httpx2/commit/cbfc0e04ef6507da29ccbb3b9c2e5b23dd693414"><code>cbfc0e0</code></a>
Improve SSE chunk buffering performance (<a
href="https://redirect.github.com/pydantic/httpx2/issues/1117">#1117</a>)</li>
<li><a
href="https://github.com/pydantic/httpx2/commit/ad141d876c7d3c3f58555cbb0a178ec2c1f15b51"><code>ad141d8</code></a>
Refactor SSE parser coordination (<a
href="https://redirect.github.com/pydantic/httpx2/issues/1118">#1118</a>)</li>
<li><a
href="https://github.com/pydantic/httpx2/commit/e0b01245e42d2091034ee9455cbb068bc0d6c0c6"><code>e0b0124</code></a>
Make <code>_client</code> depend on the <code>_transports</code> package
instead of its submodules ...</li>
<li><a
href="https://github.com/pydantic/httpx2/commit/e52f963c62f397f225c8d29573aedbe5ae613906"><code>e52f963</code></a>
Skip dependencies not needed on Emscripten (<a
href="https://redirect.github.com/pydantic/httpx2/issues/1114">#1114</a>)</li>
<li><a
href="https://github.com/pydantic/httpx2/commit/5d9eedc98186c612d0e426df417f78f6ec21e9ca"><code>5d9eedc</code></a>
Add <code>max_event_size</code> to cap SSE event buffering (<a
href="https://redirect.github.com/pydantic/httpx2/issues/1071">#1071</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/pydantic/httpx2/compare/v2.5.0...v2.10.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=httpcore2&package-manager=uv&previous-version=2.5.0&new-version=2.10.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/langchain-ai/langgraph/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-08 21:26:36 -07:00
38 changed files with 4212 additions and 1048 deletions
+1 -1
View File
@@ -41,7 +41,7 @@ def test(config: pathlib.Path, port: int, tag: str, verbose: bool):
)
# Compose up with wait (implies detach), similar to `langgraph up --wait`
args_up = [*args, "up", "--remove-orphans", "--wait"]
args_up = [*args, "up", "--remove-orphans", "--wait", "--wait-timeout", "180"]
compose_cmd = ["docker", "compose"]
if capabilities.compose_type == "standalone":
+7 -7
View File
@@ -71,7 +71,7 @@ jobs:
if [ -f ../.env ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> ../.env; fi
# Run the integration test using the built tag
REPO_ROOT=$(git rev-parse --show-toplevel)
timeout 60 python "$REPO_ROOT/.github/scripts/run_langgraph_cli_test.py" -t ${{ matrix.example.tag }}
timeout --kill-after=30 300 python "$REPO_ROOT/.github/scripts/run_langgraph_cli_test.py" -t ${{ matrix.example.tag }}
- name: Build JS service
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') &&matrix.example.name == 'A' }}
@@ -98,7 +98,7 @@ jobs:
run: |
cp apps/agent/.env.example apps/agent/.env
echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> apps/agent/.env
timeout 60 python ../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-g -c apps/agent/langgraph.json
timeout --kill-after=30 300 python ../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-g -c apps/agent/langgraph.json
- name: Build prerelease reqs service
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') &&matrix.example.name == 'A' }}
@@ -113,11 +113,11 @@ jobs:
run: |
cp ../.env.example .env
echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env
timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-h
timeout --kill-after=30 300 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-h
echo "Finished starting up langgraph-test-h"
LANGGRAPH_VERSION=$(docker run --rm --entrypoint "" langgraph-test-h python -c "import sys; from importlib.metadata import version; v = version('langgraph'); print(v);")
if [ "$LANGGRAPH_VERSION" != "1.1.5" ]; then
echo "LANGGRAPH_VERSION != 1.1.5; $LANGGRAPH_VERSION"
if [ "$LANGGRAPH_VERSION" != "1.2.6" ]; then
echo "LANGGRAPH_VERSION != 1.2.6; $LANGGRAPH_VERSION"
exit 1
fi
LANGCHAIN_OPENAI_VERSION=$(docker run --rm --entrypoint "" langgraph-test-h python -c "import sys; from importlib.metadata import version; v = version('langchain-openai'); print(v);")
@@ -150,7 +150,7 @@ jobs:
run: |
cp .env.example .env
echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env
timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-uv-simple
timeout --kill-after=30 300 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-uv-simple
- name: Build uv monorepo service
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') &&matrix.example.name == 'A' }}
@@ -165,4 +165,4 @@ jobs:
run: |
cp .env.example .env
echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env
timeout 60 python ../../../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-uv-monorepo
timeout --kill-after=30 300 python ../../../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-uv-monorepo
+4
View File
@@ -7,6 +7,10 @@ on:
paths:
- 'docs/**'
- '.github/workflows/deploy-redirects.yml'
# llms.txt is fetched from docs.langchain.com at build time, so redeploy on a
# schedule to pick up docs changes that never touch this repo.
schedule:
- cron: '17 6 * * 1'
workflow_dispatch:
permissions:
+83 -7
View File
@@ -12,13 +12,26 @@ which is SEO-friendly and treated similarly to 301 redirects by Google.
To add new redirects, simply edit redirects.json and re-run this script.
"""
import http.client
import json
import os
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
# Default fallback URL for any path not in the redirect map
DEFAULT_REDIRECT = "https://docs.langchain.com/oss/python/langgraph/overview"
# The docs site regenerates this index on every deploy, so fetching it here
# keeps the published llms.txt from drifting. The URL is a hardcoded constant,
# never built from input, and both it and the post-redirect URL are checked
# against ALLOWED_LLMS_HOST before anything is read.
CANONICAL_LLMS_URL = "https://docs.langchain.com/oss/python/langgraph/llms.txt"
ALLOWED_LLMS_HOST = "docs.langchain.com"
LLMS_FETCH_TIMEOUT = 30
LLMS_MAX_BYTES = 1_000_000
HTML_TEMPLATE = """<!doctype html>
<html lang="en">
<head>
@@ -75,6 +88,64 @@ CATCHALL_404_TEMPLATE = """<!doctype html>
"""
def is_allowed_llms_url(url):
"""Return True if url is HTTPS on the one host we accept content from."""
parsed = urllib.parse.urlsplit(url)
return parsed.scheme == "https" and parsed.hostname == ALLOWED_LLMS_HOST
def fetch_canonical_llms_txt():
"""Return the published LangGraph index, or None if it cannot be used.
Returning None leaves the caller on the committed docs/llms.txt, so a
docs.langchain.com outage degrades to a stale file rather than a broken
deploy or a published error page.
"""
if not is_allowed_llms_url(CANONICAL_LLMS_URL):
print(f"Refusing to fetch {CANONICAL_LLMS_URL}: host not allowed")
return None
try:
with urllib.request.urlopen( # noqa: S310 - constant, allowlisted URL
CANONICAL_LLMS_URL, timeout=LLMS_FETCH_TIMEOUT
) as response:
# urlopen follows redirects, so re-check where it actually landed.
if not is_allowed_llms_url(response.url):
print(f"Refusing {CANONICAL_LLMS_URL}: redirected to {response.url}")
return None
body = response.read(LLMS_MAX_BYTES + 1)
# A connection dropped mid-body raises http.client.IncompleteRead, which
# descends from HTTPException rather than OSError, so catching only the
# urllib and OS errors would let it escape and fail the whole deploy.
except (
urllib.error.URLError,
http.client.HTTPException,
TimeoutError,
OSError,
) as exc:
print(f"Could not fetch {CANONICAL_LLMS_URL}: {type(exc).__name__}: {exc}")
return None
if len(body) > LLMS_MAX_BYTES:
print(f"Refusing {CANONICAL_LLMS_URL}: larger than {LLMS_MAX_BYTES} bytes")
return None
try:
text = body.decode("utf-8")
except UnicodeDecodeError as exc:
print(f"Refusing {CANONICAL_LLMS_URL}: not valid UTF-8: {exc}")
return None
# An index opens with a markdown heading and links to the docs site. A
# body that does not is an error page or a truncated response, not content
# worth publishing.
if not text.startswith("# ") or f"https://{ALLOWED_LLMS_HOST}/" not in text:
print(f"Refusing {CANONICAL_LLMS_URL}: does not look like an llms.txt index")
return None
return text
def generate_redirects():
script_dir = Path(__file__).parent
output_dir = script_dir / "_site"
@@ -126,14 +197,19 @@ def generate_redirects():
catchall_404.write_text(CATCHALL_404_TEMPLATE.format(default_url=DEFAULT_REDIRECT))
print(f"Created: {catchall_404}")
# Copy static files (like llms.txt) that can't be redirected via HTML
static_files = ["llms.txt"]
for static_file in static_files:
src = script_dir / static_file
# llms.txt can't be redirected via HTML, so publish the docs site's own
# generated index. The committed copy is only a fallback.
llms_txt = fetch_canonical_llms_txt()
if llms_txt is not None:
(output_dir / "llms.txt").write_text(llms_txt)
print(f"Fetched: {output_dir / 'llms.txt'} (from {CANONICAL_LLMS_URL})")
else:
src = script_dir / "llms.txt"
if src.exists():
dst = output_dir / static_file
dst.write_text(src.read_text())
print(f"Copied: {dst}")
(output_dir / "llms.txt").write_text(src.read_text())
print(f"Copied: {output_dir / 'llms.txt'} (fallback, may be stale)")
else:
print("No llms.txt fetched and no committed fallback; skipping")
print(f"\nGenerated {len(redirects)} redirect files in {output_dir}")
+46 -32
View File
@@ -1,35 +1,49 @@
# LangGraph
# Docs by LangChain: LangGraph (Python)
LangGraph documentation has moved to docs.langchain.com.
> Markdown index of the LangGraph (Python) documentation.
## Overview
## LangGraph (Python)
- [LangGraph Overview](https://docs.langchain.com/oss/python/langgraph/overview): Introduction to LangGraph, a library for building stateful, multi-actor applications with LLMs.
- [Why LangGraph?](https://docs.langchain.com/oss/python/langgraph/why-langgraph): Motivation for LangGraph and its key features.
## Core Concepts
- [Graph API](https://docs.langchain.com/oss/python/langgraph/graph-api): Learn how to define state, create nodes, and connect them with edges.
- [Streaming](https://docs.langchain.com/oss/python/langgraph/streaming): Stream outputs from your graph for better UX.
- [Persistence](https://docs.langchain.com/oss/python/langgraph/persistence): Add memory and checkpointing to your graphs.
- [Add Memory](https://docs.langchain.com/oss/python/langgraph/add-memory): Implement short-term and long-term memory.
- [Workflows & Agents](https://docs.langchain.com/oss/python/langgraph/workflows-agents): Build agents and workflows with LangGraph.
## How-To Guides
- [Use Subgraphs](https://docs.langchain.com/oss/python/langgraph/use-subgraphs): Compose graphs using subgraphs.
- [Observability](https://docs.langchain.com/oss/python/langgraph/observability): Add tracing and debugging to your graphs.
- [Common Errors](https://docs.langchain.com/oss/python/langgraph/common-errors): Troubleshoot common LangGraph errors.
## Tutorials
- [Agentic RAG](https://docs.langchain.com/oss/python/langgraph/agentic-rag): Build an agentic RAG system with LangGraph.
- [SQL Agent](https://docs.langchain.com/oss/python/langgraph/sql-agent): Create a SQL agent with LangGraph.
## Reference
- [API Reference](https://reference.langchain.com/python/langgraph/): Complete API documentation for LangGraph.
## LangGraph Platform
For deploying LangGraph applications in production, see the [LangSmith documentation](https://docs.langchain.com/langsmith/agent-server).
- [Memory](https://docs.langchain.com/oss/python/langgraph/add-memory.md)
- [Build a custom RAG agent with LangGraph](https://docs.langchain.com/oss/python/langgraph/agentic-rag.md)
- [Application structure](https://docs.langchain.com/oss/python/langgraph/application-structure.md)
- [Backward compatibility](https://docs.langchain.com/oss/python/langgraph/backward-compatibility.md)
- [Case studies](https://docs.langchain.com/oss/python/langgraph/case-studies.md)
- [Changelog](https://docs.langchain.com/oss/python/langgraph/changelog-js.md)
- [Changelog](https://docs.langchain.com/oss/python/langgraph/changelog-py.md)
- [Checkpointers](https://docs.langchain.com/oss/python/langgraph/checkpointers.md)
- [Choosing between Graph and Functional APIs](https://docs.langchain.com/oss/python/langgraph/choosing-apis.md)
- [Deployment](https://docs.langchain.com/oss/python/langgraph/deploy.md)
- [GRAPH_RECURSION_LIMIT](https://docs.langchain.com/oss/python/langgraph/errors/GRAPH_RECURSION_LIMIT.md)
- [INVALID_CHAT_HISTORY](https://docs.langchain.com/oss/python/langgraph/errors/INVALID_CHAT_HISTORY.md)
- [INVALID_CONCURRENT_GRAPH_UPDATE](https://docs.langchain.com/oss/python/langgraph/errors/INVALID_CONCURRENT_GRAPH_UPDATE.md)
- [INVALID_GRAPH_NODE_RETURN_VALUE](https://docs.langchain.com/oss/python/langgraph/errors/INVALID_GRAPH_NODE_RETURN_VALUE.md)
- [MISSING_CHECKPOINTER](https://docs.langchain.com/oss/python/langgraph/errors/MISSING_CHECKPOINTER.md)
- [MULTIPLE_SUBGRAPHS](https://docs.langchain.com/oss/python/langgraph/errors/MULTIPLE_SUBGRAPHS.md)
- [Event streaming](https://docs.langchain.com/oss/python/langgraph/event-streaming.md)
- [Fault tolerance](https://docs.langchain.com/oss/python/langgraph/fault-tolerance.md)
- [Custom stream channels](https://docs.langchain.com/oss/python/langgraph/frontend/custom-stream-channels.md)
- [Graph execution](https://docs.langchain.com/oss/python/langgraph/frontend/graph-execution.md)
- [Overview](https://docs.langchain.com/oss/python/langgraph/frontend/overview.md)
- [Functional API overview](https://docs.langchain.com/oss/python/langgraph/functional-api.md)
- [Graph API overview](https://docs.langchain.com/oss/python/langgraph/graph-api.md)
- [Install LangGraph](https://docs.langchain.com/oss/python/langgraph/install.md)
- [Interrupts](https://docs.langchain.com/oss/python/langgraph/interrupts.md)
- [Run a local server](https://docs.langchain.com/oss/python/langgraph/local-server.md)
- [LangSmith Observability](https://docs.langchain.com/oss/python/langgraph/observability.md)
- [LangGraph overview](https://docs.langchain.com/oss/python/langgraph/overview.md)
- [Persistence](https://docs.langchain.com/oss/python/langgraph/persistence.md)
- [LangGraph runtime](https://docs.langchain.com/oss/python/langgraph/pregel.md)
- [Quickstart](https://docs.langchain.com/oss/python/langgraph/quickstart.md)
- [Build a custom SQL agent](https://docs.langchain.com/oss/python/langgraph/sql-agent.md)
- [Stores](https://docs.langchain.com/oss/python/langgraph/stores.md)
- [Streaming](https://docs.langchain.com/oss/python/langgraph/streaming.md)
- [LangSmith Studio](https://docs.langchain.com/oss/python/langgraph/studio.md)
- [Test](https://docs.langchain.com/oss/python/langgraph/test.md)
- [Thinking in LangGraph](https://docs.langchain.com/oss/python/langgraph/thinking-in-langgraph.md)
- [Agent Chat UI](https://docs.langchain.com/oss/python/langgraph/ui.md)
- [Use the functional API](https://docs.langchain.com/oss/python/langgraph/use-functional-api.md)
- [Use the graph API](https://docs.langchain.com/oss/python/langgraph/use-graph-api.md)
- [Subgraphs](https://docs.langchain.com/oss/python/langgraph/use-subgraphs.md)
- [Use time-travel](https://docs.langchain.com/oss/python/langgraph/use-time-travel.md)
- [Workflows and agents](https://docs.langchain.com/oss/python/langgraph/workflows-agents.md)
+3 -3
View File
@@ -13,16 +13,16 @@ wheels = [
[[package]]
name = "anyio"
version = "4.13.0"
version = "4.14.2"
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/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" }
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" }
wheels = [
{ 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" },
{ 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" },
]
[[package]]
+3 -3
View File
@@ -26,16 +26,16 @@ wheels = [
[[package]]
name = "anyio"
version = "4.12.1"
version = "4.14.2"
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/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" }
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" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
{ 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" },
]
[[package]]
+3 -3
View File
@@ -19,16 +19,16 @@ wheels = [
[[package]]
name = "anyio"
version = "4.12.1"
version = "4.14.2"
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/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" }
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" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
{ 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" },
]
[[package]]
@@ -5,5 +5,5 @@ description = "Test for prerelease stuff"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"langgraph==1.1.5"
"langgraph==1.2.6"
]
@@ -7,7 +7,7 @@ requires-python = ">=3.10"
dependencies = [
"langchain-openai==1.1.14",
"langchain-anthropic==1.4.6",
"langgraph==1.1.5"
"langgraph==1.2.6"
]
[tool.uv]
-285
View File
@@ -1,285 +0,0 @@
# This file is automatically @generated by Poetry 2.0.0 and should not be changed by hand.
[[package]]
name = "anyio"
version = "4.4.0"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"},
{file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"},
]
[package.dependencies]
exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""}
idna = ">=2.8"
sniffio = ">=1.1"
typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""}
[package.extras]
doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
trio = ["trio (>=0.23)"]
[[package]]
name = "certifi"
version = "2024.7.4"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"},
{file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"},
]
[[package]]
name = "click"
version = "8.1.7"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
groups = ["main"]
markers = "platform_system == \"Windows\""
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "exceptiongroup"
version = "1.2.1"
description = "Backport of PEP 654 (exception groups)"
optional = false
python-versions = ">=3.7"
groups = ["main"]
markers = "python_version < \"3.11\""
files = [
{file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"},
{file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"},
]
[package.extras]
test = ["pytest (>=6)"]
[[package]]
name = "h11"
version = "0.16.0"
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"},
{file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"},
]
[[package]]
name = "httpcore"
version = "1.0.9"
description = "A minimal low-level HTTP client."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"},
{file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"},
]
[package.dependencies]
certifi = "*"
h11 = ">=0.16"
[package.extras]
asyncio = ["anyio (>=4.0,<5.0)"]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"]
trio = ["trio (>=0.22.0,<1.0)"]
[[package]]
name = "httpx"
version = "0.28.1"
description = "The next generation HTTP client."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"},
{file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"},
]
[package.dependencies]
anyio = "*"
certifi = "*"
httpcore = "==1.*"
idna = "*"
[package.extras]
brotli = ["brotli", "brotlicffi"]
cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"]
zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "httpx-sse"
version = "0.4.0"
description = "Consume Server-Sent Event (SSE) messages with HTTPX."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721"},
{file = "httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f"},
]
[[package]]
name = "idna"
version = "3.7"
description = "Internationalized Domain Names in Applications (IDNA)"
optional = false
python-versions = ">=3.5"
groups = ["main"]
files = [
{file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
{file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
]
[[package]]
name = "langgraph-cli"
version = "0.1.52"
description = "CLI for interacting with LangGraph API"
optional = false
python-versions = "^3.9.0,<4.0"
groups = ["main"]
files = []
develop = true
[package.dependencies]
click = "^8.1.7"
[package.source]
type = "directory"
url = ".."
[[package]]
name = "langgraph-sdk"
version = "0.1.29"
description = "SDK for interacting with LangGraph API"
optional = false
python-versions = "^3.9.0,<4.0"
groups = ["main"]
files = []
develop = true
[package.dependencies]
httpx = ">=0.25.2"
httpx-sse = ">=0.4.0"
orjson = ">=3.10.1"
[package.source]
type = "directory"
url = "../../sdk-py"
[[package]]
name = "orjson"
version = "3.10.5"
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "orjson-3.10.5-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:545d493c1f560d5ccfc134803ceb8955a14c3fcb47bbb4b2fee0232646d0b932"},
{file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4324929c2dd917598212bfd554757feca3e5e0fa60da08be11b4aa8b90013c1"},
{file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c13ca5e2ddded0ce6a927ea5a9f27cae77eee4c75547b4297252cb20c4d30e6"},
{file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6c8e30adfa52c025f042a87f450a6b9ea29649d828e0fec4858ed5e6caecf63"},
{file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:338fd4f071b242f26e9ca802f443edc588fa4ab60bfa81f38beaedf42eda226c"},
{file = "orjson-3.10.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6970ed7a3126cfed873c5d21ece1cd5d6f83ca6c9afb71bbae21a0b034588d96"},
{file = "orjson-3.10.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:235dadefb793ad12f7fa11e98a480db1f7c6469ff9e3da5e73c7809c700d746b"},
{file = "orjson-3.10.5-cp310-none-win32.whl", hash = "sha256:be79e2393679eda6a590638abda16d167754393f5d0850dcbca2d0c3735cebe2"},
{file = "orjson-3.10.5-cp310-none-win_amd64.whl", hash = "sha256:c4a65310ccb5c9910c47b078ba78e2787cb3878cdded1702ac3d0da71ddc5228"},
{file = "orjson-3.10.5-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:cdf7365063e80899ae3a697def1277c17a7df7ccfc979990a403dfe77bb54d40"},
{file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b68742c469745d0e6ca5724506858f75e2f1e5b59a4315861f9e2b1df77775a"},
{file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7d10cc1b594951522e35a3463da19e899abe6ca95f3c84c69e9e901e0bd93d38"},
{file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcbe82b35d1ac43b0d84072408330fd3295c2896973112d495e7234f7e3da2e1"},
{file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c0eb7e0c75e1e486c7563fe231b40fdd658a035ae125c6ba651ca3b07936f5"},
{file = "orjson-3.10.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:53ed1c879b10de56f35daf06dbc4a0d9a5db98f6ee853c2dbd3ee9d13e6f302f"},
{file = "orjson-3.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:099e81a5975237fda3100f918839af95f42f981447ba8f47adb7b6a3cdb078fa"},
{file = "orjson-3.10.5-cp311-none-win32.whl", hash = "sha256:1146bf85ea37ac421594107195db8bc77104f74bc83e8ee21a2e58596bfb2f04"},
{file = "orjson-3.10.5-cp311-none-win_amd64.whl", hash = "sha256:36a10f43c5f3a55c2f680efe07aa93ef4a342d2960dd2b1b7ea2dd764fe4a37c"},
{file = "orjson-3.10.5-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:68f85ecae7af14a585a563ac741b0547a3f291de81cd1e20903e79f25170458f"},
{file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28afa96f496474ce60d3340fe8d9a263aa93ea01201cd2bad844c45cd21f5268"},
{file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cd684927af3e11b6e754df80b9ffafd9fb6adcaa9d3e8fdd5891be5a5cad51e"},
{file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d21b9983da032505f7050795e98b5d9eee0df903258951566ecc358f6696969"},
{file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ad1de7fef79736dde8c3554e75361ec351158a906d747bd901a52a5c9c8d24b"},
{file = "orjson-3.10.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d97531cdfe9bdd76d492e69800afd97e5930cb0da6a825646667b2c6c6c0211"},
{file = "orjson-3.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d69858c32f09c3e1ce44b617b3ebba1aba030e777000ebdf72b0d8e365d0b2b3"},
{file = "orjson-3.10.5-cp312-none-win32.whl", hash = "sha256:64c9cc089f127e5875901ac05e5c25aa13cfa5dbbbd9602bda51e5c611d6e3e2"},
{file = "orjson-3.10.5-cp312-none-win_amd64.whl", hash = "sha256:b2efbd67feff8c1f7728937c0d7f6ca8c25ec81373dc8db4ef394c1d93d13dc5"},
{file = "orjson-3.10.5-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:03b565c3b93f5d6e001db48b747d31ea3819b89abf041ee10ac6988886d18e01"},
{file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:584c902ec19ab7928fd5add1783c909094cc53f31ac7acfada817b0847975f26"},
{file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a35455cc0b0b3a1eaf67224035f5388591ec72b9b6136d66b49a553ce9eb1e6"},
{file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1670fe88b116c2745a3a30b0f099b699a02bb3482c2591514baf5433819e4f4d"},
{file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:185c394ef45b18b9a7d8e8f333606e2e8194a50c6e3c664215aae8cf42c5385e"},
{file = "orjson-3.10.5-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ca0b3a94ac8d3886c9581b9f9de3ce858263865fdaa383fbc31c310b9eac07c9"},
{file = "orjson-3.10.5-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dfc91d4720d48e2a709e9c368d5125b4b5899dced34b5400c3837dadc7d6271b"},
{file = "orjson-3.10.5-cp38-none-win32.whl", hash = "sha256:c05f16701ab2a4ca146d0bca950af254cb7c02f3c01fca8efbbad82d23b3d9d4"},
{file = "orjson-3.10.5-cp38-none-win_amd64.whl", hash = "sha256:8a11d459338f96a9aa7f232ba95679fc0c7cedbd1b990d736467894210205c09"},
{file = "orjson-3.10.5-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:85c89131d7b3218db1b24c4abecea92fd6c7f9fab87441cfc342d3acc725d807"},
{file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb66215277a230c456f9038d5e2d84778141643207f85336ef8d2a9da26bd7ca"},
{file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:51bbcdea96cdefa4a9b4461e690c75ad4e33796530d182bdd5c38980202c134a"},
{file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbead71dbe65f959b7bd8cf91e0e11d5338033eba34c114f69078d59827ee139"},
{file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5df58d206e78c40da118a8c14fc189207fffdcb1f21b3b4c9c0c18e839b5a214"},
{file = "orjson-3.10.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c4057c3b511bb8aef605616bd3f1f002a697c7e4da6adf095ca5b84c0fd43595"},
{file = "orjson-3.10.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b39e006b00c57125ab974362e740c14a0c6a66ff695bff44615dcf4a70ce2b86"},
{file = "orjson-3.10.5-cp39-none-win32.whl", hash = "sha256:eded5138cc565a9d618e111c6d5c2547bbdd951114eb822f7f6309e04db0fb47"},
{file = "orjson-3.10.5-cp39-none-win_amd64.whl", hash = "sha256:cc28e90a7cae7fcba2493953cff61da5a52950e78dc2dacfe931a317ee3d8de7"},
{file = "orjson-3.10.5.tar.gz", hash = "sha256:7a5baef8a4284405d96c90c7c62b755e9ef1ada84c2406c24a9ebec86b89f46d"},
]
[[package]]
name = "sniffio"
version = "1.3.1"
description = "Sniff out which async library your code is running under"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
]
[[package]]
name = "typing-extensions"
version = "4.12.2"
description = "Backported and Experimental Type Hints for Python 3.8+"
optional = false
python-versions = ">=3.8"
groups = ["main"]
markers = "python_version < \"3.11\""
files = [
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
]
[metadata]
lock-version = "2.1"
python-versions = "^3.9.0,<4.0"
content-hash = "ec5109729f30d2033a10a10e8f8d3ed94c7d96d5d31025b4815b0123664bb063"
+3 -1
View File
@@ -25,7 +25,9 @@
"@langchain/langgraph": "^1.4.13"
},
"resolutions": {
"@langchain/langgraph-checkpoint": "1.0.4"
"@langchain/langgraph-checkpoint": "1.0.4",
"jest/**/js-yaml": "3.15.2",
"@eslint/eslintrc/js-yaml": "4.3.2"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.6",
+8 -8
View File
@@ -3744,18 +3744,18 @@ js-tokens@^4.0.0:
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
js-yaml@^3.13.1:
version "3.14.2"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.2.tgz#77485ce1dd7f33c061fd1b16ecea23b55fcb04b0"
integrity sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==
js-yaml@3.15.2, js-yaml@^3.13.1:
version "3.15.2"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.15.2.tgz#3f83823ac6be17f570f23b2ecdef3777ff5ea364"
integrity sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==
dependencies:
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.3.2, js-yaml@^4.3.0:
version "4.3.2"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.3.2.tgz#8e44fb14a2643c59726bb15787b5f1512cb3d3fb"
integrity sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==
dependencies:
argparse "^2.0.1"
+3 -3
View File
@@ -1391,9 +1391,9 @@ js-tiktoken@^1.0.12:
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==
version "4.3.2"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.3.2.tgz#8e44fb14a2643c59726bb15787b5f1512cb3d3fb"
integrity sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==
dependencies:
argparse "^2.0.1"
File diff suppressed because it is too large Load Diff
+8 -2
View File
@@ -1,12 +1,18 @@
import asyncio
import signal
import sys
from collections.abc import Callable
from collections.abc import Callable, Coroutine
from contextlib import contextmanager
from typing import cast
from typing import Any, Protocol, TypeVar, cast
import click.exceptions
_T = TypeVar("_T")
class CommandRunner(Protocol):
def run(self, coro: Coroutine[Any, Any, _T]) -> _T: ...
@contextmanager
def Runner():
+180 -32
View File
@@ -2,18 +2,125 @@
from __future__ import annotations
from typing import Any
from dataclasses import dataclass
from typing import Any, Literal
from urllib.parse import urlparse
import click
import httpx
CLOUD_CONTROL_PLANE_URL = "https://api.host.langchain.com"
CLOUD_DASHBOARD_URL = "https://smith.langchain.com"
CLOUD_DOMAIN = "langchain.com"
CLOUD_API_HOST = "api.smith.langchain.com"
CLOUD_CONTROL_PLANE_HOST = "api.host.langchain.com"
CLOUD_DASHBOARD_HOST = "smith.langchain.com"
CONTROL_PLANE_PATH = "/api-host"
LANGSMITH_API_PATHS = ("/api/v1", "/api")
LOCAL_HOSTNAMES = ("localhost", "127.0.0.1")
MAX_PAGE_SIZE = 100
SourceName = Literal["internal_docker", "internal_source", "external_docker"]
@dataclass(frozen=True, slots=True)
class ControlPlaneEndpoints:
control_plane_url: str
dashboard_url: str
@classmethod
def resolve(
cls, host_url: str | None, langsmith_endpoint: str | None
) -> ControlPlaneEndpoints:
if host_url:
return cls.from_control_plane_url(host_url)
if langsmith_endpoint:
return cls.from_langsmith_endpoint(langsmith_endpoint)
return cls(CLOUD_CONTROL_PLANE_URL, CLOUD_DASHBOARD_URL)
@property
def is_cloud(self) -> bool:
hostname = urlparse(self.control_plane_url).hostname or ""
return hostname == CLOUD_CONTROL_PLANE_HOST or hostname.endswith(
f".{CLOUD_CONTROL_PLANE_HOST}"
)
@classmethod
def from_control_plane_url(cls, url: str) -> ControlPlaneEndpoints:
control_plane_url = url.rstrip("/")
hostname = urlparse(control_plane_url).hostname or ""
if control_plane_url.endswith(CONTROL_PLANE_PATH):
return cls(control_plane_url, control_plane_url[: -len(CONTROL_PLANE_PATH)])
if hostname in LOCAL_HOSTNAMES:
return cls(control_plane_url, control_plane_url)
return cls(control_plane_url, _cloud_dashboard_for(hostname))
@classmethod
def from_langsmith_endpoint(cls, endpoint: str) -> ControlPlaneEndpoints:
parsed = urlparse(endpoint.rstrip("/"))
hostname = parsed.hostname or ""
if _is_cloud_host(hostname):
return cls.from_control_plane_url(
f"https://{_cloud_control_plane_host_for(hostname)}"
)
root = f"{parsed.scheme}://{parsed.netloc}{_without_api_path(parsed.path)}"
return cls(f"{root}{CONTROL_PLANE_PATH}", root)
def _is_cloud_host(hostname: str) -> bool:
return hostname == CLOUD_DOMAIN or hostname.endswith(f".{CLOUD_DOMAIN}")
def _cloud_control_plane_host_for(langsmith_api_host: str) -> str:
if langsmith_api_host.endswith(f".{CLOUD_API_HOST}"):
region = langsmith_api_host[: -len(CLOUD_API_HOST)]
return f"{region}{CLOUD_CONTROL_PLANE_HOST}"
return CLOUD_CONTROL_PLANE_HOST
def _cloud_dashboard_for(control_plane_host: str) -> str:
if control_plane_host.endswith(f".{CLOUD_CONTROL_PLANE_HOST}"):
region = control_plane_host[: -len(CLOUD_CONTROL_PLANE_HOST) - 1]
return f"https://{region}.{CLOUD_DASHBOARD_HOST}"
return CLOUD_DASHBOARD_URL
def _without_api_path(path: str) -> str:
for api_path in LANGSMITH_API_PATHS:
if path.endswith(api_path):
return path[: -len(api_path)]
return path
def _resources(payload: object) -> list[dict[str, Any]]:
if not isinstance(payload, dict):
return []
resources = payload.get("resources")
if not isinstance(resources, list):
return []
return [item for item in resources if isinstance(item, dict)]
class HostBackendError(click.ClickException):
"""Raised when the host backend returns an error response."""
def __init__(self, message: str, status_code: int | None = None):
def __init__(
self,
message: str,
status_code: int | None = None,
detail: str | None = None,
):
super().__init__(message)
self.status_code = status_code
self.detail = detail
def _error_detail(response: httpx.Response) -> str | None:
try:
body = response.json()
except ValueError:
return None
detail = body.get("detail") if isinstance(body, dict) else None
return detail if isinstance(detail, str) else None
class HostBackendClient:
@@ -24,24 +131,37 @@ class HostBackendClient:
base_url: str,
api_key: str,
tenant_id: str | None = None,
*,
transport: httpx.BaseTransport | None = None,
):
if not base_url:
raise click.UsageError("Host backend URL is required")
transport = httpx.HTTPTransport(retries=3)
headers: dict[str, str] = {
"X-Api-Key": api_key,
"Accept": "application/json",
}
if tenant_id:
headers["X-Tenant-ID"] = tenant_id
self._base_url = base_url.rstrip("/")
self._endpoints = ControlPlaneEndpoints.from_control_plane_url(base_url)
self._base_url = self._endpoints.control_plane_url
self._client = httpx.Client(
base_url=self._base_url,
headers=headers,
transport=transport,
transport=transport or httpx.HTTPTransport(retries=3),
timeout=30,
)
@property
def base_url(self) -> str:
return self._base_url
@property
def endpoints(self) -> ControlPlaneEndpoints:
return self._endpoints
def set_tenant(self, tenant_id: str) -> None:
self._client.headers["X-Tenant-ID"] = tenant_id
def _request(
self,
method: str,
@@ -53,10 +173,12 @@ class HostBackendClient:
resp = self._client.request(method, path, json=payload, params=params)
resp.raise_for_status()
except httpx.HTTPStatusError as err:
detail = err.response.text or str(err.response.status_code)
detail = _error_detail(err.response)
reason = detail or err.response.text or str(err.response.status_code)
raise HostBackendError(
f"{method} {path} failed with status {err.response.status_code}: {detail}",
f"{method} {path} failed with status {err.response.status_code}: {reason}",
status_code=err.response.status_code,
detail=detail,
) from None
except httpx.TransportError as err:
raise HostBackendError(str(err)) from None
@@ -72,30 +194,52 @@ class HostBackendClient:
def create_deployment(
self,
name: str,
deployment_type: str,
source: str,
config_path: str | None = None,
*,
name: str | None,
source: SourceName,
source_config: dict[str, object],
source_revision_config: dict[str, object],
secrets: list[dict[str, str]] | None = None,
agent: dict[str, str] | None = None,
) -> dict[str, Any]:
"""Create a deployment."""
payload: dict[str, Any] = {
"name": name,
"source": source,
"source_config": {"deployment_type": deployment_type},
"source_revision_config": {},
"source_config": source_config,
"source_revision_config": source_revision_config,
}
if source == "internal_source" and config_path:
payload["source_revision_config"]["langgraph_config_path"] = config_path
if agent is not None:
payload["agent"] = agent
else:
payload["name"] = name
if secrets is not None:
payload["secrets"] = secrets
return self._request("POST", "/v2/deployments", payload)
def list_deployments(self, name_contains: str = "") -> dict[str, Any]:
return self._request(
"GET",
"/v2/deployments",
params={"name_contains": name_contains},
def list_deployments(
self,
*,
name: str | None = None,
name_contains: str | None = None,
limit: int | None = None,
agent_id: str | None = None,
agent_environment: str | None = None,
) -> list[dict[str, Any]]:
given = (
("name", name),
("name_contains", name_contains),
("limit", limit),
("agent_id", agent_id),
("agent_environment", agent_environment),
)
params = {key: value for key, value in given if value is not None}
return _resources(self._request("GET", "/v2/deployments", params=params))
def get_listener(self, listener_id: str) -> dict[str, Any]:
return self._request("GET", f"/v2/listeners/{listener_id}")
def list_listeners(self) -> list[dict[str, Any]]:
return _resources(
self._request("GET", "/v2/listeners", params={"limit": MAX_PAGE_SIZE})
)
def get_deployment(self, deployment_id: str) -> dict[str, Any]:
@@ -121,22 +265,21 @@ class HostBackendClient:
self,
deployment_id: str,
image_uri: str,
*,
revision_source: SourceName | None,
secrets: list[dict[str, str]] | None = None,
tracked_packages: list[str] | None = None,
) -> dict[str, Any]:
payload: dict[str, Any] = {
"revision_source": "internal_docker",
"source_revision_config": {"image_uri": image_uri},
}
if revision_source is not None:
payload["revision_source"] = revision_source
if tracked_packages:
payload["tracked_packages"] = tracked_packages
if secrets is not None:
payload["secrets"] = secrets
return self._request(
"PATCH",
f"/v2/deployments/{deployment_id}",
payload,
)
return self._request("PATCH", f"/v2/deployments/{deployment_id}", payload)
def update_deployment_internal_source(
self,
@@ -171,10 +314,15 @@ class HostBackendClient:
payload["secrets"] = secrets
return self._request("PATCH", f"/v2/deployments/{deployment_id}", payload)
def list_revisions(self, deployment_id: str, limit: int = 1) -> dict[str, Any]:
return self._request(
"GET",
f"/v2/deployments/{deployment_id}/revisions?limit={limit}",
def list_revisions(
self, deployment_id: str, limit: int = 1
) -> list[dict[str, Any]]:
return _resources(
self._request(
"GET",
f"/v2/deployments/{deployment_id}/revisions",
params={"limit": limit},
)
)
def get_revision(self, deployment_id: str, revision_id: str) -> dict[str, Any]:
+35
View File
@@ -0,0 +1,35 @@
from __future__ import annotations
from dataclasses import dataclass, replace
DIGEST_SEPARATOR = "@sha256:"
DIGEST_MARKER = "@"
TAG_SEPARATOR = ":"
PATH_SEPARATOR = "/"
@dataclass(frozen=True, slots=True)
class ImageReference:
repository: str
tag: str | None = None
@classmethod
def parse(cls, reference: str) -> ImageReference:
if DIGEST_MARKER in reference:
raise ValueError(f"{reference!r} carries a digest and cannot be tagged")
path_start = reference.rfind(PATH_SEPARATOR) + 1
name, separator, tag = reference[path_start:].partition(TAG_SEPARATOR)
if not separator:
return cls(reference)
return cls(reference[:path_start] + name, tag)
def with_tag(self, tag: str) -> ImageReference:
return replace(self, tag=tag)
def matches_digest(self, repo_digest: str) -> bool:
return repo_digest.startswith(f"{self.repository}{DIGEST_SEPARATOR}")
def __str__(self) -> str:
if self.tag is None:
return self.repository
return f"{self.repository}{TAG_SEPARATOR}{self.tag}"
+27 -31
View File
@@ -382,20 +382,18 @@ def test_deploy_list_command(monkeypatch) -> None:
def list_deployments(self, name_contains: str = ""):
captured["name_contains"] = name_contains
return {
"resources": [
{
"id": "dep-123",
"name": "alpha",
"source_config": {"custom_url": "https://alpha.example.com"},
},
{
"id": "dep-456",
"name": "beta",
"source_config": {"custom_url": "https://beta.example.com"},
},
]
}
return [
{
"id": "dep-123",
"name": "alpha",
"source_config": {"custom_url": "https://alpha.example.com"},
},
{
"id": "dep-456",
"name": "beta",
"source_config": {"custom_url": "https://beta.example.com"},
},
]
monkeypatch.setattr(deploy_module, "HostBackendClient", FakeClient)
@@ -435,7 +433,7 @@ def test_deploy_list_command_no_results(monkeypatch) -> None:
pass
def list_deployments(self, name_contains: str = ""):
return {"resources": []}
return []
monkeypatch.setattr(deploy_module, "HostBackendClient", FakeClient)
@@ -468,20 +466,18 @@ def test_deploy_revisions_list_command(monkeypatch) -> None:
def list_revisions(self, deployment_id: str, limit: int = 1):
captured["deployment_id"] = deployment_id
captured["limit"] = str(limit)
return {
"resources": [
{
"id": "rev-123",
"status": "CREATING",
"created_at": "2023-11-07T05:31:56Z",
},
{
"id": "rev-456",
"status": "DEPLOYED",
"created_at": "2023-11-08T10:00:00Z",
},
]
}
return [
{
"id": "rev-123",
"status": "CREATING",
"created_at": "2023-11-07T05:31:56Z",
},
{
"id": "rev-456",
"status": "DEPLOYED",
"created_at": "2023-11-08T10:00:00Z",
},
]
monkeypatch.setattr(deploy_module, "HostBackendClient", FakeClient)
@@ -522,7 +518,7 @@ def test_deploy_revisions_list_command_no_results(monkeypatch) -> None:
pass
def list_revisions(self, deployment_id: str, limit: int = 1):
return {"resources": []}
return []
monkeypatch.setattr(deploy_module, "HostBackendClient", FakeClient)
@@ -555,7 +551,7 @@ def test_deploy_revisions_list_command_with_explicit_limit(monkeypatch) -> None:
def list_revisions(self, deployment_id: str, limit: int = 1):
captured["deployment_id"] = deployment_id
captured["limit"] = str(limit)
return {"resources": []}
return []
monkeypatch.setattr(deploy_module, "HostBackendClient", FakeClient)
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -12,5 +12,5 @@ def disable_analytics_env() -> None:
if "LANGGRAPH_CLI_NO_ANALYTICS" in os.environ:
print("⚠️ LANGGRAPH_CLI_NO_ANALYTICS is set. Overriding it for the test.")
with patch.dict(os.environ, {"LANGGRAPH_CLI_NO_ANALYTICS": "0"}):
with patch.dict(os.environ, {"LANGGRAPH_CLI_NO_ANALYTICS": "1"}):
yield
@@ -0,0 +1,119 @@
import json
from unittest.mock import Mock
import httpx
import pytest
from click.testing import CliRunner
import langgraph_cli.deploy as deploy
from langgraph_cli.cli import cli
from langgraph_cli.host_backend import HostBackendClient
@pytest.fixture
def deployment_api(monkeypatch, tmp_path):
monkeypatch.chdir(tmp_path)
monkeypatch.delenv("LANGSMITH_DEPLOYMENT_NAME", raising=False)
monkeypatch.setattr(deploy, "_emitter", None)
monkeypatch.setattr(deploy, "_no_input", False)
(tmp_path / "langgraph.json").write_text(
json.dumps({"dependencies": ["."], "graphs": {"agent": "./agent.py:graph"}})
)
(tmp_path / ".env").write_text("LANGSMITH_DEPLOYMENT_NAME=legacy\n")
requests = []
state = {"enabled": True, "resources": []}
def handler(request):
requests.append(request)
assert request.url.path == "/v2/deployments"
if request.method == "GET":
if not state["enabled"] and (
"agent_id" in request.url.params
or "agent_environment" in request.url.params
):
return httpx.Response(
400, text="Agent filters are not available for this tenant."
)
return httpx.Response(200, json={"resources": state["resources"]})
assert request.method == "POST"
return httpx.Response(200, json={"id": "runtime-id", "name": "server-name"})
client = HostBackendClient("https://api.example.com", "test-key")
client._client.close()
client._client = httpx.Client(
base_url="https://api.example.com",
transport=httpx.MockTransport(handler),
headers={"X-Api-Key": "test-key"},
)
monkeypatch.setattr(deploy, "_create_host_backend_client", lambda *a, **kw: client)
monkeypatch.setattr(deploy, "find_tracked_packages", lambda *a: [])
remote_build = Mock(return_value=deploy.BuildResult())
monkeypatch.setattr(deploy, "_run_remote_build", remote_build)
monkeypatch.setattr(deploy, "_resolve_build_mode", lambda flag, **kw: (flag, None))
yield state, requests, remote_build
client._client.close()
AGENT_ARGS = [
"deploy",
"--agent-id",
"customer-support",
"--environment",
"staging",
"--remote",
"--no-wait",
"--no-input",
]
def test_agent_create(deployment_api, tmp_path, monkeypatch):
monkeypatch.setenv("LANGSMITH_DEPLOYMENT_NAME", "legacy")
_, requests, build = deployment_api
result = CliRunner().invoke(cli, AGENT_ARGS)
assert result.exit_code == 0, result.output
assert dict(requests[0].url.params) == {
"agent_id": "customer-support",
"agent_environment": "staging",
"limit": "100",
}
payload = json.loads(requests[1].content)
assert payload["agent"] == {
"agent_id": "customer-support",
"environment": "staging",
}
assert "name" not in payload
assert build.call_args.kwargs["deployment_id"] == "runtime-id"
assert "server-name" in result.output
assert (tmp_path / ".env").read_text() == "LANGSMITH_DEPLOYMENT_NAME=legacy\n"
def test_agent_update(deployment_api):
state, requests, build = deployment_api
state["resources"] = [{"id": "existing-id", "is_preview": False}]
result = CliRunner().invoke(cli, AGENT_ARGS)
assert result.exit_code == 0, result.output
assert len(requests) == 1
assert build.call_args.kwargs["deployment_id"] == "existing-id"
def test_agent_rejects_explicit_name(deployment_api, monkeypatch):
monkeypatch.setenv("LANGSMITH_DEPLOYMENT_NAME", "legacy")
_, requests, _ = deployment_api
result = CliRunner().invoke(cli, [*AGENT_ARGS, "--name", "legacy"])
assert result.exit_code == 2
assert "cannot be combined" in result.output
assert not requests
def test_agent_lookup_refuses_a_control_plane_that_ignores_the_filter(deployment_api):
state, requests, _ = deployment_api
state["resources"] = [
{"id": "someone-elses", "is_preview": False},
{"id": "another", "is_preview": False},
]
result = CliRunner().invoke(cli, AGENT_ARGS)
assert result.exit_code != 0
assert "does not filter deployments by agent" in result.output
assert len(requests) == 1
+529 -57
View File
@@ -13,6 +13,17 @@ import pytest
import langgraph_cli.deploy as deploy_mod
from langgraph_cli.deploy import (
ById,
ByName,
CustomerRegistrySource,
DockerBuildCommand,
ExistingDeployment,
Listener,
ManagedRegistrySource,
OnListener,
RemoteBuildSource,
RequestedPlacement,
Unplaced,
_call_host_backend_with_optional_tenant,
_create_host_backend_client,
_docker_config_for_token,
@@ -21,12 +32,14 @@ from langgraph_cli.deploy import (
_parse_env_from_config,
_resolve_env_path,
_resolve_pushed_image_digest,
_smith_dashboard_base_url,
_select_source,
_validate_prebuilt_image,
find_deployment_by_name,
normalize_image_tag,
normalize_name,
)
from langgraph_cli.host_backend import HostBackendClient, HostBackendError
from langgraph_cli.image_reference import ImageReference
class TestDockerConfigForToken:
@@ -259,31 +272,29 @@ class TestEnvWithoutDeploymentName:
class TestCallHostBackendWithOptionalTenant:
def _make_client(self, handler):
c = HostBackendClient("https://api.example.com", "test-key")
c._client = httpx.Client(
base_url="https://api.example.com",
c = HostBackendClient(
"https://api.example.com",
"test-key",
transport=httpx.MockTransport(handler),
headers={"X-Api-Key": "test-key", "Accept": "application/json"},
timeout=30,
)
return c
def _make_eu_client(self, handler):
c = HostBackendClient("https://eu.api.host.langchain.com", "test-key")
c._client = httpx.Client(
base_url="https://eu.api.host.langchain.com",
c = HostBackendClient(
"https://eu.api.host.langchain.com",
"test-key",
transport=httpx.MockTransport(handler),
headers={"X-Api-Key": "test-key", "Accept": "application/json"},
timeout=30,
)
return c
def test_success_passes_through(self):
client = self._make_client(lambda req: httpx.Response(200, json={"ok": True}))
client = self._make_client(
lambda req: httpx.Response(200, json={"resources": [{"id": "dep-1"}]})
)
result = _call_host_backend_with_optional_tenant(
client, lambda c: c.list_deployments()
)
assert result == {"ok": True}
assert result == [{"id": "dep-1"}]
def test_403_not_enabled_gives_actionable_error(self):
detail = (
@@ -334,7 +345,6 @@ class TestCallHostBackendWithOptionalTenant:
assert exc_info.value.status_code == 403
assert "smith.langchain.com" in exc_info.value.message
assert seen_tenant_ids == [None, "workspace-123"]
assert client._client.headers["X-Tenant-ID"] == "workspace-123"
def test_other_403_re_raises_original(self):
client = self._make_client(
@@ -540,60 +550,226 @@ class TestCreateHostBackendClientNoInput:
assert client is not None
class TestSmithDashboardBaseUrl:
def test_none_returns_default(self):
assert _smith_dashboard_base_url(None) == "https://smith.langchain.com"
class TestCreateHostBackendClientEndpoint:
def test_langsmith_endpoint_from_project_env_selects_self_hosted_control_plane(
self, monkeypatch
):
monkeypatch.setenv("LANGSMITH_API_KEY", "lsv2_test")
monkeypatch.delenv("LANGSMITH_ENDPOINT", raising=False)
def test_empty_returns_default(self):
assert _smith_dashboard_base_url("") == "https://smith.langchain.com"
def test_prod_host_url(self):
assert (
_smith_dashboard_base_url("https://api.host.langchain.com")
== "https://smith.langchain.com"
client = _create_host_backend_client(
host_url=None,
api_key=None,
env_vars={"LANGSMITH_ENDPOINT": "https://smith.example.com/api/v1"},
)
def test_dev_host_url(self):
assert (
_smith_dashboard_base_url("https://dev.api.host.langchain.com")
== "https://dev.smith.langchain.com"
assert client.base_url == "https://smith.example.com/api-host"
def test_explicit_host_url_wins_over_langsmith_endpoint(self, monkeypatch):
monkeypatch.setenv("LANGSMITH_API_KEY", "lsv2_test")
monkeypatch.setenv("LANGSMITH_ENDPOINT", "https://smith.example.com/api/v1")
client = _create_host_backend_client(
host_url="https://custom.host.com", api_key=None, env_vars={}
)
def test_eu_host_url(self):
assert (
_smith_dashboard_base_url("https://eu.api.host.langchain.com")
== "https://eu.smith.langchain.com"
assert client.base_url == "https://custom.host.com"
class TestDockerBuildCommand:
@pytest.mark.parametrize(
("machine", "verbose", "expected"),
[
pytest.param(
"x86_64",
False,
DockerBuildCommand(("docker", "build"), ()),
id="amd64_host_builds_natively",
),
pytest.param(
"arm64",
False,
DockerBuildCommand(
("docker", "buildx", "build"),
("--platform", "linux/amd64", "--load", "--progress=quiet"),
),
id="other_hosts_cross_build_quietly",
),
pytest.param(
"arm64",
True,
DockerBuildCommand(
("docker", "buildx", "build"),
("--platform", "linux/amd64", "--load"),
),
id="verbose_cross_build_keeps_progress_output",
),
],
)
def test_for_host_targets_the_deployment_platform(self, machine, verbose, expected):
assert DockerBuildCommand.for_host(machine, verbose=verbose) == expected
class TestSelectSource:
OPTIONS = {
"push_to": None,
"image": None,
"image_name": None,
"tag": None,
"remote_build_flag": None,
"placement": RequestedPlacement(),
"selector": ByName("my-app"),
}
REPOSITORY = "registry.example.com/app"
@pytest.mark.parametrize(
("flags", "docker_available", "expected"),
[
pytest.param(
{"push_to": REPOSITORY},
True,
CustomerRegistrySource(
reference=ImageReference(REPOSITORY, "latest"),
prebuilt_image=None,
requested_placement=RequestedPlacement(),
),
id="push_to_selects_the_external_source_with_the_default_tag",
),
pytest.param(
{"push_to": f"{REPOSITORY}:v2"},
True,
CustomerRegistrySource(
reference=ImageReference(REPOSITORY, "v2"),
prebuilt_image=None,
requested_placement=RequestedPlacement(),
),
id="push_to_keeps_a_tag_given_in_the_reference",
),
pytest.param(
{"push_to": REPOSITORY, "tag": "v3"},
True,
CustomerRegistrySource(
reference=ImageReference(REPOSITORY, "v3"),
prebuilt_image=None,
requested_placement=RequestedPlacement(),
),
id="tag_flag_composes_with_push_to",
),
pytest.param(
{"push_to": REPOSITORY, "image": "app:dev"},
False,
CustomerRegistrySource(
reference=ImageReference(REPOSITORY, "latest"),
prebuilt_image="app:dev",
requested_placement=RequestedPlacement(),
),
id="prebuilt_image_is_retagged_for_push_to_without_docker_checks",
),
pytest.param(
{
"push_to": REPOSITORY,
"placement": RequestedPlacement("listener-1", "agents"),
},
True,
CustomerRegistrySource(
reference=ImageReference(REPOSITORY, "latest"),
prebuilt_image=None,
requested_placement=RequestedPlacement("listener-1", "agents"),
),
id="push_to_carries_the_requested_placement",
),
pytest.param(
{"remote_build_flag": True},
True,
RemoteBuildSource(),
id="remote_flag_selects_the_source_upload",
),
pytest.param(
{},
False,
RemoteBuildSource(),
id="no_local_docker_falls_back_to_the_source_upload",
),
pytest.param(
{},
True,
ManagedRegistrySource(
prebuilt_image=None, image_name=None, tag="latest"
),
id="local_docker_selects_the_internal_docker_source",
),
pytest.param(
{"image": "app:dev", "tag": "v1"},
False,
ManagedRegistrySource(
prebuilt_image="app:dev", image_name=None, tag="v1"
),
id="prebuilt_image_forces_the_internal_docker_source",
),
],
)
def test_flags_select_one_source(
self, monkeypatch, mocker, flags, docker_available, expected
):
mocker.patch(
"langgraph_cli.deploy._get_emitter", return_value=mocker.MagicMock()
)
monkeypatch.setattr(
deploy_mod,
"can_build_locally",
lambda: (True, None) if docker_available else (False, "Docker is required"),
)
def test_staging_host_url(self):
assert (
_smith_dashboard_base_url("https://staging.api.host.langchain.com")
== "https://staging.smith.langchain.com"
assert _select_source(**{**self.OPTIONS, **flags}) == expected
def test_push_to_build_requires_local_docker(self, monkeypatch):
monkeypatch.setattr(
deploy_mod, "can_build_locally", lambda: (False, "Docker is required")
)
def test_localhost(self):
assert (
_smith_dashboard_base_url("http://localhost:8080")
== "http://localhost:8080"
)
with pytest.raises(click.UsageError, match="Docker is required"):
_select_source(**{**self.OPTIONS, "push_to": self.REPOSITORY})
def test_localhost_trailing_slash(self):
assert (
_smith_dashboard_base_url("http://localhost:8080/")
== "http://localhost:8080"
)
@pytest.mark.parametrize(
("flags", "message"),
[
pytest.param(
{"push_to": REPOSITORY, "remote_build_flag": True},
"--push-to cannot be combined with --remote.",
id="push_to_with_remote",
),
pytest.param(
{"push_to": f"{REPOSITORY}:v1", "tag": "v2"},
"already includes a tag",
id="push_to_with_a_tag_and_the_tag_flag",
),
pytest.param(
{"push_to": f"{REPOSITORY}@sha256:abc"},
"not a digest",
id="push_to_with_a_digest",
),
pytest.param(
{"image": "app:dev", "remote_build_flag": True},
"--image cannot be combined with --remote builds.",
id="image_with_remote",
),
pytest.param(
{"placement": RequestedPlacement(listener_id="listener-1")},
"only apply when creating a deployment with --push-to",
id="listener_without_push_to",
),
pytest.param(
{"placement": RequestedPlacement(k8s_namespace="agents")},
"only apply when creating a deployment with --push-to",
id="namespace_without_push_to",
),
],
)
def test_conflicting_flags_are_rejected(self, monkeypatch, flags, message):
monkeypatch.setattr(deploy_mod, "can_build_locally", lambda: (True, None))
def test_127_0_0_1(self):
assert (
_smith_dashboard_base_url("http://127.0.0.1:3000")
== "http://127.0.0.1:3000"
)
def test_unknown_domain_returns_default(self):
assert (
_smith_dashboard_base_url("https://custom.example.com")
== "https://smith.langchain.com"
)
with pytest.raises(click.UsageError, match=message):
_select_source(**{**self.OPTIONS, **flags})
class TestResolvePushedImageDigest:
@@ -644,6 +820,16 @@ class TestResolvePushedImageDigest:
)
assert out == "us-central1-docker.pkg.dev/proj/repo@sha256:abc123"
def test_registry_port_without_tag_still_resolves_the_digest(self):
runner = self._runner('["localhost:5000/repo@sha256:abc123"]')
out = _resolve_pushed_image_digest(
runner,
remote_image="localhost:5000/repo",
docker_config_dir=None,
verbose=False,
)
assert out == "localhost:5000/repo@sha256:abc123"
def test_empty_repodigests_falls_back_with_warning(self, mocker):
emitter = mocker.MagicMock()
mocker.patch("langgraph_cli.deploy._get_emitter", return_value=emitter)
@@ -747,3 +933,289 @@ class TestResolvePushedImageDigest:
frame_locals = captured["coro"].cr_frame.f_locals
assert "--config" not in frame_locals["args"]
captured["coro"].close()
class TestListener:
@pytest.mark.parametrize(
("resource", "expected"),
[
pytest.param(
{
"id": "listener-1",
"compute_id": "prod-cluster",
"compute_config": {"k8s_namespaces": ["agents", "agents-staging"]},
},
Listener("listener-1", "prod-cluster", ("agents", "agents-staging")),
id="reads_id_cluster_and_namespaces",
),
pytest.param(
{"id": "listener-1", "compute_id": "c", "compute_config": {}},
Listener("listener-1", "c", ()),
id="missing_namespaces",
),
pytest.param(
{"id": "listener-1", "compute_id": "c", "compute_config": None},
Listener("listener-1", "c", ()),
id="null_compute_config",
),
pytest.param(
{"id": "listener-1"},
Listener("listener-1", "", ()),
id="only_an_id",
),
],
)
def test_from_resource_reads_the_control_plane_shape(self, resource, expected):
assert Listener.from_resource(resource) == expected
ONE_NAMESPACE = Listener("listener-1", "prod-cluster", ("agents",))
TWO_NAMESPACES = Listener("listener-2", "multi-cluster", ("agents", "agents-staging"))
NO_NAMESPACE = Listener("listener-3", "broken-cluster", ())
class TestRequestedPlacement:
@pytest.mark.parametrize(
("request_", "listeners", "expected"),
[
pytest.param(
RequestedPlacement(), (), Unplaced(), id="no_listeners_no_request"
),
pytest.param(
RequestedPlacement(),
(ONE_NAMESPACE,),
OnListener("listener-1", "agents"),
id="uses_the_only_possible_answer",
),
pytest.param(
RequestedPlacement(k8s_namespace="agents-staging"),
(TWO_NAMESPACES,),
OnListener("listener-2", "agents-staging"),
id="namespace_alone_picks_the_only_listener",
),
],
)
def test_resolves_to_a_placement(self, request_, listeners, expected):
assert request_.among(listeners) == expected
@pytest.mark.parametrize(
("request_", "listeners", "message"),
[
pytest.param(
RequestedPlacement(listener_id="listener-1"),
(),
"no listeners",
id="workspace_has_no_listeners",
),
pytest.param(
RequestedPlacement(),
(ONE_NAMESPACE, TWO_NAMESPACES),
"--listener-id",
id="several_listeners_need_a_choice",
),
pytest.param(
RequestedPlacement(k8s_namespace="agents"),
(ONE_NAMESPACE, TWO_NAMESPACES),
"--listener-id",
id="namespace_alone_is_ambiguous_with_several_listeners",
),
pytest.param(
RequestedPlacement(k8s_namespace="agents"),
(),
"no listeners",
id="namespace_without_any_listener",
),
pytest.param(
RequestedPlacement(),
(TWO_NAMESPACES,),
"--k8s-namespace",
id="several_namespaces_need_a_choice",
),
],
)
def test_refuses_and_names_the_choices(self, request_, listeners, message):
with pytest.raises(click.UsageError, match=message):
request_.among(listeners)
def test_the_error_lists_every_listener_with_its_cluster_and_namespaces(self):
with pytest.raises(click.UsageError) as error:
RequestedPlacement().among((ONE_NAMESPACE, TWO_NAMESPACES))
assert "listener-1" in error.value.message
assert "prod-cluster" in error.value.message
assert "agents-staging" in error.value.message
@pytest.mark.parametrize(
("placement", "expected"),
[
pytest.param(Unplaced(), {}, id="unplaced_adds_nothing"),
pytest.param(
OnListener("listener-1", "agents"),
{
"listener_id": "listener-1",
"listener_config": {"k8s_namespace": "agents"},
},
id="placed_carries_listener_and_namespace",
),
],
)
def test_source_config_matches_the_control_plane_shape(self, placement, expected):
assert placement.source_config() == expected
def test_finding_a_deployment_by_name_narrows_the_search_for_every_server_version():
seen: dict = {}
def handler(req: httpx.Request) -> httpx.Response:
seen["params"] = dict(req.url.params)
return httpx.Response(
200,
json={"resources": [{"id": "dep-1", "name": "agent", "source": "github"}]},
)
client = HostBackendClient(
"https://api.example.com", "key", transport=httpx.MockTransport(handler)
)
found = find_deployment_by_name(client, "agent")
assert seen["params"] == {
"name": "agent",
"name_contains": "agent",
"limit": "100",
}
assert found == ExistingDeployment("dep-1", "github")
def test_a_server_that_ignores_the_exact_name_filter_never_matches_another_deployment():
client = HostBackendClient(
"https://api.example.com",
"key",
transport=httpx.MockTransport(
lambda req: httpx.Response(
200,
json={
"resources": [
{
"id": "dep-other",
"name": "another-teams-agent",
"source": "external_docker",
}
]
},
)
),
)
assert find_deployment_by_name(client, "brand-new-agent") is None
def test_a_full_page_without_a_match_refuses_to_claim_the_name_is_free():
page = [
{"id": f"dep-{index}", "name": f"other-agent-{index}"} for index in range(100)
]
client = HostBackendClient(
"https://api.example.com",
"key",
transport=httpx.MockTransport(
lambda req: httpx.Response(200, json={"resources": page})
),
)
with pytest.raises(click.ClickException, match="--deployment-id"):
find_deployment_by_name(client, "brand-new-agent")
def test_a_partial_page_without_a_match_means_the_name_is_free():
client = HostBackendClient(
"https://api.example.com",
"key",
transport=httpx.MockTransport(
lambda req: httpx.Response(
200, json={"resources": [{"id": "dep-1", "name": "other"}]}
)
),
)
assert find_deployment_by_name(client, "brand-new-agent") is None
@pytest.mark.parametrize(
"resource",
[
pytest.param({"compute_id": "c"}, id="no_id"),
pytest.param({"id": ""}, id="empty_id"),
],
)
def test_a_listener_without_an_id_is_refused(resource):
with pytest.raises(HostBackendError, match="without an id"):
Listener.from_resource(resource)
def test_a_deployment_id_with_listener_flags_is_refused_without_probing_docker(
monkeypatch,
):
def explode() -> tuple[bool, str | None]:
raise AssertionError("docker must not be probed for an argv-only conflict")
monkeypatch.setattr(deploy_mod, "can_build_locally", explode)
with pytest.raises(click.UsageError, match="--deployment-id"):
_select_source(
push_to="registry.example.com/app",
image=None,
image_name=None,
tag=None,
remote_build_flag=None,
placement=RequestedPlacement(listener_id="listener-1"),
selector=ById("dep-1"),
)
class TestPlacementOnAKnownListener:
@pytest.mark.parametrize(
("request_", "listener", "expected"),
[
pytest.param(
RequestedPlacement(listener_id="listener-1"),
ONE_NAMESPACE,
OnListener("listener-1", "agents"),
id="the_only_namespace_is_used",
),
pytest.param(
RequestedPlacement(listener_id="listener-2", k8s_namespace="agents"),
TWO_NAMESPACES,
OnListener("listener-2", "agents"),
id="the_chosen_namespace_is_used",
),
],
)
def test_places_on_the_listener(self, request_, listener, expected):
assert request_.on(listener) == expected
@pytest.mark.parametrize(
("request_", "listener", "message"),
[
pytest.param(
RequestedPlacement(listener_id="listener-2"),
TWO_NAMESPACES,
"--k8s-namespace",
id="several_namespaces_need_a_choice",
),
pytest.param(
RequestedPlacement(listener_id="listener-2", k8s_namespace="nope"),
TWO_NAMESPACES,
"does not serve namespace",
id="unknown_namespace",
),
pytest.param(
RequestedPlacement(listener_id="listener-3"),
NO_NAMESPACE,
"serves no namespaces",
id="listener_without_namespaces",
),
],
)
def test_refuses_and_names_the_namespaces(self, request_, listener, message):
with pytest.raises(click.UsageError, match=message):
request_.on(listener)
+535 -148
View File
@@ -3,29 +3,16 @@ import json
import httpx
import pytest
from langgraph_cli.host_backend import HostBackendClient, HostBackendError
@pytest.fixture
def mock_transport():
return httpx.MockTransport(lambda req: httpx.Response(200, json={"ok": True}))
@pytest.fixture
def client(mock_transport):
c = HostBackendClient("https://api.example.com", "test-key")
c._client = httpx.Client(
base_url="https://api.example.com",
transport=mock_transport,
headers={"X-Api-Key": "test-key", "Accept": "application/json"},
timeout=30,
)
return c
from langgraph_cli.host_backend import (
ControlPlaneEndpoints,
HostBackendClient,
HostBackendError,
)
def test_constructor_strips_trailing_slash():
c = HostBackendClient("https://api.example.com/", "key")
assert str(c._client.base_url) == "https://api.example.com"
assert c.base_url == "https://api.example.com"
def test_constructor_empty_url_raises():
@@ -39,12 +26,8 @@ def test_request_sends_headers():
assert req.headers["accept"] == "application/json"
return httpx.Response(200, json={"ok": True})
c = HostBackendClient("https://api.example.com", "test-key")
c._client = httpx.Client(
base_url="https://api.example.com",
transport=httpx.MockTransport(handler),
headers={"X-Api-Key": "test-key", "Accept": "application/json"},
timeout=30,
c = HostBackendClient(
"https://api.example.com", "test-key", transport=httpx.MockTransport(handler)
)
result = c._request("GET", "/test")
assert result == {"ok": True}
@@ -56,12 +39,8 @@ def test_request_sends_json_payload():
assert req.content == b'{"key":"value"}'
return httpx.Response(200, json={"created": True})
c = HostBackendClient("https://api.example.com", "test-key")
c._client = httpx.Client(
base_url="https://api.example.com",
transport=httpx.MockTransport(handler),
headers={"X-Api-Key": "test-key", "Accept": "application/json"},
timeout=30,
c = HostBackendClient(
"https://api.example.com", "test-key", transport=httpx.MockTransport(handler)
)
result = c._request("POST", "/test", {"key": "value"})
assert result == {"created": True}
@@ -69,25 +48,13 @@ def test_request_sends_json_payload():
def test_request_empty_body_returns_none():
transport = httpx.MockTransport(lambda req: httpx.Response(200, content=b""))
c = HostBackendClient("https://api.example.com", "test-key")
c._client = httpx.Client(
base_url="https://api.example.com",
transport=transport,
headers={"X-Api-Key": "test-key", "Accept": "application/json"},
timeout=30,
)
c = HostBackendClient("https://api.example.com", "test-key", transport=transport)
assert c._request("DELETE", "/test") is None
def test_request_http_error_raises():
transport = httpx.MockTransport(lambda req: httpx.Response(404, text="not found"))
c = HostBackendClient("https://api.example.com", "test-key")
c._client = httpx.Client(
base_url="https://api.example.com",
transport=transport,
headers={"X-Api-Key": "test-key", "Accept": "application/json"},
timeout=30,
)
c = HostBackendClient("https://api.example.com", "test-key", transport=transport)
with pytest.raises(HostBackendError, match="404"):
c._request("GET", "/missing")
@@ -96,13 +63,7 @@ def test_request_invalid_json_raises():
transport = httpx.MockTransport(
lambda req: httpx.Response(200, content=b"not json")
)
c = HostBackendClient("https://api.example.com", "test-key")
c._client = httpx.Client(
base_url="https://api.example.com",
transport=transport,
headers={"X-Api-Key": "test-key", "Accept": "application/json"},
timeout=30,
)
c = HostBackendClient("https://api.example.com", "test-key", transport=transport)
with pytest.raises(HostBackendError, match="Failed to decode"):
c._request("GET", "/bad-json")
@@ -111,84 +72,20 @@ def test_request_transport_error_raises():
def handler(req: httpx.Request) -> httpx.Response:
raise httpx.ConnectError("connection refused")
c = HostBackendClient("https://api.example.com", "test-key")
c._client = httpx.Client(
base_url="https://api.example.com",
transport=httpx.MockTransport(handler),
headers={"X-Api-Key": "test-key", "Accept": "application/json"},
timeout=30,
c = HostBackendClient(
"https://api.example.com", "test-key", transport=httpx.MockTransport(handler)
)
with pytest.raises(HostBackendError, match="connection refused"):
c._request("GET", "/test")
def test_create_deployment(client):
result = client.create_deployment(
name="my-deploy", deployment_type="dev", source="internal_docker"
)
assert result == {"ok": True}
def test_get_deployment(client):
result = client.get_deployment("dep-123")
assert result == {"ok": True}
def test_list_deployments(client):
result = client.list_deployments("my-app")
assert result == {"ok": True}
def test_list_deployments_sends_query_params():
def handler(req: httpx.Request) -> httpx.Response:
assert req.url.path == "/v2/deployments"
assert req.url.params["name_contains"] == "my app"
return httpx.Response(200, json={"ok": True})
c = HostBackendClient("https://api.example.com", "test-key")
c._client = httpx.Client(
base_url="https://api.example.com",
transport=httpx.MockTransport(handler),
headers={"X-Api-Key": "test-key", "Accept": "application/json"},
timeout=30,
)
result = c.list_deployments("my app")
assert result == {"ok": True}
def test_delete_deployment(client):
result = client.delete_deployment("dep-123")
assert result == {"ok": True}
def test_request_push_token(client):
result = client.request_push_token("dep-123")
assert result == {"ok": True}
def test_update_deployment(client):
result = client.update_deployment(
"dep-123", "image:latest", secrets=[{"name": "KEY", "value": "val"}]
)
assert result == {"ok": True}
def test_update_deployment_no_secrets(client):
result = client.update_deployment("dep-123", "image:latest")
assert result == {"ok": True}
def _capturing_client(captured: dict) -> HostBackendClient:
def handler(req: httpx.Request) -> httpx.Response:
captured["body"] = req.read()
return httpx.Response(200, json={"ok": True})
c = HostBackendClient("https://api.example.com", "key")
c._client = httpx.Client(
base_url="https://api.example.com",
transport=httpx.MockTransport(handler),
headers={"X-Api-Key": "key", "Accept": "application/json"},
timeout=30,
c = HostBackendClient(
"https://api.example.com", "key", transport=httpx.MockTransport(handler)
)
return c
@@ -199,6 +96,7 @@ def test_update_deployment_forwards_tracked_packages():
c.update_deployment(
"dep-123",
"image:latest",
revision_source="internal_docker",
tracked_packages=["google-adk:1.0.0"],
)
body = json.loads(captured["body"])
@@ -209,7 +107,7 @@ def test_update_deployment_forwards_tracked_packages():
def test_update_deployment_omits_tracked_packages_when_absent():
captured: dict = {}
c = _capturing_client(captured)
c.update_deployment("dep-123", "image:latest")
c.update_deployment("dep-123", "image:latest", revision_source="internal_docker")
body = json.loads(captured["body"])
assert "tracked_packages" not in body
@@ -241,33 +139,14 @@ def test_update_deployment_internal_source_omits_tracked_packages_when_absent():
assert "tracked_packages" not in body
def test_list_revisions(client):
result = client.list_revisions("dep-123", limit=5)
assert result == {"ok": True}
def test_get_revision(client):
result = client.get_revision("dep-123", "rev-456")
assert result == {"ok": True}
def test_get_build_logs(client):
result = client.get_build_logs("proj-1", "rev-1", {"limit": 10})
assert result == {"ok": True}
def test_get_deploy_logs_all_revisions():
def handler(req: httpx.Request) -> httpx.Response:
assert "/v1/projects/proj-1/deploy_logs" in str(req.url)
assert "/revisions/" not in str(req.url)
return httpx.Response(200, json={"logs": [{"message": "running"}]})
c = HostBackendClient("https://api.example.com", "key")
c._client = httpx.Client(
base_url="https://api.example.com",
transport=httpx.MockTransport(handler),
headers={"X-Api-Key": "key", "Accept": "application/json"},
timeout=30,
c = HostBackendClient(
"https://api.example.com", "key", transport=httpx.MockTransport(handler)
)
result = c.get_deploy_logs("proj-1", {"limit": 10})
assert result == {"logs": [{"message": "running"}]}
@@ -278,12 +157,520 @@ def test_get_deploy_logs_specific_revision():
assert "/v1/projects/proj-1/revisions/rev-2/deploy_logs" in str(req.url)
return httpx.Response(200, json={"logs": []})
c = HostBackendClient("https://api.example.com", "key")
c._client = httpx.Client(
base_url="https://api.example.com",
transport=httpx.MockTransport(handler),
headers={"X-Api-Key": "key", "Accept": "application/json"},
timeout=30,
c = HostBackendClient(
"https://api.example.com", "key", transport=httpx.MockTransport(handler)
)
result = c.get_deploy_logs("proj-1", {"limit": 10}, revision_id="rev-2")
assert result == {"logs": []}
def _routing_client(seen: dict) -> HostBackendClient:
def handler(req: httpx.Request) -> httpx.Response:
seen["method"] = req.method
seen["url"] = str(req.url)
return httpx.Response(200, json={"ok": True})
c = HostBackendClient(
"https://api.example.com/prefix", "key", transport=httpx.MockTransport(handler)
)
return c
@pytest.mark.parametrize(
("call", "expected_body"),
[
pytest.param(
lambda c: c.create_deployment(
name="my-deploy",
source="internal_docker",
source_config={"deployment_type": "dev"},
source_revision_config={},
),
{
"name": "my-deploy",
"source": "internal_docker",
"source_config": {"deployment_type": "dev"},
"source_revision_config": {},
},
id="internal_docker_create_omits_secrets_key_when_not_given",
),
pytest.param(
lambda c: c.create_deployment(
name="my-deploy",
source="internal_docker",
source_config={"deployment_type": "prod"},
source_revision_config={},
secrets=[{"name": "KEY", "value": "val"}],
),
{
"name": "my-deploy",
"source": "internal_docker",
"source_config": {"deployment_type": "prod"},
"source_revision_config": {},
"secrets": [{"name": "KEY", "value": "val"}],
},
id="internal_docker_create_forwards_secrets",
),
pytest.param(
lambda c: c.update_deployment(
"dep-123",
"registry.example.com/app@sha256:abc",
revision_source="internal_docker",
secrets=[{"name": "KEY", "value": "val"}],
),
{
"revision_source": "internal_docker",
"source_revision_config": {
"image_uri": "registry.example.com/app@sha256:abc"
},
"secrets": [{"name": "KEY", "value": "val"}],
},
id="internal_docker_revision_names_its_source",
),
pytest.param(
lambda c: c.update_deployment_internal_source(
"dep-123",
source_tarball_path="tarballs/src.tgz",
config_path="langgraph.json",
secrets=[],
install_command="yarn install",
build_command="yarn build",
),
{
"revision_source": "internal_source",
"source_revision_config": {
"source_tarball_path": "tarballs/src.tgz",
"langgraph_config_path": "langgraph.json",
},
"source_config": {
"install_command": "yarn install",
"build_command": "yarn build",
},
"secrets": [],
},
id="internal_source_revision_sends_js_build_commands",
),
pytest.param(
lambda c: c.update_deployment_internal_source(
"dep-123",
source_tarball_path="tarballs/src.tgz",
config_path="langgraph.json",
),
{
"revision_source": "internal_source",
"source_revision_config": {
"source_tarball_path": "tarballs/src.tgz",
"langgraph_config_path": "langgraph.json",
},
},
id="internal_source_revision_omits_source_config_without_commands",
),
pytest.param(
lambda c: c.create_deployment(
name="agent",
source="external_docker",
source_config={"resource_spec": {}},
source_revision_config={
"image_uri": "registry.example.com/agent@sha256:1"
},
secrets=[],
),
{
"name": "agent",
"source": "external_docker",
"source_config": {"resource_spec": {}},
"source_revision_config": {
"image_uri": "registry.example.com/agent@sha256:1"
},
"secrets": [],
},
id="create_sends_the_source_configs_as_given",
),
pytest.param(
lambda c: c.update_deployment(
"dep-1", "registry.example.com/agent@sha256:2", revision_source=None
),
{
"source_revision_config": {
"image_uri": "registry.example.com/agent@sha256:2"
}
},
id="revision_without_source_override_omits_revision_source",
),
pytest.param(
lambda c: c.update_deployment(
"dep-1",
"registry.example.com/agent@sha256:2",
revision_source="internal_docker",
tracked_packages=["langgraph:1.0.0"],
),
{
"revision_source": "internal_docker",
"source_revision_config": {
"image_uri": "registry.example.com/agent@sha256:2"
},
"tracked_packages": ["langgraph:1.0.0"],
},
id="revision_with_source_override_names_it",
),
],
)
def test_request_body_matches_control_plane_contract(call, expected_body):
captured: dict = {}
call(_capturing_client(captured))
assert json.loads(captured["body"]) == expected_body
@pytest.mark.parametrize(
("call", "method", "route"),
[
pytest.param(
lambda c: c.create_deployment(
name="n",
source="internal_docker",
source_config={"deployment_type": "dev"},
source_revision_config={},
),
"POST",
"/v2/deployments",
id="create_deployment",
),
pytest.param(
lambda c: c.get_deployment("dep-1"),
"GET",
"/v2/deployments/dep-1",
id="get_deployment",
),
pytest.param(
lambda c: c.delete_deployment("dep-1"),
"DELETE",
"/v2/deployments/dep-1",
id="delete_deployment",
),
pytest.param(
lambda c: c.update_deployment("dep-1", "img", revision_source=None),
"PATCH",
"/v2/deployments/dep-1",
id="patch_deployment",
),
pytest.param(
lambda c: c.request_push_token("dep-1"),
"POST",
"/v2/deployments/dep-1/push-token",
id="push_token",
),
pytest.param(
lambda c: c.request_upload_url("dep-1"),
"POST",
"/v2/deployments/dep-1/upload-url",
id="upload_url",
),
pytest.param(
lambda c: c.list_revisions("dep-1", limit=5),
"GET",
"/v2/deployments/dep-1/revisions?limit=5",
id="list_revisions_puts_limit_in_query",
),
pytest.param(
lambda c: c.get_revision("dep-1", "rev-2"),
"GET",
"/v2/deployments/dep-1/revisions/rev-2",
id="get_revision",
),
pytest.param(
lambda c: c.get_build_logs("dep-1", "rev-2", {"limit": 10}),
"POST",
"/v1/projects/dep-1/revisions/rev-2/build_logs",
id="build_logs",
),
],
)
def test_request_targets_control_plane_route_under_base_url(call, method, route):
seen: dict = {}
call(_routing_client(seen))
assert (seen["method"], seen["url"]) == (
method,
f"https://api.example.com/prefix{route}",
)
def test_injected_transport_receives_requests_under_the_prefixed_base_url():
seen: dict = {}
def handler(req: httpx.Request) -> httpx.Response:
seen["url"] = str(req.url)
seen["api_key"] = req.headers["x-api-key"]
return httpx.Response(200, json={"ok": True})
c = HostBackendClient(
"https://smith.example.com/api-host",
"key",
transport=httpx.MockTransport(handler),
)
assert c.list_revisions("dep-1", limit=2) == []
assert seen == {
"url": "https://smith.example.com/api-host/v2/deployments/dep-1/revisions?limit=2",
"api_key": "key",
}
CLOUD = ("https://api.host.langchain.com", "https://smith.langchain.com")
@pytest.mark.parametrize(
("host_url", "langsmith_endpoint", "expected"),
[
pytest.param(None, None, CLOUD, id="nothing_configured_targets_cloud"),
pytest.param(
None, "https://api.smith.langchain.com", CLOUD, id="cloud_langsmith_api"
),
pytest.param(
None,
"https://api.smith.langchain.com/api/v1",
CLOUD,
id="cloud_langsmith_api_with_versioned_path",
),
pytest.param(
None, "https://api.langchain.com", CLOUD, id="cloud_langchain_api_alias"
),
pytest.param(
None,
"https://xapi.smith.langchain.com",
CLOUD,
id="lookalike_cloud_host_is_not_rewritten_into_a_control_plane",
),
pytest.param(
None,
"https://eu.api.smith.langchain.com",
("https://eu.api.host.langchain.com", "https://eu.smith.langchain.com"),
id="eu_cloud_maps_to_eu_control_plane",
),
pytest.param(
None,
"https://dev.api.smith.langchain.com",
("https://dev.api.host.langchain.com", "https://dev.smith.langchain.com"),
id="dev_cloud_maps_to_dev_control_plane",
),
pytest.param(
None,
"https://aks.smith.langchain.dev/api",
(
"https://aks.smith.langchain.dev/api-host",
"https://aks.smith.langchain.dev",
),
id="self_hosted_api_path_becomes_api_host",
),
pytest.param(
None,
"https://smith.example.com/api/v1",
("https://smith.example.com/api-host", "https://smith.example.com"),
id="self_hosted_versioned_api_path_becomes_api_host",
),
pytest.param(
None,
"https://smith.example.com",
("https://smith.example.com/api-host", "https://smith.example.com"),
id="self_hosted_origin_gets_api_host_appended",
),
pytest.param(
None,
"https://corp.example.com/langsmith/api/v1",
(
"https://corp.example.com/langsmith/api-host",
"https://corp.example.com/langsmith",
),
id="self_hosted_path_prefix_is_kept",
),
pytest.param(
"https://custom.host.example",
"https://aks.smith.langchain.dev/api",
("https://custom.host.example", "https://smith.langchain.com"),
id="explicit_host_url_beats_langsmith_endpoint",
),
pytest.param(
"https://api.host.langchain.com",
"https://aks.smith.langchain.dev/api",
CLOUD,
id="explicit_cloud_host_url_beats_self_hosted_endpoint",
),
pytest.param(
"https://smith.example.com/api-host/",
None,
("https://smith.example.com/api-host", "https://smith.example.com"),
id="explicit_api_host_url_derives_dashboard_root",
),
pytest.param(
"https://corp.example.com/langsmith/api-host",
None,
(
"https://corp.example.com/langsmith/api-host",
"https://corp.example.com/langsmith",
),
id="explicit_api_host_url_keeps_path_prefix_in_dashboard",
),
pytest.param(
"http://localhost:8080",
None,
("http://localhost:8080", "http://localhost:8080"),
id="localhost_dashboard_is_the_same_origin",
),
pytest.param(
"http://localhost:8080/api-host",
None,
("http://localhost:8080/api-host", "http://localhost:8080"),
id="localhost_api_host_dashboard_is_the_origin",
),
pytest.param(
"https://eu.api.host.langchain.com",
None,
("https://eu.api.host.langchain.com", "https://eu.smith.langchain.com"),
id="regional_control_plane_maps_to_regional_dashboard",
),
],
)
def test_control_plane_endpoints_resolve(host_url, langsmith_endpoint, expected):
endpoints = ControlPlaneEndpoints.resolve(host_url, langsmith_endpoint)
assert (endpoints.control_plane_url, endpoints.dashboard_url) == expected
@pytest.mark.parametrize(
("payload", "expected"),
[
pytest.param(
{"resources": [{"id": "a"}, {"id": "b"}]},
[{"id": "a"}, {"id": "b"}],
id="list_returns_the_resources",
),
pytest.param({"resources": []}, [], id="empty_list"),
pytest.param({}, [], id="missing_key"),
pytest.param({"resources": None}, [], id="null_resources"),
pytest.param(
{"resources": ["nope", {"id": "a"}]}, [{"id": "a"}], id="skips_non_objects"
),
pytest.param([], [], id="unexpected_envelope"),
],
)
def test_list_endpoints_return_resource_objects(payload, expected):
def handler(req: httpx.Request) -> httpx.Response:
return httpx.Response(200, json=payload)
c = HostBackendClient(
"https://api.example.com", "key", transport=httpx.MockTransport(handler)
)
assert c.list_deployments() == expected
def test_list_listeners_asks_for_a_full_page():
seen: dict = {}
def handler(req: httpx.Request) -> httpx.Response:
seen["url"] = str(req.url)
return httpx.Response(200, json={"resources": [{"id": "listener-1"}]})
c = HostBackendClient(
"https://api.example.com", "key", transport=httpx.MockTransport(handler)
)
assert c.list_listeners() == [{"id": "listener-1"}]
assert seen["url"] == "https://api.example.com/v2/listeners?limit=100"
@pytest.mark.parametrize(
("control_plane_url", "expected"),
[
pytest.param("https://api.host.langchain.com", True, id="cloud"),
pytest.param("https://eu.api.host.langchain.com", True, id="cloud_region"),
pytest.param("https://dev.api.host.langchain.com", True, id="cloud_dev"),
pytest.param("https://smith.example.com/api-host", False, id="self_hosted"),
pytest.param(
"https://corp.example.com/langsmith/api-host",
False,
id="self_hosted_prefix",
),
pytest.param("http://localhost:8080/api-host", False, id="local"),
pytest.param(
"https://evil-api.host.langchain.com", False, id="lookalike_needs_a_dot"
),
],
)
def test_is_cloud_recognises_the_managed_control_plane(control_plane_url, expected):
endpoints = ControlPlaneEndpoints.from_control_plane_url(control_plane_url)
assert endpoints.is_cloud is expected
@pytest.mark.parametrize(
("call", "expected_params"),
[
pytest.param(
lambda c: c.list_deployments(name="agent"),
{"name": "agent"},
id="exact_name_filters_server_side",
),
pytest.param(
lambda c: c.list_deployments(name_contains="age"),
{"name_contains": "age"},
id="substring_search_keeps_its_own_parameter",
),
pytest.param(
lambda c: c.list_deployments(),
{},
id="no_filter_sends_no_parameters",
),
pytest.param(
lambda c: c.list_deployments(
name="agent", name_contains="agent", limit=100
),
{"name": "agent", "name_contains": "agent", "limit": "100"},
id="both_filters_travel_together_for_older_servers",
),
],
)
def test_list_deployments_sends_one_name_filter(call, expected_params):
seen: dict = {}
def handler(req: httpx.Request) -> httpx.Response:
seen.update(dict(req.url.params))
return httpx.Response(200, json={"resources": []})
call(
HostBackendClient(
"https://api.example.com", "key", transport=httpx.MockTransport(handler)
)
)
assert seen == expected_params
@pytest.mark.parametrize(
("body", "expected"),
[
pytest.param(
{"detail": "Source configuration error: bad listener"},
"Source configuration error: bad listener",
id="fastapi_detail_is_unwrapped",
),
pytest.param(
{"detail": {"loc": ["body"], "msg": "nope"}},
None,
id="a_structured_detail_is_left_alone",
),
pytest.param({"other": "shape"}, None, id="an_unknown_shape_is_left_alone"),
],
)
def test_error_detail_is_readable(body, expected):
c = HostBackendClient(
"https://api.example.com",
"key",
transport=httpx.MockTransport(lambda req: httpx.Response(400, json=body)),
)
with pytest.raises(HostBackendError) as error:
c.get_deployment("dep-1")
assert error.value.detail == expected
if expected is not None:
assert error.value.message.endswith(expected)
@@ -0,0 +1,71 @@
import pytest
from langgraph_cli.image_reference import ImageReference
@pytest.mark.parametrize(
("reference", "repository", "tag"),
[
pytest.param(
"registry.example.com/team/app:v1",
"registry.example.com/team/app",
"v1",
id="tag_after_last_slash",
),
pytest.param(
"registry.example.com/team/app",
"registry.example.com/team/app",
None,
id="no_tag",
),
pytest.param(
"localhost:5000/app",
"localhost:5000/app",
None,
id="registry_port_is_not_a_tag",
),
pytest.param(
"localhost:5000/app:latest",
"localhost:5000/app",
"latest",
id="registry_port_with_tag",
),
pytest.param("app:dev", "app", "dev", id="bare_name_with_tag"),
],
)
def test_parse_splits_repository_and_tag(reference, repository, tag):
assert ImageReference.parse(reference) == ImageReference(repository, tag)
def test_with_tag_replaces_the_tag():
assert ImageReference("r/app", "v1").with_tag("v2") == ImageReference("r/app", "v2")
@pytest.mark.parametrize(
("reference", "expected"),
[
pytest.param(ImageReference("r/app", "v1"), "r/app:v1", id="tagged"),
pytest.param(ImageReference("r/app"), "r/app", id="untagged"),
],
)
def test_str_renders_the_docker_reference(reference, expected):
assert str(reference) == expected
@pytest.mark.parametrize(
("repo_digest", "expected"),
[
pytest.param("localhost:5000/app@sha256:abc", True, id="same_repository"),
pytest.param("localhost:5000/app-2@sha256:abc", False, id="other_repository"),
pytest.param("mirror.example.com/app@sha256:abc", False, id="other_registry"),
],
)
def test_matches_digest_only_for_the_same_repository(repo_digest, expected):
assert ImageReference("localhost:5000/app", "v1").matches_digest(repo_digest) is (
expected
)
def test_parse_rejects_a_digest_reference():
with pytest.raises(ValueError, match="digest"):
ImageReference.parse("registry.example.com/app@sha256:abc")
+118 -15
View File
@@ -39,15 +39,15 @@ wheels = [
[[package]]
name = "anyio"
version = "4.13.0"
version = "4.14.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
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" }
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" }
wheels = [
{ 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" },
{ 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" },
]
[[package]]
@@ -217,9 +217,10 @@ wheels = [
[[package]]
name = "langchain-core"
version = "1.3.3"
version = "1.6.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
{ name = "jsonpatch" },
{ name = "langchain-protocol" },
{ name = "langsmith" },
@@ -230,9 +231,9 @@ dependencies = [
{ name = "typing-extensions" },
{ name = "uuid-utils" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d3/ae/8b74458fc3850ec3d150eb9f45e857db129dafa801fb5cf173dfc9f8bbf3/langchain_core-1.3.3.tar.gz", hash = "sha256:fa510a5db8efdc0c6ff41c0939fb5c00a0183c11f6b84233e892e3227ff69182", size = 915041, upload-time = "2026-05-05T19:02:36.612Z" }
sdist = { url = "https://files.pythonhosted.org/packages/89/7b/406b4a8dd43dd01f69e04aafdee206809a2097134b2f944e12802eae7948/langchain_core-1.6.2.tar.gz", hash = "sha256:1ec6d3a98f7c8cdbb5bb0deff86e7ca37e16bf4f8f49f022d10723656c62352d", size = 1004390, upload-time = "2026-09-04T21:29:22.769Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1f/01/4771b7ab2af1d1aba5b710bd8f13d9225c609425214b357590a17b01be77/langchain_core-1.3.3-py3-none-any.whl", hash = "sha256:18aae8506f37da7f74398492279a7d6efcee4f8e23c4c41c7af080eeb7ef7bd1", size = 543857, upload-time = "2026-05-05T19:02:34.52Z" },
{ url = "https://files.pythonhosted.org/packages/a8/62/d3fb7c215cb2f237c3fe84880bf347a38deafef6033b6d5f1339ba8ca401/langchain_core-1.6.2-py3-none-any.whl", hash = "sha256:21e6c7cf097c68b777fd69ea00864f09bd9ca76ac73e3e3fa14e1ee366eb4711", size = 571690, upload-time = "2026-09-04T21:29:21.175Z" },
]
[[package]]
@@ -249,7 +250,7 @@ wheels = [
[[package]]
name = "langgraph"
version = "1.1.6"
version = "1.2.11"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "langchain-core" },
@@ -259,9 +260,9 @@ dependencies = [
{ name = "pydantic" },
{ name = "xxhash" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5c/e5/d3f72ead3c7f15769d5a9c07e373628f1fbaf6cbe7735694d7085859acf6/langgraph-1.1.6.tar.gz", hash = "sha256:1783f764b08a607e9f288dbcf6da61caeb0dd40b337e5c9fb8b412341fbc0b60", size = 549634, upload-time = "2026-04-03T19:01:32.561Z" }
sdist = { url = "https://files.pythonhosted.org/packages/56/0d/c8e7ee98896659e1b6555db0ab115a9ca899844744645d5d894032bab1d7/langgraph-1.2.11.tar.gz", hash = "sha256:9ecfe11e50d338b34b15cf4d8a442642de103e8ae6971320efba84e4542eb363", size = 725753, upload-time = "2026-08-11T14:00:36.945Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/71/e6/b36ecdb3ff4ba9a290708d514bae89ebbe2f554b6abbe4642acf3fddbe51/langgraph-1.1.6-py3-none-any.whl", hash = "sha256:fdbf5f54fa5a5a4c4b09b7b5e537f1b2fa283d2f0f610d3457ddeecb479458b9", size = 169755, upload-time = "2026-04-03T19:01:30.686Z" },
{ url = "https://files.pythonhosted.org/packages/0a/7f/c5c30e4be99ff821029c7ac872a480676bb179c9f3df85ea3f38d13f86d4/langgraph-1.2.11-py3-none-any.whl", hash = "sha256:8bab70de7b2d00b5300fb289bcf38d8b241400f3184c1e95e8ce706fb0e8686b", size = 248854, upload-time = "2026-08-11T14:00:35.494Z" },
]
[[package]]
@@ -294,28 +295,31 @@ wheels = [
[[package]]
name = "langgraph-prebuilt"
version = "1.0.9"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "langchain-core" },
{ name = "langgraph-checkpoint" },
]
sdist = { url = "https://files.pythonhosted.org/packages/99/4c/06dac899f4945bedb0c3a1583c19484c2cc894114ea30d9a538dd270086e/langgraph_prebuilt-1.0.9.tar.gz", hash = "sha256:93de7512e9caade4b77ead92428f6215c521fdb71b8ffda8cd55f0ad814e64de", size = 165850, upload-time = "2026-04-03T14:06:37.721Z" }
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 = [
{ url = "https://files.pythonhosted.org/packages/1d/a2/8368ac187b75e7f9d938ca075d34f116683f5cfc48d924029ee79aea147b/langgraph_prebuilt-1.0.9-py3-none-any.whl", hash = "sha256:776c8e3154a5aef5ad0e5bf3f263f2dcaab3983786cc20014b7f955d99d2d1b2", size = 35958, upload-time = "2026-04-03T14:06:36.58Z" },
{ url = "https://files.pythonhosted.org/packages/e9/43/3fe1a700b8490ed02679cdbbc8c915eb23a092faf496c9c1118abcd10be3/langgraph_prebuilt-1.1.0-py3-none-any.whl", hash = "sha256:51e311747d755b751d5c6b39b0c1446124d3a7643d2515017e6714b323508fc9", size = 41043, upload-time = "2026-05-12T03:37:48.007Z" },
]
[[package]]
name = "langgraph-sdk"
version = "0.3.13"
version = "0.4.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
{ name = "langchain-core" },
{ name = "langchain-protocol" },
{ name = "orjson" },
{ name = "websockets" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0e/db/77a45127dddcfea5e4256ba916182903e4c31dc4cfca305b8c386f0a9e53/langgraph_sdk-0.3.13.tar.gz", hash = "sha256:419ca5663eec3cec192ad194ac0647c0c826866b446073eb40f384f950986cd5", size = 196360, upload-time = "2026-04-07T20:34:18.766Z" }
sdist = { url = "https://files.pythonhosted.org/packages/3f/ae/91446c1fffa04a2dc1f81afbfb5bfff3590452891a72bd9098a656ab2657/langgraph_sdk-0.4.4.tar.gz", hash = "sha256:4e651ffa09de695681579396375377bdde23bedbc8e35b070e615cbd5af7da8b", size = 345789, upload-time = "2026-08-27T21:25:22.593Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fe/ef/64d64e9f8eea47ce7b939aa6da6863b674c8d418647813c20111645fcc62/langgraph_sdk-0.3.13-py3-none-any.whl", hash = "sha256:aee09e345c90775f6de9d6f4c7b847cfc652e49055c27a2aed0d981af2af3bd0", size = 96668, upload-time = "2026-04-07T20:34:17.866Z" },
{ url = "https://files.pythonhosted.org/packages/43/d5/2ad7f3a835c6fcebf58b6669552536ecd52d2dfe4cf49c6c36648fd83572/langgraph_sdk-0.4.4-py3-none-any.whl", hash = "sha256:39afe416c91742925e6f8a93715f566d499b36e1b636b804a4ffe3190e4f4e64", size = 162270, upload-time = "2026-08-27T21:25:21.072Z" },
]
[[package]]
@@ -781,6 +785,105 @@ requires-dist = [
{ name = "langgraph-checkpoint-postgres", specifier = ">=3.0.0" },
]
[[package]]
name = "websockets"
version = "16.1.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/21/f7/bc3a25c5ec26ce62ce487690becc2f3710bbc7b33338f005ad390db0b986/websockets-16.1.1.tar.gz", hash = "sha256:db234eda965dcce15df96bb9709f587cd87d4d52aaf0e80e2f34ec04c7670c57", size = 182204, upload-time = "2026-07-17T22:51:05.858Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2b/03/47debfe28e9d6d354be5d777b67fd44c359b9eb299a5d103500bd7cc3e37/websockets-16.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d0fcf657e9f13ff4b177960ab2200237b12994232dfb6df16f1cfe1d4339f93c", size = 179566, upload-time = "2026-07-17T22:48:49.596Z" },
{ url = "https://files.pythonhosted.org/packages/72/93/31efa1ed78c17e5cfc229fd449e3966e1b9cc15753204cd585cc8dd01f4a/websockets-16.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b852788aa51764e2d8e4cf5493d559326bcae5e38d16ba25ffa322b034df272a", size = 177250, upload-time = "2026-07-17T22:48:50.942Z" },
{ url = "https://files.pythonhosted.org/packages/01/4a/542378ab3972b0c1cf1df3df3eff9591cea0d30c58c3aa3c4ddbc244e787/websockets-16.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1427fb4cf0d72f66333e2cacc3ff5f575bf2d7008166ce991a4a470b21d51a22", size = 177528, upload-time = "2026-07-17T22:48:52.59Z" },
{ url = "https://files.pythonhosted.org/packages/33/d9/162321f63c7eed558e9e1798ed7a1e34a4f6dab51f35419e4ed7a4907979/websockets-16.1.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:da4ca1a9d72f9030b3146b8d7022719a9f3d478f61efe6f7dd51d243f61c51b2", size = 186859, upload-time = "2026-07-17T22:48:53.915Z" },
{ url = "https://files.pythonhosted.org/packages/de/09/87df740f7430ce564bd52402e9c9458d4d0459cc7d2ee29e530c8204851b/websockets-16.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86d7f0f8bdb25d2c632b72527325e4776430fd5bc61b9118de4e2b8ddb5f5b01", size = 188095, upload-time = "2026-07-17T22:48:55.384Z" },
{ url = "https://files.pythonhosted.org/packages/d2/12/3d2703af7cc095f3c81904c92208cc1ae79affbc67376944b50ee9301f73/websockets-16.1.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7dfcad78ea1492ee3a9ec765cb7f51bbc17d477107aaf6b22abf7b2558d1c5a0", size = 191385, upload-time = "2026-07-17T22:48:56.742Z" },
{ url = "https://files.pythonhosted.org/packages/1d/69/986aa0234a964a00f5149cfc46e136e96c8faad1c783474550f40d31aef4/websockets-16.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fb9a0a6dc3d1b3986cb88091b6899f0396651e0f74e2c9766ab8d6ffc3842e29", size = 188653, upload-time = "2026-07-17T22:48:58.134Z" },
{ url = "https://files.pythonhosted.org/packages/35/6b/10f9d03e3970a69ba67bd3b46b87a929b586d0300fadbfe14f57c1f85490/websockets-16.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29dfa8114c4a620c69591c5973860f768eac29d3fd6904f37f34266cb219c512", size = 187426, upload-time = "2026-07-17T22:48:59.515Z" },
{ url = "https://files.pythonhosted.org/packages/56/db/bb3aad62bf63d8bb3f0634b2eabffcfb3677a34bd19492110ff6869cf703/websockets-16.1.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ff9417c0ada4d0f7d212f928303e5579bdf3ace4c802fa4afabb30995da58c3", size = 184882, upload-time = "2026-07-17T22:49:00.916Z" },
{ url = "https://files.pythonhosted.org/packages/6c/4c/c09a2ea9bfbeccce52fdc383e5f28af4bc8843338aabac28c81489af6120/websockets-16.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fe0b50da2d84535fb4f7b4bfa951280f97ce3d558a0443b541166d609e67b57", size = 187584, upload-time = "2026-07-17T22:49:02.283Z" },
{ url = "https://files.pythonhosted.org/packages/c7/8b/31bb4eb4d9eaacf1fdd39d115772a8aeaedfc19b5dc262e57ffbc8a9d42c/websockets-16.1.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:34420aaa64440ebd51ac72ca8a45ef4626429438c9b02e633ae412ed43f925d3", size = 186174, upload-time = "2026-07-17T22:49:03.973Z" },
{ url = "https://files.pythonhosted.org/packages/2f/e4/dc02d725610a1ad49e193ef91a548194d71bdc6cdf27da83067dd1f73995/websockets-16.1.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a6a61aff018180c9c50b7b0da33bfd29d378af3497429c95006c589a23a11648", size = 187986, upload-time = "2026-07-17T22:49:05.553Z" },
{ url = "https://files.pythonhosted.org/packages/e0/73/30ed84c8bfd14c73d4af29d5ed9323c3073b48e0b7b23b67070f4e7fd59b/websockets-16.1.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:04fd29a0e2fe9414a95b00e92c67ae51bf900c50c0f8a4b2dafdad621f49ea1d", size = 185565, upload-time = "2026-07-17T22:49:06.959Z" },
{ url = "https://files.pythonhosted.org/packages/7d/d3/4be8d4959f51e31b4f8fc0ece12b45bd3b6c0d15ea23b9990d9c11fc805f/websockets-16.1.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5c31aa7e39ee3e8a358573257f1c0bb5c52430d1b637030dd9c8cc2c282926be", size = 186598, upload-time = "2026-07-17T22:49:08.293Z" },
{ url = "https://files.pythonhosted.org/packages/26/fa/abb38597a52d84ed9cfacadc7a0c6f2db282c0ab23cdf72b58a666a21227/websockets-16.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d14bfb217eb4701e850f1525c9d29d79c44794cdf1c299ead25f39f8c78dea81", size = 186834, upload-time = "2026-07-17T22:49:09.766Z" },
{ url = "https://files.pythonhosted.org/packages/59/80/1119ad08a228b90c4eb77fbe48df7836731a605f5f881ba701ca826a4a65/websockets-16.1.1-cp311-cp311-win32.whl", hash = "sha256:2e28e602bb13da44fbe518c1781a88e3b9d4c3d48d02c9bad83e546164336f57", size = 179940, upload-time = "2026-07-17T22:49:11.196Z" },
{ url = "https://files.pythonhosted.org/packages/71/b2/e511c1c6f64a95c2f3fc54bffda0e14eaa7e9442be605c29270f7589b918/websockets-16.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:7421fad442de870a8cbf2287d1cad7e706ece0dbfeba5e911df132cbdc1cb56a", size = 180239, upload-time = "2026-07-17T22:49:12.519Z" },
{ url = "https://files.pythonhosted.org/packages/17/9d/681cda21c9eee743203a6cb79b9d3d05adad9aa60ec660c6c9bf4dd619ca/websockets-16.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cc97814dfb786a83b6e2dc2e79351e1b83e6d715647d6887fcabd83026417a00", size = 179600, upload-time = "2026-07-17T22:49:13.92Z" },
{ url = "https://files.pythonhosted.org/packages/fb/8d/6195a88b45e8d2a8f745fc2046e36f885a3c9763e6767d2c46229bf9510c/websockets-16.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e047dc87ef7ca50f4d309bf775ad4a71711c58556d75d7bd0604b2317f43e94b", size = 177272, upload-time = "2026-07-17T22:49:15.453Z" },
{ url = "https://files.pythonhosted.org/packages/73/e3/fe2d498c64dea0095c9a9f9a351af4cd6eef31b618395582bc1f38ba45ff/websockets-16.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:01fbdcbac298efe19360b94bc0039c8f746f0220ba570f327577bfee81059175", size = 177542, upload-time = "2026-07-17T22:49:16.875Z" },
{ url = "https://files.pythonhosted.org/packages/fe/ed/f1831681fce0e3242346e5458486003c5f124ed69e5e0b847fd029db4973/websockets-16.1.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0f62863e8a00a6d33c3d6566ec0b89f23787b747ffe0c3bc71ec0e76b82c94b1", size = 187137, upload-time = "2026-07-17T22:49:18.323Z" },
{ url = "https://files.pythonhosted.org/packages/6f/79/4ff9dcc1bb46f6b4c536936dde1fd60f9b564f3304307274db97f4c9496d/websockets-16.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8087e82f842609734c9b5a1330464f8e94e346ba0e18c832c08bafa4b0d63c15", size = 188374, upload-time = "2026-07-17T22:49:19.65Z" },
{ url = "https://files.pythonhosted.org/packages/62/c3/5c49b6efb36cab733d23773f6de575e1dba65736ead17d5d2b2a1daef779/websockets-16.1.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2bb5d041a8307d2e18782e7ce777f6fdb1e8c2f5d09291484b18c294b789d9aa", size = 191155, upload-time = "2026-07-17T22:49:21.331Z" },
{ url = "https://files.pythonhosted.org/packages/6e/f6/56ccceda3a4838d18f1d40821480da4775397e8b1eecf4031e20c50e2e90/websockets-16.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1db4de4a0e95673f7545d393c49eeb0c2f18ac1ef93073218c79d5cdb2ee75ab", size = 189011, upload-time = "2026-07-17T22:49:22.889Z" },
{ url = "https://files.pythonhosted.org/packages/86/d6/ad5286241a2bce1107e2798d3bfbd62cf79aee167bdb654f8cb1e9dbf949/websockets-16.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f17dbe07eb3ea7f99e4df9b7e0efefe80fbf30d37a8cc4d561a0aed310bc8847", size = 187766, upload-time = "2026-07-17T22:49:24.339Z" },
{ url = "https://files.pythonhosted.org/packages/bc/67/d65c970b7e347fdca69479beb7811c2060529956730a7a4e3ae7c66b0e31/websockets-16.1.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4b57693728576d84ede0a77987ab16881b783d2cd9f1dc180a8fbbc3f79c4428", size = 185173, upload-time = "2026-07-17T22:49:25.743Z" },
{ url = "https://files.pythonhosted.org/packages/1d/5b/14af3cd4ee69d8ea9baca58f3dc3cfb1ba78332a347fd478cb096549d60e/websockets-16.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2a636ff1e7a5c4edf71ef0e79adae7f25dba93b4fcbe3dc958733477ffeb0eaf", size = 187809, upload-time = "2026-07-17T22:49:27.147Z" },
{ url = "https://files.pythonhosted.org/packages/7b/11/be301710d70de97e3e7b3586e6d492c9c06d6a61bf1c2202c36cf0c75607/websockets-16.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d6bec75c290fe484a8ba4cacdf838501e17c06ecfbbf31eede81a9e431bd7751", size = 186412, upload-time = "2026-07-17T22:49:28.611Z" },
{ url = "https://files.pythonhosted.org/packages/db/07/fe1435bf6fe738a3d3b54dbe0c18dabf12cba4d909ac8b58b539ce27c1f4/websockets-16.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:54509b8e92fee4453e152b7558ddef37ce9705a044922f2095a6105e3f80c96f", size = 188290, upload-time = "2026-07-17T22:49:29.965Z" },
{ url = "https://files.pythonhosted.org/packages/8a/0a/81f394aff8efcbb01208c1ced77df0a3c7fcce584a88c7273663697946c2/websockets-16.1.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:f0aa4aad3b1b69ad3fd85a0fd0952ec64331c762bd77ec51cc814170873890b2", size = 185844, upload-time = "2026-07-17T22:49:31.447Z" },
{ url = "https://files.pythonhosted.org/packages/39/5c/dd485b995473f415510251fe9bd708f2d24458f439fce958daf8d66dc7c6/websockets-16.1.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:42290eb6db4ccaca7012656738214f8514082fb6fa40cdeb61bb9a471b52e383", size = 186823, upload-time = "2026-07-17T22:49:33.104Z" },
{ url = "https://files.pythonhosted.org/packages/9d/0b/f78de76ff446f1e66af12b43c48a35f31744de93cfdec2f4ea67d5d7bbf1/websockets-16.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:53260c8930da5771cec89439bff99c20c8cb03ddb9588b980697355a83cd4bd3", size = 187102, upload-time = "2026-07-17T22:49:34.616Z" },
{ url = "https://files.pythonhosted.org/packages/37/a1/4cf892007778eaf84ad162bfc98046e0ed89b63ac55949e3236626b2a23f/websockets-16.1.1-cp312-cp312-win32.whl", hash = "sha256:1d27fa8462ad6a1cb36206a3d0640b2333340def181fae11ed7f9adeaa5c0747", size = 179943, upload-time = "2026-07-17T22:49:36.213Z" },
{ url = "https://files.pythonhosted.org/packages/d9/de/6abe251d28c3a3f217096575400b27750b18e0b1d2fff3a2a239960fea07/websockets-16.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:b436f6ec4fc3a6b4237c84d3f83170ed2b40bb584222f0ac47a0c8a5921980c7", size = 180243, upload-time = "2026-07-17T22:49:37.626Z" },
{ url = "https://files.pythonhosted.org/packages/ce/fd/6ec6c6d2850aea25b1b2aa9901a016980bb87d01e89b3eb00470b1b5d471/websockets-16.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ab59169ace05dcb49a1d4118f0bde139557adf45091bd85747e36bf5de984dd1", size = 179587, upload-time = "2026-07-17T22:49:38.959Z" },
{ url = "https://files.pythonhosted.org/packages/5f/d8/1d299d2dd34087db39831a34cc645ef8a6f89d78efada6983093513cd81c/websockets-16.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5e3b7d601f6f84156b08cc4a5e541c2b50ad7b36cfc302b657a12477c904a5df", size = 177272, upload-time = "2026-07-17T22:49:40.293Z" },
{ url = "https://files.pythonhosted.org/packages/3d/86/0a70d3ae2f0f2256bb41302d9804dbca65d4360281e7feb3e1f94102ac46/websockets-16.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cd2ca96a082a36964aca83e992f72abeb61b7306c1a6cba4c7d06a7b93750cac", size = 177530, upload-time = "2026-07-17T22:49:41.786Z" },
{ url = "https://files.pythonhosted.org/packages/b5/c2/c676c69444d9db448b3f0a55a98dcc534affce0bce961d9d2f0b8499b10a/websockets-16.1.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f5d497865f05bb222cab7016c6034542e84e5f29f49c6fd3f4939cda7197b5b8", size = 187197, upload-time = "2026-07-17T22:49:43.658Z" },
{ url = "https://files.pythonhosted.org/packages/0b/13/88137fbaf726ebe29d62c1117fa11fa2bbb6209dc79d4ad738efbe36a2aa/websockets-16.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bae954c382e013d5ea5b190d2830526bfa45ad121c326da0049b8c769f185db6", size = 188433, upload-time = "2026-07-17T22:49:45.147Z" },
{ url = "https://files.pythonhosted.org/packages/01/6d/46c2f2ce6751cb26f39293e1ecbf8544cb01321397cd476c2756b98c216d/websockets-16.1.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e09f753a169951eb4f28c2c774f71069304f66e7277e0f5a2892423599cfa854", size = 189868, upload-time = "2026-07-17T22:49:46.581Z" },
{ url = "https://files.pythonhosted.org/packages/29/2b/170a9e8097636cfde4dc3c592b6e00b18a44a2f5407606d96ca542dd5838/websockets-16.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:024193f8551a2b0eafbdd160911012c4e6c228c28430c84433253299a9e42d6a", size = 189059, upload-time = "2026-07-17T22:49:47.972Z" },
{ url = "https://files.pythonhosted.org/packages/a7/48/f0d4ebc9ab4b473b8861b9e20fdb663d515d42f7befdf62cdb60fee7a1ec/websockets-16.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:aabe464bfd13bd25f4821faf111da6fefdc389f870265a53105580e45b0a2e49", size = 187814, upload-time = "2026-07-17T22:49:49.344Z" },
{ url = "https://files.pythonhosted.org/packages/d5/ba/39a41d3ae8e72696a9492581900611c5a91e2b07563b0bcd2523adea9854/websockets-16.1.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a28fcbc9b6baf54a2e23f8655f308e4ccc6afdd7266f8fe7954f320dcda0f785", size = 185229, upload-time = "2026-07-17T22:49:50.787Z" },
{ url = "https://files.pythonhosted.org/packages/3c/36/ac15b604f850d1907f0a85ed721cefe47cd45034b3620069b829746cccbe/websockets-16.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79eace538c6a97e96d0d03d4f9d314f9677f5ed85a8a984992ffd90b13cb8a56", size = 187874, upload-time = "2026-07-17T22:49:52.228Z" },
{ url = "https://files.pythonhosted.org/packages/a8/f3/3fbd5d71d59299c3770faa5884d4f45070236ca5a35ab3a61830812c409a/websockets-16.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:496af849a472b531f758dbd4d61338f5000538cb1a7b3d20d9d32a264517f509", size = 186469, upload-time = "2026-07-17T22:49:53.776Z" },
{ url = "https://files.pythonhosted.org/packages/b4/fc/dd90349bba58af2a53ef2ddd9c32716c81eb6d59a0687939fff561860878/websockets-16.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5283810d2646741a0d8da2aa733d6aefa0545809afccb2a5d105a26bc45125f1", size = 188347, upload-time = "2026-07-17T22:49:55.202Z" },
{ url = "https://files.pythonhosted.org/packages/4c/f3/f73ba86427682da59b78c11d77ba56d5b801c32e84afe79b274bbd6a9bb2/websockets-16.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4e3b680b1e0a27457e727a0d572fd81dffa87b6dbf8b228ab57da64f7d85aead", size = 185903, upload-time = "2026-07-17T22:49:56.75Z" },
{ url = "https://files.pythonhosted.org/packages/34/7c/f95eb20e80104173b3a0a092291f89ea4047ef6e608e0a57ca06eb14eecb/websockets-16.1.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:69159730a823dde3ea8d08783e8d47ef135a6d7e8d44eb127e32b321c9db8e3e", size = 186855, upload-time = "2026-07-17T22:49:58.467Z" },
{ url = "https://files.pythonhosted.org/packages/b0/35/dd875b3e050ff232d60fa377707f890e369f74d134f1be32e8f68879747c/websockets-16.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ed5bb271084b46530ee2ddc0410537a9961152c5ccba2fc98c5276d992ccba87", size = 187140, upload-time = "2026-07-17T22:50:00.016Z" },
{ url = "https://files.pythonhosted.org/packages/e8/dc/5cbfcb41824502f6af93b8f3943a4d06c67c23c7d2e31eb18748c4a5b2a7/websockets-16.1.1-cp313-cp313-win32.whl", hash = "sha256:cfb70b4eb56cac4da0a83588f3ad50d46beb0690391082f3d4e2d488c70b68ea", size = 179928, upload-time = "2026-07-17T22:50:01.685Z" },
{ url = "https://files.pythonhosted.org/packages/b0/c1/71e5deb5b7f8f226997ab64908c184ac3105c0155ce2d486f318e5dd08a8/websockets-16.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:d9531d9cbeac99af6f038fb1bc351403531f7d634a2c2e10e2f7c854c6ed5b68", size = 180242, upload-time = "2026-07-17T22:50:03.117Z" },
{ url = "https://files.pythonhosted.org/packages/73/a2/ba78a164eeea4620df4a4df4bd2ed6017438c4655cc0f36f2c0bc0432355/websockets-16.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:443aefe96b7fdb132e2a70806cca1f2af49bb3f28e47abcd7c2e9dcf4d8fa1b8", size = 179635, upload-time = "2026-07-17T22:50:05.001Z" },
{ url = "https://files.pythonhosted.org/packages/b9/08/d26d7a7628cd4ac34cbbdb63ac80914ca842ed8e42938c40a53567806df3/websockets-16.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6456ff333092d509127d75a638cb411afae8ff17f092635015d1902efec8a293", size = 177320, upload-time = "2026-07-17T22:50:06.427Z" },
{ url = "https://files.pythonhosted.org/packages/0f/45/ebec83e6269536aa5932533c67b0af5c781f3e73fdbcd68672dcf43f4f44/websockets-16.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fce6c48559c86d1ac3632ccb1bebc7d5442fbe79bd9bb0e40379ee54be2a4051", size = 177544, upload-time = "2026-07-17T22:50:07.834Z" },
{ url = "https://files.pythonhosted.org/packages/c9/d5/abc614d2297f6c1c3e01e61260364457a47c25cc1cf6a879038902bc6aa8/websockets-16.1.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:92b820d345f7a3fc7b8163949ee92df910f290c3fc517b3d5301c78065adafe1", size = 187270, upload-time = "2026-07-17T22:50:09.275Z" },
{ url = "https://files.pythonhosted.org/packages/52/71/4c99af3b87dff1b2927981f6876607d4acb45338c665242168d3982f7758/websockets-16.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a606d9c24035242a3e256e9d5b77ed9cd6bccfcb7cf993e5ca3c0f6f68fb6a7", size = 188509, upload-time = "2026-07-17T22:50:10.722Z" },
{ url = "https://files.pythonhosted.org/packages/9b/b4/5c8ca14b0df7eb84ed0524165c5359150210140817a3312aee57bf62a1cf/websockets-16.1.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:414e596c75f74e0994084694189d7dc9229fb278e33064d6784b73ffbba3ca31", size = 189882, upload-time = "2026-07-17T22:50:12.293Z" },
{ url = "https://files.pythonhosted.org/packages/25/c1/bedfba9e70557129cb8083748d167bdcc01483dedf0f0df143676df05cbe/websockets-16.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:536676848fc5961aca9d20389951f59169508f765637a172403dc5434d722fa0", size = 189114, upload-time = "2026-07-17T22:50:13.789Z" },
{ url = "https://files.pythonhosted.org/packages/df/09/aa835b2787835aebd839114be5de51b797cb480b63ba42b26d34dfe147cb/websockets-16.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:97fd3a0e8b53efa41970ac1dff3d8cf0d2884cadeb4caaf95db7ad1526926ee3", size = 187861, upload-time = "2026-07-17T22:50:15.179Z" },
{ url = "https://files.pythonhosted.org/packages/20/26/f6408330694dbc9830857d9d23bc14ac4f6875127a480cfdda8d5ca21198/websockets-16.1.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7b1b19636af86a3c7995d4d028dbe376f39b4bf31541146f9c123582a6c94562", size = 185286, upload-time = "2026-07-17T22:50:16.741Z" },
{ url = "https://files.pythonhosted.org/packages/17/9a/e0675e70dd8a80762cf35bb18799d3f290a4890ffe6439bc51d222796083/websockets-16.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:41c8e77f17294c0ac18008a7309b99b34ee72247ef10b6dff4c3f8b5ac29896b", size = 187935, upload-time = "2026-07-17T22:50:18.213Z" },
{ url = "https://files.pythonhosted.org/packages/33/c1/3234cfb86afde01b81e9bddcc6e534c440975d60a13991259e833069ab3e/websockets-16.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9f63bcef7f4b02b06b35fc01c93b96c43b5e88e1e8868676caacf493d5a31f3a", size = 186444, upload-time = "2026-07-17T22:50:19.67Z" },
{ url = "https://files.pythonhosted.org/packages/89/87/9c15206e1d778923d8daa9657de07aa62ea815e13448319c98458c37b281/websockets-16.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dab9eb87869da2d6ed3af3f3adf28414baae6ec9d4df355ffc18889132f3436c", size = 188409, upload-time = "2026-07-17T22:50:21.28Z" },
{ url = "https://files.pythonhosted.org/packages/f2/00/cf5de5c67676de2d3eef8b2a518f168f6796595447a5b7161ba0d012915c/websockets-16.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:43e3a9fdd7cbf7ba6040c31fae0faf84ca1474fef777c4e37912f1540f854499", size = 185958, upload-time = "2026-07-17T22:50:22.719Z" },
{ url = "https://files.pythonhosted.org/packages/62/c0/731b6ddede2e4136912ec4cff2cffbda35af73546be4762c3d7bd3bd79af/websockets-16.1.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:056ae37939ed7e9974f364f5864e76e49182622d8f9751ac1903c0d09b013985", size = 186911, upload-time = "2026-07-17T22:50:24.108Z" },
{ url = "https://files.pythonhosted.org/packages/8c/7f/39c634472c4469a24a7c09cecddffb08fac6d0e74f73881a94ee8a40a196/websockets-16.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a0eadbbf2c30f01efa58e1f110eb6fa293261f6b0b1aa38f7f48707107690af9", size = 187204, upload-time = "2026-07-17T22:50:25.548Z" },
{ url = "https://files.pythonhosted.org/packages/26/89/9667c256c256dafcc62d21328ce7a40067da857969b68ee9af375b0aaf72/websockets-16.1.1-cp314-cp314-win32.whl", hash = "sha256:195c978b065fa40910582464f99d6b15c8b314c68e0546549a55ed83f4735328", size = 179603, upload-time = "2026-07-17T22:50:27.086Z" },
{ url = "https://files.pythonhosted.org/packages/bd/dd/1c099d6c0fc5deb6b46ccdbb6981fdb4b12c917869cb3952408409dc18db/websockets-16.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:4e8d01cc3bcae7bbf8167f944aeafefed590fae5693552bba9794a9df68371cc", size = 179948, upload-time = "2026-07-17T22:50:28.521Z" },
{ url = "https://files.pythonhosted.org/packages/35/25/9956b2d5e0529d5d23924f21bba1440d4c5c88a562e4f08550871ffa97a7/websockets-16.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0ffd3031ea8bda8d61762e84220186105ba3b748b3c8da2ae4f7816fac03e573", size = 179963, upload-time = "2026-07-17T22:50:29.982Z" },
{ url = "https://files.pythonhosted.org/packages/17/06/55ffc976c488b6aee9ea05761ff7c4e88e7c1fd82818c8ca7b556ad2f90c/websockets-16.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:84a2cef8deffbd9ab8ee0ea546a2a6a7030c28f44e6cdd4547dbfeb489eb8999", size = 177497, upload-time = "2026-07-17T22:50:31.396Z" },
{ url = "https://files.pythonhosted.org/packages/0c/e8/f7dac2e980bacc92bdc26cebae4ae4d50cae5380732c50980598fc0bbae4/websockets-16.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3df13f73af9b3b38ab1195eb299ecb67a4330c911c97ae04043ff74085728abe", size = 177698, upload-time = "2026-07-17T22:50:32.829Z" },
{ url = "https://files.pythonhosted.org/packages/b2/39/26762f734113e22da2b942c3aca85798e0c0405d64c256549540ff31e5a1/websockets-16.1.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:23253dd5bcae3f9aaee0a1d30967a8dbd52e5d3cff93a2e5b84df57b77d4750d", size = 187561, upload-time = "2026-07-17T22:50:34.24Z" },
{ url = "https://files.pythonhosted.org/packages/11/94/c3f330851806b9b02138b774d593478323e73c99238681b4b93efe64e02d/websockets-16.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1c5705e314449e3308872fe084b8571ce078ee4fc55a98a769bdefe5917392", size = 188732, upload-time = "2026-07-17T22:50:36.088Z" },
{ url = "https://files.pythonhosted.org/packages/d1/f2/eb2c450f052de334ae33cf200ece6e87b0e14d186807074e4eb1cd2cdea2/websockets-16.1.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69e52d175a0a7d1e13b4b67ad41c560b7d98e8c6f6126eb0bda496c784faf8c7", size = 190872, upload-time = "2026-07-17T22:50:38.008Z" },
{ url = "https://files.pythonhosted.org/packages/70/31/2ac8cecf3a74f7fed9132129fc3d90b3998a1554570c11a69b2a8c20332d/websockets-16.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1f79c89b5eb034d1722938a891916582f8f7f503f58ca22518a63c3f2cd18499", size = 189305, upload-time = "2026-07-17T22:50:39.53Z" },
{ url = "https://files.pythonhosted.org/packages/6a/cf/8ab19650d3c0d4562c92e70ab47c257c4aa5c6a713ed87fe63766b31fefc/websockets-16.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:39f2a024af5c345ffe8fcf1ee18c049c024c94df393bb09b044a6917c77bde43", size = 188033, upload-time = "2026-07-17T22:50:40.912Z" },
{ url = "https://files.pythonhosted.org/packages/66/d7/a49a38a6127a4acb134fb1912b215d900cc657605cff32445bf519f3acc4/websockets-16.1.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:952303a7318d4cbe1011400839bb2051c9f84fa0a35923267f5daba34b15d458", size = 185748, upload-time = "2026-07-17T22:50:42.559Z" },
{ url = "https://files.pythonhosted.org/packages/95/3e/ad1fa40388c7f2e0bb2c7930d0090b6c5498594bd1cdaec18864df3d9e97/websockets-16.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:249116b4a76063d930a46391ad56e135c286e4562a18309029fc2c73f4ed4c62", size = 188285, upload-time = "2026-07-17T22:50:43.974Z" },
{ url = "https://files.pythonhosted.org/packages/35/b8/d5db28ca264b9104f82196f92dc8843e35fd391f763d42e4ad358f5bc97e/websockets-16.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:61922544a0587a13fd3f53e4c0e5e606510c7b0d9d22c8444e5fae22a06b38cb", size = 186777, upload-time = "2026-07-17T22:50:45.474Z" },
{ url = "https://files.pythonhosted.org/packages/42/9c/726cb39d0cc43ae848dce4aa2acb04eecc6738b1264ec6d700bf6bcfb9f8/websockets-16.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:46dcaa042cd1de6c59e7d9269fa63ff7572b6df40510600b678f0826b3c7af51", size = 188682, upload-time = "2026-07-17T22:50:46.973Z" },
{ url = "https://files.pythonhosted.org/packages/be/c7/1168704de8c2dd483edabe4a22cbe4465dd8be8dd95561d214f9fe092871/websockets-16.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:38565aca3e01ea8734e578fb2118dade0ecb0250533f29e22b8d1a7a196cf4d0", size = 186377, upload-time = "2026-07-17T22:50:48.413Z" },
{ url = "https://files.pythonhosted.org/packages/ca/40/f9ff2d630ffce4e7dfea0b2288e1caf9ebbf9ff8a9ec9396136ce8b94935/websockets-16.1.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:42f599f4d48c7e1a3338fdaac3acd075be3b3cf02d4b274f3bf2767aedd3d217", size = 187148, upload-time = "2026-07-17T22:50:49.845Z" },
{ url = "https://files.pythonhosted.org/packages/b5/71/e177c8299f78d7cbe2d14df228643c10c70c0e86e108e092056bbcc16e46/websockets-16.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dcc04fedf83effaeb9cce98abc9469bb1b42ef85f03e01c8c1f4438ef7555737", size = 187578, upload-time = "2026-07-17T22:50:51.619Z" },
{ url = "https://files.pythonhosted.org/packages/49/b2/b6987faf330f5af5c787a2610124c2e8403d51724f9001ec4fff6311fe7a/websockets-16.1.1-cp314-cp314t-win32.whl", hash = "sha256:8483c2096363120eea8b07c06ae7304d520f686665fffd4811fad423930a65d7", size = 179729, upload-time = "2026-07-17T22:50:53.269Z" },
{ url = "https://files.pythonhosted.org/packages/a2/6e/fbac6ed878dd362fbad7d415fa4f84d38e3e33fed8cde45c64e783acf826/websockets-16.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:bcce07e23e5769375158f5efdcdafa8d5cd014b93c6683865b840ed65b96f231", size = 180072, upload-time = "2026-07-17T22:50:54.969Z" },
{ url = "https://files.pythonhosted.org/packages/e1/ed/71fea6e141590cafc40b14dc5943b0845606bee87bdb52a21b6a73eb4311/websockets-16.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:820fb8450edddae3812fd58cbc08e2bf22812cb248ecb5f06dbb82119a56e869", size = 177185, upload-time = "2026-07-17T22:50:56.665Z" },
{ url = "https://files.pythonhosted.org/packages/01/ec/00e7eeca200facf9266a83e4cbbf1bed0e67fba1d4d45031d3e5b3d81b5c/websockets-16.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:125f22dbefaf1554fea66fc83851490edb284ce4f501d37ffed2752f418332d9", size = 177459, upload-time = "2026-07-17T22:50:58.197Z" },
{ url = "https://files.pythonhosted.org/packages/75/fd/5774c4b33f7c0d8f0c51809c8b3a93456c48e3543579262cfa64eb5f522e/websockets-16.1.1-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:30bbe120437b5648a77d3519b7024ea09530e0b5b18d3698c5a0ae536fe0cc2e", size = 178294, upload-time = "2026-07-17T22:50:59.641Z" },
{ url = "https://files.pythonhosted.org/packages/37/c3/48e2c03d2bd79bb45948841c592d24156312dd5f58cdf8f549febe652fb6/websockets-16.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b6b9dadbef0cccd9f4c4ee96b08898afa73e26803bbe0f6aeb5bb12b0074206d", size = 179190, upload-time = "2026-07-17T22:51:01.129Z" },
{ url = "https://files.pythonhosted.org/packages/2d/3f/73e511ecf2496ceac57dd4ed8388efe2bcf0769338a2dbf242c8366ae87e/websockets-16.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56cd5fc4f10a9ea8aa0804bddb7b42506cf9e136046f3b4c27de8fec9e2ecba5", size = 180330, upload-time = "2026-07-17T22:51:02.603Z" },
{ url = "https://files.pythonhosted.org/packages/be/4d/2d0d67834092e354d2b0498f014a41249a89556bc406cf86f3e1557bb463/websockets-16.1.1-py3-none-any.whl", hash = "sha256:6abbd3e82c731c8e531714466acd5d87b5e88ac3243465337ba71d68e23ae7e3", size = 173814, upload-time = "2026-07-17T22:51:04.184Z" },
]
[[package]]
name = "xxhash"
version = "3.6.0"
+115 -12
View File
@@ -13,15 +13,15 @@ wheels = [
[[package]]
name = "anyio"
version = "4.13.0"
version = "4.14.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
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" }
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" }
wheels = [
{ 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" },
{ 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" },
]
[[package]]
@@ -191,9 +191,10 @@ wheels = [
[[package]]
name = "langchain-core"
version = "1.4.0"
version = "1.6.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
{ name = "jsonpatch" },
{ name = "langchain-protocol" },
{ name = "langsmith" },
@@ -204,9 +205,9 @@ dependencies = [
{ name = "typing-extensions" },
{ name = "uuid-utils" },
]
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" }
sdist = { url = "https://files.pythonhosted.org/packages/89/7b/406b4a8dd43dd01f69e04aafdee206809a2097134b2f944e12802eae7948/langchain_core-1.6.2.tar.gz", hash = "sha256:1ec6d3a98f7c8cdbb5bb0deff86e7ca37e16bf4f8f49f022d10723656c62352d", size = 1004390, upload-time = "2026-09-04T21:29:22.769Z" }
wheels = [
{ 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" },
{ url = "https://files.pythonhosted.org/packages/a8/62/d3fb7c215cb2f237c3fe84880bf347a38deafef6033b6d5f1339ba8ca401/langchain_core-1.6.2-py3-none-any.whl", hash = "sha256:21e6c7cf097c68b777fd69ea00864f09bd9ca76ac73e3e3fa14e1ee366eb4711", size = 571690, upload-time = "2026-09-04T21:29:21.175Z" },
]
[[package]]
@@ -223,7 +224,7 @@ wheels = [
[[package]]
name = "langgraph"
version = "1.2.2"
version = "1.2.11"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "langchain-core" },
@@ -233,9 +234,9 @@ dependencies = [
{ name = "pydantic" },
{ name = "xxhash" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e6/5a/ffc12434ee8aecab830d58b4d204ddea45073eae7639c963310f671a5bf5/langgraph-1.2.2.tar.gz", hash = "sha256:f54a98458976b3ff0774683867df125fb52d8dbedeb2441d0b0656a51331cee5", size = 695730, upload-time = "2026-05-26T18:07:28.49Z" }
sdist = { url = "https://files.pythonhosted.org/packages/56/0d/c8e7ee98896659e1b6555db0ab115a9ca899844744645d5d894032bab1d7/langgraph-1.2.11.tar.gz", hash = "sha256:9ecfe11e50d338b34b15cf4d8a442642de103e8ae6971320efba84e4542eb363", size = 725753, upload-time = "2026-08-11T14:00:36.945Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/42/9b/b08d578bba73e25351152dfd3d6d21e81210a5fff1b6f26e56f33197c8f5/langgraph-1.2.2-py3-none-any.whl", hash = "sha256:0a851bf4ba5939c5474a2fd57e6b439b5315283e254e42943bd392c2d71a5e03", size = 236376, upload-time = "2026-05-26T18:07:26.577Z" },
{ url = "https://files.pythonhosted.org/packages/0a/7f/c5c30e4be99ff821029c7ac872a480676bb179c9f3df85ea3f38d13f86d4/langgraph-1.2.11-py3-none-any.whl", hash = "sha256:8bab70de7b2d00b5300fb289bcf38d8b241400f3184c1e95e8ce706fb0e8686b", size = 248854, upload-time = "2026-08-11T14:00:35.494Z" },
]
[[package]]
@@ -266,15 +267,18 @@ wheels = [
[[package]]
name = "langgraph-sdk"
version = "0.3.15"
version = "0.4.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
{ name = "langchain-core" },
{ name = "langchain-protocol" },
{ name = "orjson" },
{ name = "websockets" },
]
sdist = { url = "https://files.pythonhosted.org/packages/66/af/cdd4d6f3c05b3c1112ed3f12ef830faf15951b21d22cbc622a4becbbe25c/langgraph_sdk-0.3.15.tar.gz", hash = "sha256:29e805003d2c6e296823dd71992610976fd0428cefaa8b3304fd91f2247037de", size = 201924, upload-time = "2026-05-22T16:54:27.678Z" }
sdist = { url = "https://files.pythonhosted.org/packages/3f/ae/91446c1fffa04a2dc1f81afbfb5bfff3590452891a72bd9098a656ab2657/langgraph_sdk-0.4.4.tar.gz", hash = "sha256:4e651ffa09de695681579396375377bdde23bedbc8e35b070e615cbd5af7da8b", size = 345789, upload-time = "2026-08-27T21:25:22.593Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/be/a5/0196d9c05749c25bc198e4909d68c998bc3120297e14944921baf2f4c384/langgraph_sdk-0.3.15-py3-none-any.whl", hash = "sha256:3838773acf7456d158165385d49f48f1e856f28b56ccd99ea139a8f27004815d", size = 98166, upload-time = "2026-05-22T16:54:26.013Z" },
{ url = "https://files.pythonhosted.org/packages/43/d5/2ad7f3a835c6fcebf58b6669552536ecd52d2dfe4cf49c6c36648fd83572/langgraph_sdk-0.4.4-py3-none-any.whl", hash = "sha256:39afe416c91742925e6f8a93715f566d499b36e1b636b804a4ffe3190e4f4e64", size = 162270, upload-time = "2026-08-27T21:25:21.072Z" },
]
[[package]]
@@ -775,6 +779,105 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/44/e7/3937b9a9d6745b94dbe7b86531e098db8c53b77c8d07df7daa9577a47b8e/uuid_utils-0.16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:680799a9ade01d69c53cb9d41392ced24919d4f600bfab5060b61fca37510097", size = 178508, upload-time = "2026-05-19T07:43:43.774Z" },
]
[[package]]
name = "websockets"
version = "16.1.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/21/f7/bc3a25c5ec26ce62ce487690becc2f3710bbc7b33338f005ad390db0b986/websockets-16.1.1.tar.gz", hash = "sha256:db234eda965dcce15df96bb9709f587cd87d4d52aaf0e80e2f34ec04c7670c57", size = 182204, upload-time = "2026-07-17T22:51:05.858Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2b/03/47debfe28e9d6d354be5d777b67fd44c359b9eb299a5d103500bd7cc3e37/websockets-16.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d0fcf657e9f13ff4b177960ab2200237b12994232dfb6df16f1cfe1d4339f93c", size = 179566, upload-time = "2026-07-17T22:48:49.596Z" },
{ url = "https://files.pythonhosted.org/packages/72/93/31efa1ed78c17e5cfc229fd449e3966e1b9cc15753204cd585cc8dd01f4a/websockets-16.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b852788aa51764e2d8e4cf5493d559326bcae5e38d16ba25ffa322b034df272a", size = 177250, upload-time = "2026-07-17T22:48:50.942Z" },
{ url = "https://files.pythonhosted.org/packages/01/4a/542378ab3972b0c1cf1df3df3eff9591cea0d30c58c3aa3c4ddbc244e787/websockets-16.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1427fb4cf0d72f66333e2cacc3ff5f575bf2d7008166ce991a4a470b21d51a22", size = 177528, upload-time = "2026-07-17T22:48:52.59Z" },
{ url = "https://files.pythonhosted.org/packages/33/d9/162321f63c7eed558e9e1798ed7a1e34a4f6dab51f35419e4ed7a4907979/websockets-16.1.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:da4ca1a9d72f9030b3146b8d7022719a9f3d478f61efe6f7dd51d243f61c51b2", size = 186859, upload-time = "2026-07-17T22:48:53.915Z" },
{ url = "https://files.pythonhosted.org/packages/de/09/87df740f7430ce564bd52402e9c9458d4d0459cc7d2ee29e530c8204851b/websockets-16.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86d7f0f8bdb25d2c632b72527325e4776430fd5bc61b9118de4e2b8ddb5f5b01", size = 188095, upload-time = "2026-07-17T22:48:55.384Z" },
{ url = "https://files.pythonhosted.org/packages/d2/12/3d2703af7cc095f3c81904c92208cc1ae79affbc67376944b50ee9301f73/websockets-16.1.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7dfcad78ea1492ee3a9ec765cb7f51bbc17d477107aaf6b22abf7b2558d1c5a0", size = 191385, upload-time = "2026-07-17T22:48:56.742Z" },
{ url = "https://files.pythonhosted.org/packages/1d/69/986aa0234a964a00f5149cfc46e136e96c8faad1c783474550f40d31aef4/websockets-16.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fb9a0a6dc3d1b3986cb88091b6899f0396651e0f74e2c9766ab8d6ffc3842e29", size = 188653, upload-time = "2026-07-17T22:48:58.134Z" },
{ url = "https://files.pythonhosted.org/packages/35/6b/10f9d03e3970a69ba67bd3b46b87a929b586d0300fadbfe14f57c1f85490/websockets-16.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29dfa8114c4a620c69591c5973860f768eac29d3fd6904f37f34266cb219c512", size = 187426, upload-time = "2026-07-17T22:48:59.515Z" },
{ url = "https://files.pythonhosted.org/packages/56/db/bb3aad62bf63d8bb3f0634b2eabffcfb3677a34bd19492110ff6869cf703/websockets-16.1.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ff9417c0ada4d0f7d212f928303e5579bdf3ace4c802fa4afabb30995da58c3", size = 184882, upload-time = "2026-07-17T22:49:00.916Z" },
{ url = "https://files.pythonhosted.org/packages/6c/4c/c09a2ea9bfbeccce52fdc383e5f28af4bc8843338aabac28c81489af6120/websockets-16.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fe0b50da2d84535fb4f7b4bfa951280f97ce3d558a0443b541166d609e67b57", size = 187584, upload-time = "2026-07-17T22:49:02.283Z" },
{ url = "https://files.pythonhosted.org/packages/c7/8b/31bb4eb4d9eaacf1fdd39d115772a8aeaedfc19b5dc262e57ffbc8a9d42c/websockets-16.1.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:34420aaa64440ebd51ac72ca8a45ef4626429438c9b02e633ae412ed43f925d3", size = 186174, upload-time = "2026-07-17T22:49:03.973Z" },
{ url = "https://files.pythonhosted.org/packages/2f/e4/dc02d725610a1ad49e193ef91a548194d71bdc6cdf27da83067dd1f73995/websockets-16.1.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a6a61aff018180c9c50b7b0da33bfd29d378af3497429c95006c589a23a11648", size = 187986, upload-time = "2026-07-17T22:49:05.553Z" },
{ url = "https://files.pythonhosted.org/packages/e0/73/30ed84c8bfd14c73d4af29d5ed9323c3073b48e0b7b23b67070f4e7fd59b/websockets-16.1.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:04fd29a0e2fe9414a95b00e92c67ae51bf900c50c0f8a4b2dafdad621f49ea1d", size = 185565, upload-time = "2026-07-17T22:49:06.959Z" },
{ url = "https://files.pythonhosted.org/packages/7d/d3/4be8d4959f51e31b4f8fc0ece12b45bd3b6c0d15ea23b9990d9c11fc805f/websockets-16.1.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5c31aa7e39ee3e8a358573257f1c0bb5c52430d1b637030dd9c8cc2c282926be", size = 186598, upload-time = "2026-07-17T22:49:08.293Z" },
{ url = "https://files.pythonhosted.org/packages/26/fa/abb38597a52d84ed9cfacadc7a0c6f2db282c0ab23cdf72b58a666a21227/websockets-16.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d14bfb217eb4701e850f1525c9d29d79c44794cdf1c299ead25f39f8c78dea81", size = 186834, upload-time = "2026-07-17T22:49:09.766Z" },
{ url = "https://files.pythonhosted.org/packages/59/80/1119ad08a228b90c4eb77fbe48df7836731a605f5f881ba701ca826a4a65/websockets-16.1.1-cp311-cp311-win32.whl", hash = "sha256:2e28e602bb13da44fbe518c1781a88e3b9d4c3d48d02c9bad83e546164336f57", size = 179940, upload-time = "2026-07-17T22:49:11.196Z" },
{ url = "https://files.pythonhosted.org/packages/71/b2/e511c1c6f64a95c2f3fc54bffda0e14eaa7e9442be605c29270f7589b918/websockets-16.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:7421fad442de870a8cbf2287d1cad7e706ece0dbfeba5e911df132cbdc1cb56a", size = 180239, upload-time = "2026-07-17T22:49:12.519Z" },
{ url = "https://files.pythonhosted.org/packages/17/9d/681cda21c9eee743203a6cb79b9d3d05adad9aa60ec660c6c9bf4dd619ca/websockets-16.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cc97814dfb786a83b6e2dc2e79351e1b83e6d715647d6887fcabd83026417a00", size = 179600, upload-time = "2026-07-17T22:49:13.92Z" },
{ url = "https://files.pythonhosted.org/packages/fb/8d/6195a88b45e8d2a8f745fc2046e36f885a3c9763e6767d2c46229bf9510c/websockets-16.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e047dc87ef7ca50f4d309bf775ad4a71711c58556d75d7bd0604b2317f43e94b", size = 177272, upload-time = "2026-07-17T22:49:15.453Z" },
{ url = "https://files.pythonhosted.org/packages/73/e3/fe2d498c64dea0095c9a9f9a351af4cd6eef31b618395582bc1f38ba45ff/websockets-16.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:01fbdcbac298efe19360b94bc0039c8f746f0220ba570f327577bfee81059175", size = 177542, upload-time = "2026-07-17T22:49:16.875Z" },
{ url = "https://files.pythonhosted.org/packages/fe/ed/f1831681fce0e3242346e5458486003c5f124ed69e5e0b847fd029db4973/websockets-16.1.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0f62863e8a00a6d33c3d6566ec0b89f23787b747ffe0c3bc71ec0e76b82c94b1", size = 187137, upload-time = "2026-07-17T22:49:18.323Z" },
{ url = "https://files.pythonhosted.org/packages/6f/79/4ff9dcc1bb46f6b4c536936dde1fd60f9b564f3304307274db97f4c9496d/websockets-16.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8087e82f842609734c9b5a1330464f8e94e346ba0e18c832c08bafa4b0d63c15", size = 188374, upload-time = "2026-07-17T22:49:19.65Z" },
{ url = "https://files.pythonhosted.org/packages/62/c3/5c49b6efb36cab733d23773f6de575e1dba65736ead17d5d2b2a1daef779/websockets-16.1.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2bb5d041a8307d2e18782e7ce777f6fdb1e8c2f5d09291484b18c294b789d9aa", size = 191155, upload-time = "2026-07-17T22:49:21.331Z" },
{ url = "https://files.pythonhosted.org/packages/6e/f6/56ccceda3a4838d18f1d40821480da4775397e8b1eecf4031e20c50e2e90/websockets-16.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1db4de4a0e95673f7545d393c49eeb0c2f18ac1ef93073218c79d5cdb2ee75ab", size = 189011, upload-time = "2026-07-17T22:49:22.889Z" },
{ url = "https://files.pythonhosted.org/packages/86/d6/ad5286241a2bce1107e2798d3bfbd62cf79aee167bdb654f8cb1e9dbf949/websockets-16.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f17dbe07eb3ea7f99e4df9b7e0efefe80fbf30d37a8cc4d561a0aed310bc8847", size = 187766, upload-time = "2026-07-17T22:49:24.339Z" },
{ url = "https://files.pythonhosted.org/packages/bc/67/d65c970b7e347fdca69479beb7811c2060529956730a7a4e3ae7c66b0e31/websockets-16.1.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4b57693728576d84ede0a77987ab16881b783d2cd9f1dc180a8fbbc3f79c4428", size = 185173, upload-time = "2026-07-17T22:49:25.743Z" },
{ url = "https://files.pythonhosted.org/packages/1d/5b/14af3cd4ee69d8ea9baca58f3dc3cfb1ba78332a347fd478cb096549d60e/websockets-16.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2a636ff1e7a5c4edf71ef0e79adae7f25dba93b4fcbe3dc958733477ffeb0eaf", size = 187809, upload-time = "2026-07-17T22:49:27.147Z" },
{ url = "https://files.pythonhosted.org/packages/7b/11/be301710d70de97e3e7b3586e6d492c9c06d6a61bf1c2202c36cf0c75607/websockets-16.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d6bec75c290fe484a8ba4cacdf838501e17c06ecfbbf31eede81a9e431bd7751", size = 186412, upload-time = "2026-07-17T22:49:28.611Z" },
{ url = "https://files.pythonhosted.org/packages/db/07/fe1435bf6fe738a3d3b54dbe0c18dabf12cba4d909ac8b58b539ce27c1f4/websockets-16.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:54509b8e92fee4453e152b7558ddef37ce9705a044922f2095a6105e3f80c96f", size = 188290, upload-time = "2026-07-17T22:49:29.965Z" },
{ url = "https://files.pythonhosted.org/packages/8a/0a/81f394aff8efcbb01208c1ced77df0a3c7fcce584a88c7273663697946c2/websockets-16.1.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:f0aa4aad3b1b69ad3fd85a0fd0952ec64331c762bd77ec51cc814170873890b2", size = 185844, upload-time = "2026-07-17T22:49:31.447Z" },
{ url = "https://files.pythonhosted.org/packages/39/5c/dd485b995473f415510251fe9bd708f2d24458f439fce958daf8d66dc7c6/websockets-16.1.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:42290eb6db4ccaca7012656738214f8514082fb6fa40cdeb61bb9a471b52e383", size = 186823, upload-time = "2026-07-17T22:49:33.104Z" },
{ url = "https://files.pythonhosted.org/packages/9d/0b/f78de76ff446f1e66af12b43c48a35f31744de93cfdec2f4ea67d5d7bbf1/websockets-16.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:53260c8930da5771cec89439bff99c20c8cb03ddb9588b980697355a83cd4bd3", size = 187102, upload-time = "2026-07-17T22:49:34.616Z" },
{ url = "https://files.pythonhosted.org/packages/37/a1/4cf892007778eaf84ad162bfc98046e0ed89b63ac55949e3236626b2a23f/websockets-16.1.1-cp312-cp312-win32.whl", hash = "sha256:1d27fa8462ad6a1cb36206a3d0640b2333340def181fae11ed7f9adeaa5c0747", size = 179943, upload-time = "2026-07-17T22:49:36.213Z" },
{ url = "https://files.pythonhosted.org/packages/d9/de/6abe251d28c3a3f217096575400b27750b18e0b1d2fff3a2a239960fea07/websockets-16.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:b436f6ec4fc3a6b4237c84d3f83170ed2b40bb584222f0ac47a0c8a5921980c7", size = 180243, upload-time = "2026-07-17T22:49:37.626Z" },
{ url = "https://files.pythonhosted.org/packages/ce/fd/6ec6c6d2850aea25b1b2aa9901a016980bb87d01e89b3eb00470b1b5d471/websockets-16.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ab59169ace05dcb49a1d4118f0bde139557adf45091bd85747e36bf5de984dd1", size = 179587, upload-time = "2026-07-17T22:49:38.959Z" },
{ url = "https://files.pythonhosted.org/packages/5f/d8/1d299d2dd34087db39831a34cc645ef8a6f89d78efada6983093513cd81c/websockets-16.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5e3b7d601f6f84156b08cc4a5e541c2b50ad7b36cfc302b657a12477c904a5df", size = 177272, upload-time = "2026-07-17T22:49:40.293Z" },
{ url = "https://files.pythonhosted.org/packages/3d/86/0a70d3ae2f0f2256bb41302d9804dbca65d4360281e7feb3e1f94102ac46/websockets-16.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cd2ca96a082a36964aca83e992f72abeb61b7306c1a6cba4c7d06a7b93750cac", size = 177530, upload-time = "2026-07-17T22:49:41.786Z" },
{ url = "https://files.pythonhosted.org/packages/b5/c2/c676c69444d9db448b3f0a55a98dcc534affce0bce961d9d2f0b8499b10a/websockets-16.1.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f5d497865f05bb222cab7016c6034542e84e5f29f49c6fd3f4939cda7197b5b8", size = 187197, upload-time = "2026-07-17T22:49:43.658Z" },
{ url = "https://files.pythonhosted.org/packages/0b/13/88137fbaf726ebe29d62c1117fa11fa2bbb6209dc79d4ad738efbe36a2aa/websockets-16.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bae954c382e013d5ea5b190d2830526bfa45ad121c326da0049b8c769f185db6", size = 188433, upload-time = "2026-07-17T22:49:45.147Z" },
{ url = "https://files.pythonhosted.org/packages/01/6d/46c2f2ce6751cb26f39293e1ecbf8544cb01321397cd476c2756b98c216d/websockets-16.1.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e09f753a169951eb4f28c2c774f71069304f66e7277e0f5a2892423599cfa854", size = 189868, upload-time = "2026-07-17T22:49:46.581Z" },
{ url = "https://files.pythonhosted.org/packages/29/2b/170a9e8097636cfde4dc3c592b6e00b18a44a2f5407606d96ca542dd5838/websockets-16.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:024193f8551a2b0eafbdd160911012c4e6c228c28430c84433253299a9e42d6a", size = 189059, upload-time = "2026-07-17T22:49:47.972Z" },
{ url = "https://files.pythonhosted.org/packages/a7/48/f0d4ebc9ab4b473b8861b9e20fdb663d515d42f7befdf62cdb60fee7a1ec/websockets-16.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:aabe464bfd13bd25f4821faf111da6fefdc389f870265a53105580e45b0a2e49", size = 187814, upload-time = "2026-07-17T22:49:49.344Z" },
{ url = "https://files.pythonhosted.org/packages/d5/ba/39a41d3ae8e72696a9492581900611c5a91e2b07563b0bcd2523adea9854/websockets-16.1.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a28fcbc9b6baf54a2e23f8655f308e4ccc6afdd7266f8fe7954f320dcda0f785", size = 185229, upload-time = "2026-07-17T22:49:50.787Z" },
{ url = "https://files.pythonhosted.org/packages/3c/36/ac15b604f850d1907f0a85ed721cefe47cd45034b3620069b829746cccbe/websockets-16.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79eace538c6a97e96d0d03d4f9d314f9677f5ed85a8a984992ffd90b13cb8a56", size = 187874, upload-time = "2026-07-17T22:49:52.228Z" },
{ url = "https://files.pythonhosted.org/packages/a8/f3/3fbd5d71d59299c3770faa5884d4f45070236ca5a35ab3a61830812c409a/websockets-16.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:496af849a472b531f758dbd4d61338f5000538cb1a7b3d20d9d32a264517f509", size = 186469, upload-time = "2026-07-17T22:49:53.776Z" },
{ url = "https://files.pythonhosted.org/packages/b4/fc/dd90349bba58af2a53ef2ddd9c32716c81eb6d59a0687939fff561860878/websockets-16.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5283810d2646741a0d8da2aa733d6aefa0545809afccb2a5d105a26bc45125f1", size = 188347, upload-time = "2026-07-17T22:49:55.202Z" },
{ url = "https://files.pythonhosted.org/packages/4c/f3/f73ba86427682da59b78c11d77ba56d5b801c32e84afe79b274bbd6a9bb2/websockets-16.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4e3b680b1e0a27457e727a0d572fd81dffa87b6dbf8b228ab57da64f7d85aead", size = 185903, upload-time = "2026-07-17T22:49:56.75Z" },
{ url = "https://files.pythonhosted.org/packages/34/7c/f95eb20e80104173b3a0a092291f89ea4047ef6e608e0a57ca06eb14eecb/websockets-16.1.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:69159730a823dde3ea8d08783e8d47ef135a6d7e8d44eb127e32b321c9db8e3e", size = 186855, upload-time = "2026-07-17T22:49:58.467Z" },
{ url = "https://files.pythonhosted.org/packages/b0/35/dd875b3e050ff232d60fa377707f890e369f74d134f1be32e8f68879747c/websockets-16.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ed5bb271084b46530ee2ddc0410537a9961152c5ccba2fc98c5276d992ccba87", size = 187140, upload-time = "2026-07-17T22:50:00.016Z" },
{ url = "https://files.pythonhosted.org/packages/e8/dc/5cbfcb41824502f6af93b8f3943a4d06c67c23c7d2e31eb18748c4a5b2a7/websockets-16.1.1-cp313-cp313-win32.whl", hash = "sha256:cfb70b4eb56cac4da0a83588f3ad50d46beb0690391082f3d4e2d488c70b68ea", size = 179928, upload-time = "2026-07-17T22:50:01.685Z" },
{ url = "https://files.pythonhosted.org/packages/b0/c1/71e5deb5b7f8f226997ab64908c184ac3105c0155ce2d486f318e5dd08a8/websockets-16.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:d9531d9cbeac99af6f038fb1bc351403531f7d634a2c2e10e2f7c854c6ed5b68", size = 180242, upload-time = "2026-07-17T22:50:03.117Z" },
{ url = "https://files.pythonhosted.org/packages/73/a2/ba78a164eeea4620df4a4df4bd2ed6017438c4655cc0f36f2c0bc0432355/websockets-16.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:443aefe96b7fdb132e2a70806cca1f2af49bb3f28e47abcd7c2e9dcf4d8fa1b8", size = 179635, upload-time = "2026-07-17T22:50:05.001Z" },
{ url = "https://files.pythonhosted.org/packages/b9/08/d26d7a7628cd4ac34cbbdb63ac80914ca842ed8e42938c40a53567806df3/websockets-16.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6456ff333092d509127d75a638cb411afae8ff17f092635015d1902efec8a293", size = 177320, upload-time = "2026-07-17T22:50:06.427Z" },
{ url = "https://files.pythonhosted.org/packages/0f/45/ebec83e6269536aa5932533c67b0af5c781f3e73fdbcd68672dcf43f4f44/websockets-16.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fce6c48559c86d1ac3632ccb1bebc7d5442fbe79bd9bb0e40379ee54be2a4051", size = 177544, upload-time = "2026-07-17T22:50:07.834Z" },
{ url = "https://files.pythonhosted.org/packages/c9/d5/abc614d2297f6c1c3e01e61260364457a47c25cc1cf6a879038902bc6aa8/websockets-16.1.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:92b820d345f7a3fc7b8163949ee92df910f290c3fc517b3d5301c78065adafe1", size = 187270, upload-time = "2026-07-17T22:50:09.275Z" },
{ url = "https://files.pythonhosted.org/packages/52/71/4c99af3b87dff1b2927981f6876607d4acb45338c665242168d3982f7758/websockets-16.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a606d9c24035242a3e256e9d5b77ed9cd6bccfcb7cf993e5ca3c0f6f68fb6a7", size = 188509, upload-time = "2026-07-17T22:50:10.722Z" },
{ url = "https://files.pythonhosted.org/packages/9b/b4/5c8ca14b0df7eb84ed0524165c5359150210140817a3312aee57bf62a1cf/websockets-16.1.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:414e596c75f74e0994084694189d7dc9229fb278e33064d6784b73ffbba3ca31", size = 189882, upload-time = "2026-07-17T22:50:12.293Z" },
{ url = "https://files.pythonhosted.org/packages/25/c1/bedfba9e70557129cb8083748d167bdcc01483dedf0f0df143676df05cbe/websockets-16.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:536676848fc5961aca9d20389951f59169508f765637a172403dc5434d722fa0", size = 189114, upload-time = "2026-07-17T22:50:13.789Z" },
{ url = "https://files.pythonhosted.org/packages/df/09/aa835b2787835aebd839114be5de51b797cb480b63ba42b26d34dfe147cb/websockets-16.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:97fd3a0e8b53efa41970ac1dff3d8cf0d2884cadeb4caaf95db7ad1526926ee3", size = 187861, upload-time = "2026-07-17T22:50:15.179Z" },
{ url = "https://files.pythonhosted.org/packages/20/26/f6408330694dbc9830857d9d23bc14ac4f6875127a480cfdda8d5ca21198/websockets-16.1.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7b1b19636af86a3c7995d4d028dbe376f39b4bf31541146f9c123582a6c94562", size = 185286, upload-time = "2026-07-17T22:50:16.741Z" },
{ url = "https://files.pythonhosted.org/packages/17/9a/e0675e70dd8a80762cf35bb18799d3f290a4890ffe6439bc51d222796083/websockets-16.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:41c8e77f17294c0ac18008a7309b99b34ee72247ef10b6dff4c3f8b5ac29896b", size = 187935, upload-time = "2026-07-17T22:50:18.213Z" },
{ url = "https://files.pythonhosted.org/packages/33/c1/3234cfb86afde01b81e9bddcc6e534c440975d60a13991259e833069ab3e/websockets-16.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9f63bcef7f4b02b06b35fc01c93b96c43b5e88e1e8868676caacf493d5a31f3a", size = 186444, upload-time = "2026-07-17T22:50:19.67Z" },
{ url = "https://files.pythonhosted.org/packages/89/87/9c15206e1d778923d8daa9657de07aa62ea815e13448319c98458c37b281/websockets-16.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dab9eb87869da2d6ed3af3f3adf28414baae6ec9d4df355ffc18889132f3436c", size = 188409, upload-time = "2026-07-17T22:50:21.28Z" },
{ url = "https://files.pythonhosted.org/packages/f2/00/cf5de5c67676de2d3eef8b2a518f168f6796595447a5b7161ba0d012915c/websockets-16.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:43e3a9fdd7cbf7ba6040c31fae0faf84ca1474fef777c4e37912f1540f854499", size = 185958, upload-time = "2026-07-17T22:50:22.719Z" },
{ url = "https://files.pythonhosted.org/packages/62/c0/731b6ddede2e4136912ec4cff2cffbda35af73546be4762c3d7bd3bd79af/websockets-16.1.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:056ae37939ed7e9974f364f5864e76e49182622d8f9751ac1903c0d09b013985", size = 186911, upload-time = "2026-07-17T22:50:24.108Z" },
{ url = "https://files.pythonhosted.org/packages/8c/7f/39c634472c4469a24a7c09cecddffb08fac6d0e74f73881a94ee8a40a196/websockets-16.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a0eadbbf2c30f01efa58e1f110eb6fa293261f6b0b1aa38f7f48707107690af9", size = 187204, upload-time = "2026-07-17T22:50:25.548Z" },
{ url = "https://files.pythonhosted.org/packages/26/89/9667c256c256dafcc62d21328ce7a40067da857969b68ee9af375b0aaf72/websockets-16.1.1-cp314-cp314-win32.whl", hash = "sha256:195c978b065fa40910582464f99d6b15c8b314c68e0546549a55ed83f4735328", size = 179603, upload-time = "2026-07-17T22:50:27.086Z" },
{ url = "https://files.pythonhosted.org/packages/bd/dd/1c099d6c0fc5deb6b46ccdbb6981fdb4b12c917869cb3952408409dc18db/websockets-16.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:4e8d01cc3bcae7bbf8167f944aeafefed590fae5693552bba9794a9df68371cc", size = 179948, upload-time = "2026-07-17T22:50:28.521Z" },
{ url = "https://files.pythonhosted.org/packages/35/25/9956b2d5e0529d5d23924f21bba1440d4c5c88a562e4f08550871ffa97a7/websockets-16.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0ffd3031ea8bda8d61762e84220186105ba3b748b3c8da2ae4f7816fac03e573", size = 179963, upload-time = "2026-07-17T22:50:29.982Z" },
{ url = "https://files.pythonhosted.org/packages/17/06/55ffc976c488b6aee9ea05761ff7c4e88e7c1fd82818c8ca7b556ad2f90c/websockets-16.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:84a2cef8deffbd9ab8ee0ea546a2a6a7030c28f44e6cdd4547dbfeb489eb8999", size = 177497, upload-time = "2026-07-17T22:50:31.396Z" },
{ url = "https://files.pythonhosted.org/packages/0c/e8/f7dac2e980bacc92bdc26cebae4ae4d50cae5380732c50980598fc0bbae4/websockets-16.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3df13f73af9b3b38ab1195eb299ecb67a4330c911c97ae04043ff74085728abe", size = 177698, upload-time = "2026-07-17T22:50:32.829Z" },
{ url = "https://files.pythonhosted.org/packages/b2/39/26762f734113e22da2b942c3aca85798e0c0405d64c256549540ff31e5a1/websockets-16.1.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:23253dd5bcae3f9aaee0a1d30967a8dbd52e5d3cff93a2e5b84df57b77d4750d", size = 187561, upload-time = "2026-07-17T22:50:34.24Z" },
{ url = "https://files.pythonhosted.org/packages/11/94/c3f330851806b9b02138b774d593478323e73c99238681b4b93efe64e02d/websockets-16.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1c5705e314449e3308872fe084b8571ce078ee4fc55a98a769bdefe5917392", size = 188732, upload-time = "2026-07-17T22:50:36.088Z" },
{ url = "https://files.pythonhosted.org/packages/d1/f2/eb2c450f052de334ae33cf200ece6e87b0e14d186807074e4eb1cd2cdea2/websockets-16.1.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69e52d175a0a7d1e13b4b67ad41c560b7d98e8c6f6126eb0bda496c784faf8c7", size = 190872, upload-time = "2026-07-17T22:50:38.008Z" },
{ url = "https://files.pythonhosted.org/packages/70/31/2ac8cecf3a74f7fed9132129fc3d90b3998a1554570c11a69b2a8c20332d/websockets-16.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1f79c89b5eb034d1722938a891916582f8f7f503f58ca22518a63c3f2cd18499", size = 189305, upload-time = "2026-07-17T22:50:39.53Z" },
{ url = "https://files.pythonhosted.org/packages/6a/cf/8ab19650d3c0d4562c92e70ab47c257c4aa5c6a713ed87fe63766b31fefc/websockets-16.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:39f2a024af5c345ffe8fcf1ee18c049c024c94df393bb09b044a6917c77bde43", size = 188033, upload-time = "2026-07-17T22:50:40.912Z" },
{ url = "https://files.pythonhosted.org/packages/66/d7/a49a38a6127a4acb134fb1912b215d900cc657605cff32445bf519f3acc4/websockets-16.1.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:952303a7318d4cbe1011400839bb2051c9f84fa0a35923267f5daba34b15d458", size = 185748, upload-time = "2026-07-17T22:50:42.559Z" },
{ url = "https://files.pythonhosted.org/packages/95/3e/ad1fa40388c7f2e0bb2c7930d0090b6c5498594bd1cdaec18864df3d9e97/websockets-16.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:249116b4a76063d930a46391ad56e135c286e4562a18309029fc2c73f4ed4c62", size = 188285, upload-time = "2026-07-17T22:50:43.974Z" },
{ url = "https://files.pythonhosted.org/packages/35/b8/d5db28ca264b9104f82196f92dc8843e35fd391f763d42e4ad358f5bc97e/websockets-16.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:61922544a0587a13fd3f53e4c0e5e606510c7b0d9d22c8444e5fae22a06b38cb", size = 186777, upload-time = "2026-07-17T22:50:45.474Z" },
{ url = "https://files.pythonhosted.org/packages/42/9c/726cb39d0cc43ae848dce4aa2acb04eecc6738b1264ec6d700bf6bcfb9f8/websockets-16.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:46dcaa042cd1de6c59e7d9269fa63ff7572b6df40510600b678f0826b3c7af51", size = 188682, upload-time = "2026-07-17T22:50:46.973Z" },
{ url = "https://files.pythonhosted.org/packages/be/c7/1168704de8c2dd483edabe4a22cbe4465dd8be8dd95561d214f9fe092871/websockets-16.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:38565aca3e01ea8734e578fb2118dade0ecb0250533f29e22b8d1a7a196cf4d0", size = 186377, upload-time = "2026-07-17T22:50:48.413Z" },
{ url = "https://files.pythonhosted.org/packages/ca/40/f9ff2d630ffce4e7dfea0b2288e1caf9ebbf9ff8a9ec9396136ce8b94935/websockets-16.1.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:42f599f4d48c7e1a3338fdaac3acd075be3b3cf02d4b274f3bf2767aedd3d217", size = 187148, upload-time = "2026-07-17T22:50:49.845Z" },
{ url = "https://files.pythonhosted.org/packages/b5/71/e177c8299f78d7cbe2d14df228643c10c70c0e86e108e092056bbcc16e46/websockets-16.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dcc04fedf83effaeb9cce98abc9469bb1b42ef85f03e01c8c1f4438ef7555737", size = 187578, upload-time = "2026-07-17T22:50:51.619Z" },
{ url = "https://files.pythonhosted.org/packages/49/b2/b6987faf330f5af5c787a2610124c2e8403d51724f9001ec4fff6311fe7a/websockets-16.1.1-cp314-cp314t-win32.whl", hash = "sha256:8483c2096363120eea8b07c06ae7304d520f686665fffd4811fad423930a65d7", size = 179729, upload-time = "2026-07-17T22:50:53.269Z" },
{ url = "https://files.pythonhosted.org/packages/a2/6e/fbac6ed878dd362fbad7d415fa4f84d38e3e33fed8cde45c64e783acf826/websockets-16.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:bcce07e23e5769375158f5efdcdafa8d5cd014b93c6683865b840ed65b96f231", size = 180072, upload-time = "2026-07-17T22:50:54.969Z" },
{ url = "https://files.pythonhosted.org/packages/e1/ed/71fea6e141590cafc40b14dc5943b0845606bee87bdb52a21b6a73eb4311/websockets-16.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:820fb8450edddae3812fd58cbc08e2bf22812cb248ecb5f06dbb82119a56e869", size = 177185, upload-time = "2026-07-17T22:50:56.665Z" },
{ url = "https://files.pythonhosted.org/packages/01/ec/00e7eeca200facf9266a83e4cbbf1bed0e67fba1d4d45031d3e5b3d81b5c/websockets-16.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:125f22dbefaf1554fea66fc83851490edb284ce4f501d37ffed2752f418332d9", size = 177459, upload-time = "2026-07-17T22:50:58.197Z" },
{ url = "https://files.pythonhosted.org/packages/75/fd/5774c4b33f7c0d8f0c51809c8b3a93456c48e3543579262cfa64eb5f522e/websockets-16.1.1-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:30bbe120437b5648a77d3519b7024ea09530e0b5b18d3698c5a0ae536fe0cc2e", size = 178294, upload-time = "2026-07-17T22:50:59.641Z" },
{ url = "https://files.pythonhosted.org/packages/37/c3/48e2c03d2bd79bb45948841c592d24156312dd5f58cdf8f549febe652fb6/websockets-16.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b6b9dadbef0cccd9f4c4ee96b08898afa73e26803bbe0f6aeb5bb12b0074206d", size = 179190, upload-time = "2026-07-17T22:51:01.129Z" },
{ url = "https://files.pythonhosted.org/packages/2d/3f/73e511ecf2496ceac57dd4ed8388efe2bcf0769338a2dbf242c8366ae87e/websockets-16.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56cd5fc4f10a9ea8aa0804bddb7b42506cf9e136046f3b4c27de8fec9e2ecba5", size = 180330, upload-time = "2026-07-17T22:51:02.603Z" },
{ url = "https://files.pythonhosted.org/packages/be/4d/2d0d67834092e354d2b0498f014a41249a89556bc406cf86f3e1557bb463/websockets-16.1.1-py3-none-any.whl", hash = "sha256:6abbd3e82c731c8e531714466acd5d87b5e88ac3243465337ba71d68e23ae7e3", size = 173814, upload-time = "2026-07-17T22:51:04.184Z" },
]
[[package]]
name = "xxhash"
version = "3.7.0"
+24 -13
View File
@@ -3,7 +3,8 @@ revision = 3
requires-python = ">=3.10"
resolution-markers = [
"python_full_version >= '3.15'",
"python_full_version >= '3.11' and python_full_version < '3.15'",
"python_full_version >= '3.12' and python_full_version < '3.15' and sys_platform == 'emscripten'",
"(python_full_version >= '3.11' and python_full_version < '3.15' and sys_platform != 'emscripten') or (python_full_version == '3.11.*' and sys_platform == 'emscripten')",
"python_full_version < '3.11'",
]
@@ -18,16 +19,16 @@ wheels = [
[[package]]
name = "anyio"
version = "4.13.0"
version = "4.14.2"
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/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" }
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" }
wheels = [
{ 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" },
{ 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" },
]
[[package]]
@@ -736,15 +737,15 @@ wheels = [
[[package]]
name = "httpcore2"
version = "2.5.0"
version = "2.12.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" }
sdist = { url = "https://files.pythonhosted.org/packages/be/ad/f4f0e57345f1870f3e8cb624e058d7eca6e5a27d33bcc3311d9b618734cd/httpcore2-2.12.0.tar.gz", hash = "sha256:9293522bba0aa7c4c8e9e3f040c16575bd8868e155a77fa30c7a9085a5eae648", size = 67548, upload-time = "2026-08-18T13:22:08.211Z" }
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" },
{ url = "https://files.pythonhosted.org/packages/d2/74/d370e55600d9bcfa0d9794b0166126d49291a3d2b20c268fc98c453a4948/httpcore2-2.12.0-py3-none-any.whl", hash = "sha256:7e04258ce01013d7d615e5b910a3b27fac937d7a95038227e79652b4ba3b4ceb", size = 83074, upload-time = "2026-08-18T13:22:05.854Z" },
]
[[package]]
@@ -802,18 +803,28 @@ wheels = [
[[package]]
name = "httpx2"
version = "2.5.0"
version = "2.12.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "httpcore2" },
{ name = "anyio", marker = "sys_platform != 'emscripten'" },
{ name = "httpcore2", marker = "sys_platform != 'emscripten'" },
{ name = "httpx2-jsfetch", marker = "python_full_version >= '3.12' and sys_platform == 'emscripten'" },
{ name = "idna" },
{ name = "truststore" },
{ name = "truststore", marker = "sys_platform != 'emscripten'" },
{ 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" }
sdist = { url = "https://files.pythonhosted.org/packages/7f/f8/579a8b51e42e38ee32647df9f08aa25643ae788e275cc625b199829c4671/httpx2-2.12.0.tar.gz", hash = "sha256:7631fe9887a8a2275f4a2540e053aa670fcc50742864a9ae7c66e609fdcf12cf", size = 100040, upload-time = "2026-08-18T13:22:09.086Z" }
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" },
{ url = "https://files.pythonhosted.org/packages/c8/95/411ba65569158e862368917aaf56597f3e5fa3b91b0502919638465a08f3/httpx2-2.12.0-py3-none-any.whl", hash = "sha256:cc8b6eecb8661c146b8f89a60e97456ee086e91a784ed31ac450c3a9e613dd36", size = 95427, upload-time = "2026-08-18T13:22:06.834Z" },
]
[[package]]
name = "httpx2-jsfetch"
version = "1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cd/c4/0e5636363151a2a1795e0a77617168b9ca438e1748ec05fc9b5687f93d64/httpx2_jsfetch-1.0.tar.gz", hash = "sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60", size = 6872, upload-time = "2026-08-07T00:13:07.492Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9b/43/832f631d32e4f1211caa2ba368317739fe71f0b8530e4c9d15dc454bac2a/httpx2_jsfetch-1.0-py3-none-any.whl", hash = "sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32", size = 6382, upload-time = "2026-08-07T00:13:06.567Z" },
]
[[package]]
+66 -11
View File
@@ -12,15 +12,23 @@ from typing import (
Generic,
Literal,
NamedTuple,
TypeVar,
final,
overload,
)
from warnings import warn
from langchain_core.messages import AnyMessage
from langchain_core.runnables import Runnable, RunnableConfig
from langgraph.checkpoint.base import BaseCheckpointSaver, CheckpointMetadata
from typing_extensions import NotRequired, TypeAliasType, TypedDict, Unpack, deprecated
from pydantic import TypeAdapter
from typing_extensions import (
NotRequired,
TypeAliasType,
TypedDict,
TypeVar,
Unpack,
deprecated,
)
from xxhash import xxh3_128_hexdigest
from langgraph._internal._cache import default_cache_key
@@ -36,6 +44,7 @@ from langgraph.warnings import LangGraphDeprecatedSinceV10, LangGraphDeprecatedS
# when used in standalone type aliases.
StateT = TypeVar("StateT")
OutputT = TypeVar("OutputT")
ResponseT = TypeVar("ResponseT", default=Any)
if TYPE_CHECKING:
from langgraph.pregel.protocol import PregelProtocol
@@ -572,7 +581,7 @@ _DEFAULT_INTERRUPT_ID = "placeholder-id"
@final
@dataclass(init=False, slots=True)
class Interrupt:
class Interrupt(Generic[ResponseT]):
"""Information about an interrupt that occurred in a node.
!!! version-added "Added in version 0.2.24"
@@ -596,13 +605,22 @@ class Interrupt:
id: str
"""The ID of the interrupt. Can be used to resume the interrupt directly."""
response_schema: type[ResponseT] | dict[str, Any] | None = None
"""Schema for the value expected when resuming this interrupt, if the graph provided one.
A surfaced interrupt carries JSON Schema (a `dict`); `type[ResponseT]` records the
Python type at construction so `Interrupt[Decision]` is meaningful to type checkers."""
def __init__(
self,
value: Any,
id: str = _DEFAULT_INTERRUPT_ID,
*,
response_schema: type[ResponseT] | dict[str, Any] | None = None,
**deprecated_kwargs: Unpack[DeprecatedKwargs],
) -> None:
self.value = value
self.response_schema = response_schema
if (
(ns := deprecated_kwargs.get("ns", MISSING)) is not MISSING
@@ -614,8 +632,18 @@ class Interrupt:
self.id = id
@classmethod
def from_ns(cls, value: Any, ns: str) -> Interrupt:
return cls(value=value, id=xxh3_128_hexdigest(ns.encode()))
def from_ns(
cls,
value: Any,
ns: str,
*,
response_schema: type[ResponseT] | dict[str, Any] | None = None,
) -> Interrupt[ResponseT]:
return cls(
value=value,
id=xxh3_128_hexdigest(ns.encode()),
response_schema=response_schema,
)
@property
@deprecated("`interrupt_id` is deprecated. Use `id` instead.", category=None)
@@ -848,7 +876,17 @@ class Command(Generic[N], ToolOutputMixin):
PARENT: ClassVar[Literal["__parent__"]] = "__parent__"
def interrupt(value: Any) -> Any:
@overload
def interrupt(value: Any, *, response_schema: type[ResponseT]) -> ResponseT: ...
@overload
def interrupt(value: Any, *, response_schema: dict[str, Any] | None = None) -> Any: ...
def interrupt(
value: Any, *, response_schema: dict[str, Any] | type | None = None
) -> Any:
"""Interrupt the graph with a resumable exception from within a node.
The `interrupt` function enables human-in-the-loop workflows by pausing graph
@@ -918,7 +956,7 @@ def interrupt(value: Any) -> Any:
for chunk in graph.stream({\"foo\": \"abc\"}, config):
print(chunk)
# > {'__interrupt__': (Interrupt(value='what is your age?', id='45fda8478b2ef754419799e10992af06'),)}
# > {'__interrupt__': (Interrupt(value='what is your age?', id='45fda8478b2ef754419799e10992af06', response_schema=None),)}
command = Command(resume=\"some input from a human!!!\")
@@ -931,12 +969,20 @@ def interrupt(value: Any) -> Any:
Args:
value: The value to surface to the client when the graph is interrupted.
response_schema: Optional schema for the value expected on resume, surfaced
to clients so they can render a typed input form. Accepts a JSON Schema
`dict` (used as-is, resume values are not validated), or a Pydantic model
class, `TypedDict`, or dataclass, which are converted to JSON Schema for
clients and used to validate the resume value; the validated object is
what `interrupt` returns.
Returns:
Any: On subsequent invocations within the same node (same task to be precise), returns the value provided during the first invocation
Any: On subsequent invocations within the same node (same task to be precise), returns the value provided during the first invocation,
validated against `response_schema` when one that supports validation was given.
Raises:
GraphInterrupt: On the first invocation within the node, halts execution and surfaces the provided value to the client.
pydantic.ValidationError: When a resume value does not match a Pydantic model, `TypedDict`, or dataclass `response_schema`.
"""
from langgraph._internal._constants import (
CONFIG_KEY_CHECKPOINT_NS,
@@ -948,27 +994,36 @@ def interrupt(value: Any) -> Any:
from langgraph.errors import GraphInterrupt
conf = get_config()["configurable"]
adapter = (
None
if response_schema is None or isinstance(response_schema, dict)
else TypeAdapter(response_schema)
)
# track interrupt index
scratchpad = conf[CONFIG_KEY_SCRATCHPAD]
idx = scratchpad.interrupt_counter()
# find previous resume values
if scratchpad.resume:
if idx < len(scratchpad.resume):
conf[CONFIG_KEY_SEND]([(RESUME, scratchpad.resume)])
return scratchpad.resume[idx]
v = scratchpad.resume[idx]
validated = adapter.validate_python(v) if adapter else v
conf[CONFIG_KEY_SEND]([(RESUME, scratchpad.resume[: idx + 1])])
return validated
# find current resume value
v = scratchpad.get_null_resume(True)
if v is not None:
assert len(scratchpad.resume) == idx, (scratchpad.resume, idx)
validated = adapter.validate_python(v) if adapter else v
scratchpad.resume.append(v)
conf[CONFIG_KEY_SEND]([(RESUME, scratchpad.resume)])
return v
return validated
# no resume value found
raise GraphInterrupt(
(
Interrupt.from_ns(
value=value,
ns=conf[CONFIG_KEY_CHECKPOINT_NS],
response_schema=adapter.json_schema() if adapter else response_schema,
),
)
)
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "langgraph"
version = "1.2.11"
version = "1.2.12"
description = "Building stateful, multi-actor applications with LLMs"
authors = []
requires-python = ">=3.10"
+153 -1
View File
@@ -1,9 +1,14 @@
from dataclasses import dataclass
from typing import Any
import pytest
from langgraph.checkpoint.base import BaseCheckpointSaver
from pydantic import BaseModel, ValidationError
from typing_extensions import TypedDict
from langgraph.graph import END, START, StateGraph
from langgraph.types import Durability
from langgraph.types import Command, Durability, Interrupt, interrupt
from tests.any_str import AnyStr
pytestmark = pytest.mark.anyio
@@ -90,3 +95,150 @@ async def test_interruption_without_state_updates_async(
assert (await graph.aget_state(thread)).next == ()
n_checkpoints = len([c async for c in graph.aget_state_history(thread)])
assert n_checkpoints == (5 if durability != "exit" else 3)
class Decision(BaseModel):
approved: bool
note: str | None = None
class DecisionDict(TypedDict):
approved: bool
@dataclass
class DecisionData:
approved: bool
RAW_SCHEMA = {"type": "object", "properties": {"approved": {"type": "boolean"}}}
@pytest.mark.parametrize(
("response_schema", "expected_schema", "expected_answer"),
[
(None, None, {"approved": True, "extra": 1}),
(RAW_SCHEMA, RAW_SCHEMA, {"approved": True, "extra": 1}),
(Decision, Decision.model_json_schema(), Decision(approved=True)),
(
DecisionDict,
{
"properties": {"approved": {"title": "Approved", "type": "boolean"}},
"required": ["approved"],
"title": "DecisionDict",
"type": "object",
},
{"approved": True},
),
(
DecisionData,
{
"properties": {"approved": {"title": "Approved", "type": "boolean"}},
"required": ["approved"],
"title": "DecisionData",
"type": "object",
},
DecisionData(approved=True),
),
],
ids=["none", "raw_dict", "pydantic", "typeddict", "dataclass"],
)
def test_interrupt_response_schema(
sync_checkpointer: BaseCheckpointSaver,
response_schema: Any,
expected_schema: dict[str, Any] | None,
expected_answer: Any,
) -> None:
class State(TypedDict):
answer: Any
def node(state: State) -> State:
return {
"answer": interrupt(
{"question": "approve?"}, response_schema=response_schema
)
}
graph = (
StateGraph(State)
.add_node("node", node)
.add_edge(START, "node")
.compile(checkpointer=sync_checkpointer)
)
config = {"configurable": {"thread_id": "1"}}
expected = Interrupt(
value={"question": "approve?"}, id=AnyStr(), response_schema=expected_schema
)
assert list(graph.stream({"answer": None}, config)) == [
{"__interrupt__": (expected,)}
]
assert graph.get_state(config).tasks[0].interrupts == (expected,)
assert graph.invoke(Command(resume={"approved": True, "extra": 1}), config) == {
"answer": expected_answer
}
@pytest.mark.parametrize("resume_style", ["null", "map"])
def test_interrupt_response_schema_rejects_invalid_resume(
sync_checkpointer: BaseCheckpointSaver, resume_style: str
) -> None:
class State(TypedDict):
answer: Any
def node(state: State) -> State:
return {"answer": interrupt("approve?", response_schema=Decision)}
graph = (
StateGraph(State)
.add_node("node", node)
.add_edge(START, "node")
.compile(checkpointer=sync_checkpointer)
)
config = {"configurable": {"thread_id": "1"}}
graph.invoke({"answer": None}, config)
[pending] = graph.get_state(config).tasks[0].interrupts
def resume(value: dict[str, Any]) -> Command:
return Command(resume=value if resume_style == "null" else {pending.id: value})
with pytest.raises(ValidationError, match="approved"):
graph.invoke(resume({"approved": "nope"}), config)
assert graph.invoke(resume({"approved": False}), config) == {
"answer": Decision(approved=False)
}
@pytest.mark.parametrize("resume_style", ["null", "id_map"])
def test_interrupt_response_schema_invalid_resume_after_earlier_interrupt(
sync_checkpointer: BaseCheckpointSaver, resume_style: str
) -> None:
class State(TypedDict):
answer: Any
def node(state: State) -> State:
first = interrupt("first")
second = interrupt("approve?", response_schema=Decision)
return {"answer": [first, second]}
graph = (
StateGraph(State)
.add_node("node", node)
.add_edge(START, "node")
.compile(checkpointer=sync_checkpointer)
)
config = {"configurable": {"thread_id": "1"}}
graph.invoke({"answer": None}, config)
graph.invoke(Command(resume="ok"), config)
[pending] = graph.get_state(config).tasks[0].interrupts
def resume(value: dict[str, Any]) -> Command:
return Command(resume=value if resume_style == "null" else {pending.id: value})
with pytest.raises(ValidationError, match="approved"):
graph.invoke(resume({"approved": "nope"}), config)
assert graph.invoke(resume({"approved": True}), config) == {
"answer": ["ok", Decision(approved=True)]
}
+2
View File
@@ -5583,6 +5583,7 @@ def test_falsy_return_from_task(sync_checkpointer: BaseCheckpointSaver):
"interrupts": [
{
"id": AnyStr(),
"response_schema": None,
"value": "test",
},
],
@@ -5627,6 +5628,7 @@ def test_falsy_return_from_task(sync_checkpointer: BaseCheckpointSaver):
"interrupts": (
{
"id": AnyStr(),
"response_schema": None,
"value": "test",
},
),
+4 -4
View File
@@ -1437,7 +1437,7 @@ wheels = [
[[package]]
name = "langgraph"
version = "1.2.11"
version = "1.2.12"
source = { editable = "." }
dependencies = [
{ name = "langchain-core" },
@@ -3404,11 +3404,11 @@ wheels = [
[[package]]
name = "soupsieve"
version = "2.8.4"
version = "2.9"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/47/2c/0a5f6f8ee0d5589e48c7640213ed5175d52cf540a06725b628cc1a45d6ce/soupsieve-2.8.4.tar.gz", hash = "sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e", size = 121110, upload-time = "2026-05-24T13:55:57.154Z" }
sdist = { url = "https://files.pythonhosted.org/packages/80/f1/93422647dd7e461f23d254e6b2bfa687a85b53aeb4903fcdbb74474d4584/soupsieve-2.9.tar.gz", hash = "sha256:acee8417325c5653e1377dc31eccad59eb82cbc65942afe6174c53b3aaad63fc", size = 122122, upload-time = "2026-07-19T01:35:18.425Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65", size = 37304, upload-time = "2026-05-24T13:55:55.406Z" },
{ url = "https://files.pythonhosted.org/packages/7b/d6/3185ab5ad1280319b31986898f3206dd7227cd75e293d4dba2a5e6bf27a0/soupsieve-2.9-py3-none-any.whl", hash = "sha256:a2b2c76d67df2382d245409fd71e321a571717e58463efa32ace87dcadac2c12", size = 37387, upload-time = "2026-07-19T01:35:17.106Z" },
]
[[package]]
+1 -1
View File
@@ -286,7 +286,7 @@ wheels = [
[[package]]
name = "langgraph"
version = "1.2.11"
version = "1.2.12"
source = { editable = "../langgraph" }
dependencies = [
{ name = "langchain-core" },
+1 -1
View File
@@ -3,7 +3,7 @@ from langgraph_sdk.client import get_client, get_sync_client
from langgraph_sdk.encryption import Encryption
from langgraph_sdk.encryption.types import DecryptResult, EncryptionContext
__version__ = "0.4.4"
__version__ = "0.4.5"
__all__ = [
"Auth",
+2
View File
@@ -295,6 +295,8 @@ class Interrupt(TypedDict):
"""The value associated with the interrupt."""
id: str
"""The ID of the interrupt. Can be used to resume the interrupt."""
response_schema: NotRequired[dict[str, Any]]
"""JSON Schema for the value expected when resuming this interrupt, if the graph provided one."""
class Thread(TypedDict):
@@ -29,7 +29,10 @@ def test_sync_extension_projection_yields_matching_custom_payloads():
{"name": "progress", "step": 1},
{"name": "progress", "step": 2},
]
assert "custom:progress" in fake.stream_request_bodies[-1]["channels"]
assert any(
"custom:progress" in body.get("channels", [])
for body in fake.stream_request_bodies
)
def test_sync_extension_projection_supports_namespace_scope_on_subgraph_handle():
+33 -13
View File
@@ -17,16 +17,16 @@ wheels = [
[[package]]
name = "anyio"
version = "4.12.1"
version = "4.15.1"
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'" },
{ name = "typing-extensions", version = "4.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.15'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" }
sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
{ url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" },
]
[[package]]
@@ -181,7 +181,7 @@ name = "exceptiongroup"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
{ name = "typing-extensions", version = "4.16.0", source = { registry = "https://pypi.org/simple" } },
]
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 = [
@@ -277,7 +277,8 @@ dependencies = [
{ name = "pydantic" },
{ name = "pyyaml" },
{ name = "tenacity" },
{ name = "typing-extensions" },
{ name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.15'" },
{ name = "typing-extensions", version = "4.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.15'" },
{ name = "uuid-utils" },
]
sdist = { url = "https://files.pythonhosted.org/packages/90/12/aff76ca89c219ebe6f9dd3c5dbc4e3b1cf5450e9fc7037dccad23d45cd7a/langchain_core-1.6.1.tar.gz", hash = "sha256:1b156cb395aac4f009a8a1b38a574c7d948fe2d5f74c96e0d8a5017b4149e04f", size = 1003359, upload-time = "2026-08-27T19:31:14.956Z" }
@@ -290,7 +291,8 @@ name = "langchain-protocol"
version = "0.0.19"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
{ name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.15'" },
{ name = "typing-extensions", version = "4.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.15'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/14/56/913599f2f9cec8524868929f12d72b2ede377a6056ca8a40a32bdadfa535/langchain_protocol-0.0.19.tar.gz", hash = "sha256:79d90a1425122ac87e8052e2ec054fbd09c3edbf341bdfb6397112a495c7bf8c", size = 6265, upload-time = "2026-08-26T21:12:00.703Z" }
wheels = [
@@ -299,7 +301,7 @@ wheels = [
[[package]]
name = "langgraph"
version = "1.2.11"
version = "1.2.12"
source = { editable = "../langgraph" }
dependencies = [
{ name = "langchain-core" },
@@ -726,7 +728,8 @@ source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
{ name = "pydantic-core" },
{ name = "typing-extensions" },
{ name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.15'" },
{ name = "typing-extensions", version = "4.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.15'" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/53/ef/fc4f868f4e2cee79f863883abffceff107875f569b848507319842d2a681/pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08", size = 845750, upload-time = "2026-08-28T14:04:00.916Z" }
@@ -739,7 +742,8 @@ name = "pydantic-core"
version = "2.46.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
{ name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.15'" },
{ name = "typing-extensions", version = "4.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.15'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/af/f9/8a06bea35ef8daf588f707784c973a7046e0034c8d8cfb08828eeffb8b75/pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc", size = 472262, upload-time = "2026-08-28T10:01:31.677Z" }
wheels = [
@@ -884,7 +888,7 @@ source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" },
{ name = "pytest" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
{ name = "typing-extensions", version = "4.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" }
wheels = [
@@ -1037,7 +1041,7 @@ version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
{ name = "typing-extensions", version = "4.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b5/b4/205b0d5241d934e8add0c38aa924c4f9fb7330834ff11e5444db964ec3f9/starlette-1.6.0.tar.gz", hash = "sha256:d4e3ac5e546444960c710297a3c9fc3f7ebae1b7e963f3d36173b49da535be9b", size = 2716969, upload-time = "2026-08-08T18:27:57.512Z" }
wheels = [
@@ -1136,17 +1140,33 @@ wheels = [
name = "typing-extensions"
version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.15'",
]
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]
[[package]]
name = "typing-extensions"
version = "4.16.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.15'",
]
sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
]
[[package]]
name = "typing-inspection"
version = "0.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
{ name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.15'" },
{ name = "typing-extensions", version = "4.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.15'" },
]
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 = [