From 5af3139b1961070e180f71095dfa41d00c40ee62 Mon Sep 17 00:00:00 2001 From: "herve.le-bars" Date: Tue, 4 Jun 2024 23:55:50 +0200 Subject: [PATCH] feat: add endpoints vessels/excursions/segments & vessels/positions/last --- backend/bloom/domain/segment.py | 9 +- backend/bloom/domain/vessel_last_position.py | 23 +++++ backend/bloom/domain/vessel_position.py | 8 +- .../repositories/repository_excursion.py | 21 ++++- .../infra/repositories/repository_segment.py | 94 ++++++++++++++++++- .../repository_vessel_position.py | 27 ++++++ backend/bloom/services/api.py | 55 ++++++++--- 7 files changed, 219 insertions(+), 18 deletions(-) create mode 100644 backend/bloom/domain/vessel_last_position.py diff --git a/backend/bloom/domain/segment.py b/backend/bloom/domain/segment.py index e391e8a6..9c706a23 100644 --- a/backend/bloom/domain/segment.py +++ b/backend/bloom/domain/segment.py @@ -2,11 +2,16 @@ from typing import Union from pydantic import BaseModel, ConfigDict -from shapely import Point +from shapely import Point,Geometry +from shapely.geometry import mapping, shape class Segment(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True) + model_config = ConfigDict(arbitrary_types_allowed=True, + json_encoders = { + Geometry: lambda geometry: mapping(geometry), + }, + ) id: Union[int, None] = None excursion_id: int timestamp_start: Union[datetime, None] = None diff --git a/backend/bloom/domain/vessel_last_position.py b/backend/bloom/domain/vessel_last_position.py new file mode 100644 index 00000000..525d9af5 --- /dev/null +++ b/backend/bloom/domain/vessel_last_position.py @@ -0,0 +1,23 @@ +from datetime import datetime + +from pydantic import BaseModel, ConfigDict +from shapely import Geometry, Point +from shapely.geometry import mapping, shape +from bloom.domain.vessel import Vessel +from bloom.domain.port import Port + +from typing import Union + + +class VesselLastPosition(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True, + json_encoders = { + Geometry: lambda p: mapping(p), + },) + vessel: Vessel = None + excursion_id: Union[int, None] = None + position: Union[Point, None] = None + timestamp: Union[datetime, None] = None + heading: Union[float, None] = None + speed: Union[float, None] = None + arrival_port: Union[Port, None] = None diff --git a/backend/bloom/domain/vessel_position.py b/backend/bloom/domain/vessel_position.py index 04548ab7..b1ebabbf 100644 --- a/backend/bloom/domain/vessel_position.py +++ b/backend/bloom/domain/vessel_position.py @@ -1,13 +1,17 @@ from datetime import datetime from pydantic import BaseModel, ConfigDict -from shapely import Point +from shapely import Geometry, Point +from shapely.geometry import mapping, shape from typing import Union class VesselPosition(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True) + model_config = ConfigDict(arbitrary_types_allowed=True, + json_encoders = { + Geometry: lambda p: mapping(p), + },) id: Union[int, None] = None timestamp: datetime diff --git a/backend/bloom/infra/repositories/repository_excursion.py b/backend/bloom/infra/repositories/repository_excursion.py index f42deebf..85070431 100644 --- a/backend/bloom/infra/repositories/repository_excursion.py +++ b/backend/bloom/infra/repositories/repository_excursion.py @@ -1,5 +1,5 @@ from contextlib import AbstractContextManager -from typing import Union +from typing import Any, List, Union import pandas as pd from dependency_injector.providers import Callable @@ -34,6 +34,25 @@ def get_param_from_last_excursion(self, session: Session, vessel_id: int) -> Uni return None return {"arrival_port_id": result.arrival_port_id, "arrival_position": result.arrival_position} + def get_excursions_by_vessel_id(self, session: Session, vessel_id: int) -> List[Excursion]: + """Recheche l'excursion en cours d'un bateau, c'est-à-dire l'excursion qui n'a pas de date d'arrivée""" + stmt = select(sql_model.Excursion).where(sql_model.Excursion.vessel_id == vessel_id) + result = session.execute(stmt).scalars() + if result is None: + return [] + return [ ExcursionRepository.map_to_domain(r) for r in result] + + def get_vessel_excursion_by_id(self, session: Session, vessel_id: int, excursion_id:int) -> Union[Excursion,None]: + """Recheche l'excursion en cours d'un bateau, c'est-à-dire l'excursion qui n'a pas de date d'arrivée""" + stmt = select(sql_model.Excursion).where( (sql_model.Excursion.vessel_id == vessel_id ) + & (sql_model.Excursion.id == excursion_id )) + result = session.execute(stmt) + if result is None: + return None + return [ ExcursionRepository.map_to_domain(r) for r in result.scalars()][0] + + + def get_excursion_by_id(self, session: Session, id: int) -> Union[Excursion, None]: """Recheche l'excursion en cours d'un bateau, c'est-à-dire l'excursion qui n'a pas de date d'arrivée""" sql = select(sql_model.Excursion).where(sql_model.Excursion.id == id) diff --git a/backend/bloom/infra/repositories/repository_segment.py b/backend/bloom/infra/repositories/repository_segment.py index 9205d67e..50393c28 100644 --- a/backend/bloom/infra/repositories/repository_segment.py +++ b/backend/bloom/infra/repositories/repository_segment.py @@ -1,5 +1,6 @@ from contextlib import AbstractContextManager from datetime import datetime +from typing import Any, List, Union import pandas as pd from dependency_injector.providers import Callable @@ -10,9 +11,11 @@ from sqlalchemy.orm import Session from bloom.domain.segment import Segment +from bloom.domain.vessel_last_position import VesselLastPosition from bloom.domain.zone import Zone from bloom.infra.database import sql_model from bloom.infra.repositories.repository_zone import ZoneRepository +from bloom.infra.repositories.repository_vessel import VesselRepository class SegmentRepository: @@ -35,9 +38,96 @@ def get_segments_by_excursions(self, session: Session, id: int) -> pd.DataFrame: df = pd.DataFrame(q, columns=["segment_duration", "in_amp_zone", "in_territorial_waters", "in_costal_waters"]) return df + def get_all_vessels_last_position(self, session: Session) -> List[Segment]: + stmt = select( + sql_model.Vessel, + sql_model.Segment.excursion_id, + sql_model.Segment.end_position, + sql_model.Segment.timestamp_end, + sql_model.Segment.heading_at_end, + sql_model.Segment.speed_at_end, + sql_model.Excursion.arrival_port_id + ).join( + sql_model.Vessel, + sql_model.Excursion.vessel_id == sql_model.Vessel.id + ).filter( + sql_model.Segment.last_vessel_segment == True + ) + result = session.execute(stmt) + if result is not None : + return [VesselLastPosition( + vessel=VesselRepository.map_to_domain(record[0]), + excursion_id=record[1], + position=to_shape(record[2]), + timestamp=record[3], + heading=record[4], + speed=record[5], + arrival=record[6], + ) for record in result] + else: + return [] + + def get_vessel_last_position(self, session: Session,vessel_id:int) -> List[Segment]: + stmt = select( + sql_model.Vessel, + sql_model.Segment.excursion_id, + sql_model.Segment.end_position, + sql_model.Segment.timestamp_end, + sql_model.Segment.heading_at_end, + sql_model.Segment.speed_at_end, + sql_model.Excursion.arrival_port_id + ).join( + sql_model.Vessel, + sql_model.Excursion.vessel_id == sql_model.Vessel.id + ).filter( + sql_model.Segment.last_vessel_segment == True, + sql_model.Vessel.id == vessel_id, + ) + result = session.execute(stmt) + if result is not None: + return [VesselLastPosition( + vessel=VesselRepository.map_to_domain(record[0]), + excursion_id=record[1], + position=to_shape(record[2]), + timestamp=record[3], + heading=record[4], + speed=record[5], + arrival_port_id=record[6], + ) for record in result][0] + else: + return None + + def list_vessel_excursion_segments(self,session,vessel_id:int,excursions_id: int) -> List[Segment]: + stmt = select( + sql_model.Segment + ).join( + sql_model.Excursion, + sql_model.Segment.excursion_id == sql_model.Excursion.id + ).where( sql_model.Segment.excursion_id == excursions_id, + sql_model.Excursion.vessel_id == vessel_id) + result = session.execute(stmt) + if result is not None : + return [ SegmentRepository.map_to_domain(record) for record in result.scalars()] + else: + return [] + + def get_vessel_excursion_segment_by_id(self,session,vessel_id:int,excursions_id: int, segment_id:int) -> Union[Segment,None]: + stmt = select( + sql_model.Segment + ).join( + sql_model.Excursion, + sql_model.Segment.excursion_id == sql_model.Excursion.id + ).where( sql_model.Segment.excursion_id == excursions_id, + sql_model.Excursion.vessel_id == vessel_id, + sql_model.Segment.id == segment_id) + result = session.execute(stmt) + if result is not None : + return [ SegmentRepository.map_to_domain(record) for record in result.scalars()][0] + else: + return [] + def get_last_vessel_id_segments(self, session: Session) -> pd.DataFrame: stmt = select( - sql_model.Vessel.id, sql_model.Segment.excursion_id, sql_model.Segment.end_position, sql_model.Segment.timestamp_end, @@ -154,4 +244,4 @@ def map_to_orm(segment: Segment) -> sql_model.Segment: last_vessel_segment=segment.last_vessel_segment, created_at=segment.created_at, updated_at=segment.updated_at - ) + ) \ No newline at end of file diff --git a/backend/bloom/infra/repositories/repository_vessel_position.py b/backend/bloom/infra/repositories/repository_vessel_position.py index d7793a5b..65e340ac 100644 --- a/backend/bloom/infra/repositories/repository_vessel_position.py +++ b/backend/bloom/infra/repositories/repository_vessel_position.py @@ -1,4 +1,5 @@ from datetime import datetime +from typing import Any, List, Union import pandas as pd from dependency_injector.providers import Callable @@ -10,6 +11,8 @@ from bloom.domain.vessel_position import VesselPosition from bloom.infra.database import sql_model +from bloom.logger import logger + class VesselPositionRepository: def __init__(self, session_factory: Callable) -> None: @@ -26,6 +29,30 @@ def batch_create_vessel_position(self, session: Session, vessel_positions: list[ session.add_all(orm_list) return [VesselPositionRepository.map_to_domain(orm) for orm in orm_list] + def get_all_vessel_last_positions(self, session: Session) -> List[VesselPosition]: + + stmt=select(sql_model.VesselPosition)\ + .order_by(sql_model.VesselPosition.timestamp.desc())\ + .group_by(sql_model.VesselPosition.vessel_id) + result = session.execute(stmt).scalars() + #logger.info(type(result)) + if result is not None : + return [VesselPositionRepository.map_to_domain(record) for record in result] + else: + return [] + + def get_vessel_positions(self, session: Session, vessel_id:int, + start:datetime=datetime.now(), + end:datetime=None) -> List[VesselPosition]: + + stmt=select(sql_model.VesselPosition).filter_by(vessel_id=vessel_id).order_by(sql_model.VesselPosition.timestamp.desc()) + result = session.execute(stmt).scalars() + #logger.info(type(result)) + if result is not None : + return [VesselPositionRepository.map_to_domain(record) for record in result] + else: + return [] + def get_positions_with_vessel_created_updated_after(self, session: Session, created_updated_after: datetime) -> pd.DataFrame: stmt = select(sql_model.VesselPosition.id, sql_model.VesselPosition.timestamp, diff --git a/backend/bloom/services/api.py b/backend/bloom/services/api.py index 945238de..94c0ce1c 100644 --- a/backend/bloom/services/api.py +++ b/backend/bloom/services/api.py @@ -3,14 +3,15 @@ import redis import json -from datetime import datetime - from bloom.config import settings from bloom.container import UseCases from bloom.domain.vessel import Vessel rd = redis.Redis(host=settings.redis_host, port=settings.redis_port, db=0) +from datetime import datetime + + app = FastAPI() @app.get("/vessels") @@ -38,31 +39,55 @@ async def get_vessel(vessel_id: int): with db.session() as session: return vessel_repository.get_vessel_by_id(session,vessel_id) -@app.get("/vessels/{vessel_id}/positions") -async def list_vessel_positions(vessel_id: int, start: datetime = datetime.now(), end:datetime=None): - return {"positions": ["TODO"]} +@app.get("/vessels/all/positions/last") +async def list_all_vessel_last_position(): + use_cases = UseCases() + segment_repository = use_cases.segment_repository() + db = use_cases.db() + with db.session() as session: + return segment_repository.get_all_vessels_last_position(session) @app.get("/vessels/{vessel_id}/positions/last") -async def list_vessel_position_last(vessel_id: int, date:datetime = datetime.now()): - return {"position": ["TODO"]} +async def get_vessel_last_position(vessel_id: int): + use_cases = UseCases() + segment_repository = use_cases.segment_repository() + db = use_cases.db() + with db.session() as session: + return segment_repository.get_vessel_last_position(session,vessel_id) @app.get("/vessels/{vessel_id}/excursions") async def list_vessel_excursions(vessel_id: int): - return {"excursions": ["TODO"]} + use_cases = UseCases() + excursion_repository = use_cases.excursion_repository() + db = use_cases.db() + with db.session() as session: + return excursion_repository.get_excursions_by_vessel_id(session,vessel_id) @app.get("/vessels/{vessel_id}/excursions/{excursions_id}") async def get_vessel_excursion(vessel_id: int,excursions_id: int): - return {"excursion": "TODO"} + use_cases = UseCases() + excursion_repository = use_cases.excursion_repository() + db = use_cases.db() + with db.session() as session: + return excursion_repository.get_vessel_excursion_by_id(session,vessel_id,excursions_id) @app.get("/vessels/{vessel_id}/excursions/{excursions_id}/segments") async def list_vessel_excursion_segments(vessel_id: int,excursions_id: int): - return {"segments": ["TODO"]} + use_cases = UseCases() + segment_repository = use_cases.segment_repository() + db = use_cases.db() + with db.session() as session: + return segment_repository.list_vessel_excursion_segments(session,vessel_id,excursions_id) @app.get("/vessels/{vessel_id}/excursions/{excursions_id}/segments/{segment_id}") async def get_vessel_excursion_segment(vessel_id: int,excursions_id: int, segment_id:int): - return {"segment": "TODO"} + use_cases = UseCases() + segment_repository = use_cases.segment_repository() + db = use_cases.db() + with db.session() as session: + return segment_repository.get_vessel_excursion_segment_by_id(session,vessel_id,excursions_id,segment_id) @app.get("/ports") async def list_ports(request:Request): @@ -89,6 +114,14 @@ async def get_port(port_id:int): with db.session() as session: return port_repository.get_port_by_id(session,port_id) +@app.get("/vessels/all/positions/last") +async def list_vessel_positions(vessel_id: int, date:datetime=datetime.now()): + use_cases = UseCases() + vessel_position_repository = use_cases.vessel_position_repository() + db = use_cases.db() + with db.session() as session: + return vessel_position_repository.get_vessel_positions(session,vessel_id) + @app.get("/zones") async def list_zones(): cache= rd.get(app.url_path_for('list_zones'))