Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e8f677b6f | ||
|
|
6553da41f2 | ||
|
|
420774f1b4 | ||
|
|
888351baac | ||
|
|
d794208193 | ||
|
|
71911c0905 | ||
|
|
a7fda95c29 | ||
|
|
2f824e264f | ||
|
|
beee16bf80 | ||
|
|
77c9242944 | ||
|
|
c6316262e5 | ||
|
|
d31f1c0c61 | ||
|
|
ca3f6ef07c | ||
|
|
186dbd0720 | ||
|
|
365a1fc1b0 | ||
|
|
0a6b19f334 | ||
|
|
3081580c21 | ||
|
|
94639da820 | ||
|
|
9e6b0b574c | ||
|
|
10bc0847b6 | ||
|
|
e541508f75 | ||
|
|
ac3b496459 | ||
|
|
2bf737b9d6 | ||
|
|
ba5e73108f | ||
|
|
94613a5b1b |
@@ -2,5 +2,6 @@
|
||||
max-line-length = 120
|
||||
exclude =
|
||||
venv
|
||||
.venv
|
||||
__pycache__
|
||||
alembic
|
||||
alembic
|
||||
|
||||
@@ -10,3 +10,5 @@ uploads/*
|
||||
!uploads/.gitkeep
|
||||
src/emails/templates/html/*
|
||||
.env
|
||||
/static/airports/
|
||||
/static/airports/.gitkeep
|
||||
|
||||
+9
-7
@@ -7,17 +7,19 @@ 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
|
||||
|
||||
# uzivatel www-data v alpine neexistuje
|
||||
RUN delgroup www-data && addgroup -g 33 -S www-data && adduser -u 33 -D -S -G www-data www-data
|
||||
|
||||
|
||||
RUN apk update && \
|
||||
apk add --no-cache tzdata mariadb-client mariadb-dev && \
|
||||
pip install --upgrade pip
|
||||
|
||||
# protoze xfs uzivatel ma id=33
|
||||
RUN deluser xfs
|
||||
|
||||
# uzivatel www-data v alpine neexistuje
|
||||
RUN delgroup www-data && addgroup -g 33 -S www-data && adduser -u 33 -D -S -G www-data www-data
|
||||
|
||||
COPY requirements.txt /app/requirements.txt
|
||||
COPY --from=email_builder /app/templates/html/ /app/src/email/templates/html/
|
||||
@@ -34,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" ]
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
"""add weather info to flight plan markers
|
||||
|
||||
Revision ID: 8a67af3909aa
|
||||
Revises: 790ba00fc021
|
||||
Create Date: 2024-05-16 06:19:13.112911
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '8a67af3909aa'
|
||||
down_revision = '790ba00fc021'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('flight_plan_marker', sa.Column('weather_info_id', sa.Integer(), nullable=True))
|
||||
op.create_foreign_key(None, 'flight_plan_marker', 'weather_info', ['weather_info_id'], ['id'])
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_constraint(None, 'flight_plan_marker', type_='foreignkey')
|
||||
op.drop_column('flight_plan_marker', 'weather_info_id')
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,28 @@
|
||||
"""add flight_plan.description
|
||||
|
||||
Revision ID: a3e1f1a37899
|
||||
Revises: 8a67af3909aa
|
||||
Create Date: 2024-06-11 16:46:29.786830
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'a3e1f1a37899'
|
||||
down_revision = '8a67af3909aa'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('flight_plan', sa.Column('description', sa.Text(), server_default='', nullable=False))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('flight_plan', 'description')
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,54 @@
|
||||
"""add track to flight
|
||||
|
||||
Revision ID: f9e1e8a01683
|
||||
Revises: a3e1f1a37899
|
||||
Create Date: 2024-06-24 16:08:01.084570
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'f9e1e8a01683'
|
||||
down_revision = 'a3e1f1a37899'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('track',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('bounds', mysql.JSON(), nullable=False),
|
||||
sa.Column('created_by_id', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
|
||||
sa.ForeignKeyConstraint(['created_by_id'], ['user.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('track_point',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('timestamp', sa.DateTime(), nullable=False),
|
||||
sa.Column('track_id', sa.Integer(), nullable=False),
|
||||
sa.Column('gps_latitude', sa.Float(), nullable=False),
|
||||
sa.Column('gps_longitude', sa.Float(), nullable=False),
|
||||
sa.Column('elevation', sa.Float(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['track_id'], ['track.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.add_column('flight', sa.Column('track_id', sa.Integer(), nullable=True))
|
||||
op.create_foreign_key(None, 'flight', 'track', ['track_id'], ['id'])
|
||||
op.add_column('flight_plan', sa.Column('track_id', sa.Integer(), nullable=True))
|
||||
op.create_foreign_key(None, 'flight_plan', 'track', ['track_id'], ['id'])
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_constraint(None, 'flight_plan', type_='foreignkey')
|
||||
op.drop_column('flight_plan', 'track_id')
|
||||
op.drop_constraint(None, 'flight', type_='foreignkey')
|
||||
op.drop_column('flight', 'track_id')
|
||||
op.drop_table('track_point')
|
||||
op.drop_table('track')
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,30 @@
|
||||
"""add flight.flight_plan_id fk
|
||||
|
||||
Revision ID: 989152b7184a
|
||||
Revises: f9e1e8a01683
|
||||
Create Date: 2024-06-27 17:09:19.933094
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '989152b7184a'
|
||||
down_revision = 'f9e1e8a01683'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('flight', sa.Column('flight_plan_id', sa.Integer(), nullable=True))
|
||||
op.create_foreign_key(None, 'flight', 'flight_plan', ['flight_plan_id'], ['id'])
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_constraint(None, 'flight', type_='foreignkey')
|
||||
op.drop_column('flight', 'flight_plan_id')
|
||||
# ### end Alembic commands ###
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
"""Add poi and airport id to flight plan track marker
|
||||
|
||||
Revision ID: 8cc01c03e980
|
||||
Revises: 989152b7184a
|
||||
Create Date: 2024-06-30 00:27:08.976891
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '8cc01c03e980'
|
||||
down_revision = '989152b7184a'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column('airport', 'openaip_id',
|
||||
existing_type=mysql.VARCHAR(length=32),
|
||||
nullable=True)
|
||||
op.add_column('flight_plan_marker', sa.Column('airport_id', sa.Integer(), nullable=True))
|
||||
op.add_column('flight_plan_marker', sa.Column('point_of_interest_id', sa.Integer(), nullable=True))
|
||||
op.create_foreign_key(None, 'flight_plan_marker', 'point_of_interest', ['point_of_interest_id'], ['id'])
|
||||
op.create_foreign_key(None, 'flight_plan_marker', 'airport', ['airport_id'], ['id'])
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_constraint(None, 'flight_plan_marker', type_='foreignkey')
|
||||
op.drop_constraint(None, 'flight_plan_marker', type_='foreignkey')
|
||||
op.drop_column('flight_plan_marker', 'point_of_interest_id')
|
||||
op.drop_column('flight_plan_marker', 'airport_id')
|
||||
op.alter_column('airport', 'openaip_id',
|
||||
existing_type=mysql.VARCHAR(length=32),
|
||||
nullable=False)
|
||||
# ### end Alembic commands ###
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
"""add speed, elevation, altitude to track
|
||||
|
||||
Revision ID: 03de3f7f8fdd
|
||||
Revises: 8cc01c03e980
|
||||
Create Date: 2024-07-11 07:45:59.376941
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '03de3f7f8fdd'
|
||||
down_revision = '8cc01c03e980'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('track', sa.Column('min_speed', sa.Float(), nullable=True))
|
||||
op.add_column('track', sa.Column('max_speed', sa.Float(), nullable=True))
|
||||
op.add_column('track', sa.Column('avg_speed', sa.Float(), nullable=True))
|
||||
op.add_column('track', sa.Column('max_altitude', sa.Float(), nullable=True))
|
||||
op.add_column('track', sa.Column('avg_altitude', sa.Float(), nullable=True))
|
||||
op.add_column('track', sa.Column('total_duration', sa.Integer(), nullable=True, comment='Total duration in seconds'))
|
||||
op.add_column('track_point', sa.Column('terrain_elevation', sa.Float(), nullable=True))
|
||||
op.add_column('track_point', sa.Column('speed', sa.Float(), nullable=True))
|
||||
op.add_column('track_point', sa.Column('altitude', sa.Float(), nullable=True))
|
||||
op.add_column('track_point', sa.Column('magnetic_variation', sa.Float(), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('track_point', 'magnetic_variation')
|
||||
op.drop_column('track_point', 'altitude')
|
||||
op.drop_column('track_point', 'speed')
|
||||
op.drop_column('track_point', 'terrain_elevation')
|
||||
op.drop_column('track', 'total_duration')
|
||||
op.drop_column('track', 'avg_altitude')
|
||||
op.drop_column('track', 'max_altitude')
|
||||
op.drop_column('track', 'avg_speed')
|
||||
op.drop_column('track', 'max_speed')
|
||||
op.drop_column('track', 'min_speed')
|
||||
# ### end Alembic commands ###
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
"""delete duplicated column with altitude
|
||||
|
||||
Revision ID: e3620deb41b6
|
||||
Revises: 03de3f7f8fdd
|
||||
Create Date: 2024-07-15 06:35:13.536320
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'e3620deb41b6'
|
||||
down_revision = '03de3f7f8fdd'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
|
||||
op.rename_table("flight_track", "flight_turn_point")
|
||||
op.create_index(op.f('ix_track_point_timestamp'), 'track_point', ['timestamp'], unique=False)
|
||||
op.drop_column('track_point', 'elevation')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('track_point', sa.Column('elevation', mysql.FLOAT(), nullable=True))
|
||||
op.drop_index(op.f('ix_track_point_timestamp'), table_name='track_point')
|
||||
op.rename_table("flight_turn_point", "flight_track")
|
||||
# ### end Alembic commands ###
|
||||
@@ -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 ###
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
@@ -12,3 +11,4 @@ services:
|
||||
SENTRY_DSN: "https://184d1d6bd39f4fbb94804ae84e9afdd5@o472821.ingest.sentry.io/5506983"
|
||||
volumes:
|
||||
- ./uploads:/app/uploads
|
||||
- ./static:/app/static
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -1,40 +1,34 @@
|
||||
from aiohttp import ClientResponseError
|
||||
from sqlalchemy import select
|
||||
from database import models
|
||||
from database.transaction import get_session
|
||||
from external.elevation import elevation_api
|
||||
from external.gpx_parser import GPXParser
|
||||
from paths import FLIGHT_GPX_TRACK_PATH
|
||||
from decorators.retryable import retryable
|
||||
from external.elevation import elevation_api, update_track_points_elevation
|
||||
|
||||
|
||||
async def add_terrain_elevation_to_flight(flight_id: int, gpx_filename: str):
|
||||
gpx_parser = GPXParser(f"{FLIGHT_GPX_TRACK_PATH}/{gpx_filename}")
|
||||
coordinates = await gpx_parser.get_coordinates_all()
|
||||
|
||||
try:
|
||||
elevation = await elevation_api.get_elevation_for_points(coordinates)
|
||||
tree_with_elevation = gpx_parser.add_terrain_elevation(elevation)
|
||||
output_name = f"terrain_{gpx_filename}"
|
||||
gpx_parser.write(tree_with_elevation, f"{FLIGHT_GPX_TRACK_PATH}/{output_name}")
|
||||
|
||||
async with get_session() as db:
|
||||
await models.Flight.update(
|
||||
db, {"gpx_track_filename": output_name, "has_terrain_elevation": True},
|
||||
id=flight_id
|
||||
)
|
||||
except ClientResponseError as e:
|
||||
print(e)
|
||||
@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)
|
||||
.filter(models.TrackPoint.terrain_elevation.is_(None))
|
||||
)).all()
|
||||
await update_track_points_elevation(db, track_points)
|
||||
|
||||
|
||||
@retryable
|
||||
async def add_terrain_elevation_to_photo(photo):
|
||||
try:
|
||||
elevation = await elevation_api.get_elevation_for_points([
|
||||
{"lat": photo.gps_latitude, "lng": photo.gps_longitude}
|
||||
])
|
||||
elevation = await elevation_api.get_elevation_for_points(
|
||||
[{"lat": photo.gps_latitude, "lng": photo.gps_longitude}]
|
||||
)
|
||||
if not elevation:
|
||||
print("Cannot get elevation")
|
||||
return
|
||||
|
||||
terrain_elevation = elevation[0]['elevation']
|
||||
terrain_elevation = elevation[0].elevation
|
||||
async with get_session() as db:
|
||||
await models.Photo.update(db_session=db, obj=photo, data={"terrain_elevation": terrain_elevation})
|
||||
except Exception as e:
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
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):
|
||||
# Load the base image
|
||||
base_image = Image.open(base_image_path)
|
||||
base_width, base_height = base_image.size
|
||||
|
||||
# Calculate the new dimensions for cropping to a 1.9:1 aspect ratio
|
||||
new_width = base_width
|
||||
new_height = int(new_width / 1.9)
|
||||
if new_height > base_height:
|
||||
new_height = base_height
|
||||
new_width = int(new_height * 1.9)
|
||||
|
||||
# Center crop the base image to the new dimensions
|
||||
left = (base_width - new_width) // 2
|
||||
top = (base_height - new_height) // 2
|
||||
right = (base_width + new_width) // 2
|
||||
bottom = (base_height + new_height) // 2
|
||||
base_image = base_image.crop((left, top, right, bottom))
|
||||
base_width, base_height = base_image.size
|
||||
|
||||
# Load the avatar image
|
||||
avatar_image = Image.open(avatar_image_path)
|
||||
avatar_size = int(base_height * 0.3) # Set the size of the avatar
|
||||
avatar_image = avatar_image.resize((avatar_size, avatar_size), Image.LANCZOS)
|
||||
|
||||
# Create a mask to make the avatar circular
|
||||
mask = Image.new('L', (avatar_size, avatar_size), 0)
|
||||
draw = ImageDraw.Draw(mask)
|
||||
draw.ellipse((0, 0, avatar_size, avatar_size), fill=255)
|
||||
|
||||
# Apply the mask to the avatar image
|
||||
avatar_image = ImageOps.fit(avatar_image, mask.size, centering=(0.5, 0.5))
|
||||
avatar_image.putalpha(mask)
|
||||
|
||||
# Create a border around the avatar
|
||||
border_size = int(avatar_size * 0.03)
|
||||
border_image = Image.new('RGBA', (avatar_size + 2 * border_size, avatar_size + 2 * border_size), (255, 255, 255, 0))
|
||||
border_mask = Image.new('L', (avatar_size + 2 * border_size, avatar_size + 2 * border_size), 0)
|
||||
border_draw = ImageDraw.Draw(border_mask)
|
||||
border_draw.ellipse((0, 0, avatar_size + 2 * border_size, avatar_size + 2 * border_size), fill=255)
|
||||
border_draw.ellipse((border_size, border_size, avatar_size + border_size, avatar_size + border_size), fill=0)
|
||||
|
||||
avatar_margin = int(avatar_size * 0.1)
|
||||
|
||||
border_image.putalpha(border_mask)
|
||||
border_image.paste(avatar_image, (border_size, border_size), avatar_image)
|
||||
|
||||
# Calculate the position to paste the avatar (bottom right corner)
|
||||
position = (base_width - avatar_size - 2 * border_size - avatar_margin, base_height - avatar_size - 2 * border_size - avatar_margin)
|
||||
|
||||
# Paste the avatar with border onto the base image
|
||||
base_image.paste(border_image, position, border_image)
|
||||
|
||||
# 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',
|
||||
)
|
||||
@@ -1,25 +1,20 @@
|
||||
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 download_weather(date_time: datetime, flight_id: int, airport_id: int, type_: Literal['landing', 'takeoff']):
|
||||
async with get_session() as db:
|
||||
airport = await models.Airport.get_one(db, id=airport_id)
|
||||
gps = (airport.gps_latitude, airport.gps_longitude)
|
||||
|
||||
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)
|
||||
log.warning(weather)
|
||||
except Exception as e:
|
||||
log.error(f"Error in downloading weather: {e}")
|
||||
return None
|
||||
|
||||
data = {
|
||||
"datetime": weather['datetime'],
|
||||
return {
|
||||
"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'],
|
||||
@@ -30,6 +25,14 @@ async def download_weather(date_time: datetime, flight_id: int, airport_id: int,
|
||||
"wind_direction_surface": weather['winddirection_10m'],
|
||||
}
|
||||
|
||||
|
||||
async def download_weather_for_flight(date_time: datetime, flight_id: int, airport_id: int, type_: Literal['landing', 'takeoff']):
|
||||
async with get_session() as db:
|
||||
airport = await models.Airport.get_one(db, id=airport_id)
|
||||
gps = (airport.gps_latitude, airport.gps_longitude)
|
||||
|
||||
data = await get_weather(date_time, gps)
|
||||
|
||||
async with get_session() as db:
|
||||
flight = await models.Flight.get_one(db, id=flight_id)
|
||||
existing_weather_id = getattr(flight, f'{type_}_weather_info_id')
|
||||
@@ -39,3 +42,29 @@ async def download_weather(date_time: datetime, flight_id: int, airport_id: int,
|
||||
else:
|
||||
weather_model = await models.WeatherInfo.create(db, data=data)
|
||||
await models.Flight.update(db, obj=flight, data={f"{type_}_weather_info_id": weather_model.id})
|
||||
|
||||
|
||||
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:
|
||||
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.FlightPlanMarker.update(
|
||||
db,
|
||||
id=marker_id,
|
||||
data={"weather_info_id": weather_model.id}
|
||||
)
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import os
|
||||
|
||||
APP_DEBUG = True
|
||||
APP_DEBUG = os.environ.get("APP_ENV", 'production') == 'development'
|
||||
GRAPHIQL = True
|
||||
|
||||
REFRESH_TOKEN_VALIDITY_DAYS = 30
|
||||
|
||||
@@ -1,454 +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')
|
||||
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['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'))
|
||||
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=False, 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
|
||||
|
||||
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()
|
||||
# notes: Mapped['AircraftNotes'] = relationship()
|
||||
|
||||
|
||||
# class AircraftNotes(BaseModel):
|
||||
# __tablename__ = "aircraft_notes"
|
||||
#
|
||||
# id: Mapped[int] = mapped_column(primary_key=True)
|
||||
# aircraft_id: Mapped[int] = mapped_column(Integer, ForeignKey("aircraft.id"), nullable=False)
|
||||
# name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||
# description: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
# is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
|
||||
# created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
||||
# created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||
#
|
||||
# created_by: Mapped['User'] = relationship()
|
||||
# aircraft: Mapped['Aircraft'] = 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 FlightTrack(BaseModel):
|
||||
__tablename__ = "flight_track"
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
gpx_track_filename: Mapped[str] = mapped_column(String(128), nullable=True)
|
||||
has_terrain_elevation: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||
aircraft_id: Mapped[int] = mapped_column(Integer, ForeignKey('aircraft.id'))
|
||||
takeoff_weather_info_id: Mapped[int] = mapped_column(Integer, ForeignKey('weather_info.id'), nullable=True)
|
||||
landing_weather_info_id: Mapped[int] = mapped_column(Integer, ForeignKey('weather_info.id'), nullable=True)
|
||||
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])
|
||||
track: Mapped['FlightTrack'] = 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])
|
||||
|
||||
|
||||
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()
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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())
|
||||
@@ -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
|
||||
@@ -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])
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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])
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,24 @@
|
||||
import asyncio
|
||||
|
||||
|
||||
def retryable(func):
|
||||
max_retries = 5
|
||||
|
||||
async def func_wrapper(*args, **kwargs):
|
||||
for i in range(1, max_retries):
|
||||
try:
|
||||
return await func(*args, **kwargs)
|
||||
except Exception as ex:
|
||||
if i > max_retries:
|
||||
raise ex
|
||||
sleep_time = i * 2
|
||||
print(
|
||||
"Error while getting the data from source:"
|
||||
" decorator is going to sleep for %s seconds"
|
||||
" and retry it again (%s/%s)" % (
|
||||
sleep_time, i, max_retries
|
||||
)
|
||||
)
|
||||
await asyncio.sleep(sleep_time)
|
||||
|
||||
return func_wrapper
|
||||
@@ -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,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>
|
||||
|
||||
@@ -1,53 +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 not APP_DEBUG:
|
||||
return
|
||||
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")
|
||||
@@ -33,6 +33,8 @@ class LoginEndpoint(AuthEndpoint):
|
||||
access_token = self.access_security.create_access_token(subject=subject)
|
||||
refresh_token = self.refresh_security.create_refresh_token(subject=subject)
|
||||
|
||||
# TODO: SameSite na refresh_token_cookie!
|
||||
# https://medium.com/swlh/how-the-new-chrome-80-cookie-rule-samesite-none-secure-affects-web-development-c06380220ced
|
||||
# self.access_security.set_access_cookie(resp, access_token)
|
||||
self.refresh_security.set_refresh_cookie(
|
||||
resp, refresh_token,
|
||||
|
||||
@@ -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")
|
||||
|
||||
Vendored
+73
-13
@@ -1,27 +1,87 @@
|
||||
from typing import List, Dict
|
||||
import dataclasses
|
||||
from typing import Optional
|
||||
import aiohttp
|
||||
|
||||
from database import models
|
||||
from decorators.retryable import retryable
|
||||
from utils.list_utils import chunks
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ElevationInfo:
|
||||
id: int | None
|
||||
lat: float
|
||||
lng: float
|
||||
elevation: float
|
||||
|
||||
|
||||
class ElevationAPI:
|
||||
ELEVATION_ENDPOINT = "https://api.open-elevation.com/api/v1/lookup"
|
||||
ELEVATION_ENDPOINT: str
|
||||
|
||||
def get_request(self, points: List[Dict[str, float]]):
|
||||
return {"locations": [{"latitude": point['lat'], "longitude": point['lng']} for point in points]}
|
||||
def get_request(self, session: aiohttp.ClientSession, points: list[dict[str, float]]):
|
||||
...
|
||||
|
||||
async def call_api(self, points: List[Dict[str, float]]):
|
||||
@retryable
|
||||
async def call_api(self, points: list[dict[str, float]]):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(self.ELEVATION_ENDPOINT, json=self.get_request(points)) as resp:
|
||||
async with self.get_request(session, points) as resp:
|
||||
resp.raise_for_status()
|
||||
return await resp.json()
|
||||
|
||||
async def get_elevation_for_points(self, points: List[Dict[str, float]]) -> List[dict]:
|
||||
|
||||
class OpenElevationAPI(ElevationAPI):
|
||||
ELEVATION_ENDPOINT = "https://api.open-elevation.com/api/v1/lookup"
|
||||
|
||||
def get_request(self, session: aiohttp.ClientSession, points: list[dict[str, float]]):
|
||||
data = {"locations": [{"latitude": point['lat'], "longitude": point['lng']} for point in points]}
|
||||
return session.post(self.ELEVATION_ENDPOINT, json=data)
|
||||
|
||||
async def get_elevation_for_points(self, points: list[dict[str, float]]) -> list[ElevationInfo]:
|
||||
response = await self.call_api(points)
|
||||
|
||||
return [{
|
||||
"lat": loc['latitude'],
|
||||
"lng": loc['longitude'],
|
||||
"elevation": loc['elevation']
|
||||
} for loc in response['results']]
|
||||
elevation = []
|
||||
for i, point in enumerate(points):
|
||||
response_point = response['results'][i]
|
||||
elevation.append(ElevationInfo(**point, elevation=response_point['elevation']))
|
||||
return elevation
|
||||
|
||||
|
||||
elevation_api = ElevationAPI()
|
||||
class OpenMeteoElevationAPI(ElevationAPI):
|
||||
ELEVATION_ENDPOINT = "https://api.open-meteo.com/v1/elevation"
|
||||
|
||||
def get_request(self, session: aiohttp.ClientSession, points: list[dict[str, float]]):
|
||||
latitudes = ",".join(str(point['lat']) for point in points)
|
||||
longitudes = ",".join(str(point['lng']) for point in points)
|
||||
url = f"{self.ELEVATION_ENDPOINT}?latitude={latitudes}&longitude={longitudes}"
|
||||
|
||||
return session.get(url)
|
||||
|
||||
async def get_elevation_for_points(self, points: list[dict[str, float]]) -> list[ElevationInfo]:
|
||||
elevation = []
|
||||
for chunk in chunks(points, n=100):
|
||||
response = await self.call_api(chunk)
|
||||
chunk_elevation = []
|
||||
for i, point in enumerate(chunk):
|
||||
chunk_elevation.append(ElevationInfo(**point, elevation=response['elevation'][i]))
|
||||
|
||||
elevation += chunk_elevation
|
||||
return elevation
|
||||
|
||||
|
||||
class CombinedElevationAPI:
|
||||
async def get_elevation_for_points(self, points: list[dict[str, float]]) -> list[ElevationInfo]:
|
||||
for api in (OpenMeteoElevationAPI(), OpenElevationAPI()):
|
||||
elevation = await api.get_elevation_for_points(points)
|
||||
|
||||
if elevation:
|
||||
return elevation
|
||||
|
||||
|
||||
async def update_track_points_elevation(db, track_points: list[models.TrackPoint]):
|
||||
points = [{"id": p.id, "lat": p.gps_latitude, "lng": p.gps_longitude} for p in track_points]
|
||||
elevation = await elevation_api.get_elevation_for_points(points)
|
||||
for point in elevation:
|
||||
await models.TrackPoint.update(db, {"terrain_elevation": point.elevation}, id=point.id)
|
||||
|
||||
|
||||
elevation_api = CombinedElevationAPI()
|
||||
|
||||
Vendored
-34
@@ -1,34 +0,0 @@
|
||||
import math
|
||||
|
||||
EARTH_RADIUS = 6378.1 # Radius of the Earth
|
||||
|
||||
|
||||
def get_bearing(center_point: tuple[float, float], end_point: tuple[float, float]):
|
||||
lat3 = math.radians(end_point[0])
|
||||
long3 = math.radians(end_point[1])
|
||||
lat1 = math.radians(center_point[0])
|
||||
long1 = math.radians(center_point[1])
|
||||
|
||||
dLon = long3 - long1
|
||||
|
||||
X = math.cos(lat3) * math.sin(dLon)
|
||||
Y = math.cos(lat1) * math.sin(lat3) - math.sin(lat1) * math.cos(lat3) * math.cos(dLon)
|
||||
|
||||
end_brng = math.atan2(X, Y)
|
||||
|
||||
return end_brng
|
||||
|
||||
|
||||
def get_distance(a: tuple[float, float], b: tuple[float, float]):
|
||||
lat1 = math.radians(a[0])
|
||||
lon1 = math.radians(a[1])
|
||||
lat2 = math.radians(b[0])
|
||||
lon2 = math.radians(b[1])
|
||||
|
||||
dlon = lon2 - lon1
|
||||
dlat = lat2 - lat1
|
||||
|
||||
a = math.sin(dlat / 2) ** 2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2) ** 2
|
||||
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
|
||||
|
||||
return EARTH_RADIUS * c
|
||||
Vendored
+8
-42
@@ -1,9 +1,7 @@
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Dict, Any
|
||||
from aiocache import cached
|
||||
from lxml import etree
|
||||
from lxml.etree import _ElementTree
|
||||
|
||||
|
||||
class GPXParser:
|
||||
@@ -68,50 +66,47 @@ class GPXParser:
|
||||
def run_xpath(self, path: str):
|
||||
return self.gpx.xpath(path, namespaces=self.namespace)
|
||||
|
||||
@cached()
|
||||
async def get_times_all(self):
|
||||
nodes = self.run_xpath("//gpx:trkpt/gpx:time")
|
||||
return [datetime.fromisoformat(node.text).astimezone() for node in nodes]
|
||||
|
||||
@cached()
|
||||
async def get_times(self):
|
||||
times = await self.get_times_all()
|
||||
return await self.sample_times(times)
|
||||
|
||||
@cached()
|
||||
async def get_total_duration(self) -> timedelta:
|
||||
times = await self.get_times_all()
|
||||
return times[-1] - times[0]
|
||||
|
||||
async def get_coordinates(self) -> List[Dict[str, float]]:
|
||||
return await self.average_coordinates(await self.get_coordinates_all())
|
||||
|
||||
@cached()
|
||||
async def get_coordinates_all(self) -> List[Dict[str, float]]:
|
||||
nodes = self.run_xpath("//gpx:trkpt")
|
||||
return [{"lat": float(node.attrib["lat"]), "lng": float(node.attrib['lon'])} for node in nodes]
|
||||
|
||||
@cached()
|
||||
async def get_speed(self) -> List[float]:
|
||||
nodes = self.run_xpath("//gpx:speed")
|
||||
return await self.average_sample_numbers([float(node.text) for node in nodes])
|
||||
|
||||
@cached()
|
||||
async def get_magnetic_variation(self) -> List[float]:
|
||||
nodes = self.run_xpath("//gpx:magvar")
|
||||
return await self.average_sample_numbers([int(node.text) for node in nodes])
|
||||
|
||||
@cached()
|
||||
async def get_altitude(self) -> List[float]:
|
||||
nodes = self.run_xpath("//gpx:ele")
|
||||
return await self.average_sample_numbers([float(node.text) for node in nodes])
|
||||
|
||||
@cached()
|
||||
async def get_terrain_elevation(self) -> List[float]:
|
||||
nodes = self.run_xpath("//gpx:terrain_elevation")
|
||||
return await self.average_sample_numbers([float(node.text) for node in nodes])
|
||||
|
||||
@cached()
|
||||
async def get_max_speed(self):
|
||||
return max(await self.get_speed()) or 0
|
||||
|
||||
@cached()
|
||||
async def get_min_speed(self):
|
||||
return min(await self.get_speed()) or 0
|
||||
|
||||
async def get_avg_speed(self):
|
||||
speeds = await self.get_speed()
|
||||
if not speeds:
|
||||
@@ -119,38 +114,9 @@ class GPXParser:
|
||||
|
||||
return round(sum(speeds) / len(speeds), 2)
|
||||
|
||||
@cached()
|
||||
async def get_max_altitude(self):
|
||||
return max(await self.get_altitude()) or 0
|
||||
|
||||
@cached()
|
||||
async def get_avg_altitude(self):
|
||||
altitudes = await self.get_altitude()
|
||||
return round(sum(altitudes) / len(altitudes), 2)
|
||||
|
||||
def add_terrain_elevation(self, points_with_elevation: List[Dict[str, float]]):
|
||||
track_points = self.run_xpath("//gpx:trkpt")
|
||||
# TODO: open elevation API umi jen presnost na 6 desetinnych mist!
|
||||
track_points_index = {(float(n.attrib['lat']), float(n.attrib['lon'])): n for n in track_points}
|
||||
|
||||
for point_with_elevation in points_with_elevation:
|
||||
lat = point_with_elevation['lat']
|
||||
lng = point_with_elevation['lng']
|
||||
elevation = point_with_elevation['elevation']
|
||||
target_node = track_points_index.get((lat, lng))
|
||||
|
||||
if target_node is None:
|
||||
continue
|
||||
|
||||
extensions = target_node.find("./extensions", self.gpx.getroot().nsmap)
|
||||
if extensions is not None:
|
||||
extensions.append(etree.XML(f"<terrain_elevation>{elevation}</terrain_elevation>"))
|
||||
else:
|
||||
target_node.append(
|
||||
etree.XML(f"<extensions><terrain_elevation>{elevation}</terrain_elevation></extensions>")
|
||||
)
|
||||
|
||||
return self.gpx
|
||||
|
||||
def write(self, tree: _ElementTree, output: str):
|
||||
tree.write(output)
|
||||
|
||||
Vendored
+2
-2
@@ -8,7 +8,7 @@ import aiohttp
|
||||
from sqlalchemy import delete
|
||||
from database import models
|
||||
from database.transaction import get_session
|
||||
from external.gps_utils import get_distance, get_bearing, EARTH_RADIUS
|
||||
from utils.gps import get_distance, get_bearing, EARTH_RADIUS
|
||||
|
||||
|
||||
# https://pyopenair.readthedocs.io/en/latest/openair.html
|
||||
@@ -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: [])
|
||||
|
||||
|
||||
|
||||
Vendored
+10
-6
@@ -3,6 +3,7 @@ import urllib.parse
|
||||
from typing import Tuple, Dict
|
||||
import aiohttp
|
||||
from aiocache import cached
|
||||
from decorators.retryable import retryable
|
||||
|
||||
|
||||
class Weather:
|
||||
@@ -38,13 +39,17 @@ class Weather:
|
||||
return f"{url}{query_string}"
|
||||
|
||||
@cached(ttl=6 * 3600)
|
||||
@retryable
|
||||
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]
|
||||
@@ -52,10 +57,9 @@ class Weather:
|
||||
data = await self.download_weather_for_day(date_time.date(), gps)
|
||||
|
||||
# TODO: kontrola timezone!
|
||||
# TODO: interpolace - udelat vazenyprumer z dvou po sobe jdoucich hodin
|
||||
idx = date_time.hour
|
||||
result_data = {metric: data['hourly'][metric][idx] for metric in self.METRICS}
|
||||
result_data['datetime'] = datetime.datetime.strptime(data['hourly']['time'][idx], "%Y-%m-%dT%H:%M")
|
||||
result_data['date_time'] = datetime.datetime.strptime(data['hourly']['time'][idx], "%Y-%m-%dT%H:%M")
|
||||
|
||||
return result_data
|
||||
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
@@ -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()
|
||||
|
||||
@@ -10,8 +10,8 @@ async def load_flight_durations(ids: List[int]):
|
||||
select(
|
||||
models.Flight.id,
|
||||
func.timediff(models.Flight.landing_datetime, models.Flight.takeoff_datetime).label("diff"),
|
||||
func.coalesce(func.sum(models.FlightTrack.landing_duration), 0).label("landing_duration")
|
||||
).join(models.Flight.track, isouter=True)
|
||||
func.coalesce(func.sum(models.FlightTurnPoint.landing_duration), 0).label("landing_duration")
|
||||
).join(models.Flight.turn_points, isouter=True)
|
||||
.group_by(models.Flight.id)
|
||||
.filter(models.Flight.id.in_(ids))
|
||||
|
||||
|
||||
@@ -47,6 +47,24 @@ reporting_points_dataloader = DataLoader(
|
||||
).load, cache=False
|
||||
)
|
||||
|
||||
runways_dataloader = DataLoader(
|
||||
load_fn=MultiModelsDataloader(
|
||||
models.Runway,
|
||||
relationship_column=models.Airport.id,
|
||||
extra_join=[models.Airport.runways],
|
||||
order_by=[models.Runway.designator]
|
||||
).load, cache=False
|
||||
)
|
||||
|
||||
frequencies_dataloader = DataLoader(
|
||||
load_fn=MultiModelsDataloader(
|
||||
models.Frequency,
|
||||
relationship_column=models.Airport.id,
|
||||
extra_join=[models.Airport.frequencies],
|
||||
order_by=[models.Frequency.frequency]
|
||||
).load, cache=False
|
||||
)
|
||||
|
||||
flights_by_copilot_dataloader = DataLoader(
|
||||
load_fn=MultiModelsDataloader(
|
||||
models.Flight,
|
||||
@@ -80,7 +98,7 @@ flight_by_poi_dataloader = DataLoader(
|
||||
models.Flight,
|
||||
relationship_column=models.PointOfInterest.id,
|
||||
order_by=[models.Flight.takeoff_datetime.desc()],
|
||||
extra_join=[models.Flight.track, models.PointOfInterest]
|
||||
extra_join=[models.Flight.turn_points, models.PointOfInterest]
|
||||
).load,
|
||||
cache=False
|
||||
)
|
||||
@@ -137,11 +155,20 @@ poi_photos_dataloader = DataLoader(
|
||||
).load,
|
||||
cache=False
|
||||
)
|
||||
flight_track_dataloader = DataLoader(
|
||||
flight_turn_points_dataloader = DataLoader(
|
||||
load_fn=MultiModelsDataloader(
|
||||
models.FlightTrack,
|
||||
relationship_column=models.FlightTrack.flight_id,
|
||||
order_by=[models.FlightTrack.order]
|
||||
models.FlightTurnPoint,
|
||||
relationship_column=models.FlightTurnPoint.flight_id,
|
||||
order_by=[models.FlightTurnPoint.order]
|
||||
).load,
|
||||
cache=False
|
||||
)
|
||||
|
||||
track_points_dataloder = DataLoader(
|
||||
load_fn=MultiModelsDataloader(
|
||||
models.TrackPoint,
|
||||
relationship_column=models.TrackPoint.track_id,
|
||||
order_by=[models.TrackPoint.timestamp, models.TrackPoint.id]
|
||||
).load,
|
||||
cache=False
|
||||
)
|
||||
|
||||
@@ -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,8 @@ 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)
|
||||
flight_dataloader = create_dataloader(models.Flight)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,59 +1,45 @@
|
||||
from typing import Optional
|
||||
import strawberry
|
||||
from graphql import GraphQLError
|
||||
|
||||
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:
|
||||
@@ -68,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)
|
||||
|
||||
@@ -1,43 +1,45 @@
|
||||
from typing import List
|
||||
import strawberry
|
||||
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.resolvers.base import BaseQueryResolver
|
||||
from graphql_schema.entities.resolvers.flight_plan import FlightPlanMutationResolver
|
||||
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 BaseQueryResolver(FlightPlan, models.FlightPlan).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
|
||||
async def flight_plan(root, info: Info, id: int) -> FlightPlan:
|
||||
return await BaseQueryResolver(FlightPlan, models.FlightPlan).get_one(
|
||||
@strawberry.field(extensions=[AllowPublicAccess()])
|
||||
async def flight_plan(
|
||||
root,
|
||||
info: Info,
|
||||
id: int,
|
||||
username: str | None = None,
|
||||
public: bool | None = False
|
||||
) -> FlightPlan:
|
||||
filter_params = get_detail_filters(id)
|
||||
if username:
|
||||
filter_params['username'] = username
|
||||
|
||||
return await FlightPlanQueryResolver().get_one(
|
||||
user_id=info.context.user_id,
|
||||
object_id=id
|
||||
only_public=public,
|
||||
**filter_params
|
||||
)
|
||||
|
||||
|
||||
@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], url_slug: Optional[str]) -> 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]
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
):
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import asyncio
|
||||
from typing import List, Optional
|
||||
from sqlalchemy import delete, insert, select, func, text
|
||||
import random
|
||||
from typing import Optional
|
||||
from sqlalchemy import delete, insert, or_, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from strawberry.file_uploads import Upload
|
||||
from background_jobs.elevation import add_terrain_elevation_to_flight
|
||||
from background_jobs.weather import download_weather
|
||||
from 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
|
||||
from database.models import flight_has_copilot
|
||||
from database.transaction import get_session
|
||||
from external.gpx_parser import GPXParser
|
||||
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 (
|
||||
@@ -16,9 +18,8 @@ from graphql_schema.entities.types.mutation_input import (
|
||||
)
|
||||
from graphql_schema.entities.types.types import Flight
|
||||
from paths import FLIGHT_GPX_TRACK_PATH
|
||||
from utils.file import delete_file
|
||||
from utils.file import handle_file_upload
|
||||
from utils.str_utils import random_str
|
||||
from utils.upload import handle_file_upload
|
||||
|
||||
|
||||
class FlightQueryResolver(BaseQueryResolver):
|
||||
@@ -27,18 +28,12 @@ class FlightQueryResolver(BaseQueryResolver):
|
||||
|
||||
def get_query(
|
||||
self,
|
||||
user_id: Optional[int] = None,
|
||||
object_id: Optional[int] = None,
|
||||
url_slug: Optional[str] = None,
|
||||
only_public: Optional[bool] = False,
|
||||
user_id: int | None = None,
|
||||
only_public: bool | None = False,
|
||||
*args,
|
||||
**kwargs
|
||||
):
|
||||
filters = {}
|
||||
if object_id:
|
||||
filters['object_id'] = object_id
|
||||
if url_slug:
|
||||
filters['url_slug'] = url_slug
|
||||
filters = {key: kwargs[key] for key in ("object_id", "url_slug") if key in kwargs}
|
||||
|
||||
query = super().get_query(
|
||||
user_id,
|
||||
@@ -62,8 +57,27 @@ class FlightQueryResolver(BaseQueryResolver):
|
||||
|
||||
if kwargs.get("point_of_interest_id"):
|
||||
query = (
|
||||
query.join(models.Flight.track)
|
||||
.filter(models.FlightTrack.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'):
|
||||
@@ -79,55 +93,13 @@ class FlightMutationResolver(BaseMutationResolver):
|
||||
def __init__(self):
|
||||
super().__init__(Flight, models.Flight)
|
||||
|
||||
async def get_airport_id_by_gps(self, gps_lat: float, gps_lng: float) -> Optional[int]:
|
||||
async with (get_session() as db):
|
||||
query = (
|
||||
select(models.Airport, func.coalesce(6371 * func.acos(
|
||||
func.cos(func.radians(gps_lat)) *
|
||||
func.cos(func.radians(models.Airport.gps_latitude)) *
|
||||
func.cos(func.radians(models.Airport.gps_longitude) - func.radians(gps_lng)) +
|
||||
func.sin(func.radians(gps_lat)) *
|
||||
func.sin(func.radians(models.Airport.gps_latitude))
|
||||
), 9999).label("distance"))
|
||||
.filter(models.Airport.use_in_gpx_guess.is_(True))
|
||||
.order_by("distance")
|
||||
.having(text("distance < 1"))
|
||||
.limit(1)
|
||||
)
|
||||
|
||||
data = (await db.execute(query)).one_or_none()
|
||||
if data:
|
||||
airport, distance = data
|
||||
return airport.id
|
||||
|
||||
return None
|
||||
|
||||
async def extract_data_from_gpx(self, gpx_filename: str) -> dict:
|
||||
data = GPXParser(f"{FLIGHT_GPX_TRACK_PATH}/{gpx_filename}")
|
||||
|
||||
times, coordinates = await asyncio.gather(
|
||||
data.get_times(),
|
||||
data.get_coordinates()
|
||||
)
|
||||
takeoff_airport_id, landing_airport_id = await asyncio.gather(
|
||||
self.get_airport_id_by_gps(coordinates[0]['lat'], coordinates[0]['lng']),
|
||||
self.get_airport_id_by_gps(coordinates[-1]['lat'], coordinates[-1]['lng']),
|
||||
)
|
||||
|
||||
return {
|
||||
"takeoff_airport_id": takeoff_airport_id,
|
||||
"landing_airport_id": landing_airport_id,
|
||||
"takeoff_datetime": times[0],
|
||||
"landing_datetime": times[-1],
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
if input.gpx_track_file:
|
||||
data['gpx_track_filename'] = await handle_upload_gpx(gpx_track=input.gpx_track_file, context=context)
|
||||
data_from_gpx = await self.extract_data_from_gpx(data['gpx_track_filename'])
|
||||
data['gpx_track_filename'] = await handle_file_upload(input.gpx_track_file, FLIGHT_GPX_TRACK_PATH)
|
||||
data_from_gpx = await extract_basic_flight_info_from_gpx(data['gpx_track_filename'])
|
||||
data.update(data_from_gpx)
|
||||
else:
|
||||
async with get_session() as db:
|
||||
@@ -148,26 +120,23 @@ class FlightMutationResolver(BaseMutationResolver):
|
||||
data.update({
|
||||
"aircraft_id": aircraft_id,
|
||||
"has_terrain_elevation": False,
|
||||
"name": "",
|
||||
"name": input.name or '',
|
||||
"description": "",
|
||||
"created_by_id": context.user_id
|
||||
})
|
||||
flight = await self._do_create(db, data)
|
||||
|
||||
context.background_tasks.add_task(
|
||||
download_weather,
|
||||
flight_id=flight.id, airport_id=flight.takeoff_airport_id, date_time=flight.takeoff_datetime,
|
||||
type_="takeoff"
|
||||
)
|
||||
context.background_tasks.add_task(
|
||||
download_weather,
|
||||
flight_id=flight.id, airport_id=flight.landing_airport_id, date_time=flight.landing_datetime,
|
||||
type_="landing"
|
||||
)
|
||||
if input.track is not None:
|
||||
await handle_track_edit(db=db, flight_id=flight.id, track=input.track, user_id=user_id)
|
||||
|
||||
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)
|
||||
@@ -177,40 +146,23 @@ class FlightMutationResolver(BaseMutationResolver):
|
||||
data = input.to_dict()
|
||||
|
||||
if input.gpx_track_file is not None:
|
||||
data['gpx_track_filename'] = await handle_upload_gpx(
|
||||
gpx_track=input.gpx_track_file,
|
||||
context=context,
|
||||
original_gpx_filename=flight_data['gpx_track_filename']
|
||||
)
|
||||
data['gpx_track_filename'] = await handle_upload_gpx(gpx_track=input.gpx_track_file, flight_id=flight_id)
|
||||
|
||||
async with get_session() as db:
|
||||
if input.takeoff_airport:
|
||||
takeoff_airport_id = await handle_combobox_save(
|
||||
data['takeoff_airport_id'] = await handle_combobox_save(
|
||||
db, models.Airport, input.takeoff_airport, user_id, name_column="icao_code",
|
||||
extra_data={"name": input.takeoff_airport.name}
|
||||
)
|
||||
data['takeoff_airport_id'] = takeoff_airport_id
|
||||
data['takeoff_datetime'] = input.takeoff_datetime or flight_data['takeoff_datetime']
|
||||
|
||||
context.background_tasks.add_task(
|
||||
download_weather, flight_id=id, airport_id=takeoff_airport_id, date_time=data['takeoff_datetime'],
|
||||
type_="takeoff"
|
||||
)
|
||||
|
||||
if input.landing_airport:
|
||||
landing_airport_id = await handle_combobox_save(
|
||||
data['landing_airport_id'] = await handle_combobox_save(
|
||||
db, models.Airport, input.landing_airport, user_id, name_column="icao_code",
|
||||
extra_data={"name": input.landing_airport.name}
|
||||
)
|
||||
|
||||
data['landing_airport_id'] = landing_airport_id
|
||||
data['landing_datetime'] = input.landing_datetime or flight_data['landing_datetime']
|
||||
|
||||
context.background_tasks.add_task(
|
||||
download_weather, flight_id=id, airport_id=landing_airport_id, date_time=data['landing_datetime'],
|
||||
type_="landing"
|
||||
)
|
||||
|
||||
if input.aircraft is not None:
|
||||
data['aircraft_id'] = await handle_aircraft_save(db, user_id, input.aircraft)
|
||||
|
||||
@@ -239,21 +191,36 @@ class FlightMutationResolver(BaseMutationResolver):
|
||||
token=random_str(64)
|
||||
))
|
||||
|
||||
return await self._do_update(db, flight_data, data)
|
||||
flight_model = await self._do_update(db, flight_data, data)
|
||||
|
||||
schedule_background_tasks(flight_id, data, context)
|
||||
|
||||
return flight_model
|
||||
|
||||
|
||||
async def handle_upload_gpx(gpx_track: Upload, context, original_gpx_filename: Optional[str] = None):
|
||||
if original_gpx_filename:
|
||||
delete_file(FLIGHT_GPX_TRACK_PATH + "/" + original_gpx_filename, silent=True)
|
||||
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)
|
||||
|
||||
filename = await handle_file_upload(gpx_track, FLIGHT_GPX_TRACK_PATH)
|
||||
context.background_tasks.add_task(add_terrain_elevation_to_flight, flight_id=id, gpx_filename=filename)
|
||||
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']
|
||||
)
|
||||
|
||||
return filename
|
||||
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):
|
||||
await db.execute(delete(models.FlightTrack).filter(models.FlightTrack.flight_id == flight_id))
|
||||
async def handle_track_edit(db: AsyncSession, flight_id: int, track: list[TrackItemInput], user_id: int):
|
||||
await db.execute(delete(models.FlightTurnPoint).filter(models.FlightTurnPoint.flight_id == flight_id))
|
||||
|
||||
order = 0
|
||||
for item in track:
|
||||
@@ -262,17 +229,26 @@ async def handle_track_edit(db: AsyncSession, flight_id: int, track: List[TrackI
|
||||
|
||||
if item.point_of_interest:
|
||||
poi_id = await handle_combobox_save(
|
||||
db, models.PointOfInterest, item.point_of_interest, user_id, extra_data={"description": ""}
|
||||
db, models.PointOfInterest, item.point_of_interest, user_id, extra_data={
|
||||
"description": "",
|
||||
'gps_latitude': item.gps_latitude,
|
||||
'gps_longitude': item.gps_longitude
|
||||
}
|
||||
)
|
||||
|
||||
if item.airport:
|
||||
airport_id = await handle_combobox_save(
|
||||
db, models.Airport, item.airport, user_id,
|
||||
name_column="icao_code",
|
||||
extra_data={"name": item.airport.name}
|
||||
extra_data={
|
||||
"name": item.airport.name,
|
||||
'gps_latitude': item.gps_latitude,
|
||||
'gps_longitude': item.gps_longitude,
|
||||
'openaip_id': f'custom-{random.randint(0, 999999)}'
|
||||
}
|
||||
)
|
||||
|
||||
await models.FlightTrack.create(
|
||||
await models.FlightTurnPoint.create(
|
||||
db,
|
||||
data={
|
||||
"flight_id": flight_id,
|
||||
|
||||
@@ -1,69 +1,78 @@
|
||||
import asyncio
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
class FlightPlanQueryResolver(BaseQueryResolver):
|
||||
def __init__(self):
|
||||
super().__init__(graphql_type=FlightPlan, model=models.FlightPlan)
|
||||
|
||||
def get_query(
|
||||
self,
|
||||
user_id: int | None = None,
|
||||
only_public: bool | None = False,
|
||||
object_id: int | None = None,
|
||||
*args,
|
||||
**kwargs
|
||||
):
|
||||
filters = {"object_id": object_id} if object_id else {}
|
||||
|
||||
query = super().get_query(
|
||||
user_id,
|
||||
**filters,
|
||||
order_by=[models.FlightPlan.planned_takeoff_datetime.desc(), models.FlightPlan.id.desc()],
|
||||
only_public=only_public,
|
||||
only_my=not only_public
|
||||
)
|
||||
if kwargs.get('username'):
|
||||
query = (
|
||||
query.join(models.FlightPlan.created_by)
|
||||
.filter(models.User.public_username == kwargs['username'])
|
||||
)
|
||||
|
||||
if kwargs.get("search"):
|
||||
search = kwargs["search"]
|
||||
query = (
|
||||
query.filter(models.FlightPlan.name.contains(search))
|
||||
)
|
||||
|
||||
return query
|
||||
|
||||
|
||||
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:
|
||||
await models.FlightPlanMarker.create(db, data={
|
||||
"position": position,
|
||||
"flight_plan_id": flight_plan.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)
|
||||
@@ -71,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,
|
||||
@@ -96,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 = (
|
||||
|
||||
@@ -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
|
||||
@@ -16,7 +19,7 @@ from graphql_schema.entities.types.types import Photo
|
||||
from paths import get_photo_basepath
|
||||
from utils.file import delete_file
|
||||
from utils.image import PhotoEditor, parse_exif_info
|
||||
from utils.upload import handle_file_upload
|
||||
from utils.file import handle_file_upload
|
||||
|
||||
|
||||
class PhotoQueryResolver(BaseQueryResolver):
|
||||
@@ -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,18 +138,23 @@ class EditFlightInput(BaseGraphqlInputType):
|
||||
@strawberry.input()
|
||||
class CreateFlightInput(BaseGraphqlInputType):
|
||||
aircraft: ComboboxInput
|
||||
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
|
||||
takeoff_datetime: Optional[datetime] = None
|
||||
landing_datetime: Optional[datetime] = None
|
||||
track: Optional[List[TrackItemInput]] = None
|
||||
|
||||
|
||||
@strawberry.input()
|
||||
class TrackItemInput:
|
||||
point_of_interest: Optional[ComboboxInput] = None
|
||||
airport: Optional[ComboboxInput] = None
|
||||
landing_duration: Optional[int] = 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'])
|
||||
|
||||
@@ -1,30 +1,28 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
import strawberry
|
||||
from database import models
|
||||
from decorators.endpoints import authenticated_user_only
|
||||
from external.gps_utils import get_bearing, get_distance
|
||||
# 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
|
||||
from graphql_schema.dataloaders.multi_models import (
|
||||
poi_photos_dataloader, flight_by_poi_dataloader, flight_copilots_dataloader, flight_track_dataloader,
|
||||
poi_photos_dataloader, flight_by_poi_dataloader, flight_copilots_dataloader, flight_turn_points_dataloader,
|
||||
photos_dataloader, flights_by_aircraft_dataloader, users_in_organization_dataloader,
|
||||
aircrafts_from_organization_dataloader, user_organizations_dataloader, flights_by_event_dataloader,
|
||||
flights_by_copilot_dataloader, public_flights_by_event_dataloader, public_flights_by_copilot_dataloader,
|
||||
photo_copilots_dataloader, photos_aircraft_dataloader, copilots_in_photo_dataloader, flight_plan_markers_dataloader,
|
||||
reporting_points_dataloader, flight_plan_copilots_dataloader
|
||||
reporting_points_dataloader, flight_plan_copilots_dataloader, runways_dataloader, frequencies_dataloader, track_points_dataloder
|
||||
)
|
||||
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,
|
||||
photo_dataloader, user_dataloader
|
||||
weather_info_loader, organizations_dataloader, flight_dataloader, photo_adjustment_dataloader,
|
||||
photo_dataloader, user_dataloader, track_dataloader
|
||||
)
|
||||
from graphql_schema.permissions import IsAuthenticated
|
||||
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type
|
||||
from paths import get_avatar_url, get_title_image_url, get_photo_thumbnail_url, get_photo_url, FLIGHT_GPX_TRACK_PATH
|
||||
from paths import get_avatar_url, get_title_image_url, get_photo_thumbnail_url, get_photo_url, FLIGHT_GPX_TRACK_PATH, airport_icon_url, get_public_url
|
||||
|
||||
|
||||
@strawberry.type
|
||||
@@ -33,18 +31,6 @@ class Point:
|
||||
lng: float
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class GPXTrack:
|
||||
coordinates: List[Point]
|
||||
speed: List[float]
|
||||
altitude: List[float]
|
||||
magnetic_variation: List[float]
|
||||
terrain_elevation: List[float]
|
||||
time: List[datetime]
|
||||
max_speed: float
|
||||
avg_speed: float
|
||||
max_altitude: float
|
||||
avg_altitude: float
|
||||
|
||||
|
||||
@strawberry_sqlalchemy_type(models.ReportingPoint)
|
||||
@@ -52,14 +38,24 @@ class ReportingPoint:
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_sqlalchemy_type(models.Runway)
|
||||
class Runway:
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_sqlalchemy_type(models.Frequency)
|
||||
class Frequency:
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_sqlalchemy_type(models.Airport)
|
||||
class Airport:
|
||||
reporting_points: List[ReportingPoint] = strawberry.field(
|
||||
resolver=lambda root: reporting_points_dataloader.load(root.id)
|
||||
)
|
||||
|
||||
# TODO: loader na ikonu
|
||||
# TODO: loader na RWY a frekvence
|
||||
runways: List[Runway] = strawberry.field(resolver=lambda root: runways_dataloader.load(root.id))
|
||||
frequencies: List[Frequency] = strawberry.field(resolver=lambda root: frequencies_dataloader.load(root.id))
|
||||
icon_url: str = strawberry.field(resolver=lambda root: airport_icon_url(root.icao_code))
|
||||
|
||||
|
||||
@strawberry.type
|
||||
@@ -78,8 +74,8 @@ class Airspace:
|
||||
)
|
||||
|
||||
|
||||
@strawberry_sqlalchemy_type(models.FlightTrack)
|
||||
class FlightTrack:
|
||||
@strawberry_sqlalchemy_type(models.FlightTurnPoint)
|
||||
class FlightTurnPoint:
|
||||
point_of_interest: Optional[PointOfInterest] = strawberry.field(
|
||||
resolver=lambda root: poi_dataloader.load(root.point_of_interest_id)
|
||||
)
|
||||
@@ -123,6 +119,20 @@ class Photo:
|
||||
)
|
||||
|
||||
|
||||
@strawberry_sqlalchemy_type(models.TrackPoint)
|
||||
class TrackPoint:
|
||||
coordinates: Point = strawberry.field(resolver=lambda root: Point(lat=root.gps_latitude, lng=root.gps_longitude))
|
||||
|
||||
|
||||
@strawberry_sqlalchemy_type(models.Track, exclude_fields=['bounds'])
|
||||
class Track:
|
||||
bounds: list[tuple[float, float]]
|
||||
map_bounds: list[Point] = strawberry.field(
|
||||
resolver=lambda root: [Point(lat=point[1], lng=point[0]) for point in root.bounds]
|
||||
)
|
||||
points: list[TrackPoint] = strawberry.field(resolver=lambda root: track_points_dataloder.load(root.id))
|
||||
|
||||
|
||||
@strawberry_sqlalchemy_type(models.Flight)
|
||||
class Flight:
|
||||
def __init__(self, **kwargs):
|
||||
@@ -131,33 +141,11 @@ class Flight:
|
||||
for key, value in kwargs.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
async def load_gpx_track(root):
|
||||
if not root.gpx_track_filename:
|
||||
return None
|
||||
|
||||
try:
|
||||
gpx_parser = GPXParser(f"{FLIGHT_GPX_TRACK_PATH}/{root.gpx_track_filename}")
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
return GPXTrack(
|
||||
coordinates=[Point(**point) for point in await gpx_parser.get_coordinates()],
|
||||
speed=await gpx_parser.get_speed(),
|
||||
altitude=await gpx_parser.get_altitude(),
|
||||
terrain_elevation=await gpx_parser.get_terrain_elevation(),
|
||||
time=await gpx_parser.get_times(),
|
||||
max_speed=await gpx_parser.get_max_speed(),
|
||||
avg_speed=await gpx_parser.get_avg_speed(),
|
||||
max_altitude=await gpx_parser.get_max_altitude(),
|
||||
avg_altitude=await gpx_parser.get_avg_altitude(),
|
||||
magnetic_variation=await gpx_parser.get_magnetic_variation(),
|
||||
)
|
||||
|
||||
@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)
|
||||
|
||||
@@ -172,20 +160,20 @@ class Flight:
|
||||
resolver=lambda root: airport_dataloader.load(root.landing_airport_id)
|
||||
)
|
||||
title_photo: Optional[Photo] = strawberry.field(resolver=lambda root: photo_dataloader.load(root.title_photo_id))
|
||||
track: List[FlightTrack] = strawberry.field(resolver=lambda root: flight_track_dataloader.load(root.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))
|
||||
gpx_track: Optional[GPXTrack] = strawberry.field(resolver=load_gpx_track)
|
||||
duration_min_calculated: int = strawberry.field(
|
||||
resolver=lambda root: flight_duration_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: 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
|
||||
)
|
||||
@@ -193,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]
|
||||
|
||||
@@ -228,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)
|
||||
|
||||
@@ -257,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
|
||||
)
|
||||
@@ -283,8 +273,11 @@ 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: str | None = strawberry.field(
|
||||
resolver=lambda root: get_public_url(f'profile/{root.id}/title_photo.jpg')
|
||||
)
|
||||
organizations: List[Organization] = strawberry.field(
|
||||
resolver=lambda root: user_organizations_dataloader.load(root.id)
|
||||
)
|
||||
@@ -292,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
|
||||
|
||||
@@ -3,22 +3,21 @@ 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.upload import handle_file_upload
|
||||
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)
|
||||
@@ -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
@@ -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
@@ -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
-104
@@ -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
|
||||
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,23 +27,26 @@ class StaticFilesCache(StaticFiles):
|
||||
|
||||
|
||||
class App:
|
||||
api_router = APIRouter(dependencies=[])
|
||||
access_security = JwtAccessBearerCookie(
|
||||
secret_key=APP_SECRET_KEY,
|
||||
auto_error=False,
|
||||
)
|
||||
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(
|
||||
@@ -99,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)
|
||||
|
||||
+5
-6
@@ -1,23 +1,22 @@
|
||||
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
|
||||
|
||||
|
||||
def airport_icon_url(icao_code: str) -> str:
|
||||
return f"{API_URL}/static/airports/{icao_code}.png"
|
||||
|
||||
|
||||
def get_photo_url(root) -> str:
|
||||
filename = root.filename if not root.filename_extension else f"{root.filename}.{root.filename_extension}"
|
||||
return get_public_url(f"photos/{root.flight_id}/{filename}?cache={root.cache_key}")
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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"])
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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())
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user