repository.py

Last source

View documentation

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
"""Package defining bases for domain repositories."""

import abc
from inspect import isabstract
from typing import Any, Generic, Optional, Set, Type, TypeVar
from uuid import UUID

from isshub.domain.utils.entity import BaseEntityWithIdentifier


Entity = TypeVar("Entity", bound=BaseEntityWithIdentifier)


class RepositoryException(Exception):
    """Exception raised in a repository context.

    Attributes
    ----------
    repository: Optional[Type[AbstractRepository]]
        An optional repository attached to the exception or the exception class.

    """

    repository: Optional[Type["AbstractRepository"]] = None

    def __init__(
        self,
        message: str,
        repository: Optional[Type["AbstractRepository"]] = None,
    ) -> None:
        """Create the exception with a repository and formatted message.

        Parameters
        ----------
        message : str
            The message of the exception. Will be prefixed by the name of the repository and its
            entity class.
        repository : Optional[Type[AbstractRepository]]
            The repository (class) having raised the exception. To get the related entity class, use
            ``the_exception.repository.entity_class``.
        """
        if repository:
            self.repository = repository
        if self.repository is not None:
            entity_name = ""
            if self.repository.entity_class is not None:
                entity_name = f"[{self.repository.entity_class.__name__}]"
            message = f"{self.repository.__name__}{entity_name}: {message}"
        super().__init__(message)


class UniquenessError(RepositoryException):
    """Exception raised when an entity is added/updated that already exists."""


class NotFoundError(RepositoryException):
    """Exception raised when an entity couldn't be found in its repository."""


class AbstractRepository(abc.ABC, Generic[Entity]):
    """Base of all repositories.

    Attributes
    ----------
    entity_class : Optional[Type[Entity]]
        The entity class the repository is designed for. Passed as a named argument while defining
        the class.
    NotFoundError : Type["NotFoundError"]
        Local version of the :obj:`NotFoundError` exception, bound to the current repository.
    UniquenessError : Type["UniquenessError"]
        Local version of the :obj:`UniquenessError` exception, bound to the current repository.

    """

    entity_class: Optional[Type[Entity]] = None
    NotFoundError: Type[NotFoundError]
    UniquenessError: Type[UniquenessError]

    def __init_subclass__(
        cls,
        abstract: bool = False,
        entity_class: Optional[Type[Entity]] = None,
        **kwargs: Any,
    ) -> None:
        """Initialize the subclass for the given `entity_class`, if the subclass is not abstract.

        Parameters
        ----------
        abstract : bool
            If ``False``, the default, the `entity_class` is mandatory, else it's ignored.
        entity_class : Optional[Type[Entity]]
            The entity class the repository is designed for.
        kwargs : Any
            Other arguments passed to ``super().__init_subclass__``

        Raises
        ------
        TypeError
            If the class is a concrete subclass and `entity_class` is not given or not present on
            one of its parent classes.

        """
        super().__init_subclass__(**kwargs)  # type: ignore
        if entity_class is None:
            for klass in cls.mro():
                if issubclass(klass, AbstractRepository) and klass.entity_class:
                    entity_class = klass.entity_class
                    break
        if entity_class is None:
            if not (isabstract(cls) or abstract):
                raise TypeError(
                    f"`entity_class` is required for non abstract repository {cls}"
                )
        else:
            cls.entity_class = entity_class
        cls.NotFoundError = type("NotFoundError", (NotFoundError,), {"repository": cls})
        cls.UniquenessError = type(
            "UniquenessError", (UniquenessError,), {"repository": cls}
        )

    def exists(self, identifier: UUID) -> bool:
        """Tell if an entity with the given identifier exists in the repository.

        Parameters
        ----------
        identifier : UUID
            The UUID to check for in the repository

        Returns
        -------
        bool
            ``True`` if an entity with the given UUID exists. ``False`` otherwise.

        """

    @abc.abstractmethod
    def add(self, entity: Entity) -> Entity:
        """Add the given `entity` in the repository.

        Parameters
        ----------
        entity : Entity
            The entity to add to the repository

        Returns
        -------
        Entity
            The added entity

        """

    @abc.abstractmethod
    def get(self, identifier: UUID) -> Entity:
        """Get an entity by its `identifier`.

        Parameters
        ----------
        identifier : UUID
            The identifier of the wanted entity

        Returns
        -------
        Entity
            The wanted entity

        Raises
        ------
        self.NotFoundError
            If no entity was found with the given `identifier`

        """

    @abc.abstractmethod
    def update(self, entity: Entity) -> Entity:
        """Update the given entity in the repository.

        Parameters
        ----------
        entity : Entity
            The entity to updated in the repository. It must already exist.

        Returns
        -------
        Entity
            The updated entity

        Raises
        ------
        self.NotFoundError
            If no entity was found matching the given one

        """

    @abc.abstractmethod
    def delete(self, entity: Entity) -> None:
        """Delete the given `entity` from the repository.

        For the parameters, see :obj:`AbstractRepository.delete`.

        Raises
        ------
        self.NotFoundError
            If no entity was found matching the given one

        """


