From 8d93c964e1932222761483362e4db2e579cc3414 Mon Sep 17 00:00:00 2001 From: pi3c Date: Wed, 6 Mar 2024 02:28:59 +0300 Subject: [PATCH] uow and di basic implementation --- .pre-commit-config.yaml | 57 ++++++++++++++++ __init__.py | 0 api/di.py | 60 ++++++++++------- api/migrations/env.py | 3 +- .../ec1380cb4f18_initial.cpython-310.pyc | Bin 1110 -> 0 bytes .../ec1380cb4f18_initial.cpython-311.pyc | Bin 1868 -> 0 bytes .../versions/ec1380cb4f18_initial.py | 2 +- api/model/base.py | 12 ++++ api/model/user.py | 5 +- api/repository/user.py | 11 ++-- api/service/user.py | 10 +-- api/uow/__pycache__/__init__.cpython-311.pyc | Bin 156 -> 0 bytes api/uow/__pycache__/database.cpython-311.pyc | Bin 2614 -> 0 bytes api/uow/__pycache__/uow_base.cpython-311.pyc | Bin 1844 -> 0 bytes api/uow/database.py | 37 ----------- api/uow/repository.py | 38 +++++++++++ api/uow/uow_base.py | 62 ++++++++++++------ config/api_config.yml | 4 +- docker/api/Dockerfile | 2 + poetry.lock | 13 +++- pyproject.toml | 1 + 21 files changed, 217 insertions(+), 100 deletions(-) create mode 100644 .pre-commit-config.yaml delete mode 100644 __init__.py delete mode 100644 api/migrations/versions/__pycache__/ec1380cb4f18_initial.cpython-310.pyc delete mode 100644 api/migrations/versions/__pycache__/ec1380cb4f18_initial.cpython-311.pyc create mode 100644 api/model/base.py delete mode 100644 api/uow/__pycache__/__init__.cpython-311.pyc delete mode 100644 api/uow/__pycache__/database.cpython-311.pyc delete mode 100644 api/uow/__pycache__/uow_base.cpython-311.pyc delete mode 100644 api/uow/database.py create mode 100644 api/uow/repository.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..5451ddd --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,57 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.3.0 + hooks: + - id: trailing-whitespace # убирает лишние пробелы + - id: check-added-large-files # проверяет тяжелые файлы на изменения + - id: end-of-file-fixer # добавляет пустую строку в конце файла + - id: check-yaml # проверяет синтаксис .yaml файлов + - id: check-json # проверяет синтаксис .json файлов + - id: check-case-conflict # проверяет файлы, которые могут конфликтовать в файловых системах без учета регистра. + - id: check-merge-conflict # проверяет файлы, содержащие конфликтные строки слияния. + + # Отсортировывает импорты в проекте + - repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort + exclude: __init__.py + args: [ --profile, black, --filter-files ] + + # Обновляет синтаксис Python кода в соответствии с последними версиями + - repo: https://github.com/asottile/pyupgrade + rev: v3.3.1 + hooks: + - id: pyupgrade + args: [ --py310-plus ] + + # Форматирует код под PEP8 + - repo: https://github.com/pre-commit/mirrors-autopep8 + rev: v2.0.1 + hooks: + - id: autopep8 + args: [ "-i", "--in-place", "--max-line-length=120" ] + + # Сканер стилистических ошибок, нарушающие договоренности PEP8 + - repo: https://github.com/PyCQA/flake8 + rev: 6.0.0 + hooks: + - id: flake8 + exclude: __init__.py + args: [ "--ignore=E501,F821", "--max-line-length=120" ] + + # Форматирует код под PEP8 c помощью black + - repo: https://github.com/psf/black + rev: 23.1.0 + hooks: + - id: black + language_version: python3.10 + args: [ "--line-length=120" ] + + # Проверка статических типов с помощью mypy + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v0.991 + hooks: + - id: mypy + exclude: 'migrations' + args: [--no-strict-optional, --ignore-missing-imports] diff --git a/__init__.py b/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/api/di.py b/api/di.py index f480d03..8f868ea 100644 --- a/api/di.py +++ b/api/di.py @@ -1,44 +1,58 @@ import os +import yaml # type: ignore from dependency_injector import containers, providers +from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine -from api.repository.user import UserRepository from api.service.user import UserService -from api.uow.database import Database -from api.uow.uow_base import UowBase +from api.uow.uow_base import UnitOfWork class Container(containers.DeclarativeContainer): wiring_config = containers.WiringConfiguration(modules=["api.router.user"]) - config = providers.Configuration(yaml_files=[f"{os.getenv('CONFIG_PATH')}"]) + if not os.getenv("CONFIG_PATH"): + raise ValueError('Please set "CONFIG_PATH" variable in your environment') + + with open(os.getenv("CONFIG_PATH", "")) as f: + config_data = yaml.safe_load(f) + + config = providers.Configuration() if os.getenv("INDOCKER"): - config.db.host.update("db") + config_data["db"]["host"] = "db" + config_data["db"]["port"] = 5432 - db = providers.Singleton( - Database, - db_url="postgresql+asyncpg://{}:{}@{}:{}/{}".format( - config.db.user, - config.db.password, - config.db.host, - # config.db.port, - "5432", - config.db.database, + async_engine = providers.Factory( + create_async_engine, + "postgresql+asyncpg://{}:{}@{}:{}/{}".format( + config_data["db"]["user"], + config_data["db"]["password"], + config_data["db"]["host"], + config_data["db"]["port"], + config_data["db"]["database"], ), + echo=True, + ) + + async_session_factory = providers.Factory( + async_sessionmaker, + async_engine, + class_=AsyncSession, + expire_on_commit=False, ) uow = providers.Factory( - UowBase, - session_factory=db.provided.session, + UnitOfWork, + session_factory=async_session_factory, ) - - user_repository = providers.Factory( - UserRepository, - uow=uow, - ) - + # + # user_repository = providers.Factory( + # UserRepository, + # uow=uow, + # ) + # user_service = providers.Factory( UserService, - user_repository=user_repository, + uow=uow, ) diff --git a/api/migrations/env.py b/api/migrations/env.py index 257e7b2..9e66d19 100644 --- a/api/migrations/env.py +++ b/api/migrations/env.py @@ -4,7 +4,6 @@ from alembic import context from sqlalchemy import engine_from_config, pool import api.model.user # type: ignore -from api.uow.database import Base # this is the Alembic Config object, which provides # access to the values within the .ini file in use. @@ -19,7 +18,7 @@ if config.config_file_name is not None: # for 'autogenerate' support # from myapp import mymodel # target_metadata = mymodel.Base.metadata -target_metadata = Base.metadata +target_metadata = api.model.user.Base.metadata # other values from the config, defined by the needs of env.py, # can be acquired: diff --git a/api/migrations/versions/__pycache__/ec1380cb4f18_initial.cpython-310.pyc b/api/migrations/versions/__pycache__/ec1380cb4f18_initial.cpython-310.pyc deleted file mode 100644 index dd7d8a5d97941883fbb19b1767f76352f8ca2033..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1110 zcmZuw%}(1u5VqHj|42fL=mDxqQExdw5`qwlP*oLF2_d1Xlpb=i7Vki|ve#zU2};ks z_S$oD>>Ko1_F7eHUx8C+9aNBDS38>dX1p`=%{p&1A`92gXTRZ(V_Cn9@o>3lyuqWs zqd2#iHL{q^NNQ(fL^5aO*v58JH>-_mmF}kA$Y-@t03k%oJFu8LS^)<_=3bHw3o*aK z>IMS>;RZ%rI+Z7=JdGzjA$nHX3afv0M@`fktcltxbI+_!>&quDc*$uRMIT_w70*Tc z_-NRMq(9j2B^SGw{r#l})UX{LN}wgQk5CNTJH4IVt=?d(x7+RwhW-9jP9Cw6NDYx1BHn+*e}n{x zc=s`LpOFRLHxF2CVK00fn}sxnr@A&#AeAPZy~7>@F-fuJ6x)W3^7Nx#9aA+178g{h zn_My-auw60tNg7o~0?*GT(4^23rFn4%K5C*5(LLAT(8q${DwyIA>@ClF^;bZNo6 z8NZUW#A&JS6q~A)hx3Z>-pqC})BdEuUu_WcbiKVuQVLCKVrmEo;nu?%k68Um@gZV{IJ1ynJ>{<=07I*WGn{6L;h^8~*@Bjv4R( diff --git a/api/migrations/versions/__pycache__/ec1380cb4f18_initial.cpython-311.pyc b/api/migrations/versions/__pycache__/ec1380cb4f18_initial.cpython-311.pyc deleted file mode 100644 index d024d3cc77a06d779791e1da58258a82f41e1b03..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1868 zcmb_d&1)M+6rWv5tI=AD+~R)4uC2yS6Vb|&EZd}ZD5efAA=EAYfL5U_>z%2+VLz1F zm4i(VKIGs+4?YBHgA6XDZi+(*J@wDn1s%*lAy9JYO~t)5r@mcDnTe_>OhAc1A4c*agD|hwEOpa>h%B7OFI{9V!QhSFw zGx9v6s!MZMfX(D4N)wZXQl(Iu%#|uL8rdVHcZJy9;p@^3-9d6DBKdb3U~jq=2b z1+f?M0vGKXPd^)JDWN7j^O>Km}WM1A1=&e5k4F#xl>YtIj$lN=4x}RPzHm4PNNUwc@&M(qc`oXhlYAb`g@^&^4-Vrr+hrs z#8d5^3kMU8_x5KSvkwP6JniG@CY}y=yq8D6TfF!Ax7Y7o|9R5Gb3UGH;<>idI|r<> zwST*D`yui0tdD1#coue=c;qnd%iJ| z`*`<)A3xiSpY^&Fx)y%^-@bhFO6*D9q=6n^3m|gP;NW3 z None: - self.uow = uow - - async def get_all_users(self): - return await self.uow.get_all_users() +class UserRepository(SQLAlchemyRepository): + model = User diff --git a/api/service/user.py b/api/service/user.py index b27887f..8587910 100644 --- a/api/service/user.py +++ b/api/service/user.py @@ -1,10 +1,10 @@ -from api.repository.user import UserRepository -from api.uow.uow_base import UowBase +from api.uow.uow_base import IUnitOfWork class UserService: - def __init__(self, user_repository: UserRepository) -> None: - self.user_repository = user_repository + def __init__(self, uow: IUnitOfWork): + self.uow = uow async def get_all_users(self): - return await self.user_repository.get_all_users() + async with self.uow: + await self.uow.users.find_all() diff --git a/api/uow/__pycache__/__init__.cpython-311.pyc b/api/uow/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index 70b43619fcaa87b5ec4a0c4bc92fb1431d332e89..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 156 zcmZ3^%ge<81l&PSQ$h4&5CH>>P{wCAAY(d13PUi1CZpdNVY%lbV=4{ys}uit0%VI&~ZvkP9I@c|eZx`4hwj663);b74d+JGvWGBC;HPZB?uVLgN7|T&dW0qV!Xx-YJ)GtlhOd&zkcHxaz&b zKA9z9=mwRzpF1$aMD(qP%&AR@6%jM(B9#TVLcmGhA+~2xD{>zbbIuBxPEGyT>AZQ> z;_x1b+lU|oJID|mOhiX;#Cge)HY7tLvLkPxE=Cl_hM_ttpyp_R87BjnbwuDJ8*86g zk;Mj&6=%*|qF$^}5-rinjNKv+MwOw6P);H~g$P5HrZw!~=RzmlYshR_MC{w9EqZXO zSDIRc9Pzod3;v(6F?a$G&1J&|fvL(f-vB=nu@{5SOG2ywbzx}6d9qw}DKUeJX$QXV zMkSeMz(cH>3DHyoiKe_IP=M(UN>=tz(u*vd( z+=Rw9T0=WfpG}>+VHWM-nG&Y&fZ-mdgHVOcX^|EHV>R8itNGSCQVqAf3^wcNC^zsT zH-3;C-_K3fbCWe~k`6&f$K;sLCs0n&ne1fxxUITFt(RZP0A2(~77J*|(fx>~- z(5K3)%)STUFZiva!`#4T&)Gp4BR@hpc?!@R^Z~S!wA0RBQ`vP|p1@Z7LHrhNi%%sC zoVI=4ei$zFHQTg&!6lIn!xyne1J9eY?1!;zdjXtwfd?R#EV>Y;n=gq&hAn-W@2LI` zI=GHG3-1G@Z0P-4-V1%~Kp)%JC+hk{O*pgCcJfW`9}LB3D9l&0T{9yD*OYWA!3Yy6Z{^m@ZxZUuGKnoL&ErD=HtR} qgg&Zu=0h}E>&!=Lp{5r0)uFmNv@SK2D;R=s7LJ+j^*J3pME?NkxlMZj diff --git a/api/uow/__pycache__/uow_base.cpython-311.pyc b/api/uow/__pycache__/uow_base.cpython-311.pyc deleted file mode 100644 index 1380165a961f0f41a99e9c53dce0f0f36f55fa8a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1844 zcmah}-D?|15TCtINtSFUitNy0>ei%250OZlhY(CbHl>uNelaQJpd9DBjdRI;$a|;Q zN)<5J1V8kl2?jx-peL8$KcP?UU!asg#X%uZ3eB5>@?i4R*^^H3)xq7Jo%`*~?A+|^ z?EYLR(UDwPlVdD7imPgB zjAbX|X0@!F({k}#aVFfnmXERO6x>N|l7IkTfR)(;D|?6$9N`!?ZOY26L3!dOpO#59 zQ(F&0W|-lM?}cMcBi@m5LH9C=Q29%kjIO0zd)nFc{ZJ?kL3p61MS+HbF+>(upbrd0PK*Fuuj;2Ne z{Whk-XU%{+nxB7Dt={onTCLlknpH;Yeig&NZ<|zi4Xp#``CCuM=OC2WhgU<+@@EzAY4@ikyPk!6KV zFVPf2RHUV@6BFpH=i^+ zrDwB?&u16A=l)Ck%e`W$ef`Px$4duGe-syfFD^VQUV2`<)K^dc=@tL`>;Dho>;TW! z9v>f{Z4jKty!=or)vk%ZUdt^D>TfbZUKSXy`PH(_uZ3s0RM(@NuDia~a5&EE`ews$ zh8o7NESp326j+I4o^6&-7zaTO+0MB41xa4)E0CM)s~ka^dI6tw)6|2xZkl?q(tQ{8;G=GuUcd+4H1(AU Rf|) None: - self._engine = create_async_engine(db_url, echo=True) - self._session_factory = async_sessionmaker( - self._engine, - class_=AsyncSession, - expire_on_commit=False, - ) - - @property - def session(self): - return self._session_factory() - - async def __aenter__(self): - return self - - async def __aexit__(self, *args): - await self.session.rollback() - await self.session.close() - - async def commit(self): - await self.session.commit() - - async def rollback(self): - await self.session.rollback() diff --git a/api/uow/repository.py b/api/uow/repository.py new file mode 100644 index 0000000..de1745d --- /dev/null +++ b/api/uow/repository.py @@ -0,0 +1,38 @@ +from abc import ABC, abstractmethod +from typing import Generic, TypeVar +from uuid import UUID + +from sqlalchemy import insert, select +from sqlalchemy.ext.asyncio import AsyncSession + +from api.model.base import Base + +ModelType = TypeVar("ModelType", bound=Base) + + +class AbstractRepository(ABC): + @abstractmethod + async def add_one(self, data: dict): + raise NotImplementedError + + @abstractmethod + async def find_all(self): + raise NotImplementedError + + +class SQLAlchemyRepository(AbstractRepository, Generic[ModelType]): + model: type[ModelType] + + def __init__(self, session: AsyncSession): + self.session = session + + async def add_one(self, data: dict) -> UUID: + stmt = insert(self.model).values(**data).returning(self.model.id) + res = await self.session.execute(stmt) + return res.scalar_one() + + async def find_all(self): + stmt = select(self.model) + res = await self.session.execute(stmt) + res = [row[0].to_read_model() for row in res.all()] + return res diff --git a/api/uow/uow_base.py b/api/uow/uow_base.py index ec93aae..ae399b6 100644 --- a/api/uow/uow_base.py +++ b/api/uow/uow_base.py @@ -1,23 +1,47 @@ -from contextlib import AbstractContextManager -from typing import Iterable +from abc import ABC, abstractmethod -from dependency_injector.providers import Callable -from sqlalchemy import select -from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker -from sqlalchemy.orm import Session - -from api.model.user import User +from api.repository.user import UserRepository -class UowBase: - def __init__( - self, - session_factory, - ) -> None: - self.session = session_factory +class IUnitOfWork(ABC): + users: type[UserRepository] - async def get_all_users(self): - async with self.session as s: - query = select(User) - rr = await s.execute(query) - return rr.scalars().all() + @abstractmethod + def __init__(self): + ... + + @abstractmethod + async def __aenter__(self): + ... + + @abstractmethod + async def __aexit__(self): + ... + + @abstractmethod + async def commit(self): + ... + + @abstractmethod + async def rollback(self): + ... + + +class UnitOfWork: + def __init__(self, session_factory): + self.session_factory = session_factory + + async def __aenter__(self): + self.session = self.session_factory() + + self.users = UserRepository(self.session) + + async def __aexit__(self, *args): + await self.session.rollback() + await self.session.close() + + async def commit(self): + await self.session.commit() + + async def rollback(self): + await self.session.rollback() diff --git a/config/api_config.yml b/config/api_config.yml index e51d9f1..7b69928 100644 --- a/config/api_config.yml +++ b/config/api_config.yml @@ -1,6 +1,6 @@ db: - host: "db" - port: "5432" + host: "localhost" + port: 5432 database: "serviceman_db" user: "demo_user" password: "user_pass" diff --git a/docker/api/Dockerfile b/docker/api/Dockerfile index 8d44719..904c8b8 100644 --- a/docker/api/Dockerfile +++ b/docker/api/Dockerfile @@ -22,4 +22,6 @@ RUN poetry install --only api --no-root ENV CONFIG_PATH='/usr/src/service_man/config/api_config.yml' +ENV INDOCKER=1 + RUN touch __init__.py diff --git a/poetry.lock b/poetry.lock index dbb6f25..8341768 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1504,6 +1504,17 @@ files = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +[[package]] +name = "types-pyyaml" +version = "6.0.12.12" +description = "Typing stubs for PyYAML" +optional = false +python-versions = "*" +files = [ + {file = "types-PyYAML-6.0.12.12.tar.gz", hash = "sha256:334373d392fde0fdf95af5c3f1661885fa10c52167b14593eb856289e1855062"}, + {file = "types_PyYAML-6.0.12.12-py3-none-any.whl", hash = "sha256:c05bc6c158facb0676674b7f11fe3960db4f389718e19e62bd2b84d6205cfd24"}, +] + [[package]] name = "typing-extensions" version = "4.10.0" @@ -1660,4 +1671,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "e48df6e5c96549ed61d6fc700c1790974ee0889257fe4f88f7cabec78d58b330" +content-hash = "b3dee2bf5742acca846f13b5321e7b98317a7688ea6ffbb9a9c2db46476499a3" diff --git a/pyproject.toml b/pyproject.toml index 6554897..d0ea2f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ python = "^3.10" pre-commit = "^3.6.2" pytest = "^8.0.2" mypy = "^1.8.0" +types-pyyaml = "^6.0.12.12" [tool.poetry.group.api.dependencies]