From bc260a2a8b38c205a3c9b846234629f6dae5bc75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kv=C3=A1=C4=8Dek?= Date: Wed, 9 Aug 2023 22:46:20 +0200 Subject: [PATCH] Uprava timezony u letu, error logging --- docker-compose.yml | 2 + src/decorators/endpoints.py | 7 ++ src/decorators/error_logging.py | 13 ++ src/graphql_schema/entities/flight.py | 114 +++++++++++++----- src/graphql_schema/entities/helpers/flight.py | 2 +- src/graphql_schema/entities/user.py | 8 +- src/graphql_schema/schema.py | 6 + src/main.py | 1 + 8 files changed, 120 insertions(+), 33 deletions(-) create mode 100644 src/decorators/endpoints.py create mode 100644 src/decorators/error_logging.py diff --git a/docker-compose.yml b/docker-compose.yml index a188568..14e536a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,6 +17,8 @@ services: - 8000:8000 volumes: - .:/app + environment: + TZ: "Europe/Prague" nginx: image: nginx:alpine diff --git a/src/decorators/endpoints.py b/src/decorators/endpoints.py new file mode 100644 index 0000000..64c7f24 --- /dev/null +++ b/src/decorators/endpoints.py @@ -0,0 +1,7 @@ + + +def public_endpoint(func): + pass + +def private_endpoint(func): + pass \ No newline at end of file diff --git a/src/decorators/error_logging.py b/src/decorators/error_logging.py new file mode 100644 index 0000000..3d50a27 --- /dev/null +++ b/src/decorators/error_logging.py @@ -0,0 +1,13 @@ +from functools import wraps +from graphql import GraphQLError + + +def error_logging(func): + @wraps(func) + async def decorator(*args, **kwargs): + try: + return await func(*args, **kwargs) + except Exception as e: + raise GraphQLError(f"Not found") + + return decorator diff --git a/src/graphql_schema/entities/flight.py b/src/graphql_schema/entities/flight.py index c6ed3b1..9f27bb6 100644 --- a/src/graphql_schema/entities/flight.py +++ b/src/graphql_schema/entities/flight.py @@ -1,10 +1,10 @@ from datetime import timedelta -from typing import List, Optional, Annotated, TYPE_CHECKING +from typing import List, Optional, Annotated, TYPE_CHECKING, Tuple import strawberry from sqlalchemy import select from strawberry.file_uploads import Upload - from database import models +from decorators.error_logging import error_logging from graphql_schema.dataloaders import copilots_dataloader from graphql_schema.dataloaders.aircraft import aircraft_dataloader from graphql_schema.dataloaders.airport import airport_dataloader @@ -107,7 +107,7 @@ def get_base_query(user_id: Optional[int], username: Optional[str] = None, is_au if username: query = query.filter(models.Flight.created_by.public_username == username) - + if not is_auth: query = query.filter(models.Flight.is_public.is_(True)) @@ -126,6 +126,7 @@ class FlightQueries: return (await info.context.db.scalars(query)).all() @strawberry.field + @error_logging async def flight(root, info, id: int, username: Optional[str] = None) -> Flight: query = ( get_base_query(user_id=info.context.user_id, username=username, is_auth=bool(info.context.user_id)) @@ -135,6 +136,21 @@ class FlightQueries: return (await info.context.db.scalars(query)).one() +async def get_airports(db, takeoff_airport_id: int, landing_airport_id: int) -> Tuple[models.Airport, models.Airport]: + takeoff_airport = (await db.scalars( + select(models.Airport).filter(models.Airport.id == takeoff_airport_id) + )).one() + + if takeoff_airport_id == landing_airport_id: + landing_airport = takeoff_airport + else: + landing_airport = (await db.scalars( + select(models.Airport).filter(models.Airport.id == landing_airport_id) + )).one() + + return takeoff_airport, landing_airport + + @strawberry.type class CreateFlightMutation: @strawberry_sqlalchemy_input(models.Flight, exclude_fields=[ @@ -148,18 +164,20 @@ class CreateFlightMutation: @strawberry.mutation async def create_flight(self, info, input: CreateFlightInput) -> Flight: - aircraft_id = await handle_aircraft_save(info.context.db, info.context.user_id, input.aircraft) - takeoff_airport = (await info.context.db.scalars(select(models.Airport).filter(models.Airport.id == input.takeoff_airport.id))).one() - if input.takeoff_airport.id == input.landing_airport.id: - landing_airport = takeoff_airport - else: - landing_airport = (await info.context.db.scalars(select(models.Airport).filter(models.Airport.id == input.landing_airport.id))).one() + db = info.context.db + aircraft_id = await handle_aircraft_save(db, info.context.user_id, input.aircraft) - weather_takeoff = await handle_weather_info(info.context.db, input.takeoff_datetime, takeoff_airport) - weather_landing = await handle_weather_info(info.context.db, input.landing_datetime, landing_airport) + data = input.to_dict() + data['takeoff_datetime'] = data['takeoff_datetime'].astimezone() + data['landing_datetime'] = data['landing_datetime'].astimezone() - return await models.Flight.create(info.context.db, data={ - **input.to_dict(), + takeoff_airport, landing_airport = await get_airports(db, input.takeoff_airport.id, input.landing_airport.id) + + weather_takeoff = await handle_weather_info(db, input.takeoff_datetime, takeoff_airport) + weather_landing = await handle_weather_info(db, input.landing_datetime, landing_airport) + + return await models.Flight.create(db, data={ + **data, "weather_info_takeoff_id": weather_takeoff.id, "weather_info_landing_id": weather_landing.id, "takeoff_airport_id": takeoff_airport.id, @@ -185,43 +203,77 @@ class EditFlightMutation: @strawberry.mutation async def edit_flight(self, info, id: int, input: EditFlightInput) -> Flight: - # TODO: umoznit editovat jen vlastni lety! + db = info.context.db + user_id = info.context.user_id - flight = (await info.context.db.scalars( - get_base_query(user_id=info.context.user_id, is_auth=bool(info.context.user_id)).filter(models.Flight.id == id) + flight = (await db.scalars( + get_base_query(user_id=user_id, is_auth=bool(user_id)) + .filter(models.Flight.id == id) )).one() data = input.to_dict() + data['takeoff_datetime'] = data['takeoff_datetime'].astimezone() + data['landing_datetime'] = data['landing_datetime'].astimezone() if input.gpx_track is not None: # TODO: poresit validaci uploadovaneho souboru! path = "/app/uploads/tracks" - if flight.gpx_track_filename and file_exists(path+"/"+flight.gpx_track_filename): - delete_file(path+"/"+flight.gpx_track_filename) + if flight.gpx_track_filename and file_exists(path + "/" + flight.gpx_track_filename): + delete_file(path + "/" + flight.gpx_track_filename) data['gpx_track_filename'] = await handle_file_upload(input.gpx_track, path) - if input.takeoff_airport is not None: - # TODO: stahnout nove pocasi na novem miste! Stejne tak pri zmene data/casu odletu - data['takeoff_airport_id'] = input.takeoff_airport.id + update_takeoff_weather = False + update_landing_weather = False - if input.landing_airport is not None: - # TODO: stahnout nove pocasi na novem miste! Stejne tak pri zmene data/casu priletu + if input.takeoff_airport is not None and input.takeoff_airport.id != flight.takeoff_airport_id: + data['takeoff_airport_id'] = input.takeoff_airport.id + update_takeoff_weather = True + + if input.landing_airport is not None and input.landing_airport.id != flight.landing_airport_id: data['landing_airport_id'] = input.landing_airport.id + update_landing_weather = True + + takeoff_airport, landing_airport = await get_airports( + db, + takeoff_airport_id=input.takeoff_airport.id if input.takeoff_airport else flight.takeoff_airport_id, + landing_airport_id=input.landing_airport.id if input.landing_airport else flight.landing_airport_id, + ) + + if input.takeoff_datetime is not None and input.takeoff_datetime != flight.takeoff_datetime: + update_takeoff_weather = True + + if input.landing_datetime is not None and input.landing_datetime != flight.landing_datetime: + update_landing_weather = True + + if update_takeoff_weather: + weather = await handle_weather_info(db, input.takeoff_datetime, takeoff_airport) + + # if flight.weather_info_takeoff_id: + # db.delete(flight.weather_info_takeoff) + + data['weather_info_takeoff_id'] = weather.id + + if update_landing_weather: + weather = await handle_weather_info(db, input.landing_datetime, landing_airport) + + # if flight.weather_info_landing_id: + # db.delete(flight.weather_info_landing) + + data['weather_info_landing_id'] = weather.id + + # TODO: ^^ to pocasi smrdi zbytecne duplicitnim kodem, neslo by to nejak sjednotit? if input.aircraft is not None: - data['aircraft_id'] = await handle_aircraft_save(info.context.db, info.context.user_id, input.aircraft) + data['aircraft_id'] = await handle_aircraft_save(db, user_id, input.aircraft) - flight = await models.Flight.update(info.context.db, id=id, data=data) + data['copilot_id'] = await handle_copilot_edit(db, input.copilot, user_id) if input.copilot else None + + flight = await models.Flight.update(db, id=id, data=data) if input.track is not None: - await handle_track_edit(db=info.context.db, flight=flight, track=input.track, user_id=info.context.user_id) - - if input.copilot: - flight.copilot_id = await handle_copilot_edit(info.context.db, input.copilot, info.context.user_id) - else: - flight.copilot_id = None + await handle_track_edit(db=db, flight=flight, track=input.track, user_id=user_id) return flight diff --git a/src/graphql_schema/entities/helpers/flight.py b/src/graphql_schema/entities/helpers/flight.py index 0042625..bf636e7 100644 --- a/src/graphql_schema/entities/helpers/flight.py +++ b/src/graphql_schema/entities/helpers/flight.py @@ -10,7 +10,7 @@ weather_api = Weather() async def handle_weather_info(db: AsyncSession, date_time: datetime, airport: models.Airport) -> models.WeatherInfo: - weather = await weather_api.get_weather_for_hour(date_time, (airport.gps_latitude, airport.gps_longitude)) + weather = await weather_api.get_weather_for_hour(date_time.astimezone(), (airport.gps_latitude, airport.gps_longitude)) return await models.WeatherInfo.create(db_session=db, data={ "datetime": weather['datetime'], diff --git a/src/graphql_schema/entities/user.py b/src/graphql_schema/entities/user.py index 11377f6..ad9decc 100644 --- a/src/graphql_schema/entities/user.py +++ b/src/graphql_schema/entities/user.py @@ -1,11 +1,14 @@ +from functools import wraps from typing import Optional import strawberry from graphql import GraphQLError from passlib.hash import bcrypt from sqlalchemy import select +from sqlalchemy.exc import NoResultFound from strawberry.file_uploads import Upload from database import models from database.models import User +from decorators.error_logging import error_logging from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type, strawberry_sqlalchemy_input from upload_utils import handle_file_upload, delete_file @@ -32,8 +35,8 @@ class User: @strawberry.type class UserQueries: @strawberry.field + @error_logging async def user(root, info, username: str) -> User: - print("username") if len(username) == 0: raise GraphQLError("Username not set!") @@ -43,6 +46,9 @@ class UserQueries: @strawberry.field async def logged_user(root, info) -> User: + if not info.context.user_id: + raise GraphQLError("Not authenticated") + return (await info.context.db.scalars( select(models.User).filter_by(id=info.context.user_id) )).one() diff --git a/src/graphql_schema/schema.py b/src/graphql_schema/schema.py index 8aec9a1..2ae1fbf 100644 --- a/src/graphql_schema/schema.py +++ b/src/graphql_schema/schema.py @@ -1,11 +1,17 @@ import dataclasses +from typing import List, Optional + import strawberry from fastapi_jwt import JwtAuthorizationCredentials from fastapi_jwt.jwt import JwtAccessBearerCookie +from graphql import GraphQLError +from sqlalchemy.exc import NoResultFound from sqlalchemy.ext.asyncio import AsyncSession from starlette.background import BackgroundTasks from strawberry.extensions import SchemaExtension from strawberry.fastapi import BaseContext +from strawberry.types import ExecutionContext + from .mutation import Mutation from .query import Query diff --git a/src/main.py b/src/main.py index d3fba2c..113f46e 100644 --- a/src/main.py +++ b/src/main.py @@ -1,3 +1,4 @@ +import time from datetime import timedelta from fastapi import FastAPI, APIRouter, Depends, Security, HTTPException from fastapi_jwt import JwtAuthorizationCredentials, JwtAccessBearerCookie, JwtRefreshBearerCookie