Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 93 additions & 13 deletions app/modules/proxy/sticky_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@
from dataclasses import dataclass
from datetime import datetime, timedelta

from sqlalchemy import and_, delete, or_, select
from sqlalchemy import and_, delete, func, or_, select
from sqlalchemy.dialects.postgresql import insert as pg_insert
from sqlalchemy.dialects.sqlite import insert as sqlite_insert
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.sql import Insert, func
from sqlalchemy.sql import Insert

from app.core.utils.time import to_utc_naive, utcnow
from app.db.models import Account, StickySession, StickySessionKind
from app.modules.sticky_sessions.schemas import StickySessionSortBy, StickySessionSortDir


@dataclass(frozen=True, slots=True)
Expand Down Expand Up @@ -74,31 +75,35 @@ async def delete(self, key: str, *, kind: StickySessionKind) -> bool:
await self._session.commit()
return result.scalar_one_or_none() is not None

async def delete_entries(self, entries: Sequence[tuple[str, StickySessionKind]]) -> int:
async def delete_entries(
self,
entries: Sequence[tuple[str, StickySessionKind]],
) -> list[tuple[str, StickySessionKind]]:
targets = {(key, kind) for key, kind in entries if key}
if not targets:
return 0
return []
statement = delete(StickySession).where(
or_(*(and_(StickySession.key == key, StickySession.kind == kind) for key, kind in targets))
)
result = await self._session.execute(statement.returning(StickySession.key))
deleted = len(result.scalars().all())
result = await self._session.execute(statement.returning(StickySession.key, StickySession.kind))
await self._session.commit()
return deleted
return [(key, kind) for key, kind in result.all()]

