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 dd7d8a5..0000000 Binary files a/api/migrations/versions/__pycache__/ec1380cb4f18_initial.cpython-310.pyc and /dev/null differ 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 d024d3c..0000000 Binary files a/api/migrations/versions/__pycache__/ec1380cb4f18_initial.cpython-311.pyc and /dev/null differ diff --git a/api/migrations/versions/ec1380cb4f18_initial.py b/api/migrations/versions/ec1380cb4f18_initial.py index 902ca5d..30b9aa0 100644 --- a/api/migrations/versions/ec1380cb4f18_initial.py +++ b/api/migrations/versions/ec1380cb4f18_initial.py @@ -6,7 +6,7 @@ Create Date: 2024-03-04 03:11:36.206211 """ -from typing import Sequence, Union +from collections.abc import Sequence import sqlalchemy as sa from alembic import op diff --git a/api/model/base.py b/api/model/base.py new file mode 100644 index 0000000..75f6318 --- /dev/null +++ b/api/model/base.py @@ -0,0 +1,12 @@ +import uuid + +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column + + +class Base(DeclarativeBase): + id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), + primary_key=True, + default=uuid.uuid4, + ) diff --git a/api/model/user.py b/api/model/user.py index 045ea8a..c665b2e 100644 --- a/api/model/user.py +++ b/api/model/user.py @@ -1,12 +1,11 @@ -from sqlalchemy import Boolean, Column, Integer, String +from sqlalchemy import Boolean, Column, String -from api.uow.database import Base +from .base import Base class User(Base): __tablename__ = "users" - id = Column(Integer, primary_key=True) email = Column(String, unique=True) hashed_password = Column(String) is_active = Column(Boolean, default=True) diff --git a/api/repository/user.py b/api/repository/user.py index f288c7e..32321fc 100644 --- a/api/repository/user.py +++ b/api/repository/user.py @@ -1,9 +1,6 @@ -from api.uow.uow_base import UowBase +from api.model.user import User +from api.uow.repository import SQLAlchemyRepository -class UserRepository: - def __init__(self, uow: UowBase) -> 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 70b4361..0000000 Binary files a/api/uow/__pycache__/__init__.cpython-311.pyc and /dev/null differ diff --git a/api/uow/__pycache__/database.cpython-311.pyc b/api/uow/__pycache__/database.cpython-311.pyc deleted file mode 100644 index 6cad726..0000000 Binary files a/api/uow/__pycache__/database.cpython-311.pyc and /dev/null differ 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 1380165..0000000 Binary files a/api/uow/__pycache__/uow_base.cpython-311.pyc and /dev/null differ diff --git a/api/uow/database.py b/api/uow/database.py deleted file mode 100644 index acb81b5..0000000 --- a/api/uow/database.py +++ /dev/null @@ -1,37 +0,0 @@ -from contextlib import (AbstractContextManager, asynccontextmanager, - contextmanager) -from typing import Callable - -from sqlalchemy.ext.asyncio import (AsyncSession, async_sessionmaker, - create_async_engine) -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import Session - -Base = declarative_base() - - -class Database: - def __init__(self, db_url: str) -> 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]