From dbf0f23bdeb8419d9c332c08dc3cc5c63117a03e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Alloza=20Gonz=C3=A1lez?= Date: Sun, 13 Mar 2022 16:38:18 +0100 Subject: [PATCH] feat: treat variable meals as empty in /shift (#37) * chore: add test script * feat: improve meal shifting * fix: ensure docs folder exists before creating index * feat: treat variable meals as empty in /shift * chore: update shift test script * chore: bump version to 1.8 * chore: apply pylint suggestions --- README.md | 22 +++++-------- app/core/config.py | 1 + app/core/meals.py | 32 ++++++++++++++++++ app/crud/crud_meal.py | 20 ++++++------ app/utils/misc.py | 9 +++++ pyproject.toml | 2 +- scripts/load-docs.py | 2 +- test-scripts/test-data.json | 65 +++++++++++++++++++++++++++++++++++++ test-scripts/test-shift.sh | 24 ++++++++++++++ 9 files changed, 151 insertions(+), 26 deletions(-) create mode 100644 test-scripts/test-data.json create mode 100755 test-scripts/test-shift.sh diff --git a/README.md b/README.md index a032dcc..efda368 100644 --- a/README.md +++ b/README.md @@ -18,11 +18,6 @@ Features: Deployment is done via docker. -Notes: - -- Remember you can use the tag you like. -- The Dockerfile is designed to work on ARM systems, like a Raspberry Pi. - ```shell # Build and push it to dockerhub docker buildx build -t sralloza/meal-planner:$VERSION --platform=linux/arm/v7,linux/amd64 --push . @@ -31,36 +26,36 @@ docker buildx build -t sralloza/meal-planner:$VERSION --platform=linux/arm/v7,li docker buildx build -t sralloza/meal-planner:$VERSION --platform=linux/arm/v7,linux/amd64 --load . ``` -## Environment +### Environment You need to supply the following environment variables (required ones are marked with 🚩). Settings are grouped in categories. -### Server +#### Server - 🚩 **API_TOKEN** (`str`): token of the API. In order to use the API, users will have to provide this token in their requests via the `X-TOKEN` header. - **ENABLE_PROMETHEUS** (`bool`): if `True`, the API will enable the prometheus endpoint `/metrics`. Defaults to `False`. - **PRODUCTION** (`bool`): if `True` the server will run on production environment. Defaults to `False`. - **DISABLE_CRON_INTEGRATION** (`bool`) if `True`, the server will not launch cron jobs. It is useful to launch replicas, enabling cron integration in only one of them. It is also useful to deploy on Kubernetes, as the cron jobs can be implemented via `CronJob`. -### AWS +#### AWS - 🚩 **AWS_ACCESS_KEY_ID** (`str`): AWS access key id. - 🚩 **AWS_SECRET_ACCESS_KEY** (`str`): AWS secret access key. - 🚩 **S3_BUCKET_NAME** (`str`): name of the S3 bucket to save the backups. - **S3_FILE_NAME** (`str`): filename to save the backups in the AWS S3 Bucket. Defaults to `meals.json`. -### Notion +#### Notion - **NOTION_ADD_DAY_AFTER_TOMORROW** (`bool`): if `True`, the meals of the day after tomorrow will also be added to Notion. Defaults to `True`. - 🚩 **NOTION_BLOCK_ID** (`uuid`): id of the notion block where the meals will be showed. - 🚩 **NOTION_KEY** (`str`): notion key to use the notion API. -### Todoist +#### Todoist - 🚩 **TODOIST_PROJECT_ID** (`int`): todoist project id where the tasks will be added. - 🚩 **TODOIST_TOKEN** (`str`): todoist token to use the todoist API. -### Database +#### Database - 🚩 **MYSQL_DATABASE** (`str`): database name. - 🚩 **MYSQL_HOST** (`str`): mysql host. @@ -69,13 +64,12 @@ You need to supply the following environment variables (required ones are marked - 🚩 **MYSQL_USER** (`str`): mysql user. - **WAIT_FOR_IT_ADDRESS** (`str`): if is set, it will wait for the database to be ready for max 120 seconds. Must be set to `$MYSQL_HOST:$MYSQL_PORT`. This switch should not be used in Kubernetes deployments, as `initContainers` are designed to cover this exact use case. -### Other +#### Other - **LOCALE_WEEKDAY_NAMES** (`list(str)`): weekday names, starting with Monday and ending with Sunday. Must contain 7 elements (one for each week day). - **NULL_STR** (`str`): string to represent an empty `lunch1` or `dinner`. Defaults to `N/A`. +- **VARIABLE_STR** (`str`): string to represent a variable meal. Defaults to `Variable`. It's used in the `/shift` endpoint, where all meals which are equal to `VARIABLE_STR` will be treated as empty meals. ## Future AWS and notion settings are currently needed. If you want to use this app without one of them (or both) add an issue and I'll make it optional and configurable. - -As said before, the docker image is designed to work on ARM systems. If you want AMD64 support please fill an issue. diff --git a/app/core/config.py b/app/core/config.py index 8110c03..60a414a 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -48,6 +48,7 @@ class Settings(BaseSettings): # Other LOCALE_WEEKDAY_NAMES: Optional[List[str]] NULL_STR = "N/A" + VARIABLE_STR = "Variable" @validator("DATABASE_URI", pre=True) def assemble_db_connection(cls, v: Optional[str], values: Dict[str, Any]) -> Any: diff --git a/app/core/meals.py b/app/core/meals.py index 601fb3f..6ac42ed 100644 --- a/app/core/meals.py +++ b/app/core/meals.py @@ -7,6 +7,19 @@ from fastapi import Query from pydantic import parse_obj_as +from ..models.meal import Meal +from ..utils.misc import lowercase +from .config import settings + +NULL_MAP = { + "lunch1": settings.NULL_STR, + "lunch1_frozen": False, + "lunch2": None, + "lunch2_frozen": False, + "dinner": settings.NULL_STR, + "dinner_frozen": False, +} + class SwapMode(Enum): """Valid swap modes.""" @@ -53,3 +66,22 @@ def swap_attrs(obj1: Any, obj2: Any, attrname: str): def set_attrs(old: Any, new: Any, attrnames: List[str]): for attr in attrnames: setattr(new, attr, getattr(old, attr)) + + +def can_override_meal_from_shift(meal: Meal, attrnames: List[str]) -> bool: + """Checks if the meal has all the attrs to null. + + A meal can be shifted (override some attrs from other meal) if all + attrnames are considered null. + """ + + var_str = lowercase(settings.VARIABLE_STR) + + for attrname in attrnames: + attr = lowercase(getattr(meal, attrname)) + null_str = lowercase(NULL_MAP[attrname]) + null_str = null_str.lower() if isinstance(null_str, str) else null_str + + if attr not in (null_str, var_str): + return False + return True diff --git a/app/crud/crud_meal.py b/app/crud/crud_meal.py index 7a4bafa..af66ec0 100644 --- a/app/crud/crud_meal.py +++ b/app/crud/crud_meal.py @@ -7,21 +7,18 @@ from sqlalchemy.orm.session import Session from ..core.config import settings -from ..core.meals import SwapMode, set_attrs, swap_attrs +from ..core.meals import ( + NULL_MAP, + SwapMode, + can_override_meal_from_shift, + set_attrs, + swap_attrs, +) from ..crud.base import CRUDBase from ..models import Meal from ..schemas.meal import MealCreate, MealUpdate from ..utils.misc import get_current_week -NULL_MAP = { - "lunch1": settings.NULL_STR, - "lunch1_frozen": False, - "lunch2": None, - "lunch2_frozen": False, - "dinner": settings.NULL_STR, - "dinner_frozen": False, -} - class CRUDMeal(CRUDBase[Meal, MealCreate, MealUpdate]): """Meal CRUD operations.""" @@ -177,6 +174,9 @@ def get_days_to_shift( next_day_meal = self.create(db, obj_in=new_meal) return [first_meal, next_day_meal] + if can_override_meal_from_shift(next_day_meal, attrnames): + return [first_meal, next_day_meal] + return self.get_days_to_shift(db, next_day_meal, attrnames) + [first_meal] def remove_week(self, db: Session, *, week: int): diff --git a/app/utils/misc.py b/app/utils/misc.py index 3ff689a..0c23240 100644 --- a/app/utils/misc.py +++ b/app/utils/misc.py @@ -1,8 +1,11 @@ from datetime import datetime from pathlib import Path +from typing import TypeVar from toml import loads +T = TypeVar("T") + def get_version(): data = loads(Path(__file__).parent.parent.with_name("pyproject.toml").read_text()) @@ -11,3 +14,9 @@ def get_version(): def get_current_week(): return datetime.now().isocalendar()[1] + + +def lowercase(obj: T) -> T: + if isinstance(obj, str): + return obj.lower() + return obj diff --git a/pyproject.toml b/pyproject.toml index 5b97678..0f78df7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "meal-planner" -version = "1.7.0" +version = "1.8.0" description = "" authors = ["Diego Alloza González "] packages = [ diff --git a/scripts/load-docs.py b/scripts/load-docs.py index 786464e..9dfba7a 100644 --- a/scripts/load-docs.py +++ b/scripts/load-docs.py @@ -46,6 +46,7 @@ def load_docs(source: str, extra_week: bool): meals = filter_meals(meals) + MD_DIR.mkdir(exist_ok=True) for week, weekly_meals in groupby(meals, lambda x: x.id.isocalendar()[1]): weeks.append(week) weekly_meals = list(weekly_meals) @@ -96,7 +97,6 @@ def get_default_md_text(): def create_md(week: int, weekly_meals: List[Meal], override=True): """Creates the markdown file.""" - MD_DIR.mkdir(exist_ok=True) md_filepath = MD_DIR / f"{week}.md" content = "" weekly_meals.sort(key=lambda x: x.id) diff --git a/test-scripts/test-data.json b/test-scripts/test-data.json new file mode 100644 index 0000000..0fe6984 --- /dev/null +++ b/test-scripts/test-data.json @@ -0,0 +1,65 @@ +[ + { + "date": "2022-03-14", + "lunch1": "LUNES", + "lunch2": "LUNES", + "dinner": "LUNES", + "lunch1_frozen": false, + "lunch2_frozen": false, + "dinner_frozen": false + }, + { + "date": "2022-03-15", + "lunch1": "MARTES", + "lunch2": "MARTES", + "dinner": "MARTES", + "lunch1_frozen": false, + "lunch2_frozen": false, + "dinner_frozen": false + }, + { + "date": "2022-03-16", + "lunch1": "MIÉRCOLES", + "lunch2": "MIÉRCOLES", + "dinner": "MIÉRCOLES", + "lunch1_frozen": false, + "lunch2_frozen": false, + "dinner_frozen": false + }, + { + "date": "2022-03-17", + "lunch1": "JUEVES", + "lunch2": "JUEVES", + "dinner": "JUEVES", + "lunch1_frozen": false, + "lunch2_frozen": false, + "dinner_frozen": false + }, + { + "date": "2022-03-18", + "lunch1": "VIERNES", + "lunch2": "VIERNES", + "dinner": "VIERNES", + "lunch1_frozen": false, + "lunch2_frozen": false, + "dinner_frozen": false + }, + { + "date": "2022-03-19", + "lunch1": "SÁBADO", + "lunch2": "SÁBADO", + "dinner": "SÁBADO", + "lunch1_frozen": false, + "lunch2_frozen": false, + "dinner_frozen": false + }, + { + "date": "2022-03-20", + "lunch1": "DOMINGO", + "lunch2": "DOMINGO", + "dinner": "DOMINGO", + "lunch1_frozen": false, + "lunch2_frozen": false, + "dinner_frozen": false + } +] diff --git a/test-scripts/test-shift.sh b/test-scripts/test-shift.sh new file mode 100755 index 0000000..e95e9d1 --- /dev/null +++ b/test-scripts/test-shift.sh @@ -0,0 +1,24 @@ +#!/bin/bash +set -e +source /home/sralloza/Documents/meal-planner/.venv/bin/activate + +: ${MEAL_PLANNER_API_TOKEN:?must set \$MEAL_PLANNER_API_TOKEN} + +DATE="2022-03-14" +SHIFT_MODE="all" + +# wait-for-it.sh 127.0.0.1:8000 --timeout 60 + +echo -e "\nClearing database" +http delete :8000/meals/week/10 x-token:$MEAL_PLANNER_API_TOKEN -ph | head -1 +http delete :8000/meals/week/11 x-token:$MEAL_PLANNER_API_TOKEN -ph | head -1 +http delete :8000/meals/week/12 x-token:$MEAL_PLANNER_API_TOKEN -ph | head -1 + +echo -e "\nPutting dummy data in database" +http post :8000/meals/bulk x-token:$MEAL_PLANNER_API_TOKEN < test-scripts/test-data.json > /dev/null +http :8000/meals x-token:$MEAL_PLANNER_API_TOKEN -pb | jtbl + +echo -e "\nExecuting shift ($DATE, $SHIFT_MODE)" +http put :8000/meals/shift/$DATE x-token:$MEAL_PLANNER_API_TOKEN mode==$SHIFT_MODE -pb | jtbl +echo "" +http :8000/meals x-token:$MEAL_PLANNER_API_TOKEN -pb | jtbl