34 Commits
Author SHA1 Message Date
Michal Kváček 3e8f677b6f Fix reconnectu poolu 2026-09-23 09:46:02 +02:00
Michal Kváček 6553da41f2 Fix kontakt formu 2026-09-14 13:28:57 +02:00
Michal Kváček 420774f1b4 Fix nacitani seznamu letu 2026-01-26 16:23:35 +01:00
Michal Kváček 888351baac lepsi podpora strankovani, bugfixing, vyhledavani 2026-01-06 22:03:49 +01:00
Michal Kváček d794208193 Fix map 2025-09-18 22:02:25 +02:00
Michal Kváček 71911c0905 Nove zavislosti, logovani, refaktoring 2025-04-03 17:35:50 +02:00
Michal Kváček a7fda95c29 Bugfixing 2025-03-17 16:38:40 +01:00
Michal Kváček 2f824e264f Bugfix 2025-03-10 05:40:18 +01:00
Michal Kváček beee16bf80 Update zavislosti 2025-03-05 13:24:08 +01:00
Michal Kváček 77c9242944 Prepis strankovani na extension, sjednoceni volani resolveru 2025-03-05 13:23:38 +01:00
Michal Kváček c6316262e5 Fix ukladani fotek, pokus o odstraneni Optional 2024-10-09 06:53:42 +02:00
Michal Kváček d31f1c0c61 Bugfixing 2024-10-03 22:41:24 +02:00
Michal Kváček ca3f6ef07c Lepsi stahovani informaci o pocasi 2024-09-30 06:43:11 +02:00
Michal Kváček 186dbd0720 Fix generovani titulni fotky pro og:image 2024-09-03 16:24:22 +02:00
Michal Kváček 365a1fc1b0 Fix nastaveni titulni fotky 2024-08-29 06:25:26 +02:00
Michal Kváček 0a6b19f334 Uprava welcome emailu! 2024-08-28 22:32:26 +02:00
Michal Kváček 3081580c21 Fix emailu s resetem hesla 2024-08-28 22:30:09 +02:00
Michal Kváček 94639da820 Sada bugfixu 2024-08-12 06:27:28 +02:00
Michal Kváček 9e6b0b574c Stahovani elevace 2024-07-29 06:23:06 +02:00
Michal Kváček 10bc0847b6 Trasu z GPX nahravat do DB 2024-07-25 19:50:09 +02:00
Michal Kváček e541508f75 API pro podporu vytvareni letu z planu 2024-06-30 02:31:37 +02:00
Michal Kváček ac3b496459 Priprava modelu na GPX v DB 2024-06-24 16:22:50 +02:00
Michal Kváček 2bf737b9d6 Uprava stahovani elevace, slouceni gps utils do jednoho filu, retryable, pridani social image k pilotovi a letu 2024-06-03 23:07:23 +02:00
Michal Kváček ba5e73108f Stahovani pocasi, drobne upravy 2024-05-27 22:30:44 +02:00
Michal Kváček 94613a5b1b ZoNacitani frekvenci, RWY 2024-05-15 09:33:41 +02:00
Michal Kváček 62e46f37ce Bugfixing 2024-05-07 08:24:29 +02:00
Michal Kváček a706303155 Ukladani metadat k planum 2024-04-30 22:46:40 +02:00
Michal Kváček c8138860ca Nova struktura DB pro data z OpenAIP API 2024-04-29 06:44:33 +02:00
Michal Kváček c2b066d371 Fix nacitani trasy pro plan jen s jednim bodem 2024-04-17 13:17:59 +02:00
Michal Kváček cee5de003f Zakladni verze planovace 2024-04-15 18:13:37 +02:00
Michal Kváček 5f0a1da052 Merge branch 'master' of kvacek.cz:poletani.cz/api into planovac 2024-04-02 06:14:03 +02:00
Michal Kváček 63e2eed8c2 draft: crud pro planovac 2024-03-25 19:04:24 +01:00
Michal Kváček 01f8a616bf Merge branch 'master' of kvacek.cz:poletani.cz/api into planovac 2024-03-23 20:28:21 +01:00
Michal Kváček 24825108a7 Zaklad pro planovac letu 2024-03-23 20:15:47 +01:00
129 changed files with 3470 additions and 8574 deletions
+1
View File
@@ -2,5 +2,6 @@
max-line-length = 120 max-line-length = 120
exclude = exclude =
venv venv
.venv
__pycache__ __pycache__
alembic alembic
+2
View File
@@ -10,3 +10,5 @@ uploads/*
!uploads/.gitkeep !uploads/.gitkeep
src/emails/templates/html/* src/emails/templates/html/*
.env .env
/static/airports/
/static/airports/.gitkeep
+9 -7
View File
@@ -7,17 +7,19 @@ COPY ./src/emails/templates/mjml /app/templates
RUN mjml /app/templates/* -o /app/templates/html/ RUN mjml /app/templates/* -o /app/templates/html/
FROM python:3.11-alpine AS app FROM python:3.13-alpine AS app
# protoze xfs uzivatel ma id=33
# RUN deluser xfs
# uzivatel www-data v alpine neexistuje
RUN delgroup www-data && addgroup -g 33 -S www-data && adduser -u 33 -D -S -G www-data www-data
RUN apk update && \ RUN apk update && \
apk add --no-cache tzdata mariadb-client mariadb-dev && \ apk add --no-cache tzdata mariadb-client mariadb-dev && \
pip install --upgrade pip 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 requirements.txt /app/requirements.txt
COPY --from=email_builder /app/templates/html/ /app/src/email/templates/html/ COPY --from=email_builder /app/templates/html/ /app/src/email/templates/html/
@@ -34,4 +36,4 @@ COPY . /app
WORKDIR /app WORKDIR /app
CMD ["uvicorn", "src.asgi:app", "--reload", "--reload-dir=/app", "--port=8000", "--host=0.0.0.0", "--log-level=debug"] CMD [ "gunicorn", "src.asgi:app", "--config=/app/config/gunicorn.py" ]
@@ -0,0 +1,38 @@
"""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 ###
@@ -0,0 +1,38 @@
"""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 ###
@@ -0,0 +1,42 @@
"""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 ###
@@ -0,0 +1,116 @@
"""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 ###
@@ -0,0 +1,32 @@
"""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 ###
@@ -0,0 +1,32 @@
"""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 ###
@@ -0,0 +1,45 @@
"""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 ###
@@ -0,0 +1,30 @@
"""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 ###
@@ -0,0 +1,56 @@
"""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 ###
@@ -0,0 +1,30 @@
"""add weather info to flight plan markers
Revision ID: 8a67af3909aa
Revises: 790ba00fc021
Create Date: 2024-05-16 06:19:13.112911
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '8a67af3909aa'
down_revision = '790ba00fc021'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('flight_plan_marker', sa.Column('weather_info_id', sa.Integer(), nullable=True))
op.create_foreign_key(None, 'flight_plan_marker', 'weather_info', ['weather_info_id'], ['id'])
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'flight_plan_marker', type_='foreignkey')
op.drop_column('flight_plan_marker', 'weather_info_id')
# ### end Alembic commands ###
@@ -0,0 +1,28 @@
"""add flight_plan.description
Revision ID: a3e1f1a37899
Revises: 8a67af3909aa
Create Date: 2024-06-11 16:46:29.786830
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'a3e1f1a37899'
down_revision = '8a67af3909aa'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('flight_plan', sa.Column('description', sa.Text(), server_default='', nullable=False))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('flight_plan', 'description')
# ### end Alembic commands ###
@@ -0,0 +1,54 @@
"""add track to flight
Revision ID: f9e1e8a01683
Revises: a3e1f1a37899
Create Date: 2024-06-24 16:08:01.084570
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = 'f9e1e8a01683'
down_revision = 'a3e1f1a37899'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('track',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('bounds', mysql.JSON(), nullable=False),
sa.Column('created_by_id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['created_by_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('track_point',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('timestamp', sa.DateTime(), nullable=False),
sa.Column('track_id', sa.Integer(), nullable=False),
sa.Column('gps_latitude', sa.Float(), nullable=False),
sa.Column('gps_longitude', sa.Float(), nullable=False),
sa.Column('elevation', sa.Float(), nullable=True),
sa.ForeignKeyConstraint(['track_id'], ['track.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.add_column('flight', sa.Column('track_id', sa.Integer(), nullable=True))
op.create_foreign_key(None, 'flight', 'track', ['track_id'], ['id'])
op.add_column('flight_plan', sa.Column('track_id', sa.Integer(), nullable=True))
op.create_foreign_key(None, 'flight_plan', 'track', ['track_id'], ['id'])
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'flight_plan', type_='foreignkey')
op.drop_column('flight_plan', 'track_id')
op.drop_constraint(None, 'flight', type_='foreignkey')
op.drop_column('flight', 'track_id')
op.drop_table('track_point')
op.drop_table('track')
# ### end Alembic commands ###
@@ -0,0 +1,30 @@
"""add flight.flight_plan_id fk
Revision ID: 989152b7184a
Revises: f9e1e8a01683
Create Date: 2024-06-27 17:09:19.933094
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '989152b7184a'
down_revision = 'f9e1e8a01683'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('flight', sa.Column('flight_plan_id', sa.Integer(), nullable=True))
op.create_foreign_key(None, 'flight', 'flight_plan', ['flight_plan_id'], ['id'])
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'flight', type_='foreignkey')
op.drop_column('flight', 'flight_plan_id')
# ### end Alembic commands ###
@@ -0,0 +1,40 @@
"""Add poi and airport id to flight plan track marker
Revision ID: 8cc01c03e980
Revises: 989152b7184a
Create Date: 2024-06-30 00:27:08.976891
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '8cc01c03e980'
down_revision = '989152b7184a'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('airport', 'openaip_id',
existing_type=mysql.VARCHAR(length=32),
nullable=True)
op.add_column('flight_plan_marker', sa.Column('airport_id', sa.Integer(), nullable=True))
op.add_column('flight_plan_marker', sa.Column('point_of_interest_id', sa.Integer(), nullable=True))
op.create_foreign_key(None, 'flight_plan_marker', 'point_of_interest', ['point_of_interest_id'], ['id'])
op.create_foreign_key(None, 'flight_plan_marker', 'airport', ['airport_id'], ['id'])
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'flight_plan_marker', type_='foreignkey')
op.drop_constraint(None, 'flight_plan_marker', type_='foreignkey')
op.drop_column('flight_plan_marker', 'point_of_interest_id')
op.drop_column('flight_plan_marker', 'airport_id')
op.alter_column('airport', 'openaip_id',
existing_type=mysql.VARCHAR(length=32),
nullable=False)
# ### end Alembic commands ###
@@ -0,0 +1,46 @@
"""add speed, elevation, altitude to track
Revision ID: 03de3f7f8fdd
Revises: 8cc01c03e980
Create Date: 2024-07-11 07:45:59.376941
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '03de3f7f8fdd'
down_revision = '8cc01c03e980'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('track', sa.Column('min_speed', sa.Float(), nullable=True))
op.add_column('track', sa.Column('max_speed', sa.Float(), nullable=True))
op.add_column('track', sa.Column('avg_speed', sa.Float(), nullable=True))
op.add_column('track', sa.Column('max_altitude', sa.Float(), nullable=True))
op.add_column('track', sa.Column('avg_altitude', sa.Float(), nullable=True))
op.add_column('track', sa.Column('total_duration', sa.Integer(), nullable=True, comment='Total duration in seconds'))
op.add_column('track_point', sa.Column('terrain_elevation', sa.Float(), nullable=True))
op.add_column('track_point', sa.Column('speed', sa.Float(), nullable=True))
op.add_column('track_point', sa.Column('altitude', sa.Float(), nullable=True))
op.add_column('track_point', sa.Column('magnetic_variation', sa.Float(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('track_point', 'magnetic_variation')
op.drop_column('track_point', 'altitude')
op.drop_column('track_point', 'speed')
op.drop_column('track_point', 'terrain_elevation')
op.drop_column('track', 'total_duration')
op.drop_column('track', 'avg_altitude')
op.drop_column('track', 'max_altitude')
op.drop_column('track', 'avg_speed')
op.drop_column('track', 'max_speed')
op.drop_column('track', 'min_speed')
# ### end Alembic commands ###
@@ -0,0 +1,33 @@
"""delete duplicated column with altitude
Revision ID: e3620deb41b6
Revises: 03de3f7f8fdd
Create Date: 2024-07-15 06:35:13.536320
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = 'e3620deb41b6'
down_revision = '03de3f7f8fdd'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.rename_table("flight_track", "flight_turn_point")
op.create_index(op.f('ix_track_point_timestamp'), 'track_point', ['timestamp'], unique=False)
op.drop_column('track_point', 'elevation')
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('track_point', sa.Column('elevation', mysql.FLOAT(), nullable=True))
op.drop_index(op.f('ix_track_point_timestamp'), table_name='track_point')
op.rename_table("flight_turn_point", "flight_track")
# ### end Alembic commands ###
@@ -0,0 +1,35 @@
"""prejmenovani date_time u WeatherInfo
Revision ID: db036549f318
Revises: e3620deb41b6
Create Date: 2025-09-16 09:26:04.082396
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = 'db036549f318'
down_revision = 'e3620deb41b6'
branch_labels = None
depends_on = None
def upgrade() -> None:
op.create_unique_constraint(None, 'airport', ['openaip_id'])
op.create_index(op.f('ix_flight_turn_point_order'), 'flight_turn_point', ['order'], unique=False)
op.alter_column(
"weather_info", "datetime", new_column_name="date_time", existing_type=sa.DateTime, nullable=False
)
def downgrade() -> None:
op.alter_column(
"weather_info", "date_time", new_column_name="datetime",
existing_type=sa.DateTime, nullable=False
)
op.drop_index(op.f('ix_flight_turn_point_order'), table_name='flight_turn_point')
op.drop_constraint(None, 'airport', type_='unique')
# ### end Alembic commands ###
+18
View File
@@ -0,0 +1,18 @@
import os
# https://docs.gunicorn.org/en/stable/settings.html#settings
workers = 1 if os.environ.get("APP_ENV") == "development" else os.cpu_count()
bind = "0.0.0.0:8000"
worker_class = "uvicorn.workers.UvicornWorker"
worker_connections = 1000 # default
threads = 1 # default
# restart workeru
max_requests = 500000
max_requests_jitter = 10000
timeout = 30 # default
graceful_timeout = 30 # default
keepalive = 600 # s klientem udrzuje nginx
-4
View File
@@ -1,4 +0,0 @@
pytest
mypy
flake8
pytest-asyncio
+2 -2
View File
@@ -3,12 +3,12 @@ version: "3.3"
services: services:
api: api:
image: "docker.kvacek.cz/poletani/api:latest" image: "docker.kvacek.cz/poletani/api:latest"
command: "uvicorn src.asgi:app --port=8000 --host=0.0.0.0 --workers 4"
network_mode: "host" network_mode: "host"
restart: "always" restart: "always"
env_file: env_file:
- .env - ../.env
environment: environment:
SENTRY_DSN: "https://184d1d6bd39f4fbb94804ae84e9afdd5@o472821.ingest.sentry.io/5506983" SENTRY_DSN: "https://184d1d6bd39f4fbb94804ae84e9afdd5@o472821.ingest.sentry.io/5506983"
volumes: volumes:
- ./uploads:/app/uploads - ./uploads:/app/uploads
- ./static:/app/static
+6 -8
View File
@@ -3,10 +3,11 @@ services:
image: mariadb:10.11 image: mariadb:10.11
ports: ports:
- 3306:3306 - 3306:3306
env_file:
- .env
environment: environment:
- MYSQL_USER=root MARIADB_ROOT_PASSWORD: "rootroot"
- MYSQL_ROOT_PASSWORD=root
- MYSQL_DATABASE=ull_tracker
volumes: volumes:
# - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro # - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
- db-data:/var/lib/mysql:rw - db-data:/var/lib/mysql:rw
@@ -37,11 +38,8 @@ services:
scrapy: scrapy:
build: build:
context: ../crawlers context: ../crawlers
environment: env_file:
- MYSQL_USER=root - .env
- MYSQL_PASSWORD=root
- MYSQL_DATABASE=ull_tracker
- MYSQL_HOST=db
volumes: volumes:
- db-data:/var/lib/mysql:rw - db-data:/var/lib/mysql:rw
- ../crawlers:/app - ../crawlers:/app
-2
View File
@@ -1,2 +0,0 @@
[pytest]
asyncio_mode = auto
+19 -13
View File
@@ -1,16 +1,22 @@
fastapi==0.110.0 fastapi==0.115.7
fastapi-jwt==0.2.0 fastapi-jwt[authlib]==0.3.0
fastapi-mail==1.4.1 bcrypt==4.2.1
strawberry-graphql[fastapi]==0.217.1 fastapi-mail==1.4.2
uvicorn==0.26.0 strawberry-graphql[fastapi]==0.258.0
sqlalchemy[asyncio] == 2.0.25
gunicorn == 23.0.0
uvicorn[standard] == 0.34.0
sqlalchemy[asyncio] == 2.0.54
aiomysql==0.2.0 aiomysql==0.2.0
alembic==1.13.1 alembic==1.14.1
passlib==1.7.4 passlib==1.7.4
pydantic==2.5.3 pydantic==2.10.6
sentry-sdk[fastapi] sentry-sdk[fastapi]
pillow==10.2.0 pillow==11.1.0
exif==1.6.0 exif==1.6.1
aiocache==0.12.2 aiocache==0.12.3
aiohttp==3.9.3 aiohttp==3.11.11
lxml==5.1.0 lxml==5.3.0
colorlog
+18 -24
View File
@@ -1,40 +1,34 @@
from aiohttp import ClientResponseError from sqlalchemy import select
from database import models from database import models
from database.transaction import get_session from database.transaction import get_session
from external.elevation import elevation_api from decorators.retryable import retryable
from external.gpx_parser import GPXParser from external.elevation import elevation_api, update_track_points_elevation
from paths import FLIGHT_GPX_TRACK_PATH
async def add_terrain_elevation_to_flight(flight_id: int, gpx_filename: str): @retryable
gpx_parser = GPXParser(f"{FLIGHT_GPX_TRACK_PATH}/{gpx_filename}") async def add_terrain_elevation_to_flight(flight_id: int):
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: async with get_session() as db:
await models.Flight.update( track_points = (await db.scalars(
db, {"gpx_track_filename": output_name, "has_terrain_elevation": True}, select(models.TrackPoint)
id=flight_id .join(models.TrackPoint.track)
) .join(models.Track.flight)
except ClientResponseError as e: .filter(models.Flight.id == flight_id)
print(e) .filter(models.TrackPoint.terrain_elevation.is_(None))
)).all()
await update_track_points_elevation(db, track_points)
@retryable
async def add_terrain_elevation_to_photo(photo): async def add_terrain_elevation_to_photo(photo):
try: try:
elevation = await elevation_api.get_elevation_for_points([ elevation = await elevation_api.get_elevation_for_points(
{"lat": photo.gps_latitude, "lng": photo.gps_longitude} [{"lat": photo.gps_latitude, "lng": photo.gps_longitude}]
]) )
if not elevation: if not elevation:
print("Cannot get elevation") print("Cannot get elevation")
return return
terrain_elevation = elevation[0]['elevation'] terrain_elevation = elevation[0].elevation
async with get_session() as db: async with get_session() as db:
await models.Photo.update(db_session=db, obj=photo, data={"terrain_elevation": terrain_elevation}) await models.Photo.update(db_session=db, obj=photo, data={"terrain_elevation": terrain_elevation})
except Exception as e: except Exception as e:
+82
View File
@@ -0,0 +1,82 @@
from PIL import Image, ImageDraw, ImageOps
from sqlalchemy import select
from database import models
from database.transaction import get_session
async def add_circular_avatar(base_image_path, avatar_image_path, output_image_path):
# Load the base image
base_image = Image.open(base_image_path)
base_width, base_height = base_image.size
# Calculate the new dimensions for cropping to a 1.9:1 aspect ratio
new_width = base_width
new_height = int(new_width / 1.9)
if new_height > base_height:
new_height = base_height
new_width = int(new_height * 1.9)
# Center crop the base image to the new dimensions
left = (base_width - new_width) // 2
top = (base_height - new_height) // 2
right = (base_width + new_width) // 2
bottom = (base_height + new_height) // 2
base_image = base_image.crop((left, top, right, bottom))
base_width, base_height = base_image.size
# Load the avatar image
avatar_image = Image.open(avatar_image_path)
avatar_size = int(base_height * 0.3) # Set the size of the avatar
avatar_image = avatar_image.resize((avatar_size, avatar_size), Image.LANCZOS)
# Create a mask to make the avatar circular
mask = Image.new('L', (avatar_size, avatar_size), 0)
draw = ImageDraw.Draw(mask)
draw.ellipse((0, 0, avatar_size, avatar_size), fill=255)
# Apply the mask to the avatar image
avatar_image = ImageOps.fit(avatar_image, mask.size, centering=(0.5, 0.5))
avatar_image.putalpha(mask)
# Create a border around the avatar
border_size = int(avatar_size * 0.03)
border_image = Image.new('RGBA', (avatar_size + 2 * border_size, avatar_size + 2 * border_size), (255, 255, 255, 0))
border_mask = Image.new('L', (avatar_size + 2 * border_size, avatar_size + 2 * border_size), 0)
border_draw = ImageDraw.Draw(border_mask)
border_draw.ellipse((0, 0, avatar_size + 2 * border_size, avatar_size + 2 * border_size), fill=255)
border_draw.ellipse((border_size, border_size, avatar_size + border_size, avatar_size + border_size), fill=0)
avatar_margin = int(avatar_size * 0.1)
border_image.putalpha(border_mask)
border_image.paste(avatar_image, (border_size, border_size), avatar_image)
# Calculate the position to paste the avatar (bottom right corner)
position = (base_width - avatar_size - 2 * border_size - avatar_margin, base_height - avatar_size - 2 * border_size - avatar_margin)
# Paste the avatar with border onto the base image
base_image.paste(border_image, position, border_image)
# Save the result
base_image.save(output_image_path)
async def generate_flight_title_photo(flight_id: int, photo_id: int):
async with get_session() as db:
photo = await models.Photo.get_one(db, id=photo_id)
pilot = (await db.scalars(
select(models.User)
.select_from(models.Flight)
.join(models.Flight.created_by)
.filter(models.Flight.id == flight_id)
)).one()
photo_filename, photo_ext = photo.filename, photo.filename_extension
pilot_id, avatar_filename = pilot.id, pilot.avatar_image_filename
await add_circular_avatar(
base_image_path=f'/app/uploads/photos/{flight_id}/{photo_filename}.{photo_ext}',
avatar_image_path=f'/app/uploads/profile/{pilot_id}/{avatar_filename}',
output_image_path=f'/app/uploads/photos/{flight_id}/title_photo.jpg',
)
+38 -9
View File
@@ -1,25 +1,20 @@
from datetime import datetime from datetime import datetime
from typing import Literal from typing import Literal, Any
from database import models from database import models
from database.transaction import get_session from database.transaction import get_session
from external.weather import weather_api from external.weather import weather_api
from logger import log from logger import log
async def download_weather(date_time: datetime, flight_id: int, airport_id: int, type_: Literal['landing', 'takeoff']): async def get_weather(date_time: datetime | str, gps: tuple[float, float]) -> dict[str, Any] | None:
async with get_session() as db:
airport = await models.Airport.get_one(db, id=airport_id)
gps = (airport.gps_latitude, airport.gps_longitude)
try: try:
weather = await weather_api.get_weather_for_hour(date_time.astimezone(), gps=gps) weather = await weather_api.get_weather_for_hour(date_time.astimezone(), gps=gps)
log.warning(weather)
except Exception as e: except Exception as e:
log.error(f"Error in downloading weather: {e}") log.error(f"Error in downloading weather: {e}")
return None return None
data = { return {
"datetime": weather['datetime'], "date_time": date_time, # nemuzu pouzit weather['datetime'], protoze to mi vraci jen cele hodiny
"qnh": weather['pressure_msl'], "qnh": weather['pressure_msl'],
"temperature_surface": weather['temperature_2m'], "temperature_surface": weather['temperature_2m'],
"dewpoint_surface": weather['dewpoint_2m'], "dewpoint_surface": weather['dewpoint_2m'],
@@ -30,6 +25,14 @@ async def download_weather(date_time: datetime, flight_id: int, airport_id: int,
"wind_direction_surface": weather['winddirection_10m'], "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: async with get_session() as db:
flight = await models.Flight.get_one(db, id=flight_id) flight = await models.Flight.get_one(db, id=flight_id)
existing_weather_id = getattr(flight, f'{type_}_weather_info_id') existing_weather_id = getattr(flight, f'{type_}_weather_info_id')
@@ -39,3 +42,29 @@ async def download_weather(date_time: datetime, flight_id: int, airport_id: int,
else: else:
weather_model = await models.WeatherInfo.create(db, data=data) 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}) await models.Flight.update(db, obj=flight, data={f"{type_}_weather_info_id": weather_model.id})
async def download_weather_for_plan_marker(date_time: datetime, marker_id: int):
async with get_session() as db:
marker = await models.FlightPlanMarker.get_one(db, id=marker_id)
if not marker.gps_latitude or not marker.gps_longitude:
return
gps = (marker.gps_latitude, marker.gps_longitude)
marker_weather_info_id = marker.weather_info_id
data = await get_weather(date_time, gps)
if not data:
return
async with get_session() as db:
if marker_weather_info_id:
await models.WeatherInfo.update(db, id=marker_weather_info_id, data=data)
else:
weather_model = await models.WeatherInfo.create(db, data=data)
await models.FlightPlanMarker.update(
db,
id=marker_id,
data={"weather_info_id": weather_model.id}
)
+1 -1
View File
@@ -1,6 +1,6 @@
import os import os
APP_DEBUG = True APP_DEBUG = os.environ.get("APP_ENV", 'production') == 'development'
GRAPHIQL = True GRAPHIQL = True
REFRESH_TOKEN_VALIDITY_DAYS = 30 REFRESH_TOKEN_VALIDITY_DAYS = 30
-401
View File
@@ -1,401 +0,0 @@
from __future__ import annotations
import datetime
from typing import Set, List
from sqlalchemy import String, DateTime, ForeignKey, Text, Integer, func, Table, Column, Boolean, select, Float, Enum
from sqlalchemy.dialects.mysql import JSON
from sqlalchemy.orm import Mapped, relationship, as_declarative, mapped_column
from sqlalchemy.ext.asyncio import AsyncSession
@as_declarative()
class BaseModel:
excluded_columns_in_dict = ("deleted",)
@classmethod
def _get_column_names(cls):
return [col.name for col in cls.__table__.columns]
def as_dict(self):
return {c: getattr(self, c) for c in self._get_column_names() if c not in self.excluded_columns_in_dict}
@classmethod
async def get_one(cls, db_session: AsyncSession, **kwargs):
return (await db_session.scalars(select(cls).filter_by(**kwargs))).one()
@classmethod
async def create(cls, db_session: AsyncSession, data: dict):
model = cls(**{col: data[col] for col in cls._get_column_names() if col in data})
db_session.add(model)
await db_session.flush()
return model
@classmethod
async def update(cls, db_session: AsyncSession, data: dict, obj: BaseModel = None, id: int = None):
if not obj and not id:
raise ValueError("Provide either obj or id!")
if not obj:
obj = await cls.get_one(db_session, id=id)
for key, value in data.items():
if key in cls._get_column_names() and getattr(obj, key) != value:
setattr(obj, key, value)
return obj
user_is_in_organization = Table(
"user_is_in_organization",
BaseModel.metadata,
Column("user_id", Integer, ForeignKey("user.id"), primary_key=True),
Column("organization_id", Integer, ForeignKey("organization.id"), primary_key=True)
)
flight_has_copilot = Table(
"flight_has_copilot",
BaseModel.metadata,
Column("flight_id", ForeignKey("flight.id"), primary_key=True),
Column("copilot_id", ForeignKey("copilot.id"), primary_key=True),
Column("token", String(64), unique=True)
)
copilot_has_photo = Table(
"copilot_has_photo",
BaseModel.metadata,
Column("copilot_id", ForeignKey("copilot.id"), primary_key=True),
Column("photo_id", ForeignKey("photo.id"), primary_key=True),
)
class 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)
upper_limit: Mapped[str] = mapped_column(String(32), 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 Airport(BaseModel):
__tablename__ = "airport"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(128), nullable=False)
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')
metars: Mapped['Metar'] = relationship()
created_by: Mapped['User'] = relationship()
class PointOfInterestType(BaseModel):
__tablename__ = "point_of_interest_type"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(128), nullable=False)
icon: Mapped[str] = mapped_column(String(128), nullable=False, server_default='marker')
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
created_by: Mapped['User'] = relationship()
points_of_interest: Mapped[List[PointOfInterest]] = relationship()
class PointOfInterest(BaseModel):
__tablename__ = "point_of_interest"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(128), nullable=False)
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
url_slug: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
title_photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id'), nullable=True)
gps_latitude: Mapped[float] = mapped_column(Float, nullable=True)
gps_longitude: Mapped[float] = mapped_column(Float, nullable=True)
type_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest_type.id"), nullable=True)
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
type: Mapped[PointOfInterestType] = relationship()
created_by: Mapped['User'] = relationship()
title_photo: Mapped['Photo'] = relationship(foreign_keys=[title_photo_id])
class Photo(BaseModel):
__tablename__ = "photo"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
filename: Mapped[str] = mapped_column(String(128), nullable=False)
filename_extension: Mapped[str] = mapped_column(String(4), nullable=False)
cache_key: Mapped[str] = mapped_column(String(128), nullable=True)
width: Mapped[int] = mapped_column(Integer, nullable=False)
height: Mapped[int] = mapped_column(Integer, nullable=False)
exposed_at: Mapped[datetime] = mapped_column(DateTime, nullable=True)
gps_latitude: Mapped[float] = mapped_column(Float, nullable=True)
gps_longitude: Mapped[float] = mapped_column(Float, nullable=True)
gps_altitude: Mapped[float] = mapped_column(Float, nullable=True)
terrain_elevation: Mapped[float] = mapped_column(Float, nullable=True)
aircraft_id: Mapped[int] = mapped_column(Integer, ForeignKey("aircraft.id"), nullable=True)
point_of_interest_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest.id"), nullable=True)
flight_id: Mapped[int] = mapped_column(Integer, ForeignKey("flight.id"), nullable=False)
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
created_by_copilot_id: Mapped[int] = mapped_column(Integer, ForeignKey('copilot.id'), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
flight: Mapped['Flight'] = relationship(foreign_keys=[flight_id])
point_of_interest: Mapped['PointOfInterest'] = relationship(foreign_keys=[point_of_interest_id])
adjustment: Mapped['PhotoAdjustment'] = relationship(passive_deletes=True)
created_by: Mapped['User'] = relationship()
aircraft: Mapped['Aircraft'] = relationship(foreign_keys=[aircraft_id])
copilots: Mapped[List['Copilot']] = relationship(secondary=copilot_has_photo)
class PhotoAdjustment(BaseModel):
__tablename__ = "photo_adjustment"
id: Mapped[int] = mapped_column(primary_key=True)
photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id', ondelete='CASCADE'), nullable=False)
rotate: Mapped[float] = mapped_column(Float, nullable=True)
contrast: Mapped[float] = mapped_column(Float, nullable=True)
brightness: Mapped[float] = mapped_column(Float, nullable=True)
saturation: Mapped[float] = mapped_column(Float, nullable=True)
sharpness: Mapped[float] = mapped_column(Float, nullable=True)
crop_left: Mapped[float] = mapped_column(Float, nullable=True)
crop_top: Mapped[float] = mapped_column(Float, nullable=True)
crop_width: Mapped[float] = mapped_column(Float, nullable=True)
crop_height: Mapped[float] = mapped_column(Float, nullable=True)
updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
photo: Mapped['Photo'] = relationship()
class Aircraft(BaseModel):
__tablename__ = "aircraft"
id: Mapped[int] = mapped_column(primary_key=True)
call_sign: Mapped[str] = mapped_column(String(16), nullable=False)
title_photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id'), nullable=True)
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
manufacturer: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
model: Mapped[str] = mapped_column(String(30), nullable=False, server_default="")
seats: Mapped[str] = mapped_column(Integer, nullable=False)
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
organization_id: Mapped[int] = mapped_column(Integer, ForeignKey('organization.id'), nullable=True)
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'), nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
organization: Mapped['Organization'] = relationship()
flights: Mapped[Set['Flight']] = relationship()
created_by: Mapped['User'] = relationship()
# notes: Mapped['AircraftNotes'] = relationship()
# class AircraftNotes(BaseModel):
# __tablename__ = "aircraft_notes"
#
# id: Mapped[int] = mapped_column(primary_key=True)
# aircraft_id: Mapped[int] = mapped_column(Integer, ForeignKey("aircraft.id"), nullable=False)
# name: Mapped[str] = mapped_column(String(128), nullable=False)
# description: Mapped[str] = mapped_column(Text, nullable=False)
# is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
# created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
# created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
#
# created_by: Mapped['User'] = relationship()
# aircraft: Mapped['Aircraft'] = relationship()
class Organization(BaseModel):
__tablename__ = "organization"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(128), nullable=False)
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
users: Mapped[Set['User']] = relationship(secondary=user_is_in_organization)
created_by: Mapped['User'] = relationship()
class FlightTrack(BaseModel):
__tablename__ = "flight_track"
id: Mapped[int] = mapped_column(primary_key=True)
flight_id: Mapped[int] = mapped_column(Integer, ForeignKey("flight.id"), nullable=False)
point_of_interest_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest.id"), nullable=True)
airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=True)
landing_duration: Mapped[int] = mapped_column(Integer, nullable=True)
order: Mapped[int] = mapped_column(Integer)
flight: Mapped['Flight'] = relationship()
point_of_interest: Mapped['PointOfInterest'] = relationship()
airport: Mapped['Airport'] = relationship()
class WeatherInfo(BaseModel):
__tablename__ = "weather_info"
id: Mapped[int] = mapped_column(primary_key=True)
qnh: Mapped[int] = mapped_column(Integer, nullable=True)
temperature_surface: Mapped[Float] = mapped_column(Float, nullable=True)
dewpoint_surface: Mapped[Float] = mapped_column(Float, nullable=True)
rain: Mapped[Float] = mapped_column(Float, nullable=True)
cloudcover_low: Mapped[Float] = mapped_column(Float, nullable=True)
cloudcover_total: Mapped[Float] = mapped_column(Float, nullable=True)
wind_speed_surface: Mapped[Float] = mapped_column(Float, nullable=True)
wind_direction_surface: Mapped[Float] = mapped_column(Float, nullable=True)
datetime: Mapped[datetime] = mapped_column(DateTime, nullable=False)
class Event(BaseModel):
__tablename__ = "event"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(128), nullable=False)
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
url_slug: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
date_from: Mapped[datetime] = mapped_column(DateTime, nullable=True)
date_to: Mapped[datetime] = mapped_column(DateTime, nullable=True)
organization_id: Mapped[int] = mapped_column(Integer, ForeignKey('organization.id'), nullable=True)
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
organization: Mapped['Organization'] = relationship()
created_by: Mapped['User'] = relationship()
class Flight(BaseModel):
__tablename__ = "flight"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
debriefing_notes: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
url_slug: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
event_id: Mapped[int] = mapped_column(Integer, ForeignKey("event.id"), nullable=True)
title_photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id'), nullable=True)
takeoff_airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=True)
landing_airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=True)
takeoff_datetime: Mapped[datetime] = mapped_column(DateTime, nullable=False)
landing_datetime: Mapped[datetime] = mapped_column(DateTime, nullable=False)
duration_total: Mapped[int] = mapped_column(Integer, nullable=True)
duration_pic: Mapped[int] = mapped_column(Integer, nullable=True)
gpx_track_filename: Mapped[str] = mapped_column(String(128), nullable=True)
has_terrain_elevation: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
aircraft_id: Mapped[int] = mapped_column(Integer, ForeignKey('aircraft.id'))
takeoff_weather_info_id: Mapped[int] = mapped_column(Integer, ForeignKey('weather_info.id'), nullable=True)
landing_weather_info_id: Mapped[int] = mapped_column(Integer, ForeignKey('weather_info.id'), nullable=True)
landings: Mapped[int] = mapped_column(Integer, default=1)
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
takeoff_airport: Mapped['Airport'] = relationship(foreign_keys=[takeoff_airport_id])
landing_airport: Mapped['Airport'] = relationship(foreign_keys=[landing_airport_id])
weather_info_landing: Mapped[WeatherInfo] = relationship(foreign_keys=[landing_weather_info_id])
weather_info_takeoff: Mapped[WeatherInfo] = relationship(foreign_keys=[takeoff_weather_info_id])
track: Mapped['FlightTrack'] = relationship()
event: Mapped['Event'] = relationship()
copilots: Mapped[List['Copilot']] = relationship(secondary=flight_has_copilot)
aircraft: Mapped['Aircraft'] = relationship()
photos: Mapped[List['Photo']] = relationship(foreign_keys=[Photo.flight_id])
created_by: Mapped['User'] = relationship()
title_photo: Mapped['Photo'] = relationship(foreign_keys=[title_photo_id])
class Copilot(BaseModel):
__tablename__ = "copilot"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(128), nullable=False)
url_slug: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
title_photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id'), nullable=True)
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
flights: Mapped[Set['Flight']] = relationship(secondary=flight_has_copilot)
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',)
id: Mapped[int] = mapped_column(primary_key=True)
email: Mapped[str] = mapped_column(String(128), nullable=False, unique=True)
name: Mapped[str] = mapped_column(String(128), nullable=False)
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
public_username: Mapped[str] = mapped_column(String(128), nullable=True, unique=True)
avatar_image_filename: Mapped[str] = mapped_column(String(128), nullable=True)
title_image_filename: Mapped[str] = mapped_column(String(128), nullable=True)
password_hashed: Mapped[str] = mapped_column(String(60), nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
licences: Mapped[Set['License']] = relationship()
flights: Mapped[Set['Flight']] = relationship()
organizations: Mapped[Set['Organization']] = relationship(secondary=user_is_in_organization)
class PasswordResetToken(BaseModel):
__tablename__ = "password_reset_token"
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
token: Mapped[str] = mapped_column(String(64), nullable=False)
valid_until: Mapped[datetime] = mapped_column(DateTime, nullable=True)
user: Mapped['User'] = relationship()
+45
View File
@@ -0,0 +1,45 @@
from database.models.airport import Airport, Runway, ReportingPoint, airport_has_reporting_point, Frequency
from database.models.aircraft import Aircraft
from database.models.airspace import Airspace
from database.models.base_model import BaseModel
from database.models.copilot import Copilot, copilot_has_photo
from database.models.event import Event
from database.models.flight import Flight, FlightTurnPoint, Track, TrackPoint, flight_has_copilot
from database.models.flight_plan import FlightPlan, flight_plan_has_copilot, FlightPlanMarker
from database.models.orgnization import Organization
from database.models.password_reset_token import PasswordResetToken
from database.models.photo import PhotoAdjustment, Photo
from database.models.point_of_interest import PointOfInterest, PointOfInterestType
from database.models.user import User, user_is_in_organization
from database.models.weather_info import WeatherInfo
__all__ = [
"Aircraft",
"Airport",
"Runway",
"ReportingPoint",
"Airspace",
"BaseModel",
"Copilot",
"Event",
"Flight",
"FlightTurnPoint",
"FlightPlan",
"FlightPlanMarker",
"Frequency",
"Track",
"TrackPoint",
"Organization",
"PasswordResetToken",
"Photo",
"PhotoAdjustment",
"PointOfInterest",
"PointOfInterestType",
"WeatherInfo",
"User",
"airport_has_reporting_point",
"flight_has_copilot",
"flight_plan_has_copilot",
"user_is_in_organization",
"copilot_has_photo",
]
+25
View File
@@ -0,0 +1,25 @@
from datetime import datetime
from sqlalchemy import String, Boolean, Table, ForeignKey, Column, Integer, Text, DateTime, func, Float
from sqlalchemy.orm import mapped_column, Mapped, relationship
from database.models.base_model import BaseModel
class Aircraft(BaseModel):
__tablename__ = "aircraft"
id: Mapped[int] = mapped_column(primary_key=True)
call_sign: Mapped[str] = mapped_column(String(16), nullable=False)
title_photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id'), nullable=True)
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
manufacturer: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
model: Mapped[str] = mapped_column(String(30), nullable=False, server_default="")
seats: Mapped[str] = mapped_column(Integer, nullable=False)
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
organization_id: Mapped[int] = mapped_column(Integer, ForeignKey('organization.id'), nullable=True)
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'), nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
organization: Mapped['Organization'] = relationship()
flights: Mapped[set['Flight']] = relationship()
created_by: Mapped['User'] = relationship()
+67
View File
@@ -0,0 +1,67 @@
from datetime import datetime
from sqlalchemy import String, Boolean, Table, ForeignKey, Column, Integer, Text, DateTime, func, Float, Enum
from sqlalchemy.orm import mapped_column, Mapped, relationship
from database.models.base_model import BaseModel
airport_has_reporting_point = Table(
"airport_has_reporting_point",
BaseModel.metadata,
Column("airport_id", ForeignKey("airport.id"), primary_key=True),
Column("reporting_point_id", ForeignKey("reporting_point.id"), primary_key=True),
)
class Airport(BaseModel):
__tablename__ = "airport"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(128), nullable=False)
openaip_id: Mapped[str] = mapped_column(String(32), nullable=True, unique=True)
icao_code: Mapped[str] = mapped_column(String(8), nullable=False)
gps_latitude: Mapped[float] = mapped_column(Float, nullable=True)
gps_longitude: Mapped[float] = mapped_column(Float, nullable=True)
elevation: Mapped[int] = mapped_column(Integer, nullable=True)
airport_type: Mapped[str] = mapped_column(
Enum("airport", "ull", "heliport"),
nullable=False,
server_default='airport'
)
use_in_gpx_guess: Mapped[bool] = mapped_column(Boolean, server_default='1')
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'), nullable=True) # automaticky import nebude mit ID # noqa
runways: Mapped[list['Runway']] = relationship('Runway')
frequencies: Mapped[list['Frequency']] = relationship()
created_by: Mapped['User'] = relationship()
class Runway(BaseModel):
__tablename__ = "runway"
id: Mapped[int] = mapped_column(primary_key=True)
airport_id: Mapped[int] = mapped_column(Integer, ForeignKey('airport.id'))
heading: Mapped[int] = mapped_column(Integer, nullable=False)
designator: Mapped[str] = mapped_column(String(4), nullable=False)
surface: Mapped[str] = mapped_column(Enum("asphalt", "concrete", "grass", "other"), nullable=False, server_default="other") # noqa
length: Mapped[int] = mapped_column(Integer, nullable=True)
width: Mapped[int] = mapped_column(Integer, nullable=True)
class Frequency(BaseModel):
__tablename__ = "frequency"
id: Mapped[int] = mapped_column(primary_key=True)
airport_id: Mapped[int] = mapped_column(Integer, ForeignKey('airport.id'), nullable=True)
airspace_id: Mapped[int] = mapped_column(Integer, ForeignKey('airspace.id'), nullable=True)
frequency: Mapped[str] = mapped_column(String(7), nullable=False)
name: Mapped[str] = mapped_column(String(64), nullable=False)
class ReportingPoint(BaseModel):
__tablename__ = "reporting_point"
id: Mapped[int] = mapped_column(primary_key=True)
openaip_id: Mapped[str] = mapped_column(String(32), nullable=False)
name: Mapped[str] = mapped_column(String(64), nullable=False)
gps_latitude: Mapped[float] = mapped_column(Float, nullable=True)
gps_longitude: Mapped[float] = mapped_column(Float, nullable=True)
+17
View File
@@ -0,0 +1,17 @@
from sqlalchemy import String, JSON
from sqlalchemy.orm import mapped_column, Mapped
from database.models.base_model import BaseModel
class Airspace(BaseModel):
__tablename__ = "airspace"
id: Mapped[int] = mapped_column(primary_key=True)
country: Mapped[str] = mapped_column(String(8), nullable=False, index=True)
name: Mapped[str] = mapped_column(String(128), nullable=False, unique=True)
type: Mapped[str] = mapped_column(String(8), nullable=False, index=True)
lower_limit: Mapped[str] = mapped_column(String(32), nullable=False)
lower_alt_unit: Mapped[str] = mapped_column(String(8), nullable=False)
upper_limit: Mapped[str] = mapped_column(String(32), nullable=False)
upper_alt_unit: Mapped[str] = mapped_column(String(8), nullable=False)
bounds: Mapped[list[tuple[float, float]]] = mapped_column(JSON())
+40
View File
@@ -0,0 +1,40 @@
from __future__ import annotations
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import DeclarativeBase
class BaseModel(DeclarativeBase):
excluded_columns_in_dict = ("deleted",)
@classmethod
def _get_column_names(cls):
return [col.name for col in cls.__table__.columns]
def as_dict(self):
return {c: getattr(self, c) for c in self._get_column_names() if c not in self.excluded_columns_in_dict}
@classmethod
async def get_one(cls, db_session: AsyncSession, **kwargs):
return (await db_session.scalars(select(cls).filter_by(**kwargs))).one()
@classmethod
async def create(cls, db_session: AsyncSession, data: dict):
model = cls(**{col: data[col] for col in cls._get_column_names() if col in data})
db_session.add(model)
await db_session.flush()
return model
@classmethod
async def update(cls, db_session: AsyncSession, data: dict, obj: BaseModel = None, id: int = None):
if not obj and not id:
raise ValueError("Provide either obj or id!")
if not obj:
obj = await cls.get_one(db_session, id=id)
for key, value in data.items():
if key in cls._get_column_names() and getattr(obj, key) != value:
setattr(obj, key, value)
return obj
+32
View File
@@ -0,0 +1,32 @@
from datetime import datetime
from sqlalchemy import String, Boolean, Table, ForeignKey, Column, Integer, DateTime, func
from sqlalchemy.orm import mapped_column, Mapped, relationship
from database.models.base_model import BaseModel
from database.models.flight import flight_has_copilot
from database.models.flight_plan import flight_plan_has_copilot
copilot_has_photo = Table(
"copilot_has_photo",
BaseModel.metadata,
Column("copilot_id", ForeignKey("copilot.id"), primary_key=True),
Column("photo_id", ForeignKey("photo.id"), primary_key=True),
)
class Copilot(BaseModel):
__tablename__ = "copilot"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(128), nullable=False)
url_slug: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
title_photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id'), nullable=True)
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
flights: Mapped[set['Flight']] = relationship(secondary=flight_has_copilot)
flight_plans: Mapped[set['FlightPlan']] = relationship(secondary=flight_plan_has_copilot)
created_by: Mapped['User'] = relationship()
photos: Mapped[list['Photo']] = relationship(secondary=copilot_has_photo)
title_photo: Mapped['Photo'] = relationship(foreign_keys=[title_photo_id])
+23
View File
@@ -0,0 +1,23 @@
from datetime import datetime
from sqlalchemy import String, Boolean, Table, ForeignKey, Column, Integer, Text, DateTime, func, Float
from sqlalchemy.orm import mapped_column, Mapped, relationship
from database.models.base_model import BaseModel
class Event(BaseModel):
__tablename__ = "event"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(128), nullable=False)
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
url_slug: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
date_from: Mapped[datetime] = mapped_column(DateTime, nullable=True)
date_to: Mapped[datetime] = mapped_column(DateTime, nullable=True)
organization_id: Mapped[int] = mapped_column(Integer, ForeignKey('organization.id'), nullable=True)
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
organization: Mapped['Organization'] = relationship()
created_by: Mapped['User'] = relationship()
+103
View File
@@ -0,0 +1,103 @@
from datetime import datetime
from sqlalchemy import String, Boolean, Table, ForeignKey, Column, Integer, Text, DateTime, func, Float, JSON
from sqlalchemy.orm import mapped_column, Mapped, relationship
from database.models.base_model import BaseModel
flight_has_copilot = Table(
"flight_has_copilot",
BaseModel.metadata,
Column("flight_id", ForeignKey("flight.id"), primary_key=True),
Column("copilot_id", ForeignKey("copilot.id"), primary_key=True),
Column("token", String(64), unique=True)
)
class FlightTurnPoint(BaseModel):
__tablename__ = "flight_turn_point"
id: Mapped[int] = mapped_column(primary_key=True)
flight_id: Mapped[int] = mapped_column(Integer, ForeignKey("flight.id"), nullable=False)
point_of_interest_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest.id"), nullable=True)
airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=True)
landing_duration: Mapped[int] = mapped_column(Integer, nullable=True)
order: Mapped[int] = mapped_column(Integer, index=True)
flight: Mapped['Flight'] = relationship()
point_of_interest: Mapped['PointOfInterest'] = relationship()
airport: Mapped['Airport'] = relationship()
class Flight(BaseModel):
__tablename__ = "flight"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
debriefing_notes: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
url_slug: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
event_id: Mapped[int] = mapped_column(Integer, ForeignKey("event.id"), nullable=True)
title_photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id'), nullable=True)
takeoff_airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=True)
landing_airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=True)
takeoff_datetime: Mapped[datetime] = mapped_column(DateTime, nullable=False)
landing_datetime: Mapped[datetime] = mapped_column(DateTime, nullable=False)
duration_total: Mapped[int] = mapped_column(Integer, nullable=True)
duration_pic: Mapped[int] = mapped_column(Integer, nullable=True)
flight_plan_id: Mapped[int] = mapped_column(Integer, ForeignKey('flight_plan.id'), nullable=True)
track_id: Mapped[int] = mapped_column(Integer, ForeignKey('track.id'), nullable=True)
gpx_track_filename: Mapped[str] = mapped_column(String(128), nullable=True) # deprecated
has_terrain_elevation: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) # deprecated
aircraft_id: Mapped[int] = mapped_column(Integer, ForeignKey('aircraft.id'))
takeoff_weather_info_id: Mapped[int] = mapped_column(Integer, ForeignKey('weather_info.id'), nullable=True)
landing_weather_info_id: Mapped[int] = mapped_column(Integer, ForeignKey('weather_info.id'), nullable=True)
landings: Mapped[int] = mapped_column(Integer, default=1)
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
takeoff_airport: Mapped['Airport'] = relationship(foreign_keys=[takeoff_airport_id])
landing_airport: Mapped['Airport'] = relationship(foreign_keys=[landing_airport_id])
weather_info_landing: Mapped['WeatherInfo'] = relationship(foreign_keys=[landing_weather_info_id])
weather_info_takeoff: Mapped['WeatherInfo'] = relationship(foreign_keys=[takeoff_weather_info_id])
turn_points: Mapped[list['FlightTurnPoint']] = relationship()
event: Mapped['Event'] = relationship()
copilots: Mapped[list['Copilot']] = relationship(secondary=flight_has_copilot)
aircraft: Mapped['Aircraft'] = relationship()
created_by: Mapped['User'] = relationship()
title_photo: Mapped['Photo'] = relationship(foreign_keys=[title_photo_id])
track: Mapped['Track'] = relationship()
class Track(BaseModel):
__tablename__ = "track"
id: Mapped[int] = mapped_column(primary_key=True)
bounds: Mapped[list[tuple[float, float]]] = mapped_column(JSON())
min_speed: Mapped[float] = mapped_column(Float, nullable=True)
max_speed: Mapped[float] = mapped_column(Float, nullable=True)
avg_speed: Mapped[float] = mapped_column(Float, nullable=True)
max_altitude: Mapped[float] = mapped_column(Float, nullable=True)
avg_altitude: Mapped[float] = mapped_column(Float, nullable=True)
total_duration: Mapped[int] = mapped_column(Integer, nullable=True, comment="Total duration in seconds")
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
flight: Mapped['Flight'] = relationship()
track_points: Mapped[list['TrackPoint']] = relationship()
class TrackPoint(BaseModel):
__tablename__ = "track_point"
id: Mapped[int] = mapped_column(primary_key=True)
timestamp: Mapped[datetime] = mapped_column(DateTime, nullable=False, index=True)
track_id: Mapped[int] = mapped_column(Integer, ForeignKey('track.id'))
gps_latitude: Mapped[float] = mapped_column(Float, nullable=False)
gps_longitude: Mapped[float] = mapped_column(Float, nullable=False)
terrain_elevation: Mapped[float] = mapped_column(Float, nullable=True)
speed: Mapped[float] = mapped_column(Float, nullable=True)
altitude: Mapped[float] = mapped_column(Float, nullable=True)
magnetic_variation: Mapped[float] = mapped_column(Float, nullable=True)
track: Mapped['Track'] = relationship()
+51
View File
@@ -0,0 +1,51 @@
from datetime import datetime
from sqlalchemy import String, Boolean, Table, ForeignKey, Column, Integer, Text, DateTime, func, Float
from sqlalchemy.orm import mapped_column, Mapped, relationship
from database.models.base_model import BaseModel
flight_plan_has_copilot = Table(
"flight_plan_has_copilot",
BaseModel.metadata,
Column("flight_plan_id", ForeignKey("flight_plan.id"), primary_key=True),
Column("copilot_id", ForeignKey("copilot.id"), primary_key=True),
Column("token", String(64), unique=True)
)
class FlightPlan(BaseModel):
__tablename__ = "flight_plan"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(256), nullable=False)
is_default_name: Mapped[bool] = mapped_column(Boolean, server_default='0')
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
is_editable_by_copilot: Mapped[bool] = mapped_column(Boolean, server_default='0')
track_id: Mapped[int] = mapped_column(Integer, ForeignKey('track.id'), nullable=True)
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
planned_speed: Mapped[int] = mapped_column(Integer, nullable=False, default=150)
planned_takeoff_datetime: Mapped[datetime] = mapped_column(DateTime, nullable=True)
aircraft_id: Mapped[int] = mapped_column(Integer, ForeignKey("aircraft.id"), nullable=True)
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
copilots: Mapped[list['Copilot']] = relationship(secondary=flight_plan_has_copilot)
aircraft: Mapped['Aircraft'] = relationship()
markers: Mapped[list['FlightPlanMarker']] = relationship()
created_by: Mapped['User'] = relationship()
class FlightPlanMarker(BaseModel):
__tablename__ = "flight_plan_marker"
id: Mapped[int] = mapped_column(primary_key=True)
flight_plan_id: Mapped[int] = mapped_column(Integer, ForeignKey('flight_plan.id'))
airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=True)
point_of_interest_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest.id"), nullable=True)
weather_info_id: Mapped[int] = mapped_column(Integer, ForeignKey('weather_info.id'), nullable=True)
name: Mapped[str] = mapped_column(String(128), nullable=False)
type: Mapped[str] = mapped_column(String(64), nullable=False)
position: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
gps_latitude: Mapped[float] = mapped_column(Float, nullable=False)
gps_longitude: Mapped[float] = mapped_column(Float, nullable=False)
+19
View File
@@ -0,0 +1,19 @@
from datetime import datetime
from sqlalchemy import String, Boolean, Table, ForeignKey, Column, Integer, Text, DateTime, func, Float
from sqlalchemy.orm import mapped_column, Mapped, relationship
from database.models.base_model import BaseModel
from database.models.user import user_is_in_organization
class Organization(BaseModel):
__tablename__ = "organization"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(128), nullable=False)
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
users: Mapped[set['User']] = relationship(secondary=user_is_in_organization)
created_by: Mapped['User'] = relationship()
@@ -0,0 +1,15 @@
from datetime import datetime
from sqlalchemy import String, ForeignKey, Integer, DateTime
from sqlalchemy.orm import mapped_column, Mapped, relationship
from database.models.base_model import BaseModel
class PasswordResetToken(BaseModel):
__tablename__ = "password_reset_token"
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
token: Mapped[str] = mapped_column(String(64), nullable=False)
valid_until: Mapped[datetime] = mapped_column(DateTime, nullable=True)
user: Mapped['User'] = relationship()
+55
View File
@@ -0,0 +1,55 @@
from datetime import datetime
from sqlalchemy import String, ForeignKey, Integer, Text, DateTime, func, Float
from sqlalchemy.orm import mapped_column, Mapped, relationship
from database.models.base_model import BaseModel
class Photo(BaseModel):
__tablename__ = "photo"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
filename: Mapped[str] = mapped_column(String(128), nullable=False)
filename_extension: Mapped[str] = mapped_column(String(4), nullable=False)
cache_key: Mapped[str] = mapped_column(String(128), nullable=True)
width: Mapped[int] = mapped_column(Integer, nullable=False)
height: Mapped[int] = mapped_column(Integer, nullable=False)
exposed_at: Mapped[datetime] = mapped_column(DateTime, nullable=True)
gps_latitude: Mapped[float] = mapped_column(Float, nullable=True)
gps_longitude: Mapped[float] = mapped_column(Float, nullable=True)
gps_altitude: Mapped[float] = mapped_column(Float, nullable=True)
terrain_elevation: Mapped[float] = mapped_column(Float, nullable=True)
aircraft_id: Mapped[int] = mapped_column(Integer, ForeignKey("aircraft.id"), nullable=True)
point_of_interest_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest.id"), nullable=True)
flight_id: Mapped[int] = mapped_column(Integer, ForeignKey("flight.id"), nullable=False)
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
created_by_copilot_id: Mapped[int] = mapped_column(Integer, ForeignKey('copilot.id'), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
flight: Mapped['Flight'] = relationship(foreign_keys=[flight_id])
point_of_interest: Mapped['PointOfInterest'] = relationship(foreign_keys=[point_of_interest_id])
adjustment: Mapped['PhotoAdjustment'] = relationship(passive_deletes=True)
created_by: Mapped['User'] = relationship()
aircraft: Mapped['Aircraft'] = relationship(foreign_keys=[aircraft_id])
# copilots: Mapped[list['Copilot']] = relationship(secondary=copilot_has_photo)
class PhotoAdjustment(BaseModel):
__tablename__ = "photo_adjustment"
id: Mapped[int] = mapped_column(primary_key=True)
photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id', ondelete='CASCADE'), nullable=False)
rotate: Mapped[float] = mapped_column(Float, nullable=True)
contrast: Mapped[float] = mapped_column(Float, nullable=True)
brightness: Mapped[float] = mapped_column(Float, nullable=True)
saturation: Mapped[float] = mapped_column(Float, nullable=True)
sharpness: Mapped[float] = mapped_column(Float, nullable=True)
crop_left: Mapped[float] = mapped_column(Float, nullable=True)
crop_top: Mapped[float] = mapped_column(Float, nullable=True)
crop_width: Mapped[float] = mapped_column(Float, nullable=True)
crop_height: Mapped[float] = mapped_column(Float, nullable=True)
updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
photo: Mapped['Photo'] = relationship()
+40
View File
@@ -0,0 +1,40 @@
from datetime import datetime
from sqlalchemy import String, Boolean, Table, ForeignKey, Column, Integer, Text, DateTime, func, Float
from sqlalchemy.orm import mapped_column, Mapped, relationship
from database.models.base_model import BaseModel
class PointOfInterestType(BaseModel):
__tablename__ = "point_of_interest_type"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(128), nullable=False)
icon: Mapped[str] = mapped_column(String(128), nullable=False, server_default='marker')
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
created_by: Mapped['User'] = relationship()
points_of_interest: Mapped[list['PointOfInterest']] = relationship()
class PointOfInterest(BaseModel):
__tablename__ = "point_of_interest"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(128), nullable=False)
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
url_slug: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
title_photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id'), nullable=True)
gps_latitude: Mapped[float] = mapped_column(Float, nullable=True)
gps_longitude: Mapped[float] = mapped_column(Float, nullable=True)
type_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest_type.id"), nullable=True)
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
type: Mapped[PointOfInterestType] = relationship()
created_by: Mapped['User'] = relationship()
title_photo: Mapped['Photo'] = relationship(foreign_keys=[title_photo_id])
+31
View File
@@ -0,0 +1,31 @@
from __future__ import annotations
import datetime
from typing import Set
from sqlalchemy import String, DateTime, ForeignKey, Text, Integer, func, Table, Column
from sqlalchemy.orm import Mapped, relationship, mapped_column
from database.models.base_model import BaseModel
user_is_in_organization = Table(
"user_is_in_organization",
BaseModel.metadata,
Column("user_id", Integer, ForeignKey("user.id"), primary_key=True),
Column("organization_id", Integer, ForeignKey("organization.id"), primary_key=True)
)
class User(BaseModel):
__tablename__ = "user"
excluded_columns_in_dict = ('password_hashed',)
id: Mapped[int] = mapped_column(primary_key=True)
email: Mapped[str] = mapped_column(String(128), nullable=False, unique=True)
name: Mapped[str] = mapped_column(String(128), nullable=False)
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
public_username: Mapped[str] = mapped_column(String(128), nullable=True, unique=True)
avatar_image_filename: Mapped[str] = mapped_column(String(128), nullable=True)
title_image_filename: Mapped[str] = mapped_column(String(128), nullable=True)
password_hashed: Mapped[str] = mapped_column(String(60), nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
flights: Mapped[Set['Flight']] = relationship()
organizations: Mapped[Set['Organization']] = relationship(secondary=user_is_in_organization)
+19
View File
@@ -0,0 +1,19 @@
from datetime import datetime
from sqlalchemy import Integer, DateTime, Float
from sqlalchemy.orm import mapped_column, Mapped
from database.models.base_model import BaseModel
class WeatherInfo(BaseModel):
__tablename__ = "weather_info"
id: Mapped[int] = mapped_column(primary_key=True)
qnh: Mapped[int] = mapped_column(Integer, nullable=True)
temperature_surface: Mapped[float] = mapped_column(Float, nullable=True)
dewpoint_surface: Mapped[float] = mapped_column(Float, nullable=True)
rain: Mapped[float] = mapped_column(Float, nullable=True)
cloudcover_low: Mapped[float] = mapped_column(Float, nullable=True)
cloudcover_total: Mapped[float] = mapped_column(Float, nullable=True)
wind_speed_surface: Mapped[float] = mapped_column(Float, nullable=True)
wind_direction_surface: Mapped[float] = mapped_column(Float, nullable=True)
date_time: Mapped[datetime] = mapped_column(DateTime, nullable=False)
+10 -10
View File
@@ -1,21 +1,21 @@
from typing import Optional, Type from typing import Type
from sqlalchemy import select, or_, and_ from sqlalchemy import select, or_, and_
from database import models from database import models
class QueryBuilder: class QueryBuilder[ModelType: models.BaseModel]:
def __init__(self, model: Type[models.BaseModel]): def __init__(self, model: Type[ModelType]):
self.model = model self.model = model
def get_simple_query( def get_simple_query(
self, self,
extra_select: Optional[list] = None, extra_select: list | None = None,
created_by_id: Optional[int] = None, created_by_id: int | None = None,
order_by: Optional[list] = None, order_by: list | None = None,
only_public: Optional[bool] = False, only_public: bool | None = False,
only_my: Optional[bool] = False, only_my: bool | None = False,
include_others_public: Optional[bool] = False, include_others_public: bool | None = False,
url_slug: Optional[str] = None, url_slug: str | None = None,
include_deleted: bool = False include_deleted: bool = False
): ):
if not extra_select: if not extra_select:
-38
View File
@@ -1,38 +0,0 @@
from functools import wraps
from fastapi import HTTPException
from starlette.status import HTTP_401_UNAUTHORIZED
def raise_unauthorized():
raise HTTPException(HTTP_401_UNAUTHORIZED, "Not authorized")
def allow_public(func):
@wraps(func)
async def decorator(*args, **kwargs):
if 'info' in kwargs:
user_id = kwargs['info'].context.user_id
public = kwargs.get('public')
if not user_id and not public:
raise_unauthorized()
return await func(*args, **kwargs)
return decorator
def authenticated_user_only(raise_when_unauthorized: bool = True, return_value_unauthorized=None):
def wrapper(func):
@wraps(func)
async def decorator(*args, **kwargs):
if 'info' in kwargs:
if not kwargs['info'].context.user_id:
if raise_when_unauthorized:
raise_unauthorized()
else:
return return_value_unauthorized
return await func(*args, **kwargs)
return decorator
return wrapper
-18
View File
@@ -1,18 +0,0 @@
from functools import wraps
from fastapi import HTTPException
from graphql import GraphQLError
from sqlalchemy.exc import NoResultFound
def error_logging(func):
@wraps(func)
async def decorator(*args, **kwargs):
try:
return await func(*args, **kwargs)
except NoResultFound as e:
raise GraphQLError("Not found", original_error=e)
except HTTPException as e:
if e.status_code == 401:
raise GraphQLError("Not authorized", original_error=e)
return decorator
+24
View File
@@ -0,0 +1,24 @@
import asyncio
def retryable(func):
max_retries = 5
async def func_wrapper(*args, **kwargs):
for i in range(1, max_retries):
try:
return await func(*args, **kwargs)
except Exception as ex:
if i > max_retries:
raise ex
sleep_time = i * 2
print(
"Error while getting the data from source:"
" decorator is going to sleep for %s seconds"
" and retry it again (%s/%s)" % (
sleep_time, i, max_retries
)
)
await asyncio.sleep(sleep_time)
return func_wrapper
+2 -2
View File
@@ -9,10 +9,10 @@
</mj-text> </mj-text>
<mj-text font-size="16px" color="#333333" font-family="Arial, sans-serif" align="left"> <mj-text font-size="16px" color="#333333" font-family="Arial, sans-serif" align="left">
Od: [from] Od: {{ email_from | e }}
</mj-text> </mj-text>
<mj-text font-size="16px" color="#333333" font-family="Arial, sans-serif" align="left" line-height="1.5"> <mj-text font-size="16px" color="#333333" font-family="Arial, sans-serif" align="left" line-height="1.5">
[content] {{ content | e }}
</mj-text> </mj-text>
<mj-divider border-color="#469ccc"></mj-divider> <mj-divider border-color="#469ccc"></mj-divider>
</mj-column> </mj-column>
@@ -13,13 +13,12 @@
<p> <p>
Ahoj, Ahoj,
<br> <br>
Každý občas zapomíná. Heslo je to nejmenší. Klikni na následující tlačítko pro přihlášení do aplikace. A tam si budeš moct změnit heslo. Každý občas zapomíná. Heslo je to nejmenší. Klikni na následující tlačítko pro reset hesla.
<br> <br>
<strong>Jen admin vyzaduje zadani stareho hesla, takze to nebude tak snadne...</strong>
</p> </p>
</mj-text> </mj-text>
<mj-button background-color="#469ccc" color="white" font-size="18px" align="center" href=""> <mj-button background-color="#469ccc" color="white" font-size="18px" align="center" href="https://poletani.cz/zapomenute-heslo/[login_token]">
Přihlásit se pomocí magického odkazu [login_token] Obnovit heslo
</mj-button> </mj-button>
<mj-divider border-color="#469ccc"></mj-divider> <mj-divider border-color="#469ccc"></mj-divider>
</mj-column> </mj-column>
+8 -8
View File
@@ -8,34 +8,34 @@
Vítejte v aplikaci Polétání.cz! Vítejte v aplikaci Polétání.cz!
</mj-text> </mj-text>
<mj-text font-size="16px" color="#333333" font-family="Arial, sans-serif" align="left" line-height="1.5"> <mj-text font-size="16px" color="#333333" font-family="Arial, sans-serif" align="left" line-height="1.5">
Ahoj [Jméno], Ahoj!
<br> <br>
<p> <p>
Vítejte v aplikaci Polétání.cz – místě, kde můžete sdílet své letecké dobrodružství a fotografie s ostatními nadšenci. Vítej v aplikaci Polétání.cz – místě, kde můžete sdílet své letecké dobrodružství a fotografie s ostatními nadšenci.
</p> </p>
<p> <p>
Co můžete očekávat: Co můžeš očekávat:
</p> </p>
<ul> <ul>
<li>Jednoduché nahrávání a sdílení fotografií z vašich letů.</li> <li>Jednoduché nahrávání a sdílení fotografií z vašich letů.</li>
<li>Možnost připojení k letištním komunitám a sdílení tipů a triků.</li> <li>Naplánovat nový výlet pomocí vestavěného plánovače</li>
<li>Upozornění na nové funkce a akce v aplikaci.</li> <li>Objevovat nová místa pro další výlety</li>
</ul> </ul>
<p> <p>
Začněte nyní a sdílejte své vzdušné zážitky s celým světem! Začni nyní a sdílejte své vzdušné zážitky s celým světem!
</p> </p>
<p> <p>
S pozdravem, S pozdravem,
<br> <br>
Tým Polétání.cz Michal Kváček
</p> </p>
</mj-text> </mj-text>
<mj-button background-color="#469ccc" color="white" font-size="16px" align="center" href="[Odkaz na přihlášení]"> <mj-button background-color="#469ccc" color="white" font-size="16px" align="center" href="https://poletani.cz/login">
Přihlásit se do Polétání.cz Přihlásit se do Polétání.cz
</mj-button> </mj-button>
<mj-divider border-color="#469ccc"></mj-divider> <mj-divider border-color="#469ccc"></mj-divider>
-53
View File
@@ -1,53 +0,0 @@
from datetime import timedelta
from fastapi import FastAPI, Security, Depends, BackgroundTasks, APIRouter
from fastapi_jwt import JwtAuthorizationCredentials
from fastapi_jwt.jwt import JwtAccessBearerCookie
from sqlalchemy import select
from starlette.responses import RedirectResponse
from strawberry.fastapi import GraphQLRouter
from config import GRAPHIQL, APP_DEBUG
from database import async_session, models
from graphql_schema.schema import GraphQLContext, schema
def setup_graphql_endpoint(app: FastAPI, access_security: JwtAccessBearerCookie):
if not APP_DEBUG:
return
debug_router = APIRouter()
@debug_router.get("/graphql/autologin")
async def autologin():
access_token = access_security.create_access_token(subject={"id": 6, "name": "Franta Vomacka"})
response = RedirectResponse(url="/graphql")
access_security.set_access_cookie(response, access_token, expires_delta=timedelta(days=14))
return response
app.include_router(debug_router)
async def setup_graphql_context(credentials: JwtAuthorizationCredentials = Security(access_security)):
user_id = credentials['id'] if credentials else None
organization_ids = set()
if user_id:
async with async_session() as db:
organization_ids = set((await db.scalars(
select(models.user_is_in_organization.c.organization_id)
.filter(models.user_is_in_organization.c.user_id == user_id)
)).all())
return GraphQLContext(
user_id=user_id,
organization_ids=organization_ids,
jwt_auth_credentials=credentials,
jwt=access_security,
background_tasks=Depends(BackgroundTasks)
)
graphql_app = GraphQLRouter(
schema,
graphiql=GRAPHIQL,
debug=APP_DEBUG,
context_getter=setup_graphql_context
)
app.include_router(graphql_app, prefix="/graphql")
+2
View File
@@ -33,6 +33,8 @@ class LoginEndpoint(AuthEndpoint):
access_token = self.access_security.create_access_token(subject=subject) access_token = self.access_security.create_access_token(subject=subject)
refresh_token = self.refresh_security.create_refresh_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.access_security.set_access_cookie(resp, access_token)
self.refresh_security.set_refresh_cookie( self.refresh_security.set_refresh_cookie(
resp, refresh_token, resp, refresh_token,
+1 -2
View File
@@ -1,7 +1,6 @@
import re import re
from fastapi import HTTPException, BackgroundTasks from fastapi import HTTPException, BackgroundTasks
from sqlalchemy import select from sqlalchemy import select
from typing import Optional
from pydantic import BaseModel, Field, model_validator from pydantic import BaseModel, Field, model_validator
from background_jobs.send_email import send_email from background_jobs.send_email import send_email
from database.models import User from database.models import User
@@ -12,7 +11,7 @@ from emails.emails import WelcomeEmail
class RegistrationInput(BaseModel): class RegistrationInput(BaseModel):
email: str = Field(..., min_length=4) email: str = Field(..., min_length=4)
name: Optional[str] name: str | None
password: str password: str
@model_validator(mode="before") @model_validator(mode="before")
+73 -13
View File
@@ -1,27 +1,87 @@
from typing import List, Dict import dataclasses
from typing import Optional
import aiohttp import aiohttp
from database import models
from decorators.retryable import retryable
from utils.list_utils import chunks
@dataclasses.dataclass
class ElevationInfo:
id: int | None
lat: float
lng: float
elevation: float
class ElevationAPI: class ElevationAPI:
ELEVATION_ENDPOINT = "https://api.open-elevation.com/api/v1/lookup" ELEVATION_ENDPOINT: str
def get_request(self, points: List[Dict[str, float]]): def get_request(self, session: aiohttp.ClientSession, points: list[dict[str, float]]):
return {"locations": [{"latitude": point['lat'], "longitude": point['lng']} for point in points]} ...
async def call_api(self, points: List[Dict[str, float]]): @retryable
async def call_api(self, points: list[dict[str, float]]):
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.post(self.ELEVATION_ENDPOINT, json=self.get_request(points)) as resp: async with self.get_request(session, points) as resp:
resp.raise_for_status() resp.raise_for_status()
return await resp.json() return await resp.json()
async def get_elevation_for_points(self, points: List[Dict[str, float]]) -> List[dict]:
class OpenElevationAPI(ElevationAPI):
ELEVATION_ENDPOINT = "https://api.open-elevation.com/api/v1/lookup"
def get_request(self, session: aiohttp.ClientSession, points: list[dict[str, float]]):
data = {"locations": [{"latitude": point['lat'], "longitude": point['lng']} for point in points]}
return session.post(self.ELEVATION_ENDPOINT, json=data)
async def get_elevation_for_points(self, points: list[dict[str, float]]) -> list[ElevationInfo]:
response = await self.call_api(points) response = await self.call_api(points)
return [{ elevation = []
"lat": loc['latitude'], for i, point in enumerate(points):
"lng": loc['longitude'], response_point = response['results'][i]
"elevation": loc['elevation'] elevation.append(ElevationInfo(**point, elevation=response_point['elevation']))
} for loc in response['results']] return elevation
elevation_api = ElevationAPI() class OpenMeteoElevationAPI(ElevationAPI):
ELEVATION_ENDPOINT = "https://api.open-meteo.com/v1/elevation"
def get_request(self, session: aiohttp.ClientSession, points: list[dict[str, float]]):
latitudes = ",".join(str(point['lat']) for point in points)
longitudes = ",".join(str(point['lng']) for point in points)
url = f"{self.ELEVATION_ENDPOINT}?latitude={latitudes}&longitude={longitudes}"
return session.get(url)
async def get_elevation_for_points(self, points: list[dict[str, float]]) -> list[ElevationInfo]:
elevation = []
for chunk in chunks(points, n=100):
response = await self.call_api(chunk)
chunk_elevation = []
for i, point in enumerate(chunk):
chunk_elevation.append(ElevationInfo(**point, elevation=response['elevation'][i]))
elevation += chunk_elevation
return elevation
class CombinedElevationAPI:
async def get_elevation_for_points(self, points: list[dict[str, float]]) -> list[ElevationInfo]:
for api in (OpenMeteoElevationAPI(), OpenElevationAPI()):
elevation = await api.get_elevation_for_points(points)
if elevation:
return elevation
async def update_track_points_elevation(db, track_points: list[models.TrackPoint]):
points = [{"id": p.id, "lat": p.gps_latitude, "lng": p.gps_longitude} for p in track_points]
elevation = await elevation_api.get_elevation_for_points(points)
for point in elevation:
await models.TrackPoint.update(db, {"terrain_elevation": point.elevation}, id=point.id)
elevation_api = CombinedElevationAPI()
+12 -39
View File
@@ -1,9 +1,7 @@
from collections import defaultdict from collections import defaultdict
from datetime import datetime from datetime import datetime, timedelta
from typing import List, Dict, Any from typing import List, Dict, Any
from aiocache import cached
from lxml import etree from lxml import etree
from lxml.etree import _ElementTree
class GPXParser: class GPXParser:
@@ -68,48 +66,47 @@ class GPXParser:
def run_xpath(self, path: str): def run_xpath(self, path: str):
return self.gpx.xpath(path, namespaces=self.namespace) return self.gpx.xpath(path, namespaces=self.namespace)
@cached()
async def get_times_all(self): async def get_times_all(self):
nodes = self.run_xpath("//gpx:trkpt/gpx:time") nodes = self.run_xpath("//gpx:trkpt/gpx:time")
return [datetime.fromisoformat(node.text).astimezone() for node in nodes] return [datetime.fromisoformat(node.text).astimezone() for node in nodes]
@cached()
async def get_times(self): async def get_times(self):
times = await self.get_times_all() times = await self.get_times_all()
return await self.sample_times(times) return await self.sample_times(times)
@cached() async def get_total_duration(self) -> timedelta:
times = await self.get_times_all()
return times[-1] - times[0]
async def get_coordinates(self) -> List[Dict[str, float]]: 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") nodes = self.run_xpath("//gpx:trkpt")
return await self.average_coordinates( return [{"lat": float(node.attrib["lat"]), "lng": float(node.attrib['lon'])} for node in nodes]
[{"lat": float(node.attrib["lat"]), "lng": float(node.attrib['lon'])} for node in nodes]
)
@cached()
async def get_speed(self) -> List[float]: async def get_speed(self) -> List[float]:
nodes = self.run_xpath("//gpx:speed") nodes = self.run_xpath("//gpx:speed")
return await self.average_sample_numbers([float(node.text) for node in nodes]) return await self.average_sample_numbers([float(node.text) for node in nodes])
@cached()
async def get_magnetic_variation(self) -> List[float]: async def get_magnetic_variation(self) -> List[float]:
nodes = self.run_xpath("//gpx:magvar") nodes = self.run_xpath("//gpx:magvar")
return await self.average_sample_numbers([int(node.text) for node in nodes]) return await self.average_sample_numbers([int(node.text) for node in nodes])
@cached()
async def get_altitude(self) -> List[float]: async def get_altitude(self) -> List[float]:
nodes = self.run_xpath("//gpx:ele") nodes = self.run_xpath("//gpx:ele")
return await self.average_sample_numbers([float(node.text) for node in nodes]) return await self.average_sample_numbers([float(node.text) for node in nodes])
@cached()
async def get_terrain_elevation(self) -> List[float]: async def get_terrain_elevation(self) -> List[float]:
nodes = self.run_xpath("//gpx:terrain_elevation") nodes = self.run_xpath("//gpx:terrain_elevation")
return await self.average_sample_numbers([float(node.text) for node in nodes]) return await self.average_sample_numbers([float(node.text) for node in nodes])
@cached()
async def get_max_speed(self): async def get_max_speed(self):
return max(await self.get_speed()) or 0 return max(await self.get_speed()) or 0
@cached() async def get_min_speed(self):
return min(await self.get_speed()) or 0
async def get_avg_speed(self): async def get_avg_speed(self):
speeds = await self.get_speed() speeds = await self.get_speed()
if not speeds: if not speeds:
@@ -117,33 +114,9 @@ class GPXParser:
return round(sum(speeds) / len(speeds), 2) return round(sum(speeds) / len(speeds), 2)
@cached()
async def get_max_altitude(self): async def get_max_altitude(self):
return max(await self.get_altitude()) or 0 return max(await self.get_altitude()) or 0
@cached()
async def get_avg_altitude(self): async def get_avg_altitude(self):
altitudes = await self.get_altitude() altitudes = await self.get_altitude()
return round(sum(altitudes) / len(altitudes), 2) 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)
+20 -53
View File
@@ -8,12 +8,13 @@ import aiohttp
from sqlalchemy import delete from sqlalchemy import delete
from database import models from database import models
from database.transaction import get_session from database.transaction import get_session
from utils.gps import get_distance, get_bearing, EARTH_RADIUS
# https://pyopenair.readthedocs.io/en/latest/openair.html # https://pyopenair.readthedocs.io/en/latest/openair.html
# http://www.winpilot.com/usersguide/userairspace.asp # http://www.winpilot.com/usersguide/userairspace.asp
class AirspaceItemType(enum.Enum): class AirspaceType(enum.Enum):
AIRSPACE_A = "A" AIRSPACE_A = "A"
AIRSPACE_B = "B" AIRSPACE_B = "B"
AIRSPACE_C = "C" AIRSPACE_C = "C"
@@ -46,51 +47,17 @@ class Coordinates:
@dataclasses.dataclass @dataclasses.dataclass
class AirspaceItem: class Airspace:
type: AirspaceItemType type: AirspaceType
info: str = "" info: str = ""
upper_limit: str = None upper_limit: str = None
lower_limit: str = None lower_limit: str = None
center: Optional[Coordinates] = None center: Optional[Coordinates] = None
radius_nm: Optional[float] = None radius_nm: float | None = None
bounds: list[Coordinates] = dataclasses.field(default_factory=lambda: []) bounds: list[Coordinates] = dataclasses.field(default_factory=lambda: [])
class ArcExpander: 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 @classmethod
def get_arc_coordinates( def get_arc_coordinates(
cls, cls,
@@ -98,9 +65,9 @@ class ArcExpander:
origin_point: tuple[float, float], origin_point: tuple[float, float],
end_point: tuple[float, float] end_point: tuple[float, float]
) -> list[Coordinates]: ) -> list[Coordinates]:
initial_bearing = cls.get_bearing(center_point, origin_point) initial_bearing = get_bearing(center_point, origin_point)
target_bearing = cls.get_bearing(center_point, end_point) target_bearing = get_bearing(center_point, end_point)
distance = cls.get_distance(center_point, origin_point) distance = get_distance(center_point, origin_point)
print(f"DISTANCE {distance} meters, {center_point=}, {origin_point=}, {end_point=}") print(f"DISTANCE {distance} meters, {center_point=}, {origin_point=}, {end_point=}")
@@ -122,12 +89,12 @@ class ArcExpander:
for bearing in list_bearings: for bearing in list_bearings:
lat1, lon1 = math.radians(center_point[0]), math.radians(center_point[1]) lat1, lon1 = math.radians(center_point[0]), math.radians(center_point[1])
lat2 = math.asin( lat2 = math.asin(
math.sin(lat1) * math.cos(distance / cls.R) + math.cos(lat1) * math.sin(distance / cls.R) * math.cos(bearing) math.sin(lat1) * math.cos(distance / EARTH_RADIUS) + math.cos(lat1) * math.sin(distance / EARTH_RADIUS) * math.cos(bearing)
) )
lon2 = lon1 + math.atan2( lon2 = lon1 + math.atan2(
math.sin(bearing) * math.sin(distance / cls.R) * math.cos(lat1), math.sin(bearing) * math.sin(distance / EARTH_RADIUS) * math.cos(lat1),
math.cos(distance / cls.R) - math.sin(lat1) * math.sin(lat2) math.cos(distance / EARTH_RADIUS) - math.sin(lat1) * math.sin(lat2)
) )
lat2, lon2 = math.degrees(lat2), math.degrees(lon2) lat2, lon2 = math.degrees(lat2), math.degrees(lon2)
@@ -140,7 +107,7 @@ class OpenAirParser:
def __init__(self, file: Path | str): def __init__(self, file: Path | str):
self.file = None self.file = None
self.context_type: Optional[AirspaceItemType] = None self.context_type: Optional[AirspaceType] = None
self.context = {} self.context = {}
self.file = file self.file = file
@@ -149,7 +116,7 @@ class OpenAirParser:
if not self.context_type: if not self.context_type:
return return
data = AirspaceItem(type=self.context_type, **self.context) data = Airspace(type=self.context_type, **self.context)
self.context_type = None self.context_type = None
self.context = {} self.context = {}
@@ -227,19 +194,19 @@ class OpenAirParser:
pass pass
case 'AC': case 'AC':
try: try:
self.context_type = AirspaceItemType(value) self.context_type = AirspaceType(value)
except ValueError as e: except ValueError as e:
print(f"Neznam {e}, ignoruji") print(f"Neznam {e}, ignoruji")
return return
case 'AN': case 'AN':
if self.context_type == AirspaceItemType.AIRSPACE_G: if self.context_type == AirspaceType.AIRSPACE_G:
match value[0:2]: match value[0:2]:
case 'PG': case 'PG':
self.context_type = AirspaceItemType.PARAGLIDING_ZONE self.context_type = AirspaceType.PARAGLIDING_ZONE
case 'LK': case 'LK':
self.context_type = AirspaceItemType.ATZ self.context_type = AirspaceType.ATZ
case _: case _:
self.context_type = AirspaceItemType.TRAGA self.context_type = AirspaceType.TRAGA
self.context['info'] = value self.context['info'] = value
case 'AL': case 'AL':
@@ -282,7 +249,7 @@ async def download_airspace(source, destination):
async def save_airspace(destination): async def save_airspace(destination):
parser = OpenAirParser(destination) parser = OpenAirParser(destination)
async with get_session() as db: async with get_session() as db:
await db.execute(delete(models.AirspaceItem)) await db.execute(delete(models.Airspace))
for item in parser.parse(): for item in parser.parse():
# if 'CTR KARLOVY VARY' not in item.info: # if 'CTR KARLOVY VARY' not in item.info:
@@ -294,7 +261,7 @@ async def save_airspace(destination):
# print("---" * 10) # print("---" * 10)
async with get_session() as db: async with get_session() as db:
await models.AirspaceItem.create(db, { await models.Airspace.create(db, {
"country": "cz", "country": "cz",
"name": item.info, "name": item.info,
"type": item.type.value, "type": item.type.value,
+6 -2
View File
@@ -3,6 +3,7 @@ import urllib.parse
from typing import Tuple, Dict from typing import Tuple, Dict
import aiohttp import aiohttp
from aiocache import cached from aiocache import cached
from decorators.retryable import retryable
class Weather: class Weather:
@@ -38,13 +39,17 @@ class Weather:
return f"{url}{query_string}" return f"{url}{query_string}"
@cached(ttl=6 * 3600) @cached(ttl=6 * 3600)
@retryable
async def download_weather_for_day(self, date: datetime.date, gps: Tuple[float, float]): 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) url = self.get_weather_info_url(start_date=date, end_date=date, gps=gps)
try:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get(url) as resp: async with session.get(url) as resp:
resp.raise_for_status() resp.raise_for_status()
return await resp.json() return await resp.json()
except Exception as e:
print(f"Error in downloading weather: {e}, {type(e)}")
async def get_weather_for_hour( async def get_weather_for_hour(
self, date_time: datetime.datetime, gps: Tuple[float, float] self, date_time: datetime.datetime, gps: Tuple[float, float]
@@ -52,10 +57,9 @@ class Weather:
data = await self.download_weather_for_day(date_time.date(), gps) data = await self.download_weather_for_day(date_time.date(), gps)
# TODO: kontrola timezone! # TODO: kontrola timezone!
# TODO: interpolace - udelat vazenyprumer z dvou po sobe jdoucich hodin
idx = date_time.hour idx = date_time.hour
result_data = {metric: data['hourly'][metric][idx] for metric in self.METRICS} result_data = {metric: data['hourly'][metric][idx] for metric in self.METRICS}
result_data['datetime'] = datetime.datetime.strptime(data['hourly']['time'][idx], "%Y-%m-%dT%H:%M") result_data['date_time'] = datetime.datetime.strptime(data['hourly']['time'][idx], "%Y-%m-%dT%H:%M")
return result_data return result_data
+37
View File
@@ -0,0 +1,37 @@
import dataclasses
from datetime import datetime
from fastapi import BackgroundTasks, Depends, Security
from fastapi_jwt import JwtAuthorizationCredentials
from sqlalchemy import select
from strawberry.fastapi import BaseContext
from database import async_session, models
from jwt import access_security
@dataclasses.dataclass
class GraphQLContext(BaseContext):
request_start_time: datetime
user_id: int
organization_ids: set[int]
jwt_auth_credentials: JwtAuthorizationCredentials
background_tasks: BackgroundTasks
async def setup_graphql_context(credentials: JwtAuthorizationCredentials = Security(access_security)):
user_id = credentials['id'] if credentials else None
organization_ids = set()
if user_id:
async with async_session() as db:
organization_ids = set((await db.scalars(
select(models.user_is_in_organization.c.organization_id)
.filter(models.user_is_in_organization.c.user_id == user_id)
)).all())
return GraphQLContext(
user_id=user_id,
organization_ids=organization_ids,
jwt_auth_credentials=credentials,
background_tasks=Depends(BackgroundTasks),
request_start_time=datetime.now()
)
+20 -12
View File
@@ -1,5 +1,5 @@
from collections import defaultdict from collections import defaultdict
from typing import Type, List, Optional from typing import Type
from logger import log from logger import log
from database import models, async_session from database import models, async_session
from database.query_builder import QueryBuilder from database.query_builder import QueryBuilder
@@ -9,7 +9,7 @@ class BaseDataloader:
def __init__( def __init__(
self, self,
model: Type[models.BaseModel], model: Type[models.BaseModel],
relationship_column, filters: Optional[list] = None relationship_column, filters: list | None = None
): ):
super().__init__() super().__init__()
self.model = model self.model = model
@@ -25,11 +25,15 @@ class BaseDataloader:
class SingleModelByIdDataloader(BaseDataloader): class SingleModelByIdDataloader(BaseDataloader):
async def load(self, ids: List[int]): async def load(self, ids: list[int]):
ids_set = {id_ for id_ in set(ids) if id_ is not None}
if not ids_set:
return [None for _ in ids]
async with async_session() as session: async with async_session() as session:
query = ( query = (
self.query_builder.get_simple_query(extra_select=[self.relationship_column], include_deleted=True) self.query_builder.get_simple_query(extra_select=[self.relationship_column], include_deleted=True)
.filter(self.relationship_column.in_(set(ids))) .filter(self.relationship_column.in_(ids_set))
.filter(*self.filters) .filter(*self.filters)
) )
@@ -43,10 +47,10 @@ class MultiModelsDataloader(BaseDataloader):
self, self,
model: Type[models.BaseModel], model: Type[models.BaseModel],
relationship_column=None, relationship_column=None,
filters: Optional[list] = None, filters: list | None = None,
extra_select: Optional[list] = None, extra_select: list | None = None,
extra_join: Optional[list] = None, extra_join: list | None = None,
order_by: Optional[list] = None, order_by: list | None = None,
): ):
super().__init__(model, relationship_column, filters) super().__init__(model, relationship_column, filters)
@@ -62,13 +66,13 @@ class MultiModelsDataloader(BaseDataloader):
order_by = [model.id.desc()] # defaultne radit od nejnovejsich zaznamu order_by = [model.id.desc()] # defaultne radit od nejnovejsich zaznamu
self.order_by = order_by self.order_by = order_by
def get_query(self, ids: list[int]): def get_query(self, ids: set[int]):
query = ( query = (
self.query_builder.get_simple_query( self.query_builder.get_simple_query(
extra_select=[self.relationship_column] + self.extra_select, extra_select=[self.relationship_column] + self.extra_select,
order_by=self.order_by order_by=self.order_by
) )
.filter(self.relationship_column.in_(set(ids))) .filter(self.relationship_column.in_(ids))
.filter(*self.filters) .filter(*self.filters)
) )
@@ -80,8 +84,12 @@ class MultiModelsDataloader(BaseDataloader):
return query return query
async def load(self, ids: List[int]): async def load(self, ids: list[int]) -> list:
query = self.get_query(ids) ids_set = {id_ for id_ in set(ids) if id_ is not None}
if not ids_set:
return [[] for _ in ids]
query = self.get_query(ids_set)
async with async_session() as db: async with async_session() as db:
data = (await db.execute(query)).all() data = (await db.execute(query)).all()
@@ -10,8 +10,8 @@ async def load_flight_durations(ids: List[int]):
select( select(
models.Flight.id, models.Flight.id,
func.timediff(models.Flight.landing_datetime, models.Flight.takeoff_datetime).label("diff"), func.timediff(models.Flight.landing_datetime, models.Flight.takeoff_datetime).label("diff"),
func.coalesce(func.sum(models.FlightTrack.landing_duration), 0).label("landing_duration") func.coalesce(func.sum(models.FlightTurnPoint.landing_duration), 0).label("landing_duration")
).join(models.Flight.track, isouter=True) ).join(models.Flight.turn_points, isouter=True)
.group_by(models.Flight.id) .group_by(models.Flight.id)
.filter(models.Flight.id.in_(ids)) .filter(models.Flight.id.in_(ids))
+66 -6
View File
@@ -20,9 +20,51 @@ flight_copilots_dataloader = DataLoader(
models.flight_has_copilot.c.flight_id == models.Flight.id, models.flight_has_copilot.c.flight_id == models.Flight.id,
models.flight_has_copilot.c.copilot_id == models.Copilot.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) 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( flights_by_copilot_dataloader = DataLoader(
load_fn=MultiModelsDataloader( load_fn=MultiModelsDataloader(
models.Flight, models.Flight,
@@ -56,7 +98,7 @@ flight_by_poi_dataloader = DataLoader(
models.Flight, models.Flight,
relationship_column=models.PointOfInterest.id, relationship_column=models.PointOfInterest.id,
order_by=[models.Flight.takeoff_datetime.desc()], order_by=[models.Flight.takeoff_datetime.desc()],
extra_join=[models.Flight.track, models.PointOfInterest] extra_join=[models.Flight.turn_points, models.PointOfInterest]
).load, ).load,
cache=False cache=False
) )
@@ -113,11 +155,20 @@ poi_photos_dataloader = DataLoader(
).load, ).load,
cache=False cache=False
) )
flight_track_dataloader = DataLoader( flight_turn_points_dataloader = DataLoader(
load_fn=MultiModelsDataloader( load_fn=MultiModelsDataloader(
models.FlightTrack, models.FlightTurnPoint,
relationship_column=models.FlightTrack.flight_id, relationship_column=models.FlightTurnPoint.flight_id,
order_by=[models.FlightTrack.order] order_by=[models.FlightTurnPoint.order]
).load,
cache=False
)
track_points_dataloder = DataLoader(
load_fn=MultiModelsDataloader(
models.TrackPoint,
relationship_column=models.TrackPoint.track_id,
order_by=[models.TrackPoint.timestamp, models.TrackPoint.id]
).load, ).load,
cache=False cache=False
) )
@@ -150,3 +201,12 @@ photos_aircraft_dataloader = DataLoader(
).load, ).load,
cache=False 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
)
@@ -4,7 +4,7 @@ from database import models
from graphql_schema.dataloaders.base import SingleModelByIdDataloader from graphql_schema.dataloaders.base import SingleModelByIdDataloader
def create_dataloader(model: Type[models.BaseModel], relationship_column=None, filters: Optional[list] = None): def create_dataloader(model: Type[models.BaseModel], relationship_column=None, filters: list | None = None):
loader = SingleModelByIdDataloader(model, relationship_column, filters).load loader = SingleModelByIdDataloader(model, relationship_column, filters).load
return DataLoader(load_fn=loader, cache=False) return DataLoader(load_fn=loader, cache=False)
@@ -14,7 +14,8 @@ airport_dataloader = create_dataloader(models.Airport)
aircraft_dataloader = create_dataloader(models.Aircraft) aircraft_dataloader = create_dataloader(models.Aircraft)
event_dataloader = create_dataloader(models.Event) event_dataloader = create_dataloader(models.Event)
organizations_dataloader = create_dataloader(models.Organization) organizations_dataloader = create_dataloader(models.Organization)
airport_weather_info_loader = create_dataloader(models.WeatherInfo) weather_info_loader = create_dataloader(models.WeatherInfo)
track_dataloader = create_dataloader(models.Track)
poi_dataloader = create_dataloader(models.PointOfInterest) poi_dataloader = create_dataloader(models.PointOfInterest)
poi_type_dataloader = create_dataloader(models.PointOfInterestType) poi_type_dataloader = create_dataloader(models.PointOfInterestType)
flight_dataloader = create_dataloader(models.Flight) flight_dataloader = create_dataloader(models.Flight)
+20 -41
View File
@@ -1,70 +1,49 @@
from typing import Optional
import strawberry import strawberry
from decorators.endpoints import authenticated_user_only, allow_public from strawberry import Info
from decorators.error_logging import error_logging from .helpers.filters import get_filters
from .helpers.pagination import get_pagination_window, PaginationWindow from .helpers.pagination import PaginationWindow
from .resolvers.aircraft import AircraftMutationResolver, AircraftQueryResolver from .resolvers.aircraft import AircraftMutationResolver, AircraftQueryResolver
from graphql_schema.entities.types.mutation_input import CreateAircraftInput, EditAircraftInput from graphql_schema.entities.types.mutation_input import CreateAircraftInput, EditAircraftInput
from graphql_schema.entities.types.types import Aircraft from graphql_schema.entities.types.types import Aircraft
from ..extensions.field.auth import AllowPublicAccess, AuthenticatedOnly
from ..extensions.field.pagination import OffsetPagination
@strawberry.type @strawberry.type
class AircraftQueries: class AircraftQueries:
@strawberry.field() @strawberry.field(extensions=[OffsetPagination(item_type=Aircraft), AuthenticatedOnly()])
@error_logging async def aircrafts(root, info: Info, search: str | None = None) -> PaginationWindow[Aircraft]:
@authenticated_user_only() return AircraftQueryResolver().get_query(
async def aircrafts(root, info, limit: int, offset: int = 0) -> PaginationWindow[Aircraft]:
query = AircraftQueryResolver().get_query(
info.context.user_id, info.context.user_id,
organization_ids=info.context.organization_ids organization_ids=info.context.organization_ids,
search=search
) )
return await get_pagination_window( @strawberry.field(extensions=[AllowPublicAccess()])
query=query,
item_type=Aircraft,
limit=limit,
offset=offset
)
@strawberry.field()
@error_logging
@allow_public
async def aircraft( async def aircraft(
root, info, root, info,
id: Optional[int] = None, id: int | None = None,
call_sign: Optional[str] = None, call_sign: str | None = None,
public: Optional[bool] = False public: bool | None = False
) -> Aircraft: ) -> Aircraft:
filter_params = {}
if id:
filter_params['object_id'] = id
if call_sign:
filter_params['call_sign'] = call_sign
return await AircraftQueryResolver().get_one( return await AircraftQueryResolver().get_one(
user_id=info.context.user_id, user_id=info.context.user_id,
organization_ids=info.context.organization_ids if not public else None, organization_ids=info.context.organization_ids if not public else None,
only_public=public, only_public=public,
**filter_params **get_filters(object_id=id, call_sign=call_sign)
) )
@strawberry.type @strawberry.type
class AircraftMutation: class AircraftMutation:
@strawberry.mutation @strawberry.mutation(extensions=[AuthenticatedOnly()])
@error_logging
@authenticated_user_only()
async def create_aircraft(root, info, input: CreateAircraftInput) -> Aircraft: async def create_aircraft(root, info, input: CreateAircraftInput) -> Aircraft:
return await AircraftMutationResolver().create(info.context, input) return await AircraftMutationResolver().create(info.context, input)
@strawberry.mutation @strawberry.mutation(extensions=[AuthenticatedOnly()])
@error_logging
@authenticated_user_only()
async def edit_aircraft(root, info, id: int, input: EditAircraftInput) -> Aircraft: async def edit_aircraft(root, info, id: int, input: EditAircraftInput) -> Aircraft:
return await AircraftMutationResolver().update(id, user_id=info.context.user_id, data=input) return await AircraftMutationResolver().update(info.context, id=id, data=input)
@strawberry.mutation @strawberry.mutation(extensions=[AuthenticatedOnly()])
@authenticated_user_only()
async def delete_aircraft(self, info, id: int) -> Aircraft: async def delete_aircraft(self, info, id: int) -> Aircraft:
return await AircraftMutationResolver().delete(info.context.user_id, id) return await AircraftMutationResolver().delete(info.context, id)
+3 -9
View File
@@ -1,23 +1,17 @@
from typing import List
import strawberry import strawberry
from database import models from database import models
from decorators.error_logging import error_logging
from decorators.endpoints import authenticated_user_only
from graphql_schema.entities.resolvers.base import BaseQueryResolver from graphql_schema.entities.resolvers.base import BaseQueryResolver
from graphql_schema.entities.types.types import Airport from graphql_schema.entities.types.types import Airport
from graphql_schema.extensions.field.auth import AuthenticatedOnly
@strawberry.type @strawberry.type
class AirportQueries: class AirportQueries:
@strawberry.field() @strawberry.field()
@error_logging async def airports(root, info) -> list[Airport]:
@authenticated_user_only()
async def airports(root, info) -> List[Airport]:
return await BaseQueryResolver(Airport, models.Airport).get_list(info.context.user_id) return await BaseQueryResolver(Airport, models.Airport).get_list(info.context.user_id)
@strawberry.field() @strawberry.field(extensions=[AuthenticatedOnly()])
@error_logging
@authenticated_user_only()
async def airport(root, info, id: int) -> Airport: async def airport(root, info, id: int) -> Airport:
return await BaseQueryResolver(Airport, models.Airport).get_one( return await BaseQueryResolver(Airport, models.Airport).get_one(
object_id=id, object_id=id,
+5 -8
View File
@@ -1,7 +1,5 @@
from typing import List, Optional
import strawberry import strawberry
from database import models from database import models
from decorators.error_logging import error_logging
from graphql_schema.entities.resolvers.base import BaseQueryResolver from graphql_schema.entities.resolvers.base import BaseQueryResolver
from graphql_schema.entities.types.types import Airspace from graphql_schema.entities.types.types import Airspace
@@ -9,18 +7,17 @@ from graphql_schema.entities.types.types import Airspace
@strawberry.type @strawberry.type
class AirspaceQueries: class AirspaceQueries:
@strawberry.field() @strawberry.field()
@error_logging
async def airspaces( async def airspaces(
root, info, country: Optional[str] = None, types: Optional[list[str]] = None root, info, country: str | None = None, types: list[str] | None = None
) -> List[Airspace]: ) -> list[Airspace]:
filters = [] filters = []
if country: if country:
filters.append(models.AirspaceItem.country == country) filters.append(models.Airspace.country == country)
if types: if types:
filters.append(models.AirspaceItem.type.in_(types)) filters.append(models.Airspace.type.in_(types))
return await BaseQueryResolver(Airspace, models.AirspaceItem).get_list( return await BaseQueryResolver(Airspace, models.Airspace).get_list(
info.context.user_id, info.context.user_id,
country=country, country=country,
filters=filters filters=filters
+20 -37
View File
@@ -1,48 +1,35 @@
from typing import List, Optional
import strawberry import strawberry
from graphql import GraphQLError from graphql import GraphQLError
from strawberry.types import Info from strawberry.types import Info
from database import models from database import models
from decorators.error_logging import error_logging from graphql_schema.entities.helpers.filters import get_filters
from decorators.endpoints import authenticated_user_only, allow_public
from graphql_schema.entities.helpers.detail import get_detail_filters
from graphql_schema.entities.resolvers.base import BaseMutationResolver from graphql_schema.entities.resolvers.base import BaseMutationResolver
from graphql_schema.entities.resolvers.copilot import CopilotQueryResolver from graphql_schema.entities.resolvers.copilot import CopilotQueryResolver
from graphql_schema.entities.types.mutation_input import CreateCopilotInput, EditCopilotInput from graphql_schema.entities.types.mutation_input import CreateCopilotInput, EditCopilotInput
from graphql_schema.entities.types.types import Copilot from graphql_schema.entities.types.types import Copilot
from graphql_schema.extensions.field.auth import AllowPublicAccess, AuthenticatedOnly
@strawberry.type @strawberry.type
class CopilotQueries: class CopilotQueries:
@strawberry.field() @strawberry.field(extensions=[AuthenticatedOnly()])
@error_logging async def copilots(root, info: Info) -> list[Copilot]:
@authenticated_user_only()
async def copilots(root, info: Info) -> List[Copilot]:
return await CopilotQueryResolver().get_list(info.context.user_id) return await CopilotQueryResolver().get_list(info.context.user_id)
@strawberry.field() @strawberry.field(extensions=[AllowPublicAccess()])
@error_logging
@allow_public
async def copilot( async def copilot(
root, info: Info, root, info: Info,
id: Optional[int] = None, id: int | None = None,
url_slug: Optional[str] = None, url_slug: str | None = None,
pilot_username: Optional[str] = None, pilot_username: str | None = None,
upload_token: Optional[str] = None, upload_token: str | None = None,
upload_flight_slug: Optional[str] = None, upload_flight_slug: str | None = None,
public: Optional[bool] = False public: bool | None = False
) -> Copilot: ) -> Copilot:
filter_params = {} filter_params = get_filters(
if id: object_id=id, url_slug=url_slug, pilot_username=pilot_username, upload_token=upload_token,
filter_params['object_id'] = id upload_flight_slug=upload_flight_slug,
if url_slug is not None: )
filter_params['url_slug'] = url_slug
if upload_token and upload_flight_slug:
filter_params['upload_token'] = upload_token
filter_params['upload_flight_slug'] = upload_flight_slug
if pilot_username:
filter_params['pilot_username'] = pilot_username
if not filter_params: if not filter_params:
raise GraphQLError(f"Invalid identification supplied: {filter_params}") raise GraphQLError(f"Invalid identification supplied: {filter_params}")
@@ -55,14 +42,10 @@ class CopilotQueries:
@strawberry.type @strawberry.type
class CopilotMutation: class CopilotMutation:
@strawberry.mutation @strawberry.mutation(extensions=[AuthenticatedOnly()])
@error_logging async def create_copilot(root, info: Info, input: CreateCopilotInput) -> Copilot:
@authenticated_user_only()
async def create_copilot(root, info, input: CreateCopilotInput) -> Copilot:
return await BaseMutationResolver(Copilot, models.Copilot).create(info.context, data=input) return await BaseMutationResolver(Copilot, models.Copilot).create(info.context, data=input)
@strawberry.mutation @strawberry.mutation(extensions=[AuthenticatedOnly()])
@error_logging async def edit_copilot(root, info: Info, id: int, input: EditCopilotInput) -> Copilot:
@authenticated_user_only() return await BaseMutationResolver(Copilot, models.Copilot).update(info.context, id, input)
async def edit_copilot(root, info, id: int, input: EditCopilotInput) -> Copilot:
return await BaseMutationResolver(Copilot, models.Copilot).update(id, input, info.context.user_id)
+16 -33
View File
@@ -1,52 +1,39 @@
from typing import Optional
import strawberry import strawberry
from database import models from database import models
from decorators.endpoints import authenticated_user_only, allow_public from graphql_schema.entities.helpers.filters import get_detail_filters
from decorators.error_logging import error_logging from graphql_schema.entities.helpers.pagination import PaginationWindow
from graphql_schema.entities.helpers.detail import get_detail_filters
from graphql_schema.entities.helpers.pagination import PaginationWindow, get_pagination_window
from graphql_schema.entities.resolvers.base import BaseMutationResolver from graphql_schema.entities.resolvers.base import BaseMutationResolver
from graphql_schema.entities.resolvers.event import EventQueryResolver from graphql_schema.entities.resolvers.event import EventQueryResolver
from graphql_schema.entities.types.mutation_input import CreateEventInput, EditEventInput from graphql_schema.entities.types.mutation_input import CreateEventInput, EditEventInput
from graphql_schema.entities.types.types import Event from graphql_schema.entities.types.types import Event
from graphql_schema.extensions.field.auth import AllowPublicAccess, AuthenticatedOnly
from graphql_schema.extensions.field.pagination import OffsetPagination
@strawberry.type @strawberry.type
class EventQueries: class EventQueries:
@strawberry.field() @strawberry.field(extensions=[OffsetPagination(item_type=Event), AllowPublicAccess()])
@error_logging
@allow_public
async def events( async def events(
root, root,
info, info,
limit: int, username: str | None = None,
offset: int = 0, public: bool | None = False,
username: Optional[str] = None,
public: Optional[bool] = False,
) -> PaginationWindow[Event]: ) -> PaginationWindow[Event]:
query = EventQueryResolver().get_query( return EventQueryResolver().get_query(
user_id=info.context.user_id, user_id=info.context.user_id,
username=username, username=username,
order_by=[models.Event.date_from.desc(), models.Event.name.desc()], order_by=[models.Event.date_from.desc(), models.Event.name.desc()],
only_public=public, only_public=public,
) )
return await get_pagination_window(
query=query,
item_type=Event,
limit=limit,
offset=offset
)
@strawberry.field() @strawberry.field(extensions=[AllowPublicAccess()])
@error_logging
@allow_public
async def event( async def event(
root, info, root, info,
id: Optional[int] = None, id: int | None = None,
url_slug: Optional[str] = None, url_slug: str | None = None,
username: Optional[str] = None, username: str | None = None,
public: Optional[bool] = False public: bool | None = False
) -> Event: ) -> Event:
filter_params = get_detail_filters(id, url_slug) filter_params = get_detail_filters(id, url_slug)
if username: if username:
@@ -61,14 +48,10 @@ class EventQueries:
@strawberry.type @strawberry.type
class EventMutation: class EventMutation:
@strawberry.mutation @strawberry.mutation(extensions=[AuthenticatedOnly()])
@error_logging
@authenticated_user_only()
async def create_event(root, info, input: CreateEventInput) -> Event: async def create_event(root, info, input: CreateEventInput) -> Event:
return await BaseMutationResolver(Event, models.Event).create(info.context, input) return await BaseMutationResolver(Event, models.Event).create(info.context, input)
@strawberry.mutation @strawberry.mutation(extensions=[AuthenticatedOnly()])
@error_logging
@authenticated_user_only()
async def edit_event(root, info, id: int, input: EditEventInput) -> Event: async def edit_event(root, info, id: int, input: EditEventInput) -> Event:
return await BaseMutationResolver(Event, models.Event).update(id, input, info.context.user_id) return await BaseMutationResolver(Event, models.Event).update(info.context, id, input)
+24 -44
View File
@@ -1,59 +1,45 @@
from typing import Optional
import strawberry import strawberry
from graphql import GraphQLError
from decorators.endpoints import authenticated_user_only, allow_public
from decorators.error_logging import error_logging
from graphql_schema.entities.resolvers.flight import FlightMutationResolver, FlightQueryResolver from graphql_schema.entities.resolvers.flight import FlightMutationResolver, FlightQueryResolver
from graphql_schema.entities.types.mutation_input import EditFlightInput, CreateFlightInput from graphql_schema.entities.types.mutation_input import EditFlightInput, CreateFlightInput
from graphql_schema.entities.types.types import Flight from graphql_schema.entities.types.types import Flight
from .helpers.detail import get_detail_filters from .helpers.filters import get_detail_filters
from .helpers.pagination import PaginationWindow, get_pagination_window from .helpers.pagination import PaginationWindow
from ..extensions.field.auth import AllowPublicAccess, AuthenticatedOnly
from ..extensions.field.pagination import OffsetPagination
@strawberry.type @strawberry.type
class FlightQueries: class FlightQueries:
@strawberry.field() @strawberry.field(extensions=[OffsetPagination(item_type=Flight), AllowPublicAccess()])
@error_logging
@allow_public
async def flights( async def flights(
root, info, root, info,
limit: int, username: str | None = None,
offset: int = 0, search: str | None = None,
username: Optional[str] = None, event_id: int | None = None,
event_id: Optional[int] = None, public: bool | None = False,
public: Optional[bool] = False, copilot_id: int | None = None,
copilot_id: Optional[int] = None, point_of_interest_id: int | None = None,
point_of_interest_id: Optional[int] = None, aircraft_id: int | None = None,
aircraft_id: Optional[int] = None,
) -> PaginationWindow[Flight]: ) -> PaginationWindow[Flight]:
query = FlightQueryResolver().get_query( return FlightQueryResolver().get_query(
user_id=info.context.user_id, user_id=info.context.user_id,
username=username, username=username,
event_id=event_id, event_id=event_id,
only_public=public, only_public=public,
copilot_id=copilot_id, copilot_id=copilot_id,
aircraft_id=aircraft_id, aircraft_id=aircraft_id,
point_of_interest_id=point_of_interest_id point_of_interest_id=point_of_interest_id,
search=search
) )
return await get_pagination_window( @strawberry.field(extensions=[AllowPublicAccess()])
query=query,
item_type=Flight,
limit=limit,
offset=offset,
)
@strawberry.field()
@error_logging
@allow_public
async def flight( async def flight(
root, info, root, info,
id: Optional[int] = None, id: int | None = None,
url_slug: Optional[str] = None, url_slug: str | None = None,
username: Optional[str] = None, username: str | None = None,
public: Optional[bool] = False public: bool | None = False
) -> Flight: ) -> Flight:
filter_params = get_detail_filters(id, url_slug) filter_params = get_detail_filters(id, url_slug)
if username: if username:
@@ -68,20 +54,14 @@ class FlightQueries:
@strawberry.type @strawberry.type
class FlightMutation: class FlightMutation:
@strawberry.mutation @strawberry.mutation(extensions=[AuthenticatedOnly()])
@error_logging
@authenticated_user_only()
async def create_flight(self, info, input: CreateFlightInput) -> Flight: async def create_flight(self, info, input: CreateFlightInput) -> Flight:
return await FlightMutationResolver().create(info.context, input) return await FlightMutationResolver().create(info.context, input)
@strawberry.mutation @strawberry.mutation(extensions=[AuthenticatedOnly()])
@error_logging
@authenticated_user_only()
async def edit_flight(self, info, id: int, input: EditFlightInput) -> Flight: async def edit_flight(self, info, id: int, input: EditFlightInput) -> Flight:
return await FlightMutationResolver().update(info.context, id, input) return await FlightMutationResolver().update(info.context, id, input)
@strawberry.mutation @strawberry.mutation(extensions=[AuthenticatedOnly()])
@error_logging
@authenticated_user_only()
async def delete_flight(self, info, id: int) -> Flight: async def delete_flight(self, info, id: int) -> Flight:
return await FlightMutationResolver().delete(info.context.user_id, id) return await FlightMutationResolver().delete(info.context, id)
@@ -0,0 +1,45 @@
import strawberry
from strawberry.types import Info
from graphql_schema.entities.helpers.filters import get_detail_filters
from graphql_schema.entities.helpers.pagination import PaginationWindow
from graphql_schema.entities.resolvers.flight_plan import FlightPlanMutationResolver, FlightPlanQueryResolver
from graphql_schema.entities.types.mutation_input import CreateFlightPlanInput, EditFlightPlanInput
from graphql_schema.entities.types.types import FlightPlan
from graphql_schema.extensions.field.auth import AuthenticatedOnly, AllowPublicAccess
from graphql_schema.extensions.field.pagination import OffsetPagination
@strawberry.type
class FlightPlanQueries:
@strawberry.field(extensions=[OffsetPagination(item_type=FlightPlan), AuthenticatedOnly()])
async def flight_plans(root, info: Info, search: str | None = None) -> PaginationWindow[FlightPlan]:
return FlightPlanQueryResolver().get_query(info.context.user_id, search=search)
@strawberry.field(extensions=[AllowPublicAccess()])
async def flight_plan(
root,
info: Info,
id: int,
username: str | None = None,
public: bool | None = False
) -> FlightPlan:
filter_params = get_detail_filters(id)
if username:
filter_params['username'] = username
return await FlightPlanQueryResolver().get_one(
user_id=info.context.user_id,
only_public=public,
**filter_params
)
@strawberry.type
class FlightPlanMutation:
@strawberry.mutation(extensions=[AuthenticatedOnly()])
async def create_flight_plan(root, info: Info, input: CreateFlightPlanInput) -> FlightPlan:
return await FlightPlanMutationResolver().create(info.context, data=input)
@strawberry.mutation(extensions=[AuthenticatedOnly()])
async def edit_flight_plan(root, info: Info, id: int, input: EditFlightPlanInput) -> FlightPlan:
return await FlightPlanMutationResolver().update(info.context, id, input)
@@ -10,7 +10,7 @@ async def handle_combobox_save(
input: ComboboxInput, input: ComboboxInput,
user_id: int, user_id: int,
name_column: str = "name", name_column: str = "name",
extra_data: Optional[dict] = None extra_data: dict | None = None
) -> int: ) -> int:
if input.id: if input.id:
return input.id return input.id
@@ -1,15 +0,0 @@
from typing import Optional
from graphql import GraphQLError
def get_detail_filters(id: Optional[int], url_slug: Optional[str]) -> dict:
filter_params = {}
if id:
filter_params['object_id'] = id
if url_slug is not None:
filter_params['url_slug'] = url_slug
if not filter_params:
raise GraphQLError("You must specifiy either urlSlug or id!")
return filter_params
@@ -0,0 +1,16 @@
from typing import Any
from graphql import GraphQLError
def get_filters(**kwargs) -> dict[str, Any]:
return {k: v for k, v in kwargs.items() if v is not None}
def get_detail_filters(id: int | None = None, url_slug: str | None = None) -> dict:
filter_params = get_filters(object_id=id, url_slug=url_slug)
if not filter_params:
raise GraphQLError("You must specifiy either urlSlug or id!")
return filter_params
@@ -30,10 +30,6 @@ async def get_pagination_window(
cnt_query = query.with_only_columns(func.count()) cnt_query = query.with_only_columns(func.count())
total_items_count = (await db.scalars(cnt_query)).one() total_items_count = (await db.scalars(cnt_query)).one()
# if offset != 0 and not 0 <= offset < total_items_count:
# raise Exception(f"offset ({offset}) is out of range " f"(0-{total_items_count - 1})")
async with get_session() as db:
data = (await db.scalars(query.limit(limit).offset(offset))).all() data = (await db.scalars(query.limit(limit).offset(offset))).all()
dataset = [item_type(**i.as_dict()) for i in data] dataset = [item_type(**i.as_dict()) for i in data]
+11 -22
View File
@@ -3,27 +3,23 @@ import strawberry
from sqlalchemy import delete from sqlalchemy import delete
from sqlalchemy.dialects.mysql import insert from sqlalchemy.dialects.mysql import insert
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from strawberry import Info
from database import models from database import models
from decorators.endpoints import authenticated_user_only
from database.transaction import get_session from database.transaction import get_session
from decorators.error_logging import error_logging
from graphql_schema.entities.resolvers.base import BaseMutationResolver from graphql_schema.entities.resolvers.base import BaseMutationResolver
from graphql_schema.entities.resolvers.organization import OrganizationQueryResolver from graphql_schema.entities.resolvers.organization import OrganizationQueryResolver
from graphql_schema.entities.types.mutation_input import CreateOrganizationInput, EditOrganizationInput from graphql_schema.entities.types.mutation_input import CreateOrganizationInput, EditOrganizationInput
from graphql_schema.entities.types.types import Organization from graphql_schema.entities.types.types import Organization
from graphql_schema.extensions.field.auth import AuthenticatedOnly
@strawberry.type @strawberry.type
class OrganizationQueries: class OrganizationQueries:
@strawberry.field() @strawberry.field(extensions=[AuthenticatedOnly()])
@error_logging
@authenticated_user_only()
async def organizations(root, info) -> List[Organization]: async def organizations(root, info) -> List[Organization]:
return await OrganizationQueryResolver().get_list() return await OrganizationQueryResolver().get_list()
@strawberry.field() @strawberry.field(extensions=[AuthenticatedOnly()])
@error_logging
@authenticated_user_only()
async def organization(root, info, id: int) -> Organization: async def organization(root, info, id: int) -> Organization:
return await OrganizationQueryResolver().get_one(object_id=id) return await OrganizationQueryResolver().get_one(object_id=id)
@@ -31,17 +27,14 @@ class OrganizationQueries:
@strawberry.type @strawberry.type
class OrganizationMutation: class OrganizationMutation:
@strawberry.mutation @strawberry.mutation(extensions=[AuthenticatedOnly()])
@error_logging async def create_organization(root, info: Info, input: CreateOrganizationInput) -> Organization:
@authenticated_user_only()
async def create_organization(root, info, input: CreateOrganizationInput) -> Organization:
return await BaseMutationResolver(Organization, models.Organization).create(info.context, data=input) return await BaseMutationResolver(Organization, models.Organization).create(info.context, data=input)
@strawberry.mutation @strawberry.mutation(extensions=[AuthenticatedOnly()])
@error_logging async def edit_organization(root, info: Info, id: int, input: EditOrganizationInput) -> Organization:
@authenticated_user_only()
async def edit_organization(root, info, id: int, input: EditOrganizationInput) -> Organization:
return await BaseMutationResolver(Organization, models.Organization).update( return await BaseMutationResolver(Organization, models.Organization).update(
info.context,
id, id,
data=input, data=input,
user_id=info.context.user_id user_id=info.context.user_id
@@ -51,9 +44,7 @@ class OrganizationMutation:
@strawberry.type @strawberry.type
class OrganizationUserMutation: class OrganizationUserMutation:
@strawberry.mutation @strawberry.mutation(extensions=[AuthenticatedOnly()])
@error_logging
@authenticated_user_only()
async def add_to_organization(root, info, organization_id: int) -> Organization: async def add_to_organization(root, info, organization_id: int) -> Organization:
async with get_session() as db: async with get_session() as db:
organization = (await db.scalars( organization = (await db.scalars(
@@ -72,9 +63,7 @@ class OrganizationUserMutation:
return Organization(**organization.as_dict()) return Organization(**organization.as_dict())
@strawberry.mutation @strawberry.mutation(extensions=[AuthenticatedOnly()])
@error_logging
@authenticated_user_only()
async def remove_from_organization(root, info, organization_id: int) -> Organization: async def remove_from_organization(root, info, organization_id: int) -> Organization:
async with get_session() as db: async with get_session() as db:
organization = (await db.scalars( organization = (await db.scalars(
+26 -32
View File
@@ -1,28 +1,31 @@
from typing import List, Optional
import strawberry import strawberry
from fastapi import HTTPException
from starlette.status import HTTP_401_UNAUTHORIZED
from strawberry import Info
from database import models from database import models
from decorators.endpoints import authenticated_user_only, allow_public, raise_unauthorized
from decorators.error_logging import error_logging
from graphql_schema.entities.resolvers.base import BaseQueryResolver from graphql_schema.entities.resolvers.base import BaseQueryResolver
from graphql_schema.entities.resolvers.photo import PhotoMutationResolver, PhotoQueryResolver from graphql_schema.entities.resolvers.photo import PhotoMutationResolver, PhotoQueryResolver
from graphql_schema.entities.types.types import Photo from graphql_schema.entities.types.types import Photo
from graphql_schema.entities.types.mutation_input import EditPhotoInput, UploadPhotoInput, AdjustmentInput from graphql_schema.entities.types.mutation_input import EditPhotoInput, UploadPhotoInput, AdjustmentInput
from graphql_schema.extensions.field.auth import AuthenticatedOnly, AllowPublicAccess
def raise_unauthorized():
raise HTTPException(HTTP_401_UNAUTHORIZED, "Not authorized")
@strawberry.type @strawberry.type
class PhotoQueries: class PhotoQueries:
@strawberry.field() @strawberry.field(extensions=[AllowPublicAccess()])
@error_logging
@allow_public
async def photos( async def photos(
root, info, root, info: Info,
flight_id: Optional[int] = None, flight_id: int | None = None,
copilot_id: Optional[int] = None, copilot_id: int | None = None,
uploaded_by_copilot_id: Optional[int] = None, uploaded_by_copilot_id: int | None = None,
point_of_interest_id: Optional[int] = None, point_of_interest_id: int | None = None,
aircraft_id: Optional[int] = None, aircraft_id: int | None = None,
public: Optional[bool] = False, public: bool | None = False,
) -> List[Photo]: ) -> list[Photo]:
return await PhotoQueryResolver().get_list( return await PhotoQueryResolver().get_list(
public=public, public=public,
flight_id=flight_id, flight_id=flight_id,
@@ -34,10 +37,8 @@ class PhotoQueries:
order_by=[models.Photo.exposed_at] order_by=[models.Photo.exposed_at]
) )
@strawberry.field() @strawberry.field(extensions=[AllowPublicAccess()])
@error_logging async def photo(root, info: Info, id: int, public: bool | None = False) -> Photo:
@allow_public
async def photo(root, info, id: int, public: Optional[bool] = False, ) -> Photo:
return await BaseQueryResolver(Photo, models.Photo).get_one( return await BaseQueryResolver(Photo, models.Photo).get_one(
object_id=id, object_id=id,
user_id=info.context.user_id, user_id=info.context.user_id,
@@ -47,23 +48,19 @@ class PhotoQueries:
@strawberry.type @strawberry.type
class PhotoMutation: class PhotoMutation:
@strawberry.mutation @strawberry.mutation()
@error_logging
async def upload_photo(self, info, input: UploadPhotoInput) -> Photo: async def upload_photo(self, info, input: UploadPhotoInput) -> Photo:
if info.context.user_id is None and not input.copilot_upload_token: if info.context.user_id is None and not input.copilot_upload_token:
raise_unauthorized() raise_unauthorized()
return await PhotoMutationResolver().upload(info, input) return await PhotoMutationResolver().upload(info, input)
@strawberry.mutation() @strawberry.mutation(extensions=[AuthenticatedOnly()])
@error_logging
@authenticated_user_only()
async def edit_photo(self, info, id: int, input: EditPhotoInput) -> Photo: async def edit_photo(self, info, id: int, input: EditPhotoInput) -> Photo:
return await PhotoMutationResolver().update(id, input, info.context.user_id) return await PhotoMutationResolver().update(info.context, id, input, info.context.user_id)
@strawberry.mutation() @strawberry.mutation()
@error_logging async def change_orientation(self, info, id: int, direction: str, copilot_upload_token: str | None = None) -> Photo:
async def change_orientation(self, info, id: int, direction: str, copilot_upload_token: Optional[str] = None) -> Photo:
if info.context.user_id is None and not copilot_upload_token: if info.context.user_id is None and not copilot_upload_token:
raise_unauthorized() raise_unauthorized()
@@ -75,16 +72,13 @@ class PhotoMutation:
info=info info=info
) )
@strawberry.mutation() @strawberry.mutation(extensions=[AuthenticatedOnly()])
@error_logging
@authenticated_user_only()
async def adjust_photo(self, info, id: int, adjustment: AdjustmentInput) -> Photo: async def adjust_photo(self, info, id: int, adjustment: AdjustmentInput) -> Photo:
return await PhotoMutationResolver().adjust(id, info=info, user_id=info.context.user_id, adjustment=adjustment) return await PhotoMutationResolver().adjust(id, info=info, user_id=info.context.user_id, adjustment=adjustment)
@strawberry.mutation() @strawberry.mutation()
@error_logging async def delete_photo(self, info, id: int, copilot_upload_token: str | None = None) -> Photo:
async def delete_photo(self, info, id: int, copilot_upload_token: Optional[str] = None) -> Photo:
if info.context.user_id is None and not copilot_upload_token: if info.context.user_id is None and not copilot_upload_token:
raise_unauthorized() raise_unauthorized()
return await PhotoMutationResolver().delete(user_id=info.context.user_id, id=id) return await PhotoMutationResolver().delete(info.context, id=id)
+14 -31
View File
@@ -1,48 +1,37 @@
from typing import Optional
import strawberry import strawberry
from database import models from database import models
from decorators.endpoints import authenticated_user_only, allow_public
from database.transaction import get_session from database.transaction import get_session
from decorators.error_logging import error_logging
from graphql_schema.entities.helpers.combobox import handle_combobox_save from graphql_schema.entities.helpers.combobox import handle_combobox_save
from graphql_schema.entities.helpers.detail import get_detail_filters from graphql_schema.entities.helpers.filters import get_detail_filters
from graphql_schema.entities.helpers.pagination import get_pagination_window, PaginationWindow from graphql_schema.entities.helpers.pagination import PaginationWindow
from graphql_schema.entities.resolvers.base import BaseQueryResolver, BaseMutationResolver from graphql_schema.entities.resolvers.base import BaseQueryResolver, BaseMutationResolver
from graphql_schema.entities.types.types import PointOfInterest from graphql_schema.entities.types.types import PointOfInterest
from graphql_schema.entities.types.mutation_input import CreatePointOfInterestInput, EditPointOfInterestInput from graphql_schema.entities.types.mutation_input import CreatePointOfInterestInput, EditPointOfInterestInput
from graphql_schema.extensions.field.auth import AuthenticatedOnly, AllowPublicAccess
from graphql_schema.extensions.field.pagination import OffsetPagination
@strawberry.type @strawberry.type
class PointOfInterestQueries: class PointOfInterestQueries:
@strawberry.field() @strawberry.field(extensions=[OffsetPagination(item_type=PointOfInterest), AllowPublicAccess()])
@error_logging
@allow_public
async def points_of_interest( async def points_of_interest(
root, info, root, info,
limit: int, offset: int = 0, search: str | None = None,
search: Optional[str] = None, public: bool = False,
public: bool = False
) -> PaginationWindow[PointOfInterest]: ) -> PaginationWindow[PointOfInterest]:
query = BaseQueryResolver(PointOfInterest, models.PointOfInterest).get_query( return BaseQueryResolver(PointOfInterest, models.PointOfInterest).get_query(
info.context.user_id, info.context.user_id,
only_my=bool(info.context.user_id), only_my=bool(info.context.user_id),
include_others_public=True, include_others_public=True,
only_public=public, only_public=public,
search=search, search=search,
) )
return await get_pagination_window(
query=query,
item_type=PointOfInterest,
limit=limit,
offset=offset
)
@strawberry.field() @strawberry.field(extensions=[AllowPublicAccess()])
@allow_public
async def point_of_interest( async def point_of_interest(
root, info, root, info,
url_slug: Optional[str] = None, url_slug: str | None = None,
id: Optional[int] = None, id: int | None = None,
public: bool = False public: bool = False
) -> PointOfInterest: ) -> PointOfInterest:
filter_params = get_detail_filters(id, url_slug) filter_params = get_detail_filters(id, url_slug)
@@ -56,9 +45,7 @@ class PointOfInterestQueries:
@strawberry.type @strawberry.type
class PointOfInterestMutation: class PointOfInterestMutation:
@strawberry.mutation @strawberry.mutation(extensions=[AuthenticatedOnly()])
@error_logging
@authenticated_user_only()
async def create_point_of_interest(root, info, input: CreatePointOfInterestInput) -> PointOfInterest: async def create_point_of_interest(root, info, input: CreatePointOfInterestInput) -> PointOfInterest:
input_data = input.to_dict() input_data = input.to_dict()
@@ -73,9 +60,7 @@ class PointOfInterestMutation:
db, input_data db, input_data
) )
@strawberry.mutation @strawberry.mutation(extensions=[AuthenticatedOnly()])
@error_logging
@authenticated_user_only()
async def edit_point_of_interest(root, info, id: int, input: EditPointOfInterestInput) -> PointOfInterest: async def edit_point_of_interest(root, info, id: int, input: EditPointOfInterestInput) -> PointOfInterest:
input_data = input.to_dict() input_data = input.to_dict()
@@ -93,8 +78,6 @@ class PointOfInterestMutation:
updated_poi = await models.PointOfInterest.update(db, obj=poi, data=input_data) updated_poi = await models.PointOfInterest.update(db, obj=poi, data=input_data)
return PointOfInterest(**updated_poi.as_dict()) return PointOfInterest(**updated_poi.as_dict())
@strawberry.mutation @strawberry.mutation(extensions=[AuthenticatedOnly()])
@error_logging
@authenticated_user_only()
async def delete_point_of_interest(self, info, id: int) -> PointOfInterest: async def delete_point_of_interest(self, info, id: int) -> PointOfInterest:
return await BaseMutationResolver(PointOfInterest, models.PointOfInterest).delete(info.context.user_id, id=id) return await BaseMutationResolver(PointOfInterest, models.PointOfInterest).delete(info.context.user_id, id=id)
+4 -76
View File
@@ -1,92 +1,20 @@
from typing import List
import strawberry import strawberry
from database import models from database import models
from decorators.endpoints import authenticated_user_only
from decorators.error_logging import error_logging
from graphql_schema.entities.resolvers.base import BaseQueryResolver from graphql_schema.entities.resolvers.base import BaseQueryResolver
from graphql_schema.entities.types.types import PointOfInterestType from graphql_schema.entities.types.types import PointOfInterestType
from graphql_schema.extensions.field.auth import AuthenticatedOnly
@strawberry.type @strawberry.type
class PointOfInterestTypeQueries: class PointOfInterestTypeQueries:
@strawberry.field() @strawberry.field(extensions=[AuthenticatedOnly()])
@error_logging async def point_of_interest_types(root, info) -> list[PointOfInterestType]:
@authenticated_user_only()
async def point_of_interest_types(root, info) -> List[PointOfInterestType]:
return await BaseQueryResolver(PointOfInterestType, models.PointOfInterestType).get_list(info.context.user_id) return await BaseQueryResolver(PointOfInterestType, models.PointOfInterestType).get_list(info.context.user_id)
@strawberry.field() @strawberry.field(extensions=[AuthenticatedOnly()])
@error_logging
@authenticated_user_only()
async def point_of_interest_type(root, info, id: int) -> PointOfInterestType: async def point_of_interest_type(root, info, id: int) -> PointOfInterestType:
return await BaseQueryResolver(PointOfInterestType, models.PointOfInterestType).get_one( return await BaseQueryResolver(PointOfInterestType, models.PointOfInterestType).get_one(
object_id=id, object_id=id,
user_id=info.context.user_id user_id=info.context.user_id
) )
#
# @strawberry.type
# class CreatePointOfInterestMutation:
# @strawberry_sqlalchemy_input(models.PointOfInterest, exclude_fields=['id', 'type_id'])
# class CreatePointOfInterestInput:
# type: # Optional[ComboboxInput] = None
#
# @strawberry.mutation
# @authenticated_user_only()
# async def create_point_of_interest(root, info, input: CreatePointOfInterestInput) -> PointOfInterest:
# input_data = input.to_dict()
#
# input_data['type_id'] = await handle_combobox_save(
# info.context.db,
# models.PointOfInterestType,
# input.type,
# info.context.user_id
# )
#
# return await models.PointOfInterest.create(
# info.context.db,
# data=dict(
# **input_data,
# created_by_id=info.context.user_id,
# )
# )
#
#
# @strawberry.type
# class EditPointOfInterestMutation:
# @strawberry_sqlalchemy_input(models.PointOfInterest, exclude_fields=['id', 'type_id'])
# class EditPointOfInterestInput:
# type: Optional[ComboboxInput] = None
#
# @strawberry.mutation
# @authenticated_user_only()
# async def edit_point_of_interest(root, info, id: int, input: EditPointOfInterestInput) -> PointOfInterest:
# # TODO: kontrola organizace
# input_data = input.to_dict()
#
# if 'type' in input:
# input_data['type_id'] = await handle_combobox_save(
# info.context.db,
# models.PointOfInterestType,
# input.type,
# info.context.user_id
# )
#
# poi = (
# await info.context.db.scalars(
# get_base_query(info.context.user_id, only_my=True)
# .filter(models.PointOfInterest.id == id))
# ).one()
# return await models.PointOfInterest.update(info.context.db, obj=poi, data=input_data)
#
#
# @strawberry.type
# class DeletePointOfInterestMutation:
#
# @strawberry.mutation
# @authenticated_user_only()
# async def delete_point_of_interest(self, info, id: int) -> PointOfInterest:
# poi = get_base_query(info.context.user_id, only_my=True).filter(models.PointOfInterest.id == id).one()
#
# return await models.PointOfInterest.update(info.context.db, obj=poi, data=dict(deleted=True))
@@ -1,10 +1,7 @@
from operator import or_ from sqlalchemy import and_, or_
from typing import Set, Optional
from sqlalchemy import and_
from database import models from database import models
from database.transaction import get_session from database.transaction import get_session
from graphql_schema.context import GraphQLContext
from graphql_schema.entities.helpers.combobox import handle_combobox_save from graphql_schema.entities.helpers.combobox import handle_combobox_save
from graphql_schema.entities.resolvers.base import BaseMutationResolver, BaseQueryResolver from graphql_schema.entities.resolvers.base import BaseMutationResolver, BaseQueryResolver
from graphql_schema.entities.types.mutation_input import EditAircraftInput, CreateAircraftInput from graphql_schema.entities.types.mutation_input import EditAircraftInput, CreateAircraftInput
@@ -17,29 +14,36 @@ class AircraftQueryResolver(BaseQueryResolver):
def get_query( def get_query(
self, self,
user_id: Optional[int] = None, user_id: int | None = None,
object_id: Optional[int] = None, object_id: int | None = None,
order_by: Optional[list] = None, order_by: list | None = None,
organization_ids: Optional[Set[int]] = None, organization_ids: set[int] | None = None,
*args, *args,
**kwargs, **kwargs,
): ):
filters = [] filters = []
search = {}
if kwargs.get("call_sign"): if kwargs.get("call_sign"):
filters.append(models.Aircraft.call_sign == kwargs['call_sign']) filters.append(models.Aircraft.call_sign == kwargs['call_sign'])
if kwargs.get("search"):
search['search'] = kwargs.pop("search", None)
query = super().get_query( query = super().get_query(
object_id=object_id, object_id=object_id,
only_my=False, only_my=False,
only_public=kwargs.get("only_public", False), only_public=kwargs.get("only_public", False),
order_by=order_by, order_by=order_by,
filters=filters, filters=filters,
**search
) )
if kwargs.get("search"):
search = kwargs["search"]
query = query.filter(
or_(
models.Aircraft.call_sign.contains(search),
models.Aircraft.manufacturer.contains(search),
models.Aircraft.model.contains(search),
)
)
if organization_ids: if organization_ids:
query = ( query = (
query.filter( query.filter(
@@ -62,7 +66,7 @@ class AircraftMutationResolver(BaseMutationResolver):
def __init__(self): def __init__(self):
super().__init__(graphql_type=Aircraft, model=models.Aircraft) super().__init__(graphql_type=Aircraft, model=models.Aircraft)
async def create(self, context, data: CreateAircraftInput) -> Aircraft: async def create(self, context: GraphQLContext, data: CreateAircraftInput) -> Aircraft:
input_data = data.to_dict() input_data = data.to_dict()
async with get_session() as db: async with get_session() as db:
@@ -77,7 +81,7 @@ class AircraftMutationResolver(BaseMutationResolver):
return await self._do_create(db, data=input_data) return await self._do_create(db, data=input_data)
async def update(self, id: int, user_id: int, data: EditAircraftInput) -> Aircraft: async def update(self, context: GraphQLContext, id: int, data: EditAircraftInput) -> Aircraft:
update_data = data.to_dict() update_data = data.to_dict()
async with get_session() as db: async with get_session() as db:
if data.organization: if data.organization:
@@ -85,7 +89,7 @@ class AircraftMutationResolver(BaseMutationResolver):
db, db,
models.Organization, models.Organization,
input=data.organization, input=data.organization,
user_id=user_id, user_id=context.user_id,
) )
return await self._do_update(db, id, update_data) return await self._do_update(db, id, update_data)
+20 -19
View File
@@ -1,12 +1,13 @@
from typing import Optional, Type, TypeVar, Generic, List from typing import Type, TypeVar, Generic
from sqlalchemy import or_ from sqlalchemy import or_
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from database import models from database import models
from database.query_builder import QueryBuilder from database.query_builder import QueryBuilder
from database.transaction import get_session from database.transaction import get_session
from graphql_schema.context import GraphQLContext
from graphql_schema.entities.types.base import BaseGraphqlInputType from graphql_schema.entities.types.base import BaseGraphqlInputType
GQL_TYPE = TypeVar('GQL_TYPE') GQL_TYPE = TypeVar('GQL_TYPE')
@@ -18,7 +19,7 @@ class BaseResolver(Generic[GQL_TYPE]):
class BaseQueryResolver(BaseResolver): class BaseQueryResolver(BaseResolver):
async def _get_list(self, query) -> List[GQL_TYPE]: async def _get_list(self, query) -> list[GQL_TYPE]:
async with get_session() as db: async with get_session() as db:
items = (await db.scalars(query)).all() items = (await db.scalars(query)).all()
@@ -31,14 +32,14 @@ class BaseQueryResolver(BaseResolver):
def get_query( def get_query(
self, self,
user_id: Optional[int] = None, user_id: int | None = None,
object_id: Optional[int] = None, object_id: int | None = None,
order_by: Optional[list] = None, order_by: list | None = None,
only_public: Optional[bool] = False, only_public: bool | None = False,
only_my: Optional[bool] = False, only_my: bool | None = False,
include_others_public: Optional[bool] = False, include_others_public: bool | None = False,
url_slug: Optional[str] = None, url_slug: str | None = None,
filters: Optional[list] = None, filters: list | None = None,
**kwargs, **kwargs,
): ):
query = self.query_builder.get_simple_query( query = self.query_builder.get_simple_query(
@@ -77,17 +78,17 @@ class BaseQueryResolver(BaseResolver):
query = query.filter(or_(*search_clauses)) query = query.filter(or_(*search_clauses))
return query return query
async def get_list(self, user_id: Optional[int] = None, **kwargs) -> List[GQL_TYPE]: async def get_list(self, user_id: int | None = None, **kwargs) -> list[GQL_TYPE]:
query = self.get_query(user_id=user_id, **kwargs) query = self.get_query(user_id=user_id, **kwargs)
return await self._get_list(query) return await self._get_list(query)
async def get_one(self, user_id: Optional[int] = None, **kwargs) -> GQL_TYPE: async def get_one(self, user_id: int | None = None, **kwargs) -> GQL_TYPE:
query = self.get_query(user_id=user_id, **kwargs) query = self.get_query(user_id=user_id, **kwargs)
return await self._get_one(query) return await self._get_one(query)
class BaseMutationResolver(BaseResolver): class BaseMutationResolver(BaseResolver):
async def _get_one(self, db: AsyncSession, id: int, created_by_id: int) -> models.BaseModel: async def _get_one(self, db: AsyncSession, id: int, created_by_id: int):
query = self.query_builder.get_simple_query(created_by_id=created_by_id).filter(self.model.id == id) query = self.query_builder.get_simple_query(created_by_id=created_by_id).filter(self.model.id == id)
return (await db.scalars(query)).one() return (await db.scalars(query)).one()
@@ -107,7 +108,7 @@ class BaseMutationResolver(BaseResolver):
model = await self.model.update(db, data=data, **update_where) model = await self.model.update(db, data=data, **update_where)
return self.graphql_type(**model.as_dict()) return self.graphql_type(**model.as_dict())
async def create(self, context, data: BaseGraphqlInputType) -> GQL_TYPE: async def create(self, context: GraphQLContext, data: BaseGraphqlInputType) -> GQL_TYPE:
input_data = data.to_dict() input_data = data.to_dict()
if hasattr(self.model, "created_by_id"): if hasattr(self.model, "created_by_id"):
@@ -116,14 +117,14 @@ class BaseMutationResolver(BaseResolver):
async with get_session() as db: async with get_session() as db:
return await self._do_create(db, input_data) return await self._do_create(db, input_data)
async def update(self, id: int, data: BaseGraphqlInputType, user_id: int) -> GQL_TYPE: async def update(self, context: GraphQLContext, id: int, data: BaseGraphqlInputType) -> GQL_TYPE:
async with get_session() as db: async with get_session() as db:
item = await self._get_one(db, id, user_id) item = await self._get_one(db, id, context.user_id)
return await self._do_update(db, item, data.to_dict()) return await self._do_update(db, item, data.to_dict())
async def delete(self, user_id: int, id: int, **kwargs) -> GQL_TYPE: async def delete(self, context: GraphQLContext, id: int, **kwargs) -> GQL_TYPE:
async with get_session() as db: async with get_session() as db:
model = await self._get_one(db, id, user_id) model = await self._get_one(db, id, context.user_id)
if hasattr(self.model, "deleted"): if hasattr(self.model, "deleted"):
model = await self.model.update(db, obj=model, data=dict(deleted=True)) model = await self.model.update(db, obj=model, data=dict(deleted=True))
@@ -1,7 +1,4 @@
from typing import Optional
from sqlalchemy import and_ from sqlalchemy import and_
from database import models from database import models
from graphql_schema.entities.resolvers.base import BaseQueryResolver from graphql_schema.entities.resolvers.base import BaseQueryResolver
from graphql_schema.entities.types.types import Copilot from graphql_schema.entities.types.types import Copilot
@@ -13,10 +10,10 @@ class CopilotQueryResolver(BaseQueryResolver):
def get_query( def get_query(
self, self,
user_id: Optional[int] = None, user_id: int | None = None,
object_id: Optional[int] = None, object_id: int | None = None,
order_by: Optional[list] = None, order_by: list | None = None,
only_public: Optional[bool] = False, only_public: bool | None = False,
**kwargs **kwargs
): ):
pilot_username = kwargs.pop("pilot_username", None) pilot_username = kwargs.pop("pilot_username", None)
@@ -10,10 +10,10 @@ class EventQueryResolver(BaseQueryResolver):
def get_query( def get_query(
self, self,
user_id: Optional[int] = None, user_id: int | None = None,
object_id: Optional[int] = None, object_id: int | None = None,
order_by: Optional[list] = None, order_by: list | None = None,
only_public: Optional[bool] = True, only_public: bool | None = True,
*args, *args,
**kwargs, **kwargs,
): ):
+83 -107
View File
@@ -1,14 +1,16 @@
import asyncio import asyncio
from typing import List, Optional import random
from sqlalchemy import delete, insert, select, func, text from typing import Optional
from sqlalchemy import delete, insert, or_, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from strawberry.file_uploads import Upload
from background_jobs.elevation import add_terrain_elevation_to_flight from background_jobs.elevation import add_terrain_elevation_to_flight
from background_jobs.weather import download_weather from background_jobs.flight_title_photo import add_circular_avatar, generate_flight_title_photo
from graphql_schema.context import GraphQLContext
from utils.flight_track_helpers import handle_upload_gpx, save_track_from_gpx_to_db, extract_basic_flight_info_from_gpx
from background_jobs.weather import download_weather_for_flight
from database import models from database import models
from database.models import flight_has_copilot from database.models import flight_has_copilot
from database.transaction import get_session 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.helpers.combobox import handle_combobox_save
from graphql_schema.entities.resolvers.base import BaseMutationResolver, BaseQueryResolver from graphql_schema.entities.resolvers.base import BaseMutationResolver, BaseQueryResolver
from graphql_schema.entities.types.mutation_input import ( from graphql_schema.entities.types.mutation_input import (
@@ -16,9 +18,8 @@ from graphql_schema.entities.types.mutation_input import (
) )
from graphql_schema.entities.types.types import Flight from graphql_schema.entities.types.types import Flight
from paths import FLIGHT_GPX_TRACK_PATH from paths import FLIGHT_GPX_TRACK_PATH
from utils.file import delete_file from utils.file import handle_file_upload
from utils.str_utils import random_str from utils.str_utils import random_str
from utils.upload import handle_file_upload
class FlightQueryResolver(BaseQueryResolver): class FlightQueryResolver(BaseQueryResolver):
@@ -27,18 +28,12 @@ class FlightQueryResolver(BaseQueryResolver):
def get_query( def get_query(
self, self,
user_id: Optional[int] = None, user_id: int | None = None,
object_id: Optional[int] = None, only_public: bool | None = False,
url_slug: Optional[str] = None,
only_public: Optional[bool] = False,
*args, *args,
**kwargs **kwargs
): ):
filters = {} filters = {key: kwargs[key] for key in ("object_id", "url_slug") if key in kwargs}
if object_id:
filters['object_id'] = object_id
if url_slug:
filters['url_slug'] = url_slug
query = super().get_query( query = super().get_query(
user_id, user_id,
@@ -62,8 +57,27 @@ class FlightQueryResolver(BaseQueryResolver):
if kwargs.get("point_of_interest_id"): if kwargs.get("point_of_interest_id"):
query = ( query = (
query.join(models.Flight.track) query
.filter(models.FlightTrack.point_of_interest_id == kwargs["point_of_interest_id"]) .filter(
or_(
models.Flight.id.in_(
select(models.FlightTurnPoint.flight_id)
.filter(models.FlightTurnPoint.point_of_interest_id == kwargs["point_of_interest_id"])
),
models.Flight.id.in_(
select(models.Photo.flight_id)
.filter(models.Photo.point_of_interest_id == kwargs["point_of_interest_id"])
)
)
)
)
if kwargs.get("search"):
search = kwargs["search"]
query = (
query.filter(
models.Flight.name.contains(search)
)
) )
if kwargs.get('username'): if kwargs.get('username'):
@@ -79,55 +93,13 @@ class FlightMutationResolver(BaseMutationResolver):
def __init__(self): def __init__(self):
super().__init__(Flight, models.Flight) super().__init__(Flight, models.Flight)
async def get_airport_id_by_gps(self, gps_lat: float, gps_lng: float) -> Optional[int]: async def create(self, context: GraphQLContext, input: CreateFlightInput) -> Flight:
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() data = input.to_dict()
user_id = context.user_id user_id = context.user_id
if input.gpx_track_file: if input.gpx_track_file:
data['gpx_track_filename'] = await handle_upload_gpx(gpx_track=input.gpx_track_file, context=context) data['gpx_track_filename'] = await handle_file_upload(input.gpx_track_file, FLIGHT_GPX_TRACK_PATH)
data_from_gpx = await self.extract_data_from_gpx(data['gpx_track_filename']) data_from_gpx = await extract_basic_flight_info_from_gpx(data['gpx_track_filename'])
data.update(data_from_gpx) data.update(data_from_gpx)
else: else:
async with get_session() as db: async with get_session() as db:
@@ -148,26 +120,23 @@ class FlightMutationResolver(BaseMutationResolver):
data.update({ data.update({
"aircraft_id": aircraft_id, "aircraft_id": aircraft_id,
"has_terrain_elevation": False, "has_terrain_elevation": False,
"name": "", "name": input.name or '',
"description": "", "description": "",
"created_by_id": context.user_id "created_by_id": context.user_id
}) })
flight = await self._do_create(db, data) flight = await self._do_create(db, data)
context.background_tasks.add_task( if input.track is not None:
download_weather, await handle_track_edit(db=db, flight_id=flight.id, track=input.track, user_id=user_id)
flight_id=flight.id, airport_id=flight.takeoff_airport_id, date_time=flight.takeoff_datetime,
type_="takeoff" if 'gpx_track_filename' in data:
) await save_track_from_gpx_to_db(gpx_filename=data['gpx_track_filename'], flight_id=flight.id)
context.background_tasks.add_task(
download_weather, schedule_background_tasks(flight.id, data, context)
flight_id=flight.id, airport_id=flight.landing_airport_id, date_time=flight.landing_datetime,
type_="landing"
)
return flight return flight
async def update(self, context, id: int, input: EditFlightInput) -> Flight: async def update(self, context: GraphQLContext, id: int, input: EditFlightInput) -> Flight:
user_id = context.user_id user_id = context.user_id
async with get_session() as db: async with get_session() as db:
flight = await self._get_one(db, id, user_id) flight = await self._get_one(db, id, user_id)
@@ -177,40 +146,23 @@ class FlightMutationResolver(BaseMutationResolver):
data = input.to_dict() data = input.to_dict()
if input.gpx_track_file is not None: if input.gpx_track_file is not None:
data['gpx_track_filename'] = await handle_upload_gpx( data['gpx_track_filename'] = await handle_upload_gpx(gpx_track=input.gpx_track_file, flight_id=flight_id)
gpx_track=input.gpx_track_file,
context=context,
original_gpx_filename=flight_data['gpx_track_filename']
)
async with get_session() as db: async with get_session() as db:
if input.takeoff_airport: if input.takeoff_airport:
takeoff_airport_id = await handle_combobox_save( data['takeoff_airport_id'] = await handle_combobox_save(
db, models.Airport, input.takeoff_airport, user_id, name_column="icao_code", db, models.Airport, input.takeoff_airport, user_id, name_column="icao_code",
extra_data={"name": input.takeoff_airport.name} 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'] 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: if input.landing_airport:
landing_airport_id = await handle_combobox_save( data['landing_airport_id'] = await handle_combobox_save(
db, models.Airport, input.landing_airport, user_id, name_column="icao_code", db, models.Airport, input.landing_airport, user_id, name_column="icao_code",
extra_data={"name": input.landing_airport.name} 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'] 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: if input.aircraft is not None:
data['aircraft_id'] = await handle_aircraft_save(db, user_id, input.aircraft) data['aircraft_id'] = await handle_aircraft_save(db, user_id, input.aircraft)
@@ -239,21 +191,36 @@ class FlightMutationResolver(BaseMutationResolver):
token=random_str(64) token=random_str(64)
)) ))
return await self._do_update(db, flight_data, data) flight_model = await self._do_update(db, flight_data, data)
schedule_background_tasks(flight_id, data, context)
return flight_model
async def handle_upload_gpx(gpx_track: Upload, context, original_gpx_filename: Optional[str] = None): def schedule_background_tasks(flight_id: int, flight_data: dict, context) -> None:
if original_gpx_filename: context.background_tasks.add_task(add_terrain_elevation_to_flight, flight_id=flight_id)
delete_file(FLIGHT_GPX_TRACK_PATH + "/" + original_gpx_filename, silent=True)
filename = await handle_file_upload(gpx_track, FLIGHT_GPX_TRACK_PATH) if flight_data.get("title_photo_id"):
context.background_tasks.add_task(add_terrain_elevation_to_flight, flight_id=id, gpx_filename=filename) context.background_tasks.add_task(
generate_flight_title_photo, flight_id=flight_id, photo_id=flight_data['title_photo_id']
)
return filename if flight_data.get("takeoff_airport_id"):
context.background_tasks.add_task(
download_weather_for_flight, flight_id=flight_id, airport_id=flight_data['takeoff_airport_id'],
date_time=flight_data['takeoff_datetime'], type_="takeoff"
)
if flight_data.get("landing_airport_id"):
context.background_tasks.add_task(
download_weather_for_flight, flight_id=flight_id, airport_id=flight_data['landing_airport_id'],
date_time=flight_data['landing_datetime'], type_="landing"
)
async def handle_track_edit(db: AsyncSession, flight_id: int, track: List[TrackItemInput], user_id: int): 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)) await db.execute(delete(models.FlightTurnPoint).filter(models.FlightTurnPoint.flight_id == flight_id))
order = 0 order = 0
for item in track: for item in track:
@@ -262,17 +229,26 @@ async def handle_track_edit(db: AsyncSession, flight_id: int, track: List[TrackI
if item.point_of_interest: if item.point_of_interest:
poi_id = await handle_combobox_save( poi_id = await handle_combobox_save(
db, models.PointOfInterest, item.point_of_interest, user_id, extra_data={"description": ""} db, models.PointOfInterest, item.point_of_interest, user_id, extra_data={
"description": "",
'gps_latitude': item.gps_latitude,
'gps_longitude': item.gps_longitude
}
) )
if item.airport: if item.airport:
airport_id = await handle_combobox_save( airport_id = await handle_combobox_save(
db, models.Airport, item.airport, user_id, db, models.Airport, item.airport, user_id,
name_column="icao_code", name_column="icao_code",
extra_data={"name": item.airport.name} extra_data={
"name": item.airport.name,
'gps_latitude': item.gps_latitude,
'gps_longitude': item.gps_longitude,
'openaip_id': f'custom-{random.randint(0, 999999)}'
}
) )
await models.FlightTrack.create( await models.FlightTurnPoint.create(
db, db,
data={ data={
"flight_id": flight_id, "flight_id": flight_id,
@@ -0,0 +1,163 @@
import asyncio
from sqlalchemy import delete, select
from sqlalchemy.dialects.mysql import insert
from database import models
from database.models import flight_plan_has_copilot
from database.transaction import get_session
from graphql_schema.context import GraphQLContext
from graphql_schema.entities.helpers.combobox import handle_combobox_save
from graphql_schema.entities.resolvers.base import BaseMutationResolver, BaseQueryResolver
from graphql_schema.entities.resolvers.flight import handle_aircraft_save
from graphql_schema.entities.types.mutation_input import CreateFlightPlanInput, EditFlightPlanInput
from graphql_schema.entities.types.types import FlightPlan
from utils.flight_plan_helpers import refresh_markers_weather_info
from utils.list_utils import evenly_spaced_elements
from utils.str_utils import random_str
class FlightPlanQueryResolver(BaseQueryResolver):
def __init__(self):
super().__init__(graphql_type=FlightPlan, model=models.FlightPlan)
def get_query(
self,
user_id: int | None = None,
only_public: bool | None = False,
object_id: int | None = None,
*args,
**kwargs
):
filters = {"object_id": object_id} if object_id else {}
query = super().get_query(
user_id,
**filters,
order_by=[models.FlightPlan.planned_takeoff_datetime.desc(), models.FlightPlan.id.desc()],
only_public=only_public,
only_my=not only_public
)
if kwargs.get('username'):
query = (
query.join(models.FlightPlan.created_by)
.filter(models.User.public_username == kwargs['username'])
)
if kwargs.get("search"):
search = kwargs["search"]
query = (
query.filter(models.FlightPlan.name.contains(search))
)
return query
class FlightPlanMutationResolver(BaseMutationResolver):
def __init__(self):
super().__init__(graphql_type=FlightPlan, model=models.FlightPlan)
async def create(self, context: GraphQLContext, data: CreateFlightPlanInput) -> FlightPlan:
input_data = data.to_dict()
input_data['created_by_id'] = context.user_id
async with get_session() as db:
flight_plan = await self._do_create(db, data=input_data)
markers = await self.save_markers(db, flight_plan, data.markers)
await refresh_markers_weather_info(
planned_takeoff_datetime=flight_plan.planned_takeoff_datetime,
planned_speed=flight_plan.planned_speed,
markers=markers,
background_tasks=context.background_tasks
)
return flight_plan
async def update(self, context: GraphQLContext, id: int, data: EditFlightPlanInput) -> FlightPlan:
input_data = data.to_dict()
user_id = context.user_id
async with get_session() as db:
flight_plan_model = await self._get_one(db, id=id, created_by_id=user_id)
if data.aircraft is not None:
input_data['aircraft_id'] = await handle_aircraft_save(db, user_id, data.aircraft)
markers = []
if data.markers is not None:
await self.reset_plan_markers(db, flight_plan_model)
markers = await self.save_markers(db, flight_plan_model, data.markers)
await refresh_markers_weather_info(
planned_takeoff_datetime=flight_plan_model.planned_takeoff_datetime,
planned_speed=flight_plan_model.planned_speed,
markers=markers,
background_tasks=context.background_tasks
)
if flight_plan_model.is_default_name:
if not markers:
markers = (await db.scalars(
select(models.FlightPlanMarker)
.filter(models.FlightPlanMarker.flight_plan_id == id)
)).all()
used_markers = evenly_spaced_elements(markers, 5)
input_data['name'] = " - ".join(m.name for m in used_markers)
# TODO: pokud se zmenila rychlost nebo datum/cas letu, je potreba stahnout nove pocasi!
if data.copilots is not None:
await self.save_copilots(
db,
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)
return flight_plan
@staticmethod
async def save_markers(db, flight_plan: models.FlightPlan, markers: list):
position = 0
marker_objects = []
for marker in markers:
if marker.type == 'poi':
assert bool(marker.point_of_interest_id)
if marker.type == 'airport':
assert bool(marker.airport_id)
marker_obj = await models.FlightPlanMarker.create(db, data={
"position": position,
"flight_plan_id": flight_plan.id,
"airport_id": marker.airport_id,
"point_of_interest_id": marker.point_of_interest_id,
"type": marker.type,
"name": marker.name,
"gps_latitude": marker.gps_latitude,
"gps_longitude": marker.gps_longitude
})
marker_objects.append(marker_obj)
position += 1
return marker_objects
@staticmethod
async def reset_plan_markers(db, flight_plan: models.FlightPlan):
await db.execute(
delete(models.FlightPlanMarker)
.filter(models.FlightPlanMarker.flight_plan_id == flight_plan.id)
)
async def save_copilots(self, db, flight_plan_id: int, copilots: list, user_id: int):
await db.execute(delete(flight_plan_has_copilot).filter_by(flight_plan_id=flight_plan_id))
copilots = await asyncio.gather(*[
handle_combobox_save(db, models.Copilot, copilot, user_id) for copilot in copilots
])
for copilot_id in copilots:
await db.execute(insert(flight_plan_has_copilot).values(
flight_plan_id=flight_plan_id,
copilot_id=copilot_id,
token=random_str(64)
))
@@ -12,8 +12,8 @@ class OrganizationQueryResolver(BaseQueryResolver):
def get_query( def get_query(
self, self,
object_id: Optional[int] = None, object_id: int | None = None,
order_by: Optional[list] = None, order_by: list | None = None,
**kwargs **kwargs
): ):
query = ( query = (
+30 -19
View File
@@ -1,14 +1,17 @@
import os import os
import shutil import shutil
from time import time from time import time
from typing import Optional import sqlalchemy
from PIL import Image from PIL import Image
from graphql import GraphQLError
from pydantic import BaseModel from pydantic import BaseModel
from pymysql.err import IntegrityError
from sqlalchemy import delete, insert, select from sqlalchemy import delete, insert, select
from background_jobs.elevation import add_terrain_elevation_to_photo from background_jobs.elevation import add_terrain_elevation_to_photo
from background_jobs.photo import generate_thumbnail, resize_photo from background_jobs.photo import generate_thumbnail, resize_photo
from database import models from database import models
from database.transaction import get_session from database.transaction import get_session
from graphql_schema.context import GraphQLContext
from graphql_schema.entities.helpers.combobox import handle_combobox_save from graphql_schema.entities.helpers.combobox import handle_combobox_save
from graphql_schema.entities.resolvers.base import BaseMutationResolver, BaseQueryResolver from graphql_schema.entities.resolvers.base import BaseMutationResolver, BaseQueryResolver
from graphql_schema.entities.types.mutation_input import EditPhotoInput, UploadPhotoInput, AdjustmentInput from graphql_schema.entities.types.mutation_input import EditPhotoInput, UploadPhotoInput, AdjustmentInput
@@ -16,7 +19,7 @@ from graphql_schema.entities.types.types import Photo
from paths import get_photo_basepath from paths import get_photo_basepath
from utils.file import delete_file from utils.file import delete_file
from utils.image import PhotoEditor, parse_exif_info from utils.image import PhotoEditor, parse_exif_info
from utils.upload import handle_file_upload from utils.file import handle_file_upload
class PhotoQueryResolver(BaseQueryResolver): class PhotoQueryResolver(BaseQueryResolver):
@@ -26,10 +29,10 @@ class PhotoQueryResolver(BaseQueryResolver):
def get_query( def get_query(
self, self,
user_id: Optional[int] = None, user_id: int | None = None,
object_id: Optional[int] = None, object_id: int | None = None,
order_by: Optional[list] = None, order_by: list | None = None,
only_public: Optional[bool] = False, only_public: bool | None = False,
*args, **kwargs *args, **kwargs
): ):
query = super().get_query( query = super().get_query(
@@ -61,7 +64,7 @@ class PhotoQueryResolver(BaseQueryResolver):
class PhotoDetailInfo(BaseModel): class PhotoDetailInfo(BaseModel):
filename: str filename: str
original_filename: str original_filename: str | None
path: str path: str
flight_id: int flight_id: int
@@ -71,14 +74,14 @@ class PhotoMutationResolver(BaseMutationResolver):
super().__init__(Photo, models.Photo) super().__init__(Photo, models.Photo)
@staticmethod @staticmethod
def _copy_original(path: str, filename: str): def _copy_original(path: str, filename: str) -> str:
original_filename = "_original_" + filename original_filename = "_original_" + filename
if not os.path.isfile(path + "/" + original_filename): if not os.path.isfile(path + "/" + original_filename):
shutil.copyfile(path + "/" + filename, path + "/" + original_filename) shutil.copyfile(path + "/" + filename, path + "/" + original_filename)
return original_filename return original_filename
async def _get_photo_details(self, id: int, user_id: int, copilot_token: Optional[str]): async def _get_photo_details(self, id: int, user_id: int, copilot_token: str | None, copy_original: bool = True):
async with get_session() as db: async with get_session() as db:
if not copilot_token: if not copilot_token:
photo = await self._get_one(db, id, created_by_id=user_id) photo = await self._get_one(db, id, created_by_id=user_id)
@@ -89,15 +92,20 @@ class PhotoMutationResolver(BaseMutationResolver):
filename = photo.filename + "." + photo.filename_extension filename = photo.filename + "." + photo.filename_extension
path = get_photo_basepath(flight_id) path = get_photo_basepath(flight_id)
original_filename = None
if copy_original:
original_filename = self._copy_original(path, filename)
return PhotoDetailInfo( return PhotoDetailInfo(
flight_id=flight_id, flight_id=flight_id,
path=path, path=path,
filename=filename, filename=filename,
original_filename=self._copy_original(path, filename) # TODO: tohle nedelat v getteru! original_filename=original_filename
) )
@staticmethod @staticmethod
async def _get_photo_info_by_copilot_token(db, photo_id: int, copilot_token: Optional[str]): async def _get_photo_info_by_copilot_token(db, photo_id: int, copilot_token: str | None):
if not copilot_token: if not copilot_token:
return return
@@ -165,17 +173,17 @@ class PhotoMutationResolver(BaseMutationResolver):
return photo return photo
async def update(self, id: int, input: EditPhotoInput, user_id: int) -> Photo: async def update(self, context: GraphQLContext, id: int, input: EditPhotoInput) -> Photo:
data = input.to_dict() data = input.to_dict()
async with get_session() as db: async with get_session() as db:
photo = await self._get_one(db, id, created_by_id=user_id) photo = await self._get_one(db, id, created_by_id=context.user_id)
if input.point_of_interest: if input.point_of_interest:
data['point_of_interest_id'] = await handle_combobox_save( data['point_of_interest_id'] = await handle_combobox_save(
db, db,
models.PointOfInterest, models.PointOfInterest,
input.point_of_interest, input.point_of_interest,
user_id, context.user_id,
extra_data={"description": ""} extra_data={"description": ""}
) )
@@ -187,7 +195,7 @@ class PhotoMutationResolver(BaseMutationResolver):
return await self._do_update(db, obj=photo, data=data) return await self._do_update(db, obj=photo, data=data)
async def change_orientation( async def change_orientation(
self, id: int, user_id: int, direction: str, copilot_upload_token: Optional[str], info self, id: int, user_id: int, direction: str, copilot_upload_token: str | None, info
): ):
photo = await self._get_photo_details(id, user_id, copilot_upload_token) photo = await self._get_photo_details(id, user_id, copilot_upload_token)
@@ -261,12 +269,15 @@ class PhotoMutationResolver(BaseMutationResolver):
"cache_key": int(time()) "cache_key": int(time())
}) })
async def delete(self, user_id: int, id: int, **kwargs) -> Photo: async def delete(self, context: GraphQLContext, id: int, **kwargs) -> Photo:
copilot_upload_token = kwargs.get("copilot_upload_token") copilot_upload_token = kwargs.get("copilot_upload_token")
await self._get_photo_details(id, user_id, copilot_upload_token) await self._get_photo_details(id, context.user_id, copilot_upload_token, copy_original=False)
# ^^ pokud neexistuje, vyhodi vyjimku
try:
photo = await super().delete(context, id)
except (IntegrityError, sqlalchemy.exc.IntegrityError):
raise GraphQLError("Nelze smazat titulni fotku")
photo = await super().delete(user_id, id)
base_path = get_photo_basepath(photo.flight_id) base_path = get_photo_basepath(photo.flight_id)
files_to_delete = [ files_to_delete = [
@@ -5,20 +5,43 @@ import strawberry
from strawberry.file_uploads import Upload from strawberry.file_uploads import Upload
from database import models from database import models
from graphql_schema.entities.types.base import BaseGraphqlInputType 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 from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_input
@strawberry.input() @strawberry.input()
class ComboboxInput: class ComboboxInput:
id: Optional[int] = None id: int | None = None
name: str name: str
@strawberry_sqlalchemy_input(model=models.FlightPlanMarker, exclude_fields=["id", "flight_plan_id"])
class FlightPlanMarkerInput(BaseGraphqlInputType):
position: int | None = 0
@strawberry_sqlalchemy_input(model=models.Copilot, exclude_fields=["id"]) @strawberry_sqlalchemy_input(model=models.Copilot, exclude_fields=["id"])
class CreateCopilotInput(BaseGraphqlInputType): class CreateCopilotInput(BaseGraphqlInputType):
pass 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) @strawberry_sqlalchemy_input(model=models.Copilot, exclude_fields=["id"], all_optional=True)
class EditCopilotInput(BaseGraphqlInputType): class EditCopilotInput(BaseGraphqlInputType):
pass pass
@@ -58,19 +81,19 @@ class EditEventInput(BaseGraphqlInputType):
class UploadPhotoInput: class UploadPhotoInput:
photo: Upload photo: Upload
flight_id: int flight_id: int
name: Optional[str] = None name: str | None = None
description: Optional[str] = None description: str | None = None
point_of_interest: Optional[ComboboxInput] = None point_of_interest: Optional[ComboboxInput] = None
copilot_upload_token: Optional[str] = None copilot_upload_token: str | None = None
@strawberry.input @strawberry.input
class EditPhotoInput: class EditPhotoInput:
name: Optional[str] = None name: str | None = None
description: Optional[str] = None description: str | None = None
point_of_interest: Optional[ComboboxInput] = None point_of_interest: Optional[ComboboxInput] = None
copilots: Optional[List[ComboboxInput]] = None copilots: Optional[List[ComboboxInput]] = None
aircraft_id: Optional[int] = None aircraft_id: int | None = None
def to_dict(self): def to_dict(self):
return { return {
@@ -89,12 +112,12 @@ class CropInput(BaseGraphqlInputType):
@strawberry.input @strawberry.input
class AdjustmentInput: class AdjustmentInput:
rotate: Optional[float] = 0 rotate: float | None = 0
crop_after_rotate: Optional[bool] = True, crop_after_rotate: bool | None = True,
brightness: Optional[float] = 1 brightness: float | None = 1
contrast: Optional[float] = 1 contrast: float | None = 1
saturation: Optional[float] = 1 saturation: float | None = 1
sharpness: Optional[float] = 1 sharpness: float | None = 1
crop: Optional[CropInput] = None crop: Optional[CropInput] = None
@@ -115,18 +138,23 @@ class EditFlightInput(BaseGraphqlInputType):
@strawberry.input() @strawberry.input()
class CreateFlightInput(BaseGraphqlInputType): class CreateFlightInput(BaseGraphqlInputType):
aircraft: ComboboxInput aircraft: ComboboxInput
flight_plan_id: int | None = None
name: str | None = None
gpx_track_file: Optional[Upload] = None # TODO: poresit validaci uploadovaneho souboru! gpx_track_file: Optional[Upload] = None # TODO: poresit validaci uploadovaneho souboru!
landing_airport: Optional[ComboboxInput] = None landing_airport: Optional[ComboboxInput] = None
takeoff_airport: Optional[ComboboxInput] = None takeoff_airport: Optional[ComboboxInput] = None
takeoff_datetime: Optional[datetime] = None takeoff_datetime: Optional[datetime] = None
landing_datetime: Optional[datetime] = None landing_datetime: Optional[datetime] = None
track: Optional[List[TrackItemInput]] = None
@strawberry.input() @strawberry.input()
class TrackItemInput: class TrackItemInput:
point_of_interest: Optional[ComboboxInput] = None point_of_interest: Optional[ComboboxInput] = None
airport: Optional[ComboboxInput] = None airport: Optional[ComboboxInput] = None
landing_duration: Optional[int] = None landing_duration: int | None = None
gps_latitude: float | None = None
gps_longitude: float | None = None
@strawberry_sqlalchemy_input(models.Aircraft, exclude_fields=['id', 'photo_filename']) @strawberry_sqlalchemy_input(models.Aircraft, exclude_fields=['id', 'photo_filename'])
+118 -63
View File
@@ -1,26 +1,28 @@
from __future__ import annotations from __future__ import annotations
from datetime import datetime import math
from typing import Optional, List from typing import Optional, List
import strawberry import strawberry
from database import models from database import models
from decorators.endpoints import authenticated_user_only # from decorators.endpoints import authenticated_user_only
from utils.gps import get_bearing, get_distance
from external.gpx_parser import GPXParser from external.gpx_parser import GPXParser
from graphql_schema.dataloaders.flight_duration import flight_duration_dataloader from graphql_schema.dataloaders.flight_duration import flight_duration_dataloader
from graphql_schema.dataloaders.multi_models import ( from graphql_schema.dataloaders.multi_models import (
poi_photos_dataloader, flight_by_poi_dataloader, flight_copilots_dataloader, flight_track_dataloader, poi_photos_dataloader, flight_by_poi_dataloader, flight_copilots_dataloader, flight_turn_points_dataloader,
photos_dataloader, flights_by_aircraft_dataloader, users_in_organization_dataloader, photos_dataloader, flights_by_aircraft_dataloader, users_in_organization_dataloader,
aircrafts_from_organization_dataloader, user_organizations_dataloader, flights_by_event_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, flights_by_copilot_dataloader, public_flights_by_event_dataloader, public_flights_by_copilot_dataloader,
photo_copilots_dataloader, photos_aircraft_dataloader, copilots_in_photo_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
) )
from graphql_schema.dataloaders.single_model import ( from graphql_schema.dataloaders.single_model import (
poi_dataloader, poi_type_dataloader, event_dataloader, aircraft_dataloader, airport_dataloader, poi_dataloader, poi_type_dataloader, event_dataloader, aircraft_dataloader, airport_dataloader,
airport_weather_info_loader, organizations_dataloader, flight_dataloader, photo_adjustment_dataloader, weather_info_loader, organizations_dataloader, flight_dataloader, photo_adjustment_dataloader,
photo_dataloader, user_dataloader photo_dataloader, user_dataloader, track_dataloader
) )
from graphql_schema.permissions import IsAuthenticated from graphql_schema.permissions import IsAuthenticated
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type
from paths import get_avatar_url, get_title_image_url, get_photo_thumbnail_url, get_photo_url, FLIGHT_GPX_TRACK_PATH from paths import get_avatar_url, get_title_image_url, get_photo_thumbnail_url, get_photo_url, FLIGHT_GPX_TRACK_PATH, airport_icon_url, get_public_url
@strawberry.type @strawberry.type
@@ -29,23 +31,31 @@ class Point:
lng: float lng: float
@strawberry.type
class GPXTrack:
coordinates: List[Point] @strawberry_sqlalchemy_type(models.ReportingPoint)
speed: List[float] class ReportingPoint:
altitude: List[float] pass
magnetic_variation: List[float]
terrain_elevation: List[float]
time: List[datetime] @strawberry_sqlalchemy_type(models.Runway)
max_speed: float class Runway:
avg_speed: float pass
max_altitude: float
avg_altitude: float
@strawberry_sqlalchemy_type(models.Frequency)
class Frequency:
pass
@strawberry_sqlalchemy_type(models.Airport) @strawberry_sqlalchemy_type(models.Airport)
class Airport: class Airport:
pass 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))
@strawberry.type @strawberry.type
@@ -55,20 +65,17 @@ class Airspace:
country: str country: str
type: str type: str
lower_limit: str lower_limit: str
lower_alt_unit: str
upper_alt_unit: str
upper_limit: str upper_limit: str
center: Optional[tuple[float, float]]
radius: Optional[float]
bounds: list[tuple[float, 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( map_bounds: list[Point] = strawberry.field(
resolver=lambda root: [Point(lat=point[0], lng=point[1]) for point in root.bounds] resolver=lambda root: [Point(lat=point[1], lng=point[0]) for point in root.bounds]
) )
@strawberry_sqlalchemy_type(models.FlightTrack) @strawberry_sqlalchemy_type(models.FlightTurnPoint)
class FlightTrack: class FlightTurnPoint:
point_of_interest: Optional[PointOfInterest] = strawberry.field( point_of_interest: Optional[PointOfInterest] = strawberry.field(
resolver=lambda root: poi_dataloader.load(root.point_of_interest_id) resolver=lambda root: poi_dataloader.load(root.point_of_interest_id)
) )
@@ -112,6 +119,20 @@ class Photo:
) )
@strawberry_sqlalchemy_type(models.TrackPoint)
class TrackPoint:
coordinates: Point = strawberry.field(resolver=lambda root: Point(lat=root.gps_latitude, lng=root.gps_longitude))
@strawberry_sqlalchemy_type(models.Track, exclude_fields=['bounds'])
class Track:
bounds: list[tuple[float, float]]
map_bounds: list[Point] = strawberry.field(
resolver=lambda root: [Point(lat=point[1], lng=point[0]) for point in root.bounds]
)
points: list[TrackPoint] = strawberry.field(resolver=lambda root: track_points_dataloder.load(root.id))
@strawberry_sqlalchemy_type(models.Flight) @strawberry_sqlalchemy_type(models.Flight)
class Flight: class Flight:
def __init__(self, **kwargs): def __init__(self, **kwargs):
@@ -120,33 +141,11 @@ class Flight:
for key, value in kwargs.items(): for key, value in kwargs.items():
setattr(self, key, value) setattr(self, key, value)
async def load_gpx_track(root): # @authenticated_user_only(raise_when_unauthorized=False, return_value_unauthorized=[])
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): async def load_copilots(root):
return await flight_copilots_dataloader.load(root.id) return await flight_copilots_dataloader.load(root.id)
@authenticated_user_only(raise_when_unauthorized=False, return_value_unauthorized=[]) # @authenticated_user_only(raise_when_unauthorized=False, return_value_unauthorized=[])
async def load_event(root): async def load_event(root):
return await event_dataloader.load(root.event_id) return await event_dataloader.load(root.event_id)
@@ -161,25 +160,78 @@ class Flight:
resolver=lambda root: airport_dataloader.load(root.landing_airport_id) 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)) title_photo: Optional[Photo] = strawberry.field(resolver=lambda root: photo_dataloader.load(root.title_photo_id))
track: List[FlightTrack] = strawberry.field(resolver=lambda root: flight_track_dataloader.load(root.id)) turn_points: List[FlightTurnPoint] = strawberry.field(resolver=lambda root: flight_turn_points_dataloader.load(root.id))
takeoff_weather_info: Optional[WeatherInfo] = strawberry.field( takeoff_weather_info: Optional[WeatherInfo] = strawberry.field(
resolver=lambda root: airport_weather_info_loader.load(root.takeoff_weather_info_id) resolver=lambda root: weather_info_loader.load(root.takeoff_weather_info_id)
) )
landing_weather_info: Optional[WeatherInfo] = strawberry.field( landing_weather_info: Optional[WeatherInfo] = strawberry.field(
resolver=lambda root: airport_weather_info_loader.load(root.landing_weather_info_id) resolver=lambda root: weather_info_loader.load(root.landing_weather_info_id)
) )
photos: List[Photo] = strawberry.field(resolver=lambda root: photos_dataloader.load(root.id)) photos: List[Photo] = strawberry.field(resolver=lambda root: photos_dataloader.load(root.id))
gpx_track: Optional[GPXTrack] = strawberry.field(resolver=load_gpx_track) track: Optional[Track] = strawberry.field(resolver=lambda root: track_dataloader.load(root.track_id))
duration_min_calculated: int = strawberry.field( duration_min_calculated: int = strawberry.field(resolver=lambda root: flight_duration_dataloader.load(root.id))
resolver=lambda root: flight_duration_dataloader.load(root.id) social_image_url: str | None = strawberry.field(
resolver=lambda root: get_public_url(f'photos/{root.id}/title_photo.jpg')
) )
debriefing_notes: str | None = strawberry.field(
debriefing_notes: Optional[str] = strawberry.field(
permission_classes=[IsAuthenticated], permission_classes=[IsAuthenticated],
resolver=lambda root: root.debriefing_notes resolver=lambda root: root.debriefing_notes
) )
@strawberry_sqlalchemy_type(model=models.FlightPlanMarker)
class FlightPlanMarker:
weather_info: Optional[WeatherInfo] = strawberry.field(
resolver=lambda root: weather_info_loader.load(root.weather_info_id)
)
@strawberry.type
class FlightPlanTrack:
bearing: int | None
distance: float | None
from_: FlightPlanMarker = strawberry.field(name="from")
to: Optional[FlightPlanMarker]
@strawberry_sqlalchemy_type(model=models.FlightPlan)
class FlightPlan:
async def resolve_track(self):
markers = await self.markers()
if len(markers) == 1:
return [FlightPlanTrack(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(FlightPlanTrack(
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)
)
track: Optional[list[FlightPlanTrack]] = strawberry.field(
resolver=resolve_track
)
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) @strawberry_sqlalchemy_type(models.Copilot)
class Copilot: class Copilot:
async def resolve_flights(root, info): async def resolve_flights(root, info):
@@ -195,7 +247,7 @@ class Copilot:
flights: List[Flight] = strawberry.field(resolver=resolve_flights) flights: List[Flight] = strawberry.field(resolver=resolve_flights)
photos: List[Photo] = strawberry.field(resolver=lambda root: photo_copilots_dataloader.load(root.id)) photos: List[Photo] = strawberry.field(resolver=lambda root: photo_copilots_dataloader.load(root.id))
title_photo: Optional[Photo] = strawberry.field(resolver=lambda root: photo_dataloader.load(root.title_photo_id)) title_photo: Optional[Photo] = strawberry.field(resolver=lambda root: photo_dataloader.load(root.title_photo_id))
token: Optional[str] = strawberry.field( token: str | None = strawberry.field(
permission_classes=[IsAuthenticated], permission_classes=[IsAuthenticated],
resolver=resolve_copilot_token resolver=resolve_copilot_token
) )
@@ -221,8 +273,11 @@ class Organization:
@strawberry_sqlalchemy_type(models.User, exclude_fields=['password_hashed']) @strawberry_sqlalchemy_type(models.User, exclude_fields=['password_hashed'])
class User: class User:
avatar_image_url: Optional[str] = strawberry.field(resolver=lambda root: get_avatar_url(root)) avatar_image_url: str | None = strawberry.field(resolver=lambda root: get_avatar_url(root))
title_image_url: str = strawberry.field(resolver=lambda root: get_title_image_url(root)) title_image_url: str = strawberry.field(resolver=lambda root: get_title_image_url(root))
social_image_url: str | None = strawberry.field(
resolver=lambda root: get_public_url(f'profile/{root.id}/title_photo.jpg')
)
organizations: List[Organization] = strawberry.field( organizations: List[Organization] = strawberry.field(
resolver=lambda root: user_organizations_dataloader.load(root.id) resolver=lambda root: user_organizations_dataloader.load(root.id)
) )
@@ -230,7 +285,7 @@ class User:
@strawberry_sqlalchemy_type(models.Event) @strawberry_sqlalchemy_type(models.Event)
class Event: class Event:
async def load_flights(root, info, only_public: Optional[bool] = False): async def load_flights(root, info, only_public: bool | None = False):
is_user_logged_in = bool(info.context.user_id) is_user_logged_in = bool(info.context.user_id)
if not is_user_logged_in or only_public: if not is_user_logged_in or only_public:
dataloader = public_flights_by_event_dataloader dataloader = public_flights_by_event_dataloader
+13 -17
View File
@@ -3,22 +3,21 @@ import strawberry
from graphql import GraphQLError from graphql import GraphQLError
from passlib.hash import bcrypt from passlib.hash import bcrypt
from sqlalchemy import select from sqlalchemy import select
from strawberry import Info
from strawberry.file_uploads import Upload from strawberry.file_uploads import Upload
from background_jobs.photo import resize_photo from background_jobs.photo import resize_photo
from database import models from database import models
from decorators.endpoints import authenticated_user_only
from decorators.error_logging import error_logging
from database.transaction import get_session from database.transaction import get_session
from graphql_schema.entities.types.types import User from graphql_schema.entities.types.types import User
from graphql_schema.extensions.field.auth import AuthenticatedOnly
from utils.file import delete_file from utils.file import delete_file
from utils.upload import handle_file_upload from utils.file import handle_file_upload
@strawberry.type @strawberry.type
class UserQueries: class UserQueries:
@strawberry.field() @strawberry.field()
@error_logging async def user(root, info: Info, username: str) -> User:
async def user(root, info, username: str) -> User:
if len(username) == 0: if len(username) == 0:
raise GraphQLError("Username not set!") raise GraphQLError("Username not set!")
@@ -28,10 +27,8 @@ class UserQueries:
return user return user
@strawberry.field() @strawberry.field(extensions=[AuthenticatedOnly()])
@authenticated_user_only() async def logged_user(root, info: Info) -> User:
@error_logging
async def logged_user(root, info) -> User:
async with get_session() as db: async with get_session() as db:
user_model = (await db.scalars( user_model = (await db.scalars(
select(models.User).filter_by(id=info.context.user_id) select(models.User).filter_by(id=info.context.user_id)
@@ -44,17 +41,16 @@ class UserQueries:
class EditUserMutation: class EditUserMutation:
@strawberry.input @strawberry.input
class EditUserInput: class EditUserInput:
name: Optional[str] = None name: str | None = None
description: Optional[str] = None description: str | None = None
public_username: Optional[str] = None public_username: str | None = None
old_password: Optional[str] = None old_password: str | None = None
new_password: Optional[str] = None new_password: str | None = None
avatar_image: Optional[Upload] = None avatar_image: Optional[Upload] = None
title_image: Optional[Upload] = None title_image: Optional[Upload] = None
@strawberry.mutation @strawberry.mutation(extensions=[AuthenticatedOnly()])
@authenticated_user_only() async def edit_logged_user(root, info: Info, input: EditUserInput) -> User:
async def edit_logged_user(root, info, input: EditUserInput) -> User:
async with get_session() as db: async with get_session() as db:
user = (await db.scalars( user = (await db.scalars(
select(models.User).filter_by(id=info.context.user_id) select(models.User).filter_by(id=info.context.user_id)
@@ -0,0 +1,25 @@
from typing import Any
from fastapi import HTTPException
from starlette.status import HTTP_401_UNAUTHORIZED
from strawberry import Info
from strawberry.extensions import FieldExtension
from strawberry.extensions.field_extension import AsyncExtensionResolver
class AuthenticatedOnly(FieldExtension):
async def resolve_async(self, next_: AsyncExtensionResolver, source: Any, info: Info, **kwargs: Any) -> Any:
if not info.context.user_id:
raise HTTPException(HTTP_401_UNAUTHORIZED, "Not authorized")
return await next_(source, info, **kwargs)
class AllowPublicAccess(FieldExtension):
async def resolve_async(self, next_: AsyncExtensionResolver, source: Any, info: Info, **kwargs: Any) -> Any:
user_id = info.context.user_id
public = kwargs.get('public')
if not user_id and not public:
raise HTTPException(HTTP_401_UNAUTHORIZED, "Not authorized")
return await next_(source, info, **kwargs)
@@ -0,0 +1,48 @@
from typing import Callable, Any, Type
import strawberry
from strawberry.annotation import StrawberryAnnotation
from strawberry.extensions import FieldExtension
from strawberry.types.arguments import StrawberryArgument
from strawberry.types.field import StrawberryField
from graphql_schema.entities.helpers.pagination import get_pagination_window, PaginationWindow
class OffsetPagination[Item](FieldExtension):
def __init__(self, item_type: Type[Item]):
super().__init__()
self.item_type = item_type
def apply(self, field: StrawberryField) -> StrawberryField:
offset_arg = StrawberryArgument(
python_name="offset",
graphql_name="offset",
type_annotation=StrawberryAnnotation(annotation=int | None),
default=0,
)
limit_arg = StrawberryArgument(
python_name="limit",
graphql_name="limit",
type_annotation=StrawberryAnnotation(annotation=int),
default=10,
)
field.arguments.append(offset_arg)
field.arguments.append(limit_arg)
return field
async def resolve_async(
self, next_: Callable[..., Any], source: Any, info: strawberry.Info,
limit: int, offset: int = 0,
**kwargs
) -> PaginationWindow[Item]:
query = await next_(source, info, **kwargs)
return await get_pagination_window(
query=query,
item_type=self.item_type,
limit=limit,
offset=offset,
)
@@ -0,0 +1,19 @@
from typing import Callable, Any
from fastapi import HTTPException
from graphql import GraphQLResolveInfo, GraphQLError
from sqlalchemy.exc import NoResultFound
from strawberry.extensions import SchemaExtension
from strawberry.utils.await_maybe import AwaitableOrValue
class ErrorLogging(SchemaExtension):
async def resolve_async(self, _next: Callable, root: Any, info: GraphQLResolveInfo, *args: str, **kwargs: Any) -> AwaitableOrValue[object]:
try:
return await _next(root, info, *args, **kwargs)
except NoResultFound as e:
raise GraphQLError("Not found", original_error=e)
except HTTPException as e:
if e.status_code == 401:
raise GraphQLError("Not authorized", original_error=e)
except Exception as e:
raise GraphQLError(f"Unknown error: {e}", original_error=e)
+2
View File
@@ -3,6 +3,7 @@ from graphql_schema.entities.aircraft import AircraftMutation
from graphql_schema.entities.copilot import CopilotMutation from graphql_schema.entities.copilot import CopilotMutation
from graphql_schema.entities.event import EventMutation from graphql_schema.entities.event import EventMutation
from graphql_schema.entities.flight import FlightMutation 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.organization import OrganizationUserMutation, OrganizationMutation
from graphql_schema.entities.photo import PhotoMutation from graphql_schema.entities.photo import PhotoMutation
from graphql_schema.entities.poi import PointOfInterestMutation from graphql_schema.entities.poi import PointOfInterestMutation
@@ -11,6 +12,7 @@ from graphql_schema.entities.user import EditUserMutation
Mutation = merge_types("Mutation", ( Mutation = merge_types("Mutation", (
AircraftMutation, AircraftMutation,
FlightMutation, FlightMutation,
FlightPlanMutation,
PhotoMutation, PhotoMutation,
PointOfInterestMutation, PointOfInterestMutation,
CopilotMutation, CopilotMutation,
+2
View File
@@ -5,6 +5,7 @@ from .entities.airspace import AirspaceQueries
from .entities.copilot import CopilotQueries from .entities.copilot import CopilotQueries
from .entities.event import EventQueries from .entities.event import EventQueries
from .entities.flight import FlightQueries from .entities.flight import FlightQueries
from .entities.flight_plan import FlightPlanQueries
from .entities.organization import OrganizationQueries from .entities.organization import OrganizationQueries
from .entities.photo import PhotoQueries from .entities.photo import PhotoQueries
from .entities.poi import PointOfInterestQueries from .entities.poi import PointOfInterestQueries
@@ -19,6 +20,7 @@ Query = merge_types('Query', (
AirportQueries, AirportQueries,
AirspaceQueries, AirspaceQueries,
FlightQueries, FlightQueries,
FlightPlanQueries,
CopilotQueries, CopilotQueries,
UserQueries, UserQueries,
PhotoQueries, PhotoQueries,
+17 -32
View File
@@ -1,45 +1,30 @@
import dataclasses from datetime import datetime
from typing import Set
import strawberry import strawberry
from fastapi_jwt import JwtAuthorizationCredentials from strawberry.extensions import SchemaExtension, ValidationCache, ParserCache
from fastapi_jwt.jwt import JwtAccessBearerCookie from graphql_schema.extensions.schema.error_logging import ErrorLogging
from starlette.background import BackgroundTasks from logger import log
from strawberry.extensions import SchemaExtension
from strawberry.fastapi import BaseContext
from .mutation import Mutation from .mutation import Mutation
from .query import Query from .query import Query
# Toto se da kdyztak pouzit jako extension do Schema
# class SQLAlchemySession(Extension):
# def on_request_start(self):
# session = async_session()
# print(self.execution_context.context)
# self.execution_context.context["db"] = session
#
# async def on_request_end(self):
# await self.execution_context.context["db"].close()
class LoggingExtension(SchemaExtension): class LoggingExtension(SchemaExtension):
def on_request_start(self): def on_request_end(self):
print("request start") # nastavuje se pri vytvareni GraphQL contextu
request_start = self.execution_context.context.request_start_time
request_end = datetime.now()
async def on_request_end(self): request_time_ms = round((request_end - request_start).microseconds / 1000)
print("request end") op_name = self.execution_context.operation_name
log.info(f"Operation {op_name} took {request_time_ms}ms")
@dataclasses.dataclass
class GraphQLContext(BaseContext):
user_id: int
organization_ids: Set[int]
jwt_auth_credentials: JwtAuthorizationCredentials
jwt: JwtAccessBearerCookie
background_tasks: BackgroundTasks
schema = strawberry.Schema( schema = strawberry.Schema(
query=Query, query=Query,
mutation=Mutation, mutation=Mutation,
extensions=[LoggingExtension] extensions=[
LoggingExtension(),
ErrorLogging(),
ParserCache(),
ValidationCache()
],
) )

Some files were not shown because too many files have changed in this diff Show More