diff --git a/.flake8 b/.flake8 index 49af690..089a8e0 100644 --- a/.flake8 +++ b/.flake8 @@ -2,5 +2,6 @@ max-line-length = 120 exclude = venv + .venv __pycache__ - alembic \ No newline at end of file + alembic diff --git a/alembic/versions/20250916-092604_prejmenovani_date_time_u_weatherinfo_db036549f318.py b/alembic/versions/20250916-092604_prejmenovani_date_time_u_weatherinfo_db036549f318.py new file mode 100644 index 0000000..08948ef --- /dev/null +++ b/alembic/versions/20250916-092604_prejmenovani_date_time_u_weatherinfo_db036549f318.py @@ -0,0 +1,35 @@ +"""prejmenovani date_time u WeatherInfo + +Revision ID: db036549f318 +Revises: e3620deb41b6 +Create Date: 2025-09-16 09:26:04.082396 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = 'db036549f318' +down_revision = 'e3620deb41b6' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_unique_constraint(None, 'airport', ['openaip_id']) + op.create_index(op.f('ix_flight_turn_point_order'), 'flight_turn_point', ['order'], unique=False) + op.alter_column( + "weather_info", "datetime", new_column_name="date_time", existing_type=sa.DateTime, nullable=False + ) + + +def downgrade() -> None: + op.alter_column( + "weather_info", "date_time", new_column_name="datetime", + existing_type=sa.DateTime, nullable=False + ) + + op.drop_index(op.f('ix_flight_turn_point_order'), table_name='flight_turn_point') + op.drop_constraint(None, 'airport', type_='unique') + # ### end Alembic commands ### diff --git a/config/gunicorn.py b/config/gunicorn.py index dc8bf52..0335d0d 100644 --- a/config/gunicorn.py +++ b/config/gunicorn.py @@ -2,7 +2,7 @@ import os # https://docs.gunicorn.org/en/stable/settings.html#settings -workers = os.cpu_count() +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 diff --git a/docker-compose.yml b/docker-compose.yml index 18b4daa..8889a6a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,6 +5,9 @@ services: - 3306:3306 env_file: - .env + environment: + MARIADB_ROOT_PASSWORD: "rootroot" + volumes: # - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro - db-data:/var/lib/mysql:rw diff --git a/src/background_jobs/weather.py b/src/background_jobs/weather.py index bcb33f5..bbfb5eb 100644 --- a/src/background_jobs/weather.py +++ b/src/background_jobs/weather.py @@ -1,12 +1,12 @@ from datetime import datetime -from typing import Literal +from typing import Literal, Any from database import models from database.transaction import get_session from external.weather import weather_api from logger import log -async def get_weather(date_time: datetime, gps: tuple[float, float]): +async def get_weather(date_time: datetime | str, gps: tuple[float, float]) -> dict[str, Any] | None: try: weather = await weather_api.get_weather_for_hour(date_time.astimezone(), gps=gps) except Exception as e: @@ -14,7 +14,7 @@ async def get_weather(date_time: datetime, gps: tuple[float, float]): return None return { - "datetime": date_time, # nemuzu pouzit weather['datetime'], protoze to mi vraci jen cele hodiny + "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'], diff --git a/src/database/models.py b/src/database/models.py deleted file mode 100644 index 475b663..0000000 --- a/src/database/models.py +++ /dev/null @@ -1,482 +0,0 @@ -from __future__ import annotations -import datetime -from typing import Set, List -from sqlalchemy import String, DateTime, ForeignKey, Text, Integer, func, Table, Column, Boolean, select, Float, Enum -from sqlalchemy.dialects.mysql import JSON -from sqlalchemy.orm import Mapped, relationship, mapped_column, DeclarativeBase -from sqlalchemy.ext.asyncio import AsyncSession - - -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 - - -user_is_in_organization = Table( - "user_is_in_organization", - BaseModel.metadata, - Column("user_id", Integer, ForeignKey("user.id"), primary_key=True), - Column("organization_id", Integer, ForeignKey("organization.id"), primary_key=True) -) - -flight_has_copilot = Table( - "flight_has_copilot", - BaseModel.metadata, - Column("flight_id", ForeignKey("flight.id"), primary_key=True), - Column("copilot_id", ForeignKey("copilot.id"), primary_key=True), - Column("token", String(64), unique=True) -) - -flight_plan_has_copilot = Table( - "flight_plan_has_copilot", - BaseModel.metadata, - Column("flight_plan_id", ForeignKey("flight_plan.id"), primary_key=True), - Column("copilot_id", ForeignKey("copilot.id"), primary_key=True), - Column("token", String(64), unique=True) -) - -copilot_has_photo = Table( - "copilot_has_photo", - BaseModel.metadata, - Column("copilot_id", ForeignKey("copilot.id"), primary_key=True), - Column("photo_id", ForeignKey("photo.id"), primary_key=True), -) - -airport_has_reporting_point = Table( - "airport_has_reporting_point", - BaseModel.metadata, - Column("airport_id", ForeignKey("airport.id"), primary_key=True), - Column("reporting_point_id", ForeignKey("reporting_point.id"), primary_key=True), -) - - -class Airspace(BaseModel): - __tablename__ = "airspace" - - id: Mapped[int] = mapped_column(primary_key=True) - country: Mapped[str] = mapped_column(String(8), nullable=False, index=True) - name: Mapped[str] = mapped_column(String(128), nullable=False, unique=True) - type: Mapped[str] = mapped_column(String(8), nullable=False, index=True) - lower_limit: Mapped[str] = mapped_column(String(32), nullable=False) - lower_alt_unit: Mapped[str] = mapped_column(String(8), nullable=False) - upper_limit: Mapped[str] = mapped_column(String(32), nullable=False) - upper_alt_unit: Mapped[str] = mapped_column(String(8), nullable=False) - bounds: Mapped[list[tuple[float, float]]] = mapped_column(JSON()) - - -class FlightPlan(BaseModel): - __tablename__ = "flight_plan" - - id: Mapped[int] = mapped_column(primary_key=True) - name: Mapped[str] = mapped_column(String(256), nullable=False) - is_default_name: Mapped[bool] = mapped_column(Boolean, server_default='0') - is_public: Mapped[bool] = mapped_column(Boolean, server_default='0') - is_editable_by_copilot: Mapped[bool] = mapped_column(Boolean, server_default='0') - track_id: Mapped[int] = mapped_column(Integer, ForeignKey('track.id'), nullable=True) - description: Mapped[str] = mapped_column(Text, nullable=False, server_default="") - planned_speed: Mapped[int] = mapped_column(Integer, nullable=False, default=150) - planned_takeoff_datetime: Mapped[datetime] = mapped_column(DateTime, nullable=True) - aircraft_id: Mapped[int] = mapped_column(Integer, ForeignKey("aircraft.id"), nullable=True) - created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id')) - created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) - deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0') - - copilots: Mapped[List['Copilot']] = relationship(secondary=flight_plan_has_copilot) - aircraft: Mapped['Aircraft'] = relationship() - markers: Mapped[List['FlightPlanMarker']] = relationship() - created_by: Mapped['User'] = relationship() - - - -class Track(BaseModel): - __tablename__ = "track" - - id: Mapped[int] = mapped_column(primary_key=True) - bounds: Mapped[list[tuple[float, float]]] = mapped_column(JSON()) - min_speed: Mapped[float] = mapped_column(Float, nullable=True) - max_speed: Mapped[float] = mapped_column(Float, nullable=True) - avg_speed: Mapped[float] = mapped_column(Float, nullable=True) - max_altitude: Mapped[float] = mapped_column(Float, nullable=True) - avg_altitude: Mapped[float] = mapped_column(Float, nullable=True) - total_duration: Mapped[int] = mapped_column(Integer, nullable=True, comment="Total duration in seconds") - created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id')) - created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) - - flight: Mapped['Flight'] = relationship() - 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() - - -class FlightPlanMarker(BaseModel): - __tablename__ = "flight_plan_marker" - - id: Mapped[int] = mapped_column(primary_key=True) - flight_plan_id: Mapped[int] = mapped_column(Integer, ForeignKey('flight_plan.id')) - airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=True) - point_of_interest_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest.id"), nullable=True) - weather_info_id: Mapped[int] = mapped_column(Integer, ForeignKey('weather_info.id'), nullable=True) - name: Mapped[str] = mapped_column(String(128), nullable=False) - type: Mapped[str] = mapped_column(String(64), nullable=False) - position: Mapped[int] = mapped_column(Integer, nullable=False, default=0) - gps_latitude: Mapped[float] = mapped_column(Float, nullable=False) - gps_longitude: Mapped[float] = mapped_column(Float, nullable=False) - - - - -class Airport(BaseModel): - __tablename__ = "airport" - - id: Mapped[int] = mapped_column(primary_key=True) - name: Mapped[str] = mapped_column(String(128), nullable=False) - openaip_id: Mapped[str] = mapped_column(String(32), nullable=True, unique=True) - icao_code: Mapped[str] = mapped_column(String(8), nullable=False) - gps_latitude: Mapped[float] = mapped_column(Float, nullable=True) - gps_longitude: Mapped[float] = mapped_column(Float, nullable=True) - elevation: Mapped[int] = mapped_column(Integer, nullable=True) - airport_type: Mapped[str] = mapped_column(Enum("airport", "ull", "heliport"), nullable=False, server_default='airport') # noqa - use_in_gpx_guess: Mapped[bool] = mapped_column(Boolean, server_default='1') - created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) - created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'), nullable=True) # automaticky import nebude mit ID # noqa - - runways: Mapped[list['Runway']] = relationship('Runway') - frequencies: Mapped[list['Frequency']] = relationship() - created_by: Mapped['User'] = relationship() - - -class Runway(BaseModel): - __tablename__ = "runway" - - id: Mapped[int] = mapped_column(primary_key=True) - airport_id: Mapped[int] = mapped_column(Integer, ForeignKey('airport.id')) - heading: Mapped[int] = mapped_column(Integer, nullable=False) - designator: Mapped[str] = mapped_column(String(4), nullable=False) - surface: Mapped[str] = mapped_column(Enum("asphalt", "concrete", "grass", "other"), nullable=False, server_default="other") # noqa - length: Mapped[int] = mapped_column(Integer, nullable=True) - width: Mapped[int] = mapped_column(Integer, nullable=True) - - -class Frequency(BaseModel): - __tablename__ = "frequency" - - id: Mapped[int] = mapped_column(primary_key=True) - airport_id: Mapped[int] = mapped_column(Integer, ForeignKey('airport.id'), nullable=True) - airspace_id: Mapped[int] = mapped_column(Integer, ForeignKey('airspace.id'), nullable=True) - frequency: Mapped[str] = mapped_column(String(7), nullable=False) - name: Mapped[str] = mapped_column(String(64), nullable=False) - - -class ReportingPoint(BaseModel): - __tablename__ = "reporting_point" - - id: Mapped[int] = mapped_column(primary_key=True) - openaip_id: Mapped[str] = mapped_column(String(32), nullable=False) - name: Mapped[str] = mapped_column(String(64), nullable=False) - gps_latitude: Mapped[float] = mapped_column(Float, nullable=True) - gps_longitude: Mapped[float] = mapped_column(Float, nullable=True) - - -class PointOfInterestType(BaseModel): - __tablename__ = "point_of_interest_type" - - id: Mapped[int] = mapped_column(primary_key=True) - name: Mapped[str] = mapped_column(String(128), nullable=False) - icon: Mapped[str] = mapped_column(String(128), nullable=False, server_default='marker') - is_public: Mapped[bool] = mapped_column(Boolean, server_default='0') - created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id')) - created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) - deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0') - - created_by: Mapped['User'] = relationship() - points_of_interest: Mapped[List[PointOfInterest]] = relationship() - - -class PointOfInterest(BaseModel): - __tablename__ = "point_of_interest" - - id: Mapped[int] = mapped_column(primary_key=True) - name: Mapped[str] = mapped_column(String(128), nullable=False) - description: Mapped[str] = mapped_column(Text, nullable=False, server_default="") - url_slug: Mapped[str] = mapped_column(String(128), nullable=False, server_default="") - title_photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id'), nullable=True) - gps_latitude: Mapped[float] = mapped_column(Float, nullable=True) - gps_longitude: Mapped[float] = mapped_column(Float, nullable=True) - type_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest_type.id"), nullable=True) - is_public: Mapped[bool] = mapped_column(Boolean, server_default='0') - created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id')) - created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) - deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0') - - type: Mapped[PointOfInterestType] = relationship() - created_by: Mapped['User'] = relationship() - title_photo: Mapped['Photo'] = relationship(foreign_keys=[title_photo_id]) - - -class Photo(BaseModel): - __tablename__ = "photo" - - id: Mapped[int] = mapped_column(primary_key=True) - name: Mapped[str] = mapped_column(String(128), nullable=False, server_default="") - description: Mapped[str] = mapped_column(Text, nullable=False, server_default="") - filename: Mapped[str] = mapped_column(String(128), nullable=False) - filename_extension: Mapped[str] = mapped_column(String(4), nullable=False) - cache_key: Mapped[str] = mapped_column(String(128), nullable=True) - width: Mapped[int] = mapped_column(Integer, nullable=False) - height: Mapped[int] = mapped_column(Integer, nullable=False) - exposed_at: Mapped[datetime] = mapped_column(DateTime, nullable=True) - gps_latitude: Mapped[float] = mapped_column(Float, nullable=True) - gps_longitude: Mapped[float] = mapped_column(Float, nullable=True) - gps_altitude: Mapped[float] = mapped_column(Float, nullable=True) - terrain_elevation: Mapped[float] = mapped_column(Float, nullable=True) - aircraft_id: Mapped[int] = mapped_column(Integer, ForeignKey("aircraft.id"), nullable=True) - point_of_interest_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest.id"), nullable=True) - flight_id: Mapped[int] = mapped_column(Integer, ForeignKey("flight.id"), nullable=False) - created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id')) - created_by_copilot_id: Mapped[int] = mapped_column(Integer, ForeignKey('copilot.id'), nullable=True) - created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) - - flight: Mapped['Flight'] = relationship(foreign_keys=[flight_id]) - point_of_interest: Mapped['PointOfInterest'] = relationship(foreign_keys=[point_of_interest_id]) - adjustment: Mapped['PhotoAdjustment'] = relationship(passive_deletes=True) - created_by: Mapped['User'] = relationship() - aircraft: Mapped['Aircraft'] = relationship(foreign_keys=[aircraft_id]) - copilots: Mapped[List['Copilot']] = relationship(secondary=copilot_has_photo) - - -class PhotoAdjustment(BaseModel): - __tablename__ = "photo_adjustment" - - id: Mapped[int] = mapped_column(primary_key=True) - photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id', ondelete='CASCADE'), nullable=False) - rotate: Mapped[float] = mapped_column(Float, nullable=True) - contrast: Mapped[float] = mapped_column(Float, nullable=True) - brightness: Mapped[float] = mapped_column(Float, nullable=True) - saturation: Mapped[float] = mapped_column(Float, nullable=True) - sharpness: Mapped[float] = mapped_column(Float, nullable=True) - - crop_left: Mapped[float] = mapped_column(Float, nullable=True) - crop_top: Mapped[float] = mapped_column(Float, nullable=True) - crop_width: Mapped[float] = mapped_column(Float, nullable=True) - crop_height: Mapped[float] = mapped_column(Float, nullable=True) - - updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) - photo: Mapped['Photo'] = relationship() - - -class Aircraft(BaseModel): - __tablename__ = "aircraft" - - id: Mapped[int] = mapped_column(primary_key=True) - call_sign: Mapped[str] = mapped_column(String(16), nullable=False) - title_photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id'), nullable=True) - is_public: Mapped[bool] = mapped_column(Boolean, server_default='0') - manufacturer: Mapped[str] = mapped_column(Text, nullable=False, server_default="") - model: Mapped[str] = mapped_column(String(30), nullable=False, server_default="") - seats: Mapped[str] = mapped_column(Integer, nullable=False) - description: Mapped[str] = mapped_column(Text, nullable=False, server_default="") - organization_id: Mapped[int] = mapped_column(Integer, ForeignKey('organization.id'), nullable=True) - created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'), nullable=False) - created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) - deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0') - - organization: Mapped['Organization'] = relationship() - flights: Mapped[Set['Flight']] = relationship() - created_by: Mapped['User'] = relationship() - - -class Organization(BaseModel): - __tablename__ = "organization" - - id: Mapped[int] = mapped_column(primary_key=True) - name: Mapped[str] = mapped_column(String(128), nullable=False) - created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id')) - created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) - deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0') - - users: Mapped[Set['User']] = relationship(secondary=user_is_in_organization) - created_by: Mapped['User'] = relationship() - - -class FlightTurnPoint(BaseModel): - __tablename__ = "flight_turn_point" - - id: Mapped[int] = mapped_column(primary_key=True) - flight_id: Mapped[int] = mapped_column(Integer, ForeignKey("flight.id"), nullable=False) - point_of_interest_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest.id"), nullable=True) - airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=True) - landing_duration: Mapped[int] = mapped_column(Integer, nullable=True) - order: Mapped[int] = mapped_column(Integer, index=True) - - flight: Mapped['Flight'] = relationship() - point_of_interest: Mapped['PointOfInterest'] = relationship() - airport: Mapped['Airport'] = relationship() - - -class WeatherInfo(BaseModel): - __tablename__ = "weather_info" - - id: Mapped[int] = mapped_column(primary_key=True) - qnh: Mapped[int] = mapped_column(Integer, nullable=True) - temperature_surface: Mapped[Float] = mapped_column(Float, nullable=True) - dewpoint_surface: Mapped[Float] = mapped_column(Float, nullable=True) - rain: Mapped[Float] = mapped_column(Float, nullable=True) - cloudcover_low: Mapped[Float] = mapped_column(Float, nullable=True) - cloudcover_total: Mapped[Float] = mapped_column(Float, nullable=True) - wind_speed_surface: Mapped[Float] = mapped_column(Float, nullable=True) - wind_direction_surface: Mapped[Float] = mapped_column(Float, nullable=True) - datetime: Mapped[datetime] = mapped_column(DateTime, nullable=False) - - -class Event(BaseModel): - __tablename__ = "event" - - id: Mapped[int] = mapped_column(primary_key=True) - name: Mapped[str] = mapped_column(String(128), nullable=False) - description: Mapped[str] = mapped_column(Text, nullable=False, server_default="") - url_slug: Mapped[str] = mapped_column(String(128), nullable=False, server_default="") - date_from: Mapped[datetime] = mapped_column(DateTime, nullable=True) - date_to: Mapped[datetime] = mapped_column(DateTime, nullable=True) - organization_id: Mapped[int] = mapped_column(Integer, ForeignKey('organization.id'), nullable=True) - is_public: Mapped[bool] = mapped_column(Boolean, server_default='0') - created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id')) - created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) - deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0') - - organization: Mapped['Organization'] = relationship() - created_by: Mapped['User'] = relationship() - - -class Flight(BaseModel): - __tablename__ = "flight" - - id: Mapped[int] = mapped_column(primary_key=True) - name: Mapped[str] = mapped_column(String(128), nullable=False, server_default="") - description: Mapped[str] = mapped_column(Text, nullable=False, server_default="") - debriefing_notes: Mapped[str] = mapped_column(Text, nullable=False, server_default="") - url_slug: Mapped[str] = mapped_column(String(128), nullable=False, server_default="") - event_id: Mapped[int] = mapped_column(Integer, ForeignKey("event.id"), nullable=True) - title_photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id'), nullable=True) - takeoff_airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=True) - landing_airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=True) - takeoff_datetime: Mapped[datetime] = mapped_column(DateTime, nullable=False) - landing_datetime: Mapped[datetime] = mapped_column(DateTime, nullable=False) - duration_total: Mapped[int] = mapped_column(Integer, nullable=True) - duration_pic: Mapped[int] = mapped_column(Integer, nullable=True) - flight_plan_id: Mapped[int] = mapped_column(Integer, ForeignKey('flight_plan.id'), nullable=True) - track_id: Mapped[int] = mapped_column(Integer, ForeignKey('track.id'), nullable=True) - gpx_track_filename: Mapped[str] = mapped_column(String(128), nullable=True) # deprecated - has_terrain_elevation: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) # deprecated - aircraft_id: Mapped[int] = mapped_column(Integer, ForeignKey('aircraft.id')) - takeoff_weather_info_id: Mapped[int] = mapped_column(Integer, ForeignKey('weather_info.id'), nullable=True) - landing_weather_info_id: Mapped[int] = mapped_column(Integer, ForeignKey('weather_info.id'), nullable=True) - landings: Mapped[int] = mapped_column(Integer, default=1) - is_public: Mapped[bool] = mapped_column(Boolean, server_default='0') - created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id')) - created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) - deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0') - - takeoff_airport: Mapped['Airport'] = relationship(foreign_keys=[takeoff_airport_id]) - landing_airport: Mapped['Airport'] = relationship(foreign_keys=[landing_airport_id]) - weather_info_landing: Mapped[WeatherInfo] = relationship(foreign_keys=[landing_weather_info_id]) - weather_info_takeoff: Mapped[WeatherInfo] = relationship(foreign_keys=[takeoff_weather_info_id]) - turn_points: Mapped[list['FlightTurnPoint']] = relationship() - event: Mapped['Event'] = relationship() - copilots: Mapped[List['Copilot']] = relationship(secondary=flight_has_copilot) - aircraft: Mapped['Aircraft'] = relationship() - photos: Mapped[List['Photo']] = relationship(foreign_keys=[Photo.flight_id]) - created_by: Mapped['User'] = relationship() - title_photo: Mapped['Photo'] = relationship(foreign_keys=[title_photo_id]) - track: Mapped['Track'] = relationship() - -class Copilot(BaseModel): - __tablename__ = "copilot" - - id: Mapped[int] = mapped_column(primary_key=True) - name: Mapped[str] = mapped_column(String(128), nullable=False) - url_slug: Mapped[str] = mapped_column(String(128), nullable=False, server_default="") - title_photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id'), nullable=True) - is_public: Mapped[bool] = mapped_column(Boolean, server_default='0') - created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id')) - created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) - deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0') - - flights: Mapped[Set['Flight']] = relationship(secondary=flight_has_copilot) - flight_plans: Mapped[Set['FlightPlan']] = relationship(secondary=flight_plan_has_copilot) - created_by: Mapped['User'] = relationship() - photos: Mapped[List['Photo']] = relationship(secondary=copilot_has_photo) - title_photo: Mapped['Photo'] = relationship(foreign_keys=[title_photo_id]) - - -class User(BaseModel): - __tablename__ = "user" - excluded_columns_in_dict = ('password_hashed',) - - id: Mapped[int] = mapped_column(primary_key=True) - email: Mapped[str] = mapped_column(String(128), nullable=False, unique=True) - name: Mapped[str] = mapped_column(String(128), nullable=False) - description: Mapped[str] = mapped_column(Text, nullable=False, server_default="") - public_username: Mapped[str] = mapped_column(String(128), nullable=True, unique=True) - avatar_image_filename: Mapped[str] = mapped_column(String(128), nullable=True) - title_image_filename: Mapped[str] = mapped_column(String(128), nullable=True) - password_hashed: Mapped[str] = mapped_column(String(60), nullable=False) - created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) - - flights: Mapped[Set['Flight']] = relationship() - organizations: Mapped[Set['Organization']] = relationship(secondary=user_is_in_organization) - - -class PasswordResetToken(BaseModel): - __tablename__ = "password_reset_token" - - id: Mapped[int] = mapped_column(primary_key=True) - user_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id')) - token: Mapped[str] = mapped_column(String(64), nullable=False) - valid_until: Mapped[datetime] = mapped_column(DateTime, nullable=True) - - user: Mapped['User'] = relationship() diff --git a/src/database/models/__init__.py b/src/database/models/__init__.py new file mode 100644 index 0000000..054835b --- /dev/null +++ b/src/database/models/__init__.py @@ -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", +] diff --git a/src/database/models/aircraft.py b/src/database/models/aircraft.py new file mode 100644 index 0000000..bc9151e --- /dev/null +++ b/src/database/models/aircraft.py @@ -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() diff --git a/src/database/models/airport.py b/src/database/models/airport.py new file mode 100644 index 0000000..d5eda7a --- /dev/null +++ b/src/database/models/airport.py @@ -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) diff --git a/src/database/models/airspace.py b/src/database/models/airspace.py new file mode 100644 index 0000000..bba9cd7 --- /dev/null +++ b/src/database/models/airspace.py @@ -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()) diff --git a/src/database/models/base_model.py b/src/database/models/base_model.py new file mode 100644 index 0000000..aabc5a0 --- /dev/null +++ b/src/database/models/base_model.py @@ -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 diff --git a/src/database/models/copilot.py b/src/database/models/copilot.py new file mode 100644 index 0000000..a86802d --- /dev/null +++ b/src/database/models/copilot.py @@ -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]) diff --git a/src/database/models/event.py b/src/database/models/event.py new file mode 100644 index 0000000..a968189 --- /dev/null +++ b/src/database/models/event.py @@ -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() diff --git a/src/database/models/flight.py b/src/database/models/flight.py new file mode 100644 index 0000000..4f4ea15 --- /dev/null +++ b/src/database/models/flight.py @@ -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() diff --git a/src/database/models/flight_plan.py b/src/database/models/flight_plan.py new file mode 100644 index 0000000..261375c --- /dev/null +++ b/src/database/models/flight_plan.py @@ -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) diff --git a/src/database/models/orgnization.py b/src/database/models/orgnization.py new file mode 100644 index 0000000..30de6c0 --- /dev/null +++ b/src/database/models/orgnization.py @@ -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() diff --git a/src/database/models/password_reset_token.py b/src/database/models/password_reset_token.py new file mode 100644 index 0000000..025b114 --- /dev/null +++ b/src/database/models/password_reset_token.py @@ -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() diff --git a/src/database/models/photo.py b/src/database/models/photo.py new file mode 100644 index 0000000..2a636f9 --- /dev/null +++ b/src/database/models/photo.py @@ -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() diff --git a/src/database/models/point_of_interest.py b/src/database/models/point_of_interest.py new file mode 100644 index 0000000..2421000 --- /dev/null +++ b/src/database/models/point_of_interest.py @@ -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]) diff --git a/src/database/models/user.py b/src/database/models/user.py new file mode 100644 index 0000000..945eb4a --- /dev/null +++ b/src/database/models/user.py @@ -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) diff --git a/src/database/models/weather_info.py b/src/database/models/weather_info.py new file mode 100644 index 0000000..9800fd1 --- /dev/null +++ b/src/database/models/weather_info.py @@ -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) diff --git a/src/external/weather.py b/src/external/weather.py index b856ea3..e01bb71 100644 --- a/src/external/weather.py +++ b/src/external/weather.py @@ -43,10 +43,13 @@ class Weather: async def download_weather_for_day(self, date: datetime.date, gps: Tuple[float, float]): url = self.get_weather_info_url(start_date=date, end_date=date, gps=gps) - async with aiohttp.ClientSession() as session: - async with session.get(url) as resp: - resp.raise_for_status() - return await resp.json() + try: + async with aiohttp.ClientSession() as session: + async with session.get(url) as resp: + resp.raise_for_status() + return await resp.json() + except Exception as e: + print(f"Error in downloading weather: {e}, {type(e)}") async def get_weather_for_hour( self, date_time: datetime.datetime, gps: Tuple[float, float] @@ -56,7 +59,7 @@ class Weather: # TODO: kontrola timezone! idx = date_time.hour result_data = {metric: data['hourly'][metric][idx] for metric in self.METRICS} - result_data['datetime'] = datetime.datetime.strptime(data['hourly']['time'][idx], "%Y-%m-%dT%H:%M") + result_data['date_time'] = datetime.datetime.strptime(data['hourly']['time'][idx], "%Y-%m-%dT%H:%M") return result_data diff --git a/src/graphql_schema/context.py b/src/graphql_schema/context.py index 386df77..027476e 100644 --- a/src/graphql_schema/context.py +++ b/src/graphql_schema/context.py @@ -4,6 +4,8 @@ from fastapi import BackgroundTasks, Depends, Security from fastapi_jwt import JwtAuthorizationCredentials from sqlalchemy import select from strawberry.fastapi import BaseContext + +from logger import log from database import async_session, models from jwt import access_security @@ -20,6 +22,8 @@ async def setup_graphql_context(credentials: JwtAuthorizationCredentials = Secur user_id = credentials['id'] if credentials else None organization_ids = set() + log.error(user_id) + if user_id: async with async_session() as db: organization_ids = set((await db.scalars( diff --git a/src/graphql_schema/dataloaders/base.py b/src/graphql_schema/dataloaders/base.py index 301dcc2..7a897dd 100644 --- a/src/graphql_schema/dataloaders/base.py +++ b/src/graphql_schema/dataloaders/base.py @@ -26,10 +26,14 @@ class BaseDataloader: 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_(set(ids))) + .filter(self.relationship_column.in_(ids_set)) .filter(*self.filters) ) @@ -62,13 +66,13 @@ class MultiModelsDataloader(BaseDataloader): order_by = [model.id.desc()] # defaultne radit od nejnovejsich zaznamu self.order_by = order_by - def get_query(self, ids: list[int]): + def get_query(self, ids: set[int]): query = ( self.query_builder.get_simple_query( extra_select=[self.relationship_column] + self.extra_select, order_by=self.order_by ) - .filter(self.relationship_column.in_(set(ids))) + .filter(self.relationship_column.in_(ids)) .filter(*self.filters) ) @@ -80,8 +84,12 @@ class MultiModelsDataloader(BaseDataloader): return query - async def load(self, ids: list[int]): - query = self.get_query(ids) + async def load(self, ids: list[int]) -> list: + ids_set = {id_ for id_ in set(ids) if id_ is not None} + if not ids_set: + return [[] for _ in ids] + + query = self.get_query(ids_set) async with async_session() as db: data = (await db.execute(query)).all() diff --git a/src/logger.py b/src/logger.py index be83b28..f379433 100644 --- a/src/logger.py +++ b/src/logger.py @@ -2,27 +2,25 @@ import logging import sys from colorlog import ColoredFormatter - log = logging.getLogger(__name__) log.setLevel(logging.DEBUG) stdoutHandler = logging.StreamHandler(stream=sys.stdout) stdoutHandler.setLevel(logging.DEBUG) - fmt = ColoredFormatter( - "%(log_color)s[%(levelname)s]%(reset)s %(blue)s%(message)s%(reset)s %(light_black)s[%(filename)s:%(lineno)d]]%(reset)s", - datefmt=None, - reset=True, - log_colors={ - 'DEBUG': 'cyan', - 'INFO': 'green', - 'WARNING': 'yellow', - 'ERROR': 'red', - 'CRITICAL': 'red,bg_white', - }, - secondary_log_colors={}, - style='%' + "%(log_color)s[%(levelname)s]%(reset)s %(blue)s%(message)s%(reset)s %(light_black)s[%(filename)s:%(lineno)d]]%(reset)s", + datefmt=None, + reset=True, + log_colors={ + 'DEBUG': 'cyan', + 'INFO': 'green', + 'WARNING': 'yellow', + 'ERROR': 'red', + 'CRITICAL': 'red,bg_white', + }, + secondary_log_colors={}, + style='%' ) stdoutHandler.setFormatter(fmt) log.addHandler(stdoutHandler) diff --git a/src/main.py b/src/main.py index f6bda60..38c36ef 100644 --- a/src/main.py +++ b/src/main.py @@ -108,7 +108,6 @@ class App: async def sitemap(): return await SitemapEndpoint().on_get() - # musi byt na konci app.include_router(auth.router) app.include_router(forgotten_password.router) diff --git a/src/paths.py b/src/paths.py index acf4a0e..c8acbeb 100644 --- a/src/paths.py +++ b/src/paths.py @@ -1,13 +1,8 @@ import os -from typing import Optional from config import API_URL from logger import log -PHOTO_BASE_PATH = "" -AIRCRAFT_BASE_PATH = "" -FLIGHT_BASE_PATH = "" FLIGHT_GPX_TRACK_PATH = "/app/uploads/tracks" -AIRCRAFT_UPLOAD_DEST_PATH = "/app/uploads/aircrafts/" def get_photo_basepath(flight_id: int) -> str: diff --git a/src/routers/auth.py b/src/routers/auth.py index 771b846..c32732e 100644 --- a/src/routers/auth.py +++ b/src/routers/auth.py @@ -1,14 +1,13 @@ from fastapi import BackgroundTasks, Security, APIRouter from fastapi_jwt import JwtAuthorizationCredentials from starlette.responses import Response - from endpoints.login import LoginInput, LoginEndpoint, RefreshEndpoint, LogoutEndpoint from endpoints.registration import RegistrationInput, RegistrationEndpoint from jwt import access_security, refresh_security - router = APIRouter() + @router.post("/registration", status_code=201) async def registration(user: RegistrationInput, background_tasks: BackgroundTasks): return await RegistrationEndpoint().on_post(user, background_tasks) diff --git a/src/scripts/download_weather.py b/src/scripts/download_weather.py index e5fdafa..82e0288 100644 --- a/src/scripts/download_weather.py +++ b/src/scripts/download_weather.py @@ -8,10 +8,10 @@ from sqlalchemy.ext.asyncio import AsyncSession sys.path.insert(0, "/app/src") -from utils.flight_plan_helpers import refresh_markers_weather_info -from database.models import FlightPlan -from database import models -from database.transaction import get_session +from utils.flight_plan_helpers import refresh_markers_weather_info # noqa +from database.models import FlightPlan # noqa +from database import models # noqa +from database.transaction import get_session # noqa async def get_plans(db: AsyncSession) -> list[Any] | Result[tuple[FlightPlan, Any]]: