diff --git a/.claude/skills/pybert-mcp/SKILL.md b/.claude/skills/pybert-mcp/SKILL.md new file mode 100644 index 0000000..6fcabba --- /dev/null +++ b/.claude/skills/pybert-mcp/SKILL.md @@ -0,0 +1,98 @@ +--- +name: pybert-mcp +description: Drive PyBERT's MCP server to run headless serial-link BER simulations, read/write configuration, browse saved results, and inspect IBIS-AMI models, without opening the GUI. Use when asked to run a PyBERT simulation, tune Tx/Rx/channel/CTLE/DFE settings, evaluate link performance (BER, jitter), compare configurations, or introspect an IBIS/AMI file. +allowed-tools: mcp__pybert__get_default_config mcp__pybert__get_config mcp__pybert__set_config mcp__pybert__run_simulation mcp__pybert__inspect_results_file mcp__pybert__list_ibis_models mcp__pybert__inspect_ibis_model +--- + +# PyBERT MCP + +Seven tools, registered as `mcp__pybert__*` (server defined in `.mcp.json`, implementation in +`src/pybert/mcp/server.py`). Every call constructs a fresh, isolated `PyBERT` instance +server-side — there is no session state between calls, and no shared cache. Whatever settings you +want in effect for a given `run_simulation` (or `set_config`) call must be present in the `config` +dict you pass to *that* call. + +## Prerequisites + +- The server needs the `mcp` extra installed: `uv sync --extra mcp`. If it isn't, tool calls will + fail at the transport level (the server process itself exits with a clear "MCP support requires: + pip install 'pipbert[mcp]'" message if invoked directly, but from inside a session that just + looks like the server failing to start — tell the user to run `uv sync --extra mcp` if that + happens). +- **File paths are resolved relative to the server process's working directory**, which is this + repo's root (set by `.mcp.json`) — not the user's shell cwd, not this skill's directory. Always + pass repo-relative paths (e.g. `"models/ibisami/example_rx.ibs"`), matching how paths are used + throughout this repo's own tests and examples (`tests/test_mcp.py`, `examples/mcp_client.py`). + +## 1. Configuration — `get_default_config` / `get_config` / `set_config` + +- Always start from a *full* config dict — `get_default_config()` or `get_config(config_file)` — + never hand-build one. It has ~88 keys; real names include `bit_rate`, `nbits`, `eye_bits`, + `tx_taps`, `rx_bw`, `peak_mag`, `rx_use_viterbi`, `use_dfe`. Don't guess a key name; read it off + the dict you just fetched. +- Both `set_config` and `run_simulation` validate keys strictly: an unrecognized key (e.g. a typo + like `"bit_rat"`) raises an error naming the bad key(s). That's intentional — treat it as "you + used a wrong key name," not as a bug to route around. +- `set_config(overrides, out_file, base_config_file=None)` layers `overrides` onto + `base_config_file` (or the defaults) and writes the result to `out_file` as YAML — it does not + run a simulation. Use it to persist a tuned configuration for later use with the GUI + (`pybert -c out_file`) or CLI (`pybert sim out_file`). + +## 2. Running a simulation — `run_simulation` + +`run_simulation(config, results_file=None)` runs headless and returns scalar metrics directly: +`status`, `total_perf`, `jitter_perf`, `n_errs_dfe`, `n_errs_viterbi`, `chnl_dly`, `isi_dfe`, +`dcd_dfe`, `pj_dfe`, `rj_dfe`, `perf_info` (an HTML table), `jitter_info`. + +- **Checking success**: `status` is a free-text log message, not a boolean — don't treat any + non-empty string as success. On success it ends as `"Ready."`. On failure it starts with + `"Exception: ..."` or reads `"Aborted Simulation"`. Check for those. +- `n_errs_viterbi == -1` means Viterbi/FEC decoding wasn't enabled for that run (`rx_use_viterbi` + was false) — it's a sentinel, not an error. +- For tuning/comparison loops, shrink `nbits`/`eye_bits` first (e.g. `1000`/`500`) — full-length + runs are much slower and unnecessary until you've converged on settings worth a final, + full-length confirmation run. +- Only pass `results_file` when you actually need `inspect_results_file` afterward — it triggers + extra plot-data population and file I/O a metrics-only call doesn't need. + +## 3. Browsing saved results — `inspect_results_file` + +Only works on files written by `run_simulation(..., results_file=...)` (or `pybert sim`). It +reports `{array_name: {shape, dtype, min, max, mean}}` per waveform (impulse/step/pulse responses, +etc.) — **never** raw samples, and **never** scalar performance metrics (those exist only live, +via `run_simulation`'s return value — they aren't persisted in `.pybert_data` files). If you need +actual sample values, there is no tool for that here; say so rather than inventing numbers. + +## 4. IBIS-AMI introspection — `list_ibis_models` / `inspect_ibis_model` + +Two-step, always in this order: +1. `list_ibis_models(ibis_file)` — confirms the file parses (`parsing_errors == "Success!"`) and + lists valid `components`/`models` names. +2. `inspect_ibis_model(ibis_file, model_name)` — pass an exact name from step 1's `models` list. + +`inspect_ibis_model` returns `null` for `zout`/`slew` on Rx-only models and for `zin` on Tx-only +models — that's expected (those are driver-only / receiver-only properties), not missing data. +`ami_files` lists DLL/SO + `.ami` paths per platform/bitness; this tool never executes the AMI +model itself. + +## Common workflows + +**Tune a parameter and compare performance:** +1. `get_default_config()` (or `get_config` on an existing file) → `cfg`. +2. Shrink `cfg["nbits"]` / `cfg["eye_bits"]` for speed. +3. For each candidate setting: mutate the relevant key(s) on a copy of `cfg` (e.g. `cfg["rx_bw"]`, + `cfg["peak_mag"]`, `cfg["tx_taps"]`) → `run_simulation(cfg)` → compare `total_perf` / + `jitter_perf` across variants, checking `status` on each. +4. Once satisfied, run once more at full-length `nbits`/`eye_bits` for the number you actually + report, optionally with `results_file` set. Report the numbers a tool call actually returned — + never estimate or interpolate a result. + +**Persist a tuned configuration:** `set_config(overrides, out_file)` once you've found values +worth keeping. + +## Pitfalls + +- Don't fabricate simulation numbers or waveform contents. If a call errors or a result looks odd, + report it as-is rather than smoothing it over. +- Config-key validation errors are the server telling you the key doesn't exist — fix the key name + using the dict from `get_default_config`/`get_config`, don't retry the same key. diff --git a/.gitignore b/.gitignore index a7cc1ac..133981a 100644 --- a/.gitignore +++ b/.gitignore @@ -111,6 +111,10 @@ parsec.py/ # Temporary files .* !.*.yaml +!.mcp.json +!.claude/ +.claude/* +!.claude/skills/ dummy* # Debugging output diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..7aca9bd --- /dev/null +++ b/.mcp.json @@ -0,0 +1,9 @@ +{ + "mcpServers": { + "pybert": { + "type": "stdio", + "command": "uv", + "args": ["run", "pybert", "mcp"] + } + } +} diff --git a/CLAUDE.md b/CLAUDE.md index 4a18583..831a846 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -25,6 +25,9 @@ make type-check # Build package make build + +# Start the MCP server (requires: uv sync --extra mcp) +uv run pybert mcp ``` ## Architecture @@ -50,6 +53,19 @@ PyBERT is a serial-link BERT simulator built on the Enthought Traits/TraitsUI st | `src/pybert/utility/sparam.py` | S-parameter helpers: `import_channel`, `import_freq`, `sdd_21`, `interp_s2p` | | `src/pybert/utility/ibisami.py` | `run_ami_model()` — drives pyibisami DLL/SO | | `src/pybert/utility/sigproc.py` | Signal processing: `import_time`, `trim_impulse`, `raised_cosine` | +| `src/pybert/mcp/server.py` | MCP server (`pybert mcp`, needs the `mcp` extra): tools for running simulations, reading/writing config, browsing saved results, and inspecting IBIS-AMI models | + +### MCP server (`src/pybert/mcp/`) + +`pybert mcp` starts a stdio MCP server (built on the `mcp` package, an optional extra — +`pip install "pipbert[mcp]"` / `uv sync --extra mcp`). `src/pybert/mcp/server.py` exposes plain, +directly-testable functions (`run_simulation`, `get_config`/`set_config`, `inspect_results_file`, +`list_ibis_models`/`inspect_ibis_model`) and wraps them as MCP tools in `build_server()`. Each call +constructs its own fresh `PyBERT(gui=False)` instance; nothing is cached across calls. +`_apply_config()` round-trips config dicts through a temp YAML file and `PyBERT.load_configuration()` +so `PyBertCfg`'s existing tap-tuple/legacy-rename handling is reused rather than duplicated. Note: +`.pybert_data` files only ever hold waveform arrays, never scalar performance metrics — those are +only available live, right after `simulate()`, which is why `run_simulation` returns them directly. ### Submodule: PyAMI (`PyAMI/`) diff --git a/MANIFEST.in b/MANIFEST.in index 9362a97..a04be5a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -8,6 +8,8 @@ include docs/source/* prune docs/build include tests/*.py prune tests/__pycache__ +include examples/*.py +include examples/*.ipynb include tox.ini recursive-include type_stubs * global-exclude *.swp diff --git a/examples/mcp_client.py b/examples/mcp_client.py new file mode 100644 index 0000000..ca79090 --- /dev/null +++ b/examples/mcp_client.py @@ -0,0 +1,62 @@ +"""Example: drive PyBERT's MCP server from a plain Python MCP client. + +This spawns `pybert mcp` as a subprocess over stdio (the same way an MCP-aware +tool like Claude Code/Desktop would) and calls a few of its tools: + + * `get_default_config` - fetch PyBERT's default configuration + * `run_simulation` - run a (small, fast) headless simulation + * `list_ibis_models` - introspect an IBIS file bundled with the repo + +Requires the `mcp` extra: `uv sync --extra mcp` (or `pip install "pipbert[mcp]"`). + +Run from anywhere with: + + uv run python examples/mcp_client.py +""" +import asyncio +import json +from pathlib import Path + +from mcp import ClientSession, StdioServerParameters, stdio_client + +REPO_ROOT = Path(__file__).resolve().parent.parent +EXAMPLE_IBIS_FILE = "models/ibisami/example_rx.ibs" # relative to REPO_ROOT + + +def _text(result) -> str: + """Pull the JSON text out of a `CallToolResult`.""" + return result.content[0].text + + +async def main() -> None: + # `cwd=REPO_ROOT` matters: PyBERT config/model file paths (like EXAMPLE_IBIS_FILE + # below) are resolved relative to the server process's working directory. + params = StdioServerParameters(command="uv", args=["run", "pybert", "mcp"], cwd=str(REPO_ROOT)) + + async with stdio_client(params) as (read, write), ClientSession(read, write) as session: + await session.initialize() + + tools = await session.list_tools() + print("Available tools:", ", ".join(sorted(t.name for t in tools.tools))) + + # 1. Fetch PyBERT's default configuration. + result = await session.call_tool("get_default_config", {}) + config = json.loads(_text(result)) + print(f"\nDefault config has {len(config)} settings, e.g. bit_rate={config['bit_rate']} Gbps") + + # 2. Shrink it down and run a quick simulation. + config["nbits"] = 1000 + config["eye_bits"] = 500 + result = await session.call_tool("run_simulation", {"config": config}) + metrics = json.loads(_text(result)) + print(f"\nSimulation status: {metrics['status']}") + print(f"Total jitter/noise-limited performance: {metrics['total_perf']:.3g} bits/s") + + # 3. Introspect an IBIS-AMI model. + result = await session.call_tool("list_ibis_models", {"ibis_file": EXAMPLE_IBIS_FILE}) + ibis_info = json.loads(_text(result)) + print(f"\n{EXAMPLE_IBIS_FILE}: components={ibis_info['components']}, models={ibis_info['models']}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/mcp_notebook.ipynb b/examples/mcp_notebook.ipynb new file mode 100644 index 0000000..4f45a11 --- /dev/null +++ b/examples/mcp_notebook.ipynb @@ -0,0 +1,567 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "b30a790f", + "metadata": {}, + "source": [ + "# Driving PyBERT's MCP Server from a Notebook\n", + "\n", + "This notebook is the interactive counterpart to `examples/mcp_client.py`: it drives PyBERT's\n", + "MCP server as a real MCP client would, spawning `pybert mcp` as a subprocess and talking to it\n", + "over stdio.\n", + "\n", + "It exercises all four capability groups the server exposes:\n", + "\n", + "1. **Configuration** — `get_default_config`\n", + "2. **Simulation** — `run_simulation`\n", + "3. **Results browsing** — `inspect_results_file`\n", + "4. **IBIS-AMI introspection** — `list_ibis_models`, `inspect_ibis_model`\n", + "\n", + "**Prerequisite:** the `mcp` extra must be installed: `uv sync --extra mcp` (or `pip install\n", + "\"pipbert[mcp]\"`).\n", + "\n", + "**Note:** this notebook must be run with a kernel started from the repo root (or adjust\n", + "`REPO_ROOT` below), since PyBERT resolves config/model file paths relative to the MCP server\n", + "process's working directory.\n", + "\n", + "**Why one big call cell?** The MCP stdio transport (`anyio`) opens a task group that must be\n", + "entered and exited from the *same* asyncio task. Jupyter runs each cell's top-level `await` as a\n", + "new task, so a session opened in one cell can't be used or closed from a later one. The cell below\n", + "therefore makes the whole round of tool calls in a single `async with` block and stashes the\n", + "results in plain Python variables; every cell after that just inspects those variables (no `await`\n", + "needed, so no task-boundary problem)." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "4f7f43eb", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-30T21:11:58.838431Z", + "iopub.status.busy": "2026-08-30T21:11:58.837857Z", + "iopub.status.idle": "2026-08-30T21:11:59.233760Z", + "shell.execute_reply": "2026-08-30T21:11:59.233310Z", + "shell.execute_reply.started": "2026-08-30T21:11:58.838399Z" + } + }, + "outputs": [], + "source": [ + "import json\n", + "from pathlib import Path\n", + "\n", + "from mcp import ClientSession, StdioServerParameters, stdio_client\n", + "\n", + "REPO_ROOT = Path.cwd()\n", + "if not (REPO_ROOT / \"pyproject.toml\").exists():\n", + " REPO_ROOT = REPO_ROOT.parent # running from examples/\n", + "\n", + "EXAMPLE_IBIS_FILE = \"models/ibisami/example_rx.ibs\" # relative to REPO_ROOT\n", + "RESULTS_FILE = str(REPO_ROOT / \"examples\" / \"mcp_notebook_results.pybert_data\")\n", + "\n", + "\n", + "def _json(result) -> dict:\n", + " \"\"\"Pull the JSON payload out of a `CallToolResult`.\"\"\"\n", + " return json.loads(result.content[0].text)" + ] + }, + { + "cell_type": "markdown", + "id": "9922e109", + "metadata": {}, + "source": [ + "## Talk to the server\n", + "\n", + "One session, all six tool calls, in order. Re-run this cell any time you want a fresh round trip\n", + "against the server (e.g. after editing a config override below)." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "5ad7afc7", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-30T21:11:59.234281Z", + "iopub.status.busy": "2026-08-30T21:11:59.234158Z", + "iopub.status.idle": "2026-08-30T21:12:02.854408Z", + "shell.execute_reply": "2026-08-30T21:12:02.853801Z", + "shell.execute_reply.started": "2026-08-30T21:11:59.234273Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Available tools: get_config, get_default_config, inspect_ibis_model, inspect_results_file, list_ibis_models, run_simulation, set_config\n", + "\n", + "Default config has 88 settings, e.g. bit_rate=10.0 Gbps\n", + "\n", + "Simulation status: Ready.\n", + "Total jitter/noise-limited performance: 3.03e+05 bits/s\n", + "\n", + "23 waveform arrays saved, e.g. chnl_h: {'shape': [640], 'dtype': 'float64', 'min': 1.867756818921923e-05, 'max': 0.012641013309207678, 'mean': 0.0007597733008077639}\n", + "\n", + "models/ibisami/example_rx.ibs: components=['Example_Rx'], models=['example_rx']\n", + "\n", + "example_rx: is_ami=True, type=Input\n" + ] + } + ], + "source": [ + "params = StdioServerParameters(command=\"uv\", args=[\"run\", \"pybert\", \"mcp\"], cwd=str(REPO_ROOT))\n", + "\n", + "async with stdio_client(params) as (read, write), ClientSession(read, write) as session:\n", + " await session.initialize()\n", + "\n", + " tools = await session.list_tools()\n", + " print(\"Available tools:\", \", \".join(sorted(t.name for t in tools.tools)))\n", + "\n", + " # 1. Configuration: fetch PyBERT's default configuration.\n", + " config = _json(await session.call_tool(\"get_default_config\", {}))\n", + " print(f\"\\nDefault config has {len(config)} settings, e.g. bit_rate={config['bit_rate']} Gbps\")\n", + "\n", + " # 2. Simulation: shrink the bit count for speed, then run.\n", + " config[\"nbits\"] = 1000\n", + " config[\"eye_bits\"] = 500\n", + " metrics = _json(await session.call_tool(\"run_simulation\", {\"config\": config}))\n", + " print(f\"\\nSimulation status: {metrics['status']}\")\n", + " print(f\"Total jitter/noise-limited performance: {metrics['total_perf']:.3g} bits/s\")\n", + "\n", + " # 3. Results browsing: re-run with `results_file` set, so waveforms get saved, then inspect them.\n", + " await session.call_tool(\"run_simulation\", {\"config\": config, \"results_file\": RESULTS_FILE})\n", + " summary = _json(await session.call_tool(\"inspect_results_file\", {\"results_file\": RESULTS_FILE}))\n", + " print(f\"\\n{len(summary)} waveform arrays saved, e.g. chnl_h: {summary['chnl_h']}\")\n", + "\n", + " # 4. IBIS-AMI introspection.\n", + " ibis_info = _json(await session.call_tool(\"list_ibis_models\", {\"ibis_file\": EXAMPLE_IBIS_FILE}))\n", + " print(f\"\\n{EXAMPLE_IBIS_FILE}: components={ibis_info['components']}, models={ibis_info['models']}\")\n", + "\n", + " model_name = ibis_info[\"models\"][0]\n", + " model_info = _json(\n", + " await session.call_tool(\"inspect_ibis_model\", {\"ibis_file\": EXAMPLE_IBIS_FILE, \"model_name\": model_name})\n", + " )\n", + " print(f\"\\n{model_name}: is_ami={model_info['is_ami']}, type={model_info['type']}\")" + ] + }, + { + "cell_type": "markdown", + "id": "ce588de9", + "metadata": {}, + "source": [ + "## Inspect the results\n", + "\n", + "Everything from here on is plain Python — no more `await`, just looking at the dicts the\n", + "cell above stashed." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "bf2ad636", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-30T21:12:02.854829Z", + "iopub.status.busy": "2026-08-30T21:12:02.854729Z", + "iopub.status.idle": "2026-08-30T21:12:02.856922Z", + "shell.execute_reply": "2026-08-30T21:12:02.856461Z", + "shell.execute_reply.started": "2026-08-30T21:12:02.854822Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " \"status\": \"Ready.\",\n", + " \"total_perf\": 303119.16923251457,\n", + " \"jitter_perf\": 2050340.210735076,\n", + " \"n_errs_dfe\": 0,\n", + " \"n_errs_viterbi\": -1,\n", + " \"chnl_dly\": 2.3812500000000002e-09,\n", + " \"isi_dfe\": 2.529571747261402e-11,\n", + " \"dcd_dfe\": 2.5686152568952654e-12,\n", + " \"pj_dfe\": 1.6475914203408647e-12,\n", + " \"rj_dfe\": 1.1103009864100038e-11,\n", + " \"perf_info\": \"

