Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 1 addition & 40 deletions app/core/email.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,12 @@
"EmailSendError",
]

import base64
from datetime import datetime
from os import getenv
from pathlib import Path
from typing import Optional, Union

import resend
from flask import current_app, render_template
from flask import render_template
from redis import Redis
from resend import Attachment, RemoteAttachment, Tag
from resend.http_client_requests import RequestsClient
Expand Down Expand Up @@ -274,40 +272,3 @@ def send_password_reset_email(
text=text,
tags=[{"name": "category", "value": "password_reset"}],
)


def _build_logo_attachment() -> Attachment:
"""
Load the Drssed logo from static/emails/logo.png and return a
Resend-compatible inline attachment dict.

The content_id "logo" matches the cid:logo reference in HTML templates.
"""
logo_path = _get_static_folder() / "emails" / "logo.png"

with open(logo_path, "rb") as f:
logo_bytes = f.read()

return {
"filename": "logo.png",
"content": base64.b64encode(logo_bytes).decode(),
"content_id": "logo",
}


def _get_static_folder() -> Path:
folder = current_app.static_folder

if not folder:
raise RuntimeError("⚠️ Flask static_folder is not configured")

return Path(folder)


def _get_templates_folder() -> str:
folder = current_app.template_folder

if not folder:
raise RuntimeError("⚠️ Flask template_folder is not configured")

return str(folder)
1 change: 0 additions & 1 deletion app/routes/clothing.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ def delete_clothing_piece(clothing_id: str):
@limiter.limit("60 per minute")
@authorize_request
def patch_clothing_piece(clothing_id: str):
request.headers["Authorization"]
data = request.get_json()

