diff --git a/app/__main__.py b/app/__main__.py index 0eff2370..afa6b7b2 100644 --- a/app/__main__.py +++ b/app/__main__.py @@ -11,6 +11,11 @@ from app.utils.paths import APP_PATH from quantflow import __version__ +try: + from app.api.mcp import create_mcp as _create_mcp +except ImportError: + _create_mcp = None # type: ignore[assignment] + from .api.cointegration import cointegration_router from .api.deps import instrument_app from .api.heston import heston_router @@ -64,6 +69,8 @@ async def api_redoc() -> HTMLResponse: api.include_router(volatility_router) app.include_router(api) app.include_router(status_router, include_in_schema=False) + if _create_mcp is not None: + app.mount("/mcp", _create_mcp().streamable_http_app()) examples_dir = APP_PATH / "examples" if examples_dir.is_dir(): app.mount( diff --git a/quantflow/ai/tools/crypto.py b/app/api/mcp.py similarity index 79% rename from quantflow/ai/tools/crypto.py rename to app/api/mcp.py index a7662d2e..8854a222 100644 --- a/quantflow/ai/tools/crypto.py +++ b/app/api/mcp.py @@ -1,4 +1,4 @@ -"""Crypto tools for the quantflow MCP server.""" +"""MCP server for quantflow - crypto volatility tools served via HTTP.""" from pathlib import Path @@ -6,10 +6,9 @@ from quantflow.data.deribit import Deribit, InstrumentKind -from .base import McpTool - -def register(mcp: FastMCP, tool: McpTool) -> None: +def create_mcp() -> FastMCP: + mcp = FastMCP("quantflow") @mcp.tool() async def crypto_instruments(currency: str, kind: str = "spot") -> str: @@ -88,18 +87,4 @@ async def vol_surface_snapshot(currency: str, path: str) -> str: Path(path).write_text(inputs.model_dump_json(indent=2)) return f"Saved {len(vs.maturities)} maturities to {path}" - @mcp.tool() - async def crypto_prices(symbol: str, frequency: str = "") -> str: - """Get OHLC price history for a cryptocurrency via FMP. - - Args: - symbol: Cryptocurrency symbol e.g. BTCUSD - frequency: Data frequency - 1min, 5min, 15min, 30min, 1hour, 4hour, - or empty for daily - """ - async with tool.fmp() as client: - df = await client.prices(symbol, frequency=frequency) - if df.empty: - return f"No price data for {symbol}" - df = df[["date", "open", "high", "low", "close", "volume"]].sort_values("date") - return df.tail(50).to_csv(index=False) + return mcp diff --git a/app/api/sampling.py b/app/api/sampling.py index f8050579..18add3e9 100644 --- a/app/api/sampling.py +++ b/app/api/sampling.py @@ -1,9 +1,11 @@ import numpy as np from fastapi import APIRouter, Query from pydantic import BaseModel, Field +from scipy.stats import chisquare, ks_1samp from quantflow.sp.ou import Vasicek from quantflow.sp.poisson import PoissonProcess +from quantflow.ta.paths import Paths from quantflow.utils import bins from quantflow.utils.distributions import DoubleExponential @@ -16,6 +18,18 @@ class SamplingResponse(BaseModel): analytical: list[float] = Field(description="Analytical PDF values") +class GaussianSamplingResponse(SamplingResponse): + ks_statistic: float = Field( + description="Kolmogorov-Smirnov statistic vs analytical CDF" + ) + ks_pvalue: float = Field(description="Kolmogorov-Smirnov p-value") + + +class PoissonSamplingResponse(SamplingResponse): + chi2_statistic: float = Field(description="Chi-squared goodness-of-fit statistic") + chi2_pvalue: float = Field(description="Chi-squared p-value") + + class DoubleExponentialResponse(SamplingResponse): char_x: list[float] = Field(description="X values from characteristic function") char_y: list[float] = Field(description="Y values from characteristic function") @@ -25,28 +39,59 @@ class DoubleExponentialResponse(SamplingResponse): async def gaussian_sampling( kappa: float = Query(1.0, description="Mean reversion speed", ge=0.1, le=5.0), samples: int = Query(1000, description="Number of sample paths", ge=100, le=10000), -) -> SamplingResponse: + antithetic: bool = Query( + True, description="Use antithetic variates variance reduction" + ), +) -> GaussianSamplingResponse: pr = Vasicek(rate=0.5, kappa=kappa) - paths = pr.sample(samples, 1, 1000) + draws = Paths.normal_draws(samples, 1, 1000, antithetic_variates=antithetic) + paths = pr.sample_from_draws(draws) pdf = paths.pdf(num_bins=50) x = [float(v) for v in pdf.index] simulation = [float(v) for v in pdf["pdf"]] analytical = [float(v) for v in np.atleast_1d(pr.marginal(1).pdf(pdf.index))] - return SamplingResponse(x=x, simulation=simulation, analytical=analytical) + final_values = paths.data[-1, :] + ks = ks_1samp(final_values, lambda v: pr.analytical_cdf(1, v)) + return GaussianSamplingResponse( + x=x, + simulation=simulation, + analytical=analytical, + ks_statistic=float(ks.statistic), + ks_pvalue=float(ks.pvalue), + ) @sampling_router.get("/poisson-sampling") async def poisson_sampling( - intensity: float = Query(2.0, description="Poisson intensity", ge=2.0, le=5.0), + intensity: float = Query(2.0, description="Poisson intensity", ge=2.0, le=20.0), samples: int = Query(1000, description="Number of sample paths", ge=100, le=10000), -) -> SamplingResponse: +) -> PoissonSamplingResponse: pr = PoissonProcess(intensity=intensity) paths = pr.sample(samples, 1, 1000) pdf = paths.pdf(delta=1) x = [float(v) for v in pdf.index] simulation = [float(v) for v in pdf["pdf"]] analytical = [float(v) for v in np.atleast_1d(pr.marginal(1).pdf(pdf.index))] - return SamplingResponse(x=x, simulation=simulation, analytical=analytical) + f_obs = np.array(simulation) * samples + f_exp = np.array(analytical) * samples + f_exp = f_exp * (f_obs.sum() / f_exp.sum()) + # merge tail bins with expected count < 5 to satisfy chi-squared requirements + while len(f_exp) > 1 and f_exp[0] < 5: + f_obs[1] += f_obs[0] + f_exp[1] += f_exp[0] + f_obs, f_exp = f_obs[1:], f_exp[1:] + while len(f_exp) > 1 and f_exp[-1] < 5: + f_obs[-2] += f_obs[-1] + f_exp[-2] += f_exp[-1] + f_obs, f_exp = f_obs[:-1], f_exp[:-1] + chi2 = chisquare(f_obs, f_exp) + return PoissonSamplingResponse( + x=x, + simulation=simulation, + analytical=analytical, + chi2_statistic=float(chi2.statistic), + chi2_pvalue=float(chi2.pvalue), + ) @sampling_router.get("/double-exponential-sampling") diff --git a/dev/quantflow.dockerfile b/dev/quantflow.dockerfile index fbc3d49b..1392bd65 100644 --- a/dev/quantflow.dockerfile +++ b/dev/quantflow.dockerfile @@ -16,7 +16,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ COPY pyproject.toml uv.lock readme.md ./ # Install dependencies (no root package, with needed extras) -RUN uv sync --frozen --no-install-project --extra ai --extra docs --extra data +RUN uv sync --frozen --no-install-project --extra docs --extra data # Copy source, generate example outputs and images, then build docs COPY mkdocs.yml ./ diff --git a/docs/index.md b/docs/index.md index b0f9fca8..65df7c52 100644 --- a/docs/index.md +++ b/docs/index.md @@ -18,7 +18,6 @@ pip install quantflow ### Optional dependencies * `data` — data retrieval: `pip install quantflow[data]` -* `ai` — MCP server for AI clients: `pip install quantflow[ai,data]` * `ml` — training the Deep Implied Volatility model: `pip install quantflow[ml]` ## Features @@ -33,8 +32,6 @@ pip install quantflow * **Time Series Analysis**: exponentially weighted moving averages (EWMA), Kalman filtering, super-smoothers, and OHLC bar utilities. -* **AI Integration**: an [MCP server](https://quantflow.quantmind.com/mcp/) that exposes quantflow's data tools to AI assistants. - * **JSON Serializable**: all models and pricers are built on [Pydantic](https://docs.pydantic.dev), making them fully serializable to and from JSON. ## Citation diff --git a/docs/mcp.md b/docs/mcp.md index d72b694d..d3dd25f5 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1,28 +1,19 @@ # MCP Server -Quantflow exposes its data tools as an [MCP](https://modelcontextprotocol.io) server, allowing AI clients such as Claude to query market data, crypto volatility surfaces, and economic indicators directly. +Quantflow exposes its crypto volatility tools as an [MCP](https://modelcontextprotocol.io) server, allowing AI clients such as Claude to query live Deribit data directly. -Install with the `ai` and `data` extras: - -```bash -pip install quantflow[ai,data] -``` - -## API keys - -Store your API keys in `~/.quantflow/.vault`: +The server is hosted at: ``` -fmp=your-fmp-key -fred=your-fred-key +https://quantflow.quantmind.com/mcp ``` -Or let the AI manage them for you via the `vault_add` tool once connected. +It uses the [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) transport. No API key is required — all tools use the public Deribit API. ## Claude Code ```bash -claude mcp add quantflow -- uv run qf-mcp +claude mcp add --transport http quantflow https://quantflow.quantmind.com/mcp ``` ## Claude Desktop @@ -33,8 +24,8 @@ Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_ { "mcpServers": { "quantflow": { - "command": "uv", - "args": ["run", "qf-mcp"] + "type": "streamable-http", + "url": "https://quantflow.quantmind.com/mcp" } } } @@ -44,20 +35,8 @@ Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_ | Tool | Description | |---|---| -| `vault_keys` | List stored API keys | -| `vault_add` | Add or update an API key | -| `vault_delete` | Delete an API key | -| `stock_indices` | List stock market indices | -| `stock_search` | Search companies by name or symbol | -| `stock_profile` | Get company profile | -| `stock_prices` | Get OHLC price history | -| `sector_performance` | Sector performance and PE ratios | -| `crypto_instruments` | List Deribit instruments | -| `crypto_historical_volatility` | Historical volatility from Deribit | -| `crypto_term_structure` | Volatility term structure | -| `crypto_implied_volatility` | Implied volatility surface | -| `crypto_prices` | Crypto OHLC price history | -| `ascii_chart` | ASCII chart for any stock or crypto symbol | -| `fred_subcategories` | Browse FRED categories | -| `fred_series` | List series in a FRED category | -| `fred_data` | Fetch FRED observations | +| `crypto_instruments` | List instruments for a currency on Deribit (spot, future, option) | +| `crypto_historical_volatility` | Historical volatility index from Deribit | +| `crypto_term_structure` | Volatility term structure across maturities | +| `crypto_implied_volatility` | Implied volatility surface (all maturities or a single one) | +| `vol_surface_snapshot` | Fetch a live vol surface and write it as a JSON file | diff --git a/frontend/src/sampling.md b/frontend/src/sampling.md index 680b0537..6fb6aed5 100644 --- a/frontend/src/sampling.md +++ b/frontend/src/sampling.md @@ -13,28 +13,31 @@ import * as Plot from "npm:@observablehq/plot"; ## Gaussian (Vasicek) -Sample the Gaussian OU (Vasicek) process for different mean reversion speeds and number of paths. +Sample the Gaussian OU (Vasicek) process for different mean reversion speeds ${tex`\kappa`} and number of paths. The process has unit volatility ${tex`\sigma = 1`} and initial value ${tex`x_0 = 0.5`}. ```js -const gKappaInput = Inputs.range([0.1, 5], {step: 0.1, value: 1.0, label: "Kappa (mean reversion)"}); +const gKappaInput = Inputs.range([0.1, 5], {step: 0.1, value: 1.0, label: "kappa"}); const gKappa = Generators.input(gKappaInput); const gSamplesInput = Inputs.range([100, 10000], {step: 100, value: 1000, label: "Samples"}); const gSamples = Generators.input(gSamplesInput); + +const gAntitheticInput = Inputs.toggle({label: "Antithetic variates", value: true}); +const gAntithetic = Generators.input(gAntitheticInput); ``` ```js -display(html`
${gKappaInput}${gSamplesInput}
`); +display(html`
${gKappaInput}${gSamplesInput}${gAntitheticInput}
`); ``` ```js await new Promise(r => setTimeout(r, 300)); -const gaussianData = await fetchJson(`/.api/gaussian-sampling?kappa=${gKappa}&samples=${gSamples}`); +const gaussianData = await fetchJson(`/.api/gaussian-sampling?kappa=${gKappa}&samples=${gSamples}&antithetic=${gAntithetic}`); ``` ```js -const gBarData = gaussianData.x.map((x, i) => ({x, y: gaussianData.simulation[i]})); -const gLineData = gaussianData.x.map((x, i) => ({x, y: gaussianData.analytical[i]})); +const gBarData = gaussianData.x.map((x, i) => ({x, y: gaussianData.simulation[i], series: "Simulation"})); +const gLineData = gaussianData.x.map((x, i) => ({x, y: gaussianData.analytical[i], series: "Analytical"})); const gBinWidth = gaussianData.x[1] - gaussianData.x[0]; display(Plot.plot({ @@ -45,11 +48,20 @@ display(Plot.plot({ style: {background: "transparent"}, x: {label: "Value"}, y: {label: "Density"}, + color: {legend: true, domain: ["Simulation", "Analytical"], range: ["var(--theme-foreground-muted)", "var(--theme-foreground-focus)"]}, marks: [ - Plot.rectY(gBarData, {x1: d => d.x - gBinWidth / 2, x2: d => d.x + gBinWidth / 2, y: "y", fill: "var(--theme-foreground-muted)", fillOpacity: 0.6, tip: true}), - Plot.line(gLineData, {x: "x", y: "y", stroke: "var(--theme-foreground-focus)", strokeWidth: 2}), + Plot.rectY(gBarData, {x1: d => d.x - gBinWidth / 2, x2: d => d.x + gBinWidth / 2, y: "y", fill: "series", fillOpacity: 0.6, inset: 1, tip: true}), + Plot.line(gLineData, {x: "x", y: "y", stroke: "series", strokeWidth: 2}), ] })); + +if (gaussianData.ks_statistic != null) { + const ksColor = gaussianData.ks_pvalue < 0.05 ? "var(--theme-red)" : "var(--theme-green)"; + display(html`

