Stahovani pocasi, editace kopilota, nahravani GPX k letu
This commit is contained in:
+40
@@ -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 ###
|
||||||
@@ -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
@@ -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"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Vendored
+58
@@ -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
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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())
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
from typing import Optional
|
||||||
|
import strawberry
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry.input()
|
||||||
|
class ComboboxInput:
|
||||||
|
id: Optional[int] = None
|
||||||
|
name: str
|
||||||
+11
-6
@@ -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
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user