diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 543e31a..615295a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,30 @@ on: branches: [main, master, dev] jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - uses: astral-sh/setup-uv@v4 + with: + enable-cache: true + - name: Sync (locked) dev environment + run: uv sync --frozen --extra dev + - name: Ruff (check + format) + run: | + uv run ruff check oxyroute tests examples + uv run ruff format --check oxyroute tests examples + - name: rustfmt + clippy + run: | + cargo fmt --all -- --check + cargo clippy --all-targets -- -D warnings + test: strategy: fail-fast: false @@ -23,17 +47,28 @@ jobs: - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - # 3.14 may be pre-release on some runners until GA; keep CI green across the full range. allow-prerelease: true - - name: Install build deps - run: | - python -m pip install --upgrade pip - python -m pip install maturin pytest oxyjwt httpx granian - - name: Build and install oxyroute + - uses: astral-sh/setup-uv@v4 + with: + enable-cache: true + - name: Install locked deps, build and install oxyroute run: | - maturin build --release - python -m pip install target/wheels/oxyroute-*.whl + set -euo pipefail + uv sync --frozen --extra dev + # `uv sync` can leave a prior wheel; `maturin build` adds another (different platform tag). + # `uv pip install` with two file URLs errors with "conflicting URLs for package oxyroute". + rm -f target/wheels/oxyroute-*.whl + uv run maturin build --release + shopt -s nullglob + wheels=(target/wheels/oxyroute-*.whl) + shopt -u nullglob + if [ "${#wheels[@]}" -ne 1 ] || [ ! -f "${wheels[0]}" ]; then + echo "expected exactly one oxyroute wheel, got: ${#wheels[@]}" + ls -la target/wheels/ 2>/dev/null || true + exit 1 + fi + uv pip install --force-reinstall "${wheels[0]}" - name: Pytest (isolated dir so repo tree does not shadow the installed package) run: | cd "$(mktemp -d)" - python -m pytest "$GITHUB_WORKSPACE/tests" -v + UV_PROJECT="$GITHUB_WORKSPACE" uv run python -m pytest "$GITHUB_WORKSPACE/tests" -v diff --git a/docs/dependencies.md b/docs/dependencies.md index 3564006..8b0340c 100644 --- a/docs/dependencies.md +++ b/docs/dependencies.md @@ -2,7 +2,11 @@ [← Documentation index](index.md) -OxyRoute supports a **linear** list of **named** dependency factories. At request time, each factory is called in order; its return value is injected into the route handler as a **keyword argument** with the given name. +OxyRoute supports a **linear** list of **named** dependency factories. At request time, each factory is called in order; its return value is injected into the route handler as a **keyword argument** with the given name. Factories that appear **later** in the list are called with **keyword arguments** for every **earlier** name and value (so a factory can depend on a previous one by using the same parameter name, e.g. `def b(a: int): …` when the first tuple is `("a", make_a)`). + +### Request context (optional) + +If a factory’s signature includes a parameter named `request`, the extension passes a **dict** (once per request, shared) with string keys: `method`, `path`, `query_string`, and `headers` (a flat `str` → `str` map, when the underlying RSGI scope exposes headers—see the ASGI bridge in `oxyroute.asgi`). Factories that do **not** declare `request` are still called with **no** extra arguments when they have no prior dependencies, preserving older behavior. ## Declaring on a route @@ -17,7 +21,9 @@ def list_items(db: str) -> str: return f"ok {db}" ``` -`factory` can be **sync** or **async** (the extension detects `async` factories and awaits them in order). +`factory` can be **sync** or **async** (the extension detects `async` factories and awaits them in order). Dependency **names** must be **unique** in the list. + +**Example (chaining):** `dependencies=[("a", make_a), ("b", make_b)]` with `def make_b(a): return a + 1` — the second callable receives the value bound to `a`. ## `Depends` marker diff --git a/docs/handlers.md b/docs/handlers.md index 9a78850..93e9ef0 100644 --- a/docs/handlers.md +++ b/docs/handlers.md @@ -19,7 +19,7 @@ handler(**kwargs) | `json` | `read_json_body` is true and body parses as JSON | `dict`/list/values as converted from `serde_json` to Python | | `body` | Raw body bytes, when JSON is not used or empty | `bytes` | | `claims` | `require_jwt` is true and the JWT validates | The decoded JSON claims as a Python object (typically a `dict`) | -| Named dependencies | `dependencies=[("name", factory), ...]` | Return value of each factory, in order (see [dependencies.md](dependencies.md)) | +| Named dependencies | `dependencies=[("name", factory), ...]` | Return value of each factory, in order (see [dependencies.md](dependencies.md)). Only dependencies whose **names** appear on the route handler’s signature (or `**kwargs`) are passed to the handler—intermediate-only dependencies are not forwarded | **JWT:** if `require_jwt` is set but validation fails, the **handler is not called**; the response is 401 (or a dedicated “Expired” string for expired signature when applicable). See [jwt.md](jwt.md). diff --git a/oxyroute/__init__.py b/oxyroute/__init__.py index ddf7989..365c39f 100644 --- a/oxyroute/__init__.py +++ b/oxyroute/__init__.py @@ -1,9 +1,8 @@ # Native extension first — prevents circular import with `app` importing `_oxyroute`. import oxyroute._oxyroute # noqa: F401 - -from oxyroute.app import App, Depends from oxyroute._oxyroute import decode_jwt_hs +from oxyroute.app import App, Depends from oxyroute.response import Response -__all__ = ["App", "Depends", "Response", "decode_jwt_hs", "__version__"] +__all__ = ["App", "Depends", "Response", "__version__", "decode_jwt_hs"] __version__ = "0.1.0" diff --git a/oxyroute/app.py b/oxyroute/app.py index 3530ab8..6b76143 100644 --- a/oxyroute/app.py +++ b/oxyroute/app.py @@ -1,12 +1,13 @@ from __future__ import annotations -from typing import Any, Callable, List, Optional, Tuple, TypeVar, Union +from collections.abc import Callable +from typing import Any, TypeVar from . import _oxyroute from .asgi import build_asgi_caller F = TypeVar("F", bound=Callable[..., Any]) -Dep = Union[Callable[..., Any], _oxyroute.PyDepends] +Dep = Callable[..., Any] | _oxyroute.PyDepends def _unwrap_dep(f: Dep) -> Any: @@ -16,8 +17,8 @@ def _unwrap_dep(f: Dep) -> Any: def _norm_dependencies( - deps: Optional[List[Tuple[str, Dep]]], -) -> Optional[List[Tuple[str, Any]]]: + deps: list[tuple[str, Dep]] | None, +) -> list[tuple[str, Any]] | None: if not deps: return None return [(n, _unwrap_dep(c)) for n, c in deps] @@ -53,12 +54,12 @@ def get( path: str, *, require_jwt: bool = False, - jwt_secret: Optional[str] = None, - algorithms: Optional[List[str]] = None, - jwt_issuer: Optional[str] = None, - jwt_audience: Optional[str] = None, - jwt_leeway: Optional[int] = None, - dependencies: Optional[List[Tuple[str, Dep]]] = None, + jwt_secret: str | None = None, + algorithms: list[str] | None = None, + jwt_issuer: str | None = None, + jwt_audience: str | None = None, + jwt_leeway: int | None = None, + dependencies: list[tuple[str, Dep]] | None = None, ) -> Callable[[F], F]: return self._route( "GET", @@ -78,13 +79,13 @@ def post( path: str, *, require_jwt: bool = False, - jwt_secret: Optional[str] = None, - algorithms: Optional[List[str]] = None, + jwt_secret: str | None = None, + algorithms: list[str] | None = None, read_json_body: bool = True, - jwt_issuer: Optional[str] = None, - jwt_audience: Optional[str] = None, - jwt_leeway: Optional[int] = None, - dependencies: Optional[List[Tuple[str, Dep]]] = None, + jwt_issuer: str | None = None, + jwt_audience: str | None = None, + jwt_leeway: int | None = None, + dependencies: list[tuple[str, Dep]] | None = None, ) -> Callable[[F], F]: return self._route( "POST", @@ -104,12 +105,12 @@ def put( path: str, *, require_jwt: bool = False, - jwt_secret: Optional[str] = None, - algorithms: Optional[List[str]] = None, - jwt_issuer: Optional[str] = None, - jwt_audience: Optional[str] = None, - jwt_leeway: Optional[int] = None, - dependencies: Optional[List[Tuple[str, Dep]]] = None, + jwt_secret: str | None = None, + algorithms: list[str] | None = None, + jwt_issuer: str | None = None, + jwt_audience: str | None = None, + jwt_leeway: int | None = None, + dependencies: list[tuple[str, Dep]] | None = None, ) -> Callable[[F], F]: return self._route( "PUT", @@ -129,13 +130,13 @@ def patch( path: str, *, require_jwt: bool = False, - jwt_secret: Optional[str] = None, - algorithms: Optional[List[str]] = None, + jwt_secret: str | None = None, + algorithms: list[str] | None = None, read_json_body: bool = True, - jwt_issuer: Optional[str] = None, - jwt_audience: Optional[str] = None, - jwt_leeway: Optional[int] = None, - dependencies: Optional[List[Tuple[str, Dep]]] = None, + jwt_issuer: str | None = None, + jwt_audience: str | None = None, + jwt_leeway: int | None = None, + dependencies: list[tuple[str, Dep]] | None = None, ) -> Callable[[F], F]: return self._route( "PATCH", @@ -155,12 +156,12 @@ def delete( path: str, *, require_jwt: bool = False, - jwt_secret: Optional[str] = None, - algorithms: Optional[List[str]] = None, - jwt_issuer: Optional[str] = None, - jwt_audience: Optional[str] = None, - jwt_leeway: Optional[int] = None, - dependencies: Optional[List[Tuple[str, Dep]]] = None, + jwt_secret: str | None = None, + algorithms: list[str] | None = None, + jwt_issuer: str | None = None, + jwt_audience: str | None = None, + jwt_leeway: int | None = None, + dependencies: list[tuple[str, Dep]] | None = None, ) -> Callable[[F], F]: return self._route( "DELETE", @@ -180,14 +181,14 @@ def _route( method: str, path: str, require_jwt: bool, - jwt_secret: Optional[str], - algorithms: Optional[List[str]], + jwt_secret: str | None, + algorithms: list[str] | None, read_json_body: bool, - dependencies: Optional[List[Tuple[str, Dep]]], + dependencies: list[tuple[str, Dep]] | None, *, - jwt_issuer: Optional[str] = None, - jwt_audience: Optional[str] = None, - jwt_leeway: Optional[int] = None, + jwt_issuer: str | None = None, + jwt_audience: str | None = None, + jwt_leeway: int | None = None, ) -> Callable[[F], F]: dlist = _norm_dependencies(dependencies) @@ -209,11 +210,11 @@ def wrap(handler: F) -> F: return wrap - async def __rsgi_init__(self, *args: Any, **kwargs: Any) -> None: # noqa: D401 + async def __rsgi_init__(self, *args: Any, **kwargs: Any) -> None: """Lifespan hook (no-op). Granian may pass extra positional args; accept **kwargs.""" return None - async def __rsgi_del__(self, *args: Any, **kwargs: Any) -> None: # noqa: D401 + async def __rsgi_del__(self, *args: Any, **kwargs: Any) -> None: """Lifespan teardown (no-op). Accept extra args for Granian compatibility.""" return None diff --git a/oxyroute/asgi.py b/oxyroute/asgi.py index 69d0efb..7bca951 100644 --- a/oxyroute/asgi.py +++ b/oxyroute/asgi.py @@ -8,11 +8,12 @@ from __future__ import annotations import asyncio -from typing import Any, Callable, Dict, List, Optional, Tuple +from collections.abc import Callable +from typing import Any -def _hdr_from_asgi(raw: List[Tuple[bytes, bytes]]) -> "_HeaderView": - d: Dict[str, str] = {} +def _hdr_from_asgi(raw: list[tuple[bytes, bytes]]) -> _HeaderView: + d: dict[str, str] = {} for k, v in raw: dk = k.decode("latin-1").lower() d[dk] = v.decode("latin-1") @@ -22,7 +23,7 @@ def _hdr_from_asgi(raw: List[Tuple[bytes, bytes]]) -> "_HeaderView": class _HeaderView: __slots__ = ("_d",) - def __init__(self, d: Dict[str, str]) -> None: + def __init__(self, d: dict[str, str]) -> None: self._d = d def get(self, k: str, default: str = "") -> str: @@ -31,17 +32,17 @@ def get(self, k: str, default: str = "") -> str: class _RsgiScope: __slots__ = ( - "proto", + "authority", + "client", + "headers", + "http_version", "method", "path", + "proto", "query_string", - "headers", "rsgi_version", - "http_version", - "server", - "client", "scheme", - "authority", + "server", ) def __init__( @@ -62,13 +63,13 @@ def __init__( self.path = path self.query_string = query_string self.headers = headers - self.authority: Optional[str] = None + self.authority: str | None = None def _norm_headers_asgi( rsgi_headers: list, -) -> List[Tuple[bytes, bytes]]: - out: List[Tuple[bytes, bytes]] = [] +) -> list[tuple[bytes, bytes]]: + out: list[tuple[bytes, bytes]] = [] for p in rsgi_headers: if not isinstance(p, (list, tuple)) or len(p) != 2: continue @@ -83,13 +84,13 @@ def _norm_headers_asgi( class _RsgiProtocol: - __slots__ = ("_body", "_send", "_loop", "status", "_status") + __slots__ = ("_body", "_loop", "_send", "_status", "status") def __init__(self, body: bytes, send: Any, main_loop: asyncio.AbstractEventLoop) -> None: self._body = body self._send = send self._loop = main_loop - self._status: Optional[int] = None + self._status: int | None = None self.status = 200 def _run_send(self, coro: Any) -> None: @@ -171,7 +172,7 @@ async def _go() -> None: async def asgi_to_rsgi( app_rsgi: Callable[[Any, Any], Any], - scope: Dict[str, Any], + scope: dict[str, Any], receive: Any, send: Any, ) -> None: @@ -220,7 +221,7 @@ def _rsgi(s: Any, p: Any) -> Any: inner = getattr(c, "_app", c) return inner.handle_rsgi(s, p) - async def asgi3(scope: Dict[str, Any], receive: Any, send: Any) -> None: + async def asgi3(scope: dict[str, Any], receive: Any, send: Any) -> None: await asgi_to_rsgi(_rsgi, scope, receive, send) return asgi3 diff --git a/oxyroute/response.py b/oxyroute/response.py index 26977f0..cd8e9b3 100644 --- a/oxyroute/response.py +++ b/oxyroute/response.py @@ -1,7 +1,8 @@ from __future__ import annotations +from collections.abc import Mapping, Sequence from dataclasses import dataclass -from typing import Any, Mapping, Sequence +from typing import Any __all__ = ["Response"] diff --git a/pyproject.toml b/pyproject.toml index aafa11c..28b5147 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,10 +19,12 @@ Issues = "https://github.com/QueryaHub/OxyRoute/issues" [project.optional-dependencies] dev = [ + "maturin>=1.4,<2", "granian>=1.0", "pytest>=8", "httpx>=0.27", "oxyjwt>=0.2", + "ruff>=0.8", ] [tool.maturin] @@ -30,3 +32,24 @@ module-name = "oxyroute._oxyroute" [tool.pytest.ini_options] testpaths = ["tests"] + +[tool.ruff] +line-length = 100 +target-version = "py310" +src = ["oxyroute", "tests", "examples"] + +[tool.ruff.lint] +select = [ + "E4", + "E7", + "E9", + "F", + "I", + "B", + "UP", + "RUF", +] + +[tool.ruff.format] +quote-style = "double" +docstring-code-format = true diff --git a/src/dispatch.rs b/src/dispatch.rs index 897bbb8..6a85fdd 100644 --- a/src/dispatch.rs +++ b/src/dispatch.rs @@ -3,12 +3,12 @@ use std::sync::Arc; use std::sync::Mutex; use jsonwebtoken::errors::ErrorKind; -use jsonwebtoken::{DecodingKey, Validation, decode}; +use jsonwebtoken::{decode, DecodingKey, Validation}; use pyo3::prelude::*; use pyo3::types::{PyBytes, PyDict, PyList}; use serde_json::Value as JsonValue; -use crate::params::{header_get_lax, parse_query, value_for_path_param}; +use crate::params::{build_request_context, header_get_lax, parse_query, value_for_path_param}; use crate::response; use crate::schema::json_to_py; use crate::state::{map_method_router, AppState}; @@ -45,13 +45,7 @@ async fn send_internal_error( } else { r#"{"error":"internal server error"}"#.to_string() }; - response::send_text( - protocol, - 500, - &body, - "application/json; charset=utf-8", - ) - .await + response::send_text(protocol, 500, &body, "application/json; charset=utf-8").await } pub async fn run_rsgi( @@ -63,18 +57,19 @@ pub async fn run_rsgi( if proto != "http" { return Ok(Python::with_gil(|py| py.None())); } - let (method, path, query_string) = Python::with_gil(|py| -> PyResult<(String, String, String)> { - let s = scope.bind(py); - let qs: String = s - .getattr("query_string") - .and_then(|x| x.extract()) - .unwrap_or_default(); - Ok(( - s.getattr("method")?.extract()?, - s.getattr("path")?.extract()?, - qs, - )) - })?; + let (method, path, query_string) = + Python::with_gil(|py| -> PyResult<(String, String, String)> { + let s = scope.bind(py); + let qs: String = s + .getattr("query_string") + .and_then(|x| x.extract()) + .unwrap_or_default(); + Ok(( + s.getattr("method")?.extract()?, + s.getattr("path")?.extract()?, + qs, + )) + })?; if method == "GET" && path == "/openapi.json" { let (inc, doc) = { let st = state.lock().map_err(crate::lock_err)?; @@ -86,13 +81,8 @@ pub async fn run_rsgi( } }; if inc { - return response::send_str( - &protocol, - 200, - &doc, - "application/json; charset=utf-8", - ) - .await; + return response::send_str(&protocol, 200, &doc, "application/json; charset=utf-8") + .await; } } let read_fut = Python::with_gil(|py| { @@ -118,7 +108,7 @@ pub async fn run_rsgi( })?; let route_out: Option<(usize, HashMap)> = (|| -> PyResult<_> { let st = state.lock().map_err(crate::lock_err)?; - let g = map_method_router(&*st, &method) + let g = map_method_router(&st, &method) .ok_or_else(|| pyo3::exceptions::PyValueError::new_err("method"))?; Ok(g.at(&path).ok().map(|m| { let mut pmap = HashMap::new(); @@ -131,13 +121,8 @@ pub async fn run_rsgi( let (route_idx, param_map) = match route_out { Some(x) => x, None => { - return response::send_text( - &protocol, - 404, - "Not Found", - "text/plain; charset=utf-8", - ) - .await + return response::send_text(&protocol, 404, "Not Found", "text/plain; charset=utf-8") + .await } }; let ( @@ -153,6 +138,9 @@ pub async fn run_rsgi( dep_names, dep_factories, dep_is_async, + dep_wants_request, + handler_param_names, + handler_varkw, ) = { let st = state.lock().map_err(crate::lock_err)?; let e = st @@ -172,6 +160,9 @@ pub async fn run_rsgi( e.dep_names.clone(), e.dep_factories.clone(), e.dep_is_async.clone(), + e.dep_wants_request.clone(), + e.handler_param_names.clone(), + e.handler_varkw, ) }; let mut claims_val: Option = None; @@ -237,7 +228,7 @@ pub async fn run_rsgi( "Expired", "text/plain; charset=utf-8", ) - .await + .await; } return response::send_text( &protocol, @@ -245,7 +236,7 @@ pub async fn run_rsgi( "Unauthorized", "text/plain; charset=utf-8", ) - .await + .await; } } } @@ -272,11 +263,40 @@ pub async fn run_rsgi( } else { None }; + let need_req_ctx = dep_wants_request.iter().any(|&x| x); + let request_ctx: Option> = if need_req_ctx { + match Python::with_gil(|py| -> PyResult> { + let s = scope.bind(py); + let d = build_request_context(py, s, &method, &path, &query_string)?; + Ok(d.unbind().into()) + }) { + Ok(o) => Some(o), + Err(e) => { + return send_internal_error(&protocol, &method, &path, e).await; + } + } + } else { + None + }; let mut dep_out: Vec = Vec::with_capacity(dep_factories.len()); for (i, fact) in dep_factories.iter().enumerate() { if dep_is_async.get(i) == Some(&true) { let r = match Python::with_gil(|py| -> PyResult { - Ok(fact.bind(py).call((), None)?.unbind()) + let kw = PyDict::new_bound(py); + if dep_wants_request.get(i) == Some(&true) { + if let Some(ref rc) = request_ctx { + kw.set_item("request", rc.bind(py))?; + } + } + for j in 0..i { + kw.set_item(dep_names[j].as_str(), dep_out[j].bind(py))?; + } + let f = fact.bind(py); + if kw.is_empty() { + Ok(f.call((), None)?.unbind()) + } else { + Ok(f.call((), Some(&kw))?.unbind()) + } }) { Ok(x) => x, Err(e) => { @@ -301,7 +321,21 @@ pub async fn run_rsgi( dep_out.push(o); } else { let o = match Python::with_gil(|py| -> PyResult { - Ok(fact.bind(py).call((), None)?.unbind()) + let kw = PyDict::new_bound(py); + if dep_wants_request.get(i) == Some(&true) { + if let Some(ref rc) = request_ctx { + kw.set_item("request", rc.bind(py))?; + } + } + for j in 0..i { + kw.set_item(dep_names[j].as_str(), dep_out[j].bind(py))?; + } + let f = fact.bind(py); + if kw.is_empty() { + Ok(f.call((), None)?.unbind()) + } else { + Ok(f.call((), Some(&kw))?.unbind()) + } }) { Ok(x) => x, Err(e) => { @@ -326,7 +360,9 @@ pub async fn run_rsgi( } for (i, name) in dep_names.iter().enumerate() { if let Some(oo) = dep_out.get(i) { - kwargs.set_item(name, oo.bind(py))?; + if handler_varkw || handler_param_names.contains(name) { + kwargs.set_item(name, oo.bind(py))?; + } } } if let Some(c) = claims_val { @@ -340,10 +376,7 @@ pub async fn run_rsgi( if !body_bytes.is_empty() && body_json.is_none() { kwargs.set_item("body", PyBytes::new_bound(py, &body_bytes))?; } - let res = handler - .bind(py) - .call((), Some(&kwargs))? - .unbind(); + let res = handler.bind(py).call((), Some(&kwargs))?.unbind(); Ok((res, is_async)) }) { Ok(x) => x, @@ -377,12 +410,16 @@ pub async fn run_rsgi( } }; match mapped { - HandlerMap::WithHeaders { status, body, headers } => { - response::send_with_headers(&protocol, status, &body, headers).await - } - HandlerMap::Simple { status, body, content_type } => { - response::send_bytes(&protocol, status, &body, &content_type).await - } + HandlerMap::WithHeaders { + status, + body, + headers, + } => response::send_with_headers(&protocol, status, &body, headers).await, + HandlerMap::Simple { + status, + body, + content_type, + } => response::send_bytes(&protocol, status, &body, &content_type).await, } } @@ -475,21 +512,12 @@ fn is_oxyroute_response(_py: Python<'_>, b: &Bound<'_, PyAny>) -> PyResult Ok(b.hasattr("status")? && b.hasattr("body")? && b.hasattr("headers")?) } -fn structured_from_response_attrs( - py: Python<'_>, - b: &Bound<'_, PyAny>, -) -> PyResult { +fn structured_from_response_attrs(py: Python<'_>, b: &Bound<'_, PyAny>) -> PyResult { let st = b.getattr("status")?; let body = b.getattr("body")?; let headers = b.getattr("headers")?; let cookies = b.getattr("cookies")?; - structured_from_status_body( - py, - &st, - &body, - Some(headers), - Some(cookies), - ) + structured_from_status_body(py, &st, &body, Some(headers), Some(cookies)) } fn structured_from_status_body( @@ -527,22 +555,20 @@ fn structured_from_status_body( } } } - Ok(HandlerMap::WithHeaders { status, body, headers: pairs }) + Ok(HandlerMap::WithHeaders { + status, + body, + headers: pairs, + }) } /// JSON body, etc. fn value_to_bytes_and_ct(py: Python<'_>, b: &Bound<'_, PyAny>) -> PyResult<(Vec, String)> { if b.is_none() { - return Ok(( - Vec::new(), - "text/plain; charset=utf-8".to_string(), - )); + return Ok((Vec::new(), "text/plain; charset=utf-8".to_string())); } if let Ok(s) = b.extract::() { - return Ok(( - s.into_bytes(), - "text/plain; charset=utf-8".to_string(), - )); + return Ok((s.into_bytes(), "text/plain; charset=utf-8".to_string())); } if let Ok(s) = b.extract::<&str>() { return Ok(( @@ -551,10 +577,7 @@ fn value_to_bytes_and_ct(py: Python<'_>, b: &Bound<'_, PyAny>) -> PyResult<(Vec< )); } if let Ok(buf) = b.extract::>() { - return Ok(( - buf, - "application/octet-stream".to_string(), - )); + return Ok((buf, "application/octet-stream".to_string())); } let jmod = py.import_bound("json")?; let dumped = jmod.call_method1("dumps", (b.clone().unbind(),))?; diff --git a/src/lib.rs b/src/lib.rs index 703230f..ccf8d39 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,9 @@ +use std::collections::HashSet; use std::sync::Arc; use std::sync::Mutex; use pyo3::prelude::*; -use pyo3::types::PyList; +use pyo3::types::{PyDict, PyList, PyTuple}; use serde_json::json; mod dispatch; @@ -15,10 +16,36 @@ mod token; use dispatch::run_rsgi; use state::AppState; +type ParsedDependencies = (Vec, Vec>, Vec, Vec); + pub(crate) fn lock_err(e: std::sync::PoisonError) -> PyErr { pyo3::exceptions::PyRuntimeError::new_err(e.to_string()) } +/// Parameter names the route handler accepts, plus whether it has `**kwargs`. +fn handler_signature_kinds( + py: Python<'_>, + handler: &Bound<'_, PyAny>, +) -> PyResult<(HashSet, bool)> { + let d = PyDict::new_bound(py); + d.set_item("f", handler)?; + py.run_bound( + "import inspect\n_s = inspect.signature(f)\n_p = []\nfor _n, _param in _s.parameters.items():\n if _param.kind in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY):\n _p.append(_n)\n_w = False\nfor _x in _s.parameters.values():\n if _x.kind == inspect.Parameter.VAR_KEYWORD:\n _w = True\n break", + None, + Some(&d), + )?; + let pl = d + .get_item("_p")? + .ok_or_else(|| pyo3::exceptions::PyRuntimeError::new_err("handler _p"))?; + let v: Vec = pl.extract()?; + let names: HashSet = v.into_iter().collect(); + let w: bool = d + .get_item("_w")? + .ok_or_else(|| pyo3::exceptions::PyRuntimeError::new_err("handler _w"))? + .extract()?; + Ok((names, w)) +} + fn parse_algorithm(s: &str) -> PyResult { match s { "HS256" => Ok(jsonwebtoken::Algorithm::HS256), @@ -30,32 +57,49 @@ fn parse_algorithm(s: &str) -> PyResult { } } -fn parse_dependencies( - py: Python<'_>, - dep_list: &Bound, -) -> PyResult<(Vec, Vec>, Vec)> { +fn parse_dependencies(py: Python<'_>, dep_list: &Bound) -> PyResult { let inspect = py.import_bound("inspect")?; let iscoro = inspect.getattr("iscoroutinefunction")?; let n = dep_list.len(); let mut names = Vec::with_capacity(n); let mut facts = Vec::with_capacity(n); let mut asy = Vec::with_capacity(n); + let mut want_req = Vec::with_capacity(n); for i in 0..n { let it = dep_list.get_item(i)?; - let tup = it.downcast::()?; + let tup = it.downcast::()?; if tup.len() != 2 { return Err(pyo3::exceptions::PyValueError::new_err( "each dependency must be (name, callable)", )); } let name: String = tup.get_item(0)?.extract()?; + if names.contains(&name) { + return Err(pyo3::exceptions::PyValueError::new_err( + "duplicate dependency name", + )); + } let f: Py = tup.get_item(1)?.unbind(); let is_a: bool = iscoro.call1((f.clone_ref(py),))?.extract()?; + let f_b = f.bind(py); + let has_req: bool = dependency_wants_request(py, f_b)?; names.push(name); facts.push(f); asy.push(is_a); + want_req.push(has_req); } - Ok((names, facts, asy)) + Ok((names, facts, asy, want_req)) +} + +/// True if the factory declares a `request` parameter (for the request context dict). +fn dependency_wants_request(py: Python<'_>, f: &Bound<'_, PyAny>) -> PyResult { + let inspect = py.import_bound("inspect")?; + let sig = inspect.getattr("signature")?.call1((f,))?; + let params = sig.getattr("parameters")?; + params + .getattr("__contains__")? + .call1((pyo3::types::PyString::new_bound(py, "request"),))? + .extract() } #[pyclass] @@ -114,7 +158,7 @@ impl App { jwt_audience: Option, jwt_leeway: Option, ) -> PyResult<()> { - let st = self.state.lock().map_err(|e| lock_err(e))?; + let st = self.state.lock().map_err(lock_err)?; if st.frozen { return Err(pyo3::exceptions::PyValueError::new_err( "app is frozen; no more add_route", @@ -123,9 +167,7 @@ impl App { drop(st); let inspect = py.import_bound("inspect")?; let f = inspect.getattr("iscoroutinefunction")?; - let is_async: bool = f - .call1((handler.clone_ref(py),))? - .extract()?; + let is_async: bool = f.call1((handler.clone_ref(py),))?.extract()?; let algs: Vec = if let Some(list) = algorithms { let n = list.len(); let mut v = Vec::with_capacity(n); @@ -138,32 +180,32 @@ impl App { vec![jsonwebtoken::Algorithm::HS256] }; if require_jwt { - let need = algs - .iter() - .all(|a| { - matches!( - a, - jsonwebtoken::Algorithm::HS256 - | jsonwebtoken::Algorithm::HS384 - | jsonwebtoken::Algorithm::HS512 - ) - }); + let need = algs.iter().all(|a| { + matches!( + a, + jsonwebtoken::Algorithm::HS256 + | jsonwebtoken::Algorithm::HS384 + | jsonwebtoken::Algorithm::HS512 + ) + }); if need && jwt_secret.is_none() { return Err(pyo3::exceptions::PyValueError::new_err( "require_jwt with HMAC needs jwt_secret", )); } } - let (dep_names, dep_factories, dep_is_async) = if let Some(d) = dependencies { - parse_dependencies(py, &d)? - } else { - (vec![], vec![], vec![]) - }; + let (dep_names, dep_factories, dep_is_async, dep_wants_request) = + if let Some(d) = dependencies { + parse_dependencies(py, &d)? + } else { + (vec![], vec![], vec![], vec![]) + }; let op_id: String = handler .bind(py) .getattr(pyo3::intern!(py, "__name__"))? .extract()?; - let mut st = self.state.lock().map_err(|e| lock_err(e))?; + let (handler_param_names, handler_varkw) = handler_signature_kinds(py, handler.bind(py))?; + let mut st = self.state.lock().map_err(lock_err)?; let idx = st.routes.len(); st.routes.push(state::RouteEntry { handler, @@ -178,14 +220,18 @@ impl App { dep_names, dep_factories, dep_is_async, + dep_wants_request, + handler_param_names, + handler_varkw, }); { - let mut oa = st.openapi.lock().map_err(|e| lock_err(e))?; + let mut oa = st.openapi.lock().map_err(lock_err)?; App::openapi_add_path(&mut oa, &method, &path, &op_id); } { - let mut m = state::map_method_router(&*st, &method) - .ok_or_else(|| pyo3::exceptions::PyValueError::new_err("unsupported HTTP method"))?; + let mut m = state::map_method_router(&st, &method).ok_or_else(|| { + pyo3::exceptions::PyValueError::new_err("unsupported HTTP method") + })?; m.insert(&path, idx) .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?; } @@ -194,20 +240,20 @@ impl App { /// Lock route registration. Linear dependency list is a valid topological order (DAG of independent roots). fn freeze(&self) -> PyResult<()> { - let mut st = self.state.lock().map_err(|e| lock_err(e))?; + let mut st = self.state.lock().map_err(lock_err)?; st.frozen = true; Ok(()) } fn set_openapi_served(&self, enabled: bool) -> PyResult<()> { - let mut st = self.state.lock().map_err(|e| lock_err(e))?; + let mut st = self.state.lock().map_err(lock_err)?; st.include_openapi = enabled; Ok(()) } fn set_openapi_title(&self, title: &str) -> PyResult<()> { - let st = self.state.lock().map_err(|e| lock_err(e))?; - let mut oa = st.openapi.lock().map_err(|e| lock_err(e))?; + let st = self.state.lock().map_err(lock_err)?; + let mut oa = st.openapi.lock().map_err(lock_err)?; if let Some(info) = oa .as_object_mut() .and_then(|m| m.get_mut("info")) @@ -227,12 +273,14 @@ impl App { let state = this.state.clone(); let scope_py: Py = scope.as_any().clone().unbind(); let protocol_py: Py = protocol.as_any().clone().unbind(); - pyo3_asyncio_0_21::tokio::future_into_py(py, async move { run_rsgi(state, scope_py, protocol_py).await }) + pyo3_asyncio_0_21::tokio::future_into_py(py, async move { + run_rsgi(state, scope_py, protocol_py).await + }) } fn openapi_json(&self) -> PyResult { - let st = self.state.lock().map_err(|e| lock_err(e))?; - let oa = st.openapi.lock().map_err(|e| lock_err(e))?; + let st = self.state.lock().map_err(lock_err)?; + let oa = st.openapi.lock().map_err(lock_err)?; Ok(oa.to_string()) } } diff --git a/src/params.rs b/src/params.rs index ecd2dd2..33ef555 100644 --- a/src/params.rs +++ b/src/params.rs @@ -7,6 +7,53 @@ use pyo3::prelude::*; use pyo3::types::PyDict; use pyo3::IntoPy; +/// Context dict passed as the `request` keyword to dependency callables that declare it: +/// `method`, `path`, `query_string`, and `headers` (str → str, lowercased names when from the ASGI bridge). +pub fn build_request_context<'py>( + py: Python<'py>, + scope: &Bound<'py, PyAny>, + method: &str, + path: &str, + query_string: &str, +) -> PyResult> { + let d = PyDict::new_bound(py); + d.set_item("method", method)?; + d.set_item("path", path)?; + d.set_item("query_string", query_string)?; + d.set_item("headers", copy_scope_headers_to_dict(py, scope)?)?; + Ok(d) +} + +/// Best-effort copy of RSGI/ASGI scope `headers` into a `dict` of strings. +fn copy_scope_headers_to_dict<'py>( + py: Python<'py>, + scope: &Bound<'py, PyAny>, +) -> PyResult> { + let out = PyDict::new_bound(py); + let h = match scope.getattr("headers") { + Ok(x) => x, + Err(_) => return Ok(out), + }; + if let Ok(inner) = h.getattr("_d") { + if let Ok(hd) = inner.downcast::() { + for (k, v) in hd.iter() { + let ks: String = k.extract()?; + let vs: String = v.extract()?; + out.set_item(ks, vs)?; + } + return Ok(out); + } + } + if let Ok(hd) = h.downcast::() { + for (k, v) in hd.iter() { + let ks: String = k.extract()?; + let vs: String = v.extract()?; + out.set_item(ks, vs)?; + } + } + Ok(out) +} + /// Parse an HTTP `query` string (the part after `?`, without the `?`). /// Uses [`form_urlencoded`] so keys and values are **percent-decoded** and `+` in values is /// treated as a space, consistent with `application/x-www-form-urlencoded` / URLSearchParams diff --git a/src/response.rs b/src/response.rs index deeab54..755f5b4 100644 --- a/src/response.rs +++ b/src/response.rs @@ -24,8 +24,7 @@ pub async fn send_str( Python::with_gil(|py| { let p = protocol.bind(py); let h = build_headers_ct(py, Some(content_type))?; - p.getattr("response_str")? - .call1((u16::from(status), h, text))?; + p.getattr("response_str")?.call1((status, h, text))?; Ok(pyo3::types::PyNone::get_bound(py).to_object(py)) }) } @@ -38,8 +37,7 @@ pub async fn send_empty( Python::with_gil(|py| { let p = protocol.bind(py); let h = build_headers_ct(py, content_type)?; - p.getattr("response_empty")? - .call1((u16::from(status), h))?; + p.getattr("response_empty")?.call1((status, h))?; Ok(pyo3::types::PyNone::get_bound(py).to_object(py)) }) } @@ -56,8 +54,7 @@ pub async fn send_bytes( Python::with_gil(|py| { let p = protocol.bind(py); let h = build_headers_ct(py, Some(content_type))?; - p.getattr("response_bytes")? - .call1((u16::from(status), h, body))?; + p.getattr("response_bytes")?.call1((status, h, body))?; Ok(pyo3::types::PyNone::get_bound(py).to_object(py)) }) } @@ -75,8 +72,7 @@ pub async fn send_with_headers( Python::with_gil(|py| { let p = protocol.bind(py); let h = build_header_list_from_pairs(py, &headers)?; - p.getattr("response_bytes")? - .call1((u16::from(status), h, body))?; + p.getattr("response_bytes")?.call1((status, h, body))?; Ok(pyo3::types::PyNone::get_bound(py).to_object(py)) }) } @@ -89,8 +85,7 @@ async fn send_empty_with_header_pairs( Python::with_gil(|py| { let p = protocol.bind(py); let h = build_header_list_from_pairs(py, &headers)?; - p.getattr("response_empty")? - .call1((u16::from(status), h))?; + p.getattr("response_empty")?.call1((status, h))?; Ok(pyo3::types::PyNone::get_bound(py).to_object(py)) }) } diff --git a/src/schema.rs b/src/schema.rs index af2c729..f3a4bb6 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -4,11 +4,8 @@ use pyo3::prelude::*; use serde_json::Value as JsonValue; pub fn json_to_py(py: Python<'_>, v: JsonValue) -> PyResult> { - let s = serde_json::to_string(&v).map_err(|e| { - pyo3::exceptions::PyValueError::new_err(e.to_string()) - })?; + let s = serde_json::to_string(&v) + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; let json = py.import_bound("json")?; - Ok(json - .call_method1("loads", (s,))? - .unbind()) + Ok(json.call_method1("loads", (s,))?.unbind()) } diff --git a/src/state.rs b/src/state.rs index 75f2da4..36af248 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,3 +1,4 @@ +use std::collections::HashSet; use std::sync::Mutex; use matchit::Router; @@ -20,6 +21,13 @@ pub struct RouteEntry { pub dep_names: Vec, pub dep_factories: Vec>, pub dep_is_async: Vec, + /// Per factory: pass a `request` context dict (see `build_request_context` in dispatch). + pub dep_wants_request: Vec, + /// From `inspect.signature(handler)`: which parameter names the handler accepts (excluding + /// `*args` / only `*`-only); used to forward only matching dependency results. + pub handler_param_names: HashSet, + /// Handler has `**kwargs` (pass all dependency kwargs). + pub handler_varkw: bool, } pub struct AppState { diff --git a/src/token.rs b/src/token.rs index 1ee76db..811bced 100644 --- a/src/token.rs +++ b/src/token.rs @@ -1,7 +1,7 @@ //! JWT: Bearer extraction and HS* verification (aligned with oxyjwt validation defaults where applicable). use jsonwebtoken::errors::ErrorKind; -use jsonwebtoken::{DecodingKey, Validation, decode}; +use jsonwebtoken::{decode, DecodingKey, Validation}; use pyo3::prelude::*; use pyo3::types::PyList; use serde_json::Value as JsonValue; @@ -61,17 +61,15 @@ pub fn decode_jwt_hs( if v.is_empty() { v.push(jsonwebtoken::Algorithm::HS256); } - let claims = decode_hs_claims(token, key, &v) - .map_err(|e| { - if matches!(e.kind(), ErrorKind::ExpiredSignature) { - pyo3::exceptions::PyValueError::new_err("expired") - } else { - pyo3::exceptions::PyValueError::new_err(e.to_string()) - } - })?; - let s = serde_json::to_string(&claims).map_err(|e| { - pyo3::exceptions::PyValueError::new_err(e.to_string()) + let claims = decode_hs_claims(token, key, &v).map_err(|e| { + if matches!(e.kind(), ErrorKind::ExpiredSignature) { + pyo3::exceptions::PyValueError::new_err("expired") + } else { + pyo3::exceptions::PyValueError::new_err(e.to_string()) + } })?; + let s = serde_json::to_string(&claims) + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; let j = py.import_bound("json")?; Ok(j.call_method1("loads", (s,))?.unbind()) } diff --git a/tests/test_asgi.py b/tests/test_asgi.py index ff5c2d6..0f4c6b1 100644 --- a/tests/test_asgi.py +++ b/tests/test_asgi.py @@ -5,12 +5,12 @@ import asyncio import httpx - from oxyroute import App, Response def test_asgi_get_plain_text() -> None: app = App() + @app.get("/p") def p() -> str: return "x" @@ -29,7 +29,7 @@ def test_asgi_patch_json_body() -> None: app = App() @app.patch("/x") - def patch_x(json: dict) -> str: # noqa: A002 — injected JSON body + def patch_x(json: dict) -> str: return f"p:{json.get('a')}" async def _run() -> None: diff --git a/tests/test_dep_chain.py b/tests/test_dep_chain.py new file mode 100644 index 0000000..04f00a8 --- /dev/null +++ b/tests/test_dep_chain.py @@ -0,0 +1,74 @@ +"""Dependency chains: later factories receive earlier results by name; optional ``request`` context.""" + +from __future__ import annotations + +import asyncio + +import httpx +from oxyroute import App + + +def test_dep_second_receives_first_by_name() -> None: + def make_a() -> int: + return 10 + + def make_b(a: int) -> int: + return a + 5 + + app = App() + + @app.get("/x", dependencies=[("a", make_a), ("b", make_b)]) + def route(b: int) -> str: + return f"v={b}" + + async def run() -> None: + transport = httpx.ASGITransport(app=app) + async with httpx.AsyncClient(transport=transport, base_url="http://test") as c: + r = await c.get("/x") + assert r.status_code == 200 + assert r.text == "v=15" + + asyncio.run(run()) + + +def test_dep_request_context_headers() -> None: + def with_req(request) -> str: + return request["headers"].get("x-trace", "") + + app = App() + + @app.get("/t", dependencies=[("trace_id", with_req)]) + def route(trace_id: str) -> str: + return trace_id + + async def run() -> None: + transport = httpx.ASGITransport(app=app) + async with httpx.AsyncClient(transport=transport, base_url="http://test") as c: + r = await c.get("/t", headers={"X-Trace": "z9"}) + assert r.status_code == 200 + assert r.text == "z9" + + asyncio.run(run()) + + +def test_dep_async_chain() -> None: + async def make_a() -> str: + return "aa" + + def make_b(a: str) -> str: + return a + "b" + + app = App() + + @app.get("/a", dependencies=[("a", make_a), ("b", make_b)]) + def route(b: str) -> str: + return b + + async def run() -> None: + transport = httpx.ASGITransport(app=app) + async with httpx.AsyncClient(transport=transport, base_url="http://test") as c: + r = await c.get("/a") + assert r.status_code == 200 + assert r.text == "aab" + + asyncio.run(run()) diff --git a/tests/test_granian_e2e.py b/tests/test_granian_e2e.py index af6e92c..4495a68 100644 --- a/tests/test_granian_e2e.py +++ b/tests/test_granian_e2e.py @@ -13,7 +13,6 @@ import tempfile import textwrap import time -from typing import List import httpx import pytest @@ -48,7 +47,7 @@ def root() -> str: port = _free_port() env = os.environ.copy() env["PYTHONPATH"] = d - cmd: List[str] = [ + cmd: list[str] = [ sys.executable, "-m", "granian", @@ -87,9 +86,7 @@ def root() -> str: f"granian exited early: code={proc.returncode} stderr={err!r} last={last_err!r}" ) err = proc.stderr.read() if proc.stderr else "" - raise AssertionError( - f"server did not become ready. last={last_err!r} stderr={err!r}" - ) + raise AssertionError(f"server did not become ready. last={last_err!r} stderr={err!r}") finally: proc.terminate() try: diff --git a/tests/test_handler_errors_500.py b/tests/test_handler_errors_500.py index c70411c..557a1bc 100644 --- a/tests/test_handler_errors_500.py +++ b/tests/test_handler_errors_500.py @@ -7,7 +7,6 @@ import os import httpx - from oxyroute import App diff --git a/tests/test_jwt_parity.py b/tests/test_jwt_parity.py index 8773bed..6a47cad 100644 --- a/tests/test_jwt_parity.py +++ b/tests/test_jwt_parity.py @@ -5,7 +5,6 @@ import time import pytest - from oxyroute import decode_jwt_hs oxyjwt = pytest.importorskip("oxyjwt") diff --git a/tests/test_jwt_route_iss_aud.py b/tests/test_jwt_route_iss_aud.py index e7de57b..417a419 100644 --- a/tests/test_jwt_route_iss_aud.py +++ b/tests/test_jwt_route_iss_aud.py @@ -7,7 +7,6 @@ import httpx import pytest - from oxyroute import App oxyjwt = pytest.importorskip("oxyjwt") @@ -33,7 +32,7 @@ def test_jwt_route_iss_and_aud_match() -> None: jwt_issuer=ISS, jwt_audience=AUD, ) - def ok(claims: dict) -> str: # noqa: ARG001 + def ok(claims: dict) -> str: return "yes" tok = _token(sub="u1", iss=ISS, aud=AUD, exp=now + 3600) @@ -60,7 +59,7 @@ def test_jwt_route_wrong_iss_401() -> None: jwt_issuer=ISS, jwt_audience=AUD, ) - def p(claims: dict) -> str: # noqa: ARG001 + def p(claims: dict) -> str: return "x" tok = _token(sub="u1", iss="https://other", aud=AUD, exp=now + 3600) @@ -86,7 +85,7 @@ def test_jwt_route_wrong_aud_401() -> None: jwt_issuer=ISS, jwt_audience=AUD, ) - def p(claims: dict) -> str: # noqa: ARG001 + def p(claims: dict) -> str: return "x" tok = _token(sub="u1", iss=ISS, aud="other-aud", exp=now + 3600) diff --git a/tests/test_openapi.py b/tests/test_openapi.py index 8f4e3d7..eff9c71 100644 --- a/tests/test_openapi.py +++ b/tests/test_openapi.py @@ -5,9 +5,11 @@ def test_openapi_shows_route() -> None: app = App(title="T") + @app.get("/items/:i") def list_items() -> str: return "ok" + s = app.openapi_json() assert "paths" in s assert "/items/:i" in s diff --git a/tests/test_query_decode.py b/tests/test_query_decode.py index 2d2e803..b62d188 100644 --- a/tests/test_query_decode.py +++ b/tests/test_query_decode.py @@ -5,7 +5,6 @@ import asyncio import httpx - from oxyroute import App diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..8fe677f --- /dev/null +++ b/uv.lock @@ -0,0 +1,399 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/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 = "certifi" +version = "2026.4.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, +] + +[[package]] +name = "click" +version = "8.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "granian" +version = "2.7.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/0c/27aa25280b6c1f323312e83088304da8a7f3e5c1e568d3a560365ec6fa67/granian-2.7.4.tar.gz", hash = "sha256:1dc0530d7ae6b0ae43aafafe771ac0b8c38af68bbd71ab355828817faf13aac1", size = 128212, upload-time = "2026-04-23T11:55:55.275Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/23/ccaa1c786aa528a672d6f25ce236156aeb06a63931109aa6f2d4d3c8a350/granian-2.7.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:1c2a13c5c119e34369f984d8414edb8ba3793d7c78c37bb795942648dda3eca1", size = 6354293, upload-time = "2026-04-23T11:53:32.922Z" }, + { url = "https://files.pythonhosted.org/packages/91/b7/2b0b0e2dc07cb17febbbf3b349c96f425a53d913ba76278091f821adfc22/granian-2.7.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:91963c4928a355d772f14075057ff721423bce70612a619edc2daf04dd258577", size = 6050464, upload-time = "2026-04-23T11:53:34.912Z" }, + { url = "https://files.pythonhosted.org/packages/a3/9c/1feb488abb85446ac7d12d05a93788a3a6b42810d64d5806ebc309c65fc9/granian-2.7.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9d068796cb7e8e0b7a4c8d51077701e37104a39cd103c655a5c232ad561fb07c", size = 6883201, upload-time = "2026-04-23T11:53:36.73Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ac/176b47bb48689fe3575ccbf372d67dd52fb0390686f4d8b7174ab2538ce5/granian-2.7.4-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c932f5c292b643019c4dd410a352789dbb8cb2cb41ec5b373779a87375de398a", size = 6135933, upload-time = "2026-04-23T11:53:38.552Z" }, + { url = "https://files.pythonhosted.org/packages/25/5d/4d894789683bd074bbe0361df7e3e524a2b763e44d771e326a16d4ea014b/granian-2.7.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7a8f411408b0b65a07460e39cb53178e30a15ff5f0c77ed6aa31e1106590ea9", size = 6783971, upload-time = "2026-04-23T11:53:40.602Z" }, + { url = "https://files.pythonhosted.org/packages/88/e3/fe781d55306f1542b3db0fac13b70b39846b5e9a400fda9a3a2bf04a9af6/granian-2.7.4-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6b7ab6a1a0c0d77ec1dd1145b7c8f3da5251ec7926c005da22f7415bf1b217a7", size = 6906647, upload-time = "2026-04-23T11:53:42.352Z" }, + { url = "https://files.pythonhosted.org/packages/76/11/c10af6940d8bb73cfb4eaa6781eb3f5c7880e1b69d9d87edc63292171538/granian-2.7.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6036316f781f7ad1412d7aa10b49c5a25e69fae3f67ed766b0923ebb43aa5118", size = 6930262, upload-time = "2026-04-23T11:53:43.855Z" }, + { url = "https://files.pythonhosted.org/packages/f4/3d/5d129792626ac990d3b14d484736b8eafa202966f1a541c5b665c30fc880/granian-2.7.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:acef581d94270a22763fba192fc8cef0df77dac125080ca27e6e847a5e59cd07", size = 7050428, upload-time = "2026-04-23T11:53:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/4c/e0/9a9d9f9d0e68277d49829d53746b2e8b3761e7b79a666fd9a4f445587321/granian-2.7.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:187a85fe36561c74a1db94b858175824c3154ebe6d0aa61c97124427f5c5a5fa", size = 6899482, upload-time = "2026-04-23T11:53:47.372Z" }, + { url = "https://files.pythonhosted.org/packages/19/de/04de408275968d4bff8e6fed9d6abf406beccac27412a08c8daaab7c9534/granian-2.7.4-cp310-cp310-win_amd64.whl", hash = "sha256:c73c6099206288c903a305d975064fbb51f9d0c78d06c914b23dde56165105c9", size = 3995374, upload-time = "2026-04-23T11:53:48.751Z" }, + { url = "https://files.pythonhosted.org/packages/df/00/a7db7e3627992c59927f57d5447638be515e683e2c8037ab7845250270d2/granian-2.7.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:455c51baf51dd0c3d22004fc04f9afb0662cb84ab2b75b48e5d6bb8b3e4e3548", size = 6353285, upload-time = "2026-04-23T11:53:50.113Z" }, + { url = "https://files.pythonhosted.org/packages/b2/23/337ab1a0929cb0cfbdedc06879cff62d6c08cb725fa2d4e139c7e305fed3/granian-2.7.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2c54f3fe69790aa4b685372bcc8f382a8e9ba570b8ea4cb476e3b240a5a5a7c", size = 6050711, upload-time = "2026-04-23T11:53:51.887Z" }, + { url = "https://files.pythonhosted.org/packages/28/ba/3605834adaf5dc9ac3701b817bc9d42c73c89fb67815c7c87c7f64a9b6e1/granian-2.7.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f9549c44b325fe51ee4fc57308761f5178add4d531f1cc333b4a1eedf4a5b7af", size = 6882199, upload-time = "2026-04-23T11:53:53.298Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1e/f11c9773dbf07ed326efc26a771b39ce97f7ee25608c16d69248db2da8da/granian-2.7.4-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0910390ea8f893cc4c3f38a28c923a321609358cf46d31aa7df5c3d3e58e8337", size = 6135800, upload-time = "2026-04-23T11:53:55.186Z" }, + { url = "https://files.pythonhosted.org/packages/f1/96/ca238b4f5d813643264abace48ca630efb1ab6d10409bd9e2c05c1d1ef12/granian-2.7.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0de44552990b3dacb87ea3f37ebbcce67881712c0b0db500013821b14df7e4e", size = 6784786, upload-time = "2026-04-23T11:53:56.706Z" }, + { url = "https://files.pythonhosted.org/packages/ae/85/2feeffc37fe3c8a0f3e932393bcc99c8972984fe95907b34b380284caf1c/granian-2.7.4-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5c9c6d51a675d9b7084244e63157899dd1afe6f1a5ab014015bc86afd4871df5", size = 6906836, upload-time = "2026-04-23T11:53:58.991Z" }, + { url = "https://files.pythonhosted.org/packages/ae/b6/faed26e3abd741e1d261defc0e7e3b2ecb9a2189c557e829bb28c3281456/granian-2.7.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6be8c6ebbc53efea03284aef87de9b7367df3c9433f7df3b46c1edceaaa9d840", size = 6929985, upload-time = "2026-04-23T11:54:00.846Z" }, + { url = "https://files.pythonhosted.org/packages/25/5c/9f7a836177b5e94ad15da49046254e5b837e1d2c3d03981111d4af9a9d2b/granian-2.7.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:c10e056a6e76da640adb35f88d41ba40ae44065c5e04d4bc35f47c19a7f83a99", size = 7050668, upload-time = "2026-04-23T11:54:02.466Z" }, + { url = "https://files.pythonhosted.org/packages/08/88/19c01761d639b5e2c2eb2f12ff064c6765f32fa7f129c9f48162cdca0668/granian-2.7.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e60a3153456f8922ca73d3a427cc3bb594c021f70ec08ecded6581efe25f48c", size = 6899480, upload-time = "2026-04-23T11:54:03.948Z" }, + { url = "https://files.pythonhosted.org/packages/04/ec/9022f3c2728fcd99f9374ce633e254b201f4bf76fbe60031bbc137f1cf8a/granian-2.7.4-cp311-cp311-win_amd64.whl", hash = "sha256:e9cafbf391d16ea8b8a2e9f88501783fac8da75eb948620899062a17929c4a84", size = 3996087, upload-time = "2026-04-23T11:54:05.771Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d9/148024fd3a8bd974bb5c68a0cb48d15df7763fd1364bf090ccc2d423028a/granian-2.7.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:2c2f40aaecf2ba3d8232e55181c8f6db7bc68d9112a419ab8d5f9e2f33f631f5", size = 6374067, upload-time = "2026-04-23T11:54:07.293Z" }, + { url = "https://files.pythonhosted.org/packages/fc/bb/c53b61a7cb67d33677d96913438eca3d79de1b1b7173a361fcdf2753ade7/granian-2.7.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a8111d5e74b27721e0fdda3edba7c154d44c41b469466857ca3c51b088e3846b", size = 6046338, upload-time = "2026-04-23T11:54:08.684Z" }, + { url = "https://files.pythonhosted.org/packages/29/8d/5c9dc91b9c9a05bf6ed0b795d30f4bb8f290d61502779a89ed2fd75f9fb6/granian-2.7.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:74adbb6c1920dbf4271b824135639318b2a20ff5e33bc35639a8e2928a777234", size = 7000585, upload-time = "2026-04-23T11:54:10.451Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7c/c770593b24a472ab5265a44546f56079757efbf89f8e8b2229a8443e453b/granian-2.7.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b778d356b61e0389c823016ad2be50a634b80d3d28a33922f7ac39553e828ad", size = 6255544, upload-time = "2026-04-23T11:54:12.484Z" }, + { url = "https://files.pythonhosted.org/packages/15/46/796147587edb494a330294cb001cf68520ad8296a7da91d80ec672ac8615/granian-2.7.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3607b091c4ef225ee99150f3b02cb827de8d677b52fc75f0b28893244f7bab27", size = 6875124, upload-time = "2026-04-23T11:54:13.967Z" }, + { url = "https://files.pythonhosted.org/packages/c5/25/b867f624886e11053e7a6235244de26fd864a136e65d12295e728b3e5005/granian-2.7.4-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3d3cf4fe3cafd9b874d8b749c66c790cbf2b4225f2a7d9fb284c51b77a8e938d", size = 6982394, upload-time = "2026-04-23T11:54:15.733Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e1/5746bfe202bd2f6a1506346463ce52dd015c2b5d03d07a53ecf0fddefa3f/granian-2.7.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:846c9cbfea8684ab13d21d66855ad06dc077fb95b5590e7f5040e79994d6429d", size = 6991457, upload-time = "2026-04-23T11:54:17.325Z" }, + { url = "https://files.pythonhosted.org/packages/e0/45/fc6992839d367b6ae8fa8d88b5e70ec293162c3a2e0e6b90fc426f228df2/granian-2.7.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:d34d97cfe4a7805ecb5b1b1684f3f197bb4baf019d2a9f18e34fd1d697a03a7f", size = 7148499, upload-time = "2026-04-23T11:54:19.234Z" }, + { url = "https://files.pythonhosted.org/packages/fe/12/16ffd64a1213858d4cf824767b398758be807dd1a6df5a303dc76994b6d6/granian-2.7.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f11336e4bcd8ef5c5143b075b5260e37e8431eb36d68564cc39416ca526c797f", size = 7006829, upload-time = "2026-04-23T11:54:20.804Z" }, + { url = "https://files.pythonhosted.org/packages/95/9a/f2fcda200f8739ddf25be72591b7a28897be0ffd952a76ec655e5f877144/granian-2.7.4-cp312-cp312-win_amd64.whl", hash = "sha256:9e0a4370773ec4a0e92a55a33fc700b60003e335480e5c7fe941f4bc3dda2e18", size = 4026771, upload-time = "2026-04-23T11:54:22.36Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0f/fa7c63afedcb214edb96703cade360d946d5f1ca59ddb0b3d8e04587fb45/granian-2.7.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:d11da4a4527ba8dc28b5533d5e3241d8d9212e593195d27c6e72c8a422010af5", size = 6373513, upload-time = "2026-04-23T11:54:24.246Z" }, + { url = "https://files.pythonhosted.org/packages/be/39/3088ce32d940f7982102ea3bdc230090e34ac56dc0bce04f2d03b56ea435/granian-2.7.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:057a3db87e93eca1a11255dd13b45b5dd83f798a750fd87f02e14d54db5741b6", size = 6045232, upload-time = "2026-04-23T11:54:25.708Z" }, + { url = "https://files.pythonhosted.org/packages/ac/61/588f6b5397ea4f5bd9fc8de4b8cc092c555b8d95371c03d149b3bc419277/granian-2.7.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb63d64c686799cea850c0c328d21adf75e323991a20be04923afc729432d2b5", size = 7001059, upload-time = "2026-04-23T11:54:27.532Z" }, + { url = "https://files.pythonhosted.org/packages/58/63/2affbcecfe96f940744c2086ea3793935d5f6898207590a579c92fc8588f/granian-2.7.4-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f406648c47569e983f0c58bd0853bac30a2bcdc6227428255ee5cc65a8ee62b6", size = 6255487, upload-time = "2026-04-23T11:54:29.397Z" }, + { url = "https://files.pythonhosted.org/packages/87/ac/31f7155a467020e7640e91af15ca3a70b0e7da210de42e3d3344e5eba8d0/granian-2.7.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bd56306eed06e293f4848c5ea997e1d019d1ad13b8252dde1f0bc773aca85ef", size = 6875068, upload-time = "2026-04-23T11:54:31.128Z" }, + { url = "https://files.pythonhosted.org/packages/99/22/402cc903e5c4e82bd363177392d4e1dcab8b27c1f7006c5316c37c597056/granian-2.7.4-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:732639e612e6b6e8d481f399f367e8c9bbb6f0e1b7b0aa74db340c574ee3dd98", size = 6982487, upload-time = "2026-04-23T11:54:32.704Z" }, + { url = "https://files.pythonhosted.org/packages/d3/92/3878f977bda82fc3a66fc7e95a54366a7b82edd53e6c9fdb3ec053693280/granian-2.7.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:47b8fdbfb369d52bb3fb884514a6a3a7e4d8e81c65fd26e5232985f2b46ebe0f", size = 6990683, upload-time = "2026-04-23T11:54:34.301Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b3/a1239f3bc4e9034e07cb32403e6a6d26db01bba1c244dd654f6a76bf2612/granian-2.7.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:b679086082bfd7c1aa8c248ef673b715616a4ce58eec6fbeef8b83b30ac84283", size = 7148570, upload-time = "2026-04-23T11:54:36.494Z" }, + { url = "https://files.pythonhosted.org/packages/89/3c/fef781ea7356b21f671615dd0d53adc00fad81031a9ea506f80d1f46a43d/granian-2.7.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a29191e949a99ffae2807abb7a864f7493f7a744e4fe2ddd2b5cd8db9b71378d", size = 7006976, upload-time = "2026-04-23T11:54:38.135Z" }, + { url = "https://files.pythonhosted.org/packages/56/54/ae2979fc45c06fbb37f595ee10eb6b138b6056202163b8e274d140d3f87b/granian-2.7.4-cp313-cp313-win_amd64.whl", hash = "sha256:07d26325cc69371ea2dc9d3a9cd0cc851c1c8e3dce40aca90e8c204547b5ba7e", size = 4027044, upload-time = "2026-04-23T11:54:39.957Z" }, + { url = "https://files.pythonhosted.org/packages/21/51/10344430e495bfa128dccc114957b33e712e971f91668788c08fe791df73/granian-2.7.4-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:4e093fe9511387313ad7ec9a76b0c78397cc584ef3dff47d46c336c5aee9cd8d", size = 6249290, upload-time = "2026-04-23T11:54:41.738Z" }, + { url = "https://files.pythonhosted.org/packages/ec/46/c7eda2e71a89a13e174598649f721c63ed3d908c0904b62621e8a433af0f/granian-2.7.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:227889f821526b8b60c5edf31b01fc987c4193bb0fc198c0998e0841e0cb719c", size = 5901799, upload-time = "2026-04-23T11:54:43.708Z" }, + { url = "https://files.pythonhosted.org/packages/72/d8/79e51f9f794389a9d6cab3d7c6b834b87d65fba72a43784eb5d2664a57a6/granian-2.7.4-cp313-cp313t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2b28d4aec5a9f2758a48da1897649a01b70ee1c00f2c4649db574527a3d00943", size = 6037594, upload-time = "2026-04-23T11:54:45.595Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d8/835873a407279435fa0c8e8ac52392d3ba5c9a652bb15c0036aa07d9c302/granian-2.7.4-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f708fea5024a40e0dfba1c17c1c4b09e02e00ac0ac9ac1e345b409f0c11b71e5", size = 6966672, upload-time = "2026-04-23T11:54:47.242Z" }, + { url = "https://files.pythonhosted.org/packages/92/5f/21eacdda27c38e4194de5f9bef36c4045058daf6d58533fadb7c54c70573/granian-2.7.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7006dfe9852cded794bc60008a168faf4dc2ecc18f1d74b5fde545685b699ec", size = 6563668, upload-time = "2026-04-23T11:54:49.751Z" }, + { url = "https://files.pythonhosted.org/packages/bd/06/9b19956d75277df44ee380e873a86b9890c431f2e2bcde32b3ba341f0efa/granian-2.7.4-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:77103af44034e30505fb5577b8214b0ad39cd6cbdc854ff980d4755faf93adaa", size = 6664285, upload-time = "2026-04-23T11:54:51.502Z" }, + { url = "https://files.pythonhosted.org/packages/85/33/740e0c9478be49c0778c4ea1773357680980e10e84b59bc19664033996dc/granian-2.7.4-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:b23194e1e0652297086224212605edb4998442511637e732d6009506277f8ff9", size = 6820367, upload-time = "2026-04-23T11:54:53.506Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ad/3453fc1212268a01fee957122f2b1699af0efe50eca07ac570e11d1be12b/granian-2.7.4-cp313-cp313t-musllinux_1_1_armv7l.whl", hash = "sha256:f62941a4ffa1f1c2c5750cfc0b0ad96aa85d63b016125289779eef8888f5340d", size = 7132366, upload-time = "2026-04-23T11:54:55.123Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ca/8479e4d2a02f210ce68b5dc73c77953ec1dfd3769bf725d06e6ec420d502/granian-2.7.4-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:ea6f97d2ade676f1bf49b79088fa4b5640b8b9804b7470218486df3d4be50046", size = 6842094, upload-time = "2026-04-23T11:54:56.665Z" }, + { url = "https://files.pythonhosted.org/packages/0d/96/71f95c73220726aee3e908b3ad2745c4c44fbfba508cb5ed615a9d4d367f/granian-2.7.4-cp313-cp313t-win_amd64.whl", hash = "sha256:759140ceef02ef72e57a184461927d72bcc2ddd3664c3cbbf4def7516f818041", size = 3974523, upload-time = "2026-04-23T11:54:58.541Z" }, + { url = "https://files.pythonhosted.org/packages/98/5d/a0c3d8778cd8aa68131974d34c439a38a00a32953e71e3b549759a5e3cdb/granian-2.7.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c19ebe797d7383cbb3497c599b8201af71f9fff6b18deaf9965d106f61588ab8", size = 6322736, upload-time = "2026-04-23T11:55:00.292Z" }, + { url = "https://files.pythonhosted.org/packages/5e/99/211da053030574f2402c750f3e3e5dc587f5192eac4888affe6ca8894a9f/granian-2.7.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4cee0bdba9179537669c2fa0afab2ce89327a372f1b2a82f280798da321c996c", size = 6052103, upload-time = "2026-04-23T11:55:02.797Z" }, + { url = "https://files.pythonhosted.org/packages/ab/9d/23ec1fd519a4c0db961b05d1821869ed6371cbaf8b3d3a0a85c04f89e6ca/granian-2.7.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a4bc5b54845bfb5f87537483f25c8f8e6003c3c1b4b0eadf6b93a432d0604265", size = 7000868, upload-time = "2026-04-23T11:55:04.826Z" }, + { url = "https://files.pythonhosted.org/packages/98/35/b8798c98c90d3293d9c85580ea6021f148d5ab73ab99d1f82a0e66f73131/granian-2.7.4-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b550fb98b89465c8192b6e506993de6bfb956838e715ffb58e944aec1afdae99", size = 6257266, upload-time = "2026-04-23T11:55:06.962Z" }, + { url = "https://files.pythonhosted.org/packages/6b/4f/5574db17193d90a5831120a0ce2a2dc64a711110ccb9af5a3630260c3597/granian-2.7.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7100a6a6d3835fec2a207fef536a259dd42d9efdb5c46933cf6f9d55d5bfaad", size = 6849667, upload-time = "2026-04-23T11:55:08.862Z" }, + { url = "https://files.pythonhosted.org/packages/66/a7/90b85cc6a31cbee772fc8ee731479429a64169e389444a5fdd685d44a342/granian-2.7.4-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:034ac1bfe8c19b5a7916d35a1ca426845db9ac11215f1b367566aec3b6530549", size = 6902612, upload-time = "2026-04-23T11:55:10.888Z" }, + { url = "https://files.pythonhosted.org/packages/06/6c/ba203ca40bd406db0412bca70281e44712f941bc6aafb59a628f4811d517/granian-2.7.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:baf1c390a25d3d9840204c39e7b801c909e99e896ae2713d898c46b563cbf962", size = 6927025, upload-time = "2026-04-23T11:55:12.663Z" }, + { url = "https://files.pythonhosted.org/packages/ee/52/77e2abfba54523943eea275ebbe733a6d186fe646304fe25f6d22b243d03/granian-2.7.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:3bb99778ae05c1118cd694717d025cc0b85f5ee81f60cbcb2a8783692798db96", size = 7146800, upload-time = "2026-04-23T11:55:14.459Z" }, + { url = "https://files.pythonhosted.org/packages/1d/66/7209201856b7de8d3c643ba87e11272c4d651c216d05ea3fcbdce0da4ab0/granian-2.7.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:13f0a39872afa81c6aaa8e29832371fd831373140f1f04de459ff862824f488b", size = 6999983, upload-time = "2026-04-23T11:55:16.236Z" }, + { url = "https://files.pythonhosted.org/packages/c3/45/bd1e521284714615996dcee48dad47d8b97ca2767a7e7cccd392f25fc176/granian-2.7.4-cp314-cp314-win_amd64.whl", hash = "sha256:97b5aeec98a9c6c0695bf8f068bd03aca83fc17c0d977a9c3a2e57bb5f10d47e", size = 3989433, upload-time = "2026-04-23T11:55:17.774Z" }, + { url = "https://files.pythonhosted.org/packages/45/a2/609f8f0dca7f596b5fb6e57b988b4b8f4d6579724b2720933c379d43301a/granian-2.7.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:a7b1aca6c654f0e61c9e493dd6d3ddb1698f47dc33ed04566a6635948b081b64", size = 6251034, upload-time = "2026-04-23T11:55:19.29Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f5/2eefa8ff477cce7b119ed2fe97fc1f3b2d108397d4755e83a5198149f2c8/granian-2.7.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d4e0c8cc6850dec7180a26b6805b2c4cdbac4c1c48077fd7857a3cd8ff342d9d", size = 5912772, upload-time = "2026-04-23T11:55:21.581Z" }, + { url = "https://files.pythonhosted.org/packages/ae/40/9a5070badaed4ceecf4082855985840c320f7232b8c1ddc93e1732c63265/granian-2.7.4-cp314-cp314t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7e6b1f6e0fe873efa3393ef28803ff699a94254f2a7dc07422cc01d9849e2136", size = 6037318, upload-time = "2026-04-23T11:55:23.855Z" }, + { url = "https://files.pythonhosted.org/packages/95/52/1db412e63425cb12f5ca61877956583c6d12f21657b1a3e47eb3200e9c1b/granian-2.7.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dce110217825cff60f68da83280bc20471b10e004e720fa94b845e01925d8698", size = 6962778, upload-time = "2026-04-23T11:55:26.095Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f2/fcca39f617bf70e29ef903bb7a4d037970c637023484f2112d9ed6882516/granian-2.7.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:058f9a4ebfc7b9c2577569c6ecfd333628d0d045de272afaa65ee9933849778c", size = 6566618, upload-time = "2026-04-23T11:55:28.233Z" }, + { url = "https://files.pythonhosted.org/packages/ee/20/0da1bb552746d74275017e1ffc7fc419dd1a33345f132f6f5a90f9f41142/granian-2.7.4-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:7c05f74fa5b5dcedc9f035a7c10b8afd90a3d941975a370f1e07c3f3095dd883", size = 6670850, upload-time = "2026-04-23T11:55:29.945Z" }, + { url = "https://files.pythonhosted.org/packages/11/2a/d0d9cdb10d2760e2f47bd4600c8eef02e326f8f7e253a80ce4ba384265e6/granian-2.7.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:8b992bbc667e3c74de4ad48ac8d735c7cddf3f709fc2097f7dd230ecc46fd7b3", size = 6824752, upload-time = "2026-04-23T11:55:32.066Z" }, + { url = "https://files.pythonhosted.org/packages/3d/79/0432f92f9df6e54394e4dd1c159c0d4814d255a2d2541fa9a5c187d19152/granian-2.7.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:df05e0f85712b3e90ddf28cb8be358664b1afa8cb8f09978141ca70052dca3a7", size = 7130809, upload-time = "2026-04-23T11:55:33.807Z" }, + { url = "https://files.pythonhosted.org/packages/19/03/11cc0e08f59f03a3cd6a1fe46d7632a0f8690ef945a495b1303140bb7541/granian-2.7.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:dbc620f35b67cf6b03d2b6a24b9b442d1bf52961eaebadb2c3ff214d3d0c8dc4", size = 6845920, upload-time = "2026-04-23T11:55:35.583Z" }, + { url = "https://files.pythonhosted.org/packages/b4/49/bcbaaeec0f68d3d1a3dd1fdd21e4a6963d303ae18027c42b2b53f87d6b89/granian-2.7.4-cp314-cp314t-win_amd64.whl", hash = "sha256:b9df8aead4d71562753788264db23d32db34147bb73294ddd90833bef1f4cf35", size = 3981107, upload-time = "2026-04-23T11:55:37.597Z" }, + { url = "https://files.pythonhosted.org/packages/51/18/577637bb861ab688db8eb5d698ad700133818debd7ae6f58c0574c43f70e/granian-2.7.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ce50300cf876f418ba0545f6e8c56d8c75038fc503add0fd1b58d9a3057d95ea", size = 6363314, upload-time = "2026-04-23T11:55:39.837Z" }, + { url = "https://files.pythonhosted.org/packages/48/41/11a6219baa10270f1a6a2a101cfa372e5d55a46a839a43b49a8d087fac09/granian-2.7.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:eb7f727f14d7d485a5df4078e7cc3038864b4e7c380865968e75e1e51e62457a", size = 6027259, upload-time = "2026-04-23T11:55:42.122Z" }, + { url = "https://files.pythonhosted.org/packages/bf/58/12b19b17fb79ee064a8a77a865a031bb49f4ea813789ad63186458ea02c9/granian-2.7.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abbab303b502a770355c13c93569e6c0c71ccc864ab41b59636720d5a643f6b3", size = 6760497, upload-time = "2026-04-23T11:55:44.116Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/572711f882423f599707aae577ccdbc1700cf0cc3ceb4e9500e00c6b8d14/granian-2.7.4-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:f0b0423fa33a1afb9730fbfb5700fef4dac16bf7a1b7a2a79d0349739c1b1f44", size = 6843897, upload-time = "2026-04-23T11:55:46.322Z" }, + { url = "https://files.pythonhosted.org/packages/31/f9/75d51721069a184cd00310c4b0b0d614a6370905c13a096ccee193432ba3/granian-2.7.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:efa0d4fc35ab42562747e4103124e1c4f21afab081c1591de6472174a3416802", size = 6876194, upload-time = "2026-04-23T11:55:48.231Z" }, + { url = "https://files.pythonhosted.org/packages/44/5e/fd81492529bc2b02dafc63c95d03c2c7faa26ac883ccd94aa93b21fc68c3/granian-2.7.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:03b5ce06df095b5db49bd4e976ac8d8419bb0e73dc160613fc3db5e5d5dcd1af", size = 7094066, upload-time = "2026-04-23T11:55:50.328Z" }, + { url = "https://files.pythonhosted.org/packages/c7/04/f2fa35dc2956edb9a5abaabc0840aed92b4121ce27adf684a1c75e3c70ac/granian-2.7.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:9247db25dd66f74766a6a9488f1279c9b40cf422c6d7a04010492fa1aa7c9019", size = 6892580, upload-time = "2026-04-23T11:55:52.084Z" }, + { url = "https://files.pythonhosted.org/packages/72/6f/f45fa86c36fcc34f6e613bb223b10fd36c6acd9f7aa43d4f65d0f1eff4cc/granian-2.7.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:efccd6818a1ac4cba7eededf5e2768f56d4a8c7c93bd5e3a8d7a901510976944", size = 3971242, upload-time = "2026-04-23T11:55:53.834Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "maturin" +version = "1.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/16/b284a7bc4af3dd87717c784278c1b8cb18606ad1f6f7a671c47bfd9c3df0/maturin-1.13.1.tar.gz", hash = "sha256:9a87ff3b8e4d1c6eac33ebfe8e261e8236516d98d45c0323550621819b5a1a2f", size = 340369, upload-time = "2026-04-09T15:14:07.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/4d/a23fc95be881aa8c7a6ea353410417872e4d7065df03d7f3db8f0dbed4a7/maturin-1.13.1-py3-none-linux_armv6l.whl", hash = "sha256:416e4e01cb88b798e606ee43929df897e42c1647b722ef68283816cca99a8742", size = 10102444, upload-time = "2026-04-09T15:13:48.393Z" }, + { url = "https://files.pythonhosted.org/packages/a6/1e/65c385d65bae95cf04895d52f39dbed8b1453ae55da2903d252ade40a774/maturin-1.13.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:72888e87819ce546d0d2df900e4b385e4ef299077d92ee37b48923a5602dae94", size = 19576043, upload-time = "2026-04-09T15:14:08.685Z" }, + { url = "https://files.pythonhosted.org/packages/8f/13/f6bc868d0bfecd9314870b97f530a167e31f7878ac4945c78245c6eef69c/maturin-1.13.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:98b5fcf1a186c217830a8295ecc2989c6b1cf50945417adfc15252107b9475b7", size = 10117339, upload-time = "2026-04-09T15:13:40.559Z" }, + { url = "https://files.pythonhosted.org/packages/51/58/279e081305c11c1c1c4fccacf77df8959646c5d4de7a57ec7e787653e270/maturin-1.13.1-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:3da18cccf2f683c0977bff9146a0908d6ffce836d600665736ac01679f588cb9", size = 10139689, upload-time = "2026-04-09T15:13:38.291Z" }, + { url = "https://files.pythonhosted.org/packages/00/94/69391af5396c6aab723932240803f49e5f3de3dd7c57d32f02d237a0ce32/maturin-1.13.1-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:6b1e5916a253243e8f5f9e847b62bbc98420eec48c9ce2e2e8724c6da89d359b", size = 10551141, upload-time = "2026-04-09T15:13:42.887Z" }, + { url = "https://files.pythonhosted.org/packages/9e/bf/4edac2667b49e3733438062ae416413b8fc8d42e1bd499ba15e1fb02fc55/maturin-1.13.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:dc91031e0619c1e28730279ef9ee5f106c9b9ec806b013f888676b242f892eb7", size = 9983094, upload-time = "2026-04-09T15:13:56.868Z" }, + { url = "https://files.pythonhosted.org/packages/79/94/a6d651cfe8fc6bf2e892c90e3cdbb25c06d81c9115140d03ea1a68a97575/maturin-1.13.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:001741c6cff56aa8ea59a0d78ae990c0550d0e3e82b00b683eedb4158a8ef7e6", size = 9949980, upload-time = "2026-04-09T15:13:59.185Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d1/82c067464f848e38af9910bce55eb54302b1c1284a279d515dbfcf5994f5/maturin-1.13.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:01c845825c917c07c1d0b2c9032c59c16a7d383d1e649a46481d3e5693c2750f", size = 13186276, upload-time = "2026-04-09T15:13:45.725Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f4/25367baf1025580f047f9b37598bb3fadc416e24536afd4f28e190335c73/maturin-1.13.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f69093ed4a0e6464e52a7fc26d714f859ce15630ec8070743398c6bf41f38a9e", size = 10891837, upload-time = "2026-04-09T15:13:35.68Z" }, + { url = "https://files.pythonhosted.org/packages/af/be/caafad8ce74974b7deafdf144d12f758993dfea4c66c9905b138f51a7792/maturin-1.13.1-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:c1490584f3c70af45466ee99065b49e6657ebdccac6b10571bb44681309c9396", size = 10351032, upload-time = "2026-04-09T15:14:01.632Z" }, + { url = "https://files.pythonhosted.org/packages/66/0e/970a721d27cfa410e8bfa0a1e32e6ef52cb8169692110a5fdabe1af3f570/maturin-1.13.1-py3-none-win32.whl", hash = "sha256:c6a720b252c99de072922dbe4432ab19662b6f80045b0355fec23bdfccb450da", size = 8855465, upload-time = "2026-04-09T15:13:51.122Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/7c1e0d65fa147d5479055a171541c82b8cdfc1c825d85a82240470f14176/maturin-1.13.1-py3-none-win_amd64.whl", hash = "sha256:a2017d2281203d0c6570240e7d746564d766d756105823b7de68bda6ae722711", size = 10230471, upload-time = "2026-04-09T15:13:53.89Z" }, + { url = "https://files.pythonhosted.org/packages/c5/2a/afe0193b673a79ffd2e01ad999511b7e9e6b49af02bb3759d82a78c3043d/maturin-1.13.1-py3-none-win_arm64.whl", hash = "sha256:2839024dcd65776abb4759e5bca29941971e095574162a4d335191da4be9ff24", size = 8905575, upload-time = "2026-04-09T15:14:03.891Z" }, +] + +[[package]] +name = "oxyjwt" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/b7/236a87d014d3a7bdf672c2d707b5449cd89f96025d246cc6cf931a52e89a/oxyjwt-0.2.0.tar.gz", hash = "sha256:649bc62a44bc324fe19937474d9d60069ebfe9af5edf1e4a280f9a252dd001ab", size = 24303, upload-time = "2026-04-26T07:49:26.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/b4/94102d3f63b03e50b82ddff85f0d76f93d88cb2d3e09946ba8768286a84b/oxyjwt-0.2.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:27c5099416c5803ad54798e4e8ce05e0e11a1b639258d365bbe2a8969da50976", size = 1499308, upload-time = "2026-04-26T07:49:17.512Z" }, + { url = "https://files.pythonhosted.org/packages/97/55/8260ce13707ec152d44e95066a9f58cc611b909b09dc088153d1c792480c/oxyjwt-0.2.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:0c19dd0dd1e7ab69539f3786e8e9ed3b93303e3cc3b70076adb989b86052979f", size = 1423862, upload-time = "2026-04-26T07:49:19.726Z" }, + { url = "https://files.pythonhosted.org/packages/f0/2d/ab847b81dcf4ed4a553a24b244822ad0c01500449489fad2643128097b52/oxyjwt-0.2.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4baafb218b3e740983bb3fb597e8f57be8f4a5ec8afe796b4b8e6f3db6d37ded", size = 790573, upload-time = "2026-04-26T07:49:21.657Z" }, + { url = "https://files.pythonhosted.org/packages/4a/14/b26db05c2b1b824c17b24e21cae7beb5df32b4a63cc1dda49bc020caaff0/oxyjwt-0.2.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b58e987492b68a18b1640abbb3ac4a2d232c6f8cf9fb4046d928a9b7ec95db3b", size = 1486775, upload-time = "2026-04-26T07:49:23.849Z" }, + { url = "https://files.pythonhosted.org/packages/69/c1/5d26a2ab75cf7c840a8cffc5de964543d593e9294989c0a19bea9ac7816d/oxyjwt-0.2.0-cp310-abi3-win_amd64.whl", hash = "sha256:791767a6d94c58280193ec7935b07f9c368fd859b17230f280b9179a66fd30ff", size = 1159141, upload-time = "2026-04-26T07:49:25.436Z" }, +] + +[[package]] +name = "oxyroute" +version = "0.1.0" +source = { editable = "." } + +[package.optional-dependencies] +dev = [ + { name = "granian" }, + { name = "httpx" }, + { name = "maturin" }, + { name = "oxyjwt" }, + { name = "pytest" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "granian", marker = "extra == 'dev'", specifier = ">=1.0" }, + { name = "httpx", marker = "extra == 'dev'", specifier = ">=0.27" }, + { name = "maturin", marker = "extra == 'dev'", specifier = ">=1.4,<2" }, + { name = "oxyjwt", marker = "extra == 'dev'", specifier = ">=0.2" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.8" }, +] +provides-extras = ["dev"] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/99/43/3291f1cc9106f4c63bdce7a8d0df5047fe8422a75b091c16b5e9355e0b11/ruff-0.15.12.tar.gz", hash = "sha256:ecea26adb26b4232c0c2ca19ccbc0083a68344180bba2a600605538ce51a40a6", size = 4643852, upload-time = "2026-04-24T18:17:14.305Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/6e/e78ffb61d4686f3d96ba3df2c801161843746dcbcbb17a1e927d4829312b/ruff-0.15.12-py3-none-linux_armv6l.whl", hash = "sha256:f86f176e188e94d6bdbc09f09bfd9dc729059ad93d0e7390b5a73efe19f8861c", size = 10640713, upload-time = "2026-04-24T18:17:22.841Z" }, + { url = "https://files.pythonhosted.org/packages/ae/08/a317bc231fb9e7b93e4ef3089501e51922ff88d6936ce5cf870c4fe55419/ruff-0.15.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e3bcd123364c3770b8e1b7baaf343cc99a35f197c5c6e8af79015c666c423a6c", size = 11069267, upload-time = "2026-04-24T18:17:30.105Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fe87510d000220aa1ed530d4448a7c696a0cae1213e5ec30e5874287b66557b5", size = 10397182, upload-time = "2026-04-24T18:17:07.177Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/3310fc6d1b5e1fdea22bf3b1b807c7e187b581021b0d7d4514cccdb5fb71/ruff-0.15.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84a1630093121375a3e2a95b4a6dc7b59e2b4ee76216e32d81aae550a832d002", size = 10758012, upload-time = "2026-04-24T18:16:55.759Z" }, + { url = "https://files.pythonhosted.org/packages/11/c1/a606911aee04c324ddaa883ae418f3569792fd3c4a10c50e0dd0a2311e1e/ruff-0.15.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fb129f40f114f089ebe0ca56c0d251cf2061b17651d464bb6478dc01e69f11f5", size = 10447479, upload-time = "2026-04-24T18:16:51.677Z" }, + { url = "https://files.pythonhosted.org/packages/9d/68/4201e8444f0894f21ab4aeeaee68aa4f10b51613514a20d80bd628d57e88/ruff-0.15.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0c862b172d695db7598426b8af465e7e9ac00a3ea2a3630ee67eb82e366aaa6", size = 11234040, upload-time = "2026-04-24T18:17:16.529Z" }, + { url = "https://files.pythonhosted.org/packages/34/ff/8a6d6cf4ccc23fd67060874e832c18919d1557a0611ebef03fdb01fff11e/ruff-0.15.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2849ea9f3484c3aca43a82f484210370319e7170df4dfe4843395ddf6c57bc33", size = 12087377, upload-time = "2026-04-24T18:17:04.944Z" }, + { url = "https://files.pythonhosted.org/packages/85/f6/c669cf73f5152f623d34e69866a46d5e6185816b19fcd5b6dd8a2d299922/ruff-0.15.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e77c7e51c07fe396826d5969a5b846d9cd4c402535835fb6e21ce8b28fef847", size = 11367784, upload-time = "2026-04-24T18:17:25.409Z" }, + { url = "https://files.pythonhosted.org/packages/e8/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b2f4f2f3b1026b5fb449b467d9264bf22067b600f7b6f41fc5958909f449d0", size = 11344088, upload-time = "2026-04-24T18:17:12.258Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8d/49afab3645e31e12c590acb6d3b5b69d7aab5b81926dbaf7461f9441f37a/ruff-0.15.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9ba3b8f1afd7e2e43d8943e55f249e13f9682fde09711644a6e7290eb4f3e339", size = 11271770, upload-time = "2026-04-24T18:17:02.457Z" }, + { url = "https://files.pythonhosted.org/packages/46/06/33f41fe94403e2b755481cdfb9b7ef3e4e0ed031c4581124658d935d52b4/ruff-0.15.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e852ba9fdc890655e1d78f2df1499efbe0e54126bd405362154a75e2bde159c5", size = 10719355, upload-time = "2026-04-24T18:17:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/0d/59/18aa4e014debbf559670e4048e39260a85c7fcee84acfd761ac01e7b8d35/ruff-0.15.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dd8aed930da53780d22fc70bdf84452c843cf64f8cb4eb38984319c24c5cd5fd", size = 10462758, upload-time = "2026-04-24T18:17:32.347Z" }, + { url = "https://files.pythonhosted.org/packages/25/e7/cc9f16fd0f3b5fddcbd7ec3d6ae30c8f3fde1047f32a4093a98d633c6570/ruff-0.15.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:01da3988d225628b709493d7dc67c3b9b12c0210016b08690ef9bd27970b262b", size = 10953498, upload-time = "2026-04-24T18:17:20.674Z" }, + { url = "https://files.pythonhosted.org/packages/72/7a/a9ba7f98c7a575978698f4230c5e8cc54bbc761af34f560818f933dafa0c/ruff-0.15.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9cae0f92bd5700d1213188b31cd3bdd2b315361296d10b96b8e2337d3d11f53e", size = 11447765, upload-time = "2026-04-24T18:17:09.755Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f9/0ae446942c846b8266059ad8a30702a35afae55f5cdc54c5adf8d7afdc27/ruff-0.15.12-py3-none-win32.whl", hash = "sha256:d0185894e038d7043ba8fd6aee7499ece6462dc0ea9f1e260c7451807c714c20", size = 10657277, upload-time = "2026-04-24T18:17:18.591Z" }, + { url = "https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.whl", hash = "sha256:c87a162d61ab3adca47c03f7f717c68672edec7d1b5499e652331780fe74950d", size = 11837758, upload-time = "2026-04-24T18:17:00.113Z" }, + { url = "https://files.pythonhosted.org/packages/c0/98/6beb4b351e472e5f4c4613f7c35a5290b8be2497e183825310c4c3a3984b/ruff-0.15.12-py3-none-win_arm64.whl", hash = "sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f", size = 11120821, upload-time = "2026-04-24T18:16:57.979Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +]