Uprava stahovani elevace, slouceni gps utils do jednoho filu, retryable, pridani social image k pilotovi a letu
This commit is contained in:
+7
-5
@@ -9,15 +9,17 @@ RUN mjml /app/templates/* -o /app/templates/html/
|
||||
|
||||
FROM python:3.11-alpine AS app
|
||||
|
||||
# protoze xfs uzivatel ma id=33
|
||||
# RUN deluser xfs
|
||||
|
||||
# uzivatel www-data v alpine neexistuje
|
||||
RUN delgroup www-data && addgroup -g 33 -S www-data && adduser -u 33 -D -S -G www-data www-data
|
||||
|
||||
|
||||
RUN apk update && \
|
||||
apk add --no-cache tzdata mariadb-client mariadb-dev && \
|
||||
pip install --upgrade pip
|
||||
|
||||
# protoze xfs uzivatel ma id=33
|
||||
RUN deluser xfs
|
||||
|
||||
# uzivatel www-data v alpine neexistuje
|
||||
RUN delgroup www-data && addgroup -g 33 -S www-data && adduser -u 33 -D -S -G www-data www-data
|
||||
|
||||
COPY requirements.txt /app/requirements.txt
|
||||
COPY --from=email_builder /app/templates/html/ /app/src/email/templates/html/
|
||||
|
||||
@@ -12,3 +12,4 @@ services:
|
||||
SENTRY_DSN: "https://184d1d6bd39f4fbb94804ae84e9afdd5@o472821.ingest.sentry.io/5506983"
|
||||
volumes:
|
||||
- ./uploads:/app/uploads
|
||||
- ./static:/app/static
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
from aiohttp import ClientResponseError
|
||||
from database import models
|
||||
from database.transaction import get_session
|
||||
from decorators.retryable import retryable
|
||||
from external.elevation import elevation_api
|
||||
from external.gpx_parser import GPXParser
|
||||
from paths import FLIGHT_GPX_TRACK_PATH
|
||||
|
||||
|
||||
@retryable
|
||||
async def add_terrain_elevation_to_flight(flight_id: int, gpx_filename: str):
|
||||
gpx_parser = GPXParser(f"{FLIGHT_GPX_TRACK_PATH}/{gpx_filename}")
|
||||
coordinates = await gpx_parser.get_coordinates_all()
|
||||
@@ -25,6 +27,7 @@ async def add_terrain_elevation_to_flight(flight_id: int, gpx_filename: str):
|
||||
print(e)
|
||||
|
||||
|
||||
@retryable
|
||||
async def add_terrain_elevation_to_photo(photo):
|
||||
try:
|
||||
elevation = await elevation_api.get_elevation_for_points([
|
||||
@@ -34,7 +37,7 @@ async def add_terrain_elevation_to_photo(photo):
|
||||
print("Cannot get elevation")
|
||||
return
|
||||
|
||||
terrain_elevation = elevation[0]['elevation']
|
||||
terrain_elevation = elevation[0].elevation
|
||||
async with get_session() as db:
|
||||
await models.Photo.update(db_session=db, obj=photo, data={"terrain_elevation": terrain_elevation})
|
||||
except Exception as e:
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
from PIL import Image, ImageDraw, ImageOps
|
||||
|
||||
|
||||
async def add_circular_avatar(base_image_path, avatar_image_path, output_image_path):
|
||||
# Load the base image
|
||||
base_image = Image.open(base_image_path)
|
||||
base_width, base_height = base_image.size
|
||||
|
||||
# Calculate the new dimensions for cropping to a 1.9:1 aspect ratio
|
||||
new_width = base_width
|
||||
new_height = int(new_width / 1.9)
|
||||
if new_height > base_height:
|
||||
new_height = base_height
|
||||
new_width = int(new_height * 1.9)
|
||||
|
||||
# Center crop the base image to the new dimensions
|
||||
left = (base_width - new_width) // 2
|
||||
top = (base_height - new_height) // 2
|
||||
right = (base_width + new_width) // 2
|
||||
bottom = (base_height + new_height) // 2
|
||||
base_image = base_image.crop((left, top, right, bottom))
|
||||
base_width, base_height = base_image.size
|
||||
|
||||
# Load the avatar image
|
||||
avatar_image = Image.open(avatar_image_path)
|
||||
avatar_size = int(base_height * 0.3) # Set the size of the avatar
|
||||
avatar_image = avatar_image.resize((avatar_size, avatar_size), Image.LANCZOS)
|
||||
|
||||
# Create a mask to make the avatar circular
|
||||
mask = Image.new('L', (avatar_size, avatar_size), 0)
|
||||
draw = ImageDraw.Draw(mask)
|
||||
draw.ellipse((0, 0, avatar_size, avatar_size), fill=255)
|
||||
|
||||
# Apply the mask to the avatar image
|
||||
avatar_image = ImageOps.fit(avatar_image, mask.size, centering=(0.5, 0.5))
|
||||
avatar_image.putalpha(mask)
|
||||
|
||||
# Create a border around the avatar
|
||||
border_size = int(avatar_size * 0.03)
|
||||
border_image = Image.new('RGBA', (avatar_size + 2 * border_size, avatar_size + 2 * border_size), (255, 255, 255, 0))
|
||||
border_mask = Image.new('L', (avatar_size + 2 * border_size, avatar_size + 2 * border_size), 0)
|
||||
border_draw = ImageDraw.Draw(border_mask)
|
||||
border_draw.ellipse((0, 0, avatar_size + 2 * border_size, avatar_size + 2 * border_size), fill=255)
|
||||
border_draw.ellipse((border_size, border_size, avatar_size + border_size, avatar_size + border_size), fill=0)
|
||||
|
||||
avatar_margin = int(avatar_size * 0.1)
|
||||
|
||||
border_image.putalpha(border_mask)
|
||||
border_image.paste(avatar_image, (border_size, border_size), avatar_image)
|
||||
|
||||
# Calculate the position to paste the avatar (bottom right corner)
|
||||
position = (base_width - avatar_size - 2 * border_size - avatar_margin, base_height - avatar_size - 2 * border_size - avatar_margin)
|
||||
|
||||
# Paste the avatar with border onto the base image
|
||||
base_image.paste(border_image, position, border_image)
|
||||
|
||||
# Save the result
|
||||
base_image.save(output_image_path)
|
||||
@@ -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
|
||||
@@ -11,19 +11,18 @@ from graphql_schema.schema import GraphQLContext, schema
|
||||
|
||||
|
||||
def setup_graphql_endpoint(app: FastAPI, access_security: JwtAccessBearerCookie):
|
||||
if not APP_DEBUG:
|
||||
return
|
||||
debug_router = APIRouter()
|
||||
if APP_DEBUG:
|
||||
debug_router = APIRouter()
|
||||
|
||||
@debug_router.get("/graphql/autologin")
|
||||
async def autologin():
|
||||
access_token = access_security.create_access_token(subject={"id": 6, "name": "Franta Vomacka"})
|
||||
response = RedirectResponse(url="/graphql")
|
||||
access_security.set_access_cookie(response, access_token, expires_delta=timedelta(days=14))
|
||||
@debug_router.get("/graphql/autologin")
|
||||
async def autologin():
|
||||
access_token = access_security.create_access_token(subject={"id": 6, "name": "Franta Vomacka"})
|
||||
response = RedirectResponse(url="/graphql")
|
||||
access_security.set_access_cookie(response, access_token, expires_delta=timedelta(days=14))
|
||||
|
||||
return response
|
||||
return response
|
||||
|
||||
app.include_router(debug_router)
|
||||
app.include_router(debug_router)
|
||||
|
||||
async def setup_graphql_context(credentials: JwtAuthorizationCredentials = Security(access_security)):
|
||||
user_id = credentials['id'] if credentials else None
|
||||
|
||||
Vendored
+62
-11
@@ -1,27 +1,78 @@
|
||||
import asyncio
|
||||
import dataclasses
|
||||
from typing import List, Dict
|
||||
import aiohttp
|
||||
from decorators.retryable import retryable
|
||||
from utils.list_utils import chunks
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ElevationInfo:
|
||||
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]]):
|
||||
...
|
||||
|
||||
@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']]
|
||||
return [
|
||||
ElevationInfo(lat=loc['latitude'], lng=loc['longitude'], elevation=loc['elevation'])
|
||||
for loc in response['results']
|
||||
]
|
||||
|
||||
|
||||
elevation_api = ElevationAPI()
|
||||
class OpenMeteoElevationAPI(ElevationAPI):
|
||||
ELEVATION_ENDPOINT = "https://api.open-meteo.com/v1/elevation"
|
||||
|
||||
def get_request(self, session: aiohttp.ClientSession, points: List[Dict[str, float]]):
|
||||
latitudes = ",".join(str(point['lat']) for point in points)
|
||||
longitudes = ",".join(str(point['lng']) for point in points)
|
||||
url = f"{self.ELEVATION_ENDPOINT}?latitude={latitudes}&longitude={longitudes}"
|
||||
|
||||
return session.get(url)
|
||||
|
||||
async def get_elevation_for_points(self, points: List[Dict[str, float]]) -> List[ElevationInfo]:
|
||||
elevation = []
|
||||
for chunk in chunks(points, n=100):
|
||||
response = await self.call_api(chunk)
|
||||
|
||||
chunk_elevation = [
|
||||
ElevationInfo(**point, elevation=response['elevation'][i]) for i, point in enumerate(points)
|
||||
]
|
||||
|
||||
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
|
||||
|
||||
|
||||
elevation_api = CombinedElevationAPI()
|
||||
|
||||
Vendored
-34
@@ -1,34 +0,0 @@
|
||||
import math
|
||||
|
||||
EARTH_RADIUS = 6378.1 # Radius of the Earth
|
||||
|
||||
|
||||
def get_bearing(center_point: tuple[float, float], end_point: tuple[float, float]):
|
||||
lat3 = math.radians(end_point[0])
|
||||
long3 = math.radians(end_point[1])
|
||||
lat1 = math.radians(center_point[0])
|
||||
long1 = math.radians(center_point[1])
|
||||
|
||||
dLon = long3 - long1
|
||||
|
||||
X = math.cos(lat3) * math.sin(dLon)
|
||||
Y = math.cos(lat1) * math.sin(lat3) - math.sin(lat1) * math.cos(lat3) * math.cos(dLon)
|
||||
|
||||
end_brng = math.atan2(X, Y)
|
||||
|
||||
return end_brng
|
||||
|
||||
|
||||
def get_distance(a: tuple[float, float], b: tuple[float, float]):
|
||||
lat1 = math.radians(a[0])
|
||||
lon1 = math.radians(a[1])
|
||||
lat2 = math.radians(b[0])
|
||||
lon2 = math.radians(b[1])
|
||||
|
||||
dlon = lon2 - lon1
|
||||
dlat = lat2 - lat1
|
||||
|
||||
a = math.sin(dlat / 2) ** 2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2) ** 2
|
||||
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
|
||||
|
||||
return EARTH_RADIUS * c
|
||||
Vendored
+1
-1
@@ -8,7 +8,7 @@ import aiohttp
|
||||
from sqlalchemy import delete
|
||||
from database import models
|
||||
from database.transaction import get_session
|
||||
from external.gps_utils import get_distance, get_bearing, EARTH_RADIUS
|
||||
from utils.gps import get_distance, get_bearing, EARTH_RADIUS
|
||||
|
||||
|
||||
# https://pyopenair.readthedocs.io/en/latest/openair.html
|
||||
|
||||
Vendored
+2
@@ -3,6 +3,7 @@ import urllib.parse
|
||||
from typing import Tuple, Dict
|
||||
import aiohttp
|
||||
from aiocache import cached
|
||||
from decorators.retryable import retryable
|
||||
|
||||
|
||||
class Weather:
|
||||
@@ -38,6 +39,7 @@ class Weather:
|
||||
return f"{url}{query_string}"
|
||||
|
||||
@cached(ttl=6 * 3600)
|
||||
@retryable
|
||||
async def download_weather_for_day(self, date: datetime.date, gps: Tuple[float, float]):
|
||||
url = self.get_weather_info_url(start_date=date, end_date=date, gps=gps)
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from typing import Optional
|
||||
import strawberry
|
||||
from graphql import GraphQLError
|
||||
|
||||
from decorators.endpoints import authenticated_user_only, allow_public
|
||||
from decorators.error_logging import error_logging
|
||||
from graphql_schema.entities.resolvers.flight import FlightMutationResolver, FlightQueryResolver
|
||||
|
||||
@@ -18,7 +18,7 @@ from graphql_schema.entities.types.types import Flight
|
||||
from paths import FLIGHT_GPX_TRACK_PATH
|
||||
from utils.file import delete_file
|
||||
from utils.str_utils import random_str
|
||||
from utils.upload import handle_file_upload
|
||||
from utils.file import handle_file_upload
|
||||
|
||||
|
||||
class FlightQueryResolver(BaseQueryResolver):
|
||||
|
||||
@@ -16,7 +16,7 @@ from graphql_schema.entities.types.types import Photo
|
||||
from paths import get_photo_basepath
|
||||
from utils.file import delete_file
|
||||
from utils.image import PhotoEditor, parse_exif_info
|
||||
from utils.upload import handle_file_upload
|
||||
from utils.file import handle_file_upload
|
||||
|
||||
|
||||
class PhotoQueryResolver(BaseQueryResolver):
|
||||
|
||||
@@ -6,7 +6,7 @@ from typing import Optional, List
|
||||
import strawberry
|
||||
from database import models
|
||||
from decorators.endpoints import authenticated_user_only
|
||||
from external.gps_utils import get_bearing, get_distance
|
||||
from utils.gps import get_bearing, get_distance
|
||||
from external.gpx_parser import GPXParser
|
||||
from graphql_schema.dataloaders.flight_duration import flight_duration_dataloader
|
||||
from graphql_schema.dataloaders.multi_models import (
|
||||
@@ -24,7 +24,7 @@ from graphql_schema.dataloaders.single_model import (
|
||||
)
|
||||
from graphql_schema.permissions import IsAuthenticated
|
||||
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type
|
||||
from paths import get_avatar_url, get_title_image_url, get_photo_thumbnail_url, get_photo_url, FLIGHT_GPX_TRACK_PATH, airport_icon_url
|
||||
from paths import get_avatar_url, get_title_image_url, get_photo_thumbnail_url, get_photo_url, FLIGHT_GPX_TRACK_PATH, airport_icon_url, get_public_url
|
||||
|
||||
|
||||
@strawberry.type
|
||||
@@ -194,7 +194,9 @@ class Flight:
|
||||
duration_min_calculated: int = strawberry.field(
|
||||
resolver=lambda root: flight_duration_dataloader.load(root.id)
|
||||
)
|
||||
|
||||
social_image_url: Optional[str] = strawberry.field(
|
||||
resolver=lambda root: get_public_url(f'photos/{root.id}/title_photo.jpg')
|
||||
)
|
||||
debriefing_notes: Optional[str] = strawberry.field(
|
||||
permission_classes=[IsAuthenticated],
|
||||
resolver=lambda root: root.debriefing_notes
|
||||
@@ -295,6 +297,9 @@ class Organization:
|
||||
class User:
|
||||
avatar_image_url: Optional[str] = strawberry.field(resolver=lambda root: get_avatar_url(root))
|
||||
title_image_url: str = strawberry.field(resolver=lambda root: get_title_image_url(root))
|
||||
social_image_url: Optional[str] = strawberry.field(
|
||||
resolver=lambda root: get_public_url(f'profile/{root.id}/title_photo.jpg')
|
||||
)
|
||||
organizations: List[Organization] = strawberry.field(
|
||||
resolver=lambda root: user_organizations_dataloader.load(root.id)
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ from decorators.error_logging import error_logging
|
||||
from database.transaction import get_session
|
||||
from graphql_schema.entities.types.types import User
|
||||
from utils.file import delete_file
|
||||
from utils.upload import handle_file_upload
|
||||
from utils.file import handle_file_upload
|
||||
|
||||
|
||||
@strawberry.type
|
||||
|
||||
@@ -18,15 +18,15 @@ async def add_elevation_to_photos():
|
||||
coordinates = [
|
||||
{"lat": p.gps_latitude, "lng": p.gps_longitude} for p in photos if p.gps_latitude or p.gps_longitude
|
||||
]
|
||||
photos_by_corrdinates = {(p.gps_latitude, p.gps_longitude): p for p in photos}
|
||||
photos_by_coordinates = {(p.gps_latitude, p.gps_longitude): p for p in photos}
|
||||
if not coordinates:
|
||||
print("all done")
|
||||
return
|
||||
|
||||
points = await elevation_api.get_elevation_for_points(coordinates)
|
||||
for point in points:
|
||||
photo = photos_by_corrdinates[point['lat'], point['lng']]
|
||||
await models.Photo.update(db_session=session, obj=photo, data={"terrain_elevation": point['elevation']})
|
||||
photo = photos_by_coordinates[point.lat, point.lng]
|
||||
await models.Photo.update(db_session=session, obj=photo, data={"terrain_elevation": point.elevation})
|
||||
await session.flush()
|
||||
await session.commit()
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@ import asyncio
|
||||
import dataclasses
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from PIL import Image, ImageDraw, ImageFont, ImageFilter
|
||||
from PIL import Image, ImageDraw, ImageFilter
|
||||
from sqlalchemy import select
|
||||
|
||||
sys.path.insert(0, "/app/src")
|
||||
|
||||
from database import models
|
||||
from database.transaction import get_session
|
||||
from database import models # noqa
|
||||
from database.transaction import get_session # noqa
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
@@ -127,13 +127,11 @@ def draw_runways(airport_img, runways: list[Runway], size: int):
|
||||
start = center - all_runways_width // 2
|
||||
|
||||
colors = {
|
||||
"asphalt": "#555555",
|
||||
"concrete": "#dddddd",
|
||||
"asphalt": "#777777",
|
||||
"concrete": "#cccccc",
|
||||
"grass": "#00aa00"
|
||||
}
|
||||
|
||||
font = ImageFont.truetype('/app/static/Calibri.ttf', 50)
|
||||
|
||||
rwy = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
||||
heading_sum = 0
|
||||
draw = ImageDraw.Draw(rwy)
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import asyncio
|
||||
import sys
|
||||
from sqlalchemy import select
|
||||
|
||||
sys.path.insert(0, '/app/src')
|
||||
from background_jobs.flight_title_photo import add_circular_avatar # noqa
|
||||
from database import models # noqa
|
||||
from database.transaction import get_session # noqa
|
||||
|
||||
|
||||
async def flight_title_photos():
|
||||
async with get_session() as db:
|
||||
flights = (await db.execute(
|
||||
select(models.Photo, models.Flight, models.User)
|
||||
.join(models.Flight.title_photo)
|
||||
.join(models.Flight.created_by)
|
||||
)).all()
|
||||
|
||||
for photo, flight, pilot in flights:
|
||||
await add_circular_avatar(
|
||||
base_image_path=f'/app/uploads/photos/{flight.id}/{photo.filename}.{photo.filename_extension}',
|
||||
avatar_image_path=f'/app/uploads/profile/{pilot.id}/{pilot.avatar_image_filename}',
|
||||
output_image_path=f'/app/uploads/photos/{flight.id}/title_photo.jpg',
|
||||
)
|
||||
|
||||
|
||||
async def user_title_photos():
|
||||
async with get_session() as db:
|
||||
users = (await db.execute(
|
||||
select(models.User.id, models.User.title_image_filename, models.User.avatar_image_filename)
|
||||
.filter(models.User.title_image_filename != '')
|
||||
)).all()
|
||||
|
||||
for user_id, title_image, avatar_image in users:
|
||||
await add_circular_avatar(
|
||||
base_image_path=f"/app/uploads/profile/{user_id}/{title_image}",
|
||||
avatar_image_path=f"/app/uploads/profile/{user_id}/{avatar_image}",
|
||||
output_image_path=f'/app/uploads/profile/{user_id}/title_photo.jpg'
|
||||
)
|
||||
|
||||
|
||||
async def generate_images():
|
||||
await user_title_photos()
|
||||
await flight_title_photos()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(generate_images())
|
||||
@@ -1,4 +1,7 @@
|
||||
import os
|
||||
import re
|
||||
import uuid
|
||||
from strawberry.file_uploads import Upload
|
||||
|
||||
|
||||
def delete_file(path: str, silent: bool = False):
|
||||
@@ -14,3 +17,31 @@ def check_directories(path: str):
|
||||
os.makedirs(path, exist_ok=True)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
async def handle_file_upload(
|
||||
file: Upload,
|
||||
path: str,
|
||||
filename_maxlength: int = 64,
|
||||
uid_prefix: bool = True,
|
||||
overwrite: bool = True
|
||||
) -> str:
|
||||
check_directories(path)
|
||||
|
||||
prefix = f"{uuid.uuid4()}-" if uid_prefix else ""
|
||||
filename = f"{prefix}{file.filename}"[-1 * filename_maxlength:]
|
||||
|
||||
# sanitize filename
|
||||
filename = re.sub(r'[^\w_. -]', '', filename).replace(" ", "-")
|
||||
|
||||
target_path = f"{path}/{filename}"
|
||||
|
||||
if os.path.exists(target_path) and not overwrite:
|
||||
raise FileExistsError(f"File {filename} already exists")
|
||||
|
||||
content = await file.read()
|
||||
image = open(target_path, "wb")
|
||||
image.write(content)
|
||||
image.close()
|
||||
|
||||
return filename
|
||||
|
||||
@@ -1,4 +1,38 @@
|
||||
from typing import Tuple
|
||||
import math
|
||||
|
||||
EARTH_RADIUS = 6378.1 # Radius of the Earth
|
||||
|
||||
|
||||
def get_bearing(center_point: tuple[float, float], end_point: tuple[float, float]):
|
||||
lat3 = math.radians(end_point[0])
|
||||
long3 = math.radians(end_point[1])
|
||||
lat1 = math.radians(center_point[0])
|
||||
long1 = math.radians(center_point[1])
|
||||
|
||||
dLon = long3 - long1
|
||||
|
||||
X = math.cos(lat3) * math.sin(dLon)
|
||||
Y = math.cos(lat1) * math.sin(lat3) - math.sin(lat1) * math.cos(lat3) * math.cos(dLon)
|
||||
|
||||
end_brng = math.atan2(X, Y)
|
||||
|
||||
return end_brng
|
||||
|
||||
|
||||
def get_distance(a: tuple[float, float], b: tuple[float, float]):
|
||||
lat1 = math.radians(a[0])
|
||||
lon1 = math.radians(a[1])
|
||||
lat2 = math.radians(b[0])
|
||||
lon2 = math.radians(b[1])
|
||||
|
||||
dlon = lon2 - lon1
|
||||
dlat = lat2 - lat1
|
||||
|
||||
a = math.sin(dlat / 2) ** 2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2) ** 2
|
||||
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
|
||||
|
||||
return EARTH_RADIUS * c
|
||||
|
||||
|
||||
def gps_to_decimal(input: Tuple[float, float, float]) -> float:
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import typing
|
||||
from itertools import islice
|
||||
|
||||
|
||||
def chunks(items: list | dict, n: int) -> typing.Generator:
|
||||
"""Yield successive n-sized chunks from lst."""
|
||||
|
||||
if isinstance(items, list):
|
||||
for i in range(0, len(items), n):
|
||||
yield items[i:i + n]
|
||||
elif isinstance(items, dict):
|
||||
it = iter(items)
|
||||
for i in range(0, len(items), n):
|
||||
yield {k: items[k] for k in islice(it, n)}
|
||||
@@ -1,33 +0,0 @@
|
||||
import os
|
||||
import re
|
||||
import uuid
|
||||
from strawberry.file_uploads import Upload
|
||||
from utils.file import check_directories
|
||||
|
||||
|
||||
async def handle_file_upload(
|
||||
file: Upload,
|
||||
path: str,
|
||||
filename_maxlength: int = 64,
|
||||
uid_prefix: bool = True,
|
||||
overwrite: bool = True
|
||||
) -> str:
|
||||
check_directories(path)
|
||||
|
||||
prefix = f"{uuid.uuid4()}-" if uid_prefix else ""
|
||||
filename = f"{prefix}{file.filename}"[-1 * filename_maxlength:]
|
||||
|
||||
# sanitize filename
|
||||
filename = re.sub(r'[^\w_. -]', '', filename).replace(" ", "-")
|
||||
|
||||
target_path = f"{path}/{filename}"
|
||||
|
||||
if os.path.exists(target_path) and not overwrite:
|
||||
raise FileExistsError(f"File {filename} already exists")
|
||||
|
||||
content = await file.read()
|
||||
image = open(target_path, "wb")
|
||||
image.write(content)
|
||||
image.close()
|
||||
|
||||
return filename
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user