Add public repository interfaces and subclassable adapter CRUD API.
Code Quality Pipeline / code-quality (pull_request) Failing after 19s
Test Python Package / test (pull_request) Successful in 50s

Define split JSON and object repository ABCs, promote adapter methods to public CRUD, add example domain repositories, modernize interface stubs, and make the package installable for tests.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Brian Bjarke Jensen
2026-06-28 10:58:33 +02:00
co-authored by Cursor
parent f4280f15c1
commit e98fd3b90d
21 changed files with 547 additions and 190 deletions
+15 -3
View File
@@ -1,6 +1,18 @@
"""python_repositories: Unified repository interfaces and adapters."""
from . import adapters
from . import interfaces
from .adapters import MinioAdapter, RedisAdapter
from .interfaces import (
ConnectionAwareInterface,
ContextAwareInterface,
JsonRepositoryInterface,
ObjectRepositoryInterface,
)
__all__ = ["adapters", "interfaces"]
__all__ = [
"ConnectionAwareInterface",
"ContextAwareInterface",
"JsonRepositoryInterface",
"ObjectRepositoryInterface",
"RedisAdapter",
"MinioAdapter",
]
@@ -8,8 +8,9 @@ import structlog
from python_utils import check_env
from python_repositories.interfaces import (
ContextAwareInterface,
ConnectionAwareInterface,
ContextAwareInterface,
ObjectRepositoryInterface,
)
# Handle optional dependencies
@@ -18,6 +19,7 @@ if find_spec("minio") is not None:
class MinioAdapter(
ObjectRepositoryInterface,
ContextAwareInterface,
ConnectionAwareInterface,
):
@@ -113,7 +115,7 @@ class MinioAdapter(
self.logger.debug(res)
return res
def _put(
def put(
self,
object_name: str,
data: BytesIO,
@@ -147,7 +149,7 @@ class MinioAdapter(
f"Put object '{object_name}' into bucket '{self._bucket_name}'"
)
def _get(self, object_name: str) -> BytesIO | None:
def get(self, object_name: str) -> BytesIO | None:
"""Get an object from the Minio bucket."""
# Check input
if not isinstance(object_name, str) or len(object_name) == 0:
@@ -182,7 +184,7 @@ class MinioAdapter(
self.logger.error(repr(exc))
return None
def _delete(self, object_name: str) -> None:
def delete(self, object_name: str) -> None:
"""Delete an object from the Minio bucket."""
# Check input
if not isinstance(object_name, str) or len(object_name) == 0:
@@ -200,7 +202,7 @@ class MinioAdapter(
f"Deleted object '{object_name}' from bucket '{self._bucket_name}'"
)
def _list_objects(self, prefix: str = "") -> list[str]:
def list_objects(self, prefix: str = "") -> list[str]:
"""List objects in the Minio bucket with an optional prefix."""
# Check input
if not isinstance(prefix, str):
@@ -9,8 +9,9 @@ import structlog
from python_utils import check_env
from python_repositories.interfaces import (
ContextAwareInterface,
ConnectionAwareInterface,
ContextAwareInterface,
JsonRepositoryInterface,
)
# Handle optional dependencies
@@ -20,6 +21,7 @@ if find_spec("redis") is not None:
class RedisAdapter(
JsonRepositoryInterface,
ContextAwareInterface,
ConnectionAwareInterface,
):
@@ -92,7 +94,7 @@ class RedisAdapter(
self.logger.debug(res)
return res
def _set(self, key: str, data: dict) -> None:
def set(self, key: str, data: dict) -> None:
"""Set a JSON object in Redis."""
# Check input
if not isinstance(key, str) or len(key) == 0:
@@ -106,7 +108,7 @@ class RedisAdapter(
self._client.json().set(key, self.path, data)
self.logger.debug(f"Set {key} to {data}")
def _get(self, key: str) -> dict | None:
def get(self, key: str) -> dict | None:
"""Get a JSON object from Redis."""
# Check input
if not isinstance(key, str) or len(key) == 0:
@@ -122,7 +124,7 @@ class RedisAdapter(
self.logger.debug(f"Got {data} from {key}")
return data
def _delete(self, key: str) -> None:
def delete(self, key: str) -> None:
"""Delete data from Redis."""
# Check input
if not isinstance(key, str) or len(key) == 0:
@@ -134,7 +136,7 @@ class RedisAdapter(
self._client.json().delete(key)
self.logger.debug(f"Deleted {key}")
def _list_keys(self, pattern: str) -> list[str]:
def list_keys(self, pattern: str) -> list[str]:
"""List keys in Redis matching a pattern."""
# Check input
if not isinstance(pattern, str) or len(pattern) == 0:
+1
View File
@@ -0,0 +1 @@
"""Example domain repositories built on technology adapters."""
@@ -0,0 +1,23 @@
"""Example domain repository backed by MinIO objects."""
from io import BytesIO
from python_repositories.adapters.minio_adapter import MinioAdapter
class ArtifactObjectRepository(MinioAdapter):
"""Example: domain repository backed by MinIO objects."""
def _object_name(self, artifact_id: str) -> str:
return f"artifacts/{artifact_id}"
def get_artifact(self, artifact_id: str) -> BytesIO | None:
return self.get(self._object_name(artifact_id))
def store_artifact(
self,
artifact_id: str,
data: BytesIO,
content_type: str = "application/octet-stream",
) -> None:
self.put(self._object_name(artifact_id), data, content_type)
@@ -0,0 +1,19 @@
"""Example domain repository backed by Redis JSON."""
from python_repositories.adapters.redis_adapter import RedisAdapter
class UserJsonRepository(RedisAdapter):
"""Example: domain repository backed by Redis JSON."""
def _key(self, user_id: str) -> str:
return f"user:{user_id}"
def get_user(self, user_id: str) -> dict | None:
return self.get(self._key(user_id))
def save_user(self, user_id: str, user: dict) -> None:
self.set(self._key(user_id), user)
def delete_user(self, user_id: str) -> None:
self.delete(self._key(user_id))
@@ -2,8 +2,16 @@ from .connection_aware_interface import (
ConnectionAwareInterface as ConnectionAwareInterface,
)
from .context_aware_interface import ContextAwareInterface as ContextAwareInterface
from .json_repository_interface import (
JsonRepositoryInterface as JsonRepositoryInterface,
)
from .object_repository_interface import (
ObjectRepositoryInterface as ObjectRepositoryInterface,
)
__all__ = [
"ConnectionAwareInterface",
"ContextAwareInterface",
"JsonRepositoryInterface",
"ObjectRepositoryInterface",
]
@@ -9,15 +9,15 @@ class ConnectionAwareInterface(ABC):
@abstractmethod
def connect(self) -> None:
"""Connect to resource."""
raise NotImplementedError()
...
@abstractmethod
def disconnect(self) -> None:
"""Disconnect from resource."""
raise NotImplementedError()
...
@property
@abstractmethod
def is_connected(self) -> bool:
"""Check if connected to resource."""
raise NotImplementedError()
...
@@ -1,4 +1,4 @@
"""Definition of ConnectionAwareInterface abstract base class."""
"""Definition of ContextAwareInterface abstract base class."""
from __future__ import annotations
@@ -6,16 +6,16 @@ from abc import ABC, abstractmethod
class ContextAwareInterface(ABC):
"""Interface that defined context-related methods."""
"""Interface that defines context-related methods."""
@abstractmethod
def __enter__(self) -> ContextAwareInterface:
"""Enter the context."""
raise NotImplementedError()
...
@abstractmethod
def __exit__(
self, exc_type: type | None, exc_val: object | None, exc_tb: object | None
) -> None:
"""Exit the context."""
raise NotImplementedError()
...
@@ -0,0 +1,27 @@
"""Definition of JsonRepositoryInterface abstract base class."""
from abc import ABC, abstractmethod
class JsonRepositoryInterface(ABC):
"""Interface that defines JSON document CRUD methods."""
@abstractmethod
def get(self, key: str) -> dict | None:
"""Get a JSON object by key."""
...
@abstractmethod
def set(self, key: str, data: dict) -> None:
"""Set a JSON object by key."""
...
@abstractmethod
def delete(self, key: str) -> None:
"""Delete a JSON object by key."""
...
@abstractmethod
def list_keys(self, pattern: str) -> list[str]:
"""List keys matching a glob pattern."""
...
@@ -0,0 +1,33 @@
"""Definition of ObjectRepositoryInterface abstract base class."""
from abc import ABC, abstractmethod
from io import BytesIO
class ObjectRepositoryInterface(ABC):
"""Interface that defines binary object CRUD methods."""
@abstractmethod
def get(self, object_name: str) -> BytesIO | None:
"""Get an object by name."""
...
@abstractmethod
def put(
self,
object_name: str,
data: BytesIO,
content_type: str = "application/octet-stream",
) -> None:
"""Put an object by name."""
...
@abstractmethod
def delete(self, object_name: str) -> None:
"""Delete an object by name."""
...
@abstractmethod
def list_objects(self, prefix: str = "") -> list[str]:
"""List object names with an optional prefix."""
...