diff --git a/Makefile b/Makefile index cad3242fbce..e7c88124eac 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ .PHONY: help test test-unit test-unit-llms test-unit-proxy-guardrails test-unit-proxy-core test-unit-proxy-misc \ test-unit-integrations test-unit-core-utils test-unit-other test-unit-root \ test-proxy-unit-a test-proxy-unit-b test-integration test-unit-helm \ - test-rust-extension rust-sqlx-prepare \ + test-rust-extension rust-sqlx-prepare db-codegen db-migration-report \ info lint lint-inner lint-dev lint-checks format \ lint-basedpyright lint-e2e-basedpyright lint-basedpyright-budget-update lint-type-discipline lint-type-discipline-budget-update \ lint-ruff-budget lint-ruff-budget-update lint-budget-update lint-gate \ @@ -57,6 +57,8 @@ help: @echo " make test-unit-helm - Run helm unit tests" @echo " make test-rust-extension - Build the Rust extension and run its public Python tests" @echo " make rust-sqlx-prepare - Refresh litellm-rust/crates/db/.sqlx against a migrated Postgres container" + @echo " make db-codegen - Regenerate litellm/proxy/db/queries/generated from litellm-rust/crates/db" + @echo " make db-migration-report - Count Prisma call sites per migration group (GROUP=O1 lists one group)" @echo "" @echo "Heavy targets (check, lint) queue for LITELLM_GATE_SLOTS machine-wide" @echo "slots (default 2; 0 disables) so parallel sessions don't thrash one machine." @@ -307,9 +309,27 @@ test-rust-extension: LITELLM_RUST=1 LITELLM_LOCAL_MODEL_COST_MAP=True \ "$$temporary/venv/bin/python" -I -m pytest --import-mode=importlib -m requires_rust_extension tests/test_litellm_rust +DB_GENERATED := litellm/proxy/db/queries/generated +DB_SKILLS := litellm-rust/crates/db/.agents/skills + rust-sqlx-prepare: cd litellm-rust && cargo run -p litellm-db-testing --bin sqlx-prepare +db-codegen: + @schema=$$(mktemp) && \ + trap 'rm -f "$$schema"' EXIT HUP INT TERM && \ + (cd litellm-rust && cargo run -q --locked -p litellm-db --features schema --bin gen-python -- ../$(DB_GENERATED)/catalog.py) > "$$schema" && \ + $(UV_RUN) datamodel-codegen --input "$$schema" --input-file-type jsonschema --output $(DB_GENERATED)/models.py \ + --output-model-type pydantic_v2.BaseModel --target-python-version 3.10 --use-title-as-name \ + --use-union-operator --use-standard-collections --use-annotated --enable-faux-immutability \ + --use-type-alias --skip-root-model --use-one-literal-as-default --disable-timestamp \ + --formatters builtin --custom-file-header '# @generated by `make db-codegen` from litellm-rust/crates/db. Do not edit.' && \ + $(UV_RUN) ruff check --quiet --select I --fix $(DB_GENERATED) && \ + $(UV_RUN) ruff format --quiet $(DB_GENERATED) + +db-migration-report: + $(UV_RUN) python $(DB_SKILLS)/db-migrate-classify/scripts/report.py $(if $(GROUP),--group $(GROUP)) + test: install-test-deps $(UV_RUN) pytest tests/ diff --git a/litellm-rust/Cargo.lock b/litellm-rust/Cargo.lock index 19458cf34b1..bf980340bdc 100644 --- a/litellm-rust/Cargo.lock +++ b/litellm-rust/Cargo.lock @@ -3439,8 +3439,12 @@ dependencies = [ name = "litellm-db" version = "0.1.0" dependencies = [ + "rstest", + "schemars 1.2.2", "serde", + "serde_json", "sqlx", + "thiserror 2.0.19", ] [[package]] @@ -5560,6 +5564,7 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" dependencies = [ + "chrono", "dyn-clone", "ref-cast", "schemars_derive", diff --git a/litellm-rust/crates/db/.sqlx/query-34720197d10a6ea8fd4339bd40ccd6b77a6d9d4f3df9e4ecac85056709996254.json b/litellm-rust/crates/db/.sqlx/query-34720197d10a6ea8fd4339bd40ccd6b77a6d9d4f3df9e4ecac85056709996254.json new file mode 100644 index 00000000000..fc17c3492b1 --- /dev/null +++ b/litellm-rust/crates/db/.sqlx/query-34720197d10a6ea8fd4339bd40ccd6b77a6d9d4f3df9e4ecac85056709996254.json @@ -0,0 +1,31 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT $1::text AS first, $2::int8 AS second\n", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "first", + "type_info": "Text", + "origin": "Expression" + }, + { + "ordinal": 1, + "name": "second", + "type_info": "Int8", + "origin": "Expression" + } + ], + "parameters": { + "Left": [ + "Text", + "Int8" + ] + }, + "nullable": [ + null, + null + ] + }, + "hash": "34720197d10a6ea8fd4339bd40ccd6b77a6d9d4f3df9e4ecac85056709996254" +} diff --git a/litellm-rust/crates/db/Cargo.toml b/litellm-rust/crates/db/Cargo.toml index ca9d6f555b0..6ace467bc52 100644 --- a/litellm-rust/crates/db/Cargo.toml +++ b/litellm-rust/crates/db/Cargo.toml @@ -5,6 +5,19 @@ edition.workspace = true license.workspace = true repository.workspace = true +[features] +schema = ["dep:schemars", "dep:serde_json", "dep:thiserror"] + +[[bin]] +name = "gen-python" +required-features = ["schema"] + [dependencies] +schemars = { workspace = true, optional = true, features = ["chrono04"] } serde.workspace = true +serde_json = { workspace = true, optional = true } sqlx.workspace = true +thiserror = { workspace = true, optional = true } + +[dev-dependencies] +rstest.workspace = true diff --git a/litellm-rust/crates/db/src/bin/gen-python.rs b/litellm-rust/crates/db/src/bin/gen-python.rs new file mode 100644 index 00000000000..aa917459d45 --- /dev/null +++ b/litellm-rust/crates/db/src/bin/gen-python.rs @@ -0,0 +1,37 @@ +use std::{ + env, fs, + io::{self, Write}, + process::ExitCode, +}; + +use litellm_db::{ + CATALOG, + codegen::{catalog_module, schema_document}, +}; + +fn main() -> ExitCode { + let Some(catalog_path) = env::args().nth(1) else { + eprintln!("usage: gen-python ; the JSON Schema goes to stdout"); + return ExitCode::FAILURE; + }; + let queries: Vec<_> = CATALOG.iter().flat_map(|domain| domain.iter()).collect(); + let catalog = match catalog_module(&queries) { + Ok(catalog) => catalog, + Err(error) => { + eprintln!("{error}"); + return ExitCode::FAILURE; + } + }; + if let Err(error) = fs::write(&catalog_path, catalog) { + eprintln!("writing {catalog_path}: {error}"); + return ExitCode::FAILURE; + } + let schema = schema_document(&queries); + match writeln!(io::stdout().lock(), "{schema:#}") { + Ok(()) => ExitCode::SUCCESS, + Err(error) => { + eprintln!("writing the JSON Schema: {error}"); + ExitCode::FAILURE + } + } +} diff --git a/litellm-rust/crates/db/src/codegen.rs b/litellm-rust/crates/db/src/codegen.rs new file mode 100644 index 00000000000..be7cdad5dc5 --- /dev/null +++ b/litellm-rust/crates/db/src/codegen.rs @@ -0,0 +1,151 @@ +use std::{borrow::Cow, path::Path}; + +use schemars::{JsonSchema, Schema, SchemaGenerator, generate::SchemaSettings}; +use serde_json::{Value, json}; + +use crate::DbFailure; + +const QUERY_ROOT: &str = "../../../litellm/proxy/db/queries/"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Cardinality { + Many, + Optional, + One, + Execute, +} + +impl Cardinality { + fn descriptor(self) -> &'static str { + match self { + Self::Many => "ManyQuery", + Self::Optional => "OptionalQuery", + Self::One => "OneQuery", + Self::Execute => "ExecuteQuery", + } + } +} + +#[derive(Clone, Copy)] +pub struct Model { + name: fn() -> Cow<'static, str>, + schema: fn(&mut SchemaGenerator) -> Schema, +} + +impl Model { + pub const fn of() -> Self { + Self { + name: T::schema_name, + schema: SchemaGenerator::subschema_for::, + } + } + + fn name(self) -> Cow<'static, str> { + (self.name)() + } +} + +pub struct QuerySpec { + pub name: &'static str, + pub file: &'static str, + pub cardinality: Cardinality, + pub arguments: &'static [&'static str], + pub params: Model, + pub row: Option, +} + +impl QuerySpec { + fn models(&self) -> impl Iterator { + [Some(self.params), self.row].into_iter().flatten() + } +} + +#[derive(Debug, thiserror::Error, PartialEq, Eq)] +pub enum CodegenError { + #[error("`{query}` reads {file}, which is not a .sql file under {QUERY_ROOT}")] + OutsideQueryRoot { + query: &'static str, + file: &'static str, + }, +} + +pub fn schema_document(queries: &[&QuerySpec]) -> Value { + let mut generator = SchemaSettings::draft2020_12() + .for_serialize() + .into_generator(); + generator.subschema_for::(); + for model in queries.iter().flat_map(|query| query.models()) { + (model.schema)(&mut generator); + } + json!({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": generator.take_definitions(true), + }) +} + +pub fn catalog_module(queries: &[&QuerySpec]) -> Result { + let descriptors = queries + .iter() + .map(|query| descriptor(query)) + .collect::, _>>()?; + if descriptors.is_empty() { + return Ok(String::from(HEADER)); + } + let mut models: Vec<_> = queries + .iter() + .flat_map(|query| query.models()) + .map(Model::name) + .collect(); + models.sort(); + models.dedup(); + let mut kinds: Vec<_> = queries + .iter() + .map(|query| query.cardinality.descriptor()) + .collect(); + kinds.sort_unstable(); + kinds.dedup(); + Ok(format!( + "{HEADER}\nfrom typing import Final\n\nfrom litellm.proxy.db.queries import load\nfrom litellm.proxy.db.queries.generated.models import {}\nfrom litellm.proxy.db.queries.runtime import {}\n{}", + models.join(", "), + kinds.join(", "), + descriptors.concat(), + )) +} + +const HEADER: &str = + "# @generated by `make db-codegen` from litellm-rust/crates/db. Do not edit.\n"; + +fn descriptor(query: &QuerySpec) -> Result { + let outside = || CodegenError::OutsideQueryRoot { + query: query.name, + file: query.file, + }; + let relative = query + .file + .strip_prefix("../../../") + .and_then(|path| path.strip_suffix(".sql")) + .filter(|_| query.file.starts_with(QUERY_ROOT)) + .ok_or_else(outside)?; + let path = Path::new(relative); + let (Some(package), Some(stem)) = (path.parent(), path.file_name()) else { + return Err(outside()); + }; + let package = package.to_string_lossy().replace('/', "."); + let stem = stem.to_string_lossy(); + let arguments: String = query + .arguments + .iter() + .map(|argument| format!("\"{argument}\", ")) + .collect(); + let row = query + .row + .map(|row| format!(" row={},\n", row.name())) + .unwrap_or_default(); + Ok(format!( + "\n{constant}: Final = {kind}(\n sql=load(\"{package}\", \"{stem}\"),\n arguments=({arguments}),\n params={params},\n{row})\n", + constant = query.name.to_uppercase(), + kind = query.cardinality.descriptor(), + arguments = arguments.trim_end_matches(' '), + params = query.params.name(), + )) +} diff --git a/litellm-rust/crates/db/src/failure.rs b/litellm-rust/crates/db/src/failure.rs index 85c52454bed..b5553bb7c98 100644 --- a/litellm-rust/crates/db/src/failure.rs +++ b/litellm-rust/crates/db/src/failure.rs @@ -1,10 +1,16 @@ use serde::Serialize; #[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "schema", schemars(extend("discriminator" = {"propertyName": "kind"})))] #[serde(tag = "kind", rename_all = "snake_case")] pub enum DbFailure { + #[cfg_attr(feature = "schema", schemars(title = "UniqueViolation"))] UniqueViolation, + #[cfg_attr(feature = "schema", schemars(title = "ForeignKeyViolation"))] ForeignKeyViolation, + #[cfg_attr(feature = "schema", schemars(title = "WriteConflict"))] WriteConflict, + #[cfg_attr(feature = "schema", schemars(title = "NotFound"))] NotFound, } diff --git a/litellm-rust/crates/db/src/lib.rs b/litellm-rust/crates/db/src/lib.rs index b7ce211d906..9e75c887085 100644 --- a/litellm-rust/crates/db/src/lib.rs +++ b/litellm-rust/crates/db/src/lib.rs @@ -1,3 +1,8 @@ +#[cfg(feature = "schema")] +pub mod codegen; mod failure; +mod queries; pub use failure::DbFailure; +#[cfg(feature = "schema")] +pub use queries::CATALOG; diff --git a/litellm-rust/crates/db/src/queries/echo.sql b/litellm-rust/crates/db/src/queries/echo.sql new file mode 100644 index 00000000000..8c30f2ff9c8 --- /dev/null +++ b/litellm-rust/crates/db/src/queries/echo.sql @@ -0,0 +1 @@ +SELECT $1::text AS first, $2::int8 AS second diff --git a/litellm-rust/crates/db/src/queries/mod.rs b/litellm-rust/crates/db/src/queries/mod.rs new file mode 100644 index 00000000000..51b465a2143 --- /dev/null +++ b/litellm-rust/crates/db/src/queries/mod.rs @@ -0,0 +1,94 @@ +#[cfg_attr( + not(all(test, feature = "schema")), + expect( + unused_macros, + reason = "no query is declared yet; the first ported query uses it" + ) +)] +macro_rules! declare_queries { + ($($cardinality:ident $name:ident($params:ident { $($field:ident: $type:ty),* $(,)? }) $(-> $row:ident)? => $file:literal;)+) => { + $( + #[cfg_attr( + not(feature = "schema"), + expect(dead_code, reason = "built by Python until Rust executes the query") + )] + #[derive(Debug, Clone, PartialEq)] + #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] + struct $params { + $($field: $type),* + } + + declare_queries!(@check $name($params { $($field),* }) $(-> $row)? => $file); + )+ + + #[cfg(feature = "schema")] + pub(crate) const QUERIES: &[$crate::codegen::QuerySpec] = &[ + $(declare_queries!(@spec $cardinality $name($params { $($field),* }) $(-> $row)? => $file)),+ + ]; + }; + (@check $name:ident($params:ident { $($field:ident),* }) -> $row:ident => $file:literal) => { + #[expect(dead_code, reason = "declared for the compile-time check; Python executes it until Rust takes it over")] + fn $name(params: &$params) { + let _ = sqlx::query_file_as!($row, $file $(, params.$field)*); + } + }; + (@check $name:ident($params:ident { $($field:ident),* }) => $file:literal) => { + #[expect(dead_code, reason = "declared for the compile-time check; Python executes it until Rust takes it over")] + fn $name(params: &$params) { + let _ = sqlx::query_file!($file $(, params.$field)*); + } + }; + (@spec Execute $name:ident($params:ident { $($field:ident),* }) -> $row:ident => $file:literal) => { + compile_error!(concat!("`", stringify!($name), "` is Execute, which returns no rows; drop `-> ", stringify!($row), "`")) + }; + (@spec $cardinality:ident $name:ident($params:ident { $($field:ident),* }) -> $row:ident => $file:literal) => { + $crate::codegen::QuerySpec { + name: stringify!($name), + file: $file, + cardinality: $crate::codegen::Cardinality::$cardinality, + arguments: &[$(stringify!($field)),*], + params: $crate::codegen::Model::of::<$params>(), + row: Some($crate::codegen::Model::of::<$row>()), + } + }; + (@spec Execute $name:ident($params:ident { $($field:ident),* }) => $file:literal) => { + $crate::codegen::QuerySpec { + name: stringify!($name), + file: $file, + cardinality: $crate::codegen::Cardinality::Execute, + arguments: &[$(stringify!($field)),*], + params: $crate::codegen::Model::of::<$params>(), + row: None, + } + }; +} + +#[cfg_attr( + not(all(test, feature = "schema")), + expect( + unused_macros, + reason = "no query is declared yet; the first ported query uses it" + ) +)] +macro_rules! declare_rows { + ($($(#[$attr:meta])* struct $name:ident { $($field:ident: $type:ty),* $(,)? })+) => { + $( + #[cfg_attr( + not(feature = "schema"), + expect(dead_code, reason = "row of a query Python executes until Rust takes it over") + )] + #[derive(Debug, Clone, PartialEq)] + #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] + $(#[$attr])* + struct $name { + $($field: $type),* + } + )+ + }; +} + +#[cfg(feature = "schema")] +pub const CATALOG: &[&[crate::codegen::QuerySpec]] = &[]; + +#[cfg(all(test, feature = "schema"))] +mod tests; diff --git a/litellm-rust/crates/db/src/queries/tests.rs b/litellm-rust/crates/db/src/queries/tests.rs new file mode 100644 index 00000000000..d7b5f526e8e --- /dev/null +++ b/litellm-rust/crates/db/src/queries/tests.rs @@ -0,0 +1,37 @@ +use rstest::rstest; + +use crate::codegen::Cardinality; + +declare_queries! { + Many echo(EchoParams { first: String, second: i64 }) -> EchoRow => "src/queries/echo.sql"; + Execute echo_discarded(EchoDiscardedParams { first: String, second: i64 }) => "src/queries/echo.sql"; +} + +declare_rows! { + struct EchoRow { + first: Option, + second: Option, + } +} + +#[rstest] +#[case::rows(0, "echo", Cardinality::Many, true)] +#[case::no_rows(1, "echo_discarded", Cardinality::Execute, false)] +fn declarations_keep_parameter_order_and_cardinality( + #[case] index: usize, + #[case] name: &str, + #[case] cardinality: Cardinality, + #[case] has_row: bool, +) { + let query = &QUERIES[index]; + + assert_eq!( + ( + query.name, + query.arguments, + query.cardinality, + query.row.is_some() + ), + (name, &["first", "second"][..], cardinality, has_row), + ); +} diff --git a/litellm/proxy/db/queries/__init__.py b/litellm/proxy/db/queries/__init__.py new file mode 100644 index 00000000000..eb1983136c2 --- /dev/null +++ b/litellm/proxy/db/queries/__init__.py @@ -0,0 +1,5 @@ +from importlib.resources import files + + +def load(package: str, name: str) -> str: + return files(package).joinpath(f"{name}.sql").read_text(encoding="utf-8") diff --git a/litellm/proxy/db/queries/generated/__init__.py b/litellm/proxy/db/queries/generated/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/litellm/proxy/db/queries/generated/catalog.py b/litellm/proxy/db/queries/generated/catalog.py new file mode 100644 index 00000000000..cdc9b6a28a1 --- /dev/null +++ b/litellm/proxy/db/queries/generated/catalog.py @@ -0,0 +1 @@ +# @generated by `make db-codegen` from litellm-rust/crates/db. Do not edit. diff --git a/litellm/proxy/db/queries/generated/models.py b/litellm/proxy/db/queries/generated/models.py new file mode 100644 index 00000000000..758ca5b2546 --- /dev/null +++ b/litellm/proxy/db/queries/generated/models.py @@ -0,0 +1,45 @@ +# @generated by `make db-codegen` from litellm-rust/crates/db. Do not edit. + +from __future__ import annotations + +from typing import Annotated, Literal + +from pydantic import BaseModel, ConfigDict, Field +from typing_extensions import TypeAliasType + + +class UniqueViolation(BaseModel): + model_config = ConfigDict( + frozen=True, + ) + kind: Literal["unique_violation"] = "unique_violation" + + +class ForeignKeyViolation(BaseModel): + model_config = ConfigDict( + frozen=True, + ) + kind: Literal["foreign_key_violation"] = "foreign_key_violation" + + +class WriteConflict(BaseModel): + model_config = ConfigDict( + frozen=True, + ) + kind: Literal["write_conflict"] = "write_conflict" + + +class NotFound(BaseModel): + model_config = ConfigDict( + frozen=True, + ) + kind: Literal["not_found"] = "not_found" + + +DbFailure = TypeAliasType( + "DbFailure", + Annotated[ + UniqueViolation | ForeignKeyViolation | WriteConflict | NotFound, + Field(discriminator="kind"), + ], +) diff --git a/litellm/proxy/db/queries/prisma_executor.py b/litellm/proxy/db/queries/prisma_executor.py new file mode 100644 index 00000000000..12ff3834b68 --- /dev/null +++ b/litellm/proxy/db/queries/prisma_executor.py @@ -0,0 +1,56 @@ +from collections.abc import Mapping, Sequence +from dataclasses import dataclass +from types import MappingProxyType +from typing import Final, Protocol + +from prisma.errors import RawQueryError +from pydantic import BaseModel, JsonValue + +from litellm.proxy.db.queries.generated.models import DbFailure, ForeignKeyViolation, UniqueViolation, WriteConflict + +FAILURES_BY_SQLSTATE: Final[Mapping[str, DbFailure]] = MappingProxyType( + { + "23505": UniqueViolation(), + "23503": ForeignKeyViolation(), + "40001": WriteConflict(), + "40P01": WriteConflict(), + } +) + + +class RawClient(Protocol): + async def query_raw(self, query: str, *args: JsonValue) -> Sequence[Mapping[str, object]]: ... + + async def execute_raw(self, query: str, *args: JsonValue) -> int: ... + + +class _RawQueryMeta(BaseModel): + code: str | None = None + + +def _failure(error: RawQueryError) -> DbFailure | None: + meta: Final = _RawQueryMeta.model_validate(error.meta if isinstance(error.meta, Mapping) else {}) + return FAILURES_BY_SQLSTATE.get(meta.code or "") + + +@dataclass(frozen=True, slots=True) +class PrismaExecutor: + client: RawClient + + async def query(self, sql: str, arguments: tuple[JsonValue, ...]) -> Sequence[Mapping[str, object]] | DbFailure: + try: + return await self.client.query_raw(sql, *arguments) + except RawQueryError as error: + failure: Final = _failure(error) + if failure is None: + raise + return failure + + async def execute(self, sql: str, arguments: tuple[JsonValue, ...]) -> int | DbFailure: + try: + return await self.client.execute_raw(sql, *arguments) + except RawQueryError as error: + failure: Final = _failure(error) + if failure is None: + raise + return failure diff --git a/litellm/proxy/db/queries/runtime.py b/litellm/proxy/db/queries/runtime.py new file mode 100644 index 00000000000..78bc3b56fca --- /dev/null +++ b/litellm/proxy/db/queries/runtime.py @@ -0,0 +1,85 @@ +from collections.abc import Mapping, Sequence +from dataclasses import dataclass +from typing import Final, Generic, Protocol, TypeVar + +from pydantic import BaseModel, JsonValue + +from litellm.proxy.db.queries.generated.models import DbFailure, NotFound + +ParamsT = TypeVar("ParamsT", bound=BaseModel) +RowT = TypeVar("RowT", bound=BaseModel) + + +class Executor(Protocol): + async def query(self, sql: str, arguments: tuple[JsonValue, ...]) -> Sequence[Mapping[str, object]] | DbFailure: ... + + async def execute(self, sql: str, arguments: tuple[JsonValue, ...]) -> int | DbFailure: ... + + +@dataclass(frozen=True, slots=True) +class ManyQuery(Generic[ParamsT, RowT]): + sql: str + arguments: tuple[str, ...] + params: type[ParamsT] + row: type[RowT] + + +@dataclass(frozen=True, slots=True) +class OptionalQuery(Generic[ParamsT, RowT]): + sql: str + arguments: tuple[str, ...] + params: type[ParamsT] + row: type[RowT] + + +@dataclass(frozen=True, slots=True) +class OneQuery(Generic[ParamsT, RowT]): + sql: str + arguments: tuple[str, ...] + params: type[ParamsT] + row: type[RowT] + + +@dataclass(frozen=True, slots=True) +class ExecuteQuery(Generic[ParamsT]): + sql: str + arguments: tuple[str, ...] + params: type[ParamsT] + + +def _bind(arguments: tuple[str, ...], params: BaseModel) -> tuple[JsonValue, ...]: + values: Final[dict[str, JsonValue]] = params.model_dump(mode="json") + return tuple(values[name] for name in arguments) + + +async def _rows( + executor: Executor, query: ManyQuery[ParamsT, RowT] | OptionalQuery[ParamsT, RowT] | OneQuery[ParamsT, RowT], params: ParamsT +) -> tuple[RowT, ...] | DbFailure: + rows: Final = await executor.query(query.sql, _bind(query.arguments, params)) + if not isinstance(rows, Sequence): + return rows + return tuple(query.row.model_validate(row) for row in rows) + + +async def fetch_many(executor: Executor, query: ManyQuery[ParamsT, RowT], params: ParamsT) -> tuple[RowT, ...] | DbFailure: + return await _rows(executor, query, params) + + +async def fetch_optional( + executor: Executor, query: OptionalQuery[ParamsT, RowT], params: ParamsT +) -> RowT | None | DbFailure: + rows: Final = await _rows(executor, query, params) + if not isinstance(rows, tuple): + return rows + return rows[0] if rows else None + + +async def fetch_one(executor: Executor, query: OneQuery[ParamsT, RowT], params: ParamsT) -> RowT | DbFailure: + rows: Final = await _rows(executor, query, params) + if not isinstance(rows, tuple): + return rows + return rows[0] if rows else NotFound() + + +async def execute(executor: Executor, query: ExecuteQuery[ParamsT], params: ParamsT) -> int | DbFailure: + return await executor.execute(query.sql, _bind(query.arguments, params)) diff --git a/pyproject.toml b/pyproject.toml index 28b00379cc7..62b3d879d86 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -236,6 +236,8 @@ dev = [ "pytest-timeout==2.4.0", "vcrpy==8.2.1", "pytest-recording==0.13.4", + "datamodel-code-generator==0.83.0", + "ast-grep-cli==0.45.3", ] e2e-dev = [ "playwright==1.61.0", diff --git a/uv.lock b/uv.lock index 527f53bd372..d826af96ae1 100644 --- a/uv.lock +++ b/uv.lock @@ -339,6 +339,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9f/64/2e54428beba8d9992aa478bb8f6de9e4ecaa5f8f513bcfd567ed7fb0262d/apscheduler-3.11.2-py3-none-any.whl", hash = "sha256:ce005177f741409db4e4dd40a7431b76feb856b9dd69d57e0da49d6715bfd26d", size = 64439, upload-time = "2025-12-22T00:39:33.303Z" }, ] +[[package]] +name = "argcomplete" +version = "3.7.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/87/6f/5a73f04007ca950701765949209f068da628bd11f9c2da287278ce91e0ee/argcomplete-3.7.2.tar.gz", hash = "sha256:aad8b69a0b9969edb62db0d1752354c0d50717b10e0cbb00e2a958381b9fc6b9", size = 74473, upload-time = "2026-08-06T04:53:21.662Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/bd/551ee6af426af84ca33e02622be722925c196608e9127d731ef17c47f06e/argcomplete-3.7.2-py3-none-any.whl", hash = "sha256:6029205678bdd9c1c728a155f5f9ecf5812393f969eef58807641a2bc2aa5b19", size = 43294, upload-time = "2026-08-06T04:53:20.246Z" }, +] + [[package]] name = "argon2-cffi" version = "25.1.0" @@ -415,6 +424,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/66/00/24d6c9d60e4f2036cee7cc28fb73a7ab5e71c0171201586c9fbc383021ea/assemblyai-0.52.4-py3-none-any.whl", hash = "sha256:4d358c9cc4c6685cde296ccfa0a544417272a2b74a248e92273db7af6ea60901", size = 53900, upload-time = "2026-02-25T15:30:21.337Z" }, ] +[[package]] +name = "ast-grep-cli" +version = "0.45.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/80/46/35e89cbf27fe8a866796f9cb6ae711b88e0ff67e14e2580ced649b3d6a19/ast_grep_cli-0.45.3.tar.gz", hash = "sha256:d8b527b71655eede16637311bc3ca49624b8198c48bffeb5dcf06a78332bf917", size = 283689, upload-time = "2026-08-31T02:27:07.267Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/8d/fded0d3cf2138b56454101e061ba8525bd47c9d022d924fd92320fad4d98/ast_grep_cli-0.45.3-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ebc1510a6775f65a8681345dd422590fb257fe48036a7c599a5ca9f54dea8801", size = 15743557, upload-time = "2026-08-31T01:50:19.053Z" }, + { url = "https://files.pythonhosted.org/packages/30/f8/d9b41ae197bf9a41034a03d742d9bb2ca05bf4f7da741c00242019b44d3e/ast_grep_cli-0.45.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7afbe49870d8a34c0b224ac940da1fbf736617fcc81aa846884e377eddf8c1a0", size = 7870417, upload-time = "2026-08-31T01:50:21.881Z" }, + { url = "https://files.pythonhosted.org/packages/a9/d5/584cc4cf46a751f35fc086e9dca9962c05de2b37f95056f3653dc4736e19/ast_grep_cli-0.45.3-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:ced023ae9fbc7c779570cd8bcc0fa7626d9561afd60369d3c335bfce2ca565b3", size = 7752458, upload-time = "2026-08-31T01:50:23.52Z" }, + { url = "https://files.pythonhosted.org/packages/c2/20/1445bff6593d492c31904fe309c157cb16bcb73cfd30b4a55718acef5cb1/ast_grep_cli-0.45.3-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:2d151b89c6d45c2640338fe5d24b2ee0810224902d603cb271803bf7418054ea", size = 8027608, upload-time = "2026-08-31T01:50:25.179Z" }, + { url = "https://files.pythonhosted.org/packages/2e/da/03562d9c4994d756d7fa7b751f7e92f4218d96fd86bdd27a5fe7be63e4d8/ast_grep_cli-0.45.3-py3-none-win32.whl", hash = "sha256:b96c897dc44052c7b1d08ba6086d0c270fe89d37645e38f08e2d4fa15948ea3c", size = 14970295, upload-time = "2026-08-31T01:50:27.325Z" }, + { url = "https://files.pythonhosted.org/packages/b2/cc/a8d5a4039470b58a4b0a5ce9826975796ff091539849984f04a0ac98582f/ast_grep_cli-0.45.3-py3-none-win_amd64.whl", hash = "sha256:13dfb43b2028bb585f41b2f399e8b30bf0310e9bbeff2369d60a2e36accbb2e6", size = 15914271, upload-time = "2026-08-31T01:50:30.211Z" }, + { url = "https://files.pythonhosted.org/packages/ec/8e/bb893d035ee6349a07287f83b471fc54b067c2c61b34dd6c9e2072a7064f/ast_grep_cli-0.45.3-py3-none-win_arm64.whl", hash = "sha256:6c4222e17ac7a9e23c839b6eefb3a7858349e7a37a704acd451c266beadf75b4", size = 15293472, upload-time = "2026-08-31T02:27:05.122Z" }, +] + [[package]] name = "astroid" version = "4.0.4" @@ -758,6 +782,50 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5", size = 32764, upload-time = "2024-02-18T19:09:04.156Z" }, ] +[[package]] +name = "black" +version = "26.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "pytokens" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/37/5628dd55bf2b34257fc7603f0fe97c40e3aaf24265f416a9c85c95ca1436/black-26.5.1.tar.gz", hash = "sha256:dd321f668053961824bcc1be1cc1df748b2d7e4fa28086b08331e577b0100a73", size = 679439, upload-time = "2026-05-18T16:53:36.107Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/84/b3f55026206a9e8820a91503308075ca48eadc515e436731ca01dbe043b3/black-26.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9942db8888e06943c5dde66ca0037dcff82a2a4ec1ad0ada9e0d2ee9d9823893", size = 1987719, upload-time = "2026-05-18T17:05:02.757Z" }, + { url = "https://files.pythonhosted.org/packages/c6/34/7db312c5e5783d6e76cffd9d5ac8972a32badae4c6e3288dac0eed8d3bed/black-26.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:89c93167a74d3a75dfaa38a5c7cca015537d5820dd7f17d63267d674a61cae90", size = 1810083, upload-time = "2026-05-18T17:05:04.302Z" }, + { url = "https://files.pythonhosted.org/packages/33/e2/e0101e73c2c8727634e2efcb35e2b34bd23ad70dfa673789f5773a591b21/black-26.5.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22f2cd76d069cc54c71f10360744ba8983fbb616903b4304a85b734915c8e1b4", size = 1860633, upload-time = "2026-05-18T17:05:06.391Z" }, + { url = "https://files.pythonhosted.org/packages/b0/4c/e15c0c5b23cf3651035fe5addcce90e283af3548a3f91bb03d81b83106ab/black-26.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:87ed5c6f450580a2f6790bc7cbfb016dfc73bc750249762268a3695361315eef", size = 1477886, upload-time = "2026-05-18T17:05:07.96Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3f/59d43ade98d2ce5c8dc34a4e46cbecd177e6d55d7d4092969c6003ccc655/black-26.5.1-cp310-cp310-win_arm64.whl", hash = "sha256:58b4bd92cf88aacf83d88479c8f9caee044b1ec55f2451a337354a7ea2590a22", size = 1277111, upload-time = "2026-05-18T17:05:09.473Z" }, + { url = "https://files.pythonhosted.org/packages/4b/96/3c3e09f09f44a37aac36b178a279cd19aa7001bd796187a7b162a294c81f/black-26.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:96ae2c733b2aabdd9986e2c5df628ff3473676cd1c5faded1ff496cf6d74083c", size = 1970639, upload-time = "2026-05-18T17:05:11.461Z" }, + { url = "https://files.pythonhosted.org/packages/83/ea/5ad117b9ee3ecd933c712bcbae610006e5b7cc9f41c526cd7ed3b6c4124c/black-26.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0e48b87e03bf109288e55cfceadcfa15ff5470aca2851a851950ed2926f450d7", size = 1792130, upload-time = "2026-05-18T17:05:12.983Z" }, + { url = "https://files.pythonhosted.org/packages/06/3a/7c448bc623fcdfa96672531beb5a616ea5e64f6975955254d7731ffb0ad9/black-26.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5119fa92ae61f786e8c3662fd60aece1d0a2dd5cca5d0c79417a95e7a4272a59", size = 1846134, upload-time = "2026-05-18T17:05:14.506Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5b/0b39b3a5917f0657ac014ad2edb58c139553a478adfe7f817abf1622ff6e/black-26.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:30d3c14661f2792e9142cce3eeeb1cbc175b3eb5f733be0c8eeb99651e52b0c3", size = 1478883, upload-time = "2026-05-18T17:05:16.542Z" }, + { url = "https://files.pythonhosted.org/packages/4c/48/dc222692e0f95030db1bbfb6c857e76858bad09058221ea7aae815255327/black-26.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:1ef92b76f7733f282fd096ea406200b5a286c42947412b0eaff3a74e3616cefe", size = 1277776, upload-time = "2026-05-18T17:05:18.029Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/7744b906703228264ef73bdd534df88ec1ef3de45c4e78f6d31b9e32d0c9/black-26.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4ad6fa01f941920f54f2bbb35f3df7673428a0ef98a0b0840c2eaef3b110efa8", size = 2012518, upload-time = "2026-05-18T17:05:20.108Z" }, + { url = "https://files.pythonhosted.org/packages/b7/c0/c5a3b1636dfd09c42534f2b3cf33506814f6d3e066fb0879ffa16c1ae860/black-26.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3915f256e75a2d7cf88d8953d37f780455dc586cc72dee059c528fe77f581217", size = 1816016, upload-time = "2026-05-18T17:05:21.84Z" }, + { url = "https://files.pythonhosted.org/packages/1f/0e/36044316b65ca471d3bb6d3703fd06fb50c6b727c3562f6a5a3153634f88/black-26.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d98d4137277c75dfb898ec8d846c4fd68ba1e9cf77f95e2865c203dc18f4c3d", size = 1884150, upload-time = "2026-05-18T17:05:23.546Z" }, + { url = "https://files.pythonhosted.org/packages/b3/33/dafc5808c2af43672912111d7c3354af1615f7e2be3bed7a878461abbe4d/black-26.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:a1dca32d9f1784af512a13410ec204c6f7f0aa9797a111c42e1c03449821c264", size = 1486825, upload-time = "2026-05-18T17:05:25.004Z" }, + { url = "https://files.pythonhosted.org/packages/82/14/b965ee6ad2a311f28bdbf692def3ee9848d2ae289dab28b27657fcee3e78/black-26.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:1037d5ac7b7b310b2632ad867ec8d0e4c4819dcdb0b820f63135da746a24e418", size = 1288646, upload-time = "2026-05-18T17:05:26.477Z" }, + { url = "https://files.pythonhosted.org/packages/3f/5c/c384363980e11e25ca6b93205949bb331fbf35f4e0dbec376dfa6326cec8/black-26.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b36cf2ddf5566e205f6535f782a62194a184d33e175b64ae8c40b1737522be3", size = 2009020, upload-time = "2026-05-18T17:05:28.132Z" }, + { url = "https://files.pythonhosted.org/packages/0b/df/9f31c5e0babbfed77d505fc5d120beb98b21b33feaeded3924ea941fe360/black-26.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f7ea64ebfa01b50f693508fc39f875e264446d3b097088f84f203b9d09618a0", size = 1813335, upload-time = "2026-05-18T17:05:31.266Z" }, + { url = "https://files.pythonhosted.org/packages/fb/24/8e7b9a2fa61b0afd82209efe937557d180a1fa055bd7f6161eb9defc3719/black-26.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecb3e624844c798144e9bd986954e0adc81d8911a1f30f375e1252fe26e8c294", size = 1881614, upload-time = "2026-05-18T17:05:32.718Z" }, + { url = "https://files.pythonhosted.org/packages/49/ad/b4e0d9365ba8ac34f6bbab62a4b1b2dd5d618fac3fa1b8db968c844201b5/black-26.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:e1a26503279b6b310669fb0b219c39e4820b77e8189fe80f522bb511f247db0a", size = 1488925, upload-time = "2026-05-18T17:05:34.259Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4b/652b859bf5df88a751c30451b09338f7fd26a77d1271c666992f836b7711/black-26.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c34b25da232ead53a6f335b76dbea124f4d152ad568b9080d6f944bc2b34b52", size = 1289883, upload-time = "2026-05-18T17:05:36.019Z" }, + { url = "https://files.pythonhosted.org/packages/a6/16/a8da8eb208c51c7f4ce74609a45d0dcc6d8a2141e45e81ee5289d1bb0d59/black-26.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e88976690a64b0af98312ca958415849cb42423423c5f2ee74af4b49a97a2168", size = 2004800, upload-time = "2026-05-18T17:05:38.182Z" }, + { url = "https://files.pythonhosted.org/packages/11/8a/a479296a19e383b70a725882a6cf3d786540601ff03cabbaaf1cce864c5a/black-26.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32d5ea7f6c8bdfa6e648326ebca1f02b0764e2a029edc6f8dce2627e19d468c3", size = 1815576, upload-time = "2026-05-18T17:05:40.309Z" }, + { url = "https://files.pythonhosted.org/packages/81/6b/cfaf3d39f25132c156a068f6b805576c9103a84086019507c70e1911ee7d/black-26.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea8d16dc41655aa113cd64665e7219446cd7e4ff2248d7178eaa905190c86b18", size = 1877927, upload-time = "2026-05-18T17:05:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/66/76/302e313964bcff7e28df329d39f84f5270095730d85ff0acc260610a0d82/black-26.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:577f21094ea469ef92ec1adaf2c9441a226d2144d01a5be2fa823cecf6543e50", size = 1511860, upload-time = "2026-05-18T17:05:43.943Z" }, + { url = "https://files.pythonhosted.org/packages/27/4e/a3827e35e0e567f9f9ee59e2a0ab979267dca98718f25547ca8c6733afd4/black-26.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:ed1a20af114c301a0269bf01163d51dbef72737fd65f850001e7cbe7f3c7abae", size = 1316632, upload-time = "2026-05-18T17:05:45.521Z" }, + { url = "https://files.pythonhosted.org/packages/94/51/f975cae76d44274cc2868dc9040ac5d58d464784610234455b4e7b19c6ef/black-26.5.1-py3-none-any.whl", hash = "sha256:4ed7f7da04046d2e488437170797d3b4a4ad83906683bcb7dfc68b673bbce5e2", size = 213693, upload-time = "2026-05-18T16:53:33.964Z" }, +] + [[package]] name = "blinker" version = "1.9.0" @@ -1615,6 +1683,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c3/be/d0d44e092656fe7a06b55e6103cbce807cdbdee17884a5367c68c9860853/dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a", size = 28686, upload-time = "2024-06-09T16:20:16.715Z" }, ] +[[package]] +name = "datamodel-code-generator" +version = "0.83.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argcomplete" }, + { name = "black", marker = "sys_platform != 'emscripten'" }, + { name = "genson" }, + { name = "inflect" }, + { name = "isort", marker = "sys_platform != 'emscripten'" }, + { name = "jinja2" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/ad/bd0c954c4c4671e586b340059139ead38570ba916a237086e245982ab14d/datamodel_code_generator-0.83.0.tar.gz", hash = "sha256:30d841e31b82095c4ef11f3dbfc99525f559a146250b9e685e2a2e4a196ca0a1", size = 3074761, upload-time = "2026-09-24T08:56:40.622Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/4e/4a0231a7d3d4043cd27a746a50be87bc7637463023d7db9991610cd5fe56/datamodel_code_generator-0.83.0-py3-none-any.whl", hash = "sha256:371d80be360b2a57bc48462169553af57fa30225c0264ad85ae5f0d59b9d143a", size = 751087, upload-time = "2026-09-24T08:56:38.791Z" }, +] + [[package]] name = "ddtrace" version = "4.11.0" @@ -2229,6 +2318,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/3c/6a2bf344106328fd04963664a60b9bb6496fc25df8e962fcdc1367285fb9/fsspec-2026.7.0-py3-none-any.whl", hash = "sha256:b57ddbafedfaef7018c1ecab32aa200a9d7ca26b77965f64e48b70061249d279", size = 206583, upload-time = "2026-07-28T16:34:49.538Z" }, ] +[[package]] +name = "genson" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/53/de162dc8e03fccd9ebe59d17c7812378fe8bd2b604f6b1b94d00165140ac/genson-1.4.0.tar.gz", hash = "sha256:bc7f1c1bae87a21ca44d81149aec95a3f4468d676de9b8b08caa064f3c50b3da", size = 47908, upload-time = "2026-07-06T08:21:50.331Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/02/767f744ab6d4cb7761e5008acc3d534b7a0481af62563d52e391fbcb2140/genson-1.4.0-py3-none-any.whl", hash = "sha256:03bc71bbe52defde70660cc4dcd1ea1097997da5a1cbb90a9dbd3acc7c9e1b65", size = 24484, upload-time = "2026-07-06T08:21:49.046Z" }, +] + [[package]] name = "gevent" version = "25.9.1" @@ -3637,6 +3735,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514, upload-time = "2024-09-11T14:56:07.019Z" }, ] +[[package]] +name = "inflect" +version = "7.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, + { name = "typeguard" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/c6/943357d44a21fd995723d07ccaddd78023eace03c1846049a2645d4324a3/inflect-7.5.0.tar.gz", hash = "sha256:faf19801c3742ed5a05a8ce388e0d8fe1a07f8d095c82201eb904f5d27ad571f", size = 73751, upload-time = "2024-12-28T17:11:18.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/eb/427ed2b20a38a4ee29f24dbe4ae2dafab198674fe9a85e3d6adf9e5f5f41/inflect-7.5.0-py3-none-any.whl", hash = "sha256:2aea70e5e70c35d8350b8097396ec155ffd68def678c7ff97f51aa69c1d92344", size = 35197, upload-time = "2024-12-28T17:11:15.931Z" }, +] + [[package]] name = "inflection" version = "0.5.1" @@ -4679,8 +4790,10 @@ ci = [ { name = "traceloop-sdk" }, ] dev = [ + { name = "ast-grep-cli" }, { name = "basedpyright" }, { name = "botocore-stubs" }, + { name = "datamodel-code-generator" }, { name = "diff-cover" }, { name = "fakeredis" }, { name = "fastapi-offline" }, @@ -4894,8 +5007,10 @@ ci = [ { name = "traceloop-sdk", specifier = "==0.34.0" }, ] dev = [ + { name = "ast-grep-cli", specifier = "==0.45.3" }, { name = "basedpyright", specifier = "==1.39.7" }, { name = "botocore-stubs", specifier = "==1.43.14" }, + { name = "datamodel-code-generator", specifier = "==0.83.0" }, { name = "diff-cover", specifier = "==9.7.2" }, { name = "fakeredis", specifier = "==2.34.1" }, { name = "fastapi-offline", specifier = "==1.7.6" }, @@ -8228,6 +8343,45 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/07/14/49d9828443b58bd5cc80a454c91b0f867fbf36a24975d501945e6cb9e32f/python3_saml-1.16.0-py3-none-any.whl", hash = "sha256:20b97d11b04f01ee22e98f4a38242e2fea2e28fbc7fbc9bdd57cab5ac7fc2d0d", size = 76155, upload-time = "2023-10-09T10:40:34.001Z" }, ] +[[package]] +name = "pytokens" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/24/f206113e05cb8ef51b3850e7ef88f20da6f4bf932190ceb48bd3da103e10/pytokens-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5", size = 161522, upload-time = "2026-01-30T01:02:50.393Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e9/06a6bf1b90c2ed81a9c7d2544232fe5d2891d1cd480e8a1809ca354a8eb2/pytokens-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe", size = 246945, upload-time = "2026-01-30T01:02:52.399Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/f6fb1007a4c3d8b682d5d65b7c1fb33257587a5f782647091e3408abe0b8/pytokens-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:670d286910b531c7b7e3c0b453fd8156f250adb140146d234a82219459b9640c", size = 259525, upload-time = "2026-01-30T01:02:53.737Z" }, + { url = "https://files.pythonhosted.org/packages/04/92/086f89b4d622a18418bac74ab5db7f68cf0c21cf7cc92de6c7b919d76c88/pytokens-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4e691d7f5186bd2842c14813f79f8884bb03f5995f0575272009982c5ac6c0f7", size = 262693, upload-time = "2026-01-30T01:02:54.871Z" }, + { url = "https://files.pythonhosted.org/packages/b4/7b/8b31c347cf94a3f900bdde750b2e9131575a61fdb620d3d3c75832262137/pytokens-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:27b83ad28825978742beef057bfe406ad6ed524b2d28c252c5de7b4a6dd48fa2", size = 103567, upload-time = "2026-01-30T01:02:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/3d/92/790ebe03f07b57e53b10884c329b9a1a308648fc083a6d4a39a10a28c8fc/pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440", size = 160864, upload-time = "2026-01-30T01:02:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/13/25/a4f555281d975bfdd1eba731450e2fe3a95870274da73fb12c40aeae7625/pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc", size = 248565, upload-time = "2026-01-30T01:02:59.912Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/bc0394b4ad5b1601be22fa43652173d47e4c9efbf0044c62e9a59b747c56/pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d", size = 260824, upload-time = "2026-01-30T01:03:01.471Z" }, + { url = "https://files.pythonhosted.org/packages/4e/54/3e04f9d92a4be4fc6c80016bc396b923d2a6933ae94b5f557c939c460ee0/pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16", size = 264075, upload-time = "2026-01-30T01:03:04.143Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1b/44b0326cb5470a4375f37988aea5d61b5cc52407143303015ebee94abfd6/pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6", size = 103323, upload-time = "2026-01-30T01:03:05.412Z" }, + { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" }, + { url = "https://files.pythonhosted.org/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", size = 268076, upload-time = "2026-01-30T01:03:10.957Z" }, + { url = "https://files.pythonhosted.org/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", size = 103552, upload-time = "2026-01-30T01:03:12.066Z" }, + { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, + { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" }, + { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" }, + { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" }, + { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" }, + { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" }, + { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, +] + [[package]] name = "pytz" version = "2026.2" @@ -9923,6 +10077,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, ] +[[package]] +name = "typeguard" +version = "4.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/de/4420db493fa8fc0856d5e5c1b159c63a323d2de2317babe36b01568928e8/typeguard-4.6.0.tar.gz", hash = "sha256:e7414f09111317de3e335de92cd397c5c0ca00b1cc1676de12e1d444a79b3f21", size = 82330, upload-time = "2026-07-26T08:40:23.207Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/eb/461d5f167b6f5c7d97696f397c82f82e3480e003fce3f0a1cd1dd26e2eb2/typeguard-4.6.0-py3-none-any.whl", hash = "sha256:79878165bb86f2cf5d41d159a0ff1792a796cf496882d2fe1b1c6c7049b9cdd7", size = 36884, upload-time = "2026-07-26T08:40:21.868Z" }, +] + [[package]] name = "types-awscrt" version = "0.34.1" @@ -10132,11 +10298,11 @@ wheels = [ [[package]] name = "typing-extensions" -version = "4.15.0" +version = "4.16.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" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/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" }, + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ] [[package]]