Files
monitoring-tg/src/parser_bot/db/models.py
2026-06-17 17:01:34 +03:00

154 lines
6.3 KiB
Python

from datetime import datetime
from sqlalchemy import (
BigInteger,
DateTime,
ForeignKey,
Index,
String,
Text,
UniqueConstraint,
func,
)
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class Section(Base):
"""A sub-section inside a vertical, e.g. ('real_estate', 'dubai').
The pair (vertical, slug) is unique and identifies a section in URLs
and API calls. A channel belongs to exactly one section, the section
knows its vertical, and the LLM prompt store can hold a per-section
override that falls back to the vertical-level prompt.
"""
__tablename__ = "sections"
__table_args__ = (
UniqueConstraint(
"vertical",
"department_id",
"slug",
name="uq_section_vertical_department_slug",
),
Index("ix_sections_vertical_department", "vertical", "department_id"),
)
id: Mapped[int] = mapped_column(primary_key=True)
vertical: Mapped[str] = mapped_column(String(32))
department_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
slug: Mapped[str] = mapped_column(String(64))
title: Mapped[str] = mapped_column(String(255))
emoji: Mapped[str | None] = mapped_column(String(8), nullable=True)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
channels: Mapped[list["Channel"]] = relationship(back_populates="section")
class Channel(Base):
__tablename__ = "channels"
__table_args__ = (
UniqueConstraint("section_id", "identifier", name="uq_channels_section_identifier"),
Index("ix_channels_source_channel_id", "source_channel_id"),
)
id: Mapped[int] = mapped_column(primary_key=True)
# Telegram numeric channel id (peer id), nullable until first resolve
tg_id: Mapped[int | None] = mapped_column(BigInteger, unique=True, nullable=True)
# Username or t.me/joinchat link supplied by user
identifier: Mapped[str] = mapped_column(String(255))
title: Mapped[str | None] = mapped_column(String(512), nullable=True)
# 'real_estate' or 'hr' — picks which LLM prompt and lead schema is used
vertical: Mapped[str] = mapped_column(
String(32), default="real_estate", server_default="real_estate", index=True
)
section_id: Mapped[int] = mapped_column(
ForeignKey("sections.id", ondelete="RESTRICT"), index=True
)
source_channel_id: Mapped[int | None] = mapped_column(
ForeignKey("channels.id", ondelete="SET NULL"), nullable=True
)
is_active: Mapped[bool] = mapped_column(default=True, server_default="true")
last_message_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
last_polled_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
last_poll_status: Mapped[str | None] = mapped_column(String(32), nullable=True)
last_poll_error_code: Mapped[str | None] = mapped_column(String(64), nullable=True)
last_poll_error: Mapped[str | None] = mapped_column(Text, nullable=True)
last_poll_error_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
section: Mapped[Section] = relationship(back_populates="channels")
messages: Mapped[list["Message"]] = relationship(
back_populates="channel",
cascade="all, delete-orphan",
passive_deletes=True,
)
class Message(Base):
__tablename__ = "messages"
__table_args__ = (
UniqueConstraint("channel_id", "tg_message_id", name="uq_channel_message"),
Index("ix_messages_channel_date", "channel_id", "date"),
)
id: Mapped[int] = mapped_column(primary_key=True)
channel_id: Mapped[int] = mapped_column(ForeignKey("channels.id", ondelete="CASCADE"))
tg_message_id: Mapped[int] = mapped_column(BigInteger)
date: Mapped[datetime] = mapped_column(DateTime(timezone=True))
text: Mapped[str | None] = mapped_column(Text, nullable=True)
sender_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
sender_username: Mapped[str | None] = mapped_column(String(64), nullable=True)
sender_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
grouped_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
has_media: Mapped[bool] = mapped_column(default=False, server_default="false")
views: Mapped[int | None] = mapped_column(nullable=True)
forwards: Mapped[int | None] = mapped_column(nullable=True)
raw: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
media_files: Mapped[list | None] = mapped_column(JSONB, nullable=True)
extracted: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
fetched_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
channel: Mapped[Channel] = relationship(back_populates="messages")
class MessageClassification(Base):
__tablename__ = "message_classifications"
__table_args__ = (
UniqueConstraint("message_id", "section_id", name="uq_message_classification_section"),
Index("ix_message_classifications_message", "message_id"),
Index("ix_message_classifications_section", "section_id"),
)
id: Mapped[int] = mapped_column(primary_key=True)
message_id: Mapped[int] = mapped_column(ForeignKey("messages.id", ondelete="CASCADE"))
section_id: Mapped[int] = mapped_column(ForeignKey("sections.id", ondelete="CASCADE"))
vertical: Mapped[str] = mapped_column(String(32))
verdict: Mapped[dict] = mapped_column(JSONB)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
class AppSetting(Base):
"""Runtime-editable settings, edited from the UI without a restart."""
__tablename__ = "app_settings"
key: Mapped[str] = mapped_column(String(255), primary_key=True)
value: Mapped[dict | str | int | bool | None] = mapped_column(JSONB, nullable=False)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)