async def list_entries(
async def list_entry_identifiers(
self,
*,
kind: StickySessionKind | None = None,
updated_before: datetime | None = None,
offset: int = 0,
limit: int | None = None,
) -> Sequence[StickySessionListEntryRecord]:
account_query: str | None = None,
key_query: str | None = None,
) -> list[tuple[str, StickySessionKind]]:
statement = (
self._apply_filters(
select(StickySession, Account.email),
select(StickySession.key, StickySession.kind),
kind=kind,
updated_before=updated_before,
account_query=account_query,
key_query=key_query,
)
.join(Account, Account.id == StickySession.account_id)
.order_by(
Expand All @@ -107,6 +112,33 @@ async def list_entries(
StickySession.key.asc(),
)
)
result = await self._session.execute(statement)
return [(key, kind) for key, kind in result.all()]

async def list_entries(
self,
*,
kind: StickySessionKind | None = None,
updated_before: datetime | None = None,
account_query: str | None = None,
key_query: str | None = None,
sort_by: StickySessionSortBy = "updated_at",
sort_dir: StickySessionSortDir = "desc",
offset: int = 0,
limit: int | None = None,
) -> Sequence[StickySessionListEntryRecord]:
order_by = self._build_order_by(sort_by=sort_by, sort_dir=sort_dir)
statement = (
self._apply_filters(
select(StickySession, Account.email),
kind=kind,
updated_before=updated_before,
account_query=account_query,
key_query=key_query,
)
.join(Account, Account.id == StickySession.account_id)
.order_by(*order_by)
)
if offset > 0:
statement = statement.offset(offset)
if limit is not None:
Expand All @@ -122,11 +154,15 @@ async def count_entries(
*,
kind: StickySessionKind | None = None,
updated_before: datetime | None = None,
account_query: str | None = None,
key_query: str | None = None,
) -> int:
statement = self._apply_filters(
select(func.count()).select_from(StickySession),
select(func.count()).select_from(StickySession).join(Account, Account.id == StickySession.account_id),
kind=kind,
updated_before=updated_before,
account_query=account_query,
key_query=key_query,
)
result = await self._session.execute(statement)
return int(result.scalar_one())
Expand Down Expand Up @@ -166,9 +202,53 @@ def _apply_filters(
*,
kind: StickySessionKind | None,
updated_before: datetime | None,
account_query: str | None,
key_query: str | None,
):
if kind is not None:
statement = statement.where(StickySession.kind == kind)
if updated_before is not None:
statement = statement.where(StickySession.updated_at < to_utc_naive(updated_before))
if account_query:
statement = statement.where(func.lower(Account.email).contains(account_query.lower()))
if key_query:
statement = statement.where(func.lower(StickySession.key).contains(key_query.lower()))
return statement

@staticmethod
def _build_order_by(
*,
sort_by: StickySessionSortBy,
sort_dir: StickySessionSortDir,
):
sort_column_map = {
"updated_at": StickySession.updated_at,
"created_at": StickySession.created_at,
"account": Account.email,
"key": StickySession.key,
}
primary = sort_column_map[sort_by]
primary_order = primary.asc() if sort_dir == "asc" else primary.desc()
if sort_by == "updated_at":
return (
primary_order,
StickySession.created_at.desc(),
StickySession.key.asc(),
)
if sort_by == "created_at":
return (
primary_order,
StickySession.updated_at.desc(),
StickySession.key.asc(),
)
if sort_by == "account":
return (
primary_order,
StickySession.updated_at.desc(),
StickySession.key.asc(),
)
return (
primary_order,
StickySession.updated_at.desc(),
StickySession.created_at.desc(),
)
44 changes: 41 additions & 3 deletions app/modules/sticky_sessions/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,17 @@
from app.db.models import StickySessionKind
from app.dependencies import StickySessionsContext, get_sticky_sessions_context
from app.modules.sticky_sessions.schemas import (
StickySessionDeleteFailure,
StickySessionDeleteResponse,
StickySessionEntryResponse,
StickySessionIdentifier,
StickySessionsDeleteFilteredRequest,
StickySessionsDeleteFilteredResponse,
StickySessionsDeleteRequest,
StickySessionsDeleteResponse,
StickySessionsListResponse,
StickySessionSortBy,
StickySessionSortDir,
StickySessionsPurgeRequest,
StickySessionsPurgeResponse,
)
Expand All @@ -27,11 +33,24 @@
async def list_sticky_sessions(
kind: StickySessionKind | None = Query(default=None),
stale_only: bool = Query(default=False, alias="staleOnly"),
account_query: str | None = Query(default=None, alias="accountQuery"),
key_query: str | None = Query(default=None, alias="keyQuery"),
sort_by: StickySessionSortBy = Query(default="updated_at", alias="sortBy"),
sort_dir: StickySessionSortDir = Query(default="desc", alias="sortDir"),
offset: int = Query(default=0, ge=0),
limit: int = Query(default=100, ge=1, le=500),
context: StickySessionsContext = Depends(get_sticky_sessions_context),
) -> StickySessionsListResponse:
result = await context.service.list_entries(kind=kind, stale_only=stale_only, offset=offset, limit=limit)
result = await context.service.list_entries(
kind=kind,
stale_only=stale_only,
account_query=account_query,
key_query=key_query,
sort_by=sort_by,
sort_dir=sort_dir,
offset=offset,
limit=limit,
)
return StickySessionsListResponse(
entries=[
StickySessionEntryResponse(
Expand Down Expand Up @@ -65,8 +84,27 @@ async def delete_sticky_sessions(
payload: StickySessionsDeleteRequest,
context: StickySessionsContext = Depends(get_sticky_sessions_context),
) -> StickySessionsDeleteResponse:
deleted_count = await context.service.delete_entries([(entry.key, entry.kind) for entry in payload.sessions])
return StickySessionsDeleteResponse(deleted_count=deleted_count)
result = await context.service.delete_entries([(entry.key, entry.kind) for entry in payload.sessions])
return StickySessionsDeleteResponse(
deleted_count=result.deleted_count,
deleted=[StickySessionIdentifier(key=key, kind=kind) for key, kind in result.deleted],
failed=[
StickySessionDeleteFailure(key=entry.key, kind=entry.kind, reason=entry.reason) for entry in result.failed
],
)


@router.post("/delete-filtered", response_model=StickySessionsDeleteFilteredResponse)
async def delete_filtered_sticky_sessions(
payload: StickySessionsDeleteFilteredRequest,
context: StickySessionsContext = Depends(get_sticky_sessions_context),
) -> StickySessionsDeleteFilteredResponse:
deleted_count = await context.service.delete_filtered_entries(
stale_only=payload.stale_only,
account_query=payload.account_query,
key_query=payload.key_query,
)
return StickySessionsDeleteFilteredResponse(deleted_count=deleted_count)


@router.delete("/{kind}/{key:path}", response_model=StickySessionDeleteResponse)
Expand Down
33 changes: 32 additions & 1 deletion app/modules/sticky_sessions/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
from datetime import datetime
from typing import Literal

from pydantic import Field
from pydantic import Field, model_validator

from app.db.models import StickySessionKind
from app.modules.shared.schemas import DashboardModel

StickySessionSortBy = Literal["updated_at", "created_at", "account", "key"]
StickySessionSortDir = Literal["asc", "desc"]


class StickySessionEntryResponse(DashboardModel):
key: str
Expand Down Expand Up @@ -38,9 +41,37 @@ class StickySessionDeleteResponse(DashboardModel):
class StickySessionsDeleteRequest(DashboardModel):
sessions: list[StickySessionIdentifier] = Field(min_length=1, max_length=500)

@model_validator(mode="after")
def validate_unique_sessions(self) -> "StickySessionsDeleteRequest":
seen: set[tuple[str, StickySessionKind]] = set()
for session in self.sessions:
target = (session.key, session.kind)
if target in seen:
raise ValueError("duplicate sticky session targets are not allowed")
seen.add(target)
return self


class StickySessionDeleteFailure(DashboardModel):
key: str
kind: StickySessionKind
reason: str


class StickySessionsDeleteResponse(DashboardModel):
deleted_count: int
deleted: list[StickySessionIdentifier] = Field(default_factory=list)
failed: list[StickySessionDeleteFailure] = Field(default_factory=list)


class StickySessionsDeleteFilteredRequest(DashboardModel):
stale_only: bool = False
account_query: str = ""
key_query: str = ""


class StickySessionsDeleteFilteredResponse(DashboardModel):
deleted_count: int


class StickySessionsPurgeRequest(DashboardModel):
Expand Down
Loading
Loading