Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10b7163f23 |
@@ -10,5 +10,3 @@ uploads/*
|
||||
!uploads/.gitkeep
|
||||
src/emails/templates/html/*
|
||||
.env
|
||||
/static/airports/
|
||||
/static/airports/.gitkeep
|
||||
|
||||
+5
-7
@@ -9,17 +9,15 @@ RUN mjml /app/templates/* -o /app/templates/html/
|
||||
|
||||
FROM python:3.11-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/
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
"""add flight_plan table
|
||||
|
||||
Revision ID: 7a3738a805c5
|
||||
Revises: aba4c3876b28
|
||||
Create Date: 2024-03-21 16:42:08.428875
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '7a3738a805c5'
|
||||
down_revision = 'aba4c3876b28'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('flight_plan',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=128), nullable=False),
|
||||
sa.Column('markers', mysql.JSON(), nullable=False),
|
||||
sa.Column('is_public', sa.Boolean(), server_default='0', nullable=False),
|
||||
sa.Column('created_by_id', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
|
||||
sa.Column('deleted', sa.Boolean(), server_default='0', nullable=False),
|
||||
sa.ForeignKeyConstraint(['created_by_id'], ['user.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('flight_plan')
|
||||
# ### end Alembic commands ###
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
"""make markers in flight planner separate table
|
||||
|
||||
Revision ID: de70fc471699
|
||||
Revises: 7a3738a805c5
|
||||
Create Date: 2024-03-24 21:55:12.307588
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'de70fc471699'
|
||||
down_revision = '7a3738a805c5'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('flight_plan_marker',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('flight_plan_id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=128), nullable=False),
|
||||
sa.Column('gps_latitude', sa.Float(), nullable=True),
|
||||
sa.Column('gps_longitude', sa.Float(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['flight_plan_id'], ['flight_plan.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.drop_column('flight_plan', 'markers')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('flight_plan', sa.Column('markers', mysql.LONGTEXT(charset='utf8mb4', collation='utf8mb4_bin'), nullable=False))
|
||||
op.drop_table('flight_plan_marker')
|
||||
# ### end Alembic commands ###
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
"""add type and position to flight_plan_marker
|
||||
|
||||
Revision ID: 70152c47cb49
|
||||
Revises: de70fc471699
|
||||
Create Date: 2024-03-24 22:00:35.234920
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '70152c47cb49'
|
||||
down_revision = 'de70fc471699'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('flight_plan_marker', sa.Column('type', sa.String(length=64), nullable=False))
|
||||
op.add_column('flight_plan_marker', sa.Column('position', sa.Integer(), nullable=False))
|
||||
op.alter_column('flight_plan_marker', 'gps_latitude',
|
||||
existing_type=mysql.FLOAT(),
|
||||
nullable=False)
|
||||
op.alter_column('flight_plan_marker', 'gps_longitude',
|
||||
existing_type=mysql.FLOAT(),
|
||||
nullable=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column('flight_plan_marker', 'gps_longitude',
|
||||
existing_type=mysql.FLOAT(),
|
||||
nullable=True)
|
||||
op.alter_column('flight_plan_marker', 'gps_latitude',
|
||||
existing_type=mysql.FLOAT(),
|
||||
nullable=True)
|
||||
op.drop_column('flight_plan_marker', 'position')
|
||||
op.drop_column('flight_plan_marker', 'type')
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,116 +0,0 @@
|
||||
"""reworked airspace and airport db structure
|
||||
|
||||
Revision ID: cbdb018c5490
|
||||
Revises: 70152c47cb49
|
||||
Create Date: 2024-04-21 22:32:50.058166
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'cbdb018c5490'
|
||||
down_revision = '70152c47cb49'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('airspace',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('country', sa.String(length=8), nullable=False),
|
||||
sa.Column('name', sa.String(length=128), nullable=False),
|
||||
sa.Column('type', sa.String(length=8), nullable=False),
|
||||
sa.Column('lower_limit', sa.String(length=32), nullable=False),
|
||||
sa.Column('upper_limit', sa.String(length=32), nullable=False),
|
||||
sa.Column('center', mysql.JSON(), nullable=False),
|
||||
sa.Column('radius', sa.Float(), nullable=True),
|
||||
sa.Column('bounds', mysql.JSON(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('name')
|
||||
)
|
||||
op.create_index(op.f('ix_airspace_country'), 'airspace', ['country'], unique=False)
|
||||
op.create_index(op.f('ix_airspace_type'), 'airspace', ['type'], unique=False)
|
||||
op.create_table('frequency',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('airport_id', sa.Integer(), nullable=True),
|
||||
sa.Column('airspace_id', sa.Integer(), nullable=True),
|
||||
sa.Column('frequency', sa.String(length=7), nullable=False),
|
||||
sa.Column('name', sa.String(length=64), nullable=False),
|
||||
sa.ForeignKeyConstraint(['airport_id'], ['airport.id'], ),
|
||||
sa.ForeignKeyConstraint(['airspace_id'], ['airspace.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('runway',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('airport_id', sa.Integer(), nullable=False),
|
||||
sa.Column('heading', sa.Integer(), nullable=False),
|
||||
sa.Column('designator', sa.String(length=4), nullable=False),
|
||||
sa.Column('surface', sa.Enum('asphalt', 'concrete', 'grass', 'other'), server_default='other', nullable=False),
|
||||
sa.Column('length', sa.Integer(), nullable=True),
|
||||
sa.Column('width', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['airport_id'], ['airport.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.drop_table('metar')
|
||||
op.drop_table('license')
|
||||
op.drop_index('ix_airspace_item_country', table_name='airspace_item')
|
||||
op.drop_index('ix_airspace_item_type', table_name='airspace_item')
|
||||
op.drop_index('name', table_name='airspace_item')
|
||||
op.drop_table('airspace_item')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('airspace_item',
|
||||
sa.Column('id', mysql.INTEGER(display_width=11), autoincrement=True, nullable=False),
|
||||
sa.Column('country', mysql.VARCHAR(length=8), nullable=False),
|
||||
sa.Column('name', mysql.VARCHAR(length=128), nullable=False),
|
||||
sa.Column('type', mysql.VARCHAR(length=8), nullable=False),
|
||||
sa.Column('lower_limit', mysql.VARCHAR(length=32), nullable=False),
|
||||
sa.Column('upper_limit', mysql.VARCHAR(length=32), nullable=False),
|
||||
sa.Column('center', mysql.LONGTEXT(charset='utf8mb4', collation='utf8mb4_bin'), nullable=False),
|
||||
sa.Column('radius', mysql.FLOAT(), nullable=True),
|
||||
sa.Column('bounds', mysql.LONGTEXT(charset='utf8mb4', collation='utf8mb4_bin'), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
mysql_collate='utf8mb4_general_ci',
|
||||
mysql_default_charset='utf8mb4',
|
||||
mysql_engine='InnoDB'
|
||||
)
|
||||
op.create_index('name', 'airspace_item', ['name'], unique=True)
|
||||
op.create_index('ix_airspace_item_type', 'airspace_item', ['type'], unique=False)
|
||||
op.create_index('ix_airspace_item_country', 'airspace_item', ['country'], unique=False)
|
||||
op.create_table('license',
|
||||
sa.Column('id', mysql.INTEGER(display_width=11), autoincrement=True, nullable=False),
|
||||
sa.Column('name', mysql.VARCHAR(length=128), nullable=False),
|
||||
sa.Column('number', mysql.VARCHAR(length=30), nullable=False),
|
||||
sa.Column('valid_until', mysql.DATETIME(), nullable=False),
|
||||
sa.Column('created_by_id', mysql.INTEGER(display_width=11), autoincrement=False, nullable=False),
|
||||
sa.Column('created_at', mysql.DATETIME(), server_default=sa.text('current_timestamp()'), nullable=False),
|
||||
sa.ForeignKeyConstraint(['created_by_id'], ['user.id'], name='license_ibfk_1'),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
mysql_collate='utf8mb4_general_ci',
|
||||
mysql_default_charset='utf8mb4',
|
||||
mysql_engine='InnoDB'
|
||||
)
|
||||
op.create_table('metar',
|
||||
sa.Column('id', mysql.INTEGER(display_width=11), autoincrement=True, nullable=False),
|
||||
sa.Column('airport_id', mysql.INTEGER(display_width=11), autoincrement=False, nullable=False),
|
||||
sa.Column('metar', mysql.TEXT(), nullable=False),
|
||||
sa.Column('issued_at', mysql.DATETIME(), nullable=False),
|
||||
sa.Column('deleted', mysql.TINYINT(display_width=1), server_default=sa.text('0'), autoincrement=False, nullable=False),
|
||||
sa.ForeignKeyConstraint(['airport_id'], ['airport.id'], name='metar_ibfk_1'),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
mysql_collate='utf8mb4_general_ci',
|
||||
mysql_default_charset='utf8mb4',
|
||||
mysql_engine='InnoDB'
|
||||
)
|
||||
op.drop_table('runway')
|
||||
op.drop_table('frequency')
|
||||
op.drop_index(op.f('ix_airspace_type'), table_name='airspace')
|
||||
op.drop_index(op.f('ix_airspace_country'), table_name='airspace')
|
||||
op.drop_table('airspace')
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,32 +0,0 @@
|
||||
"""remove old airspace columns, add altitude unit
|
||||
|
||||
Revision ID: e07a5c0f1dcc
|
||||
Revises: cbdb018c5490
|
||||
Create Date: 2024-04-22 23:42:52.140389
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'e07a5c0f1dcc'
|
||||
down_revision = 'cbdb018c5490'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('airspace', sa.Column('altitude_unit', sa.String(length=8), nullable=False))
|
||||
op.drop_column('airspace', 'center')
|
||||
op.drop_column('airspace', 'radius')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('airspace', sa.Column('radius', mysql.FLOAT(), nullable=True))
|
||||
op.add_column('airspace', sa.Column('center', mysql.LONGTEXT(charset='utf8mb4', collation='utf8mb4_bin'), nullable=False))
|
||||
op.drop_column('airspace', 'altitude_unit')
|
||||
# ### end Alembic commands ###
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
"""add alt unit for lower/upper limits in airspaces
|
||||
|
||||
Revision ID: 2012eca075c7
|
||||
Revises: e07a5c0f1dcc
|
||||
Create Date: 2024-04-22 23:49:17.725111
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '2012eca075c7'
|
||||
down_revision = 'e07a5c0f1dcc'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('airspace', sa.Column('lower_alt_unit', sa.String(length=8), nullable=False))
|
||||
op.add_column('airspace', sa.Column('upper_alt_unit', sa.String(length=8), nullable=False))
|
||||
op.drop_column('airspace', 'altitude_unit')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('airspace', sa.Column('altitude_unit', mysql.VARCHAR(length=8), nullable=False))
|
||||
op.drop_column('airspace', 'upper_alt_unit')
|
||||
op.drop_column('airspace', 'lower_alt_unit')
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,45 +0,0 @@
|
||||
"""add table for reporting points
|
||||
|
||||
Revision ID: 03ba4dfea911
|
||||
Revises: 2012eca075c7
|
||||
Create Date: 2024-04-23 21:39:18.254033
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '03ba4dfea911'
|
||||
down_revision = '2012eca075c7'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('reporting_point',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('openaip_id', sa.String(length=32), nullable=False),
|
||||
sa.Column('name', sa.String(length=64), nullable=False),
|
||||
sa.Column('gps_latitude', sa.Float(), nullable=True),
|
||||
sa.Column('gps_longitude', sa.Float(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('airport_has_reporting_point',
|
||||
sa.Column('airport_id', sa.Integer(), nullable=False),
|
||||
sa.Column('reporting_point_id', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['airport_id'], ['airport.id'], ),
|
||||
sa.ForeignKeyConstraint(['reporting_point_id'], ['reporting_point.id'], ),
|
||||
sa.PrimaryKeyConstraint('airport_id', 'reporting_point_id')
|
||||
)
|
||||
op.add_column('airport', sa.Column('openaip_id', sa.String(length=32), nullable=False))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('airport', 'openaip_id')
|
||||
op.drop_table('airport_has_reporting_point')
|
||||
op.drop_table('reporting_point')
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,30 +0,0 @@
|
||||
"""remove obsolete airport columns
|
||||
|
||||
Revision ID: f7ab4bdac987
|
||||
Revises: 03ba4dfea911
|
||||
Create Date: 2024-04-24 06:24:48.219726
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'f7ab4bdac987'
|
||||
down_revision = '03ba4dfea911'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('airport', 'deleted')
|
||||
op.drop_column('airport', 'is_public')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('airport', sa.Column('is_public', mysql.TINYINT(display_width=1), server_default=sa.text('0'), autoincrement=False, nullable=False))
|
||||
op.add_column('airport', sa.Column('deleted', mysql.TINYINT(display_width=1), server_default=sa.text('0'), autoincrement=False, nullable=False))
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,56 +0,0 @@
|
||||
"""add flight plan meta information
|
||||
|
||||
Revision ID: 790ba00fc021
|
||||
Revises: f7ab4bdac987
|
||||
Create Date: 2024-04-29 16:40:09.829317
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '790ba00fc021'
|
||||
down_revision = 'f7ab4bdac987'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('flight_plan_has_copilot',
|
||||
sa.Column('flight_plan_id', sa.Integer(), nullable=False),
|
||||
sa.Column('copilot_id', sa.Integer(), nullable=False),
|
||||
sa.Column('token', sa.String(length=64), nullable=True),
|
||||
sa.ForeignKeyConstraint(['copilot_id'], ['copilot.id'], ),
|
||||
sa.ForeignKeyConstraint(['flight_plan_id'], ['flight_plan.id'], ),
|
||||
sa.PrimaryKeyConstraint('flight_plan_id', 'copilot_id'),
|
||||
sa.UniqueConstraint('token')
|
||||
)
|
||||
op.add_column('flight_plan', sa.Column('is_default_name', sa.Boolean(), server_default='0', nullable=False))
|
||||
op.add_column('flight_plan', sa.Column('is_editable_by_copilot', sa.Boolean(), server_default='0', nullable=False))
|
||||
op.add_column('flight_plan', sa.Column('planned_speed', sa.Integer(), nullable=False))
|
||||
op.add_column('flight_plan', sa.Column('planned_takeoff_datetime', sa.DateTime(), nullable=True))
|
||||
op.add_column('flight_plan', sa.Column('aircraft_id', sa.Integer(), nullable=True))
|
||||
op.alter_column('flight_plan', 'name',
|
||||
existing_type=mysql.VARCHAR(length=128),
|
||||
type_=sa.String(length=256),
|
||||
existing_nullable=False)
|
||||
op.create_foreign_key(None, 'flight_plan', 'aircraft', ['aircraft_id'], ['id'])
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_constraint(None, 'flight_plan', type_='foreignkey')
|
||||
op.alter_column('flight_plan', 'name',
|
||||
existing_type=sa.String(length=256),
|
||||
type_=mysql.VARCHAR(length=128),
|
||||
existing_nullable=False)
|
||||
op.drop_column('flight_plan', 'aircraft_id')
|
||||
op.drop_column('flight_plan', 'planned_takeoff_datetime')
|
||||
op.drop_column('flight_plan', 'planned_speed')
|
||||
op.drop_column('flight_plan', 'is_editable_by_copilot')
|
||||
op.drop_column('flight_plan', 'is_default_name')
|
||||
op.drop_table('flight_plan_has_copilot')
|
||||
# ### end Alembic commands ###
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
"""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 ###
|
||||
@@ -1,28 +0,0 @@
|
||||
"""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 ###
|
||||
@@ -1,54 +0,0 @@
|
||||
"""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 ###
|
||||
@@ -1,30 +0,0 @@
|
||||
"""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
@@ -1,40 +0,0 @@
|
||||
"""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
@@ -1,46 +0,0 @@
|
||||
"""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
@@ -1,33 +0,0 @@
|
||||
"""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,4 @@
|
||||
pytest
|
||||
mypy
|
||||
flake8
|
||||
pytest-asyncio
|
||||
@@ -7,9 +7,8 @@ services:
|
||||
network_mode: "host"
|
||||
restart: "always"
|
||||
env_file:
|
||||
- ../.env
|
||||
- .env
|
||||
environment:
|
||||
SENTRY_DSN: "https://184d1d6bd39f4fbb94804ae84e9afdd5@o472821.ingest.sentry.io/5506983"
|
||||
volumes:
|
||||
- ./uploads:/app/uploads
|
||||
- ./static:/app/static
|
||||
|
||||
+21
-16
@@ -3,8 +3,10 @@ services:
|
||||
image: mariadb:10.11
|
||||
ports:
|
||||
- 3306:3306
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- MYSQL_USER=root
|
||||
- MYSQL_ROOT_PASSWORD=root
|
||||
- MYSQL_DATABASE=ull_tracker
|
||||
volumes:
|
||||
# - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
|
||||
- db-data:/var/lib/mysql:rw
|
||||
@@ -26,20 +28,23 @@ services:
|
||||
ALLOW_CORS_ORIGINS: "http://localhost:9000 http://localhost:9001 http://localhost:9101 http://localhost:3000"
|
||||
SENTRY_DSN: "https://184d1d6bd39f4fbb94804ae84e9afdd5@o472821.ingest.sentry.io/5506983"
|
||||
|
||||
# adminer:
|
||||
# image: adminer
|
||||
# restart: always
|
||||
# ports:
|
||||
# - 8080:8080
|
||||
#
|
||||
# scrapy:
|
||||
# build:
|
||||
# context: ../crawlers
|
||||
# env_file:
|
||||
# - .env
|
||||
# volumes:
|
||||
# - db-data:/var/lib/mysql:rw
|
||||
# - ../crawlers:/app
|
||||
adminer:
|
||||
image: adminer
|
||||
restart: always
|
||||
ports:
|
||||
- 8080:8080
|
||||
|
||||
scrapy:
|
||||
build:
|
||||
context: ../crawlers
|
||||
environment:
|
||||
- MYSQL_USER=root
|
||||
- MYSQL_PASSWORD=root
|
||||
- MYSQL_DATABASE=ull_tracker
|
||||
- MYSQL_HOST=db
|
||||
volumes:
|
||||
- db-data:/var/lib/mysql:rw
|
||||
- ../crawlers:/app
|
||||
|
||||
volumes:
|
||||
db-data: { }
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
[pytest]
|
||||
asyncio_mode = auto
|
||||
@@ -14,4 +14,3 @@ exif==1.6.0
|
||||
aiocache==0.12.2
|
||||
aiohttp==3.9.3
|
||||
lxml==5.1.0
|
||||
geopy==2.4.1
|
||||
|
||||
@@ -1,34 +1,40 @@
|
||||
from sqlalchemy import select
|
||||
from aiohttp import ClientResponseError
|
||||
from database import models
|
||||
from database.transaction import get_session
|
||||
from decorators.retryable import retryable
|
||||
from external.elevation import elevation_api, update_track_points_elevation
|
||||
from external.elevation import elevation_api
|
||||
from external.gpx_parser import GPXParser
|
||||
from paths import FLIGHT_GPX_TRACK_PATH
|
||||
|
||||
|
||||
@retryable
|
||||
async def add_terrain_elevation_to_flight(flight_id: int):
|
||||
async with get_session() as db:
|
||||
track_points = (
|
||||
await db.scalars(
|
||||
select(models.TrackPoint)
|
||||
.join(models.TrackPoint.track)
|
||||
.join(models.Track.flight)
|
||||
.filter(models.Flight.id == flight_id))
|
||||
).all()
|
||||
await update_track_points_elevation(db, track_points)
|
||||
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()
|
||||
|
||||
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_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:
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
from datetime import datetime, timedelta
|
||||
from sqlalchemy import select, delete
|
||||
from database import models
|
||||
from database.transaction import get_session
|
||||
from utils.gps import generate_points
|
||||
|
||||
|
||||
async def generate_track_for_flight_plan(flight_plan_id: int):
|
||||
async with (get_session() as db):
|
||||
flight_plan = await models.FlightPlan.get_one(db, id=flight_plan_id)
|
||||
|
||||
turn_points = (
|
||||
await db.scalars(
|
||||
select(models.FlightPlanMarker)
|
||||
.filter(models.FlightPlanMarker.flight_plan_id == flight_plan_id)
|
||||
)).all()
|
||||
|
||||
if len(turn_points) == 1:
|
||||
return
|
||||
|
||||
if flight_plan.track_id:
|
||||
track = await models.Track.get_one(db, id=flight_plan.track_id)
|
||||
await db.execute(
|
||||
delete(models.TrackPoint)
|
||||
.filter(models.TrackPoint.track_id == track.id)
|
||||
)
|
||||
else:
|
||||
track = await models.Track.create(db, {"bounds": [], "created_by_id": flight_plan.created_by_id})
|
||||
flight_plan.track_id = track.id
|
||||
|
||||
point_ts = flight_plan.planned_takeoff_datetime or datetime.now()
|
||||
track_points_resolution_meters = 500
|
||||
|
||||
speed_m_s = flight_plan.planned_speed / 3.6
|
||||
delta_t_sec = track_points_resolution_meters / speed_m_s
|
||||
|
||||
for i in range(len(turn_points) - 1):
|
||||
from_ = turn_points[i]
|
||||
to = turn_points[i + 1]
|
||||
|
||||
point_from = (from_.gps_latitude, from_.gps_longitude)
|
||||
point_to = (to.gps_latitude, to.gps_longitude)
|
||||
points_between = generate_points(point_from, point_to)
|
||||
|
||||
for point in points_between:
|
||||
await models.TrackPoint.create(db, {
|
||||
"track_id": track.id,
|
||||
"timestamp": point_ts,
|
||||
"gps_latitude": point.latitude,
|
||||
"gps_longitude": point.longitude,
|
||||
})
|
||||
|
||||
point_ts += timedelta(seconds=delta_t_sec)
|
||||
@@ -1,58 +0,0 @@
|
||||
from PIL import Image, ImageDraw, ImageOps
|
||||
|
||||
|
||||
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)
|
||||
@@ -6,14 +6,19 @@ from external.weather import weather_api
|
||||
from logger import log
|
||||
|
||||
|
||||
async def get_weather(date_time: datetime, gps: tuple[float, float]):
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
return {
|
||||
data = {
|
||||
"datetime": weather['datetime'],
|
||||
"qnh": weather['pressure_msl'],
|
||||
"temperature_surface": weather['temperature_2m'],
|
||||
@@ -25,14 +30,6 @@ async def get_weather(date_time: datetime, gps: tuple[float, float]):
|
||||
"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')
|
||||
@@ -42,20 +39,3 @@ async def download_weather_for_flight(date_time: datetime, flight_id: int, airpo
|
||||
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(date_time: datetime, flight_plan_id: int, gps: tuple[float, float]):
|
||||
data = await get_weather(date_time, gps)
|
||||
|
||||
async with get_session() as db:
|
||||
plan = await models.FlightPlan.get_one(db, id=flight_plan_id)
|
||||
|
||||
if plan.weather_info_id:
|
||||
await models.WeatherInfo.update(db, id=plan.weather_info.id, data=data)
|
||||
else:
|
||||
weather_model = await models.WeatherInfo.create(db, data=data)
|
||||
await models.FlightPlan.update(
|
||||
db,
|
||||
obj=plan,
|
||||
data={"weather_info_id": weather_model.id}
|
||||
)
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import os
|
||||
|
||||
APP_DEBUG = os.environ.get("APP_ENV", 'production') == 'development'
|
||||
APP_DEBUG = True
|
||||
GRAPHIQL = True
|
||||
|
||||
REFRESH_TOKEN_VALIDITY_DAYS = 30
|
||||
|
||||
+57
-137
@@ -59,14 +59,6 @@ flight_has_copilot = Table(
|
||||
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,
|
||||
@@ -74,153 +66,41 @@ copilot_has_photo = Table(
|
||||
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"
|
||||
class AirspaceItem(BaseModel):
|
||||
__tablename__ = "airspace_item"
|
||||
|
||||
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)
|
||||
center: Mapped[List[dict]] = mapped_column(JSON())
|
||||
radius: Mapped[float] = mapped_column(Float, nullable=True)
|
||||
bounds: Mapped[list[tuple[float, float]]] = mapped_column(JSON())
|
||||
|
||||
|
||||
class FlightPlan(BaseModel):
|
||||
__tablename__ = "flight_plan"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(256), nullable=False)
|
||||
is_default_name: Mapped[bool] = mapped_column(Boolean, server_default='0')
|
||||
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
|
||||
is_editable_by_copilot: Mapped[bool] = mapped_column(Boolean, server_default='0')
|
||||
track_id: Mapped[int] = mapped_column(Integer, ForeignKey('track.id'), nullable=True)
|
||||
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
|
||||
planned_speed: Mapped[int] = mapped_column(Integer, nullable=False, default=150)
|
||||
planned_takeoff_datetime: Mapped[datetime] = mapped_column(DateTime, nullable=True)
|
||||
aircraft_id: Mapped[int] = mapped_column(Integer, ForeignKey("aircraft.id"), nullable=True)
|
||||
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
|
||||
|
||||
copilots: Mapped[List['Copilot']] = relationship(secondary=flight_plan_has_copilot)
|
||||
aircraft: Mapped['Aircraft'] = relationship()
|
||||
markers: Mapped[List['FlightPlanMarker']] = relationship()
|
||||
created_by: Mapped['User'] = relationship()
|
||||
|
||||
|
||||
class Track(BaseModel):
|
||||
__tablename__ = "track"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
bounds: Mapped[list[tuple[float, float]]] = mapped_column(JSON())
|
||||
min_speed: Mapped[float] = mapped_column(Float, nullable=True)
|
||||
max_speed: Mapped[float] = mapped_column(Float, nullable=True)
|
||||
avg_speed: Mapped[float] = mapped_column(Float, nullable=True)
|
||||
max_altitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||
avg_altitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||
total_duration: Mapped[int] = mapped_column(Integer, nullable=True, comment="Total duration in seconds")
|
||||
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||
|
||||
flight: Mapped['Flight'] = relationship()
|
||||
flight_plan: Mapped['FlightPlan'] = relationship()
|
||||
# created_by: Mapped['User'] = relationship()
|
||||
|
||||
|
||||
class TrackPoint(BaseModel):
|
||||
__tablename__ = "track_point"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
timestamp: Mapped[datetime] = mapped_column(DateTime, nullable=False, index=True)
|
||||
track_id: Mapped[id] = mapped_column(Integer, ForeignKey('track.id'))
|
||||
gps_latitude: Mapped[float] = mapped_column(Float, nullable=False)
|
||||
gps_longitude: Mapped[float] = mapped_column(Float, nullable=False)
|
||||
terrain_elevation: Mapped[float] = mapped_column(Float, nullable=True)
|
||||
speed: Mapped[float] = mapped_column(Float, nullable=True)
|
||||
altitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||
magnetic_variation: Mapped[float] = mapped_column(Float, nullable=True)
|
||||
|
||||
track: Mapped['Track'] = relationship()
|
||||
|
||||
|
||||
class FlightPlanMarker(BaseModel):
|
||||
__tablename__ = "flight_plan_marker"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
flight_plan_id: Mapped[int] = mapped_column(Integer, ForeignKey('flight_plan.id'))
|
||||
airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=True)
|
||||
point_of_interest_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest.id"), nullable=True)
|
||||
weather_info_id: Mapped[int] = mapped_column(Integer, ForeignKey('weather_info.id'), nullable=True)
|
||||
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||
type: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
position: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
gps_latitude: Mapped[float] = mapped_column(Float, nullable=False)
|
||||
gps_longitude: Mapped[float] = mapped_column(Float, nullable=False)
|
||||
|
||||
|
||||
class Airport(BaseModel):
|
||||
__tablename__ = "airport"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||
openaip_id: Mapped[str] = mapped_column(String(32), nullable=True, unique=True)
|
||||
icao_code: Mapped[str] = mapped_column(String(8), nullable=False)
|
||||
gps_latitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||
gps_longitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||
elevation: Mapped[int] = mapped_column(Integer, nullable=True)
|
||||
airport_type: Mapped[str] = mapped_column(Enum("airport", "ull", "heliport"), nullable=False, server_default='airport') # noqa
|
||||
use_in_gpx_guess: Mapped[bool] = mapped_column(Boolean, server_default='1')
|
||||
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
|
||||
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
|
||||
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
|
||||
|
||||
runways: Mapped[list['Runway']] = relationship('Runway')
|
||||
frequencies: Mapped[list['Frequency']] = relationship()
|
||||
metars: Mapped['Metar'] = relationship()
|
||||
created_by: Mapped['User'] = relationship()
|
||||
|
||||
|
||||
class Runway(BaseModel):
|
||||
__tablename__ = "runway"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
airport_id: Mapped[int] = mapped_column(Integer, ForeignKey('airport.id'))
|
||||
heading: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
designator: Mapped[str] = mapped_column(String(4), nullable=False)
|
||||
surface: Mapped[str] = mapped_column(Enum("asphalt", "concrete", "grass", "other"), nullable=False, server_default="other") # noqa
|
||||
length: Mapped[int] = mapped_column(Integer, nullable=True)
|
||||
width: Mapped[int] = mapped_column(Integer, nullable=True)
|
||||
|
||||
|
||||
class Frequency(BaseModel):
|
||||
__tablename__ = "frequency"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
airport_id: Mapped[int] = mapped_column(Integer, ForeignKey('airport.id'), nullable=True)
|
||||
airspace_id: Mapped[int] = mapped_column(Integer, ForeignKey('airspace.id'), nullable=True)
|
||||
frequency: Mapped[str] = mapped_column(String(7), nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
|
||||
|
||||
class ReportingPoint(BaseModel):
|
||||
__tablename__ = "reporting_point"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
openaip_id: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
gps_latitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||
gps_longitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||
|
||||
|
||||
class PointOfInterestType(BaseModel):
|
||||
__tablename__ = "point_of_interest_type"
|
||||
|
||||
@@ -327,6 +207,22 @@ class Aircraft(BaseModel):
|
||||
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):
|
||||
@@ -342,15 +238,15 @@ class Organization(BaseModel):
|
||||
created_by: Mapped['User'] = relationship()
|
||||
|
||||
|
||||
class FlightTurnPoint(BaseModel):
|
||||
__tablename__ = "flight_turn_point"
|
||||
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, index=True)
|
||||
order: Mapped[int] = mapped_column(Integer)
|
||||
|
||||
flight: Mapped['Flight'] = relationship()
|
||||
point_of_interest: Mapped['PointOfInterest'] = relationship()
|
||||
@@ -407,10 +303,8 @@ class Flight(BaseModel):
|
||||
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
|
||||
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)
|
||||
@@ -424,14 +318,14 @@ class Flight(BaseModel):
|
||||
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()
|
||||
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])
|
||||
track: Mapped['Track'] = relationship()
|
||||
|
||||
|
||||
class Copilot(BaseModel):
|
||||
__tablename__ = "copilot"
|
||||
@@ -446,12 +340,37 @@ class Copilot(BaseModel):
|
||||
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 Metar(BaseModel):
|
||||
__tablename__ = "metar"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
airport_id: Mapped[int] = mapped_column(Integer, ForeignKey('airport.id'))
|
||||
metar: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
issued_at: Mapped[datetime] = mapped_column(DateTime)
|
||||
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
|
||||
|
||||
airport: Mapped['Airport'] = relationship()
|
||||
|
||||
|
||||
class License(BaseModel):
|
||||
__tablename__ = "license"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||
number: Mapped[str] = mapped_column(String(30), nullable=False)
|
||||
valid_until: Mapped[datetime] = mapped_column(DateTime, nullable=False)
|
||||
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||
|
||||
user: Mapped['User'] = relationship()
|
||||
created_by: Mapped['User'] = relationship()
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
__tablename__ = "user"
|
||||
excluded_columns_in_dict = ('password_hashed',)
|
||||
@@ -466,6 +385,7 @@ class User(BaseModel):
|
||||
password_hashed: Mapped[str] = mapped_column(String(60), nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||
|
||||
licences: Mapped[Set['License']] = relationship()
|
||||
flights: Mapped[Set['Flight']] = relationship()
|
||||
organizations: Mapped[Set['Organization']] = relationship(secondary=user_is_in_organization)
|
||||
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
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
|
||||
@@ -11,18 +11,19 @@ from graphql_schema.schema import GraphQLContext, schema
|
||||
|
||||
|
||||
def setup_graphql_endpoint(app: FastAPI, access_security: JwtAccessBearerCookie):
|
||||
if APP_DEBUG:
|
||||
debug_router = APIRouter()
|
||||
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))
|
||||
@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
|
||||
return response
|
||||
|
||||
app.include_router(debug_router)
|
||||
app.include_router(debug_router)
|
||||
|
||||
async def setup_graphql_context(credentials: JwtAuthorizationCredentials = Security(access_security)):
|
||||
user_id = credentials['id'] if credentials else None
|
||||
|
||||
@@ -33,8 +33,6 @@ 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,
|
||||
|
||||
Vendored
+13
-73
@@ -1,87 +1,27 @@
|
||||
import dataclasses
|
||||
from typing import Optional
|
||||
from typing import List, Dict
|
||||
import aiohttp
|
||||
|
||||
from database import models
|
||||
from decorators.retryable import retryable
|
||||
from utils.list_utils import chunks
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ElevationInfo:
|
||||
lat: float
|
||||
lng: float
|
||||
elevation: float
|
||||
id: Optional[int] = None
|
||||
|
||||
|
||||
class ElevationAPI:
|
||||
ELEVATION_ENDPOINT: str
|
||||
ELEVATION_ENDPOINT = "https://api.open-elevation.com/api/v1/lookup"
|
||||
|
||||
def get_request(self, session: aiohttp.ClientSession, points: list[dict[str, float]]):
|
||||
...
|
||||
def get_request(self, points: List[Dict[str, float]]):
|
||||
return {"locations": [{"latitude": point['lat'], "longitude": point['lng']} for point in points]}
|
||||
|
||||
@retryable
|
||||
async def call_api(self, points: list[dict[str, float]]):
|
||||
async def call_api(self, points: List[Dict[str, float]]):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with self.get_request(session, points) as resp:
|
||||
async with session.post(self.ELEVATION_ENDPOINT, json=self.get_request(points)) as resp:
|
||||
resp.raise_for_status()
|
||||
return await resp.json()
|
||||
|
||||
|
||||
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]:
|
||||
async def get_elevation_for_points(self, points: List[Dict[str, float]]) -> List[dict]:
|
||||
response = await self.call_api(points)
|
||||
|
||||
elevation = []
|
||||
for i, point in enumerate(points):
|
||||
response_point = response['results'][i]
|
||||
elevation.append(ElevationInfo(**point, elevation=response_point['elevation']))
|
||||
return elevation
|
||||
return [{
|
||||
"lat": loc['latitude'],
|
||||
"lng": loc['longitude'],
|
||||
"elevation": loc['elevation']
|
||||
} for loc in response['results']]
|
||||
|
||||
|
||||
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()
|
||||
elevation_api = ElevationAPI()
|
||||
|
||||
Vendored
+39
-12
@@ -1,7 +1,9 @@
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Any
|
||||
from aiocache import cached
|
||||
from lxml import etree
|
||||
from lxml.etree import _ElementTree
|
||||
|
||||
|
||||
class GPXParser:
|
||||
@@ -66,47 +68,48 @@ 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)
|
||||
|
||||
async def get_total_duration(self) -> timedelta:
|
||||
times = await self.get_times_all()
|
||||
return times[-1] - times[0]
|
||||
|
||||
@cached()
|
||||
async def get_coordinates(self) -> List[Dict[str, float]]:
|
||||
return await self.average_coordinates(await self.get_coordinates_all())
|
||||
|
||||
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]
|
||||
return await self.average_coordinates(
|
||||
[{"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
|
||||
|
||||
async def get_min_speed(self):
|
||||
return min(await self.get_speed()) or 0
|
||||
|
||||
@cached()
|
||||
async def get_avg_speed(self):
|
||||
speeds = await self.get_speed()
|
||||
if not speeds:
|
||||
@@ -114,9 +117,33 @@ 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)
|
||||
extensions.append(etree.XML(f"<terrain_elevation>{elevation}</terrain_elevation>"))
|
||||
|
||||
return self.gpx
|
||||
|
||||
def write(self, tree: _ElementTree, output: str):
|
||||
tree.write(output)
|
||||
|
||||
Vendored
+1
-2
@@ -3,7 +3,6 @@ import urllib.parse
|
||||
from typing import Tuple, Dict
|
||||
import aiohttp
|
||||
from aiocache import cached
|
||||
from decorators.retryable import retryable
|
||||
|
||||
|
||||
class Weather:
|
||||
@@ -39,7 +38,6 @@ 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)
|
||||
|
||||
@@ -54,6 +52,7 @@ 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")
|
||||
|
||||
@@ -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.FlightTurnPoint.landing_duration), 0).label("landing_duration")
|
||||
).join(models.Flight.turn_points, isouter=True)
|
||||
func.coalesce(func.sum(models.FlightTrack.landing_duration), 0).label("landing_duration")
|
||||
).join(models.Flight.track, isouter=True)
|
||||
.group_by(models.Flight.id)
|
||||
.filter(models.Flight.id.in_(ids))
|
||||
|
||||
|
||||
@@ -20,51 +20,9 @@ flight_copilots_dataloader = DataLoader(
|
||||
models.flight_has_copilot.c.flight_id == models.Flight.id,
|
||||
models.flight_has_copilot.c.copilot_id == models.Copilot.id,
|
||||
],
|
||||
extra_join=[models.Copilot.flights]
|
||||
).load,
|
||||
extra_join=[models.Copilot.flights]).load,
|
||||
cache=False)
|
||||
|
||||
|
||||
flight_plan_copilots_dataloader = DataLoader(
|
||||
load_fn=FlightCopilotDataloader(
|
||||
models.Copilot,
|
||||
relationship_column=models.FlightPlan.id,
|
||||
extra_select=[models.flight_plan_has_copilot.c.token],
|
||||
# filters=[
|
||||
# models.flight_plan_has_copilot.c.flight_plan_id == models.FlightPlan.id,
|
||||
# models.flight_plan_has_copilot.c.copilot_id == models.Copilot.id,
|
||||
# ],
|
||||
extra_join=[models.Copilot.flight_plans]
|
||||
).load,
|
||||
cache=False
|
||||
)
|
||||
|
||||
reporting_points_dataloader = DataLoader(
|
||||
load_fn=MultiModelsDataloader(
|
||||
models.ReportingPoint,
|
||||
relationship_column=models.airport_has_reporting_point.c.airport_id,
|
||||
extra_join=[models.airport_has_reporting_point]
|
||||
).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,
|
||||
@@ -98,7 +56,7 @@ flight_by_poi_dataloader = DataLoader(
|
||||
models.Flight,
|
||||
relationship_column=models.PointOfInterest.id,
|
||||
order_by=[models.Flight.takeoff_datetime.desc()],
|
||||
extra_join=[models.Flight.turn_points, models.PointOfInterest]
|
||||
extra_join=[models.Flight.track, models.PointOfInterest]
|
||||
).load,
|
||||
cache=False
|
||||
)
|
||||
@@ -155,20 +113,11 @@ poi_photos_dataloader = DataLoader(
|
||||
).load,
|
||||
cache=False
|
||||
)
|
||||
flight_turn_points_dataloader = DataLoader(
|
||||
flight_track_dataloader = DataLoader(
|
||||
load_fn=MultiModelsDataloader(
|
||||
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]
|
||||
models.FlightTrack,
|
||||
relationship_column=models.FlightTrack.flight_id,
|
||||
order_by=[models.FlightTrack.order]
|
||||
).load,
|
||||
cache=False
|
||||
)
|
||||
@@ -201,12 +150,3 @@ photos_aircraft_dataloader = DataLoader(
|
||||
).load,
|
||||
cache=False
|
||||
)
|
||||
|
||||
flight_plan_markers_dataloader = DataLoader(
|
||||
load_fn=MultiModelsDataloader(
|
||||
models.FlightPlanMarker,
|
||||
relationship_column=models.FlightPlanMarker.flight_plan_id,
|
||||
order_by=[models.FlightPlanMarker.position, models.FlightPlanMarker.id]
|
||||
).load,
|
||||
cache=False
|
||||
)
|
||||
|
||||
@@ -15,7 +15,6 @@ 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)
|
||||
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)
|
||||
|
||||
@@ -11,7 +11,7 @@ from graphql_schema.entities.types.types import Airport
|
||||
class AirportQueries:
|
||||
@strawberry.field()
|
||||
@error_logging
|
||||
# @authenticated_user_only()
|
||||
@authenticated_user_only()
|
||||
async def airports(root, info) -> List[Airport]:
|
||||
return await BaseQueryResolver(Airport, models.Airport).get_list(info.context.user_id)
|
||||
|
||||
|
||||
@@ -16,11 +16,11 @@ class AirspaceQueries:
|
||||
|
||||
filters = []
|
||||
if country:
|
||||
filters.append(models.Airspace.country == country)
|
||||
filters.append(models.AirspaceItem.country == country)
|
||||
if types:
|
||||
filters.append(models.Airspace.type.in_(types))
|
||||
filters.append(models.AirspaceItem.type.in_(types))
|
||||
|
||||
return await BaseQueryResolver(Airspace, models.Airspace).get_list(
|
||||
return await BaseQueryResolver(Airspace, models.AirspaceItem).get_list(
|
||||
info.context.user_id,
|
||||
country=country,
|
||||
filters=filters
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
from typing import List, Optional
|
||||
import strawberry
|
||||
from strawberry.types import Info
|
||||
from decorators.endpoints import authenticated_user_only, allow_public
|
||||
from decorators.error_logging import error_logging
|
||||
from graphql_schema.entities.helpers.detail import get_detail_filters
|
||||
from graphql_schema.entities.resolvers.flight_plan import FlightPlanMutationResolver, FlightPlanQueryResolver
|
||||
from graphql_schema.entities.types.mutation_input import CreateFlightPlanInput, EditFlightPlanInput
|
||||
from graphql_schema.entities.types.types import FlightPlan
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class FlightPlanQueries:
|
||||
@strawberry.field()
|
||||
@error_logging
|
||||
@authenticated_user_only()
|
||||
async def flight_plans(root, info: Info) -> List[FlightPlan]:
|
||||
return await FlightPlanQueryResolver().get_list(info.context.user_id)
|
||||
|
||||
@strawberry.field()
|
||||
@error_logging
|
||||
@allow_public
|
||||
async def flight_plan(
|
||||
root,
|
||||
info: Info,
|
||||
id: int,
|
||||
username: Optional[str] = None,
|
||||
public: Optional[bool] = 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,
|
||||
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:
|
||||
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)
|
||||
@@ -2,7 +2,7 @@ from typing import Optional
|
||||
from graphql import GraphQLError
|
||||
|
||||
|
||||
def get_detail_filters(id: Optional[int] = None, url_slug: Optional[str] = None) -> dict:
|
||||
def get_detail_filters(id: Optional[int], url_slug: Optional[str]) -> dict:
|
||||
filter_params = {}
|
||||
if id:
|
||||
filter_params['object_id'] = id
|
||||
|
||||
@@ -87,7 +87,7 @@ class BaseQueryResolver(BaseResolver):
|
||||
|
||||
|
||||
class BaseMutationResolver(BaseResolver):
|
||||
async def _get_one(self, db: AsyncSession, id: int, created_by_id: int):
|
||||
async def _get_one(self, db: AsyncSession, id: int, created_by_id: int) -> models.BaseModel:
|
||||
query = self.query_builder.get_simple_query(created_by_id=created_by_id).filter(self.model.id == id)
|
||||
return (await db.scalars(query)).one()
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import asyncio
|
||||
import random
|
||||
from typing import Optional
|
||||
from sqlalchemy import delete, insert
|
||||
from typing import List, Optional
|
||||
from sqlalchemy import delete, insert, select, func, text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from strawberry.file_uploads import Upload
|
||||
from background_jobs.elevation import add_terrain_elevation_to_flight
|
||||
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 background_jobs.weather import download_weather
|
||||
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,8 +16,9 @@ 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 handle_file_upload
|
||||
from utils.file import delete_file
|
||||
from utils.str_utils import random_str
|
||||
from utils.upload import handle_file_upload
|
||||
|
||||
|
||||
class FlightQueryResolver(BaseQueryResolver):
|
||||
@@ -27,11 +28,17 @@ 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,
|
||||
*args,
|
||||
**kwargs
|
||||
):
|
||||
filters = {key: kwargs[key] for key in ("object_id", "url_slug") if key in kwargs}
|
||||
filters = {}
|
||||
if object_id:
|
||||
filters['object_id'] = object_id
|
||||
if url_slug:
|
||||
filters['url_slug'] = url_slug
|
||||
|
||||
query = super().get_query(
|
||||
user_id,
|
||||
@@ -55,8 +62,8 @@ class FlightQueryResolver(BaseQueryResolver):
|
||||
|
||||
if kwargs.get("point_of_interest_id"):
|
||||
query = (
|
||||
query.join(models.Flight.turn_points)
|
||||
.filter(models.FlightTurnPoint.point_of_interest_id == kwargs["point_of_interest_id"])
|
||||
query.join(models.Flight.track)
|
||||
.filter(models.FlightTrack.point_of_interest_id == kwargs["point_of_interest_id"])
|
||||
)
|
||||
|
||||
if kwargs.get('username'):
|
||||
@@ -72,13 +79,55 @@ 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:
|
||||
data = input.to_dict()
|
||||
user_id = context.user_id
|
||||
|
||||
if input.gpx_track_file:
|
||||
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['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.update(data_from_gpx)
|
||||
else:
|
||||
async with get_session() as db:
|
||||
@@ -99,19 +148,22 @@ class FlightMutationResolver(BaseMutationResolver):
|
||||
data.update({
|
||||
"aircraft_id": aircraft_id,
|
||||
"has_terrain_elevation": False,
|
||||
"name": input.name or '',
|
||||
"name": "",
|
||||
"description": "",
|
||||
"created_by_id": context.user_id
|
||||
})
|
||||
flight = await self._do_create(db, data)
|
||||
|
||||
if input.track is not None:
|
||||
await handle_track_edit(db=db, flight_id=flight.id, track=input.track, user_id=user_id)
|
||||
|
||||
if data['gpx_track_filename']:
|
||||
await save_track_from_gpx_to_db(gpx_filename=data['gpx_track_filename'], flight_id=flight.id)
|
||||
|
||||
schedule_background_tasks(flight.id, data, context)
|
||||
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"
|
||||
)
|
||||
|
||||
return flight
|
||||
|
||||
@@ -125,23 +177,40 @@ 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, flight_id=flight_id)
|
||||
data['gpx_track_filename'] = await handle_upload_gpx(
|
||||
gpx_track=input.gpx_track_file,
|
||||
context=context,
|
||||
original_gpx_filename=flight_data['gpx_track_filename']
|
||||
)
|
||||
|
||||
async with get_session() as db:
|
||||
if input.takeoff_airport:
|
||||
data['takeoff_airport_id'] = await handle_combobox_save(
|
||||
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:
|
||||
data['landing_airport_id'] = await handle_combobox_save(
|
||||
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)
|
||||
|
||||
@@ -170,27 +239,21 @@ class FlightMutationResolver(BaseMutationResolver):
|
||||
token=random_str(64)
|
||||
))
|
||||
|
||||
flight_model = await self._do_update(db, flight_data, data)
|
||||
|
||||
schedule_background_tasks(flight_id, data, context)
|
||||
|
||||
return flight_model
|
||||
return await self._do_update(db, flight_data, data)
|
||||
|
||||
|
||||
def schedule_background_tasks(flight_id: int, flight_data: dict, context) -> None:
|
||||
context.background_tasks.add_task(add_terrain_elevation_to_flight, flight_id=flight_id)
|
||||
context.background_tasks.add_task(
|
||||
download_weather_for_flight, flight_id=id, airport_id=flight_data['takeoff_airport_id'],
|
||||
date_time=flight_data['takeoff_datetime'], type_="takeoff"
|
||||
)
|
||||
context.background_tasks.add_task(
|
||||
download_weather_for_flight, flight_id=id, airport_id=flight_data['landing_airport_id'],
|
||||
date_time=flight_data['landing_datetime'], type_="landing"
|
||||
)
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
return filename
|
||||
|
||||
|
||||
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))
|
||||
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))
|
||||
|
||||
order = 0
|
||||
for item in track:
|
||||
@@ -199,26 +262,17 @@ 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": "",
|
||||
'gps_latitude': item.gps_latitude,
|
||||
'gps_longitude': item.gps_longitude
|
||||
}
|
||||
db, models.PointOfInterest, item.point_of_interest, user_id, extra_data={"description": ""}
|
||||
)
|
||||
|
||||
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,
|
||||
'gps_latitude': item.gps_latitude,
|
||||
'gps_longitude': item.gps_longitude,
|
||||
'openaip_id': f'custom-{random.randint(0, 999999)}'
|
||||
}
|
||||
extra_data={"name": item.airport.name}
|
||||
)
|
||||
|
||||
await models.FlightTurnPoint.create(
|
||||
await models.FlightTrack.create(
|
||||
db,
|
||||
data={
|
||||
"flight_id": flight_id,
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
import asyncio
|
||||
from typing import Optional
|
||||
from sqlalchemy import delete, select
|
||||
from sqlalchemy.dialects.mysql import insert
|
||||
|
||||
from background_jobs.flight_plan_track import generate_track_for_flight_plan
|
||||
from database import models
|
||||
from database.models import flight_plan_has_copilot
|
||||
from database.transaction import get_session
|
||||
from graphql_schema.entities.helpers.combobox import handle_combobox_save
|
||||
from graphql_schema.entities.resolvers.base import BaseMutationResolver, BaseQueryResolver
|
||||
from graphql_schema.entities.resolvers.flight import handle_aircraft_save
|
||||
from graphql_schema.entities.types.mutation_input import CreateFlightPlanInput, EditFlightPlanInput
|
||||
from graphql_schema.entities.types.types import FlightPlan
|
||||
from utils.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: Optional[int] = None,
|
||||
only_public: Optional[bool] = False,
|
||||
object_id: Optional[int] = 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'])
|
||||
)
|
||||
|
||||
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:
|
||||
if marker.type == 'poi':
|
||||
assert bool(marker.point_of_interest_id)
|
||||
|
||||
if marker.type == 'airport':
|
||||
assert bool(marker.airport_id)
|
||||
|
||||
await models.FlightPlanMarker.create(db, data={
|
||||
"position": position,
|
||||
"flight_plan_id": flight_plan.id,
|
||||
"airport_id": marker.airport_id,
|
||||
"point_of_interest_id": marker.point_of_interest_id,
|
||||
"type": marker.type,
|
||||
"name": marker.name,
|
||||
"gps_latitude": marker.gps_latitude,
|
||||
"gps_longitude": marker.gps_longitude
|
||||
})
|
||||
position += 1
|
||||
|
||||
@staticmethod
|
||||
async def reset_plan_markers(db, flight_plan: models.FlightPlan):
|
||||
await db.execute(
|
||||
delete(models.FlightPlanMarker)
|
||||
.filter(models.FlightPlanMarker.flight_plan_id == flight_plan.id)
|
||||
)
|
||||
|
||||
async def create(self, context, data: CreateFlightPlanInput) -> FlightPlan:
|
||||
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)
|
||||
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:
|
||||
input_data = data.to_dict()
|
||||
|
||||
async with get_session() as db:
|
||||
flight_plan_model = await self._get_one(db, id=id, created_by_id=user_id)
|
||||
|
||||
if data.aircraft is not None:
|
||||
input_data['aircraft_id'] = await handle_aircraft_save(db, user_id, data.aircraft)
|
||||
|
||||
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)
|
||||
|
||||
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()
|
||||
|
||||
used_markers = evenly_spaced_elements(markers, 5)
|
||||
input_data['name'] = " - ".join(m.name for m in used_markers)
|
||||
|
||||
if data.copilots is not None:
|
||||
await self.save_copilots(
|
||||
db,
|
||||
flight_plan_id=id,
|
||||
copilots=data.copilots,
|
||||
user_id=user_id
|
||||
)
|
||||
|
||||
flight_plan = await self._do_update(db, obj=flight_plan_model, data=input_data)
|
||||
|
||||
await generate_track_for_flight_plan(flight_plan.id)
|
||||
|
||||
return flight_plan
|
||||
@@ -16,7 +16,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.file import handle_file_upload
|
||||
from utils.upload import handle_file_upload
|
||||
|
||||
|
||||
class PhotoQueryResolver(BaseQueryResolver):
|
||||
|
||||
@@ -5,7 +5,6 @@ import strawberry
|
||||
from strawberry.file_uploads import Upload
|
||||
from database import models
|
||||
from graphql_schema.entities.types.base import BaseGraphqlInputType
|
||||
from graphql_schema.entities.types.types import Aircraft
|
||||
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_input
|
||||
|
||||
|
||||
@@ -15,33 +14,11 @@ class ComboboxInput:
|
||||
name: str
|
||||
|
||||
|
||||
@strawberry_sqlalchemy_input(model=models.FlightPlanMarker, exclude_fields=["id", "flight_plan_id"])
|
||||
class FlightPlanMarkerInput(BaseGraphqlInputType):
|
||||
position: Optional[int] = 0
|
||||
|
||||
|
||||
@strawberry_sqlalchemy_input(model=models.Copilot, exclude_fields=["id"])
|
||||
class CreateCopilotInput(BaseGraphqlInputType):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_sqlalchemy_input(model=models.FlightPlan, exclude_fields=["id", "markers"])
|
||||
class CreateFlightPlanInput(BaseGraphqlInputType):
|
||||
markers: list[FlightPlanMarkerInput]
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
data = super().to_dict()
|
||||
data['markers'] = [marker.to_dict() for marker in self.markers]
|
||||
return data
|
||||
|
||||
|
||||
@strawberry_sqlalchemy_input(model=models.FlightPlan, exclude_fields=["id", "markers"], all_optional=True)
|
||||
class EditFlightPlanInput(BaseGraphqlInputType):
|
||||
markers: Optional[list[FlightPlanMarkerInput]] = None
|
||||
copilots: Optional[list[ComboboxInput]] = None
|
||||
aircraft: Optional[ComboboxInput] = None
|
||||
|
||||
|
||||
@strawberry_sqlalchemy_input(model=models.Copilot, exclude_fields=["id"], all_optional=True)
|
||||
class EditCopilotInput(BaseGraphqlInputType):
|
||||
pass
|
||||
@@ -138,14 +115,11 @@ class EditFlightInput(BaseGraphqlInputType):
|
||||
@strawberry.input()
|
||||
class CreateFlightInput(BaseGraphqlInputType):
|
||||
aircraft: ComboboxInput
|
||||
flight_plan_id: Optional[int] = None
|
||||
name: Optional[str] = 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()
|
||||
@@ -153,8 +127,6 @@ class TrackItemInput:
|
||||
point_of_interest: Optional[ComboboxInput] = None
|
||||
airport: Optional[ComboboxInput] = None
|
||||
landing_duration: Optional[int] = None
|
||||
gps_latitude: Optional[float] = None
|
||||
gps_longitude: Optional[float] = None
|
||||
|
||||
|
||||
@strawberry_sqlalchemy_input(models.Aircraft, exclude_fields=['id', 'photo_filename'])
|
||||
|
||||
@@ -1,28 +1,26 @@
|
||||
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 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_turn_points_dataloader,
|
||||
poi_photos_dataloader, flight_by_poi_dataloader, flight_copilots_dataloader, flight_track_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, runways_dataloader, frequencies_dataloader, track_points_dataloder
|
||||
photo_copilots_dataloader, photos_aircraft_dataloader, copilots_in_photo_dataloader
|
||||
)
|
||||
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, track_dataloader
|
||||
photo_dataloader, user_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, airport_icon_url, get_public_url
|
||||
from paths import get_avatar_url, get_title_image_url, get_photo_thumbnail_url, get_photo_url, FLIGHT_GPX_TRACK_PATH
|
||||
|
||||
|
||||
@strawberry.type
|
||||
@@ -31,31 +29,23 @@ class Point:
|
||||
lng: float
|
||||
|
||||
|
||||
|
||||
|
||||
@strawberry_sqlalchemy_type(models.ReportingPoint)
|
||||
class ReportingPoint:
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_sqlalchemy_type(models.Runway)
|
||||
class Runway:
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_sqlalchemy_type(models.Frequency)
|
||||
class Frequency:
|
||||
pass
|
||||
@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.Airport)
|
||||
class Airport:
|
||||
reporting_points: List[ReportingPoint] = strawberry.field(
|
||||
resolver=lambda root: reporting_points_dataloader.load(root.id)
|
||||
)
|
||||
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))
|
||||
pass
|
||||
|
||||
|
||||
@strawberry.type
|
||||
@@ -65,17 +55,20 @@ class Airspace:
|
||||
country: str
|
||||
type: str
|
||||
lower_limit: str
|
||||
lower_alt_unit: str
|
||||
upper_alt_unit: str
|
||||
upper_limit: str
|
||||
center: Optional[tuple[float, float]]
|
||||
radius: Optional[float]
|
||||
bounds: list[tuple[float, float]]
|
||||
map_center: Optional[Point] = strawberry.field(
|
||||
resolver=lambda root: Point(lat=root.center[0], lng=root.center[1]) if root.center else None
|
||||
)
|
||||
map_bounds: list[Point] = strawberry.field(
|
||||
resolver=lambda root: [Point(lat=point[1], lng=point[0]) for point in root.bounds]
|
||||
resolver=lambda root: [Point(lat=point[0], lng=point[1]) for point in root.bounds]
|
||||
)
|
||||
|
||||
|
||||
@strawberry_sqlalchemy_type(models.FlightTurnPoint)
|
||||
class FlightTurnPoint:
|
||||
@strawberry_sqlalchemy_type(models.FlightTrack)
|
||||
class FlightTrack:
|
||||
point_of_interest: Optional[PointOfInterest] = strawberry.field(
|
||||
resolver=lambda root: poi_dataloader.load(root.point_of_interest_id)
|
||||
)
|
||||
@@ -119,20 +112,6 @@ 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):
|
||||
@@ -141,6 +120,28 @@ 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=[])
|
||||
async def load_copilots(root):
|
||||
return await flight_copilots_dataloader.load(root.id)
|
||||
@@ -160,7 +161,7 @@ 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))
|
||||
turn_points: List[FlightTurnPoint] = strawberry.field(resolver=lambda root: flight_turn_points_dataloader.load(root.id))
|
||||
track: List[FlightTrack] = strawberry.field(resolver=lambda root: flight_track_dataloader.load(root.id))
|
||||
takeoff_weather_info: Optional[WeatherInfo] = strawberry.field(
|
||||
resolver=lambda root: airport_weather_info_loader.load(root.takeoff_weather_info_id)
|
||||
)
|
||||
@@ -168,67 +169,17 @@ class Flight:
|
||||
resolver=lambda root: airport_weather_info_loader.load(root.landing_weather_info_id)
|
||||
)
|
||||
photos: List[Photo] = strawberry.field(resolver=lambda root: photos_dataloader.load(root.id))
|
||||
track: Optional[Track] = strawberry.field(resolver=lambda root: track_dataloader.load(root.track_id))
|
||||
duration_min_calculated: int = strawberry.field(resolver=lambda root: flight_duration_dataloader.load(root.id))
|
||||
social_image_url: Optional[str] = strawberry.field(
|
||||
resolver=lambda root: get_public_url(f'photos/{root.id}/title_photo.jpg')
|
||||
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)
|
||||
)
|
||||
|
||||
debriefing_notes: Optional[str] = strawberry.field(
|
||||
permission_classes=[IsAuthenticated],
|
||||
resolver=lambda root: root.debriefing_notes
|
||||
)
|
||||
|
||||
|
||||
@strawberry_sqlalchemy_type(model=models.FlightPlanMarker)
|
||||
class FlightPlanMarker:
|
||||
pass
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class FlightPlanNavlog:
|
||||
bearing: Optional[int]
|
||||
distance: Optional[float]
|
||||
from_: FlightPlanMarker = strawberry.field(name="from")
|
||||
to: Optional[FlightPlanMarker]
|
||||
|
||||
|
||||
@strawberry_sqlalchemy_type(model=models.FlightPlan)
|
||||
class FlightPlan:
|
||||
async def resolve_navlog(self):
|
||||
markers = await self.markers()
|
||||
|
||||
if len(markers) == 1:
|
||||
return [FlightPlanNavlog(bearing=None, distance=None, from_=markers[0], to=None)]
|
||||
|
||||
navigation = []
|
||||
for i in range(len(markers) - 1):
|
||||
from_ = markers[i]
|
||||
to = markers[i + 1]
|
||||
|
||||
point_from = (from_.gps_latitude, from_.gps_longitude)
|
||||
point_to = (to.gps_latitude, to.gps_longitude)
|
||||
|
||||
navigation.append(FlightPlanNavlog(
|
||||
bearing=round(math.degrees(get_bearing(point_from, point_to)) % 360),
|
||||
distance=round(get_distance(point_from, point_to), 1),
|
||||
from_=from_,
|
||||
to=to)
|
||||
)
|
||||
return navigation
|
||||
|
||||
@authenticated_user_only(raise_when_unauthorized=False, return_value_unauthorized=[])
|
||||
async def load_copilots(root):
|
||||
return await flight_plan_copilots_dataloader.load(root.id)
|
||||
|
||||
markers: list[FlightPlanMarker] = strawberry.field(
|
||||
resolver=lambda root: flight_plan_markers_dataloader.load(root.id)
|
||||
)
|
||||
navlog: Optional[list[FlightPlanNavlog]] = strawberry.field(resolver=resolve_navlog)
|
||||
track: Optional[Track] = strawberry.field(resolver=lambda root: track_dataloader.load(root.track_id))
|
||||
copilots: List[Copilot] = strawberry.field(resolver=load_copilots)
|
||||
aircraft: Optional[Aircraft] = strawberry.field(resolver=lambda root: aircraft_dataloader.load(root.aircraft_id))
|
||||
|
||||
|
||||
@strawberry_sqlalchemy_type(models.Copilot)
|
||||
class Copilot:
|
||||
async def resolve_flights(root, info):
|
||||
@@ -272,9 +223,6 @@ class Organization:
|
||||
class User:
|
||||
avatar_image_url: Optional[str] = strawberry.field(resolver=lambda root: get_avatar_url(root))
|
||||
title_image_url: str = strawberry.field(resolver=lambda root: get_title_image_url(root))
|
||||
social_image_url: Optional[str] = strawberry.field(
|
||||
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)
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ from decorators.error_logging import error_logging
|
||||
from database.transaction import get_session
|
||||
from graphql_schema.entities.types.types import User
|
||||
from utils.file import delete_file
|
||||
from utils.file import handle_file_upload
|
||||
from utils.upload import handle_file_upload
|
||||
|
||||
|
||||
@strawberry.type
|
||||
|
||||
@@ -3,7 +3,6 @@ from graphql_schema.entities.aircraft import AircraftMutation
|
||||
from graphql_schema.entities.copilot import CopilotMutation
|
||||
from graphql_schema.entities.event import EventMutation
|
||||
from graphql_schema.entities.flight import FlightMutation
|
||||
from graphql_schema.entities.flight_plan import FlightPlanMutation
|
||||
from graphql_schema.entities.organization import OrganizationUserMutation, OrganizationMutation
|
||||
from graphql_schema.entities.photo import PhotoMutation
|
||||
from graphql_schema.entities.poi import PointOfInterestMutation
|
||||
@@ -12,7 +11,6 @@ from graphql_schema.entities.user import EditUserMutation
|
||||
Mutation = merge_types("Mutation", (
|
||||
AircraftMutation,
|
||||
FlightMutation,
|
||||
FlightPlanMutation,
|
||||
PhotoMutation,
|
||||
PointOfInterestMutation,
|
||||
CopilotMutation,
|
||||
|
||||
@@ -5,7 +5,6 @@ from .entities.airspace import AirspaceQueries
|
||||
from .entities.copilot import CopilotQueries
|
||||
from .entities.event import EventQueries
|
||||
from .entities.flight import FlightQueries
|
||||
from .entities.flight_plan import FlightPlanQueries
|
||||
from .entities.organization import OrganizationQueries
|
||||
from .entities.photo import PhotoQueries
|
||||
from .entities.poi import PointOfInterestQueries
|
||||
@@ -20,7 +19,6 @@ Query = merge_types('Query', (
|
||||
AirportQueries,
|
||||
AirspaceQueries,
|
||||
FlightQueries,
|
||||
FlightPlanQueries,
|
||||
CopilotQueries,
|
||||
UserQueries,
|
||||
PhotoQueries,
|
||||
|
||||
+1
-2
@@ -9,7 +9,7 @@ from starlette.background import BackgroundTasks
|
||||
from starlette.middleware.cors import CORSMiddleware
|
||||
from starlette.responses import Response, JSONResponse
|
||||
from starlette.staticfiles import StaticFiles
|
||||
from config import APP_SECRET_KEY, ALLOW_CORS_ORIGINS, SENTRY_DSN, REFRESH_TOKEN_VALIDITY_DAYS, APP_DEBUG
|
||||
from config import APP_SECRET_KEY, ALLOW_CORS_ORIGINS, SENTRY_DSN, REFRESH_TOKEN_VALIDITY_DAYS
|
||||
from endpoints.contact import ContactEndpoint, ContactInput
|
||||
from endpoints.forgotten_password import ForgottenPasswordRequest, ForgottenPasswordEndpoint, ChangeForgottenPassword
|
||||
from endpoints.graphql import setup_graphql_endpoint
|
||||
@@ -35,7 +35,6 @@ class App:
|
||||
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,
|
||||
|
||||
@@ -8,13 +8,12 @@ import aiohttp
|
||||
from sqlalchemy import delete
|
||||
from database import models
|
||||
from database.transaction import get_session
|
||||
from utils.gps import get_distance, get_bearing, EARTH_RADIUS
|
||||
|
||||
|
||||
# https://pyopenair.readthedocs.io/en/latest/openair.html
|
||||
# http://www.winpilot.com/usersguide/userairspace.asp
|
||||
|
||||
class AirspaceType(enum.Enum):
|
||||
class AirspaceItemType(enum.Enum):
|
||||
AIRSPACE_A = "A"
|
||||
AIRSPACE_B = "B"
|
||||
AIRSPACE_C = "C"
|
||||
@@ -47,8 +46,8 @@ class Coordinates:
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Airspace:
|
||||
type: AirspaceType
|
||||
class AirspaceItem:
|
||||
type: AirspaceItemType
|
||||
info: str = ""
|
||||
upper_limit: str = None
|
||||
lower_limit: str = None
|
||||
@@ -58,6 +57,40 @@ class Airspace:
|
||||
|
||||
|
||||
class ArcExpander:
|
||||
R = 6378.1 # Radius of the Earth
|
||||
|
||||
@staticmethod
|
||||
def get_bearing(center_point, end_point):
|
||||
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
|
||||
|
||||
@classmethod
|
||||
def get_distance(cls, a, b):
|
||||
|
||||
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 cls.R * c
|
||||
|
||||
@classmethod
|
||||
def get_arc_coordinates(
|
||||
cls,
|
||||
@@ -65,9 +98,9 @@ class ArcExpander:
|
||||
origin_point: tuple[float, float],
|
||||
end_point: tuple[float, float]
|
||||
) -> list[Coordinates]:
|
||||
initial_bearing = get_bearing(center_point, origin_point)
|
||||
target_bearing = get_bearing(center_point, end_point)
|
||||
distance = get_distance(center_point, origin_point)
|
||||
initial_bearing = cls.get_bearing(center_point, origin_point)
|
||||
target_bearing = cls.get_bearing(center_point, end_point)
|
||||
distance = cls.get_distance(center_point, origin_point)
|
||||
|
||||
print(f"DISTANCE {distance} meters, {center_point=}, {origin_point=}, {end_point=}")
|
||||
|
||||
@@ -89,12 +122,12 @@ class ArcExpander:
|
||||
for bearing in list_bearings:
|
||||
lat1, lon1 = math.radians(center_point[0]), math.radians(center_point[1])
|
||||
lat2 = math.asin(
|
||||
math.sin(lat1) * math.cos(distance / EARTH_RADIUS) + math.cos(lat1) * math.sin(distance / EARTH_RADIUS) * math.cos(bearing)
|
||||
math.sin(lat1) * math.cos(distance / cls.R) + math.cos(lat1) * math.sin(distance / cls.R) * math.cos(bearing)
|
||||
)
|
||||
|
||||
lon2 = lon1 + math.atan2(
|
||||
math.sin(bearing) * math.sin(distance / EARTH_RADIUS) * math.cos(lat1),
|
||||
math.cos(distance / EARTH_RADIUS) - math.sin(lat1) * math.sin(lat2)
|
||||
math.sin(bearing) * math.sin(distance / cls.R) * math.cos(lat1),
|
||||
math.cos(distance / cls.R) - math.sin(lat1) * math.sin(lat2)
|
||||
)
|
||||
|
||||
lat2, lon2 = math.degrees(lat2), math.degrees(lon2)
|
||||
@@ -107,7 +140,7 @@ class OpenAirParser:
|
||||
def __init__(self, file: Path | str):
|
||||
self.file = None
|
||||
|
||||
self.context_type: Optional[AirspaceType] = None
|
||||
self.context_type: Optional[AirspaceItemType] = None
|
||||
self.context = {}
|
||||
|
||||
self.file = file
|
||||
@@ -116,7 +149,7 @@ class OpenAirParser:
|
||||
if not self.context_type:
|
||||
return
|
||||
|
||||
data = Airspace(type=self.context_type, **self.context)
|
||||
data = AirspaceItem(type=self.context_type, **self.context)
|
||||
|
||||
self.context_type = None
|
||||
self.context = {}
|
||||
@@ -194,19 +227,19 @@ class OpenAirParser:
|
||||
pass
|
||||
case 'AC':
|
||||
try:
|
||||
self.context_type = AirspaceType(value)
|
||||
self.context_type = AirspaceItemType(value)
|
||||
except ValueError as e:
|
||||
print(f"Neznam {e}, ignoruji")
|
||||
return
|
||||
case 'AN':
|
||||
if self.context_type == AirspaceType.AIRSPACE_G:
|
||||
if self.context_type == AirspaceItemType.AIRSPACE_G:
|
||||
match value[0:2]:
|
||||
case 'PG':
|
||||
self.context_type = AirspaceType.PARAGLIDING_ZONE
|
||||
self.context_type = AirspaceItemType.PARAGLIDING_ZONE
|
||||
case 'LK':
|
||||
self.context_type = AirspaceType.ATZ
|
||||
self.context_type = AirspaceItemType.ATZ
|
||||
case _:
|
||||
self.context_type = AirspaceType.TRAGA
|
||||
self.context_type = AirspaceItemType.TRAGA
|
||||
|
||||
self.context['info'] = value
|
||||
case 'AL':
|
||||
@@ -249,7 +282,7 @@ async def download_airspace(source, destination):
|
||||
async def save_airspace(destination):
|
||||
parser = OpenAirParser(destination)
|
||||
async with get_session() as db:
|
||||
await db.execute(delete(models.Airspace))
|
||||
await db.execute(delete(models.AirspaceItem))
|
||||
|
||||
for item in parser.parse():
|
||||
# if 'CTR KARLOVY VARY' not in item.info:
|
||||
@@ -261,7 +294,7 @@ async def save_airspace(destination):
|
||||
# print("---" * 10)
|
||||
|
||||
async with get_session() as db:
|
||||
await models.Airspace.create(db, {
|
||||
await models.AirspaceItem.create(db, {
|
||||
"country": "cz",
|
||||
"name": item.info,
|
||||
"type": item.type.value,
|
||||
@@ -18,10 +18,6 @@ def get_public_url(filename: Optional[str]) -> 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}")
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
from sqlalchemy import select
|
||||
from database import models
|
||||
from database.transaction import get_session
|
||||
|
||||
|
||||
class FlighPlanWeather:
|
||||
@staticmethod
|
||||
async def get_plans() -> list:
|
||||
async with get_session() as db:
|
||||
markers_without_weather = (await db.execute(
|
||||
select(models.FlightPlanMarker).filter(models.FlightPlanMarker.weather_info_id.is_(None))
|
||||
))
|
||||
flight_plan_ids = {m.flight_plan_id for m in markers_without_weather}
|
||||
|
||||
if not flight_plan_ids:
|
||||
return []
|
||||
|
||||
return (await db.execute(
|
||||
select(models.FlightPlan, models.FlightPlan.markers)
|
||||
.filter(models.FlightPlan.id.in_(flight_plan_ids))
|
||||
))
|
||||
|
||||
+35
-16
@@ -5,48 +5,67 @@ from sqlalchemy import select
|
||||
sys.path.insert(0, "/app/src")
|
||||
from paths import FLIGHT_GPX_TRACK_PATH # noqa
|
||||
from database import async_session, models # noqa
|
||||
from external.elevation import elevation_api, update_track_points_elevation # noqa
|
||||
from external.elevation import elevation_api # noqa
|
||||
from external.gpx_parser import GPXParser # noqa
|
||||
from database.transaction import get_session
|
||||
|
||||
|
||||
async def add_elevation_to_photos():
|
||||
async with get_session() as db:
|
||||
photos = (await db.scalars(
|
||||
async with async_session() as session:
|
||||
photos = (await session.scalars(
|
||||
select(models.Photo)
|
||||
.filter(models.Photo.terrain_elevation.is_(None))
|
||||
)).all()
|
||||
|
||||
coordinates = [
|
||||
{"lat": p.gps_latitude, "lng": p.gps_longitude, "id": p.id} for p in photos if p.gps_latitude or p.gps_longitude
|
||||
{"lat": p.gps_latitude, "lng": p.gps_longitude} for p in photos if p.gps_latitude or p.gps_longitude
|
||||
]
|
||||
photos_by_corrdinates = {(p.gps_latitude, p.gps_longitude): p for p in photos}
|
||||
if not coordinates:
|
||||
print("all done")
|
||||
return
|
||||
|
||||
points = await elevation_api.get_elevation_for_points(coordinates)
|
||||
for point in points:
|
||||
await models.Photo.update(db_session=db, id=point.id, data={"terrain_elevation": point.elevation})
|
||||
photo = photos_by_corrdinates[point['lat'], point['lng']]
|
||||
await models.Photo.update(db_session=session, obj=photo, data={"terrain_elevation": point['elevation']})
|
||||
await session.flush()
|
||||
await session.commit()
|
||||
|
||||
|
||||
async def add_elevation_to_tracks():
|
||||
async with get_session() as db:
|
||||
track_points = (await db.scalars(
|
||||
select(models.TrackPoint)
|
||||
.filter(models.TrackPoint.terrain_elevation.is_(None))
|
||||
async with async_session() as session:
|
||||
flights = (await session.scalars(
|
||||
select(models.Flight)
|
||||
.filter(models.Flight.has_terrain_elevation.is_(False))
|
||||
.filter(models.Flight.gpx_track_filename.isnot(None))
|
||||
)).all()
|
||||
|
||||
if not track_points:
|
||||
if not flights:
|
||||
print("all done")
|
||||
return
|
||||
await update_track_points_elevation(db, track_points)
|
||||
|
||||
for flight in flights:
|
||||
gpx_file = f"{FLIGHT_GPX_TRACK_PATH}/{flight.gpx_track_filename}"
|
||||
gpx = GPXParser(gpx_file)
|
||||
|
||||
coordinates = await gpx.get_coordinates()
|
||||
elevation = await elevation_api.get_elevation_for_points(coordinates)
|
||||
gpx_with_elevation = gpx.add_terrain_elevation(elevation)
|
||||
|
||||
output_name = f"terrain_{flight.gpx_track_filename[30:]}"
|
||||
gpx.write(gpx_with_elevation, output=f"{FLIGHT_GPX_TRACK_PATH}/{output_name}")
|
||||
await models.Flight.update(
|
||||
db_session=session, obj=flight, data={
|
||||
"has_terrain_elevation": True,
|
||||
"gpx_track_filename": output_name
|
||||
}
|
||||
)
|
||||
await session.flush()
|
||||
await session.commit()
|
||||
|
||||
|
||||
async def run_all():
|
||||
await asyncio.gather(
|
||||
add_elevation_to_photos(),
|
||||
add_elevation_to_tracks()
|
||||
)
|
||||
await asyncio.gather(add_elevation_to_photos(), add_elevation_to_tracks())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from PIL import Image, ImageDraw, ImageFilter
|
||||
from sqlalchemy import select
|
||||
|
||||
sys.path.insert(0, "/app/src")
|
||||
|
||||
from database import models # noqa
|
||||
from database.transaction import get_session # noqa
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class Runway:
|
||||
heading: int
|
||||
surface: str
|
||||
designator: str
|
||||
|
||||
|
||||
@dataclasses.dataclass()
|
||||
class Airport:
|
||||
name: str
|
||||
runways: list[Runway]
|
||||
|
||||
|
||||
async def get_airports():
|
||||
async with get_session() as db:
|
||||
airports_data = (await db.scalars(
|
||||
select(models.Airport)
|
||||
# .filter(models.Airport.icao_code.in_(["LKJH", "LKLT", "LKBE"]))
|
||||
)).all()
|
||||
runways_data = (await db.execute(
|
||||
select(models.Runway.designator, models.Runway.heading, models.Runway.surface, models.Runway.airport_id)
|
||||
.distinct()
|
||||
.order_by(models.Runway.designator)
|
||||
))
|
||||
|
||||
runways = defaultdict(set)
|
||||
for rwy in runways_data:
|
||||
runways[rwy.airport_id].add(rwy)
|
||||
|
||||
airports = []
|
||||
for airport in airports_data:
|
||||
airport_rwy = runways[airport.id]
|
||||
if not airport_rwy:
|
||||
continue
|
||||
|
||||
airports.append(
|
||||
Airport(
|
||||
name=airport.icao_code,
|
||||
runways=[Runway(heading=rwy.heading, surface=rwy.surface, designator=rwy.designator) for rwy in airport_rwy]
|
||||
)
|
||||
)
|
||||
|
||||
return airports
|
||||
|
||||
|
||||
def draw_airport(draw, icon_size):
|
||||
diameter = icon_size // 2
|
||||
|
||||
draw.ellipse(
|
||||
[(diameter // 2, diameter // 2), (icon_size - diameter // 2, icon_size - diameter // 2)],
|
||||
fill="#cccccc",
|
||||
outline="black"
|
||||
)
|
||||
return draw
|
||||
|
||||
|
||||
# def make_rectangle(l, w, theta, offset=(0, 0)):
|
||||
# c, s = math.cos(theta), math.sin(theta)
|
||||
# rect_coords = [(l / 2.0, w / 2.0), (l / 2.0, -w / 2.0), (-l / 2.0, -w / 2.0), (-l / 2.0, w / 2.0)]
|
||||
# return [(c * x - s * y + offset[0], s * x + c * y + offset[1]) for (x, y) in rect_coords]
|
||||
#
|
||||
# def draw_runway(airport_img, runway: Runway, size):
|
||||
# if runway.heading > 180:
|
||||
# return
|
||||
#
|
||||
# rwy = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
||||
#
|
||||
# draw = ImageDraw.Draw(rwy)
|
||||
#
|
||||
# width = 60
|
||||
# center = size // 2 - width // 2
|
||||
#
|
||||
# colors = {
|
||||
# "asphalt": "#333333",
|
||||
# "concrete": "#dddddd",
|
||||
# "grass": "#00ff00"
|
||||
# }
|
||||
#
|
||||
# draw.rectangle((center, 60, center + width, size-60), fill=colors[runway.surface])
|
||||
#
|
||||
# font = ImageFont.truetype('/app/static/Calibri.ttf', 60)
|
||||
# draw.text((center, size - width), runway.designator, font=font, fill="black")
|
||||
# rwy = rwy.rotate(-1 * runway.heading, expand=False)
|
||||
#
|
||||
# airport_img.paste(rwy, mask=rwy)
|
||||
|
||||
# print(f"Kreslim RWY {runway}")
|
||||
# length = size * 0.9
|
||||
# width = 40
|
||||
# vertices = make_rectangle(length, width, theta=runway.heading, offset=(250, 250))
|
||||
# draw.polygon(vertices, fill="#dddddd", outline="black")
|
||||
# return draw
|
||||
|
||||
|
||||
def add_edge_border(image, border_size=5, border_color=(255, 0, 0, 255)):
|
||||
mask = image.split()[3]
|
||||
edge_mask = mask.filter(ImageFilter.FIND_EDGES)
|
||||
|
||||
border_image = Image.new("RGBA", image.size, (0, 0, 0, 0))
|
||||
border_image.paste(border_color, mask=edge_mask)
|
||||
for _ in range(border_size - 1):
|
||||
edge_mask = edge_mask.filter(ImageFilter.MaxFilter(3))
|
||||
border_image.paste(border_color, mask=edge_mask)
|
||||
bordered_image = Image.alpha_composite(border_image, image)
|
||||
return bordered_image
|
||||
|
||||
def draw_runways(airport_img, runways: list[Runway], size: int):
|
||||
rwy_width = 60
|
||||
rwy_margin = 10
|
||||
center = size // 2
|
||||
|
||||
all_runways_width = len(runways) * rwy_width + (len(runways) - 1) * rwy_margin
|
||||
|
||||
start = center - all_runways_width // 2
|
||||
|
||||
colors = {
|
||||
"asphalt": "#777777",
|
||||
"concrete": "#cccccc",
|
||||
"grass": "#00aa00"
|
||||
}
|
||||
|
||||
rwy = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
||||
heading_sum = 0
|
||||
draw = ImageDraw.Draw(rwy)
|
||||
for runway in runways:
|
||||
top_left = (start, 80)
|
||||
bottom_right = (start + rwy_width, size - 80)
|
||||
|
||||
draw.rectangle((*top_left, *bottom_right), fill=colors[runway.surface])
|
||||
# draw.text((start, size - 60), runway.designator, font=font, fill="black")
|
||||
|
||||
start += rwy_width + rwy_margin
|
||||
heading_sum += runway.heading
|
||||
|
||||
rwy = rwy.rotate(-1 * heading_sum // len(runways), expand=False)
|
||||
airport_img.paste(rwy, mask=rwy)
|
||||
|
||||
|
||||
async def generate_icon(airport: Airport):
|
||||
icon_size = 500
|
||||
|
||||
# creating new Image object
|
||||
img = Image.new("RGBA", (icon_size, icon_size), (0, 0, 0, 0))
|
||||
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
if len(airport.name) == 4:
|
||||
draw = draw_airport(draw, icon_size)
|
||||
|
||||
parallel_runways = defaultdict(list)
|
||||
for runway in airport.runways:
|
||||
key = runway.designator[:2]
|
||||
parallel_runways[key].append(runway)
|
||||
|
||||
for designator, runways in parallel_runways.items():
|
||||
draw_runways(img, runways, icon_size)
|
||||
|
||||
img = add_edge_border(img, border_size=10, border_color=(0, 0, 255))
|
||||
|
||||
img = img.resize((40, 40), resample=Image.LANCZOS)
|
||||
img.save(f'/app/static/airports/{airport.name}.png', 'PNG')
|
||||
# img.save(f'/app/static/airports/{airport.name}.webp', 'WEBP')
|
||||
|
||||
print(airport.name, [(rwy.designator, rwy.surface) for rwy in airport.runways])
|
||||
|
||||
|
||||
async def generate_icons():
|
||||
airports = await get_airports()
|
||||
|
||||
for airport in airports:
|
||||
await generate_icon(airport)
|
||||
# return
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(generate_icons())
|
||||
@@ -1,15 +0,0 @@
|
||||
import asyncio
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, "/app/src")
|
||||
|
||||
from background_jobs.flight_plan_track import generate_track_for_flight_plan
|
||||
|
||||
|
||||
async def run_all():
|
||||
await generate_track_for_flight_plan(11)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(run_all())
|
||||
@@ -1,26 +0,0 @@
|
||||
import asyncio
|
||||
import sys
|
||||
from sqlalchemy import select
|
||||
|
||||
sys.path.insert(0, "/app/src")
|
||||
from utils.flight_track_helpers import save_track_from_gpx_to_db
|
||||
from database import models
|
||||
from database.transaction import get_session
|
||||
|
||||
|
||||
async def migrate_gpx():
|
||||
async with get_session() as db:
|
||||
flights = (
|
||||
await db.execute(
|
||||
select(models.Flight.id, models.Flight.gpx_track_filename)
|
||||
.filter(models.Flight.gpx_track_filename.is_not(None)))
|
||||
).all()
|
||||
flight_tracks = {f.id: f.gpx_track_filename for f in flights}
|
||||
|
||||
for id, gpx_filename in flight_tracks.items():
|
||||
await save_track_from_gpx_to_db(flight_id=id, gpx_filename=gpx_filename)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(migrate_gpx())
|
||||
@@ -1,49 +0,0 @@
|
||||
import asyncio
|
||||
import sys
|
||||
from sqlalchemy import select
|
||||
|
||||
sys.path.insert(0, '/app/src')
|
||||
from background_jobs.flight_title_photo import add_circular_avatar # noqa
|
||||
from database import models # noqa
|
||||
from database.transaction import get_session # noqa
|
||||
|
||||
|
||||
async def flight_title_photos():
|
||||
async with get_session() as db:
|
||||
flights = (await db.execute(
|
||||
select(models.Photo, models.Flight, models.User)
|
||||
.join(models.Flight.title_photo)
|
||||
.join(models.Flight.created_by)
|
||||
)).all()
|
||||
|
||||
for photo, flight, pilot in flights:
|
||||
await add_circular_avatar(
|
||||
base_image_path=f'/app/uploads/photos/{flight.id}/{photo.filename}.{photo.filename_extension}',
|
||||
avatar_image_path=f'/app/uploads/profile/{pilot.id}/{pilot.avatar_image_filename}',
|
||||
output_image_path=f'/app/uploads/photos/{flight.id}/title_photo.jpg',
|
||||
)
|
||||
|
||||
|
||||
async def user_title_photos():
|
||||
async with get_session() as db:
|
||||
users = (await db.execute(
|
||||
select(models.User.id, models.User.title_image_filename, models.User.avatar_image_filename)
|
||||
.filter(models.User.title_image_filename != '')
|
||||
)).all()
|
||||
|
||||
for user_id, title_image, avatar_image in users:
|
||||
await add_circular_avatar(
|
||||
base_image_path=f"/app/uploads/profile/{user_id}/{title_image}",
|
||||
avatar_image_path=f"/app/uploads/profile/{user_id}/{avatar_image}",
|
||||
output_image_path=f'/app/uploads/profile/{user_id}/title_photo.jpg'
|
||||
)
|
||||
|
||||
|
||||
async def generate_images():
|
||||
await user_title_photos()
|
||||
await flight_title_photos()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(generate_images())
|
||||
@@ -1,7 +1,4 @@
|
||||
import os
|
||||
import re
|
||||
import uuid
|
||||
from strawberry.file_uploads import Upload
|
||||
|
||||
|
||||
def delete_file(path: str, silent: bool = False):
|
||||
@@ -17,31 +14,3 @@ def check_directories(path: str):
|
||||
os.makedirs(path, exist_ok=True)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
async def handle_file_upload(
|
||||
file: Upload,
|
||||
path: str,
|
||||
filename_maxlength: int = 64,
|
||||
uid_prefix: bool = True,
|
||||
overwrite: bool = True
|
||||
) -> str:
|
||||
check_directories(path)
|
||||
|
||||
prefix = f"{uuid.uuid4()}-" if uid_prefix else ""
|
||||
filename = f"{prefix}{file.filename}"[-1 * filename_maxlength:]
|
||||
|
||||
# sanitize filename
|
||||
filename = re.sub(r'[^\w_. -]', '', filename).replace(" ", "-")
|
||||
|
||||
target_path = f"{path}/{filename}"
|
||||
|
||||
if os.path.exists(target_path) and not overwrite:
|
||||
raise FileExistsError(f"File {filename} already exists")
|
||||
|
||||
content = await file.read()
|
||||
image = open(target_path, "wb")
|
||||
image.write(content)
|
||||
image.close()
|
||||
|
||||
return filename
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
import asyncio
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import delete, func, select, text
|
||||
from sqlalchemy import delete
|
||||
from strawberry.file_uploads import Upload
|
||||
|
||||
from database import models
|
||||
from database.transaction import get_session
|
||||
from external.gpx_parser import GPXParser
|
||||
from paths import FLIGHT_GPX_TRACK_PATH
|
||||
from utils.file import handle_file_upload, delete_file
|
||||
|
||||
|
||||
def get_bounds(coordinates: list[dict[str, float]]) -> list[tuple[float, float]]:
|
||||
latitudes = [c['lat'] for c in coordinates]
|
||||
longitudes = [c['lng'] for c in coordinates]
|
||||
|
||||
return [
|
||||
(min(latitudes), min(longitudes)),
|
||||
(min(latitudes), max(longitudes)),
|
||||
(max(latitudes), min(longitudes)),
|
||||
(max(latitudes), max(latitudes))
|
||||
]
|
||||
|
||||
|
||||
async def get_airport_id_by_gps(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_basic_flight_info_from_gpx(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(
|
||||
get_airport_id_by_gps(coordinates[0]['lat'], coordinates[0]['lng']),
|
||||
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 handle_upload_gpx(gpx_track: Upload, flight_id: int):
|
||||
filename = await handle_file_upload(gpx_track, FLIGHT_GPX_TRACK_PATH)
|
||||
await save_track_from_gpx_to_db(gpx_filename=filename, flight_id=flight_id)
|
||||
return filename
|
||||
|
||||
|
||||
async def save_track_from_gpx_to_db(gpx_filename: str, flight_id: int | None = None):
|
||||
try:
|
||||
gpx_parser = GPXParser(file=f"{FLIGHT_GPX_TRACK_PATH}/{gpx_filename}")
|
||||
except OSError as e:
|
||||
print(f"Cannot process {gpx_filename}: {e}")
|
||||
return
|
||||
|
||||
altitudes = await gpx_parser.get_altitude()
|
||||
terrain_elevations = await gpx_parser.get_terrain_elevation()
|
||||
coordinates = await gpx_parser.get_coordinates()
|
||||
speeds = await gpx_parser.get_speed()
|
||||
magnetic_variations = await gpx_parser.get_magnetic_variation()
|
||||
times = await gpx_parser.get_times()
|
||||
|
||||
track_data = {
|
||||
"bounds": get_bounds(coordinates),
|
||||
"min_speed": await gpx_parser.get_min_speed(),
|
||||
"avg_speed": await gpx_parser.get_avg_speed(),
|
||||
"max_speed": await gpx_parser.get_max_speed(),
|
||||
"total_duration": (await gpx_parser.get_total_duration()).seconds,
|
||||
"max_altitude": await gpx_parser.get_max_altitude(),
|
||||
"avg_altitude": await gpx_parser.get_avg_altitude(),
|
||||
}
|
||||
|
||||
async with get_session() as db:
|
||||
flight = await models.Flight.get_one(db, id=flight_id)
|
||||
|
||||
if flight.track_id is None:
|
||||
track = await models.Track.create(db, {
|
||||
**track_data,
|
||||
"created_by_id": flight.created_by_id
|
||||
})
|
||||
flight.track_id = track.id
|
||||
else:
|
||||
await db.execute(delete(models.TrackPoint).filter(models.TrackPoint.track_id == flight.track_id))
|
||||
track = await models.Track.get_one(db, id=flight.track_id)
|
||||
await models.Track.update(db, data=track_data, obj=track)
|
||||
|
||||
for i in range(len(coordinates)):
|
||||
await models.TrackPoint.create(db, {
|
||||
"track_id": track.id,
|
||||
"altitude": altitudes[i] if i < len(altitudes) else None,
|
||||
"magnetic_variation": magnetic_variations[i] if i < len(magnetic_variations) else None,
|
||||
"terrain_elevation": terrain_elevations[i] if i < len(terrain_elevations) else None,
|
||||
"speed": speeds[i] if i < len(speeds) else None,
|
||||
"gps_latitude": coordinates[i]['lat'],
|
||||
"gps_longitude": coordinates[i]['lng'],
|
||||
"timestamp": times[i]
|
||||
})
|
||||
@@ -1,63 +1,6 @@
|
||||
from typing import Tuple
|
||||
import math
|
||||
|
||||
from geopy import Point
|
||||
from geopy.distance import geodesic
|
||||
|
||||
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
|
||||
|
||||
|
||||
def gps_to_decimal(input: Tuple[float, float, float]) -> float:
|
||||
d, m, s = input
|
||||
return d + (m / 60.0) + (s / 3600.0)
|
||||
|
||||
|
||||
def generate_points(start: tuple[float, float], end: tuple[float, float], interval_meters: int = 500):
|
||||
start = Point(start[0], start[1])
|
||||
end = Point(end[0], end[1])
|
||||
|
||||
total_distance = geodesic(start, end).meters
|
||||
num_intervals = int(total_distance // interval_meters)
|
||||
|
||||
points = [start]
|
||||
|
||||
for i in range(1, num_intervals + 1):
|
||||
# Calculate intermediate point
|
||||
intermediate_distance = interval_meters * i
|
||||
intermediate_point = geodesic(meters=intermediate_distance).destination(start, get_bearing(start, end))
|
||||
points.append(intermediate_point)
|
||||
|
||||
points.append(end)
|
||||
|
||||
return points
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import typing
|
||||
from itertools import islice
|
||||
|
||||
|
||||
def chunks(items: list | dict, n: int) -> typing.Generator:
|
||||
"""Yield successive n-sized chunks from lst."""
|
||||
|
||||
if isinstance(items, list):
|
||||
for i in range(0, len(items), n):
|
||||
yield items[i:i + n]
|
||||
elif isinstance(items, dict):
|
||||
it = iter(items)
|
||||
for i in range(0, len(items), n):
|
||||
yield {k: items[k] for k in islice(it, n)}
|
||||
|
||||
|
||||
def evenly_spaced_elements(lst: list, count: int) -> list:
|
||||
if count > len(lst):
|
||||
return lst
|
||||
interval = (len(lst) - 1) / (count - 1) if count > 1 else 0
|
||||
return [lst[int(round(i * interval))] for i in range(count)]
|
||||
@@ -0,0 +1,33 @@
|
||||
import os
|
||||
import re
|
||||
import uuid
|
||||
from strawberry.file_uploads import Upload
|
||||
from utils.file import check_directories
|
||||
|
||||
|
||||
async def handle_file_upload(
|
||||
file: Upload,
|
||||
path: str,
|
||||
filename_maxlength: int = 64,
|
||||
uid_prefix: bool = True,
|
||||
overwrite: bool = True
|
||||
) -> str:
|
||||
check_directories(path)
|
||||
|
||||
prefix = f"{uuid.uuid4()}-" if uid_prefix else ""
|
||||
filename = f"{prefix}{file.filename}"[-1 * filename_maxlength:]
|
||||
|
||||
# sanitize filename
|
||||
filename = re.sub(r'[^\w_. -]', '', filename).replace(" ", "-")
|
||||
|
||||
target_path = f"{path}/{filename}"
|
||||
|
||||
if os.path.exists(target_path) and not overwrite:
|
||||
raise FileExistsError(f"File {filename} already exists")
|
||||
|
||||
content = await file.read()
|
||||
image = open(target_path, "wb")
|
||||
image.write(content)
|
||||
image.close()
|
||||
|
||||
return filename
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,44 @@
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import pytest
|
||||
from sqlalchemy.ext.asyncio import async_sessionmaker, AsyncSession
|
||||
|
||||
ROOT_DIR = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT_DIR / "src"))
|
||||
|
||||
from logger import log
|
||||
|
||||
|
||||
def pytest_sessionstart():
|
||||
os.environ['MYSQL_HOST'] = 'localhost'
|
||||
os.environ['MYSQL_USER'] = 'root'
|
||||
os.environ['MYSQL_PASSWORD'] = 'root'
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
async def db():
|
||||
# from database.transaction import get_session
|
||||
#
|
||||
# async with get_session() as db:
|
||||
# yield db
|
||||
# await db.rollback()
|
||||
|
||||
|
||||
import database
|
||||
from database.config import create_db_engine
|
||||
|
||||
test_engine = create_db_engine()
|
||||
test_async_session = async_sessionmaker(test_engine, expire_on_commit=True, class_=AsyncSession)
|
||||
|
||||
database.config.engine = test_engine
|
||||
database.async_session = test_async_session
|
||||
|
||||
async with test_async_session() as session:
|
||||
async with session.begin():
|
||||
try:
|
||||
yield session
|
||||
await session.rollback()
|
||||
except Exception as e:
|
||||
log.exception("Error", exc_info=e)
|
||||
await session.rollback()
|
||||
@@ -0,0 +1,16 @@
|
||||
from typing import Optional
|
||||
|
||||
from database import models
|
||||
from utils.str_utils import random_str
|
||||
|
||||
|
||||
async def create_pilot(db, data: Optional[dict] = None) -> models.User:
|
||||
if not data:
|
||||
data = {}
|
||||
|
||||
return await models.User.create(db, {
|
||||
"email": data.pop("email", f"{random_str(5)}@{random_str(10)}.cz"),
|
||||
"name": data.pop("name", "Pepa Tester"),
|
||||
"description": data.pop("description", ""),
|
||||
"password_hashed": data.pop("password_hashed", "***")
|
||||
})
|
||||
@@ -0,0 +1,49 @@
|
||||
from collections import namedtuple
|
||||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
|
||||
from database import models
|
||||
from graphql_schema.entities.resolvers.aircraft import AircraftQueryResolver
|
||||
from tests.factories import create_pilot
|
||||
from utils.str_utils import random_str
|
||||
|
||||
AircraftData = namedtuple('AircraftData', ["aircraft", "user"])
|
||||
|
||||
|
||||
async def create_aircraft(db, data: Optional[dict] = None) -> AircraftData:
|
||||
if not data:
|
||||
data = {}
|
||||
|
||||
if not data.get("created_by_id"):
|
||||
pilot = await create_pilot(db)
|
||||
data["created_by_id"] = pilot.id
|
||||
else:
|
||||
pilot = await models.User.get_one(db, id=data['created_by_id'])
|
||||
|
||||
return AircraftData(
|
||||
aircraft=await models.Aircraft.create(
|
||||
db,
|
||||
{
|
||||
"call_sign": data.pop("call_sign", f"OK-{random_str(4)}"),
|
||||
"is_public": data.pop("is_public", False),
|
||||
"created_by_id": data.pop('created_by_id'),
|
||||
"seats": data.pop("seats", 2),
|
||||
**data
|
||||
}
|
||||
),
|
||||
user=pilot
|
||||
)
|
||||
|
||||
|
||||
async def test_private_aircraft(db):
|
||||
aircraft_data = await create_aircraft(db)
|
||||
|
||||
resolver = AircraftQueryResolver()
|
||||
|
||||
params =[
|
||||
{"id": aircraft_data.aircraft.id, "created_by_id": 234}
|
||||
]
|
||||
for param in params:
|
||||
aircraft = await resolver.get_one(**param)
|
||||
assert aircraft.id == aircraft_data.aircraft.id
|
||||
@@ -0,0 +1,3 @@
|
||||
async def test_simple_query(db):
|
||||
# query_builder = QueryBuilder()
|
||||
pass
|
||||
Reference in New Issue
Block a user