mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-12-28 05:05:12 -05:00
improve developer tooling (backend) (#1051)
* add basic pre-commit file * add flake8 * add isort * add pep585-upgrade (typing upgrades) * use namespace for import * add mypy * update ci for backend * flake8 scope * fix version format * update makefile * disable strict option (temporary) * fix mypy issues * upgrade type hints (pre-commit) * add vscode typing check * add types to dev deps * remote container draft * update setup script * update compose version * run setup on create * dev containers update * remove unused pages * update setup tips * expose ports * Update pre-commit to include flask8-print (#1053) * Add in flake8-print to pre-commit * pin version of flake8-print * formatting * update getting strated docs * add mypy to pre-commit * purge .mypy_cache on clean * drop mypy Co-authored-by: zackbcom <zackbcom@users.noreply.github.com>
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
from abc import ABC
|
||||
from functools import cached_property
|
||||
from typing import Type
|
||||
|
||||
from fastapi import Depends
|
||||
|
||||
@@ -29,7 +28,7 @@ class BaseUserController(ABC):
|
||||
|
||||
deps: SharedDependencies = Depends(SharedDependencies.user)
|
||||
|
||||
def registered_exceptions(self, ex: Type[Exception]) -> str:
|
||||
def registered_exceptions(self, ex: type[Exception]) -> str:
|
||||
registered = {
|
||||
**mealie_registered_exceptions(self.deps.t),
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ This file contains code taken from fastapi-utils project. The code is licensed u
|
||||
See their repository for details -> https://github.com/dmontagu/fastapi-utils
|
||||
"""
|
||||
import inspect
|
||||
from typing import Any, Callable, List, Tuple, Type, TypeVar, Union, cast, get_type_hints
|
||||
from collections.abc import Callable
|
||||
from typing import Any, TypeVar, Union, cast, get_type_hints
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi.routing import APIRoute
|
||||
@@ -18,7 +19,7 @@ INCLUDE_INIT_PARAMS_KEY = "__include_init_params__"
|
||||
RETURN_TYPES_FUNC_KEY = "__return_types_func__"
|
||||
|
||||
|
||||
def controller(router: APIRouter, *urls: str) -> Callable[[Type[T]], Type[T]]:
|
||||
def controller(router: APIRouter, *urls: str) -> Callable[[type[T]], type[T]]:
|
||||
"""
|
||||
This function returns a decorator that converts the decorated into a class-based view for the provided router.
|
||||
Any methods of the decorated class that are decorated as endpoints using the router provided to this function
|
||||
@@ -28,14 +29,14 @@ def controller(router: APIRouter, *urls: str) -> Callable[[Type[T]], Type[T]]:
|
||||
https://fastapi-utils.davidmontague.xyz/user-guide/class-based-views/#the-cbv-decorator
|
||||
"""
|
||||
|
||||
def decorator(cls: Type[T]) -> Type[T]:
|
||||
def decorator(cls: type[T]) -> type[T]:
|
||||
# Define cls as cbv class exclusively when using the decorator
|
||||
return _cbv(router, cls, *urls)
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def _cbv(router: APIRouter, cls: Type[T], *urls: str, instance: Any = None) -> Type[T]:
|
||||
def _cbv(router: APIRouter, cls: type[T], *urls: str, instance: Any = None) -> type[T]:
|
||||
"""
|
||||
Replaces any methods of the provided class `cls` that are endpoints of routes in `router` with updated
|
||||
function calls that will properly inject an instance of `cls`.
|
||||
@@ -45,7 +46,7 @@ def _cbv(router: APIRouter, cls: Type[T], *urls: str, instance: Any = None) -> T
|
||||
return cls
|
||||
|
||||
|
||||
def _init_cbv(cls: Type[Any], instance: Any = None) -> None:
|
||||
def _init_cbv(cls: type[Any], instance: Any = None) -> None:
|
||||
"""
|
||||
Idempotently modifies the provided `cls`, performing the following modifications:
|
||||
* The `__init__` function is updated to set any class-annotated dependencies as instance attributes
|
||||
@@ -60,7 +61,7 @@ def _init_cbv(cls: Type[Any], instance: Any = None) -> None:
|
||||
x for x in old_parameters if x.kind not in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD)
|
||||
]
|
||||
|
||||
dependency_names: List[str] = []
|
||||
dependency_names: list[str] = []
|
||||
for name, hint in get_type_hints(cls).items():
|
||||
if is_classvar(hint):
|
||||
continue
|
||||
@@ -88,7 +89,7 @@ def _init_cbv(cls: Type[Any], instance: Any = None) -> None:
|
||||
setattr(cls, CBV_CLASS_KEY, True)
|
||||
|
||||
|
||||
def _register_endpoints(router: APIRouter, cls: Type[Any], *urls: str) -> None:
|
||||
def _register_endpoints(router: APIRouter, cls: type[Any], *urls: str) -> None:
|
||||
cbv_router = APIRouter()
|
||||
function_members = inspect.getmembers(cls, inspect.isfunction)
|
||||
for url in urls:
|
||||
@@ -97,7 +98,7 @@ def _register_endpoints(router: APIRouter, cls: Type[Any], *urls: str) -> None:
|
||||
for route in router.routes:
|
||||
assert isinstance(route, APIRoute)
|
||||
route_methods: Any = route.methods
|
||||
cast(Tuple[Any], route_methods)
|
||||
cast(tuple[Any], route_methods)
|
||||
router_roles.append((route.path, tuple(route_methods)))
|
||||
|
||||
if len(set(router_roles)) != len(router_roles):
|
||||
@@ -110,7 +111,7 @@ def _register_endpoints(router: APIRouter, cls: Type[Any], *urls: str) -> None:
|
||||
}
|
||||
|
||||
prefix_length = len(router.prefix)
|
||||
routes_to_append: List[Tuple[int, Union[Route, WebSocketRoute]]] = []
|
||||
routes_to_append: list[tuple[int, Union[Route, WebSocketRoute]]] = []
|
||||
for _, func in function_members:
|
||||
index_route = numbered_routes_by_endpoint.get(func)
|
||||
|
||||
@@ -138,9 +139,9 @@ def _register_endpoints(router: APIRouter, cls: Type[Any], *urls: str) -> None:
|
||||
router.include_router(cbv_router, prefix=cbv_prefix)
|
||||
|
||||
|
||||
def _allocate_routes_by_method_name(router: APIRouter, url: str, function_members: List[Tuple[str, Any]]) -> None:
|
||||
def _allocate_routes_by_method_name(router: APIRouter, url: str, function_members: list[tuple[str, Any]]) -> None:
|
||||
# sourcery skip: merge-nested-ifs
|
||||
existing_routes_endpoints: List[Tuple[Any, str]] = [
|
||||
existing_routes_endpoints: list[tuple[Any, str]] = [
|
||||
(route.endpoint, route.path) for route in router.routes if isinstance(route, APIRoute)
|
||||
]
|
||||
for name, func in function_members:
|
||||
@@ -165,13 +166,13 @@ def _allocate_routes_by_method_name(router: APIRouter, url: str, function_member
|
||||
api_resource(func)
|
||||
|
||||
|
||||
def _update_cbv_route_endpoint_signature(cls: Type[Any], route: Union[Route, WebSocketRoute]) -> None:
|
||||
def _update_cbv_route_endpoint_signature(cls: type[Any], route: Union[Route, WebSocketRoute]) -> None:
|
||||
"""
|
||||
Fixes the endpoint signature for a cbv route to ensure FastAPI performs dependency injection properly.
|
||||
"""
|
||||
old_endpoint = route.endpoint
|
||||
old_signature = inspect.signature(old_endpoint)
|
||||
old_parameters: List[inspect.Parameter] = list(old_signature.parameters.values())
|
||||
old_parameters: list[inspect.Parameter] = list(old_signature.parameters.values())
|
||||
old_first_parameter = old_parameters[0]
|
||||
new_first_parameter = old_first_parameter.replace(default=Depends(cls))
|
||||
new_parameters = [new_first_parameter] + [
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from collections.abc import Callable
|
||||
from logging import Logger
|
||||
from typing import Callable, Generic, Type, TypeVar
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
from pydantic import UUID4, BaseModel
|
||||
@@ -26,14 +27,14 @@ class CrudMixins(Generic[C, R, U]):
|
||||
"""
|
||||
|
||||
repo: RepositoryGeneric
|
||||
exception_msgs: Callable[[Type[Exception]], str] | None
|
||||
exception_msgs: Callable[[type[Exception]], str] | None
|
||||
default_message: str = "An unexpected error occurred."
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
repo: RepositoryGeneric,
|
||||
logger: Logger,
|
||||
exception_msgs: Callable[[Type[Exception]], str] = None,
|
||||
exception_msgs: Callable[[type[Exception]], str] = None,
|
||||
default_message: str = None,
|
||||
) -> None:
|
||||
|
||||
@@ -83,7 +84,7 @@ class CrudMixins(Generic[C, R, U]):
|
||||
return item
|
||||
|
||||
def update_one(self, data: U, item_id: int | str | UUID4) -> R:
|
||||
item: R = self.repo.get_one(item_id)
|
||||
item = self.repo.get_one(item_id)
|
||||
|
||||
if not item:
|
||||
raise HTTPException(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import List, Optional
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
@@ -10,7 +10,7 @@ class AdminAPIRouter(APIRouter):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tags: Optional[List[str]] = None,
|
||||
tags: Optional[list[str]] = None,
|
||||
prefix: str = "",
|
||||
):
|
||||
super().__init__(tags=tags, prefix=prefix, dependencies=[Depends(get_admin_user)])
|
||||
@@ -21,7 +21,7 @@ class UserAPIRouter(APIRouter):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tags: Optional[List[str]] = None,
|
||||
tags: Optional[list[str]] = None,
|
||||
prefix: str = "",
|
||||
):
|
||||
super().__init__(tags=tags, prefix=prefix, dependencies=[Depends(get_current_user)])
|
||||
|
||||
@@ -52,16 +52,15 @@ def get_token(data: CustomOAuth2Form = Depends(), session: Session = Depends(gen
|
||||
email = data.username
|
||||
password = data.password
|
||||
|
||||
user: PrivateUser = authenticate_user(session, email, password)
|
||||
user = authenticate_user(session, email, password) # type: ignore
|
||||
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
duration = timedelta(days=14) if data.remember_me else None
|
||||
access_token = security.create_access_token(dict(sub=str(user.id)), duration)
|
||||
access_token = security.create_access_token(dict(sub=str(user.id)), duration) # type: ignore
|
||||
return MealieAuthToken.respond(access_token)
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from functools import cached_property
|
||||
from typing import Type
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import UUID4
|
||||
@@ -24,7 +23,7 @@ class GroupCookbookController(BaseUserController):
|
||||
def repo(self):
|
||||
return self.deps.repos.cookbooks.by_group(self.group_id)
|
||||
|
||||
def registered_exceptions(self, ex: Type[Exception]) -> str:
|
||||
def registered_exceptions(self, ex: type[Exception]) -> str:
|
||||
registered = {
|
||||
**mealie_registered_exceptions(self.deps.t),
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from functools import cached_property
|
||||
from typing import Type
|
||||
|
||||
from fastapi import APIRouter
|
||||
from pydantic import UUID4
|
||||
@@ -19,7 +18,7 @@ class GroupReportsController(BaseUserController):
|
||||
def repo(self):
|
||||
return self.deps.repos.group_reports.by_group(self.deps.acting_user.group_id)
|
||||
|
||||
def registered_exceptions(self, ex: Type[Exception]) -> str:
|
||||
def registered_exceptions(self, ex: type[Exception]) -> str:
|
||||
return {
|
||||
**mealie_registered_exceptions(self.deps.t),
|
||||
}.get(ex, "An unexpected error occurred.")
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from datetime import date, timedelta
|
||||
from functools import cached_property
|
||||
from typing import Type
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
@@ -24,7 +23,7 @@ class GroupMealplanController(BaseUserController):
|
||||
def repo(self) -> RepositoryMeals:
|
||||
return self.repos.meals.by_group(self.group_id)
|
||||
|
||||
def registered_exceptions(self, ex: Type[Exception]) -> str:
|
||||
def registered_exceptions(self, ex: type[Exception]) -> str:
|
||||
registered = {
|
||||
**mealie_registered_exceptions(self.deps.t),
|
||||
}
|
||||
@@ -58,7 +57,7 @@ class GroupMealplanController(BaseUserController):
|
||||
)
|
||||
|
||||
recipe_repo = self.repos.recipes.by_group(self.group_id)
|
||||
random_recipes: Recipe = []
|
||||
random_recipes: list[Recipe] = []
|
||||
|
||||
if not rules: # If no rules are set, return any random recipe from the group
|
||||
random_recipes = recipe_repo.get_random()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import Depends, File, Form
|
||||
from fastapi.datastructures import UploadFile
|
||||
@@ -8,7 +9,13 @@ from mealie.routes._base import BaseUserController, controller
|
||||
from mealie.routes._base.routers import UserAPIRouter
|
||||
from mealie.schema.group.group_migration import SupportedMigrations
|
||||
from mealie.schema.reports.reports import ReportSummary
|
||||
from mealie.services.migrations import ChowdownMigrator, MealieAlphaMigrator, NextcloudMigrator, PaprikaMigrator
|
||||
from mealie.services.migrations import (
|
||||
BaseMigrator,
|
||||
ChowdownMigrator,
|
||||
MealieAlphaMigrator,
|
||||
NextcloudMigrator,
|
||||
PaprikaMigrator,
|
||||
)
|
||||
|
||||
router = UserAPIRouter(prefix="/groups/migrations", tags=["Group: Migrations"])
|
||||
|
||||
@@ -21,7 +28,7 @@ class GroupMigrationController(BaseUserController):
|
||||
add_migration_tag: bool = Form(False),
|
||||
migration_type: SupportedMigrations = Form(...),
|
||||
archive: UploadFile = File(...),
|
||||
temp_path: str = Depends(temporary_zip_path),
|
||||
temp_path: Path = Depends(temporary_zip_path),
|
||||
):
|
||||
# Save archive to temp_path
|
||||
with temp_path.open("wb") as buffer:
|
||||
@@ -36,6 +43,8 @@ class GroupMigrationController(BaseUserController):
|
||||
"add_migration_tag": add_migration_tag,
|
||||
}
|
||||
|
||||
migrator: BaseMigrator
|
||||
|
||||
match migration_type:
|
||||
case SupportedMigrations.chowdown:
|
||||
migrator = ChowdownMigrator(**args)
|
||||
|
||||
@@ -23,7 +23,6 @@ def register_debug_handler(app: FastAPI):
|
||||
|
||||
@app.exception_handler(RequestValidationError)
|
||||
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
||||
|
||||
exc_str = f"{exc}".replace("\n", " ").replace(" ", " ")
|
||||
log_wrapper(request, exc)
|
||||
content = {"status_code": status.HTTP_422_UNPROCESSABLE_ENTITY, "message": exc_str, "data": None}
|
||||
|
||||
@@ -173,7 +173,7 @@ class RecipeController(BaseRecipeController):
|
||||
task.append_log(f"Error: Failed to create recipe from url: {b.url}")
|
||||
task.append_log(f"Error: {e}")
|
||||
self.deps.logger.error(f"Failed to create recipe from url: {b.url}")
|
||||
self.deps.error(e)
|
||||
self.deps.logger.error(e)
|
||||
database.server_tasks.update(task.id, task)
|
||||
|
||||
task.set_finished()
|
||||
@@ -225,12 +225,13 @@ class RecipeController(BaseRecipeController):
|
||||
return self.mixins.get_one(slug)
|
||||
|
||||
@router.post("", status_code=201, response_model=str)
|
||||
def create_one(self, data: CreateRecipe) -> str:
|
||||
def create_one(self, data: CreateRecipe) -> str | None:
|
||||
"""Takes in a JSON string and loads data into the database as a new entry"""
|
||||
try:
|
||||
return self.service.create_one(data).slug
|
||||
except Exception as e:
|
||||
self.handle_exceptions(e)
|
||||
return None
|
||||
|
||||
@router.put("/{slug}")
|
||||
def update_one(self, slug: str, data: Recipe):
|
||||
@@ -263,7 +264,7 @@ class RecipeController(BaseRecipeController):
|
||||
# Image and Assets
|
||||
|
||||
@router.post("/{slug}/image", tags=["Recipe: Images and Assets"])
|
||||
def scrape_image_url(self, slug: str, url: CreateRecipeByUrl) -> str:
|
||||
def scrape_image_url(self, slug: str, url: CreateRecipeByUrl):
|
||||
recipe = self.mixins.get_one(slug)
|
||||
data_service = RecipeDataService(recipe.id)
|
||||
data_service.scrape_image(url.url)
|
||||
@@ -303,7 +304,7 @@ class RecipeController(BaseRecipeController):
|
||||
if not dest.is_file():
|
||||
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
recipe: Recipe = self.mixins.get_one(slug)
|
||||
recipe = self.mixins.get_one(slug)
|
||||
recipe.assets.append(asset_in)
|
||||
|
||||
self.mixins.update_one(recipe, slug)
|
||||
|
||||
Reference in New Issue
Block a user