# NOTE: Default to private entries until social features implemented
Expand Down
31 changes: 0 additions & 31 deletions app/services/image.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
__all__ = [
"move_preview_image_to_permanent",
"save_outfit_preview",
"load_clothing_image_by_id",
"generate_outfit_preview",
"delete_outfit_preview",
Expand All @@ -12,7 +11,6 @@
from urllib.parse import urljoin

from PIL import Image
from werkzeug.datastructures import FileStorage

from app.core.logging import get_logger

Expand Down Expand Up @@ -47,35 +45,6 @@ def move_preview_image_to_permanent(
raise


def save_outfit_preview(outfit_id: str, preview_file: FileStorage) -> str:
"""
Returns: public_url
"""
mimetype = (preview_file.mimetype or "").lower()
if mimetype not in ("image/png", "image/webp", "image/jpeg", "image/jpg"):
raise ValueError("Invalid preview mimetype. Must be png/webp/jpeg.")

preview_file.stream.seek(0)
img = Image.open(preview_file.stream).convert("RGBA")

img.thumbnail((1024, 1024), Image.Resampling.LANCZOS)

alpha = img.getchannel("A")
bbox = alpha.getbbox()
if bbox:
img = img.crop(bbox)

path = f"app/static/outfit_collages/{outfit_id}.webp"
img.save(path, "WEBP")

return str(
urljoin(
os.getenv("API_BASE_URL", ""),
f"static/outfit_collages/{outfit_id}.webp",
)
)


def load_clothing_image_by_id(image_id: str) -> Image.Image:
image_path = os.path.join("app/static/clothing_images", f"{image_id}.webp")

Expand Down
14 changes: 0 additions & 14 deletions app/utils/exceptions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@
from app.utils.exceptions.clothing import (
ClothingColorMissingError,
ClothingIDMissingError,
ClothingImageInvalidError,
ClothingImageMissingError,
ClothingNameMissingError,
ClothingNameTooLongError,
ClothingNameTooShortError,
ClothingSubCategoryMissingError,
Expand All @@ -32,19 +30,16 @@
)
from app.utils.exceptions.outfits import (
OutfitClothingIDInvalidError,
OutfitClothingIDsMissingError,
OutfitFavoriteMissingError,
OutfitIDMissingError,
OutfitLimitInvalidError,
OutfitNameMissingError,
OutfitNameTooLongError,
OutfitNameTooShortError,
OutfitOffsetInvalidError,
OutfitPreviewInvalidError,
OutfitPublicMissingError,
OutfitSceneInvalidError,
OutfitSceneMissingError,
OutfitSeasonsInvalidError,
OutfitTagsInvalidError,
)
from app.utils.exceptions.user import (
Expand All @@ -62,9 +57,7 @@
UsernameTooShortError,
)
from app.utils.exceptions.validation import (
FileTooLargeError,
ImageUnclearError,
UnsupportedFileTypeError,
)
from app.utils.exceptions.wear import (
WearAlreadyLoggedError,
Expand Down Expand Up @@ -95,27 +88,21 @@
"ClothingValidationError",
"ClothingNotFoundError",
"ClothingConflictError",
"UnsupportedFileTypeError",
"FileTooLargeError",
"ImageUnclearError",
"ClothingNameMissingError",
"ClothingSubCategoryMissingError",
"ClothingColorMissingError",
"ClothingIDMissingError",
"ClothingImageMissingError",
"ClothingNameTooShortError",
"ClothingNameTooLongError",
"ClothingImageInvalidError",
"SeasonsInvalidError",
"ClothingTagsInvalidError",
"ClothingWarmthLevelInvalidError",
"OutfitValidationError",
"OutfitNotFoundError",
"OutfitIDMissingError",
"OutfitClothingIDsMissingError",
"OutfitNameMissingError",
"OutfitClothingIDInvalidError",
"OutfitSeasonsInvalidError",
"OutfitTagsInvalidError",
"OutfitLimitInvalidError",
"OutfitOffsetInvalidError",
Expand All @@ -125,7 +112,6 @@
"OutfitFavoriteMissingError",
"OutfitSceneMissingError",
"OutfitSceneInvalidError",
"OutfitPreviewInvalidError",
"WearValidationError",
"WearNotFoundError",
"WearAlreadyLoggedError",
Expand Down
12 changes: 1 addition & 11 deletions app/utils/exceptions/clothing.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
from app.utils.exceptions.base import ClothingConflictError, ClothingValidationError
from app.utils.exceptions.base import ClothingValidationError


class ClothingIDMissingError(ClothingValidationError):
def __init__(self, message="Clothing ID is missing"):
super().__init__(message)


class ClothingNameMissingError(ClothingValidationError):
def __init__(self, message="Clothing name is missing"):
super().__init__(message)


class ClothingSubCategoryMissingError(ClothingValidationError):
def __init__(self, message="Clothing category is missing"):
super().__init__(message)
Expand All @@ -36,11 +31,6 @@ def __init__(self, message="Clothing name has to be at most 50 characters long")
super().__init__(message)


class ClothingImageInvalidError(ClothingConflictError):
def __init__(self, message="Clothing image is invalid"):
super().__init__(message)


class SeasonsInvalidError(ClothingValidationError):
def __init__(self, message="Clothing seasons are invalid"):
super().__init__(message)
Expand Down
15 changes: 0 additions & 15 deletions app/utils/exceptions/outfits.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,6 @@ def __init__(self, message="Outfit ID is missing"):
super().__init__(message)


class OutfitClothingIDsMissingError(OutfitValidationError):
def __init__(self, message="Outfit clothing ID(s) are missing"):
super().__init__(message)


class OutfitNameMissingError(OutfitValidationError):
def __init__(self, message="Outfit name is missing"):
super().__init__(message)
Expand All @@ -21,11 +16,6 @@ def __init__(self, message="Outfit clothing ID is invalid"):
super().__init__(message)


class OutfitSeasonsInvalidError(OutfitValidationError):
def __init__(self, message="Outfit seasons are invalid"):
super().__init__(message)


class OutfitTagsInvalidError(OutfitValidationError):
def __init__(self, message="Outfit tags are invalid"):
super().__init__(message)
Expand Down Expand Up @@ -69,8 +59,3 @@ def __init__(self, message="Outfit scene is missing."):
class OutfitSceneInvalidError(OutfitValidationError):
def __init__(self, message="Outfit scene is invalid."):
super().__init__(message)


class OutfitPreviewInvalidError(OutfitValidationError):
def __init__(self, message="Outfit preview is invalid."):
super().__init__(message)
10 changes: 0 additions & 10 deletions app/utils/exceptions/validation.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,6 @@
from app.utils.exceptions.base import ValidationError


class UnsupportedFileTypeError(ValidationError):
def __init__(self, message="Unsupported file type [Supported: .png, .jpg, .jpeg]"):
super().__init__(message)


class FileTooLargeError(ValidationError):
def __init__(self, message="File is too large (max 2MB)"):
super().__init__(message)


class ImageUnclearError(ValidationError):
def __init__(self, message="Image is unclear"):
super().__init__(message)
27 changes: 0 additions & 27 deletions app/utils/helpers.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,15 @@
__all__ = ["helper"]

from datetime import datetime, timezone
from decimal import Decimal
from typing import Any, Sequence, cast

from flask import g, has_request_context, request

from app.core.logging import get_logger

from ..models.outfit import CanvasPlacement

logger = get_logger()


class HelperFunctions:
@staticmethod
def ensure_dict(result: Any) -> dict:
"""
:param result: The object to check
:return: Result as dictionary
:raises TypeError: If result is not a dictionary
"""
if not isinstance(result, dict):
raise TypeError(f"Expected a dictionary, but got {type(result).__name__}")

return result

@staticmethod
def build_paginated_response(items, limit, offset, total):
"""
Expand Down Expand Up @@ -56,17 +40,6 @@ def get_request_context(self) -> dict:

return context

def _parse_canvas_row(self, row: Sequence) -> CanvasPlacement:
clothing_id, x, y, z, scale, rotation = row
return CanvasPlacement(
clothing_id=cast(str, clothing_id),
x=float(cast(Decimal, x)),
y=float(cast(Decimal, y)),
z=int(cast(Decimal, z)),
scale=float(cast(Decimal, scale)),
rotation=float(cast(Decimal, rotation)),
)


helper = HelperFunctions()

Expand Down
Loading
Loading