+ KS statistic: ${gaussianData.ks_statistic.toFixed(4)}  |  + p-value: ${gaussianData.ks_pvalue.toFixed(4)} +

`); +} ``` ## Poisson @@ -57,7 +69,7 @@ display(Plot.plot({ Evaluate the Monte Carlo simulation for the Poisson process against the analytical PDF. ```js -const pIntensityInput = Inputs.range([2, 5], {step: 0.1, value: 2.0, label: "Intensity (λ)"}); +const pIntensityInput = Inputs.range([2, 20], {step: 0.1, value: 2.0, label: "Intensity (λ)"}); const pIntensity = Generators.input(pIntensityInput); const pSamplesInput = Inputs.range([100, 10000], {step: 100, value: 1000, label: "Samples"}); @@ -76,7 +88,6 @@ const poissonData = await fetchJson(`/.api/poisson-sampling?intensity=${pIntensi ```js const pBarSim = poissonData.x.map((x, i) => ({x, y: poissonData.simulation[i], type: "Simulation"})); const pBarAna = poissonData.x.map((x, i) => ({x, y: poissonData.analytical[i], type: "Analytical"})); -const pBinWidth = poissonData.x.length > 1 ? poissonData.x[1] - poissonData.x[0] : 1; display(Plot.plot({ width: 800, @@ -86,12 +97,20 @@ display(Plot.plot({ style: {background: "transparent"}, x: {label: "Value"}, y: {label: "Probability"}, - color: {legend: true, label: "Source"}, + color: {legend: true, domain: ["Simulation", "Analytical"], range: ["var(--theme-foreground-muted)", "var(--theme-foreground-focus)"]}, marks: [ - Plot.barY(pBarSim, {x: "x", y: "y", fill: "var(--theme-foreground-muted)", fillOpacity: 0.6, dx: -2, tip: true}), - Plot.barY(pBarAna, {x: "x", y: "y", fill: "var(--theme-foreground-focus)", fillOpacity: 0.6, dx: 2}), + Plot.rectY(pBarSim, {x1: d => d.x - 0.3, x2: d => d.x - 0.05, y: "y", fill: "type", fillOpacity: 0.6, tip: true}), + Plot.rectY(pBarAna, {x1: d => d.x + 0.05, x2: d => d.x + 0.3, y: "y", fill: "type", fillOpacity: 0.6, tip: true}), ] })); + +if (poissonData.chi2_statistic != null) { + const chi2Color = poissonData.chi2_pvalue < 0.05 ? "var(--theme-red)" : "var(--theme-green)"; + display(html`

+ χ² statistic: ${poissonData.chi2_statistic.toFixed(4)}  |  + p-value: ${poissonData.chi2_pvalue.toFixed(4)} +

