Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API Back Front interface #185

Merged
merged 34 commits into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
d93cb61
feat: add fastapi package
Jun 3, 2024
bec2f3c
feat: base api + docker container
Jun 3, 2024
69811be
feat: add vessels & vessels/{id}
Jun 3, 2024
b9b8604
feat: docker add load-data script and volume persistence
Jun 3, 2024
b70010b
feat: add ports & ports/{id} endpoints
Jun 3, 2024
102b604
feat: add zones & zones/{id} endpoints
Jun 3, 2024
e127f57
fix: fix load data to use bloom/tasks (loading positions missing)
May 12, 2024
872db80
feat: fix zones/{id} endpoint
Jun 3, 2024
fb8197e
feat: ajout serveur redis + cache system pour fastapi
Jun 3, 2024
eef38cb
feat: add endpoints vessels/excursions/segments & vessels/positions/last
Jun 4, 2024
456807b
feat: add docker/up_with_data.sh
Jun 4, 2024
1e9d689
revert: modif non souhaitée
Jun 4, 2024
11f12da
feat: ajout cache/all/flush + noache parameter pour zones et ports
Jun 7, 2024
3035ae5
feat: api add endpoint /zones/by-category/{category}
Jun 7, 2024
eace9e5
feat: + data loading + segments & excursions creation
Jun 7, 2024
fd5a9da
feat: ajout endpoint /zones/all/categories pour avoir la liste des ca…
Jun 7, 2024
62bd8f7
fix: endpoint excursions
Jun 7, 2024
475d667
fix: endpoint /vessels/{vessil_id}/excursions/{excursion_id}
Jun 7, 2024
4fe80ea
fix: nocache=false au lieu de nocache=0
Jun 7, 2024
cc6f5a3
fix: /vessels/all/positions/last
Jun 7, 2024
6923b8e
feat: add redis cache for endpoints /vessels /vessels/{vessel_id}/exc…
Jun 7, 2024
7c3df5a
fix: endpoint /vessels/all|id/positions/last format string => json
Jun 7, 2024
5aa4bbc
fix: api endpoint /vessels/id/excursions
Jun 7, 2024
f3d74ca
fix/temp: remove frontend, problem of compilation
Jun 7, 2024
f163e05
fix: ajout ZoneCategory
Jun 7, 2024
816f92c
fix: up with data
Jun 7, 2024
c67278c
fix: map to domain rename for ZoneCategory
Jun 7, 2024
4b5ce06
feat: add endpoint /zones/by-category/{cat|all}/by-sub-category/{sub}
Jun 7, 2024
138970b
clean: endpoint non réalisés
Jun 7, 2024
81b379f
feat: endpoint category & sub category defaut "all"
Jun 7, 2024
c8779d4
Quelques petites corrections
njouanin Jun 8, 2024
1bde468
Corrections de types
njouanin Jun 8, 2024
141847a
MAJ poetry.lock
njouanin Jun 10, 2024
e48d34b
feat: api diminutation de cache à 15 min
Jun 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ POSTGRES_USER=bloom_user
POSTGRES_PASSWORD=bloom
POSTGRES_DB=bloom_db
POSTGRES_PORT=5432
API_PORT=8000
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_CACHE_EXPIRATION=900
SPIRE_TOKEN=
SLACK_URL=

Expand Down
2 changes: 1 addition & 1 deletion backend/alembic/init_script/load_positions_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

df = pd.read_csv(
Path(settings.data_folder).joinpath("./spire_positions_subset.csv"),
sep=","
sep=";"
)

df.to_sql("spire_vessel_positions", engine, if_exists="append", index=False)
4 changes: 4 additions & 0 deletions backend/bloom/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ class Settings(BaseSettings):
spire_token:str = Field(default='')
data_folder:str=Field(default=str(Path(__file__).parent.parent.parent.joinpath('./data')))
db_url:str=Field(default='')

redis_host: str = Field(default='localhost')
redis_port: int = Field(default=6379)
redis_cache_expiration: int = Field(default=900)