class AbstractInMemoryRepository(AbstractRepository[Entity], abstract=True):
    """Repository to handle entities in memory.

    Notes
    -----
    The class is created with ``abstract=True`` because as all methods from the :obj:`AbstractRepository`
    are defined, it is not viewed as abstract by ``inspect.isabstract``.

    """

    def __init__(self) -> None:
        """Initialize the repository with an empty collection (a set)."""
        self._collection: Set[Entity] = set()
        super().__init__()

    def exists(self, identifier: UUID) -> bool:
        """Tell if an entity with the given identifier exists in the repository.

        For the parameters, see :obj:`AbstractRepository.exists`.

        Returns
        -------
        bool
            ``True`` if an entity with the given UUID exists. ``False`` otherwise.

        """
        return any(
            entity for entity in self._collection if entity.identifier == identifier
        )

    def add(self, entity: Entity) -> Entity:
        """Add the given `entity` in the repository.

        For the parameters, see :obj:`AbstractRepository.add`.

        Notes
        -----
        The `entity` will be validated before being saved.

        Returns
        -------
        Entity
            The added entity

        Raises
        ------
        self.UniquenessError
            If an entity with the same identifier as the given one already exists.

        """
        entity.validate()

        if self.exists(entity.identifier):
            raise self.UniquenessError(
                f"One already exists with identifier={entity.identifier}"
            )

        self._collection.add(entity)
        return entity

    def get(self, identifier: UUID) -> Entity:
        """Get an entity by its `identifier`.

        For the parameters, see :obj:`AbstractRepository.get`.

        Returns
        -------
        Entity
            The wanted entity

        Raises
        ------
        self.NotFoundError
            If no entity was found with the given `identifier`

        """
        try:
            return [
                entity for entity in self._collection if entity.identifier == identifier
            ][0]
        except IndexError as exception:
            raise self.NotFoundError(
                f"Unable to find one with identifier={identifier}"
            ) from exception

    def update(self, entity: Entity) -> Entity:
        """Update the given entity in the repository.

        For the parameters, see :obj:`AbstractRepository.update`.

        Notes
        -----
        The `entity` will be validated before being saved.

        Returns
        -------
        Entity
            The updated entity

        Raises
        ------
        self.NotFoundError
            If no entity was found matching the given one

        """
        entity.validate()
        self.delete(entity)
        return self.add(entity)

    def delete(self, entity: Entity) -> None:
        """Delete the given `entity` from the repository.

        For the parameters, see :obj:`AbstractRepository.delete`.

        Raises
        ------
        self.NotFoundError
            If no entity was found matching the given one

        """
        entity = self.get(entity.identifier)
        self._collection.remove(entity)

Changes

feat(repository): Add domain repositories

Commit
Hash

27f013e2a3722926a9bbe300a77a493604f0993c

Date

2020-10-06 17:30:45 +0200

Type

Added

Stats

+329 -0

