python: deepseek-harness SDK and runtime carrier packages
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
README.md: 56199c1daaac4d55a29dae8852277f0ba77238b2
|
||||
README.zh.md: e34f3afd455926bd6b09d3451509594cee78ad88
|
||||
@@ -0,0 +1,68 @@
|
||||
# DeepSeek Harness Python SDK
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
Python packages for driving DeepSeek Harness as a subprocess: a client SDK that spawns the `dsh-jsonrpc-agent` binary and talks newline-delimited JSON-RPC over stdio. The runtime carrier is the single-file executable produced by this repo; design, build, and acceptance details live in [docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md](../docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md).
|
||||
|
||||
## Packages
|
||||
|
||||
| Directory | Dist / module | Role |
|
||||
|---|---|---|
|
||||
| [sdk](sdk/) | `deepseek-harness` / `deepseek_harness` | Client SDK: the `DeepSeekHarness` high-level turns API and the lower-level `HarnessClient` JSON-RPC client |
|
||||
| [sdk-runtime](sdk-runtime/) | `deepseek-harness-runtime-bin` / `deepseek_harness_runtime` | Runtime carrier: locates the bundled runtime binaries and ships the default agent configuration |
|
||||
|
||||
## Building the runtime executable
|
||||
|
||||
The platform executables are build artifacts, not checked into git. From the repo root:
|
||||
|
||||
```sh
|
||||
pnpm install
|
||||
pnpm exec tsx scripts/build-exe-for-python-sdk.ts # host platform, ~2 min
|
||||
pnpm exec tsx scripts/build-exe-for-python-sdk.ts --skip-build # lib/ artifacts already built
|
||||
pnpm exec tsx scripts/build-exe-for-python-sdk.ts --targets=node24-linux-x64,node24-linux-arm64,node24-macos-arm64
|
||||
```
|
||||
|
||||
Products land in `dist-exe/` (CI artifact shape) and are synced into this package at `sdk-runtime/src/deepseek_harness_runtime/runtime/dsh-jsonrpc-agent-pkg-<platform>-<arch>` (platform: `linux`/`macos`; arch: `x64`/`arm64`) — after a build the SDK finds the executable with no further setup. Alternatively, download the platform artifact from the `build-exe-for-python-sdk` CI workflow (manual dispatch) and place it at that same path. Which plugins the exe bundles and how the carriers are organized: [sdk-runtime README](sdk-runtime/README.md); the build also refreshes the dev-only node carrier (see "against the Node source" below).
|
||||
|
||||
## Validating the SDK against the executable
|
||||
|
||||
```sh
|
||||
export UV_PROJECT_ENVIRONMENT="$PWD/tmp/py-sdk-venv" # keep the venv out of python/
|
||||
uv sync --project python/sdk --group test
|
||||
uv run --project python/sdk pytest python/sdk/tests/test_bundled_runtime.py # boots the real carriers
|
||||
uv run --project python/sdk pytest # full suite; keyless tests included
|
||||
```
|
||||
|
||||
For an interactive check (needs `DEEPSEEK_API_KEY` in the environment or the repo-root `.env`):
|
||||
|
||||
```python
|
||||
from deepseek_harness import DeepSeekHarness
|
||||
print(DeepSeekHarness().run("say hi").final_response) # auto-resolution picks the bundled exe
|
||||
```
|
||||
|
||||
## Running the SDK against the Node source (no executable)
|
||||
|
||||
Two flavors, both for repo members:
|
||||
|
||||
- **Built node carrier** — set `DSH_RUNTIME_MODE=node` and the SDK runs `runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-agent/lib/bin.js` on the system Node (>= 22.19). The tree is refreshed on every build-script run and is the same dependency closure the exe snapshots, so plugin semantics are identical. Never auto-selected, never distributed.
|
||||
- **Unbuilt source (tsx)** — point the client straight at the bin's TypeScript source for edit-run loops and debugging: `launch_args_override=("./node_modules/.bin/tsx", "packages/ui/jsonrpc-agent/src/bin.ts")` with `cwd` at the repo root, plus a config via `cordis=...` (or rely on the default-config injection). [sdk/tests/manual_sdk_agent_smoke.py](sdk/tests/manual_sdk_agent_smoke.py) is the worked example.
|
||||
|
||||
## Distributing the Python packages
|
||||
|
||||
Build one wheel per package; the runtime wheel embeds whatever executables are present under `runtime/` at build time (and always the default `cordis.yml`), and excludes `runtime/node/`:
|
||||
|
||||
```sh
|
||||
(cd python/sdk-runtime && uv build) # or: hatch build
|
||||
(cd python/sdk && uv build)
|
||||
pip install dist/deepseek_harness_runtime_bin-*.whl dist/deepseek_harness-*.whl
|
||||
```
|
||||
|
||||
Once the binaries are in place (built or downloaded), installing the two wheels (or `pip install ./python/sdk-runtime ./python/sdk` straight from the directories) gives a working zero-config `DeepSeekHarness()`. Two pre-release caveats: the runtime wheel is currently tagged `py3-none-any` while embedding platform-specific binaries — build it with only the matching platform's exe in place (one wheel per platform), or drop all three exes in for a fat universal wheel (~500 MB); and versioning/publishing policy (`0.0.0-dev`, no registry) is deliberately unset until the first tagged release.
|
||||
|
||||
## Zero-config semantics
|
||||
|
||||
The runtime binary itself always requires an explicit config (`$DSH_CORDIS_CONFIG`, or a config path as the first argv argument), has no built-in fallback, and boots only what the config lists. Zero-config is SDK wrapper behavior: when the caller uses no explicit channel, the client injects the runtime package's checked-in default configuration ([runtime/cordis.yml](sdk-runtime/src/deepseek_harness_runtime/runtime/cordis.yml)) via `DSH_CORDIS_CONFIG`; any explicit channel wins and disables the injection. The full injection conditions live in the [sdk README](sdk/README.md); the default config's contents and the hard semantic in the [sdk-runtime README](sdk-runtime/README.md).
|
||||
|
||||
## Test layout
|
||||
|
||||
`test_client.py` is fully keyless (a Python fake runtime is the peer). `test_bundled_runtime.py` boots each bundled carrier and skips per carrier when its artifact is missing. `test_runtime_resolution.py` covers the carrier-resolution rules without spawning anything.
|
||||
@@ -0,0 +1,68 @@
|
||||
# DeepSeek Harness Python SDK
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
以子进程方式驱动 DeepSeek Harness 的 Python 包:客户端 SDK spawn `dsh-jsonrpc-agent` 二进制,在 stdio 上以换行分隔的 JSON-RPC 与之通信。运行时载体是本仓库产出的单文件可执行文件;设计、构建与验收细节见 [docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md](../docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md)。
|
||||
|
||||
## 包
|
||||
|
||||
| 目录 | Dist / 模块 | 职责 |
|
||||
|---|---|---|
|
||||
| [sdk](sdk/) | `deepseek-harness` / `deepseek_harness` | 客户端 SDK:高层回合 API `DeepSeekHarness` 与低层 JSON-RPC 客户端 `HarnessClient` |
|
||||
| [sdk-runtime](sdk-runtime/) | `deepseek-harness-runtime-bin` / `deepseek_harness_runtime` | 运行时载体:定位内置的运行时二进制,并携带默认的 agent(智能体)配置 |
|
||||
|
||||
## 构建运行时可执行文件
|
||||
|
||||
各平台可执行文件是构建产物,不检入 git。在仓库根目录执行:
|
||||
|
||||
```sh
|
||||
pnpm install
|
||||
pnpm exec tsx scripts/build-exe-for-python-sdk.ts # host platform, ~2 min
|
||||
pnpm exec tsx scripts/build-exe-for-python-sdk.ts --skip-build # lib/ artifacts already built
|
||||
pnpm exec tsx scripts/build-exe-for-python-sdk.ts --targets=node24-linux-x64,node24-linux-arm64,node24-macos-arm64
|
||||
```
|
||||
|
||||
产物落入 `dist-exe/`(CI 产物形态),并同步进本包的 `sdk-runtime/src/deepseek_harness_runtime/runtime/dsh-jsonrpc-agent-pkg-<platform>-<arch>`(platform:`linux`/`macos`;arch:`x64`/`arm64`),构建完成后 SDK 不需要额外设置就能找到可执行文件。也可以从 `build-exe-for-python-sdk` CI workflow(手动触发)下载对应平台的产物放到同一路径。exe 内置哪些插件、载体如何组织,见 [sdk-runtime README](sdk-runtime/README.md);构建还会顺带刷新仅供开发用的 node 载体(见下文「对着 Node 源码运行」)。
|
||||
|
||||
## 用可执行文件验证 SDK
|
||||
|
||||
```sh
|
||||
export UV_PROJECT_ENVIRONMENT="$PWD/tmp/py-sdk-venv" # keep the venv out of python/
|
||||
uv sync --project python/sdk --group test
|
||||
uv run --project python/sdk pytest python/sdk/tests/test_bundled_runtime.py # boots the real carriers
|
||||
uv run --project python/sdk pytest # full suite; keyless tests included
|
||||
```
|
||||
|
||||
交互式验证(需要环境变量或仓库根 `.env` 中的 `DEEPSEEK_API_KEY`):
|
||||
|
||||
```python
|
||||
from deepseek_harness import DeepSeekHarness
|
||||
print(DeepSeekHarness().run("say hi").final_response) # auto-resolution picks the bundled exe
|
||||
```
|
||||
|
||||
## 对着 Node 源码运行 SDK(不用可执行文件)
|
||||
|
||||
两种方式,均面向仓库成员:
|
||||
|
||||
- **已构建的 node 载体**——设置 `DSH_RUNTIME_MODE=node`,SDK 会用系统 Node(>= 22.19)运行 `runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-agent/lib/bin.js`。这棵树每次跑构建脚本都会刷新,与 exe 打进 VFS 的是同一份依赖闭包,插件语义一致。不会被自动选中,也不进入分发物。
|
||||
- **未构建的源码(tsx)**——把客户端直接指向 bin 的 TypeScript 源码,用于编辑-运行循环与调试:`launch_args_override=("./node_modules/.bin/tsx", "packages/ui/jsonrpc-agent/src/bin.ts")`,`cwd` 设为仓库根,另经 `cordis=...` 传入配置(或依赖默认配置注入)。[sdk/tests/manual_sdk_agent_smoke.py](sdk/tests/manual_sdk_agent_smoke.py) 是现成范例。
|
||||
|
||||
## 分发 Python 包
|
||||
|
||||
每个包各构建一个 wheel;runtime wheel 会内嵌构建时 `runtime/` 下存在的全部可执行文件(以及始终包含的默认 `cordis.yml`),并排除 `runtime/node/`:
|
||||
|
||||
```sh
|
||||
(cd python/sdk-runtime && uv build) # or: hatch build
|
||||
(cd python/sdk && uv build)
|
||||
pip install dist/deepseek_harness_runtime_bin-*.whl dist/deepseek_harness-*.whl
|
||||
```
|
||||
|
||||
二进制就位(构建或下载)后,安装这两个 wheel(或直接 `pip install ./python/sdk-runtime ./python/sdk` 从目录安装),就得到零配置可用的 `DeepSeekHarness()`。两个 pre-release 注意事项:runtime wheel 目前的标签是 `py3-none-any`,内容却是平台相关的二进制——构建时只放匹配平台的 exe(每平台一个 wheel),或把三个 exe 全部放入做成通用胖 wheel(约 500 MB);版本与发布策略(`0.0.0-dev`、无 registry)刻意留待首个 tagged release 再定。
|
||||
|
||||
## 零配置语义
|
||||
|
||||
运行时二进制本身始终要求显式配置(`$DSH_CORDIS_CONFIG`,或作为首个 argv 参数的配置路径),没有内置兜底,也只启动配置里列出的东西。零配置是 SDK 包装层的行为:调用方没有走任何显式通道时,客户端把 runtime 包检入的默认配置([runtime/cordis.yml](sdk-runtime/src/deepseek_harness_runtime/runtime/cordis.yml))注入 `DSH_CORDIS_CONFIG`;任一显式通道存在即胜出并禁用注入。注入条件的完整定义见 [sdk README](sdk/README.md),默认配置的内容与硬语义见 [sdk-runtime README](sdk-runtime/README.md)。
|
||||
|
||||
## 测试布局
|
||||
|
||||
`test_client.py` 完全无需 key(对端是 Python 假运行时)。`test_bundled_runtime.py` 逐个启动内置载体,某载体产物缺失时对应用例跳过。`test_runtime_resolution.py` 覆盖载体解析规则,不 spawn 任何进程。
|
||||
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
README.md: 5543b0ba00816c230dbe4e5bba11b7bf4b38096d
|
||||
README.zh.md: 163b6d8e8373f0f770a5d95be688785907108b8a
|
||||
@@ -0,0 +1,27 @@
|
||||
# DeepSeek Harness Runtime Wheel
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
Runtime carrier package for the Python SDK (dist `deepseek-harness-runtime-bin`, module `deepseek_harness_runtime`): it locates the bundled runtime binaries the `deepseek-harness` client spawns, and ships the default configuration behind zero-config runs.
|
||||
|
||||
## Runtime carriers
|
||||
|
||||
Two carriers coexist under `src/deepseek_harness_runtime/runtime/`, both injected by the repo's `scripts/build-exe-for-python-sdk.ts` build and both gitignored:
|
||||
|
||||
- **exe (production)** — single-file executables `dsh-jsonrpc-agent-pkg-<platform>-<arch>` (platform: `linux`/`macos`; arch: `x64`/`arm64`). No Node installation needed on the target machine. This is the only carrier that ships in wheel/sdist distributions.
|
||||
- **node (dev-only)** — the full deploy closure under `runtime/node/` (`package.json` + `node_modules/`), executed as `node runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-agent/lib/bin.js` on a system Node >= 22.19. It is the current checkout's source build, meant for repo-local development and verification only; it is never selected automatically and is excluded from distributions.
|
||||
|
||||
Both carriers hold the same content, defined once: the [package.json](package.json) at this package's root is the deploy root of the single-exe pipeline — a pure dependency manifest (no code of its own) whose dependency closure IS both the plugin set compiled into the exe and the tree materialized into `runtime/node/`. Adding a plugin to the distribution means adding one dependency line there and rebuilding.
|
||||
|
||||
Missing carriers raise `FileNotFoundError` naming the acquisition routes: build via `scripts/build-exe-for-python-sdk.ts` in a deepseek-harness checkout, or download the platform artifact of the `build-exe-for-python-sdk` CI workflow and place it in the runtime directory. Acquisition strategy is deliberately separate from the lookup interface, so an on-demand download can replace it later without touching callers.
|
||||
|
||||
## Resolution API
|
||||
|
||||
- `resolve_bundled_launch_args(mode=None) -> tuple[str, ...]` — the argv tuple that launches the bundled runtime: `(exe_path,)` in exe mode, `(node_path, bin_js_path)` in node mode. Mode selection: explicit argument > `DSH_RUNTIME_MODE` env var (`exe` | `node`) > automatic. Automatic resolution finds the production exe ONLY — the dev-only node carrier must be opted into explicitly so a production deployment can never silently ride on a source build.
|
||||
- `bundled_runtime_path() -> Path` — the platform exe path (exe carrier only; the node carrier has no single-path equivalent and launches via the argv tuple above).
|
||||
- `bundled_default_config_path() -> Path` — the checked-in default config (see below).
|
||||
- `bundled_package_dir() -> Path` — the installed package data root.
|
||||
|
||||
## Zero-config design
|
||||
|
||||
The runtime binary always demands an explicit config (`$DSH_CORDIS_CONFIG`, or a config path as an argv positional argument) and exits loudly without one — that hard semantic is part of the runtime's design and this package does not soften it. The bin (`dsh-jsonrpc-agent`) boots only the plugins the config lists; the serving surface (the stdio JSON-RPC server) is itself one of its entries (`@deepseek-ai/dsh-jsonrpc`), and without it the booted agent has no channel to the outside. This package checks in `runtime/cordis.yml` (the JSON-RPC serving entry, agent core, preloaded DeepSeek adapter, JSONL session persistence, local bash, each parameterized by the `DSH_*` env vars the SDK sets); when the caller uses no explicit config channel, the `deepseek_harness` client injects that file's path via `DSH_CORDIS_CONFIG` (injection conditions: [sdk README](../sdk/README.md)). Zero-config is thus an explicit, visible parameter pass in the wrapper, not a hidden fallback in the runtime.
|
||||
@@ -0,0 +1,27 @@
|
||||
# DeepSeek Harness 运行时 wheel
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
Python SDK 的运行时载体包(dist 名 `deepseek-harness-runtime-bin`,模块名 `deepseek_harness_runtime`):它定位 `deepseek-harness` 客户端要 spawn 的内置运行时二进制,并附带支撑零配置运行的默认配置。
|
||||
|
||||
## 运行时载体
|
||||
|
||||
两种载体并存于 `src/deepseek_harness_runtime/runtime/` 之下,均由仓库的 `scripts/build-exe-for-python-sdk.ts` 构建注入,且均被 gitignore:
|
||||
|
||||
- **exe(生产)**——单文件可执行程序 `dsh-jsonrpc-agent-pkg-<platform>-<arch>`(platform:`linux`/`macos`;arch:`x64`/`arm64`)。目标机器无需安装 Node。这是唯一进入 wheel/sdist 分发物的载体。
|
||||
- **node(仅限开发)**——`runtime/node/` 下的完整 deploy 闭包(`package.json` + `node_modules/`),在系统 Node >= 22.19 上以 `node runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-agent/lib/bin.js` 执行。它是当前检出的源码构建,仅用于仓库本地的开发与验证;不会被自动选中,也不进入分发物。
|
||||
|
||||
两种载体承载相同的内容,且只定义一次:本包根目录的 [package.json](package.json) 是 single-exe 流水线的 deploy root——一份零代码的纯依赖 manifest,其依赖闭包既是编译进 exe 的插件集,也是物化到 `runtime/node/` 的文件树。往分发物里加插件,就是在那里加一行依赖再重新构建。
|
||||
|
||||
载体缺失时抛出 `FileNotFoundError` 并写明获取途径:在 deepseek-harness 检出中经 `scripts/build-exe-for-python-sdk.ts` 构建,或下载 `build-exe-for-python-sdk` CI 工作流的对应平台产物并放入 runtime 目录。获取策略与查找接口刻意分离,之后可以换成按需下载而不动任何调用方。
|
||||
|
||||
## 解析 API
|
||||
|
||||
- `resolve_bundled_launch_args(mode=None) -> tuple[str, ...]`——启动内置运行时的 argv 元组:exe 模式下为 `(exe_path,)`,node 模式下为 `(node_path, bin_js_path)`。模式选择:显式参数 > `DSH_RUNTIME_MODE` 环境变量(`exe` | `node`)> 自动。自动解析只找生产 exe——仅限开发的 node 载体必须显式选用,从而生产部署绝不会悄悄跑在源码构建上。
|
||||
- `bundled_runtime_path() -> Path`——平台 exe 路径(仅 exe 载体;node 载体没有单一路径的等价物,经由上面的 argv 元组启动)。
|
||||
- `bundled_default_config_path() -> Path`——检入的默认配置(见下文)。
|
||||
- `bundled_package_dir() -> Path`——已安装包的数据根目录。
|
||||
|
||||
## 零配置设计
|
||||
|
||||
运行时二进制始终要求显式配置(`$DSH_CORDIS_CONFIG`,或作为 argv 位置参数的配置路径),缺了就报错退出——这一硬语义是运行时设计的一部分,本包不软化它。bin(`dsh-jsonrpc-agent`)只启动配置里列出的插件;服务面(stdio JSON-RPC 服务器)也是其中一个条目(`@deepseek-ai/dsh-jsonrpc`),缺了它启动出的 agent 没有对外通道。本包检入 `runtime/cordis.yml`(JSON-RPC 服务条目、agent core、预载的 DeepSeek 适配器、JSONL 会话持久化、本地 bash,各项由 SDK 设置的 `DSH_*` 环境变量参数化);调用方未走任何显式配置通道时,`deepseek_harness` 客户端把该文件路径注入 `DSH_CORDIS_CONFIG`(注入条件见 [sdk README](../sdk/README.md))。零配置因此是 wrapper 里一次显式、可见的参数传递,不是运行时里的隐藏回退。
|
||||
@@ -0,0 +1,21 @@
|
||||
[build-system]
|
||||
requires = ["hatchling>=1.24.0"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "deepseek-harness-runtime-bin"
|
||||
version = "0.0.0-dev"
|
||||
description = "Pinned DeepSeek Harness runtime for the Python SDK"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
license = { text = "BSD-3-Clause" }
|
||||
|
||||
# Distributions carry the platform executables (build-injected, VCS-ignored —
|
||||
# hence `artifacts`) and the checked-in runtime/cordis.yml; the dev-only node
|
||||
# closure under runtime/node/ is explicitly excluded from wheel and sdist.
|
||||
[tool.hatch.build]
|
||||
artifacts = ["src/deepseek_harness_runtime/runtime/dsh-jsonrpc-agent-*"]
|
||||
exclude = ["src/deepseek_harness_runtime/runtime/node"]
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/deepseek_harness_runtime"]
|
||||
@@ -0,0 +1,151 @@
|
||||
"""Locate the bundled DeepSeek Harness SDK runtime shipped with this package.
|
||||
|
||||
Two runtime carriers coexist under ``runtime/``, both injected by the repo's
|
||||
``scripts/build-exe-for-python-sdk.ts`` build (neither is checked into git):
|
||||
|
||||
- **exe (production)**: single-file executables named
|
||||
``dsh-jsonrpc-agent-pkg-<platform>-<arch>`` (platform in {linux, macos}, arch in
|
||||
{x64, arm64}); the target machine needs no Node installation.
|
||||
- **node (dev-only)**: the full deploy closure under ``runtime/node/``
|
||||
(``package.json`` + ``node_modules/``), executed as ``node
|
||||
runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-agent/lib/bin.js`` on a
|
||||
system Node >= 22.19. It is the current checkout's source build, never
|
||||
selected automatically, and excluded from wheel/sdist distributions.
|
||||
|
||||
``runtime/cordis.yml`` IS checked in: it is the default agent configuration
|
||||
the client SDK injects via ``$DSH_CORDIS_CONFIG`` for zero-config runs — the
|
||||
runtime itself always requires an explicit config and has no built-in
|
||||
fallback.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
PACKAGE_METADATA_FILENAME = "deepseek-harness-runtime.json"
|
||||
|
||||
RUNTIME_MODE_ENV_VAR = "DSH_RUNTIME_MODE"
|
||||
|
||||
_PLATFORM_TAGS = {"linux": "linux", "darwin": "macos"}
|
||||
_ARCH_TAGS = {"x86_64": "x64", "amd64": "x64", "arm64": "arm64", "aarch64": "arm64"}
|
||||
|
||||
_EXE_ACQUISITION_HINT = (
|
||||
"Two ways to get the executable: run `scripts/build-exe-for-python-sdk.ts` (via tsx) in a "
|
||||
"deepseek-harness checkout, or download the platform artifact of the `build-exe-for-python-sdk` "
|
||||
"CI workflow; place the file in this package's runtime/ directory. For local development "
|
||||
"against a repo source build, explicitly select the dev-only node carrier with "
|
||||
f"{RUNTIME_MODE_ENV_VAR}=node (or resolve_bundled_launch_args('node'))."
|
||||
)
|
||||
|
||||
|
||||
def bundled_package_dir() -> Path:
|
||||
"""Root directory of the installed runtime package data (the directory of this module)."""
|
||||
root = Path(__file__).resolve().parent
|
||||
metadata = root / PACKAGE_METADATA_FILENAME
|
||||
if not metadata.is_file():
|
||||
raise FileNotFoundError(f"deepseek-harness-runtime-bin is missing {metadata}")
|
||||
return root
|
||||
|
||||
|
||||
def bundled_default_config_path() -> Path:
|
||||
"""Path of the checked-in default runtime configuration (``runtime/cordis.yml``).
|
||||
|
||||
The client SDK injects this path via ``$DSH_CORDIS_CONFIG`` when the caller
|
||||
supplies no config and the launch resolves to the bundled runtime — the
|
||||
runtime binary itself always demands an explicit config.
|
||||
"""
|
||||
path = bundled_package_dir() / "runtime" / "cordis.yml"
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError(
|
||||
f"deepseek-harness-runtime-bin is missing the default runtime config at {path}"
|
||||
)
|
||||
return path
|
||||
|
||||
|
||||
def bundled_runtime_path() -> Path:
|
||||
"""Absolute path of the bundled single-file runtime executable for the current platform.
|
||||
|
||||
Raises FileNotFoundError when the platform is unsupported or the executable
|
||||
has not been placed into this package; the message names the acquisition
|
||||
routes (acquisition strategy is deliberately separate from this lookup
|
||||
interface, so an on-demand download can replace it without touching
|
||||
callers).
|
||||
"""
|
||||
tag = _current_platform_tag()
|
||||
path = bundled_package_dir() / "runtime" / f"dsh-jsonrpc-agent-pkg-{tag}"
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError(
|
||||
f"deepseek-harness-runtime-bin is missing the runtime executable at {path}. "
|
||||
+ _EXE_ACQUISITION_HINT
|
||||
)
|
||||
return path
|
||||
|
||||
|
||||
def resolve_bundled_launch_args(mode: str | None = None) -> tuple[str, ...]:
|
||||
"""The argv tuple that launches the bundled runtime.
|
||||
|
||||
Mode selection: the explicit ``mode`` argument wins, then the
|
||||
``DSH_RUNTIME_MODE`` environment variable (``exe`` | ``node``), then
|
||||
automatic resolution. Automatic resolution finds the production exe ONLY —
|
||||
the dev-only node carrier must be selected explicitly so a production
|
||||
deployment can never silently ride on a source build. Returns
|
||||
``(exe_path,)`` in exe mode and ``(node_path, bin_js_path)`` in node mode;
|
||||
raises FileNotFoundError when the selected carrier is unavailable and
|
||||
ValueError for an unknown mode value.
|
||||
"""
|
||||
selected = mode if mode is not None else os.environ.get(RUNTIME_MODE_ENV_VAR)
|
||||
if selected is None or selected == "exe":
|
||||
return (str(bundled_runtime_path()),)
|
||||
if selected == "node":
|
||||
return _node_launch_args()
|
||||
raise ValueError(
|
||||
f"unsupported DeepSeek Harness runtime mode {selected!r}: expected 'exe' or 'node' "
|
||||
f"(explicit argument or ${RUNTIME_MODE_ENV_VAR})"
|
||||
)
|
||||
|
||||
|
||||
def _current_platform_tag() -> str:
|
||||
plat = _PLATFORM_TAGS.get(sys.platform)
|
||||
arch = _ARCH_TAGS.get(platform.machine().lower())
|
||||
if plat is None or arch is None:
|
||||
raise FileNotFoundError(
|
||||
"no bundled dsh-jsonrpc-agent executable exists for this platform "
|
||||
f"(sys.platform={sys.platform!r}, machine={platform.machine()!r}); supported: "
|
||||
"linux/macos on x64/arm64. " + _EXE_ACQUISITION_HINT
|
||||
)
|
||||
return f"{plat}-{arch}"
|
||||
|
||||
|
||||
def _node_launch_args() -> tuple[str, str]:
|
||||
node_root = bundled_package_dir() / "runtime" / "node"
|
||||
bin_js = (
|
||||
node_root / "node_modules" / "@deepseek-ai" / "dsh-jsonrpc-agent" / "lib" / "bin.js"
|
||||
)
|
||||
if not bin_js.is_file():
|
||||
raise FileNotFoundError(
|
||||
f"the dev-only node runtime closure is missing at {node_root} "
|
||||
f"(no {bin_js}); run `scripts/build-exe-for-python-sdk.ts` in a deepseek-harness "
|
||||
"checkout, which builds and copies the deploy closure here. The node carrier "
|
||||
"is for repo-local development only — production uses the single-file exe."
|
||||
)
|
||||
node = shutil.which("node")
|
||||
if node is None:
|
||||
raise FileNotFoundError(
|
||||
"the node runtime mode needs a system `node` (>=22.19) on PATH; "
|
||||
"install Node.js or use the exe mode"
|
||||
)
|
||||
return (node, str(bin_js))
|
||||
|
||||
|
||||
__all__ = [
|
||||
"PACKAGE_METADATA_FILENAME",
|
||||
"RUNTIME_MODE_ENV_VAR",
|
||||
"bundled_default_config_path",
|
||||
"bundled_package_dir",
|
||||
"bundled_runtime_path",
|
||||
"resolve_bundled_launch_args",
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
{"name":"deepseek-harness-runtime-bin","version":"0.0.0-dev"}
|
||||
@@ -0,0 +1,50 @@
|
||||
# Default runtime configuration for the bundled dsh-jsonrpc-agent. The runtime
|
||||
# binary has NO built-in fallback — it always requires an explicit config via
|
||||
# `$DSH_CORDIS_CONFIG` (wins) or an argv positional path. The Python client SDK
|
||||
# injects THIS file's path via `$DSH_CORDIS_CONFIG` when the caller supplies
|
||||
# no config and the launch resolves to the bundled runtime; that explicit
|
||||
# injection is what restores the zero-config experience. The runtime bin only
|
||||
# boots this config; the serving surface (the stdio JSON-RPC server) comes
|
||||
# from the @deepseek-ai/dsh-jsonrpc entry below.
|
||||
#
|
||||
# $DSH_SESSION_ROOT and $DSH_CWD are set by the SDK per launch; the `!!js`
|
||||
# fallbacks keep this file usable when the runtime is driven manually.
|
||||
|
||||
# The serving surface: HarnessSdkServer + line-delimited JSON-RPC transport on
|
||||
# stdio. Without this entry the runtime boots an agent nobody can talk to.
|
||||
- id: jsonrpc
|
||||
name: '@deepseek-ai/dsh-jsonrpc'
|
||||
|
||||
# The agent spine bundle: session store, system prompt, tool registry, agent
|
||||
# registry, and the agent loop. No pre-created agents — the SDK server creates
|
||||
# one per session/prompt sessionId.
|
||||
- id: agent-core
|
||||
name: '@deepseek-ai/dsh-agent-core'
|
||||
|
||||
# The DeepSeek adapter, preloaded for the stock models. The adapter fails loud
|
||||
# at load without an API key, so keyless boots must still export a dummy
|
||||
# DEEPSEEK_API_KEY (initialize/shutdown never call the model). baseURL falls
|
||||
# back to the public endpoint when unset.
|
||||
- id: llm-deepseek
|
||||
name: '@deepseek-ai/dsh-llm-deepseek'
|
||||
config:
|
||||
apiKey: !!js process.env.DEEPSEEK_API_KEY
|
||||
baseURL: !!js process.env.DEEPSEEK_BASE_URL
|
||||
models:
|
||||
- deepseek-v4-flash
|
||||
- deepseek-v4-pro
|
||||
|
||||
# JSONL session persistence. $DSH_SESSION_ROOT (set by the SDK whenever
|
||||
# `session_root` is configured) wins; otherwise ./.sessions relative to the
|
||||
# runtime process cwd.
|
||||
- id: sessions
|
||||
name: '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
config:
|
||||
root: !!js process.env.DSH_SESSION_ROOT ?? './.sessions'
|
||||
|
||||
# Local bash executor behind the spine's `bash` tool. $DSH_CWD (always set by
|
||||
# the SDK) wins; otherwise the runtime process cwd.
|
||||
- id: bash
|
||||
name: '@deepseek-ai/dsh-bash-local'
|
||||
config:
|
||||
cwd: !!js process.env.DSH_CWD ?? process.cwd()
|
||||
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
README.md: 67dbd04520ac01318c1f21393c12b7bf4965b4cb
|
||||
README.zh.md: 36ea4df4ff73e12ce3fda5bbee4108584dd5ad92
|
||||
@@ -0,0 +1,27 @@
|
||||
# DeepSeek Harness Python SDK
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
Python subprocess SDK for driving DeepSeek Harness over JSON-RPC stdio. The
|
||||
runtime inherits normal DeepSeek Harness environment variables such as
|
||||
`DEEPSEEK_BASE_URL` and `DEEPSEEK_API_KEY`, so callers can use real model
|
||||
endpoints directly or point those variables at a local proxy during
|
||||
benchmark runs.
|
||||
|
||||
By default, the SDK launches the bundled single-file `dsh-jsonrpc-agent` executable from the `deepseek-harness-runtime-bin` package and injects that package's default configuration (the stdio JSON-RPC server, agent core, preloaded DeepSeek adapter, JSONL session persistence, local bash) via `DSH_CORDIS_CONFIG`. To run a plugin composition of your own, keep the `@deepseek-ai/dsh-jsonrpc` entry in the config and pass the Cordis config path.
|
||||
|
||||
```py
|
||||
from deepseek_harness import DeepSeekHarness
|
||||
|
||||
with DeepSeekHarness(
|
||||
model="deepseek-v4-flash",
|
||||
cordis="examples/dsbench-coding-agent/cordis.yml",
|
||||
) as harness:
|
||||
result = harness.run("Make the requested code change.")
|
||||
```
|
||||
|
||||
`TurnResult.final_response` is the text content from the last
|
||||
`assistant/message` event in the turn. Use `TurnResult.events` for the complete
|
||||
event stream, including intermediate assistant messages and tool activity.
|
||||
|
||||
The same behavior can be selected for the runtime subprocess with `DSH_CORDIS_CONFIG`. If neither `cordis` nor `DSH_CORDIS_CONFIG` is set and the launch resolves to the bundled runtime, the bundled default configuration is used; an explicit `runtime_bin` or `launch_args_override` disables the injection entirely. See the [sdk-runtime README](../sdk-runtime/README.md) for the runtime carriers (production exe vs dev-only node closure) and how to obtain them.
|
||||
@@ -0,0 +1,21 @@
|
||||
# DeepSeek Harness Python SDK
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
通过 JSON-RPC stdio 驱动 DeepSeek Harness 的 Python 子进程 SDK。运行时继承常规的 DeepSeek Harness 环境变量(如 `DEEPSEEK_BASE_URL` 与 `DEEPSEEK_API_KEY`),调用方可以直接用真实模型端点,也可以在跑基准测试时把它们指向本地代理。
|
||||
|
||||
默认情况下,SDK 启动 `deepseek-harness-runtime-bin` 包内置的单文件 `dsh-jsonrpc-agent` 可执行程序,并通过 `DSH_CORDIS_CONFIG` 注入该包的默认配置(stdio JSON-RPC 服务器、agent core、预载的 DeepSeek 适配器、JSONL 会话持久化、本地 bash)。要运行自己用插件组合需要在配置里保留 `@deepseek-ai/dsh-jsonrpc` 条目,并传入 Cordis 配置路径。
|
||||
|
||||
```py
|
||||
from deepseek_harness import DeepSeekHarness
|
||||
|
||||
with DeepSeekHarness(
|
||||
model="deepseek-v4-flash",
|
||||
cordis="examples/dsbench-coding-agent/cordis.yml",
|
||||
) as harness:
|
||||
result = harness.run("Make the requested code change.")
|
||||
```
|
||||
|
||||
`TurnResult.final_response` 是本轮次最后一个 `assistant/message` 事件的文本内容。完整的事件流(包括中间的助手消息与工具活动)用 `TurnResult.events` 获取。
|
||||
|
||||
同样的行为也可以用 `DSH_CORDIS_CONFIG` 为运行时子进程选定。若 `cordis` 与 `DSH_CORDIS_CONFIG` 均未设置,且启动解析到内置运行时,则使用内置的默认配置;显式给出 `runtime_bin` 或 `launch_args_override` 则完全禁用注入。运行时载体(生产用 exe 与仅限开发的 node 闭包)及其获取方式见 [sdk-runtime README](../sdk-runtime/README.md)。
|
||||
@@ -0,0 +1,31 @@
|
||||
[build-system]
|
||||
requires = ["hatchling>=1.24.0"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "deepseek-harness"
|
||||
version = "0.0.0-dev"
|
||||
description = "Python SDK for DeepSeek Harness"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
license = { text = "BSD-3-Clause" }
|
||||
dependencies = [
|
||||
"pydantic>=2.12",
|
||||
"deepseek-harness-runtime-bin==0.0.0-dev",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
test = ["pytest>=8.0"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
addopts = "-q"
|
||||
testpaths = ["tests"]
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/deepseek_harness"]
|
||||
|
||||
# Editable: the runtime package's executables are injected into its source
|
||||
# tree AFTER install (by scripts/build-exe-for-python-sdk.ts or a manual copy); an
|
||||
# editable install sees them immediately instead of freezing a wheel snapshot.
|
||||
[tool.uv.sources]
|
||||
deepseek-harness-runtime-bin = { path = "../sdk-runtime", editable = true }
|
||||
@@ -0,0 +1,17 @@
|
||||
from .api import DeepSeekHarness, DeepSeekHarnessConfig, Session, TurnResult
|
||||
from .client import HarnessClient, HarnessConfig
|
||||
from .models import IncomingRequest, InitializeResponse, JsonObject, Notification, ServerInfo
|
||||
|
||||
__all__ = [
|
||||
"DeepSeekHarness",
|
||||
"DeepSeekHarnessConfig",
|
||||
"Session",
|
||||
"TurnResult",
|
||||
"HarnessClient",
|
||||
"HarnessConfig",
|
||||
"IncomingRequest",
|
||||
"InitializeResponse",
|
||||
"JsonObject",
|
||||
"Notification",
|
||||
"ServerInfo",
|
||||
]
|
||||
@@ -0,0 +1,223 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
|
||||
from .client import HarnessClient, HarnessConfig
|
||||
from .models import JsonObject, Notification
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class DeepSeekHarnessConfig:
|
||||
"""Configuration for launching the local DeepSeek Harness SDK runtime.
|
||||
|
||||
The runtime inherits the caller's environment by default, so existing
|
||||
DEEPSEEK_API_KEY and DEEPSEEK_BASE_URL settings keep working. Use ``env`` to
|
||||
intentionally override or inject variables for a subprocess.
|
||||
"""
|
||||
|
||||
model: str = "deepseek-v4-flash"
|
||||
cwd: str | None = None
|
||||
runtime_cwd: str | None = None
|
||||
session_root: str | None = None
|
||||
cordis: str | None = None
|
||||
system_prompt: str | None = None
|
||||
env: dict[str, str] = field(default_factory=dict)
|
||||
runtime_bin: str | None = None
|
||||
launch_args_override: tuple[str, ...] | None = None
|
||||
request_timeout_seconds: float | None = None
|
||||
shutdown_timeout_seconds: float | None = 1.0
|
||||
client_name: str = "deepseek_harness_python_sdk"
|
||||
client_version: str = "0.0.0-dev"
|
||||
base_url: str | None = None
|
||||
api_key: str | None = None
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class TurnResult:
|
||||
session_id: str
|
||||
status: str
|
||||
final_response: str
|
||||
events: list[JsonObject]
|
||||
notifications: list[Notification]
|
||||
session_root: str | None = None
|
||||
|
||||
|
||||
class DeepSeekHarness:
|
||||
"""Synchronous high-level SDK for running DeepSeek Harness agent turns."""
|
||||
|
||||
def __init__(self, config: DeepSeekHarnessConfig | None = None, **kwargs: object) -> None:
|
||||
if config is not None and kwargs:
|
||||
raise TypeError("pass either DeepSeekHarnessConfig or keyword options, not both")
|
||||
self.config = config or DeepSeekHarnessConfig(**kwargs)
|
||||
cwd = self.config.cwd or str(Path.cwd())
|
||||
runtime_cwd = self.config.runtime_cwd or cwd
|
||||
env = dict(self.config.env)
|
||||
if self.config.session_root is not None:
|
||||
env["DSH_SESSION_ROOT"] = self.config.session_root
|
||||
if self.config.cordis is not None:
|
||||
env["DSH_CORDIS_CONFIG"] = self.config.cordis
|
||||
else:
|
||||
self._inject_bundled_default_config(env)
|
||||
env["DSH_CWD"] = cwd
|
||||
if self.config.base_url is not None:
|
||||
env["DEEPSEEK_BASE_URL"] = self.config.base_url
|
||||
if self.config.api_key is not None:
|
||||
env["DEEPSEEK_API_KEY"] = self.config.api_key
|
||||
|
||||
self._client = HarnessClient(
|
||||
HarnessConfig(
|
||||
runtime_bin=self.config.runtime_bin,
|
||||
launch_args_override=self.config.launch_args_override,
|
||||
cwd=runtime_cwd,
|
||||
env=env,
|
||||
request_timeout_seconds=self.config.request_timeout_seconds,
|
||||
shutdown_timeout_seconds=self.config.shutdown_timeout_seconds,
|
||||
client_name=self.config.client_name,
|
||||
client_version=self.config.client_version,
|
||||
)
|
||||
)
|
||||
self._initialized = False
|
||||
|
||||
def __enter__(self) -> "DeepSeekHarness":
|
||||
self.start()
|
||||
return self
|
||||
|
||||
def __exit__(self, _exc_type, _exc, _tb) -> None:
|
||||
self.close()
|
||||
|
||||
@property
|
||||
def client(self) -> HarnessClient:
|
||||
return self._client
|
||||
|
||||
def start(self) -> None:
|
||||
if self._initialized:
|
||||
return
|
||||
self._client.start()
|
||||
self._client.initialize(
|
||||
cwd=self.config.cwd or str(Path.cwd()),
|
||||
model=self.config.model,
|
||||
session_root=self.config.session_root,
|
||||
system_prompt=self.config.system_prompt,
|
||||
)
|
||||
self._initialized = True
|
||||
|
||||
def close(self) -> None:
|
||||
self._client.close()
|
||||
self._initialized = False
|
||||
|
||||
def _inject_bundled_default_config(self, env: dict[str, str]) -> None:
|
||||
"""Restore the zero-config experience over the config-mandatory bundled runtime.
|
||||
|
||||
The bundled runtime (single-file exe or the dev-only node closure)
|
||||
always demands an explicit config. When the caller neither provided
|
||||
``cordis`` nor selected a runtime explicitly (``runtime_bin`` /
|
||||
``launch_args_override``), and no ambient ``DSH_CORDIS_CONFIG`` exists,
|
||||
inject the runtime package's checked-in default cordis.yml. With an
|
||||
explicit runtime or config channel the SDK stays out of the way.
|
||||
"""
|
||||
uses_bundled_runtime = self.config.runtime_bin is None and self.config.launch_args_override is None
|
||||
if not uses_bundled_runtime or "DSH_CORDIS_CONFIG" in env or "DSH_CORDIS_CONFIG" in os.environ:
|
||||
return
|
||||
try:
|
||||
from deepseek_harness_runtime import bundled_default_config_path
|
||||
except ImportError:
|
||||
# Only the runtime package's absence reaches here; swallow it so
|
||||
# HarnessClient.start() reports the actionable install error.
|
||||
return
|
||||
env["DSH_CORDIS_CONFIG"] = str(bundled_default_config_path())
|
||||
|
||||
def start_session(self, session_id: str | None = None) -> "Session":
|
||||
self.start()
|
||||
return Session(self, session_id or f"session-{uuid.uuid4().hex}")
|
||||
|
||||
def run(
|
||||
self,
|
||||
input: str | list[JsonObject],
|
||||
*,
|
||||
session_id: str | None = None,
|
||||
profile: str | None = None,
|
||||
on_notification: Callable[[Notification], None] | None = None,
|
||||
) -> TurnResult:
|
||||
return self.start_session(session_id).run(input, profile=profile, on_notification=on_notification)
|
||||
|
||||
|
||||
class Session:
|
||||
def __init__(self, harness: DeepSeekHarness, session_id: str) -> None:
|
||||
self.harness = harness
|
||||
self.id = session_id
|
||||
|
||||
def run(
|
||||
self,
|
||||
input: str | list[JsonObject],
|
||||
*,
|
||||
profile: str | None = None,
|
||||
on_notification: Callable[[Notification], None] | None = None,
|
||||
) -> TurnResult:
|
||||
content_blocks = normalize_input(input)
|
||||
notifications: list[Notification] = []
|
||||
events: list[JsonObject] = []
|
||||
status = "error"
|
||||
finished = False
|
||||
|
||||
def collect(notification: Notification) -> None:
|
||||
nonlocal finished, status
|
||||
notifications.append(notification)
|
||||
if on_notification is not None:
|
||||
on_notification(notification)
|
||||
if notification.method == "session.event":
|
||||
event = notification.payload.get("event")
|
||||
if isinstance(event, dict):
|
||||
events.append(event)
|
||||
if notification.method == "session.finished" and notification.payload.get("sessionId") == self.id:
|
||||
status = str(notification.payload.get("status") or "ok")
|
||||
finished = True
|
||||
|
||||
with self.harness.client.subscribe_session_notifications(self.id) as subscription:
|
||||
self.harness.client.session_prompt(
|
||||
self.id,
|
||||
content_blocks,
|
||||
profile=profile,
|
||||
on_notification=collect,
|
||||
notification_subscription=subscription,
|
||||
)
|
||||
|
||||
while not finished:
|
||||
notification = subscription.next()
|
||||
collect(notification)
|
||||
|
||||
return TurnResult(
|
||||
session_id=self.id,
|
||||
status=status,
|
||||
final_response=final_response(events),
|
||||
events=events,
|
||||
notifications=notifications,
|
||||
session_root=self.harness.config.session_root,
|
||||
)
|
||||
|
||||
|
||||
def normalize_input(input: str | list[JsonObject]) -> list[JsonObject]:
|
||||
if isinstance(input, str):
|
||||
return [{"type": "text", "text": input}]
|
||||
return input
|
||||
|
||||
|
||||
def final_response(events: list[JsonObject]) -> str:
|
||||
for event in reversed(events):
|
||||
if event.get("type") != "assistant/message":
|
||||
continue
|
||||
data = event.get("data")
|
||||
if not isinstance(data, dict):
|
||||
continue
|
||||
content = data.get("content")
|
||||
if not isinstance(content, list):
|
||||
continue
|
||||
parts: list[str] = []
|
||||
for block in content:
|
||||
if isinstance(block, dict) and block.get("type") == "text":
|
||||
parts.append(str(block.get("text") or ""))
|
||||
return "".join(parts)
|
||||
return ""
|
||||
@@ -0,0 +1,481 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import queue
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
from collections import deque
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable, Literal, TypeAlias, TypeVar
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .errors import JsonRpcError, TransportClosedError
|
||||
from .models import IncomingRequest, InitializeResponse, JsonObject, JsonValue, Notification
|
||||
|
||||
ModelT = TypeVar("ModelT", bound=BaseModel)
|
||||
NotificationFilter: TypeAlias = Callable[[Notification], bool]
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class HarnessConfig:
|
||||
"""Configuration for launching the local DeepSeek Harness SDK runtime."""
|
||||
|
||||
runtime_bin: str | None = None
|
||||
bridge_bin: str | None = None
|
||||
launch_args_override: tuple[str, ...] | None = None
|
||||
cwd: str | None = None
|
||||
env: dict[str, str] | None = None
|
||||
request_timeout_seconds: float | None = None
|
||||
shutdown_timeout_seconds: float | None = 1.0
|
||||
client_name: str = "deepseek_harness_python_sdk"
|
||||
client_version: str = "0.0.0-dev"
|
||||
|
||||
|
||||
class HarnessClient:
|
||||
"""Synchronous JSON-RPC client for the DeepSeek Harness SDK runtime over stdio."""
|
||||
|
||||
def __init__(self, config: HarnessConfig | None = None) -> None:
|
||||
self.config = config or HarnessConfig()
|
||||
self._proc: subprocess.Popen[str] | None = None
|
||||
self._lock = threading.Lock()
|
||||
self._write_lock = threading.Lock()
|
||||
self._responses: dict[str, queue.Queue[JsonValue | BaseException]] = {}
|
||||
self._notifications: queue.Queue[Notification | BaseException] = queue.Queue()
|
||||
self._notification_subscribers: dict[
|
||||
str, tuple[queue.Queue[Notification | BaseException], NotificationFilter | None]
|
||||
] = {}
|
||||
self._requests: queue.Queue[IncomingRequest | BaseException] = queue.Queue()
|
||||
self._stderr_lines: deque[str] = deque(maxlen=400)
|
||||
self._reader_thread: threading.Thread | None = None
|
||||
self._stderr_thread: threading.Thread | None = None
|
||||
|
||||
def __enter__(self) -> "HarnessClient":
|
||||
self.start()
|
||||
return self
|
||||
|
||||
def __exit__(self, _exc_type, _exc, _tb) -> None:
|
||||
self.close()
|
||||
|
||||
def start(self) -> None:
|
||||
if self._proc is not None:
|
||||
return
|
||||
args = list(self.config.launch_args_override or self._default_launch_args())
|
||||
env = os.environ.copy()
|
||||
if self.config.env:
|
||||
env.update(self.config.env)
|
||||
self._proc = subprocess.Popen(
|
||||
args,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
cwd=self.config.cwd,
|
||||
env=env,
|
||||
bufsize=1,
|
||||
)
|
||||
self._start_reader_thread()
|
||||
self._start_stderr_thread()
|
||||
|
||||
def close(self) -> None:
|
||||
proc = self._proc
|
||||
if proc is None:
|
||||
return
|
||||
try:
|
||||
self.request("shutdown", None, response_model=_ShutdownResponse, timeout_seconds=self.config.shutdown_timeout_seconds)
|
||||
except Exception as exc:
|
||||
self._stderr_lines.append(f"shutdown request failed: {exc}")
|
||||
self._proc = None
|
||||
if proc.stdin:
|
||||
try:
|
||||
proc.stdin.close()
|
||||
except Exception as exc:
|
||||
self._stderr_lines.append(f"stdin close failed: {exc}")
|
||||
try:
|
||||
if proc.poll() is None:
|
||||
proc.terminate()
|
||||
proc.wait(timeout=2)
|
||||
except Exception:
|
||||
proc.kill()
|
||||
self._fail_waiters(self._runtime_closed_error("DeepSeek Harness runtime closed"))
|
||||
if self._reader_thread and self._reader_thread.is_alive():
|
||||
self._reader_thread.join(timeout=0.5)
|
||||
if self._stderr_thread and self._stderr_thread.is_alive():
|
||||
self._stderr_thread.join(timeout=0.5)
|
||||
|
||||
def initialize(
|
||||
self,
|
||||
*,
|
||||
cwd: str,
|
||||
model: str,
|
||||
session_root: str | None = None,
|
||||
system_prompt: str | None = None,
|
||||
) -> InitializeResponse:
|
||||
payload: JsonObject = {
|
||||
"clientInfo": {
|
||||
"name": self.config.client_name,
|
||||
"version": self.config.client_version,
|
||||
},
|
||||
"cwd": cwd,
|
||||
"model": model,
|
||||
}
|
||||
if session_root is not None:
|
||||
payload["sessionRoot"] = session_root
|
||||
if system_prompt is not None:
|
||||
payload["systemPrompt"] = system_prompt
|
||||
return self.request("initialize", payload, response_model=InitializeResponse)
|
||||
|
||||
def session_prompt(
|
||||
self,
|
||||
session_id: str,
|
||||
content_blocks: list[JsonObject],
|
||||
*,
|
||||
profile: str | None = None,
|
||||
on_notification: Callable[[Notification], None] | None = None,
|
||||
notification_subscription: "NotificationSubscription | None" = None,
|
||||
) -> None:
|
||||
payload: JsonObject = {"sessionId": session_id, "contentBlocks": content_blocks}
|
||||
if profile is not None:
|
||||
payload["profile"] = profile
|
||||
self.request(
|
||||
"session/prompt",
|
||||
payload,
|
||||
response_model=_SessionPromptResponse,
|
||||
on_notification=on_notification,
|
||||
notification_filter=_notification_belongs_to_session(session_id),
|
||||
notification_subscription=notification_subscription,
|
||||
)
|
||||
|
||||
def request(
|
||||
self,
|
||||
method: str,
|
||||
params: JsonObject | None,
|
||||
*,
|
||||
response_model: type[ModelT],
|
||||
timeout_seconds: float | None = None,
|
||||
on_notification: Callable[[Notification], None] | None = None,
|
||||
notification_filter: NotificationFilter | None = None,
|
||||
notification_subscription: "NotificationSubscription | None" = None,
|
||||
) -> ModelT:
|
||||
result = self._request_raw(
|
||||
method,
|
||||
params,
|
||||
timeout_seconds=timeout_seconds,
|
||||
on_notification=on_notification,
|
||||
notification_filter=notification_filter,
|
||||
notification_subscription=notification_subscription,
|
||||
)
|
||||
if not isinstance(result, dict):
|
||||
raise TypeError(f"{method} response must be a JSON object")
|
||||
return response_model.model_validate(result)
|
||||
|
||||
def notify(self, method: str, params: JsonObject | None = None) -> None:
|
||||
message: JsonObject = {"jsonrpc": "2.0", "method": method}
|
||||
if params is not None:
|
||||
message["params"] = params
|
||||
self._write_message(message)
|
||||
|
||||
def next_notification(self) -> Notification:
|
||||
item = self._notifications.get()
|
||||
if isinstance(item, BaseException):
|
||||
raise item
|
||||
return item
|
||||
|
||||
def subscribe_notifications(
|
||||
self,
|
||||
notification_filter: NotificationFilter | None = None,
|
||||
) -> "NotificationSubscription":
|
||||
subscription_id = str(uuid.uuid4())
|
||||
notifications: queue.Queue[Notification | BaseException] = queue.Queue()
|
||||
with self._lock:
|
||||
self._notification_subscribers[subscription_id] = (notifications, notification_filter)
|
||||
return NotificationSubscription(self, subscription_id, notifications)
|
||||
|
||||
def subscribe_session_notifications(self, session_id: str) -> "NotificationSubscription":
|
||||
return self.subscribe_notifications(_notification_belongs_to_session(session_id))
|
||||
|
||||
def next_request(self) -> IncomingRequest:
|
||||
item = self._requests.get()
|
||||
if isinstance(item, BaseException):
|
||||
raise item
|
||||
return item
|
||||
|
||||
def respond(self, request_id: str | int, result: JsonValue) -> None:
|
||||
self._write_message({"jsonrpc": "2.0", "id": request_id, "result": result})
|
||||
|
||||
def respond_error(
|
||||
self,
|
||||
request_id: str | int,
|
||||
*,
|
||||
code: int,
|
||||
message: str,
|
||||
data: JsonValue | None = None,
|
||||
) -> None:
|
||||
error: JsonObject = {"code": code, "message": message}
|
||||
if data is not None:
|
||||
error["data"] = data
|
||||
self._write_message({"jsonrpc": "2.0", "id": request_id, "error": error})
|
||||
|
||||
def _request_raw(
|
||||
self,
|
||||
method: str,
|
||||
params: JsonObject | None = None,
|
||||
*,
|
||||
timeout_seconds: float | None = None,
|
||||
on_notification: Callable[[Notification], None] | None = None,
|
||||
notification_filter: NotificationFilter | None = None,
|
||||
notification_subscription: "NotificationSubscription | None" = None,
|
||||
) -> JsonValue:
|
||||
request_id = str(uuid.uuid4())
|
||||
waiter: queue.Queue[JsonValue | BaseException] = queue.Queue(maxsize=1)
|
||||
temp_subscription: NotificationSubscription | None = None
|
||||
subscription = notification_subscription
|
||||
with self._lock:
|
||||
self._responses[request_id] = waiter
|
||||
if on_notification is not None and subscription is None:
|
||||
temp_subscription = self.subscribe_notifications(notification_filter)
|
||||
subscription = temp_subscription
|
||||
try:
|
||||
message: JsonObject = {"jsonrpc": "2.0", "id": request_id, "method": method}
|
||||
if params is not None:
|
||||
message["params"] = params
|
||||
self._write_message(message)
|
||||
except BaseException:
|
||||
with self._lock:
|
||||
self._responses.pop(request_id, None)
|
||||
if temp_subscription is not None:
|
||||
temp_subscription.close()
|
||||
raise
|
||||
timeout = self.config.request_timeout_seconds if timeout_seconds is None else timeout_seconds
|
||||
deadline = None if timeout is None else time.monotonic() + timeout
|
||||
try:
|
||||
while True:
|
||||
if on_notification is not None and subscription is not None:
|
||||
subscription.drain(on_notification)
|
||||
wait_timeout = None
|
||||
if on_notification is not None:
|
||||
wait_timeout = 0.05
|
||||
if deadline is not None:
|
||||
remaining = deadline - time.monotonic()
|
||||
if remaining <= 0:
|
||||
with self._lock:
|
||||
self._responses.pop(request_id, None)
|
||||
raise TimeoutError(f"{method} timed out waiting for DeepSeek Harness runtime")
|
||||
wait_timeout = remaining if wait_timeout is None else min(wait_timeout, remaining)
|
||||
try:
|
||||
item = waiter.get(timeout=wait_timeout)
|
||||
if on_notification is not None and subscription is not None:
|
||||
subscription.drain(on_notification)
|
||||
break
|
||||
except queue.Empty:
|
||||
continue
|
||||
except BaseException:
|
||||
with self._lock:
|
||||
self._responses.pop(request_id, None)
|
||||
if temp_subscription is not None:
|
||||
temp_subscription.close()
|
||||
raise
|
||||
finally:
|
||||
if temp_subscription is not None:
|
||||
temp_subscription.close()
|
||||
if isinstance(item, BaseException):
|
||||
raise item
|
||||
return item
|
||||
|
||||
def _write_message(self, message: JsonObject) -> None:
|
||||
proc = self._proc
|
||||
if proc is None or proc.stdin is None:
|
||||
raise TransportClosedError("DeepSeek Harness runtime is not running")
|
||||
try:
|
||||
payload = json.dumps(message, separators=(",", ":")) + "\n"
|
||||
with self._write_lock:
|
||||
proc.stdin.write(payload)
|
||||
proc.stdin.flush()
|
||||
except Exception as exc:
|
||||
raise self._runtime_closed_error("Failed to write to DeepSeek Harness runtime") from exc
|
||||
|
||||
def _start_reader_thread(self) -> None:
|
||||
self._reader_thread = threading.Thread(target=self._reader_loop, name="dsh-runtime-reader", daemon=True)
|
||||
self._reader_thread.start()
|
||||
|
||||
def _start_stderr_thread(self) -> None:
|
||||
self._stderr_thread = threading.Thread(target=self._stderr_loop, name="dsh-runtime-stderr", daemon=True)
|
||||
self._stderr_thread.start()
|
||||
|
||||
def _reader_loop(self) -> None:
|
||||
proc = self._proc
|
||||
if proc is None or proc.stdout is None:
|
||||
return
|
||||
try:
|
||||
for line in proc.stdout:
|
||||
if not line.strip():
|
||||
continue
|
||||
try:
|
||||
message = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
self._handle_message(message)
|
||||
except BaseException as exc:
|
||||
self._fail_waiters(exc)
|
||||
finally:
|
||||
self._fail_waiters(self._runtime_closed_error("DeepSeek Harness runtime stdout closed"))
|
||||
|
||||
def _stderr_loop(self) -> None:
|
||||
proc = self._proc
|
||||
if proc is None or proc.stderr is None:
|
||||
return
|
||||
for line in proc.stderr:
|
||||
self._stderr_lines.append(line.rstrip())
|
||||
|
||||
def _handle_message(self, message: object) -> None:
|
||||
if not isinstance(message, dict):
|
||||
return
|
||||
msg_id = message.get("id")
|
||||
method = message.get("method")
|
||||
if isinstance(msg_id, (str, int)) and isinstance(method, str):
|
||||
params = message.get("params")
|
||||
self._requests.put(IncomingRequest(id=msg_id, method=method, payload=params if isinstance(params, dict) else {}))
|
||||
return
|
||||
if isinstance(msg_id, (str, int)):
|
||||
with self._lock:
|
||||
waiter = self._responses.pop(str(msg_id), None)
|
||||
if waiter is None:
|
||||
return
|
||||
if isinstance(message.get("error"), dict):
|
||||
err = message["error"]
|
||||
waiter.put(JsonRpcError(_int_or_none(err.get("code")), str(err.get("message", "JSON-RPC error")), err.get("data")))
|
||||
else:
|
||||
waiter.put(message.get("result"))
|
||||
return
|
||||
if isinstance(method, str):
|
||||
params = message.get("params")
|
||||
notification = Notification(method=method, payload=params if isinstance(params, dict) else {})
|
||||
with self._lock:
|
||||
subscribers = list(self._notification_subscribers.values())
|
||||
delivered = False
|
||||
for subscriber, predicate in subscribers:
|
||||
if predicate is None or predicate(notification):
|
||||
subscriber.put(notification)
|
||||
delivered = True
|
||||
if not delivered:
|
||||
self._notifications.put(notification)
|
||||
|
||||
def _fail_waiters(self, exc: BaseException) -> None:
|
||||
with self._lock:
|
||||
waiters = list(self._responses.values())
|
||||
self._responses.clear()
|
||||
subscribers = list(self._notification_subscribers.values())
|
||||
self._notification_subscribers.clear()
|
||||
for waiter in waiters:
|
||||
waiter.put(exc)
|
||||
for subscriber, _predicate in subscribers:
|
||||
subscriber.put(exc)
|
||||
self._notifications.put(exc)
|
||||
self._requests.put(exc)
|
||||
|
||||
def _runtime_closed_error(self, reason: str) -> TransportClosedError:
|
||||
proc = self._proc
|
||||
if (
|
||||
proc is not None
|
||||
and proc.poll() is not None
|
||||
and self._stderr_thread is not None
|
||||
and self._stderr_thread.is_alive()
|
||||
and threading.current_thread() is not self._stderr_thread
|
||||
):
|
||||
self._stderr_thread.join(timeout=0.1)
|
||||
|
||||
parts = [reason]
|
||||
if proc is not None:
|
||||
exit_code = proc.poll()
|
||||
if exit_code is not None:
|
||||
parts.append(f"exit code: {exit_code}")
|
||||
if self._stderr_lines:
|
||||
parts.append("stderr tail:\n" + "\n".join(self._stderr_lines))
|
||||
return TransportClosedError("\n".join(parts))
|
||||
|
||||
def _default_launch_args(self) -> tuple[str, ...]:
|
||||
if self.config.runtime_bin is not None:
|
||||
return (self.config.runtime_bin,)
|
||||
if self.config.bridge_bin is not None:
|
||||
return (self.config.bridge_bin,)
|
||||
try:
|
||||
from deepseek_harness_runtime import resolve_bundled_launch_args
|
||||
except ImportError as exc:
|
||||
raise FileNotFoundError(
|
||||
"Unable to locate the bundled DeepSeek Harness SDK runtime. "
|
||||
"Install deepseek-harness-runtime-bin or set HarnessConfig.runtime_bin."
|
||||
) from exc
|
||||
return resolve_bundled_launch_args()
|
||||
|
||||
def _unsubscribe_notifications(self, subscription_id: str) -> None:
|
||||
with self._lock:
|
||||
self._notification_subscribers.pop(subscription_id, None)
|
||||
|
||||
|
||||
class NotificationSubscription:
|
||||
def __init__(
|
||||
self,
|
||||
client: HarnessClient,
|
||||
subscription_id: str,
|
||||
notifications: queue.Queue[Notification | BaseException],
|
||||
) -> None:
|
||||
self._client = client
|
||||
self._subscription_id = subscription_id
|
||||
self._notifications = notifications
|
||||
self._closed = False
|
||||
|
||||
def __enter__(self) -> "NotificationSubscription":
|
||||
return self
|
||||
|
||||
def __exit__(self, _exc_type, _exc, _tb) -> None:
|
||||
self.close()
|
||||
|
||||
def close(self) -> None:
|
||||
if self._closed:
|
||||
return
|
||||
self._closed = True
|
||||
self._client._unsubscribe_notifications(self._subscription_id)
|
||||
|
||||
def next(self) -> Notification:
|
||||
item = self._notifications.get()
|
||||
if isinstance(item, BaseException):
|
||||
raise item
|
||||
return item
|
||||
|
||||
def drain(self, on_notification: Callable[[Notification], None]) -> None:
|
||||
while True:
|
||||
try:
|
||||
item = self._notifications.get_nowait()
|
||||
except queue.Empty:
|
||||
return
|
||||
if isinstance(item, BaseException):
|
||||
raise item
|
||||
on_notification(item)
|
||||
|
||||
|
||||
class _SessionPromptResponse(BaseModel):
|
||||
accepted: Literal[True]
|
||||
|
||||
|
||||
class _ShutdownResponse(BaseModel):
|
||||
pass
|
||||
|
||||
|
||||
def _int_or_none(value: object) -> int | None:
|
||||
return value if isinstance(value, int) else None
|
||||
|
||||
|
||||
def _notification_belongs_to_session(session_id: str) -> NotificationFilter:
|
||||
def belongs(notification: Notification) -> bool:
|
||||
payload = notification.payload
|
||||
return (
|
||||
payload.get("sessionId") == session_id
|
||||
or payload.get("parentSessionId") == session_id
|
||||
or payload.get("childSessionId") == session_id
|
||||
)
|
||||
|
||||
return belongs
|
||||
@@ -0,0 +1,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
class HarnessError(Exception):
|
||||
"""Base exception for SDK and runtime failures."""
|
||||
|
||||
|
||||
class TransportClosedError(HarnessError):
|
||||
"""Raised when the runtime subprocess exits or closes stdout."""
|
||||
|
||||
|
||||
class JsonRpcError(HarnessError):
|
||||
"""Raised when the runtime returns a JSON-RPC error response."""
|
||||
|
||||
def __init__(self, code: int | None, message: str, data: object | None = None) -> None:
|
||||
super().__init__(message)
|
||||
self.code = code
|
||||
self.message = message
|
||||
self.data = data
|
||||
@@ -0,0 +1,32 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import TypeAlias
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
JsonScalar: TypeAlias = str | int | float | bool | None
|
||||
JsonValue: TypeAlias = JsonScalar | dict[str, "JsonValue"] | list["JsonValue"]
|
||||
JsonObject: TypeAlias = dict[str, JsonValue]
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class Notification:
|
||||
method: str
|
||||
payload: JsonObject
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class IncomingRequest:
|
||||
id: str | int
|
||||
method: str
|
||||
payload: JsonObject
|
||||
|
||||
|
||||
class ServerInfo(BaseModel):
|
||||
name: str | None = None
|
||||
version: str | None = None
|
||||
|
||||
|
||||
class InitializeResponse(BaseModel):
|
||||
serverInfo: ServerInfo | None = None
|
||||
@@ -0,0 +1,127 @@
|
||||
"""Manual keyless smoke: drive the repo-source jsonrpc-agent bin (node + tsx).
|
||||
|
||||
Runs the SDK against `packages/ui/jsonrpc-agent/src/bin.ts` executed from the
|
||||
repo checkout (requires `pnpm install`; no build, no API key — the model
|
||||
endpoint is a local mock SSE server). The bin only boots the supplied
|
||||
cordis.yml — the stdio JSON-RPC server itself comes from the config's
|
||||
`@deepseek-ai/dsh-jsonrpc` entry — so the runtime package's default cordis.yml
|
||||
is passed explicitly. Not collected by pytest; run it directly:
|
||||
`python tests/manual_sdk_agent_smoke.py`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import shutil
|
||||
import tempfile
|
||||
import threading
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from deepseek_harness import DeepSeekHarness
|
||||
from deepseek_harness_runtime import bundled_default_config_path
|
||||
|
||||
|
||||
class MockCompletionHandler(BaseHTTPRequestHandler):
|
||||
requests: list[dict[str, Any]] = []
|
||||
|
||||
def do_POST(self) -> None:
|
||||
length = int(self.headers.get("content-length", "0"))
|
||||
body = self.rfile.read(length).decode("utf-8")
|
||||
self.requests.append({
|
||||
"path": self.path,
|
||||
"authorization": self.headers.get("authorization"),
|
||||
"body": json.loads(body),
|
||||
})
|
||||
self.send_response(200)
|
||||
self.send_header("content-type", "text/event-stream")
|
||||
self.end_headers()
|
||||
self.wfile.write(b'data: {"choices":[{"delta":{"role":"assistant","content":null,"reasoning_content":""}}]}\n\n')
|
||||
self.wfile.write(b'data: {"choices":[{"delta":{"content":"SDK runtime reached the configured HTTP model endpoint."}}]}\n\n')
|
||||
self.wfile.write(b'data: {"choices":[{"delta":{"content":""},"finish_reason":"stop"}],"usage":{"prompt_tokens":7,"completion_tokens":9}}\n\n')
|
||||
self.wfile.write(b"data: [DONE]\n\n")
|
||||
|
||||
def log_message(self, _format: str, *_args: object) -> None:
|
||||
return
|
||||
|
||||
|
||||
def run_smoke(repo_root: Path, keep_sessions: bool) -> None:
|
||||
session_root = Path(tempfile.mkdtemp(prefix="dsh-sdk-smoke-sessions-"))
|
||||
runtime_entry = repo_root / "packages/ui/jsonrpc-agent/src/bin.ts"
|
||||
server = ThreadingHTTPServer(("127.0.0.1", 0), MockCompletionHandler)
|
||||
thread = threading.Thread(target=server.serve_forever, name="mock-openai-compatible-server", daemon=True)
|
||||
thread.start()
|
||||
base_url = f"http://127.0.0.1:{server.server_address[1]}"
|
||||
|
||||
print(f"repo_root={repo_root}")
|
||||
print(f"session_root={session_root}")
|
||||
print(f"mock_base_url={base_url}")
|
||||
|
||||
try:
|
||||
with DeepSeekHarness(
|
||||
model="sdk-smoke-model",
|
||||
cwd=str(repo_root / "python/sdk"),
|
||||
runtime_cwd=str(repo_root),
|
||||
session_root=str(session_root),
|
||||
cordis=str(bundled_default_config_path()),
|
||||
system_prompt="You are running a Python SDK smoke test.",
|
||||
launch_args_override=("node", "--import", "tsx", str(runtime_entry)),
|
||||
env={
|
||||
"DEEPSEEK_BASE_URL": base_url,
|
||||
"DEEPSEEK_API_KEY": "sdk-smoke-key",
|
||||
},
|
||||
request_timeout_seconds=20,
|
||||
shutdown_timeout_seconds=2,
|
||||
) as harness:
|
||||
result = harness.run(
|
||||
"Please reply with a short confirmation and do not call tools.",
|
||||
session_id="sdk-smoke-main",
|
||||
profile="build",
|
||||
)
|
||||
print(f"turn_status={result.status}")
|
||||
print(f"final_response={result.final_response}")
|
||||
assert result.status == "ok", result
|
||||
assert "configured HTTP model endpoint" in result.final_response
|
||||
assert len(MockCompletionHandler.requests) == 1
|
||||
request = MockCompletionHandler.requests[0]
|
||||
print(json.dumps(request, ensure_ascii=False, indent=2)[:4000])
|
||||
assert request["authorization"] == "Bearer sdk-smoke-key"
|
||||
assert request["body"]["model"] == "sdk-smoke-model"
|
||||
|
||||
jsonl_files = sorted(session_root.rglob("*.jsonl"))
|
||||
assert jsonl_files, f"no jsonl sessions were written under {session_root}"
|
||||
print("session_jsonl_files:")
|
||||
for path in jsonl_files:
|
||||
print(f" {path} bytes={path.stat().st_size}")
|
||||
with path.open("r", encoding="utf-8") as handle:
|
||||
first_line = handle.readline().strip()
|
||||
if first_line:
|
||||
print(f" first_line={first_line[:500]}")
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
|
||||
if keep_sessions:
|
||||
print(f"kept_session_root={session_root}")
|
||||
else:
|
||||
shutil.rmtree(session_root)
|
||||
print("removed temporary session root")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--repo-root",
|
||||
type=Path,
|
||||
default=Path(__file__).resolve().parents[3],
|
||||
help="Path to the deepseek-harness checkout.",
|
||||
)
|
||||
parser.add_argument("--keep-sessions", action="store_true")
|
||||
args = parser.parse_args()
|
||||
run_smoke(args.repo_root.resolve(), args.keep_sessions)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,120 @@
|
||||
"""Smoke tests against the bundled dsh-jsonrpc-agent artifacts.
|
||||
|
||||
These boot the runtime the way an installed SDK does, once per bundled
|
||||
carrier: the platform single-file exe (production) and the dev-only node
|
||||
closure under ``runtime/node`` driven by system ``node``. Each carrier skips
|
||||
independently when its artifact is absent on this machine — build or fetch it
|
||||
per the FileNotFoundError guidance quoted in the skip reason. Keyless: the
|
||||
dummy DEEPSEEK_API_KEY only satisfies the adapter's load-time check;
|
||||
initialize/shutdown never call a model.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from deepseek_harness import DeepSeekHarness, HarnessClient, HarnessConfig
|
||||
from deepseek_harness.errors import TransportClosedError
|
||||
from deepseek_harness_runtime import bundled_default_config_path, resolve_bundled_launch_args
|
||||
|
||||
_MODES = ("exe", "node")
|
||||
|
||||
# The serving surface is itself a plugin: without the dsh-jsonrpc entry the
|
||||
# runtime boots an agent nobody can talk to and exits 0 on stdin EOF.
|
||||
_CORDIS_YML = """\
|
||||
- id: jsonrpc
|
||||
name: '@deepseek-ai/dsh-jsonrpc'
|
||||
- id: agent-core
|
||||
name: '@deepseek-ai/dsh-agent-core'
|
||||
- id: sessions
|
||||
name: '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
config:
|
||||
root: './sessions'
|
||||
- id: bash
|
||||
name: '@deepseek-ai/dsh-bash-local'
|
||||
config:
|
||||
cwd: '.'
|
||||
- id: todo
|
||||
name: '@deepseek-ai/dsh-tool-todo'
|
||||
"""
|
||||
|
||||
|
||||
def _launch_args(mode: str) -> tuple[str, ...]:
|
||||
try:
|
||||
return resolve_bundled_launch_args(mode)
|
||||
except FileNotFoundError as exc:
|
||||
pytest.skip(f"bundled {mode}-mode runtime unavailable on this machine: {exc}")
|
||||
|
||||
|
||||
def _client(tmp_path: Path, launch_args: tuple[str, ...]) -> HarnessClient:
|
||||
return HarnessClient(
|
||||
HarnessConfig(
|
||||
launch_args_override=launch_args,
|
||||
cwd=str(tmp_path),
|
||||
env={
|
||||
"DSH_CORDIS_CONFIG": "./cordis.yml",
|
||||
"DSH_SESSION_ROOT": str(tmp_path / "sessions"),
|
||||
"DSH_CWD": str(tmp_path),
|
||||
# initialize() lazily mounts the llm-deepseek adapter for the
|
||||
# requested model; a dummy key keeps the keyless boot green
|
||||
# (initialize/shutdown never call the model).
|
||||
"DEEPSEEK_API_KEY": "sk-dummy-for-boot",
|
||||
"DEEPSEEK_BASE_URL": "http://127.0.0.1:9",
|
||||
},
|
||||
request_timeout_seconds=120,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", _MODES)
|
||||
def test_bundled_runtime_boots_a_cordis_config(tmp_path: Path, mode: str) -> None:
|
||||
launch_args = _launch_args(mode)
|
||||
(tmp_path / "cordis.yml").write_text(_CORDIS_YML)
|
||||
|
||||
with _client(tmp_path, launch_args) as client:
|
||||
init = client.initialize(cwd=str(tmp_path), model="deepseek-v4-pro")
|
||||
|
||||
assert init.serverInfo is not None
|
||||
assert init.serverInfo.name == "deepseek-harness-sdk-runtime"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", _MODES)
|
||||
def test_bundled_runtime_surfaces_unbundled_plugin_failure(tmp_path: Path, mode: str) -> None:
|
||||
launch_args = _launch_args(mode)
|
||||
(tmp_path / "cordis.yml").write_text(
|
||||
"- id: missing\n name: '@deepseek-ai/dsh-does-not-exist'\n"
|
||||
)
|
||||
|
||||
client = _client(tmp_path, launch_args)
|
||||
client.start()
|
||||
try:
|
||||
with pytest.raises((TransportClosedError, TimeoutError)) as excinfo:
|
||||
client.initialize(cwd=str(tmp_path), model="deepseek-v4-pro")
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
assert "@deepseek-ai/dsh-does-not-exist" in str(excinfo.value)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", _MODES)
|
||||
def test_zero_config_run_injects_bundled_default_cordis_config(
|
||||
tmp_path: Path, mode: str, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
_launch_args(mode) # skip early when this carrier is unavailable
|
||||
monkeypatch.setenv("DSH_RUNTIME_MODE", mode)
|
||||
monkeypatch.delenv("DSH_CORDIS_CONFIG", raising=False)
|
||||
|
||||
harness = DeepSeekHarness(
|
||||
model="deepseek-v4-pro",
|
||||
cwd=str(tmp_path),
|
||||
session_root=str(tmp_path / "sessions"),
|
||||
api_key="sk-dummy-for-boot",
|
||||
base_url="http://127.0.0.1:9",
|
||||
request_timeout_seconds=120,
|
||||
)
|
||||
assert harness.client.config.env is not None
|
||||
assert harness.client.config.env["DSH_CORDIS_CONFIG"] == str(bundled_default_config_path())
|
||||
with harness:
|
||||
pass # __enter__ boots the runtime on the injected default config; __exit__ shuts it down
|
||||
@@ -0,0 +1,592 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from deepseek_harness import DeepSeekHarness, HarnessClient, HarnessConfig
|
||||
|
||||
|
||||
def test_high_level_sdk_runs_turn_and_collects_final_response(tmp_path: Path) -> None:
|
||||
script = tmp_path / "fake_runtime.py"
|
||||
env_dump = tmp_path / "env.json"
|
||||
script.write_text(
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
env_dump = os.environ["ENV_DUMP"]
|
||||
json.dump({
|
||||
"DEEPSEEK_API_KEY": os.environ.get("DEEPSEEK_API_KEY"),
|
||||
"DEEPSEEK_BASE_URL": os.environ.get("DEEPSEEK_BASE_URL"),
|
||||
"DSH_CWD": os.environ.get("DSH_CWD"),
|
||||
"DSH_SESSION_ROOT": os.environ.get("DSH_SESSION_ROOT"),
|
||||
"DSH_CORDIS_CONFIG": os.environ.get("DSH_CORDIS_CONFIG"),
|
||||
}, open(env_dump, "w"))
|
||||
|
||||
for line in sys.stdin:
|
||||
msg = json.loads(line)
|
||||
method = msg.get("method")
|
||||
if method == "initialize":
|
||||
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
|
||||
elif method == "session/prompt":
|
||||
params = msg.get("params") or {}
|
||||
print(json.dumps({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "session.event",
|
||||
"params": {
|
||||
"sessionId": params["sessionId"],
|
||||
"event": {
|
||||
"type": "assistant/message",
|
||||
"data": {"content": [{"type": "text", "text": "hello from runtime"}]},
|
||||
},
|
||||
},
|
||||
}), flush=True)
|
||||
print(json.dumps({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "session.finished",
|
||||
"params": {"sessionId": params["sessionId"], "status": "ok"},
|
||||
}), flush=True)
|
||||
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": True}}), flush=True)
|
||||
elif method == "shutdown":
|
||||
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
|
||||
break
|
||||
""".strip()
|
||||
)
|
||||
|
||||
with DeepSeekHarness(
|
||||
model="deepseek-v4-flash",
|
||||
cwd=str(tmp_path),
|
||||
cordis=str(tmp_path / "cordis.yml"),
|
||||
session_root=str(tmp_path / "sessions"),
|
||||
launch_args_override=(sys.executable, str(script)),
|
||||
env={
|
||||
"ENV_DUMP": str(env_dump),
|
||||
"DEEPSEEK_API_KEY": "env-key",
|
||||
"DEEPSEEK_BASE_URL": "http://127.0.0.1:4321",
|
||||
},
|
||||
) as harness:
|
||||
result = harness.run("say hello", session_id="main")
|
||||
|
||||
assert result.status == "ok"
|
||||
assert result.final_response == "hello from runtime"
|
||||
assert result.events[0]["type"] == "assistant/message"
|
||||
dumped_env = json.loads(env_dump.read_text())
|
||||
assert dumped_env["DEEPSEEK_API_KEY"] == "env-key"
|
||||
assert dumped_env["DEEPSEEK_BASE_URL"] == "http://127.0.0.1:4321"
|
||||
assert dumped_env["DSH_CWD"] == str(tmp_path)
|
||||
assert dumped_env["DSH_SESSION_ROOT"] == str(tmp_path / "sessions")
|
||||
assert dumped_env["DSH_CORDIS_CONFIG"] == str(tmp_path / "cordis.yml")
|
||||
|
||||
|
||||
def test_session_run_invokes_notification_callback_before_returning(tmp_path: Path) -> None:
|
||||
script = tmp_path / "fake_runtime.py"
|
||||
script.write_text(
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
|
||||
for line in sys.stdin:
|
||||
msg = json.loads(line)
|
||||
method = msg.get("method")
|
||||
if method == "initialize":
|
||||
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
|
||||
elif method == "session/prompt":
|
||||
print(json.dumps({"jsonrpc": "2.0", "method": "subagent.started", "params": {"parentSessionId": "main", "childSessionId": "child"}}), flush=True)
|
||||
print(json.dumps({"jsonrpc": "2.0", "method": "session.finished", "params": {"sessionId": "main", "status": "ok"}}), flush=True)
|
||||
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": True}}), flush=True)
|
||||
elif method == "shutdown":
|
||||
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
|
||||
break
|
||||
""".strip()
|
||||
)
|
||||
|
||||
seen: list[str] = []
|
||||
with DeepSeekHarness(
|
||||
launch_args_override=(sys.executable, str(script)),
|
||||
cwd=str(tmp_path),
|
||||
) as harness:
|
||||
session = harness.start_session("main")
|
||||
result = session.run(
|
||||
"spawn a helper",
|
||||
on_notification=lambda notification: seen.append(notification.method),
|
||||
)
|
||||
|
||||
assert result.status == "ok"
|
||||
assert seen == ["subagent.started", "session.finished"]
|
||||
|
||||
|
||||
def test_session_run_includes_subagent_finished_for_parent_session(tmp_path: Path) -> None:
|
||||
script = tmp_path / "fake_runtime.py"
|
||||
script.write_text(
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
|
||||
for line in sys.stdin:
|
||||
msg = json.loads(line)
|
||||
method = msg.get("method")
|
||||
if method == "initialize":
|
||||
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
|
||||
elif method == "session/prompt":
|
||||
print(json.dumps({"jsonrpc": "2.0", "method": "subagent.started", "params": {"parentSessionId": "main", "childSessionId": "child"}}), flush=True)
|
||||
print(json.dumps({"jsonrpc": "2.0", "method": "subagent.finished", "params": {"parentSessionId": "main", "childSessionId": "child", "status": "ok", "stopReason": "completed"}}), flush=True)
|
||||
print(json.dumps({"jsonrpc": "2.0", "method": "session.finished", "params": {"sessionId": "main", "status": "ok"}}), flush=True)
|
||||
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": True}}), flush=True)
|
||||
elif method == "shutdown":
|
||||
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
|
||||
break
|
||||
""".strip()
|
||||
)
|
||||
|
||||
with DeepSeekHarness(
|
||||
launch_args_override=(sys.executable, str(script)),
|
||||
cwd=str(tmp_path),
|
||||
) as harness:
|
||||
result = harness.run("spawn a helper", session_id="main")
|
||||
|
||||
assert result.status == "ok"
|
||||
assert [notification.method for notification in result.notifications] == [
|
||||
"subagent.started",
|
||||
"subagent.finished",
|
||||
"session.finished",
|
||||
]
|
||||
|
||||
|
||||
def test_session_run_ignores_notifications_for_other_sessions(tmp_path: Path) -> None:
|
||||
script = tmp_path / "fake_runtime.py"
|
||||
script.write_text(
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
|
||||
for line in sys.stdin:
|
||||
msg = json.loads(line)
|
||||
method = msg.get("method")
|
||||
if method == "initialize":
|
||||
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
|
||||
elif method == "session/prompt":
|
||||
params = msg.get("params") or {}
|
||||
print(json.dumps({"jsonrpc": "2.0", "method": "session.event", "params": {"sessionId": "other", "event": {"type": "assistant/message", "data": {"content": [{"type": "text", "text": "wrong session"}]}}}}), flush=True)
|
||||
print(json.dumps({"jsonrpc": "2.0", "method": "session.finished", "params": {"sessionId": "other", "status": "ok"}}), flush=True)
|
||||
print(json.dumps({"jsonrpc": "2.0", "method": "session.event", "params": {"sessionId": params["sessionId"], "event": {"type": "assistant/message", "data": {"content": [{"type": "text", "text": "right session"}]}}}}), flush=True)
|
||||
print(json.dumps({"jsonrpc": "2.0", "method": "session.finished", "params": {"sessionId": params["sessionId"], "status": "ok"}}), flush=True)
|
||||
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": True}}), flush=True)
|
||||
elif method == "shutdown":
|
||||
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
|
||||
break
|
||||
""".strip()
|
||||
)
|
||||
|
||||
with DeepSeekHarness(
|
||||
launch_args_override=(sys.executable, str(script)),
|
||||
cwd=str(tmp_path),
|
||||
) as harness:
|
||||
result = harness.run("stay in your lane", session_id="main")
|
||||
|
||||
assert result.status == "ok"
|
||||
assert result.final_response == "right session"
|
||||
assert [notification.payload.get("sessionId") for notification in result.notifications] == ["main", "main"]
|
||||
|
||||
|
||||
def test_high_level_session_run_does_not_accumulate_global_notifications(tmp_path: Path) -> None:
|
||||
script = tmp_path / "fake_runtime.py"
|
||||
script.write_text(
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
|
||||
for line in sys.stdin:
|
||||
msg = json.loads(line)
|
||||
method = msg.get("method")
|
||||
if method == "initialize":
|
||||
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
|
||||
elif method == "session/prompt":
|
||||
params = msg.get("params") or {}
|
||||
print(json.dumps({"jsonrpc": "2.0", "method": "session.event", "params": {"sessionId": params["sessionId"], "event": {"type": "assistant/message", "data": {"content": [{"type": "text", "text": "ok"}]}}}}), flush=True)
|
||||
print(json.dumps({"jsonrpc": "2.0", "method": "session.finished", "params": {"sessionId": params["sessionId"], "status": "ok"}}), flush=True)
|
||||
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": True}}), flush=True)
|
||||
elif method == "shutdown":
|
||||
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
|
||||
break
|
||||
""".strip()
|
||||
)
|
||||
|
||||
with DeepSeekHarness(launch_args_override=(sys.executable, str(script)), cwd=str(tmp_path)) as harness:
|
||||
result = harness.run("one turn", session_id="main")
|
||||
assert result.status == "ok"
|
||||
assert harness.client._notifications.qsize() == 0
|
||||
|
||||
|
||||
def test_session_run_waits_for_late_finished_without_replaying_stale_notifications(tmp_path: Path) -> None:
|
||||
script = tmp_path / "fake_runtime.py"
|
||||
script.write_text(
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
|
||||
turn = 0
|
||||
for line in sys.stdin:
|
||||
msg = json.loads(line)
|
||||
method = msg.get("method")
|
||||
if method == "initialize":
|
||||
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
|
||||
elif method == "session/prompt":
|
||||
turn += 1
|
||||
params = msg.get("params") or {}
|
||||
session_id = params["sessionId"]
|
||||
if turn == 1:
|
||||
print(json.dumps({"jsonrpc": "2.0", "method": "session.event", "params": {"sessionId": session_id, "event": {"type": "assistant/message", "data": {"content": [{"type": "text", "text": "first"}]}}}}), flush=True)
|
||||
print(json.dumps({"jsonrpc": "2.0", "method": "session.finished", "params": {"sessionId": session_id, "status": "ok"}}), flush=True)
|
||||
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": True}}), flush=True)
|
||||
else:
|
||||
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": True}}), flush=True)
|
||||
time.sleep(0.05)
|
||||
print(json.dumps({"jsonrpc": "2.0", "method": "session.event", "params": {"sessionId": session_id, "event": {"type": "assistant/message", "data": {"content": [{"type": "text", "text": "second"}]}}}}), flush=True)
|
||||
print(json.dumps({"jsonrpc": "2.0", "method": "session.finished", "params": {"sessionId": session_id, "status": "ok"}}), flush=True)
|
||||
elif method == "shutdown":
|
||||
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
|
||||
break
|
||||
""".strip()
|
||||
)
|
||||
|
||||
with DeepSeekHarness(launch_args_override=(sys.executable, str(script)), cwd=str(tmp_path)) as harness:
|
||||
first = harness.run("first turn", session_id="main")
|
||||
second = harness.run("second turn", session_id="main")
|
||||
|
||||
assert first.final_response == "first"
|
||||
assert second.final_response == "second"
|
||||
assert [notification.payload.get("sessionId") for notification in second.notifications] == ["main", "main"]
|
||||
|
||||
|
||||
def test_client_starts_subprocess_sends_requests_and_routes_notifications(tmp_path: Path) -> None:
|
||||
script = tmp_path / "fake_bridge.py"
|
||||
script.write_text(
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
|
||||
for line in sys.stdin:
|
||||
msg = json.loads(line)
|
||||
method = msg.get("method")
|
||||
if method == "initialize":
|
||||
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
|
||||
elif method == "session/prompt":
|
||||
params = msg.get("params") or {}
|
||||
print(json.dumps({"jsonrpc": "2.0", "method": "llm/request", "params": {"requestId": "req-1", "sessionId": params["sessionId"], "model": "dsagent", "messages": []}}), flush=True)
|
||||
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": True}}), flush=True)
|
||||
elif method == "shutdown":
|
||||
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
|
||||
break
|
||||
""".strip()
|
||||
)
|
||||
|
||||
with HarnessClient(
|
||||
HarnessConfig(launch_args_override=(sys.executable, str(script)))
|
||||
) as client:
|
||||
init = client.initialize(cwd="/workspace", model="dsagent")
|
||||
assert init.serverInfo.name == "fake-dsh"
|
||||
|
||||
client.session_prompt("main", [{"type": "text", "text": "fix it"}], profile="build")
|
||||
notification = client.next_notification()
|
||||
assert notification.method == "llm/request"
|
||||
assert notification.payload["requestId"] == "req-1"
|
||||
assert notification.payload["sessionId"] == "main"
|
||||
|
||||
|
||||
def test_client_keeps_unmatched_notifications_available_globally_while_subscribed() -> None:
|
||||
client = HarnessClient()
|
||||
with client.subscribe_session_notifications("main"):
|
||||
client._handle_message({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "session.event",
|
||||
"params": {"sessionId": "other", "event": {"type": "assistant/message"}},
|
||||
})
|
||||
|
||||
assert client._notifications.qsize() == 1
|
||||
notification = client._notifications.get_nowait()
|
||||
assert not isinstance(notification, BaseException)
|
||||
assert notification.method == "session.event"
|
||||
assert notification.payload["sessionId"] == "other"
|
||||
|
||||
|
||||
def test_client_rejects_unaccepted_session_prompt_response(tmp_path: Path) -> None:
|
||||
script = tmp_path / "fake_bridge.py"
|
||||
script.write_text(
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
|
||||
for line in sys.stdin:
|
||||
msg = json.loads(line)
|
||||
method = msg.get("method")
|
||||
if method == "initialize":
|
||||
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
|
||||
elif method == "session/prompt":
|
||||
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": False}}), flush=True)
|
||||
elif method == "shutdown":
|
||||
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
|
||||
break
|
||||
""".strip()
|
||||
)
|
||||
|
||||
with HarnessClient(HarnessConfig(launch_args_override=(sys.executable, str(script)))) as client:
|
||||
client.initialize(cwd="/workspace", model="dsagent")
|
||||
with pytest.raises(ValueError):
|
||||
client.session_prompt("main", [{"type": "text", "text": "fix it"}], profile="build")
|
||||
|
||||
|
||||
def test_client_routes_bridge_requests_and_sends_responses(tmp_path: Path) -> None:
|
||||
script = tmp_path / "fake_bridge.py"
|
||||
script.write_text(
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
|
||||
for line in sys.stdin:
|
||||
msg = json.loads(line)
|
||||
method = msg.get("method")
|
||||
if method == "initialize":
|
||||
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
|
||||
print(json.dumps({"jsonrpc": "2.0", "id": "bridge-req-1", "method": "llm.request", "params": {"requestId": "req-1", "sessionId": "main", "model": "dsagent", "messages": []}}), flush=True)
|
||||
elif "id" in msg and "method" not in msg:
|
||||
print(json.dumps({"jsonrpc": "2.0", "method": "response/seen", "params": {"result": msg.get("result")}}), flush=True)
|
||||
elif method == "shutdown":
|
||||
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
|
||||
break
|
||||
""".strip()
|
||||
)
|
||||
|
||||
with HarnessClient(
|
||||
HarnessConfig(launch_args_override=(sys.executable, str(script)))
|
||||
) as client:
|
||||
client.initialize(cwd="/workspace", model="dsagent")
|
||||
|
||||
request = client.next_request()
|
||||
assert request.id == "bridge-req-1"
|
||||
assert request.method == "llm.request"
|
||||
assert request.payload["requestId"] == "req-1"
|
||||
|
||||
client.respond(request.id, {"content_blocks": [{"type": "text", "text": "done"}]})
|
||||
notification = client.next_notification()
|
||||
assert notification.method == "response/seen"
|
||||
assert notification.payload["result"]["content_blocks"][0]["text"] == "done"
|
||||
|
||||
|
||||
def test_client_ignores_non_json_stdout_lines(tmp_path: Path) -> None:
|
||||
script = tmp_path / "fake_bridge.py"
|
||||
script.write_text(
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
|
||||
print("node warning: experimental loader", flush=True)
|
||||
for line in sys.stdin:
|
||||
msg = json.loads(line)
|
||||
if msg.get("method") == "initialize":
|
||||
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
|
||||
elif msg.get("method") == "shutdown":
|
||||
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
|
||||
break
|
||||
""".strip()
|
||||
)
|
||||
|
||||
with HarnessClient(
|
||||
HarnessConfig(launch_args_override=(sys.executable, str(script)))
|
||||
) as client:
|
||||
init = client.initialize(cwd="/workspace", model="dsagent")
|
||||
assert init.serverInfo.name == "fake-dsh"
|
||||
|
||||
|
||||
def test_client_request_times_out_when_bridge_does_not_respond(tmp_path: Path) -> None:
|
||||
script = tmp_path / "fake_bridge.py"
|
||||
script.write_text(
|
||||
"""
|
||||
import time
|
||||
|
||||
time.sleep(60)
|
||||
""".strip()
|
||||
)
|
||||
|
||||
with HarnessClient(
|
||||
HarnessConfig(
|
||||
launch_args_override=(sys.executable, str(script)),
|
||||
request_timeout_seconds=0.1,
|
||||
)
|
||||
) as client:
|
||||
start = time.monotonic()
|
||||
try:
|
||||
client.initialize(cwd="/workspace", model="dsagent")
|
||||
except TimeoutError:
|
||||
assert time.monotonic() - start < 2
|
||||
else:
|
||||
raise AssertionError("initialize should time out")
|
||||
|
||||
|
||||
def test_client_close_times_out_when_shutdown_does_not_respond(tmp_path: Path) -> None:
|
||||
script = tmp_path / "fake_bridge.py"
|
||||
script.write_text(
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
|
||||
for line in sys.stdin:
|
||||
msg = json.loads(line)
|
||||
if msg.get("method") == "initialize":
|
||||
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
|
||||
elif msg.get("method") == "shutdown":
|
||||
time.sleep(60)
|
||||
""".strip()
|
||||
)
|
||||
|
||||
client = HarnessClient(
|
||||
HarnessConfig(
|
||||
launch_args_override=(sys.executable, str(script)),
|
||||
shutdown_timeout_seconds=0.1,
|
||||
)
|
||||
)
|
||||
client.start()
|
||||
client.initialize(cwd="/workspace", model="dsagent")
|
||||
start = time.monotonic()
|
||||
client.close()
|
||||
assert time.monotonic() - start < 2
|
||||
|
||||
|
||||
def test_client_close_is_idempotent_before_and_after_start(tmp_path: Path) -> None:
|
||||
HarnessClient().close()
|
||||
|
||||
script = tmp_path / "fake_bridge.py"
|
||||
script.write_text(
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
|
||||
for line in sys.stdin:
|
||||
msg = json.loads(line)
|
||||
if msg.get("method") == "initialize":
|
||||
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
|
||||
elif msg.get("method") == "shutdown":
|
||||
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
|
||||
break
|
||||
""".strip()
|
||||
)
|
||||
|
||||
client = HarnessClient(HarnessConfig(launch_args_override=(sys.executable, str(script))))
|
||||
client.start()
|
||||
client.initialize(cwd="/workspace", model="dsagent")
|
||||
client.close()
|
||||
client.close()
|
||||
|
||||
|
||||
def test_runtime_closed_error_includes_stderr_tail(tmp_path: Path) -> None:
|
||||
script = tmp_path / "crashing_runtime.py"
|
||||
script.write_text(
|
||||
"""
|
||||
import sys
|
||||
|
||||
print("fatal bridge exploded", file=sys.stderr, flush=True)
|
||||
sys.exit(42)
|
||||
""".strip()
|
||||
)
|
||||
|
||||
with HarnessClient(
|
||||
HarnessConfig(
|
||||
launch_args_override=(sys.executable, str(script)),
|
||||
request_timeout_seconds=2,
|
||||
)
|
||||
) as client:
|
||||
with pytest.raises(Exception, match="fatal bridge exploded"):
|
||||
client.initialize(cwd="/workspace", model="dsagent")
|
||||
|
||||
|
||||
def test_client_serializes_concurrent_writes(tmp_path: Path) -> None:
|
||||
script = tmp_path / "fake_bridge.py"
|
||||
output = tmp_path / "seen.jsonl"
|
||||
script.write_text(
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
with open(os.environ["SEEN"], "w") as seen:
|
||||
for line in sys.stdin:
|
||||
seen.write(line)
|
||||
seen.flush()
|
||||
msg = json.loads(line)
|
||||
if "id" in msg and msg.get("method") == "initialize":
|
||||
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
|
||||
elif "id" in msg and msg.get("method") == "shutdown":
|
||||
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
|
||||
break
|
||||
""".strip()
|
||||
)
|
||||
|
||||
with HarnessClient(
|
||||
HarnessConfig(
|
||||
launch_args_override=(sys.executable, str(script)),
|
||||
env={"SEEN": str(output)},
|
||||
)
|
||||
) as client:
|
||||
client.initialize(cwd="/workspace", model="dsagent")
|
||||
threads = [
|
||||
threading.Thread(target=client.notify, args=(f"notice-{index}", {"index": index}))
|
||||
for index in range(50)
|
||||
]
|
||||
for thread in threads:
|
||||
thread.start()
|
||||
for thread in threads:
|
||||
thread.join()
|
||||
|
||||
for line in output.read_text().splitlines():
|
||||
json.loads(line)
|
||||
|
||||
|
||||
def test_client_uses_bundled_runtime_package_by_default(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
runtime = tmp_path / "dsh-jsonrpc-agent"
|
||||
runtime.write_text(
|
||||
"""#!/usr/bin/env python3
|
||||
import json
|
||||
import sys
|
||||
|
||||
for line in sys.stdin:
|
||||
msg = json.loads(line)
|
||||
if msg.get("method") == "initialize":
|
||||
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "bundled-runtime"}}}), flush=True)
|
||||
elif msg.get("method") == "shutdown":
|
||||
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
|
||||
break
|
||||
""".strip()
|
||||
)
|
||||
runtime.chmod(0o755)
|
||||
|
||||
module_dir = tmp_path / "deepseek_harness_runtime"
|
||||
module_dir.mkdir()
|
||||
(module_dir / "__init__.py").write_text(
|
||||
f"""
|
||||
def resolve_bundled_launch_args(mode=None):
|
||||
return ({str(runtime)!r},)
|
||||
""".strip()
|
||||
)
|
||||
|
||||
monkeypatch.syspath_prepend(str(tmp_path))
|
||||
monkeypatch.delitem(sys.modules, "deepseek_harness_runtime", raising=False)
|
||||
|
||||
with HarnessClient() as client:
|
||||
init = client.initialize(cwd="/workspace", model="deepseek-v4-pro")
|
||||
|
||||
assert init.serverInfo.name == "bundled-runtime"
|
||||
|
||||
|
||||
def test_client_reports_missing_bundled_runtime_dependency(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.delitem(sys.modules, "deepseek_harness_runtime", raising=False)
|
||||
monkeypatch.setattr(sys, "path", [])
|
||||
|
||||
with pytest.raises(FileNotFoundError, match="Install deepseek-harness-runtime-bin"):
|
||||
HarnessClient().start()
|
||||
@@ -0,0 +1,43 @@
|
||||
"""Keyless tests for the deepseek_harness_runtime resolution API.
|
||||
|
||||
These never launch a runtime, so they run everywhere regardless of which
|
||||
bundled artifacts are present; the launch-and-boot coverage lives in
|
||||
``test_bundled_runtime.py``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from deepseek_harness_runtime import (
|
||||
RUNTIME_MODE_ENV_VAR,
|
||||
bundled_default_config_path,
|
||||
bundled_package_dir,
|
||||
resolve_bundled_launch_args,
|
||||
)
|
||||
|
||||
|
||||
def test_default_config_is_shipped_with_the_package() -> None:
|
||||
path = bundled_default_config_path()
|
||||
assert path == bundled_package_dir() / "runtime" / "cordis.yml"
|
||||
assert "@deepseek-ai/dsh-agent-core" in path.read_text()
|
||||
|
||||
|
||||
def test_unknown_explicit_mode_fails_loud() -> None:
|
||||
with pytest.raises(ValueError, match="expected 'exe' or 'node'"):
|
||||
resolve_bundled_launch_args("bogus")
|
||||
|
||||
|
||||
def test_unknown_env_mode_fails_loud(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv(RUNTIME_MODE_ENV_VAR, "bogus")
|
||||
with pytest.raises(ValueError, match="expected 'exe' or 'node'"):
|
||||
resolve_bundled_launch_args()
|
||||
|
||||
|
||||
def test_explicit_mode_wins_over_env_mode(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv(RUNTIME_MODE_ENV_VAR, "bogus")
|
||||
try:
|
||||
args = resolve_bundled_launch_args("exe")
|
||||
except FileNotFoundError:
|
||||
return # explicit 'exe' was honored; only the artifact is missing
|
||||
assert args[0].endswith(("-x64", "-arm64"))
|
||||
Generated
+321
@@ -0,0 +1,321 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.10"
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
version = "0.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-harness"
|
||||
version = "0.0.0.dev0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "deepseek-harness-runtime-bin" },
|
||||
{ name = "pydantic" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
test = [
|
||||
{ name = "pytest" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "deepseek-harness-runtime-bin", editable = "../sdk-runtime" },
|
||||
{ name = "pydantic", specifier = ">=2.12" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
test = [{ name = "pytest", specifier = ">=8.0" }]
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-harness-runtime-bin"
|
||||
version = "0.0.0.dev0"
|
||||
source = { editable = "../sdk-runtime" }
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
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 = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "26.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.13.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-types" },
|
||||
{ name = "pydantic-core" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.46.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.20.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
||||
{ name = "iniconfig" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pluggy" },
|
||||
{ name = "pygments" },
|
||||
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
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" },
|
||||
]
|
||||
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 = [
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
|
||||
]
|
||||
Reference in New Issue
Block a user