logging_level:str=Field(
default="INFO",
Expand Down
9 changes: 7 additions & 2 deletions backend/bloom/domain/excursion.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@
from typing import Union

from pydantic import BaseModel, ConfigDict
from shapely import Point
from shapely import Geometry, Point, MultiPolygon,Polygon
from shapely.geometry import mapping, shape


class Excursion(BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True)
model_config = ConfigDict(arbitrary_types_allowed=True,
json_encoders = {
#Point: lambda point: mapping(point) if point != None else None,
Geometry: lambda p: mapping(p),
},)
id: Union[int, None] = None
vessel_id: int
departure_port_id: Union[int, None] = None
Expand Down
13 changes: 9 additions & 4 deletions backend/bloom/domain/port.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@
# For compliance with python 3.9 syntax
from pydantic import BaseModel, ConfigDict
from shapely import Point, Polygon

from shapely.geometry import mapping, shape
from typing import Union


class Port(BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True)
model_config = ConfigDict(
arbitrary_types_allowed=True,
json_encoders = {
Point: lambda point: mapping(point),
Polygon: lambda polygon: mapping(polygon),
},
)
id: Union[int, None] = None
name: str
locode: str
Expand All @@ -20,4 +25,4 @@ class Port(BaseModel):
geometry_buffer: Union[Polygon, None] = None
has_excursion: bool = False
created_at: Union[datetime, None] = None
updated_at: Union[datetime, None] = None
updated_at: Union[datetime, None] = None
9 changes: 7 additions & 2 deletions backend/bloom/domain/segment.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions backend/bloom/domain/vessel_last_position.py
Original file line number Diff line number Diff line change
@@ -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
8 changes: 6 additions & 2 deletions backend/bloom/domain/vessel_position.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
9 changes: 7 additions & 2 deletions backend/bloom/domain/zone.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@
from typing import Union

from pydantic import BaseModel, ConfigDict
from shapely import Geometry, Point
from shapely import Geometry, Point, MultiPolygon,Polygon
from shapely.geometry import mapping, shape


class Zone(BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True)
model_config = ConfigDict(arbitrary_types_allowed=True,
json_encoders = {
#Point: lambda point: mapping(point) if point != None else None,
Geometry: lambda p: mapping(p),
},)
id: Union[int, None] = None
category: str
sub_category: Union[str, None] = None
Expand Down
12 changes: 12 additions & 0 deletions backend/bloom/domain/zone_category.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from datetime import datetime
from typing import Union

from pydantic import BaseModel, ConfigDict
from shapely import Geometry, Point, MultiPolygon,Polygon
from shapely.geometry import mapping, shape


class ZoneCategory(BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True)
category: str
sub_category: Union[str, None]
19 changes: 18 additions & 1 deletion backend/bloom/infra/repositories/repository_excursion.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -34,6 +34,23 @@ 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 not result:
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).scalar()
if not result:
return None
return ExcursionRepository.map_to_domain(result)

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)
Expand Down
2 changes: 1 addition & 1 deletion backend/bloom/infra/repositories/repository_port.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def __init__(self, session_factory: Callable) -> None:
self.session_factory = session_factory

def get_port_by_id(self, session: Session, port_id: int) -> Union[Port, None]:
entity = session.get(sql_model.Port, port_id).scalar()
entity = session.get(sql_model.Port, port_id)
if entity is not None:
return PortRepository.map_to_domain(entity)
else:
Expand Down
100 changes: 99 additions & 1 deletion backend/bloom/infra/repositories/repository_segment.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -9,10 +10,13 @@
from sqlalchemy import and_, or_, select, update, text
from sqlalchemy.orm import Session

from bloom.logger import logger
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:
Expand All @@ -35,6 +39,100 @@ 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
).join(
sql_model.Segment,
sql_model.Segment.excursion_id == sql_model.Excursion.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
).join(
sql_model.Segment,
sql_model.Excursion.id == sql_model.Segment.excursion_id
).filter(
sql_model.Segment.last_vessel_segment == True,
sql_model.Vessel.id == vessel_id,
)
result = session.execute(stmt).fetchone()
if result is not None :
return VesselLastPosition(
vessel=VesselRepository.map_to_domain(result[0]),
excursion_id=result[1],
position=to_shape(result[2]),
timestamp=result[3],
heading=result[4],
speed=result[5],
arrival=result[6],
)
else:
return []

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,
Expand Down Expand Up @@ -172,4 +270,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
)
)
27 changes: 27 additions & 0 deletions backend/bloom/infra/repositories/repository_vessel_position.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from datetime import datetime
from typing import Any, List, Union

import pandas as pd
from dependency_injector.providers import Callable
Expand All @@ -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:
Expand All @@ -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,
Expand Down
Loading
Loading