From caecbfd77cd3b9a9dddd4379aca776da98ab8fa5 Mon Sep 17 00:00:00 2001 From: Lemonyte <49930425+lemonyte@users.noreply.github.com> Date: Wed, 23 Oct 2024 00:14:36 -0700 Subject: [PATCH 1/2] Rename Base -> Collection --- examples/{base => collections}/__init__.py | 0 examples/{base => collections}/basic_usage.py | 6 +- .../{base => collections}/pydantic_usage.py | 6 +- src/contiguity/__init__.py | 14 +- .../{base => collections}/__init__.py | 8 +- .../async_collection.py} | 8 +- .../base.py => collections/collection.py} | 8 +- .../{base => collections}/common.py | 0 .../{base => collections}/exceptions.py | 0 tests/base/test_async_base_model.py | 156 ------------------ tests/base/test_base_model.py | 156 ------------------ tests/{base => collections}/__init__.py | 0 .../test_async_collection_dict_typed.py} | 114 ++++++------- .../test_async_collection_dict_untyped.py} | 114 ++++++------- .../test_async_collection_model.py | 156 ++++++++++++++++++ .../test_async_collection_typeddict.py} | 112 ++++++------- .../test_collection_dict_typed.py} | 114 ++++++------- .../test_collection_dict_untyped.py} | 114 ++++++------- tests/collections/test_collection_model.py | 156 ++++++++++++++++++ .../test_collection_typeddict.py} | 112 ++++++------- 20 files changed, 681 insertions(+), 673 deletions(-) rename examples/{base => collections}/__init__.py (100%) rename examples/{base => collections}/basic_usage.py (92%) rename examples/{base => collections}/pydantic_usage.py (93%) rename src/contiguity/{base => collections}/__init__.py (66%) rename src/contiguity/{base/async_base.py => collections/async_collection.py} (97%) rename src/contiguity/{base/base.py => collections/collection.py} (97%) rename src/contiguity/{base => collections}/common.py (100%) rename src/contiguity/{base => collections}/exceptions.py (100%) delete mode 100644 tests/base/test_async_base_model.py delete mode 100644 tests/base/test_base_model.py rename tests/{base => collections}/__init__.py (100%) rename tests/{base/test_async_base_dict_typed.py => collections/test_async_collection_dict_typed.py} (50%) rename tests/{base/test_async_base_dict_untyped.py => collections/test_async_collection_dict_untyped.py} (51%) create mode 100644 tests/collections/test_async_collection_model.py rename tests/{base/test_async_base_typeddict.py => collections/test_async_collection_typeddict.py} (52%) rename tests/{base/test_base_dict_typed.py => collections/test_collection_dict_typed.py} (53%) rename tests/{base/test_base_dict_untyped.py => collections/test_collection_dict_untyped.py} (54%) create mode 100644 tests/collections/test_collection_model.py rename tests/{base/test_base_typeddict.py => collections/test_collection_typeddict.py} (55%) diff --git a/examples/base/__init__.py b/examples/collections/__init__.py similarity index 100% rename from examples/base/__init__.py rename to examples/collections/__init__.py diff --git a/examples/base/basic_usage.py b/examples/collections/basic_usage.py similarity index 92% rename from examples/base/basic_usage.py rename to examples/collections/basic_usage.py index c673c22..3940894 100644 --- a/examples/base/basic_usage.py +++ b/examples/collections/basic_usage.py @@ -1,8 +1,8 @@ # ruff: noqa: T201 -from contiguity import Base +from contiguity import Collection -# Create a Base instance. -db = Base("my-base") +# Create a Collection instance. +db = Collection("my-collection") # Put an item with a specific key. put_result = db.put({"key": "foo", "value": "Hello world!"}) diff --git a/examples/base/pydantic_usage.py b/examples/collections/pydantic_usage.py similarity index 93% rename from examples/base/pydantic_usage.py rename to examples/collections/pydantic_usage.py index 305c750..41de672 100644 --- a/examples/base/pydantic_usage.py +++ b/examples/collections/pydantic_usage.py @@ -1,7 +1,7 @@ # ruff: noqa: T201 from pydantic import BaseModel -from contiguity import Base +from contiguity import Collection # Create a Pydantic model for the item. @@ -12,9 +12,9 @@ class MyItem(BaseModel): interests: list[str] = [] -# Create a Base instance. +# Create a Collection instance. # Static type checking will work with the Pydantic model. -db = Base("members", item_type=MyItem) +db = Collection("members", item_type=MyItem) # Put an item with a specific key. put_result = db.put( diff --git a/src/contiguity/__init__.py b/src/contiguity/__init__.py index b6658d7..5dce53c 100644 --- a/src/contiguity/__init__.py +++ b/src/contiguity/__init__.py @@ -1,6 +1,14 @@ from ._client import ApiClient from .analytics import EmailAnalytics -from .base import AsyncBase, Base, BaseItem, InvalidKeyError, ItemConflictError, ItemNotFoundError, QueryResponse +from .collections import ( + AsyncCollection, + BaseItem, + Collection, + InvalidKeyError, + ItemConflictError, + ItemNotFoundError, + QueryResponse, +) from .otp import OTP from .quota import Quota from .send import Send @@ -47,7 +55,7 @@ def login(token: str, /, *, debug: bool = False) -> Contiguity: __all__ = ( - "AsyncBase", + "AsyncCollection", "Contiguity", "Send", "Verify", @@ -55,7 +63,7 @@ def login(token: str, /, *, debug: bool = False) -> Contiguity: "Quota", "OTP", "Template", - "Base", + "Collection", "BaseItem", "InvalidKeyError", "ItemConflictError", diff --git a/src/contiguity/base/__init__.py b/src/contiguity/collections/__init__.py similarity index 66% rename from src/contiguity/base/__init__.py rename to src/contiguity/collections/__init__.py index 8aada10..279d691 100644 --- a/src/contiguity/base/__init__.py +++ b/src/contiguity/collections/__init__.py @@ -1,11 +1,11 @@ -from .async_base import AsyncBase -from .base import Base +from .async_collection import AsyncCollection +from .collection import Collection from .common import BaseItem, QueryResponse from .exceptions import InvalidKeyError, ItemConflictError, ItemNotFoundError __all__ = ( - "AsyncBase", - "Base", + "AsyncCollection", + "Collection", "BaseItem", "InvalidKeyError", "ItemConflictError", diff --git a/src/contiguity/base/async_base.py b/src/contiguity/collections/async_collection.py similarity index 97% rename from src/contiguity/base/async_base.py rename to src/contiguity/collections/async_collection.py index 3a0c3c1..0b7cfcf 100644 --- a/src/contiguity/base/async_base.py +++ b/src/contiguity/collections/async_collection.py @@ -36,7 +36,7 @@ from typing_extensions import Self -class AsyncBase(Generic[ItemT]): +class AsyncCollection(Generic[ItemT]): EXPIRES_ATTRIBUTE = "__expires" PUT_LIMIT = 30 @@ -83,14 +83,14 @@ def __init__( # noqa: PLR0913 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}'" + msg = f"invalid Collection name '{name}'" raise ValueError(msg) self.name = name self.item_type = item_type self.data_key = data_key or project_key or get_data_key() self.project_id = project_id or get_project_id() - self.host = host or os.getenv("CONTIGUITY_BASE_HOST") or "api.base.contiguity.co" + self.host = host or os.getenv("CONTIGUITY_COLLECTIONS_HOST") or "api.base.contiguity.co" self.api_version = api_version self.json_decoder = json_decoder self.util = Updates() @@ -190,7 +190,7 @@ async def get( return self._response_as_item_type(response, sequence=False) async def delete(self: Self, key: str, /) -> None: - """Delete an item from the Base.""" + """Delete an item from the Collection.""" key = check_key(key) response = await self._client.delete(f"/items/{key}") try: diff --git a/src/contiguity/base/base.py b/src/contiguity/collections/collection.py similarity index 97% rename from src/contiguity/base/base.py rename to src/contiguity/collections/collection.py index 819899d..68137a7 100644 --- a/src/contiguity/base/base.py +++ b/src/contiguity/collections/collection.py @@ -45,7 +45,7 @@ from typing_extensions import Self -class Base(Generic[ItemT]): +class Collection(Generic[ItemT]): EXPIRES_ATTRIBUTE = "__expires" PUT_LIMIT = 30 @@ -92,14 +92,14 @@ def __init__( # noqa: PLR0913 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}'" + msg = f"invalid Collection name '{name}'" raise ValueError(msg) self.name = name self.item_type = item_type self.data_key = data_key or project_key or get_data_key() self.project_id = project_id or get_project_id() - self.host = host or os.getenv("CONTIGUITY_BASE_HOST") or "api.base.contiguity.co" + self.host = host or os.getenv("CONTIGUITY_COLLECTIONS_HOST") or "api.base.contiguity.co" self.api_version = api_version self.json_decoder = json_decoder self.util = Updates() @@ -199,7 +199,7 @@ def get( return self._response_as_item_type(response, sequence=False) def delete(self: Self, key: str, /) -> None: - """Delete an item from the Base.""" + """Delete an item from the Collection.""" key = check_key(key) response = self._client.delete(f"/items/{key}") try: diff --git a/src/contiguity/base/common.py b/src/contiguity/collections/common.py similarity index 100% rename from src/contiguity/base/common.py rename to src/contiguity/collections/common.py diff --git a/src/contiguity/base/exceptions.py b/src/contiguity/collections/exceptions.py similarity index 100% rename from src/contiguity/base/exceptions.py rename to src/contiguity/collections/exceptions.py diff --git a/tests/base/test_async_base_model.py b/tests/base/test_async_base_model.py deleted file mode 100644 index e0704de..0000000 --- a/tests/base/test_async_base_model.py +++ /dev/null @@ -1,156 +0,0 @@ -# ruff: noqa: S101, S311, PLR2004 -import random -from collections.abc import AsyncGenerator -from typing import Any - -import pytest -from dotenv import load_dotenv -from pydantic import BaseModel - -from contiguity import AsyncBase, InvalidKeyError, ItemConflictError, ItemNotFoundError, QueryResponse -from tests import random_string - -load_dotenv() - - -class TestItemModel(BaseModel): - key: str = "test_key" - field1: int = random.randint(1, 1000) - field2: str = random_string() - field3: int = 1 - field4: int = 0 - field5: list[str] = ["foo", "bar"] - field6: list[int] = [1, 2] - field7: dict[str, str] = {"foo": "bar"} - - -@pytest.fixture -async def base() -> AsyncGenerator[AsyncBase[TestItemModel], Any]: - base = AsyncBase("test_base_model", item_type=TestItemModel) - for item in (await base.query()).items: - await base.delete(item.key) - yield base - for item in (await base.query()).items: - await base.delete(item.key) - - -def test_bad_base_name() -> None: - with pytest.raises(ValueError, match="invalid Base name ''"): - AsyncBase("", item_type=TestItemModel) - - -async def test_bad_key(base: AsyncBase[TestItemModel]) -> None: - with pytest.raises(InvalidKeyError): - await base.get("") - with pytest.raises(InvalidKeyError): - await base.delete("") - with pytest.raises(InvalidKeyError): - await base.update({"foo": "bar"}, key="") - - -async def test_get(base: AsyncBase[TestItemModel]) -> None: - item = TestItemModel() - await base.insert(item) - fetched_item = await base.get("test_key") - assert fetched_item == item - - -async def test_get_nonexistent(base: AsyncBase[TestItemModel]) -> None: - with pytest.warns(DeprecationWarning): - assert await base.get("nonexistent_key") is None - - -async def test_get_default(base: AsyncBase[TestItemModel]) -> None: - for default_item in (None, "foo", 42, TestItemModel()): - fetched_item = await base.get("nonexistent_key", default=default_item) - assert fetched_item == default_item - - -async def test_delete(base: AsyncBase[TestItemModel]) -> None: - item = TestItemModel() - await base.insert(item) - await base.delete("test_key") - with pytest.warns(DeprecationWarning): - assert await base.get("test_key") is None - - -async def test_insert(base: AsyncBase[TestItemModel]) -> None: - item = TestItemModel() - inserted_item = await base.insert(item) - assert inserted_item == item - - -async def test_insert_existing(base: AsyncBase[TestItemModel]) -> None: - item = TestItemModel() - await base.insert(item) - with pytest.raises(ItemConflictError): - await base.insert(item) - - -async def test_put(base: AsyncBase[TestItemModel]) -> None: - items = [TestItemModel(key=f"test_key_{i}") for i in range(3)] - for _ in range(2): - response = await base.put(*items) - assert response == items - - -async def test_put_empty(base: AsyncBase[TestItemModel]) -> None: - items = [] - response = await base.put(*items) - assert response == items - - -async def test_put_too_many(base: AsyncBase[TestItemModel]) -> None: - items = [TestItemModel(key=f"test_key_{i}") for i in range(base.PUT_LIMIT + 1)] - with pytest.raises(ValueError, match=f"cannot put more than {base.PUT_LIMIT} items at a time"): - await base.put(*items) - - -async def test_update(base: AsyncBase[TestItemModel]) -> None: - item = TestItemModel() - await base.insert(item) - updated_item = await 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, - ) - - -async def test_update_nonexistent(base: AsyncBase[TestItemModel]) -> None: - with pytest.raises(ItemNotFoundError): - await base.update({"foo": "bar"}, key=random_string()) - - -async def test_update_empty(base: AsyncBase[TestItemModel]) -> None: - with pytest.raises(ValueError, match="no updates provided"): - await base.update({}, key="test_key") - - -async def test_query_empty(base: AsyncBase[TestItemModel]) -> None: - items = [TestItemModel(key=f"test_key_{i}", field1=i) for i in range(5)] - await base.put(*items) - response = await base.query() - assert response == QueryResponse(count=5, last_key=None, items=items) - - -async def test_query(base: AsyncBase[TestItemModel]) -> None: - items = [TestItemModel(key=f"test_key_{i}", field1=i) for i in range(5)] - await base.put(*items) - response = await base.query({"field1?gt": 1}) - assert response == QueryResponse(count=3, last_key=None, items=[item for item in items if item.field1 > 1]) diff --git a/tests/base/test_base_model.py b/tests/base/test_base_model.py deleted file mode 100644 index d299318..0000000 --- a/tests/base/test_base_model.py +++ /dev/null @@ -1,156 +0,0 @@ -# ruff: noqa: S101, S311, PLR2004 -import random -from collections.abc import Generator -from typing import Any - -import pytest -from dotenv import load_dotenv -from pydantic import BaseModel - -from contiguity import Base, InvalidKeyError, ItemConflictError, ItemNotFoundError, QueryResponse -from tests import random_string - -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/base/__init__.py b/tests/collections/__init__.py similarity index 100% rename from tests/base/__init__.py rename to tests/collections/__init__.py diff --git a/tests/base/test_async_base_dict_typed.py b/tests/collections/test_async_collection_dict_typed.py similarity index 50% rename from tests/base/test_async_base_dict_typed.py rename to tests/collections/test_async_collection_dict_typed.py index fd65184..0892b79 100644 --- a/tests/base/test_async_base_dict_typed.py +++ b/tests/collections/test_async_collection_dict_typed.py @@ -7,7 +7,7 @@ from dotenv import load_dotenv from pydantic import JsonValue -from contiguity import AsyncBase, InvalidKeyError, ItemConflictError, ItemNotFoundError, QueryResponse +from contiguity import AsyncCollection, InvalidKeyError, ItemConflictError, ItemNotFoundError, QueryResponse from tests import random_string load_dotenv() @@ -28,88 +28,88 @@ def create_test_item(**kwargs: JsonValue) -> DictItemType: @pytest.fixture -async def base() -> AsyncGenerator[AsyncBase[DictItemType], Any]: - base = AsyncBase("test_base_dict_typed", item_type=DictItemType) - for item in (await base.query()).items: - await base.delete(str(item["key"])) - yield base - for item in (await base.query()).items: - await base.delete(str(item["key"])) +async def db() -> AsyncGenerator[AsyncCollection[DictItemType], Any]: + db = AsyncCollection("test_db_dict_typed", item_type=DictItemType) + for item in (await db.query()).items: + await db.delete(str(item["key"])) + yield db + for item in (await db.query()).items: + await db.delete(str(item["key"])) -def test_bad_base_name() -> None: - with pytest.raises(ValueError, match="invalid Base name ''"): - AsyncBase("", item_type=DictItemType) +def test_bad_db_name() -> None: + with pytest.raises(ValueError, match="invalid Collection name ''"): + AsyncCollection("", item_type=DictItemType) -async def test_bad_key(base: AsyncBase[DictItemType]) -> None: +async def test_bad_key(db: AsyncCollection[DictItemType]) -> None: with pytest.raises(InvalidKeyError): - await base.get("") + await db.get("") with pytest.raises(InvalidKeyError): - await base.delete("") + await db.delete("") with pytest.raises(InvalidKeyError): - await base.update({"foo": "bar"}, key="") + await db.update({"foo": "bar"}, key="") -async def test_get(base: AsyncBase[DictItemType]) -> None: +async def test_get(db: AsyncCollection[DictItemType]) -> None: item = create_test_item() - await base.insert(item) - fetched_item = await base.get("test_key") + await db.insert(item) + fetched_item = await db.get("test_key") assert fetched_item == item -async def test_get_nonexistent(base: AsyncBase[DictItemType]) -> None: +async def test_get_nonexistent(db: AsyncCollection[DictItemType]) -> None: with pytest.warns(DeprecationWarning): - assert await base.get("nonexistent_key") is None + assert await db.get("nonexistent_key") is None -async def test_get_default(base: AsyncBase[DictItemType]) -> None: +async def test_get_default(db: AsyncCollection[DictItemType]) -> None: for default_item in (None, "foo", 42, create_test_item()): - fetched_item = await base.get("nonexistent_key", default=default_item) + fetched_item = await db.get("nonexistent_key", default=default_item) assert fetched_item == default_item -async def test_delete(base: AsyncBase[DictItemType]) -> None: +async def test_delete(db: AsyncCollection[DictItemType]) -> None: item = create_test_item() - await base.insert(item) - await base.delete("test_key") + await db.insert(item) + await db.delete("test_key") with pytest.warns(DeprecationWarning): - assert await base.get("test_key") is None + assert await db.get("test_key") is None -async def test_insert(base: AsyncBase[DictItemType]) -> None: +async def test_insert(db: AsyncCollection[DictItemType]) -> None: item = create_test_item() - inserted_item = await base.insert(item) + inserted_item = await db.insert(item) assert inserted_item == item -async def test_insert_existing(base: AsyncBase[DictItemType]) -> None: +async def test_insert_existing(db: AsyncCollection[DictItemType]) -> None: item = create_test_item() - await base.insert(item) + await db.insert(item) with pytest.raises(ItemConflictError): - await base.insert(item) + await db.insert(item) -async def test_put(base: AsyncBase[DictItemType]) -> None: +async def test_put(db: AsyncCollection[DictItemType]) -> None: items = [create_test_item(key=f"test_key_{i}", field1=i) for i in range(3)] for _ in range(2): - response = await base.put(*items) + response = await db.put(*items) assert response == items -async def test_put_empty(base: AsyncBase[DictItemType]) -> None: +async def test_put_empty(db: AsyncCollection[DictItemType]) -> None: items = [] - response = await base.put(*items) + response = await db.put(*items) assert response == items -async def test_put_too_many(base: AsyncBase[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"): - await base.put(*items) +async def test_put_too_many(db: AsyncCollection[DictItemType]) -> None: + items = [create_test_item(key=f"test_key_{i}") for i in range(db.PUT_LIMIT + 1)] + with pytest.raises(ValueError, match=f"cannot put more than {db.PUT_LIMIT} items at a time"): + await db.put(*items) -async def test_update(base: AsyncBase[DictItemType]) -> None: +async def test_update(db: AsyncCollection[DictItemType]) -> None: item = { "key": "test_key", "field1": random_string(), @@ -120,15 +120,15 @@ async def test_update(base: AsyncBase[DictItemType]) -> None: "field6": [1, 2], "field7": {"foo": "bar"}, } - await base.insert(item) - updated_item = await base.update( + await db.insert(item) + updated_item = await db.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]), + "field2": db.util.trim(), + "field3": db.util.increment(2), + "field4": db.util.increment(-2), + "field5": db.util.append("baz"), + "field6": db.util.prepend([3, 4]), }, key="test_key", ) @@ -143,27 +143,27 @@ async def test_update(base: AsyncBase[DictItemType]) -> None: } -async def test_update_nonexistent(base: AsyncBase[DictItemType]) -> None: +async def test_update_nonexistent(db: AsyncCollection[DictItemType]) -> None: with pytest.raises(ItemNotFoundError): - await base.update({"foo": "bar"}, key=random_string()) + await db.update({"foo": "bar"}, key=random_string()) -async def test_update_empty(base: AsyncBase[DictItemType]) -> None: +async def test_update_empty(db: AsyncCollection[DictItemType]) -> None: with pytest.raises(ValueError, match="no updates provided"): - await base.update({}, key="test_key") + await db.update({}, key="test_key") -async def test_query_empty(base: AsyncBase[DictItemType]) -> None: +async def test_query_empty(db: AsyncCollection[DictItemType]) -> None: items = [create_test_item(key=f"test_key_{i}", field1=i) for i in range(5)] - await base.put(*items) - response = await base.query() + await db.put(*items) + response = await db.query() assert response == QueryResponse(count=5, last_key=None, items=items) -async def test_query(base: AsyncBase[DictItemType]) -> None: +async def test_query(db: AsyncCollection[DictItemType]) -> None: items = [create_test_item(key=f"test_key_{i}", field1=i) for i in range(5)] - await base.put(*items) - response = await base.query({"field1?gt": 1}) + await db.put(*items) + response = await db.query({"field1?gt": 1}) assert response == QueryResponse( count=3, last_key=None, diff --git a/tests/base/test_async_base_dict_untyped.py b/tests/collections/test_async_collection_dict_untyped.py similarity index 51% rename from tests/base/test_async_base_dict_untyped.py rename to tests/collections/test_async_collection_dict_untyped.py index 1de7a6f..0d22c53 100644 --- a/tests/base/test_async_base_dict_untyped.py +++ b/tests/collections/test_async_collection_dict_untyped.py @@ -7,7 +7,7 @@ from dotenv import load_dotenv from pydantic import JsonValue -from contiguity import AsyncBase, InvalidKeyError, ItemConflictError, ItemNotFoundError, QueryResponse +from contiguity import AsyncCollection, InvalidKeyError, ItemConflictError, ItemNotFoundError, QueryResponse from tests import random_string load_dotenv() @@ -26,88 +26,88 @@ def create_test_item(**kwargs: JsonValue) -> dict: @pytest.fixture -async def base() -> AsyncGenerator[AsyncBase, Any]: - base = AsyncBase("test_base_dict_untyped") - for item in (await base.query()).items: - await base.delete(str(item["key"])) - yield base - for item in (await base.query()).items: - await base.delete(str(item["key"])) +async def db() -> AsyncGenerator[AsyncCollection, Any]: + db = AsyncCollection("test_db_dict_untyped") + for item in (await db.query()).items: + await db.delete(str(item["key"])) + yield db + for item in (await db.query()).items: + await db.delete(str(item["key"])) -def test_bad_base_name() -> None: - with pytest.raises(ValueError, match="invalid Base name ''"): - AsyncBase("") +def test_bad_db_name() -> None: + with pytest.raises(ValueError, match="invalid Collection name ''"): + AsyncCollection("") -async def test_bad_key(base: AsyncBase) -> None: +async def test_bad_key(db: AsyncCollection) -> None: with pytest.raises(InvalidKeyError): - await base.get("") + await db.get("") with pytest.raises(InvalidKeyError): - await base.delete("") + await db.delete("") with pytest.raises(InvalidKeyError): - await base.update({"foo": "bar"}, key="") + await db.update({"foo": "bar"}, key="") -async def test_get(base: AsyncBase) -> None: +async def test_get(db: AsyncCollection) -> None: item = create_test_item() - await base.insert(item) - fetched_item = await base.get("test_key") + await db.insert(item) + fetched_item = await db.get("test_key") assert fetched_item == item -async def test_get_nonexistent(base: AsyncBase) -> None: +async def test_get_nonexistent(db: AsyncCollection) -> None: with pytest.warns(DeprecationWarning): - assert await base.get("nonexistent_key") is None + assert await db.get("nonexistent_key") is None -async def test_get_default(base: AsyncBase) -> None: +async def test_get_default(db: AsyncCollection) -> None: for default_item in (None, "foo", 42, create_test_item()): - fetched_item = await base.get("nonexistent_key", default=default_item) + fetched_item = await db.get("nonexistent_key", default=default_item) assert fetched_item == default_item -async def test_delete(base: AsyncBase) -> None: +async def test_delete(db: AsyncCollection) -> None: item = create_test_item() - await base.insert(item) - await base.delete("test_key") + await db.insert(item) + await db.delete("test_key") with pytest.warns(DeprecationWarning): - assert await base.get("test_key") is None + assert await db.get("test_key") is None -async def test_insert(base: AsyncBase) -> None: +async def test_insert(db: AsyncCollection) -> None: item = create_test_item() - inserted_item = await base.insert(item) + inserted_item = await db.insert(item) assert inserted_item == item -async def test_insert_existing(base: AsyncBase) -> None: +async def test_insert_existing(db: AsyncCollection) -> None: item = create_test_item() - await base.insert(item) + await db.insert(item) with pytest.raises(ItemConflictError): - await base.insert(item) + await db.insert(item) -async def test_put(base: AsyncBase) -> None: +async def test_put(db: AsyncCollection) -> None: items = [create_test_item(key=f"test_key_{i}") for i in range(3)] for _ in range(2): - response = await base.put(*items) + response = await db.put(*items) assert response == items -async def test_put_empty(base: AsyncBase) -> None: +async def test_put_empty(db: AsyncCollection) -> None: items = [] - response = await base.put(*items) + response = await db.put(*items) assert response == items -async def test_put_too_many(base: AsyncBase) -> None: - items = [create_test_item(key=f"test_key_{i}") for i in range(base.PUT_LIMIT + 1)] - with pytest.raises(ValueError, match=f"cannot put more than {base.PUT_LIMIT} items at a time"): - await base.put(*items) +async def test_put_too_many(db: AsyncCollection) -> None: + items = [create_test_item(key=f"test_key_{i}") for i in range(db.PUT_LIMIT + 1)] + with pytest.raises(ValueError, match=f"cannot put more than {db.PUT_LIMIT} items at a time"): + await db.put(*items) -async def test_update(base: AsyncBase) -> None: +async def test_update(db: AsyncCollection) -> None: item = { "key": "test_key", "field1": random_string(), @@ -118,15 +118,15 @@ async def test_update(base: AsyncBase) -> None: "field6": [1, 2], "field7": {"foo": "bar"}, } - await base.insert(item) - updated_item = await base.update( + await db.insert(item) + updated_item = await db.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]), + "field2": db.util.trim(), + "field3": db.util.increment(2), + "field4": db.util.increment(-2), + "field5": db.util.append("baz"), + "field6": db.util.prepend([3, 4]), }, key="test_key", ) @@ -141,25 +141,25 @@ async def test_update(base: AsyncBase) -> None: } -async def test_update_nonexistent(base: AsyncBase) -> None: +async def test_update_nonexistent(db: AsyncCollection) -> None: with pytest.raises(ItemNotFoundError): - await base.update({"foo": "bar"}, key=random_string()) + await db.update({"foo": "bar"}, key=random_string()) -async def test_update_empty(base: AsyncBase) -> None: +async def test_update_empty(db: AsyncCollection) -> None: with pytest.raises(ValueError, match="no updates provided"): - await base.update({}, key="test_key") + await db.update({}, key="test_key") -async def test_query_empty(base: AsyncBase) -> None: +async def test_query_empty(db: AsyncCollection) -> None: items = [create_test_item(key=f"test_key_{i}", field1=i) for i in range(5)] - await base.put(*items) - response = await base.query() + await db.put(*items) + response = await db.query() assert response == QueryResponse(count=5, last_key=None, items=items) -async def test_query(base: AsyncBase) -> None: +async def test_query(db: AsyncCollection) -> None: items = [create_test_item(key=f"test_key_{i}", field1=i) for i in range(5)] - await base.put(*items) - response = await base.query({"field1?gt": 1}) + await db.put(*items) + response = await db.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/collections/test_async_collection_model.py b/tests/collections/test_async_collection_model.py new file mode 100644 index 0000000..b7d5809 --- /dev/null +++ b/tests/collections/test_async_collection_model.py @@ -0,0 +1,156 @@ +# ruff: noqa: S101, S311, PLR2004 +import random +from collections.abc import AsyncGenerator +from typing import Any + +import pytest +from dotenv import load_dotenv +from pydantic import BaseModel + +from contiguity import AsyncCollection, 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 +async def db() -> AsyncGenerator[AsyncCollection[TestItemModel], Any]: + db = AsyncCollection("test_db_model", item_type=TestItemModel) + for item in (await db.query()).items: + await db.delete(item.key) + yield db + for item in (await db.query()).items: + await db.delete(item.key) + + +def test_bad_db_name() -> None: + with pytest.raises(ValueError, match="invalid Collection name ''"): + AsyncCollection("", item_type=TestItemModel) + + +async def test_bad_key(db: AsyncCollection[TestItemModel]) -> None: + with pytest.raises(InvalidKeyError): + await db.get("") + with pytest.raises(InvalidKeyError): + await db.delete("") + with pytest.raises(InvalidKeyError): + await db.update({"foo": "bar"}, key="") + + +async def test_get(db: AsyncCollection[TestItemModel]) -> None: + item = TestItemModel() + await db.insert(item) + fetched_item = await db.get("test_key") + assert fetched_item == item + + +async def test_get_nonexistent(db: AsyncCollection[TestItemModel]) -> None: + with pytest.warns(DeprecationWarning): + assert await db.get("nonexistent_key") is None + + +async def test_get_default(db: AsyncCollection[TestItemModel]) -> None: + for default_item in (None, "foo", 42, TestItemModel()): + fetched_item = await db.get("nonexistent_key", default=default_item) + assert fetched_item == default_item + + +async def test_delete(db: AsyncCollection[TestItemModel]) -> None: + item = TestItemModel() + await db.insert(item) + await db.delete("test_key") + with pytest.warns(DeprecationWarning): + assert await db.get("test_key") is None + + +async def test_insert(db: AsyncCollection[TestItemModel]) -> None: + item = TestItemModel() + inserted_item = await db.insert(item) + assert inserted_item == item + + +async def test_insert_existing(db: AsyncCollection[TestItemModel]) -> None: + item = TestItemModel() + await db.insert(item) + with pytest.raises(ItemConflictError): + await db.insert(item) + + +async def test_put(db: AsyncCollection[TestItemModel]) -> None: + items = [TestItemModel(key=f"test_key_{i}") for i in range(3)] + for _ in range(2): + response = await db.put(*items) + assert response == items + + +async def test_put_empty(db: AsyncCollection[TestItemModel]) -> None: + items = [] + response = await db.put(*items) + assert response == items + + +async def test_put_too_many(db: AsyncCollection[TestItemModel]) -> None: + items = [TestItemModel(key=f"test_key_{i}") for i in range(db.PUT_LIMIT + 1)] + with pytest.raises(ValueError, match=f"cannot put more than {db.PUT_LIMIT} items at a time"): + await db.put(*items) + + +async def test_update(db: AsyncCollection[TestItemModel]) -> None: + item = TestItemModel() + await db.insert(item) + updated_item = await db.update( + { + "field1": db.util.trim(), + "field2": "updated_value", + "field3": db.util.increment(2), + "field4": db.util.increment(-2), + "field5": db.util.append("baz"), + "field6": db.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, + ) + + +async def test_update_nonexistent(db: AsyncCollection[TestItemModel]) -> None: + with pytest.raises(ItemNotFoundError): + await db.update({"foo": "bar"}, key=random_string()) + + +async def test_update_empty(db: AsyncCollection[TestItemModel]) -> None: + with pytest.raises(ValueError, match="no updates provided"): + await db.update({}, key="test_key") + + +async def test_query_empty(db: AsyncCollection[TestItemModel]) -> None: + items = [TestItemModel(key=f"test_key_{i}", field1=i) for i in range(5)] + await db.put(*items) + response = await db.query() + assert response == QueryResponse(count=5, last_key=None, items=items) + + +async def test_query(db: AsyncCollection[TestItemModel]) -> None: + items = [TestItemModel(key=f"test_key_{i}", field1=i) for i in range(5)] + await db.put(*items) + response = await db.query({"field1?gt": 1}) + assert response == QueryResponse(count=3, last_key=None, items=[item for item in items if item.field1 > 1]) diff --git a/tests/base/test_async_base_typeddict.py b/tests/collections/test_async_collection_typeddict.py similarity index 52% rename from tests/base/test_async_base_typeddict.py rename to tests/collections/test_async_collection_typeddict.py index 627ccf1..18e171f 100644 --- a/tests/base/test_async_base_typeddict.py +++ b/tests/collections/test_async_collection_typeddict.py @@ -8,7 +8,7 @@ from dotenv import load_dotenv from typing_extensions import TypedDict -from contiguity import AsyncBase, InvalidKeyError, ItemConflictError, ItemNotFoundError, QueryResponse +from contiguity import AsyncCollection, InvalidKeyError, ItemConflictError, ItemNotFoundError, QueryResponse from tests import random_string if TYPE_CHECKING: @@ -51,99 +51,99 @@ def create_test_item( # noqa: PLR0913 @pytest.fixture -async def base() -> AsyncGenerator[AsyncBase[TestItemDict], Any]: - base = AsyncBase("test_base_typeddict", item_type=TestItemDict) - for item in (await base.query()).items: - await base.delete(item["key"]) - yield base - for item in (await base.query()).items: - await base.delete(item["key"]) +async def db() -> AsyncGenerator[AsyncCollection[TestItemDict], Any]: + db = AsyncCollection("test_db_typeddict", item_type=TestItemDict) + for item in (await db.query()).items: + await db.delete(item["key"]) + yield db + for item in (await db.query()).items: + await db.delete(item["key"]) -def test_bad_base_name() -> None: - with pytest.raises(ValueError, match="invalid Base name ''"): - AsyncBase("", item_type=TestItemDict) +def test_bad_db_name() -> None: + with pytest.raises(ValueError, match="invalid Collection name ''"): + AsyncCollection("", item_type=TestItemDict) -async def test_bad_key(base: AsyncBase[TestItemDict]) -> None: +async def test_bad_key(db: AsyncCollection[TestItemDict]) -> None: with pytest.raises(InvalidKeyError): - await base.get("") + await db.get("") with pytest.raises(InvalidKeyError): - await base.delete("") + await db.delete("") with pytest.raises(InvalidKeyError): - await base.update({"foo": "bar"}, key="") + await db.update({"foo": "bar"}, key="") -async def test_get(base: AsyncBase[TestItemDict]) -> None: +async def test_get(db: AsyncCollection[TestItemDict]) -> None: item = create_test_item() - await base.insert(item) - fetched_item = await base.get("test_key") + await db.insert(item) + fetched_item = await db.get("test_key") assert fetched_item == item -async def test_get_nonexistent(base: AsyncBase[TestItemDict]) -> None: +async def test_get_nonexistent(db: AsyncCollection[TestItemDict]) -> None: with pytest.warns(DeprecationWarning): - assert await base.get("nonexistent_key") is None + assert await db.get("nonexistent_key") is None -async def test_get_default(base: AsyncBase[TestItemDict]) -> None: +async def test_get_default(db: AsyncCollection[TestItemDict]) -> None: for default_item in (None, "foo", 42, create_test_item()): - fetched_item = await base.get("nonexistent_key", default=default_item) + fetched_item = await db.get("nonexistent_key", default=default_item) assert fetched_item == default_item -async def test_delete(base: AsyncBase[TestItemDict]) -> None: +async def test_delete(db: AsyncCollection[TestItemDict]) -> None: item = create_test_item() - await base.insert(item) - await base.delete("test_key") + await db.insert(item) + await db.delete("test_key") with pytest.warns(DeprecationWarning): - assert await base.get("test_key") is None + assert await db.get("test_key") is None -async def test_insert(base: AsyncBase[TestItemDict]) -> None: +async def test_insert(db: AsyncCollection[TestItemDict]) -> None: item = create_test_item() - inserted_item = await base.insert(item) + inserted_item = await db.insert(item) assert inserted_item == item -async def test_insert_existing(base: AsyncBase[TestItemDict]) -> None: +async def test_insert_existing(db: AsyncCollection[TestItemDict]) -> None: item = create_test_item() - await base.insert(item) + await db.insert(item) with pytest.raises(ItemConflictError): - await base.insert(item) + await db.insert(item) -async def test_put(base: AsyncBase[TestItemDict]) -> None: +async def test_put(db: AsyncCollection[TestItemDict]) -> None: items = [create_test_item(f"test_key_{i}") for i in range(3)] for _ in range(2): - response = await base.put(*items) + response = await db.put(*items) assert response == items -async def test_put_empty(base: AsyncBase[TestItemDict]) -> None: +async def test_put_empty(db: AsyncCollection[TestItemDict]) -> None: items = [] - response = await base.put(*items) + response = await db.put(*items) assert response == items -async def test_put_too_many(base: AsyncBase[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"): - await base.put(*items) +async def test_put_too_many(db: AsyncCollection[TestItemDict]) -> None: + items = [create_test_item(key=f"test_key_{i}") for i in range(db.PUT_LIMIT + 1)] + with pytest.raises(ValueError, match=f"cannot put more than {db.PUT_LIMIT} items at a time"): + await db.put(*items) -async def test_update(base: AsyncBase[TestItemDict]) -> None: +async def test_update(db: AsyncCollection[TestItemDict]) -> None: item = create_test_item() - await base.insert(item) - updated_item = await base.update( + await db.insert(item) + updated_item = await db.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]), + "field3": db.util.increment(2), + "field4": db.util.increment(-2), + "field5": db.util.append("baz"), + "field6": db.util.prepend([3, 4]), }, key="test_key", ) @@ -159,25 +159,25 @@ async def test_update(base: AsyncBase[TestItemDict]) -> None: ) -async def test_update_nonexistent(base: AsyncBase[TestItemDict]) -> None: +async def test_update_nonexistent(db: AsyncCollection[TestItemDict]) -> None: with pytest.raises(ItemNotFoundError): - await base.update({"foo": "bar"}, key=random_string()) + await db.update({"foo": "bar"}, key=random_string()) -async def test_update_empty(base: AsyncBase[TestItemDict]) -> None: +async def test_update_empty(db: AsyncCollection[TestItemDict]) -> None: with pytest.raises(ValueError, match="no updates provided"): - await base.update({}, key="test_key") + await db.update({}, key="test_key") -async def test_query_empty(base: AsyncBase[TestItemDict]) -> None: +async def test_query_empty(db: AsyncCollection[TestItemDict]) -> None: items = [create_test_item(key=f"test_key_{i}", field1=i) for i in range(5)] - await base.put(*items) - response = await base.query() + await db.put(*items) + response = await db.query() assert response == QueryResponse(count=5, last_key=None, items=items) -async def test_query(base: AsyncBase[TestItemDict]) -> None: +async def test_query(db: AsyncCollection[TestItemDict]) -> None: items = [create_test_item(key=f"test_key_{i}", field1=i) for i in range(5)] - await base.put(*items) - response = await base.query({"field1?gt": 1}) + await db.put(*items) + response = await db.query({"field1?gt": 1}) assert response == QueryResponse(count=3, last_key=None, items=[item for item in items if item["field1"] > 1]) diff --git a/tests/base/test_base_dict_typed.py b/tests/collections/test_collection_dict_typed.py similarity index 53% rename from tests/base/test_base_dict_typed.py rename to tests/collections/test_collection_dict_typed.py index 1c3553a..af43031 100644 --- a/tests/base/test_base_dict_typed.py +++ b/tests/collections/test_collection_dict_typed.py @@ -7,7 +7,7 @@ from dotenv import load_dotenv from pydantic import JsonValue -from contiguity import Base, InvalidKeyError, ItemConflictError, ItemNotFoundError, QueryResponse +from contiguity import Collection, InvalidKeyError, ItemConflictError, ItemNotFoundError, QueryResponse from tests import random_string load_dotenv() @@ -28,88 +28,88 @@ def create_test_item(**kwargs: JsonValue) -> DictItemType: @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 db() -> Generator[Collection[DictItemType], Any, None]: + db = Collection("test_db_dict_typed", item_type=DictItemType) + for item in db.query().items: + db.delete(str(item["key"])) + yield db + for item in db.query().items: + db.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_db_name() -> None: + with pytest.raises(ValueError, match="invalid Collection name ''"): + Collection("", item_type=DictItemType) -def test_bad_key(base: Base[DictItemType]) -> None: +def test_bad_key(db: Collection[DictItemType]) -> None: with pytest.raises(InvalidKeyError): - base.get("") + db.get("") with pytest.raises(InvalidKeyError): - base.delete("") + db.delete("") with pytest.raises(InvalidKeyError): - base.update({"foo": "bar"}, key="") + db.update({"foo": "bar"}, key="") -def test_get(base: Base[DictItemType]) -> None: +def test_get(db: Collection[DictItemType]) -> None: item = create_test_item() - base.insert(item) - fetched_item = base.get("test_key") + db.insert(item) + fetched_item = db.get("test_key") assert fetched_item == item -def test_get_nonexistent(base: Base[DictItemType]) -> None: +def test_get_nonexistent(db: Collection[DictItemType]) -> None: with pytest.warns(DeprecationWarning): - assert base.get("nonexistent_key") is None + assert db.get("nonexistent_key") is None -def test_get_default(base: Base[DictItemType]) -> None: +def test_get_default(db: Collection[DictItemType]) -> None: for default_item in (None, "foo", 42, create_test_item()): - fetched_item = base.get("nonexistent_key", default=default_item) + fetched_item = db.get("nonexistent_key", default=default_item) assert fetched_item == default_item -def test_delete(base: Base[DictItemType]) -> None: +def test_delete(db: Collection[DictItemType]) -> None: item = create_test_item() - base.insert(item) - base.delete("test_key") + db.insert(item) + db.delete("test_key") with pytest.warns(DeprecationWarning): - assert base.get("test_key") is None + assert db.get("test_key") is None -def test_insert(base: Base[DictItemType]) -> None: +def test_insert(db: Collection[DictItemType]) -> None: item = create_test_item() - inserted_item = base.insert(item) + inserted_item = db.insert(item) assert inserted_item == item -def test_insert_existing(base: Base[DictItemType]) -> None: +def test_insert_existing(db: Collection[DictItemType]) -> None: item = create_test_item() - base.insert(item) + db.insert(item) with pytest.raises(ItemConflictError): - base.insert(item) + db.insert(item) -def test_put(base: Base[DictItemType]) -> None: +def test_put(db: Collection[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) + response = db.put(*items) assert response == items -def test_put_empty(base: Base[DictItemType]) -> None: +def test_put_empty(db: Collection[DictItemType]) -> None: items = [] - response = base.put(*items) + response = db.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_put_too_many(db: Collection[DictItemType]) -> None: + items = [create_test_item(key=f"test_key_{i}") for i in range(db.PUT_LIMIT + 1)] + with pytest.raises(ValueError, match=f"cannot put more than {db.PUT_LIMIT} items at a time"): + db.put(*items) -def test_update(base: Base[DictItemType]) -> None: +def test_update(db: Collection[DictItemType]) -> None: item = { "key": "test_key", "field1": random_string(), @@ -120,15 +120,15 @@ def test_update(base: Base[DictItemType]) -> None: "field6": [1, 2], "field7": {"foo": "bar"}, } - base.insert(item) - updated_item = base.update( + db.insert(item) + updated_item = db.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]), + "field2": db.util.trim(), + "field3": db.util.increment(2), + "field4": db.util.increment(-2), + "field5": db.util.append("baz"), + "field6": db.util.prepend([3, 4]), }, key="test_key", ) @@ -143,27 +143,27 @@ def test_update(base: Base[DictItemType]) -> None: } -def test_update_nonexistent(base: Base[DictItemType]) -> None: +def test_update_nonexistent(db: Collection[DictItemType]) -> None: with pytest.raises(ItemNotFoundError): - base.update({"foo": "bar"}, key=random_string()) + db.update({"foo": "bar"}, key=random_string()) -def test_update_empty(base: Base[DictItemType]) -> None: +def test_update_empty(db: Collection[DictItemType]) -> None: with pytest.raises(ValueError, match="no updates provided"): - base.update({}, key="test_key") + db.update({}, key="test_key") -def test_query_empty(base: Base[DictItemType]) -> None: +def test_query_empty(db: Collection[DictItemType]) -> None: items = [create_test_item(key=f"test_key_{i}", field1=i) for i in range(5)] - base.put(*items) - response = base.query() + db.put(*items) + response = db.query() assert response == QueryResponse(count=5, last_key=None, items=items) -def test_query(base: Base[DictItemType]) -> None: +def test_query(db: Collection[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}) + db.put(*items) + response = db.query({"field1?gt": 1}) assert response == QueryResponse( count=3, last_key=None, diff --git a/tests/base/test_base_dict_untyped.py b/tests/collections/test_collection_dict_untyped.py similarity index 54% rename from tests/base/test_base_dict_untyped.py rename to tests/collections/test_collection_dict_untyped.py index 37bce0c..0278658 100644 --- a/tests/base/test_base_dict_untyped.py +++ b/tests/collections/test_collection_dict_untyped.py @@ -7,7 +7,7 @@ from dotenv import load_dotenv from pydantic import JsonValue -from contiguity import Base, InvalidKeyError, ItemConflictError, ItemNotFoundError, QueryResponse +from contiguity import Collection, InvalidKeyError, ItemConflictError, ItemNotFoundError, QueryResponse from tests import random_string load_dotenv() @@ -26,88 +26,88 @@ def create_test_item(**kwargs: JsonValue) -> dict: @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 db() -> Generator[Collection, Any, None]: + db = Collection("test_db_dict_untyped") + for item in db.query().items: + db.delete(str(item["key"])) + yield db + for item in db.query().items: + db.delete(str(item["key"])) -def test_bad_base_name() -> None: - with pytest.raises(ValueError, match="invalid Base name ''"): - Base("") +def test_bad_db_name() -> None: + with pytest.raises(ValueError, match="invalid Collection name ''"): + Collection("") -def test_bad_key(base: Base) -> None: +def test_bad_key(db: Collection) -> None: with pytest.raises(InvalidKeyError): - base.get("") + db.get("") with pytest.raises(InvalidKeyError): - base.delete("") + db.delete("") with pytest.raises(InvalidKeyError): - base.update({"foo": "bar"}, key="") + db.update({"foo": "bar"}, key="") -def test_get(base: Base) -> None: +def test_get(db: Collection) -> None: item = create_test_item() - base.insert(item) - fetched_item = base.get("test_key") + db.insert(item) + fetched_item = db.get("test_key") assert fetched_item == item -def test_get_nonexistent(base: Base) -> None: +def test_get_nonexistent(db: Collection) -> None: with pytest.warns(DeprecationWarning): - assert base.get("nonexistent_key") is None + assert db.get("nonexistent_key") is None -def test_get_default(base: Base) -> None: +def test_get_default(db: Collection) -> None: for default_item in (None, "foo", 42, create_test_item()): - fetched_item = base.get("nonexistent_key", default=default_item) + fetched_item = db.get("nonexistent_key", default=default_item) assert fetched_item == default_item -def test_delete(base: Base) -> None: +def test_delete(db: Collection) -> None: item = create_test_item() - base.insert(item) - base.delete("test_key") + db.insert(item) + db.delete("test_key") with pytest.warns(DeprecationWarning): - assert base.get("test_key") is None + assert db.get("test_key") is None -def test_insert(base: Base) -> None: +def test_insert(db: Collection) -> None: item = create_test_item() - inserted_item = base.insert(item) + inserted_item = db.insert(item) assert inserted_item == item -def test_insert_existing(base: Base) -> None: +def test_insert_existing(db: Collection) -> None: item = create_test_item() - base.insert(item) + db.insert(item) with pytest.raises(ItemConflictError): - base.insert(item) + db.insert(item) -def test_put(base: Base) -> None: +def test_put(db: Collection) -> None: items = [create_test_item(key=f"test_key_{i}") for i in range(3)] for _ in range(2): - response = base.put(*items) + response = db.put(*items) assert response == items -def test_put_empty(base: Base) -> None: +def test_put_empty(db: Collection) -> None: items = [] - response = base.put(*items) + response = db.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_put_too_many(db: Collection) -> None: + items = [create_test_item(key=f"test_key_{i}") for i in range(db.PUT_LIMIT + 1)] + with pytest.raises(ValueError, match=f"cannot put more than {db.PUT_LIMIT} items at a time"): + db.put(*items) -def test_update(base: Base) -> None: +def test_update(db: Collection) -> None: item = { "key": "test_key", "field1": random_string(), @@ -118,15 +118,15 @@ def test_update(base: Base) -> None: "field6": [1, 2], "field7": {"foo": "bar"}, } - base.insert(item) - updated_item = base.update( + db.insert(item) + updated_item = db.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]), + "field2": db.util.trim(), + "field3": db.util.increment(2), + "field4": db.util.increment(-2), + "field5": db.util.append("baz"), + "field6": db.util.prepend([3, 4]), }, key="test_key", ) @@ -141,25 +141,25 @@ def test_update(base: Base) -> None: } -def test_update_nonexistent(base: Base) -> None: +def test_update_nonexistent(db: Collection) -> None: with pytest.raises(ItemNotFoundError): - base.update({"foo": "bar"}, key=random_string()) + db.update({"foo": "bar"}, key=random_string()) -def test_update_empty(base: Base) -> None: +def test_update_empty(db: Collection) -> None: with pytest.raises(ValueError, match="no updates provided"): - base.update({}, key="test_key") + db.update({}, key="test_key") -def test_query_empty(base: Base) -> None: +def test_query_empty(db: Collection) -> None: items = [create_test_item(key=f"test_key_{i}", field1=i) for i in range(5)] - base.put(*items) - response = base.query() + db.put(*items) + response = db.query() assert response == QueryResponse(count=5, last_key=None, items=items) -def test_query(base: Base) -> None: +def test_query(db: Collection) -> 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}) + db.put(*items) + response = db.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/collections/test_collection_model.py b/tests/collections/test_collection_model.py new file mode 100644 index 0000000..2c06ea5 --- /dev/null +++ b/tests/collections/test_collection_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 import Collection, 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 db() -> Generator[Collection[TestItemModel], Any, None]: + db = Collection("test_db_model", item_type=TestItemModel) + for item in db.query().items: + db.delete(item.key) + yield db + for item in db.query().items: + db.delete(item.key) + + +def test_bad_db_name() -> None: + with pytest.raises(ValueError, match="invalid Collection name ''"): + Collection("", item_type=TestItemModel) + + +def test_bad_key(db: Collection[TestItemModel]) -> None: + with pytest.raises(InvalidKeyError): + db.get("") + with pytest.raises(InvalidKeyError): + db.delete("") + with pytest.raises(InvalidKeyError): + db.update({"foo": "bar"}, key="") + + +def test_get(db: Collection[TestItemModel]) -> None: + item = TestItemModel() + db.insert(item) + fetched_item = db.get("test_key") + assert fetched_item == item + + +def test_get_nonexistent(db: Collection[TestItemModel]) -> None: + with pytest.warns(DeprecationWarning): + assert db.get("nonexistent_key") is None + + +def test_get_default(db: Collection[TestItemModel]) -> None: + for default_item in (None, "foo", 42, TestItemModel()): + fetched_item = db.get("nonexistent_key", default=default_item) + assert fetched_item == default_item + + +def test_delete(db: Collection[TestItemModel]) -> None: + item = TestItemModel() + db.insert(item) + db.delete("test_key") + with pytest.warns(DeprecationWarning): + assert db.get("test_key") is None + + +def test_insert(db: Collection[TestItemModel]) -> None: + item = TestItemModel() + inserted_item = db.insert(item) + assert inserted_item == item + + +def test_insert_existing(db: Collection[TestItemModel]) -> None: + item = TestItemModel() + db.insert(item) + with pytest.raises(ItemConflictError): + db.insert(item) + + +def test_put(db: Collection[TestItemModel]) -> None: + items = [TestItemModel(key=f"test_key_{i}") for i in range(3)] + for _ in range(2): + response = db.put(*items) + assert response == items + + +def test_put_empty(db: Collection[TestItemModel]) -> None: + items = [] + response = db.put(*items) + assert response == items + + +def test_put_too_many(db: Collection[TestItemModel]) -> None: + items = [TestItemModel(key=f"test_key_{i}") for i in range(db.PUT_LIMIT + 1)] + with pytest.raises(ValueError, match=f"cannot put more than {db.PUT_LIMIT} items at a time"): + db.put(*items) + + +def test_update(db: Collection[TestItemModel]) -> None: + item = TestItemModel() + db.insert(item) + updated_item = db.update( + { + "field1": db.util.trim(), + "field2": "updated_value", + "field3": db.util.increment(2), + "field4": db.util.increment(-2), + "field5": db.util.append("baz"), + "field6": db.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(db: Collection[TestItemModel]) -> None: + with pytest.raises(ItemNotFoundError): + db.update({"foo": "bar"}, key=random_string()) + + +def test_update_empty(db: Collection[TestItemModel]) -> None: + with pytest.raises(ValueError, match="no updates provided"): + db.update({}, key="test_key") + + +def test_query_empty(db: Collection[TestItemModel]) -> None: + items = [TestItemModel(key=f"test_key_{i}", field1=i) for i in range(5)] + db.put(*items) + response = db.query() + assert response == QueryResponse(count=5, last_key=None, items=items) + + +def test_query(db: Collection[TestItemModel]) -> None: + items = [TestItemModel(key=f"test_key_{i}", field1=i) for i in range(5)] + db.put(*items) + response = db.query({"field1?gt": 1}) + assert response == QueryResponse(count=3, last_key=None, items=[item for item in items if item.field1 > 1]) diff --git a/tests/base/test_base_typeddict.py b/tests/collections/test_collection_typeddict.py similarity index 55% rename from tests/base/test_base_typeddict.py rename to tests/collections/test_collection_typeddict.py index 39e4003..21532f5 100644 --- a/tests/base/test_base_typeddict.py +++ b/tests/collections/test_collection_typeddict.py @@ -8,7 +8,7 @@ from dotenv import load_dotenv from typing_extensions import TypedDict -from contiguity import Base, InvalidKeyError, ItemConflictError, ItemNotFoundError, QueryResponse +from contiguity import Collection, InvalidKeyError, ItemConflictError, ItemNotFoundError, QueryResponse from tests import random_string if TYPE_CHECKING: @@ -51,99 +51,99 @@ def create_test_item( # noqa: PLR0913 @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 db() -> Generator[Collection[TestItemDict], Any, None]: + db = Collection("test_db_typeddict", item_type=TestItemDict) + for item in db.query().items: + db.delete(item["key"]) + yield db + for item in db.query().items: + db.delete(item["key"]) -def test_bad_base_name() -> None: - with pytest.raises(ValueError, match="invalid Base name ''"): - Base("", item_type=TestItemDict) +def test_bad_db_name() -> None: + with pytest.raises(ValueError, match="invalid Collection name ''"): + Collection("", item_type=TestItemDict) -def test_bad_key(base: Base[TestItemDict]) -> None: +def test_bad_key(db: Collection[TestItemDict]) -> None: with pytest.raises(InvalidKeyError): - base.get("") + db.get("") with pytest.raises(InvalidKeyError): - base.delete("") + db.delete("") with pytest.raises(InvalidKeyError): - base.update({"foo": "bar"}, key="") + db.update({"foo": "bar"}, key="") -def test_get(base: Base[TestItemDict]) -> None: +def test_get(db: Collection[TestItemDict]) -> None: item = create_test_item() - base.insert(item) - fetched_item = base.get("test_key") + db.insert(item) + fetched_item = db.get("test_key") assert fetched_item == item -def test_get_nonexistent(base: Base[TestItemDict]) -> None: +def test_get_nonexistent(db: Collection[TestItemDict]) -> None: with pytest.warns(DeprecationWarning): - assert base.get("nonexistent_key") is None + assert db.get("nonexistent_key") is None -def test_get_default(base: Base[TestItemDict]) -> None: +def test_get_default(db: Collection[TestItemDict]) -> None: for default_item in (None, "foo", 42, create_test_item()): - fetched_item = base.get("nonexistent_key", default=default_item) + fetched_item = db.get("nonexistent_key", default=default_item) assert fetched_item == default_item -def test_delete(base: Base[TestItemDict]) -> None: +def test_delete(db: Collection[TestItemDict]) -> None: item = create_test_item() - base.insert(item) - base.delete("test_key") + db.insert(item) + db.delete("test_key") with pytest.warns(DeprecationWarning): - assert base.get("test_key") is None + assert db.get("test_key") is None -def test_insert(base: Base[TestItemDict]) -> None: +def test_insert(db: Collection[TestItemDict]) -> None: item = create_test_item() - inserted_item = base.insert(item) + inserted_item = db.insert(item) assert inserted_item == item -def test_insert_existing(base: Base[TestItemDict]) -> None: +def test_insert_existing(db: Collection[TestItemDict]) -> None: item = create_test_item() - base.insert(item) + db.insert(item) with pytest.raises(ItemConflictError): - base.insert(item) + db.insert(item) -def test_put(base: Base[TestItemDict]) -> None: +def test_put(db: Collection[TestItemDict]) -> None: items = [create_test_item(f"test_key_{i}") for i in range(3)] for _ in range(2): - response = base.put(*items) + response = db.put(*items) assert response == items -def test_put_empty(base: Base[TestItemDict]) -> None: +def test_put_empty(db: Collection[TestItemDict]) -> None: items = [] - response = base.put(*items) + response = db.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_put_too_many(db: Collection[TestItemDict]) -> None: + items = [create_test_item(key=f"test_key_{i}") for i in range(db.PUT_LIMIT + 1)] + with pytest.raises(ValueError, match=f"cannot put more than {db.PUT_LIMIT} items at a time"): + db.put(*items) -def test_update(base: Base[TestItemDict]) -> None: +def test_update(db: Collection[TestItemDict]) -> None: item = create_test_item() - base.insert(item) - updated_item = base.update( + db.insert(item) + updated_item = db.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]), + "field3": db.util.increment(2), + "field4": db.util.increment(-2), + "field5": db.util.append("baz"), + "field6": db.util.prepend([3, 4]), }, key="test_key", ) @@ -159,25 +159,25 @@ def test_update(base: Base[TestItemDict]) -> None: ) -def test_update_nonexistent(base: Base[TestItemDict]) -> None: +def test_update_nonexistent(db: Collection[TestItemDict]) -> None: with pytest.raises(ItemNotFoundError): - base.update({"foo": "bar"}, key=random_string()) + db.update({"foo": "bar"}, key=random_string()) -def test_update_empty(base: Base[TestItemDict]) -> None: +def test_update_empty(db: Collection[TestItemDict]) -> None: with pytest.raises(ValueError, match="no updates provided"): - base.update({}, key="test_key") + db.update({}, key="test_key") -def test_query_empty(base: Base[TestItemDict]) -> None: +def test_query_empty(db: Collection[TestItemDict]) -> None: items = [create_test_item(key=f"test_key_{i}", field1=i) for i in range(5)] - base.put(*items) - response = base.query() + db.put(*items) + response = db.query() assert response == QueryResponse(count=5, last_key=None, items=items) -def test_query(base: Base[TestItemDict]) -> None: +def test_query(db: Collection[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}) + db.put(*items) + response = db.query({"field1?gt": 1}) assert response == QueryResponse(count=3, last_key=None, items=[item for item in items if item["field1"] > 1]) From 4bdc907bac2b737cff423f7882ee2966eac19849 Mon Sep 17 00:00:00 2001 From: Lemonyte <49930425+lemonyte@users.noreply.github.com> Date: Thu, 24 Oct 2024 15:16:09 -0700 Subject: [PATCH 2/2] Add backwards compatibility for Base and AsyncBase --- src/contiguity/__init__.py | 3 +++ src/contiguity/collections/compatibility.py | 15 +++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 src/contiguity/collections/compatibility.py diff --git a/src/contiguity/__init__.py b/src/contiguity/__init__.py index 5dce53c..eb0b5b4 100644 --- a/src/contiguity/__init__.py +++ b/src/contiguity/__init__.py @@ -9,6 +9,7 @@ ItemNotFoundError, QueryResponse, ) +from .collections.compatibility import AsyncBase, Base from .otp import OTP from .quota import Quota from .send import Send @@ -55,7 +56,9 @@ def login(token: str, /, *, debug: bool = False) -> Contiguity: __all__ = ( + "AsyncBase", "AsyncCollection", + "Base", "Contiguity", "Send", "Verify", diff --git a/src/contiguity/collections/compatibility.py b/src/contiguity/collections/compatibility.py new file mode 100644 index 0000000..73a383d --- /dev/null +++ b/src/contiguity/collections/compatibility.py @@ -0,0 +1,15 @@ +from typing_extensions import deprecated + +from .async_collection import AsyncCollection +from .collection import Collection +from .common import ItemT + + +@deprecated("This class has been renamed to `Collection` and will be removed in a future release.") +class Base(Collection[ItemT]): + pass + + +@deprecated("This class has been renamed to `AsyncCollection` and will be removed in a future release.") +class AsyncBase(AsyncCollection[ItemT]): + pass