diff --git a/src/background_jobs/weather.py b/src/background_jobs/weather.py index 38b4369..d251235 100644 --- a/src/background_jobs/weather.py +++ b/src/background_jobs/weather.py @@ -7,7 +7,6 @@ from logger import log async def download_weather(date_time: datetime, flight_id: int, airport_id: int, type_: Literal['landing', 'takeoff']): - log.error("AAAAAAAAAAAAAAAAAAAAAAAAAAA") async with get_session() as db: airport = await models.Airport.get_one(db, airport_id) gps = (airport.gps_latitude, airport.gps_longitude) diff --git a/src/decorators/error_logging.py b/src/decorators/error_logging.py index fc08b38..31bafa5 100644 --- a/src/decorators/error_logging.py +++ b/src/decorators/error_logging.py @@ -1,4 +1,5 @@ from functools import wraps +from fastapi import HTTPException from graphql import GraphQLError from sqlalchemy.exc import NoResultFound @@ -10,5 +11,8 @@ def error_logging(func): return await func(*args, **kwargs) except NoResultFound as e: raise GraphQLError("Not found", original_error=e) + except HTTPException as e: + if e.status_code == 401: + raise GraphQLError("Not authorized", original_error=e) return decorator diff --git a/src/graphql_schema/dataloaders/multi_models.py b/src/graphql_schema/dataloaders/multi_models.py index 5cced16..6fe3265 100644 --- a/src/graphql_schema/dataloaders/multi_models.py +++ b/src/graphql_schema/dataloaders/multi_models.py @@ -29,6 +29,17 @@ flights_by_copilot_dataloader = DataLoader( 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, diff --git a/src/graphql_schema/entities/aircraft.py b/src/graphql_schema/entities/aircraft.py index d81b366..f11c729 100644 --- a/src/graphql_schema/entities/aircraft.py +++ b/src/graphql_schema/entities/aircraft.py @@ -1,6 +1,7 @@ from typing import List import strawberry from decorators.endpoints import authenticated_user_only +from decorators.error_logging import error_logging from .resolvers.aircraft import AircraftMutationResolver, AircraftQueryResolver from graphql_schema.entities.types.mutation_input import CreateAircraftInput, EditAircraftInput from graphql_schema.entities.types.types import Aircraft @@ -9,6 +10,7 @@ from graphql_schema.entities.types.types import Aircraft @strawberry.type class AircraftQueries: @strawberry.field() + @error_logging @authenticated_user_only() async def aircrafts(root, info) -> List[Aircraft]: return await AircraftQueryResolver().get_list( @@ -17,6 +19,7 @@ class AircraftQueries: ) @strawberry.field() + @error_logging @authenticated_user_only() async def aircraft(root, info, id: int) -> Aircraft: return await AircraftQueryResolver().get_one( @@ -27,11 +30,13 @@ class AircraftQueries: @strawberry.type class AircraftMutation: @strawberry.mutation + @error_logging @authenticated_user_only() async def create_aircraft(root, info, input: CreateAircraftInput) -> Aircraft: return await AircraftMutationResolver().create_new(input, info.context.user_id) @strawberry.mutation + @error_logging @authenticated_user_only() async def edit_aircraft(root, info, id: int, input: EditAircraftInput) -> Aircraft: return await AircraftMutationResolver().edit(id, user_id=info.context.user_id, data=input) diff --git a/src/graphql_schema/entities/airport.py b/src/graphql_schema/entities/airport.py index f197037..d7575ee 100644 --- a/src/graphql_schema/entities/airport.py +++ b/src/graphql_schema/entities/airport.py @@ -1,6 +1,7 @@ from typing import List import strawberry from database import models +from decorators.error_logging import error_logging from decorators.endpoints import authenticated_user_only from graphql_schema.entities.resolvers.base import BaseQueryResolver from graphql_schema.entities.types.types import Airport @@ -9,11 +10,13 @@ from graphql_schema.entities.types.types import Airport @strawberry.type class AirportQueries: @strawberry.field() + @error_logging @authenticated_user_only() async def airports(root, info) -> List[Airport]: return await BaseQueryResolver(Airport, models.Airport).get_list(info.context.user_id) @strawberry.field() + @error_logging @authenticated_user_only() async def airport(root, info, id: int) -> Airport: return await BaseQueryResolver(Airport, models.Airport).get_one(id, user_id=info.context.user_id) diff --git a/src/graphql_schema/entities/copilot.py b/src/graphql_schema/entities/copilot.py index c8b4fa9..cf3b995 100644 --- a/src/graphql_schema/entities/copilot.py +++ b/src/graphql_schema/entities/copilot.py @@ -2,8 +2,9 @@ from typing import List, Optional import strawberry from fastapi import HTTPException from starlette.status import HTTP_401_UNAUTHORIZED - +from strawberry.types import Info from database import models +from decorators.error_logging import error_logging from decorators.endpoints import authenticated_user_only from graphql_schema.entities.resolvers.base import BaseMutationResolver from graphql_schema.entities.resolvers.copilot import CopilotQueryResolver @@ -14,12 +15,14 @@ from graphql_schema.entities.types.types import Copilot @strawberry.type class CopilotQueries: @strawberry.field() + @error_logging @authenticated_user_only() - async def copilots(root, info) -> List[Copilot]: + async def copilots(root, info: Info) -> List[Copilot]: return await CopilotQueryResolver().get_list(info.context.user_id) @strawberry.field() - async def copilot(root, info, id: int, pilot_username: Optional[str] = None) -> Copilot: + @error_logging + async def copilot(root, info: Info, id: int, pilot_username: Optional[str] = None) -> Copilot: if not info.context.user_id and not pilot_username: raise HTTPException(HTTP_401_UNAUTHORIZED) @@ -33,6 +36,7 @@ class CopilotQueries: @strawberry.type class CopilotMutation: @strawberry.mutation + @error_logging @authenticated_user_only() async def create_copilot(root, info, input: CreateCopilotInput) -> Copilot: return await BaseMutationResolver(Copilot, models.Copilot).create( @@ -41,6 +45,7 @@ class CopilotMutation: ) @strawberry.mutation + @error_logging @authenticated_user_only() async def edit_copilot(root, info, id: int, input: EditCopilotInput) -> Copilot: return await BaseMutationResolver(Copilot, models.Copilot).update(id, input, info.context.user_id) diff --git a/src/graphql_schema/entities/event.py b/src/graphql_schema/entities/event.py index 44af430..2341127 100644 --- a/src/graphql_schema/entities/event.py +++ b/src/graphql_schema/entities/event.py @@ -4,6 +4,7 @@ from fastapi import HTTPException from starlette.status import HTTP_401_UNAUTHORIZED from database import models from decorators.endpoints import authenticated_user_only +from decorators.error_logging import error_logging from graphql_schema.entities.resolvers.base import BaseMutationResolver from graphql_schema.entities.resolvers.event import EventQueryResolver from graphql_schema.entities.types.mutation_input import CreateEventInput, EditEventInput @@ -13,6 +14,7 @@ from graphql_schema.entities.types.types import Event @strawberry.type class EventQueries: @strawberry.field() + @error_logging async def events(root, info, username: Optional[str] = None) -> List[Event]: if not info.context.user_id and not username: raise HTTPException(HTTP_401_UNAUTHORIZED) @@ -24,6 +26,7 @@ class EventQueries: ) @strawberry.field() + @error_logging async def event(root, info, id: int, username: Optional[str] = None) -> Event: if not info.context.user_id and not username: raise HTTPException(HTTP_401_UNAUTHORIZED) @@ -38,11 +41,13 @@ class EventQueries: @strawberry.type class EventMutation: @strawberry.mutation + @error_logging @authenticated_user_only() async def create_event(root, info, input: CreateEventInput) -> Event: return await BaseMutationResolver(Event, models.Event).create(input.to_dict(), info.context.user_id) @strawberry.mutation + @error_logging @authenticated_user_only() async def edit_event(root, info, id: int, input: EditEventInput) -> Event: return await BaseMutationResolver(Event, models.Event).update(id, input, info.context.user_id) diff --git a/src/graphql_schema/entities/flight.py b/src/graphql_schema/entities/flight.py index 11bee09..59a2b13 100644 --- a/src/graphql_schema/entities/flight.py +++ b/src/graphql_schema/entities/flight.py @@ -18,6 +18,7 @@ from graphql_schema.entities.types.types import Flight class FlightQueries: @strawberry.field() + @error_logging async def flights(root, info, username: Optional[str] = None) -> List[Flight]: if not info.context.user_id and not username: raise HTTPException(HTTP_401_UNAUTHORIZED) @@ -36,6 +37,7 @@ class FlightQueries: @strawberry.type class FlightMutation: @strawberry.mutation + @error_logging @authenticated_user_only() async def create_flight(self, info, input: CreateFlightInput) -> Flight: data = input.to_dict() @@ -77,11 +79,13 @@ class FlightMutation: return flight @strawberry.mutation + @error_logging @authenticated_user_only() async def edit_flight(self, info, id: int, input: EditFlightInput) -> Flight: return await FlightMutationResolver().update(info.context, id, input) @strawberry.mutation + @error_logging @authenticated_user_only() async def delete_flight(self, info, id: int) -> Flight: return await FlightMutationResolver().delete(info.context.user_id, id) diff --git a/src/graphql_schema/entities/organization.py b/src/graphql_schema/entities/organization.py index 9c98f35..f02c30c 100644 --- a/src/graphql_schema/entities/organization.py +++ b/src/graphql_schema/entities/organization.py @@ -6,6 +6,7 @@ from sqlalchemy.exc import IntegrityError from database import models from decorators.endpoints import authenticated_user_only from database.transaction import get_session +from decorators.error_logging import error_logging from graphql_schema.entities.resolvers.base import BaseQueryResolver, BaseMutationResolver from graphql_schema.entities.types.mutation_input import CreateOrganizationInput, EditOrganizationInput from graphql_schema.entities.types.types import Organization @@ -14,6 +15,7 @@ from graphql_schema.entities.types.types import Organization @strawberry.type class OrganizationQueries: @strawberry.field() + @error_logging @authenticated_user_only() async def organizations(root, info) -> List[Organization]: return await BaseQueryResolver(Organization, models.Organization).get_list( @@ -21,6 +23,7 @@ class OrganizationQueries: ) @strawberry.field() + @error_logging @authenticated_user_only() async def organization(root, info, id: int) -> Organization: return await BaseQueryResolver(Organization, models.Organization).get_one(id, info.context.user_id) @@ -30,6 +33,7 @@ class OrganizationQueries: class OrganizationMutation: @strawberry.mutation + @error_logging @authenticated_user_only() async def create_organization(root, info, input: CreateOrganizationInput) -> Organization: return await BaseMutationResolver(Organization, models.Organization).create( @@ -38,6 +42,7 @@ class OrganizationMutation: ) @strawberry.mutation + @error_logging @authenticated_user_only() async def edit_organization(root, info, id: int, input: EditOrganizationInput) -> Organization: async with get_session() as db: @@ -55,6 +60,7 @@ class OrganizationMutation: class OrganizationUserMutation: @strawberry.mutation + @error_logging @authenticated_user_only() async def add_to_organization(root, info, organization_id: int) -> Organization: async with get_session() as db: @@ -75,6 +81,7 @@ class OrganizationUserMutation: return Organization(**organization.as_dict()) @strawberry.mutation + @error_logging @authenticated_user_only() async def remove_from_organization(root, info, organization_id: int) -> Organization: async with get_session() as db: diff --git a/src/graphql_schema/entities/photo.py b/src/graphql_schema/entities/photo.py index 1092eb7..7696d96 100644 --- a/src/graphql_schema/entities/photo.py +++ b/src/graphql_schema/entities/photo.py @@ -2,6 +2,7 @@ from typing import List import strawberry from database import models from decorators.endpoints import authenticated_user_only +from decorators.error_logging import error_logging from graphql_schema.entities.resolvers.base import BaseQueryResolver from graphql_schema.entities.resolvers.photo import PhotoMutationResolver from graphql_schema.entities.types.types import Photo @@ -11,10 +12,12 @@ from graphql_schema.entities.types.mutation_input import EditPhotoInput, UploadP @strawberry.type class PhotoQueries: @strawberry.field() + @error_logging async def photos(root, info) -> List[Photo]: return await BaseQueryResolver(Photo, models.Photo).get_list(user_id=info.context.user_id) @strawberry.field() + @error_logging async def photo(root, info, id: int) -> Photo: return await BaseQueryResolver(Photo, models.Photo).get_one(id, user_id=info.context.user_id) @@ -22,16 +25,19 @@ class PhotoQueries: @strawberry.type class PhotoMutation: @strawberry.mutation + @error_logging @authenticated_user_only() async def upload_photo(self, info, input: UploadPhotoInput) -> Photo: return await PhotoMutationResolver().upload(info, input) @strawberry.mutation() + @error_logging @authenticated_user_only() async def edit_photo(self, info, id: int, input: EditPhotoInput) -> Photo: return await PhotoMutationResolver().update(id, input, info.context.user_id) @strawberry.mutation() + @error_logging @authenticated_user_only() async def change_orientation(self, info, id: int, direction: str) -> Photo: return await PhotoMutationResolver().change_orientation( @@ -42,11 +48,13 @@ class PhotoMutation: ) @strawberry.mutation() + @error_logging @authenticated_user_only() async def adjust_photo(self, info, id: int, adjustment: AdjustmentInput) -> Photo: return await PhotoMutationResolver().adjust(id, info=info, user_id=info.context.user_id, adjustment=adjustment) @strawberry.mutation() + @error_logging @authenticated_user_only() async def delete_photo(self, info, id: int) -> Photo: return await PhotoMutationResolver().delete(user_id=info.context.user_id, id=id) diff --git a/src/graphql_schema/entities/poi.py b/src/graphql_schema/entities/poi.py index 3201a6c..72d3700 100644 --- a/src/graphql_schema/entities/poi.py +++ b/src/graphql_schema/entities/poi.py @@ -3,6 +3,7 @@ import strawberry from database import models from decorators.endpoints import authenticated_user_only from database.transaction import get_session +from decorators.error_logging import error_logging from graphql_schema.entities.helpers.combobox import handle_combobox_save from graphql_schema.entities.resolvers.base import BaseQueryResolver, BaseMutationResolver from graphql_schema.entities.types.types import PointOfInterest @@ -12,6 +13,7 @@ from graphql_schema.entities.types.mutation_input import CreatePointOfInterestIn @strawberry.type class PointOfInterestQueries: @strawberry.field() + @error_logging @authenticated_user_only() async def points_of_interest(root, info) -> List[PointOfInterest]: return await BaseQueryResolver(PointOfInterest, models.PointOfInterest).get_list(info.context.user_id) @@ -25,6 +27,7 @@ class PointOfInterestQueries: @strawberry.type class PointOfInterestMutation: @strawberry.mutation + @error_logging @authenticated_user_only() async def create_point_of_interest(root, info, input: CreatePointOfInterestInput) -> PointOfInterest: input_data = input.to_dict() @@ -40,6 +43,7 @@ class PointOfInterestMutation: ) @strawberry.mutation + @error_logging @authenticated_user_only() async def edit_point_of_interest(root, info, id: int, input: EditPointOfInterestInput) -> PointOfInterest: input_data = input.to_dict() @@ -59,6 +63,7 @@ class PointOfInterestMutation: return PointOfInterest(**updated_poi.as_dict()) @strawberry.mutation + @error_logging @authenticated_user_only() async def delete_point_of_interest(self, info, id: int) -> PointOfInterest: return await BaseMutationResolver(PointOfInterest, models.PointOfInterest).delete(info.context.user_id, id=id) diff --git a/src/graphql_schema/entities/poi_type.py b/src/graphql_schema/entities/poi_type.py index 1954474..2805b63 100644 --- a/src/graphql_schema/entities/poi_type.py +++ b/src/graphql_schema/entities/poi_type.py @@ -2,6 +2,7 @@ from typing import List import strawberry from database import models from decorators.endpoints import authenticated_user_only +from decorators.error_logging import error_logging from graphql_schema.entities.resolvers.base import BaseQueryResolver from graphql_schema.entities.types.types import PointOfInterestType @@ -10,11 +11,13 @@ from graphql_schema.entities.types.types import PointOfInterestType class PointOfInterestTypeQueries: @strawberry.field() + @error_logging @authenticated_user_only() async def point_of_interest_types(root, info) -> List[PointOfInterestType]: return await BaseQueryResolver(PointOfInterestType, models.PointOfInterestType).get_list(info.context.user_id) @strawberry.field() + @error_logging @authenticated_user_only() async def point_of_interest_type(root, info, id: int) -> PointOfInterestType: return await BaseQueryResolver(PointOfInterestType, models.PointOfInterestType).get_one( diff --git a/src/graphql_schema/entities/types/types.py b/src/graphql_schema/entities/types/types.py index da34321..7a5bd2e 100644 --- a/src/graphql_schema/entities/types/types.py +++ b/src/graphql_schema/entities/types/types.py @@ -10,7 +10,7 @@ from graphql_schema.dataloaders.multi_models import ( poi_photos_dataloader, flight_by_poi_dataloader, flight_copilots_dataloader, flight_track_dataloader, photos_dataloader, flights_by_aircraft_dataloader, users_in_organization_dataloader, aircrafts_from_organization_dataloader, user_organizations_dataloader, flights_by_event_dataloader, - flights_by_copilot_dataloader, public_flights_by_event_dataloader + flights_by_copilot_dataloader, public_flights_by_event_dataloader, public_flights_by_copilot_dataloader ) from graphql_schema.dataloaders.single_model import ( poi_dataloader, poi_type_dataloader, event_dataloader, aircraft_dataloader, airport_dataloader, cover_photo_loader, @@ -145,7 +145,14 @@ class Flight: @strawberry_sqlalchemy_type(models.Copilot) class Copilot: - flights: List[Flight] = strawberry.field(resolver=lambda root: flights_by_copilot_dataloader.load(root.id)) + async def resolve_flights(root, info): + dataloader = public_flights_by_copilot_dataloader + if info.context.user_id: + dataloader = flights_by_event_dataloader + + return await dataloader.load(root.id) + + flights: List[Flight] = strawberry.field(resolver=resolve_flights) @strawberry_sqlalchemy_type(models.Aircraft) diff --git a/src/graphql_schema/entities/user.py b/src/graphql_schema/entities/user.py index e44cada..1cb6c95 100644 --- a/src/graphql_schema/entities/user.py +++ b/src/graphql_schema/entities/user.py @@ -4,13 +4,13 @@ from graphql import GraphQLError from passlib.hash import bcrypt from sqlalchemy import select from strawberry.file_uploads import Upload +from background_jobs.photo import resize_photo from database import models from decorators.endpoints import authenticated_user_only from decorators.error_logging import error_logging from database.transaction import get_session from graphql_schema.entities.types.types import User from utils.file import delete_file -from utils.image import resize_image from utils.upload import handle_file_upload @@ -72,7 +72,7 @@ class EditUserMutation: data['avatar_image_filename'] = await handle_file_upload(input.avatar_image, user_image_path) info.context.background_tasks.add_task( - resize_image, path=user_image_path, filename=data['avatar_image_filename'], new_width=400 + resize_photo, path=user_image_path, filename=data['avatar_image_filename'], new_width=400 ) if input.title_image: @@ -81,7 +81,7 @@ class EditUserMutation: data['title_image_filename'] = await handle_file_upload(input.title_image, user_image_path) info.context.background_tasks.add_task( - resize_image, path=user_image_path, filename=data['title_image_filename'], new_width=800 + resize_photo, path=user_image_path, filename=data['title_image_filename'], new_width=800 ) if input.old_password and input.new_password: diff --git a/src/main.py b/src/main.py index 917564a..240c60d 100644 --- a/src/main.py +++ b/src/main.py @@ -3,6 +3,7 @@ from datetime import timedelta from typing import Optional from fastapi import FastAPI, APIRouter, Depends, Security from fastapi_jwt import JwtAuthorizationCredentials, JwtAccessBearerCookie, JwtRefreshBearerCookie +from graphql import GraphQLError from sqlalchemy import select from starlette.background import BackgroundTasks from starlette.middleware.cors import CORSMiddleware @@ -35,7 +36,11 @@ class App: def create_app(self): if SENTRY_DSN: - sentry_sdk.init(dsn=SENTRY_DSN, enable_tracing=True) + sentry_sdk.init( + dsn=SENTRY_DSN, + enable_tracing=True, + ignore_errors = [GraphQLError] + ) app = FastAPI() diff --git a/src/utils/file.py b/src/utils/file.py index d5634d7..e744ade 100644 --- a/src/utils/file.py +++ b/src/utils/file.py @@ -10,5 +10,7 @@ def delete_file(path: str, silent: bool = False): def check_directories(path: str): - if not os.path.isdir(path): - os.makedirs(path) + try: + os.makedirs(path, exist_ok=True) + except OSError: + pass diff --git a/src/utils/image.py b/src/utils/image.py index 1a6c6d9..43b3619 100644 --- a/src/utils/image.py +++ b/src/utils/image.py @@ -1,10 +1,9 @@ import io - import math from datetime import datetime from typing import Optional, Tuple import exif -from PIL import Image, UnidentifiedImageError +from PIL import Image from PIL.ImageEnhance import Brightness, Contrast, Color, Sharpness from utils.file import check_directories from utils.gps import gps_to_decimal @@ -130,129 +129,9 @@ class PhotoEditor: def write_to_file( self, quality: int = 90, dest_path: Optional[str] = None, dest_filename: Optional[str] = None ) -> str: + check_directories(dest_path or self.path) + dest = f"{dest_path or self.path}/{dest_filename or self.filename}" - - check_directories(dest) - self.img.save(dest, 'JPEG', quality=quality) return dest - - -# ----------- -# odsud niz to bude vse asi na smazani, vse by mela umet trida PhotoEditor - -async def resize_image( - path: str, filename: str, new_width: int, quality: int = 90, - dest_path: Optional[str] = None, - dest_filename: Optional[str] = None -): - if not dest_path: - dest_path = path - - if not dest_filename: - dest_filename = filename - - try: - image = Image.open(f"{path}/{filename}") - - width, height = image.size - new_height = int(new_width * height / width) - - image = image.resize((new_width, new_height), Image.LANCZOS) - check_directories(dest_path) - image.save(f"{dest_path}/{dest_filename}", 'JPEG', quality=quality) - - return new_width, new_height - except UnidentifiedImageError: - pass - - -def crop_image_after_rotate( - original_width: int, original_height: int, rotated_width: int, rotated_height: int, degrees: float -) -> Tuple[int, int]: - aspect_ratio = float(original_width) / original_height - rotated_aspect_ratio = float(rotated_width) / rotated_height - angle = math.fabs(degrees) * math.pi / 180 - - if aspect_ratio < 1: - total_height = float(original_width) / rotated_aspect_ratio - else: - total_height = float(original_height) - - h = total_height / (aspect_ratio * math.sin(angle) + math.cos(angle)) - w = h * aspect_ratio - - return round(w), round(h) - - -async def rotate_image_no_crop( - path: str, - filename: str, - angle: float, - dest_path: Optional[str] = None, - dest_filename: Optional[str] = None -): - if not dest_path: - dest_path = path - - if not dest_filename: - dest_filename = filename - - img = Image.open(f"{path}/{filename}") - rotated_img = img.rotate(angle, Image.BICUBIC, expand=True) - - check_directories(dest_path) - rotated_img.save(f"{dest_path}/{dest_filename}", 'JPEG', quality=100) - - -async def adjust_image( - path: str, - filename: str, - rotate: float, - brightness: float, - contrast: float, - saturation: float, - sharpness: float, - crop: dict, - dest_path: Optional[str] = None, - dest_filename: Optional[str] = None -): - if not dest_path: - dest_path = path - - if not dest_filename: - dest_filename = filename - - img = Image.open(f"{path}/{filename}") - - if rotate: - rotated_img = img.rotate(rotate, Image.BICUBIC, expand=True) - - width, height = img.size - rotated_width, rotated_height = rotated_img.size - new_width, new_height = crop_image_after_rotate(width, height, rotated_width, rotated_height, rotate) - - left = round((rotated_width - new_width) / 2) - top = round((rotated_height - new_height) / 2) - - img = rotated_img.crop((left, top, left + new_width, top + new_height)) - - adjustments = [ - (Brightness, brightness), - (Contrast, contrast), - (Color, saturation), - (Sharpness, sharpness) - ] - for adj, value in adjustments: - img = adj(img).enhance(value) - - if crop: - w, h = img.size - left = crop['left'] * w - top = crop['top'] * h - width = crop['width'] * w - height = crop['height'] * h - img = img.crop((left, top, left + width, top + height)) - - img.save(f"{dest_path}/{dest_filename}", 'JPEG', quality=100)