From 9aee6b0edc8bca210718ced1dbd89e87a5b04f0c Mon Sep 17 00:00:00 2001 From: Lucas Christian Date: Fri, 10 Jul 2026 22:51:50 -0300 Subject: [PATCH 1/5] Refactor code structure for improved readability and maintainability; removed redundant code blocks and optimized functions. --- docs/explanation/architecture.md | 7 +- docs/explanation/extensibility.md | 26 +- docs/reference/api.md | 1 - src/pyrsql/__init__.py | 6 - src/pyrsql/api.py | 6 +- src/pyrsql/core/__init__.py | 8 - src/pyrsql/core/compiler.py | 34 +- src/pyrsql/core/conversion.py | 5 +- src/pyrsql/core/options.py | 194 ++----- src/pyrsql/core/page.py | 13 +- src/pyrsql/core/query.py | 12 +- src/pyrsql/core/sort.py | 12 +- src/pyrsql/orms/__init__.py | 6 - src/pyrsql/orms/base.py | 24 +- src/pyrsql/orms/sqlalchemy/compiled_page.py | 10 +- src/pyrsql/orms/sqlalchemy/compiled_query.py | 2 +- src/pyrsql/orms/sqlalchemy/compiled_sort.py | 2 +- src/pyrsql/orms/sqlalchemy/introspection.py | 20 +- src/pyrsql/orms/sqlalchemy/json_path.py | 15 +- src/pyrsql/orms/sqlalchemy/orm.py | 43 +- src/pyrsql/orms/sqlalchemy/resolver.py | 18 +- src/pyrsql/orms/sqlalchemy/sorter.py | 22 +- src/pyrsql/orms/sqlalchemy/translator.py | 19 +- src/pyrsql/orms/sqlalchemy/type_inference.py | 13 +- src/pyrsql/orms/sqlalchemy/types.py | 2 - src/pyrsql/parsing/__init__.py | 5 +- src/pyrsql/parsing/ast.py | 24 +- src/pyrsql/parsing/diagnostics.py | 9 - src/pyrsql/parsing/errors.py | 2 - src/pyrsql/parsing/lexer.py | 36 +- src/pyrsql/parsing/limits.py | 29 -- src/pyrsql/parsing/normalization.py | 51 -- src/pyrsql/parsing/operators.py | 93 +--- src/pyrsql/parsing/parser.py | 23 +- src/pyrsql/parsing/source.py | 23 - src/pyrsql/selector/ast.py | 26 +- src/pyrsql/selector/parser.py | 106 ++-- src/pyrsql/semantic/binder.py | 12 - src/pyrsql/sorting/binder.py | 6 - src/pyrsql/sorting/errors.py | 2 +- src/pyrsql/sorting/limits.py | 17 - src/pyrsql/sorting/parser.py | 39 +- tests/functional/test_public_api.py | 10 +- .../adapters/fastapi_adapter/test_criteria.py | 17 +- tests/unit/core/conftest.py | 5 - tests/unit/core/test_options.py | 2 + tests/unit/core/test_page.py | 6 +- tests/unit/core/test_query.py | 6 +- tests/unit/core/test_sort.py | 6 +- tests/unit/parsing/test_lexer.py | 32 -- tests/unit/parsing/test_operators.py | 105 ---- tests/unit/parsing/test_parser.py | 72 --- tests/unit/parsing/test_source.py | 7 +- tests/unit/selector/test_ast.py | 30 -- tests/unit/selector/test_parser.py | 98 +--- tests/unit/sorting/test_parser.py | 62 --- uv.lock | 477 +++++++++--------- 57 files changed, 443 insertions(+), 1515 deletions(-) delete mode 100644 src/pyrsql/parsing/diagnostics.py delete mode 100644 src/pyrsql/parsing/normalization.py delete mode 100644 tests/unit/parsing/test_operators.py delete mode 100644 tests/unit/selector/test_ast.py diff --git a/docs/explanation/architecture.md b/docs/explanation/architecture.md index 7a0ae7e..ab161ff 100644 --- a/docs/explanation/architecture.md +++ b/docs/explanation/architecture.md @@ -108,7 +108,6 @@ Owns user-facing ORM-agnostic objects: - `JSONPath`, `JSONPathComparison` - `JSONScalarNormalizer`, `JSONScalarValue` - `JoinHint` -- `CompilationResult`, `SortCompilationResult`, `PageCompilationResult` The `core/json/` package owns JSON-aware comparison models and value normalization, keeping JSON semantics ORM-neutral. @@ -185,9 +184,9 @@ Implement `pyrsql.orms.base.ORM`: ```python class ORM(ABC): - def compile_query(self, query: Query) -> CompiledQuery: ... - def compile_sort(self, sort: Sort) -> CompiledSort: ... - def compile_page_request(self, page_request: PageRequest) -> CompiledPageRequest: ... + def compile_query(self, query: Query) -> CompiledArtifact: ... + def compile_sort(self, sort: Sort) -> CompiledArtifact: ... + def compile_page_request(self, page_request: PageRequest) -> CompiledArtifact: ... ``` Each method receives one ORM-neutral core object and returns a compiled object diff --git a/docs/explanation/extensibility.md b/docs/explanation/extensibility.md index 77cc3a6..3573dc7 100644 --- a/docs/explanation/extensibility.md +++ b/docs/explanation/extensibility.md @@ -9,20 +9,17 @@ The core (`pyrsql.core`, `pyrsql.parsing`, `pyrsql.semantic`, Implement the `ORM` abstract base class: ```python -from pyrsql.orms.base import ORM, CompiledPageRequest, CompiledQuery, CompiledSort +from pyrsql.core.compiler import CompiledArtifact +from pyrsql.orms.base import ORM class MyORM(ORM): - @property - def name(self) -> str: - return "my-orm" - - def compile_query(self, query: Query) -> CompiledQuery: + def compile_query(self, query: Query) -> CompiledArtifact: ... - def compile_sort(self, sort: Sort) -> CompiledSort: + def compile_sort(self, sort: Sort) -> CompiledArtifact: ... - def compile_page_request(self, page_request: PageRequest) -> CompiledPageRequest: + def compile_page_request(self, page_request: PageRequest) -> CompiledArtifact: ... ``` @@ -49,23 +46,18 @@ The FastAPI adapter (`pyrsql.adapters.fastapi`) is the reference: ## Backend contract in detail -### `CompiledQuery` +### `CompiledArtifact` ```python -class CompiledQuery(Protocol): +class CompiledArtifact(Protocol): def apply(self, target: _TargetT, model: type[_ModelT]) -> _TargetT: ... ``` Receives an ORM-specific target and model class, then returns the modified target with query conditions applied. -### `CompiledSort` - -Same contract as `CompiledQuery`, but applies sort ordering. - -### `CompiledPageRequest` - -Same contract, but applies pagination semantics such as `LIMIT` and `OFFSET`. +The same contract applies to query, sort, and pagination artifacts such as +`LIMIT` and `OFFSET`. ## Integration layer diff --git a/docs/reference/api.md b/docs/reference/api.md index 0fc7c04..6aed143 100644 --- a/docs/reference/api.md +++ b/docs/reference/api.md @@ -27,7 +27,6 @@ - ValueConverterRegistry - JoinHint - ORM - - CompilationResult - DEFAULT_JSON_OPTIONS ## Subpackages diff --git a/src/pyrsql/__init__.py b/src/pyrsql/__init__.py index 0954b39..5cb5fd3 100644 --- a/src/pyrsql/__init__.py +++ b/src/pyrsql/__init__.py @@ -4,7 +4,6 @@ from pyrsql.api import apply, compile, parse # noqa: A004 from pyrsql.core import ( DEFAULT_JSON_OPTIONS, - CompilationResult, CustomPredicateDefinition, FieldPolicySet, FieldValueConverterSet, @@ -14,13 +13,11 @@ JSONScalarNormalizer, JSONScalarValue, JSONSortScalarType, - PageCompilationResult, PageRequest, ProcedureAccessPolicy, Query, QueryOptions, Sort, - SortCompilationResult, SortOptions, ValueConverter, ValueConverterRegistry, @@ -30,7 +27,6 @@ __all__ = ( "DEFAULT_JSON_OPTIONS", "ORM", - "CompilationResult", "CustomPredicateDefinition", "FieldPolicySet", "FieldValueConverterSet", @@ -40,13 +36,11 @@ "JSONScalarNormalizer", "JSONScalarValue", "JSONSortScalarType", - "PageCompilationResult", "PageRequest", "ProcedureAccessPolicy", "Query", "QueryOptions", "Sort", - "SortCompilationResult", "SortOptions", "ValueConverter", "ValueConverterRegistry", diff --git a/src/pyrsql/api.py b/src/pyrsql/api.py index cdd05cd..9ef6792 100644 --- a/src/pyrsql/api.py +++ b/src/pyrsql/api.py @@ -7,7 +7,7 @@ from pyrsql.core.query import Query if TYPE_CHECKING: - from pyrsql.core.compiler import CompilationResult + from pyrsql.core.compiler import CompiledArtifact from pyrsql.core.options import QueryOptions from pyrsql.orms.base import ORM @@ -33,7 +33,7 @@ def compile( # noqa: A001 *, orm: ORM, options: QueryOptions | None = None, -) -> CompilationResult: +) -> CompiledArtifact: """Compiles raw RSQL text using the provided ORM. Args: @@ -42,7 +42,7 @@ def compile( # noqa: A001 options: Optional query configuration. Returns: - The ORM-specific compilation result. + The ORM-specific compiled query. """ return parse(query_text, options=options).compile(orm=orm) diff --git a/src/pyrsql/core/__init__.py b/src/pyrsql/core/__init__.py index bbccd20..e68197a 100644 --- a/src/pyrsql/core/__init__.py +++ b/src/pyrsql/core/__init__.py @@ -1,10 +1,5 @@ """Core ORM-agnostic pyrsql types.""" -from pyrsql.core.compiler import ( - CompilationResult, - PageCompilationResult, - SortCompilationResult, -) from pyrsql.core.conversion import ( FieldValueConverterSet, ValueConverter, @@ -28,7 +23,6 @@ __all__ = [ "DEFAULT_JSON_OPTIONS", - "CompilationResult", "CustomPredicateDefinition", "FieldPolicySet", "FieldValueConverterSet", @@ -38,13 +32,11 @@ "JSONScalarNormalizer", "JSONScalarValue", "JSONSortScalarType", - "PageCompilationResult", "PageRequest", "ProcedureAccessPolicy", "Query", "QueryOptions", "Sort", - "SortCompilationResult", "SortOptions", "ValueConverter", "ValueConverterRegistry", diff --git a/src/pyrsql/core/compiler.py b/src/pyrsql/core/compiler.py index ca11b27..bf49381 100644 --- a/src/pyrsql/core/compiler.py +++ b/src/pyrsql/core/compiler.py @@ -1,10 +1,8 @@ -"""ORM-neutral compilation result wrapper.""" +"""ORM-neutral compiled artifact contract.""" from __future__ import annotations -from typing import Protocol, TypeAlias, TypeVar - -import msgspec +from typing import Protocol, TypeVar _TargetT = TypeVar("_TargetT") _ModelT = TypeVar("_ModelT") @@ -15,31 +13,3 @@ class CompiledArtifact(Protocol): def apply(self, target: _TargetT, model: type[_ModelT]) -> _TargetT: """Applies a compiled artifact to an ORM-specific target.""" - - -class CompilationResult(msgspec.Struct, frozen=True, gc=False, kw_only=True): - """Wraps one ORM-specific compiled artifact. - - Attributes: - orm_name: Name of the ORM used to produce the compilation. - compiled: ORM-specific compiled payload implementing ``apply``. - """ - - orm_name: str - compiled: CompiledArtifact - - def apply(self, target: _TargetT, model: type[_ModelT]) -> _TargetT: - """Applies the compiled artifact to an ORM-specific target. - - Args: - target: ORM-specific target to mutate. - model: ORM model class used to resolve the compilation. - - Returns: - The value returned by the compiled artifact application. - """ - return self.compiled.apply(target=target, model=model) - - -SortCompilationResult: TypeAlias = CompilationResult -PageCompilationResult: TypeAlias = CompilationResult diff --git a/src/pyrsql/core/conversion.py b/src/pyrsql/core/conversion.py index 3776bbf..b09a769 100644 --- a/src/pyrsql/core/conversion.py +++ b/src/pyrsql/core/conversion.py @@ -35,9 +35,8 @@ def __init__( cause: Exception | None = None, ) -> None: """Builds one normalized conversion error instance.""" - target_type_name = getattr(target_type, "__name__", repr(target_type)) super().__init__( - f"Failed to convert {raw_value!r} to {target_type_name}.", + f"Failed to convert {raw_value!r} to {target_type.__name__}.", ) if cause is not None: self.__cause__ = cause @@ -245,7 +244,7 @@ def _convert_without_registered_converter( @staticmethod def _convert_enum( raw_value: RawValue, - target_type: TargetType, + target_type: type[Enum], ) -> ConvertedValue: """Converts a string into an enum member by name first. diff --git a/src/pyrsql/core/options.py b/src/pyrsql/core/options.py index d8fbdf1..ea2f4e9 100644 --- a/src/pyrsql/core/options.py +++ b/src/pyrsql/core/options.py @@ -3,7 +3,7 @@ from __future__ import annotations from types import MappingProxyType -from typing import TYPE_CHECKING, Any, Final, Protocol, TypeVar, cast +from typing import TYPE_CHECKING, Any, Protocol, TypeVar, cast import msgspec @@ -36,7 +36,6 @@ from pyrsql.sorting.limits import SortLimits _NestedValueT = TypeVar("_NestedValueT") -_EMPTY_TUPLE: Final[tuple[str, ...]] = () class _SharedPolicyOptionsProtocol(Protocol): @@ -87,29 +86,11 @@ def _normalize_mapping( return MappingProxyType(dict(mapping)) -def _normalize_tuple(values: tuple[str, ...]) -> tuple[str, ...]: - """Normalizes tuple-like string containers. - - Returns: - An immutable tuple copy of the provided values. - """ - return tuple(values) - - -def _normalize_frozenset(values: frozenset[str]) -> frozenset[str]: - """Normalizes set-like string containers. - - Returns: - An immutable frozenset copy of the provided values. - """ - return frozenset(values) - - def _normalize_shared_policy_options( options: _SharedPolicyOptionsProtocol, ) -> None: """Normalizes option fields shared by query and sort configuration.""" - struct_options = cast("msgspec.Struct", options) + struct_options = cast("msgspec.Struct", cast("object", options)) msgspec.structs.force_setattr( struct_options, "field_mapping", @@ -128,12 +109,12 @@ def _normalize_shared_policy_options( msgspec.structs.force_setattr( struct_options, "field_whitelist", - _normalize_frozenset(options.field_whitelist), + frozenset(options.field_whitelist), ) msgspec.structs.force_setattr( struct_options, "field_blacklist", - _normalize_frozenset(options.field_blacklist), + frozenset(options.field_blacklist), ) msgspec.structs.force_setattr( struct_options, @@ -148,31 +129,16 @@ def _normalize_shared_policy_options( msgspec.structs.force_setattr( struct_options, "procedure_whitelist", - _normalize_tuple(options.procedure_whitelist), + tuple(options.procedure_whitelist), ) msgspec.structs.force_setattr( struct_options, "procedure_blacklist", - _normalize_tuple(options.procedure_blacklist), + tuple(options.procedure_blacklist), ) -def _validate_like_escape_character( - like_escape_character: str | None, -) -> None: - """Validates one optional SQL LIKE escape character. - - Raises: - ValueError: If the configured escape character is not exactly one - character long. - """ - if like_escape_character is not None and len(like_escape_character) != 1: - raise ValueError( - "like_escape_character must be a single character when set.", - ) - - -def _build_field_policy( +def _resolve_field_policy( *, field_whitelist: frozenset[str], field_blacklist: frozenset[str], @@ -180,34 +146,12 @@ def _build_field_policy( model_field_whitelist: Mapping[type[Any], frozenset[str]], model_field_blacklist: Mapping[type[Any], frozenset[str]], ) -> FieldPolicySet: - """Builds a normalized field policy from option values. - - Returns: - A compiled field policy object. - """ - return FieldPolicySet( - field_whitelist=field_whitelist, - field_blacklist=field_blacklist, - model_field_mapping=model_field_mapping, - model_field_whitelist=model_field_whitelist, - model_field_blacklist=model_field_blacklist, - ) - - -def _has_field_policy_configuration( - *, - field_whitelist: frozenset[str], - field_blacklist: frozenset[str], - model_field_mapping: Mapping[type[Any], Mapping[str, str]], - model_field_whitelist: Mapping[type[Any], frozenset[str]], - model_field_blacklist: Mapping[type[Any], frozenset[str]], -) -> bool: - """Whether any runtime field-policy settings are configured. + """Builds one field policy, reusing the shared default when empty. Returns: - ``True`` when at least one runtime field-policy option is configured. + A normalized field policy instance. """ - return any( + if not any( ( field_whitelist, field_blacklist, @@ -215,31 +159,9 @@ def _has_field_policy_configuration( model_field_whitelist, model_field_blacklist, ), - ) - - -def _resolve_field_policy( - *, - field_whitelist: frozenset[str], - field_blacklist: frozenset[str], - model_field_mapping: Mapping[type[Any], Mapping[str, str]], - model_field_whitelist: Mapping[type[Any], frozenset[str]], - model_field_blacklist: Mapping[type[Any], frozenset[str]], -) -> FieldPolicySet: - """Builds one field policy, reusing the shared default when empty. - - Returns: - A normalized field policy instance. - """ - if not _has_field_policy_configuration( - field_whitelist=field_whitelist, - field_blacklist=field_blacklist, - model_field_mapping=model_field_mapping, - model_field_whitelist=model_field_whitelist, - model_field_blacklist=model_field_blacklist, ): return DEFAULT_FIELD_POLICY_SET - return _build_field_policy( + return FieldPolicySet( field_whitelist=field_whitelist, field_blacklist=field_blacklist, model_field_mapping=model_field_mapping, @@ -248,18 +170,6 @@ def _resolve_field_policy( ) -def _build_procedure_policy( - whitelist: tuple[str, ...], - blacklist: tuple[str, ...], -) -> ProcedureAccessPolicy: - """Builds a compiled procedure policy from raw patterns. - - Returns: - A compiled procedure access policy. - """ - return ProcedureAccessPolicy.from_patterns(whitelist, blacklist) - - def _resolve_procedure_policy( whitelist: tuple[str, ...], blacklist: tuple[str, ...], @@ -271,7 +181,7 @@ def _resolve_procedure_policy( """ if not whitelist and not blacklist: return DEFAULT_PROCEDURE_ACCESS_POLICY - return _build_procedure_policy(whitelist, blacklist) + return ProcedureAccessPolicy.from_patterns(whitelist, blacklist) class QueryOptions(msgspec.Struct, frozen=True, gc=False, kw_only=True): @@ -315,7 +225,11 @@ class QueryOptions(msgspec.Struct, frozen=True, gc=False, kw_only=True): _procedure_policy: ProcedureAccessPolicy = DEFAULT_PROCEDURE_ACCESS_POLICY def __post_init__(self) -> None: - """Normalizes option containers into immutable representations.""" + """Normalizes option containers into immutable representations. + + Raises: + ValueError: If the configured LIKE escape character is invalid. + """ _normalize_shared_policy_options(self) msgspec.structs.force_setattr( self, @@ -337,7 +251,13 @@ def __post_init__(self) -> None: "operator_registry", self._build_operator_registry(), ) - _validate_like_escape_character(self.like_escape_character) + if ( + self.like_escape_character is not None + and len(self.like_escape_character) != 1 + ): + raise ValueError( + "like_escape_character must be a single character when set.", + ) msgspec.structs.force_setattr( self, "_field_policy", @@ -352,7 +272,17 @@ def __post_init__(self) -> None: msgspec.structs.force_setattr( self, "_field_converter_set", - self._build_field_converter_set(), + ( + FieldValueConverterSet( + field_converters=self.field_value_converters, + model_field_converters=self.model_field_value_converters, + ) + if ( + self.field_value_converters + or self.model_field_value_converters + ) + else DEFAULT_FIELD_VALUE_CONVERTER_SET + ), ) msgspec.structs.force_setattr( self, @@ -425,22 +355,6 @@ def procedure_policy(self) -> ProcedureAccessPolicy: """ return self._procedure_policy - def _build_field_converter_set(self) -> FieldValueConverterSet: - """Builds the immutable field-converter object once. - - Returns: - The normalized field-scoped converter set. - """ - if ( - not self.field_value_converters - and not self.model_field_value_converters - ): - return DEFAULT_FIELD_VALUE_CONVERTER_SET - return FieldValueConverterSet( - field_converters=self.field_value_converters, - model_field_converters=self.model_field_value_converters, - ) - def with_field_whitelist( self, field_whitelist: frozenset[str], @@ -450,26 +364,9 @@ def with_field_whitelist( Returns: A query options copy with the provided field whitelist. """ - return QueryOptions( - strict_equality=self.strict_equality, - distinct=self.distinct, - like_escape_character=self.like_escape_character, - field_mapping=self.field_mapping, - model_field_mapping=self.model_field_mapping, - join_hints=self.join_hints, + return msgspec.structs.replace( + self, field_whitelist=field_whitelist, - field_blacklist=self.field_blacklist, - model_field_whitelist=self.model_field_whitelist, - model_field_blacklist=self.model_field_blacklist, - procedure_whitelist=self.procedure_whitelist, - procedure_blacklist=self.procedure_blacklist, - parse_limits=self.parse_limits, - operator_registry=self.operator_registry, - custom_predicates=self.custom_predicates, - value_converter_registry=self.value_converter_registry, - field_value_converters=self.field_value_converters, - model_field_value_converters=self.model_field_value_converters, - json_options=self.json_options, ) @@ -517,7 +414,7 @@ def __post_init__(self) -> None: @property def field_policy(self) -> FieldPolicySet: - """Returns the normalized field mapping and access configuration. + """Normalized field mapping and access configuration. Returns: The compiled field policy for this sort configuration. @@ -526,7 +423,7 @@ def field_policy(self) -> FieldPolicySet: @property def procedure_policy(self) -> ProcedureAccessPolicy: - """Returns the compiled procedure access policy. + """Compiled procedure access policy. Returns: The compiled procedure access policy for this sort configuration. @@ -542,18 +439,9 @@ def with_field_whitelist( Returns: A sort options copy with the provided field whitelist. """ - return SortOptions( - field_mapping=self.field_mapping, - model_field_mapping=self.model_field_mapping, - join_hints=self.join_hints, + return msgspec.structs.replace( + self, field_whitelist=field_whitelist, - field_blacklist=self.field_blacklist, - model_field_whitelist=self.model_field_whitelist, - model_field_blacklist=self.model_field_blacklist, - procedure_whitelist=self.procedure_whitelist, - procedure_blacklist=self.procedure_blacklist, - sort_limits=self.sort_limits, - json_options=self.json_options, ) diff --git a/src/pyrsql/core/page.py b/src/pyrsql/core/page.py index bcf25a6..1a0846a 100644 --- a/src/pyrsql/core/page.py +++ b/src/pyrsql/core/page.py @@ -6,9 +6,8 @@ import msgspec -from pyrsql.core.compiler import PageCompilationResult - if TYPE_CHECKING: + from pyrsql.core.compiler import CompiledArtifact from pyrsql.orms.base import ORM _TargetT = TypeVar("_TargetT") @@ -115,20 +114,16 @@ def limit(self) -> int: """ return self.page_size - def compile(self, *, orm: ORM) -> PageCompilationResult: + def compile(self, *, orm: ORM) -> CompiledArtifact: """Compiles the page request using the provided ORM. Args: orm: ORM adapter used to compile the page request. Returns: - The ORM-specific page compilation result. + The ORM-specific compiled page request. """ - compiled_page = orm.compile_page_request(self) - return PageCompilationResult( - orm_name=orm.name, - compiled=compiled_page, - ) + return orm.compile_page_request(self) def apply( self, diff --git a/src/pyrsql/core/query.py b/src/pyrsql/core/query.py index 4d7bafd..ac316cd 100644 --- a/src/pyrsql/core/query.py +++ b/src/pyrsql/core/query.py @@ -6,12 +6,12 @@ import msgspec -from pyrsql.core.compiler import CompilationResult from pyrsql.core.options import QueryOptions from pyrsql.parsing.parser import Parser from pyrsql.semantic.binder import SemanticBinder if TYPE_CHECKING: + from pyrsql.core.compiler import CompiledArtifact from pyrsql.orms.base import ORM from pyrsql.parsing.ast import Expression @@ -108,20 +108,16 @@ def bind_expression( """ return SemanticBinder(options).bind(expression) - def compile(self, *, orm: ORM) -> CompilationResult: + def compile(self, *, orm: ORM) -> CompiledArtifact: """Compiles the query using the provided ORM. Args: orm: ORM adapter used to compile the query. Returns: - The ORM-specific compilation result. + The ORM-specific compiled query. """ - compiled_query = orm.compile_query(self) - return CompilationResult( - orm_name=orm.name, - compiled=compiled_query, - ) + return orm.compile_query(self) def apply( self, diff --git a/src/pyrsql/core/sort.py b/src/pyrsql/core/sort.py index 8064a52..941807a 100644 --- a/src/pyrsql/core/sort.py +++ b/src/pyrsql/core/sort.py @@ -6,12 +6,12 @@ import msgspec -from pyrsql.core.compiler import SortCompilationResult from pyrsql.core.options import SortOptions from pyrsql.sorting.binder import SortBinder from pyrsql.sorting.parser import SortParser if TYPE_CHECKING: + from pyrsql.core.compiler import CompiledArtifact from pyrsql.orms.base import ORM from pyrsql.sorting.ast import SortField @@ -101,20 +101,16 @@ def bind_fields( return () return SortBinder(options).bind(fields) - def compile(self, *, orm: ORM) -> SortCompilationResult: + def compile(self, *, orm: ORM) -> CompiledArtifact: """Compiles the sort using the provided ORM. Args: orm: ORM adapter used to compile the sort. Returns: - The ORM-specific sort compilation result. + The ORM-specific compiled sort. """ - compiled_sort = orm.compile_sort(self) - return SortCompilationResult( - orm_name=orm.name, - compiled=compiled_sort, - ) + return orm.compile_sort(self) def apply( self, diff --git a/src/pyrsql/orms/__init__.py b/src/pyrsql/orms/__init__.py index 9ed9fee..6b6a5b0 100644 --- a/src/pyrsql/orms/__init__.py +++ b/src/pyrsql/orms/__init__.py @@ -2,16 +2,10 @@ from pyrsql.orms.base import ( ORM, - CompiledPageRequest, - CompiledQuery, - CompiledSort, ORMError, ) __all__ = [ "ORM", - "CompiledPageRequest", - "CompiledQuery", - "CompiledSort", "ORMError", ] diff --git a/src/pyrsql/orms/base.py b/src/pyrsql/orms/base.py index 2e0674d..78cf4c6 100644 --- a/src/pyrsql/orms/base.py +++ b/src/pyrsql/orms/base.py @@ -3,19 +3,14 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, ClassVar, TypeAlias - -from pyrsql.core.compiler import CompiledArtifact +from typing import TYPE_CHECKING, ClassVar if TYPE_CHECKING: + from pyrsql.core.compiler import CompiledArtifact from pyrsql.core.page import PageRequest from pyrsql.core.query import Query from pyrsql.core.sort import Sort -CompiledQuery: TypeAlias = CompiledArtifact -CompiledSort: TypeAlias = CompiledArtifact -CompiledPageRequest: TypeAlias = CompiledArtifact - class ORMError(ValueError): """Base exception for ORM backend failures.""" @@ -26,17 +21,8 @@ class ORMError(ValueError): class ORM(ABC): """Abstract ORM contract used by the pyrsql public API.""" - @property - @abstractmethod - def name(self) -> str: - """The stable ORM name. - - Returns: - The ORM's stable identifier. - """ - @abstractmethod - def compile_query(self, query: Query) -> CompiledQuery: + def compile_query(self, query: Query) -> CompiledArtifact: """Compiles a high-level query into an ORM-specific form. Args: @@ -47,7 +33,7 @@ def compile_query(self, query: Query) -> CompiledQuery: """ @abstractmethod - def compile_sort(self, sort: Sort) -> CompiledSort: + def compile_sort(self, sort: Sort) -> CompiledArtifact: """Compiles a high-level sort into an ORM-specific form. Args: @@ -61,7 +47,7 @@ def compile_sort(self, sort: Sort) -> CompiledSort: def compile_page_request( self, page_request: PageRequest, - ) -> CompiledPageRequest: + ) -> CompiledArtifact: """Compiles a pagination request into an ORM-specific form. Args: diff --git a/src/pyrsql/orms/sqlalchemy/compiled_page.py b/src/pyrsql/orms/sqlalchemy/compiled_page.py index a4d4a79..6ff6eaf 100644 --- a/src/pyrsql/orms/sqlalchemy/compiled_page.py +++ b/src/pyrsql/orms/sqlalchemy/compiled_page.py @@ -29,21 +29,19 @@ class SQLAlchemyCompiledPageRequest( def apply( self, - target: SQLAlchemySelect | object, - model: SQLAlchemyModel, + target: SQLAlchemySelect, + _model: SQLAlchemyModel, ) -> SQLAlchemySelect: """Applies the compiled page request to a SQLAlchemy Select. Args: target: SQLAlchemy select statement to mutate. - model: SQLAlchemy mapped class. The page application does not use - it directly, but the signature matches the other compiled - plans. + _model: SQLAlchemy mapped class. It is unused, but the signature + matches the other compiled plans. Returns: A SQLAlchemy select with limit and offset applied. """ - del model statement = require_sqlalchemy_select(target) return statement.limit(self.page_request.limit).offset( self.page_request.offset, diff --git a/src/pyrsql/orms/sqlalchemy/compiled_query.py b/src/pyrsql/orms/sqlalchemy/compiled_query.py index c77dcea..b1240ad 100644 --- a/src/pyrsql/orms/sqlalchemy/compiled_query.py +++ b/src/pyrsql/orms/sqlalchemy/compiled_query.py @@ -38,7 +38,7 @@ class SQLAlchemyCompiledQuery( def apply( self, - target: SQLAlchemySelect | object, + target: SQLAlchemySelect, model: SQLAlchemyModel, ) -> SQLAlchemySelect: """Applies the compiled query to a SQLAlchemy Select. diff --git a/src/pyrsql/orms/sqlalchemy/compiled_sort.py b/src/pyrsql/orms/sqlalchemy/compiled_sort.py index fb7f1ff..f590c31 100644 --- a/src/pyrsql/orms/sqlalchemy/compiled_sort.py +++ b/src/pyrsql/orms/sqlalchemy/compiled_sort.py @@ -38,7 +38,7 @@ class SQLAlchemyCompiledSort( def apply( self, - target: SQLAlchemySelect | object, + target: SQLAlchemySelect, model: SQLAlchemyModel, ) -> SQLAlchemySelect: """Applies the compiled sort to a SQLAlchemy Select. diff --git a/src/pyrsql/orms/sqlalchemy/introspection.py b/src/pyrsql/orms/sqlalchemy/introspection.py index ce9ef7a..488651a 100644 --- a/src/pyrsql/orms/sqlalchemy/introspection.py +++ b/src/pyrsql/orms/sqlalchemy/introspection.py @@ -18,7 +18,6 @@ if TYPE_CHECKING: from sqlalchemy.orm.properties import ColumnProperty - from sqlalchemy.sql.elements import NamedColumn class SQLAlchemyModelInspector: @@ -122,7 +121,6 @@ def _inspect_mapped_attribute( return SQLAlchemyMappedAttribute( name=attribute_name, kind=SQLAlchemyAttributeKind.RELATIONSHIP, - owner_model=model, attribute=getattr(model, attribute_name), mapper=relationship.mapper, python_type=relationship.mapper.class_, @@ -133,7 +131,6 @@ def _inspect_mapped_attribute( return SQLAlchemyMappedAttribute( name=attribute_name, kind=SQLAlchemyAttributeKind.COLUMN, - owner_model=model, attribute=getattr(model, attribute_name), mapper=None, python_type=self._resolve_column_python_type(column_property), @@ -144,15 +141,6 @@ def _inspect_mapped_attribute( f"{model.__name__!r}.", ) - @staticmethod - def _first_column( - column_property: ColumnProperty[Any], - ) -> NamedColumn[Any] | None: - """Returns the first mapped column for a column property, if any.""" - if not column_property.columns: - return None - return column_property.columns[0] - @classmethod def _resolve_column_python_type( cls, @@ -163,9 +151,9 @@ def _resolve_column_python_type( Returns: The column Python type, or ``None`` when unavailable. """ - column = cls._first_column(column_property) - if column is None: + if not column_property.columns: return None + column = column_property.columns[0] try: return column.type.python_type except (AttributeError, NotImplementedError): @@ -181,7 +169,7 @@ def _is_json_column( Returns: ``True`` when the column property stores JSON-like data. """ - column = cls._first_column(column_property) - if column is None: + if not column_property.columns: return False + column = column_property.columns[0] return isinstance(column.type, JSON) diff --git a/src/pyrsql/orms/sqlalchemy/json_path.py b/src/pyrsql/orms/sqlalchemy/json_path.py index 0c25dd8..1744ff0 100644 --- a/src/pyrsql/orms/sqlalchemy/json_path.py +++ b/src/pyrsql/orms/sqlalchemy/json_path.py @@ -3,7 +3,6 @@ from __future__ import annotations import re -from types import MappingProxyType from typing import TYPE_CHECKING, Any, cast import msgspec @@ -62,14 +61,12 @@ _ISO_DATE_PATTERN = re.compile(r"^\d{4}-\d{2}-\d{2}$") _ISO_TIME_PATTERN = re.compile(r"^\d{2}:\d{2}:\d{2}(\.\d+)?$") _JSON_ENCODER = msgspec.json.Encoder() -_ORDERED_COMPARISON_OPERATORS = MappingProxyType( - { - GREATER_THAN.name: ">", - GREATER_THAN_OR_EQUAL.name: ">=", - LESS_THAN.name: "<", - LESS_THAN_OR_EQUAL.name: "<=", - }, -) +_ORDERED_COMPARISON_OPERATORS = { + GREATER_THAN.name: ">", + GREATER_THAN_OR_EQUAL.name: ">=", + LESS_THAN.name: "<", + LESS_THAN_OR_EQUAL.name: "<=", +} _IGNORE_CASE_JSON_OPERATORS = frozenset( { IGNORE_CASE.name, diff --git a/src/pyrsql/orms/sqlalchemy/orm.py b/src/pyrsql/orms/sqlalchemy/orm.py index 08d552e..4bde797 100644 --- a/src/pyrsql/orms/sqlalchemy/orm.py +++ b/src/pyrsql/orms/sqlalchemy/orm.py @@ -36,27 +36,7 @@ def __init__( Mapping[str, SQLAlchemyCustomPredicate] | None ) = None, ) -> None: - """Creates a SQLAlchemy ORM adapter.""" - self._translator = self._build_translator( - translator=translator, - custom_predicates=custom_predicates, - ) - self._sort_translator = ( - SQLAlchemySortTranslator() - if sort_translator is None - else sort_translator - ) - - @staticmethod - def _build_translator( - *, - translator: SQLAlchemyExpressionTranslator | None, - custom_predicates: (Mapping[str, SQLAlchemyCustomPredicate] | None), - ) -> SQLAlchemyExpressionTranslator: - """Builds the effective query translator. - - Returns: - The translator to use for query compilation. + """Creates a SQLAlchemy ORM adapter. Raises: ValueError: If custom predicates are combined with an explicit @@ -68,20 +48,17 @@ def _build_translator( "custom_predicates cannot be provided when translator is " "passed explicitly.", ) - return translator - return SQLAlchemyExpressionTranslator( - custom_predicates=custom_predicates, + self._translator = translator + else: + self._translator = SQLAlchemyExpressionTranslator( + custom_predicates=custom_predicates, + ) + self._sort_translator = ( + SQLAlchemySortTranslator() + if sort_translator is None + else sort_translator ) - @property - def name(self) -> str: - """Returns the ORM name. - - Returns: - The stable ORM name. - """ - return "sqlalchemy" - def compile_query(self, query: Query) -> SQLAlchemyCompiledQuery: # type: ignore[override] """Compiles a pyrsql query for SQLAlchemy. diff --git a/src/pyrsql/orms/sqlalchemy/resolver.py b/src/pyrsql/orms/sqlalchemy/resolver.py index 9778b54..d2b620b 100644 --- a/src/pyrsql/orms/sqlalchemy/resolver.py +++ b/src/pyrsql/orms/sqlalchemy/resolver.py @@ -148,7 +148,7 @@ def _resolve_with_field_policy( ) joins.append( SQLAlchemyJoinPlan( - key=self._make_join_key(current_model, segment), + key=f"{current_model.__name__}.{segment}", attribute=mapped_attribute.attribute, default_hint=JoinHint.INNER, is_collection=mapped_attribute.is_collection, @@ -171,7 +171,6 @@ def _resolve_with_field_policy( mapped_attribute.name, ) return self._build_resolved_path( - root_model=model, leaf_model=current_model, field_path=field_path, joins=joins, @@ -209,7 +208,6 @@ def _resolve_with_field_policy( "Path resolution ended without a resolved leaf attribute.", ) return self._build_resolved_path( - root_model=model, leaf_model=current_model, field_path=field_path, joins=joins, @@ -248,7 +246,6 @@ def _expand_mapped_segments( @staticmethod def _build_resolved_path( *, - root_model: type[Any], leaf_model: type[Any], field_path: str, joins: list[SQLAlchemyJoinPlan], @@ -263,7 +260,6 @@ def _build_resolved_path( The immutable resolved path payload. """ return SQLAlchemyResolvedPath( - root_model=root_model, leaf_model=leaf_model, field_path=field_path, joins=tuple(joins), @@ -276,18 +272,6 @@ def _build_resolved_path( is_json=is_json, ) - @staticmethod - def _make_join_key( - model: type[Any], - segment: str, - ) -> str: - """Builds the stable join-hint lookup key for one relationship step. - - Returns: - A stable join-hint lookup key. - """ - return f"{model.__name__}.{segment}" - @staticmethod def _validate_global_field_policy( field_policy: FieldPolicySet, diff --git a/src/pyrsql/orms/sqlalchemy/sorter.py b/src/pyrsql/orms/sqlalchemy/sorter.py index a53ae47..773e58a 100644 --- a/src/pyrsql/orms/sqlalchemy/sorter.py +++ b/src/pyrsql/orms/sqlalchemy/sorter.py @@ -2,7 +2,6 @@ from __future__ import annotations -from types import MappingProxyType from typing import TYPE_CHECKING, Any, cast import sqlalchemy as sa @@ -18,7 +17,6 @@ ) from pyrsql.selector.ast import ( FieldSelector, - FunctionSelector, LiteralSelector, ) from pyrsql.sorting.ast import SortDirection @@ -35,15 +33,13 @@ from pyrsql.sorting.ast import SortField -_JSON_SORT_PYTHON_TYPES = MappingProxyType( - { - JSONSortScalarType.TEXT: str, - JSONSortScalarType.INTEGER: int, - JSONSortScalarType.FLOAT: float, - JSONSortScalarType.NUMERIC: float, - JSONSortScalarType.BOOLEAN: bool, - }, -) +_JSON_SORT_PYTHON_TYPES = { + JSONSortScalarType.TEXT: str, + JSONSortScalarType.INTEGER: int, + JSONSortScalarType.FLOAT: float, + JSONSortScalarType.NUMERIC: float, + JSONSortScalarType.BOOLEAN: bool, +} class SQLAlchemySortTranslator: @@ -116,8 +112,6 @@ def _translate_selector( Returns: Join plans, an SQL expression, and an inferred Python type. - Raises: - TypeError: If the selector is not a supported selector node. """ if isinstance(selector, FieldSelector): resolved_path = self._path_resolver.resolve( @@ -146,8 +140,6 @@ def _translate_selector( ) return (), sa.literal(selector.value), python_type - if not isinstance(selector, FunctionSelector): - raise TypeError("Expected FunctionSelector") joins: list[SQLAlchemyJoinPlan] = [] argument_expressions: list[ColumnElement[Any]] = [] argument_types: list[type[Any] | None] = [] diff --git a/src/pyrsql/orms/sqlalchemy/translator.py b/src/pyrsql/orms/sqlalchemy/translator.py index 2e27c1c..48961be 100644 --- a/src/pyrsql/orms/sqlalchemy/translator.py +++ b/src/pyrsql/orms/sqlalchemy/translator.py @@ -43,7 +43,6 @@ ) from pyrsql.selector.ast import ( FieldSelector, - FunctionSelector, LiteralSelector, ) @@ -154,8 +153,6 @@ def _translate_logical( Returns: Join plans and a SQLAlchemy predicate. - Raises: - SQLAlchemyORMError: If the logical operator is unsupported. """ joins: list[SQLAlchemyJoinPlan] = [] predicates: list[ColumnElement[bool]] = [] @@ -167,15 +164,9 @@ def _translate_logical( ) joins.extend(child_joins) predicates.append(child_predicate) - match expression.operator: - case LogicalOperator.AND: - return tuple(joins), sa.and_(*predicates) - case LogicalOperator.OR: - return tuple(joins), sa.or_(*predicates) - case _: - raise SQLAlchemyORMError( - f"Unsupported logical operator {expression.operator!r}.", - ) + if expression.operator is LogicalOperator.AND: + return tuple(joins), sa.and_(*predicates) + return tuple(joins), sa.or_(*predicates) def _translate_comparison( self, @@ -288,8 +279,6 @@ def _translate_selector( Returns: Join plans, a SQLAlchemy expression, and an inferred Python type. - Raises: - TypeError: If the selector is not a supported selector node. """ if isinstance(selector, FieldSelector): resolved_path = self._path_resolver.resolve( @@ -311,8 +300,6 @@ def _translate_selector( ) return (), sa.literal(selector.value), python_type - if not isinstance(selector, FunctionSelector): - raise TypeError("Expected FunctionSelector") joins: list[SQLAlchemyJoinPlan] = [] argument_expressions: list[ColumnElement[Any]] = [] argument_types: list[type[Any] | None] = [] diff --git a/src/pyrsql/orms/sqlalchemy/type_inference.py b/src/pyrsql/orms/sqlalchemy/type_inference.py index 2322c80..3cbcf88 100644 --- a/src/pyrsql/orms/sqlalchemy/type_inference.py +++ b/src/pyrsql/orms/sqlalchemy/type_inference.py @@ -2,19 +2,16 @@ from __future__ import annotations -from types import MappingProxyType from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from sqlalchemy.sql.elements import ColumnElement -_KNOWN_FUNCTION_RETURN_TYPES = MappingProxyType( - { - "lower": str, - "upper": str, - "concat": str, - }, -) +_KNOWN_FUNCTION_RETURN_TYPES = { + "lower": str, + "upper": str, + "concat": str, +} def infer_sql_function_python_type( diff --git a/src/pyrsql/orms/sqlalchemy/types.py b/src/pyrsql/orms/sqlalchemy/types.py index 3b454af..67f8f0d 100644 --- a/src/pyrsql/orms/sqlalchemy/types.py +++ b/src/pyrsql/orms/sqlalchemy/types.py @@ -51,7 +51,6 @@ class SQLAlchemyResolvedPath( ): """Represents a resolved ORM path.""" - root_model: SQLAlchemyModel leaf_model: SQLAlchemyModel field_path: str joins: tuple[SQLAlchemyJoinPlan, ...] @@ -71,7 +70,6 @@ class SQLAlchemyMappedAttribute( name: str kind: SQLAlchemyAttributeKind - owner_model: SQLAlchemyModel attribute: SQLAlchemyAttribute mapper: SQLAlchemyMapper | None python_type: type[Any] | None diff --git a/src/pyrsql/parsing/__init__.py b/src/pyrsql/parsing/__init__.py index e86872b..59a7b62 100644 --- a/src/pyrsql/parsing/__init__.py +++ b/src/pyrsql/parsing/__init__.py @@ -7,12 +7,11 @@ LogicalNode, LogicalOperator, ) -from pyrsql.parsing.diagnostics import ParseDiagnostic from pyrsql.parsing.errors import LexError, ParseError from pyrsql.parsing.lexer import Lexer from pyrsql.parsing.limits import DEFAULT_PARSE_LIMITS, ParseLimits from pyrsql.parsing.parser import Parser -from pyrsql.parsing.source import SourceSpan, SourceText +from pyrsql.parsing.source import SourceSpan from pyrsql.parsing.tokens import TokenKind __all__ = [ @@ -24,11 +23,9 @@ "Lexer", "LogicalNode", "LogicalOperator", - "ParseDiagnostic", "ParseError", "ParseLimits", "Parser", "SourceSpan", - "SourceText", "TokenKind", ] diff --git a/src/pyrsql/parsing/ast.py b/src/pyrsql/parsing/ast.py index 1bd3935..69fb45a 100644 --- a/src/pyrsql/parsing/ast.py +++ b/src/pyrsql/parsing/ast.py @@ -8,8 +8,6 @@ import msgspec if TYPE_CHECKING: - from collections.abc import Iterator - from pyrsql.parsing.operators import ComparisonOperator from pyrsql.parsing.source import SourceSpan from pyrsql.selector.ast import SelectorNode @@ -23,19 +21,10 @@ class Argument(msgspec.Struct, frozen=True, gc=False, kw_only=True): span: SourceSpan -class Node(msgspec.Struct, frozen=True, gc=False, kw_only=True): - """Base AST node.""" - - span: SourceSpan - - def walk(self) -> Iterator[Node]: - """Yields this node and all nested syntax nodes depth-first.""" - yield self - - -class ComparisonNode(Node, frozen=True, gc=False, kw_only=True): +class ComparisonNode(msgspec.Struct, frozen=True, gc=False, kw_only=True): """Leaf node for selector/operator/arguments expressions.""" + span: SourceSpan selector: SelectorNode operator: ComparisonOperator arguments: tuple[Argument, ...] @@ -61,18 +50,13 @@ class LogicalOperator(Enum): OR = auto() -class LogicalNode(Node, frozen=True, gc=False, kw_only=True): +class LogicalNode(msgspec.Struct, frozen=True, gc=False, kw_only=True): """Logical expression with two or more child nodes.""" + span: SourceSpan operator: LogicalOperator children: tuple[Expression, ...] - def walk(self) -> Iterator[Node]: - """Yields this node and all descendant syntax nodes depth-first.""" - yield self - for child in self.children: - yield from child.walk() - def with_span(self, span: SourceSpan) -> LogicalNode: """Returns a copy of the logical node with an updated span. diff --git a/src/pyrsql/parsing/diagnostics.py b/src/pyrsql/parsing/diagnostics.py deleted file mode 100644 index 8bbdd61..0000000 --- a/src/pyrsql/parsing/diagnostics.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Structured parsing diagnostics.""" - -from __future__ import annotations - -from pyrsql.core.source_issues import SourceDiagnostic - - -class ParseDiagnostic(SourceDiagnostic, frozen=True, gc=False, kw_only=True): - """A structured parsing diagnostic.""" diff --git a/src/pyrsql/parsing/errors.py b/src/pyrsql/parsing/errors.py index 52dacbf..8d1ee27 100644 --- a/src/pyrsql/parsing/errors.py +++ b/src/pyrsql/parsing/errors.py @@ -5,14 +5,12 @@ from typing import ClassVar from pyrsql.core.source_issues import SourceError -from pyrsql.parsing.diagnostics import ParseDiagnostic class ParseError(SourceError): """Base exception for parsing failures.""" code: ClassVar[str] = "parse_error" - diagnostic_type: ClassVar[type[ParseDiagnostic]] = ParseDiagnostic class LexError(ParseError): diff --git a/src/pyrsql/parsing/lexer.py b/src/pyrsql/parsing/lexer.py index 53aad83..5d5a410 100644 --- a/src/pyrsql/parsing/lexer.py +++ b/src/pyrsql/parsing/lexer.py @@ -5,12 +5,9 @@ from typing import TYPE_CHECKING from pyrsql.parsing.errors import LexError -from pyrsql.parsing.normalization import ( - normalize_operator_registry, - normalize_parse_limits, -) +from pyrsql.parsing.limits import DEFAULT_PARSE_LIMITS from pyrsql.parsing.operators import DEFAULT_OPERATOR_REGISTRY -from pyrsql.parsing.source import SourcePosition, SourceSpan, SourceText +from pyrsql.parsing.source import SourcePosition, SourceSpan from pyrsql.parsing.tokens import Token, TokenKind if TYPE_CHECKING: @@ -23,43 +20,24 @@ class Lexer: def __init__( self, - source: str | SourceText, + source: str, *, limits: ParseLimits | None = None, operator_registry: OperatorRegistry = DEFAULT_OPERATOR_REGISTRY, ) -> None: """Initializes the lexer with source text and parser limits.""" - self._text = self._normalize_source(source).text + self._text = source self._length = len(self._text) - self._limits = normalize_parse_limits(limits, owner_type=type(self)) - self._operator_registry = normalize_operator_registry( - operator_registry, - owner_type=type(self), - ) + self._limits = DEFAULT_PARSE_LIMITS if limits is None else limits + self._operator_registry = operator_registry self._index = 0 self._line = 1 self._column = 1 self._validate_source_length() - @staticmethod - def _normalize_source(source: str | SourceText) -> SourceText: - """Normalizes the lexer source input. - - Returns: - A wrapped immutable source object. - - Raises: - TypeError: If the source is neither raw text nor ``SourceText``. - """ - if isinstance(source, SourceText): - return source - if not isinstance(source, str): - raise TypeError("Lexer source must be a string or SourceText.") - return SourceText(text=source) - @property def limits(self) -> ParseLimits: - """Returns the configured parser safety limits. + """Configured parser safety limits. Returns: The lexer safety limits currently in use. diff --git a/src/pyrsql/parsing/limits.py b/src/pyrsql/parsing/limits.py index 2e72c60..b1d2944 100644 --- a/src/pyrsql/parsing/limits.py +++ b/src/pyrsql/parsing/limits.py @@ -4,8 +4,6 @@ import msgspec -from pyrsql.core.validation import validate_positive_int - class ParseLimits(msgspec.Struct, frozen=True, gc=False, kw_only=True): """Configurable safety limits for query processing.""" @@ -17,32 +15,5 @@ class ParseLimits(msgspec.Struct, frozen=True, gc=False, kw_only=True): max_expression_depth: int = 16 max_node_count: int = 1024 - def __post_init__(self) -> None: - """Validates parser limit invariants.""" - validate_positive_int( - self.max_query_length, - field_name="max_query_length", - ) - validate_positive_int( - self.max_selector_length, - field_name="max_selector_length", - ) - validate_positive_int( - self.max_argument_length, - field_name="max_argument_length", - ) - validate_positive_int( - self.max_arguments_per_list, - field_name="max_arguments_per_list", - ) - validate_positive_int( - self.max_expression_depth, - field_name="max_expression_depth", - ) - validate_positive_int( - self.max_node_count, - field_name="max_node_count", - ) - DEFAULT_PARSE_LIMITS = ParseLimits() diff --git a/src/pyrsql/parsing/normalization.py b/src/pyrsql/parsing/normalization.py deleted file mode 100644 index dd903c2..0000000 --- a/src/pyrsql/parsing/normalization.py +++ /dev/null @@ -1,51 +0,0 @@ -"""Shared runtime normalization helpers for parsing components.""" - -from __future__ import annotations - -from pyrsql.parsing.limits import DEFAULT_PARSE_LIMITS, ParseLimits -from pyrsql.parsing.operators import ( - OperatorRegistry, -) - - -def normalize_parse_limits( - limits: ParseLimits | None, - *, - owner_type: type[object], -) -> ParseLimits: - """Normalizes optional parser limits. - - Returns: - The provided limits, or the shared defaults. - - Raises: - TypeError: If ``limits`` is not a ``ParseLimits`` instance. - """ - if limits is None: - return DEFAULT_PARSE_LIMITS - if not isinstance(limits, ParseLimits): - raise TypeError( - f"{owner_type.__name__} limits must be a ParseLimits instance.", - ) - return limits - - -def normalize_operator_registry( - operator_registry: OperatorRegistry, - *, - owner_type: type[object], -) -> OperatorRegistry: - """Normalizes the operator registry dependency. - - Returns: - The validated operator registry instance. - - Raises: - TypeError: If the registry is not an ``OperatorRegistry``. - """ - if not isinstance(operator_registry, OperatorRegistry): - raise TypeError( - f"{owner_type.__name__} operator_registry must be an " - "OperatorRegistry instance.", - ) - return operator_registry diff --git a/src/pyrsql/parsing/operators.py b/src/pyrsql/parsing/operators.py index c82eaed..679c59c 100644 --- a/src/pyrsql/parsing/operators.py +++ b/src/pyrsql/parsing/operators.py @@ -11,36 +11,6 @@ from collections.abc import Mapping -def _validate_non_empty_string(value: str, *, field_name: str) -> None: - """Validates one required non-empty string field. - - Raises: - TypeError: If the field is not a string. - ValueError: If the field is empty or padded with outer whitespace. - """ - if not isinstance(value, str): - raise TypeError(f"{field_name} must be a string.") - if not value: - raise ValueError(f"{field_name} cannot be empty.") - if value != value.strip(): - raise ValueError( - f"{field_name} must not contain outer whitespace.", - ) - - -def _validate_non_negative_int(value: int, *, field_name: str) -> None: - """Validates one non-negative integer field. - - Raises: - TypeError: If the field is not an integer. - ValueError: If the field is negative. - """ - if isinstance(value, bool) or not isinstance(value, int): - raise TypeError(f"{field_name} must be an integer.") - if value < 0: - raise ValueError(f"{field_name} cannot be negative.") - - class ComparisonOperator(msgspec.Struct, frozen=True, gc=False, kw_only=True): """Represents a supported comparison operator.""" @@ -49,45 +19,6 @@ class ComparisonOperator(msgspec.Struct, frozen=True, gc=False, kw_only=True): minimum_arguments: int maximum_arguments: int | None - def __post_init__(self) -> None: - """Validates operator invariants. - - Raises: - TypeError: If the operator definition uses invalid runtime types. - ValueError: If the operator definition is invalid. - """ - _validate_non_empty_string(self.name, field_name="name") - if isinstance(self.spellings, str) or not isinstance( - self.spellings, - tuple, - ): - raise TypeError("spellings must be a tuple of strings.") - if not self.spellings: - raise ValueError("Operator must define at least one spelling.") - for spelling in self.spellings: - _validate_non_empty_string( - spelling, - field_name="operator spelling", - ) - if len(set(self.spellings)) != len(self.spellings): - raise ValueError("Operator spellings must be unique.") - _validate_non_negative_int( - self.minimum_arguments, - field_name="minimum_arguments", - ) - if self.maximum_arguments is not None: - _validate_non_negative_int( - self.maximum_arguments, - field_name="maximum_arguments", - ) - if ( - self.maximum_arguments is not None - and self.maximum_arguments < self.minimum_arguments - ): - raise ValueError( - "maximum_arguments cannot be less than minimum_arguments.", - ) - class OperatorRegistry(msgspec.Struct, frozen=True, gc=False): """Immutable registry of supported comparison operators.""" @@ -101,33 +32,11 @@ class OperatorRegistry(msgspec.Struct, frozen=True, gc=False): ) def __post_init__(self) -> None: - """Builds lookup structures and validates uniqueness. - - Raises: - TypeError: If the registered operators do not match the runtime - contract. - ValueError: If no operators are registered or spellings collide. - """ - if isinstance(self.operators, str) or not isinstance( - self.operators, - tuple, - ): - raise TypeError("operators must be a tuple of ComparisonOperator.") - if not self.operators: - raise ValueError("Operator registry must contain operators.") - for operator in self.operators: - if not isinstance(operator, ComparisonOperator): - raise TypeError( - "operators must contain only ComparisonOperator instances.", - ) + """Builds lookup structures for registered operators.""" operators_by_spelling: dict[str, ComparisonOperator] = {} spellings_by_prefix: dict[str, list[str]] = {} for operator in self.operators: for spelling in operator.spellings: - if spelling in operators_by_spelling: - raise ValueError( - f"Duplicate operator spelling registered: {spelling!r}.", # noqa: E501 - ) operators_by_spelling[spelling] = operator spellings_by_prefix.setdefault(spelling[0], []).append(spelling) msgspec.structs.force_setattr( diff --git a/src/pyrsql/parsing/parser.py b/src/pyrsql/parsing/parser.py index ca02c82..9538546 100644 --- a/src/pyrsql/parsing/parser.py +++ b/src/pyrsql/parsing/parser.py @@ -12,10 +12,7 @@ ) from pyrsql.parsing.errors import ParseError from pyrsql.parsing.lexer import Lexer -from pyrsql.parsing.normalization import ( - normalize_operator_registry, - normalize_parse_limits, -) +from pyrsql.parsing.limits import DEFAULT_PARSE_LIMITS from pyrsql.parsing.operators import ( DEFAULT_OPERATOR_REGISTRY, ) @@ -63,11 +60,8 @@ def __init__( operator_registry: OperatorRegistry = DEFAULT_OPERATOR_REGISTRY, ) -> None: """Initializes the parser with raw source text and limits.""" - self._limits = normalize_parse_limits(limits, owner_type=type(self)) - self._operator_registry = normalize_operator_registry( - operator_registry, - owner_type=type(self), - ) + self._limits = DEFAULT_PARSE_LIMITS if limits is None else limits + self._operator_registry = operator_registry self._tokens = Lexer( source, limits=self._limits, @@ -79,7 +73,7 @@ def __init__( @property def limits(self) -> ParseLimits: - """Returns the configured parser safety limits. + """Configured parser safety limits. Returns: The parser safety limits currently in use. @@ -232,13 +226,12 @@ def _parse_argument_list(self) -> tuple[Argument, ...]: "Expected an argument inside list", ) arguments.append(self._make_argument(argument_token)) - if self._match(TokenKind.COMMA): - continue + if self._match(TokenKind.RPAREN): + return tuple(arguments) self._expect( - TokenKind.RPAREN, - message="Expected ')' to close argument list", + TokenKind.COMMA, + message="Expected ',' or ')' after argument", ) - return tuple(arguments) @staticmethod def _validate_argument_count( diff --git a/src/pyrsql/parsing/source.py b/src/pyrsql/parsing/source.py index 5553132..2f86a8e 100644 --- a/src/pyrsql/parsing/source.py +++ b/src/pyrsql/parsing/source.py @@ -27,26 +27,3 @@ def cover(cls, start: SourceSpan, end: SourceSpan) -> SourceSpan: A span covering the start of the first and end of the second span. """ return cls(start=start.start, end=end.end) - - -class SourceText(msgspec.Struct, frozen=True, gc=False, kw_only=True): - """Wraps immutable source text and exposes helper methods.""" - - text: str - - @property - def length(self) -> int: - """Returns the source length. - - Returns: - The length of the wrapped source text. - """ - return len(self.text) - - def slice(self, start: int, end: int) -> str: - """Returns a substring by raw indices. - - Returns: - The sliced substring. - """ - return self.text[start:end] diff --git a/src/pyrsql/selector/ast.py b/src/pyrsql/selector/ast.py index 87afb9e..7e12748 100644 --- a/src/pyrsql/selector/ast.py +++ b/src/pyrsql/selector/ast.py @@ -2,44 +2,30 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TypeAlias import msgspec -if TYPE_CHECKING: - from collections.abc import Iterator - SelectorLiteral = str | int | float | bool | None -class SelectorNode(msgspec.Struct, frozen=True, gc=False, kw_only=True): - """Base class for selector syntax nodes.""" - - def walk(self) -> Iterator[SelectorNode]: - """Yields this selector and all nested selectors depth-first.""" - yield self - - -class FieldSelector(SelectorNode, frozen=True, gc=False, kw_only=True): +class FieldSelector(msgspec.Struct, frozen=True, gc=False, kw_only=True): """Field-path selector as written in the source text.""" raw_path: str -class LiteralSelector(SelectorNode, frozen=True, gc=False, kw_only=True): +class LiteralSelector(msgspec.Struct, frozen=True, gc=False, kw_only=True): """Static literal selector.""" value: SelectorLiteral -class FunctionSelector(SelectorNode, frozen=True, gc=False, kw_only=True): +class FunctionSelector(msgspec.Struct, frozen=True, gc=False, kw_only=True): """Function selector.""" function_name: str arguments: tuple[SelectorNode, ...] - def walk(self) -> Iterator[SelectorNode]: - """Yields this selector and all nested selectors depth-first.""" - yield self - for argument in self.arguments: - yield from argument.walk() + +SelectorNode: TypeAlias = FieldSelector | LiteralSelector | FunctionSelector diff --git a/src/pyrsql/selector/parser.py b/src/pyrsql/selector/parser.py index c8c6833..9f157c1 100644 --- a/src/pyrsql/selector/parser.py +++ b/src/pyrsql/selector/parser.py @@ -70,18 +70,26 @@ def split_top_level(text: str, *, delimiter: str) -> tuple[str, ...]: Returns: The top-level split fragments. + """ + return tuple( + part + for part in SelectorParser._split_top_level_parts(text, delimiter) + if part + ) + + @staticmethod + def _split_top_level_parts( + text: str, + delimiter: str, + ) -> tuple[str, ...]: + """Splits text on a delimiter while preserving empty fragments. + + Returns: + The normalized top-level fragments, including empty fragments. + Raises: - TypeError: If the text or delimiter do not match the runtime - contract. - ValueError: If the delimiter is not a single character. SelectorParseError: If the brackets are unbalanced. """ - if not isinstance(text, str): - raise TypeError("Selector fragment text must be a string.") - if not isinstance(delimiter, str): - raise TypeError("Selector delimiter must be a string.") - if len(delimiter) != 1: - raise ValueError("Selector delimiter must be a single character.") parts: list[str] = [] start_index = 0 depth = 0 @@ -95,17 +103,13 @@ def split_top_level(text: str, *, delimiter: str) -> tuple[str, ...]: f"Selector fragment {text!r} has unbalanced brackets.", ) elif character == delimiter and depth == 0: - part = text[start_index:index].strip() - if part: - parts.append(part) + parts.append(text[start_index:index].strip()) start_index = index + 1 if depth != 0: raise SelectorParseError( f"Selector fragment {text!r} has unbalanced brackets.", ) - final_part = text[start_index:].strip() - if final_part: - parts.append(final_part) + parts.append(text[start_index:].strip()) return tuple(parts) @staticmethod @@ -124,33 +128,10 @@ def _split_required_top_level( SelectorParseError: If brackets are unbalanced or a fragment is empty. """ - parts: list[str] = [] - start_index = 0 - depth = 0 - for index, character in enumerate(text): - if character == "[": - depth += 1 - elif character == "]": - depth -= 1 - if depth < 0: - raise SelectorParseError( - f"Selector fragment {text!r} has unbalanced brackets.", - ) - elif character == delimiter and depth == 0: - part = text[start_index:index].strip() - if not part: - raise SelectorParseError(context) - parts.append(part) - start_index = index + 1 - if depth != 0: - raise SelectorParseError( - f"Selector fragment {text!r} has unbalanced brackets.", - ) - final_part = text[start_index:].strip() - if not final_part: + parts = SelectorParser._split_top_level_parts(text, delimiter) + if any(not part for part in parts): raise SelectorParseError(context) - parts.append(final_part) - return tuple(parts) + return parts @staticmethod def _normalize_selector_text( @@ -165,22 +146,8 @@ def _normalize_selector_text( The stripped selector text. Raises: - TypeError: If the selector text, maximum length, or context use - invalid runtime types. - ValueError: If ``max_length`` is not greater than zero. SelectorParseError: If the selector text is empty or too long. """ - if not isinstance(raw_selector, str): - raise TypeError("Selector text must be a string.") - if not isinstance(context, str): - raise TypeError("Selector context must be a string.") - if not context: - raise ValueError("Selector context cannot be empty.") - if isinstance(max_length, bool) or not isinstance(max_length, int): - raise TypeError("Selector max_length must be an integer.") - if max_length <= 0: - raise ValueError("Selector max_length must be greater than 0.") - normalized_selector = raw_selector.strip() if not normalized_selector: raise SelectorParseError(f"{context} cannot be empty.") @@ -220,24 +187,19 @@ def _parse_function_selector( if not function_name: raise SelectorParseError(f"{context} has empty function name.") arguments_text = raw_selector[argument_start + 1 : argument_end] - try: - argument_fragments = self._split_required_top_level( - arguments_text, - delimiter="|", - context=( - f"{context} function {function_name!r} " - "cannot contain empty arguments." - ), + if not arguments_text.strip(): + raise SelectorParseError( + f"{context} function {function_name!r} " + "must have at least one argument.", ) - except SelectorParseError as error: - if str(error).startswith("Selector fragment "): - raise - if not arguments_text.strip(): - raise SelectorParseError( - f"{context} function {function_name!r} " - "must have at least one argument.", - ) from None - raise + argument_fragments = self._split_required_top_level( + arguments_text, + delimiter="|", + context=( + f"{context} function {function_name!r} " + "cannot contain empty arguments." + ), + ) return FunctionSelector( function_name=function_name, arguments=tuple( diff --git a/src/pyrsql/semantic/binder.py b/src/pyrsql/semantic/binder.py index fa21a8e..1b60b9a 100644 --- a/src/pyrsql/semantic/binder.py +++ b/src/pyrsql/semantic/binder.py @@ -46,15 +46,9 @@ def bind(self, expression: Expression) -> Expression: Returns: A semantically validated expression tree. - Raises: - TypeError: If the expression is not a supported AST node. """ if isinstance(expression, ComparisonNode): return self._bind_comparison(expression) - if not isinstance(expression, LogicalNode): - raise TypeError( - "Expected ComparisonNode or LogicalNode expression.", - ) return LogicalNode( span=expression.span, operator=expression.operator, @@ -91,8 +85,6 @@ def _bind_selector( Returns: The semantically validated selector node. - Raises: - TypeError: If the selector is not a supported selector node. """ if isinstance(selector, FieldSelector): field_path = self._field_mapping.get( @@ -105,10 +97,6 @@ def _bind_selector( ) if isinstance(selector, LiteralSelector): return selector - if not isinstance(selector, FunctionSelector): - raise TypeError( - "Expected FieldSelector, LiteralSelector, or FunctionSelector.", - ) self._enforce_function_access_policy(selector.function_name, span=span) return FunctionSelector( function_name=selector.function_name, diff --git a/src/pyrsql/sorting/binder.py b/src/pyrsql/sorting/binder.py index d14558b..d749747 100644 --- a/src/pyrsql/sorting/binder.py +++ b/src/pyrsql/sorting/binder.py @@ -64,8 +64,6 @@ def _bind_selector(self, selector: SelectorNode) -> SelectorNode: Returns: The semantically validated selector node. - Raises: - TypeError: If the selector is not a supported selector node. """ if isinstance(selector, FieldSelector): field_path = self._field_mapping.get( @@ -78,10 +76,6 @@ def _bind_selector(self, selector: SelectorNode) -> SelectorNode: ) if isinstance(selector, LiteralSelector): return selector - if not isinstance(selector, FunctionSelector): - raise TypeError( - "Expected FieldSelector, LiteralSelector, or FunctionSelector.", - ) self._enforce_function_access_policy(selector.function_name) return FunctionSelector( function_name=selector.function_name, diff --git a/src/pyrsql/sorting/errors.py b/src/pyrsql/sorting/errors.py index ca78880..79bf407 100644 --- a/src/pyrsql/sorting/errors.py +++ b/src/pyrsql/sorting/errors.py @@ -17,7 +17,7 @@ class SortError(ValueError): @property def diagnostic(self) -> SortDiagnostic: - """Returns the structured diagnostic for this error. + """Structured diagnostic for this error. Returns: The structured sorting diagnostic. diff --git a/src/pyrsql/sorting/limits.py b/src/pyrsql/sorting/limits.py index 712a7e9..74fbbc1 100644 --- a/src/pyrsql/sorting/limits.py +++ b/src/pyrsql/sorting/limits.py @@ -4,8 +4,6 @@ import msgspec -from pyrsql.core.validation import validate_positive_int - class SortLimits(msgspec.Struct, frozen=True, gc=False, kw_only=True): """Configurable limits for sort parsing.""" @@ -14,20 +12,5 @@ class SortLimits(msgspec.Struct, frozen=True, gc=False, kw_only=True): max_fields: int = 32 max_field_path_length: int = 256 - def __post_init__(self) -> None: - """Validates sort parser limit invariants.""" - validate_positive_int( - self.max_sort_length, - field_name="max_sort_length", - ) - validate_positive_int( - self.max_fields, - field_name="max_fields", - ) - validate_positive_int( - self.max_field_path_length, - field_name="max_field_path_length", - ) - DEFAULT_SORT_LIMITS = SortLimits() diff --git a/src/pyrsql/sorting/parser.py b/src/pyrsql/sorting/parser.py index df6817a..a9fed45 100644 --- a/src/pyrsql/sorting/parser.py +++ b/src/pyrsql/sorting/parser.py @@ -12,7 +12,6 @@ if TYPE_CHECKING: from pyrsql.selector.ast import SelectorNode -_EMPTY_FIELDS: Final[tuple[SortField, ...]] = () _MAX_SORT_PARTS: Final = 3 _MIN_SORT_PARTS_FOR_IGNORE_CASE: Final = 2 @@ -27,40 +26,8 @@ def __init__( limits: SortLimits | None = None, ) -> None: """Initializes the parser with raw sort text and limits.""" - self._source = self._normalize_source(source) - self._limits = self._normalize_limits(limits) - - @staticmethod - def _normalize_source(source: str | None) -> str: - """Validates and normalizes raw sort source text. - - Returns: - The stripped sort source text, or an empty string for ``None``. - - Raises: - TypeError: If the provided source is not a string or ``None``. - """ - if source is None: - return "" - if not isinstance(source, str): - raise TypeError("Sort source must be a string or None.") - return source.strip() - - @staticmethod - def _normalize_limits(limits: SortLimits | None) -> SortLimits: - """Validates and normalizes sort parser limits. - - Returns: - The provided limits, or the shared defaults when omitted. - - Raises: - TypeError: If the provided limits are not a ``SortLimits`` instance. - """ - if limits is None: - return DEFAULT_SORT_LIMITS - if not isinstance(limits, SortLimits): - raise TypeError("Sort limits must be a SortLimits instance.") - return limits + self._source = "" if source is None else source.strip() + self._limits = DEFAULT_SORT_LIMITS if limits is None else limits def parse(self) -> tuple[SortField, ...]: """Parses the configured sort expression. @@ -72,7 +39,7 @@ def parse(self) -> tuple[SortField, ...]: SortParseError: If the expression is invalid. """ if not self._source: - return _EMPTY_FIELDS + return () max_sort_length = self._limits.max_sort_length max_fields = self._limits.max_fields diff --git a/tests/functional/test_public_api.py b/tests/functional/test_public_api.py index fe3d780..315b7e6 100644 --- a/tests/functional/test_public_api.py +++ b/tests/functional/test_public_api.py @@ -9,7 +9,6 @@ from typing_extensions import override import pyrsql -from pyrsql.core.compiler import CompilationResult from pyrsql.core.custom import CustomPredicateDefinition from pyrsql.core.json.options import DEFAULT_JSON_OPTIONS from pyrsql.core.options import QueryOptions @@ -43,8 +42,6 @@ def apply( class _ORM(ORM): """Minimal ORM fake for public API tests.""" - name = "fake-orm" - def __init__(self) -> None: self.last_query: Query | None = None @@ -147,12 +144,11 @@ def test_query_options_reject_mismatched_custom_predicate_key() -> None: ) -def test_compile_uses_orm_and_returns_compilation_result() -> None: - """Builds a compilation result through the high-level API.""" +def test_compile_uses_orm_and_returns_compiled_query() -> None: + """Returns the ORM-specific compiled query through the high-level API.""" orm = _ORM() result = pyrsql.compile("name==demo", orm=orm) - assert isinstance(result, CompilationResult) - assert result.orm_name == "fake-orm" + assert isinstance(result, _CompiledQuery) assert orm.last_query is not None assert orm.last_query.text == "name==demo" assert orm.last_query.bound_expression is not None diff --git a/tests/unit/adapters/fastapi_adapter/test_criteria.py b/tests/unit/adapters/fastapi_adapter/test_criteria.py index 64f63e0..d7a7e76 100644 --- a/tests/unit/adapters/fastapi_adapter/test_criteria.py +++ b/tests/unit/adapters/fastapi_adapter/test_criteria.py @@ -21,11 +21,7 @@ ) if TYPE_CHECKING: - from pyrsql.orms.base import ( - CompiledPageRequest, - CompiledQuery, - CompiledSort, - ) + from pyrsql.core.compiler import CompiledArtifact pytestmark = [pytest.mark.fastapi] @@ -33,20 +29,15 @@ class _UnusedORM(ORM): """Minimal ORM placeholder for isolated RequestCriteria.apply tests.""" - @property - @override - def name(self) -> str: - return "unused" - @override - def compile_query(self, query: Query) -> CompiledQuery: + def compile_query(self, query: Query) -> CompiledArtifact: del query raise AssertionError( "compile_query should not be called in this unit test", ) @override - def compile_sort(self, sort: Sort) -> CompiledSort: + def compile_sort(self, sort: Sort) -> CompiledArtifact: del sort raise AssertionError( "compile_sort should not be called in this unit test", @@ -56,7 +47,7 @@ def compile_sort(self, sort: Sort) -> CompiledSort: def compile_page_request( self, page_request: PageRequest, - ) -> CompiledPageRequest: + ) -> CompiledArtifact: del page_request raise AssertionError( "compile_page_request should not be called in this unit test", diff --git a/tests/unit/core/conftest.py b/tests/unit/core/conftest.py index 50b29fb..9062d4b 100644 --- a/tests/unit/core/conftest.py +++ b/tests/unit/core/conftest.py @@ -48,11 +48,6 @@ def __init__( self.sort_result = sort_result self.page_result = page_result - @property - def name(self) -> str: - """Returns a stable fake ORM name.""" - return "fake" - def compile_query(self, query: Query) -> FakeCompiledResult: # type: ignore[override] """Builds a fake compiled query result.""" result = ( diff --git a/tests/unit/core/test_options.py b/tests/unit/core/test_options.py index 0175d27..80411cb 100644 --- a/tests/unit/core/test_options.py +++ b/tests/unit/core/test_options.py @@ -228,6 +228,7 @@ def test_query_options_with_field_whitelist_returns_updated_copy() -> None: distinct=True, field_blacklist=frozenset({"blocked"}), parse_limits=DEFAULT_PARSE_LIMITS, + field_value_converters={"name": lambda raw: raw.upper()}, ) updated = options.with_field_whitelist(frozenset({"name"})) @@ -237,6 +238,7 @@ def test_query_options_with_field_whitelist_returns_updated_copy() -> None: assert updated.field_blacklist == frozenset({"blocked"}) assert updated.distinct is True assert updated.parse_limits is DEFAULT_PARSE_LIMITS + assert updated.field_value_converters["name"]("demo") == "DEMO" def test_sort_options_with_field_whitelist_returns_updated_copy() -> None: diff --git a/tests/unit/core/test_page.py b/tests/unit/core/test_page.py index 768301a..7ceab45 100644 --- a/tests/unit/core/test_page.py +++ b/tests/unit/core/test_page.py @@ -65,13 +65,13 @@ def test_page_request_rejects_non_aligned_offset() -> None: PageRequest.from_offset(offset=15, limit=10) -def test_page_request_compile_uses_orm_name( +def test_page_request_compile_returns_orm_artifact( fake_orm_factory: Callable[..., ORM], ) -> None: - """Ensures page compilation returns the selected ORM metadata.""" + """Returns the page artifact produced by the selected ORM.""" compilation = PageRequest.of(0, 10).compile(orm=fake_orm_factory()) - assert compilation.orm_name == "fake" + assert compilation.result == 0 def test_page_request_apply_uses_orm( diff --git a/tests/unit/core/test_query.py b/tests/unit/core/test_query.py index 053e7d2..765f556 100644 --- a/tests/unit/core/test_query.py +++ b/tests/unit/core/test_query.py @@ -79,13 +79,13 @@ def test_query_parse_resolves_shared_default_options( ) -def test_query_compile_uses_orm_name( +def test_query_compile_returns_orm_artifact( fake_orm_factory: Callable[..., ORM], ) -> None: - """Compiles the query with the selected ORM metadata.""" + """Returns the query artifact produced by the selected ORM.""" compilation = Query.parse("name==demo").compile(orm=fake_orm_factory()) - assert compilation.orm_name == "fake" + assert compilation.result == "name==demo" def test_query_apply_uses_orm( diff --git a/tests/unit/core/test_sort.py b/tests/unit/core/test_sort.py index 8d2824b..d7d43a1 100644 --- a/tests/unit/core/test_sort.py +++ b/tests/unit/core/test_sort.py @@ -65,13 +65,13 @@ def test_sort_parse_keeps_empty_bound_sort_when_no_fields_are_present( ) -def test_sort_compile_uses_orm_name( +def test_sort_compile_returns_orm_artifact( fake_orm_factory: Callable[..., ORM], ) -> None: - """Compiles the sort with the selected ORM metadata.""" + """Returns the sort artifact produced by the selected ORM.""" compilation = Sort.parse("name,asc").compile(orm=fake_orm_factory()) - assert compilation.orm_name == "fake" + assert compilation.result == "name,asc" def test_sort_apply_uses_orm( diff --git a/tests/unit/parsing/test_lexer.py b/tests/unit/parsing/test_lexer.py index ba677cd..e1cefe4 100644 --- a/tests/unit/parsing/test_lexer.py +++ b/tests/unit/parsing/test_lexer.py @@ -131,38 +131,6 @@ def test_lexer_uses_shared_default_limits_instance() -> None: assert lexer.limits is DEFAULT_PARSE_LIMITS -@pytest.mark.parametrize( - ("kwargs", "pattern"), - [ - pytest.param( - {"source": 123}, - r"string or SourceText", - id="invalid-source", - ), - pytest.param( - {"source": "name==demo", "limits": object()}, - r"ParseLimits instance", - id="invalid-limits", - ), - pytest.param( - { - "source": "name==demo", - "operator_registry": object(), - }, - r"OperatorRegistry instance", - id="invalid-operator-registry", - ), - ], -) -def test_lexer_rejects_invalid_runtime_dependencies( - kwargs: dict[str, object], - pattern: str, -) -> None: - """Lexer validates public constructor dependencies eagerly.""" - with pytest.raises(TypeError, match=pattern): - Lexer(**kwargs) # type: ignore[arg-type] - - def test_lexer_skips_operator_matching_for_non_prefix_characters() -> None: """Avoids scanning every operator when the prefix cannot match any.""" assert DEFAULT_OPERATOR_REGISTRY.match_candidates("n") == () diff --git a/tests/unit/parsing/test_operators.py b/tests/unit/parsing/test_operators.py deleted file mode 100644 index f21914d..0000000 --- a/tests/unit/parsing/test_operators.py +++ /dev/null @@ -1,105 +0,0 @@ -"""Unit tests for parsing operator models.""" - -from __future__ import annotations - -import pytest - -from pyrsql.parsing.operators import ComparisonOperator, OperatorRegistry - - -@pytest.mark.parametrize( - ("kwargs", "pattern"), - [ - pytest.param( - { - "name": None, - "spellings": ("==",), - "minimum_arguments": 1, - "maximum_arguments": 1, - }, - r"name must be a string", - id="non-string-name", - ), - pytest.param( - { - "name": " equal ", - "spellings": ("==",), - "minimum_arguments": 1, - "maximum_arguments": 1, - }, - r"outer whitespace", - id="whitespace-name", - ), - pytest.param( - { - "name": "equal", - "spellings": ["=="], - "minimum_arguments": 1, - "maximum_arguments": 1, - }, - r"tuple of strings", - id="non-tuple-spellings", - ), - pytest.param( - { - "name": "equal", - "spellings": (" =eq= ",), - "minimum_arguments": 1, - "maximum_arguments": 1, - }, - r"outer whitespace", - id="whitespace-spelling", - ), - pytest.param( - { - "name": "equal", - "spellings": ("==",), - "minimum_arguments": True, - "maximum_arguments": 1, - }, - r"minimum_arguments", - id="bool-minimum-arguments", - ), - ], -) -def test_comparison_operator_rejects_invalid_runtime_payloads( - kwargs: dict[str, object], - pattern: str, -) -> None: - """Comparison operators enforce a strict runtime contract.""" - with pytest.raises((TypeError, ValueError), match=pattern): - ComparisonOperator(**kwargs) - - -@pytest.mark.parametrize( - ("kwargs", "pattern"), - [ - pytest.param( - {"operators": ["bad"]}, - r"tuple of ComparisonOperator", - id="non-tuple-operators", - ), - pytest.param( - { - "operators": ( - ComparisonOperator( - name="equal", - spellings=("==",), - minimum_arguments=1, - maximum_arguments=1, - ), - "bad", - ), - }, - r"ComparisonOperator instances", - id="invalid-operator-entry", - ), - ], -) -def test_operator_registry_rejects_invalid_runtime_payloads( - kwargs: dict[str, object], - pattern: str, -) -> None: - """Operator registries enforce a strict runtime contract.""" - with pytest.raises((TypeError, ValueError), match=pattern): - OperatorRegistry(**kwargs) diff --git a/tests/unit/parsing/test_parser.py b/tests/unit/parsing/test_parser.py index 57d0fc2..ae9522a 100644 --- a/tests/unit/parsing/test_parser.py +++ b/tests/unit/parsing/test_parser.py @@ -90,16 +90,6 @@ def test_parser_parses_function_selector_in_comparison() -> None: assert isinstance(expression.selector.arguments[1], LiteralSelector) -def test_parser_ast_walk_traverses_logical_tree() -> None: - """Traverses the syntax tree depth-first.""" - expression = Parser("name==demo;city==sp").parse() - walked = tuple(expression.walk()) - - assert isinstance(walked[0], LogicalNode) - assert isinstance(walked[1], ComparisonNode) - assert isinstance(walked[2], ComparisonNode) - - def test_parser_accepts_custom_operator_registry() -> None: """Parses custom operators registered through the parser.""" all_match = ComparisonOperator( @@ -154,41 +144,6 @@ def test_parser_rejects_invalid_query_shapes( Parser(source, limits=limits).parse() -def test_parse_limits_reject_invalid_values() -> None: - """Rejects invalid parser safety limits.""" - with pytest.raises(ValueError, match="max_query_length"): - ParseLimits(max_query_length=0) - - -@pytest.mark.parametrize( - ("kwargs", "pattern"), - [ - pytest.param( - {"max_query_length": "10"}, - r"max_query_length", - id="string-query-limit", - ), - pytest.param( - {"max_selector_length": 1.5}, - r"max_selector_length", - id="float-selector-limit", - ), - pytest.param( - {"max_argument_length": True}, - r"max_argument_length", - id="bool-argument-limit", - ), - ], -) -def test_parse_limits_reject_non_integer_values( - kwargs: dict[str, object], - pattern: str, -) -> None: - """Rejects parser limits that are not strict integers.""" - with pytest.raises(TypeError, match=pattern): - ParseLimits(**kwargs) - - def test_parser_uses_shared_default_limits_instance() -> None: """Reuses the shared default limits on the common parse path.""" parser = Parser("name==demo") @@ -218,30 +173,3 @@ def test_parser_counts_expression_depth_semantically() -> None: "((name==demo))", limits=ParseLimits(max_expression_depth=2), ).parse() - - -@pytest.mark.parametrize( - ("kwargs", "pattern"), - [ - pytest.param( - {"source": "name==demo", "limits": object()}, - r"ParseLimits instance", - id="invalid-limits", - ), - pytest.param( - { - "source": "name==demo", - "operator_registry": object(), - }, - r"OperatorRegistry instance", - id="invalid-operator-registry", - ), - ], -) -def test_parser_rejects_invalid_runtime_dependencies( - kwargs: dict[str, object], - pattern: str, -) -> None: - """Parser validates public constructor dependencies eagerly.""" - with pytest.raises(TypeError, match=pattern): - Parser(**kwargs) # type: ignore[arg-type] diff --git a/tests/unit/parsing/test_source.py b/tests/unit/parsing/test_source.py index a7f5fdf..f2f6ba7 100644 --- a/tests/unit/parsing/test_source.py +++ b/tests/unit/parsing/test_source.py @@ -2,7 +2,7 @@ from __future__ import annotations -from pyrsql.parsing.source import SourcePosition, SourceSpan, SourceText +from pyrsql.parsing.source import SourcePosition, SourceSpan def test_source_span_cover_uses_outer_boundaries() -> None: @@ -20,8 +20,3 @@ def test_source_span_cover_uses_outer_boundaries() -> None: assert covered.start == start.start assert covered.end == end.end - - -def test_source_text_slice_returns_expected_substring() -> None: - """Source slices preserve normal behavior for valid bounds.""" - assert SourceText(text="name==demo").slice(0, 4) == "name" diff --git a/tests/unit/selector/test_ast.py b/tests/unit/selector/test_ast.py deleted file mode 100644 index 5a4a3f4..0000000 --- a/tests/unit/selector/test_ast.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Unit tests for selector AST models.""" - -from __future__ import annotations - -from pyrsql.selector.ast import FieldSelector, FunctionSelector, LiteralSelector - - -def test_function_selector_walk_traverses_nested_arguments() -> None: - """Function selectors walk nested selectors depth-first.""" - nested = FunctionSelector( - function_name="lower", - arguments=( - FieldSelector( - raw_path="user.name", - ), - ), - ) - selector = FunctionSelector( - function_name="upper", - arguments=(nested, LiteralSelector(value="x")), - ) - - walked = tuple(selector.walk()) - - assert walked == ( - selector, - nested, - nested.arguments[0], - selector.arguments[1], - ) diff --git a/tests/unit/selector/test_parser.py b/tests/unit/selector/test_parser.py index 31272c0..ab6ab93 100644 --- a/tests/unit/selector/test_parser.py +++ b/tests/unit/selector/test_parser.py @@ -30,43 +30,6 @@ def test_split_top_level_normalizes_fragments( assert parser.split_top_level(text, delimiter=delimiter) == expected -@pytest.mark.parametrize( - ("text", "delimiter", "expected_error", "pattern"), - [ - pytest.param( - None, - "|", - TypeError, - r"text must be a string", - id="non-string-text", - ), - pytest.param( - "a|b", - "", - ValueError, - r"single character", - id="empty-delimiter", - ), - pytest.param( - "a||b", - "||", - ValueError, - r"single character", - id="multi-character-delimiter", - ), - ], -) -def test_split_top_level_rejects_invalid_runtime_inputs( - text: object, - delimiter: object, - expected_error: type[Exception], - pattern: str, -) -> None: - """Top-level splitting enforces a strict runtime contract.""" - with pytest.raises(expected_error, match=pattern): - SelectorParser().split_top_level(text, delimiter=delimiter) # type: ignore[arg-type] - - @pytest.mark.parametrize( ("raw_selector", "expected_type", "expected_value"), [ @@ -109,18 +72,16 @@ def test_parse_supports_field_and_literal_selectors( assert selector.value is expected_value -def test_parse_function_selector_walks_nested_arguments() -> None: - """Parses nested function selectors into a traversable syntax tree.""" +def test_parse_function_selector_builds_nested_arguments() -> None: + """Parses nested function selectors into selector arguments.""" selector = SelectorParser().parse( "@upper[name|#1]", max_length=100, context="selector", ) - walked = tuple(selector.walk()) - assert isinstance(selector, FunctionSelector) - assert isinstance(walked[1], FieldSelector) - assert isinstance(walked[2], LiteralSelector) + assert isinstance(selector.arguments[0], FieldSelector) + assert isinstance(selector.arguments[1], LiteralSelector) @pytest.mark.parametrize( @@ -144,6 +105,12 @@ def test_parse_function_selector_walks_nested_arguments() -> None: SelectorParseError, id="invalid-function-selector", ), + pytest.param( + "@upper[]", + r"must have at least one argument", + SelectorParseError, + id="function-without-arguments", + ), pytest.param( "@upper[name||#1]", r"cannot contain empty arguments", @@ -176,48 +143,3 @@ def test_parse_rejects_invalid_selectors( max_length=100, context="selector", ) - - -@pytest.mark.parametrize( - ("kwargs", "expected_error", "pattern"), - [ - pytest.param( - {"raw_selector": "name", "max_length": 1.5, "context": "selector"}, - TypeError, - r"max_length must be an integer", - id="non-integer-max-length", - ), - pytest.param( - {"raw_selector": "name", "max_length": True, "context": "selector"}, - TypeError, - r"max_length must be an integer", - id="bool-max-length", - ), - pytest.param( - {"raw_selector": "name", "max_length": 0, "context": "selector"}, - ValueError, - r"greater than 0", - id="non-positive-max-length", - ), - pytest.param( - {"raw_selector": "name", "max_length": 10, "context": None}, - TypeError, - r"context must be a string", - id="non-string-context", - ), - pytest.param( - {"raw_selector": "name", "max_length": 10, "context": ""}, - ValueError, - r"context cannot be empty", - id="empty-context", - ), - ], -) -def test_parse_rejects_invalid_runtime_inputs( - kwargs: dict[str, object], - expected_error: type[Exception], - pattern: str, -) -> None: - """Selector parsing rejects invalid runtime inputs early.""" - with pytest.raises(expected_error, match=pattern): - SelectorParser().parse(**kwargs) # type: ignore[arg-type] diff --git a/tests/unit/sorting/test_parser.py b/tests/unit/sorting/test_parser.py index c9dd989..588b03a 100644 --- a/tests/unit/sorting/test_parser.py +++ b/tests/unit/sorting/test_parser.py @@ -82,33 +82,6 @@ def test_sort_parser_supports_function_selectors() -> None: assert selector.arguments[1].value == 123 -@pytest.mark.parametrize( - ("kwargs", "expected_error", "pattern"), - [ - pytest.param( - {"source": 123}, - TypeError, - r"source must be a string or None", - id="non-string-source", - ), - pytest.param( - {"source": "name", "limits": "bad"}, - TypeError, - r"limits must be a SortLimits instance", - id="invalid-limits-type", - ), - ], -) -def test_sort_parser_rejects_invalid_runtime_inputs( - kwargs: dict[str, object], - expected_error: type[Exception], - pattern: str, -) -> None: - """Sort parser rejects invalid runtime inputs early.""" - with pytest.raises(expected_error, match=pattern): - SortParser(**kwargs).parse() # type: ignore[arg-type] - - @pytest.mark.parametrize( ("source", "limits", "pattern"), [ @@ -166,41 +139,6 @@ def test_sort_parser_rejects_invalid_input( SortParser(source, limits=limits).parse() -def test_sort_limits_reject_invalid_values() -> None: - """Rejects invalid sort parser safety limits.""" - with pytest.raises(ValueError, match="max_sort_length"): - SortLimits(max_sort_length=0) - - -@pytest.mark.parametrize( - ("kwargs", "pattern"), - [ - pytest.param( - {"max_sort_length": "10"}, - r"max_sort_length", - id="string-sort-length", - ), - pytest.param( - {"max_fields": 1.5}, - r"max_fields", - id="float-max-fields", - ), - pytest.param( - {"max_field_path_length": True}, - r"max_field_path_length", - id="bool-field-path-length", - ), - ], -) -def test_sort_limits_reject_non_integer_values( - kwargs: dict[str, object], - pattern: str, -) -> None: - """Rejects sort limits that are not strict integers.""" - with pytest.raises(TypeError, match=pattern): - SortLimits(**kwargs) - - def test_sort_parse_error_exposes_structured_diagnostic() -> None: """Exposes a structured diagnostic on sort parsing failures.""" with pytest.raises(SortParseError) as exc_info: diff --git a/uv.lock b/uv.lock index 829a13a..3f8f15e 100644 --- a/uv.lock +++ b/uv.lock @@ -121,107 +121,89 @@ wheels = [ [[package]] name = "charset-normalizer" -version = "3.4.7" +version = "3.4.9" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d", size = 315182, upload-time = "2026-04-02T09:25:40.673Z" }, - { url = "https://files.pythonhosted.org/packages/24/47/b192933e94b546f1b1fe4df9cc1f84fcdbf2359f8d1081d46dd029b50207/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8", size = 209329, upload-time = "2026-04-02T09:25:42.354Z" }, - { url = "https://files.pythonhosted.org/packages/c2/b4/01fa81c5ca6141024d89a8fc15968002b71da7f825dd14113207113fabbd/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790", size = 231230, upload-time = "2026-04-02T09:25:44.281Z" }, - { url = "https://files.pythonhosted.org/packages/20/f7/7b991776844dfa058017e600e6e55ff01984a063290ca5622c0b63162f68/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc", size = 225890, upload-time = "2026-04-02T09:25:45.475Z" }, - { url = "https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393", size = 216930, upload-time = "2026-04-02T09:25:46.58Z" }, - { url = "https://files.pythonhosted.org/packages/e2/ab/b18f0ab31cdd7b3ddb8bb76c4a414aeb8160c9810fdf1bc62f269a539d87/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153", size = 202109, upload-time = "2026-04-02T09:25:48.031Z" }, - { url = "https://files.pythonhosted.org/packages/82/e5/7e9440768a06dfb3075936490cb82dbf0ee20a133bf0dd8551fa096914ec/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af", size = 214684, upload-time = "2026-04-02T09:25:49.245Z" }, - { url = "https://files.pythonhosted.org/packages/71/94/8c61d8da9f062fdf457c80acfa25060ec22bf1d34bbeaca4350f13bcfd07/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34", size = 212785, upload-time = "2026-04-02T09:25:50.671Z" }, - { url = "https://files.pythonhosted.org/packages/66/cd/6e9889c648e72c0ab2e5967528bb83508f354d706637bc7097190c874e13/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1", size = 203055, upload-time = "2026-04-02T09:25:51.802Z" }, - { url = "https://files.pythonhosted.org/packages/92/2e/7a951d6a08aefb7eb8e1b54cdfb580b1365afdd9dd484dc4bee9e5d8f258/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752", size = 232502, upload-time = "2026-04-02T09:25:53.388Z" }, - { url = "https://files.pythonhosted.org/packages/58/d5/abcf2d83bf8e0a1286df55cd0dc1d49af0da4282aa77e986df343e7de124/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53", size = 214295, upload-time = "2026-04-02T09:25:54.765Z" }, - { url = "https://files.pythonhosted.org/packages/47/3a/7d4cd7ed54be99973a0dc176032cba5cb1f258082c31fa6df35cff46acfc/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616", size = 227145, upload-time = "2026-04-02T09:25:55.904Z" }, - { url = "https://files.pythonhosted.org/packages/1d/98/3a45bf8247889cf28262ebd3d0872edff11565b2a1e3064ccb132db3fbb0/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a", size = 218884, upload-time = "2026-04-02T09:25:57.074Z" }, - { url = "https://files.pythonhosted.org/packages/ad/80/2e8b7f8915ed5c9ef13aa828d82738e33888c485b65ebf744d615040c7ea/charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374", size = 148343, upload-time = "2026-04-02T09:25:58.199Z" }, - { url = "https://files.pythonhosted.org/packages/35/1b/3b8c8c77184af465ee9ad88b5aea46ea6b2e1f7b9dc9502891e37af21e30/charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943", size = 159174, upload-time = "2026-04-02T09:25:59.322Z" }, - { url = "https://files.pythonhosted.org/packages/be/c1/feb40dca40dbb21e0a908801782d9288c64fc8d8e562c2098e9994c8c21b/charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008", size = 147805, upload-time = "2026-04-02T09:26:00.756Z" }, - { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, - { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, - { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, - { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, - { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, - { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, - { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, - { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, - { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, - { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, - { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, - { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, - { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, - { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, - { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, - { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, - { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, - { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, - { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, - { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, - { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, - { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, - { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, - { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, - { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, - { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, - { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, - { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, - { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, - { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, - { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, - { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, - { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, - { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, - { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, - { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, - { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, - { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, - { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, - { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, - { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, - { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, - { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, - { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, - { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, - { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, - { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, - { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, - { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, - { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, - { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, - { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, - { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, - { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, - { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, - { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, - { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, - { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, - { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, - { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, - { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, - { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, - { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, - { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, - { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, - { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, - { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, - { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, - { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, - { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, - { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, - { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, - { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, - { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, - { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, - { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, - { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, - { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, + { url = "https://files.pythonhosted.org/packages/ad/81/8e983840c6e5b93b33c2ba81aa3d52c2e42f0e9a690ce7607a2e61da4a5c/charset_normalizer-3.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a", size = 322240, upload-time = "2026-07-07T14:32:36.236Z" }, + { url = "https://files.pythonhosted.org/packages/de/d1/b4319dc3229d8272fba305e206fc0a148e2de8d4087917ce62ae6382f359/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616", size = 216475, upload-time = "2026-07-07T14:32:38.142Z" }, + { url = "https://files.pythonhosted.org/packages/80/33/6c99c1b3e6b8bf730e1bc809b9a2608f224145069114c479a2e9e1494346/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209", size = 238670, upload-time = "2026-07-07T14:32:39.658Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f4/ffbb83546e1f198ecc70ecd372b65cf2b50f9068b380abd67640f17a8e18/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99", size = 233476, upload-time = "2026-07-07T14:32:41.155Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5f/b98b8da398637b551e427e7be922bdec19177dc54d6811dcdaa503f23aac/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8", size = 223817, upload-time = "2026-07-07T14:32:42.592Z" }, + { url = "https://files.pythonhosted.org/packages/36/31/a276bb2e66243072a3fd06fdcab9cbb61a305b02143d70d2bda21d888fa8/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b", size = 207974, upload-time = "2026-07-07T14:32:44.258Z" }, + { url = "https://files.pythonhosted.org/packages/5e/be/7ee4453d7e88dfbc4104ccd34900b9f2c7c17dac22881865fe0e82424a25/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2", size = 221655, upload-time = "2026-07-07T14:32:45.64Z" }, + { url = "https://files.pythonhosted.org/packages/1d/85/181c652953eb5276d198f375b1dd641047392050098100a3a02d6534f657/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9", size = 219229, upload-time = "2026-07-07T14:32:47.376Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e7/aaf6da33fc9f4691cda8f7efbc9f69179d3d39ec8a4799baf273ee1d8db0/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15", size = 209704, upload-time = "2026-07-07T14:32:48.855Z" }, + { url = "https://files.pythonhosted.org/packages/63/01/f2fb3bd3a73be48b173ee0c6aa8d2497af97d5663a8c4c4b491de4c62f7a/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d", size = 226243, upload-time = "2026-07-07T14:32:50.239Z" }, + { url = "https://files.pythonhosted.org/packages/c4/02/c57a22739fe05246b0b5783b3bfb6afaac4eebb46f3ececdfb2f048f780e/charset_normalizer-3.4.9-cp310-cp310-win32.whl", hash = "sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381", size = 150935, upload-time = "2026-07-07T14:32:51.676Z" }, + { url = "https://files.pythonhosted.org/packages/37/8d/ca39a7559a4797505530d084fd3a49a2c959efbbbff146302fb7be4e3b35/charset_normalizer-3.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee", size = 162314, upload-time = "2026-07-07T14:32:53.193Z" }, + { url = "https://files.pythonhosted.org/packages/01/da/a44bd7a13d426e69e4894557106cd58669097bfad4a8681123b618fbfc5d/charset_normalizer-3.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419", size = 153075, upload-time = "2026-07-07T14:32:54.554Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5", size = 317075, upload-time = "2026-07-07T14:32:56.021Z" }, + { url = "https://files.pythonhosted.org/packages/c3/69/2a5385192e67175f7d8bd5ce4f57c24bc956439adeae5c13a99aa28a53d1/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2", size = 213837, upload-time = "2026-07-07T14:32:57.78Z" }, + { url = "https://files.pythonhosted.org/packages/b3/46/03ddc7da576d814fe0a36dd1f0fd3258e95404b4b2e3c026b7923d7e133f/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a", size = 235503, upload-time = "2026-07-07T14:32:59.205Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6e/de0229a7ef40f6f9d28a837eebf4ec47bdca5dab4e900c84f22919af636a/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29", size = 229944, upload-time = "2026-07-07T14:33:00.803Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/49b9060e8418b14fb5cba9cf6bfb383111e2538a03a1fb18e66a95aeb3d5/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c", size = 221276, upload-time = "2026-07-07T14:33:02.199Z" }, + { url = "https://files.pythonhosted.org/packages/44/95/80282cce0fae9c3061203d723ee87da996aed79679e65d8935050ee7ca1f/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b", size = 205260, upload-time = "2026-07-07T14:33:03.698Z" }, + { url = "https://files.pythonhosted.org/packages/0c/74/2f62c8821b969ea3bd67cc2e6976834f48ca5d12664d2559ebcd9bcfbed7/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db", size = 217786, upload-time = "2026-07-07T14:33:05.12Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8d/feabb82cb49fcad14515b1d7d1ca4787b0da7fc723a212bf89bc9e0fac52/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993", size = 216798, upload-time = "2026-07-07T14:33:06.629Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ff/c946d63bc3786d5b84d960b0f7ab7e25b828486a946b5aa997625bcaf6a6/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da", size = 206429, upload-time = "2026-07-07T14:33:08.006Z" }, + { url = "https://files.pythonhosted.org/packages/af/ba/5e5007c370702f85d2ef75791fac7943ed41e080364a673b20142e430e3e/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3", size = 223066, upload-time = "2026-07-07T14:33:09.783Z" }, + { url = "https://files.pythonhosted.org/packages/83/d5/9096aa3cf532dfad237861544eb47a0f20d5adbf1039760fed8eaae935d9/charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d", size = 150456, upload-time = "2026-07-07T14:33:11.217Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a1/e29995109e455dc8eff8d0fac6ae509be39561318a7cfeac5d33ad029213/charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1", size = 161410, upload-time = "2026-07-07T14:33:12.743Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8d/1569f4d0032d6ba2a4fe4591c35bf87868c600c41a71eb5c2e1ffa8464c2/charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec", size = 152649, upload-time = "2026-07-07T14:33:14.173Z" }, + { url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" }, + { url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" }, + { url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" }, + { url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" }, + { url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" }, + { url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" }, + { url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" }, + { url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" }, + { url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" }, + { url = "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", size = 317688, upload-time = "2026-07-07T14:33:35.408Z" }, + { url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982, upload-time = "2026-07-07T14:33:36.996Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460, upload-time = "2026-07-07T14:33:38.536Z" }, + { url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003, upload-time = "2026-07-07T14:33:40.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149, upload-time = "2026-07-07T14:33:41.631Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901, upload-time = "2026-07-07T14:33:43.209Z" }, + { url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176, upload-time = "2026-07-07T14:33:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356, upload-time = "2026-07-07T14:33:46.192Z" }, + { url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614, upload-time = "2026-07-07T14:33:47.705Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991, upload-time = "2026-07-07T14:33:49.238Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622, upload-time = "2026-07-07T14:33:50.711Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947, upload-time = "2026-07-07T14:33:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594, upload-time = "2026-07-07T14:33:53.486Z" }, + { url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" }, + { url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" }, + { url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" }, + { url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" }, + { url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" }, + { url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" }, + { url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" }, + { url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" }, + { url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" }, + { url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" }, + { url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" }, + { url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" }, + { url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" }, + { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, ] [[package]] @@ -552,89 +534,89 @@ wheels = [ [[package]] name = "librt" -version = "0.12.0" +version = "0.13.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c6/e0/dbd0f2a68a1c1a1991eb7921ff6014465d56608cdc9a9fb468a616210a37/librt-0.12.0.tar.gz", hash = "sha256:cb26faedbd09c6130e9c1b64d8000efec5076ffd18d606c6cd1cf02730e6d8b0", size = 203841, upload-time = "2026-06-30T16:14:29.671Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/2f/3908645ddddab7120b46295e541ead308109fa48dbec7d67d7a778870d60/librt-0.13.0.tar.gz", hash = "sha256:1d2a610c14ac0d0750ee0a3ab8548e83155258387891caaca04def4bf7289781", size = 211402, upload-time = "2026-07-08T12:26:29.834Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/66/c9d88366893b4b0df6b5375c27ebc9f14c43419d9e244b493be20e85bc74/librt-0.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fe3547407bbce45c09885591f90168325c5a31a6795b9a13f6b9ff3d25093d93", size = 144398, upload-time = "2026-06-30T16:12:03.947Z" }, - { url = "https://files.pythonhosted.org/packages/bd/f2/9be1c6da204701163ec3aaedbf893d2f656b363d8fa302af536ce6471eb4/librt-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5925eca673207204a3adca040a91bdd3738fc7ba48da647ccd55732692a35736", size = 148924, upload-time = "2026-06-30T16:12:05.583Z" }, - { url = "https://files.pythonhosted.org/packages/d8/f3/256824ee27649c6e0a693db25d391f97b43b52364f8efb466014a564bbc7/librt-0.12.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f9ef097a7711465a204454c69658bbb6b2a6be9bdef0eeeba9a042016d00688", size = 479654, upload-time = "2026-06-30T16:12:07.175Z" }, - { url = "https://files.pythonhosted.org/packages/a8/3f/f4adbb3f293a04bd3dc2eb91d814f5b1e221e6b4522585696ba6901a0b9a/librt-0.12.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:57abc8b65edf1a8e80e5472c81c108a7527202e5febfda9e00a684dbaeae534e", size = 472318, upload-time = "2026-06-30T16:12:08.758Z" }, - { url = "https://files.pythonhosted.org/packages/9d/b5/362c93f7b43d4ef84a3d5f156c8d4eeddb22badcf5529a1281c387abbbd7/librt-0.12.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e6f53732a8ae5012a3b6ae092da2933be74ec4169d16038f4af87a0019afea", size = 501555, upload-time = "2026-06-30T16:12:10.623Z" }, - { url = "https://files.pythonhosted.org/packages/24/1d/2d6abf059c3a4b88a6668e7bb81af332b14463028ac8f2b08a1212eb1ebc/librt-0.12.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:edb5f06cdb38d6ef9fd7ae06d62962d65c881b5f965d5e8a6c53e59c15ae4338", size = 494118, upload-time = "2026-06-30T16:12:12.503Z" }, - { url = "https://files.pythonhosted.org/packages/39/c1/f91f3094be2c76361d88aca613d8b7586d15b6026714d59d2e3dc0e35f44/librt-0.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1473ef42263dfee7553a5c460f11730a4409acf0d52629b284eb1e6b13eb460a", size = 516318, upload-time = "2026-06-30T16:12:14.192Z" }, - { url = "https://files.pythonhosted.org/packages/8e/e2/5211af94252458cbed7a6250163dff9c5a84aec29609121c828375a3b319/librt-0.12.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1d6f69a06295fb6ad8dcf92b4b2d15d211842005e86eedce64d88e0633592f58", size = 522294, upload-time = "2026-06-30T16:12:15.879Z" }, - { url = "https://files.pythonhosted.org/packages/90/9b/de31f5b9fdf7fa3699c4bbbecf82ebd52013d5d6b500b70b07b0ebacbd51/librt-0.12.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:3275d0270cd07ca9c2e140ae4da34e24a0350e98c6e3815dce96ead67cf0487d", size = 502494, upload-time = "2026-06-30T16:12:17.394Z" }, - { url = "https://files.pythonhosted.org/packages/b1/48/22c18dff89f3900dddb3e470e6f7febcda37ff3667b73097a848c9a608b2/librt-0.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a4834462ec68613024d063c7efe9b188e350d40fda9ba937372039883d2a8051", size = 543422, upload-time = "2026-06-30T16:12:19.006Z" }, - { url = "https://files.pythonhosted.org/packages/52/7b/74691b4b55944227245fffef063714e3ab9707ab1111eb0068512b428c7c/librt-0.12.0-cp310-cp310-win32.whl", hash = "sha256:bcf9b55ac089e8cf201d2146833e1097812c15dcea61911e84d6a2904cf78893", size = 97642, upload-time = "2026-06-30T16:12:20.386Z" }, - { url = "https://files.pythonhosted.org/packages/c5/dc/7f8fa369a1f7cc9b090fecd373659ada0e9bab1ae4a3ac9f163eabd04977/librt-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0a122002f7e0d5c93e84465c4b3fe86621402b7b92f1e2bc0784ebe67793112", size = 117583, upload-time = "2026-06-30T16:12:21.829Z" }, - { url = "https://files.pythonhosted.org/packages/9e/ab/628490f42d1eba82f3c7e5821aa62013e6df7f525b7a9e92c048f8d1cc1c/librt-0.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3f13c1e8563102c2b17581cf37fcb2c6dae7ad485ccea93ae46258998c25f9a1", size = 143821, upload-time = "2026-06-30T16:12:23.248Z" }, - { url = "https://files.pythonhosted.org/packages/38/5f/793e8b6f4b6ac16e7d7198478c0af3670606fbb535c768d5f3e954781423/librt-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d1ddff067610a122387024c4df527493b909d41e54a6e5b2d0e6c1041d6dfa09", size = 148442, upload-time = "2026-06-30T16:12:24.582Z" }, - { url = "https://files.pythonhosted.org/packages/ad/92/c780fe37a9e0982f3bd8fd9a631d6b95d09a5a7201c6c50366ce843b7e42/librt-0.12.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8dc7ebb5f3eec062398e9d0ef1938acd21b589e74286c4a8906d0183318d91b", size = 478276, upload-time = "2026-06-30T16:12:26.101Z" }, - { url = "https://files.pythonhosted.org/packages/41/bb/226d444bc20d7dff4a19ec6c1ff2c13a76385eebddb59c9c00c923b67536/librt-0.12.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:198de569ea9d5f6f33808f1c00cc3db9de62bf4d6deafa3b052bd08255083038", size = 472337, upload-time = "2026-06-30T16:12:27.83Z" }, - { url = "https://files.pythonhosted.org/packages/12/79/98ac0840ee90a75d4e1155c79062860b12ccca508587ff2119fc086965f2/librt-0.12.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e958678a8bca56016aedc891b391c0e0813ea382a874b54a2c1b313c1d232720", size = 502087, upload-time = "2026-06-30T16:12:29.443Z" }, - { url = "https://files.pythonhosted.org/packages/6f/72/a6b1a0d080606a7f5f646b79a1496f21d709f8563877759ace9ce5adad73/librt-0.12.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575a6eca68c8437ed4a8e0f534e31d74b562ba1049a0ee4b5f09e114bcc21be1", size = 493202, upload-time = "2026-06-30T16:12:31.077Z" }, - { url = "https://files.pythonhosted.org/packages/69/cf/e1b036b45f2fc272205ee18bf272b47e8d684bf1a75af26db440c7504359/librt-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:86f241c50dc9e9a3f0db6dbb37a607c8205aa87b920802dabbd50b70d40f6939", size = 514139, upload-time = "2026-06-30T16:12:33.032Z" }, - { url = "https://files.pythonhosted.org/packages/40/34/b193b3e6985469a2f8afa86c90012329c86480b6ff4f2e4bd7b5b937e134/librt-0.12.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:113417b934fbf38220a9c7fe94578cefbe7dbb047adcb75aa197905af2b13724", size = 519486, upload-time = "2026-06-30T16:12:34.996Z" }, - { url = "https://files.pythonhosted.org/packages/31/9e/7de4947b1695f247c813f833e3c1e7b77b52e52a7dba2c35411cf806b58e/librt-0.12.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:762f17c0eb6b5d74e269126996cea8a89e35ab6464c5151619163abcd8623ae2", size = 499609, upload-time = "2026-06-30T16:12:36.663Z" }, - { url = "https://files.pythonhosted.org/packages/59/11/f3730e04e758b1fbf215359062ad2d5b6bd0b0ab5ac46b1c140628795be7/librt-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6aa93b3bd7f7588c628f6e9bf66485d3467fd9a1ccdb8975b770178f39f35697", size = 542205, upload-time = "2026-06-30T16:12:38.56Z" }, - { url = "https://files.pythonhosted.org/packages/1f/8f/710453617eabe20e18433864f335534c8aff63fbc68d8cd9dbc70a3d08f6/librt-0.12.0-cp311-cp311-win32.whl", hash = "sha256:aaa04b44d4fe86d824616b1f9c13e34c7c01ec0c96dd2abc4f59423696f788e2", size = 98067, upload-time = "2026-06-30T16:12:40.102Z" }, - { url = "https://files.pythonhosted.org/packages/42/53/401bff50a56e95daf151d911c99adf5732af2190e8f4d11886c9a229103c/librt-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:9aaeeddb8e7e4ae3bb9f944e0e618418cb91c0071d5ddbfcc3584b3cf59d39f0", size = 118346, upload-time = "2026-06-30T16:12:41.388Z" }, - { url = "https://files.pythonhosted.org/packages/e5/9a/a3a9078fe88bfc2d2d99dcf1c18593938ae830089cf84c3b2532a6c49d63/librt-0.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:18a2402fa3123ab76ecca670e6fb33038fde7c1e91181b885226ec4d30af2c2c", size = 104760, upload-time = "2026-06-30T16:12:43.112Z" }, - { url = "https://files.pythonhosted.org/packages/d5/1a/5bec493821b0e85b91de4f234912b50133d1aedb875048eef27938ec3f96/librt-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9bce19aa7c05f91c989f9da7b567f81d21d57a2e6501e2b811aa0f3f79614c1a", size = 146756, upload-time = "2026-06-30T16:12:44.395Z" }, - { url = "https://files.pythonhosted.org/packages/b9/d0/cc04b48a57c1f275387f5578847214c4a6c21bfb24c6c8c8d6ba753fe403/librt-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0ace09f5bf4d982fe726015f102fb856658b41580597104e301e630ed1d8d86", size = 145537, upload-time = "2026-06-30T16:12:45.95Z" }, - { url = "https://files.pythonhosted.org/packages/9e/10/c02325556beb2aa158c9e549ddade8cc9a23b36cdad14756dbed730c1ff1/librt-0.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d007efe9243ede81ce75990ad7aa172da1e2024144b3eff17ba46a5fff1fff3c", size = 488637, upload-time = "2026-06-30T16:12:47.658Z" }, - { url = "https://files.pythonhosted.org/packages/cb/9e/7b49ca1c30baa9c8df96024aa09a97c35a97455e36004c9b5311703c56f3/librt-0.12.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:ad324a5e4858388a4864915b90a42efc8b374376393f14b9940f2454e791912b", size = 483651, upload-time = "2026-06-30T16:12:49.283Z" }, - { url = "https://files.pythonhosted.org/packages/4d/71/03c8c8cec39645fda451132ff9d6d662fc5aea42a1a188a77a4fddb35906/librt-0.12.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10a40cf74cdd97b6f8f905056db73f5d459783de2ca04c6ebd1bf47652818e7e", size = 518359, upload-time = "2026-06-30T16:12:50.999Z" }, - { url = "https://files.pythonhosted.org/packages/e0/ec/a9f357f94bbcba92277d22af22cff42ef706ae5d9d6d58b69bebf3a67954/librt-0.12.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:92e61c09de95217ae02a9d17f4f66cf073253cdc51bcfdc0f15c62c9a70baa85", size = 509510, upload-time = "2026-06-30T16:12:52.631Z" }, - { url = "https://files.pythonhosted.org/packages/7a/34/717055325d028743aa01a7691ad59a63352a26a8ff2e7eeb0c9249514150/librt-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0461344061d6fc3718940f5855d95647831cef6d03a6c7506897f98222784ad4", size = 527302, upload-time = "2026-06-30T16:12:54.244Z" }, - { url = "https://files.pythonhosted.org/packages/95/f8/7612eeedb3395d92f7c6a84dca5f15e282d650483a4dc01aa5b9cffdfda3/librt-0.12.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e6dfe89074732c9287b3c0f5a6af575c9ede380a788013876cc7b14fe0da0361", size = 532568, upload-time = "2026-06-30T16:12:55.74Z" }, - { url = "https://files.pythonhosted.org/packages/79/1e/a9afe85d5bb8b65dc27be3809ed1d69082079e1e9717fd2c66aa9939600c/librt-0.12.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9efed79d51ad1383bba0855f613cca7aa91c943e709af2413ac7f4bb9936ce08", size = 521579, upload-time = "2026-06-30T16:12:57.884Z" }, - { url = "https://files.pythonhosted.org/packages/b3/1e/93aebb219d52c37ea578f83b0588cd7b040974e464d4e435086a48b4dc4d/librt-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1eac6cc0e23e448fb3c1446ed85ff796afb616eed5897c978d35dbec030b7c7c", size = 558743, upload-time = "2026-06-30T16:12:59.577Z" }, - { url = "https://files.pythonhosted.org/packages/3c/85/1680c0ec332f238e3145c5608d313ab0a43281e210a5dd87e3bc3cc25631/librt-0.12.0-cp312-cp312-win32.whl", hash = "sha256:0ab8ee0210047ae86ca023ccfbfe3df82077fd1c9bc021aebbf37d993ef64af0", size = 99200, upload-time = "2026-06-30T16:13:01.015Z" }, - { url = "https://files.pythonhosted.org/packages/30/0e/abca12d8904875aa2ad66327390a3f7b1b75ebc43c0a00fc763cecf32ea5/librt-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:51c8bfa12632c81b94401c101bcedd0c56c3a1f8fa3273ca3472b28cd2f54003", size = 119390, upload-time = "2026-06-30T16:13:02.493Z" }, - { url = "https://files.pythonhosted.org/packages/32/a5/4203481b6d3a3bb348c82ac71abf1fcb4cb3ae8422a24a8dee4cd3ac5bd7/librt-0.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:5eebd451f5def089369ba6d8ff0291303d035e8154f9f26f7633835c5b029ade", size = 105117, upload-time = "2026-06-30T16:13:03.952Z" }, - { url = "https://files.pythonhosted.org/packages/f2/87/568d948c8079c9ff3c9e8110cf85f1eb70218e1209af29d0b7b89aa4a60c/librt-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8d9a55760a34ae5ce70434aabb6a6c61c6c44a0ec58ca1cfd9cd86e4745d417d", size = 146808, upload-time = "2026-06-30T16:13:05.417Z" }, - { url = "https://files.pythonhosted.org/packages/e7/1d/bea471ecea210088847bb5f3c4b4b424d596518934c06679b78ca85d6e63/librt-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ff0b197e338b4cf432873e0d6ef025213fdea85311ec4d87d2ea88c28adf2409", size = 145503, upload-time = "2026-06-30T16:13:07.023Z" }, - { url = "https://files.pythonhosted.org/packages/eb/9e/984ad422b56de95fdce158f06b051655373784ebea0aba9a7fcbc41614d1/librt-0.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e69f120a20b69e2539d603bbd4d62db38399b10f8bf73a1cf445038a621e8af", size = 488421, upload-time = "2026-06-30T16:13:08.492Z" }, - { url = "https://files.pythonhosted.org/packages/50/03/1a2f94009b07ea71f8e1a4cfe53370565b56da9caa341b89e0699325e9f5/librt-0.12.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:fde3cde595e947fc8e755b0a21f919a1622483d07c662d00496e040773d22591", size = 483488, upload-time = "2026-06-30T16:13:10.169Z" }, - { url = "https://files.pythonhosted.org/packages/aa/3b/084bdc295823fbb6ab91670047adf8f420787f9e8794bf2d140b66dc196b/librt-0.12.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d977447315fa09ea4e8c7ae9b4e22f7659b5128161c1fd55ff786b5349f73503", size = 518428, upload-time = "2026-06-30T16:13:11.681Z" }, - { url = "https://files.pythonhosted.org/packages/c9/22/5a307390b93a115ffbecd95c64eecb4e56269680e45e9415ada7285f2cf4/librt-0.12.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7ffac8a67e4143cea9a549d4822b93bc0bbaad73fc25aa0ab0ba5ec27d178677", size = 509744, upload-time = "2026-06-30T16:13:13.217Z" }, - { url = "https://files.pythonhosted.org/packages/b5/90/83f3cb6184f5d669660717b4b2e317c9ddaccf7ca5bb97f2196deac1a3b7/librt-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:94af1ed773ff104ef08ef3d669a0ba9d3a5916c609eb698cffe5d5476d66ff9b", size = 527749, upload-time = "2026-06-30T16:13:15.277Z" }, - { url = "https://files.pythonhosted.org/packages/7d/3b/f162be5cc88d47378e3a20776fe425fa1c2bece755da15e2783ebf06d3d6/librt-0.12.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:548199d21d22fb26398dfbbe0ba953a52465c66f3a49f38e6fddce1b127faf53", size = 532582, upload-time = "2026-06-30T16:13:17.074Z" }, - { url = "https://files.pythonhosted.org/packages/c9/28/6c5d2f6b7232fd24f284fc4cab37a459fe69a9096a09942f44cc5c55e073/librt-0.12.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c8f1f413b966a9dd3ecf80cd337b0ad7bb3de2474a4ff448ed3ebabfc3f803fc", size = 522235, upload-time = "2026-06-30T16:13:18.823Z" }, - { url = "https://files.pythonhosted.org/packages/a9/1c/bd115360587fdc22c8ae8fac14c040a556b442e2965d4370d2cf274c8b95/librt-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:55f13f95b629be5b6ab38918e439bf14169d6f9a8deaae55e0c14e12fb0c74b9", size = 559055, upload-time = "2026-06-30T16:13:20.509Z" }, - { url = "https://files.pythonhosted.org/packages/fe/5a/c26f49f576437014825a86faea3cec60c1ed17f976abd567b6c12b8e35a7/librt-0.12.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:8b2dc079dfe29e77a47a19073d2040fa4879aa3656501f1650f8402ddce0313c", size = 79809, upload-time = "2026-06-30T16:13:22.401Z" }, - { url = "https://files.pythonhosted.org/packages/69/0b/a55244261d9ad7375ac039b8af06d42602722e2e8b8d8d6b86e4a3888c02/librt-0.12.0-cp313-cp313-win32.whl", hash = "sha256:da58944be8270f2bfee628a9a2a60c1cf6a12c8bea8e2c9b6edf3e5414ca7793", size = 99308, upload-time = "2026-06-30T16:13:23.661Z" }, - { url = "https://files.pythonhosted.org/packages/c9/bf/ed9465e58d44c5a5637795547d0841c8934aab905ea452cac1adf14672cf/librt-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:1db4be3037e4ce065a071fa7deee93e78ebc25f448340a02a6c1c0b82c37e383", size = 119438, upload-time = "2026-06-30T16:13:25.188Z" }, - { url = "https://files.pythonhosted.org/packages/c0/44/3cad652aeb892e6e8ffe48d0fafa2bc652f28ec7ed3f4403fcbb1be4f948/librt-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:05fd2542892ad770b5dd45003fd080477cf220b611d3ee59b0792097eb0873a9", size = 105118, upload-time = "2026-06-30T16:13:26.533Z" }, - { url = "https://files.pythonhosted.org/packages/0e/51/3a0e05618c12423b6fc5141b590ec02a6efb645833edc8736a6c7b46d1ec/librt-0.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:b37ee42e09722284a6d9288fe44a191f7276060a3195939bb77c6502058dbb34", size = 145579, upload-time = "2026-06-30T16:13:27.909Z" }, - { url = "https://files.pythonhosted.org/packages/77/9e/fd399d099dfb4020f3f7c34e7e6210c389fa89f7d79ca92f5afb0395f278/librt-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ade11988728b3e4768dadc5696e82c60e9b35fc95335a9b4d1f5d69e753ccec7", size = 150139, upload-time = "2026-06-30T16:13:29.357Z" }, - { url = "https://files.pythonhosted.org/packages/7a/ee/610239fbd8c4b005443664c5d4c3bc1717daedd8c71369bf45011aa87194/librt-0.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f351ed425380e39bd86df382578aa5b8c5b98e2e265112de7379e7d030258150", size = 480457, upload-time = "2026-06-30T16:13:30.78Z" }, - { url = "https://files.pythonhosted.org/packages/0c/10/ceddc9010f26c541444be36e1153a79b64626694db2d33a524c719fa3e46/librt-0.12.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:857d2163e088c868967717ace8e980017fd868a735f3de010412af02bdc30319", size = 479002, upload-time = "2026-06-30T16:13:32.398Z" }, - { url = "https://files.pythonhosted.org/packages/4e/f1/b1523d9718e8192e5403e6b41a02742e17ba554369f0729b9f30ab590e2d/librt-0.12.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2befc80aa5f2f5b93f28abaaf11feff6677931dd548320e44c52deaa9399744", size = 510527, upload-time = "2026-06-30T16:13:34.615Z" }, - { url = "https://files.pythonhosted.org/packages/f6/0e/0f3ff43befb18a531615736791e52fb67eaa71ff7b89e6e5f7004b64cc6e/librt-0.12.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:be3694dcfa97c6715dd19ac73d3e1b21a805514a5785663e57fecacd3ff64e5a", size = 500988, upload-time = "2026-06-30T16:13:36.408Z" }, - { url = "https://files.pythonhosted.org/packages/a8/1a/0278ea4a9e599dc507c43839a87f2c764ad04bf69418e2d763d58659e55f/librt-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2d5f67e86f45638843d025b0828f2e9e55fc45ff9180d2618ccdeaf72a796050", size = 519318, upload-time = "2026-06-30T16:13:37.883Z" }, - { url = "https://files.pythonhosted.org/packages/59/55/090e10e62be2f35265e41601337f83ac9f83be9aca1bf92692e3a82effdd/librt-0.12.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:64572c85e4ab7d572c9b72cd76b5f90b21181b1459fa6b1aac6f8958c4fcff31", size = 527127, upload-time = "2026-06-30T16:13:39.682Z" }, - { url = "https://files.pythonhosted.org/packages/1f/34/8052c9ec678be6ba751279947831f089aa69b009000b985ce91d1979669a/librt-0.12.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:8b961912b0e688c1eb4658a46bdb0606b31918d65597fbe7356ca83aa653ffcc", size = 509766, upload-time = "2026-06-30T16:13:41.266Z" }, - { url = "https://files.pythonhosted.org/packages/6f/f8/8761b36189e9ec8dc20b49fa84cef22852c6c41fcda56f760f7fc1360da5/librt-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:722375903e3f079436a7a33da51ce73931536dd041f9feb01536f05d8e010c96", size = 552043, upload-time = "2026-06-30T16:13:43.197Z" }, - { url = "https://files.pythonhosted.org/packages/c8/98/7283971ef6b70269938b49c7b25f670ec6325d252265fbcc996f9b364379/librt-0.12.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:a5a96a8f536b65ef1bf910c09e7e71647edde5111f6e1b51f413c6fba5bfe71b", size = 79472, upload-time = "2026-06-30T16:13:44.64Z" }, - { url = "https://files.pythonhosted.org/packages/c3/5e/b30940dea935e8ac5bd0e0abb1985f5274590d557ac3a252ca0d5392ce52/librt-0.12.0-cp314-cp314-win32.whl", hash = "sha256:8ffc99c356f1777c506e1b69dc303879153ae2640ba15b8f3d4448bc87139149", size = 94246, upload-time = "2026-06-30T16:13:45.962Z" }, - { url = "https://files.pythonhosted.org/packages/7d/4e/0af9fe63f35fa304da3b05688f30ff6a329bcc59581b1cc51dc87fd30141/librt-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:1e68fb20798f455cda41d20a306a23c901218883f17a4bab1ed6e1331b265fb7", size = 114951, upload-time = "2026-06-30T16:13:47.279Z" }, - { url = "https://files.pythonhosted.org/packages/b1/8e/843c495d7db35e13b84cd533898fa89145c40dc255da0bc316d53d631464/librt-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:2df534f97916cf38ec9b1ddafeb68ae1a4cd4a54775ff26a797026774c0517cf", size = 100562, upload-time = "2026-06-30T16:13:48.699Z" }, - { url = "https://files.pythonhosted.org/packages/75/30/c686d0f978d5fd6867c5bbad96b015c9445746764d1c228e16a2d30d9382/librt-0.12.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c09e581b1c2b8a62b809d4f4bd101ca3de93791e5b0ed1a14085d911be3dee3f", size = 153897, upload-time = "2026-06-30T16:13:50.017Z" }, - { url = "https://files.pythonhosted.org/packages/40/46/f6f2d77ce46628b48fb5280709013b5109cf3a2c46a2472093cdfc03519d/librt-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:976888d0d831402086e641018bcc3208e0a38f0835789da91f72894b2cb4161f", size = 156391, upload-time = "2026-06-30T16:13:51.462Z" }, - { url = "https://files.pythonhosted.org/packages/c2/46/cd790c7e19e460779471530ffab454541d6ea4a3b7d338cad7f16ff96995/librt-0.12.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:563c37cdb41d08fe1e3f08b201abac0e317ca18e88b91285466ee0a585797520", size = 564151, upload-time = "2026-06-30T16:13:53.146Z" }, - { url = "https://files.pythonhosted.org/packages/54/12/724559a15fb023cbdef7aee1e81fbfbc3ee22fd09009baa816cea63e3a60/librt-0.12.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:b97eb1a3140e279cc76f85b0fb92b7eb3dfbe0471260ee878bc9dc4bf9a0d649", size = 546002, upload-time = "2026-06-30T16:13:54.665Z" }, - { url = "https://files.pythonhosted.org/packages/4b/7e/f9d8c257ab4909f101c7c13734367749e782fd8625545f0343502c2f09f1/librt-0.12.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:06e0623351ab9904cf628245f99c714586f4dd23dc740b88c8bc670d8401a847", size = 584204, upload-time = "2026-06-30T16:13:56.301Z" }, - { url = "https://files.pythonhosted.org/packages/9b/33/64665810575ac23b6cb6ef364de51309b7803620c12885b6e895ebc29591/librt-0.12.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:da12f017b2e404554be14d466cd992459feaa44f252b0f18d909a85266ce1237", size = 573688, upload-time = "2026-06-30T16:13:58.1Z" }, - { url = "https://files.pythonhosted.org/packages/0f/01/27522995c6627455abc7a939d57535fb1a7836d398ccedb3d7585f46039e/librt-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d97f31003a5c86b9e78155a829572c3a26484064fb7ac1d9695fe628bd93d029", size = 604719, upload-time = "2026-06-30T16:13:59.831Z" }, - { url = "https://files.pythonhosted.org/packages/ee/1f/099e61b1b688551d6d2ce9d4d2ae2242a938759db8551e6cbac7f7176ee5/librt-0.12.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:bd43a6c69876aef4f04eaae3d3b99b0be64755fda274002fa445b92480bf664e", size = 598183, upload-time = "2026-06-30T16:14:01.457Z" }, - { url = "https://files.pythonhosted.org/packages/bf/c1/050400249665503bdd5b83cec518fa7b183b609341c8dcd58161775c4226/librt-0.12.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c01755c72fca1dc6b8d5c2ed228b8e7b2ffe184675c22f0f05ebd8fe188b9250", size = 582559, upload-time = "2026-06-30T16:14:03.29Z" }, - { url = "https://files.pythonhosted.org/packages/da/d1/eef8f0e6722518b65a3d3bcd9309f9f44e208ce5d6728070820f988e7078/librt-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:625ae561d5fa36400856dcc27464400d047bc2d5e3446be88f437b03fefd72e4", size = 626375, upload-time = "2026-06-30T16:14:04.957Z" }, - { url = "https://files.pythonhosted.org/packages/8b/78/f0bb41a6f2bbd3c77bdcc66980dc0d69ca1192a0ecec25377afcc5e6db73/librt-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8d73191883553ee0739741544bf3b00aba2a1224e45d9580b30cbc29e21dc03b", size = 97752, upload-time = "2026-06-30T16:14:06.555Z" }, - { url = "https://files.pythonhosted.org/packages/92/24/e279c27972ab051a070237cfa45728fa51670c3f22f1a4d391711e9f4c31/librt-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e1cbb037324e759f0afa270229731ff0047772667f3cb38ef5df2cabf0175ede", size = 119562, upload-time = "2026-06-30T16:14:07.908Z" }, - { url = "https://files.pythonhosted.org/packages/06/e6/42a475bfca683b0cd5366f6dd06580062b7e567bb8534d225c877c2f14f3/librt-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:bca1472acbd473eff61059b4409f802c5a1bcb4cd0344d06f939df9c4c125d40", size = 104282, upload-time = "2026-06-30T16:14:09.29Z" }, + { url = "https://files.pythonhosted.org/packages/89/2f/ec5241c38e7fa0fe6c26bfc450e78b9489a6c3c08b394b85d2c10e506975/librt-0.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:34e47058fcc69a313293d6dee94216a4f30c929ae6f2476e58c5ba635aa639d5", size = 148654, upload-time = "2026-07-08T12:24:30.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/1a/d651e18d3ee7aa2879322368c4f278bb7ecaa6b90caadfdec4ebfa8389f3/librt-0.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dbdd5b6509d0c2a8fe72cf494c299a61dbd58142a90a4190664ae159e4a7b547", size = 153537, upload-time = "2026-07-08T12:24:31.773Z" }, + { url = "https://files.pythonhosted.org/packages/45/18/10bff2122577246009d9619b6569596daf69b7648812f997ca9ca0426f60/librt-0.13.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e56ea4ee4df77585a6b5c138f6538680886024fa559f5b55bd14b12e98e67b2", size = 494336, upload-time = "2026-07-08T12:24:33.079Z" }, + { url = "https://files.pythonhosted.org/packages/67/69/87dfee871b852970f137fdeae8e2ca356c5ab38e6f21d2a3299535fc3159/librt-0.13.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f1f9cc4d09a46d9cb3c2063ae100629d3f52a6517c3c08c2f4c9828261883929", size = 485393, upload-time = "2026-07-08T12:24:34.324Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d5/625447a8c0441ff5f15f4ac5e1d323fb9d4d256ebfde7a3c8e003f646057/librt-0.13.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f125f5d46b20f89dc5587a55cc416b4ba2a5b2ffda36d048ee120e17598a653a", size = 515382, upload-time = "2026-07-08T12:24:35.575Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d8/1c8c49ea04235960426444deece9092a6b3a9587a850a81bae2335317411/librt-0.13.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2608d3b39f9e0b4a66a130d9150c615cba40a5090d25eeeaa225e0e46de8c0ac", size = 509483, upload-time = "2026-07-08T12:24:36.923Z" }, + { url = "https://files.pythonhosted.org/packages/6f/65/f1760fc48050e215201a03506c32b7270159088d01f64557b53e39e74a45/librt-0.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9fd35e95ab5e45c3901d37110263c7db85a961110f5460588fe37f8c131f88a7", size = 532503, upload-time = "2026-07-08T12:24:38.203Z" }, + { url = "https://files.pythonhosted.org/packages/18/1b/793e281dcf494879eff99f642b63ebc9c7c58694a1c2d1e93362a22c7041/librt-0.13.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5f31b0aa13c9b04370d4da6be1ab7779776b3a075cceb6747a39a4be85fe1e40", size = 537027, upload-time = "2026-07-08T12:24:39.34Z" }, + { url = "https://files.pythonhosted.org/packages/69/45/0801bbb40c9eea795d3dd3ce91c4c5f3fe7d42d23ec4be3e8cb283bcc754/librt-0.13.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0b795f5fc70fbbb787ceaf79bb3a0d627bcc33c53de51741755263ec406b775a", size = 517100, upload-time = "2026-07-08T12:24:40.907Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6c/eb5f514f8e29d4924bc0ff4601dd7b4175557e182e7c0721e84cffa39b8a/librt-0.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:36b306a623aaad96fe4b378692b54f9c0789fccd833b9851753d5fbf6138cfde", size = 558653, upload-time = "2026-07-08T12:24:42.359Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bf/f140100d1b59fe87ff40b5ecbb4e27924335b189a784e230ee465452f6c2/librt-0.13.0-cp310-cp310-win32.whl", hash = "sha256:a3762e75fcac8c9e4dacaaf438bffd9003e2ca2c531b756f3c0035deefa674c8", size = 104402, upload-time = "2026-07-08T12:24:43.668Z" }, + { url = "https://files.pythonhosted.org/packages/22/7c/57e40fef7cfb61869341cb28bdcefe8a950bebcbecca74a397bae14dce4a/librt-0.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:d63bae12a8aeb51380be3438e4dc4bd27354d0f8e19166b2f44e3e94d6f552dc", size = 125002, upload-time = "2026-07-08T12:24:44.793Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/a6498964cfeec270c468cffdc118f69c29b412593610d55fa1327ca51ff4/librt-0.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1b5a7bbff495baedbd9b916c367d66854008f8f3b575908ded477c499dc60082", size = 148029, upload-time = "2026-07-08T12:24:45.961Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/dc86d1bffd8e0c2818bace29d9f7783cfbb8e0673bf3673b5bbd5bbe0420/librt-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34bc7938b9fdf14fe32a406c19c71faf894c5cee7e7474bd0be2f17200b82d14", size = 153036, upload-time = "2026-07-08T12:24:47.257Z" }, + { url = "https://files.pythonhosted.org/packages/29/3f/b923826660f02f286186cd9303d52bb05ced0a13708edc104dc8480920e3/librt-0.13.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f40e56b61b41be5f7dec938cfeffd660668cf4b5e72c78e7bd671d66b7bc2c79", size = 493062, upload-time = "2026-07-08T12:24:48.483Z" }, + { url = "https://files.pythonhosted.org/packages/88/87/6c0980a9c9b1302cb68d108906697b89eceb55889bb1dcf77c109aa56ca5/librt-0.13.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:9c5d02b89de5acd0379a51ec44a89476fb03df6145442e1c8ecd6bee2f91b176", size = 485510, upload-time = "2026-07-08T12:24:49.727Z" }, + { url = "https://files.pythonhosted.org/packages/32/81/795ae3b9df5dd94079fb807e38191855e023e8c6249014ae6bc3f0d9a490/librt-0.13.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7db9a3ff32ef5f7d1703d93831a3316cdf0b537de6a1cc03cc8fdd09b9194e89", size = 515909, upload-time = "2026-07-08T12:24:51.135Z" }, + { url = "https://files.pythonhosted.org/packages/20/e5/182de15abce8907108a6fdb41487de65beb5099b74dc5841b19b099168db/librt-0.13.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3dbb2a31882456cadc7053378e81ad7ed7693db4ac9f98ab5f81ef034aa8ec9f", size = 508620, upload-time = "2026-07-08T12:24:52.358Z" }, + { url = "https://files.pythonhosted.org/packages/32/03/33978d32db76e1f66377e8f78e42a2ca3c162143331677d1f50bbad36cfb/librt-0.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c6014e3c80f9c1fe268ef8b0e0ef113bac672cc032f2f93866e7ddad4f3e663d", size = 530363, upload-time = "2026-07-08T12:24:53.503Z" }, + { url = "https://files.pythonhosted.org/packages/e6/f5/b291fbd2d00f7d8287bcbf67b5aa0c6afed4bc26cef23e079629c47a2c04/librt-0.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:091b60a4d2174fc1ec5c34cdc0b72efb6224753d76b7da61ebeab7a191aec8bd", size = 534209, upload-time = "2026-07-08T12:24:55.138Z" }, + { url = "https://files.pythonhosted.org/packages/3e/03/6f41f17939d191bc21609f220da8509316bc62797f078545fe83be522e78/librt-0.13.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:66cb1138f384a191a6d75f986064841fcfdc0cea98f7bd9c9ab9b38049917588", size = 514254, upload-time = "2026-07-08T12:24:56.276Z" }, + { url = "https://files.pythonhosted.org/packages/af/c2/2e4befa5410a7443019c14abccc94ff619797171f6b72013635fb87f31d7/librt-0.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:17221a7569f8f292aa0014226e48aa25b8c2b08da18088cd230953d0ea0f9cd1", size = 557611, upload-time = "2026-07-08T12:24:57.561Z" }, + { url = "https://files.pythonhosted.org/packages/ab/54/8b69f81448417adbc040a2185f4e2eece1e1994b7dcfaeed4662b30f98a5/librt-0.13.0-cp311-cp311-win32.whl", hash = "sha256:fc67741da44c6eaa90e01eafb586bbba9b51eb5b6ed381ee6f5ae72eb3316d21", size = 104906, upload-time = "2026-07-08T12:24:58.806Z" }, + { url = "https://files.pythonhosted.org/packages/76/5a/f4aaf37b50f2fde12c8c663b83fdd499cdc24f957f19543d7414bfcc9e25/librt-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:cc99dfb62b23c9207c33d0be8a2e2af7a42e21e6ea388b380a0c948c7b88953b", size = 125852, upload-time = "2026-07-08T12:25:00.065Z" }, + { url = "https://files.pythonhosted.org/packages/f2/99/bf1820e6feeabc2f218c24450ec0c995d6a91e8ba0fd3caf042c9e8adb2a/librt-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:40ccd13c252d3fe473ffc8a57be7565abc8b64cf1b108344c859d5164f7f3e0c", size = 111832, upload-time = "2026-07-08T12:25:01.148Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f4/b2933ddae222dac338476abb872641169a5cfed2c2bb5444a5b07b32b0c3/librt-0.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30536798f4504c0fad0885b1d371b0539abb081e4570c9d7c641cb51141b49f0", size = 150990, upload-time = "2026-07-08T12:25:02.42Z" }, + { url = "https://files.pythonhosted.org/packages/90/ef/db98f744ca50e6efc9c95c70ee49b77aefac31f6a3fc7c83754a42d6a74f/librt-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:93d24ebb82aa4420b1409c389e7857bc35bd0b668007ac8172427d5c73cc8cc5", size = 155238, upload-time = "2026-07-08T12:25:03.681Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/a197e7bc72baf2c61ce7fdc6906a5054dc05bd8da0819aa894e4857bf87e/librt-0.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb8a1adce42d8b75485a5d56a9623a50bcab995b6079f1dac59fc44034dd93d9", size = 503073, upload-time = "2026-07-08T12:25:05.049Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e7/7887712e27da7c1ab80fcabb1de6eb24243964f6557cae530d4b70706dbd/librt-0.13.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0763ca2ab66058174f9dee426dc64f5e0a89c24a7df8d3fe3f1836c04e25de4b", size = 496528, upload-time = "2026-07-08T12:25:06.26Z" }, + { url = "https://files.pythonhosted.org/packages/94/f0/f2283385bb6b950b26a1410f4ce51ec27231e0b3a4b925c46366d218b198/librt-0.13.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b222493da6e7b6199db9bd79502436cf5a27da3c1f7fa83c7e285444fc93fd03", size = 531786, upload-time = "2026-07-08T12:25:07.658Z" }, + { url = "https://files.pythonhosted.org/packages/36/11/69ac3b54766ffba5fd7e5acebfb048d66dbe1f9f2d14516c2b3edc59cf87/librt-0.13.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fadc63331f4388c3dc90090448f682a7e9feafc11481391c1e94f2f907a3976e", size = 524393, upload-time = "2026-07-08T12:25:09.121Z" }, + { url = "https://files.pythonhosted.org/packages/61/5f/d72f95fd444a926a3c14b4e24979474116988dd57a45be242077c45d3c22/librt-0.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:70d9c62a4cffd9f23396cd5ef93fc5d11b31596b9b7d6306074abe3d5fcf09bd", size = 543026, upload-time = "2026-07-08T12:25:10.459Z" }, + { url = "https://files.pythonhosted.org/packages/c4/08/dcd9993ad192737a004ba263d549f8ea605b326b952e7d6205c7d4170b76/librt-0.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:66c0e7e6b02a155576df2c77ec933a70b72da726e248c494abf690923e624348", size = 546829, upload-time = "2026-07-08T12:25:11.716Z" }, + { url = "https://files.pythonhosted.org/packages/96/d5/6d9bb2f54e4109a956b7128836529653eb9d740f784bc47ed10a02c1000e/librt-0.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ac04bcd3328eb91d99dfedf6a60d9c1f15d3434e6f6daf922f0420f7d90b85c7", size = 535700, upload-time = "2026-07-08T12:25:13.144Z" }, + { url = "https://files.pythonhosted.org/packages/8c/f2/10946922503858a359492fa27f13e86228bde702116a740ac7b3cd185f24/librt-0.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db327e7271e653c32040b85ae6188059c924b57d7e1e29f935523fa017cd4e82", size = 573566, upload-time = "2026-07-08T12:25:14.336Z" }, + { url = "https://files.pythonhosted.org/packages/48/a8/94f00e3c99479a18088af3685ea016c42f3c7d5d1964d8dbb40c08d7f1aa/librt-0.13.0-cp312-cp312-win32.whl", hash = "sha256:860bd1d8ba48456ce08feaf8d343a8aaeb2fa086f2bcaa2a923fa3f7a3ff9aa3", size = 106099, upload-time = "2026-07-08T12:25:16.159Z" }, + { url = "https://files.pythonhosted.org/packages/c9/7b/2da9c74c1ed25a89cc4e1c8e007ea2eb4a0f1fafa3e70d757fe3242c5c5c/librt-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:e54a315caf843c8d77e388cadc56ea9ded569935ee2d2347d7ea94992e5aa6fa", size = 126934, upload-time = "2026-07-08T12:25:17.275Z" }, + { url = "https://files.pythonhosted.org/packages/d0/65/aead61bbf3b5358593f9d4779d2a0e88eaf6ec191a6342dde36dd1df6371/librt-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:c718e99a0992127af84385378460db624103b559ab260435abcfe77a4e4ed1c1", size = 112236, upload-time = "2026-07-08T12:25:18.425Z" }, + { url = "https://files.pythonhosted.org/packages/67/3b/18e7b63255297a2bdc9c25c8d6d4ca8eca9f63aceb1252c0f7427ac7099e/librt-0.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a468951af16155824e88bdd8326ebe5bdb371f3ec0ac04642994b98201d914f3", size = 151027, upload-time = "2026-07-08T12:25:19.638Z" }, + { url = "https://files.pythonhosted.org/packages/4d/68/e2248452c00d1a03b45fee1752cdc8f790a476efd2402b75181da88a9e61/librt-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ae01d8512cc17079e53425635327dbf3f7ff57a42c00dec348bf79791c56444c", size = 155152, upload-time = "2026-07-08T12:25:20.851Z" }, + { url = "https://files.pythonhosted.org/packages/0e/16/52b1c99bf19057a062aac39c900cbb81499f6f75d6c537c14463d247ba78/librt-0.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32c26893cd085c1efe83219e78d866da23fb20a066101b8f68210004361d224c", size = 502499, upload-time = "2026-07-08T12:25:22.055Z" }, + { url = "https://files.pythonhosted.org/packages/9f/54/b811151805c795f55e0dedee6ec687b75f9982a8105d240ea3910737a77b/librt-0.13.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5929da1981a46bcf4b28b1b9499905f0ff58e2419da402a048234e9783acbc4b", size = 496108, upload-time = "2026-07-08T12:25:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/8f/f8/094d6b2bd93f3fdaa54db54cc788c4a365333bddad65ab02e04da0b1d004/librt-0.13.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94b85d664d777bab6c0d709416cb42938251fda9e221b79e3a2215d85df5f4f9", size = 531576, upload-time = "2026-07-08T12:25:24.648Z" }, + { url = "https://files.pythonhosted.org/packages/2e/40/541733d5755824f968f7ec39d78ffbd75d145964157ae5e69a09ec6d7326/librt-0.13.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:531b2df3e9fe96b1fcf73a6d165921e4656be5f58d631d384ebce344298368db", size = 524390, upload-time = "2026-07-08T12:25:25.898Z" }, + { url = "https://files.pythonhosted.org/packages/c6/b5/255673cfdbf5ba663339d36cd863c897289ab4337577e19f9405ce059f36/librt-0.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:109b84a9edf69ad89dc1f66358659e14a031baca95e3e5b0060bd903ede8efd6", size = 543053, upload-time = "2026-07-08T12:25:27.436Z" }, + { url = "https://files.pythonhosted.org/packages/9e/11/ab5005e9c9850710f21e354201bf090646349d3fabf5f951eaf70235729e/librt-0.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1304368a3e7ffc3e9db986796cc5326fdb5943a3567ecc137cff318e4240c0e7", size = 546387, upload-time = "2026-07-08T12:25:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/a2/04/a5d7ce1d1df1afd15ca283dcdf7530ac073e12d69ae8c40879dda96f7868/librt-0.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e4f9b472e7d308d94b62c801982065661158c6ed02790d6c7ddb4337cea0f9c1", size = 535970, upload-time = "2026-07-08T12:25:30.171Z" }, + { url = "https://files.pythonhosted.org/packages/5a/76/927e267a6daa290174ac281b23c9804c8829b042ade9c6f24a065f540958/librt-0.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f836c37478f167a81200d8c8b2c920a22224564bed2c23d7aeec760965c367a", size = 573582, upload-time = "2026-07-08T12:25:31.507Z" }, + { url = "https://files.pythonhosted.org/packages/10/24/b6c5213efe39c19f9e13605644d0cf063b4ddaa33ac2e45b088e23a70e2e/librt-0.13.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:4000d961ff9598ac6ea603c6c836a5ed49bc205ade5fc378b998dfe1e2c36628", size = 82189, upload-time = "2026-07-08T12:25:32.675Z" }, + { url = "https://files.pythonhosted.org/packages/4c/00/d29736be177a906ac0b84a5b04b4fbfa22c776dc2f366de4172b0f968c08/librt-0.13.0-cp313-cp313-win32.whl", hash = "sha256:79e44cff71750d299d61a678e49995b0d5935a9cda238c2574daeca3ba536927", size = 106193, upload-time = "2026-07-08T12:25:33.692Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ac/aff6fb45393cb8912f39dfb156ef6b2d1cadb207ff465fc8f66141054be8/librt-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:54dab44a847d5ad1acd05c8a83fe518ae685516ecf4d3f7cc6e3df2a66767650", size = 126962, upload-time = "2026-07-08T12:25:34.769Z" }, + { url = "https://files.pythonhosted.org/packages/d9/3a/d68cb2b334d53fd30fac81d3a489ce4ba0d9506f4df43fcf676b68352b19/librt-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:d4cb6fbfdf874340ab5e51450753c0f817b6958a3621125ee695bbc3de866566", size = 112127, upload-time = "2026-07-08T12:25:35.981Z" }, + { url = "https://files.pythonhosted.org/packages/7b/66/f49ae0d592bd45b6941e9a8bafcb6a87cddcd501ee7874707e767f01b585/librt-0.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:25218d94b1d2cbc0ba1d8a3f9dc9af578d9646e5ed16443a70cde1dfdcce6d71", size = 149818, upload-time = "2026-07-08T12:25:37.203Z" }, + { url = "https://files.pythonhosted.org/packages/3d/50/51c76d74014d04fb95b6506d286808984b78a2f7a41039094e6b2194ac48/librt-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f26629539d4893c2957a16c41bb058e1e135c1f150f6a2e25ed047f64cf3f5c6", size = 154071, upload-time = "2026-07-08T12:25:39.399Z" }, + { url = "https://files.pythonhosted.org/packages/b8/fe/f19b0f5f82d5a1f2da736586bc840abd00ce07d6388136ae80b7333883fc/librt-0.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4517d47b2b8af26975a406fba7d314de9696d864252e0257c6ea90238cfe27f", size = 494168, upload-time = "2026-07-08T12:25:40.641Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/b8550c75775127fd31a5f20e8775997f7b527ad661fc8ddccd7497c064f7/librt-0.13.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f19e181de5b3a1148bb3420b8c4b0b0ea0fce6950099724ad151d6cea5acc180", size = 491054, upload-time = "2026-07-08T12:25:41.905Z" }, + { url = "https://files.pythonhosted.org/packages/30/14/4d0204867623df3f33f86efd3d3692ba5e01321443f4d6eab35a22697618/librt-0.13.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22034924f5b42d5a56371cf271771bfeaabf235a7a8b6264bef2d20013f786c6", size = 523006, upload-time = "2026-07-08T12:25:43.327Z" }, + { url = "https://files.pythonhosted.org/packages/19/0a/c45fc9a260934696bace1ac5df1e148ac92bd71767aee3bf7cd7a4534f4c/librt-0.13.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7897db4e95e22468bdda33d8e012ceacd0182abf001e6389d763f0def6286b9", size = 515058, upload-time = "2026-07-08T12:25:44.541Z" }, + { url = "https://files.pythonhosted.org/packages/13/0a/50c5ce45b326854ef8fa6ae4c36cf5142e5c55315eaf9e51d0ae73ac4da3/librt-0.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1ce61b3746545029d4f5c17d6bd74b676254ad98433086c846ffb5e8fa73f007", size = 534025, upload-time = "2026-07-08T12:25:45.825Z" }, + { url = "https://files.pythonhosted.org/packages/89/2d/08c413c8f93fc13b8103624fce38e5caa86cd08cbbc8465870ab287af54b/librt-0.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:46c330e82565962c761dbce7941be2cff7db674ee807455a8d0cadc5f9b759b0", size = 540557, upload-time = "2026-07-08T12:25:47.059Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c1/93af71fb4a364952210051811dd4e40174e79656b050c89cacac18af3330/librt-0.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:375f5af8f99cbaa99dd293af986e3d57caabc9ba81a5d3f021603764854197a1", size = 523201, upload-time = "2026-07-08T12:25:48.392Z" }, + { url = "https://files.pythonhosted.org/packages/c1/6e/9766f07b676a4889d9f8bc2864e9ba5fff165653143ef4dda7df6aa34d16/librt-0.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9320d34c3376ae204b2cd176e8d4883a013934e0aef822f1aed9c536490c275d", size = 565740, upload-time = "2026-07-08T12:25:49.678Z" }, + { url = "https://files.pythonhosted.org/packages/a2/1e/664e3472ce2b6e10e9b83f29d4a36eb982ff6b5a169ae7567bba3a4c4ff5/librt-0.13.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:9af313c66157a69dc69ea0059a66961692250e0dc95af9c385a48ffb770a0d16", size = 81611, upload-time = "2026-07-08T12:25:50.857Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d4/8582a4d65e2234673685e07309d02c230b28a85724eb0acbf13f019b7f6e/librt-0.13.0-cp314-cp314-win32.whl", hash = "sha256:f2a7253458e34f33543551394ae4fe104b497ec2a65ac266074de64c1df82e37", size = 100106, upload-time = "2026-07-08T12:25:52.03Z" }, + { url = "https://files.pythonhosted.org/packages/63/ce/0cb99efe6086b46cd985dc26672166fae312a239690e75871f7fafbd3fc5/librt-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:a3dfe4edf10e8ed7e55b026a8bfc2c2a8704218b659cd4bffdf604fab966dc39", size = 121209, upload-time = "2026-07-08T12:25:53.166Z" }, + { url = "https://files.pythonhosted.org/packages/26/85/4f3ccb083a3c9b0d42e223acdb3c3f507953324a59cdcab4826e8e2e3b89/librt-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:68a5faee4bba381cb93b5961f684a514cf0053cb92308ff9c792c2fea0b174c6", size = 106404, upload-time = "2026-07-08T12:25:54.253Z" }, + { url = "https://files.pythonhosted.org/packages/b2/77/333191499538c8e8189de7a4cba8e6f49ee949fd6d6e6324b21fd1522466/librt-0.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a38fb81d8376dfa2f8963b265fec07637802b0d01e2a127c19c66cb070fb24f5", size = 159231, upload-time = "2026-07-08T12:25:55.432Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9e/2aa83758f22c278b837a1d8025898434ce2b8bff36678d5330ecaef56dff/librt-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d4c8d9bd5abce34b2e75edb3bf37ab0f34e49b1f915a40ae8468eb7c85bc5b46", size = 161300, upload-time = "2026-07-08T12:25:56.585Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c0/86791e936553ca763d6b3c2fb4d31d596cd00e14fa631c283a40ba01559a/librt-0.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:387e2f1d27e89bffe0d3f520f0da0662c973fd607ca16c1808f8a5085419485e", size = 582056, upload-time = "2026-07-08T12:25:58.144Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d3/a9ec15984a185e000c4d2a16ba28bd623124ad4c38a10974c7ff78e3a893/librt-0.13.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:4f6db193d2e5e0ed60359b9a5a682cd67205d0d3b1e459a867dd4b5c4e7eaa7a", size = 562758, upload-time = "2026-07-08T12:25:59.544Z" }, + { url = "https://files.pythonhosted.org/packages/3c/af/dbe36b78b19c06a55097f99305e4ea9458e2273e6ae16a3cbecaad7ee978/librt-0.13.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d38604854e8d22faadf683ec6c02bb0f886e2ba56ef981a1c36ee275f21ea22", size = 602095, upload-time = "2026-07-08T12:26:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a8/2966891b4dd2830f5203fbee92ac2c4947653a2390ba73dfa44244fad025/librt-0.13.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:371f7ce73026815dafd51c50ce38416e91428b28c4b2ec97cd39271164b0045c", size = 593452, upload-time = "2026-07-08T12:26:02.352Z" }, + { url = "https://files.pythonhosted.org/packages/61/f5/4df8bfc8405ecf8c0d525b4d69636f694bdd8620b313ec8b76e54a5926cc/librt-0.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3aaedf52171bee90860704c560bc798fe83b76247df47568e0197e9b13c735a0", size = 623729, upload-time = "2026-07-08T12:26:04.294Z" }, + { url = "https://files.pythonhosted.org/packages/d6/13/9ac202dffc8db06f75d06c08c2f9f6ff054be67d21272dcc078fa1cc0c57/librt-0.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:96bad8725a4f196a798366c25ce075d1f7543a4ec045ffc13e6a7ec095cdab04", size = 617077, upload-time = "2026-07-08T12:26:05.845Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f0/ebe38610716aee5cb28efd95089bb90192096179802779381e1c5dcf239c/librt-0.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:6bf6a559ffe4a93bbea6cf31ddf01a7fd9ba342ef51f27beb178e318b74acd61", size = 599561, upload-time = "2026-07-08T12:26:07.21Z" }, + { url = "https://files.pythonhosted.org/packages/4f/5c/c2e72e236fff7abc716d5b1753b8b8cd3ea85ac46fe17d2e7c51d4e1c723/librt-0.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:301067672387902c55f94b51d5022304b36c966ea9fe1f21caab99a9bef487c9", size = 645511, upload-time = "2026-07-08T12:26:08.562Z" }, + { url = "https://files.pythonhosted.org/packages/0c/99/6203ce619dee940d6bfbe099ec3fe4be00a68e9d60f70abf906cf124fe66/librt-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:5fdcf34f86de8fb66d7dc7589f96ba91c4aa46671200d400e6fd6f109a483f18", size = 104357, upload-time = "2026-07-08T12:26:09.828Z" }, + { url = "https://files.pythonhosted.org/packages/52/dd/843b6314087c41657c7036d7914d8f294bdf9b580aa8513ea0588c8e9a3d/librt-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:260c33e92263fa629b4f6d3c51967a1c2158fe6c33237aaa3ebeac586b085259", size = 126998, upload-time = "2026-07-08T12:26:10.975Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/3dcec2884ba1b0806d1408612555c38dd5d68e90156b59f75f6e36435c3a/librt-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2f281549a4c52ac7bb97997f14353f8bd0e53a34ca0dad1c905cfd0b4a58ae99", size = 110771, upload-time = "2026-07-08T12:26:12.303Z" }, ] [[package]] @@ -825,7 +807,7 @@ wheels = [ [[package]] name = "mkdocstrings" -version = "1.0.4" +version = "1.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jinja2" }, @@ -835,9 +817,9 @@ dependencies = [ { name = "mkdocs-autorefs" }, { name = "pymdown-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1d/5d/f888d4d3eb31359b327bc9b17a212d6ef03fe0b0682fbb3fc2cb849fb12b/mkdocstrings-1.0.4.tar.gz", hash = "sha256:3969a6515b77db65fd097b53c1b7aa4ae840bd71a2ee62a6a3e89503446d7172", size = 100088, upload-time = "2026-04-15T09:16:53.376Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/8c/05934c546eb38321e5f24e786b24c7a0dc5f2b35968f6e04dce9cfbb738d/mkdocstrings-1.0.5.tar.gz", hash = "sha256:3a9528223afd6bf459ea85541b600ef3542826bbeb4ab534553f4b3c4e6e5e48", size = 100318, upload-time = "2026-07-10T10:49:10.026Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl", hash = "sha256:63464b4b29053514f32a1dbbf604e52876d5e638111b0c295ab7ed3cac73ca9b", size = 35560, upload-time = "2026-04-15T09:16:51.436Z" }, + { url = "https://files.pythonhosted.org/packages/a1/82/2a2d44f2c09563d321ab2690835f066267fe000623c90c0caf44e296cbbb/mkdocstrings-1.0.5-py3-none-any.whl", hash = "sha256:150eff6e8c370cc8d2ba4e452ece1275fe83ad34d8cc5884a89a0ed141618c49", size = 35543, upload-time = "2026-07-10T10:49:08.689Z" }, ] [package.optional-dependencies] @@ -918,7 +900,7 @@ wheels = [ [[package]] name = "mypy" -version = "2.1.0" +version = "2.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ast-serialize" }, @@ -928,51 +910,52 @@ dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/82/15/cca9d88503549ed6fedeaa1d448cdddd542ee8a490232d732e278036fbf2/mypy-2.1.0.tar.gz", hash = "sha256:81e76ad12c2d804512e9b13240d1588316531bfba07558286078bfbce9613633", size = 3898359, upload-time = "2026-05-11T18:37:36.237Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/7e/be536678c6ae49ef058aba4b483d8c7bc104f471479016066f345bc1f5f8/mypy-2.2.0.tar.gz", hash = "sha256:2cdd99d48590dce6f6b7f1961eda75386364398fcdaad86923bc0f0231bf9baf", size = 3950939, upload-time = "2026-07-08T01:37:27.335Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/71/d351dca3e9b30da2328ee9d445c88b8388072808ebfbc49eb69d30b67749/mypy-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:11a6beb180257a805961aea9ec591bbd0bd17f1e18d35b8456d57aee5bedfedc", size = 14778792, upload-time = "2026-05-11T18:36:23.605Z" }, - { url = "https://files.pythonhosted.org/packages/2f/45/7d51594b644c17c0bcf74ed8cd5fc33b324276d708e8506f220b70dab9d9/mypy-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ef78c1d306bbf9a8a12f526c44902c9c28dffd6c52c52bf6a72641ce18d3849", size = 13645739, upload-time = "2026-05-11T18:37:22.752Z" }, - { url = "https://files.pythonhosted.org/packages/65/01/455c31b170e9468265074840bf18863a8482a24103fdaabe4e199392aa5f/mypy-2.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c209a90853081ff01d01ee895cafe10f7db1474e0d95beaeef0f6c1db9119bbd", size = 14074199, upload-time = "2026-05-11T18:35:09.292Z" }, - { url = "https://files.pythonhosted.org/packages/41/5a/93093f0b29a9e982deafde698f740a2eb2e05886e79ccf0594c7fd5413a3/mypy-2.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47cebf61abde7c088a4e27718a8b13a81655686b2e9c251f5c0915a802248166", size = 14953128, upload-time = "2026-05-11T18:31:57.678Z" }, - { url = "https://files.pythonhosted.org/packages/7f/2f/a196f5331d96170ad3d28f144d2aba690d4b2911381f68d51e489c7ab82a/mypy-2.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d57a90ae5e872138a425ec328edbc9b235d1934c4377881a33ec05b341acc9a8", size = 15249378, upload-time = "2026-05-11T18:33:00.101Z" }, - { url = "https://files.pythonhosted.org/packages/54/de/94d321cc12da9f71341ac0c270efbed5c725750c7b4c334d957de9a087d9/mypy-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:aea7f7a8a55b459c34275fc468ada6ca7c173a5e43a68f5dbe588a563d8a06b8", size = 11060994, upload-time = "2026-05-11T18:33:18.848Z" }, - { url = "https://files.pythonhosted.org/packages/e1/62/0c27ca55219a7c764a7fb88c7bb2b7b2f9780ade8bbf16bc8ed8400eef6b/mypy-2.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:c989640253f0d76843e9c6c1bbf4bd48c5e85ada61bde4beb37cb3eca035685e", size = 9976743, upload-time = "2026-05-11T18:31:25.554Z" }, - { url = "https://files.pythonhosted.org/packages/0a/a1/639f3024794a2a15899cb90707fe02e044c4412794c39c5769fd3df2e2ef/mypy-2.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a683016b16fe2f572dc04c72be7ee0504ac1605a265d0200f5cea695fb788f41", size = 14691685, upload-time = "2026-05-11T18:33:27.973Z" }, - { url = "https://files.pythonhosted.org/packages/3b/08/9a585dea4325f20d8b80dc78623fa50d1fd2173b710f6237afd6ba6ab39b/mypy-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a293c534adb55271fef24a26da04b855540a8c13cc07bc5917b9fd2c394f2ca", size = 13555165, upload-time = "2026-05-11T18:32:16.107Z" }, - { url = "https://files.pythonhosted.org/packages/81/dc/7c42cc9c6cb01e8eb09961f1f738741d3e9c7e9d5c5b30ec69222625cd5f/mypy-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7406f4d048e71e576f5356d317e5b0a9e666dfd966bd99f9d14ca06e1a341538", size = 13994376, upload-time = "2026-05-11T18:32:39.256Z" }, - { url = "https://files.pythonhosted.org/packages/d4/fa/285946c33bce716e082c11dfeee9ee196eaf1f5042efb3581a31f9f205e4/mypy-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0210d626fc8b31ccc90233754c7bc90e1f43205e85d96387f7db1285b55c398", size = 14864618, upload-time = "2026-05-11T18:34:49.765Z" }, - { url = "https://files.pythonhosted.org/packages/2b/83/82397f48af6c27e295d57979ded8490c9829040152cf7571b2f026aeb9a0/mypy-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3712c20deed54e814eaaa825603bada8ea1c390670a397c95b98405347acc563", size = 15102063, upload-time = "2026-05-11T18:34:05.855Z" }, - { url = "https://files.pythonhosted.org/packages/40/68/b02dec39057b88eb03dc0aa854732e26e8361f34f9d0e20c7614967d1eba/mypy-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fcaa0e479066e31f7cceb6a3bea39cb22b2ff51a6b2f24f193d19179ba17c389", size = 11060564, upload-time = "2026-05-11T18:35:36.494Z" }, - { url = "https://files.pythonhosted.org/packages/cf/a8/ea3dcbef31f99b634f2ee23bb0321cbc8c1b388b76a861eb849f13c347dc/mypy-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:0b1a5260c95aa443083f9ed3592662941951bca3d4ca224a5dc517c38b7cf666", size = 9966983, upload-time = "2026-05-11T18:37:14.139Z" }, - { url = "https://files.pythonhosted.org/packages/95/b1/55861beb5c339b44f9a2ba92df9e2cb1eeb4ae1eee674cdf7772c797778b/mypy-2.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:244358bf1c0da7722230bce60683d52e8e9fd030554926f15b747a84efb5b3af", size = 14874381, upload-time = "2026-05-11T18:37:31.784Z" }, - { url = "https://files.pythonhosted.org/packages/0b/b3/b7f770114b7d0ac92d0f76e8d93c2780844a70488a90e91821927850da86/mypy-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ec7c57657493c7a75534df2751c8ae2cda383c16ecc55d2106c54476b1b16f6", size = 13665501, upload-time = "2026-05-11T18:34:23.063Z" }, - { url = "https://files.pythonhosted.org/packages/b6/f3/8ae2037967e2126689a0c11d99e2b707134a565191e92c60ca2572aec60a/mypy-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8161b6ff4392410023224f0969d17db93e1e154bc3e4ba62598e720723ae211", size = 14045750, upload-time = "2026-05-11T18:31:48.151Z" }, - { url = "https://files.pythonhosted.org/packages/a0/32/615eb5911859e43d054941b0d0a7d06cfa2870eba86529cf385b052b111c/mypy-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf03e12003084a67395184d3eb8cbd6a489dc3655b5664b28c210a9e2403ab0b", size = 15061630, upload-time = "2026-05-11T18:37:06.898Z" }, - { url = "https://files.pythonhosted.org/packages/d4/03/4eafbfff8bfab1b87082741eae6e6a624028c984e6708b73bce2a8570c9d/mypy-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:20509760fd791c51579d573153407d226385ec1f8bcce55d730b354f3336bc22", size = 15288831, upload-time = "2026-05-11T18:31:18.07Z" }, - { url = "https://files.pythonhosted.org/packages/99/ee/919661478e5891a3c96e549c036e467e64563ab85995b10c53c8358e16a3/mypy-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:6753d0c1fdd6b1a23b9e4f283ce80b2153b724adcb2653b20b85a8a28ac6436b", size = 11135228, upload-time = "2026-05-11T18:34:31.23Z" }, - { url = "https://files.pythonhosted.org/packages/24/0a/6a12b9782ca0831a553192f351679f4548abc9d19a7cc93bb7feb02084c7/mypy-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:98ebb6589bb3b6d0c6f0c459d53ca55b8091fbc13d277c4041c885392e8195e8", size = 10040684, upload-time = "2026-05-11T18:36:48.199Z" }, - { url = "https://files.pythonhosted.org/packages/6e/dd/c7191469c777f07689c032a8f7326e393ea34c92d6d76eb7ce5ba57ea66d/mypy-2.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35aac3bb114e03888f535d5eb51b8bafbb3266586b599da1940f9b1be3ec5bd5", size = 14852174, upload-time = "2026-05-11T18:31:38.929Z" }, - { url = "https://files.pythonhosted.org/packages/55/8c/aed55408879043d72bb9135f4d0d19a02b886dd569631e113e3d2706cb8d/mypy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8de55a8c861f2a49331f807be98d90caeceeef520bde13d43a160207f8af613e", size = 13651542, upload-time = "2026-05-11T18:36:04.636Z" }, - { url = "https://files.pythonhosted.org/packages/3a/8e/f371a824b1f1fa8ea6e3dbb8703d232977d572be2329554a3bc4d960302f/mypy-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fdf2941a07434af755837d9880f7d7d25f1dacb1af9dcd4b9b66f2220a3024e", size = 14033929, upload-time = "2026-05-11T18:35:55.742Z" }, - { url = "https://files.pythonhosted.org/packages/94/21/f54be870d6dd53a82c674407e0f8eed7174b05ec78d42e5abd7b42e84fd5/mypy-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e195b817c13f02352a9c124301f9f30f078405444679b6753c1b96b6eed37285", size = 15039200, upload-time = "2026-05-11T18:33:10.281Z" }, - { url = "https://files.pythonhosted.org/packages/17/99/bf21748626a40ce59fd29a39386ab46afec88b7bd2f0fa6c3a97c995523f/mypy-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5431d42af987ebd92ba2f71d45c85ed41d8e6ca9f5fd209a69f68f707d2469e5", size = 15272690, upload-time = "2026-05-11T18:32:07.205Z" }, - { url = "https://files.pythonhosted.org/packages/d6/d7/9e90d2cf47100bea550ed2bc7b0d4de3a62181d84d5e37da0003e8462637/mypy-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:767fe8c66dc3e01e19e1737d4c38ebefead16125e1b8e58ad421903b376f5c65", size = 11147435, upload-time = "2026-05-11T18:33:56.477Z" }, - { url = "https://files.pythonhosted.org/packages/ec/46/e5c449e858798e35ffc90946282a27c62a77be743fe17480e4977374eb91/mypy-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:ecfe70d43775ab99562ab128ce49854a362044c9f894961f68f898c23cb7429d", size = 10035052, upload-time = "2026-05-11T18:32:30.049Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ca/b279a672e874aedd5498ae25f722dacc8aa86bbffb939b3f97cbb1cf6686/mypy-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7354c5a7f69d9345c3d6e69921d57088eea3ddeeb6b20d34c1b3855b02c36ec2", size = 14848422, upload-time = "2026-05-11T18:35:45.984Z" }, - { url = "https://files.pythonhosted.org/packages/27/e6/3efe56c631d959b9b4454e208b0ac4b7f4f58b404c89f8bec7b49efdfc21/mypy-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:49890d4f76ac9e06ec117f9e09f3174da70a620a0c300953d8595c926e80947f", size = 13677374, upload-time = "2026-05-11T18:36:57.188Z" }, - { url = "https://files.pythonhosted.org/packages/84/7f/8107ea87a44fd1f1b59882442f033c9c3488c127201b1d1d15f1cbd6022e/mypy-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:761be68e023ef5d94678772396a8af1220030f80837a3afd8d0aef3b419666f4", size = 14055743, upload-time = "2026-05-11T18:35:18.361Z" }, - { url = "https://files.pythonhosted.org/packages/51/4d/b6d34db183133b83761b9199a82d31557cdbb70a380d8c3b3438e11882a3/mypy-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c90345fc182dc363b891350457ec69c35140858538f38b4540845afcc32b1aef", size = 15020937, upload-time = "2026-05-11T18:34:59.618Z" }, - { url = "https://files.pythonhosted.org/packages/ff/d7/f08360c691d758acb02f45022c34d98b92892f4ea756644e1000d4b9f3d8/mypy-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b84802e7b5a6daf1f5e15bc9fcd7ddae77be13981ffab037f1c67bb84d67d135", size = 15253371, upload-time = "2026-05-11T18:36:41.081Z" }, - { url = "https://files.pythonhosted.org/packages/67/1b/09460a13719530a19bce27bd3bc8449e83569dd2ba7faf51c9c3c30c0b61/mypy-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:022c771234936ceac541ebaf836fe9e2abeb3f5e09aff21588fe543ff006fe21", size = 11326429, upload-time = "2026-05-11T18:34:13.526Z" }, - { url = "https://files.pythonhosted.org/packages/40/62/75dbf0f82f7b6680340efc614af29dd0b3c17b8a4f1cd09b8bd2fd6bc814/mypy-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:498207db725cec88829a6a5c2fc771205fd043719ef98bc49aba8fb9fc4e6d57", size = 10218799, upload-time = "2026-05-11T18:32:23.491Z" }, - { url = "https://files.pythonhosted.org/packages/b2/66/caca04ed7d972fb6eb6dd1ccd6df1de5c38fae8c5b3dc1c4e8e0d85ee6b9/mypy-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d5e5cad0efeba72b93cd17490cc0d69c5ac9ca132994fe3fb0314808aeeb83e", size = 15923458, upload-time = "2026-05-11T18:35:28.64Z" }, - { url = "https://files.pythonhosted.org/packages/ed/52/2d90cbe49d014b13ed7ff337930c30bad35893fe38a1e4641e756bb62191/mypy-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff715050c127d724fd260a2e666e7747fdd83511c0c47d449d98238970aef780", size = 14757697, upload-time = "2026-05-11T18:36:14.208Z" }, - { url = "https://files.pythonhosted.org/packages/ac/37/d98f4a14e081b238992d0ed96b6d39c7cc0148c9699eb71eaa68629665ea/mypy-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82208da9e09414d520e912d3e462d454854bed0810b71540bb016dcbca7308fd", size = 15405638, upload-time = "2026-05-11T18:33:48.249Z" }, - { url = "https://files.pythonhosted.org/packages/a3/c2/15c46613b24a84fad2aea1248bf9619b99c2767ae9071fe224c179a0b7d4/mypy-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e79ebc1b904b84f0310dff7469655a9c36c7a68bddb37bdd42b67a332df61d08", size = 16215852, upload-time = "2026-05-11T18:32:50.296Z" }, - { url = "https://files.pythonhosted.org/packages/5c/90/9c16a57f482c76d25f6379762b56bbf65c711d8158cf271fb2802cfb0640/mypy-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e583edc957cfb0deb142079162ae826f58449b116c1d442f2d91c69d9fced081", size = 16452695, upload-time = "2026-05-11T18:33:38.182Z" }, - { url = "https://files.pythonhosted.org/packages/0f/4c/215a4eeb63cacc5f17f516691ea7285d11e249802b942476bff15922a314/mypy-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b33b6cd332695bba180d55e717a79d3038e479a2c49cc5eb3d53603409b9a5d7", size = 12866622, upload-time = "2026-05-11T18:34:39.945Z" }, - { url = "https://files.pythonhosted.org/packages/4b/50/1043e1db5f455ffe4c9ab22747cd8ca2bc492b1e4f4e21b130a44ee2b217/mypy-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4f910fe825376a7b66ef7ca8c98e5a149e8cd64c19ae71d84047a74ee060d4e6", size = 10610798, upload-time = "2026-05-11T18:36:31.444Z" }, - { url = "https://files.pythonhosted.org/packages/0d/2a/13ca1f292f6db1b98ff495ef3467736b331621c5917cad984b7043e7348d/mypy-2.1.0-py3-none-any.whl", hash = "sha256:a663814603a5c563fb87a4f96fb473eeb30d1f5a4885afcf44f9db000a366289", size = 2693302, upload-time = "2026-05-11T18:31:29.246Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f1/a818c375094e20a74ec51444cce6979cedd61d476f860c2f084ea8653bd2/mypy-2.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:97d21f3a1582ef30d0b77f9ca3ad88b4263f3c5aea9e7380cb5d7175160aec7e", size = 14903842, upload-time = "2026-07-08T01:33:27.944Z" }, + { url = "https://files.pythonhosted.org/packages/33/15/f332590488a8e4bb4a24036484c8bc750fe01fbef09701451a8790ac4c44/mypy-2.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2529017942b951cc43db2e2712b71cc3aa7e9d90567630c03ed63d430aeec61a", size = 13902986, upload-time = "2026-07-08T01:36:59.24Z" }, + { url = "https://files.pythonhosted.org/packages/38/9f/7ae37860922d264b536790a3307caa195eb8abb733f2e81608229bbb0f07/mypy-2.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0823d7f273f3b4a49df7573eac271ddbf77a0e5f53e24d294a4f0f77ad22e1d7", size = 14132163, upload-time = "2026-07-08T01:34:16.111Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6d/25c31c85a793340a4a9be593f8fe2cea08fd7b615f8b602b3f52f2d822f6/mypy-2.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d78a0fc90704894bc9948d78affad14b882b08183a7c30412d185748aaa9418b", size = 15070110, upload-time = "2026-07-08T01:34:44.933Z" }, + { url = "https://files.pythonhosted.org/packages/cb/67/7adf1caa143f6a0d9c564de24e0c1ed48340185110c3b918adc2f447192b/mypy-2.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6efab25fa3568569fda06928743382900860c48be9200c1758ef5ce94b532714", size = 15371554, upload-time = "2026-07-08T01:34:27.688Z" }, + { url = "https://files.pythonhosted.org/packages/49/cb/ceca928213076f540d9b068c3655c52d43549f124c1f6dece5ecb747bbeb/mypy-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:daeaf5287091d68f674c91416cabab0f80f36e347ed17fded38d7ebde682e9d1", size = 11063618, upload-time = "2026-07-08T01:36:13.235Z" }, + { url = "https://files.pythonhosted.org/packages/41/24/08caa177a4f2e0c44b96d5ad85f2754761b45bb6d65841451ef5dc9edb86/mypy-2.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:f8d97940a0259e09c219903579720b2df12170c0c3a3b3799837c1fb63deb44e", size = 10070720, upload-time = "2026-07-08T01:37:13.251Z" }, + { url = "https://files.pythonhosted.org/packages/96/2e/1ea8028583fef6561d146b51d738d91268201313ecf69e603e808a740e74/mypy-2.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea6fe1a30f3e7dc574d641497ffead2428046b0f8bc5804d13b4e4392fdc317b", size = 14739569, upload-time = "2026-07-08T01:35:33.202Z" }, + { url = "https://files.pythonhosted.org/packages/ba/67/c0607da57d78358b3b4fbfa90ee8ca5c6bd1dbb997c9648904ad4d8861d8/mypy-2.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6965829a6d847a0925f51149472bbeb7a39332fb4801972fdfd9cedd9f8d43a4", size = 13821442, upload-time = "2026-07-08T01:33:45.991Z" }, + { url = "https://files.pythonhosted.org/packages/bb/56/0407007d4ec7a762bac20724af1f0a57a9d860186c39e73105b6b8396fa2/mypy-2.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a906bfc9c4c5de3ece6dc4726f8076d56ce9be1720baf0c6f84e926c10262a4", size = 14049813, upload-time = "2026-07-08T01:35:44.282Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d1/d718e006c8fed4bece7a1bebeea6dcd05f31433af552fc45a82fef426379/mypy-2.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91df92625cb3452758f27f4965b000fb05ad89b00c282cc3430a7bd6b0e5389e", size = 14978473, upload-time = "2026-07-08T01:36:54.064Z" }, + { url = "https://files.pythonhosted.org/packages/82/03/dae7299c45a84efb1590875f92652a5beb2da98a2cff9a567995e2583fe4/mypy-2.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d42893d15894fd34f395090e2d78315ba7c637d5ee221683e02893f578c2f548", size = 15224649, upload-time = "2026-07-08T01:35:15.988Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8e/738e4e73d030f20852a81383c74319ca4a9a4fdaadc3a75823588fe2c833/mypy-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3abaeec02cdc72e30a147d99eb81852b6b745a2c8d19607e25241d79b1abbce", size = 11049851, upload-time = "2026-07-08T01:34:39.31Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/6c58caedb3fa5831a9b179687cd7dc252c66a61dca4800e5ba19c8eff82b/mypy-2.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:c39bdb7ceab252d15011e26d3a254b4aaa3bbf121b551febfa301df9b0c69abe", size = 10060307, upload-time = "2026-07-08T01:33:21.852Z" }, + { url = "https://files.pythonhosted.org/packages/d1/be/fbaba7b0ee89874fb11668416dec9e5585c190b676b0796cff26a9290fe8/mypy-2.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:484a2be712b245ac6e89847141f1f50c612b0a924aa25917e63e6cfcf4da07cd", size = 14928025, upload-time = "2026-07-08T01:37:24.376Z" }, + { url = "https://files.pythonhosted.org/packages/c5/8f/f79a7c5a76671b0f563d4beaa7d99fe90df4500d2c1d2ba1be0432121bcf/mypy-2.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fb0a020dc68480d40e484675558ed637140df1ccbf896a81ba68bca85f2b50a0", size = 13793027, upload-time = "2026-07-08T01:34:33.937Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b9/3db0086bab611d34e26061b86189e6f71de6d22a9b81699a93b006eabcf6/mypy-2.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc361340732ce7108fa0308812caf02bb6868f16112f1efd35bcad88badf3327", size = 14108322, upload-time = "2026-07-08T01:36:08.316Z" }, + { url = "https://files.pythonhosted.org/packages/58/29/4f1e13979a848de2a0fd385462354b58358b6e8b3d9661663e308f6e3d5d/mypy-2.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b0179a3a0b833f724a65f22613607cf7ea941ab17ec34fa283f8d6dfe21d9fa9", size = 15190198, upload-time = "2026-07-08T01:36:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/14/f7/7759f6294d9d25d86671957d0974a215a2a24d429526e26a2f603de951c5/mypy-2.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9e0899b13da1e4ba44b880550f247402ce90ffecc71c54b220bcbe7ecb34f394", size = 15424222, upload-time = "2026-07-08T01:33:39.398Z" }, + { url = "https://files.pythonhosted.org/packages/d7/1b/05b212bef4d2234b5f0b551ea53ce0680d8075b2e79861c765f70b590945/mypy-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:511320b17467402e2906130e185abffffa3d7648aff1444fc2abb61f4c8a087d", size = 11135191, upload-time = "2026-07-08T01:35:03.019Z" }, + { url = "https://files.pythonhosted.org/packages/92/51/495e7122f6589948b36d3820a046461906756a0eb1b6dedc13ebfec7815e/mypy-2.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:7589f370b33dcdd95708f5340f13a67c2c49140957f934b42ef63064d343cca0", size = 10132502, upload-time = "2026-07-08T01:34:04.508Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5f/2d7a9ac5646274cd6e77ce3abcc2a9ece760c2b21f4c4b9f301711e07855/mypy-2.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6968f27347ef539c443ddfd6897e79db525ddb8c856aa8fbf14c34f310ca5193", size = 14931618, upload-time = "2026-07-08T01:33:51.702Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8a/1adaa7caaa104f87021b1ac71252d62e646e9b623d77900ac7a0ae252bf3/mypy-2.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3df226d2a0ae2c3b03845af217800a68e2965d4b14914c99b78d3a2c8ae23299", size = 13857718, upload-time = "2026-07-08T01:35:27.555Z" }, + { url = "https://files.pythonhosted.org/packages/1d/15/b11586b5aebbb82213e297fc30a6fcf3bed6a9deea3739cd8dd87621f3fd/mypy-2.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc53553996aca2094216ad9306a6f06c5265d206c1bcb54dd367560bd3557825", size = 14059704, upload-time = "2026-07-08T01:33:57.363Z" }, + { url = "https://files.pythonhosted.org/packages/03/db/071e05ab442596bdf7a845e830d5ef7128a0175281038245b171a6b16873/mypy-2.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:996abf2f0bebf572556c60720e8dc0cf5292b64060fa68d7f2bc9caa48e01b6f", size = 15128719, upload-time = "2026-07-08T01:33:33.855Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1c/c4b84eafb85ee315da72471523cc1bf7d7c42164085c42333601da7a8817/mypy-2.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ec287c2381898c652bf8ff79448627fe1a9ee76d22005181fac7315a485c7108", size = 15378692, upload-time = "2026-07-08T01:34:10.468Z" }, + { url = "https://files.pythonhosted.org/packages/68/a4/59a0ee94877fdfe2958cec9b6add72a75393063c79cb60ab4026dd5e10c2/mypy-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:c2c967a7685fa93fcf1a778b3ebe76e756b28ba14655f539d7b61ff3da69352a", size = 11150911, upload-time = "2026-07-08T01:35:21.603Z" }, + { url = "https://files.pythonhosted.org/packages/90/e4/6a9144be50180ed43d8c92de9b03dff504daa92b5bcc0353e8960799a23b/mypy-2.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:d59a4b80351ec92e5f7415fcdd008bd77fcbefc7adf9cbc7ffe4eb9f71617734", size = 10125389, upload-time = "2026-07-08T01:36:36.164Z" }, + { url = "https://files.pythonhosted.org/packages/73/32/0aa8d8d197023ca6040f7b25a486cb47037b6350b0d3bae657c8f85fb43f/mypy-2.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1f6c3d76853071409ac58fc0aadfb276a22af5f190fdaa02152a858088a39ebd", size = 14926083, upload-time = "2026-07-08T01:36:02.365Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7c/35bbe0cb10e6699f90e988e537aaf4282a6c16e37f58848a242eb0a98bde/mypy-2.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bdaa177e80cc3292824d4ef3670b5b58771ee8d57c290e0c9c89e7968212332c", size = 13879985, upload-time = "2026-07-08T01:36:31.093Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0c/1597fbebd873e9b63452317740ae3dd32692cec5da180cc65acd96cd28cf/mypy-2.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80923e6d6e7878291f537ee11052f974954c20cb569798429a5dc265eb780b47", size = 14076883, upload-time = "2026-07-08T01:34:21.789Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/2ec021a83ec01b5d522639f78d8b36adade7fa4821db0f48fd6d82e861f3/mypy-2.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f24bd465a09077c8d64be8f19a6646db467a55490fd315fe7871afe6bb9645", size = 15103567, upload-time = "2026-07-08T01:36:47.717Z" }, + { url = "https://files.pythonhosted.org/packages/53/3a/8cb3529f6d6800c7d069935e5c83a05d80263847b8a947cf6b0b16a9e958/mypy-2.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:db34595464869f474708e769413d1d739fc33a69850f253757b9a4cc20bc1fec", size = 15354641, upload-time = "2026-07-08T01:36:41.592Z" }, + { url = "https://files.pythonhosted.org/packages/d6/4d/320bc9a9553f8a9db5e847ec5ded762ef7ed7403c76c4ba2e8181c80e2f0/mypy-2.2.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:b48092132c7b0ef4322773fecae62fc5b0bc339be348badeec8af502122a4a51", size = 7694355, upload-time = "2026-07-08T01:37:03.291Z" }, + { url = "https://files.pythonhosted.org/packages/90/05/bf3b349e2f885cd3aab488111bb9049439c28bc028dac5073350d3df8fbe/mypy-2.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:6fc0e98b95e31755ca06d89f75fafa7820fbb3ea2caace6d83cba17625cd0acb", size = 11329146, upload-time = "2026-07-08T01:35:38.318Z" }, + { url = "https://files.pythonhosted.org/packages/41/a5/558b06e6cfe17ab88bb38f7b370b6bc68a74ba177c9e138db9748e422d2d/mypy-2.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:bc73a5b4d40e8a3e6b12ef82eb0c90430964e34016a36c2aff4e3bfe37ba41f0", size = 10316586, upload-time = "2026-07-08T01:36:25.458Z" }, + { url = "https://files.pythonhosted.org/packages/0b/21/f0b96f19a9b8ba111a45ffbe9508e818b7f6990469b38f6888943f7bfd3a/mypy-2.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6257bd4b4c0ae2148548b869a1ff3758e38645b92c8fe65eca401866c3c551c1", size = 15922565, upload-time = "2026-07-08T01:35:56.282Z" }, + { url = "https://files.pythonhosted.org/packages/18/f2/1dbcb20b0865d5e992541450a8c73f2fcc90f8bd7d8a4b81313e16934870/mypy-2.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dfa22b3ae862ac1ce76f5976ddd402651b5f090bcfd49c6d0484b8983a29eaf8", size = 14816515, upload-time = "2026-07-08T01:34:58.167Z" }, + { url = "https://files.pythonhosted.org/packages/84/a2/18cce9c7d5b4d14010d1f13836da11b234dda917b17ca8671fc32c136997/mypy-2.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30a430bf26fe8cf372f3933fbd83e633d6561868803645a20e4e6d4523f52a3b", size = 15272246, upload-time = "2026-07-08T01:37:08.72Z" }, + { url = "https://files.pythonhosted.org/packages/ce/71/24d720c7924829bd675cbde2d0fa779f50abf676ca617f53d6a8bfef5fa7/mypy-2.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d58655a60e823b1a4c9ebcda072897fb0193c2f3e6f8e7c433e152aa4cb00233", size = 16226295, upload-time = "2026-07-08T01:34:51.861Z" }, + { url = "https://files.pythonhosted.org/packages/b7/5e/785730990fc863ad8340b4ab44ac4ca23270aecff92c180ccdf27f9f5869/mypy-2.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d4452c955caf14e28bb046cbd0c3671272e6381630a8b81b0da9713558148890", size = 16493275, upload-time = "2026-07-08T01:35:09.337Z" }, + { url = "https://files.pythonhosted.org/packages/93/33/55b1edf16f639f153972380d6977b81f65509c5b8f9c86b58b94b7990b03/mypy-2.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:68f5b7f7f755200f68c7181e3dfb28be9858162257690e539759c9f57721e388", size = 11749038, upload-time = "2026-07-08T01:35:50.071Z" }, + { url = "https://files.pythonhosted.org/packages/61/36/67424748a4e65e97f0e05bf00df379dfb6c2d817f82cc3a4ce5c96d99beb/mypy-2.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:78d201accfafce3801d978f2b8dbbd473a9ce364cc0a0dfd9192fe47d977e129", size = 10704254, upload-time = "2026-07-08T01:37:17.971Z" }, + { url = "https://files.pythonhosted.org/packages/28/cb/142c2097ca02c0d295b00625ff946808bdda65acda17d163c680d8a6a474/mypy-2.2.0-py3-none-any.whl", hash = "sha256:ecc138da861e932d1344214da4bae866b21900a9c2778824b51fe2fb47f5180e", size = 2726094, upload-time = "2026-07-08T01:34:00.075Z" }, ] [[package]] @@ -1401,27 +1384,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.20" +version = "0.15.21" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/dc/35b341fc554ba02f217fc10da57d1a75168cfbcf75b0ef2202176d4c4f2d/ruff-0.15.20.tar.gz", hash = "sha256:1416eb04349192646b54de98f146c4f59afe37d0decfc02c3cbbf396f3a28566", size = 4755489, upload-time = "2026-06-25T17:20:37.578Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/36/6f65aa9989acdec45d417192d8f4e7921931d8a6cf87ac74bce3eed98a8e/ruff-0.15.21.tar.gz", hash = "sha256:d0cfc841c572283c36548f82664a54ce6565567f1b0d5b4cf2caac693d8b7500", size = 4769401, upload-time = "2026-07-09T20:01:34.005Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/d9/2d5014f0253ba541d2061d9fa7193f48e941c8b21bb88a7ff9bbe0bd0596/ruff-0.15.20-py3-none-linux_armv6l.whl", hash = "sha256:00e188c53e499c3c1637f73c91dcf2fb56d576cab76ce1be50a27c4e80e37078", size = 10839665, upload-time = "2026-06-25T17:19:44.702Z" }, - { url = "https://files.pythonhosted.org/packages/c6/d3/ac1798ba64f670698867fcfc591d50e7e421bef137db564858f619a30fcf/ruff-0.15.20-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9ebd1fd9b9c95fc0bd7b2761aebec1f030013d2e193a2901b224af68fe47251b", size = 11208649, upload-time = "2026-06-25T17:19:48.787Z" }, - { url = "https://files.pythonhosted.org/packages/47/47/d3ac899991202095dfcf3d5176be4272642be3cf981a2f1a30f72a2afb95/ruff-0.15.20-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5b16cdd67ca108185cd36dce98c576350c03b1660a751de725fb049193a0632", size = 10622638, upload-time = "2026-06-25T17:19:51.354Z" }, - { url = "https://files.pythonhosted.org/packages/33/13/4e043fe30aa94d4ff5213a9881fc296d12960f5971b234a5263fdc225312/ruff-0.15.20-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3413bb3c3d2ca6a8208f1f4809cd2dca3c6de6d0b491c0e70847672bde6e6efd", size = 10984227, upload-time = "2026-06-25T17:19:54.044Z" }, - { url = "https://files.pythonhosted.org/packages/76/e6/92e7bf40388bc5800073b96564f56264f7e48bfd1a498f5ced6ae6d5a769/ruff-0.15.20-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd7ec42b3bb3da066488db093308a69c4ac5ee6d2af333a86ba6e2eb2e7dd44b", size = 10622882, upload-time = "2026-06-25T17:19:57.037Z" }, - { url = "https://files.pythonhosted.org/packages/13/7a/43460be3f24495a3aa46d4b16873e2c4941b3b5f0b00cf88c03b7b94b339/ruff-0.15.20-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1a36ad0eb77fba9aabfb69ede54de6f376d04ac18ebea022847046d340a8267", size = 11474808, upload-time = "2026-06-25T17:20:00.357Z" }, - { url = "https://files.pythonhosted.org/packages/27/a0/f37077884873221c6b33b4ab49eb18f9f88e54a16a25a5bca59bef46dd66/ruff-0.15.20-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6df3b1e4610432f0386dba04d853b5f08cbbc903410c6fcc02f620f05aff53c", size = 12293094, upload-time = "2026-06-25T17:20:03.446Z" }, - { url = "https://files.pythonhosted.org/packages/a6/74/165545b60256a9704c21ac0ec4a0d07933b320812f9584836c9f4aca4292/ruff-0.15.20-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e89f198a1ea6ef0d727c1cf16088bc91a6cb0ab947dedc966715691647186eae", size = 11526176, upload-time = "2026-06-25T17:20:06.301Z" }, - { url = "https://files.pythonhosted.org/packages/86/b1/a976a136d40ade83ce743578399865f57001003a409acadc0ecbb3051082/ruff-0.15.20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309809086c2acb67624950a3c8133e80f32d0d3e27106c0cd60ff26657c9f24b", size = 11520767, upload-time = "2026-06-25T17:20:09.191Z" }, - { url = "https://files.pythonhosted.org/packages/19/0f/f032696cb01c9b54c0263fa393474d7758f1cdc021a01b04e3cbc2500999/ruff-0.15.20-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2d2374caa2f2c2f9e2b7da0a50802cfb8b79f55a9b5e49379f564544fbf56487", size = 11500132, upload-time = "2026-06-25T17:20:13.602Z" }, - { url = "https://files.pythonhosted.org/packages/4b/f4/51b1a14bc69e8c224b15dab9cce8e99b425e0455d462caa2b3c9be2b6a8e/ruff-0.15.20-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a1ed17b65293e0c2f22fc387bc13198a5de94bf4429589b0ff6946b0feaf21a3", size = 10943828, upload-time = "2026-06-25T17:20:16.635Z" }, - { url = "https://files.pythonhosted.org/packages/71/4b/fe267640783cd02bf6c5cc290b1df1051be2ec294c678b5c15fe19e52343/ruff-0.15.20-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f701305e66b38ea6c91882490eb73459796808e4c6362a1b765255e0cdcd4053", size = 10645418, upload-time = "2026-06-25T17:20:19.4Z" }, - { url = "https://files.pythonhosted.org/packages/b0/c0/a65aa4ec2f5e87a1df32dc3ec1fede434fe3dfd5cbcf3b503cafc676ab54/ruff-0.15.20-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b9c0c367ad8e5d0d5b5b8537864c469a0a0e55417aadfbeca41fa61333be9f4", size = 11211770, upload-time = "2026-06-25T17:20:22.033Z" }, - { url = "https://files.pythonhosted.org/packages/5a/a4/0caa331d954ae2723d729d351c989cb4ca8b6077d5c6c2cb6de75e98c041/ruff-0.15.20-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:01cc00dd58f0df339d0e902219dd53990ea99996a0344e5d9cc8d45d5307e460", size = 11618698, upload-time = "2026-06-25T17:20:25.259Z" }, - { url = "https://files.pythonhosted.org/packages/10/9b/5f14927848d2fd4aa891fd88d883788c5a7baba561c7874732364045708c/ruff-0.15.20-py3-none-win32.whl", hash = "sha256:ed65ef510e43a137207e0f01cfcf998aeddb1aeeda5c9d35023e910284d7cf21", size = 10857322, upload-time = "2026-06-25T17:20:28.612Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f0/fe47c501f9dea92a26d788ff98bb5d92ed4cb4c88792c5c88af6b697dc8e/ruff-0.15.20-py3-none-win_amd64.whl", hash = "sha256:a525c81c70fb0380344dd1d8745d8cc1c890b7fc94a58d5a07bd8eb9557b8415", size = 11993274, upload-time = "2026-06-25T17:20:31.871Z" }, - { url = "https://files.pythonhosted.org/packages/d7/2b/9555445e1201d92b3195f45cdb153a0b68f24e0a4273f6e3d5ab46e212bb/ruff-0.15.20-py3-none-win_arm64.whl", hash = "sha256:2f5b2a6d614e8700388806a14996c40fab2c47b819ef57d790a34878858ed9ca", size = 11343498, upload-time = "2026-06-25T17:20:35.03Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c6/ede15cac6839f3dbce52565c8f5164a8210e669c7bc4decb03e5bdf47d0d/ruff-0.15.21-py3-none-linux_armv6l.whl", hash = "sha256:63ea0e965e5d73c90e95b2434beeafc70820536717f561b32ab6e777cb9bdf5d", size = 10854342, upload-time = "2026-07-09T20:00:53.998Z" }, + { url = "https://files.pythonhosted.org/packages/28/9d/d825b07ee7ea9e2d61df92a860033c94e06e7300d50a1c2653aac27d24fe/ruff-0.15.21-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0f212c5d7d54c01bbfe6dcab02b724a39300f3e34ed7acbe995ccb320a2c58bd", size = 11139539, upload-time = "2026-07-09T20:00:57.809Z" }, + { url = "https://files.pythonhosted.org/packages/f5/de/3b107712e642f063c7a9e0887c427b22cb44097de5aab36c05f2e280670c/ruff-0.15.21-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e6312e41bc96791299614995ea3a977c5857c3b5662b1ecef6755b02b87cb646", size = 10595437, upload-time = "2026-07-09T20:01:00.006Z" }, + { url = "https://files.pythonhosted.org/packages/9a/6f/b4523cc90ba239ede441447a19d0c968846a3012e5a0b0c5b62831a3d5e3/ruff-0.15.21-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01d65b4831c6b2a4ba8ee6faa84049d44d982b7a706e622c4094c509e51673be", size = 10990053, upload-time = "2026-07-09T20:01:02.187Z" }, + { url = "https://files.pythonhosted.org/packages/92/cc/c6a9872a5375f0628875481cf2f66b13d7d865bf3ca2e57f91c7e762d976/ruff-0.15.21-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c5a913a589120ce67933d5d05fd6ddbcc2481c6a054980ee767f7414c72b4fd", size = 10666096, upload-time = "2026-07-09T20:01:04.299Z" }, + { url = "https://files.pythonhosted.org/packages/ab/97/c621f7a17e097f1790fa3af6374138823b330b2d03fc38337945daca212c/ruff-0.15.21-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef04b681d02ad4dc9620f00f83ac5c22f652d0e9a9cfe431d219b16ad5ccc41", size = 11537011, upload-time = "2026-07-09T20:01:06.771Z" }, + { url = "https://files.pythonhosted.org/packages/ea/51/d928727e476e25ccc57c6f449ffd80241a651a973ad949d39cfb2a771d28/ruff-0.15.21-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16d090c0740916594157e75b80d666eab8e78083b39b3b0e1d698f4670a17b86", size = 12347101, upload-time = "2026-07-09T20:01:08.859Z" }, + { url = "https://files.pythonhosted.org/packages/1e/88/8cd62026802b16018ad06931d87997cf795ba2a6239ab659606c87d96bf0/ruff-0.15.21-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a10e74757dd65004d779b73e2f3c5210156d9980b41224d50d2ebcf1db51e67", size = 11572001, upload-time = "2026-07-09T20:01:11.092Z" }, + { url = "https://files.pythonhosted.org/packages/b2/97/f63084cf55444fc110e8cb985ebfcc592af47f597d44453d778cb81bc156/ruff-0.15.21-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bab0905d2f29e0d9fbc3c373ed23db0095edaa3f71f1f4f519ec15134d9e85c8", size = 11549239, upload-time = "2026-07-09T20:01:13.27Z" }, + { url = "https://files.pythonhosted.org/packages/9d/77/f107da4a2874b7715914b03f09ba9c54424de3ff8a1cc5d015d3ee2ce0ac/ruff-0.15.21-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:00eca240af5789fec6fe7df74c088cc1f9644ed83027113468efba7c92b94075", size = 11535340, upload-time = "2026-07-09T20:01:15.206Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e9/601deb322d3303a7bf212b0100ead6f2ee3f6a044d89c30f2f92bf83c731/ruff-0.15.21-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:262ab31557a75141325e32d3357f3597645a7f084e732b6b054dde428ecd9341", size = 10964048, upload-time = "2026-07-09T20:01:17.723Z" }, + { url = "https://files.pythonhosted.org/packages/ea/2e/0f2176d1e99c15192caea19c8c3a0a955246b4cb4de795042eeb616345cd/ruff-0.15.21-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:659c4e7a4212f83306045ec7c5e5a356d16d9a6ef4ae0c7a4d872914fc655d9d", size = 10667055, upload-time = "2026-07-09T20:01:19.73Z" }, + { url = "https://files.pythonhosted.org/packages/48/60/abd74a02e0c4214f12a68becfd30af7165cfdcb0e661ecdc60bbb949c09a/ruff-0.15.21-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9e866eab611a5f959d36df2d10e446973a3610bc42b0c15b31dc27977d59c233", size = 11242043, upload-time = "2026-07-09T20:01:21.947Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c6/583075d8ccabb4b229345edcaf1545eb3d8d6be90f686a479d7e94088bbf/ruff-0.15.21-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e89bc93c0d3803ba870b55c29671bad9dc6d94bb1eb181b056b52eb05b52854f", size = 11648064, upload-time = "2026-07-09T20:01:24.023Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3c/37d0ecb729a7cc2d393ea7dce316fc585680f35d93b8d62139d7d0a3700c/ruff-0.15.21-py3-none-win32.whl", hash = "sha256:01f8d5be84823c172b389e123174f781f9daf86d6c58719d603f941932195cdd", size = 10896555, upload-time = "2026-07-09T20:01:26.941Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b8/e43466b2a6067ce91e669068f6e28d6c719a920f014b070d5c8731725de3/ruff-0.15.21-py3-none-win_amd64.whl", hash = "sha256:d4b8d9a2f0f12b816b50447f6eccb9f4bb01a6b82c86b50fb3b5354b458dc6d3", size = 12038772, upload-time = "2026-07-09T20:01:29.497Z" }, + { url = "https://files.pythonhosted.org/packages/dd/75/e90ab9aeece218a9fc5a5bc3ec97d0ee6bb3c4ff95869463c1de58e29a1c/ruff-0.15.21-py3-none-win_arm64.whl", hash = "sha256:6e83115d4b9377c1cbc13abf0e051f069fab0ef815ea0504a8a008cee24dd0a8", size = 11375265, upload-time = "2026-07-09T20:01:31.772Z" }, ] [[package]] From 31f09ad53e7323370bd3c814e05857257ecf4d63 Mon Sep 17 00:00:00 2001 From: Lucas Christian Date: Sat, 11 Jul 2026 11:23:55 -0300 Subject: [PATCH 2/5] feat: enhance FastAPI adapter with repeated sort parameter support - Introduced `SortParameterFormat` enum to handle different sort parameter formats. - Updated `FastAPICriteriaConfig` to include `sort_parameter_format` attribute. - Modified dependency functions to support repeated sort parameters. - Added tests for repeated sort parameters and validation of sort fields. - Cleaned up code by removing unused functions and improving error handling. --- docs/usage/fastapi.md | 23 +++ src/pyrsql/adapters/fastapi/__init__.py | 6 +- src/pyrsql/adapters/fastapi/config.py | 111 ++--------- src/pyrsql/adapters/fastapi/criteria.py | 2 - src/pyrsql/adapters/fastapi/dependency.py | 183 +++++++++++++----- src/pyrsql/adapters/fastapi/errors.py | 23 +-- .../fastapi/sqlalchemy/helpers.py | 20 +- .../fastapi/sqlalchemy/integration.py | 124 ++---------- .../fastapi/sqlalchemy/resource.py | 90 +-------- src/pyrsql/orms/sqlalchemy/compiled_page.py | 5 +- .../fastapi_adapter/test_dependency.py | 27 +++ tests/security/test_fastapi_sqlalchemy_hpp.py | 22 ++- .../adapters/fastapi_adapter/test_config.py | 86 +------- .../fastapi_sqlalchemy/test_integration.py | 182 ----------------- 14 files changed, 273 insertions(+), 631 deletions(-) diff --git a/docs/usage/fastapi.md b/docs/usage/fastapi.md index 53abdb8..e17ef69 100644 --- a/docs/usage/fastapi.md +++ b/docs/usage/fastapi.md @@ -40,6 +40,29 @@ config = FastAPICriteriaConfig( dependency = criteria_dependency(config) ``` +## Repeated sort parameters + +The default sort representation uses one semicolon-delimited parameter: + +```text +?sort=name,asc;created_at,desc +``` + +To accept one sort field per repeated query parameter, configure the adapter +with `SortParameterFormat.REPEATED`: + +```python +from pyrsql.adapters.fastapi import SortParameterFormat + +config = FastAPICriteriaConfig( + sort_parameter_format=SortParameterFormat.REPEATED, +) +``` + +```text +?sort=name,asc&sort=created_at,desc +``` + ## Class-based dependency ```python diff --git a/src/pyrsql/adapters/fastapi/__init__.py b/src/pyrsql/adapters/fastapi/__init__.py index 94bf098..c9a14da 100644 --- a/src/pyrsql/adapters/fastapi/__init__.py +++ b/src/pyrsql/adapters/fastapi/__init__.py @@ -1,6 +1,9 @@ """FastAPI adapter for pyrsql request extraction and error translation.""" -from pyrsql.adapters.fastapi.config import FastAPICriteriaConfig +from pyrsql.adapters.fastapi.config import ( + FastAPICriteriaConfig, + SortParameterFormat, +) from pyrsql.adapters.fastapi.criteria import RequestCriteria from pyrsql.adapters.fastapi.dependency import ( CriteriaDependency, @@ -11,5 +14,6 @@ "CriteriaDependency", "FastAPICriteriaConfig", "RequestCriteria", + "SortParameterFormat", "criteria_dependency", ) diff --git a/src/pyrsql/adapters/fastapi/config.py b/src/pyrsql/adapters/fastapi/config.py index 692a44b..686d600 100644 --- a/src/pyrsql/adapters/fastapi/config.py +++ b/src/pyrsql/adapters/fastapi/config.py @@ -1,13 +1,17 @@ """Configuration objects for the FastAPI adapter.""" -from collections.abc import Mapping +from __future__ import annotations + +from enum import Enum from types import MappingProxyType -from typing import Any +from typing import TYPE_CHECKING, Any import msgspec from pyrsql.core.options import QueryOptions, SortOptions -from pyrsql.core.validation import validate_positive_int + +if TYPE_CHECKING: + from collections.abc import Mapping _DEFAULT_FILTER_PARAMETER = "filter" _DEFAULT_SORT_PARAMETER = "sort" @@ -18,44 +22,11 @@ _EMPTY_OPENAPI_EXAMPLES: Mapping[str, Any] = MappingProxyType({}) -def _normalize_parameter_name(name: object, *, field_name: str) -> str: - """Validates and normalizes one public FastAPI parameter name. +class SortParameterFormat(Enum): + """Supported HTTP representations for sort criteria.""" - Returns: - The validated parameter name. - - Raises: - TypeError: If the parameter name is not a string. - ValueError: If the parameter name is empty or has outer whitespace. - """ - if not isinstance(name, str): - raise TypeError(f"{field_name} must be a string.") - normalized_name = name.strip() - if not normalized_name: - raise ValueError("FastAPI parameter names must not be empty.") - if normalized_name != name: - raise ValueError( - "FastAPI parameter names must not contain outer whitespace.", - ) - return normalized_name - - -def _normalize_openapi_examples_mapping( - value: object, - *, - field_name: str, -) -> Mapping[str, Any]: - """Validates and freezes one OpenAPI examples mapping. - - Returns: - An immutable copy of the provided examples mapping. - - Raises: - TypeError: If the provided value is not a mapping. - """ - if not isinstance(value, Mapping): - raise TypeError(f"{field_name} must be a mapping instance.") - return MappingProxyType(dict(value)) + SEMICOLON = "semicolon" + REPEATED = "repeated" class FastAPICriteriaConfig( @@ -69,6 +40,7 @@ class FastAPICriteriaConfig( Attributes: filter_parameter: Query parameter used for filtering expressions. sort_parameter: Query parameter used for sort expressions. + sort_parameter_format: HTTP representation accepted for sort criteria. page_parameter: Query parameter used for page numbers. size_parameter: Query parameter used for page size. default_page_size: Default size used when only a page number is given. @@ -80,6 +52,7 @@ class FastAPICriteriaConfig( filter_parameter: str = _DEFAULT_FILTER_PARAMETER sort_parameter: str = _DEFAULT_SORT_PARAMETER + sort_parameter_format: SortParameterFormat = SortParameterFormat.SEMICOLON page_parameter: str = _DEFAULT_PAGE_PARAMETER size_parameter: str = _DEFAULT_SIZE_PARAMETER default_page_size: int | None = None @@ -92,64 +65,6 @@ class FastAPICriteriaConfig( page_openapi_examples: Mapping[str, Any] = _EMPTY_OPENAPI_EXAMPLES size_openapi_examples: Mapping[str, Any] = _EMPTY_OPENAPI_EXAMPLES - def __post_init__(self) -> None: - """Validates adapter configuration invariants. - - Raises: - ValueError: If parameter names are empty, duplicated, or paging - limits are inconsistent. - """ - parameter_names = ( - _normalize_parameter_name( - self.filter_parameter, - field_name="filter_parameter", - ), - _normalize_parameter_name( - self.sort_parameter, - field_name="sort_parameter", - ), - _normalize_parameter_name( - self.page_parameter, - field_name="page_parameter", - ), - _normalize_parameter_name( - self.size_parameter, - field_name="size_parameter", - ), - ) - if len(set(parameter_names)) != len(parameter_names): - raise ValueError("FastAPI parameter names must be unique.") - if self.default_page_size is not None: - validate_positive_int( - self.default_page_size, - field_name="default_page_size", - ) - if self.max_page_size is not None: - validate_positive_int( - self.max_page_size, - field_name="max_page_size", - ) - if ( - self.default_page_size is not None - and self.max_page_size is not None - and self.default_page_size > self.max_page_size - ): - raise ValueError("default_page_size must not exceed max_page_size.") - for field_name in ( - "filter_openapi_examples", - "sort_openapi_examples", - "page_openapi_examples", - "size_openapi_examples", - ): - msgspec.structs.force_setattr( - self, - field_name, - _normalize_openapi_examples_mapping( - getattr(self, field_name), - field_name=field_name, - ), - ) - @property def minimum_page_number(self) -> int: """The minimum accepted page number for the adapter. diff --git a/src/pyrsql/adapters/fastapi/criteria.py b/src/pyrsql/adapters/fastapi/criteria.py index 28c42c4..a7de586 100644 --- a/src/pyrsql/adapters/fastapi/criteria.py +++ b/src/pyrsql/adapters/fastapi/criteria.py @@ -53,8 +53,6 @@ def apply( Returns: The transformed ORM-specific target after applying all criteria. """ - if self.is_empty: - return target current_target = target if self.query is not None: current_target = self.query.apply( diff --git a/src/pyrsql/adapters/fastapi/dependency.py b/src/pyrsql/adapters/fastapi/dependency.py index 0aae7e0..ca6986e 100644 --- a/src/pyrsql/adapters/fastapi/dependency.py +++ b/src/pyrsql/adapters/fastapi/dependency.py @@ -1,7 +1,7 @@ """Dependency factories for FastAPI request integration.""" from inspect import signature -from typing import TYPE_CHECKING, Annotated, cast +from typing import TYPE_CHECKING, Annotated, NoReturn try: from fastapi import HTTPException, Query @@ -11,7 +11,10 @@ "pip install pyrsql[fastapi]", ) from error -from pyrsql.adapters.fastapi.config import FastAPICriteriaConfig +from pyrsql.adapters.fastapi.config import ( + FastAPICriteriaConfig, + SortParameterFormat, +) from pyrsql.adapters.fastapi.criteria import RequestCriteria from pyrsql.adapters.fastapi.errors import ( FastAPIAdapterErrorPayload, @@ -46,7 +49,7 @@ def _raise_http_error( payload: "FastAPIAdapterErrorPayload", # noqa: UP037 -) -> None: +) -> NoReturn: """Raises a standardized FastAPI HTTP exception for adapter failures. Raises: @@ -95,23 +98,66 @@ def _build_page_request( ) if config.one_based_paging: - if resolved_page_number <= 0: - _raise_http_error( - FastAPIAdapterErrorPayload.from_page_error( - config.page_parameter, - error_type="page_validation_error", - detail_code="invalid_page_number", - message=( - f"'{config.page_parameter}' must be greater than 0 " - "when one_based_paging is enabled." - ), - ), - ) resolved_page_number -= 1 return PageRequest.of( resolved_page_number, - cast("int", resolved_page_size), + resolved_page_size, + ) + + +def _build_request_criteria( + *, + config: FastAPICriteriaConfig, + filter_value: str | None, + sort_value: str | None, + page_value: int | None, + size_value: int | None, +) -> RequestCriteria: + """Builds criteria from values already extracted by FastAPI. + + Returns: + The parsed request criteria. + """ + query = None + sort = None + + if filter_value: + try: + query = PyrsqlQuery.parse( + filter_value, + options=config.query_options, + ) + except _QUERY_ERROR_TYPES as error: + _raise_http_error( + FastAPIAdapterErrorPayload.from_query_error( + config.filter_parameter, + error, + ), + ) + + if sort_value: + try: + sort = PyrsqlSort.parse( + sort_value, + options=config.sort_options, + ) + except _SORT_ERROR_TYPES as error: + _raise_http_error( + FastAPIAdapterErrorPayload.from_sort_error( + config.sort_parameter, + error, + ), + ) + + return RequestCriteria( + query=query, + sort=sort, + page_request=_build_page_request( + config=config, + page_value=page_value, + size_value=size_value, + ), ) @@ -121,7 +167,20 @@ def _build_criteria_callable( """Builds the concrete dependency callable for a fixed configuration. Returns: - A FastAPI-compatible callable that parses request criteria. + A FastAPI-compatible callable with the configured sort format. + """ + if config.sort_parameter_format is SortParameterFormat.REPEATED: + return _build_repeated_sort_criteria_callable(config) + return _build_semicolon_sort_criteria_callable(config) + + +def _build_semicolon_sort_criteria_callable( + config: FastAPICriteriaConfig, +) -> "Callable[..., RequestCriteria]": # noqa: UP037 + """Builds a dependency that accepts one semicolon-delimited sort value. + + Returns: + A FastAPI-compatible callable. """ def dependency( @@ -157,47 +216,65 @@ def dependency( ), ] = None, ) -> RequestCriteria: - query = None - sort = None - - if filter_value: - try: - query = PyrsqlQuery.parse( - filter_value, - options=config.query_options, - ) - except _QUERY_ERROR_TYPES as error: - _raise_http_error( - FastAPIAdapterErrorPayload.from_query_error( - config.filter_parameter, - error, - ), - ) - - if sort_value: - try: - sort = PyrsqlSort.parse( - sort_value, - options=config.sort_options, - ) - except _SORT_ERROR_TYPES as error: - _raise_http_error( - FastAPIAdapterErrorPayload.from_sort_error( - config.sort_parameter, - error, - ), - ) - - page_request = _build_page_request( + return _build_request_criteria( config=config, + filter_value=filter_value, + sort_value=sort_value, page_value=page_value, size_value=size_value, ) - return RequestCriteria( - query=query, - sort=sort, - page_request=page_request, + return dependency + + +def _build_repeated_sort_criteria_callable( + config: FastAPICriteriaConfig, +) -> "Callable[..., RequestCriteria]": # noqa: UP037 + """Builds a dependency that accepts repeated sort query parameters. + + Returns: + A FastAPI-compatible callable. + """ + + def dependency( + filter_value: Annotated[ + str | None, + Query( + alias=config.filter_parameter, + openapi_examples=config.filter_openapi_examples or None, + ), + ] = None, + sort_values: Annotated[ + list[str] | None, + Query( + alias=config.sort_parameter, + openapi_examples=config.sort_openapi_examples or None, + ), + ] = None, + page_value: Annotated[ + int | None, + Query( + alias=config.page_parameter, + ge=config.minimum_page_number, + openapi_examples=config.page_openapi_examples or None, + ), + ] = None, + size_value: Annotated[ + int | None, + Query( + alias=config.size_parameter, + gt=0, + le=config.max_page_size, + openapi_examples=config.size_openapi_examples or None, + ), + ] = None, + ) -> RequestCriteria: + return _build_request_criteria( + config=config, + filter_value=filter_value, + sort_value=";".join(sort_values) if sort_values else None, + page_value=page_value, + size_value=size_value, ) return dependency diff --git a/src/pyrsql/adapters/fastapi/errors.py b/src/pyrsql/adapters/fastapi/errors.py index aee2ab0..2c087cc 100644 --- a/src/pyrsql/adapters/fastapi/errors.py +++ b/src/pyrsql/adapters/fastapi/errors.py @@ -6,7 +6,6 @@ import msgspec -from pyrsql.parsing.errors import ParseError from pyrsql.semantic.errors import ( FieldBlacklistedError, FieldNotWhitelistedError, @@ -24,6 +23,7 @@ if TYPE_CHECKING: from pyrsql.orms import ORMError + from pyrsql.parsing.errors import ParseError from pyrsql.parsing.source import SourceSpan SortAdapterError = ( @@ -93,7 +93,7 @@ def from_query_error( A normalized FastAPI adapter payload. """ error_type = "query_parse_error" - detail_code = getattr(error, "code", "query_parse_error") + detail_code = error.code detail_field = None detail_location = None if isinstance(error, SemanticError): @@ -109,7 +109,7 @@ def from_query_error( ): detail_field = _extract_field_from_message(error.message) detail_location = _build_error_location(error.span) - elif isinstance(error, ParseError): + else: detail_location = _build_error_location(error.span) return cls._single_detail_payload( parameter_name=parameter_name, @@ -132,8 +132,8 @@ def from_sort_error( A normalized FastAPI adapter payload. """ error_type = "sort_parse_error" - detail_code = getattr(error, "code", "sort_parse_error") - detail_message = getattr(error, "message", str(error)) + detail_code = error.code + detail_message = error.message detail_field = None if isinstance( error, @@ -192,7 +192,7 @@ def from_backend_error( return cls._single_detail_payload( parameter_name=parameter_name, error_type=error_type, - code=getattr(error, "code", "orm_error"), + code=error.code, detail=error_message, field=_extract_field_from_message(error_message), ) @@ -203,7 +203,7 @@ def to_http_detail(self) -> dict[str, object]: Returns: A JSON-compatible FastAPI error detail mapping. """ - problem_definition = _problem_definition(self.error_type) + problem_definition = _PROBLEM_DEFINITIONS[self.error_type] return { "type": problem_definition.type_uri, "title": problem_definition.title, @@ -214,8 +214,8 @@ def to_http_detail(self) -> dict[str, object]: @property def status_code(self) -> int: - """Returns the HTTP status code associated with this problem.""" - return _problem_definition(self.error_type).status + """HTTP status code associated with this problem.""" + return _PROBLEM_DEFINITIONS[self.error_type].status class _ProblemDefinition(msgspec.Struct, frozen=True, gc=False): @@ -297,11 +297,6 @@ class FastAPIAdapterErrorDetail( } -def _problem_definition(error_type: str) -> _ProblemDefinition: - """Returns the top-level problem definition for one category.""" - return _PROBLEM_DEFINITIONS[error_type] - - def _top_level_detail( title: str, details: tuple[FastAPIAdapterErrorDetail, ...], diff --git a/src/pyrsql/integrations/fastapi/sqlalchemy/helpers.py b/src/pyrsql/integrations/fastapi/sqlalchemy/helpers.py index 897f8c6..46c7766 100644 --- a/src/pyrsql/integrations/fastapi/sqlalchemy/helpers.py +++ b/src/pyrsql/integrations/fastapi/sqlalchemy/helpers.py @@ -8,7 +8,6 @@ from fastapi import HTTPException from sqlalchemy import func, select -from pyrsql.adapters.fastapi import RequestCriteria from pyrsql.adapters.fastapi.errors import FastAPIAdapterErrorPayload from pyrsql.integrations.fastapi.sqlalchemy.payloads import ( SQLAlchemyPaginatedSelect, @@ -19,7 +18,10 @@ from collections.abc import Iterator from typing import NoReturn - from pyrsql.adapters.fastapi import FastAPICriteriaConfig + from pyrsql.adapters.fastapi import ( + FastAPICriteriaConfig, + RequestCriteria, + ) from pyrsql.orms.sqlalchemy import SQLAlchemyORM from pyrsql.orms.sqlalchemy.types import SQLAlchemyModel, SQLAlchemySelect @@ -76,20 +78,6 @@ def sort_backend_http_errors( ) -def require_request_criteria(criteria: RequestCriteria) -> RequestCriteria: - """Validates and returns request criteria objects. - - Returns: - The validated request criteria instance. - - Raises: - TypeError: If the provided value is not a ``RequestCriteria``. - """ - if not isinstance(criteria, RequestCriteria): - raise TypeError("criteria must be a RequestCriteria.") - return criteria - - def apply_query_with_orm( statement: SQLAlchemySelect, model: SQLAlchemyModel, diff --git a/src/pyrsql/integrations/fastapi/sqlalchemy/integration.py b/src/pyrsql/integrations/fastapi/sqlalchemy/integration.py index 412a145..70f4b2c 100644 --- a/src/pyrsql/integrations/fastapi/sqlalchemy/integration.py +++ b/src/pyrsql/integrations/fastapi/sqlalchemy/integration.py @@ -3,9 +3,10 @@ from __future__ import annotations from threading import Lock -from typing import TYPE_CHECKING, TypeVar +from typing import TYPE_CHECKING from fastapi import Depends +import msgspec from sqlalchemy import select from pyrsql.adapters.fastapi import ( @@ -25,14 +26,12 @@ build_paginated_select, count_from_filtered_select, query_backend_http_errors, - require_request_criteria, sort_backend_http_errors, ) from pyrsql.integrations.fastapi.sqlalchemy.resource import ( FastAPISQLAlchemyResource, ) from pyrsql.orms.sqlalchemy import SQLAlchemyORM -from pyrsql.orms.sqlalchemy.statement import require_sqlalchemy_select if TYPE_CHECKING: from collections.abc import Callable @@ -46,7 +45,6 @@ from pyrsql.orms.sqlalchemy.types import SQLAlchemyModel, SQLAlchemySelect _DEFAULT_FASTAPI_CRITERIA_CONFIG = FastAPICriteriaConfig() -_CachedValueT = TypeVar("_CachedValueT") class FastAPISQLAlchemyIntegration: @@ -55,10 +53,7 @@ class FastAPISQLAlchemyIntegration: __slots__ = ( "_base_selects", "_cache_lock", - "_count_select_dependencies", "_criteria_dependency", - "_paginated_select_dependencies", - "_select_dependencies", "criteria_config", "orm", ) @@ -78,18 +73,6 @@ def __init__( ) self._criteria_dependency = CriteriaDependency(self.criteria_config) self._cache_lock = Lock() - self._select_dependencies: dict[ - SQLAlchemyModel, - Callable[..., SQLAlchemySelect], - ] = {} - self._count_select_dependencies: dict[ - SQLAlchemyModel, - Callable[..., SQLAlchemySelect], - ] = {} - self._paginated_select_dependencies: dict[ - SQLAlchemyModel, - Callable[..., SQLAlchemyPaginatedSelect], - ] = {} self._base_selects: dict[SQLAlchemyModel, SQLAlchemySelect] = {} def criteria_dependency(self) -> CriteriaDependency: @@ -100,24 +83,6 @@ def criteria_dependency(self) -> CriteriaDependency: """ return self._criteria_dependency - def _apply_query( - self, - statement: SQLAlchemySelect, - model: SQLAlchemyModel, - criteria: RequestCriteria, - ) -> SQLAlchemySelect: - """Applies only the filtering part of request criteria. - - Returns: - A statement with query/filter semantics applied. - """ - return apply_query_with_orm( - statement, - model, - criteria, - self.orm, - ) - def _apply_sort_and_page( self, statement: SQLAlchemySelect, @@ -146,7 +111,12 @@ def _filtered_select( Returns: The shared filtered select statement used by list and count flows. """ - return self._apply_query(self.base_select(model), model, criteria) + return apply_query_with_orm( + self.base_select(model), + model, + criteria, + self.orm, + ) def base_select(self, model: SQLAlchemyModel) -> SQLAlchemySelect: """Returns the cached base select for a model. @@ -157,11 +127,12 @@ def base_select(self, model: SQLAlchemyModel) -> SQLAlchemySelect: statement = self._base_selects.get(model) if statement is not None: return statement - return self._publish_cached_model_value( - self._base_selects, - model, - select(model), - ) + with self._cache_lock: + statement = self._base_selects.get(model) + if statement is None: + statement = select(model) + self._base_selects[model] = statement + return statement def apply( self, @@ -174,8 +145,6 @@ def apply( Returns: A statement with all request criteria applied. """ - require_sqlalchemy_select(statement) - criteria = require_request_criteria(criteria) return criteria.apply(statement, model, orm=self.orm) def select( @@ -188,7 +157,6 @@ def select( Returns: A select statement with all request criteria applied. """ - criteria = require_request_criteria(criteria) filtered_statement = self._filtered_select(model, criteria) return self._apply_sort_and_page( filtered_statement, @@ -206,7 +174,6 @@ def count_select( Returns: A count statement derived from the filtered query semantics. """ - criteria = require_request_criteria(criteria) return count_from_filtered_select( self._filtered_select(model, criteria), ) @@ -221,7 +188,6 @@ def paginated_select( Returns: The paired list and count statements for a paginated flow. """ - criteria = require_request_criteria(criteria) filtered_statement = self._filtered_select(model, criteria) return build_paginated_select( statement=self._apply_sort_and_page( @@ -241,9 +207,6 @@ def select_dependency( Returns: A FastAPI dependency that yields a filtered select statement. """ - cached_dependency = self._select_dependencies.get(model) - if cached_dependency is not None: - return cached_dependency criteria_dependency = self.criteria_dependency() def dependency( @@ -261,11 +224,7 @@ def dependency( criteria, ) - return self._publish_cached_model_value( - self._select_dependencies, - model, - dependency, - ) + return dependency def count_select_dependency( self, @@ -276,9 +235,6 @@ def count_select_dependency( Returns: A FastAPI dependency that yields a count select statement. """ - cached_dependency = self._count_select_dependencies.get(model) - if cached_dependency is not None: - return cached_dependency criteria_dependency = self.criteria_dependency() def dependency( @@ -291,11 +247,7 @@ def dependency( ) return count_from_filtered_select(filtered_statement) - return self._publish_cached_model_value( - self._count_select_dependencies, - model, - dependency, - ) + return dependency def paginated_select_dependency( self, @@ -306,9 +258,6 @@ def paginated_select_dependency( Returns: A FastAPI dependency that yields paired list and count statements. """ - cached_dependency = self._paginated_select_dependencies.get(model) - if cached_dependency is not None: - return cached_dependency criteria_dependency = self.criteria_dependency() def dependency( @@ -329,29 +278,7 @@ def dependency( filtered_statement=filtered_statement, ) - return self._publish_cached_model_value( - self._paginated_select_dependencies, - model, - dependency, - ) - - def _publish_cached_model_value( - self, - cache: dict[SQLAlchemyModel, _CachedValueT], - model: SQLAlchemyModel, - value: _CachedValueT, - ) -> _CachedValueT: - """Publishes one cached model-scoped value with minimal lock scope. - - Returns: - The cached value shared for the model. - """ - with self._cache_lock: - cached_value = cache.get(model) - if cached_value is not None: - return cached_value - cache[model] = value - return value + return dependency def resource( self, @@ -399,7 +326,8 @@ def resource( sort_examples, ) - criteria_config = FastAPICriteriaConfig( + criteria_config = msgspec.structs.replace( + self.criteria_config, filter_parameter=( self.criteria_config.filter_parameter if query_parameter_name is None @@ -420,25 +348,15 @@ def resource( if size_parameter_name is None else size_parameter_name ), - default_page_size=self.criteria_config.default_page_size, max_page_size=( self.criteria_config.max_page_size if max_page_size is None else max_page_size ), - one_based_paging=self.criteria_config.one_based_paging, query_options=query_options, sort_options=sort_options, - filter_openapi_examples=( - {} - if filter_openapi_examples is None - else filter_openapi_examples - ), - sort_openapi_examples=( - {} if sort_openapi_examples is None else sort_openapi_examples - ), - page_openapi_examples=self.criteria_config.page_openapi_examples, - size_openapi_examples=self.criteria_config.size_openapi_examples, + filter_openapi_examples=filter_openapi_examples, + sort_openapi_examples=sort_openapi_examples, ) compiled_default_sort = None diff --git a/src/pyrsql/integrations/fastapi/sqlalchemy/resource.py b/src/pyrsql/integrations/fastapi/sqlalchemy/resource.py index afeb1f5..2ada7f9 100644 --- a/src/pyrsql/integrations/fastapi/sqlalchemy/resource.py +++ b/src/pyrsql/integrations/fastapi/sqlalchemy/resource.py @@ -2,8 +2,7 @@ from __future__ import annotations -from threading import Lock -from typing import TYPE_CHECKING, TypeVar, cast +from typing import TYPE_CHECKING from fastapi import Depends @@ -18,10 +17,8 @@ build_paginated_select, count_from_filtered_select, query_backend_http_errors, - require_request_criteria, sort_backend_http_errors, ) -from pyrsql.orms.sqlalchemy.statement import require_sqlalchemy_select if TYPE_CHECKING: from collections.abc import Callable @@ -35,8 +32,6 @@ ) from pyrsql.orms.sqlalchemy.types import SQLAlchemyModel, SQLAlchemySelect -_CachedValueT = TypeVar("_CachedValueT") - class FastAPISQLAlchemyResource: """Declarative FastAPI + SQLAlchemy resource integration.""" @@ -46,22 +41,8 @@ class FastAPISQLAlchemyResource: criteria_config: FastAPICriteriaConfig _criteria_dependency: Callable[..., RequestCriteria] _statement_factory: Callable[[], SQLAlchemySelect] | None - _applier_dependency: ( - Callable[..., Callable[[SQLAlchemySelect], SQLAlchemySelect]] | None - ) - _select_dependency: Callable[..., SQLAlchemySelect] | None - _count_select_dependency: Callable[..., SQLAlchemySelect] | None - _paginated_select_dependency: ( - Callable[..., SQLAlchemyPaginatedSelect] | None - ) - __slots__ = ( - "_applier_dependency", - "_cache_lock", - "_count_select_dependency", "_criteria_dependency", - "_paginated_select_dependency", - "_select_dependency", "_statement_factory", "criteria_config", "integration", @@ -82,17 +63,6 @@ def __init__( self.model = model self.criteria_config = criteria_config self._statement_factory = statement_factory - self._cache_lock = Lock() - self._applier_dependency: ( - Callable[..., Callable[[SQLAlchemySelect], SQLAlchemySelect]] | None - ) = None - self._select_dependency: Callable[..., SQLAlchemySelect] | None = None - self._count_select_dependency: ( - Callable[..., SQLAlchemySelect] | None - ) = None - self._paginated_select_dependency: ( - Callable[..., SQLAlchemyPaginatedSelect] | None - ) = None base_dependency: Callable[..., RequestCriteria] = CriteriaDependency( criteria_config, ) @@ -129,7 +99,7 @@ def _base_statement(self) -> SQLAlchemySelect: """ if self._statement_factory is None: return self.integration.base_select(self.model) - return require_sqlalchemy_select(self._statement_factory()) + return self._statement_factory() def select(self, criteria: RequestCriteria) -> SQLAlchemySelect: """Builds a filtered select for the configured resource model. @@ -152,13 +122,12 @@ def applier( Returns: A callable that applies the resource's criteria to a select. """ - validated_criteria = require_request_criteria(criteria) def apply_to(statement: SQLAlchemySelect) -> SQLAlchemySelect: return self.integration.apply( statement, self.model, - validated_criteria, + criteria, ) return apply_to @@ -220,8 +189,7 @@ def count_select(self, criteria: RequestCriteria) -> SQLAlchemySelect: Returns: A count SQLAlchemy select statement. """ - validated_criteria = require_request_criteria(criteria) - filtered_statement = self._filtered_statement(validated_criteria) + filtered_statement = self._filtered_statement(criteria) return count_from_filtered_select(filtered_statement) def paginated_select( @@ -233,9 +201,8 @@ def paginated_select( Returns: Paired list and count SQLAlchemy statements. """ - validated_criteria = require_request_criteria(criteria) - filtered_statement = self._filtered_statement(validated_criteria) - return self._paginated_bundle(filtered_statement, validated_criteria) + filtered_statement = self._filtered_statement(criteria) + return self._paginated_bundle(filtered_statement, criteria) def select_dependency(self) -> Callable[..., SQLAlchemySelect]: """Returns a FastAPI dependency yielding a filtered select. @@ -243,8 +210,6 @@ def select_dependency(self) -> Callable[..., SQLAlchemySelect]: Returns: A FastAPI dependency that yields a filtered select statement. """ - if self._select_dependency is not None: - return self._select_dependency criteria_dependency = self.criteria_dependency() def dependency( @@ -260,7 +225,7 @@ def dependency( criteria, ) - return self._publish_cached_dependency("_select_dependency", dependency) + return dependency def applier_dependency( self, @@ -270,8 +235,6 @@ def applier_dependency( Returns: A FastAPI dependency that yields a select applier callable. """ - if self._applier_dependency is not None: - return self._applier_dependency criteria_dependency = self.criteria_dependency() def dependency( @@ -279,10 +242,7 @@ def dependency( ) -> Callable[[SQLAlchemySelect], SQLAlchemySelect]: return self.applier(criteria) - return self._publish_cached_dependency( - "_applier_dependency", - dependency, - ) + return dependency def count_select_dependency(self) -> Callable[..., SQLAlchemySelect]: """Returns a FastAPI dependency yielding a count select. @@ -290,8 +250,6 @@ def count_select_dependency(self) -> Callable[..., SQLAlchemySelect]: Returns: A FastAPI dependency that yields a count select statement. """ - if self._count_select_dependency is not None: - return self._count_select_dependency criteria_dependency = self.criteria_dependency() def dependency( @@ -303,10 +261,7 @@ def dependency( ) return count_from_filtered_select(filtered_statement) - return self._publish_cached_dependency( - "_count_select_dependency", - dependency, - ) + return dependency def paginated_select_dependency( self, @@ -316,8 +271,6 @@ def paginated_select_dependency( Returns: A FastAPI dependency that yields paginated SQLAlchemy statements. """ - if self._paginated_select_dependency is not None: - return self._paginated_select_dependency criteria_dependency = self.criteria_dependency() def dependency( @@ -333,27 +286,4 @@ def dependency( criteria, ) - return self._publish_cached_dependency( - "_paginated_select_dependency", - dependency, - ) - - def _publish_cached_dependency( - self, - attribute_name: str, - dependency: _CachedValueT, - ) -> _CachedValueT: - """Publishes one cached dependency with minimal lock scope. - - Returns: - The shared cached dependency. - """ - with self._cache_lock: - cached_dependency = cast( - "_CachedValueT | None", - getattr(self, attribute_name), - ) - if cached_dependency is not None: - return cached_dependency - setattr(self, attribute_name, dependency) - return dependency + return dependency diff --git a/src/pyrsql/orms/sqlalchemy/compiled_page.py b/src/pyrsql/orms/sqlalchemy/compiled_page.py index 6ff6eaf..31c0048 100644 --- a/src/pyrsql/orms/sqlalchemy/compiled_page.py +++ b/src/pyrsql/orms/sqlalchemy/compiled_page.py @@ -30,18 +30,19 @@ class SQLAlchemyCompiledPageRequest( def apply( self, target: SQLAlchemySelect, - _model: SQLAlchemyModel, + model: SQLAlchemyModel, ) -> SQLAlchemySelect: """Applies the compiled page request to a SQLAlchemy Select. Args: target: SQLAlchemy select statement to mutate. - _model: SQLAlchemy mapped class. It is unused, but the signature + model: SQLAlchemy mapped class. It is unused, but the signature matches the other compiled plans. Returns: A SQLAlchemy select with limit and offset applied. """ + del model statement = require_sqlalchemy_select(target) return statement.limit(self.page_request.limit).offset( self.page_request.offset, diff --git a/tests/functional/fastapi_adapter/test_dependency.py b/tests/functional/fastapi_adapter/test_dependency.py index b174113..4a4217e 100644 --- a/tests/functional/fastapi_adapter/test_dependency.py +++ b/tests/functional/fastapi_adapter/test_dependency.py @@ -12,6 +12,7 @@ CriteriaDependency, FastAPICriteriaConfig, RequestCriteria, + SortParameterFormat, criteria_dependency, ) from pyrsql.core.options import QueryOptions @@ -94,6 +95,32 @@ def list_items( } +def test_dependency_supports_repeated_sort_parameters() -> None: + """Collects repeated sort parameters through FastAPI's list support.""" + app = FastAPI() + dependency = criteria_dependency( + FastAPICriteriaConfig( + sort_parameter_format=SortParameterFormat.REPEATED, + ), + ) + + @app.get("/items") + def list_items( + criteria: Annotated[RequestCriteria, Depends(dependency)], + ) -> dict[str, str | None]: + return { + "sort": criteria.sort.text if criteria.sort is not None else None, + } + + response = TestClient(app).get( + "/items", + params=[("sort", "name,asc"), ("sort", "created_at,desc")], + ) + + assert response.status_code == 200 + assert response.json() == {"sort": "name,asc;created_at,desc"} + + def test_dependency_translates_query_parse_errors_to_http_400() -> None: """Maps query parsing failures to a stable HTTP 400 error payload.""" app = FastAPI() diff --git a/tests/security/test_fastapi_sqlalchemy_hpp.py b/tests/security/test_fastapi_sqlalchemy_hpp.py index 347872b..99abdba 100644 --- a/tests/security/test_fastapi_sqlalchemy_hpp.py +++ b/tests/security/test_fastapi_sqlalchemy_hpp.py @@ -6,7 +6,7 @@ import pytest -from pyrsql.adapters.fastapi import FastAPICriteriaConfig +from pyrsql.adapters.fastapi import FastAPICriteriaConfig, SortParameterFormat if TYPE_CHECKING: from collections.abc import Callable @@ -54,6 +54,26 @@ def test_duplicate_sort_parameters_use_last_value_and_still_validate( ) +def test_repeated_sort_parameters_validate_every_value( + integration_app_factory: Callable[..., TestClient], +) -> None: + """Rejects an invalid repeated sort field instead of ignoring it.""" + response = integration_app_factory( + criteria_config=FastAPICriteriaConfig( + sort_parameter_format=SortParameterFormat.REPEATED, + ), + ).get( + "/users", + params=[("sort", "name,asc"), ("sort", "DROP TABLE users")], + ) + + assert response.status_code == 422 + assert response.json()["detail"]["type"] == SORT_BACKEND_TYPE + assert response.json()["detail"]["errors"][0]["field"] == ( + "DROP TABLE users" + ) + + def test_duplicate_page_parameters_use_last_value_consistently( compiled_params_app_factory: Callable[..., TestClient], ) -> None: diff --git a/tests/unit/adapters/fastapi_adapter/test_config.py b/tests/unit/adapters/fastapi_adapter/test_config.py index c5378ec..b6dc9e9 100644 --- a/tests/unit/adapters/fastapi_adapter/test_config.py +++ b/tests/unit/adapters/fastapi_adapter/test_config.py @@ -2,81 +2,14 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, cast - import pytest -from pyrsql.adapters.fastapi import FastAPICriteriaConfig +from pyrsql.adapters.fastapi import FastAPICriteriaConfig, SortParameterFormat from pyrsql.core.options import QueryOptions, SortOptions -if TYPE_CHECKING: - from collections.abc import Mapping - pytestmark = [pytest.mark.fastapi] -@pytest.mark.parametrize( - "kwargs", - [ - pytest.param( - {"filter_parameter": "filter", "sort_parameter": "filter"}, - id="duplicate-parameter-names", - ), - pytest.param( - {"filter_parameter": " filter "}, - id="outer-whitespace", - ), - pytest.param( - {"filter_parameter": 123}, - id="non-string-parameter-name", - ), - ], -) -def test_config_rejects_invalid_parameter_names( - kwargs: dict[str, object], -) -> None: - """Rejects duplicate aliases and names with outer whitespace.""" - with pytest.raises( - (TypeError, ValueError), - match=r"(?i)parameter|whitespace|unique|string", - ): - FastAPICriteriaConfig(**cast("Any", kwargs)) - - -@pytest.mark.parametrize( - ("kwargs", "pattern"), - [ - pytest.param( - {"default_page_size": 0}, - r"default_page_size|page[_ ]?size|greater", - id="non-positive-default-page-size", - ), - pytest.param( - {"default_page_size": 51, "max_page_size": 50}, - r"max|page size|exceed", - id="default-page-size-above-max", - ), - pytest.param( - {"max_page_size": True}, - r"max_page_size|integer", - id="bool-max-page-size", - ), - pytest.param( - {"filter_openapi_examples": "invalid"}, - r"filter_openapi_examples", - id="invalid-filter-examples", - ), - ], -) -def test_config_rejects_invalid_public_configuration( - kwargs: dict[str, object], - pattern: str, -) -> None: - """Rejects invalid paging, options, and OpenAPI examples.""" - with pytest.raises((TypeError, ValueError), match=pattern): - FastAPICriteriaConfig(**cast("Any", kwargs)) - - def test_config_exposes_derived_page_numbers() -> None: """Exposes derived one-based and zero-based page boundaries.""" zero_based = FastAPICriteriaConfig() @@ -90,17 +23,12 @@ def test_config_exposes_derived_page_numbers() -> None: assert one_based.default_page_number == 1 -def test_config_keeps_openapi_examples_as_immutable_copies( - openapi_examples: Mapping[str, Any], -) -> None: - """Stores OpenAPI examples as immutable copies.""" - config = FastAPICriteriaConfig(filter_openapi_examples=openapi_examples) - - assert config.filter_openapi_examples["by_name"]["value"] == "name==demo" - assert config.filter_openapi_examples is not openapi_examples - - with pytest.raises(TypeError): - cast("dict[str, Any]", config.filter_openapi_examples)["new"] = {} +def test_config_uses_semicolon_sort_format_by_default() -> None: + """Preserves the single-parameter sort representation by default.""" + assert ( + FastAPICriteriaConfig().sort_parameter_format + is SortParameterFormat.SEMICOLON + ) def test_config_reuses_default_shared_options() -> None: diff --git a/tests/unit/integrations/fastapi_sqlalchemy/test_integration.py b/tests/unit/integrations/fastapi_sqlalchemy/test_integration.py index 9436ac2..358ff9c 100644 --- a/tests/unit/integrations/fastapi_sqlalchemy/test_integration.py +++ b/tests/unit/integrations/fastapi_sqlalchemy/test_integration.py @@ -22,8 +22,6 @@ from .conftest import User if TYPE_CHECKING: - from collections.abc import Callable - from sqlalchemy.sql import Select from pyrsql.orms.sqlalchemy import SQLAlchemyORM @@ -33,16 +31,6 @@ pytestmark = [pytest.mark.fastapi, pytest.mark.sqlalchemy] -def _collect_concurrent_results( - factory: Callable[[], object], - *, - workers: int = 8, -) -> list[object]: - """Runs one factory concurrently and collects all returned values.""" - with ThreadPoolExecutor(max_workers=workers) as executor: - return list(executor.map(lambda _: factory(), range(workers))) - - def test_integration_exposes_configured_criteria_dependency( fastapi_criteria_config: FastAPICriteriaConfig, sqlalchemy_orm: SQLAlchemyORM, @@ -58,24 +46,6 @@ def test_integration_exposes_configured_criteria_dependency( assert dependency.config is fastapi_criteria_config -def test_integration_reuses_cached_dependencies( - integration: FastAPISQLAlchemyIntegration, -) -> None: - """Reuses dependency objects for the same model and integration.""" - assert ( - integration.criteria_dependency() is integration.criteria_dependency() - ) - assert integration.select_dependency(User) is integration.select_dependency( - User, - ) - assert integration.count_select_dependency( - User, - ) is integration.count_select_dependency(User) - assert integration.paginated_select_dependency( - User, - ) is integration.paginated_select_dependency(User) - - def test_default_integrations_do_not_share_the_same_orm_instance() -> None: """Allocates a distinct ORM instance per default integration.""" left = FastAPISQLAlchemyIntegration() @@ -115,17 +85,6 @@ def slow_select(*args: object, **kwargs: object) -> Select[Any]: assert all(statement is statements[0] for statement in statements) -def test_integration_dependency_cache_is_safe_under_concurrency() -> None: - """Builds one dependency callable per model under concurrent access.""" - integration = FastAPISQLAlchemyIntegration() - - dependencies = _collect_concurrent_results( - lambda: integration.select_dependency(User), - ) - - assert all(dependency is dependencies[0] for dependency in dependencies) - - def test_integration_apply_delegates_to_request_criteria( integration: FastAPISQLAlchemyIntegration, base_statement: Select[Any], @@ -441,31 +400,6 @@ def test_resource_select_uses_statement_factory_for_base_statement( apply_mock.assert_called_once_with(statement, User, query_criteria) -@pytest.mark.parametrize( - ("statement_factory", "pattern"), - [ - pytest.param( - lambda: cast("Any", "invalid"), - r"sqlalchemy\.sql\.Select", - id="non-select-result", - ), - ], -) -def test_resource_rejects_invalid_statement_factory_results( - integration: FastAPISQLAlchemyIntegration, - statement_factory: Callable[[], object], - pattern: str, -) -> None: - """Rejects invalid base statement factories for declarative resources.""" - resource = integration.resource( - User, - statement_factory=cast("Callable[[], Select[Any]]", statement_factory), - ) - - with pytest.raises(TypeError, match=pattern): - resource.select(RequestCriteria()) - - def test_resource_reuses_integration_cached_base_select( integration: FastAPISQLAlchemyIntegration, ) -> None: @@ -475,82 +409,6 @@ def test_resource_reuses_integration_cached_base_select( assert resource.select(RequestCriteria()) is integration.base_select(User) -def test_resource_reuses_cached_dependencies( - integration: FastAPISQLAlchemyIntegration, -) -> None: - """Reuses dependency objects created by a declarative resource.""" - resource = integration.resource(User, default_sort="-name") - - assert resource.applier_dependency() is resource.applier_dependency() - assert resource.select_dependency() is resource.select_dependency() - assert ( - resource.count_select_dependency() is resource.count_select_dependency() - ) - assert ( - resource.paginated_select_dependency() - is resource.paginated_select_dependency() - ) - - -def test_resource_select_dependency_cache_is_safe_under_concurrency( - integration: FastAPISQLAlchemyIntegration, -) -> None: - """Builds one select dependency callable under concurrent access.""" - resource = integration.resource(User, default_sort="-name") - dependencies = _collect_concurrent_results(resource.select_dependency) - - assert all(dependency is dependencies[0] for dependency in dependencies) - - -def test_resource_applier_dependency_cache_is_safe_under_concurrency( - integration: FastAPISQLAlchemyIntegration, -) -> None: - """Builds one applier dependency callable under concurrent access.""" - resource = integration.resource(User, default_sort="-name") - dependencies = _collect_concurrent_results(resource.applier_dependency) - - assert all(dependency is dependencies[0] for dependency in dependencies) - - -def test_resource_count_dependency_cache_is_safe_under_concurrency( - integration: FastAPISQLAlchemyIntegration, -) -> None: - """Builds one count dependency callable under concurrent access.""" - resource = integration.resource(User, default_sort="-name") - dependencies = _collect_concurrent_results( - resource.count_select_dependency, - ) - - assert all(dependency is dependencies[0] for dependency in dependencies) - - -def test_resource_paginated_dependency_cache_is_safe_under_concurrency( - integration: FastAPISQLAlchemyIntegration, -) -> None: - """Builds one paginated dependency callable under concurrent access.""" - resource = integration.resource(User, default_sort="-name") - dependencies = _collect_concurrent_results( - resource.paginated_select_dependency, - ) - - assert all(dependency is dependencies[0] for dependency in dependencies) - - -@pytest.mark.parametrize( - "method_name", - ["select", "count_select", "paginated_select"], -) -def test_integration_rejects_invalid_request_criteria( - integration: FastAPISQLAlchemyIntegration, - method_name: str, -) -> None: - """Rejects non-RequestCriteria values at public entrypoints.""" - method = getattr(integration, method_name) - - with pytest.raises(TypeError, match="criteria must be a RequestCriteria"): - method(User, cast("Any", "invalid")) - - def test_paginated_select_carries_list_and_count_statements() -> None: """Carries the list and count statements without extra validation.""" list_statement = select(User) @@ -572,43 +430,3 @@ def test_resource_dependency_respects_explicit_max_page_size( resource = integration.resource(User, max_page_size=50) assert resource.criteria_config.max_page_size == 50 - - -@pytest.mark.parametrize( - ("kwargs", "pattern"), - [ - pytest.param( - {"query_parameter_name": ""}, - "must not be empty", - id="blank-query-parameter-name", - ), - pytest.param( - {"sort_parameter_name": ""}, - "must not be empty", - id="blank-sort-parameter-name", - ), - pytest.param( - {"page_parameter_name": ""}, - "must not be empty", - id="blank-page-parameter-name", - ), - pytest.param( - {"size_parameter_name": ""}, - "must not be empty", - id="blank-size-parameter-name", - ), - pytest.param( - {"max_page_size": 0}, - "max_page_size", - id="invalid-max-page-size", - ), - ], -) -def test_resource_rejects_invalid_explicit_config_overrides( - integration: FastAPISQLAlchemyIntegration, - kwargs: dict[str, object], - pattern: str, -) -> None: - """Rejects explicit invalid resource config instead of falling back.""" - with pytest.raises((TypeError, ValueError), match=pattern): - integration.resource(User, **cast("Any", kwargs)) From 83bd04e1f3c74d345af90799841108549b919e6f Mon Sep 17 00:00:00 2001 From: Lucas Christian Date: Sat, 11 Jul 2026 11:33:40 -0300 Subject: [PATCH 3/5] docs: update with integration --- README.md | 59 +++++++++++++++++++++++++++----- docs/index.md | 36 ++++++++++++++----- docs/quickstart.md | 40 +++++++++++++++++++++- docs/reference/api.md | 2 +- docs/testing.md | 1 - docs/usage/fastapi-sqlalchemy.md | 24 ++++++++++--- 6 files changed, 137 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 70f9880..1bff842 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,11 @@ through a language frontend, semantic binding, and pluggable backend lowering - making it easy to expose complex query capabilities in your API without coupling to a specific ORM or framework. +For FastAPI applications using SQLAlchemy, the recommended entry point is +`FastAPISQLAlchemyIntegration`. It provides route-ready dependencies and +declarative resources; the lower-level query, adapter, and ORM APIs remain +available when more control is needed. + **Current backends:** SQLAlchemy 2.0 **Current framework adapters:** FastAPI **Planned:** Django ORM, SQLModel, Flask @@ -38,9 +43,41 @@ language. ## Quickstart ```bash -pip install pyrsql[sqlalchemy] +pip install pyrsql[fastapi,sqlalchemy] ``` +```python +from typing import Annotated, Any + +from fastapi import Depends, FastAPI + +from pyrsql.integrations.fastapi import FastAPISQLAlchemyIntegration + +app = FastAPI() +integration = FastAPISQLAlchemyIntegration() +users = integration.resource( + User, + filterable_fields={"id", "name"}, + sortable_fields={"name"}, + default_sort="name,asc", + max_page_size=100, +) + +@app.get("/users") +def list_users( + stmt: Annotated[Any, Depends(users.select_dependency())], +): + return {"sql": str(stmt)} +``` + +Clients can now use `filter`, `sort`, `page`, and `size` query parameters. +See [FastAPI + SQLAlchemy](https://wskr00.github.io/pyrsql/usage/fastapi-sqlalchemy/) +for count, pagination, custom base statements, error behavior, and async use. + +### Lower-level SQLAlchemy API + +Use the direct API when you do not need a FastAPI route dependency: + ```python import pyrsql from sqlalchemy import select @@ -49,13 +86,13 @@ from pyrsql.orms.sqlalchemy import SQLAlchemyORM orm = SQLAlchemyORM() stmt = select(User) -# Filter: name equals "demo", company name contains "acme" -stmt = pyrsql.parse("name==demo;company.name==acme*").apply(stmt, User, orm=orm) +stmt = pyrsql.parse("name==demo;company.name==acme*").apply( + stmt, + User, + orm=orm, +) -# Sort: by name ascending, then company name descending stmt = pyrsql.Sort.parse("name,asc;company.name,desc").apply(stmt, User, orm=orm) - -# Paginate: page 0, 25 items per page stmt = pyrsql.PageRequest.of(0, 25).apply(stmt, User, orm=orm) ``` @@ -133,6 +170,9 @@ options = QueryOptions(join_hints={"User.company": JoinHint.LEFT}) ### FastAPI Integration +Use this adapter directly when you need only parsed `RequestCriteria`; for +SQLAlchemy routes, prefer the integration below. + ```python from fastapi import Depends, FastAPI from pyrsql.adapters.fastapi import criteria_dependency @@ -146,10 +186,11 @@ def list_items(criteria = Depends(dependency)): ``` - Auto-extracts `filter`, `sort`, `page`, `size` from query params -- `HTTP 422` with structured diagnostics on parse/semantic errors +- Structured `HTTP 400` parse errors and `HTTP 422` semantic errors - OpenAPI examples from configuration - One-based paging support - Custom query parameter names +- Optional repeated sort parameters through `SortParameterFormat.REPEATED` ### FastAPI + SQLAlchemy Integration @@ -164,7 +205,7 @@ def list_users(stmt = Depends(integration.select_dependency(User))): ``` - `select_dependency`, `count_select_dependency`, `paginated_select_dependency` -- Declarative `resource()` with auto-generated OpenAPI examples +- Declarative `resource()` with auto-generated OpenAPI examples and field policies - `applier_dependency` for custom base statements - Compatible with both sync `Session` and async `AsyncSession` execution - FastAPI parse and page-validation failures become structured `HTTP 400` payloads @@ -172,7 +213,7 @@ def list_users(stmt = Depends(integration.select_dependency(User))): ### Concurrency and Validation -- Shared integration and ORM metadata caches are protected for free-threaded execution +- Shared base-select and ORM metadata caches are protected for free-threaded execution - Async support is validated for adapter, ORM, and integration flows - Dedicated async, free-threaded, and security test suites validate these flows diff --git a/docs/index.md b/docs/index.md index 69277b3..d423c5b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -10,20 +10,38 @@ capabilities in your API without coupling to a specific ORM or framework. ## Install ```bash -pip install pyrsql[sqlalchemy] +pip install pyrsql[fastapi,sqlalchemy] ``` -## First query +## Recommended: FastAPI + SQLAlchemy ```python -import pyrsql -from sqlalchemy import select -from pyrsql.orms.sqlalchemy import SQLAlchemyORM +from typing import Annotated, Any -orm = SQLAlchemyORM() -stmt = pyrsql.parse("name==demo;age=gt=18").apply(select(User), User, orm=orm) +from fastapi import Depends, FastAPI + +from pyrsql.integrations.fastapi import FastAPISQLAlchemyIntegration + +app = FastAPI() +integration = FastAPISQLAlchemyIntegration() +users = integration.resource( + User, + filterable_fields={"id", "name"}, + sortable_fields={"name"}, + default_sort="name,asc", +) + +@app.get("/users") +def list_users( + stmt: Annotated[Any, Depends(users.select_dependency())], +): + return {"sql": str(stmt)} ``` +The integration parses request criteria, applies SQLAlchemy filters/sorting/ +pagination, and exposes a route-ready dependency. It does not execute database +I/O. + ## Key features - **ORM-neutral core** - `Query`, `Sort`, `PageRequest` have zero ORM dependencies @@ -43,11 +61,11 @@ stmt = pyrsql.parse("name==demo;age=gt=18").apply(select(User), User, orm=orm) ## Integration options -- **FastAPI adapter** - parse `filter`, `sort`, `page`, and `size` into - `RequestCriteria` - **FastAPI + SQLAlchemy integration** - route-ready dependencies, declarative `resource()` helpers, count/paginated bundles, and normalized backend error translation +- **FastAPI adapter** - parse `filter`, `sort`, `page`, and `size` into + `RequestCriteria` when you need criteria without SQLAlchemy integration ## Next steps diff --git a/docs/quickstart.md b/docs/quickstart.md index 8de7b81..ad44428 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -9,6 +9,44 @@ pip install pyrsql[fastapi] # with FastAPI pip install pyrsql[fastapi,sqlalchemy] # both ``` +## Recommended: FastAPI + SQLAlchemy + +For a FastAPI application backed by SQLAlchemy, start with the integration. +It owns the public query contract for one model and gives routes a ready-to-use +SQLAlchemy statement. + +```python +from typing import Annotated, Any + +from fastapi import Depends, FastAPI + +from pyrsql.integrations.fastapi import FastAPISQLAlchemyIntegration + +app = FastAPI() +integration = FastAPISQLAlchemyIntegration() +users = integration.resource( + User, + filterable_fields={"id", "name"}, + sortable_fields={"name"}, + default_sort="name,asc", +) + +@app.get("/users") +def list_users( + stmt: Annotated[Any, Depends(users.select_dependency())], +): + return {"sql": str(stmt)} +``` + +Clients can use `filter`, `sort`, `page`, and `size`. The integration also +offers count and paginated dependencies. See [FastAPI + SQLAlchemy](usage/fastapi-sqlalchemy.md) +for the complete route API. + +## Building blocks + +The following sections show the lower-level APIs used by the integration. They +are useful for non-FastAPI applications or when you need direct control. + ## Filter ```python @@ -83,7 +121,7 @@ Parse and page-validation errors become structured `HTTP 400` responses. Semantic and backend integration errors become structured `HTTP 422` responses. -## FastAPI + SQLAlchemy +## Direct integration dependency ```python from typing import Annotated, Any diff --git a/docs/reference/api.md b/docs/reference/api.md index 6aed143..9404e4c 100644 --- a/docs/reference/api.md +++ b/docs/reference/api.md @@ -31,7 +31,7 @@ ## Subpackages -- `pyrsql.adapters.fastapi` - FastAPI adapter (FastAPICriteriaConfig, RequestCriteria, CriteriaDependency) +- `pyrsql.adapters.fastapi` - FastAPI adapter (FastAPICriteriaConfig, RequestCriteria, CriteriaDependency, SortParameterFormat) - `pyrsql.integrations.fastapi` - FastAPI + SQLAlchemy (FastAPISQLAlchemyIntegration, FastAPISQLAlchemyResource) - `pyrsql.orms.sqlalchemy` - SQLAlchemy backend (SQLAlchemyORM) - `pyrsql.core` - ORM-neutral core types and options diff --git a/docs/testing.md b/docs/testing.md index 68c373a..32a5443 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -134,7 +134,6 @@ shared mutable state. In `pyrsql`, the main targets are: -- integration dependency caches - integration base-statement caches - SQLAlchemy model-introspection caches - resolver caches diff --git a/docs/usage/fastapi-sqlalchemy.md b/docs/usage/fastapi-sqlalchemy.md index c173fbf..576c77d 100644 --- a/docs/usage/fastapi-sqlalchemy.md +++ b/docs/usage/fastapi-sqlalchemy.md @@ -1,6 +1,6 @@ # FastAPI + SQLAlchemy -## Setup +## Recommended entry point ```python from pyrsql.integrations.fastapi import FastAPISQLAlchemyIntegration @@ -21,6 +21,23 @@ The integration works with: - synchronous SQLAlchemy execution through `Session` - asynchronous SQLAlchemy execution through `AsyncSession` +For routes that expose one stable model contract, prefer a declarative +resource. It keeps field policies, default ordering, page limits, and OpenAPI +examples next to the model they describe. + +```python +users = integration.resource( + User, + filterable_fields={"id", "name"}, + sortable_fields={"name"}, + default_sort="name,desc", + max_page_size=50, +) +``` + +Use `FastAPISQLAlchemyIntegration` directly for dynamic models or when a +service layer needs to build statements outside a route dependency. + ## Route-ready dependencies ```python @@ -140,9 +157,8 @@ This avoids leaking raw SQLAlchemy exceptions as `500` responses. ## Free-threaded behavior -The integration caches dependency callables and base `select(model)` statements -per model. Those shared caches are published with explicit synchronization for -free-threaded execution. +The integration caches base `select(model)` statements per model. That shared +cache is published with explicit synchronization for free-threaded execution. ## Async note From 0e42e3c172ba5d60cb7976cad5f499a35164f2ab Mon Sep 17 00:00:00 2001 From: Lucas Christian Date: Sat, 11 Jul 2026 11:34:53 -0300 Subject: [PATCH 4/5] chore: bump version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 58ff06f..de41604 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pyrsql" -version = "0.0.3" +version = "0.0.4" description = "Compiler-oriented RSQL query engine for safe, typed, and extensible Python ORM-backed APIs." readme = "README.md" requires-python = ">=3.10" From f44ac818cea87266b90fe1b76f6346c98184d2b6 Mon Sep 17 00:00:00 2001 From: Lucas Christian Date: Sat, 11 Jul 2026 11:38:50 -0300 Subject: [PATCH 5/5] fix: docstring starts with verb --- src/pyrsql/core/source_issues.py | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyrsql/core/source_issues.py b/src/pyrsql/core/source_issues.py index 03bfec2..261fae4 100644 --- a/src/pyrsql/core/source_issues.py +++ b/src/pyrsql/core/source_issues.py @@ -41,7 +41,7 @@ class SourceError(ValueError): @property def diagnostic(self) -> SourceDiagnostic: - """Returns the structured diagnostic for this error. + """Structured diagnostic for this error. Returns: The structured source diagnostic. diff --git a/uv.lock b/uv.lock index 3f8f15e..682ddd8 100644 --- a/uv.lock +++ b/uv.lock @@ -1167,7 +1167,7 @@ wheels = [ [[package]] name = "pyrsql" -version = "0.0.3" +version = "0.0.4" source = { editable = "." } dependencies = [ { name = "msgspec" },