Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/api/api_v1/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from fastapi import APIRouter

from api.api_v1.projects import router as project_router
from api.api_v1.tags import router as tag_router

from .auth import router as auth_router
from .upload_file import router as upload_file_router
Expand All @@ -15,4 +16,5 @@
router.include_router(auth_router)
router.include_router(upload_file_router)
router.include_router(project_router)
router.include_router(tag_router)
router.include_router(user_router)
2 changes: 1 addition & 1 deletion app/api/api_v1/projects/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from fastapi import APIRouter

from .details_views import router as details_router
from .list_view import router as list_router
from .list_views import router as list_router

router = APIRouter(
prefix="/projects",
Expand Down
12 changes: 12 additions & 0 deletions app/api/api_v1/tags/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from fastapi import APIRouter

from .details_views import router as details_views_router
from .list_views import router as list_views_router

router = APIRouter(
prefix="/tags",
tags=["Tags"],
)

router.include_router(list_views_router)
router.include_router(details_views_router)
36 changes: 36 additions & 0 deletions app/api/api_v1/tags/details_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from dependencies.annotations import AuthUserByAccessTokenDep, TagServiceDep
from fastapi import APIRouter, status
from schemas.tag import TagResponseList

router = APIRouter()


@router.get(
"/project/{project_id}",
status_code=status.HTTP_200_OK,
response_model=TagResponseList,
)
async def get_project_tags(
project_id: int,
user_id: AuthUserByAccessTokenDep,
tag_service: TagServiceDep,
) -> TagResponseList:
return await tag_service.get_project_tags(
project_id=project_id,
user_id=user_id,
)


@router.delete(
"/{tag_id}",
status_code=status.HTTP_204_NO_CONTENT,
)
async def delete_project_tag(
tag_id: int,
user_id: AuthUserByAccessTokenDep,
tag_service: TagServiceDep,
) -> None:
return await tag_service.delete_by_id(
tag_id=tag_id,
user_id=user_id,
)
21 changes: 21 additions & 0 deletions app/api/api_v1/tags/list_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from dependencies.annotations import AuthUserByAccessTokenDep, TagServiceDep
from fastapi import APIRouter, status
from schemas.tag import TagCreate, TagResponse

router = APIRouter()


@router.post(
"/create",
status_code=status.HTTP_201_CREATED,
response_model=TagResponse,
)
async def create_tag(
user_id: AuthUserByAccessTokenDep,
create_tag_data: TagCreate,
tag_service: TagServiceDep,
) -> TagResponse:
return await tag_service.create_tag(
user_id=user_id,
create_tag_data=create_tag_data,
)
3 changes: 3 additions & 0 deletions app/core/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
PROJECT_PAGINATION_SIZE_MIN_VALUE = 1
PROJECT_PAGINATION_PAGE_MIN_VALUE = 1

TAG_MIN_LENGTH = 3
TAG_MAX_LENGTH = 25

RENDER_WIDTH_MIN_VALUE = 20
RENDER_WIDTH_MAX_VALUE = 2160

Expand Down
23 changes: 23 additions & 0 deletions app/core/exceptions/tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from core.exceptions.base import NotFoundError


class TagNotFoundError(NotFoundError):
"""
Класс для ошибок, связанных с
ненахождением тэга проекта.
"""

def __init__(self, detail: str) -> None:
self.detail = detail


class TagIdNotFoundError(TagNotFoundError):
"""
Класс для ошибок, связанных с
ненахождением тэга по id.
"""

def __init__(self, tag_id: int) -> None:
self.tag_id = tag_id
detail = f"Tag with id = {tag_id} not found."
super().__init__(detail)
38 changes: 36 additions & 2 deletions app/core/interfaces/repositories.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from abc import ABC, abstractmethod

from infrastructure.database.models import File, Outbox, Project, Render, User
from infrastructure.database.models import File, Outbox, Project, Render, Tag, User
from pydantic import EmailStr
from schemas.event import EventCreate
from schemas.file import FileCreate
from schemas.project import ProjectCreate, ProjectPartialUpdate
from schemas.render import RenderCreate
from schemas.tag import TagCreate
from schemas.user import UserCreate, UserUpdate


Expand Down Expand Up @@ -182,9 +183,42 @@ async def delete_by_id(self, project_id: int) -> None:
"""


class AbstractTagRepository(AbstractRepository):
"""
Интерфейс для работы с тэгами проектов.
"""

@abstractmethod
async def get_by_id(self, tag_id: int) -> Tag | None:
"""
Метод для получения информации о тэге по id.
"""

@abstractmethod
async def get_project_tags(self, project_id: int) -> list[Tag] | None:
"""
Метод для получения тэгов проекта.
"""

@abstractmethod
async def create_tag(
self,
create_tag_data: TagCreate,
) -> Tag:
"""
Метод для добавления тэга к проекту.
"""

@abstractmethod
async def delete_by_id(self, tag_id: int) -> None:
"""
Метод для удаления тэга по id.
"""


class AbstractOutboxRepository(AbstractRepository):
"""
Класс для работы с outbox таблицей.
Интерфейс для работы с outbox таблицей.
"""

@abstractmethod
Expand Down
7 changes: 7 additions & 0 deletions app/dependencies/annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from services.auth import AuthService
from services.file_uploader import FileUploader
from services.project import ProjectService
from services.tag import TagService
from services.user import UserService

from dependencies.auth import (
Expand All @@ -21,6 +22,7 @@
get_auth_service,
get_input_file_uploader,
get_project_service,
get_tag_service,
get_user_service,
)

Expand All @@ -39,6 +41,11 @@
Depends(get_auth_service),
]

TagServiceDep = Annotated[
TagService,
Depends(get_tag_service),
]

AuthUserByAccessTokenDep = Annotated[
int,
Depends(get_auth_user_by_access_token),
Expand Down
13 changes: 13 additions & 0 deletions app/dependencies/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from services.auth import AuthService
from services.file_uploader import FileUploader
from services.project import ProjectService
from services.tag import TagService
from services.user import UserService

from dependencies.minio import get_minio_client
Expand Down Expand Up @@ -70,3 +71,15 @@ async def get_project_service(
unit_of_work,
)
yield project_service


async def get_tag_service(
unit_of_work: Annotated[
UnitOfWork,
Depends(get_unit_of_work),
],
) -> AsyncGenerator[TagService]:
tag_service = TagService(
unit_of_work,
)
yield tag_service
2 changes: 2 additions & 0 deletions app/infrastructure/database/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
"Outbox",
"Project",
"Render",
"Tag",
"User",
)

from .file import File
from .outbox import Outbox
from .project import Project
from .render import Render
from .tag import Tag
from .user import User
7 changes: 6 additions & 1 deletion app/infrastructure/database/models/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from infrastructure.database.core import Base

if TYPE_CHECKING:
from infrastructure.database.models import File, Render, User
from infrastructure.database.models import File, Render, Tag, User


class Project(Base):
Expand Down Expand Up @@ -59,6 +59,11 @@ class Project(Base):
foreign_keys=[render_id],
back_populates="project",
)
tags: Mapped[list["Tag"]] = relationship(
"Tag",
foreign_keys="Tag.project_id",
back_populates="project",
)

__table_args__ = (
CheckConstraint(
Expand Down
34 changes: 34 additions & 0 deletions app/infrastructure/database/models/tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from typing import TYPE_CHECKING

from core.constants import TAG_MAX_LENGTH, TAG_MIN_LENGTH
from sqlalchemy import CheckConstraint, ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column, relationship

from infrastructure.database.core import Base
from infrastructure.database.models import Project

if TYPE_CHECKING:
from infrastructure.database.models import Project


class Tag(Base):
__tablename__ = "tags"

name: Mapped[str] = mapped_column(String(TAG_MAX_LENGTH))
project_id: Mapped[int] = mapped_column(ForeignKey("projects.id"))

project: Mapped["Project"] = relationship(
"Project",
foreign_keys=[project_id],
back_populates="tags",
)

__table_args__ = (
CheckConstraint(
f"""
LENGTH(name) >= {TAG_MIN_LENGTH}
AND LENGTH(name) <= {TAG_MAX_LENGTH}
""",
name="length_tag_name",
),
)
2 changes: 2 additions & 0 deletions app/infrastructure/database/repositories/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
"OutboxRepository",
"ProjectRepository",
"RenderRepository",
"TagRepository",
"UserRepository",
)
from .file import FileRepository
from .outbox import OutboxRepository
from .project import ProjectRepository
from .render import RenderRepository
from .tag import TagRepository
from .user import UserRepository
33 changes: 33 additions & 0 deletions app/infrastructure/database/repositories/tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from core.interfaces.repositories import AbstractTagRepository
from schemas.tag import TagCreate
from sqlalchemy import delete, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload

from infrastructure.database.models import Tag


class TagRepository(AbstractTagRepository):
def __init__(self, session: AsyncSession) -> None:
self.session = session

async def get_by_id(self, tag_id: int) -> Tag | None:
stmt = select(Tag).options(joinedload(Tag.project)).where(Tag.id == tag_id)
result = await self.session.execute(stmt)
return result.scalar()

async def get_project_tags(self, project_id: int) -> list[Tag] | None:
stmt = select(Tag).where(Tag.project_id == project_id)
result = await self.session.execute(stmt)
return list(result.scalars().all())

async def create_tag(self, create_tag_data: TagCreate) -> Tag:
tag = Tag(**create_tag_data.model_dump())
self.session.add(tag)
await self.session.flush()
return tag

async def delete_by_id(self, tag_id: int) -> None:
stmt = delete(Tag).where(Tag.id == tag_id)
await self.session.execute(stmt)
await self.session.flush()
39 changes: 39 additions & 0 deletions app/migrations/versions/c237a3caa1ac_add_tags_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""add tags table

Revision ID: c237a3caa1ac
Revises: cde50160235e
Create Date: 2026-08-26 16:00:57.287138

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = 'c237a3caa1ac'
down_revision: Union[str, Sequence[str], None] = 'cde50160235e'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('tags',
sa.Column('name', sa.String(length=25), nullable=False),
sa.Column('project_id', sa.Integer(), nullable=False),
sa.Column('id', sa.Integer(), nullable=False),
sa.CheckConstraint('\n LENGTH(name) >= 3\n AND LENGTH(name) <= 25\n ', name=op.f('ck_tags_length_tag_name')),
sa.ForeignKeyConstraint(['project_id'], ['projects.id'], name=op.f('fk_tags_project_id_projects')),
sa.PrimaryKeyConstraint('id', name=op.f('pk_tags_id'))
)
# ### end Alembic commands ###


def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('tags')
# ### end Alembic commands ###
Loading
Loading