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 ffb8d07ebe fix nacitani letadla podle volacky 2024-03-29 10:40:52 +01:00
Michal Kváček fd0aef83b1 fix nacitani fotek na verejne casti 2024-03-25 22:35:11 +01:00
Michal Kváček 66d9877552 Fix nacitani detailu letadla 2024-03-25 19:14:03 +01: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 6a7d51efc5 Rychlost v GPX muze byt float 2024-03-23 20:28:11 +01:00
Michal Kváček 24825108a7 Zaklad pro planovac letu 2024-03-23 20:15:47 +01:00
Michal Kváček ed35de96c9 Parsovani vzdusneho prostoru 2024-03-21 16:40:23 +01:00
Michal Kváček 7c81069e96 Fix nacitani eventu 2024-03-07 07:07:33 +01:00
Michal Kváček 400ad8b75a Moznost nahravat fotky copilotem 2024-03-07 06:45:00 +01:00
Michal Kváček c0ae73440d Bugfixing 2024-02-28 09:41:46 +01:00
Michal Kváček 1f8b338297 Pridani sitemap, fix flake 2024-02-26 09:44:40 +01:00
Michal Kváček ff32c36a94 Pridani samplovani dat - neni potreba vracet polohu a rychlost kazdych 10s 2024-02-22 08:17:40 +01:00
Michal Kváček e28a6fce57 Uprava queries a dotazovani se pres base query builder, fix nahledu fotky pro editor, cache na fotky 2024-02-13 22:59:51 +01:00
Michal Kváček 51316c492b Fix nacitani detailu udalosti 2024-02-12 06:33:09 +01:00
Michal Kváček c26eb97288 Reset hesla, doplneni mailu 2024-02-08 16:26:12 +01:00
Michal Kváček 03c9f2d189 Vyhledavani v POI 2024-02-01 08:32:10 +01:00
Michal Kváček 77b9552a9d Zakladni mailing 2024-02-01 07:22:14 +01:00
Michal Kváček cc95197682 Odstraneni generovanych HTML sablon mailu 2024-01-20 23:21:26 +01:00
Michal Kváček 57aad05359 Zakladni nastrel emailu 2024-01-20 23:20:37 +01:00
Michal Kváček dfb96e768e Aktualizace zavislosti, pridani debrief poznamek 2024-01-20 22:09:05 +01:00
Michal Kváček 0e890dc201 Odstraneni zbytecnych importu 2024-01-20 15:39:49 +01:00
Michal Kváček 3c757df7f1 Bugfixing 2024-01-18 06:53:01 +01:00
Michal Kváček dabed327dd Slugy, webp, hromada zmen 2024-01-13 23:29:24 +01:00
Michal Kváček 3b1fd0351a Fix razeni fotek 2023-12-19 20:35:06 +01:00
Michal Kváček 3edc1c853f Fix nacitani letu k eventu 2023-12-12 23:54:28 +01:00
Michal Kváček f4d709d621 Strankovani 2023-12-12 23:41:44 +01:00
Michal Kváček f535d2866c Uprava razeni fotek, drobnosti 2023-12-10 22:11:44 +01:00
Michal Kváček 89f5b8bb0c Upravy v dotazovani na verejna data 2023-12-05 12:15:54 +01:00
Michal Kváček 3167ea2f8a Hromada uprav kvuli ssr renderu a verejne casti 2023-12-04 22:45:44 +01:00
Michal Kváček ecb37fb961 Povoleni CORS i z dev SSR domeny, dataloader prevadi list IDcek na set 2023-11-23 16:40:42 +01:00
Michal Kváček 108a731608 Fix vytvareni letu s novym letadlem 2023-11-18 23:41:43 +01:00
Michal Kváček 19c1ff1846 Bugfixes 2023-11-09 06:17:01 +01:00
Michal Kváček bd5b84c9e7 Error logging, uprava dotazovani na seznam letu u kopilota 2023-11-07 17:45:26 +01:00
Michal Kváček e3ab6ba7f0 Pridani kontroly existence cesty pro zapsani fotky, pridano razeni letu u kopilota 2023-11-06 21:41:44 +01:00
Michal Kváček e8aa678555 Dotazovani na detail kopilota jen po prihlaseni nebo jen s username 2023-11-02 16:29:02 +01:00
Michal Kváček f2e2c1a1ab Fix flake 2023-11-02 09:25:23 +01:00
Michal Kváček ebf2995d51 Filtrovani copilotu 2023-11-02 09:12:01 +01:00
Michal Kváček 52ddf6502a Refaktoring photo endpointu 2023-11-02 09:03:35 +01:00
Michal Kváček 0de73a5ec2 Pridani sentry, fix mazani upravenych fotek 2023-10-31 17:30:29 +01:00
Michal Kváček 3223d2ac00 Pridani editace fotek 2023-10-27 22:30:59 +02:00
Michal Kváček 322e61bce3 Fix otaceni fotek, drobny refaktoring 2023-10-15 14:24:57 +02:00
Michal Kváček bc258efa3a Refaktoring 2023-10-14 22:42:51 +02:00
Michal Kváček 7f7f79dad6 Refaktoring a bugfixing 2023-10-13 23:24:53 +02:00
Michal Kváček ce2c023eaf Do sloupce photo.exposed_at ukladat datetime_original, ne datum vyvolani rawu 2023-10-09 15:07:32 +02:00
Michal Kváček 86fe3949f6 Presun nacitani doby letu do dataloaderu 2023-10-09 10:24:53 +02:00
Michal Kváček b355d0b439 Refaktoring, pridani letiste pro mezipristani 2023-10-07 21:36:56 +02:00
Michal Kváček 2422f26f10 Fix loginu 2023-09-26 06:56:38 +02:00
Michal Kváček 3f03021f5c Refaktoring 2023-09-26 06:49:15 +02:00
Michal Kváček dc49cd347c Pouziti base resolveru u letiste 2023-09-25 10:07:56 +02:00
Michal Kváček d820b92964 Refaktoring resolveru 2023-09-25 10:04:03 +02:00
Michal Kváček 22a28ae70e Uprava razeni letu 2023-09-25 06:26:16 +02:00
Michal Kváček 2c53a8ffa3 Otaceni fotek 2023-09-21 09:39:39 +02:00
Michal Kváček f50c9660c7 Fix nacitani seznamu letu 2023-09-20 14:14:53 +02:00
Michal Kváček 037c7daef6 Sprava eventu a organizaci, bugfixing a drobny refaktoring 2023-09-20 13:26:34 +02:00
Michal Kváček 1760dba99d Uprava prace s DB, refaktoring 2023-09-13 10:04:06 +02:00
Michal Kváček 64cb544e2d Pridani skriptu pro pridani elevace, uprava prace s transakcemi 2023-09-12 11:00:36 +02:00
171 changed files with 7611 additions and 2086 deletions
+24
View File
@@ -0,0 +1,24 @@
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8
# 4 space indentation
[*.{py,java,r,R}]
indent_style = space
indent_size = 4
# 2 space indentation
[*.{js,json,yml,yaml,html}]
indent_style = space
indent_size = 2
[*.{md,Rmd,rst}]
trim_trailing_whitespace = false
indent_style = space
indent_size = 2
+5
View File
@@ -7,3 +7,8 @@ MYSQL_DATABASE: "poletani_cz"
MYSQL_USER: "mysql_user"
MYSQL_PASSWORD: "****"
APP_ENV: "production"
SMTP_HOST: ""
SMTP_PORT: ""
SMTP_USERNAME: ""
SMTP_PASSWORD: ""
+2 -1
View File
@@ -2,5 +2,6 @@
max-line-length = 120
exclude =
venv
.venv
__pycache__
alembic
alembic
+5 -1
View File
@@ -7,4 +7,8 @@ __pycache__/
# app specific files and dirs
uploads/*
!uploads/.gitkeep
!uploads/.gitkeep
src/emails/templates/html/*
.env
/static/airports/
/static/airports/.gitkeep
+19 -6
View File
@@ -1,15 +1,28 @@
FROM python:3.11-alpine
FROM node:20-alpine AS email_builder
RUN npm install -g mjml
RUN mkdir -p /app/templates/html
COPY ./src/emails/templates/mjml /app/templates
RUN mjml /app/templates/* -o /app/templates/html/
FROM python:3.13-alpine AS app
RUN apk update && \
apk add --no-cache tzdata mariadb-client mariadb-dev && \
pip install --upgrade pip
# protoze xfs uzivatel ma id=33
RUN deluser xfs
# RUN deluser xfs
# uzivatel www-data v alpine neexistuje
RUN delgroup www-data && addgroup -g 33 -S www-data && adduser -u 33 -D -S -G www-data www-data
RUN apk update && \
apk add --no-cache tzdata mariadb-client mariadb-dev && \
pip install --upgrade pip
COPY requirements.txt /app/requirements.txt
COPY --from=email_builder /app/templates/html/ /app/src/email/templates/html/
# required packages for pip (becasue of yarl lib)
ENV INSTALL_PACKAGES build-base linux-headers
@@ -23,4 +36,4 @@ COPY . /app
WORKDIR /app
CMD ["uvicorn", "src.asgi:app", "--reload", "--reload-dir=/app", "--port=8000", "--host=0.0.0.0", "--log-level=debug"]
CMD [ "gunicorn", "src.asgi:app", "--config=/app/config/gunicorn.py" ]
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+3
View File
@@ -0,0 +1,3 @@
# Polétání.cz
@@ -0,0 +1,46 @@
"""add terrain elevation to photos and event table
Revision ID: 6e5cc5123a2b
Revises: 76752a74a304
Create Date: 2023-09-05 09:54:45.509461
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '6e5cc5123a2b'
down_revision = '76752a74a304'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('event',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=128), nullable=False),
sa.Column('description', sa.Text(), 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')
)
op.add_column('flight', sa.Column('event_id', sa.Integer(), nullable=True))
op.add_column('flight', sa.Column('has_terrain_elevation', sa.Boolean(), nullable=False))
op.create_foreign_key(None, 'flight', 'event', ['event_id'], ['id'])
op.add_column('photo', sa.Column('terrain_elevation', sa.Float(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('photo', 'terrain_elevation')
op.drop_constraint(None, 'flight', type_='foreignkey')
op.drop_column('flight', 'has_terrain_elevation')
op.drop_column('flight', 'event_id')
op.drop_table('event')
# ### end Alembic commands ###
@@ -0,0 +1,38 @@
"""make gps in airport nullable
Revision ID: a09857ec9721
Revises: 6e5cc5123a2b
Create Date: 2023-09-13 09:32:06.444179
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = 'a09857ec9721'
down_revision = '6e5cc5123a2b'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('airport', 'gps_latitude',
existing_type=mysql.FLOAT(),
nullable=True)
op.alter_column('airport', 'gps_longitude',
existing_type=mysql.FLOAT(),
nullable=True)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('airport', 'gps_longitude',
existing_type=mysql.FLOAT(),
nullable=False)
op.alter_column('airport', 'gps_latitude',
existing_type=mysql.FLOAT(),
nullable=False)
# ### end Alembic commands ###
@@ -0,0 +1,50 @@
"""add from/to to event, organization to event
Revision ID: 39a62618eacb
Revises: a09857ec9721
Create Date: 2023-09-20 11:53:00.566736
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '39a62618eacb'
down_revision = 'a09857ec9721'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('aircraft_notes')
op.add_column('event', sa.Column('event_from', sa.DateTime(), nullable=True))
op.add_column('event', sa.Column('event_to', sa.DateTime(), nullable=True))
op.add_column('event', sa.Column('organization_id', sa.Integer(), nullable=True))
op.create_foreign_key(None, 'event', 'organization', ['organization_id'], ['id'])
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'event', type_='foreignkey')
op.drop_column('event', 'organization_id')
op.drop_column('event', 'event_to')
op.drop_column('event', 'event_from')
op.create_table('aircraft_notes',
sa.Column('id', mysql.INTEGER(display_width=11), autoincrement=True, nullable=False),
sa.Column('aircraft_id', mysql.INTEGER(display_width=11), autoincrement=False, nullable=False),
sa.Column('name', mysql.VARCHAR(length=128), nullable=False),
sa.Column('description', mysql.TEXT(), nullable=False),
sa.Column('is_public', mysql.TINYINT(display_width=1), server_default=sa.text('0'), autoincrement=False, 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(['aircraft_id'], ['aircraft.id'], name='aircraft_notes_ibfk_1'),
sa.ForeignKeyConstraint(['created_by_id'], ['user.id'], name='aircraft_notes_ibfk_2'),
sa.PrimaryKeyConstraint('id'),
mysql_collate='utf8mb4_general_ci',
mysql_default_charset='utf8mb4',
mysql_engine='InnoDB'
)
# ### end Alembic commands ###
@@ -0,0 +1,34 @@
"""rename from/to in events
Revision ID: 8b0c020dc0c4
Revises: 39a62618eacb
Create Date: 2023-09-22 08:53:20.901142
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '8b0c020dc0c4'
down_revision = '39a62618eacb'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('event', sa.Column('date_from', sa.DateTime(), nullable=True))
op.add_column('event', sa.Column('date_to', sa.DateTime(), nullable=True))
op.drop_column('event', 'event_from')
op.drop_column('event', 'event_to')
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('event', sa.Column('event_to', mysql.DATETIME(), nullable=True))
op.add_column('event', sa.Column('event_from', mysql.DATETIME(), nullable=True))
op.drop_column('event', 'date_to')
op.drop_column('event', 'date_from')
# ### end Alembic commands ###
@@ -0,0 +1,38 @@
"""add airports to track
Revision ID: 956d295689bf
Revises: 8b0c020dc0c4
Create Date: 2023-09-28 22:43:52.286599
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '956d295689bf'
down_revision = '8b0c020dc0c4'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('flight_track', sa.Column('airport_id', sa.Integer(), nullable=True))
op.add_column('flight_track', sa.Column('landing_duration', sa.Integer(), nullable=True))
op.alter_column('flight_track', 'point_of_interest_id',
existing_type=mysql.INTEGER(display_width=11),
nullable=True)
op.create_foreign_key(None, 'flight_track', 'airport', ['airport_id'], ['id'])
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'flight_track', type_='foreignkey')
op.alter_column('flight_track', 'point_of_interest_id',
existing_type=mysql.INTEGER(display_width=11),
nullable=False)
op.drop_column('flight_track', 'landing_duration')
op.drop_column('flight_track', 'airport_id')
# ### end Alembic commands ###
@@ -0,0 +1,39 @@
"""Add photo adjustment
Revision ID: 2acb6fa61028
Revises: 956d295689bf
Create Date: 2023-10-15 22:33:04.911765
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '2acb6fa61028'
down_revision = '956d295689bf'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('photo_adjustment',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('photo_id', sa.Integer(), nullable=False),
sa.Column('rotate', sa.Float(), nullable=True),
sa.Column('contrast', sa.Float(), nullable=True),
sa.Column('brightness', sa.Float(), nullable=True),
sa.Column('saturation', sa.Float(), nullable=True),
sa.Column('sharpness', sa.Float(), nullable=True),
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['photo_id'], ['photo.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('photo_adjustment')
# ### end Alembic commands ###
@@ -0,0 +1,34 @@
"""add crop info
Revision ID: c3aef4ae2669
Revises: 2acb6fa61028
Create Date: 2023-10-20 23:53:21.772545
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'c3aef4ae2669'
down_revision = '2acb6fa61028'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('photo_adjustment', sa.Column('crop_left', sa.Float(), nullable=True))
op.add_column('photo_adjustment', sa.Column('crop_top', sa.Float(), nullable=True))
op.add_column('photo_adjustment', sa.Column('crop_width', sa.Float(), nullable=True))
op.add_column('photo_adjustment', sa.Column('crop_height', sa.Float(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('photo_adjustment', 'crop_height')
op.drop_column('photo_adjustment', 'crop_width')
op.drop_column('photo_adjustment', 'crop_top')
op.drop_column('photo_adjustment', 'crop_left')
# ### end Alembic commands ###
@@ -0,0 +1,30 @@
"""add size to photos
Revision ID: 4996ad8bc231
Revises: c3aef4ae2669
Create Date: 2023-10-24 16:32:10.826763
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '4996ad8bc231'
down_revision = 'c3aef4ae2669'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('photo', sa.Column('width', sa.Integer(), nullable=False))
op.add_column('photo', sa.Column('height', sa.Integer(), nullable=False))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('photo', 'height')
op.drop_column('photo', 'width')
# ### end Alembic commands ###
@@ -0,0 +1,30 @@
"""on delete cascade u photo adjustment
Revision ID: d42172840f6c
Revises: 4996ad8bc231
Create Date: 2023-10-30 16:30:57.118694
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'd42172840f6c'
down_revision = '4996ad8bc231'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint('photo_adjustment_ibfk_1', 'photo_adjustment', type_='foreignkey')
op.create_foreign_key(None, 'photo_adjustment', 'photo', ['photo_id'], ['id'], ondelete='CASCADE')
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'photo_adjustment', type_='foreignkey')
op.create_foreign_key('photo_adjustment_ibfk_1', 'photo_adjustment', 'photo', ['photo_id'], ['id'])
# ### end Alembic commands ###
@@ -0,0 +1,50 @@
"""add photos to aircraft, poi and copilots
Revision ID: 91a9608b509d
Revises: d42172840f6c
Create Date: 2023-11-28 16:32:56.725312
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '91a9608b509d'
down_revision = 'd42172840f6c'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('copilot_has_photo',
sa.Column('copilot_id', sa.Integer(), nullable=False),
sa.Column('photo_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['copilot_id'], ['copilot.id'], ),
sa.ForeignKeyConstraint(['photo_id'], ['photo.id'], ),
sa.PrimaryKeyConstraint('copilot_id', 'photo_id')
)
op.add_column('aircraft', sa.Column('title_photo_id', sa.Integer(), nullable=True))
op.create_foreign_key(None, 'aircraft', 'photo', ['title_photo_id'], ['id'])
op.add_column('flight', sa.Column('title_photo_id', sa.Integer(), nullable=True))
op.create_foreign_key(None, 'flight', 'photo', ['title_photo_id'], ['id'])
op.add_column('photo', sa.Column('aircraft_id', sa.Integer(), nullable=True))
op.create_foreign_key(None, 'photo', 'aircraft', ['aircraft_id'], ['id'])
op.add_column('point_of_interest', sa.Column('title_photo_id', sa.Integer(), nullable=True))
op.create_foreign_key(None, 'point_of_interest', 'photo', ['title_photo_id'], ['id'])
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'point_of_interest', type_='foreignkey')
op.drop_column('point_of_interest', 'title_photo_id')
op.drop_constraint(None, 'photo', type_='foreignkey')
op.drop_column('photo', 'aircraft_id')
op.drop_constraint(None, 'flight', type_='foreignkey')
op.drop_column('flight', 'title_photo_id')
op.drop_constraint(None, 'aircraft', type_='foreignkey')
op.drop_column('aircraft', 'title_photo_id')
op.drop_table('copilot_has_photo')
# ### end Alembic commands ###
@@ -0,0 +1,28 @@
"""add is_public to aircraft
Revision ID: e5682ac4355c
Revises: 91a9608b509d
Create Date: 2023-12-02 12:14:25.055308
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'e5682ac4355c'
down_revision = '91a9608b509d'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('aircraft', sa.Column('is_public', sa.Boolean(), server_default='0', nullable=False))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('aircraft', 'is_public')
# ### end Alembic commands ###
@@ -0,0 +1,30 @@
"""add title photo to copilot
Revision ID: 022bf6fbcd47
Revises: e5682ac4355c
Create Date: 2023-12-02 13:05:46.267006
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '022bf6fbcd47'
down_revision = 'e5682ac4355c'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('copilot', sa.Column('title_photo_id', sa.Integer(), nullable=True))
op.create_foreign_key(None, 'copilot', 'photo', ['title_photo_id'], ['id'])
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'copilot', type_='foreignkey')
op.drop_column('copilot', 'title_photo_id')
# ### end Alembic commands ###
@@ -0,0 +1,60 @@
"""pridani slugu, odstraneni deprecated sloupcu, pridani defaultu ke jmenu a popisumm
Revision ID: 6648fb80dd0e
Revises: 022bf6fbcd47
Create Date: 2024-01-03 06:17:13.457060
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '6648fb80dd0e'
down_revision = '022bf6fbcd47'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('aircraft', 'photo_filename')
op.add_column('airport', sa.Column('airport_type', sa.Enum('airport', 'ull', 'heliport'), server_default='airport', nullable=False))
op.add_column('airport', sa.Column('use_in_gpx_guess', sa.Boolean(), server_default='1', nullable=False))
op.add_column('copilot', sa.Column('url_slug', sa.String(length=128), server_default='', nullable=False))
op.add_column('copilot', sa.Column('is_public', sa.Boolean(), server_default='0', nullable=False))
op.add_column('event', sa.Column('url_slug', sa.String(length=128), server_default='', nullable=False))
op.add_column('flight', sa.Column('url_slug', sa.String(length=128), server_default='', nullable=False))
op.alter_column('flight', 'takeoff_airport_id',
existing_type=mysql.INTEGER(display_width=11),
nullable=True)
op.alter_column('flight', 'landing_airport_id',
existing_type=mysql.INTEGER(display_width=11),
nullable=True)
op.add_column('photo', sa.Column('filename_extension', sa.String(length=4), nullable=False))
op.add_column('photo', sa.Column('cache_key', sa.String(length=128), nullable=True))
op.drop_column('photo', 'is_flight_cover')
op.add_column('point_of_interest', sa.Column('url_slug', sa.String(length=128), server_default='', nullable=False))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('point_of_interest', 'url_slug')
op.add_column('photo', sa.Column('is_flight_cover', mysql.TINYINT(display_width=1), server_default=sa.text('0'), autoincrement=False, nullable=False))
op.drop_column('photo', 'cache_key')
op.drop_column('photo', 'filename_extension')
op.alter_column('flight', 'landing_airport_id',
existing_type=mysql.INTEGER(display_width=11),
nullable=False)
op.alter_column('flight', 'takeoff_airport_id',
existing_type=mysql.INTEGER(display_width=11),
nullable=False)
op.drop_column('flight', 'url_slug')
op.drop_column('event', 'url_slug')
op.drop_column('copilot', 'is_public')
op.drop_column('copilot', 'url_slug')
op.drop_column('airport', 'use_in_gpx_guess')
op.drop_column('airport', 'airport_type')
op.add_column('aircraft', sa.Column('photo_filename', mysql.VARCHAR(length=128), nullable=True))
# ### end Alembic commands ###
@@ -0,0 +1,28 @@
"""add flight.debriefind_notes field
Revision ID: 39ebf5a1ab7e
Revises: 6648fb80dd0e
Create Date: 2024-01-20 16:22:00.487226
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '39ebf5a1ab7e'
down_revision = '6648fb80dd0e'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('flight', sa.Column('debriefing_notes', sa.Text(), server_default='', nullable=False))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('flight', 'debriefing_notes')
# ### end Alembic commands ###
@@ -0,0 +1,35 @@
"""add password_reset_token table
Revision ID: a410455fb3bb
Revises: 39ebf5a1ab7e
Create Date: 2024-02-08 07:59:02.066212
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'a410455fb3bb'
down_revision = '39ebf5a1ab7e'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('password_reset_token',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('token', sa.String(length=64), nullable=False),
sa.Column('valid_until', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('password_reset_token')
# ### end Alembic commands ###
@@ -0,0 +1,53 @@
"""add flight_has_copilot.token
Revision ID: 7dd139549851
Revises: a410455fb3bb
Create Date: 2024-03-02 15:02:26.594830
"""
import random
import string
from alembic import op
import sqlalchemy as sa
from sqlalchemy import text
# revision identifiers, used by Alembic.
revision = '7dd139549851'
down_revision = 'a410455fb3bb'
branch_labels = None
depends_on = None
def rand_str():
alphabet = string.ascii_letters + string.digits
return ''.join([random.choice(alphabet) for _ in range(64)])
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('flight_has_copilot', sa.Column('token', sa.String(length=64), nullable=True))
conn = op.get_bind()
data = conn.execute(text("SELECT flight_id, copilot_id FROM flight_has_copilot"))
for flight_id, copilot_id in data:
print(flight_id, copilot_id)
conn.execute(text(
f"UPDATE flight_has_copilot SET token='{rand_str()}'"
f" WHERE flight_id={flight_id} AND copilot_id={copilot_id}"
))
op.create_unique_constraint(None, 'flight_has_copilot', ['token'])
op.add_column('photo', sa.Column('created_by_copilot_id', sa.Integer(), nullable=True))
op.create_foreign_key(None, 'photo', 'copilot', ['created_by_copilot_id'], ['id'])
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint('photo_ibfk_5', 'photo', type_='foreignkey')
op.drop_column('photo', 'created_by_copilot_id')
op.drop_constraint('token', 'flight_has_copilot', type_='unique')
op.drop_column('flight_has_copilot', 'token')
# ### end Alembic commands ###
@@ -0,0 +1,44 @@
"""Add airspace_item table
Revision ID: aba4c3876b28
Revises: 7dd139549851
Create Date: 2024-03-18 10:05:57.516697
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = 'aba4c3876b28'
down_revision = '7dd139549851'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('airspace_item',
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_item_country'), 'airspace_item', ['country'], unique=False)
op.create_index(op.f('ix_airspace_item_type'), 'airspace_item', ['type'], unique=False)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_airspace_item_type'), table_name='airspace_item')
op.drop_index(op.f('ix_airspace_item_country'), table_name='airspace_item')
op.drop_table('airspace_item')
# ### end Alembic commands ###
@@ -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
+18 -5
View File
@@ -2,8 +2,16 @@ import csv
def deg_to_dec(val: str):
val = float(val[:-1]) / 100
deg = int(val)
frac = val % 1
# https://www.pgc.umn.edu/apps/convert/ N47 17.6 E012 47.3
print(frac, frac * 60, frac * 3600)
return deg + (frac * 60)
val = val[:-1].lstrip("0")
# h_m, s = val.split(".")
# h_m = h_m.strip("0")
# h = int(h_m[0:2])
@@ -16,15 +24,20 @@ def deg_to_dec(val: str):
# print("--konec-----------------------------------")
# return h + (m / 60.0) + (int(s) / 3600.0)
return len(val or "")
# return val
# return len(val or "")
# ZDAKOV: @49.504378,14.1808905
# 4930.250N / 100 -> cele cislo stupne, desetinne prevest do sedesatkove soustavy
with open("./poi.csv") as f:
reader = csv.DictReader(f)
for row in reader:
# if row['name'] != 'ZDAKOV':
# continue
if row['name'] != 'ZDAKOV':
continue
print(row['name'], row['lat'], row['lon'], deg_to_dec(row['lat']), deg_to_dec(row['lon']))
print(row['name'], row['lat'], row['lon'], deg_to_dec(row['lat']), deg_to_dec(row['lon']))
+1 -1
View File
@@ -21,4 +21,4 @@ ssh -t michal@kvacek.cz << EOF
docker-compose down --remove-orphans
docker-compose up -d
docker-compose exec -T api alembic upgrade head
EOF
EOF
+4 -2
View File
@@ -3,10 +3,12 @@ version: "3.3"
services:
api:
image: "docker.kvacek.cz/poletani/api:latest"
command: "uvicorn src.asgi:app --port=8000 --host=0.0.0.0 --workers 4"
network_mode: "host"
restart: "always"
env_file:
- .env
- ../.env
environment:
SENTRY_DSN: "https://184d1d6bd39f4fbb94804ae84e9afdd5@o472821.ingest.sentry.io/5506983"
volumes:
- ./uploads:/app/uploads
- ./static:/app/static
+19 -11
View File
@@ -3,12 +3,13 @@ services:
image: mariadb:10.11
ports:
- 3306:3306
env_file:
- .env
environment:
- MYSQL_USER=root
- MYSQL_DATABASE=ull_tracker
- MARIADB_ALLOW_EMPTY_ROOT_PASSWORD=true
MARIADB_ROOT_PASSWORD: "rootroot"
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
api:
build:
@@ -17,24 +18,31 @@ services:
- 8000:8000
volumes:
- .:/app
env_file:
- .env
environment:
TZ: "Europe/Prague"
API_URL: "http://localhost:8000"
APP_ENV: "development"
ALLOW_CORS_ORIGINS: "http://localhost:9000 http://localhost:9001"
MYSQL_USER: "root"
MYSQL_PASSWORD: "root"
ALLOW_CORS_ORIGINS: "http://localhost:9000 http://localhost:9001 http://localhost:9101 http://localhost:3000"
SENTRY_DSN: "https://184d1d6bd39f4fbb94804ae84e9afdd5@o472821.ingest.sentry.io/5506983"
adminer:
image: adminer
restart: always
ports:
- 8080:8080
scrapy:
build:
context: ../crawlers
environment:
- MYSQL_USER=root
- MYSQL_PASSWORD=
- MYSQL_DATABASE=ull_tracker
- MYSQL_HOST=db
env_file:
- .env
volumes:
- db-data:/var/lib/mysql:rw
- ../crawlers:/app
volumes:
db-data: { }
db-data: { }
+21 -14
View File
@@ -1,15 +1,22 @@
fastapi==0.100.0
fastapi-jwt==0.1.12
strawberry-graphql[fastapi]==0.194.4
uvicorn==0.22.0
sqlalchemy[asyncio] >= 2.0.9
aiomysql==0.2.0
alembic==1.11.1
passlib==1.7.4
pydantic==1.10.11 # vysla uz 2.0, ale nejak mi to nefunguje
fastapi==0.115.7
fastapi-jwt[authlib]==0.3.0
bcrypt==4.2.1
fastapi-mail==1.4.2
strawberry-graphql[fastapi]==0.258.0
pillow
exif
aiocache
aiohttp
lxml
gunicorn == 23.0.0
uvicorn[standard] == 0.34.0
sqlalchemy[asyncio] == 2.0.54
aiomysql==0.2.0
alembic==1.14.1
passlib==1.7.4
pydantic==2.10.6
sentry-sdk[fastapi]
pillow==11.1.0
exif==1.6.1
aiocache==0.12.3
aiohttp==3.11.11
lxml==5.3.0
colorlog
+35
View File
@@ -0,0 +1,35 @@
from sqlalchemy import select
from database import models
from database.transaction import get_session
from decorators.retryable import retryable
from external.elevation import elevation_api, update_track_points_elevation
@retryable
async def add_terrain_elevation_to_flight(flight_id: int):
async with get_session() as db:
track_points = (await db.scalars(
select(models.TrackPoint)
.join(models.TrackPoint.track)
.join(models.Track.flight)
.filter(models.Flight.id == flight_id)
.filter(models.TrackPoint.terrain_elevation.is_(None))
)).all()
await update_track_points_elevation(db, track_points)
@retryable
async def add_terrain_elevation_to_photo(photo):
try:
elevation = await elevation_api.get_elevation_for_points(
[{"lat": photo.gps_latitude, "lng": photo.gps_longitude}]
)
if not elevation:
print("Cannot get elevation")
return
terrain_elevation = elevation[0].elevation
async with get_session() as db:
await models.Photo.update(db_session=db, obj=photo, data={"terrain_elevation": terrain_elevation})
except Exception as e:
print(f"Cannot get elevation: {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',
)
+41
View File
@@ -0,0 +1,41 @@
import os.path
from time import time
from database import models
from database.transaction import get_session
from utils.image import PhotoEditor
async def resize_photo(path: str, filename: str, photo_id: int):
editor = PhotoEditor(path, filename)
editor.resize(new_width=2500)
name, _ = os.path.splitext(filename)
editor.write_to_file(quality=95, format_="webp", dest_filename=f"{name}.webp")
editor.write_to_file(quality=80) # toto je potreba pro prvni nacteni nahledu ihned po nahrani, pripadne pro
# vygenerovani nahledu (async, muze se delat pred/behem zmensovani fotky -> v tu dobu jeste neexistuje webp)
width, height = editor.img_size
# TODO: doresit uklid JPGu -> jsou zbytecne
async with get_session() as db:
await models.Photo.update(
db,
id=photo_id,
data={
"width": width,
"height": height,
"filename_extension": "webp",
"cache_key": int(time())
})
async def generate_thumbnail(path: str, filename: str):
editor = PhotoEditor(path, filename)
editor.resize(new_width=300)
name, _ = os.path.splitext(filename)
editor.write_to_file(
quality=85,
dest_path=f"{path}/thumbs",
dest_filename=f"{name}.webp",
format_="webp")
+7
View File
@@ -0,0 +1,7 @@
from emails.emails import Email
from mailer import Mailer
async def send_email(email: Email):
mailer = Mailer()
return await mailer.send(email)
+70
View File
@@ -0,0 +1,70 @@
from datetime import datetime
from typing import Literal, Any
from database import models
from database.transaction import get_session
from external.weather import weather_api
from logger import log
async def get_weather(date_time: datetime | str, gps: tuple[float, float]) -> dict[str, Any] | None:
try:
weather = await weather_api.get_weather_for_hour(date_time.astimezone(), gps=gps)
except Exception as e:
log.error(f"Error in downloading weather: {e}")
return None
return {
"date_time": date_time, # nemuzu pouzit weather['datetime'], protoze to mi vraci jen cele hodiny
"qnh": weather['pressure_msl'],
"temperature_surface": weather['temperature_2m'],
"dewpoint_surface": weather['dewpoint_2m'],
"rain": weather['rain'],
"cloudcover_total": weather['cloudcover'],
"cloudcover_low": weather['cloudcover_low'],
"wind_speed_surface": weather['windspeed_10m'],
"wind_direction_surface": weather['winddirection_10m'],
}
async def download_weather_for_flight(date_time: datetime, flight_id: int, airport_id: int, type_: Literal['landing', 'takeoff']):
async with get_session() as db:
airport = await models.Airport.get_one(db, id=airport_id)
gps = (airport.gps_latitude, airport.gps_longitude)
data = await get_weather(date_time, gps)
async with get_session() as db:
flight = await models.Flight.get_one(db, id=flight_id)
existing_weather_id = getattr(flight, f'{type_}_weather_info_id')
if existing_weather_id:
await models.WeatherInfo.update(db, id=existing_weather_id, data=data)
else:
weather_model = await models.WeatherInfo.create(db, data=data)
await models.Flight.update(db, obj=flight, data={f"{type_}_weather_info_id": weather_model.id})
async def download_weather_for_plan_marker(date_time: datetime, marker_id: int):
async with get_session() as db:
marker = await models.FlightPlanMarker.get_one(db, id=marker_id)
if not marker.gps_latitude or not marker.gps_longitude:
return
gps = (marker.gps_latitude, marker.gps_longitude)
marker_weather_info_id = marker.weather_info_id
data = await get_weather(date_time, gps)
if not data:
return
async with get_session() as db:
if marker_weather_info_id:
await models.WeatherInfo.update(db, id=marker_weather_info_id, data=data)
else:
weather_model = await models.WeatherInfo.create(db, data=data)
await models.FlightPlanMarker.update(
db,
id=marker_id,
data={"weather_info_id": weather_model.id}
)
+12 -2
View File
@@ -1,10 +1,20 @@
import os
APP_DEBUG = True
APP_DEBUG = os.environ.get("APP_ENV", 'production') == 'development'
GRAPHIQL = True
REFRESH_TOKEN_VALIDITY_DAYS = 30
API_URL = os.environ.get("API_URL") or "http://localhost:8000"
APP_SECRET_KEY = os.environ.get("APP_SECRET_KEY") or "test"
ALLOW_CORS_ORIGINS = os.environ.get("ALLOW_CORS_ORIGINS", "").split()
SENTRY_DSN = os.environ.get("SENTRY_DSN")
APP_SECRET_KEY = os.environ.get("APP_SECRET_KEY") or "test"
SMTP_HOST = os.environ.get("SMTP_HOST")
SMTP_PORT = int(os.environ.get("SMTP_PORT") or 0)
SMTP_USERNAME = os.environ.get("SMTP_USERNAME")
SMTP_PASSWORD = os.environ.get("SMTP_PASSWORD")
if not APP_SECRET_KEY:
raise ValueError("Missing APP_SECRET_KEY!")
-299
View File
@@ -1,299 +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
from sqlalchemy.orm import Mapped, relationship, as_declarative, mapped_column
from sqlalchemy.ext.asyncio import AsyncSession
@as_declarative()
class BaseModel:
excluded_columns_in_dict = tuple()
def as_dict(self):
return {
c.name: getattr(self, c.name)
for c in self.__table__.columns
if c.name not in self.excluded_columns_in_dict
}
@classmethod
async def get_one(cls, db_session: AsyncSession, id: int):
return (await db_session.scalars(select(cls).filter_by(id=id))).one()
@classmethod
async def create(cls, db_session: AsyncSession, data: dict):
model = cls(**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)
for key, value in data.items():
if 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),
)
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=False)
gps_longitude: Mapped[float] = mapped_column(Float, nullable=False)
elevation: Mapped[int] = mapped_column(Integer, nullable=True)
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'), nullable=True) # automaticky import nebude mit ID
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
metars: Mapped['Metar'] = relationship(back_populates="airport")
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)
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')
photos: Mapped[List[Photo]] = relationship()
type: Mapped[PointOfInterestType] = relationship()
created_by: Mapped['User'] = relationship()
class Photo(BaseModel):
__tablename__ = "photo"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(128), nullable=False)
filename: Mapped[str] = mapped_column(String(128), nullable=False)
is_flight_cover: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default="0")
description: Mapped[str] = mapped_column(Text, nullable=False)
exposed_at: Mapped[datetime] = mapped_column(DateTime, nullable=True)
gps_latitude: Mapped[float] = mapped_column(Float, nullable=True)
gps_longitude: Mapped[float] = mapped_column(Float, nullable=True)
gps_altitude: Mapped[float] = mapped_column(Float, nullable=True)
point_of_interest_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest.id"), nullable=True)
flight_id: Mapped[int] = mapped_column(Integer, ForeignKey("flight.id"), nullable=False)
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(foreign_keys=[flight_id])
point_of_interest: Mapped['PointOfInterest'] = relationship()
created_by: Mapped['User'] = relationship()
class Aircraft(BaseModel):
__tablename__ = "aircraft"
id: Mapped[int] = mapped_column(primary_key=True)
call_sign: Mapped[str] = mapped_column(String(16), nullable=False)
photo_filename: Mapped[str] = mapped_column(String(128), nullable=True)
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(back_populates="notes")
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(back_populates='organizations', 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=False)
order: Mapped[int] = mapped_column(Integer)
flight: Mapped['Flight'] = relationship()
point_of_interest: Mapped['PointOfInterest'] = relationship()
class WeatherInfo(BaseModel):
__tablename__ = "weather_info"
id: Mapped[int] = mapped_column(primary_key=True)
qnh: Mapped[int] = mapped_column(Integer, nullable=True)
temperature_surface: Mapped[Float] = mapped_column(Float, nullable=True)
dewpoint_surface: Mapped[Float] = mapped_column(Float, nullable=True)
rain: Mapped[Float] = mapped_column(Float, nullable=True)
cloudcover_low: Mapped[Float] = mapped_column(Float, nullable=True)
cloudcover_total: Mapped[Float] = mapped_column(Float, nullable=True)
wind_speed_surface: Mapped[Float] = mapped_column(Float, nullable=True)
wind_direction_surface: Mapped[Float] = mapped_column(Float, nullable=True)
datetime: Mapped[datetime] = mapped_column(DateTime, nullable=False)
class Flight(BaseModel):
__tablename__ = "flight"
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)
takeoff_airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=False)
landing_airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=False)
takeoff_datetime: Mapped[datetime] = mapped_column(DateTime, nullable=False)
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)
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()
copilots: Mapped[List['Copilot']] = relationship(secondary=flight_has_copilot)
aircraft: Mapped['Aircraft'] = relationship(back_populates="flights")
photos: Mapped[List['Photo']] = relationship(foreign_keys=[Photo.flight_id])
created_by: Mapped['User'] = relationship()
class Copilot(BaseModel):
__tablename__ = "copilot"
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')
flights: Mapped[Set['Flight']] = relationship(secondary=flight_has_copilot)
created_by: Mapped['User'] = relationship()
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(back_populates="metars")
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(back_populates="licences")
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)
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)
+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)
+67
View File
@@ -0,0 +1,67 @@
from typing import Type
from sqlalchemy import select, or_, and_
from database import models
class QueryBuilder[ModelType: models.BaseModel]:
def __init__(self, model: Type[ModelType]):
self.model = model
def get_simple_query(
self,
extra_select: list | None = None,
created_by_id: int | None = None,
order_by: list | None = None,
only_public: bool | None = False,
only_my: bool | None = False,
include_others_public: bool | None = False,
url_slug: str | None = None,
include_deleted: bool = False
):
if not extra_select:
extra_select = []
query = select(self.model, *extra_select)
if not include_deleted and hasattr(self.model, "deleted"):
query = query.filter(self.model.deleted.is_(False))
ownership_filters = []
my_filters = []
if only_public and hasattr(self.model, "is_public"):
my_filters.append(self.model.is_public.is_(True))
if hasattr(self.model, "url_slug"):
my_filters.append(self.model.url_slug != '')
if only_my:
if created_by_id and hasattr(self.model, "created_by_id"):
my_filters.append(self.model.created_by_id == created_by_id)
others_filters = []
if include_others_public:
if hasattr(self.model, "is_public"):
others_filters.append(self.model.is_public.is_(True))
if hasattr(self.model, "created_by_id"):
others_filters.append(or_(
self.model.created_by_id != created_by_id,
self.model.created_by_id.is_(None)
))
if my_filters:
ownership_filters.append(and_(*my_filters))
if others_filters:
ownership_filters.append(and_(*others_filters))
query = query.filter(or_(*ownership_filters))
if url_slug is not None and hasattr(self.model, 'url_slug'):
query = query.filter(self.model.url_slug == url_slug)
if order_by:
query = query.order_by(*order_by)
elif hasattr(self.model, "name"):
query = query.order_by(self.model.name)
return query
+18
View File
@@ -0,0 +1,18 @@
from contextlib import asynccontextmanager
from database import async_session
@asynccontextmanager
async def get_session():
async with async_session() as session:
async with session.begin():
try:
yield session
await session.commit()
except Exception as e:
await session.rollback()
print(f"ERROR: {e}")
raise
finally:
session.expunge_all()
await session.close()
-24
View File
@@ -1,24 +0,0 @@
from functools import wraps
from fastapi import HTTPException
from starlette.status import HTTP_401_UNAUTHORIZED
def public_endpoint(func):
pass
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 HTTPException(HTTP_401_UNAUTHORIZED, "Not authorized")
else:
return return_value_unauthorized
return await func(*args, **kwargs)
return decorator
return wrapper
-14
View File
@@ -1,14 +0,0 @@
from functools import wraps
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(f"Not found", 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
-9
View File
@@ -1,9 +0,0 @@
from database import async_session
async def db_session():
async with async_session() as session:
async with session.begin():
yield session
await session.flush()
await session.commit()
-10
View File
@@ -1,10 +0,0 @@
# from fastapi import Depends, Security
# from fastapi_jwt import JwtAuthorizationCredentials
#
#
# async def check_jwt_token():
# authorize.jwt_required()
#
# subject = authorize.get_jwt_subject()
#
# print("Subject: ", subject)
+54
View File
@@ -0,0 +1,54 @@
from typing import Any
from fastapi_mail import MessageSchema, MessageType
from pydantic import EmailStr
class Email(MessageSchema):
template: str
def __init__(
self, to: list[EmailStr | str], subject: str, template: str, template_variables: dict, **data: Any
) -> None:
data['subtype'] = MessageType.html
data['recipients'] = to
data['template'] = template
data['template_body'] = template_variables
data['subject'] = subject + " | Polétání.cz"
super().__init__(**data)
class ContactFormEmail(Email):
def __init__(self, email_from: str, content: str):
super().__init__(
to=["michal@kvacek.cz"],
reply_to=[email_from],
subject="Zpráva z kontaktního formuláře",
template="contact_form.html",
template_variables={
"content": content,
"email_from": email_from
}
)
class WelcomeEmail(Email):
def __init__(self, to: EmailStr | str):
super().__init__(
to=[to],
subject="Vítej",
template="welcome.html",
template_variables={}
)
class ResetPasswordEmail(Email):
def __init__(self, to: EmailStr | str, reset_token: str, validity_hours: int):
super().__init__(
to=[to],
subject="Žádost o reset",
template="reset_password.html",
template_variables={
"token": reset_token,
"validity_hours": validity_hours,
}
)
@@ -0,0 +1,22 @@
<mjml>
<mj-body background-color="#f4f4f4">
<mj-container>
<mj-section>
<mj-column>
<mj-divider border-color="#469ccc"></mj-divider>
<mj-text font-size="20px" color="#469ccc" font-family="Arial, sans-serif" align="center" padding-bottom="20px">
Zpráva z kontaktního formuláře | Polétání.cz
</mj-text>
<mj-text font-size="16px" color="#333333" font-family="Arial, sans-serif" align="left">
Od: {{ email_from | e }}
</mj-text>
<mj-text font-size="16px" color="#333333" font-family="Arial, sans-serif" align="left" line-height="1.5">
{{ content | e }}
</mj-text>
<mj-divider border-color="#469ccc"></mj-divider>
</mj-column>
</mj-section>
</mj-container>
</mj-body>
</mjml>
@@ -0,0 +1,28 @@
<mjml>
<mj-body background-color="#f4f4f4">
<mj-container>
<mj-section>
<mj-column>
<mj-divider border-color="#469ccc"></mj-divider>
<mj-text font-size="20px" color="#469ccc" font-family="Arial, sans-serif" align="center" padding-bottom="20px">
Zapomenuté heslo | Polétání.cz
</mj-text>
<mj-text font-size="16px" color="#333333" font-family="Arial, sans-serif" align="left" line-height="1.5">
<p>
Ahoj,
<br>
Každý občas zapomíná. Heslo je to nejmenší. Klikni na následující tlačítko pro reset hesla.
<br>
</p>
</mj-text>
<mj-button background-color="#469ccc" color="white" font-size="18px" align="center" href="https://poletani.cz/zapomenute-heslo/[login_token]">
Obnovit heslo
</mj-button>
<mj-divider border-color="#469ccc"></mj-divider>
</mj-column>
</mj-section>
</mj-container>
</mj-body>
</mjml>
+46
View File
@@ -0,0 +1,46 @@
<mjml>
<mj-body background-color="#f4f4f4">
<mj-container>
<mj-section>
<mj-column>
<mj-divider border-color="#469ccc"></mj-divider>
<mj-text font-size="20px" color="#469ccc" font-family="Arial, sans-serif" align="center" padding-bottom="20px">
Vítejte v aplikaci Polétání.cz!
</mj-text>
<mj-text font-size="16px" color="#333333" font-family="Arial, sans-serif" align="left" line-height="1.5">
Ahoj!
<br>
<p>
Vítej v aplikaci Polétání.cz – místě, kde můžete sdílet své letecké dobrodružství a fotografie s ostatními nadšenci.
</p>
<p>
Co můžeš očekávat:
</p>
<ul>
<li>Jednoduché nahrávání a sdílení fotografií z vašich letů.</li>
<li>Naplánovat nový výlet pomocí vestavěného plánovače</li>
<li>Objevovat nová místa pro další výlety</li>
</ul>
<p>
Začni nyní a sdílejte své vzdušné zážitky s celým světem!
</p>
<p>
S pozdravem,
<br>
Michal Kváček
</p>
</mj-text>
<mj-button background-color="#469ccc" color="white" font-size="16px" align="center" href="https://poletani.cz/login">
Přihlásit se do Polétání.cz
</mj-button>
<mj-divider border-color="#469ccc"></mj-divider>
</mj-column>
</mj-section>
</mj-container>
</mj-body>
</mjml>
-6
View File
@@ -1,12 +1,6 @@
from fastapi_jwt.jwt import JwtAccess, JwtRefresh
class BaseEndpoint(object):
def __init__(self, db):
self.db = db
super().__init__()
class AuthEndpoint:
def __init__(self, *args, **kwargs):
self.access_security: JwtAccess = kwargs.pop("access_token")
+25
View File
@@ -0,0 +1,25 @@
from fastapi import BackgroundTasks
from pydantic import BaseModel
from background_jobs.send_email import send_email
from emails.emails import ContactFormEmail
class ContactInput(BaseModel):
email_from: str
content: str
class ContactEndpoint():
async def on_post(self, input: ContactInput, backgorund_tasks: BackgroundTasks):
backgorund_tasks.add_task(send_email, ContactFormEmail(
email_from=input.email_from,
content=input.content
))
return {
"status": "success",
"email": {
"from": input.email_from,
"content": input.content
}
}
+81
View File
@@ -0,0 +1,81 @@
import random
import string
from datetime import datetime, timedelta
from fastapi import BackgroundTasks
from passlib.hash import bcrypt
from pydantic import BaseModel
from background_jobs.send_email import send_email
from database import models
from database.transaction import get_session
from emails.emails import ResetPasswordEmail
class ForgottenPasswordRequest(BaseModel):
email: str
class ChangeForgottenPassword(BaseModel):
password: str
token: str
class ForgottenPasswordEndpoint():
@staticmethod
def generate_random_token(length):
characters = string.ascii_letters + string.digits
return ''.join(random.choice(characters) for i in range(length))
@staticmethod
async def token_info(token: str):
async with get_session() as db:
reset = await models.PasswordResetToken.get_one(db, token=token)
user = await models.User.get_one(db, id=reset.user_id)
token_validity = reset.valid_until
email, name = user.email, user.name
return {
"token": token,
"token_validity": token_validity,
"user_email": email,
"user_name": name
}
async def request(self, input: ForgottenPasswordRequest, backgorund_tasks: BackgroundTasks):
validity_hours = 8
validity = datetime.now() + timedelta(hours=validity_hours)
random_str_token = self.generate_random_token(64)
async with get_session() as db:
user = await models.User.get_one(db, email=input.email)
token = await models.PasswordResetToken.create(db, {
"user_id": user.id,
"token": random_str_token,
"valid_until": validity
})
backgorund_tasks.add_task(
send_email,
email=ResetPasswordEmail(
to=input.email,
reset_token=token,
validity_hours=validity_hours
)
)
@staticmethod
async def change_password(input: ChangeForgottenPassword):
async with get_session() as db:
token = await models.PasswordResetToken.get_one(db, token=input.token)
user_id = token.user_id
await models.User.update(
db, id=token.user_id,
data={"password_hashed": bcrypt.hash(input.password)}
)
await db.delete(token)
return {
"user_id": user_id
}
+16 -10
View File
@@ -4,7 +4,8 @@ from passlib.hash import bcrypt
from sqlalchemy import select
from starlette.responses import Response
from database.models import User
from endpoints.base import BaseEndpoint, AuthEndpoint
from database.transaction import get_session
from endpoints.base import AuthEndpoint
from pydantic import BaseModel
@@ -13,21 +14,27 @@ class LoginInput(BaseModel):
password: str
class LoginEndpoint(AuthEndpoint, BaseEndpoint):
class LoginEndpoint(AuthEndpoint):
async def on_post(self, user_data: LoginInput, resp: Response) -> dict:
query = select(User).filter_by(email=user_data.email)
logged_user = (await self.db.scalars(query)).first()
if not logged_user:
raise HTTPException(status_code=401, detail="Invalid user")
async with get_session() as db:
logged_user = (await db.scalars(query)).first()
if not logged_user:
raise HTTPException(status_code=401, detail="Invalid user")
if not bcrypt.verify(user_data.password, logged_user.password_hashed):
user = logged_user.as_dict()
password_hashed = logged_user.password_hashed
if not bcrypt.verify(user_data.password, password_hashed):
raise HTTPException(status_code=401, detail="Bad username or password")
subject = {"id": logged_user.id, "email": logged_user.email}
subject = {"id": user['id'], "email": user['email']}
access_token = self.access_security.create_access_token(subject=subject)
refresh_token = self.refresh_security.create_refresh_token(subject=subject)
# TODO: SameSite na refresh_token_cookie!
# https://medium.com/swlh/how-the-new-chrome-80-cookie-rule-samesite-none-secure-affects-web-development-c06380220ced
# self.access_security.set_access_cookie(resp, access_token)
self.refresh_security.set_refresh_cookie(
resp, refresh_token,
@@ -35,9 +42,8 @@ class LoginEndpoint(AuthEndpoint, BaseEndpoint):
)
return {
"user": logged_user.as_dict(),
"access_token": access_token,
"access_token_validity": self.access_security.access_expires_delta.total_seconds(),
"user": user,
"access_token": access_token
}
+51
View File
@@ -0,0 +1,51 @@
import os.path
from sqlalchemy import select
from starlette.responses import StreamingResponse
from database import models
from database.transaction import get_session
from endpoints.base import AuthEndpoint
from paths import get_photo_basepath
from utils.image import PhotoEditor
class PhotoEditorEndpoint(AuthEndpoint):
@staticmethod
async def show_preview(photo_id: int, **kwargs):
async with get_session() as db:
photo = (await db.scalars(
select(models.Photo)
.filter(models.Photo.id == photo_id)
)).one()
basepath = get_photo_basepath(photo.flight_id)
filename = f"{photo.filename}.{photo.filename_extension}"
original_filename = '_original_' + filename
if os.path.exists(f"{basepath}/{original_filename}"):
filename = original_filename
editor = PhotoEditor(basepath, filename)
editor.resize(new_height=900)
# TODO: idealni je udelat co nejdriv resize
# velikost muze ovlivnit: orez, otoceni, coz jsou dve nejnarocnejsi operace...
# ^^ tohle by bylo fajn cachovat
if kwargs.get("rotate"):
editor.rotate(kwargs['rotate'], True)
crop = {
key.replace("crop_", ""): value
for key, value in kwargs.items()
if key in ('crop_left', 'crop_top', 'crop_width', 'crop_height') and value is not None
}
if crop and len(crop.keys()) == 4:
editor.crop(**crop)
adjustments = {
key: value for key, value in kwargs.items()
if key in ('saturation', 'brightness', 'contrast', 'sharpness') and value is not None
}
if adjustments:
editor.adjust(**adjustments)
return StreamingResponse(content=editor.get_as_stream(), media_type="image/jpeg")
+22 -20
View File
@@ -1,19 +1,20 @@
import re
from fastapi import HTTPException
from fastapi import HTTPException, BackgroundTasks
from sqlalchemy import select
from typing import Optional
from pydantic import BaseModel, root_validator, Field
from pydantic import BaseModel, Field, model_validator
from background_jobs.send_email import send_email
from database.models import User
from endpoints.base import BaseEndpoint
from database.transaction import get_session
from passlib.hash import bcrypt
from emails.emails import WelcomeEmail
class RegistrationInput(BaseModel):
email: str = Field(..., min_length=4)
name: Optional[str]
name: str | None
password: str
@root_validator()
@model_validator(mode="before")
def validate_email(cls, values):
email = values.get("email") or ""
@@ -23,22 +24,23 @@ class RegistrationInput(BaseModel):
return values
class RegistrationEndpoint(BaseEndpoint):
async def on_post(self, user_data: RegistrationInput) -> User:
class RegistrationEndpoint:
@staticmethod
async def on_post(user_data: RegistrationInput, backgorund_tasks: BackgroundTasks) -> User:
query = select(User).filter_by(email=user_data.email)
existing_user = (await self.db.scalars(query)).first()
async with get_session() as db:
existing_user = (await db.scalars(query)).first()
if existing_user:
raise HTTPException(status_code=422, detail="User already exists")
if existing_user:
raise HTTPException(status_code=422, detail="User already exists")
model = await User.create(self.db, {
"name": user_data.name,
"email": user_data.email,
"password_hashed": bcrypt.hash(user_data.password),
"description": ""
})
model = await User.create(db, {
"name": user_data.name,
"email": user_data.email,
"password_hashed": bcrypt.hash(user_data.password),
"description": ""
})
await self.db.commit()
backgorund_tasks.add_task(send_email, WelcomeEmail(to=user_data.email))
return model.as_dict()
return model.as_dict()
+133
View File
@@ -0,0 +1,133 @@
from pydantic import BaseModel
from sqlalchemy import select
from starlette.responses import Response
from database import models
from database.transaction import get_session
class SitemapURL(BaseModel):
url: str
priority: float
class SitemapEndpoint():
def get_static_routes(self):
routes = [
("", 1),
("kontakt", 0.5),
("napoveda", 0.7),
("podminky", 0.5),
("mista", 0.8),
("o-projektu", 0.8),
("osobni-udaje", 0.4),
("funkce", 0.8)
]
return [SitemapURL(url=url, priority=priority) for url, priority in routes]
async def get_public_pilots(self):
async with get_session() as db:
pilots = (await db.scalars(
select(models.User.public_username)
.filter(models.User.public_username != '')
))
return [SitemapURL(url=username, priority=0.8) for username in pilots]
async def get_public_flights(self):
async with get_session() as db:
flights = (await db.execute(
select(models.Flight, models.User.public_username)
.join(models.Flight.created_by)
.filter(models.Flight.is_public.is_(True))
.filter(models.Flight.url_slug != '')
.filter(models.User.public_username != '')
)).fetchall()
return [SitemapURL(
url=f"{pilot_username}/{flight.url_slug}",
priority=0.8
) for flight, pilot_username in flights]
async def get_public_copilots(self):
async with get_session() as db:
copilots = (await db.execute(
select(models.Copilot, models.User.public_username)
.join(models.Copilot.created_by)
.filter(models.Copilot.is_public.is_(True))
.filter(models.Copilot.url_slug != '')
.filter(models.User.public_username != '')
)).fetchall()
return [SitemapURL(
url=f"{pilot_username}/a/{copilot.url_slug}",
priority=0.5
) for copilot, pilot_username in copilots]
async def get_public_aircrafts(self):
async with get_session() as db:
aircrafts = (await db.scalars(
select(models.Aircraft.call_sign)
.filter(models.Aircraft.is_public.is_(True))
)).fetchall()
return [SitemapURL(url=f"letadlo/{call_sign}", priority=0.6) for call_sign in aircrafts]
async def get_public_places(self):
async with get_session() as db:
pois = (await db.scalars(
select(models.PointOfInterest)
.filter(models.PointOfInterest.is_public.is_(True))
.filter(models.PointOfInterest.url_slug != '')
)).fetchall()
return [SitemapURL(url=f"mista/{poi.url_slug}", priority=0.6) for poi in pois]
async def get_public_events(self):
async with get_session() as db:
data = (await db.execute(
select(models.Event, models.User.public_username)
.join(models.Event.created_by)
.filter(models.Event.is_public.is_(True))
.filter(models.User.public_username != '')
))
pilots_with_events = set()
routes = []
for event, pilot_username in data:
if pilot_username not in pilots_with_events:
routes.append(SitemapURL(url=f"{pilot_username}/udalosti", priority=0.7))
pilots_with_events.add(pilot_username)
routes.append(SitemapURL(url=f"{pilot_username}/udalosti/{event.url_slug}", priority=0.7))
return routes
async def on_get(self):
my_sitemap = """<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">"""
sitemap_entries = [
*self.get_static_routes(),
*(await self.get_public_pilots()),
*(await self.get_public_flights()),
*(await self.get_public_aircrafts()),
*(await self.get_public_events()),
*(await self.get_public_places()),
*(await self.get_public_copilots()),
]
# copilots = self.get_public_copilots()
for sitemap_entry in sitemap_entries:
my_sitemap += f"""
<url>
<loc>https://poletani.cz/{sitemap_entry.url}</loc>
<priority>{sitemap_entry.priority}</priority>
</url>
"""
my_sitemap += "</urlset>"
return Response(content=my_sitemap, media_type="application/xml")
+75 -13
View File
@@ -1,25 +1,87 @@
from typing import List, Tuple, Dict
import dataclasses
from typing import Optional
import aiohttp
from database import models
from decorators.retryable import retryable
from utils.list_utils import chunks
@dataclasses.dataclass
class ElevationInfo:
id: int | None
lat: float
lng: float
elevation: float
class ElevationAPI:
ELEVATION_ENDPOINT = "https://api.open-elevation.com/api/v1/lookup"
ELEVATION_ENDPOINT: str
def get_request(self, points: List[Dict[str, float]]):
return {"locations": [{"latitude": point['lat'], "longitude": point['lng']} for point in points]}
def get_request(self, session: aiohttp.ClientSession, points: list[dict[str, float]]):
...
async def call_api(self, points: List[Dict[str, float]]):
@retryable
async def call_api(self, points: list[dict[str, float]]):
async with aiohttp.ClientSession() as session:
async with session.post(self.ELEVATION_ENDPOINT, json=self.get_request(points)) as resp:
async with self.get_request(session, points) as resp:
resp.raise_for_status()
return await resp.json()
async def get_elevation_for_points(self, points: List[Dict[str, float]]) -> List[dict]:
class OpenElevationAPI(ElevationAPI):
ELEVATION_ENDPOINT = "https://api.open-elevation.com/api/v1/lookup"
def get_request(self, session: aiohttp.ClientSession, points: list[dict[str, float]]):
data = {"locations": [{"latitude": point['lat'], "longitude": point['lng']} for point in points]}
return session.post(self.ELEVATION_ENDPOINT, json=data)
async def get_elevation_for_points(self, points: list[dict[str, float]]) -> list[ElevationInfo]:
response = await self.call_api(points)
return [{
"lat": loc['latitude'],
"lng": loc['longitude'],
"elevation": loc['elevation']
} for loc in response['results']]
elevation = []
for i, point in enumerate(points):
response_point = response['results'][i]
elevation.append(ElevationInfo(**point, elevation=response_point['elevation']))
return elevation
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()
+77 -48
View File
@@ -1,9 +1,7 @@
from datetime import datetime
from typing import List, Dict
from aiocache import cached
from collections import defaultdict
from datetime import datetime, timedelta
from typing import List, Dict, Any
from lxml import etree
from lxml.etree import _ElementTree
class GPXParser:
@@ -12,8 +10,55 @@ class GPXParser:
self.gpx = etree.parse(self.file)
self.namespace = None
self.precision_digits = 6
self.max_samples_per_minute = 2
self.set_namespace()
def get_timestamp_key(self, dt: datetime):
ts = round(dt.replace(microsecond=0).timestamp())
return ts - (ts % (60 / self.max_samples_per_minute))
async def group_data(self, data: list):
times = await self.get_times_all()
indexed_data: dict[datetime, Any] = {}
for time, item in zip(times, data):
indexed_data[time] = item
sampled_data = defaultdict(list)
for time, item in indexed_data.items():
key = self.get_timestamp_key(time)
sampled_data[key].append(item)
return sampled_data
async def average_sample_numbers(self, data: list[int | float]):
sampled_data = await self.group_data(data)
return [sum(items) / len(items) for items in sampled_data.values()]
async def sample_times(self, times: List[datetime]):
sampled_times = set()
for time in times:
original_tz = time.tzinfo
sampled_times.add(datetime.fromtimestamp(self.get_timestamp_key(time), tz=original_tz))
times_sorted = list(sampled_times)
times_sorted.sort()
return times_sorted
async def average_coordinates(self, coordinates: list[dict[str, float]]):
sampled_data = await self.group_data(coordinates)
coordinates_aggregated = []
for coordinates_group in sampled_data.values():
lat = [c['lat'] for c in coordinates_group]
lng = [c['lng'] for c in coordinates_group]
coordinates_aggregated.append({
"lat": sum(lat) / len(lat),
"lng": sum(lng) / len(lng),
})
return coordinates_aggregated
def set_namespace(self):
namespace = self.gpx.getroot().nsmap.get(None)
self.namespace = {'gpx': namespace}
@@ -21,73 +66,57 @@ class GPXParser:
def run_xpath(self, path: str):
return self.gpx.xpath(path, namespaces=self.namespace)
@cached()
async def get_times(self):
nodes = self.run_xpath("//gpx:time")
async def get_times_all(self):
nodes = self.run_xpath("//gpx:trkpt/gpx:time")
return [datetime.fromisoformat(node.text).astimezone() for node in nodes]
@cached()
async def get_times(self):
times = await self.get_times_all()
return await self.sample_times(times)
async def get_total_duration(self) -> timedelta:
times = await self.get_times_all()
return times[-1] - times[0]
async def get_coordinates(self) -> List[Dict[str, float]]:
return await self.average_coordinates(await self.get_coordinates_all())
async def get_coordinates_all(self) -> List[Dict[str, float]]:
nodes = self.run_xpath("//gpx:trkpt")
return [{"lat": float(node.attrib["lat"]), "lng": float(node.attrib['lon'])} for node in nodes]
@cached()
async def get_speed(self) -> List[int]:
async def get_speed(self) -> List[float]:
nodes = self.run_xpath("//gpx:speed")
return [int(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[int]:
async def get_magnetic_variation(self) -> List[float]:
nodes = self.run_xpath("//gpx:magvar")
return [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]:
nodes = self.run_xpath("//gpx:ele")
return [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]:
nodes = self.run_xpath("//gpx:terrain_elevation")
return [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):
return max(await self.get_speed())
return max(await self.get_speed()) or 0
async def get_min_speed(self):
return min(await self.get_speed()) or 0
@cached()
async def get_avg_speed(self):
speeds = await self.get_speed()
if not speeds:
return 0
return round(sum(speeds) / len(speeds), 2)
@cached()
async def get_max_altitude(self):
return max(await self.get_altitude())
return max(await self.get_altitude()) or 0
@cached()
async def get_avg_altitude(self):
altitudes = await self.get_altitude()
return round(sum(altitudes) / len(altitudes), 2)
def add_terrain_elevation(self, points_with_elevation: List[Dict[str, float]]):
track_points = self.run_xpath("//gpx:trkpt")
# TODO: open elevation API umi jen presnost na 6 desetinnych mist!
track_points_index = {(float(n.attrib['lat']), float(n.attrib['lon'])): n for n in track_points}
for point_with_elevation in points_with_elevation:
lat = point_with_elevation['lat']
lng = point_with_elevation['lng']
elevation = point_with_elevation['elevation']
target_node = track_points_index.get((lat, lng))
if target_node is None:
continue
extensions = target_node.find("./extensions", self.gpx.getroot().nsmap)
extensions.append(etree.XML(f"<terrain_elevation>{elevation}</terrain_elevation>"))
return self.gpx
def write(self, tree: _ElementTree, output: str):
print(f"ZAPISUJI DO {output}")
tree.write(output)
+281
View File
@@ -0,0 +1,281 @@
import asyncio
import dataclasses
import enum
import math
from pathlib import Path
from typing import Optional
import aiohttp
from sqlalchemy import delete
from database import models
from database.transaction import get_session
from utils.gps import get_distance, get_bearing, EARTH_RADIUS
# https://pyopenair.readthedocs.io/en/latest/openair.html
# http://www.winpilot.com/usersguide/userairspace.asp
class AirspaceType(enum.Enum):
AIRSPACE_A = "A"
AIRSPACE_B = "B"
AIRSPACE_C = "C"
AIRSPACE_D = "D"
AIRSPACE_E = "E"
AIRSPACE_F = "F"
AIRSPACE_G = "G"
ATZ = "ATZ"
PARAGLIDING_ZONE = "PGZ"
TRAGA = "GSEC"
CTR = "CTR"
TMA = "TMA"
TMZ = "TMZ"
FIR = "FIR"
RMZ = "RMZ"
RESTRICTED = "R"
DANGER = "Q"
PROHIBITED = "P"
class ArcDirection(enum.Enum):
CLOCKWISE = "+"
COUNTER_CLOCKWISE = "-"
@dataclasses.dataclass
class Coordinates:
lat: float
lng: float
@dataclasses.dataclass
class Airspace:
type: AirspaceType
info: str = ""
upper_limit: str = None
lower_limit: str = None
center: Optional[Coordinates] = None
radius_nm: float | None = None
bounds: list[Coordinates] = dataclasses.field(default_factory=lambda: [])
class ArcExpander:
@classmethod
def get_arc_coordinates(
cls,
center_point: tuple[float, float],
origin_point: tuple[float, float],
end_point: tuple[float, float]
) -> list[Coordinates]:
initial_bearing = get_bearing(center_point, origin_point)
target_bearing = get_bearing(center_point, end_point)
distance = get_distance(center_point, origin_point)
print(f"DISTANCE {distance} meters, {center_point=}, {origin_point=}, {end_point=}")
def radians_range(start: float, stop: float, step_deg: int = 15):
start_deg = int(math.degrees(start)) % 360
stop_deg = int(math.degrees(stop)) % 360
for i in range(start_deg, stop_deg, step_deg):
yield math.radians(i)
list_bearings = list(radians_range(initial_bearing, target_bearing))
print("RADIANS", list_bearings)
print("\n")
print(f"DEGREES {list(map(math.degrees, list_bearings))}")
print(f"INIT DEGREES {math.degrees(initial_bearing)=}, {math.degrees(target_bearing)=}")
print("==========================================")
coordinates = []
for bearing in list_bearings:
lat1, lon1 = math.radians(center_point[0]), math.radians(center_point[1])
lat2 = math.asin(
math.sin(lat1) * math.cos(distance / EARTH_RADIUS) + math.cos(lat1) * math.sin(distance / EARTH_RADIUS) * math.cos(bearing)
)
lon2 = lon1 + math.atan2(
math.sin(bearing) * math.sin(distance / EARTH_RADIUS) * math.cos(lat1),
math.cos(distance / EARTH_RADIUS) - math.sin(lat1) * math.sin(lat2)
)
lat2, lon2 = math.degrees(lat2), math.degrees(lon2)
coordinates.append(Coordinates(lat2, lon2))
return coordinates
class OpenAirParser:
def __init__(self, file: Path | str):
self.file = None
self.context_type: Optional[AirspaceType] = None
self.context = {}
self.file = file
def save_context(self):
if not self.context_type:
return
data = Airspace(type=self.context_type, **self.context)
self.context_type = None
self.context = {}
return data
def parse(self):
with open(self.file, "r") as file:
for line in file:
stripped_line = line.strip()
if not stripped_line:
if self.context_type:
yield self.save_context()
else:
self.parse_line(stripped_line)
if self.context_type:
yield self.save_context()
@staticmethod
def dms_to_dd(dms, hemisphere):
if '.' in dms:
degrees, minutes = dms.split(':')
seconds = 0
else:
degrees, minutes, seconds = dms.split(":")
decimal = float(degrees) + float(minutes) / 60 + float(seconds) / 3600
if hemisphere in ['S', 'W']:
decimal *= -1
return decimal
def parse_coordinates(self, coordinates: str) -> Coordinates:
parts = coordinates.split()
return Coordinates(
self.dms_to_dd(parts[0], parts[1]),
self.dms_to_dd(parts[2], parts[3])
)
def parse_arcs(self, arcs: str) -> list[Coordinates]:
arc_from, arc_to = arcs.split(",")
if self.context.get('arc_direction', ArcDirection.CLOCKWISE) == ArcDirection.CLOCKWISE:
starting_point = self.parse_coordinates(arc_from)
ending_point = self.parse_coordinates(arc_to)
else:
# souradnice jsou zadane proti smeru hodinovych rucicek -> prohodim souradnice a pocitam jako by byly po smeru
starting_point = self.parse_coordinates(arc_to)
ending_point = self.parse_coordinates(arc_from)
center = self.context['center']
del self.context['center']
if self.context.get('arc_direction'):
del self.context['arc_direction']
# https://stackoverflow.com/questions/62712947/calculate-arc-coordinates-based-on-original-point-end-point-center-distance-a
return ArcExpander.get_arc_coordinates(
(center.lat, center.lng),
(starting_point.lat, starting_point.lng),
(ending_point.lat, ending_point.lng)
)
def parse_line(self, line: str):
tokens = line.split(" ")
keyword = tokens[0]
value = None
if len(tokens) > 1:
value = " ".join(tokens[1:])
match keyword:
case '*':
# comment
pass
case 'AC':
try:
self.context_type = AirspaceType(value)
except ValueError as e:
print(f"Neznam {e}, ignoruji")
return
case 'AN':
if self.context_type == AirspaceType.AIRSPACE_G:
match value[0:2]:
case 'PG':
self.context_type = AirspaceType.PARAGLIDING_ZONE
case 'LK':
self.context_type = AirspaceType.ATZ
case _:
self.context_type = AirspaceType.TRAGA
self.context['info'] = value
case 'AL':
self.context['lower_limit'] = value
case 'AH':
self.context['upper_limit'] = value
case 'DP':
if 'bounds' not in self.context:
self.context['bounds'] = []
self.context['bounds'].append(self.parse_coordinates(value))
case 'V':
match value[0]:
case 'X':
self.context['center'] = self.parse_coordinates(value[2:]) # odstranit X= na zacatku
case 'D':
self.context['arc_direction'] = ArcDirection(value[2:])
case 'DB':
if 'bounds' not in self.context:
self.context['bounds'] = []
self.context['bounds'] = [*self.context['bounds'], *self.parse_arcs(value)]
case 'DC':
self.context['radius_nm'] = float(value)
case _:
print(f"Neznam {keyword}, radek: {line}")
src = "https://www.alus.it/AirspaceConverter/openAIP/cz_asp.txt"
dest = "/app/static/airspaces/cz_asp.txt"
async def download_airspace(source, destination):
async with aiohttp.ClientSession() as session:
async with session.get(source) as response:
with open(destination, "w") as f:
f.write(await response.text(encoding="latin-1"))
async def save_airspace(destination):
parser = OpenAirParser(destination)
async with get_session() as db:
await db.execute(delete(models.Airspace))
for item in parser.parse():
# if 'CTR KARLOVY VARY' not in item.info:
# continue
# print("---" * 10)
# print(len(item.bounds))
# print(item.bounds)
# print("---" * 10)
async with get_session() as db:
await models.Airspace.create(db, {
"country": "cz",
"name": item.info,
"type": item.type.value,
'lower_limit': item.lower_limit,
"upper_limit": item.upper_limit,
"center": [item.center.lat, item.center.lng] if item.center else None,
"radius": item.radius_nm * 1000 * 1.852 if item.radius_nm else None,
"bounds": [(round(i.lat, 6), round(i.lng, 6)) for i in item.bounds]
})
async def refresh_airspaces(src, dest):
await download_airspace(src, dest)
await save_airspace(dest)
asyncio.run(refresh_airspaces(src, dest))
+18 -9
View File
@@ -3,6 +3,7 @@ import urllib.parse
from typing import Tuple, Dict
import aiohttp
from aiocache import cached
from decorators.retryable import retryable
class Weather:
@@ -37,22 +38,30 @@ class Weather:
query_string = urllib.parse.urlencode(params)
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]):
url = self.get_weather_info_url(start_date=date, end_date=date, gps=gps)
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
resp.raise_for_status()
return await resp.json()
try:
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
resp.raise_for_status()
return await resp.json()
except Exception as e:
print(f"Error in downloading weather: {e}, {type(e)}")
async def get_weather_for_hour(self, date_time: datetime.datetime, gps: Tuple[float, float]) -> Dict[str, float|str]:
async def get_weather_for_hour(
self, date_time: datetime.datetime, gps: Tuple[float, float]
) -> Dict[str, float | str]:
data = await self.download_weather_for_day(date_time.date(), gps)
# TODO: kontrola timezone!
# TODO: interpolace - udelat vazenyprumer z dvou po sobe jdoucich hodin
idx = date_time.hour
result_data = {metric: data['hourly'][metric][idx] for metric in self.METRICS}
result_data['datetime'] = datetime.datetime.strptime(data['hourly']['time'][idx], "%Y-%m-%dT%H:%M")
result_data['date_time'] = datetime.datetime.strptime(data['hourly']['time'][idx], "%Y-%m-%dT%H:%M")
return result_data
return result_data
weather_api = Weather()
-13
View File
@@ -1,13 +0,0 @@
import asyncio
from external.elevation import ElevationAPI
from external.gpx_parser import GPXParser
async def test():
elevation_api = ElevationAPI()
parser = GPXParser("./uploads/tracks/37b979cb-71c0-4d09-a2c1-cfdad5f7a0cf-OK-AUR_28_AUR_Bristell_NG5_Zapisnik_letu_2023-08-04-00 00_2023-08-04-12 00(1).gpx")
points = await parser.get_coordinates()
elevation = await elevation_api.get_elevation_for_points(points)
parser.add_terrain_elevation(elevation)
asyncio.run(test())
+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()
)
@@ -1,16 +0,0 @@
from typing import List
from sqlalchemy import select
from strawberry.dataloader import DataLoader
from database import async_session
from database.models import Aircraft
async def load(ids: List[int]):
async with async_session() as session:
models = (await session.scalars(select(Aircraft).filter(Aircraft.id.in_(ids)))).all()
models_by_id = {model.id: model for model in models}
return [models_by_id.get(id_) for id_ in ids]
aircraft_dataloader = DataLoader(load_fn=load, cache=False)
-16
View File
@@ -1,16 +0,0 @@
from typing import List
from sqlalchemy import select
from strawberry.dataloader import DataLoader
from database import async_session
from database.models import Airport
async def load(ids: List[int]):
async with async_session() as session:
models = (await session.scalars(select(Airport).filter(Airport.id.in_(ids)))).all()
models_by_id = {model.id: model for model in models}
return [models_by_id[id_] for id_ in ids]
airport_dataloader = DataLoader(load_fn=load, cache=False)
+121
View File
@@ -0,0 +1,121 @@
from collections import defaultdict
from typing import Type
from logger import log
from database import models, async_session
from database.query_builder import QueryBuilder
class BaseDataloader:
def __init__(
self,
model: Type[models.BaseModel],
relationship_column, filters: list | None = None
):
super().__init__()
self.model = model
self.query_builder = QueryBuilder(self.model)
if relationship_column is None:
relationship_column = model.id
self.relationship_column = relationship_column
if filters is None:
filters = []
self.filters = filters
class SingleModelByIdDataloader(BaseDataloader):
async def load(self, ids: list[int]):
ids_set = {id_ for id_ in set(ids) if id_ is not None}
if not ids_set:
return [None for _ in ids]
async with async_session() as session:
query = (
self.query_builder.get_simple_query(extra_select=[self.relationship_column], include_deleted=True)
.filter(self.relationship_column.in_(ids_set))
.filter(*self.filters)
)
items = (await session.execute(query)).all()
items_by_id = {rel_id: item for item, rel_id in items}
return [items_by_id.get(id_) for id_ in ids]
class MultiModelsDataloader(BaseDataloader):
def __init__(
self,
model: Type[models.BaseModel],
relationship_column=None,
filters: list | None = None,
extra_select: list | None = None,
extra_join: list | None = None,
order_by: list | None = None,
):
super().__init__(model, relationship_column, filters)
if extra_join is None:
extra_join = []
self.extra_join = extra_join
if extra_select is None:
extra_select = []
self.extra_select = extra_select
if order_by is None:
order_by = [model.id.desc()] # defaultne radit od nejnovejsich zaznamu
self.order_by = order_by
def get_query(self, ids: set[int]):
query = (
self.query_builder.get_simple_query(
extra_select=[self.relationship_column] + self.extra_select,
order_by=self.order_by
)
.filter(self.relationship_column.in_(ids))
.filter(*self.filters)
)
for joined_table in self.extra_join:
query = query.join(joined_table)
if self.filters:
query = query.filter(*self.filters)
return query
async def load(self, ids: list[int]) -> list:
ids_set = {id_ for id_ in set(ids) if id_ is not None}
if not ids_set:
return [[] for _ in ids]
query = self.get_query(ids_set)
async with async_session() as db:
data = (await db.execute(query)).all()
result_data = self.process_data(data)
return [result_data[id_] for id_ in ids]
def process_data(self, data):
result_data = defaultdict(list)
for row in data:
item, rel_id = row[0:2]
extra = row[2:]
if extra:
log.warning(f"Override function process_data, extra params={extra} are going to be discarded!")
result_data[rel_id].append(item)
return result_data
class FlightCopilotDataloader(MultiModelsDataloader):
def process_data(self, data):
result_data = defaultdict(list)
for row in data:
item, rel_id = row[0:2]
token = row[2]
item.token = token
result_data[rel_id].append(item)
return result_data
@@ -1,24 +0,0 @@
from collections import defaultdict
from typing import List
from sqlalchemy import select
from strawberry.dataloader import DataLoader
from database import async_session
from database.models import Copilot, Flight
async def load(ids: List[int]):
async with async_session() as session:
models = (await session.execute(
select(Copilot, Flight.id)
.join(Copilot.flights)
.filter(Flight.id.in_(ids))
)).all()
copilots_by_flight_id = defaultdict(list)
for copilot, flight_id in models:
copilots_by_flight_id[flight_id].append(copilot)
return [copilots_by_flight_id[id_] for id_ in ids]
flight_copilots_dataloader = DataLoader(load_fn=load, cache=False)
-40
View File
@@ -1,40 +0,0 @@
from collections import defaultdict
from typing import List, Optional
from sqlalchemy import select
from strawberry.dataloader import DataLoader
from database import async_session
from database.models import Flight, Copilot, PointOfInterest
class FlightsLoader:
def __init__(self, relationship_column, extra_join: Optional[list] = None):
if extra_join is None:
extra_join = []
self.relationship_column = relationship_column
self.extra_join = extra_join
async def load(self, ids: List[int]):
async with async_session() as session:
rel_column = self.relationship_column
query = (
select(Flight, rel_column)
.filter(rel_column.in_(ids))
)
for table in self.extra_join:
query = query.join(table)
data = (await session.execute(query)).all()
result_data = defaultdict(list)
for item, rel_id in data:
result_data[rel_id].append(item)
return [result_data[id_] for id_ in ids]
flights_by_copilot_dataloader = DataLoader(load_fn=FlightsLoader(Copilot.id, extra_join=[Flight.copilots]).load, cache=False)
flights_by_aircraft_dataloader = DataLoader(load_fn=FlightsLoader(Flight.aircraft_id).load, cache=False)
flight_by_poi_dataloader = DataLoader(load_fn=FlightsLoader(PointOfInterest.id, extra_join=[Flight.track, PointOfInterest]).load, cache=False)
@@ -0,0 +1,24 @@
from typing import List
from sqlalchemy import select, func
from strawberry.dataloader import DataLoader
from database import async_session, models
async def load_flight_durations(ids: List[int]):
async with async_session() as db:
flights = (await db.execute(
select(
models.Flight.id,
func.timediff(models.Flight.landing_datetime, models.Flight.takeoff_datetime).label("diff"),
func.coalesce(func.sum(models.FlightTurnPoint.landing_duration), 0).label("landing_duration")
).join(models.Flight.turn_points, isouter=True)
.group_by(models.Flight.id)
.filter(models.Flight.id.in_(ids))
)).all()
items_by_id = {item.id: item.diff.seconds // 60 - item.landing_duration for item in flights}
return [items_by_id.get(id_) for id_ in ids]
flight_duration_dataloader = DataLoader(load_fn=load_flight_durations, cache=False)
@@ -0,0 +1,212 @@
from strawberry.dataloader import DataLoader
from database import models
from graphql_schema.dataloaders.base import MultiModelsDataloader, FlightCopilotDataloader
aircrafts_from_organization_dataloader = DataLoader(
load_fn=MultiModelsDataloader(
models.Aircraft,
relationship_column=models.Organization.id,
extra_join=[models.Aircraft.organization]
).load,
cache=False
)
flight_copilots_dataloader = DataLoader(
load_fn=FlightCopilotDataloader(
models.Copilot,
relationship_column=models.Flight.id,
extra_select=[models.flight_has_copilot.c.token],
filters=[
models.flight_has_copilot.c.flight_id == models.Flight.id,
models.flight_has_copilot.c.copilot_id == models.Copilot.id,
],
extra_join=[models.Copilot.flights]
).load,
cache=False)
flight_plan_copilots_dataloader = DataLoader(
load_fn=FlightCopilotDataloader(
models.Copilot,
relationship_column=models.FlightPlan.id,
extra_select=[models.flight_plan_has_copilot.c.token],
# filters=[
# models.flight_plan_has_copilot.c.flight_plan_id == models.FlightPlan.id,
# models.flight_plan_has_copilot.c.copilot_id == models.Copilot.id,
# ],
extra_join=[models.Copilot.flight_plans]
).load,
cache=False
)
reporting_points_dataloader = DataLoader(
load_fn=MultiModelsDataloader(
models.ReportingPoint,
relationship_column=models.airport_has_reporting_point.c.airport_id,
extra_join=[models.airport_has_reporting_point]
).load, cache=False
)
runways_dataloader = DataLoader(
load_fn=MultiModelsDataloader(
models.Runway,
relationship_column=models.Airport.id,
extra_join=[models.Airport.runways],
order_by=[models.Runway.designator]
).load, cache=False
)
frequencies_dataloader = DataLoader(
load_fn=MultiModelsDataloader(
models.Frequency,
relationship_column=models.Airport.id,
extra_join=[models.Airport.frequencies],
order_by=[models.Frequency.frequency]
).load, cache=False
)
flights_by_copilot_dataloader = DataLoader(
load_fn=MultiModelsDataloader(
models.Flight,
relationship_column=models.Copilot.id,
extra_join=[models.Flight.copilots],
order_by=[models.Flight.takeoff_datetime.desc()]
).load,
cache=False
)
public_flights_by_copilot_dataloader = DataLoader(
load_fn=MultiModelsDataloader(
models.Flight,
relationship_column=models.Copilot.id,
extra_join=[models.Flight.copilots],
filters=[models.Flight.is_public.is_(True)],
order_by=[models.Flight.takeoff_datetime.desc()]
).load,
cache=False
)
flights_by_aircraft_dataloader = DataLoader(
load_fn=MultiModelsDataloader(
models.Flight,
relationship_column=models.Flight.aircraft_id,
order_by=[models.Flight.takeoff_datetime.desc()]
).load, cache=False)
flight_by_poi_dataloader = DataLoader(
load_fn=MultiModelsDataloader(
models.Flight,
relationship_column=models.PointOfInterest.id,
order_by=[models.Flight.takeoff_datetime.desc()],
extra_join=[models.Flight.turn_points, models.PointOfInterest]
).load,
cache=False
)
flights_by_event_dataloader = DataLoader(
load_fn=MultiModelsDataloader(
models.Flight,
relationship_column=models.Event.id,
extra_join=[models.Flight.event],
order_by=[models.Flight.takeoff_datetime.desc()]
).load,
cache=False
)
public_flights_by_event_dataloader = DataLoader(
load_fn=MultiModelsDataloader(
models.Flight,
relationship_column=models.Event.id,
filters=[models.Flight.is_public.is_(True)],
order_by=[models.Flight.takeoff_datetime.desc()],
extra_join=[models.Flight.event]
).load,
cache=False
)
user_organizations_dataloader = DataLoader(load_fn=MultiModelsDataloader(
models.Organization,
relationship_column=models.user_is_in_organization.c.user_id,
extra_join=[models.user_is_in_organization]
).load, cache=False)
users_in_organization_dataloader = DataLoader(
load_fn=MultiModelsDataloader(
models.User,
relationship_column=models.Organization.id,
extra_join=[models.Organization.users]
).load,
cache=False
)
photos_dataloader = DataLoader(
load_fn=MultiModelsDataloader(
models.Photo,
relationship_column=models.Photo.flight_id,
order_by=[models.Photo.exposed_at]
).load,
cache=False
)
poi_photos_dataloader = DataLoader(
load_fn=MultiModelsDataloader(
models.Photo,
relationship_column=models.Photo.point_of_interest_id,
order_by=[models.Photo.exposed_at]
).load,
cache=False
)
flight_turn_points_dataloader = DataLoader(
load_fn=MultiModelsDataloader(
models.FlightTurnPoint,
relationship_column=models.FlightTurnPoint.flight_id,
order_by=[models.FlightTurnPoint.order]
).load,
cache=False
)
track_points_dataloder = DataLoader(
load_fn=MultiModelsDataloader(
models.TrackPoint,
relationship_column=models.TrackPoint.track_id,
order_by=[models.TrackPoint.timestamp, models.TrackPoint.id]
).load,
cache=False
)
copilots_in_photo_dataloader = DataLoader(
load_fn=MultiModelsDataloader(
models.Copilot,
relationship_column=models.copilot_has_photo.c.photo_id,
extra_join=[models.copilot_has_photo],
order_by=[models.Copilot.name]
).load,
cache=False
)
photo_copilots_dataloader = DataLoader(
load_fn=MultiModelsDataloader(
models.Photo,
relationship_column=models.copilot_has_photo.c.copilot_id,
extra_join=[models.copilot_has_photo],
order_by=[models.Photo.exposed_at]
).load,
cache=False
)
photos_aircraft_dataloader = DataLoader(
load_fn=MultiModelsDataloader(
models.Photo,
relationship_column=models.Photo.aircraft_id,
order_by=[models.Photo.exposed_at]
).load,
cache=False
)
flight_plan_markers_dataloader = DataLoader(
load_fn=MultiModelsDataloader(
models.FlightPlanMarker,
relationship_column=models.FlightPlanMarker.flight_plan_id,
order_by=[models.FlightPlanMarker.position, models.FlightPlanMarker.id]
).load,
cache=False
)

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