TorbatYar/backend/services/experience/app/specifications/publishing.py
Mortezakoohjani 203671a7bf feat(experience): ship Experience Platform phases 11.0-11.10
Add the experience service with sites through analytics/AI hooks, migrations through 0011, and phase docs/manifests marking the track complete.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 11:43:10 +03:30

271 lines
9.9 KiB
Python

"""Publishing / SEO / PWA list specifications — Phase 11.7."""
from __future__ import annotations
from dataclasses import dataclass
from uuid import UUID
from sqlalchemy import Select, asc, desc, or_
from sqlalchemy.sql import ColumnElement
from app.models.publishing import (
DomainEdgeBinding,
PublishRelease,
PwaManifest,
RobotsShell,
SeoMetadata,
SitemapShell,
)
from app.models.types import (
DomainEdgeStatus,
PublishReleaseStatus,
PublishTarget,
PwaStatus,
RobotsStatus,
SeoStatus,
SeoTarget,
SitemapStatus,
)
PUBLISH_SORT = frozenset({"created_at", "updated_at", "completed_at", "status"})
EDGE_SORT = frozenset({"created_at", "updated_at", "host", "status"})
SEO_SORT = frozenset({"created_at", "updated_at", "title", "status", "target_type"})
SITEMAP_SORT = frozenset({"created_at", "updated_at", "status"})
ROBOTS_SORT = frozenset({"created_at", "updated_at", "status"})
PWA_SORT = frozenset({"created_at", "updated_at", "name", "status"})
def _enum_clause(column, value, enum_cls):
if value is None:
return None
parsed = value if isinstance(value, enum_cls) else enum_cls(str(value))
return column == parsed
@dataclass(frozen=True, slots=True)
class PublishReleaseListSpec:
workspace_id: UUID | None = None
target_type: PublishTarget | str | None = None
target_id: UUID | None = None
status: PublishReleaseStatus | str | None = None
q: str | None = None
sort_by: str = "created_at"
sort_dir: str = "desc"
def filter_clauses(self) -> list[ColumnElement]:
clauses: list[ColumnElement] = []
if self.workspace_id is not None:
clauses.append(PublishRelease.workspace_id == self.workspace_id)
c = _enum_clause(PublishRelease.target_type, self.target_type, PublishTarget)
if c is not None:
clauses.append(c)
if self.target_id is not None:
clauses.append(PublishRelease.target_id == self.target_id)
c = _enum_clause(PublishRelease.status, self.status, PublishReleaseStatus)
if c is not None:
clauses.append(c)
if self.q:
term = f"%{self.q.strip()}%"
clauses.append(
or_(PublishRelease.notes.ilike(term), PublishRelease.error_message.ilike(term))
)
return clauses
def apply(self, stmt: Select) -> Select:
clauses = self.filter_clauses()
if clauses:
stmt = stmt.where(*clauses)
sort_key = self.sort_by if self.sort_by in PUBLISH_SORT else "created_at"
column = getattr(PublishRelease, sort_key)
order = desc(column) if self.sort_dir.lower() != "asc" else asc(column)
return stmt.order_by(order)
@dataclass(frozen=True, slots=True)
class DomainEdgeBindingListSpec:
workspace_id: UUID | None = None
site_id: UUID | None = None
site_domain_id: UUID | None = None
status: DomainEdgeStatus | str | None = None
q: str | None = None
sort_by: str = "created_at"
sort_dir: str = "desc"
def filter_clauses(self) -> list[ColumnElement]:
clauses: list[ColumnElement] = []
if self.workspace_id is not None:
clauses.append(DomainEdgeBinding.workspace_id == self.workspace_id)
if self.site_id is not None:
clauses.append(DomainEdgeBinding.site_id == self.site_id)
if self.site_domain_id is not None:
clauses.append(DomainEdgeBinding.site_domain_id == self.site_domain_id)
c = _enum_clause(DomainEdgeBinding.status, self.status, DomainEdgeStatus)
if c is not None:
clauses.append(c)
if self.q:
term = f"%{self.q.strip()}%"
clauses.append(
or_(
DomainEdgeBinding.host.ilike(term),
DomainEdgeBinding.dns_status_ref.ilike(term),
DomainEdgeBinding.ssl_cert_ref.ilike(term),
)
)
return clauses
def apply(self, stmt: Select) -> Select:
clauses = self.filter_clauses()
if clauses:
stmt = stmt.where(*clauses)
sort_key = self.sort_by if self.sort_by in EDGE_SORT else "created_at"
column = getattr(DomainEdgeBinding, sort_key)
order = desc(column) if self.sort_dir.lower() != "asc" else asc(column)
return stmt.order_by(order)
@dataclass(frozen=True, slots=True)
class SeoMetadataListSpec:
workspace_id: UUID | None = None
target_type: SeoTarget | str | None = None
target_id: UUID | None = None
status: SeoStatus | str | None = None
q: str | None = None
sort_by: str = "created_at"
sort_dir: str = "desc"
def filter_clauses(self) -> list[ColumnElement]:
clauses: list[ColumnElement] = []
if self.workspace_id is not None:
clauses.append(SeoMetadata.workspace_id == self.workspace_id)
c = _enum_clause(SeoMetadata.target_type, self.target_type, SeoTarget)
if c is not None:
clauses.append(c)
if self.target_id is not None:
clauses.append(SeoMetadata.target_id == self.target_id)
c = _enum_clause(SeoMetadata.status, self.status, SeoStatus)
if c is not None:
clauses.append(c)
if self.q:
term = f"%{self.q.strip()}%"
clauses.append(
or_(
SeoMetadata.title.ilike(term),
SeoMetadata.description.ilike(term),
SeoMetadata.canonical_url.ilike(term),
)
)
return clauses
def apply(self, stmt: Select) -> Select:
clauses = self.filter_clauses()
if clauses:
stmt = stmt.where(*clauses)
sort_key = self.sort_by if self.sort_by in SEO_SORT else "created_at"
column = getattr(SeoMetadata, sort_key)
order = desc(column) if self.sort_dir.lower() != "asc" else asc(column)
return stmt.order_by(order)
@dataclass(frozen=True, slots=True)
class SitemapShellListSpec:
workspace_id: UUID | None = None
site_id: UUID | None = None
status: SitemapStatus | str | None = None
q: str | None = None
sort_by: str = "created_at"
sort_dir: str = "desc"
def filter_clauses(self) -> list[ColumnElement]:
clauses: list[ColumnElement] = []
if self.workspace_id is not None:
clauses.append(SitemapShell.workspace_id == self.workspace_id)
if self.site_id is not None:
clauses.append(SitemapShell.site_id == self.site_id)
c = _enum_clause(SitemapShell.status, self.status, SitemapStatus)
if c is not None:
clauses.append(c)
if self.q:
term = f"%{self.q.strip()}%"
clauses.append(SitemapShell.storage_file_ref.ilike(term))
return clauses
def apply(self, stmt: Select) -> Select:
clauses = self.filter_clauses()
if clauses:
stmt = stmt.where(*clauses)
sort_key = self.sort_by if self.sort_by in SITEMAP_SORT else "created_at"
column = getattr(SitemapShell, sort_key)
order = desc(column) if self.sort_dir.lower() != "asc" else asc(column)
return stmt.order_by(order)
@dataclass(frozen=True, slots=True)
class RobotsShellListSpec:
workspace_id: UUID | None = None
site_id: UUID | None = None
status: RobotsStatus | str | None = None
q: str | None = None
sort_by: str = "created_at"
sort_dir: str = "desc"
def filter_clauses(self) -> list[ColumnElement]:
clauses: list[ColumnElement] = []
if self.workspace_id is not None:
clauses.append(RobotsShell.workspace_id == self.workspace_id)
if self.site_id is not None:
clauses.append(RobotsShell.site_id == self.site_id)
c = _enum_clause(RobotsShell.status, self.status, RobotsStatus)
if c is not None:
clauses.append(c)
if self.q:
term = f"%{self.q.strip()}%"
clauses.append(RobotsShell.content_text.ilike(term))
return clauses
def apply(self, stmt: Select) -> Select:
clauses = self.filter_clauses()
if clauses:
stmt = stmt.where(*clauses)
sort_key = self.sort_by if self.sort_by in ROBOTS_SORT else "created_at"
column = getattr(RobotsShell, sort_key)
order = desc(column) if self.sort_dir.lower() != "asc" else asc(column)
return stmt.order_by(order)
@dataclass(frozen=True, slots=True)
class PwaManifestListSpec:
workspace_id: UUID | None = None
site_id: UUID | None = None
status: PwaStatus | str | None = None
q: str | None = None
sort_by: str = "created_at"
sort_dir: str = "desc"
def filter_clauses(self) -> list[ColumnElement]:
clauses: list[ColumnElement] = []
if self.workspace_id is not None:
clauses.append(PwaManifest.workspace_id == self.workspace_id)
if self.site_id is not None:
clauses.append(PwaManifest.site_id == self.site_id)
c = _enum_clause(PwaManifest.status, self.status, PwaStatus)
if c is not None:
clauses.append(c)
if self.q:
term = f"%{self.q.strip()}%"
clauses.append(
or_(
PwaManifest.name.ilike(term),
PwaManifest.short_name.ilike(term),
PwaManifest.start_url.ilike(term),
)
)
return clauses
def apply(self, stmt: Select) -> Select:
clauses = self.filter_clauses()
if clauses:
stmt = stmt.where(*clauses)
sort_key = self.sort_by if self.sort_by in PWA_SORT else "created_at"
column = getattr(PwaManifest, sort_key)
order = desc(column) if self.sort_dir.lower() != "asc" else asc(column)
return stmt.order_by(order)