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:
Hayden
2022-02-13 12:23:42 -09:00
committed by GitHub
parent 9a82a172cb
commit c617251f4c
157 changed files with 1866 additions and 1578 deletions

0
mealie/pkgs/__init__.py Normal file
View File

1
mealie/pkgs/cache/__init__.py vendored Normal file
View File

@@ -0,0 +1 @@
from .cache_key import *

8
mealie/pkgs/cache/cache_key.py vendored Normal file
View 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))

View 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 *

View 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
View 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

View 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
View 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)

View File

@@ -0,0 +1 @@
from .fs_stats import *

View 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"