`); +} ``` ## Double Exponential @@ -116,9 +135,9 @@ const deData = await fetchJson(`/.api/double-exponential-sampling?log_kappa=${de ``` ```js -const deBarData = deData.x.map((x, i) => ({x, y: deData.simulation[i]})); -const deLineData = deData.x.map((x, i) => ({x, y: deData.analytical[i]})); -const deCharData = deData.char_x.map((x, i) => ({x, y: deData.char_y[i]})); +const deBarData = deData.x.map((x, i) => ({x, y: deData.simulation[i], series: "Simulation"})); +const deLineData = deData.x.map((x, i) => ({x, y: deData.analytical[i], series: "Analytical"})); +const deCharData = deData.char_x.map((x, i) => ({x, y: deData.char_y[i], series: "Char. function"})); const deBinWidth = deData.x[1] - deData.x[0]; display(Plot.plot({ @@ -129,10 +148,11 @@ display(Plot.plot({ style: {background: "transparent"}, x: {label: "Value"}, y: {label: "Density"}, + color: {legend: true, domain: ["Simulation", "Analytical", "Char. function"], range: ["var(--theme-foreground-muted)", "var(--theme-foreground-focus)", "var(--theme-accent)"]}, marks: [ - Plot.rectY(deBarData, {x1: d => d.x - deBinWidth / 2, x2: d => d.x + deBinWidth / 2, y: "y", fill: "var(--theme-foreground-muted)", fillOpacity: 0.6, tip: true}), - Plot.line(deLineData, {x: "x", y: "y", stroke: "var(--theme-foreground-focus)", strokeWidth: 2}), - Plot.dot(deCharData, {x: "x", y: "y", fill: "var(--theme-accent)", r: 3}), + Plot.rectY(deBarData, {x1: d => d.x - deBinWidth / 2, x2: d => d.x + deBinWidth / 2, y: "y", fill: "series", fillOpacity: 0.6, inset: 1, tip: true}), + Plot.line(deLineData, {x: "x", y: "y", stroke: "series", strokeWidth: 2}), + Plot.dot(deCharData, {x: "x", y: "y", fill: "series", r: 3}), ] })); ``` diff --git a/mkdocs.yml b/mkdocs.yml index 1a64957c..2bf80493 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -131,7 +131,7 @@ nav: - Inversion: theory/inversion.md - Lévy Process: theory/levy.md - Option Pricing: theory/option_pricing.md - - Live Examples: https://quantflow.quantmind.com/examples/ + - Live Examples: https://quantflow.quantmind.com/examples - MCP Server: mcp.md - Glossary: glossary.md - Contributing: contributing.md diff --git a/pyproject.toml b/pyproject.toml index eef7e76e..834725a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,6 @@ dependencies = [ "pandas>=2.0.0", "pydantic>=2.0.2", "ccy>=2.0.0", - "python-dotenv>=1.0.1", "statsmodels>=0.14.6,<0.15.0", ] @@ -47,15 +46,6 @@ Documentation = "https://quantflow.quantmind.com" Issues = "https://github.com/quantmind/quantflow/issues" [project.optional-dependencies] -ai = [ - "asciichartpy>=1.5.25", - "ccy[holidays]>=2.0.0", - "google-genai>=1.61.0", - "mcp>=1.26.0", - "openai>=2.16.0", - "pydantic-ai-slim>=1.51.0", - "rich>=13.9.4", -] book = [ "altair>=6.0.0", "autodocsumm>=0.2.14", @@ -70,33 +60,34 @@ data = [ dev = [ "black>=26.3.1", "ghp-import>=2.0.2", + "hypothesis>=6.152.2", "isort>=8.0.0", "mypy>=1.14.1", "pytest-asyncio>=1.0.0", "pytest-cov>=7.0.0", + "python-dotenv>=1.0.1", "ruff>=0.15.4", "types-python-dateutil>=2.9.0.20251115", ] docs = [ "aio-fluid[http,log,k8s]>=2.2.6", - "fastapi>=0.129.0", + "fastapi>=0.136.3", "griffe-pydantic>=1.1.0", "griffe-typingdoc>=0.2.7", "kaleido>=1.2.0", + "mcp>=1.26.0", "mkdocs-macros-plugin>=1.3.7", "mkdocs-material>=9.7.0", "mkdocs-redirects>=1.2.1", "mkdocstrings[python]==1.0.0", "plotly>=6.2.0", "pyarrow>=19.0.0", + "python-dotenv>=1.0.1", ] ml = [ "torch>=2.10.0", ] -[project.scripts] -qf-mcp = "quantflow.ai.server:main" - [build-system] requires = [ "hatchling", @@ -190,8 +181,3 @@ module = [ ] ignore_missing_imports = true disallow_untyped_defs = false - -[dependency-groups] -dev = [ - "hypothesis>=6.152.2", -] diff --git a/quantflow/ai/__init__.py b/quantflow/ai/__init__.py deleted file mode 100644 index b633b2b2..00000000 --- a/quantflow/ai/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""AI module for quantflow - MCP server exposing quantflow data tools.""" diff --git a/quantflow/ai/server.py b/quantflow/ai/server.py deleted file mode 100644 index c985392f..00000000 --- a/quantflow/ai/server.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Quantflow MCP server.""" - -from mcp.server.fastmcp import FastMCP - -from quantflow.ai.tools import charts, crypto, fred, stocks, vault - -from .tools.base import McpTool - - -def create_server() -> FastMCP: - mcp = FastMCP("quantflow") - tool = McpTool() - vault.register(mcp, tool) - crypto.register(mcp, tool) - stocks.register(mcp, tool) - fred.register(mcp, tool) - charts.register(mcp, tool) - return mcp - - -def main() -> None: - server = create_server() - server.run() - - -if __name__ == "__main__": - main() diff --git a/quantflow/ai/tools/__init__.py b/quantflow/ai/tools/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/quantflow/ai/tools/base.py b/quantflow/ai/tools/base.py deleted file mode 100644 index e58b8a9f..00000000 --- a/quantflow/ai/tools/base.py +++ /dev/null @@ -1,33 +0,0 @@ -from dataclasses import dataclass, field -from pathlib import Path - -from mcp.server.fastmcp.exceptions import ToolError - -from quantflow.data.fmp import FMP -from quantflow.data.fred import Fred -from quantflow.data.vault import Vault - -VAULT_PATH = Path.home() / ".quantflow" / ".vault" - - -@dataclass -class McpTool: - vault: Vault = field(default_factory=lambda: Vault(VAULT_PATH)) - - def fmp(self) -> FMP: - key = self.vault.get("fmp") - if not key: - raise ToolError( - "FMP API key not found in vault. " - " Please add it using the vault_add tool." - ) - return FMP(key=key) - - def fred(self) -> Fred: - key = self.vault.get("fred") - if not key: - raise ToolError( - "FRED API key not found in vault. " - " Please add it using the vault_add tool." - ) - return Fred(key=key) diff --git a/quantflow/ai/tools/charts.py b/quantflow/ai/tools/charts.py deleted file mode 100644 index b475228c..00000000 --- a/quantflow/ai/tools/charts.py +++ /dev/null @@ -1,40 +0,0 @@ -"""Chart tools for the quantflow MCP server.""" - -from mcp.server.fastmcp import FastMCP - -from .base import McpTool - - -def register(mcp: FastMCP, tool: McpTool) -> None: - - @mcp.tool() - async def ascii_chart(symbol: str, frequency: str = "", height: int = 20) -> str: - """Plot an ASCII candlestick chart for a stock or cryptocurrency. - - Args: - symbol: Ticker symbol e.g. AAPL, BTCUSD, ETHUSD - frequency: Data frequency - 1min, 5min, 15min, 30min, 1hour, 4hour, - or empty for daily - height: Chart height in terminal rows (default: 20) - """ - import asciichartpy as ac - - async with tool.fmp() as client: - df = await client.prices(symbol, frequency=frequency) - if df.empty: - return f"No price data for {symbol}" - - df = df.sort_values("date").tail(50) - prices = df["close"].tolist() - first_date = df["date"].iloc[0] - last_date = df["date"].iloc[-1] - low = min(prices) - high = max(prices) - last = prices[-1] - - chart = ac.plot(prices, {"height": height, "format": "{:8,.0f}"}) - return ( - f"{symbol} Close Price ({first_date} → {last_date})\n" - f"High: {high:,.2f} Low: {low:,.2f} Last: {last:,.2f}\n\n" - f"{chart}" - ) diff --git a/quantflow/ai/tools/fred.py b/quantflow/ai/tools/fred.py deleted file mode 100644 index dc363efc..00000000 --- a/quantflow/ai/tools/fred.py +++ /dev/null @@ -1,82 +0,0 @@ -"""FRED tools for the quantflow MCP server.""" - -from mcp.server.fastmcp import FastMCP - -from .base import McpTool - - -def register(mcp: FastMCP, tool: McpTool) -> None: - - @mcp.tool() - async def fred_subcategories(category_id: str | None = None) -> str: - """List FRED categories. Omit category_id to get top-level categories. - - Args: - category_id: FRED category ID (optional, defaults to root) - """ - from fluid.utils.data import compact_dict - - async with tool.fred() as client: - data = await client.subcategories( - params=compact_dict(category_id=category_id) - ) - cats = data.get("categories", []) - if not cats: - return "No categories found" - import pandas as pd - - df = pd.DataFrame(cats, columns=["id", "name"]) - return df.to_csv(index=False) - - @mcp.tool() - async def fred_series(category_id: str) -> str: - """List data series available in a FRED category. - - Args: - category_id: FRED category ID - """ - from fluid.utils.data import compact_dict - - async with tool.fred() as client: - data = await client.series(params=compact_dict(category_id=category_id)) - series = data.get("seriess", []) - if not series: - return f"No series found for category {category_id}" - import pandas as pd - - df = pd.DataFrame( - series, - columns=[ - "id", - "popularity", - "title", - "frequency", - "observation_start", - "observation_end", - ], - ).sort_values("popularity", ascending=False) - return df.to_csv(index=False) - - @mcp.tool() - async def fred_data( - series_id: str, - length: int = 100, - frequency: str = "d", - ) -> str: - """Fetch observations for a FRED data series. - - Args: - series_id: FRED series ID e.g. GDP, UNRATE, DGS10 - length: Number of data points to return (default: 100) - frequency: Frequency - d, w, bw, m, q, sa, a (default: d for daily) - """ - async with tool.fred() as client: - df = await client.serie_data( - params=dict( - series_id=series_id, - limit=length, - frequency=frequency, - sort_order="desc", - ) - ) - return df.to_csv(index=False) diff --git a/quantflow/ai/tools/stocks.py b/quantflow/ai/tools/stocks.py deleted file mode 100644 index bc9cd595..00000000 --- a/quantflow/ai/tools/stocks.py +++ /dev/null @@ -1,99 +0,0 @@ -"""Stocks tools for the quantflow MCP server.""" - -from datetime import timedelta -from typing import cast - -import pandas as pd -from ccy import period as to_period -from ccy.tradingcentres import prevbizday -from fluid.utils.data import compact_dict -from mcp.server.fastmcp import FastMCP - -from quantflow.utils.dates import utcnow - -from .base import McpTool - - -def register(mcp: FastMCP, tool: McpTool) -> None: - - @mcp.tool() - async def stock_indices() -> str: - """List available stock market indices.""" - async with tool.fmp() as client: - data = await client.indices() - return pd.DataFrame(data).to_csv(index=False) - - @mcp.tool() - async def stock_search(query: str) -> str: - """Search for stocks by company name or symbol. - - Args: - query: Company name or ticker symbol to search for - """ - async with tool.fmp() as client: - data = await client.search(query) - - df = pd.DataFrame(data, columns=["symbol", "name", "currency", "stockExchange"]) - return df.to_csv(index=False) - - @mcp.tool() - async def stock_profile(symbol: str) -> str: - """Get company profile for a stock symbol. - - Args: - symbol: Stock ticker symbol e.g. AAPL, MSFT - """ - async with tool.fmp() as client: - data = await client.profile(symbol) - if not data: - return f"No profile found for {symbol}" - d = dict(data[0]) - description = d.pop("description", "") or "" - lines = "\n".join(f"{k}: {v}" for k, v in d.items()) - return f"{description}\n\n{lines}".strip() - - @mcp.tool() - async def stock_prices(symbol: str, frequency: str = "") -> str: - """Get OHLC price history for a stock. - - Args: - symbol: Stock ticker symbol e.g. AAPL, MSFT - frequency: Data frequency - 1min, 5min, 15min, 30min, 1hour, 4hour, - or empty for daily - """ - async with tool.fmp() as client: - df = await client.prices(symbol, frequency=frequency) - if df.empty: - return f"No price data for {symbol}" - df = df[["date", "open", "high", "low", "close", "volume"]].sort_values("date") - return df.to_csv(index=False) - - @mcp.tool() - async def sector_performance(period: str = "1d") -> str: - """Get sector performance and PE ratios. - - Args: - period: Time period - 1d, 1w, 1m, 3m, 6m, 1y (default: 1d) - """ - async with tool.fmp() as client: - to_date = utcnow().date() - if period != "1d": - from_date = to_date - timedelta(days=to_period(period).totaldays) - sp = await client.sector_performance( - from_date=prevbizday(from_date, 0), - to_date=prevbizday(to_date, 0), - summary=True, - ) - else: - sp = await client.sector_performance() - pe = await client.sector_pe( - params=compact_dict(date=prevbizday(to_date, 0).isoformat()) - ) - spd = cast(dict, sp) - pes = {k["sector"]: round(float(k["pe"]), 3) for k in pe if k["sector"] in spd} - rows = [ - {"sector": k, "performance": float(v), "pe": pes.get(k, float("nan"))} - for k, v in spd.items() - ] - df = pd.DataFrame(rows).sort_values("performance", ascending=False) - return df.to_csv(index=False) diff --git a/quantflow/ai/tools/vault.py b/quantflow/ai/tools/vault.py deleted file mode 100644 index bf359a1c..00000000 --- a/quantflow/ai/tools/vault.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Vault tools for the quantflow MCP server.""" - -from mcp.server.fastmcp import FastMCP - -from .base import McpTool - - -def register(mcp: FastMCP, tool: McpTool) -> None: - - @mcp.tool() - def vault_keys() -> list[str]: - """List all API keys stored in the vault.""" - return tool.vault.keys() - - @mcp.tool() - def vault_add(key: str, value: str) -> str: - """Add or update an API key in the vault. - - Args: - key: Key name e.g. fmp, fred - value: API key value - """ - tool.vault.add(key, value) - return f"Key '{key}' saved to vault" - - @mcp.tool() - def vault_delete(key: str) -> str: - """Delete an API key from the vault. - - Args: - key: Key name to delete - """ - if tool.vault.delete(key): - return f"Key '{key}' deleted from vault" - return f"Key '{key}' not found in vault" diff --git a/quantflow_tests/test_ai.py b/quantflow_tests/test_ai.py index 9de7bfec..ab1d09c8 100644 --- a/quantflow_tests/test_ai.py +++ b/quantflow_tests/test_ai.py @@ -1,4 +1,4 @@ -"""Unit tests for the quantflow MCP server tools.""" +"""Tests for the quantflow MCP crypto tools.""" from __future__ import annotations @@ -10,478 +10,112 @@ import pytest from mcp.server.fastmcp import FastMCP -from quantflow.ai import server as ai_server -from quantflow.ai.tools import charts, crypto, fred, stocks, vault -from quantflow.ai.tools.base import McpTool -from quantflow.data.vault import Vault - -# --------------------------------------------------------------------------- -# Helpers -# --------------------------------------------------------------------------- +from app.api.mcp import create_mcp def text(result: Any) -> str: - """Extract text from a call_tool result (tuple of (blocks, metadata)).""" + """Extract text from a call_tool result.""" blocks = result[0] if isinstance(result, tuple) else result if blocks and hasattr(blocks[0], "text"): return blocks[0].text return str(result) -def raw(result: Any) -> Any: - """Get the raw return value from call_tool result.""" - if isinstance(result, tuple) and len(result) > 1: - return result[1].get("result") - return result - - -# --------------------------------------------------------------------------- -# Fixtures -# --------------------------------------------------------------------------- - - -@pytest.fixture -def vault_path(tmp_path: Path) -> Path: - return tmp_path / ".vault" - - -@pytest.fixture -def mcp_tool(vault_path: Path) -> McpTool: - return McpTool(vault=Vault(vault_path)) - - -@pytest.fixture -def mock_fmp() -> AsyncMock: - mock = AsyncMock() - mock.__aenter__ = AsyncMock(return_value=mock) - mock.__aexit__ = AsyncMock(return_value=False) - return mock - - -@pytest.fixture -def mock_fred() -> AsyncMock: - mock = AsyncMock() - mock.__aenter__ = AsyncMock(return_value=mock) - mock.__aexit__ = AsyncMock(return_value=False) - return mock - - -@pytest.fixture -def vault_server(mcp_tool: McpTool) -> FastMCP: - mcp = FastMCP("test-vault") - vault.register(mcp, mcp_tool) - return mcp - - -@pytest.fixture -def stocks_server(mcp_tool: McpTool) -> FastMCP: - mcp = FastMCP("test-stocks") - stocks.register(mcp, mcp_tool) - return mcp - - -@pytest.fixture -def crypto_server(mcp_tool: McpTool) -> FastMCP: - mcp = FastMCP("test-crypto") - crypto.register(mcp, mcp_tool) - return mcp - - -@pytest.fixture -def fred_server(mcp_tool: McpTool) -> FastMCP: - mcp = FastMCP("test-fred") - fred.register(mcp, mcp_tool) - return mcp - - @pytest.fixture -def charts_server(mcp_tool: McpTool) -> FastMCP: - mcp = FastMCP("test-charts") - charts.register(mcp, mcp_tool) - return mcp - - -# --------------------------------------------------------------------------- -# Vault tools -# --------------------------------------------------------------------------- - - -async def test_vault_keys_empty(vault_server: FastMCP) -> None: - result = await vault_server.call_tool("vault_keys", {}) - assert raw(result) == [] - - -async def test_vault_add(vault_server: FastMCP, mcp_tool: McpTool) -> None: - result = await vault_server.call_tool("vault_add", {"key": "fmp", "value": "abc"}) - assert "fmp" in text(result) - assert mcp_tool.vault.get("fmp") == "abc" - - -async def test_vault_keys_after_add(vault_server: FastMCP) -> None: - await vault_server.call_tool("vault_add", {"key": "fred", "value": "xyz"}) - result = await vault_server.call_tool("vault_keys", {}) - assert "fred" in raw(result) - - -async def test_vault_delete_existing(vault_server: FastMCP) -> None: - await vault_server.call_tool("vault_add", {"key": "fmp", "value": "abc"}) - result = await vault_server.call_tool("vault_delete", {"key": "fmp"}) - assert "deleted" in text(result) - - -async def test_vault_delete_missing(vault_server: FastMCP) -> None: - result = await vault_server.call_tool("vault_delete", {"key": "nope"}) - assert "not found" in text(result) - - -# --------------------------------------------------------------------------- -# Stock tools -# --------------------------------------------------------------------------- - - -async def test_stock_indices( - stocks_server: FastMCP, mcp_tool: McpTool, mock_fmp: AsyncMock -) -> None: - mcp_tool.vault.add("fmp", "test-key") - mock_fmp.indices.return_value = [ - {"symbol": "^GSPC", "name": "S&P 500"}, - {"symbol": "^IXIC", "name": "NASDAQ Composite"}, - ] - with patch("quantflow.ai.tools.base.FMP", return_value=mock_fmp): - result = await stocks_server.call_tool("stock_indices", {}) - assert "^GSPC" in text(result) - assert "S&P 500" in text(result) - - -async def test_stock_search( - stocks_server: FastMCP, mcp_tool: McpTool, mock_fmp: AsyncMock -) -> None: - mcp_tool.vault.add("fmp", "test-key") - mock_fmp.search.return_value = [ - { - "symbol": "AAPL", - "name": "Apple Inc.", - "currency": "USD", - "stockExchange": "NASDAQ", - }, - ] - with patch("quantflow.ai.tools.base.FMP", return_value=mock_fmp): - result = await stocks_server.call_tool("stock_search", {"query": "Apple"}) - assert "AAPL" in text(result) - - -async def test_stock_profile_found( - stocks_server: FastMCP, mcp_tool: McpTool, mock_fmp: AsyncMock -) -> None: - mcp_tool.vault.add("fmp", "test-key") - mock_fmp.profile.return_value = [ - { - "symbol": "AAPL", - "companyName": "Apple Inc.", - "description": "Tech company.", - "price": 200.0, - } - ] - with patch("quantflow.ai.tools.base.FMP", return_value=mock_fmp): - result = await stocks_server.call_tool("stock_profile", {"symbol": "AAPL"}) - assert "Tech company" in text(result) - assert "AAPL" in text(result) - - -async def test_stock_profile_not_found( - stocks_server: FastMCP, mcp_tool: McpTool, mock_fmp: AsyncMock -) -> None: - mcp_tool.vault.add("fmp", "test-key") - mock_fmp.profile.return_value = [] - with patch("quantflow.ai.tools.base.FMP", return_value=mock_fmp): - result = await stocks_server.call_tool("stock_profile", {"symbol": "FAKE"}) - assert "No profile" in text(result) - - -async def test_stock_prices( - stocks_server: FastMCP, mcp_tool: McpTool, mock_fmp: AsyncMock -) -> None: - mcp_tool.vault.add("fmp", "test-key") - mock_fmp.prices.return_value = pd.DataFrame( - [ - { - "date": "2025-01-01", - "open": 100, - "high": 110, - "low": 90, - "close": 105, - "volume": 1000, - } - ] - ) - with patch("quantflow.ai.tools.base.FMP", return_value=mock_fmp): - result = await stocks_server.call_tool("stock_prices", {"symbol": "AAPL"}) - assert "2025-01-01" in text(result) +def mcp_server() -> FastMCP: + return create_mcp() -async def test_stock_prices_empty( - stocks_server: FastMCP, mcp_tool: McpTool, mock_fmp: AsyncMock -) -> None: - mcp_tool.vault.add("fmp", "test-key") - mock_fmp.prices.return_value = pd.DataFrame() - with patch("quantflow.ai.tools.base.FMP", return_value=mock_fmp): - result = await stocks_server.call_tool("stock_prices", {"symbol": "FAKE"}) - assert "No price data" in text(result) - - -# --------------------------------------------------------------------------- -# Crypto tools -# --------------------------------------------------------------------------- - - -async def test_crypto_instruments(crypto_server: FastMCP) -> None: +async def test_crypto_instruments(mcp_server: FastMCP) -> None: mock_client = AsyncMock() mock_client.get_instruments.return_value = [ MagicMock(__str__=lambda self: "BTC-SPOT") ] - - with patch("quantflow.ai.tools.crypto.Deribit") as MockDeribit: + with patch("app.api.mcp.Deribit") as MockDeribit: MockDeribit.return_value.__aenter__ = AsyncMock(return_value=mock_client) MockDeribit.return_value.__aexit__ = AsyncMock(return_value=False) - - result = await crypto_server.call_tool( - "crypto_instruments", {"currency": "BTC"} - ) - assert "BTC" in text(result) + result = await mcp_server.call_tool("crypto_instruments", {"currency": "BTC"}) + assert "BTC" in text(result) -async def test_crypto_instruments_empty(crypto_server: FastMCP) -> None: +async def test_crypto_instruments_empty(mcp_server: FastMCP) -> None: mock_client = AsyncMock() mock_client.get_instruments.return_value = [] - - with patch("quantflow.ai.tools.crypto.Deribit") as MockDeribit: + with patch("app.api.mcp.Deribit") as MockDeribit: MockDeribit.return_value.__aenter__ = AsyncMock(return_value=mock_client) MockDeribit.return_value.__aexit__ = AsyncMock(return_value=False) - - result = await crypto_server.call_tool( - "crypto_instruments", {"currency": "BTC"} - ) - assert "No instruments" in text(result) + result = await mcp_server.call_tool("crypto_instruments", {"currency": "BTC"}) + assert "No instruments" in text(result) -async def test_crypto_historical_volatility(crypto_server: FastMCP) -> None: +async def test_crypto_historical_volatility(mcp_server: FastMCP) -> None: mock_client = AsyncMock() mock_client.get_volatility.return_value = pd.DataFrame( [{"date": "2025-01-01", "volatility": 0.8}] ) - - with patch("quantflow.ai.tools.crypto.Deribit") as MockDeribit: + with patch("app.api.mcp.Deribit") as MockDeribit: MockDeribit.return_value.__aenter__ = AsyncMock(return_value=mock_client) MockDeribit.return_value.__aexit__ = AsyncMock(return_value=False) - - result = await crypto_server.call_tool( + result = await mcp_server.call_tool( "crypto_historical_volatility", {"currency": "BTC"} ) - assert "volatility" in text(result) - assert "2025-01-01" in text(result) + assert "volatility" in text(result) + assert "2025-01-01" in text(result) -async def test_crypto_historical_volatility_empty(crypto_server: FastMCP) -> None: +async def test_crypto_historical_volatility_empty(mcp_server: FastMCP) -> None: mock_client = AsyncMock() mock_client.get_volatility.return_value = pd.DataFrame() - - with patch("quantflow.ai.tools.crypto.Deribit") as MockDeribit: + with patch("app.api.mcp.Deribit") as MockDeribit: MockDeribit.return_value.__aenter__ = AsyncMock(return_value=mock_client) MockDeribit.return_value.__aexit__ = AsyncMock(return_value=False) - - result = await crypto_server.call_tool( + result = await mcp_server.call_tool( "crypto_historical_volatility", {"currency": "BTC"} ) - assert "No volatility data" in text(result) + assert "No volatility data" in text(result) -async def test_crypto_term_structure(crypto_server: FastMCP, vol_surface: Any) -> None: +async def test_crypto_term_structure(mcp_server: FastMCP, vol_surface: Any) -> None: mock_loader = MagicMock() mock_loader.surface.return_value = vol_surface mock_client = AsyncMock() mock_client.volatility_surface_loader.return_value = mock_loader - - with patch("quantflow.ai.tools.crypto.Deribit") as MockDeribit: + with patch("app.api.mcp.Deribit") as MockDeribit: MockDeribit.return_value.__aenter__ = AsyncMock(return_value=mock_client) MockDeribit.return_value.__aexit__ = AsyncMock(return_value=False) - - result = await crypto_server.call_tool( + result = await mcp_server.call_tool( "crypto_term_structure", {"currency": "ETH"} ) - assert "ttm" in text(result) + assert "ttm" in text(result) -async def test_crypto_implied_volatility( - crypto_server: FastMCP, vol_surface: Any -) -> None: +async def test_crypto_implied_volatility(mcp_server: FastMCP, vol_surface: Any) -> None: mock_loader = MagicMock() mock_loader.surface.return_value = vol_surface mock_client = AsyncMock() mock_client.volatility_surface_loader.return_value = mock_loader - - with patch("quantflow.ai.tools.crypto.Deribit") as MockDeribit: + with patch("app.api.mcp.Deribit") as MockDeribit: MockDeribit.return_value.__aenter__ = AsyncMock(return_value=mock_client) MockDeribit.return_value.__aexit__ = AsyncMock(return_value=False) - - result = await crypto_server.call_tool( + result = await mcp_server.call_tool( "crypto_implied_volatility", {"currency": "ETH"} ) - assert "iv" in text(result) - - -async def test_crypto_prices( - crypto_server: FastMCP, mcp_tool: McpTool, mock_fmp: AsyncMock -) -> None: - mcp_tool.vault.add("fmp", "test-key") - mock_fmp.prices.return_value = pd.DataFrame( - [ - { - "date": "2025-01-01", - "open": 90000, - "high": 95000, - "low": 88000, - "close": 92000, - "volume": 500, - } - ] - ) - with patch("quantflow.ai.tools.base.FMP", return_value=mock_fmp): - result = await crypto_server.call_tool("crypto_prices", {"symbol": "BTCUSD"}) - assert "close" in text(result) - assert "2025-01-01" in text(result) - - -# --------------------------------------------------------------------------- -# FRED tools -# --------------------------------------------------------------------------- - - -async def test_fred_subcategories( - fred_server: FastMCP, mcp_tool: McpTool, mock_fred: AsyncMock -) -> None: - mcp_tool.vault.add("fred", "test-key") - mock_fred.subcategories.return_value = { - "categories": [{"id": "32991", "name": "Money, Banking, & Finance"}] - } - with patch("quantflow.ai.tools.base.Fred", return_value=mock_fred): - result = await fred_server.call_tool("fred_subcategories", {}) - assert "Money" in text(result) + assert "iv" in text(result) -async def test_fred_subcategories_empty( - fred_server: FastMCP, mcp_tool: McpTool, mock_fred: AsyncMock +async def test_vol_surface_snapshot( + mcp_server: FastMCP, vol_surface: Any, tmp_path: Path ) -> None: - mcp_tool.vault.add("fred", "test-key") - mock_fred.subcategories.return_value = {"categories": []} - with patch("quantflow.ai.tools.base.Fred", return_value=mock_fred): - result = await fred_server.call_tool("fred_subcategories", {}) - assert "No categories" in text(result) - - -async def test_fred_series( - fred_server: FastMCP, mcp_tool: McpTool, mock_fred: AsyncMock -) -> None: - mcp_tool.vault.add("fred", "test-key") - mock_fred.series.return_value = { - "seriess": [ - { - "id": "GDP", - "popularity": 90, - "title": "Gross Domestic Product", - "frequency": "Quarterly", - "observation_start": "1947-01-01", - "observation_end": "2025-01-01", - } - ] - } - with patch("quantflow.ai.tools.base.Fred", return_value=mock_fred): - result = await fred_server.call_tool("fred_series", {"category_id": "106"}) - assert "GDP" in text(result) - - -async def test_fred_series_empty( - fred_server: FastMCP, mcp_tool: McpTool, mock_fred: AsyncMock -) -> None: - mcp_tool.vault.add("fred", "test-key") - mock_fred.series.return_value = {"seriess": []} - with patch("quantflow.ai.tools.base.Fred", return_value=mock_fred): - result = await fred_server.call_tool("fred_series", {"category_id": "999"}) - assert "No series" in text(result) - - -async def test_fred_data( - fred_server: FastMCP, mcp_tool: McpTool, mock_fred: AsyncMock -) -> None: - mcp_tool.vault.add("fred", "test-key") - mock_fred.serie_data.return_value = pd.DataFrame( - [{"date": "2025-01-01", "value": 27000.0}] - ) - with patch("quantflow.ai.tools.base.Fred", return_value=mock_fred): - result = await fred_server.call_tool("fred_data", {"series_id": "GDP"}) - assert "value" in text(result) - assert "2025-01-01" in text(result) - - -# --------------------------------------------------------------------------- -# Charts tools -# --------------------------------------------------------------------------- - - -async def test_ascii_chart( - charts_server: FastMCP, mcp_tool: McpTool, mock_fmp: AsyncMock -) -> None: - mcp_tool.vault.add("fmp", "test-key") - mock_fmp.prices.return_value = pd.DataFrame( - [ - { - "date": f"2025-01-{i:02d}", - "open": 100 + i, - "high": 110 + i, - "low": 90 + i, - "close": 105 + i, - "volume": 1000, - } - for i in range(1, 11) - ] - ) - with patch("quantflow.ai.tools.base.FMP", return_value=mock_fmp): - result = await charts_server.call_tool("ascii_chart", {"symbol": "AAPL"}) - t = text(result) - assert "AAPL" in t - assert "High" in t - assert "Low" in t - - -async def test_ascii_chart_empty( - charts_server: FastMCP, mcp_tool: McpTool, mock_fmp: AsyncMock -) -> None: - mcp_tool.vault.add("fmp", "test-key") - mock_fmp.prices.return_value = pd.DataFrame() - with patch("quantflow.ai.tools.base.FMP", return_value=mock_fmp): - result = await charts_server.call_tool("ascii_chart", {"symbol": "FAKE"}) - assert "No price data" in text(result) - - -def test_create_server_registers_all_tools() -> None: - fake_tool = MagicMock() - with ( - patch("quantflow.ai.server.McpTool", return_value=fake_tool), - patch("quantflow.ai.server.vault.register") as vault_register, - patch("quantflow.ai.server.crypto.register") as crypto_register, - patch("quantflow.ai.server.stocks.register") as stocks_register, - patch("quantflow.ai.server.fred.register") as fred_register, - patch("quantflow.ai.server.charts.register") as charts_register, - ): - mcp = ai_server.create_server() - vault_register.assert_called_once_with(mcp, fake_tool) - crypto_register.assert_called_once_with(mcp, fake_tool) - stocks_register.assert_called_once_with(mcp, fake_tool) - fred_register.assert_called_once_with(mcp, fake_tool) - charts_register.assert_called_once_with(mcp, fake_tool) - - -def test_main_runs_server() -> None: - mock_server = MagicMock() - with patch("quantflow.ai.server.create_server", return_value=mock_server): - ai_server.main() - mock_server.run.assert_called_once_with() + mock_loader = MagicMock() + mock_loader.surface.return_value = vol_surface + mock_client = AsyncMock() + mock_client.volatility_surface_loader.return_value = mock_loader + out = str(tmp_path / "snapshot.json") + with patch("app.api.mcp.Deribit") as MockDeribit: + MockDeribit.return_value.__aenter__ = AsyncMock(return_value=mock_client) + MockDeribit.return_value.__aexit__ = AsyncMock(return_value=False) + result = await mcp_server.call_tool( + "vol_surface_snapshot", {"currency": "ETH", "path": out} + ) + assert "Saved" in text(result) + assert Path(out).exists() diff --git a/uv.lock b/uv.lock index 58e89c55..8e81e96e 100644 --- a/uv.lock +++ b/uv.lock @@ -227,18 +227,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, ] -[[package]] -name = "asciichartpy" -version = "1.5.25" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "setuptools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/41/3a/b01436be647f881515ec2f253616bf4a40c1d27d02a69e7f038e27fcdf81/asciichartpy-1.5.25.tar.gz", hash = "sha256:63a305302b2aad51da288b58226009b7b0313eba7d8e2452d5a21a13fcf44d74", size = 8201, upload-time = "2020-08-17T02:07:18.292Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/d0/7b958df957e4827837b590944008f0b28078f552b451f7407b4b3d54f574/asciichartpy-1.5.25-py2.py3-none-any.whl", hash = "sha256:33c417a3c8ef7d0a11b98eb9ea6dd9b2c1b17559e539b207a17d26d4302d0258", size = 7228, upload-time = "2020-08-17T02:07:16.386Z" }, -] - [[package]] name = "ast-serialize" version = "0.5.0" @@ -384,11 +372,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4e/73/cff406b1a149e698dbd1628ad8a590ea5c33b570a9dd47d04f3f17b24cfb/ccy-2.0.0-py3-none-any.whl", hash = "sha256:cfbb27d1e4fc07f9902852e42890d66ca83d90782f5b77c9c8c89912c99fcf03", size = 17050, upload-time = "2026-03-28T21:49:20.08Z" }, ] -[package.optional-dependencies] -holidays = [ - { name = "holidays" }, -] - [[package]] name = "certifi" version = "2026.5.20" @@ -828,15 +811,6 @@ nvtx = [ { name = "nvidia-nvtx-cu12", marker = "sys_platform == 'linux'" }, ] -[[package]] -name = "distro" -version = "1.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, -] - [[package]] name = "docutils" version = "0.22.4" @@ -893,7 +867,7 @@ wheels = [ [[package]] name = "fastapi" -version = "0.136.1" +version = "0.136.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, @@ -902,9 +876,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5d/45/c130091c2dfa061bbfe3150f2a5091ef1adf149f2a8d2ae769ecaf6e99a2/fastapi-0.136.1.tar.gz", hash = "sha256:7af665ad7acfa0a3baf8983d393b6b471b9da10ede59c60045f49fbc89a0fa7f", size = 397448, upload-time = "2026-04-23T16:49:44.046Z" } +sdist = { url = "https://files.pythonhosted.org/packages/81/2d/ff8d91d7b564d464629a0fd50a4489c97fcb836ac230bf3a7269232a9b1f/fastapi-0.136.3.tar.gz", hash = "sha256:e487fae93ad408e6f47641ee4dfe389864fd7bec92e547ea8498fc13f43e83ab", size = 396410, upload-time = "2026-05-23T18:53:15.192Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/ff/2e4eca3ade2c22fe1dea7043b8ee9dabe47753349eb1b56a202de8af6349/fastapi-0.136.1-py3-none-any.whl", hash = "sha256:a6e9d7eeada96c93a4d69cb03836b44fa34e2854accb7244a1ece36cd4781c3f", size = 117683, upload-time = "2026-04-23T16:49:42.437Z" }, + { url = "https://files.pythonhosted.org/packages/e0/82/45359b62a067409bd929ae8a56b8ed13e5a8c8a61194b3c236920999ab83/fastapi-0.136.3-py3-none-any.whl", hash = "sha256:3d2a69bdf04b7e9f3afa292c3bc7a98816bbfafa10bc9b45f3f3700d2f761620", size = 117481, upload-time = "2026-05-23T18:53:16.924Z" }, ] [[package]] @@ -1030,19 +1004,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl", hash = "sha256:11ef7bb35dab8a394fde6e608221d5cf3e8499401c249bebaeaad760a1a8dec2", size = 203402, upload-time = "2026-04-29T20:42:36.842Z" }, ] -[[package]] -name = "genai-prices" -version = "0.0.61" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "httpx" }, - { name = "pydantic" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/65/71/0c76010eec75f4b3623d521044785c0977c14adabe1cac72b004349567fb/genai_prices-0.0.61.tar.gz", hash = "sha256:4b3bcfd49f174c05831b09f9ee36557d3648569e2f594af6c24b72031b3f0e52", size = 67806, upload-time = "2026-05-19T17:01:36.902Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/ec/b08dc2e834ca00fd8dfedcb17ae2e920667adaad617b45e32b7a3b146f24/genai_prices-0.0.61-py3-none-any.whl", hash = "sha256:d77142f61c13e69909ac19c8e44fd315fd65f3afd714e8d55e914fab0eaf47a2", size = 70853, upload-time = "2026-05-19T17:01:37.858Z" }, -] - [[package]] name = "ghp-import" version = "2.1.0" @@ -1055,45 +1016,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, ] -[[package]] -name = "google-auth" -version = "2.53.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, - { name = "pyasn1-modules" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c6/ad/ff781329bbbdc0974a098d996e89c9e1f7024262f9e3eec442fbb9ad1ac6/google_auth-2.53.0.tar.gz", hash = "sha256:e7e6aa16f6bee7b2b264830fd04f08087a1d5a836df516251a5d15327b246c9c", size = 335844, upload-time = "2026-05-15T20:53:07.928Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/c9/db44165ba7c581268c6d46017ef63339110378305062830104fc7fa144cb/google_auth-2.53.0-py3-none-any.whl", hash = "sha256:6e7449917c599b35126a99ec268ec6880301f2fea41dce198fe8fd83ff642b68", size = 246071, upload-time = "2026-05-15T20:53:05.609Z" }, -] - -[package.optional-dependencies] -requests = [ - { name = "requests" }, -] - -[[package]] -name = "google-genai" -version = "2.6.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "distro" }, - { name = "google-auth", extra = ["requests"] }, - { name = "httpx" }, - { name = "pydantic" }, - { name = "requests" }, - { name = "sniffio" }, - { name = "tenacity" }, - { name = "typing-extensions" }, - { name = "websockets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/dd/ec/6e49f50f5c70588d97c6ed25e0b8c18828bf4d58895f397b53a7522168a1/google_genai-2.6.0.tar.gz", hash = "sha256:7d4f777234002f2e94be499dbdfb43b506a6aca9dbbec13e61d3dc6ce640ffa7", size = 554809, upload-time = "2026-05-22T01:34:33.581Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/9e/e8ba4e58a9d5daf42343f3ea1cb0efb721eba36a1d6624e9873d039a5c1e/google_genai-2.6.0-py3-none-any.whl", hash = "sha256:272b6f6320f5d355735241ad441f972af095ec80dc10cb075cb430d96721648a", size = 821003, upload-time = "2026-05-22T01:34:31.55Z" }, -] - [[package]] name = "griffe-pydantic" version = "1.3.1" @@ -1219,18 +1141,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1f/7f/13cd798d180af4bf4c0ceddeefba2b864a63c71645abc0308b768d67bb81/hjson-3.1.0-py3-none-any.whl", hash = "sha256:65713cdcf13214fb554eb8b4ef803419733f4f5e551047c9b711098ab7186b89", size = 54018, upload-time = "2022-08-13T02:52:59.899Z" }, ] -[[package]] -name = "holidays" -version = "0.97" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "python-dateutil" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0c/61/058f3f05dd318b9d9546df513f8f6611557919e5360cd608a3ce7c7500d4/holidays-0.97.tar.gz", hash = "sha256:8fe491270bd4aeed6f9584d459d5df506a414727ba76fdd9ebd6323def606935", size = 911304, upload-time = "2026-05-18T19:48:19.37Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/54/7947b0cba0c91b81801da793ea2d273aab5bdd5484a5ba6c2d3863110f07/holidays-0.97-py3-none-any.whl", hash = "sha256:0bcd55e64abddce2f9aa9224d68afe87acdd7eac7c2aef251b0ef7986bb5220b", size = 1479247, upload-time = "2026-05-18T19:48:16.881Z" }, -] - [[package]] name = "httpcore" version = "1.0.9" @@ -1358,96 +1268,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] -[[package]] -name = "jiter" -version = "0.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/66/b5/55f06bb281d92fb3cc86d14e1def2bd908bb77693183e7cb1f5a3c388b0c/jiter-0.15.0.tar.gz", hash = "sha256:4251acc80e2b7c9b7b8823456ea0fceeb0734dac2df7636d3c711b38476b5a76", size = 166640, upload-time = "2026-05-19T10:09:48.361Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/13/daa722f5765c393576f466378f9dfd29d77c9bed939e0688f96afa3601ea/jiter-0.15.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0f862193b8696249d22ec433e85fd2ab0ad9596bc3e45e6c0bc55e8aeba97be2", size = 310899, upload-time = "2026-05-19T10:07:12.89Z" }, - { url = "https://files.pythonhosted.org/packages/7f/82/2d2551829b082f4b6d82b9f939b031fb808a10aab1ec0664f82e150bb9a2/jiter-0.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1303d4d68a9b051ea90502402063ecf3807da00ad2affa19ca1ae3b90b3c5f67", size = 314963, upload-time = "2026-05-19T10:07:14.539Z" }, - { url = "https://files.pythonhosted.org/packages/2a/0a/8b1a51466f7fe9f31dbe4bc7e0ca848674f9825e0f737b929b97e8c60aa7/jiter-0.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:392b8ab019e5502d08aff85c6272209c24bc2cbe706ea82a56368f524236614a", size = 341730, upload-time = "2026-05-19T10:07:15.869Z" }, - { url = "https://files.pythonhosted.org/packages/f6/2a/e71dea19822e2e404e83992a08c1d6b9b617bb944f28c9c2fbd85d02c91e/jiter-0.15.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:773b6eb282ce11ee19f05f6b2d4404fa308e5bbd353b0b80a0262caad6db2cd7", size = 366214, upload-time = "2026-05-19T10:07:17.259Z" }, - { url = "https://files.pythonhosted.org/packages/c4/59/97e1fa539d124a509a00ab7f669289d1c1d236ecabf12948a18f16c91082/jiter-0.15.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2c0c44d569ce0f2850f5c926f8caeb5f245fbc84475aeb36efccc2103e6dbd", size = 459527, upload-time = "2026-05-19T10:07:18.741Z" }, - { url = "https://files.pythonhosted.org/packages/d1/7a/4a68d331aef8cf2e2393c14a3aacb635c62aa86071b0229899fb5baaa907/jiter-0.15.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:032396229564bca02440396bd327710719f724f5e7b7e9f7a8eb3faa4a2c2281", size = 375451, upload-time = "2026-05-19T10:07:20.208Z" }, - { url = "https://files.pythonhosted.org/packages/7b/7e/1c445c2b6f0e30a274dc8082e0c3c7825411cce80d726bccd697c98cc8d3/jiter-0.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3d37768fce7f88dd2a8c6091f2325dea27d30d30d5c6e7a1c0f0af77723b708", size = 349428, upload-time = "2026-05-19T10:07:22.372Z" }, - { url = "https://files.pythonhosted.org/packages/00/94/e20d38984fc17a636371bffd2ae0f698124fdc8e75ef969cd2da6ba7cea7/jiter-0.15.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:2c9cb907439d20bd0c7d7565ca01ee52234203208433749bae5b516907526928", size = 355405, upload-time = "2026-05-19T10:07:23.916Z" }, - { url = "https://files.pythonhosted.org/packages/94/fa/4d09f814779d0ea80a28ed8e4c6662ec9a4a8ecef0ac52190ebac6262d14/jiter-0.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9100ddbec09741cc66feb0fc6773f8bdbd0e3c345689368f260082ff85dcc0cd", size = 393688, upload-time = "2026-05-19T10:07:25.854Z" }, - { url = "https://files.pythonhosted.org/packages/54/9d/8eb5d4fb8bf7e93a75964a5da71a75c67c864baf7fa3f98598187b3c7e57/jiter-0.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ae1b0d82ac2d987f9ea512b1c9adfcc71a28de3dea3a6039b54d76cffda9901e", size = 520853, upload-time = "2026-05-19T10:07:27.303Z" }, - { url = "https://files.pythonhosted.org/packages/e7/2c/5e07874e59e623a943a0acf1552a80d05b70f31b402287a8fc6d7ec634c7/jiter-0.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8020c99ec13a7db2b6f96cbe82ef4721c88b426a4892f27478044af0284615ef", size = 551016, upload-time = "2026-05-19T10:07:28.846Z" }, - { url = "https://files.pythonhosted.org/packages/22/ed/d2d34422143474cadc15b60d482b1c35683dbc5c63c24346ddd0df09bcaf/jiter-0.15.0-cp311-cp311-win32.whl", hash = "sha256:42bfb257930800cf43e7c62c832402c704ab60797c992faf88d20e903eac8f32", size = 209518, upload-time = "2026-05-19T10:07:30.431Z" }, - { url = "https://files.pythonhosted.org/packages/1d/7d/52778b930e5cc3e52a37d950b1c10494244308b4329b25a0ff0d88303a81/jiter-0.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:860a74063284a2ae9bfedd694f299cc2c68e2696c5f3d440cc9d18bb81b9dd04", size = 200565, upload-time = "2026-05-19T10:07:32.125Z" }, - { url = "https://files.pythonhosted.org/packages/3b/4f/d9b4067feb69b3fa6eb0488e1b59e2ad5b463fe39f59e527eab2aca00bb0/jiter-0.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:37a10c377ce3a4a85f4a67f28b7afe093154cde77eaf248a72e856aa08b4d865", size = 195488, upload-time = "2026-05-19T10:07:33.846Z" }, - { url = "https://files.pythonhosted.org/packages/44/53/4f6bddbcde3c71e56d0aa1337ec95950f3d27dd4153e25aadf0feac71751/jiter-0.15.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0e90a1c315a0226ec822d973817967f9223b7701546c8c2a7913e7ab0926294d", size = 308793, upload-time = "2026-05-19T10:07:35.25Z" }, - { url = "https://files.pythonhosted.org/packages/01/84/c01099b59a285a1ebba64ae93f62bfa036675340fd1b0045ae65890a0442/jiter-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8c9004af7c8d67cce7f1aae1026fb55607f4aa600710d08ede3a3ce4aeefe7e0", size = 309570, upload-time = "2026-05-19T10:07:36.919Z" }, - { url = "https://files.pythonhosted.org/packages/58/64/8fb7f9d45bb98190355454cd04dad8d8f27223d6bd52f83af07f637168a6/jiter-0.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c210f8b35dc6f30aafd4b4365ca89b9d1189f21ab49b8e68fa6322a847aef138", size = 336783, upload-time = "2026-05-19T10:07:38.694Z" }, - { url = "https://files.pythonhosted.org/packages/c3/b6/f5739011d009b3a30f6a53c5240979030ba29ae46a8c67e3a15759f7c37d/jiter-0.15.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f30bae8bc1c2d613e28e5af3e8cceb09b742f1c8a8a5f839fb67afaffc03b61", size = 363555, upload-time = "2026-05-19T10:07:40.832Z" }, - { url = "https://files.pythonhosted.org/packages/e5/12/98a9d9f766665e8a3b6252454e17cb0c464606a28cf2fa09399b003345fa/jiter-0.15.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c60e71b6d10cfc284c9bf36bd885e8d44c46f688ce50aa91b5edd90181dea687", size = 452255, upload-time = "2026-05-19T10:07:42.62Z" }, - { url = "https://files.pythonhosted.org/packages/e8/d5/60f972840f79c5e7544fce567c56f1e4e50468f996baba3e78d823dd62a6/jiter-0.15.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ab068bce62a45aa3e7367eceaffb5dde60b7eb853be8dece45132e3d0ff4879", size = 373559, upload-time = "2026-05-19T10:07:44.201Z" }, - { url = "https://files.pythonhosted.org/packages/ee/cf/d46ef1234ba335aabc2f013210db8e0821a22f5e644a2e9449df199ecc23/jiter-0.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa248c9eb220197d363f688818dac2fd4b2f0cd7d843ca7105d652034823427d", size = 346055, upload-time = "2026-05-19T10:07:46.005Z" }, - { url = "https://files.pythonhosted.org/packages/f0/63/4d2749d8d54d230bad9b3a6b0d00cc28c6ff6b2fdffc26a8ccf76cc5a974/jiter-0.15.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2a77aadd57cac1682e4401a72724d2796d89a4ba129b1a5812aa94ee480826eb", size = 351406, upload-time = "2026-05-19T10:07:47.855Z" }, - { url = "https://files.pythonhosted.org/packages/d9/b9/9965b990035d8773328e0a8c8b457a87bf2b19f6c4126d9d99296be5d16a/jiter-0.15.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2ae901f3a55bfafdde31d289590fa25e3245735a2b1e8c7cc15871710a002871", size = 389357, upload-time = "2026-05-19T10:07:49.665Z" }, - { url = "https://files.pythonhosted.org/packages/2d/55/9ddf903deda1413e87fed792f416b7123daee5b8efbad6a202a7421c36a5/jiter-0.15.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f0b271b462769543716f92d3a4f90527df6ef5ed05ee95ec4137f513e21e1b77", size = 517263, upload-time = "2026-05-19T10:07:51.537Z" }, - { url = "https://files.pythonhosted.org/packages/e8/76/a0c40ad064d3a20a4fde231e35d56e9a01ce82164278180e82d5daf85469/jiter-0.15.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2fb6a5d26af81fc0f00f9360a891e05cf755e149bba391c4d563adc54812973d", size = 548646, upload-time = "2026-05-19T10:07:53.196Z" }, - { url = "https://files.pythonhosted.org/packages/23/4f/eca9b954942916ba2f453891b8593ab444cd872396fe66a3936616f236f3/jiter-0.15.0-cp312-cp312-win32.whl", hash = "sha256:c2f6bb8b5216ab9e7873bc08b5d7bef2b8abbb578a3069bf1cd14a45d71d771d", size = 206427, upload-time = "2026-05-19T10:07:55.307Z" }, - { url = "https://files.pythonhosted.org/packages/95/bf/8ead82a87495149542748e828d153fd232a512a22c83b02c4815c1a9c7d8/jiter-0.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:40b2c7e92c44a84d748d21706c68dc6ff8161d80b59c99d774721a0d2317d7c7", size = 197300, upload-time = "2026-05-19T10:07:56.651Z" }, - { url = "https://files.pythonhosted.org/packages/f4/e4/9b8a78fb2d894471bc344e37f1949bdd784bd914d031dba0ba3a40c71dd7/jiter-0.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:cc0bc345cf2df9d1c00ac443f50d543c1ccfa8b0422cb85b1ab70d681c0b255b", size = 192702, upload-time = "2026-05-19T10:07:58.307Z" }, - { url = "https://files.pythonhosted.org/packages/e5/f4/f708c900ecee41b2025ef8413d5351e5649eb2125c506f6720cc69b06f5c/jiter-0.15.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1c11465f97e2abf45a014b83b730222f8f1c5335e802c7055a67d50de6f1f4e3", size = 307829, upload-time = "2026-05-19T10:07:59.704Z" }, - { url = "https://files.pythonhosted.org/packages/86/59/db537c0949e83668c38481d426b9f2fd5ab758c4ee53a811dd0a510626a0/jiter-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1e7b1776f0797956c509e123d0952d10d293a9492dea9f288ab9570ec01d1a5", size = 308445, upload-time = "2026-05-19T10:08:01.184Z" }, - { url = "https://files.pythonhosted.org/packages/37/38/ea0e13b18c30ef951da0d47d39e7fa9edb82a93a62990ffbd7cea9b622d4/jiter-0.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:351a341c2105aa430b7047e30f1bf7975f6313b00165d3fc07be2edaf741f279", size = 336181, upload-time = "2026-05-19T10:08:02.688Z" }, - { url = "https://files.pythonhosted.org/packages/58/fc/2303901b16c4ba05865588990a420c0b4156270b44379c20931544a1d962/jiter-0.15.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ab395feec8d249ec4044e228e98a7033f043426a265df439dc3698823f0a4e4", size = 362985, upload-time = "2026-05-19T10:08:04.394Z" }, - { url = "https://files.pythonhosted.org/packages/5b/6f/11bace093c52e7d4d26c8e606ccd7ae8c972189622469ec0d9e28161e28b/jiter-0.15.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2a438005b6f22d0273413484d6094d7c2c5d10ec1b3a3bf128e0d1d3ba53258", size = 453292, upload-time = "2026-05-19T10:08:05.967Z" }, - { url = "https://files.pythonhosted.org/packages/22/db/987f2f086ca4d7a6582eb4ccd513f9b26b42d9e4243a087609a3137a8fc7/jiter-0.15.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f18f85e4218d1b40f000f42a92239a7a61a902cd42c65e6c360dbd17dcb20894", size = 373501, upload-time = "2026-05-19T10:08:07.857Z" }, - { url = "https://files.pythonhosted.org/packages/8f/7c/89fbcabb2739b7a5b8dc959a1b6c5761f6484f5fed3486854b3c789bb1de/jiter-0.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1aa62e277fc1cbd80e6deacae6f4d983b41b3d7728e0645c5d741a6149bba45", size = 344683, upload-time = "2026-05-19T10:08:09.431Z" }, - { url = "https://files.pythonhosted.org/packages/30/6f/6cca7692e7dddfec6d8d76c54dc97f2af2a41df4ac0674b999df1f09a5f3/jiter-0.15.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:6550fa135c7deb8ead6af49ed7ff648532ea8334a1447fe34a36315ef79c5c29", size = 350892, upload-time = "2026-05-19T10:08:11.352Z" }, - { url = "https://files.pythonhosted.org/packages/39/14/0338d6190cb8e6d22e677ab1d4eabd4117f67cca70c54cd04b82ff64e068/jiter-0.15.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:066f8f33f18b2419cd8213b2436fa7fbc9c499f315971cfa3ce1f9820c001b1b", size = 388723, upload-time = "2026-05-19T10:08:12.912Z" }, - { url = "https://files.pythonhosted.org/packages/90/31/cc19f4a1bdb6afb09ce6a2f2615aa8d44d994eba0d8e6105ed1af920e736/jiter-0.15.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:75e8a04e91432dde9f1838373cf93d23726c79d3e908d319acf0e796f85592e7", size = 516648, upload-time = "2026-05-19T10:08:14.808Z" }, - { url = "https://files.pythonhosted.org/packages/49/9f/833c541512cd091b63c10c0381973dfe11bc7a503a818c16384417e0c81e/jiter-0.15.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a97261f1fccb8e50ecd2890a96e46efdc3f57c80a197324c6777827231eca712", size = 547382, upload-time = "2026-05-19T10:08:16.927Z" }, - { url = "https://files.pythonhosted.org/packages/d2/11/e7b70e91f90bc4477e8eee9e8a5f7cf3cb41b4525d6394dc98a714eb8f7f/jiter-0.15.0-cp313-cp313-win32.whl", hash = "sha256:c77496cb10bd7549690fbbab3e5ec05857b83e49276f4a9423a766ddd2afcd4c", size = 205845, upload-time = "2026-05-19T10:08:18.401Z" }, - { url = "https://files.pythonhosted.org/packages/4b/23/5c20d9ad6f02c493e4023e5d2d09e1c1f15fe2753c9102c544aff068a88e/jiter-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b15741f501469009ae0ae90b7147958a664a7dede40aa7ff174a8a4645f546d0", size = 196842, upload-time = "2026-05-19T10:08:20.131Z" }, - { url = "https://files.pythonhosted.org/packages/6b/11/1eb400ef248e8c925fd883fbe325daf5e42cd1b0d308539dd332bd4f7ffc/jiter-0.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d6a60072b44c3c2b797a7ddcbcbbf2b34ea3cfd4721580fbfd2a09d9d9b84ba", size = 192212, upload-time = "2026-05-19T10:08:21.807Z" }, - { url = "https://files.pythonhosted.org/packages/8a/60/2fd8d7c79da8acf9b7b277c7616847773779356b92acfc9bb158452174da/jiter-0.15.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ef1fd24d9413f6209e00d3d5a453e67acfe004a25cc6c8e8484faed4311ab9e8", size = 315065, upload-time = "2026-05-19T10:08:23.218Z" }, - { url = "https://files.pythonhosted.org/packages/46/f4/008fb7d65e8ac2abf00811651a661e025c4ba80bbc6f378450384ddd3aed/jiter-0.15.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:144f8e72cb53dab146347b91cceac01f5481237f2b93b4a339a1ee8f8878b67c", size = 339444, upload-time = "2026-05-19T10:08:24.701Z" }, - { url = "https://files.pythonhosted.org/packages/00/55/90b0c7b9c6896c0f2a591dd36d36b71d22e09674bfef178fa03ba3f81499/jiter-0.15.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:553fcac2ef2cb990877f9fc0833b8b629a3e6a5670b6b5fd58219b41a653ddc4", size = 347779, upload-time = "2026-05-19T10:08:26.408Z" }, - { url = "https://files.pythonhosted.org/packages/51/6b/69666cec5000fd57734c118437394516c749ae8dbeea9fb66d6fef9c4775/jiter-0.15.0-cp313-cp313t-win_amd64.whl", hash = "sha256:774f93f65031856bf14ad9f59bdcab8b8cad501e5ceabd51ba3525f76937a25b", size = 200395, upload-time = "2026-05-19T10:08:28.055Z" }, - { url = "https://files.pythonhosted.org/packages/39/04/a6aa62cd27e8149b0d28df5561f10f6cceaf7935a9ccf3f1c5a05f9a0cd8/jiter-0.15.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f1e1754960f38ec40613a07e5e372df67acb3b890fb383b6fb3de3e49ddbf3c7", size = 190516, upload-time = "2026-05-19T10:08:29.35Z" }, - { url = "https://files.pythonhosted.org/packages/eb/d2/079f350ebf7859d081de30aa890f9e3be68516f754f3ba32366ffff4dcee/jiter-0.15.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:ac0d9ddea4350974be7a221fc25895f251a8fee748c889bdced2141c0fec1a49", size = 308884, upload-time = "2026-05-19T10:08:31.667Z" }, - { url = "https://files.pythonhosted.org/packages/04/4e/a2c30a7f69b48c03b20935d647479106fe932f6e63f75faf53937197e05d/jiter-0.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:01a8222cf05ab1128e239421156c207949808acaaea2bdfd33130ae666786e86", size = 310028, upload-time = "2026-05-19T10:08:33.304Z" }, - { url = "https://files.pythonhosted.org/packages/40/90/2e7cdfd3cf8ca967be38c48f5cf474d79f089efaf559a40f15984a77ae69/jiter-0.15.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:182226cbc930c9fab81bc2e41a4da672f89539906dadb05e75670ac07b94f71f", size = 337485, upload-time = "2026-05-19T10:08:35.259Z" }, - { url = "https://files.pythonhosted.org/packages/9b/11/15a1aa28b120b8ee5b4f1fb894c125046225f09847738bd64233d3b84883/jiter-0.15.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:71683c38c825452999b5717fcae07ea708e8c93003e808be4319c1b02e3d176e", size = 364223, upload-time = "2026-05-19T10:08:36.694Z" }, - { url = "https://files.pythonhosted.org/packages/b7/25/f442e8af5f3d0dcf47b39e83a0efd9ee45ea946aa6d04625dc3181eae3b6/jiter-0.15.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30f2218e6a9e5c18bc10fe6d41ac189c442c88eacf11bad9f28ef95a9bef00e6", size = 456387, upload-time = "2026-05-19T10:08:38.143Z" }, - { url = "https://files.pythonhosted.org/packages/da/f4/37f2d2c9f64f49af7da652ed7532bb5a2372e588e6927c3fdd76f911db65/jiter-0.15.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5157de9f76eb4bc5ea74a1219366a25f945ad305641d74e04f59c54087091aa9", size = 374461, upload-time = "2026-05-19T10:08:39.869Z" }, - { url = "https://files.pythonhosted.org/packages/60/28/edcfbbbf0cb15436f36664a8908a0df47ab9006298d4cd937dc08ea932d6/jiter-0.15.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c5db5527c221249a876160663ab891ace358c17f7b9c93ec1478b7f0550e5c", size = 345924, upload-time = "2026-05-19T10:08:41.668Z" }, - { url = "https://files.pythonhosted.org/packages/47/13/89fba6398dab7f202b7278c4b4aac122399d2c0183971c4a57a3b7088df5/jiter-0.15.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:3e4540b8e74e4268811ac05db226a6a128ff572e7e0ce3f1163b693cadb184cd", size = 352283, upload-time = "2026-05-19T10:08:43.091Z" }, - { url = "https://files.pythonhosted.org/packages/1b/da/0f6af8cef2c565a1ab44d970f268c43ccaa72707386ea6388e6fe2b6cd26/jiter-0.15.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:62ebd14e47e9aed9df4472afcb2663668ce4d74891cd54f86bf6e44029d6dc89", size = 389985, upload-time = "2026-05-19T10:08:44.915Z" }, - { url = "https://files.pythonhosted.org/packages/a1/ec/b9cb7d6d29e24ee14910266157d2a279d7a8f60ee0df7fa840882976ba64/jiter-0.15.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0be6f5ad41a809f303f416d17cec92a7a725902fb9b4f3de3d19362ac0ef8554", size = 517695, upload-time = "2026-05-19T10:08:46.486Z" }, - { url = "https://files.pythonhosted.org/packages/64/5e/6d1bda880723aae0ad86b4b763f044362448efe31e3e819635d41cb03451/jiter-0.15.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:813dfbb17d65328bf86e5f0905dd277ba2265d3ca20556e86c0c7035b7182e5a", size = 548868, upload-time = "2026-05-19T10:08:48.026Z" }, - { url = "https://files.pythonhosted.org/packages/0c/72/7de501cf38dcacaf35098796f3a50e0f2e338baba18a58946c618544b809/jiter-0.15.0-cp314-cp314-win32.whl", hash = "sha256:50e51156192722a9c58db112837d3f8ef96fb3c5ecc14e95f409134b08b158ec", size = 206380, upload-time = "2026-05-19T10:08:49.738Z" }, - { url = "https://files.pythonhosted.org/packages/1e/a9/e19addf4b0c1bdce52c6da12351e6bc42c340c45e7c09e2158e46d293ccc/jiter-0.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:30ce1a5d16b5641dc935d50ef775af6a0871e3d14ab05d6fc54dff371b78e558", size = 197687, upload-time = "2026-05-19T10:08:51.088Z" }, - { url = "https://files.pythonhosted.org/packages/f2/c9/776b1db01db25fc6c1d58d1979a37b0a9fe787e5f5b1d062d2eaacb77923/jiter-0.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:510c8b3c17a0ed9ac69850c0438dada3c9b82d9c4d589fcb62002a5a9cf3a866", size = 192571, upload-time = "2026-05-19T10:08:52.451Z" }, - { url = "https://files.pythonhosted.org/packages/a0/f6/45bb4670bacf300fd2c7abadbfb3af376e5f1b6ae75fd9bc069891d15870/jiter-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7553333dd0930c104a5a0db8df72bf7219fe663d731383b576bb6ed6351c984d", size = 317151, upload-time = "2026-05-19T10:08:53.867Z" }, - { url = "https://files.pythonhosted.org/packages/d7/68/ed635ad5acd7b73e454283083bbb7c8205ad10e88b0d9d7d793b09fe8226/jiter-0.15.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2143ab06181d2b029eedcb6af3cebe95f11bbac62441781860f98ee9330a6a6", size = 341243, upload-time = "2026-05-19T10:08:55.383Z" }, - { url = "https://files.pythonhosted.org/packages/5d/db/3ff4176b817b8ea33879e71e13d8bc2b0d481a7ed3fe9e080f333d415c16/jiter-0.15.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6eac374c5c975709b69c10f09afd199df74150172156ad10c8d4fd785b7da995", size = 363629, upload-time = "2026-05-19T10:08:56.928Z" }, - { url = "https://files.pythonhosted.org/packages/ab/24/5f8270e0ba9c883582f96f722f8a0b58015c7ce1f8c6d4571cf394e99b6b/jiter-0.15.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b3b3b775e33d3bfaec9899edc526ae97b0da0bf9d071a46124ba419149a414f8", size = 456198, upload-time = "2026-05-19T10:08:58.618Z" }, - { url = "https://files.pythonhosted.org/packages/45/5b/76fc02b0b5c54c3d18c60653156e2f76fde1816f9b4722db68d6ee2c897e/jiter-0.15.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3071db3346334beae1360b46da4606da57bf3528c167b3c38533afaf9f2c5", size = 373710, upload-time = "2026-05-19T10:09:00.151Z" }, - { url = "https://files.pythonhosted.org/packages/c4/52/4310821b0ea9277994d3e1f49fc6a4b34e4800caebacb2c0af81da59a454/jiter-0.15.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6694a173ecabc12eb60efbc0b474464ead1951ff65cd8b1e72100715c64512b", size = 349901, upload-time = "2026-05-19T10:09:01.621Z" }, - { url = "https://files.pythonhosted.org/packages/93/fe/67648c35b3594fba8854ac64cc8a826d8bcd18324bbdb53d77697c60b6ef/jiter-0.15.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:a254e10b593624d230c365b6d616b22ca0ad65e63a16e6631c2b3466022e6ba8", size = 352438, upload-time = "2026-05-19T10:09:03.216Z" }, - { url = "https://files.pythonhosted.org/packages/cb/28/0a1879d07ad6b3e025a2750027363452ced93c2d16d1c9d4b153ffd51c91/jiter-0.15.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d8d2955167274e15d79a7a020afdd9b39c990eb80b2d89fca695d92dcfdd38ec", size = 388152, upload-time = "2026-05-19T10:09:04.741Z" }, - { url = "https://files.pythonhosted.org/packages/c1/78/46c6f6b56ba85c90021f4afd72ed42f691f8f84daacb5fe27277070e3858/jiter-0.15.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:acf4ee4d1fc55917239fe72972fb292dd773055d05eb040d36f4326e02cc2c0e", size = 517707, upload-time = "2026-05-19T10:09:06.231Z" }, - { url = "https://files.pythonhosted.org/packages/ca/cb/720662d4c88fcad606e826fef5424365527ba43ce4868a479aed8f8c507e/jiter-0.15.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:e7196e56f1cd69af1dbb07dff02dcfb260a50b45a82d409d92a06fedb32473b5", size = 548241, upload-time = "2026-05-19T10:09:08.093Z" }, - { url = "https://files.pythonhosted.org/packages/60/e3/935b8034fd143f21125c87d51404a9e0e1449186a494405721ff5d1d695e/jiter-0.15.0-cp314-cp314t-win32.whl", hash = "sha256:7f6163c0f10b055245f814dcc59f4818da60dfe72f3e72ab89fc24b6bd5e9c52", size = 207950, upload-time = "2026-05-19T10:09:09.616Z" }, - { url = "https://files.pythonhosted.org/packages/93/59/984fd9ece895953dad3e0880a650e766f5a2da2c5514f0eafdaaabbeb5f9/jiter-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:980c256edb05b78a111b99c4de3b1d32e31634b867fd1fc2cf726e7b7bba9854", size = 200055, upload-time = "2026-05-19T10:09:11.367Z" }, - { url = "https://files.pythonhosted.org/packages/0e/a4/cf8d779feb133a27a2e3bc833bccb9e13aa332cdf820497ebf72c10ce8c3/jiter-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:66b1880df2d01e206e8339769d1c7c1753bcb653efd6289e203f6f24ebada0c0", size = 191244, upload-time = "2026-05-19T10:09:12.74Z" }, - { url = "https://files.pythonhosted.org/packages/65/43/1fc62172aa98b50a7de9a25554060db510f85c89cfbed0dfe13e1907a139/jiter-0.15.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:411fa4dfa5a7ae3d11491027ffb9beadec3996010a986862db70d91abba1c750", size = 305585, upload-time = "2026-05-19T10:09:35.995Z" }, - { url = "https://files.pythonhosted.org/packages/e8/c4/dd58fcd9e2df83666e5c1c1347bef58ce919cd8efc3ffa38aeea62ce493b/jiter-0.15.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:2b0074e2f56eb2dacca1689760fd2852a068f85a0547a157b82cb4cafeb6768b", size = 306936, upload-time = "2026-05-19T10:09:37.435Z" }, - { url = "https://files.pythonhosted.org/packages/39/86/b695e16f1180c07f43ea98e73ecd21cf63fa2e1b0c1103739013784d11ae/jiter-0.15.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:913d02d29c9606643418d9ccfc3b72492ab25a6bf7889934e09a3490f8d3438b", size = 342453, upload-time = "2026-05-19T10:09:39.294Z" }, - { url = "https://files.pythonhosted.org/packages/34/56/55d76614af37fe3f22a3347d1e410d2a15da581997cb2da499a625000bb5/jiter-0.15.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b15d3ec9b0449c40e85319bdb4caa8b77ab526e74f5532ed94bec15e2f66822c", size = 345606, upload-time = "2026-05-19T10:09:40.727Z" }, - { url = "https://files.pythonhosted.org/packages/73/38/505941b2b092fd5bbbd60a52a880db1173f1690ae6751bed3af1c9ddcb4e/jiter-0.15.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:631f13a3d04e97d4e083993b10f4b99530e3a10d953e2eb5e196b7dc7f812ce0", size = 303769, upload-time = "2026-05-19T10:09:42.203Z" }, - { url = "https://files.pythonhosted.org/packages/e7/95/a06692b29e77473f286e1ec1f426d3ca44d7b5843be8ad21d7a5f3fcdcc0/jiter-0.15.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:b6c0ffae686c39bf3737be60793783267628783ea42545632c10b291105aee45", size = 305128, upload-time = "2026-05-19T10:09:43.657Z" }, - { url = "https://files.pythonhosted.org/packages/23/85/7270d7ad41d6061a25b950c6bf91d638bd9aacb113200a8c8d57a055fd67/jiter-0.15.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d54fb5b31dea401a41af3f8a7d2512e9b6a6a005491e6166c7e4ffab9639a9c", size = 340459, upload-time = "2026-05-19T10:09:45.452Z" }, - { url = "https://files.pythonhosted.org/packages/c8/8d/302cb2057b7513327b4d575cff6b1d066ee6431a5357fc3f8867cd684406/jiter-0.15.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d5d6090cdc1b7c9e780dfb04949a990adb1e301a2fc0bbcee7de4638d33f9a", size = 344469, upload-time = "2026-05-19T10:09:46.864Z" }, -] - [[package]] name = "jsonschema" version = "4.26.0" @@ -1601,15 +1421,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ce/62/b40b382fa0c66fee1478073eb8db352a4a6beda4a1adccf1df911d8c289c/librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253", size = 102572, upload-time = "2026-05-10T18:17:06.809Z" }, ] -[[package]] -name = "logfire-api" -version = "4.33.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/47/1a/c44eb3a02407aa739669822d19734e76e8751284b86cd99c73baca36998a/logfire_api-4.33.0.tar.gz", hash = "sha256:0a604f710e803db08a2ddc41e6152bf8d8a56b549f8856cbf82baa36bc7de2c9", size = 81483, upload-time = "2026-05-13T15:14:17.348Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/58/dc21672e0d5294668f4d35aaba4d839d031f096b4cf6802399c4b411b5fc/logfire_api-4.33.0-py3-none-any.whl", hash = "sha256:b992727bab71412b5a00f54453eec18e559232c83d7120cf5c6a9edd33fb0c4e", size = 128896, upload-time = "2026-05-13T15:14:14.322Z" }, -] - [[package]] name = "logistro" version = "2.0.1" @@ -1748,18 +1559,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, ] -[[package]] -name = "markdown-it-py" -version = "4.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mdurl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, -] - [[package]] name = "markupsafe" version = "3.0.3" @@ -1859,15 +1658,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/73/42d9596facebdb533b7f0b86c1b0364ef350d1f8ba78b1052e8a58b48b65/mcp-1.27.1-py3-none-any.whl", hash = "sha256:1af3c4203b329430fde7a87b4fcb6392a041f5cb851fd68fc674016ab4e7c06f", size = 216260, upload-time = "2026-05-08T16:50:10.547Z" }, ] -[[package]] -name = "mdurl" -version = "0.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, -] - [[package]] name = "mergedeep" version = "1.3.4" @@ -2533,37 +2323,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, ] -[[package]] -name = "openai" -version = "2.38.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "distro" }, - { name = "httpx" }, - { name = "jiter" }, - { name = "pydantic" }, - { name = "sniffio" }, - { name = "tqdm" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8f/12/cfa322c5f5dd8fa21aab9a7a8e979e7a11123800f86ca8d82eb68a83d213/openai-2.38.0.tar.gz", hash = "sha256:798694c6cf74145541fda94325b6f8f72d8e1fd0262cc137c8d728177a6a4ce3", size = 772764, upload-time = "2026-05-21T21:23:42.105Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/bf/ccff9be562e24207716d04ef9dc931c76aff0c89a7265da43e2104d7fe06/openai-2.38.0-py3-none-any.whl", hash = "sha256:ec6661c57b2dcc47414a767e6e3335c7ed3d19c9696999283a3c82e95c756a3c", size = 1344910, upload-time = "2026-05-21T21:23:39.636Z" }, -] - -[[package]] -name = "opentelemetry-api" -version = "1.42.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b4/1c/125e1c936c0873796771b7f04f6c93b9f1bf5d424cea90fda94a99f61da8/opentelemetry_api-1.42.1.tar.gz", hash = "sha256:56c63bea9f77b62856be8c47600474acad853b2924b99b1687c4cb6297166716", size = 72296, upload-time = "2026-05-21T16:32:49.335Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/ca/9520cc1f3dfbbd03ac5903bbf55833e257bc64b1cf30fa8b0d6df374d821/opentelemetry_api-1.42.1-py3-none-any.whl", hash = "sha256:51a69edacadbc03a8950ace1c4c21099cacc538820ac2c9e36277e78cebba714", size = 61311, upload-time = "2026-05-21T16:32:28.822Z" }, -] - [[package]] name = "orjson" version = "3.11.9" @@ -2992,27 +2751,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/51/be/6f79d55816d5c22557cf27533543d5d70dfe692adfbee4b99f2760674f38/pyarrow-24.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:c91d00057f23b8d353039520dc3a6c09d8608164c692e9f59a175a42b2ae0c19", size = 28131282, upload-time = "2026-04-21T10:51:16.815Z" }, ] -[[package]] -name = "pyasn1" -version = "0.6.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" }, -] - -[[package]] -name = "pyasn1-modules" -version = "0.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyasn1" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, -] - [[package]] name = "pycountry" version = "26.2.16" @@ -3046,24 +2784,6 @@ 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-ai-slim" -version = "1.102.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "genai-prices" }, - { name = "griffelib" }, - { name = "httpx" }, - { name = "opentelemetry-api" }, - { name = "pydantic" }, - { name = "pydantic-graph" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e2/3e/14980440e8f0532535e1fbe936fec5f8d8e7bc6cafa81f6f3c51b1884fe5/pydantic_ai_slim-1.102.0.tar.gz", hash = "sha256:0b8f2b70fa2b40efcbd09d341a346934fc4e46622ae281f858c6bfd3d0d3152b", size = 739988, upload-time = "2026-05-23T01:14:32.808Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/2e/089df86adaf904dd97a1b139d29fe728af0e41430d747f5b6315df3b0c1e/pydantic_ai_slim-1.102.0-py3-none-any.whl", hash = "sha256:f9fa9c3fb58a76f85522f78d1037d201b424de46d532263ed780b3730060449f", size = 919311, upload-time = "2026-05-23T01:14:23.464Z" }, -] - [[package]] name = "pydantic-core" version = "2.46.4" @@ -3166,21 +2886,6 @@ wheels = [ { 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 = "pydantic-graph" -version = "1.102.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "httpx" }, - { name = "logfire-api" }, - { name = "pydantic" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/51/37/4265a1a63eddf35a5aa621c9b2355525bdeae3eb59c3954b165fbfe31404/pydantic_graph-1.102.0.tar.gz", hash = "sha256:e285bd7115e4e92676eaf0a5e7e6faa64cda8c4819f67923a118c50666b909ab", size = 62584, upload-time = "2026-05-23T01:14:36.056Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/49/5597c52d50114440047dd4ce4f6505e32ee336f43267639907d1a17648ee/pydantic_graph-1.102.0-py3-none-any.whl", hash = "sha256:b1a28314adc4abca4db02cf095d064782ec5712e0847ce7a6b79a3c84bf1fc01", size = 80100, upload-time = "2026-05-23T01:14:27.583Z" }, -] - [[package]] name = "pydantic-settings" version = "2.14.1" @@ -3511,21 +3216,11 @@ dependencies = [ { name = "ccy" }, { name = "pandas" }, { name = "pydantic" }, - { name = "python-dotenv" }, { name = "scipy" }, { name = "statsmodels" }, ] [package.optional-dependencies] -ai = [ - { name = "asciichartpy" }, - { name = "ccy", extra = ["holidays"] }, - { name = "google-genai" }, - { name = "mcp" }, - { name = "openai" }, - { name = "pydantic-ai-slim" }, - { name = "rich" }, -] book = [ { name = "altair" }, { name = "autodocsumm" }, @@ -3540,10 +3235,12 @@ data = [ dev = [ { name = "black" }, { name = "ghp-import" }, + { name = "hypothesis" }, { name = "isort" }, { name = "mypy" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, + { name = "python-dotenv" }, { name = "ruff" }, { name = "types-python-dateutil" }, ] @@ -3553,58 +3250,51 @@ docs = [ { name = "griffe-pydantic" }, { name = "griffe-typingdoc" }, { name = "kaleido" }, + { name = "mcp" }, { name = "mkdocs-macros-plugin" }, { name = "mkdocs-material" }, { name = "mkdocs-redirects" }, { name = "mkdocstrings", extra = ["python"] }, { name = "plotly" }, { name = "pyarrow" }, + { name = "python-dotenv" }, ] ml = [ { name = "torch" }, ] -[package.dev-dependencies] -dev = [ - { name = "hypothesis" }, -] - [package.metadata] requires-dist = [ { name = "aio-fluid", extras = ["http"], marker = "extra == 'data'", specifier = ">=2.2.6" }, { name = "aio-fluid", extras = ["http", "log", "k8s"], marker = "extra == 'docs'", specifier = ">=2.2.6" }, { name = "altair", marker = "extra == 'book'", specifier = ">=6.0.0" }, - { name = "asciichartpy", marker = "extra == 'ai'", specifier = ">=1.5.25" }, { name = "autodocsumm", marker = "extra == 'book'", specifier = ">=0.2.14" }, { name = "black", marker = "extra == 'dev'", specifier = ">=26.3.1" }, { name = "ccy", specifier = ">=2.0.0" }, - { name = "ccy", extras = ["holidays"], marker = "extra == 'ai'", specifier = ">=2.0.0" }, { name = "duckdb", marker = "extra == 'book'", specifier = ">=1.4.4" }, - { name = "fastapi", marker = "extra == 'docs'", specifier = ">=0.129.0" }, + { name = "fastapi", marker = "extra == 'docs'", specifier = ">=0.136.3" }, { name = "ghp-import", marker = "extra == 'dev'", specifier = ">=2.0.2" }, - { name = "google-genai", marker = "extra == 'ai'", specifier = ">=1.61.0" }, { name = "griffe-pydantic", marker = "extra == 'docs'", specifier = ">=1.1.0" }, { name = "griffe-typingdoc", marker = "extra == 'docs'", specifier = ">=0.2.7" }, + { name = "hypothesis", marker = "extra == 'dev'", specifier = ">=6.152.2" }, { name = "isort", marker = "extra == 'dev'", specifier = ">=8.0.0" }, { name = "kaleido", marker = "extra == 'docs'", specifier = ">=1.2.0" }, { name = "marimo", marker = "extra == 'book'", specifier = ">=0.23.7" }, - { name = "mcp", marker = "extra == 'ai'", specifier = ">=1.26.0" }, + { name = "mcp", marker = "extra == 'docs'", specifier = ">=1.26.0" }, { name = "mkdocs-macros-plugin", marker = "extra == 'docs'", specifier = ">=1.3.7" }, { name = "mkdocs-material", marker = "extra == 'docs'", specifier = ">=9.7.0" }, { name = "mkdocs-redirects", marker = "extra == 'docs'", specifier = ">=1.2.1" }, { name = "mkdocstrings", extras = ["python"], marker = "extra == 'docs'", specifier = "==1.0.0" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.14.1" }, - { name = "openai", marker = "extra == 'ai'", specifier = ">=2.16.0" }, { name = "pandas", specifier = ">=2.0.0" }, { name = "plotly", marker = "extra == 'book'", specifier = ">=6.2.0" }, { name = "plotly", marker = "extra == 'docs'", specifier = ">=6.2.0" }, { name = "pyarrow", marker = "extra == 'docs'", specifier = ">=19.0.0" }, { name = "pydantic", specifier = ">=2.0.2" }, - { name = "pydantic-ai-slim", marker = "extra == 'ai'", specifier = ">=1.51.0" }, { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=1.0.0" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=7.0.0" }, - { name = "python-dotenv", specifier = ">=1.0.1" }, - { name = "rich", marker = "extra == 'ai'", specifier = ">=13.9.4" }, + { name = "python-dotenv", marker = "extra == 'dev'", specifier = ">=1.0.1" }, + { name = "python-dotenv", marker = "extra == 'docs'", specifier = ">=1.0.1" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15.4" }, { name = "scipy", specifier = ">=1.14.1" }, { name = "statsmodels", specifier = ">=0.14.6,<0.15.0" }, @@ -3612,10 +3302,7 @@ requires-dist = [ { name = "torch", marker = "extra == 'ml'", specifier = ">=2.10.0", index = "https://download.pytorch.org/whl/cu126" }, { name = "types-python-dateutil", marker = "extra == 'dev'", specifier = ">=2.9.0.20251115" }, ] -provides-extras = ["ai", "book", "data", "dev", "docs", "ml"] - -[package.metadata.requires-dev] -dev = [{ name = "hypothesis", specifier = ">=6.152.2" }] +provides-extras = ["book", "data", "dev", "docs", "ml"] [[package]] name = "redis" @@ -3676,19 +3363,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, ] -[[package]] -name = "rich" -version = "15.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown-it-py" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, -] - [[package]] name = "roman-numerals" version = "4.1.0" @@ -3984,22 +3658,13 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] -[[package]] -name = "sniffio" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, -] - [[package]] name = "snowballstemmer" -version = "3.0.1" +version = "3.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } +sdist = { url = "https://files.pythonhosted.org/packages/63/ee/67eef9600338e245ad7838230969a34c823ddbdbccc5e1fc43cd75b55bc9/snowballstemmer-3.1.0.tar.gz", hash = "sha256:fd9e34526b23340cd23ffea6c9f9760974ecc2c2ac9e1d81401443ccdb2a801f", size = 122523, upload-time = "2026-05-24T19:04:19.691Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/49/83/ddbf4533c62dd32667ef1238952abef155f3d3391f5be69a352ad1638a42/snowballstemmer-3.1.0-py3-none-any.whl", hash = "sha256:17e6d1da216aa07db6dad37139ea70cf13c4b2e9a096f6e64a9648fc657d3154", size = 104550, upload-time = "2026-05-24T19:04:18.026Z" }, ] [[package]] @@ -4223,15 +3888,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, ] -[[package]] -name = "tenacity" -version = "9.1.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, -] - [[package]] name = "termcolor" version = "3.3.0" @@ -4354,18 +4010,6 @@ wheels = [ { url = "https://download-r2.pytorch.org/whl/cu126/torch-2.12.0%2Bcu126-cp314-cp314t-win_amd64.whl", hash = "sha256:ec030dd30287eda9338ee401dcca722758862337bd0cdb904325b8eeba5c2694", upload-time = "2026-05-12T23:42:43Z" }, ] -[[package]] -name = "tqdm" -version = "4.67.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, -] - [[package]] name = "triton" version = "3.7.0" @@ -4435,15 +4079,15 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.47.0" +version = "0.48.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f6/b1/8e7077a8641086aea449e1b5752a570f1b5906c64e0a33cd6d93b63a066b/uvicorn-0.47.0.tar.gz", hash = "sha256:7c9a0ea1a9414106bbab7324609c162d8fa0cdcdcb703060987269d77c7bb533", size = 90582, upload-time = "2026-05-14T18:16:54.455Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/bf/f6544ba992ddb9a6077343a576f9844f7f8f06ab819aefd00206e9255f18/uvicorn-0.48.0.tar.gz", hash = "sha256:a5504207195d08c2511bf9125ede5ac4a4b71725d519e758d01dcf0bc2d31c37", size = 91074, upload-time = "2026-05-24T12:08:41.925Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/41/ac2dfdbc1f60c7af4f994c7a335cfa7040c01642b605d65f611cecc2a1e4/uvicorn-0.47.0-py3-none-any.whl", hash = "sha256:2c5715bc12d1892d84752049f400cd1c3cb018514967fdfeb97640443a6a9432", size = 71301, upload-time = "2026-05-14T18:16:51.762Z" }, + { url = "https://files.pythonhosted.org/packages/01/be/72532be3da7acc5fdfbccdb95215cd04f995a0886532a5b423f929cda4cc/uvicorn-0.48.0-py3-none-any.whl", hash = "sha256:48097851328b87ec36117d3d575234519eb58c2b22d79666e9bbc6c49a761dad", size = 71410, upload-time = "2026-05-24T12:08:40.258Z" }, ] [[package]]