Restructure with Turborepo
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
from .redis_cache import redis_cache
|
||||
|
||||
__all__ = ["redis_cache"]
|
||||
@@ -0,0 +1,73 @@
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
import redis.asyncio as aioredis
|
||||
|
||||
from src.infrastructure.config.settings import settings
|
||||
|
||||
|
||||
class RedisCache:
|
||||
def __init__(self) -> None:
|
||||
self._client: aioredis.Redis | None = None
|
||||
|
||||
async def connect(self) -> None:
|
||||
self._client = aioredis.from_url(
|
||||
settings.REDIS_URL,
|
||||
decode_responses=True,
|
||||
max_connections=20,
|
||||
)
|
||||
|
||||
async def disconnect(self) -> None:
|
||||
if self._client:
|
||||
await self._client.close()
|
||||
|
||||
@property
|
||||
def client(self) -> aioredis.Redis:
|
||||
if self._client is None:
|
||||
raise RuntimeError("Redis client not connected. Call connect() first.")
|
||||
return self._client
|
||||
|
||||
async def get(self, key: str) -> Any | None:
|
||||
data = await self.client.get(key)
|
||||
if data is None:
|
||||
return None
|
||||
try:
|
||||
return json.loads(data)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return data
|
||||
|
||||
async def set(self, key: str, value: Any, ttl: int | None = None) -> None:
|
||||
ttl = ttl or settings.REDIS_CACHE_TTL
|
||||
if isinstance(value, (dict, list, tuple)):
|
||||
value = json.dumps(value)
|
||||
await self.client.set(key, value, ex=ttl)
|
||||
|
||||
async def delete(self, key: str) -> None:
|
||||
await self.client.delete(key)
|
||||
|
||||
async def exists(self, key: str) -> bool:
|
||||
return bool(await self.client.exists(key))
|
||||
|
||||
async def get_with_lock(self, key: str, lock_timeout: int = 10) -> Any | None:
|
||||
async with self.client.lock(f"{key}:lock", timeout=lock_timeout):
|
||||
return await self.get(key)
|
||||
|
||||
async def set_with_lock(self, key: str, value: Any, ttl: int | None = None, lock_timeout: int = 10) -> None:
|
||||
async with self.client.lock(f"{key}:lock", timeout=lock_timeout):
|
||||
await self.set(key, value, ttl)
|
||||
|
||||
async def invalidate_pattern(self, pattern: str) -> None:
|
||||
keys = []
|
||||
async for key in self.client.scan_iter(match=pattern):
|
||||
keys.append(key)
|
||||
if keys:
|
||||
await self.client.delete(*keys)
|
||||
|
||||
async def health_check(self) -> bool:
|
||||
try:
|
||||
return await self.client.ping()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
redis_cache = RedisCache()
|
||||
@@ -0,0 +1,58 @@
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env",
|
||||
env_file_encoding="utf-8",
|
||||
env_nested_delimiter="__",
|
||||
extra="ignore",
|
||||
)
|
||||
|
||||
APP_NAME: str = "PIP - Pharmacy Integration Platform"
|
||||
APP_VERSION: str = "0.1.0"
|
||||
DEBUG: bool = False
|
||||
|
||||
DATABASE_URL: str = "postgresql+asyncpg://pip:pip@localhost:5432/pip"
|
||||
DATABASE_POOL_SIZE: int = 20
|
||||
DATABASE_MAX_OVERFLOW: int = 10
|
||||
DATABASE_POOL_RECYCLE: int = 3600
|
||||
|
||||
REDIS_URL: str = "redis://localhost:6379/0"
|
||||
REDIS_CACHE_TTL: int = 300
|
||||
|
||||
RABBITMQ_URL: str = "amqp://pip:pip@localhost:5672/pip"
|
||||
|
||||
JWT_SECRET_KEY: str = "change-me-in-production"
|
||||
JWT_ALGORITHM: str = "HS256"
|
||||
JWT_ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
|
||||
JWT_REFRESH_TOKEN_EXPIRE_DAYS: int = 7
|
||||
JWT_ISSUER: str = "pip-platform"
|
||||
|
||||
OAUTH2_TOKEN_URL: str = "/api/v1/auth/token"
|
||||
|
||||
RATE_LIMIT_PER_MINUTE: int = 60
|
||||
RATE_LIMIT_AUTH_PER_MINUTE: int = 10
|
||||
|
||||
API_KEY_PREFIX: str = "pip"
|
||||
API_KEY_LENGTH: int = 32
|
||||
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: str = "http://localhost:4317"
|
||||
OTEL_SERVICE_NAME: str = "pip-platform"
|
||||
OTEL_TRACES_ENABLED: bool = True
|
||||
OTEL_METRICS_ENABLED: bool = True
|
||||
|
||||
LOG_LEVEL: str = "INFO"
|
||||
LOG_JSON_FORMAT: bool = True
|
||||
|
||||
CORS_ORIGINS: list[str] = ["*"]
|
||||
CORS_ALLOW_CREDENTIALS: bool = True
|
||||
|
||||
HEALTH_CHECK_CACHE_TTL: int = 10
|
||||
|
||||
@property
|
||||
def DATABASE_URL_SYNC(self) -> str:
|
||||
return self.DATABASE_URL.replace("+asyncpg", "+psycopg2", 1)
|
||||
|
||||
|
||||
settings = Settings()
|
||||
@@ -0,0 +1,9 @@
|
||||
from .models import *
|
||||
from .session import async_session_factory, engine, get_db_session, get_db_session_no_commit
|
||||
|
||||
__all__ = [
|
||||
"async_session_factory",
|
||||
"engine",
|
||||
"get_db_session",
|
||||
"get_db_session_no_commit",
|
||||
]
|
||||
@@ -0,0 +1,41 @@
|
||||
from .models import (
|
||||
APIClientModel,
|
||||
APIKeyModel,
|
||||
AuditLogModel,
|
||||
Base,
|
||||
BaseMixin,
|
||||
ConnectorConfigModel,
|
||||
ConnectorModel,
|
||||
ConnectorVersionModel,
|
||||
ERPSystemModel,
|
||||
InventoryHistoryModel,
|
||||
InventoryRecordModel,
|
||||
MedicationCatalogEntryModel,
|
||||
MedicationModel,
|
||||
PharmacyModel,
|
||||
ReservationModel,
|
||||
SyncJobModel,
|
||||
SyncLogModel,
|
||||
TechnicalUserModel,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"APIClientModel",
|
||||
"APIKeyModel",
|
||||
"AuditLogModel",
|
||||
"Base",
|
||||
"BaseMixin",
|
||||
"ConnectorConfigModel",
|
||||
"ConnectorModel",
|
||||
"ConnectorVersionModel",
|
||||
"ERPSystemModel",
|
||||
"InventoryHistoryModel",
|
||||
"InventoryRecordModel",
|
||||
"MedicationCatalogEntryModel",
|
||||
"MedicationModel",
|
||||
"PharmacyModel",
|
||||
"ReservationModel",
|
||||
"SyncJobModel",
|
||||
"SyncLogModel",
|
||||
"TechnicalUserModel",
|
||||
]
|
||||
@@ -0,0 +1,349 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import (
|
||||
Boolean,
|
||||
DateTime,
|
||||
Enum,
|
||||
Float,
|
||||
ForeignKey,
|
||||
Index,
|
||||
Integer,
|
||||
LargeBinary,
|
||||
String,
|
||||
Text,
|
||||
UniqueConstraint,
|
||||
func,
|
||||
)
|
||||
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
class BaseMixin:
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
|
||||
|
||||
|
||||
class PharmacyModel(Base, BaseMixin):
|
||||
__tablename__ = "pharmacies"
|
||||
|
||||
code: Mapped[str] = mapped_column(String(50), unique=True, nullable=False, index=True)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
address: Mapped[str | None] = mapped_column(String(500))
|
||||
city: Mapped[str | None] = mapped_column(String(100))
|
||||
province: Mapped[str | None] = mapped_column(String(100))
|
||||
postal_code: Mapped[str | None] = mapped_column(String(20))
|
||||
country: Mapped[str] = mapped_column(String(2), default="ES")
|
||||
latitude: Mapped[float | None] = mapped_column(Float)
|
||||
longitude: Mapped[float | None] = mapped_column(Float)
|
||||
phone: Mapped[str | None] = mapped_column(String(50))
|
||||
email: Mapped[str | None] = mapped_column(String(255))
|
||||
opening_hours: Mapped[dict | None] = mapped_column(JSONB)
|
||||
status: Mapped[str] = mapped_column(String(20), default="pending", index=True)
|
||||
erp_type: Mapped[str | None] = mapped_column(String(50))
|
||||
erp_version: Mapped[str | None] = mapped_column(String(50))
|
||||
connector_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("connectors.id"), nullable=True)
|
||||
sync_strategy: Mapped[str] = mapped_column(String(20), default="incremental")
|
||||
sync_interval_minutes: Mapped[int] = mapped_column(Integer, default=30)
|
||||
last_sync_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
last_sync_status: Mapped[str | None] = mapped_column(String(20))
|
||||
metadata_: Mapped[dict | None] = mapped_column("metadata", JSONB)
|
||||
|
||||
connector = relationship("ConnectorModel", back_populates="pharmacies", foreign_keys=[connector_id])
|
||||
inventory_records = relationship("InventoryRecordModel", back_populates="pharmacy", cascade="all, delete-orphan")
|
||||
reservations = relationship("ReservationModel", back_populates="pharmacy", cascade="all, delete-orphan")
|
||||
catalog_entries = relationship("MedicationCatalogEntryModel", back_populates="pharmacy", cascade="all, delete-orphan")
|
||||
sync_jobs = relationship("SyncJobModel", back_populates="pharmacy", cascade="all, delete-orphan")
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_pharmacies_location", "latitude", "longitude"),
|
||||
Index("ix_pharmacies_status_erp", "status", "erp_type"),
|
||||
)
|
||||
|
||||
|
||||
class ERPSystemModel(Base, BaseMixin):
|
||||
__tablename__ = "erp_systems"
|
||||
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
erp_type: Mapped[str] = mapped_column(String(50), nullable=False, index=True)
|
||||
version: Mapped[str | None] = mapped_column(String(50))
|
||||
base_url: Mapped[str | None] = mapped_column(String(500))
|
||||
auth_config: Mapped[dict | None] = mapped_column(JSONB)
|
||||
status: Mapped[str] = mapped_column(String(20), default="pending", index=True)
|
||||
metadata_: Mapped[dict | None] = mapped_column("metadata", JSONB)
|
||||
|
||||
connectors = relationship("ConnectorModel", back_populates="erp_system", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class ConnectorModel(Base, BaseMixin):
|
||||
__tablename__ = "connectors"
|
||||
|
||||
erp_system_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("erp_systems.id"), nullable=False, index=True)
|
||||
pharmacy_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("pharmacies.id"), nullable=False, index=True)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
connector_type: Mapped[str] = mapped_column(String(50), nullable=False, index=True)
|
||||
version: Mapped[str] = mapped_column(String(20), default="1.0.0")
|
||||
config: Mapped[dict | None] = mapped_column(JSONB)
|
||||
status: Mapped[str] = mapped_column(String(20), default="pending", index=True)
|
||||
last_heartbeat_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
last_error: Mapped[str | None] = mapped_column(Text)
|
||||
metadata_: Mapped[dict | None] = mapped_column("metadata", JSONB)
|
||||
|
||||
erp_system = relationship("ERPSystemModel", back_populates="connectors")
|
||||
pharmacies = relationship("PharmacyModel", back_populates="connector", foreign_keys="PharmacyModel.connector_id")
|
||||
configs = relationship("ConnectorConfigModel", back_populates="connector", cascade="all, delete-orphan")
|
||||
connector_versions = relationship("ConnectorVersionModel", back_populates="connector", cascade="all, delete-orphan")
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("erp_system_id", "pharmacy_id", name="uq_connector_erp_pharmacy"),
|
||||
)
|
||||
|
||||
|
||||
class ConnectorVersionModel(Base, BaseMixin):
|
||||
__tablename__ = "connector_versions"
|
||||
|
||||
connector_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("connectors.id"), nullable=False, index=True)
|
||||
connector_type: Mapped[str] = mapped_column(String(50), nullable=False, index=True)
|
||||
version: Mapped[str] = mapped_column(String(20), nullable=False)
|
||||
changelog: Mapped[str | None] = mapped_column(Text)
|
||||
download_url: Mapped[str | None] = mapped_column(String(500))
|
||||
checksum: Mapped[str | None] = mapped_column(String(128))
|
||||
is_latest: Mapped[bool] = mapped_column(Boolean, default=False, index=True)
|
||||
is_required: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
released_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
|
||||
connector = relationship("ConnectorModel", back_populates="connector_versions")
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("connector_type", "version", name="uq_connector_version"),
|
||||
)
|
||||
|
||||
|
||||
class ConnectorConfigModel(Base, BaseMixin):
|
||||
__tablename__ = "connector_configs"
|
||||
|
||||
connector_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("connectors.id"), nullable=False, index=True)
|
||||
key: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
value: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
encrypted: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
description: Mapped[str | None] = mapped_column(String(500))
|
||||
|
||||
connector = relationship("ConnectorModel", back_populates="configs")
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("connector_id", "key", name="uq_connector_config_key"),
|
||||
)
|
||||
|
||||
|
||||
class MedicationModel(Base, BaseMixin):
|
||||
__tablename__ = "medications"
|
||||
|
||||
nregistro: Mapped[str] = mapped_column(String(50), unique=True, nullable=False, index=True)
|
||||
name: Mapped[str] = mapped_column(String(500), nullable=False, index=True)
|
||||
active_ingredient: Mapped[str | None] = mapped_column(String(500))
|
||||
dosage: Mapped[str | None] = mapped_column(String(100))
|
||||
form: Mapped[str | None] = mapped_column(String(100))
|
||||
atc_code: Mapped[str | None] = mapped_column(String(20), index=True)
|
||||
prescription_required: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
manufacturer: Mapped[str | None] = mapped_column(String(255))
|
||||
metadata_: Mapped[dict | None] = mapped_column("metadata", JSONB)
|
||||
|
||||
catalog_entries = relationship("MedicationCatalogEntryModel", back_populates="medication", cascade="all, delete-orphan")
|
||||
inventory_records = relationship("InventoryRecordModel", back_populates="medication", cascade="all, delete-orphan")
|
||||
reservations = relationship("ReservationModel", back_populates="medication", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class MedicationCatalogEntryModel(Base, BaseMixin):
|
||||
__tablename__ = "medication_catalog"
|
||||
|
||||
pharmacy_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("pharmacies.id"), nullable=False, index=True)
|
||||
medication_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("medications.id"), nullable=False, index=True)
|
||||
price: Mapped[float | None] = mapped_column(Float)
|
||||
stock: Mapped[int] = mapped_column(Integer, default=0)
|
||||
is_available: Mapped[bool] = mapped_column(Boolean, default=False, index=True)
|
||||
last_verified_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
metadata_: Mapped[dict | None] = mapped_column("metadata", JSONB)
|
||||
|
||||
pharmacy = relationship("PharmacyModel", back_populates="catalog_entries")
|
||||
medication = relationship("MedicationModel", back_populates="catalog_entries")
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("pharmacy_id", "medication_id", name="uq_catalog_pharmacy_medication"),
|
||||
)
|
||||
|
||||
|
||||
class InventoryRecordModel(Base, BaseMixin):
|
||||
__tablename__ = "inventory_records"
|
||||
|
||||
pharmacy_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("pharmacies.id"), nullable=False, index=True)
|
||||
medication_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("medications.id"), nullable=False, index=True)
|
||||
stock: Mapped[int] = mapped_column(Integer, default=0)
|
||||
reserved_stock: Mapped[int] = mapped_column(Integer, default=0)
|
||||
available_stock: Mapped[int] = mapped_column(Integer, default=0)
|
||||
min_stock: Mapped[int | None] = mapped_column(Integer)
|
||||
max_stock: Mapped[int | None] = mapped_column(Integer)
|
||||
last_updated_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), index=True)
|
||||
source: Mapped[str | None] = mapped_column(String(100))
|
||||
batch_id: Mapped[str | None] = mapped_column(String(100))
|
||||
metadata_: Mapped[dict | None] = mapped_column("metadata", JSONB)
|
||||
|
||||
pharmacy = relationship("PharmacyModel", back_populates="inventory_records")
|
||||
medication = relationship("MedicationModel", back_populates="inventory_records")
|
||||
history = relationship("InventoryHistoryModel", back_populates="inventory_record", cascade="all, delete-orphan")
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("pharmacy_id", "medication_id", name="uq_inventory_pharmacy_medication"),
|
||||
)
|
||||
|
||||
|
||||
class InventoryHistoryModel(Base):
|
||||
__tablename__ = "inventory_history"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
inventory_record_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("inventory_records.id"), nullable=False, index=True)
|
||||
pharmacy_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("pharmacies.id"), nullable=False, index=True)
|
||||
medication_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("medications.id"), nullable=False, index=True)
|
||||
previous_stock: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
new_stock: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
delta: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
reason: Mapped[str | None] = mapped_column(String(255))
|
||||
source: Mapped[str | None] = mapped_column(String(100))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False, index=True)
|
||||
|
||||
inventory_record = relationship("InventoryRecordModel", back_populates="history")
|
||||
|
||||
|
||||
class ReservationModel(Base, BaseMixin):
|
||||
__tablename__ = "reservations"
|
||||
|
||||
pharmacy_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("pharmacies.id"), nullable=False, index=True)
|
||||
medication_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("medications.id"), nullable=False, index=True)
|
||||
user_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("technical_users.id"), nullable=True, index=True)
|
||||
external_reference: Mapped[str | None] = mapped_column(String(255))
|
||||
quantity: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
status: Mapped[str] = mapped_column(String(20), default="pending", index=True)
|
||||
expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), index=True)
|
||||
confirmed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
cancelled_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
idempotency_key: Mapped[str | None] = mapped_column(String(255), unique=True, index=True)
|
||||
metadata_: Mapped[dict | None] = mapped_column("metadata", JSONB)
|
||||
|
||||
pharmacy = relationship("PharmacyModel", back_populates="reservations")
|
||||
medication = relationship("MedicationModel", back_populates="reservations")
|
||||
user = relationship("TechnicalUserModel", back_populates="reservations")
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_reservations_pharmacy_status", "pharmacy_id", "status"),
|
||||
Index("ix_reservations_expires", "expires_at"),
|
||||
)
|
||||
|
||||
|
||||
class SyncJobModel(Base, BaseMixin):
|
||||
__tablename__ = "sync_jobs"
|
||||
|
||||
pharmacy_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("pharmacies.id"), nullable=False, index=True)
|
||||
connector_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("connectors.id"), nullable=False, index=True)
|
||||
strategy: Mapped[str] = mapped_column(String(20), nullable=False)
|
||||
status: Mapped[str] = mapped_column(String(20), default="pending", index=True)
|
||||
started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
items_total: Mapped[int | None] = mapped_column(Integer)
|
||||
items_processed: Mapped[int | None] = mapped_column(Integer)
|
||||
items_failed: Mapped[int | None] = mapped_column(Integer)
|
||||
error_message: Mapped[str | None] = mapped_column(Text)
|
||||
metadata_: Mapped[dict | None] = mapped_column("metadata", JSONB)
|
||||
|
||||
pharmacy = relationship("PharmacyModel", back_populates="sync_jobs")
|
||||
logs = relationship("SyncLogModel", back_populates="sync_job", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class SyncLogModel(Base):
|
||||
__tablename__ = "sync_logs"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
sync_job_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("sync_jobs.id"), nullable=False, index=True)
|
||||
level: Mapped[str] = mapped_column(String(20), default="INFO", nullable=False)
|
||||
message: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
details: Mapped[dict | None] = mapped_column(JSONB)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False, index=True)
|
||||
|
||||
sync_job = relationship("SyncJobModel", back_populates="logs")
|
||||
|
||||
|
||||
class AuditLogModel(Base):
|
||||
__tablename__ = "audit_logs"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
actor_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), index=True)
|
||||
actor_type: Mapped[str | None] = mapped_column(String(50))
|
||||
action: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
|
||||
resource_type: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
|
||||
resource_id: Mapped[str | None] = mapped_column(String(255), index=True)
|
||||
old_value: Mapped[dict | None] = mapped_column(JSONB)
|
||||
new_value: Mapped[dict | None] = mapped_column(JSONB)
|
||||
ip_address: Mapped[str | None] = mapped_column(String(45))
|
||||
user_agent: Mapped[str | None] = mapped_column(String(500))
|
||||
request_id: Mapped[str | None] = mapped_column(String(100), index=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False, index=True)
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_audit_logs_action_resource", "action", "resource_type"),
|
||||
Index("ix_audit_logs_actor_created", "actor_id", "created_at"),
|
||||
)
|
||||
|
||||
|
||||
class TechnicalUserModel(Base, BaseMixin):
|
||||
__tablename__ = "technical_users"
|
||||
|
||||
username: Mapped[str] = mapped_column(String(100), unique=True, nullable=False, index=True)
|
||||
email: Mapped[str | None] = mapped_column(String(255))
|
||||
full_name: Mapped[str | None] = mapped_column(String(255))
|
||||
password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
role: Mapped[str] = mapped_column(String(20), default="viewer", index=True)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True, index=True)
|
||||
scopes: Mapped[dict | None] = mapped_column(JSONB, default=list)
|
||||
last_login_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
|
||||
reservations = relationship("ReservationModel", back_populates="user")
|
||||
api_clients = relationship("APIClientModel", back_populates="owner")
|
||||
|
||||
|
||||
class APIClientModel(Base, BaseMixin):
|
||||
__tablename__ = "api_clients"
|
||||
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
description: Mapped[str | None] = mapped_column(Text)
|
||||
owner_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("technical_users.id"), nullable=True, index=True)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True, index=True)
|
||||
scopes: Mapped[dict | None] = mapped_column(JSONB, default=list)
|
||||
rate_limit: Mapped[int | None] = mapped_column(Integer)
|
||||
allowed_ips: Mapped[dict | None] = mapped_column(JSONB, default=list)
|
||||
metadata_: Mapped[dict | None] = mapped_column("metadata", JSONB)
|
||||
|
||||
owner = relationship("TechnicalUserModel", back_populates="api_clients")
|
||||
api_keys = relationship("APIKeyModel", back_populates="api_client", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class APIKeyModel(Base):
|
||||
__tablename__ = "api_keys"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
api_client_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("api_clients.id"), nullable=False, index=True)
|
||||
key_prefix: Mapped[str] = mapped_column(String(10), nullable=False, index=True)
|
||||
key_hash: Mapped[bytes] = mapped_column(LargeBinary(64), nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True, index=True)
|
||||
expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
last_used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
|
||||
api_client = relationship("APIClientModel", back_populates="api_keys")
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_api_keys_prefix_active", "key_prefix", "is_active"),
|
||||
)
|
||||
@@ -0,0 +1,32 @@
|
||||
from .pharmacy_repository import PharmacyRepository
|
||||
from .technical_user_repository import TechnicalUserRepository
|
||||
from .audit_log_repository import AuditLogRepository
|
||||
from .api_client_repository import APIClientRepository, APIKeyRepository
|
||||
from .connector_repository import ConnectorRepository
|
||||
from .erp_system_repository import ERPSystemRepository
|
||||
from .connector_config_repository import ConnectorConfigRepository
|
||||
from .connector_version_repository import ConnectorVersionRepository
|
||||
from .medication_repository import MedicationRepository
|
||||
from .medication_catalog_repository import MedicationCatalogRepository
|
||||
from .inventory_repository import InventoryRepository
|
||||
from .reservation_repository import ReservationRepository
|
||||
from .sync_job_repository import SyncJobRepository
|
||||
from .sync_log_repository import SyncLogRepository
|
||||
|
||||
__all__ = [
|
||||
"APIClientRepository",
|
||||
"APIKeyRepository",
|
||||
"AuditLogRepository",
|
||||
"ConnectorConfigRepository",
|
||||
"ConnectorRepository",
|
||||
"ConnectorVersionRepository",
|
||||
"ERPSystemRepository",
|
||||
"InventoryRepository",
|
||||
"MedicationCatalogRepository",
|
||||
"MedicationRepository",
|
||||
"PharmacyRepository",
|
||||
"ReservationRepository",
|
||||
"SyncJobRepository",
|
||||
"SyncLogRepository",
|
||||
"TechnicalUserRepository",
|
||||
]
|
||||
@@ -0,0 +1,116 @@
|
||||
import uuid
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import delete, func, select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.domain.entities import APIClientEntity, APIKeyEntity
|
||||
from src.domain.interfaces import IAPIClientRepository, IAPIKeyRepository
|
||||
from src.infrastructure.database.models import APIClientModel, APIKeyModel
|
||||
|
||||
|
||||
class APIClientRepository(IAPIClientRepository):
|
||||
def __init__(self, session: AsyncSession) -> None:
|
||||
self._session = session
|
||||
|
||||
async def get_by_id(self, client_id: uuid.UUID) -> APIClientEntity | None:
|
||||
stmt = select(APIClientModel).where(APIClientModel.id == client_id)
|
||||
result = await self._session.execute(stmt)
|
||||
m = result.scalar_one_or_none()
|
||||
if not m:
|
||||
return None
|
||||
return APIClientEntity(
|
||||
id=m.id, name=m.name, description=m.description, owner_id=m.owner_id,
|
||||
is_active=m.is_active, scopes=m.scopes or [], rate_limit=m.rate_limit,
|
||||
allowed_ips=m.allowed_ips or [], metadata_=m.metadata_, created_at=m.created_at, updated_at=m.updated_at,
|
||||
)
|
||||
|
||||
async def list_clients(self, *, is_active: bool | None = None, offset: int = 0, limit: int = 50) -> tuple[Sequence[APIClientEntity], int]:
|
||||
filters: list[Any] = []
|
||||
if is_active is not None:
|
||||
filters.append(APIClientModel.is_active == is_active)
|
||||
count_stmt = select(func.count()).select_from(APIClientModel)
|
||||
if filters:
|
||||
count_stmt = count_stmt.where(*filters)
|
||||
total = (await self._session.execute(count_stmt)).scalar() or 0
|
||||
stmt = select(APIClientModel).order_by(APIClientModel.name).offset(offset).limit(limit)
|
||||
if filters:
|
||||
stmt = stmt.where(*filters)
|
||||
result = await self._session.execute(stmt)
|
||||
models = result.scalars().all()
|
||||
entities = [
|
||||
APIClientEntity(id=m.id, name=m.name, description=m.description, owner_id=m.owner_id,
|
||||
is_active=m.is_active, scopes=m.scopes or [], rate_limit=m.rate_limit,
|
||||
allowed_ips=m.allowed_ips or [], metadata_=m.metadata_, created_at=m.created_at, updated_at=m.updated_at)
|
||||
for m in models
|
||||
]
|
||||
return entities, total
|
||||
|
||||
async def create(self, entity: APIClientEntity) -> APIClientEntity:
|
||||
model = APIClientModel(
|
||||
name=entity.name, description=entity.description, owner_id=entity.owner_id,
|
||||
is_active=entity.is_active, scopes=entity.scopes, rate_limit=entity.rate_limit,
|
||||
allowed_ips=entity.allowed_ips, metadata_=entity.metadata_,
|
||||
)
|
||||
self._session.add(model)
|
||||
await self._session.flush()
|
||||
return await self.get_by_id(model.id) # type: ignore[return-value]
|
||||
|
||||
async def update(self, entity: APIClientEntity) -> APIClientEntity:
|
||||
stmt = update(APIClientModel).where(APIClientModel.id == entity.id).values(
|
||||
name=entity.name, description=entity.description, is_active=entity.is_active,
|
||||
scopes=entity.scopes, rate_limit=entity.rate_limit, allowed_ips=entity.allowed_ips,
|
||||
metadata_=entity.metadata_,
|
||||
)
|
||||
await self._session.execute(stmt)
|
||||
await self._session.flush()
|
||||
return await self.get_by_id(entity.id) # type: ignore[return-value]
|
||||
|
||||
|
||||
class APIKeyRepository(IAPIKeyRepository):
|
||||
def __init__(self, session: AsyncSession) -> None:
|
||||
self._session = session
|
||||
|
||||
async def get_by_prefix(self, key_prefix: str) -> APIKeyEntity | None:
|
||||
stmt = select(APIKeyModel).where(APIKeyModel.key_prefix == key_prefix, APIKeyModel.is_active == True) # noqa: E712
|
||||
result = await self._session.execute(stmt)
|
||||
m = result.scalar_one_or_none()
|
||||
if not m:
|
||||
return None
|
||||
return APIKeyEntity(
|
||||
id=m.id, api_client_id=m.api_client_id, key_prefix=m.key_prefix,
|
||||
key_hash=m.key_hash, name=m.name, is_active=m.is_active,
|
||||
expires_at=m.expires_at, last_used_at=m.last_used_at, created_at=m.created_at,
|
||||
)
|
||||
|
||||
async def list_by_client(self, api_client_id: uuid.UUID) -> Sequence[APIKeyEntity]:
|
||||
stmt = select(APIKeyModel).where(APIKeyModel.api_client_id == api_client_id).order_by(APIKeyModel.created_at.desc())
|
||||
result = await self._session.execute(stmt)
|
||||
models = result.scalars().all()
|
||||
return [
|
||||
APIKeyEntity(
|
||||
id=m.id, api_client_id=m.api_client_id, key_prefix=m.key_prefix,
|
||||
key_hash=m.key_hash, name=m.name, is_active=m.is_active,
|
||||
expires_at=m.expires_at, last_used_at=m.last_used_at, created_at=m.created_at,
|
||||
)
|
||||
for m in models
|
||||
]
|
||||
|
||||
async def create(self, entity: APIKeyEntity) -> APIKeyEntity:
|
||||
model = APIKeyModel(
|
||||
api_client_id=entity.api_client_id, key_prefix=entity.key_prefix,
|
||||
key_hash=entity.key_hash, name=entity.name, is_active=entity.is_active,
|
||||
expires_at=entity.expires_at,
|
||||
)
|
||||
self._session.add(model)
|
||||
await self._session.flush()
|
||||
return APIKeyEntity(
|
||||
id=model.id, api_client_id=model.api_client_id, key_prefix=model.key_prefix,
|
||||
key_hash=model.key_hash, name=model.name, is_active=model.is_active,
|
||||
expires_at=model.expires_at, created_at=model.created_at,
|
||||
)
|
||||
|
||||
async def update_last_used(self, key_id: uuid.UUID) -> None:
|
||||
stmt = update(APIKeyModel).where(APIKeyModel.id == key_id).values(last_used_at=func.now())
|
||||
await self._session.execute(stmt)
|
||||
@@ -0,0 +1,78 @@
|
||||
import uuid
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import delete, func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.domain.entities import AuditLogEntity
|
||||
from src.domain.interfaces import IAuditLogRepository
|
||||
from src.infrastructure.database.models import AuditLogModel
|
||||
|
||||
|
||||
def _model_to_entity(m: AuditLogModel) -> AuditLogEntity:
|
||||
return AuditLogEntity(
|
||||
id=m.id,
|
||||
actor_id=m.actor_id,
|
||||
actor_type=m.actor_type,
|
||||
action=m.action,
|
||||
resource_type=m.resource_type,
|
||||
resource_id=m.resource_id,
|
||||
old_value=m.old_value,
|
||||
new_value=m.new_value,
|
||||
ip_address=m.ip_address,
|
||||
user_agent=m.user_agent,
|
||||
request_id=m.request_id,
|
||||
created_at=m.created_at,
|
||||
)
|
||||
|
||||
|
||||
class AuditLogRepository(IAuditLogRepository):
|
||||
def __init__(self, session: AsyncSession) -> None:
|
||||
self._session = session
|
||||
|
||||
async def add(self, entity: AuditLogEntity) -> AuditLogEntity:
|
||||
model = AuditLogModel(
|
||||
actor_id=entity.actor_id,
|
||||
actor_type=entity.actor_type,
|
||||
action=entity.action,
|
||||
resource_type=entity.resource_type,
|
||||
resource_id=entity.resource_id,
|
||||
old_value=entity.old_value,
|
||||
new_value=entity.new_value,
|
||||
ip_address=entity.ip_address,
|
||||
user_agent=entity.user_agent,
|
||||
request_id=entity.request_id,
|
||||
)
|
||||
self._session.add(model)
|
||||
await self._session.flush()
|
||||
return _model_to_entity(model)
|
||||
|
||||
async def list_logs(
|
||||
self,
|
||||
*,
|
||||
actor_id: uuid.UUID | None = None,
|
||||
action: str | None = None,
|
||||
resource_type: str | None = None,
|
||||
offset: int = 0,
|
||||
limit: int = 50,
|
||||
) -> tuple[Sequence[AuditLogEntity], int]:
|
||||
filters: list[Any] = []
|
||||
if actor_id:
|
||||
filters.append(AuditLogModel.actor_id == actor_id)
|
||||
if action:
|
||||
filters.append(AuditLogModel.action == action)
|
||||
if resource_type:
|
||||
filters.append(AuditLogModel.resource_type == resource_type)
|
||||
|
||||
count_stmt = select(func.count()).select_from(AuditLogModel)
|
||||
if filters:
|
||||
count_stmt = count_stmt.where(*filters)
|
||||
total = (await self._session.execute(count_stmt)).scalar() or 0
|
||||
|
||||
stmt = select(AuditLogModel).order_by(AuditLogModel.created_at.desc()).offset(offset).limit(limit)
|
||||
if filters:
|
||||
stmt = stmt.where(*filters)
|
||||
result = await self._session.execute(stmt)
|
||||
models = result.scalars().all()
|
||||
return [_model_to_entity(m) for m in models], total
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
import uuid
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import delete, func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.domain.entities import ConnectorConfigEntity
|
||||
from src.domain.interfaces import IConnectorConfigRepository
|
||||
from src.infrastructure.database.models import ConnectorConfigModel
|
||||
|
||||
|
||||
def _model_to_entity(m: ConnectorConfigModel) -> ConnectorConfigEntity:
|
||||
return ConnectorConfigEntity(
|
||||
id=m.id,
|
||||
connector_id=m.connector_id,
|
||||
key=m.key,
|
||||
value=m.value,
|
||||
encrypted=m.encrypted,
|
||||
description=m.description,
|
||||
created_at=m.created_at,
|
||||
updated_at=m.updated_at,
|
||||
)
|
||||
|
||||
|
||||
class ConnectorConfigRepository(IConnectorConfigRepository):
|
||||
def __init__(self, session: AsyncSession) -> None:
|
||||
self._session = session
|
||||
|
||||
async def list_by_connector(self, connector_id: uuid.UUID) -> Sequence[ConnectorConfigEntity]:
|
||||
stmt = select(ConnectorConfigModel).where(ConnectorConfigModel.connector_id == connector_id).order_by(ConnectorConfigModel.key)
|
||||
result = await self._session.execute(stmt)
|
||||
models = result.scalars().all()
|
||||
return [_model_to_entity(m) for m in models]
|
||||
|
||||
async def get_by_key(self, connector_id: uuid.UUID, key: str) -> ConnectorConfigEntity | None:
|
||||
stmt = select(ConnectorConfigModel).where(
|
||||
ConnectorConfigModel.connector_id == connector_id,
|
||||
ConnectorConfigModel.key == key,
|
||||
)
|
||||
result = await self._session.execute(stmt)
|
||||
model = result.scalar_one_or_none()
|
||||
return _model_to_entity(model) if model else None
|
||||
|
||||
async def upsert(self, entity: ConnectorConfigEntity) -> ConnectorConfigEntity:
|
||||
existing = await self.get_by_key(entity.connector_id, entity.key)
|
||||
if existing:
|
||||
stmt = (
|
||||
ConnectorConfigModel.__table__.update()
|
||||
.where(ConnectorConfigModel.id == existing.id)
|
||||
.values(value=entity.value, encrypted=entity.encrypted, description=entity.description)
|
||||
)
|
||||
await self._session.execute(stmt)
|
||||
await self._session.flush()
|
||||
return await self.get_by_key(entity.connector_id, entity.key) # type: ignore[return-value]
|
||||
else:
|
||||
model = ConnectorConfigModel(
|
||||
connector_id=entity.connector_id,
|
||||
key=entity.key,
|
||||
value=entity.value,
|
||||
encrypted=entity.encrypted,
|
||||
description=entity.description,
|
||||
)
|
||||
self._session.add(model)
|
||||
await self._session.flush()
|
||||
return _model_to_entity(model)
|
||||
|
||||
async def delete(self, config_id: uuid.UUID) -> bool:
|
||||
stmt = delete(ConnectorConfigModel).where(ConnectorConfigModel.id == config_id)
|
||||
result = await self._session.execute(stmt)
|
||||
return result.rowcount > 0
|
||||
@@ -0,0 +1,101 @@
|
||||
import uuid
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import delete, func, select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.domain.entities import ConnectorEntity, ConnectorStatus
|
||||
from src.domain.interfaces import IConnectorRepository
|
||||
from src.infrastructure.database.models import ConnectorModel
|
||||
|
||||
|
||||
def _model_to_entity(m: ConnectorModel) -> ConnectorEntity:
|
||||
return ConnectorEntity(
|
||||
id=m.id,
|
||||
erp_system_id=m.erp_system_id,
|
||||
pharmacy_id=m.pharmacy_id,
|
||||
name=m.name,
|
||||
connector_type=m.connector_type,
|
||||
version=m.version,
|
||||
config=m.config,
|
||||
status=m.status,
|
||||
last_heartbeat_at=m.last_heartbeat_at,
|
||||
last_error=m.last_error,
|
||||
metadata_=m.metadata_,
|
||||
created_at=m.created_at,
|
||||
updated_at=m.updated_at,
|
||||
)
|
||||
|
||||
|
||||
def _entity_to_model_data(e: ConnectorEntity) -> dict[str, Any]:
|
||||
return {
|
||||
"erp_system_id": e.erp_system_id,
|
||||
"pharmacy_id": e.pharmacy_id,
|
||||
"name": e.name,
|
||||
"connector_type": e.connector_type if isinstance(e.connector_type, str) else e.connector_type.value,
|
||||
"version": e.version,
|
||||
"config": e.config,
|
||||
"status": e.status if isinstance(e.status, str) else e.status.value,
|
||||
"last_heartbeat_at": e.last_heartbeat_at,
|
||||
"last_error": e.last_error,
|
||||
"metadata_": e.metadata_,
|
||||
}
|
||||
|
||||
|
||||
class ConnectorRepository(IConnectorRepository):
|
||||
def __init__(self, session: AsyncSession) -> None:
|
||||
self._session = session
|
||||
|
||||
async def get_by_id(self, connector_id: uuid.UUID) -> ConnectorEntity | None:
|
||||
stmt = select(ConnectorModel).where(ConnectorModel.id == connector_id)
|
||||
result = await self._session.execute(stmt)
|
||||
model = result.scalar_one_or_none()
|
||||
return _model_to_entity(model) if model else None
|
||||
|
||||
async def list_connectors(
|
||||
self,
|
||||
*,
|
||||
erp_system_id: uuid.UUID | None = None,
|
||||
pharmacy_id: uuid.UUID | None = None,
|
||||
status: str | None = None,
|
||||
offset: int = 0,
|
||||
limit: int = 50,
|
||||
) -> tuple[Sequence[ConnectorEntity], int]:
|
||||
filters: list[Any] = []
|
||||
if erp_system_id:
|
||||
filters.append(ConnectorModel.erp_system_id == erp_system_id)
|
||||
if pharmacy_id:
|
||||
filters.append(ConnectorModel.pharmacy_id == pharmacy_id)
|
||||
if status:
|
||||
filters.append(ConnectorModel.status == status)
|
||||
|
||||
count_stmt = select(func.count()).select_from(ConnectorModel)
|
||||
if filters:
|
||||
count_stmt = count_stmt.where(*filters)
|
||||
total = (await self._session.execute(count_stmt)).scalar() or 0
|
||||
|
||||
stmt = select(ConnectorModel).order_by(ConnectorModel.name).offset(offset).limit(limit)
|
||||
if filters:
|
||||
stmt = stmt.where(*filters)
|
||||
result = await self._session.execute(stmt)
|
||||
models = result.scalars().all()
|
||||
return [_model_to_entity(m) for m in models], total
|
||||
|
||||
async def create(self, entity: ConnectorEntity) -> ConnectorEntity:
|
||||
model = ConnectorModel(**_entity_to_model_data(entity))
|
||||
self._session.add(model)
|
||||
await self._session.flush()
|
||||
return _model_to_entity(model)
|
||||
|
||||
async def update(self, entity: ConnectorEntity) -> ConnectorEntity:
|
||||
data = _entity_to_model_data(entity)
|
||||
stmt = update(ConnectorModel).where(ConnectorModel.id == entity.id).values(**data)
|
||||
await self._session.execute(stmt)
|
||||
await self._session.flush()
|
||||
return await self.get_by_id(entity.id) # type: ignore[return-value]
|
||||
|
||||
async def delete(self, connector_id: uuid.UUID) -> bool:
|
||||
stmt = delete(ConnectorModel).where(ConnectorModel.id == connector_id)
|
||||
result = await self._session.execute(stmt)
|
||||
return result.rowcount > 0
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.domain.entities import ConnectorVersionEntity
|
||||
from src.domain.interfaces import IConnectorVersionRepository
|
||||
from src.infrastructure.database.models import ConnectorVersionModel
|
||||
|
||||
|
||||
def _model_to_entity(m: ConnectorVersionModel) -> ConnectorVersionEntity:
|
||||
return ConnectorVersionEntity(
|
||||
id=m.id,
|
||||
connector_id=m.connector_id,
|
||||
connector_type=m.connector_type,
|
||||
version=m.version,
|
||||
changelog=m.changelog,
|
||||
download_url=m.download_url,
|
||||
checksum=m.checksum,
|
||||
is_latest=m.is_latest,
|
||||
is_required=m.is_required,
|
||||
released_at=m.released_at,
|
||||
created_at=m.created_at,
|
||||
)
|
||||
|
||||
|
||||
class ConnectorVersionRepository(IConnectorVersionRepository):
|
||||
def __init__(self, session: AsyncSession) -> None:
|
||||
self._session = session
|
||||
|
||||
async def get_latest(self, connector_type: str) -> ConnectorVersionEntity | None:
|
||||
stmt = (
|
||||
select(ConnectorVersionModel)
|
||||
.where(ConnectorVersionModel.connector_type == connector_type, ConnectorVersionModel.is_latest == True) # noqa: E712
|
||||
.order_by(ConnectorVersionModel.released_at.desc())
|
||||
.limit(1)
|
||||
)
|
||||
result = await self._session.execute(stmt)
|
||||
model = result.scalar_one_or_none()
|
||||
return _model_to_entity(model) if model else None
|
||||
|
||||
async def list_versions(
|
||||
self,
|
||||
*,
|
||||
connector_type: str | None = None,
|
||||
offset: int = 0,
|
||||
limit: int = 50,
|
||||
) -> tuple[Sequence[ConnectorVersionEntity], int]:
|
||||
filters: list[Any] = []
|
||||
if connector_type:
|
||||
filters.append(ConnectorVersionModel.connector_type == connector_type)
|
||||
|
||||
count_stmt = select(func.count()).select_from(ConnectorVersionModel)
|
||||
if filters:
|
||||
count_stmt = count_stmt.where(*filters)
|
||||
total = (await self._session.execute(count_stmt)).scalar() or 0
|
||||
|
||||
stmt = select(ConnectorVersionModel).order_by(ConnectorVersionModel.released_at.desc()).offset(offset).limit(limit)
|
||||
if filters:
|
||||
stmt = stmt.where(*filters)
|
||||
result = await self._session.execute(stmt)
|
||||
models = result.scalars().all()
|
||||
return [_model_to_entity(m) for m in models], total
|
||||
|
||||
async def create(self, entity: ConnectorVersionEntity) -> ConnectorVersionEntity:
|
||||
model = ConnectorVersionModel(
|
||||
connector_id=entity.connector_id or uuid.uuid4(),
|
||||
connector_type=entity.connector_type if isinstance(entity.connector_type, str) else entity.connector_type.value,
|
||||
version=entity.version,
|
||||
changelog=entity.changelog,
|
||||
download_url=entity.download_url,
|
||||
checksum=entity.checksum,
|
||||
is_latest=entity.is_latest,
|
||||
is_required=entity.is_required,
|
||||
released_at=entity.released_at,
|
||||
)
|
||||
self._session.add(model)
|
||||
await self._session.flush()
|
||||
return _model_to_entity(model)
|
||||
@@ -0,0 +1,89 @@
|
||||
import uuid
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import delete, func, select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.domain.entities import ERPSystemEntity
|
||||
from src.domain.interfaces import IERPSystemRepository
|
||||
from src.infrastructure.database.models import ERPSystemModel
|
||||
|
||||
|
||||
def _model_to_entity(m: ERPSystemModel) -> ERPSystemEntity:
|
||||
return ERPSystemEntity(
|
||||
id=m.id,
|
||||
name=m.name,
|
||||
erp_type=m.erp_type,
|
||||
version=m.version,
|
||||
base_url=m.base_url,
|
||||
auth_config=m.auth_config,
|
||||
status=m.status,
|
||||
metadata_=m.metadata_,
|
||||
created_at=m.created_at,
|
||||
updated_at=m.updated_at,
|
||||
)
|
||||
|
||||
|
||||
def _entity_to_model_data(e: ERPSystemEntity) -> dict[str, Any]:
|
||||
return {
|
||||
"name": e.name,
|
||||
"erp_type": e.erp_type if isinstance(e.erp_type, str) else e.erp_type.value,
|
||||
"version": e.version,
|
||||
"base_url": e.base_url,
|
||||
"auth_config": e.auth_config,
|
||||
"status": e.status if isinstance(e.status, str) else e.status.value,
|
||||
"metadata_": e.metadata_,
|
||||
}
|
||||
|
||||
|
||||
class ERPSystemRepository(IERPSystemRepository):
|
||||
def __init__(self, session: AsyncSession) -> None:
|
||||
self._session = session
|
||||
|
||||
async def get_by_id(self, erp_id: uuid.UUID) -> ERPSystemEntity | None:
|
||||
stmt = select(ERPSystemModel).where(ERPSystemModel.id == erp_id)
|
||||
result = await self._session.execute(stmt)
|
||||
model = result.scalar_one_or_none()
|
||||
return _model_to_entity(model) if model else None
|
||||
|
||||
async def list_erp_systems(
|
||||
self,
|
||||
*,
|
||||
erp_type: str | None = None,
|
||||
offset: int = 0,
|
||||
limit: int = 50,
|
||||
) -> tuple[Sequence[ERPSystemEntity], int]:
|
||||
filters: list[Any] = []
|
||||
if erp_type:
|
||||
filters.append(ERPSystemModel.erp_type == erp_type)
|
||||
|
||||
count_stmt = select(func.count()).select_from(ERPSystemModel)
|
||||
if filters:
|
||||
count_stmt = count_stmt.where(*filters)
|
||||
total = (await self._session.execute(count_stmt)).scalar() or 0
|
||||
|
||||
stmt = select(ERPSystemModel).order_by(ERPSystemModel.name).offset(offset).limit(limit)
|
||||
if filters:
|
||||
stmt = stmt.where(*filters)
|
||||
result = await self._session.execute(stmt)
|
||||
models = result.scalars().all()
|
||||
return [_model_to_entity(m) for m in models], total
|
||||
|
||||
async def create(self, entity: ERPSystemEntity) -> ERPSystemEntity:
|
||||
model = ERPSystemModel(**_entity_to_model_data(entity))
|
||||
self._session.add(model)
|
||||
await self._session.flush()
|
||||
return _model_to_entity(model)
|
||||
|
||||
async def update(self, entity: ERPSystemEntity) -> ERPSystemEntity:
|
||||
data = _entity_to_model_data(entity)
|
||||
stmt = update(ERPSystemModel).where(ERPSystemModel.id == entity.id).values(**data)
|
||||
await self._session.execute(stmt)
|
||||
await self._session.flush()
|
||||
return await self.get_by_id(entity.id) # type: ignore[return-value]
|
||||
|
||||
async def delete(self, erp_id: uuid.UUID) -> bool:
|
||||
stmt = delete(ERPSystemModel).where(ERPSystemModel.id == erp_id)
|
||||
result = await self._session.execute(stmt)
|
||||
return result.rowcount > 0
|
||||
@@ -0,0 +1,142 @@
|
||||
import uuid
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import func, select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.domain.entities import InventoryHistoryEntity, InventoryRecordEntity
|
||||
from src.domain.interfaces import IInventoryRepository
|
||||
from src.infrastructure.database.models import InventoryHistoryModel, InventoryRecordModel
|
||||
|
||||
|
||||
def _record_model_to_entity(m: InventoryRecordModel) -> InventoryRecordEntity:
|
||||
return InventoryRecordEntity(
|
||||
id=m.id,
|
||||
pharmacy_id=m.pharmacy_id,
|
||||
medication_id=m.medication_id,
|
||||
stock=m.stock,
|
||||
reserved_stock=m.reserved_stock,
|
||||
available_stock=m.available_stock,
|
||||
min_stock=m.min_stock,
|
||||
max_stock=m.max_stock,
|
||||
last_updated_at=m.last_updated_at,
|
||||
source=m.source,
|
||||
batch_id=m.batch_id,
|
||||
metadata_=m.metadata_,
|
||||
created_at=m.created_at,
|
||||
updated_at=m.updated_at,
|
||||
)
|
||||
|
||||
|
||||
def _record_entity_to_model_data(e: InventoryRecordEntity) -> dict[str, Any]:
|
||||
return {
|
||||
"pharmacy_id": e.pharmacy_id,
|
||||
"medication_id": e.medication_id,
|
||||
"stock": e.stock,
|
||||
"reserved_stock": e.reserved_stock,
|
||||
"available_stock": e.available_stock,
|
||||
"min_stock": e.min_stock,
|
||||
"max_stock": e.max_stock,
|
||||
"last_updated_at": e.last_updated_at,
|
||||
"source": e.source,
|
||||
"batch_id": e.batch_id,
|
||||
"metadata_": e.metadata_,
|
||||
}
|
||||
|
||||
|
||||
def _history_model_to_entity(m: InventoryHistoryModel) -> InventoryHistoryEntity:
|
||||
return InventoryHistoryEntity(
|
||||
id=m.id,
|
||||
pharmacy_id=m.pharmacy_id,
|
||||
medication_id=m.medication_id,
|
||||
previous_stock=m.previous_stock,
|
||||
new_stock=m.new_stock,
|
||||
delta=m.delta,
|
||||
reason=m.reason,
|
||||
source=m.source,
|
||||
created_at=m.created_at,
|
||||
)
|
||||
|
||||
|
||||
class InventoryRepository(IInventoryRepository):
|
||||
def __init__(self, session: AsyncSession) -> None:
|
||||
self._session = session
|
||||
|
||||
async def get_record(self, pharmacy_id: uuid.UUID, medication_id: uuid.UUID) -> InventoryRecordEntity | None:
|
||||
stmt = select(InventoryRecordModel).where(
|
||||
InventoryRecordModel.pharmacy_id == pharmacy_id,
|
||||
InventoryRecordModel.medication_id == medication_id,
|
||||
)
|
||||
result = await self._session.execute(stmt)
|
||||
model = result.scalar_one_or_none()
|
||||
return _record_model_to_entity(model) if model else None
|
||||
|
||||
async def list_by_pharmacy(self, pharmacy_id: uuid.UUID, *, offset: int = 0, limit: int = 50) -> tuple[Sequence[InventoryRecordEntity], int]:
|
||||
count_stmt = select(func.count()).select_from(InventoryRecordModel).where(
|
||||
InventoryRecordModel.pharmacy_id == pharmacy_id
|
||||
)
|
||||
total = (await self._session.execute(count_stmt)).scalar() or 0
|
||||
|
||||
stmt = (
|
||||
select(InventoryRecordModel)
|
||||
.where(InventoryRecordModel.pharmacy_id == pharmacy_id)
|
||||
.order_by(InventoryRecordModel.last_updated_at.desc().nullslast())
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
)
|
||||
result = await self._session.execute(stmt)
|
||||
models = result.scalars().all()
|
||||
return [_record_model_to_entity(m) for m in models], total
|
||||
|
||||
async def upsert(self, entity: InventoryRecordEntity) -> InventoryRecordEntity:
|
||||
existing = await self.get_record(entity.pharmacy_id, entity.medication_id)
|
||||
if existing:
|
||||
data = _record_entity_to_model_data(entity)
|
||||
stmt = (
|
||||
update(InventoryRecordModel)
|
||||
.where(
|
||||
InventoryRecordModel.pharmacy_id == entity.pharmacy_id,
|
||||
InventoryRecordModel.medication_id == entity.medication_id,
|
||||
)
|
||||
.values(**data)
|
||||
)
|
||||
await self._session.execute(stmt)
|
||||
await self._session.flush()
|
||||
return await self.get_record(entity.pharmacy_id, entity.medication_id) # type: ignore[return-value]
|
||||
else:
|
||||
model = InventoryRecordModel(**_record_entity_to_model_data(entity))
|
||||
self._session.add(model)
|
||||
await self._session.flush()
|
||||
return _record_model_to_entity(model)
|
||||
|
||||
async def add_history(self, entity: InventoryHistoryEntity) -> InventoryHistoryEntity:
|
||||
model = InventoryHistoryModel(
|
||||
inventory_record_id=entity.inventory_record_id,
|
||||
pharmacy_id=entity.pharmacy_id,
|
||||
medication_id=entity.medication_id,
|
||||
previous_stock=entity.previous_stock,
|
||||
new_stock=entity.new_stock,
|
||||
delta=entity.delta,
|
||||
reason=entity.reason,
|
||||
source=entity.source,
|
||||
)
|
||||
self._session.add(model)
|
||||
await self._session.flush()
|
||||
return _history_model_to_entity(model)
|
||||
|
||||
async def get_history(self, pharmacy_id: uuid.UUID, medication_id: uuid.UUID, *, offset: int = 0, limit: int = 50) -> Sequence[InventoryHistoryEntity]:
|
||||
record = await self.get_record(pharmacy_id, medication_id)
|
||||
if not record:
|
||||
return []
|
||||
|
||||
stmt = (
|
||||
select(InventoryHistoryModel)
|
||||
.where(InventoryHistoryModel.inventory_record_id == record.id)
|
||||
.order_by(InventoryHistoryModel.created_at.desc())
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
)
|
||||
result = await self._session.execute(stmt)
|
||||
models = result.scalars().all()
|
||||
return [_history_model_to_entity(m) for m in models]
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
import uuid
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.domain.entities import MedicationCatalogEntryEntity
|
||||
from src.domain.interfaces import IMedicationCatalogRepository
|
||||
from src.infrastructure.database.models import MedicationCatalogEntryModel
|
||||
|
||||
|
||||
def _model_to_entity(m: MedicationCatalogEntryModel) -> MedicationCatalogEntryEntity:
|
||||
return MedicationCatalogEntryEntity(
|
||||
id=m.id,
|
||||
pharmacy_id=m.pharmacy_id,
|
||||
medication_id=m.medication_id,
|
||||
price=m.price,
|
||||
stock=m.stock,
|
||||
is_available=m.is_available,
|
||||
last_verified_at=m.last_verified_at,
|
||||
metadata_=m.metadata_,
|
||||
created_at=m.created_at,
|
||||
updated_at=m.updated_at,
|
||||
)
|
||||
|
||||
|
||||
def _entity_to_model_data(e: MedicationCatalogEntryEntity) -> dict[str, Any]:
|
||||
return {
|
||||
"pharmacy_id": e.pharmacy_id,
|
||||
"medication_id": e.medication_id,
|
||||
"price": e.price,
|
||||
"stock": e.stock,
|
||||
"is_available": e.is_available,
|
||||
"last_verified_at": e.last_verified_at,
|
||||
"metadata_": e.metadata_,
|
||||
}
|
||||
|
||||
|
||||
class MedicationCatalogRepository(IMedicationCatalogRepository):
|
||||
def __init__(self, session: AsyncSession) -> None:
|
||||
self._session = session
|
||||
|
||||
async def get_entry(self, pharmacy_id: uuid.UUID, medication_id: uuid.UUID) -> MedicationCatalogEntryEntity | None:
|
||||
stmt = select(MedicationCatalogEntryModel).where(
|
||||
MedicationCatalogEntryModel.pharmacy_id == pharmacy_id,
|
||||
MedicationCatalogEntryModel.medication_id == medication_id,
|
||||
)
|
||||
result = await self._session.execute(stmt)
|
||||
model = result.scalar_one_or_none()
|
||||
return _model_to_entity(model) if model else None
|
||||
|
||||
async def list_by_pharmacy(self, pharmacy_id: uuid.UUID, *, offset: int = 0, limit: int = 50) -> tuple[Sequence[MedicationCatalogEntryEntity], int]:
|
||||
count_stmt = select(func.count()).select_from(MedicationCatalogEntryModel).where(
|
||||
MedicationCatalogEntryModel.pharmacy_id == pharmacy_id
|
||||
)
|
||||
total = (await self._session.execute(count_stmt)).scalar() or 0
|
||||
|
||||
stmt = (
|
||||
select(MedicationCatalogEntryModel)
|
||||
.where(MedicationCatalogEntryModel.pharmacy_id == pharmacy_id)
|
||||
.order_by(MedicationCatalogEntryModel.is_available.desc(), MedicationCatalogEntryModel.price.asc())
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
)
|
||||
result = await self._session.execute(stmt)
|
||||
models = result.scalars().all()
|
||||
return [_model_to_entity(m) for m in models], total
|
||||
|
||||
async def upsert(self, entity: MedicationCatalogEntryEntity) -> MedicationCatalogEntryEntity:
|
||||
existing = await self.get_entry(entity.pharmacy_id, entity.medication_id)
|
||||
if existing:
|
||||
from sqlalchemy import update
|
||||
data = _entity_to_model_data(entity)
|
||||
stmt = (
|
||||
update(MedicationCatalogEntryModel)
|
||||
.where(
|
||||
MedicationCatalogEntryModel.pharmacy_id == entity.pharmacy_id,
|
||||
MedicationCatalogEntryModel.medication_id == entity.medication_id,
|
||||
)
|
||||
.values(**data)
|
||||
)
|
||||
await self._session.execute(stmt)
|
||||
await self._session.flush()
|
||||
return await self.get_entry(entity.pharmacy_id, entity.medication_id) # type: ignore[return-value]
|
||||
else:
|
||||
model = MedicationCatalogEntryModel(**_entity_to_model_data(entity))
|
||||
self._session.add(model)
|
||||
await self._session.flush()
|
||||
return _model_to_entity(model)
|
||||
@@ -0,0 +1,101 @@
|
||||
import uuid
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import delete, func, or_, select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.domain.entities import MedicationEntity
|
||||
from src.domain.interfaces import IMedicationRepository
|
||||
from src.infrastructure.database.models import MedicationModel
|
||||
|
||||
|
||||
def _model_to_entity(m: MedicationModel) -> MedicationEntity:
|
||||
return MedicationEntity(
|
||||
id=m.id,
|
||||
nregistro=m.nregistro,
|
||||
name=m.name,
|
||||
active_ingredient=m.active_ingredient,
|
||||
dosage=m.dosage,
|
||||
form=m.form,
|
||||
atc_code=m.atc_code,
|
||||
prescription_required=m.prescription_required,
|
||||
manufacturer=m.manufacturer,
|
||||
metadata_=m.metadata_,
|
||||
created_at=m.created_at,
|
||||
updated_at=m.updated_at,
|
||||
)
|
||||
|
||||
|
||||
def _entity_to_model_data(e: MedicationEntity) -> dict[str, Any]:
|
||||
return {
|
||||
"nregistro": e.nregistro,
|
||||
"name": e.name,
|
||||
"active_ingredient": e.active_ingredient,
|
||||
"dosage": e.dosage,
|
||||
"form": e.form,
|
||||
"atc_code": e.atc_code,
|
||||
"prescription_required": e.prescription_required,
|
||||
"manufacturer": e.manufacturer,
|
||||
"metadata_": e.metadata_,
|
||||
}
|
||||
|
||||
|
||||
class MedicationRepository(IMedicationRepository):
|
||||
def __init__(self, session: AsyncSession) -> None:
|
||||
self._session = session
|
||||
|
||||
async def get_by_id(self, medication_id: uuid.UUID) -> MedicationEntity | None:
|
||||
stmt = select(MedicationModel).where(MedicationModel.id == medication_id)
|
||||
result = await self._session.execute(stmt)
|
||||
model = result.scalar_one_or_none()
|
||||
return _model_to_entity(model) if model else None
|
||||
|
||||
async def get_by_nregistro(self, nregistro: str) -> MedicationEntity | None:
|
||||
stmt = select(MedicationModel).where(MedicationModel.nregistro == nregistro)
|
||||
result = await self._session.execute(stmt)
|
||||
model = result.scalar_one_or_none()
|
||||
return _model_to_entity(model) if model else None
|
||||
|
||||
async def search(self, query: str, *, offset: int = 0, limit: int = 50) -> tuple[Sequence[MedicationEntity], int]:
|
||||
pattern = f"%{query}%"
|
||||
filters = [
|
||||
or_(
|
||||
MedicationModel.name.ilike(pattern),
|
||||
MedicationModel.active_ingredient.ilike(pattern),
|
||||
MedicationModel.nregistro.ilike(pattern),
|
||||
MedicationModel.atc_code.ilike(pattern),
|
||||
)
|
||||
]
|
||||
|
||||
count_stmt = select(func.count()).select_from(MedicationModel).where(*filters)
|
||||
total = (await self._session.execute(count_stmt)).scalar() or 0
|
||||
|
||||
stmt = (
|
||||
select(MedicationModel)
|
||||
.where(*filters)
|
||||
.order_by(MedicationModel.name)
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
)
|
||||
result = await self._session.execute(stmt)
|
||||
models = result.scalars().all()
|
||||
return [_model_to_entity(m) for m in models], total
|
||||
|
||||
async def create(self, entity: MedicationEntity) -> MedicationEntity:
|
||||
model = MedicationModel(**_entity_to_model_data(entity))
|
||||
self._session.add(model)
|
||||
await self._session.flush()
|
||||
return _model_to_entity(model)
|
||||
|
||||
async def update(self, entity: MedicationEntity) -> MedicationEntity:
|
||||
data = _entity_to_model_data(entity)
|
||||
stmt = update(MedicationModel).where(MedicationModel.id == entity.id).values(**data)
|
||||
await self._session.execute(stmt)
|
||||
await self._session.flush()
|
||||
return await self.get_by_id(entity.id) # type: ignore[return-value]
|
||||
|
||||
async def delete(self, medication_id: uuid.UUID) -> bool:
|
||||
stmt = delete(MedicationModel).where(MedicationModel.id == medication_id)
|
||||
result = await self._session.execute(stmt)
|
||||
return result.rowcount > 0
|
||||
@@ -0,0 +1,126 @@
|
||||
import uuid
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import delete, func, select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.domain.entities import PharmacyEntity
|
||||
from src.domain.interfaces import IPharmacyRepository
|
||||
from src.infrastructure.database.models import PharmacyModel
|
||||
|
||||
|
||||
def _model_to_entity(m: PharmacyModel) -> PharmacyEntity:
|
||||
return PharmacyEntity(
|
||||
id=m.id,
|
||||
code=m.code,
|
||||
name=m.name,
|
||||
address=m.address,
|
||||
city=m.city,
|
||||
province=m.province,
|
||||
postal_code=m.postal_code,
|
||||
country=m.country,
|
||||
latitude=m.latitude,
|
||||
longitude=m.longitude,
|
||||
phone=m.phone,
|
||||
email=m.email,
|
||||
opening_hours=m.opening_hours,
|
||||
status=m.status,
|
||||
erp_type=m.erp_type,
|
||||
erp_version=m.erp_version,
|
||||
connector_id=m.connector_id,
|
||||
sync_strategy=m.sync_strategy,
|
||||
sync_interval_minutes=m.sync_interval_minutes,
|
||||
last_sync_at=m.last_sync_at,
|
||||
last_sync_status=m.last_sync_status,
|
||||
metadata_=m.metadata_,
|
||||
created_at=m.created_at,
|
||||
updated_at=m.updated_at,
|
||||
)
|
||||
|
||||
|
||||
def _entity_to_model_data(e: PharmacyEntity) -> dict[str, Any]:
|
||||
return {
|
||||
"code": e.code,
|
||||
"name": e.name,
|
||||
"address": e.address,
|
||||
"city": e.city,
|
||||
"province": e.province,
|
||||
"postal_code": e.postal_code,
|
||||
"country": e.country,
|
||||
"latitude": e.latitude,
|
||||
"longitude": e.longitude,
|
||||
"phone": e.phone,
|
||||
"email": e.email,
|
||||
"opening_hours": e.opening_hours,
|
||||
"status": e.status,
|
||||
"erp_type": e.erp_type,
|
||||
"erp_version": e.erp_version,
|
||||
"connector_id": e.connector_id,
|
||||
"sync_strategy": e.sync_strategy,
|
||||
"sync_interval_minutes": e.sync_interval_minutes,
|
||||
"last_sync_at": e.last_sync_at,
|
||||
"last_sync_status": e.last_sync_status,
|
||||
"metadata_": e.metadata_,
|
||||
}
|
||||
|
||||
|
||||
class PharmacyRepository(IPharmacyRepository):
|
||||
def __init__(self, session: AsyncSession) -> None:
|
||||
self._session = session
|
||||
|
||||
async def get_by_id(self, pharmacy_id: uuid.UUID) -> PharmacyEntity | None:
|
||||
stmt = select(PharmacyModel).where(PharmacyModel.id == pharmacy_id)
|
||||
result = await self._session.execute(stmt)
|
||||
model = result.scalar_one_or_none()
|
||||
return _model_to_entity(model) if model else None
|
||||
|
||||
async def get_by_code(self, code: str) -> PharmacyEntity | None:
|
||||
stmt = select(PharmacyModel).where(PharmacyModel.code == code)
|
||||
result = await self._session.execute(stmt)
|
||||
model = result.scalar_one_or_none()
|
||||
return _model_to_entity(model) if model else None
|
||||
|
||||
async def list_pharmacies(
|
||||
self,
|
||||
*,
|
||||
status: str | None = None,
|
||||
erp_type: str | None = None,
|
||||
offset: int = 0,
|
||||
limit: int = 50,
|
||||
) -> tuple[Sequence[PharmacyEntity], int]:
|
||||
filters: list[Any] = []
|
||||
if status:
|
||||
filters.append(PharmacyModel.status == status)
|
||||
if erp_type:
|
||||
filters.append(PharmacyModel.erp_type == erp_type)
|
||||
|
||||
count_stmt = select(func.count()).select_from(PharmacyModel)
|
||||
if filters:
|
||||
count_stmt = count_stmt.where(*filters)
|
||||
total = (await self._session.execute(count_stmt)).scalar() or 0
|
||||
|
||||
stmt = select(PharmacyModel).order_by(PharmacyModel.name).offset(offset).limit(limit)
|
||||
if filters:
|
||||
stmt = stmt.where(*filters)
|
||||
result = await self._session.execute(stmt)
|
||||
models = result.scalars().all()
|
||||
return [_model_to_entity(m) for m in models], total
|
||||
|
||||
async def create(self, entity: PharmacyEntity) -> PharmacyEntity:
|
||||
model = PharmacyModel(**_entity_to_model_data(entity))
|
||||
self._session.add(model)
|
||||
await self._session.flush()
|
||||
return _model_to_entity(model)
|
||||
|
||||
async def update(self, entity: PharmacyEntity) -> PharmacyEntity:
|
||||
data = _entity_to_model_data(entity)
|
||||
stmt = update(PharmacyModel).where(PharmacyModel.id == entity.id).values(**data)
|
||||
await self._session.execute(stmt)
|
||||
await self._session.flush()
|
||||
return await self.get_by_id(entity.id) # type: ignore[return-value]
|
||||
|
||||
async def delete(self, pharmacy_id: uuid.UUID) -> bool:
|
||||
stmt = delete(PharmacyModel).where(PharmacyModel.id == pharmacy_id)
|
||||
result = await self._session.execute(stmt)
|
||||
return result.rowcount > 0
|
||||
@@ -0,0 +1,94 @@
|
||||
import uuid
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import delete, func, select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.domain.entities import ReservationEntity
|
||||
from src.domain.interfaces import IReservationRepository
|
||||
from src.infrastructure.database.models import ReservationModel
|
||||
|
||||
|
||||
def _model_to_entity(m: ReservationModel) -> ReservationEntity:
|
||||
return ReservationEntity(
|
||||
id=m.id,
|
||||
pharmacy_id=m.pharmacy_id,
|
||||
medication_id=m.medication_id,
|
||||
user_id=m.user_id,
|
||||
external_reference=m.external_reference,
|
||||
quantity=m.quantity,
|
||||
status=m.status,
|
||||
expires_at=m.expires_at,
|
||||
confirmed_at=m.confirmed_at,
|
||||
cancelled_at=m.cancelled_at,
|
||||
idempotency_key=m.idempotency_key,
|
||||
metadata_=m.metadata_,
|
||||
created_at=m.created_at,
|
||||
updated_at=m.updated_at,
|
||||
)
|
||||
|
||||
|
||||
def _entity_to_model_data(e: ReservationEntity) -> dict[str, Any]:
|
||||
return {
|
||||
"pharmacy_id": e.pharmacy_id,
|
||||
"medication_id": e.medication_id,
|
||||
"user_id": e.user_id,
|
||||
"external_reference": e.external_reference,
|
||||
"quantity": e.quantity,
|
||||
"status": e.status,
|
||||
"expires_at": e.expires_at,
|
||||
"confirmed_at": e.confirmed_at,
|
||||
"cancelled_at": e.cancelled_at,
|
||||
"idempotency_key": e.idempotency_key,
|
||||
"metadata_": e.metadata_,
|
||||
}
|
||||
|
||||
|
||||
class ReservationRepository(IReservationRepository):
|
||||
def __init__(self, session: AsyncSession) -> None:
|
||||
self._session = session
|
||||
|
||||
async def get_by_id(self, reservation_id: uuid.UUID) -> ReservationEntity | None:
|
||||
stmt = select(ReservationModel).where(ReservationModel.id == reservation_id)
|
||||
result = await self._session.execute(stmt)
|
||||
model = result.scalar_one_or_none()
|
||||
return _model_to_entity(model) if model else None
|
||||
|
||||
async def get_by_idempotency_key(self, key: str) -> ReservationEntity | None:
|
||||
stmt = select(ReservationModel).where(ReservationModel.idempotency_key == key)
|
||||
result = await self._session.execute(stmt)
|
||||
model = result.scalar_one_or_none()
|
||||
return _model_to_entity(model) if model else None
|
||||
|
||||
async def list_by_pharmacy(self, pharmacy_id: uuid.UUID, *, status: str | None = None, offset: int = 0, limit: int = 50) -> tuple[Sequence[ReservationEntity], int]:
|
||||
filters: list[Any] = [ReservationModel.pharmacy_id == pharmacy_id]
|
||||
if status:
|
||||
filters.append(ReservationModel.status == status)
|
||||
|
||||
count_stmt = select(func.count()).select_from(ReservationModel).where(*filters)
|
||||
total = (await self._session.execute(count_stmt)).scalar() or 0
|
||||
|
||||
stmt = (
|
||||
select(ReservationModel)
|
||||
.where(*filters)
|
||||
.order_by(ReservationModel.created_at.desc())
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
)
|
||||
result = await self._session.execute(stmt)
|
||||
models = result.scalars().all()
|
||||
return [_model_to_entity(m) for m in models], total
|
||||
|
||||
async def create(self, entity: ReservationEntity) -> ReservationEntity:
|
||||
model = ReservationModel(**_entity_to_model_data(entity))
|
||||
self._session.add(model)
|
||||
await self._session.flush()
|
||||
return _model_to_entity(model)
|
||||
|
||||
async def update(self, entity: ReservationEntity) -> ReservationEntity:
|
||||
data = _entity_to_model_data(entity)
|
||||
stmt = update(ReservationModel).where(ReservationModel.id == entity.id).values(**data)
|
||||
await self._session.execute(stmt)
|
||||
await self._session.flush()
|
||||
return await self.get_by_id(entity.id) # type: ignore[return-value]
|
||||
@@ -0,0 +1,95 @@
|
||||
import uuid
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import func, select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.domain.entities import SyncJobEntity, SyncJobStatus
|
||||
from src.domain.interfaces import ISyncJobRepository
|
||||
from src.infrastructure.database.models import SyncJobModel
|
||||
|
||||
|
||||
def _model_to_entity(m: SyncJobModel) -> SyncJobEntity:
|
||||
return SyncJobEntity(
|
||||
id=m.id,
|
||||
pharmacy_id=m.pharmacy_id,
|
||||
connector_id=m.connector_id,
|
||||
strategy=m.strategy,
|
||||
status=m.status,
|
||||
started_at=m.started_at,
|
||||
completed_at=m.completed_at,
|
||||
items_total=m.items_total,
|
||||
items_processed=m.items_processed,
|
||||
items_failed=m.items_failed,
|
||||
error_message=m.error_message,
|
||||
metadata_=m.metadata_,
|
||||
created_at=m.created_at,
|
||||
updated_at=m.updated_at,
|
||||
)
|
||||
|
||||
|
||||
def _entity_to_model_data(e: SyncJobEntity) -> dict[str, Any]:
|
||||
return {
|
||||
"pharmacy_id": e.pharmacy_id,
|
||||
"connector_id": e.connector_id,
|
||||
"strategy": e.strategy,
|
||||
"status": e.status,
|
||||
"started_at": e.started_at,
|
||||
"completed_at": e.completed_at,
|
||||
"items_total": e.items_total,
|
||||
"items_processed": e.items_processed,
|
||||
"items_failed": e.items_failed,
|
||||
"error_message": e.error_message,
|
||||
"metadata_": e.metadata_,
|
||||
}
|
||||
|
||||
|
||||
class SyncJobRepository(ISyncJobRepository):
|
||||
def __init__(self, session: AsyncSession) -> None:
|
||||
self._session = session
|
||||
|
||||
async def get_by_id(self, job_id: uuid.UUID) -> SyncJobEntity | None:
|
||||
stmt = select(SyncJobModel).where(SyncJobModel.id == job_id)
|
||||
result = await self._session.execute(stmt)
|
||||
model = result.scalar_one_or_none()
|
||||
return _model_to_entity(model) if model else None
|
||||
|
||||
async def list_by_pharmacy(
|
||||
self,
|
||||
pharmacy_id: uuid.UUID,
|
||||
*,
|
||||
status: str | None = None,
|
||||
offset: int = 0,
|
||||
limit: int = 50,
|
||||
) -> tuple[Sequence[SyncJobEntity], int]:
|
||||
filters: list[Any] = [SyncJobModel.pharmacy_id == pharmacy_id]
|
||||
if status:
|
||||
filters.append(SyncJobModel.status == status)
|
||||
|
||||
count_stmt = select(func.count()).select_from(SyncJobModel).where(*filters)
|
||||
total = (await self._session.execute(count_stmt)).scalar() or 0
|
||||
|
||||
stmt = (
|
||||
select(SyncJobModel)
|
||||
.where(*filters)
|
||||
.order_by(SyncJobModel.created_at.desc())
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
)
|
||||
result = await self._session.execute(stmt)
|
||||
models = result.scalars().all()
|
||||
return [_model_to_entity(m) for m in models], total
|
||||
|
||||
async def create(self, entity: SyncJobEntity) -> SyncJobEntity:
|
||||
model = SyncJobModel(**_entity_to_model_data(entity))
|
||||
self._session.add(model)
|
||||
await self._session.flush()
|
||||
return _model_to_entity(model)
|
||||
|
||||
async def update(self, entity: SyncJobEntity) -> SyncJobEntity:
|
||||
data = _entity_to_model_data(entity)
|
||||
stmt = update(SyncJobModel).where(SyncJobModel.id == entity.id).values(**data)
|
||||
await self._session.execute(stmt)
|
||||
await self._session.flush()
|
||||
return await self.get_by_id(entity.id) # type: ignore[return-value]
|
||||
@@ -0,0 +1,53 @@
|
||||
import uuid
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.domain.entities import SyncLogEntity
|
||||
from src.domain.interfaces import ISyncLogRepository
|
||||
from src.infrastructure.database.models import SyncLogModel
|
||||
|
||||
|
||||
def _model_to_entity(m: SyncLogModel) -> SyncLogEntity:
|
||||
return SyncLogEntity(
|
||||
id=m.id,
|
||||
sync_job_id=m.sync_job_id,
|
||||
level=m.level,
|
||||
message=m.message,
|
||||
details=m.details,
|
||||
created_at=m.created_at,
|
||||
)
|
||||
|
||||
|
||||
class SyncLogRepository(ISyncLogRepository):
|
||||
def __init__(self, session: AsyncSession) -> None:
|
||||
self._session = session
|
||||
|
||||
async def add(self, entity: SyncLogEntity) -> SyncLogEntity:
|
||||
model = SyncLogModel(
|
||||
sync_job_id=entity.sync_job_id,
|
||||
level=entity.level,
|
||||
message=entity.message,
|
||||
details=entity.details,
|
||||
)
|
||||
self._session.add(model)
|
||||
await self._session.flush()
|
||||
return _model_to_entity(model)
|
||||
|
||||
async def list_by_job(
|
||||
self,
|
||||
job_id: uuid.UUID,
|
||||
*,
|
||||
level: str | None = None,
|
||||
offset: int = 0,
|
||||
limit: int = 100,
|
||||
) -> Sequence[SyncLogEntity]:
|
||||
stmt = select(SyncLogModel).where(SyncLogModel.sync_job_id == job_id)
|
||||
if level:
|
||||
stmt = stmt.where(SyncLogModel.level == level)
|
||||
stmt = stmt.order_by(SyncLogModel.created_at.desc()).offset(offset).limit(limit)
|
||||
result = await self._session.execute(stmt)
|
||||
models = result.scalars().all()
|
||||
return [_model_to_entity(m) for m in models]
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
import uuid
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import delete, func, select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.domain.entities import TechnicalUserEntity
|
||||
from src.domain.interfaces import ITechnicalUserRepository
|
||||
from src.infrastructure.database.models import TechnicalUserModel
|
||||
|
||||
|
||||
def _model_to_entity(m: TechnicalUserModel) -> TechnicalUserEntity:
|
||||
return TechnicalUserEntity(
|
||||
id=m.id,
|
||||
username=m.username,
|
||||
email=m.email,
|
||||
full_name=m.full_name,
|
||||
role=m.role,
|
||||
is_active=m.is_active,
|
||||
scopes=m.scopes or [],
|
||||
last_login_at=m.last_login_at,
|
||||
created_at=m.created_at,
|
||||
updated_at=m.updated_at,
|
||||
)
|
||||
|
||||
|
||||
class TechnicalUserRepository(ITechnicalUserRepository):
|
||||
def __init__(self, session: AsyncSession) -> None:
|
||||
self._session = session
|
||||
|
||||
async def get_by_id(self, user_id: uuid.UUID) -> TechnicalUserEntity | None:
|
||||
stmt = select(TechnicalUserModel).where(TechnicalUserModel.id == user_id)
|
||||
result = await self._session.execute(stmt)
|
||||
model = result.scalar_one_or_none()
|
||||
return _model_to_entity(model) if model else None
|
||||
|
||||
async def get_by_username(self, username: str) -> TechnicalUserEntity | None:
|
||||
stmt = select(TechnicalUserModel).where(TechnicalUserModel.username == username)
|
||||
result = await self._session.execute(stmt)
|
||||
model = result.scalar_one_or_none()
|
||||
return _model_to_entity(model) if model else None
|
||||
|
||||
async def list_users(self, *, role: str | None = None, is_active: bool | None = None, offset: int = 0, limit: int = 50) -> tuple[Sequence[TechnicalUserEntity], int]:
|
||||
filters: list[Any] = []
|
||||
if role:
|
||||
filters.append(TechnicalUserModel.role == role)
|
||||
if is_active is not None:
|
||||
filters.append(TechnicalUserModel.is_active == is_active)
|
||||
|
||||
count_stmt = select(func.count()).select_from(TechnicalUserModel)
|
||||
if filters:
|
||||
count_stmt = count_stmt.where(*filters)
|
||||
total = (await self._session.execute(count_stmt)).scalar() or 0
|
||||
|
||||
stmt = select(TechnicalUserModel).order_by(TechnicalUserModel.username).offset(offset).limit(limit)
|
||||
if filters:
|
||||
stmt = stmt.where(*filters)
|
||||
result = await self._session.execute(stmt)
|
||||
models = result.scalars().all()
|
||||
return [_model_to_entity(m) for m in models], total
|
||||
|
||||
async def create(self, entity: TechnicalUserEntity, password_hash: str) -> TechnicalUserEntity:
|
||||
model = TechnicalUserModel(
|
||||
username=entity.username,
|
||||
email=entity.email,
|
||||
full_name=entity.full_name,
|
||||
password_hash=password_hash,
|
||||
role=entity.role,
|
||||
is_active=entity.is_active,
|
||||
scopes=entity.scopes,
|
||||
)
|
||||
self._session.add(model)
|
||||
await self._session.flush()
|
||||
return _model_to_entity(model)
|
||||
|
||||
async def update(self, entity: TechnicalUserEntity) -> TechnicalUserEntity:
|
||||
stmt = update(TechnicalUserModel).where(TechnicalUserModel.id == entity.id).values(
|
||||
email=entity.email,
|
||||
full_name=entity.full_name,
|
||||
role=entity.role,
|
||||
is_active=entity.is_active,
|
||||
scopes=entity.scopes,
|
||||
last_login_at=entity.last_login_at,
|
||||
)
|
||||
await self._session.execute(stmt)
|
||||
await self._session.flush()
|
||||
return await self.get_by_id(entity.id) # type: ignore[return-value]
|
||||
@@ -0,0 +1,40 @@
|
||||
from collections.abc import AsyncGenerator
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||
|
||||
from src.infrastructure.config.settings import settings
|
||||
|
||||
engine = create_async_engine(
|
||||
settings.DATABASE_URL,
|
||||
pool_size=settings.DATABASE_POOL_SIZE,
|
||||
max_overflow=settings.DATABASE_MAX_OVERFLOW,
|
||||
pool_recycle=settings.DATABASE_POOL_RECYCLE,
|
||||
echo=settings.DEBUG,
|
||||
pool_pre_ping=True,
|
||||
)
|
||||
|
||||
async_session_factory = async_sessionmaker(
|
||||
engine,
|
||||
class_=AsyncSession,
|
||||
expire_on_commit=False,
|
||||
)
|
||||
|
||||
|
||||
async def get_db_session() -> AsyncGenerator[AsyncSession, None]:
|
||||
async with async_session_factory() as session:
|
||||
try:
|
||||
yield session
|
||||
await session.commit()
|
||||
except Exception:
|
||||
await session.rollback()
|
||||
raise
|
||||
finally:
|
||||
await session.close()
|
||||
|
||||
|
||||
async def get_db_session_no_commit() -> AsyncGenerator[AsyncSession, None]:
|
||||
async with async_session_factory() as session:
|
||||
try:
|
||||
yield session
|
||||
finally:
|
||||
await session.close()
|
||||
@@ -0,0 +1,3 @@
|
||||
from .rabbitmq import rabbitmq_client
|
||||
|
||||
__all__ = ["rabbitmq_client"]
|
||||
@@ -0,0 +1,131 @@
|
||||
import json
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import aio_pika
|
||||
from aio_pika import ExchangeType, Message, RobustConnection
|
||||
|
||||
from src.infrastructure.config.settings import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RabbitMQClient:
|
||||
PIP_EXCHANGE = "pip.events"
|
||||
PIP_DLX = "pip.dlx"
|
||||
|
||||
ROUTING_KEYS = {
|
||||
"inventory_updated": "inventory.updated",
|
||||
"reservation_created": "reservation.created",
|
||||
"reservation_confirmed": "reservation.confirmed",
|
||||
"reservation_cancelled": "reservation.cancelled",
|
||||
"reservation_expired": "reservation.expired",
|
||||
"sync_started": "sync.started",
|
||||
"sync_completed": "sync.completed",
|
||||
"sync_failed": "sync.failed",
|
||||
"sync_cancelled": "sync.cancelled",
|
||||
"sync_created": "sync.created",
|
||||
"connector_heartbeat": "connector.heartbeat",
|
||||
"connector_error": "connector.error",
|
||||
"connector_created": "connector.created",
|
||||
"connector_updated": "connector.updated",
|
||||
"connector_deleted": "connector.deleted",
|
||||
"connector_deployed": "connector.deployed",
|
||||
"connector_deactivated": "connector.deactivated",
|
||||
"pharmacy_updated": "pharmacy.updated",
|
||||
"medication_created": "medication.created",
|
||||
"medication_updated": "medication.updated",
|
||||
"medication_deleted": "medication.deleted",
|
||||
"catalog_upserted": "catalog.upserted",
|
||||
}
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._connection: RobustConnection | None = None
|
||||
self._channel: aio_pika.RobustChannel | None = None
|
||||
self._exchange: aio_pika.RobustExchange | None = None
|
||||
self._dlx: aio_pika.RobustExchange | None = None
|
||||
|
||||
async def connect(self) -> None:
|
||||
self._connection = await aio_pika.connect_robust(settings.RABBITMQ_URL)
|
||||
self._channel = await self._connection.channel()
|
||||
await self._channel.set_qos(prefetch_count=100)
|
||||
|
||||
self._exchange = await self._channel.declare_exchange(
|
||||
self.PIP_EXCHANGE,
|
||||
ExchangeType.TOPIC,
|
||||
durable=True,
|
||||
)
|
||||
|
||||
self._dlx = await self._channel.declare_exchange(
|
||||
self.PIP_DLX,
|
||||
ExchangeType.TOPIC,
|
||||
durable=True,
|
||||
)
|
||||
|
||||
logger.info("rabbitmq_connected", extra={"url": settings.RABBITMQ_URL})
|
||||
|
||||
async def disconnect(self) -> None:
|
||||
if self._connection:
|
||||
await self._connection.close()
|
||||
|
||||
async def publish(self, routing_key: str, payload: dict[str, Any], headers: dict[str, str] | None = None) -> None:
|
||||
if not self._exchange:
|
||||
raise RuntimeError("RabbitMQ not connected. Call connect() first.")
|
||||
|
||||
message = Message(
|
||||
body=json.dumps(payload, default=str).encode(),
|
||||
content_type="application/json",
|
||||
delivery_mode=aio_pika.DeliveryMode.PERSISTENT,
|
||||
headers=headers or {},
|
||||
)
|
||||
|
||||
await self._exchange.publish(message, routing_key=routing_key)
|
||||
|
||||
logger.debug(
|
||||
"message_published",
|
||||
extra={"routing_key": routing_key},
|
||||
)
|
||||
|
||||
async def consume(
|
||||
self,
|
||||
queue_name: str,
|
||||
routing_keys: list[str],
|
||||
callback: Any,
|
||||
dlq: bool = True,
|
||||
) -> aio_pika.RobustQueue:
|
||||
if not self._channel or not self._exchange:
|
||||
raise RuntimeError("RabbitMQ not connected. Call connect() first.")
|
||||
|
||||
queue_arguments = {
|
||||
"x-dead-letter-exchange": self.PIP_DLX,
|
||||
"x-dead-letter-routing-key": queue_name,
|
||||
}
|
||||
|
||||
queue = await self._channel.declare_queue(
|
||||
queue_name,
|
||||
durable=True,
|
||||
arguments=queue_arguments,
|
||||
)
|
||||
|
||||
for routing_key in routing_keys:
|
||||
await queue.bind(self._exchange, routing_key=routing_key)
|
||||
|
||||
if dlq and self._dlx:
|
||||
dlq_name = f"{queue_name}.dlq"
|
||||
dlq = await self._channel.declare_queue(dlq_name, durable=True)
|
||||
await dlq.bind(self._dlx, routing_key=queue_name)
|
||||
|
||||
await queue.consume(callback)
|
||||
|
||||
return queue
|
||||
|
||||
async def health_check(self) -> bool:
|
||||
try:
|
||||
if not self._connection or self._connection.is_closed:
|
||||
return False
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
rabbitmq_client = RabbitMQClient()
|
||||
Reference in New Issue
Block a user