Merge branch 'mealie-next' into fix/translation-issues-when-scraping

This commit is contained in:
Michael Genson
2024-02-04 13:20:44 -06:00
committed by GitHub
183 changed files with 4530 additions and 2172 deletions

View File

@@ -5,7 +5,7 @@ from pathlib import Path
from uuid import uuid4
import fastapi
from fastapi import Depends, HTTPException, Request, status
from fastapi import BackgroundTasks, Depends, HTTPException, Request, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from sqlalchemy.orm.session import Session
@@ -215,14 +215,14 @@ async def temporary_zip_path() -> AsyncGenerator[Path, None]:
temp_path.unlink(missing_ok=True)
async def temporary_dir() -> AsyncGenerator[Path, None]:
async def temporary_dir(background_tasks: BackgroundTasks) -> AsyncGenerator[Path, None]:
temp_path = app_dirs.TEMP_DIR.joinpath(uuid4().hex)
temp_path.mkdir(exist_ok=True, parents=True)
try:
yield temp_path
finally:
shutil.rmtree(temp_path)
background_tasks.add_task(shutil.rmtree, temp_path)
def temporary_file(ext: str = "") -> Callable[[], Generator[tempfile._TemporaryFileWrapper, None, None]]:

View File

@@ -1,17 +1,15 @@
from functools import lru_cache
from typing import Protocol
from passlib.context import CryptContext
import bcrypt
from mealie.core.config import get_app_settings
class Hasher(Protocol):
def hash(self, password: str) -> str:
...
def hash(self, password: str) -> str: ...
def verify(self, password: str, hashed: str) -> bool:
...
def verify(self, password: str, hashed: str) -> bool: ...
class FakeHasher:
@@ -22,15 +20,16 @@ class FakeHasher:
return password == hashed
class PasslibHasher:
def __init__(self) -> None:
self.ctx = CryptContext(schemes=["bcrypt"], deprecated="auto")
class BcryptHasher:
def hash(self, password: str) -> str:
return self.ctx.hash(password)
password_bytes = password.encode("utf-8")
hashed = bcrypt.hashpw(password_bytes, bcrypt.gensalt())
return hashed.decode("utf-8")
def verify(self, password: str, hashed: str) -> bool:
return self.ctx.verify(password, hashed)
password_bytes = password.encode("utf-8")
hashed_bytes = hashed.encode("utf-8")
return bcrypt.checkpw(password_bytes, hashed_bytes)
@lru_cache(maxsize=1)
@@ -40,4 +39,4 @@ def get_hasher() -> Hasher:
if settings.TESTING:
return FakeHasher()
return PasslibHasher()
return BcryptHasher()

View File

@@ -18,8 +18,7 @@ ALGORITHM = "HS256"
logger = root_logger.get_logger("security")
class UserLockedOut(Exception):
...
class UserLockedOut(Exception): ...
def create_access_token(data: dict, expires_delta: timedelta | None = None) -> str:

View File

@@ -7,13 +7,11 @@ from pydantic import BaseModel, BaseSettings, PostgresDsn
class AbstractDBProvider(ABC):
@property
@abstractmethod
def db_url(self) -> str:
...
def db_url(self) -> str: ...
@property
@abstractmethod
def db_url_public(self) -> str:
...
def db_url_public(self) -> str: ...
class SQLiteProvider(AbstractDBProvider, BaseModel):

View File

@@ -31,5 +31,4 @@ class GroupWebhooksModel(SqlAlchemyBase, BaseMixins):
time: Mapped[str | None] = mapped_column(String, default="00:00")
@auto_init()
def __init__(self, **_) -> None:
...
def __init__(self, **_) -> None: ...

View File

@@ -0,0 +1,35 @@
{
"generic": {
"server-error": "An unexpected error occurred"
},
"recipe": {
"unique-name-error": "Recipe names must be unique"
},
"mealplan": {
"no-recipes-match-your-rules": "No recipes match your rules"
},
"user": {
"user-updated": "User updated",
"password-updated": "Password updated",
"invalid-current-password": "Invalid current password",
"ldap-update-password-unavailable": "Unable to update password, user is controlled by LDAP"
},
"group": {
"report-deleted": "Report deleted."
},
"exceptions": {
"permission_denied": "You do not have permission to perform this action",
"no-entry-found": "The requested resource was not found",
"integrity-error": "Database integrity error",
"username-conflict-error": "This username is already taken",
"email-conflict-error": "This email is already in use"
},
"notifications": {
"generic-created": "{name} was created",
"generic-updated": "{name} was updated",
"generic-created-with-url": "{name} has been created, {url}",
"generic-updated-with-url": "{name} has been updated, {url}",
"generic-duplicated": "{name} has been duplicated",
"generic-deleted": "{name} has been deleted"
}
}

View File