@@ -0,0 +1,329 @@
+"""Package defining bases for domain repositories."""
+
+import abc
+from inspect import isabstract
+from typing import Any, Generic, Optional, Set, Type, TypeVar
+from uuid import UUID
+
+from isshub.domain.utils.entity import BaseEntityWithIdentifier
+
+
+Entity = TypeVar("Entity", bound=BaseEntityWithIdentifier)
+
+
+class RepositoryException(Exception):
+    """Exception raised in a repository context.
+
+    Attributes
+    ----------
+    repository: Optional[Type[AbstractRepository]]
+        An optional repository attached to the exception or the exception class.
+
+    """
+
+    repository: Optional[Type["AbstractRepository"]] = None
+
+    def __init__(
+        self,
+        message: str,
+        repository: Optional[Type["AbstractRepository"]] = None,
+    ) -> None:
+        """Create the exception with a repository and formatted message.
+
+        Parameters
+        ----------
+        message : str
+            The message of the exception. Will be prefixed by the name of the repository and its
+            entity class.
+        repository : Optional[Type[AbstractRepository]]
+            The repository (class) having raised the exception. To get the related entity class, use
+            ``the_exception.repository.entity_class``.
+        """
+        if repository:
+            self.repository = repository
+        if self.repository is not None:
+            entity_name = ""
+            if self.repository.entity_class is not None:
+                entity_name = f"[{self.repository.entity_class.__name__}]"
+            message = f"{self.repository.__name__}{entity_name}: {message}"
+        super().__init__(message)
+
+
+class UniquenessError(RepositoryException):
+    """Exception raised when an entity is added/updated that already exists."""
+
+
+class NotFoundError(RepositoryException):
+    """Exception raised when an entity couldn't be found in its repository."""
+
+
+class AbstractRepository(abc.ABC, Generic[Entity]):
+    """Base of all repositories.
+
+    Attributes
+    ----------
+    entity_class : Optional[Type[Entity]]
+        The entity class the repository is designed for. Passed as a named argument while defining
+        the class.
+    NotFoundError : Type["NotFoundError"]
+        Local version of the :obj:`NotFoundError` exception, bound to the current repository.
+    UniquenessError : Type["UniquenessError"]
+        Local version of the :obj:`UniquenessError` exception, bound to the current repository.
+
+    """
+
+    entity_class: Optional[Type[Entity]] = None
+    NotFoundError: Type[NotFoundError]
+    UniquenessError: Type[UniquenessError]
+
+    def __init_subclass__(
+        cls,
+        abstract: bool = False,
+        entity_class: Optional[Type[Entity]] = None,
+        **kwargs: Any,
+    ) -> None:
+        """Initialize the subclass for the given `entity_class`, if the subclass is not abstract.
+
+        Parameters
+        ----------
+        abstract : bool
+            If ``False``, the default, the `entity_class` is mandatory, else it's ignored.
+        entity_class : Optional[Type[Entity]]
+            The entity class the repository is designed for.
+        kwargs : Any
+            Other arguments passed to ``super().__init_subclass__``
+
+        Raises
+        ------
+        TypeError
+            If the class is a concrete subclass and `entity_class` is not given or not present on
+            one of its parent classes.
+
+        """
+        super().__init_subclass__(**kwargs)  # type: ignore
+        if entity_class is None:
+            for klass in cls.mro():
+                if issubclass(klass, AbstractRepository) and klass.entity_class:
+                    entity_class = klass.entity_class
+                    break
+        if entity_class is None:
+            if not (isabstract(cls) or abstract):
+                raise TypeError(
+                    f"`entity_class` is required for non abstract repository {cls}"
+                )
+        else:
+            cls.entity_class = entity_class
+        cls.NotFoundError = type("NotFoundError", (NotFoundError,), {"repository": cls})
+        cls.UniquenessError = type(
+            "UniquenessError", (UniquenessError,), {"repository": cls}
+        )
+
+    def exists(self, identifier: UUID) -> bool:
+        """Tell if an entity with the given identifier exists in the repository.
+
+        Parameters
+        ----------
+        identifier : UUID
+            The UUID to check for in the repository
+
+        Returns
+        -------
+        bool
+            ``True`` if an entity with the given UUID exists. ``False`` otherwise.
+
+        """
+
+    @abc.abstractmethod
+    def add(self, entity: Entity) -> Entity:
+        """Add the given `entity` in the repository.
+
+        Parameters
+        ----------
+        entity : Entity
+            The entity to add to the repository
+
+        Returns
+        -------
+        Entity
+            The added entity
+
+        """
+
+    @abc.abstractmethod
+    def get(self, identifier: UUID) -> Entity:
+        """Get an entity by its `identifier`.
+
+        Parameters
+        ----------
+        identifier : UUID
+            The identifier of the wanted entity
+
+        Returns
+        -------
+        Entity
+            The wanted entity
+
+        Raises
+        ------
+        self.NotFoundError
+            If no entity was found with the given `identifier`
+
+        """
+
+    @abc.abstractmethod
+    def update(self, entity: Entity) -> Entity:
+        """Update the given entity in the repository.
+
+        Parameters
+        ----------
+        entity : Entity
+            The entity to updated in the repository. It must already exist.
+
+        Returns
+        -------
+        Entity
+            The updated entity
+
+        Raises
+        ------
+        self.NotFoundError
+            If no entity was found matching the given one
+
+        """
+
+    @abc.abstractmethod
+    def delete(self, entity: Entity) -> None:
+        """Delete the given `entity` from the repository.
+
+        For the parameters, see :obj:`AbstractRepository.delete`.
+
+        Raises
+        ------
+        self.NotFoundError
+            If no entity was found matching the given one
+
+        """
+
+
+class AbstractInMemoryRepository(AbstractRepository[Entity], abstract=True):
+    """Repository to handle entities in memory.
+
+    Notes
+    -----
+    The class is created with ``abstract=True`` because as all methods from the :obj:`AbstractRepository`
+    are defined, it is not viewed as abstract by ``inspect.isabstract``.
+
+    """
+
+    def __init__(self) -> None:
+        """Initialize the repository with an empty collection (a set)."""
+        self._collection: Set[Entity] = set()
+        super().__init__()
+
+    def exists(self, identifier: UUID) -> bool:
+        """Tell if an entity with the given identifier exists in the repository.
+
+        For the parameters, see :obj:`AbstractRepository.exists`.
+
+        Returns
+        -------
+        bool
+            ``True`` if an entity with the given UUID exists. ``False`` otherwise.
+
+        """
+        return any(
+            entity for entity in self._collection if entity.identifier == identifier
+        )
+
+    def add(self, entity: Entity) -> Entity:
+        """Add the given `entity` in the repository.
+
+        For the parameters, see :obj:`AbstractRepository.add`.
+
+        Notes
+        -----
+        The `entity` will be validated before being saved.
+
+        Returns
+        -------
+        Entity
+            The added entity
+
+        Raises
+        ------
+        self.UniquenessError
+            If an entity with the same identifier as the given one already exists.
+
+        """
+        entity.validate()
+
+        if self.exists(entity.identifier):
+            raise self.UniquenessError(
+                f"One already exists with identifier={entity.identifier}"
+            )
+
+        self._collection.add(entity)
+        return entity
+
+    def get(self, identifier: UUID) -> Entity:
+        """Get an entity by its `identifier`.
+
+        For the parameters, see :obj:`AbstractRepository.get`.
+
+        Returns
+        -------
+        Entity
+            The wanted entity
+
+        Raises
+        ------
+        self.NotFoundError
+            If no entity was found with the given `identifier`
+
+        """
+        try:
+            return [
+                entity for entity in self._collection if entity.identifier == identifier
+            ][0]
+        except IndexError as exception:
+            raise self.NotFoundError(
+                f"Unable to find one with identifier={identifier}"
+            ) from exception
+
+    def update(self, entity: Entity) -> Entity:
+        """Update the given entity in the repository.
+
+        For the parameters, see :obj:`AbstractRepository.update`.
+
+        Notes
+        -----
+        The `entity` will be validated before being saved.
+
+        Returns
+        -------
+        Entity
+            The updated entity
+
+        Raises
+        ------
+        self.NotFoundError
+            If no entity was found matching the given one
+
+        """
+        entity.validate()
+        self.delete(entity)
+        return self.add(entity)
+
+    def delete(self, entity: Entity) -> None:
+        """Delete the given `entity` from the repository.
+
+        For the parameters, see :obj:`AbstractRepository.delete`.
+
+        Raises
+        ------
+        self.NotFoundError
+            If no entity was found matching the given one
+
+        """
+        entity = self.get(entity.identifier)
+        self._collection.remove(entity)