diff --git a/.gitignore b/.gitignore
index 3f65bf3..8a52af8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
+.vscode/
+
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 0e06ff5..9b8fb29 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.6.0
+ rev: v5.0.0
hooks:
- id: check-added-large-files
- id: check-case-conflict
@@ -11,12 +11,12 @@ repos:
args: [--markdown-linebreak-ext=md]
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.6.8
+ rev: v0.6.9
hooks:
- id: ruff
- id: ruff-format
- repo: https://github.com/RobertCraigie/pyright-python
- rev: v1.1.382.post1
+ rev: v1.1.384
hooks:
- id: pyright
diff --git a/README.md b/README.md
index 2a3a6b3..9086412 100644
--- a/README.md
+++ b/README.md
@@ -23,13 +23,13 @@ db = connect("your-api-key", "your-project-id")
You can get an API key by fetching it in the [dashboard](https://base.contiguity.co), and a project ID is given to you when creating a project.
##
For those moving from Deta Space
-Contiguity Base is a one-to-one replacement for the old Deta Base API, Deta Base JavaScript SDK, Deta Base Python SDK, and Deta Base Go SDK. The only thing that has changed is initialization.
+Contiguity Base is a one-to-one replacement for the old Deta Base API, Deta Base JavaScript SDK, Deta Base Python SDK, and Deta Base Go SDK. The only thing that has changed is initialization.
Instead of `deta = Deta(project_key)`, you'll use `db = connect(api_key, project_id)`
The rest stays the same, because at Contiguity, we think it's crazy for a cloud provider to give you 45 days to move dozens of apps from their proprietary database.
-If you're transitioning from Deta Space to Contiguity, welcome!
+If you're transitioning from Deta Space to Contiguity, welcome!
## Creating your first "base" 📊
@@ -129,7 +129,7 @@ results = my_base.fetch({
```python
{
- "age": 22,
+ "age": 22,
"name": "Sarah"
}
```
@@ -137,7 +137,7 @@ results = my_base.fetch({
- **Hierarchical**
```python
{
- "user.profile.age": 22,
+ "user.profile.age": 22,
"user.profile.name": "Sarah"
}
```
@@ -152,8 +152,8 @@ results = my_base.fetch({
- **Nested Object**
```python
{
- "time": {
- "day": "Tuesday",
+ "time": {
+ "day": "Tuesday",
"hour": "08:00"
}
}
diff --git a/api.md b/api.md
new file mode 100644
index 0000000..9cd621b
--- /dev/null
+++ b/api.md
@@ -0,0 +1,189 @@
+# Base API
+
+## GET /items/{key}
+
+SDK method: `get(key: str) -> Item`
+
+Response `200`:
+Returns a single item.
+
+```json
+{
+ "key": "foo",
+ "field1": "bar"
+ // ...other item fields
+}
+```
+
+Response `404`:
+Key does not exist.
+
+```text
+No content
+```
+
+## DELETE /items/{key}
+
+SDK method: `delete(key: str) -> None`
+
+Response `204`:
+Returns nothing.
+
+```text
+No content
+```
+
+## POST /items
+
+SDK method: `insert(item: Item) -> Item`
+
+Request body:
+
+```json
+{
+ "item": {
+ "key": "foo",
+ "field1": "bar",
+ // ...other item fields
+ "__expires": 1727765807 // optional Unix timestamp
+ }
+}
+```
+
+Response `201`:
+Returns a list of items.
+
+```json
+[
+ {
+ "key": "foo",
+ "field1": "bar"
+ // ...other item fields
+ }
+ // ...other items
+]
+```
+
+Response `409`:
+Key already exists.
+
+```text
+No content
+```
+
+## PUT /items
+
+SDK method: `put(items: Item[]) -> Item[]`
+
+Request body:
+
+```json
+{
+ "items": [
+ {
+ "key": "foo",
+ "field1": "bar",
+ // ...other item fields
+ "__expires": 1727765807 // optional Unix timestamp
+ }
+ // ...other items
+ ]
+}
+```
+
+Response `201`:
+Returns a list of items.
+
+```json
+[
+ {
+ "key": "foo",
+ "field1": "bar"
+ // ...other item fields
+ }
+ // ...other items
+]
+```
+
+## PATCH /items/{key}
+
+SDK method: `update(updates: Mapping, key: str) -> Item`
+
+Request body:
+
+```json
+{
+ "set": {
+ "field1": "bar",
+ // ...other item fields
+ "__expires": 1727765807 // optional Unix timestamp
+ },
+ "increment": {
+ "field2": 1,
+ "field3": -2
+ },
+ "append": {
+ "field4": ["baz"]
+ },
+ "prepend": {
+ "field5": ["foo", "bar"]
+ },
+ "delete": ["field6"]
+}
+```
+
+Response `200`:
+Returns a single item.
+
+```json
+{
+ "key": "foo",
+ "field1": "bar"
+ // ...other item fields
+}
+```
+
+Response `404`:
+Key does not exist.
+
+```text
+No content
+```
+
+## GET /query
+
+SDK method: `fetch(*query: Query) -> Item[]`
+
+Request body:
+
+```json
+{
+ "limit": 10,
+ "last_key": "foo",
+ "query": [
+ {
+ "field1": "foo",
+ "field4?contains": "bar"
+ // multiple conditions are ANDed
+ },
+ {
+ "field3?gt": 2
+ }
+ // multiple queries are ORed
+ ]
+}
+```
+
+Response `200`:
+Returns a list of items.
+
+```json
+[
+ {
+ "key": "foo",
+ "field1": "bar"
+ // ...other item fields
+ }
+ // ...other items
+]
+```
diff --git a/examples/__init__.py b/examples/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/examples/basic_usage.py b/examples/basic_usage.py
index 0802e0f..a3805e4 100644
--- a/examples/basic_usage.py
+++ b/examples/basic_usage.py
@@ -1,44 +1,40 @@
-from contiguity_base import connect
-
-# Initialize the Contiguity client
-db = connect("api-key", "project-id")
-
-# Create a Base instance
-my_base = db.Base("randomBase9000")
-
-# Helper function to print results
-def print_result(operation, result):
- print(f"{operation} result: {result}")
- if result is None:
- print(f"Warning: {operation} operation failed!")
-
-# Put an item with a specific key
-result1 = my_base.put({"value": "Hello world!"}, "my-key-py")
-print_result("Put", result1)
-
-# Insert an item with a specific key
-result2 = my_base.insert({"name": "John Doe", "age": 30}, "john-doe-py")
-print_result("Insert", result2)
-
-# Insert an item with a specific key and expireIn
-result3 = my_base.insert({"name": "Jane Doe", "age": 28}, "jane-doe-py", expire_in=3600)
-print_result("Insert with expireIn", result3)
-
-# Get an item
-get_result = my_base.get("john-doe-py")
-print_result("Get", get_result)
-
-# Update an item
-update_result = my_base.update({"age": 31}, "john-doe-py")
-print_result("Update", update_result)
-
-# Fetch items
-try:
- fetch_result = my_base.fetch({"age": {"$gt": 25}}, limit=10)
- print_result("Fetch", fetch_result)
-except Exception as e:
- print(f"Fetch operation failed: {str(e)}")
-
-# Delete an item
-delete_result = my_base.delete("jane-doe-py")
-print_result("Delete", delete_result)
\ No newline at end of file
+# ruff: noqa: T201
+from contiguity_base import Base
+
+# Create a Base instance.
+db = Base("my-base")
+
+# Put an item with a specific key.
+put_result = db.put({"key": "foo", "value": "Hello world!"})
+print("Put result:", put_result)
+
+# Put multiple items.
+put_result = db.put({"key": "bar", "value": "Bar"}, {"key": "baz", "value": "Baz"})
+print("Put many result:", put_result)
+
+# Insert an item with a specific key.
+insert_result = db.insert({"key": "john-doe", "name": "John Doe", "age": 30})
+print("Insert result:", insert_result)
+
+# Insert an item with a specific key that expires in 1 hour.
+expiring_insert_result = db.insert({"key": "jane-doe", "name": "Jane Doe", "age": 28}, expire_in=3600)
+print("Insert with expiry result:", expiring_insert_result)
+
+# Get an item using a key.
+get_result = db.get("foo")
+print("Get result:", get_result)
+
+# Update an item.
+update_result = db.update({"age": db.util.increment(2), "name": "Mr. Doe"}, key="john-doe")
+print("Update result:", update_result)
+
+# Query items.
+query_result = db.query({"age?gt": 25}, limit=10)
+print("Query result:", query_result)
+
+# Delete an item.
+db.delete("jane-doe-py")
+
+# Delete all items.
+for item in db.query().items:
+ db.delete(str(item["key"]))
diff --git a/examples/pydantic_usage.py b/examples/pydantic_usage.py
new file mode 100644
index 0000000..db9bd17
--- /dev/null
+++ b/examples/pydantic_usage.py
@@ -0,0 +1,69 @@
+# ruff: noqa: T201
+from pydantic import BaseModel
+
+from contiguity_base import Base
+
+
+# Create a Pydantic model for the item.
+class MyItem(BaseModel):
+ key: str # Make sure to include the key field.
+ name: str
+ age: int
+ interests: list[str] = []
+
+
+# Create a Base instance.
+# Static type checking will work with the Pydantic model.
+db = Base("members", item_type=MyItem)
+
+# Put an item with a specific key.
+put_result = db.put(
+ MyItem(
+ key="foo",
+ name="John Doe",
+ age=30,
+ interests=["Python", "JavaScript"],
+ ),
+)
+print("Put result:", put_result)
+
+# Put multiple items.
+put_result = db.put(
+ MyItem(key="bar", name="Jane Doe", age=28),
+ MyItem(key="baz", name="Alice", age=25),
+)
+print("Put many result:", put_result)
+
+# Insert an item with a specific key.
+insert_result = db.insert(
+ MyItem(
+ key="xyz",
+ name="Arthur",
+ age=33,
+ interests=["Anime", "Music"],
+ ),
+)
+print("Insert result:", insert_result)
+
+# Insert an item with a specific key that expires in 1 hour.
+expiring_insert_result = db.insert(MyItem(key="abc", name="David", age=20), expire_in=3600)
+print("Insert with expiry result:", expiring_insert_result)
+
+# Get an item using a key.
+get_result = db.get("foo")
+print("Get result:", get_result)
+
+# Update an item.
+update_result = db.update({"age": db.util.increment(2), "name": "Mr. Doe"}, key="foo")
+print("Update result:", update_result)
+
+# Query items.
+query_result = db.query({"age?gt": 25}, limit=10)
+print("Query result:", query_result)
+
+# Delete an item.
+db.delete("bar")
+
+# Delete all items.
+for item in db.query().items:
+ db.delete(str(item.key))
diff --git a/pyproject.toml b/pyproject.toml
index 9520edb..0fef85e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -31,7 +31,9 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
- "requests>=2.25.1,<3.0.0",
+ "httpx>=0.27.2",
+ "pydantic>=2.9.0,<3.0.0",
+ "typing-extensions>=4.12.2,<5.0.0",
]
[project.urls]
@@ -46,6 +48,9 @@ version = {attr = "contiguity_base.__version__"}
[tool.uv]
dev-dependencies = [
"pre-commit~=3.8.0",
+ "pytest~=8.3.3",
+ "pytest-cov~=5.0.0",
+ "python-dotenv~=1.0.1",
]
[tool.ruff]
@@ -60,3 +65,4 @@ ignore = ["A", "D"]
[tool.pyright]
venvPath = "."
venv = ".venv"
+reportUnnecessaryTypeIgnoreComment = true
diff --git a/src/contiguity_base/__init__.py b/src/contiguity_base/__init__.py
index 99993ab..e70cb0f 100644
--- a/src/contiguity_base/__init__.py
+++ b/src/contiguity_base/__init__.py
@@ -1,9 +1,11 @@
-from .base import Base, Contiguity, Util, connect
+from .base import Base, BaseItem, InvalidKeyError, ItemConflictError, ItemNotFoundError, QueryResponse
__all__ = (
"Base",
- "Contiguity",
- "Util",
- "connect",
+ "BaseItem",
+ "InvalidKeyError",
+ "ItemConflictError",
+ "ItemNotFoundError",
+ "QueryResponse",
)
__version__ = "1.0.0"
diff --git a/src/contiguity_base/_auth.py b/src/contiguity_base/_auth.py
new file mode 100644
index 0000000..a390e37
--- /dev/null
+++ b/src/contiguity_base/_auth.py
@@ -0,0 +1,23 @@
+from __future__ import annotations
+
+import os
+
+
+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"
+ raise ValueError(msg)
+ return value
+
+
+def get_contiguity_token() -> str:
+ return _get_env_var("CONTIGUITY_TOKEN", "Contiguity token")
+
+
+def get_base_token() -> str:
+ return _get_env_var("CONTIGUITY_BASE_TOKEN", "Base token")
+
+
+def get_project_id() -> str:
+ return _get_env_var("CONTIGUITY_PROJECT_ID", "Project ID")
diff --git a/src/contiguity_base/_client.py b/src/contiguity_base/_client.py
new file mode 100644
index 0000000..1f67c92
--- /dev/null
+++ b/src/contiguity_base/_client.py
@@ -0,0 +1,50 @@
+from __future__ import annotations
+
+from httpx import AsyncClient as HttpxAsyncClient
+from httpx import Client as HttpxClient
+
+from contiguity_base._auth import get_contiguity_token
+
+
+class ApiError(Exception):
+ pass
+
+
+class ApiClient(HttpxClient):
+ def __init__(
+ self: ApiClient,
+ *,
+ base_url: str = "https://api.contiguity.co",
+ api_key: str | None = None,
+ timeout: int = 5,
+ ) -> None:
+ if not api_key:
+ api_key = get_contiguity_token()
+ super().__init__(
+ headers={
+ "Content-Type": "application/json",
+ "X-API-Key": api_key,
+ },
+ timeout=timeout,
+ base_url=base_url,
+ )
+
+
+class AsyncApiClient(HttpxAsyncClient):
+ def __init__(
+ self: AsyncApiClient,
+ *,
+ base_url: str = "https://api.contiguity.co",
+ api_key: str | None = None,
+ timeout: int = 5,
+ ) -> None:
+ if not api_key:
+ api_key = get_contiguity_token()
+ super().__init__(
+ headers={
+ "Content-Type": "application/json",
+ "X-API-Key": api_key,
+ },
+ timeout=timeout,
+ base_url=base_url,
+ )
diff --git a/src/contiguity_base/base.py b/src/contiguity_base/base.py
index 06b0f1e..980b3da 100644
--- a/src/contiguity_base/base.py
+++ b/src/contiguity_base/base.py
@@ -1,152 +1,438 @@
-import requests
-from typing import Union, List, Dict, Any, Optional
-from datetime import datetime
+# TODO @lemonyte: todo list. # noqa: TD003, FIX002
+# - [ ] new docstrings
+# - [ ] test expiring items
+# - [ ] support dataclasses
+# - [ ] support models for queries
+# - [ ] examples
+# - [ ] add async
+# - [ ] add drive support
+# - [ ] merge into main sdk
+from __future__ import annotations
-class Util:
- @staticmethod
- def increment(value: int = 1) -> Dict[str, Any]:
- return {"__op": "increment", "value": value}
+import json
+import os
+from collections.abc import Mapping, Sequence
+from datetime import datetime, timedelta, timezone
+from http import HTTPStatus
+from typing import TYPE_CHECKING, Any, Generic, Literal, TypeVar, Union, overload
+from urllib.parse import quote
+from warnings import warn
- @staticmethod
- def append(value: Any) -> Dict[str, Any]:
- return {"__op": "append", "value": value}
+from httpx import HTTPStatusError
+from pydantic import BaseModel, TypeAdapter
+from pydantic import JsonValue as DataType
+from typing_extensions import deprecated
- @staticmethod
- def prepend(value: Any) -> Dict[str, Any]:
- return {"__op": "prepend", "value": value}
+from contiguity_base._auth import get_base_token, get_project_id
+from contiguity_base._client import ApiClient, ApiError
- @staticmethod
- def trim() -> Dict[str, str]:
- return {"__op": "trim"}
+if TYPE_CHECKING:
+ from httpx import Response as HttpxResponse
+ from typing_extensions import Self
+TimestampType = Union[int, datetime]
+QueryType = Mapping[str, DataType]
-class Base:
- def __init__(self, db: 'Contiguity', name: str):
- self.db = db
- self.name = name
- self.util = Util()
+ItemType = Union[Mapping[str, Any], BaseModel]
+ItemT = TypeVar("ItemT", bound=ItemType)
+DefaultItemT = TypeVar("DefaultItemT")
- def _fetch(self, method: str, path: str, body: Optional[Dict] = None) -> Optional[Dict]:
- url = f"{self.db.base_url}/{self.db.project_id}/{self.name}{path}"
- headers = {
- "x-api-key": self.db.api_key,
- "Content-Type": "application/json",
- }
- if self.db.debug:
- print(f"Sending {method} request to: {url}")
+class _Unset:
+ pass
- try:
- response = requests.request(
- method, url, headers=headers, json=body)
- response.raise_for_status()
- return response.json()
- except requests.exceptions.HTTPError as e:
- if e.response.status_code == 404:
- return None
- print(
- f"HTTP error! status: {e.response.status_code}, body: {e.response.text}")
- except requests.exceptions.RequestException as e:
- if self.db.debug:
- print(f"Request failed: {str(e)}")
- return None
-
- def put(self, data: Union[Dict, List[Dict]], key: Optional[str] = None, expire_in: Optional[int] = None, expire_at: Optional[Union[datetime, str]] = None) -> Optional[Dict]:
- path = "/items"
-
- if isinstance(data, dict):
- if key:
- data['key'] = key
- items = [data]
- elif isinstance(data, list):
- items = data
- else:
- raise ValueError(
- "Data must be either a dictionary or a list of dictionaries")
- request_body = {"items": items}
+_UNSET = _Unset()
- if expire_in is not None:
- request_body["expireIn"] = expire_in
- if expire_at is not None:
- request_body["expireAt"] = expire_at
+class BaseItem(BaseModel):
+ key: str
- return self._fetch("PUT", path, request_body)
- def get(self, key: str) -> Optional[Dict]:
- return self._fetch("GET", f"/items/{key}")
+class ItemConflictError(ApiError):
+ def __init__(self, key: str, *args: object) -> None:
+ super().__init__(f"item with key '{key}' already exists", *args)
- def delete(self, key: str) -> Optional[Dict]:
- return self._fetch("DELETE", f"/items/{key}")
- def update(self, updates: Dict[str, Any], key: str, expire_in: Optional[int] = None, expire_at: Optional[Union[datetime, str]] = None) -> Optional[Dict]:
- processed_updates = {
- field: (value if isinstance(value, dict) and "__op" in value else {"__op": "set", "value": value})
- for field, value in updates.items()
- }
-
- request_body = {"updates": processed_updates}
-
- if expire_in is not None:
- request_body["expireIn"] = expire_in
-
- if expire_at is not None:
- if isinstance(expire_at, datetime):
- request_body["expireAt"] = expire_at.isoformat()
- else:
- request_body["expireAt"] = expire_at
+class ItemNotFoundError(ApiError):
+ def __init__(self, key: str, *args: object) -> None:
+ super().__init__(f"key '{key}' not found", *args)
- return self._fetch("PATCH", f"/items/{key}", request_body)
- def fetch(self, query: Optional[Dict] = None, limit: Optional[int] = None, last: Optional[str] = None) -> Dict[str, Any]:
- query_params = {
- "query": query,
- "limit": limit,
- "last": last
- }
- query_params = {k: v for k, v in query_params.items() if v is not None}
+class InvalidKeyError(ValueError):
+ def __init__(self, key: str, *args: object) -> None:
+ super().__init__(f"invalid key '{key}'", *args)
- if self.db.debug:
- print(f"Fetch params sent to server: {query_params}")
- response = self._fetch("POST", "/query", query_params)
- return {
- "items": response.get("items", []),
- "last": response.get("last"),
- "count": response.get("count", 0)
- }
+class QueryResponse(BaseModel, Generic[ItemT]):
+ count: int = 0
+ last_key: Union[str, None] = None # noqa: UP007 Pydantic doesn't support `X | Y` syntax in Python 3.9.
+ items: Sequence[ItemT] = []
- def insert(self, data: Dict, key: Optional[str] = None, expire_in: Optional[int] = None, expire_at: Optional[Union[datetime, str]] = None) -> Optional[Dict]:
- path = "/items"
-
- if key:
- data['key'] = key
+class _UpdateOperation:
+ pass
- request_body = {"item": data}
- if expire_in is not None:
- request_body["expireIn"] = expire_in
-
- if expire_at is not None:
- if isinstance(expire_at, datetime):
- request_body["expireAt"] = expire_at.isoformat()
- else:
- request_body["expireAt"] = expire_at
+class _Trim(_UpdateOperation):
+ pass
+
+
+class _Increment(_UpdateOperation):
+ def __init__(self: _Increment, value: int = 1, /) -> None:
+ self.value = value
+
+
+class _Append(_UpdateOperation):
+ def __init__(self: _Append, value: DataType, /) -> None:
+ if isinstance(value, (list, tuple)):
+ self.value = value
+ else:
+ self.value = [value]
- return self._fetch("POST", path, request_body)
-class Contiguity:
- def __init__(self, api_key: str, project_id: str, debug: bool = False):
- self.api_key = api_key
- self.project_id = project_id
- self.base_url = "https://api.base.contiguity.co/v1"
- self.debug = debug
+class _Prepend(_Append):
+ pass
- def Base(self, name: str) -> Base:
- return Base(self, name)
-def connect(api_key: str, project_id: str, debug: bool = False) -> Contiguity:
- return Contiguity(api_key, project_id, debug)
\ No newline at end of file
+class _Updates:
+ @staticmethod
+ def trim() -> _Trim:
+ return _Trim()
+
+ @staticmethod
+ def increment(value: int = 1, /) -> _Increment:
+ return _Increment(value)
+
+ @staticmethod
+ def append(value: DataType, /) -> _Append:
+ return _Append(value)
+
+ @staticmethod
+ def prepend(value: DataType, /) -> _Prepend:
+ return _Prepend(value)
+
+
+class _UpdatePayload(BaseModel):
+ set: dict[str, DataType] = {}
+ increment: dict[str, int] = {}
+ append: dict[str, Sequence[DataType]] = {}
+ prepend: dict[str, Sequence[DataType]] = {}
+ delete: list[str] = []
+
+ @classmethod
+ def from_updates_mapping(cls: type[Self], updates: Mapping[str, DataType | _UpdateOperation], /) -> Self:
+ set = {}
+ increment = {}
+ append = {}
+ prepend = {}
+ delete = []
+ for attr, value in updates.items():
+ if isinstance(value, _UpdateOperation):
+ if isinstance(value, _Trim):
+ delete.append(attr)
+ elif isinstance(value, _Increment):
+ increment[attr] = value.value
+ # Prepend must be checked before Append because it's a subclass of Append.
+ elif isinstance(value, _Prepend):
+ prepend[attr] = value.value
+ elif isinstance(value, _Append):
+ append[attr] = value.value
+ else:
+ set[attr] = value
+ return cls(
+ set=set,
+ increment=increment,
+ append=append,
+ prepend=prepend,
+ delete=delete,
+ )
+
+
+def _check_key(key: str, /) -> str:
+ if not key:
+ raise InvalidKeyError(key)
+ return quote(key, safe="")
+
+
+class Base(Generic[ItemT]):
+ EXPIRES_ATTRIBUTE = "__expires"
+ PUT_LIMIT = 30
+
+ @overload
+ def __init__(
+ self: Self,
+ name: str,
+ /,
+ *,
+ item_type: type[ItemT] | None = None,
+ base_token: str | None = None,
+ project_id: str | None = None,
+ host: str | None = None,
+ api_version: str = "v1",
+ json_decoder: type[json.JSONDecoder] = json.JSONDecoder,
+ ) -> None: ...
+
+ @overload
+ @deprecated("The `project_key` parameter has been renamed to `base_token`.")
+ def __init__(
+ self: Self,
+ name: str,
+ /,
+ *,
+ item_type: type[ItemT] | None = None,
+ project_key: str | None = None,
+ project_id: str | None = None,
+ host: str | None = None,
+ api_version: str = "v1",
+ json_decoder: type[json.JSONDecoder] = json.JSONDecoder,
+ ) -> None: ...
+
+ def __init__( # noqa: PLR0913
+ self: Self,
+ name: str,
+ /,
+ *,
+ item_type: type[ItemT] | None = None,
+ base_token: str | None = None,
+ project_key: str | None = None, # Deprecated.
+ 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.
+ ) -> None:
+ if not name:
+ msg = f"invalid Base name '{name}'"
+ raise ValueError(msg)
+
+ self.name = name
+ self.item_type = item_type
+ self.base_token = base_token or project_key or get_base_token()
+ self.project_id = project_id or get_project_id()
+ self.host = host or os.getenv("CONTIGUITY_BASE_HOST") or "api.base.contiguity.co"
+ self.api_version = api_version
+ self.json_decoder = json_decoder
+ self.util = _Updates()
+ self._client = ApiClient(
+ base_url=f"https://{self.host}/{api_version}/{self.project_id}/{self.name}",
+ api_key=self.base_token,
+ timeout=300,
+ )
+
+ @overload
+ def _response_as_item_type(
+ self: Self,
+ response: HttpxResponse,
+ /,
+ *,
+ sequence: Literal[False] = False,
+ ) -> ItemT: ...
+ @overload
+ def _response_as_item_type(
+ self: Self,
+ response: HttpxResponse,
+ /,
+ *,
+ sequence: Literal[True] = True,
+ ) -> Sequence[ItemT]: ...
+
+ def _response_as_item_type(
+ self: Self,
+ response: HttpxResponse,
+ /,
+ *,
+ sequence: bool = False,
+ ) -> ItemT | Sequence[ItemT]:
+ try:
+ response.raise_for_status()
+ except HTTPStatusError as exc:
+ 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 response.json(cls=self.json_decoder)
+
+ def _insert_expires_attr(
+ self: Self,
+ item: ItemT | Mapping[str, DataType],
+ expire_in: int | None = None,
+ expire_at: TimestampType | None = None,
+ ) -> dict[str, DataType]:
+ if expire_in and expire_at:
+ msg = "cannot use both expire_in and expire_at"
+ raise ValueError(msg)
+
+ item_dict = item.model_dump() if isinstance(item, BaseModel) else dict(item)
+
+ if not expire_in and not expire_at:
+ return item_dict
+ if expire_in:
+ expire_at = datetime.now(tz=timezone.utc) + timedelta(seconds=expire_in)
+ if isinstance(expire_at, datetime):
+ expire_at = int(expire_at.replace(microsecond=0).timestamp())
+ if not isinstance(expire_at, int):
+ msg = "expire_at should be a datetime or int"
+ raise TypeError(msg)
+
+ item_dict[self.EXPIRES_ATTRIBUTE] = expire_at
+ return item_dict
+
+ @overload
+ def get(self: Self, key: str, /) -> ItemT | None: ...
+
+ @overload
+ def get(self: Self, key: str, /, *, default: ItemT) -> ItemT: ...
+
+ @overload
+ def get(self: Self, key: str, /, *, default: DefaultItemT) -> ItemT | DefaultItemT: ...
+
+ def get(
+ self: Self,
+ key: str,
+ /,
+ *,
+ default: ItemT | DefaultItemT | _Unset = _UNSET,
+ ) -> ItemT | DefaultItemT | None:
+ key = _check_key(key)
+ response = self._client.get(f"/items/{key}")
+ if response.status_code == HTTPStatus.NOT_FOUND:
+ if not isinstance(default, _Unset):
+ return default
+ msg = (
+ "ItemNotFoundError will be raised in the future."
+ " To receive None for non-existent keys, set default=None."
+ )
+ warn(DeprecationWarning(msg), stacklevel=2)
+ return None
+
+ return self._response_as_item_type(response, sequence=False)
+
+ def delete(self: Self, key: str, /) -> None:
+ """Delete an item from the Base."""
+ key = _check_key(key)
+ response = self._client.delete(f"/items/{key}")
+ try:
+ response.raise_for_status()
+ except HTTPStatusError as exc:
+ raise ApiError(exc.response.text) from exc
+
+ def insert(
+ self: Self,
+ item: ItemT,
+ /,
+ *,
+ expire_in: int | None = None,
+ expire_at: TimestampType | None = None,
+ ) -> ItemT:
+ item_dict = self._insert_expires_attr(item, expire_in=expire_in, expire_at=expire_at)
+ response = self._client.post("/items", json={"item": item_dict})
+
+ if response.status_code == HTTPStatus.CONFLICT:
+ raise ItemConflictError(str(item_dict.get("key")))
+
+ if not (returned_item := self._response_as_item_type(response, sequence=True)):
+ msg = "expected a single item, got an empty response"
+ raise ApiError(msg)
+ return returned_item[0]
+
+ def put(
+ self: Self,
+ *items: ItemT,
+ expire_in: int | None = None,
+ expire_at: TimestampType | None = None,
+ ) -> Sequence[ItemT]:
+ """store (put) an item in the database. Overrides an item if key already exists.
+ `key` could be provided as function argument or a field in the data dict.
+ If `key` is not provided, the server will generate a random 12 chars key.
+ """
+ if not items:
+ return []
+ if len(items) > self.PUT_LIMIT:
+ msg = f"cannot put more than {self.PUT_LIMIT} items at a time"
+ raise ValueError(msg)
+
+ item_dicts = [self._insert_expires_attr(item, expire_in=expire_in, expire_at=expire_at) for item in items]
+ response = self._client.put("/items", json={"items": item_dicts})
+ return self._response_as_item_type(response, sequence=True)
+
+ @deprecated("This method will be removed in a future release. You can pass multiple items to `put`.")
+ def put_many(
+ self: Self,
+ items: Sequence[ItemT],
+ /,
+ *,
+ expire_in: int | None = None,
+ expire_at: TimestampType | None = None,
+ ) -> Sequence[ItemT]:
+ return self.put(*items, expire_in=expire_in, expire_at=expire_at)
+
+ def update(
+ self: Self,
+ updates: Mapping[str, DataType | _UpdateOperation],
+ /,
+ *,
+ key: str,
+ expire_in: int | None = None,
+ expire_at: TimestampType | None = None,
+ ) -> ItemT:
+ """update an item in the database
+ `updates` specifies the attribute names and values to update,add or remove
+ `key` is the key of the item to be updated
+ """
+ key = _check_key(key)
+ if not updates:
+ msg = "no updates provided"
+ raise ValueError(msg)
+
+ payload = _UpdatePayload.from_updates_mapping(updates)
+ payload.set = self._insert_expires_attr(
+ payload.set,
+ expire_in=expire_in,
+ expire_at=expire_at,
+ )
+
+ response = self._client.patch(f"/items/{key}", json={"updates": payload.model_dump()})
+ if response.status_code == HTTPStatus.NOT_FOUND:
+ raise ItemNotFoundError(key)
+
+ return self._response_as_item_type(response, sequence=False)
+
+ def query(
+ self: Self,
+ *queries: QueryType,
+ limit: int = 1000,
+ last: str | None = None,
+ ) -> QueryResponse[ItemT]:
+ """fetch items from the database.
+ `query` is an optional filter or list of filters. Without filter, it will return the whole db.
+ """
+
+ payload = {
+ "limit": limit,
+ "last_key": last,
+ }
+
+ if queries:
+ payload["query"] = queries
+
+ response = self._client.post("/query", json=payload)
+ try:
+ 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
+
+ @deprecated("This method has been renamed to `query` and will be removed in a future release.")
+ def fetch(
+ self: Self,
+ *queries: QueryType,
+ limit: int = 1000,
+ last: str | None = None,
+ ) -> QueryResponse[ItemT]:
+ return self.query(*queries, limit=limit, last=last)
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..97f1811
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1,6 @@
+import random
+import string
+
+
+def random_string(length: int = 10) -> str:
+ return "".join(random.choices(string.ascii_lowercase, k=length)) # noqa: S311
diff --git a/tests/test_base_dict_typed.py b/tests/test_base_dict_typed.py
new file mode 100644
index 0000000..1102c37
--- /dev/null
+++ b/tests/test_base_dict_typed.py
@@ -0,0 +1,171 @@
+# ruff: noqa: S101, S311, PLR2004
+import random
+from collections.abc import Generator, Mapping
+from typing import Any
+
+import pytest
+from dotenv import load_dotenv
+from pydantic import JsonValue
+
+from contiguity_base import Base, InvalidKeyError, ItemConflictError, ItemNotFoundError, QueryResponse
+from tests import random_string
+
+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]:
+ base = Base("test_base_dict_typed", item_type=DictItemType)
+ for item in base.query().items:
+ base.delete(str(item["key"]))
+ yield base
+ for item in base.query().items:
+ base.delete(str(item["key"]))
+
+
+def test_bad_base_name() -> None:
+ with pytest.raises(ValueError, match="invalid Base name ''"):
+ Base("", item_type=DictItemType)
+
+
+def test_bad_key(base: Base[DictItemType]) -> None:
+ with pytest.raises(InvalidKeyError):
+ base.get("")
+ with pytest.raises(InvalidKeyError):
+ base.delete("")
+ with pytest.raises(InvalidKeyError):
+ base.update({"foo": "bar"}, key="")
+
+
+def test_get(base: Base[DictItemType]) -> None:
+ item = create_test_item()
+ base.insert(item)
+ fetched_item = base.get("test_key")
+ assert fetched_item == item
+
+
+def test_get_nonexistent(base: Base[DictItemType]) -> None:
+ with pytest.warns(DeprecationWarning):
+ assert base.get("nonexistent_key") is None
+
+
+def test_get_default(base: Base[DictItemType]) -> 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[DictItemType]) -> None:
+ item = create_test_item()
+ base.insert(item)
+ base.delete("test_key")
+ with pytest.warns(DeprecationWarning):
+ assert base.get("test_key") is None
+
+
+def test_insert(base: Base[DictItemType]) -> None:
+ item = create_test_item()
+ inserted_item = base.insert(item)
+ assert inserted_item == item
+
+
+def test_insert_existing(base: Base[DictItemType]) -> None:
+ item = create_test_item()
+ base.insert(item)
+ with pytest.raises(ItemConflictError):
+ base.insert(item)
+
+
+def test_put(base: Base[DictItemType]) -> None:
+ items = [create_test_item(key=f"test_key_{i}", field1=i) for i in range(3)]
+ for _ in range(2):
+ response = base.put(*items)
+ assert response == items
+
+
+def test_put_empty(base: Base[DictItemType]) -> None:
+ items = []
+ response = base.put(*items)
+ assert response == items
+
+
+def test_put_too_many(base: Base[DictItemType]) -> 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[DictItemType]) -> None:
+ item = {
+ "key": "test_key",
+ "field1": random_string(),
+ "field2": random_string(),
+ "field3": 1,
+ "field4": 0,
+ "field5": ["foo", "bar"],
+ "field6": [1, 2],
+ "field7": {"foo": "bar"},
+ }
+ base.insert(item)
+ updated_item = base.update(
+ {
+ "field1": "updated_value",
+ "field2": base.util.trim(),
+ "field3": base.util.increment(2),
+ "field4": base.util.increment(-2),
+ "field5": base.util.append("baz"),
+ "field6": base.util.prepend([3, 4]),
+ },
+ key="test_key",
+ )
+ assert updated_item == {
+ "key": "test_key",
+ "field1": "updated_value",
+ "field3": item["field3"] + 2,
+ "field4": item["field4"] - 2,
+ "field5": [*item["field5"], "baz"],
+ "field6": [3, 4, *item["field6"]],
+ "field7": item["field7"],
+ }
+
+
+def test_update_nonexistent(base: Base[DictItemType]) -> None:
+ with pytest.raises(ItemNotFoundError):
+ base.update({"foo": "bar"}, key=random_string())
+
+
+def test_update_empty(base: Base[DictItemType]) -> None:
+ with pytest.raises(ValueError, match="no updates provided"):
+ base.update({}, key="test_key")
+
+
+def test_query_empty(base: Base[DictItemType]) -> 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[DictItemType]) -> 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})
+ assert response == QueryResponse(
+ count=3,
+ last_key=None,
+ items=[item for item in items if isinstance(item["field1"], int) and item["field1"] > 1],
+ )
diff --git a/tests/test_base_dict_untyped.py b/tests/test_base_dict_untyped.py
new file mode 100644
index 0000000..9b144cc
--- /dev/null
+++ b/tests/test_base_dict_untyped.py
@@ -0,0 +1,165 @@
+# 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_base import Base, InvalidKeyError, ItemConflictError, ItemNotFoundError, QueryResponse
+from tests import random_string
+
+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")
+ for item in base.query().items:
+ base.delete(str(item["key"]))
+ yield base
+ for item in base.query().items:
+ base.delete(str(item["key"]))
+
+
+def test_bad_base_name() -> None:
+ with pytest.raises(ValueError, match="invalid Base name ''"):
+ Base("")
+
+
+def test_bad_key(base: Base) -> None:
+ with pytest.raises(InvalidKeyError):
+ base.get("")
+ with pytest.raises(InvalidKeyError):
+ base.delete("")
+ with pytest.raises(InvalidKeyError):
+ base.update({"foo": "bar"}, key="")
+
+
+def test_get(base: Base) -> None:
+ item = create_test_item()
+ base.insert(item)
+ fetched_item = base.get("test_key")
+ assert fetched_item == item
+
+
+def test_get_nonexistent(base: Base) -> None:
+ with pytest.warns(DeprecationWarning):
+ assert base.get("nonexistent_key") is None
+
+
+def test_get_default(base: Base) -> 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) -> None:
+ item = create_test_item()
+ base.insert(item)
+ base.delete("test_key")
+ with pytest.warns(DeprecationWarning):
+ assert base.get("test_key") is None
+
+
+def test_insert(base: Base) -> None:
+ item = create_test_item()
+ inserted_item = base.insert(item)
+ assert inserted_item == item
+
+
+def test_insert_existing(base: Base) -> None:
+ item = create_test_item()
+ base.insert(item)
+ with pytest.raises(ItemConflictError):
+ base.insert(item)
+
+
+def test_put(base: Base) -> None:
+ items = [create_test_item(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) -> None:
+ items = []
+ response = base.put(*items)
+ assert response == items
+
+
+def test_put_too_many(base: Base) -> 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) -> None:
+ item = {
+ "key": "test_key",
+ "field1": random_string(),
+ "field2": random_string(),
+ "field3": 1,
+ "field4": 0,
+ "field5": ["foo", "bar"],
+ "field6": [1, 2],
+ "field7": {"foo": "bar"},
+ }
+ base.insert(item)
+ updated_item = base.update(
+ {
+ "field1": "updated_value",
+ "field2": base.util.trim(),
+ "field3": base.util.increment(2),
+ "field4": base.util.increment(-2),
+ "field5": base.util.append("baz"),
+ "field6": base.util.prepend([3, 4]),
+ },
+ key="test_key",
+ )
+ assert updated_item == {
+ "key": "test_key",
+ "field1": "updated_value",
+ "field3": item["field3"] + 2,
+ "field4": item["field4"] - 2,
+ "field5": [*item["field5"], "baz"],
+ "field6": [3, 4, *item["field6"]],
+ "field7": item["field7"],
+ }
+
+
+def test_update_nonexistent(base: Base) -> None:
+ with pytest.raises(ItemNotFoundError):
+ base.update({"foo": "bar"}, key=random_string())
+
+
+def test_update_empty(base: Base) -> None:
+ with pytest.raises(ValueError, match="no updates provided"):
+ base.update({}, key="test_key")
+
+
+def test_query_empty(base: Base) -> 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) -> 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})
+ assert response == QueryResponse(count=3, last_key=None, items=[item for item in items if item["field1"] > 1])
diff --git a/tests/test_base_model.py b/tests/test_base_model.py
new file mode 100644
index 0000000..21e936a
--- /dev/null
+++ b/tests/test_base_model.py
@@ -0,0 +1,156 @@
+# 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_base import Base, InvalidKeyError, ItemConflictError, ItemNotFoundError, QueryResponse
+from tests import random_string
+
+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)
+ for item in base.query().items:
+ base.delete(item.key)
+ yield base
+ for item in base.query().items:
+ base.delete(item.key)
+
+
+def test_bad_base_name() -> None:
+ with pytest.raises(ValueError, match="invalid Base name ''"):
+ Base("", item_type=TestItemModel)
+
+
+def test_bad_key(base: Base[TestItemModel]) -> None:
+ with pytest.raises(InvalidKeyError):
+ base.get("")
+ with pytest.raises(InvalidKeyError):
+ base.delete("")
+ with pytest.raises(InvalidKeyError):
+ base.update({"foo": "bar"}, key="")
+
+
+def test_get(base: Base[TestItemModel]) -> None:
+ item = TestItemModel()
+ base.insert(item)
+ fetched_item = base.get("test_key")
+ assert fetched_item == item
+
+
+def test_get_nonexistent(base: Base[TestItemModel]) -> 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()):
+ fetched_item = base.get("nonexistent_key", default=default_item)
+ assert fetched_item == default_item
+
+
+def test_delete(base: Base[TestItemModel]) -> None:
+ item = TestItemModel()
+ 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()
+ inserted_item = base.insert(item)
+ assert inserted_item == item
+
+
+def test_insert_existing(base: Base[TestItemModel]) -> None:
+ item = TestItemModel()
+ 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)]
+ for _ in range(2):
+ response = base.put(*items)
+ assert response == items
+
+
+def test_put_empty(base: Base[TestItemModel]) -> 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)]
+ 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()
+ base.insert(item)
+ updated_item = base.update(
+ {
+ "field1": base.util.trim(),
+ "field2": "updated_value",
+ "field3": base.util.increment(2),
+ "field4": base.util.increment(-2),
+ "field5": base.util.append("baz"),
+ "field6": base.util.prepend([3, 4]),
+ },
+ key="test_key",
+ )
+ assert updated_item == TestItemModel(
+ key="test_key",
+ field1=updated_item.field1,
+ field2="updated_value",
+ field3=item.field3 + 2,
+ field4=item.field4 - 2,
+ field5=[*item.field5, "baz"],
+ field6=[3, 4, *item.field6],
+ field7=item.field7,
+ )
+
+
+def test_update_nonexistent(base: Base[TestItemModel]) -> None:
+ with pytest.raises(ItemNotFoundError):
+ base.update({"foo": "bar"}, key=random_string())
+
+
+def test_update_empty(base: Base[TestItemModel]) -> 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)]
+ 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)]
+ 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/test_base_typeddict.py b/tests/test_base_typeddict.py
new file mode 100644
index 0000000..3b7681f
--- /dev/null
+++ b/tests/test_base_typeddict.py
@@ -0,0 +1,183 @@
+# ruff: noqa: S101, S311, PLR2004
+from __future__ import annotations
+
+import random
+from typing import TYPE_CHECKING, Any
+
+import pytest
+from dotenv import load_dotenv
+from typing_extensions import TypedDict
+
+from contiguity_base import Base, InvalidKeyError, ItemConflictError, ItemNotFoundError, QueryResponse
+from tests import random_string
+
+if TYPE_CHECKING:
+ from collections.abc import Generator
+
+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)
+ for item in base.query().items:
+ base.delete(item["key"])
+ yield base
+ for item in base.query().items:
+ base.delete(item["key"])
+
+
+def test_bad_base_name() -> None:
+ with pytest.raises(ValueError, match="invalid Base name ''"):
+ Base("", item_type=TestItemDict)
+
+
+def test_bad_key(base: Base[TestItemDict]) -> None:
+ with pytest.raises(InvalidKeyError):
+ base.get("")
+ with pytest.raises(InvalidKeyError):
+ base.delete("")
+ with pytest.raises(InvalidKeyError):
+ base.update({"foo": "bar"}, key="")
+
+
+def test_get(base: Base[TestItemDict]) -> 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:
+ with pytest.warns(DeprecationWarning):
+ assert base.get("nonexistent_key") is None
+
+
+def test_get_default(base: Base[TestItemDict]) -> 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:
+ item = create_test_item()
+ base.insert(item)
+ base.delete("test_key")
+ with pytest.warns(DeprecationWarning):
+ assert base.get("test_key") is None
+
+
+def test_insert(base: Base[TestItemDict]) -> None:
+ item = create_test_item()
+ inserted_item = base.insert(item)
+ assert inserted_item == item
+
+
+def test_insert_existing(base: Base[TestItemDict]) -> None:
+ item = create_test_item()
+ base.insert(item)
+ with pytest.raises(ItemConflictError):
+ base.insert(item)
+
+
+def test_put(base: Base[TestItemDict]) -> 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:
+ items = []
+ response = base.put(*items)
+ assert response == items
+
+
+def test_put_too_many(base: Base[TestItemDict]) -> 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:
+ item = create_test_item()
+ base.insert(item)
+ updated_item = base.update(
+ {
+ # Trim will not pass type validation when using TypedDict
+ # because TypedDict does not support default values.
+ "field2": "updated_value",
+ "field3": base.util.increment(2),
+ "field4": base.util.increment(-2),
+ "field5": base.util.append("baz"),
+ "field6": base.util.prepend([3, 4]),
+ },
+ key="test_key",
+ )
+ assert updated_item == TestItemDict(
+ key="test_key",
+ field1=item["field1"],
+ field2="updated_value",
+ field3=item["field3"] + 2,
+ field4=item["field4"] - 2,
+ field5=[*item["field5"], "baz"],
+ field6=[3, 4, *item["field6"]],
+ field7=item["field7"],
+ )
+
+
+def test_update_nonexistent(base: Base[TestItemDict]) -> None:
+ with pytest.raises(ItemNotFoundError):
+ base.update({"foo": "bar"}, key=random_string())
+
+
+def test_update_empty(base: Base[TestItemDict]) -> None:
+ with pytest.raises(ValueError, match="no updates provided"):
+ base.update({}, key="test_key")
+
+
+def test_query_empty(base: Base[TestItemDict]) -> 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:
+ 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})
+ assert response == QueryResponse(count=3, last_key=None, items=[item for item in items if item["field1"] > 1])
diff --git a/uv.lock b/uv.lock
index cf81c55..0d5fe20 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1,5 +1,33 @@
version = 1
requires-python = ">=3.9"
+resolution-markers = [
+ "python_full_version < '3.13'",
+ "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"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "idna" },
+ { name = "sniffio" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/78/49/f3f17ec11c4a91fe79275c426658e509b07547f874b14c1a526d86a83fc8/anyio-4.6.0.tar.gz", hash = "sha256:137b4559cbb034c477165047febb6ff83f390fc3b20bf181c1fc0a728cb8beeb", size = 170983 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9e/ef/7a4f225581a0d7886ea28359179cb861d7fbcdefad29663fc1167b86f69f/anyio-4.6.0-py3-none-any.whl", hash = "sha256:c7d2e9d63e31599eeb636c8c5c03a7e108d73b345f064f1c19fdc87b79036a9a", size = 89631 },
+]
[[package]]
name = "certifi"
@@ -20,72 +48,12 @@ wheels = [
]
[[package]]
-name = "charset-normalizer"
-version = "3.3.2"
+name = "colorama"
+version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/63/09/c1bc53dab74b1816a00d8d030de5bf98f724c52c1635e07681d312f20be8/charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", size = 104809 }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/2b/61/095a0aa1a84d1481998b534177c8566fdc50bb1233ea9a0478cd3cc075bd/charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", size = 194219 },
- { url = "https://files.pythonhosted.org/packages/cc/94/f7cf5e5134175de79ad2059edf2adce18e0685ebdb9227ff0139975d0e93/charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", size = 122521 },
- { url = "https://files.pythonhosted.org/packages/46/6a/d5c26c41c49b546860cc1acabdddf48b0b3fb2685f4f5617ac59261b44ae/charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", size = 120383 },
- { url = "https://files.pythonhosted.org/packages/b8/60/e2f67915a51be59d4539ed189eb0a2b0d292bf79270410746becb32bc2c3/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", size = 138223 },
- { url = "https://files.pythonhosted.org/packages/05/8c/eb854996d5fef5e4f33ad56927ad053d04dc820e4a3d39023f35cad72617/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", size = 148101 },
- { url = "https://files.pythonhosted.org/packages/f6/93/bb6cbeec3bf9da9b2eba458c15966658d1daa8b982c642f81c93ad9b40e1/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", size = 140699 },
- { url = "https://files.pythonhosted.org/packages/da/f1/3702ba2a7470666a62fd81c58a4c40be00670e5006a67f4d626e57f013ae/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", size = 142065 },
- { url = "https://files.pythonhosted.org/packages/3f/ba/3f5e7be00b215fa10e13d64b1f6237eb6ebea66676a41b2bcdd09fe74323/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", size = 144505 },
- { url = "https://files.pythonhosted.org/packages/33/c3/3b96a435c5109dd5b6adc8a59ba1d678b302a97938f032e3770cc84cd354/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", size = 139425 },
- { url = "https://files.pythonhosted.org/packages/43/05/3bf613e719efe68fb3a77f9c536a389f35b95d75424b96b426a47a45ef1d/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", size = 145287 },
- { url = "https://files.pythonhosted.org/packages/58/78/a0bc646900994df12e07b4ae5c713f2b3e5998f58b9d3720cce2aa45652f/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", size = 149929 },
- { url = "https://files.pythonhosted.org/packages/eb/5c/97d97248af4920bc68687d9c3b3c0f47c910e21a8ff80af4565a576bd2f0/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", size = 141605 },
- { url = "https://files.pythonhosted.org/packages/a8/31/47d018ef89f95b8aded95c589a77c072c55e94b50a41aa99c0a2008a45a4/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", size = 142646 },
- { url = "https://files.pythonhosted.org/packages/ae/d5/4fecf1d58bedb1340a50f165ba1c7ddc0400252d6832ff619c4568b36cc0/charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", size = 92846 },
- { url = "https://files.pythonhosted.org/packages/a2/a0/4af29e22cb5942488cf45630cbdd7cefd908768e69bdd90280842e4e8529/charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", size = 100343 },
- { url = "https://files.pythonhosted.org/packages/68/77/02839016f6fbbf808e8b38601df6e0e66c17bbab76dff4613f7511413597/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", size = 191647 },
- { url = "https://files.pythonhosted.org/packages/3e/33/21a875a61057165e92227466e54ee076b73af1e21fe1b31f1e292251aa1e/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", size = 121434 },
- { url = "https://files.pythonhosted.org/packages/dd/51/68b61b90b24ca35495956b718f35a9756ef7d3dd4b3c1508056fa98d1a1b/charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", size = 118979 },
- { url = "https://files.pythonhosted.org/packages/e4/a6/7ee57823d46331ddc37dd00749c95b0edec2c79b15fc0d6e6efb532e89ac/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", size = 136582 },
- { url = "https://files.pythonhosted.org/packages/74/f1/0d9fe69ac441467b737ba7f48c68241487df2f4522dd7246d9426e7c690e/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", size = 146645 },
- { url = "https://files.pythonhosted.org/packages/05/31/e1f51c76db7be1d4aef220d29fbfa5dbb4a99165d9833dcbf166753b6dc0/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", size = 139398 },
- { url = "https://files.pythonhosted.org/packages/40/26/f35951c45070edc957ba40a5b1db3cf60a9dbb1b350c2d5bef03e01e61de/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", size = 140273 },
- { url = "https://files.pythonhosted.org/packages/07/07/7e554f2bbce3295e191f7e653ff15d55309a9ca40d0362fcdab36f01063c/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", size = 142577 },
- { url = "https://files.pythonhosted.org/packages/d8/b5/eb705c313100defa57da79277d9207dc8d8e45931035862fa64b625bfead/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", size = 137747 },
- { url = "https://files.pythonhosted.org/packages/19/28/573147271fd041d351b438a5665be8223f1dd92f273713cb882ddafe214c/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", size = 143375 },
- { url = "https://files.pythonhosted.org/packages/cf/7c/f3b682fa053cc21373c9a839e6beba7705857075686a05c72e0f8c4980ca/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", size = 148474 },
- { url = "https://files.pythonhosted.org/packages/1e/49/7ab74d4ac537ece3bc3334ee08645e231f39f7d6df6347b29a74b0537103/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", size = 140232 },
- { url = "https://files.pythonhosted.org/packages/2d/dc/9dacba68c9ac0ae781d40e1a0c0058e26302ea0660e574ddf6797a0347f7/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", size = 140859 },
- { url = "https://files.pythonhosted.org/packages/6c/c2/4a583f800c0708dd22096298e49f887b49d9746d0e78bfc1d7e29816614c/charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", size = 92509 },
- { url = "https://files.pythonhosted.org/packages/57/ec/80c8d48ac8b1741d5b963797b7c0c869335619e13d4744ca2f67fc11c6fc/charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", size = 99870 },
- { url = "https://files.pythonhosted.org/packages/d1/b2/fcedc8255ec42afee97f9e6f0145c734bbe104aac28300214593eb326f1d/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", size = 192892 },
- { url = "https://files.pythonhosted.org/packages/2e/7d/2259318c202f3d17f3fe6438149b3b9e706d1070fe3fcbb28049730bb25c/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", size = 122213 },
- { url = "https://files.pythonhosted.org/packages/3a/52/9f9d17c3b54dc238de384c4cb5a2ef0e27985b42a0e5cc8e8a31d918d48d/charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", size = 119404 },
- { url = "https://files.pythonhosted.org/packages/99/b0/9c365f6d79a9f0f3c379ddb40a256a67aa69c59609608fe7feb6235896e1/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", size = 137275 },
- { url = "https://files.pythonhosted.org/packages/91/33/749df346e93d7a30cdcb90cbfdd41a06026317bfbfb62cd68307c1a3c543/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", size = 147518 },
- { url = "https://files.pythonhosted.org/packages/72/1a/641d5c9f59e6af4c7b53da463d07600a695b9824e20849cb6eea8a627761/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", size = 140182 },
- { url = "https://files.pythonhosted.org/packages/ee/fb/14d30eb4956408ee3ae09ad34299131fb383c47df355ddb428a7331cfa1e/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", size = 141869 },
- { url = "https://files.pythonhosted.org/packages/df/3e/a06b18788ca2eb6695c9b22325b6fde7dde0f1d1838b1792a0076f58fe9d/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", size = 144042 },
- { url = "https://files.pythonhosted.org/packages/45/59/3d27019d3b447a88fe7e7d004a1e04be220227760264cc41b405e863891b/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", size = 138275 },
- { url = "https://files.pythonhosted.org/packages/7b/ef/5eb105530b4da8ae37d506ccfa25057961b7b63d581def6f99165ea89c7e/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", size = 144819 },
- { url = "https://files.pythonhosted.org/packages/a2/51/e5023f937d7f307c948ed3e5c29c4b7a3e42ed2ee0b8cdf8f3a706089bf0/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", size = 149415 },
- { url = "https://files.pythonhosted.org/packages/24/9d/2e3ef673dfd5be0154b20363c5cdcc5606f35666544381bee15af3778239/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", size = 141212 },
- { url = "https://files.pythonhosted.org/packages/5b/ae/ce2c12fcac59cb3860b2e2d76dc405253a4475436b1861d95fe75bdea520/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", size = 142167 },
- { url = "https://files.pythonhosted.org/packages/ed/3a/a448bf035dce5da359daf9ae8a16b8a39623cc395a2ffb1620aa1bce62b0/charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", size = 93041 },
- { url = "https://files.pythonhosted.org/packages/b6/7c/8debebb4f90174074b827c63242c23851bdf00a532489fba57fef3416e40/charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", size = 100397 },
- { url = "https://files.pythonhosted.org/packages/f7/9d/bcf4a449a438ed6f19790eee543a86a740c77508fbc5ddab210ab3ba3a9a/charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", size = 194198 },
- { url = "https://files.pythonhosted.org/packages/66/fe/c7d3da40a66a6bf2920cce0f436fa1f62ee28aaf92f412f0bf3b84c8ad6c/charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", size = 122494 },
- { url = "https://files.pythonhosted.org/packages/2a/9d/a6d15bd1e3e2914af5955c8eb15f4071997e7078419328fee93dfd497eb7/charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", size = 120393 },
- { url = "https://files.pythonhosted.org/packages/3d/85/5b7416b349609d20611a64718bed383b9251b5a601044550f0c8983b8900/charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", size = 138331 },
- { url = "https://files.pythonhosted.org/packages/79/66/8946baa705c588521afe10b2d7967300e49380ded089a62d38537264aece/charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", size = 148097 },
- { url = "https://files.pythonhosted.org/packages/44/80/b339237b4ce635b4af1c73742459eee5f97201bd92b2371c53e11958392e/charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", size = 140711 },
- { url = "https://files.pythonhosted.org/packages/98/69/5d8751b4b670d623aa7a47bef061d69c279e9f922f6705147983aa76c3ce/charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", size = 142251 },
- { url = "https://files.pythonhosted.org/packages/1f/8d/33c860a7032da5b93382cbe2873261f81467e7b37f4ed91e25fed62fd49b/charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", size = 144636 },
- { url = "https://files.pythonhosted.org/packages/c2/65/52aaf47b3dd616c11a19b1052ce7fa6321250a7a0b975f48d8c366733b9f/charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", size = 139514 },
- { url = "https://files.pythonhosted.org/packages/51/fd/0ee5b1c2860bb3c60236d05b6e4ac240cf702b67471138571dad91bcfed8/charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", size = 145528 },
- { url = "https://files.pythonhosted.org/packages/e1/9c/60729bf15dc82e3aaf5f71e81686e42e50715a1399770bcde1a9e43d09db/charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", size = 149804 },
- { url = "https://files.pythonhosted.org/packages/53/cd/aa4b8a4d82eeceb872f83237b2d27e43e637cac9ffaef19a1321c3bafb67/charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", size = 141708 },
- { url = "https://files.pythonhosted.org/packages/54/7f/cad0b328759630814fcf9d804bfabaf47776816ad4ef2e9938b7e1123d04/charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561", size = 142708 },
- { url = "https://files.pythonhosted.org/packages/c1/9d/254a2f1bcb0ce9acad838e94ed05ba71a7cb1e27affaa4d9e1ca3958cdb6/charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", size = 92830 },
- { url = "https://files.pythonhosted.org/packages/2f/0e/d7303ccae9735ff8ff01e36705ad6233ad2002962e8668a970fc000c5e1b/charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", size = 100376 },
- { url = "https://files.pythonhosted.org/packages/28/76/e6222113b83e3622caa4bb41032d0b1bf785250607392e1b778aca0b8a7d/charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", size = 48543 },
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
]
[[package]]
@@ -93,19 +61,107 @@ name = "contiguity-base"
version = "1.0.0"
source = { editable = "." }
dependencies = [
- { name = "requests" },
+ { name = "httpx" },
+ { name = "pydantic" },
+ { name = "typing-extensions" },
]
[package.dev-dependencies]
dev = [
{ name = "pre-commit" },
+ { name = "pytest" },
+ { name = "pytest-cov" },
+ { name = "python-dotenv" },
]
[package.metadata]
-requires-dist = [{ name = "requests", specifier = ">=2.25.1,<3.0.0" }]
+requires-dist = [
+ { name = "httpx", specifier = ">=0.27.2" },
+ { name = "pydantic", specifier = ">=2.9.0,<3.0.0" },
+ { name = "typing-extensions", specifier = ">=4.12.2,<5.0.0" },
+]
[package.metadata.requires-dev]
-dev = [{ name = "pre-commit", specifier = "~=3.8.0" }]
+dev = [
+ { name = "pre-commit", specifier = "~=3.8.0" },
+ { name = "pytest", specifier = "~=8.3.3" },
+ { name = "pytest-cov", specifier = "~=5.0.0" },
+ { name = "python-dotenv", specifier = "~=1.0.1" },
+]
+
+[[package]]
+name = "coverage"
+version = "7.6.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f7/08/7e37f82e4d1aead42a7443ff06a1e406aabf7302c4f00a546e4b320b994c/coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d", size = 798791 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/61/eb7ce5ed62bacf21beca4937a90fe32545c91a3c8a42a30c6616d48fc70d/coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16", size = 206690 },
+ { url = "https://files.pythonhosted.org/packages/7d/73/041928e434442bd3afde5584bdc3f932fb4562b1597629f537387cec6f3d/coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36", size = 207127 },
+ { url = "https://files.pythonhosted.org/packages/c7/c8/6ca52b5147828e45ad0242388477fdb90df2c6cbb9a441701a12b3c71bc8/coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02", size = 235654 },
+ { url = "https://files.pythonhosted.org/packages/d5/da/9ac2b62557f4340270942011d6efeab9833648380109e897d48ab7c1035d/coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc", size = 233598 },
+ { url = "https://files.pythonhosted.org/packages/53/23/9e2c114d0178abc42b6d8d5281f651a8e6519abfa0ef460a00a91f80879d/coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23", size = 234732 },
+ { url = "https://files.pythonhosted.org/packages/0f/7e/a0230756fb133343a52716e8b855045f13342b70e48e8ad41d8a0d60ab98/coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34", size = 233816 },
+ { url = "https://files.pythonhosted.org/packages/28/7c/3753c8b40d232b1e5eeaed798c875537cf3cb183fb5041017c1fdb7ec14e/coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c", size = 232325 },
+ { url = "https://files.pythonhosted.org/packages/57/e3/818a2b2af5b7573b4b82cf3e9f137ab158c90ea750a8f053716a32f20f06/coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959", size = 233418 },
+ { url = "https://files.pythonhosted.org/packages/c8/fb/4532b0b0cefb3f06d201648715e03b0feb822907edab3935112b61b885e2/coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232", size = 209343 },
+ { url = "https://files.pythonhosted.org/packages/5a/25/af337cc7421eca1c187cc9c315f0a755d48e755d2853715bfe8c418a45fa/coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0", size = 210136 },
+ { url = "https://files.pythonhosted.org/packages/ad/5f/67af7d60d7e8ce61a4e2ddcd1bd5fb787180c8d0ae0fbd073f903b3dd95d/coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93", size = 206796 },
+ { url = "https://files.pythonhosted.org/packages/e1/0e/e52332389e057daa2e03be1fbfef25bb4d626b37d12ed42ae6281d0a274c/coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3", size = 207244 },
+ { url = "https://files.pythonhosted.org/packages/aa/cd/766b45fb6e090f20f8927d9c7cb34237d41c73a939358bc881883fd3a40d/coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff", size = 239279 },
+ { url = "https://files.pythonhosted.org/packages/70/6c/a9ccd6fe50ddaf13442a1e2dd519ca805cbe0f1fcd377fba6d8339b98ccb/coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d", size = 236859 },
+ { url = "https://files.pythonhosted.org/packages/14/6f/8351b465febb4dbc1ca9929505202db909c5a635c6fdf33e089bbc3d7d85/coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6", size = 238549 },
+ { url = "https://files.pythonhosted.org/packages/68/3c/289b81fa18ad72138e6d78c4c11a82b5378a312c0e467e2f6b495c260907/coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56", size = 237477 },
+ { url = "https://files.pythonhosted.org/packages/ed/1c/aa1efa6459d822bd72c4abc0b9418cf268de3f60eeccd65dc4988553bd8d/coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234", size = 236134 },
+ { url = "https://files.pythonhosted.org/packages/fb/c8/521c698f2d2796565fe9c789c2ee1ccdae610b3aa20b9b2ef980cc253640/coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133", size = 236910 },
+ { url = "https://files.pythonhosted.org/packages/7d/30/033e663399ff17dca90d793ee8a2ea2890e7fdf085da58d82468b4220bf7/coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c", size = 209348 },
+ { url = "https://files.pythonhosted.org/packages/20/05/0d1ccbb52727ccdadaa3ff37e4d2dc1cd4d47f0c3df9eb58d9ec8508ca88/coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6", size = 210230 },
+ { url = "https://files.pythonhosted.org/packages/7e/d4/300fc921dff243cd518c7db3a4c614b7e4b2431b0d1145c1e274fd99bd70/coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778", size = 206983 },
+ { url = "https://files.pythonhosted.org/packages/e1/ab/6bf00de5327ecb8db205f9ae596885417a31535eeda6e7b99463108782e1/coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391", size = 207221 },
+ { url = "https://files.pythonhosted.org/packages/92/8f/2ead05e735022d1a7f3a0a683ac7f737de14850395a826192f0288703472/coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8", size = 240342 },
+ { url = "https://files.pythonhosted.org/packages/0f/ef/94043e478201ffa85b8ae2d2c79b4081e5a1b73438aafafccf3e9bafb6b5/coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d", size = 237371 },
+ { url = "https://files.pythonhosted.org/packages/1f/0f/c890339dd605f3ebc269543247bdd43b703cce6825b5ed42ff5f2d6122c7/coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca", size = 239455 },
+ { url = "https://files.pythonhosted.org/packages/d1/04/7fd7b39ec7372a04efb0f70c70e35857a99b6a9188b5205efb4c77d6a57a/coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163", size = 238924 },
+ { url = "https://files.pythonhosted.org/packages/ed/bf/73ce346a9d32a09cf369f14d2a06651329c984e106f5992c89579d25b27e/coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a", size = 237252 },
+ { url = "https://files.pythonhosted.org/packages/86/74/1dc7a20969725e917b1e07fe71a955eb34bc606b938316bcc799f228374b/coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d", size = 238897 },
+ { url = "https://files.pythonhosted.org/packages/b6/e9/d9cc3deceb361c491b81005c668578b0dfa51eed02cd081620e9a62f24ec/coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5", size = 209606 },
+ { url = "https://files.pythonhosted.org/packages/47/c8/5a2e41922ea6740f77d555c4d47544acd7dc3f251fe14199c09c0f5958d3/coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb", size = 210373 },
+ { url = "https://files.pythonhosted.org/packages/8c/f9/9aa4dfb751cb01c949c990d136a0f92027fbcc5781c6e921df1cb1563f20/coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106", size = 207007 },
+ { url = "https://files.pythonhosted.org/packages/b9/67/e1413d5a8591622a46dd04ff80873b04c849268831ed5c304c16433e7e30/coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9", size = 207269 },
+ { url = "https://files.pythonhosted.org/packages/14/5b/9dec847b305e44a5634d0fb8498d135ab1d88330482b74065fcec0622224/coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c", size = 239886 },
+ { url = "https://files.pythonhosted.org/packages/7b/b7/35760a67c168e29f454928f51f970342d23cf75a2bb0323e0f07334c85f3/coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a", size = 237037 },
+ { url = "https://files.pythonhosted.org/packages/f7/95/d2fd31f1d638df806cae59d7daea5abf2b15b5234016a5ebb502c2f3f7ee/coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060", size = 239038 },
+ { url = "https://files.pythonhosted.org/packages/6e/bd/110689ff5752b67924efd5e2aedf5190cbbe245fc81b8dec1abaffba619d/coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862", size = 238690 },
+ { url = "https://files.pythonhosted.org/packages/d3/a8/08d7b38e6ff8df52331c83130d0ab92d9c9a8b5462f9e99c9f051a4ae206/coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388", size = 236765 },
+ { url = "https://files.pythonhosted.org/packages/d6/6a/9cf96839d3147d55ae713eb2d877f4d777e7dc5ba2bce227167d0118dfe8/coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155", size = 238611 },
+ { url = "https://files.pythonhosted.org/packages/74/e4/7ff20d6a0b59eeaab40b3140a71e38cf52547ba21dbcf1d79c5a32bba61b/coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a", size = 209671 },
+ { url = "https://files.pythonhosted.org/packages/35/59/1812f08a85b57c9fdb6d0b383d779e47b6f643bc278ed682859512517e83/coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129", size = 210368 },
+ { url = "https://files.pythonhosted.org/packages/9c/15/08913be1c59d7562a3e39fce20661a98c0a3f59d5754312899acc6cb8a2d/coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e", size = 207758 },
+ { url = "https://files.pythonhosted.org/packages/c4/ae/b5d58dff26cade02ada6ca612a76447acd69dccdbb3a478e9e088eb3d4b9/coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962", size = 208035 },
+ { url = "https://files.pythonhosted.org/packages/b8/d7/62095e355ec0613b08dfb19206ce3033a0eedb6f4a67af5ed267a8800642/coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb", size = 250839 },
+ { url = "https://files.pythonhosted.org/packages/7c/1e/c2967cb7991b112ba3766df0d9c21de46b476d103e32bb401b1b2adf3380/coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704", size = 246569 },
+ { url = "https://files.pythonhosted.org/packages/8b/61/a7a6a55dd266007ed3b1df7a3386a0d760d014542d72f7c2c6938483b7bd/coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b", size = 248927 },
+ { url = "https://files.pythonhosted.org/packages/c8/fa/13a6f56d72b429f56ef612eb3bc5ce1b75b7ee12864b3bd12526ab794847/coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f", size = 248401 },
+ { url = "https://files.pythonhosted.org/packages/75/06/0429c652aa0fb761fc60e8c6b291338c9173c6aa0f4e40e1902345b42830/coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223", size = 246301 },
+ { url = "https://files.pythonhosted.org/packages/52/76/1766bb8b803a88f93c3a2d07e30ffa359467810e5cbc68e375ebe6906efb/coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3", size = 247598 },
+ { url = "https://files.pythonhosted.org/packages/66/8b/f54f8db2ae17188be9566e8166ac6df105c1c611e25da755738025708d54/coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f", size = 210307 },
+ { url = "https://files.pythonhosted.org/packages/9f/b0/e0dca6da9170aefc07515cce067b97178cefafb512d00a87a1c717d2efd5/coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657", size = 211453 },
+ { url = "https://files.pythonhosted.org/packages/19/d3/d54c5aa83268779d54c86deb39c1c4566e5d45c155369ca152765f8db413/coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255", size = 206688 },
+ { url = "https://files.pythonhosted.org/packages/a5/fe/137d5dca72e4a258b1bc17bb04f2e0196898fe495843402ce826a7419fe3/coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8", size = 207120 },
+ { url = "https://files.pythonhosted.org/packages/78/5b/a0a796983f3201ff5485323b225d7c8b74ce30c11f456017e23d8e8d1945/coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2", size = 235249 },
+ { url = "https://files.pythonhosted.org/packages/4e/e1/76089d6a5ef9d68f018f65411fcdaaeb0141b504587b901d74e8587606ad/coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a", size = 233237 },
+ { url = "https://files.pythonhosted.org/packages/9a/6f/eef79b779a540326fee9520e5542a8b428cc3bfa8b7c8f1022c1ee4fc66c/coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc", size = 234311 },
+ { url = "https://files.pythonhosted.org/packages/75/e1/656d65fb126c29a494ef964005702b012f3498db1a30dd562958e85a4049/coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004", size = 233453 },
+ { url = "https://files.pythonhosted.org/packages/68/6a/45f108f137941a4a1238c85f28fd9d048cc46b5466d6b8dda3aba1bb9d4f/coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb", size = 231958 },
+ { url = "https://files.pythonhosted.org/packages/9b/e7/47b809099168b8b8c72ae311efc3e88c8d8a1162b3ba4b8da3cfcdb85743/coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36", size = 232938 },
+ { url = "https://files.pythonhosted.org/packages/52/80/052222ba7058071f905435bad0ba392cc12006380731c37afaf3fe749b88/coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c", size = 209352 },
+ { url = "https://files.pythonhosted.org/packages/b8/d8/1b92e0b3adcf384e98770a00ca095da1b5f7b483e6563ae4eb5e935d24a1/coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca", size = 210153 },
+ { url = "https://files.pythonhosted.org/packages/a5/2b/0354ed096bca64dc8e32a7cbcae28b34cb5ad0b1fe2125d6d99583313ac0/coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df", size = 198926 },
+]
+
+[package.optional-dependencies]
+toml = [
+ { name = "tomli", marker = "python_full_version <= '3.11'" },
+]
[[package]]
name = "distlib"
@@ -116,6 +172,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8e/41/9307e4f5f9976bc8b7fea0b66367734e8faf3ec84bc0d412d8cfabbb66cd/distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784", size = 468850 },
]
+[[package]]
+name = "exceptiongroup"
+version = "1.2.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 },
+]
+
[[package]]
name = "filelock"
version = "3.16.1"
@@ -125,6 +190,44 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163 },
]
+[[package]]
+name = "h11"
+version = "0.14.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 },
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/17/b0/5e8b8674f8d203335a62fdfcfa0d11ebe09e23613c3391033cbba35f7926/httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61", size = 83234 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/d4/e5d7e4f2174f8a4d63c8897d79eb8fe2503f7ecc03282fee1fa2719c2704/httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5", size = 77926 },
+]
+
+[[package]]
+name = "httpx"
+version = "0.27.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "certifi" },
+ { name = "httpcore" },
+ { name = "idna" },
+ { name = "sniffio" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/78/82/08f8c936781f67d9e6b9eeb8a0c8b4e406136ea4c3d1f89a5db71d42e0e6/httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2", size = 144189 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/56/95/9377bcb415797e44274b51d46e3249eba641711cf3348050f76ee7b15ffc/httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", size = 76395 },
+]
+
[[package]]
name = "identify"
version = "2.6.1"
@@ -143,6 +246,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
]
+[[package]]
+name = "iniconfig"
+version = "2.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
+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 = "nodeenv"
version = "1.9.1"
@@ -152,6 +264,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 },
]
+[[package]]
+name = "packaging"
+version = "24.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/51/65/50db4dda066951078f0a96cf12f4b9ada6e4b811516bf0262c0f4f7064d4/packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", size = 148788 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124", size = 53985 },
+]
+
[[package]]
name = "platformdirs"
version = "4.3.6"
@@ -161,6 +282,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 },
]
+[[package]]
+name = "pluggy"
+version = "1.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 },
+]
+
[[package]]
name = "pre-commit"
version = "3.8.0"
@@ -177,6 +307,146 @@ 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"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "iniconfig" },
+ { name = "packaging" },
+ { name = "pluggy" },
+ { name = "tomli", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8b/6c/62bbd536103af674e227c41a8f3dcd022d591f6eed5facb5a0f31ee33bbc/pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", size = 1442487 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 },
+]
+
+[[package]]
+name = "pytest-cov"
+version = "5.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "coverage", extra = ["toml"] },
+ { name = "pytest" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/74/67/00efc8d11b630c56f15f4ad9c7f9223f1e5ec275aaae3fa9118c6a223ad2/pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857", size = 63042 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/3a/af5b4fa5961d9a1e6237b530eb87dd04aea6eb83da09d2a4073d81b54ccf/pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652", size = 21990 },
+]
+
+[[package]]
+name = "python-dotenv"
+version = "1.0.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 },
+]
+
[[package]]
name = "pyyaml"
version = "6.0.2"
@@ -231,27 +501,30 @@ wheels = [
]
[[package]]
-name = "requests"
-version = "2.32.3"
+name = "sniffio"
+version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "certifi" },
- { name = "charset-normalizer" },
- { name = "idna" },
- { name = "urllib3" },
+sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
]
-sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 }
+
+[[package]]
+name = "tomli"
+version = "2.0.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c0/3f/d7af728f075fb08564c5949a9c95e44352e23dee646869fa104a3b2060a3/tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f", size = 15164 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
+ { url = "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", size = 12757 },
]
[[package]]
-name = "urllib3"
-version = "2.2.3"
+name = "typing-extensions"
+version = "4.12.2"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 }
+sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 },
+ { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
]
[[package]]