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 ----- # ----- install & configure poetry -----
#---------------------------------------------- #----------------------------------------------
- name: Install Poetry - name: Install Poetry
uses: snok/install-poetry@v1.1.1 uses: snok/install-poetry@v1
with: with:
virtualenvs-create: true virtualenvs-create: true
virtualenvs-in-project: true virtualenvs-in-project: true
@ -57,7 +57,7 @@ jobs:
run: | run: |
poetry install poetry install
poetry add "psycopg2-binary==2.8.6" 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 # run test suite
#---------------------------------------------- #----------------------------------------------

View File

@ -24,12 +24,19 @@ services:
ports: ports:
- 9092:80 - 9092:80
environment: environment:
# DB_ENGINE: postgres # Optional: 'sqlite', 'postgres' DB_ENGINE: postgres # Optional: 'sqlite', 'postgres'
# POSTGRES_USER: mealie POSTGRES_USER: mealie
# POSTGRES_PASSWORD: mealie POSTGRES_PASSWORD: mealie
# POSTGRES_SERVER: postgres POSTGRES_SERVER: postgres
# POSTGRES_PORT: 5432 POSTGRES_PORT: 5432
# POSTGRES_DB: mealie POSTGRES_DB: mealie
# WORKERS_PER_CORE: 0.5 WORKERS_PER_CORE: 0.5
MAX_WORKERS: 1 MAX_WORKERS: 1
WEB_CONCURRENCY: 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"> <script lang="ts">
import { computed, defineComponent, reactive, toRefs } from "@nuxtjs/composition-api"; import { computed, defineComponent, reactive, toRefs } from "@nuxtjs/composition-api";
import { isSameDay, addDays, subDays, parseISO, format } from "date-fns"; 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 draggable from "vuedraggable";
import { useMealplans } from "~/composables/use-group-mealplan"; import { useMealplans } from "~/composables/use-group-mealplan";
import { useRecipes, allRecipes } from "~/composables/use-recipes"; 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.core.root_logger import get_logger
from mealie.routes import backup_routes, migration_routes, router, utility_routes from mealie.routes import backup_routes, migration_routes, router, utility_routes
from mealie.routes.about import about_router 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.media import media_router
from mealie.routes.site_settings import settings_router from mealie.routes.site_settings import settings_router
from mealie.services.events import create_general_event from mealie.services.events import create_general_event
@ -36,7 +35,6 @@ def api_routers():
app.include_router(media_router) app.include_router(media_router)
app.include_router(about_router) app.include_router(about_router)
# Meal Routes # Meal Routes
app.include_router(meal_plan_router)
# Settings Routes # Settings Routes
app.include_router(settings_router) app.include_router(settings_router)
# Backups/Imports Routes # Backups/Imports Routes

View File

@ -7,7 +7,7 @@ from jose import JWTError, jwt
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
from mealie.core.config import app_dirs, settings 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.db.db_setup import generate_session
from mealie.schema.user import LongLiveTokenInDB, PrivateUser, TokenData 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: except JWTError:
raise credentials_exception 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: if user is None:
raise credentials_exception raise credentials_exception
return user 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: 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: for token in tokens:
token: LongLiveTokenInDB token: LongLiveTokenInDB

View File

@ -5,7 +5,7 @@ from jose import jwt
from passlib.context import CryptContext from passlib.context import CryptContext
from mealie.core.config import settings 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 from mealie.schema.user import PrivateUser
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") 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: 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: 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: if not user:
return False 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") 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 """A Generic BaseAccess Model method to perform common operations on the database
Args: Args:
@ -22,7 +22,8 @@ class BaseAccessModel(Generic[T, D]):
Generic ([D]): Represents the SqlAlchemyModel Model 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.primary_key = primary_key
self.sql_model = sql_model self.sql_model = sql_model
self.schema = schema self.schema = schema
@ -37,9 +38,7 @@ class BaseAccessModel(Generic[T, D]):
for observer in self.observers: for observer in self.observers:
observer() observer()
def get_all( def get_all(self, limit: int = None, order_by: str = None, start=0, override_schema=None) -> list[T]:
self, session: Session, limit: int = None, order_by: str = None, start=0, override_schema=None
) -> list[T]:
eff_schema = override_schema or self.schema eff_schema = override_schema or self.schema
if order_by: if order_by:
@ -47,27 +46,20 @@ class BaseAccessModel(Generic[T, D]):
return [ return [
eff_schema.from_orm(x) 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( def multi_query(self, query_by: dict[str, str], start=0, limit: int = None, override_schema=None) -> list[T]:
self,
session: Session,
query_by: dict[str, str],
start=0,
limit: int = None,
override_schema=None,
) -> list[T]:
eff_schema = override_schema or self.schema eff_schema = override_schema or self.schema
return [ return [
eff_schema.from_orm(x) 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 """Queries the database for the selected model. Restricts return responses to the
keys specified under "fields" keys specified under "fields"
@ -79,9 +71,9 @@ class BaseAccessModel(Generic[T, D]):
Returns: Returns:
list[SqlAlchemyBase]: Returns a list of ORM objects 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 """Queries the database of the selected model and returns a list
of all primary_key values of all primary_key values
@ -91,11 +83,11 @@ class BaseAccessModel(Generic[T, D]):
Returns: Returns:
list[str]: 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] results_as_dict = [x.dict() for x in results]
return [x.get(self.primary_key) for x in results_as_dict] 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 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. 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: if match_key is None:
match_key = self.primary_key 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 key = key or self.primary_key
if any_case: if any_case:
search_attr = getattr(self.sql_model, key) 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: 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: if not result:
return return
@ -121,7 +113,7 @@ class BaseAccessModel(Generic[T, D]):
return eff_schema.from_orm(result) return eff_schema.from_orm(result)
def get( 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]: ) -> T | list[T]:
"""Retrieves an entry from the database by matching a key/value pair. If no """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. 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: if any_case:
search_attr = getattr(self.sql_model, match_key) search_attr = getattr(self.sql_model, match_key)
result = ( 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: 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 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] 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. """Creates a new database entry for the given SQL Alchemy Model.
Args: Args:
@ -167,17 +162,17 @@ class BaseAccessModel(Generic[T, D]):
dict: A dictionary representation of the database entry dict: A dictionary representation of the database entry
""" """
document = document if isinstance(document, dict) else document.dict() document = document if isinstance(document, dict) else document.dict()
new_document = self.sql_model(session=session, **document) new_document = self.sql_model(session=self.session, **document)
session.add(new_document) self.session.add(new_document)
session.commit() self.session.commit()
session.refresh(new_document) self.session.refresh(new_document)
if self.observers: if self.observers:
self.update_observers() self.update_observers()
return self.schema.from_orm(new_document) 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. """Update a database entry.
Args: Args:
session (Session): Database Session 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() 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)
entry.update(session=session, **new_data) entry.update(session=self.session, **new_data)
if self.observers: if self.observers:
self.update_observers() self.update_observers()
session.commit() self.session.commit()
return self.schema.from_orm(entry) 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() 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: if not entry:
return return
@ -209,43 +204,43 @@ class BaseAccessModel(Generic[T, D]):
entry_as_dict = self.schema.from_orm(entry).dict() entry_as_dict = self.schema.from_orm(entry).dict()
entry_as_dict.update(new_data) 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: def delete(self, primary_key_value) -> D:
result = session.query(self.sql_model).filter_by(**{self.primary_key: primary_key_value}).one() result = self.session.query(self.sql_model).filter_by(**{self.primary_key: primary_key_value}).one()
results_as_model = self.schema.from_orm(result) results_as_model = self.schema.from_orm(result)
session.delete(result) self.session.delete(result)
session.commit() self.session.commit()
if self.observers: if self.observers:
self.update_observers() self.update_observers()
return results_as_model return results_as_model
def delete_all(self, session: Session) -> None: def delete_all(self) -> None:
session.query(self.sql_model).delete() self.session.query(self.sql_model).delete()
session.commit() self.session.commit()
if self.observers: if self.observers:
self.update_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]: if None in [match_key, match_value]:
return session.query(self.sql_model).count() return self.session.query(self.sql_model).count()
else: 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( 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]: ) -> Union[int, T]:
eff_schema = override_schema or self.schema eff_schema = override_schema or self.schema
# attr_filter = getattr(self.sql_model, attribute_name) # attr_filter = getattr(self.sql_model, attribute_name)
if count: 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: else:
return [ return [
eff_schema.from_orm(x) 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.meal_plan.meal import MealPlanOut
from mealie.schema.user.user import GroupInDB 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]: 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 """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 datetime import date
from sqlalchemy.orm.session import Session
from mealie.db.models.group import GroupMealPlan from mealie.db.models.group import GroupMealPlan
from mealie.schema.meal_plan.new_meal import ReadPlanEntry 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]): class MealDataAccessModel(AccessModel[ReadPlanEntry, GroupMealPlan]):
def get_slice(self, session: Session, start: date, end: date, group_id: int) -> list[ReadPlanEntry]: def get_slice(self, start: date, end: date, group_id: int) -> list[ReadPlanEntry]:
start = start.strftime("%Y-%m-%d") start = start.strftime("%Y-%m-%d")
end = end.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.date.between(start, end),
GroupMealPlan.group_id == group_id, GroupMealPlan.group_id == group_id,
) )
return [self.schema.from_orm(x) for x in qry.all()] 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() 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()] return [self.schema.from_orm(x) for x in qry.all()]

View File

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

View File

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

View File

@ -10,7 +10,7 @@ def sql_global_init(db_url: str):
if "sqlite" in db_url: if "sqlite" in db_url:
connect_args["check_same_thread"] = False 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) 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 import root_logger
from mealie.core.config import settings from mealie.core.config import settings
from mealie.core.security import hash_password 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.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.db_setup import create_session, engine
from mealie.db.models._model_base import SqlAlchemyBase from mealie.db.models._model_base import SqlAlchemyBase
from mealie.schema.admin import SiteSettings from mealie.schema.admin import SiteSettings
@ -21,30 +20,24 @@ def create_all_models():
SqlAlchemyBase.metadata.create_all(engine) SqlAlchemyBase.metadata.create_all(engine)
def init_db(session: Session = None) -> None: def init_db(db: Database) -> None:
create_all_models() default_group_init(db)
default_settings_init(db)
if not session: default_user_init(db)
session = create_session() default_recipe_unit_init(db)
with session:
default_group_init(session)
default_settings_init(session)
default_user_init(session)
default_recipe_unit_init(db, session)
def default_settings_init(session: Session): def default_settings_init(db: Database):
document = db.settings.create(session, SiteSettings().dict()) document = db.settings.create(SiteSettings().dict())
logger.info(f"Created Site Settings: \n {document}") 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") 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 = { default_user = {
"full_name": "Change Me", "full_name": "Change Me",
"username": "admin", "username": "admin",
@ -55,21 +48,26 @@ def default_user_init(session: Session):
} }
logger.info("Generating Default User") logger.info("Generating Default User")
db.users.create(session, default_user) db.users.create(default_user)
def main(): def main():
create_all_models()
session = create_session()
db = get_database(session)
try: try:
session = create_session() init_user = db.users.get("1", "id")
init_user = db.users.get(session, "1", "id")
except Exception: except Exception:
init_db() init_db(db)
return return
if init_user: if init_user:
logger.info("Database Exists") logger.info("Database Exists")
else: else:
logger.info("Database Doesn't Exists, Initializing...") logger.info("Database Doesn't Exists, Initializing...")
init_db() init_db(db)
create_general_event("Initialize Database", "Initialize database with default values", session) 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 from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
@ -11,6 +11,7 @@ class IngredientUnitModel(SqlAlchemyBase, BaseMixins):
name = Column(String) name = Column(String)
description = Column(String) description = Column(String)
abbreviation = Column(String) abbreviation = Column(String)
fraction = Column(Boolean)
ingredients = orm.relationship("RecipeIngredient", back_populates="unit") ingredients = orm.relationship("RecipeIngredient", back_populates="unit")
@auto_init() @auto_init()

View File

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

View File

@ -4,7 +4,7 @@ from fastapi import Depends, status
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
from mealie.core.root_logger import get_logger 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.db.db_setup import generate_session
from mealie.routes.routers import AdminAPIRouter from mealie.routes.routers import AdminAPIRouter
from mealie.schema.events import EventNotificationIn, EventNotificationOut, TestEvent from mealie.schema.events import EventNotificationIn, EventNotificationOut, TestEvent
@ -21,8 +21,9 @@ async def create_event_notification(
session: Session = Depends(generate_session), session: Session = Depends(generate_session),
): ):
""" Create event_notification in the Database """ """ 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") @router.post("/notifications/test")
@ -31,9 +32,10 @@ async def test_notification_route(
session: Session = Depends(generate_session), session: Session = Depends(generate_session),
): ):
""" Create event_notification in the Database """ """ Create event_notification in the Database """
db = get_database(session)
if test_data.id: 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 test_data.test_url = event_obj.notification_url
try: try:
@ -46,8 +48,8 @@ async def test_notification_route(
@router.get("/notifications", response_model=list[EventNotificationOut]) @router.get("/notifications", response_model=list[EventNotificationOut])
async def get_all_event_notification(session: Session = Depends(generate_session)): async def get_all_event_notification(session: Session = Depends(generate_session)):
""" Get all event_notification from the Database """ """ Get all event_notification from the Database """
# Get Item db = get_database(session)
return db.event_notifications.get_all(session, override_schema=EventNotificationOut) return db.event_notifications.get_all(override_schema=EventNotificationOut)
@router.put("/notifications/{id}") @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)): async def delete_event_notification(id: int, session: Session = Depends(generate_session)):
""" Delete event_notification from the Database """ """ Delete event_notification from the Database """
# Delete Item # 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) @router.get("/statistics", response_model=AppStatistics)
async def get_app_statistics(session: Session = Depends(generate_session)): async def get_app_statistics(session: Session = Depends(generate_session)):
db = get_database() db = get_database(session)
return AppStatistics( return AppStatistics(
total_recipes=db.recipes.count_all(session), total_recipes=db.recipes.count_all(),
uncategorized_recipes=db.recipes.count_uncategorized(session), uncategorized_recipes=db.recipes.count_uncategorized(),
untagged_recipes=db.recipes.count_untagged(session), untagged_recipes=db.recipes.count_untagged(),
total_users=db.users.count_all(session), total_users=db.users.count_all(),
total_groups=db.groups.count_all(session), 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 sqlalchemy.orm.session import Session
from mealie.core.dependencies import get_current_user 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.db.db_setup import generate_session
from mealie.routes.routers import AdminAPIRouter from mealie.routes.routers import AdminAPIRouter
from mealie.schema.user import GroupBase, GroupInDB, PrivateUser, UpdateGroup from mealie.schema.user import GroupBase, GroupInDB, PrivateUser, UpdateGroup
@ -14,8 +14,9 @@ router = AdminAPIRouter(prefix="/groups")
@router.get("", response_model=list[GroupInDB]) @router.get("", response_model=list[GroupInDB])
async def get_all_groups(session: Session = Depends(generate_session)): async def get_all_groups(session: Session = Depends(generate_session)):
""" Returns a list of all groups in the database """ """ 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) @router.post("", status_code=status.HTTP_201_CREATED, response_model=GroupInDB)
@ -25,9 +26,10 @@ async def create_group(
session: Session = Depends(generate_session), session: Session = Depends(generate_session),
): ):
""" Creates a Group in the Database """ """ Creates a Group in the Database """
db = get_database(session)
try: 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) background_tasks.add_task(create_group_event, "Group Created", f"'{group_data.name}' created", session)
return new_group return new_group
except Exception: except Exception:
@ -37,7 +39,8 @@ async def create_group(
@router.put("/{id}") @router.put("/{id}")
async def update_group_data(id: int, group_data: UpdateGroup, session: Session = Depends(generate_session)): async def update_group_data(id: int, group_data: UpdateGroup, session: Session = Depends(generate_session)):
""" Updates a User Group """ """ 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}") @router.delete("/{id}")
@ -48,11 +51,12 @@ async def delete_user_group(
session: Session = Depends(generate_session), session: Session = Depends(generate_session),
): ):
""" Removes a user group from the database """ """ Removes a user group from the database """
db = get_database(session)
if id == 1: if id == 1:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="DEFAULT_GROUP") 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: if not group:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="GROUP_NOT_FOUND") 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 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 sqlalchemy.orm.session import Session
from mealie.core.dependencies import is_logged_in 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.db.db_setup import generate_session
from mealie.routes.routers import AdminAPIRouter, UserAPIRouter from mealie.routes.routers import AdminAPIRouter, UserAPIRouter
from mealie.schema.recipe import CategoryIn, RecipeCategoryResponse from mealie.schema.recipe import CategoryIn, RecipeCategoryResponse
@ -15,13 +15,15 @@ admin_router = AdminAPIRouter()
@public_router.get("") @public_router.get("")
async def get_all_recipe_categories(session: Session = Depends(generate_session)): async def get_all_recipe_categories(session: Session = Depends(generate_session)):
""" Returns a list of available categories in the database """ """ 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") @public_router.get("/empty")
def get_empty_categories(session: Session = Depends(generate_session)): def get_empty_categories(session: Session = Depends(generate_session)):
""" Returns a list of categories that do not contain any recipes""" """ 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) @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) category: str, session: Session = Depends(generate_session), is_user: bool = Depends(is_logged_in)
): ):
""" Returns a list of recipes associated with the provided category. """ """ 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) category_obj = RecipeCategoryResponse.from_orm(category_obj)
if not is_user: if not is_user:
@ -42,9 +45,10 @@ def get_all_recipes_by_category(
@user_router.post("") @user_router.post("")
async def create_recipe_category(category: CategoryIn, session: Session = Depends(generate_session)): async def create_recipe_category(category: CategoryIn, session: Session = Depends(generate_session)):
""" Creates a Category in the database """ """ Creates a Category in the database """
db = get_database(session)
try: try:
return db.categories.create(session, category.dict()) return db.categories.create(category.dict())
except Exception: except Exception:
raise HTTPException(status.HTTP_400_BAD_REQUEST) 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) @admin_router.put("/{category}", response_model=RecipeCategoryResponse)
async def update_recipe_category(category: str, new_category: CategoryIn, session: Session = Depends(generate_session)): async def update_recipe_category(category: str, new_category: CategoryIn, session: Session = Depends(generate_session)):
""" Updates an existing Tag in the database """ """ Updates an existing Tag in the database """
db = get_database(session)
try: try:
return db.categories.update(session, category, new_category.dict()) return db.categories.update(category, new_category.dict())
except Exception: except Exception:
raise HTTPException(status.HTTP_400_BAD_REQUEST) 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 category does not impact a recipe. The category will be removed
from any recipes that contain it from any recipes that contain it
""" """
db = get_database(session)
try: try:
db.categories.delete(session, category) db.categories.delete(category)
except Exception: except Exception:
raise HTTPException(status.HTTP_400_BAD_REQUEST) raise HTTPException(status.HTTP_400_BAD_REQUEST)

View File

@ -7,12 +7,10 @@ router = APIRouter()
@router.get("", response_model=list[ReadInviteToken]) @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() return g_service.get_invite_tokens()
@router.post("", response_model=ReadInviteToken, status_code=status.HTTP_201_CREATED) @router.post("", response_model=ReadInviteToken, status_code=status.HTTP_201_CREATED)
def create_invite_token( def create_invite_token(uses: CreateInviteToken, g_service: GroupSelfService = Depends(GroupSelfService.private)):
uses: CreateInviteToken, g_service: GroupSelfService = Depends(GroupSelfService.write_existing)
):
return g_service.create_invite_token(uses.uses) 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 fastapi import APIRouter, Depends
from sqlalchemy.orm.session import Session 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.db.db_setup import generate_session
from mealie.schema.recipe import RecipeSummary from mealie.schema.recipe import RecipeSummary
@ -10,9 +10,11 @@ router = APIRouter()
@router.get("/summary/untagged", response_model=list[RecipeSummary]) @router.get("/summary/untagged", response_model=list[RecipeSummary])
async def get_untagged_recipes(count: bool = False, session: Session = Depends(generate_session)): 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]) @router.get("/summary/uncategorized", response_model=list[RecipeSummary])
async def get_uncategorized_recipes(count: bool = False, session: Session = Depends(generate_session)): 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 sqlalchemy.orm.session import Session
from mealie.core.dependencies import get_current_user 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.db.db_setup import generate_session
from mealie.routes.routers import UserAPIRouter from mealie.routes.routers import UserAPIRouter
from mealie.schema.recipe import CommentOut, CreateComment, SaveComment from mealie.schema.recipe import CommentOut, CreateComment, SaveComment
@ -21,9 +21,10 @@ async def create_comment(
current_user: PrivateUser = Depends(get_current_user), current_user: PrivateUser = Depends(get_current_user),
): ):
""" Create comment in the Database """ """ Create comment in the Database """
db = get_database(session)
new_comment = SaveComment(user=current_user.id, text=new_comment.text, recipe_slug=slug) 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}") @router.put("/{slug}/comments/{id}")
@ -34,12 +35,13 @@ async def update_comment(
current_user: PrivateUser = Depends(get_current_user), current_user: PrivateUser = Depends(get_current_user),
): ):
""" Update comment in the Database """ """ 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: if current_user.id != old_comment.user.id:
raise HTTPException(status.HTTP_403_FORBIDDEN) 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}") @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) id: int, session: Session = Depends(generate_session), current_user: PrivateUser = Depends(get_current_user)
): ):
""" Delete comment from the Database """ """ 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: if current_user.id == comment.user.id or current_user.admin:
db.comments.delete(session, id) db.comments.delete(id)
return return
raise HTTPException(status.HTTP_403_FORBIDDEN) raise HTTPException(status.HTTP_403_FORBIDDEN)

View File

@ -5,7 +5,7 @@ from fastapi.datastructures import UploadFile
from slugify import slugify from slugify import slugify
from sqlalchemy.orm.session import Session 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.db.db_setup import generate_session
from mealie.routes.routers import UserAPIRouter from mealie.routes.routers import UserAPIRouter
from mealie.schema.recipe import CreateRecipeByURL, Recipe, RecipeAsset from mealie.schema.recipe import CreateRecipeByURL, Recipe, RecipeAsset
@ -32,8 +32,9 @@ def update_recipe_image(
session: Session = Depends(generate_session), session: Session = Depends(generate_session),
): ):
""" Removes an existing image and replaces it with the incoming file. """ """ Removes an existing image and replaces it with the incoming file. """
db = get_database(session)
write_image(slug, image, extension) 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} return {"image": new_version}
@ -58,7 +59,9 @@ def upload_recipe_asset(
if not dest.is_file(): if not dest.is_file():
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR) 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) recipe.assets.append(asset_in)
db.recipes.update(session, slug, recipe.dict()) db.recipes.update(slug, recipe.dict())
return asset_in 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.dependencies import temporary_zip_path
from mealie.core.root_logger import get_logger 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.db.db_setup import generate_session
from mealie.routes.routers import UserAPIRouter from mealie.routes.routers import UserAPIRouter
from mealie.schema.recipe import CreateRecipeByURL, Recipe, RecipeImageTypes from mealie.schema.recipe import CreateRecipeByURL, Recipe, RecipeImageTypes
@ -71,7 +71,9 @@ async def create_recipe_from_zip(
with myzip.open(file) as myfile: with myzip.open(file) as myfile:
recipe_image = myfile.read() 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") 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) 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 """ """ 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) image_asset = recipe.image_dir.joinpath(RecipeImageTypes.original.value)
@ -105,14 +109,12 @@ async def get_recipe_as_zip(
@user_router.put("/{slug}") @user_router.put("/{slug}")
def update_recipe(data: Recipe, recipe_service: RecipeService = Depends(RecipeService.write_existing)): def update_recipe(data: Recipe, recipe_service: RecipeService = Depends(RecipeService.write_existing)):
""" Updates a recipe by existing slug and data. """ """ Updates a recipe by existing slug and data. """
return recipe_service.update_one(data) return recipe_service.update_one(data)
@user_router.patch("/{slug}") @user_router.patch("/{slug}")
def patch_recipe(data: Recipe, recipe_service: RecipeService = Depends(RecipeService.write_existing)): def patch_recipe(data: Recipe, recipe_service: RecipeService = Depends(RecipeService.write_existing)):
""" Updates a recipe by existing slug and data. """ """ Updates a recipe by existing slug and data. """
return recipe_service.patch_one(data) return recipe_service.patch_one(data)

View File

@ -2,7 +2,7 @@ from fastapi import Depends
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
from mealie.core.dependencies import get_current_user 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.db.db_setup import generate_session
from mealie.routes.routers import UserAPIRouter from mealie.routes.routers import UserAPIRouter
from mealie.schema.meal_plan import ShoppingListIn, ShoppingListOut from mealie.schema.meal_plan import ShoppingListIn, ShoppingListOut
@ -18,25 +18,28 @@ async def create_shopping_list(
session: Session = Depends(generate_session), session: Session = Depends(generate_session),
): ):
""" Create Shopping List in the Database """ """ Create Shopping List in the Database """
db = get_database(session)
list_in.group = current_user.group 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) @router.get("/{id}", response_model=ShoppingListOut)
async def get_shopping_list(id: int, session: Session = Depends(generate_session)): async def get_shopping_list(id: int, session: Session = Depends(generate_session)):
""" Get Shopping List from the Database """ """ 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) @router.put("/{id}", response_model=ShoppingListOut)
async def update_shopping_list(id: int, new_data: ShoppingListIn, session: Session = Depends(generate_session)): async def update_shopping_list(id: int, new_data: ShoppingListIn, session: Session = Depends(generate_session)):
""" Update Shopping List in the Database """ """ 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}") @router.delete("/{id}")
async def delete_shopping_list(id: int, session: Session = Depends(generate_session)): async def delete_shopping_list(id: int, session: Session = Depends(generate_session)):
""" Delete Shopping List from the Database """ """ 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 sqlalchemy.orm.session import Session
from mealie.core.dependencies import get_current_user 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.db.db_setup import generate_session
from mealie.routes.routers import AdminAPIRouter from mealie.routes.routers import AdminAPIRouter
from mealie.schema.admin import SiteSettings from mealie.schema.admin import SiteSettings
@ -16,8 +16,9 @@ admin_router = AdminAPIRouter(prefix="/api/site-settings", tags=["Settings"])
@public_router.get("") @public_router.get("")
def get_main_settings(session: Session = Depends(generate_session)): def get_main_settings(session: Session = Depends(generate_session)):
""" Returns basic site settings """ """ Returns basic site settings """
db = get_database(session)
return db.settings.get(session, 1) return db.settings.get(1)
@admin_router.put("") @admin_router.put("")
@ -26,7 +27,8 @@ def update_settings(
session: Session = Depends(generate_session), session: Session = Depends(generate_session),
): ):
""" Returns Site Settings """ """ 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") @admin_router.post("/webhooks/test")
@ -35,7 +37,8 @@ def test_webhooks(
session: Session = Depends(generate_session), session: Session = Depends(generate_session),
): ):
""" Run the function to test your webhooks """ """ 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: try:
post_webhooks(group_entry.id, session) post_webhooks(group_entry.id, session)

