Refaktoring a bugfixing

This commit is contained in:
Michal Kváček
2023-10-13 23:24:53 +02:00
parent ce2c023eaf
commit 7f7f79dad6
36 changed files with 698 additions and 722 deletions
+4 -5
View File
@@ -1,21 +1,20 @@
from aiohttp import ClientResponseError
from database import models
from dependencies.db import get_session
from database.transaction import get_session
from external.elevation import elevation_api
from external.gpx_parser import GPXParser
from paths import FLIGHT_GPX_TRACK_PATH
async def add_terrain_elevation_to_flight(flight_id: int, gpx_filename: str):
path = "/app/uploads/tracks" # TODO vytahnout do configu
gpx_parser = GPXParser(f"{path}/{gpx_filename}")
gpx_parser = GPXParser(f"{FLIGHT_GPX_TRACK_PATH}/{gpx_filename}")
coordinates = await gpx_parser.get_coordinates()
try:
elevation = await elevation_api.get_elevation_for_points(coordinates)
tree_with_elevation = gpx_parser.add_terrain_elevation(elevation)
output_name = f"terrain_{gpx_filename}"
gpx_parser.write(tree_with_elevation, f"{path}/{output_name}")
gpx_parser.write(tree_with_elevation, f"{FLIGHT_GPX_TRACK_PATH}/{output_name}")
async with get_session() as db:
await models.Flight.update(
+15
View File
@@ -0,0 +1,15 @@
from utils.image import resize_image
async def resize_photo(path: str, filename: str):
return await resize_image(path, filename, new_width=2500, quality=85)
async def generate_thumbnail(path: str, filename: str, quality: int = 90):
return await resize_image(
path, filename,
new_width=300,
dest_path=f"{path}/thumbs/",
dest_filename=filename,
quality=quality
)
+41
View File
@@ -0,0 +1,41 @@
from datetime import datetime
from typing import Literal
from database import models
from database.transaction import get_session
from external.weather import weather_api
from logger import log
async def download_weather(date_time: datetime, flight_id: int, airport_id: int, type_: Literal['landing', 'takeoff']):
log.error("AAAAAAAAAAAAAAAAAAAAAAAAAAA")
async with get_session() as db:
airport = await models.Airport.get_one(db, airport_id)
gps = (airport.gps_latitude, airport.gps_longitude)
try:
weather = await weather_api.get_weather_for_hour(date_time.astimezone(), gps=gps)
except Exception as e:
log.error(f"Error in downloading weather: {e}")
return None
data = {
"datetime": weather['datetime'],
"qnh": weather['pressure_msl'],
"temperature_surface": weather['temperature_2m'],
"dewpoint_surface": weather['dewpoint_2m'],
"rain": weather['rain'],
"cloudcover_total": weather['cloudcover'],
"cloudcover_low": weather['cloudcover_low'],
"wind_speed_surface": weather['windspeed_10m'],
"wind_direction_surface": weather['winddirection_10m'],
}
async with get_session() as db:
flight = await models.Flight.get_one(db, flight_id)
existing_weather_id = getattr(flight, f'{type_}_weather_info_id')
if existing_weather_id:
await models.WeatherInfo.update(db, id=existing_weather_id, data=data)
else:
weather_model = await models.WeatherInfo.create(db, data=data)
await models.Flight.update(db, obj=flight, data={f"{type_}_weather_info_id": weather_model.id})
+7 -7
View File
@@ -10,12 +10,12 @@ from sqlalchemy.ext.asyncio import AsyncSession
class BaseModel:
excluded_columns_in_dict = ("deleted",)
@classmethod
def _get_column_names(cls):
return [col.name for col in cls.__table__.columns]
def as_dict(self):
return {
c.name: getattr(self, c.name)
for c in self.__table__.columns
if c.name not in self.excluded_columns_in_dict
}
return {c: getattr(self, c) for c in self._get_column_names() if c not in self.excluded_columns_in_dict}
@classmethod
async def get_one(cls, db_session: AsyncSession, id: int):
@@ -23,7 +23,7 @@ class BaseModel:
@classmethod
async def create(cls, db_session: AsyncSession, data: dict):
model = cls(**data)
model = cls(**{col: data[col] for col in cls._get_column_names() if col in data})
db_session.add(model)
await db_session.flush()
@@ -37,7 +37,7 @@ class BaseModel:
if not obj:
obj = await cls.get_one(db_session, id)
for key, value in data.items():
if getattr(obj, key) != value:
if key in cls._get_column_names() and getattr(obj, key) != value:
setattr(obj, key, value)
return obj
View File
-10
View File
@@ -1,10 +0,0 @@
# from fastapi import Depends, Security
# from fastapi_jwt import JwtAuthorizationCredentials
#
#
# async def check_jwt_token():
# authorize.jwt_required()
#
# subject = authorize.get_jwt_subject()
#
# print("Subject: ", subject)
+1 -1
View File
@@ -4,7 +4,7 @@ from passlib.hash import bcrypt
from sqlalchemy import select
from starlette.responses import Response
from database.models import User
from dependencies.db import get_session
from database.transaction import get_session
from endpoints.base import AuthEndpoint
from pydantic import BaseModel
+1 -1
View File
@@ -4,7 +4,7 @@ from sqlalchemy import select
from typing import Optional
from pydantic import BaseModel, root_validator, Field
from database.models import User
from dependencies.db import get_session
from database.transaction import get_session
from passlib.hash import bcrypt
+5 -3
View File
@@ -53,16 +53,19 @@ class GPXParser:
@cached()
async def get_max_speed(self):
return max(await self.get_speed())
return max(await self.get_speed()) or 0
@cached()
async def get_avg_speed(self):
speeds = await self.get_speed()
if not speeds:
return 0
return round(sum(speeds) / len(speeds), 2)
@cached()
async def get_max_altitude(self):
return max(await self.get_altitude())
return max(await self.get_altitude()) or 0
@cached()
async def get_avg_altitude(self):
@@ -89,5 +92,4 @@ class GPXParser:
return self.gpx
def write(self, tree: _ElementTree, output: str):
print(f"ZAPISUJI DO {output}")
tree.write(output)
+32 -62
View File
@@ -1,56 +1,46 @@
from typing import List, Optional, TYPE_CHECKING
from typing import List
import strawberry
from strawberry.file_uploads import Upload
from database import models
from decorators.endpoints import authenticated_user_only
from dependencies.db import get_session
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_input
from upload_utils import handle_file_upload, delete_file
from database.transaction import get_session
from graphql_schema.entities.helpers.combobox import handle_combobox_save
from .resolvers.aircraft import get_aircraft_resolver
from .resolvers.base import get_list, get_one
from graphql_schema.entities.types.mutation_input import ComboboxInput
from paths import AIRCRAFT_UPLOAD_DEST_PATH
from utils.file import delete_file
from utils.upload import handle_file_upload
from .resolvers.aircraft import AircraftMutationResolver, AircraftQueryResolver
from graphql_schema.entities.types.mutation_input import CreateAircraftInput, EditAircraftInput
from graphql_schema.entities.types.types import Aircraft
if TYPE_CHECKING:
pass
AIRCRAFT_UPLOAD_DEST_PATH = "/app/uploads/aircrafts/"
@strawberry.type
class AircraftQueries:
@strawberry.field()
@authenticated_user_only()
async def aircrafts(root, info) -> List[Aircraft]:
query = get_aircraft_resolver(info.context.user_id, info.context.organization_ids)
return await get_list(models.Aircraft, query)
return await AircraftQueryResolver().get_list(
info.context.user_id,
organization_ids=info.context.organization_ids
)
@strawberry.field()
@authenticated_user_only()
async def aircraft(root, info, id: int) -> Aircraft:
query = get_aircraft_resolver(info.context.user_id, info.context.organization_ids, id)
return await get_one(models.Aircraft, query)
return await AircraftQueryResolver().get_one(
id, user_id=info.context.user_id, organization_ids=info.context.organization_ids
)
@strawberry.type
class CreateAircraftMutation:
@strawberry_sqlalchemy_input(models.Aircraft, exclude_fields=['id', 'photo_filename'])
class CreateAircraftInput:
photo: Optional[Upload]
organization: Optional[ComboboxInput] = None
class AircraftMutation:
@strawberry.mutation
@authenticated_user_only()
async def create_aircraft(root, info, input: CreateAircraftInput) -> Aircraft:
# TODO: kontrola organizace
input_data = input.to_dict()
if input.photo:
input_data['photo_filename'] = await handle_file_upload(input.photo, AIRCRAFT_UPLOAD_DEST_PATH)
async with get_session() as db:
if input.organization:
if input.organization:
async with get_session() as db:
input_data['organization_id'] = await handle_combobox_save(
db,
models.Organization,
@@ -58,29 +48,11 @@ class CreateAircraftMutation:
user_id=info.context.user_id,
)
aircraft = await models.Aircraft.create(
db,
data=dict(
**input_data,
created_by_id=info.context.user_id,
)
)
return Aircraft(**aircraft.as_dict())
@strawberry.type
class EditAircraftMutation:
@strawberry_sqlalchemy_input(models.Aircraft, exclude_fields=['photo_filename'])
class EditAircraftInput:
photo: Optional[Upload]
organization: Optional[ComboboxInput] = None
return await AircraftMutationResolver().create(data=input_data, user_id=info.context.user_id)
@strawberry.mutation
@authenticated_user_only()
async def edit_aircraft(root, info, id: int, input: EditAircraftInput) -> Aircraft:
# TODO: kontrola organizace
update_data = input.to_dict()
async with get_session() as db:
if input.organization:
@@ -91,26 +63,24 @@ class EditAircraftMutation:
user_id=info.context.user_id,
)
aircraft = (await db.scalars(get_aircraft_resolver(user_id=info.context.user_id, aircraft_id=id))).one()
query = AircraftQueryResolver().get_query(
user_id=info.context.user_id,
object_id=id,
organization_ids=info.context.organization_ids
)
aircraft = (await db.scalars(query)).one()
existing_photo_filename = aircraft.photo_filename
if input.photo:
if aircraft.photo_filename:
delete_file(AIRCRAFT_UPLOAD_DEST_PATH + "/" + aircraft.photo_filename, silent=True)
update_data['photo_filename'] = await handle_file_upload(input.photo, AIRCRAFT_UPLOAD_DEST_PATH)
aircraft = await models.Aircraft.update(db, obj=aircraft, data=update_data)
if input.photo:
if existing_photo_filename:
delete_file(AIRCRAFT_UPLOAD_DEST_PATH + "/" + existing_photo_filename, silent=True)
update_data['photo_filename'] = await handle_file_upload(input.photo, AIRCRAFT_UPLOAD_DEST_PATH)
async with get_session() as db:
aircraft = await models.Aircraft.update(db, id=id, data=update_data)
return Aircraft(**aircraft.as_dict())
@strawberry.type
class DeleteAircraftMutation:
@strawberry.mutation
@authenticated_user_only()
async def delete_aircraft(self, info, id: int) -> Aircraft:
async with get_session() as db:
aircraft = (await db.scalars(get_aircraft_resolver(info.context.user_id, aircraft_id=id))).one()
aircraft = await models.Aircraft.update(db, obj=aircraft, data=dict(deleted=True))
return Aircraft(**aircraft.as_dict())
return await AircraftMutationResolver().delete(info.context.user_id, id)
+4 -11
View File
@@ -2,13 +2,8 @@ from typing import List
import strawberry
from database import models
from decorators.endpoints import authenticated_user_only
from graphql_schema.entities.resolvers.base import get_list, get_one, get_base_resolver
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type
@strawberry_sqlalchemy_type(models.Airport)
class Airport:
pass
from graphql_schema.entities.resolvers.base import BaseQueryResolver
from graphql_schema.entities.types.types import Airport
@strawberry.type
@@ -16,11 +11,9 @@ class AirportQueries:
@strawberry.field()
@authenticated_user_only()
async def airports(root, info) -> List[Airport]:
query = get_base_resolver(models.Airport, user_id=info.context.user_id)
return await get_list(models.Airport, query)
return await BaseQueryResolver(Airport, models.Airport).get_list(info.context.user_id)
@strawberry.field()
@authenticated_user_only()
async def airport(root, info, id: int) -> Airport:
query = get_base_resolver(models.Airport, user_id=info.context.user_id, object_id=id)
return await get_one(models.Airport, query)
return await BaseQueryResolver(Airport, models.Airport).get_one(id, user_id=info.context.user_id)
+11 -31
View File
@@ -1,65 +1,45 @@
from typing import List, TYPE_CHECKING
from typing import List
import strawberry
from database import models
from decorators.endpoints import authenticated_user_only
from dependencies.db import get_session
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_input
from .resolvers.base import get_base_resolver, get_list, get_one
from database.transaction import get_session
from graphql_schema.entities.resolvers.base import BaseQueryResolver, BaseMutationResolver
from graphql_schema.entities.types.mutation_input import CreateCopilotInput, EditCopilotInput
from graphql_schema.entities.types.types import Copilot
if TYPE_CHECKING:
pass
@strawberry.type
class CopilotQueries:
@strawberry.field()
@authenticated_user_only()
async def copilots(root, info) -> List[Copilot]:
query = get_base_resolver(models.Copilot, user_id=info.context.user_id, order_by=[models.Copilot.name])
return await get_list(models.Copilot, query)
return await BaseQueryResolver(Copilot, models.Copilot).get_list(info.context.user_id)
@strawberry.field()
@authenticated_user_only()
async def copilot(root, info, id: int) -> Copilot:
query = get_base_resolver(models.Copilot, object_id=id, user_id=info.context.user_id)
return await get_one(models.Copilot, query)
return await BaseQueryResolver(Copilot, models.Copilot).get_one(id, user_id=info.context.user_id)
@strawberry.type
class CreateCopilotMutation:
@strawberry_sqlalchemy_input(model=models.Copilot, exclude_fields=["id"])
class CreateCopilotInput:
pass
@strawberry.mutation
@authenticated_user_only()
async def create_copilot(root, info, input: CreateCopilotInput) -> Copilot:
input_data = input.to_dict()
async with get_session() as db:
copilot = await models.Copilot.create(
db,
data=dict(
**input_data,
created_by_id=info.context.user_id,
)
)
return Copilot(**copilot.as_dict())
return await BaseMutationResolver(Copilot, models.Copilot).create(
data=input.to_dict(),
user_id=info.context.user_id
)
@strawberry.type
class EditCopilotMutation:
@strawberry_sqlalchemy_input(model=models.Copilot, exclude_fields=["id"])
class EditCopilotInput:
pass
@strawberry.mutation
@authenticated_user_only()
async def edit_copilot(root, info, id: int, input: EditCopilotInput) -> Copilot:
async with get_session() as db:
copilot = (await db.scalars(
query=get_base_resolver(models.Copilot, object_id=id, user_id=info.context.user_id)
BaseQueryResolver(Copilot, models.Copilot).get_query(object_id=id, user_id=info.context.user_id)
)).one()
updated_copilot = await models.Copilot.update(db, obj=copilot, data=input.to_dict())
+10 -36
View File
@@ -1,68 +1,42 @@
from typing import List, TYPE_CHECKING
from typing import List
import strawberry
from database import models
from decorators.endpoints import authenticated_user_only
from dependencies.db import get_session
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_input
from .resolvers.base import get_base_resolver, get_list, get_one
from database.transaction import get_session
from graphql_schema.entities.resolvers.base import BaseQueryResolver, BaseMutationResolver
from graphql_schema.entities.types.mutation_input import CreateEventInput, EditEventInput
from graphql_schema.entities.types.types import Event
if TYPE_CHECKING:
pass
@strawberry.type
class EventQueries:
@strawberry.field()
@authenticated_user_only()
async def events(root, info) -> List[Event]:
query = get_base_resolver(
models.Event, user_id=info.context.user_id,
return await BaseQueryResolver(Event, models.Event).get_list(
info.context.user_id,
order_by=[models.Event.date_from.desc(), models.Event.id.desc()]
)
return await get_list(models.Event, query)
@strawberry.field()
@authenticated_user_only()
async def event(root, info, id: int) -> Event:
query = get_base_resolver(models.Event, user_id=info.context.user_id, object_id=id)
return await get_one(models.Event, query)
return await BaseQueryResolver(Event, models.Event).get_one(id, user_id=info.context.user_id)
@strawberry.type
class CreateEventMutation:
@strawberry_sqlalchemy_input(model=models.Event, exclude_fields=["id"])
class CreateEventInput:
pass
class EventMutation:
@strawberry.mutation
@authenticated_user_only()
async def create_event(root, info, input: CreateEventInput) -> Event:
input_data = input.to_dict()
async with get_session() as db:
event = await models.Event.create(
db,
data=dict(
**input_data,
created_by_id=info.context.user_id,
)
)
return Event(**event.as_dict())
@strawberry.type
class EditEventMutation:
@strawberry_sqlalchemy_input(model=models.Event, exclude_fields=["id"])
class EditEventInput:
pass
return await BaseMutationResolver(Event, models.Event).create(input.to_dict(), info.context.user_id)
@strawberry.mutation
@authenticated_user_only()
async def edit_event(root, info, id: int, input: EditEventInput) -> Event:
async with get_session() as db:
event = (await db.scalars(
get_base_resolver(models.Event, user_id=info.context.user_id, object_id=id)
BaseQueryResolver(Event, models.Event).get_query(user_id=info.context.user_id, object_id=id)
)).one()
updated_event = await models.Event.update(db, obj=event, data=input.to_dict())
+40 -60
View File
@@ -2,43 +2,18 @@ import asyncio
from typing import List, Optional
import strawberry
from fastapi import HTTPException
from sqlalchemy import select
from starlette.status import HTTP_401_UNAUTHORIZED
from background_jobs.weather import download_weather
from database import models
from decorators.endpoints import authenticated_user_only
from decorators.error_logging import error_logging
from dependencies.db import get_session
from graphql_schema.entities.resolvers.flight import (
handle_aircraft_save, handle_weather_info, FlightMutationResolver, get_airport,
)
from database.transaction import get_session
from graphql_schema.entities.resolvers.flight import handle_aircraft_save, FlightMutationResolver, FlightQueryResolver
from graphql_schema.entities.types.mutation_input import EditFlightInput, CreateFlightInput
from .resolvers.base import get_list, get_one
from .helpers.combobox import handle_combobox_save
from graphql_schema.entities.types.types import Flight
def get_base_query(user_id: Optional[int], username: Optional[str] = None, is_auth: bool = False):
query = (
select(models.Flight)
.filter(models.Flight.deleted.is_(False))
.order_by(models.Flight.takeoff_datetime.desc())
)
if user_id:
query = query.filter(models.Flight.created_by_id == user_id)
if username:
query = (
query
.join(models.Flight.created_by)
.filter(models.User.public_username == username)
)
if not is_auth:
query = query.filter(models.Flight.is_public.is_(True))
return query
@strawberry.type
class FlightQueries:
@@ -47,11 +22,7 @@ class FlightQueries:
if not info.context.user_id and not username:
raise HTTPException(HTTP_401_UNAUTHORIZED)
query = (
get_base_query(user_id=info.context.user_id, username=username, is_auth=bool(info.context.user_id))
.order_by(models.Flight.id.desc())
)
return await get_list(models.Flight, query)
return await FlightQueryResolver().get_list(user_id=info.context.user_id, username=username)
@strawberry.field()
@error_logging
@@ -59,11 +30,7 @@ class FlightQueries:
if not info.context.user_id and not username:
raise HTTPException(HTTP_401_UNAUTHORIZED)
query = (
get_base_query(user_id=info.context.user_id, username=username, is_auth=bool(info.context.user_id))
.filter(models.Flight.id == id)
)
return await get_one(models.Flight, query)
return await FlightQueryResolver().get_one(id, user_id=info.context.user_id, username=username)
@strawberry.type
@@ -72,36 +39,49 @@ class FlightMutation:
@authenticated_user_only()
async def create_flight(self, info, input: CreateFlightInput) -> Flight:
data = input.to_dict()
async with (get_session() as db):
takeoff_airport = await get_airport(db, input.takeoff_airport, info.context.user_id)
landing_airport = await get_airport(db, input.landing_airport, info.context.user_id)
user_id = info.context.user_id
async with get_session() as db:
aircraft_id = await handle_aircraft_save(db, info.context.user_id, input.aircraft)
weather_takeoff, weather_landing = await asyncio.gather(
handle_weather_info(db, data['takeoff_datetime'], takeoff_airport),
handle_weather_info(db, data['landing_datetime'], landing_airport)
)
await db.flush()
flight = await models.Flight.create(db, data={
**data,
"takeoff_weather_info_id": weather_takeoff.id if weather_takeoff else None,
"landing_weather_info_id": weather_landing.id if weather_landing else None,
"takeoff_airport_id": takeoff_airport.id,
"landing_airport_id": landing_airport.id,
"has_terrain_elevation": False,
# TODO: tohle je blbost, bude to vyrabet dve stejne instance!
takeoff_airport_id, landing_airport_id = await asyncio.gather(
handle_combobox_save(
db, models.Airport, input.takeoff_airport, user_id, name_column="icao_code",
extra_data={"name": input.takeoff_airport.name}
),
handle_combobox_save(
db, models.Airport, input.landing_airport, user_id, name_column="icao_code",
extra_data={"name": input.landing_airport.name}
)
)
data.update({
"takeoff_airport_id": takeoff_airport_id,
"landing_airport_id": landing_airport_id,
"aircraft_id": aircraft_id,
"created_by_id": info.context.user_id
"has_terrain_elevation": False,
"name": "",
"description": ""
})
return Flight(**flight.as_dict())
flight = FlightMutationResolver().create(data, info.context.user_id)
info.context.background_tasks.add_task(
download_weather,
flight_id=flight.id, airport_id=takeoff_airport_id, date_time=flight.takeoff_datetime, type_="takeoff"
)
info.context.background_tasks.add_task(
download_weather,
flight_id=flight.id, airport_id=landing_airport_id, date_time=flight.landing_datetime, type_="landing"
)
return flight
@strawberry.mutation
@authenticated_user_only()
async def edit_flight(self, info, id: int, input: EditFlightInput) -> Flight:
return await FlightMutationResolver.update(info.context, id, input)
return await FlightMutationResolver().update(info.context, id, input)
@strawberry.mutation
@authenticated_user_only()
async def delete_flight(self, info, id: int) -> Flight:
return await FlightMutationResolver.delete(info.context.user_id, id)
return await FlightMutationResolver().delete(info.context.user_id, id)
+31 -46
View File
@@ -1,54 +1,54 @@
from typing import List, TYPE_CHECKING
from typing import List
import strawberry
from sqlalchemy import delete
from sqlalchemy.dialects.mysql import insert
from sqlalchemy.exc import IntegrityError
from database import models
from decorators.endpoints import authenticated_user_only
from dependencies.db import get_session
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_input
from .resolvers.base import get_base_resolver, get_list, get_one
from database.transaction import get_session
from graphql_schema.entities.resolvers.base import BaseQueryResolver, BaseMutationResolver
from graphql_schema.entities.types.mutation_input import CreateOrganizationInput, EditOrganizationInput
from graphql_schema.entities.types.types import Organization
if TYPE_CHECKING:
pass
@strawberry.type
class OrganizationQueries:
@strawberry.field()
@authenticated_user_only()
async def organizations(root, info) -> List[Organization]:
query = get_base_resolver(models.Organization, order_by=[models.Organization.name])
return await get_list(models.Organization, query)
return await BaseQueryResolver(Organization, models.Organization).get_list(
info.context.user_id, order_by=[models.Organization.name]
)
@strawberry.field()
@authenticated_user_only()
async def organization(root, info, id: int) -> Organization:
query = get_base_resolver(models.Organization, object_id=id)
return await get_one(models.Organization, query)
return await BaseQueryResolver(Organization, models.Organization).get_one(id, info.context.user_id)
@strawberry.type
class CreateOrganizationMutation:
@strawberry_sqlalchemy_input(model=models.Organization, exclude_fields=["id"])
class CreateOrganizationInput:
pass
class OrganizationMutation:
@strawberry.mutation
@authenticated_user_only()
async def create_organization(root, info, input: CreateOrganizationInput) -> Organization:
input_data = input.to_dict()
async with get_session() as db:
organization = await models.Organization.create(
db,
data=dict(
**input_data,
created_by_id=info.context.user_id,
)
)
return await BaseMutationResolver(Organization, models.Organization).create(
data=input.to_dict(),
user_id=info.context.user_id
)
return Organization(**organization.as_dict())
@strawberry.mutation
@authenticated_user_only()
async def edit_organization(root, info, id: int, input: EditOrganizationInput) -> Organization:
async with get_session() as db:
organization = (await db.scalars(
BaseQueryResolver(Organization, models.Organization).get_query(
user_id=info.context.user_id, object_id=id
)
)).one()
updated_organization = await models.Organization.update(db, obj=organization, data=input.to_dict())
return Organization(**updated_organization.as_dict())
@strawberry.type
@@ -58,7 +58,9 @@ class OrganizationUserMutation:
@authenticated_user_only()
async def add_to_organization(root, info, organization_id: int) -> Organization:
async with get_session() as db:
organization = (await db.scalars(get_base_resolver(models.Organization, object_id=organization_id))).one()
organization = (await db.scalars(
BaseQueryResolver(Organization, models.Organization).get_query(object_id=organization_id)
)).one()
try:
await db.execute(
@@ -68,7 +70,6 @@ class OrganizationUserMutation:
)
)
except IntegrityError:
print("jiz existuje")
pass
return Organization(**organization.as_dict())
@@ -77,7 +78,9 @@ class OrganizationUserMutation:
@authenticated_user_only()
async def remove_from_organization(root, info, organization_id: int) -> Organization:
async with get_session() as db:
organization = (await db.scalars(get_base_resolver(models.Organization, object_id=organization_id))).one()
organization = (await db.scalars(
BaseQueryResolver(Organization, models.Organization).get_query(object_id=organization_id)
)).one()
await db.execute(
delete(models.user_is_in_organization).filter_by(
@@ -87,21 +90,3 @@ class OrganizationUserMutation:
)
return Organization(**organization.as_dict())
@strawberry.type
class EditOrganizationMutation:
@strawberry_sqlalchemy_input(model=models.Organization, exclude_fields=["id"])
class EditOrganizationInput:
pass
@strawberry.mutation
@authenticated_user_only()
async def edit_organization(root, info, id: int, input: EditOrganizationInput) -> Organization:
async with get_session() as db:
organization = (await db.scalars(
get_base_resolver(models.Organization, user_id=info.context.user_id, object_id=id)
)).one()
updated_organization = await models.Organization.update(db, obj=organization, data=input.to_dict())
return Organization(**updated_organization.as_dict())
+38 -41
View File
@@ -4,17 +4,17 @@ import strawberry
from sqlalchemy import update
from strawberry.file_uploads import Upload
from background_jobs.elevation import add_terrain_elevation_to_photo
from background_jobs.photo import generate_thumbnail, resize_photo
from database import models
from decorators.endpoints import authenticated_user_only
from dependencies.db import get_session
from database.transaction import get_session
from graphql_schema.entities.resolvers.base import BaseQueryResolver, BaseMutationResolver
from graphql_schema.entities.types.types import Photo
from paths import get_photo_basepath
from upload_utils import (
handle_file_upload, delete_file, parse_exif_info, generate_thumbnail, resize_image,
rotate_image
)
from graphql_schema.entities.helpers.combobox import handle_combobox_save
from .resolvers.base import get_base_resolver, get_list
from utils.file import delete_file
from utils.image import parse_exif_info, rotate_image
from utils.upload import handle_file_upload
from graphql_schema.entities.types.mutation_input import ComboboxInput
@@ -22,8 +22,7 @@ from graphql_schema.entities.types.mutation_input import ComboboxInput
class PhotoQueries:
@strawberry.field()
async def photos(root, info) -> List[Photo]:
query = get_base_resolver(models.Photo, user_id=info.context.user_id)
return await get_list(models.Photo, query)
return await BaseQueryResolver(Photo, models.Photo).get_list(user_id=info.context.user_id)
@strawberry.type
@@ -58,8 +57,7 @@ class UploadPhotoMutation:
}, db_session=db)
photo = Photo(**photo_model.as_dict())
# TODO: udelat primo konkretni bg joby na resize a thumbnaily
info.context.background_tasks.add_task(resize_image, path=path, filename=filename, new_width=2500, quality=85)
info.context.background_tasks.add_task(resize_photo, path=path, filename=filename)
info.context.background_tasks.add_task(generate_thumbnail, path=path, filename=filename)
if exif_info.get("gps_latitude") and exif_info.get("gps_longitude"):
@@ -77,19 +75,20 @@ class EditPhotoMutation:
point_of_interest: Optional[ComboboxInput] = None
is_flight_cover: Optional[bool] = None
def to_dict(self):
return {
key: getattr(self, key) for key in ('name', 'description', 'is_flight_cover')
if getattr(self, key) is not None
}
@strawberry.mutation()
@authenticated_user_only()
async def edit_photo(self, info, id: int, input: EditPhotoInput) -> Photo:
# TODO: base trida pro inputy s definici to_dict/as_dict?
data = {
key: getattr(input, key) for key in ('name', 'description', 'is_flight_cover')
if getattr(input, key) is not None
}
query = get_base_resolver(models.Photo, user_id=info.context.user_id, object_id=id)
data = input.to_dict()
async with get_session() as db:
photo = (await db.scalars(query)).one()
photo = (await db.scalars(
BaseQueryResolver(Photo, models.Photo).get_query(user_id=info.context.user_id, object_id=id)
)).one()
if input.point_of_interest:
data['point_of_interest_id'] = await handle_combobox_save(
@@ -116,25 +115,27 @@ class EditPhotoMutation:
@strawberry.mutation()
@authenticated_user_only()
async def rotate_photo(self, info, id: int, angle: int) -> Photo:
async with get_session() as db:
query = get_base_resolver(models.Photo, user_id=info.context.user_id, object_id=id)
photo = (await db.scalars(query)).one()
photo = (await db.scalars(
BaseQueryResolver(Photo, models.Photo).get_query(user_id=info.context.user_id, object_id=id)
)).one()
photo_filename = photo.filename
photo_as_dict = photo.as_dict()
await asyncio.gather(
rotate_image(
path=get_photo_basepath(photo.flight_id),
filename=photo.filename,
angle=angle,
),
rotate_image(
path=get_photo_basepath(photo.flight_id) + "/thumbs",
filename=photo.filename,
angle=angle,
),
)
await asyncio.gather(
rotate_image(
path=get_photo_basepath(photo.flight_id),
filename=photo_filename,
angle=angle,
),
rotate_image(
path=get_photo_basepath(photo.flight_id) + "/thumbs",
filename=photo_filename,
angle=angle,
),
)
return Photo(**photo.as_dict())
return Photo(**photo_as_dict)
@strawberry.type
@@ -142,14 +143,10 @@ class DeletePhotoMutation:
@strawberry.mutation()
@authenticated_user_only()
async def delete_photo(self, info, id: int) -> Photo:
query = get_base_resolver(models.Photo, user_id=info.context.user_id, object_id=id)
async with get_session() as db:
photo_model = (await db.scalars(query)).one()
await db.delete(photo_model)
photo = Photo(**photo_model.as_dict())
photo = await BaseMutationResolver(Photo, models.Photo).delete(user_id=info.context.user_id, id=id)
base_path = get_photo_basepath(photo.flight_id)
delete_file(f"{base_path}/{photo.filename}", silent=True)
delete_file(f"{base_path}/thumbs/{photo.filename}", silent=True)
return photo_model
return photo
+13 -38
View File
@@ -1,13 +1,12 @@
from typing import List, Optional
from typing import List
import strawberry
from database import models
from decorators.endpoints import authenticated_user_only
from dependencies.db import get_session
from database.transaction import get_session
from graphql_schema.entities.helpers.combobox import handle_combobox_save
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_input
from graphql_schema.entities.resolvers.base import BaseQueryResolver, BaseMutationResolver
from graphql_schema.entities.types.types import PointOfInterest
from .resolvers.base import get_base_resolver, get_list, get_one
from graphql_schema.entities.types.mutation_input import ComboboxInput
from graphql_schema.entities.types.mutation_input import CreatePointOfInterestInput, EditPointOfInterestInput
@strawberry.type
@@ -15,22 +14,16 @@ class PointOfInterestQueries:
@strawberry.field()
@authenticated_user_only()
async def points_of_interest(root, info) -> List[PointOfInterest]:
query = get_base_resolver(models.PointOfInterest, user_id=info.context.user_id)
return await get_list(models.PointOfInterest, query)
return await BaseQueryResolver(PointOfInterest, models.PointOfInterest).get_list(info.context.user_id)
@strawberry.field()
@authenticated_user_only()
async def point_of_interest(root, info, id: int) -> PointOfInterest:
query = get_base_resolver(models.PointOfInterest, user_id=info.context.user_id, object_id=id)
return await get_one(models.PointOfInterest, query)
return await BaseQueryResolver(PointOfInterest, models.PointOfInterest).get_one(id, info.context.user_id)
@strawberry.type
class CreatePointOfInterestMutation:
@strawberry_sqlalchemy_input(models.PointOfInterest, exclude_fields=['id', 'type_id'])
class CreatePointOfInterestInput:
type: Optional[ComboboxInput] = None
class PointOfInterestMutation:
@strawberry.mutation
@authenticated_user_only()
async def create_point_of_interest(root, info, input: CreatePointOfInterestInput) -> PointOfInterest:
@@ -42,23 +35,17 @@ class CreatePointOfInterestMutation:
db, models.PointOfInterestType, input.type, info.context.user_id
)
poi = await models.PointOfInterest.create(db, data=dict(**input_data, created_by_id=info.context.user_id))
return PointOfInterest(**poi.as_dict())
@strawberry.type
class EditPointOfInterestMutation:
@strawberry_sqlalchemy_input(models.PointOfInterest, exclude_fields=['id', 'type_id'])
class EditPointOfInterestInput:
type: Optional[ComboboxInput] = None
return await BaseMutationResolver(PointOfInterest, models.PointOfInterest).create(
input_data, info.context.user_id
)
@strawberry.mutation
@authenticated_user_only()
async def edit_point_of_interest(root, info, id: int, input: EditPointOfInterestInput) -> PointOfInterest:
input_data = input.to_dict()
query = get_base_resolver(
models.PointOfInterest, user_id=info.context.user_id, object_id=id, include_public=False
query = BaseQueryResolver(PointOfInterest, models.PointOfInterest).get_query(
user_id=info.context.user_id, object_id=id, include_public=False
)
async with get_session() as db:
@@ -71,19 +58,7 @@ class EditPointOfInterestMutation:
updated_poi = await models.PointOfInterest.update(db, obj=poi, data=input_data)
return PointOfInterest(**updated_poi.as_dict())
@strawberry.type
class DeletePointOfInterestMutation:
@strawberry.mutation
@authenticated_user_only()
async def delete_point_of_interest(self, info, id: int) -> PointOfInterest:
query = get_base_resolver(
models.PointOfInterest, user_id=info.context.user_id, object_id=id, include_public=False
)
async with get_session() as db:
poi = (await db.scalars(query)).one()
updated_poi = await models.PointOfInterest.update(db, obj=poi, data=dict(deleted=True))
return PointOfInterest(**updated_poi.as_dict())
return await BaseMutationResolver(PointOfInterest, models.PointOfInterest).delete(info.context.user_id, id=id)
+5 -5
View File
@@ -2,7 +2,7 @@ from typing import List
import strawberry
from database import models
from decorators.endpoints import authenticated_user_only
from graphql_schema.entities.resolvers.base import get_base_resolver, get_list, get_one
from graphql_schema.entities.resolvers.base import BaseQueryResolver
from graphql_schema.entities.types.types import PointOfInterestType
@@ -12,14 +12,14 @@ class PointOfInterestTypeQueries:
@strawberry.field()
@authenticated_user_only()
async def point_of_interest_types(root, info) -> List[PointOfInterestType]:
query = get_base_resolver(models.PointOfInterestType, user_id=info.context.user_id)
return await get_list(models.PointOfInterestType, query)
return await BaseQueryResolver(PointOfInterestType, models.PointOfInterestType).get_list(info.context.user_id)
@strawberry.field()
@authenticated_user_only()
async def point_of_interest_type(root, info, id: int) -> PointOfInterestType:
query = get_base_resolver(models.PointOfInterestType, user_id=info.context.user_id, object_id=id)
return await get_one(models.PointOfInterestType, query)
return await BaseQueryResolver(PointOfInterestType, models.PointOfInterestType).get_one(
id, info.context.user_id
)
#
# @strawberry.type
@@ -1,24 +1,41 @@
from operator import or_
from typing import Set, Optional
from database import models
from graphql_schema.entities.resolvers.base import get_base_resolver
from graphql_schema.entities.resolvers.base import BaseMutationResolver, BaseQueryResolver
from graphql_schema.entities.types.types import Aircraft
def get_aircraft_resolver(user_id: int, organization_ids: Optional[Set[int]] = None, aircraft_id: Optional[int] = None):
query = get_base_resolver(
model=models.Aircraft,
order_by=[models.Aircraft.id.desc()],
object_id=aircraft_id,
user_id=user_id if not organization_ids else None
)
if organization_ids:
query = (
query.filter(
or_(
models.Aircraft.created_by_id == user_id,
models.Aircraft.organization_id.in_(organization_ids)
class AircraftQueryResolver(BaseQueryResolver):
def __init__(self):
super().__init__(graphql_type=Aircraft, model=models.Aircraft)
def get_query(
self,
user_id: Optional[int] = None,
object_id: Optional[int] = None,
order_by: Optional[list] = None,
organization_ids: Optional[Set[int]] = None,
*args,
**kwargs,
):
query = super().get_query(
order_by=[models.Aircraft.id.desc()],
object_id=object_id,
user_id=user_id if not organization_ids else None
)
if organization_ids:
query = (
query.filter(
or_(
models.Aircraft.created_by_id == user_id,
models.Aircraft.organization_id.in_(organization_ids)
)
)
)
)
return query
return query
class AircraftMutationResolver(BaseMutationResolver):
def __init__(self):
super().__init__(graphql_type=Aircraft, model=models.Aircraft)
+101 -46
View File
@@ -1,53 +1,108 @@
from typing import Optional, Type
from typing import Optional, Type, T
from sqlalchemy import select, or_
from sqlalchemy.ext.asyncio import AsyncSession
from database import models
from dependencies.db import get_session
from database.transaction import get_session
def get_base_resolver(
model: Type[models.BaseModel],
object_id: Optional[int] = None,
user_id: Optional[int] = None,
order_by: Optional[list] = None,
include_public: Optional[bool] = True,
):
query = select(model)
class BaseQueryResolver:
if object_id:
if hasattr(model, "id"):
query = query.filter(model.id == object_id)
def __init__(self, graphql_type, model):
self.graphql_type = graphql_type
self.model = model
def get_query(
self,
user_id: Optional[int] = None,
object_id: Optional[int] = None,
order_by: Optional[list] = None,
include_public: Optional[bool] = True,
*args,
**kwargs,
):
query = select(self.model)
if object_id:
if hasattr(self.model, "id"):
query = query.filter(self.model.id == object_id)
else:
raise AssertionError(f"Model {self.model} has no ID column! Cannot query by ID!")
if hasattr(self.model, "deleted"):
query = query.filter(self.model.deleted.is_(False))
ownership_clause = []
if hasattr(self.model, "is_public") and include_public:
ownership_clause.append(self.model.is_public.is_(True))
if hasattr(self.model, "created_by_id") and user_id:
ownership_clause.append(self.model.created_by_id == user_id)
if len(ownership_clause) > 1:
query = query.filter(or_(*ownership_clause))
elif len(ownership_clause) == 1:
query = query.filter(*ownership_clause)
if order_by:
query = query.order_by(*order_by)
return query
async def _get_list(self, query):
async with get_session() as db:
items = (await db.scalars(query)).all()
return [self.model(**m.as_dict()) for m in items]
async def _get_one(self, query):
async with get_session() as db:
data = (await db.scalars(query)).one()
return self.model(**data.as_dict())
async def get_list(self, user_id: Optional[int] = None, **kwargs) -> list:
query = self.get_query(user_id, **kwargs)
return await self._get_list(query)
async def get_one(self, id: int, user_id: Optional[int] = None, **kwargs):
query = self.get_query(user_id, object_id=id, **kwargs)
return await self._get_one(query)
class BaseMutationResolver:
model: Type[models.BaseModel]
graphql_type: Type[T] = None
def __init__(self, graphql_type: Type[T], model: Type[models.BaseModel]):
self.graphql_type = graphql_type
self.model = model
async def delete(self, user_id: int, id: int) -> T:
async with get_session() as db:
query = BaseQueryResolver(self.graphql_type, self.model).get_query(user_id, object_id=id)
model = (await db.scalars(query)).one()
if hasattr(self.model, "deleted"):
model = await self.model.update(db, obj=model, data=dict(deleted=True))
else:
await db.delete(model)
return self.graphql_type(**model.as_dict())
async def create(self, data: dict, user_id: Optional[int] = None) -> T:
input_data = {**data}
if hasattr(self.model, "created_by_id"):
input_data['created_by_id'] = user_id
async with get_session() as db:
model = await self.model.create(db, data=input_data)
return self.graphql_type(**model.as_dict())
async def _do_update(self, db: AsyncSession, obj: models.BaseModel | dict, data: dict) -> T:
update_where = {}
if isinstance(obj, models.BaseModel):
update_where['obj'] = obj
else:
raise AssertionError(f"Model {model} has no ID column! Cannot query by ID!")
update_where['id'] = obj['id']
if hasattr(model, "deleted"):
query = query.filter(model.deleted.is_(False))
ownership_clause = []
if hasattr(model, "is_public") and include_public:
ownership_clause.append(model.is_public.is_(True))
if hasattr(model, "created_by_id") and user_id:
ownership_clause.append(model.created_by_id == user_id)
if len(ownership_clause) > 1:
query = query.filter(or_(*ownership_clause))
elif len(ownership_clause) == 1:
query = query.filter(*ownership_clause)
if order_by:
query = query.order_by(*order_by)
return query
async def get_list(model, query):
async with get_session() as db:
items = (await db.scalars(query)).all()
return [model(**m.as_dict()) for m in items]
async def get_one(model, query):
async with get_session() as db:
data = (await db.scalars(query)).one()
return model(**data.as_dict())
model = await self.model.update(db, data=data, **update_where)
return self.graphql_type(**model.as_dict())
+60 -123
View File
@@ -1,77 +1,51 @@
import asyncio
from datetime import datetime
from typing import List, Optional, Type
from aiocache import cached
from typing import List, Optional
from sqlalchemy import delete, insert
from sqlalchemy.ext.asyncio import AsyncSession
from strawberry.file_uploads import Upload
from background_jobs.elevation import add_terrain_elevation_to_flight
from background_jobs.weather import download_weather
from database import models
from database.models import flight_has_copilot
from dependencies.db import get_session
from external.weather import weather_api
from database.transaction import get_session
from graphql_schema.entities.helpers.combobox import handle_combobox_save
from graphql_schema.entities.resolvers.base import get_base_resolver
from graphql_schema.entities.resolvers.base import BaseMutationResolver, BaseQueryResolver
from graphql_schema.entities.types.mutation_input import EditFlightInput, TrackItemInput, ComboboxInput
from graphql_schema.entities.types.types import Flight
from upload_utils import delete_file, handle_file_upload
from paths import FLIGHT_GPX_TRACK_PATH
from utils.file import delete_file
from utils.upload import handle_file_upload
class BaseMutationResolver:
model: Type[models.BaseModel]
graphql_type = Flight
class FlightQueryResolver(BaseQueryResolver):
def __init__(self):
super().__init__(graphql_type=Flight, model=models.Flight)
@classmethod
async def delete(cls, user_id: int, id: int):
async with get_session() as db:
model = (
(await db.scalars(
get_base_resolver(cls.model, user_id=user_id, object_id=id)
.filter(cls.model.id == id))
)
.one()
def get_query(self, user_id: int, object_id: Optional[int] = None, *args, **kwargs):
query = super().get_query(
user_id, object_id,
order_by=[models.Flight.takeoff_datetime.desc()],
include_public=bool(user_id)
)
if kwargs.get('username'):
query = (
query
.join(models.Flight.created_by)
.filter(models.User.public_username == kwargs['username'])
)
if hasattr(cls.model, "deleted"):
model = await cls.model.update(db, obj=model, data=dict(deleted=True))
else:
db.delete(model)
return cls.graphql_type(**model.as_dict())
@classmethod
async def _do_update(cls, db: AsyncSession, obj: models.BaseModel | dict, data: dict):
update_where = {}
if isinstance(obj, models.BaseModel):
update_where['obj'] = obj
else:
update_where['id'] = obj['id']
model = await cls.model.update(db, data=data, **update_where)
return cls.graphql_type(**model.as_dict())
async def handle_upload_gpx(original_gpx_filename: str, gpx_track: Upload):
path = "/app/uploads/tracks"
if original_gpx_filename:
delete_file(path + "/" + original_gpx_filename, silent=True)
return await handle_file_upload(gpx_track, path)
return query
class FlightMutationResolver(BaseMutationResolver):
model = models.Flight
graphql_type = Flight
def __init__(self):
super().__init__(Flight, models.Flight)
async def create(self):
pass
@classmethod
async def update(cls, context, id: int, input: EditFlightInput):
async def update(self, context, id: int, input: EditFlightInput):
user_id = context.user_id
async with get_session() as db:
flight = (await db.scalars(get_base_resolver(models.Flight, user_id=user_id, object_id=id))).one()
flight = await FlightQueryResolver().get_one(id, user_id)
flight_data = flight.as_dict()
flight_id = flight.id
@@ -84,31 +58,32 @@ class FlightMutationResolver(BaseMutationResolver):
)
async with get_session() as db:
if input.landing_airport:
landing_airport = await get_airport(db, input.landing_airport, user_id)
landing_datetime = input.landing_datetime or flight_data['landing_datetime']
data['landing_airport_id'] = landing_airport.id
data['landing_datetime'] = landing_datetime
weather_info = await handle_weather_info(
db, landing_datetime, landing_airport, flight_data['landing_weather_info_id']
)
if weather_info:
data['landing_weather_info_id'] = weather_info.id
if input.takeoff_airport:
takeoff_airport = await get_airport(db, input.takeoff_airport, user_id)
takeoff_datetime = input.takeoff_datetime or flight_data['takeoff_datetime']
takeoff_airport_id = await handle_combobox_save(
db, models.Airport, input.takeoff_airport, user_id, name_column="icao_code",
extra_data={"name": input.takeoff_airport.name}
)
data['takeoff_airport_id'] = takeoff_airport_id
data['takeoff_datetime'] = input.takeoff_datetime or flight_data['takeoff_datetime']
data['takeoff_airport_id'] = takeoff_airport.id
data['takeoff_datetime'] = takeoff_datetime
context.background_tasks.add_task(
download_weather, flight_id=id, airport_id=takeoff_airport_id, date_time=data['takeoff_datetime'],
type_="takeoff"
)
weather_info = await handle_weather_info(
db, takeoff_datetime, takeoff_airport, flight_data['takeoff_weather_info_id']
if input.landing_airport:
landing_airport_id = await handle_combobox_save(
db, models.Airport, input.landing_airport, user_id, name_column="icao_code",
extra_data={"name": input.landing_airport.name}
)
data['landing_airport_id'] = landing_airport_id
data['landing_datetime'] = input.landing_datetime or flight_data['landing_datetime']
context.background_tasks.add_task(
download_weather, flight_id=id, airport_id=landing_airport_id, date_time=data['landing_datetime'],
type_="landing"
)
if weather_info:
data['takeoff_weather_info_id'] = weather_info.id
if input.aircraft is not None:
data['aircraft_id'] = await handle_aircraft_save(db, user_id, input.aircraft)
@@ -127,46 +102,21 @@ class FlightMutationResolver(BaseMutationResolver):
if input.copilots is not None:
await db.execute(delete(flight_has_copilot).filter_by(flight_id=flight_id))
copilots = await handle_copilots_edit(db, input.copilots or [], user_id)
copilots = await asyncio.gather(*[
handle_combobox_save(db, models.Copilot, copilot, user_id) for copilot in input.copilots
])
for copilot_id in copilots:
await db.execute(insert(flight_has_copilot).values(flight_id=flight_id, copilot_id=copilot_id))
return await cls._do_update(db, flight_data, data)
return await self._do_update(db, flight_data, data)
async def handle_weather_info(
db: AsyncSession, date_time: datetime, airport: models.Airport, existing_weather_id: Optional[int] = None
) -> Optional[models.WeatherInfo]:
if not airport.gps_latitude or not airport.gps_longitude:
return None
async def handle_upload_gpx(original_gpx_filename: str, gpx_track: Upload):
if original_gpx_filename:
delete_file(FLIGHT_GPX_TRACK_PATH + "/" + original_gpx_filename, silent=True)
try:
weather = await weather_api.get_weather_for_hour(
date_time.astimezone(),
gps=(airport.gps_latitude, airport.gps_longitude)
)
except Exception as e:
print(e)
return None
data = {
"datetime": weather['datetime'],
"qnh": weather['pressure_msl'],
"temperature_surface": weather['temperature_2m'],
"dewpoint_surface": weather['dewpoint_2m'],
"rain": weather['rain'],
"cloudcover_total": weather['cloudcover'],
"cloudcover_low": weather['cloudcover_low'],
"wind_speed_surface": weather['windspeed_10m'],
"wind_direction_surface": weather['winddirection_10m'],
}
if existing_weather_id:
model = await models.WeatherInfo.update(db, id=existing_weather_id, data=data)
else:
model = await models.WeatherInfo.create(db, data=data)
return model
return await handle_file_upload(gpx_track, FLIGHT_GPX_TRACK_PATH)
async def handle_track_edit(db: AsyncSession, flight_id: int, track: List[TrackItemInput], user_id: int):
@@ -176,6 +126,7 @@ async def handle_track_edit(db: AsyncSession, flight_id: int, track: List[TrackI
for item in track:
poi_id = None
airport_id = None
if item.point_of_interest:
poi_id = await handle_combobox_save(
db, models.PointOfInterest, item.point_of_interest, user_id, extra_data={"description": ""}
@@ -195,7 +146,7 @@ async def handle_track_edit(db: AsyncSession, flight_id: int, track: List[TrackI
"point_of_interest_id": poi_id,
"airport_id": airport_id,
"order": order,
"landing_duration": item.landing_duration
"landing_duration": item.landing_duration if airport_id else None
}
)
order += 1
@@ -211,17 +162,3 @@ async def handle_aircraft_save(db: AsyncSession, user_id: int, aircraft: Combobo
"seats": 2,
"manufacturer": "",
})
@cached()
async def get_airport(db: AsyncSession, input: ComboboxInput, user_id: int):
airport_id = await handle_combobox_save(
db, models.Airport, input, user_id, name_column="icao_code", extra_data={"name": input.name}
)
return (await db.scalars(get_base_resolver(models.Airport, object_id=airport_id))).one()
async def handle_copilots_edit(db: AsyncSession, copilots: List[ComboboxInput], user_id: int) -> tuple:
cors = [handle_combobox_save(db, models.Copilot, copilot, user_id) for copilot in copilots]
return await asyncio.gather(*cors)
+21
View File
@@ -0,0 +1,21 @@
from datetime import datetime
from typing import List
class BaseGraphqlInputType:
base_ignored_fields: List[str] = ["created_at", "created_by_id", "updated_by_id", "updated_at", "deleted"]
ignored_fields: List[str] = []
def to_dict(self) -> dict:
dict_data = {}
for key in self.__annotations__.keys():
value = getattr(self, key)
if value is None or key in self.ignored_fields + self.base_ignored_fields:
continue
if isinstance(value, datetime):
value = value.astimezone()
dict_data[key] = value
return dict_data
@@ -1,8 +1,10 @@
from __future__ import annotations
from datetime import datetime
from typing import Optional, List
import strawberry
from strawberry.file_uploads import Upload
from database import models
from graphql_schema.entities.types.base import BaseGraphqlInputType
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_input
@@ -12,11 +14,51 @@ class ComboboxInput:
name: str
@strawberry_sqlalchemy_input(model=models.Copilot, exclude_fields=["id"])
class CreateCopilotInput(BaseGraphqlInputType):
pass
@strawberry_sqlalchemy_input(model=models.Copilot, exclude_fields=["id"])
class EditCopilotInput(BaseGraphqlInputType):
pass
@strawberry_sqlalchemy_input(models.PointOfInterest, exclude_fields=['id', 'type_id'])
class CreatePointOfInterestInput(BaseGraphqlInputType):
type: Optional[ComboboxInput] = None
@strawberry_sqlalchemy_input(models.PointOfInterest, exclude_fields=['id', 'type_id'])
class EditPointOfInterestInput(BaseGraphqlInputType):
type: Optional[ComboboxInput] = None
@strawberry_sqlalchemy_input(model=models.Organization, exclude_fields=["id"])
class CreateOrganizationInput(BaseGraphqlInputType):
pass
@strawberry_sqlalchemy_input(model=models.Organization, exclude_fields=["id"])
class EditOrganizationInput(BaseGraphqlInputType):
pass
@strawberry_sqlalchemy_input(model=models.Event, exclude_fields=["id"])
class CreateEventInput(BaseGraphqlInputType):
pass
@strawberry_sqlalchemy_input(model=models.Event, exclude_fields=["id"])
class EditEventInput(BaseGraphqlInputType):
pass
@strawberry_sqlalchemy_input(models.Flight, exclude_fields=[
"id", "aircraft_id", "deleted", "landing_airport_id", "takeoff_airport_id",
"takeoff_weather_info_id", "landing_weather_info_id", "gpx_track_filename"
"takeoff_weather_info_id", "landing_weather_info_id", "gpx_track_filename", "event_id"
], all_optional=True)
class EditFlightInput:
class EditFlightInput(BaseGraphqlInputType):
gpx_track: Optional[Upload] = None # TODO: poresit validaci uploadovaneho souboru!
track: Optional[List[TrackItemInput]] = None
copilots: Optional[List[ComboboxInput]] = None
@@ -26,19 +68,29 @@ class EditFlightInput:
event: Optional[ComboboxInput] = None
@strawberry_sqlalchemy_input(models.Flight, exclude_fields=[
"id", "aircraft_id", "landing_airport_id", "takeoff_airport_id", "weather_info_takeoff_id",
"weather_info_landing_id", "with_instructor", "has_terrain_elevation"
])
class CreateFlightInput:
@strawberry.input()
class CreateFlightInput(BaseGraphqlInputType):
aircraft: ComboboxInput
landing_airport: ComboboxInput
takeoff_airport: ComboboxInput
takeoff_datetime: datetime
landing_datetime: datetime
@strawberry.input()
class TrackItemInput:
# order: int
point_of_interest: Optional[ComboboxInput] = None
airport: Optional[ComboboxInput] = None
landing_duration: Optional[int] = None
@strawberry_sqlalchemy_input(models.Aircraft, exclude_fields=['id', 'photo_filename'])
class CreateAircraftInput(BaseGraphqlInputType):
photo: Optional[Upload]
organization: Optional[ComboboxInput] = None
@strawberry_sqlalchemy_input(models.Aircraft, exclude_fields=['photo_filename'])
class EditAircraftInput(BaseGraphqlInputType):
photo: Optional[Upload]
organization: Optional[ComboboxInput] = None
+50 -79
View File
@@ -2,7 +2,6 @@ from __future__ import annotations
from datetime import datetime
from typing import Optional, Annotated, List
import strawberry
from config import API_URL
from database import models
from decorators.endpoints import authenticated_user_only
from external.gpx_parser import GPXParser
@@ -17,30 +16,10 @@ from graphql_schema.dataloaders.single_model import (
poi_dataloader, poi_type_dataloader, event_dataloader, aircraft_dataloader, airport_dataloader, cover_photo_loader,
airport_weather_info_loader, organizations_dataloader
)
from graphql_schema.entities.airport import Airport
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type
from paths import get_photo_basepath
from upload_utils import file_exists, get_public_url
@strawberry_sqlalchemy_type(models.FlightTrack)
class FlightTrack:
point_of_interest: Optional[PointOfInterest] = strawberry.field(
resolver=lambda root: poi_dataloader.load(root.point_of_interest_id)
)
airport: Optional[Airport] = strawberry.field(
resolver=lambda root: airport_dataloader.load(root.airport_id)
)
@strawberry_sqlalchemy_type(models.PointOfInterestType)
class PointOfInterestType:
pass
@strawberry_sqlalchemy_type(models.WeatherInfo)
class WeatherInfo:
pass
from paths import (
get_public_url, get_avatar_url, get_title_image_url, get_photo_thumbnail_url, get_photo_url, FLIGHT_GPX_TRACK_PATH
)
@strawberry.type
@@ -49,35 +28,6 @@ class Point:
lng: float
@strawberry_sqlalchemy_type(models.PointOfInterest)
class PointOfInterest:
type: Optional[PointOfInterestType] = strawberry.field(
resolver=lambda root: poi_type_dataloader.load(root.type_id)
)
photos: List[Annotated["Photo", strawberry.lazy('.photo')]] = strawberry.field(
resolver=lambda root: poi_photos_dataloader.load(root.id)
)
flights: List[Annotated["Flight", strawberry.lazy('.flight')]] = strawberry.field(
resolver=lambda root: flight_by_poi_dataloader.load(root.id)
)
@strawberry_sqlalchemy_type(models.Photo)
class Photo:
def resolve_thumb_url(root):
thumbnail = get_photo_basepath(root.flight_id) + "/thumbs/" + root.filename
if not file_exists(thumbnail):
return get_public_url(f"photos/{root.flight_id}/{root.filename}")
return get_public_url(f"photos/{root.flight_id}/thumbs/{root.filename}")
url: str = strawberry.field(resolver=lambda root: get_public_url(f"photos/{root.flight_id}/{root.filename}"))
thumbnail_url: str = strawberry.field(resolver=resolve_thumb_url)
point_of_interest: Optional[Annotated["PointOfInterest", strawberry.lazy('.poi')]] = strawberry.field(
resolver=lambda root: poi_dataloader.load(root.point_of_interest_id)
)
@strawberry.type
class GPXTrack:
coordinates: List[Point]
@@ -92,6 +42,45 @@ class GPXTrack:
avg_altitude: float
@strawberry_sqlalchemy_type(models.Airport)
class Airport:
pass
@strawberry_sqlalchemy_type(models.FlightTrack)
class FlightTrack:
point_of_interest: Optional[PointOfInterest] = strawberry.field(
resolver=lambda root: poi_dataloader.load(root.point_of_interest_id)
)
airport: Optional[Airport] = strawberry.field(resolver=lambda root: airport_dataloader.load(root.airport_id))
@strawberry_sqlalchemy_type(models.PointOfInterestType)
class PointOfInterestType:
pass
@strawberry_sqlalchemy_type(models.WeatherInfo)
class WeatherInfo:
pass
@strawberry_sqlalchemy_type(models.PointOfInterest)
class PointOfInterest:
type: Optional[PointOfInterestType] = strawberry.field(resolver=lambda root: poi_type_dataloader.load(root.type_id))
photos: List[Photo] = strawberry.field(resolver=lambda root: poi_photos_dataloader.load(root.id))
flights: List[Flight] = strawberry.field(resolver=lambda root: flight_by_poi_dataloader.load(root.id))
@strawberry_sqlalchemy_type(models.Photo)
class Photo:
url: str = strawberry.field(resolver=get_photo_url)
thumbnail_url: str = strawberry.field(resolver=get_photo_thumbnail_url)
point_of_interest: Optional[PointOfInterest] = strawberry.field(
resolver=lambda root: poi_dataloader.load(root.point_of_interest_id)
)
@strawberry_sqlalchemy_type(models.Flight)
class Flight:
async def load_gpx_track(root):
@@ -99,7 +88,7 @@ class Flight:
return None
try:
gpx_parser = GPXParser(f"/app/uploads/tracks/{root.gpx_track_filename}")
gpx_parser = GPXParser(f"{FLIGHT_GPX_TRACK_PATH}/{root.gpx_track_filename}")
except OSError:
return None
@@ -146,9 +135,7 @@ class Flight:
@strawberry_sqlalchemy_type(models.Copilot)
class Copilot:
flights: List[Annotated["Flight", strawberry.lazy('.flight')]] = strawberry.field(
resolver=lambda root: flights_by_copilot_dataloader.load(root.id)
)
flights: List[Flight] = strawberry.field(resolver=lambda root: flights_by_copilot_dataloader.load(root.id))
@strawberry_sqlalchemy_type(models.Aircraft)
@@ -156,40 +143,24 @@ class Aircraft:
photo_url: Optional[str] = strawberry.field(
resolver=lambda root: get_public_url(f"aircrafts/{root.photo_filename}") if root.photo_filename else None
)
flights: List[Annotated["Flight", strawberry.lazy('.flight')]] = strawberry.field(
resolver=lambda root: flights_by_aircraft_dataloader.load(root.id)
)
organization: Optional[Annotated["Organization", strawberry.lazy(".organization")]] = strawberry.field(
flights: List[Flight] = strawberry.field(resolver=lambda root: flights_by_aircraft_dataloader.load(root.id))
organization: Optional[Organization] = strawberry.field(
resolver=lambda root: organizations_dataloader.load(root.organization_id)
)
@strawberry_sqlalchemy_type(models.Organization)
class Organization:
users: List[Annotated["User", strawberry.lazy(".user")]] = strawberry.field(
resolver=lambda root: users_in_organization_dataloader.load(root.id)
)
aircrafts: List[Annotated["Aircraft", strawberry.lazy(".aircraft")]] = strawberry.field(
users: List[User] = strawberry.field(resolver=lambda root: users_in_organization_dataloader.load(root.id))
aircrafts: List[Aircraft] = strawberry.field(
resolver=lambda root: aircrafts_from_organization_dataloader.load(root.id)
)
@strawberry_sqlalchemy_type(models.User, exclude_fields=['password_hashed'])
class User:
async def load_avatar_image_url(root):
if not root.avatar_image_filename:
return None
return get_public_url(f"profile/{root.id}/{root.avatar_image_filename}")
async def load_title_image_url(root):
if not root.title_image_filename:
return f"{API_URL}/static/default-title-image.jpg"
return get_public_url(f"profile/{root.id}/{root.title_image_filename}")
avatar_image_url: Optional[str] = strawberry.field(resolver=load_avatar_image_url)
title_image_url: str = strawberry.field(resolver=load_title_image_url)
avatar_image_url: Optional[str] = strawberry.field(resolver=lambda root: get_avatar_url(root))
title_image_url: str = strawberry.field(resolver=lambda root: get_title_image_url(root))
organizations: List[Annotated['Organization', strawberry.lazy(".organization")]] = strawberry.field(
resolver=lambda root: user_organizations_dataloader.load(root.id)
)
+5 -6
View File
@@ -1,4 +1,4 @@
from typing import Optional, TYPE_CHECKING
from typing import Optional
import strawberry
from graphql import GraphQLError
from passlib.hash import bcrypt
@@ -7,12 +7,11 @@ from strawberry.file_uploads import Upload
from database import models
from decorators.endpoints import authenticated_user_only
from decorators.error_logging import error_logging
from dependencies.db import get_session
from upload_utils import handle_file_upload, delete_file, resize_image
from database.transaction import get_session
from graphql_schema.entities.types.types import User
if TYPE_CHECKING:
pass
from utils.file import delete_file
from utils.image import resize_image
from utils.upload import handle_file_upload
@strawberry.type
+8 -15
View File
@@ -1,31 +1,24 @@
from strawberry.tools import merge_types
from graphql_schema.entities.aircraft import CreateAircraftMutation, EditAircraftMutation, DeleteAircraftMutation
from graphql_schema.entities.aircraft import AircraftMutation
from graphql_schema.entities.copilot import CreateCopilotMutation, EditCopilotMutation
from graphql_schema.entities.event import CreateEventMutation, EditEventMutation
from graphql_schema.entities.event import EventMutation
from graphql_schema.entities.flight import FlightMutation
from graphql_schema.entities.organization import (
CreateOrganizationMutation, EditOrganizationMutation, OrganizationUserMutation
)
from graphql_schema.entities.organization import OrganizationUserMutation, OrganizationMutation
from graphql_schema.entities.photo import UploadPhotoMutation, DeletePhotoMutation, EditPhotoMutation
from graphql_schema.entities.poi import CreatePointOfInterestMutation, EditPointOfInterestMutation
from graphql_schema.entities.poi import PointOfInterestMutation
from graphql_schema.entities.user import EditUserMutation
Mutation = merge_types("Mutation", (
CreateAircraftMutation,
EditAircraftMutation,
DeleteAircraftMutation,
AircraftMutation,
FlightMutation,
UploadPhotoMutation,
EditPhotoMutation,
DeletePhotoMutation,
CreatePointOfInterestMutation,
EditPointOfInterestMutation,
PointOfInterestMutation,
CreateCopilotMutation,
EditCopilotMutation,
EditUserMutation,
CreateEventMutation,
EditEventMutation,
CreateOrganizationMutation,
EditOrganizationMutation,
EventMutation,
OrganizationMutation,
OrganizationUserMutation,
))
@@ -1,10 +1,10 @@
import typing
from datetime import datetime
from typing import List, Optional
import strawberry
import sqlalchemy
from sqlalchemy import Column
from database.models import BaseModel
from graphql_schema.entities.types.base import BaseGraphqlInputType
def get_columns_from_model(model: BaseModel, exclude_fields: List[str]) -> List[typing.Tuple[str, Column]]:
@@ -49,20 +49,7 @@ def strawberry_sqlalchemy_input(
if exclude_fields is None:
exclude_fields = []
ignored_fields = exclude_fields + ["created_at", "created_by_id", "updated_by_id", "updated_at", "deleted"]
def to_dict(self):
dict_data = {}
for key, _ in get_columns_from_model(model, ignored_fields):
value = getattr(self, key)
if value is None:
continue
if isinstance(value, datetime):
value = value.astimezone()
dict_data[key] = value
return dict_data
ignored_fields = exclude_fields + BaseGraphqlInputType.base_ignored_fields
def wrapper(cls):
annotations = get_annotations_for_scalars(
@@ -72,7 +59,6 @@ def strawberry_sqlalchemy_input(
)
cls.__annotations__.update(annotations)
cls.to_dict = to_dict
for col, col_type in annotations.items():
try:
+22
View File
@@ -0,0 +1,22 @@
import logging
import sys
log = logging.getLogger(__name__)
log.setLevel(logging.DEBUG)
# Create handlers for logging to the standard output and a file
stdoutHandler = logging.StreamHandler(stream=sys.stdout)
# Set the log levels on the handlers
stdoutHandler.setLevel(logging.DEBUG)
# Create a log format using Log Record attributes
fmt = logging.Formatter(
"%(name)s: %(asctime)s | %(levelname)s | %(filename)s:%(lineno)s | %(process)d >>> %(message)s"
)
# Set the log format on each handler
stdoutHandler.setFormatter(fmt)
# Add each handler to the Logger object
log.addHandler(stdoutHandler)
+34
View File
@@ -1,7 +1,41 @@
import os
from typing import Optional
from config import API_URL
PHOTO_BASE_PATH = ""
AIRCRAFT_BASE_PATH = ""
FLIGHT_BASE_PATH = ""
FLIGHT_GPX_TRACK_PATH = "/app/uploads/tracks"
AIRCRAFT_UPLOAD_DEST_PATH = "/app/uploads/aircrafts/"
def get_photo_basepath(flight_id: int) -> str:
return f"/app/uploads/photos/{flight_id}"
def get_public_url(filename: Optional[str]) -> str:
return f"{API_URL}/uploads/{filename}" if filename else None
def get_photo_url(root) -> str:
return get_public_url(f"photos/{root.flight_id}/{root.filename}")
def get_photo_thumbnail_url(root) -> str:
thumbnail = get_photo_basepath(root.flight_id) + "/thumbs/" + root.filename
if not os.path.isfile(thumbnail):
# TODO: logovani
return get_public_url(f"photos/{root.flight_id}/{root.filename}")
return get_public_url(f"photos/{root.flight_id}/thumbs/{root.filename}")
def get_avatar_url(user) -> str:
return get_public_url(f"profile/{user.id}/{user.avatar_image_filename}") if user.avatar_image_filename else None
def get_title_image_url(user):
if not user.title_image_filename:
return f"{API_URL}/static/default-title-image.jpg"
return get_public_url(f"profile/{user.id}/{user.title_image_filename}")
+4 -2
View File
@@ -2,6 +2,8 @@ import asyncio
import sys
from sqlalchemy import select
from paths import FLIGHT_GPX_TRACK_PATH
sys.path.insert(0, "/app/src")
from database import async_session, models # noqa
@@ -45,7 +47,7 @@ async def add_elevation_to_tracks():
return
for flight in flights:
gpx_file = f"/app/uploads/tracks/{flight.gpx_track_filename}"
gpx_file = f"{FLIGHT_GPX_TRACK_PATH}/{flight.gpx_track_filename}"
gpx = GPXParser(gpx_file)
coordinates = await gpx.get_coordinates()
@@ -53,7 +55,7 @@ async def add_elevation_to_tracks():
gpx_with_elevation = gpx.add_terrain_elevation(elevation)
output_name = f"terrain_{flight.gpx_track_filename[30:]}"
gpx.write(gpx_with_elevation, output=f"/app/uploads/tracks/{output_name}")
gpx.write(gpx_with_elevation, output=f"{FLIGHT_GPX_TRACK_PATH}/{output_name}")
await models.Flight.update(
db_session=session, obj=flight, data={
"has_terrain_elevation": True,
+14
View File
@@ -0,0 +1,14 @@
import os
def delete_file(path: str, silent: bool = False):
try:
os.remove(path)
except Exception:
if not silent:
raise
def check_directories(path: str):
if not os.path.isdir(path):
os.makedirs(path)
+6
View File
@@ -0,0 +1,6 @@
from typing import Tuple
def gps_to_decimal(input: Tuple[float, float, float]) -> float:
d, m, s = input
return d + (m / 60.0) + (s / 3600.0)
+3 -54
View File
@@ -1,42 +1,9 @@
import os
import uuid
from datetime import datetime
from typing import Optional, Tuple
from typing import Optional
import exif
from PIL import Image, UnidentifiedImageError
from strawberry.file_uploads import Upload
from config import API_URL
def get_public_url(filename: Optional[str]):
return f"{API_URL}/uploads/{filename}" if filename else None
def check_directories(path: str):
if not os.path.isdir(path):
os.makedirs(path)
def file_exists(path: str):
return os.path.isfile(path)
async def handle_file_upload(file: Upload, path: str):
check_directories(path)
filename = f"{uuid.uuid4()}-{file.filename}"
content = await file.read()
image = open(path + "/" + filename, "wb")
image.write(content)
image.close()
return filename
def gps_to_decimal(input: Tuple[float, float, float]) -> float:
d, m, s = input
return d + (m / 60.0) + (s / 3600.0)
from utils.file import check_directories
from utils.gps import gps_to_decimal
async def parse_exif_info(path: str, filename: str) -> dict:
@@ -101,21 +68,3 @@ async def rotate_image(
img = img.rotate(angle, Image.LANCZOS, expand=True)
check_directories(dest_path)
img.save(f"{dest_path}/{dest_filename}", 'JPEG', quality=100)
async def generate_thumbnail(path: str, filename: str, quality: int = 90):
return await resize_image(
path, filename,
new_width=300,
dest_path=f"{path}/thumbs/",
dest_filename=filename,
quality=quality
)
def delete_file(path: str, silent: bool = False):
try:
os.remove(path)
except Exception:
if not silent:
raise
+17
View File
@@ -0,0 +1,17 @@
import uuid
from strawberry.file_uploads import Upload
from utils.file import check_directories
async def handle_file_upload(file: Upload, path: str) -> str:
check_directories(path)
filename = f"{uuid.uuid4()}-{file.filename}"
content = await file.read()
image = open(path + "/" + filename, "wb")
image.write(content)
image.close()
return filename