From 190773c5d762c7e0d84dea1c571e7fcb4a3cb39d Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Sun, 9 Jan 2022 21:04:24 -0900 Subject: [PATCH] Feature/group based notifications (#918) * fix group page * setup group notification for backend * update type generators * script to auto-generate schema exports * setup frontend CRUD interface * remove old notifications UI * drop old events api * add test functionality * update naming for fields * add event dispatcher functionality * bump to python 3.10 * bump python version * purge old event code * use-async apprise * set mealie logo as image * unify styles for buttons rows * add links to banners --- .github/workflows/backend-tests.yml | 2 +- .pylintrc | 1 - Dockerfile | 2 +- dev/code-generation/_gen_utils.py | 12 +- dev/code-generation/_static.py | 1 + dev/code-generation/gen_frontend_types.py | 99 +++ dev/code-generation/gen_schema_exports.py | 35 + dev/scripts/gen_global_components.py | 58 -- dev/scripts/types_gen.py | 37 -- .../class-interfaces/event-notifications.ts | 41 -- .../class-interfaces/group-event-notifier.ts | 18 + frontend/api/index.ts | 13 +- .../Domain/Recipe/RecipeInstructions.vue | 66 +- .../Domain/User/UserProfileLinkCard.vue | 6 +- frontend/components/Layout/AppSidebar.vue | 2 +- .../components/global/BannerExperimental.vue | 16 + frontend/components/global/MarkdownEditor.vue | 51 +- frontend/composables/use-notifications.ts | 89 --- frontend/layouts/admin.vue | 5 - frontend/pages/admin/manage/groups/index.vue | 4 - frontend/pages/admin/toolbox/categories.vue | 24 - .../pages/admin/toolbox/notifications.vue | 226 ------- frontend/pages/admin/toolbox/tags.vue | 24 - frontend/pages/shopping-lists/_id.vue | 2 +- frontend/pages/user/group/cookbooks.vue | 30 +- frontend/pages/user/group/notifiers.vue | 307 +++++++++ frontend/pages/user/group/webhooks.vue | 38 +- frontend/pages/user/profile/index.vue | 12 +- frontend/static/svgs/manage-notifiers.svg | 1 + frontend/types/api-types/group.ts | 279 ++++++++ frontend/types/api-types/labels.ts | 59 ++ frontend/types/api-types/recipe.ts | 71 +- frontend/types/api-types/user.ts | 44 +- frontend/types/components.d.ts | 4 + makefile | 8 +- mealie/core/settings/db_providers.py | 2 +- mealie/db/models/event.py | 23 +- mealie/db/models/group/__init__.py | 1 + mealie/db/models/group/events.py | 61 ++ mealie/db/models/group/group.py | 5 +- mealie/repos/repository_factory.py | 13 +- mealie/repos/repository_generic.py | 18 +- mealie/routes/_base/mixins.py | 17 +- mealie/routes/about/__init__.py | 3 +- mealie/routes/about/notifications.py | 67 -- mealie/routes/groups/__init__.py | 3 +- mealie/routes/groups/notifications.py | 85 +++ mealie/routes/groups/shopping_lists.py | 14 +- mealie/schema/admin/__init__.py | 1 + mealie/schema/cookbook/__init__.py | 1 + mealie/schema/events/__init__.py | 2 +- mealie/schema/events/event_notifications.py | 61 -- mealie/schema/group/__init__.py | 8 + mealie/schema/group/group_events.py | 89 +++ mealie/schema/labels/__init__.py | 38 +- mealie/schema/labels/multi_purpose_label.py | 36 ++ mealie/schema/meal_plan/__init__.py | 1 + mealie/schema/recipe/__init__.py | 8 + mealie/schema/recipe/recipe_bulk_actions.py | 2 +- mealie/schema/recipe/recipe_category.py | 6 +- mealie/schema/reports/__init__.py | 1 + mealie/schema/response/__init__.py | 19 +- mealie/schema/response/error_response.py | 17 + mealie/schema/server/__init__.py | 1 + mealie/schema/user/__init__.py | 3 + mealie/services/backups/imports.py | 26 +- mealie/services/event_bus_service/__init__.py | 0 .../event_bus_service/event_bus_service.py | 46 ++ .../event_bus_service/message_types.py | 47 ++ .../services/event_bus_service/publisher.py | 29 + mealie/services/events.py | 38 +- poetry.lock | 610 ++++++++---------- pyproject.toml | 14 +- .../test_group_notifications.py | 118 ++++ 74 files changed, 1992 insertions(+), 1229 deletions(-) create mode 100644 dev/code-generation/gen_frontend_types.py create mode 100644 dev/code-generation/gen_schema_exports.py delete mode 100644 dev/scripts/gen_global_components.py delete mode 100644 dev/scripts/types_gen.py delete mode 100644 frontend/api/class-interfaces/event-notifications.ts create mode 100644 frontend/api/class-interfaces/group-event-notifier.ts delete mode 100644 frontend/composables/use-notifications.ts delete mode 100644 frontend/pages/admin/toolbox/categories.vue delete mode 100644 frontend/pages/admin/toolbox/notifications.vue delete mode 100644 frontend/pages/admin/toolbox/tags.vue create mode 100644 frontend/pages/user/group/notifiers.vue create mode 100644 frontend/static/svgs/manage-notifiers.svg create mode 100644 frontend/types/api-types/labels.ts create mode 100644 mealie/db/models/group/events.py delete mode 100644 mealie/routes/about/notifications.py create mode 100644 mealie/routes/groups/notifications.py delete mode 100644 mealie/schema/events/event_notifications.py create mode 100644 mealie/schema/group/group_events.py create mode 100644 mealie/schema/labels/multi_purpose_label.py create mode 100644 mealie/schema/response/error_response.py create mode 100644 mealie/services/event_bus_service/__init__.py create mode 100644 mealie/services/event_bus_service/event_bus_service.py create mode 100644 mealie/services/event_bus_service/message_types.py create mode 100644 mealie/services/event_bus_service/publisher.py create mode 100644 tests/integration_tests/user_group_tests/test_group_notifications.py diff --git a/.github/workflows/backend-tests.yml b/.github/workflows/backend-tests.yml index 1752f201..9a5ecfe2 100644 --- a/.github/workflows/backend-tests.yml +++ b/.github/workflows/backend-tests.yml @@ -43,7 +43,7 @@ jobs: - name: Set up python uses: actions/setup-python@v2 with: - python-version: 3.9 + python-version: "3.10" #---------------------------------------------- # ----- install & configure poetry ----- #---------------------------------------------- diff --git a/.pylintrc b/.pylintrc index c149059a..4d6de197 100644 --- a/.pylintrc +++ b/.pylintrc @@ -476,7 +476,6 @@ ignore-comments=yes # Ignore docstrings when computing similarities. ignore-docstrings=yes -w54 # Ignore imports when computing similarities. ignore-imports=no diff --git a/Dockerfile b/Dockerfile index c6211f4a..32fa3b40 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ ############################################### # Base Image ############################################### -FROM python:3.9-slim as python-base +FROM python:3.10-slim as python-base ENV MEALIE_HOME="/app" diff --git a/dev/code-generation/_gen_utils.py b/dev/code-generation/_gen_utils.py index ffc5466f..27718c84 100644 --- a/dev/code-generation/_gen_utils.py +++ b/dev/code-generation/_gen_utils.py @@ -1,18 +1,26 @@ +from __future__ import annotations + import re from dataclasses import dataclass from pathlib import Path from typing import Tuple import black +import isort from jinja2 import Template -def render_python_template(template_file: Path, dest: Path, data: dict) -> str: +def render_python_template(template_file: Path | str, dest: Path, data: dict) -> str: """Render and Format a Jinja2 Template for Python Code""" - tplt = Template(template_file.read_text()) + if isinstance(template_file, Path): + tplt = Template(template_file.read_text()) + else: + tplt = Template(template_file) + text = tplt.render(data=data) text = black.format_str(text, mode=black.FileMode()) dest.write_text(text) + isort.file(dest) @dataclass diff --git a/dev/code-generation/_static.py b/dev/code-generation/_static.py index c677687d..1a0ee7ee 100644 --- a/dev/code-generation/_static.py +++ b/dev/code-generation/_static.py @@ -1,6 +1,7 @@ from pathlib import Path CWD = Path(__file__).parent +PROJECT_DIR = Path(__file__).parent.parent.parent class Directories: diff --git a/dev/code-generation/gen_frontend_types.py b/dev/code-generation/gen_frontend_types.py new file mode 100644 index 00000000..1960ce3f --- /dev/null +++ b/dev/code-generation/gen_frontend_types.py @@ -0,0 +1,99 @@ +from pathlib import Path + +from jinja2 import Template +from pydantic2ts import generate_typescript_defs + +# ============================================================ +# Global Compoenents Generator + +template = """// This Code is auto generated by gen_global_components.py +{% for name in global %} import {{ name }} from "@/components/global/{{ name }}.vue"; +{% endfor %} +{% for name in layout %} import {{ name }} from "@/components/layout/{{ name }}.vue"; +{% endfor %} + +declare module "vue" { + export interface GlobalComponents { + // Global Components + {% for name in global %} {{ name }}: typeof {{ name }}; + {% endfor %} // Layout Components + {% for name in layout %} {{ name }}: typeof {{ name }}; + {% endfor %} + } +} + +export {}; +""" + +CWD = Path(__file__).parent +PROJECT_DIR = Path(__file__).parent.parent.parent + + +def generate_global_components_types() -> None: + destination_file = PROJECT_DIR / "frontend" / "types" / "components.d.ts" + + component_paths = { + "global": PROJECT_DIR / "frontend" / "components" / "global", + "layout": PROJECT_DIR / "frontend" / "components" / "Layout", + } + + def render_template(template: str, data: dict) -> None: + template = Template(template) + return template.render(**data) + + def build_data() -> dict: + data = {} + for name, path in component_paths.items(): + components = [component.stem for component in path.glob("*.vue")] + data[name] = components + + return data + + def write_template(text: str) -> None: + destination_file.write_text(text) + + text = render_template(template, build_data()) + write_template(text) + + +# ============================================================ +# Pydantic To Typescript Generator + + +def generate_typescript_types() -> None: + def path_to_module(path: Path): + path: str = str(path) + + path = path.removeprefix(str(PROJECT_DIR)) + path = path.removeprefix("/") + path = path.replace("/", ".") + + return path + + schema_path = PROJECT_DIR / "mealie" / "schema" + types_dir = PROJECT_DIR / "frontend" / "types" / "api-types" + + for module in schema_path.iterdir(): + + if not module.is_dir() or not module.joinpath("__init__.py").is_file(): + continue + + ts_out_name = module.name.replace("_", "-") + ".ts" + + out_path = types_dir.joinpath(ts_out_name) + + print(module) # noqa + try: + path_as_module = path_to_module(module) + generate_typescript_defs(path_as_module, str(out_path), exclude=("CamelModel")) + except Exception as e: + print(f"Failed to generate {module}") # noqa + print(e) # noqa + + +if __name__ == "__main__": + print("\n-- Starting Global Components Generator --") # noqa + generate_global_components_types() + + print("\n-- Starting Pydantic To Typescript Generator --") # noqa + generate_typescript_types() diff --git a/dev/code-generation/gen_schema_exports.py b/dev/code-generation/gen_schema_exports.py new file mode 100644 index 00000000..063e4d34 --- /dev/null +++ b/dev/code-generation/gen_schema_exports.py @@ -0,0 +1,35 @@ +from _gen_utils import render_python_template +from _static import PROJECT_DIR + +template = """# GENERATED CODE - DO NOT MODIFY BY HAND +{% for file in data.files %}from .{{ file }} import * +{% endfor %} +""" + +SCHEMA_PATH = PROJECT_DIR / "mealie" / "schema" + + +def generate_init_files() -> None: + + for schema in SCHEMA_PATH.iterdir(): + if not schema.is_dir(): + print(f"Skipping {schema}") + continue + + print(f"Generating {schema}") + init_file = schema.joinpath("__init__.py") + + module_files = [ + f.stem for f in schema.iterdir() if f.is_file() and f.suffix == ".py" and not f.stem.startswith("_") + ] + render_python_template(template, init_file, {"files": module_files}) + + +def main(): + print("Starting...") + generate_init_files() + print("Finished...") + + +if __name__ == "__main__": + main() diff --git a/dev/scripts/gen_global_components.py b/dev/scripts/gen_global_components.py deleted file mode 100644 index 4acdbb20..00000000 --- a/dev/scripts/gen_global_components.py +++ /dev/null @@ -1,58 +0,0 @@ -from pathlib import Path - -from jinja2 import Template - -template = """// This Code is auto generated by gen_global_components.py -{% for name in global %} import {{ name }} from "@/components/global/{{ name }}.vue"; -{% endfor %} -{% for name in layout %} import {{ name }} from "@/components/layout/{{ name }}.vue"; -{% endfor %} - -declare module "vue" { - export interface GlobalComponents { - // Global Components - {% for name in global %} {{ name }}: typeof {{ name }}; - {% endfor %} // Layout Components - {% for name in layout %} {{ name }}: typeof {{ name }}; - {% endfor %} - } -} - -export {}; -""" - -project_dir = Path(__file__).parent.parent.parent - -destination_file = project_dir / "frontend" / "types" / "components.d.ts" - -component_paths = { - "global": project_dir / "frontend" / "components" / "global", - "layout": project_dir / "frontend" / "components" / "Layout", -} - - -def render_template(template: str, data: dict) -> None: - template = Template(template) - - return template.render(**data) - - -def build_data(component_paths: dict) -> dict: - data = {} - for name, path in component_paths.items(): - components = [] - for component in path.glob("*.vue"): - components.append(component.stem) - data[name] = components - - return data - - -def write_template(text: str) -> None: - destination_file.write_text(text) - - -if __name__ == "__main__": - data = build_data(component_paths) - text = render_template(template, build_data(component_paths)) - write_template(text) diff --git a/dev/scripts/types_gen.py b/dev/scripts/types_gen.py deleted file mode 100644 index db470695..00000000 --- a/dev/scripts/types_gen.py +++ /dev/null @@ -1,37 +0,0 @@ -from pathlib import Path - -from pydantic2ts import generate_typescript_defs - -CWD = Path(__file__).parent - -PROJECT_DIR = Path(__file__).parent.parent.parent -SCHEMA_PATH = PROJECT_DIR / "mealie" / "schema" - -TYPES_DIR = CWD / "output" / "types" / "api-types" - - -def path_to_module(path: Path): - path: str = str(path) - - path = path.removeprefix(str(PROJECT_DIR)) - path = path.removeprefix("/") - path = path.replace("/", ".") - - return path - - -for module in SCHEMA_PATH.iterdir(): - - if not module.is_dir() or not module.joinpath("__init__.py").is_file(): - continue - - ts_out_name = module.name.replace("_", "-") + ".ts" - - out_path = TYPES_DIR.joinpath(ts_out_name) - - print(module) - try: - path_as_module = path_to_module(module) - generate_typescript_defs(path_as_module, str(out_path), exclude=("CamelModel")) - except Exception: - pass diff --git a/frontend/api/class-interfaces/event-notifications.ts b/frontend/api/class-interfaces/event-notifications.ts deleted file mode 100644 index 09112bc0..00000000 --- a/frontend/api/class-interfaces/event-notifications.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { BaseCRUDAPI } from "../_base"; - -export type EventCategory = "general" | "recipe" | "backup" | "scheduled" | "migration" | "group" | "user"; -export type DeclaredTypes = "General" | "Discord" | "Gotify" | "Pushover" | "Home Assistant"; -export type GotifyPriority = "low" | "moderate" | "normal" | "high"; - -export interface EventNotification { - id?: number; - name?: string; - type?: DeclaredTypes & string; - general?: boolean; - recipe?: boolean; - backup?: boolean; - scheduled?: boolean; - migration?: boolean; - group?: boolean; - user?: boolean; -} - -export interface CreateEventNotification extends EventNotification { - notificationUrl?: string; -} - -const prefix = "/api"; - -const routes = { - aboutEventsNotifications: `${prefix}/about/events/notifications`, - aboutEventsNotificationsTest: `${prefix}/about/events/notifications/test`, - - aboutEventsNotificationsId: (id: number) => `${prefix}/about/events/notifications/${id}`, -}; - -export class NotificationsAPI extends BaseCRUDAPI { - baseRoute = routes.aboutEventsNotifications; - itemRoute = routes.aboutEventsNotificationsId; - /** Returns the Group Data for the Current User - */ - async testNotification(id: number | null = null, testUrl: string | null = null) { - return await this.requests.post(routes.aboutEventsNotificationsTest, { id, testUrl }); - } -} diff --git a/frontend/api/class-interfaces/group-event-notifier.ts b/frontend/api/class-interfaces/group-event-notifier.ts new file mode 100644 index 00000000..8d0ffa64 --- /dev/null +++ b/frontend/api/class-interfaces/group-event-notifier.ts @@ -0,0 +1,18 @@ +import { BaseCRUDAPI } from "../_base"; +import { GroupEventNotifierCreate, GroupEventNotifierOut } from "~/types/api-types/group"; + +const prefix = "/api"; + +const routes = { + eventNotifier: `${prefix}/groups/events/notifications`, + eventNotifierId: (id: string | number) => `${prefix}/groups/events/notifications/${id}`, +}; + +export class GroupEventNotifierApi extends BaseCRUDAPI { + baseRoute = routes.eventNotifier; + itemRoute = routes.eventNotifierId; + + async test(itemId: string) { + return await this.requests.post(`${this.baseRoute}/${itemId}/test`, {}); + } +} diff --git a/frontend/api/index.ts b/frontend/api/index.ts index 5de55dfc..32ccea77 100644 --- a/frontend/api/index.ts +++ b/frontend/api/index.ts @@ -7,7 +7,6 @@ import { UploadFile } from "./class-interfaces/upload"; import { CategoriesAPI } from "./class-interfaces/categories"; import { TagsAPI } from "./class-interfaces/tags"; import { UtilsAPI } from "./class-interfaces/utils"; -import { NotificationsAPI } from "./class-interfaces/event-notifications"; import { FoodAPI } from "./class-interfaces/recipe-foods"; import { UnitAPI } from "./class-interfaces/recipe-units"; import { CookbookAPI } from "./class-interfaces/group-cookbooks"; @@ -23,10 +22,10 @@ import { GroupMigrationApi } from "./class-interfaces/group-migrations"; import { GroupReportsApi } from "./class-interfaces/group-reports"; import { ShoppingApi } from "./class-interfaces/group-shopping-lists"; import { MultiPurposeLabelsApi } from "./class-interfaces/group-multiple-purpose-labels"; +import { GroupEventNotifierApi } from "./class-interfaces/group-event-notifier"; import { ApiRequestInstance } from "~/types/api"; class Api { - // private static instance: Api; public recipes: RecipeAPI; public users: UserApi; public groups: GroupAPI; @@ -35,7 +34,6 @@ class Api { public categories: CategoriesAPI; public tags: TagsAPI; public utils: UtilsAPI; - public notifications: NotificationsAPI; public foods: FoodAPI; public units: UnitAPI; public cookbooks: CookbookAPI; @@ -50,14 +48,10 @@ class Api { public tools: ToolsApi; public shopping: ShoppingApi; public multiPurposeLabels: MultiPurposeLabelsApi; - // Utils + public groupEventNotifier: GroupEventNotifierApi; public upload: UploadFile; constructor(requests: ApiRequestInstance) { - // if (Api.instance instanceof Api) { - // return Api.instance; - // } - // Recipes this.recipes = new RecipeAPI(requests); this.categories = new CategoriesAPI(requests); @@ -84,7 +78,6 @@ class Api { // Admin this.events = new EventsAPI(requests); this.backups = new BackupAPI(requests); - this.notifications = new NotificationsAPI(requests); // Utils this.upload = new UploadFile(requests); @@ -92,9 +85,9 @@ class Api { this.email = new EmailAPI(requests); this.bulk = new BulkActionsAPI(requests); + this.groupEventNotifier = new GroupEventNotifierApi(requests); Object.freeze(this); - // Api.instance = this; } } diff --git a/frontend/components/Domain/Recipe/RecipeInstructions.vue b/frontend/components/Domain/Recipe/RecipeInstructions.vue index ba075ed8..98868c86 100644 --- a/frontend/components/Domain/Recipe/RecipeInstructions.vue +++ b/frontend/components/Domain/Recipe/RecipeInstructions.vue @@ -107,19 +107,43 @@ {{ $t("recipe.step-index", { step: index + 1 }) }} -
- - -
- {{ $globals.icons.arrowUpDown }} + + {{ $globals.icons.checkboxMarkedCircle }} @@ -127,7 +151,11 @@ - +
([]); + + function togglePreviewState(index: number) { + const temp = [...previewStates.value]; + temp[index] = !temp[index]; + previewStates.value = temp; + } + return { + togglePreviewState, + previewStates, ...toRefs(state), actionEvents, activeRefs, diff --git a/frontend/components/Domain/User/UserProfileLinkCard.vue b/frontend/components/Domain/User/UserProfileLinkCard.vue index dd8adf89..39af0323 100644 --- a/frontend/components/Domain/User/UserProfileLinkCard.vue +++ b/frontend/components/Domain/User/UserProfileLinkCard.vue @@ -1,9 +1,9 @@ + + + \ No newline at end of file diff --git a/frontend/components/global/MarkdownEditor.vue b/frontend/components/global/MarkdownEditor.vue index 1bd3b18a..9147399f 100644 --- a/frontend/components/global/MarkdownEditor.vue +++ b/frontend/components/global/MarkdownEditor.vue @@ -1,17 +1,19 @@ - +
diff --git a/frontend/pages/user/group/cookbooks.vue b/frontend/pages/user/group/cookbooks.vue index 2b548699..be71fbe3 100644 --- a/frontend/pages/user/group/cookbooks.vue +++ b/frontend/pages/user/group/cookbooks.vue @@ -11,7 +11,7 @@ - +
@@ -23,8 +23,8 @@ {{ $globals.icons.arrowUpDown }} - - + + {{ $globals.icons.edit }} @@ -38,8 +38,22 @@ - - + @@ -70,9 +84,3 @@ export default defineComponent({ }, }); - - \ No newline at end of file diff --git a/frontend/pages/user/group/notifiers.vue b/frontend/pages/user/group/notifiers.vue new file mode 100644 index 00000000..8ba4fd90 --- /dev/null +++ b/frontend/pages/user/group/notifiers.vue @@ -0,0 +1,307 @@ + + diff --git a/frontend/pages/user/group/webhooks.vue b/frontend/pages/user/group/webhooks.vue index 06ebdcf9..a3f04c09 100644 --- a/frontend/pages/user/group/webhooks.vue +++ b/frontend/pages/user/group/webhooks.vue @@ -11,7 +11,7 @@ - +
@@ -20,8 +20,8 @@ {{ webhook.name }} - {{ webhook.time }}
- + - diff --git a/frontend/static/svgs/manage-notifiers.svg b/frontend/static/svgs/manage-notifiers.svg new file mode 100644 index 00000000..b3347bbf --- /dev/null +++ b/frontend/static/svgs/manage-notifiers.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/types/api-types/group.ts b/frontend/types/api-types/group.ts index 672f9244..963f8e33 100644 --- a/frontend/types/api-types/group.ts +++ b/frontend/types/api-types/group.ts @@ -5,12 +5,211 @@ /* Do not modify it by hand - just update the pydantic models and then re-run the script */ +export type SupportedMigrations = "nextcloud" | "chowdown" | "paprika" | "mealie_alpha"; + +export interface CreateGroupPreferences { + privateGroup?: boolean; + firstDayOfWeek?: number; + recipePublic?: boolean; + recipeShowNutrition?: boolean; + recipeShowAssets?: boolean; + recipeLandscapeView?: boolean; + recipeDisableComments?: boolean; + recipeDisableAmount?: boolean; + groupId: string; +} +export interface CreateInviteToken { + uses: number; +} export interface CreateWebhook { enabled?: boolean; name?: string; url?: string; time?: string; } +export interface DataMigrationCreate { + sourceType: SupportedMigrations; +} +export interface EmailInitationResponse { + success: boolean; + error?: string; +} +export interface EmailInvitation { + email: string; + token: string; +} +export interface GroupAdminUpdate { + id: string; + name: string; + preferences: UpdateGroupPreferences; +} +export interface UpdateGroupPreferences { + privateGroup?: boolean; + firstDayOfWeek?: number; + recipePublic?: boolean; + recipeShowNutrition?: boolean; + recipeShowAssets?: boolean; + recipeLandscapeView?: boolean; + recipeDisableComments?: boolean; + recipeDisableAmount?: boolean; +} +export interface GroupDataExport { + id: string; + groupId: string; + name: string; + filename: string; + path: string; + size: string; + expires: string; +} +export interface GroupEventNotifierCreate { + name: string; + appriseUrl: string; +} +/** + * These events are in-sync with the EventTypes found in the EventBusService. + * If you modify this, make sure to update the EventBusService as well. + */ +export interface GroupEventNotifierOptions { + recipeCreated?: boolean; + recipeUpdated?: boolean; + recipeDeleted?: boolean; + userSignup?: boolean; + dataMigrations?: boolean; + dataExport?: boolean; + dataImport?: boolean; + mealplanEntryCreated?: boolean; + shoppingListCreated?: boolean; + shoppingListUpdated?: boolean; + shoppingListDeleted?: boolean; + cookbookCreated?: boolean; + cookbookUpdated?: boolean; + cookbookDeleted?: boolean; + tagCreated?: boolean; + tagUpdated?: boolean; + tagDeleted?: boolean; + categoryCreated?: boolean; + categoryUpdated?: boolean; + categoryDeleted?: boolean; +} +/** + * These events are in-sync with the EventTypes found in the EventBusService. + * If you modify this, make sure to update the EventBusService as well. + */ +export interface GroupEventNotifierOptionsOut { + recipeCreated?: boolean; + recipeUpdated?: boolean; + recipeDeleted?: boolean; + userSignup?: boolean; + dataMigrations?: boolean; + dataExport?: boolean; + dataImport?: boolean; + mealplanEntryCreated?: boolean; + shoppingListCreated?: boolean; + shoppingListUpdated?: boolean; + shoppingListDeleted?: boolean; + cookbookCreated?: boolean; + cookbookUpdated?: boolean; + cookbookDeleted?: boolean; + tagCreated?: boolean; + tagUpdated?: boolean; + tagDeleted?: boolean; + categoryCreated?: boolean; + categoryUpdated?: boolean; + categoryDeleted?: boolean; + id: string; +} +/** + * These events are in-sync with the EventTypes found in the EventBusService. + * If you modify this, make sure to update the EventBusService as well. + */ +export interface GroupEventNotifierOptionsSave { + recipeCreated?: boolean; + recipeUpdated?: boolean; + recipeDeleted?: boolean; + userSignup?: boolean; + dataMigrations?: boolean; + dataExport?: boolean; + dataImport?: boolean; + mealplanEntryCreated?: boolean; + shoppingListCreated?: boolean; + shoppingListUpdated?: boolean; + shoppingListDeleted?: boolean; + cookbookCreated?: boolean; + cookbookUpdated?: boolean; + cookbookDeleted?: boolean; + tagCreated?: boolean; + tagUpdated?: boolean; + tagDeleted?: boolean; + categoryCreated?: boolean; + categoryUpdated?: boolean; + categoryDeleted?: boolean; + notifierId: string; +} +export interface GroupEventNotifierOut { + id: string; + name: string; + enabled: boolean; + groupId: string; + options: GroupEventNotifierOptionsOut; +} +export interface GroupEventNotifierPrivate { + id: string; + name: string; + enabled: boolean; + groupId: string; + options: GroupEventNotifierOptionsOut; + appriseUrl: string; +} +export interface GroupEventNotifierSave { + name: string; + appriseUrl: string; + enabled?: boolean; + groupId: string; + options?: GroupEventNotifierOptions; +} +export interface GroupEventNotifierUpdate { + name: string; + appriseUrl?: string; + enabled?: boolean; + groupId: string; + options?: GroupEventNotifierOptions; + id: string; +} +export interface IngredientFood { + name: string; + description?: string; + id: number; +} +export interface IngredientUnit { + name: string; + description?: string; + fraction?: boolean; + abbreviation?: string; + id: number; +} +export interface MultiPurposeLabelSummary { + name: string; + groupId: string; + id: string; +} +export interface ReadGroupPreferences { + privateGroup?: boolean; + firstDayOfWeek?: number; + recipePublic?: boolean; + recipeShowNutrition?: boolean; + recipeShowAssets?: boolean; + recipeLandscapeView?: boolean; + recipeDisableComments?: boolean; + recipeDisableAmount?: boolean; + groupId: string; + id: number; +} +export interface ReadInviteToken { + token: string; + usesLeft: number; + groupId: string; +} export interface ReadWebhook { enabled?: boolean; name?: string; @@ -19,6 +218,11 @@ export interface ReadWebhook { groupId: string; id: number; } +export interface SaveInviteToken { + usesLeft: number; + groupId: string; + token: string; +} export interface SaveWebhook { enabled?: boolean; name?: string; @@ -26,3 +230,78 @@ export interface SaveWebhook { time?: string; groupId: string; } +export interface SetPermissions { + userId: string; + canManage?: boolean; + canInvite?: boolean; + canOrganize?: boolean; +} +/** + * Create Shopping List + */ +export interface ShoppingListCreate { + name?: string; +} +export interface ShoppingListItemCreate { + shoppingListId: string; + checked?: boolean; + position?: number; + isFood?: boolean; + note?: string; + quantity?: number; + unitId?: number; + unit?: IngredientUnit; + foodId?: number; + food?: IngredientFood; + recipeId?: number; + labelId?: string; +} +export interface ShoppingListItemOut { + shoppingListId: string; + checked?: boolean; + position?: number; + isFood?: boolean; + note?: string; + quantity?: number; + unitId?: number; + unit?: IngredientUnit; + foodId?: number; + food?: IngredientFood; + recipeId?: number; + labelId?: string; + id: string; + label?: MultiPurposeLabelSummary; +} +/** + * Create Shopping List + */ +export interface ShoppingListOut { + name?: string; + groupId: string; + id: string; + listItems?: ShoppingListItemOut[]; +} +/** + * Create Shopping List + */ +export interface ShoppingListSave { + name?: string; + groupId: string; +} +/** + * Create Shopping List + */ +export interface ShoppingListSummary { + name?: string; + groupId: string; + id: string; +} +/** + * Create Shopping List + */ +export interface ShoppingListUpdate { + name?: string; + groupId: string; + id: string; + listItems?: ShoppingListItemOut[]; +} diff --git a/frontend/types/api-types/labels.ts b/frontend/types/api-types/labels.ts new file mode 100644 index 00000000..58951d2e --- /dev/null +++ b/frontend/types/api-types/labels.ts @@ -0,0 +1,59 @@ +/* tslint:disable */ +/* eslint-disable */ +/** +/* This file was automatically generated from pydantic models by running pydantic2ts. +/* Do not modify it by hand - just update the pydantic models and then re-run the script +*/ + +export interface IngredientFood { + name: string; + description?: string; + id: number; +} +export interface MultiPurposeLabelCreate { + name: string; +} +export interface MultiPurposeLabelOut { + name: string; + groupId: string; + id: string; + shoppingListItems?: ShoppingListItemOut[]; + foods?: IngredientFood[]; +} +export interface ShoppingListItemOut { + shoppingListId: string; + checked?: boolean; + position?: number; + isFood?: boolean; + note?: string; + quantity?: number; + unitId?: number; + unit?: IngredientUnit; + foodId?: number; + food?: IngredientFood; + recipeId?: number; + labelId?: string; + id: string; + label?: MultiPurposeLabelSummary; +} +export interface IngredientUnit { + name: string; + description?: string; + fraction?: boolean; + abbreviation?: string; + id: number; +} +export interface MultiPurposeLabelSummary { + name: string; + groupId: string; + id: string; +} +export interface MultiPurposeLabelSave { + name: string; + groupId: string; +} +export interface MultiPurposeLabelUpdate { + name: string; + groupId: string; + id: string; +} diff --git a/frontend/types/api-types/recipe.ts b/frontend/types/api-types/recipe.ts index dbec3d51..4290b164 100644 --- a/frontend/types/api-types/recipe.ts +++ b/frontend/types/api-types/recipe.ts @@ -5,13 +5,36 @@ /* Do not modify it by hand - just update the pydantic models and then re-run the script */ +export type ExportTypes = "json"; export type RegisteredParser = "nlp" | "brute"; +export interface AssignCategories { + recipes: string[]; + categories: CategoryBase[]; +} export interface CategoryBase { name: string; id: number; slug: string; } +export interface AssignTags { + recipes: string[]; + tags: TagBase[]; +} +export interface TagBase { + name: string; + id: number; + slug: string; +} +export interface BulkActionError { + recipe: string; + error: string; +} +export interface BulkActionsResponse { + success: boolean; + message: string; + errors?: BulkActionError[]; +} export interface CategoryIn { name: string; } @@ -47,6 +70,13 @@ export interface CreateRecipeByUrl { export interface CreateRecipeByUrlBulk { imports: CreateRecipeBulk[]; } +export interface DeleteRecipes { + recipes: string[]; +} +export interface ExportRecipes { + recipes: string[]; + exportType?: ExportTypes & string; +} export interface IngredientConfidence { average?: number; comment?: number; @@ -60,6 +90,12 @@ export interface IngredientFood { description?: string; id: number; } +/** + * A list of ingredient references. + */ +export interface IngredientReferences { + referenceId?: string; +} export interface IngredientRequest { parser?: RegisteredParser & string; ingredient: string; @@ -141,12 +177,6 @@ export interface RecipeStep { text: string; ingredientReferences?: IngredientReferences[]; } -/** - * A list of ingredient references. - */ -export interface IngredientReferences { - referenceId?: string; -} export interface RecipeSettings { public?: boolean; showNutrition?: boolean; @@ -198,6 +228,30 @@ export interface RecipeCommentUpdate { id: string; text: string; } +export interface RecipeShareToken { + recipeId: number; + expiresAt?: string; + groupId: string; + id: string; + createdAt: string; + recipe: Recipe; +} +export interface RecipeShareTokenCreate { + recipeId: number; + expiresAt?: string; +} +export interface RecipeShareTokenSave { + recipeId: number; + expiresAt?: string; + groupId: string; +} +export interface RecipeShareTokenSummary { + recipeId: number; + expiresAt?: string; + groupId: string; + id: string; + createdAt: string; +} export interface RecipeSlug { slug: string; } @@ -247,11 +301,6 @@ export interface RecipeToolResponse { recipes?: Recipe[]; } export interface SlugResponse {} -export interface TagBase { - name: string; - id: number; - slug: string; -} export interface TagIn { name: string; } diff --git a/frontend/types/api-types/user.ts b/frontend/types/api-types/user.ts index c258699d..99954cc4 100644 --- a/frontend/types/api-types/user.ts +++ b/frontend/types/api-types/user.ts @@ -19,6 +19,19 @@ export interface CreateToken { userId: string; token: string; } +export interface CreateUserRegistration { + group?: string; + groupToken?: string; + email: string; + username: string; + password: string; + passwordConfirm: string; + advanced?: boolean; + private?: boolean; +} +export interface ForgotPassword { + email: string; +} export interface GroupBase { name: string; } @@ -28,7 +41,6 @@ export interface GroupInDB { categories?: CategoryBase[]; webhooks?: unknown[]; users?: UserOut[]; - shoppingLists?: ShoppingListOut[]; preferences?: ReadGroupPreferences; } export interface UserOut { @@ -52,18 +64,6 @@ export interface LongLiveTokenOut { id: number; createdAt: string; } -export interface ShoppingListOut { - name: string; - group?: string; - items: ListItem[]; - id: number; -} -export interface ListItem { - title?: string; - text?: string; - quantity?: number; - checked?: boolean; -} export interface ReadGroupPreferences { privateGroup?: boolean; firstDayOfWeek?: number; @@ -103,6 +103,11 @@ export interface PrivateUser { cacheKey: string; password: string; } +export interface PrivatePasswordResetToken { + userId: string; + token: string; + user: PrivateUser; +} export interface RecipeSummary { id?: number; userId?: string; @@ -166,6 +171,16 @@ export interface CreateIngredientFood { name: string; description?: string; } +export interface ResetPassword { + token: string; + email: string; + password: string; + passwordConfirm: string; +} +export interface SavePasswordResetToken { + userId: string; + token: string; +} export interface SignUpIn { name: string; admin: boolean; @@ -231,3 +246,6 @@ export interface UserIn { canOrganize?: boolean; password: string; } +export interface ValidateResetToken { + token: string; +} diff --git a/frontend/types/components.d.ts b/frontend/types/components.d.ts index 963fc3da..e48e9e3b 100644 --- a/frontend/types/components.d.ts +++ b/frontend/types/components.d.ts @@ -5,7 +5,9 @@ import BaseOverflowButton from "@/components/global/BaseOverflowButton.vue"; import ReportTable from "@/components/global/ReportTable.vue"; import AppToolbar from "@/components/global/AppToolbar.vue"; + import BaseButtonGroup from "@/components/global/BaseButtonGroup.vue"; import BaseButton from "@/components/global/BaseButton.vue"; + import BannerExperimental from "@/components/global/BannerExperimental.vue"; import BaseDialog from "@/components/global/BaseDialog.vue"; import RecipeJsonEditor from "@/components/global/RecipeJsonEditor.vue"; import BaseStatCard from "@/components/global/BaseStatCard.vue"; @@ -31,7 +33,9 @@ declare module "vue" { BaseOverflowButton: typeof BaseOverflowButton; ReportTable: typeof ReportTable; AppToolbar: typeof AppToolbar; + BaseButtonGroup: typeof BaseButtonGroup; BaseButton: typeof BaseButton; + BannerExperimental: typeof BannerExperimental; BaseDialog: typeof BaseDialog; RecipeJsonEditor: typeof RecipeJsonEditor; BaseStatCard: typeof BaseStatCard; diff --git a/makefile b/makefile index ab7931a7..e53adc44 100644 --- a/makefile +++ b/makefile @@ -60,8 +60,6 @@ lint: ## 🧺 Format, Check and Flake8 poetry run flake8 mealie tests -lint-frontend: ## 🧺 Run yarn lint - cd frontend && yarn lint coverage: ## ☂️ Check code coverage quickly with the default Python poetry run pytest @@ -95,6 +93,12 @@ frontend: ## 🎬 Start Mealie Frontend Development Server frontend-build: ## 🏗 Build Frontend in frontend/dist cd frontend && yarn run build +frontend-generate: ## 🏗 Generate Code for Frontend + poetry run python dev/code-generation/gen_frontend_types.py + +frontend-lint: ## 🧺 Run yarn lint + cd frontend && yarn lint + .PHONY: docs docs: ## 📄 Start Mkdocs Development Server poetry run python dev/scripts/api_docs_gen.py && \ diff --git a/mealie/core/settings/db_providers.py b/mealie/core/settings/db_providers.py index 2a7a39cd..c6a764d9 100644 --- a/mealie/core/settings/db_providers.py +++ b/mealie/core/settings/db_providers.py @@ -62,4 +62,4 @@ def db_provider_factory(provider_name: str, data_dir: Path, env_file: Path, env_ elif provider_name == "sqlite": return SQLiteProvider(data_dir=data_dir) else: - return + return SQLiteProvider(data_dir=data_dir) diff --git a/mealie/db/models/event.py b/mealie/db/models/event.py index 5ec0fc1e..545a2957 100644 --- a/mealie/db/models/event.py +++ b/mealie/db/models/event.py @@ -1,31 +1,10 @@ -from sqlalchemy import Boolean, Column, DateTime, Integer, String +from sqlalchemy import Column, DateTime, Integer, String from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase from ._model_utils import auto_init -class EventNotification(SqlAlchemyBase, BaseMixins): - __tablename__ = "event_notifications" - id = Column(Integer, primary_key=True) - name = Column(String) - type = Column(String) - notification_url = Column(String) - - # Event Types - general = Column(Boolean, default=False) - recipe = Column(Boolean, default=False) - backup = Column(Boolean, default=False) - scheduled = Column(Boolean, default=False) - migration = Column(Boolean, default=False) - group = Column(Boolean, default=False) - user = Column(Boolean, default=False) - - @auto_init() - def __init__(self, **_) -> None: - pass - - class Event(SqlAlchemyBase, BaseMixins): __tablename__ = "events" id = Column(Integer, primary_key=True) diff --git a/mealie/db/models/group/__init__.py b/mealie/db/models/group/__init__.py index 0a7c1519..5053f5c5 100644 --- a/mealie/db/models/group/__init__.py +++ b/mealie/db/models/group/__init__.py @@ -1,4 +1,5 @@ from .cookbook import * +from .events import * from .exports import * from .group import * from .invite_tokens import * diff --git a/mealie/db/models/group/events.py b/mealie/db/models/group/events.py new file mode 100644 index 00000000..2eb8f886 --- /dev/null +++ b/mealie/db/models/group/events.py @@ -0,0 +1,61 @@ +from sqlalchemy import Boolean, Column, ForeignKey, String, orm + +from .._model_base import BaseMixins, SqlAlchemyBase +from .._model_utils import GUID, auto_init + + +class GroupEventNotifierOptionsModel(SqlAlchemyBase, BaseMixins): + __tablename__ = "group_events_notifier_options" + + id = Column(GUID, primary_key=True, default=GUID.generate) + event_notifier_id = Column(GUID, ForeignKey("group_events_notifiers.id"), nullable=False) + + recipe_created = Column(Boolean, default=False, nullable=False) + recipe_updated = Column(Boolean, default=False, nullable=False) + recipe_deleted = Column(Boolean, default=False, nullable=False) + + user_signup = Column(Boolean, default=False, nullable=False) + + data_migrations = Column(Boolean, default=False, nullable=False) + data_export = Column(Boolean, default=False, nullable=False) + data_import = Column(Boolean, default=False, nullable=False) + + mealplan_entry_created = Column(Boolean, default=False, nullable=False) + + shopping_list_created = Column(Boolean, default=False, nullable=False) + shopping_list_updated = Column(Boolean, default=False, nullable=False) + shopping_list_deleted = Column(Boolean, default=False, nullable=False) + + cookbook_created = Column(Boolean, default=False, nullable=False) + cookbook_updated = Column(Boolean, default=False, nullable=False) + cookbook_deleted = Column(Boolean, default=False, nullable=False) + + tag_created = Column(Boolean, default=False, nullable=False) + tag_updated = Column(Boolean, default=False, nullable=False) + tag_deleted = Column(Boolean, default=False, nullable=False) + + category_created = Column(Boolean, default=False, nullable=False) + category_updated = Column(Boolean, default=False, nullable=False) + category_deleted = Column(Boolean, default=False, nullable=False) + + @auto_init() + def __init__(self, **_) -> None: + pass + + +class GroupEventNotifierModel(SqlAlchemyBase, BaseMixins): + __tablename__ = "group_events_notifiers" + + id = Column(GUID, primary_key=True, default=GUID.generate) + name = Column(String, nullable=False) + enabled = Column(String, default=True, nullable=False) + apprise_url = Column(String, nullable=False) + + group = orm.relationship("Group", back_populates="group_event_notifiers", single_parent=True) + group_id = Column(GUID, ForeignKey("groups.id"), index=True) + + options = orm.relationship(GroupEventNotifierOptionsModel, uselist=False, cascade="all, delete-orphan") + + @auto_init() + def __init__(self, **_) -> None: + pass diff --git a/mealie/db/models/group/group.py b/mealie/db/models/group/group.py index 672e3f30..95f301c9 100644 --- a/mealie/db/models/group/group.py +++ b/mealie/db/models/group/group.py @@ -1,5 +1,3 @@ -import uuid - import sqlalchemy as sa import sqlalchemy.orm as orm from sqlalchemy.orm.session import Session @@ -22,7 +20,7 @@ settings = get_app_settings() class Group(SqlAlchemyBase, BaseMixins): __tablename__ = "groups" - id = sa.Column(GUID, primary_key=True, default=uuid.uuid4) + id = sa.Column(GUID, primary_key=True, default=GUID.generate) name = sa.Column(sa.String, index=True, nullable=False, unique=True) users = orm.relationship("User", back_populates="group") categories = orm.relationship(Category, secondary=group2categories, single_parent=True, uselist=True) @@ -57,6 +55,7 @@ class Group(SqlAlchemyBase, BaseMixins): data_exports = orm.relationship("GroupDataExportsModel", **common_args) shopping_lists = orm.relationship("ShoppingList", **common_args) group_reports = orm.relationship("ReportModel", **common_args) + group_event_notifiers = orm.relationship("GroupEventNotifierModel", **common_args) class Config: exclude = { diff --git a/mealie/repos/repository_factory.py b/mealie/repos/repository_factory.py index 79b2c6bf..b83736b7 100644 --- a/mealie/repos/repository_factory.py +++ b/mealie/repos/repository_factory.py @@ -2,9 +2,10 @@ from functools import cached_property from sqlalchemy.orm import Session -from mealie.db.models.event import Event, EventNotification +from mealie.db.models.event import Event from mealie.db.models.group import Group, GroupMealPlan, ReportEntryModel, ReportModel from mealie.db.models.group.cookbook import CookBook +from mealie.db.models.group.events import GroupEventNotifierModel from mealie.db.models.group.exports import GroupDataExportsModel from mealie.db.models.group.invite_tokens import GroupInviteToken from mealie.db.models.group.preferences import GroupPreferencesModel @@ -24,7 +25,7 @@ from mealie.db.models.users import LongLiveToken, User from mealie.db.models.users.password_reset import PasswordResetModel 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_events import GroupEventNotifierOut from mealie.schema.group.group_exports import GroupDataExport from mealie.schema.group.group_preferences import ReadGroupPreferences from mealie.schema.group.group_shopping_list import ShoppingListItemOut, ShoppingListOut @@ -116,10 +117,6 @@ class AllRepositories: def sign_up(self) -> RepositoryGeneric[SignUpOut, SignUp]: return RepositoryGeneric(self.session, pk_id, SignUp, SignUpOut) - @cached_property - def event_notifications(self) -> RepositoryGeneric[EventNotificationIn, EventNotification]: - return RepositoryGeneric(self.session, pk_id, EventNotification, EventNotificationIn) - @cached_property def events(self) -> RepositoryGeneric[EventSchema, Event]: return RepositoryGeneric(self.session, pk_id, Event, EventSchema) @@ -193,3 +190,7 @@ class AllRepositories: @cached_property def group_multi_purpose_labels(self) -> RepositoryGeneric[MultiPurposeLabelOut, MultiPurposeLabel]: return RepositoryGeneric(self.session, pk_id, MultiPurposeLabel, MultiPurposeLabelOut) + + @cached_property + def group_event_notifier(self) -> RepositoryGeneric[GroupEventNotifierOut, GroupEventNotifierModel]: + return RepositoryGeneric(self.session, pk_id, GroupEventNotifierModel, GroupEventNotifierOut) diff --git a/mealie/repos/repository_generic.py b/mealie/repos/repository_generic.py index 8be460c4..675a1edb 100644 --- a/mealie/repos/repository_generic.py +++ b/mealie/repos/repository_generic.py @@ -70,6 +70,8 @@ class RepositoryGeneric(Generic[T, D]): def get_all(self, limit: int = None, order_by: str = None, start=0, override_schema=None) -> list[T]: eff_schema = override_schema or self.schema + filter = self._filter_builder() + order_attr = None if order_by: order_attr = getattr(self.sql_model, str(order_by)) @@ -77,10 +79,18 @@ class RepositoryGeneric(Generic[T, D]): return [ eff_schema.from_orm(x) - for x in self.session.query(self.sql_model).order_by(order_attr).offset(start).limit(limit).all() + for x in self.session.query(self.sql_model) + .order_by(order_attr) + .filter_by(**filter) + .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()] + return [ + eff_schema.from_orm(x) + for x in self.session.query(self.sql_model).filter_by(**filter).offset(start).limit(limit).all() + ] def multi_query( self, @@ -92,6 +102,8 @@ class RepositoryGeneric(Generic[T, D]): ) -> list[T]: eff_schema = override_schema or self.schema + filer = self._filter_builder(**query_by) + order_attr = None if order_by: order_attr = getattr(self.sql_model, str(order_by)) @@ -100,7 +112,7 @@ class RepositoryGeneric(Generic[T, D]): return [ eff_schema.from_orm(x) for x in self.session.query(self.sql_model) - .filter_by(**query_by) + .filter_by(**filer) .order_by(order_attr) .offset(start) .limit(limit) diff --git a/mealie/routes/_base/mixins.py b/mealie/routes/_base/mixins.py index 5c528315..598f0dbd 100644 --- a/mealie/routes/_base/mixins.py +++ b/mealie/routes/_base/mixins.py @@ -76,6 +76,17 @@ class CrudMixins: return item + def get_one(self, item_id): + item = self.repo.get(item_id) + + if not item: + raise HTTPException( + status.HTTP_404_NOT_FOUND, + detail=ErrorResponse.respond(message="Not found."), + ) + + return item + def update_one(self, data, item_id): item = self.repo.get(item_id) @@ -98,11 +109,11 @@ class CrudMixins: self.handle_exception(ex) def delete_one(self, item_id): - item = self.repo.get(item_id) - self.logger.info(f"Deleting item with id {item}") + self.logger.info(f"Deleting item with id {item_id}") try: - item = self.repo.delete(item) + item = self.repo.delete(item_id) + self.logger.info(item) except Exception as ex: self.handle_exception(ex) diff --git a/mealie/routes/about/__init__.py b/mealie/routes/about/__init__.py index 1ef6258e..ec834e62 100644 --- a/mealie/routes/about/__init__.py +++ b/mealie/routes/about/__init__.py @@ -1,8 +1,7 @@ from fastapi import APIRouter -from . import events, notifications +from . import events about_router = APIRouter(prefix="/api/about") about_router.include_router(events.router, tags=["Events: CRUD"]) -about_router.include_router(notifications.router, tags=["Events: Notifications"]) diff --git a/mealie/routes/about/notifications.py b/mealie/routes/about/notifications.py deleted file mode 100644 index 7df5257f..00000000 --- a/mealie/routes/about/notifications.py +++ /dev/null @@ -1,67 +0,0 @@ -from http.client import HTTPException - -from fastapi import Depends, status -from sqlalchemy.orm.session import Session - -from mealie.core.root_logger import get_logger -from mealie.db.db_setup import generate_session -from mealie.repos.all_repositories import get_repositories -from mealie.routes.routers import AdminAPIRouter -from mealie.schema.events import EventNotificationIn, EventNotificationOut, TestEvent -from mealie.services.events import test_notification - -router = AdminAPIRouter() - -logger = get_logger() - - -@router.post("/notifications") -async def create_event_notification( - event_data: EventNotificationIn, - session: Session = Depends(generate_session), -): - """Create event_notification in the Database""" - db = get_repositories(session) - - return db.event_notifications.create(event_data) - - -@router.post("/notifications/test") -async def test_notification_route( - test_data: TestEvent, - session: Session = Depends(generate_session), -): - """Create event_notification in the Database""" - db = get_repositories(session) - - if test_data.id: - event_obj: EventNotificationIn = db.event_notifications.get(test_data.id) - test_data.test_url = event_obj.notification_url - - try: - test_notification(test_data.test_url) - except Exception as e: - logger.error(e) - raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR) - - -@router.get("/notifications", response_model=list[EventNotificationOut]) -async def get_all_event_notification(session: Session = Depends(generate_session)): - """Get all event_notification from the Database""" - db = get_repositories(session) - return db.event_notifications.get_all(override_schema=EventNotificationOut) - - -@router.put("/notifications/{id}") -async def update_event_notification(id: int, session: Session = Depends(generate_session)): - """Update event_notification in the Database""" - # not yet implemented - raise HTTPException(status.HTTP_405_METHOD_NOT_ALLOWED) - - -@router.delete("/notifications/{id}") -async def delete_event_notification(id: int, session: Session = Depends(generate_session)): - """Delete event_notification from the Database""" - # Delete Item - db = get_repositories(session) - return db.event_notifications.delete(id) diff --git a/mealie/routes/groups/__init__.py b/mealie/routes/groups/__init__.py index 4773a8d2..9d5b0437 100644 --- a/mealie/routes/groups/__init__.py +++ b/mealie/routes/groups/__init__.py @@ -8,7 +8,7 @@ from mealie.services.group_services import CookbookService, WebhookService from mealie.services.group_services.meal_service import MealService from mealie.services.group_services.reports_service import GroupReportService -from . import categories, invitations, labels, migrations, preferences, self_service, shopping_lists +from . import categories, invitations, labels, migrations, notifications, preferences, self_service, shopping_lists router = APIRouter() @@ -56,3 +56,4 @@ def get_all_reports( router.include_router(report_router) router.include_router(shopping_lists.router) router.include_router(labels.router) +router.include_router(notifications.router) diff --git a/mealie/routes/groups/notifications.py b/mealie/routes/groups/notifications.py new file mode 100644 index 00000000..0efd482c --- /dev/null +++ b/mealie/routes/groups/notifications.py @@ -0,0 +1,85 @@ +from functools import cached_property +from sqlite3 import IntegrityError +from typing import Type + +from fastapi import APIRouter, Depends +from pydantic import UUID4 + +from mealie.routes._base.controller import controller +from mealie.routes._base.dependencies import SharedDependencies +from mealie.routes._base.mixins import CrudMixins +from mealie.schema.group.group_events import ( + GroupEventNotifierCreate, + GroupEventNotifierOut, + GroupEventNotifierPrivate, + GroupEventNotifierSave, + GroupEventNotifierUpdate, +) +from mealie.schema.mapper import cast +from mealie.schema.query import GetAll +from mealie.services.event_bus_service.event_bus_service import EventBusService + +router = APIRouter(prefix="/groups/events/notifications", tags=["Group: Event Notifications"]) + + +@controller(router) +class GroupEventsNotifierController: + deps: SharedDependencies = Depends(SharedDependencies.user) + event_bus: EventBusService = Depends(EventBusService) + + @cached_property + def repo(self): + if not self.deps.acting_user: + raise Exception("No user is logged in.") + + return self.deps.repos.group_event_notifier.by_group(self.deps.acting_user.group_id) + + def registered_exceptions(self, ex: Type[Exception]) -> str: + registered = { + Exception: "An unexpected error occurred.", + IntegrityError: "An unexpected error occurred.", + } + + return registered.get(ex, "An unexpected error occurred.") + + # ======================================================================= + # CRUD Operations + + @property + def mixins(self) -> CrudMixins: + return CrudMixins(self.repo, self.deps.logger, self.registered_exceptions, "An unexpected error occurred.") + + @router.get("", response_model=list[GroupEventNotifierOut]) + def get_all(self, q: GetAll = Depends(GetAll)): + return self.repo.get_all(start=q.start, limit=q.limit) + + @router.post("", response_model=GroupEventNotifierOut, status_code=201) + def create_one(self, data: GroupEventNotifierCreate): + save_data = cast(data, GroupEventNotifierSave, group_id=self.deps.acting_user.group_id) + return self.mixins.create_one(save_data) + + @router.get("/{item_id}", response_model=GroupEventNotifierOut) + def get_one(self, item_id: UUID4): + return self.mixins.get_one(item_id) + + @router.put("/{item_id}", response_model=GroupEventNotifierOut) + def update_one(self, item_id: UUID4, data: GroupEventNotifierUpdate): + if data.apprise_url is None: + current_data: GroupEventNotifierPrivate = self.repo.get_one( + item_id, override_schema=GroupEventNotifierPrivate + ) + data.apprise_url = current_data.apprise_url + + return self.mixins.update_one(data, item_id) + + @router.delete("/{item_id}", status_code=204) + def delete_one(self, item_id: UUID4): + self.mixins.delete_one(item_id) # type: ignore + + # ======================================================================= + # Test Event Notifications + + @router.post("/{item_id}/test", status_code=204) + def test_notification(self, item_id: UUID4): + item: GroupEventNotifierPrivate = self.repo.get_one(item_id, override_schema=GroupEventNotifierPrivate) + self.event_bus.test_publisher(item.apprise_url) diff --git a/mealie/routes/groups/shopping_lists.py b/mealie/routes/groups/shopping_lists.py index 94fe1dd1..6219ef46 100644 --- a/mealie/routes/groups/shopping_lists.py +++ b/mealie/routes/groups/shopping_lists.py @@ -17,6 +17,8 @@ from mealie.schema.group.group_shopping_list import ( ) from mealie.schema.mapper import cast from mealie.schema.query import GetAll +from mealie.services.event_bus_service.event_bus_service import EventBusService +from mealie.services.event_bus_service.message_types import EventTypes from mealie.services.group_services.shopping_lists import ShoppingListService router = APIRouter(prefix="/groups/shopping/lists", tags=["Group: Shopping Lists"]) @@ -26,6 +28,7 @@ router = APIRouter(prefix="/groups/shopping/lists", tags=["Group: Shopping Lists class ShoppingListRoutes: deps: SharedDependencies = Depends(SharedDependencies.user) service: ShoppingListService = Depends(ShoppingListService.private) + event_bus: EventBusService = Depends(EventBusService) @cached_property def repo(self): @@ -56,7 +59,16 @@ class ShoppingListRoutes: @router.post("", response_model=ShoppingListOut) def create_one(self, data: ShoppingListCreate): save_data = cast(data, ShoppingListSave, group_id=self.deps.acting_user.group_id) - return self.mixins.create_one(save_data) + val = self.mixins.create_one(save_data) + + if val: + self.event_bus.dispatch( + self.deps.acting_user.group_id, + EventTypes.shopping_list_created, + msg="A new shopping list has been created.", + ) + + return val @router.get("/{item_id}", response_model=ShoppingListOut) def get_one(self, item_id: UUID4): diff --git a/mealie/schema/admin/__init__.py b/mealie/schema/admin/__init__.py index 9f530ed1..12dea356 100644 --- a/mealie/schema/admin/__init__.py +++ b/mealie/schema/admin/__init__.py @@ -1,3 +1,4 @@ +# GENERATED CODE - DO NOT MODIFY BY HAND from .about import * from .backup import * from .migration import * diff --git a/mealie/schema/cookbook/__init__.py b/mealie/schema/cookbook/__init__.py index 3fae82e2..497a9953 100644 --- a/mealie/schema/cookbook/__init__.py +++ b/mealie/schema/cookbook/__init__.py @@ -1 +1,2 @@ +# GENERATED CODE - DO NOT MODIFY BY HAND from .cookbook import * diff --git a/mealie/schema/events/__init__.py b/mealie/schema/events/__init__.py index 3eb95665..8803b69f 100644 --- a/mealie/schema/events/__init__.py +++ b/mealie/schema/events/__init__.py @@ -1,2 +1,2 @@ -from .event_notifications import * +# GENERATED CODE - DO NOT MODIFY BY HAND from .events import * diff --git a/mealie/schema/events/event_notifications.py b/mealie/schema/events/event_notifications.py deleted file mode 100644 index ea3ad424..00000000 --- a/mealie/schema/events/event_notifications.py +++ /dev/null @@ -1,61 +0,0 @@ -from enum import Enum -from typing import Optional - -from fastapi_camelcase import CamelModel - - -class DeclaredTypes(str, Enum): - general = "General" - discord = "Discord" - gotify = "Gotify" - pushover = "Pushover" - home_assistant = "Home Assistant" - - -class EventNotificationOut(CamelModel): - id: Optional[int] - name: str = "" - type: DeclaredTypes = DeclaredTypes.general - general: bool = True - recipe: bool = True - backup: bool = True - scheduled: bool = True - migration: bool = True - group: bool = True - user: bool = True - - class Config: - orm_mode = True - - -class EventNotificationIn(EventNotificationOut): - notification_url: str = "" - - class Config: - orm_mode = True - - -class Discord(CamelModel): - webhook_id: str - webhook_token: str - - @property - def create_url(self) -> str: - return f"discord://{self.webhook_id}/{self.webhook_token}/" - - -class GotifyPriority(str, Enum): - low = "low" - moderate = "moderate" - normal = "normal" - high = "high" - - -class Gotify(CamelModel): - hostname: str - token: str - priority: GotifyPriority = GotifyPriority.normal - - @property - def create_url(self) -> str: - return f"gotifys://{self.hostname}/{self.token}/?priority={self.priority}" diff --git a/mealie/schema/group/__init__.py b/mealie/schema/group/__init__.py index 17a42b44..508495e2 100644 --- a/mealie/schema/group/__init__.py +++ b/mealie/schema/group/__init__.py @@ -1,2 +1,10 @@ +# GENERATED CODE - DO NOT MODIFY BY HAND +from .group import * +from .group_events import * +from .group_exports import * +from .group_migration import * +from .group_permissions import * +from .group_preferences import * from .group_shopping_list import * +from .invite_token import * from .webhook import * diff --git a/mealie/schema/group/group_events.py b/mealie/schema/group/group_events.py new file mode 100644 index 00000000..3e0bce86 --- /dev/null +++ b/mealie/schema/group/group_events.py @@ -0,0 +1,89 @@ +from fastapi_camelcase import CamelModel +from pydantic import UUID4 + +# ============================================================================= +# Group Events Notifier Options + + +class GroupEventNotifierOptions(CamelModel): + """ + These events are in-sync with the EventTypes found in the EventBusService. + If you modify this, make sure to update the EventBusService as well. + """ + + recipe_created: bool = False + recipe_updated: bool = False + recipe_deleted: bool = False + + user_signup: bool = False + + data_migrations: bool = False + data_export: bool = False + data_import: bool = False + + mealplan_entry_created: bool = False + + shopping_list_created: bool = False + shopping_list_updated: bool = False + shopping_list_deleted: bool = False + + cookbook_created: bool = False + cookbook_updated: bool = False + cookbook_deleted: bool = False + + tag_created: bool = False + tag_updated: bool = False + tag_deleted: bool = False + + category_created: bool = False + category_updated: bool = False + category_deleted: bool = False + + +class GroupEventNotifierOptionsSave(GroupEventNotifierOptions): + notifier_id: UUID4 + + +class GroupEventNotifierOptionsOut(GroupEventNotifierOptions): + id: UUID4 + + class Config: + orm_mode = True + + +# ======================================================================= +# Notifiers + + +class GroupEventNotifierCreate(CamelModel): + name: str + apprise_url: str + + +class GroupEventNotifierSave(GroupEventNotifierCreate): + enabled: bool = True + group_id: UUID4 + options: GroupEventNotifierOptions = GroupEventNotifierOptions() + + +class GroupEventNotifierUpdate(GroupEventNotifierSave): + id: UUID4 + apprise_url: str = None + + +class GroupEventNotifierOut(CamelModel): + id: UUID4 + name: str + enabled: bool + group_id: UUID4 + options: GroupEventNotifierOptionsOut + + class Config: + orm_mode = True + + +class GroupEventNotifierPrivate(GroupEventNotifierOut): + apprise_url: str + + class Config: + orm_mode = True diff --git a/mealie/schema/labels/__init__.py b/mealie/schema/labels/__init__.py index e305e9e8..c5c9a65b 100644 --- a/mealie/schema/labels/__init__.py +++ b/mealie/schema/labels/__init__.py @@ -1,36 +1,2 @@ -from fastapi_camelcase import CamelModel -from pydantic import UUID4 - -from mealie.schema.recipe import IngredientFood - - -class MultiPurposeLabelCreate(CamelModel): - name: str - - -class MultiPurposeLabelSave(MultiPurposeLabelCreate): - group_id: UUID4 - - -class MultiPurposeLabelUpdate(MultiPurposeLabelSave): - id: UUID4 - - -class MultiPurposeLabelSummary(MultiPurposeLabelUpdate): - pass - - class Config: - orm_mode = True - - -class MultiPurposeLabelOut(MultiPurposeLabelUpdate): - shopping_list_items: "list[ShoppingListItemOut]" = [] - foods: list[IngredientFood] = [] - - class Config: - orm_mode = True - - -from mealie.schema.group.group_shopping_list import ShoppingListItemOut - -MultiPurposeLabelOut.update_forward_refs() +# GENERATED CODE - DO NOT MODIFY BY HAND +from .multi_purpose_label import * diff --git a/mealie/schema/labels/multi_purpose_label.py b/mealie/schema/labels/multi_purpose_label.py new file mode 100644 index 00000000..e305e9e8 --- /dev/null +++ b/mealie/schema/labels/multi_purpose_label.py @@ -0,0 +1,36 @@ +from fastapi_camelcase import CamelModel +from pydantic import UUID4 + +from mealie.schema.recipe import IngredientFood + + +class MultiPurposeLabelCreate(CamelModel): + name: str + + +class MultiPurposeLabelSave(MultiPurposeLabelCreate): + group_id: UUID4 + + +class MultiPurposeLabelUpdate(MultiPurposeLabelSave): + id: UUID4 + + +class MultiPurposeLabelSummary(MultiPurposeLabelUpdate): + pass + + class Config: + orm_mode = True + + +class MultiPurposeLabelOut(MultiPurposeLabelUpdate): + shopping_list_items: "list[ShoppingListItemOut]" = [] + foods: list[IngredientFood] = [] + + class Config: + orm_mode = True + + +from mealie.schema.group.group_shopping_list import ShoppingListItemOut + +MultiPurposeLabelOut.update_forward_refs() diff --git a/mealie/schema/meal_plan/__init__.py b/mealie/schema/meal_plan/__init__.py index 38a27ed4..8532fae6 100644 --- a/mealie/schema/meal_plan/__init__.py +++ b/mealie/schema/meal_plan/__init__.py @@ -1,3 +1,4 @@ +# GENERATED CODE - DO NOT MODIFY BY HAND from .meal import * from .new_meal import * from .shopping_list import * diff --git a/mealie/schema/recipe/__init__.py b/mealie/schema/recipe/__init__.py index df5c118f..0dfd2a7d 100644 --- a/mealie/schema/recipe/__init__.py +++ b/mealie/schema/recipe/__init__.py @@ -1,7 +1,15 @@ +# GENERATED CODE - DO NOT MODIFY BY HAND from .recipe import * +from .recipe_asset import * +from .recipe_bulk_actions import * from .recipe_category import * from .recipe_comments import * from .recipe_image_types import * from .recipe_ingredient import * +from .recipe_notes import * +from .recipe_nutrition import * +from .recipe_settings import * +from .recipe_share_token import * +from .recipe_step import * from .recipe_tool import * from .request_helpers import * diff --git a/mealie/schema/recipe/recipe_bulk_actions.py b/mealie/schema/recipe/recipe_bulk_actions.py index 7ad4196c..b29cd8b3 100644 --- a/mealie/schema/recipe/recipe_bulk_actions.py +++ b/mealie/schema/recipe/recipe_bulk_actions.py @@ -2,7 +2,7 @@ import enum from fastapi_camelcase import CamelModel -from . import CategoryBase, TagBase +from mealie.schema.recipe.recipe_category import CategoryBase, TagBase class ExportTypes(str, enum.Enum): diff --git a/mealie/schema/recipe/recipe_category.py b/mealie/schema/recipe/recipe_category.py index 92f37a93..1e239c44 100644 --- a/mealie/schema/recipe/recipe_category.py +++ b/mealie/schema/recipe/recipe_category.py @@ -1,5 +1,3 @@ -from typing import List - from fastapi_camelcase import CamelModel from pydantic.utils import GetterDict @@ -23,7 +21,7 @@ class CategoryBase(CategoryIn): class RecipeCategoryResponse(CategoryBase): - recipes: List["Recipe"] = [] + recipes: "list[Recipe]" = [] class Config: orm_mode = True @@ -42,7 +40,7 @@ class RecipeTagResponse(RecipeCategoryResponse): pass -from . import Recipe +from mealie.schema.recipe.recipe import Recipe RecipeCategoryResponse.update_forward_refs() RecipeTagResponse.update_forward_refs() diff --git a/mealie/schema/reports/__init__.py b/mealie/schema/reports/__init__.py index e063b17c..02e53dd0 100644 --- a/mealie/schema/reports/__init__.py +++ b/mealie/schema/reports/__init__.py @@ -1 +1,2 @@ +# GENERATED CODE - DO NOT MODIFY BY HAND from .reports import * diff --git a/mealie/schema/response/__init__.py b/mealie/schema/response/__init__.py index 3eb5f5d3..343a0c52 100644 --- a/mealie/schema/response/__init__.py +++ b/mealie/schema/response/__init__.py @@ -1,17 +1,2 @@ -from typing import Optional - -from pydantic import BaseModel - - -class ErrorResponse(BaseModel): - message: str - error: bool = True - exception: Optional[str] = None - - @classmethod - def respond(cls, message: str, exception: Optional[str] = None) -> dict: - """ - This method is an helper to create an obect and convert to a dictionary - in the same call, for use while providing details to a HTTPException - """ - return cls(message=message, exception=exception).dict() +# GENERATED CODE - DO NOT MODIFY BY HAND +from .error_response import * diff --git a/mealie/schema/response/error_response.py b/mealie/schema/response/error_response.py new file mode 100644 index 00000000..3eb5f5d3 --- /dev/null +++ b/mealie/schema/response/error_response.py @@ -0,0 +1,17 @@ +from typing import Optional + +from pydantic import BaseModel + + +class ErrorResponse(BaseModel): + message: str + error: bool = True + exception: Optional[str] = None + + @classmethod + def respond(cls, message: str, exception: Optional[str] = None) -> dict: + """ + This method is an helper to create an obect and convert to a dictionary + in the same call, for use while providing details to a HTTPException + """ + return cls(message=message, exception=exception).dict() diff --git a/mealie/schema/server/__init__.py b/mealie/schema/server/__init__.py index f5456c32..5fa497e0 100644 --- a/mealie/schema/server/__init__.py +++ b/mealie/schema/server/__init__.py @@ -1 +1,2 @@ +# GENERATED CODE - DO NOT MODIFY BY HAND from .tasks import * diff --git a/mealie/schema/user/__init__.py b/mealie/schema/user/__init__.py index a6a16224..13c7e7a8 100644 --- a/mealie/schema/user/__init__.py +++ b/mealie/schema/user/__init__.py @@ -1,3 +1,6 @@ +# GENERATED CODE - DO NOT MODIFY BY HAND from .auth import * +from .registration import * from .sign_up import * from .user import * +from .user_passwords import * diff --git a/mealie/services/backups/imports.py b/mealie/services/backups/imports.py index 312921a0..7d3569d3 100644 --- a/mealie/services/backups/imports.py +++ b/mealie/services/backups/imports.py @@ -9,8 +9,7 @@ from sqlalchemy.orm.session import Session from mealie.core.config import get_app_dirs from mealie.repos.all_repositories import get_repositories -from mealie.schema.admin import CommentImport, GroupImport, NotificationImport, RecipeImport, UserImport -from mealie.schema.events import EventNotificationIn +from mealie.schema.admin import CommentImport, GroupImport, RecipeImport, UserImport from mealie.schema.recipe import Recipe, RecipeCommentOut from mealie.schema.user import PrivateUser, UpdateGroup from mealie.services.image import minify @@ -159,24 +158,6 @@ class ImportDatabase: minify.migrate_images() - def import_notifications(self): - notify_file = self.import_dir.joinpath("notifications", "notifications.json") - notifications = ImportDatabase.read_models_file(notify_file, EventNotificationIn) - import_notifications = [] - - for notify in notifications: - import_status = self.import_model( - db_table=self.db.event_notifications, - model=notify, - return_model=NotificationImport, - name_attr="name", - search_key="notification_url", - ) - - import_notifications.append(import_status) - - return import_notifications - def import_settings(self): return [] @@ -330,11 +311,6 @@ def import_database( user_report = import_session.import_users() notification_report = [] - if import_notifications: - notification_report = import_session.import_notifications() - - # if import_recipes: - # import_session.import_comments() import_session.clean_up() diff --git a/mealie/services/event_bus_service/__init__.py b/mealie/services/event_bus_service/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/mealie/services/event_bus_service/event_bus_service.py b/mealie/services/event_bus_service/event_bus_service.py new file mode 100644 index 00000000..f3afd054 --- /dev/null +++ b/mealie/services/event_bus_service/event_bus_service.py @@ -0,0 +1,46 @@ +from fastapi import BackgroundTasks, Depends +from pydantic import UUID4 + +from mealie.db.db_setup import generate_session +from mealie.repos.repository_factory import AllRepositories +from mealie.schema.group.group_events import GroupEventNotifierPrivate + +from .message_types import EventBusMessage, EventTypes +from .publisher import ApprisePublisher, PublisherLike + + +class EventBusService: + def __init__(self, bg: BackgroundTasks, session=Depends(generate_session)) -> None: + self.bg = bg + self._publisher = ApprisePublisher + self.session = session + self.group_id = None + + @property + def publisher(self) -> PublisherLike: + return self._publisher() + + def get_urls(self, event_type: EventTypes) -> list[str]: + repos = AllRepositories(self.session) + + notifiers: list[GroupEventNotifierPrivate] = repos.group_event_notifier.by_group(self.group_id).multi_query( + {"enabled": True}, override_schema=GroupEventNotifierPrivate + ) + + return [notifier.apprise_url for notifier in notifiers if getattr(notifier.options, event_type.name)] + + def dispatch(self, group_id: UUID4, event_type: EventTypes, msg: str = "") -> None: + self.group_id = group_id + + def _dispatch(): + if urls := self.get_urls(event_type): + self.publisher.publish(EventBusMessage.from_type(event_type, body=msg), urls) + + self.bg.add_task(_dispatch) + + def test_publisher(self, url: str) -> None: + self.bg.add_task( + self.publisher.publish, + event=EventBusMessage.from_type(EventTypes.test_message, body="This is a test event."), + notification_urls=[url], + ) diff --git a/mealie/services/event_bus_service/message_types.py b/mealie/services/event_bus_service/message_types.py new file mode 100644 index 00000000..ec012f3b --- /dev/null +++ b/mealie/services/event_bus_service/message_types.py @@ -0,0 +1,47 @@ +from enum import Enum, auto + + +class EventTypes(Enum): + test_message = auto() + + recipe_created = auto() + recipe_updated = auto() + recipe_deleted = auto() + + user_signup = auto() + + data_migrations = auto() + data_export = auto() + data_import = auto() + + mealplan_entry_created = auto() + + shopping_list_created = auto() + shopping_list_updated = auto() + shopping_list_deleted = auto() + + cookbook_created = auto() + cookbook_updated = auto() + cookbook_deleted = auto() + + tag_created = auto() + tag_updated = auto() + tag_deleted = auto() + + category_created = auto() + category_updated = auto() + category_deleted = auto() + + +class EventBusMessage: + title: str + body: str = "" + + def __init__(self, title, body) -> None: + self.title = title + self.body = body + + @classmethod + def from_type(cls, event_type: EventTypes, body: str = "") -> "EventBusMessage": + title = event_type.name.replace("_", " ").title() + return cls(title=title, body=body) diff --git a/mealie/services/event_bus_service/publisher.py b/mealie/services/event_bus_service/publisher.py new file mode 100644 index 00000000..dec7806b --- /dev/null +++ b/mealie/services/event_bus_service/publisher.py @@ -0,0 +1,29 @@ +from typing import Protocol + +import apprise + +from mealie.services.event_bus_service.event_bus_service import EventBusMessage + + +class PublisherLike(Protocol): + def publish(self, event: EventBusMessage, notification_urls: list[str]): + ... + + +class ApprisePublisher: + def __init__(self, hard_fail=False) -> None: + asset = apprise.AppriseAsset( + async_mode=True, + image_url_mask="https://raw.githubusercontent.com/hay-kot/mealie/dev/frontend/public/img/icons/android-chrome-maskable-512x512.png", + ) + self.apprise = apprise.Apprise(asset=asset) + self.hard_fail = hard_fail + + def publish(self, event: EventBusMessage, notification_urls: list[str]): + for dest in notification_urls: + status = self.apprise.add(dest) + + if not status and self.hard_fail: + raise Exception("Apprise URL Add Failed") + + self.apprise.notify(title=event.title, body=event.body) diff --git a/mealie/services/events.py b/mealie/services/events.py index 0e68da5e..97265f6d 100644 --- a/mealie/services/events.py +++ b/mealie/services/events.py @@ -1,4 +1,3 @@ -import apprise from sqlalchemy.orm.session import Session from mealie.db.db_setup import create_session @@ -6,44 +5,12 @@ from mealie.repos.all_repositories import get_repositories from mealie.schema.events import Event, EventCategory -def test_notification(notification_url, event=None) -> bool: - if event is None: - event = Event( - title="Test Notification", - text="This is a test message from the Mealie API server", - category=EventCategory.general.value, - ) - - post_notifications(event, [notification_url], hard_fail=True) - - -def post_notifications(event: Event, notification_urls=list[str], hard_fail=False, attachment=None): - asset = apprise.AppriseAsset(async_mode=False) - apobj = apprise.Apprise(asset=asset) - - for dest in notification_urls: - status = apobj.add(dest) - - if not status and hard_fail: - raise Exception("Apprise URL Add Failed") - - apobj.notify( - body=event.text, - title=event.title, - attach=str(attachment), - ) - - -def save_event(title, text, category, session: Session, attachment=None): +def save_event(title, text, category, session: Session): event = Event(title=title, text=text, category=category) session = session or create_session() db = get_repositories(session) db.events.create(event.dict()) - notification_objects = db.event_notifications.get(match_value=True, match_key=category, limit=9999) - notification_urls = [x.notification_url for x in notification_objects] - post_notifications(event, notification_urls, attachment=attachment) - def create_general_event(title, text, session=None): category = EventCategory.general @@ -52,8 +19,7 @@ def create_general_event(title, text, session=None): def create_recipe_event(title, text, session=None, attachment=None): category = EventCategory.recipe - - save_event(title=title, text=text, category=category, session=session, attachment=attachment) + save_event(title=title, text=text, category=category, session=session) def create_backup_event(title, text, session=None): diff --git a/poetry.lock b/poetry.lock index ba782bb5..389d3fba 100644 --- a/poetry.lock +++ b/poetry.lock @@ -41,7 +41,7 @@ python-versions = "*" [[package]] name = "apprise" -version = "0.9.3" +version = "0.9.6" description = "Push Notifications that work with just about every platform!" category = "main" optional = false @@ -49,7 +49,6 @@ python-versions = ">=2.7" [package.dependencies] click = ">=5.0" -cryptography = "*" markdown = "*" PyYAML = "*" requests = "*" @@ -84,15 +83,14 @@ zookeeper = ["kazoo"] [[package]] name = "astroid" -version = "2.8.4" +version = "2.9.3" description = "An abstract syntax tree for Python with inference support." category = "dev" optional = false -python-versions = "~=3.6" +python-versions = ">=3.6.2" [package.dependencies] lazy-object-proxy = ">=1.4.0" -typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""} wrapt = ">=1.11,<1.14" [[package]] @@ -105,17 +103,17 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "attrs" -version = "21.2.0" +version = "21.4.0" description = "Classes Without Boilerplate" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] [[package]] name = "bcrypt" @@ -150,7 +148,7 @@ lxml = ["lxml"] [[package]] name = "black" -version = "21.11b1" +version = "21.12b0" description = "The uncompromising code formatter." category = "dev" optional = false @@ -161,7 +159,6 @@ click = ">=7.1.2" mypy-extensions = ">=0.4.3" pathspec = ">=0.9.0,<1" platformdirs = ">=2" -regex = ">=2021.4.4" tomli = ">=0.2.6,<2.0.0" typing-extensions = [ {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}, @@ -177,11 +174,11 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "cachetools" -version = "4.2.4" +version = "5.0.0" description = "Extensible memoizing collections and decorators" category = "main" optional = false -python-versions = "~=3.5" +python-versions = "~=3.7" [[package]] name = "certifi" @@ -212,7 +209,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "charset-normalizer" -version = "2.0.7" +version = "2.0.10" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "main" optional = false @@ -259,25 +256,6 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" [package.extras] toml = ["toml"] -[[package]] -name = "cryptography" -version = "35.0.0" -description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -cffi = ">=1.12" - -[package.extras] -docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] -docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] -pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] -sdist = ["setuptools_rust (>=0.11.4)"] -ssh = ["bcrypt (>=3.1.5)"] -test = ["pytest (>=6.2.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] - [[package]] name = "cssselect" version = "1.1.0" @@ -371,7 +349,7 @@ test = ["pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "mypy (==0.91 [[package]] name = "fastapi-camelcase" -version = "1.0.3" +version = "1.0.5" description = "Package provides an easy way to have camelcase request/response bodies for Pydantic" category = "main" optional = false @@ -383,16 +361,16 @@ pyhumps = "*" [[package]] name = "flake8" -version = "3.9.2" +version = "4.0.1" description = "the modular source code checker: pep8 pyflakes and co" category = "dev" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +python-versions = ">=3.6" [package.dependencies] mccabe = ">=0.6.0,<0.7.0" -pycodestyle = ">=2.7.0,<2.8.0" -pyflakes = ">=2.3.0,<2.4.0" +pycodestyle = ">=2.8.0,<2.9.0" +pyflakes = ">=2.4.0,<2.5.0" [[package]] name = "flake8-print" @@ -504,11 +482,11 @@ python-versions = ">=3.5" [[package]] name = "importlib-metadata" -version = "4.8.1" +version = "4.10.0" description = "Read metadata from Python packages" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] zipp = ">=0.5" @@ -516,7 +494,7 @@ zipp = ">=0.5" [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] perf = ["ipython"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] [[package]] name = "iniconfig" @@ -528,7 +506,7 @@ python-versions = "*" [[package]] name = "isodate" -version = "0.6.0" +version = "0.6.1" description = "An ISO 8601 date/time/duration parser and formatter" category = "main" optional = false @@ -539,7 +517,7 @@ six = "*" [[package]] name = "isort" -version = "5.9.3" +version = "5.10.1" description = "A Python utility / library to sort Python imports." category = "dev" optional = false @@ -575,15 +553,15 @@ python-versions = "*" [[package]] name = "lazy-object-proxy" -version = "1.6.0" +version = "1.7.1" description = "A fast and thorough lazy object proxy." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +python-versions = ">=3.6" [[package]] name = "lxml" -version = "4.6.2" +version = "4.7.1" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." category = "main" optional = false @@ -597,7 +575,7 @@ source = ["Cython (>=0.29.7)"] [[package]] name = "markdown" -version = "3.3.4" +version = "3.3.6" description = "Python implementation of Markdown." category = "main" optional = false @@ -713,14 +691,14 @@ signedtoken = ["cryptography (>=3.0.0,<4)", "pyjwt (>=2.0.0,<3)"] [[package]] name = "packaging" -version = "21.2" +version = "21.3" description = "Core utilities for Python packages" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] -pyparsing = ">=2.0.2,<3" +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" [[package]] name = "passlib" @@ -765,11 +743,11 @@ python-versions = ">=3.6" [[package]] name = "platformdirs" -version = "2.4.0" +version = "2.4.1" 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" +python-versions = ">=3.7" [package.extras] docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] @@ -808,7 +786,7 @@ test = ["nose", "mock"] [[package]] name = "psycopg2-binary" -version = "2.9.1" +version = "2.9.3" description = "psycopg2 - Python-PostgreSQL Database Adapter" category = "main" optional = true @@ -816,11 +794,11 @@ python-versions = ">=3.6" [[package]] name = "py" -version = "1.10.0" +version = "1.11.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "pyasn1" @@ -843,15 +821,15 @@ pyasn1 = ">=0.4.6,<0.5.0" [[package]] name = "pycodestyle" -version = "2.7.0" +version = "2.8.0" description = "Python style guide checker" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "pycparser" -version = "2.20" +version = "2.21" description = "C parser in Python" category = "main" optional = false @@ -886,7 +864,7 @@ pydantic = "*" [[package]] name = "pyflakes" -version = "2.3.1" +version = "2.4.0" description = "passive checker of Python programs" category = "dev" optional = false @@ -894,7 +872,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pygments" -version = "2.10.0" +version = "2.11.2" description = "Pygments is a syntax highlighting package written in Python." category = "dev" optional = false @@ -902,7 +880,7 @@ python-versions = ">=3.5" [[package]] name = "pyhumps" -version = "3.0.2" +version = "3.5.0" description = "🐫 Convert strings (and dictionary keys) between snake case, camel case and pascal case in Python. Inspired by Humps for Node" category = "main" optional = false @@ -910,24 +888,23 @@ python-versions = "*" [[package]] name = "pylint" -version = "2.11.1" +version = "2.12.2" description = "python code static checker" category = "dev" optional = false -python-versions = "~=3.6" +python-versions = ">=3.6.2" [package.dependencies] -astroid = ">=2.8.0,<2.9" +astroid = ">=2.9.0,<2.10" colorama = {version = "*", markers = "sys_platform == \"win32\""} isort = ">=4.2.5,<6" mccabe = ">=0.6,<0.7" platformdirs = ">=2.2.0" -toml = ">=0.7.1" -typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} +toml = ">=0.9.2" [[package]] name = "pymdown-extensions" -version = "9.0" +version = "9.1" description = "Extension pack for Python Markdown." category = "dev" optional = false @@ -938,11 +915,14 @@ Markdown = ">=3.2" [[package]] name = "pyparsing" -version = "2.4.7" +version = "3.0.6" description = "Python parsing module" category = "main" optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = ">=3.6" + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pyrdfa3" @@ -1046,11 +1026,11 @@ pycryptodome = ["pycryptodome (>=3.3.1,<4.0.0)", "pyasn1"] [[package]] name = "python-ldap" -version = "3.3.1" +version = "3.4.0" description = "Python modules for implementing LDAP clients" category = "main" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +python-versions = ">=3.6" [package.dependencies] pyasn1 = ">=0.3.7" @@ -1121,7 +1101,7 @@ pyyaml = "*" [[package]] name = "rdflib" -version = "6.0.2" +version = "6.1.1" description = "RDFLib is a Python library for working with RDF, a simple yet powerful language for representing information." category = "main" optional = false @@ -1134,7 +1114,7 @@ pyparsing = "*" [package.extras] docs = ["sphinx (<5)", "sphinxcontrib-apidoc"] html = ["html5lib"] -tests = ["html5lib", "networkx", "nose (==1.3.7)", "nose-timer", "coverage", "black (==21.6b0)", "flake8", "doctest-ignore-unicode (==0.1.2)"] +tests = ["berkeleydb", "html5lib", "networkx", "pytest", "pytest-cov", "pytest-subtests"] [[package]] name = "rdflib-jsonld" @@ -1149,7 +1129,7 @@ rdflib = ">=5.0.0" [[package]] name = "recipe-scrapers" -version = "13.5.0" +version = "13.10.1" description = "Python package, scraping recipes from all over the internet" category = "main" optional = false @@ -1160,17 +1140,9 @@ beautifulsoup4 = ">=4.6.0" extruct = ">=0.8.0" requests = ">=2.19.1" -[[package]] -name = "regex" -version = "2021.9.30" -description = "Alternative regular expression module, to replace re." -category = "dev" -optional = false -python-versions = "*" - [[package]] name = "requests" -version = "2.26.0" +version = "2.27.1" description = "Python HTTP for Humans." category = "main" optional = false @@ -1203,7 +1175,7 @@ rsa = ["oauthlib[signedtoken] (>=3.0.0)"] [[package]] name = "rich" -version = "10.12.0" +version = "10.16.2" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" category = "dev" optional = false @@ -1219,11 +1191,11 @@ jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] [[package]] name = "rsa" -version = "4.7.2" +version = "4.8" description = "Pure-Python RSA implementation" category = "main" optional = false -python-versions = ">=3.5, <4" +python-versions = ">=3.6,<4" [package.dependencies] pyasn1 = ">=0.1.3" @@ -1246,7 +1218,7 @@ python-versions = ">=3.5" [[package]] name = "soupsieve" -version = "2.2.1" +version = "2.3.1" description = "A modern CSS selector implementation for Beautiful Soup." category = "main" optional = false @@ -1316,7 +1288,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "tomli" -version = "1.2.2" +version = "1.2.3" description = "A lil' TOML parser" category = "dev" optional = false @@ -1324,11 +1296,11 @@ python-versions = ">=3.6" [[package]] name = "typing-extensions" -version = "3.10.0.2" -description = "Backported and Experimental Type Hints for Python 3.5+" +version = "4.0.1" +description = "Backported and Experimental Type Hints for Python 3.6+" category = "main" optional = false -python-versions = "*" +python-versions = ">=3.6" [[package]] name = "tzdata" @@ -1356,7 +1328,7 @@ test = ["pytest-mock (>=3.3)", "pytest (>=4.3)"] [[package]] name = "urllib3" -version = "1.26.7" +version = "1.26.8" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false @@ -1458,23 +1430,23 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [[package]] name = "zipp" -version = "3.6.0" +version = "3.7.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] [extras] pgsql = ["psycopg2-binary"] [metadata] lock-version = "1.1" -python-versions = "^3.9" -content-hash = "eb1ef72becee98486ddf7fd709ca90f7e020cb85c567bd9add2d8be34c6c3533" +python-versions = "^3.10" +content-hash = "e39f0333c3869cda1b5aed5a1399dbde99bce2a0b956abb0899b79d3590d0eb9" [metadata.files] aiofiles = [ @@ -1494,24 +1466,24 @@ appdirs = [ {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, ] apprise = [ - {file = "apprise-0.9.3-py2.py3-none-any.whl", hash = "sha256:caa84e0488de9bfa26964bc525e51abea4ed8af7af116a47730f19d874b00500"}, - {file = "apprise-0.9.3.tar.gz", hash = "sha256:c8ace9c891d4224558570acbea808f31df9049530bd9ccd69901c27a65d600c4"}, + {file = "apprise-0.9.6-py2.py3-none-any.whl", hash = "sha256:c8fe142102e3d6410c5b04b4a10c4c99b267f8e946126c0105954ba84aca39ac"}, + {file = "apprise-0.9.6.tar.gz", hash = "sha256:15ed06208197c9d28fd83cd69e727b48280879a4c611afd6b5aea5d0def397f8"}, ] apscheduler = [ {file = "APScheduler-3.8.1-py2.py3-none-any.whl", hash = "sha256:c22cb14b411a31435eb2c530dfbbec948ac63015b517087c7978adb61b574865"}, {file = "APScheduler-3.8.1.tar.gz", hash = "sha256:5cf344ebcfbdaa48ae178c029c055cec7bc7a4a47c21e315e4d1f08bd35f2355"}, ] astroid = [ - {file = "astroid-2.8.4-py3-none-any.whl", hash = "sha256:0755c998e7117078dcb7d0bda621391dd2a85da48052d948c7411ab187325346"}, - {file = "astroid-2.8.4.tar.gz", hash = "sha256:1e83a69fd51b013ebf5912d26b9338d6643a55fec2f20c787792680610eed4a2"}, + {file = "astroid-2.9.3-py3-none-any.whl", hash = "sha256:506daabe5edffb7e696ad82483ad0228245a9742ed7d2d8c9cdb31537decf9f6"}, + {file = "astroid-2.9.3.tar.gz", hash = "sha256:1efdf4e867d4d8ba4a9f6cf9ce07cd182c4c41de77f23814feb27ca93ca9d877"}, ] atomicwrites = [ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, ] attrs = [ - {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, - {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, + {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, + {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, ] bcrypt = [ {file = "bcrypt-3.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c95d4cbebffafcdd28bd28bb4e25b31c50f6da605c81ffd9ad8a3d1b2ab7b1b6"}, @@ -1527,12 +1499,12 @@ beautifulsoup4 = [ {file = "beautifulsoup4-4.10.0.tar.gz", hash = "sha256:c23ad23c521d818955a4151a67d81580319d4bf548d3d49f4223ae041ff98891"}, ] black = [ - {file = "black-21.11b1-py3-none-any.whl", hash = "sha256:802c6c30b637b28645b7fde282ed2569c0cd777dbe493a41b6a03c1d903f99ac"}, - {file = "black-21.11b1.tar.gz", hash = "sha256:a042adbb18b3262faad5aff4e834ff186bb893f95ba3a8013f09de1e5569def2"}, + {file = "black-21.12b0-py3-none-any.whl", hash = "sha256:a615e69ae185e08fdd73e4715e260e2479c861b5740057fde6e8b4e3b7dd589f"}, + {file = "black-21.12b0.tar.gz", hash = "sha256:77b80f693a569e2e527958459634f18df9b0ba2625ba4e0c2d5da5be42e6f2b3"}, ] cachetools = [ - {file = "cachetools-4.2.4-py3-none-any.whl", hash = "sha256:92971d3cb7d2a97efff7c7bb1657f21a8f5fb309a37530537c71b1774189f2d1"}, - {file = "cachetools-4.2.4.tar.gz", hash = "sha256:89ea6f1b638d5a73a4f9226be57ac5e4f399d22770b92355f92dcb0f7f001693"}, + {file = "cachetools-5.0.0-py3-none-any.whl", hash = "sha256:8fecd4203a38af17928be7b90689d8083603073622229ca7077b72d8e5a976e4"}, + {file = "cachetools-5.0.0.tar.gz", hash = "sha256:486471dfa8799eb7ec503a8059e263db000cdda20075ce5e48903087f79d5fd6"}, ] certifi = [ {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, @@ -1595,8 +1567,8 @@ chardet = [ {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, ] charset-normalizer = [ - {file = "charset-normalizer-2.0.7.tar.gz", hash = "sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0"}, - {file = "charset_normalizer-2.0.7-py3-none-any.whl", hash = "sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b"}, + {file = "charset-normalizer-2.0.10.tar.gz", hash = "sha256:876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd"}, + {file = "charset_normalizer-2.0.10-py3-none-any.whl", hash = "sha256:cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455"}, ] click = [ {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, @@ -1664,28 +1636,6 @@ coverage = [ {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, ] -cryptography = [ - {file = "cryptography-35.0.0-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:d57e0cdc1b44b6cdf8af1d01807db06886f10177469312fbde8f44ccbb284bc9"}, - {file = "cryptography-35.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:ced40344e811d6abba00295ced98c01aecf0c2de39481792d87af4fa58b7b4d6"}, - {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:54b2605e5475944e2213258e0ab8696f4f357a31371e538ef21e8d61c843c28d"}, - {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:7b7ceeff114c31f285528ba8b390d3e9cfa2da17b56f11d366769a807f17cbaa"}, - {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d69645f535f4b2c722cfb07a8eab916265545b3475fdb34e0be2f4ee8b0b15e"}, - {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a2d0e0acc20ede0f06ef7aa58546eee96d2592c00f450c9acb89c5879b61992"}, - {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:07bb7fbfb5de0980590ddfc7f13081520def06dc9ed214000ad4372fb4e3c7f6"}, - {file = "cryptography-35.0.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7eba2cebca600a7806b893cb1d541a6e910afa87e97acf2021a22b32da1df52d"}, - {file = "cryptography-35.0.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:18d90f4711bf63e2fb21e8c8e51ed8189438e6b35a6d996201ebd98a26abbbe6"}, - {file = "cryptography-35.0.0-cp36-abi3-win32.whl", hash = "sha256:c10c797ac89c746e488d2ee92bd4abd593615694ee17b2500578b63cad6b93a8"}, - {file = "cryptography-35.0.0-cp36-abi3-win_amd64.whl", hash = "sha256:7075b304cd567694dc692ffc9747f3e9cb393cc4aa4fb7b9f3abd6f5c4e43588"}, - {file = "cryptography-35.0.0-pp36-pypy36_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a688ebcd08250eab5bb5bca318cc05a8c66de5e4171a65ca51db6bd753ff8953"}, - {file = "cryptography-35.0.0-pp36-pypy36_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d99915d6ab265c22873f1b4d6ea5ef462ef797b4140be4c9d8b179915e0985c6"}, - {file = "cryptography-35.0.0-pp36-pypy36_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:928185a6d1ccdb816e883f56ebe92e975a262d31cc536429041921f8cb5a62fd"}, - {file = "cryptography-35.0.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:ebeddd119f526bcf323a89f853afb12e225902a24d29b55fe18dd6fcb2838a76"}, - {file = "cryptography-35.0.0-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:22a38e96118a4ce3b97509443feace1d1011d0571fae81fc3ad35f25ba3ea999"}, - {file = "cryptography-35.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb80e8a1f91e4b7ef8b33041591e6d89b2b8e122d787e87eeb2b08da71bb16ad"}, - {file = "cryptography-35.0.0-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:abb5a361d2585bb95012a19ed9b2c8f412c5d723a9836418fab7aaa0243e67d2"}, - {file = "cryptography-35.0.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:1ed82abf16df40a60942a8c211251ae72858b25b7421ce2497c2eb7a1cee817c"}, - {file = "cryptography-35.0.0.tar.gz", hash = "sha256:9933f28f70d0517686bd7de36166dda42094eac49415459d9bdf5e7df3e0086d"}, -] cssselect = [ {file = "cssselect-1.1.0-py2.py3-none-any.whl", hash = "sha256:f612ee47b749c877ebae5bb77035d8f4202c6ad0f0fc1271b3c18ad6c4468ecf"}, {file = "cssselect-1.1.0.tar.gz", hash = "sha256:f95f8dedd925fd8f54edb3d2dfb44c190d9d18512377d3c1e2388d16126879bc"}, @@ -1711,11 +1661,11 @@ fastapi = [ {file = "fastapi-0.71.0.tar.gz", hash = "sha256:2b5ac0ae89c80b40d1dd4b2ea0bb1f78d7c4affd3644d080bf050f084759fff2"}, ] fastapi-camelcase = [ - {file = "fastapi_camelcase-1.0.3.tar.gz", hash = "sha256:260249df56bc6bc1e90452659ddd84be92b5e408636d1559ce22a8a1a6d8c5fe"}, + {file = "fastapi_camelcase-1.0.5.tar.gz", hash = "sha256:2cee005fb1b75649491b9f7cfccc640b12f028eb88084565f7d8cf415192026a"}, ] flake8 = [ - {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, - {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, + {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, + {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, ] flake8-print = [ {file = "flake8-print-4.0.0.tar.gz", hash = "sha256:5afac374b7dc49aac2c36d04b5eb1d746d72e6f5df75a6ecaecd99e9f79c6516"}, @@ -1815,20 +1765,20 @@ idna = [ {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, ] importlib-metadata = [ - {file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"}, - {file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"}, + {file = "importlib_metadata-4.10.0-py3-none-any.whl", hash = "sha256:b7cf7d3fef75f1e4c80a96ca660efbd51473d7e8f39b5ab9210febc7809012a4"}, + {file = "importlib_metadata-4.10.0.tar.gz", hash = "sha256:92a8b58ce734b2a4494878e0ecf7d79ccd7a128b5fc6014c401e0b61f006f0f6"}, ] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] isodate = [ - {file = "isodate-0.6.0-py2.py3-none-any.whl", hash = "sha256:aa4d33c06640f5352aca96e4b81afd8ab3b47337cc12089822d6f322ac772c81"}, - {file = "isodate-0.6.0.tar.gz", hash = "sha256:2e364a3d5759479cdb2d37cce6b9376ea504db2ff90252a2e5b7cc89cc9ff2d8"}, + {file = "isodate-0.6.1-py2.py3-none-any.whl", hash = "sha256:0751eece944162659049d35f4f549ed815792b38793f07cf73381c1c87cbed96"}, + {file = "isodate-0.6.1.tar.gz", hash = "sha256:48c5881de7e8b0a0d648cb024c8062dc84e7b840ed81e864c7614fd3c127bde9"}, ] isort = [ - {file = "isort-5.9.3-py3-none-any.whl", hash = "sha256:e17d6e2b81095c9db0a03a8025a957f334d6ea30b26f9ec70805411e5c7c81f2"}, - {file = "isort-5.9.3.tar.gz", hash = "sha256:9c2ea1e62d871267b78307fe511c0838ba0da28698c5732d54e2790bf3ba9899"}, + {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, + {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, ] jinja2 = [ {file = "Jinja2-2.11.3-py2.py3-none-any.whl", hash = "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419"}, @@ -1838,71 +1788,109 @@ jstyleson = [ {file = "jstyleson-0.0.2.tar.gz", hash = "sha256:680003f3b15a2959e4e6a351f3b858e3c07dd3e073a0d54954e34d8ea5e1308e"}, ] lazy-object-proxy = [ - {file = "lazy-object-proxy-1.6.0.tar.gz", hash = "sha256:489000d368377571c6f982fba6497f2aa13c6d1facc40660963da62f5c379726"}, - {file = "lazy_object_proxy-1.6.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:c6938967f8528b3668622a9ed3b31d145fab161a32f5891ea7b84f6b790be05b"}, - {file = "lazy_object_proxy-1.6.0-cp27-cp27m-win32.whl", hash = "sha256:ebfd274dcd5133e0afae738e6d9da4323c3eb021b3e13052d8cbd0e457b1256e"}, - {file = "lazy_object_proxy-1.6.0-cp27-cp27m-win_amd64.whl", hash = "sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93"}, - {file = "lazy_object_proxy-1.6.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d900d949b707778696fdf01036f58c9876a0d8bfe116e8d220cfd4b15f14e741"}, - {file = "lazy_object_proxy-1.6.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5743a5ab42ae40caa8421b320ebf3a998f89c85cdc8376d6b2e00bd12bd1b587"}, - {file = "lazy_object_proxy-1.6.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:bf34e368e8dd976423396555078def5cfc3039ebc6fc06d1ae2c5a65eebbcde4"}, - {file = "lazy_object_proxy-1.6.0-cp36-cp36m-win32.whl", hash = "sha256:b579f8acbf2bdd9ea200b1d5dea36abd93cabf56cf626ab9c744a432e15c815f"}, - {file = "lazy_object_proxy-1.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:4f60460e9f1eb632584c9685bccea152f4ac2130e299784dbaf9fae9f49891b3"}, - {file = "lazy_object_proxy-1.6.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d7124f52f3bd259f510651450e18e0fd081ed82f3c08541dffc7b94b883aa981"}, - {file = "lazy_object_proxy-1.6.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:22ddd618cefe54305df49e4c069fa65715be4ad0e78e8d252a33debf00f6ede2"}, - {file = "lazy_object_proxy-1.6.0-cp37-cp37m-win32.whl", hash = "sha256:9d397bf41caad3f489e10774667310d73cb9c4258e9aed94b9ec734b34b495fd"}, - {file = "lazy_object_proxy-1.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:24a5045889cc2729033b3e604d496c2b6f588c754f7a62027ad4437a7ecc4837"}, - {file = "lazy_object_proxy-1.6.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:17e0967ba374fc24141738c69736da90e94419338fd4c7c7bef01ee26b339653"}, - {file = "lazy_object_proxy-1.6.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:410283732af311b51b837894fa2f24f2c0039aa7f220135192b38fcc42bd43d3"}, - {file = "lazy_object_proxy-1.6.0-cp38-cp38-win32.whl", hash = "sha256:85fb7608121fd5621cc4377a8961d0b32ccf84a7285b4f1d21988b2eae2868e8"}, - {file = "lazy_object_proxy-1.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:d1c2676e3d840852a2de7c7d5d76407c772927addff8d742b9808fe0afccebdf"}, - {file = "lazy_object_proxy-1.6.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:b865b01a2e7f96db0c5d12cfea590f98d8c5ba64ad222300d93ce6ff9138bcad"}, - {file = "lazy_object_proxy-1.6.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4732c765372bd78a2d6b2150a6e99d00a78ec963375f236979c0626b97ed8e43"}, - {file = "lazy_object_proxy-1.6.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:9698110e36e2df951c7c36b6729e96429c9c32b3331989ef19976592c5f3c77a"}, - {file = "lazy_object_proxy-1.6.0-cp39-cp39-win32.whl", hash = "sha256:1fee665d2638491f4d6e55bd483e15ef21f6c8c2095f235fef72601021e64f61"}, - {file = "lazy_object_proxy-1.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b"}, + {file = "lazy-object-proxy-1.7.1.tar.gz", hash = "sha256:d609c75b986def706743cdebe5e47553f4a5a1da9c5ff66d76013ef396b5a8a4"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb8c5fd1684d60a9902c60ebe276da1f2281a318ca16c1d0a96db28f62e9166b"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a57d51ed2997e97f3b8e3500c984db50a554bb5db56c50b5dab1b41339b37e36"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd45683c3caddf83abbb1249b653a266e7069a09f486daa8863fb0e7496a9fdb"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8561da8b3dd22d696244d6d0d5330618c993a215070f473b699e00cf1f3f6443"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fccdf7c2c5821a8cbd0a9440a456f5050492f2270bd54e94360cac663398739b"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-win32.whl", hash = "sha256:898322f8d078f2654d275124a8dd19b079080ae977033b713f677afcfc88e2b9"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:85b232e791f2229a4f55840ed54706110c80c0a210d076eee093f2b2e33e1bfd"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:46ff647e76f106bb444b4533bb4153c7370cdf52efc62ccfc1a28bdb3cc95442"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12f3bb77efe1367b2515f8cb4790a11cffae889148ad33adad07b9b55e0ab22c"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c19814163728941bb871240d45c4c30d33b8a2e85972c44d4e63dd7107faba44"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:e40f2013d96d30217a51eeb1db28c9ac41e9d0ee915ef9d00da639c5b63f01a1"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:2052837718516a94940867e16b1bb10edb069ab475c3ad84fd1e1a6dd2c0fcfc"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-win32.whl", hash = "sha256:6a24357267aa976abab660b1d47a34aaf07259a0c3859a34e536f1ee6e76b5bb"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-win_amd64.whl", hash = "sha256:6aff3fe5de0831867092e017cf67e2750c6a1c7d88d84d2481bd84a2e019ec35"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6a6e94c7b02641d1311228a102607ecd576f70734dc3d5e22610111aeacba8a0"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ce15276a1a14549d7e81c243b887293904ad2d94ad767f42df91e75fd7b5b6"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e368b7f7eac182a59ff1f81d5f3802161932a41dc1b1cc45c1f757dc876b5d2c"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6ecbb350991d6434e1388bee761ece3260e5228952b1f0c46ffc800eb313ff42"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:553b0f0d8dbf21890dd66edd771f9b1b5f51bd912fa5f26de4449bfc5af5e029"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-win32.whl", hash = "sha256:c7a683c37a8a24f6428c28c561c80d5f4fd316ddcf0c7cab999b15ab3f5c5c69"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-win_amd64.whl", hash = "sha256:df2631f9d67259dc9620d831384ed7732a198eb434eadf69aea95ad18c587a28"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07fa44286cda977bd4803b656ffc1c9b7e3bc7dff7d34263446aec8f8c96f88a"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dca6244e4121c74cc20542c2ca39e5c4a5027c81d112bfb893cf0790f96f57e"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91ba172fc5b03978764d1df5144b4ba4ab13290d7bab7a50f12d8117f8630c38"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:043651b6cb706eee4f91854da4a089816a6606c1428fd391573ef8cb642ae4f7"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b9e89b87c707dd769c4ea91f7a31538888aad05c116a59820f28d59b3ebfe25a"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-win32.whl", hash = "sha256:9d166602b525bf54ac994cf833c385bfcc341b364e3ee71e3bf5a1336e677b55"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:8f3953eb575b45480db6568306893f0bd9d8dfeeebd46812aa09ca9579595148"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dd7ed7429dbb6c494aa9bc4e09d94b778a3579be699f9d67da7e6804c422d3de"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70ed0c2b380eb6248abdef3cd425fc52f0abd92d2b07ce26359fcbc399f636ad"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7096a5e0c1115ec82641afbdd70451a144558ea5cf564a896294e346eb611be1"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f769457a639403073968d118bc70110e7dce294688009f5c24ab78800ae56dc8"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:39b0e26725c5023757fc1ab2a89ef9d7ab23b84f9251e28f9cc114d5b59c1b09"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-win32.whl", hash = "sha256:2130db8ed69a48a3440103d4a520b89d8a9405f1b06e2cc81640509e8bf6548f"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61"}, + {file = "lazy_object_proxy-1.7.1-pp37.pp38-none-any.whl", hash = "sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84"}, ] lxml = [ - {file = "lxml-4.6.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a9d6bc8642e2c67db33f1247a77c53476f3a166e09067c0474facb045756087f"}, - {file = "lxml-4.6.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:791394449e98243839fa822a637177dd42a95f4883ad3dec2a0ce6ac99fb0a9d"}, - {file = "lxml-4.6.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:68a5d77e440df94011214b7db907ec8f19e439507a70c958f750c18d88f995d2"}, - {file = "lxml-4.6.2-cp27-cp27m-win32.whl", hash = "sha256:fc37870d6716b137e80d19241d0e2cff7a7643b925dfa49b4c8ebd1295eb506e"}, - {file = "lxml-4.6.2-cp27-cp27m-win_amd64.whl", hash = "sha256:69a63f83e88138ab7642d8f61418cf3180a4d8cd13995df87725cb8b893e950e"}, - {file = "lxml-4.6.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:42ebca24ba2a21065fb546f3e6bd0c58c3fe9ac298f3a320147029a4850f51a2"}, - {file = "lxml-4.6.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:f83d281bb2a6217cd806f4cf0ddded436790e66f393e124dfe9731f6b3fb9afe"}, - {file = "lxml-4.6.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:535f067002b0fd1a4e5296a8f1bf88193080ff992a195e66964ef2a6cfec5388"}, - {file = "lxml-4.6.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:366cb750140f221523fa062d641393092813b81e15d0e25d9f7c6025f910ee80"}, - {file = "lxml-4.6.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:97db258793d193c7b62d4e2586c6ed98d51086e93f9a3af2b2034af01450a74b"}, - {file = "lxml-4.6.2-cp35-cp35m-win32.whl", hash = "sha256:648914abafe67f11be7d93c1a546068f8eff3c5fa938e1f94509e4a5d682b2d8"}, - {file = "lxml-4.6.2-cp35-cp35m-win_amd64.whl", hash = "sha256:4e751e77006da34643ab782e4a5cc21ea7b755551db202bc4d3a423b307db780"}, - {file = "lxml-4.6.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:681d75e1a38a69f1e64ab82fe4b1ed3fd758717bed735fb9aeaa124143f051af"}, - {file = "lxml-4.6.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:127f76864468d6630e1b453d3ffbbd04b024c674f55cf0a30dc2595137892d37"}, - {file = "lxml-4.6.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4fb85c447e288df535b17ebdebf0ec1cf3a3f1a8eba7e79169f4f37af43c6b98"}, - {file = "lxml-4.6.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:5be4a2e212bb6aa045e37f7d48e3e1e4b6fd259882ed5a00786f82e8c37ce77d"}, - {file = "lxml-4.6.2-cp36-cp36m-win32.whl", hash = "sha256:8c88b599e226994ad4db29d93bc149aa1aff3dc3a4355dd5757569ba78632bdf"}, - {file = "lxml-4.6.2-cp36-cp36m-win_amd64.whl", hash = "sha256:6e4183800f16f3679076dfa8abf2db3083919d7e30764a069fb66b2b9eff9939"}, - {file = "lxml-4.6.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d8d3d4713f0c28bdc6c806a278d998546e8efc3498949e3ace6e117462ac0a5e"}, - {file = "lxml-4.6.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:8246f30ca34dc712ab07e51dc34fea883c00b7ccb0e614651e49da2c49a30711"}, - {file = "lxml-4.6.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:923963e989ffbceaa210ac37afc9b906acebe945d2723e9679b643513837b089"}, - {file = "lxml-4.6.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:1471cee35eba321827d7d53d104e7b8c593ea3ad376aa2df89533ce8e1b24a01"}, - {file = "lxml-4.6.2-cp37-cp37m-win32.whl", hash = "sha256:2363c35637d2d9d6f26f60a208819e7eafc4305ce39dc1d5005eccc4593331c2"}, - {file = "lxml-4.6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:f4822c0660c3754f1a41a655e37cb4dbbc9be3d35b125a37fab6f82d47674ebc"}, - {file = "lxml-4.6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0448576c148c129594d890265b1a83b9cd76fd1f0a6a04620753d9a6bcfd0a4d"}, - {file = "lxml-4.6.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:60a20bfc3bd234d54d49c388950195d23a5583d4108e1a1d47c9eef8d8c042b3"}, - {file = "lxml-4.6.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2e5cc908fe43fe1aa299e58046ad66981131a66aea3129aac7770c37f590a644"}, - {file = "lxml-4.6.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:50c348995b47b5a4e330362cf39fc503b4a43b14a91c34c83b955e1805c8e308"}, - {file = "lxml-4.6.2-cp38-cp38-win32.whl", hash = "sha256:94d55bd03d8671686e3f012577d9caa5421a07286dd351dfef64791cf7c6c505"}, - {file = "lxml-4.6.2-cp38-cp38-win_amd64.whl", hash = "sha256:7a7669ff50f41225ca5d6ee0a1ec8413f3a0d8aa2b109f86d540887b7ec0d72a"}, - {file = "lxml-4.6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e0bfe9bb028974a481410432dbe1b182e8191d5d40382e5b8ff39cdd2e5c5931"}, - {file = "lxml-4.6.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:6fd8d5903c2e53f49e99359b063df27fdf7acb89a52b6a12494208bf61345a03"}, - {file = "lxml-4.6.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7e9eac1e526386df7c70ef253b792a0a12dd86d833b1d329e038c7a235dfceb5"}, - {file = "lxml-4.6.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:7ee8af0b9f7de635c61cdd5b8534b76c52cd03536f29f51151b377f76e214a1a"}, - {file = "lxml-4.6.2-cp39-cp39-win32.whl", hash = "sha256:2e6fd1b8acd005bd71e6c94f30c055594bbd0aa02ef51a22bbfa961ab63b2d75"}, - {file = "lxml-4.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:535332fe9d00c3cd455bd3dd7d4bacab86e2d564bdf7606079160fa6251caacf"}, - {file = "lxml-4.6.2.tar.gz", hash = "sha256:cd11c7e8d21af997ee8079037fff88f16fda188a9776eb4b81c7e4c9c0a7d7fc"}, + {file = "lxml-4.7.1-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:d546431636edb1d6a608b348dd58cc9841b81f4116745857b6cb9f8dadb2725f"}, + {file = "lxml-4.7.1-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6308062534323f0d3edb4e702a0e26a76ca9e0e23ff99be5d82750772df32a9e"}, + {file = "lxml-4.7.1-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f76dbe44e31abf516114f6347a46fa4e7c2e8bceaa4b6f7ee3a0a03c8eba3c17"}, + {file = "lxml-4.7.1-cp27-cp27m-win32.whl", hash = "sha256:d5618d49de6ba63fe4510bdada62d06a8acfca0b4b5c904956c777d28382b419"}, + {file = "lxml-4.7.1-cp27-cp27m-win_amd64.whl", hash = "sha256:9393a05b126a7e187f3e38758255e0edf948a65b22c377414002d488221fdaa2"}, + {file = "lxml-4.7.1-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:50d3dba341f1e583265c1a808e897b4159208d814ab07530202b6036a4d86da5"}, + {file = "lxml-4.7.1-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:44f552e0da3c8ee3c28e2eb82b0b784200631687fc6a71277ea8ab0828780e7d"}, + {file = "lxml-4.7.1-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:e662c6266e3a275bdcb6bb049edc7cd77d0b0f7e119a53101d367c841afc66dc"}, + {file = "lxml-4.7.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4c093c571bc3da9ebcd484e001ba18b8452903cd428c0bc926d9b0141bcb710e"}, + {file = "lxml-4.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3e26ad9bc48d610bf6cc76c506b9e5ad9360ed7a945d9be3b5b2c8535a0145e3"}, + {file = "lxml-4.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a5f623aeaa24f71fce3177d7fee875371345eb9102b355b882243e33e04b7175"}, + {file = "lxml-4.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7b5e2acefd33c259c4a2e157119c4373c8773cf6793e225006a1649672ab47a6"}, + {file = "lxml-4.7.1-cp310-cp310-win32.whl", hash = "sha256:67fa5f028e8a01e1d7944a9fb616d1d0510d5d38b0c41708310bd1bc45ae89f6"}, + {file = "lxml-4.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:b1d381f58fcc3e63fcc0ea4f0a38335163883267f77e4c6e22d7a30877218a0e"}, + {file = "lxml-4.7.1-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:38d9759733aa04fb1697d717bfabbedb21398046bd07734be7cccc3d19ea8675"}, + {file = "lxml-4.7.1-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:dfd0d464f3d86a1460683cd742306d1138b4e99b79094f4e07e1ca85ee267fe7"}, + {file = "lxml-4.7.1-cp35-cp35m-win32.whl", hash = "sha256:534e946bce61fd162af02bad7bfd2daec1521b71d27238869c23a672146c34a5"}, + {file = "lxml-4.7.1-cp35-cp35m-win_amd64.whl", hash = "sha256:6ec829058785d028f467be70cd195cd0aaf1a763e4d09822584ede8c9eaa4b03"}, + {file = "lxml-4.7.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:ade74f5e3a0fd17df5782896ddca7ddb998845a5f7cd4b0be771e1ffc3b9aa5b"}, + {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:41358bfd24425c1673f184d7c26c6ae91943fe51dfecc3603b5e08187b4bcc55"}, + {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6e56521538f19c4a6690f439fefed551f0b296bd785adc67c1777c348beb943d"}, + {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5b0f782f0e03555c55e37d93d7a57454efe7495dab33ba0ccd2dbe25fc50f05d"}, + {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:490712b91c65988012e866c411a40cc65b595929ececf75eeb4c79fcc3bc80a6"}, + {file = "lxml-4.7.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:34c22eb8c819d59cec4444d9eebe2e38b95d3dcdafe08965853f8799fd71161d"}, + {file = "lxml-4.7.1-cp36-cp36m-win32.whl", hash = "sha256:2a906c3890da6a63224d551c2967413b8790a6357a80bf6b257c9a7978c2c42d"}, + {file = "lxml-4.7.1-cp36-cp36m-win_amd64.whl", hash = "sha256:36b16fecb10246e599f178dd74f313cbdc9f41c56e77d52100d1361eed24f51a"}, + {file = "lxml-4.7.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:a5edc58d631170de90e50adc2cc0248083541affef82f8cd93bea458e4d96db8"}, + {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:87c1b0496e8c87ec9db5383e30042357b4839b46c2d556abd49ec770ce2ad868"}, + {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:0a5f0e4747f31cff87d1eb32a6000bde1e603107f632ef4666be0dc065889c7a"}, + {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:bf6005708fc2e2c89a083f258b97709559a95f9a7a03e59f805dd23c93bc3986"}, + {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc15874816b9320581133ddc2096b644582ab870cf6a6ed63684433e7af4b0d3"}, + {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0b5e96e25e70917b28a5391c2ed3ffc6156513d3db0e1476c5253fcd50f7a944"}, + {file = "lxml-4.7.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ec9027d0beb785a35aa9951d14e06d48cfbf876d8ff67519403a2522b181943b"}, + {file = "lxml-4.7.1-cp37-cp37m-win32.whl", hash = "sha256:9fbc0dee7ff5f15c4428775e6fa3ed20003140560ffa22b88326669d53b3c0f4"}, + {file = "lxml-4.7.1-cp37-cp37m-win_amd64.whl", hash = "sha256:1104a8d47967a414a436007c52f533e933e5d52574cab407b1e49a4e9b5ddbd1"}, + {file = "lxml-4.7.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:fc9fb11b65e7bc49f7f75aaba1b700f7181d95d4e151cf2f24d51bfd14410b77"}, + {file = "lxml-4.7.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:317bd63870b4d875af3c1be1b19202de34c32623609ec803b81c99193a788c1e"}, + {file = "lxml-4.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:610807cea990fd545b1559466971649e69302c8a9472cefe1d6d48a1dee97440"}, + {file = "lxml-4.7.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:09b738360af8cb2da275998a8bf79517a71225b0de41ab47339c2beebfff025f"}, + {file = "lxml-4.7.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6a2ab9d089324d77bb81745b01f4aeffe4094306d939e92ba5e71e9a6b99b71e"}, + {file = "lxml-4.7.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:eed394099a7792834f0cb4a8f615319152b9d801444c1c9e1b1a2c36d2239f9e"}, + {file = "lxml-4.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:735e3b4ce9c0616e85f302f109bdc6e425ba1670a73f962c9f6b98a6d51b77c9"}, + {file = "lxml-4.7.1-cp38-cp38-win32.whl", hash = "sha256:772057fba283c095db8c8ecde4634717a35c47061d24f889468dc67190327bcd"}, + {file = "lxml-4.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:13dbb5c7e8f3b6a2cf6e10b0948cacb2f4c9eb05029fe31c60592d08ac63180d"}, + {file = "lxml-4.7.1-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:718d7208b9c2d86aaf0294d9381a6acb0158b5ff0f3515902751404e318e02c9"}, + {file = "lxml-4.7.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:5bee1b0cbfdb87686a7fb0e46f1d8bd34d52d6932c0723a86de1cc532b1aa489"}, + {file = "lxml-4.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:e410cf3a2272d0a85526d700782a2fa92c1e304fdcc519ba74ac80b8297adf36"}, + {file = "lxml-4.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:585ea241ee4961dc18a95e2f5581dbc26285fcf330e007459688096f76be8c42"}, + {file = "lxml-4.7.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a555e06566c6dc167fbcd0ad507ff05fd9328502aefc963cb0a0547cfe7f00db"}, + {file = "lxml-4.7.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:adaab25be351fff0d8a691c4f09153647804d09a87a4e4ea2c3f9fe9e8651851"}, + {file = "lxml-4.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:82d16a64236970cb93c8d63ad18c5b9f138a704331e4b916b2737ddfad14e0c4"}, + {file = "lxml-4.7.1-cp39-cp39-win32.whl", hash = "sha256:59e7da839a1238807226f7143c68a479dee09244d1b3cf8c134f2fce777d12d0"}, + {file = "lxml-4.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:a1bbc4efa99ed1310b5009ce7f3a1784698082ed2c1ef3895332f5df9b3b92c2"}, + {file = "lxml-4.7.1-pp37-pypy37_pp73-macosx_10_14_x86_64.whl", hash = "sha256:0607ff0988ad7e173e5ddf7bf55ee65534bd18a5461183c33e8e41a59e89edf4"}, + {file = "lxml-4.7.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:6c198bfc169419c09b85ab10cb0f572744e686f40d1e7f4ed09061284fc1303f"}, + {file = "lxml-4.7.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a58d78653ae422df6837dd4ca0036610b8cb4962b5cfdbd337b7b24de9e5f98a"}, + {file = "lxml-4.7.1-pp38-pypy38_pp73-macosx_10_14_x86_64.whl", hash = "sha256:e18281a7d80d76b66a9f9e68a98cf7e1d153182772400d9a9ce855264d7d0ce7"}, + {file = "lxml-4.7.1-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8e54945dd2eeb50925500957c7c579df3cd07c29db7810b83cf30495d79af267"}, + {file = "lxml-4.7.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:447d5009d6b5447b2f237395d0018901dcc673f7d9f82ba26c1b9f9c3b444b60"}, + {file = "lxml-4.7.1.tar.gz", hash = "sha256:a1613838aa6b89af4ba10a0f3a972836128801ed008078f8c1244e65958f1b24"}, ] markdown = [ - {file = "Markdown-3.3.4-py3-none-any.whl", hash = "sha256:96c3ba1261de2f7547b46a00ea8463832c921d3f9d6aba3f255a6f71386db20c"}, - {file = "Markdown-3.3.4.tar.gz", hash = "sha256:31b5b491868dcc87d6c24b7e3d19a0d730d59d3e46f4eea6430a321bed387a49"}, + {file = "Markdown-3.3.6-py3-none-any.whl", hash = "sha256:9923332318f843411e9932237530df53162e29dc7a4e2b91e35764583c46c9a3"}, + {file = "Markdown-3.3.6.tar.gz", hash = "sha256:76df8ae32294ec39dcf89340382882dfa12975f87f45c3ed1ecdb1e8cefc7006"}, ] markupsafe = [ {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, @@ -2007,8 +1995,8 @@ oauthlib = [ {file = "oauthlib-3.1.1.tar.gz", hash = "sha256:8f0215fcc533dd8dd1bee6f4c412d4f0cd7297307d43ac61666389e3bc3198a3"}, ] packaging = [ - {file = "packaging-21.2-py3-none-any.whl", hash = "sha256:14317396d1e8cdb122989b916fa2c7e9ca8e2be9e8060a6eff75b6b7b4d8a7e0"}, - {file = "packaging-21.2.tar.gz", hash = "sha256:096d689d78ca690e4cd8a89568ba06d07ca097e3306a4381635073ca91479966"}, + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] passlib = [ {file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"}, @@ -2066,8 +2054,8 @@ pillow = [ {file = "Pillow-8.4.0.tar.gz", hash = "sha256:b8e2f83c56e141920c39464b852de3719dfbfb6e3c99a2d8da0edf4fb33176ed"}, ] platformdirs = [ - {file = "platformdirs-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"}, - {file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"}, + {file = "platformdirs-2.4.1-py3-none-any.whl", hash = "sha256:1d7385c7db91728b83efd0ca99a5afb296cab9d0ed8313a45ed8ba17967ecfca"}, + {file = "platformdirs-2.4.1.tar.gz", hash = "sha256:440633ddfebcc36264232365d7840a970e75e1018d15b4327d11f91909045fda"}, ] pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, @@ -2078,46 +2066,66 @@ premailer = [ {file = "premailer-3.10.0.tar.gz", hash = "sha256:d1875a8411f5dc92b53ef9f193db6c0f879dc378d618e0ad292723e388bfe4c2"}, ] psycopg2-binary = [ - {file = "psycopg2-binary-2.9.1.tar.gz", hash = "sha256:b0221ca5a9837e040ebf61f48899926b5783668b7807419e4adae8175a31f773"}, - {file = "psycopg2_binary-2.9.1-cp310-cp310-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:24b0b6688b9f31a911f2361fe818492650795c9e5d3a1bc647acbd7440142a4f"}, - {file = "psycopg2_binary-2.9.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:542875f62bc56e91c6eac05a0deadeae20e1730be4c6334d8f04c944fcd99759"}, - {file = "psycopg2_binary-2.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:661509f51531ec125e52357a489ea3806640d0ca37d9dada461ffc69ee1e7b6e"}, - {file = "psycopg2_binary-2.9.1-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:d92272c7c16e105788efe2cfa5d680f07e34e0c29b03c1908f8636f55d5f915a"}, - {file = "psycopg2_binary-2.9.1-cp310-cp310-manylinux_2_24_ppc64le.whl", hash = "sha256:736b8797b58febabb85494142c627bd182b50d2a7ec65322983e71065ad3034c"}, - {file = "psycopg2_binary-2.9.1-cp310-cp310-win32.whl", hash = "sha256:ebccf1123e7ef66efc615a68295bf6fdba875a75d5bba10a05073202598085fc"}, - {file = "psycopg2_binary-2.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:1f6ca4a9068f5c5c57e744b4baa79f40e83e3746875cac3c45467b16326bab45"}, - {file = "psycopg2_binary-2.9.1-cp36-cp36m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:c250a7ec489b652c892e4f0a5d122cc14c3780f9f643e1a326754aedf82d9a76"}, - {file = "psycopg2_binary-2.9.1-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aef9aee84ec78af51107181d02fe8773b100b01c5dfde351184ad9223eab3698"}, - {file = "psycopg2_binary-2.9.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:123c3fb684e9abfc47218d3784c7b4c47c8587951ea4dd5bc38b6636ac57f616"}, - {file = "psycopg2_binary-2.9.1-cp36-cp36m-manylinux_2_24_aarch64.whl", hash = "sha256:995fc41ebda5a7a663a254a1dcac52638c3e847f48307b5416ee373da15075d7"}, - {file = "psycopg2_binary-2.9.1-cp36-cp36m-manylinux_2_24_ppc64le.whl", hash = "sha256:fbb42a541b1093385a2d8c7eec94d26d30437d0e77c1d25dae1dcc46741a385e"}, - {file = "psycopg2_binary-2.9.1-cp36-cp36m-win32.whl", hash = "sha256:20f1ab44d8c352074e2d7ca67dc00843067788791be373e67a0911998787ce7d"}, - {file = "psycopg2_binary-2.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f6fac64a38f6768e7bc7b035b9e10d8a538a9fadce06b983fb3e6fa55ac5f5ce"}, - {file = "psycopg2_binary-2.9.1-cp37-cp37m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:1e3a362790edc0a365385b1ac4cc0acc429a0c0d662d829a50b6ce743ae61b5a"}, - {file = "psycopg2_binary-2.9.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f8559617b1fcf59a9aedba2c9838b5b6aa211ffedecabca412b92a1ff75aac1a"}, - {file = "psycopg2_binary-2.9.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a36c7eb6152ba5467fb264d73844877be8b0847874d4822b7cf2d3c0cb8cdcb0"}, - {file = "psycopg2_binary-2.9.1-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:2f62c207d1740b0bde5c4e949f857b044818f734a3d57f1d0d0edc65050532ed"}, - {file = "psycopg2_binary-2.9.1-cp37-cp37m-manylinux_2_24_ppc64le.whl", hash = "sha256:cfc523edecddaef56f6740d7de1ce24a2fdf94fd5e704091856a201872e37f9f"}, - {file = "psycopg2_binary-2.9.1-cp37-cp37m-win32.whl", hash = "sha256:1e85b74cbbb3056e3656f1cc4781294df03383127a8114cbc6531e8b8367bf1e"}, - {file = "psycopg2_binary-2.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:1473c0215b0613dd938db54a653f68251a45a78b05f6fc21af4326f40e8360a2"}, - {file = "psycopg2_binary-2.9.1-cp38-cp38-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:35c4310f8febe41f442d3c65066ca93cccefd75013df3d8c736c5b93ec288140"}, - {file = "psycopg2_binary-2.9.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c13d72ed6af7fd2c8acbd95661cf9477f94e381fce0792c04981a8283b52917"}, - {file = "psycopg2_binary-2.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14db1752acdd2187d99cb2ca0a1a6dfe57fc65c3281e0f20e597aac8d2a5bd90"}, - {file = "psycopg2_binary-2.9.1-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:aed4a9a7e3221b3e252c39d0bf794c438dc5453bc2963e8befe9d4cd324dff72"}, - {file = "psycopg2_binary-2.9.1-cp38-cp38-manylinux_2_24_ppc64le.whl", hash = "sha256:da113b70f6ec40e7d81b43d1b139b9db6a05727ab8be1ee559f3a69854a69d34"}, - {file = "psycopg2_binary-2.9.1-cp38-cp38-win32.whl", hash = "sha256:4235f9d5ddcab0b8dbd723dca56ea2922b485ea00e1dafacf33b0c7e840b3d32"}, - {file = "psycopg2_binary-2.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:988b47ac70d204aed01589ed342303da7c4d84b56c2f4c4b8b00deda123372bf"}, - {file = "psycopg2_binary-2.9.1-cp39-cp39-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:7360647ea04db2e7dff1648d1da825c8cf68dc5fbd80b8fb5b3ee9f068dcd21a"}, - {file = "psycopg2_binary-2.9.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca86db5b561b894f9e5f115d6a159fff2a2570a652e07889d8a383b5fae66eb4"}, - {file = "psycopg2_binary-2.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ced67f1e34e1a450cdb48eb53ca73b60aa0af21c46b9b35ac3e581cf9f00e31"}, - {file = "psycopg2_binary-2.9.1-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:0f2e04bd2a2ab54fa44ee67fe2d002bb90cee1c0f1cc0ebc3148af7b02034cbd"}, - {file = "psycopg2_binary-2.9.1-cp39-cp39-manylinux_2_24_ppc64le.whl", hash = "sha256:3242b9619de955ab44581a03a64bdd7d5e470cc4183e8fcadd85ab9d3756ce7a"}, - {file = "psycopg2_binary-2.9.1-cp39-cp39-win32.whl", hash = "sha256:0b7dae87f0b729922e06f85f667de7bf16455d411971b2043bbd9577af9d1975"}, - {file = "psycopg2_binary-2.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:b4d7679a08fea64573c969f6994a2631908bb2c0e69a7235648642f3d2e39a68"}, + {file = "psycopg2-binary-2.9.3.tar.gz", hash = "sha256:761df5313dc15da1502b21453642d7599d26be88bff659382f8f9747c7ebea4e"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:539b28661b71da7c0e428692438efbcd048ca21ea81af618d845e06ebfd29478"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e82d38390a03da28c7985b394ec3f56873174e2c88130e6966cb1c946508e65"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57804fc02ca3ce0dbfbef35c4b3a4a774da66d66ea20f4bda601294ad2ea6092"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:083a55275f09a62b8ca4902dd11f4b33075b743cf0d360419e2051a8a5d5ff76"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_24_ppc64le.whl", hash = "sha256:0a29729145aaaf1ad8bafe663131890e2111f13416b60e460dae0a96af5905c9"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a79d622f5206d695d7824cbf609a4f5b88ea6d6dab5f7c147fc6d333a8787e4"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:090f3348c0ab2cceb6dfbe6bf721ef61262ddf518cd6cc6ecc7d334996d64efa"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:a9e1f75f96ea388fbcef36c70640c4efbe4650658f3d6a2967b4cc70e907352e"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c3ae8e75eb7160851e59adc77b3a19a976e50622e44fd4fd47b8b18208189d42"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-win32.whl", hash = "sha256:7b1e9b80afca7b7a386ef087db614faebbf8839b7f4db5eb107d0f1a53225029"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:8b344adbb9a862de0c635f4f0425b7958bf5a4b927c8594e6e8d261775796d53"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:e847774f8ffd5b398a75bc1c18fbb56564cda3d629fe68fd81971fece2d3c67e"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68641a34023d306be959101b345732360fc2ea4938982309b786f7be1b43a4a1"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3303f8807f342641851578ee7ed1f3efc9802d00a6f83c101d21c608cb864460"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_24_aarch64.whl", hash = "sha256:e3699852e22aa68c10de06524a3721ade969abf382da95884e6a10ff798f9281"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_24_ppc64le.whl", hash = "sha256:526ea0378246d9b080148f2d6681229f4b5964543c170dd10bf4faaab6e0d27f"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:b1c8068513f5b158cf7e29c43a77eb34b407db29aca749d3eb9293ee0d3103ca"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:15803fa813ea05bef089fa78835118b5434204f3a17cb9f1e5dbfd0b9deea5af"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:152f09f57417b831418304c7f30d727dc83a12761627bb826951692cc6491e57"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:404224e5fef3b193f892abdbf8961ce20e0b6642886cfe1fe1923f41aaa75c9d"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-win32.whl", hash = "sha256:1f6b813106a3abdf7b03640d36e24669234120c72e91d5cbaeb87c5f7c36c65b"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-win_amd64.whl", hash = "sha256:2d872e3c9d5d075a2e104540965a1cf898b52274a5923936e5bfddb58c59c7c2"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:10bb90fb4d523a2aa67773d4ff2b833ec00857f5912bafcfd5f5414e45280fb1"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:874a52ecab70af13e899f7847b3e074eeb16ebac5615665db33bce8a1009cf33"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a29b3ca4ec9defec6d42bf5feb36bb5817ba3c0230dd83b4edf4bf02684cd0ae"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:12b11322ea00ad8db8c46f18b7dfc47ae215e4df55b46c67a94b4effbaec7094"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_24_ppc64le.whl", hash = "sha256:53293533fcbb94c202b7c800a12c873cfe24599656b341f56e71dd2b557be063"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c381bda330ddf2fccbafab789d83ebc6c53db126e4383e73794c74eedce855ef"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9d29409b625a143649d03d0fd7b57e4b92e0ecad9726ba682244b73be91d2fdb"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:183a517a3a63503f70f808b58bfbf962f23d73b6dccddae5aa56152ef2bcb232"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:15c4e4cfa45f5a60599d9cec5f46cd7b1b29d86a6390ec23e8eebaae84e64554"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-win32.whl", hash = "sha256:adf20d9a67e0b6393eac162eb81fb10bc9130a80540f4df7e7355c2dd4af9fba"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:2f9ffd643bc7349eeb664eba8864d9e01f057880f510e4681ba40a6532f93c71"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:def68d7c21984b0f8218e8a15d514f714d96904265164f75f8d3a70f9c295667"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dffc08ca91c9ac09008870c9eb77b00a46b3378719584059c034b8945e26b272"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:280b0bb5cbfe8039205c7981cceb006156a675362a00fe29b16fbc264e242834"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:af9813db73395fb1fc211bac696faea4ca9ef53f32dc0cfa27e4e7cf766dcf24"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_24_ppc64le.whl", hash = "sha256:63638d875be8c2784cfc952c9ac34e2b50e43f9f0a0660b65e2a87d656b3116c"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ffb7a888a047696e7f8240d649b43fb3644f14f0ee229077e7f6b9f9081635bd"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0c9d5450c566c80c396b7402895c4369a410cab5a82707b11aee1e624da7d004"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:d1c1b569ecafe3a69380a94e6ae09a4789bbb23666f3d3a08d06bbd2451f5ef1"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8fc53f9af09426a61db9ba357865c77f26076d48669f2e1bb24d85a22fb52307"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-win32.whl", hash = "sha256:6472a178e291b59e7f16ab49ec8b4f3bdada0a879c68d3817ff0963e722a82ce"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:35168209c9d51b145e459e05c31a9eaeffa9a6b0fd61689b48e07464ffd1a83e"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:47133f3f872faf28c1e87d4357220e809dfd3fa7c64295a4a148bcd1e6e34ec9"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91920527dea30175cc02a1099f331aa8c1ba39bf8b7762b7b56cbf54bc5cce42"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:887dd9aac71765ac0d0bac1d0d4b4f2c99d5f5c1382d8b770404f0f3d0ce8a39"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:1f14c8b0942714eb3c74e1e71700cbbcb415acbc311c730370e70c578a44a25c"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_24_ppc64le.whl", hash = "sha256:7af0dd86ddb2f8af5da57a976d27cd2cd15510518d582b478fbb2292428710b4"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:93cd1967a18aa0edd4b95b1dfd554cf15af657cb606280996d393dadc88c3c35"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bda845b664bb6c91446ca9609fc69f7db6c334ec5e4adc87571c34e4f47b7ddb"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:01310cf4cf26db9aea5158c217caa92d291f0500051a6469ac52166e1a16f5b7"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:99485cab9ba0fa9b84f1f9e1fef106f44a46ef6afdeec8885e0b88d0772b49e8"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-win32.whl", hash = "sha256:46f0e0a6b5fa5851bbd9ab1bc805eef362d3a230fbdfbc209f4a236d0a7a990d"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:accfe7e982411da3178ec690baaceaad3c278652998b2c45828aaac66cd8285f"}, ] py = [ - {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, - {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ] pyasn1 = [ {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"}, @@ -2150,12 +2158,12 @@ pyasn1-modules = [ {file = "pyasn1_modules-0.2.8-py3.7.egg", hash = "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd"}, ] pycodestyle = [ - {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, - {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, + {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, + {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, ] pycparser = [ - {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, - {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, ] pydantic = [ {file = "pydantic-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cb23bcc093697cdea2708baae4f9ba0e972960a835af22560f6ae4e7e47d33f5"}, @@ -2200,28 +2208,28 @@ pydantic-to-typescript = [ {file = "pydantic_to_typescript-1.0.7-py3.8.egg", hash = "sha256:b4917453d74df1b401259b881117ae164bf1bac2cac849a88466badb43825e6d"}, ] pyflakes = [ - {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, - {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, + {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, + {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, ] pygments = [ - {file = "Pygments-2.10.0-py3-none-any.whl", hash = "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380"}, - {file = "Pygments-2.10.0.tar.gz", hash = "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"}, + {file = "Pygments-2.11.2-py3-none-any.whl", hash = "sha256:44238f1b60a76d78fc8ca0528ee429702aae011c265fe6a8dd8b63049ae41c65"}, + {file = "Pygments-2.11.2.tar.gz", hash = "sha256:4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a"}, ] pyhumps = [ - {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.5.0-py3-none-any.whl", hash = "sha256:2433eef13d1c258227a0bd5de9660ba17dd6a307e1255d2d20ec9287f8626d96"}, + {file = "pyhumps-3.5.0.tar.gz", hash = "sha256:55e37f16846eaab26057200924cbdadd2152bf0a5d49175a42358464fa881c73"}, ] pylint = [ - {file = "pylint-2.11.1-py3-none-any.whl", hash = "sha256:0f358e221c45cbd4dad2a1e4b883e75d28acdcccd29d40c76eb72b307269b126"}, - {file = "pylint-2.11.1.tar.gz", hash = "sha256:2c9843fff1a88ca0ad98a256806c82c5a8f86086e7ccbdb93297d86c3f90c436"}, + {file = "pylint-2.12.2-py3-none-any.whl", hash = "sha256:daabda3f7ed9d1c60f52d563b1b854632fd90035bcf01443e234d3dc794e3b74"}, + {file = "pylint-2.12.2.tar.gz", hash = "sha256:9d945a73640e1fec07ee34b42f5669b770c759acd536ec7b16d7e4b87a9c9ff9"}, ] pymdown-extensions = [ - {file = "pymdown-extensions-9.0.tar.gz", hash = "sha256:01e4bec7f4b16beaba0087a74496401cf11afd69e3a11fe95cb593e5c698ef40"}, - {file = "pymdown_extensions-9.0-py3-none-any.whl", hash = "sha256:430cc2fbb30cef2df70edac0b4f62614a6a4d2b06462e32da4ca96098b7c1dfb"}, + {file = "pymdown-extensions-9.1.tar.gz", hash = "sha256:74247f2c80f1d9e3c7242abe1c16317da36c6f26c7ad4b8a7f457f0ec20f0365"}, + {file = "pymdown_extensions-9.1-py3-none-any.whl", hash = "sha256:b03e66f91f33af4a6e7a0e20c740313522995f69a03d86316b1449766c473d0e"}, ] pyparsing = [ - {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, - {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, + {file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"}, + {file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"}, ] pyrdfa3 = [ {file = "pyRdfa3-3.5.3-py3-none-any.whl", hash = "sha256:4da7ed49e8f524b573ed67e4f7bc7f403bff3be00546d7438fe263c924a91ccf"}, @@ -2252,7 +2260,7 @@ python-jose = [ {file = "python_jose-3.3.0-py2.py3-none-any.whl", hash = "sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a"}, ] python-ldap = [ - {file = "python-ldap-3.3.1.tar.gz", hash = "sha256:4711cacf013e298754abd70058ccc995758177fb425f1c2d30e71adfc1d00aa5"}, + {file = "python-ldap-3.4.0.tar.gz", hash = "sha256:60464c8fc25e71e0fd40449a24eae482dcd0fb7fcf823e7de627a6525b3e0d12"}, ] python-multipart = [ {file = "python-multipart-0.0.5.tar.gz", hash = "sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"}, @@ -2304,63 +2312,20 @@ pyyaml-env-tag = [ {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, ] rdflib = [ - {file = "rdflib-6.0.2-py3-none-any.whl", hash = "sha256:b7642daac8cdad1ba157fecb236f5d1b2aa1de64e714dcee80d65e2b794d88a6"}, - {file = "rdflib-6.0.2.tar.gz", hash = "sha256:6136ae056001474ee2aff5fc5b956e62a11c3a9c66bb0f3d9c0aaa5fbb56854e"}, + {file = "rdflib-6.1.1-py3-none-any.whl", hash = "sha256:fc81cef513cd552d471f2926141396b633207109d0154c8e77926222c70367fe"}, + {file = "rdflib-6.1.1.tar.gz", hash = "sha256:8dbfa0af2990b98471dacbc936d6494c997ede92fd8ed693fb84ee700ef6f754"}, ] rdflib-jsonld = [ {file = "rdflib-jsonld-0.6.2.tar.gz", hash = "sha256:107cd3019d41354c31687e64af5e3fd3c3e3fa5052ce635f5ce595fd31853a63"}, {file = "rdflib_jsonld-0.6.2-py2.py3-none-any.whl", hash = "sha256:011afe67672353ca9978ab9a4bee964dff91f14042f2d8a28c22a573779d2f8b"}, ] recipe-scrapers = [ - {file = "recipe_scrapers-13.5.0-py3-none-any.whl", hash = "sha256:37d311c01aaac5903d08cf6771512231fc3e908eb363acbca7a401a6dbdbcfb9"}, - {file = "recipe_scrapers-13.5.0.tar.gz", hash = "sha256:84d3c57b84e7ffe08bd3488fe7fba50b66819bb818d8451bfc47a84656099a14"}, -] -regex = [ - {file = "regex-2021.9.30-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:66696c8336a1b5d1182464f3af3427cc760118f26d0b09a2ddc16a976a4d2637"}, - {file = "regex-2021.9.30-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d87459ad3ab40cd8493774f8a454b2e490d8e729e7e402a0625867a983e4e02"}, - {file = "regex-2021.9.30-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78cf6a1e023caf5e9a982f5377414e1aeac55198831b852835732cfd0a0ca5ff"}, - {file = "regex-2021.9.30-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:255791523f80ea8e48e79af7120b4697ef3b74f6886995dcdb08c41f8e516be0"}, - {file = "regex-2021.9.30-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e502f8d4e5ef714bcc2c94d499684890c94239526d61fdf1096547db91ca6aa6"}, - {file = "regex-2021.9.30-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4907fb0f9b9309a5bded72343e675a252c2589a41871874feace9a05a540241e"}, - {file = "regex-2021.9.30-cp310-cp310-win32.whl", hash = "sha256:3be40f720af170a6b20ddd2ad7904c58b13d2b56f6734ee5d09bbdeed2fa4816"}, - {file = "regex-2021.9.30-cp310-cp310-win_amd64.whl", hash = "sha256:c2b180ed30856dfa70cfe927b0fd38e6b68198a03039abdbeb1f2029758d87e7"}, - {file = "regex-2021.9.30-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e6f2d2f93001801296fe3ca86515eb04915472b5380d4d8752f09f25f0b9b0ed"}, - {file = "regex-2021.9.30-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fa7ba9ab2eba7284e0d7d94f61df7af86015b0398e123331362270d71fab0b9"}, - {file = "regex-2021.9.30-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28040e89a04b60d579c69095c509a4f6a1a5379cd865258e3a186b7105de72c6"}, - {file = "regex-2021.9.30-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f588209d3e4797882cd238195c175290dbc501973b10a581086b5c6bcd095ffb"}, - {file = "regex-2021.9.30-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:42952d325439ef223e4e9db7ee6d9087b5c68c5c15b1f9de68e990837682fc7b"}, - {file = "regex-2021.9.30-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cae4099031d80703954c39680323dabd87a69b21262303160776aa0e55970ca0"}, - {file = "regex-2021.9.30-cp36-cp36m-win32.whl", hash = "sha256:0de8ad66b08c3e673b61981b9e3626f8784d5564f8c3928e2ad408c0eb5ac38c"}, - {file = "regex-2021.9.30-cp36-cp36m-win_amd64.whl", hash = "sha256:b345ecde37c86dd7084c62954468a4a655fd2d24fd9b237949dd07a4d0dd6f4c"}, - {file = "regex-2021.9.30-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6f08187136f11e430638c2c66e1db091105d7c2e9902489f0dbc69b44c222b4"}, - {file = "regex-2021.9.30-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b55442650f541d195a535ccec33078c78a9521973fb960923da7515e9ed78fa6"}, - {file = "regex-2021.9.30-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87e9c489aa98f50f367fb26cc9c8908d668e9228d327644d7aa568d47e456f47"}, - {file = "regex-2021.9.30-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e2cb7d4909ed16ed35729d38af585673f1f0833e73dfdf0c18e5be0061107b99"}, - {file = "regex-2021.9.30-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0861e7f6325e821d5c40514c551fd538b292f8cc3960086e73491b9c5d8291d"}, - {file = "regex-2021.9.30-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:81fdc90f999b2147fc62e303440c424c47e5573a9b615ed5d43a5b832efcca9e"}, - {file = "regex-2021.9.30-cp37-cp37m-win32.whl", hash = "sha256:8c1ad61fa024195136a6b7b89538030bd00df15f90ac177ca278df9b2386c96f"}, - {file = "regex-2021.9.30-cp37-cp37m-win_amd64.whl", hash = "sha256:e3770781353a4886b68ef10cec31c1f61e8e3a0be5f213c2bb15a86efd999bc4"}, - {file = "regex-2021.9.30-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9c065d95a514a06b92a5026766d72ac91bfabf581adb5b29bc5c91d4b3ee9b83"}, - {file = "regex-2021.9.30-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9925985be05d54b3d25fd6c1ea8e50ff1f7c2744c75bdc4d3b45c790afa2bcb3"}, - {file = "regex-2021.9.30-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:470f2c882f2672d8eeda8ab27992aec277c067d280b52541357e1acd7e606dae"}, - {file = "regex-2021.9.30-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ad0517df22a97f1da20d8f1c8cb71a5d1997fa383326b81f9cf22c9dadfbdf34"}, - {file = "regex-2021.9.30-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9e30838df7bfd20db6466fd309d9b580d32855f8e2c2e6d74cf9da27dcd9b63"}, - {file = "regex-2021.9.30-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5b34d2335d6aedec7dcadd3f8283b9682fadad8b9b008da8788d2fce76125ebe"}, - {file = "regex-2021.9.30-cp38-cp38-win32.whl", hash = "sha256:e07049cece3462c626d650e8bf42ddbca3abf4aa08155002c28cb6d9a5a281e2"}, - {file = "regex-2021.9.30-cp38-cp38-win_amd64.whl", hash = "sha256:37868075eda024470bd0feab872c692ac4ee29db1e14baec103257bf6cc64346"}, - {file = "regex-2021.9.30-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d331f238a7accfbbe1c4cd1ba610d4c087b206353539331e32a8f05345c74aec"}, - {file = "regex-2021.9.30-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6348a7ab2a502cbdd0b7fd0496d614007489adb7361956b38044d1d588e66e04"}, - {file = "regex-2021.9.30-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce7b1cca6c23f19bee8dc40228d9c314d86d1e51996b86f924aca302fc8f8bf9"}, - {file = "regex-2021.9.30-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1f1125bc5172ab3a049bc6f4b9c0aae95a2a2001a77e6d6e4239fa3653e202b5"}, - {file = "regex-2021.9.30-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:638e98d069b14113e8afba6a54d1ca123f712c0d105e67c1f9211b2a825ef926"}, - {file = "regex-2021.9.30-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9a0b0db6b49da7fa37ca8eddf9f40a8dbc599bad43e64f452284f37b6c34d91c"}, - {file = "regex-2021.9.30-cp39-cp39-win32.whl", hash = "sha256:9910869c472e5a6728680ca357b5846546cbbd2ab3ad5bef986ef0bc438d0aa6"}, - {file = "regex-2021.9.30-cp39-cp39-win_amd64.whl", hash = "sha256:3b71213ec3bad9a5a02e049f2ec86b3d7c3e350129ae0f4e2f99c12b5da919ed"}, - {file = "regex-2021.9.30.tar.gz", hash = "sha256:81e125d9ba54c34579e4539a967e976a3c56150796674aec318b1b2f49251be7"}, + {file = "recipe_scrapers-13.10.1-py3-none-any.whl", hash = "sha256:6dbe178af1183002b32722ad6caef4227e925502cb6b8c2b40521632be39da7e"}, + {file = "recipe_scrapers-13.10.1.tar.gz", hash = "sha256:2d0e28e3cbd7d87549b0b1fa43b56ac581631ece2164a4de1f85043440281707"}, ] requests = [ - {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, - {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, + {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, + {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, ] requests-oauthlib = [ {file = "requests-oauthlib-1.3.0.tar.gz", hash = "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"}, @@ -2368,12 +2333,12 @@ requests-oauthlib = [ {file = "requests_oauthlib-1.3.0-py3.7.egg", hash = "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"}, ] rich = [ - {file = "rich-10.12.0-py3-none-any.whl", hash = "sha256:c30d6808d1cd3defd56a7bd2d587d13e53b5f55de6cf587f035bcbb56bc3f37b"}, - {file = "rich-10.12.0.tar.gz", hash = "sha256:83fb3eff778beec3c55201455c17cccde1ccdf66d5b4dade8ef28f56b50c4bd4"}, + {file = "rich-10.16.2-py3-none-any.whl", hash = "sha256:c59d73bd804c90f747c8d7b1d023b88f2a9ac2454224a4aeaf959b21eeb42d03"}, + {file = "rich-10.16.2.tar.gz", hash = "sha256:720974689960e06c2efdb54327f8bf0cdbdf4eae4ad73b6c94213cad405c371b"}, ] rsa = [ - {file = "rsa-4.7.2-py3-none-any.whl", hash = "sha256:78f9a9bf4e7be0c5ded4583326e7461e3a3c5aae24073648b4bdfa797d78c9d2"}, - {file = "rsa-4.7.2.tar.gz", hash = "sha256:9d689e6ca1b3038bc82bf8d23e944b6b6037bc02301a574935b2dd946e0353b9"}, + {file = "rsa-4.8-py3-none-any.whl", hash = "sha256:95c5d300c4e879ee69708c428ba566c59478fd653cc3a22243eeb8ed846950bb"}, + {file = "rsa-4.8.tar.gz", hash = "sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17"}, ] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, @@ -2384,8 +2349,8 @@ sniffio = [ {file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"}, ] soupsieve = [ - {file = "soupsieve-2.2.1-py3-none-any.whl", hash = "sha256:c2c1c2d44f158cdbddab7824a9af8c4f83c76b1e23e049479aa432feb6c4c23b"}, - {file = "soupsieve-2.2.1.tar.gz", hash = "sha256:052774848f448cf19c7e959adf5566904d525f33a3f8b6ba6f6f8f26ec7de0cc"}, + {file = "soupsieve-2.3.1-py3-none-any.whl", hash = "sha256:1a3cca2617c6b38c0343ed661b1fa5de5637f257d4fe22bd9f1338010a1efefb"}, + {file = "soupsieve-2.3.1.tar.gz", hash = "sha256:b8d49b1cd4f037c7082a9683dfa1801aa2597fb11c3a1155b7a5b94829b4f1f9"}, ] sqlalchemy = [ {file = "SQLAlchemy-1.4.29-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:da64423c05256f4ab8c0058b90202053b201cbe3a081f3a43eb590cd554395ab"}, @@ -2438,13 +2403,12 @@ toml = [ {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] tomli = [ - {file = "tomli-1.2.2-py3-none-any.whl", hash = "sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade"}, - {file = "tomli-1.2.2.tar.gz", hash = "sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee"}, + {file = "tomli-1.2.3-py3-none-any.whl", hash = "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"}, + {file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"}, ] typing-extensions = [ - {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, - {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, - {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, + {file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"}, + {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"}, ] tzdata = [ {file = "tzdata-2021.5-py2.py3-none-any.whl", hash = "sha256:3eee491e22ebfe1e5cfcc97a4137cd70f092ce59144d81f8924a844de05ba8f5"}, @@ -2455,8 +2419,8 @@ tzlocal = [ {file = "tzlocal-4.1.tar.gz", hash = "sha256:0f28015ac68a5c067210400a9197fc5d36ba9bc3f8eaf1da3cbd59acdfed9e09"}, ] urllib3 = [ - {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"}, - {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"}, + {file = "urllib3-1.26.8-py2.py3-none-any.whl", hash = "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed"}, + {file = "urllib3-1.26.8.tar.gz", hash = "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"}, ] uvicorn = [ {file = "uvicorn-0.13.4-py3-none-any.whl", hash = "sha256:7587f7b08bd1efd2b9bad809a3d333e972f1d11af8a5e52a9371ee3a5de71524"}, @@ -2595,6 +2559,6 @@ wrapt = [ {file = "wrapt-1.13.3.tar.gz", hash = "sha256:1fea9cd438686e6682271d36f3481a9f3636195578bab9ca3382e2f5f01fc185"}, ] zipp = [ - {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"}, - {file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"}, + {file = "zipp-3.7.0-py3-none-any.whl", hash = "sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375"}, + {file = "zipp-3.7.0.tar.gz", hash = "sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d"}, ] diff --git a/pyproject.toml b/pyproject.toml index 7b87c916..1dc1b75f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,13 +9,13 @@ license = "MIT" start = "mealie.app:main" [tool.poetry.dependencies] -python = "^3.9" +python = "^3.10" aiofiles = "0.5.0" aniso8601 = "7.0.0" appdirs = "1.4.4" fastapi = "^0.71.0" uvicorn = {extras = ["standard"], version = "^0.13.0"} -APScheduler = "^3.6.3" +APScheduler = "^3.8.1" SQLAlchemy = "^1.4.29" Jinja2 = "^2.11.2" python-dotenv = "^0.15.0" @@ -28,10 +28,10 @@ fastapi-camelcase = "^1.0.2" bcrypt = "^3.2.0" python-jose = "^3.3.0" passlib = "^1.7.4" -lxml = "4.6.2" +lxml = "^4.7.1" Pillow = "^8.2.0" pathvalidate = "^2.4.1" -apprise = "0.9.3" +apprise = "^0.9.6" recipe-scrapers = "^13.5.0" psycopg2-binary = {version = "^2.9.1", optional = true} gunicorn = "^20.1.0" @@ -39,20 +39,20 @@ emails = "^0.6" python-i18n = "^0.3.9" python-ldap = "^3.3.1" pydantic = "^1.9.0" +tzdata = "^2021.5" [tool.poetry.dev-dependencies] pylint = "^2.6.0" pytest = "^6.2.1" pytest-cov = "^2.11.0" mkdocs-material = "^7.0.2" -flake8 = "^3.9.0" +flake8 = "^4.0.1" coverage = "^5.5" pydantic-to-typescript = "^1.0.7" rich = "^10.7.0" isort = "^5.9.3" -regex = "2021.9.30" # TODO: Remove during Upgrade -> https://github.com/psf/black/issues/2524 flake8-print = "^4.0.0" -black = "^21.11b1" +black = "^21.12b0" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/tests/integration_tests/user_group_tests/test_group_notifications.py b/tests/integration_tests/user_group_tests/test_group_notifications.py new file mode 100644 index 00000000..abc4bfe6 --- /dev/null +++ b/tests/integration_tests/user_group_tests/test_group_notifications.py @@ -0,0 +1,118 @@ +from fastapi.testclient import TestClient + +from mealie.schema.group.group_events import GroupEventNotifierCreate, GroupEventNotifierOptions +from tests.utils.assertion_helpers import assert_ignore_keys +from tests.utils.factories import random_bool, random_string +from tests.utils.fixture_schemas import TestUser + + +class Routes: + base = "/api/groups/events/notifications" + + def item(item_id: int) -> str: + return f"{Routes.base}/{item_id}" + + +def preferences_generator(): + return GroupEventNotifierOptions( + recipe_created=random_bool(), + recipe_updated=random_bool(), + recipe_deleted=random_bool(), + user_signup=random_bool(), + data_migrations=random_bool(), + data_export=random_bool(), + data_import=random_bool(), + mealplan_entry_created=random_bool(), + shopping_list_created=random_bool(), + shopping_list_updated=random_bool(), + shopping_list_deleted=random_bool(), + cookbook_created=random_bool(), + cookbook_updated=random_bool(), + cookbook_deleted=random_bool(), + tag_created=random_bool(), + tag_updated=random_bool(), + tag_deleted=random_bool(), + category_created=random_bool(), + category_updated=random_bool(), + category_deleted=random_bool(), + ).dict(by_alias=True) + + +def notifier_generator(): + return GroupEventNotifierCreate( + name=random_string(), + apprise_url=random_string(), + ).dict(by_alias=True) + + +def test_create_notification(api_client: TestClient, unique_user: TestUser): + payload = notifier_generator() + response = api_client.post(Routes.base, json=payload, headers=unique_user.token) + assert response.status_code == 201 + + payload_as_dict = response.json() + + assert payload_as_dict["name"] == payload["name"] + assert payload_as_dict["enabled"] is True + + # Ensure Apprise URL Staysa Private + assert "apprise_url" not in payload_as_dict + + # Cleanup + response = api_client.delete(Routes.item(payload_as_dict["id"]), headers=unique_user.token) + + +def test_ensure_apprise_url_is_secret(api_client: TestClient, unique_user: TestUser): + payload = notifier_generator() + response = api_client.post(Routes.base, json=payload, headers=unique_user.token) + assert response.status_code == 201 + + payload_as_dict = response.json() + + # Ensure Apprise URL Staysa Private + assert "apprise_url" not in payload_as_dict + + +def test_update_notification(api_client: TestClient, unique_user: TestUser): + payload = notifier_generator() + response = api_client.post(Routes.base, json=payload, headers=unique_user.token) + assert response.status_code == 201 + + update_payload = response.json() + + # Set Update Values + update_payload["name"] = random_string() + update_payload["enabled"] = random_bool() + update_payload["options"] = preferences_generator() + + response = api_client.put(Routes.item(update_payload["id"]), json=update_payload, headers=unique_user.token) + + assert response.status_code == 200 + + # Re-Get The Item + response = api_client.get(Routes.item(update_payload["id"]), headers=unique_user.token) + assert response.status_code == 200 + + # Validate Updated Values + updated_payload = response.json() + + assert updated_payload["name"] == update_payload["name"] + assert updated_payload["enabled"] == update_payload["enabled"] + assert_ignore_keys(updated_payload["options"], update_payload["options"]) + + # Cleanup + response = api_client.delete(Routes.item(update_payload["id"]), headers=unique_user.token) + + +def test_delete_notification(api_client: TestClient, unique_user: TestUser): + payload = notifier_generator() + response = api_client.post(Routes.base, json=payload, headers=unique_user.token) + assert response.status_code == 201 + + payload_as_dict = response.json() + + response = api_client.delete(Routes.item(payload_as_dict["id"]), headers=unique_user.token) + assert response.status_code == 204 + + response = api_client.get(Routes.item(payload_as_dict["id"]), headers=unique_user.token) + assert response.status_code == 404