@@ -6,16 +6,16 @@
"unique-name-error": "Ime recepta mora biti unikatno"
},
"mealplan": {
"no-recipes-match-your-rules": "No recipes match your rules"
"no-recipes-match-your-rules": "Noben recept ne ustreza vašim pogojem"
},
"user": {
"user-updated": "User updated",
"password-updated": "Password updated",
"invalid-current-password": "Invalid current password",
"ldap-update-password-unavailable": "Unable to update password, user is controlled by LDAP"
"user-updated": "Uporabnik posodobljen",
"password-updated": "Geslo posodobljeno",
"invalid-current-password": "Neveljavno trenutno geslo",
"ldap-update-password-unavailable": "Gesla ni mogoče posodobiti, uporabnik je nadzorovan preko LDAP"
},
"group": {
"report-deleted": "Report deleted."
"report-deleted": "Poročilo izbrisano."
},
"exceptions": {
"permission_denied": "Nimate dovoljenja za izvedbo zahtevanega dejanja",

View File

@@ -11,7 +11,7 @@
"user": {
"user-updated": "User updated",
"password-updated": "Password updated",
"invalid-current-password": "Invalid current password",
"invalid-current-password": "目前密碼無效",
"ldap-update-password-unavailable": "Unable to update password, user is controlled by LDAP"
},
"group": {
@@ -22,7 +22,7 @@
"no-entry-found": "The requested resource was not found",
"integrity-error": "Database integrity error",
"username-conflict-error": "This username is already taken",
"email-conflict-error": "This email is already in use"
"email-conflict-error": "該電子郵件已被使用"
},
"notifications": {
"generic-created": "{name} was created",

View File

@@ -3,5 +3,4 @@ The img package is a collection of utilities for working with images. While it o
within the img package should not be tightly coupled to Mealie.
"""
from .minify import *

View File

@@ -46,8 +46,7 @@ class ABCMinifier(ABC):
)
@abstractmethod
def minify(self, image: Path, force=True):
...
def minify(self, image: Path, force=True): ...
def purge(self, image: Path):
if not self._purge:

View File

@@ -14,7 +14,7 @@ from mealie.db.models.recipe.tag import Tag
from mealie.db.models.recipe.tool import Tool
from mealie.db.models.users.users import User
from mealie.schema.group.group_statistics import GroupStatistics
from mealie.schema.user.user import GroupBase, GroupInDB
from mealie.schema.user.user import GroupBase, GroupInDB, UpdateGroup
from ..db.models._model_base import SqlAlchemyBase
from .repository_generic import RepositoryGeneric
@@ -45,6 +45,18 @@ class RepositoryGroup(RepositoryGeneric[GroupInDB, Group]):
# since create uses special logic for resolving slugs, we don't want to use the standard create_many method
return [self.create(new_group) for new_group in data]
def update(self, match_value: str | int | UUID4, new_data: UpdateGroup | dict) -> GroupInDB:
if isinstance(new_data, GroupBase):
new_data.slug = slugify(new_data.name)
else:
new_data["slug"] = slugify(new_data["name"])
return super().update(match_value, new_data)
def update_many(self, data: Iterable[UpdateGroup | dict]) -> list[GroupInDB]:
# since update uses special logic for resolving slugs, we don't want to use the standard update_many method
return [self.update(group["id"] if isinstance(group, dict) else group.id, group) for group in data]
def get_by_name(self, name: str) -> GroupInDB | None:
dbgroup = self.session.execute(select(self.model).filter_by(name=name)).scalars().one_or_none()
if dbgroup is None:

View File

@@ -53,7 +53,7 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
stmt = (
select(self.model)
.join(RecipeSettings)
.filter(RecipeSettings.public == True) # noqa: 711
.filter(RecipeSettings.public == True) # noqa: E712
.order_by(order_attr.desc())
.offset(start)
.limit(limit)
@@ -63,7 +63,7 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
stmt = (
select(self.model)
.join(RecipeSettings)
.filter(RecipeSettings.public == True) # noqa: 711
.filter(RecipeSettings.public == True) # noqa: E712
.offset(start)
.limit(limit)
)

View File

@@ -25,5 +25,4 @@ class AbstractSeeder(ABC):
self.resources = Path(__file__).parent / "resources"
@abstractmethod
def seed(self, locale: str | None = None) -> None:
...
def seed(self, locale: str | None = None) -> None: ...

View File

@@ -0,0 +1,222 @@
{
"acorn-squash": "acorn squash",
"alfalfa-sprouts": "alfalfa sprouts",
"anchovies": "anchovies",
"apples": "apples",
"artichoke": "artichoke",
"arugula": "arugula",
"asparagus": "asparagus",
"aubergine": "aubergine",
"avocado": "avocado",
"bacon": "bacon",
"baking-powder": "baking powder",
"baking-soda": "baking soda",
"baking-sugar": "baking sugar",
"bar-sugar": "bar sugar",
"basil": "basil",
"bell-peppers": "bell peppers",
"blackberries": "blackberries",
"brassicas": "brassicas",
"bok-choy": "bok choy",
"broccoflower": "broccoflower",
"broccoli": "broccoli",
"broccolini": "broccolini",
"broccoli-rabe": "broccoli rabe",
"brussels-sprouts": "brussels sprouts",
"cabbage": "cabbage",
"cauliflower": "cauliflower",
"chinese-leaves": "chinese leaves",
"collard-greens": "collard greens",
"kohlrabi": "kohlrabi",
"bread": "bread",
"breadfruit": "breadfruit",
"broad-beans": "broad beans",
"brown-sugar": "brown sugar",
"butter": "butter",
"butternut-pumpkin": "butternut pumpkin",
"butternut-squash": "butternut squash",
"cactus-edible": "cactus, edible",
"calabrese": "calabrese",
"cannabis": "cannabis",
"capsicum": "capsicum",
"caraway": "caraway",
"carrot": "carrot",
"castor-sugar": "castor sugar",
"cayenne-pepper": "cayenne pepper",
"celeriac": "celeriac",
"celery": "celery",
"cereal-grains": "cereal grains",
"rice": "rice",
"chard": "chard",
"cheese": "cheese",
"chicory": "chicory",
"chilli-peppers": "chilli peppers",
"chives": "chives",
"chocolate": "chocolate",
"cilantro": "cilantro",
"cinnamon": "cinnamon",
"clarified-butter": "clarified butter",
"coconut": "coconut",
"coconut-milk": "coconut milk",
"coffee": "coffee",
"confectioners-sugar": "confectioners' sugar",
"coriander": "coriander",
"corn": "corn",
"corn-syrup": "corn syrup",
"cottonseed-oil": "cottonseed oil",
"courgette": "courgette",
"cream-of-tartar": "cream of tartar",
"cucumber": "cucumber",
"cumin": "cumin",
"daikon": "daikon",
"dairy-products-and-dairy-substitutes": "dairy products and dairy substitutes",
"eggs": "eggs",
"ghee": "ghee",
"milk": "milk",
"dandelion": "dandelion",
"demerara-sugar": "demerara sugar",
"dough": "dough",
"edible-cactus": "edible cactus",
"eggplant": "eggplant",
"endive": "endive",
"fats": "fats",
"speck": "speck",
"fava-beans": "fava beans",
"fiddlehead": "fiddlehead",
"fish": "fish",
"catfish": "catfish ",
"cod": "cod",
"salt-cod": "salt cod",
"salmon": "salmon",
"skate": "skate",
"stockfish": "stockfish",
"trout": "trout",
"tuna": "tuna",
"five-spice-powder": "five spice powder",
"flour": "flour",
"frisee": "frisee",
"fructose": "fructose",
"fruit": "fruit",
"apple": "apple",
"oranges": "oranges",
"pear": "pear",
"tomato": "tomato ",
"fruit-sugar": "fruit sugar",
"garam-masala": "garam masala",
"garlic": "garlic",
"gem-squash": "gem squash",
"ginger": "ginger",
"giblets": "giblets",
"grains": "grains",
"maize": "maize",
"sweetcorn": "sweetcorn",
"teff": "teff",
"grape-seed-oil": "grape seed oil",
"green-onion": "green onion",
"heart-of-palm": "heart of palm",
"hemp": "hemp",
"herbs": "herbs",
"oregano": "oregano",
"parsley": "parsley",
"honey": "honey",
"icing-sugar": "icing sugar",
"isomalt": "isomalt",
"jackfruit": "jackfruit",
"jaggery": "jaggery",
"jams": "jams",
"jellies": "jellies",
"jerusalem-artichoke": "jerusalem artichoke",
"jicama": "jicama",
"kale": "kale",
"kumara": "kumara",
"leavening-agents": "leavening agents",
"leek": "leek",
"legumes": "legumes ",
"peas": "peas",
"beans": "beans",
"lentils": "lentils",
"lemongrass": "lemongrass",
"lettuce": "lettuce",
"liver": "liver",
"maple-syrup": "maple syrup",
"meat": "meat",
"mortadella": "mortadella",
"mushroom": "mushroom",
"white-mushroom": "white mushroom",
"mussels": "mussels",
"nori": "nori",
"nutmeg": "nutmeg",
"nutritional-yeast-flakes": "nutritional yeast flakes",
"nuts": "nuts",
"nanaimo-bar-mix": "nanaimo bar mix",
"octopuses": "octopuses",
"oils": "oils",
"olive-oil": "olive oil",
"okra": "okra",
"olive": "olive",
"onion-family": "onion family",
"onion": "onion",
"scallion": "scallion",
"shallot": "shallot",
"spring-onion": "spring onion",
"orange-blossom-water": "orange blossom water",
"oysters": "oysters",
"panch-puran": "panch puran",
"paprika": "paprika",
"parsnip": "parsnip",
"pepper": "pepper",
"peppers": "peppers",
"plantain": "plantain",
"pineapple": "pineapple",
"poppy-seeds": "poppy seeds",
"potatoes": "potatoes",
"poultry": "poultry",
"powdered-sugar": "powdered sugar",
"pumpkin": "pumpkin",
"pumpkin-seeds": "pumpkin seeds",
"radish": "radish",
"raw-sugar": "raw sugar",
"refined-sugar": "refined sugar",
"rice-flour": "rice flour",
"rock-sugar": "rock sugar",
"rum": "rum",
"salt": "salt",
"seafood": "seafood",
"seeds": "seeds",
"sesame-seeds": "sesame seeds",
"sunflower-seeds": "sunflower seeds",
"soda": "soda",
"soda-baking": "soda, baking",
"soybean": "soybean",
"spaghetti-squash": "spaghetti squash",
"spices": "spices",
"spinach": "spinach",
"squash-family": "squash family",
"squash": "squash",
"zucchini": "zucchini",
"sugar": "sugar",
"caster-sugar": "caster sugar",
"granulated-sugar": "granulated sugar",
"superfine-sugar": "superfine sugar",
"turbanado-sugar": "turbanado sugar",
"unrefined-sugar": "unrefined sugar",
"white-sugar": "white sugar",
"sweet-potato": "sweet potato",
"sweeteners": "sweeteners",
"cane-sugar": "cane sugar",
"tahini": "tahini",
"tubers": "tubers",
"potato": "potato",
"sunchoke": "sunchoke",
"taro": "taro",
"yam": "yam",
"turnip": "turnip",
"vanilla": "vanilla",
"vegetables": "vegetables",
"fiddlehead-fern": "fiddlehead fern",
"ful": "ful",
"watercress": "watercress",
"watermelon": "watermelon",
"xanthan-gum": "xanthan gum",
"yeast": "yeast"
}

View File

@@ -1,6 +1,6 @@
{
"acorn-squash": "acorn squash",
"alfalfa-sprouts": "alfalfa sprouts",
"acorn-squash": "meşe palamudu kabağı",
"alfalfa-sprouts": "yonca filizi",
"anchovies": "hamsi",
"apples": "elma",
"artichoke": "enginar",
@@ -10,14 +10,14 @@
"avocado": "avokado",
"bacon": "domuz pastırması",
"baking-powder": "kabartma tozu",
"baking-soda": "baking soda",
"baking-soda": "karbonat",
"baking-sugar": "baking sugar",
"bar-sugar": "kesme şeker",
"basil": "fesleğen",
"bell-peppers": "kırmızı biber",
"blackberries": "blackberries",
"blackberries": "böğürtlen",
"brassicas": "brassicas",
"bok-choy": "bok choy",
"bok-choy": "çin lahanası",
"broccoflower": "broccoflower",
"broccoli": "brokoli",
"broccolini": "broccolini",
@@ -27,7 +27,7 @@
"cauliflower": "karnabahar",
"chinese-leaves": "chinese leaves",
"collard-greens": "collard greens",
"kohlrabi": "kohlrabi",
"kohlrabi": "alabaş",
"bread": "ekmek",
"breadfruit": "breadfruit",
"broad-beans": "bakla",
@@ -35,64 +35,64 @@
"butter": "tereyağı",
"butternut-pumpkin": "butternut pumpkin",
"butternut-squash": "butternut squash",
"cactus-edible": "cactus, edible",
"cactus-edible": "yenilebilir kaktüs",
"calabrese": "calabrese",
"cannabis": "kenevir",
"capsicum": "capsicum",
"caraway": "caraway",
"capsicum": "kırmızı biber",
"caraway": "kimyon",
"carrot": "havuç",
"castor-sugar": "castor sugar",
"cayenne-pepper": "cayenne pepper",
"cayenne-pepper": "kırmızı biber",
"celeriac": "kereviz",
"celery": "kereviz",
"cereal-grains": "cereal grains",
"cereal-grains": "tam taneli tahıl",
"rice": "pirinç",
"chard": "chard",
"chard": "pazı",
"cheese": "peynir",
"chicory": "chicory",
"chilli-peppers": "chilli peppers",
"chives": "chives",
"chicory": "hindiba",
"chilli-peppers": "acı biber",
"chives": "frenk soğanı",
"chocolate": "çikolata",
"cilantro": "cilantro",
"cilantro": "kişniş",
"cinnamon": "tarçın",
"clarified-butter": "clarified butter",
"clarified-butter": "sade yağ",
"coconut": "hindistan cevizi",
"coconut-milk": "coconut milk",
"coconut-milk": "hindistan cevizi sütü",
"coffee": "kahve",
"confectioners-sugar": "confectioners' sugar",
"coriander": "coriander",
"corn": "corn",
"corn-syrup": "corn syrup",
"cottonseed-oil": "cottonseed oil",
"courgette": "courgette",
"cream-of-tartar": "cream of tartar",
"confectioners-sugar": "pudra şekeri",
"coriander": "kişniş",
"corn": "mısır",
"corn-syrup": "mısır şurubu",
"cottonseed-oil": "pamuk yağı",
"courgette": "dolmalık kabak",
"cream-of-tartar": "krem tartar",
"cucumber": "salatalık",
"cumin": "cumin",
"daikon": "daikon",
"dairy-products-and-dairy-substitutes": "dairy products and dairy substitutes",
"cumin": "kimyon",
"daikon": "beyaz turp",
"dairy-products-and-dairy-substitutes": "süt ürünleri ve süt yerine geçen ürünler",
"eggs": "yumurta",
"ghee": "ghee",
"ghee": "saf yağ",
"milk": "süt",
"dandelion": "dandelion",
"demerara-sugar": "demerara sugar",
"dandelion": "karahindiba",
"demerara-sugar": "esmer şeker",
"dough": "hamur",
"edible-cactus": "edible cactus",
"edible-cactus": "yenilebilir kaktüs",
"eggplant": "patlıcan",
"endive": "hindiba",
"fats": "yağlar",
"speck": "speck",
"fava-beans": "fava beans",
"fiddlehead": "fiddlehead",
"fava-beans": "fava fasulyesi",
"fiddlehead": "eğrelti otu filizi",
"fish": "balık",
"catfish": "kedibalığı ",
"cod": "cod",
"salt-cod": "salt cod",
"cod": "morina",
"salt-cod": "tuzlu morina",
"salmon": "somon",
"skate": "skate",
"stockfish": "stockfish",
"skate": "çemçe balığı",
"stockfish": "kurutulmuş balık",
"trout": "alabalık",
"tuna": "ton balığı",
"five-spice-powder": "five spice powder",
"five-spice-powder": "beşli baharat",
"flour": "un",
"frisee": "frisee",
"fructose": "fruktoz",
@@ -101,7 +101,7 @@
"oranges": "portakal",
"pear": "armut",
"tomato": "domates ",
"fruit-sugar": "fruit sugar",
"fruit-sugar": "meyve şekeri",
"garam-masala": "garam masala",
"garlic": "sarımsak",
"gem-squash": "gem squash",
@@ -109,7 +109,7 @@
"giblets": "giblets",
"grains": "tahıllar",
"maize": "maize",
"sweetcorn": "sweetcorn",
"sweetcorn": "tatlı mısır",
"teff": "teff",
"grape-seed-oil": "üzüm çekirdeği yağı",
"green-onion": "taze soğan",
@@ -125,69 +125,69 @@
"jaggery": "jaggery",
"jams": "reçel",
"jellies": "jellies",
"jerusalem-artichoke": "jerusalem artichoke",
"jicama": "jicama",
"jerusalem-artichoke": "yerelması",
"jicama": "meksika turpu",
"kale": "kale",
"kumara": "kumara",
"kumara": "tatlı patates",
"leavening-agents": "leavening agents",
"leek": "leek",
"legumes": "legumes ",
"peas": "peas",
"beans": "beans",
"lentils": "lentils",
"lemongrass": "lemongrass",
"lettuce": "lettuce",
"liver": "liver",
"maple-syrup": "maple syrup",
"meat": "meat",
"legumes": "baklagiller ",
"peas": "bezelye",
"beans": "fasulye",
"lentils": "mercimek",
"lemongrass": "limon otu",
"lettuce": "marul",
"liver": "karaciğer",
"maple-syrup": "akçaağaç şurubu",
"meat": "et",
"mortadella": "mortadella",
"mushroom": "mushroom",
"white-mushroom": "white mushroom",
"mussels": "mussels",
"mushroom": "mantar",
"white-mushroom": "beyaz mantar",
"mussels": "midye",
"nori": "nori",
"nutmeg": "nutmeg",
"nutmeg": "muskat",
"nutritional-yeast-flakes": "nutritional yeast flakes",
"nuts": "nuts",
"nanaimo-bar-mix": "nanaimo bar mix",
"octopuses": "octopuses",
"oils": "oils",
"octopuses": "ahtapotlar",
"oils": "yağ",
"olive-oil": "zeytin yağı",
"okra": "okra",
"okra": "bamya",
"olive": "zeytin",
"onion-family": "onion family",
"onion": "soğan",
"scallion": "scallion",
"shallot": "shallot",
"spring-onion": "spring onion",
"scallion": "taze soğan",
"shallot": "arpacık soğan",
"spring-onion": "yeşil soğan",
"orange-blossom-water": "orange blossom water",
"oysters": "istiridye",
"panch-puran": "panch puran",
"paprika": "kırmızı biber",
"parsnip": "parsnip",
"parsnip": "yaban havucu",
"pepper": "biber",
"peppers": "biber",
"plantain": "plantain",
"pineapple": "ananas",
"poppy-seeds": "poppy seeds",
"poppy-seeds": "haşhaş tohumu",
"potatoes": "patates",
"poultry": "kümes hayvanları",
"powdered-sugar": "powdered sugar",
"powdered-sugar": "pudra şekeri",
"pumpkin": "balkabağı",
"pumpkin-seeds": "kabak çekirdeği",
"radish": "turp",
"raw-sugar": "kesme şeker",
"refined-sugar": "refined sugar",
"refined-sugar": "rafine şeker",
"rice-flour": "pirinç unu",
"rock-sugar": "rock sugar",
"rock-sugar": "kide şekeri",
"rum": "rom",
"salt": "tuz",
"seafood": "deniz ürünleri",
"seeds": "tohumlar",
"sesame-seeds": "sesame seeds",
"sesame-seeds": "susam",
"sunflower-seeds": "ay çekirdeği",
"soda": "soda",
"soda-baking": "soda, baking",
"soybean": "soybean",
"soda-baking": "karbonat",
"soybean": "soya fasulyesi",
"spaghetti-squash": "spaghetti squash",
"spices": "baharatlar",
"spinach": "ıspanak",

View File

@@ -0,0 +1,65 @@
[
{
"name": "Produce"
},
{
"name": "Grains"
},
{
"name": "Fruits"
},
{
"name": "Vegetables"
},
{
"name": "Meat"
},
{
"name": "Seafood"
},
{
"name": "Beverages"
},
{
"name": "Baked Goods"
},
{
"name": "Canned Goods"
},
{
"name": "Condiments"
},
{
"name": "Confectionary"
},
{
"name": "Dairy Products"
},
{
"name": "Frozen Foods"
},
{
"name": "Health Foods"
},
{
"name": "Household"
},
{
"name": "Meat Products"
},
{
"name": "Snacks"
},
{
"name": "Spices"
},
{
"name": "Sweets"
},
{
"name": "Alcohol"
},
{
"name": "Other"
}
]

View File

@@ -0,0 +1,102 @@
{
"teaspoon": {
"name": "teaspoon",
"description": "",
"abbreviation": "tsp"
},
"tablespoon": {
"name": "tablespoon",
"description": "",
"abbreviation": "tbsp"
},
"cup": {
"name": "cup",
"description": "",
"abbreviation": "cup"
},
"fluid-ounce": {
"name": "fluid ounce",
"description": "",
"abbreviation": "fl oz"
},
"pint": {
"name": "pint",
"description": "",
"abbreviation": "pt"
},
"quart": {
"name": "quart",
"description": "",
"abbreviation": "qt"
},
"gallon": {
"name": "gallon",
"description": "",
"abbreviation": "gal"
},
"milliliter": {
"name": "milliliter",
"description": "",
"abbreviation": "ml"
},
"liter": {
"name": "liter",
"description": "",
"abbreviation": "l"
},
"pound": {
"name": "pound",
"description": "",
"abbreviation": "lb"
},
"ounce": {
"name": "ounce",
"description": "",
"abbreviation": "oz"
},
"gram": {
"name": "gram",
"description": "",
"abbreviation": "g"
},
"kilogram": {
"name": "kilogram",
"description": "",
"abbreviation": "kg"
},
"milligram": {
"name": "milligram",
"description": "",
"abbreviation": "mg"
},
"splash": {
"name": "splash",
"description": "",
"abbreviation": ""
},
"dash": {
"name": "dash",
"description": "",
"abbreviation": ""
},
"serving": {
"name": "serving",
"description": "",
"abbreviation": ""
},
"head": {
"name": "head",
"description": "",
"abbreviation": ""
},
"clove": {
"name": "clove",
"description": "",
"abbreviation": ""
},
"can": {
"name": "can",
"description": "",
"abbreviation": ""
}
}

View File

@@ -12,7 +12,7 @@
"cup": {
"name": "cup",
"description": "",
"abbreviation": "cup"
"abbreviation": "cană"
},
"fluid-ounce": {
"name": "fluid ounce",
@@ -20,7 +20,7 @@
"abbreviation": "fl oz"
},
"pint": {
"name": "pint",
"name": "halbă",
"description": "",
"abbreviation": "pt"
},
@@ -30,12 +30,12 @@
"abbreviation": "qt"
},
"gallon": {
"name": "gallon",
"name": "galon",
"description": "",
"abbreviation": "gal"
},
"milliliter": {
"name": "milliliter",
"name": "mililitru",
"description": "",
"abbreviation": "ml"
},
@@ -45,19 +45,19 @@
"abbreviation": "l"
},
"pound": {
"name": "pound",
"name": "livră",
"description": "",
"abbreviation": "lb"
},
"ounce": {
"name": "ounce",
"name": "uncie",
"description": "",
"abbreviation": "oz"
"abbreviation": "uncii"
},
"gram": {
"name": "gram",
"description": "",
"abbreviation": "g"
"abbreviation": "h"
},
"kilogram": {
"name": "kilogram",
@@ -65,7 +65,7 @@
"abbreviation": "kg"
},
"milligram": {
"name": "milligram",
"name": "miligram",
"description": "",
"abbreviation": "mg"
},
@@ -95,7 +95,7 @@
"abbreviation": ""
},
"can": {
"name": "can",
"name": "cutie",
"description": "",
"abbreviation": ""
}

View File

@@ -3,6 +3,7 @@ This file contains code taken from fastapi-utils project. The code is licensed u
See their repository for details -> https://github.com/dmontagu/fastapi-utils
"""
import inspect
from collections.abc import Callable
from typing import Any, TypeVar, cast, get_type_hints

View File

@@ -34,7 +34,7 @@ class MealieCrudRoute(APIRoute):
with contextlib.suppress(JSONDecodeError):
response = await original_route_handler(request)
response_body = json.loads(response.body)
if type(response_body) == dict:
if isinstance(response_body, dict):
if last_modified := response_body.get("updateAt"):
response.headers["last-modified"] = last_modified

View File

@@ -67,7 +67,8 @@ def get_token(
if "," in ip: # if there are multiple IPs, the first one is canonically the true client
ip = str(ip.split(",")[0])
else:
ip = request.client.host
# request.client should never be null, except sometimes during testing
ip = request.client.host if request.client else "unknown"
try:
user = authenticate_user(session, email, password) # type: ignore

View File

@@ -1,4 +1,5 @@
import os
import sys
import requests
@@ -14,9 +15,9 @@ def main():
r = requests.get(url)
if r.status_code == 200:
exit(0)
sys.exit(0)
else:
exit(1)
sys.exit(1)
if __name__ == "__main__":

View File

@@ -2,10 +2,11 @@ import datetime
import uuid
from os import path
from pathlib import Path
from typing import Any
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel
from sqlalchemy import ForeignKeyConstraint, MetaData, create_engine, insert, text
from sqlalchemy import ForeignKey, ForeignKeyConstraint, MetaData, Table, create_engine, insert, text
from sqlalchemy.engine import base
from sqlalchemy.orm import sessionmaker
@@ -41,13 +42,27 @@ class AlchemyExporter(BaseService):
self.session_maker = sessionmaker(bind=self.engine)
@staticmethod
def is_uuid(value: str) -> bool:
def is_uuid(value: Any) -> bool:
try:
uuid.UUID(value)
return True
except ValueError:
return False
@staticmethod
def is_valid_foreign_key(db_dump: dict[str, list[dict]], fk: ForeignKey, fk_value: Any) -> bool:
if not fk_value:
return True
foreign_table_name = fk.column.table.name
foreign_field_name = fk.column.name
for row in db_dump.get(foreign_table_name, []):
if row[foreign_field_name] == fk_value:
return True
return False
def convert_types(self, data: dict) -> dict:
"""
walks the dictionary to restore all things that look like string representations of their complex types
@@ -70,6 +85,33 @@ class AlchemyExporter(BaseService):
data[key] = self.DateTimeParser(time=value).time
return data
def clean_rows(self, db_dump: dict[str, list[dict]], table: Table, rows: list[dict]) -> list[dict]:
"""
Checks rows against foreign key restraints and removes any rows that would violate them
"""
fks = table.foreign_keys
valid_rows = []
for row in rows:
is_valid_row = True
for fk in fks:
fk_value = row.get(fk.parent.name)
if self.is_valid_foreign_key(db_dump, fk, row.get(fk.parent.name)):
continue
is_valid_row = False
self.logger.warning(
f"Removing row from table {table.name} because of invalid foreign key {fk.parent.name}: {fk_value}"
)
self.logger.warning(f"Row: {row}")
break
if is_valid_row:
valid_rows.append(row)
return valid_rows
def dump_schema(self) -> dict:
"""
Returns the schema of the SQLAlchemy database as a python dictionary. This dictionary is wrapped by
@@ -125,6 +167,7 @@ class AlchemyExporter(BaseService):
if not rows:
continue
table = self.meta.tables[table_name]
rows = self.clean_rows(db_dump, table, rows)
connection.execute(table.delete())
connection.execute(insert(table), rows)

View File

@@ -9,8 +9,7 @@ from mealie.services.backups_v2.alchemy_exporter import AlchemyExporter
from mealie.services.backups_v2.backup_file import BackupFile
class BackupSchemaMismatch(Exception):
...
class BackupSchemaMismatch(Exception): ...
class BackupV2(BaseService):
@@ -69,7 +68,7 @@ class BackupV2(BaseService):
shutil.copytree(f, self.directories.DATA_DIR / f.name)
def restore(self, backup_path: Path) -> None:
self.logger.info("initially backup restore")
self.logger.info("initializing backup restore")
backup = BackupFile(backup_path)

View File

@@ -3,6 +3,10 @@ import typing
from abc import ABC, abstractmethod
from dataclasses import dataclass
from email import message
from email.utils import formatdate
from uuid import uuid4
from html2text import html2text
from mealie.services._base_service import BaseService
@@ -36,8 +40,20 @@ class Message:
msg["Subject"] = self.subject
msg["From"] = f"{self.mail_from_name} <{self.mail_from_address}>"
msg["To"] = to
msg["Date"] = formatdate(localtime=True)
msg.add_alternative(html2text(self.html), subtype="plain")
msg.add_alternative(self.html, subtype="html")
try:
message_id = f"<{uuid4()}@{self.mail_from_address.split('@')[1]}>"
except IndexError:
# this should never happen with a valid email address,
# but we let the SMTP server handle it instead of raising it here
message_id = f"<{uuid4()}@{self.mail_from_address}>"
msg["Message-ID"] = message_id
msg["MIME-Version"] = "1.0"
if smtp.ssl:
with smtplib.SMTP_SSL(smtp.host, smtp.port) as server:
if smtp.username and smtp.password:
@@ -57,8 +73,7 @@ class Message:
class ABCEmailSender(ABC):
@abstractmethod
def send(self, email_to: str, subject: str, html: str) -> bool:
...
def send(self, email_to: str, subject: str, html: str) -> bool: ...
class DefaultEmailSender(ABCEmailSender, BaseService):

View File

@@ -100,9 +100,12 @@ class AppriseEventListener(EventListenerBase):
return [
# We use query params to add custom key: value pairs to the Apprise payload by prepending the key with ":".
AppriseEventListener.merge_query_parameters(url, {f":{k}": v for k, v in params.items()})
# only certain endpoints support the custom key: value pairs, so we only apply them to those endpoints
if AppriseEventListener.is_custom_url(url) else url
(
AppriseEventListener.merge_query_parameters(url, {f":{k}": v for k, v in params.items()})
# only certain endpoints support the custom key: value pairs, so we only apply them to those endpoints
if AppriseEventListener.is_custom_url(url)
else url
)
for url in urls
]

View File

@@ -8,8 +8,7 @@ from mealie.services.event_bus_service.event_types import Event
class PublisherLike(Protocol):
def publish(self, event: Event, notification_urls: list[str]):
...
def publish(self, event: Event, notification_urls: list[str]): ...
class ApprisePublisher:

View File

@@ -37,12 +37,10 @@ class ABCExporter(BaseService):
super().__init__()
@abstractproperty
def destination_dir(self) -> str:
...
def destination_dir(self) -> str: ...
@abstractmethod
def items(self) -> Iterator[ExportedItem]:
...
def items(self) -> Iterator[ExportedItem]: ...
def _post_export_hook(self, _: BaseModel) -> None:
pass

View File

@@ -39,7 +39,7 @@ class MealieAlphaMigrator(BaseMigrator):
with contextlib.suppress(KeyError):
if "" in recipe["categories"]:
recipe["categories"] = [cat for cat in recipe["categories"] if cat != ""]
if type(recipe["extras"]) == list:
if isinstance(recipe["extras"], list):
recipe["extras"] = {}
recipe["comments"] = []

View File

@@ -23,8 +23,7 @@ def plantoeat_recipes(file: Path):
for name in Path(tmpdir).glob("**/[!.]*.csv"):
with open(name, newline="") as csvfile:
reader = csv.DictReader(csvfile)
for row in reader:
yield row
yield from reader
def get_value_as_string_or_none(dictionary: dict, key: str):

View File

@@ -187,7 +187,7 @@ def import_data(lines):
token = unclump(token)
# turn B-NAME/123 back into "name"
tag, confidence = re.split(r"/", columns[-1], 1)
tag, confidence = re.split(r"/", columns[-1], maxsplit=1)
tag = re.sub(r"^[BI]\-", "", tag).lower() # noqa: W605 - invalid dscape sequence
# ====================

View File

@@ -106,12 +106,10 @@ class ABCIngredientParser(ABC):
return 70
@abstractmethod
def parse_one(self, ingredient_string: str) -> ParsedIngredient:
...
def parse_one(self, ingredient_string: str) -> ParsedIngredient: ...
@abstractmethod
def parse(self, ingredients: list[str]) -> list[ParsedIngredient]:
...
def parse(self, ingredients: list[str]) -> list[ParsedIngredient]: ...
@classmethod
def find_match(cls, match_value: str, *, store_map: dict[str, T], fuzzy_match_threshold: int = 0) -> T | None:

View File

@@ -98,7 +98,7 @@ class RecipeBulkScraperService(BaseService):
tasks = [_do(b.url) for b in urls.imports]
results = await asyncio.gather(*tasks, return_exceptions=True)
for b, recipe in zip(urls.imports, results, strict=True):
if not recipe or isinstance(recipe, Exception):
if not recipe or isinstance(recipe, BaseException):
continue
if b.tags:

View File

@@ -84,8 +84,7 @@ class ABCScraperStrategy(ABC):
self.translator = translator
@abstractmethod
async def get_html(self, url: str) -> str:
...
async def get_html(self, url: str) -> str: ...
@abstractmethod
async def parse(self) -> tuple[Recipe, ScrapedExtras] | tuple[None, None]: