refactor(backend): ♻️ cleanup HTTP service classes and remove database singleton (#687)

* refactor(backend): ♻️ cleanup duplicate code in http services

* refactor(backend): ♻️ refactor database away from singleton design

removed the database single and instead injected the session into a new Database class that is created during each request life-cycle. Now sessions no longer need to be passed into each method on the database

All tests pass, but there are likely some hidden breaking changes that were not discovered.

* fix venv

* disable venv cache

* fix install script

* bump poetry version

* postgres fixes

* revert install

* fix db initialization for postgres

* add postgres to docker

* refactor(backend): ♻️ cleanup unused and duplicate code in http services

* refactor(backend): remove sessions from arguments

* refactor(backend): ♻️ convert units and ingredients to use http service class

* test(backend):  add unit and food tests

* lint

* update tags

* re-enable cache

* fix missing fraction in db

* fix lint

Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
Hayden 2021-09-19 15:31:34 -08:00 committed by GitHub
parent c0e3f04c23
commit 476aefeeb0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
68 changed files with 1131 additions and 1084 deletions

View file

@ -37,7 +37,7 @@ jobs:
# ----- install & configure poetry -----
#----------------------------------------------
- name: Install Poetry
uses: snok/install-poetry@v1.1.1
uses: snok/install-poetry@v1
with:
virtualenvs-create: true
virtualenvs-in-project: true
@ -57,7 +57,7 @@ jobs:
run: |
poetry install
poetry add "psycopg2-binary==2.8.6"
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
# if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
#----------------------------------------------
# run test suite
#----------------------------------------------

View file

@ -24,12 +24,19 @@ services:
ports:
- 9092:80
environment:
# DB_ENGINE: postgres # Optional: 'sqlite', 'postgres'
# POSTGRES_USER: mealie
# POSTGRES_PASSWORD: mealie
# POSTGRES_SERVER: postgres
# POSTGRES_PORT: 5432
# POSTGRES_DB: mealie
# WORKERS_PER_CORE: 0.5
DB_ENGINE: postgres # Optional: 'sqlite', 'postgres'
POSTGRES_USER: mealie
POSTGRES_PASSWORD: mealie
POSTGRES_SERVER: postgres
POSTGRES_PORT: 5432
POSTGRES_DB: mealie
WORKERS_PER_CORE: 0.5
MAX_WORKERS: 1
WEB_CONCURRENCY: 1
postgres:
container_name: postgres
image: postgres
restart: always
environment:
POSTGRES_PASSWORD: mealie
POSTGRES_USER: mealie

View file

@ -94,7 +94,7 @@
<script lang="ts">
import { computed, defineComponent, reactive, toRefs } from "@nuxtjs/composition-api";
import { isSameDay, addDays, subDays, parseISO, format } from "date-fns";
import { SortableEvent } from "sortablejs";
import { SortableEvent } from "sortablejs"; // eslint-disable-line
import draggable from "vuedraggable";
import { useMealplans } from "~/composables/use-group-mealplan";
import { useRecipes, allRecipes } from "~/composables/use-recipes";

View file

@ -6,7 +6,6 @@ from mealie.core.config import APP_VERSION, settings
from mealie.core.root_logger import get_logger
from mealie.routes import backup_routes, migration_routes, router, utility_routes
from mealie.routes.about import about_router
from mealie.routes.mealplans import meal_plan_router
from mealie.routes.media import media_router
from mealie.routes.site_settings import settings_router
from mealie.services.events import create_general_event
@ -36,7 +35,6 @@ def api_routers():
app.include_router(media_router)
app.include_router(about_router)
# Meal Routes
app.include_router(meal_plan_router)
# Settings Routes
app.include_router(settings_router)
# Backups/Imports Routes

View file

@ -7,7 +7,7 @@ from jose import JWTError, jwt
from sqlalchemy.orm.session import Session
from mealie.core.config import app_dirs, settings
from mealie.db.database import db
from mealie.db.database import get_database
from mealie.db.db_setup import generate_session
from mealie.schema.user import LongLiveTokenInDB, PrivateUser, TokenData
@ -69,7 +69,9 @@ async def get_current_user(token: str = Depends(oauth2_scheme), session=Depends(
except JWTError:
raise credentials_exception
user = db.users.get(session, token_data.username, "email", any_case=True)
db = get_database(session)
user = db.users.get(token_data.username, "email", any_case=True)
if user is None:
raise credentials_exception
return user
@ -82,8 +84,9 @@ async def get_admin_user(current_user=Depends(get_current_user)) -> PrivateUser:
def validate_long_live_token(session: Session, client_token: str, id: int) -> PrivateUser:
db = get_database(session)
tokens: list[LongLiveTokenInDB] = db.api_tokens.get(session, id, "parent_id", limit=9999)
tokens: list[LongLiveTokenInDB] = db.api_tokens.get(id, "parent_id", limit=9999)
for token in tokens:
token: LongLiveTokenInDB

View file

@ -5,7 +5,7 @@ from jose import jwt
from passlib.context import CryptContext
from mealie.core.config import settings
from mealie.db.database import db
from mealie.db.database import get_database
from mealie.schema.user import PrivateUser
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
@ -28,10 +28,12 @@ def create_file_token(file_path: Path) -> bool:
def authenticate_user(session, email: str, password: str) -> PrivateUser:
user: PrivateUser = db.users.get(session, email, "email", any_case=True)
db = get_database(session)
user: PrivateUser = db.users.get(email, "email", any_case=True)
if not user:
user = db.users.get(session, email, "username", any_case=True)
user = db.users.get(email, "username", any_case=True)
if not user:
return False

View file

@ -1 +1 @@
from .db_access import DatabaseAccessLayer
from .access_model_factory import Database

View file

@ -14,7 +14,7 @@ T = TypeVar("T")
D = TypeVar("D")
class BaseAccessModel(Generic[T, D]):
class AccessModel(Generic[T, D]):
"""A Generic BaseAccess Model method to perform common operations on the database
Args:
@ -22,7 +22,8 @@ class BaseAccessModel(Generic[T, D]):
Generic ([D]): Represents the SqlAlchemyModel Model
"""
def __init__(self, primary_key: Union[str, int], sql_model: D, schema: T) -> None:
def __init__(self, session: Session, primary_key: Union[str, int], sql_model: D, schema: T) -> None:
self.session = session
self.primary_key = primary_key
self.sql_model = sql_model
self.schema = schema
@ -37,9 +38,7 @@ class BaseAccessModel(Generic[T, D]):
for observer in self.observers:
observer()
def get_all(
self, session: Session, limit: int = None, order_by: str = None, start=0, override_schema=None
) -> list[T]:
def get_all(self, limit: int = None, order_by: str = None, start=0, override_schema=None) -> list[T]:
eff_schema = override_schema or self.schema
if order_by:
@ -47,27 +46,20 @@ class BaseAccessModel(Generic[T, D]):
return [
eff_schema.from_orm(x)
for x in session.query(self.sql_model).order_by(order_attr.desc()).offset(start).limit(limit).all()
for x in self.session.query(self.sql_model).order_by(order_attr.desc()).offset(start).limit(limit).all()
]
return [eff_schema.from_orm(x) for x in session.query(self.sql_model).offset(start).limit(limit).all()]
return [eff_schema.from_orm(x) for x in self.session.query(self.sql_model).offset(start).limit(limit).all()]
def multi_query(
self,
session: Session,
query_by: dict[str, str],
start=0,
limit: int = None,
override_schema=None,
) -> list[T]:
def multi_query(self, query_by: dict[str, str], start=0, limit: int = None, override_schema=None) -> list[T]:
eff_schema = override_schema or self.schema
return [
eff_schema.from_orm(x)
for x in session.query(self.sql_model).filter_by(**query_by).offset(start).limit(limit).all()
for x in self.session.query(self.sql_model).filter_by(**query_by).offset(start).limit(limit).all()
]
def get_all_limit_columns(self, session: Session, fields: list[str], limit: int = None) -> list[D]:
def get_all_limit_columns(self, fields: list[str], limit: int = None) -> list[D]:
"""Queries the database for the selected model. Restricts return responses to the
keys specified under "fields"
@ -79,9 +71,9 @@ class BaseAccessModel(Generic[T, D]):
Returns:
list[SqlAlchemyBase]: Returns a list of ORM objects
"""
return session.query(self.sql_model).options(load_only(*fields)).limit(limit).all()
return self.session.query(self.sql_model).options(load_only(*fields)).limit(limit).all()
def get_all_primary_keys(self, session: Session) -> list[str]:
def get_all_primary_keys(self) -> list[str]:
"""Queries the database of the selected model and returns a list
of all primary_key values
@ -91,11 +83,11 @@ class BaseAccessModel(Generic[T, D]):
Returns:
list[str]:
"""
results = session.query(self.sql_model).options(load_only(str(self.primary_key)))
results = self.session.query(self.sql_model).options(load_only(str(self.primary_key)))
results_as_dict = [x.dict() for x in results]
return [x.get(self.primary_key) for x in results_as_dict]
def _query_one(self, session: Session, match_value: str, match_key: str = None) -> D:
def _query_one(self, match_value: str, match_key: str = None) -> D:
"""
Query the sql database for one item an return the sql alchemy model
object. If no match key is provided the primary_key attribute will be used.
@ -103,16 +95,16 @@ class BaseAccessModel(Generic[T, D]):
if match_key is None:
match_key = self.primary_key
return session.query(self.sql_model).filter_by(**{match_key: match_value}).one()
return self.session.query(self.sql_model).filter_by(**{match_key: match_value}).one()
def get_one(self, session: Session, value: str | int, key: str = None, any_case=False, override_schema=None) -> T:
def get_one(self, value: str | int, key: str = None, any_case=False, override_schema=None) -> T:
key = key or self.primary_key
if any_case:
search_attr = getattr(self.sql_model, key)
result = session.query(self.sql_model).filter(func.lower(search_attr) == key.lower()).one_or_none()
result = self.session.query(self.sql_model).filter(func.lower(search_attr) == key.lower()).one_or_none()
else:
result = session.query(self.sql_model).filter_by(**{key: value}).one_or_none()
result = self.session.query(self.sql_model).filter_by(**{key: value}).one_or_none()
if not result:
return
@ -121,7 +113,7 @@ class BaseAccessModel(Generic[T, D]):
return eff_schema.from_orm(result)
def get(
self, session: Session, match_value: str, match_key: str = None, limit=1, any_case=False, override_schema=None
self, match_value: str, match_key: str = None, limit=1, any_case=False, override_schema=None
) -> T | list[T]:
"""Retrieves an entry from the database by matching a key/value pair. If no
key is provided the class objects primary key will be used to match against.
@ -141,10 +133,13 @@ class BaseAccessModel(Generic[T, D]):
if any_case:
search_attr = getattr(self.sql_model, match_key)
result = (
session.query(self.sql_model).filter(func.lower(search_attr) == match_value.lower()).limit(limit).all()
self.session.query(self.sql_model)
.filter(func.lower(search_attr) == match_value.lower())
.limit(limit)
.all()
)
else:
result = session.query(self.sql_model).filter_by(**{match_key: match_value}).limit(limit).all()
result = self.session.query(self.sql_model).filter_by(**{match_key: match_value}).limit(limit).all()
eff_schema = override_schema or self.schema
@ -156,7 +151,7 @@ class BaseAccessModel(Generic[T, D]):
return [eff_schema.from_orm(x) for x in result]
def create(self, session: Session, document: T) -> T:
def create(self, document: T) -> T:
"""Creates a new database entry for the given SQL Alchemy Model.
Args:
@ -167,17 +162,17 @@ class BaseAccessModel(Generic[T, D]):
dict: A dictionary representation of the database entry
"""
document = document if isinstance(document, dict) else document.dict()
new_document = self.sql_model(session=session, **document)
session.add(new_document)
session.commit()
session.refresh(new_document)
new_document = self.sql_model(session=self.session, **document)
self.session.add(new_document)
self.session.commit()
self.session.refresh(new_document)
if self.observers:
self.update_observers()
return self.schema.from_orm(new_document)
def update(self, session: Session, match_value: str, new_data: dict) -> T:
def update(self, match_value: str, new_data: dict) -> T:
"""Update a database entry.
Args:
session (Session): Database Session
@ -189,19 +184,19 @@ class BaseAccessModel(Generic[T, D]):
"""
new_data = new_data if isinstance(new_data, dict) else new_data.dict()
entry = self._query_one(session=session, match_value=match_value)
entry.update(session=session, **new_data)
entry = self._query_one(match_value=match_value)
entry.update(session=self.session, **new_data)
if self.observers:
self.update_observers()
session.commit()
self.session.commit()
return self.schema.from_orm(entry)
def patch(self, session: Session, match_value: str, new_data: dict) -> T:
def patch(self, match_value: str, new_data: dict) -> T:
new_data = new_data if isinstance(new_data, dict) else new_data.dict()
entry = self._query_one(session=session, match_value=match_value)
entry = self._query_one(match_value=match_value)
if not entry:
return
@ -209,43 +204,43 @@ class BaseAccessModel(Generic[T, D]):
entry_as_dict = self.schema.from_orm(entry).dict()
entry_as_dict.update(new_data)
return self.update(session, match_value, entry_as_dict)
return self.update(match_value, entry_as_dict)
def delete(self, session: Session, primary_key_value) -> D:
result = session.query(self.sql_model).filter_by(**{self.primary_key: primary_key_value}).one()
def delete(self, primary_key_value) -> D:
result = self.session.query(self.sql_model).filter_by(**{self.primary_key: primary_key_value}).one()
results_as_model = self.schema.from_orm(result)
session.delete(result)
session.commit()
self.session.delete(result)
self.session.commit()
if self.observers:
self.update_observers()
return results_as_model
def delete_all(self, session: Session) -> None:
session.query(self.sql_model).delete()
session.commit()
def delete_all(self) -> None:
self.session.query(self.sql_model).delete()
self.session.commit()
if self.observers:
self.update_observers()
def count_all(self, session: Session, match_key=None, match_value=None) -> int:
def count_all(self, match_key=None, match_value=None) -> int:
if None in [match_key, match_value]:
return session.query(self.sql_model).count()
return self.session.query(self.sql_model).count()
else:
return session.query(self.sql_model).filter_by(**{match_key: match_value}).count()
return self.session.query(self.sql_model).filter_by(**{match_key: match_value}).count()
def _count_attribute(
self, session: Session, attribute_name: str, attr_match: str = None, count=True, override_schema=None
self, attribute_name: str, attr_match: str = None, count=True, override_schema=None
) -> Union[int, T]:
eff_schema = override_schema or self.schema
# attr_filter = getattr(self.sql_model, attribute_name)
if count:
return session.query(self.sql_model).filter(attribute_name == attr_match).count() # noqa: 711
return self.session.query(self.sql_model).filter(attribute_name == attr_match).count() # noqa: 711
else:
return [
eff_schema.from_orm(x)
for x in session.query(self.sql_model).filter(attribute_name == attr_match).all() # noqa: 711
for x in self.session.query(self.sql_model).filter(attribute_name == attr_match).all() # noqa: 711
]

View file

@ -0,0 +1,145 @@
from functools import cached_property
from sqlalchemy.orm import Session
from mealie.db.models.event import Event, EventNotification
from mealie.db.models.group import Group, GroupMealPlan
from mealie.db.models.group.cookbook import CookBook
from mealie.db.models.group.invite_tokens import GroupInviteToken
from mealie.db.models.group.preferences import GroupPreferencesModel
from mealie.db.models.group.webhooks import GroupWebhooksModel
from mealie.db.models.recipe.category import Category
from mealie.db.models.recipe.comment import RecipeComment
from mealie.db.models.recipe.ingredient import IngredientFoodModel, IngredientUnitModel
from mealie.db.models.recipe.recipe import RecipeModel
from mealie.db.models.recipe.tag import Tag
from mealie.db.models.settings import SiteSettings
from mealie.db.models.sign_up import SignUp
from mealie.db.models.users import LongLiveToken, User
from mealie.schema.admin import SiteSettings as SiteSettingsSchema
from mealie.schema.cookbook.cookbook import ReadCookBook
from mealie.schema.events import Event as EventSchema
from mealie.schema.events import EventNotificationIn
from mealie.schema.group.group_preferences import ReadGroupPreferences
from mealie.schema.group.invite_token import ReadInviteToken
from mealie.schema.group.webhook import ReadWebhook
from mealie.schema.meal_plan.new_meal import ReadPlanEntry
from mealie.schema.recipe import CommentOut, Recipe, RecipeCategoryResponse, RecipeTagResponse
from mealie.schema.recipe.recipe_ingredient import IngredientFood, IngredientUnit
from mealie.schema.user import GroupInDB, LongLiveTokenInDB, PrivateUser, SignUpOut
from ._access_model import AccessModel
from .group_access_model import GroupDataAccessModel
from .meal_access_model import MealDataAccessModel
from .recipe_access_model import RecipeDataAccessModel
from .user_access_model import UserDataAccessModel
pk_id = "id"
pk_slug = "slug"
pk_token = "token"
class CategoryDataAccessModel(AccessModel):
def get_empty(self):
return self.session.query(Category).filter(~Category.recipes.any()).all()
class TagsDataAccessModel(AccessModel):
def get_empty(self):
return self.session.query(Tag).filter(~Tag.recipes.any()).all()
class Database:
def __init__(self, session: Session) -> None:
"""
`DatabaseAccessLayer` class is the data access layer for all database actions within
Mealie. Database uses composition from classes derived from AccessModel. These
can be substantiated from the AccessModel class or through inheritance when
additional methods are required.
"""
self.session = session
# ================================================================
# Recipe Items
@cached_property
def recipes(self) -> RecipeDataAccessModel:
return RecipeDataAccessModel(self.session, pk_slug, RecipeModel, Recipe)
@cached_property
def ingredient_foods(self) -> AccessModel:
return AccessModel(self.session, pk_id, IngredientFoodModel, IngredientFood)
@cached_property
def ingredient_units(self) -> AccessModel:
return AccessModel(self.session, pk_id, IngredientUnitModel, IngredientUnit)
@cached_property
def comments(self) -> AccessModel:
return AccessModel(self.session, pk_id, RecipeComment, CommentOut)
@cached_property
def categories(self) -> CategoryDataAccessModel:
return CategoryDataAccessModel(self.session, pk_id, Category, RecipeCategoryResponse)
@cached_property
def tags(self) -> TagsDataAccessModel:
return TagsDataAccessModel(self.session, pk_id, Tag, RecipeTagResponse)
# ================================================================
# Site Items
@cached_property
def settings(self) -> AccessModel:
return AccessModel(self.session, pk_id, SiteSettings, SiteSettingsSchema)
@cached_property
def sign_up(self) -> AccessModel:
return AccessModel(self.session, pk_id, SignUp, SignUpOut)
@cached_property
def event_notifications(self) -> AccessModel:
return AccessModel(self.session, pk_id, EventNotification, EventNotificationIn)
@cached_property
def events(self) -> AccessModel:
return AccessModel(self.session, pk_id, Event, EventSchema)
# ================================================================
# User Items
@cached_property
def users(self) -> UserDataAccessModel:
return UserDataAccessModel(self.session, pk_id, User, PrivateUser)
@cached_property
def api_tokens(self) -> AccessModel:
return AccessModel(self.session, pk_id, LongLiveToken, LongLiveTokenInDB)
# ================================================================
# Group Items
@cached_property
def groups(self) -> GroupDataAccessModel:
return GroupDataAccessModel(self.session, pk_id, Group, GroupInDB)
@cached_property
def group_invite_tokens(self) -> AccessModel:
return AccessModel(self.session, "token", GroupInviteToken, ReadInviteToken)
@cached_property
def group_preferences(self) -> AccessModel:
return AccessModel(self.session, "group_id", GroupPreferencesModel, ReadGroupPreferences)
@cached_property
def meals(self) -> MealDataAccessModel:
return MealDataAccessModel(self.session, pk_id, GroupMealPlan, ReadPlanEntry)
@cached_property
def cookbooks(self) -> AccessModel:
return AccessModel(self.session, pk_id, CookBook, ReadCookBook)
@cached_property
def webhooks(self) -> AccessModel:
return AccessModel(self.session, pk_id, GroupWebhooksModel, ReadWebhook)

View file

@ -1,98 +0,0 @@
from logging import getLogger
from sqlalchemy.orm.session import Session
from mealie.db.data_access_layer.group_access_model import GroupDataAccessModel
from mealie.db.data_access_layer.meal_access_model import MealDataAccessModel
from mealie.db.models.event import Event, EventNotification
from mealie.db.models.group import Group, GroupMealPlan
from mealie.db.models.group.cookbook import CookBook
from mealie.db.models.group.invite_tokens import GroupInviteToken
from mealie.db.models.group.preferences import GroupPreferencesModel
from mealie.db.models.group.shopping_list import ShoppingList
from mealie.db.models.group.webhooks import GroupWebhooksModel
from mealie.db.models.recipe.category import Category
from mealie.db.models.recipe.comment import RecipeComment
from mealie.db.models.recipe.ingredient import IngredientFoodModel, IngredientUnitModel
from mealie.db.models.recipe.recipe import RecipeModel, Tag
from mealie.db.models.settings import SiteSettings
from mealie.db.models.sign_up import SignUp
from mealie.db.models.users import LongLiveToken, User
from mealie.schema.admin import SiteSettings as SiteSettingsSchema
from mealie.schema.cookbook import ReadCookBook
from mealie.schema.events import Event as EventSchema
from mealie.schema.events import EventNotificationIn
from mealie.schema.group.group_preferences import ReadGroupPreferences
from mealie.schema.group.invite_token import ReadInviteToken
from mealie.schema.group.webhook import ReadWebhook
from mealie.schema.meal_plan import ShoppingListOut
from mealie.schema.meal_plan.new_meal import ReadPlanEntry
from mealie.schema.recipe import (
CommentOut,
IngredientFood,
IngredientUnit,
Recipe,
RecipeCategoryResponse,
RecipeTagResponse,
)
from mealie.schema.user import GroupInDB, LongLiveTokenInDB, PrivateUser, SignUpOut
from ._base_access_model import BaseAccessModel
from .recipe_access_model import RecipeDataAccessModel
from .user_access_model import UserDataAccessModel
logger = getLogger()
pk_id = "id"
pk_slug = "slug"
pk_token = "token"
class CategoryDataAccessModel(BaseAccessModel):
def get_empty(self, session: Session):
return session.query(Category).filter(~Category.recipes.any()).all()
class TagsDataAccessModel(BaseAccessModel):
def get_empty(self, session: Session):
return session.query(Tag).filter(~Tag.recipes.any()).all()
class DatabaseAccessLayer:
def __init__(self) -> None:
"""
`DatabaseAccessLayer` class is the data access layer for all database actions within
Mealie. Database uses composition from classes derived from BaseAccessModel. These
can be substantiated from the BaseAccessModel class or through inheritance when
additional methods are required.
"""
# Recipes
self.recipes = RecipeDataAccessModel(pk_slug, RecipeModel, Recipe)
self.ingredient_foods = BaseAccessModel(pk_id, IngredientFoodModel, IngredientFood)
self.ingredient_units = BaseAccessModel(pk_id, IngredientUnitModel, IngredientUnit)
self.comments = BaseAccessModel(pk_id, RecipeComment, CommentOut)
# Tags and Categories
self.categories = CategoryDataAccessModel(pk_slug, Category, RecipeCategoryResponse)
self.tags = TagsDataAccessModel(pk_slug, Tag, RecipeTagResponse)
# Site
self.settings = BaseAccessModel(pk_id, SiteSettings, SiteSettingsSchema)
self.sign_ups = BaseAccessModel(pk_token, SignUp, SignUpOut)
self.event_notifications = BaseAccessModel(pk_id, EventNotification, EventNotificationIn)
self.events = BaseAccessModel(pk_id, Event, EventSchema)
# Users
self.users = UserDataAccessModel(pk_id, User, PrivateUser)
self.api_tokens = BaseAccessModel(pk_id, LongLiveToken, LongLiveTokenInDB)
# Group Data
self.groups = GroupDataAccessModel(pk_id, Group, GroupInDB)
self.group_tokens = BaseAccessModel("token", GroupInviteToken, ReadInviteToken)
self.meals = MealDataAccessModel(pk_id, GroupMealPlan, ReadPlanEntry)
self.webhooks = BaseAccessModel(pk_id, GroupWebhooksModel, ReadWebhook)
self.shopping_lists = BaseAccessModel(pk_id, ShoppingList, ShoppingListOut)
self.cookbooks = BaseAccessModel(pk_id, CookBook, ReadCookBook)
self.group_preferences = BaseAccessModel("group_id", GroupPreferencesModel, ReadGroupPreferences)

View file

@ -4,10 +4,10 @@ from mealie.db.models.group import Group
from mealie.schema.meal_plan.meal import MealPlanOut
from mealie.schema.user.user import GroupInDB
from ._base_access_model import BaseAccessModel
from ._access_model import AccessModel
class GroupDataAccessModel(BaseAccessModel[GroupInDB, Group]):
class GroupDataAccessModel(AccessModel[GroupInDB, Group]):
def get_meals(self, session: Session, match_value: str, match_key: str = "name") -> list[MealPlanOut]:
"""A Helper function to get the group from the database and return a sorted list of

View file

@ -1,26 +1,24 @@
from datetime import date
from sqlalchemy.orm.session import Session
from mealie.db.models.group import GroupMealPlan
from mealie.schema.meal_plan.new_meal import ReadPlanEntry
from ._base_access_model import BaseAccessModel
from ._access_model import AccessModel
class MealDataAccessModel(BaseAccessModel[ReadPlanEntry, GroupMealPlan]):
def get_slice(self, session: Session, start: date, end: date, group_id: int) -> list[ReadPlanEntry]:
class MealDataAccessModel(AccessModel[ReadPlanEntry, GroupMealPlan]):
def get_slice(self, start: date, end: date, group_id: int) -> list[ReadPlanEntry]:
start = start.strftime("%Y-%m-%d")
end = end.strftime("%Y-%m-%d")
qry = session.query(GroupMealPlan).filter(
qry = self.session.query(GroupMealPlan).filter(
GroupMealPlan.date.between(start, end),
GroupMealPlan.group_id == group_id,
)
return [self.schema.from_orm(x) for x in qry.all()]
def get_today(self, session: Session, group_id: int) -> list[ReadPlanEntry]:
def get_today(self, group_id: int) -> list[ReadPlanEntry]:
today = date.today()
qry = session.query(GroupMealPlan).filter(GroupMealPlan.date == today, GroupMealPlan.group_id == group_id)
qry = self.session.query(GroupMealPlan).filter(GroupMealPlan.date == today, GroupMealPlan.group_id == group_id)
return [self.schema.from_orm(x) for x in qry.all()]

View file

@ -1,16 +1,14 @@
from random import randint
from sqlalchemy.orm.session import Session
from mealie.db.models.recipe.recipe import RecipeModel
from mealie.db.models.recipe.settings import RecipeSettings
from mealie.schema.recipe import Recipe
from ._base_access_model import BaseAccessModel
from ._access_model import AccessModel
class RecipeDataAccessModel(BaseAccessModel[Recipe, RecipeModel]):
def get_all_public(self, session: Session, limit: int = None, order_by: str = None, start=0, override_schema=None):
class RecipeDataAccessModel(AccessModel[Recipe, RecipeModel]):
def get_all_public(self, limit: int = None, order_by: str = None, start=0, override_schema=None):
eff_schema = override_schema or self.schema
if order_by:
@ -18,7 +16,7 @@ class RecipeDataAccessModel(BaseAccessModel[Recipe, RecipeModel]):
return [
eff_schema.from_orm(x)
for x in session.query(self.sql_model)
for x in self.session.query(self.sql_model)
.join(RecipeSettings)
.filter(RecipeSettings.public == True) # noqa: 711
.order_by(order_attr.desc())
@ -29,7 +27,7 @@ class RecipeDataAccessModel(BaseAccessModel[Recipe, RecipeModel]):
return [
eff_schema.from_orm(x)
for x in session.query(self.sql_model)
for x in self.session.query(self.sql_model)
.join(RecipeSettings)
.filter(RecipeSettings.public == True) # noqa: 711
.offset(start)
@ -37,23 +35,25 @@ class RecipeDataAccessModel(BaseAccessModel[Recipe, RecipeModel]):
.all()
]
def update_image(self, session: Session, slug: str, _: str = None) -> str:
entry: RecipeModel = self._query_one(session, match_value=slug)
def update_image(self, slug: str, _: str = None) -> str:
entry: RecipeModel = self._query_one(match_value=slug)
entry.image = randint(0, 255)
session.commit()
self.session.commit()
return entry.image
def count_uncategorized(self, session: Session, count=True, override_schema=None) -> int:
def count_uncategorized(self, count=True, override_schema=None) -> int:
return self._count_attribute(
session,
attribute_name=RecipeModel.recipe_category,
attr_match=None,
count=count,
override_schema=override_schema,
)
def count_untagged(self, session: Session, count=True, override_schema=None) -> int:
def count_untagged(self, count=True, override_schema=None) -> int:
return self._count_attribute(
session, attribute_name=RecipeModel.tags, attr_match=None, count=count, override_schema=override_schema
attribute_name=RecipeModel.tags,
attr_match=None,
count=count,
override_schema=override_schema,
)

View file

@ -1,12 +1,12 @@
from mealie.db.models.users import User
from mealie.schema.user.user import PrivateUser
from ._base_access_model import BaseAccessModel
from ._access_model import AccessModel
class UserDataAccessModel(BaseAccessModel[PrivateUser, User]):
class UserDataAccessModel(AccessModel[PrivateUser, User]):
def update_password(self, session, id, password: str):
entry = self._query_one(session=session, match_value=id)
entry = self._query_one(match_value=id)
entry.update_password(password)
session.commit()

View file

@ -1,9 +1,7 @@
import json
from pathlib import Path
from sqlalchemy.orm.session import Session
from ..data_access_layer import DatabaseAccessLayer
from mealie.db.data_access_layer.access_model_factory import Database
CWD = Path(__file__).parent
@ -20,15 +18,15 @@ def get_default_units():
return units
def default_recipe_unit_init(db: DatabaseAccessLayer, session: Session) -> None:
def default_recipe_unit_init(db: Database) -> None:
for unit in get_default_units():
try:
db.ingredient_units.create(session, unit)
db.ingredient_units.create(unit)
except Exception as e:
print(e)
for food in get_default_foods():
try:
db.ingredient_foods.create(session, food)
db.ingredient_foods.create(food)
except Exception as e:
print(e)

View file

@ -1,10 +1,7 @@
from functools import lru_cache
from sqlalchemy.orm import Session
from .data_access_layer import DatabaseAccessLayer
db = DatabaseAccessLayer()
from .data_access_layer.access_model_factory import Database
@lru_cache
def get_database():
return db
def get_database(session: Session):
return Database(session)

View file

@ -10,7 +10,7 @@ def sql_global_init(db_url: str):
if "sqlite" in db_url:
connect_args["check_same_thread"] = False
engine = sa.create_engine(db_url, echo=False, connect_args=connect_args)
engine = sa.create_engine(db_url, echo=False, connect_args=connect_args, pool_pre_ping=True)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

View file

@ -1,10 +1,9 @@
from sqlalchemy.orm import Session
from mealie.core import root_logger
from mealie.core.config import settings
from mealie.core.security import hash_password
from mealie.db.data_access_layer.access_model_factory import Database
from mealie.db.data_initialization.init_units_foods import default_recipe_unit_init
from mealie.db.database import db
from mealie.db.database import get_database
from mealie.db.db_setup import create_session, engine
from mealie.db.models._model_base import SqlAlchemyBase
from mealie.schema.admin import SiteSettings
@ -21,30 +20,24 @@ def create_all_models():
SqlAlchemyBase.metadata.create_all(engine)
def init_db(session: Session = None) -> None:
create_all_models()
if not session:
session = create_session()
with session:
default_group_init(session)
default_settings_init(session)
default_user_init(session)
default_recipe_unit_init(db, session)
def init_db(db: Database) -> None:
default_group_init(db)
default_settings_init(db)
default_user_init(db)
default_recipe_unit_init(db)
def default_settings_init(session: Session):
document = db.settings.create(session, SiteSettings().dict())
def default_settings_init(db: Database):
document = db.settings.create(SiteSettings().dict())
logger.info(f"Created Site Settings: \n {document}")
def default_group_init(session: Session):
def default_group_init(db: Database):
logger.info("Generating Default Group")
create_new_group(session, GroupBase(name=settings.DEFAULT_GROUP))
create_new_group(db, GroupBase(name=settings.DEFAULT_GROUP))
def default_user_init(session: Session):
def default_user_init(db: Database):
default_user = {
"full_name": "Change Me",
"username": "admin",
@ -55,21 +48,26 @@ def default_user_init(session: Session):
}
logger.info("Generating Default User")
db.users.create(session, default_user)
db.users.create(default_user)
def main():
create_all_models()
session = create_session()
db = get_database(session)
try:
session = create_session()
init_user = db.users.get(session, "1", "id")
init_user = db.users.get("1", "id")
except Exception:
init_db()
init_db(db)
return
if init_user:
logger.info("Database Exists")
else:
logger.info("Database Doesn't Exists, Initializing...")
init_db()
init_db(db)
create_general_event("Initialize Database", "Initialize database with default values", session)

View file

@ -1,4 +1,4 @@
from sqlalchemy import Column, ForeignKey, Integer, String, orm
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, orm
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
@ -11,6 +11,7 @@ class IngredientUnitModel(SqlAlchemyBase, BaseMixins):
name = Column(String)
description = Column(String)
abbreviation = Column(String)
fraction = Column(Boolean)
ingredients = orm.relationship("RecipeIngredient", back_populates="unit")
@auto_init()

View file

@ -2,7 +2,7 @@ from fastapi import Depends
from sqlalchemy.orm.session import Session
from mealie.core.root_logger import get_logger
from mealie.db.database import db
from mealie.db.database import get_database
from mealie.db.db_setup import generate_session
from mealie.routes.routers import AdminAPIRouter
from mealie.schema.events import EventsOut
@ -15,18 +15,20 @@ logger = get_logger()
@router.get("", response_model=EventsOut)
async def get_events(session: Session = Depends(generate_session)):
""" Get event from the Database """
# Get Item
return EventsOut(total=db.events.count_all(session), events=db.events.get_all(session, order_by="time_stamp"))
db = get_database(session)
return EventsOut(total=db.events.count_all(), events=db.events.get_all(order_by="time_stamp"))
@router.delete("")
async def delete_events(session: Session = Depends(generate_session)):
""" Get event from the Database """
# Get Item
db = get_database(session)
return db.events.delete_all(session)
@router.delete("/{id}")
async def delete_event(id: int, session: Session = Depends(generate_session)):
""" Delete event from the Database """
db = get_database(session)
return db.events.delete(session, id)

View file

@ -4,7 +4,7 @@ from fastapi import Depends, status
from sqlalchemy.orm.session import Session
from mealie.core.root_logger import get_logger
from mealie.db.database import db
from mealie.db.database import get_database
from mealie.db.db_setup import generate_session
from mealie.routes.routers import AdminAPIRouter
from mealie.schema.events import EventNotificationIn, EventNotificationOut, TestEvent
@ -21,8 +21,9 @@ async def create_event_notification(
session: Session = Depends(generate_session),
):
""" Create event_notification in the Database """
db = get_database(session)
return db.event_notifications.create(session, event_data)
return db.event_notifications.create(event_data)
@router.post("/notifications/test")
@ -31,9 +32,10 @@ async def test_notification_route(
session: Session = Depends(generate_session),
):
""" Create event_notification in the Database """
db = get_database(session)
if test_data.id:
event_obj: EventNotificationIn = db.event_notifications.get(session, test_data.id)
event_obj: EventNotificationIn = db.event_notifications.get(test_data.id)
test_data.test_url = event_obj.notification_url
try:
@ -46,8 +48,8 @@ async def test_notification_route(
@router.get("/notifications", response_model=list[EventNotificationOut])
async def get_all_event_notification(session: Session = Depends(generate_session)):
""" Get all event_notification from the Database """
# Get Item
return db.event_notifications.get_all(session, override_schema=EventNotificationOut)
db = get_database(session)
return db.event_notifications.get_all(override_schema=EventNotificationOut)
@router.put("/notifications/{id}")
@ -61,4 +63,5 @@ async def update_event_notification(id: int, session: Session = Depends(generate
async def delete_event_notification(id: int, session: Session = Depends(generate_session)):
""" Delete event_notification from the Database """
# Delete Item
return db.event_notifications.delete(session, id)
db = get_database(session)
return db.event_notifications.delete(id)

View file

@ -28,11 +28,11 @@ async def get_app_info():
@router.get("/statistics", response_model=AppStatistics)
async def get_app_statistics(session: Session = Depends(generate_session)):
db = get_database()
db = get_database(session)
return AppStatistics(
total_recipes=db.recipes.count_all(session),
uncategorized_recipes=db.recipes.count_uncategorized(session),
untagged_recipes=db.recipes.count_untagged(session),
total_users=db.users.count_all(session),
total_groups=db.groups.count_all(session),
total_recipes=db.recipes.count_all(),
uncategorized_recipes=db.recipes.count_uncategorized(),
untagged_recipes=db.recipes.count_untagged(),
total_users=db.users.count_all(),
total_groups=db.groups.count_all(),
)

View file

@ -2,7 +2,7 @@ from fastapi import BackgroundTasks, Depends, HTTPException, status
from sqlalchemy.orm.session import Session
from mealie.core.dependencies import get_current_user
from mealie.db.database import db
from mealie.db.database import get_database
from mealie.db.db_setup import generate_session
from mealie.routes.routers import AdminAPIRouter
from mealie.schema.user import GroupBase, GroupInDB, PrivateUser, UpdateGroup
@ -14,8 +14,9 @@ router = AdminAPIRouter(prefix="/groups")
@router.get("", response_model=list[GroupInDB])
async def get_all_groups(session: Session = Depends(generate_session)):
""" Returns a list of all groups in the database """
db = get_database(session)
return db.groups.get_all(session)
return db.groups.get_all()
@router.post("", status_code=status.HTTP_201_CREATED, response_model=GroupInDB)
@ -25,9 +26,10 @@ async def create_group(
session: Session = Depends(generate_session),
):
""" Creates a Group in the Database """
db = get_database(session)
try:
new_group = db.groups.create(session, group_data.dict())
new_group = db.groups.create(group_data.dict())
background_tasks.add_task(create_group_event, "Group Created", f"'{group_data.name}' created", session)
return new_group
except Exception:
@ -37,7 +39,8 @@ async def create_group(
@router.put("/{id}")
async def update_group_data(id: int, group_data: UpdateGroup, session: Session = Depends(generate_session)):
""" Updates a User Group """
db.groups.update(session, id, group_data.dict())
db = get_database(session)
db.groups.update(id, group_data.dict())
@router.delete("/{id}")
@ -48,11 +51,12 @@ async def delete_user_group(
session: Session = Depends(generate_session),
):
""" Removes a user group from the database """
db = get_database(session)
if id == 1:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="DEFAULT_GROUP")
group: GroupInDB = db.groups.get(session, id)
group: GroupInDB = db.groups.get(id)
if not group:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="GROUP_NOT_FOUND")
@ -64,4 +68,4 @@ async def delete_user_group(
create_group_event, "Group Deleted", f"'{group.name}' deleted by {current_user.full_name}", session
)
db.groups.delete(session, id)
db.groups.delete(id)

View file

@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm.session import Session
from mealie.core.dependencies import is_logged_in
from mealie.db.database import db
from mealie.db.database import get_database
from mealie.db.db_setup import generate_session
from mealie.routes.routers import AdminAPIRouter, UserAPIRouter
from mealie.schema.recipe import CategoryIn, RecipeCategoryResponse
@ -15,13 +15,15 @@ admin_router = AdminAPIRouter()
@public_router.get("")
async def get_all_recipe_categories(session: Session = Depends(generate_session)):
""" Returns a list of available categories in the database """
return db.categories.get_all_limit_columns(session, ["slug", "name"])
db = get_database(session)
return db.categories.get_all_limit_columns(fields=["slug", "name"])
@public_router.get("/empty")
def get_empty_categories(session: Session = Depends(generate_session)):
""" Returns a list of categories that do not contain any recipes"""
return db.categories.get_empty(session)
db = get_database(session)
return db.categories.get_empty()
@public_router.get("/{category}", response_model=RecipeCategoryResponse)
@ -29,8 +31,9 @@ def get_all_recipes_by_category(
category: str, session: Session = Depends(generate_session), is_user: bool = Depends(is_logged_in)
):
""" Returns a list of recipes associated with the provided category. """
db = get_database(session)
category_obj = db.categories.get(session, category)
category_obj = db.categories.get(category)
category_obj = RecipeCategoryResponse.from_orm(category_obj)
if not is_user:
@ -42,9 +45,10 @@ def get_all_recipes_by_category(
@user_router.post("")
async def create_recipe_category(category: CategoryIn, session: Session = Depends(generate_session)):
""" Creates a Category in the database """
db = get_database(session)
try:
return db.categories.create(session, category.dict())
return db.categories.create(category.dict())
except Exception:
raise HTTPException(status.HTTP_400_BAD_REQUEST)
@ -52,9 +56,10 @@ async def create_recipe_category(category: CategoryIn, session: Session = Depend
@admin_router.put("/{category}", response_model=RecipeCategoryResponse)
async def update_recipe_category(category: str, new_category: CategoryIn, session: Session = Depends(generate_session)):
""" Updates an existing Tag in the database """
db = get_database(session)
try:
return db.categories.update(session, category, new_category.dict())
return db.categories.update(category, new_category.dict())
except Exception:
raise HTTPException(status.HTTP_400_BAD_REQUEST)
@ -66,8 +71,9 @@ async def delete_recipe_category(category: str, session: Session = Depends(gener
category does not impact a recipe. The category will be removed
from any recipes that contain it
"""
db = get_database(session)
try:
db.categories.delete(session, category)
db.categories.delete(category)
except Exception:
raise HTTPException(status.HTTP_400_BAD_REQUEST)

View file

@ -7,12 +7,10 @@ router = APIRouter()
@router.get("", response_model=list[ReadInviteToken])
def get_invite_tokens(g_service: GroupSelfService = Depends(GroupSelfService.write_existing)):
def get_invite_tokens(g_service: GroupSelfService = Depends(GroupSelfService.private)):
return g_service.get_invite_tokens()
@router.post("", response_model=ReadInviteToken, status_code=status.HTTP_201_CREATED)
def create_invite_token(
uses: CreateInviteToken, g_service: GroupSelfService = Depends(GroupSelfService.write_existing)
):
def create_invite_token(uses: CreateInviteToken, g_service: GroupSelfService = Depends(GroupSelfService.private)):
return g_service.create_invite_token(uses.uses)

View file

@ -1,10 +0,0 @@
from fastapi import APIRouter
from . import crud, helpers, mealplans
meal_plan_router = APIRouter()
meal_plan_router.include_router(crud.router)
meal_plan_router.include_router(crud.public_router)
meal_plan_router.include_router(helpers.router)
meal_plan_router.include_router(mealplans.router)

View file

@ -1,126 +0,0 @@
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, status
from sqlalchemy.orm.session import Session
from starlette.responses import FileResponse
from mealie.core.dependencies import get_current_user
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.routers import UserAPIRouter
from mealie.schema.meal_plan import MealPlanIn, MealPlanOut
from mealie.schema.user import GroupInDB, PrivateUser
from mealie.services.events import create_group_event
from mealie.services.image import image
from mealie.services.meal_services import get_todays_meal, set_mealplan_dates
router = UserAPIRouter(prefix="/api/meal-plans", tags=["Meal Plan"])
public_router = APIRouter(prefix="/api/meal-plans", tags=["Meal Plan"])
@router.get("/all", response_model=list[MealPlanOut])
def get_all_meals(
current_user: PrivateUser = Depends(get_current_user),
session: Session = Depends(generate_session),
):
""" Returns a list of all available Meal Plan """
return db.groups.get_meals(session, current_user.group)
@router.get("/this-week", response_model=MealPlanOut)
def get_this_week(session: Session = Depends(generate_session), current_user: PrivateUser = Depends(get_current_user)):
""" Returns the meal plan data for this week """
plans = db.groups.get_meals(session, current_user.group)
if plans:
return plans[0]
@router.get("/today", tags=["Meal Plan"])
def get_today(session: Session = Depends(generate_session), current_user: PrivateUser = Depends(get_current_user)):
"""
Returns the recipe slug for the meal scheduled for today.
If no meal is scheduled nothing is returned
"""
group_in_db: GroupInDB = db.groups.get(session, current_user.group, "name")
recipe = get_todays_meal(session, group_in_db)
if recipe:
return recipe
@public_router.get("/today/image", tags=["Meal Plan"])
def get_todays_image(session: Session = Depends(generate_session), group_name: str = "Home"):
"""
Returns the image for todays meal-plan.
"""
group_in_db: GroupInDB = db.groups.get(session, group_name, "name")
recipe = get_todays_meal(session, group_in_db)
recipe_image = recipe.image_dir.joinpath(image.ImageOptions.ORIGINAL_IMAGE)
if not recipe and not recipe_image.exists():
raise HTTPException(status.HTTP_404_NOT_FOUND)
return FileResponse(recipe_image)
@router.get("/{id}", response_model=MealPlanOut)
def get_meal_plan(
id,
session: Session = Depends(generate_session),
):
""" Returns a single Meal Plan from the Database """
return db.meals.get(session, id)
@router.post("/create", status_code=status.HTTP_201_CREATED)
def create_meal_plan(
background_tasks: BackgroundTasks,
data: MealPlanIn,
session: Session = Depends(generate_session),
current_user: PrivateUser = Depends(get_current_user),
):
""" Creates a meal plan database entry """
set_mealplan_dates(data)
background_tasks.add_task(
create_group_event, "Meal Plan Created", f"Mealplan Created for '{current_user.group}'", session=session
)
return db.meals.create(session, data.dict())
@router.put("/{plan_id}")
def update_meal_plan(
background_tasks: BackgroundTasks,
plan_id: str,
meal_plan: MealPlanIn,
session: Session = Depends(generate_session),
current_user: PrivateUser = Depends(get_current_user),
):
""" Updates a meal plan based off ID """
set_mealplan_dates(meal_plan)
processed_plan = MealPlanOut(id=plan_id, **meal_plan.dict())
try:
db.meals.update(session, plan_id, processed_plan.dict())
background_tasks.add_task(
create_group_event, "Meal Plan Updated", f"Mealplan Updated for '{current_user.group}'", session=session
)
except Exception:
raise HTTPException(status.HTTP_400_BAD_REQUEST)
@router.delete("/{plan_id}")
def delete_meal_plan(
background_tasks: BackgroundTasks,
plan_id,
session: Session = Depends(generate_session),
current_user: PrivateUser = Depends(get_current_user),
):
""" Removes a meal plan from the database """
try:
db.meals.delete(session, plan_id)
background_tasks.add_task(
create_group_event, "Meal Plan Deleted", f"Mealplan Deleted for '{current_user.group}'", session=session
)
except Exception:
raise HTTPException(status.HTTP_400_BAD_REQUEST)

View file

@ -1,50 +0,0 @@
from fastapi import Depends
from sqlalchemy.orm.session import Session
from mealie.core.dependencies import get_current_user
from mealie.core.root_logger import get_logger
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.routers import UserAPIRouter
from mealie.schema.meal_plan import ListItem, MealPlanOut, ShoppingListIn, ShoppingListOut
from mealie.schema.recipe import Recipe
from mealie.schema.user import PrivateUser
logger = get_logger()
router = UserAPIRouter(prefix="/api/meal-plans", tags=["Meal Plan"])
@router.get("/{id}/shopping-list")
def get_shopping_list(
id: str,
session: Session = Depends(generate_session),
current_user: PrivateUser = Depends(get_current_user),
):
mealplan: MealPlanOut = db.meals.get(session, id)
all_ingredients = []
for plan_day in mealplan.plan_days:
for meal in plan_day.meals:
if not meal.slug:
continue
try:
recipe: Recipe = db.recipes.get(session, meal.slug)
all_ingredients += recipe.recipe_ingredient
except Exception:
logger.error("Recipe Not Found")
new_list = ShoppingListIn(
name="MealPlan Shopping List", group=current_user.group, items=[ListItem(text=t.note) for t in all_ingredients]
)
created_list: ShoppingListOut = db.shopping_lists.create(session, new_list)
mealplan.shopping_list = created_list.id
db.meals.update(session, mealplan.id, mealplan)
return created_list

View file

@ -1,8 +0,0 @@
from fastapi import APIRouter
from mealie.routes.mealplans import crud, helpers
router = APIRouter()
router.include_router(crud.router)
router.include_router(helpers.router)

View file

@ -1,7 +1,7 @@
from fastapi import APIRouter, Depends
from sqlalchemy.orm.session import Session
from mealie.db.database import db
from mealie.db.database import get_database
from mealie.db.db_setup import generate_session
from mealie.schema.recipe import RecipeSummary
@ -10,9 +10,11 @@ router = APIRouter()
@router.get("/summary/untagged", response_model=list[RecipeSummary])
async def get_untagged_recipes(count: bool = False, session: Session = Depends(generate_session)):
return db.recipes.count_untagged(session, count=count, override_schema=RecipeSummary)
db = get_database(session)
return db.recipes.count_untagged(count=count, override_schema=RecipeSummary)
@router.get("/summary/uncategorized", response_model=list[RecipeSummary])
async def get_uncategorized_recipes(count: bool = False, session: Session = Depends(generate_session)):
return db.recipes.count_uncategorized(session, count=count, override_schema=RecipeSummary)
db = get_database(session)
return db.recipes.count_uncategorized(count=count, override_schema=RecipeSummary)

View file

@ -4,7 +4,7 @@ from fastapi import Depends, status
from sqlalchemy.orm.session import Session
from mealie.core.dependencies import get_current_user
from mealie.db.database import db
from mealie.db.database import get_database
from mealie.db.db_setup import generate_session
from mealie.routes.routers import UserAPIRouter
from mealie.schema.recipe import CommentOut, CreateComment, SaveComment
@ -21,9 +21,10 @@ async def create_comment(
current_user: PrivateUser = Depends(get_current_user),
):
""" Create comment in the Database """
db = get_database(session)
new_comment = SaveComment(user=current_user.id, text=new_comment.text, recipe_slug=slug)
return db.comments.create(session, new_comment)
return db.comments.create(new_comment)
@router.put("/{slug}/comments/{id}")
@ -34,12 +35,13 @@ async def update_comment(
current_user: PrivateUser = Depends(get_current_user),
):
""" Update comment in the Database """
old_comment: CommentOut = db.comments.get(session, id)
db = get_database(session)
old_comment: CommentOut = db.comments.get(id)
if current_user.id != old_comment.user.id:
raise HTTPException(status.HTTP_403_FORBIDDEN)
return db.comments.update(session, id, new_comment)
return db.comments.update(id, new_comment)
@router.delete("/{slug}/comments/{id}")
@ -47,9 +49,10 @@ async def delete_comment(
id: int, session: Session = Depends(generate_session), current_user: PrivateUser = Depends(get_current_user)
):
""" Delete comment from the Database """
comment: CommentOut = db.comments.get(session, id)
db = get_database(session)
comment: CommentOut = db.comments.get(id)
if current_user.id == comment.user.id or current_user.admin:
db.comments.delete(session, id)
db.comments.delete(id)
return
raise HTTPException(status.HTTP_403_FORBIDDEN)

View file

@ -5,7 +5,7 @@ from fastapi.datastructures import UploadFile
from slugify import slugify
from sqlalchemy.orm.session import Session
from mealie.db.database import db
from mealie.db.database import get_database
from mealie.db.db_setup import generate_session
from mealie.routes.routers import UserAPIRouter
from mealie.schema.recipe import CreateRecipeByURL, Recipe, RecipeAsset
@ -32,8 +32,9 @@ def update_recipe_image(
session: Session = Depends(generate_session),
):
""" Removes an existing image and replaces it with the incoming file. """
db = get_database(session)
write_image(slug, image, extension)
new_version = db.recipes.update_image(session, slug, extension)
new_version = db.recipes.update_image(slug, extension)
return {"image": new_version}
@ -58,7 +59,9 @@ def upload_recipe_asset(
if not dest.is_file():
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)
recipe: Recipe = db.recipes.get(session, slug)
db = get_database(session)
recipe: Recipe = db.recipes.get(slug)
recipe.assets.append(asset_in)
db.recipes.update(session, slug, recipe.dict())
db.recipes.update(slug, recipe.dict())
return asset_in

View file

@ -10,7 +10,7 @@ from starlette.responses import FileResponse
from mealie.core.dependencies import temporary_zip_path
from mealie.core.root_logger import get_logger
from mealie.db.database import db
from mealie.db.database import get_database
from mealie.db.db_setup import generate_session
from mealie.routes.routers import UserAPIRouter
from mealie.schema.recipe import CreateRecipeByURL, Recipe, RecipeImageTypes
@ -71,7 +71,9 @@ async def create_recipe_from_zip(
with myzip.open(file) as myfile:
recipe_image = myfile.read()
recipe: Recipe = db.recipes.create(session, Recipe(**recipe_dict))
db = get_database(session)
recipe: Recipe = db.recipes.create(Recipe(**recipe_dict))
write_image(recipe.slug, recipe_image, "webp")
@ -89,7 +91,9 @@ async def get_recipe_as_zip(
slug: str, session: Session = Depends(generate_session), temp_path=Depends(temporary_zip_path)
):
""" Get a Recipe and It's Original Image as a Zip File """
recipe: Recipe = db.recipes.get(session, slug)
db = get_database(session)
recipe: Recipe = db.recipes.get(slug)
image_asset = recipe.image_dir.joinpath(RecipeImageTypes.original.value)
@ -105,14 +109,12 @@ async def get_recipe_as_zip(
@user_router.put("/{slug}")
def update_recipe(data: Recipe, recipe_service: RecipeService = Depends(RecipeService.write_existing)):
""" Updates a recipe by existing slug and data. """
return recipe_service.update_one(data)
@user_router.patch("/{slug}")
def patch_recipe(data: Recipe, recipe_service: RecipeService = Depends(RecipeService.write_existing)):
""" Updates a recipe by existing slug and data. """
return recipe_service.patch_one(data)

View file

@ -2,7 +2,7 @@ from fastapi import Depends
from sqlalchemy.orm.session import Session
from mealie.core.dependencies import get_current_user
from mealie.db.database import db
from mealie.db.database import get_database
from mealie.db.db_setup import generate_session
from mealie.routes.routers import UserAPIRouter
from mealie.schema.meal_plan import ShoppingListIn, ShoppingListOut
@ -18,25 +18,28 @@ async def create_shopping_list(
session: Session = Depends(generate_session),
):
""" Create Shopping List in the Database """
db = get_database(session)
list_in.group = current_user.group
return db.shopping_lists.create(session, list_in)
return db.shopping_lists.create(list_in)
@router.get("/{id}", response_model=ShoppingListOut)
async def get_shopping_list(id: int, session: Session = Depends(generate_session)):
""" Get Shopping List from the Database """
return db.shopping_lists.get(session, id)
db = get_database(session)
return db.shopping_lists.get(id)
@router.put("/{id}", response_model=ShoppingListOut)
async def update_shopping_list(id: int, new_data: ShoppingListIn, session: Session = Depends(generate_session)):
""" Update Shopping List in the Database """
return db.shopping_lists.update(session, id, new_data)
db = get_database(session)
return db.shopping_lists.update(id, new_data)
@router.delete("/{id}")
async def delete_shopping_list(id: int, session: Session = Depends(generate_session)):
""" Delete Shopping List from the Database """
return db.shopping_lists.delete(session, id)
db = get_database(session)
return db.shopping_lists.delete(id)

View file

@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm.session import Session
from mealie.core.dependencies import get_current_user
from mealie.db.database import db
from mealie.db.database import get_database
from mealie.db.db_setup import generate_session
from mealie.routes.routers import AdminAPIRouter
from mealie.schema.admin import SiteSettings
@ -16,8 +16,9 @@ admin_router = AdminAPIRouter(prefix="/api/site-settings", tags=["Settings"])
@public_router.get("")
def get_main_settings(session: Session = Depends(generate_session)):
""" Returns basic site settings """
db = get_database(session)
return db.settings.get(session, 1)
return db.settings.get(1)
@admin_router.put("")
@ -26,7 +27,8 @@ def update_settings(
session: Session = Depends(generate_session),
):
""" Returns Site Settings """
db.settings.update(session, 1, data.dict())
db = get_database(session)
db.settings.update(1, data.dict())
@admin_router.post("/webhooks/test")
@ -35,7 +37,8 @@ def test_webhooks(
session: Session = Depends(generate_session),
):
""" Run the function to test your webhooks """
group_entry: GroupInDB = db.groups.get(session, current_user.group, "name")
db = get_database(session)
group_entry: GroupInDB = db.groups.get(current_user.group, "name")
try:
post_webhooks(group_entry.id, session)

View file

@ -1,8 +1,8 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm.session import Session
from sqlalchemy.orm import Session
from mealie.core.dependencies import is_logged_in
from mealie.db.database import db
from mealie.db.database import get_database
from mealie.db.db_setup import generate_session
from mealie.routes.routers import AdminAPIRouter, UserAPIRouter
from mealie.schema.recipe import RecipeTagResponse, TagIn
@ -15,13 +15,15 @@ admin_router = AdminAPIRouter()
@public_router.get("")
async def get_all_recipe_tags(session: Session = Depends(generate_session)):
""" Returns a list of available tags in the database """
return db.tags.get_all_limit_columns(session, ["slug", "name"])
db = get_database(session)
return db.tags.get_all_limit_columns(["slug", "name"])
@public_router.get("/empty")
def get_empty_tags(session: Session = Depends(generate_session)):
""" Returns a list of tags that do not contain any recipes"""
return db.tags.get_empty(session)
db = get_database(session)
return db.tags.get_empty()
@public_router.get("/{tag}", response_model=RecipeTagResponse)
@ -29,7 +31,8 @@ def get_all_recipes_by_tag(
tag: str, session: Session = Depends(generate_session), is_user: bool = Depends(is_logged_in)
):
""" Returns a list of recipes associated with the provided tag. """
tag_obj = db.tags.get(session, tag)
db = get_database(session)
tag_obj = db.tags.get(tag)
tag_obj = RecipeTagResponse.from_orm(tag_obj)
if not is_user:
@ -41,15 +44,15 @@ def get_all_recipes_by_tag(
@user_router.post("")
async def create_recipe_tag(tag: TagIn, session: Session = Depends(generate_session)):
""" Creates a Tag in the database """
return db.tags.create(session, tag.dict())
db = get_database(session)
return db.tags.create(tag.dict())
@admin_router.put("/{tag}", response_model=RecipeTagResponse)
async def update_recipe_tag(tag: str, new_tag: TagIn, session: Session = Depends(generate_session)):
""" Updates an existing Tag in the database """
return db.tags.update(session, tag, new_tag.dict())
db = get_database(session)
return db.tags.update(tag, new_tag.dict())
@admin_router.delete("/{tag}")
@ -59,6 +62,7 @@ async def delete_recipe_tag(tag: str, session: Session = Depends(generate_sessio
from any recipes that contain it"""
try:
db.tags.delete(session, tag)
db = get_database(session)
db.tags.delete(tag)
except Exception:
raise HTTPException(status.HTTP_400_BAD_REQUEST)

View file

@ -1,8 +1,10 @@
from fastapi import APIRouter
from . import food_routes, unit_routes
from mealie.services._base_http_service.router_factory import RouterFactory
from mealie.services.recipe.recipe_food_service import RecipeFoodService
from mealie.services.recipe.recipe_unit_service import RecipeUnitService
router = APIRouter()
router.include_router(food_routes.router, prefix="/foods", tags=["Recipes: Foods"])
router.include_router(unit_routes.router, prefix="/units", tags=["Recipes: Units"])
router.include_router(RouterFactory(RecipeFoodService, prefix="/foods", tags=["Recipes: Foods"]))
router.include_router(RouterFactory(RecipeUnitService, prefix="/units", tags=["Recipes: Units"]))

View file

@ -1,44 +0,0 @@
from fastapi import Depends, status
from mealie.db.database import db
from mealie.db.db_setup import Session, generate_session
from mealie.routes.routers import UserAPIRouter
from mealie.schema.recipe import CreateIngredientFood, IngredientFood
router = UserAPIRouter()
@router.get("", response_model=list[IngredientFood])
async def get_all(
session: Session = Depends(generate_session),
):
""" Get unit from the Database """
# Get unit
return db.ingredient_foods.get_all(session)
@router.post("", response_model=IngredientFood, status_code=status.HTTP_201_CREATED)
async def create_food(unit: CreateIngredientFood, session: Session = Depends(generate_session)):
""" Create unit in the Database """
return db.ingredient_foods.create(session, unit)
@router.get("/{id}")
async def get_food(id: str, session: Session = Depends(generate_session)):
""" Get unit from the Database """
return db.ingredient_foods.get(session, id)
@router.put("/{id}")
async def update_food(id: str, unit: CreateIngredientFood, session: Session = Depends(generate_session)):
""" Update unit in the Database """
return db.ingredient_foods.update(session, id, unit)
@router.delete("/{id}")
async def delete_food(id: str, session: Session = Depends(generate_session)):
""" Delete unit from the Database """
return db.ingredient_foods.delete(session, id)

View file

@ -1,44 +0,0 @@
from fastapi import Depends, status
from mealie.db.database import db
from mealie.db.db_setup import Session, generate_session
from mealie.routes.routers import UserAPIRouter
from mealie.schema.recipe import CreateIngredientUnit, IngredientUnit
router = UserAPIRouter()
@router.get("", response_model=list[IngredientUnit])
async def get_all(
session: Session = Depends(generate_session),
):
""" Get unit from the Database """
# Get unit
return db.ingredient_units.get_all(session)
@router.post("", response_model=IngredientUnit, status_code=status.HTTP_201_CREATED)
async def create_unit(unit: CreateIngredientUnit, session: Session = Depends(generate_session)):
""" Create unit in the Database """
return db.ingredient_units.create(session, unit)
@router.get("/{id}")
async def get_unit(id: str, session: Session = Depends(generate_session)):
""" Get unit from the Database """
return db.ingredient_units.get(session, id)
@router.put("/{id}")
async def update_unit(id: str, unit: CreateIngredientUnit, session: Session = Depends(generate_session)):
""" Update unit in the Database """
return db.ingredient_units.update(session, id, unit)
@router.delete("/{id}")
async def delete_unit(id: str, session: Session = Depends(generate_session)):
""" Delete unit from the Database """
return db.ingredient_units.delete(session, id)

View file

@ -6,7 +6,7 @@ from sqlalchemy.orm.session import Session
from mealie.core.dependencies import get_current_user
from mealie.core.security import create_access_token
from mealie.db.database import db
from mealie.db.database import get_database
from mealie.db.db_setup import generate_session
from mealie.routes.routers import UserAPIRouter
from mealie.schema.user import CreateToken, LoingLiveTokenIn, LongLiveTokenInDB, PrivateUser
@ -33,7 +33,9 @@ async def create_api_token(
parent_id=current_user.id,
)
new_token_in_db = db.api_tokens.create(session, token_model)
db = get_database(session)
new_token_in_db = db.api_tokens.create(token_model)
if new_token_in_db:
return {"token": token}
@ -46,13 +48,14 @@ async def delete_api_token(
session: Session = Depends(generate_session),
):
""" Delete api_token from the Database """
token: LongLiveTokenInDB = db.api_tokens.get(session, token_id)
db = get_database(session)
token: LongLiveTokenInDB = db.api_tokens.get(token_id)
if not token:
raise HTTPException(status.HTTP_404_NOT_FOUND, f"Could not locate token with id '{token_id}' in database")
if token.user.email == current_user.email:
deleted_token = db.api_tokens.delete(session, token_id)
deleted_token = db.api_tokens.delete(token_id)
return {"token_delete": deleted_token.name}
else:
raise HTTPException(status.HTTP_403_FORBIDDEN)

View file

@ -4,7 +4,7 @@ from sqlalchemy.orm.session import Session
from mealie.core import security
from mealie.core.dependencies import get_current_user
from mealie.core.security import hash_password
from mealie.db.database import db
from mealie.db.database import get_database
from mealie.db.db_setup import generate_session
from mealie.routes.routers import AdminAPIRouter, UserAPIRouter
from mealie.routes.users._helpers import assert_user_change_allowed
@ -17,7 +17,8 @@ admin_router = AdminAPIRouter(prefix="")
@admin_router.get("", response_model=list[UserOut])
async def get_all_users(session: Session = Depends(generate_session)):
return db.users.get_all(session)
db = get_database(session)
return db.users.get_all()
@admin_router.post("", response_model=UserOut, status_code=201)
@ -33,12 +34,14 @@ async def create_user(
create_user_event, "User Created", f"Created by {current_user.full_name}", session=session
)
return db.users.create(session, new_user.dict())
db = get_database(session)
return db.users.create(new_user.dict())
@admin_router.get("/{id}", response_model=UserOut)
async def get_user(id: int, session: Session = Depends(generate_session)):
return db.users.get(session, id)
db = get_database(session)
return db.users.get(id)
@admin_router.delete("/{id}")
@ -56,7 +59,8 @@ def delete_user(
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="SUPER_USER")
try:
db.users.delete(session, id)
db = get_database(session)
db.users.delete(id)
background_tasks.add_task(create_user_event, "User Deleted", f"User ID: {id}", session=session)
except Exception:
raise HTTPException(status.HTTP_400_BAD_REQUEST)
@ -87,7 +91,9 @@ async def update_user(
# prevent an admin from demoting themself
raise HTTPException(status.HTTP_403_FORBIDDEN)
db.users.update(session, id, new_data.dict())
db = get_database(session)
db.users.update(id, new_data.dict())
if current_user.id == id:
access_token = security.create_access_token(data=dict(sub=new_data.email))
return {"access_token": access_token, "token_type": "bearer"}

View file

@ -2,7 +2,7 @@ from fastapi import Depends
from sqlalchemy.orm.session import Session
from mealie.core.dependencies import get_current_user
from mealie.db.database import db
from mealie.db.database import get_database
from mealie.db.db_setup import generate_session
from mealie.routes.routers import UserAPIRouter
from mealie.routes.users._helpers import assert_user_change_allowed
@ -14,8 +14,8 @@ user_router = UserAPIRouter()
@user_router.get("/{id}/favorites", response_model=UserFavorites)
async def get_favorites(id: str, session: Session = Depends(generate_session)):
""" Get user's favorite recipes """
return db.users.get(session, id, override_schema=UserFavorites)
db = get_database(session)
return db.users.get(id, override_schema=UserFavorites)
@user_router.post("/{id}/favorites/{slug}")
@ -29,7 +29,8 @@ def add_favorite(
assert_user_change_allowed(id, current_user)
current_user.favorite_recipes.append(slug)
db.users.update(session, current_user.id, current_user)
db = get_database(session)
db.users.update(current_user.id, current_user)
@user_router.delete("/{id}/favorites/{slug}")
@ -43,6 +44,7 @@ def remove_favorite(
assert_user_change_allowed(id, current_user)
current_user.favorite_recipes = [x for x in current_user.favorite_recipes if x != slug]
db.users.update(session, current_user.id, current_user)
db = get_database(session)
db.users.update(current_user.id, current_user)
return

View file

@ -3,7 +3,7 @@ from sqlalchemy.orm.session import Session
from mealie.core.config import settings
from mealie.core.security import hash_password
from mealie.db.database import db
from mealie.db.database import get_database
from mealie.db.db_setup import generate_session
from mealie.routes.routers import UserAPIRouter
from mealie.schema.user import ChangePassword
@ -15,7 +15,9 @@ user_router = UserAPIRouter(prefix="")
@user_router.put("/{id}/reset-password")
async def reset_user_password(id: int, session: Session = Depends(generate_session)):
new_password = hash_password(settings.DEFAULT_PASSWORD)
db.users.update_password(session, id, new_password)
db = get_database(session)
db.users.update_password(id, new_password)
@user_router.put("/{id}/password")

View file

@ -7,7 +7,6 @@ from sqlalchemy.orm.session import Session
from mealie.core.config import get_app_dirs, get_settings
from mealie.core.root_logger import get_logger
from mealie.db.data_access_layer.db_access import DatabaseAccessLayer
from mealie.db.database import get_database
from mealie.db.db_setup import SessionLocal
from mealie.schema.user.user import PrivateUser
@ -45,8 +44,6 @@ class BaseHttpService(Generic[T, D], ABC):
delete_one: Callable = None
delete_all: Callable = None
db_access: DatabaseAccessLayer = None
# Type Definitions
_schema = None
@ -64,7 +61,7 @@ class BaseHttpService(Generic[T, D], ABC):
self.background_tasks = background_tasks
# Static Globals Dependency Injection
self.db = get_database()
self.db = get_database(session)
self.app_dirs = get_app_dirs()
self.settings = get_settings()
@ -110,7 +107,7 @@ class BaseHttpService(Generic[T, D], ABC):
def group_id(self):
# TODO: Populate Group in Private User Call WARNING: May require significant refactoring
if not self._group_id_cache:
group = self.db.groups.get(self.session, self.user.group, "name")
group = self.db.groups.get(self.user.group, "name")
self._group_id_cache = group.id
return self._group_id_cache

View file

@ -1,27 +1,37 @@
from abc import ABC, abstractmethod
from typing import Generic, TypeVar
from fastapi import HTTPException, status
from pydantic import BaseModel
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from mealie.core.root_logger import get_logger
from mealie.db.data_access_layer.db_access import DatabaseAccessLayer
from mealie.db.data_access_layer._access_model import AccessModel
C = TypeVar("C", bound=BaseModel)
R = TypeVar("R", bound=BaseModel)
U = TypeVar("U", bound=BaseModel)
DAL = TypeVar("DAL", bound=DatabaseAccessLayer)
DAL = TypeVar("DAL", bound=AccessModel)
logger = get_logger()
class CrudHttpMixins(Generic[C, R, U]):
item: C
class CrudHttpMixins(Generic[C, R, U], ABC):
item: R
session: Session
dal: DAL
@property
@abstractmethod
def dal(self) -> DAL:
...
def populate_item(self, id: int) -> R:
self.item = self.dal.get_one(id)
return self.item
def _create_one(self, data: C, exception_msg="generic-create-error") -> R:
try:
self.item = self.dal.create(self.session, data)
self.item = self.dal.create(data)
except Exception as ex:
logger.exception(ex)
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail={"message": exception_msg, "exception": str(ex)})
@ -33,17 +43,26 @@ class CrudHttpMixins(Generic[C, R, U]):
return
target_id = item_id or self.item.id
self.item = self.dal.update(self.session, target_id, data)
self.item = self.dal.update(target_id, data)
return self.item
def _patch_one(self) -> None:
raise NotImplementedError
def _delete_one(self, item_id: int = None) -> None:
if not self.item:
return
def _patch_one(self, data: U, item_id: int) -> None:
try:
self.item = self.dal.patch(item_id, data.dict(exclude_unset=True, exclude_defaults=True))
except IntegrityError:
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail={"message": "generic-patch-error"})
def _delete_one(self, item_id: int = None) -> R:
target_id = item_id or self.item.id
self.item = self.dal.delete(self.session, target_id)
logger.info(f"Deleting item with id {target_id}")
try:
self.item = self.dal.delete(target_id)
except Exception as ex:
logger.exception(ex)
raise HTTPException(
status.HTTP_400_BAD_REQUEST, detail={"message": "generic-delete-error", "exception": str(ex)}
)
return self.item

View file

@ -10,7 +10,7 @@ from pydantic.main import BaseModel
from mealie.core import root_logger
from mealie.core.config import app_dirs
from mealie.db.database import db
from mealie.db.database import get_database
from mealie.db.db_setup import create_session
from mealie.services.events import create_backup_event
@ -114,29 +114,31 @@ def backup_all(
):
db_export = ExportDatabase(tag=tag, templates=templates)
db = get_database(session)
if export_users:
all_users = db.users.get_all(session)
all_users = db.users.get_all()
db_export.export_items(all_users, "users")
if export_groups:
all_groups = db.groups.get_all(session)
all_groups = db.groups.get_all()
db_export.export_items(all_groups, "groups")
if export_recipes:
all_recipes = db.recipes.get_all(session)
all_recipes = db.recipes.get_all()
db_export.export_recipe_dirs()
db_export.export_items(all_recipes, "recipes", export_list=False, slug_folder=True)
db_export.export_templates(all_recipes)
all_comments = db.comments.get_all(session)
all_comments = db.comments.get_all()
db_export.export_items(all_comments, "comments")
if export_settings:
all_settings = db.settings.get_all(session)
all_settings = db.settings.get_all()
db_export.export_items(all_settings, "settings")
if export_notifications:
all_notifications = db.event_notifications.get_all(session)
all_notifications = db.event_notifications.get_all()
db_export.export_items(all_notifications, "notifications")
return db_export.finish_export()

View file

@ -8,7 +8,7 @@ from pydantic.main import BaseModel
from sqlalchemy.orm.session import Session
from mealie.core.config import app_dirs
from mealie.db.database import db
from mealie.db.database import get_database
from mealie.schema.admin import (
CommentImport,
GroupImport,
@ -44,6 +44,7 @@ class ImportDatabase:
"""
self.user = user
self.session = session
self.db = get_database(session)
self.archive = app_dirs.BACKUP_DIR.joinpath(zip_archive)
self.force_imports = force_import
@ -72,7 +73,7 @@ class ImportDatabase:
recipe.user_id = self.user.id
import_status = self.import_model(
db_table=db.recipes,
db_table=self.db.recipes,
model=recipe,
return_model=RecipeImport,
name_attr="name",
@ -101,7 +102,7 @@ class ImportDatabase:
comment: CommentOut
self.import_model(
db_table=db.comments,
db_table=self.db.comments,
model=comment,
return_model=CommentImport,
name_attr="uuid",
@ -166,7 +167,7 @@ class ImportDatabase:
for notify in notifications:
import_status = self.import_model(
db_table=db.event_notifications,
db_table=self.db.event_notifications,
model=notify,
return_model=NotificationImport,
name_attr="name",
@ -183,7 +184,7 @@ class ImportDatabase:
settings = settings[0]
try:
db.settings.update(self.session, 1, settings.dict())
self.db.settings.update(1, settings.dict())
import_status = SettingsImport(name="Site Settings", status=True)
except Exception as inst:
@ -198,7 +199,7 @@ class ImportDatabase:
group_imports = []
for group in groups:
import_status = self.import_model(db.groups, group, GroupImport, search_key="name")
import_status = self.import_model(self.db.groups, group, GroupImport, search_key="name")
group_imports.append(import_status)
return group_imports
@ -209,13 +210,13 @@ class ImportDatabase:
user_imports = []
for user in users:
if user.id == 1: # Update Default User
db.users.update(self.session, 1, user.dict())
self.db.users.update(1, user.dict())
import_status = UserImport(name=user.full_name, status=True)
user_imports.append(import_status)
continue
import_status = self.import_model(
db_table=db.users,
db_table=self.db.users,
model=user,
return_model=UserImport,
name_attr="full_name",
@ -283,7 +284,7 @@ class ImportDatabase:
model_name = getattr(model, name_attr)
search_value = getattr(model, search_key)
item = db_table.get(self.session, search_value, search_key)
item = db_table.get(search_value, search_key)
if item:
if not self.force_imports:
return return_model(
@ -293,9 +294,9 @@ class ImportDatabase:
)
primary_key = getattr(item, db_table.primary_key)
db_table.delete(self.session, primary_key)
db_table.delete(primary_key)
try:
db_table.create(self.session, model.dict())
db_table.create(model.dict())
import_status = return_model(name=model_name, status=True)
except Exception as inst:

View file

@ -1,13 +1,12 @@
import apprise
from sqlalchemy.orm.session import Session
from mealie.db.database import db
from mealie.db.database import get_database
from mealie.db.db_setup import create_session
from mealie.schema.events import Event, EventCategory
def test_notification(notification_url, event=None) -> bool:
if event is None:
event = Event(
title="Test Notification",
@ -38,9 +37,10 @@ def post_notifications(event: Event, notification_urls=list[str], hard_fail=Fals
def save_event(title, text, category, session: Session, attachment=None):
event = Event(title=title, text=text, category=category)
session = session or create_session()
db.events.create(session, event.dict())
db = get_database(session)
db.events.create(event.dict())
notification_objects = db.event_notifications.get(session=session, match_value=True, match_key=category, limit=9999)
notification_objects = db.event_notifications.get(match_value=True, match_key=category, limit=9999)
notification_urls = [x.notification_url for x in notification_objects]
post_notifications(event, notification_urls, attachment=attachment)

View file

@ -1,7 +1,8 @@
from __future__ import annotations
from functools import cached_property
from mealie.core.root_logger import get_logger
from mealie.db.database import get_database
from mealie.schema.cookbook.cookbook import CreateCookBook, ReadCookBook, RecipeCookBook, SaveCookBook, UpdateCookBook
from mealie.services._base_http_service.crud_http_mixins import CrudHttpMixins
from mealie.services._base_http_service.http_services import UserHttpService
@ -12,17 +13,16 @@ logger = get_logger(module=__name__)
class CookbookService(
UserHttpService[int, ReadCookBook],
CrudHttpMixins[CreateCookBook, ReadCookBook, UpdateCookBook],
UserHttpService[int, ReadCookBook],
):
event_func = create_group_event
_restrict_by_group = True
_schema = ReadCookBook
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.dal = get_database().cookbooks
@cached_property
def dal(self):
return self.db.cookbooks
def populate_item(self, item_id: int) -> RecipeCookBook:
try:
@ -31,13 +31,13 @@ class CookbookService(
pass
if isinstance(item_id, int):
self.item = self.db.cookbooks.get_one(self.session, item_id, override_schema=RecipeCookBook)
self.item = self.dal.get_one(item_id, override_schema=RecipeCookBook)
else:
self.item = self.db.cookbooks.get_one(self.session, item_id, key="slug", override_schema=RecipeCookBook)
self.item = self.dal.get_one(item_id, key="slug", override_schema=RecipeCookBook)
def get_all(self) -> list[ReadCookBook]:
items = self.db.cookbooks.get(self.session, self.group_id, "group_id", limit=999)
items = self.dal.get(self.group_id, "group_id", limit=999)
items.sort(key=lambda x: x.position)
return items
@ -52,7 +52,7 @@ class CookbookService(
updated = []
for cookbook in data:
cb = self.db.cookbooks.update(self.session, cookbook.id, cookbook)
cb = self.dal.update(cookbook.id, cookbook)
updated.append(cb)
return updated

View file

@ -2,7 +2,7 @@ from __future__ import annotations
from uuid import uuid4
from fastapi import Depends, HTTPException, status
from fastapi import Depends
from mealie.core.dependencies.grouped import UserDeps
from mealie.core.root_logger import get_logger
@ -17,7 +17,7 @@ logger = get_logger(module=__name__)
class GroupSelfService(UserHttpService[int, str]):
_restrict_by_group = True
_restrict_by_group = False
event_func = create_group_event
item: GroupInDB
@ -31,31 +31,21 @@ class GroupSelfService(UserHttpService[int, str]):
"""Override parent method to remove `item_id` from arguments"""
return super().write_existing(item_id=0, deps=deps)
def assert_existing(self, _: str = None):
self.populate_item()
if not self.item:
raise HTTPException(status.HTTP_404_NOT_FOUND)
if self.item.id != self.group_id:
raise HTTPException(status.HTTP_403_FORBIDDEN)
def populate_item(self, _: str = None) -> GroupInDB:
self.item = self.db.groups.get(self.session, self.group_id)
self.item = self.db.groups.get(self.group_id)
return self.item
def update_categories(self, new_categories: list[CategoryBase]):
self.item.categories = new_categories
return self.db.groups.update(self.session, self.group_id, self.item)
return self.db.groups.update(self.group_id, self.item)
def update_preferences(self, new_preferences: UpdateGroupPreferences):
self.db.group_preferences.update(self.session, self.group_id, new_preferences)
self.db.group_preferences.update(self.group_id, new_preferences)
return self.populate_item()
def create_invite_token(self, uses: int = 1) -> None:
token = SaveInviteToken(uses_left=uses, group_id=self.group_id, token=uuid4().hex)
return self.db.group_tokens.create(self.session, token)
return self.db.group_invite_tokens.create(token)
def get_invite_tokens(self) -> list[ReadInviteToken]:
return self.db.group_tokens.multi_query(self.session, {"group_id": self.group_id})
return self.db.group_invite_tokens.multi_query({"group_id": self.group_id})

View file

@ -1,16 +1,15 @@
from mealie.db.database import get_database
from mealie.db.data_access_layer.access_model_factory import Database
from mealie.schema.group.group_preferences import CreateGroupPreferences
from mealie.schema.user.user import GroupBase, GroupInDB
def create_new_group(session, g_base: GroupBase, g_preferences: CreateGroupPreferences = None) -> GroupInDB:
db = get_database()
created_group = db.groups.create(session, g_base)
def create_new_group(db: Database, g_base: GroupBase, g_preferences: CreateGroupPreferences = None) -> GroupInDB:
created_group = db.groups.create(g_base)
g_preferences = g_preferences or CreateGroupPreferences(group_id=0) # Assign Temporary ID before group is created
g_preferences.group_id = created_group.id
db.group_preferences.create(session, g_preferences)
db.group_preferences.create(g_preferences)
return created_group

View file

@ -1,9 +1,9 @@
from __future__ import annotations
from datetime import date
from functools import cached_property
from mealie.core.root_logger import get_logger
from mealie.db.database import get_database
from mealie.schema.meal_plan import CreatePlanEntry, ReadPlanEntry, SavePlanEntry, UpdatePlanEntry
from .._base_http_service.crud_http_mixins import CrudHttpMixins
@ -13,26 +13,27 @@ from ..events import create_group_event
logger = get_logger(module=__name__)
class MealService(UserHttpService[int, ReadPlanEntry], CrudHttpMixins[CreatePlanEntry, ReadPlanEntry, UpdatePlanEntry]):
class MealService(CrudHttpMixins[CreatePlanEntry, ReadPlanEntry, UpdatePlanEntry], UserHttpService[int, ReadPlanEntry]):
event_func = create_group_event
_restrict_by_group = True
_schema = ReadPlanEntry
item: ReadPlanEntry
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.dal = get_database().meals
@cached_property
def dal(self):
return self.db.meals
def populate_item(self, id: int) -> ReadPlanEntry:
self.item = self.db.meals.get_one(self.session, id)
self.item = self.dal.get_one(id)
return self.item
def get_slice(self, start: date = None, end: date = None) -> list[ReadPlanEntry]:
# 2 days ago
return self.db.meals.get_slice(self.session, start, end, group_id=self.group_id)
return self.dal.get_slice(start, end, group_id=self.group_id)
def get_today(self) -> list[ReadPlanEntry]:
return self.db.meals.get_today(self.session, group_id=self.group_id)
return self.dal.get_today(group_id=self.group_id)
def create_one(self, data: CreatePlanEntry) -> ReadPlanEntry:
data = self.cast(data, SavePlanEntry)

View file

@ -1,7 +1,8 @@
from __future__ import annotations
from functools import cached_property
from mealie.core.root_logger import get_logger
from mealie.db.database import get_database
from mealie.schema.group import ReadWebhook
from mealie.schema.group.webhook import CreateWebhook, SaveWebhook
from mealie.services._base_http_service.crud_http_mixins import CrudHttpMixins
@ -11,22 +12,21 @@ from mealie.services.events import create_group_event
logger = get_logger(module=__name__)
class WebhookService(UserHttpService[int, ReadWebhook], CrudHttpMixins[ReadWebhook, CreateWebhook, CreateWebhook]):
class WebhookService(CrudHttpMixins[ReadWebhook, CreateWebhook, CreateWebhook], UserHttpService[int, ReadWebhook]):
event_func = create_group_event
_restrict_by_group = True
_schema = ReadWebhook
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.dal = get_database().webhooks
@cached_property
def dal(self):
return self.db.webhooks
def populate_item(self, id: int) -> ReadWebhook:
self.item = self.db.webhooks.get_one(self.session, id)
self.item = self.dal.get_one(id)
return self.item
def get_all(self) -> list[ReadWebhook]:
return self.db.webhooks.get(self.session, self.group_id, match_key="group_id", limit=9999)
return self.dal.get(self.group_id, match_key="group_id", limit=9999)
def create_one(self, data: CreateWebhook) -> ReadWebhook:
data = self.cast(data, SaveWebhook)

View file

@ -1,50 +0,0 @@
from datetime import date, timedelta
from typing import Union
from sqlalchemy.orm.session import Session
from mealie.db.database import db
from mealie.db.db_setup import create_session
from mealie.schema.meal_plan import MealDayIn, MealPlanIn
from mealie.schema.recipe import Recipe
from mealie.schema.user import GroupInDB
def set_mealplan_dates(meal_plan_base: MealPlanIn) -> MealPlanIn:
for x, plan_days in enumerate(meal_plan_base.plan_days):
plan_days: MealDayIn
plan_days.date = meal_plan_base.start_date + timedelta(days=x)
def get_todays_meal(session: Session, group: Union[int, GroupInDB]) -> Recipe:
"""Returns the given mealplan for today based off the group. If the group
Type is of type int, then a query will be made to the database to get the
grop object."
Args:
session (Session): SqlAlchemy Session
group (Union[int, GroupInDB]): Either the id of the group or the GroupInDB Object
Returns:
Recipe: Pydantic Recipe Object
"""
session = session or create_session()
if isinstance(group, int):
group: GroupInDB = db.groups.get(session, group)
today_slug = None
for mealplan in group.mealplans:
for plan_day in mealplan.plan_days:
if plan_day.date == date.today():
if plan_day.meals[0].slug and plan_day.meals[0].slug != "":
today_slug = plan_day.meals[0].slug
else:
return plan_day.meals[0]
if today_slug:
return db.recipes.get(session, today_slug)
else:
return None

View file

@ -7,7 +7,7 @@ import yaml
from pydantic import BaseModel
from mealie.core import root_logger
from mealie.db.database import db
from mealie.db.database import get_database
from mealie.schema.admin import MigrationImport
from mealie.schema.recipe import Recipe
from mealie.schema.user.user import PrivateUser
@ -37,6 +37,10 @@ class MigrationBase(BaseModel):
user: PrivateUser
@property
def db(self):
return get_database(self.session)
@property
def temp_dir(self) -> TemporaryDirectory:
"""unpacks the migration_file into a temporary directory
@ -66,7 +70,7 @@ class MigrationBase(BaseModel):
with open(yaml_file, "r") as f:
contents = f.read().split("---")
recipe_data = {}
for x, document in enumerate(contents):
for _, document in enumerate(contents):
# Check if None or Empty String
if document is None or document == "":
@ -172,7 +176,7 @@ class MigrationBase(BaseModel):
exception = ""
status = False
try:
db.recipes.create(self.session, recipe.dict())
self.db.recipes.create(recipe.dict())
status = True
except Exception as inst:

View file

@ -0,0 +1,37 @@
from __future__ import annotations
from functools import cached_property
from mealie.schema.recipe.recipe_ingredient import CreateIngredientFood, IngredientFood
from mealie.services._base_http_service.crud_http_mixins import CrudHttpMixins
from mealie.services._base_http_service.http_services import UserHttpService
from mealie.services.events import create_recipe_event
class RecipeFoodService(
CrudHttpMixins[IngredientFood, CreateIngredientFood, CreateIngredientFood],
UserHttpService[int, IngredientFood],
):
event_func = create_recipe_event
_restrict_by_group = False
_schema = IngredientFood
@cached_property
def dal(self):
return self.db.ingredient_foods
def populate_item(self, id: int) -> IngredientFood:
self.item = self.dal.get_one(id)
return self.item
def get_all(self) -> list[IngredientFood]:
return self.dal.get_all()
def create_one(self, data: CreateIngredientFood) -> IngredientFood:
return self._create_one(data)
def update_one(self, data: IngredientFood, item_id: int = None) -> IngredientFood:
return self._update_one(data, item_id)
def delete_one(self, id: int = None) -> IngredientFood:
return self._delete_one(id)

View file

@ -1,13 +1,15 @@
from functools import cached_property
from pathlib import Path
from shutil import copytree, rmtree
from typing import Union
from fastapi import Depends, HTTPException, status
from sqlalchemy.exc import IntegrityError
from mealie.core.dependencies.grouped import PublicDeps, UserDeps
from mealie.core.root_logger import get_logger
from mealie.db.data_access_layer.recipe_access_model import RecipeDataAccessModel
from mealie.schema.recipe.recipe import CreateRecipe, Recipe, RecipeSummary
from mealie.services._base_http_service.crud_http_mixins import CrudHttpMixins
from mealie.services._base_http_service.http_services import UserHttpService
from mealie.services.events import create_recipe_event
from mealie.services.recipe.mixins import recipe_creation_factory
@ -15,7 +17,7 @@ from mealie.services.recipe.mixins import recipe_creation_factory
logger = get_logger(module=__name__)
class RecipeService(UserHttpService[str, Recipe]):
class RecipeService(CrudHttpMixins[CreateRecipe, Recipe, Recipe], UserHttpService[str, Recipe]):
"""
Class Methods:
`read_existing`: Reads an existing recipe from the database.
@ -25,6 +27,10 @@ class RecipeService(UserHttpService[str, Recipe]):
event_func = create_recipe_event
@cached_property
def dal(self) -> RecipeDataAccessModel:
return self.db.recipes
@classmethod
def write_existing(cls, slug: str, deps: UserDeps = Depends()):
return super().write_existing(slug, deps)
@ -35,75 +41,49 @@ class RecipeService(UserHttpService[str, Recipe]):
def assert_existing(self, slug: str):
self.populate_item(slug)
if not self.item:
raise HTTPException(status.HTTP_404_NOT_FOUND)
if not self.item.settings.public and not self.user:
raise HTTPException(status.HTTP_403_FORBIDDEN)
def populate_item(self, slug: str) -> Recipe:
self.item = self.db.recipes.get(self.session, slug)
return self.item
# CRUD METHODS
def get_all(self, start=0, limit=None):
return self.db.recipes.multi_query(
self.session, {"group_id": self.user.group_id}, start=start, limit=limit, override_schema=RecipeSummary
{"group_id": self.user.group_id},
start=start,
limit=limit,
override_schema=RecipeSummary,
)
def create_one(self, create_data: Union[Recipe, CreateRecipe]) -> Recipe:
create_data = recipe_creation_factory(self.user, name=create_data.name, additional_attrs=create_data.dict())
try:
self.item = self.db.recipes.create(self.session, create_data)
except IntegrityError:
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail={"message": "RECIPE_ALREADY_EXISTS"})
self._create_one(create_data, "RECIPE_ALREAD_EXISTS")
self._create_event(
"Recipe Created",
f"'{self.item.name}' by {self.user.username} \n {self.settings.BASE_URL}/recipe/{self.item.slug}",
)
return self.item
def update_one(self, update_data: Recipe) -> Recipe:
original_slug = self.item.slug
try:
self.item = self.db.recipes.update(self.session, original_slug, update_data)
except IntegrityError:
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail={"message": "RECIPE_ALREADY_EXISTS"})
self._check_assets(original_slug)
self._update_one(update_data, original_slug)
self.check_assets(original_slug)
return self.item
def patch_one(self, patch_data: Recipe) -> Recipe:
original_slug = self.item.slug
try:
self.item = self.db.recipes.patch(
self.session, original_slug, patch_data.dict(exclude_unset=True, exclude_defaults=True)
)
except IntegrityError:
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail={"message": "RECIPE_ALREADY_EXISTS"})
self._check_assets(original_slug)
self._patch_one(patch_data, original_slug)
self.check_assets(original_slug)
return self.item
def delete_one(self) -> Recipe:
try:
recipe: Recipe = self.db.recipes.delete(self.session, self.item.slug)
self._delete_assets()
except Exception:
raise HTTPException(status.HTTP_400_BAD_REQUEST)
self._delete_one(self.item.slug)
self.delete_assets()
self._create_event("Recipe Delete", f"'{self.item.name}' deleted by {self.user.full_name}")
return self.item
self._create_event("Recipe Delete", f"'{recipe.name}' deleted by {self.user.full_name}")
return recipe
def _check_assets(self, original_slug: str) -> None:
def check_assets(self, original_slug: str) -> None:
"""Checks if the recipe slug has changed, and if so moves the assets to a new file with the new slug."""
if original_slug != self.item.slug:
current_dir = self.app_dirs.RECIPE_DATA_DIR.joinpath(original_slug)
@ -123,7 +103,7 @@ class RecipeService(UserHttpService[str, Recipe]):
if file.name not in all_asset_files:
file.unlink()
def _delete_assets(self) -> None:
def delete_assets(self) -> None:
recipe_dir = self.item.directory
rmtree(recipe_dir, ignore_errors=True)
logger.info(f"Recipe Directory Removed: {self.item.slug}")

View file

@ -0,0 +1,37 @@
from __future__ import annotations
from functools import cached_property
from mealie.schema.recipe.recipe_ingredient import CreateIngredientUnit, IngredientUnit
from mealie.services._base_http_service.crud_http_mixins import CrudHttpMixins
from mealie.services._base_http_service.http_services import UserHttpService
from mealie.services.events import create_recipe_event
class RecipeUnitService(
CrudHttpMixins[IngredientUnit, CreateIngredientUnit, CreateIngredientUnit],
UserHttpService[int, IngredientUnit],
):
event_func = create_recipe_event
_restrict_by_group = False
_schema = IngredientUnit
@cached_property
def dal(self):
return self.db.ingredient_units
def populate_item(self, id: int) -> IngredientUnit:
self.item = self.dal.get_one(id)
return self.item
def get_all(self) -> list[IngredientUnit]:
return self.dal.get_all()
def create_one(self, data: CreateIngredientUnit) -> IngredientUnit:
return self._create_one(data)
def update_one(self, data: IngredientUnit, item_id: int = None) -> IngredientUnit:
return self._update_one(data, item_id)
def delete_one(self, id: int = None) -> IngredientUnit:
return self._delete_one(id)

View file

@ -3,7 +3,7 @@ import datetime
from apscheduler.schedulers.background import BackgroundScheduler
from mealie.core import root_logger
from mealie.db.database import db
from mealie.db.database import get_database
from mealie.db.db_setup import create_session
from mealie.db.models.event import Event
from mealie.schema.user import GroupInDB
@ -39,7 +39,8 @@ def update_webhook_schedule():
poll the database for changes and reschedule the webhook time
"""
session = create_session()
all_groups: list[GroupInDB] = db.groups.get_all(session)
db = get_database(session)
all_groups: list[GroupInDB] = db.groups.get_all()
for group in all_groups:
@ -100,7 +101,8 @@ def add_group_to_schedule(scheduler, group: GroupInDB):
def init_webhook_schedule(scheduler, job_store: dict):
session = create_session()
all_groups: list[GroupInDB] = db.groups.get_all(session)
db = get_database(session)
all_groups: list[GroupInDB] = db.groups.get_all()
for group in all_groups:
job_store.update(add_group_to_schedule(scheduler, group))

View file

@ -29,14 +29,14 @@ class RegistrationService(PublicHttpService[int, str]):
elif registration.group_token and registration.group_token != "":
token_entry = self.db.group_tokens.get(self.session, registration.group_token)
token_entry = self.db.group_invite_tokens.get(registration.group_token)
print("Token Entry", token_entry)
if not token_entry:
raise HTTPException(status.HTTP_400_BAD_REQUEST, {"message": "Invalid group token"})
group = self.db.groups.get(self.session, token_entry.group_id)
group = self.db.groups.get(token_entry.group_id)
else:
raise HTTPException(status.HTTP_400_BAD_REQUEST, {"message": "Missing group"})
@ -47,10 +47,10 @@ class RegistrationService(PublicHttpService[int, str]):
token_entry.uses_left = token_entry.uses_left - 1
if token_entry.uses_left == 0:
self.db.group_tokens.delete(self.session, token_entry.token)
self.db.group_invite_tokens.delete(token_entry.token)
else:
self.db.group_tokens.update(self.session, token_entry.token, token_entry)
self.db.group_invite_tokens.update(token_entry.token, token_entry)
return user
@ -64,7 +64,7 @@ class RegistrationService(PublicHttpService[int, str]):
group=group.name,
)
return self.db.users.create(self.session, new_user)
return self.db.users.create(new_user)
def _register_new_group(self) -> GroupInDB:
group_data = GroupBase(name=self.registration.group)
@ -81,4 +81,4 @@ class RegistrationService(PublicHttpService[int, str]):
recipe_disable_amount=self.registration.advanced,
)
return create_new_group(self.session, group_data, group_preferences)
return create_new_group(self.db, group_data, group_preferences)

View file

@ -25,7 +25,7 @@ class UserService(UserHttpService[int, str]):
def _populate_target_user(self, id: int = None):
if id:
self.target_user = self.db.users.get(self.session, id)
self.target_user = self.db.users.get(id)
if not self.target_user:
raise HTTPException(status.HTTP_404_NOT_FOUND)
else:
@ -38,4 +38,4 @@ class UserService(UserHttpService[int, str]):
self.target_user.password = hash_password(password_change.new_password)
return self.db.users.update_password(self.session, self.target_user.id, self.target_user.password)
return self.db.users.update_password(self.target_user.id, self.target_user.password)

View file

@ -3,21 +3,22 @@ import json
import requests
from sqlalchemy.orm.session import Session
from mealie.db.database import db
from mealie.db.database import get_database
from mealie.db.db_setup import create_session
from mealie.schema.user import GroupInDB
from mealie.services.events import create_scheduled_event
from mealie.services.meal_services import get_todays_meal
def post_webhooks(group: int, session: Session = None, force=True):
session = session or create_session()
group_settings: GroupInDB = db.groups.get(session, group)
db = get_database(session)
group_settings: GroupInDB = db.groups.get(group)
if not group_settings.webhook_enable and not force:
return
todays_recipe = get_todays_meal(session, group)
# TODO: Fix Mealplan Webhooks
todays_recipe = None
if not todays_recipe:
return

593
poetry.lock generated
View file

@ -67,7 +67,7 @@ zookeeper = ["kazoo"]
[[package]]
name = "astroid"
version = "2.6.5"
version = "2.8.0"
description = "An abstract syntax tree for Python with inference support."
category = "dev"
optional = false
@ -75,6 +75,7 @@ python-versions = "~=3.6"
[package.dependencies]
lazy-object-proxy = ">=1.4.0"
typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""}
wrapt = ">=1.11,<1.13"
[[package]]
@ -117,14 +118,14 @@ typecheck = ["mypy"]
[[package]]
name = "beautifulsoup4"
version = "4.9.3"
version = "4.10.0"
description = "Screen-scraping library"
category = "main"
optional = false
python-versions = "*"
python-versions = ">3.0.0"
[package.dependencies]
soupsieve = {version = ">1.2", markers = "python_version >= \"3.0\""}
soupsieve = ">1.2"
[package.extras]
html5lib = ["html5lib"]
@ -173,7 +174,7 @@ pycparser = "*"
[[package]]
name = "charset-normalizer"
version = "2.0.4"
version = "2.0.6"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
category = "main"
optional = false
@ -222,7 +223,7 @@ toml = ["toml"]
[[package]]
name = "cryptography"
version = "3.4.7"
version = "3.4.8"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
category = "main"
optional = false
@ -241,7 +242,7 @@ test = ["pytest (>=6.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pret
[[package]]
name = "decorator"
version = "5.0.9"
version = "5.1.0"
description = "Decorators for Humans"
category = "main"
optional = false
@ -343,7 +344,7 @@ dev = ["twine", "markdown", "flake8"]
[[package]]
name = "greenlet"
version = "1.1.0"
version = "1.1.1"
description = "Lightweight in-process concurrent programming"
category = "main"
optional = false
@ -424,7 +425,7 @@ python-versions = ">=3.5"
[[package]]
name = "importlib-metadata"
version = "4.6.3"
version = "4.8.1"
description = "Read metadata from Python packages"
category = "dev"
optional = false
@ -588,7 +589,7 @@ i18n = ["babel (>=2.9.0)"]
[[package]]
name = "mkdocs-material"
version = "7.2.2"
version = "7.2.7"
description = "A Material Design theme for MkDocs"
category = "dev"
optional = false
@ -596,21 +597,18 @@ python-versions = "*"
[package.dependencies]
markdown = ">=3.2"
mkdocs = ">=1.1"
mkdocs = ">=1.2.2"
mkdocs-material-extensions = ">=1.0"
Pygments = ">=2.4"
pymdown-extensions = ">=7.0"
[[package]]
name = "mkdocs-material-extensions"
version = "1.0.1"
version = "1.0.3"
description = "Extension pack for Python Markdown."
category = "dev"
optional = false
python-versions = ">=3.5"
[package.dependencies]
mkdocs-material = ">=5.0.0"
python-versions = ">=3.6"
[[package]]
name = "mypy-extensions"
@ -679,22 +677,35 @@ test = ["allpairspy", "click", "faker", "pytest (>=6.0.1)", "pytest-discord (>=0
[[package]]
name = "pillow"
version = "8.3.1"
version = "8.3.2"
description = "Python Imaging Library (Fork)"
category = "main"
optional = false
python-versions = ">=3.6"
[[package]]
name = "platformdirs"
version = "2.3.0"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev"
optional = false
python-versions = ">=3.6"
[package.extras]
docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"]
test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"]
[[package]]
name = "pluggy"
version = "0.13.1"
version = "1.0.0"
description = "plugin and hook calling mechanisms for python"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
python-versions = ">=3.6"
[package.extras]
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "psycopg2-binary"
@ -773,7 +784,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "pygments"
version = "2.9.0"
version = "2.10.0"
description = "Pygments is a syntax highlighting package written in Python."
category = "dev"
optional = false
@ -789,18 +800,20 @@ python-versions = "*"
[[package]]
name = "pylint"
version = "2.9.6"
version = "2.11.1"
description = "python code static checker"
category = "dev"
optional = false
python-versions = "~=3.6"
[package.dependencies]
astroid = ">=2.6.5,<2.7"
astroid = ">=2.8.0,<2.9"
colorama = {version = "*", markers = "sys_platform == \"win32\""}
isort = ">=4.2.5,<6"
mccabe = ">=0.6,<0.7"
platformdirs = ">=2.2.0"
toml = ">=0.7.1"
typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""}
[[package]]
name = "pymdown-extensions"
@ -835,7 +848,7 @@ rdflib = "*"
[[package]]
name = "pytest"
version = "6.2.4"
version = "6.2.5"
description = "pytest: simple powerful testing with Python"
category = "dev"
optional = false
@ -847,7 +860,7 @@ attrs = ">=19.2.0"
colorama = {version = "*", markers = "sys_platform == \"win32\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=0.12,<1.0.0a1"
pluggy = ">=0.12,<2.0"
py = ">=1.8.2"
toml = "*"
@ -964,7 +977,7 @@ pyyaml = "*"
[[package]]
name = "rdflib"
version = "6.0.0"
version = "6.0.1"
description = "RDFLib is a Python library for working with RDF, a simple yet powerful language for representing information."
category = "main"
optional = false
@ -977,22 +990,22 @@ pyparsing = "*"
[package.extras]
docs = ["sphinx (<5)", "sphinxcontrib-apidoc"]
html = ["html5lib"]
tests = ["html5lib", "networkx", "nose (==1.3.7)", "nose-timer", "coverage", "black (==21.7b0)", "flake8", "doctest-ignore-unicode (==0.1.2)"]
tests = ["html5lib", "networkx", "nose (==1.3.7)", "nose-timer", "coverage", "black (==21.6b0)", "flake8", "doctest-ignore-unicode (==0.1.2)"]
[[package]]
name = "rdflib-jsonld"
version = "0.5.0"
version = "0.6.2"
description = "rdflib extension adding JSON-LD parser and serializer"
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
rdflib = ">=4.2.2"
rdflib = ">=5.0.0"
[[package]]
name = "recipe-scrapers"
version = "13.3.5"
version = "13.4.0"
description = "Python package, scraping recipes from all over the internet"
category = "main"
optional = false
@ -1005,7 +1018,7 @@ requests = ">=2.19.1"
[[package]]
name = "regex"
version = "2021.7.6"
version = "2021.8.28"
description = "Alternative regular expression module, to replace re."
category = "dev"
optional = false
@ -1046,7 +1059,7 @@ rsa = ["oauthlib[signedtoken] (>=3.0.0)"]
[[package]]
name = "rich"
version = "10.7.0"
version = "10.10.0"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
category = "dev"
optional = false
@ -1103,14 +1116,14 @@ python-versions = ">=3.6"
[[package]]
name = "sqlalchemy"
version = "1.4.22"
version = "1.4.23"
description = "Database Abstraction Library"
category = "main"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
[package.dependencies]
greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\""}
greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and platform_machine in \"x86_64 X86_64 aarch64 AARCH64 ppc64le PPC64LE amd64 AMD64 win32 WIN32\""}
[package.extras]
aiomysql = ["greenlet (!=0.4.17)", "aiomysql"]
@ -1120,7 +1133,7 @@ mariadb_connector = ["mariadb (>=1.0.1)"]
mssql = ["pyodbc"]
mssql_pymssql = ["pymssql"]
mssql_pyodbc = ["pyodbc"]
mypy = ["sqlalchemy2-stubs", "mypy (>=0.800)"]
mypy = ["sqlalchemy2-stubs", "mypy (>=0.910)"]
mysql = ["mysqlclient (>=1.4.0,<2)", "mysqlclient (>=1.4.0)"]
mysql_connector = ["mysqlconnector"]
oracle = ["cx_oracle (>=7,<8)", "cx_oracle (>=7)"]
@ -1169,7 +1182,7 @@ python-versions = "*"
[[package]]
name = "typing-extensions"
version = "3.10.0.0"
version = "3.10.0.2"
description = "Backported and Experimental Type Hints for Python 3.5+"
category = "main"
optional = false
@ -1223,16 +1236,16 @@ standard = ["websockets (>=8.0.0,<9.0.0)", "watchgod (>=0.6)", "python-dotenv (>
[[package]]
name = "uvloop"
version = "0.15.3"
version = "0.16.0"
description = "Fast implementation of asyncio event loop on top of libuv"
category = "main"
optional = false
python-versions = ">=3.7"
[package.extras]
dev = ["Cython (>=0.29.20,<0.30.0)", "pytest (>=3.6.0)", "Sphinx (>=1.7.3,<1.8.0)", "sphinxcontrib-asyncio (>=0.2.0,<0.3.0)", "sphinx-rtd-theme (>=0.2.4,<0.3.0)", "aiohttp", "flake8 (>=3.8.4,<3.9.0)", "psutil", "pycodestyle (>=2.6.0,<2.7.0)", "pyOpenSSL (>=19.0.0,<19.1.0)", "mypy (>=0.800)"]
docs = ["Sphinx (>=1.7.3,<1.8.0)", "sphinxcontrib-asyncio (>=0.2.0,<0.3.0)", "sphinx-rtd-theme (>=0.2.4,<0.3.0)"]
test = ["aiohttp", "flake8 (>=3.8.4,<3.9.0)", "psutil", "pycodestyle (>=2.6.0,<2.7.0)", "pyOpenSSL (>=19.0.0,<19.1.0)", "mypy (>=0.800)"]
dev = ["Cython (>=0.29.24,<0.30.0)", "pytest (>=3.6.0)", "Sphinx (>=4.1.2,<4.2.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "aiohttp", "flake8 (>=3.9.2,<3.10.0)", "psutil", "pycodestyle (>=2.7.0,<2.8.0)", "pyOpenSSL (>=19.0.0,<19.1.0)", "mypy (>=0.800)"]
docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)"]
test = ["aiohttp", "flake8 (>=3.9.2,<3.10.0)", "psutil", "pycodestyle (>=2.7.0,<2.8.0)", "pyOpenSSL (>=19.0.0,<19.1.0)", "mypy (>=0.800)"]
[[package]]
name = "validators"
@ -1262,7 +1275,7 @@ six = ">=1.4.1"
[[package]]
name = "watchdog"
version = "2.1.3"
version = "2.1.5"
description = "Filesystem events monitoring"
category = "dev"
optional = false
@ -1321,7 +1334,7 @@ pgsql = ["psycopg2-binary"]
[metadata]
lock-version = "1.1"
python-versions = "^3.9"
content-hash = "03d6e9fea568f4167c5cc6865d417c57575305f7ad6813dd503c6f40e85090d7"
content-hash = "1b9a18e7114a8f157226c20e951dce0bd08ac884e0795f0f816e9f57d72ec309"
[metadata.files]
aiofiles = [
@ -1345,8 +1358,8 @@ apscheduler = [
{file = "APScheduler-3.7.0.tar.gz", hash = "sha256:1cab7f2521e107d07127b042155b632b7a1cd5e02c34be5a28ff62f77c900c6a"},
]
astroid = [
{file = "astroid-2.6.5-py3-none-any.whl", hash = "sha256:7b963d1c590d490f60d2973e57437115978d3a2529843f160b5003b721e1e925"},
{file = "astroid-2.6.5.tar.gz", hash = "sha256:83e494b02d75d07d4e347b27c066fd791c0c74fc96c613d1ea3de0c82c48168f"},
{file = "astroid-2.8.0-py3-none-any.whl", hash = "sha256:dcc06f6165f415220013801642bd6c9808a02967070919c4b746c6864c205471"},
{file = "astroid-2.8.0.tar.gz", hash = "sha256:fe81f80c0b35264acb5653302ffbd935d394f1775c5e4487df745bf9c2442708"},
]
atomicwrites = [
{file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
@ -1366,9 +1379,8 @@ bcrypt = [
{file = "bcrypt-3.2.0.tar.gz", hash = "sha256:5b93c1726e50a93a033c36e5ca7fdcd29a5c7395af50a6892f5d9e7c6cfbfb29"},
]
beautifulsoup4 = [
{file = "beautifulsoup4-4.9.3-py2-none-any.whl", hash = "sha256:4c98143716ef1cb40bf7f39a8e3eec8f8b009509e74904ba3a7b315431577e35"},
{file = "beautifulsoup4-4.9.3-py3-none-any.whl", hash = "sha256:fff47e031e34ec82bf17e00da8f592fe7de69aeea38be00523c04623c04fb666"},
{file = "beautifulsoup4-4.9.3.tar.gz", hash = "sha256:84729e322ad1d5b4d25f805bfa05b902dd96450f43842c4e99067d5e1369eb25"},
{file = "beautifulsoup4-4.10.0-py3-none-any.whl", hash = "sha256:9a315ce70049920ea4572a4055bc4bd700c940521d36fc858205ad4fcde149bf"},
{file = "beautifulsoup4-4.10.0.tar.gz", hash = "sha256:c23ad23c521d818955a4151a67d81580319d4bf548d3d49f4223ae041ff98891"},
]
black = [
{file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"},
@ -1425,8 +1437,8 @@ cffi = [
{file = "cffi-1.14.6.tar.gz", hash = "sha256:c9a875ce9d7fe32887784274dd533c57909b7b1dcadcc128a2ac21331a9765dd"},
]
charset-normalizer = [
{file = "charset-normalizer-2.0.4.tar.gz", hash = "sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3"},
{file = "charset_normalizer-2.0.4-py3-none-any.whl", hash = "sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b"},
{file = "charset-normalizer-2.0.6.tar.gz", hash = "sha256:5ec46d183433dcbd0ab716f2d7f29d8dee50505b3fdb40c6b985c7c4f5a3591f"},
{file = "charset_normalizer-2.0.6-py3-none-any.whl", hash = "sha256:5d209c0a931f215cee683b6445e2d77677e7e75e159f78def0db09d68fafcaa6"},
]
click = [
{file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"},
@ -1495,22 +1507,27 @@ coverage = [
{file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"},
]
cryptography = [
{file = "cryptography-3.4.7-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3d8427734c781ea5f1b41d6589c293089704d4759e34597dce91014ac125aad1"},
{file = "cryptography-3.4.7-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:8e56e16617872b0957d1c9742a3f94b43533447fd78321514abbe7db216aa250"},
{file = "cryptography-3.4.7-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:37340614f8a5d2fb9aeea67fd159bfe4f5f4ed535b1090ce8ec428b2f15a11f2"},
{file = "cryptography-3.4.7-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:240f5c21aef0b73f40bb9f78d2caff73186700bf1bc6b94285699aff98cc16c6"},
{file = "cryptography-3.4.7-cp36-abi3-manylinux2014_x86_64.whl", hash = "sha256:1e056c28420c072c5e3cb36e2b23ee55e260cb04eee08f702e0edfec3fb51959"},
{file = "cryptography-3.4.7-cp36-abi3-win32.whl", hash = "sha256:0f1212a66329c80d68aeeb39b8a16d54ef57071bf22ff4e521657b27372e327d"},
{file = "cryptography-3.4.7-cp36-abi3-win_amd64.whl", hash = "sha256:de4e5f7f68220d92b7637fc99847475b59154b7a1b3868fb7385337af54ac9ca"},
{file = "cryptography-3.4.7-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:26965837447f9c82f1855e0bc8bc4fb910240b6e0d16a664bb722df3b5b06873"},
{file = "cryptography-3.4.7-pp36-pypy36_pp73-manylinux2014_x86_64.whl", hash = "sha256:eb8cc2afe8b05acbd84a43905832ec78e7b3873fb124ca190f574dca7389a87d"},
{file = "cryptography-3.4.7-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:7ec5d3b029f5fa2b179325908b9cd93db28ab7b85bb6c1db56b10e0b54235177"},
{file = "cryptography-3.4.7-pp37-pypy37_pp73-manylinux2014_x86_64.whl", hash = "sha256:ee77aa129f481be46f8d92a1a7db57269a2f23052d5f2433b4621bb457081cc9"},
{file = "cryptography-3.4.7.tar.gz", hash = "sha256:3d10de8116d25649631977cb37da6cbdd2d6fa0e0281d014a5b7d337255ca713"},
{file = "cryptography-3.4.8-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:a00cf305f07b26c351d8d4e1af84ad7501eca8a342dedf24a7acb0e7b7406e14"},
{file = "cryptography-3.4.8-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:f44d141b8c4ea5eb4dbc9b3ad992d45580c1d22bf5e24363f2fbf50c2d7ae8a7"},
{file = "cryptography-3.4.8-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0a7dcbcd3f1913f664aca35d47c1331fce738d44ec34b7be8b9d332151b0b01e"},
{file = "cryptography-3.4.8-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34dae04a0dce5730d8eb7894eab617d8a70d0c97da76b905de9efb7128ad7085"},
{file = "cryptography-3.4.8-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1eb7bb0df6f6f583dd8e054689def236255161ebbcf62b226454ab9ec663746b"},
{file = "cryptography-3.4.8-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:9965c46c674ba8cc572bc09a03f4c649292ee73e1b683adb1ce81e82e9a6a0fb"},
{file = "cryptography-3.4.8-cp36-abi3-win32.whl", hash = "sha256:21ca464b3a4b8d8e86ba0ee5045e103a1fcfac3b39319727bc0fc58c09c6aff7"},
{file = "cryptography-3.4.8-cp36-abi3-win_amd64.whl", hash = "sha256:3520667fda779eb788ea00080124875be18f2d8f0848ec00733c0ec3bb8219fc"},
{file = "cryptography-3.4.8-pp36-pypy36_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d2a6e5ef66503da51d2110edf6c403dc6b494cc0082f85db12f54e9c5d4c3ec5"},
{file = "cryptography-3.4.8-pp36-pypy36_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a305600e7a6b7b855cd798e00278161b681ad6e9b7eca94c721d5f588ab212af"},
{file = "cryptography-3.4.8-pp36-pypy36_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:3fa3a7ccf96e826affdf1a0a9432be74dc73423125c8f96a909e3835a5ef194a"},
{file = "cryptography-3.4.8-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:d9ec0e67a14f9d1d48dd87a2531009a9b251c02ea42851c060b25c782516ff06"},
{file = "cryptography-3.4.8-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5b0fbfae7ff7febdb74b574055c7466da334a5371f253732d7e2e7525d570498"},
{file = "cryptography-3.4.8-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94fff993ee9bc1b2440d3b7243d488c6a3d9724cc2b09cdb297f6a886d040ef7"},
{file = "cryptography-3.4.8-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:8695456444f277af73a4877db9fc979849cd3ee74c198d04fc0776ebc3db52b9"},
{file = "cryptography-3.4.8-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:cd65b60cfe004790c795cc35f272e41a3df4631e2fb6b35aa7ac6ef2859d554e"},
{file = "cryptography-3.4.8.tar.gz", hash = "sha256:94cc5ed4ceaefcbe5bf38c8fba6a21fc1d365bb8fb826ea1688e3370b2e24a1c"},
]
decorator = [
{file = "decorator-5.0.9-py3-none-any.whl", hash = "sha256:6e5c199c16f7a9f0e3a61a4a54b3d27e7dad0dbdde92b944426cb20914376323"},
{file = "decorator-5.0.9.tar.gz", hash = "sha256:72ecfba4320a893c53f9706bebb2d55c270c1e51a28789361aa93e4a21319ed5"},
{file = "decorator-5.1.0-py3-none-any.whl", hash = "sha256:7b12e7c3c6ab203a29e157335e9122cb03de9ab7264b137594103fd4a683b374"},
{file = "decorator-5.1.0.tar.gz", hash = "sha256:e59913af105b9860aa2c8d3272d9de5a56a4e608db9a2f167a8480b323d529a7"},
]
ecdsa = [
{file = "ecdsa-0.17.0-py2.py3-none-any.whl", hash = "sha256:5cf31d5b33743abe0dfc28999036c849a69d548f994b535e527ee3cb7f3ef676"},
@ -1535,55 +1552,56 @@ ghp-import = [
{file = "ghp-import-2.0.1.tar.gz", hash = "sha256:753de2eace6e0f7d4edfb3cce5e3c3b98cd52aadb80163303d1d036bda7b4483"},
]
greenlet = [
{file = "greenlet-1.1.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:60848099b76467ef09b62b0f4512e7e6f0a2c977357a036de602b653667f5f4c"},
{file = "greenlet-1.1.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f42ad188466d946f1b3afc0a9e1a266ac8926461ee0786c06baac6bd71f8a6f3"},
{file = "greenlet-1.1.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:76ed710b4e953fc31c663b079d317c18f40235ba2e3d55f70ff80794f7b57922"},
{file = "greenlet-1.1.0-cp27-cp27m-win32.whl", hash = "sha256:b33b51ab057f8a20b497ffafdb1e79256db0c03ef4f5e3d52e7497200e11f821"},
{file = "greenlet-1.1.0-cp27-cp27m-win_amd64.whl", hash = "sha256:ed1377feed808c9c1139bdb6a61bcbf030c236dd288d6fca71ac26906ab03ba6"},
{file = "greenlet-1.1.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:da862b8f7de577bc421323714f63276acb2f759ab8c5e33335509f0b89e06b8f"},
{file = "greenlet-1.1.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:5f75e7f237428755d00e7460239a2482fa7e3970db56c8935bd60da3f0733e56"},
{file = "greenlet-1.1.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:258f9612aba0d06785143ee1cbf2d7361801c95489c0bd10c69d163ec5254a16"},
{file = "greenlet-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d928e2e3c3906e0a29b43dc26d9b3d6e36921eee276786c4e7ad9ff5665c78a"},
{file = "greenlet-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cc407b68e0a874e7ece60f6639df46309376882152345508be94da608cc0b831"},
{file = "greenlet-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c557c809eeee215b87e8a7cbfb2d783fb5598a78342c29ade561440abae7d22"},
{file = "greenlet-1.1.0-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:3d13da093d44dee7535b91049e44dd2b5540c2a0e15df168404d3dd2626e0ec5"},
{file = "greenlet-1.1.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b3090631fecdf7e983d183d0fad7ea72cfb12fa9212461a9b708ff7907ffff47"},
{file = "greenlet-1.1.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:06ecb43b04480e6bafc45cb1b4b67c785e183ce12c079473359e04a709333b08"},
{file = "greenlet-1.1.0-cp35-cp35m-win32.whl", hash = "sha256:944fbdd540712d5377a8795c840a97ff71e7f3221d3fddc98769a15a87b36131"},
{file = "greenlet-1.1.0-cp35-cp35m-win_amd64.whl", hash = "sha256:c767458511a59f6f597bfb0032a1c82a52c29ae228c2c0a6865cfeaeaac4c5f5"},
{file = "greenlet-1.1.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:2325123ff3a8ecc10ca76f062445efef13b6cf5a23389e2df3c02a4a527b89bc"},
{file = "greenlet-1.1.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:598bcfd841e0b1d88e32e6a5ea48348a2c726461b05ff057c1b8692be9443c6e"},
{file = "greenlet-1.1.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:be9768e56f92d1d7cd94185bab5856f3c5589a50d221c166cc2ad5eb134bd1dc"},
{file = "greenlet-1.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfe7eac0d253915116ed0cd160a15a88981a1d194c1ef151e862a5c7d2f853d3"},
{file = "greenlet-1.1.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a6b035aa2c5fcf3dbbf0e3a8a5bc75286fc2d4e6f9cfa738788b433ec894919"},
{file = "greenlet-1.1.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca1c4a569232c063615f9e70ff9a1e2fee8c66a6fb5caf0f5e8b21a396deec3e"},
{file = "greenlet-1.1.0-cp36-cp36m-win32.whl", hash = "sha256:3096286a6072553b5dbd5efbefc22297e9d06a05ac14ba017233fedaed7584a8"},
{file = "greenlet-1.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c35872b2916ab5a240d52a94314c963476c989814ba9b519bc842e5b61b464bb"},
{file = "greenlet-1.1.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:b97c9a144bbeec7039cca44df117efcbeed7209543f5695201cacf05ba3b5857"},
{file = "greenlet-1.1.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:16183fa53bc1a037c38d75fdc59d6208181fa28024a12a7f64bb0884434c91ea"},
{file = "greenlet-1.1.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6b1d08f2e7f2048d77343279c4d4faa7aef168b3e36039cba1917fffb781a8ed"},
{file = "greenlet-1.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14927b15c953f8f2d2a8dffa224aa78d7759ef95284d4c39e1745cf36e8cdd2c"},
{file = "greenlet-1.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bdcff4b9051fb1aa4bba4fceff6a5f770c6be436408efd99b76fc827f2a9319"},
{file = "greenlet-1.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c70c7dd733a4c56838d1f1781e769081a25fade879510c5b5f0df76956abfa05"},
{file = "greenlet-1.1.0-cp37-cp37m-win32.whl", hash = "sha256:0de64d419b1cb1bfd4ea544bedea4b535ef3ae1e150b0f2609da14bbf48a4a5f"},
{file = "greenlet-1.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:8833e27949ea32d27f7e96930fa29404dd4f2feb13cce483daf52e8842ec246a"},
{file = "greenlet-1.1.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:c1580087ab493c6b43e66f2bdd165d9e3c1e86ef83f6c2c44a29f2869d2c5bd5"},
{file = "greenlet-1.1.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:ad80bb338cf9f8129c049837a42a43451fc7c8b57ad56f8e6d32e7697b115505"},
{file = "greenlet-1.1.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:a9017ff5fc2522e45562882ff481128631bf35da444775bc2776ac5c61d8bcae"},
{file = "greenlet-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7920e3eccd26b7f4c661b746002f5ec5f0928076bd738d38d894bb359ce51927"},
{file = "greenlet-1.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:408071b64e52192869129a205e5b463abda36eff0cebb19d6e63369440e4dc99"},
{file = "greenlet-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be13a18cec649ebaab835dff269e914679ef329204704869f2f167b2c163a9da"},
{file = "greenlet-1.1.0-cp38-cp38-win32.whl", hash = "sha256:22002259e5b7828b05600a762579fa2f8b33373ad95a0ee57b4d6109d0e589ad"},
{file = "greenlet-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:206295d270f702bc27dbdbd7651e8ebe42d319139e0d90217b2074309a200da8"},
{file = "greenlet-1.1.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:096cb0217d1505826ba3d723e8981096f2622cde1eb91af9ed89a17c10aa1f3e"},
{file = "greenlet-1.1.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:03f28a5ea20201e70ab70518d151116ce939b412961c33827519ce620957d44c"},
{file = "greenlet-1.1.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:7db68f15486d412b8e2cfcd584bf3b3a000911d25779d081cbbae76d71bd1a7e"},
{file = "greenlet-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70bd1bb271e9429e2793902dfd194b653221904a07cbf207c3139e2672d17959"},
{file = "greenlet-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f92731609d6625e1cc26ff5757db4d32b6b810d2a3363b0ff94ff573e5901f6f"},
{file = "greenlet-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06d7ac89e6094a0a8f8dc46aa61898e9e1aec79b0f8b47b2400dd51a44dbc832"},
{file = "greenlet-1.1.0-cp39-cp39-win32.whl", hash = "sha256:adb94a28225005890d4cf73648b5131e885c7b4b17bc762779f061844aabcc11"},
{file = "greenlet-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:aa4230234d02e6f32f189fd40b59d5a968fe77e80f59c9c933384fe8ba535535"},
{file = "greenlet-1.1.0.tar.gz", hash = "sha256:c87df8ae3f01ffb4483c796fe1b15232ce2b219f0b18126948616224d3f658ee"},
{file = "greenlet-1.1.1-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:476ba9435afaead4382fbab8f1882f75e3fb2285c35c9285abb3dd30237f9142"},
{file = "greenlet-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:44556302c0ab376e37939fd0058e1f0db2e769580d340fb03b01678d1ff25f68"},
{file = "greenlet-1.1.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:40abb7fec4f6294225d2b5464bb6d9552050ded14a7516588d6f010e7e366dcc"},
{file = "greenlet-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:a11b6199a0b9dc868990456a2667167d0ba096c5224f6258e452bfbe5a9742c5"},
{file = "greenlet-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e22a82d2b416d9227a500c6860cf13e74060cf10e7daf6695cbf4e6a94e0eee4"},
{file = "greenlet-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bad269e442f1b7ffa3fa8820b3c3aa66f02a9f9455b5ba2db5a6f9eea96f56de"},
{file = "greenlet-1.1.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:8ddb38fb6ad96c2ef7468ff73ba5c6876b63b664eebb2c919c224261ae5e8378"},
{file = "greenlet-1.1.1-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:84782c80a433d87530ae3f4b9ed58d4a57317d9918dfcc6a59115fa2d8731f2c"},
{file = "greenlet-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac991947ca6533ada4ce7095f0e28fe25d5b2f3266ad5b983ed4201e61596acf"},
{file = "greenlet-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5317701c7ce167205c0569c10abc4bd01c7f4cf93f642c39f2ce975fa9b78a3c"},
{file = "greenlet-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4870b018ca685ff573edd56b93f00a122f279640732bb52ce3a62b73ee5c4a92"},
{file = "greenlet-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:990e0f5e64bcbc6bdbd03774ecb72496224d13b664aa03afd1f9b171a3269272"},
{file = "greenlet-1.1.1-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:a414f8e14aa7bacfe1578f17c11d977e637d25383b6210587c29210af995ef04"},
{file = "greenlet-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:e02780da03f84a671bb4205c5968c120f18df081236d7b5462b380fd4f0b497b"},
{file = "greenlet-1.1.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:dfcb5a4056e161307d103bc013478892cfd919f1262c2bb8703220adcb986362"},
{file = "greenlet-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:655ab836324a473d4cd8cf231a2d6f283ed71ed77037679da554e38e606a7117"},
{file = "greenlet-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:6ce9d0784c3c79f3e5c5c9c9517bbb6c7e8aa12372a5ea95197b8a99402aa0e6"},
{file = "greenlet-1.1.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:3fc6a447735749d651d8919da49aab03c434a300e9f0af1c886d560405840fd1"},
{file = "greenlet-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8039f5fe8030c43cd1732d9a234fdcbf4916fcc32e21745ca62e75023e4d4649"},
{file = "greenlet-1.1.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fddfb31aa2ac550b938d952bca8a87f1db0f8dc930ffa14ce05b5c08d27e7fd1"},
{file = "greenlet-1.1.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97a807437b81f90f85022a9dcfd527deea38368a3979ccb49d93c9198b2c722"},
{file = "greenlet-1.1.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cf31e894dabb077a35bbe6963285d4515a387ff657bd25b0530c7168e48f167f"},
{file = "greenlet-1.1.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4eae94de9924bbb4d24960185363e614b1b62ff797c23dc3c8a7c75bbb8d187e"},
{file = "greenlet-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:c1862f9f1031b1dee3ff00f1027fcd098ffc82120f43041fe67804b464bbd8a7"},
{file = "greenlet-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:9b02e6039eafd75e029d8c58b7b1f3e450ca563ef1fe21c7e3e40b9936c8d03e"},
{file = "greenlet-1.1.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:84488516639c3c5e5c0e52f311fff94ebc45b56788c2a3bfe9cf8e75670f4de3"},
{file = "greenlet-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:3f8fc59bc5d64fa41f58b0029794f474223693fd00016b29f4e176b3ee2cfd9f"},
{file = "greenlet-1.1.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:3e594015a2349ec6dcceda9aca29da8dc89e85b56825b7d1f138a3f6bb79dd4c"},
{file = "greenlet-1.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e41f72f225192d5d4df81dad2974a8943b0f2d664a2a5cfccdf5a01506f5523c"},
{file = "greenlet-1.1.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:75ff270fd05125dce3303e9216ccddc541a9e072d4fc764a9276d44dee87242b"},
{file = "greenlet-1.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cde7ee190196cbdc078511f4df0be367af85636b84d8be32230f4871b960687"},
{file = "greenlet-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:f253dad38605486a4590f9368ecbace95865fea0f2b66615d121ac91fd1a1563"},
{file = "greenlet-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a91ee268f059583176c2c8b012a9fce7e49ca6b333a12bbc2dd01fc1a9783885"},
{file = "greenlet-1.1.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:34e6675167a238bede724ee60fe0550709e95adaff6a36bcc97006c365290384"},
{file = "greenlet-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bf3725d79b1ceb19e83fb1aed44095518c0fcff88fba06a76c0891cfd1f36837"},
{file = "greenlet-1.1.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:5c3b735ccf8fc8048664ee415f8af5a3a018cc92010a0d7195395059b4b39b7d"},
{file = "greenlet-1.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2002a59453858c7f3404690ae80f10c924a39f45f6095f18a985a1234c37334"},
{file = "greenlet-1.1.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04e1849c88aa56584d4a0a6e36af5ec7cc37993fdc1fda72b56aa1394a92ded3"},
{file = "greenlet-1.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8d4ed48eed7414ccb2aaaecbc733ed2a84c299714eae3f0f48db085342d5629"},
{file = "greenlet-1.1.1-cp38-cp38-win32.whl", hash = "sha256:2f89d74b4f423e756a018832cd7a0a571e0a31b9ca59323b77ce5f15a437629b"},
{file = "greenlet-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:d15cb6f8706678dc47fb4e4f8b339937b04eda48a0af1cca95f180db552e7663"},
{file = "greenlet-1.1.1-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:b050dbb96216db273b56f0e5960959c2b4cb679fe1e58a0c3906fa0a60c00662"},
{file = "greenlet-1.1.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:6e0696525500bc8aa12eae654095d2260db4dc95d5c35af2b486eae1bf914ccd"},
{file = "greenlet-1.1.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:07e6d88242e09b399682b39f8dfa1e7e6eca66b305de1ff74ed9eb1a7d8e539c"},
{file = "greenlet-1.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98b491976ed656be9445b79bc57ed21decf08a01aaaf5fdabf07c98c108111f6"},
{file = "greenlet-1.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e72db813c28906cdc59bd0da7c325d9b82aa0b0543014059c34c8c4ad20e16"},
{file = "greenlet-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:090126004c8ab9cd0787e2acf63d79e80ab41a18f57d6448225bbfcba475034f"},
{file = "greenlet-1.1.1-cp39-cp39-win32.whl", hash = "sha256:1796f2c283faab2b71c67e9b9aefb3f201fdfbee5cb55001f5ffce9125f63a45"},
{file = "greenlet-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:4adaf53ace289ced90797d92d767d37e7cdc29f13bd3830c3f0a561277a4ae83"},
{file = "greenlet-1.1.1.tar.gz", hash = "sha256:c0f22774cd8294078bdf7392ac73cf00bfa1e5e0ed644bd064fdabc5f2a2f481"},
]
gunicorn = [
{file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"},
@ -1623,8 +1641,8 @@ idna = [
{file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"},
]
importlib-metadata = [
{file = "importlib_metadata-4.6.3-py3-none-any.whl", hash = "sha256:51c6635429c77cf1ae634c997ff9e53ca3438b495f10a55ba28594dd69764a8b"},
{file = "importlib_metadata-4.6.3.tar.gz", hash = "sha256:0645585859e9a6689c523927a5032f2ba5919f1f7d0e84bd4533312320de1ff9"},
{file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"},
{file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"},
]
iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
@ -1764,12 +1782,12 @@ mkdocs = [
{file = "mkdocs-1.2.2.tar.gz", hash = "sha256:a334f5bd98ec960638511366eb8c5abc9c99b9083a0ed2401d8791b112d6b078"},
]
mkdocs-material = [
{file = "mkdocs-material-7.2.2.tar.gz", hash = "sha256:4f501e139e2f8546653e7d8777c9b97ca639d03d8c86345a60609864cc5bbb03"},
{file = "mkdocs_material-7.2.2-py2.py3-none-any.whl", hash = "sha256:76de22213f0e0319b9bddf1bfa86530e93efb4a604e9ddf8f8419f0438572523"},
{file = "mkdocs-material-7.2.7.tar.gz", hash = "sha256:2ce49ece52fc92a9728ea4a3aea941744bfe23442814a1e1db93368f858aee30"},
{file = "mkdocs_material-7.2.7-py2.py3-none-any.whl", hash = "sha256:4be317aa17829746d9e36150207c5c6311fb82042dbd0a56ae2a3301a351fda1"},
]
mkdocs-material-extensions = [
{file = "mkdocs-material-extensions-1.0.1.tar.gz", hash = "sha256:6947fb7f5e4291e3c61405bad3539d81e0b3cd62ae0d66ced018128af509c68f"},
{file = "mkdocs_material_extensions-1.0.1-py3-none-any.whl", hash = "sha256:d90c807a88348aa6d1805657ec5c0b2d8d609c110e62b9dce4daf7fa981fa338"},
{file = "mkdocs-material-extensions-1.0.3.tar.gz", hash = "sha256:bfd24dfdef7b41c312ede42648f9eb83476ea168ec163b613f9abd12bbfddba2"},
{file = "mkdocs_material_extensions-1.0.3-py3-none-any.whl", hash = "sha256:a82b70e533ce060b2a5d9eb2bc2e1be201cf61f901f93704b4acf6e3d5983a44"},
]
mypy-extensions = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
@ -1796,49 +1814,67 @@ pathvalidate = [
{file = "pathvalidate-2.4.1.tar.gz", hash = "sha256:3c9bd94c7ec23e9cfb211ffbe356ae75f979d6c099a2c745ee9490f524f32468"},
]
pillow = [
{file = "Pillow-8.3.1-1-cp36-cp36m-win_amd64.whl", hash = "sha256:fd7eef578f5b2200d066db1b50c4aa66410786201669fb76d5238b007918fb24"},
{file = "Pillow-8.3.1-1-cp37-cp37m-win_amd64.whl", hash = "sha256:75e09042a3b39e0ea61ce37e941221313d51a9c26b8e54e12b3ececccb71718a"},
{file = "Pillow-8.3.1-1-cp38-cp38-win_amd64.whl", hash = "sha256:c0e0550a404c69aab1e04ae89cca3e2a042b56ab043f7f729d984bf73ed2a093"},
{file = "Pillow-8.3.1-1-cp39-cp39-win_amd64.whl", hash = "sha256:479ab11cbd69612acefa8286481f65c5dece2002ffaa4f9db62682379ca3bb77"},
{file = "Pillow-8.3.1-1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:f156d6ecfc747ee111c167f8faf5f4953761b5e66e91a4e6767e548d0f80129c"},
{file = "Pillow-8.3.1-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:196560dba4da7a72c5e7085fccc5938ab4075fd37fe8b5468869724109812edd"},
{file = "Pillow-8.3.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c9569049d04aaacd690573a0398dbd8e0bf0255684fee512b413c2142ab723"},
{file = "Pillow-8.3.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c088a000dfdd88c184cc7271bfac8c5b82d9efa8637cd2b68183771e3cf56f04"},
{file = "Pillow-8.3.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fc214a6b75d2e0ea7745488da7da3c381f41790812988c7a92345978414fad37"},
{file = "Pillow-8.3.1-cp36-cp36m-win32.whl", hash = "sha256:a17ca41f45cf78c2216ebfab03add7cc350c305c38ff34ef4eef66b7d76c5229"},
{file = "Pillow-8.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:67b3666b544b953a2777cb3f5a922e991be73ab32635666ee72e05876b8a92de"},
{file = "Pillow-8.3.1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:ff04c373477723430dce2e9d024c708a047d44cf17166bf16e604b379bf0ca14"},
{file = "Pillow-8.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9364c81b252d8348e9cc0cb63e856b8f7c1b340caba6ee7a7a65c968312f7dab"},
{file = "Pillow-8.3.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a2f381932dca2cf775811a008aa3027671ace723b7a38838045b1aee8669fdcf"},
{file = "Pillow-8.3.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d0da39795049a9afcaadec532e7b669b5ebbb2a9134576ebcc15dd5bdae33cc0"},
{file = "Pillow-8.3.1-cp37-cp37m-win32.whl", hash = "sha256:2b6dfa068a8b6137da34a4936f5a816aba0ecc967af2feeb32c4393ddd671cba"},
{file = "Pillow-8.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a4eef1ff2d62676deabf076f963eda4da34b51bc0517c70239fafed1d5b51500"},
{file = "Pillow-8.3.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:660a87085925c61a0dcc80efb967512ac34dbb256ff7dd2b9b4ee8dbdab58cf4"},
{file = "Pillow-8.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:15a2808e269a1cf2131930183dcc0419bc77bb73eb54285dde2706ac9939fa8e"},
{file = "Pillow-8.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:969cc558cca859cadf24f890fc009e1bce7d7d0386ba7c0478641a60199adf79"},
{file = "Pillow-8.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2ee77c14a0299d0541d26f3d8500bb57e081233e3fa915fa35abd02c51fa7fae"},
{file = "Pillow-8.3.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:c11003197f908878164f0e6da15fce22373ac3fc320cda8c9d16e6bba105b844"},
{file = "Pillow-8.3.1-cp38-cp38-win32.whl", hash = "sha256:3f08bd8d785204149b5b33e3b5f0ebbfe2190ea58d1a051c578e29e39bfd2367"},
{file = "Pillow-8.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:70af7d222df0ff81a2da601fab42decb009dc721545ed78549cb96e3a1c5f0c8"},
{file = "Pillow-8.3.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:37730f6e68bdc6a3f02d2079c34c532330d206429f3cee651aab6b66839a9f0e"},
{file = "Pillow-8.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4bc3c7ef940eeb200ca65bd83005eb3aae8083d47e8fcbf5f0943baa50726856"},
{file = "Pillow-8.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c35d09db702f4185ba22bb33ef1751ad49c266534339a5cebeb5159d364f6f82"},
{file = "Pillow-8.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b2efa07f69dc395d95bb9ef3299f4ca29bcb2157dc615bae0b42c3c20668ffc"},
{file = "Pillow-8.3.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cc866706d56bd3a7dbf8bac8660c6f6462f2f2b8a49add2ba617bc0c54473d83"},
{file = "Pillow-8.3.1-cp39-cp39-win32.whl", hash = "sha256:9a211b663cf2314edbdb4cf897beeb5c9ee3810d1d53f0e423f06d6ebbf9cd5d"},
{file = "Pillow-8.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:c2a5ff58751670292b406b9f06e07ed1446a4b13ffced6b6cab75b857485cbc8"},
{file = "Pillow-8.3.1-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c379425c2707078dfb6bfad2430728831d399dc95a7deeb92015eb4c92345eaf"},
{file = "Pillow-8.3.1-pp36-pypy36_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:114f816e4f73f9ec06997b2fde81a92cbf0777c9e8f462005550eed6bae57e63"},
{file = "Pillow-8.3.1-pp36-pypy36_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8960a8a9f4598974e4c2aeb1bff9bdd5db03ee65fd1fce8adf3223721aa2a636"},
{file = "Pillow-8.3.1-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:147bd9e71fb9dcf08357b4d530b5167941e222a6fd21f869c7911bac40b9994d"},
{file = "Pillow-8.3.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1fd5066cd343b5db88c048d971994e56b296868766e461b82fa4e22498f34d77"},
{file = "Pillow-8.3.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f4ebde71785f8bceb39dcd1e7f06bcc5d5c3cf48b9f69ab52636309387b097c8"},
{file = "Pillow-8.3.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:1c03e24be975e2afe70dfc5da6f187eea0b49a68bb2b69db0f30a61b7031cee4"},
{file = "Pillow-8.3.1.tar.gz", hash = "sha256:2cac53839bfc5cece8fdbe7f084d5e3ee61e1303cccc86511d351adcb9e2c792"},
{file = "Pillow-8.3.2-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:c691b26283c3a31594683217d746f1dad59a7ae1d4cfc24626d7a064a11197d4"},
{file = "Pillow-8.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f514c2717012859ccb349c97862568fdc0479aad85b0270d6b5a6509dbc142e2"},
{file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be25cb93442c6d2f8702c599b51184bd3ccd83adebd08886b682173e09ef0c3f"},
{file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d675a876b295afa114ca8bf42d7f86b5fb1298e1b6bb9a24405a3f6c8338811c"},
{file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59697568a0455764a094585b2551fd76bfd6b959c9f92d4bdec9d0e14616303a"},
{file = "Pillow-8.3.2-cp310-cp310-win32.whl", hash = "sha256:2d5e9dc0bf1b5d9048a94c48d0813b6c96fccfa4ccf276d9c36308840f40c228"},
{file = "Pillow-8.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:11c27e74bab423eb3c9232d97553111cc0be81b74b47165f07ebfdd29d825875"},
{file = "Pillow-8.3.2-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:11eb7f98165d56042545c9e6db3ce394ed8b45089a67124298f0473b29cb60b2"},
{file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f23b2d3079522fdf3c09de6517f625f7a964f916c956527bed805ac043799b8"},
{file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19ec4cfe4b961edc249b0e04b5618666c23a83bc35842dea2bfd5dfa0157f81b"},
{file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5a31c07cea5edbaeb4bdba6f2b87db7d3dc0f446f379d907e51cc70ea375629"},
{file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15ccb81a6ffc57ea0137f9f3ac2737ffa1d11f786244d719639df17476d399a7"},
{file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:8f284dc1695caf71a74f24993b7c7473d77bc760be45f776a2c2f4e04c170550"},
{file = "Pillow-8.3.2-cp36-cp36m-win32.whl", hash = "sha256:4abc247b31a98f29e5224f2d31ef15f86a71f79c7f4d2ac345a5d551d6393073"},
{file = "Pillow-8.3.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a048dad5ed6ad1fad338c02c609b862dfaa921fcd065d747194a6805f91f2196"},
{file = "Pillow-8.3.2-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:06d1adaa284696785375fa80a6a8eb309be722cf4ef8949518beb34487a3df71"},
{file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd24054aaf21e70a51e2a2a5ed1183560d3a69e6f9594a4bfe360a46f94eba83"},
{file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27a330bf7014ee034046db43ccbb05c766aa9e70b8d6c5260bfc38d73103b0ba"},
{file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13654b521fb98abdecec105ea3fb5ba863d1548c9b58831dd5105bb3873569f1"},
{file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a1bd983c565f92779be456ece2479840ec39d386007cd4ae83382646293d681b"},
{file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4326ea1e2722f3dc00ed77c36d3b5354b8fb7399fb59230249ea6d59cbed90da"},
{file = "Pillow-8.3.2-cp37-cp37m-win32.whl", hash = "sha256:085a90a99404b859a4b6c3daa42afde17cb3ad3115e44a75f0d7b4a32f06a6c9"},
{file = "Pillow-8.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:18a07a683805d32826c09acfce44a90bf474e6a66ce482b1c7fcd3757d588df3"},
{file = "Pillow-8.3.2-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4e59e99fd680e2b8b11bbd463f3c9450ab799305d5f2bafb74fefba6ac058616"},
{file = "Pillow-8.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4d89a2e9219a526401015153c0e9dd48319ea6ab9fe3b066a20aa9aee23d9fd3"},
{file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56fd98c8294f57636084f4b076b75f86c57b2a63a8410c0cd172bc93695ee979"},
{file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b11c9d310a3522b0fd3c35667914271f570576a0e387701f370eb39d45f08a4"},
{file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0412516dcc9de9b0a1e0ae25a280015809de8270f134cc2c1e32c4eeb397cf30"},
{file = "Pillow-8.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bcb04ff12e79b28be6c9988f275e7ab69f01cc2ba319fb3114f87817bb7c74b6"},
{file = "Pillow-8.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0b9911ec70731711c3b6ebcde26caea620cbdd9dcb73c67b0730c8817f24711b"},
{file = "Pillow-8.3.2-cp38-cp38-win32.whl", hash = "sha256:ce2e5e04bb86da6187f96d7bab3f93a7877830981b37f0287dd6479e27a10341"},
{file = "Pillow-8.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:35d27687f027ad25a8d0ef45dd5208ef044c588003cdcedf05afb00dbc5c2deb"},
{file = "Pillow-8.3.2-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:04835e68ef12904bc3e1fd002b33eea0779320d4346082bd5b24bec12ad9c3e9"},
{file = "Pillow-8.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:10e00f7336780ca7d3653cf3ac26f068fa11b5a96894ea29a64d3dc4b810d630"},
{file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cde7a4d3687f21cffdf5bb171172070bb95e02af448c4c8b2f223d783214056"},
{file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c3ff00110835bdda2b1e2b07f4a2548a39744bb7de5946dc8e95517c4fb2ca6"},
{file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35d409030bf3bd05fa66fb5fdedc39c521b397f61ad04309c90444e893d05f7d"},
{file = "Pillow-8.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bff50ba9891be0a004ef48828e012babaaf7da204d81ab9be37480b9020a82b"},
{file = "Pillow-8.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7dbfbc0020aa1d9bc1b0b8bcf255a7d73f4ad0336f8fd2533fcc54a4ccfb9441"},
{file = "Pillow-8.3.2-cp39-cp39-win32.whl", hash = "sha256:963ebdc5365d748185fdb06daf2ac758116deecb2277ec5ae98139f93844bc09"},
{file = "Pillow-8.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:cc9d0dec711c914ed500f1d0d3822868760954dce98dfb0b7382a854aee55d19"},
{file = "Pillow-8.3.2-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:2c661542c6f71dfd9dc82d9d29a8386287e82813b0375b3a02983feac69ef864"},
{file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:548794f99ff52a73a156771a0402f5e1c35285bd981046a502d7e4793e8facaa"},
{file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8b68f565a4175e12e68ca900af8910e8fe48aaa48fd3ca853494f384e11c8bcd"},
{file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:838eb85de6d9307c19c655c726f8d13b8b646f144ca6b3771fa62b711ebf7624"},
{file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:feb5db446e96bfecfec078b943cc07744cc759893cef045aa8b8b6d6aaa8274e"},
{file = "Pillow-8.3.2-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:fc0db32f7223b094964e71729c0361f93db43664dd1ec86d3df217853cedda87"},
{file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fd4fd83aa912d7b89b4b4a1580d30e2a4242f3936882a3f433586e5ab97ed0d5"},
{file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d0c8ebbfd439c37624db98f3877d9ed12c137cadd99dde2d2eae0dab0bbfc355"},
{file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6cb3dd7f23b044b0737317f892d399f9e2f0b3a02b22b2c692851fb8120d82c6"},
{file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a66566f8a22561fc1a88dc87606c69b84fa9ce724f99522cf922c801ec68f5c1"},
{file = "Pillow-8.3.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ce651ca46d0202c302a535d3047c55a0131a720cf554a578fc1b8a2aff0e7d96"},
{file = "Pillow-8.3.2.tar.gz", hash = "sha256:dde3f3ed8d00c72631bc19cbfff8ad3b6215062a5eed402381ad365f82f0c18c"},
]
platformdirs = [
{file = "platformdirs-2.3.0-py3-none-any.whl", hash = "sha256:8003ac87717ae2c7ee1ea5a84a1a61e87f3fbd16eb5aadba194ea30a9019f648"},
{file = "platformdirs-2.3.0.tar.gz", hash = "sha256:15b056538719b1c94bdaccb29e5f81879c7f7f0f4a153f46086d155dffcd4f0f"},
]
pluggy = [
{file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
{file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
]
psycopg2-binary = [
{file = "psycopg2-binary-2.9.1.tar.gz", hash = "sha256:b0221ca5a9837e040ebf61f48899926b5783668b7807419e4adae8175a31f773"},
@ -1932,16 +1968,16 @@ pyflakes = [
{file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"},
]
pygments = [
{file = "Pygments-2.9.0-py3-none-any.whl", hash = "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e"},
{file = "Pygments-2.9.0.tar.gz", hash = "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f"},
{file = "Pygments-2.10.0-py3-none-any.whl", hash = "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380"},
{file = "Pygments-2.10.0.tar.gz", hash = "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"},
]
pyhumps = [
{file = "pyhumps-3.0.2-py3-none-any.whl", hash = "sha256:367b1aadcaa64f8196a3cd14f56559a5602950aeb8486f49318e7394f5e18052"},
{file = "pyhumps-3.0.2.tar.gz", hash = "sha256:042b4b6eec6c1f862f8310c0eebbae19293e9edab8cafb030ff78c890ef1aa34"},
]
pylint = [
{file = "pylint-2.9.6-py3-none-any.whl", hash = "sha256:2e1a0eb2e8ab41d6b5dbada87f066492bb1557b12b76c47c2ee8aa8a11186594"},
{file = "pylint-2.9.6.tar.gz", hash = "sha256:8b838c8983ee1904b2de66cce9d0b96649a91901350e956d78f289c3bc87b48e"},
{file = "pylint-2.11.1-py3-none-any.whl", hash = "sha256:0f358e221c45cbd4dad2a1e4b883e75d28acdcccd29d40c76eb72b307269b126"},
{file = "pylint-2.11.1.tar.gz", hash = "sha256:2c9843fff1a88ca0ad98a256806c82c5a8f86086e7ccbdb93297d86c3f90c436"},
]
pymdown-extensions = [
{file = "pymdown-extensions-8.2.tar.gz", hash = "sha256:b6daa94aad9e1310f9c64c8b1f01e4ce82937ab7eb53bfc92876a97aca02a6f4"},
@ -1956,8 +1992,8 @@ pyrdfa3 = [
{file = "pyRdfa3-3.5.3.tar.gz", hash = "sha256:157663a92b87df345b6f69bde235dff5f797891608e12fe1e4fa8dad687131ae"},
]
pytest = [
{file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"},
{file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"},
{file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"},
{file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"},
]
pytest-cov = [
{file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"},
@ -2013,54 +2049,59 @@ pyyaml-env-tag = [
{file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"},
]
rdflib = [
{file = "rdflib-6.0.0-py3-none-any.whl", hash = "sha256:bb24f0058070d5843503e15b37c597bc3858d328d11acd9476efad3aa62f555d"},
{file = "rdflib-6.0.0.tar.gz", hash = "sha256:7ce4d757eb26f4dd43205ec340d8c097f29e5adfe45d6ea20238c731dc679879"},
{file = "rdflib-6.0.1-py3-none-any.whl", hash = "sha256:a775069ab1c3d38b7e04666603666fb8a31937a4671a5afc91ca136109f8047a"},
{file = "rdflib-6.0.1.tar.gz", hash = "sha256:f071caff0b68634e4a7bd1d66ea3416ac98f1cc3b915938147ea899c32608728"},
]
rdflib-jsonld = [
{file = "rdflib-jsonld-0.5.0.tar.gz", hash = "sha256:4f7d55326405071c7bce9acf5484643bcb984eadb84a6503053367da207105ed"},
{file = "rdflib-jsonld-0.6.2.tar.gz", hash = "sha256:107cd3019d41354c31687e64af5e3fd3c3e3fa5052ce635f5ce595fd31853a63"},
{file = "rdflib_jsonld-0.6.2-py2.py3-none-any.whl", hash = "sha256:011afe67672353ca9978ab9a4bee964dff91f14042f2d8a28c22a573779d2f8b"},
]
recipe-scrapers = [
{file = "recipe_scrapers-13.3.5-py3-none-any.whl", hash = "sha256:98ef4fd27ad70c3026aff255a4081f7bc072d9f2c3590753b1f11a66e42202e6"},
{file = "recipe_scrapers-13.3.5.tar.gz", hash = "sha256:02db921b596ea7ea1ceb552ee10670fd307315b1b196c35ea133da49d71d41c8"},
{file = "recipe_scrapers-13.4.0-py3-none-any.whl", hash = "sha256:f38a0c6c0e6394ce3062e715ac43a6d1b40c0d7930d0ec92849a154f7b0501ad"},
{file = "recipe_scrapers-13.4.0.tar.gz", hash = "sha256:40296e0e77cb45018ada196dc1d3d58a8fc37e52361ba11b0b0ab286940406c0"},
]
regex = [
{file = "regex-2021.7.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:6afe6a627888c9a6cfbb603d1d017ce204cebd589d66e0703309b8048c3b0854"},
{file = "regex-2021.7.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ccb3d2190476d00414aab36cca453e4596e8f70a206e2aa8db3d495a109153d2"},
{file = "regex-2021.7.6-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:ed693137a9187052fc46eedfafdcb74e09917166362af4cc4fddc3b31560e93d"},
{file = "regex-2021.7.6-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:99d8ab206a5270c1002bfcf25c51bf329ca951e5a169f3b43214fdda1f0b5f0d"},
{file = "regex-2021.7.6-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:b85ac458354165405c8a84725de7bbd07b00d9f72c31a60ffbf96bb38d3e25fa"},
{file = "regex-2021.7.6-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:3f5716923d3d0bfb27048242a6e0f14eecdb2e2a7fac47eda1d055288595f222"},
{file = "regex-2021.7.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5983c19d0beb6af88cb4d47afb92d96751fb3fa1784d8785b1cdf14c6519407"},
{file = "regex-2021.7.6-cp36-cp36m-win32.whl", hash = "sha256:c92831dac113a6e0ab28bc98f33781383fe294df1a2c3dfd1e850114da35fd5b"},
{file = "regex-2021.7.6-cp36-cp36m-win_amd64.whl", hash = "sha256:791aa1b300e5b6e5d597c37c346fb4d66422178566bbb426dd87eaae475053fb"},
{file = "regex-2021.7.6-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:564a4c8a29435d1f2256ba247a0315325ea63335508ad8ed938a4f14c4116a5d"},
{file = "regex-2021.7.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:59c00bb8dd8775473cbfb967925ad2c3ecc8886b3b2d0c90a8e2707e06c743f0"},
{file = "regex-2021.7.6-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:9a854b916806c7e3b40e6616ac9e85d3cdb7649d9e6590653deb5b341a736cec"},
{file = "regex-2021.7.6-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:db2b7df831c3187a37f3bb80ec095f249fa276dbe09abd3d35297fc250385694"},
{file = "regex-2021.7.6-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:173bc44ff95bc1e96398c38f3629d86fa72e539c79900283afa895694229fe6a"},
{file = "regex-2021.7.6-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:15dddb19823f5147e7517bb12635b3c82e6f2a3a6b696cc3e321522e8b9308ad"},
{file = "regex-2021.7.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ddeabc7652024803666ea09f32dd1ed40a0579b6fbb2a213eba590683025895"},
{file = "regex-2021.7.6-cp37-cp37m-win32.whl", hash = "sha256:f080248b3e029d052bf74a897b9d74cfb7643537fbde97fe8225a6467fb559b5"},
{file = "regex-2021.7.6-cp37-cp37m-win_amd64.whl", hash = "sha256:d8bbce0c96462dbceaa7ac4a7dfbbee92745b801b24bce10a98d2f2b1ea9432f"},
{file = "regex-2021.7.6-cp38-cp38-manylinux1_i686.whl", hash = "sha256:422dec1e7cbb2efbbe50e3f1de36b82906def93ed48da12d1714cabcd993d7f0"},
{file = "regex-2021.7.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cbe23b323988a04c3e5b0c387fe3f8f363bf06c0680daf775875d979e376bd26"},
{file = "regex-2021.7.6-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:0eb2c6e0fcec5e0f1d3bcc1133556563222a2ffd2211945d7b1480c1b1a42a6f"},
{file = "regex-2021.7.6-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:1c78780bf46d620ff4fff40728f98b8afd8b8e35c3efd638c7df67be2d5cddbf"},
{file = "regex-2021.7.6-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:bc84fb254a875a9f66616ed4538542fb7965db6356f3df571d783f7c8d256edd"},
{file = "regex-2021.7.6-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:598c0a79b4b851b922f504f9f39a863d83ebdfff787261a5ed061c21e67dd761"},
{file = "regex-2021.7.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:875c355360d0f8d3d827e462b29ea7682bf52327d500a4f837e934e9e4656068"},
{file = "regex-2021.7.6-cp38-cp38-win32.whl", hash = "sha256:e586f448df2bbc37dfadccdb7ccd125c62b4348cb90c10840d695592aa1b29e0"},
{file = "regex-2021.7.6-cp38-cp38-win_amd64.whl", hash = "sha256:2fe5e71e11a54e3355fa272137d521a40aace5d937d08b494bed4529964c19c4"},
{file = "regex-2021.7.6-cp39-cp39-manylinux1_i686.whl", hash = "sha256:4f64fc59fd5b10557f6cd0937e1597af022ad9b27d454e182485f1db3008f417"},
{file = "regex-2021.7.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:89e5528803566af4df368df2d6f503c84fbfb8249e6631c7b025fe23e6bd0cde"},
{file = "regex-2021.7.6-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2366fe0479ca0e9afa534174faa2beae87847d208d457d200183f28c74eaea59"},
{file = "regex-2021.7.6-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f9392a4555f3e4cb45310a65b403d86b589adc773898c25a39184b1ba4db8985"},
{file = "regex-2021.7.6-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:2bceeb491b38225b1fee4517107b8491ba54fba77cf22a12e996d96a3c55613d"},
{file = "regex-2021.7.6-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:f98dc35ab9a749276f1a4a38ab3e0e2ba1662ce710f6530f5b0a6656f1c32b58"},
{file = "regex-2021.7.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:319eb2a8d0888fa6f1d9177705f341bc9455a2c8aca130016e52c7fe8d6c37a3"},
{file = "regex-2021.7.6-cp39-cp39-win32.whl", hash = "sha256:eaf58b9e30e0e546cdc3ac06cf9165a1ca5b3de8221e9df679416ca667972035"},
{file = "regex-2021.7.6-cp39-cp39-win_amd64.whl", hash = "sha256:4c9c3155fe74269f61e27617529b7f09552fbb12e44b1189cebbdb24294e6e1c"},
{file = "regex-2021.7.6.tar.gz", hash = "sha256:8394e266005f2d8c6f0bc6780001f7afa3ef81a7a2111fa35058ded6fce79e4d"},
{file = "regex-2021.8.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9d05ad5367c90814099000442b2125535e9d77581855b9bee8780f1b41f2b1a2"},
{file = "regex-2021.8.28-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3bf1bc02bc421047bfec3343729c4bbbea42605bcfd6d6bfe2c07ade8b12d2a"},
{file = "regex-2021.8.28-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f6a808044faae658f546dd5f525e921de9fa409de7a5570865467f03a626fc0"},
{file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a617593aeacc7a691cc4af4a4410031654f2909053bd8c8e7db837f179a630eb"},
{file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79aef6b5cd41feff359acaf98e040844613ff5298d0d19c455b3d9ae0bc8c35a"},
{file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0fc1f8f06977c2d4f5e3d3f0d4a08089be783973fc6b6e278bde01f0544ff308"},
{file = "regex-2021.8.28-cp310-cp310-win32.whl", hash = "sha256:6eebf512aa90751d5ef6a7c2ac9d60113f32e86e5687326a50d7686e309f66ed"},
{file = "regex-2021.8.28-cp310-cp310-win_amd64.whl", hash = "sha256:ac88856a8cbccfc14f1b2d0b829af354cc1743cb375e7f04251ae73b2af6adf8"},
{file = "regex-2021.8.28-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c206587c83e795d417ed3adc8453a791f6d36b67c81416676cad053b4104152c"},
{file = "regex-2021.8.28-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8690ed94481f219a7a967c118abaf71ccc440f69acd583cab721b90eeedb77c"},
{file = "regex-2021.8.28-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:328a1fad67445550b982caa2a2a850da5989fd6595e858f02d04636e7f8b0b13"},
{file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c7cb4c512d2d3b0870e00fbbac2f291d4b4bf2634d59a31176a87afe2777c6f0"},
{file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66256b6391c057305e5ae9209941ef63c33a476b73772ca967d4a2df70520ec1"},
{file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8e44769068d33e0ea6ccdf4b84d80c5afffe5207aa4d1881a629cf0ef3ec398f"},
{file = "regex-2021.8.28-cp36-cp36m-win32.whl", hash = "sha256:08d74bfaa4c7731b8dac0a992c63673a2782758f7cfad34cf9c1b9184f911354"},
{file = "regex-2021.8.28-cp36-cp36m-win_amd64.whl", hash = "sha256:abb48494d88e8a82601af905143e0de838c776c1241d92021e9256d5515b3645"},
{file = "regex-2021.8.28-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b4c220a1fe0d2c622493b0a1fd48f8f991998fb447d3cd368033a4b86cf1127a"},
{file = "regex-2021.8.28-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4a332404baa6665b54e5d283b4262f41f2103c255897084ec8f5487ce7b9e8e"},
{file = "regex-2021.8.28-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c61dcc1cf9fd165127a2853e2c31eb4fb961a4f26b394ac9fe5669c7a6592892"},
{file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ee329d0387b5b41a5dddbb6243a21cb7896587a651bebb957e2d2bb8b63c0791"},
{file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f60667673ff9c249709160529ab39667d1ae9fd38634e006bec95611f632e759"},
{file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b844fb09bd9936ed158ff9df0ab601e2045b316b17aa8b931857365ea8586906"},
{file = "regex-2021.8.28-cp37-cp37m-win32.whl", hash = "sha256:4cde065ab33bcaab774d84096fae266d9301d1a2f5519d7bd58fc55274afbf7a"},
{file = "regex-2021.8.28-cp37-cp37m-win_amd64.whl", hash = "sha256:1413b5022ed6ac0d504ba425ef02549a57d0f4276de58e3ab7e82437892704fc"},
{file = "regex-2021.8.28-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ed4b50355b066796dacdd1cf538f2ce57275d001838f9b132fab80b75e8c84dd"},
{file = "regex-2021.8.28-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28fc475f560d8f67cc8767b94db4c9440210f6958495aeae70fac8faec631797"},
{file = "regex-2021.8.28-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdc178caebd0f338d57ae445ef8e9b737ddf8fbc3ea187603f65aec5b041248f"},
{file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:999ad08220467b6ad4bd3dd34e65329dd5d0df9b31e47106105e407954965256"},
{file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:808ee5834e06f57978da3e003ad9d6292de69d2bf6263662a1a8ae30788e080b"},
{file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d5111d4c843d80202e62b4fdbb4920db1dcee4f9366d6b03294f45ed7b18b42e"},
{file = "regex-2021.8.28-cp38-cp38-win32.whl", hash = "sha256:473858730ef6d6ff7f7d5f19452184cd0caa062a20047f6d6f3e135a4648865d"},
{file = "regex-2021.8.28-cp38-cp38-win_amd64.whl", hash = "sha256:31a99a4796bf5aefc8351e98507b09e1b09115574f7c9dbb9cf2111f7220d2e2"},
{file = "regex-2021.8.28-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:04f6b9749e335bb0d2f68c707f23bb1773c3fb6ecd10edf0f04df12a8920d468"},
{file = "regex-2021.8.28-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b006628fe43aa69259ec04ca258d88ed19b64791693df59c422b607b6ece8bb"},
{file = "regex-2021.8.28-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:121f4b3185feaade3f85f70294aef3f777199e9b5c0c0245c774ae884b110a2d"},
{file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a577a21de2ef8059b58f79ff76a4da81c45a75fe0bfb09bc8b7bb4293fa18983"},
{file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1743345e30917e8c574f273f51679c294effba6ad372db1967852f12c76759d8"},
{file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e1e8406b895aba6caa63d9fd1b6b1700d7e4825f78ccb1e5260551d168db38ed"},
{file = "regex-2021.8.28-cp39-cp39-win32.whl", hash = "sha256:ed283ab3a01d8b53de3a05bfdf4473ae24e43caee7dcb5584e86f3f3e5ab4374"},
{file = "regex-2021.8.28-cp39-cp39-win_amd64.whl", hash = "sha256:610b690b406653c84b7cb6091facb3033500ee81089867ee7d59e675f9ca2b73"},
{file = "regex-2021.8.28.tar.gz", hash = "sha256:f585cbbeecb35f35609edccb95efd95a3e35824cd7752b586503f7e6087303f1"},
]
requests = [
{file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"},
@ -2072,8 +2113,8 @@ requests-oauthlib = [
{file = "requests_oauthlib-1.3.0-py3.7.egg", hash = "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"},
]
rich = [
{file = "rich-10.7.0-py3-none-any.whl", hash = "sha256:517b4e0efd064dd1fe821ca93dd3095d73380ceac1f0a07173d507d9b18f1396"},
{file = "rich-10.7.0.tar.gz", hash = "sha256:13ac80676e12cf528dc4228dc682c8402f82577c2aa67191e294350fa2c3c4e9"},
{file = "rich-10.10.0-py3-none-any.whl", hash = "sha256:0b8cbcb0b8d476a7f002feaed9f35e51615f673c6c291d76ddf0c555574fd3c7"},
{file = "rich-10.10.0.tar.gz", hash = "sha256:bacf58b25fea6b920446fe4e7abdc6c7664c4530c4098e7a1bc79b16b8551dfa"},
]
rsa = [
{file = "rsa-4.7.2-py3-none-any.whl", hash = "sha256:78f9a9bf4e7be0c5ded4583326e7461e3a3c5aae24073648b4bdfa797d78c9d2"},
@ -2092,36 +2133,36 @@ soupsieve = [
{file = "soupsieve-2.2.1.tar.gz", hash = "sha256:052774848f448cf19c7e959adf5566904d525f33a3f8b6ba6f6f8f26ec7de0cc"},
]
sqlalchemy = [
{file = "SQLAlchemy-1.4.22-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:488608953385d6c127d2dcbc4b11f8d7f2f30b89f6bd27c01b042253d985cc2f"},
{file = "SQLAlchemy-1.4.22-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:5d856cc50fd26fc8dd04892ed5a5a3d7eeb914fea2c2e484183e2d84c14926e0"},
{file = "SQLAlchemy-1.4.22-cp27-cp27m-win32.whl", hash = "sha256:a00d9c6d3a8afe1d1681cd8a5266d2f0ed684b0b44bada2ca82403b9e8b25d39"},
{file = "SQLAlchemy-1.4.22-cp27-cp27m-win_amd64.whl", hash = "sha256:5908ea6c652a050d768580d01219c98c071e71910ab8e7b42c02af4010608397"},
{file = "SQLAlchemy-1.4.22-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b7fb937c720847879c7402fe300cfdb2aeff22349fa4ea3651bca4e2d6555939"},
{file = "SQLAlchemy-1.4.22-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:9bfe882d5a1bbde0245dca0bd48da0976bd6634cf2041d2fdf0417c5463e40e5"},
{file = "SQLAlchemy-1.4.22-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eedd76f135461cf237534a6dc0d1e0f6bb88a1dc193678fab48a11d223462da5"},
{file = "SQLAlchemy-1.4.22-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6a16c7c4452293da5143afa3056680db2d187b380b3ef4d470d4e29885720de3"},
{file = "SQLAlchemy-1.4.22-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44d23ea797a5e0be71bc5454b9ae99158ea0edc79e2393c6e9a2354de88329c0"},
{file = "SQLAlchemy-1.4.22-cp36-cp36m-win32.whl", hash = "sha256:a5e14cb0c0a4ac095395f24575a0e7ab5d1be27f5f9347f1762f21505e3ba9f1"},
{file = "SQLAlchemy-1.4.22-cp36-cp36m-win_amd64.whl", hash = "sha256:bc34a007e604091ca3a4a057525efc4cefd2b7fe970f44d20b9cfa109ab1bddb"},
{file = "SQLAlchemy-1.4.22-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:756f5d2f5b92d27450167247fb574b09c4cd192a3f8c2e493b3e518a204ee543"},
{file = "SQLAlchemy-1.4.22-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fcbb4b4756b250ed19adc5e28c005b8ed56fdb5c21efa24c6822c0575b4964d"},
{file = "SQLAlchemy-1.4.22-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:09dbb4bc01a734ccddbf188deb2a69aede4b3c153a72b6d5c6900be7fb2945b1"},
{file = "SQLAlchemy-1.4.22-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f028ef6a1d828bc754852a022b2160e036202ac8658a6c7d34875aafd14a9a15"},
{file = "SQLAlchemy-1.4.22-cp37-cp37m-win32.whl", hash = "sha256:68393d3fd31469845b6ba11f5b4209edbea0b58506be0e077aafbf9aa2e21e11"},
{file = "SQLAlchemy-1.4.22-cp37-cp37m-win_amd64.whl", hash = "sha256:891927a49b2363a4199763a9d436d97b0b42c65922a4ea09025600b81a00d17e"},
{file = "SQLAlchemy-1.4.22-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:fd2102a8f8a659522719ed73865dff3d3cc76eb0833039dc473e0ad3041d04be"},
{file = "SQLAlchemy-1.4.22-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4014978de28163cd8027434916a92d0f5bb1a3a38dff5e8bf8bff4d9372a9117"},
{file = "SQLAlchemy-1.4.22-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f814d80844969b0d22ea63663da4de5ca1c434cfbae226188901e5d368792c17"},
{file = "SQLAlchemy-1.4.22-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d09a760b0a045b4d799102ae7965b5491ccf102123f14b2a8cc6c01d1021a2d9"},
{file = "SQLAlchemy-1.4.22-cp38-cp38-win32.whl", hash = "sha256:26daa429f039e29b1e523bf763bfab17490556b974c77b5ca7acb545b9230e9a"},
{file = "SQLAlchemy-1.4.22-cp38-cp38-win_amd64.whl", hash = "sha256:12bac5fa1a6ea870bdccb96fe01610641dd44ebe001ed91ef7fcd980e9702db5"},
{file = "SQLAlchemy-1.4.22-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:39b5d36ab71f73c068cdcf70c38075511de73616e6c7fdd112d6268c2704d9f5"},
{file = "SQLAlchemy-1.4.22-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5102b9face693e8b2db3b2539c7e1a5d9a5b4dc0d79967670626ffd2f710d6e6"},
{file = "SQLAlchemy-1.4.22-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c9373ef67a127799027091fa53449125351a8c943ddaa97bec4e99271dbb21f4"},
{file = "SQLAlchemy-1.4.22-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36a089dc604032d41343d86290ce85d4e6886012eea73faa88001260abf5ff81"},
{file = "SQLAlchemy-1.4.22-cp39-cp39-win32.whl", hash = "sha256:b48148ceedfb55f764562e04c00539bb9ea72bf07820ca15a594a9a049ff6b0e"},
{file = "SQLAlchemy-1.4.22-cp39-cp39-win_amd64.whl", hash = "sha256:1fdae7d980a2fa617d119d0dc13ecb5c23cc63a8b04ffcb5298f2c59d86851e9"},
{file = "SQLAlchemy-1.4.22.tar.gz", hash = "sha256:ec1be26cdccd60d180359a527d5980d959a26269a2c7b1b327a1eea0cab37ed8"},
{file = "SQLAlchemy-1.4.23-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:25e9b2e5ca088879ce3740d9ccd4d58cb9061d49566a0b5e12166f403d6f4da0"},
{file = "SQLAlchemy-1.4.23-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d9667260125688c71ccf9af321c37e9fb71c2693575af8210f763bfbbee847c7"},
{file = "SQLAlchemy-1.4.23-cp27-cp27m-win32.whl", hash = "sha256:cec1a4c6ddf5f82191301a25504f0e675eccd86635f0d5e4c69e0661691931c5"},
{file = "SQLAlchemy-1.4.23-cp27-cp27m-win_amd64.whl", hash = "sha256:ae07895b55c7d58a7dd47438f437ac219c0f09d24c2e7d69fdebc1ea75350f00"},
{file = "SQLAlchemy-1.4.23-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:967307ea52985985224a79342527c36ec2d1daa257a39748dd90e001a4be4d90"},
{file = "SQLAlchemy-1.4.23-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:be185b3daf651c6c0639987a916bf41e97b60e68f860f27c9cb6574385f5cbb4"},
{file = "SQLAlchemy-1.4.23-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a0d3b3d51c83a66f5b72c57e1aad061406e4c390bd42cf1fda94effe82fac81"},
{file = "SQLAlchemy-1.4.23-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a8395c4db3e1450eef2b68069abf500cc48af4b442a0d98b5d3c9535fe40cde8"},
{file = "SQLAlchemy-1.4.23-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b128a78581faea7a5ee626ad4471353eee051e4e94616dfeff4742b6e5ba262"},
{file = "SQLAlchemy-1.4.23-cp36-cp36m-win32.whl", hash = "sha256:43fc207be06e50158e4dae4cc4f27ce80afbdbfa7c490b3b22feb64f6d9775a0"},
{file = "SQLAlchemy-1.4.23-cp36-cp36m-win_amd64.whl", hash = "sha256:e9d4f4552aa5e0d1417fc64a2ce1cdf56a30bab346ba6b0dd5e838eb56db4d29"},
{file = "SQLAlchemy-1.4.23-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:512f52a8872e8d63d898e4e158eda17e2ee40b8d2496b3b409422e71016db0bd"},
{file = "SQLAlchemy-1.4.23-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:355024cf061ed04271900414eb4a22671520241d2216ddb691bdd8a992172389"},
{file = "SQLAlchemy-1.4.23-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:82c03325111eab88d64e0ff48b6fe15c75d23787429fa1d84c0995872e702787"},
{file = "SQLAlchemy-1.4.23-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0aa312f9906ecebe133d7f44168c3cae4c76f27a25192fa7682f3fad505543c9"},
{file = "SQLAlchemy-1.4.23-cp37-cp37m-win32.whl", hash = "sha256:059c5f41e8630f51741a234e6ba2a034228c11b3b54a15478e61d8b55fa8bd9d"},
{file = "SQLAlchemy-1.4.23-cp37-cp37m-win_amd64.whl", hash = "sha256:cd68c5f9d13ffc8f4d6802cceee786678c5b1c668c97bc07b9f4a60883f36cd1"},
{file = "SQLAlchemy-1.4.23-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:6a8dbf3d46e889d864a57ee880c4ad3a928db5aa95e3d359cbe0da2f122e50c4"},
{file = "SQLAlchemy-1.4.23-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c15191f2430a30082f540ec6f331214746fc974cfdf136d7a1471d1c61d68ff"},
{file = "SQLAlchemy-1.4.23-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cd0e85dd2067159848c7672acd517f0c38b7b98867a347411ea01b432003f8d9"},
{file = "SQLAlchemy-1.4.23-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:370f4688ce47f0dc1e677a020a4d46252a31a2818fd67f5c256417faefc938af"},
{file = "SQLAlchemy-1.4.23-cp38-cp38-win32.whl", hash = "sha256:bd41f8063a9cd11b76d6d7d6af8139ab3c087f5dbbe5a50c02cb8ece7da34d67"},
{file = "SQLAlchemy-1.4.23-cp38-cp38-win_amd64.whl", hash = "sha256:2bca9a6e30ee425cc321d988a152a5fe1be519648e7541ac45c36cd4f569421f"},
{file = "SQLAlchemy-1.4.23-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:4803a481d4c14ce6ad53dc35458c57821863e9a079695c27603d38355e61fb7f"},
{file = "SQLAlchemy-1.4.23-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07b9099a95dd2b2620498544300eda590741ac54915c6b20809b6de7e3c58090"},
{file = "SQLAlchemy-1.4.23-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:37f2bd1b8e32c5999280f846701712347fc0ee7370e016ede2283c71712e127a"},
{file = "SQLAlchemy-1.4.23-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:448612570aa1437a5d1b94ada161805778fe80aba5b9a08a403e8ae4e071ded6"},
{file = "SQLAlchemy-1.4.23-cp39-cp39-win32.whl", hash = "sha256:e0ce4a2e48fe0a9ea3a5160411a4c5135da5255ed9ac9c15f15f2bcf58c34194"},
{file = "SQLAlchemy-1.4.23-cp39-cp39-win_amd64.whl", hash = "sha256:0aa746d1173587743960ff17b89b540e313aacfe6c1e9c81aa48393182c36d4f"},
{file = "SQLAlchemy-1.4.23.tar.gz", hash = "sha256:76ff246881f528089bf19385131b966197bb494653990396d2ce138e2a447583"},
]
starlette = [
{file = "starlette-0.13.6-py3-none-any.whl", hash = "sha256:bd2ffe5e37fb75d014728511f8e68ebf2c80b0fa3d04ca1479f4dc752ae31ac9"},
@ -2168,9 +2209,9 @@ typed-ast = [
{file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"},
]
typing-extensions = [
{file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"},
{file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"},
{file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"},
{file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"},
{file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"},
{file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"},
]
tzlocal = [
{file = "tzlocal-2.1-py2.py3-none-any.whl", hash = "sha256:e2cb6c6b5b604af38597403e9852872d7f534962ae2954c7f35efcb1ccacf4a4"},
@ -2185,16 +2226,22 @@ uvicorn = [
{file = "uvicorn-0.13.4.tar.gz", hash = "sha256:3292251b3c7978e8e4a7868f4baf7f7f7bb7e40c759ecc125c37e99cdea34202"},
]
uvloop = [
{file = "uvloop-0.15.3-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:e71fb9038bfcd7646ca126c5ef19b17e48d4af9e838b2bcfda7a9f55a6552a32"},
{file = "uvloop-0.15.3-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7522df4e45e4f25b50adbbbeb5bb9847495c438a628177099d2721f2751ff825"},
{file = "uvloop-0.15.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae2b325c0f6d748027f7463077e457006b4fdb35a8788f01754aadba825285ee"},
{file = "uvloop-0.15.3-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:0de811931e90ae2da9e19ce70ffad73047ab0c1dba7c6e74f9ae1a3aabeb89bd"},
{file = "uvloop-0.15.3-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7f4b8a905df909a407c5791fb582f6c03b0d3b491ecdc1cdceaefbc9bf9e08f6"},
{file = "uvloop-0.15.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d8ffe44ae709f839c54bacf14ed283f41bee90430c3b398e521e10f8d117b3a"},
{file = "uvloop-0.15.3-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:63a3288abbc9c8ee979d7e34c34e780b2fbab3e7e53d00b6c80271119f277399"},
{file = "uvloop-0.15.3-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5cda65fc60a645470b8525ce014516b120b7057b576fa876cdfdd5e60ab1efbb"},
{file = "uvloop-0.15.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ff05116ede1ebdd81802df339e5b1d4cab1dfbd99295bf27e90b4cec64d70e9"},
{file = "uvloop-0.15.3.tar.gz", hash = "sha256:905f0adb0c09c9f44222ee02f6b96fd88b493478fffb7a345287f9444e926030"},
{file = "uvloop-0.16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6224f1401025b748ffecb7a6e2652b17768f30b1a6a3f7b44660e5b5b690b12d"},
{file = "uvloop-0.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:30ba9dcbd0965f5c812b7c2112a1ddf60cf904c1c160f398e7eed3a6b82dcd9c"},
{file = "uvloop-0.16.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bd53f7f5db562f37cd64a3af5012df8cac2c464c97e732ed556800129505bd64"},
{file = "uvloop-0.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:772206116b9b57cd625c8a88f2413df2fcfd0b496eb188b82a43bed7af2c2ec9"},
{file = "uvloop-0.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b572256409f194521a9895aef274cea88731d14732343da3ecdb175228881638"},
{file = "uvloop-0.16.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:04ff57aa137230d8cc968f03481176041ae789308b4d5079118331ab01112450"},
{file = "uvloop-0.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a19828c4f15687675ea912cc28bbcb48e9bb907c801873bd1519b96b04fb805"},
{file = "uvloop-0.16.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e814ac2c6f9daf4c36eb8e85266859f42174a4ff0d71b99405ed559257750382"},
{file = "uvloop-0.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bd8f42ea1ea8f4e84d265769089964ddda95eb2bb38b5cbe26712b0616c3edee"},
{file = "uvloop-0.16.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:647e481940379eebd314c00440314c81ea547aa636056f554d491e40503c8464"},
{file = "uvloop-0.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e0d26fa5875d43ddbb0d9d79a447d2ace4180d9e3239788208527c4784f7cab"},
{file = "uvloop-0.16.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6ccd57ae8db17d677e9e06192e9c9ec4bd2066b77790f9aa7dede2cc4008ee8f"},
{file = "uvloop-0.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:089b4834fd299d82d83a25e3335372f12117a7d38525217c2258e9b9f4578897"},
{file = "uvloop-0.16.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98d117332cc9e5ea8dfdc2b28b0a23f60370d02e1395f88f40d1effd2cb86c4f"},
{file = "uvloop-0.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e5f2e2ff51aefe6c19ee98af12b4ae61f5be456cd24396953244a30880ad861"},
{file = "uvloop-0.16.0.tar.gz", hash = "sha256:f74bc20c7b67d1c27c72601c78cf95be99d5c2cdd4514502b4f3eb0933ff1228"},
]
validators = [
{file = "validators-0.18.2-py3-none-any.whl", hash = "sha256:0143dcca8a386498edaf5780cbd5960da1a4c85e0719f3ee5c9b41249c4fefbd"},
@ -2205,27 +2252,29 @@ w3lib = [
{file = "w3lib-1.22.0.tar.gz", hash = "sha256:0ad6d0203157d61149fd45aaed2e24f53902989c32fc1dccc2e2bfba371560df"},
]
watchdog = [
{file = "watchdog-2.1.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9628f3f85375a17614a2ab5eac7665f7f7be8b6b0a2a228e6f6a2e91dd4bfe26"},
{file = "watchdog-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:acc4e2d5be6f140f02ee8590e51c002829e2c33ee199036fcd61311d558d89f4"},
{file = "watchdog-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:85b851237cf3533fabbc034ffcd84d0fa52014b3121454e5f8b86974b531560c"},
{file = "watchdog-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a12539ecf2478a94e4ba4d13476bb2c7a2e0a2080af2bb37df84d88b1b01358a"},
{file = "watchdog-2.1.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6fe9c8533e955c6589cfea6f3f0a1a95fb16867a211125236c82e1815932b5d7"},
{file = "watchdog-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d9456f0433845e7153b102fffeb767bde2406b76042f2216838af3b21707894e"},
{file = "watchdog-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fd8c595d5a93abd441ee7c5bb3ff0d7170e79031520d113d6f401d0cf49d7c8f"},
{file = "watchdog-2.1.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0bcfe904c7d404eb6905f7106c54873503b442e8e918cc226e1828f498bdc0ca"},
{file = "watchdog-2.1.3-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bf84bd94cbaad8f6b9cbaeef43080920f4cb0e61ad90af7106b3de402f5fe127"},
{file = "watchdog-2.1.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b8ddb2c9f92e0c686ea77341dcb58216fa5ff7d5f992c7278ee8a392a06e86bb"},
{file = "watchdog-2.1.3-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8805a5f468862daf1e4f4447b0ccf3acaff626eaa57fbb46d7960d1cf09f2e6d"},
{file = "watchdog-2.1.3-py3-none-manylinux2014_armv7l.whl", hash = "sha256:3e305ea2757f81d8ebd8559d1a944ed83e3ab1bdf68bcf16ec851b97c08dc035"},
{file = "watchdog-2.1.3-py3-none-manylinux2014_i686.whl", hash = "sha256:431a3ea70b20962e6dee65f0eeecd768cd3085ea613ccb9b53c8969de9f6ebd2"},
{file = "watchdog-2.1.3-py3-none-manylinux2014_ppc64.whl", hash = "sha256:e4929ac2aaa2e4f1a30a36751160be391911da463a8799460340901517298b13"},
{file = "watchdog-2.1.3-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:201cadf0b8c11922f54ec97482f95b2aafca429c4c3a4bb869a14f3c20c32686"},
{file = "watchdog-2.1.3-py3-none-manylinux2014_s390x.whl", hash = "sha256:3a7d242a7963174684206093846537220ee37ba9986b824a326a8bb4ef329a33"},
{file = "watchdog-2.1.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:54e057727dd18bd01a3060dbf5104eb5a495ca26316487e0f32a394fd5fe725a"},
{file = "watchdog-2.1.3-py3-none-win32.whl", hash = "sha256:b5fc5c127bad6983eecf1ad117ab3418949f18af9c8758bd10158be3647298a9"},
{file = "watchdog-2.1.3-py3-none-win_amd64.whl", hash = "sha256:44acad6f642996a2b50bb9ce4fb3730dde08f23e79e20cd3d8e2a2076b730381"},
{file = "watchdog-2.1.3-py3-none-win_ia64.whl", hash = "sha256:0bcdf7b99b56a3ae069866c33d247c9994ffde91b620eaf0306b27e099bd1ae0"},
{file = "watchdog-2.1.3.tar.gz", hash = "sha256:e5236a8e8602ab6db4b873664c2d356c365ab3cac96fbdec4970ad616415dd45"},
{file = "watchdog-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5f57ce4f7e498278fb2a091f39359930144a0f2f90ea8cbf4523c4e25de34028"},
{file = "watchdog-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8b74d0d92a69a7ab5f101f9fe74e44ba017be269efa824337366ccbb4effde85"},
{file = "watchdog-2.1.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:59767f476cd1f48531bf378f0300565d879688c82da8369ca8c52f633299523c"},
{file = "watchdog-2.1.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:814d396859c95598f7576d15bc257c3bd3ba61fa4bc1db7dfc18f09070ded7da"},
{file = "watchdog-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:28777dbed3bbd95f9c70f461443990a36c07dbf49ae7cd69932cdd1b8fb2850c"},
{file = "watchdog-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5cf78f794c9d7bc64a626ef4f71aff88f57a7ae288e0b359a9c6ea711a41395f"},
{file = "watchdog-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:43bf728eb7830559f329864ab5da2302c15b2efbac24ad84ccc09949ba753c40"},
{file = "watchdog-2.1.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a7053d4d22dc95c5e0c90aeeae1e4ed5269d2f04001798eec43a654a03008d22"},
{file = "watchdog-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6f3ad1d973fe8fc8fe64ba38f6a934b74346342fa98ef08ad5da361a05d46044"},
{file = "watchdog-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:41d44ef21a77a32b55ce9bf59b75777063751f688de51098859b7c7f6466589a"},
{file = "watchdog-2.1.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ed4ca4351cd2bb0d863ee737a2011ca44d8d8be19b43509bd4507f8a449b376b"},
{file = "watchdog-2.1.5-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8874d5ad6b7f43b18935d9b0183e29727a623a216693d6938d07dfd411ba462f"},
{file = "watchdog-2.1.5-py3-none-manylinux2014_aarch64.whl", hash = "sha256:50a7f81f99d238f72185f481b493f9de80096e046935b60ea78e1276f3d76960"},
{file = "watchdog-2.1.5-py3-none-manylinux2014_armv7l.whl", hash = "sha256:e40e33a4889382824846b4baa05634e1365b47c6fa40071dc2d06b4d7c715fc1"},
{file = "watchdog-2.1.5-py3-none-manylinux2014_i686.whl", hash = "sha256:78b1514067ff4089f4dac930b043a142997a5b98553120919005e97fbaba6546"},
{file = "watchdog-2.1.5-py3-none-manylinux2014_ppc64.whl", hash = "sha256:58ae842300cbfe5e62fb068c83901abe76e4f413234b7bec5446e4275eb1f9cb"},
{file = "watchdog-2.1.5-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:b0cc7d8b7d60da6c313779d85903ce39a63d89d866014b085f720a083d5f3e9a"},
{file = "watchdog-2.1.5-py3-none-manylinux2014_s390x.whl", hash = "sha256:e60d3bb7166b7cb830b86938d1eb0e6cfe23dfd634cce05c128f8f9967895193"},
{file = "watchdog-2.1.5-py3-none-manylinux2014_x86_64.whl", hash = "sha256:51af09ae937ada0e9a10cc16988ec03c649754a91526170b6839b89fc56d6acb"},
{file = "watchdog-2.1.5-py3-none-win32.whl", hash = "sha256:9391003635aa783957b9b11175d9802d3272ed67e69ef2e3394c0b6d9d24fa9a"},
{file = "watchdog-2.1.5-py3-none-win_amd64.whl", hash = "sha256:eab14adfc417c2c983fbcb2c73ef3f28ba6990d1fff45d1180bf7e38bda0d98d"},
{file = "watchdog-2.1.5-py3-none-win_ia64.whl", hash = "sha256:a2888a788893c4ef7e562861ec5433875b7915f930a5a7ed3d32c048158f1be5"},
{file = "watchdog-2.1.5.tar.gz", hash = "sha256:5563b005907613430ef3d4aaac9c78600dd5704e84764cb6deda4b3d72807f09"},
]
watchgod = [
{file = "watchgod-0.7-py3-none-any.whl", hash = "sha256:d6c1ea21df37847ac0537ca0d6c2f4cdf513562e95f77bb93abbcf05573407b7"},

View file

@ -32,7 +32,7 @@ passlib = "^1.7.4"
lxml = "4.6.2"
Pillow = "^8.2.0"
pathvalidate = "^2.4.1"
apprise = "^0.9.2"
apprise = "0.9.3"
recipe-scrapers = "^13.2.7"
psycopg2-binary = {version = "^2.9.1", optional = true}
gunicorn = "^20.1.0"

View file

@ -22,12 +22,15 @@ def test_create_webhook(api_client: TestClient, unique_user: TestUser, webhook_d
assert response.status_code == 201
def test_read_webhook(api_client: TestClient, webhook_data, unique_user: TestUser):
def test_read_webhook(api_client: TestClient, unique_user: TestUser, webhook_data):
response = api_client.post(Routes.base, json=webhook_data, headers=unique_user.token)
response = api_client.get(Routes.item(1), headers=unique_user.token)
webhook = response.json()
assert webhook["id"] == 1
print(webhook)
assert webhook["id"]
assert webhook["name"] == webhook_data["name"]
assert webhook["url"] == webhook_data["url"]
assert webhook["time"] == webhook_data["time"]

View file

@ -0,0 +1,77 @@
import pytest
from fastapi.testclient import TestClient
from mealie.schema.recipe.recipe_ingredient import CreateIngredientFood
from tests.utils.factories import random_string
from tests.utils.fixture_schemas import TestUser
class Routes:
base = "/api/foods"
def item(item_id: int) -> str:
return f"{Routes.base}/{item_id}"
@pytest.fixture(scope="function")
def food(api_client: TestClient, unique_user: TestUser) -> dict:
data = CreateIngredientFood(
name=random_string(10),
description=random_string(10),
).dict(by_alias=True)
response = api_client.post(Routes.base, json=data, headers=unique_user.token)
assert response.status_code == 201
yield response.json()
response = api_client.delete(Routes.item(response.json()["id"]), headers=unique_user.token)
def test_create_food(api_client: TestClient, unique_user: TestUser):
data = CreateIngredientFood(
name=random_string(10),
description=random_string(10),
).dict(by_alias=True)
response = api_client.post(Routes.base, json=data, headers=unique_user.token)
assert response.status_code == 201
def test_read_food(api_client: TestClient, food: dict, unique_user: TestUser):
response = api_client.get(Routes.item(food["id"]), headers=unique_user.token)
assert response.status_code == 200
as_json = response.json()
assert as_json["id"] == food["id"]
assert as_json["name"] == food["name"]
assert as_json["description"] == food["description"]
def test_update_food(api_client: TestClient, food: dict, unique_user: TestUser):
update_data = {
"id": food["id"],
"name": random_string(10),
"description": random_string(10),
}
response = api_client.put(Routes.item(food["id"]), json=update_data, headers=unique_user.token)
assert response.status_code == 200
as_json = response.json()
assert as_json["id"] == food["id"]
assert as_json["name"] == update_data["name"]
assert as_json["description"] == update_data["description"]
def test_delete_food(api_client: TestClient, food: dict, unique_user: TestUser):
id = food["id"]
response = api_client.delete(Routes.item(id), headers=unique_user.token)
assert response.status_code == 200
response = api_client.get(Routes.item(id), headers=unique_user.token)
print(response.json())
assert response.status_code == 404

View file

@ -0,0 +1,84 @@
import pytest
from fastapi.testclient import TestClient
from mealie.schema.recipe.recipe_ingredient import CreateIngredientUnit
from tests.utils.factories import random_bool, random_string
from tests.utils.fixture_schemas import TestUser
class Routes:
base = "/api/units"
def item(item_id: int) -> str:
return f"{Routes.base}/{item_id}"
@pytest.fixture(scope="function")
def unit(api_client: TestClient, unique_user: TestUser) -> dict:
data = CreateIngredientUnit(
name=random_string(10),
description=random_string(10),
fraction=random_bool(),
abbreviation=random_string(3) + ".",
).dict(by_alias=True)
response = api_client.post(Routes.base, json=data, headers=unique_user.token)
assert response.status_code == 201
yield response.json()
response = api_client.delete(Routes.item(response.json()["id"]), headers=unique_user.token)
def test_create_unit(api_client: TestClient, unique_user: TestUser):
data = CreateIngredientUnit(
name=random_string(10),
description=random_string(10),
).dict(by_alias=True)
response = api_client.post(Routes.base, json=data, headers=unique_user.token)
assert response.status_code == 201
def test_read_unit(api_client: TestClient, unit: dict, unique_user: TestUser):
response = api_client.get(Routes.item(unit["id"]), headers=unique_user.token)
assert response.status_code == 200
as_json = response.json()
assert as_json["id"] == unit["id"]
assert as_json["name"] == unit["name"]
assert as_json["description"] == unit["description"]
assert as_json["fraction"] == unit["fraction"]
assert as_json["abbreviation"] == unit["abbreviation"]
def test_update_unit(api_client: TestClient, unit: dict, unique_user: TestUser):
update_data = {
"id": unit["id"],
"name": random_string(10),
"description": random_string(10),
"fraction": not unit["fraction"],
"abbreviation": random_string(3) + ".",
}
response = api_client.put(Routes.item(unit["id"]), json=update_data, headers=unique_user.token)
assert response.status_code == 200
as_json = response.json()
assert as_json["id"] == unit["id"]
assert as_json["name"] == update_data["name"]
assert as_json["description"] == update_data["description"]
assert as_json["fraction"] == update_data["fraction"]
assert as_json["abbreviation"] == update_data["abbreviation"]
def test_delete_unit(api_client: TestClient, unit: dict, unique_user: TestUser):
id = unit["id"]
response = api_client.delete(Routes.item(id), headers=unique_user.token)
assert response.status_code == 200
response = api_client.get(Routes.item(id), headers=unique_user.token)
assert response.status_code == 404

View file

@ -12,6 +12,10 @@ def random_email(length=10) -> str:
return "".join(random.choice(string.ascii_lowercase + string.digits) for _ in range(length)) + "@fake.com"
def random_bool() -> bool:
return bool(random.getrandbits(1))
def user_registration_factory() -> CreateUserRegistration:
return CreateUserRegistration(
group=random_string(),