- 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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,4 +1,5 @@
|
||||
"""User repository handling database operations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
Reference in New Issue
Block a user