From 64cb544e2d325b09c843859e7386342c00a7770a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kv=C3=A1=C4=8Dek?= Date: Tue, 12 Sep 2023 11:00:36 +0200 Subject: [PATCH] Pridani skriptu pro pridani elevace, uprava prace s transakcemi --- ...n_elevation_to_photos_and__6e5cc5123a2b.py | 46 +++++ src/background_jobs/photo.py | 19 ++ src/database/models.py | 20 ++- src/decorators/db.py | 35 ++++ src/dependencies/db.py | 21 +++ src/external/elevation.py | 5 +- src/graphql_schema/dataloaders/photos.py | 1 + src/graphql_schema/entities/aircraft.py | 29 ++- src/graphql_schema/entities/airport.py | 21 ++- src/graphql_schema/entities/copilot.py | 49 ++++-- src/graphql_schema/entities/flight.py | 166 ++++++++++-------- src/graphql_schema/entities/helpers/flight.py | 6 +- src/graphql_schema/entities/photo.py | 96 +++++----- src/graphql_schema/entities/poi.py | 67 +++---- src/graphql_schema/entities/poi_type.py | 9 +- src/graphql_schema/entities/user.py | 70 ++++---- src/graphql_schema/query.py | 2 + src/graphql_schema/schema.py | 1 - .../sqlalchemy_to_strawberry_type.py | 4 +- src/main.py | 6 +- src/paths.py | 0 src/scripts/elevation.py | 72 ++++++++ 22 files changed, 519 insertions(+), 226 deletions(-) create mode 100644 alembic/versions/20230905-095445_add_terrain_elevation_to_photos_and__6e5cc5123a2b.py create mode 100644 src/background_jobs/photo.py create mode 100644 src/decorators/db.py create mode 100644 src/paths.py create mode 100644 src/scripts/elevation.py diff --git a/alembic/versions/20230905-095445_add_terrain_elevation_to_photos_and__6e5cc5123a2b.py b/alembic/versions/20230905-095445_add_terrain_elevation_to_photos_and__6e5cc5123a2b.py new file mode 100644 index 0000000..311b83d --- /dev/null +++ b/alembic/versions/20230905-095445_add_terrain_elevation_to_photos_and__6e5cc5123a2b.py @@ -0,0 +1,46 @@ +"""add terrain elevation to photos and event table + +Revision ID: 6e5cc5123a2b +Revises: 76752a74a304 +Create Date: 2023-09-05 09:54:45.509461 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '6e5cc5123a2b' +down_revision = '76752a74a304' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('event', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=128), nullable=False), + sa.Column('description', sa.Text(), nullable=False), + sa.Column('is_public', sa.Boolean(), server_default='0', nullable=False), + sa.Column('created_by_id', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.Column('deleted', sa.Boolean(), server_default='0', nullable=False), + sa.ForeignKeyConstraint(['created_by_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.add_column('flight', sa.Column('event_id', sa.Integer(), nullable=True)) + op.add_column('flight', sa.Column('has_terrain_elevation', sa.Boolean(), nullable=False)) + op.create_foreign_key(None, 'flight', 'event', ['event_id'], ['id']) + op.add_column('photo', sa.Column('terrain_elevation', sa.Float(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('photo', 'terrain_elevation') + op.drop_constraint(None, 'flight', type_='foreignkey') + op.drop_column('flight', 'has_terrain_elevation') + op.drop_column('flight', 'event_id') + op.drop_table('event') + # ### end Alembic commands ### diff --git a/src/background_jobs/photo.py b/src/background_jobs/photo.py new file mode 100644 index 0000000..5fd1f36 --- /dev/null +++ b/src/background_jobs/photo.py @@ -0,0 +1,19 @@ +from database import models +from dependencies.db import get_session +from external.elevation import elevation_api + + +async def add_terrain_elevation(photo): + async with get_session() as db: + try: + elevation = await elevation_api.get_elevation_for_points([{"lat": photo.gps_latitude, "lng": photo.gps_longitude}]) + if not elevation: + print("Cannot get elevation") + return + + terrain_elevation = elevation[0]['elevation'] + await models.Photo.update(db_session=db, obj=photo, data={"terrain_elevation": terrain_elevation}) + except Exception as e: + print(f"Cannot get elevation: {e}") + + diff --git a/src/database/models.py b/src/database/models.py index afc4d26..27de7bd 100644 --- a/src/database/models.py +++ b/src/database/models.py @@ -8,7 +8,7 @@ from sqlalchemy.ext.asyncio import AsyncSession @as_declarative() class BaseModel: - excluded_columns_in_dict = tuple() + excluded_columns_in_dict = ("deleted",) def as_dict(self): return { @@ -122,6 +122,7 @@ class Photo(BaseModel): gps_latitude: Mapped[float] = mapped_column(Float, nullable=True) gps_longitude: Mapped[float] = mapped_column(Float, nullable=True) gps_altitude: Mapped[float] = mapped_column(Float, nullable=True) + terrain_elevation: Mapped[float] = mapped_column(Float, nullable=True) point_of_interest_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest.id"), nullable=True) flight_id: Mapped[int] = mapped_column(Integer, ForeignKey("flight.id"), nullable=False) created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id')) @@ -208,12 +209,27 @@ class WeatherInfo(BaseModel): datetime: Mapped[datetime] = mapped_column(DateTime, nullable=False) +class Event(BaseModel): + __tablename__ = "event" + + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(String(128), nullable=False) + description: Mapped[str] = mapped_column(Text, nullable=False) + is_public: Mapped[bool] = mapped_column(Boolean, server_default='0') + created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id')) + created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) + deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0') + + created_by: Mapped['User'] = relationship() + + class Flight(BaseModel): __tablename__ = "flight" id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column(String(128), nullable=False) description: Mapped[str] = mapped_column(Text, nullable=False) + event_id: Mapped[int] = mapped_column(Integer, ForeignKey("event.id"), nullable=True) takeoff_airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=False) landing_airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=False) takeoff_datetime: Mapped[datetime] = mapped_column(DateTime, nullable=False) @@ -221,6 +237,7 @@ class Flight(BaseModel): duration_total: Mapped[int] = mapped_column(Integer, nullable=True) duration_pic: Mapped[int] = mapped_column(Integer, nullable=True) gpx_track_filename: Mapped[str] = mapped_column(String(128), nullable=True) + has_terrain_elevation: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) aircraft_id: Mapped[int] = mapped_column(Integer, ForeignKey('aircraft.id')) takeoff_weather_info_id: Mapped[int] = mapped_column(Integer, ForeignKey('weather_info.id'), nullable=True) landing_weather_info_id: Mapped[int] = mapped_column(Integer, ForeignKey('weather_info.id'), nullable=True) @@ -235,6 +252,7 @@ class Flight(BaseModel): weather_info_landing: Mapped[WeatherInfo] = relationship(foreign_keys=[landing_weather_info_id]) weather_info_takeoff: Mapped[WeatherInfo] = relationship(foreign_keys=[takeoff_weather_info_id]) track: Mapped['FlightTrack'] = relationship() + event: Mapped['Event'] = relationship() copilots: Mapped[List['Copilot']] = relationship(secondary=flight_has_copilot) aircraft: Mapped['Aircraft'] = relationship(back_populates="flights") photos: Mapped[List['Photo']] = relationship(foreign_keys=[Photo.flight_id]) diff --git a/src/decorators/db.py b/src/decorators/db.py new file mode 100644 index 0000000..e16a966 --- /dev/null +++ b/src/decorators/db.py @@ -0,0 +1,35 @@ +import asyncio +import functools +from typing import Callable + +from database import async_session + + +def transactional(func: Callable) -> Callable: + @functools.wraps(func) + async def _wrapper(*args, **kwargs): + # db_session = db_session_context.get() + # if db_session: + # return func(*args, **kwargs) + + db_session = async_session() + + print("STARTUJI TRANSAKCI") + db_session.begin() + # db_session_context.set(db_session) + try: + kwargs['db'] = db_session + result = await func(*args, **kwargs) + await db_session.commit() + print("KONEC TRANSAKCE V DEKORATORU") + + except Exception as e: + await db_session.rollback() + raise + + finally: + await db_session.close() + # db_session_context.set(None) + return result + + return _wrapper \ No newline at end of file diff --git a/src/dependencies/db.py b/src/dependencies/db.py index 80edb7e..2f7e79c 100644 --- a/src/dependencies/db.py +++ b/src/dependencies/db.py @@ -1,9 +1,30 @@ +from contextlib import asynccontextmanager +from typing import AsyncGenerator + +from sqlalchemy.ext.asyncio import AsyncSession + from database import async_session +@asynccontextmanager +async def get_session(): + async with async_session() as session: + async with session.begin(): + try: + yield session + await session.commit() + except: + await session.rollback() + raise + finally: + session.expunge_all() + await session.close() + + async def db_session(): async with async_session() as session: async with session.begin(): yield session await session.flush() await session.commit() + print("CCCCCCCCCCCCCOOOOOOOOOOOOOOMMMMMMMMMMIIIIIIIIIIIITTTTTTTTTTTT") diff --git a/src/external/elevation.py b/src/external/elevation.py index 195737e..5b4003c 100644 --- a/src/external/elevation.py +++ b/src/external/elevation.py @@ -22,4 +22,7 @@ class ElevationAPI: "lat": loc['latitude'], "lng": loc['longitude'], "elevation": loc['elevation'] - } for loc in response['results']] \ No newline at end of file + } for loc in response['results']] + + +elevation_api = ElevationAPI() diff --git a/src/graphql_schema/dataloaders/photos.py b/src/graphql_schema/dataloaders/photos.py index 43c9cb0..c4a125b 100644 --- a/src/graphql_schema/dataloaders/photos.py +++ b/src/graphql_schema/dataloaders/photos.py @@ -17,6 +17,7 @@ class PhotoDataloader: models = (await session.scalars( select(Photo) .filter(getattr(Photo, self.relationship_column).in_(ids)) + .order_by(Photo.exposed_at) )).all() photos_by_relationship_id = defaultdict(list) for photo in models: diff --git a/src/graphql_schema/entities/aircraft.py b/src/graphql_schema/entities/aircraft.py index 7824ea5..6bb0c5b 100644 --- a/src/graphql_schema/entities/aircraft.py +++ b/src/graphql_schema/entities/aircraft.py @@ -4,6 +4,7 @@ from strawberry.file_uploads import Upload from sqlalchemy import select 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_type, strawberry_sqlalchemy_input from upload_utils import handle_file_upload, delete_file, get_public_url from ..dataloaders.flight import flights_by_aircraft_dataloader @@ -44,8 +45,10 @@ class AircraftQueries: get_base_query(info.context.user_id) .order_by(models.Aircraft.id.desc()) ) + async with get_session() as db: + aircrafts = (await db.scalars(query)).all() - return (await info.context.db.scalars(query)).all() + return [Aircraft(**a.as_dict()) for a in aircrafts] @strawberry.field() @authenticated_user_only() @@ -54,7 +57,9 @@ class AircraftQueries: get_base_query(info.context.user_id) .filter(models.Aircraft.id == id) ) - return (await info.context.db.scalars(query)).one() + async with get_session() as db: + aircraft = (await db.scalars(query)).one() + return Aircraft(**aircraft.as_dict()) @strawberry.type @@ -73,7 +78,7 @@ class CreateAircraftMutation: input_data['photo_filename'] = await handle_file_upload(input.photo, AIRCRAFT_UPLOAD_DEST_PATH) return await models.Aircraft.create( - info.context.db, + db, data=dict( **input_data, created_by_id=info.context.user_id, @@ -91,17 +96,20 @@ class EditAircraftMutation: @authenticated_user_only() async def edit_aircraft(root, info, id: int, input: EditAircraftInput) -> Aircraft: # TODO: kontrola organizace - # TODO: kontrola opravneni na akci update_data = input.to_dict() - aircraft = await models.Aircraft.get_one(info.context.db, id) + async with get_session() as db: + aircraft = (await db.scalars( + get_base_query(info.context.user_id) + .filter(models.Aircraft.id == id) + )).one() 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) - return await models.Aircraft.update(info.context.db, obj=aircraft, data=update_data) + return await models.Aircraft.update(db, obj=aircraft, data=update_data) @strawberry.type @@ -110,6 +118,11 @@ class DeleteAircraftMutation: @strawberry.mutation @authenticated_user_only() async def delete_aircraft(self, info, id: int) -> Aircraft: - # TODO: kontrola opravneni na akci + async with get_session() as db: + aircraft = (await db.scalars( + get_base_query(info.context.user_id) + .filter(models.Aircraft.id == id) + )).one() - return await models.Aircraft.update(info.context.db, id=id, data=dict(deleted=True)) + aircraft = await models.Aircraft.update(db, obj=aircraft, data=dict(deleted=True)) + return Aircraft(**aircraft.as_dict()) diff --git a/src/graphql_schema/entities/airport.py b/src/graphql_schema/entities/airport.py index e06929a..e2fec8b 100644 --- a/src/graphql_schema/entities/airport.py +++ b/src/graphql_schema/entities/airport.py @@ -3,6 +3,7 @@ import strawberry from sqlalchemy import select, or_ 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_type @@ -19,27 +20,29 @@ def get_base_query(user_id: int): models.Airport.created_by_id == user_id, models.Airport.created_by_id.is_(None), )) + .order_by(models.Airport.icao_code) ) @strawberry.type class AirportQueries: - - @strawberry.field + @strawberry.field() @authenticated_user_only() async def airports(root, info) -> List[Airport]: - query = ( - get_base_query(info.context.user_id) - .order_by(models.Airport.id.desc()) - ) + query = get_base_query(info.context.user_id) - return (await info.context.db.scalars(query)).all() + async with get_session() as db: + airports = (await db.scalars(query)).all() + return [Airport(**a.as_dict()) for a in airports] - @strawberry.field + @strawberry.field() @authenticated_user_only() async def airport(root, info, id: int) -> Airport: query = ( get_base_query(info.context.user_id) .filter(models.Airport.id == id) ) - return (await info.context.db.scalars(query)).one() + + async with get_session() as db: + airport = (await db.scalars(query)).one() + return Airport(**airport.as_dict()) diff --git a/src/graphql_schema/entities/copilot.py b/src/graphql_schema/entities/copilot.py index 24275ed..f8cd8e6 100644 --- a/src/graphql_schema/entities/copilot.py +++ b/src/graphql_schema/entities/copilot.py @@ -3,6 +3,7 @@ import strawberry from sqlalchemy import select from database import models from decorators.endpoints import authenticated_user_only +from dependencies.db import get_session from graphql_schema.dataloaders.flight import flights_by_copilot_dataloader from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type, strawberry_sqlalchemy_input @@ -23,7 +24,7 @@ def get_base_query(user_id: int): select(models.Copilot) .filter(models.Copilot.created_by_id == user_id) .filter(models.Copilot.deleted.is_(False)) - .order_by(models.Copilot.id.desc()) + .order_by(models.Copilot.name) ) @@ -32,17 +33,22 @@ class CopilotQueries: @strawberry.field() @authenticated_user_only() async def copilots(root, info) -> List[Copilot]: - return (await info.context.db.scalars( - get_base_query(info.context.user_id) - )).all() + async with get_session() as db: + copilots = (await db.scalars( + get_base_query(info.context.user_id) + )).all() + + return [Copilot(**c.as_dict()) for c in copilots] @strawberry.field() @authenticated_user_only() async def copilot(root, info, id: int) -> Copilot: - return (await info.context.db.scalars( - get_base_query(info.context.user_id) - .filter(models.Copilot.id == id) - )).one() + async with get_session() as db: + copilot = (await db.scalars( + get_base_query(info.context.user_id) + .filter(models.Copilot.id == id) + )).one() + return Copilot(**copilot.as_dict()) @strawberry.type @@ -56,13 +62,16 @@ class CreateCopilotMutation: @authenticated_user_only() async def create_copilot(root, info, input: CreateCopilotInput) -> Copilot: input_data = input.to_dict() - return await models.Copilot.create( - info.context.db, - data=dict( - **input_data, - created_by_id=info.context.user_id, + 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()) @strawberry.type @@ -75,8 +84,12 @@ class EditCopilotMutation: @strawberry.mutation @authenticated_user_only() async def edit_copilot(root, info, id: int, input: EditCopilotInput) -> Copilot: - copilot = (await info.context.db.scalars( - get_base_query(info.context.user_id).filter(models.Copilot.id == id) - )).one() - return await models.Copilot.update(info.context.db, obj=copilot, data=input.to_dict()) \ No newline at end of file + async with get_session() as db: + copilot = (await db.scalars( + get_base_query(info.context.user_id).filter(models.Copilot.id == id) + )).one() + + + updated_copilot = await models.Copilot.update(db, obj=copilot, data=input.to_dict()) + return Copilot(**updated_copilot.as_dict()) \ No newline at end of file diff --git a/src/graphql_schema/entities/flight.py b/src/graphql_schema/entities/flight.py index 1325f5d..b538d61 100644 --- a/src/graphql_schema/entities/flight.py +++ b/src/graphql_schema/entities/flight.py @@ -11,6 +11,7 @@ from database import models from database.models import flight_has_copilot from decorators.endpoints import authenticated_user_only from decorators.error_logging import error_logging +from dependencies.db import get_session from external.gpx_parser import GPXParser from graphql_schema.dataloaders.aircraft import aircraft_dataloader from graphql_schema.dataloaders.airport import airport_dataloader @@ -157,7 +158,11 @@ def get_base_query(user_id: Optional[int], username: Optional[str] = None, is_au 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) + 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)) @@ -171,13 +176,16 @@ class FlightQueries: @strawberry.field() async def flights(root, info, username: Optional[str] = None) -> List[Flight]: if not info.context.user_id and not username: - raise HTTPException(HTTP_401_UNAUTHORIZED, f"user_id={info.context.user_id}, {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 info.context.db.scalars(query)).all() + + async with get_session() as db: + flights = (await db.scalars(query)).all() + return [Flight(**f.as_dict()) for f in flights] @strawberry.field() @error_logging @@ -189,14 +197,17 @@ class FlightQueries: 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 info.context.db.scalars(query)).one() + + async with get_session() as db: + flight = (await db.scalars(query)).one() + return Flight(**flight.as_dict()) @strawberry.type class CreateFlightMutation: @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" + "weather_info_landing_id", "with_instructor", "has_terrain_elevation" ]) class CreateFlightInput: aircraft: ComboboxInput @@ -206,28 +217,29 @@ class CreateFlightMutation: @strawberry.mutation @authenticated_user_only() async def create_flight(self, info, input: CreateFlightInput) -> Flight: - db = info.context.db data = input.to_dict() - takeoff_airport, landing_airport = await get_airports(db, input.takeoff_airport.id, input.landing_airport.id) + async with get_session() as db: + takeoff_airport, landing_airport = await get_airports(db, input.takeoff_airport.id, input.landing_airport.id) - 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() + 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, - "landing_weather_info_id": weather_landing.id, - "takeoff_airport_id": takeoff_airport.id, - "landing_airport_id": landing_airport.id, - "aircraft_id": aircraft_id, - "created_by_id": info.context.user_id - }) - return flight + flight = await models.Flight.create(db, data={ + **data, + "takeoff_weather_info_id": weather_takeoff.id, + "landing_weather_info_id": weather_landing.id, + "takeoff_airport_id": takeoff_airport.id, + "landing_airport_id": landing_airport.id, + "has_terrain_elevation": False, + "aircraft_id": aircraft_id, + "created_by_id": info.context.user_id + }) + return Flight(**flight.as_dict()) @strawberry.type @@ -247,56 +259,66 @@ class EditFlightMutation: @strawberry.mutation @authenticated_user_only() async def edit_flight(self, info, id: int, input: EditFlightInput) -> Flight: - db = info.context.db user_id = info.context.user_id - flight = (await db.scalars( - get_base_query(user_id=user_id, is_auth=bool(user_id)).filter(models.Flight.id == id) - )).one() + async with get_session() as db: - 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, - ) + 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() - - if input.gpx_track is not None: - data['gpx_track_filename'] = await handle_upload_gpx(flight, input.gpx_track) - info.context.background_tasks.add_task(add_terrain_elevation, flight=flight, gpx_filename=data['gpx_track_filename'], db=db) - - # TODO: nasledujici metody volat i pokud se zmenil cas vzletu! - if input.takeoff_airport and input.takeoff_airport.id != flight.takeoff_airport_id: - await handle_airport_changed( + takeoff_airport, landing_airport = await get_airports( db, - flight, - takeoff_airport, - type_="takeoff", - input_datetime=data.get('takeoff_datetime') + 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.landing_airport and input.landing_airport.id != flight.landing_airport_id: - await handle_airport_changed( - db, - flight, - landing_airport, - type_="landing", - input_datetime=data.get('landing_datetime') - ) + data = input.to_dict() - if input.aircraft is not None: - data['aircraft_id'] = await handle_aircraft_save(db, user_id, input.aircraft) + if input.gpx_track is not None: + data['gpx_track_filename'] = await handle_upload_gpx(flight, input.gpx_track) + info.context.background_tasks.add_task( + add_terrain_elevation, flight=flight, gpx_filename=data['gpx_track_filename'], db=db + ) - if input.track is not None: - await handle_track_edit(db=db, flight=flight, track=input.track, user_id=user_id) + if ( + (input.takeoff_airport and input.takeoff_airport.id != flight.takeoff_airport_id) or + (data.get('takeoff_datetime') and data.get('takeoff_datetime') != flight.takeoff_datetime) + ): + await handle_airport_changed( + db, + flight, + takeoff_airport, + type_="takeoff", + input_datetime=data.get('takeoff_datetime') + ) - copilots = await handle_copilots_edit(db, input.copilots or [], user_id) - await db.execute(delete(flight_has_copilot).filter_by(flight_id=flight.id)) - for copilot_id in copilots: - await db.execute(insert(flight_has_copilot).values(flight_id=flight.id, copilot_id=copilot_id)) + if ( + (input.landing_airport and input.landing_airport.id != flight.landing_airport_id) or + (data.get('landing_datetime') and data.get('landing_datetime') != flight.takeoff_datetime) + ): + await handle_airport_changed( + db, + flight, + landing_airport, + type_="landing", + input_datetime=data.get('landing_datetime') + ) - return await models.Flight.update(db, obj=flight, data=data) + if input.aircraft is not None: + data['aircraft_id'] = await handle_aircraft_save(db, user_id, input.aircraft) + + if input.track is not None: + await handle_track_edit(db=db, flight=flight, track=input.track, user_id=user_id) + + copilots = await handle_copilots_edit(db, input.copilots or [], user_id) + await db.execute(delete(flight_has_copilot).filter_by(flight_id=flight.id)) + for copilot_id in copilots: + await db.execute(insert(flight_has_copilot).values(flight_id=flight.id, copilot_id=copilot_id)) + + updated_flight = await models.Flight.update(db, obj=flight, data=data) + + return Flight(**updated_flight.as_dict()) @strawberry.type @@ -305,13 +327,15 @@ class DeleteFlightMutation: @strawberry.mutation @authenticated_user_only() async def delete_flight(self, info, id: int) -> Flight: - flight = ( - (await info.context.db.scalars( - get_base_query(user_id=info.context.user_id, is_auth=True) - .filter(models.Flight.id == id)) - ) - .one() - ) - flight.deleted = True + async with get_session() as db: + flight = ( + (await db.scalars( + get_base_query(user_id=info.context.user_id, is_auth=True) + .filter(models.Flight.id == id)) + ) + .one() + ) - return flight + updated_flight = await models.Flight.update(db, obj=flight, data=dict(deleted=True)) + + return Flight(**updated_flight.as_dict()) diff --git a/src/graphql_schema/entities/helpers/flight.py b/src/graphql_schema/entities/helpers/flight.py index b7f255a..3d32c52 100644 --- a/src/graphql_schema/entities/helpers/flight.py +++ b/src/graphql_schema/entities/helpers/flight.py @@ -9,7 +9,7 @@ from starlette.background import BackgroundTasks from strawberry.file_uploads import Upload from database import models -from external.elevation import ElevationAPI +from external.elevation import ElevationAPI, elevation_api from external.gpx_parser import GPXParser from external.weather import Weather from graphql_schema.types import ComboboxInput @@ -104,6 +104,7 @@ async def handle_airport_changed( flight_datetime = getattr(flight, f"{type_}_datetime") if input_datetime and input_datetime != flight_datetime: weather = await handle_weather_info(db, input_datetime, airport) + await db.flush() existing_weather_id = getattr(flight, f"{type_}_weather_info_id") if existing_weather_id: @@ -119,7 +120,6 @@ async def handle_airport_changed( async def add_terrain_elevation(db: AsyncSession, flight: models.Flight, gpx_filename: str): path = "/app/uploads/tracks" # TODO vytahnout do configu - elevation_api = ElevationAPI() gpx_parser = GPXParser(f"{path}/{gpx_filename}") coordinates = await gpx_parser.get_coordinates() @@ -131,7 +131,7 @@ async def add_terrain_elevation(db: AsyncSession, flight: models.Flight, gpx_fil 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}") - await models.Flight.update(db, {"gpx_track_filename": output_name}, obj=flight) + await models.Flight.update(db, {"gpx_track_filename": output_name, "has_terrain_elevation": True}, obj=flight) except ClientResponseError: print("NEumim elevation!") diff --git a/src/graphql_schema/entities/photo.py b/src/graphql_schema/entities/photo.py index 3d66235..795b77d 100644 --- a/src/graphql_schema/entities/photo.py +++ b/src/graphql_schema/entities/photo.py @@ -2,8 +2,10 @@ from typing import List, Optional, Annotated, TYPE_CHECKING import strawberry from sqlalchemy import select, update from strawberry.file_uploads import Upload +from background_jobs.photo import add_terrain_elevation from database import models from decorators.endpoints import authenticated_user_only +from dependencies.db import get_session from graphql_schema.dataloaders.poi import poi_dataloader from graphql_schema.entities.poi import PointOfInterest from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type @@ -55,7 +57,10 @@ class PhotoQueries: @strawberry.field() async def photos(root, info) -> List[Photo]: query = get_base_query(info.context.user_id) - return (await info.context.db.scalars(query)).all() + + async with get_session() as db: + photos = (await db.scalars(query)).all() + return [Photo(**photo.as_dict()) for photo in photos] @strawberry.type @@ -73,24 +78,30 @@ class UploadPhotoMutation: async def upload_photo(self, info, input: UploadPhotoInput) -> Photo: path = get_photo_basepath(input.flight_id) filename = await handle_file_upload(input.photo, path) - - info.context.background_tasks.add_task(resize_image, path=path, filename=filename, new_width=2500) - info.context.background_tasks.add_task(generate_thumbnail, path=path, filename=filename) - exif_info = await parse_exif_info(path, filename) - return await models.Photo.create(data={ - "flight_id": input.flight_id, - "name": input.name, - "filename": filename, - "description": input.description, - "exposed_at": exif_info.get("datetime"), - "gps_latitude": exif_info.get("gps_latitude"), - "gps_longitude": exif_info.get("gps_longitude"), - "gps_altitude": exif_info.get("gps_altitude"), - "is_flight_cover": False, - "created_by_id": info.context.user_id, - }, db_session=info.context.db) + async with get_session() as db: + photo_model = await models.Photo.create(data={ + "flight_id": input.flight_id, + "name": input.name, + "filename": filename, + "description": input.description, + "exposed_at": exif_info.get("datetime"), + "gps_latitude": exif_info.get("gps_latitude"), + "gps_longitude": exif_info.get("gps_longitude"), + "gps_altitude": exif_info.get("gps_altitude"), + "is_flight_cover": False, + "created_by_id": info.context.user_id, + }, db_session=db) + photo = Photo(**photo_model.as_dict()) + + info.context.background_tasks.add_task(resize_image, path=path, filename=filename, new_width=2500, quality=85) + info.context.background_tasks.add_task(generate_thumbnail, path=path, filename=filename) + + if exif_info.get("gps_latitude") and exif_info.get("gps_longitude"): + info.context.background_tasks.add_task(add_terrain_elevation, photo=photo) + + return photo @strawberry.type @@ -106,34 +117,36 @@ class EditPhotoMutation: @authenticated_user_only() async def edit_photo(self, info, id: int, input: EditPhotoInput) -> Photo: query = get_base_query(info.context.user_id) - photo = (await info.context.db.scalars(query.filter(models.Photo.id == id))).one() data = { key: getattr(input, key) for key in ('name', 'description', 'is_flight_cover') if getattr(input, key) is not None } - if input.point_of_interest: - data['point_of_interest_id'] = await handle_combobox_save( - info.context.db, - models.PointOfInterest, - input.point_of_interest, - info.context.user_id, - extra_data={ - "description": "" - } - ) - updated_model = await models.Photo.update(info.context.db, obj=photo, data=data) + async with get_session() as db: + photo = (await db.scalars(query.filter(models.Photo.id == id))).one() - if input.is_flight_cover: - # reset other covers - (await info.context.db.execute( - update(models.Photo) - .filter(models.Photo.flight_id == photo.flight_id) - .filter(models.Photo.id != id).values(is_flight_cover=False)) - ) + if input.point_of_interest: + data['point_of_interest_id'] = await handle_combobox_save( + db, + models.PointOfInterest, + input.point_of_interest, + info.context.user_id, + extra_data={ + "description": "" + } + ) - return updated_model + if input.is_flight_cover: + # reset other covers + (await db.execute( + update(models.Photo) + .filter(models.Photo.flight_id == photo.flight_id) + .filter(models.Photo.id != id).values(is_flight_cover=False)) + ) + + updated_model = await models.Photo.update(db, obj=photo, data=data) + return Photo(**updated_model.as_dict()) @strawberry.type @@ -142,7 +155,10 @@ class DeletePhotoMutation: @authenticated_user_only() async def delete_photo(self, info, id: int) -> Photo: query = get_base_query(info.context.user_id) - photo = (await info.context.db.scalars(query.filter(models.Photo.id == id))).one() + async with get_session() as db: + photo_model = (await db.scalars(query.filter(models.Photo.id == id))).one() + await db.delete(photo_model) + photo = Photo(**photo_model.as_dict()) base_path = get_photo_basepath(photo.flight_id) try: @@ -151,6 +167,4 @@ class DeletePhotoMutation: except Exception as e: print(e) - await info.context.db.delete(photo) - - return photo + return photo_model diff --git a/src/graphql_schema/entities/poi.py b/src/graphql_schema/entities/poi.py index 1759a7d..ef96874 100644 --- a/src/graphql_schema/entities/poi.py +++ b/src/graphql_schema/entities/poi.py @@ -3,6 +3,7 @@ import strawberry from sqlalchemy import select, or_ from database import models from decorators.endpoints import authenticated_user_only +from dependencies.db import get_session from graphql_schema.dataloaders.flight import flight_by_poi_dataloader from graphql_schema.dataloaders.photos import poi_photos_dataloader from graphql_schema.dataloaders.poi import poi_type_dataloader @@ -15,6 +16,7 @@ if TYPE_CHECKING: from .flight import Flight from .photo import Photo + @strawberry_sqlalchemy_type(models.PointOfInterest) class PointOfInterest: async def load_photos(root): @@ -36,7 +38,6 @@ def get_base_query(user_id: int, only_my: bool = False): select(models.PointOfInterest) .filter(models.PointOfInterest.deleted.is_(False)) ) - if only_my: query = query.filter(models.PointOfInterest.created_by_id == user_id) else: @@ -58,8 +59,9 @@ class PointOfInterestQueries: get_base_query(info.context.user_id) .order_by(models.PointOfInterest.id.desc()) ) - - return (await info.context.db.scalars(query)).all() + async with get_session() as db: + pois = (await db.scalars(query)).all() + return [PointOfInterest(**poi.as_dict()) for poi in pois] @strawberry.field() @authenticated_user_only() @@ -68,7 +70,9 @@ class PointOfInterestQueries: get_base_query(info.context.user_id) .filter(models.PointOfInterest.id == id) ) - return (await info.context.db.scalars(query)).one() + async with get_session() as db: + poi = (await db.scalars(query)).one() + return PointOfInterest(**poi.as_dict()) @strawberry.type @@ -82,21 +86,14 @@ class CreatePointOfInterestMutation: async def create_point_of_interest(root, info, input: CreatePointOfInterestInput) -> PointOfInterest: input_data = input.to_dict() - if input.type: - input_data['type_id'] = await handle_combobox_save( - info.context.db, - models.PointOfInterestType, - input.type, - info.context.user_id - ) + async with get_session() as db: + if input.type: + input_data['type_id'] = await handle_combobox_save( + db, models.PointOfInterestType, input.type, info.context.user_id + ) - return await models.PointOfInterest.create( - info.context.db, - data=dict( - **input_data, - created_by_id=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 @@ -111,20 +108,19 @@ class EditPointOfInterestMutation: # TODO: kontrola organizace input_data = input.to_dict() - if input.type is not None: - input_data['type_id'] = await handle_combobox_save( - info.context.db, - models.PointOfInterestType, - input.type, - info.context.user_id - ) + async with get_session() as db: + if input.type is not None: + input_data['type_id'] = await handle_combobox_save( + db, models.PointOfInterestType, input.type, info.context.user_id + ) - poi = ( - await info.context.db.scalars( - get_base_query(info.context.user_id, only_my=True) - .filter(models.PointOfInterest.id == id)) - ).one() - return await models.PointOfInterest.update(info.context.db, obj=poi, data=input_data) + poi = ( + await db.scalars( + get_base_query(info.context.user_id, only_my=True) + .filter(models.PointOfInterest.id == id) + )).one() + updated_poi = await models.PointOfInterest.update(db, obj=poi, data=input_data) + return PointOfInterest(**updated_poi.as_dict()) @strawberry.type @@ -133,6 +129,11 @@ class DeletePointOfInterestMutation: @strawberry.mutation @authenticated_user_only() async def delete_point_of_interest(self, info, id: int) -> PointOfInterest: - poi = get_base_query(info.context.user_id, only_my=True).filter(models.PointOfInterest.id == id).one() + async with get_session() as db: + poi = (await db.scalars( + get_base_query(info.context.user_id, only_my=True) + .filter(models.PointOfInterest.id == id) + )).one() - return await models.PointOfInterest.update(info.context.db, obj=poi, data=dict(deleted=True)) + updated_poi = await models.PointOfInterest.update(db, obj=poi, data=dict(deleted=True)) + return PointOfInterest(**updated_poi.as_dict()) diff --git a/src/graphql_schema/entities/poi_type.py b/src/graphql_schema/entities/poi_type.py index ed45d40..52913ac 100644 --- a/src/graphql_schema/entities/poi_type.py +++ b/src/graphql_schema/entities/poi_type.py @@ -3,6 +3,7 @@ import strawberry from sqlalchemy import select, or_ 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_type from graphql_schema.types import ComboboxInput @@ -40,7 +41,9 @@ class PointOfInterestTypeQueries: .order_by(models.PointOfInterestType.id.desc()) ) - return (await info.context.db.scalars(query)).all() + async with get_session() as db: + poi_types = (await db.scalars(query)).all() + return [PointOfInterestType(**poi_type.as_dict()) for poi_type in poi_types] @strawberry.field() @authenticated_user_only() @@ -49,7 +52,9 @@ class PointOfInterestTypeQueries: get_base_query(info.context.user_id) .filter(models.PointOfInterestType.id == id) ) - return (await info.context.db.scalars(query)).one() + async with get_session() as db: + poi_type = (await db.scalars(query)).one() + return PointOfInterestType(**poi_type.as_dict()) # # @strawberry.type diff --git a/src/graphql_schema/entities/user.py b/src/graphql_schema/entities/user.py index c343a4a..84bd05a 100644 --- a/src/graphql_schema/entities/user.py +++ b/src/graphql_schema/entities/user.py @@ -7,14 +7,14 @@ from sqlalchemy import select from strawberry.file_uploads import Upload from config import API_URL from database import models -from database.models import User from decorators.endpoints import authenticated_user_only from decorators.error_logging import error_logging +from dependencies.db import get_session from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type from upload_utils import handle_file_upload, delete_file, get_public_url, resize_image -@strawberry_sqlalchemy_type(User, exclude_fields=['password_hashed']) +@strawberry_sqlalchemy_type(models.User, exclude_fields=['password_hashed']) class User: async def load_avatar_image_url(root): if not root.avatar_image_filename: @@ -40,15 +40,22 @@ class UserQueries: if len(username) == 0: raise GraphQLError("Username not set!") - return (await info.context.db.scalars(select(models.User).filter_by(public_username=username))).one() + async with get_session() as db: + user_model = (await db.scalars(select(models.User).filter_by(public_username=username))).one() + user = User(**user_model.as_dict()) + + return user @strawberry.field() @authenticated_user_only() @error_logging async def logged_user(root, info) -> User: - return (await info.context.db.scalars( - select(models.User).filter_by(id=info.context.user_id) - )).one() + async with get_session() as db: + user_model = (await db.scalars( + select(models.User).filter_by(id=info.context.user_id) + )).one() + + return User(**user_model.as_dict()) @strawberry.type @@ -66,35 +73,36 @@ class EditUserMutation: @strawberry.mutation @authenticated_user_only() async def edit_logged_user(root, info, input: EditUserInput) -> User: - user = (await info.context.db.scalars( - select(models.User).filter_by(id=info.context.user_id) - )).one() + async with get_session() as db: + user = (await db.scalars( + select(models.User).filter_by(id=info.context.user_id) + )).one() - user_image_path = f"/app/uploads/profile/{user.id}" - data = input.to_dict() - if input.avatar_image: - if user.avatar_image_filename: - delete_file(f"{user_image_path}/{user.avatar_image_filename}", silent=True) + user_image_path = f"/app/uploads/profile/{user.id}" + data = input.to_dict() + if input.avatar_image: + if user.avatar_image_filename: + delete_file(f"{user_image_path}/{user.avatar_image_filename}", silent=True) - data['avatar_image_filename'] = await handle_file_upload(input.avatar_image, user_image_path) - info.context.background_tasks.add_task( - resize_image, path=user_image_path, filename=data['avatar_image_filename'], new_width=400 - ) + data['avatar_image_filename'] = await handle_file_upload(input.avatar_image, user_image_path) + info.context.background_tasks.add_task( + resize_image, path=user_image_path, filename=data['avatar_image_filename'], new_width=400 + ) - if input.title_image: - if user.title_image_filename: - delete_file(f"{user_image_path}/{user.title_image_filename}", silent=True) + if input.title_image: + if user.title_image_filename: + delete_file(f"{user_image_path}/{user.title_image_filename}", silent=True) - data['title_image_filename'] = await handle_file_upload(input.title_image, user_image_path) + data['title_image_filename'] = await handle_file_upload(input.title_image, user_image_path) + info.context.background_tasks.add_task( + resize_image, path=user_image_path, filename=data['title_image_filename'], new_width=800 + ) - info.context.background_tasks.add_task( - resize_image, path=user_image_path, filename=data['title_image_filename'], new_width=800 - ) + if input.old_password and input.new_password: + if not bcrypt.verify(input.old_password, user.password_hashed): + raise GraphQLError("Bad password") - if input.old_password and input.new_password: - if not bcrypt.verify(input.old_password, user.password_hashed): - raise GraphQLError("Bad password") + data['password_hashed'] = bcrypt.hash(input.new_password) - data['password_hashed'] = bcrypt.hash(input.new_password) - - return await models.User.update(info.context.db, obj=user, data=data) + user_model = await models.User.update(db, obj=user, data=data) + return User(**user_model.as_dict()) diff --git a/src/graphql_schema/query.py b/src/graphql_schema/query.py index dda0a3e..27a191c 100644 --- a/src/graphql_schema/query.py +++ b/src/graphql_schema/query.py @@ -3,6 +3,7 @@ from .entities.aircraft import AircraftQueries from .entities.airport import AirportQueries from .entities.copilot import CopilotQueries from .entities.flight import FlightQueries +from .entities.photo import PhotoQueries from .entities.poi import PointOfInterestQueries from .entities.poi_type import PointOfInterestTypeQueries from .entities.user import UserQueries @@ -16,6 +17,7 @@ Query = merge_types('Query', ( FlightQueries, CopilotQueries, UserQueries, + PhotoQueries, PointOfInterestQueries, PointOfInterestTypeQueries, )) diff --git a/src/graphql_schema/schema.py b/src/graphql_schema/schema.py index 8aec9a1..6554007 100644 --- a/src/graphql_schema/schema.py +++ b/src/graphql_schema/schema.py @@ -30,7 +30,6 @@ class LoggingExtension(SchemaExtension): @dataclasses.dataclass class GraphQLContext(BaseContext): - db: AsyncSession user_id: int jwt_auth_credentials: JwtAuthorizationCredentials jwt: JwtAccessBearerCookie diff --git a/src/graphql_schema/sqlalchemy_to_strawberry_type.py b/src/graphql_schema/sqlalchemy_to_strawberry_type.py index a286cc8..d1c6b89 100644 --- a/src/graphql_schema/sqlalchemy_to_strawberry_type.py +++ b/src/graphql_schema/sqlalchemy_to_strawberry_type.py @@ -31,8 +31,8 @@ def strawberry_sqlalchemy_type(model, exclude_fields: Optional[typing.Union[List if exclude_fields is None: exclude_fields = [] - def from_sqlalchemy_model(model: BaseModel): - return model + def from_sqlalchemy_model(cls, model: BaseModel): + return cls(model) def wrapper(cls): cls.__annotations__.update(get_annotations_for_scalars(model, exclude_fields=exclude_fields + ["deleted"])) diff --git a/src/main.py b/src/main.py index 7d59f59..91cc43e 100644 --- a/src/main.py +++ b/src/main.py @@ -57,12 +57,8 @@ class App: app.mount("/static", StaticFiles(directory="/app/static"), name="static") def setup_graphql_endpoint(self, app: FastAPI): - def setup_graphql_context( - credentials: JwtAuthorizationCredentials = Security(self.access_security), - db: AsyncSession = Depends(db_session) - ): + def setup_graphql_context(credentials: JwtAuthorizationCredentials = Security(self.access_security)): return GraphQLContext( - db=db, user_id=credentials['id'] if credentials else None, jwt_auth_credentials=credentials, jwt=self.access_security, diff --git a/src/paths.py b/src/paths.py new file mode 100644 index 0000000..e69de29 diff --git a/src/scripts/elevation.py b/src/scripts/elevation.py new file mode 100644 index 0000000..fe45ffc --- /dev/null +++ b/src/scripts/elevation.py @@ -0,0 +1,72 @@ +import asyncio +import sys + +from sqlalchemy import select + +sys.path.insert(0, "/app/src") + +from database import async_session, models +from external.elevation import elevation_api +from external.gpx_parser import GPXParser + + +async def add_elevation_to_photos(): + async with async_session() as session: + photos = (await session.scalars( + select(models.Photo) + .filter(models.Photo.terrain_elevation.is_(None)) + )).all() + + coordinates = [{"lat": p.gps_latitude, "lng": p.gps_longitude} for p in photos if p.gps_latitude or p.gps_longitude] + photos_by_corrdinates = {(p.gps_latitude, p.gps_longitude): p for p in photos} + if not coordinates: + print("all done") + return + + points = await elevation_api.get_elevation_for_points(coordinates) + for point in points: + photo = photos_by_corrdinates[point['lat'], point['lng']] + await models.Photo.update(db_session=session, obj=photo, data={"terrain_elevation": point['elevation']}) + await session.flush() + await session.commit() + + +async def add_elevation_to_tracks(): + async with async_session() as session: + flights = (await session.scalars( + select(models.Flight) + .filter(models.Flight.has_terrain_elevation == False) + .filter(models.Flight.gpx_track_filename.isnot(None)) + )).all() + + if not flights: + print("all done") + return + + for flight in flights: + gpx_file = f"/app/uploads/tracks/{flight.gpx_track_filename}" + gpx = GPXParser(gpx_file) + + coordinates = await gpx.get_coordinates() + elevation = await elevation_api.get_elevation_for_points(coordinates) + 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}") + await models.Flight.update( + db_session=session, obj=flight, data={ + "has_terrain_elevation": True, + "gpx_track_filename": output_name + } + ) + await session.flush() + await session.commit() + + +async def run_all(): + await asyncio.gather(add_elevation_to_photos(), add_elevation_to_tracks()) + + +if __name__ == "__main__": + loop = asyncio.get_event_loop() + loop.run_until_complete(run_all())