diff --git a/app/api/v1/analytics.py b/app/api/v1/analytics.py index 6ccc759..9a282be 100644 --- a/app/api/v1/analytics.py +++ b/app/api/v1/analytics.py @@ -95,5 +95,5 @@ async def deals_funnel( breakdowns: list[StageBreakdown] = await service.get_deal_funnel(context.organization_id) return DealFunnelResponse( - stages=[StageBreakdownModel.model_validate(item) for item in breakdowns] + stages=[StageBreakdownModel.model_validate(item) for item in breakdowns], ) diff --git a/app/api/v1/contacts.py b/app/api/v1/contacts.py index 52d09e8..db899e7 100644 --- a/app/api/v1/contacts.py +++ b/app/api/v1/contacts.py @@ -83,7 +83,7 @@ async def create_contact( service: ContactService = Depends(get_contact_service), ) -> ContactRead: data = payload.to_domain( - organization_id=context.organization_id, fallback_owner=context.user_id + organization_id=context.organization_id, fallback_owner=context.user_id, ) try: contact = await service.create_contact(data, context=context) diff --git a/app/api/v1/deals.py b/app/api/v1/deals.py index 991c54a..65de8a2 100644 --- a/app/api/v1/deals.py +++ b/app/api/v1/deals.py @@ -68,7 +68,7 @@ async def list_deals( stage_value = DealStage(stage) if stage else None except ValueError as exc: raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid deal filter" + status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid deal filter", ) from exc params = DealQueryParams( @@ -100,7 +100,7 @@ async def create_deal( """Create a new deal within the current organization.""" data = payload.to_domain( - organization_id=context.organization_id, fallback_owner=context.user_id + organization_id=context.organization_id, fallback_owner=context.user_id, ) try: deal = await service.create_deal(data, context=context) diff --git a/app/core/cache.py b/app/core/cache.py index c75995d..d76f109 100644 --- a/app/core/cache.py +++ b/app/core/cache.py @@ -46,7 +46,7 @@ class RedisCacheManager: if self._client is not None: return self._client = redis.from_url( - settings.redis_url, encoding="utf-8", decode_responses=False + settings.redis_url, encoding="utf-8", decode_responses=False, ) await self._refresh_availability() @@ -63,7 +63,7 @@ class RedisCacheManager: async with self._lock: if self._client is None: self._client = redis.from_url( - settings.redis_url, encoding="utf-8", decode_responses=False + settings.redis_url, encoding="utf-8", decode_responses=False, ) await self._refresh_availability() @@ -127,7 +127,7 @@ async def read_json(client: Redis, key: str) -> Any | None: async def write_json( - client: Redis, key: str, value: Any, ttl_seconds: int, backoff_ms: int + client: Redis, key: str, value: Any, ttl_seconds: int, backoff_ms: int, ) -> None: """Serialize data to JSON and store it with TTL using retry/backoff.""" payload = json.dumps(value, separators=(",", ":"), ensure_ascii=True).encode("utf-8") diff --git a/app/core/config.py b/app/core/config.py index 7a0de46..3842fef 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -17,7 +17,7 @@ class Settings(BaseSettings): db_name: str = Field(default="test_task_crm", description="Database name") db_user: str = Field(default="postgres", description="Database user") db_password: SecretStr = Field( - default=SecretStr("postgres"), description="Database user password" + default=SecretStr("postgres"), description="Database user password", ) database_url_override: str | None = Field( default=None, @@ -32,7 +32,7 @@ class Settings(BaseSettings): redis_enabled: bool = Field(default=False, description="Toggle Redis-backed cache usage") redis_url: str = Field(default="redis://localhost:6379/0", description="Redis connection URL") analytics_cache_ttl_seconds: int = Field( - default=120, ge=1, description="TTL for cached analytics responses" + default=120, ge=1, description="TTL for cached analytics responses", ) analytics_cache_backoff_ms: int = Field( default=200, diff --git a/app/models/user.py b/app/models/user.py index e435a45..4b69860 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -38,7 +38,7 @@ class User(Base): ) memberships = relationship( - "OrganizationMember", back_populates="user", cascade="all, delete-orphan" + "OrganizationMember", back_populates="user", cascade="all, delete-orphan", ) owned_contacts = relationship("Contact", back_populates="owner") owned_deals = relationship("Deal", back_populates="owner") diff --git a/app/repositories/activity_repo.py b/app/repositories/activity_repo.py index b32471f..f980370 100644 --- a/app/repositories/activity_repo.py +++ b/app/repositories/activity_repo.py @@ -41,7 +41,7 @@ class ActivityRepository: select(Activity) .join(Deal, Deal.id == Activity.deal_id) .where( - Activity.deal_id == params.deal_id, Deal.organization_id == params.organization_id + Activity.deal_id == params.deal_id, Deal.organization_id == params.organization_id, ) .order_by(Activity.created_at) ) diff --git a/app/repositories/contact_repo.py b/app/repositories/contact_repo.py index 7fbf505..bf245bb 100644 --- a/app/repositories/contact_repo.py +++ b/app/repositories/contact_repo.py @@ -46,7 +46,7 @@ class ContactRepository: user_id: int, ) -> Sequence[Contact]: stmt: Select[tuple[Contact]] = select(Contact).where( - Contact.organization_id == params.organization_id + Contact.organization_id == params.organization_id, ) stmt = self._apply_filters(stmt, params, role, user_id) offset = (max(params.page, 1) - 1) * params.page_size @@ -63,7 +63,7 @@ class ContactRepository: user_id: int, ) -> Contact | None: stmt = select(Contact).where( - Contact.id == contact_id, Contact.organization_id == organization_id + Contact.id == contact_id, Contact.organization_id == organization_id, ) result = await self._session.scalars(stmt) return result.first() diff --git a/app/repositories/deal_repo.py b/app/repositories/deal_repo.py index c7446bd..6d6c482 100644 --- a/app/repositories/deal_repo.py +++ b/app/repositories/deal_repo.py @@ -148,7 +148,7 @@ class DealRepository: return stmt def _apply_ordering( - self, stmt: Select[tuple[Deal]], params: DealQueryParams + 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 diff --git a/app/repositories/task_repo.py b/app/repositories/task_repo.py index 5f78e48..4a4fa73 100644 --- a/app/repositories/task_repo.py +++ b/app/repositories/task_repo.py @@ -107,7 +107,7 @@ class TaskRepository: return task def _apply_filters( - self, stmt: Select[tuple[Task]], params: TaskQueryParams + 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) diff --git a/app/services/analytics_service.py b/app/services/analytics_service.py index 93f52d8..c63a1de 100644 --- a/app/services/analytics_service.py +++ b/app/services/analytics_service.py @@ -169,7 +169,7 @@ class AnalyticsService: return _deserialize_summary(payload) async def _store_summary_cache( - self, organization_id: int, days: int, summary: DealSummary + self, organization_id: int, days: int, summary: DealSummary, ) -> None: if not self._is_cache_enabled() or self._cache is None: return @@ -187,7 +187,7 @@ class AnalyticsService: return _deserialize_funnel(payload) async def _store_funnel_cache( - self, organization_id: int, breakdowns: list[StageBreakdown] + self, organization_id: int, breakdowns: list[StageBreakdown], ) -> None: if not self._is_cache_enabled() or self._cache is None: return @@ -321,7 +321,7 @@ def _deserialize_funnel(payload: Any) -> list[StageBreakdown] | None: async def invalidate_analytics_cache( - cache: Redis | None, organization_id: int, backoff_ms: int + cache: Redis | None, organization_id: int, backoff_ms: int, ) -> None: """Remove cached analytics payloads for the organization.""" diff --git a/app/services/contact_service.py b/app/services/contact_service.py index 87e2767..fc4465f 100644 --- a/app/services/contact_service.py +++ b/app/services/contact_service.py @@ -80,7 +80,7 @@ class ContactService: ) try: return await self._repository.list( - params=params, role=context.role, user_id=context.user_id + params=params, role=context.role, user_id=context.user_id, ) except ContactAccessError as exc: raise ContactForbiddenError(str(exc)) from exc @@ -126,7 +126,7 @@ class ContactService: return contact try: return await self._repository.update( - contact, payload, role=context.role, user_id=context.user_id + contact, payload, role=context.role, user_id=context.user_id, ) except ContactAccessError as exc: raise ContactForbiddenError(str(exc)) from exc diff --git a/app/services/deal_service.py b/app/services/deal_service.py index cceb5a1..72bc87a 100644 --- a/app/services/deal_service.py +++ b/app/services/deal_service.py @@ -79,7 +79,7 @@ class DealService: await self._ensure_contact_in_organization(data.contact_id, context.organization_id) deal = await self._repository.create(data=data, role=context.role, user_id=context.user_id) await invalidate_analytics_cache( - self._cache, context.organization_id, self._cache_backoff_ms + self._cache, context.organization_id, self._cache_backoff_ms, ) return deal @@ -120,7 +120,7 @@ class DealService: return deal updated = await self._repository.update( - deal, changes, role=context.role, user_id=context.user_id + deal, changes, role=context.role, user_id=context.user_id, ) await self._log_activities( deal_id=deal.id, @@ -128,7 +128,7 @@ class DealService: activities=[activity for activity in [stage_activity, status_activity] if activity], ) await invalidate_analytics_cache( - self._cache, context.organization_id, self._cache_backoff_ms + self._cache, context.organization_id, self._cache_backoff_ms, ) return updated @@ -150,7 +150,7 @@ class DealService: return for activity_type, payload in entries: activity = Activity( - deal_id=deal_id, author_id=author_id, type=activity_type, payload=payload + deal_id=deal_id, author_id=author_id, type=activity_type, payload=payload, ) self._repository.session.add(activity) await self._repository.session.flush() @@ -160,7 +160,7 @@ class DealService: raise DealOrganizationMismatchError("Operation targets a different organization") async def _ensure_contact_in_organization( - self, contact_id: int, organization_id: int + self, contact_id: int, organization_id: int, ) -> Contact: contact = await self._repository.session.get(Contact, contact_id) if contact is None or contact.organization_id != organization_id: @@ -185,5 +185,5 @@ class DealService: effective_amount = updates.amount if updates.amount is not None else deal.amount if effective_amount is None or Decimal(effective_amount) <= Decimal("0"): raise DealStatusValidationError( - "Amount must be greater than zero to mark a deal as won" + "Amount must be greater than zero to mark a deal as won", ) diff --git a/app/services/organization_service.py b/app/services/organization_service.py index c2d4b83..85e840c 100644 --- a/app/services/organization_service.py +++ b/app/services/organization_service.py @@ -56,7 +56,7 @@ class OrganizationService: self._repository = repository async def get_context( - self, *, user_id: int, organization_id: int | None + self, *, user_id: int, organization_id: int | None, ) -> OrganizationContext: """Resolve request context ensuring the user belongs to the given organization.""" @@ -70,7 +70,7 @@ class OrganizationService: return OrganizationContext(organization=membership.organization, membership=membership) def ensure_entity_in_context( - self, *, entity_organization_id: int, context: OrganizationContext + self, *, entity_organization_id: int, context: OrganizationContext, ) -> None: """Make sure a resource belongs to the current organization.""" diff --git a/tests/api/v1/task_activity_shared.py b/tests/api/v1/task_activity_shared.py index 3ad1c78..c17b939 100644 --- a/tests/api/v1/task_activity_shared.py +++ b/tests/api/v1/task_activity_shared.py @@ -28,7 +28,7 @@ class Scenario: async def prepare_scenario(session_factory: async_sessionmaker[AsyncSession]) -> Scenario: async with session_factory() as session: user = User( - email="owner@example.com", hashed_password="hashed", name="Owner", is_active=True + email="owner@example.com", hashed_password="hashed", name="Owner", is_active=True, ) org = Organization(name="Acme LLC") session.add_all([user, org]) diff --git a/tests/api/v1/test_analytics.py b/tests/api/v1/test_analytics.py index eaac856..5857e0d 100644 --- a/tests/api/v1/test_analytics.py +++ b/tests/api/v1/test_analytics.py @@ -32,7 +32,7 @@ async def prepare_analytics_scenario( async with session_factory() as session: org = Organization(name="Analytics Org") user = User( - email="analytics@example.com", hashed_password="hashed", name="Analyst", is_active=True + email="analytics@example.com", hashed_password="hashed", name="Analyst", is_active=True, ) session.add_all([org, user]) await session.flush() diff --git a/tests/api/v1/test_organizations.py b/tests/api/v1/test_organizations.py index f01ab64..b059db4 100644 --- a/tests/api/v1/test_organizations.py +++ b/tests/api/v1/test_organizations.py @@ -61,7 +61,7 @@ async def test_list_user_organizations_returns_memberships( ) -> None: async with session_factory() as session: user = User( - email="owner@example.com", hashed_password="hashed", name="Owner", is_active=True + email="owner@example.com", hashed_password="hashed", name="Owner", is_active=True, ) session.add(user) await session.flush() @@ -115,10 +115,10 @@ async def test_owner_can_add_member_to_organization( ) -> None: async with session_factory() as session: owner = User( - email="owner-add@example.com", hashed_password="hashed", name="Owner", is_active=True + email="owner-add@example.com", hashed_password="hashed", name="Owner", is_active=True, ) invitee = User( - email="new-member@example.com", hashed_password="hashed", name="Member", is_active=True + email="new-member@example.com", hashed_password="hashed", name="Member", is_active=True, ) session.add_all([owner, invitee]) await session.flush() @@ -220,10 +220,10 @@ async def test_member_role_cannot_add_users( ) -> None: async with session_factory() as session: member_user = User( - email="member@example.com", hashed_password="hashed", name="Member", is_active=True + email="member@example.com", hashed_password="hashed", name="Member", is_active=True, ) invitee = User( - email="invitee@example.com", hashed_password="hashed", name="Invitee", is_active=True + email="invitee@example.com", hashed_password="hashed", name="Invitee", is_active=True, ) session.add_all([member_user, invitee]) await session.flush() @@ -266,10 +266,10 @@ async def test_cannot_add_duplicate_member( ) -> None: async with session_factory() as session: owner = User( - email="dup-owner@example.com", hashed_password="hashed", name="Owner", is_active=True + email="dup-owner@example.com", hashed_password="hashed", name="Owner", is_active=True, ) invitee = User( - email="dup-member@example.com", hashed_password="hashed", name="Invitee", is_active=True + email="dup-member@example.com", hashed_password="hashed", name="Invitee", is_active=True, ) session.add_all([owner, invitee]) await session.flush() diff --git a/tests/services/test_activity_service.py b/tests/services/test_activity_service.py index e80f5a9..32bbc3c 100644 --- a/tests/services/test_activity_service.py +++ b/tests/services/test_activity_service.py @@ -98,7 +98,7 @@ async def test_list_activities_returns_only_current_deal(session: AsyncSession) payload={"text": "hi"}, ), Activity( - deal_id=deal_id + 1, author_id=context.user_id, type=ActivityType.SYSTEM, payload={} + deal_id=deal_id + 1, author_id=context.user_id, type=ActivityType.SYSTEM, payload={}, ), ], ) @@ -120,7 +120,7 @@ async def test_add_comment_rejects_empty_text(session: AsyncSession) -> None: with pytest.raises(ActivityValidationError): await service.add_comment( - deal_id=deal_id, author_id=context.user_id, text=" ", context=context + deal_id=deal_id, author_id=context.user_id, text=" ", context=context, ) diff --git a/tests/services/test_analytics_service.py b/tests/services/test_analytics_service.py index 6de4a81..7c4a6f2 100644 --- a/tests/services/test_analytics_service.py +++ b/tests/services/test_analytics_service.py @@ -39,16 +39,16 @@ async def session() -> AsyncGenerator[AsyncSession, None]: async def _seed_data(session: AsyncSession) -> tuple[int, int, int]: org = Organization(name="Analytics Org") user = User( - email="analytics@example.com", hashed_password="hashed", name="Analyst", is_active=True + email="analytics@example.com", hashed_password="hashed", name="Analyst", is_active=True, ) session.add_all([org, user]) await session.flush() member = OrganizationMember( - organization_id=org.id, user_id=user.id, role=OrganizationRole.OWNER + organization_id=org.id, user_id=user.id, role=OrganizationRole.OWNER, ) contact = Contact( - organization_id=org.id, owner_id=user.id, name="Client", email="client@example.com" + organization_id=org.id, owner_id=user.id, name="Client", email="client@example.com", ) session.add_all([member, contact]) await session.flush() diff --git a/tests/services/test_auth_service.py b/tests/services/test_auth_service.py index f496451..a13fa7e 100644 --- a/tests/services/test_auth_service.py +++ b/tests/services/test_auth_service.py @@ -50,7 +50,7 @@ def jwt_service() -> JWTService: @pytest.mark.asyncio async def test_authenticate_success( - password_hasher: PasswordHasher, jwt_service: JWTService + password_hasher: PasswordHasher, jwt_service: JWTService, ) -> None: hashed = password_hasher.hash("StrongPass123") user = User(email="user@example.com", hashed_password=hashed, name="Alice", is_active=True) @@ -103,7 +103,7 @@ async def test_refresh_tokens_returns_new_pair( jwt_service: JWTService, ) -> None: user = User( - email="refresh@example.com", hashed_password="hashed", name="Refresh", is_active=True + email="refresh@example.com", hashed_password="hashed", name="Refresh", is_active=True, ) user.id = 7 service = AuthService(StubUserRepository(user), password_hasher, jwt_service) @@ -121,7 +121,7 @@ async def test_refresh_tokens_rejects_access_token( jwt_service: JWTService, ) -> None: user = User( - email="refresh@example.com", hashed_password="hashed", name="Refresh", is_active=True + email="refresh@example.com", hashed_password="hashed", name="Refresh", is_active=True, ) user.id = 9 service = AuthService(StubUserRepository(user), password_hasher, jwt_service) diff --git a/tests/services/test_deal_service.py b/tests/services/test_deal_service.py index 1bb7e5d..ac16a37 100644 --- a/tests/services/test_deal_service.py +++ b/tests/services/test_deal_service.py @@ -65,7 +65,7 @@ def _make_context(org: Organization, user: User, role: OrganizationRole) -> Orga async def _persist_base( - session: AsyncSession, *, role: OrganizationRole = OrganizationRole.MANAGER + session: AsyncSession, *, role: OrganizationRole = OrganizationRole.MANAGER, ) -> tuple[ OrganizationContext, Contact, diff --git a/tests/services/test_organization_service.py b/tests/services/test_organization_service.py index d5a4239..55f5ffd 100644 --- a/tests/services/test_organization_service.py +++ b/tests/services/test_organization_service.py @@ -28,7 +28,7 @@ class StubOrganizationRepository(OrganizationRepository): self._membership = membership async def get_membership( - self, organization_id: int, user_id: int + self, organization_id: int, user_id: int, ) -> OrganizationMember | None: # pragma: no cover - helper if ( self._membership @@ -40,7 +40,7 @@ class StubOrganizationRepository(OrganizationRepository): def make_membership( - role: OrganizationRole, *, organization_id: int = 1, user_id: int = 10 + role: OrganizationRole, *, organization_id: int = 1, user_id: int = 10, ) -> OrganizationMember: organization = Organization(name="Acme Inc") organization.id = organization_id @@ -75,7 +75,7 @@ class MembershipRepositoryStub(OrganizationRepository): """Repository stub that can emulate duplicate checks for add_member.""" def __init__( - self, memberships: dict[tuple[int, int], OrganizationMember] | None = None + self, memberships: dict[tuple[int, int], OrganizationMember] | None = None, ) -> None: self._session_stub = SessionStub() super().__init__(session=cast(AsyncSession, self._session_stub)) @@ -95,7 +95,7 @@ async def test_get_context_success() -> None: service = OrganizationService(StubOrganizationRepository(membership)) context = await service.get_context( - user_id=membership.user_id, organization_id=membership.organization_id + user_id=membership.user_id, organization_id=membership.organization_id, ) assert context.organization_id == membership.organization_id @@ -183,7 +183,7 @@ async def test_add_member_rejects_duplicate_membership() -> None: with pytest.raises(OrganizationMemberAlreadyExistsError): await service.add_member( - context=context, user_id=duplicate_user_id, role=OrganizationRole.MANAGER + context=context, user_id=duplicate_user_id, role=OrganizationRole.MANAGER, ) diff --git a/tests/services/test_task_service.py b/tests/services/test_task_service.py index c9b9078..58e67a5 100644 --- a/tests/services/test_task_service.py +++ b/tests/services/test_task_service.py @@ -190,7 +190,7 @@ async def test_member_cannot_update_foreign_task(session: AsyncSession) -> None: role=OrganizationRole.MEMBER, ) member_context = OrganizationContext( - organization=context_owner.organization, membership=membership + organization=context_owner.organization, membership=membership, ) with pytest.raises(TaskForbiddenError):