refacto: rename app into backend, to prepare for frontend dev
This commit is contained in:
parent
017092040d
commit
e605bf8603
32 changed files with 2 additions and 2 deletions
1
server/backend/.dockerignore
Normal file
1
server/backend/.dockerignore
Normal file
|
|
@ -0,0 +1 @@
|
|||
Dockerfile
|
||||
6
server/backend/Dockerfile
Normal file
6
server/backend/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 . .
|
||||
ENTRYPOINT ["python"]
|
||||
0
server/backend/adapters/__init__.py
Normal file
0
server/backend/adapters/__init__.py
Normal file
3
server/backend/adapters/http/__init__.py
Normal file
3
server/backend/adapters/http/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from .readings import readings_router
|
||||
|
||||
__all__ = ["readings_router"]
|
||||
28
server/backend/adapters/http/_readings_schemas.py
Normal file
28
server/backend/adapters/http/_readings_schemas.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
from datetime import datetime
|
||||
from pydantic import BaseModel
|
||||
|
||||
from domain.entities import ConsumptionResponse
|
||||
|
||||
class ConsumptionPointSchema(BaseModel):
|
||||
period: datetime
|
||||
pulse_count_start: int
|
||||
pulse_count_end: int
|
||||
delta_pulses: int
|
||||
delta_m3: float
|
||||
|
||||
class ConsumptionResponseSchema(BaseModel):
|
||||
dev_eui: str
|
||||
start: datetime
|
||||
end: datetime
|
||||
granularity: str
|
||||
points: list[ConsumptionPointSchema]
|
||||
|
||||
@classmethod
|
||||
def from_domain(cls, r: ConsumptionResponse) -> "ConsumptionResponseSchema":
|
||||
return cls(
|
||||
dev_eui=r.dev_eui,
|
||||
start=r.start,
|
||||
end=r.end,
|
||||
granularity=r.granularity,
|
||||
points=[ConsumptionPointSchema(**p.__dict__) for p in r.points],
|
||||
)
|
||||
18
server/backend/adapters/http/main.py
Normal file
18
server/backend/adapters/http/main.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from .readings import readings_router
|
||||
|
||||
app = FastAPI(title="SimuGazAPI", version="1.0.0")
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_methods=["GET"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.include_router(readings_router)
|
||||
|
||||
@app.get("/health")
|
||||
def health():
|
||||
return {"status": "ok"}
|
||||
30
server/backend/adapters/http/readings.py
Normal file
30
server/backend/adapters/http/readings.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
from datetime import datetime
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, HTTPException
|
||||
|
||||
from domain.value_objects import Granularity
|
||||
from domain.exceptions import ValidationError, DatabaseError
|
||||
from services.consumption_service import ConsumptionService
|
||||
from dependencies import get_consumption_service
|
||||
|
||||
from ._readings_schemas import ConsumptionResponseSchema
|
||||
|
||||
readings_router = APIRouter(prefix="/readings", tags=["readings"])
|
||||
|
||||
|
||||
@readings_router.get("/{dev_eui}", response_model=ConsumptionResponseSchema)
|
||||
def get_consumption(
|
||||
dev_eui: str,
|
||||
start: Annotated[datetime, Query()],
|
||||
end: Annotated[datetime, Query()],
|
||||
granularity: Annotated[Granularity, Query()] = "day",
|
||||
service: ConsumptionService = Depends(get_consumption_service),
|
||||
):
|
||||
try:
|
||||
result = service.get_consumption(dev_eui, start, end, granularity)
|
||||
return ConsumptionResponseSchema.from_domain(result)
|
||||
except ValidationError as e:
|
||||
raise HTTPException(status_code=422, detail=str(e))
|
||||
except DatabaseError as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
65
server/backend/adapters/mqtt.py
Normal file
65
server/backend/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()
|
||||
4
server/backend/adapters/postgres/__init__.py
Normal file
4
server/backend/adapters/postgres/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from .reading_repository import PgReadingQueryRepository, PgReadingRepository
|
||||
from .device_repository import PgDeviceRepository
|
||||
|
||||
__all__ = ["PgReadingQueryRepository", "PgDeviceRepository", "PgReadingRepository"]
|
||||
30
server/backend/adapters/postgres/device_repository.py
Normal file
30
server/backend/adapters/postgres/device_repository.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import logging
|
||||
|
||||
import psycopg2
|
||||
|
||||
from ports import DeviceRepository
|
||||
from domain.exceptions import DatabaseError
|
||||
from infrastructure.db import get_conn
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PgDeviceRepository(DeviceRepository):
|
||||
def get_or_create_device_id(self, dev_eui: str) -> str:
|
||||
try:
|
||||
with get_conn() as conn:
|
||||
with 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]) # type: ignore
|
||||
except psycopg2.DatabaseError as e:
|
||||
raise DatabaseError(f"Erreur de création du device {dev_eui}") from e
|
||||
91
server/backend/adapters/postgres/reading_repository.py
Normal file
91
server/backend/adapters/postgres/reading_repository.py
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
from datetime import datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
import psycopg2
|
||||
|
||||
from domain.entities import ConsumptionPoint
|
||||
from domain.exceptions import DatabaseError
|
||||
from domain.value_objects import Granularity
|
||||
from ports.reading_query_repository import ReadingQueryRepository
|
||||
from ports.reading_repository import ReadingRepository
|
||||
from infrastructure.db import get_conn
|
||||
|
||||
|
||||
_GRANULARITY_DELTA = {
|
||||
"hour": relativedelta(hours=1),
|
||||
"day": relativedelta(days=1),
|
||||
"week": relativedelta(weeks=1),
|
||||
"month": relativedelta(months=1),
|
||||
}
|
||||
|
||||
|
||||
class PgReadingQueryRepository(ReadingQueryRepository):
|
||||
def get_consumption(
|
||||
self,
|
||||
dev_eui: str,
|
||||
start: datetime,
|
||||
end: datetime,
|
||||
granularity: Granularity,
|
||||
) -> list[ConsumptionPoint]:
|
||||
if start == end:
|
||||
end = start + _GRANULARITY_DELTA[granularity]
|
||||
adjusted_start = start - _GRANULARITY_DELTA[granularity]
|
||||
date_trunc = granularity
|
||||
query = """
|
||||
WITH periods AS (
|
||||
SELECT
|
||||
DATE_TRUNC(%s, r.date) AS period,
|
||||
MAX(r.pulses) AS pulse_end
|
||||
FROM reading r
|
||||
JOIN device d ON d.device_id = r.device_id
|
||||
WHERE d.device_eui = %s
|
||||
AND r.date >= %s
|
||||
AND r.date < %s
|
||||
GROUP BY period
|
||||
)
|
||||
SELECT
|
||||
period,
|
||||
LAG(pulse_end) OVER (ORDER BY period) AS pulse_start,
|
||||
pulse_end,
|
||||
pulse_end - LAG(pulse_end) OVER (ORDER BY period) AS delta_pulses
|
||||
FROM periods
|
||||
ORDER BY period ASC
|
||||
"""
|
||||
try:
|
||||
with get_conn() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(query, (date_trunc, dev_eui, adjusted_start, end))
|
||||
rows = cur.fetchall()
|
||||
except psycopg2.DatabaseError as e:
|
||||
raise DatabaseError(f"Erreur requête consumption : {e}") from e
|
||||
|
||||
return [
|
||||
ConsumptionPoint(
|
||||
period=row[0],
|
||||
pulse_count_start=row[1],
|
||||
pulse_count_end=row[2],
|
||||
delta_pulses=row[3],
|
||||
delta_m3=round(row[3] * 0.010, 3),
|
||||
)
|
||||
for row in rows
|
||||
if row[1] is not None
|
||||
]
|
||||
|
||||
|
||||
|
||||
class PgReadingRepository(ReadingRepository):
|
||||
def insert_reading(self, device_id: str, pulse_count: int) -> None:
|
||||
try:
|
||||
with get_conn() as conn:
|
||||
with 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
|
||||
13
server/backend/api.py
Normal file
13
server/backend/api.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import uvicorn
|
||||
|
||||
from core.logging import setup_logging
|
||||
|
||||
setup_logging()
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(
|
||||
"adapters.http.main:app",
|
||||
host="0.0.0.0",
|
||||
port=8000,
|
||||
reload=False,
|
||||
)
|
||||
10
server/backend/consumer.py
Normal file
10
server/backend/consumer.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
from core.logging import setup_logging
|
||||
from dependencies import get_uplink_service, get_mqtt_broker
|
||||
|
||||
setup_logging()
|
||||
|
||||
if __name__ == "__main__":
|
||||
broker = get_mqtt_broker()
|
||||
uplink = get_uplink_service()
|
||||
|
||||
broker.start(on_uplink=uplink.handle)
|
||||
13
server/backend/core/logging.py
Normal file
13
server/backend/core/logging.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import logging
|
||||
import sys
|
||||
import os
|
||||
|
||||
def setup_logging() -> None:
|
||||
level = os.getenv("LOG_LEVEL", "INFO").upper()
|
||||
logging.basicConfig(
|
||||
level=getattr(logging, level, logging.INFO),
|
||||
format="%(asctime)s [%(levelname)s] %(name)s — %(message)s",
|
||||
datefmt="%Y-%m-%dT%H:%M:%S",
|
||||
stream=sys.stdout,
|
||||
force=True,
|
||||
)
|
||||
34
server/backend/dependencies.py
Normal file
34
server/backend/dependencies.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import os
|
||||
|
||||
from adapters.postgres import PgDeviceRepository, PgReadingRepository, PgReadingQueryRepository
|
||||
from adapters.mqtt import PahoMqttBroker
|
||||
from services.uplink_service import UplinkService
|
||||
from services.consumption_service import ConsumptionService
|
||||
|
||||
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")
|
||||
|
||||
|
||||
## Repositories
|
||||
def get_device_repo() -> PgDeviceRepository:
|
||||
return PgDeviceRepository()
|
||||
|
||||
def get_reading_repo() -> PgReadingRepository:
|
||||
return PgReadingRepository()
|
||||
|
||||
def get_query_repo() -> PgReadingQueryRepository:
|
||||
return PgReadingQueryRepository()
|
||||
|
||||
|
||||
## Services
|
||||
def get_uplink_service() -> UplinkService:
|
||||
return UplinkService(get_device_repo(), get_reading_repo())
|
||||
|
||||
def get_consumption_service() -> ConsumptionService:
|
||||
return ConsumptionService(get_query_repo())
|
||||
|
||||
|
||||
## Adapters
|
||||
def get_mqtt_broker() -> PahoMqttBroker:
|
||||
return PahoMqttBroker(MQTT_HOST, MQTT_PORT, MQTT_TOPIC)
|
||||
0
server/backend/domain/__init__.py
Normal file
0
server/backend/domain/__init__.py
Normal file
4
server/backend/domain/entities/__init__.py
Normal file
4
server/backend/domain/entities/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from .uplink_event import UplinkEvent
|
||||
from .consumption_point import ConsumptionPoint, ConsumptionResponse
|
||||
|
||||
__all__ = ["UplinkEvent", "ConsumptionPoint", "ConsumptionResponse"]
|
||||
19
server/backend/domain/entities/consumption_point.py
Normal file
19
server/backend/domain/entities/consumption_point.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from domain.value_objects import Granularity
|
||||
|
||||
@dataclass
|
||||
class ConsumptionPoint:
|
||||
period: datetime
|
||||
pulse_count_start: int
|
||||
pulse_count_end: int
|
||||
delta_pulses: int
|
||||
delta_m3: float
|
||||
|
||||
@dataclass
|
||||
class ConsumptionResponse:
|
||||
dev_eui: str
|
||||
start: datetime
|
||||
end: datetime
|
||||
granularity: Granularity
|
||||
points: list[ConsumptionPoint]
|
||||
6
server/backend/domain/entities/uplink_event.py
Normal file
6
server/backend/domain/entities/uplink_event.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class UplinkEvent:
|
||||
dev_eui: str
|
||||
pulse_count: int
|
||||
17
server/backend/domain/exceptions.py
Normal file
17
server/backend/domain/exceptions.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
class DomainError(Exception):
|
||||
"""Base pour toutes les erreurs de domaine"""
|
||||
|
||||
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"""
|
||||
|
||||
class ValidationError(DomainError):
|
||||
"""Données d'entrée invalides"""
|
||||
3
server/backend/domain/value_objects.py
Normal file
3
server/backend/domain/value_objects.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from typing import Literal
|
||||
|
||||
Granularity = Literal["hour", "day", "month"]
|
||||
57
server/backend/infrastructure/db.py
Normal file
57
server/backend/infrastructure/db.py
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import os
|
||||
import time
|
||||
import logging
|
||||
import psycopg2
|
||||
from typing import Generator, Any
|
||||
from psycopg2.pool import ThreadedConnectionPool
|
||||
from psycopg2.extensions import connection
|
||||
from contextlib import contextmanager
|
||||
from functools import lru_cache
|
||||
|
||||
from domain.exceptions import DatabaseConnectionError
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
_MIN_CONN = 1
|
||||
_MAX_CONN = 10
|
||||
|
||||
DB_URI = os.getenv("DATABASE_URI", "postgresql://simugaz:simugaz@db/simugaz")
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def get_pool() -> ThreadedConnectionPool:
|
||||
return _create_pool(DB_URI)
|
||||
|
||||
|
||||
def _create_pool(
|
||||
uri: str, retries: int = 10, base_delay: float = 1.0
|
||||
) -> ThreadedConnectionPool:
|
||||
for attempt in range(retries):
|
||||
try:
|
||||
pool = ThreadedConnectionPool(_MIN_CONN, _MAX_CONN, uri)
|
||||
log.info(
|
||||
"Pool PostgreSQL initialisé (%d–%d connexions)", _MIN_CONN, _MAX_CONN
|
||||
)
|
||||
return pool
|
||||
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"
|
||||
)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def get_conn() -> Generator[connection, Any, None]:
|
||||
pool = get_pool()
|
||||
conn = pool.getconn() # type: ignore
|
||||
try:
|
||||
yield conn
|
||||
except Exception:
|
||||
conn.rollback() # type: ignore
|
||||
raise
|
||||
finally:
|
||||
pool.putconn(conn) # type: ignore
|
||||
6
server/backend/ports/__init__.py
Normal file
6
server/backend/ports/__init__.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from .device_repository import DeviceRepository
|
||||
from .reading_repository import ReadingRepository
|
||||
from .message_broker import MessageBroker
|
||||
from .reading_query_repository import ReadingQueryRepository
|
||||
|
||||
__all__ = ["DeviceRepository", "ReadingRepository", "MessageBroker", "ReadingQueryRepository"]
|
||||
7
server/backend/ports/device_repository.py
Normal file
7
server/backend/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/backend/ports/message_broker.py
Normal file
10
server/backend/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"""
|
||||
...
|
||||
16
server/backend/ports/reading_query_repository.py
Normal file
16
server/backend/ports/reading_query_repository.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime
|
||||
|
||||
from domain.value_objects import Granularity
|
||||
from domain.entities import ConsumptionPoint
|
||||
|
||||
class ReadingQueryRepository(ABC):
|
||||
@abstractmethod
|
||||
def get_consumption(
|
||||
self,
|
||||
dev_eui: str,
|
||||
start: datetime,
|
||||
end: datetime,
|
||||
granularity: Granularity,
|
||||
) -> list[ConsumptionPoint]:
|
||||
...
|
||||
7
server/backend/ports/reading_repository.py
Normal file
7
server/backend/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é"""
|
||||
...
|
||||
6
server/backend/requirements.txt
Normal file
6
server/backend/requirements.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
paho-mqtt==v2.1.0
|
||||
psycopg2-binary==2.9.12
|
||||
pydantic==2.13.4
|
||||
fastapi==0.136.1
|
||||
uvicorn==0.46.0
|
||||
python-dateutil==2.9.0
|
||||
0
server/backend/services/__init__.py
Normal file
0
server/backend/services/__init__.py
Normal file
31
server/backend/services/consumption_service.py
Normal file
31
server/backend/services/consumption_service.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
from datetime import datetime
|
||||
|
||||
from domain.entities.consumption_point import ConsumptionResponse
|
||||
from domain.exceptions import ValidationError
|
||||
from domain.value_objects import Granularity
|
||||
from ports import ReadingQueryRepository
|
||||
|
||||
|
||||
class ConsumptionService:
|
||||
def __init__(self, repo: ReadingQueryRepository) -> None:
|
||||
self._repo = repo
|
||||
|
||||
def get_consumption(
|
||||
self,
|
||||
dev_eui: str,
|
||||
start: datetime,
|
||||
end: datetime,
|
||||
granularity: Granularity,
|
||||
) -> ConsumptionResponse:
|
||||
if start > end:
|
||||
raise ValidationError("'start' doit être inférieur ou égal à 'end'")
|
||||
|
||||
points = self._repo.get_consumption(dev_eui, start, end, granularity)
|
||||
|
||||
return ConsumptionResponse(
|
||||
dev_eui=dev_eui,
|
||||
start=start,
|
||||
end=end,
|
||||
granularity=granularity,
|
||||
points=points,
|
||||
)
|
||||
18
server/backend/services/uplink_service.py
Normal file
18
server/backend/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