refacto: reorganize files to easily add an API service
This commit is contained in:
parent
5c480db410
commit
9c883a8eca
22 changed files with 26 additions and 23 deletions
1
server/app/.dockerignore
Normal file
1
server/app/.dockerignore
Normal file
|
|
@ -0,0 +1 @@
|
|||
Dockerfile
|
||||
6
server/app/Dockerfile
Normal file
6
server/app/Dockerfile
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
FROM python:3.13-slim
|
||||
WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY . .
|
||||
CMD ["python", "consumer.py"]
|
||||
0
server/app/adapters/__init__.py
Normal file
0
server/app/adapters/__init__.py
Normal file
65
server/app/adapters/mqtt.py
Normal file
65
server/app/adapters/mqtt.py
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import json
|
||||
import logging
|
||||
from typing import Callable
|
||||
|
||||
import paho.mqtt.client as mqtt
|
||||
from paho.mqtt.client import ConnectFlags
|
||||
from paho.mqtt.enums import CallbackAPIVersion
|
||||
from paho.mqtt.properties import Properties
|
||||
from paho.mqtt.reasoncodes import ReasonCode
|
||||
|
||||
from domain.exceptions import MessageBrokerError, InfrastructureError
|
||||
from domain.entities import UplinkEvent
|
||||
from ports import MessageBroker
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PahoMqttBroker(MessageBroker):
|
||||
def __init__(self, host: str, port: int, topic: str):
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._topic = topic
|
||||
|
||||
def start(self, on_uplink: Callable[[UplinkEvent], None]) -> None:
|
||||
_client = mqtt.Client(CallbackAPIVersion.VERSION2)
|
||||
|
||||
def _on_connect(
|
||||
client: mqtt.Client,
|
||||
userdata: None,
|
||||
flags: ConnectFlags,
|
||||
reason_code: ReasonCode,
|
||||
props: Properties | None,
|
||||
) -> None:
|
||||
log.info("MQTT connecté (code=%s)", reason_code)
|
||||
client.subscribe(self._topic, qos=1)
|
||||
|
||||
def _on_message(client: mqtt.Client, userdata: None, msg: mqtt.MQTTMessage) -> None:
|
||||
try:
|
||||
body = json.loads(msg.payload)
|
||||
except json.JSONDecodeError as e:
|
||||
log.error("Payload JSON invalide sur %s : %s", msg.topic, e)
|
||||
return
|
||||
|
||||
try:
|
||||
dev_eui = body["deviceInfo"]["devEui"]
|
||||
pulse_count = int(body["object"]["pulse_count"])
|
||||
except (KeyError, ValueError, TypeError) as e:
|
||||
log.error("Champs manquants ou invalides : %s | body=%s", e, body)
|
||||
return
|
||||
|
||||
try:
|
||||
on_uplink(UplinkEvent(dev_eui=dev_eui, pulse_count=pulse_count))
|
||||
except InfrastructureError as e:
|
||||
log.error("Erreur infrastructure pour %s : %s", dev_eui, e)
|
||||
|
||||
_client.on_connect = _on_connect
|
||||
_client.on_message = _on_message
|
||||
_client.reconnect_delay_set(min_delay=1, max_delay=30)
|
||||
|
||||
try:
|
||||
_client.connect(self._host, self._port, keepalive=60)
|
||||
except OSError as e:
|
||||
raise MessageBrokerError(f"Impossible de joindre {self._host}:{self._port} : {e}") from e
|
||||
|
||||
_client.loop_forever()
|
||||
63
server/app/adapters/postgres.py
Normal file
63
server/app/adapters/postgres.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import logging
|
||||
import time
|
||||
|
||||
import psycopg2
|
||||
from psycopg2.extensions import connection
|
||||
from ports import DeviceRepository, ReadingRepository
|
||||
from domain.exceptions import DatabaseConnectionError, DatabaseError
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def connect(uri: str, retries: int = 10, base_delay: float = 1.0) -> connection:
|
||||
for attempt in range(retries):
|
||||
try:
|
||||
conn = psycopg2.connect(uri)
|
||||
conn.autocommit = True
|
||||
log.info("PostgreSQL connecté")
|
||||
return conn
|
||||
except psycopg2.OperationalError as e:
|
||||
delay = min(base_delay * 2 ** attempt, 30.0)
|
||||
log.warning("Attente PostgreSQL (tentative %d/%d) : %s", attempt + 1, retries, e)
|
||||
time.sleep(delay)
|
||||
raise DatabaseConnectionError(f"Impossible de se connecter après {retries} tentatives")
|
||||
|
||||
|
||||
class PgDeviceRepository(DeviceRepository):
|
||||
def __init__(self, conn: connection):
|
||||
self._conn = conn
|
||||
|
||||
def get_or_create_device_id(self, dev_eui: str) -> str:
|
||||
try:
|
||||
with self._conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO device (device_eui)
|
||||
VALUES (%s)
|
||||
ON CONFLICT (device_eui) DO NOTHING
|
||||
""",
|
||||
(dev_eui,),
|
||||
)
|
||||
cur.execute(
|
||||
"SELECT device_id FROM device WHERE device_eui = %s", (dev_eui,)
|
||||
)
|
||||
return str(cur.fetchone()[0])
|
||||
except psycopg2.DatabaseError as e:
|
||||
raise DatabaseError(f"Erreur de création du device {dev_eui}") from e
|
||||
|
||||
|
||||
class PgReadingRepository(ReadingRepository):
|
||||
def __init__(self, conn: connection):
|
||||
self._conn = conn
|
||||
|
||||
def insert_reading(self, device_id: str, pulse_count: int) -> None:
|
||||
try:
|
||||
with self._conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO reading (device_id, date, pulses)
|
||||
VALUES (%s, NOW(), %s)
|
||||
""",
|
||||
(device_id, pulse_count),
|
||||
)
|
||||
except psycopg2.DatabaseError as e:
|
||||
raise DatabaseError(f"Erreur d'enregistrement de la télérelève sur le device {device_id}") from e
|
||||
31
server/app/consumer.py
Normal file
31
server/app/consumer.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import logging
|
||||
import sys
|
||||
import os
|
||||
|
||||
from adapters.postgres import connect, PgDeviceRepository, PgReadingRepository
|
||||
from adapters.mqtt import PahoMqttBroker
|
||||
from services.uplink_service import UplinkService
|
||||
|
||||
MQTT_HOST = os.getenv("MQTT_HOST", "mosquitto")
|
||||
MQTT_PORT = int(os.getenv("MQTT_PORT", 1883))
|
||||
MQTT_TOPIC = os.getenv("MQTT_TOPIC", "application/+/device/+/event/up")
|
||||
DB_URI = os.getenv("DATABASE_URL", "postgresql://simugaz:simugaz@db/simugaz")
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(message)s",
|
||||
datefmt="%Y-%m-%dT%H:%M:%S",
|
||||
stream=sys.stdout,
|
||||
force=True,
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
conn = connect(DB_URI)
|
||||
broker = PahoMqttBroker(MQTT_HOST, MQTT_PORT, MQTT_TOPIC)
|
||||
|
||||
devices = PgDeviceRepository(conn)
|
||||
readings = PgReadingRepository(conn)
|
||||
|
||||
uplink = UplinkService(devices, readings)
|
||||
|
||||
broker.start(on_uplink=uplink.handle)
|
||||
0
server/app/domain/__init__.py
Normal file
0
server/app/domain/__init__.py
Normal file
3
server/app/domain/entities/__init__.py
Normal file
3
server/app/domain/entities/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from .uplink_event import UplinkEvent
|
||||
|
||||
__all__ = ["UplinkEvent"]
|
||||
6
server/app/domain/entities/uplink_event.py
Normal file
6
server/app/domain/entities/uplink_event.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class UplinkEvent:
|
||||
dev_eui: str
|
||||
pulse_count: int
|
||||
13
server/app/domain/exceptions.py
Normal file
13
server/app/domain/exceptions.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# domain/exceptions.py (ou dans domain.py directement)
|
||||
|
||||
class InfrastructureError(Exception):
|
||||
"""Erreur technique levée par un adapter"""
|
||||
|
||||
class DatabaseConnectionError(InfrastructureError):
|
||||
"""Impossible de se connecter à la db"""
|
||||
|
||||
class DatabaseError(InfrastructureError):
|
||||
"""Erreur lors d'une opération en base de données."""
|
||||
|
||||
class MessageBrokerError(InfrastructureError):
|
||||
"""Impossible de se connecter au broker MQTT"""
|
||||
5
server/app/ports/__init__.py
Normal file
5
server/app/ports/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from .device_repository import DeviceRepository
|
||||
from .reading_repository import ReadingRepository
|
||||
from .message_broker import MessageBroker
|
||||
|
||||
__all__ = ["DeviceRepository", "ReadingRepository", "MessageBroker"]
|
||||
7
server/app/ports/device_repository.py
Normal file
7
server/app/ports/device_repository.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
from abc import ABC, abstractmethod
|
||||
|
||||
class DeviceRepository(ABC):
|
||||
@abstractmethod
|
||||
def get_or_create_device_id(self, dev_eui: str) -> str:
|
||||
"""Retourne le device_id, crée le device s'il est inconnu"""
|
||||
...
|
||||
10
server/app/ports/message_broker.py
Normal file
10
server/app/ports/message_broker.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from typing import Callable
|
||||
|
||||
from domain.entities import UplinkEvent
|
||||
|
||||
class MessageBroker(ABC):
|
||||
@abstractmethod
|
||||
def start(self, on_uplink: Callable[[UplinkEvent], None]) -> None:
|
||||
"""Démarre l'écoute et appelle on_uplink(UplinkEvent) à chaque message"""
|
||||
...
|
||||
7
server/app/ports/reading_repository.py
Normal file
7
server/app/ports/reading_repository.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
from abc import ABC, abstractmethod
|
||||
|
||||
class ReadingRepository(ABC):
|
||||
@abstractmethod
|
||||
def insert_reading(self, device_id: str, pulse_count: int) -> None:
|
||||
"""Persiste un relevé"""
|
||||
...
|
||||
2
server/app/requirements.txt
Normal file
2
server/app/requirements.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
paho-mqtt==v2.1.0
|
||||
psycopg2-binary
|
||||
0
server/app/services/__init__.py
Normal file
0
server/app/services/__init__.py
Normal file
18
server/app/services/uplink_service.py
Normal file
18
server/app/services/uplink_service.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import logging
|
||||
from ports import DeviceRepository, ReadingRepository
|
||||
from domain.entities import UplinkEvent
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class UplinkService:
|
||||
def __init__(self, devices: DeviceRepository, readings: ReadingRepository):
|
||||
self._devices = devices
|
||||
self._readings = readings
|
||||
|
||||
def handle(self, event: UplinkEvent) -> None:
|
||||
device_id = self._devices.get_or_create_device_id(event.dev_eui)
|
||||
self._readings.insert_reading(device_id, event.pulse_count)
|
||||
log.info(
|
||||
"[UP] dev_eui=%s | device_id=%s | pulses=%d",
|
||||
event.dev_eui, device_id, event.pulse_count
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue