90 changed files with 1703 additions and 1415 deletions
+2 -1
View File
@@ -2,5 +2,6 @@
max-line-length = 120
exclude =
venv
.venv
__pycache__
alembic
alembic
+2 -2
View File
@@ -7,7 +7,7 @@ COPY ./src/emails/templates/mjml /app/templates
RUN mjml /app/templates/* -o /app/templates/html/
FROM python:3.11-alpine AS app
FROM python:3.13-alpine AS app
# protoze xfs uzivatel ma id=33
# RUN deluser xfs
@@ -36,4 +36,4 @@ COPY . /app
WORKDIR /app
CMD ["uvicorn", "src.asgi:app", "--reload", "--reload-dir=/app", "--port=8000", "--host=0.0.0.0", "--log-level=debug"]
CMD [ "gunicorn", "src.asgi:app", "--config=/app/config/gunicorn.py" ]
@@ -0,0 +1,35 @@
"""prejmenovani date_time u WeatherInfo
Revision ID: db036549f318
Revises: e3620deb41b6
Create Date: 2025-09-16 09:26:04.082396
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = 'db036549f318'
down_revision = 'e3620deb41b6'
branch_labels = None
depends_on = None
def upgrade() -> None:
op.create_unique_constraint(None, 'airport', ['openaip_id'])
op.create_index(op.f('ix_flight_turn_point_order'), 'flight_turn_point', ['order'], unique=False)
op.alter_column(
"weather_info", "datetime", new_column_name="date_time", existing_type=sa.DateTime, nullable=False
)
def downgrade() -> None:
op.alter_column(
"weather_info", "date_time", new_column_name="datetime",
existing_type=sa.DateTime, nullable=False
)
op.drop_index(op.f('ix_flight_turn_point_order'), table_name='flight_turn_point')
op.drop_constraint(None, 'airport', type_='unique')
# ### end Alembic commands ###
+18
View File
@@ -0,0 +1,18 @@
import os
# https://docs.gunicorn.org/en/stable/settings.html#settings
workers = 1 if os.environ.get("APP_ENV") == "development" else os.cpu_count()
bind = "0.0.0.0:8000"
worker_class = "uvicorn.workers.UvicornWorker"
worker_connections = 1000 # default
threads = 1 # default
# restart workeru
max_requests = 500000
max_requests_jitter = 10000
timeout = 30 # default
graceful_timeout = 30 # default
keepalive = 600 # s klientem udrzuje nginx
+1 -1
View File
@@ -21,4 +21,4 @@ ssh -t michal@kvacek.cz << EOF
docker-compose down --remove-orphans
docker-compose up -d
docker-compose exec -T api alembic upgrade head
EOF
EOF
-1
View File
@@ -3,7 +3,6 @@ version: "3.3"
services:
api:
image: "docker.kvacek.cz/poletani/api:latest"
command: "uvicorn src.asgi:app --port=8000 --host=0.0.0.0 --workers 4"
network_mode: "host"
restart: "always"
env_file:
+3
View File
@@ -5,6 +5,9 @@ services:
- 3306:3306
env_file:
- .env
environment:
MARIADB_ROOT_PASSWORD: "rootroot"
volumes:
# - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
- db-data:/var/lib/mysql:rw
+19 -13
View File
@@ -1,16 +1,22 @@
fastapi==0.110.0
fastapi-jwt==0.2.0
fastapi-mail==1.4.1
strawberry-graphql[fastapi]==0.217.1
uvicorn==0.26.0
sqlalchemy[asyncio] == 2.0.25
fastapi==0.115.7
fastapi-jwt[authlib]==0.3.0
bcrypt==4.2.1
fastapi-mail==1.4.2
strawberry-graphql[fastapi]==0.258.0
gunicorn == 23.0.0
uvicorn[standard] == 0.34.0
sqlalchemy[asyncio] == 2.0.54
aiomysql==0.2.0
alembic==1.13.1
alembic==1.14.1
passlib==1.7.4
pydantic==2.5.3
pydantic==2.10.6
sentry-sdk[fastapi]
pillow==10.2.0
exif==1.6.0
aiocache==0.12.2
aiohttp==3.9.3
lxml==5.1.0
pillow==11.1.0
exif==1.6.1
aiocache==0.12.3
aiohttp==3.11.11
lxml==5.3.0
colorlog
+7 -7
View File
@@ -8,13 +8,13 @@ from external.elevation import elevation_api, update_track_points_elevation
@retryable
async def add_terrain_elevation_to_flight(flight_id: int):
async with get_session() as db:
track_points = (
await db.scalars(
select(models.TrackPoint)
.join(models.TrackPoint.track)
.join(models.Track.flight)
.filter(models.Flight.id == flight_id))
).all()
track_points = (await db.scalars(
select(models.TrackPoint)
.join(models.TrackPoint.track)
.join(models.Track.flight)
.filter(models.Flight.id == flight_id)
.filter(models.TrackPoint.terrain_elevation.is_(None))
)).all()
await update_track_points_elevation(db, track_points)
+24
View File
@@ -1,4 +1,8 @@
from PIL import Image, ImageDraw, ImageOps
from sqlalchemy import select
from database import models
from database.transaction import get_session
async def add_circular_avatar(base_image_path, avatar_image_path, output_image_path):
@@ -56,3 +60,23 @@ async def add_circular_avatar(base_image_path, avatar_image_path, output_image_p
# Save the result
base_image.save(output_image_path)
async def generate_flight_title_photo(flight_id: int, photo_id: int):
async with get_session() as db:
photo = await models.Photo.get_one(db, id=photo_id)
pilot = (await db.scalars(
select(models.User)
.select_from(models.Flight)
.join(models.Flight.created_by)
.filter(models.Flight.id == flight_id)
)).one()
photo_filename, photo_ext = photo.filename, photo.filename_extension
pilot_id, avatar_filename = pilot.id, pilot.avatar_image_filename
await add_circular_avatar(
base_image_path=f'/app/uploads/photos/{flight_id}/{photo_filename}.{photo_ext}',
avatar_image_path=f'/app/uploads/profile/{pilot_id}/{avatar_filename}',
output_image_path=f'/app/uploads/photos/{flight_id}/title_photo.jpg',
)
+19 -10
View File
@@ -1,12 +1,12 @@
from datetime import datetime
from typing import Literal
from typing import Literal, Any
from database import models
from database.transaction import get_session
from external.weather import weather_api
from logger import log
async def get_weather(date_time: datetime, gps: tuple[float, float]):
async def get_weather(date_time: datetime | str, gps: tuple[float, float]) -> dict[str, Any] | None:
try:
weather = await weather_api.get_weather_for_hour(date_time.astimezone(), gps=gps)
except Exception as e:
@@ -14,7 +14,7 @@ async def get_weather(date_time: datetime, gps: tuple[float, float]):
return None
return {
"datetime": weather['datetime'],
"date_time": date_time, # nemuzu pouzit weather['datetime'], protoze to mi vraci jen cele hodiny
"qnh": weather['pressure_msl'],
"temperature_surface": weather['temperature_2m'],
"dewpoint_surface": weather['dewpoint_2m'],
@@ -44,18 +44,27 @@ async def download_weather_for_flight(date_time: datetime, flight_id: int, airpo
await models.Flight.update(db, obj=flight, data={f"{type_}_weather_info_id": weather_model.id})
async def download_weather_for_plan(date_time: datetime, flight_plan_id: int, gps: tuple[float, float]):
async def download_weather_for_plan_marker(date_time: datetime, marker_id: int):
async with get_session() as db:
marker = await models.FlightPlanMarker.get_one(db, id=marker_id)
if not marker.gps_latitude or not marker.gps_longitude:
return
gps = (marker.gps_latitude, marker.gps_longitude)
marker_weather_info_id = marker.weather_info_id
data = await get_weather(date_time, gps)
if not data:
return
async with get_session() as db:
plan = await models.FlightPlan.get_one(db, id=flight_plan_id)
if plan.weather_info_id:
await models.WeatherInfo.update(db, id=plan.weather_info.id, data=data)
if marker_weather_info_id:
await models.WeatherInfo.update(db, id=marker_weather_info_id, data=data)
else:
weather_model = await models.WeatherInfo.create(db, data=data)
await models.FlightPlan.update(
await models.FlightPlanMarker.update(
db,
obj=plan,
id=marker_id,
data={"weather_info_id": weather_model.id}
)
-480
View File
@@ -1,480 +0,0 @@
from __future__ import annotations
import datetime
from typing import Set, List
from sqlalchemy import String, DateTime, ForeignKey, Text, Integer, func, Table, Column, Boolean, select, Float, Enum
from sqlalchemy.dialects.mysql import JSON
from sqlalchemy.orm import Mapped, relationship, as_declarative, mapped_column
from sqlalchemy.ext.asyncio import AsyncSession
@as_declarative()
class BaseModel:
excluded_columns_in_dict = ("deleted",)
@classmethod
def _get_column_names(cls):
return [col.name for col in cls.__table__.columns]
def as_dict(self):
return {c: getattr(self, c) for c in self._get_column_names() if c not in self.excluded_columns_in_dict}
@classmethod
async def get_one(cls, db_session: AsyncSession, **kwargs):
return (await db_session.scalars(select(cls).filter_by(**kwargs))).one()
@classmethod
async def create(cls, db_session: AsyncSession, data: dict):
model = cls(**{col: data[col] for col in cls._get_column_names() if col in data})
db_session.add(model)
await db_session.flush()
return model
@classmethod
async def update(cls, db_session: AsyncSession, data: dict, obj: BaseModel = None, id: int = None):
if not obj and not id:
raise ValueError("Provide either obj or id!")
if not obj:
obj = await cls.get_one(db_session, id=id)
for key, value in data.items():
if key in cls._get_column_names() and getattr(obj, key) != value:
setattr(obj, key, value)
return obj
user_is_in_organization = Table(
"user_is_in_organization",
BaseModel.metadata,
Column("user_id", Integer, ForeignKey("user.id"), primary_key=True),
Column("organization_id", Integer, ForeignKey("organization.id"), primary_key=True)
)
flight_has_copilot = Table(
"flight_has_copilot",
BaseModel.metadata,
Column("flight_id", ForeignKey("flight.id"), primary_key=True),
Column("copilot_id", ForeignKey("copilot.id"), primary_key=True),
Column("token", String(64), unique=True)
)
flight_plan_has_copilot = Table(
"flight_plan_has_copilot",
BaseModel.metadata,
Column("flight_plan_id", ForeignKey("flight_plan.id"), primary_key=True),
Column("copilot_id", ForeignKey("copilot.id"), primary_key=True),
Column("token", String(64), unique=True)
)
copilot_has_photo = Table(
"copilot_has_photo",
BaseModel.metadata,
Column("copilot_id", ForeignKey("copilot.id"), primary_key=True),
Column("photo_id", ForeignKey("photo.id"), primary_key=True),
)
airport_has_reporting_point = Table(
"airport_has_reporting_point",
BaseModel.metadata,
Column("airport_id", ForeignKey("airport.id"), primary_key=True),
Column("reporting_point_id", ForeignKey("reporting_point.id"), primary_key=True),
)
class Airspace(BaseModel):
__tablename__ = "airspace"
id: Mapped[int] = mapped_column(primary_key=True)
country: Mapped[str] = mapped_column(String(8), nullable=False, index=True)
name: Mapped[str] = mapped_column(String(128), nullable=False, unique=True)
type: Mapped[str] = mapped_column(String(8), nullable=False, index=True)
lower_limit: Mapped[str] = mapped_column(String(32), nullable=False)
lower_alt_unit: Mapped[str] = mapped_column(String(8), nullable=False)
upper_limit: Mapped[str] = mapped_column(String(32), nullable=False)
upper_alt_unit: Mapped[str] = mapped_column(String(8), nullable=False)
bounds: Mapped[list[tuple[float, float]]] = mapped_column(JSON())
class FlightPlan(BaseModel):
__tablename__ = "flight_plan"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(256), nullable=False)
is_default_name: Mapped[bool] = mapped_column(Boolean, server_default='0')
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
is_editable_by_copilot: Mapped[bool] = mapped_column(Boolean, server_default='0')
track_id: Mapped[int] = mapped_column(Integer, ForeignKey('track.id'), nullable=True)
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
planned_speed: Mapped[int] = mapped_column(Integer, nullable=False, default=150)
planned_takeoff_datetime: Mapped[datetime] = mapped_column(DateTime, nullable=True)
aircraft_id: Mapped[int] = mapped_column(Integer, ForeignKey("aircraft.id"), nullable=True)
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
copilots: Mapped[List['Copilot']] = relationship(secondary=flight_plan_has_copilot)
aircraft: Mapped['Aircraft'] = relationship()
markers: Mapped[List['FlightPlanMarker']] = relationship()
created_by: Mapped['User'] = relationship()
class Track(BaseModel):
__tablename__ = "track"
id: Mapped[int] = mapped_column(primary_key=True)
bounds: Mapped[list[tuple[float, float]]] = mapped_column(JSON())
min_speed: Mapped[float] = mapped_column(Float, nullable=True)
max_speed: Mapped[float] = mapped_column(Float, nullable=True)
avg_speed: Mapped[float] = mapped_column(Float, nullable=True)
max_altitude: Mapped[float] = mapped_column(Float, nullable=True)
avg_altitude: Mapped[float] = mapped_column(Float, nullable=True)
total_duration: Mapped[int] = mapped_column(Integer, nullable=True, comment="Total duration in seconds")
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
flight: Mapped['Flight'] = relationship()
# created_by: Mapped['User'] = relationship()
class TrackPoint(BaseModel):
__tablename__ = "track_point"
id: Mapped[int] = mapped_column(primary_key=True)
timestamp: Mapped[datetime] = mapped_column(DateTime, nullable=False, index=True)
track_id: Mapped[id] = mapped_column(Integer, ForeignKey('track.id'))
gps_latitude: Mapped[float] = mapped_column(Float, nullable=False)
gps_longitude: Mapped[float] = mapped_column(Float, nullable=False)
terrain_elevation: Mapped[float] = mapped_column(Float, nullable=True)
speed: Mapped[float] = mapped_column(Float, nullable=True)
altitude: Mapped[float] = mapped_column(Float, nullable=True)
magnetic_variation: Mapped[float] = mapped_column(Float, nullable=True)
track: Mapped['Track'] = relationship()
class FlightPlanMarker(BaseModel):
__tablename__ = "flight_plan_marker"
id: Mapped[int] = mapped_column(primary_key=True)
flight_plan_id: Mapped[int] = mapped_column(Integer, ForeignKey('flight_plan.id'))
airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=True)
point_of_interest_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest.id"), nullable=True)
weather_info_id: Mapped[int] = mapped_column(Integer, ForeignKey('weather_info.id'), nullable=True)
name: Mapped[str] = mapped_column(String(128), nullable=False)
type: Mapped[str] = mapped_column(String(64), nullable=False)
position: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
gps_latitude: Mapped[float] = mapped_column(Float, nullable=False)
gps_longitude: Mapped[float] = mapped_column(Float, nullable=False)
class Airport(BaseModel):
__tablename__ = "airport"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(128), nullable=False)
openaip_id: Mapped[str] = mapped_column(String(32), nullable=True, unique=True)
icao_code: Mapped[str] = mapped_column(String(8), nullable=False)
gps_latitude: Mapped[float] = mapped_column(Float, nullable=True)
gps_longitude: Mapped[float] = mapped_column(Float, nullable=True)
elevation: Mapped[int] = mapped_column(Integer, nullable=True)
airport_type: Mapped[str] = mapped_column(Enum("airport", "ull", "heliport"), nullable=False, server_default='airport') # noqa
use_in_gpx_guess: Mapped[bool] = mapped_column(Boolean, server_default='1')
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 # noqa
runways: Mapped[list['Runway']] = relationship('Runway')
frequencies: Mapped[list['Frequency']] = relationship()
created_by: Mapped['User'] = relationship()
class Runway(BaseModel):
__tablename__ = "runway"
id: Mapped[int] = mapped_column(primary_key=True)
airport_id: Mapped[int] = mapped_column(Integer, ForeignKey('airport.id'))
heading: Mapped[int] = mapped_column(Integer, nullable=False)
designator: Mapped[str] = mapped_column(String(4), nullable=False)
surface: Mapped[str] = mapped_column(Enum("asphalt", "concrete", "grass", "other"), nullable=False, server_default="other") # noqa
length: Mapped[int] = mapped_column(Integer, nullable=True)
width: Mapped[int] = mapped_column(Integer, nullable=True)
class Frequency(BaseModel):
__tablename__ = "frequency"
id: Mapped[int] = mapped_column(primary_key=True)
airport_id: Mapped[int] = mapped_column(Integer, ForeignKey('airport.id'), nullable=True)
airspace_id: Mapped[int] = mapped_column(Integer, ForeignKey('airspace.id'), nullable=True)
frequency: Mapped[str] = mapped_column(String(7), nullable=False)
name: Mapped[str] = mapped_column(String(64), nullable=False)
class ReportingPoint(BaseModel):
__tablename__ = "reporting_point"
id: Mapped[int] = mapped_column(primary_key=True)
openaip_id: Mapped[str] = mapped_column(String(32), nullable=False)
name: Mapped[str] = mapped_column(String(64), nullable=False)
gps_latitude: Mapped[float] = mapped_column(Float, nullable=True)
gps_longitude: Mapped[float] = mapped_column(Float, nullable=True)
class PointOfInterestType(BaseModel):
__tablename__ = "point_of_interest_type"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(128), nullable=False)
icon: Mapped[str] = mapped_column(String(128), nullable=False, server_default='marker')
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
created_by: Mapped['User'] = relationship()
points_of_interest: Mapped[List[PointOfInterest]] = relationship()
class PointOfInterest(BaseModel):
__tablename__ = "point_of_interest"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(128), nullable=False)
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
url_slug: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
title_photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id'), nullable=True)
gps_latitude: Mapped[float] = mapped_column(Float, nullable=True)
gps_longitude: Mapped[float] = mapped_column(Float, nullable=True)
type_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest_type.id"), nullable=True)
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
type: Mapped[PointOfInterestType] = relationship()
created_by: Mapped['User'] = relationship()
title_photo: Mapped['Photo'] = relationship(foreign_keys=[title_photo_id])
class Photo(BaseModel):
__tablename__ = "photo"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
filename: Mapped[str] = mapped_column(String(128), nullable=False)
filename_extension: Mapped[str] = mapped_column(String(4), nullable=False)
cache_key: Mapped[str] = mapped_column(String(128), nullable=True)
width: Mapped[int] = mapped_column(Integer, nullable=False)
height: Mapped[int] = mapped_column(Integer, nullable=False)
exposed_at: Mapped[datetime] = mapped_column(DateTime, nullable=True)
gps_latitude: Mapped[float] = mapped_column(Float, nullable=True)
gps_longitude: Mapped[float] = mapped_column(Float, nullable=True)
gps_altitude: Mapped[float] = mapped_column(Float, nullable=True)
terrain_elevation: Mapped[float] = mapped_column(Float, nullable=True)
aircraft_id: Mapped[int] = mapped_column(Integer, ForeignKey("aircraft.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)
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
created_by_copilot_id: Mapped[int] = mapped_column(Integer, ForeignKey('copilot.id'), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
flight: Mapped['Flight'] = relationship(foreign_keys=[flight_id])
point_of_interest: Mapped['PointOfInterest'] = relationship(foreign_keys=[point_of_interest_id])
adjustment: Mapped['PhotoAdjustment'] = relationship(passive_deletes=True)
created_by: Mapped['User'] = relationship()
aircraft: Mapped['Aircraft'] = relationship(foreign_keys=[aircraft_id])
copilots: Mapped[List['Copilot']] = relationship(secondary=copilot_has_photo)
class PhotoAdjustment(BaseModel):
__tablename__ = "photo_adjustment"
id: Mapped[int] = mapped_column(primary_key=True)
photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id', ondelete='CASCADE'), nullable=False)
rotate: Mapped[float] = mapped_column(Float, nullable=True)
contrast: Mapped[float] = mapped_column(Float, nullable=True)
brightness: Mapped[float] = mapped_column(Float, nullable=True)
saturation: Mapped[float] = mapped_column(Float, nullable=True)
sharpness: Mapped[float] = mapped_column(Float, nullable=True)
crop_left: Mapped[float] = mapped_column(Float, nullable=True)
crop_top: Mapped[float] = mapped_column(Float, nullable=True)
crop_width: Mapped[float] = mapped_column(Float, nullable=True)
crop_height: Mapped[float] = mapped_column(Float, nullable=True)
updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
photo: Mapped['Photo'] = relationship()
class Aircraft(BaseModel):
__tablename__ = "aircraft"
id: Mapped[int] = mapped_column(primary_key=True)
call_sign: Mapped[str] = mapped_column(String(16), nullable=False)
title_photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id'), nullable=True)
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
manufacturer: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
model: Mapped[str] = mapped_column(String(30), nullable=False, server_default="")
seats: Mapped[str] = mapped_column(Integer, nullable=False)
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
organization_id: Mapped[int] = mapped_column(Integer, ForeignKey('organization.id'), nullable=True)
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'), nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
organization: Mapped['Organization'] = relationship()
flights: Mapped[Set['Flight']] = relationship()
created_by: Mapped['User'] = relationship()
class Organization(BaseModel):
__tablename__ = "organization"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(128), nullable=False)
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
users: Mapped[Set['User']] = relationship(secondary=user_is_in_organization)
created_by: Mapped['User'] = relationship()
class FlightTurnPoint(BaseModel):
__tablename__ = "flight_turn_point"
id: Mapped[int] = mapped_column(primary_key=True)
flight_id: Mapped[int] = mapped_column(Integer, ForeignKey("flight.id"), nullable=False)
point_of_interest_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest.id"), nullable=True)
airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=True)
landing_duration: Mapped[int] = mapped_column(Integer, nullable=True)
order: Mapped[int] = mapped_column(Integer, index=True)
flight: Mapped['Flight'] = relationship()
point_of_interest: Mapped['PointOfInterest'] = relationship()
airport: Mapped['Airport'] = 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 Event(BaseModel):
__tablename__ = "event"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(128), nullable=False)
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
url_slug: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
date_from: Mapped[datetime] = mapped_column(DateTime, nullable=True)
date_to: Mapped[datetime] = mapped_column(DateTime, nullable=True)
organization_id: Mapped[int] = mapped_column(Integer, ForeignKey('organization.id'), nullable=True)
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
organization: Mapped['Organization'] = relationship()
created_by: Mapped['User'] = relationship()
class Flight(BaseModel):
__tablename__ = "flight"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
debriefing_notes: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
url_slug: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
event_id: Mapped[int] = mapped_column(Integer, ForeignKey("event.id"), nullable=True)
title_photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id'), nullable=True)
takeoff_airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=True)
landing_airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=True)
takeoff_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_pic: Mapped[int] = mapped_column(Integer, nullable=True)
flight_plan_id: Mapped[int] = mapped_column(Integer, ForeignKey('flight_plan.id'), nullable=True)
track_id: Mapped[int] = mapped_column(Integer, ForeignKey('track.id'), nullable=True)
gpx_track_filename: Mapped[str] = mapped_column(String(128), nullable=True) # deprecated
has_terrain_elevation: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) # deprecated
aircraft_id: Mapped[int] = mapped_column(Integer, ForeignKey('aircraft.id'))
takeoff_weather_info_id: Mapped[int] = mapped_column(Integer, ForeignKey('weather_info.id'), nullable=True)
landing_weather_info_id: Mapped[int] = mapped_column(Integer, ForeignKey('weather_info.id'), nullable=True)
landings: Mapped[int] = mapped_column(Integer, default=1)
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
takeoff_airport: Mapped['Airport'] = relationship(foreign_keys=[takeoff_airport_id])
landing_airport: Mapped['Airport'] = relationship(foreign_keys=[landing_airport_id])
weather_info_landing: Mapped[WeatherInfo] = relationship(foreign_keys=[landing_weather_info_id])
weather_info_takeoff: Mapped[WeatherInfo] = relationship(foreign_keys=[takeoff_weather_info_id])
turn_points: Mapped[list['FlightTurnPoint']] = relationship()
event: Mapped['Event'] = relationship()
copilots: Mapped[List['Copilot']] = relationship(secondary=flight_has_copilot)
aircraft: Mapped['Aircraft'] = relationship()
photos: Mapped[List['Photo']] = relationship(foreign_keys=[Photo.flight_id])
created_by: Mapped['User'] = relationship()
title_photo: Mapped['Photo'] = relationship(foreign_keys=[title_photo_id])
track: Mapped['Track'] = relationship()
class Copilot(BaseModel):
__tablename__ = "copilot"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(128), nullable=False)
url_slug: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
title_photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id'), nullable=True)
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
flights: Mapped[Set['Flight']] = relationship(secondary=flight_has_copilot)
flight_plans: Mapped[Set['FlightPlan']] = relationship(secondary=flight_plan_has_copilot)
created_by: Mapped['User'] = relationship()
photos: Mapped[List['Photo']] = relationship(secondary=copilot_has_photo)
title_photo: Mapped['Photo'] = relationship(foreign_keys=[title_photo_id])
class User(BaseModel):
__tablename__ = "user"
excluded_columns_in_dict = ('password_hashed',)
id: Mapped[int] = mapped_column(primary_key=True)
email: Mapped[str] = mapped_column(String(128), nullable=False, unique=True)
name: Mapped[str] = mapped_column(String(128), nullable=False)
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
public_username: Mapped[str] = mapped_column(String(128), nullable=True, unique=True)
avatar_image_filename: Mapped[str] = mapped_column(String(128), nullable=True)
title_image_filename: Mapped[str] = mapped_column(String(128), nullable=True)
password_hashed: Mapped[str] = mapped_column(String(60), nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
flights: Mapped[Set['Flight']] = relationship()
organizations: Mapped[Set['Organization']] = relationship(secondary=user_is_in_organization)
class PasswordResetToken(BaseModel):
__tablename__ = "password_reset_token"
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
token: Mapped[str] = mapped_column(String(64), nullable=False)
valid_until: Mapped[datetime] = mapped_column(DateTime, nullable=True)
user: Mapped['User'] = relationship()
+45
View File
@@ -0,0 +1,45 @@
from database.models.airport import Airport, Runway, ReportingPoint, airport_has_reporting_point, Frequency
from database.models.aircraft import Aircraft
from database.models.airspace import Airspace
from database.models.base_model import BaseModel
from database.models.copilot import Copilot, copilot_has_photo
from database.models.event import Event
from database.models.flight import Flight, FlightTurnPoint, Track, TrackPoint, flight_has_copilot
from database.models.flight_plan import FlightPlan, flight_plan_has_copilot, FlightPlanMarker
from database.models.orgnization import Organization
from database.models.password_reset_token import PasswordResetToken
from database.models.photo import PhotoAdjustment, Photo
from database.models.point_of_interest import PointOfInterest, PointOfInterestType
from database.models.user import User, user_is_in_organization
from database.models.weather_info import WeatherInfo
__all__ = [
"Aircraft",
"Airport",
"Runway",
"ReportingPoint",
"Airspace",
"BaseModel",
"Copilot",
"Event",
"Flight",
"FlightTurnPoint",
"FlightPlan",
"FlightPlanMarker",
"Frequency",
"Track",
"TrackPoint",
"Organization",
"PasswordResetToken",
"Photo",
"PhotoAdjustment",
"PointOfInterest",
"PointOfInterestType",
"WeatherInfo",
"User",
"airport_has_reporting_point",
"flight_has_copilot",
"flight_plan_has_copilot",
"user_is_in_organization",
"copilot_has_photo",
]
+25
View File
@@ -0,0 +1,25 @@
from datetime import datetime
from sqlalchemy import String, Boolean, Table, ForeignKey, Column, Integer, Text, DateTime, func, Float
from sqlalchemy.orm import mapped_column, Mapped, relationship
from database.models.base_model import BaseModel
class Aircraft(BaseModel):
__tablename__ = "aircraft"
id: Mapped[int] = mapped_column(primary_key=True)
call_sign: Mapped[str] = mapped_column(String(16), nullable=False)
title_photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id'), nullable=True)
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
manufacturer: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
model: Mapped[str] = mapped_column(String(30), nullable=False, server_default="")
seats: Mapped[str] = mapped_column(Integer, nullable=False)
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
organization_id: Mapped[int] = mapped_column(Integer, ForeignKey('organization.id'), nullable=True)
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'), nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
organization: Mapped['Organization'] = relationship()
flights: Mapped[set['Flight']] = relationship()
created_by: Mapped['User'] = relationship()
+67
View File
@@ -0,0 +1,67 @@
from datetime import datetime
from sqlalchemy import String, Boolean, Table, ForeignKey, Column, Integer, Text, DateTime, func, Float, Enum
from sqlalchemy.orm import mapped_column, Mapped, relationship
from database.models.base_model import BaseModel
airport_has_reporting_point = Table(
"airport_has_reporting_point",
BaseModel.metadata,
Column("airport_id", ForeignKey("airport.id"), primary_key=True),
Column("reporting_point_id", ForeignKey("reporting_point.id"), primary_key=True),
)
class Airport(BaseModel):
__tablename__ = "airport"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(128), nullable=False)
openaip_id: Mapped[str] = mapped_column(String(32), nullable=True, unique=True)
icao_code: Mapped[str] = mapped_column(String(8), nullable=False)
gps_latitude: Mapped[float] = mapped_column(Float, nullable=True)
gps_longitude: Mapped[float] = mapped_column(Float, nullable=True)
elevation: Mapped[int] = mapped_column(Integer, nullable=True)
airport_type: Mapped[str] = mapped_column(
Enum("airport", "ull", "heliport"),
nullable=False,
server_default='airport'
)
use_in_gpx_guess: Mapped[bool] = mapped_column(Boolean, server_default='1')
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 # noqa
runways: Mapped[list['Runway']] = relationship('Runway')
frequencies: Mapped[list['Frequency']] = relationship()
created_by: Mapped['User'] = relationship()
class Runway(BaseModel):
__tablename__ = "runway"
id: Mapped[int] = mapped_column(primary_key=True)
airport_id: Mapped[int] = mapped_column(Integer, ForeignKey('airport.id'))
heading: Mapped[int] = mapped_column(Integer, nullable=False)
designator: Mapped[str] = mapped_column(String(4), nullable=False)
surface: Mapped[str] = mapped_column(Enum("asphalt", "concrete", "grass", "other"), nullable=False, server_default="other") # noqa
length: Mapped[int] = mapped_column(Integer, nullable=True)
width: Mapped[int] = mapped_column(Integer, nullable=True)
class Frequency(BaseModel):
__tablename__ = "frequency"
id: Mapped[int] = mapped_column(primary_key=True)
airport_id: Mapped[int] = mapped_column(Integer, ForeignKey('airport.id'), nullable=True)
airspace_id: Mapped[int] = mapped_column(Integer, ForeignKey('airspace.id'), nullable=True)
frequency: Mapped[str] = mapped_column(String(7), nullable=False)
name: Mapped[str] = mapped_column(String(64), nullable=False)
class ReportingPoint(BaseModel):
__tablename__ = "reporting_point"
id: Mapped[int] = mapped_column(primary_key=True)
openaip_id: Mapped[str] = mapped_column(String(32), nullable=False)
name: Mapped[str] = mapped_column(String(64), nullable=False)
gps_latitude: Mapped[float] = mapped_column(Float, nullable=True)
gps_longitude: Mapped[float] = mapped_column(Float, nullable=True)
+17
View File
@@ -0,0 +1,17 @@
from sqlalchemy import String, JSON
from sqlalchemy.orm import mapped_column, Mapped
from database.models.base_model import BaseModel
class Airspace(BaseModel):
__tablename__ = "airspace"
id: Mapped[int] = mapped_column(primary_key=True)
country: Mapped[str] = mapped_column(String(8), nullable=False, index=True)
name: Mapped[str] = mapped_column(String(128), nullable=False, unique=True)
type: Mapped[str] = mapped_column(String(8), nullable=False, index=True)
lower_limit: Mapped[str] = mapped_column(String(32), nullable=False)
lower_alt_unit: Mapped[str] = mapped_column(String(8), nullable=False)
upper_limit: Mapped[str] = mapped_column(String(32), nullable=False)
upper_alt_unit: Mapped[str] = mapped_column(String(8), nullable=False)
bounds: Mapped[list[tuple[float, float]]] = mapped_column(JSON())
+40
View File
@@ -0,0 +1,40 @@
from __future__ import annotations
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import DeclarativeBase
class BaseModel(DeclarativeBase):
excluded_columns_in_dict = ("deleted",)
@classmethod
def _get_column_names(cls):
return [col.name for col in cls.__table__.columns]
def as_dict(self):
return {c: getattr(self, c) for c in self._get_column_names() if c not in self.excluded_columns_in_dict}
@classmethod
async def get_one(cls, db_session: AsyncSession, **kwargs):
return (await db_session.scalars(select(cls).filter_by(**kwargs))).one()
@classmethod
async def create(cls, db_session: AsyncSession, data: dict):
model = cls(**{col: data[col] for col in cls._get_column_names() if col in data})
db_session.add(model)
await db_session.flush()
return model
@classmethod
async def update(cls, db_session: AsyncSession, data: dict, obj: BaseModel = None, id: int = None):
if not obj and not id:
raise ValueError("Provide either obj or id!")
if not obj:
obj = await cls.get_one(db_session, id=id)
for key, value in data.items():
if key in cls._get_column_names() and getattr(obj, key) != value:
setattr(obj, key, value)
return obj
+32
View File
@@ -0,0 +1,32 @@
from datetime import datetime
from sqlalchemy import String, Boolean, Table, ForeignKey, Column, Integer, DateTime, func
from sqlalchemy.orm import mapped_column, Mapped, relationship
from database.models.base_model import BaseModel
from database.models.flight import flight_has_copilot
from database.models.flight_plan import flight_plan_has_copilot
copilot_has_photo = Table(
"copilot_has_photo",
BaseModel.metadata,
Column("copilot_id", ForeignKey("copilot.id"), primary_key=True),
Column("photo_id", ForeignKey("photo.id"), primary_key=True),
)
class Copilot(BaseModel):
__tablename__ = "copilot"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(128), nullable=False)
url_slug: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
title_photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id'), nullable=True)
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
flights: Mapped[set['Flight']] = relationship(secondary=flight_has_copilot)
flight_plans: Mapped[set['FlightPlan']] = relationship(secondary=flight_plan_has_copilot)
created_by: Mapped['User'] = relationship()
photos: Mapped[list['Photo']] = relationship(secondary=copilot_has_photo)
title_photo: Mapped['Photo'] = relationship(foreign_keys=[title_photo_id])
+23
View File
@@ -0,0 +1,23 @@
from datetime import datetime
from sqlalchemy import String, Boolean, Table, ForeignKey, Column, Integer, Text, DateTime, func, Float
from sqlalchemy.orm import mapped_column, Mapped, relationship
from database.models.base_model import BaseModel
class Event(BaseModel):
__tablename__ = "event"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(128), nullable=False)
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
url_slug: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
date_from: Mapped[datetime] = mapped_column(DateTime, nullable=True)
date_to: Mapped[datetime] = mapped_column(DateTime, nullable=True)
organization_id: Mapped[int] = mapped_column(Integer, ForeignKey('organization.id'), nullable=True)
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
organization: Mapped['Organization'] = relationship()
created_by: Mapped['User'] = relationship()
+103
View File
@@ -0,0 +1,103 @@
from datetime import datetime
from sqlalchemy import String, Boolean, Table, ForeignKey, Column, Integer, Text, DateTime, func, Float, JSON
from sqlalchemy.orm import mapped_column, Mapped, relationship
from database.models.base_model import BaseModel
flight_has_copilot = Table(
"flight_has_copilot",
BaseModel.metadata,
Column("flight_id", ForeignKey("flight.id"), primary_key=True),
Column("copilot_id", ForeignKey("copilot.id"), primary_key=True),
Column("token", String(64), unique=True)
)
class FlightTurnPoint(BaseModel):
__tablename__ = "flight_turn_point"
id: Mapped[int] = mapped_column(primary_key=True)
flight_id: Mapped[int] = mapped_column(Integer, ForeignKey("flight.id"), nullable=False)
point_of_interest_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest.id"), nullable=True)
airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=True)
landing_duration: Mapped[int] = mapped_column(Integer, nullable=True)
order: Mapped[int] = mapped_column(Integer, index=True)
flight: Mapped['Flight'] = relationship()
point_of_interest: Mapped['PointOfInterest'] = relationship()
airport: Mapped['Airport'] = relationship()
class Flight(BaseModel):
__tablename__ = "flight"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
debriefing_notes: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
url_slug: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
event_id: Mapped[int] = mapped_column(Integer, ForeignKey("event.id"), nullable=True)
title_photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id'), nullable=True)
takeoff_airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=True)
landing_airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=True)
takeoff_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_pic: Mapped[int] = mapped_column(Integer, nullable=True)
flight_plan_id: Mapped[int] = mapped_column(Integer, ForeignKey('flight_plan.id'), nullable=True)
track_id: Mapped[int] = mapped_column(Integer, ForeignKey('track.id'), nullable=True)
gpx_track_filename: Mapped[str] = mapped_column(String(128), nullable=True) # deprecated
has_terrain_elevation: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) # deprecated
aircraft_id: Mapped[int] = mapped_column(Integer, ForeignKey('aircraft.id'))
takeoff_weather_info_id: Mapped[int] = mapped_column(Integer, ForeignKey('weather_info.id'), nullable=True)
landing_weather_info_id: Mapped[int] = mapped_column(Integer, ForeignKey('weather_info.id'), nullable=True)
landings: Mapped[int] = mapped_column(Integer, default=1)
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
takeoff_airport: Mapped['Airport'] = relationship(foreign_keys=[takeoff_airport_id])
landing_airport: Mapped['Airport'] = relationship(foreign_keys=[landing_airport_id])
weather_info_landing: Mapped['WeatherInfo'] = relationship(foreign_keys=[landing_weather_info_id])
weather_info_takeoff: Mapped['WeatherInfo'] = relationship(foreign_keys=[takeoff_weather_info_id])
turn_points: Mapped[list['FlightTurnPoint']] = relationship()
event: Mapped['Event'] = relationship()
copilots: Mapped[list['Copilot']] = relationship(secondary=flight_has_copilot)
aircraft: Mapped['Aircraft'] = relationship()
created_by: Mapped['User'] = relationship()
title_photo: Mapped['Photo'] = relationship(foreign_keys=[title_photo_id])
track: Mapped['Track'] = relationship()
class Track(BaseModel):
__tablename__ = "track"
id: Mapped[int] = mapped_column(primary_key=True)
bounds: Mapped[list[tuple[float, float]]] = mapped_column(JSON())
min_speed: Mapped[float] = mapped_column(Float, nullable=True)
max_speed: Mapped[float] = mapped_column(Float, nullable=True)
avg_speed: Mapped[float] = mapped_column(Float, nullable=True)
max_altitude: Mapped[float] = mapped_column(Float, nullable=True)
avg_altitude: Mapped[float] = mapped_column(Float, nullable=True)
total_duration: Mapped[int] = mapped_column(Integer, nullable=True, comment="Total duration in seconds")
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
flight: Mapped['Flight'] = relationship()
track_points: Mapped[list['TrackPoint']] = relationship()
class TrackPoint(BaseModel):
__tablename__ = "track_point"
id: Mapped[int] = mapped_column(primary_key=True)
timestamp: Mapped[datetime] = mapped_column(DateTime, nullable=False, index=True)
track_id: Mapped[int] = mapped_column(Integer, ForeignKey('track.id'))
gps_latitude: Mapped[float] = mapped_column(Float, nullable=False)
gps_longitude: Mapped[float] = mapped_column(Float, nullable=False)
terrain_elevation: Mapped[float] = mapped_column(Float, nullable=True)
speed: Mapped[float] = mapped_column(Float, nullable=True)
altitude: Mapped[float] = mapped_column(Float, nullable=True)
magnetic_variation: Mapped[float] = mapped_column(Float, nullable=True)
track: Mapped['Track'] = relationship()
+51
View File
@@ -0,0 +1,51 @@
from datetime import datetime
from sqlalchemy import String, Boolean, Table, ForeignKey, Column, Integer, Text, DateTime, func, Float
from sqlalchemy.orm import mapped_column, Mapped, relationship
from database.models.base_model import BaseModel
flight_plan_has_copilot = Table(
"flight_plan_has_copilot",
BaseModel.metadata,
Column("flight_plan_id", ForeignKey("flight_plan.id"), primary_key=True),
Column("copilot_id", ForeignKey("copilot.id"), primary_key=True),
Column("token", String(64), unique=True)
)
class FlightPlan(BaseModel):
__tablename__ = "flight_plan"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(256), nullable=False)
is_default_name: Mapped[bool] = mapped_column(Boolean, server_default='0')
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
is_editable_by_copilot: Mapped[bool] = mapped_column(Boolean, server_default='0')
track_id: Mapped[int] = mapped_column(Integer, ForeignKey('track.id'), nullable=True)
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
planned_speed: Mapped[int] = mapped_column(Integer, nullable=False, default=150)
planned_takeoff_datetime: Mapped[datetime] = mapped_column(DateTime, nullable=True)
aircraft_id: Mapped[int] = mapped_column(Integer, ForeignKey("aircraft.id"), nullable=True)
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
copilots: Mapped[list['Copilot']] = relationship(secondary=flight_plan_has_copilot)
aircraft: Mapped['Aircraft'] = relationship()
markers: Mapped[list['FlightPlanMarker']] = relationship()
created_by: Mapped['User'] = relationship()
class FlightPlanMarker(BaseModel):
__tablename__ = "flight_plan_marker"
id: Mapped[int] = mapped_column(primary_key=True)
flight_plan_id: Mapped[int] = mapped_column(Integer, ForeignKey('flight_plan.id'))
airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=True)
point_of_interest_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest.id"), nullable=True)
weather_info_id: Mapped[int] = mapped_column(Integer, ForeignKey('weather_info.id'), nullable=True)
name: Mapped[str] = mapped_column(String(128), nullable=False)
type: Mapped[str] = mapped_column(String(64), nullable=False)
position: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
gps_latitude: Mapped[float] = mapped_column(Float, nullable=False)
gps_longitude: Mapped[float] = mapped_column(Float, nullable=False)
+19
View File
@@ -0,0 +1,19 @@
from datetime import datetime
from sqlalchemy import String, Boolean, Table, ForeignKey, Column, Integer, Text, DateTime, func, Float
from sqlalchemy.orm import mapped_column, Mapped, relationship
from database.models.base_model import BaseModel
from database.models.user import user_is_in_organization
class Organization(BaseModel):
__tablename__ = "organization"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(128), nullable=False)
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
users: Mapped[set['User']] = relationship(secondary=user_is_in_organization)
created_by: Mapped['User'] = relationship()
@@ -0,0 +1,15 @@
from datetime import datetime
from sqlalchemy import String, ForeignKey, Integer, DateTime
from sqlalchemy.orm import mapped_column, Mapped, relationship
from database.models.base_model import BaseModel
class PasswordResetToken(BaseModel):
__tablename__ = "password_reset_token"
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
token: Mapped[str] = mapped_column(String(64), nullable=False)
valid_until: Mapped[datetime] = mapped_column(DateTime, nullable=True)
user: Mapped['User'] = relationship()
+55
View File
@@ -0,0 +1,55 @@
from datetime import datetime
from sqlalchemy import String, ForeignKey, Integer, Text, DateTime, func, Float
from sqlalchemy.orm import mapped_column, Mapped, relationship
from database.models.base_model import BaseModel
class Photo(BaseModel):
__tablename__ = "photo"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
filename: Mapped[str] = mapped_column(String(128), nullable=False)
filename_extension: Mapped[str] = mapped_column(String(4), nullable=False)
cache_key: Mapped[str] = mapped_column(String(128), nullable=True)
width: Mapped[int] = mapped_column(Integer, nullable=False)
height: Mapped[int] = mapped_column(Integer, nullable=False)
exposed_at: Mapped[datetime] = mapped_column(DateTime, nullable=True)
gps_latitude: Mapped[float] = mapped_column(Float, nullable=True)
gps_longitude: Mapped[float] = mapped_column(Float, nullable=True)
gps_altitude: Mapped[float] = mapped_column(Float, nullable=True)
terrain_elevation: Mapped[float] = mapped_column(Float, nullable=True)
aircraft_id: Mapped[int] = mapped_column(Integer, ForeignKey("aircraft.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)
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
created_by_copilot_id: Mapped[int] = mapped_column(Integer, ForeignKey('copilot.id'), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
flight: Mapped['Flight'] = relationship(foreign_keys=[flight_id])
point_of_interest: Mapped['PointOfInterest'] = relationship(foreign_keys=[point_of_interest_id])
adjustment: Mapped['PhotoAdjustment'] = relationship(passive_deletes=True)
created_by: Mapped['User'] = relationship()
aircraft: Mapped['Aircraft'] = relationship(foreign_keys=[aircraft_id])
# copilots: Mapped[list['Copilot']] = relationship(secondary=copilot_has_photo)
class PhotoAdjustment(BaseModel):
__tablename__ = "photo_adjustment"
id: Mapped[int] = mapped_column(primary_key=True)
photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id', ondelete='CASCADE'), nullable=False)
rotate: Mapped[float] = mapped_column(Float, nullable=True)
contrast: Mapped[float] = mapped_column(Float, nullable=True)
brightness: Mapped[float] = mapped_column(Float, nullable=True)
saturation: Mapped[float] = mapped_column(Float, nullable=True)
sharpness: Mapped[float] = mapped_column(Float, nullable=True)
crop_left: Mapped[float] = mapped_column(Float, nullable=True)
crop_top: Mapped[float] = mapped_column(Float, nullable=True)
crop_width: Mapped[float] = mapped_column(Float, nullable=True)
crop_height: Mapped[float] = mapped_column(Float, nullable=True)
updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
photo: Mapped['Photo'] = relationship()
+40
View File
@@ -0,0 +1,40 @@
from datetime import datetime
from sqlalchemy import String, Boolean, Table, ForeignKey, Column, Integer, Text, DateTime, func, Float
from sqlalchemy.orm import mapped_column, Mapped, relationship
from database.models.base_model import BaseModel
class PointOfInterestType(BaseModel):
__tablename__ = "point_of_interest_type"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(128), nullable=False)
icon: Mapped[str] = mapped_column(String(128), nullable=False, server_default='marker')
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
created_by: Mapped['User'] = relationship()
points_of_interest: Mapped[list['PointOfInterest']] = relationship()
class PointOfInterest(BaseModel):
__tablename__ = "point_of_interest"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(128), nullable=False)
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
url_slug: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
title_photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id'), nullable=True)
gps_latitude: Mapped[float] = mapped_column(Float, nullable=True)
gps_longitude: Mapped[float] = mapped_column(Float, nullable=True)
type_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest_type.id"), nullable=True)
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
type: Mapped[PointOfInterestType] = relationship()
created_by: Mapped['User'] = relationship()
title_photo: Mapped['Photo'] = relationship(foreign_keys=[title_photo_id])
+31
View File
@@ -0,0 +1,31 @@
from __future__ import annotations
import datetime
from typing import Set
from sqlalchemy import String, DateTime, ForeignKey, Text, Integer, func, Table, Column
from sqlalchemy.orm import Mapped, relationship, mapped_column
from database.models.base_model import BaseModel
user_is_in_organization = Table(
"user_is_in_organization",
BaseModel.metadata,
Column("user_id", Integer, ForeignKey("user.id"), primary_key=True),
Column("organization_id", Integer, ForeignKey("organization.id"), primary_key=True)
)
class User(BaseModel):
__tablename__ = "user"
excluded_columns_in_dict = ('password_hashed',)
id: Mapped[int] = mapped_column(primary_key=True)
email: Mapped[str] = mapped_column(String(128), nullable=False, unique=True)
name: Mapped[str] = mapped_column(String(128), nullable=False)
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
public_username: Mapped[str] = mapped_column(String(128), nullable=True, unique=True)
avatar_image_filename: Mapped[str] = mapped_column(String(128), nullable=True)
title_image_filename: Mapped[str] = mapped_column(String(128), nullable=True)
password_hashed: Mapped[str] = mapped_column(String(60), nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
flights: Mapped[Set['Flight']] = relationship()
organizations: Mapped[Set['Organization']] = relationship(secondary=user_is_in_organization)
+19
View File
@@ -0,0 +1,19 @@
from datetime import datetime
from sqlalchemy import Integer, DateTime, Float
from sqlalchemy.orm import mapped_column, Mapped
from database.models.base_model import BaseModel
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)
date_time: Mapped[datetime] = mapped_column(DateTime, nullable=False)
+10 -10
View File
@@ -1,21 +1,21 @@
from typing import Optional, Type
from typing import Type
from sqlalchemy import select, or_, and_
from database import models
class QueryBuilder:
def __init__(self, model: Type[models.BaseModel]):
class QueryBuilder[ModelType: models.BaseModel]:
def __init__(self, model: Type[ModelType]):
self.model = model
def get_simple_query(
self,
extra_select: Optional[list] = None,
created_by_id: Optional[int] = None,
order_by: Optional[list] = None,
only_public: Optional[bool] = False,
only_my: Optional[bool] = False,
include_others_public: Optional[bool] = False,
url_slug: Optional[str] = None,
extra_select: list | None = None,
created_by_id: int | None = None,
order_by: list | None = None,
only_public: bool | None = False,
only_my: bool | None = False,
include_others_public: bool | None = False,
url_slug: str | None = None,
include_deleted: bool = False
):
if not extra_select:
-38
View File
@@ -1,38 +0,0 @@
from functools import wraps
from fastapi import HTTPException
from starlette.status import HTTP_401_UNAUTHORIZED
def raise_unauthorized():
raise HTTPException(HTTP_401_UNAUTHORIZED, "Not authorized")
def allow_public(func):
@wraps(func)
async def decorator(*args, **kwargs):
if 'info' in kwargs:
user_id = kwargs['info'].context.user_id
public = kwargs.get('public')
if not user_id and not public:
raise_unauthorized()
return await func(*args, **kwargs)
return decorator
def authenticated_user_only(raise_when_unauthorized: bool = True, return_value_unauthorized=None):
def wrapper(func):
@wraps(func)
async def decorator(*args, **kwargs):
if 'info' in kwargs:
if not kwargs['info'].context.user_id:
if raise_when_unauthorized:
raise_unauthorized()
else:
return return_value_unauthorized
return await func(*args, **kwargs)
return decorator
return wrapper
-18
View File
@@ -1,18 +0,0 @@
from functools import wraps
from fastapi import HTTPException
from graphql import GraphQLError
from sqlalchemy.exc import NoResultFound
def error_logging(func):
@wraps(func)
async def decorator(*args, **kwargs):
try:
return await func(*args, **kwargs)
except NoResultFound as e:
raise GraphQLError("Not found", original_error=e)
except HTTPException as e:
if e.status_code == 401:
raise GraphQLError("Not authorized", original_error=e)
return decorator
+2 -2
View File
@@ -9,10 +9,10 @@
</mj-text>
<mj-text font-size="16px" color="#333333" font-family="Arial, sans-serif" align="left">
Od: [from]
Od: {{ email_from | e }}
</mj-text>
<mj-text font-size="16px" color="#333333" font-family="Arial, sans-serif" align="left" line-height="1.5">
[content]
{{ content | e }}
</mj-text>
<mj-divider border-color="#469ccc"></mj-divider>
</mj-column>
@@ -13,13 +13,12 @@
<p>
Ahoj,
<br>
Každý občas zapomíná. Heslo je to nejmenší. Klikni na následující tlačítko pro přihlášení do aplikace. A tam si budeš moct změnit heslo.
Každý občas zapomíná. Heslo je to nejmenší. Klikni na následující tlačítko pro reset hesla.
<br>
<strong>Jen admin vyzaduje zadani stareho hesla, takze to nebude tak snadne...</strong>
</p>
</mj-text>
<mj-button background-color="#469ccc" color="white" font-size="18px" align="center" href="">
Přihlásit se pomocí magického odkazu [login_token]
<mj-button background-color="#469ccc" color="white" font-size="18px" align="center" href="https://poletani.cz/zapomenute-heslo/[login_token]">
Obnovit heslo
</mj-button>
<mj-divider border-color="#469ccc"></mj-divider>
</mj-column>
+8 -8
View File
@@ -8,34 +8,34 @@
Vítejte v aplikaci Polétání.cz!
</mj-text>
<mj-text font-size="16px" color="#333333" font-family="Arial, sans-serif" align="left" line-height="1.5">
Ahoj [Jméno],
Ahoj!
<br>
<p>
Vítejte v aplikaci Polétání.cz – místě, kde můžete sdílet své letecké dobrodružství a fotografie s ostatními nadšenci.
Vítej v aplikaci Polétání.cz – místě, kde můžete sdílet své letecké dobrodružství a fotografie s ostatními nadšenci.
</p>
<p>
Co můžete očekávat:
Co můžeš očekávat:
</p>
<ul>
<li>Jednoduché nahrávání a sdílení fotografií z vašich letů.</li>
<li>Možnost připojení k letištním komunitám a sdílení tipů a triků.</li>
<li>Upozornění na nové funkce a akce v aplikaci.</li>
<li>Naplánovat nový výlet pomocí vestavěného plánovače</li>
<li>Objevovat nová místa pro další výlety</li>
</ul>
<p>
Začněte nyní a sdílejte své vzdušné zážitky s celým světem!
Začni nyní a sdílejte své vzdušné zážitky s celým světem!
</p>
<p>
S pozdravem,
<br>
Tým Polétání.cz
Michal Kváček
</p>
</mj-text>
<mj-button background-color="#469ccc" color="white" font-size="16px" align="center" href="[Odkaz na přihlášení]">
<mj-button background-color="#469ccc" color="white" font-size="16px" align="center" href="https://poletani.cz/login">
Přihlásit se do Polétání.cz
</mj-button>
<mj-divider border-color="#469ccc"></mj-divider>
-52
View File
@@ -1,52 +0,0 @@
from datetime import timedelta
from fastapi import FastAPI, Security, Depends, BackgroundTasks, APIRouter
from fastapi_jwt import JwtAuthorizationCredentials
from fastapi_jwt.jwt import JwtAccessBearerCookie
from sqlalchemy import select
from starlette.responses import RedirectResponse
from strawberry.fastapi import GraphQLRouter
from config import GRAPHIQL, APP_DEBUG
from database import async_session, models
from graphql_schema.schema import GraphQLContext, schema
def setup_graphql_endpoint(app: FastAPI, access_security: JwtAccessBearerCookie):
if APP_DEBUG:
debug_router = APIRouter()
@debug_router.get("/graphql/autologin")
async def autologin():
access_token = access_security.create_access_token(subject={"id": 6, "name": "Franta Vomacka"})
response = RedirectResponse(url="/graphql")
access_security.set_access_cookie(response, access_token, expires_delta=timedelta(days=14))
return response
app.include_router(debug_router)
async def setup_graphql_context(credentials: JwtAuthorizationCredentials = Security(access_security)):
user_id = credentials['id'] if credentials else None
organization_ids = set()
if user_id:
async with async_session() as db:
organization_ids = set((await db.scalars(
select(models.user_is_in_organization.c.organization_id)
.filter(models.user_is_in_organization.c.user_id == user_id)
)).all())
return GraphQLContext(
user_id=user_id,
organization_ids=organization_ids,
jwt_auth_credentials=credentials,
jwt=access_security,
background_tasks=Depends(BackgroundTasks)
)
graphql_app = GraphQLRouter(
schema,
graphiql=GRAPHIQL,
debug=APP_DEBUG,
context_getter=setup_graphql_context
)
app.include_router(graphql_app, prefix="/graphql")
+1 -2
View File
@@ -1,7 +1,6 @@
import re
from fastapi import HTTPException, BackgroundTasks
from sqlalchemy import select
from typing import Optional
from pydantic import BaseModel, Field, model_validator
from background_jobs.send_email import send_email
from database.models import User
@@ -12,7 +11,7 @@ from emails.emails import WelcomeEmail
class RegistrationInput(BaseModel):
email: str = Field(..., min_length=4)
name: Optional[str]
name: str | None
password: str
@model_validator(mode="before")
+1 -1
View File
@@ -9,7 +9,7 @@ from utils.list_utils import chunks
@dataclasses.dataclass
class ElevationInfo:
id: Optional[int]
id: int | None
lat: float
lng: float
elevation: float
+1 -1
View File
@@ -53,7 +53,7 @@ class Airspace:
upper_limit: str = None
lower_limit: str = None
center: Optional[Coordinates] = None
radius_nm: Optional[float] = None
radius_nm: float | None = None
bounds: list[Coordinates] = dataclasses.field(default_factory=lambda: [])
+8 -5
View File
@@ -43,10 +43,13 @@ class Weather:
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()
try:
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
resp.raise_for_status()
return await resp.json()
except Exception as e:
print(f"Error in downloading weather: {e}, {type(e)}")
async def get_weather_for_hour(
self, date_time: datetime.datetime, gps: Tuple[float, float]
@@ -56,7 +59,7 @@ class Weather:
# TODO: kontrola timezone!
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")
result_data['date_time'] = datetime.datetime.strptime(data['hourly']['time'][idx], "%Y-%m-%dT%H:%M")
return result_data
+37
View File
@@ -0,0 +1,37 @@
import dataclasses
from datetime import datetime
from fastapi import BackgroundTasks, Depends, Security
from fastapi_jwt import JwtAuthorizationCredentials
from sqlalchemy import select
from strawberry.fastapi import BaseContext
from database import async_session, models
from jwt import access_security
@dataclasses.dataclass
class GraphQLContext(BaseContext):
request_start_time: datetime
user_id: int
organization_ids: set[int]
jwt_auth_credentials: JwtAuthorizationCredentials
background_tasks: BackgroundTasks
async def setup_graphql_context(credentials: JwtAuthorizationCredentials = Security(access_security)):
user_id = credentials['id'] if credentials else None
organization_ids = set()
if user_id:
async with async_session() as db:
organization_ids = set((await db.scalars(
select(models.user_is_in_organization.c.organization_id)
.filter(models.user_is_in_organization.c.user_id == user_id)
)).all())
return GraphQLContext(
user_id=user_id,
organization_ids=organization_ids,
jwt_auth_credentials=credentials,
background_tasks=Depends(BackgroundTasks),
request_start_time=datetime.now()
)
+20 -12
View File
@@ -1,5 +1,5 @@
from collections import defaultdict
from typing import Type, List, Optional
from typing import Type
from logger import log
from database import models, async_session
from database.query_builder import QueryBuilder
@@ -9,7 +9,7 @@ class BaseDataloader:
def __init__(
self,
model: Type[models.BaseModel],
relationship_column, filters: Optional[list] = None
relationship_column, filters: list | None = None
):
super().__init__()
self.model = model
@@ -25,11 +25,15 @@ class BaseDataloader:
class SingleModelByIdDataloader(BaseDataloader):
async def load(self, ids: List[int]):
async def load(self, ids: list[int]):
ids_set = {id_ for id_ in set(ids) if id_ is not None}
if not ids_set:
return [None for _ in ids]
async with async_session() as session:
query = (
self.query_builder.get_simple_query(extra_select=[self.relationship_column], include_deleted=True)
.filter(self.relationship_column.in_(set(ids)))
.filter(self.relationship_column.in_(ids_set))
.filter(*self.filters)
)
@@ -43,10 +47,10 @@ class MultiModelsDataloader(BaseDataloader):
self,
model: Type[models.BaseModel],
relationship_column=None,
filters: Optional[list] = None,
extra_select: Optional[list] = None,
extra_join: Optional[list] = None,
order_by: Optional[list] = None,
filters: list | None = None,
extra_select: list | None = None,
extra_join: list | None = None,
order_by: list | None = None,
):
super().__init__(model, relationship_column, filters)
@@ -62,13 +66,13 @@ class MultiModelsDataloader(BaseDataloader):
order_by = [model.id.desc()] # defaultne radit od nejnovejsich zaznamu
self.order_by = order_by
def get_query(self, ids: list[int]):
def get_query(self, ids: set[int]):
query = (
self.query_builder.get_simple_query(
extra_select=[self.relationship_column] + self.extra_select,
order_by=self.order_by
)
.filter(self.relationship_column.in_(set(ids)))
.filter(self.relationship_column.in_(ids))
.filter(*self.filters)
)
@@ -80,8 +84,12 @@ class MultiModelsDataloader(BaseDataloader):
return query
async def load(self, ids: List[int]):
query = self.get_query(ids)
async def load(self, ids: list[int]) -> list:
ids_set = {id_ for id_ in set(ids) if id_ is not None}
if not ids_set:
return [[] for _ in ids]
query = self.get_query(ids_set)
async with async_session() as db:
data = (await db.execute(query)).all()
@@ -4,7 +4,7 @@ from database import models
from graphql_schema.dataloaders.base import SingleModelByIdDataloader
def create_dataloader(model: Type[models.BaseModel], relationship_column=None, filters: Optional[list] = None):
def create_dataloader(model: Type[models.BaseModel], relationship_column=None, filters: list | None = None):
loader = SingleModelByIdDataloader(model, relationship_column, filters).load
return DataLoader(load_fn=loader, cache=False)
@@ -14,7 +14,7 @@ airport_dataloader = create_dataloader(models.Airport)
aircraft_dataloader = create_dataloader(models.Aircraft)
event_dataloader = create_dataloader(models.Event)
organizations_dataloader = create_dataloader(models.Organization)
airport_weather_info_loader = create_dataloader(models.WeatherInfo)
weather_info_loader = create_dataloader(models.WeatherInfo)
track_dataloader = create_dataloader(models.Track)
poi_dataloader = create_dataloader(models.PointOfInterest)
poi_type_dataloader = create_dataloader(models.PointOfInterestType)
+20 -41
View File
@@ -1,70 +1,49 @@
from typing import Optional
import strawberry
from decorators.endpoints import authenticated_user_only, allow_public
from decorators.error_logging import error_logging
from .helpers.pagination import get_pagination_window, PaginationWindow
from strawberry import Info
from .helpers.filters import get_filters
from .helpers.pagination import PaginationWindow
from .resolvers.aircraft import AircraftMutationResolver, AircraftQueryResolver
from graphql_schema.entities.types.mutation_input import CreateAircraftInput, EditAircraftInput
from graphql_schema.entities.types.types import Aircraft
from ..extensions.field.auth import AllowPublicAccess, AuthenticatedOnly
from ..extensions.field.pagination import OffsetPagination
@strawberry.type
class AircraftQueries:
@strawberry.field()
@error_logging
@authenticated_user_only()
async def aircrafts(root, info, limit: int, offset: int = 0) -> PaginationWindow[Aircraft]:
query = AircraftQueryResolver().get_query(
@strawberry.field(extensions=[OffsetPagination(item_type=Aircraft), AuthenticatedOnly()])
async def aircrafts(root, info: Info, search: str | None = None) -> PaginationWindow[Aircraft]:
return AircraftQueryResolver().get_query(
info.context.user_id,
organization_ids=info.context.organization_ids
organization_ids=info.context.organization_ids,
search=search
)
return await get_pagination_window(
query=query,
item_type=Aircraft,
limit=limit,
offset=offset
)
@strawberry.field()
@error_logging
@allow_public
@strawberry.field(extensions=[AllowPublicAccess()])
async def aircraft(
root, info,
id: Optional[int] = None,
call_sign: Optional[str] = None,
public: Optional[bool] = False
id: int | None = None,
call_sign: str | None = None,
public: bool | None = False
) -> Aircraft:
filter_params = {}
if id:
filter_params['object_id'] = id
if call_sign:
filter_params['call_sign'] = call_sign
return await AircraftQueryResolver().get_one(
user_id=info.context.user_id,
organization_ids=info.context.organization_ids if not public else None,
only_public=public,
**filter_params
**get_filters(object_id=id, call_sign=call_sign)
)
@strawberry.type
class AircraftMutation:
@strawberry.mutation
@error_logging
@authenticated_user_only()
@strawberry.mutation(extensions=[AuthenticatedOnly()])
async def create_aircraft(root, info, input: CreateAircraftInput) -> Aircraft:
return await AircraftMutationResolver().create(info.context, input)
@strawberry.mutation
@error_logging
@authenticated_user_only()
@strawberry.mutation(extensions=[AuthenticatedOnly()])
async def edit_aircraft(root, info, id: int, input: EditAircraftInput) -> Aircraft:
return await AircraftMutationResolver().update(id, user_id=info.context.user_id, data=input)
return await AircraftMutationResolver().update(info.context, id=id, data=input)
@strawberry.mutation
@authenticated_user_only()
@strawberry.mutation(extensions=[AuthenticatedOnly()])
async def delete_aircraft(self, info, id: int) -> Aircraft:
return await AircraftMutationResolver().delete(info.context.user_id, id)
return await AircraftMutationResolver().delete(info.context, id)
+3 -9
View File
@@ -1,23 +1,17 @@
from typing import List
import strawberry
from database import models
from decorators.error_logging import error_logging
from decorators.endpoints import authenticated_user_only
from graphql_schema.entities.resolvers.base import BaseQueryResolver
from graphql_schema.entities.types.types import Airport
from graphql_schema.extensions.field.auth import AuthenticatedOnly
@strawberry.type
class AirportQueries:
@strawberry.field()
@error_logging
# @authenticated_user_only()
async def airports(root, info) -> List[Airport]:
async def airports(root, info) -> list[Airport]:
return await BaseQueryResolver(Airport, models.Airport).get_list(info.context.user_id)
@strawberry.field()
@error_logging
@authenticated_user_only()
@strawberry.field(extensions=[AuthenticatedOnly()])
async def airport(root, info, id: int) -> Airport:
return await BaseQueryResolver(Airport, models.Airport).get_one(
object_id=id,
+2 -5
View File
@@ -1,7 +1,5 @@
from typing import List, Optional
import strawberry
from database import models
from decorators.error_logging import error_logging
from graphql_schema.entities.resolvers.base import BaseQueryResolver
from graphql_schema.entities.types.types import Airspace
@@ -9,10 +7,9 @@ from graphql_schema.entities.types.types import Airspace
@strawberry.type
class AirspaceQueries:
@strawberry.field()
@error_logging
async def airspaces(
root, info, country: Optional[str] = None, types: Optional[list[str]] = None
) -> List[Airspace]:
root, info, country: str | None = None, types: list[str] | None = None
) -> list[Airspace]:
filters = []
if country:
+20 -37
View File
@@ -1,48 +1,35 @@
from typing import List, Optional
import strawberry
from graphql import GraphQLError
from strawberry.types import Info
from database import models
from decorators.error_logging import error_logging
from decorators.endpoints import authenticated_user_only, allow_public
from graphql_schema.entities.helpers.detail import get_detail_filters
from graphql_schema.entities.helpers.filters import get_filters
from graphql_schema.entities.resolvers.base import BaseMutationResolver
from graphql_schema.entities.resolvers.copilot import CopilotQueryResolver
from graphql_schema.entities.types.mutation_input import CreateCopilotInput, EditCopilotInput
from graphql_schema.entities.types.types import Copilot
from graphql_schema.extensions.field.auth import AllowPublicAccess, AuthenticatedOnly
@strawberry.type
class CopilotQueries:
@strawberry.field()
@error_logging
@authenticated_user_only()
async def copilots(root, info: Info) -> List[Copilot]:
@strawberry.field(extensions=[AuthenticatedOnly()])
async def copilots(root, info: Info) -> list[Copilot]:
return await CopilotQueryResolver().get_list(info.context.user_id)
@strawberry.field()
@error_logging
@allow_public
@strawberry.field(extensions=[AllowPublicAccess()])
async def copilot(
root, info: Info,
id: Optional[int] = None,
url_slug: Optional[str] = None,
pilot_username: Optional[str] = None,
upload_token: Optional[str] = None,
upload_flight_slug: Optional[str] = None,
public: Optional[bool] = False
id: int | None = None,
url_slug: str | None = None,
pilot_username: str | None = None,
upload_token: str | None = None,
upload_flight_slug: str | None = None,
public: bool | None = False
) -> Copilot:
filter_params = {}
if id:
filter_params['object_id'] = id
if url_slug is not None:
filter_params['url_slug'] = url_slug
if upload_token and upload_flight_slug:
filter_params['upload_token'] = upload_token
filter_params['upload_flight_slug'] = upload_flight_slug
if pilot_username:
filter_params['pilot_username'] = pilot_username
filter_params = get_filters(
object_id=id, url_slug=url_slug, pilot_username=pilot_username, upload_token=upload_token,
upload_flight_slug=upload_flight_slug,
)
if not filter_params:
raise GraphQLError(f"Invalid identification supplied: {filter_params}")
@@ -55,14 +42,10 @@ class CopilotQueries:
@strawberry.type
class CopilotMutation:
@strawberry.mutation
@error_logging
@authenticated_user_only()
async def create_copilot(root, info, input: CreateCopilotInput) -> Copilot:
@strawberry.mutation(extensions=[AuthenticatedOnly()])
async def create_copilot(root, info: Info, input: CreateCopilotInput) -> Copilot:
return await BaseMutationResolver(Copilot, models.Copilot).create(info.context, data=input)
@strawberry.mutation
@error_logging
@authenticated_user_only()
async def edit_copilot(root, info, id: int, input: EditCopilotInput) -> Copilot:
return await BaseMutationResolver(Copilot, models.Copilot).update(id, input, info.context.user_id)
@strawberry.mutation(extensions=[AuthenticatedOnly()])
async def edit_copilot(root, info: Info, id: int, input: EditCopilotInput) -> Copilot:
return await BaseMutationResolver(Copilot, models.Copilot).update(info.context, id, input)
+16 -33
View File
@@ -1,52 +1,39 @@
from typing import Optional
import strawberry
from database import models
from decorators.endpoints import authenticated_user_only, allow_public
from decorators.error_logging import error_logging
from graphql_schema.entities.helpers.detail import get_detail_filters
from graphql_schema.entities.helpers.pagination import PaginationWindow, get_pagination_window
from graphql_schema.entities.helpers.filters import get_detail_filters
from graphql_schema.entities.helpers.pagination import PaginationWindow
from graphql_schema.entities.resolvers.base import BaseMutationResolver
from graphql_schema.entities.resolvers.event import EventQueryResolver
from graphql_schema.entities.types.mutation_input import CreateEventInput, EditEventInput
from graphql_schema.entities.types.types import Event
from graphql_schema.extensions.field.auth import AllowPublicAccess, AuthenticatedOnly
from graphql_schema.extensions.field.pagination import OffsetPagination
@strawberry.type
class EventQueries:
@strawberry.field()
@error_logging
@allow_public
@strawberry.field(extensions=[OffsetPagination(item_type=Event), AllowPublicAccess()])
async def events(
root,
info,
limit: int,
offset: int = 0,
username: Optional[str] = None,
public: Optional[bool] = False,
username: str | None = None,
public: bool | None = False,
) -> PaginationWindow[Event]:
query = EventQueryResolver().get_query(
return EventQueryResolver().get_query(
user_id=info.context.user_id,
username=username,
order_by=[models.Event.date_from.desc(), models.Event.name.desc()],
only_public=public,
)
return await get_pagination_window(
query=query,
item_type=Event,
limit=limit,
offset=offset
)
@strawberry.field()
@error_logging
@allow_public
@strawberry.field(extensions=[AllowPublicAccess()])
async def event(
root, info,
id: Optional[int] = None,
url_slug: Optional[str] = None,
username: Optional[str] = None,
public: Optional[bool] = False
id: int | None = None,
url_slug: str | None = None,
username: str | None = None,
public: bool | None = False
) -> Event:
filter_params = get_detail_filters(id, url_slug)
if username:
@@ -61,14 +48,10 @@ class EventQueries:
@strawberry.type
class EventMutation:
@strawberry.mutation
@error_logging
@authenticated_user_only()
@strawberry.mutation(extensions=[AuthenticatedOnly()])
async def create_event(root, info, input: CreateEventInput) -> Event:
return await BaseMutationResolver(Event, models.Event).create(info.context, input)
@strawberry.mutation
@error_logging
@authenticated_user_only()
@strawberry.mutation(extensions=[AuthenticatedOnly()])
async def edit_event(root, info, id: int, input: EditEventInput) -> Event:
return await BaseMutationResolver(Event, models.Event).update(id, input, info.context.user_id)
return await BaseMutationResolver(Event, models.Event).update(info.context, id, input)
+24 -42
View File
@@ -1,57 +1,45 @@
from typing import Optional
import strawberry
from decorators.endpoints import authenticated_user_only, allow_public
from decorators.error_logging import error_logging
from graphql_schema.entities.resolvers.flight import FlightMutationResolver, FlightQueryResolver
from graphql_schema.entities.types.mutation_input import EditFlightInput, CreateFlightInput
from graphql_schema.entities.types.types import Flight
from .helpers.detail import get_detail_filters
from .helpers.pagination import PaginationWindow, get_pagination_window
from .helpers.filters import get_detail_filters
from .helpers.pagination import PaginationWindow
from ..extensions.field.auth import AllowPublicAccess, AuthenticatedOnly
from ..extensions.field.pagination import OffsetPagination
@strawberry.type
class FlightQueries:
@strawberry.field()
@error_logging
@allow_public
@strawberry.field(extensions=[OffsetPagination(item_type=Flight), AllowPublicAccess()])
async def flights(
root, info,
limit: int,
offset: int = 0,
username: Optional[str] = None,
event_id: Optional[int] = None,
public: Optional[bool] = False,
copilot_id: Optional[int] = None,
point_of_interest_id: Optional[int] = None,
aircraft_id: Optional[int] = None,
username: str | None = None,
search: str | None = None,
event_id: int | None = None,
public: bool | None = False,
copilot_id: int | None = None,
point_of_interest_id: int | None = None,
aircraft_id: int | None = None,
) -> PaginationWindow[Flight]:
query = FlightQueryResolver().get_query(
return FlightQueryResolver().get_query(
user_id=info.context.user_id,
username=username,
event_id=event_id,
only_public=public,
copilot_id=copilot_id,
aircraft_id=aircraft_id,
point_of_interest_id=point_of_interest_id
point_of_interest_id=point_of_interest_id,
search=search
)
return await get_pagination_window(
query=query,
item_type=Flight,
limit=limit,
offset=offset,
)
@strawberry.field()
@error_logging
@allow_public
@strawberry.field(extensions=[AllowPublicAccess()])
async def flight(
root, info,
id: Optional[int] = None,
url_slug: Optional[str] = None,
username: Optional[str] = None,
public: Optional[bool] = False
id: int | None = None,
url_slug: str | None = None,
username: str | None = None,
public: bool | None = False
) -> Flight:
filter_params = get_detail_filters(id, url_slug)
if username:
@@ -66,20 +54,14 @@ class FlightQueries:
@strawberry.type
class FlightMutation:
@strawberry.mutation
@error_logging
@authenticated_user_only()
@strawberry.mutation(extensions=[AuthenticatedOnly()])
async def create_flight(self, info, input: CreateFlightInput) -> Flight:
return await FlightMutationResolver().create(info.context, input)
@strawberry.mutation
@error_logging
@authenticated_user_only()
@strawberry.mutation(extensions=[AuthenticatedOnly()])
async def edit_flight(self, info, id: int, input: EditFlightInput) -> Flight:
return await FlightMutationResolver().update(info.context, id, input)
@strawberry.mutation
@error_logging
@authenticated_user_only()
@strawberry.mutation(extensions=[AuthenticatedOnly()])
async def delete_flight(self, info, id: int) -> Flight:
return await FlightMutationResolver().delete(info.context.user_id, id)
return await FlightMutationResolver().delete(info.context, id)
+15 -23
View File
@@ -1,31 +1,27 @@
from typing import List, Optional
import strawberry
from strawberry.types import Info
from decorators.endpoints import authenticated_user_only, allow_public
from decorators.error_logging import error_logging
from graphql_schema.entities.helpers.detail import get_detail_filters
from graphql_schema.entities.helpers.filters import get_detail_filters
from graphql_schema.entities.helpers.pagination import PaginationWindow
from graphql_schema.entities.resolvers.flight_plan import FlightPlanMutationResolver, FlightPlanQueryResolver
from graphql_schema.entities.types.mutation_input import CreateFlightPlanInput, EditFlightPlanInput
from graphql_schema.entities.types.types import FlightPlan
from graphql_schema.extensions.field.auth import AuthenticatedOnly, AllowPublicAccess
from graphql_schema.extensions.field.pagination import OffsetPagination
@strawberry.type
class FlightPlanQueries:
@strawberry.field()
@error_logging
@authenticated_user_only()
async def flight_plans(root, info: Info) -> List[FlightPlan]:
return await FlightPlanQueryResolver().get_list(info.context.user_id)
@strawberry.field(extensions=[OffsetPagination(item_type=FlightPlan), AuthenticatedOnly()])
async def flight_plans(root, info: Info, search: str | None = None) -> PaginationWindow[FlightPlan]:
return FlightPlanQueryResolver().get_query(info.context.user_id, search=search)
@strawberry.field()
@error_logging
@allow_public
@strawberry.field(extensions=[AllowPublicAccess()])
async def flight_plan(
root,
info: Info,
id: int,
username: Optional[str] = None,
public: Optional[bool] = False
username: str | None = None,
public: bool | None = False
) -> FlightPlan:
filter_params = get_detail_filters(id)
if username:
@@ -40,14 +36,10 @@ class FlightPlanQueries:
@strawberry.type
class FlightPlanMutation:
@strawberry.mutation
@error_logging
@authenticated_user_only()
async def create_flight_plan(root, info, input: CreateFlightPlanInput) -> FlightPlan:
@strawberry.mutation(extensions=[AuthenticatedOnly()])
async def create_flight_plan(root, info: Info, input: CreateFlightPlanInput) -> FlightPlan:
return await FlightPlanMutationResolver().create(info.context, data=input)
@strawberry.mutation
@error_logging
@authenticated_user_only()
async def edit_flight_plan(root, info, id: int, input: EditFlightPlanInput) -> FlightPlan:
return await FlightPlanMutationResolver().update(id, input, info.context.user_id)
@strawberry.mutation(extensions=[AuthenticatedOnly()])
async def edit_flight_plan(root, info: Info, id: int, input: EditFlightPlanInput) -> FlightPlan:
return await FlightPlanMutationResolver().update(info.context, id, input)
@@ -10,7 +10,7 @@ async def handle_combobox_save(
input: ComboboxInput,
user_id: int,
name_column: str = "name",
extra_data: Optional[dict] = None
extra_data: dict | None = None
) -> int:
if input.id:
return input.id
@@ -1,15 +0,0 @@
from typing import Optional
from graphql import GraphQLError
def get_detail_filters(id: Optional[int] = None, url_slug: Optional[str] = None) -> dict:
filter_params = {}
if id:
filter_params['object_id'] = id
if url_slug is not None:
filter_params['url_slug'] = url_slug
if not filter_params:
raise GraphQLError("You must specifiy either urlSlug or id!")
return filter_params
@@ -0,0 +1,16 @@
from typing import Any
from graphql import GraphQLError
def get_filters(**kwargs) -> dict[str, Any]:
return {k: v for k, v in kwargs.items() if v is not None}
def get_detail_filters(id: int | None = None, url_slug: str | None = None) -> dict:
filter_params = get_filters(object_id=id, url_slug=url_slug)
if not filter_params:
raise GraphQLError("You must specifiy either urlSlug or id!")
return filter_params
@@ -30,10 +30,6 @@ async def get_pagination_window(
cnt_query = query.with_only_columns(func.count())
total_items_count = (await db.scalars(cnt_query)).one()
# if offset != 0 and not 0 <= offset < total_items_count:
# raise Exception(f"offset ({offset}) is out of range " f"(0-{total_items_count - 1})")
async with get_session() as db:
data = (await db.scalars(query.limit(limit).offset(offset))).all()
dataset = [item_type(**i.as_dict()) for i in data]
+11 -22
View File
@@ -3,27 +3,23 @@ import strawberry
from sqlalchemy import delete
from sqlalchemy.dialects.mysql import insert
from sqlalchemy.exc import IntegrityError
from strawberry import Info
from database import models
from decorators.endpoints import authenticated_user_only
from database.transaction import get_session
from decorators.error_logging import error_logging
from graphql_schema.entities.resolvers.base import BaseMutationResolver
from graphql_schema.entities.resolvers.organization import OrganizationQueryResolver
from graphql_schema.entities.types.mutation_input import CreateOrganizationInput, EditOrganizationInput
from graphql_schema.entities.types.types import Organization
from graphql_schema.extensions.field.auth import AuthenticatedOnly
@strawberry.type
class OrganizationQueries:
@strawberry.field()
@error_logging
@authenticated_user_only()
@strawberry.field(extensions=[AuthenticatedOnly()])
async def organizations(root, info) -> List[Organization]:
return await OrganizationQueryResolver().get_list()
@strawberry.field()
@error_logging
@authenticated_user_only()
@strawberry.field(extensions=[AuthenticatedOnly()])
async def organization(root, info, id: int) -> Organization:
return await OrganizationQueryResolver().get_one(object_id=id)
@@ -31,17 +27,14 @@ class OrganizationQueries:
@strawberry.type
class OrganizationMutation:
@strawberry.mutation
@error_logging
@authenticated_user_only()
async def create_organization(root, info, input: CreateOrganizationInput) -> Organization:
@strawberry.mutation(extensions=[AuthenticatedOnly()])
async def create_organization(root, info: Info, input: CreateOrganizationInput) -> Organization:
return await BaseMutationResolver(Organization, models.Organization).create(info.context, data=input)
@strawberry.mutation
@error_logging
@authenticated_user_only()
async def edit_organization(root, info, id: int, input: EditOrganizationInput) -> Organization:
@strawberry.mutation(extensions=[AuthenticatedOnly()])
async def edit_organization(root, info: Info, id: int, input: EditOrganizationInput) -> Organization:
return await BaseMutationResolver(Organization, models.Organization).update(
info.context,
id,
data=input,
user_id=info.context.user_id
@@ -51,9 +44,7 @@ class OrganizationMutation:
@strawberry.type
class OrganizationUserMutation:
@strawberry.mutation
@error_logging
@authenticated_user_only()
@strawberry.mutation(extensions=[AuthenticatedOnly()])
async def add_to_organization(root, info, organization_id: int) -> Organization:
async with get_session() as db:
organization = (await db.scalars(
@@ -72,9 +63,7 @@ class OrganizationUserMutation:
return Organization(**organization.as_dict())
@strawberry.mutation
@error_logging
@authenticated_user_only()
@strawberry.mutation(extensions=[AuthenticatedOnly()])
async def remove_from_organization(root, info, organization_id: int) -> Organization:
async with get_session() as db:
organization = (await db.scalars(
+26 -32
View File
@@ -1,28 +1,31 @@
from typing import List, Optional
import strawberry
from fastapi import HTTPException
from starlette.status import HTTP_401_UNAUTHORIZED
from strawberry import Info
from database import models
from decorators.endpoints import authenticated_user_only, allow_public, raise_unauthorized
from decorators.error_logging import error_logging
from graphql_schema.entities.resolvers.base import BaseQueryResolver
from graphql_schema.entities.resolvers.photo import PhotoMutationResolver, PhotoQueryResolver
from graphql_schema.entities.types.types import Photo
from graphql_schema.entities.types.mutation_input import EditPhotoInput, UploadPhotoInput, AdjustmentInput
from graphql_schema.extensions.field.auth import AuthenticatedOnly, AllowPublicAccess
def raise_unauthorized():
raise HTTPException(HTTP_401_UNAUTHORIZED, "Not authorized")
@strawberry.type
class PhotoQueries:
@strawberry.field()
@error_logging
@allow_public
@strawberry.field(extensions=[AllowPublicAccess()])
async def photos(
root, info,
flight_id: Optional[int] = None,
copilot_id: Optional[int] = None,
uploaded_by_copilot_id: Optional[int] = None,
point_of_interest_id: Optional[int] = None,
aircraft_id: Optional[int] = None,
public: Optional[bool] = False,
) -> List[Photo]:
root, info: Info,
flight_id: int | None = None,
copilot_id: int | None = None,
uploaded_by_copilot_id: int | None = None,
point_of_interest_id: int | None = None,
aircraft_id: int | None = None,
public: bool | None = False,
) -> list[Photo]:
return await PhotoQueryResolver().get_list(
public=public,
flight_id=flight_id,
@@ -34,10 +37,8 @@ class PhotoQueries:
order_by=[models.Photo.exposed_at]
)
@strawberry.field()
@error_logging
@allow_public
async def photo(root, info, id: int, public: Optional[bool] = False, ) -> Photo:
@strawberry.field(extensions=[AllowPublicAccess()])
async def photo(root, info: Info, id: int, public: bool | None = False) -> Photo:
return await BaseQueryResolver(Photo, models.Photo).get_one(
object_id=id,
user_id=info.context.user_id,
@@ -47,23 +48,19 @@ class PhotoQueries:
@strawberry.type
class PhotoMutation:
@strawberry.mutation
@error_logging
@strawberry.mutation()
async def upload_photo(self, info, input: UploadPhotoInput) -> Photo:
if info.context.user_id is None and not input.copilot_upload_token:
raise_unauthorized()
return await PhotoMutationResolver().upload(info, input)
@strawberry.mutation()
@error_logging
@authenticated_user_only()
@strawberry.mutation(extensions=[AuthenticatedOnly()])
async def edit_photo(self, info, id: int, input: EditPhotoInput) -> Photo:
return await PhotoMutationResolver().update(id, input, info.context.user_id)
return await PhotoMutationResolver().update(info.context, id, input, info.context.user_id)
@strawberry.mutation()
@error_logging
async def change_orientation(self, info, id: int, direction: str, copilot_upload_token: Optional[str] = None) -> Photo:
async def change_orientation(self, info, id: int, direction: str, copilot_upload_token: str | None = None) -> Photo:
if info.context.user_id is None and not copilot_upload_token:
raise_unauthorized()
@@ -75,16 +72,13 @@ class PhotoMutation:
info=info
)
@strawberry.mutation()
@error_logging
@authenticated_user_only()
@strawberry.mutation(extensions=[AuthenticatedOnly()])
async def adjust_photo(self, info, id: int, adjustment: AdjustmentInput) -> Photo:
return await PhotoMutationResolver().adjust(id, info=info, user_id=info.context.user_id, adjustment=adjustment)
@strawberry.mutation()
@error_logging
async def delete_photo(self, info, id: int, copilot_upload_token: Optional[str] = None) -> Photo:
async def delete_photo(self, info, id: int, copilot_upload_token: str | None = None) -> Photo:
if info.context.user_id is None and not copilot_upload_token:
raise_unauthorized()
return await PhotoMutationResolver().delete(user_id=info.context.user_id, id=id)
return await PhotoMutationResolver().delete(info.context, id=id)
+14 -31
View File
@@ -1,48 +1,37 @@
from typing import Optional
import strawberry
from database import models
from decorators.endpoints import authenticated_user_only, allow_public
from database.transaction import get_session
from decorators.error_logging import error_logging
from graphql_schema.entities.helpers.combobox import handle_combobox_save
from graphql_schema.entities.helpers.detail import get_detail_filters
from graphql_schema.entities.helpers.pagination import get_pagination_window, PaginationWindow
from graphql_schema.entities.helpers.filters import get_detail_filters
from graphql_schema.entities.helpers.pagination import PaginationWindow
from graphql_schema.entities.resolvers.base import BaseQueryResolver, BaseMutationResolver
from graphql_schema.entities.types.types import PointOfInterest
from graphql_schema.entities.types.mutation_input import CreatePointOfInterestInput, EditPointOfInterestInput
from graphql_schema.extensions.field.auth import AuthenticatedOnly, AllowPublicAccess
from graphql_schema.extensions.field.pagination import OffsetPagination
@strawberry.type
class PointOfInterestQueries:
@strawberry.field()
@error_logging
@allow_public
@strawberry.field(extensions=[OffsetPagination(item_type=PointOfInterest), AllowPublicAccess()])
async def points_of_interest(
root, info,
limit: int, offset: int = 0,
search: Optional[str] = None,
public: bool = False
search: str | None = None,
public: bool = False,
) -> PaginationWindow[PointOfInterest]:
query = BaseQueryResolver(PointOfInterest, models.PointOfInterest).get_query(
return BaseQueryResolver(PointOfInterest, models.PointOfInterest).get_query(
info.context.user_id,
only_my=bool(info.context.user_id),
include_others_public=True,
only_public=public,
search=search,
)
return await get_pagination_window(
query=query,
item_type=PointOfInterest,
limit=limit,
offset=offset
)
@strawberry.field()
@allow_public
@strawberry.field(extensions=[AllowPublicAccess()])
async def point_of_interest(
root, info,
url_slug: Optional[str] = None,
id: Optional[int] = None,
url_slug: str | None = None,
id: int | None = None,
public: bool = False
) -> PointOfInterest:
filter_params = get_detail_filters(id, url_slug)
@@ -56,9 +45,7 @@ class PointOfInterestQueries:
@strawberry.type
class PointOfInterestMutation:
@strawberry.mutation
@error_logging
@authenticated_user_only()
@strawberry.mutation(extensions=[AuthenticatedOnly()])
async def create_point_of_interest(root, info, input: CreatePointOfInterestInput) -> PointOfInterest:
input_data = input.to_dict()
@@ -73,9 +60,7 @@ class PointOfInterestMutation:
db, input_data
)
@strawberry.mutation
@error_logging
@authenticated_user_only()
@strawberry.mutation(extensions=[AuthenticatedOnly()])
async def edit_point_of_interest(root, info, id: int, input: EditPointOfInterestInput) -> PointOfInterest:
input_data = input.to_dict()
@@ -93,8 +78,6 @@ class PointOfInterestMutation:
updated_poi = await models.PointOfInterest.update(db, obj=poi, data=input_data)
return PointOfInterest(**updated_poi.as_dict())
@strawberry.mutation
@error_logging
@authenticated_user_only()
@strawberry.mutation(extensions=[AuthenticatedOnly()])
async def delete_point_of_interest(self, info, id: int) -> PointOfInterest:
return await BaseMutationResolver(PointOfInterest, models.PointOfInterest).delete(info.context.user_id, id=id)
+4 -76
View File
@@ -1,92 +1,20 @@
from typing import List
import strawberry
from database import models
from decorators.endpoints import authenticated_user_only
from decorators.error_logging import error_logging
from graphql_schema.entities.resolvers.base import BaseQueryResolver
from graphql_schema.entities.types.types import PointOfInterestType
from graphql_schema.extensions.field.auth import AuthenticatedOnly
@strawberry.type
class PointOfInterestTypeQueries:
@strawberry.field()
@error_logging
@authenticated_user_only()
async def point_of_interest_types(root, info) -> List[PointOfInterestType]:
@strawberry.field(extensions=[AuthenticatedOnly()])
async def point_of_interest_types(root, info) -> list[PointOfInterestType]:
return await BaseQueryResolver(PointOfInterestType, models.PointOfInterestType).get_list(info.context.user_id)
@strawberry.field()
@error_logging
@authenticated_user_only()
@strawberry.field(extensions=[AuthenticatedOnly()])
async def point_of_interest_type(root, info, id: int) -> PointOfInterestType:
return await BaseQueryResolver(PointOfInterestType, models.PointOfInterestType).get_one(
object_id=id,
user_id=info.context.user_id
)
#
# @strawberry.type
# class CreatePointOfInterestMutation:
# @strawberry_sqlalchemy_input(models.PointOfInterest, exclude_fields=['id', 'type_id'])
# class CreatePointOfInterestInput:
# type: # Optional[ComboboxInput] = None
#
# @strawberry.mutation
# @authenticated_user_only()
# async def create_point_of_interest(root, info, input: CreatePointOfInterestInput) -> PointOfInterest:
# input_data = input.to_dict()
#
# input_data['type_id'] = await handle_combobox_save(
# info.context.db,
# models.PointOfInterestType,
# input.type,
# info.context.user_id
# )
#
# return await models.PointOfInterest.create(
# info.context.db,
# data=dict(
# **input_data,
# created_by_id=info.context.user_id,
# )
# )
#
#
# @strawberry.type
# class EditPointOfInterestMutation:
# @strawberry_sqlalchemy_input(models.PointOfInterest, exclude_fields=['id', 'type_id'])
# class EditPointOfInterestInput:
# type: Optional[ComboboxInput] = None
#
# @strawberry.mutation
# @authenticated_user_only()
# async def edit_point_of_interest(root, info, id: int, input: EditPointOfInterestInput) -> PointOfInterest:
# # TODO: kontrola organizace
# input_data = input.to_dict()
#
# if 'type' in input:
# input_data['type_id'] = await handle_combobox_save(
# info.context.db,
# models.PointOfInterestType,
# input.type,
# info.context.user_id
# )
#
# poi = (
# await info.context.db.scalars(
# get_base_query(info.context.user_id, only_my=True)
# .filter(models.PointOfInterest.id == id))
# ).one()
# return await models.PointOfInterest.update(info.context.db, obj=poi, data=input_data)
#
#
# @strawberry.type
# class DeletePointOfInterestMutation:
#
# @strawberry.mutation
# @authenticated_user_only()
# async def delete_point_of_interest(self, info, id: int) -> PointOfInterest:
# poi = get_base_query(info.context.user_id, only_my=True).filter(models.PointOfInterest.id == id).one()
#
# return await models.PointOfInterest.update(info.context.db, obj=poi, data=dict(deleted=True))
@@ -1,10 +1,7 @@
from operator import or_
from typing import Set, Optional
from sqlalchemy import and_
from sqlalchemy import and_, or_
from database import models
from database.transaction import get_session
from graphql_schema.context import GraphQLContext
from graphql_schema.entities.helpers.combobox import handle_combobox_save
from graphql_schema.entities.resolvers.base import BaseMutationResolver, BaseQueryResolver
from graphql_schema.entities.types.mutation_input import EditAircraftInput, CreateAircraftInput
@@ -17,29 +14,36 @@ class AircraftQueryResolver(BaseQueryResolver):
def get_query(
self,
user_id: Optional[int] = None,
object_id: Optional[int] = None,
order_by: Optional[list] = None,
organization_ids: Optional[Set[int]] = None,
user_id: int | None = None,
object_id: int | None = None,
order_by: list | None = None,
organization_ids: set[int] | None = None,
*args,
**kwargs,
):
filters = []
search = {}
if kwargs.get("call_sign"):
filters.append(models.Aircraft.call_sign == kwargs['call_sign'])
if kwargs.get("search"):
search['search'] = kwargs.pop("search", None)
query = super().get_query(
object_id=object_id,
only_my=False,
only_public=kwargs.get("only_public", False),
order_by=order_by,
filters=filters,
**search
)
if kwargs.get("search"):
search = kwargs["search"]
query = query.filter(
or_(
models.Aircraft.call_sign.contains(search),
models.Aircraft.manufacturer.contains(search),
models.Aircraft.model.contains(search),
)
)
if organization_ids:
query = (
query.filter(
@@ -62,7 +66,7 @@ class AircraftMutationResolver(BaseMutationResolver):
def __init__(self):
super().__init__(graphql_type=Aircraft, model=models.Aircraft)
async def create(self, context, data: CreateAircraftInput) -> Aircraft:
async def create(self, context: GraphQLContext, data: CreateAircraftInput) -> Aircraft:
input_data = data.to_dict()
async with get_session() as db:
@@ -77,7 +81,7 @@ class AircraftMutationResolver(BaseMutationResolver):
return await self._do_create(db, data=input_data)
async def update(self, id: int, user_id: int, data: EditAircraftInput) -> Aircraft:
async def update(self, context: GraphQLContext, id: int, data: EditAircraftInput) -> Aircraft:
update_data = data.to_dict()
async with get_session() as db:
if data.organization:
@@ -85,7 +89,7 @@ class AircraftMutationResolver(BaseMutationResolver):
db,
models.Organization,
input=data.organization,
user_id=user_id,
user_id=context.user_id,
)
return await self._do_update(db, id, update_data)
+19 -18
View File
@@ -1,12 +1,13 @@
from typing import Optional, Type, TypeVar, Generic, List
from typing import Type, TypeVar, Generic
from sqlalchemy import or_
from sqlalchemy.ext.asyncio import AsyncSession
from database import models
from database.query_builder import QueryBuilder
from database.transaction import get_session
from graphql_schema.context import GraphQLContext
from graphql_schema.entities.types.base import BaseGraphqlInputType
GQL_TYPE = TypeVar('GQL_TYPE')
@@ -18,7 +19,7 @@ class BaseResolver(Generic[GQL_TYPE]):
class BaseQueryResolver(BaseResolver):
async def _get_list(self, query) -> List[GQL_TYPE]:
async def _get_list(self, query) -> list[GQL_TYPE]:
async with get_session() as db:
items = (await db.scalars(query)).all()
@@ -31,14 +32,14 @@ class BaseQueryResolver(BaseResolver):
def get_query(
self,
user_id: Optional[int] = None,
object_id: Optional[int] = None,
order_by: Optional[list] = None,
only_public: Optional[bool] = False,
only_my: Optional[bool] = False,
include_others_public: Optional[bool] = False,
url_slug: Optional[str] = None,
filters: Optional[list] = None,
user_id: int | None = None,
object_id: int | None = None,
order_by: list | None = None,
only_public: bool | None = False,
only_my: bool | None = False,
include_others_public: bool | None = False,
url_slug: str | None = None,
filters: list | None = None,
**kwargs,
):
query = self.query_builder.get_simple_query(
@@ -77,11 +78,11 @@ class BaseQueryResolver(BaseResolver):
query = query.filter(or_(*search_clauses))
return query
async def get_list(self, user_id: Optional[int] = None, **kwargs) -> List[GQL_TYPE]:
async def get_list(self, user_id: int | None = None, **kwargs) -> list[GQL_TYPE]:
query = self.get_query(user_id=user_id, **kwargs)
return await self._get_list(query)
async def get_one(self, user_id: Optional[int] = None, **kwargs) -> GQL_TYPE:
async def get_one(self, user_id: int | None = None, **kwargs) -> GQL_TYPE:
query = self.get_query(user_id=user_id, **kwargs)
return await self._get_one(query)
@@ -107,7 +108,7 @@ class BaseMutationResolver(BaseResolver):
model = await self.model.update(db, data=data, **update_where)
return self.graphql_type(**model.as_dict())
async def create(self, context, data: BaseGraphqlInputType) -> GQL_TYPE:
async def create(self, context: GraphQLContext, data: BaseGraphqlInputType) -> GQL_TYPE:
input_data = data.to_dict()
if hasattr(self.model, "created_by_id"):
@@ -116,14 +117,14 @@ class BaseMutationResolver(BaseResolver):
async with get_session() as db:
return await self._do_create(db, input_data)
async def update(self, id: int, data: BaseGraphqlInputType, user_id: int) -> GQL_TYPE:
async def update(self, context: GraphQLContext, id: int, data: BaseGraphqlInputType) -> GQL_TYPE:
async with get_session() as db:
item = await self._get_one(db, id, user_id)
item = await self._get_one(db, id, context.user_id)
return await self._do_update(db, item, data.to_dict())
async def delete(self, user_id: int, id: int, **kwargs) -> GQL_TYPE:
async def delete(self, context: GraphQLContext, id: int, **kwargs) -> GQL_TYPE:
async with get_session() as db:
model = await self._get_one(db, id, user_id)
model = await self._get_one(db, id, context.user_id)
if hasattr(self.model, "deleted"):
model = await self.model.update(db, obj=model, data=dict(deleted=True))
@@ -1,7 +1,4 @@
from typing import Optional
from sqlalchemy import and_
from database import models
from graphql_schema.entities.resolvers.base import BaseQueryResolver
from graphql_schema.entities.types.types import Copilot
@@ -13,10 +10,10 @@ class CopilotQueryResolver(BaseQueryResolver):
def get_query(
self,
user_id: Optional[int] = None,
object_id: Optional[int] = None,
order_by: Optional[list] = None,
only_public: Optional[bool] = False,
user_id: int | None = None,
object_id: int | None = None,
order_by: list | None = None,
only_public: bool | None = False,
**kwargs
):
pilot_username = kwargs.pop("pilot_username", None)
@@ -10,10 +10,10 @@ class EventQueryResolver(BaseQueryResolver):
def get_query(
self,
user_id: Optional[int] = None,
object_id: Optional[int] = None,
order_by: Optional[list] = None,
only_public: Optional[bool] = True,
user_id: int | None = None,
object_id: int | None = None,
order_by: list | None = None,
only_public: bool | None = True,
*args,
**kwargs,
):
+46 -16
View File
@@ -1,9 +1,11 @@
import asyncio
import random
from typing import Optional
from sqlalchemy import delete, insert
from sqlalchemy import delete, insert, or_, select
from sqlalchemy.ext.asyncio import AsyncSession
from background_jobs.elevation import add_terrain_elevation_to_flight
from background_jobs.flight_title_photo import add_circular_avatar, generate_flight_title_photo
from graphql_schema.context import GraphQLContext
from utils.flight_track_helpers import handle_upload_gpx, save_track_from_gpx_to_db, extract_basic_flight_info_from_gpx
from background_jobs.weather import download_weather_for_flight
from database import models
@@ -26,8 +28,8 @@ class FlightQueryResolver(BaseQueryResolver):
def get_query(
self,
user_id: Optional[int] = None,
only_public: Optional[bool] = False,
user_id: int | None = None,
only_public: bool | None = False,
*args,
**kwargs
):
@@ -55,8 +57,27 @@ class FlightQueryResolver(BaseQueryResolver):
if kwargs.get("point_of_interest_id"):
query = (
query.join(models.Flight.turn_points)
.filter(models.FlightTurnPoint.point_of_interest_id == kwargs["point_of_interest_id"])
query
.filter(
or_(
models.Flight.id.in_(
select(models.FlightTurnPoint.flight_id)
.filter(models.FlightTurnPoint.point_of_interest_id == kwargs["point_of_interest_id"])
),
models.Flight.id.in_(
select(models.Photo.flight_id)
.filter(models.Photo.point_of_interest_id == kwargs["point_of_interest_id"])
)
)
)
)
if kwargs.get("search"):
search = kwargs["search"]
query = (
query.filter(
models.Flight.name.contains(search)
)
)
if kwargs.get('username'):
@@ -72,7 +93,7 @@ class FlightMutationResolver(BaseMutationResolver):
def __init__(self):
super().__init__(Flight, models.Flight)
async def create(self, context, input: CreateFlightInput) -> Flight:
async def create(self, context: GraphQLContext, input: CreateFlightInput) -> Flight:
data = input.to_dict()
user_id = context.user_id
@@ -108,14 +129,14 @@ class FlightMutationResolver(BaseMutationResolver):
if input.track is not None:
await handle_track_edit(db=db, flight_id=flight.id, track=input.track, user_id=user_id)
if data['gpx_track_filename']:
if 'gpx_track_filename' in data:
await save_track_from_gpx_to_db(gpx_filename=data['gpx_track_filename'], flight_id=flight.id)
schedule_background_tasks(flight.id, data, context)
return flight
async def update(self, context, id: int, input: EditFlightInput) -> Flight:
async def update(self, context: GraphQLContext, id: int, input: EditFlightInput) -> Flight:
user_id = context.user_id
async with get_session() as db:
flight = await self._get_one(db, id, user_id)
@@ -179,14 +200,23 @@ class FlightMutationResolver(BaseMutationResolver):
def schedule_background_tasks(flight_id: int, flight_data: dict, context) -> None:
context.background_tasks.add_task(add_terrain_elevation_to_flight, flight_id=flight_id)
context.background_tasks.add_task(
download_weather_for_flight, flight_id=id, airport_id=flight_data['takeoff_airport_id'],
date_time=flight_data['takeoff_datetime'], type_="takeoff"
)
context.background_tasks.add_task(
download_weather_for_flight, flight_id=id, airport_id=flight_data['landing_airport_id'],
date_time=flight_data['landing_datetime'], type_="landing"
)
if flight_data.get("title_photo_id"):
context.background_tasks.add_task(
generate_flight_title_photo, flight_id=flight_id, photo_id=flight_data['title_photo_id']
)
if flight_data.get("takeoff_airport_id"):
context.background_tasks.add_task(
download_weather_for_flight, flight_id=flight_id, airport_id=flight_data['takeoff_airport_id'],
date_time=flight_data['takeoff_datetime'], type_="takeoff"
)
if flight_data.get("landing_airport_id"):
context.background_tasks.add_task(
download_weather_for_flight, flight_id=flight_id, airport_id=flight_data['landing_airport_id'],
date_time=flight_data['landing_datetime'], type_="landing"
)
async def handle_track_edit(db: AsyncSession, flight_id: int, track: list[TrackItemInput], user_id: int):
@@ -1,15 +1,17 @@
import asyncio
from typing import Optional
from sqlalchemy import delete, select
from sqlalchemy.dialects.mysql import insert
from database import models
from database.models import flight_plan_has_copilot
from database.transaction import get_session
from graphql_schema.context import GraphQLContext
from graphql_schema.entities.helpers.combobox import handle_combobox_save
from graphql_schema.entities.resolvers.base import BaseMutationResolver, BaseQueryResolver
from graphql_schema.entities.resolvers.flight import handle_aircraft_save
from graphql_schema.entities.types.mutation_input import CreateFlightPlanInput, EditFlightPlanInput
from graphql_schema.entities.types.types import FlightPlan
from utils.flight_plan_helpers import refresh_markers_weather_info
from utils.list_utils import evenly_spaced_elements
from utils.str_utils import random_str
@@ -19,9 +21,9 @@ class FlightPlanQueryResolver(BaseQueryResolver):
def get_query(
self,
user_id: Optional[int] = None,
only_public: Optional[bool] = False,
object_id: Optional[int] = None,
user_id: int | None = None,
only_public: bool | None = False,
object_id: int | None = None,
*args,
**kwargs
):
@@ -40,6 +42,12 @@ class FlightPlanQueryResolver(BaseQueryResolver):
.filter(models.User.public_username == kwargs['username'])
)
if kwargs.get("search"):
search = kwargs["search"]
query = (
query.filter(models.FlightPlan.name.contains(search))
)
return query
@@ -47,61 +55,24 @@ class FlightPlanMutationResolver(BaseMutationResolver):
def __init__(self):
super().__init__(graphql_type=FlightPlan, model=models.FlightPlan)
@staticmethod
async def save_markers(db, flight_plan: models.FlightPlan, markers: list):
position = 0
for marker in markers:
if marker.type == 'poi':
assert bool(marker.point_of_interest_id)
if marker.type == 'airport':
assert bool(marker.airport_id)
await models.FlightPlanMarker.create(db, data={
"position": position,
"flight_plan_id": flight_plan.id,
"airport_id": marker.airport_id,
"point_of_interest_id": marker.point_of_interest_id,
"type": marker.type,
"name": marker.name,
"gps_latitude": marker.gps_latitude,
"gps_longitude": marker.gps_longitude
})
position += 1
@staticmethod
async def reset_plan_markers(db, flight_plan: models.FlightPlan):
await db.execute(
delete(models.FlightPlanMarker)
.filter(models.FlightPlanMarker.flight_plan_id == flight_plan.id)
)
async def create(self, context, data: CreateFlightPlanInput) -> FlightPlan:
async def create(self, context: GraphQLContext, data: CreateFlightPlanInput) -> FlightPlan:
input_data = data.to_dict()
input_data['created_by_id'] = context.user_id
async with get_session() as db:
flight_plan = await self._do_create(db, data=input_data)
await self.save_markers(db, flight_plan, data.markers)
markers = await self.save_markers(db, flight_plan, data.markers)
await refresh_markers_weather_info(
planned_takeoff_datetime=flight_plan.planned_takeoff_datetime,
planned_speed=flight_plan.planned_speed,
markers=markers,
background_tasks=context.background_tasks
)
return flight_plan
async def save_copilots(self, db, flight_plan_id: int, copilots: list, user_id: int):
await db.execute(delete(flight_plan_has_copilot).filter_by(flight_plan_id=flight_plan_id))
copilots = await asyncio.gather(*[
handle_combobox_save(db, models.Copilot, copilot, user_id) for copilot in copilots
])
for copilot_id in copilots:
await db.execute(insert(flight_plan_has_copilot).values(
flight_plan_id=flight_plan_id,
copilot_id=copilot_id,
token=random_str(64)
))
async def update(self, id: int, data: EditFlightPlanInput, user_id: int) -> FlightPlan:
async def update(self, context: GraphQLContext, id: int, data: EditFlightPlanInput) -> FlightPlan:
input_data = data.to_dict()
user_id = context.user_id
async with get_session() as db:
flight_plan_model = await self._get_one(db, id=id, created_by_id=user_id)
@@ -109,19 +80,29 @@ class FlightPlanMutationResolver(BaseMutationResolver):
if data.aircraft is not None:
input_data['aircraft_id'] = await handle_aircraft_save(db, user_id, data.aircraft)
markers = []
if data.markers is not None:
await self.reset_plan_markers(db, flight_plan_model)
await self.save_markers(db, flight_plan_model, data.markers)
markers = await self.save_markers(db, flight_plan_model, data.markers)
await refresh_markers_weather_info(
planned_takeoff_datetime=flight_plan_model.planned_takeoff_datetime,
planned_speed=flight_plan_model.planned_speed,
markers=markers,
background_tasks=context.background_tasks
)
if flight_plan_model.is_default_name:
if data.markers:
markers = data.markers
else:
markers = (await db.scalars(select(models.FlightPlanMarker).filter(models.FlightPlanMarker.flight_plan_id == id))).all()
if not markers:
markers = (await db.scalars(
select(models.FlightPlanMarker)
.filter(models.FlightPlanMarker.flight_plan_id == id)
)).all()
used_markers = evenly_spaced_elements(markers, 5)
input_data['name'] = " - ".join(m.name for m in used_markers)
# TODO: pokud se zmenila rychlost nebo datum/cas letu, je potreba stahnout nove pocasi!
if data.copilots is not None:
await self.save_copilots(
db,
@@ -134,8 +115,49 @@ class FlightPlanMutationResolver(BaseMutationResolver):
return flight_plan
def evenly_spaced_elements(lst: list, count: int) -> list:
if count > len(lst):
return lst
interval = (len(lst) - 1) / (count - 1) if count > 1 else 0
return [lst[int(round(i * interval))] for i in range(count)]
@staticmethod
async def save_markers(db, flight_plan: models.FlightPlan, markers: list):
position = 0
marker_objects = []
for marker in markers:
if marker.type == 'poi':
assert bool(marker.point_of_interest_id)
if marker.type == 'airport':
assert bool(marker.airport_id)
marker_obj = await models.FlightPlanMarker.create(db, data={
"position": position,
"flight_plan_id": flight_plan.id,
"airport_id": marker.airport_id,
"point_of_interest_id": marker.point_of_interest_id,
"type": marker.type,
"name": marker.name,
"gps_latitude": marker.gps_latitude,
"gps_longitude": marker.gps_longitude
})
marker_objects.append(marker_obj)
position += 1
return marker_objects
@staticmethod
async def reset_plan_markers(db, flight_plan: models.FlightPlan):
await db.execute(
delete(models.FlightPlanMarker)
.filter(models.FlightPlanMarker.flight_plan_id == flight_plan.id)
)
async def save_copilots(self, db, flight_plan_id: int, copilots: list, user_id: int):
await db.execute(delete(flight_plan_has_copilot).filter_by(flight_plan_id=flight_plan_id))
copilots = await asyncio.gather(*[
handle_combobox_save(db, models.Copilot, copilot, user_id) for copilot in copilots
])
for copilot_id in copilots:
await db.execute(insert(flight_plan_has_copilot).values(
flight_plan_id=flight_plan_id,
copilot_id=copilot_id,
token=random_str(64)
))
@@ -12,8 +12,8 @@ class OrganizationQueryResolver(BaseQueryResolver):
def get_query(
self,
object_id: Optional[int] = None,
order_by: Optional[list] = None,
object_id: int | None = None,
order_by: list | None = None,
**kwargs
):
query = (
+29 -18
View File
@@ -1,14 +1,17 @@
import os
import shutil
from time import time
from typing import Optional
import sqlalchemy
from PIL import Image
from graphql import GraphQLError
from pydantic import BaseModel
from pymysql.err import IntegrityError
from sqlalchemy import delete, insert, select
from background_jobs.elevation import add_terrain_elevation_to_photo
from background_jobs.photo import generate_thumbnail, resize_photo
from database import models
from database.transaction import get_session
from graphql_schema.context import GraphQLContext
from graphql_schema.entities.helpers.combobox import handle_combobox_save
from graphql_schema.entities.resolvers.base import BaseMutationResolver, BaseQueryResolver
from graphql_schema.entities.types.mutation_input import EditPhotoInput, UploadPhotoInput, AdjustmentInput
@@ -26,10 +29,10 @@ class PhotoQueryResolver(BaseQueryResolver):
def get_query(
self,
user_id: Optional[int] = None,
object_id: Optional[int] = None,
order_by: Optional[list] = None,
only_public: Optional[bool] = False,
user_id: int | None = None,
object_id: int | None = None,
order_by: list | None = None,
only_public: bool | None = False,
*args, **kwargs
):
query = super().get_query(
@@ -61,7 +64,7 @@ class PhotoQueryResolver(BaseQueryResolver):
class PhotoDetailInfo(BaseModel):
filename: str
original_filename: str
original_filename: str | None
path: str
flight_id: int
@@ -71,14 +74,14 @@ class PhotoMutationResolver(BaseMutationResolver):
super().__init__(Photo, models.Photo)
@staticmethod
def _copy_original(path: str, filename: str):
def _copy_original(path: str, filename: str) -> str:
original_filename = "_original_" + filename
if not os.path.isfile(path + "/" + original_filename):
shutil.copyfile(path + "/" + filename, path + "/" + original_filename)
return original_filename
async def _get_photo_details(self, id: int, user_id: int, copilot_token: Optional[str]):
async def _get_photo_details(self, id: int, user_id: int, copilot_token: str | None, copy_original: bool = True):
async with get_session() as db:
if not copilot_token:
photo = await self._get_one(db, id, created_by_id=user_id)
@@ -89,15 +92,20 @@ class PhotoMutationResolver(BaseMutationResolver):
filename = photo.filename + "." + photo.filename_extension
path = get_photo_basepath(flight_id)
original_filename = None
if copy_original:
original_filename = self._copy_original(path, filename)
return PhotoDetailInfo(
flight_id=flight_id,
path=path,
filename=filename,
original_filename=self._copy_original(path, filename) # TODO: tohle nedelat v getteru!
original_filename=original_filename
)
@staticmethod
async def _get_photo_info_by_copilot_token(db, photo_id: int, copilot_token: Optional[str]):
async def _get_photo_info_by_copilot_token(db, photo_id: int, copilot_token: str | None):
if not copilot_token:
return
@@ -165,17 +173,17 @@ class PhotoMutationResolver(BaseMutationResolver):
return photo
async def update(self, id: int, input: EditPhotoInput, user_id: int) -> Photo:
async def update(self, context: GraphQLContext, id: int, input: EditPhotoInput) -> Photo:
data = input.to_dict()
async with get_session() as db:
photo = await self._get_one(db, id, created_by_id=user_id)
photo = await self._get_one(db, id, created_by_id=context.user_id)
if input.point_of_interest:
data['point_of_interest_id'] = await handle_combobox_save(
db,
models.PointOfInterest,
input.point_of_interest,
user_id,
context.user_id,
extra_data={"description": ""}
)
@@ -187,7 +195,7 @@ class PhotoMutationResolver(BaseMutationResolver):
return await self._do_update(db, obj=photo, data=data)
async def change_orientation(
self, id: int, user_id: int, direction: str, copilot_upload_token: Optional[str], info
self, id: int, user_id: int, direction: str, copilot_upload_token: str | None, info
):
photo = await self._get_photo_details(id, user_id, copilot_upload_token)
@@ -261,12 +269,15 @@ class PhotoMutationResolver(BaseMutationResolver):
"cache_key": int(time())
})
async def delete(self, user_id: int, id: int, **kwargs) -> Photo:
async def delete(self, context: GraphQLContext, id: int, **kwargs) -> Photo:
copilot_upload_token = kwargs.get("copilot_upload_token")
await self._get_photo_details(id, user_id, copilot_upload_token)
# ^^ pokud neexistuje, vyhodi vyjimku
await self._get_photo_details(id, context.user_id, copilot_upload_token, copy_original=False)
try:
photo = await super().delete(context, id)
except (IntegrityError, sqlalchemy.exc.IntegrityError):
raise GraphQLError("Nelze smazat titulni fotku")
photo = await super().delete(user_id, id)
base_path = get_photo_basepath(photo.flight_id)
files_to_delete = [
@@ -11,13 +11,13 @@ from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_i
@strawberry.input()
class ComboboxInput:
id: Optional[int] = None
id: int | None = None
name: str
@strawberry_sqlalchemy_input(model=models.FlightPlanMarker, exclude_fields=["id", "flight_plan_id"])
class FlightPlanMarkerInput(BaseGraphqlInputType):
position: Optional[int] = 0
position: int | None = 0
@strawberry_sqlalchemy_input(model=models.Copilot, exclude_fields=["id"])
@@ -81,19 +81,19 @@ class EditEventInput(BaseGraphqlInputType):
class UploadPhotoInput:
photo: Upload
flight_id: int
name: Optional[str] = None
description: Optional[str] = None
name: str | None = None
description: str | None = None
point_of_interest: Optional[ComboboxInput] = None
copilot_upload_token: Optional[str] = None
copilot_upload_token: str | None = None
@strawberry.input
class EditPhotoInput:
name: Optional[str] = None
description: Optional[str] = None
name: str | None = None
description: str | None = None
point_of_interest: Optional[ComboboxInput] = None
copilots: Optional[List[ComboboxInput]] = None
aircraft_id: Optional[int] = None
aircraft_id: int | None = None
def to_dict(self):
return {
@@ -112,12 +112,12 @@ class CropInput(BaseGraphqlInputType):
@strawberry.input
class AdjustmentInput:
rotate: Optional[float] = 0
crop_after_rotate: Optional[bool] = True,
brightness: Optional[float] = 1
contrast: Optional[float] = 1
saturation: Optional[float] = 1
sharpness: Optional[float] = 1
rotate: float | None = 0
crop_after_rotate: bool | None = True,
brightness: float | None = 1
contrast: float | None = 1
saturation: float | None = 1
sharpness: float | None = 1
crop: Optional[CropInput] = None
@@ -138,8 +138,8 @@ class EditFlightInput(BaseGraphqlInputType):
@strawberry.input()
class CreateFlightInput(BaseGraphqlInputType):
aircraft: ComboboxInput
flight_plan_id: Optional[int] = None
name: Optional[str] = None
flight_plan_id: int | None = None
name: str | None = None
gpx_track_file: Optional[Upload] = None # TODO: poresit validaci uploadovaneho souboru!
landing_airport: Optional[ComboboxInput] = None
takeoff_airport: Optional[ComboboxInput] = None
@@ -152,9 +152,9 @@ class CreateFlightInput(BaseGraphqlInputType):
class TrackItemInput:
point_of_interest: Optional[ComboboxInput] = None
airport: Optional[ComboboxInput] = None
landing_duration: Optional[int] = None
gps_latitude: Optional[float] = None
gps_longitude: Optional[float] = None
landing_duration: int | None = None
gps_latitude: float | None = None
gps_longitude: float | None = None
@strawberry_sqlalchemy_input(models.Aircraft, exclude_fields=['id', 'photo_filename'])
+18 -16
View File
@@ -3,7 +3,7 @@ import math
from typing import Optional, List
import strawberry
from database import models
from decorators.endpoints import authenticated_user_only
# from decorators.endpoints import authenticated_user_only
from utils.gps import get_bearing, get_distance
from external.gpx_parser import GPXParser
from graphql_schema.dataloaders.flight_duration import flight_duration_dataloader
@@ -17,7 +17,7 @@ from graphql_schema.dataloaders.multi_models import (
)
from graphql_schema.dataloaders.single_model import (
poi_dataloader, poi_type_dataloader, event_dataloader, aircraft_dataloader, airport_dataloader,
airport_weather_info_loader, organizations_dataloader, flight_dataloader, photo_adjustment_dataloader,
weather_info_loader, organizations_dataloader, flight_dataloader, photo_adjustment_dataloader,
photo_dataloader, user_dataloader, track_dataloader
)
from graphql_schema.permissions import IsAuthenticated
@@ -141,11 +141,11 @@ class Flight:
for key, value in kwargs.items():
setattr(self, key, value)
@authenticated_user_only(raise_when_unauthorized=False, return_value_unauthorized=[])
# @authenticated_user_only(raise_when_unauthorized=False, return_value_unauthorized=[])
async def load_copilots(root):
return await flight_copilots_dataloader.load(root.id)
@authenticated_user_only(raise_when_unauthorized=False, return_value_unauthorized=[])
# @authenticated_user_only(raise_when_unauthorized=False, return_value_unauthorized=[])
async def load_event(root):
return await event_dataloader.load(root.event_id)
@@ -162,18 +162,18 @@ class Flight:
title_photo: Optional[Photo] = strawberry.field(resolver=lambda root: photo_dataloader.load(root.title_photo_id))
turn_points: List[FlightTurnPoint] = strawberry.field(resolver=lambda root: flight_turn_points_dataloader.load(root.id))
takeoff_weather_info: Optional[WeatherInfo] = strawberry.field(
resolver=lambda root: airport_weather_info_loader.load(root.takeoff_weather_info_id)
resolver=lambda root: weather_info_loader.load(root.takeoff_weather_info_id)
)
landing_weather_info: Optional[WeatherInfo] = strawberry.field(
resolver=lambda root: airport_weather_info_loader.load(root.landing_weather_info_id)
resolver=lambda root: weather_info_loader.load(root.landing_weather_info_id)
)
photos: List[Photo] = strawberry.field(resolver=lambda root: photos_dataloader.load(root.id))
track: Optional[Track] = strawberry.field(resolver=lambda root: track_dataloader.load(root.track_id))
duration_min_calculated: int = strawberry.field(resolver=lambda root: flight_duration_dataloader.load(root.id))
social_image_url: Optional[str] = strawberry.field(
social_image_url: str | None = strawberry.field(
resolver=lambda root: get_public_url(f'photos/{root.id}/title_photo.jpg')
)
debriefing_notes: Optional[str] = strawberry.field(
debriefing_notes: str | None = strawberry.field(
permission_classes=[IsAuthenticated],
resolver=lambda root: root.debriefing_notes
)
@@ -181,13 +181,15 @@ class Flight:
@strawberry_sqlalchemy_type(model=models.FlightPlanMarker)
class FlightPlanMarker:
pass
weather_info: Optional[WeatherInfo] = strawberry.field(
resolver=lambda root: weather_info_loader.load(root.weather_info_id)
)
@strawberry.type
class FlightPlanTrack:
bearing: Optional[int]
distance: Optional[float]
bearing: int | None
distance: float | None
from_: FlightPlanMarker = strawberry.field(name="from")
to: Optional[FlightPlanMarker]
@@ -216,7 +218,7 @@ class FlightPlan:
)
return navigation
@authenticated_user_only(raise_when_unauthorized=False, return_value_unauthorized=[])
# @authenticated_user_only(raise_when_unauthorized=False, return_value_unauthorized=[])
async def load_copilots(root):
return await flight_plan_copilots_dataloader.load(root.id)
@@ -245,7 +247,7 @@ class Copilot:
flights: List[Flight] = strawberry.field(resolver=resolve_flights)
photos: List[Photo] = strawberry.field(resolver=lambda root: photo_copilots_dataloader.load(root.id))
title_photo: Optional[Photo] = strawberry.field(resolver=lambda root: photo_dataloader.load(root.title_photo_id))
token: Optional[str] = strawberry.field(
token: str | None = strawberry.field(
permission_classes=[IsAuthenticated],
resolver=resolve_copilot_token
)
@@ -271,9 +273,9 @@ class Organization:
@strawberry_sqlalchemy_type(models.User, exclude_fields=['password_hashed'])
class User:
avatar_image_url: Optional[str] = strawberry.field(resolver=lambda root: get_avatar_url(root))
avatar_image_url: str | None = strawberry.field(resolver=lambda root: get_avatar_url(root))
title_image_url: str = strawberry.field(resolver=lambda root: get_title_image_url(root))
social_image_url: Optional[str] = strawberry.field(
social_image_url: str | None = strawberry.field(
resolver=lambda root: get_public_url(f'profile/{root.id}/title_photo.jpg')
)
organizations: List[Organization] = strawberry.field(
@@ -283,7 +285,7 @@ class User:
@strawberry_sqlalchemy_type(models.Event)
class Event:
async def load_flights(root, info, only_public: Optional[bool] = False):
async def load_flights(root, info, only_public: bool | None = False):
is_user_logged_in = bool(info.context.user_id)
if not is_user_logged_in or only_public:
dataloader = public_flights_by_event_dataloader
+12 -16
View File
@@ -3,13 +3,13 @@ import strawberry
from graphql import GraphQLError
from passlib.hash import bcrypt
from sqlalchemy import select
from strawberry import Info
from strawberry.file_uploads import Upload
from background_jobs.photo import resize_photo
from database import models
from decorators.endpoints import authenticated_user_only
from decorators.error_logging import error_logging
from database.transaction import get_session
from graphql_schema.entities.types.types import User
from graphql_schema.extensions.field.auth import AuthenticatedOnly
from utils.file import delete_file
from utils.file import handle_file_upload
@@ -17,8 +17,7 @@ from utils.file import handle_file_upload
@strawberry.type
class UserQueries:
@strawberry.field()
@error_logging
async def user(root, info, username: str) -> User:
async def user(root, info: Info, username: str) -> User:
if len(username) == 0:
raise GraphQLError("Username not set!")
@@ -28,10 +27,8 @@ class UserQueries:
return user
@strawberry.field()
@authenticated_user_only()
@error_logging
async def logged_user(root, info) -> User:
@strawberry.field(extensions=[AuthenticatedOnly()])
async def logged_user(root, info: Info) -> User:
async with get_session() as db:
user_model = (await db.scalars(
select(models.User).filter_by(id=info.context.user_id)
@@ -44,17 +41,16 @@ class UserQueries:
class EditUserMutation:
@strawberry.input
class EditUserInput:
name: Optional[str] = None
description: Optional[str] = None
public_username: Optional[str] = None
old_password: Optional[str] = None
new_password: Optional[str] = None
name: str | None = None
description: str | None = None
public_username: str | None = None
old_password: str | None = None
new_password: str | None = None
avatar_image: Optional[Upload] = None
title_image: Optional[Upload] = None
@strawberry.mutation
@authenticated_user_only()
async def edit_logged_user(root, info, input: EditUserInput) -> User:
@strawberry.mutation(extensions=[AuthenticatedOnly()])
async def edit_logged_user(root, info: Info, input: EditUserInput) -> User:
async with get_session() as db:
user = (await db.scalars(
select(models.User).filter_by(id=info.context.user_id)
@@ -0,0 +1,25 @@
from typing import Any
from fastapi import HTTPException
from starlette.status import HTTP_401_UNAUTHORIZED
from strawberry import Info
from strawberry.extensions import FieldExtension
from strawberry.extensions.field_extension import AsyncExtensionResolver
class AuthenticatedOnly(FieldExtension):
async def resolve_async(self, next_: AsyncExtensionResolver, source: Any, info: Info, **kwargs: Any) -> Any:
if not info.context.user_id:
raise HTTPException(HTTP_401_UNAUTHORIZED, "Not authorized")
return await next_(source, info, **kwargs)
class AllowPublicAccess(FieldExtension):
async def resolve_async(self, next_: AsyncExtensionResolver, source: Any, info: Info, **kwargs: Any) -> Any:
user_id = info.context.user_id
public = kwargs.get('public')
if not user_id and not public:
raise HTTPException(HTTP_401_UNAUTHORIZED, "Not authorized")
return await next_(source, info, **kwargs)
@@ -0,0 +1,48 @@
from typing import Callable, Any, Type
import strawberry
from strawberry.annotation import StrawberryAnnotation
from strawberry.extensions import FieldExtension
from strawberry.types.arguments import StrawberryArgument
from strawberry.types.field import StrawberryField
from graphql_schema.entities.helpers.pagination import get_pagination_window, PaginationWindow
class OffsetPagination[Item](FieldExtension):
def __init__(self, item_type: Type[Item]):
super().__init__()
self.item_type = item_type
def apply(self, field: StrawberryField) -> StrawberryField:
offset_arg = StrawberryArgument(
python_name="offset",
graphql_name="offset",
type_annotation=StrawberryAnnotation(annotation=int | None),
default=0,
)
limit_arg = StrawberryArgument(
python_name="limit",
graphql_name="limit",
type_annotation=StrawberryAnnotation(annotation=int),
default=10,
)
field.arguments.append(offset_arg)
field.arguments.append(limit_arg)
return field
async def resolve_async(
self, next_: Callable[..., Any], source: Any, info: strawberry.Info,
limit: int, offset: int = 0,
**kwargs
) -> PaginationWindow[Item]:
query = await next_(source, info, **kwargs)
return await get_pagination_window(
query=query,
item_type=self.item_type,
limit=limit,
offset=offset,
)
@@ -0,0 +1,19 @@
from typing import Callable, Any
from fastapi import HTTPException
from graphql import GraphQLResolveInfo, GraphQLError
from sqlalchemy.exc import NoResultFound
from strawberry.extensions import SchemaExtension
from strawberry.utils.await_maybe import AwaitableOrValue
class ErrorLogging(SchemaExtension):
async def resolve_async(self, _next: Callable, root: Any, info: GraphQLResolveInfo, *args: str, **kwargs: Any) -> AwaitableOrValue[object]:
try:
return await _next(root, info, *args, **kwargs)
except NoResultFound as e:
raise GraphQLError("Not found", original_error=e)
except HTTPException as e:
if e.status_code == 401:
raise GraphQLError("Not authorized", original_error=e)
except Exception as e:
raise GraphQLError(f"Unknown error: {e}", original_error=e)
+17 -32
View File
@@ -1,45 +1,30 @@
import dataclasses
from typing import Set
from datetime import datetime
import strawberry
from fastapi_jwt import JwtAuthorizationCredentials
from fastapi_jwt.jwt import JwtAccessBearerCookie
from starlette.background import BackgroundTasks
from strawberry.extensions import SchemaExtension
from strawberry.fastapi import BaseContext
from strawberry.extensions import SchemaExtension, ValidationCache, ParserCache
from graphql_schema.extensions.schema.error_logging import ErrorLogging
from logger import log
from .mutation import Mutation
from .query import Query
# Toto se da kdyztak pouzit jako extension do Schema
# class SQLAlchemySession(Extension):
# def on_request_start(self):
# session = async_session()
# print(self.execution_context.context)
# self.execution_context.context["db"] = session
#
# async def on_request_end(self):
# await self.execution_context.context["db"].close()
class LoggingExtension(SchemaExtension):
def on_request_start(self):
print("request start")
def on_request_end(self):
# nastavuje se pri vytvareni GraphQL contextu
request_start = self.execution_context.context.request_start_time
request_end = datetime.now()
async def on_request_end(self):
print("request end")
@dataclasses.dataclass
class GraphQLContext(BaseContext):
user_id: int
organization_ids: Set[int]
jwt_auth_credentials: JwtAuthorizationCredentials
jwt: JwtAccessBearerCookie
background_tasks: BackgroundTasks
request_time_ms = round((request_end - request_start).microseconds / 1000)
op_name = self.execution_context.operation_name
log.info(f"Operation {op_name} took {request_time_ms}ms")
schema = strawberry.Schema(
query=Query,
mutation=Mutation,
extensions=[LoggingExtension]
extensions=[
LoggingExtension(),
ErrorLogging(),
ParserCache(),
ValidationCache()
],
)
@@ -28,7 +28,7 @@ def get_annotations_for_scalars(model: BaseModel, exclude_fields=None, force_opt
return annotations_
def strawberry_sqlalchemy_type(model, exclude_fields: Optional[typing.Union[List, typing.Tuple]] = None):
def strawberry_sqlalchemy_type(model: BaseModel, exclude_fields: list | tuple | None = None):
if exclude_fields is None:
exclude_fields = []
@@ -47,7 +47,7 @@ def strawberry_sqlalchemy_input(
model,
exclude_fields: Optional[typing.Union[List, typing.Tuple]] = None,
all_optional: bool = False
) -> typing.Callable[[...], strawberry.object_type]:
) -> typing.Callable[[...], strawberry.type]:
if exclude_fields is None:
exclude_fields = []
+14
View File
@@ -0,0 +1,14 @@
from datetime import timedelta
from fastapi_jwt import JwtAccessBearerCookie, JwtRefreshBearerCookie
from config import APP_SECRET_KEY, APP_DEBUG, REFRESH_TOKEN_VALIDITY_DAYS
access_security = JwtAccessBearerCookie(
secret_key=APP_SECRET_KEY,
auto_error=False,
access_expires_delta=timedelta(days=1) if APP_DEBUG else timedelta(minutes=20)
)
refresh_security = JwtRefreshBearerCookie(
secret_key=APP_SECRET_KEY,
auto_error=True,
refresh_expires_delta=timedelta(days=REFRESH_TOKEN_VALIDITY_DAYS),
)
+14 -10
View File
@@ -1,22 +1,26 @@
import logging
import sys
from colorlog import ColoredFormatter
log = logging.getLogger(__name__)
log.setLevel(logging.DEBUG)
# Create handlers for logging to the standard output and a file
stdoutHandler = logging.StreamHandler(stream=sys.stdout)
# Set the log levels on the handlers
stdoutHandler.setLevel(logging.DEBUG)
# Create a log format using Log Record attributes
fmt = logging.Formatter(
"%(name)s: %(asctime)s | %(levelname)s | %(filename)s:%(lineno)s | %(process)d >>> %(message)s"
fmt = ColoredFormatter(
"%(log_color)s[%(levelname)s]%(reset)s %(blue)s%(message)s%(reset)s %(light_black)s[%(filename)s:%(lineno)d]]%(reset)s",
datefmt=None,
reset=True,
log_colors={
'DEBUG': 'cyan',
'INFO': 'green',
'WARNING': 'yellow',
'ERROR': 'red',
'CRITICAL': 'red,bg_white',
},
secondary_log_colors={},
style='%'
)
# Set the log format on each handler
stdoutHandler.setFormatter(fmt)
# Add each handler to the Logger object
log.addHandler(stdoutHandler)
+28 -105
View File
@@ -1,22 +1,18 @@
import sentry_sdk
from datetime import timedelta
from typing import Optional
from fastapi import FastAPI, APIRouter, Security, HTTPException
from fastapi_jwt import JwtAuthorizationCredentials, JwtAccessBearerCookie, JwtRefreshBearerCookie
from fastapi import FastAPI, HTTPException, APIRouter
from graphql import GraphQLError
from sentry_sdk.integrations.fastapi import FastApiIntegration
from sentry_sdk.integrations.starlette import StarletteIntegration
from sentry_sdk.integrations.strawberry import StrawberryIntegration
from sqlalchemy.exc import NoResultFound
from starlette.background import BackgroundTasks
from starlette.middleware.cors import CORSMiddleware
from starlette.responses import Response, JSONResponse
from starlette.staticfiles import StaticFiles
from config import APP_SECRET_KEY, ALLOW_CORS_ORIGINS, SENTRY_DSN, REFRESH_TOKEN_VALIDITY_DAYS, APP_DEBUG
from config import ALLOW_CORS_ORIGINS, SENTRY_DSN
from endpoints.contact import ContactEndpoint, ContactInput
from endpoints.forgotten_password import ForgottenPasswordRequest, ForgottenPasswordEndpoint, ChangeForgottenPassword
from endpoints.graphql import setup_graphql_endpoint
from endpoints.login import LoginEndpoint, LoginInput, RefreshEndpoint, LogoutEndpoint
from endpoints.photo_editor_preview import PhotoEditorEndpoint
from endpoints.registration import RegistrationInput, RegistrationEndpoint
from endpoints.sitemap import SitemapEndpoint
from routers import forgotten_password, auth, photo_preview, graphql
class StaticFilesCache(StaticFiles):
@@ -31,24 +27,26 @@ class StaticFilesCache(StaticFiles):
class App:
api_router = APIRouter(dependencies=[])
access_security = JwtAccessBearerCookie(
secret_key=APP_SECRET_KEY,
auto_error=False,
access_expires_delta=timedelta(days=1) if APP_DEBUG else timedelta(minutes=20)
)
refresh_security = JwtRefreshBearerCookie(
secret_key=APP_SECRET_KEY,
auto_error=True,
refresh_expires_delta=timedelta(days=REFRESH_TOKEN_VALIDITY_DAYS),
)
def create_app(self):
if SENTRY_DSN:
fast_api_params = dict(
transaction_style="endpoint",
failed_request_status_codes={403, *range(500, 599)},
http_methods_to_capture=("GET", "POST"),
)
sentry_sdk.init(
dsn=SENTRY_DSN,
enable_tracing=True,
ignore_errors=[GraphQLError, HTTPException]
traces_sample_rate=0.5,
profiles_sample_rate=0.5,
ignore_errors=[GraphQLError, HTTPException],
integrations=[
StrawberryIntegration(async_execution=True),
StarletteIntegration(**fast_api_params),
FastApiIntegration(**fast_api_params),
]
)
app = FastAPI(
@@ -100,94 +98,19 @@ class App:
)
def setup_routes(self, app: FastAPI):
@self.api_router.post("/registration", status_code=201)
async def registration(user: RegistrationInput, background_tasks: BackgroundTasks):
return await RegistrationEndpoint().on_post(user, background_tasks)
api_router = APIRouter()
@self.api_router.post("/login")
async def login(resp: Response, user: LoginInput):
return await LoginEndpoint(
access_token=self.access_security,
refresh_token=self.refresh_security
).on_post(user, resp)
@self.api_router.post("/refresh", summary="Refresh access token")
async def refresh(
resp: Response,
credentials: JwtAuthorizationCredentials = Security(self.refresh_security)
):
return await RefreshEndpoint(
access_token=self.access_security,
refresh_token=self.refresh_security
).on_post(resp, credentials)
@self.api_router.post("/logout")
async def logout(resp: Response):
return await LogoutEndpoint(
access_token=self.access_security,
refresh_token=self.refresh_security
).on_post(resp)
@self.api_router.get(
"/forgotten-password/token/{token}",
summary="Info about token used for resetting password"
)
async def token_info(token: str):
return await ForgottenPasswordEndpoint().token_info(token)
@self.api_router.post(
"/forgotten-password/request",
summary="Request password change, e-mail will be sent to validate your request."
)
async def request_password_change(input: ForgottenPasswordRequest, background_tasks: BackgroundTasks):
return await ForgottenPasswordEndpoint().request(input, background_tasks)
@self.api_router.post(
"/forgotten-password/reset",
summary="Set new password after successfull token validation"
)
async def reset_password(input: ChangeForgottenPassword):
return await ForgottenPasswordEndpoint().change_password(input)
@self.api_router.post("/contact", summary="Send email from contact form")
@api_router.post("/contact", summary="Send email from contact form")
async def contact_form_message(input: ContactInput, background_tasks: BackgroundTasks):
return await ContactEndpoint().on_post(input, background_tasks)
@self.api_router.get("/sitemap.xml")
@api_router.get("/sitemap.xml")
async def sitemap():
return await SitemapEndpoint().on_get()
@self.api_router.get("/photo/editor-preview/{photo_id}", summary="Photo editor preview")
async def photo_editor_preview(
photo_id: int,
brightness: Optional[float] = None,
contrast: Optional[float] = None,
saturation: Optional[float] = None,
sharpness: Optional[float] = None,
rotate: Optional[float] = None,
crop_left: Optional[float] = None,
crop_top: Optional[float] = None,
crop_width: Optional[float] = None,
crop_height: Optional[float] = None,
):
return await PhotoEditorEndpoint(
access_token=self.access_security,
refresh_token=self.refresh_security
).show_preview(
photo_id=photo_id,
logged_user_id=0,
saturation=saturation,
brightness=brightness,
contrast=contrast,
sharpness=sharpness,
crop_top=crop_top,
crop_left=crop_left,
crop_height=crop_height,
crop_width=crop_width,
rotate=rotate,
)
setup_graphql_endpoint(app, self.access_security)
# musi byt na konci
app.include_router(self.api_router)
app.include_router(auth.router)
app.include_router(forgotten_password.router)
app.include_router(photo_preview.router)
app.include_router(graphql.router)
app.include_router(api_router)
+1 -6
View File
@@ -1,20 +1,15 @@
import os
from typing import Optional
from config import API_URL
from logger import log
PHOTO_BASE_PATH = ""
AIRCRAFT_BASE_PATH = ""
FLIGHT_BASE_PATH = ""
FLIGHT_GPX_TRACK_PATH = "/app/uploads/tracks"
AIRCRAFT_UPLOAD_DEST_PATH = "/app/uploads/aircrafts/"
def get_photo_basepath(flight_id: int) -> str:
return f"/app/uploads/photos/{flight_id}"
def get_public_url(filename: Optional[str]) -> str:
def get_public_url(filename: str | None) -> str:
return f"{API_URL}/uploads/{filename}" if filename else None
View File
+40
View File
@@ -0,0 +1,40 @@
from fastapi import BackgroundTasks, Security, APIRouter
from fastapi_jwt import JwtAuthorizationCredentials
from starlette.responses import Response
from endpoints.login import LoginInput, LoginEndpoint, RefreshEndpoint, LogoutEndpoint
from endpoints.registration import RegistrationInput, RegistrationEndpoint
from jwt import access_security, refresh_security
router = APIRouter()
@router.post("/registration", status_code=201)
async def registration(user: RegistrationInput, background_tasks: BackgroundTasks):
return await RegistrationEndpoint().on_post(user, background_tasks)
@router.post("/login")
async def login(resp: Response, user: LoginInput):
return await LoginEndpoint(
access_token=access_security,
refresh_token=refresh_security
).on_post(user, resp)
@router.post("/refresh", summary="Refresh access token")
async def refresh(
resp: Response,
credentials: JwtAuthorizationCredentials = Security(refresh_security)
):
return await RefreshEndpoint(
access_token=access_security,
refresh_token=refresh_security
).on_post(resp, credentials)
@router.post("/logout")
async def logout(resp: Response):
return await LogoutEndpoint(
access_token=access_security,
refresh_token=refresh_security
).on_post(resp)
+25
View File
@@ -0,0 +1,25 @@
from fastapi import BackgroundTasks, APIRouter
from endpoints.forgotten_password import ForgottenPasswordEndpoint, ForgottenPasswordRequest, ChangeForgottenPassword
router = APIRouter()
@router.get(
"/forgotten-password/token/{token}",
summary="Info about token used for resetting password"
)
async def token_info(token: str):
return await ForgottenPasswordEndpoint().token_info(token)
@router.post(
"/forgotten-password/request",
summary="Request password change, e-mail will be sent to validate your request."
)
async def request_password_change(input: ForgottenPasswordRequest, background_tasks: BackgroundTasks):
return await ForgottenPasswordEndpoint().request(input, background_tasks)
@router.post(
"/forgotten-password/reset",
summary="Set new password after successfull token validation"
)
async def reset_password(input: ChangeForgottenPassword):
return await ForgottenPasswordEndpoint().change_password(input)
+30
View File
@@ -0,0 +1,30 @@
from datetime import timedelta
from fastapi import APIRouter
from starlette.responses import RedirectResponse
from strawberry.fastapi import GraphQLRouter
from config import GRAPHIQL, APP_DEBUG
from graphql_schema.context import setup_graphql_context
from graphql_schema.schema import schema
from jwt import access_security
router = APIRouter()
if APP_DEBUG:
@router.get("/graphql/autologin")
async def autologin():
access_token = access_security.create_access_token(subject={"id": 1, "name": "Franta Vomacka"})
response = RedirectResponse(url="/graphql")
access_security.set_access_cookie(response, access_token, expires_delta=timedelta(days=14))
return response
gql_router = GraphQLRouter(
schema,
graphiql=GRAPHIQL,
debug=APP_DEBUG,
context_getter=setup_graphql_context,
multipart_uploads_enabled=True,
prefix="/graphql"
)
router.include_router(gql_router, tags=["login"])
+36
View File
@@ -0,0 +1,36 @@
from fastapi import APIRouter
from endpoints.photo_editor_preview import PhotoEditorEndpoint
from jwt import access_security, refresh_security
router = APIRouter()
@router.get("/photo/editor-preview/{photo_id}", summary="Photo editor preview")
async def photo_editor_preview(
photo_id: int,
brightness: float | None = None,
contrast: float | None = None,
saturation: float | None = None,
sharpness: float | None = None,
rotate: float | None = None,
crop_left: float | None = None,
crop_top: float | None = None,
crop_width: float | None = None,
crop_height: float | None = None,
):
return await PhotoEditorEndpoint(
access_token=access_security,
refresh_token=refresh_security
).show_preview(
photo_id=photo_id,
logged_user_id=0,
saturation=saturation,
brightness=brightness,
contrast=contrast,
sharpness=sharpness,
crop_top=crop_top,
crop_left=crop_left,
crop_height=crop_height,
crop_width=crop_width,
rotate=rotate,
)
+69
View File
@@ -0,0 +1,69 @@
import asyncio
import sys
from datetime import datetime
from itertools import groupby
from sqlalchemy import select
sys.path.insert(0, "/app/src")
from database import models
from database.transaction import get_session
def find_closest(needle: datetime, haystack, _best_difference: float = sys.maxsize):
if len(haystack) == 0:
return None
if len(haystack) == 1:
# nalezeno
return haystack[0]
index = len(haystack) / 2
diff = haystack[index].timestamp - needle
if diff < _best_difference:
_best_difference = diff
return find_closest(needle, haystack[:index], _best_difference)
else:
return find_closest(needle, haystack[index + 1:], _best_difference)
async def add_gps_to_photos():
async with get_session() as db:
photos = (await db.execute(
select(models.Photo, models.Photo.flight)
.join(models.Photo.flight)
.filter(models.Photo.gps_latitude.is_(None))
.filter(models.Photo.gps_longitude.is_(None))
)).all()
flight_ids = {photo.flight_id for photo, flight in photos}
tracks = (await db.execute(
select(models.Flight.track_id, models.Flight.id)
.select_from(models.Flight)
.join(models.Flight.track)
.filter(models.Flight.id.in_(flight_ids))
)).all()
track_id_to_flight_id = {track_id: flight_id for track_id, flight_id in tracks}
track_points_data = (await db.scalars(
select(models.TrackPoint)
.filter(models.TrackPoint.track_id.in_(track_id_to_flight_id.keys()))
.order_by(models.TrackPoint.timestamp)
)).all()
print(track_points_data)
grouped_points_by_track_id = groupby(track_points_data, key=lambda x: x.track_id)
for photo, flight in photos:
best_track_point = find_closest(photo.exposed_at, grouped_points_by_track_id[flight.track_id])
print(best_track_point)
break
print(grouped_points_by_track_id)
# tracks_by_flight_id = {track.flight_id: track_points for track, track_points in tracks}
if __name__ == "__main__":
loop = asyncio.get_event_loop()
loop.run_until_complete(add_gps_to_photos())
+55 -17
View File
@@ -1,22 +1,60 @@
from sqlalchemy import select
from database import models
from database.transaction import get_session
import asyncio
import sys
from collections import defaultdict
from typing import Any
from fastapi import BackgroundTasks
from sqlalchemy import select, Result
from sqlalchemy.ext.asyncio import AsyncSession
sys.path.insert(0, "/app/src")
from utils.flight_plan_helpers import refresh_markers_weather_info # noqa
from database.models import FlightPlan # noqa
from database import models # noqa
from database.transaction import get_session # noqa
class FlighPlanWeather:
@staticmethod
async def get_plans() -> list:
async with get_session() as db:
markers_without_weather = (await db.execute(
select(models.FlightPlanMarker).filter(models.FlightPlanMarker.weather_info_id.is_(None))
))
flight_plan_ids = {m.flight_plan_id for m in markers_without_weather}
async def get_plans(db: AsyncSession) -> list[Any] | Result[tuple[FlightPlan, Any]]:
markers_without_weather = (await db.scalars(
select(models.FlightPlanMarker).filter(models.FlightPlanMarker.weather_info_id.is_(None))
))
flight_plan_ids = {m.flight_plan_id for m in markers_without_weather}
flight_plan_ids.add(11)
if not flight_plan_ids:
return []
if not flight_plan_ids:
return []
return (await db.execute(
select(models.FlightPlan, models.FlightPlanMarker)
.join(models.FlightPlan.markers)
.filter(models.FlightPlan.planned_takeoff_datetime.is_not(None))
.filter(models.FlightPlan.id.in_(flight_plan_ids))
))
return (await db.execute(
select(models.FlightPlan, models.FlightPlan.markers)
.filter(models.FlightPlan.id.in_(flight_plan_ids))
))
async def download_flight_plan_weather():
tasks = BackgroundTasks()
async with get_session() as db:
plans_with_markers_without_weather = await get_plans(db)
markers_by_plan = defaultdict(list)
for plan, marker in plans_with_markers_without_weather:
markers_by_plan[plan].append(marker)
for plan, markers in markers_by_plan.items():
await refresh_markers_weather_info(
plan.planned_takeoff_datetime,
plan.planned_speed, markers,
background_tasks=tasks
)
await tasks()
async def run_all():
await asyncio.gather(download_flight_plan_weather())
if __name__ == "__main__":
loop = asyncio.get_event_loop()
loop.run_until_complete(run_all())
+31
View File
@@ -0,0 +1,31 @@
from datetime import datetime, timedelta
from fastapi import BackgroundTasks
from background_jobs.weather import download_weather_for_plan_marker
from database import models
from utils.gps import get_distance
async def refresh_markers_weather_info(
planned_takeoff_datetime: datetime, planned_speed: float,
markers: list[models.FlightPlanMarker], background_tasks: BackgroundTasks
):
if not planned_takeoff_datetime:
return
marker_date_time = planned_takeoff_datetime
previous_marker = markers[0] if markers else None
for marker in markers:
if marker_date_time:
# TODO: tohle by se melo volat i v pripade zmeny planovaneho casu vzletu i planovane rychlosti
distance_from_prev = get_distance(
a=(previous_marker.gps_latitude, previous_marker.gps_longitude),
b=(marker.gps_latitude, marker.gps_longitude)
)
time_from_prev_sec = distance_from_prev / planned_speed * 3600 # pracuji v km/h
marker_date_time = marker_date_time + timedelta(seconds=time_from_prev_sec)
background_tasks.add_task(
download_weather_for_plan_marker,
marker_id=marker.id, date_time=marker_date_time
)
previous_marker = marker
+1 -1
View File
@@ -24,7 +24,7 @@ def get_bounds(coordinates: list[dict[str, float]]) -> list[tuple[float, float]]
]
async def get_airport_id_by_gps(gps_lat: float, gps_lng: float) -> Optional[int]:
async def get_airport_id_by_gps(gps_lat: float, gps_lng: float) -> int | None:
async with get_session() as db:
query = (
select(models.Airport, func.coalesce(6371 * func.acos(
+7 -7
View File
@@ -38,7 +38,7 @@ class PhotoEditor:
self.img = Image.open(f"{path}/{filename}")
self.img_size = self.img.size
def resize(self, new_width: Optional[int] = None, new_height: Optional[int] = None):
def resize(self, new_width: int | None = None, new_height: int | None = None):
if not new_width and not new_height:
raise ValueError("Set either new_width or new_height")
@@ -102,10 +102,10 @@ class PhotoEditor:
def adjust(
self,
brightness: Optional[float] = None,
contrast: Optional[float] = None,
saturation: Optional[float] = None,
sharpness: Optional[float] = None
brightness: float | None = None,
contrast: float | None = None,
saturation: float | None = None,
sharpness: float | None = None
):
adjustments = [
(Brightness, brightness),
@@ -127,8 +127,8 @@ class PhotoEditor:
return img_io
def write_to_file(
self, quality: int = 90, dest_path: Optional[str] = None, dest_filename: Optional[str] = None,
format_: Optional[str] = "JPEG"
self, quality: int = 90, dest_path: str | None = None, dest_filename: str | None = None,
format_: str | None = "JPEG"
) -> str:
check_directories(dest_path or self.path)
+7
View File
@@ -12,3 +12,10 @@ def chunks(items: list | dict, n: int) -> typing.Generator:
it = iter(items)
for i in range(0, len(items), n):
yield {k: items[k] for k in islice(it, n)}
def evenly_spaced_elements(lst: list, count: int) -> list:
if count > len(lst):
return lst
interval = (len(lst) - 1) / (count - 1) if count > 1 else 0
return [lst[int(round(i * interval))] for i in range(count)]