Проект разделен на два репозитория
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from collections.abc import AsyncIterator
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from enum import StrEnum
|
||||
|
||||
import asyncpg
|
||||
from sqlalchemy import (
|
||||
Boolean,
|
||||
DateTime,
|
||||
Enum,
|
||||
ForeignKey,
|
||||
Index,
|
||||
Integer,
|
||||
Numeric,
|
||||
SmallInteger,
|
||||
String,
|
||||
Text,
|
||||
UniqueConstraint,
|
||||
func,
|
||||
text,
|
||||
)
|
||||
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
||||
from sqlalchemy.ext.asyncio import (
|
||||
AsyncEngine,
|
||||
AsyncSession,
|
||||
async_sessionmaker,
|
||||
create_async_engine,
|
||||
)
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||
|
||||
SCHEMA = "sms"
|
||||
|
||||
|
||||
class Channel(StrEnum):
|
||||
SMS = "SMS"
|
||||
|
||||
|
||||
class SendStatus(StrEnum):
|
||||
PENDING = "pending"
|
||||
ACCEPTED = "accepted"
|
||||
REJECTED = "rejected"
|
||||
FAILED = "failed"
|
||||
UNCERTAIN = "uncertain"
|
||||
SKIPPED = "skipped"
|
||||
|
||||
|
||||
class DeliveryStatus(StrEnum):
|
||||
UNKNOWN = "unknown"
|
||||
SENT = "sent"
|
||||
DELIVERED = "delivered"
|
||||
UNDELIVERED = "undelivered"
|
||||
UNSENT = "unsent"
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
class SmsTemplate(Base):
|
||||
__tablename__ = "sms_template"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("code", "channel", "locale", "version", name="uq_template_version"),
|
||||
Index(
|
||||
"uq_template_active",
|
||||
"code",
|
||||
"channel",
|
||||
"locale",
|
||||
unique=True,
|
||||
postgresql_where=text("is_active"),
|
||||
),
|
||||
{"schema": SCHEMA},
|
||||
)
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True)
|
||||
code: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
channel: Mapped[Channel] = mapped_column(
|
||||
Enum(
|
||||
Channel,
|
||||
name="sms_channel",
|
||||
schema=SCHEMA,
|
||||
values_callable=lambda x: [e.value for e in x],
|
||||
)
|
||||
)
|
||||
locale: Mapped[str] = mapped_column(String(16), nullable=False)
|
||||
version: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
body_template: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
placeholders: Mapped[list[str]] = mapped_column(JSONB, nullable=False)
|
||||
sender_name: Mapped[str | None] = mapped_column(String(64))
|
||||
max_parts: Mapped[int] = mapped_column(SmallInteger, nullable=False)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||
approved_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||||
)
|
||||
created_by: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
|
||||
|
||||
class SmsSetting(Base):
|
||||
__tablename__ = "sms_setting"
|
||||
__table_args__ = {"schema": SCHEMA}
|
||||
|
||||
setting_key: Mapped[str] = mapped_column(String(128), primary_key=True)
|
||||
setting_value: Mapped[object] = mapped_column(JSONB, nullable=False)
|
||||
value_type: Mapped[str] = mapped_column(String(16), nullable=False)
|
||||
description: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||||
)
|
||||
|
||||
|
||||
class SmsOutboundMessage(Base):
|
||||
__tablename__ = "sms_outbound_message"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("requester_service", "idempotency_key", name="uq_outbound_idempotency"),
|
||||
Index(
|
||||
"uq_outbound_provider_message",
|
||||
"provider",
|
||||
"provider_message_id",
|
||||
unique=True,
|
||||
postgresql_where=text("provider_message_id IS NOT NULL"),
|
||||
),
|
||||
Index("ix_outbound_phone_created", "phone_e164", text("created_at DESC")),
|
||||
Index(
|
||||
"ix_outbound_requester_process_created",
|
||||
"requester_service",
|
||||
"process",
|
||||
text("created_at DESC"),
|
||||
),
|
||||
Index("ix_outbound_customer_ref", "customer_ref"),
|
||||
Index("ix_outbound_send_created", "send_status", "created_at"),
|
||||
Index("ix_outbound_delivery_updated", "delivery_status", "updated_at"),
|
||||
{"schema": SCHEMA},
|
||||
)
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||||
)
|
||||
requested_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
accepted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
sent_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
delivered_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||||
)
|
||||
requester_service: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
process: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
channel: Mapped[str] = mapped_column(String(16), nullable=False)
|
||||
provider: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||
phone_e164: Mapped[str] = mapped_column(String(16), nullable=False)
|
||||
phone_digits: Mapped[str] = mapped_column(String(15), nullable=False)
|
||||
phone_masked: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||
template_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey(f"{SCHEMA}.sms_template.id"), nullable=False
|
||||
)
|
||||
template_code: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
body_rendered: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
substitutions: Mapped[dict[str, object]] = mapped_column(JSONB, nullable=False)
|
||||
send_status: Mapped[SendStatus] = mapped_column(
|
||||
Enum(
|
||||
SendStatus,
|
||||
name="sms_send_status",
|
||||
schema=SCHEMA,
|
||||
values_callable=lambda x: [e.value for e in x],
|
||||
),
|
||||
nullable=False,
|
||||
)
|
||||
delivery_status: Mapped[DeliveryStatus] = mapped_column(
|
||||
Enum(
|
||||
DeliveryStatus,
|
||||
name="sms_delivery_status",
|
||||
schema=SCHEMA,
|
||||
values_callable=lambda x: [e.value for e in x],
|
||||
),
|
||||
nullable=False,
|
||||
)
|
||||
provider_message_id: Mapped[str | None] = mapped_column(String(128))
|
||||
provider_external_id: Mapped[str | None] = mapped_column(String(128))
|
||||
customer_ref: Mapped[str | None] = mapped_column(String(128))
|
||||
idempotency_key: Mapped[str] = mapped_column(String(192), nullable=False)
|
||||
request_fingerprint: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
request_id: Mapped[str | None] = mapped_column(String(128))
|
||||
traceparent: Mapped[str | None] = mapped_column(String(55))
|
||||
provider_http_status: Mapped[int | None] = mapped_column(Integer)
|
||||
provider_error_code: Mapped[str | None] = mapped_column(String(64))
|
||||
provider_error_message: Mapped[str | None] = mapped_column(String(256))
|
||||
sender_name: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
message_ttl_sec: Mapped[int | None] = mapped_column(Integer)
|
||||
attempt_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
last_attempt_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
next_attempt_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
worker_locked_until: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
parts: Mapped[int | None] = mapped_column(Integer)
|
||||
price: Mapped[Decimal | None] = mapped_column(Numeric(14, 4))
|
||||
currency: Mapped[str | None] = mapped_column(String(3))
|
||||
callback_last_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
|
||||
|
||||
class SmsCallbackEvent(Base):
|
||||
__tablename__ = "sms_callback_event"
|
||||
__table_args__ = (
|
||||
UniqueConstraint(
|
||||
"message_uuid",
|
||||
"callback_event",
|
||||
"status",
|
||||
"status_time",
|
||||
name="uq_callback_event",
|
||||
),
|
||||
{"schema": SCHEMA},
|
||||
)
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True)
|
||||
message_uuid: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||
callback_event: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||
status: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||
status_time: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
received_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||||
)
|
||||
|
||||
|
||||
def asyncpg_dsn(url: str) -> str:
|
||||
return url.replace("postgresql+asyncpg://", "postgresql://", 1)
|
||||
|
||||
|
||||
def create_postgres_engine(url: str) -> AsyncEngine:
|
||||
dsn = asyncpg_dsn(url)
|
||||
|
||||
async def connect() -> asyncpg.Connection:
|
||||
return await asyncpg.connect(dsn=dsn)
|
||||
|
||||
return create_async_engine(
|
||||
"postgresql+asyncpg://", async_creator=connect, pool_pre_ping=True
|
||||
)
|
||||
|
||||
|
||||
class Database:
|
||||
def __init__(self, url: str) -> None:
|
||||
self.engine = create_postgres_engine(url)
|
||||
self.sessions = async_sessionmaker(self.engine, expire_on_commit=False)
|
||||
|
||||
async def session(self) -> AsyncIterator[AsyncSession]:
|
||||
async with self.sessions() as session:
|
||||
yield session
|
||||
|
||||
async def close(self) -> None:
|
||||
await self.engine.dispose()
|
||||
Reference in New Issue
Block a user