Stahovani pocasi, editace kopilota, nahravani GPX k letu

This commit is contained in:
Michal Kváček
2023-08-07 23:21:59 +02:00
parent fcd80f20e8
commit ca500472e3
24 changed files with 680 additions and 122 deletions
@@ -0,0 +1,40 @@
"""gps u letiste nejsou nullable, pridani sloupce photo.exposed_at
Revision ID: 708fc32f6a3b
Revises: 804e55bbf855
Create Date: 2023-07-31 04:51:26.257834
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '708fc32f6a3b'
down_revision = '804e55bbf855'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('airport', 'gps_latitude',
existing_type=mysql.FLOAT(),
nullable=False)
op.alter_column('airport', 'gps_longitude',
existing_type=mysql.FLOAT(),
nullable=False)
op.add_column('photo', sa.Column('exposed_at', sa.DateTime(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('photo', 'exposed_at')
op.alter_column('airport', 'gps_longitude',
existing_type=mysql.FLOAT(),
nullable=True)
op.alter_column('airport', 'gps_latitude',
existing_type=mysql.FLOAT(),
nullable=True)
# ### end Alembic commands ###
@@ -0,0 +1,28 @@
"""photo.gps_altitude
Revision ID: cf1fb904e9ab
Revises: 708fc32f6a3b
Create Date: 2023-07-31 08:57:10.715298
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'cf1fb904e9ab'
down_revision = '708fc32f6a3b'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('photo', sa.Column('gps_altitude', sa.Float(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('photo', 'gps_altitude')
# ### end Alembic commands ###
@@ -0,0 +1,38 @@
"""add weather info
Revision ID: 36ea098fde31
Revises: cf1fb904e9ab
Create Date: 2023-08-01 04:37:17.188376
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '36ea098fde31'
down_revision = 'cf1fb904e9ab'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('airport', sa.Column('created_by_id', sa.Integer(), nullable=True))
op.create_foreign_key(None, 'airport', 'user', ['created_by_id'], ['id'])
op.add_column('flight', sa.Column('weather_info_takeoff_id', sa.Integer(), nullable=True))
op.add_column('flight', sa.Column('weather_info_landing_id', sa.Integer(), nullable=True))
op.create_foreign_key(None, 'flight', 'weather_info', ['weather_info_takeoff_id'], ['id'])
op.create_foreign_key(None, 'flight', 'weather_info', ['weather_info_landing_id'], ['id'])
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint('flight_ibfk_6', 'flight', type_='foreignkey')
op.drop_constraint('flight_ibfk_7', 'flight', type_='foreignkey')
op.drop_column('flight', 'weather_info_landing_id')
op.drop_column('flight', 'weather_info_takeoff_id')
op.drop_constraint('airport_ibfk_1', 'airport', type_='foreignkey')
op.drop_column('airport', 'created_by_id')
# ### end Alembic commands ###
@@ -0,0 +1,28 @@
"""add datetime to weather_info
Revision ID: 2a320e1e093d
Revises: 36ea098fde31
Create Date: 2023-08-01 04:47:22.122423
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '2a320e1e093d'
down_revision = '36ea098fde31'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('weather_info', sa.Column('datetime', sa.DateTime(), nullable=False))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('weather_info', 'datetime')
# ### end Alembic commands ###
@@ -0,0 +1,28 @@
"""add gpx to flight
Revision ID: 1e04cd7838dd
Revises: 2a320e1e093d
Create Date: 2023-08-07 18:55:00.914684
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '1e04cd7838dd'
down_revision = '2a320e1e093d'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('flight', sa.Column('gpx_track_filename', sa.String(length=128), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('flight', 'gpx_track_filename')
# ### end Alembic commands ###
@@ -0,0 +1,38 @@
"""airports cannot be nullable
Revision ID: f85556b2ce2c
Revises: 1e04cd7838dd
Create Date: 2023-08-07 19:14:17.791435
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = 'f85556b2ce2c'
down_revision = '1e04cd7838dd'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('flight', 'takeoff_airport_id',
existing_type=mysql.INTEGER(display_width=11),
nullable=False)
op.alter_column('flight', 'landing_airport_id',
existing_type=mysql.INTEGER(display_width=11),
nullable=False)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('flight', 'landing_airport_id',
existing_type=mysql.INTEGER(display_width=11),
nullable=True)
op.alter_column('flight', 'takeoff_airport_id',
existing_type=mysql.INTEGER(display_width=11),
nullable=True)
# ### end Alembic commands ###
@@ -0,0 +1,30 @@
"""remove not used fields in flight
Revision ID: b5c66e5a2df7
Revises: f85556b2ce2c
Create Date: 2023-08-07 20:53:00.717813
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = 'b5c66e5a2df7'
down_revision = 'f85556b2ce2c'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('flight', 'with_instructor')
op.drop_column('flight', 'solo')
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('flight', sa.Column('solo', mysql.TINYINT(display_width=1), autoincrement=False, nullable=False))
op.add_column('flight', sa.Column('with_instructor', mysql.TINYINT(display_width=1), autoincrement=False, nullable=False))
# ### end Alembic commands ###
+5
View File
@@ -8,3 +8,8 @@ alembic==1.11.1
passlib==1.7.4 passlib==1.7.4
pydantic==1.10.11 # vysla uz 2.0, ale nejak mi to nefunguje pydantic==1.10.11 # vysla uz 2.0, ale nejak mi to nefunguje
pillow
exif
aiocache
aiohttp
+29 -12
View File
@@ -27,8 +27,7 @@ class BaseModel:
async def create(cls, db_session: AsyncSession, data: dict): async def create(cls, db_session: AsyncSession, data: dict):
model = cls(**data) model = cls(**data)
db_session.add(model) db_session.add(model)
# await db_session.flush() await db_session.flush()
# await db_session.refresh(model)
return model return model
@@ -43,12 +42,10 @@ class BaseModel:
if getattr(obj, key) != value: if getattr(obj, key) != value:
setattr(obj, key, value) setattr(obj, key, value)
# await db_session.commit()
return obj return obj
# TODO: doplnit GPX k letu, pocasi k letu (podle lokality, mozna do FlightTrack) # TODO: doplnit GPX k letu
user_is_in_organization = Table( user_is_in_organization = Table(
"user_is_in_organization", "user_is_in_organization",
@@ -67,10 +64,13 @@ class Airport(BaseModel):
gps_latitude: Mapped[float] = mapped_column(Float, nullable=False) gps_latitude: Mapped[float] = mapped_column(Float, nullable=False)
gps_longitude: Mapped[float] = mapped_column(Float, nullable=False) gps_longitude: Mapped[float] = mapped_column(Float, nullable=False)
elevation: Mapped[int] = mapped_column(Integer, nullable=True) elevation: Mapped[int] = mapped_column(Integer, nullable=True)
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'), nullable=True) # automaticky import nebude mit ID
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0') deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
metars: Mapped['Metar'] = relationship(back_populates="airport") metars: Mapped['Metar'] = relationship(back_populates="airport")
created_by: Mapped['User'] = relationship()
class PointOfInterestType(BaseModel): class PointOfInterestType(BaseModel):
@@ -112,8 +112,10 @@ class Photo(BaseModel):
filename: Mapped[str] = mapped_column(String(128), nullable=False) filename: Mapped[str] = mapped_column(String(128), nullable=False)
is_flight_cover: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default="0") is_flight_cover: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default="0")
description: Mapped[str] = mapped_column(Text, nullable=False) description: Mapped[str] = mapped_column(Text, nullable=False)
exposed_at: Mapped[datetime] = mapped_column(DateTime, nullable=True)
gps_latitude: Mapped[float] = mapped_column(Float, nullable=True) gps_latitude: Mapped[float] = mapped_column(Float, nullable=True)
gps_longitude: 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)
point_of_interest_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest.id"), 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) flight_id: Mapped[int] = mapped_column(Integer, ForeignKey("flight.id"), nullable=False)
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id')) created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
@@ -184,23 +186,38 @@ class FlightTrack(BaseModel):
point_of_interest: Mapped['PointOfInterest'] = relationship() point_of_interest: Mapped['PointOfInterest'] = relationship()
class WeatherInfo(BaseModel):
__tablename__ = "weather_info"
id: Mapped[int] = mapped_column(primary_key=True)
qnh: Mapped[int] = mapped_column(Integer, nullable=True)
temperature_surface: Mapped[Float] = mapped_column(Float, nullable=True)
dewpoint_surface: Mapped[Float] = mapped_column(Float, nullable=True)
rain: Mapped[Float] = mapped_column(Float, nullable=True)
cloudcover_low: Mapped[Float] = mapped_column(Float, nullable=True)
cloudcover_total: Mapped[Float] = mapped_column(Float, nullable=True)
wind_speed_surface: Mapped[Float] = mapped_column(Float, nullable=True)
wind_direction_surface: Mapped[Float] = mapped_column(Float, nullable=True)
datetime: Mapped[datetime] = mapped_column(DateTime, nullable=False)
class Flight(BaseModel): class Flight(BaseModel):
__tablename__ = "flight" __tablename__ = "flight"
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(128), nullable=False) name: Mapped[str] = mapped_column(String(128), nullable=False)
description: Mapped[str] = mapped_column(Text, nullable=False) description: Mapped[str] = mapped_column(Text, nullable=False)
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) takeoff_datetime: Mapped[datetime] = mapped_column(DateTime, nullable=False)
landing_datetime: Mapped[datetime] = mapped_column(DateTime, nullable=False) landing_datetime: Mapped[datetime] = mapped_column(DateTime, nullable=False)
duration_total: Mapped[int] = mapped_column(Integer, nullable=True) duration_total: Mapped[int] = mapped_column(Integer, nullable=True)
duration_pic: Mapped[int] = mapped_column(Integer, nullable=True) duration_pic: Mapped[int] = mapped_column(Integer, nullable=True)
takeoff_airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=True) gpx_track_filename: Mapped[str] = mapped_column(String(128), nullable=True)
landing_airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=True)
aircraft_id: Mapped[int] = mapped_column(Integer, ForeignKey('aircraft.id')) aircraft_id: Mapped[int] = mapped_column(Integer, ForeignKey('aircraft.id'))
copilot_id: Mapped[int] = mapped_column(Integer, ForeignKey('copilot.id'), nullable=True) copilot_id: Mapped[int] = mapped_column(Integer, ForeignKey('copilot.id'), nullable=True)
weather_info_takeoff_id: Mapped[int] = mapped_column(Integer, ForeignKey('weather_info.id'), nullable=True)
solo: Mapped[bool] = mapped_column(Boolean, default=True) weather_info_landing_id: Mapped[int] = mapped_column(Integer, ForeignKey('weather_info.id'), nullable=True)
with_instructor: Mapped[bool] = mapped_column(Boolean, default=False)
landings: Mapped[int] = mapped_column(Integer, default=1) landings: Mapped[int] = mapped_column(Integer, default=1)
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id')) created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
@@ -209,6 +226,8 @@ class Flight(BaseModel):
takeoff_airport: Mapped['Airport'] = relationship(foreign_keys=[takeoff_airport_id]) takeoff_airport: Mapped['Airport'] = relationship(foreign_keys=[takeoff_airport_id])
landing_airport: Mapped['Airport'] = relationship(foreign_keys=[landing_airport_id]) landing_airport: Mapped['Airport'] = relationship(foreign_keys=[landing_airport_id])
weather_info_landing: Mapped[WeatherInfo] = relationship(foreign_keys=[weather_info_landing_id])
weather_info_takeoff: Mapped[WeatherInfo] = relationship(foreign_keys=[weather_info_takeoff_id])
track: Mapped['FlightTrack'] = relationship() track: Mapped['FlightTrack'] = relationship()
copilot: Mapped['Copilot'] = relationship(back_populates="flights") copilot: Mapped['Copilot'] = relationship(back_populates="flights")
aircraft: Mapped['Aircraft'] = relationship(back_populates="flights") aircraft: Mapped['Aircraft'] = relationship(back_populates="flights")
@@ -216,8 +235,6 @@ class Flight(BaseModel):
user: Mapped['User'] = relationship(back_populates="flights") user: Mapped['User'] = relationship(back_populates="flights")
created_by: Mapped['User'] = relationship() created_by: Mapped['User'] = relationship()
# flight_track: Mapped[List['PointOfInterest']] = relationship(secondary=FlightTrack)
class Copilot(BaseModel): class Copilot(BaseModel):
__tablename__ = "copilot" __tablename__ = "copilot"
+1
View File
@@ -48,6 +48,7 @@ class LoginEndpoint(BaseEndpoint):
"access_token_validity": self.access_security.access_expires_delta.total_seconds(), "access_token_validity": self.access_security.access_expires_delta.total_seconds(),
} }
class RefreshEndpoint(BaseEndpoint): class RefreshEndpoint(BaseEndpoint):
def __init__(self, access_token: JwtAccess, refresh_token: JwtRefresh): def __init__(self, access_token: JwtAccess, refresh_token: JwtRefresh):
super().__init__(db=None) super().__init__(db=None)
+58
View File
@@ -0,0 +1,58 @@
import datetime
import urllib.parse
from typing import Tuple, Dict
import aiohttp
from aiocache import cached
class Weather:
FORECAST_URL = "https://api.open-meteo.com/v1/forecast?"
ARCHIVE_URL = "https://archive-api.open-meteo.com/v1/archive?"
TIMEZONE = "Europe/Prague"
METRICS = (
"pressure_msl", "temperature_2m", "dewpoint_2m", "rain", "cloudcover_low", "cloudcover", "windspeed_10m",
"winddirection_10m"
)
def get_weather_info_url(self, start_date: datetime.date, end_date: datetime.date, gps: Tuple[float, float]) -> str:
today = datetime.datetime.now().date()
date_diff = today - end_date
if date_diff.days >= 7:
# historical API offers data only older than 5 days
url = self.ARCHIVE_URL
else:
# forecast contains data even 14 days ago
url = self.FORECAST_URL
params = {
"latitude": gps[0],
"longitude": gps[1],
"timezone": self.TIMEZONE,
"hourly": ','.join(self.METRICS),
"start_date": start_date,
"end_date": end_date,
}
query_string = urllib.parse.urlencode(params)
return f"{url}{query_string}"
@cached(ttl=6*3600)
async def download_weather_for_day(self, date: datetime.date, gps: Tuple[float, float]):
url = self.get_weather_info_url(start_date=date, end_date=date, gps=gps)
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
resp.raise_for_status()
return await resp.json()
async def get_weather_for_hour(self, date_time: datetime.datetime, gps: Tuple[float, float]) -> Dict[str, float|str]:
data = await self.download_weather_for_day(date_time.date(), gps)
# TODO: kontrola timezone!
# TODO: interpolace - udelat vazenyprumer z dvou po sobe jdoucich hodin
idx = date_time.hour
result_data = {metric: data['hourly'][metric][idx] for metric in self.METRICS}
result_data['datetime'] = datetime.datetime.strptime(data['hourly']['time'][idx], "%Y-%m-%dT%H:%M")
return result_data
+1 -1
View File
@@ -10,7 +10,7 @@ async def load(ids: List[int]):
models = (await session.scalars(select(Airport).filter(Airport.id.in_(ids)))).all() models = (await session.scalars(select(Airport).filter(Airport.id.in_(ids)))).all()
models_by_id = {model.id: model for model in models} models_by_id = {model.id: model for model in models}
return [models_by_id.get(id_) for id_ in ids] return [models_by_id[id_] for id_ in ids]
airport_dataloader = DataLoader(load_fn=load, cache=False) airport_dataloader = DataLoader(load_fn=load, cache=False)
+3 -1
View File
@@ -24,4 +24,6 @@ class FlightsLoader:
return [result_data[id_] for id_ in ids] return [result_data[id_] for id_ in ids]
flights_by_copilot_dataloader = DataLoader(load_fn=FlightsLoader("copilot_id").load, cache=False)
flights_by_copilot_dataloader = DataLoader(load_fn=FlightsLoader("copilot_id").load, cache=False)
flights_by_aircraft_dataloader = DataLoader(load_fn=FlightsLoader("aircraft_id").load, cache=False)
+16
View File
@@ -0,0 +1,16 @@
from typing import List
from sqlalchemy import select
from strawberry.dataloader import DataLoader
from database import async_session
from database.models import WeatherInfo
async def load(ids: List[int]):
async with async_session() as session:
models = (await session.scalars(select(WeatherInfo).filter(WeatherInfo.id.in_(ids)))).all()
models_by_id = {model.id: model for model in models}
return [models_by_id.get(id_) for id_ in ids]
airport_weather_info_loader = DataLoader(load_fn=load, cache=False)
+10 -1
View File
@@ -1,20 +1,29 @@
from typing import List, Optional from typing import List, Optional, Annotated, TYPE_CHECKING
import strawberry import strawberry
from strawberry.file_uploads import Upload from strawberry.file_uploads import Upload
from sqlalchemy import select from sqlalchemy import select
from database import models from database import models
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type, strawberry_sqlalchemy_input 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 upload_utils import handle_file_upload, delete_file, get_public_url
from ..dataloaders.flight import flights_by_aircraft_dataloader
if TYPE_CHECKING:
from .flight import Flight
AIRCRAFT_UPLOAD_DEST_PATH = "/app/uploads/aircrafts/" AIRCRAFT_UPLOAD_DEST_PATH = "/app/uploads/aircrafts/"
@strawberry_sqlalchemy_type(models.Aircraft) @strawberry_sqlalchemy_type(models.Aircraft)
class Aircraft: class Aircraft:
async def load_flights(root):
return await flights_by_aircraft_dataloader.load(root.id)
photo_url: Optional[str] = strawberry.field( photo_url: Optional[str] = strawberry.field(
resolver=lambda root: get_public_url(f"aircrafts/{root.photo_filename}") if root.photo_filename else None 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=load_flights)
def get_base_query(user_id: int): def get_base_query(user_id: int):
return ( return (
+40 -4
View File
@@ -3,13 +3,14 @@ import strawberry
from sqlalchemy import select from sqlalchemy import select
from database import models from database import models
from graphql_schema.dataloaders.flight import flights_by_copilot_dataloader from graphql_schema.dataloaders.flight import flights_by_copilot_dataloader
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type, strawberry_sqlalchemy_input
if TYPE_CHECKING: if TYPE_CHECKING:
from .flight import Flight from .flight import Flight
@strawberry_sqlalchemy_type(models.Copilot) @strawberry_sqlalchemy_type(models.Copilot)
class CopilotType: class Copilot:
async def load_flights(root): async def load_flights(root):
return await flights_by_copilot_dataloader.load(root.id) return await flights_by_copilot_dataloader.load(root.id)
@@ -28,14 +29,49 @@ def get_base_query(user_id: int):
@strawberry.type @strawberry.type
class CopilotQueries: class CopilotQueries:
@strawberry.field @strawberry.field
async def copilots(root, info) -> List[CopilotType]: async def copilots(root, info) -> List[Copilot]:
return (await info.context.db.scalars( return (await info.context.db.scalars(
get_base_query(info.context.user_id) get_base_query(info.context.user_id)
)).all() )).all()
@strawberry.field @strawberry.field
async def copilot(root, info, id: int) -> CopilotType: async def copilot(root, info, id: int) -> Copilot:
return (await info.context.db.scalars( return (await info.context.db.scalars(
get_base_query(info.context.user_id) get_base_query(info.context.user_id)
.filter(models.Copilot.id == id) .filter(models.Copilot.id == id)
)).one() )).one()
@strawberry.type
class CreateCopilotMutation:
@strawberry_sqlalchemy_input(model=models.Copilot, exclude_fields=["id"])
class CreateCopilotInput:
pass
@strawberry.mutation
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,
)
)
@strawberry.type
class EditCopilotMutation:
@strawberry_sqlalchemy_input(model=models.Copilot, exclude_fields=["id"])
class EditCopilotInput:
pass
@strawberry.mutation
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())
+95 -81
View File
@@ -1,8 +1,9 @@
from datetime import timedelta from datetime import timedelta
from typing import List, Optional, Annotated, TYPE_CHECKING from typing import List, Optional, Annotated, TYPE_CHECKING
import strawberry import strawberry
from sqlalchemy import select, delete from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from strawberry.file_uploads import Upload
from database import models from database import models
from graphql_schema.dataloaders import copilots_dataloader from graphql_schema.dataloaders import copilots_dataloader
from graphql_schema.dataloaders.aircraft import aircraft_dataloader from graphql_schema.dataloaders.aircraft import aircraft_dataloader
@@ -11,26 +12,16 @@ from graphql_schema.dataloaders.photos import photos_dataloader, cover_photo_loa
from graphql_schema.dataloaders.poi import flight_track_dataloader, poi_dataloader from graphql_schema.dataloaders.poi import flight_track_dataloader, poi_dataloader
from graphql_schema.entities.aircraft import Aircraft from graphql_schema.entities.aircraft import Aircraft
from graphql_schema.entities.airport import Airport from graphql_schema.entities.airport import Airport
from graphql_schema.entities.copilot import CopilotType
from graphql_schema.entities.photo import Photo from graphql_schema.entities.photo import Photo
from graphql_schema.entities.poi import PointOfInterest from graphql_schema.entities.poi import PointOfInterest
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type, strawberry_sqlalchemy_input from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type, strawberry_sqlalchemy_input
from upload_utils import get_public_url, handle_file_upload, check_directories, file_exists, delete_file
from .helpers.flight import handle_aircraft_save, handle_track_edit, handle_copilot_edit, handle_weather_info
from ..dataloaders.weather import airport_weather_info_loader
from ..types import ComboboxInput
if TYPE_CHECKING: if TYPE_CHECKING:
from .copilot import CopilotType from .copilot import Copilot
@strawberry.input()
class PointOfInterestInput:
id: Optional[int] = None
name: str
@strawberry.input()
class CopilotInput:
id: Optional[int] = None
name: str
@strawberry_sqlalchemy_type(models.FlightTrack) @strawberry_sqlalchemy_type(models.FlightTrack)
@@ -41,6 +32,11 @@ class FlightTrack:
point_of_interest: PointOfInterest = strawberry.field(resolver=load_poi) point_of_interest: PointOfInterest = strawberry.field(resolver=load_poi)
@strawberry_sqlalchemy_type(models.WeatherInfo)
class WeatherInfo:
pass
@strawberry_sqlalchemy_type(models.Flight) @strawberry_sqlalchemy_type(models.Flight)
class Flight: class Flight:
async def load_takeoff_airport(root): async def load_takeoff_airport(root):
@@ -64,6 +60,12 @@ class Flight:
async def load_cover_photo(root): async def load_cover_photo(root):
return await cover_photo_loader.load(root.id) return await cover_photo_loader.load(root.id)
async def load_takeoff_weather_info(root):
return await airport_weather_info_loader.load(root.weather_info_takeoff_id)
async def load_landing_weather_info(root):
return await airport_weather_info_loader.load(root.weather_info_landing_id)
def duration_min_calculated(root): def duration_min_calculated(root):
if root.duration_total: if root.duration_total:
return root.duration_total return root.duration_total
@@ -74,15 +76,23 @@ class Flight:
return 0 return 0
def load_gpx_track_url(root):
if not root.gpx_track_filename:
return None
return f"http://localhost:8000/uploads/tracks/{root.gpx_track_filename}"
duration_min_calculated: int = strawberry.field(resolver=duration_min_calculated) duration_min_calculated: int = strawberry.field(resolver=duration_min_calculated)
copilot: Optional[Annotated["CopilotType", strawberry.lazy(".copilot")]] = strawberry.field(resolver=load_copilot) copilot: Optional[Annotated["Copilot", strawberry.lazy(".copilot")]] = strawberry.field(resolver=load_copilot)
aircraft: Aircraft = strawberry.field(resolver=load_aircraft) aircraft: Aircraft = strawberry.field(resolver=load_aircraft)
takeoff_airport: Airport = strawberry.field(resolver=load_takeoff_airport) takeoff_airport: Airport = strawberry.field(resolver=load_takeoff_airport)
landing_airport: Airport = strawberry.field(resolver=load_landing_airport) landing_airport: Airport = strawberry.field(resolver=load_landing_airport)
cover_photo: Optional[Photo] = strawberry.field(resolver=load_cover_photo) cover_photo: Optional[Photo] = strawberry.field(resolver=load_cover_photo)
track: List[FlightTrack] = strawberry.field(resolver=load_track) track: List[FlightTrack] = strawberry.field(resolver=load_track)
takeoff_weather_info: Optional[WeatherInfo] = strawberry.field(resolver=load_takeoff_weather_info)
landing_weather_info: Optional[WeatherInfo] = strawberry.field(resolver=load_landing_weather_info)
photos: List[Photo] = strawberry.field(resolver=load_photos) photos: List[Photo] = strawberry.field(resolver=load_photos)
gpx_track_url: Optional[str] = strawberry.field(resolver=load_gpx_track_url)
def get_base_query(user_id: int): def get_base_query(user_id: int):
@@ -96,12 +106,9 @@ def get_base_query(user_id: int):
@strawberry.type @strawberry.type
class FlightQueries: class FlightQueries:
@strawberry.input
class FlightFilters:
takeoff: Optional[int]
@strawberry.field @strawberry.field
async def flights(root, info, filters: Optional[FlightFilters] = None) -> List[Flight]: async def flights(root, info) -> List[Flight]:
query = get_base_query(info.context.user_id).order_by(models.Flight.id.desc()) query = get_base_query(info.context.user_id).order_by(models.Flight.id.desc())
return (await info.context.db.scalars(query)).all() return (await info.context.db.scalars(query)).all()
@@ -118,88 +125,95 @@ class FlightQueries:
@strawberry.type @strawberry.type
class CreateFlightMutation: class CreateFlightMutation:
@strawberry_sqlalchemy_input(models.Flight, exclude_fields=["id"]) @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"
])
class CreateFlightInput: class CreateFlightInput:
pass aircraft: ComboboxInput
landing_airport: ComboboxInput
takeoff_airport: ComboboxInput
@strawberry.mutation @strawberry.mutation
async def create_flight(self, info, input: CreateFlightInput) -> Flight: async def create_flight(self, info, input: CreateFlightInput) -> Flight:
aircraft_id = await handle_aircraft_save(info.context.db, info.context.user_id, input.aircraft)
takeoff_airport = (await info.context.db.scalars(select(models.Airport).filter(models.Airport.id == input.takeoff_airport.id))).one()
if input.takeoff_airport.id == input.landing_airport.id:
landing_airport = takeoff_airport
else:
landing_airport = (await info.context.db.scalars(select(models.Airport).filter(models.Airport.id == input.landing_airport.id))).one()
weather_takeoff = await handle_weather_info(info.context.db, input.takeoff_datetime, takeoff_airport)
weather_landing = await handle_weather_info(info.context.db, input.landing_datetime, landing_airport)
return await models.Flight.create(info.context.db, data={ return await models.Flight.create(info.context.db, data={
**input.to_dict(), **input.to_dict(),
"weather_info_takeoff_id": weather_takeoff.id,
"weather_info_landing_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 "created_by_id": info.context.user_id
}) })
async def handle_track_edit(db: AsyncSession, flight: models.Flight, track: List[PointOfInterestInput], user_id: int):
await db.execute(delete(models.FlightTrack).filter(models.FlightTrack.flight_id == flight.id))
existing_poi_ids = [i.id for i in track if i.id]
poi_query = (
select(models.PointOfInterest)
.filter(models.PointOfInterest.created_by_id == user_id)
.filter(models.PointOfInterest.id.in_(existing_poi_ids))
)
pois = (await db.scalars(poi_query)).all()
poi_map = {poi.id: poi for poi in pois}
order = 0
for item in track:
poi_object = None
if item.id:
poi_object = poi_map.get(item.id)
if not poi_object:
poi_object = await models.PointOfInterest.create(db, data=dict(created_by_id=user_id, name=item.name))
await db.flush()
await models.FlightTrack.create(
db,
data={
"flight_id": flight.id,
"point_of_interest_id": poi_object.id,
"order": order
}
)
order += 1
async def handle_copilot_edit(db: AsyncSession, copilot: CopilotInput, user_id: int) -> int:
if copilot.id:
return copilot.id
else:
copilot = await models.Copilot.create(
db,
data={
"name": copilot.name,
"created_by_id": user_id,
}
)
await db.flush()
return copilot.id
@strawberry.type @strawberry.type
class EditFlightMutation: class EditFlightMutation:
@strawberry_sqlalchemy_input(models.Flight, exclude_fields=["id", "copilot_id", "deleted"], all_optional=True) @strawberry_sqlalchemy_input(models.Flight, exclude_fields=[
"id", "aircraft_id", "copilot_id", "deleted", "landing_airport_id", "takeoff_airport_id",
"weather_info_takeoff_id", "weather_info_landing_id", "gpx_track_filename"
], all_optional=True)
class EditFlightInput: class EditFlightInput:
track: Optional[List[PointOfInterestInput]] = None gpx_track: Optional[Upload] = None
copilot: Optional[CopilotInput] = None track: Optional[List[ComboboxInput]] = None
copilot: Optional[ComboboxInput] = None
aircraft: Optional[ComboboxInput] = None
landing_airport: Optional[ComboboxInput] = None
takeoff_airport: Optional[ComboboxInput] = None
@strawberry.mutation @strawberry.mutation
async def edit_flight(self, info, id: int, input: EditFlightInput) -> Flight: async def edit_flight(self, info, id: int, input: EditFlightInput) -> Flight:
# TODO: umoznit editovat jen vlastni lety! # TODO: umoznit editovat jen vlastni lety!
flight = await models.Flight.update(info.context.db, id=id, data=input.to_dict())
flight = (await info.context.db.scalars(
get_base_query(info.context.user_id).filter(models.Flight.id == id)
)).one()
data = input.to_dict()
if input.gpx_track is not None:
# TODO: poresit validaci uploadovaneho souboru!
path = "/app/uploads/tracks"
if flight.gpx_track_filename and file_exists(path+"/"+flight.gpx_track_filename):
delete_file(path+"/"+flight.gpx_track_filename)
data['gpx_track_filename'] = await handle_file_upload(input.gpx_track, path)
if input.takeoff_airport is not None:
# TODO: stahnout nove pocasi na novem miste! Stejne tak pri zmene data/casu odletu
data['takeoff_airport_id'] = input.takeoff_airport.id
if input.landing_airport is not None:
# TODO: stahnout nove pocasi na novem miste! Stejne tak pri zmene data/casu priletu
data['landing_airport_id'] = input.landing_airport.id
if input.aircraft is not None:
data['aircraft_id'] = await handle_aircraft_save(info.context.db, info.context.user_id, input.aircraft)
flight = await models.Flight.update(info.context.db, id=id, data=data)
if input.track is not None: if input.track is not None:
await handle_track_edit(db=info.context.db, flight=flight, track=input.track, user_id=info.context.user_id) await handle_track_edit(db=info.context.db, flight=flight, track=input.track, user_id=info.context.user_id)
if flight.solo: if input.copilot:
flight.copilot_id = None
elif input.copilot is not None:
flight.copilot_id = await handle_copilot_edit(info.context.db, input.copilot, info.context.user_id) flight.copilot_id = await handle_copilot_edit(info.context.db, input.copilot, info.context.user_id)
else:
flight.copilot_id = None
return flight return flight
@strawberry.type @strawberry.type
class DeleteFlightMutation: class DeleteFlightMutation:
@@ -214,4 +228,4 @@ class DeleteFlightMutation:
) )
flight.deleted = True flight.deleted = True
return flight return flight
@@ -0,0 +1,96 @@
from datetime import datetime
from typing import List, Type
from sqlalchemy import select, delete
from sqlalchemy.ext.asyncio import AsyncSession
from database import models
from external.weather import Weather
from graphql_schema.types import ComboboxInput
weather_api = Weather()
async def handle_weather_info(db: AsyncSession, date_time: datetime, airport: models.Airport) -> models.WeatherInfo:
weather = await weather_api.get_weather_for_hour(date_time, (airport.gps_latitude, airport.gps_longitude))
return await models.WeatherInfo.create(db_session=db, 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 def handle_track_edit(db: AsyncSession, flight: models.Flight, track: List[ComboboxInput], user_id: int):
await db.execute(delete(models.FlightTrack).filter(models.FlightTrack.flight_id == flight.id))
existing_poi_ids = [i.id for i in track if i.id]
poi_query = (
select(models.PointOfInterest)
.filter(models.PointOfInterest.created_by_id == user_id)
.filter(models.PointOfInterest.id.in_(existing_poi_ids))
)
pois = (await db.scalars(poi_query)).all()
poi_map = {poi.id: poi for poi in pois}
order = 0
for item in track:
poi_object = None
if item.id:
poi_object = poi_map.get(item.id)
if not poi_object:
poi_object = await models.PointOfInterest.create(db, data=dict(created_by_id=user_id, name=item.name))
await db.flush()
await models.FlightTrack.create(
db,
data={
"flight_id": flight.id,
"point_of_interest_id": poi_object.id,
"order": order
}
)
order += 1
async def handle_aircraft_save(db: AsyncSession, user_id: int, aircraft: ComboboxInput):
if aircraft.id:
return aircraft.id
else:
obj = await models.Aircraft.create(db, {
"call_sign": aircraft.name,
"description": "",
"model": "",
"manufacturer": "",
"created_by_id": user_id
})
await db.flush()
return obj.id
async def handle_copilot_edit(db: AsyncSession, copilot: ComboboxInput, user_id: int) -> int:
return await handle_combobox_save(db, models.Copilot, copilot, user_id)
async def handle_combobox_save(
db: AsyncSession, model: Type[models.BaseModel],
input: ComboboxInput,
user_id: int,
name_column: str = "name"
):
if input.id:
return input.id
else:
data = {name_column: input.name}
if hasattr(model, "created_by_id"):
data["created_by_id"] = user_id
obj = await model.create(db, data)
await db.flush()
return obj.id
+25 -14
View File
@@ -4,17 +4,23 @@ from sqlalchemy import select, update
from strawberry.file_uploads import Upload from strawberry.file_uploads import Upload
from database import models from database import models
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type, strawberry_sqlalchemy_input from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type, strawberry_sqlalchemy_input
from upload_utils import get_public_url, handle_file_upload, delete_file from upload_utils import get_public_url, handle_file_upload, delete_file, parse_exif_info, generate_thumbnail, file_exists
@strawberry_sqlalchemy_type(models.Photo) @strawberry_sqlalchemy_type(models.Photo)
class Photo: class Photo:
url: str = strawberry.field( def resolve_url(root):
resolver=lambda root: get_public_url(f"photos/{root.flight_id}/{root.filename}") return get_public_url(f"photos/{root.flight_id}/{root.filename}")
)
thumbnail_url: str = strawberry.field( def resolve_thumb_url(root):
resolver=lambda root: get_public_url(f"photos/{root.flight_id}/{root.filename}") # TODO: doplnit thumb! 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=resolve_url)
thumbnail_url: str = strawberry.field(resolver=resolve_thumb_url)
def get_base_query(user_id: int): def get_base_query(user_id: int):
@@ -39,28 +45,33 @@ class PhotoQueries:
@strawberry.type @strawberry.type
class UploadPhotoMutation: class UploadPhotoMutation:
@strawberry_sqlalchemy_input(models.Photo, exclude_fields=["id", "filename", "is_flight_cover"]) @strawberry_sqlalchemy_input(models.Photo, exclude_fields=[
"id", "filename", "is_flight_cover", "exposed_at", "gps_latitude", "gps_longitude", "gps_altitude"
])
class UploadPhotoInput: class UploadPhotoInput:
photo: Upload photo: Upload
@strawberry.mutation @strawberry.mutation
async def upload_photo(self, info, input: UploadPhotoInput) -> Photo: async def upload_photo(self, info, input: UploadPhotoInput) -> Photo:
filename = await handle_file_upload(input.photo, get_photo_basepath(input.flight_id)) path = get_photo_basepath(input.flight_id)
filename = await handle_file_upload(input.photo, path)
info.context.background_tasks.add_task(generate_thumbnail, path=path, filename=filename, size=(300, 200))
# todo: udelat nahled do thumbs slozky exif_info = await parse_exif_info(path, filename)
is_flight_cover = False # TODO: pokud k letu neexistuje zadna fotka, vybrat nahodne jednu a tu nastavit jako cover
created_photo = await models.Photo.create(data={ created_photo = await models.Photo.create(data={
"flight_id": input.flight_id, "flight_id": input.flight_id,
"name": input.name, "name": input.name,
"filename": filename, "filename": filename,
"description": input.description, "description": input.description,
"is_flight_cover": is_flight_cover, "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, "created_by_id": info.context.user_id,
}, db_session=info.context.db) }, db_session=info.context.db)
await info.context.db.flush()
return created_photo return created_photo
+3
View File
@@ -1,5 +1,6 @@
from strawberry.tools import merge_types from strawberry.tools import merge_types
from graphql_schema.entities.aircraft import CreateAircraftMutation, EditAircraftMutation, DeleteAircraftMutation from graphql_schema.entities.aircraft import CreateAircraftMutation, EditAircraftMutation, DeleteAircraftMutation
from graphql_schema.entities.copilot import CreateCopilotMutation, EditCopilotMutation
from graphql_schema.entities.flight import CreateFlightMutation, EditFlightMutation, DeleteFlightMutation from graphql_schema.entities.flight import CreateFlightMutation, EditFlightMutation, DeleteFlightMutation
from graphql_schema.entities.photo import UploadPhotoMutation, DeletePhotoMutation, EditPhotoMutation from graphql_schema.entities.photo import UploadPhotoMutation, DeletePhotoMutation, EditPhotoMutation
from graphql_schema.entities.poi import CreatePointOfInterestMutation, EditPointOfInterestMutation from graphql_schema.entities.poi import CreatePointOfInterestMutation, EditPointOfInterestMutation
@@ -16,4 +17,6 @@ Mutation = merge_types("Mutation", (
DeletePhotoMutation, DeletePhotoMutation,
CreatePointOfInterestMutation, CreatePointOfInterestMutation,
EditPointOfInterestMutation, EditPointOfInterestMutation,
CreateCopilotMutation,
EditCopilotMutation,
)) ))
+3 -1
View File
@@ -1,8 +1,9 @@
import dataclasses import dataclasses
import strawberry import strawberry
from fastapi_jwt import JwtAuthorizationCredentials from fastapi_jwt import JwtAuthorizationCredentials
from fastapi_jwt.jwt import JwtAccessBearer, JwtAccessBearerCookie from fastapi_jwt.jwt import JwtAccessBearerCookie
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from starlette.background import BackgroundTasks
from strawberry.extensions import SchemaExtension from strawberry.extensions import SchemaExtension
from strawberry.fastapi import BaseContext from strawberry.fastapi import BaseContext
from .mutation import Mutation from .mutation import Mutation
@@ -33,6 +34,7 @@ class GraphQLContext(BaseContext):
user_id: int user_id: int
jwt_auth_credentials: JwtAuthorizationCredentials jwt_auth_credentials: JwtAuthorizationCredentials
jwt: JwtAccessBearerCookie jwt: JwtAccessBearerCookie
background_tasks: BackgroundTasks
schema = strawberry.Schema( schema = strawberry.Schema(
+8
View File
@@ -0,0 +1,8 @@
from typing import Optional
import strawberry
@strawberry.input()
class ComboboxInput:
id: Optional[int] = None
name: str
+11 -6
View File
@@ -1,9 +1,11 @@
from datetime import timedelta from datetime import timedelta
from fastapi import FastAPI, APIRouter, Depends, Security, HTTPException from fastapi import FastAPI, APIRouter, Depends, Security, HTTPException
from fastapi_jwt import JwtAuthorizationCredentials, JwtRefreshBearer, JwtAccessBearerCookie, JwtRefreshBearerCookie from fastapi_jwt import JwtAuthorizationCredentials, JwtAccessBearerCookie, JwtRefreshBearerCookie
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from starlette.background import BackgroundTasks
from starlette.middleware.cors import CORSMiddleware from starlette.middleware.cors import CORSMiddleware
from starlette.responses import RedirectResponse, Response from starlette.responses import RedirectResponse, Response
from starlette.staticfiles import StaticFiles
from starlette.status import HTTP_401_UNAUTHORIZED from starlette.status import HTTP_401_UNAUTHORIZED
from strawberry.fastapi import GraphQLRouter from strawberry.fastapi import GraphQLRouter
from config import APP_SECRET_KEY, GRAPHIQL, APP_DEBUG from config import APP_SECRET_KEY, GRAPHIQL, APP_DEBUG
@@ -19,7 +21,7 @@ class App:
access_security = JwtAccessBearerCookie( access_security = JwtAccessBearerCookie(
secret_key=APP_SECRET_KEY, secret_key=APP_SECRET_KEY,
auto_error=False, auto_error=False,
access_expires_delta=timedelta(seconds=30) access_expires_delta=timedelta(minutes=20)
) )
refresh_security = JwtRefreshBearerCookie( refresh_security = JwtRefreshBearerCookie(
secret_key=APP_SECRET_KEY, secret_key=APP_SECRET_KEY,
@@ -41,6 +43,7 @@ class App:
@staticmethod @staticmethod
def setup_middleware(app: FastAPI): def setup_middleware(app: FastAPI):
print("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=["http://localhost:9001"], allow_origins=["http://localhost:9001"],
@@ -62,7 +65,7 @@ class App:
def setup_graphql_context( def setup_graphql_context(
credentials: JwtAuthorizationCredentials = Security(self.access_security), credentials: JwtAuthorizationCredentials = Security(self.access_security),
db: AsyncSession = Depends(db_session), db: AsyncSession = Depends(db_session)
): ):
if not credentials: if not credentials:
raise HTTPException(status_code=HTTP_401_UNAUTHORIZED) raise HTTPException(status_code=HTTP_401_UNAUTHORIZED)
@@ -71,7 +74,8 @@ class App:
jwt_auth_credentials=credentials, jwt_auth_credentials=credentials,
user_id=credentials['id'], user_id=credentials['id'],
db=db, db=db,
jwt=self.access_security jwt=self.access_security,
background_tasks=Depends(BackgroundTasks)
) )
graphql_app = GraphQLRouter( graphql_app = GraphQLRouter(
@@ -96,11 +100,12 @@ class App:
resp: Response, resp: Response,
credentials: JwtAuthorizationCredentials = Security(self.refresh_security) credentials: JwtAuthorizationCredentials = Security(self.refresh_security)
): ):
return await RefreshEndpoint(self.access_security, self.refresh_security).on_post(resp, credentials) return await RefreshEndpoint(self.access_security, self.refresh_security).on_post(resp, credentials)
self.setup_graphql_endpoint(app) self.setup_graphql_endpoint(app)
app.mount("/uploads", StaticFiles(directory="/app/uploads"), name="uploads")
# public endpoints # public endpoints
@self.api_router.post("/login") @self.api_router.post("/login")
+46 -1
View File
@@ -1,10 +1,14 @@
import os import os
import uuid import uuid
from typing import Optional from datetime import datetime
from typing import Optional, Tuple
import exif
from PIL import Image, UnidentifiedImageError
from strawberry.file_uploads import Upload from strawberry.file_uploads import Upload
def get_public_url(filename: Optional[str]): def get_public_url(filename: Optional[str]):
# TODO: pouzit staticfiles z /uploads - port na API, nginx nebude potreba (pro dev)
return f"http://localhost:8999/{filename}" if filename else None return f"http://localhost:8999/{filename}" if filename else None
@@ -13,6 +17,10 @@ def check_directories(path: str):
os.makedirs(path) os.makedirs(path)
def file_exists(path: str):
return os.path.isfile(path)
async def handle_file_upload(file: Upload, path: str): async def handle_file_upload(file: Upload, path: str):
check_directories(path) check_directories(path)
@@ -26,5 +34,42 @@ async def handle_file_upload(file: Upload, path: str):
return filename return filename
def gps_to_decimal(input: Tuple[float, float, float]) -> float:
d, m, s = input
return d + (m / 60.0) + (s / 3600.0)
async def parse_exif_info(path: str, filename: str) -> dict:
with open(f"{path}/{filename}", "rb") as f:
img = exif.Image(f)
if not img.has_exif:
return {}
exif_info = img.get_all()
for datetime_field in ("datetime", "datetime_original", "datetime_digitized"):
if exif_info.get(datetime_field):
print(exif_info[datetime_field])
exif_info[datetime_field] = datetime.strptime(exif_info[datetime_field], "%Y:%m:%d %H:%M:%S")
if exif_info.get("gps_latitude"):
exif_info["gps_latitude"] = gps_to_decimal(exif_info["gps_latitude"])
if exif_info.get("gps_longitude"):
exif_info["gps_longitude"] = gps_to_decimal(exif_info["gps_longitude"])
return exif_info
async def generate_thumbnail(path: str, filename: str, size: Tuple[int, int]):
try:
image = Image.open(f"{path}/{filename}")
image.thumbnail(size)
check_directories(f"{path}/thumbs/")
image.save(f"{path}/thumbs/{filename}")
except UnidentifiedImageError:
pass
def delete_file(path: str, silent: bool = False): def delete_file(path: str, silent: bool = False):
os.remove(path) os.remove(path)