diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9b8fb29..8d6a2b1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,12 +11,12 @@ repos: args: [--markdown-linebreak-ext=md] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.6.9 + rev: v0.9.9 hooks: - id: ruff - id: ruff-format - repo: https://github.com/RobertCraigie/pyright-python - rev: v1.1.384 + rev: v1.1.396 hooks: - id: pyright diff --git a/examples/base/pydantic_usage.py b/examples/base/msgspec_usage.py similarity index 90% rename from examples/base/pydantic_usage.py rename to examples/base/msgspec_usage.py index 305c750..5c67c81 100644 --- a/examples/base/pydantic_usage.py +++ b/examples/base/msgspec_usage.py @@ -1,11 +1,11 @@ # ruff: noqa: T201 -from pydantic import BaseModel +import msgspec from contiguity import Base -# Create a Pydantic model for the item. -class MyItem(BaseModel): +# Create a msgspec struct for the item. +class MyItem(msgspec.Struct): key: str # Make sure to include the key field. name: str age: int @@ -13,7 +13,7 @@ class MyItem(BaseModel): # Create a Base instance. -# Static type checking will work with the Pydantic model. +# Static type checking will work with the msgspec struct. db = Base("members", item_type=MyItem) # Put an item with a specific key. diff --git a/pyproject.toml b/pyproject.toml index d67ec4c..36b9c95 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,8 +35,8 @@ classifiers = [ ] dependencies = [ "httpx>=0.27.2", + "msgspec>=0.19.0", "phonenumbers>=8.13.47,<9.0.0", - "pydantic>=2.9.0,<3.0.0", "typing-extensions>=4.12.2,<5.0.0", ] @@ -67,6 +67,9 @@ target-version = "py39" select = ["ALL"] ignore = ["A", "D", "T201"] +[tool.ruff.lint.per-file-ignores] +"tests/**" = ["S101"] + [tool.pyright] venvPath = "." venv = ".venv" diff --git a/src/contiguity/__init__.py b/src/contiguity/__init__.py index b6658d7..e1b08ea 100644 --- a/src/contiguity/__init__.py +++ b/src/contiguity/__init__.py @@ -47,20 +47,20 @@ def login(token: str, /, *, debug: bool = False) -> Contiguity: __all__ = ( - "AsyncBase", - "Contiguity", - "Send", - "Verify", - "EmailAnalytics", - "Quota", "OTP", - "Template", + "AsyncBase", "Base", "BaseItem", + "Contiguity", + "EmailAnalytics", "InvalidKeyError", "ItemConflictError", "ItemNotFoundError", "QueryResponse", + "Quota", + "Send", + "Template", + "Verify", "login", ) __version__ = "2.0.0" diff --git a/src/contiguity/_auth.py b/src/contiguity/_auth.py index 8e533e4..9717420 100644 --- a/src/contiguity/_auth.py +++ b/src/contiguity/_auth.py @@ -1,9 +1,7 @@ -from __future__ import annotations - import os -def _get_env_var(var_name: str, friendly_name: str | None = None) -> str: +def _get_env_var(var_name: str, friendly_name: "str | None" = None) -> str: value = os.getenv(var_name, "") if not value: msg = f"no {friendly_name or var_name} provided" diff --git a/src/contiguity/_client.py b/src/contiguity/_client.py index 5ffbbdb..0b54afa 100644 --- a/src/contiguity/_client.py +++ b/src/contiguity/_client.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from httpx import AsyncClient as HttpxAsyncClient from httpx import Client as HttpxClient @@ -12,10 +10,10 @@ class ApiError(Exception): class ApiClient(HttpxClient): def __init__( - self: ApiClient, + self, *, base_url: str = "https://api.contiguity.co", - api_key: str | None = None, + api_key: "str | None" = None, timeout: int = 5, ) -> None: if not api_key: @@ -33,10 +31,10 @@ def __init__( class AsyncApiClient(HttpxAsyncClient): def __init__( - self: AsyncApiClient, + self, *, base_url: str = "https://api.contiguity.co", - api_key: str | None = None, + api_key: "str | None" = None, timeout: int = 5, ) -> None: if not api_key: diff --git a/src/contiguity/_common.py b/src/contiguity/_common.py index f0d3392..abd5ec7 100644 --- a/src/contiguity/_common.py +++ b/src/contiguity/_common.py @@ -1,7 +1,7 @@ -from pydantic import BaseModel +import msgspec -class Crumbs(BaseModel): +class Crumbs(msgspec.Struct): plan: str quota: int type: str diff --git a/src/contiguity/base/async_base.py b/src/contiguity/base/async_base.py index 3a0c3c1..452486b 100644 --- a/src/contiguity/base/async_base.py +++ b/src/contiguity/base/async_base.py @@ -8,9 +8,8 @@ from typing import TYPE_CHECKING, Generic, Literal, overload from warnings import warn +import msgspec from httpx import HTTPStatusError -from pydantic import BaseModel, TypeAdapter -from pydantic import JsonValue as DataType from typing_extensions import deprecated from contiguity._auth import get_data_key, get_project_id @@ -18,6 +17,7 @@ from .common import ( UNSET, + DataType, DefaultItemT, ItemT, QueryResponse, @@ -33,7 +33,6 @@ if TYPE_CHECKING: from httpx import Response as HttpxResponse - from typing_extensions import Self class AsyncBase(Generic[ItemT]): @@ -42,7 +41,7 @@ class AsyncBase(Generic[ItemT]): @overload def __init__( - self: Self, + self, name: str, /, *, @@ -57,7 +56,7 @@ def __init__( @overload @deprecated("The `project_key` parameter has been renamed to `data_key`.") def __init__( - self: Self, + self, name: str, /, *, @@ -70,7 +69,7 @@ def __init__( ) -> None: ... def __init__( # noqa: PLR0913 - self: Self, + self, name: str, /, *, @@ -80,7 +79,7 @@ def __init__( # noqa: PLR0913 project_id: str | None = None, host: str | None = None, api_version: str = "v1", - json_decoder: type[json.JSONDecoder] = json.JSONDecoder, # Only used when item_type is not a Pydantic model. + json_decoder: type[json.JSONDecoder] = json.JSONDecoder, # Only used when item_type is not a msgspec struct. ) -> None: if not name: msg = f"invalid Base name '{name}'" @@ -102,7 +101,7 @@ def __init__( # noqa: PLR0913 @overload def _response_as_item_type( - self: Self, + self, response: HttpxResponse, /, *, @@ -110,7 +109,7 @@ def _response_as_item_type( ) -> ItemT: ... @overload def _response_as_item_type( - self: Self, + self, response: HttpxResponse, /, *, @@ -118,7 +117,7 @@ def _response_as_item_type( ) -> Sequence[ItemT]: ... def _response_as_item_type( - self: Self, + self, response: HttpxResponse, /, *, @@ -130,12 +129,12 @@ def _response_as_item_type( raise ApiError(exc.response.text) from exc if self.item_type: if sequence: - return TypeAdapter(Sequence[self.item_type]).validate_json(response.content) - return TypeAdapter(self.item_type).validate_json(response.content) + return msgspec.json.decode(response.content, type=Sequence[self.item_type]) + return msgspec.json.decode(response.content, type=self.item_type) return response.json(cls=self.json_decoder) def _insert_expires_attr( - self: Self, + self, item: ItemT | Mapping[str, DataType], expire_in: int | None = None, expire_at: TimestampType | None = None, @@ -144,7 +143,7 @@ def _insert_expires_attr( msg = "cannot use both expire_in and expire_at" raise ValueError(msg) - item_dict = item.model_dump() if isinstance(item, BaseModel) else dict(item) + item_dict = msgspec.structs.asdict(item) if isinstance(item, msgspec.Struct) else dict(item) if not expire_in and not expire_at: return item_dict @@ -160,16 +159,16 @@ def _insert_expires_attr( return item_dict @overload - async def get(self: Self, key: str, /) -> ItemT | None: ... + async def get(self, key: str, /) -> ItemT | None: ... @overload - async def get(self: Self, key: str, /, *, default: ItemT) -> ItemT: ... + async def get(self, key: str, /, *, default: ItemT) -> ItemT: ... @overload - async def get(self: Self, key: str, /, *, default: DefaultItemT) -> ItemT | DefaultItemT: ... + async def get(self, key: str, /, *, default: DefaultItemT) -> ItemT | DefaultItemT: ... async def get( - self: Self, + self, key: str, /, *, @@ -189,7 +188,7 @@ async def get( return self._response_as_item_type(response, sequence=False) - async def delete(self: Self, key: str, /) -> None: + async def delete(self, key: str, /) -> None: """Delete an item from the Base.""" key = check_key(key) response = await self._client.delete(f"/items/{key}") @@ -199,7 +198,7 @@ async def delete(self: Self, key: str, /) -> None: raise ApiError(exc.response.text) from exc async def insert( - self: Self, + self, item: ItemT, /, *, @@ -218,7 +217,7 @@ async def insert( return returned_item[0] async def put( - self: Self, + self, *items: ItemT, expire_in: int | None = None, expire_at: TimestampType | None = None, @@ -239,7 +238,7 @@ async def put( @deprecated("This method will be removed in a future release. You can pass multiple items to `put`.") async def put_many( - self: Self, + self, items: Sequence[ItemT], /, *, @@ -249,7 +248,7 @@ async def put_many( return await self.put(*items, expire_in=expire_in, expire_at=expire_at) async def update( - self: Self, + self, updates: Mapping[str, DataType | UpdateOperation], /, *, @@ -273,14 +272,14 @@ async def update( expire_at=expire_at, ) - response = await self._client.patch(f"/items/{key}", json={"updates": payload.model_dump()}) + response = await self._client.patch(f"/items/{key}", json={"updates": msgspec.structs.asdict(payload)}) if response.status_code == HTTPStatus.NOT_FOUND: raise ItemNotFoundError(key) return self._response_as_item_type(response, sequence=False) async def query( - self: Self, + self, *queries: QueryType, limit: int = 1000, last: str | None = None, @@ -302,15 +301,11 @@ async def query( response.raise_for_status() except HTTPStatusError as exc: raise ApiError(exc.response.text) from exc - query_response = QueryResponse[ItemT].model_validate_json(response.content) - if self.item_type: - # HACK: Pydantic model_validate_json doesn't validate Sequence[ItemT] properly. # noqa: FIX004 - query_response.items = TypeAdapter(Sequence[self.item_type]).validate_python(query_response.items) - return query_response + return msgspec.json.decode(response.content, type=QueryResponse[ItemT]) @deprecated("This method has been renamed to `query` and will be removed in a future release.") async def fetch( - self: Self, + self, *queries: QueryType, limit: int = 1000, last: str | None = None, diff --git a/src/contiguity/base/base.py b/src/contiguity/base/base.py index 819899d..a78c5b1 100644 --- a/src/contiguity/base/base.py +++ b/src/contiguity/base/base.py @@ -2,7 +2,7 @@ # - [ ] new docstrings # - [ ] test expiring items # - [ ] support dataclasses -# - [ ] support models for queries +# - [ ] support structs for queries # - [ ] examples # - [ ] add async # - [ ] add drive support @@ -17,9 +17,8 @@ from typing import TYPE_CHECKING, Generic, Literal, overload from warnings import warn +import msgspec from httpx import HTTPStatusError -from pydantic import BaseModel, TypeAdapter -from pydantic import JsonValue as DataType from typing_extensions import deprecated from contiguity._auth import get_data_key, get_project_id @@ -27,6 +26,7 @@ from .common import ( UNSET, + DataType, DefaultItemT, ItemT, QueryResponse, @@ -42,7 +42,6 @@ if TYPE_CHECKING: from httpx import Response as HttpxResponse - from typing_extensions import Self class Base(Generic[ItemT]): @@ -51,7 +50,7 @@ class Base(Generic[ItemT]): @overload def __init__( - self: Self, + self, name: str, /, *, @@ -66,7 +65,7 @@ def __init__( @overload @deprecated("The `project_key` parameter has been renamed to `data_key`.") def __init__( - self: Self, + self, name: str, /, *, @@ -79,7 +78,7 @@ def __init__( ) -> None: ... def __init__( # noqa: PLR0913 - self: Self, + self, name: str, /, *, @@ -89,7 +88,7 @@ def __init__( # noqa: PLR0913 project_id: str | None = None, host: str | None = None, api_version: str = "v1", - json_decoder: type[json.JSONDecoder] = json.JSONDecoder, # Only used when item_type is not a Pydantic model. + json_decoder: type[json.JSONDecoder] = json.JSONDecoder, # Only used when item_type is not a msgspec struct. ) -> None: if not name: msg = f"invalid Base name '{name}'" @@ -111,7 +110,7 @@ def __init__( # noqa: PLR0913 @overload def _response_as_item_type( - self: Self, + self, response: HttpxResponse, /, *, @@ -119,7 +118,7 @@ def _response_as_item_type( ) -> ItemT: ... @overload def _response_as_item_type( - self: Self, + self, response: HttpxResponse, /, *, @@ -127,7 +126,7 @@ def _response_as_item_type( ) -> Sequence[ItemT]: ... def _response_as_item_type( - self: Self, + self, response: HttpxResponse, /, *, @@ -139,12 +138,12 @@ def _response_as_item_type( raise ApiError(exc.response.text) from exc if self.item_type: if sequence: - return TypeAdapter(Sequence[self.item_type]).validate_json(response.content) - return TypeAdapter(self.item_type).validate_json(response.content) + return msgspec.json.decode(response.content, type=Sequence[self.item_type]) + return msgspec.json.decode(response.content, type=self.item_type) return response.json(cls=self.json_decoder) def _insert_expires_attr( - self: Self, + self, item: ItemT | Mapping[str, DataType], expire_in: int | None = None, expire_at: TimestampType | None = None, @@ -153,7 +152,7 @@ def _insert_expires_attr( msg = "cannot use both expire_in and expire_at" raise ValueError(msg) - item_dict = item.model_dump() if isinstance(item, BaseModel) else dict(item) + item_dict = msgspec.structs.asdict(item) if isinstance(item, msgspec.Struct) else dict(item) if not expire_in and not expire_at: return item_dict @@ -169,16 +168,16 @@ def _insert_expires_attr( return item_dict @overload - def get(self: Self, key: str, /) -> ItemT | None: ... + def get(self, key: str, /) -> ItemT | None: ... @overload - def get(self: Self, key: str, /, *, default: ItemT) -> ItemT: ... + def get(self, key: str, /, *, default: ItemT) -> ItemT: ... @overload - def get(self: Self, key: str, /, *, default: DefaultItemT) -> ItemT | DefaultItemT: ... + def get(self, key: str, /, *, default: DefaultItemT) -> ItemT | DefaultItemT: ... def get( - self: Self, + self, key: str, /, *, @@ -198,7 +197,7 @@ def get( return self._response_as_item_type(response, sequence=False) - def delete(self: Self, key: str, /) -> None: + def delete(self, key: str, /) -> None: """Delete an item from the Base.""" key = check_key(key) response = self._client.delete(f"/items/{key}") @@ -208,7 +207,7 @@ def delete(self: Self, key: str, /) -> None: raise ApiError(exc.response.text) from exc def insert( - self: Self, + self, item: ItemT, /, *, @@ -227,7 +226,7 @@ def insert( return returned_item[0] def put( - self: Self, + self, *items: ItemT, expire_in: int | None = None, expire_at: TimestampType | None = None, @@ -248,7 +247,7 @@ def put( @deprecated("This method will be removed in a future release. You can pass multiple items to `put`.") def put_many( - self: Self, + self, items: Sequence[ItemT], /, *, @@ -258,7 +257,7 @@ def put_many( return self.put(*items, expire_in=expire_in, expire_at=expire_at) def update( - self: Self, + self, updates: Mapping[str, DataType | UpdateOperation], /, *, @@ -282,14 +281,14 @@ def update( expire_at=expire_at, ) - response = self._client.patch(f"/items/{key}", json={"updates": payload.model_dump()}) + response = self._client.patch(f"/items/{key}", json={"updates": msgspec.structs.asdict(payload)}) if response.status_code == HTTPStatus.NOT_FOUND: raise ItemNotFoundError(key) return self._response_as_item_type(response, sequence=False) def query( - self: Self, + self, *queries: QueryType, limit: int = 1000, last: str | None = None, @@ -311,15 +310,11 @@ def query( response.raise_for_status() except HTTPStatusError as exc: raise ApiError(exc.response.text) from exc - query_response = QueryResponse[ItemT].model_validate_json(response.content) - if self.item_type: - # HACK: Pydantic model_validate_json doesn't validate Sequence[ItemT] properly. # noqa: FIX004 - query_response.items = TypeAdapter(Sequence[self.item_type]).validate_python(query_response.items) - return query_response + return msgspec.json.decode(response.content, type=QueryResponse[ItemT]) @deprecated("This method has been renamed to `query` and will be removed in a future release.") def fetch( - self: Self, + self, *queries: QueryType, limit: int = 1000, last: str | None = None, diff --git a/src/contiguity/base/common.py b/src/contiguity/base/common.py index dc0b61e..74fc2a9 100644 --- a/src/contiguity/base/common.py +++ b/src/contiguity/base/common.py @@ -1,21 +1,17 @@ -from __future__ import annotations - from collections.abc import Mapping, Sequence from datetime import datetime -from typing import Any, Generic, TypeVar, Union +from typing import Generic, TypeVar, Union from urllib.parse import quote -from pydantic import BaseModel -from pydantic import JsonValue as DataType -from typing_extensions import Self +import msgspec from .exceptions import InvalidKeyError TimestampType = Union[int, datetime] +DataType = Union[list["DataType"], dict[str, "DataType"], str, bool, int, float, None] QueryType = Mapping[str, DataType] -ItemType = Union[Mapping[str, Any], BaseModel] -ItemT = TypeVar("ItemT", bound=ItemType) +ItemT = TypeVar("ItemT", bound=msgspec.Struct) DefaultItemT = TypeVar("DefaultItemT") @@ -26,13 +22,13 @@ class Unset: UNSET = Unset() -class BaseItem(BaseModel): +class BaseItem(msgspec.Struct): key: str -class QueryResponse(BaseModel, Generic[ItemT]): +class QueryResponse(msgspec.Struct, Generic[ItemT]): count: int = 0 - last_key: Union[str, None] = None # noqa: UP007 Pydantic doesn't support `X | Y` syntax in Python 3.9. + last_key: Union[str, None] = None items: Sequence[ItemT] = [] @@ -45,12 +41,12 @@ class Trim(UpdateOperation): class Increment(UpdateOperation): - def __init__(self: Increment, value: int = 1, /) -> None: + def __init__(self, value: int = 1, /) -> None: self.value = value class Append(UpdateOperation): - def __init__(self: Append, value: DataType, /) -> None: + def __init__(self, value: DataType, /) -> None: if isinstance(value, (list, tuple)): self.value = value else: @@ -79,7 +75,7 @@ def prepend(value: DataType, /) -> Prepend: return Prepend(value) -class UpdatePayload(BaseModel): +class UpdatePayload(msgspec.Struct): set: dict[str, DataType] = {} increment: dict[str, int] = {} append: dict[str, Sequence[DataType]] = {} @@ -87,7 +83,7 @@ class UpdatePayload(BaseModel): delete: list[str] = [] @classmethod - def from_updates_mapping(cls: type[Self], updates: Mapping[str, DataType | UpdateOperation], /) -> Self: + def from_updates_mapping(cls, updates: Mapping[str, "DataType | UpdateOperation"], /) -> "UpdatePayload": set = {} increment = {} append = {} diff --git a/src/contiguity/otp.py b/src/contiguity/otp.py index 7e00cfd..d5d05c8 100644 --- a/src/contiguity/otp.py +++ b/src/contiguity/otp.py @@ -1,16 +1,11 @@ -from __future__ import annotations - from enum import Enum from http import HTTPStatus -from typing import TYPE_CHECKING +import msgspec import phonenumbers -from pydantic import BaseModel - -from ._common import Crumbs # noqa: TCH001 Pydantic needs this to be outside of the TYPE_CHECKING block. -if TYPE_CHECKING: - from ._client import ApiClient +from ._client import ApiClient +from ._common import Crumbs class OTPLanguage(str, Enum): @@ -52,18 +47,18 @@ class OTPLanguage(str, Enum): VIETNAMESE = "vi" -class OTPSendResponse(BaseModel): +class OTPSendResponse(msgspec.Struct): message: str crumbs: Crumbs otp_id: str -class OTPResendResponse(BaseModel): +class OTPResendResponse(msgspec.Struct): message: str resent: bool -class OTPVerifyResponse(BaseModel): +class OTPVerifyResponse(msgspec.Struct): message: str verified: bool @@ -78,7 +73,7 @@ def send( to: str, /, *, - name: str | None = None, + name: "str | None" = None, language: OTPLanguage = OTPLanguage.ENGLISH, ) -> OTPSendResponse: e164 = phonenumbers.format_number(phonenumbers.parse(to), phonenumbers.PhoneNumberFormat.E164) @@ -91,7 +86,7 @@ def send( "name": name, }, ) - data = OTPSendResponse.model_validate_json(response.content) + data = msgspec.json.decode(response.content, type=OTPSendResponse) if response.status_code != HTTPStatus.OK: msg = f"Contiguity couldn't send your OTP. Received: {response.status_code} with reason: '{data.message}'" @@ -108,12 +103,11 @@ def resend(self, otp_id: str, /) -> OTPResendResponse: "otp_id": otp_id, }, ) - data = OTPResendResponse.model_validate_json(response.content) + data = msgspec.json.decode(response.content, type=OTPResendResponse) if response.status_code != HTTPStatus.OK: msg = ( - "Contiguity couldn't resend your OTP." - f" Received: {response.status_code} with reason: '{data.message}'" + f"Contiguity couldn't resend your OTP. Received: {response.status_code} with reason: '{data.message}'" ) raise ValueError(msg) if self.debug: @@ -121,7 +115,7 @@ def resend(self, otp_id: str, /) -> OTPResendResponse: return data - def verify(self, otp: int | str, /, *, otp_id: str) -> OTPVerifyResponse: + def verify(self, otp: "int | str", /, *, otp_id: str) -> OTPVerifyResponse: response = self._client.post( "/otp/verify", json={ @@ -129,12 +123,11 @@ def verify(self, otp: int | str, /, *, otp_id: str) -> OTPVerifyResponse: "otp_id": otp_id, }, ) - data = OTPVerifyResponse.model_validate_json(response.content) + data = msgspec.json.decode(response.content, type=OTPVerifyResponse) if response.status_code != HTTPStatus.OK: msg = ( - "Contiguity couldn't verify your OTP." - f" Received: {response.status_code} with reason: '{data.message}'" + f"Contiguity couldn't verify your OTP. Received: {response.status_code} with reason: '{data.message}'" ) raise ValueError(msg) if self.debug: diff --git a/src/contiguity/send.py b/src/contiguity/send.py index d5f9c63..bc61e5f 100644 --- a/src/contiguity/send.py +++ b/src/contiguity/send.py @@ -1,23 +1,19 @@ -from __future__ import annotations - from http import HTTPStatus -from typing import TYPE_CHECKING, overload +from typing import overload +import msgspec import phonenumbers -from pydantic import BaseModel - -from ._common import Crumbs # noqa: TCH001 Pydantic needs this to be outside of the TYPE_CHECKING block. -if TYPE_CHECKING: - from ._client import ApiClient +from ._client import ApiClient +from ._common import Crumbs -class TextResponse(BaseModel): +class TextResponse(msgspec.Struct): message: str crumbs: Crumbs -class EmailResponse(BaseModel): +class EmailResponse(msgspec.Struct): message: str crumbs: Crumbs @@ -54,7 +50,7 @@ def text(self, to: str, message: str) -> TextResponse: "message": message, }, ) - data = TextResponse.model_validate_json(response.content) + data = msgspec.json.decode(response.content, type=TextResponse) if response.status_code != HTTPStatus.OK: msg = ( @@ -99,8 +95,8 @@ def email( # noqa: PLR0913 subject: str, reply_to: str = "", cc: str = "", - text: str | None = None, - html: str | None = None, + text: "str | None" = None, + html: "str | None" = None, ) -> EmailResponse: """ Send an email. @@ -134,12 +130,11 @@ def email( # noqa: PLR0913 email_payload["cc"] = cc response = self._client.post("/send/email", json=email_payload) - data = EmailResponse.model_validate_json(response.content) + data = msgspec.json.decode(response.content, type=EmailResponse) if response.status_code != HTTPStatus.OK: msg = ( - "Contiguity couldn't send your email." - f" Received: {response.status_code} with reason: '{data.message}'" + f"Contiguity couldn't send your email. Received: {response.status_code} with reason: '{data.message}'" ) raise ValueError(msg) if self.debug: diff --git a/src/contiguity/template.py b/src/contiguity/template.py index b15b052..64aa345 100644 --- a/src/contiguity/template.py +++ b/src/contiguity/template.py @@ -1,12 +1,9 @@ -from __future__ import annotations - from pathlib import Path - -from typing_extensions import Never +from typing import NoReturn class Template: - def local(self, file_path: Path | str) -> str: + def local(self, file_path: "Path | str") -> str: try: file_path = Path(file_path) return file_path.read_text() @@ -14,6 +11,6 @@ def local(self, file_path: Path | str) -> str: msg = "reading files is not supported in the this environment" raise ValueError(msg) from exc - async def online(self, file_path: str) -> Never: + async def online(self, file_path: str) -> NoReturn: # Coming soon raise NotImplementedError diff --git a/tests/base/__init__.py b/tests/base/__init__.py index e69de29..4262496 100644 --- a/tests/base/__init__.py +++ b/tests/base/__init__.py @@ -0,0 +1,76 @@ +# ruff: noqa: S311 +import random +from collections.abc import Mapping +from typing import Callable, TypedDict, TypeVar + +import msgspec + +from contiguity.base.common import DataType +from tests import random_string + +DictItemType = Mapping[str, DataType] +T = TypeVar("T", DictItemType, dict) + + +class ItemStruct(msgspec.Struct): + key: str = "test_key" + field1: int = random.randint(1, 1000) + field2: str = random_string() + field3: int = 1 + field4: int = 0 + field5: list[str] = msgspec.field(default_factory=lambda: ["foo", "bar"]) + field6: list[int] = msgspec.field(default_factory=lambda: [1, 2]) + field7: dict[str, str] = msgspec.field(default_factory=lambda: {"foo": "bar"}) + + +class ItemTypedDict(TypedDict): + key: str + field1: int + field2: str + field3: int + field4: int + field5: list[str] + field6: list[int] + field7: dict[str, str] + + +def create_test_item_typed_dict( # noqa: PLR0913 + key: str = "test_key", + field1: int = random.randint(1, 1000), + field2: str = random_string(), + field3: int = 1, + field4: int = 0, + field5: "list[str] | None" = None, + field6: "list[int] | None" = None, + field7: "dict[str, str] | None" = None, +) -> ItemTypedDict: + return ItemTypedDict( + key=key, + field1=field1, + field2=field2, + field3=field3, + field4=field4, + field5=field5 or ["foo", "bar"], + field6=field6 or [1, 2], + field7=field7 or {"foo": "bar"}, + ) + + +# Yes, this is a factory function to create an item factory function. +def test_item_factory_factory(type: type[T], /) -> Callable[..., T]: + def test_item_factory(**kwargs: DataType) -> T: + kwargs.setdefault("key", "test_key") + kwargs.setdefault("field1", random.randint(1, 1000)) + kwargs.setdefault("field2", random_string()) + kwargs.setdefault("field3", 1) + kwargs.setdefault("field4", 0) + kwargs.setdefault("field5", ["foo", "bar"]) + kwargs.setdefault("field6", [1, 2]) + kwargs.setdefault("field7", {"foo": "bar"}) + return type(**kwargs) + + return test_item_factory + + +create_test_item_dict = test_item_factory_factory(dict) +create_test_item_dict_typed = test_item_factory_factory(DictItemType) diff --git a/tests/base/test_async_base_dict_typed.py b/tests/base/test_async_base_dict_typed.py index fd65184..c5b320f 100644 --- a/tests/base/test_async_base_dict_typed.py +++ b/tests/base/test_async_base_dict_typed.py @@ -1,31 +1,16 @@ -# ruff: noqa: S101, S311, PLR2004 -import random -from collections.abc import AsyncGenerator, Mapping +from collections.abc import AsyncGenerator from typing import Any import pytest from dotenv import load_dotenv -from pydantic import JsonValue from contiguity import AsyncBase, InvalidKeyError, ItemConflictError, ItemNotFoundError, QueryResponse from tests import random_string +from tests.base import DictItemType +from tests.base import create_test_item_dict as create_test_item load_dotenv() -DictItemType = Mapping[str, JsonValue] - - -def create_test_item(**kwargs: JsonValue) -> DictItemType: - kwargs.setdefault("key", "test_key") - kwargs.setdefault("field1", random.randint(1, 1000)) - kwargs.setdefault("field2", random_string()) - kwargs.setdefault("field3", 1) - kwargs.setdefault("field4", 0) - kwargs.setdefault("field5", ["foo", "bar"]) - kwargs.setdefault("field6", [1, 2]) - kwargs.setdefault("field7", {"foo": "bar"}) - return kwargs - @pytest.fixture async def base() -> AsyncGenerator[AsyncBase[DictItemType], Any]: diff --git a/tests/base/test_async_base_dict_untyped.py b/tests/base/test_async_base_dict_untyped.py index 1de7a6f..83aae10 100644 --- a/tests/base/test_async_base_dict_untyped.py +++ b/tests/base/test_async_base_dict_untyped.py @@ -1,30 +1,16 @@ -# ruff: noqa: S101, S311, PLR2004 -import random from collections.abc import AsyncGenerator from typing import Any import pytest from dotenv import load_dotenv -from pydantic import JsonValue from contiguity import AsyncBase, InvalidKeyError, ItemConflictError, ItemNotFoundError, QueryResponse from tests import random_string +from tests.base import create_test_item_dict as create_test_item load_dotenv() -def create_test_item(**kwargs: JsonValue) -> dict: - kwargs.setdefault("key", "test_key") - kwargs.setdefault("field1", random.randint(1, 1000)) - kwargs.setdefault("field2", random_string()) - kwargs.setdefault("field3", 1) - kwargs.setdefault("field4", 0) - kwargs.setdefault("field5", ["foo", "bar"]) - kwargs.setdefault("field6", [1, 2]) - kwargs.setdefault("field7", {"foo": "bar"}) - return kwargs - - @pytest.fixture async def base() -> AsyncGenerator[AsyncBase, Any]: base = AsyncBase("test_base_dict_untyped") diff --git a/tests/base/test_async_base_model.py b/tests/base/test_async_base_struct.py similarity index 59% rename from tests/base/test_async_base_model.py rename to tests/base/test_async_base_struct.py index e0704de..6513913 100644 --- a/tests/base/test_async_base_model.py +++ b/tests/base/test_async_base_struct.py @@ -1,32 +1,19 @@ -# ruff: noqa: S101, S311, PLR2004 -import random from collections.abc import AsyncGenerator from typing import Any import pytest from dotenv import load_dotenv -from pydantic import BaseModel from contiguity import AsyncBase, InvalidKeyError, ItemConflictError, ItemNotFoundError, QueryResponse from tests import random_string +from tests.base import ItemStruct load_dotenv() -class TestItemModel(BaseModel): - key: str = "test_key" - field1: int = random.randint(1, 1000) - field2: str = random_string() - field3: int = 1 - field4: int = 0 - field5: list[str] = ["foo", "bar"] - field6: list[int] = [1, 2] - field7: dict[str, str] = {"foo": "bar"} - - @pytest.fixture -async def base() -> AsyncGenerator[AsyncBase[TestItemModel], Any]: - base = AsyncBase("test_base_model", item_type=TestItemModel) +async def base() -> AsyncGenerator[AsyncBase[ItemStruct], Any]: + base = AsyncBase("test_base_model", item_type=ItemStruct) for item in (await base.query()).items: await base.delete(item.key) yield base @@ -36,10 +23,10 @@ async def base() -> AsyncGenerator[AsyncBase[TestItemModel], Any]: def test_bad_base_name() -> None: with pytest.raises(ValueError, match="invalid Base name ''"): - AsyncBase("", item_type=TestItemModel) + AsyncBase("", item_type=ItemStruct) -async def test_bad_key(base: AsyncBase[TestItemModel]) -> None: +async def test_bad_key(base: AsyncBase[ItemStruct]) -> None: with pytest.raises(InvalidKeyError): await base.get("") with pytest.raises(InvalidKeyError): @@ -48,66 +35,66 @@ async def test_bad_key(base: AsyncBase[TestItemModel]) -> None: await base.update({"foo": "bar"}, key="") -async def test_get(base: AsyncBase[TestItemModel]) -> None: - item = TestItemModel() +async def test_get(base: AsyncBase[ItemStruct]) -> None: + item = ItemStruct() await base.insert(item) fetched_item = await base.get("test_key") assert fetched_item == item -async def test_get_nonexistent(base: AsyncBase[TestItemModel]) -> None: +async def test_get_nonexistent(base: AsyncBase[ItemStruct]) -> None: with pytest.warns(DeprecationWarning): assert await base.get("nonexistent_key") is None -async def test_get_default(base: AsyncBase[TestItemModel]) -> None: - for default_item in (None, "foo", 42, TestItemModel()): +async def test_get_default(base: AsyncBase[ItemStruct]) -> None: + for default_item in (None, "foo", 42, ItemStruct()): fetched_item = await base.get("nonexistent_key", default=default_item) assert fetched_item == default_item -async def test_delete(base: AsyncBase[TestItemModel]) -> None: - item = TestItemModel() +async def test_delete(base: AsyncBase[ItemStruct]) -> None: + item = ItemStruct() await base.insert(item) await base.delete("test_key") with pytest.warns(DeprecationWarning): assert await base.get("test_key") is None -async def test_insert(base: AsyncBase[TestItemModel]) -> None: - item = TestItemModel() +async def test_insert(base: AsyncBase[ItemStruct]) -> None: + item = ItemStruct() inserted_item = await base.insert(item) assert inserted_item == item -async def test_insert_existing(base: AsyncBase[TestItemModel]) -> None: - item = TestItemModel() +async def test_insert_existing(base: AsyncBase[ItemStruct]) -> None: + item = ItemStruct() await base.insert(item) with pytest.raises(ItemConflictError): await base.insert(item) -async def test_put(base: AsyncBase[TestItemModel]) -> None: - items = [TestItemModel(key=f"test_key_{i}") for i in range(3)] +async def test_put(base: AsyncBase[ItemStruct]) -> None: + items = [ItemStruct(key=f"test_key_{i}") for i in range(3)] for _ in range(2): response = await base.put(*items) assert response == items -async def test_put_empty(base: AsyncBase[TestItemModel]) -> None: +async def test_put_empty(base: AsyncBase[ItemStruct]) -> None: items = [] response = await base.put(*items) assert response == items -async def test_put_too_many(base: AsyncBase[TestItemModel]) -> None: - items = [TestItemModel(key=f"test_key_{i}") for i in range(base.PUT_LIMIT + 1)] +async def test_put_too_many(base: AsyncBase[ItemStruct]) -> None: + items = [ItemStruct(key=f"test_key_{i}") for i in range(base.PUT_LIMIT + 1)] with pytest.raises(ValueError, match=f"cannot put more than {base.PUT_LIMIT} items at a time"): await base.put(*items) -async def test_update(base: AsyncBase[TestItemModel]) -> None: - item = TestItemModel() +async def test_update(base: AsyncBase[ItemStruct]) -> None: + item = ItemStruct() await base.insert(item) updated_item = await base.update( { @@ -120,7 +107,7 @@ async def test_update(base: AsyncBase[TestItemModel]) -> None: }, key="test_key", ) - assert updated_item == TestItemModel( + assert updated_item == ItemStruct( key="test_key", field1=updated_item.field1, field2="updated_value", @@ -132,25 +119,25 @@ async def test_update(base: AsyncBase[TestItemModel]) -> None: ) -async def test_update_nonexistent(base: AsyncBase[TestItemModel]) -> None: +async def test_update_nonexistent(base: AsyncBase[ItemStruct]) -> None: with pytest.raises(ItemNotFoundError): await base.update({"foo": "bar"}, key=random_string()) -async def test_update_empty(base: AsyncBase[TestItemModel]) -> None: +async def test_update_empty(base: AsyncBase[ItemStruct]) -> None: with pytest.raises(ValueError, match="no updates provided"): await base.update({}, key="test_key") -async def test_query_empty(base: AsyncBase[TestItemModel]) -> None: - items = [TestItemModel(key=f"test_key_{i}", field1=i) for i in range(5)] +async def test_query_empty(base: AsyncBase[ItemStruct]) -> None: + items = [ItemStruct(key=f"test_key_{i}", field1=i) for i in range(5)] await base.put(*items) response = await base.query() assert response == QueryResponse(count=5, last_key=None, items=items) -async def test_query(base: AsyncBase[TestItemModel]) -> None: - items = [TestItemModel(key=f"test_key_{i}", field1=i) for i in range(5)] +async def test_query(base: AsyncBase[ItemStruct]) -> None: + items = [ItemStruct(key=f"test_key_{i}", field1=i) for i in range(5)] await base.put(*items) response = await base.query({"field1?gt": 1}) assert response == QueryResponse(count=3, last_key=None, items=[item for item in items if item.field1 > 1]) diff --git a/tests/base/test_async_base_typeddict.py b/tests/base/test_async_base_typeddict.py index 627ccf1..b63ad75 100644 --- a/tests/base/test_async_base_typeddict.py +++ b/tests/base/test_async_base_typeddict.py @@ -1,58 +1,20 @@ -# ruff: noqa: S101, S311, PLR2004 -from __future__ import annotations - -import random -from typing import TYPE_CHECKING, Any +from collections.abc import AsyncGenerator +from typing import Any import pytest from dotenv import load_dotenv -from typing_extensions import TypedDict from contiguity import AsyncBase, InvalidKeyError, ItemConflictError, ItemNotFoundError, QueryResponse from tests import random_string - -if TYPE_CHECKING: - from collections.abc import AsyncGenerator +from tests.base import ItemTypedDict +from tests.base import create_test_item_typed_dict as create_test_item load_dotenv() -class TestItemDict(TypedDict): - key: str - field1: int - field2: str - field3: int - field4: int - field5: list[str] - field6: list[int] - field7: dict[str, str] - - -def create_test_item( # noqa: PLR0913 - key: str = "test_key", - field1: int = random.randint(1, 1000), - field2: str = random_string(), - field3: int = 1, - field4: int = 0, - field5: list[str] | None = None, - field6: list[int] | None = None, - field7: dict[str, str] | None = None, -) -> TestItemDict: - return TestItemDict( - key=key, - field1=field1, - field2=field2, - field3=field3, - field4=field4, - field5=field5 or ["foo", "bar"], - field6=field6 or [1, 2], - field7=field7 or {"foo": "bar"}, - ) - - @pytest.fixture -async def base() -> AsyncGenerator[AsyncBase[TestItemDict], Any]: - base = AsyncBase("test_base_typeddict", item_type=TestItemDict) +async def base() -> AsyncGenerator[AsyncBase[ItemTypedDict], Any]: + base = AsyncBase("test_base_typeddict", item_type=ItemTypedDict) for item in (await base.query()).items: await base.delete(item["key"]) yield base @@ -62,10 +24,10 @@ async def base() -> AsyncGenerator[AsyncBase[TestItemDict], Any]: def test_bad_base_name() -> None: with pytest.raises(ValueError, match="invalid Base name ''"): - AsyncBase("", item_type=TestItemDict) + AsyncBase("", item_type=ItemTypedDict) -async def test_bad_key(base: AsyncBase[TestItemDict]) -> None: +async def test_bad_key(base: AsyncBase[ItemTypedDict]) -> None: with pytest.raises(InvalidKeyError): await base.get("") with pytest.raises(InvalidKeyError): @@ -74,25 +36,25 @@ async def test_bad_key(base: AsyncBase[TestItemDict]) -> None: await base.update({"foo": "bar"}, key="") -async def test_get(base: AsyncBase[TestItemDict]) -> None: +async def test_get(base: AsyncBase[ItemTypedDict]) -> None: item = create_test_item() await base.insert(item) fetched_item = await base.get("test_key") assert fetched_item == item -async def test_get_nonexistent(base: AsyncBase[TestItemDict]) -> None: +async def test_get_nonexistent(base: AsyncBase[ItemTypedDict]) -> None: with pytest.warns(DeprecationWarning): assert await base.get("nonexistent_key") is None -async def test_get_default(base: AsyncBase[TestItemDict]) -> None: +async def test_get_default(base: AsyncBase[ItemTypedDict]) -> None: for default_item in (None, "foo", 42, create_test_item()): fetched_item = await base.get("nonexistent_key", default=default_item) assert fetched_item == default_item -async def test_delete(base: AsyncBase[TestItemDict]) -> None: +async def test_delete(base: AsyncBase[ItemTypedDict]) -> None: item = create_test_item() await base.insert(item) await base.delete("test_key") @@ -100,39 +62,39 @@ async def test_delete(base: AsyncBase[TestItemDict]) -> None: assert await base.get("test_key") is None -async def test_insert(base: AsyncBase[TestItemDict]) -> None: +async def test_insert(base: AsyncBase[ItemTypedDict]) -> None: item = create_test_item() inserted_item = await base.insert(item) assert inserted_item == item -async def test_insert_existing(base: AsyncBase[TestItemDict]) -> None: +async def test_insert_existing(base: AsyncBase[ItemTypedDict]) -> None: item = create_test_item() await base.insert(item) with pytest.raises(ItemConflictError): await base.insert(item) -async def test_put(base: AsyncBase[TestItemDict]) -> None: +async def test_put(base: AsyncBase[ItemTypedDict]) -> None: items = [create_test_item(f"test_key_{i}") for i in range(3)] for _ in range(2): response = await base.put(*items) assert response == items -async def test_put_empty(base: AsyncBase[TestItemDict]) -> None: +async def test_put_empty(base: AsyncBase[ItemTypedDict]) -> None: items = [] response = await base.put(*items) assert response == items -async def test_put_too_many(base: AsyncBase[TestItemDict]) -> None: +async def test_put_too_many(base: AsyncBase[ItemTypedDict]) -> None: items = [create_test_item(key=f"test_key_{i}") for i in range(base.PUT_LIMIT + 1)] with pytest.raises(ValueError, match=f"cannot put more than {base.PUT_LIMIT} items at a time"): await base.put(*items) -async def test_update(base: AsyncBase[TestItemDict]) -> None: +async def test_update(base: AsyncBase[ItemTypedDict]) -> None: item = create_test_item() await base.insert(item) updated_item = await base.update( @@ -147,7 +109,7 @@ async def test_update(base: AsyncBase[TestItemDict]) -> None: }, key="test_key", ) - assert updated_item == TestItemDict( + assert updated_item == ItemTypedDict( key="test_key", field1=item["field1"], field2="updated_value", @@ -159,24 +121,24 @@ async def test_update(base: AsyncBase[TestItemDict]) -> None: ) -async def test_update_nonexistent(base: AsyncBase[TestItemDict]) -> None: +async def test_update_nonexistent(base: AsyncBase[ItemTypedDict]) -> None: with pytest.raises(ItemNotFoundError): await base.update({"foo": "bar"}, key=random_string()) -async def test_update_empty(base: AsyncBase[TestItemDict]) -> None: +async def test_update_empty(base: AsyncBase[ItemTypedDict]) -> None: with pytest.raises(ValueError, match="no updates provided"): await base.update({}, key="test_key") -async def test_query_empty(base: AsyncBase[TestItemDict]) -> None: +async def test_query_empty(base: AsyncBase[ItemTypedDict]) -> None: items = [create_test_item(key=f"test_key_{i}", field1=i) for i in range(5)] await base.put(*items) response = await base.query() assert response == QueryResponse(count=5, last_key=None, items=items) -async def test_query(base: AsyncBase[TestItemDict]) -> None: +async def test_query(base: AsyncBase[ItemTypedDict]) -> None: items = [create_test_item(key=f"test_key_{i}", field1=i) for i in range(5)] await base.put(*items) response = await base.query({"field1?gt": 1}) diff --git a/tests/base/test_base_dict_typed.py b/tests/base/test_base_dict_typed.py index 1c3553a..f9c0160 100644 --- a/tests/base/test_base_dict_typed.py +++ b/tests/base/test_base_dict_typed.py @@ -1,31 +1,16 @@ -# ruff: noqa: S101, S311, PLR2004 -import random -from collections.abc import Generator, Mapping +from collections.abc import Generator from typing import Any import pytest from dotenv import load_dotenv -from pydantic import JsonValue from contiguity import Base, InvalidKeyError, ItemConflictError, ItemNotFoundError, QueryResponse from tests import random_string +from tests.base import DictItemType +from tests.base import create_test_item_dict as create_test_item load_dotenv() -DictItemType = Mapping[str, JsonValue] - - -def create_test_item(**kwargs: JsonValue) -> DictItemType: - kwargs.setdefault("key", "test_key") - kwargs.setdefault("field1", random.randint(1, 1000)) - kwargs.setdefault("field2", random_string()) - kwargs.setdefault("field3", 1) - kwargs.setdefault("field4", 0) - kwargs.setdefault("field5", ["foo", "bar"]) - kwargs.setdefault("field6", [1, 2]) - kwargs.setdefault("field7", {"foo": "bar"}) - return kwargs - @pytest.fixture def base() -> Generator[Base[DictItemType], Any, None]: diff --git a/tests/base/test_base_dict_untyped.py b/tests/base/test_base_dict_untyped.py index 37bce0c..979400e 100644 --- a/tests/base/test_base_dict_untyped.py +++ b/tests/base/test_base_dict_untyped.py @@ -1,30 +1,16 @@ -# ruff: noqa: S101, S311, PLR2004 -import random from collections.abc import Generator from typing import Any import pytest from dotenv import load_dotenv -from pydantic import JsonValue from contiguity import Base, InvalidKeyError, ItemConflictError, ItemNotFoundError, QueryResponse from tests import random_string +from tests.base import create_test_item_dict as create_test_item load_dotenv() -def create_test_item(**kwargs: JsonValue) -> dict: - kwargs.setdefault("key", "test_key") - kwargs.setdefault("field1", random.randint(1, 1000)) - kwargs.setdefault("field2", random_string()) - kwargs.setdefault("field3", 1) - kwargs.setdefault("field4", 0) - kwargs.setdefault("field5", ["foo", "bar"]) - kwargs.setdefault("field6", [1, 2]) - kwargs.setdefault("field7", {"foo": "bar"}) - return kwargs - - @pytest.fixture def base() -> Generator[Base, Any, None]: base = Base("test_base_dict_untyped") diff --git a/tests/base/test_base_model.py b/tests/base/test_base_struct.py similarity index 59% rename from tests/base/test_base_model.py rename to tests/base/test_base_struct.py index d299318..ba91566 100644 --- a/tests/base/test_base_model.py +++ b/tests/base/test_base_struct.py @@ -1,32 +1,19 @@ -# ruff: noqa: S101, S311, PLR2004 -import random from collections.abc import Generator from typing import Any import pytest from dotenv import load_dotenv -from pydantic import BaseModel from contiguity import Base, InvalidKeyError, ItemConflictError, ItemNotFoundError, QueryResponse from tests import random_string +from tests.base import ItemStruct load_dotenv() -class TestItemModel(BaseModel): - key: str = "test_key" - field1: int = random.randint(1, 1000) - field2: str = random_string() - field3: int = 1 - field4: int = 0 - field5: list[str] = ["foo", "bar"] - field6: list[int] = [1, 2] - field7: dict[str, str] = {"foo": "bar"} - - @pytest.fixture -def base() -> Generator[Base[TestItemModel], Any, None]: - base = Base("test_base_model", item_type=TestItemModel) +def base() -> Generator[Base[ItemStruct], Any, None]: + base = Base("test_base_model", item_type=ItemStruct) for item in base.query().items: base.delete(item.key) yield base @@ -36,10 +23,10 @@ def base() -> Generator[Base[TestItemModel], Any, None]: def test_bad_base_name() -> None: with pytest.raises(ValueError, match="invalid Base name ''"): - Base("", item_type=TestItemModel) + Base("", item_type=ItemStruct) -def test_bad_key(base: Base[TestItemModel]) -> None: +def test_bad_key(base: Base[ItemStruct]) -> None: with pytest.raises(InvalidKeyError): base.get("") with pytest.raises(InvalidKeyError): @@ -48,66 +35,66 @@ def test_bad_key(base: Base[TestItemModel]) -> None: base.update({"foo": "bar"}, key="") -def test_get(base: Base[TestItemModel]) -> None: - item = TestItemModel() +def test_get(base: Base[ItemStruct]) -> None: + item = ItemStruct() base.insert(item) fetched_item = base.get("test_key") assert fetched_item == item -def test_get_nonexistent(base: Base[TestItemModel]) -> None: +def test_get_nonexistent(base: Base[ItemStruct]) -> None: with pytest.warns(DeprecationWarning): assert base.get("nonexistent_key") is None -def test_get_default(base: Base[TestItemModel]) -> None: - for default_item in (None, "foo", 42, TestItemModel()): +def test_get_default(base: Base[ItemStruct]) -> None: + for default_item in (None, "foo", 42, ItemStruct()): fetched_item = base.get("nonexistent_key", default=default_item) assert fetched_item == default_item -def test_delete(base: Base[TestItemModel]) -> None: - item = TestItemModel() +def test_delete(base: Base[ItemStruct]) -> None: + item = ItemStruct() base.insert(item) base.delete("test_key") with pytest.warns(DeprecationWarning): assert base.get("test_key") is None -def test_insert(base: Base[TestItemModel]) -> None: - item = TestItemModel() +def test_insert(base: Base[ItemStruct]) -> None: + item = ItemStruct() inserted_item = base.insert(item) assert inserted_item == item -def test_insert_existing(base: Base[TestItemModel]) -> None: - item = TestItemModel() +def test_insert_existing(base: Base[ItemStruct]) -> None: + item = ItemStruct() base.insert(item) with pytest.raises(ItemConflictError): base.insert(item) -def test_put(base: Base[TestItemModel]) -> None: - items = [TestItemModel(key=f"test_key_{i}") for i in range(3)] +def test_put(base: Base[ItemStruct]) -> None: + items = [ItemStruct(key=f"test_key_{i}") for i in range(3)] for _ in range(2): response = base.put(*items) assert response == items -def test_put_empty(base: Base[TestItemModel]) -> None: +def test_put_empty(base: Base[ItemStruct]) -> None: items = [] response = base.put(*items) assert response == items -def test_put_too_many(base: Base[TestItemModel]) -> None: - items = [TestItemModel(key=f"test_key_{i}") for i in range(base.PUT_LIMIT + 1)] +def test_put_too_many(base: Base[ItemStruct]) -> None: + items = [ItemStruct(key=f"test_key_{i}") for i in range(base.PUT_LIMIT + 1)] with pytest.raises(ValueError, match=f"cannot put more than {base.PUT_LIMIT} items at a time"): base.put(*items) -def test_update(base: Base[TestItemModel]) -> None: - item = TestItemModel() +def test_update(base: Base[ItemStruct]) -> None: + item = ItemStruct() base.insert(item) updated_item = base.update( { @@ -120,7 +107,7 @@ def test_update(base: Base[TestItemModel]) -> None: }, key="test_key", ) - assert updated_item == TestItemModel( + assert updated_item == ItemStruct( key="test_key", field1=updated_item.field1, field2="updated_value", @@ -132,25 +119,25 @@ def test_update(base: Base[TestItemModel]) -> None: ) -def test_update_nonexistent(base: Base[TestItemModel]) -> None: +def test_update_nonexistent(base: Base[ItemStruct]) -> None: with pytest.raises(ItemNotFoundError): base.update({"foo": "bar"}, key=random_string()) -def test_update_empty(base: Base[TestItemModel]) -> None: +def test_update_empty(base: Base[ItemStruct]) -> None: with pytest.raises(ValueError, match="no updates provided"): base.update({}, key="test_key") -def test_query_empty(base: Base[TestItemModel]) -> None: - items = [TestItemModel(key=f"test_key_{i}", field1=i) for i in range(5)] +def test_query_empty(base: Base[ItemStruct]) -> None: + items = [ItemStruct(key=f"test_key_{i}", field1=i) for i in range(5)] base.put(*items) response = base.query() assert response == QueryResponse(count=5, last_key=None, items=items) -def test_query(base: Base[TestItemModel]) -> None: - items = [TestItemModel(key=f"test_key_{i}", field1=i) for i in range(5)] +def test_query(base: Base[ItemStruct]) -> None: + items = [ItemStruct(key=f"test_key_{i}", field1=i) for i in range(5)] base.put(*items) response = base.query({"field1?gt": 1}) assert response == QueryResponse(count=3, last_key=None, items=[item for item in items if item.field1 > 1]) diff --git a/tests/base/test_base_typeddict.py b/tests/base/test_base_typeddict.py index 39e4003..7198fa7 100644 --- a/tests/base/test_base_typeddict.py +++ b/tests/base/test_base_typeddict.py @@ -1,58 +1,20 @@ -# ruff: noqa: S101, S311, PLR2004 -from __future__ import annotations - -import random -from typing import TYPE_CHECKING, Any +from collections.abc import Generator +from typing import Any import pytest from dotenv import load_dotenv -from typing_extensions import TypedDict from contiguity import Base, InvalidKeyError, ItemConflictError, ItemNotFoundError, QueryResponse from tests import random_string - -if TYPE_CHECKING: - from collections.abc import Generator +from tests.base import ItemTypedDict +from tests.base import create_test_item_typed_dict as create_test_item load_dotenv() -class TestItemDict(TypedDict): - key: str - field1: int - field2: str - field3: int - field4: int - field5: list[str] - field6: list[int] - field7: dict[str, str] - - -def create_test_item( # noqa: PLR0913 - key: str = "test_key", - field1: int = random.randint(1, 1000), - field2: str = random_string(), - field3: int = 1, - field4: int = 0, - field5: list[str] | None = None, - field6: list[int] | None = None, - field7: dict[str, str] | None = None, -) -> TestItemDict: - return TestItemDict( - key=key, - field1=field1, - field2=field2, - field3=field3, - field4=field4, - field5=field5 or ["foo", "bar"], - field6=field6 or [1, 2], - field7=field7 or {"foo": "bar"}, - ) - - @pytest.fixture -def base() -> Generator[Base[TestItemDict], Any, None]: - base = Base("test_base_typeddict", item_type=TestItemDict) +def base() -> Generator[Base[ItemTypedDict], Any, None]: + base = Base("test_base_typeddict", item_type=ItemTypedDict) for item in base.query().items: base.delete(item["key"]) yield base @@ -62,10 +24,10 @@ def base() -> Generator[Base[TestItemDict], Any, None]: def test_bad_base_name() -> None: with pytest.raises(ValueError, match="invalid Base name ''"): - Base("", item_type=TestItemDict) + Base("", item_type=ItemTypedDict) -def test_bad_key(base: Base[TestItemDict]) -> None: +def test_bad_key(base: Base[ItemTypedDict]) -> None: with pytest.raises(InvalidKeyError): base.get("") with pytest.raises(InvalidKeyError): @@ -74,25 +36,25 @@ def test_bad_key(base: Base[TestItemDict]) -> None: base.update({"foo": "bar"}, key="") -def test_get(base: Base[TestItemDict]) -> None: +def test_get(base: Base[ItemTypedDict]) -> None: item = create_test_item() base.insert(item) fetched_item = base.get("test_key") assert fetched_item == item -def test_get_nonexistent(base: Base[TestItemDict]) -> None: +def test_get_nonexistent(base: Base[ItemTypedDict]) -> None: with pytest.warns(DeprecationWarning): assert base.get("nonexistent_key") is None -def test_get_default(base: Base[TestItemDict]) -> None: +def test_get_default(base: Base[ItemTypedDict]) -> None: for default_item in (None, "foo", 42, create_test_item()): fetched_item = base.get("nonexistent_key", default=default_item) assert fetched_item == default_item -def test_delete(base: Base[TestItemDict]) -> None: +def test_delete(base: Base[ItemTypedDict]) -> None: item = create_test_item() base.insert(item) base.delete("test_key") @@ -100,39 +62,39 @@ def test_delete(base: Base[TestItemDict]) -> None: assert base.get("test_key") is None -def test_insert(base: Base[TestItemDict]) -> None: +def test_insert(base: Base[ItemTypedDict]) -> None: item = create_test_item() inserted_item = base.insert(item) assert inserted_item == item -def test_insert_existing(base: Base[TestItemDict]) -> None: +def test_insert_existing(base: Base[ItemTypedDict]) -> None: item = create_test_item() base.insert(item) with pytest.raises(ItemConflictError): base.insert(item) -def test_put(base: Base[TestItemDict]) -> None: +def test_put(base: Base[ItemTypedDict]) -> None: items = [create_test_item(f"test_key_{i}") for i in range(3)] for _ in range(2): response = base.put(*items) assert response == items -def test_put_empty(base: Base[TestItemDict]) -> None: +def test_put_empty(base: Base[ItemTypedDict]) -> None: items = [] response = base.put(*items) assert response == items -def test_put_too_many(base: Base[TestItemDict]) -> None: +def test_put_too_many(base: Base[ItemTypedDict]) -> None: items = [create_test_item(key=f"test_key_{i}") for i in range(base.PUT_LIMIT + 1)] with pytest.raises(ValueError, match=f"cannot put more than {base.PUT_LIMIT} items at a time"): base.put(*items) -def test_update(base: Base[TestItemDict]) -> None: +def test_update(base: Base[ItemTypedDict]) -> None: item = create_test_item() base.insert(item) updated_item = base.update( @@ -147,7 +109,7 @@ def test_update(base: Base[TestItemDict]) -> None: }, key="test_key", ) - assert updated_item == TestItemDict( + assert updated_item == ItemTypedDict( key="test_key", field1=item["field1"], field2="updated_value", @@ -159,24 +121,24 @@ def test_update(base: Base[TestItemDict]) -> None: ) -def test_update_nonexistent(base: Base[TestItemDict]) -> None: +def test_update_nonexistent(base: Base[ItemTypedDict]) -> None: with pytest.raises(ItemNotFoundError): base.update({"foo": "bar"}, key=random_string()) -def test_update_empty(base: Base[TestItemDict]) -> None: +def test_update_empty(base: Base[ItemTypedDict]) -> None: with pytest.raises(ValueError, match="no updates provided"): base.update({}, key="test_key") -def test_query_empty(base: Base[TestItemDict]) -> None: +def test_query_empty(base: Base[ItemTypedDict]) -> None: items = [create_test_item(key=f"test_key_{i}", field1=i) for i in range(5)] base.put(*items) response = base.query() assert response == QueryResponse(count=5, last_key=None, items=items) -def test_query(base: Base[TestItemDict]) -> None: +def test_query(base: Base[ItemTypedDict]) -> None: items = [create_test_item(key=f"test_key_{i}", field1=i) for i in range(5)] base.put(*items) response = base.query({"field1?gt": 1}) diff --git a/uv.lock b/uv.lock index 28e7970..9f28091 100644 --- a/uv.lock +++ b/uv.lock @@ -6,15 +6,6 @@ resolution-markers = [ "python_full_version >= '3.13'", ] -[[package]] -name = "annotated-types" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, -] - [[package]] name = "anyio" version = "4.6.0" @@ -62,8 +53,8 @@ name = "contiguity" source = { editable = "." } dependencies = [ { name = "httpx" }, + { name = "msgspec" }, { name = "phonenumbers" }, - { name = "pydantic" }, { name = "typing-extensions" }, ] @@ -79,8 +70,8 @@ dev = [ [package.metadata] requires-dist = [ { name = "httpx", specifier = ">=0.27.2" }, + { name = "msgspec", specifier = ">=0.19.0" }, { name = "phonenumbers", specifier = ">=8.13.47,<9.0.0" }, - { name = "pydantic", specifier = ">=2.9.0,<3.0.0" }, { name = "typing-extensions", specifier = ">=4.12.2,<5.0.0" }, ] @@ -259,6 +250,49 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, ] +[[package]] +name = "msgspec" +version = "0.19.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/9b/95d8ce458462b8b71b8a70fa94563b2498b89933689f3a7b8911edfae3d7/msgspec-0.19.0.tar.gz", hash = "sha256:604037e7cd475345848116e89c553aa9a233259733ab51986ac924ab1b976f8e", size = 216934 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/40/817282b42f58399762267b30deb8ac011d8db373f8da0c212c85fbe62b8f/msgspec-0.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d8dd848ee7ca7c8153462557655570156c2be94e79acec3561cf379581343259", size = 190019 }, + { url = "https://files.pythonhosted.org/packages/92/99/bd7ed738c00f223a8119928661167a89124140792af18af513e6519b0d54/msgspec-0.19.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0553bbc77662e5708fe66aa75e7bd3e4b0f209709c48b299afd791d711a93c36", size = 183680 }, + { url = "https://files.pythonhosted.org/packages/e5/27/322badde18eb234e36d4a14122b89edd4e2973cdbc3da61ca7edf40a1ccd/msgspec-0.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe2c4bf29bf4e89790b3117470dea2c20b59932772483082c468b990d45fb947", size = 209334 }, + { url = "https://files.pythonhosted.org/packages/c6/65/080509c5774a1592b2779d902a70b5fe008532759927e011f068145a16cb/msgspec-0.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e87ecfa9795ee5214861eab8326b0e75475c2e68a384002aa135ea2a27d909", size = 211551 }, + { url = "https://files.pythonhosted.org/packages/6f/2e/1c23c6b4ca6f4285c30a39def1054e2bee281389e4b681b5e3711bd5a8c9/msgspec-0.19.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3c4ec642689da44618f68c90855a10edbc6ac3ff7c1d94395446c65a776e712a", size = 215099 }, + { url = "https://files.pythonhosted.org/packages/83/fe/95f9654518879f3359d1e76bc41189113aa9102452170ab7c9a9a4ee52f6/msgspec-0.19.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2719647625320b60e2d8af06b35f5b12d4f4d281db30a15a1df22adb2295f633", size = 218211 }, + { url = "https://files.pythonhosted.org/packages/79/f6/71ca7e87a1fb34dfe5efea8156c9ef59dd55613aeda2ca562f122cd22012/msgspec-0.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:695b832d0091edd86eeb535cd39e45f3919f48d997685f7ac31acb15e0a2ed90", size = 186174 }, + { url = "https://files.pythonhosted.org/packages/24/d4/2ec2567ac30dab072cce3e91fb17803c52f0a37aab6b0c24375d2b20a581/msgspec-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa77046904db764b0462036bc63ef71f02b75b8f72e9c9dd4c447d6da1ed8f8e", size = 187939 }, + { url = "https://files.pythonhosted.org/packages/2b/c0/18226e4328897f4f19875cb62bb9259fe47e901eade9d9376ab5f251a929/msgspec-0.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:047cfa8675eb3bad68722cfe95c60e7afabf84d1bd8938979dd2b92e9e4a9551", size = 182202 }, + { url = "https://files.pythonhosted.org/packages/81/25/3a4b24d468203d8af90d1d351b77ea3cffb96b29492855cf83078f16bfe4/msgspec-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e78f46ff39a427e10b4a61614a2777ad69559cc8d603a7c05681f5a595ea98f7", size = 209029 }, + { url = "https://files.pythonhosted.org/packages/85/2e/db7e189b57901955239f7689b5dcd6ae9458637a9c66747326726c650523/msgspec-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c7adf191e4bd3be0e9231c3b6dc20cf1199ada2af523885efc2ed218eafd011", size = 210682 }, + { url = "https://files.pythonhosted.org/packages/03/97/7c8895c9074a97052d7e4a1cc1230b7b6e2ca2486714eb12c3f08bb9d284/msgspec-0.19.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f04cad4385e20be7c7176bb8ae3dca54a08e9756cfc97bcdb4f18560c3042063", size = 214003 }, + { url = "https://files.pythonhosted.org/packages/61/61/e892997bcaa289559b4d5869f066a8021b79f4bf8e955f831b095f47a4cd/msgspec-0.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45c8fb410670b3b7eb884d44a75589377c341ec1392b778311acdbfa55187716", size = 216833 }, + { url = "https://files.pythonhosted.org/packages/ce/3d/71b2dffd3a1c743ffe13296ff701ee503feaebc3f04d0e75613b6563c374/msgspec-0.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:70eaef4934b87193a27d802534dc466778ad8d536e296ae2f9334e182ac27b6c", size = 186184 }, + { url = "https://files.pythonhosted.org/packages/b2/5f/a70c24f075e3e7af2fae5414c7048b0e11389685b7f717bb55ba282a34a7/msgspec-0.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f98bd8962ad549c27d63845b50af3f53ec468b6318400c9f1adfe8b092d7b62f", size = 190485 }, + { url = "https://files.pythonhosted.org/packages/89/b0/1b9763938cfae12acf14b682fcf05c92855974d921a5a985ecc197d1c672/msgspec-0.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:43bbb237feab761b815ed9df43b266114203f53596f9b6e6f00ebd79d178cdf2", size = 183910 }, + { url = "https://files.pythonhosted.org/packages/87/81/0c8c93f0b92c97e326b279795f9c5b956c5a97af28ca0fbb9fd86c83737a/msgspec-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cfc033c02c3e0aec52b71710d7f84cb3ca5eb407ab2ad23d75631153fdb1f12", size = 210633 }, + { url = "https://files.pythonhosted.org/packages/d0/ef/c5422ce8af73928d194a6606f8ae36e93a52fd5e8df5abd366903a5ca8da/msgspec-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d911c442571605e17658ca2b416fd8579c5050ac9adc5e00c2cb3126c97f73bc", size = 213594 }, + { url = "https://files.pythonhosted.org/packages/19/2b/4137bc2ed45660444842d042be2cf5b18aa06efd2cda107cff18253b9653/msgspec-0.19.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:757b501fa57e24896cf40a831442b19a864f56d253679f34f260dcb002524a6c", size = 214053 }, + { url = "https://files.pythonhosted.org/packages/9d/e6/8ad51bdc806aac1dc501e8fe43f759f9ed7284043d722b53323ea421c360/msgspec-0.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5f0f65f29b45e2816d8bded36e6b837a4bf5fb60ec4bc3c625fa2c6da4124537", size = 219081 }, + { url = "https://files.pythonhosted.org/packages/b1/ef/27dd35a7049c9a4f4211c6cd6a8c9db0a50647546f003a5867827ec45391/msgspec-0.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:067f0de1c33cfa0b6a8206562efdf6be5985b988b53dd244a8e06f993f27c8c0", size = 187467 }, + { url = "https://files.pythonhosted.org/packages/3c/cb/2842c312bbe618d8fefc8b9cedce37f773cdc8fa453306546dba2c21fd98/msgspec-0.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f12d30dd6266557aaaf0aa0f9580a9a8fbeadfa83699c487713e355ec5f0bd86", size = 190498 }, + { url = "https://files.pythonhosted.org/packages/58/95/c40b01b93465e1a5f3b6c7d91b10fb574818163740cc3acbe722d1e0e7e4/msgspec-0.19.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82b2c42c1b9ebc89e822e7e13bbe9d17ede0c23c187469fdd9505afd5a481314", size = 183950 }, + { url = "https://files.pythonhosted.org/packages/e8/f0/5b764e066ce9aba4b70d1db8b087ea66098c7c27d59b9dd8a3532774d48f/msgspec-0.19.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19746b50be214a54239aab822964f2ac81e38b0055cca94808359d779338c10e", size = 210647 }, + { url = "https://files.pythonhosted.org/packages/9d/87/bc14f49bc95c4cb0dd0a8c56028a67c014ee7e6818ccdce74a4862af259b/msgspec-0.19.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60ef4bdb0ec8e4ad62e5a1f95230c08efb1f64f32e6e8dd2ced685bcc73858b5", size = 213563 }, + { url = "https://files.pythonhosted.org/packages/53/2f/2b1c2b056894fbaa975f68f81e3014bb447516a8b010f1bed3fb0e016ed7/msgspec-0.19.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac7f7c377c122b649f7545810c6cd1b47586e3aa3059126ce3516ac7ccc6a6a9", size = 213996 }, + { url = "https://files.pythonhosted.org/packages/aa/5a/4cd408d90d1417e8d2ce6a22b98a6853c1b4d7cb7669153e4424d60087f6/msgspec-0.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5bc1472223a643f5ffb5bf46ccdede7f9795078194f14edd69e3aab7020d327", size = 219087 }, + { url = "https://files.pythonhosted.org/packages/23/d8/f15b40611c2d5753d1abb0ca0da0c75348daf1252220e5dda2867bd81062/msgspec-0.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:317050bc0f7739cb30d257ff09152ca309bf5a369854bbf1e57dffc310c1f20f", size = 187432 }, + { url = "https://files.pythonhosted.org/packages/ea/d0/323f867eaec1f2236ba30adf613777b1c97a7e8698e2e881656b21871fa4/msgspec-0.19.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15c1e86fff77184c20a2932cd9742bf33fe23125fa3fcf332df9ad2f7d483044", size = 189926 }, + { url = "https://files.pythonhosted.org/packages/a8/37/c3e1b39bdae90a7258d77959f5f5e36ad44b40e2be91cff83eea33c54d43/msgspec-0.19.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3b5541b2b3294e5ffabe31a09d604e23a88533ace36ac288fa32a420aa38d229", size = 183873 }, + { url = "https://files.pythonhosted.org/packages/cb/a2/48f2c15c7644668e51f4dce99d5f709bd55314e47acb02e90682f5880f35/msgspec-0.19.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f5c043ace7962ef188746e83b99faaa9e3e699ab857ca3f367b309c8e2c6b12", size = 209272 }, + { url = "https://files.pythonhosted.org/packages/25/3c/aa339cf08b990c3f07e67b229a3a8aa31bf129ed974b35e5daa0df7d9d56/msgspec-0.19.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca06aa08e39bf57e39a258e1996474f84d0dd8130d486c00bec26d797b8c5446", size = 211396 }, + { url = "https://files.pythonhosted.org/packages/c7/00/c7fb9d524327c558b2803973cc3f988c5100a1708879970a9e377bdf6f4f/msgspec-0.19.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e695dad6897896e9384cf5e2687d9ae9feaef50e802f93602d35458e20d1fb19", size = 215002 }, + { url = "https://files.pythonhosted.org/packages/3f/bf/d9f9fff026c1248cde84a5ce62b3742e8a63a3c4e811f99f00c8babf7615/msgspec-0.19.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3be5c02e1fee57b54130316a08fe40cca53af92999a302a6054cd451700ea7db", size = 218132 }, + { url = "https://files.pythonhosted.org/packages/00/03/b92011210f79794958167a3a3ea64a71135d9a2034cfb7597b545a42606d/msgspec-0.19.0-cp39-cp39-win_amd64.whl", hash = "sha256:0684573a821be3c749912acf5848cce78af4298345cb2d7a8b8948a0a5a27cfe", size = 186301 }, +] + [[package]] name = "nodeenv" version = "1.9.1" @@ -320,107 +354,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/07/92/caae8c86e94681b42c246f0bca35c059a2f0529e5b92619f6aba4cf7e7b6/pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f", size = 204643 }, ] -[[package]] -name = "pydantic" -version = "2.9.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a9/b7/d9e3f12af310e1120c21603644a1cd86f59060e040ec5c3a80b8f05fae30/pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f", size = 769917 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/e4/ba44652d562cbf0bf320e0f3810206149c8a4e99cdbf66da82e97ab53a15/pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12", size = 434928 }, -] - -[[package]] -name = "pydantic-core" -version = "2.23.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e2/aa/6b6a9b9f8537b872f552ddd46dd3da230367754b6f707b8e1e963f515ea3/pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863", size = 402156 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/8b/d3ae387f66277bd8104096d6ec0a145f4baa2966ebb2cad746c0920c9526/pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b", size = 1867835 }, - { url = "https://files.pythonhosted.org/packages/46/76/f68272e4c3a7df8777798282c5e47d508274917f29992d84e1898f8908c7/pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166", size = 1776689 }, - { url = "https://files.pythonhosted.org/packages/cc/69/5f945b4416f42ea3f3bc9d2aaec66c76084a6ff4ff27555bf9415ab43189/pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb", size = 1800748 }, - { url = "https://files.pythonhosted.org/packages/50/ab/891a7b0054bcc297fb02d44d05c50e68154e31788f2d9d41d0b72c89fdf7/pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916", size = 1806469 }, - { url = "https://files.pythonhosted.org/packages/31/7c/6e3fa122075d78f277a8431c4c608f061881b76c2b7faca01d317ee39b5d/pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07", size = 2002246 }, - { url = "https://files.pythonhosted.org/packages/ad/6f/22d5692b7ab63fc4acbc74de6ff61d185804a83160adba5e6cc6068e1128/pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232", size = 2659404 }, - { url = "https://files.pythonhosted.org/packages/11/ac/1e647dc1121c028b691028fa61a4e7477e6aeb5132628fde41dd34c1671f/pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2", size = 2053940 }, - { url = "https://files.pythonhosted.org/packages/91/75/984740c17f12c3ce18b5a2fcc4bdceb785cce7df1511a4ce89bca17c7e2d/pydantic_core-2.23.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f", size = 1921437 }, - { url = "https://files.pythonhosted.org/packages/a0/74/13c5f606b64d93f0721e7768cd3e8b2102164866c207b8cd6f90bb15d24f/pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3", size = 1966129 }, - { url = "https://files.pythonhosted.org/packages/18/03/9c4aa5919457c7b57a016c1ab513b1a926ed9b2bb7915bf8e506bf65c34b/pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071", size = 2110908 }, - { url = "https://files.pythonhosted.org/packages/92/2c/053d33f029c5dc65e5cf44ff03ceeefb7cce908f8f3cca9265e7f9b540c8/pydantic_core-2.23.4-cp310-none-win32.whl", hash = "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119", size = 1735278 }, - { url = "https://files.pythonhosted.org/packages/de/81/7dfe464eca78d76d31dd661b04b5f2036ec72ea8848dd87ab7375e185c23/pydantic_core-2.23.4-cp310-none-win_amd64.whl", hash = "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f", size = 1917453 }, - { url = "https://files.pythonhosted.org/packages/5d/30/890a583cd3f2be27ecf32b479d5d615710bb926d92da03e3f7838ff3e58b/pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8", size = 1865160 }, - { url = "https://files.pythonhosted.org/packages/1d/9a/b634442e1253bc6889c87afe8bb59447f106ee042140bd57680b3b113ec7/pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d", size = 1776777 }, - { url = "https://files.pythonhosted.org/packages/75/9a/7816295124a6b08c24c96f9ce73085032d8bcbaf7e5a781cd41aa910c891/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e", size = 1799244 }, - { url = "https://files.pythonhosted.org/packages/a9/8f/89c1405176903e567c5f99ec53387449e62f1121894aa9fc2c4fdc51a59b/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607", size = 1805307 }, - { url = "https://files.pythonhosted.org/packages/d5/a5/1a194447d0da1ef492e3470680c66048fef56fc1f1a25cafbea4bc1d1c48/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd", size = 2000663 }, - { url = "https://files.pythonhosted.org/packages/13/a5/1df8541651de4455e7d587cf556201b4f7997191e110bca3b589218745a5/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea", size = 2655941 }, - { url = "https://files.pythonhosted.org/packages/44/31/a3899b5ce02c4316865e390107f145089876dff7e1dfc770a231d836aed8/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e", size = 2052105 }, - { url = "https://files.pythonhosted.org/packages/1b/aa/98e190f8745d5ec831f6d5449344c48c0627ac5fed4e5340a44b74878f8e/pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b", size = 1919967 }, - { url = "https://files.pythonhosted.org/packages/ae/35/b6e00b6abb2acfee3e8f85558c02a0822e9a8b2f2d812ea8b9079b118ba0/pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0", size = 1964291 }, - { url = "https://files.pythonhosted.org/packages/13/46/7bee6d32b69191cd649bbbd2361af79c472d72cb29bb2024f0b6e350ba06/pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64", size = 2109666 }, - { url = "https://files.pythonhosted.org/packages/39/ef/7b34f1b122a81b68ed0a7d0e564da9ccdc9a2924c8d6c6b5b11fa3a56970/pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f", size = 1732940 }, - { url = "https://files.pythonhosted.org/packages/2f/76/37b7e76c645843ff46c1d73e046207311ef298d3f7b2f7d8f6ac60113071/pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3", size = 1916804 }, - { url = "https://files.pythonhosted.org/packages/74/7b/8e315f80666194b354966ec84b7d567da77ad927ed6323db4006cf915f3f/pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231", size = 1856459 }, - { url = "https://files.pythonhosted.org/packages/14/de/866bdce10ed808323d437612aca1ec9971b981e1c52e5e42ad9b8e17a6f6/pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee", size = 1770007 }, - { url = "https://files.pythonhosted.org/packages/dc/69/8edd5c3cd48bb833a3f7ef9b81d7666ccddd3c9a635225214e044b6e8281/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87", size = 1790245 }, - { url = "https://files.pythonhosted.org/packages/80/33/9c24334e3af796ce80d2274940aae38dd4e5676298b4398eff103a79e02d/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8", size = 1801260 }, - { url = "https://files.pythonhosted.org/packages/a5/6f/e9567fd90104b79b101ca9d120219644d3314962caa7948dd8b965e9f83e/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327", size = 1996872 }, - { url = "https://files.pythonhosted.org/packages/2d/ad/b5f0fe9e6cfee915dd144edbd10b6e9c9c9c9d7a56b69256d124b8ac682e/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2", size = 2661617 }, - { url = "https://files.pythonhosted.org/packages/06/c8/7d4b708f8d05a5cbfda3243aad468052c6e99de7d0937c9146c24d9f12e9/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36", size = 2071831 }, - { url = "https://files.pythonhosted.org/packages/89/4d/3079d00c47f22c9a9a8220db088b309ad6e600a73d7a69473e3a8e5e3ea3/pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126", size = 1917453 }, - { url = "https://files.pythonhosted.org/packages/e9/88/9df5b7ce880a4703fcc2d76c8c2d8eb9f861f79d0c56f4b8f5f2607ccec8/pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e", size = 1968793 }, - { url = "https://files.pythonhosted.org/packages/e3/b9/41f7efe80f6ce2ed3ee3c2dcfe10ab7adc1172f778cc9659509a79518c43/pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24", size = 2116872 }, - { url = "https://files.pythonhosted.org/packages/63/08/b59b7a92e03dd25554b0436554bf23e7c29abae7cce4b1c459cd92746811/pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84", size = 1738535 }, - { url = "https://files.pythonhosted.org/packages/88/8d/479293e4d39ab409747926eec4329de5b7129beaedc3786eca070605d07f/pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9", size = 1917992 }, - { url = "https://files.pythonhosted.org/packages/ad/ef/16ee2df472bf0e419b6bc68c05bf0145c49247a1095e85cee1463c6a44a1/pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc", size = 1856143 }, - { url = "https://files.pythonhosted.org/packages/da/fa/bc3dbb83605669a34a93308e297ab22be82dfb9dcf88c6cf4b4f264e0a42/pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd", size = 1770063 }, - { url = "https://files.pythonhosted.org/packages/4e/48/e813f3bbd257a712303ebdf55c8dc46f9589ec74b384c9f652597df3288d/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05", size = 1790013 }, - { url = "https://files.pythonhosted.org/packages/b4/e0/56eda3a37929a1d297fcab1966db8c339023bcca0b64c5a84896db3fcc5c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d", size = 1801077 }, - { url = "https://files.pythonhosted.org/packages/04/be/5e49376769bfbf82486da6c5c1683b891809365c20d7c7e52792ce4c71f3/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510", size = 1996782 }, - { url = "https://files.pythonhosted.org/packages/bc/24/e3ee6c04f1d58cc15f37bcc62f32c7478ff55142b7b3e6d42ea374ea427c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6", size = 2661375 }, - { url = "https://files.pythonhosted.org/packages/c1/f8/11a9006de4e89d016b8de74ebb1db727dc100608bb1e6bbe9d56a3cbbcce/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b", size = 2071635 }, - { url = "https://files.pythonhosted.org/packages/7c/45/bdce5779b59f468bdf262a5bc9eecbae87f271c51aef628d8c073b4b4b4c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327", size = 1916994 }, - { url = "https://files.pythonhosted.org/packages/d8/fa/c648308fe711ee1f88192cad6026ab4f925396d1293e8356de7e55be89b5/pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6", size = 1968877 }, - { url = "https://files.pythonhosted.org/packages/16/16/b805c74b35607d24d37103007f899abc4880923b04929547ae68d478b7f4/pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f", size = 2116814 }, - { url = "https://files.pythonhosted.org/packages/d1/58/5305e723d9fcdf1c5a655e6a4cc2a07128bf644ff4b1d98daf7a9dbf57da/pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769", size = 1738360 }, - { url = "https://files.pythonhosted.org/packages/a5/ae/e14b0ff8b3f48e02394d8acd911376b7b66e164535687ef7dc24ea03072f/pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5", size = 1919411 }, - { url = "https://files.pythonhosted.org/packages/7a/04/2580b2deaae37b3e30fc30c54298be938b973990b23612d6b61c7bdd01c7/pydantic_core-2.23.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a", size = 1868200 }, - { url = "https://files.pythonhosted.org/packages/39/6e/e311bd0751505350f0cdcee3077841eb1f9253c5a1ddbad048cd9fbf7c6e/pydantic_core-2.23.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36", size = 1749316 }, - { url = "https://files.pythonhosted.org/packages/d0/b4/95b5eb47c6dc8692508c3ca04a1f8d6f0884c9dacb34cf3357595cbe73be/pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b", size = 1800880 }, - { url = "https://files.pythonhosted.org/packages/da/79/41c4f817acd7f42d94cd1e16526c062a7b089f66faed4bd30852314d9a66/pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323", size = 1807077 }, - { url = "https://files.pythonhosted.org/packages/fb/53/d13d1eb0a97d5c06cf7a225935d471e9c241afd389a333f40c703f214973/pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3", size = 2002859 }, - { url = "https://files.pythonhosted.org/packages/53/7d/6b8a1eff453774b46cac8c849e99455b27167971a003212f668e94bc4c9c/pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df", size = 2661437 }, - { url = "https://files.pythonhosted.org/packages/6c/ea/8820f57f0b46e6148ee42d8216b15e8fe3b360944284bbc705bf34fac888/pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c", size = 2054404 }, - { url = "https://files.pythonhosted.org/packages/0f/36/d4ae869e473c3c7868e1cd1e2a1b9e13bce5cd1a7d287f6ac755a0b1575e/pydantic_core-2.23.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55", size = 1921680 }, - { url = "https://files.pythonhosted.org/packages/0d/f8/eed5c65b80c4ac4494117e2101973b45fc655774ef647d17dde40a70f7d2/pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040", size = 1966093 }, - { url = "https://files.pythonhosted.org/packages/e8/c8/1d42ce51d65e571ab53d466cae83434325a126811df7ce4861d9d97bee4b/pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605", size = 2111437 }, - { url = "https://files.pythonhosted.org/packages/aa/c9/7fea9d13383c2ec6865919e09cffe44ab77e911eb281b53a4deaafd4c8e8/pydantic_core-2.23.4-cp39-none-win32.whl", hash = "sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6", size = 1735049 }, - { url = "https://files.pythonhosted.org/packages/98/95/dd7045c4caa2b73d0bf3b989d66b23cfbb7a0ef14ce99db15677a000a953/pydantic_core-2.23.4-cp39-none-win_amd64.whl", hash = "sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29", size = 1920180 }, - { url = "https://files.pythonhosted.org/packages/13/a9/5d582eb3204464284611f636b55c0a7410d748ff338756323cb1ce721b96/pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5", size = 1857135 }, - { url = "https://files.pythonhosted.org/packages/2c/57/faf36290933fe16717f97829eabfb1868182ac495f99cf0eda9f59687c9d/pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec", size = 1740583 }, - { url = "https://files.pythonhosted.org/packages/91/7c/d99e3513dc191c4fec363aef1bf4c8af9125d8fa53af7cb97e8babef4e40/pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480", size = 1793637 }, - { url = "https://files.pythonhosted.org/packages/29/18/812222b6d18c2d13eebbb0f7cdc170a408d9ced65794fdb86147c77e1982/pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068", size = 1941963 }, - { url = "https://files.pythonhosted.org/packages/0f/36/c1f3642ac3f05e6bb4aec3ffc399fa3f84895d259cf5f0ce3054b7735c29/pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801", size = 1915332 }, - { url = "https://files.pythonhosted.org/packages/f7/ca/9c0854829311fb446020ebb540ee22509731abad886d2859c855dd29b904/pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728", size = 1957926 }, - { url = "https://files.pythonhosted.org/packages/c0/1c/7836b67c42d0cd4441fcd9fafbf6a027ad4b79b6559f80cf11f89fd83648/pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433", size = 2100342 }, - { url = "https://files.pythonhosted.org/packages/a9/f9/b6bcaf874f410564a78908739c80861a171788ef4d4f76f5009656672dfe/pydantic_core-2.23.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753", size = 1920344 }, - { url = "https://files.pythonhosted.org/packages/32/fd/ac9cdfaaa7cf2d32590b807d900612b39acb25e5527c3c7e482f0553025b/pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21", size = 1857850 }, - { url = "https://files.pythonhosted.org/packages/08/fe/038f4b2bcae325ea643c8ad353191187a4c92a9c3b913b139289a6f2ef04/pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb", size = 1740265 }, - { url = "https://files.pythonhosted.org/packages/51/14/b215c9c3cbd1edaaea23014d4b3304260823f712d3fdee52549b19b25d62/pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59", size = 1793912 }, - { url = "https://files.pythonhosted.org/packages/62/de/2c3ad79b63ba564878cbce325be725929ba50089cd5156f89ea5155cb9b3/pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577", size = 1942870 }, - { url = "https://files.pythonhosted.org/packages/cb/55/c222af19e4644c741b3f3fe4fd8bbb6b4cdca87d8a49258b61cf7826b19e/pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744", size = 1915610 }, - { url = "https://files.pythonhosted.org/packages/c4/7a/9a8760692a6f76bb54bcd43f245ff3d8b603db695899bbc624099c00af80/pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef", size = 1958403 }, - { url = "https://files.pythonhosted.org/packages/4c/91/9b03166feb914bb5698e2f6499e07c2617e2eebf69f9374d0358d7eb2009/pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8", size = 2101154 }, - { url = "https://files.pythonhosted.org/packages/1d/d9/1d7ecb98318da4cb96986daaf0e20d66f1651d0aeb9e2d4435b916ce031d/pydantic_core-2.23.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e", size = 1920855 }, -] - [[package]] name = "pytest" version = "8.3.3"