From b67533ec5d10366664f63b2c0662d4b2e6337bc4 Mon Sep 17 00:00:00 2001 From: "herve.le-bars" Date: Sat, 5 Oct 2024 22:58:14 +0200 Subject: [PATCH 1/8] init router metrics --- backend/bloom/routers/metrics.py | 28 ++++++++++++++++++++++++++++ backend/bloom/services/api.py | 3 +++ 2 files changed, 31 insertions(+) create mode 100644 backend/bloom/routers/metrics.py diff --git a/backend/bloom/routers/metrics.py b/backend/bloom/routers/metrics.py new file mode 100644 index 00000000..4ba8c7e2 --- /dev/null +++ b/backend/bloom/routers/metrics.py @@ -0,0 +1,28 @@ +from fastapi import APIRouter, Depends, Query +from redis import Redis +from bloom.config import settings +from bloom.container import UseCases +from pydantic import BaseModel, Field +from typing_extensions import Annotated, Literal +from datetime import datetime + +router = APIRouter() +redis_client = Redis(host=settings.redis_host, port=settings.redis_port, db=0) + +@router.get("/metrics/vessels-in-activity/total", tags=['metrics']) +def read_metrics_vessels_in_activity_total(start_at: datetime, end_at: datetime = None): + pass + +@router.get("/metrics/zone-visited/total", tags=['metrics']) +def read_metrics_vessels_in_activity_total(start_at: datetime, end_at: datetime = None): + pass + +@router.get("/metrics/vessels/{vessel_id}/visits/{visit_type}", tags=['metrics']) +def read_metrics_vessels_visits_by_visit_type( + vessel_id: int, + visit_type: str, + start_at: datetime, + end_at: datetime = None, + limit: int = 10, + orderBy: str = 'DESC'): + pass \ No newline at end of file diff --git a/backend/bloom/services/api.py b/backend/bloom/services/api.py index be15b3e1..41dcf74d 100644 --- a/backend/bloom/services/api.py +++ b/backend/bloom/services/api.py @@ -2,6 +2,8 @@ from fastapi import Request from fastapi.security import APIKeyHeader +from bloom.routers.metrics import router as router_metrics + header_scheme = APIKeyHeader(name="x-key") import redis @@ -18,6 +20,7 @@ app = FastAPI() +app.include_router(router_metrics) def check_apikey(key:str): if key != settings.api_key : From e9630e59895b60673f2c2dfe7d336a062eff79ac Mon Sep 17 00:00:00 2001 From: "herve.le-bars" Date: Sun, 6 Oct 2024 01:54:03 +0200 Subject: [PATCH 2/8] + endpoint /metrics/vessels-in-activity start_at|end_at|limit|order_by --- backend/bloom/routers/metrics.py | 88 +++++++++++++++++++++++++++++--- 1 file changed, 81 insertions(+), 7 deletions(-) diff --git a/backend/bloom/routers/metrics.py b/backend/bloom/routers/metrics.py index 4ba8c7e2..26196173 100644 --- a/backend/bloom/routers/metrics.py +++ b/backend/bloom/routers/metrics.py @@ -3,18 +3,92 @@ from bloom.config import settings from bloom.container import UseCases from pydantic import BaseModel, Field -from typing_extensions import Annotated, Literal -from datetime import datetime +from typing_extensions import Annotated, Literal, Optional +from datetime import datetime, timedelta +from sqlalchemy import select, func, and_, or_ +from bloom.infra.database import sql_model +from bloom.domain.segment import Segment +from bloom.infra.repositories.repository_segment import SegmentRepository +import json +from pydantic import BaseModel, ConfigDict router = APIRouter() redis_client = Redis(host=settings.redis_host, port=settings.redis_port, db=0) -@router.get("/metrics/vessels-in-activity/total", tags=['metrics']) -def read_metrics_vessels_in_activity_total(start_at: datetime, end_at: datetime = None): - pass -@router.get("/metrics/zone-visited/total", tags=['metrics']) -def read_metrics_vessels_in_activity_total(start_at: datetime, end_at: datetime = None): +class ResponseMetricsVesselInActiviySchema(BaseModel): + id: int + mmsi: int + ship_name: str + width: Optional[float] = None + length: Optional[float] = None + country_iso3: Optional[str] = None + type: Optional[str] = None + imo: Optional[int] = None + cfr: Optional[str] = None + external_marking: Optional[str] = None + ircs: Optional[str] = None + home_port_id: Optional[int] = None + details: Optional[str] = None + tracking_activated: Optional[bool] + tracking_status: Optional[str] + length_class: Optional[str] + check: Optional[str] + total_time_at_sea: timedelta + +@router.get("/metrics/vessels-in-activity", + response_model=list[ResponseMetricsVesselInActiviySchema], + tags=['metrics']) +def read_metrics_vessels_in_activity_total(start_at: datetime, + end_at: datetime = datetime.now(), + limit: int = None, + order_by: str = 'DESC' + ): + use_cases = UseCases() + db = use_cases.db() + with db.session() as session: + stmt=select(sql_model.Vessel.id, + sql_model.Vessel.mmsi, + sql_model.Vessel.ship_name, + sql_model.Vessel.width, + sql_model.Vessel.length, + sql_model.Vessel.country_iso3, + sql_model.Vessel.type, + sql_model.Vessel.imo, + sql_model.Vessel.cfr, + sql_model.Vessel.external_marking, + sql_model.Vessel.ircs, + sql_model.Vessel.home_port_id, + sql_model.Vessel.details, + sql_model.Vessel.tracking_activated, + sql_model.Vessel.tracking_status, + sql_model.Vessel.length_class, + sql_model.Vessel.check, + func.sum(sql_model.Excursion.total_time_at_sea).label("total_time_at_sea") + )\ + .select_from(sql_model.Segment)\ + .join(sql_model.Excursion, sql_model.Segment.excursion_id == sql_model.Excursion.id)\ + .join(sql_model.Vessel, sql_model.Excursion.vessel_id == sql_model.Vessel.id)\ + .where( + or_( + sql_model.Excursion.arrival_at.between(start_at,end_at), + and_(sql_model.Excursion.departure_at <= end_at, + sql_model.Excursion.arrival_at == None)) + )\ + .group_by(sql_model.Vessel.id,sql_model.Excursion.total_time_at_sea) + stmt = stmt.limit(limit) if limit != None else stmt + + stmt = stmt.order_by(sql_model.Excursion.total_time_at_sea.asc())\ + if order_by.upper() == 'ASC' \ + else stmt.order_by(sql_model.Excursion.total_time_at_sea.desc()) + return session.execute(stmt).all() + +@router.get("/metrics/zone-visited", + tags=['metrics'] ) +def read_metrics_vessels_in_activity_total(start_at: datetime, + end_at: datetime = None, + limit: int = None, + order_by: str = 'DESC'): pass @router.get("/metrics/vessels/{vessel_id}/visits/{visit_type}", tags=['metrics']) From 3c2be7cd500a50dce2a6261a9a0d93e91be325fd Mon Sep 17 00:00:00 2001 From: "herve.le-bars" Date: Sun, 6 Oct 2024 15:48:36 +0200 Subject: [PATCH 3/8] add /metrics/zones/{zone_id}/visiting-time-by-vessel --- backend/bloom/domain/metrics.py | 46 +++++++++++++++++ backend/bloom/routers/metrics.py | 84 +++++++++++++++++++++++--------- 2 files changed, 108 insertions(+), 22 deletions(-) create mode 100644 backend/bloom/domain/metrics.py diff --git a/backend/bloom/domain/metrics.py b/backend/bloom/domain/metrics.py new file mode 100644 index 00000000..ae8ca5f8 --- /dev/null +++ b/backend/bloom/domain/metrics.py @@ -0,0 +1,46 @@ +from pydantic import BaseModel, ConfigDict +from typing_extensions import Annotated, Literal, Optional +from datetime import datetime, timedelta + +class ResponseMetricsVesselInActiviySchema(BaseModel): + id: int + mmsi: int + ship_name: str + width: Optional[float] = None + length: Optional[float] = None + country_iso3: Optional[str] = None + type: Optional[str] = None + imo: Optional[int] = None + cfr: Optional[str] = None + external_marking: Optional[str] = None + ircs: Optional[str] = None + home_port_id: Optional[int] = None + details: Optional[str] = None + tracking_activated: Optional[bool] + tracking_status: Optional[str] + length_class: Optional[str] + check: Optional[str] + total_time_at_sea: timedelta + +class ResponseMetricsZoneVisitedSchema(BaseModel): + id : int + category: str + sub_category: Optional[str] = None + name: str + visiting_duration: timedelta + +class ResponseMetricsZoneVisitingTimeByVesselSchema(BaseModel): + zone_id : int + zone_category: str + zone_sub_category: Optional[str] = None + zone_name: str + vessel_id : int + vessel_name: str + vessel_type: Optional[str] = None + vessel_length_class: Optional[str] = None + zone_visiting_time_by_vessel: timedelta + + +class TemporalRequest(BaseModel): + start_at: datetime + end_at: datetime = datetime.now() \ No newline at end of file diff --git a/backend/bloom/routers/metrics.py b/backend/bloom/routers/metrics.py index 26196173..d0c9ce28 100644 --- a/backend/bloom/routers/metrics.py +++ b/backend/bloom/routers/metrics.py @@ -7,34 +7,19 @@ from datetime import datetime, timedelta from sqlalchemy import select, func, and_, or_ from bloom.infra.database import sql_model -from bloom.domain.segment import Segment from bloom.infra.repositories.repository_segment import SegmentRepository import json from pydantic import BaseModel, ConfigDict +from bloom.domain.metrics import ResponseMetricsVesselInActiviySchema,\ + ResponseMetricsZoneVisitedSchema,\ + ResponseMetricsZoneVisitingTimeByVesselSchema,\ + DatetimeRangeRequest router = APIRouter() redis_client = Redis(host=settings.redis_host, port=settings.redis_port, db=0) -class ResponseMetricsVesselInActiviySchema(BaseModel): - id: int - mmsi: int - ship_name: str - width: Optional[float] = None - length: Optional[float] = None - country_iso3: Optional[str] = None - type: Optional[str] = None - imo: Optional[int] = None - cfr: Optional[str] = None - external_marking: Optional[str] = None - ircs: Optional[str] = None - home_port_id: Optional[int] = None - details: Optional[str] = None - tracking_activated: Optional[bool] - tracking_status: Optional[str] - length_class: Optional[str] - check: Optional[str] - total_time_at_sea: timedelta + @router.get("/metrics/vessels-in-activity", response_model=list[ResponseMetricsVesselInActiviySchema], @@ -84,12 +69,67 @@ def read_metrics_vessels_in_activity_total(start_at: datetime, return session.execute(stmt).all() @router.get("/metrics/zone-visited", + response_model=list[ResponseMetricsZoneVisitedSchema], tags=['metrics'] ) def read_metrics_vessels_in_activity_total(start_at: datetime, - end_at: datetime = None, + end_at: datetime = datetime.now(), limit: int = None, order_by: str = 'DESC'): - pass + use_cases = UseCases() + db = use_cases.db() + with db.session() as session: + stmt=select( + sql_model.Zone.id.label("zone_id"), + sql_model.Zone.category.label("zone_category"), + sql_model.Zone.sub_category.label("zone_sub_category"), + sql_model.Zone.name.label("zone_name"), + func.sum(sql_model.Segment.segment_duration).label("visiting_duration") + )\ + .where( + or_( + sql_model.Segment.timestamp_start.between(start_at,end_at), + sql_model.Segment.timestamp_end.between(start_at,end_at),) + )\ + .group_by(sql_model.Zone.id) + stmt = stmt.limit(limit) if limit != None else stmt + stmt = stmt.order_by("visiting_duration")\ + if order_by.upper() == 'ASC' \ + else stmt.order_by("visiting_duration") + return session.execute(stmt).all() + +@router.get("/metrics/zones/{zone_id}/visiting-time-by-vessel", + response_model=list[ResponseMetricsZoneVisitingTimeByVesselSchema], + tags=['metrics']) +def read_metrics_zone_visiting_time_by_vessel( + zone_id: int, + start_at: datetime, + end_at: datetime = datetime.now(), + limit: int = None, + order_by: str = 'DESC'): + use_cases = UseCases() + db = use_cases.db() + with db.session() as session: + stmt=select( + sql_model.Zone.id.label("zone_id"), + sql_model.Zone.category.label("zone_category"), + sql_model.Zone.sub_category.label("zone_sub_category"), + sql_model.Zone.name.label("zone_name"), + sql_model.Vessel.id.label("vessel_id"), + sql_model.Vessel.ship_name.label("vessel_name"), + sql_model.Vessel.type.label("vessel_type"), + sql_model.Vessel.length_class.label("vessel_length_class"), + func.sum(sql_model.Segment.segment_duration).label("zone_visiting_time_by_vessel") + )\ + .select_from(sql_model.Zone)\ + .join(sql_model.RelSegmentZone, sql_model.RelSegmentZone.zone_id == sql_model.Zone.id)\ + .join(sql_model.Segment, sql_model.RelSegmentZone.segment_id == sql_model.Segment.id)\ + .join(sql_model.Excursion, sql_model.Excursion.id == sql_model.Segment.excursion_id)\ + .join(sql_model.Vessel, sql_model.Excursion.vessel_id == sql_model.Vessel.id)\ + .where(sql_model.Zone.id == zone_id)\ + .group_by(sql_model.Zone.id,sql_model.Vessel.id) + return session.execute(stmt).all() + + @router.get("/metrics/vessels/{vessel_id}/visits/{visit_type}", tags=['metrics']) def read_metrics_vessels_visits_by_visit_type( From d381cd54c10d0679d27b345c3e1ec58cf4f243e1 Mon Sep 17 00:00:00 2001 From: RV Date: Mon, 7 Oct 2024 15:52:46 +0200 Subject: [PATCH 4/8] =?UTF-8?q?d=C3=A9but=20refacto=20+=20datetimerange=20?= =?UTF-8?q?+=20auth?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/bloom/domain/api.py | 59 ++++++++++++++++++++++++++++++++ backend/bloom/domain/metrics.py | 10 +++--- backend/bloom/routers/metrics.py | 57 +++++++++++++++--------------- backend/bloom/services/api.py | 39 +++++++++++---------- 4 files changed, 113 insertions(+), 52 deletions(-) create mode 100644 backend/bloom/domain/api.py diff --git a/backend/bloom/domain/api.py b/backend/bloom/domain/api.py new file mode 100644 index 00000000..1ce9adf8 --- /dev/null +++ b/backend/bloom/domain/api.py @@ -0,0 +1,59 @@ +from fastapi import Request +from pydantic import BaseModel, ConfigDict, Field,conint +from typing import Generic,TypeVar, List +from typing_extensions import Annotated, Literal, Optional +from datetime import datetime, timedelta +from enum import Enum +from pydantic.generics import GenericModel +from fastapi.security import APIKeyHeader + +## Reference for pagination design +## https://jayhawk24.hashnode.dev/how-to-implement-pagination-in-fastapi-feat-sqlalchemy +X_API_KEY_HEADER=APIKeyHeader(name="x-key") + +class DatetimeRangeRequest(BaseModel): + start_at: datetime = datetime.now()-timedelta(days=7) + end_at: datetime = datetime.now() + +class OrderByEnum(str, Enum): + ascending = "ASC" + descending = "DESC" + +class OrderByRequest(BaseModel): + order: OrderByEnum = OrderByEnum.ascending + +class PaginatedRequest(BaseModel): + offset: int|None = 0 + limit: int|None = 100 + order_by: OrderByRequest = OrderByEnum.ascending + + +class PageParams(BaseModel): + """ Request query params for paginated API. """ + offset: conint(ge=0) = 0 + limit: conint(ge=1, le=1000) = 100 + +T = TypeVar("T") + +class PagedResponseSchema(GenericModel,Generic[T]): + total: int + limit: int + offset: int + next: str|None + previous: str|None + results: List[T] + +def paginate(request: Request, page_params: PageParams, query, ResponseSchema: BaseModel) -> PagedResponseSchema[T]: + """Paginate the query.""" + + print(f"{request.url.scheme}://{request.client}/{request.url.path}") + paginated_query = query.offset((page_params.offset) * page_params.limit).limit(page_params.limit).all() + + return PagedResponseSchema( + total=query.count(), + offset=page_params.offset, + limit=page_params.limit, + next="", + previous="", + results=[ResponseSchema.from_orm(item) for item in paginated_query], + ) \ No newline at end of file diff --git a/backend/bloom/domain/metrics.py b/backend/bloom/domain/metrics.py index ae8ca5f8..5a505915 100644 --- a/backend/bloom/domain/metrics.py +++ b/backend/bloom/domain/metrics.py @@ -1,8 +1,11 @@ from pydantic import BaseModel, ConfigDict +from typing import Generic,TypeVar, List from typing_extensions import Annotated, Literal, Optional from datetime import datetime, timedelta +from enum import Enum class ResponseMetricsVesselInActiviySchema(BaseModel): + model_config = ConfigDict(from_attributes=True) id: int mmsi: int ship_name: str @@ -38,9 +41,4 @@ class ResponseMetricsZoneVisitingTimeByVesselSchema(BaseModel): vessel_name: str vessel_type: Optional[str] = None vessel_length_class: Optional[str] = None - zone_visiting_time_by_vessel: timedelta - - -class TemporalRequest(BaseModel): - start_at: datetime - end_at: datetime = datetime.now() \ No newline at end of file + zone_visiting_time_by_vessel: timedelta \ No newline at end of file diff --git a/backend/bloom/routers/metrics.py b/backend/bloom/routers/metrics.py index d0c9ce28..ec9b19b6 100644 --- a/backend/bloom/routers/metrics.py +++ b/backend/bloom/routers/metrics.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends, Query +from fastapi import APIRouter, Depends, Query, Body,Request from redis import Redis from bloom.config import settings from bloom.container import UseCases @@ -9,11 +9,13 @@ from bloom.infra.database import sql_model from bloom.infra.repositories.repository_segment import SegmentRepository import json -from pydantic import BaseModel, ConfigDict -from bloom.domain.metrics import ResponseMetricsVesselInActiviySchema,\ - ResponseMetricsZoneVisitedSchema,\ - ResponseMetricsZoneVisitingTimeByVesselSchema,\ - DatetimeRangeRequest +from bloom.domain.metrics import (ResponseMetricsVesselInActiviySchema, + ResponseMetricsZoneVisitedSchema, + ResponseMetricsZoneVisitingTimeByVesselSchema) +from bloom.domain.api import ( DatetimeRangeRequest, + PaginatedRequest,OrderByRequest,OrderByEnum, + paginate,PagedResponseSchema,PageParams, + X_API_KEY_HEADER) router = APIRouter() redis_client = Redis(host=settings.redis_host, port=settings.redis_port, db=0) @@ -24,10 +26,11 @@ @router.get("/metrics/vessels-in-activity", response_model=list[ResponseMetricsVesselInActiviySchema], tags=['metrics']) -def read_metrics_vessels_in_activity_total(start_at: datetime, - end_at: datetime = datetime.now(), - limit: int = None, - order_by: str = 'DESC' +def read_metrics_vessels_in_activity_total(request: Request, + datetime_range: DatetimeRangeRequest = Depends(), + #pagination: PageParams = Depends(), + order: OrderByRequest = Depends(), + auth: str = Depends(X_API_KEY_HEADER), ): use_cases = UseCases() db = use_cases.db() @@ -56,25 +59,24 @@ def read_metrics_vessels_in_activity_total(start_at: datetime, .join(sql_model.Vessel, sql_model.Excursion.vessel_id == sql_model.Vessel.id)\ .where( or_( - sql_model.Excursion.arrival_at.between(start_at,end_at), - and_(sql_model.Excursion.departure_at <= end_at, + sql_model.Excursion.arrival_at.between(datetime_range.start_at,datetime_range.end_at), + and_(sql_model.Excursion.departure_at <= datetime_range.end_at, sql_model.Excursion.arrival_at == None)) )\ .group_by(sql_model.Vessel.id,sql_model.Excursion.total_time_at_sea) - stmt = stmt.limit(limit) if limit != None else stmt + #stmt = stmt.limit(pagination.limit) if pagination.limit != None else stmt stmt = stmt.order_by(sql_model.Excursion.total_time_at_sea.asc())\ - if order_by.upper() == 'ASC' \ + if order.order == OrderByEnum.ascending \ else stmt.order_by(sql_model.Excursion.total_time_at_sea.desc()) return session.execute(stmt).all() @router.get("/metrics/zone-visited", response_model=list[ResponseMetricsZoneVisitedSchema], tags=['metrics'] ) -def read_metrics_vessels_in_activity_total(start_at: datetime, - end_at: datetime = datetime.now(), - limit: int = None, - order_by: str = 'DESC'): +def read_metrics_vessels_in_activity_total(datetime_range: DatetimeRangeRequest = Depends(), + pagination: PaginatedRequest = Depends(), + auth: str = Depends(X_API_KEY_HEADER),): use_cases = UseCases() db = use_cases.db() with db.session() as session: @@ -87,13 +89,13 @@ def read_metrics_vessels_in_activity_total(start_at: datetime, )\ .where( or_( - sql_model.Segment.timestamp_start.between(start_at,end_at), - sql_model.Segment.timestamp_end.between(start_at,end_at),) + sql_model.Segment.timestamp_start.between(datetime_range.start_at,datetime_range.end_at), + sql_model.Segment.timestamp_end.between(datetime_range.start_at,datetime_range.end_at),) )\ .group_by(sql_model.Zone.id) stmt = stmt.limit(limit) if limit != None else stmt stmt = stmt.order_by("visiting_duration")\ - if order_by.upper() == 'ASC' \ + if pagination.order_by == OrderByRequest.ascending \ else stmt.order_by("visiting_duration") return session.execute(stmt).all() @@ -101,11 +103,11 @@ def read_metrics_vessels_in_activity_total(start_at: datetime, response_model=list[ResponseMetricsZoneVisitingTimeByVesselSchema], tags=['metrics']) def read_metrics_zone_visiting_time_by_vessel( + datetime_range: Annotated[DatetimeRangeRequest,Body()], zone_id: int, - start_at: datetime, - end_at: datetime = datetime.now(), limit: int = None, - order_by: str = 'DESC'): + order_by: str = 'DESC', + auth: str = Depends(X_API_KEY_HEADER),): use_cases = UseCases() db = use_cases.db() with db.session() as session: @@ -135,8 +137,7 @@ def read_metrics_zone_visiting_time_by_vessel( def read_metrics_vessels_visits_by_visit_type( vessel_id: int, visit_type: str, - start_at: datetime, - end_at: datetime = None, - limit: int = 10, - orderBy: str = 'DESC'): + datetime_range: DatetimeRangeRequest = Depends(), + pagination: PaginatedRequest = Depends(), + auth: str = Depends(X_API_KEY_HEADER),): pass \ No newline at end of file diff --git a/backend/bloom/services/api.py b/backend/bloom/services/api.py index 41dcf74d..55ef39a9 100644 --- a/backend/bloom/services/api.py +++ b/backend/bloom/services/api.py @@ -3,8 +3,10 @@ from fastapi.security import APIKeyHeader from bloom.routers.metrics import router as router_metrics - -header_scheme = APIKeyHeader(name="x-key") +from bloom.domain.api import ( DatetimeRangeRequest, + PaginatedRequest,OrderByRequest, + paginate,PagedResponseSchema,PageParams, + X_API_KEY_HEADER) import redis import json @@ -28,13 +30,14 @@ def check_apikey(key:str): return True @app.get("/cache/all/flush") -async def cache_all_flush(request:Request,key: str = Depends(header_scheme)): +async def cache_all_flush(request:Request,key: str = Depends(X_API_KEY_HEADER)): check_apikey(key) rd.flushall() return {"code":0} @app.get("/vessels") -async def list_vessels(nocache:bool=False,key: str = Depends(header_scheme)): +async def list_vessels(nocache:bool=False,key: str = Depends(X_API_KEY_HEADER)): + print(f"KEY:{key}") check_apikey(key) endpoint=f"/vessels" cache= rd.get(endpoint) @@ -57,7 +60,7 @@ async def list_vessels(nocache:bool=False,key: str = Depends(header_scheme)): return json_data @app.get("/vessels/{vessel_id}") -async def get_vessel(vessel_id: int,key: str = Depends(header_scheme)): +async def get_vessel(vessel_id: int,key: str = Depends(X_API_KEY_HEADER)): check_apikey(key) use_cases = UseCases() vessel_repository = use_cases.vessel_repository() @@ -66,7 +69,7 @@ async def get_vessel(vessel_id: int,key: str = Depends(header_scheme)): return vessel_repository.get_vessel_by_id(session,vessel_id) @app.get("/vessels/all/positions/last") -async def list_all_vessel_last_position(nocache:bool=False,key: str = Depends(header_scheme)): +async def list_all_vessel_last_position(nocache:bool=False,key: str = Depends(X_API_KEY_HEADER)): check_apikey(key) endpoint=f"/vessels/all/positions/last" cache= rd.get(endpoint) @@ -89,7 +92,7 @@ async def list_all_vessel_last_position(nocache:bool=False,key: str = Depends(he return json_data @app.get("/vessels/{vessel_id}/positions/last") -async def get_vessel_last_position(vessel_id: int, nocache:bool=False,key: str = Depends(header_scheme)): +async def get_vessel_last_position(vessel_id: int, nocache:bool=False,key: str = Depends(X_API_KEY_HEADER)): check_apikey(key) endpoint=f"/vessels/{vessel_id}/positions/last" cache= rd.get(endpoint) @@ -112,7 +115,7 @@ async def get_vessel_last_position(vessel_id: int, nocache:bool=False,key: str = return json_data @app.get("/vessels/{vessel_id}/excursions") -async def list_vessel_excursions(vessel_id: int, nocache:bool=False,key: str = Depends(header_scheme)): +async def list_vessel_excursions(vessel_id: int, nocache:bool=False,key: str = Depends(X_API_KEY_HEADER)): check_apikey(key) endpoint=f"/vessels/{vessel_id}/excursions" cache= rd.get(endpoint) @@ -136,7 +139,7 @@ async def list_vessel_excursions(vessel_id: int, nocache:bool=False,key: str = D @app.get("/vessels/{vessel_id}/excursions/{excursions_id}") -async def get_vessel_excursion(vessel_id: int,excursions_id: int,key: str = Depends(header_scheme)): +async def get_vessel_excursion(vessel_id: int,excursions_id: int,key: str = Depends(X_API_KEY_HEADER)): check_apikey(key) use_cases = UseCases() excursion_repository = use_cases.excursion_repository() @@ -146,7 +149,7 @@ async def get_vessel_excursion(vessel_id: int,excursions_id: int,key: str = Depe @app.get("/vessels/{vessel_id}/excursions/{excursions_id}/segments") -async def list_vessel_excursion_segments(vessel_id: int,excursions_id: int,key: str = Depends(header_scheme)): +async def list_vessel_excursion_segments(vessel_id: int,excursions_id: int,key: str = Depends(X_API_KEY_HEADER)): check_apikey(key) use_cases = UseCases() segment_repository = use_cases.segment_repository() @@ -155,7 +158,7 @@ async def list_vessel_excursion_segments(vessel_id: int,excursions_id: int,key: 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,key: str = Depends(header_scheme)): +async def get_vessel_excursion_segment(vessel_id: int,excursions_id: int, segment_id:int,key: str = Depends(X_API_KEY_HEADER)): check_apikey(key) use_cases = UseCases() segment_repository = use_cases.segment_repository() @@ -164,7 +167,7 @@ async def get_vessel_excursion_segment(vessel_id: int,excursions_id: int, segmen 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,nocache:bool=False,key: str = Depends(header_scheme)): +async def list_ports(request:Request,nocache:bool=False,key: str = Depends(X_API_KEY_HEADER)): check_apikey(key) endpoint=f"/ports" cache= rd.get(endpoint) @@ -188,7 +191,7 @@ async def list_ports(request:Request,nocache:bool=False,key: str = Depends(heade @app.get("/ports/{port_id}") -async def get_port(port_id:int,key: str = Depends(header_scheme)): +async def get_port(port_id:int,key: str = Depends(X_API_KEY_HEADER)): check_apikey(key) use_cases = UseCases() port_repository = use_cases.port_repository() @@ -197,7 +200,7 @@ async def get_port(port_id:int,key: str = Depends(header_scheme)): return port_repository.get_port_by_id(session,port_id) @app.get("/zones") -async def list_zones(request:Request,nocache:bool=False,key: str = Depends(header_scheme)): +async def list_zones(request:Request,nocache:bool=False,key: str = Depends(X_API_KEY_HEADER)): check_apikey(key) endpoint=f"/zones" cache= rd.get(endpoint) @@ -220,7 +223,7 @@ async def list_zones(request:Request,nocache:bool=False,key: str = Depends(heade return json_data @app.get("/zones/all/categories") -async def list_zone_categories(request:Request,nocache:bool=False,key: str = Depends(header_scheme)): +async def list_zone_categories(request:Request,nocache:bool=False,key: str = Depends(X_API_KEY_HEADER)): check_apikey(key) endpoint=f"/zones/all/categories" cache= rd.get(endpoint) @@ -243,7 +246,7 @@ async def list_zone_categories(request:Request,nocache:bool=False,key: str = Dep return json_data @app.get("/zones/by-category/{category}/by-sub-category/{sub}") -async def get_zone_all_by_category(category:str="all",sub:str=None,nocache:bool=False,key: str = Depends(header_scheme)): +async def get_zone_all_by_category(category:str="all",sub:str=None,nocache:bool=False,key: str = Depends(X_API_KEY_HEADER)): check_apikey(key) endpoint=f"/zones/by-category/{category}/by-sub-category/{sub}" cache= rd.get(endpoint) @@ -266,7 +269,7 @@ async def get_zone_all_by_category(category:str="all",sub:str=None,nocache:bool= return json_data @app.get("/zones/by-category/{category}") -async def get_zone_all_by_category(category:str="all",nocache:bool=False,key: str = Depends(header_scheme)): +async def get_zone_all_by_category(category:str="all",nocache:bool=False,key: str = Depends(X_API_KEY_HEADER)): check_apikey(key) endpoint=f"/zones/by-category/{category}" cache= rd.get(endpoint) @@ -289,7 +292,7 @@ async def get_zone_all_by_category(category:str="all",nocache:bool=False,key: st return json_data @app.get("/zones/{zones_id}") -async def get_zone(zones_id:int,key: str = Depends(header_scheme)): +async def get_zone(zones_id:int,key: str = Depends(X_API_KEY_HEADER)): check_apikey(key) use_cases = UseCases() zone_repository = use_cases.zone_repository() From e9c7c2b9240fbf3806d90803fad192d891185b8f Mon Sep 17 00:00:00 2001 From: RV Date: Mon, 7 Oct 2024 16:27:59 +0200 Subject: [PATCH 5/8] refacto: +++ (ports, vessels, zones) --- backend/bloom/domain/api.py | 8 +- backend/bloom/routers/metrics.py | 8 +- backend/bloom/routers/ports.py | 63 +++++++ backend/bloom/routers/vessels.py | 166 ++++++++++++++++++ backend/bloom/routers/zones.py | 133 +++++++++++++++ backend/bloom/services/api.py | 278 +------------------------------ 6 files changed, 381 insertions(+), 275 deletions(-) create mode 100644 backend/bloom/routers/ports.py create mode 100644 backend/bloom/routers/vessels.py create mode 100644 backend/bloom/routers/zones.py diff --git a/backend/bloom/domain/api.py b/backend/bloom/domain/api.py index 1ce9adf8..18522089 100644 --- a/backend/bloom/domain/api.py +++ b/backend/bloom/domain/api.py @@ -1,4 +1,4 @@ -from fastapi import Request +from fastapi import Request, HTTPException from pydantic import BaseModel, ConfigDict, Field,conint from typing import Generic,TypeVar, List from typing_extensions import Annotated, Literal, Optional @@ -6,11 +6,17 @@ from enum import Enum from pydantic.generics import GenericModel from fastapi.security import APIKeyHeader +from bloom.config import settings ## Reference for pagination design ## https://jayhawk24.hashnode.dev/how-to-implement-pagination-in-fastapi-feat-sqlalchemy X_API_KEY_HEADER=APIKeyHeader(name="x-key") +def check_apikey(key:str): + if key != settings.api_key : + raise HTTPException(status_code=401, detail="Unauthorized") + return True + class DatetimeRangeRequest(BaseModel): start_at: datetime = datetime.now()-timedelta(days=7) end_at: datetime = datetime.now() diff --git a/backend/bloom/routers/metrics.py b/backend/bloom/routers/metrics.py index ec9b19b6..171d0a2c 100644 --- a/backend/bloom/routers/metrics.py +++ b/backend/bloom/routers/metrics.py @@ -25,7 +25,7 @@ @router.get("/metrics/vessels-in-activity", response_model=list[ResponseMetricsVesselInActiviySchema], - tags=['metrics']) + tags=['Metrics']) def read_metrics_vessels_in_activity_total(request: Request, datetime_range: DatetimeRangeRequest = Depends(), #pagination: PageParams = Depends(), @@ -73,7 +73,7 @@ def read_metrics_vessels_in_activity_total(request: Request, @router.get("/metrics/zone-visited", response_model=list[ResponseMetricsZoneVisitedSchema], - tags=['metrics'] ) + tags=['Metrics'] ) def read_metrics_vessels_in_activity_total(datetime_range: DatetimeRangeRequest = Depends(), pagination: PaginatedRequest = Depends(), auth: str = Depends(X_API_KEY_HEADER),): @@ -101,7 +101,7 @@ def read_metrics_vessels_in_activity_total(datetime_range: DatetimeRangeRequest @router.get("/metrics/zones/{zone_id}/visiting-time-by-vessel", response_model=list[ResponseMetricsZoneVisitingTimeByVesselSchema], - tags=['metrics']) + tags=['Metrics']) def read_metrics_zone_visiting_time_by_vessel( datetime_range: Annotated[DatetimeRangeRequest,Body()], zone_id: int, @@ -133,7 +133,7 @@ def read_metrics_zone_visiting_time_by_vessel( -@router.get("/metrics/vessels/{vessel_id}/visits/{visit_type}", tags=['metrics']) +@router.get("/metrics/vessels/{vessel_id}/visits/{visit_type}", tags=['Metrics']) def read_metrics_vessels_visits_by_visit_type( vessel_id: int, visit_type: str, diff --git a/backend/bloom/routers/ports.py b/backend/bloom/routers/ports.py new file mode 100644 index 00000000..ed0d405b --- /dev/null +++ b/backend/bloom/routers/ports.py @@ -0,0 +1,63 @@ +from fastapi import APIRouter, Depends, HTTPException, Request +from redis import Redis +from bloom.config import settings +from bloom.container import UseCases +from pydantic import BaseModel, Field +from typing_extensions import Annotated, Literal, Optional +from datetime import datetime, timedelta +import time +import redis +import json +from sqlalchemy import select, func, and_, or_ +from bloom.infra.database import sql_model +from bloom.infra.repositories.repository_segment import SegmentRepository +from bloom.config import settings +from bloom.container import UseCases +from bloom.domain.vessel import Vessel +from bloom.logger import logger +from bloom.domain.metrics import (ResponseMetricsVesselInActiviySchema, + ResponseMetricsZoneVisitedSchema, + ResponseMetricsZoneVisitingTimeByVesselSchema) +from bloom.domain.api import ( DatetimeRangeRequest, + PaginatedRequest,OrderByRequest,OrderByEnum, + paginate,PagedResponseSchema,PageParams, + X_API_KEY_HEADER,check_apikey) +from bloom.config import settings + +router = APIRouter() +rd = redis.Redis(host=settings.redis_host, port=settings.redis_port, db=0) + +@router.get("/ports", + tags=['Ports']) +async def list_ports(request:Request,nocache:bool=False,key: str = Depends(X_API_KEY_HEADER)): + check_apikey(key) + endpoint=f"/ports" + cache= rd.get(endpoint) + start = time.time() + if cache and not nocache: + logger.debug(f"{endpoint} cached ({settings.redis_cache_expiration})s") + payload=json.loads(cache) + logger.debug(f"{endpoint} elapsed Time: {time.time()-start}") + return payload + else: + use_cases = UseCases() + port_repository = use_cases.port_repository() + db = use_cases.db() + with db.session() as session: + json_data = [json.loads(p.model_dump_json() if p else "{}") + for p in port_repository.get_all_ports(session)] + rd.set(endpoint, json.dumps(json_data)) + rd.expire(endpoint,settings.redis_cache_expiration) + logger.debug(f"{endpoint} elapsed Time: {time.time()-start}") + return json_data + + +@router.get("/ports/{port_id}", + tags=['Ports']) +async def get_port(port_id:int,key: str = Depends(X_API_KEY_HEADER)): + check_apikey(key) + use_cases = UseCases() + port_repository = use_cases.port_repository() + db = use_cases.db() + with db.session() as session: + return port_repository.get_port_by_id(session,port_id) \ No newline at end of file diff --git a/backend/bloom/routers/vessels.py b/backend/bloom/routers/vessels.py new file mode 100644 index 00000000..e78ffa6e --- /dev/null +++ b/backend/bloom/routers/vessels.py @@ -0,0 +1,166 @@ +from fastapi import APIRouter, Depends, HTTPException +from redis import Redis +from bloom.config import settings +from bloom.container import UseCases +from pydantic import BaseModel, Field +from typing_extensions import Annotated, Literal, Optional +from datetime import datetime, timedelta +import time +import redis +import json +from sqlalchemy import select, func, and_, or_ +from bloom.infra.database import sql_model +from bloom.infra.repositories.repository_segment import SegmentRepository +from bloom.config import settings +from bloom.container import UseCases +from bloom.domain.vessel import Vessel +from bloom.logger import logger +from bloom.domain.metrics import (ResponseMetricsVesselInActiviySchema, + ResponseMetricsZoneVisitedSchema, + ResponseMetricsZoneVisitingTimeByVesselSchema) +from bloom.domain.api import ( DatetimeRangeRequest, + PaginatedRequest,OrderByRequest,OrderByEnum, + paginate,PagedResponseSchema,PageParams, + X_API_KEY_HEADER,check_apikey) + +router = APIRouter() +rd = redis.Redis(host=settings.redis_host, port=settings.redis_port, db=0) + +@router.get("/vessels", + tags=['Vessels']) +async def list_vessels(nocache:bool=False,key: str = Depends(X_API_KEY_HEADER)): + print(f"KEY:{key}") + check_apikey(key) + endpoint=f"/vessels" + cache= rd.get(endpoint) + start = time.time() + if cache and not nocache: + logger.debug(f"{endpoint} cached ({settings.redis_cache_expiration})s") + payload=json.loads(cache) + logger.debug(f"{endpoint} elapsed Time: {time.time()-start}") + return payload + else: + use_cases = UseCases() + vessel_repository = use_cases.vessel_repository() + db = use_cases.db() + with db.session() as session: + + json_data = [json.loads(v.model_dump_json() if v else "{}") + for v in vessel_repository.get_vessels_list(session)] + rd.set(endpoint, json.dumps(json_data)) + rd.expire(endpoint,settings.redis_cache_expiration) + return json_data + +@router.get("/vessels/{vessel_id}", + tags=['Vessels']) +async def get_vessel(vessel_id: int,key: str = Depends(X_API_KEY_HEADER)): + check_apikey(key) + use_cases = UseCases() + vessel_repository = use_cases.vessel_repository() + db = use_cases.db() + with db.session() as session: + return vessel_repository.get_vessel_by_id(session,vessel_id) + +@router.get("/vessels/all/positions/last", + tags=['Vessels']) +async def list_all_vessel_last_position(nocache:bool=False,key: str = Depends(X_API_KEY_HEADER)): + check_apikey(key) + endpoint=f"/vessels/all/positions/last" + cache= rd.get(endpoint) + start = time.time() + if cache and not nocache: + logger.debug(f"{endpoint} cached ({settings.redis_cache_expiration})s") + payload=json.loads(cache) + logger.debug(f"{endpoint} elapsed Time: {time.time()-start}") + return payload + else: + use_cases = UseCases() + segment_repository = use_cases.segment_repository() + db = use_cases.db() + with db.session() as session: + json_data = [json.loads(p.model_dump_json() if p else "{}") + for p in segment_repository.get_all_vessels_last_position(session)] + rd.set(endpoint, json.dumps(json_data)) + rd.expire(endpoint,settings.redis_cache_expiration) + logger.debug(f"{endpoint} elapsed Time: {time.time()-start}") + return json_data + +@router.get("/vessels/{vessel_id}/positions/last", + tags=['Vessels']) +async def get_vessel_last_position(vessel_id: int, nocache:bool=False,key: str = Depends(X_API_KEY_HEADER)): + check_apikey(key) + endpoint=f"/vessels/{vessel_id}/positions/last" + cache= rd.get(endpoint) + start = time.time() + if cache and not nocache: + logger.debug(f"{endpoint} cached ({settings.redis_cache_expiration})s") + payload=json.loads(cache) + logger.debug(f"{endpoint} elapsed Time: {time.time()-start}") + return payload + else: + use_cases = UseCases() + segment_repository = use_cases.segment_repository() + db = use_cases.db() + with db.session() as session: + result=segment_repository.get_vessel_last_position(session,vessel_id) + json_data = json.loads(result.model_dump_json() if result else "{}") + rd.set(endpoint, json.dumps(json_data)) + rd.expire(endpoint,settings.redis_cache_expiration) + logger.debug(f"{endpoint} elapsed Time: {time.time()-start}") + return json_data + +@router.get("/vessels/{vessel_id}/excursions", + tags=['Vessels']) +async def list_vessel_excursions(vessel_id: int, nocache:bool=False,key: str = Depends(X_API_KEY_HEADER)): + check_apikey(key) + endpoint=f"/vessels/{vessel_id}/excursions" + cache= rd.get(endpoint) + start = time.time() + if cache and not nocache: + logger.debug(f"{endpoint} cached ({settings.redis_cache_expiration})s") + payload=json.loads(cache) + logger.debug(f"{endpoint} elapsed Time: {time.time()-start}") + return payload + else: + use_cases = UseCases() + excursion_repository = use_cases.excursion_repository() + db = use_cases.db() + with db.session() as session: + json_data = [json.loads(p.model_dump_json() if p else "{}") + for p in excursion_repository.get_excursions_by_vessel_id(session,vessel_id)] + rd.set(endpoint, json.dumps(json_data)) + rd.expire(endpoint,settings.redis_cache_expiration) + logger.debug(f"{endpoint} elapsed Time: {time.time()-start}") + return json_data + + +@router.get("/vessels/{vessel_id}/excursions/{excursions_id}", + tags=['Vessels']) +async def get_vessel_excursion(vessel_id: int,excursions_id: int,key: str = Depends(X_API_KEY_HEADER)): + check_apikey(key) + 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) + + +@router.get("/vessels/{vessel_id}/excursions/{excursions_id}/segments", + tags=['Vessels']) +async def list_vessel_excursion_segments(vessel_id: int,excursions_id: int,key: str = Depends(X_API_KEY_HEADER)): + check_apikey(key) + 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) + +@router.get("/vessels/{vessel_id}/excursions/{excursions_id}/segments/{segment_id}", + tags=['Vessels']) +async def get_vessel_excursion_segment(vessel_id: int,excursions_id: int, segment_id:int,key: str = Depends(X_API_KEY_HEADER)): + check_apikey(key) + 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) \ No newline at end of file diff --git a/backend/bloom/routers/zones.py b/backend/bloom/routers/zones.py new file mode 100644 index 00000000..ad5dc9fc --- /dev/null +++ b/backend/bloom/routers/zones.py @@ -0,0 +1,133 @@ +from fastapi import APIRouter, Depends, HTTPException, Request +from redis import Redis +from bloom.config import settings +from bloom.container import UseCases +from pydantic import BaseModel, Field +from typing_extensions import Annotated, Literal, Optional +from datetime import datetime, timedelta +import time +import redis +import json +from sqlalchemy import select, func, and_, or_ +from bloom.infra.database import sql_model +from bloom.infra.repositories.repository_segment import SegmentRepository +from bloom.config import settings +from bloom.container import UseCases +from bloom.domain.vessel import Vessel +from bloom.logger import logger +from bloom.domain.metrics import (ResponseMetricsVesselInActiviySchema, + ResponseMetricsZoneVisitedSchema, + ResponseMetricsZoneVisitingTimeByVesselSchema) +from bloom.domain.api import ( DatetimeRangeRequest, + PaginatedRequest,OrderByRequest,OrderByEnum, + paginate,PagedResponseSchema,PageParams, + X_API_KEY_HEADER,check_apikey) + +router = APIRouter() +rd = redis.Redis(host=settings.redis_host, port=settings.redis_port, db=0) + +@router.get("/zones", + tags=["Zones"]) +async def list_zones(request:Request,nocache:bool=False,key: str = Depends(X_API_KEY_HEADER)): + check_apikey(key) + endpoint=f"/zones" + cache= rd.get(endpoint) + start = time.time() + if cache and not nocache: + logger.debug(f"{endpoint} cached ({settings.redis_cache_expiration})s") + payload=json.loads(cache) + logger.debug(f"{endpoint} elapsed Time: {time.time()-start}") + return payload + else: + use_cases = UseCases() + zone_repository = use_cases.zone_repository() + db = use_cases.db() + with db.session() as session: + json_data = [json.loads(z.model_dump_json() if z else "{}") + for z in zone_repository.get_all_zones(session)] + rd.set(endpoint, json.dumps(json_data)) + rd.expire(endpoint,settings.redis_cache_expiration) + logger.debug(f"{endpoint} elapsed Time: {time.time()-start}") + return json_data + +@router.get("/zones/all/categories", + tags=["Zones"]) +async def list_zone_categories(request:Request,nocache:bool=False,key: str = Depends(X_API_KEY_HEADER)): + check_apikey(key) + endpoint=f"/zones/all/categories" + cache= rd.get(endpoint) + start = time.time() + if cache and not nocache: + logger.debug(f"{endpoint} cached ({settings.redis_cache_expiration})s") + payload=json.loads(cache) + logger.debug(f"{endpoint} elapsed Time: {time.time()-start}") + return payload + else: + use_cases = UseCases() + zone_repository = use_cases.zone_repository() + db = use_cases.db() + with db.session() as session: + json_data = [json.loads(z.model_dump_json() if z else "{}") + for z in zone_repository.get_all_zone_categories(session)] + rd.set(endpoint, json.dumps(json_data)) + rd.expire(endpoint,settings.redis_cache_expiration) + logger.debug(f"{endpoint} elapsed Time: {time.time()-start}") + return json_data + +@router.get("/zones/by-category/{category}/by-sub-category/{sub}", + tags=["Zones"]) +async def get_zone_all_by_category(category:str="all",sub:str=None,nocache:bool=False,key: str = Depends(X_API_KEY_HEADER)): + check_apikey(key) + endpoint=f"/zones/by-category/{category}/by-sub-category/{sub}" + cache= rd.get(endpoint) + start = time.time() + if cache and not nocache: + logger.debug(f"{endpoint} cached ({settings.redis_cache_expiration})s") + payload=json.loads(cache) + logger.debug(f"{endpoint} elapsed Time: {time.time()-start}") + return payload + else: + use_cases = UseCases() + zone_repository = use_cases.zone_repository() + db = use_cases.db() + with db.session() as session: + json_data = [json.loads(z.model_dump_json() if z else "{}") + for z in zone_repository.get_all_zones_by_category(session,category if category != 'all' else None,sub)] + rd.set(endpoint, json.dumps(json_data)) + rd.expire(endpoint,settings.redis_cache_expiration) + logger.debug(f"{endpoint} elapsed Time: {time.time()-start}") + return json_data + +@router.get("/zones/by-category/{category}", + tags=["Zones"]) +async def get_zone_all_by_category(category:str="all",nocache:bool=False,key: str = Depends(X_API_KEY_HEADER)): + check_apikey(key) + endpoint=f"/zones/by-category/{category}" + cache= rd.get(endpoint) + start = time.time() + if cache and not nocache: + logger.debug(f"{endpoint} cached ({settings.redis_cache_expiration})s") + payload=json.loads(cache) + logger.debug(f"{endpoint} elapsed Time: {time.time()-start}") + return payload + else: + use_cases = UseCases() + zone_repository = use_cases.zone_repository() + db = use_cases.db() + with db.session() as session: + json_data = [json.loads(z.model_dump_json() if z else "{}") + for z in zone_repository.get_all_zones_by_category(session,category if category != 'all' else None)] + rd.set(endpoint, json.dumps(json_data)) + rd.expire(endpoint,settings.redis_cache_expiration) + logger.debug(f"{endpoint} elapsed Time: {time.time()-start}") + return json_data + +@router.get("/zones/{zones_id}", + tags=["Zones"]) +async def get_zone(zones_id:int,key: str = Depends(X_API_KEY_HEADER)): + check_apikey(key) + use_cases = UseCases() + zone_repository = use_cases.zone_repository() + db = use_cases.db() + with db.session() as session: + return zone_repository.get_zone_by_id(session,zones_id) \ No newline at end of file diff --git a/backend/bloom/services/api.py b/backend/bloom/services/api.py index 55ef39a9..099a2a05 100644 --- a/backend/bloom/services/api.py +++ b/backend/bloom/services/api.py @@ -3,10 +3,13 @@ from fastapi.security import APIKeyHeader from bloom.routers.metrics import router as router_metrics +from bloom.routers.vessels import router as router_vessels +from bloom.routers.ports import router as router_ports +from bloom.routers.zones import router as router_zones from bloom.domain.api import ( DatetimeRangeRequest, PaginatedRequest,OrderByRequest, paginate,PagedResponseSchema,PageParams, - X_API_KEY_HEADER) + X_API_KEY_HEADER,check_apikey) import redis import json @@ -23,11 +26,11 @@ app = FastAPI() app.include_router(router_metrics) +app.include_router(router_vessels) +app.include_router(router_ports) +app.include_router(router_zones) + -def check_apikey(key:str): - if key != settings.api_key : - raise HTTPException(status_code=401, detail="Unauthorized") - return True @app.get("/cache/all/flush") async def cache_all_flush(request:Request,key: str = Depends(X_API_KEY_HEADER)): @@ -35,271 +38,6 @@ async def cache_all_flush(request:Request,key: str = Depends(X_API_KEY_HEADER)): rd.flushall() return {"code":0} -@app.get("/vessels") -async def list_vessels(nocache:bool=False,key: str = Depends(X_API_KEY_HEADER)): - print(f"KEY:{key}") - check_apikey(key) - endpoint=f"/vessels" - cache= rd.get(endpoint) - start = time.time() - if cache and not nocache: - logger.debug(f"{endpoint} cached ({settings.redis_cache_expiration})s") - payload=json.loads(cache) - logger.debug(f"{endpoint} elapsed Time: {time.time()-start}") - return payload - else: - use_cases = UseCases() - vessel_repository = use_cases.vessel_repository() - db = use_cases.db() - with db.session() as session: - - json_data = [json.loads(v.model_dump_json() if v else "{}") - for v in vessel_repository.get_vessels_list(session)] - rd.set(endpoint, json.dumps(json_data)) - rd.expire(endpoint,settings.redis_cache_expiration) - return json_data - -@app.get("/vessels/{vessel_id}") -async def get_vessel(vessel_id: int,key: str = Depends(X_API_KEY_HEADER)): - check_apikey(key) - use_cases = UseCases() - vessel_repository = use_cases.vessel_repository() - db = use_cases.db() - with db.session() as session: - return vessel_repository.get_vessel_by_id(session,vessel_id) - -@app.get("/vessels/all/positions/last") -async def list_all_vessel_last_position(nocache:bool=False,key: str = Depends(X_API_KEY_HEADER)): - check_apikey(key) - endpoint=f"/vessels/all/positions/last" - cache= rd.get(endpoint) - start = time.time() - if cache and not nocache: - logger.debug(f"{endpoint} cached ({settings.redis_cache_expiration})s") - payload=json.loads(cache) - logger.debug(f"{endpoint} elapsed Time: {time.time()-start}") - return payload - else: - use_cases = UseCases() - segment_repository = use_cases.segment_repository() - db = use_cases.db() - with db.session() as session: - json_data = [json.loads(p.model_dump_json() if p else "{}") - for p in segment_repository.get_all_vessels_last_position(session)] - rd.set(endpoint, json.dumps(json_data)) - rd.expire(endpoint,settings.redis_cache_expiration) - logger.debug(f"{endpoint} elapsed Time: {time.time()-start}") - return json_data - -@app.get("/vessels/{vessel_id}/positions/last") -async def get_vessel_last_position(vessel_id: int, nocache:bool=False,key: str = Depends(X_API_KEY_HEADER)): - check_apikey(key) - endpoint=f"/vessels/{vessel_id}/positions/last" - cache= rd.get(endpoint) - start = time.time() - if cache and not nocache: - logger.debug(f"{endpoint} cached ({settings.redis_cache_expiration})s") - payload=json.loads(cache) - logger.debug(f"{endpoint} elapsed Time: {time.time()-start}") - return payload - else: - use_cases = UseCases() - segment_repository = use_cases.segment_repository() - db = use_cases.db() - with db.session() as session: - result=segment_repository.get_vessel_last_position(session,vessel_id) - json_data = json.loads(result.model_dump_json() if result else "{}") - rd.set(endpoint, json.dumps(json_data)) - rd.expire(endpoint,settings.redis_cache_expiration) - logger.debug(f"{endpoint} elapsed Time: {time.time()-start}") - return json_data - -@app.get("/vessels/{vessel_id}/excursions") -async def list_vessel_excursions(vessel_id: int, nocache:bool=False,key: str = Depends(X_API_KEY_HEADER)): - check_apikey(key) - endpoint=f"/vessels/{vessel_id}/excursions" - cache= rd.get(endpoint) - start = time.time() - if cache and not nocache: - logger.debug(f"{endpoint} cached ({settings.redis_cache_expiration})s") - payload=json.loads(cache) - logger.debug(f"{endpoint} elapsed Time: {time.time()-start}") - return payload - else: - use_cases = UseCases() - excursion_repository = use_cases.excursion_repository() - db = use_cases.db() - with db.session() as session: - json_data = [json.loads(p.model_dump_json() if p else "{}") - for p in excursion_repository.get_excursions_by_vessel_id(session,vessel_id)] - rd.set(endpoint, json.dumps(json_data)) - rd.expire(endpoint,settings.redis_cache_expiration) - logger.debug(f"{endpoint} elapsed Time: {time.time()-start}") - return json_data - - -@app.get("/vessels/{vessel_id}/excursions/{excursions_id}") -async def get_vessel_excursion(vessel_id: int,excursions_id: int,key: str = Depends(X_API_KEY_HEADER)): - check_apikey(key) - 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,key: str = Depends(X_API_KEY_HEADER)): - check_apikey(key) - 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,key: str = Depends(X_API_KEY_HEADER)): - check_apikey(key) - 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,nocache:bool=False,key: str = Depends(X_API_KEY_HEADER)): - check_apikey(key) - endpoint=f"/ports" - cache= rd.get(endpoint) - start = time.time() - if cache and not nocache: - logger.debug(f"{endpoint} cached ({settings.redis_cache_expiration})s") - payload=json.loads(cache) - logger.debug(f"{endpoint} elapsed Time: {time.time()-start}") - return payload - else: - use_cases = UseCases() - port_repository = use_cases.port_repository() - db = use_cases.db() - with db.session() as session: - json_data = [json.loads(p.model_dump_json() if p else "{}") - for p in port_repository.get_all_ports(session)] - rd.set(endpoint, json.dumps(json_data)) - rd.expire(endpoint,settings.redis_cache_expiration) - logger.debug(f"{endpoint} elapsed Time: {time.time()-start}") - return json_data - - -@app.get("/ports/{port_id}") -async def get_port(port_id:int,key: str = Depends(X_API_KEY_HEADER)): - check_apikey(key) - use_cases = UseCases() - port_repository = use_cases.port_repository() - db = use_cases.db() - with db.session() as session: - return port_repository.get_port_by_id(session,port_id) - -@app.get("/zones") -async def list_zones(request:Request,nocache:bool=False,key: str = Depends(X_API_KEY_HEADER)): - check_apikey(key) - endpoint=f"/zones" - cache= rd.get(endpoint) - start = time.time() - if cache and not nocache: - logger.debug(f"{endpoint} cached ({settings.redis_cache_expiration})s") - payload=json.loads(cache) - logger.debug(f"{endpoint} elapsed Time: {time.time()-start}") - return payload - else: - use_cases = UseCases() - zone_repository = use_cases.zone_repository() - db = use_cases.db() - with db.session() as session: - json_data = [json.loads(z.model_dump_json() if z else "{}") - for z in zone_repository.get_all_zones(session)] - rd.set(endpoint, json.dumps(json_data)) - rd.expire(endpoint,settings.redis_cache_expiration) - logger.debug(f"{endpoint} elapsed Time: {time.time()-start}") - return json_data - -@app.get("/zones/all/categories") -async def list_zone_categories(request:Request,nocache:bool=False,key: str = Depends(X_API_KEY_HEADER)): - check_apikey(key) - endpoint=f"/zones/all/categories" - cache= rd.get(endpoint) - start = time.time() - if cache and not nocache: - logger.debug(f"{endpoint} cached ({settings.redis_cache_expiration})s") - payload=json.loads(cache) - logger.debug(f"{endpoint} elapsed Time: {time.time()-start}") - return payload - else: - use_cases = UseCases() - zone_repository = use_cases.zone_repository() - db = use_cases.db() - with db.session() as session: - json_data = [json.loads(z.model_dump_json() if z else "{}") - for z in zone_repository.get_all_zone_categories(session)] - rd.set(endpoint, json.dumps(json_data)) - rd.expire(endpoint,settings.redis_cache_expiration) - logger.debug(f"{endpoint} elapsed Time: {time.time()-start}") - return json_data - -@app.get("/zones/by-category/{category}/by-sub-category/{sub}") -async def get_zone_all_by_category(category:str="all",sub:str=None,nocache:bool=False,key: str = Depends(X_API_KEY_HEADER)): - check_apikey(key) - endpoint=f"/zones/by-category/{category}/by-sub-category/{sub}" - cache= rd.get(endpoint) - start = time.time() - if cache and not nocache: - logger.debug(f"{endpoint} cached ({settings.redis_cache_expiration})s") - payload=json.loads(cache) - logger.debug(f"{endpoint} elapsed Time: {time.time()-start}") - return payload - else: - use_cases = UseCases() - zone_repository = use_cases.zone_repository() - db = use_cases.db() - with db.session() as session: - json_data = [json.loads(z.model_dump_json() if z else "{}") - for z in zone_repository.get_all_zones_by_category(session,category if category != 'all' else None,sub)] - rd.set(endpoint, json.dumps(json_data)) - rd.expire(endpoint,settings.redis_cache_expiration) - logger.debug(f"{endpoint} elapsed Time: {time.time()-start}") - return json_data - -@app.get("/zones/by-category/{category}") -async def get_zone_all_by_category(category:str="all",nocache:bool=False,key: str = Depends(X_API_KEY_HEADER)): - check_apikey(key) - endpoint=f"/zones/by-category/{category}" - cache= rd.get(endpoint) - start = time.time() - if cache and not nocache: - logger.debug(f"{endpoint} cached ({settings.redis_cache_expiration})s") - payload=json.loads(cache) - logger.debug(f"{endpoint} elapsed Time: {time.time()-start}") - return payload - else: - use_cases = UseCases() - zone_repository = use_cases.zone_repository() - db = use_cases.db() - with db.session() as session: - json_data = [json.loads(z.model_dump_json() if z else "{}") - for z in zone_repository.get_all_zones_by_category(session,category if category != 'all' else None)] - rd.set(endpoint, json.dumps(json_data)) - rd.expire(endpoint,settings.redis_cache_expiration) - logger.debug(f"{endpoint} elapsed Time: {time.time()-start}") - return json_data - -@app.get("/zones/{zones_id}") -async def get_zone(zones_id:int,key: str = Depends(X_API_KEY_HEADER)): - check_apikey(key) - use_cases = UseCases() - zone_repository = use_cases.zone_repository() - db = use_cases.db() - with db.session() as session: - return zone_repository.get_zone_by_id(session,zones_id) - @app.get("/") async def root(request:Request): return { From 17b8022325fbaddb77dc5c63fdc00afbef1a062d Mon Sep 17 00:00:00 2001 From: RV Date: Mon, 7 Oct 2024 17:35:07 +0200 Subject: [PATCH 6/8] refacto ++ routers metrics/ports/vessels/zones --- backend/bloom/domain/api.py | 3 + backend/bloom/domain/metrics.py | 2 +- backend/bloom/routers/metrics.py | 106 ++++++++++++++++++------------- backend/bloom/routers/ports.py | 14 ++-- backend/bloom/routers/vessels.py | 19 ++++-- backend/bloom/routers/zones.py | 3 - 6 files changed, 84 insertions(+), 63 deletions(-) diff --git a/backend/bloom/domain/api.py b/backend/bloom/domain/api.py index 18522089..13aefa55 100644 --- a/backend/bloom/domain/api.py +++ b/backend/bloom/domain/api.py @@ -12,6 +12,9 @@ ## https://jayhawk24.hashnode.dev/how-to-implement-pagination-in-fastapi-feat-sqlalchemy X_API_KEY_HEADER=APIKeyHeader(name="x-key") +class CachedRequest(BaseModel): + nocache:bool=False + def check_apikey(key:str): if key != settings.api_key : raise HTTPException(status_code=401, detail="Unauthorized") diff --git a/backend/bloom/domain/metrics.py b/backend/bloom/domain/metrics.py index 5a505915..6cc5e796 100644 --- a/backend/bloom/domain/metrics.py +++ b/backend/bloom/domain/metrics.py @@ -4,7 +4,7 @@ from datetime import datetime, timedelta from enum import Enum -class ResponseMetricsVesselInActiviySchema(BaseModel): +class ResponseMetricsVesselInActivitySchema(BaseModel): model_config = ConfigDict(from_attributes=True) id: int mmsi: int diff --git a/backend/bloom/routers/metrics.py b/backend/bloom/routers/metrics.py index 171d0a2c..5b552d22 100644 --- a/backend/bloom/routers/metrics.py +++ b/backend/bloom/routers/metrics.py @@ -2,74 +2,90 @@ from redis import Redis from bloom.config import settings from bloom.container import UseCases +from bloom.logger import logger from pydantic import BaseModel, Field from typing_extensions import Annotated, Literal, Optional from datetime import datetime, timedelta from sqlalchemy import select, func, and_, or_ from bloom.infra.database import sql_model from bloom.infra.repositories.repository_segment import SegmentRepository +from sqlalchemy.ext.serializer import loads,dumps import json -from bloom.domain.metrics import (ResponseMetricsVesselInActiviySchema, +import time +from bloom.infra.database.database_manager import Base +from bloom.domain.metrics import (ResponseMetricsVesselInActivitySchema, ResponseMetricsZoneVisitedSchema, ResponseMetricsZoneVisitingTimeByVesselSchema) from bloom.domain.api import ( DatetimeRangeRequest, PaginatedRequest,OrderByRequest,OrderByEnum, paginate,PagedResponseSchema,PageParams, - X_API_KEY_HEADER) + X_API_KEY_HEADER, check_apikey,CachedRequest) router = APIRouter() -redis_client = Redis(host=settings.redis_host, port=settings.redis_port, db=0) - - - +rd = Redis(host=settings.redis_host, port=settings.redis_port, db=0) @router.get("/metrics/vessels-in-activity", - response_model=list[ResponseMetricsVesselInActiviySchema], + response_model=list[ResponseMetricsVesselInActivitySchema], tags=['Metrics']) def read_metrics_vessels_in_activity_total(request: Request, datetime_range: DatetimeRangeRequest = Depends(), - #pagination: PageParams = Depends(), + pagination: PageParams = Depends(), order: OrderByRequest = Depends(), - auth: str = Depends(X_API_KEY_HEADER), + caching: CachedRequest = Depends(), + key: str = Depends(X_API_KEY_HEADER), ): + check_apikey(key) use_cases = UseCases() db = use_cases.db() - with db.session() as session: - stmt=select(sql_model.Vessel.id, - sql_model.Vessel.mmsi, - sql_model.Vessel.ship_name, - sql_model.Vessel.width, - sql_model.Vessel.length, - sql_model.Vessel.country_iso3, - sql_model.Vessel.type, - sql_model.Vessel.imo, - sql_model.Vessel.cfr, - sql_model.Vessel.external_marking, - sql_model.Vessel.ircs, - sql_model.Vessel.home_port_id, - sql_model.Vessel.details, - sql_model.Vessel.tracking_activated, - sql_model.Vessel.tracking_status, - sql_model.Vessel.length_class, - sql_model.Vessel.check, - func.sum(sql_model.Excursion.total_time_at_sea).label("total_time_at_sea") - )\ - .select_from(sql_model.Segment)\ - .join(sql_model.Excursion, sql_model.Segment.excursion_id == sql_model.Excursion.id)\ - .join(sql_model.Vessel, sql_model.Excursion.vessel_id == sql_model.Vessel.id)\ - .where( - or_( - sql_model.Excursion.arrival_at.between(datetime_range.start_at,datetime_range.end_at), - and_(sql_model.Excursion.departure_at <= datetime_range.end_at, - sql_model.Excursion.arrival_at == None)) - )\ - .group_by(sql_model.Vessel.id,sql_model.Excursion.total_time_at_sea) - #stmt = stmt.limit(pagination.limit) if pagination.limit != None else stmt - - stmt = stmt.order_by(sql_model.Excursion.total_time_at_sea.asc())\ - if order.order == OrderByEnum.ascending \ - else stmt.order_by(sql_model.Excursion.total_time_at_sea.desc()) - return session.execute(stmt).all() + endpoint=f"/vessels" + cache= rd.get(endpoint) + start = time.time() + payload = [] + if cache and not caching.nocache: + logger.debug(f"{endpoint} cached ({settings.redis_cache_expiration})s") + payload=loads(cache) + else: + with db.session() as session: + stmt=select(sql_model.Vessel.id, + sql_model.Vessel.mmsi, + sql_model.Vessel.ship_name, + sql_model.Vessel.width, + sql_model.Vessel.length, + sql_model.Vessel.country_iso3, + sql_model.Vessel.type, + sql_model.Vessel.imo, + sql_model.Vessel.cfr, + sql_model.Vessel.external_marking, + sql_model.Vessel.ircs, + sql_model.Vessel.home_port_id, + sql_model.Vessel.details, + sql_model.Vessel.tracking_activated, + sql_model.Vessel.tracking_status, + sql_model.Vessel.length_class, + sql_model.Vessel.check, + func.sum(sql_model.Excursion.total_time_at_sea).label("total_time_at_sea") + )\ + .select_from(sql_model.Segment)\ + .join(sql_model.Excursion, sql_model.Segment.excursion_id == sql_model.Excursion.id)\ + .join(sql_model.Vessel, sql_model.Excursion.vessel_id == sql_model.Vessel.id)\ + .where( + or_( + sql_model.Excursion.arrival_at.between(datetime_range.start_at,datetime_range.end_at), + and_(sql_model.Excursion.departure_at <= datetime_range.end_at, + sql_model.Excursion.arrival_at == None)) + )\ + .group_by(sql_model.Vessel.id,sql_model.Excursion.total_time_at_sea) + stmt = stmt.limit(pagination.limit) if pagination.limit != None else stmt + stmt = stmt.offset(pagination.offset) if pagination.offset != None else stmt + stmt = stmt.order_by(sql_model.Excursion.total_time_at_sea.asc())\ + if order.order == OrderByEnum.ascending \ + else stmt.order_by(sql_model.Excursion.total_time_at_sea.desc()) + payload=session.execute(stmt).all() + serialized=dumps(payload) + rd.set(endpoint, serialized) + rd.expire(endpoint,settings.redis_cache_expiration) + logger.debug(f"{endpoint} elapsed Time: {time.time()-start}") + return payload @router.get("/metrics/zone-visited", response_model=list[ResponseMetricsZoneVisitedSchema], diff --git a/backend/bloom/routers/ports.py b/backend/bloom/routers/ports.py index ed0d405b..4397f2f5 100644 --- a/backend/bloom/routers/ports.py +++ b/backend/bloom/routers/ports.py @@ -15,13 +15,10 @@ from bloom.container import UseCases from bloom.domain.vessel import Vessel from bloom.logger import logger -from bloom.domain.metrics import (ResponseMetricsVesselInActiviySchema, - ResponseMetricsZoneVisitedSchema, - ResponseMetricsZoneVisitingTimeByVesselSchema) from bloom.domain.api import ( DatetimeRangeRequest, PaginatedRequest,OrderByRequest,OrderByEnum, paginate,PagedResponseSchema,PageParams, - X_API_KEY_HEADER,check_apikey) + X_API_KEY_HEADER,check_apikey,CachedRequest) from bloom.config import settings router = APIRouter() @@ -29,12 +26,14 @@ @router.get("/ports", tags=['Ports']) -async def list_ports(request:Request,nocache:bool=False,key: str = Depends(X_API_KEY_HEADER)): +async def list_ports( request:Request, + caching: CachedRequest = Depends(), + key: str = Depends(X_API_KEY_HEADER)): check_apikey(key) endpoint=f"/ports" cache= rd.get(endpoint) start = time.time() - if cache and not nocache: + if cache and not caching.nocache: logger.debug(f"{endpoint} cached ({settings.redis_cache_expiration})s") payload=json.loads(cache) logger.debug(f"{endpoint} elapsed Time: {time.time()-start}") @@ -54,7 +53,8 @@ async def list_ports(request:Request,nocache:bool=False,key: str = Depends(X_API @router.get("/ports/{port_id}", tags=['Ports']) -async def get_port(port_id:int,key: str = Depends(X_API_KEY_HEADER)): +async def get_port(port_id:int, + key: str = Depends(X_API_KEY_HEADER)): check_apikey(key) use_cases = UseCases() port_repository = use_cases.port_repository() diff --git a/backend/bloom/routers/vessels.py b/backend/bloom/routers/vessels.py index e78ffa6e..e052119d 100644 --- a/backend/bloom/routers/vessels.py +++ b/backend/bloom/routers/vessels.py @@ -15,9 +15,6 @@ from bloom.container import UseCases from bloom.domain.vessel import Vessel from bloom.logger import logger -from bloom.domain.metrics import (ResponseMetricsVesselInActiviySchema, - ResponseMetricsZoneVisitedSchema, - ResponseMetricsZoneVisitingTimeByVesselSchema) from bloom.domain.api import ( DatetimeRangeRequest, PaginatedRequest,OrderByRequest,OrderByEnum, paginate,PagedResponseSchema,PageParams, @@ -29,7 +26,6 @@ @router.get("/vessels", tags=['Vessels']) async def list_vessels(nocache:bool=False,key: str = Depends(X_API_KEY_HEADER)): - print(f"KEY:{key}") check_apikey(key) endpoint=f"/vessels" cache= rd.get(endpoint) @@ -111,7 +107,11 @@ async def get_vessel_last_position(vessel_id: int, nocache:bool=False,key: str = @router.get("/vessels/{vessel_id}/excursions", tags=['Vessels']) -async def list_vessel_excursions(vessel_id: int, nocache:bool=False,key: str = Depends(X_API_KEY_HEADER)): +async def list_vessel_excursions(vessel_id: int, nocache:bool=False, + datetime_range: DatetimeRangeRequest = Depends(), + pagination: PageParams = Depends(), + order: OrderByRequest = Depends(), + key: str = Depends(X_API_KEY_HEADER)): check_apikey(key) endpoint=f"/vessels/{vessel_id}/excursions" cache= rd.get(endpoint) @@ -147,7 +147,9 @@ async def get_vessel_excursion(vessel_id: int,excursions_id: int,key: str = Depe @router.get("/vessels/{vessel_id}/excursions/{excursions_id}/segments", tags=['Vessels']) -async def list_vessel_excursion_segments(vessel_id: int,excursions_id: int,key: str = Depends(X_API_KEY_HEADER)): +async def list_vessel_excursion_segments(vessel_id: int, + excursions_id: int, + key: str = Depends(X_API_KEY_HEADER)): check_apikey(key) use_cases = UseCases() segment_repository = use_cases.segment_repository() @@ -157,7 +159,10 @@ async def list_vessel_excursion_segments(vessel_id: int,excursions_id: int,key: @router.get("/vessels/{vessel_id}/excursions/{excursions_id}/segments/{segment_id}", tags=['Vessels']) -async def get_vessel_excursion_segment(vessel_id: int,excursions_id: int, segment_id:int,key: str = Depends(X_API_KEY_HEADER)): +async def get_vessel_excursion_segment(vessel_id: int, + excursions_id: int, + segment_id:int, + key: str = Depends(X_API_KEY_HEADER)): check_apikey(key) use_cases = UseCases() segment_repository = use_cases.segment_repository() diff --git a/backend/bloom/routers/zones.py b/backend/bloom/routers/zones.py index ad5dc9fc..4f6bce34 100644 --- a/backend/bloom/routers/zones.py +++ b/backend/bloom/routers/zones.py @@ -15,9 +15,6 @@ from bloom.container import UseCases from bloom.domain.vessel import Vessel from bloom.logger import logger -from bloom.domain.metrics import (ResponseMetricsVesselInActiviySchema, - ResponseMetricsZoneVisitedSchema, - ResponseMetricsZoneVisitingTimeByVesselSchema) from bloom.domain.api import ( DatetimeRangeRequest, PaginatedRequest,OrderByRequest,OrderByEnum, paginate,PagedResponseSchema,PageParams, From f0a1b3a4a94b54af00930d2ee51bbafc0ac176c9 Mon Sep 17 00:00:00 2001 From: RV Date: Tue, 8 Oct 2024 17:14:49 +0200 Subject: [PATCH 7/8] ++ --- backend/bloom/domain/api.py | 6 ++ backend/bloom/domain/metrics.py | 45 ++++++------ backend/bloom/infra/database/sql_model.py | 23 +++++- backend/bloom/routers/metrics.py | 88 ++++++++++++++--------- 4 files changed, 105 insertions(+), 57 deletions(-) diff --git a/backend/bloom/domain/api.py b/backend/bloom/domain/api.py index 13aefa55..1a6d94ec 100644 --- a/backend/bloom/domain/api.py +++ b/backend/bloom/domain/api.py @@ -4,6 +4,7 @@ from typing_extensions import Annotated, Literal, Optional from datetime import datetime, timedelta from enum import Enum +import redis from pydantic.generics import GenericModel from fastapi.security import APIKeyHeader from bloom.config import settings @@ -12,6 +13,8 @@ ## https://jayhawk24.hashnode.dev/how-to-implement-pagination-in-fastapi-feat-sqlalchemy X_API_KEY_HEADER=APIKeyHeader(name="x-key") +rd = redis.Redis(host=settings.redis_host, port=settings.redis_port, db=0) + class CachedRequest(BaseModel): nocache:bool=False @@ -20,6 +23,9 @@ def check_apikey(key:str): raise HTTPException(status_code=401, detail="Unauthorized") return True +def check_cache(request:Request): + cache= rd.get(request.url.path) + class DatetimeRangeRequest(BaseModel): start_at: datetime = datetime.now()-timedelta(days=7) end_at: datetime = datetime.now() diff --git a/backend/bloom/domain/metrics.py b/backend/bloom/domain/metrics.py index 6cc5e796..acd400ce 100644 --- a/backend/bloom/domain/metrics.py +++ b/backend/bloom/domain/metrics.py @@ -3,33 +3,34 @@ from typing_extensions import Annotated, Literal, Optional from datetime import datetime, timedelta from enum import Enum +from bloom.domain.vessel import Vessel class ResponseMetricsVesselInActivitySchema(BaseModel): model_config = ConfigDict(from_attributes=True) - id: int - mmsi: int - ship_name: str - width: Optional[float] = None - length: Optional[float] = None - country_iso3: Optional[str] = None - type: Optional[str] = None - imo: Optional[int] = None - cfr: Optional[str] = None - external_marking: Optional[str] = None - ircs: Optional[str] = None - home_port_id: Optional[int] = None - details: Optional[str] = None - tracking_activated: Optional[bool] - tracking_status: Optional[str] - length_class: Optional[str] - check: Optional[str] - total_time_at_sea: timedelta + vessel_id: Optional[int] + """ vessel_mmsi: int + vessel_ship_name: str + vessel_width: Optional[float] = None + vessel_length: Optional[float] = None + vessel_country_iso3: Optional[str] = None + vessel_type: Optional[str] = None + vessel_imo: Optional[int] = None + vessel_cfr: Optional[str] = None + vessel_external_marking: Optional[str] = None + vessel_ircs: Optional[str] = None + vessel_home_port_id: Optional[int] = None + vessel_details: Optional[str] = None + vessel_tracking_activated: Optional[bool] + vessel_tracking_status: Optional[str] + vessel_length_class: Optional[str] + vessel_check: Optional[str]""" + total_time_at_sea: Optional[timedelta] class ResponseMetricsZoneVisitedSchema(BaseModel): - id : int - category: str - sub_category: Optional[str] = None - name: str + zone_id : int + zone_category: Optional[str] + zone_sub_category: Optional[str] = None + zone_name: str visiting_duration: timedelta class ResponseMetricsZoneVisitingTimeByVesselSchema(BaseModel): diff --git a/backend/bloom/infra/database/sql_model.py b/backend/bloom/infra/database/sql_model.py index 22e17ca7..91652f13 100644 --- a/backend/bloom/infra/database/sql_model.py +++ b/backend/bloom/infra/database/sql_model.py @@ -13,7 +13,10 @@ PrimaryKeyConstraint ) from sqlalchemy.dialects.postgresql import JSONB -from sqlalchemy.sql import func +from sqlalchemy.sql import func, select +from sqlalchemy.orm import mapped_column, Mapped, relationship +from typing_extensions import Annotated, Literal, Optional +from datetime import timedelta class Vessel(Base): @@ -235,3 +238,21 @@ class RelSegmentZone(Base): segment_id = Column("segment_id", Integer, ForeignKey("fct_segment.id"), nullable=False) zone_id = Column("zone_id", Integer, ForeignKey("dim_zone.id"), nullable=False) created_at = Column("created_at", DateTime(timezone=True), server_default=func.now()) + +vessel_in_activity_request=( + select( + Vessel.id, + Excursion.vessel_id, + func.sum(Excursion.total_time_at_sea).label("total_time_at_sea") + )\ + .select_from(Segment)\ + .join(Excursion, Segment.excursion_id == Excursion.id)\ + .join(Vessel, Excursion.vessel_id == Vessel.id)\ + .group_by(Vessel.id,Excursion.vessel_id,Excursion.total_time_at_sea)\ + .subquery()) + +class MetricsVesselInActivity(Base): + __table__ = vessel_in_activity_request + #vessel_id: Mapped[Optional[int]] + #total_time_at_sea: Mapped[Optional[timedelta]] + diff --git a/backend/bloom/routers/metrics.py b/backend/bloom/routers/metrics.py index 5b552d22..cac86840 100644 --- a/backend/bloom/routers/metrics.py +++ b/backend/bloom/routers/metrics.py @@ -37,13 +37,13 @@ def read_metrics_vessels_in_activity_total(request: Request, check_apikey(key) use_cases = UseCases() db = use_cases.db() - endpoint=f"/vessels" - cache= rd.get(endpoint) + cache_key=f"{request.url.path}?{request.query_params}" + cache_payload= rd.get(cache_key) start = time.time() payload = [] - if cache and not caching.nocache: - logger.debug(f"{endpoint} cached ({settings.redis_cache_expiration})s") - payload=loads(cache) + if cache_payload and not caching.nocache: + logger.debug(f"{cache_key} cached ({settings.redis_cache_expiration})s") + payload=loads(cache_payload) else: with db.session() as session: stmt=select(sql_model.Vessel.id, @@ -79,41 +79,61 @@ def read_metrics_vessels_in_activity_total(request: Request, stmt = stmt.offset(pagination.offset) if pagination.offset != None else stmt stmt = stmt.order_by(sql_model.Excursion.total_time_at_sea.asc())\ if order.order == OrderByEnum.ascending \ - else stmt.order_by(sql_model.Excursion.total_time_at_sea.desc()) + else stmt.order_by(sql_model.Excursion.total_time_at_sea.desc())""" payload=session.execute(stmt).all() + for item in session.execute(stmt).scalars(): + print(f"{item.vessel_id},{item.total_time_at_sea}") serialized=dumps(payload) - rd.set(endpoint, serialized) - rd.expire(endpoint,settings.redis_cache_expiration) - logger.debug(f"{endpoint} elapsed Time: {time.time()-start}") - return payload + rd.set(cache_key, serialized) + rd.expire(cache_key,settings.redis_cache_expiration) + logger.debug(f"{cache_key} elapsed Time: {time.time()-start}") + return [] @router.get("/metrics/zone-visited", response_model=list[ResponseMetricsZoneVisitedSchema], tags=['Metrics'] ) -def read_metrics_vessels_in_activity_total(datetime_range: DatetimeRangeRequest = Depends(), - pagination: PaginatedRequest = Depends(), - auth: str = Depends(X_API_KEY_HEADER),): - use_cases = UseCases() - db = use_cases.db() - with db.session() as session: - stmt=select( - sql_model.Zone.id.label("zone_id"), - sql_model.Zone.category.label("zone_category"), - sql_model.Zone.sub_category.label("zone_sub_category"), - sql_model.Zone.name.label("zone_name"), - func.sum(sql_model.Segment.segment_duration).label("visiting_duration") - )\ - .where( - or_( - sql_model.Segment.timestamp_start.between(datetime_range.start_at,datetime_range.end_at), - sql_model.Segment.timestamp_end.between(datetime_range.start_at,datetime_range.end_at),) - )\ - .group_by(sql_model.Zone.id) - stmt = stmt.limit(limit) if limit != None else stmt - stmt = stmt.order_by("visiting_duration")\ - if pagination.order_by == OrderByRequest.ascending \ - else stmt.order_by("visiting_duration") - return session.execute(stmt).all() +def read_metrics_vessels_in_activity_total(request: Request, + datetime_range: DatetimeRangeRequest = Depends(), + pagination: PageParams = Depends(), + order: OrderByRequest = Depends(), + caching: CachedRequest = Depends(), + key: str = Depends(X_API_KEY_HEADER),): + check_apikey(key) + cache_key=f"{request.url.path}?{request.query_params}" + cache_payload= rd.get(cache_key) + start = time.time() + payload=[] + if cache_payload and not caching.nocache: + logger.debug(f"{cache_key} cached ({settings.redis_cache_expiration})s") + payload=loads(cache_payload) + else: + use_cases = UseCases() + payload = [] + db = use_cases.db() + with db.session() as session: + stmt=select( + sql_model.Zone.id.label("zone_id"), + sql_model.Zone.category.label("zone_category"), + sql_model.Zone.sub_category.label("zone_sub_category"), + sql_model.Zone.name.label("zone_name"), + func.sum(sql_model.Segment.segment_duration).label("visiting_duration") + )\ + .where( + or_( + sql_model.Segment.timestamp_start.between(datetime_range.start_at,datetime_range.end_at), + sql_model.Segment.timestamp_end.between(datetime_range.start_at,datetime_range.end_at),) + )\ + .group_by(sql_model.Zone.id) + stmt = stmt.limit(pagination.limit) if pagination.limit != None else stmt + stmt = stmt.order_by("visiting_duration")\ + if order.order == OrderByEnum.ascending \ + else stmt.order_by("visiting_duration") + payload=session.execute(stmt).all() + serialized=dumps(payload) + rd.set(cache_key, serialized) + rd.expire(cache_key,settings.redis_cache_expiration) + logger.debug(f"{cache_key} elapsed Time: {time.time()-start}") + return payload @router.get("/metrics/zones/{zone_id}/visiting-time-by-vessel", response_model=list[ResponseMetricsZoneVisitingTimeByVesselSchema], From 09330650da7cbec498cf66aaa3077f2814d17bcc Mon Sep 17 00:00:00 2001 From: "herve.le-bars" Date: Tue, 8 Oct 2024 20:01:39 +0200 Subject: [PATCH 8/8] + api add metrics vessel activity by activity type --- backend/bloom/domain/api.py | 19 ++- backend/bloom/domain/metrics.py | 10 +- backend/bloom/routers/metrics.py | 199 +++++++++++++++++++++---------- 3 files changed, 159 insertions(+), 69 deletions(-) diff --git a/backend/bloom/domain/api.py b/backend/bloom/domain/api.py index 1a6d94ec..baff6beb 100644 --- a/backend/bloom/domain/api.py +++ b/backend/bloom/domain/api.py @@ -27,13 +27,28 @@ def check_cache(request:Request): cache= rd.get(request.url.path) class DatetimeRangeRequest(BaseModel): - start_at: datetime = datetime.now()-timedelta(days=7) + start_at: datetime = Field(default=datetime.now()-timedelta(days=7)) end_at: datetime = datetime.now() class OrderByEnum(str, Enum): ascending = "ASC" descending = "DESC" + +class TotalTimeActivityTypeEnum(str, Enum): + total_time_at_sea: str = "Total Time at Sea" + total_time_in_amp: str = "Total Time in AMP" + total_time_in_territorial_waters: str = "Total Time in Territorial Waters" + total_time_in_costal_waters: str = "Total Time in Costal Waters" + total_time_fishing: str = "Total Time Fishing" + total_time_fishing_in_amp: str = "Total Time Fishing in AMP" + total_time_fishing_in_territorial_waters: str = "Total Time Fishing in Territorial Waters" + total_time_fishing_in_costal_waters: str = "Total Time Fishing in Costal Waters" + total_time_fishing_in_extincting_amp: str = "Total Time in Extincting AMP" + +class TotalTimeActivityTypeRequest(BaseModel): + type: TotalTimeActivityTypeEnum + class OrderByRequest(BaseModel): order: OrderByEnum = OrderByEnum.ascending @@ -46,7 +61,7 @@ class PaginatedRequest(BaseModel): class PageParams(BaseModel): """ Request query params for paginated API. """ offset: conint(ge=0) = 0 - limit: conint(ge=1, le=1000) = 100 + limit: conint(ge=1, le=100000) = 100 T = TypeVar("T") diff --git a/backend/bloom/domain/metrics.py b/backend/bloom/domain/metrics.py index acd400ce..54be1c94 100644 --- a/backend/bloom/domain/metrics.py +++ b/backend/bloom/domain/metrics.py @@ -8,7 +8,7 @@ class ResponseMetricsVesselInActivitySchema(BaseModel): model_config = ConfigDict(from_attributes=True) vessel_id: Optional[int] - """ vessel_mmsi: int + vessel_mmsi: int vessel_ship_name: str vessel_width: Optional[float] = None vessel_length: Optional[float] = None @@ -23,7 +23,7 @@ class ResponseMetricsVesselInActivitySchema(BaseModel): vessel_tracking_activated: Optional[bool] vessel_tracking_status: Optional[str] vessel_length_class: Optional[str] - vessel_check: Optional[str]""" + vessel_check: Optional[str] total_time_at_sea: Optional[timedelta] class ResponseMetricsZoneVisitedSchema(BaseModel): @@ -42,4 +42,8 @@ class ResponseMetricsZoneVisitingTimeByVesselSchema(BaseModel): vessel_name: str vessel_type: Optional[str] = None vessel_length_class: Optional[str] = None - zone_visiting_time_by_vessel: timedelta \ No newline at end of file + zone_visiting_time_by_vessel: timedelta + +class ResponseMetricsVesselTotalTimeActivityByActivityTypeSchema(BaseModel): + vessel_id : int + total_activity_time: timedelta \ No newline at end of file diff --git a/backend/bloom/routers/metrics.py b/backend/bloom/routers/metrics.py index cac86840..d599c36e 100644 --- a/backend/bloom/routers/metrics.py +++ b/backend/bloom/routers/metrics.py @@ -6,7 +6,7 @@ from pydantic import BaseModel, Field from typing_extensions import Annotated, Literal, Optional from datetime import datetime, timedelta -from sqlalchemy import select, func, and_, or_ +from sqlalchemy import select, func, and_, or_, text, literal_column, Row from bloom.infra.database import sql_model from bloom.infra.repositories.repository_segment import SegmentRepository from sqlalchemy.ext.serializer import loads,dumps @@ -15,11 +15,13 @@ from bloom.infra.database.database_manager import Base from bloom.domain.metrics import (ResponseMetricsVesselInActivitySchema, ResponseMetricsZoneVisitedSchema, - ResponseMetricsZoneVisitingTimeByVesselSchema) + ResponseMetricsZoneVisitingTimeByVesselSchema, + ResponseMetricsVesselTotalTimeActivityByActivityTypeSchema) from bloom.domain.api import ( DatetimeRangeRequest, PaginatedRequest,OrderByRequest,OrderByEnum, paginate,PagedResponseSchema,PageParams, - X_API_KEY_HEADER, check_apikey,CachedRequest) + X_API_KEY_HEADER, check_apikey,CachedRequest, + TotalTimeActivityTypeRequest) router = APIRouter() rd = Redis(host=settings.redis_host, port=settings.redis_port, db=0) @@ -46,23 +48,23 @@ def read_metrics_vessels_in_activity_total(request: Request, payload=loads(cache_payload) else: with db.session() as session: - stmt=select(sql_model.Vessel.id, - sql_model.Vessel.mmsi, - sql_model.Vessel.ship_name, - sql_model.Vessel.width, - sql_model.Vessel.length, - sql_model.Vessel.country_iso3, - sql_model.Vessel.type, - sql_model.Vessel.imo, - sql_model.Vessel.cfr, - sql_model.Vessel.external_marking, - sql_model.Vessel.ircs, - sql_model.Vessel.home_port_id, - sql_model.Vessel.details, - sql_model.Vessel.tracking_activated, - sql_model.Vessel.tracking_status, - sql_model.Vessel.length_class, - sql_model.Vessel.check, + stmt=select(sql_model.Vessel.id.label("vessel_id"), + sql_model.Vessel.mmsi.label("vessel_mmsi"), + sql_model.Vessel.ship_name.label("vessel_ship_name"), + sql_model.Vessel.width.label("vessel_width"), + sql_model.Vessel.length.label("vessel_length"), + sql_model.Vessel.country_iso3.label("vessel_country_iso3"), + sql_model.Vessel.type.label("vessel_type"), + sql_model.Vessel.imo.label("vessel_imo"), + sql_model.Vessel.cfr.label("vessel_cfr"), + sql_model.Vessel.external_marking.label("vessel_external_marking"), + sql_model.Vessel.ircs.label("vessel_ircs"), + sql_model.Vessel.home_port_id.label("vessel_home_port_id"), + sql_model.Vessel.details.label("vessel_details"), + sql_model.Vessel.tracking_activated.label("vessel_tracking_activated"), + sql_model.Vessel.tracking_status.label("vessel_tracking_status"), + sql_model.Vessel.length_class.label("vessel_length_class"), + sql_model.Vessel.check.label("vessel_check"), func.sum(sql_model.Excursion.total_time_at_sea).label("total_time_at_sea") )\ .select_from(sql_model.Segment)\ @@ -75,19 +77,17 @@ def read_metrics_vessels_in_activity_total(request: Request, sql_model.Excursion.arrival_at == None)) )\ .group_by(sql_model.Vessel.id,sql_model.Excursion.total_time_at_sea) - stmt = stmt.limit(pagination.limit) if pagination.limit != None else stmt stmt = stmt.offset(pagination.offset) if pagination.offset != None else stmt stmt = stmt.order_by(sql_model.Excursion.total_time_at_sea.asc())\ if order.order == OrderByEnum.ascending \ - else stmt.order_by(sql_model.Excursion.total_time_at_sea.desc())""" + else stmt.order_by(sql_model.Excursion.total_time_at_sea.desc()) + stmt = stmt.limit(pagination.limit) if pagination.limit != None else stmt payload=session.execute(stmt).all() - for item in session.execute(stmt).scalars(): - print(f"{item.vessel_id},{item.total_time_at_sea}") serialized=dumps(payload) rd.set(cache_key, serialized) rd.expire(cache_key,settings.redis_cache_expiration) logger.debug(f"{cache_key} elapsed Time: {time.time()-start}") - return [] + return payload @router.get("/metrics/zone-visited", response_model=list[ResponseMetricsZoneVisitedSchema], @@ -118,16 +118,21 @@ def read_metrics_vessels_in_activity_total(request: Request, sql_model.Zone.name.label("zone_name"), func.sum(sql_model.Segment.segment_duration).label("visiting_duration") )\ + .select_from(sql_model.Zone)\ + .join(sql_model.RelSegmentZone,sql_model.RelSegmentZone.zone_id == sql_model.Zone.id)\ + .join(sql_model.Segment,sql_model.RelSegmentZone.segment_id == sql_model.Segment.id)\ .where( or_( sql_model.Segment.timestamp_start.between(datetime_range.start_at,datetime_range.end_at), sql_model.Segment.timestamp_end.between(datetime_range.start_at,datetime_range.end_at),) )\ .group_by(sql_model.Zone.id) - stmt = stmt.limit(pagination.limit) if pagination.limit != None else stmt - stmt = stmt.order_by("visiting_duration")\ + stmt = stmt.order_by(func.sum(sql_model.Segment.segment_duration).asc())\ if order.order == OrderByEnum.ascending \ - else stmt.order_by("visiting_duration") + else stmt.order_by(func.sum(sql_model.Segment.segment_duration).desc()) + stmt = stmt.offset(pagination.offset) if pagination.offset != None else stmt + stmt = stmt.limit(pagination.limit) if pagination.limit != None else stmt + print(stmt) payload=session.execute(stmt).all() serialized=dumps(payload) rd.set(cache_key, serialized) @@ -138,42 +143,108 @@ def read_metrics_vessels_in_activity_total(request: Request, @router.get("/metrics/zones/{zone_id}/visiting-time-by-vessel", response_model=list[ResponseMetricsZoneVisitingTimeByVesselSchema], tags=['Metrics']) -def read_metrics_zone_visiting_time_by_vessel( - datetime_range: Annotated[DatetimeRangeRequest,Body()], - zone_id: int, - limit: int = None, - order_by: str = 'DESC', - auth: str = Depends(X_API_KEY_HEADER),): - use_cases = UseCases() - db = use_cases.db() - with db.session() as session: - stmt=select( - sql_model.Zone.id.label("zone_id"), - sql_model.Zone.category.label("zone_category"), - sql_model.Zone.sub_category.label("zone_sub_category"), - sql_model.Zone.name.label("zone_name"), - sql_model.Vessel.id.label("vessel_id"), - sql_model.Vessel.ship_name.label("vessel_name"), - sql_model.Vessel.type.label("vessel_type"), - sql_model.Vessel.length_class.label("vessel_length_class"), - func.sum(sql_model.Segment.segment_duration).label("zone_visiting_time_by_vessel") - )\ - .select_from(sql_model.Zone)\ - .join(sql_model.RelSegmentZone, sql_model.RelSegmentZone.zone_id == sql_model.Zone.id)\ - .join(sql_model.Segment, sql_model.RelSegmentZone.segment_id == sql_model.Segment.id)\ - .join(sql_model.Excursion, sql_model.Excursion.id == sql_model.Segment.excursion_id)\ - .join(sql_model.Vessel, sql_model.Excursion.vessel_id == sql_model.Vessel.id)\ - .where(sql_model.Zone.id == zone_id)\ - .group_by(sql_model.Zone.id,sql_model.Vessel.id) - return session.execute(stmt).all() +def read_metrics_zone_visiting_time_by_vessel(request: Request, + zone_id: int, + datetime_range: DatetimeRangeRequest = Depends(), + pagination: PageParams = Depends(), + order: OrderByRequest = Depends(), + caching: CachedRequest = Depends(), + key: str = Depends(X_API_KEY_HEADER),): + check_apikey(key) + cache_key=f"{request.url.path}?{request.query_params}" + cache_payload= rd.get(cache_key) + start = time.time() + payload=[] + if cache_payload and not caching.nocache: + logger.debug(f"{cache_key} cached ({settings.redis_cache_expiration})s") + payload=loads(cache_payload) + else: + use_cases = UseCases() + db = use_cases.db() + with db.session() as session: + stmt=select( + sql_model.Zone.id.label("zone_id"), + sql_model.Zone.category.label("zone_category"), + sql_model.Zone.sub_category.label("zone_sub_category"), + sql_model.Zone.name.label("zone_name"), + sql_model.Vessel.id.label("vessel_id"), + sql_model.Vessel.ship_name.label("vessel_name"), + sql_model.Vessel.type.label("vessel_type"), + sql_model.Vessel.length_class.label("vessel_length_class"), + func.sum(sql_model.Segment.segment_duration).label("zone_visiting_time_by_vessel") + )\ + .select_from(sql_model.Zone)\ + .join(sql_model.RelSegmentZone, sql_model.RelSegmentZone.zone_id == sql_model.Zone.id)\ + .join(sql_model.Segment, sql_model.RelSegmentZone.segment_id == sql_model.Segment.id)\ + .join(sql_model.Excursion, sql_model.Excursion.id == sql_model.Segment.excursion_id)\ + .join(sql_model.Vessel, sql_model.Excursion.vessel_id == sql_model.Vessel.id)\ + .where( + and_(sql_model.Zone.id == zone_id, + or_( + sql_model.Segment.timestamp_start.between(datetime_range.start_at,datetime_range.end_at), + sql_model.Segment.timestamp_end.between(datetime_range.start_at,datetime_range.end_at),)) + )\ + .group_by(sql_model.Zone.id,sql_model.Vessel.id) + + stmt = stmt.order_by(func.sum(sql_model.Segment.segment_duration).asc())\ + if order.order == OrderByEnum.ascending \ + else stmt.order_by(func.sum(sql_model.Segment.segment_duration).desc()) + stmt = stmt.offset(pagination.offset) if pagination.offset != None else stmt + stmt = stmt.limit(pagination.limit) if pagination.limit != None else stmt + payload=session.execute(stmt).all() + serialized=dumps(payload) + rd.set(cache_key, serialized) + rd.expire(cache_key,settings.redis_cache_expiration) + logger.debug(f"{cache_key} elapsed Time: {time.time()-start}") + return payload -@router.get("/metrics/vessels/{vessel_id}/visits/{visit_type}", tags=['Metrics']) -def read_metrics_vessels_visits_by_visit_type( - vessel_id: int, - visit_type: str, - datetime_range: DatetimeRangeRequest = Depends(), - pagination: PaginatedRequest = Depends(), - auth: str = Depends(X_API_KEY_HEADER),): - pass \ No newline at end of file +@router.get("/metrics/vessels/{vessel_id}/activity/{activity_type}", + response_model=ResponseMetricsVesselTotalTimeActivityByActivityTypeSchema, + tags=['Metrics']) +def read_metrics_vessels_visits_by_activity_type(request: Request, + vessel_id: int, + activity_type: TotalTimeActivityTypeRequest = Depends(), + datetime_range: DatetimeRangeRequest = Depends(), + #pagination: PageParams = Depends(), + #order: OrderByRequest = Depends(), + caching: CachedRequest = Depends(), + key: str = Depends(X_API_KEY_HEADER),): + check_apikey(key) + cache_key=f"{request.url.path}?{request.query_params}" + cache_payload= rd.get(cache_key) + start = time.time() + payload=[] + if cache_payload and not caching.nocache: + logger.debug(f"{cache_key} cached ({settings.redis_cache_expiration})s") + payload=loads(cache_payload) + else: + use_cases = UseCases() + db = use_cases.db() + with db.session() as session: + stmt=select(sql_model.Excursion.vessel_id, + literal_column(f"'{activity_type.type.value}'").label('activity'), + func.sum(sql_model.Excursion.total_time_at_sea).label("total_activity_time") + )\ + .select_from(sql_model.Excursion)\ + .where( + and_(sql_model.Excursion.vessel_id == vessel_id, + or_( + sql_model.Excursion.departure_at.between(datetime_range.start_at,datetime_range.end_at), + sql_model.Excursion.arrival_at.between(datetime_range.start_at,datetime_range.end_at),)) + )\ + .group_by(sql_model.Excursion.vessel_id)\ + .union(select( + literal_column(vessel_id), + literal_column(f"'{activity_type.type.value}'"), + literal_column('0 seconds'), + )) + print(type(session.execute(stmt.limit(1)).all()[0])) + payload=session.execute(stmt.limit(1)).all()[0] + serialized=dumps(payload) + rd.set(cache_key, serialized) + rd.expire(cache_key,settings.redis_cache_expiration) + + logger.debug(f"{cache_key} elapsed Time: {time.time()-start}") + return payload \ No newline at end of file