Performance by Component

\\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n
ComponentPerformance (Msmpls./min.)
Channel63.825
Tx Preemphasis345.541
CTLE157.980
FFE465.911
DFE50.580
Viterbi 0.000
Jitter Analysis123.020
TOTAL18.187
Plotting213331.709
\\n\",\n", + " \"jitter_info\": \"

Jitter Rejection by Equalization Component

\\n

Tx Preemphasis

\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n
Jitter ComponentInput (ps)Output (ps)Rejection (dB)
ISI26.60128.816-0.3
DCD 0.001 2.748-35.4
Pj 1.449 1.414n/a
Rj 4.817 4.819n/a
\\n

CTLE (+ AMI DFE)

\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n
Jitter ComponentInput (ps)Output (ps)Rejection (dB)
ISI28.81625.296 0.6
DCD 2.748 2.569 0.3
Pj 1.414 1.439-0.1
Rj 4.819 6.517-1.3
\\n

DFE

\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n
Jitter ComponentInput (ps)Output (ps)Rejection (dB)
ISI25.29625.296 0.0
DCD 2.569 2.569 0.0
Pj 1.439 1.648-0.6
Rj 6.51711.103-2.3
\\n

TOTAL

\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n
Jitter ComponentInput (ps)Output (ps)Rejection (dB)
ISI26.60125.296 0.2
DCD 0.001 2.569-35.1
Pj 1.414 1.648-0.7
Rj 4.81911.103-3.6
\\n\"\n", + "}\n" + ] + } + ], + "source": [ + "print(json.dumps(metrics, indent=2))" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "9cafb999", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-30T21:12:02.857204Z", + "iopub.status.busy": "2026-08-30T21:12:02.857143Z", + "iopub.status.idle": "2026-08-30T21:12:02.861810Z", + "shell.execute_reply": "2026-08-30T21:12:02.861349Z", + "shell.execute_reply.started": "2026-08-30T21:12:02.857198Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " \"chnl_h\": {\n", + " \"shape\": [\n", + " 640\n", + " ],\n", + " \"dtype\": \"float64\",\n", + " \"min\": 1.867756818921923e-05,\n", + " \"max\": 0.012641013309207678,\n", + " \"mean\": 0.0007597733008077639\n", + " },\n", + " \"tx_out_h\": {\n", + " \"shape\": [\n", + " 640\n", + " ],\n", + " \"dtype\": \"float64\",\n", + " \"min\": 0.0,\n", + " \"max\": 0.012641013309207678,\n", + " \"mean\": 0.0007566076416065674\n", + " },\n", + " \"ctle_out_h\": {\n", + " \"shape\": [\n", + " 640\n", + " ],\n", + " \"dtype\": \"float64\",\n", + " \"min\": -1.5553752483938216e-05,\n", + " \"max\": 0.012374019171556502,\n", + " \"mean\": 0.0007100476099718481\n", + " },\n", + " \"dfe_out_h\": {\n", + " \"shape\": [\n", + " 640\n", + " ],\n", + " \"dtype\": \"float64\",\n", + " \"min\": -1.5553752483938216e-05,\n", + " \"max\": 0.012374019171556502,\n", + " \"mean\": 0.0006970391785260132\n", + " },\n", + " \"chnl_s\": {\n", + " \"shape\": [\n", + " 640\n", + " ],\n", + " \"dtype\": \"float64\",\n", + " \"min\": 0.00017333743386144625,\n", + " \"max\": 0.4862549125169685,\n", + " \"mean\": 0.4468304970141168\n", + " },\n", + " \"tx_s\": {\n", + " \"shape\": [\n", + " 640\n", + " ],\n", + " \"dtype\": \"float64\",\n", + " \"min\": 0.0,\n", + " \"max\": 1.0,\n", + " \"mean\": 0.85\n", + " },\n", + " \"ctle_s\": {\n", + " \"shape\": [\n", + " 960\n", + " ],\n", + " \"dtype\": \"float64\",\n", + " \"min\": -0.02750331509012227,\n", + " \"max\": 1.0236602922617737,\n", + " \"mean\": 0.6610900234838056\n", + " },\n", + " \"dfe_s\": {\n", + " \"shape\": [\n", + " 640\n", + " ],\n", + " \"dtype\": \"float64\",\n", + " \"min\": 1.0,\n", + " \"max\": 1.0,\n", + " \"mean\": 1.0\n", + " },\n", + " \"tx_out_s\": {\n", + " \"shape\": [\n", + " 640\n", + " ],\n", + " \"dtype\": \"float64\",\n", + " \"min\": 0.0,\n", + " \"max\": 0.48422889062820257,\n", + " \"mean\": 0.374036290537997\n", + " },\n", + " \"ctle_out_s\": {\n", + " \"shape\": [\n", + " 640\n", + " ],\n", + " \"dtype\": \"float64\",\n", + " \"min\": -0.0002166227700901169,\n", + " \"max\": 0.45443047038198303,\n", + " \"mean\": 0.1419765890387012\n", + " },\n", + " \"dfe_out_s\": {\n", + " \"shape\": [\n", + " 640\n", + " ],\n", + " \"dtype\": \"float64\",\n", + " \"min\": -0.0002166227700901169,\n", + " \"max\": 0.4461050742566487,\n", + " \"mean\": 0.09690128826515317\n", + " },\n", + " \"chnl_p\": {\n", + " \"shape\": [\n", + " 640\n", + " ],\n", + " \"dtype\": \"float64\",\n", + " \"min\": 0.00017333743386144625,\n", + " \"max\": 0.30611177795563155,\n", + " \"mean\": 0.02429792026086423\n", + " },\n", + " \"tx_out_p\": {\n", + " \"shape\": [\n", + " 640\n", + " ],\n", + " \"dtype\": \"float64\",\n", + " \"min\": 0.0,\n", + " \"max\": 0.30611177795563155,\n", + " \"mean\": 0.024192290332994165\n", + " },\n", + " \"ctle_out_p\": {\n", + " \"shape\": [\n", + " 640\n", + " ],\n", + " \"dtype\": \"float64\",\n", + " \"min\": -0.00012070148464843557,\n", + " \"max\": 0.30190060501479704,\n", + " \"mean\": 0.022639956720702535\n", + " },\n", + " \"dfe_out_p\": {\n", + " \"shape\": [\n", + " 640\n", + " ],\n", + " \"dtype\": \"float64\",\n", + " \"min\": -0.00012070148464843557,\n", + " \"max\": 0.30190060501479704,\n", + " \"mean\": 0.022149031377669475\n", + " },\n", + " \"chnl_H\": {\n", + " \"shape\": [\n", + " 4000\n", + " ],\n", + " \"dtype\": \"float64\",\n", + " \"min\": -82.3935418336317,\n", + " \"max\": -6.1220144682427255,\n", + " \"mean\": -45.07345440767763\n", + " },\n", + " \"tx_H\": {\n", + " \"shape\": [\n", + " 4000\n", + " ],\n", + " \"dtype\": \"float64\",\n", + " \"min\": -1.9286549331065743e-15,\n", + " \"max\": 1.928654933106574e-15,\n", + " \"mean\": -1.157192959863945e-16\n", + " },\n", + " \"ctle_H\": {\n", + " \"shape\": [\n", + " 4000\n", + " ],\n", + " \"dtype\": \"float64\",\n", + " \"min\": -17.423412098939465,\n", + " \"max\": 0.06180903013105719,\n", + " \"mean\": -4.642434741170688\n", + " },\n", + " \"dfe_H\": {\n", + " \"shape\": [\n", + " 4000\n", + " ],\n", + " \"dtype\": \"float64\",\n", + " \"min\": 0.0,\n", + " \"max\": 0.0,\n", + " \"mean\": 0.0\n", + " },\n", + " \"tx_out_H\": {\n", + " \"shape\": [\n", + " 4000\n", + " ],\n", + " \"dtype\": \"float64\",\n", + " \"min\": -87.98847956117056,\n", + " \"max\": -6.299729256098062,\n", + " \"mean\": -46.430720239464875\n", + " },\n", + " \"ctle_out_H\": {\n", + " \"shape\": [\n", + " 4000\n", + " ],\n", + " \"dtype\": \"float64\",\n", + " \"min\": -106.99120811131523,\n", + " \"max\": -6.850873488323125,\n", + " \"mean\": -48.51861701405144\n", + " },\n", + " \"dfe_out_H\": {\n", + " \"shape\": [\n", + " 4000\n", + " ],\n", + " \"dtype\": \"float64\",\n", + " \"min\": -86.65635505951953,\n", + " \"max\": -7.011396359213245,\n", + " \"mean\": -47.057792650509484\n", + " },\n", + " \"tx_out\": {\n", + " \"shape\": [],\n", + " \"dtype\": \"object\"\n", + " }\n", + "}\n" + ] + } + ], + "source": [ + "print(json.dumps(summary, indent=2))" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "28dfc477", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-30T21:12:02.862079Z", + "iopub.status.busy": "2026-08-30T21:12:02.862024Z", + "iopub.status.idle": "2026-08-30T21:12:02.863662Z", + "shell.execute_reply": "2026-08-30T21:12:02.863327Z", + "shell.execute_reply.started": "2026-08-30T21:12:02.862074Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " \"type\": \"Input\",\n", + " \"is_ami\": true,\n", + " \"zout\": null,\n", + " \"zin\": 50.000000000000014,\n", + " \"ccomp\": [\n", + " 1e-12,\n", + " 1e-14,\n", + " 5e-12\n", + " ],\n", + " \"slew\": null,\n", + " \"ami_files\": {\n", + " \"32-bit\": {\n", + " \"lin\": [\n", + " \"example_rx_x86.so\",\n", + " \"example_rx.ami\"\n", + " ],\n", + " \"win\": [\n", + " \"example_rx_x86.dll\",\n", + " \"example_rx.ami\"\n", + " ]\n", + " },\n", + " \"64-bit\": {\n", + " \"lin\": [\n", + " \"example_rx_x86_amd64.so\",\n", + " \"example_rx.ami\"\n", + " ],\n", + " \"win\": [\n", + " \"example_rx_x86_amd64.dll\",\n", + " \"example_rx.ami\"\n", + " ]\n", + " }\n", + " },\n", + " \"test_configs\": [],\n", + " \"summary\": \"Model Type:\\tInput\\nC_comp: \\t[1e-12, 1e-14, 5e-12]\\nCref: \\tNone\\nVref: \\tNone\\nVmeas: \\tNone\\nRref: \\tNone\\nTemperature Range:\\t[25.0, 0.0, 100.0]\\nVoltage Range: \\t[1.8, 1.62, 1.98]\\nAlgorithmic Model:\\n\\t32-bit:\\n\\t\\tLinux: ['example_rx_x86.so', 'example_rx.ami']\\n\\t\\tWindows: ['example_rx_x86.dll', 'example_rx.ami']\\n\\t64-bit:\\n\\t\\tLinux: ['example_rx_x86_amd64.so', 'example_rx.ami']\\n\\t\\tWindows: ['example_rx_x86_amd64.dll', 'example_rx.ami']\\n\"\n", + "}\n" + ] + } + ], + "source": [ + "print(json.dumps(model_info, indent=2))" + ] + }, + { + "cell_type": "markdown", + "id": "59b282f5", + "metadata": {}, + "source": [ + "### Bonus: plotting the channel impulse response\n", + "\n", + "`inspect_results_file` only reports summary statistics over MCP — that's a deliberate\n", + "design choice, so a tool result stays small even for arrays with hundreds of thousands of\n", + "samples. To actually plot a waveform, unpickle the saved file directly (this part talks to the\n", + "filesystem, not the MCP server)." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "edee955f", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-30T21:12:02.863894Z", + "iopub.status.busy": "2026-08-30T21:12:02.863839Z", + "iopub.status.idle": "2026-08-30T21:12:03.566979Z", + "shell.execute_reply": "2026-08-30T21:12:03.566500Z", + "shell.execute_reply.started": "2026-08-30T21:12:02.863889Z" + } + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjUAAAHGCAYAAACM+3qyAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjEsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvctoD+AAAAAlwSFlzAAAPYQAAD2EBqD+naQAAUn5JREFUeJzt3QeYU1X6+PF3emFm6L2DoCKigNKUXQsgFtQFsbtY17JW1F1x14JlUdG1/f25dl3bih0QEVCxI2CliNJBep3K9Pt/3pPckMxkZm4ySSbJfD/PE5LcnHtzc+Ym9+Wc95ybYFmWJQAAADEusaF3AAAAIBQIagAAQFwgqAEAAHGBoAYAAMQFghoAABAXCGoAAEBcIKgBAABxgaAGAADEBYIaIAgrVqyQRx55RHbt2hU39bd27VrzmbZu3SrxJF4/F4Dqkv0sAxq1ffv2yddffy3r1q0zz7t06SKHH364tG7d2lNm8eLFcsMNN8iIESOkZcuWEg+WLVtmPtOQIUOkXbt2NZbbvHmzTJs2Tf74xz9K//79JV4+VyjZdWRLTEyU5s2byxFHHCEHH3xwRPYBaIxoqQHc9IohDzzwgHTo0EGuuuoq+eKLL+Tbb7+Vf/zjH9KpUyc5++yzpaysrNHX15o1a0yQ8NlnnzX6uqirjt59910THK9cuVJeeukl6dOnj4wfP15KS0upOyAMaKkB3K644gp5/vnn5T//+Y9ccsklPvWiJ/Dzzz9fSkpKJCUlhTqDI3/605/k+uuv9zx/8MEH5eabb5ajjjrKZzmA0CCoAURk7ty58vTTT8utt95aLaBR2tWigU1qamq114qKiuSDDz6Q3bt3my6Oww47zOf17du3y2uvvWYeJyQkSHp6uvTq1Uv+8Ic/SHJysk+ezuzZs+WCCy6QjIyMWrcZSFnb3r175dNPP5UtW7ZImzZtTNdZs2bNQvL3994fraNZs2aZbrwTTjhB2rdvb8oUFBSY5bofxx9/vPTs2bPWbehnysvLMwHAIYcc4lNWu/++/PJLufLKKyUtLa1aXZ9yyilywAEH1LrPuj+ffPKJqQ/dx6FDh/p0MYaj3i688EIT1MybN69aUKOtgHqMrVq1yvxN9Zjr1q2bT5nKykpTZvXq1ZKVlSUDBw40x5L3Z3r22Wfl2GOPlUMPPVTmzJljWooOPPBAOeaYY8zxV5XW8ccff2w+n37+4447zqdL1Xub/fr1M2V1H/Xvp3VRdZt17WMgnxcImF6lG2jszjzzTL1avbVlyxZH5V9++WVTfvr06dbQoUOtCy64wBoxYoRZ9sADD/iU3bBhg3Xdddd5buedd57VunVrq1evXj7vF8g2AymrnnvuOSs7O9saOHCgdckll1hDhgyxmjZtas2aNctTZsaMGWb9b775ptbP/sUXX5hyDz/8cLX9efvtt63BgwdbEyZMsAYNGmRlZWVZCxcutJYuXWre+89//rN575SUFGv27Nl+P9Obb75pDRgwwHymkSNHWgkJCdbNN9/sU3bKlCmm7J49e3yWL1q0yLON2j7X/PnzzefXfbz88sutsWPHWj169LCef/75gOvNaR2p3bt3m+WjR4/2Wa511K1bN6tr166m7saMGWOlpqZad911l6eMHisHHXSQKXPRRReZ+jn44IOtK664wlNm48aNZvv333+/qTv9XOPHj7eaNGliHXfccVZ+fr7P+7733ntWs2bNrD59+pht9u/f35T973//W22bDz74oHXqqadaf/rTn6yzzz7bSktLs04++WSf7TnZR6efFwgGQQ1gWVb37t2tTp06Oa4L+wR87LHHWjt27PAs1xNkZmamtX379lrXz8vLsw488EDrnHPOCWqbgZSdN2+eCQxuu+02n33QAEuDjs2bN4csqNETp564VXl5uQkCNMgZN26ctXPnTrO8oqLCOvroo62+ffv6rVN9bdu2bZ7lTzzxhFk+bdq0kAU1Rx55pDVq1CifdQsLC63PPvss4HpzWkdKgyZdfs8993iW6d+qZcuWZn+Kioo8y999911P4Ko0sNNy3oFJZWWlT3BoByCdO3e2FixY4Fn+ww8/mKDhqquu8ixbvXq1lZ6eboKUsrIyz/YuvfRSKykpyfr+++99tqnfkcWLF3vWf/31183ymTNnepY52UennxcIBonCgIjs2bMnqC6Fc889V1q1auV5/uc//9l0Ry1cuLBak7yOqHruuefk0UcfNffaxP/VV18FvU2nZadOnWq6Te644w6fdSdPniyFhYWerrFQ0P3RUT4qKSlJzjjjDJNsPXLkSE+Xho4EGjdunCxdulR27txZbRuakK37653r1L17d5PrFMq/d3l5uVRUVHiWZWZmmi7BUNabdq/ocPKHHnrIdGtefvnlMmbMGJNEbNNjQacG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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import pickle\n", + "\n", + "import matplotlib.pyplot as plt\n", + "\n", + "with open(RESULTS_FILE, \"rb\") as f:\n", + " saved = pickle.load(f)\n", + "\n", + "chnl_h = saved.the_data.arrays[\"chnl_h\"]\n", + "plt.plot(chnl_h)\n", + "plt.title(\"Channel Impulse Response\")\n", + "plt.xlabel(\"Sample\")\n", + "plt.grid()\n", + "plt.show()\n", + "\n", + "Path(RESULTS_FILE).unlink(missing_ok=True)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/pyproject.toml b/pyproject.toml index 6d91c50..d40ea13 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "pipbert" description = "Serial communication link bit error rate tester simulator, written in Python." -version = "10.2.1" +version = "11.0.0" authors = [ {name = "David Banas", email = "capn.freako@gmail.com"} ] readme = "README.md" @@ -64,9 +64,15 @@ PyPi = "https://pypi.org/project/PipBERT/" [project.scripts] pybert = "pybert.cli:cli" +[project.optional-dependencies] +mcp = ["mcp>=2.0"] + [tool.setuptools.packages.find] where = ["src"] +[tool.setuptools.data-files] +examples = ["examples/mcp_client.py", "examples/mcp_notebook.ipynb"] + [tool.black] line-length = 119 diff --git a/src/pybert/cli.py b/src/pybert/cli.py index 9b0ca05..8f798a3 100644 --- a/src/pybert/cli.py +++ b/src/pybert/cli.py @@ -45,3 +45,13 @@ def sim(config_file, results): if not results: results = Path(config_file).with_suffix(".pybert_data") pybert.save_results(results) + + +@cli.command(context_settings={"help_option_names": ['-h', '--help']}) +def mcp(): + """Start the PyBERT MCP server over stdio.""" + try: + from pybert.mcp.server import build_server + build_server().run() + except ImportError as err: + raise click.ClickException("MCP support requires: pip install 'pipbert[mcp]'") from err diff --git a/src/pybert/mcp/__init__.py b/src/pybert/mcp/__init__.py new file mode 100644 index 0000000..abbfa88 --- /dev/null +++ b/src/pybert/mcp/__init__.py @@ -0,0 +1 @@ +"""MCP (Model Context Protocol) server for PyBERT.""" diff --git a/src/pybert/mcp/server.py b/src/pybert/mcp/server.py new file mode 100644 index 0000000..817d528 --- /dev/null +++ b/src/pybert/mcp/server.py @@ -0,0 +1,200 @@ +"""MCP tool implementations for PyBERT, and the MCP server that exposes them. + +Every tool constructs its own, fresh ``PyBERT(gui=False)`` instance. Nothing is cached or +shared across calls: ``PyBERT`` is a large, mutable ``HasTraits`` object, and simulations can +take a while, so cross-call state would be a correctness hazard for no benefit. +""" +import pickle +import tempfile +import time +from pathlib import Path +from typing import Any + +import numpy as np +import yaml + +from pybert import __version__ as VERSION +from pybert.configuration import PyBertCfg +from pybert.pybert import PyBERT +from pyibisami.ibis.file import IBISModel + +RESULT_TRAIT_NAMES = [ + "status", + "total_perf", + "jitter_perf", + "n_errs_dfe", + "n_errs_viterbi", + "chnl_dly", + "isi_dfe", + "dcd_dfe", + "pj_dfe", + "rj_dfe", + "perf_info", + "jitter_info", +] + + +def _jsonify(value: Any) -> Any: + """Recursively convert numpy/tuple values into plain JSON-serializable types.""" + if isinstance(value, np.generic): + return value.item() + if isinstance(value, np.ndarray): + return value.tolist() + if isinstance(value, dict): + return {k: _jsonify(v) for k, v in value.items()} + if isinstance(value, (list, tuple)): + return [_jsonify(v) for v in value] + return value + + +def _config_dict(pybert: PyBERT) -> dict: + cfg = PyBertCfg(pybert, time.asctime(), VERSION) + return _jsonify(vars(cfg)) + + +def _apply_config(pybert: PyBERT, config: dict) -> None: + """Apply a config dict (full or partial, as produced by `_config_dict`) onto `pybert`. + + Round-trips through a temp YAML file and `PyBERT.load_configuration()` so that + `PyBertCfg.load_from_file`'s existing special-casing (tap-tuple lists, legacy field + renames) is reused rather than reimplemented here. + + Raises: + ValueError: If `config` contains a key that isn't a real PyBERT config attribute. + """ + valid_keys = set(vars(PyBertCfg(pybert, time.asctime(), VERSION))) + unknown = set(config) - valid_keys + if unknown: + raise ValueError(f"Unknown configuration key(s): {sorted(unknown)}") + + cfg = PyBertCfg.__new__(PyBertCfg) # bypass __init__; it requires a live PyBERT to copy from + cfg.__dict__.update(config) + + with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as tmp: + yaml.dump(cfg, tmp, indent=4, sort_keys=False) + tmp_path = Path(tmp.name) + try: + pybert.load_configuration(tmp_path) + finally: + tmp_path.unlink(missing_ok=True) + + +def get_default_config() -> dict: + """Return PyBERT's default configuration, as a JSON-serializable dict.""" + return _config_dict(PyBERT(run_simulation=False, gui=False)) + + +def get_config(config_file: str) -> dict: + """Load a saved configuration file and return its contents as a dict.""" + pybert = PyBERT(run_simulation=False, gui=False) + pybert.load_configuration(Path(config_file)) + return _config_dict(pybert) + + +def set_config(overrides: dict, out_file: str, base_config_file: str | None = None) -> dict: + """Apply ``overrides`` on top of a base configuration and save the result. + + Args: + overrides: Trait name/value pairs to change. + out_file: Where to save the resulting configuration (``.yaml``). + base_config_file: An existing config file to start from; PyBERT's defaults if omitted. + """ + pybert = PyBERT(run_simulation=False, gui=False) + if base_config_file: + pybert.load_configuration(Path(base_config_file)) + + _apply_config(pybert, overrides) + pybert.save_configuration(Path(out_file)) + return {"saved": out_file, "config": _config_dict(pybert)} + + +def run_simulation(config: dict, results_file: str | None = None) -> dict: + """Run a headless PyBERT simulation and return its summary performance metrics. + + Args: + config: A configuration dict, as returned by ``get_config``/``get_default_config``, + optionally with overrides applied. + results_file: If given, also save the waveform results to this path (``.pybert_data``). + """ + pybert = PyBERT(run_simulation=False, gui=False) + _apply_config(pybert, config) + # update_plots populates pybert.plotdata, which save_results() needs; skip it otherwise. + pybert.simulate(initial_run=True, update_plots=bool(results_file)) + + if results_file: + pybert.save_results(Path(results_file)) + + return {name: _jsonify(getattr(pybert, name)) for name in RESULT_TRAIT_NAMES} + + +def inspect_results_file(results_file: str) -> dict: + """Report the waveform arrays stored in a saved ``.pybert_data`` file. + + Note: these files only ever hold waveform arrays (impulse/step/frequency responses, etc.), + never scalar performance metrics (BER, jitter, eye height/width) — those are only available + live, right after a simulation runs, via ``run_simulation``'s return value. + """ + with open(results_file, "rb") as f: + results = pickle.load(f) + + summary = {} + for name, arr in results.the_data.arrays.items(): + info: dict[str, Any] = {"shape": list(arr.shape), "dtype": str(arr.dtype)} + if arr.dtype != object and arr.size: + info.update(min=float(arr.min()), max=float(arr.max()), mean=float(arr.mean())) + summary[name] = info + return summary + + +def list_ibis_models(ibis_file: str) -> dict: + """List the components and models defined in an IBIS file.""" + model = IBISModel(ibis_file, debug=False, gui=False) + return { + "components": list(model.model_dict["components"].keys()), + "models": list(model.model_dict["models"].keys()), + "parsing_errors": model.ibis_parsing_errors, + } + + +def _safe_attr(obj: Any, name: str) -> Any: + """`getattr`, tolerating attributes that only exist for some model types (e.g. `zout` is + driver-only, `zin` is receiver-only, on `pyibisami`'s `Model` class).""" + try: + return getattr(obj, name) + except AttributeError: + return None + + +def inspect_ibis_model(ibis_file: str, model_name: str) -> dict: + """Report the parameters of a single model from an IBIS file.""" + ibis_model = IBISModel(ibis_file, debug=False, gui=False) + model = ibis_model.model_dict["models"][model_name] + return { + "type": model.mtype, + "is_ami": model.is_ami, + "zout": _jsonify(_safe_attr(model, "zout")), + "zin": _jsonify(_safe_attr(model, "zin")), + "ccomp": _jsonify(_safe_attr(model, "ccomp")), + "slew": _jsonify(_safe_attr(model, "slew")), + "ami_files": model.ami_files, + "test_configs": list(model.test_configs.keys()) if model.is_ami else [], + "summary": str(model), + } + + +def build_server(): + """Construct the MCP server and register all PyBERT tools on it.""" + from mcp.server import MCPServer + + server = MCPServer("pybert", version=VERSION) + for tool in ( + get_default_config, + get_config, + set_config, + run_simulation, + inspect_results_file, + list_ibis_models, + inspect_ibis_model, + ): + server.add_tool(tool) + return server diff --git a/tests/test_mcp.py b/tests/test_mcp.py new file mode 100644 index 0000000..3a6bb81 --- /dev/null +++ b/tests/test_mcp.py @@ -0,0 +1,103 @@ +"""Unit test coverage for the `pybert.mcp` tool implementations.""" + +import asyncio +import math + +import pytest + +from pybert.mcp.server import ( + get_config, + get_default_config, + inspect_ibis_model, + inspect_results_file, + list_ibis_models, + run_simulation, + set_config, +) + +IBIS_FILE = "models/ibisami/example_rx.ibs" + + +def _fast_config() -> dict: + """A default config, shrunk down so `run_simulation` is fast in tests.""" + cfg = get_default_config() + cfg["nbits"] = 1000 + cfg["eye_bits"] = 500 + return cfg + + +def test_run_simulation_returns_metrics(): + """`run_simulation` reports a finite `total_perf` and the expected metric keys.""" + result = run_simulation(_fast_config()) + assert result["status"] + assert math.isfinite(result["total_perf"]) + assert math.isfinite(result["jitter_perf"]) + + +def test_run_simulation_saves_results(tmp_path): + """`results_file`, when given, is written and can be inspected afterward.""" + results_file = tmp_path / "results.pybert_data" + run_simulation(_fast_config(), results_file=str(results_file)) + assert results_file.exists() + + summary = inspect_results_file(str(results_file)) + assert summary["chnl_h"]["shape"][0] > 0 + assert math.isfinite(summary["chnl_h"]["mean"]) + + +def test_get_set_config_round_trip(tmp_path): + """Overrides applied by `set_config` are visible via `get_config`.""" + config_file = tmp_path / "config.yaml" + saved = set_config({"bit_rate": 25, "nbits": 2000}, str(config_file)) + assert saved["config"]["bit_rate"] == 25 + + reloaded = get_config(str(config_file)) + assert reloaded["bit_rate"] == 25 + assert reloaded["nbits"] == 2000 + + +def test_set_config_rejects_unknown_key(tmp_path): + """An override key that isn't a real PyBERT config attribute raises `ValueError`.""" + with pytest.raises(ValueError, match="not_a_real_key"): + set_config({"not_a_real_key": 1}, str(tmp_path / "config.yaml")) + + +def test_run_simulation_rejects_unknown_key(): + """A typo in `config` (e.g. from an LLM caller) fails loudly rather than being ignored.""" + config = _fast_config() + config["bit_rat"] = 25 # typo of "bit_rate" + with pytest.raises(ValueError, match="bit_rat"): + run_simulation(config) + + +def test_list_ibis_models(): + """`list_ibis_models` reports the components/models parsed out of an IBIS file.""" + info = list_ibis_models(IBIS_FILE) + assert "example_rx" in info["models"] + assert info["parsing_errors"] == "Success!" + + +def test_inspect_ibis_model(): + """`inspect_ibis_model` reports AMI file references for an AMI-enabled model.""" + info = inspect_ibis_model(IBIS_FILE, "example_rx") + assert info["is_ami"] is True + assert info["ami_files"]["64-bit"]["lin"] + + +def test_build_server_registers_all_tools(): + """The MCP server exposes every tool function.""" + pytest.importorskip("mcp") + from pybert.mcp.server import build_server + + server = build_server() + tools = asyncio.run(server.list_tools()) + tool_names = {t.name for t in tools} + assert { + "get_default_config", + "get_config", + "set_config", + "run_simulation", + "inspect_results_file", + "list_ibis_models", + "inspect_ibis_model", + } <= tool_names diff --git a/uv.lock b/uv.lock index a49c010..d9607aa 100644 --- a/uv.lock +++ b/uv.lock @@ -162,6 +162,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/96/78/5fe6dc3a3a5b2f5a2a4faef8bfe336d5fa049a38884ab3172e0098160c01/alembic-1.18.5-py3-none-any.whl", hash = "sha256:06d8ba9d04558022f5395e9317de03d270f3dced49cee01f89fe7a13c26f14bc", size = 264664, upload-time = "2026-06-25T15:20:56.673Z" }, ] +[[package]] +name = "annotated-types" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, +] + [[package]] name = "anyio" version = "4.14.2" @@ -759,6 +768,50 @@ toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] +[[package]] +name = "cryptography" +version = "50.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/5c/59086b4aac5e879d38ddbcf74e4be7ade89cebc3eb199a55da998c3bb46a/cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03", size = 4001252, upload-time = "2026-07-31T14:23:33.331Z" }, + { url = "https://files.pythonhosted.org/packages/57/ef/8f2df13c7216bcad3e1c74e07f6e193d93e998e114f524a53877c9af27ad/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645", size = 4719554, upload-time = "2026-07-31T14:23:35.611Z" }, + { url = "https://files.pythonhosted.org/packages/d9/41/029086c34d91052fc3b88bcc8056f709a7c915c7a23b235a54eb800b1c97/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7", size = 4702130, upload-time = "2026-07-31T14:23:37.635Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ff/b6ce0954962e7f7b969f850a883744197bb3910bdfd7b6da162eab7d9f68/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3", size = 4725244, upload-time = "2026-07-31T14:23:39.471Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/63a1027cb7fec360a182208e1b7767d5aa1fe57be3d6aa856e69a321edc0/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f", size = 5342265, upload-time = "2026-07-31T14:23:41.286Z" }, + { url = "https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae", size = 4734609, upload-time = "2026-07-31T14:23:43.139Z" }, + { url = "https://files.pythonhosted.org/packages/15/37/36a9c479bbe49acea2636c7fd3360d20f7b7e079c300352011c44850b181/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a", size = 4356517, upload-time = "2026-07-31T14:23:44.939Z" }, + { url = "https://files.pythonhosted.org/packages/32/98/8a151d64367204cbc63ec65d37502f1d9c53cf4bfc6ec3c532614dbec60d/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987", size = 4724529, upload-time = "2026-07-31T14:23:46.93Z" }, + { url = "https://files.pythonhosted.org/packages/22/f6/ec13b470172126464a86bf54d2294a46d29837fc51ba3e45d4047946fb5e/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169", size = 5299852, upload-time = "2026-07-31T14:23:48.851Z" }, + { url = "https://files.pythonhosted.org/packages/da/3a/f05e32c99d440c9bb891ea0e36c9091891e36be5a9a87ab2ee6ea20729f6/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f", size = 4734462, upload-time = "2026-07-31T14:23:50.861Z" }, + { url = "https://files.pythonhosted.org/packages/ca/dc/bd72b26be8953f80625f63151efd38eee71c76ca6cf591c08ff34615a79e/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105", size = 4852708, upload-time = "2026-07-31T14:23:52.715Z" }, + { url = "https://files.pythonhosted.org/packages/27/20/c930314a2ab476d15dec966ec87e2e9637bb02b06106b12c0396c57bb603/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef", size = 5004179, upload-time = "2026-07-31T14:23:54.887Z" }, + { url = "https://files.pythonhosted.org/packages/32/2e/c9db68a0c4bfa28e310707527c0ee3a2bd254104d2e02e68f368e197aa4c/cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30", size = 3840395, upload-time = "2026-07-31T14:23:56.677Z" }, + { url = "https://files.pythonhosted.org/packages/03/37/73d005be173aff344af30e9fd2a576575cb2391a7101d9cd3842e1fa8cce/cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07", size = 4036009, upload-time = "2026-07-31T14:24:24.122Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c6/7a6202a534e32103a285b7834a120869557fe198d51d7cfe59754c8bda9c/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3", size = 4745252, upload-time = "2026-07-31T14:24:26.118Z" }, + { url = "https://files.pythonhosted.org/packages/85/4f/0fa8c2f4428198f15d9ff8d63400e27afbf94ce833f6108da1eb3753f945/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f", size = 4728939, upload-time = "2026-07-31T14:24:27.994Z" }, + { url = "https://files.pythonhosted.org/packages/d1/63/54dd723490ba2dc09b299682c10b38db38f159728bcaae8c591b8af2f22d/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5", size = 4748483, upload-time = "2026-07-31T14:24:30.254Z" }, + { url = "https://files.pythonhosted.org/packages/1d/dd/7c77d26285cc7f6991efce64a0f5b4f9383bfa5dd8c5033003eaf7db4cdb/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f", size = 5367599, upload-time = "2026-07-31T14:24:32.457Z" }, + { url = "https://files.pythonhosted.org/packages/46/c9/f60aed34c013f317f92817b6c171c2d22a78270fa41109bd4b08af26b194/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025", size = 4762647, upload-time = "2026-07-31T14:24:34.599Z" }, + { url = "https://files.pythonhosted.org/packages/be/f3/f9a0173b139372c3a48ed98154b45cc6b9de17c789d5ab552e621c293609/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a", size = 4385197, upload-time = "2026-07-31T14:24:36.647Z" }, + { url = "https://files.pythonhosted.org/packages/d8/36/83bb81f6e569bc38e1e4a7bc80f29b46bb9601920bc455fc8e888f5d5742/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b", size = 4748095, upload-time = "2026-07-31T14:24:39.493Z" }, + { url = "https://files.pythonhosted.org/packages/6b/16/d3008eff98c764979865834c3d386d4fd041b5f52e7f34fc29ac1a5eb515/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708", size = 5325948, upload-time = "2026-07-31T14:24:41.556Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f8/d97f9603efda3888187bfdb893f26c41be4735c10631d05d284ee6b047c4/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47", size = 4762400, upload-time = "2026-07-31T14:24:43.636Z" }, + { url = "https://files.pythonhosted.org/packages/64/a2/4615c8f7d81a00b1d6e6afe19f694e1543582349fb5f4076f6cb5dc36485/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9", size = 4878208, upload-time = "2026-07-31T14:24:45.522Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/57/30/4a22984d4f1bdfb8c054f07a92bc176b97a3134cc1d6c4b3bffb1f3688b4/cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba", size = 3874135, upload-time = "2026-07-31T14:24:50.085Z" }, + { url = "https://files.pythonhosted.org/packages/9d/3e/e54cde8c01631a5a8226ccd617eab9e57fd5cfdad90f1a9e6bb570794631/cryptography-50.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5e34edd123674534acd70147f0ca331eaa2c74e6325fb2028c886aa26ba0b68c", size = 3963170, upload-time = "2026-07-31T14:24:51.968Z" }, + { url = "https://files.pythonhosted.org/packages/01/b6/0b9e125e90f3d2dcf599a218a899cda7326a3158cfa258723f0b398b08f6/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8eb5e1172eb569ea8a872796576e6a67c276351728b6455d5beb01242b027c6a", size = 4692441, upload-time = "2026-07-31T14:24:53.743Z" }, + { url = "https://files.pythonhosted.org/packages/53/c9/a5151588710785a96d7bc4de27d4cd62f263bbbcb203cfe29df537eb6505/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:910d11e1a385c654bf738bf3e6b8e6ed5de0f5610fcae2be9e5b398d8081d20e", size = 4699810, upload-time = "2026-07-31T14:24:55.746Z" }, + { url = "https://files.pythonhosted.org/packages/c7/1a/15b92b25eb6ce3089cd49377ae990a0f3ad485a510f968aed1f19dbdcdf2/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:62598a8a57f815db4c6259a4e97d857dab56697e7de8e8ab02352ab74da1995d", size = 4691924, upload-time = "2026-07-31T14:24:58.082Z" }, + { url = "https://files.pythonhosted.org/packages/62/15/219075012ab13e8905f3cd572204f4acb4b111df787104346b9bc0cea789/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:07479a1cb08219ab719147e742e76090c9c773321959bb94946fffdd397a6437", size = 4699593, upload-time = "2026-07-31T14:24:59.951Z" }, + { url = "https://files.pythonhosted.org/packages/8e/b5/c2c5fce26f0ee40d21bafe7f191d29a34b35a65ac4fe8a1191d1983612e9/cryptography-50.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c99c003e088647b8a5b7c145d6f78c335f6348332b62e142d411c4b63d1460b9", size = 3813796, upload-time = "2026-07-31T14:25:02.298Z" }, +] + [[package]] name = "cycler" version = "0.12.1" @@ -1143,6 +1196,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, ] +[[package]] +name = "httpcore2" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11", marker = "python_full_version < '3.11' or sys_platform != 'emscripten'" }, + { name = "truststore", marker = "python_full_version < '3.11' or sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/ad/f4f0e57345f1870f3e8cb624e058d7eca6e5a27d33bcc3311d9b618734cd/httpcore2-2.12.0.tar.gz", hash = "sha256:9293522bba0aa7c4c8e9e3f040c16575bd8868e155a77fa30c7a9085a5eae648", size = 67548, upload-time = "2026-08-18T13:22:08.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/74/d370e55600d9bcfa0d9794b0166126d49291a3d2b20c268fc98c453a4948/httpcore2-2.12.0-py3-none-any.whl", hash = "sha256:7e04258ce01013d7d615e5b910a3b27fac937d7a95038227e79652b4ba3b4ceb", size = 83074, upload-time = "2026-08-18T13:22:05.854Z" }, +] + [[package]] name = "httpx" version = "0.28.1" @@ -1158,6 +1224,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] +[[package]] +name = "httpx2" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "sys_platform != 'emscripten'" }, + { name = "httpcore2", marker = "sys_platform != 'emscripten'" }, + { name = "httpx2-jsfetch", marker = "python_full_version >= '3.12' and sys_platform == 'emscripten'" }, + { name = "idna" }, + { name = "truststore", marker = "sys_platform != 'emscripten'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/f8/579a8b51e42e38ee32647df9f08aa25643ae788e275cc625b199829c4671/httpx2-2.12.0.tar.gz", hash = "sha256:7631fe9887a8a2275f4a2540e053aa670fcc50742864a9ae7c66e609fdcf12cf", size = 100040, upload-time = "2026-08-18T13:22:09.086Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/95/411ba65569158e862368917aaf56597f3e5fa3b91b0502919638465a08f3/httpx2-2.12.0-py3-none-any.whl", hash = "sha256:cc8b6eecb8661c146b8f89a60e97456ee086e91a784ed31ac450c3a9e613dd36", size = 95427, upload-time = "2026-08-18T13:22:06.834Z" }, +] + +[[package]] +name = "httpx2-jsfetch" +version = "1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/c4/0e5636363151a2a1795e0a77617168b9ca438e1748ec05fc9b5687f93d64/httpx2_jsfetch-1.0.tar.gz", hash = "sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60", size = 6872, upload-time = "2026-08-07T00:13:07.492Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/43/832f631d32e4f1211caa2ba368317739fe71f0b8530e4c9d15dc454bac2a/httpx2_jsfetch-1.0-py3-none-any.whl", hash = "sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32", size = 6382, upload-time = "2026-08-07T00:13:06.567Z" }, +] + [[package]] name = "idna" version = "3.18" @@ -2101,6 +2193,44 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl", hash = "sha256:3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6", size = 9534, upload-time = "2026-05-08T17:33:32.055Z" }, ] +[[package]] +name = "mcp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx2" }, + { name = "jsonschema" }, + { name = "mcp-types" }, + { name = "opentelemetry-api" }, + { name = "pydantic" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/33/32d4dff2c95bb5d897c3ef4c83649a08996b17b58f0a326d2495d4c81179/mcp-2.0.0.tar.gz", hash = "sha256:0f440e735c13ece8bb19bc62cf0b86f4313448432fbb77d35e14034f4e050728", size = 1662284, upload-time = "2026-07-28T13:45:32.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/72/7d7897418912c1d12e87556630dfb7bf0eac71160e9bef8b447960804ee3/mcp-2.0.0-py3-none-any.whl", hash = "sha256:1cb4c75d2d2c7b8c1d756355e5d82a39f2822cc7f13e22a2051d7ca3592349d6", size = 349980, upload-time = "2026-07-28T13:45:28.853Z" }, +] + +[[package]] +name = "mcp-types" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/56/9b8e1c152f61f6c6b07c4b5896c88c7d0ae90bac6ee6306f852fcc5c1eb0/mcp_types-2.0.0.tar.gz", hash = "sha256:d7d939b9285c9961ae8866ba75ef85da34d12bafe276efbf4eb6a131786d8379", size = 66632, upload-time = "2026-07-28T13:45:33.804Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/4c/c78d78c3d52b0ac594ad7cc8ef5972adfe070e3597a8a4c6ce0cd39196ea/mcp_types-2.0.0-py3-none-any.whl", hash = "sha256:6b2de797ca2797f568b79529e1b25948e34de511bcc0bd82fef1039a6d1b8eb0", size = 69649, upload-time = "2026-07-28T13:45:30.713Z" }, +] + [[package]] name = "mistune" version = "0.8.4" @@ -2562,6 +2692,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b1/a4/9376b38b7387a0296b1f626b966e5503578625c9673777db1b45bf70acb0/numpy_typing_compat-20260602.2.5-py3-none-any.whl", hash = "sha256:21ba7757c8924d359a9ed3ab2163c282a70983ae64498fdba6d1892a6641c8b1", size = 5881, upload-time = "2026-06-02T15:52:34.167Z" }, ] +[[package]] +name = "opentelemetry-api" +version = "1.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/8b/aa9e2d8b8dfa7c946f7dec5d1f8f6ba8eca062f43509a06bdb5ce93d26c0/opentelemetry_api-1.44.0.tar.gz", hash = "sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a", size = 72406, upload-time = "2026-07-16T15:25:32.678Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/6f/a04e900f465ff3221ccc395522503e2d10e79fa21f2723c8e177aae1e0d1/opentelemetry_api-1.44.0-py3-none-any.whl", hash = "sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef", size = 60018, upload-time = "2026-07-16T15:25:11.657Z" }, +] + [[package]] name = "optuna" version = "4.9.0" @@ -2902,7 +3044,7 @@ wheels = [ [[package]] name = "pipbert" -version = "10.2.1" +version = "11.0.0" source = { editable = "." } dependencies = [ { name = "chaco" }, @@ -2930,6 +3072,11 @@ dependencies = [ { name = "xlrd" }, ] +[package.optional-dependencies] +mcp = [ + { name = "mcp" }, +] + [package.dev-dependencies] dev = [ { name = "m2r2" }, @@ -2955,6 +3102,7 @@ requires-dist = [ { name = "click", specifier = "==8.4.2" }, { name = "enable", specifier = "==6.1.0" }, { name = "kiwisolver", specifier = "==1.5.0" }, + { name = "mcp", marker = "extra == 'mcp'", specifier = ">=2.0" }, { name = "numpy", specifier = ">=1.26" }, { name = "optuna", specifier = "==4.9.0" }, { name = "pandas", specifier = ">=2.0" }, @@ -2970,6 +3118,7 @@ requires-dist = [ { name = "traitsui", specifier = "==8.0.0" }, { name = "xlrd", specifier = "==2.0.2" }, ] +provides-extras = ["mcp"] [package.metadata.requires-dev] dev = [ @@ -3189,6 +3338,107 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] +[[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/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 = "pyface" version = "8.0.0" @@ -3282,6 +3532,23 @@ dev = [ { name = "typing-extensions", specifier = ">=4.15.0" }, ] +[[package]] +name = "pyjwt" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + [[package]] name = "pyparsing" version = "3.3.2" @@ -3405,6 +3672,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/be/0631a861af4d1c875f096c07d34e9a63639560a717130e7a87cbc82b7e3f/python_json_logger-4.1.0-py3-none-any.whl", hash = "sha256:132994765cf75bf44554be9aa49b06ef2345d23661a96720262716438141b6b2", size = 15021, upload-time = "2026-03-29T04:39:55.266Z" }, ] +[[package]] +name = "python-multipart" +version = "0.0.32" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" }, +] + [[package]] name = "pytz" version = "2026.2" @@ -3414,6 +3690,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl", hash = "sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126", size = 510141, upload-time = "2026-05-04T01:35:27.408Z" }, ] +[[package]] +name = "pywin32" +version = "312" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/1b/9cfdeac80ee45bebbbcb31f1b7b99a0d81a1c72de48d837be984e0e88b1d/pywin32-312-cp310-cp310-win32.whl", hash = "sha256:772235332b5d1024c696f11cea1ae4be7930f0a8b894bb43db14e3f435f1ff7e", size = 6361387, upload-time = "2026-06-04T07:49:14.329Z" }, + { url = "https://files.pythonhosted.org/packages/33/b1/7afc96d041d982c27bc2df6f853d43f01fd273e3d39d04be3647ddeb533d/pywin32-312-cp310-cp310-win_amd64.whl", hash = "sha256:5dbc35d2b5320dc07f25fa31269cfb767471002b17de5eb067d03da68c7cb2db", size = 6926780, upload-time = "2026-06-04T07:49:16.881Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/4140da9ad54108e517f4a16b2d83da3033e08662144623e1239587cb7db6/pywin32-312-cp310-cp310-win_arm64.whl", hash = "sha256:3020656e34f1cf7faeb7bccd2b84653a607c6ff0c55ada85e6487d61716deabd", size = 4307203, upload-time = "2026-06-04T07:49:18.993Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f5/10a6e845a00fc5e7afd0a988b744f403d4d57162a28d160a093c4d9322f0/pywin32-312-cp311-cp311-win32.whl", hash = "sha256:17948aeadbdb091f0ced6ef0841620794e68327b94ee415571c1203594b7215c", size = 6362659, upload-time = "2026-06-04T07:49:21.349Z" }, + { url = "https://files.pythonhosted.org/packages/35/c4/dcd2d62b5944b6d5db53413a5899016ccd57ffcb7278f3f81655d25d2027/pywin32-312-cp311-cp311-win_amd64.whl", hash = "sha256:d11417d84412f859b722fad0841b3614459ed0047f7542d8362e77884f6b6e8a", size = 6928825, upload-time = "2026-06-04T07:49:23.934Z" }, + { url = "https://files.pythonhosted.org/packages/b7/56/3cbb433fe4501cdba2eb9040f56a4e1a8243faa4186b25295564d1a7a79d/pywin32-312-cp311-cp311-win_arm64.whl", hash = "sha256:b2200a054ca6d6625c4842fc56a4976a4b47f96b73dbe5538c3f813a80359f47", size = 6721875, upload-time = "2026-06-04T07:49:26.416Z" }, + { url = "https://files.pythonhosted.org/packages/83/ff/32aa7d2ed0ab12b323aaa64f9b75e6ad4f8fd09f9ccfc28c79414d46838d/pywin32-312-cp312-cp312-win32.whl", hash = "sha256:dab4f65ac9c4e48400a2a0530c46c3c579cd5905ecd11b80692373915269208b", size = 6371877, upload-time = "2026-06-04T07:49:28.836Z" }, + { url = "https://files.pythonhosted.org/packages/03/d9/77040d3b43df3f3be32ea289433d660d2727f5ba327bc73be835127d9d60/pywin32-312-cp312-cp312-win_amd64.whl", hash = "sha256:b457f6d628a47e8a7346ce22acb7e1a46a4a78b52e1d17e1af56871bd19a93bc", size = 6914841, upload-time = "2026-06-04T07:49:31.85Z" }, + { url = "https://files.pythonhosted.org/packages/e3/cc/7b1ec671775756020a0ee7f4feeaf3c568f0ab86bd3900088cf986937a92/pywin32-312-cp312-cp312-win_arm64.whl", hash = "sha256:6017c58e12f6809fbb0555b75df144c2922a9ffd18e4b9b5afa863b6c1a9d950", size = 6727901, upload-time = "2026-06-04T07:49:34.244Z" }, + { url = "https://files.pythonhosted.org/packages/2d/41/12fbfd7f36ed2146d8bc9de96c2741296bf0d490b98508496cff322e274c/pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c", size = 6370184, upload-time = "2026-06-04T07:49:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/ba/db/36a78e3403099d31d9746d13fdcde5accc43c1155f375a34d15983a479a7/pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9", size = 6914298, upload-time = "2026-06-04T07:49:38.876Z" }, + { url = "https://files.pythonhosted.org/packages/84/37/c1697194092b76de9ed47ca124323f02c57ffc8a45c06f88a3d5acaf01eb/pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831", size = 6727640, upload-time = "2026-06-04T07:49:41.083Z" }, +] + [[package]] name = "pywinpty" version = "3.0.5" @@ -4307,6 +4602,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e2/22/dbf013a12ec759e54a34a119e9e217435b3f71b2dd5c61a7ade0a25dae87/sqlalchemy-2.0.51-py3-none-any.whl", hash = "sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5", size = 1944334, upload-time = "2026-06-15T16:09:22.418Z" }, ] +[[package]] +name = "sse-starlette" +version = "3.4.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/00/b42a44342a054d58cb1115d7c8aa9cb4290dd9442f9c1b91a4b8173dba22/sse_starlette-3.4.8.tar.gz", hash = "sha256:ed89ffbb75cbf78a5fe2f2109cd584792ee7f9dfac96f791db546df8f15f3f9c", size = 32548, upload-time = "2026-08-05T11:19:49.982Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/3a/764912c58293d95b6dcdf4cc255f9d10de310580ced547b082eb9d72018c/sse_starlette-3.4.8-py3-none-any.whl", hash = "sha256:6e82314c786709a3cd9520f2285cf9fff90e181e598e8a357b0cf80f66afba0d", size = 16516, upload-time = "2026-08-05T11:19:48.748Z" }, +] + [[package]] name = "stack-data" version = "0.6.3" @@ -4321,6 +4629,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, ] +[[package]] +name = "starlette" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/b4/205b0d5241d934e8add0c38aa924c4f9fb7330834ff11e5444db964ec3f9/starlette-1.6.0.tar.gz", hash = "sha256:d4e3ac5e546444960c710297a3c9fc3f7ebae1b7e963f3d36173b49da535be9b", size = 2716969, upload-time = "2026-08-08T18:27:57.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/cb/6a6a47d5b464bd08695d254f3da6e7986cc70c9fa5d778eda57538edfe56/starlette-1.6.0-py3-none-any.whl", hash = "sha256:a86dd39d14bb45f85a3d18525215a9ef0cfd1f192ac793220e72598c90335f0c", size = 75969, upload-time = "2026-08-08T18:27:56.196Z" }, +] + [[package]] name = "tenacity" version = "9.1.4" @@ -4475,6 +4796,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6e/c7/c96fcb966c1c61fbf2b58219cfa394d2c7160f31c9f4728402ead8f9a17c/traitsui-8.0.0-py3-none-any.whl", hash = "sha256:0e09c0965a8de12dd05e0b4bbdae31bdd576fa551fdaaf2f4e13ac4aa51980ee", size = 1524098, upload-time = "2023-05-22T15:07:19.473Z" }, ] +[[package]] +name = "truststore" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, +] + [[package]] name = "types-pyyaml" version = "6.0.12.20260518" @@ -4502,6 +4832,18 @@ 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.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" }, +] + [[package]] name = "tzdata" version = "2026.3" @@ -4529,6 +4871,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] +[[package]] +name = "uvicorn" +version = "0.52.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", marker = "python_full_version < '3.11' or sys_platform != 'emscripten'" }, + { name = "h11", marker = "python_full_version < '3.11' or sys_platform != 'emscripten'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/0f/3f86e61397dd33bf2ccf28188c40db6a740658aeebbbf6e7dbc101a1f487/uvicorn-0.52.4.tar.gz", hash = "sha256:73acfee47a0b133c5de13d219492d62d8a31e935f4fe6e41a232451a15379f86", size = 100627, upload-time = "2026-08-19T06:27:41.821Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/79/4a20b54ab0491485ccd8c077db2d39187c7f12b3e15485d38a7be37c81b4/uvicorn-0.52.4-py3-none-any.whl", hash = "sha256:f86e41a149d7d05a9969337e3946a9c171c06a5d42680896daaba624aeac8da1", size = 79871, upload-time = "2026-08-19T06:27:40.36Z" }, +] + [[package]] name = "wcwidth" version = "0.8.2"