MediaRouter / app /social /models.py
basyx's picture
Upload 340 files
3493993 verified
Raw
History Blame Contribute Delete
24.9 kB
from __future__ import annotations
from datetime import datetime, timezone
from uuid import uuid4
from sqlalchemy import (
JSON,
BigInteger,
DateTime,
ForeignKey,
Index,
Integer,
String,
Text,
UniqueConstraint,
)
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
def utcnow() -> datetime:
return datetime.now(timezone.utc)
def new_id() -> str:
return str(uuid4())
class SocialBase(DeclarativeBase):
"""Separate metadata keeps production schema changes migration-only."""
class SocialAccount(SocialBase):
__tablename__ = "social_accounts"
__table_args__ = (
UniqueConstraint(
"workspace_id",
"provider",
"external_account_id",
name="uq_social_account_workspace_provider_external",
),
Index("ix_social_accounts_workspace_id", "workspace_id"),
Index("ix_social_accounts_provider_status", "provider", "status"),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id)
workspace_id: Mapped[str] = mapped_column(String(120), nullable=False)
provider: Mapped[str] = mapped_column(String(32), nullable=False)
account_type: Mapped[str] = mapped_column(String(64), nullable=False)
external_account_id: Mapped[str] = mapped_column(String(255), nullable=False)
username: Mapped[str | None] = mapped_column(String(255))
display_name: Mapped[str | None] = mapped_column(String(255))
avatar_url: Mapped[str | None] = mapped_column(String(2048))
status: Mapped[str] = mapped_column(String(32), nullable=False, default="pending")
metadata_json: Mapped[dict[str, object]] = mapped_column(
"metadata", JSON, nullable=False, default=dict
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=utcnow
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=utcnow, onupdate=utcnow
)
last_synced_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
class SocialAccountToken(SocialBase):
__tablename__ = "social_account_tokens"
__table_args__ = (
UniqueConstraint("social_account_id", name="uq_social_account_token"),
Index("ix_social_account_tokens_expires_at", "expires_at"),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id)
social_account_id: Mapped[str] = mapped_column(
String(36), ForeignKey("social_accounts.id", ondelete="CASCADE"), nullable=False
)
access_token_secret_id: Mapped[str | None] = mapped_column(String(255))
refresh_token_secret_id: Mapped[str | None] = mapped_column(String(255))
encrypted_payload: Mapped[str | None] = mapped_column(Text)
expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
scopes: Mapped[list[str]] = mapped_column(JSON, nullable=False, default=list)
token_type: Mapped[str | None] = mapped_column(String(64))
last_refreshed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=utcnow
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=utcnow, onupdate=utcnow
)
class SocialAccountCapability(SocialBase):
__tablename__ = "social_account_capabilities"
__table_args__ = (
UniqueConstraint("social_account_id", "capability", name="uq_social_account_capability"),
Index("ix_social_account_capabilities_account", "social_account_id"),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id)
social_account_id: Mapped[str] = mapped_column(
String(36), ForeignKey("social_accounts.id", ondelete="CASCADE"), nullable=False
)
capability: Mapped[str] = mapped_column(String(100), nullable=False)
enabled: Mapped[bool] = mapped_column(nullable=False, default=False)
metadata_json: Mapped[dict[str, object]] = mapped_column(
"metadata", JSON, nullable=False, default=dict
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=utcnow, onupdate=utcnow
)
class MediaVariant(SocialBase):
__tablename__ = "media_variants"
__table_args__ = (
Index("ix_media_variants_workspace_source", "workspace_id", "source_asset_id"),
Index("ix_media_variants_platform", "platform"),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id)
workspace_id: Mapped[str] = mapped_column(String(120), nullable=False)
source_asset_id: Mapped[str] = mapped_column(String(255), nullable=False)
asset_reference: Mapped[str | None] = mapped_column(String(2048))
template_id: Mapped[str | None] = mapped_column(String(255))
platform: Mapped[str | None] = mapped_column(String(32))
width: Mapped[int | None] = mapped_column(Integer)
height: Mapped[int | None] = mapped_column(Integer)
duration_seconds: Mapped[float | None] = mapped_column()
codec: Mapped[str | None] = mapped_column(String(64))
container: Mapped[str | None] = mapped_column(String(64))
bitrate: Mapped[int | None] = mapped_column(BigInteger)
file_size: Mapped[int | None] = mapped_column(BigInteger)
metadata_json: Mapped[dict[str, object]] = mapped_column(
"metadata", JSON, nullable=False, default=dict
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=utcnow
)
class SocialMediaAsset(SocialBase):
"""A workspace-owned reference to an output published by MediaRouter.
The media processor remains the source of truth for files. This table
only records a tenant binding and immutable download locator so social
workers can validate and stream the exact output without trusting a
caller-supplied filesystem path or URL.
"""
__tablename__ = "social_media_assets"
__table_args__ = (
UniqueConstraint(
"workspace_id", "request_id", "filename", name="uq_social_media_asset_workspace_output"
),
Index("ix_social_media_assets_workspace_id", "workspace_id"),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id)
workspace_id: Mapped[str] = mapped_column(String(120), nullable=False)
# Canonical ownership lives in the security/asset domain. This local
# social binding is cacheable publishing metadata, not ownership proof.
canonical_asset_id: Mapped[str | None] = mapped_column(String(36), nullable=True)
request_id: Mapped[str] = mapped_column(String(36), nullable=False)
filename: Mapped[str] = mapped_column(String(255), nullable=False)
mime_type: Mapped[str] = mapped_column(String(255), nullable=False)
file_size: Mapped[int] = mapped_column(BigInteger, nullable=False)
metadata_json: Mapped[dict[str, object]] = mapped_column(
"metadata", JSON, nullable=False, default=dict
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=utcnow
)
class SocialCampaign(SocialBase):
__tablename__ = "social_campaigns"
__table_args__ = (Index("ix_social_campaigns_workspace_id", "workspace_id"),)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id)
workspace_id: Mapped[str] = mapped_column(String(120), nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
description: Mapped[str | None] = mapped_column(Text)
status: Mapped[str] = mapped_column(String(32), nullable=False, default="draft")
metadata_json: Mapped[dict[str, object]] = mapped_column(
"metadata", JSON, nullable=False, default=dict
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=utcnow
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=utcnow, onupdate=utcnow
)
class SocialPost(SocialBase):
__tablename__ = "social_posts"
__table_args__ = (
UniqueConstraint(
"workspace_id", "idempotency_key", name="uq_social_posts_workspace_idempotency"
),
Index("ix_social_posts_workspace_created", "workspace_id", "created_at"),
Index("ix_social_posts_status", "status"),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id)
workspace_id: Mapped[str] = mapped_column(String(120), nullable=False)
project_id: Mapped[str | None] = mapped_column(String(36), nullable=True)
campaign_id: Mapped[str | None] = mapped_column(
String(36), ForeignKey("social_campaigns.id", ondelete="SET NULL")
)
media_asset_id: Mapped[str | None] = mapped_column(String(255), nullable=True)
canonical_caption: Mapped[str | None] = mapped_column(Text)
canonical_hashtags: Mapped[list[str]] = mapped_column(JSON, nullable=False, default=list)
brand_kit_version_id: Mapped[str | None] = mapped_column(String(36), nullable=True)
source_variant_id: Mapped[str | None] = mapped_column(
String(36), ForeignKey("media_variants.id", ondelete="SET NULL")
)
status: Mapped[str] = mapped_column(String(32), nullable=False, default="draft")
publish_mode: Mapped[str] = mapped_column(String(32), nullable=False, default="draft")
idempotency_key: Mapped[str | None] = mapped_column(String(255))
request_fingerprint: Mapped[str | None] = mapped_column(String(64))
metadata_json: Mapped[dict[str, object]] = mapped_column(
"metadata", JSON, nullable=False, default=dict
)
created_by: Mapped[str | None] = mapped_column(String(120))
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=utcnow
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=utcnow, onupdate=utcnow
)
published_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
revision: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
class SocialPostTarget(SocialBase):
__tablename__ = "social_post_targets"
__table_args__ = (
UniqueConstraint("social_post_id", "social_account_id", name="uq_social_post_target"),
Index("ix_social_post_targets_post", "social_post_id"),
Index("ix_social_post_targets_account", "social_account_id"),
Index("ix_social_post_targets_status", "status"),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id)
social_post_id: Mapped[str] = mapped_column(
String(36), ForeignKey("social_posts.id", ondelete="CASCADE"), nullable=False
)
social_account_id: Mapped[str] = mapped_column(
String(36), ForeignKey("social_accounts.id", ondelete="RESTRICT"), nullable=False
)
provider: Mapped[str] = mapped_column(String(32), nullable=False)
status: Mapped[str] = mapped_column(String(32), nullable=False, default="draft")
caption_json: Mapped[dict[str, object]] = mapped_column(
"caption", JSON, nullable=False, default=dict
)
platform_metadata: Mapped[dict[str, object]] = mapped_column(JSON, nullable=False, default=dict)
external_post_id: Mapped[str | None] = mapped_column(String(255))
external_url: Mapped[str | None] = mapped_column(String(2048))
error_code: Mapped[str | None] = mapped_column(String(100))
error_message: Mapped[str | None] = mapped_column(Text)
published_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
cancellation_requested_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=utcnow
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=utcnow, onupdate=utcnow
)
class SocialPostMedia(SocialBase):
__tablename__ = "social_post_media"
__table_args__ = (Index("ix_social_post_media_post", "social_post_id"),)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id)
social_post_id: Mapped[str] = mapped_column(
String(36), ForeignKey("social_posts.id", ondelete="CASCADE"), nullable=False
)
media_variant_id: Mapped[str | None] = mapped_column(
String(36), ForeignKey("media_variants.id", ondelete="SET NULL")
)
media_asset_id: Mapped[str | None] = mapped_column(String(255))
position: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
kind: Mapped[str] = mapped_column(String(32), nullable=False, default="video")
metadata_json: Mapped[dict[str, object]] = mapped_column(
"metadata", JSON, nullable=False, default=dict
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=utcnow
)
class SocialSchedule(SocialBase):
__tablename__ = "social_schedules"
__table_args__ = (
UniqueConstraint("social_post_id", name="uq_social_schedule_post"),
Index("ix_social_schedules_due", "status", "scheduled_at"),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id)
social_post_id: Mapped[str] = mapped_column(
String(36), ForeignKey("social_posts.id", ondelete="CASCADE"), nullable=False
)
scheduled_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
timezone: Mapped[str] = mapped_column(String(100), nullable=False)
status: Mapped[str] = mapped_column(String(32), nullable=False, default="scheduled")
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=utcnow
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=utcnow, onupdate=utcnow
)
revision: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
class SocialJob(SocialBase):
__tablename__ = "social_jobs"
__table_args__ = (
UniqueConstraint(
"workspace_id", "idempotency_key", name="uq_social_jobs_workspace_idempotency"
),
Index("ix_social_jobs_workspace_status", "workspace_id", "status"),
Index("ix_social_jobs_next_attempt", "status", "next_attempt_at"),
Index("ix_social_jobs_post", "social_post_id"),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id)
workspace_id: Mapped[str] = mapped_column(String(120), nullable=False)
social_post_id: Mapped[str] = mapped_column(
String(36), ForeignKey("social_posts.id", ondelete="CASCADE"), nullable=False
)
social_post_target_id: Mapped[str | None] = mapped_column(
String(36), ForeignKey("social_post_targets.id", ondelete="CASCADE")
)
provider: Mapped[str | None] = mapped_column(String(32))
status: Mapped[str] = mapped_column(String(32), nullable=False, default="queued")
attempt_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
max_attempts: Mapped[int] = mapped_column(Integer, nullable=False, default=5)
next_attempt_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
idempotency_key: Mapped[str | None] = mapped_column(String(255))
error_code: Mapped[str | None] = mapped_column(String(100))
error_message: Mapped[str | None] = mapped_column(Text)
payload_json: Mapped[dict[str, object]] = mapped_column(
"payload", JSON, nullable=False, default=dict
)
# Provider resumable-session URLs are bearer-like credentials. They must
# survive a worker restart but must never be present in job REST/MCP/SDK
# payloads, so they are encrypted separately from payload JSON.
provider_state_encrypted: Mapped[str | None] = mapped_column(Text)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=utcnow
)
started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
cancellation_requested_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=utcnow, onupdate=utcnow
)
class SocialJobAttempt(SocialBase):
__tablename__ = "social_job_attempts"
__table_args__ = (
UniqueConstraint("social_job_id", "attempt_number", name="uq_social_job_attempt_number"),
Index("ix_social_job_attempts_job", "social_job_id", "attempt_number"),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id)
social_job_id: Mapped[str] = mapped_column(
String(36), ForeignKey("social_jobs.id", ondelete="CASCADE"), nullable=False
)
attempt_number: Mapped[int] = mapped_column(Integer, nullable=False)
status: Mapped[str] = mapped_column(String(32), nullable=False)
error_code: Mapped[str | None] = mapped_column(String(100))
error_message: Mapped[str | None] = mapped_column(Text)
provider_request_id: Mapped[str | None] = mapped_column(String(255))
started_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=utcnow
)
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
class OAuthState(SocialBase):
__tablename__ = "oauth_states"
__table_args__ = (
Index("ix_oauth_states_state", "state", unique=True),
Index("ix_oauth_states_expires_at", "expires_at"),
Index("ix_oauth_states_workspace", "workspace_id"),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id)
state: Mapped[str] = mapped_column(String(255), nullable=False)
provider: Mapped[str] = mapped_column(String(32), nullable=False)
workspace_id: Mapped[str] = mapped_column(String(120), nullable=False)
user_id: Mapped[str | None] = mapped_column(String(120))
redirect_uri: Mapped[str] = mapped_column(String(2048), nullable=False)
requested_account_type: Mapped[str | None] = mapped_column(String(64))
requested_scopes: Mapped[list[str]] = mapped_column(JSON, nullable=False, default=list)
code_verifier_encrypted: Mapped[str | None] = mapped_column(Text)
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=utcnow
)
class SocialWebhookEvent(SocialBase):
__tablename__ = "social_webhook_events"
__table_args__ = (
UniqueConstraint(
"provider", "external_event_id", name="uq_social_webhook_provider_external"
),
Index("ix_social_webhook_events_status", "status"),
Index("ix_social_webhook_events_received", "received_at"),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id)
provider: Mapped[str] = mapped_column(String(32), nullable=False)
event_type: Mapped[str] = mapped_column(String(100), nullable=False)
external_event_id: Mapped[str] = mapped_column(String(255), nullable=False)
workspace_id: Mapped[str | None] = mapped_column(String(120))
payload_json: Mapped[dict[str, object]] = mapped_column(
"payload", JSON, nullable=False, default=dict
)
received_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=utcnow
)
processed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
status: Mapped[str] = mapped_column(String(32), nullable=False, default="received")
error_message: Mapped[str | None] = mapped_column(Text)
class SocialPostMetric(SocialBase):
__tablename__ = "social_post_metrics"
__table_args__ = (
Index("ix_social_post_metrics_target_retrieved", "social_post_target_id", "retrieved_at"),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id)
social_post_id: Mapped[str] = mapped_column(
String(36), ForeignKey("social_posts.id", ondelete="CASCADE"), nullable=False
)
social_post_target_id: Mapped[str | None] = mapped_column(
String(36), ForeignKey("social_post_targets.id", ondelete="CASCADE")
)
provider: Mapped[str] = mapped_column(String(32), nullable=False)
views: Mapped[int | None] = mapped_column(BigInteger)
impressions: Mapped[int | None] = mapped_column(BigInteger)
likes: Mapped[int | None] = mapped_column(BigInteger)
comments: Mapped[int | None] = mapped_column(BigInteger)
shares: Mapped[int | None] = mapped_column(BigInteger)
engagement_rate: Mapped[float | None] = mapped_column()
published_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
retrieved_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=utcnow
)
raw_metrics: Mapped[dict[str, object]] = mapped_column(JSON, nullable=False, default=dict)
class SocialAuditEvent(SocialBase):
__tablename__ = "social_audit_events"
__table_args__ = (
Index("ix_social_audit_events_workspace_created", "workspace_id", "created_at"),
Index("ix_social_audit_events_type", "event_type"),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id)
workspace_id: Mapped[str] = mapped_column(String(120), nullable=False)
api_key_id: Mapped[str | None] = mapped_column(String(36))
event_type: Mapped[str] = mapped_column(String(100), nullable=False)
provider: Mapped[str | None] = mapped_column(String(32))
social_account_id: Mapped[str | None] = mapped_column(String(36))
social_post_id: Mapped[str | None] = mapped_column(String(36))
social_job_id: Mapped[str | None] = mapped_column(String(36))
request_id: Mapped[str | None] = mapped_column(String(64))
metadata_json: Mapped[dict[str, object]] = mapped_column(
"metadata", JSON, nullable=False, default=dict
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=utcnow
)
class SocialPublishingBatch(SocialBase):
__tablename__ = "social_publishing_batches"
__table_args__ = (
UniqueConstraint(
"workspace_id", "idempotency_key", name="uq_social_publishing_batch_idempotency"
),
Index("ix_social_publishing_batches_workspace_status", "workspace_id", "status"),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id)
workspace_id: Mapped[str] = mapped_column(String(120), nullable=False)
operation: Mapped[str] = mapped_column(String(32), nullable=False)
status: Mapped[str] = mapped_column(String(32), nullable=False, default="queued")
idempotency_key: Mapped[str] = mapped_column(String(255), nullable=False)
requested_by: Mapped[str | None] = mapped_column(String(120))
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=utcnow
)
started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
class SocialPublishingBatchItem(SocialBase):
__tablename__ = "social_publishing_batch_items"
__table_args__ = (
UniqueConstraint("batch_id", "social_post_id", name="uq_social_publishing_batch_item"),
Index("ix_social_publishing_batch_items_status", "status"),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id)
batch_id: Mapped[str] = mapped_column(
String(36), ForeignKey("social_publishing_batches.id", ondelete="CASCADE"), nullable=False
)
social_post_id: Mapped[str] = mapped_column(
String(36), ForeignKey("social_posts.id", ondelete="CASCADE"), nullable=False
)
status: Mapped[str] = mapped_column(String(32), nullable=False, default="queued")
error_code: Mapped[str | None] = mapped_column(String(100))
error_message: Mapped[str | None] = mapped_column(Text)
metadata_json: Mapped[dict[str, object]] = mapped_column(
"metadata", JSON, nullable=False, default=dict
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=utcnow
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=utcnow, onupdate=utcnow
)