mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-01-18 15:01:19 -05:00
feature: proper multi-tenant-support (#969)(WIP)
* update naming * refactor tests to use shared structure * shorten names * add tools test case * refactor to support multi-tenant * set group_id on creation * initial refactor for multitenant tags/cats * spelling * additional test case for same valued resources * fix recipe update tests * apply indexes to foreign keys * fix performance regressions * handle unknown exception * utility decorator for function debugging * migrate recipe_id to UUID * GUID for recipes * remove unused import * move image functions into package * move utilities to packages dir * update import * linter * image image and asset routes * update assets and images to use UUIDs * fix migration base * image asset test coverage * use ids for categories and tag crud functions * refactor recipe organizer test suite to reduce duplication * add uuid serlization utility * organizer base router * slug routes testing and fixes * fix postgres error * adopt UUIDs * move tags, categories, and tools under "organizers" umbrella * update composite label * generate ts types * fix import error * update frontend types * fix type errors * fix postgres errors * fix #978 * add null check for title validation * add note in docs on multi-tenancy
This commit is contained in:
0
mealie/pkgs/__init__.py
Normal file
0
mealie/pkgs/__init__.py
Normal file
1
mealie/pkgs/cache/__init__.py
vendored
Normal file
1
mealie/pkgs/cache/__init__.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
from .cache_key import *
|
||||
8
mealie/pkgs/cache/cache_key.py
vendored
Normal file
8
mealie/pkgs/cache/cache_key.py
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
import random
|
||||
import string
|
||||
|
||||
|
||||
def new_key(length=4) -> str:
|
||||
"""returns a 4 character string to be used as a cache key for frontend data"""
|
||||
options = string.ascii_letters + string.digits
|
||||
return "".join(random.choices(options, k=length))
|
||||
7
mealie/pkgs/dev/__init__.py
Normal file
7
mealie/pkgs/dev/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""
|
||||
This package containers helpful development tools to be used for development and testing. It shouldn't be used for or imported
|
||||
in production
|
||||
"""
|
||||
|
||||
from .lifespan_tracker import *
|
||||
from .timer import *
|
||||
26
mealie/pkgs/dev/lifespan_tracker.py
Normal file
26
mealie/pkgs/dev/lifespan_tracker.py
Normal file
@@ -0,0 +1,26 @@
|
||||
import time
|
||||
|
||||
|
||||
# log_lifetime is a class decorator that logs the creation and destruction of a class
|
||||
# It is used to track the lifespan of a class during development or testing.
|
||||
# It SHOULD NOT be used in production code.
|
||||
def log_lifetime(cls):
|
||||
class LifeTimeClass(cls):
|
||||
def __init__(self, *args, **kwargs):
|
||||
print(f"Creating an instance of {cls.__name__}") # noqa: T001
|
||||
self.__lifespan_timer_start = time.perf_counter()
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def __del__(self):
|
||||
toc = time.perf_counter()
|
||||
print(f"Downloaded the tutorial in {toc - self.__lifespan_timer_start:0.4f} seconds") # noqa: T001
|
||||
|
||||
print(f"Deleting an instance of {cls.__name__}") # noqa: T001
|
||||
|
||||
try:
|
||||
super().__del__()
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
return LifeTimeClass
|
||||
12
mealie/pkgs/dev/timer.py
Normal file
12
mealie/pkgs/dev/timer.py
Normal file
@@ -0,0 +1,12 @@
|
||||
import time
|
||||
|
||||
|
||||
def timer(func):
|
||||
def wrapper(*args, **kwargs):
|
||||
start = time.time()
|
||||
result = func(*args, **kwargs)
|
||||
end = time.time()
|
||||
print(f"{func.__name__} took {end - start} seconds") # noqa: T001
|
||||
return result
|
||||
|
||||
return wrapper
|
||||
7
mealie/pkgs/img/__init__.py
Normal file
7
mealie/pkgs/img/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""
|
||||
The img package is a collection of utilities for working with images. While it offers some Mealie specific functionality, libraries
|
||||
within the img package should not be tightly coupled to Mealie.
|
||||
"""
|
||||
|
||||
|
||||
from .minify import *
|
||||
130
mealie/pkgs/img/minify.py
Normal file
130
mealie/pkgs/img/minify.py
Normal file
@@ -0,0 +1,130 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from logging import Logger
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image
|
||||
|
||||
WEBP = ".webp"
|
||||
FORMAT = "WEBP"
|
||||
|
||||
IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp"}
|
||||
|
||||
|
||||
def get_format(image: Path) -> str:
|
||||
img = Image.open(image)
|
||||
return img.format
|
||||
|
||||
|
||||
def sizeof_fmt(file_path: Path, decimal_places=2):
|
||||
if not file_path.exists():
|
||||
return "(File Not Found)"
|
||||
size = file_path.stat().st_size
|
||||
for unit in ["B", "kB", "MB", "GB", "TB", "PB"]:
|
||||
if size < 1024.0 or unit == "PiB":
|
||||
break
|
||||
size /= 1024.0
|
||||
return f"{size:.{decimal_places}f} {unit}"
|
||||
|
||||
|
||||
@dataclass
|
||||
class MinifierOptions:
|
||||
original: bool = True
|
||||
minature: bool = True
|
||||
tiny: bool = True
|
||||
|
||||
|
||||
class ABCMinifier(ABC):
|
||||
def __init__(self, purge=False, opts: MinifierOptions = None, logger: Logger = None):
|
||||
self._purge = purge
|
||||
self._opts = opts or MinifierOptions()
|
||||
self._logger = logger or Logger("Minifier")
|
||||
|
||||
def get_image_sizes(self, org_img: Path, min_img: Path, tiny_img: Path):
|
||||
self._logger.info(
|
||||
f"{org_img.name} Minified: {sizeof_fmt(org_img)} -> {sizeof_fmt(min_img)} -> {sizeof_fmt(tiny_img)}"
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def minify(self, image: Path, force=True):
|
||||
...
|
||||
|
||||
def purge(self, image: Path):
|
||||
if not self._purge:
|
||||
return
|
||||
|
||||
for file in image.parent.glob("*.*"):
|
||||
if file.suffix != WEBP:
|
||||
file.unlink()
|
||||
|
||||
|
||||
class PillowMinifier(ABCMinifier):
|
||||
@staticmethod
|
||||
def to_webp(image_file: Path, dest: Path = None, quality: int = 100) -> Path:
|
||||
"""
|
||||
Converts an image to the webp format in-place. The original image is not
|
||||
removed By default, the quality is set to 100.
|
||||
"""
|
||||
if image_file.suffix == WEBP:
|
||||
return image_file
|
||||
|
||||
img = Image.open(image_file)
|
||||
|
||||
dest = dest or image_file.with_suffix(WEBP)
|
||||
img.save(dest, FORMAT, quality=quality)
|
||||
|
||||
return dest
|
||||
|
||||
@staticmethod
|
||||
def crop_center(pil_img: Image, crop_width=300, crop_height=300):
|
||||
img_width, img_height = pil_img.size
|
||||
return pil_img.crop(
|
||||
(
|
||||
(img_width - crop_width) // 2,
|
||||
(img_height - crop_height) // 2,
|
||||
(img_width + crop_width) // 2,
|
||||
(img_height + crop_height) // 2,
|
||||
)
|
||||
)
|
||||
|
||||
def minify(self, image_file: Path, force=True):
|
||||
if not image_file.exists():
|
||||
raise FileNotFoundError(f"{image_file.name} does not exist")
|
||||
|
||||
org_dest = image_file.parent.joinpath("original.webp")
|
||||
min_dest = image_file.parent.joinpath("min-original.webp")
|
||||
tiny_dest = image_file.parent.joinpath("tiny-original.webp")
|
||||
|
||||
if not force and min_dest.exists() and tiny_dest.exists() and org_dest.exists():
|
||||
self._logger.info(f"{image_file.name} already minified")
|
||||
return
|
||||
|
||||
success = False
|
||||
|
||||
if self._opts.original:
|
||||
if not force and org_dest.exists():
|
||||
self._logger.info(f"{image_file.name} already minified")
|
||||
else:
|
||||
PillowMinifier.to_webp(image_file, org_dest, quality=70)
|
||||
success = True
|
||||
|
||||
if self._opts.minature:
|
||||
if not force and min_dest.exists():
|
||||
self._logger.info(f"{image_file.name} already minified")
|
||||
else:
|
||||
PillowMinifier.to_webp(image_file, min_dest, quality=70)
|
||||
self._logger.info(f"{image_file.name} minified")
|
||||
success = True
|
||||
|
||||
if self._opts.tiny:
|
||||
if not force and tiny_dest.exists():
|
||||
self._logger.info(f"{image_file.name} already minified")
|
||||
else:
|
||||
img = Image.open(image_file)
|
||||
tiny_image = PillowMinifier.crop_center(img)
|
||||
tiny_image.save(tiny_dest, FORMAT, quality=70)
|
||||
self._logger.info("Tiny image saved")
|
||||
success = True
|
||||
|
||||
if self._purge and success:
|
||||
self.purge(image_file)
|
||||
1
mealie/pkgs/stats/__init__.py
Normal file
1
mealie/pkgs/stats/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .fs_stats import *
|
||||
15
mealie/pkgs/stats/fs_stats.py
Normal file
15
mealie/pkgs/stats/fs_stats.py
Normal file
@@ -0,0 +1,15 @@
|
||||
def pretty_size(size: int) -> str:
|
||||
"""
|
||||
Pretty size takes in a integer value of a file size and returns the most applicable
|
||||
file unit and the size.
|
||||
"""
|
||||
if size < 1024:
|
||||
return f"{size} bytes"
|
||||
elif size < 1024 ** 2:
|
||||
return f"{round(size / 1024, 2)} KB"
|
||||
elif size < 1024 ** 2 * 1024:
|
||||
return f"{round(size / 1024 / 1024, 2)} MB"
|
||||
elif size < 1024 ** 2 * 1024 * 1024:
|
||||
return f"{round(size / 1024 / 1024 / 1024, 2)} GB"
|
||||
else:
|
||||
return f"{round(size / 1024 / 1024 / 1024 / 1024, 2)} TB"
|
||||
Reference in New Issue
Block a user