Loading src/portal_service/adapters/database/mappers.py +36 −1 Changes for src/portal_service/adapters/database/mappers.py: 36 added lines, 1 removed line. Original line number Diff line number Diff line from portal_service.adapters.database.sql import ApiClientRow, LoginAttemptRow, LoginSessionRow from portal_service.adapters.database.sql import ( ApiClientEntitlementRow, ApiClientRow, LoginAttemptRow, LoginSessionRow, ) from portal_service.domain.models import ( ApiClient, ApiClientEntitlement, ApprovalStatus, DecidedBy, DisabledBy, EntitlementState, LoginAttempt, LoginSession, OperationalState, Loading Loading @@ -98,3 +106,30 @@ class ApiClientMapper: secret_rotated_at=domain.secret_rotated_at, deleted_at=domain.deleted_at, ) class ApiClientEntitlementMapper: @staticmethod def to_domain(row: ApiClientEntitlementRow) -> ApiClientEntitlement: return ApiClientEntitlement( id=row.id, api_client_id=row.api_client_id, product=row.product, state=EntitlementState(row.state), decided_by=DecidedBy(row.decided_by), reason=row.reason, decided_at=row.decided_at, requested_at=row.requested_at, ) @staticmethod def to_row(domain: ApiClientEntitlement) -> ApiClientEntitlementRow: return ApiClientEntitlementRow( id=domain.id, api_client_id=domain.api_client_id, product=domain.product, state=domain.state, decided_by=domain.decided_by, reason=domain.reason, decided_at=domain.decided_at, ) src/portal_service/adapters/database/repos/__init__.py +4 −0 Changes for src/portal_service/adapters/database/repos/__init__.py: 4 added lines, 0 removed lines. Original line number Diff line number Diff line from portal_service.adapters.database.repos.api_client_entitlements import ( SqlApiClientEntitlementRepository, ) from portal_service.adapters.database.repos.api_clients import SqlApiClientRepository from portal_service.adapters.database.repos.login_attempts import SqlLoginAttemptRepository from portal_service.adapters.database.repos.login_sessions import SqlLoginSessionRepository __all__ = [ "SqlApiClientEntitlementRepository", "SqlApiClientRepository", "SqlLoginAttemptRepository", "SqlLoginSessionRepository", Loading src/portal_service/adapters/database/repos/api_client_entitlements.py 0 → 100644 +31 −0 Changes for src/portal_service/adapters/database/repos/api_client_entitlements.py: 31 added lines, 0 removed lines. Original line number Diff line number Diff line from uuid import UUID from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from portal_service.adapters.database.mappers import ApiClientEntitlementMapper from portal_service.adapters.database.sql import ApiClientEntitlementRow from portal_service.domain.models import ApiClientEntitlement from portal_service.domain.ports.database.api_client_entitlements import ( ApiClientEntitlementRepository, ) class SqlApiClientEntitlementRepository(ApiClientEntitlementRepository): def __init__(self, session: AsyncSession) -> None: self._session = session async def save(self, entitlement: ApiClientEntitlement) -> ApiClientEntitlement: merged = await self._session.merge(ApiClientEntitlementMapper.to_row(entitlement)) await self._session.flush() await self._session.refresh(merged) return ApiClientEntitlementMapper.to_domain(merged) async def list_by_client(self, api_client_id: UUID) -> list[ApiClientEntitlement]: stmt = ( select(ApiClientEntitlementRow) .where(ApiClientEntitlementRow.api_client_id == api_client_id) .order_by(ApiClientEntitlementRow.requested_at) ) rows = await self._session.scalars(stmt) return [ApiClientEntitlementMapper.to_domain(row) for row in rows] src/portal_service/adapters/database/sql.py +30 −1 Changes for src/portal_service/adapters/database/sql.py: 30 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -9,7 +9,7 @@ already exists. from datetime import datetime from uuid import UUID from sqlalchemy import DateTime, Index, LargeBinary, MetaData, String, func, text from sqlalchemy import DateTime, ForeignKey, Index, LargeBinary, MetaData, String, func, text from sqlalchemy.ext.asyncio import AsyncAttrs from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column Loading Loading @@ -107,3 +107,32 @@ class ApiClientRow(Base): updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), onupdate=func.now() ) class ApiClientEntitlementRow(Base): """ One API product for one API client.d """ __tablename__ = "api_client_entitlements" __table_args__ = ( Index("idx_api_client_entitlements_api_client_id", "api_client_id"), Index( "uq_api_client_entitlements_active_product", "api_client_id", "product", unique=True, postgresql_where=text("state IN ('pending', 'approved')"), ), ) id: Mapped[UUID] = mapped_column(primary_key=True) api_client_id: Mapped[UUID] = mapped_column(ForeignKey("api_clients.id")) product: Mapped[str] = mapped_column(String(64)) state: Mapped[str] = mapped_column(String(32)) decided_by: Mapped[str] = mapped_column(String(32)) reason: Mapped[str | None] = mapped_column(String(255)) decided_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) requested_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now() ) src/portal_service/domain/models.py +27 −0 Changes for src/portal_service/domain/models.py: 27 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -146,3 +146,30 @@ class ApiClient(BaseModel): deleted_at: datetime | None = None created_at: datetime | None = None updated_at: datetime | None = None class EntitlementState(StrEnum): PENDING = "pending" APPROVED = "approved" REJECTED = "rejected" REVOKED = "revoked" class DecidedBy(StrEnum): AUTO = "auto" CREATOR = "creator" OPERATOR = "operator" SYSTEM = "system" class ApiClientEntitlement(BaseModel): """Whether an API client may use one API product, and who decided it.""" id: UUID api_client_id: UUID product: str state: EntitlementState decided_by: DecidedBy reason: str | None = None decided_at: datetime | None = None requested_at: datetime | None = None Loading
src/portal_service/adapters/database/mappers.py +36 −1 Changes for src/portal_service/adapters/database/mappers.py: 36 added lines, 1 removed line. Original line number Diff line number Diff line from portal_service.adapters.database.sql import ApiClientRow, LoginAttemptRow, LoginSessionRow from portal_service.adapters.database.sql import ( ApiClientEntitlementRow, ApiClientRow, LoginAttemptRow, LoginSessionRow, ) from portal_service.domain.models import ( ApiClient, ApiClientEntitlement, ApprovalStatus, DecidedBy, DisabledBy, EntitlementState, LoginAttempt, LoginSession, OperationalState, Loading Loading @@ -98,3 +106,30 @@ class ApiClientMapper: secret_rotated_at=domain.secret_rotated_at, deleted_at=domain.deleted_at, ) class ApiClientEntitlementMapper: @staticmethod def to_domain(row: ApiClientEntitlementRow) -> ApiClientEntitlement: return ApiClientEntitlement( id=row.id, api_client_id=row.api_client_id, product=row.product, state=EntitlementState(row.state), decided_by=DecidedBy(row.decided_by), reason=row.reason, decided_at=row.decided_at, requested_at=row.requested_at, ) @staticmethod def to_row(domain: ApiClientEntitlement) -> ApiClientEntitlementRow: return ApiClientEntitlementRow( id=domain.id, api_client_id=domain.api_client_id, product=domain.product, state=domain.state, decided_by=domain.decided_by, reason=domain.reason, decided_at=domain.decided_at, )
src/portal_service/adapters/database/repos/__init__.py +4 −0 Changes for src/portal_service/adapters/database/repos/__init__.py: 4 added lines, 0 removed lines. Original line number Diff line number Diff line from portal_service.adapters.database.repos.api_client_entitlements import ( SqlApiClientEntitlementRepository, ) from portal_service.adapters.database.repos.api_clients import SqlApiClientRepository from portal_service.adapters.database.repos.login_attempts import SqlLoginAttemptRepository from portal_service.adapters.database.repos.login_sessions import SqlLoginSessionRepository __all__ = [ "SqlApiClientEntitlementRepository", "SqlApiClientRepository", "SqlLoginAttemptRepository", "SqlLoginSessionRepository", Loading
src/portal_service/adapters/database/repos/api_client_entitlements.py 0 → 100644 +31 −0 Changes for src/portal_service/adapters/database/repos/api_client_entitlements.py: 31 added lines, 0 removed lines. Original line number Diff line number Diff line from uuid import UUID from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from portal_service.adapters.database.mappers import ApiClientEntitlementMapper from portal_service.adapters.database.sql import ApiClientEntitlementRow from portal_service.domain.models import ApiClientEntitlement from portal_service.domain.ports.database.api_client_entitlements import ( ApiClientEntitlementRepository, ) class SqlApiClientEntitlementRepository(ApiClientEntitlementRepository): def __init__(self, session: AsyncSession) -> None: self._session = session async def save(self, entitlement: ApiClientEntitlement) -> ApiClientEntitlement: merged = await self._session.merge(ApiClientEntitlementMapper.to_row(entitlement)) await self._session.flush() await self._session.refresh(merged) return ApiClientEntitlementMapper.to_domain(merged) async def list_by_client(self, api_client_id: UUID) -> list[ApiClientEntitlement]: stmt = ( select(ApiClientEntitlementRow) .where(ApiClientEntitlementRow.api_client_id == api_client_id) .order_by(ApiClientEntitlementRow.requested_at) ) rows = await self._session.scalars(stmt) return [ApiClientEntitlementMapper.to_domain(row) for row in rows]
src/portal_service/adapters/database/sql.py +30 −1 Changes for src/portal_service/adapters/database/sql.py: 30 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -9,7 +9,7 @@ already exists. from datetime import datetime from uuid import UUID from sqlalchemy import DateTime, Index, LargeBinary, MetaData, String, func, text from sqlalchemy import DateTime, ForeignKey, Index, LargeBinary, MetaData, String, func, text from sqlalchemy.ext.asyncio import AsyncAttrs from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column Loading Loading @@ -107,3 +107,32 @@ class ApiClientRow(Base): updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), onupdate=func.now() ) class ApiClientEntitlementRow(Base): """ One API product for one API client.d """ __tablename__ = "api_client_entitlements" __table_args__ = ( Index("idx_api_client_entitlements_api_client_id", "api_client_id"), Index( "uq_api_client_entitlements_active_product", "api_client_id", "product", unique=True, postgresql_where=text("state IN ('pending', 'approved')"), ), ) id: Mapped[UUID] = mapped_column(primary_key=True) api_client_id: Mapped[UUID] = mapped_column(ForeignKey("api_clients.id")) product: Mapped[str] = mapped_column(String(64)) state: Mapped[str] = mapped_column(String(32)) decided_by: Mapped[str] = mapped_column(String(32)) reason: Mapped[str | None] = mapped_column(String(255)) decided_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) requested_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now() )
src/portal_service/domain/models.py +27 −0 Changes for src/portal_service/domain/models.py: 27 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -146,3 +146,30 @@ class ApiClient(BaseModel): deleted_at: datetime | None = None created_at: datetime | None = None updated_at: datetime | None = None class EntitlementState(StrEnum): PENDING = "pending" APPROVED = "approved" REJECTED = "rejected" REVOKED = "revoked" class DecidedBy(StrEnum): AUTO = "auto" CREATOR = "creator" OPERATOR = "operator" SYSTEM = "system" class ApiClientEntitlement(BaseModel): """Whether an API client may use one API product, and who decided it.""" id: UUID api_client_id: UUID product: str state: EntitlementState decided_by: DecidedBy reason: str | None = None decided_at: datetime | None = None requested_at: datetime | None = None