diff --git a/.gitignore b/.gitignore index c43ea0b6..231db452 100644 --- a/.gitignore +++ b/.gitignore @@ -158,3 +158,4 @@ dev/code-generation/generated/openapi.json dev/code-generation/generated/test_routes.py mealie/services/parser_services/crfpp/model.crfmodel lcov.info +dev/code-generation/openapi.json diff --git a/dev/code-generation/_static.py b/dev/code-generation/_static.py deleted file mode 100644 index d05fba10..00000000 --- a/dev/code-generation/_static.py +++ /dev/null @@ -1,26 +0,0 @@ -from pathlib import Path - -CWD = Path(__file__).parent -PROJECT_DIR = Path(__file__).parent.parent.parent - - -class Directories: - out_dir = CWD / "generated" - - -class CodeTemplates: - interface = CWD / "templates" / "interface.js" - pytest_routes = CWD / "templates" / "test_routes.py.j2" - - -class CodeDest: - interface = CWD / "generated" / "interface.js" - pytest_routes = CWD / "generated" / "test_routes.py" - use_locales = PROJECT_DIR / "frontend" / "composables" / "use-locales" / "available-locales.ts" - - -class CodeKeys: - """Hard coded comment IDs that are used to generate code""" - - nuxt_local_messages = "MESSAGE_LOCALES" - nuxt_local_dates = "DATE_LOCALES" diff --git a/dev/scripts/api_docs_gen.py b/dev/code-generation/gen_docs_api.py similarity index 100% rename from dev/scripts/api_docs_gen.py rename to dev/code-generation/gen_docs_api.py diff --git a/dev/code-generation/gen_test_data_paths.py b/dev/code-generation/gen_py_pytest_data_paths.py similarity index 94% rename from dev/code-generation/gen_test_data_paths.py rename to dev/code-generation/gen_py_pytest_data_paths.py index ed357966..51cdd854 100644 --- a/dev/code-generation/gen_test_data_paths.py +++ b/dev/code-generation/gen_py_pytest_data_paths.py @@ -1,8 +1,8 @@ from dataclasses import dataclass from pathlib import Path -from _gen_utils import log, render_python_template from slugify import slugify +from utils import render_python_template CWD = Path(__file__).parent @@ -25,9 +25,7 @@ class TestDataPath: # Remove any file extension var = var.split(".")[0] - var = var.replace("'", "") - var = slugify(var, separator="_") return cls(var, rel_path) @@ -97,8 +95,6 @@ def rename_non_compliant_paths(): def main(): - log.info("Starting Template Generation") - rename_non_compliant_paths() GENERATED.mkdir(exist_ok=True) @@ -115,8 +111,6 @@ def main(): {"children": all_children}, ) - log.info("Finished Template Generation") - if __name__ == "__main__": main() diff --git a/dev/code-generation/gen_py_pytest_routes.py b/dev/code-generation/gen_py_pytest_routes.py new file mode 100644 index 00000000..858e83d1 --- /dev/null +++ b/dev/code-generation/gen_py_pytest_routes.py @@ -0,0 +1,84 @@ +import json +from pathlib import Path + +from fastapi import FastAPI +from jinja2 import Template +from pydantic import BaseModel +from utils import PROJECT_DIR, CodeTemplates, HTTPRequest, RouteObject + +CWD = Path(__file__).parent + +OUTFILE = PROJECT_DIR / "tests" / "utils" / "api_routes" / "__init__.py" + + +class PathObject(BaseModel): + route_object: RouteObject + http_verbs: list[HTTPRequest] + + class Config: + arbitrary_types_allowed = True + + +def get_path_objects(app: FastAPI): + paths = [] + + for key, value in app.openapi().items(): + if key == "paths": + for key, value in value.items(): + + paths.append( + PathObject( + route_object=RouteObject(key), + http_verbs=[HTTPRequest(request_type=k, **v) for k, v in value.items()], + ) + ) + + return paths + + +def dump_open_api(app: FastAPI): + """Writes the Open API as JSON to a json file""" + OPEN_API_FILE = CWD / "openapi.json" + + with open(OPEN_API_FILE, "w") as f: + f.write(json.dumps(app.openapi())) + + +def read_template(file: Path): + with open(file) as f: + return f.read() + + +def generate_python_templates(static_paths: list[PathObject], function_paths: list[PathObject]): + + template = Template(read_template(CodeTemplates.pytest_routes)) + content = template.render( + paths={ + "prefix": "/api", + "static_paths": static_paths, + "function_paths": function_paths, + } + ) + with open(OUTFILE, "w") as f: + f.write(content) + + return + + +def main(): + from mealie.app import app + + dump_open_api(app) + paths = get_path_objects(app) + + static_paths = [x.route_object for x in paths if not x.route_object.is_function] + function_paths = [x.route_object for x in paths if x.route_object.is_function] + + static_paths.sort(key=lambda x: x.router_slug) + function_paths.sort(key=lambda x: x.router_slug) + + generate_python_templates(static_paths, function_paths) + + +if __name__ == "__main__": + main() diff --git a/dev/code-generation/gen_py_schema_exports.py b/dev/code-generation/gen_py_schema_exports.py new file mode 100644 index 00000000..73f520c5 --- /dev/null +++ b/dev/code-generation/gen_py_schema_exports.py @@ -0,0 +1,102 @@ +import pathlib +import re +from dataclasses import dataclass, field + +from utils import PROJECT_DIR, log, render_python_template + +template = """# This file is auto-generated by gen_schema_exports.py +{% for file in data.module.files %}{{ file.import_str() }} +{% endfor %} + +__all__ = [ + {% for file in data.module.files %} + {%- for class in file.classes -%} + "{{ class }}", + {%- endfor -%} + {%- endfor %} +] + +""" + +SCHEMA_PATH = PROJECT_DIR / "mealie" / "schema" + +SKIP = {"static", "__pycache__"} + + +class PyFile: + import_path: str + """The import path of the file""" + + classes: list[str] + """A list of classes in the file""" + + def __init__(self, path: pathlib.Path): + self.import_path = path.stem + self.classes = [] + + self.classes = PyFile.extract_classes(path) + self.classes.sort() + + def import_str(self) -> str: + """Returns a string that can be used to import the file""" + return f"from .{self.import_path} import {', '.join(self.classes)}" + + @staticmethod + def extract_classes(file_path: pathlib.Path) -> list[str]: + name = file_path.stem + + if name == "__init__" or name.startswith("_"): + return [] + + classes = re.findall(r"(?m)^class\s(\w+)", file_path.read_text()) + return classes + + +@dataclass +class Modules: + directory: pathlib.Path + """The directory to search for modules""" + + files: list[PyFile] = field(default_factory=list) + """A list of files in the directory""" + + def __post_init__(self): + for file in self.directory.glob("*.py"): + if file.name.startswith("_"): + continue + + pfile = PyFile(file) + + if len(pfile.classes) > 0: + self.files.append(pfile) + + else: + log.debug(f"Skipping {file.name} as it has no classes") + + +def find_modules(root: pathlib.Path) -> list[Modules]: + """Finds all the top level modules in the provided folder""" + modules: list[Modules] = [] + for file in root.iterdir(): + if file.is_dir() and file.name not in SKIP: + + modules.append(Modules(directory=file)) + + return modules + + +def main(): + + modules = find_modules(SCHEMA_PATH) + + for module in modules: + log.debug(f"Module: {module.directory.name}") + for file in module.files: + log.debug(f" File: {file.import_path}") + log.debug(f" Classes: [{', '.join(file.classes)}]") + + render_python_template(template, module.directory / "__init__.py", {"module": module}) + + +if __name__ == "__main__": + main() diff --git a/dev/code-generation/gen_pytest_routes.py b/dev/code-generation/gen_pytest_routes.py deleted file mode 100644 index 8e7e288d..00000000 --- a/dev/code-generation/gen_pytest_routes.py +++ /dev/null @@ -1,53 +0,0 @@ -import json -from typing import Any - -from _gen_utils import render_python_template -from _open_api_parser import OpenAPIParser -from _static import CodeDest, CodeTemplates -from rich.console import Console - -from mealie.app import app - -""" -This code is used for generating route objects for each route in the OpenAPI Specification. -Currently, they are NOT automatically injected into the test suite. As such, you'll need to copy -the relevant contents of the generated file into the test suite where applicable. I am slowly -migrating the test suite to use this new generated file and this process will be "automated" in the -future. -""" - -console = Console() - - -def write_dict_to_file(file_name: str, data: dict[str, Any]): - with open(file_name, "w") as f: - f.write(json.dumps(data, indent=4)) - - -def main(): - print("Starting...") - open_api = OpenAPIParser(app) - modules = open_api.get_by_module() - - mods = [] - - for mod, value in modules.items(): - - routes = [] - existings = set() - # Reduce routes by unique py_route attribute - for route in value: - if route.py_route not in existings: - existings.add(route.py_route) - routes.append(route) - - module = {"name": mod, "routes": routes} - mods.append(module) - - render_python_template(CodeTemplates.pytest_routes, CodeDest.pytest_routes, {"mods": mods}) - - print("Finished...") - - -if __name__ == "__main__": - main() diff --git a/dev/code-generation/gen_schema_exports.py b/dev/code-generation/gen_schema_exports.py deleted file mode 100644 index 33ad1f33..00000000 --- a/dev/code-generation/gen_schema_exports.py +++ /dev/null @@ -1,34 +0,0 @@ -from _gen_utils import log, 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(): - log.info(f"Skipping {schema}") - continue - - log.info(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(): - log.info("Starting...") - generate_init_files() - log.info("Finished...") - - -if __name__ == "__main__": - main() diff --git a/dev/code-generation/gen_locales.py b/dev/code-generation/gen_ts_locales.py similarity index 91% rename from dev/code-generation/gen_locales.py rename to dev/code-generation/gen_ts_locales.py index c9f997d1..451918c3 100644 --- a/dev/code-generation/gen_locales.py +++ b/dev/code-generation/gen_ts_locales.py @@ -1,11 +1,12 @@ import pathlib +from pathlib import Path -import _static import dotenv import requests -from _gen_utils import log from jinja2 import Template +from pydantic import Extra from requests import Response +from utils import CodeDest, CodeKeys, inject_inline, log from mealie.schema._mealie import MealieModel @@ -13,9 +14,6 @@ BASE = pathlib.Path(__file__).parent.parent.parent API_KEY = dotenv.get_key(BASE / ".env", "CROWDIN_API_KEY") -if API_KEY is None or API_KEY == "": - log.info("CROWDIN_API_KEY is not set") - exit(1) NAMES = { "en-US": "American English", @@ -71,6 +69,9 @@ class TargetLanguage(MealieModel): twoLettersCode: str progress: float = 0.0 + class Config: + extra = Extra.allow + class CrowdinApi: project_name = "Mealie" @@ -127,11 +128,6 @@ class CrowdinApi: return response.json() -from pathlib import Path - -from _gen_utils import inject_inline -from _static import CodeKeys - PROJECT_DIR = Path(__file__).parent.parent.parent @@ -156,7 +152,7 @@ def inject_nuxt_values(): lang_string = f'{{ code: "{match.stem}", file: "{match.name}" }},' all_langs.append(lang_string) - log.info(f"injecting locales into nuxt config -> {nuxt_config}") + log.debug(f"injecting locales into nuxt config -> {nuxt_config}") inject_inline(nuxt_config, CodeKeys.nuxt_local_messages, all_langs) inject_inline(nuxt_config, CodeKeys.nuxt_local_dates, all_date_locales) @@ -167,18 +163,19 @@ def generate_locales_ts_file(): tmpl = Template(LOCALE_TEMPLATE) rendered = tmpl.render(locales=models) - log.info(f"generating locales ts file -> {_static.CodeDest.use_locales}") - with open(_static.CodeDest.use_locales, "w") as f: + log.debug(f"generating locales ts file -> {CodeDest.use_locales}") + with open(CodeDest.use_locales, "w") as f: f.write(rendered) # type:ignore def main(): + if API_KEY is None or API_KEY == "": + log.error("CROWDIN_API_KEY is not set") + return + generate_locales_ts_file() - inject_nuxt_values() - log.info("finished code generation") - if __name__ == "__main__": main() diff --git a/dev/code-generation/gen_frontend_types.py b/dev/code-generation/gen_ts_types.py similarity index 75% rename from dev/code-generation/gen_frontend_types.py rename to dev/code-generation/gen_ts_types.py index 172e8a42..d1c319e8 100644 --- a/dev/code-generation/gen_frontend_types.py +++ b/dev/code-generation/gen_ts_types.py @@ -1,29 +1,29 @@ from pathlib import Path -from _gen_utils import log from jinja2 import Template from pydantic2ts import generate_typescript_defs +from utils import log # ============================================================ -# 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"; +{% for name in global %}import {{ name }} from "@/components/global/{{ name }}.vue"; +{% endfor %}{% for name in layout %}import {{ name }} from "@/components/layout/{{ 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 %} - } +{% for name in global -%} + {{ " " }}{{ name }}: typeof {{ name }}; +{% endfor -%} + {{ " " }}// Layout Components +{% for name in layout -%} + {{ " " }}{{ name }}: typeof {{ name }}; +{% endfor -%}{{ " }"}} } export {}; + """ CWD = Path(__file__).parent @@ -46,6 +46,7 @@ def generate_global_components_types() -> None: data = {} for name, path in component_paths.items(): components = [component.stem for component in path.glob("*.vue")] + components.sort() data[name] = components return data @@ -101,22 +102,27 @@ def generate_typescript_types() -> None: failed_modules.append(module) log.error(f"Module Error: {e}") - log.info("\n📁 Skipped Directories:") + log.debug("\n📁 Skipped Directories:") for skipped_dir in skipped_dirs: - log.info(f" 📁 {skipped_dir.name}") + log.debug(f" 📁 {skipped_dir.name}") - log.info("📄 Skipped Files:") + log.debug("📄 Skipped Files:") for f in skipped_files: - log.info(f" 📄 {f.name}") + log.debug(f" 📄 {f.name}") - log.error("❌ Failed Modules:") - for f in failed_modules: - log.error(f" ❌ {f.name}") + if len(failed_modules) > 0: + log.error("❌ Failed Modules:") + for f in failed_modules: + log.error(f" ❌ {f.name}") + + +def main(): + log.debug("\n-- Starting Global Components Generator --") + generate_global_components_types() + + log.debug("\n-- Starting Pydantic To Typescript Generator --") + generate_typescript_types() if __name__ == "__main__": - log.info("\n-- Starting Global Components Generator --") - generate_global_components_types() - - log.info("\n-- Starting Pydantic To Typescript Generator --") - generate_typescript_types() + main() diff --git a/dev/code-generation/main.py b/dev/code-generation/main.py new file mode 100644 index 00000000..2fd542e7 --- /dev/null +++ b/dev/code-generation/main.py @@ -0,0 +1,28 @@ +from pathlib import Path + +import gen_py_pytest_data_paths +import gen_py_pytest_routes +import gen_py_schema_exports +import gen_ts_locales +import gen_ts_types +from utils import log + +CWD = Path(__file__).parent + + +def main(): + items = [ + (gen_py_schema_exports.main, "schema exports"), + (gen_ts_types.main, "frontend types"), + (gen_ts_locales.main, "locales"), + (gen_py_pytest_data_paths.main, "test data paths"), + (gen_py_pytest_routes.main, "pytest routes"), + ] + + for func, name in items: + log.info(f"Generating {name}...") + func() + + +if __name__ == "__main__": + main() diff --git a/dev/code-generation/templates/test_routes.py.j2 b/dev/code-generation/templates/test_routes.py.j2 index 621d492e..5dcae462 100644 --- a/dev/code-generation/templates/test_routes.py.j2 +++ b/dev/code-generation/templates/test_routes.py.j2 @@ -1,9 +1,11 @@ -{% for mod in mods %} -class {{mod.name}}Routes:{% for route in mod.routes %}{% if not route.path_is_func %} - {{route.name_snake}} = "{{ route.py_route }}"{% endif %}{% endfor %}{% for route in mod.routes %} -{% if route.path_is_func %} - @staticmethod - def {{route.name_snake}}({{ route.path_vars|join(", ") }}): - return f"{{route.py_route}}" -{% endif %}{% endfor %} -{% endfor %} \ No newline at end of file +# This Content is Auto Generated for Pytest +prefix = "{{paths.prefix}}" +{% for path in paths.static_paths %} +{{ path.router_slug }} = "{{path.prefix}}{{ path.route }}" +"""`{{path.prefix}}{{ path.route }}`"""{% endfor %} +{% for path in paths.function_paths %} + +def {{path.router_slug}}({{path.var|join(", ")}}): + """`{{ paths.prefix }}{{ path.route }}`""" + return f"{prefix}{{ path.route }}" +{% endfor %} diff --git a/dev/code-generation/utils/__init__.py b/dev/code-generation/utils/__init__.py new file mode 100644 index 00000000..f5f650a2 --- /dev/null +++ b/dev/code-generation/utils/__init__.py @@ -0,0 +1,25 @@ +from .open_api_parser import OpenAPIParser +from .route import HTTPRequest, ParameterIn, RequestBody, RequestType, RouteObject, RouterParameter +from .static import PROJECT_DIR, CodeDest, CodeKeys, CodeTemplates, Directories +from .template import CodeSlicer, find_start_end, get_indentation_of_string, inject_inline, log, render_python_template + +__all__ = [ + "CodeDest", + "CodeKeys", + "CodeSlicer", + "CodeTemplates", + "Directories", + "find_start_end", + "get_indentation_of_string", + "HTTPRequest", + "inject_inline", + "log", + "OpenAPIParser", + "ParameterIn", + "PROJECT_DIR", + "render_python_template", + "RequestBody", + "RequestType", + "RouteObject", + "RouterParameter", +] diff --git a/dev/code-generation/_open_api_parser.py b/dev/code-generation/utils/open_api_parser.py similarity index 99% rename from dev/code-generation/_open_api_parser.py rename to dev/code-generation/utils/open_api_parser.py index 9bb336dc..1f042793 100644 --- a/dev/code-generation/_open_api_parser.py +++ b/dev/code-generation/utils/open_api_parser.py @@ -3,11 +3,12 @@ import re from pathlib import Path from typing import Any -from _static import Directories from fastapi import FastAPI from humps import camelize from slugify import slugify +from .static import Directories + def get_openapi_spec_by_ref(app, type_reference: str) -> dict: if not type_reference: diff --git a/dev/code-generation/_router.py b/dev/code-generation/utils/route.py similarity index 90% rename from dev/code-generation/_router.py rename to dev/code-generation/utils/route.py index f89a084d..16f40615 100644 --- a/dev/code-generation/_router.py +++ b/dev/code-generation/utils/route.py @@ -3,7 +3,7 @@ from enum import Enum from typing import Optional from humps import camelize -from pydantic import BaseModel, Field +from pydantic import BaseModel, Extra, Field from slugify import slugify @@ -30,6 +30,7 @@ class RequestType(str, Enum): class ParameterIn(str, Enum): query = "query" path = "path" + header = "header" class RouterParameter(BaseModel): @@ -37,10 +38,16 @@ class RouterParameter(BaseModel): name: str location: ParameterIn = Field(..., alias="in") + class Config: + extra = Extra.allow + class RequestBody(BaseModel): required: bool = False + class Config: + extra = Extra.allow + class HTTPRequest(BaseModel): request_type: RequestType @@ -49,7 +56,10 @@ class HTTPRequest(BaseModel): requestBody: Optional[RequestBody] parameters: list[RouterParameter] = [] - tags: list[str] + tags: list[str] | None = [] + + class Config: + extra = Extra.allow def list_as_js_object_string(self, parameters, braces=True): if len(parameters) == 0: diff --git a/dev/code-generation/utils/static.py b/dev/code-generation/utils/static.py new file mode 100644 index 00000000..7c48f0c6 --- /dev/null +++ b/dev/code-generation/utils/static.py @@ -0,0 +1,26 @@ +from pathlib import Path + +PARENT = Path(__file__).parent.parent +PROJECT_DIR = Path(__file__).parent.parent.parent.parent + + +class Directories: + out_dir = PARENT / "generated" + + +class CodeTemplates: + interface = PARENT / "templates" / "interface.js" + pytest_routes = PARENT / "templates" / "test_routes.py.j2" + + +class CodeDest: + interface = PARENT / "generated" / "interface.js" + pytest_routes = PARENT / "generated" / "test_routes.py" + use_locales = PROJECT_DIR / "frontend" / "composables" / "use-locales" / "available-locales.ts" + + +class CodeKeys: + """Hard coded comment IDs that are used to generate code""" + + nuxt_local_messages = "MESSAGE_LOCALES" + nuxt_local_dates = "DATE_LOCALES" diff --git a/dev/code-generation/_gen_utils.py b/dev/code-generation/utils/template.py similarity index 98% rename from dev/code-generation/_gen_utils.py rename to dev/code-generation/utils/template.py index 8d2af8f5..eb5f21b0 100644 --- a/dev/code-generation/_gen_utils.py +++ b/dev/code-generation/utils/template.py @@ -22,7 +22,9 @@ def render_python_template(template_file: Path | str, dest: Path, data: dict): tplt = Template(template_file) text = tplt.render(data=data) + text = black.format_str(text, mode=black.FileMode()) + dest.write_text(text) isort.file(dest) @@ -52,7 +54,7 @@ def get_indentation_of_string(line: str, comment_char: str = "//") -> str: return re.sub(rf"{comment_char}.*", "", line).removesuffix("\n") -def find_start_end(file_text: list[str], gen_id: str) -> tuple[int, int]: +def find_start_end(file_text: list[str], gen_id: str) -> tuple[int, int, str]: start = None end = None indentation = None @@ -90,7 +92,7 @@ def inject_inline(file_path: Path, key: str, code: list[str]) -> None: """ - with open(file_path, "r") as f: + with open(file_path) as f: file_text = f.readlines() start, end, indentation = find_start_end(file_text, key) diff --git a/dev/scripts/all_recipes_stress_test.py b/dev/scripts/all_recipes_stress_test.py index 40135a25..b5f3a745 100644 --- a/dev/scripts/all_recipes_stress_test.py +++ b/dev/scripts/all_recipes_stress_test.py @@ -2,8 +2,13 @@ import json import random import string import time +from dataclasses import dataclass import requests +from rich.console import Console +from rich.table import Table + +console = Console() def random_string(length: int) -> str: @@ -14,6 +19,218 @@ def payload_factory() -> dict: return {"name": random_string(15)} +def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dict: + return { + "id": id, + "userId": userId, + "groupId": groupId, + "name": name, + "slug": slug, + "image": "tNRG", + "recipeYield": "9 servings", + "totalTime": "33 Minutes", + "prepTime": "20 Minutes", + "cookTime": None, + "performTime": "13 Minutes", + "description": "These Levain Bakery-Style Peanut Butter Cookies are the ULTIMATE for serious PB lovers! Supremely thick and chewy with gooey centers and a soft texture, they're packed with peanut butter flavor and Reese's Pieces for the most amazing cookie ever!", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": None, + "orgURL": "https://thedomesticrebel.com/2021/04/28/levain-bakery-style-ultimate-peanut-butter-cookies/", + "recipeIngredient": [ + { + "title": None, + "note": "1 cup unsalted butter, cut into cubes", + "unit": None, + "food": None, + "disableAmount": True, + "quantity": 1, + "originalText": None, + "referenceId": "ea3b6702-9532-4fbc-a40b-f99917831c26", + }, + { + "title": None, + "note": "1 cup light brown sugar", + "unit": None, + "food": None, + "disableAmount": True, + "quantity": 1, + "originalText": None, + "referenceId": "c5bbfefb-1e23-4ffd-af88-c0363a0fae82", + }, + { + "title": None, + "note": "1/2 cup granulated white sugar", + "unit": None, + "food": None, + "disableAmount": True, + "quantity": 1, + "originalText": None, + "referenceId": "034f481b-c426-4a17-b983-5aea9be4974b", + }, + { + "title": None, + "note": "2 large eggs", + "unit": None, + "food": None, + "disableAmount": True, + "quantity": 1, + "originalText": None, + "referenceId": "37c1f796-3bdb-4856-859f-dbec90bc27e4", + }, + { + "title": None, + "note": "2 tsp vanilla extract", + "unit": None, + "food": None, + "disableAmount": True, + "quantity": 1, + "originalText": None, + "referenceId": "85561ace-f249-401d-834c-e600a2f6280e", + }, + { + "title": None, + "note": "1/2 cup creamy peanut butter", + "unit": None, + "food": None, + "disableAmount": True, + "quantity": 1, + "originalText": None, + "referenceId": "ac91bda0-e8a8-491a-976a-ae4e72418cfd", + }, + { + "title": None, + "note": "1 tsp cornstarch", + "unit": None, + "food": None, + "disableAmount": True, + "quantity": 1, + "originalText": None, + "referenceId": "4d1256b3-115e-4475-83cd-464fbc304cb0", + }, + { + "title": None, + "note": "1 tsp baking soda", + "unit": None, + "food": None, + "disableAmount": True, + "quantity": 1, + "originalText": None, + "referenceId": "64627441-39f9-4ee3-8494-bafe36451d12", + }, + { + "title": None, + "note": "1/2 tsp salt", + "unit": None, + "food": None, + "disableAmount": True, + "quantity": 1, + "originalText": None, + "referenceId": "7ae212d0-3cd1-44b0-899e-ec5bd91fd384", + }, + { + "title": None, + "note": "1 cup cake flour", + "unit": None, + "food": None, + "disableAmount": True, + "quantity": 1, + "originalText": None, + "referenceId": "06967994-8548-4952-a8cc-16e8db228ebd", + }, + { + "title": None, + "note": "2 cups all-purpose flour", + "unit": None, + "food": None, + "disableAmount": True, + "quantity": 1, + "originalText": None, + "referenceId": "bdb33b23-c767-4465-acf8-3b8e79eb5691", + }, + { + "title": None, + "note": "2 cups peanut butter chips", + "unit": None, + "food": None, + "disableAmount": True, + "quantity": 1, + "originalText": None, + "referenceId": "12ba0af8-affd-4fb2-9cca-6f1b3e8d3aef", + }, + { + "title": None, + "note": "1½ cups Reese's Pieces candies", + "unit": None, + "food": None, + "disableAmount": True, + "quantity": 1, + "originalText": None, + "referenceId": "4bdc0598-a3eb-41ee-8af0-4da9348fbfe2", + }, + ], + "dateAdded": "2022-09-03", + "dateUpdated": "2022-09-10T15:18:19.866085", + "createdAt": "2022-09-03T18:31:17.488118", + "updateAt": "2022-09-10T15:18:19.869630", + "recipeInstructions": [ + { + "id": "60ae53a3-b3ff-40ee-bae3-89fea0b1c637", + "title": "", + "text": "Preheat oven to 410° degrees F. Line 2 baking sheets with parchment paper or silicone liners; set aside.", + "ingredientReferences": [], + }, + { + "id": "4e1c30c2-2e96-4a0a-b750-23c9ea3640f8", + "title": "", + "text": "In the bowl of a stand mixer, cream together the cubed butter, brown sugar and granulated sugar with the paddle attachment for 30 seconds on low speed. Increase speed to medium and beat for another 30 seconds, then increase to medium-high speed and beat for another 30 seconds until mixture is creamy and smooth. Beat in the eggs, one at a time, followed by the vanilla extract and peanut butter, scraping down the sides and bottom of the bowl as needed.", + "ingredientReferences": [], + }, + { + "id": "9fb8e2a2-d410-445c-bafc-c059203e6f4b", + "title": "", + "text": "Add in the cornstarch, baking soda, salt, cake flour, and all-purpose flour and mix on low speed until just combined. Fold in the peanut butter chips and Reese's Pieces candies by hand until fully incorporated. Chill the dough uncovered in the fridge for 15 minutes.", + "ingredientReferences": [], + }, + { + "id": "1ceb9aa4-49f7-4d4a-996f-3c715eb74642", + "title": "", + "text": 'Using a digital kitchen scale for accuracy, weigh out 6 ounces of cookie dough in a loose, rough textured ball. I like to make my cookie dough balls kind of tall as well. You do not want the dough balls to be smooth and compacted. Place on the baking sheet. Repeat with remaining dough balls, staggering on the baking sheet at least 3" apart from one another, and only placing 4 dough balls per baking sheet.', + "ingredientReferences": [], + }, + { + "id": "591993fc-72bb-4091-8a12-84640c523fc1", + "title": "", + "text": "Bake one baking sheet at a time in the center rack of the oven for 10-13 minutes or until the tops are light golden brown and the exterior is dry and dull looking. Centers will be slightly underdone and gooey; this is okay and the cookies will finish cooking some once removed from the oven. Let stand on the baking sheets for at least 30 minutes before serving; the cookies are very delicate and fragile once removed from the oven and need time to set before being moved. Keep remaining dough refrigerated while other cookies bake.", + "ingredientReferences": [], + }, + ], + "nutrition": { + "calories": None, + "fatContent": None, + "proteinContent": None, + "carbohydrateContent": None, + "fiberContent": None, + "sodiumContent": None, + "sugarContent": None, + }, + "settings": { + "public": True, + "showNutrition": False, + "showAssets": False, + "landscapeView": False, + "disableComments": False, + "disableAmount": True, + "locked": False, + }, + "assets": [], + "notes": [], + "extras": {}, + "comments": [], + } + + def login(username="changeme@email.com", password="MyPassword"): payload = {"username": username, "password": password} @@ -30,32 +247,90 @@ def populate_data(token): r = requests.post("http://localhost:9000/api/recipes", json=payload, headers=token) if r.status_code != 201: - print(f"Error: {r.status_code}") - print(r.text) + console.print(f"Error: {r.status_code}") + console.print(r.text) exit() - else: - print(f"Created recipe: {payload}") + recipe_json = requests.get(f"http://localhost:9000/api/recipes/{payload['name']}", headers=token) + + if recipe_json.status_code != 200: + console.print(f"Error: {recipe_json.status_code}") + console.print(recipe_json.text) + exit() + + recipe = json.loads(recipe_json.text) + update_data = recipe_data(recipe["name"], recipe["slug"], recipe["id"], recipe["userId"], recipe["groupId"]) + + r = requests.put(f"http://localhost:9000/api/recipes/{update_data['slug']}", json=update_data, headers=token) + if r.status_code != 200: + console.print(f"Error: {r.status_code}") + console.print(r.text) + exit() -def time_request(url, headers): +@dataclass(slots=True) +class Result: + recipes: int + time: float + + +def time_request(url, headers) -> Result: start = time.time() r = requests.get(url, headers=headers) - - print(f"Total Recipes {len(r.json())}") end = time.time() - print(end - start) + + return Result(len(r.json()["items"]), end - start) def main(): - print("Starting...") token = login() # populate_data(token) - for _ in range(10): - time_request("http://localhost:9000/api/recipes", token) + results: list[Result] = [] - print("Finished...") + for _ in range(10): + result = time_request("http://localhost:9000/api/recipes?perPage=-1&page=1&loadFood=true", token) + results.append(result) + + min, max, average = 99, 0, 0 + + for result in results: + if result.time < min: + min = result.time + + if result.time > max: + max = result.time + + average += result.time + + tbl1 = Table(title="Requests") + + tbl1.add_column("Recipes", justify="right", style="cyan", no_wrap=True) + tbl1.add_column("Time", justify="right", style="magenta") + + for result in results: + tbl1.add_row( + str(result.recipes), + str(result.time), + ) + + tbl2 = Table(title="Summary") + + tbl2.add_column("Min", justify="right", style="green") + tbl2.add_column("Max", justify="right", style="green") + tbl2.add_column("Average", justify="right", style="green") + + tbl2.add_row( + str(round(min * 1000)) + "ms", + str(round(max * 1000)) + "ms", + str(round((average / len(results)) * 1000)) + "ms", + ) + + console = Console() + console.print(tbl1) + console.print(tbl2) + + # Best Time 289 / 405/ 247 if __name__ == "__main__": diff --git a/dev/scripts/app_routes_gen.py b/dev/scripts/app_routes_gen.py deleted file mode 100644 index 03e62ea6..00000000 --- a/dev/scripts/app_routes_gen.py +++ /dev/null @@ -1,214 +0,0 @@ -import json -import re -from enum import Enum -from itertools import groupby -from pathlib import Path -from typing import Optional - -from fastapi import FastAPI -from humps import camelize -from jinja2 import Template -from pydantic import BaseModel, Field -from slugify import slugify - -from mealie.app import app - -CWD = Path(__file__).parent -OUT_DIR = CWD / "output" -TEMPLATES_DIR = CWD / "templates" - -JS_DIR = OUT_DIR / "javascriptAPI" -JS_DIR.mkdir(exist_ok=True, parents=True) - - -class RouteObject: - def __init__(self, route_string) -> None: - self.prefix = "/" + route_string.split("/")[1] - self.route = "/" + route_string.split("/", 2)[2] - self.js_route = self.route.replace("{", "${") - self.parts = route_string.split("/")[1:] - self.var = re.findall(r"\{(.*?)\}", route_string) - self.is_function = "{" in self.route - self.router_slug = slugify("_".join(self.parts[1:]), separator="_") - self.router_camel = camelize(self.router_slug) - - -class RequestType(str, Enum): - get = "get" - put = "put" - post = "post" - patch = "patch" - delete = "delete" - - -class ParameterIn(str, Enum): - query = "query" - path = "path" - - -class RouterParameter(BaseModel): - required: bool = False - name: str - location: ParameterIn = Field(..., alias="in") - - -class RequestBody(BaseModel): - required: bool = False - - -class HTTPRequest(BaseModel): - request_type: RequestType - description: str = "" - summary: str - requestBody: Optional[RequestBody] - - parameters: list[RouterParameter] = [] - tags: list[str] - - def list_as_js_object_string(self, parameters, braces=True): - if len(parameters) == 0: - return "" - - if braces: - return "{" + ", ".join(parameters) + "}" - else: - return ", ".join(parameters) - - def payload(self): - return "payload" if self.requestBody else "" - - def function_args(self): - all_params = [p.name for p in self.parameters] - if self.requestBody: - all_params.append("payload") - return self.list_as_js_object_string(all_params) - - def query_params(self): - params = [param.name for param in self.parameters if param.location == ParameterIn.query] - return self.list_as_js_object_string(params) - - def path_params(self): - params = [param.name for param in self.parameters if param.location == ParameterIn.path] - return self.list_as_js_object_string(parameters=params, braces=False) - - @property - def summary_camel(self): - return camelize(slugify(self.summary)) - - @property - def js_docs(self): - return self.description.replace("\n", " \n * ") - - -class PathObject(BaseModel): - route_object: RouteObject - http_verbs: list[HTTPRequest] - - class Config: - arbitrary_types_allowed = True - - -def get_path_objects(app: FastAPI): - paths = [] - - for key, value in app.openapi().items(): - if key == "paths": - for key, value in value.items(): - - paths.append( - PathObject( - route_object=RouteObject(key), - http_verbs=[HTTPRequest(request_type=k, **v) for k, v in value.items()], - ) - ) - - return paths - - -def dump_open_api(app: FastAPI): - """Writes the Open API as JSON to a json file""" - OPEN_API_FILE = CWD / "openapi.json" - - with open(OPEN_API_FILE, "w") as f: - f.write(json.dumps(app.openapi())) - - -def read_template(file: Path): - with open(file, "r") as f: - return f.read() - - -def generate_python_templates(static_paths: list[PathObject], function_paths: list[PathObject]): - PYTEST_TEMPLATE = TEMPLATES_DIR / "pytest_routes.j2" - PYTHON_OUT_FILE = OUT_DIR / "app_routes.py" - - template = Template(read_template(PYTEST_TEMPLATE)) - content = template.render( - paths={ - "prefix": "/api", - "static_paths": static_paths, - "function_paths": function_paths, - } - ) - with open(PYTHON_OUT_FILE, "w") as f: - f.write(content) - - return - - -def generate_js_templates(paths: list[PathObject]): - # Template Path - JS_API_INTERFACE = TEMPLATES_DIR / "js_api_interface.j2" - JS_INDEX = TEMPLATES_DIR / "js_index.j2" - - INTERFACES_DIR = JS_DIR / "interfaces" - INTERFACES_DIR.mkdir(exist_ok=True, parents=True) - - all_tags = [] - for tag, tag_paths in groupby(paths, lambda x: x.http_verbs[0].tags[0]): - file_name = slugify(tag, separator="-") - - tag = camelize(tag) - - tag_paths: list[PathObject] = list(tag_paths) - - template = Template(read_template(JS_API_INTERFACE)) - content = template.render( - paths={ - "prefix": "/api", - "static_paths": [x.route_object for x in tag_paths if not x.route_object.is_function], - "function_paths": [x.route_object for x in tag_paths if x.route_object.is_function], - "all_paths": tag_paths, - "export_name": tag, - } - ) - - tag: dict = {"camel": camelize(tag), "slug": file_name} - all_tags.append(tag) - - with open(INTERFACES_DIR.joinpath(file_name + ".ts"), "w") as f: - f.write(content) - - template = Template(read_template(JS_INDEX)) - content = template.render(files={"files": all_tags}) - - with open(JS_DIR.joinpath("index.js"), "w") as f: - f.write(content) - - -def generate_template(app): - dump_open_api(app) - paths = get_path_objects(app) - - static_paths = [x.route_object for x in paths if not x.route_object.is_function] - function_paths = [x.route_object for x in paths if x.route_object.is_function] - - static_paths.sort(key=lambda x: x.router_slug) - function_paths.sort(key=lambda x: x.router_slug) - - generate_python_templates(static_paths, function_paths) - generate_js_templates(paths) - - -if __name__ == "__main__": - generate_template(app) diff --git a/dev/scripts/dummy_users.py b/dev/scripts/dummy_users.py deleted file mode 100644 index 2fe17e3f..00000000 --- a/dev/scripts/dummy_users.py +++ /dev/null @@ -1,26 +0,0 @@ -import json -from pathlib import Path - -import requests - -CWD = Path(__file__).parent - - -def login(username="changeme@email.com", password="MyPassword"): - - payload = {"username": username, "password": password} - r = requests.post("http://localhost:9000/api/auth/token", payload) - - # Bearer - token = json.loads(r.text).get("access_token") - return {"Authorization": f"Bearer {token}"} - - -def main(): - print("Starting...") - - print("Finished...") - - -if __name__ == "__main__": - main() diff --git a/dev/scripts/gen_error_messages.py b/dev/scripts/gen_error_messages.py deleted file mode 100644 index 58c939e9..00000000 --- a/dev/scripts/gen_error_messages.py +++ /dev/null @@ -1,159 +0,0 @@ -import json -import re -from dataclasses import dataclass -from pathlib import Path - -from slugify import slugify - -CWD = Path(__file__).parent - -PROJECT_BASE = CWD.parent.parent - -server_side_msgs = PROJECT_BASE / "mealie" / "utils" / "error_messages.py" -en_us_msgs = PROJECT_BASE / "frontend" / "lang" / "errors" / "en-US.json" -client_side_msgs = PROJECT_BASE / "frontend" / "utils" / "error-messages.ts" - -GENERATE_MESSAGES = [ - # User Related - "user", - "webhook", - "token", - # Group Related - "group", - "cookbook", - "mealplan", - # Recipe Related - "scraper", - "recipe", - "ingredient", - "food", - "unit", - # Admin Related - "backup", - "migration", - "event", -] - - -class ErrorMessage: - def __init__(self, prefix, verb) -> None: - self.message = f"{prefix.title()} {verb.title()} Failed" - self.snake = slugify(self.message, separator="_") - self.kabab = slugify(self.message, separator="-") - - def factory(prefix) -> list["ErrorMessage"]: - verbs = ["Create", "Update", "Delete"] - return [ErrorMessage(prefix, verb) for verb in verbs] - - -@dataclass -class CodeGenLines: - start: int - end: int - - indentation: str - text: list[str] - - _next_line = None - - def purge_lines(self) -> None: - start = self.start + 1 - end = self.end - del self.text[start:end] - - def push_line(self, string: str) -> None: - self._next_line = self._next_line or self.start + 1 - self.text.insert(self._next_line, self.indentation + string) - self._next_line += 1 - - -def find_start(file_text: list[str], gen_id: str): - for x, line in enumerate(file_text): - if "CODE_GEN_ID:" in line and gen_id in line: - return x, line - - return None - - -def find_end(file_text: list[str], gen_id: str): - for x, line in enumerate(file_text): - if f"END {gen_id}" in line: - return x, line - return None - - -def get_indentation_of_string(line: str): - return re.sub(r"#.*", "", line).removesuffix("\n") - - -def get_messages(message_prefix: str) -> str: - prefix = message_prefix.lower() - - return [ - f'{prefix}_create_failure = "{prefix}-create-failure"\n', - f'{prefix}_update_failure = "{prefix}-update-failure"\n', - f'{prefix}_delete_failure = "{prefix}-delete-failure"\n', - ] - - -def code_gen_factory(file_path: Path) -> CodeGenLines: - with open(file_path, "r") as file: - text = file.readlines() - start_num, line = find_start(text, "ERROR_MESSAGE_ENUMS") - indentation = get_indentation_of_string(line) - end_num, line = find_end(text, "ERROR_MESSAGE_ENUMS") - - return CodeGenLines( - start=start_num, - end=end_num, - indentation=indentation, - text=text, - ) - - -def write_to_locals(messages: list[ErrorMessage]) -> None: - with open(en_us_msgs, "r") as f: - existing_msg = json.loads(f.read()) - - for msg in messages: - if msg.kabab in existing_msg: - continue - - existing_msg[msg.kabab] = msg.message - print(f"Added Key {msg.kabab} to 'en-US.json'") - - with open(en_us_msgs, "w") as f: - f.write(json.dumps(existing_msg, indent=4)) - - -def main(): - print("Starting...") - GENERATE_MESSAGES.sort() - - code_gen = code_gen_factory(server_side_msgs) - code_gen.purge_lines() - - messages = [] - for msg_type in GENERATE_MESSAGES: - messages += get_messages(msg_type) - messages.append("\n") - - for msg in messages: - code_gen.push_line(msg) - - with open(server_side_msgs, "w") as file: - file.writelines(code_gen.text) - - # Locals - - local_msgs = [] - for msg_type in GENERATE_MESSAGES: - local_msgs += ErrorMessage.factory(msg_type) - - write_to_locals(local_msgs) - - print("Done!") - - -if __name__ == "__main__": - main() diff --git a/dev/scripts/github_get_release_issues.py b/dev/scripts/github_get_release_issues.py deleted file mode 100644 index 5cc5fca7..00000000 --- a/dev/scripts/github_get_release_issues.py +++ /dev/null @@ -1,32 +0,0 @@ -import json - -import requests -from pydantic import BaseModel - - -class GithubIssue(BaseModel): - url: str - number: int - title: str - - -def get_issues_by_label(label="fixed-pending-release") -> list[GithubIssue]: - - issues_url = f"https://api.github.com/repos/hay-kot/mealie/issues?labels={label}" - - response = requests.get(issues_url) - issues = json.loads(response.text) - return [GithubIssue(**issue) for issue in issues] - - -def format_markdown_list(issues: list[GithubIssue]) -> str: - return "\n".join(f"- [{issue.number}]({issue.url}) - {issue.title}" for issue in issues) - - -def main() -> None: - issues = get_issues_by_label() - print(format_markdown_list(issues)) - - -if __name__ == "__main__": - main() diff --git a/dev/scripts/output/app_routes.py b/dev/scripts/output/app_routes.py deleted file mode 100644 index 91968dcc..00000000 --- a/dev/scripts/output/app_routes.py +++ /dev/null @@ -1,156 +0,0 @@ -# This Content is Auto Generated for Pytest - - -class AppRoutes: - def __init__(self) -> None: - self.prefix = '/api' - - self.about_events = "/api/about/events" - self.about_events_notifications = "/api/about/events/notifications" - self.about_events_notifications_test = "/api/about/events/notifications/test" - self.about_recipes_defaults = "/api/about/recipes/defaults" - self.auth_refresh = "/api/auth/refresh" - self.auth_token = "/api/auth/token" - self.auth_token_long = "/api/auth/token/long" - self.backups_available = "/api/backups/available" - self.backups_export_database = "/api/backups/export/database" - self.backups_upload = "/api/backups/upload" - self.categories = "/api/categories" - self.categories_empty = "/api/categories/empty" - self.debug = "/api/debug" - self.debug_last_recipe_json = "/api/debug/last-recipe-json" - self.debug_log = "/api/debug/log" - self.debug_statistics = "/api/debug/statistics" - self.debug_version = "/api/debug/version" - self.groups = "/api/groups" - self.groups_self = "/api/groups/self" - self.meal_plans_all = "/api/meal-plans/all" - self.meal_plans_create = "/api/meal-plans/create" - self.meal_plans_this_week = "/api/meal-plans/this-week" - self.meal_plans_today = "/api/meal-plans/today" - self.meal_plans_today_image = "/api/meal-plans/today/image" - self.migrations = "/api/migrations" - self.recipes = "/api/recipes" - self.recipes_category = "/api/recipes/category" - self.recipes_create = "/api/recipes/create" - self.recipes_create_from_zip = "/api/recipes/create-from-zip" - self.recipes_create_url = "/api/recipes/create-url" - self.recipes_summary_uncategorized = "/api/recipes/summary/uncategorized" - self.recipes_summary_untagged = "/api/recipes/summary/untagged" - self.recipes_tag = "/api/recipes/tag" - self.recipes_test_scrape_url = "/api/recipes/test-scrape-url" - self.shopping_lists = "/api/shopping-lists" - self.site_settings = "/api/site-settings" - self.site_settings_custom_pages = "/api/site-settings/custom-pages" - self.site_settings_webhooks_test = "/api/site-settings/webhooks/test" - self.tags = "/api/tags" - self.tags_empty = "/api/tags/empty" - self.themes = "/api/themes" - self.themes_create = "/api/themes/create" - self.users = "/api/users" - self.users_api_tokens = "/api/users/api-tokens" - self.users_self = "/api/users/self" - self.users_sign_ups = "/api/users/sign-ups" - self.utils_download = "/api/utils/download" - - def about_events_id(self, id): - return f"{self.prefix}/about/events/{id}" - - def about_events_notifications_id(self, id): - return f"{self.prefix}/about/events/notifications/{id}" - - def backups_file_name_delete(self, file_name): - return f"{self.prefix}/backups/{file_name}/delete" - - def backups_file_name_download(self, file_name): - return f"{self.prefix}/backups/{file_name}/download" - - def backups_file_name_import(self, file_name): - return f"{self.prefix}/backups/{file_name}/import" - - def categories_category(self, category): - return f"{self.prefix}/categories/{category}" - - def debug_log_num(self, num): - return f"{self.prefix}/debug/log/{num}" - - def groups_id(self, id): - return f"{self.prefix}/groups/{id}" - - def meal_plans_id(self, id): - return f"{self.prefix}/meal-plans/{id}" - - def meal_plans_id_shopping_list(self, id): - return f"{self.prefix}/meal-plans/{id}/shopping-list" - - def meal_plans_plan_id(self, plan_id): - return f"{self.prefix}/meal-plans/{plan_id}" - - def media_recipes_recipe_slug_assets_file_name(self, recipe_slug, file_name): - return f"{self.prefix}/media/recipes/{recipe_slug}/assets/{file_name}" - - def media_recipes_recipe_slug_images_file_name(self, recipe_slug, file_name): - return f"{self.prefix}/media/recipes/{recipe_slug}/images/{file_name}" - - def migrations_import_type_file_name_delete(self, import_type, file_name): - return f"{self.prefix}/migrations/{import_type}/{file_name}/delete" - - def migrations_import_type_file_name_import(self, import_type, file_name): - return f"{self.prefix}/migrations/{import_type}/{file_name}/import" - - def migrations_import_type_upload(self, import_type): - return f"{self.prefix}/migrations/{import_type}/upload" - - def recipes_recipe_slug(self, recipe_slug): - return f"{self.prefix}/recipes/{recipe_slug}" - - def recipes_recipe_slug_assets(self, recipe_slug): - return f"{self.prefix}/recipes/{recipe_slug}/assets" - - def recipes_recipe_slug_image(self, recipe_slug): - return f"{self.prefix}/recipes/{recipe_slug}/image" - - def recipes_recipe_slug_zip(self, recipe_slug): - return f"{self.prefix}/recipes/{recipe_slug}/zip" - - def recipes_slug_comments(self, slug): - return f"{self.prefix}/recipes/{slug}/comments" - - def recipes_slug_comments_id(self, slug, id): - return f"{self.prefix}/recipes/{slug}/comments/{id}" - - def shopping_lists_id(self, id): - return f"{self.prefix}/shopping-lists/{id}" - - def site_settings_custom_pages_id(self, id): - return f"{self.prefix}/site-settings/custom-pages/{id}" - - def tags_tag(self, tag): - return f"{self.prefix}/tags/{tag}" - - def themes_id(self, id): - return f"{self.prefix}/themes/{id}" - - def users_api_tokens_token_id(self, token_id): - return f"{self.prefix}/users/api-tokens/{token_id}" - - def users_id(self, id): - return f"{self.prefix}/users/{id}" - - def users_id_favorites(self, id): - return f"{self.prefix}/users/{id}/favorites" - - def users_id_favorites_slug(self, id, slug): - return f"{self.prefix}/users/{id}/favorites/{slug}" - - def users_id_image(self, id): - return f"{self.prefix}/users/{id}/image" - - def users_id_password(self, id): - return f"{self.prefix}/users/{id}/password" - - def users_id_reset_password(self, id): - return f"{self.prefix}/users/{id}/reset-password" - - def users_sign_ups_token(self, token): - return f"{self.prefix}/users/sign-ups/{token}" diff --git a/dev/scripts/publish-release-branch.sh b/dev/scripts/publish-release-branch.sh deleted file mode 100644 index 22bd2aa8..00000000 --- a/dev/scripts/publish-release-branch.sh +++ /dev/null @@ -1,11 +0,0 @@ -git checkout dev -git merge --strategy=ours master # keep the content of this branch, but record a merge -git checkout master -git merge dev # fast-forward master up to the merge - - -## TODOs - -# Create New Branch v0.x.x -# Push Branch Version to Github -# Create Pull Request \ No newline at end of file diff --git a/dev/scripts/templates/js_api_interface.j2 b/dev/scripts/templates/js_api_interface.j2 deleted file mode 100644 index 9fbce2db..00000000 --- a/dev/scripts/templates/js_api_interface.j2 +++ /dev/null @@ -1,17 +0,0 @@ -import { requests } from "../requests"; - -const prefix = '{{paths.prefix}}' - -const routes = { {% for path in paths.static_paths %} - {{ path.router_camel }}: `${prefix}{{ path.route }}`,{% endfor %} -{% for path in paths.function_paths %} - {{path.router_camel}}: ({{path.var|join(", ")}}) => `${prefix}{{ path.js_route }}`,{% endfor %} -} - -export const {{paths.export_name}}API = { {% for path in paths.all_paths %} {% for verb in path.http_verbs %} - {% if verb.js_docs %}/** {{ verb.js_docs }} - */ {% endif %} - async {{ verb.summary_camel }}({{ verb.function_args() }}) { - return await requests.{{ verb.request_type.value }}(routes.{{ path.route_object.router_camel }}{% if path.route_object.is_function %}({{verb.path_params()}}){% endif %}, {{ verb.query_params() }} {{ verb.payload() }}) - }, {% endfor %} {% endfor %} -} diff --git a/dev/scripts/templates/js_index.j2 b/dev/scripts/templates/js_index.j2 deleted file mode 100644 index 277511c1..00000000 --- a/dev/scripts/templates/js_index.j2 +++ /dev/null @@ -1,7 +0,0 @@ -{% for api in files.files %} -import { {{ api.camel }}API } from "./interfaces/{{ api.slug }}" {% endfor %} - -export const api = { -{% for api in files.files %} - {{api.camel}}: {{api.camel}}API, {% endfor %} -} \ No newline at end of file diff --git a/dev/scripts/templates/pytest_routes.j2 b/dev/scripts/templates/pytest_routes.j2 deleted file mode 100644 index ecd9e09c..00000000 --- a/dev/scripts/templates/pytest_routes.j2 +++ /dev/null @@ -1,12 +0,0 @@ -# This Content is Auto Generated for Pytest - - -class AppRoutes: - def __init__(self) -> None: - self.prefix = '{{paths.prefix}}' -{% for path in paths.static_paths %} - self.{{ path.router_slug }} = "{{path.prefix}}{{ path.route }}"{% endfor %} -{% for path in paths.function_paths %} - def {{path.router_slug}}(self, {{path.var|join(", ")}}): - return f"{self.prefix}{{ path.route }}" -{% endfor %} \ No newline at end of file diff --git a/frontend/components/Domain/Recipe/RecipeOcrEditorPage/RecipeOcrEditorPage.vue b/frontend/components/Domain/Recipe/RecipeOcrEditorPage/RecipeOcrEditorPage.vue index 58355ad6..96aaa964 100644 --- a/frontend/components/Domain/Recipe/RecipeOcrEditorPage/RecipeOcrEditorPage.vue +++ b/frontend/components/Domain/Recipe/RecipeOcrEditorPage/RecipeOcrEditorPage.vue @@ -146,7 +146,7 @@ import { until } from "@vueuse/core"; import { invoke } from "@vueuse/shared"; import draggable from "vuedraggable"; import { useUserApi, useStaticRoutes } from "~/composables/api"; -import { OcrTsvResponse } from "~/types/api-types/ocr"; +import { OcrTsvResponse as NullableOcrTsvResponse } from "~/types/api-types/ocr"; import { validators } from "~/composables/use-validators"; import { Recipe, RecipeIngredient, RecipeStep } from "~/types/api-types/recipe"; import { Paths, Leaves, SelectedRecipeLeaves } from "~/types/ocr-types"; @@ -159,6 +159,10 @@ import RecipeOcrEditorPageHelp from "~/components/Domain/Recipe/RecipeOcrEditorP import { uuid4 } from "~/composables/use-utils"; import { NoUndefinedField } from "~/types/api"; +// Temporary Shim until we have a better solution +// https://github.com/phillipdupuis/pydantic-to-typescript/issues/28 +type OcrTsvResponse = NoUndefinedField; + export default defineComponent({ components: { RecipeIngredientEditor, diff --git a/frontend/components/Domain/Recipe/RecipeOcrEditorPage/RecipeOcrEditorPageParts/RecipeOcrEditorPageCanvas.vue b/frontend/components/Domain/Recipe/RecipeOcrEditorPage/RecipeOcrEditorPageParts/RecipeOcrEditorPageCanvas.vue index 7cb9cb98..4b80c5aa 100644 --- a/frontend/components/Domain/Recipe/RecipeOcrEditorPage/RecipeOcrEditorPageParts/RecipeOcrEditorPageCanvas.vue +++ b/frontend/components/Domain/Recipe/RecipeOcrEditorPage/RecipeOcrEditorPageParts/RecipeOcrEditorPageCanvas.vue @@ -41,9 +41,14 @@