init: new structure + fix lint errors

This commit is contained in:
Daniil
2026-02-03 02:15:07 +03:00
commit 67e0f22b4f
89 changed files with 7654 additions and 0 deletions
View File
+27
View File
@@ -0,0 +1,27 @@
from __future__ import annotations
import uuid
from sqlalchemy import ForeignKey, String
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column
from cpv3.db.base import Base, BaseModelMixin
class Webhook(Base, BaseModelMixin):
__tablename__ = "webhooks"
project_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True),
ForeignKey("projects.id", ondelete="RESTRICT"),
nullable=True,
index=True,
)
user_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("users.id", ondelete="RESTRICT"), nullable=True, index=True
)
event: Mapped[str | None] = mapped_column(String(255), nullable=True)
url: Mapped[str] = mapped_column(String(1024))
secret: Mapped[str | None] = mapped_column(String(255), nullable=True)
+63
View File
@@ -0,0 +1,63 @@
from __future__ import annotations
import uuid
from sqlalchemy import Select, select
from sqlalchemy.ext.asyncio import AsyncSession
from cpv3.modules.users.models import User
from cpv3.modules.webhooks.models import Webhook
from cpv3.modules.webhooks.schemas import WebhookCreate, WebhookUpdate
class WebhookRepository:
"""Repository for Webhook database operations."""
def __init__(self, session: AsyncSession) -> None:
self._session = session
async def list_all(self, *, requester: User) -> list[Webhook]:
stmt: Select[tuple[Webhook]] = select(Webhook).where(
Webhook.is_active.is_(True)
)
if not requester.is_staff:
stmt = stmt.where(Webhook.user_id == requester.id)
result = await self._session.execute(stmt.order_by(Webhook.created_at.desc()))
return list(result.scalars().all())
async def get_by_id(self, webhook_id: uuid.UUID) -> Webhook | None:
result = await self._session.execute(
select(Webhook)
.where(Webhook.id == webhook_id)
.where(Webhook.is_active.is_(True))
)
return result.scalar_one_or_none()
async def create(self, *, requester: User, data: WebhookCreate) -> Webhook:
webhook = Webhook(
user_id=requester.id,
project_id=data.project_id,
event=data.event,
url=data.url,
secret=data.secret,
is_active=data.is_active,
)
self._session.add(webhook)
await self._session.commit()
await self._session.refresh(webhook)
return webhook
async def update(self, webhook: Webhook, data: WebhookUpdate) -> Webhook:
for key, value in data.model_dump(exclude_unset=True).items():
if value is not None:
setattr(webhook, key, value)
await self._session.commit()
await self._session.refresh(webhook)
return webhook
async def deactivate(self, webhook: Webhook) -> None:
webhook.is_active = False
await self._session.commit()
+89
View File
@@ -0,0 +1,89 @@
from __future__ import annotations
import uuid
from fastapi import APIRouter, Depends, HTTPException, Response, status
from sqlalchemy.ext.asyncio import AsyncSession
from cpv3.infrastructure.auth import get_current_user
from cpv3.db.session import get_db
from cpv3.modules.users.models import User
from cpv3.modules.webhooks.schemas import WebhookCreate, WebhookRead, WebhookUpdate
from cpv3.modules.webhooks.service import WebhookService
router = APIRouter(prefix="/api/webhooks", tags=["Webhooks"])
@router.get("/", response_model=list[WebhookRead])
async def list_all_webhooks(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> list[WebhookRead]:
service = WebhookService(db)
items = await service.list_webhooks(requester=current_user)
return [WebhookRead.model_validate(w) for w in items]
@router.post("/", response_model=WebhookRead, status_code=status.HTTP_201_CREATED)
async def create_webhook_endpoint(
body: WebhookCreate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> WebhookRead:
service = WebhookService(db)
webhook = await service.create_webhook(requester=current_user, data=body)
return WebhookRead.model_validate(webhook)
@router.get("/{webhook_id}/", response_model=WebhookRead)
async def retrieve_webhook_endpoint(
webhook_id: uuid.UUID,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> WebhookRead:
service = WebhookService(db)
webhook = await service.get_webhook(webhook_id)
if webhook is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not found")
if not current_user.is_staff and webhook.user_id != current_user.id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden")
return WebhookRead.model_validate(webhook)
@router.patch("/{webhook_id}/", response_model=WebhookRead)
async def patch_webhook_endpoint(
webhook_id: uuid.UUID,
body: WebhookUpdate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> WebhookRead:
service = WebhookService(db)
webhook = await service.get_webhook(webhook_id)
if webhook is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not found")
if not current_user.is_staff and webhook.user_id != current_user.id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden")
webhook = await service.update_webhook(webhook, body)
return WebhookRead.model_validate(webhook)
@router.delete("/{webhook_id}/", status_code=status.HTTP_204_NO_CONTENT)
async def delete_webhook_endpoint(
webhook_id: uuid.UUID,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> Response:
service = WebhookService(db)
webhook = await service.get_webhook(webhook_id)
if webhook is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not found")
if not current_user.is_staff and webhook.user_id != current_user.id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden")
await service.deactivate_webhook(webhook)
return Response(status_code=status.HTTP_204_NO_CONTENT)
+35
View File
@@ -0,0 +1,35 @@
from __future__ import annotations
from datetime import datetime
from uuid import UUID
from cpv3.common.schemas import Schema
class WebhookRead(Schema):
id: UUID
project_id: UUID | None
user_id: UUID | None
event: str | None
url: str
secret: str | None
is_active: bool
created_at: datetime
updated_at: datetime
class WebhookCreate(Schema):
project_id: UUID | None = None
event: str | None = None
url: str
secret: str | None = None
is_active: bool = True
class WebhookUpdate(Schema):
event: str | None = None
url: str | None = None
secret: str | None = None
is_active: bool | None = None
+32
View File
@@ -0,0 +1,32 @@
from __future__ import annotations
import uuid
from sqlalchemy.ext.asyncio import AsyncSession
from cpv3.modules.webhooks.models import Webhook
from cpv3.modules.webhooks.repository import WebhookRepository
from cpv3.modules.webhooks.schemas import WebhookCreate, WebhookUpdate
from cpv3.modules.users.models import User
class WebhookService:
"""Service for webhook business logic and orchestration."""
def __init__(self, session: AsyncSession) -> None:
self._repo = WebhookRepository(session)
async def list_webhooks(self, *, requester: User) -> list[Webhook]:
return await self._repo.list_all(requester=requester)
async def get_webhook(self, webhook_id: uuid.UUID) -> Webhook | None:
return await self._repo.get_by_id(webhook_id)
async def create_webhook(self, *, requester: User, data: WebhookCreate) -> Webhook:
return await self._repo.create(requester=requester, data=data)
async def update_webhook(self, webhook: Webhook, data: WebhookUpdate) -> Webhook:
return await self._repo.update(webhook, data)
async def deactivate_webhook(self, webhook: Webhook) -> None:
await self._repo.deactivate(webhook)