fix(entity): id changed from int to uuid4, renamed to identifier¶
Description¶
Abstract¶
The id field of entities is renamed to identifier and the type is changed from int, validated as positive integer, to uuid.UUID, validated to be a UUID version 4.
Motivation¶
The id term is more a “technical” term, that is a shortened version of the word identifier. And it cannot be used as an argument without shadowing the id python builtin. So it’s better to use the full word.
And being an integer means that checks have to be done for uniqueness. UUIDs exists exactly to solve this.
Rationale¶
N/A
Info¶
- Hash
79f704bde4575a9ddeb623d67d8965a62138adc9
- Date
2020-10-05 10:51:49 +0200
- Parents
style(entity): Change the world “model” by “entity” [c2dd0fc6] — 2020-10-04 21:07:00 +0200
- Children
feat(repository): Add domain repositories [27f013e2] — 2020-10-06 17:30:45 +0200
- Branches
- Tags
(No tags)
Changes¶
isshub/domain/contexts/code_repository/entities/namespace/__init__.py
isshub/domain/contexts/code_repository/entities/namespace/features/describe.feature
isshub/domain/contexts/code_repository/entities/namespace/tests/factories.py
isshub/domain/contexts/code_repository/entities/namespace/tests/test_describe.py
isshub/domain/contexts/code_repository/entities/repository/__init__.py
isshub/domain/contexts/code_repository/entities/repository/features/describe.feature
isshub/domain/contexts/code_repository/entities/repository/tests/factories.py
isshub/domain/contexts/code_repository/entities/repository/tests/test_describe.py
isshub/domain/contexts/code_repository/entities/namespace/__init__.py¶
- Type
Modified
- Stats
+3 -3
@@ -4,7 +4,7 @@ import enum
from typing import Any, Optional
from isshub.domain.utils.entity import (
- BaseEntityWithId,
+ BaseEntityWithIdentifier,
field_validator,
optional_field,
required_field,
@@ -21,12 +21,12 @@ class NamespaceKind(enum.Enum):
@validated()
-class Namespace(BaseEntityWithId):
+class Namespace(BaseEntityWithIdentifier):
"""A namespace can contain namespaces and repositories.
Attributes
----------
- id : int
+ identifier : UUID
The unique identifier of the namespace
name : str
The name of the namespace. Unique in its parent namespace.
isshub/domain/contexts/code_repository/entities/namespace/features/describe.feature¶
- Type
Modified
- Stats
+8 -8
@@ -1,20 +1,20 @@
Feature: Describing a Namespace
- Scenario: A Namespace has an id
+ Scenario: A Namespace has an identifier
Given a Namespace
- Then it must have a field named id
+ Then it must have a field named identifier
- Scenario: A Namespace id is a positive integer
+ Scenario: A Namespace identifier is a uuid
Given a Namespace
- Then its id must be a positive integer
+ Then its identifier must be a uuid
- Scenario: A Namespace id is mandatory
+ Scenario: A Namespace identifier is mandatory
Given a Namespace
- Then its id is mandatory
+ Then its identifier is mandatory
- Scenario: A Namespace id cannot be changed
+ Scenario: A Namespace identifier cannot be changed
Given a Namespace
- Then its id cannot be changed
+ Then its identifier cannot be changed
Scenario: A Namespace has a name
Given a Namespace
isshub/domain/contexts/code_repository/entities/namespace/tests/factories.py¶
- Type
Modified
- Stats
+1 -1
@@ -21,6 +21,6 @@ class NamespaceFactory(factory.Factory):
model = Namespace
- id = factory.Faker("pyint", min_value=1)
+ identifier = factory.Faker("uuid4", cast_to=None)
name = factory.Faker("pystr", min_chars=2)
kind = factory.Faker("enum", enum_cls=NamespaceKind)
isshub/domain/contexts/code_repository/entities/namespace/tests/test_describe.py¶
- Type
Modified
- Stats
+10 -9
@@ -1,4 +1,5 @@
"""Module holding BDD tests for isshub Namespace code_repository entity."""
+from uuid import uuid4
import pytest
from pytest import mark
@@ -11,16 +12,16 @@ from isshub.domain.utils.testing.validation import (
check_field_not_nullable,
check_field_nullable,
check_field_value,
- positive_integer_only,
string_only,
+ uuid4_only,
)
from .fixtures import namespace, namespace_factory
-@mark.parametrize(["value", "exception"], positive_integer_only)
-@scenario("../features/describe.feature", "A Namespace id is a positive integer")
-def test_namespace_id_is_a_positive_integer(value, exception):
+@mark.parametrize(["value", "exception"], uuid4_only)
+@scenario("../features/describe.feature", "A Namespace identifier is a uuid")
+def test_namespace_identifier_is_a_uuid(value, exception):
pass
@@ -89,15 +90,15 @@ def namespace_field_is_optional(namespace_factory, field_name):
check_field_nullable(namespace_factory, field_name)
-@scenario("../features/describe.feature", "A Namespace id cannot be changed")
-def test_namespace_id_cannot_be_changed():
+@scenario("../features/describe.feature", "A Namespace identifier cannot be changed")
+def test_namespace_identifier_cannot_be_changed():
pass
-@then("its id cannot be changed")
-def namespace_id_cannot_be_changed(namespace):
+@then("its identifier cannot be changed")
+def namespace_identifier_cannot_be_changed(namespace):
with pytest.raises(FrozenAttributeError):
- namespace.id = namespace.id + 1
+ namespace.identifier = uuid4()
@scenario("../features/describe.feature", "A Namespace cannot be contained in itself")
isshub/domain/contexts/code_repository/entities/repository/__init__.py¶
- Type
Modified
- Stats
+7 -3
@@ -1,16 +1,20 @@
"""Package defining the ``Repository`` entity."""
from isshub.domain.contexts.code_repository.entities.namespace import Namespace
-from isshub.domain.utils.entity import BaseEntityWithId, required_field, validated
+from isshub.domain.utils.entity import (
+ BaseEntityWithIdentifier,
+ required_field,
+ validated,
+)
@validated()
-class Repository(BaseEntityWithId):
+class Repository(BaseEntityWithIdentifier):
"""A repository holds code, issues, code requests...
Attributes
----------
- id : int
+ identifier : UUID
The unique identifier of the repository
name : str
The name of the repository. Unique in its namespace.
isshub/domain/contexts/code_repository/entities/repository/features/describe.feature¶
- Type
Modified
- Stats
+8 -8
@@ -1,20 +1,20 @@
Feature: Describing a Repository
- Scenario: A Repository has an id
+ Scenario: A Repository has an identifier
Given a Repository
- Then it must have a field named id
+ Then it must have a field named identifier
- Scenario: A Repository id is a positive integer
+ Scenario: A Repository identifier is a uuid
Given a Repository
- Then its id must be a positive integer
+ Then its identifier must be a uuid
- Scenario: A Repository id is mandatory
+ Scenario: A Repository identifier is mandatory
Given a Repository
- Then its id is mandatory
+ Then its identifier is mandatory
- Scenario: A Repository id cannot be changed
+ Scenario: A Repository identifier cannot be changed
Given a Repository
- Then its id cannot be changed
+ Then its identifier cannot be changed
Scenario: A Repository has a name
Given a Repository
isshub/domain/contexts/code_repository/entities/repository/tests/factories.py¶
- Type
Modified
- Stats
+1 -1
@@ -15,6 +15,6 @@ class RepositoryFactory(factory.Factory):
model = Repository
- id = factory.Faker("pyint", min_value=1)
+ identifier = factory.Faker("uuid4", cast_to=None)
name = factory.Faker("pystr", min_chars=2)
namespace = factory.SubFactory(NamespaceFactory)
isshub/domain/contexts/code_repository/entities/repository/tests/test_describe.py¶
- Type
Modified
- Stats
+10 -9
@@ -1,4 +1,5 @@
"""Module holding BDD tests for isshub Repository code_repository entity."""
+from uuid import uuid4
import pytest
from pytest import mark
@@ -9,17 +10,17 @@ from isshub.domain.utils.testing.validation import (
check_field,
check_field_not_nullable,
check_field_value,
- positive_integer_only,
string_only,
+ uuid4_only,
)
from ...namespace.tests.fixtures import namespace
from .fixtures import repository_factory
-@mark.parametrize(["value", "exception"], positive_integer_only)
-@scenario("../features/describe.feature", "A Repository id is a positive integer")
-def test_repository_id_is_a_positive_integer(value, exception):
+@mark.parametrize(["value", "exception"], uuid4_only)
+@scenario("../features/describe.feature", "A Repository identifier is a uuid")
+def test_repository_identifier_is_a_uuid(value, exception):
pass
@@ -68,12 +69,12 @@ def repository_field_is_mandatory(repository_factory, field_name):
check_field_not_nullable(repository_factory, field_name)
-@scenario("../features/describe.feature", "A Repository id cannot be changed")
-def test_repository_id_cannot_be_changed():
+@scenario("../features/describe.feature", "A Repository identifier cannot be changed")
+def test_repository_identifier_cannot_be_changed():
pass
-@then("its id cannot be changed")
-def repository_id_cannot_be_changed(repository):
+@then("its identifier cannot be changed")
+def repository_identifier_cannot_be_changed(repository):
with pytest.raises(FrozenAttributeError):
- repository.id = repository.id + 1
+ repository.identifier = uuid4()
isshub/domain/utils/entity.py¶
- Type
Modified
- Stats
+72 -10
@@ -14,6 +14,7 @@ from typing import (
Union,
cast,
)
+from uuid import UUID
import attr
@@ -382,6 +383,67 @@ def validate_positive_integer(
raise ValueError(f"{display_name} must be a positive integer")
+def validate_uuid(value: Any, none_allowed: bool, display_name: str) -> None:
+ """Validate that the given `value` is a uuid (version 4) (``None`` accepted if `none_allowed`).
+
+ Parameters
+ ----------
+ value : Any
+ The value to validate as a uuid.
+ none_allowed : bool
+ If ``True``, the value can be ``None``. If ``False``, the value must be a uuid.
+ display_name : str
+ The name of the field to display in errors.
+
+ Raises
+ ------
+ TypeError
+ If `value` is not of type ``UUID`` version 4 .
+
+ Examples
+ --------
+ >>> from uuid import UUID
+ >>> from isshub.domain.utils.entity import field_validator, required_field, BaseEntity
+ >>>
+ >>> @validated()
+ ... class MyEntity(BaseEntity):
+ ... my_field: UUID = required_field(UUID)
+ ...
+ ... @field_validator(my_field)
+ ... def validate_my_field(self, field, value):
+ ... validate_uuid(
+ ... value=value,
+ ... none_allowed=False,
+ ... display_name=f"{self.__class__.__name__}.my_field",
+ ... )
+ >>>
+ >>> instance = MyEntity(my_field='foo')
+ Traceback (most recent call last):
+ ...
+ TypeError: ("'my_field' must be <class 'uuid.UUID'> (got 'foo' that is a <class 'str'>)...
+ >>> instance = MyEntity(my_field='7298d61a-f08f-4f83-b75e-934e786eb43d')
+ Traceback (most recent call last):
+ ...
+ TypeError: ("'my_field' must be <class 'uuid.UUID'> (got '7298d61a-f08f-4f83-b75e-934e786eb43d' that is a <class 'str'>)...
+ >>> instance = MyEntity(my_field=UUID('19f49bc8-06e5-11eb-8465-bf44725d7bd3'))
+ Traceback (most recent call last):
+ ...
+ TypeError: MyEntity.my_field must be a UUID version 4
+ >>> instance = MyEntity(my_field=UUID('7298d61a-f08f-4f83-b75e-934e786eb43d'))
+ >>> instance.my_field = UUID('19f49bc8-06e5-11eb-8465-bf44725d7bd3')
+ >>> instance.validate()
+ Traceback (most recent call last):
+ ...
+ TypeError: MyEntity.my_field must be a UUID version 4
+
+ """
+ if none_allowed and value is None:
+ return
+
+ if not isinstance(value, UUID) or value.version != 4:
+ raise TypeError(f"{display_name} must be a UUID version 4")
+
+
@validated()
class BaseEntity:
"""A base entity without any field, that is able to validate itself."""
@@ -399,23 +461,23 @@ class BaseEntity:
@validated()
-class BaseEntityWithId(BaseEntity):
- """A base entity with an ``id``, that is able to validate itself.
+class BaseEntityWithIdentifier(BaseEntity):
+ """A base entity with an ``identifier``, that is able to validate itself.
Attributes
----------
- id : int
- The identifier of the instance. Validated to be a positive integer.
+ identifier : UUID
+ The identifier of the instance. Validated to be a UUID version 4.
"""
- id: int = required_field(int, frozen=True)
+ identifier: UUID = required_field(UUID, frozen=True)
- @field_validator(id)
- def validate_id_is_positive_integer( # noqa # pylint: disable=unused-argument
+ @field_validator(identifier)
+ def validate_id_is_uuid( # noqa # pylint: disable=unused-argument
self, field: "Attribute[_T]", value: _T
) -> None:
- """Validate that the ``id`` field is a positive integer.
+ """Validate that the ``identifier`` field is a uuid.
Parameters
----------
@@ -425,8 +487,8 @@ class BaseEntityWithId(BaseEntity):
The value to validate for the `field`.
"""
- validate_positive_integer(
+ validate_uuid(
value=value,
none_allowed=False,
- display_name=f"{self.__class__.__name__}.id",
+ display_name=f"{self.__class__.__name__}.identifier",
)
isshub/domain/utils/testing/validation.py¶
- Type
Modified
- Stats
+7 -0
@@ -1,6 +1,7 @@
"""Validation helpers for BDD tests for isshub entities."""
from typing import Any, Callable, List, Optional, Tuple, Type
+from uuid import UUID
import pytest
@@ -29,6 +30,12 @@ positive_integer_only: ValuesValidation = integer_only + no_zero
string_only: ValuesValidation = [("foo", None), (1, TypeError), (-0.1, TypeError)]
+uuid4_only: ValuesValidation = [
+ (UUID("19f49bc8-06e5-11eb-8465-bf44725d7bd3"), TypeError),
+ ("7298d61a-f08f-4f83-b75e-934e786eb43d", TypeError),
+ (UUID("7298d61a-f08f-4f83-b75e-934e786eb43d"), None),
+]
+
def check_field(obj: BaseEntity, field_name: str) -> None:
"""Assert that the given `obj` has an attribute named `field_name`.
pylintrc¶
- Type
Modified
- Stats
+2 -1
@@ -142,7 +142,8 @@ disable=print-statement,
too-few-public-methods,
bad-continuation,
duplicate-code,
- wrong-import-position
+ wrong-import-position,
+ line-too-long,
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option