View File

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

View File

@ -1,8 +1,10 @@
from fastapi import APIRouter 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 = APIRouter()
router.include_router(food_routes.router, prefix="/foods", tags=["Recipes: Foods"]) router.include_router(RouterFactory(RecipeFoodService, prefix="/foods", tags=["Recipes: Foods"]))
router.include_router(unit_routes.router, prefix="/units", tags=["Recipes: Units"]) 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.dependencies import get_current_user
from mealie.core.security import create_access_token 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.db.db_setup import generate_session
from mealie.routes.routers import UserAPIRouter from mealie.routes.routers import UserAPIRouter
from mealie.schema.user import CreateToken, LoingLiveTokenIn, LongLiveTokenInDB, PrivateUser from mealie.schema.user import CreateToken, LoingLiveTokenIn, LongLiveTokenInDB, PrivateUser
@ -33,7 +33,9 @@ async def create_api_token(
parent_id=current_user.id, 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: if new_token_in_db:
return {"token": token} return {"token": token}
@ -46,13 +48,14 @@ async def delete_api_token(
session: Session = Depends(generate_session), session: Session = Depends(generate_session),
): ):
""" Delete api_token from the Database """ """ 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: if not token:
raise HTTPException(status.HTTP_404_NOT_FOUND, f"Could not locate token with id '{token_id}' in database") 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: 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} return {"token_delete": deleted_token.name}
else: else:
raise HTTPException(status.HTTP_403_FORBIDDEN) 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 import security
from mealie.core.dependencies import get_current_user from mealie.core.dependencies import get_current_user
from mealie.core.security import hash_password 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.db.db_setup import generate_session
from mealie.routes.routers import AdminAPIRouter, UserAPIRouter from mealie.routes.routers import AdminAPIRouter, UserAPIRouter
from mealie.routes.users._helpers import assert_user_change_allowed 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]) @admin_router.get("", response_model=list[UserOut])
async def get_all_users(session: Session = Depends(generate_session)): 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) @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 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) @admin_router.get("/{id}", response_model=UserOut)
async def get_user(id: int, session: Session = Depends(generate_session)): 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}") @admin_router.delete("/{id}")
@ -56,7 +59,8 @@ def delete_user(
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="SUPER_USER") raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="SUPER_USER")
try: 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) background_tasks.add_task(create_user_event, "User Deleted", f"User ID: {id}", session=session)
except Exception: except Exception:
raise HTTPException(status.HTTP_400_BAD_REQUEST) raise HTTPException(status.HTTP_400_BAD_REQUEST)
@ -87,7 +91,9 @@ async def update_user(
# prevent an admin from demoting themself # prevent an admin from demoting themself
raise HTTPException(status.HTTP_403_FORBIDDEN) 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: if current_user.id == id:
access_token = security.create_access_token(data=dict(sub=new_data.email)) access_token = security.create_access_token(data=dict(sub=new_data.email))
return {"access_token": access_token, "token_type": "bearer"} 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 sqlalchemy.orm.session import Session
from mealie.core.dependencies import get_current_user 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.db.db_setup import generate_session
from mealie.routes.routers import UserAPIRouter from mealie.routes.routers import UserAPIRouter
from mealie.routes.users._helpers import assert_user_change_allowed 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) @user_router.get("/{id}/favorites", response_model=UserFavorites)
async def get_favorites(id: str, session: Session = Depends(generate_session)): async def get_favorites(id: str, session: Session = Depends(generate_session)):
""" Get user's favorite recipes """ """ Get user's favorite recipes """
db = get_database(session)
return db.users.get(session, id, override_schema=UserFavorites) return db.users.get(id, override_schema=UserFavorites)
@user_router.post("/{id}/favorites/{slug}") @user_router.post("/{id}/favorites/{slug}")
@ -29,7 +29,8 @@ def add_favorite(
assert_user_change_allowed(id, current_user) assert_user_change_allowed(id, current_user)
current_user.favorite_recipes.append(slug) 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}") @user_router.delete("/{id}/favorites/{slug}")
@ -43,6 +44,7 @@ def remove_favorite(
assert_user_change_allowed(id, current_user) assert_user_change_allowed(id, current_user)
current_user.favorite_recipes = [x for x in current_user.favorite_recipes if x != slug] 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 return

View File

@ -3,7 +3,7 @@ from sqlalchemy.orm.session import Session
from mealie.core.config import settings from mealie.core.config import settings
from mealie.core.security import hash_password 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.db.db_setup import generate_session
from mealie.routes.routers import UserAPIRouter from mealie.routes.routers import UserAPIRouter
from mealie.schema.user import ChangePassword from mealie.schema.user import ChangePassword
@ -15,7 +15,9 @@ user_router = UserAPIRouter(prefix="")
@user_router.put("/{id}/reset-password") @user_router.put("/{id}/reset-password")
async def reset_user_password(id: int, session: Session = Depends(generate_session)): async def reset_user_password(id: int, session: Session = Depends(generate_session)):
new_password = hash_password(settings.DEFAULT_PASSWORD) 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") @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.config import get_app_dirs, get_settings
from mealie.core.root_logger import get_logger 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.database import get_database
from mealie.db.db_setup import SessionLocal from mealie.db.db_setup import SessionLocal
from mealie.schema.user.user import PrivateUser from mealie.schema.user.user import PrivateUser
@ -45,8 +44,6 @@ class BaseHttpService(Generic[T, D], ABC):
delete_one: Callable = None delete_one: Callable = None
delete_all: Callable = None delete_all: Callable = None
db_access: DatabaseAccessLayer = None
# Type Definitions # Type Definitions
_schema = None _schema = None
@ -64,7 +61,7 @@ class BaseHttpService(Generic[T, D], ABC):
self.background_tasks = background_tasks self.background_tasks = background_tasks
# Static Globals Dependency Injection # Static Globals Dependency Injection
self.db = get_database() self.db = get_database(session)
self.app_dirs = get_app_dirs() self.app_dirs = get_app_dirs()
self.settings = get_settings() self.settings = get_settings()
@ -110,7 +107,7 @@ class BaseHttpService(Generic[T, D], ABC):
def group_id(self): def group_id(self):
# TODO: Populate Group in Private User Call WARNING: May require significant refactoring # TODO: Populate Group in Private User Call WARNING: May require significant refactoring
if not self._group_id_cache: 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 self._group_id_cache = group.id
return self._group_id_cache return self._group_id_cache

View File

@ -1,27 +1,37 @@
from abc import ABC, abstractmethod
from typing import Generic, TypeVar from typing import Generic, TypeVar
from fastapi import HTTPException, status from fastapi import HTTPException, status
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from mealie.core.root_logger import get_logger 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) C = TypeVar("C", bound=BaseModel)
R = TypeVar("R", bound=BaseModel) R = TypeVar("R", bound=BaseModel)
U = TypeVar("U", bound=BaseModel) U = TypeVar("U", bound=BaseModel)
DAL = TypeVar("DAL", bound=DatabaseAccessLayer) DAL = TypeVar("DAL", bound=AccessModel)
logger = get_logger() logger = get_logger()
class CrudHttpMixins(Generic[C, R, U]): class CrudHttpMixins(Generic[C, R, U], ABC):
item: C item: R
session: Session 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: def _create_one(self, data: C, exception_msg="generic-create-error") -> R:
try: try:
self.item = self.dal.create(self.session, data) self.item = self.dal.create(data)
except Exception as ex: except Exception as ex:
logger.exception(ex) logger.exception(ex)
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail={"message": exception_msg, "exception": str(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 return
target_id = item_id or self.item.id 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 return self.item
def _patch_one(self) -> None: def _patch_one(self, data: U, item_id: int) -> None:
raise NotImplementedError try:
self.item = self.dal.patch(item_id, data.dict(exclude_unset=True, exclude_defaults=True))
def _delete_one(self, item_id: int = None) -> None: except IntegrityError:
if not self.item: raise HTTPException(status.HTTP_400_BAD_REQUEST, detail={"message": "generic-patch-error"})
return
def _delete_one(self, item_id: int = None) -> R:
target_id = item_id or self.item.id 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 return self.item

View File

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

View File

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

View File

@ -1,13 +1,12 @@
import apprise import apprise
from sqlalchemy.orm.session import Session 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.db.db_setup import create_session
from mealie.schema.events import Event, EventCategory from mealie.schema.events import Event, EventCategory
def test_notification(notification_url, event=None) -> bool: def test_notification(notification_url, event=None) -> bool:
if event is None: if event is None:
event = Event( event = Event(
title="Test Notification", 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): def save_event(title, text, category, session: Session, attachment=None):
event = Event(title=title, text=text, category=category) event = Event(title=title, text=text, category=category)
session = session or create_session() 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] notification_urls = [x.notification_url for x in notification_objects]
post_notifications(event, notification_urls, attachment=attachment) post_notifications(event, notification_urls, attachment=attachment)

View File

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

View File

@ -2,7 +2,7 @@ from __future__ import annotations
from uuid import uuid4 from uuid import uuid4
from fastapi import Depends, HTTPException, status from fastapi import Depends
from mealie.core.dependencies.grouped import UserDeps from mealie.core.dependencies.grouped import UserDeps
from mealie.core.root_logger import get_logger from mealie.core.root_logger import get_logger
@ -17,7 +17,7 @@ logger = get_logger(module=__name__)
class GroupSelfService(UserHttpService[int, str]): class GroupSelfService(UserHttpService[int, str]):
_restrict_by_group = True _restrict_by_group = False
event_func = create_group_event event_func = create_group_event
item: GroupInDB item: GroupInDB
@ -31,31 +31,21 @@ class GroupSelfService(UserHttpService[int, str]):
"""Override parent method to remove `item_id` from arguments""" """Override parent method to remove `item_id` from arguments"""
return super().write_existing(item_id=0, deps=deps) 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: 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 return self.item
def update_categories(self, new_categories: list[CategoryBase]): def update_categories(self, new_categories: list[CategoryBase]):
self.item.categories = new_categories 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): 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() return self.populate_item()
def create_invite_token(self, uses: int = 1) -> None: def create_invite_token(self, uses: int = 1) -> None:
token = SaveInviteToken(uses_left=uses, group_id=self.group_id, token=uuid4().hex) 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]: 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.group.group_preferences import CreateGroupPreferences
from mealie.schema.user.user import GroupBase, GroupInDB from mealie.schema.user.user import GroupBase, GroupInDB
def create_new_group(session, g_base: GroupBase, g_preferences: CreateGroupPreferences = None) -> GroupInDB: def create_new_group(db: Database, g_base: GroupBase, g_preferences: CreateGroupPreferences = None) -> GroupInDB:
db = get_database() created_group = db.groups.create(g_base)
created_group = db.groups.create(session, g_base)
g_preferences = g_preferences or CreateGroupPreferences(group_id=0) # Assign Temporary ID before group is created g_preferences = g_preferences or CreateGroupPreferences(group_id=0) # Assign Temporary ID before group is created
g_preferences.group_id = created_group.id g_preferences.group_id = created_group.id
db.group_preferences.create(session, g_preferences) db.group_preferences.create(g_preferences)
return created_group return created_group

View File

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

View File

@ -1,7 +1,8 @@
from __future__ import annotations from __future__ import annotations
from functools import cached_property
from mealie.core.root_logger import get_logger 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 import ReadWebhook
from mealie.schema.group.webhook import CreateWebhook, SaveWebhook from mealie.schema.group.webhook import CreateWebhook, SaveWebhook
from mealie.services._base_http_service.crud_http_mixins import CrudHttpMixins 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__) 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 event_func = create_group_event
_restrict_by_group = True _restrict_by_group = True
_schema = ReadWebhook _schema = ReadWebhook
def __init__(self, *args, **kwargs): @cached_property
super().__init__(*args, **kwargs) def dal(self):
self.dal = get_database().webhooks return self.db.webhooks
def populate_item(self, id: int) -> ReadWebhook: 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 return self.item
def get_all(self) -> list[ReadWebhook]: 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: def create_one(self, data: CreateWebhook) -> ReadWebhook:
data = self.cast(data, SaveWebhook) 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 pydantic import BaseModel
from mealie.core import root_logger 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.admin import MigrationImport
from mealie.schema.recipe import Recipe from mealie.schema.recipe import Recipe
from mealie.schema.user.user import PrivateUser from mealie.schema.user.user import PrivateUser
@ -37,6 +37,10 @@ class MigrationBase(BaseModel):
user: PrivateUser user: PrivateUser
@property
def db(self):
return get_database(self.session)
@property @property
def temp_dir(self) -> TemporaryDirectory: def temp_dir(self) -> TemporaryDirectory:
"""unpacks the migration_file into a temporary directory """unpacks the migration_file into a temporary directory
@ -66,7 +70,7 @@ class MigrationBase(BaseModel):
with open(yaml_file, "r") as f: with open(yaml_file, "r") as f:
contents = f.read().split("---") contents = f.read().split("---")
recipe_data = {} recipe_data = {}
for x, document in enumerate(contents): for _, document in enumerate(contents):
# Check if None or Empty String # Check if None or Empty String
if document is None or document == "": if document is None or document == "":
@ -172,7 +176,7 @@ class MigrationBase(BaseModel):
exception = "" exception = ""
status = False status = False
try: try:
db.recipes.create(self.session, recipe.dict()) self.db.recipes.create(recipe.dict())
status = True status = True
except Exception as inst: 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 pathlib import Path
from shutil import copytree, rmtree from shutil import copytree, rmtree
from typing import Union from typing import Union
from fastapi import Depends, HTTPException, status from fastapi import Depends, HTTPException, status
from sqlalchemy.exc import IntegrityError
from mealie.core.dependencies.grouped import PublicDeps, UserDeps from mealie.core.dependencies.grouped import PublicDeps, UserDeps
from mealie.core.root_logger import get_logger 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.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._base_http_service.http_services import UserHttpService
from mealie.services.events import create_recipe_event from mealie.services.events import create_recipe_event
from mealie.services.recipe.mixins import recipe_creation_factory 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__) logger = get_logger(module=__name__)
class RecipeService(UserHttpService[str, Recipe]): class RecipeService(CrudHttpMixins[CreateRecipe, Recipe, Recipe], UserHttpService[str, Recipe]):
""" """
Class Methods: Class Methods:
`read_existing`: Reads an existing recipe from the database. `read_existing`: Reads an existing recipe from the database.
@ -25,6 +27,10 @@ class RecipeService(UserHttpService[str, Recipe]):
event_func = create_recipe_event event_func = create_recipe_event
@cached_property
def dal(self) -> RecipeDataAccessModel:
return self.db.recipes
@classmethod @classmethod
def write_existing(cls, slug: str, deps: UserDeps = Depends()): def write_existing(cls, slug: str, deps: UserDeps = Depends()):
return super().write_existing(slug, deps) return super().write_existing(slug, deps)
@ -35,75 +41,49 @@ class RecipeService(UserHttpService[str, Recipe]):
def assert_existing(self, slug: str): def assert_existing(self, slug: str):
self.populate_item(slug) self.populate_item(slug)
if not self.item: if not self.item:
raise HTTPException(status.HTTP_404_NOT_FOUND) raise HTTPException(status.HTTP_404_NOT_FOUND)
if not self.item.settings.public and not self.user: if not self.item.settings.public and not self.user:
raise HTTPException(status.HTTP_403_FORBIDDEN) 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 # CRUD METHODS
def get_all(self, start=0, limit=None): def get_all(self, start=0, limit=None):
return self.db.recipes.multi_query( 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: 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()) create_data = recipe_creation_factory(self.user, name=create_data.name, additional_attrs=create_data.dict())
self._create_one(create_data, "RECIPE_ALREAD_EXISTS")
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_event( self._create_event(
"Recipe Created", "Recipe Created",
f"'{self.item.name}' by {self.user.username} \n {self.settings.BASE_URL}/recipe/{self.item.slug}", f"'{self.item.name}' by {self.user.username} \n {self.settings.BASE_URL}/recipe/{self.item.slug}",
) )
return self.item return self.item
def update_one(self, update_data: Recipe) -> Recipe: def update_one(self, update_data: Recipe) -> Recipe:
original_slug = self.item.slug original_slug = self.item.slug
self._update_one(update_data, original_slug)
try: self.check_assets(original_slug)
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)
return self.item return self.item
def patch_one(self, patch_data: Recipe) -> Recipe: def patch_one(self, patch_data: Recipe) -> Recipe:
original_slug = self.item.slug original_slug = self.item.slug
self._patch_one(patch_data, original_slug)
try: self.check_assets(original_slug)
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)
return self.item return self.item
def delete_one(self) -> Recipe: def delete_one(self) -> Recipe:
try: self._delete_one(self.item.slug)
recipe: Recipe = self.db.recipes.delete(self.session, self.item.slug) self.delete_assets()
self._delete_assets() self._create_event("Recipe Delete", f"'{self.item.name}' deleted by {self.user.full_name}")
except Exception: return self.item
raise HTTPException(status.HTTP_400_BAD_REQUEST)
self._create_event("Recipe Delete", f"'{recipe.name}' deleted by {self.user.full_name}") def check_assets(self, original_slug: str) -> None:
return recipe
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.""" """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: if original_slug != self.item.slug:
current_dir = self.app_dirs.RECIPE_DATA_DIR.joinpath(original_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: if file.name not in all_asset_files:
file.unlink() file.unlink()
def _delete_assets(self) -> None: def delete_assets(self) -> None:
recipe_dir = self.item.directory recipe_dir = self.item.directory
rmtree(recipe_dir, ignore_errors=True) rmtree(recipe_dir, ignore_errors=True)
logger.info(f"Recipe Directory Removed: {self.item.slug}") 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 apscheduler.schedulers.background import BackgroundScheduler
from mealie.core import root_logger 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.db_setup import create_session
from mealie.db.models.event import Event from mealie.db.models.event import Event
from mealie.schema.user import GroupInDB from mealie.schema.user import GroupInDB
@ -39,7 +39,8 @@ def update_webhook_schedule():
poll the database for changes and reschedule the webhook time poll the database for changes and reschedule the webhook time
""" """
session = create_session() 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: 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): def init_webhook_schedule(scheduler, job_store: dict):
session = create_session() 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: for group in all_groups:
job_store.update(add_group_to_schedule(scheduler, group)) 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 != "": 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) print("Token Entry", token_entry)
if not token_entry: if not token_entry:
raise HTTPException(status.HTTP_400_BAD_REQUEST, {"message": "Invalid group token"}) 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: else:
raise HTTPException(status.HTTP_400_BAD_REQUEST, {"message": "Missing group"}) 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 token_entry.uses_left = token_entry.uses_left - 1
if token_entry.uses_left == 0: 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: 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 return user
@ -64,7 +64,7 @@ class RegistrationService(PublicHttpService[int, str]):
group=group.name, 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: def _register_new_group(self) -> GroupInDB:
group_data = GroupBase(name=self.registration.group) group_data = GroupBase(name=self.registration.group)
@ -81,4 +81,4 @@ class RegistrationService(PublicHttpService[int, str]):
recipe_disable_amount=self.registration.advanced, 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): def _populate_target_user(self, id: int = None):
if id: 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: if not self.target_user:
raise HTTPException(status.HTTP_404_NOT_FOUND) raise HTTPException(status.HTTP_404_NOT_FOUND)
else: else:
@ -38,4 +38,4 @@ class UserService(UserHttpService[int, str]):
self.target_user.password = hash_password(password_change.new_password) 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 import requests
from sqlalchemy.orm.session import Session 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.db.db_setup import create_session
from mealie.schema.user import GroupInDB from mealie.schema.user import GroupInDB
from mealie.services.events import create_scheduled_event 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): def post_webhooks(group: int, session: Session = None, force=True):
session = session or create_session() 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: if not group_settings.webhook_enable and not force:
return return
todays_recipe = get_todays_meal(session, group) # TODO: Fix Mealplan Webhooks
todays_recipe = None
if not todays_recipe: if not todays_recipe:
return return

593
poetry.lock generated
View File

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