Refactor code for improved readability and consistency
Test / test (push) Successful in 15s

- Reformatted function signatures in `organization_service.py` and `task_service.py` for better alignment.
- Updated import statements across multiple files for consistency and organization.
- Enhanced test files by improving formatting and ensuring consistent use of async session factories.
- Added type hints and improved type safety in various service and test files.
- Adjusted `pyproject.toml` to include configuration for isort, mypy, and ruff for better code quality checks.
- Cleaned up unused imports and organized existing ones in several test files.
This commit is contained in:
Artem Kashaev
2025-12-01 16:18:03 +05:00
parent eecb74c523
commit 5fcb574aca
62 changed files with 765 additions and 476 deletions
+4 -1
View File
@@ -1,4 +1,5 @@
"""Repository helpers for deal activities."""
from __future__ import annotations
from collections.abc import Sequence
@@ -39,7 +40,9 @@ class ActivityRepository:
stmt = (
select(Activity)
.join(Deal, Deal.id == Activity.deal_id)
.where(Activity.deal_id == params.deal_id, Deal.organization_id == params.organization_id)
.where(
Activity.deal_id == params.deal_id, Deal.organization_id == params.organization_id
)
.order_by(Activity.created_at)
)
stmt = self._apply_window(stmt, params)
+2 -1
View File
@@ -1,4 +1,5 @@
"""Analytics-specific data access helpers."""
from __future__ import annotations
from dataclasses import dataclass
@@ -58,7 +59,7 @@ class AnalyticsRepository:
deal_count=int(count or 0),
amount_sum=_to_decimal(amount_sum),
amount_count=int(amount_count or 0),
)
),
)
return rollup
+8 -3
View File
@@ -1,4 +1,5 @@
"""Repository helpers for contacts with role-aware access."""
from __future__ import annotations
from collections.abc import Mapping, Sequence
@@ -44,7 +45,9 @@ class ContactRepository:
role: OrganizationRole,
user_id: int,
) -> Sequence[Contact]:
stmt: Select[tuple[Contact]] = select(Contact).where(Contact.organization_id == params.organization_id)
stmt: Select[tuple[Contact]] = select(Contact).where(
Contact.organization_id == params.organization_id
)
stmt = self._apply_filters(stmt, params, role, user_id)
offset = (max(params.page, 1) - 1) * params.page_size
stmt = stmt.order_by(Contact.created_at.desc()).offset(offset).limit(params.page_size)
@@ -59,7 +62,9 @@ class ContactRepository:
role: OrganizationRole,
user_id: int,
) -> Contact | None:
stmt = select(Contact).where(Contact.id == contact_id, Contact.organization_id == organization_id)
stmt = select(Contact).where(
Contact.id == contact_id, Contact.organization_id == organization_id
)
result = await self._session.scalars(stmt)
return result.first()
@@ -117,7 +122,7 @@ class ContactRepository:
pattern = f"%{params.search.lower()}%"
stmt = stmt.where(
func.lower(Contact.name).like(pattern)
| func.lower(func.coalesce(Contact.email, "")).like(pattern)
| func.lower(func.coalesce(Contact.email, "")).like(pattern),
)
if params.owner_id is not None:
if role == OrganizationRole.MEMBER:
+117 -115
View File
@@ -1,4 +1,5 @@
"""Deal repository with access-aware CRUD helpers."""
from __future__ import annotations
from collections.abc import Mapping, Sequence
@@ -12,142 +13,143 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.models.deal import Deal, DealCreate, DealStage, DealStatus
from app.models.organization_member import OrganizationRole
ORDERABLE_COLUMNS: dict[str, Any] = {
"created_at": Deal.created_at,
"amount": Deal.amount,
"title": Deal.title,
"created_at": Deal.created_at,
"amount": Deal.amount,
"title": Deal.title,
}
class DealAccessError(Exception):
"""Raised when a user attempts an operation without sufficient permissions."""
"""Raised when a user attempts an operation without sufficient permissions."""
@dataclass(slots=True)
class DealQueryParams:
"""Filters supported by list queries."""
"""Filters supported by list queries."""
organization_id: int
page: int = 1
page_size: int = 20
statuses: Sequence[DealStatus] | None = None
stage: DealStage | None = None
owner_id: int | None = None
min_amount: Decimal | None = None
max_amount: Decimal | None = None
order_by: str | None = None
order_desc: bool = True
organization_id: int
page: int = 1
page_size: int = 20
statuses: Sequence[DealStatus] | None = None
stage: DealStage | None = None
owner_id: int | None = None
min_amount: Decimal | None = None
max_amount: Decimal | None = None
order_by: str | None = None
order_desc: bool = True
class DealRepository:
"""Provides CRUD helpers for deals with role-aware filtering."""
"""Provides CRUD helpers for deals with role-aware filtering."""
def __init__(self, session: AsyncSession) -> None:
self._session = session
def __init__(self, session: AsyncSession) -> None:
self._session = session
@property
def session(self) -> AsyncSession:
return self._session
@property
def session(self) -> AsyncSession:
return self._session
async def list(
self,
*,
params: DealQueryParams,
role: OrganizationRole,
user_id: int,
) -> Sequence[Deal]:
stmt = select(Deal).where(Deal.organization_id == params.organization_id)
stmt = self._apply_filters(stmt, params, role, user_id)
stmt = self._apply_ordering(stmt, params)
async def list(
self,
*,
params: DealQueryParams,
role: OrganizationRole,
user_id: int,
) -> Sequence[Deal]:
stmt = select(Deal).where(Deal.organization_id == params.organization_id)
stmt = self._apply_filters(stmt, params, role, user_id)
stmt = self._apply_ordering(stmt, params)
offset = (max(params.page, 1) - 1) * params.page_size
stmt = stmt.offset(offset).limit(params.page_size)
result = await self._session.scalars(stmt)
return result.all()
offset = (max(params.page, 1) - 1) * params.page_size
stmt = stmt.offset(offset).limit(params.page_size)
result = await self._session.scalars(stmt)
return result.all()
async def get(
self,
deal_id: int,
*,
organization_id: int,
role: OrganizationRole,
user_id: int,
require_owner: bool = False,
) -> Deal | None:
stmt = select(Deal).where(Deal.id == deal_id, Deal.organization_id == organization_id)
stmt = self._apply_role_clause(stmt, role, user_id, require_owner=require_owner)
result = await self._session.scalars(stmt)
return result.first()
async def get(
self,
deal_id: int,
*,
organization_id: int,
role: OrganizationRole,
user_id: int,
require_owner: bool = False,
) -> Deal | None:
stmt = select(Deal).where(Deal.id == deal_id, Deal.organization_id == organization_id)
stmt = self._apply_role_clause(stmt, role, user_id, require_owner=require_owner)
result = await self._session.scalars(stmt)
return result.first()
async def create(
self,
data: DealCreate,
*,
role: OrganizationRole,
user_id: int,
) -> Deal:
if role == OrganizationRole.MEMBER and data.owner_id != user_id:
raise DealAccessError("Members can only create deals they own")
deal = Deal(**data.model_dump())
self._session.add(deal)
await self._session.flush()
return deal
async def create(
self,
data: DealCreate,
*,
role: OrganizationRole,
user_id: int,
) -> Deal:
if role == OrganizationRole.MEMBER and data.owner_id != user_id:
raise DealAccessError("Members can only create deals they own")
deal = Deal(**data.model_dump())
self._session.add(deal)
await self._session.flush()
return deal
async def update(
self,
deal: Deal,
updates: Mapping[str, Any],
*,
role: OrganizationRole,
user_id: int,
) -> Deal:
if role == OrganizationRole.MEMBER and deal.owner_id != user_id:
raise DealAccessError("Members can only modify their own deals")
for field, value in updates.items():
if hasattr(deal, field):
setattr(deal, field, value)
await self._session.flush()
await self._session.refresh(deal)
return deal
async def update(
self,
deal: Deal,
updates: Mapping[str, Any],
*,
role: OrganizationRole,
user_id: int,
) -> Deal:
if role == OrganizationRole.MEMBER and deal.owner_id != user_id:
raise DealAccessError("Members can only modify their own deals")
for field, value in updates.items():
if hasattr(deal, field):
setattr(deal, field, value)
await self._session.flush()
await self._session.refresh(deal)
return deal
def _apply_filters(
self,
stmt: Select[tuple[Deal]],
params: DealQueryParams,
role: OrganizationRole,
user_id: int,
) -> Select[tuple[Deal]]:
if params.statuses:
stmt = stmt.where(Deal.status.in_(params.statuses))
if params.stage:
stmt = stmt.where(Deal.stage == params.stage)
if params.owner_id is not None:
if role == OrganizationRole.MEMBER and params.owner_id != user_id:
raise DealAccessError("Members cannot filter by other owners")
stmt = stmt.where(Deal.owner_id == params.owner_id)
if params.min_amount is not None:
stmt = stmt.where(Deal.amount >= params.min_amount)
if params.max_amount is not None:
stmt = stmt.where(Deal.amount <= params.max_amount)
def _apply_filters(
self,
stmt: Select[tuple[Deal]],
params: DealQueryParams,
role: OrganizationRole,
user_id: int,
) -> Select[tuple[Deal]]:
if params.statuses:
stmt = stmt.where(Deal.status.in_(params.statuses))
if params.stage:
stmt = stmt.where(Deal.stage == params.stage)
if params.owner_id is not None:
if role == OrganizationRole.MEMBER and params.owner_id != user_id:
raise DealAccessError("Members cannot filter by other owners")
stmt = stmt.where(Deal.owner_id == params.owner_id)
if params.min_amount is not None:
stmt = stmt.where(Deal.amount >= params.min_amount)
if params.max_amount is not None:
stmt = stmt.where(Deal.amount <= params.max_amount)
return self._apply_role_clause(stmt, role, user_id)
return self._apply_role_clause(stmt, role, user_id)
def _apply_role_clause(
self,
stmt: Select[tuple[Deal]],
role: OrganizationRole,
user_id: int,
*,
require_owner: bool = False,
) -> Select[tuple[Deal]]:
if role in {OrganizationRole.OWNER, OrganizationRole.ADMIN, OrganizationRole.MANAGER}:
return stmt
if require_owner:
return stmt.where(Deal.owner_id == user_id)
return stmt
def _apply_role_clause(
self,
stmt: Select[tuple[Deal]],
role: OrganizationRole,
user_id: int,
*,
require_owner: bool = False,
) -> Select[tuple[Deal]]:
if role in {OrganizationRole.OWNER, OrganizationRole.ADMIN, OrganizationRole.MANAGER}:
return stmt
if require_owner:
return stmt.where(Deal.owner_id == user_id)
return stmt
def _apply_ordering(self, stmt: Select[tuple[Deal]], params: DealQueryParams) -> Select[tuple[Deal]]:
column = ORDERABLE_COLUMNS.get(params.order_by or "created_at", Deal.created_at)
order_func = desc if params.order_desc else asc
return stmt.order_by(order_func(column))
def _apply_ordering(
self, stmt: Select[tuple[Deal]], params: DealQueryParams
) -> Select[tuple[Deal]]:
column = ORDERABLE_COLUMNS.get(params.order_by or "created_at", Deal.created_at)
order_func = desc if params.order_desc else asc
return stmt.order_by(order_func(column))
+2 -1
View File
@@ -1,11 +1,12 @@
"""Organization repository for database operations."""
from __future__ import annotations
from collections.abc import Sequence
from sqlalchemy import select
from sqlalchemy.orm import selectinload
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.models.organization import Organization, OrganizationCreate
from app.models.organization_member import OrganizationMember
+4 -1
View File
@@ -1,4 +1,5 @@
"""Task repository providing role-aware CRUD helpers."""
from __future__ import annotations
from collections.abc import Mapping, Sequence
@@ -105,7 +106,9 @@ class TaskRepository:
await self._session.flush()
return task
def _apply_filters(self, stmt: Select[tuple[Task]], params: TaskQueryParams) -> Select[tuple[Task]]:
def _apply_filters(
self, stmt: Select[tuple[Task]], params: TaskQueryParams
) -> Select[tuple[Task]]:
if params.deal_id is not None:
stmt = stmt.where(Task.deal_id == params.deal_id)
if params.only_open:
+1
View File
@@ -1,4 +1,5 @@
"""User repository handling database operations."""
from __future__ import annotations
from collections.abc import Sequence