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:
Hayden
2022-03-15 15:01:56 -08:00
committed by GitHub
parent e109391e9a
commit 3c2744a3da
105 changed files with 723 additions and 437 deletions

View File

@@ -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),
}

View File

@@ -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] + [

View File

@@ -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(

View File

@@ -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)])

View File

@@ -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)

View File

@@ -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),
}

View File

@@ -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.")

View File

@@ -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()

View File

@@ -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)

View File

@@ -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}

View File

@@ -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)