mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-22 14:33:33 -07:00
refactor settings to class for testing
This commit is contained in:
parent
11c94f2987
commit
deeb8bd707
33 changed files with 292 additions and 315 deletions
8
makefile
8
makefile
|
@ -73,4 +73,10 @@ docker-prod: ## Build and Start Docker Production Stack
|
||||||
|
|
||||||
|
|
||||||
code-gen: ## Run Code-Gen Scripts
|
code-gen: ## Run Code-Gen Scripts
|
||||||
poetry run python dev/scripts/app_routes_gen.py
|
poetry run python dev/scripts/app_routes_gen.py
|
||||||
|
|
||||||
|
coverage: ## check code coverage quickly with the default Python
|
||||||
|
poetry run pytest
|
||||||
|
coverage report -m
|
||||||
|
coverage html
|
||||||
|
$(BROWSER) htmlcov/index.html
|
|
@ -3,20 +3,20 @@ from fastapi import FastAPI
|
||||||
from fastapi.logger import logger
|
from fastapi.logger import logger
|
||||||
|
|
||||||
# import utils.startup as startup
|
# import utils.startup as startup
|
||||||
from mealie.core.config import APP_VERSION, PORT, docs_url, redoc_url
|
from mealie.core.config import APP_VERSION, settings
|
||||||
from mealie.routes import backup_routes, debug_routes, migration_routes, theme_routes
|
from mealie.routes import backup_routes, debug_routes, migration_routes, theme_routes
|
||||||
from mealie.routes.site_settings import all_settings
|
|
||||||
from mealie.routes.groups import groups
|
from mealie.routes.groups import groups
|
||||||
from mealie.routes.mealplans import mealplans
|
from mealie.routes.mealplans import mealplans
|
||||||
from mealie.routes.recipe import all_recipe_routes, category_routes, recipe_crud_routes, tag_routes
|
from mealie.routes.recipe import all_recipe_routes, category_routes, recipe_crud_routes, tag_routes
|
||||||
|
from mealie.routes.site_settings import all_settings
|
||||||
from mealie.routes.users import users
|
from mealie.routes.users import users
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="Mealie",
|
title="Mealie",
|
||||||
description="A place for all your recipes",
|
description="A place for all your recipes",
|
||||||
version=APP_VERSION,
|
version=APP_VERSION,
|
||||||
docs_url=docs_url,
|
docs_url=settings.DOCS_URL,
|
||||||
redoc_url=redoc_url,
|
redoc_url=settings.REDOC_URL,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ def main():
|
||||||
uvicorn.run(
|
uvicorn.run(
|
||||||
"app:app",
|
"app:app",
|
||||||
host="0.0.0.0",
|
host="0.0.0.0",
|
||||||
port=PORT,
|
port=settings.API_PORT,
|
||||||
reload=True,
|
reload=True,
|
||||||
reload_dirs=["mealie"],
|
reload_dirs=["mealie"],
|
||||||
debug=True,
|
debug=True,
|
||||||
|
|
|
@ -8,80 +8,23 @@ APP_VERSION = "v0.4.0"
|
||||||
DB_VERSION = "v0.4.0"
|
DB_VERSION = "v0.4.0"
|
||||||
|
|
||||||
CWD = Path(__file__).parent
|
CWD = Path(__file__).parent
|
||||||
|
BASE_DIR = CWD.parent.parent
|
||||||
|
|
||||||
|
ENV = BASE_DIR.joinpath(".env")
|
||||||
def ensure_dirs():
|
|
||||||
for dir in REQUIRED_DIRS:
|
|
||||||
dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
|
|
||||||
# Register ENV
|
|
||||||
ENV = CWD.joinpath(".env") # ! I'm Broken Fix Me!
|
|
||||||
dotenv.load_dotenv(ENV)
|
dotenv.load_dotenv(ENV)
|
||||||
PRODUCTION = os.environ.get("ENV")
|
PRODUCTION = os.environ.get("ENV")
|
||||||
|
|
||||||
|
|
||||||
# General
|
def determine_data_dir(production: bool) -> Path:
|
||||||
PORT = int(os.getenv("mealie_port", 9000))
|
global CWD
|
||||||
API = os.getenv("api_docs", True)
|
if production:
|
||||||
|
return Path("/app/data")
|
||||||
|
|
||||||
if API:
|
return CWD.parent.parent.joinpath("dev", "data")
|
||||||
docs_url = "/docs"
|
|
||||||
redoc_url = "/redoc"
|
|
||||||
else:
|
|
||||||
docs_url = None
|
|
||||||
redoc_url = None
|
|
||||||
|
|
||||||
# Helpful Globals
|
|
||||||
DATA_DIR = CWD.parent.parent.joinpath("dev", "data")
|
|
||||||
if PRODUCTION:
|
|
||||||
DATA_DIR = Path("/app/data")
|
|
||||||
|
|
||||||
WEB_PATH = CWD.joinpath("dist")
|
|
||||||
IMG_DIR = DATA_DIR.joinpath("img")
|
|
||||||
BACKUP_DIR = DATA_DIR.joinpath("backups")
|
|
||||||
DEBUG_DIR = DATA_DIR.joinpath("debug")
|
|
||||||
MIGRATION_DIR = DATA_DIR.joinpath("migration")
|
|
||||||
NEXTCLOUD_DIR = MIGRATION_DIR.joinpath("nextcloud")
|
|
||||||
CHOWDOWN_DIR = MIGRATION_DIR.joinpath("chowdown")
|
|
||||||
TEMPLATE_DIR = DATA_DIR.joinpath("templates")
|
|
||||||
USER_DIR = DATA_DIR.joinpath("users")
|
|
||||||
SQLITE_DIR = DATA_DIR.joinpath("db")
|
|
||||||
RECIPE_DATA_DIR = DATA_DIR.joinpath("recipes")
|
|
||||||
TEMP_DIR = DATA_DIR.joinpath(".temp")
|
|
||||||
|
|
||||||
REQUIRED_DIRS = [
|
|
||||||
DATA_DIR,
|
|
||||||
IMG_DIR,
|
|
||||||
BACKUP_DIR,
|
|
||||||
DEBUG_DIR,
|
|
||||||
MIGRATION_DIR,
|
|
||||||
TEMPLATE_DIR,
|
|
||||||
SQLITE_DIR,
|
|
||||||
NEXTCLOUD_DIR,
|
|
||||||
CHOWDOWN_DIR,
|
|
||||||
RECIPE_DATA_DIR,
|
|
||||||
USER_DIR,
|
|
||||||
]
|
|
||||||
|
|
||||||
ensure_dirs()
|
|
||||||
|
|
||||||
LOGGER_FILE = DATA_DIR.joinpath("mealie.log")
|
|
||||||
|
|
||||||
|
|
||||||
# DATABASE ENV
|
def determine_secrets(production: bool) -> str:
|
||||||
SQLITE_FILE = None
|
if not production:
|
||||||
DATABASE_TYPE = os.getenv("DB_TYPE", "sqlite")
|
|
||||||
if DATABASE_TYPE == "sqlite":
|
|
||||||
USE_SQL = True
|
|
||||||
SQLITE_FILE = SQLITE_DIR.joinpath(f"mealie_{DB_VERSION}.sqlite")
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise Exception("Unable to determine database type. Acceptible options are 'sqlite' ")
|
|
||||||
|
|
||||||
|
|
||||||
def determine_secrets() -> str:
|
|
||||||
if not PRODUCTION:
|
|
||||||
return "shh-secret-test-key"
|
return "shh-secret-test-key"
|
||||||
|
|
||||||
secrets_file = DATA_DIR.joinpath(".secret")
|
secrets_file = DATA_DIR.joinpath(".secret")
|
||||||
|
@ -90,22 +33,75 @@ def determine_secrets() -> str:
|
||||||
return f.read()
|
return f.read()
|
||||||
else:
|
else:
|
||||||
with open(secrets_file, "w") as f:
|
with open(secrets_file, "w") as f:
|
||||||
f.write(secrets.token_hex(32))
|
new_secret = secrets.token_hex(32)
|
||||||
|
f.write(new_secret)
|
||||||
|
return new_secret
|
||||||
|
|
||||||
|
|
||||||
SECRET = "determine_secrets()"
|
class AppDirectories:
|
||||||
|
def __init__(self, cwd, data_dir) -> None:
|
||||||
|
self.WEB_PATH = cwd.joinpath("dist")
|
||||||
|
self.IMG_DIR = data_dir.joinpath("img")
|
||||||
|
self.BACKUP_DIR = data_dir.joinpath("backups")
|
||||||
|
self.DEBUG_DIR = data_dir.joinpath("debug")
|
||||||
|
self.MIGRATION_DIR = data_dir.joinpath("migration")
|
||||||
|
self.NEXTCLOUD_DIR = self.MIGRATION_DIR.joinpath("nextcloud")
|
||||||
|
self.CHOWDOWN_DIR = self.MIGRATION_DIR.joinpath("chowdown")
|
||||||
|
self.TEMPLATE_DIR = data_dir.joinpath("templates")
|
||||||
|
self.USER_DIR = data_dir.joinpath("users")
|
||||||
|
self.SQLITE_DIR = data_dir.joinpath("db")
|
||||||
|
self.RECIPE_DATA_DIR = data_dir.joinpath("recipes")
|
||||||
|
self.TEMP_DIR = data_dir.joinpath(".temp")
|
||||||
|
|
||||||
# Mongo Database
|
self.ensure_directories()
|
||||||
DEFAULT_GROUP = os.getenv("DEFAULT_GROUP", "Home")
|
|
||||||
DEFAULT_PASSWORD = os.getenv("DEFAULT_PASSWORD", "MyPassword")
|
|
||||||
|
|
||||||
# Database
|
def ensure_directories(self):
|
||||||
MEALIE_DB_NAME = os.getenv("mealie_db_name", "mealie")
|
required_dirs = [
|
||||||
DB_USERNAME = os.getenv("db_username", "root")
|
self.IMG_DIR,
|
||||||
DB_PASSWORD = os.getenv("db_password", "example")
|
self.BACKUP_DIR,
|
||||||
DB_HOST = os.getenv("db_host", "mongo")
|
self.DEBUG_DIR,
|
||||||
DB_PORT = os.getenv("db_port", 27017)
|
self.MIGRATION_DIR,
|
||||||
|
self.TEMPLATE_DIR,
|
||||||
|
self.SQLITE_DIR,
|
||||||
|
self.NEXTCLOUD_DIR,
|
||||||
|
self.CHOWDOWN_DIR,
|
||||||
|
self.RECIPE_DATA_DIR,
|
||||||
|
self.USER_DIR,
|
||||||
|
]
|
||||||
|
|
||||||
# SFTP Email Stuff - For use Later down the line!
|
for dir in required_dirs:
|
||||||
SFTP_USERNAME = os.getenv("sftp_username", None)
|
dir.mkdir(parents=True, exist_ok=True)
|
||||||
SFTP_PASSWORD = os.getenv("sftp_password", None)
|
|
||||||
|
|
||||||
|
class AppSettings:
|
||||||
|
def __init__(self, app_dirs: AppDirectories) -> None:
|
||||||
|
global DB_VERSION
|
||||||
|
self.PRODUCTION = bool(os.environ.get("ENV"))
|
||||||
|
self.API_PORT = int(os.getenv("API_PORT", 9000))
|
||||||
|
self.API = bool(os.getenv("API_DOCS", True))
|
||||||
|
self.DOCS_URL = "/docs" if self.API else None
|
||||||
|
self.REDOC_URL = "/redoc" if self.API else None
|
||||||
|
self.SECRET = determine_secrets(self.PRODUCTION)
|
||||||
|
self.DATABASE_TYPE = os.getenv("DB_TYPE", "sqlite")
|
||||||
|
|
||||||
|
# Used to Set SQLite File Version
|
||||||
|
self.SQLITE_FILE = None
|
||||||
|
if self.DATABASE_TYPE == "sqlite":
|
||||||
|
self.SQLITE_FILE = app_dirs.SQLITE_DIR.joinpath(f"mealie_{DB_VERSION}.sqlite")
|
||||||
|
else:
|
||||||
|
raise Exception("Unable to determine database type. Acceptible options are 'sqlite' ")
|
||||||
|
|
||||||
|
self.DEFAULT_GROUP = os.getenv("DEFAULT_GROUP", "Home")
|
||||||
|
self.DEFAULT_PASSWORD = os.getenv("DEFAULT_PASSWORD", "MyPassword")
|
||||||
|
|
||||||
|
# Not Used!
|
||||||
|
self.SFTP_USERNAME = os.getenv("SFTP_USERNAME", None)
|
||||||
|
self.SFTP_PASSWORD = os.getenv("SFTP_PASSWORD", None)
|
||||||
|
|
||||||
|
|
||||||
|
# General
|
||||||
|
DATA_DIR = determine_data_dir(PRODUCTION)
|
||||||
|
LOGGER_FILE = DATA_DIR.joinpath("mealie.log")
|
||||||
|
|
||||||
|
app_dirs = AppDirectories(CWD, DATA_DIR)
|
||||||
|
settings = AppSettings(app_dirs)
|
||||||
|
|
|
@ -2,7 +2,7 @@ from datetime import datetime, timedelta
|
||||||
from mealie.schema.user import UserInDB
|
from mealie.schema.user import UserInDB
|
||||||
|
|
||||||
from jose import jwt
|
from jose import jwt
|
||||||
from mealie.core.config import SECRET
|
from mealie.core.config import settings
|
||||||
from mealie.db.database import db
|
from mealie.db.database import db
|
||||||
from passlib.context import CryptContext
|
from passlib.context import CryptContext
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ def create_access_token(data: dict(), expires_delta: timedelta = None) -> str:
|
||||||
else:
|
else:
|
||||||
expire = datetime.utcnow() + timedelta(minutes=120)
|
expire = datetime.utcnow() + timedelta(minutes=120)
|
||||||
to_encode.update({"exp": expire})
|
to_encode.update({"exp": expire})
|
||||||
return jwt.encode(to_encode, SECRET, algorithm=ALGORITHM)
|
return jwt.encode(to_encode, settings.SECRET, algorithm=ALGORITHM)
|
||||||
|
|
||||||
|
|
||||||
def authenticate_user(session, email: str, password: str) -> UserInDB:
|
def authenticate_user(session, email: str, password: str) -> UserInDB:
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
|
from mealie.db.models.model_base import SqlAlchemyBase
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from sqlalchemy.orm import load_only
|
from sqlalchemy.orm import load_only
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
from mealie.db.models.model_base import SqlAlchemyBase
|
|
||||||
|
|
||||||
|
|
||||||
class BaseDocument:
|
class BaseDocument:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
|
@ -21,12 +20,12 @@ class BaseDocument:
|
||||||
if self.orm_mode:
|
if self.orm_mode:
|
||||||
return [self.schema.from_orm(x) for x in session.query(self.sql_model).limit(limit).all()]
|
return [self.schema.from_orm(x) for x in session.query(self.sql_model).limit(limit).all()]
|
||||||
|
|
||||||
list = [x.dict() for x in session.query(self.sql_model).limit(limit).all()]
|
# list = [x.dict() for x in session.query(self.sql_model).limit(limit).all()]
|
||||||
|
|
||||||
if limit == 1:
|
# if limit == 1:
|
||||||
return list[0]
|
# return list[0]
|
||||||
|
|
||||||
return list
|
# return list
|
||||||
|
|
||||||
def get_all_limit_columns(self, session: Session, fields: List[str], limit: int = None) -> List[SqlAlchemyBase]:
|
def get_all_limit_columns(self, session: Session, fields: List[str], limit: int = None) -> List[SqlAlchemyBase]:
|
||||||
"""Queries the database for the selected model. Restricts return responses to the
|
"""Queries the database for the selected model. Restricts return responses to the
|
||||||
|
@ -95,6 +94,7 @@ class BaseDocument:
|
||||||
return self.schema.from_orm(result[0])
|
return self.schema.from_orm(result[0])
|
||||||
except IndexError:
|
except IndexError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return [self.schema.from_orm(x) for x in result]
|
return [self.schema.from_orm(x) for x in result]
|
||||||
|
|
||||||
def create(self, session: Session, document: dict) -> BaseModel:
|
def create(self, session: Session, document: dict) -> BaseModel:
|
||||||
|
@ -111,10 +111,8 @@ class BaseDocument:
|
||||||
session.add(new_document)
|
session.add(new_document)
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
if self.orm_mode:
|
return self.schema.from_orm(new_document)
|
||||||
return self.schema.from_orm(new_document)
|
|
||||||
|
|
||||||
return new_document.dict()
|
|
||||||
|
|
||||||
def update(self, session: Session, match_value: str, new_data: str) -> BaseModel:
|
def update(self, session: Session, match_value: str, new_data: str) -> BaseModel:
|
||||||
"""Update a database entry.
|
"""Update a database entry.
|
||||||
|
@ -131,13 +129,10 @@ class BaseDocument:
|
||||||
entry = self._query_one(session=session, match_value=match_value)
|
entry = self._query_one(session=session, match_value=match_value)
|
||||||
entry.update(session=session, **new_data)
|
entry.update(session=session, **new_data)
|
||||||
|
|
||||||
if self.orm_mode:
|
|
||||||
session.commit()
|
|
||||||
return self.schema.from_orm(entry)
|
|
||||||
|
|
||||||
return_data = entry.dict()
|
|
||||||
session.commit()
|
session.commit()
|
||||||
return return_data
|
return self.schema.from_orm(entry)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def delete(self, session: Session, primary_key_value) -> dict:
|
def delete(self, session: Session, primary_key_value) -> dict:
|
||||||
result = session.query(self.sql_model).filter_by(**{self.primary_key: primary_key_value}).one()
|
result = session.query(self.sql_model).filter_by(**{self.primary_key: primary_key_value}).one()
|
||||||
|
|
|
@ -1,15 +1,12 @@
|
||||||
from mealie.core.config import SQLITE_FILE, USE_SQL
|
from mealie.core.config import settings
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
from mealie.db.models.db_session import sql_global_init
|
from mealie.db.models.db_session import sql_global_init
|
||||||
|
|
||||||
sql_exists = True
|
sql_exists = True
|
||||||
|
|
||||||
if USE_SQL:
|
sql_exists = settings.SQLITE_FILE.is_file()
|
||||||
sql_exists = SQLITE_FILE.is_file()
|
SessionLocal = sql_global_init(settings.SQLITE_FILE)
|
||||||
SessionLocal = sql_global_init(SQLITE_FILE)
|
|
||||||
else:
|
|
||||||
raise Exception("Cannot identify database type")
|
|
||||||
|
|
||||||
|
|
||||||
def create_session() -> Session:
|
def create_session() -> Session:
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from fastapi.logger import logger
|
from fastapi.logger import logger
|
||||||
from mealie.core.config import DEFAULT_GROUP, DEFAULT_PASSWORD
|
from mealie.core.config import settings
|
||||||
from mealie.core.security import get_password_hash
|
from mealie.core.security import get_password_hash
|
||||||
from mealie.db.database import db
|
from mealie.db.database import db
|
||||||
from mealie.db.db_setup import create_session, sql_exists
|
from mealie.db.db_setup import create_session, sql_exists
|
||||||
|
@ -30,7 +30,7 @@ def default_settings_init(session: Session):
|
||||||
|
|
||||||
|
|
||||||
def default_group_init(session: Session):
|
def default_group_init(session: Session):
|
||||||
default_group = {"name": DEFAULT_GROUP}
|
default_group = {"name": settings.DEFAULT_GROUP}
|
||||||
logger.info("Generating Default Group")
|
logger.info("Generating Default Group")
|
||||||
db.groups.create(session, default_group)
|
db.groups.create(session, default_group)
|
||||||
|
|
||||||
|
@ -39,8 +39,8 @@ def default_user_init(session: Session):
|
||||||
default_user = {
|
default_user = {
|
||||||
"full_name": "Change Me",
|
"full_name": "Change Me",
|
||||||
"email": "changeme@email.com",
|
"email": "changeme@email.com",
|
||||||
"password": get_password_hash(DEFAULT_PASSWORD),
|
"password": get_password_hash(settings.DEFAULT_PASSWORD),
|
||||||
"group": DEFAULT_GROUP,
|
"group": settings.DEFAULT_GROUP,
|
||||||
"admin": True,
|
"admin": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
import sqlalchemy.orm as orm
|
import sqlalchemy.orm as orm
|
||||||
from fastapi.logger import logger
|
from fastapi.logger import logger
|
||||||
from mealie.core.config import DEFAULT_GROUP
|
from mealie.core.config import settings
|
||||||
from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase
|
from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase
|
||||||
from mealie.db.models.recipe.category import Category, group2categories
|
from mealie.db.models.recipe.category import Category, group2categories
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
@ -63,7 +63,7 @@ class Group(SqlAlchemyBase, BaseMixins):
|
||||||
return item
|
return item
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_if_not_exist(session, name: str = DEFAULT_GROUP):
|
def create_if_not_exist(session, name: str = settings.DEFAULT_GROUP):
|
||||||
result = session.query(Group).filter(Group.name == name).one_or_none()
|
result = session.query(Group).filter(Group.name == name).one_or_none()
|
||||||
if result:
|
if result:
|
||||||
logger.info("Group exists, associating recipe")
|
logger.info("Group exists, associating recipe")
|
||||||
|
|
|
@ -1,47 +1,8 @@
|
||||||
from typing import List
|
|
||||||
|
|
||||||
import sqlalchemy.ext.declarative as dec
|
import sqlalchemy.ext.declarative as dec
|
||||||
from sqlalchemy.orm.session import Session
|
|
||||||
|
|
||||||
SqlAlchemyBase = dec.declarative_base()
|
SqlAlchemyBase = dec.declarative_base()
|
||||||
|
|
||||||
|
|
||||||
class BaseMixins:
|
class BaseMixins:
|
||||||
@staticmethod
|
def _pass_on_me():
|
||||||
def _sql_remove_list(session: Session, list_of_tables: list, parent_id):
|
pass
|
||||||
for table in list_of_tables:
|
|
||||||
session.query(table).filter(parent_id == parent_id).delete()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _flatten_dict(list_of_dict: List[dict]):
|
|
||||||
finalMap = {}
|
|
||||||
for d in list_of_dict:
|
|
||||||
|
|
||||||
finalMap.update(d.dict())
|
|
||||||
|
|
||||||
return finalMap
|
|
||||||
|
|
||||||
|
|
||||||
# ! Don't use!
|
|
||||||
def update_generics(func):
|
|
||||||
"""An experimental function that does the initial work of updating attributes on a class
|
|
||||||
and passing "complex" data types recuresively to an "self.update()" function if one exists.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
func ([type]): [description]
|
|
||||||
"""
|
|
||||||
|
|
||||||
def wrapper(class_object, session, new_data: dict):
|
|
||||||
complex_attributed = {}
|
|
||||||
for key, value in new_data.items():
|
|
||||||
|
|
||||||
attribute = getattr(class_object, key, None)
|
|
||||||
|
|
||||||
if attribute and isinstance(attribute, SqlAlchemyBase):
|
|
||||||
attribute.update(session, value)
|
|
||||||
|
|
||||||
elif attribute:
|
|
||||||
setattr(class_object, key, value)
|
|
||||||
func(class_object, complex_attributed)
|
|
||||||
|
|
||||||
return wrapper
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from mealie.core.config import DEFAULT_GROUP
|
from mealie.core.config import settings
|
||||||
from mealie.db.models.group import Group
|
from mealie.db.models.group import Group
|
||||||
from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase
|
from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase
|
||||||
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, orm
|
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, orm
|
||||||
|
@ -26,12 +26,12 @@ class User(SqlAlchemyBase, BaseMixins):
|
||||||
full_name,
|
full_name,
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
group: str = DEFAULT_GROUP,
|
group: str = settings.DEFAULT_GROUP,
|
||||||
admin=False,
|
admin=False,
|
||||||
id=None,
|
id=None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
group = group if group else DEFAULT_GROUP
|
group = group or settings.DEFAULT_GROUP
|
||||||
self.full_name = full_name
|
self.full_name = full_name
|
||||||
self.email = email
|
self.email = email
|
||||||
self.group = Group.get_ref(session, group)
|
self.group = Group.get_ref(session, group)
|
||||||
|
|
|
@ -2,7 +2,7 @@ import operator
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
||||||
from mealie.core.config import BACKUP_DIR, TEMPLATE_DIR
|
from mealie.core.config import app_dirs
|
||||||
from mealie.db.db_setup import generate_session
|
from mealie.db.db_setup import generate_session
|
||||||
from mealie.routes.deps import get_current_user
|
from mealie.routes.deps import get_current_user
|
||||||
from mealie.schema.backup import BackupJob, ImportJob, Imports, LocalBackup
|
from mealie.schema.backup import BackupJob, ImportJob, Imports, LocalBackup
|
||||||
|
@ -19,11 +19,11 @@ router = APIRouter(prefix="/api/backups", tags=["Backups"], dependencies=[Depend
|
||||||
def available_imports():
|
def available_imports():
|
||||||
"""Returns a list of avaiable .zip files for import into Mealie."""
|
"""Returns a list of avaiable .zip files for import into Mealie."""
|
||||||
imports = []
|
imports = []
|
||||||
for archive in BACKUP_DIR.glob("*.zip"):
|
for archive in app_dirs.app_dirs.BACKUP_DIR.glob("*.zip"):
|
||||||
backup = LocalBackup(name=archive.name, date=archive.stat().st_ctime)
|
backup = LocalBackup(name=archive.name, date=archive.stat().st_ctime)
|
||||||
imports.append(backup)
|
imports.append(backup)
|
||||||
|
|
||||||
templates = [template.name for template in TEMPLATE_DIR.glob("*.*")]
|
templates = [template.name for template in app_dirs.TEMPLATE_DIR.glob("*.*")]
|
||||||
imports.sort(key=operator.attrgetter("date"), reverse=True)
|
imports.sort(key=operator.attrgetter("date"), reverse=True)
|
||||||
|
|
||||||
return Imports(imports=imports, templates=templates)
|
return Imports(imports=imports, templates=templates)
|
||||||
|
@ -55,7 +55,7 @@ def export_database(data: BackupJob, session: Session = Depends(generate_session
|
||||||
@router.post("/upload")
|
@router.post("/upload")
|
||||||
def upload_backup_file(archive: UploadFile = File(...)):
|
def upload_backup_file(archive: UploadFile = File(...)):
|
||||||
""" Upload a .zip File to later be imported into Mealie """
|
""" Upload a .zip File to later be imported into Mealie """
|
||||||
dest = BACKUP_DIR.joinpath(archive.filename)
|
dest = app_dirs.BACKUP_DIR.joinpath(archive.filename)
|
||||||
|
|
||||||
with dest.open("wb") as buffer:
|
with dest.open("wb") as buffer:
|
||||||
shutil.copyfileobj(archive.file, buffer)
|
shutil.copyfileobj(archive.file, buffer)
|
||||||
|
@ -69,7 +69,7 @@ def upload_backup_file(archive: UploadFile = File(...)):
|
||||||
@router.get("/{file_name}/download")
|
@router.get("/{file_name}/download")
|
||||||
async def download_backup_file(file_name: str):
|
async def download_backup_file(file_name: str):
|
||||||
""" Upload a .zip File to later be imported into Mealie """
|
""" Upload a .zip File to later be imported into Mealie """
|
||||||
file = BACKUP_DIR.joinpath(file_name)
|
file = app_dirs.BACKUP_DIR.joinpath(file_name)
|
||||||
|
|
||||||
if file.is_file:
|
if file.is_file:
|
||||||
return FileResponse(file, media_type="application/octet-stream", filename=file_name)
|
return FileResponse(file, media_type="application/octet-stream", filename=file_name)
|
||||||
|
@ -100,7 +100,7 @@ def delete_backup(file_name: str):
|
||||||
""" Removes a database backup from the file system """
|
""" Removes a database backup from the file system """
|
||||||
|
|
||||||
try:
|
try:
|
||||||
BACKUP_DIR.joinpath(file_name).unlink()
|
app_dirs.BACKUP_DIR.joinpath(file_name).unlink()
|
||||||
except:
|
except:
|
||||||
HTTPException(
|
HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
from mealie.core.config import APP_VERSION, DEBUG_DIR, LOGGER_FILE
|
from mealie.core.config import APP_VERSION, LOGGER_FILE, app_dirs
|
||||||
from mealie.routes.deps import get_current_user
|
from mealie.routes.deps import get_current_user
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/debug", tags=["Debug"], dependencies=[Depends(get_current_user)])
|
router = APIRouter(prefix="/api/debug", tags=["Debug"], dependencies=[Depends(get_current_user)])
|
||||||
|
@ -17,7 +17,7 @@ async def get_mealie_version():
|
||||||
async def get_last_recipe_json():
|
async def get_last_recipe_json():
|
||||||
""" Doc Str """
|
""" Doc Str """
|
||||||
|
|
||||||
with open(DEBUG_DIR.joinpath("last_recipe.json"), "r") as f:
|
with open(app_dirs.DEBUG_DIR.joinpath("last_recipe.json"), "r") as f:
|
||||||
return json.loads(f.read())
|
return json.loads(f.read())
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from fastapi import Depends, HTTPException, status
|
from fastapi import Depends, HTTPException, status
|
||||||
from fastapi.security import OAuth2PasswordBearer
|
from fastapi.security import OAuth2PasswordBearer
|
||||||
from jose import JWTError, jwt
|
from jose import JWTError, jwt
|
||||||
from mealie.core.config import SECRET
|
from mealie.core.config import settings
|
||||||
from mealie.db.database import db
|
from mealie.db.database import db
|
||||||
from mealie.db.db_setup import generate_session
|
from mealie.db.db_setup import generate_session
|
||||||
from mealie.schema.auth import TokenData
|
from mealie.schema.auth import TokenData
|
||||||
|
@ -18,7 +18,7 @@ async def get_current_user(token: str = Depends(oauth2_scheme), session=Depends(
|
||||||
headers={"WWW-Authenticate": "Bearer"},
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
payload = jwt.decode(token, SECRET, algorithms=[ALGORITHM])
|
payload = jwt.decode(token, settings.SECRET, algorithms=[ALGORITHM])
|
||||||
username: str = payload.get("sub")
|
username: str = payload.get("sub")
|
||||||
if username is None:
|
if username is None:
|
||||||
raise credentials_exception
|
raise credentials_exception
|
||||||
|
|
|
@ -3,7 +3,7 @@ import shutil
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, File, UploadFile
|
from fastapi import APIRouter, Depends, File, UploadFile
|
||||||
from mealie.core.config import MIGRATION_DIR
|
from mealie.core.config import app_dirs
|
||||||
from mealie.db.db_setup import generate_session
|
from mealie.db.db_setup import generate_session
|
||||||
from mealie.routes.deps import get_current_user
|
from mealie.routes.deps import get_current_user
|
||||||
from mealie.schema.migration import MigrationFile, Migrations
|
from mealie.schema.migration import MigrationFile, Migrations
|
||||||
|
@ -20,8 +20,8 @@ def get_avaiable_nextcloud_imports():
|
||||||
""" Returns a list of avaiable directories that can be imported into Mealie """
|
""" Returns a list of avaiable directories that can be imported into Mealie """
|
||||||
response_data = []
|
response_data = []
|
||||||
migration_dirs = [
|
migration_dirs = [
|
||||||
MIGRATION_DIR.joinpath("nextcloud"),
|
app_dirs.MIGRATION_DIR.joinpath("nextcloud"),
|
||||||
MIGRATION_DIR.joinpath("chowdown"),
|
app_dirs.MIGRATION_DIR.joinpath("chowdown"),
|
||||||
]
|
]
|
||||||
for directory in migration_dirs:
|
for directory in migration_dirs:
|
||||||
migration = Migrations(type=directory.stem)
|
migration = Migrations(type=directory.stem)
|
||||||
|
@ -39,7 +39,7 @@ def get_avaiable_nextcloud_imports():
|
||||||
@router.post("/{type}/{file_name}/import")
|
@router.post("/{type}/{file_name}/import")
|
||||||
def import_nextcloud_directory(type: str, file_name: str, session: Session = Depends(generate_session)):
|
def import_nextcloud_directory(type: str, file_name: str, session: Session = Depends(generate_session)):
|
||||||
""" Imports all the recipes in a given directory """
|
""" Imports all the recipes in a given directory """
|
||||||
file_path = MIGRATION_DIR.joinpath(type, file_name)
|
file_path = app_dirs.MIGRATION_DIR.joinpath(type, file_name)
|
||||||
if type == "nextcloud":
|
if type == "nextcloud":
|
||||||
return nextcloud_migrate(session, file_path)
|
return nextcloud_migrate(session, file_path)
|
||||||
elif type == "chowdown":
|
elif type == "chowdown":
|
||||||
|
@ -52,7 +52,7 @@ def import_nextcloud_directory(type: str, file_name: str, session: Session = Dep
|
||||||
def delete_migration_data(type: str, file_name: str):
|
def delete_migration_data(type: str, file_name: str):
|
||||||
""" Removes migration data from the file system """
|
""" Removes migration data from the file system """
|
||||||
|
|
||||||
remove_path = MIGRATION_DIR.joinpath(type, file_name)
|
remove_path = app_dirs.MIGRATION_DIR.joinpath(type, file_name)
|
||||||
|
|
||||||
if remove_path.is_file():
|
if remove_path.is_file():
|
||||||
remove_path.unlink()
|
remove_path.unlink()
|
||||||
|
@ -67,7 +67,7 @@ def delete_migration_data(type: str, file_name: str):
|
||||||
@router.post("/{type}/upload")
|
@router.post("/{type}/upload")
|
||||||
def upload_nextcloud_zipfile(type: str, archive: UploadFile = File(...)):
|
def upload_nextcloud_zipfile(type: str, archive: UploadFile = File(...)):
|
||||||
""" Upload a .zip File to later be imported into Mealie """
|
""" Upload a .zip File to later be imported into Mealie """
|
||||||
dir = MIGRATION_DIR.joinpath(type)
|
dir = app_dirs.MIGRATION_DIR.joinpath(type)
|
||||||
dir.mkdir(parents=True, exist_ok=True)
|
dir.mkdir(parents=True, exist_ok=True)
|
||||||
dest = dir.joinpath(archive.filename)
|
dest = dir.joinpath(archive.filename)
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ from datetime import timedelta
|
||||||
from fastapi import APIRouter, Depends, File, UploadFile
|
from fastapi import APIRouter, Depends, File, UploadFile
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
from mealie.core import security
|
from mealie.core import security
|
||||||
from mealie.core.config import DEFAULT_PASSWORD, USER_DIR
|
from mealie.core.config import settings, app_dirs
|
||||||
from mealie.core.security import get_password_hash, verify_password
|
from mealie.core.security import get_password_hash, verify_password
|
||||||
from mealie.db.database import db
|
from mealie.db.database import db
|
||||||
from mealie.db.db_setup import generate_session
|
from mealie.db.db_setup import generate_session
|
||||||
|
@ -65,7 +65,7 @@ async def reset_user_password(
|
||||||
session: Session = Depends(generate_session),
|
session: Session = Depends(generate_session),
|
||||||
):
|
):
|
||||||
|
|
||||||
new_password = get_password_hash(DEFAULT_PASSWORD)
|
new_password = get_password_hash(settings.DEFAULT_PASSWORD)
|
||||||
db.users.update_password(session, id, new_password)
|
db.users.update_password(session, id, new_password)
|
||||||
|
|
||||||
return SnackResponse.success("Users Password Reset")
|
return SnackResponse.success("Users Password Reset")
|
||||||
|
@ -92,7 +92,7 @@ async def update_user(
|
||||||
@router.get("/{id}/image")
|
@router.get("/{id}/image")
|
||||||
async def get_user_image(id: str):
|
async def get_user_image(id: str):
|
||||||
""" Returns a users profile picture """
|
""" Returns a users profile picture """
|
||||||
user_dir = USER_DIR.joinpath(id)
|
user_dir = app_dirs.USER_DIR.joinpath(id)
|
||||||
for recipe_image in user_dir.glob("profile_image.*"):
|
for recipe_image in user_dir.glob("profile_image.*"):
|
||||||
return FileResponse(recipe_image)
|
return FileResponse(recipe_image)
|
||||||
else:
|
else:
|
||||||
|
@ -109,14 +109,14 @@ async def update_user_image(
|
||||||
|
|
||||||
extension = profile_image.filename.split(".")[-1]
|
extension = profile_image.filename.split(".")[-1]
|
||||||
|
|
||||||
USER_DIR.joinpath(id).mkdir(parents=True, exist_ok=True)
|
app_dirs.USER_DIR.joinpath(id).mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
[x.unlink() for x in USER_DIR.join(id).glob("profile_image.*")]
|
[x.unlink() for x in app_dirs.USER_DIR.join(id).glob("profile_image.*")]
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
dest = USER_DIR.joinpath(id, f"profile_image.{extension}")
|
dest = app_dirs.USER_DIR.joinpath(id, f"profile_image.{extension}")
|
||||||
|
|
||||||
with dest.open("wb") as buffer:
|
with dest.open("wb") as buffer:
|
||||||
shutil.copyfileobj(profile_image.file, buffer)
|
shutil.copyfileobj(profile_image.file, buffer)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi_camelcase import CamelModel
|
from fastapi_camelcase import CamelModel
|
||||||
from mealie.core.config import DEFAULT_GROUP
|
from mealie.core.config import settings
|
||||||
from mealie.db.models.group import Group
|
from mealie.db.models.group import Group
|
||||||
from mealie.db.models.users import User
|
from mealie.db.models.users import User
|
||||||
from mealie.schema.category import CategoryBase
|
from mealie.schema.category import CategoryBase
|
||||||
|
@ -40,7 +40,7 @@ class UserBase(CamelModel):
|
||||||
schema_extra = {
|
schema_extra = {
|
||||||
"fullName": "Change Me",
|
"fullName": "Change Me",
|
||||||
"email": "changeme@email.com",
|
"email": "changeme@email.com",
|
||||||
"group": DEFAULT_GROUP,
|
"group": settings.DEFAULT_GROUP,
|
||||||
"admin": "false",
|
"admin": "false",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ from typing import Union
|
||||||
|
|
||||||
from fastapi.logger import logger
|
from fastapi.logger import logger
|
||||||
from jinja2 import Template
|
from jinja2 import Template
|
||||||
from mealie.core.config import BACKUP_DIR, IMG_DIR, TEMP_DIR, TEMPLATE_DIR
|
from mealie.core.config import app_dirs
|
||||||
from mealie.db.database import db
|
from mealie.db.database import db
|
||||||
from mealie.db.db_setup import create_session
|
from mealie.db.db_setup import create_session
|
||||||
from pydantic.main import BaseModel
|
from pydantic.main import BaseModel
|
||||||
|
@ -28,12 +28,12 @@ class ExportDatabase:
|
||||||
else:
|
else:
|
||||||
export_tag = datetime.now().strftime("%Y-%b-%d")
|
export_tag = datetime.now().strftime("%Y-%b-%d")
|
||||||
|
|
||||||
self.main_dir = TEMP_DIR.joinpath(export_tag)
|
self.main_dir = app_dirs.TEMP_DIR.joinpath(export_tag)
|
||||||
self.img_dir = self.main_dir.joinpath("images")
|
self.img_dir = self.main_dir.joinpath("images")
|
||||||
self.templates_dir = self.main_dir.joinpath("templates")
|
self.templates_dir = self.main_dir.joinpath("templates")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.templates = [TEMPLATE_DIR.joinpath(x) for x in templates]
|
self.templates = [app_dirs.TEMPLATE_DIR.joinpath(x) for x in templates]
|
||||||
except:
|
except:
|
||||||
self.templates = False
|
self.templates = False
|
||||||
logger.info("No Jinja2 Templates Registered for Export")
|
logger.info("No Jinja2 Templates Registered for Export")
|
||||||
|
@ -65,7 +65,7 @@ class ExportDatabase:
|
||||||
f.write(content)
|
f.write(content)
|
||||||
|
|
||||||
def export_images(self):
|
def export_images(self):
|
||||||
for file in IMG_DIR.iterdir():
|
for file in app_dirs.IMG_DIR.iterdir():
|
||||||
shutil.copy(file, self.img_dir.joinpath(file.name))
|
shutil.copy(file, self.img_dir.joinpath(file.name))
|
||||||
|
|
||||||
def export_items(self, items: list[BaseModel], folder_name: str, export_list=True):
|
def export_items(self, items: list[BaseModel], folder_name: str, export_list=True):
|
||||||
|
@ -87,10 +87,10 @@ class ExportDatabase:
|
||||||
f.write(json_data)
|
f.write(json_data)
|
||||||
|
|
||||||
def finish_export(self):
|
def finish_export(self):
|
||||||
zip_path = BACKUP_DIR.joinpath(f"{self.main_dir.name}")
|
zip_path = app_dirs.BACKUP_DIR.joinpath(f"{self.main_dir.name}")
|
||||||
shutil.make_archive(zip_path, "zip", self.main_dir)
|
shutil.make_archive(zip_path, "zip", self.main_dir)
|
||||||
|
|
||||||
shutil.rmtree(TEMP_DIR)
|
shutil.rmtree(app_dirs.TEMP_DIR)
|
||||||
|
|
||||||
return str(zip_path.absolute()) + ".zip"
|
return str(zip_path.absolute()) + ".zip"
|
||||||
|
|
||||||
|
@ -138,10 +138,10 @@ def backup_all(
|
||||||
|
|
||||||
|
|
||||||
def auto_backup_job():
|
def auto_backup_job():
|
||||||
for backup in BACKUP_DIR.glob("Auto*.zip"):
|
for backup in app_dirs.BACKUP_DIR.glob("Auto*.zip"):
|
||||||
backup.unlink()
|
backup.unlink()
|
||||||
|
|
||||||
templates = [template for template in TEMPLATE_DIR.iterdir()]
|
templates = [template for template in app_dirs.TEMPLATE_DIR.iterdir()]
|
||||||
session = create_session()
|
session = create_session()
|
||||||
backup_all(session=session, tag="Auto", templates=templates)
|
backup_all(session=session, tag="Auto", templates=templates)
|
||||||
logger.info("Auto Backup Called")
|
logger.info("Auto Backup Called")
|
||||||
|
|
|
@ -4,7 +4,7 @@ import zipfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Callable, List
|
from typing import Callable, List
|
||||||
|
|
||||||
from mealie.core.config import BACKUP_DIR, IMG_DIR, TEMP_DIR
|
from mealie.core.config import app_dirs
|
||||||
from mealie.db.database import db
|
from mealie.db.database import db
|
||||||
from mealie.schema.recipe import Recipe
|
from mealie.schema.recipe import Recipe
|
||||||
from mealie.schema.restore import CustomPageImport, GroupImport, RecipeImport, SettingsImport, ThemeImport, UserImport
|
from mealie.schema.restore import CustomPageImport, GroupImport, RecipeImport, SettingsImport, ThemeImport, UserImport
|
||||||
|
@ -33,11 +33,11 @@ class ImportDatabase:
|
||||||
Exception: If the zip file does not exists an exception raise.
|
Exception: If the zip file does not exists an exception raise.
|
||||||
"""
|
"""
|
||||||
self.session = session
|
self.session = session
|
||||||
self.archive = BACKUP_DIR.joinpath(zip_archive)
|
self.archive = app_dirs.BACKUP_DIR.joinpath(zip_archive)
|
||||||
self.force_imports = force_import
|
self.force_imports = force_import
|
||||||
|
|
||||||
if self.archive.is_file():
|
if self.archive.is_file():
|
||||||
self.import_dir = TEMP_DIR.joinpath("active_import")
|
self.import_dir = app_dirs.TEMP_DIR.joinpath("active_import")
|
||||||
self.import_dir.mkdir(parents=True, exist_ok=True)
|
self.import_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
with zipfile.ZipFile(self.archive, "r") as zip_ref:
|
with zipfile.ZipFile(self.archive, "r") as zip_ref:
|
||||||
|
@ -108,7 +108,7 @@ class ImportDatabase:
|
||||||
image_dir = self.import_dir.joinpath("images")
|
image_dir = self.import_dir.joinpath("images")
|
||||||
for image in image_dir.iterdir():
|
for image in image_dir.iterdir():
|
||||||
if image.stem in successful_imports:
|
if image.stem in successful_imports:
|
||||||
shutil.copy(image, IMG_DIR)
|
shutil.copy(image, app_dirs.IMG_DIR)
|
||||||
|
|
||||||
def import_themes(self):
|
def import_themes(self):
|
||||||
themes_file = self.import_dir.joinpath("themes", "themes.json")
|
themes_file = self.import_dir.joinpath("themes", "themes.json")
|
||||||
|
@ -275,7 +275,7 @@ class ImportDatabase:
|
||||||
return import_status
|
return import_status
|
||||||
|
|
||||||
def clean_up(self):
|
def clean_up(self):
|
||||||
shutil.rmtree(TEMP_DIR)
|
shutil.rmtree(app_dirs.TEMP_DIR)
|
||||||
|
|
||||||
|
|
||||||
def import_database(
|
def import_database(
|
||||||
|
|
|
@ -2,23 +2,23 @@ import shutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from mealie.core.config import IMG_DIR
|
|
||||||
from fastapi.logger import logger
|
from fastapi.logger import logger
|
||||||
|
from mealie.core.config import app_dirs
|
||||||
|
|
||||||
|
|
||||||
def read_image(recipe_slug: str) -> Path:
|
def read_image(recipe_slug: str) -> Path:
|
||||||
if IMG_DIR.joinpath(recipe_slug).is_file():
|
if app_dirs.IMG_DIR.joinpath(recipe_slug).is_file():
|
||||||
return IMG_DIR.joinpath(recipe_slug)
|
return app_dirs.IMG_DIR.joinpath(recipe_slug)
|
||||||
else:
|
|
||||||
recipe_slug = recipe_slug.split(".")[0]
|
recipe_slug = recipe_slug.split(".")[0]
|
||||||
for file in IMG_DIR.glob(f"{recipe_slug}*"):
|
for file in app_dirs.IMG_DIR.glob(f"{recipe_slug}*"):
|
||||||
return file
|
return file
|
||||||
|
|
||||||
|
|
||||||
def write_image(recipe_slug: str, file_data: bytes, extension: str) -> Path.name:
|
def write_image(recipe_slug: str, file_data: bytes, extension: str) -> Path.name:
|
||||||
delete_image(recipe_slug)
|
delete_image(recipe_slug)
|
||||||
|
|
||||||
image_path = Path(IMG_DIR.joinpath(f"{recipe_slug}.{extension}"))
|
image_path = Path(app_dirs.IMG_DIR.joinpath(f"{recipe_slug}.{extension}"))
|
||||||
with open(image_path, "ab") as f:
|
with open(image_path, "ab") as f:
|
||||||
f.write(file_data)
|
f.write(file_data)
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ def write_image(recipe_slug: str, file_data: bytes, extension: str) -> Path.name
|
||||||
|
|
||||||
def delete_image(recipe_slug: str) -> str:
|
def delete_image(recipe_slug: str) -> str:
|
||||||
recipe_slug = recipe_slug.split(".")[0]
|
recipe_slug = recipe_slug.split(".")[0]
|
||||||
for file in IMG_DIR.glob(f"{recipe_slug}*"):
|
for file in app_dirs.IMG_DIR.glob(f"{recipe_slug}*"):
|
||||||
return file.unlink()
|
return file.unlink()
|
||||||
|
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ def scrape_image(image_url: str, slug: str) -> Path:
|
||||||
image_url = image_url.get("url")
|
image_url = image_url.get("url")
|
||||||
|
|
||||||
filename = slug + "." + image_url.split(".")[-1]
|
filename = slug + "." + image_url.split(".")[-1]
|
||||||
filename = IMG_DIR.joinpath(filename)
|
filename = app_dirs.IMG_DIR.joinpath(filename)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r = requests.get(image_url, stream=True)
|
r = requests.get(image_url, stream=True)
|
||||||
|
|
|
@ -3,7 +3,7 @@ from pathlib import Path
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from fastapi.logger import logger
|
from fastapi.logger import logger
|
||||||
from mealie.core.config import IMG_DIR, TEMP_DIR
|
from mealie.core.config import app_dirs
|
||||||
from mealie.db.database import db
|
from mealie.db.database import db
|
||||||
from mealie.schema.recipe import Recipe
|
from mealie.schema.recipe import Recipe
|
||||||
from mealie.utils.unzip import unpack_zip
|
from mealie.utils.unzip import unpack_zip
|
||||||
|
@ -64,8 +64,8 @@ def chowdown_migrate(session: Session, zip_file: Path):
|
||||||
|
|
||||||
with temp_dir as dir:
|
with temp_dir as dir:
|
||||||
chow_dir = next(Path(dir).iterdir())
|
chow_dir = next(Path(dir).iterdir())
|
||||||
image_dir = TEMP_DIR.joinpath(chow_dir, "images")
|
image_dir = app_dirs.TEMP_DIR.joinpath(chow_dir, "images")
|
||||||
recipe_dir = TEMP_DIR.joinpath(chow_dir, "_recipes")
|
recipe_dir = app_dirs.TEMP_DIR.joinpath(chow_dir, "_recipes")
|
||||||
|
|
||||||
failed_recipes = []
|
failed_recipes = []
|
||||||
successful_recipes = []
|
successful_recipes = []
|
||||||
|
@ -83,7 +83,7 @@ def chowdown_migrate(session: Session, zip_file: Path):
|
||||||
for image in image_dir.iterdir():
|
for image in image_dir.iterdir():
|
||||||
try:
|
try:
|
||||||
if image.stem not in failed_recipes:
|
if image.stem not in failed_recipes:
|
||||||
shutil.copy(image, IMG_DIR.joinpath(image.name))
|
shutil.copy(image, app_dirs.IMG_DIR.joinpath(image.name))
|
||||||
except Exception as inst:
|
except Exception as inst:
|
||||||
logger.error(inst)
|
logger.error(inst)
|
||||||
failed_images.append(image.name)
|
failed_images.append(image.name)
|
||||||
|
|
|
@ -4,10 +4,10 @@ import shutil
|
||||||
import zipfile
|
import zipfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from mealie.core.config import IMG_DIR, MIGRATION_DIR, TEMP_DIR
|
from mealie.core.config import app_dirs
|
||||||
|
from mealie.db.database import db
|
||||||
from mealie.schema.recipe import Recipe
|
from mealie.schema.recipe import Recipe
|
||||||
from mealie.services.scraper.cleaner import Cleaner
|
from mealie.services.scraper.cleaner import Cleaner
|
||||||
from mealie.db.database import db
|
|
||||||
|
|
||||||
|
|
||||||
def process_selection(selection: Path) -> Path:
|
def process_selection(selection: Path) -> Path:
|
||||||
|
@ -15,7 +15,7 @@ def process_selection(selection: Path) -> Path:
|
||||||
return selection
|
return selection
|
||||||
elif selection.suffix == ".zip":
|
elif selection.suffix == ".zip":
|
||||||
with zipfile.ZipFile(selection, "r") as zip_ref:
|
with zipfile.ZipFile(selection, "r") as zip_ref:
|
||||||
nextcloud_dir = TEMP_DIR.joinpath("nextcloud")
|
nextcloud_dir = app_dirs.TEMP_DIR.joinpath("nextcloud")
|
||||||
nextcloud_dir.mkdir(exist_ok=False, parents=True)
|
nextcloud_dir.mkdir(exist_ok=False, parents=True)
|
||||||
zip_ref.extractall(nextcloud_dir)
|
zip_ref.extractall(nextcloud_dir)
|
||||||
return nextcloud_dir
|
return nextcloud_dir
|
||||||
|
@ -46,27 +46,27 @@ def import_recipes(recipe_dir: Path) -> Recipe:
|
||||||
recipe = Recipe(**recipe_data)
|
recipe = Recipe(**recipe_data)
|
||||||
|
|
||||||
if image:
|
if image:
|
||||||
shutil.copy(image, IMG_DIR.joinpath(image_name))
|
shutil.copy(image, app_dirs.IMG_DIR.joinpath(image_name))
|
||||||
|
|
||||||
return recipe
|
return recipe
|
||||||
|
|
||||||
|
|
||||||
def prep():
|
def prep():
|
||||||
try:
|
try:
|
||||||
shutil.rmtree(TEMP_DIR)
|
shutil.rmtree(app_dirs.TEMP_DIR)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
TEMP_DIR.mkdir(exist_ok=True, parents=True)
|
app_dirs.TEMP_DIR.mkdir(exist_ok=True, parents=True)
|
||||||
|
|
||||||
|
|
||||||
def cleanup():
|
def cleanup():
|
||||||
shutil.rmtree(TEMP_DIR)
|
shutil.rmtree(app_dirs.TEMP_DIR)
|
||||||
|
|
||||||
|
|
||||||
def migrate(session, selection: str):
|
def migrate(session, selection: str):
|
||||||
prep()
|
prep()
|
||||||
MIGRATION_DIR.mkdir(exist_ok=True)
|
app_dirs.MIGRATION_DIR.mkdir(exist_ok=True)
|
||||||
selection = MIGRATION_DIR.joinpath(selection)
|
selection = app_dirs.MIGRATION_DIR.joinpath(selection)
|
||||||
|
|
||||||
nextcloud_dir = process_selection(selection)
|
nextcloud_dir = process_selection(selection)
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
|
||||||
import extruct
|
import extruct
|
||||||
from mealie.core.config import DEBUG_DIR
|
from mealie.core.config import app_dirs
|
||||||
from slugify import slugify
|
from slugify import slugify
|
||||||
from w3lib.html import get_base_url
|
from w3lib.html import get_base_url
|
||||||
|
|
||||||
LAST_JSON = DEBUG_DIR.joinpath("last_recipe.json")
|
LAST_JSON = app_dirs.DEBUG_DIR.joinpath("last_recipe.json")
|
||||||
|
|
||||||
|
|
||||||
def og_field(properties: dict, field_name: str) -> str:
|
def og_field(properties: dict, field_name: str) -> str:
|
||||||
|
|
|
@ -3,14 +3,14 @@ from typing import List
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import scrape_schema_recipe
|
import scrape_schema_recipe
|
||||||
from mealie.core.config import DEBUG_DIR
|
from mealie.core.config import app_dirs
|
||||||
from fastapi.logger import logger
|
from fastapi.logger import logger
|
||||||
from mealie.services.image_services import scrape_image
|
from mealie.services.image_services import scrape_image
|
||||||
from mealie.schema.recipe import Recipe
|
from mealie.schema.recipe import Recipe
|
||||||
from mealie.services.scraper import open_graph
|
from mealie.services.scraper import open_graph
|
||||||
from mealie.services.scraper.cleaner import Cleaner
|
from mealie.services.scraper.cleaner import Cleaner
|
||||||
|
|
||||||
LAST_JSON = DEBUG_DIR.joinpath("last_recipe.json")
|
LAST_JSON = app_dirs.DEBUG_DIR.joinpath("last_recipe.json")
|
||||||
|
|
||||||
|
|
||||||
def create_from_url(url: str) -> Recipe:
|
def create_from_url(url: str) -> Recipe:
|
||||||
|
|
|
@ -2,12 +2,12 @@ import tempfile
|
||||||
import zipfile
|
import zipfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from mealie.core.config import TEMP_DIR
|
from mealie.core.config import app_dirs
|
||||||
|
|
||||||
|
|
||||||
def unpack_zip(selection: Path) -> tempfile.TemporaryDirectory:
|
def unpack_zip(selection: Path) -> tempfile.TemporaryDirectory:
|
||||||
TEMP_DIR.mkdir(parents=True, exist_ok=True)
|
app_dirs.TEMP_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
temp_dir = tempfile.TemporaryDirectory(dir=TEMP_DIR)
|
temp_dir = tempfile.TemporaryDirectory(dir=app_dirs.TEMP_DIR)
|
||||||
temp_dir_path = Path(temp_dir.name)
|
temp_dir_path = Path(temp_dir.name)
|
||||||
if selection.suffix == ".zip":
|
if selection.suffix == ".zip":
|
||||||
with zipfile.ZipFile(selection, "r") as zip_ref:
|
with zipfile.ZipFile(selection, "r") as zip_ref:
|
||||||
|
|
2
poetry.lock
generated
2
poetry.lock
generated
|
@ -1154,7 +1154,7 @@ python-versions = "*"
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "1.1"
|
||||||
python-versions = "^3.9"
|
python-versions = "^3.9"
|
||||||
content-hash = "a95c51999f18bd8ac8e0d08e22d54ef47d00c91ba06e8fbacec2969d466d6cc1"
|
content-hash = "a6c10e179bc15efc30627c9793218bb944f43dce5e624a7bcabcc47545e661e8"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
aiofiles = [
|
aiofiles = [
|
||||||
|
|
|
@ -38,6 +38,7 @@ pytest = "^6.2.1"
|
||||||
pytest-cov = "^2.11.0"
|
pytest-cov = "^2.11.0"
|
||||||
mkdocs-material = "^7.0.2"
|
mkdocs-material = "^7.0.2"
|
||||||
flake8 = "^3.9.0"
|
flake8 = "^3.9.0"
|
||||||
|
coverage = "^5.5"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core>=1.0.0"]
|
requires = ["poetry-core>=1.0.0"]
|
||||||
|
|
8
template.env
Normal file
8
template.env
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
DEFAULT_GROUP=Home
|
||||||
|
ENV=False
|
||||||
|
API_PORT=9000
|
||||||
|
API_DOCS=True
|
||||||
|
DB_TYPE='sqlite'
|
||||||
|
DEFAULT_PASSWORD=MyPassword
|
||||||
|
SFTP_USERNAME=None
|
||||||
|
SFTP_PASSWORD=None
|
|
@ -3,7 +3,7 @@ import json
|
||||||
import requests
|
import requests
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
from mealie.app import app
|
from mealie.app import app
|
||||||
from mealie.core.config import DEFAULT_PASSWORD, SQLITE_DIR
|
from mealie.core.config import app_dirs, settings
|
||||||
from mealie.db.db_setup import generate_session, sql_global_init
|
from mealie.db.db_setup import generate_session, sql_global_init
|
||||||
from mealie.db.init_db import init_db
|
from mealie.db.init_db import init_db
|
||||||
from pytest import fixture
|
from pytest import fixture
|
||||||
|
@ -12,7 +12,7 @@ from tests.app_routes import AppRoutes
|
||||||
from tests.test_config import TEST_DATA
|
from tests.test_config import TEST_DATA
|
||||||
from tests.utils.recipe_data import build_recipe_store, get_raw_no_image, get_raw_recipe
|
from tests.utils.recipe_data import build_recipe_store, get_raw_no_image, get_raw_recipe
|
||||||
|
|
||||||
SQLITE_FILE = SQLITE_DIR.joinpath("test.db")
|
SQLITE_FILE = app_dirs.SQLITE_DIR.joinpath("test.db")
|
||||||
SQLITE_FILE.unlink(missing_ok=True)
|
SQLITE_FILE.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
@ -50,7 +50,7 @@ def test_image():
|
||||||
|
|
||||||
@fixture(scope="session")
|
@fixture(scope="session")
|
||||||
def token(api_client: requests, api_routes: AppRoutes):
|
def token(api_client: requests, api_routes: AppRoutes):
|
||||||
form_data = {"username": "changeme@email.com", "password": DEFAULT_PASSWORD}
|
form_data = {"username": "changeme@email.com", "password": settings.DEFAULT_PASSWORD}
|
||||||
response = api_client.post(api_routes.auth_token, form_data)
|
response = api_client.post(api_routes.auth_token, form_data)
|
||||||
|
|
||||||
token = json.loads(response.text).get("access_token")
|
token = json.loads(response.text).get("access_token")
|
||||||
|
|
|
@ -4,7 +4,7 @@ from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
from mealie.core.config import MIGRATION_DIR
|
from mealie.core.config import app_dirs
|
||||||
from tests.app_routes import AppRoutes
|
from tests.app_routes import AppRoutes
|
||||||
from tests.test_config import TEST_CHOWDOWN_DIR, TEST_NEXTCLOUD_DIR
|
from tests.test_config import TEST_CHOWDOWN_DIR, TEST_NEXTCLOUD_DIR
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ def test_upload_chowdown_zip(api_client: TestClient, api_routes: AppRoutes, chow
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
assert MIGRATION_DIR.joinpath("chowdown", chowdown_zip.name).is_file()
|
assert app_dirs.MIGRATION_DIR.joinpath("chowdown", chowdown_zip.name).is_file()
|
||||||
|
|
||||||
|
|
||||||
def test_import_chowdown_directory(api_client: TestClient, api_routes: AppRoutes, chowdown_zip: Path, token):
|
def test_import_chowdown_directory(api_client: TestClient, api_routes: AppRoutes, chowdown_zip: Path, token):
|
||||||
|
@ -58,7 +58,7 @@ def test_delete_chowdown_migration_data(api_client: TestClient, api_routes: AppR
|
||||||
response = api_client.delete(delete_url, headers=token)
|
response = api_client.delete(delete_url, headers=token)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert not MIGRATION_DIR.joinpath(chowdown_zip.name).is_file()
|
assert not app_dirs.MIGRATION_DIR.joinpath(chowdown_zip.name).is_file()
|
||||||
|
|
||||||
|
|
||||||
# Nextcloud
|
# Nextcloud
|
||||||
|
@ -81,7 +81,7 @@ def test_upload_nextcloud_zip(api_client: TestClient, api_routes: AppRoutes, nex
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
assert MIGRATION_DIR.joinpath("nextcloud", nextcloud_zip.name).is_file()
|
assert app_dirs.MIGRATION_DIR.joinpath("nextcloud", nextcloud_zip.name).is_file()
|
||||||
|
|
||||||
|
|
||||||
def test_import_nextcloud_directory(api_client: TestClient, api_routes: AppRoutes, nextcloud_zip, token):
|
def test_import_nextcloud_directory(api_client: TestClient, api_routes: AppRoutes, nextcloud_zip, token):
|
||||||
|
@ -106,4 +106,4 @@ def test_delete__nextcloud_migration_data(api_client: TestClient, api_routes: Ap
|
||||||
response = api_client.delete(delete_url, headers=token)
|
response = api_client.delete(delete_url, headers=token)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert not MIGRATION_DIR.joinpath(nextcloud_zip.name).is_file()
|
assert not app_dirs.MIGRATION_DIR.joinpath(nextcloud_zip.name).is_file()
|
||||||
|
|
|
@ -1,4 +1,46 @@
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
|
import pytest
|
||||||
from mealie.services.scraper.cleaner import Cleaner
|
from mealie.services.scraper.cleaner import Cleaner
|
||||||
|
from mealie.services.scraper.scraper import extract_recipe_from_html
|
||||||
|
from tests.test_config import TEST_RAW_HTML, TEST_RAW_RECIPES
|
||||||
|
|
||||||
|
# https://github.com/django/django/blob/stable/1.3.x/django/core/validators.py#L45
|
||||||
|
url_validation_regex = re.compile(
|
||||||
|
r"^(?:http|ftp)s?://" # http:// or https://
|
||||||
|
r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|" # domain...
|
||||||
|
r"localhost|" # localhost...
|
||||||
|
r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})" # ...or ip
|
||||||
|
r"(?::\d+)?" # optional port
|
||||||
|
r"(?:/?|[/?]\S+)$",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"json_file,num_steps",
|
||||||
|
[
|
||||||
|
("best-homemade-salsa-recipe.json", 2),
|
||||||
|
(
|
||||||
|
"blue-cheese-stuffed-turkey-meatballs-with-raspberry-balsamic-glaze-2.json",
|
||||||
|
3,
|
||||||
|
),
|
||||||
|
("bon_appetit.json", 8),
|
||||||
|
("chunky-apple-cake.json", 4),
|
||||||
|
("dairy-free-impossible-pumpkin-pie.json", 7),
|
||||||
|
("how-to-make-instant-pot-spaghetti.json", 8),
|
||||||
|
("instant-pot-chicken-and-potatoes.json", 4),
|
||||||
|
("instant-pot-kerala-vegetable-stew.json", 13),
|
||||||
|
("jalapeno-popper-dip.json", 4),
|
||||||
|
("microwave_sweet_potatoes_04783.json", 4),
|
||||||
|
("moroccan-skirt-steak-with-roasted-pepper-couscous.json", 4),
|
||||||
|
("Pizza-Knoblauch-Champignon-Paprika-vegan.html.json", 3),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_cleaner_clean(json_file, num_steps):
|
||||||
|
recipe_data = Cleaner.clean(json.load(open(TEST_RAW_RECIPES.joinpath(json_file))))
|
||||||
|
assert len(recipe_data["recipeInstructions"]) == num_steps
|
||||||
|
|
||||||
|
|
||||||
def test_clean_category():
|
def test_clean_category():
|
||||||
|
@ -7,3 +49,45 @@ def test_clean_category():
|
||||||
|
|
||||||
def test_clean_html():
|
def test_clean_html():
|
||||||
assert Cleaner.html("<div>Hello World</div>") == "Hello World"
|
assert Cleaner.html("<div>Hello World</div>") == "Hello World"
|
||||||
|
|
||||||
|
|
||||||
|
def test_clean_image():
|
||||||
|
assert Cleaner.image(None) == "no image"
|
||||||
|
assert Cleaner.image("https://my.image/path/") == "https://my.image/path/"
|
||||||
|
assert Cleaner.image({"url": "My URL!"}) == "My URL!"
|
||||||
|
assert Cleaner.image(["My URL!", "MY SECOND URL"]) == "My URL!"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"instructions",
|
||||||
|
[
|
||||||
|
"A\n\nB\n\nC\n\n",
|
||||||
|
"A\nB\nC\n",
|
||||||
|
"A\r\n\r\nB\r\n\r\nC\r\n\r\n",
|
||||||
|
"A\r\nB\r\nC\r\n",
|
||||||
|
["A", "B", "C"],
|
||||||
|
[{"@type": "HowToStep", "text": x} for x in ["A", "B", "C"]],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_cleaner_instructions(instructions):
|
||||||
|
assert Cleaner.instructions(instructions) == [
|
||||||
|
{"text": "A"},
|
||||||
|
{"text": "B"},
|
||||||
|
{"text": "C"},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_html_with_recipe_data():
|
||||||
|
path = TEST_RAW_HTML.joinpath("healthy_pasta_bake_60759.html")
|
||||||
|
url = "https://www.bbc.co.uk/food/recipes/healthy_pasta_bake_60759"
|
||||||
|
recipe_data = extract_recipe_from_html(open(path).read(), url)
|
||||||
|
|
||||||
|
assert len(recipe_data["name"]) > 10
|
||||||
|
assert len(recipe_data["slug"]) > 10
|
||||||
|
assert recipe_data["orgURL"] == url
|
||||||
|
assert len(recipe_data["description"]) > 100
|
||||||
|
assert url_validation_regex.match(recipe_data["image"])
|
||||||
|
assert len(recipe_data["recipeIngredient"]) == 13
|
||||||
|
assert len(recipe_data["recipeInstructions"]) == 4
|
||||||
|
|
6
tests/unit_tests/test_config.py
Normal file
6
tests/unit_tests/test_config.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from mealie.core.config import determine_secrets
|
||||||
|
|
||||||
|
def test_determine_secret(monkeypatch):
|
||||||
|
secret = determine_secrets()
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from mealie.core.config import TEMP_DIR
|
from mealie.core.config import app_dirs
|
||||||
from mealie.schema.recipe import Recipe
|
from mealie.schema.recipe import Recipe
|
||||||
from mealie.services.image_services import IMG_DIR
|
|
||||||
from mealie.services.migrations.nextcloud import cleanup, import_recipes, prep, process_selection
|
from mealie.services.migrations.nextcloud import cleanup, import_recipes, prep, process_selection
|
||||||
from tests.test_config import TEST_NEXTCLOUD_DIR
|
from tests.test_config import TEST_NEXTCLOUD_DIR
|
||||||
|
|
||||||
CWD = Path(__file__).parent
|
CWD = Path(__file__).parent
|
||||||
TEST_NEXTCLOUD_DIR
|
TEST_NEXTCLOUD_DIR
|
||||||
TEMP_NEXTCLOUD = TEMP_DIR.joinpath("nextcloud")
|
TEMP_NEXTCLOUD = app_dirs.TEMP_DIR.joinpath("nextcloud")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
@ -37,4 +36,4 @@ def test_zip_extraction(file_name: str, final_path: Path):
|
||||||
def test_nextcloud_migration(recipe_dir: Path):
|
def test_nextcloud_migration(recipe_dir: Path):
|
||||||
recipe = import_recipes(recipe_dir)
|
recipe = import_recipes(recipe_dir)
|
||||||
assert isinstance(recipe, Recipe)
|
assert isinstance(recipe, Recipe)
|
||||||
IMG_DIR.joinpath(recipe.image).unlink(missing_ok=True)
|
app_dirs.IMG_DIR.joinpath(recipe.image).unlink(missing_ok=True)
|
||||||
|
|
|
@ -1,76 +0,0 @@
|
||||||
import json
|
|
||||||
import re
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from mealie.services.scraper.cleaner import Cleaner
|
|
||||||
from mealie.services.scraper.scraper import extract_recipe_from_html
|
|
||||||
from tests.test_config import TEST_RAW_HTML, TEST_RAW_RECIPES
|
|
||||||
|
|
||||||
# https://github.com/django/django/blob/stable/1.3.x/django/core/validators.py#L45
|
|
||||||
url_validation_regex = re.compile(
|
|
||||||
r"^(?:http|ftp)s?://" # http:// or https://
|
|
||||||
r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|" # domain...
|
|
||||||
r"localhost|" # localhost...
|
|
||||||
r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})" # ...or ip
|
|
||||||
r"(?::\d+)?" # optional port
|
|
||||||
r"(?:/?|[/?]\S+)$",
|
|
||||||
re.IGNORECASE,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"json_file,num_steps",
|
|
||||||
[
|
|
||||||
("best-homemade-salsa-recipe.json", 2),
|
|
||||||
(
|
|
||||||
"blue-cheese-stuffed-turkey-meatballs-with-raspberry-balsamic-glaze-2.json",
|
|
||||||
3,
|
|
||||||
),
|
|
||||||
("bon_appetit.json", 8),
|
|
||||||
("chunky-apple-cake.json", 4),
|
|
||||||
("dairy-free-impossible-pumpkin-pie.json", 7),
|
|
||||||
("how-to-make-instant-pot-spaghetti.json", 8),
|
|
||||||
("instant-pot-chicken-and-potatoes.json", 4),
|
|
||||||
("instant-pot-kerala-vegetable-stew.json", 13),
|
|
||||||
("jalapeno-popper-dip.json", 4),
|
|
||||||
("microwave_sweet_potatoes_04783.json", 4),
|
|
||||||
("moroccan-skirt-steak-with-roasted-pepper-couscous.json", 4),
|
|
||||||
("Pizza-Knoblauch-Champignon-Paprika-vegan.html.json", 3),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_normalize_data(json_file, num_steps):
|
|
||||||
recipe_data = Cleaner.clean(json.load(open(TEST_RAW_RECIPES.joinpath(json_file))))
|
|
||||||
assert len(recipe_data["recipeInstructions"]) == num_steps
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"instructions",
|
|
||||||
[
|
|
||||||
"A\n\nB\n\nC\n\n",
|
|
||||||
"A\nB\nC\n",
|
|
||||||
"A\r\n\r\nB\r\n\r\nC\r\n\r\n",
|
|
||||||
"A\r\nB\r\nC\r\n",
|
|
||||||
["A", "B", "C"],
|
|
||||||
[{"@type": "HowToStep", "text": x} for x in ["A", "B", "C"]],
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_normalize_instructions(instructions):
|
|
||||||
assert Cleaner.instructions(instructions) == [
|
|
||||||
{"text": "A"},
|
|
||||||
{"text": "B"},
|
|
||||||
{"text": "C"},
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def test_html_with_recipe_data():
|
|
||||||
path = TEST_RAW_HTML.joinpath("healthy_pasta_bake_60759.html")
|
|
||||||
url = "https://www.bbc.co.uk/food/recipes/healthy_pasta_bake_60759"
|
|
||||||
recipe_data = extract_recipe_from_html(open(path).read(), url)
|
|
||||||
|
|
||||||
assert len(recipe_data["name"]) > 10
|
|
||||||
assert len(recipe_data["slug"]) > 10
|
|
||||||
assert recipe_data["orgURL"] == url
|
|
||||||
assert len(recipe_data["description"]) > 100
|
|
||||||
assert url_validation_regex.match(recipe_data["image"])
|
|
||||||
assert len(recipe_data["recipeIngredient"]) == 13
|
|
||||||
assert len(recipe_data["recipeInstructions"]) == 4
|
|
Loading…
Add table
Add a link
Reference in a new issue