diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..ec4d9b3 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,14 @@ +name: pre-commit + +on: + pull_request: + push: + branches: [main] + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v3 + - uses: pre-commit/action@v3.0.0 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..16782b6 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,55 @@ +exclude: "helm" +default_stages: [commit] + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-toml + - id: check-yaml + - id: debug-statements + - id: check-builtin-literals + - id: check-case-conflict + - id: detect-private-key + + - repo: https://github.com/pre-commit/mirrors-prettier + rev: "v3.0.3" + hooks: + - id: prettier + args: ["--tab-width", "2"] + + - repo: https://github.com/asottile/pyupgrade + rev: v3.10.1 + hooks: + - id: pyupgrade + args: [--py311-plus] + exclude: hooks/ + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.5.1 + hooks: + - id: mypy + additional_dependencies: + - pydantic==1.10.12 + - requests==2.31 + - fastapi==0.99 + - dependency_injector==4.41.0 + - langchain==0.0.264 + args: [--install-types, --non-interactive] + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.0.287 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + + - repo: https://github.com/psf/black + rev: 23.7.0 + hooks: + - id: black + +ci: + autofix_commit_msg: 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks + autoupdate_commit_msg: ⬆ [pre-commit.ci] pre-commit autoupdate diff --git a/ddd_framework/domain/model.py b/ddd_framework/domain/model.py index 8619303..30ab049 100644 --- a/ddd_framework/domain/model.py +++ b/ddd_framework/domain/model.py @@ -1,9 +1,11 @@ +from __future__ import annotations + from abc import ABC from datetime import datetime -from typing import Any, Optional, Protocol, Type, TypeVar, Union +from typing import Any, Protocol, TypeVar -from attr import define, field, make_class import cattrs +from attr import define, field, make_class from ddd_framework.utils.types import get_generic_type @@ -16,6 +18,9 @@ class Event: time: datetime = field(factory=datetime.now) +# endregion + + # region "Value Object" @define(kw_only=True) class ValueObject: @@ -24,28 +29,30 @@ class ValueObject: # endregion + # region "Identity" @define(kw_only=False, frozen=True) class Id(ValueObject): """An identifier""" - id: Union[str, int] + id: str | int @classmethod - def from_raw_id(cls, raw_id: Union[str, int]) -> 'Id': + def from_raw_id(cls, raw_id: str | int) -> Id: return cls(id=raw_id) -def NewId(name: str, base: Id = Id): +def NewId(name: str, base: type[Id] = Id) -> type[Id]: """Create a new identifier's type.""" return make_class(name, attrs={}, bases=(base,)) -def structure_id(value: Any, _klass: Type) -> Id: +def structure_id(value: Any, _klass: type[Id]) -> Id: if isinstance(value, dict): return _klass(**value) return _klass(id=value) + cattrs.register_structure_hook(Id, structure_id) @@ -57,10 +64,11 @@ def structure_id(value: Any, _klass: Type) -> Id: class Entity: """Represent an entity.""" - id: Optional[Id] = field(default=None) + id: Id | None = field(default=None) - def __hash__(self): - return hash(self.id.id) + def __hash__(self) -> int: + # TODO: Fix "Item "None" of "Id | None" has no attribute "id"" + return hash(self.id.id) # type: ignore # endregion @@ -76,15 +84,15 @@ class Aggregate(ABC, Entity): # region "Repository" -AggregateType = TypeVar('AggregateType', bound=Aggregate, covariant=True) +AggregateType_co = TypeVar('AggregateType_co', bound=Aggregate, covariant=True) -class IRepository(Protocol[AggregateType]): +class IRepository(Protocol[AggregateType_co]): """A domain interface of an aggregate's repository.""" @property - def aggregate_cls(self) -> AggregateType: - return get_generic_type(self.__class__) + def aggregate_cls(self) -> AggregateType_co: + return get_generic_type(self.__class__) # type: ignore # endregion diff --git a/ddd_framework/infrastructure/persistance/mongo.py b/ddd_framework/infrastructure/persistance/mongo.py index 05087b1..b5d18ea 100644 --- a/ddd_framework/infrastructure/persistance/mongo.py +++ b/ddd_framework/infrastructure/persistance/mongo.py @@ -1,25 +1,31 @@ +from __future__ import annotations + from abc import ABC, abstractmethod from functools import cached_property -from typing import Any, Mapping, Optional, Sequence, Union +from typing import TYPE_CHECKING, Any, ClassVar import attrs import pymongo.database from pymongo import IndexModel -from pymongo.collection import Collection + +if TYPE_CHECKING: + from collections.abc import Mapping, Sequence + + from pymongo.collection import Collection @attrs.define class Index: """Represent a Mongo index - :cvar keys: Document fields to add to an index. Mirrors `keys` from :class:`pymongo.collection.IndexModel` - :cvar unique: Unique constraint, boolean + :var keys: Document fields to add to an index. Mirrors `keys` from :class:`pymongo.collection.IndexModel` + :var unique: Unique constraint, boolean """ - keys: Union[str, Sequence[tuple[str, Union[int, str, Mapping[str, Any]]]]] + keys: str | Sequence[tuple[str, int | str | Mapping[str, Any]]] unique: bool = False - def to_dict(self) -> dict: + def to_dict(self) -> dict[str, Any]: return attrs.asdict(self) @@ -34,8 +40,8 @@ class MongoRepositoryMixin(ABC): meaning that all the old indexes will be deleted and new added. """ - collection_name: str = None - indexes: Optional[list[Index]] = None + collection_name: ClassVar[str] + indexes: ClassVar[list[Index] | None] = None @abstractmethod def get_database(self) -> pymongo.database.Database: diff --git a/ddd_framework/utils/types.py b/ddd_framework/utils/types.py index ad1d070..78f5840 100644 --- a/ddd_framework/utils/types.py +++ b/ddd_framework/utils/types.py @@ -1,5 +1,7 @@ -from typing import get_args +from __future__ import annotations +from typing import Any, get_args -def get_generic_type(cls: type) -> type: + +def get_generic_type(cls: type[Any]) -> Any: return get_args(cls.__orig_bases__[0])[0] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a3ffe33 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,115 @@ +[project] +name = "docsie-ddd-framework" +version = "0.0.1" +description = "The package includes helper methods and types for working in DDD" +readme = "README.md" +requires-python = ">=3.8" +license = { file = "LICENSE" } +authors = [ + { name = "Philippe Trounev", email = "philippe.trounev@likalo.com" }, + { name = "Nikita Belyaev", email = "nick@docsie.io" }, +] +maintainers = [ + { name = "Philippe Trounev", email = "philippe.trounev@likalo.com" }, + { name = "Nikita Belyaev", email = "nick@docsie.io" }, +] +classifiers = ["Programming Language :: Python"] +dependencies = ["cattrs~=23.1.0", "attrs~=23.1.0", "pymongo~=4.3.3"] + +[project.optional-dependencies] +dev = ["pre-commit", "mypy", "ruff"] + +[project.urls] +Repository = "https://github.com/LikaloLLC/ddd-framework.git" + +[tool.setuptools.packages] +find = {} + +[tool.mypy] +strict = true +explicit_package_bases = true +packages = ["ddd_framework"] +ignore_missing_imports = true +exclude = ['ddd_framework/utils/types.py'] + +[tool.ruff] +src = ["ddd_frameowrk"] + +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "C", # flake8-comprehensions + "B", # flake8-bugbear + "ANN", # flake8-annotations + "FA", # flake8-future-annotations + "T20", # flake8-print + "Q", # flake8-quotes + "SIM", # flake8-simplify + "TCH", # flake8-type-checking + "PL", # Pylint +] +ignore = [ + "E501", # line too long, handled by black + "B008", # do not perform function calls in argument defaults + "C901", # too complex + "ANN101", # Missing type annotation for `self` in method + "ANN002", # Missing type annotation for `*args` + "ANN003", # Missing type annotation for `**kwargs` + "ANN401", # Dynamically typed expressions (typing.Any) are disallowed in ... TODO: It's better to bring it back + "ANN102", # Missing type annotation for `cls` in classmethod + "PLR0913", # Too many arguments to function call (7 > 5) + "PLW1508", #Invalid type for environment variable default; expected `str` or `None` +] + +# Allow autofix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +unfixable = [] + +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "venv", +] + +# Same as Black. +line-length = 128 + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +target-version = "py38" + +[tool.ruff.per-file-ignores] +"__init__.py" = ["F401"] + +[tool.ruff.flake8-quotes] +docstring-quotes = "double" +inline-quotes = "single" +multiline-quotes = "single" + + +[tool.black] +line-length = 128 +skip-string-normalization = true +target-version = ['py38'] diff --git a/setup.py b/setup.py deleted file mode 100644 index 8221aee..0000000 --- a/setup.py +++ /dev/null @@ -1,18 +0,0 @@ -from setuptools import setup, find_packages - -setup( - name='docsie-ddd-framework', - version='0.1.0', - packages=find_packages(), - url='https://github.com/LikaloLLC/ddd-framework', - license='Apache License', - author='Docsie', - author_email='', - description='The package includes helper methods and types for working in DDD', - include_package_data=True, - install_requires=[ - "cattrs~=23.1.0", - "attrs~=23.1.0", - "pymongo~=4.3.3", - ] -)