diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
new file mode 100644
index 000000000000..d9127a9ec677
--- /dev/null
+++ b/.devcontainer/Dockerfile
@@ -0,0 +1,38 @@
+# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.224.2/containers/python-3/.devcontainer/base.Dockerfile
+
+# [Choice] Python version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.10, 3.9, 3.8, 3.7, 3.6, 3-bullseye, 3.10-bullseye, 3.9-bullseye, 3.8-bullseye, 3.7-bullseye, 3.6-bullseye, 3-buster, 3.10-buster, 3.9-buster, 3.8-buster, 3.7-buster, 3.6-buster
+ARG VARIANT="3.10-bullseye"
+FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT}
+
+# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10
+ARG NODE_VERSION="none"
+RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
+
+# install poetry - respects $POETRY_VERSION & $POETRY_HOME
+
+ENV PYTHONUNBUFFERED=1 \
+ PYTHONDONTWRITEBYTECODE=1 \
+ PIP_NO_CACHE_DIR=off \
+ PIP_DISABLE_PIP_VERSION_CHECK=on \
+ PIP_DEFAULT_TIMEOUT=100 \
+ POETRY_HOME="/opt/poetry" \
+ POETRY_VIRTUALENVS_IN_PROJECT=true \
+ POETRY_NO_INTERACTION=1 \
+ PYSETUP_PATH="/opt/pysetup" \
+ VENV_PATH="/opt/pysetup/.venv"
+
+# prepend poetry and venv to path
+ENV PATH="$POETRY_HOME/bin:$VENV_PATH/bin:$PATH"
+
+RUN curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py | python -
+
+RUN apt-get update \
+ && apt-get install --no-install-recommends -y \
+ curl \
+ build-essential \
+ libpq-dev \
+ libwebp-dev \
+ # LDAP Dependencies
+ libsasl2-dev libldap2-dev libssl-dev \
+ gnupg gnupg2 gnupg1 \
+ && pip install -U --no-cache-dir pip
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 000000000000..9f13d65ac5be
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,48 @@
+// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
+// https://github.com/microsoft/vscode-dev-containers/tree/v0.224.2/containers/python-3
+{
+ "name": "Python 3",
+ "build": {
+ "dockerfile": "Dockerfile",
+ "context": "..",
+ "args": {
+ // Update 'VARIANT' to pick a Python version: 3, 3.10, 3.9, 3.8, 3.7, 3.6
+ // Append -bullseye or -buster to pin to an OS version.
+ // Use -bullseye variants on local on arm64/Apple Silicon.
+ "VARIANT": "3.10-bullseye",
+ // Options
+ "NODE_VERSION": "16"
+ }
+ },
+
+ // Set *default* container specific settings.json values on container create.
+ "settings": {
+ "python.defaultInterpreterPath": "/usr/local/bin/python",
+ "python.linting.enabled": true,
+ "python.linting.pylintEnabled": true,
+ "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
+ "python.formatting.blackPath": "/usr/local/py-utils/bin/black",
+ "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
+ "python.linting.banditPath": "/usr/local/py-utils/bin/bandit",
+ "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8",
+ "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy",
+ "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle",
+ "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle",
+ "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint"
+ },
+
+ // Add the IDs of extensions you want installed when the container is created.
+ "extensions": ["ms-python.python", "ms-python.vscode-pylance"],
+
+ // Use 'forwardPorts' to make a list of ports inside the container available locally.
+ "forwardPorts": [3000, 9000],
+
+ // Use 'postCreateCommand' to run commands after the container is created.
+ "postCreateCommand": "make setup",
+
+ // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
+ "remoteUser": "vscode",
+ "features": {
+ "git": "latest"
+ }
+}
diff --git a/.github/workflows/backend-tests.yml b/.github/workflows/backend-tests.yml
index 9a5ecfe261e8..fdbde127087c 100644
--- a/.github/workflows/backend-tests.yml
+++ b/.github/workflows/backend-tests.yml
@@ -69,13 +69,23 @@ jobs:
sudo apt-get install libsasl2-dev libldap2-dev libssl-dev
poetry install
poetry add "psycopg2-binary==2.8.6"
- # if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
+ if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
#----------------------------------------------
# run test suite
#----------------------------------------------
- - name: Run Test Suite
+ - name: Formatting (Black & isort)
+ run: |
+ poetry run black . --check
+ poetry run isort . --check-only
+ - name: Lint (Flake8)
+ run: |
+ make backend-lint
+ - name: Mypy Typecheck
+ run: |
+ make backend-typecheck
+ - name: Pytest
env:
DB_ENGINE: ${{ matrix.Database }}
POSTGRES_SERVER: localhost
run: |
- make test-all
+ make backend-test
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 000000000000..92273046e26c
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,30 @@
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v2.3.0
+ hooks:
+ - id: check-yaml
+ exclude: "mkdocs.yml"
+ - id: check-json
+ exclude: "devcontainer.json"
+ - id: check-toml
+ - id: end-of-file-fixer
+ - id: trailing-whitespace
+ - repo: https://github.com/sondrelg/pep585-upgrade
+ rev: "v1.0.1" # Use the sha / tag you want to point at
+ hooks:
+ - id: upgrade-type-hints
+ - repo: https://github.com/pycqa/isort
+ rev: 5.10.1
+ hooks:
+ - id: isort
+ name: isort (python)
+ - repo: https://github.com/psf/black
+ rev: 21.12b0
+ hooks:
+ - id: black
+ - repo: https://github.com/pycqa/flake8
+ rev: "4.0.1"
+ hooks:
+ - id: flake8
+ additional_dependencies:
+ - "flake8-print==4.0.0"
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 68e5e36cd84e..1b51f1eedcde 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -42,9 +42,10 @@
"python.testing.pytestArgs": ["tests"],
"python.testing.pytestEnabled": true,
"python.testing.unittestEnabled": false,
- "python.analysis.typeCheckingMode": "off",
+ "python.analysis.typeCheckingMode": "basic",
+ "python.linting.mypyEnabled": true,
+ "python.sortImports.path": "${workspaceFolder}/.venv/bin/isort",
"search.mode": "reuseEditor",
"vetur.validation.template": false,
- "python.sortImports.path": "${workspaceFolder}/.venv/bin/isort",
"coverage-gutters.lcovname": "${workspaceFolder}/.coverage"
}
diff --git a/Dockerfile b/Dockerfile
index 8019c89dcf90..72f51b7f3a32 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -35,7 +35,7 @@ RUN apt-get update \
libpq-dev \
libwebp-dev \
# LDAP Dependencies
- libsasl2-dev libldap2-dev libssl-dev \
+ libsasl2-dev libldap2-dev libssl-dev \
gnupg gnupg2 gnupg1 \
&& pip install -U --no-cache-dir pip
@@ -65,8 +65,9 @@ COPY --from=builder-base $PYSETUP_PATH $PYSETUP_PATH
COPY ./mealie $MEALIE_HOME/mealie
COPY ./poetry.lock ./pyproject.toml $MEALIE_HOME/
-#! Future
-# COPY ./alembic ./alembic.ini $MEALIE_HOME/
+# Alembic
+COPY ./alembic $MEALIE_HOME/alembic
+COPY ./alembic.ini $MEALIE_HOME/
# venv already has runtime deps installed we get a quicker install
WORKDIR $MEALIE_HOME
@@ -81,7 +82,7 @@ ENTRYPOINT $MEALIE_HOME/mealie/run.sh "reload"
###############################################
FROM hkotel/crfpp as crfpp
-RUN echo "crfpp-container"
+RUN echo "crfpp-container"
###############################################
# Production Image
@@ -114,7 +115,7 @@ COPY ./mealie $MEALIE_HOME/mealie
COPY ./poetry.lock ./pyproject.toml $MEALIE_HOME/
COPY ./gunicorn_conf.py $MEALIE_HOME
-#! Future
+# Alembic
COPY ./alembic $MEALIE_HOME/alembic
COPY ./alembic.ini $MEALIE_HOME/
diff --git a/README.md b/README.md
index a0013defee39..028126eb8e57 100644
--- a/README.md
+++ b/README.md
@@ -9,9 +9,9 @@
[](https://github.com/hay-kot/mealie/actions/workflows/test-all.yml)
[](https://github.com/hay-kot/mealie/actions/workflows/dockerbuild.dev.yml)
[](https://github.com/hay-kot/mealie/actions/workflows/test-all.yml)
-
-
+
+
@@ -32,13 +32,13 @@
View Demo
ยท
- Report Bug
+ Report Bug
ยท
API
ยท
Request Feature
-
+
ยท
Docker Hub
@@ -47,60 +47,25 @@
-[![Product Name Screen Shot][product-screenshot]](https://example.com)
+[![Product Name Screen Shot][product-screenshot]](https://docs.mealie.io)
# About The Project
-Mealie is a self hosted recipe manager and meal planner with a RestAPI backend and a reactive frontend application built in Vue for a pleasant user experience for the whole family. Easily add recipes into your database by providing the url and Mealie will automatically import the relevant data or add a family recipe with the UI editor. Mealie also provides an API for interactions from 3rd party applications.
+Mealie is a self hosted recipe manager and meal planner with a RestAPI backend and a reactive frontend application built in Vue for a pleasant user experience for the whole family. Easily add recipes into your database by providing the url and Mealie will automatically import the relevant data or add a family recipe with the UI editor. Mealie also provides an API for interactions from 3rd party applications.
-[Remember to join the Discord](https://discord.gg/QuStdQGSGK)!
-
-
-
-## Key Features
-- ๐ Fuzzy search
-- ๐ท๏ธ Tag recipes with categories or tags for flexible sorting
-- ๐ธ Import recipes from around the web by URL
-- ๐ช Powerful bulk Category/Tag assignment
-- ๐ฑ Beautiful Mobile Views
-- ๐ Create Meal Plans
-- ๐ Generate shopping lists
-- ๐ณ Easy setup with Docker
-- ๐จ Customize your interface with color themes
-- ๐พ Export all your data in any format with Jinja2 Templates
-- ๐ Keep your data safe with automated backup and easy restore options
-- ๐ localized in many languages
-- โ Plus tons more!
- - Flexible API
- - Custom key/value pairs for recipes
- - Webhook support
- - Interactive API Documentation thanks to [FastAPI](https://fastapi.tiangolo.com/) and [Swagger](https://petstore.swagger.io/)
- - Raw JSON Recipe Editor
- - Migration from other platforms
- - Chowdown
- - Nextcloud Cookbook
- - Random meal plan generation
-
-## FAQ
-
-### Why An API?
-An API allows integration into applications like [Home Assistant](https://www.home-assistant.io/) that can act as notification engines to provide custom notifications based of Meal Plan data to remind you to defrost the chicken, marinade the steak, or start the CrockPot. Additionally, you can access nearly any backend service via the API giving you total control to extend the application. To explore the API spin up your server and navigate to http://yourserver.com/docs for interactive API documentation.
-
-### Why a Database?
-Some users of static-site generator applications like ChowDown have expressed concerns about their data being stuck in a database. Considering this is a new project it is a valid concern to be worried about your data. Mealie specifically addresses this concern by provided automatic daily backups that export your data in json, plain-text markdown files, and/or custom Jinja2 templates. **This puts you in controls of how your data is represented** when exported from Mealie, which means you can easily migrate to any other service provided Mealie doesn't work for you.
-
-As to why we need a database?
-
-- **Developer Experience:** Without a database a lot of the work to maintain your data is taken on by the developer instead of a battle tested platform for storing data.
-- **Multi User Support:** With a solid database as backend storage for your data Mealie can better support multi-user sites and avoid read/write access errors when multiple actions are taken at the same time.
+- [Remember to join the Discord](https://discord.gg/QuStdQGSGK)!
+- [Documentation](https://docs.mealie.io)
## Contributing
-Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. Especially test. Literally any tests. See the [Contributors Guide](https://hay-kot.github.io/mealie/contributors/non-coders/) for help getting started.
+Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. If you're going to be working on the code-base you'll want to use the nightly documentation to ensure you get the latest information.
-If you are not a coder, you can still contribute financially. financial contributions help me prioritize working on this project over others and helps me know that there is a real demand for project development.
+- See the [Contributors Guide](https://nightly.mealie.io/contributors/developers-guide/code-contributions/) for help getting started.
+- We use VSCode Dev Contains to make it easy for contributors to get started!
+
+If you are not a coder, you can still contribute financially. financial contributions help me prioritize working on this project over others and helps me know that there is a real demand for project development.
@@ -116,8 +81,8 @@ Huge thanks to all the sponsors of this project on [Github Sponsors](https://git
Thanks to Linode for providing Hosting for the Demo, Beta, and Documentation sites! Another big thanks to JetBrains for providing their IDEs for development.
diff --git a/dev/code-generation/_gen_utils.py b/dev/code-generation/_gen_utils.py
index b51f0dc925fe..8c6c6e2b3a0b 100644
--- a/dev/code-generation/_gen_utils.py
+++ b/dev/code-generation/_gen_utils.py
@@ -1,7 +1,6 @@
import re
from dataclasses import dataclass
from pathlib import Path
-from typing import Tuple
import black
import isort
@@ -47,7 +46,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]:
start = None
end = None
indentation = None
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
index 87a1d51f153c..32b6ced126c9 100644
--- a/docker-compose.dev.yml
+++ b/docker-compose.dev.yml
@@ -1,5 +1,5 @@
# Use root/example as user/password credentials
-version: "3.1"
+version: "3.4"
services:
# Vue Frontend
mealie-frontend:
diff --git a/docker-compose.yml b/docker-compose.yml
index da0ab8e5df66..c2d4aee3c0cb 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,4 +1,4 @@
-version: "3.1"
+version: "3.4"
services:
mealie-frontend:
container_name: mealie-frontend
diff --git a/docs/docs/contributors/developers-guide/general-guidelines.md b/docs/docs/contributors/developers-guide/general-guidelines.md
deleted file mode 100644
index 24ab52cdf257..000000000000
--- a/docs/docs/contributors/developers-guide/general-guidelines.md
+++ /dev/null
@@ -1,7 +0,0 @@
-# Guidelines
-
-## Python
-
-## Vue
-
-[See The Style Guide](../developers-guide/style-guide.md)
\ No newline at end of file
diff --git a/docs/docs/contributors/developers-guide/starting-dev-server.md b/docs/docs/contributors/developers-guide/starting-dev-server.md
index 0d419a73f582..d3c1dd50fdaf 100644
--- a/docs/docs/contributors/developers-guide/starting-dev-server.md
+++ b/docs/docs/contributors/developers-guide/starting-dev-server.md
@@ -1,26 +1,30 @@
# Development: Getting Started
!!! warning
- Be sure to use the [Nightly version](https://nightly.mealie.io/) of the docs to ensure you're up to date with
+ Be sure to use the [Nightly version](https://nightly.mealie.io/) of the docs to ensure you're up to date with
the latest changes.
After reading through the [Code Contributions Guide](../developers-guide/code-contributions.md) and forking the repo you can start working. This project is developed with :whale: docker and as such you will be greatly aided by using docker for development. It's not necessary but it is helpful.
-## With Docker
-
-!!! error "Broken"
- Developing with Docker is currently broken. Please use the "Without Docker" instructions until this is resolved, or better yet help us fix it!
-
- - [PR #756 - add frontend developer dockerfile](https://github.com/hay-kot/mealie/pull/756)
+## With VS Code Dev Containers
Prerequisites
- Docker
-- docker-compose
+- Visual Studio Code
-You can easily start the development stack by running `make docker-dev` in the root of the project directory. This will run and build the docker-compose.dev.yml file.
+First ensure that docker is running. Then when you clone the repo and open with VS Code you should see a popup asking you to reopen the project inside a development container. Click yes and it will build the development container and run the setup required to run both the backend API and the frontend webserver. This also pre-configures pre-commit hooks to ensure that the code is up to date before committing.
-## Without Docker
+Checkout the makefile for all of the available commands.
+
+!!! tip
+ For slow terminal checkout the solution in this [GitHub Issue](https://github.com/microsoft/vscode/issues/133215)
+
+ ```bash
+ git config oh-my-zsh.hide-info 1
+ ```
+
+## Without Dev Containers
### Prerequisites
- [Python 3.10](https://www.python.org/downloads/)
@@ -30,7 +34,7 @@ You can easily start the development stack by running `make docker-dev` in the r
### Installing Dependencies
-Once the prerequisites are installed you can cd into the project base directory and run `make setup` to install the python and node dependencies.
+Once the prerequisites are installed you can cd into the project base directory and run `make setup` to install the python and node dependencies.
=== "Linux / MacOs"
@@ -65,8 +69,8 @@ Once that is complete you're ready to start the servers. You'll need two shells
=== "Linux / MacOs"
```bash
- # Terminal #1
- make backend
+ # Terminal #1
+ make backend
# Terminal #2
make frontend
@@ -84,35 +88,29 @@ Once that is complete you're ready to start the servers. You'll need two shells
yarn run dev
```
-## Make File Reference
+## Make File Reference
Run `make help` for reference. If you're on a system that doesn't support makefiles in most cases you can use the commands directly in your terminal by copy/pasting them from the Makefile.
```
-purge โ ๏ธ Removes All Developer Data for a fresh server start
-clean ๐งน Remove all build, test, coverage and Python artifacts
-clean-pyc ๐งน Remove Python file artifacts
-clean-test ๐งน Remove test and coverage artifacts
-test-all ๐งช Check Lint Format and Testing
-test ๐งช Run tests quickly with the default Python
-lint ๐งบ Format, Check and Flake8
-coverage โ๏ธ Check code coverage quickly with the default Python
+docs ๐ Start Mkdocs Development Server
+code-gen ๐ค Run Code-Gen Scripts
setup ๐ Setup Development Instance
setup-model ๐ค Get the latest NLP CRF++ Model
+clean-data โ ๏ธ Removes All Developer Data for a fresh server start
+clean-pyc ๐งน Remove Python file artifacts
+clean-test ๐งน Remove test and coverage artifacts
+backend-clean ๐งน Remove all build, test, coverage and Python artifacts
+backend-test ๐งช Run tests quickly with the default Python
+backend-format ๐งบ Format, Check and Flake8
+backend-all ๐งช Runs all the backend checks and tests
+backend-coverage โ๏ธ Check code coverage quickly with the default Python
backend ๐ฌ Start Mealie Backend Development Server
frontend ๐ฌ Start Mealie Frontend Development Server
frontend-build ๐ Build Frontend in frontend/dist
frontend-generate ๐ Generate Code for Frontend
frontend-lint ๐งบ Run yarn lint
-docs ๐ Start Mkdocs Development Server
docker-dev ๐ณ Build and Start Docker Development Stack
docker-prod ๐ณ Build and Start Docker Production Stack
-code-gen ๐ค Run Code-Gen Scripts
```
-
-## Before you Commit!
-
-Before you commit any changes on the backend/python side you'll want to run `make format` to format all the code with black. `make lint` to check with flake8, and `make test` to run pytests. You can also use `make test-all` to run both `lint` and `test`.
-
-Run into another issue? [Ask for help on discord](https://discord.gg/QuStdQGSGK)
\ No newline at end of file
diff --git a/docs/docs/contributors/developers-guide/style-guide.md b/docs/docs/contributors/developers-guide/style-guide.md
deleted file mode 100644
index 342374c64507..000000000000
--- a/docs/docs/contributors/developers-guide/style-guide.md
+++ /dev/null
@@ -1,33 +0,0 @@
-# Style Guide
-
-!!! note
- Unifying styles across the application is an ongoing process, we are working on making the site more consistent.
-
-## Button Guidelines
-
-1. Buttons should follow the general color/icon scheme as outlined.
-2. All buttons should have an icon on the left side of the button and text on the right.
-3. Primary action buttons should be the default Vuetify styling.
-4. Primary action buttons should be right aligned
-5. Secondary buttons should be `text` or `outlined`. Text is preferred
-6. Other buttons should generally be "info" or "primary" color and can take any style type depending on context
-
-### Button Colors and Icons
-
-| Type | Color | Icon |
-| ----------- | :------------------ | :------------------------------------------------- |
-| Default | `info` or `primary` | None |
-| Create/New | `success` | `mdi-plus` or `$globals.icons.create` |
-| Update/Save | `success` | `mdi-save-content` or `$globals.icons.save` |
-| Edit | `info` | `mdi-square-edit-outline` or `$globals.icons.edit` |
-
-### Example
-```html
-
- mdi-plus
- Primary Button
-
-
-```
-
-
diff --git a/docs/docs/overrides/api.html b/docs/docs/overrides/api.html
index 0f34d063675f..282b77ae13f6 100644
--- a/docs/docs/overrides/api.html
+++ b/docs/docs/overrides/api.html
@@ -14,7 +14,7 @@
diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml
index 85ee8018e6d3..c5b57d3211ae 100644
--- a/docs/mkdocs.yml
+++ b/docs/mkdocs.yml
@@ -82,8 +82,6 @@ nav:
- Developers Guide:
- Code Contributions: "contributors/developers-guide/code-contributions.md"
- Dev Getting Started: "contributors/developers-guide/starting-dev-server.md"
- - Guidelines: "contributors/developers-guide/general-guidelines.md"
- - Style Guide: "contributors/developers-guide/style-guide.md"
- Guides:
- Improving Ingredient Parser: "contributors/guides/ingredient-parser.md"
diff --git a/makefile b/makefile
index cd09f2605c0f..dd178114e93c 100644
--- a/makefile
+++ b/makefile
@@ -23,15 +23,45 @@ BROWSER := python -c "$$BROWSER_PYSCRIPT"
help:
@python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST)
-purge: clean ## โ ๏ธ Removes All Developer Data for a fresh server start
+.PHONY: docs
+docs: ## ๐ Start Mkdocs Development Server
+ poetry run python dev/scripts/api_docs_gen.py && \
+ cd docs && poetry run python -m mkdocs serve
+
+code-gen: ## ๐ค Run Code-Gen Scripts
+ poetry run python dev/scripts/app_routes_gen.py
+
+# -----------------------------------------------------------------------------
+# Backend makefile
+
+.PHONY: setup
+setup: ## ๐ Setup Development Instance
+ poetry install && \
+ cd frontend && \
+ yarn install && \
+ cd ..
+
+ poetry run pre-commit install
+
+ cp -n template.env .env || true
+
+ @echo "๐ Development Setup Complete "
+ @echo "โ๏ธ Tips"
+ @echo " 1. run 'make backend' to start the API server"
+ @echo " 2. run 'make frontend' to start the Node Server"
+ @echo " 3. Testing the Natural Language Processor? Try 'make setup-model' to get the most recent model"
+
+setup-model: ## ๐ค Get the latest NLP CRF++ Model
+ @echo Fetching NLP Model - CRF++ is still Required
+ curl -L0 https://github.com/mealie-recipes/nlp-model/releases/download/v1.0.0/model.crfmodel --output ./mealie/services/parser_services/crfpp/model.crfmodel
+
+clean-data: clean ## โ ๏ธ Removes All Developer Data for a fresh server start
rm -r ./dev/data/recipes/
rm -r ./dev/data/users/
rm -f ./dev/data/mealie*.db
rm -f ./dev/data/mealie.log
rm -f ./dev/data/.secret
-clean: clean-pyc clean-test ## ๐งน Remove all build, test, coverage and Python artifacts
-
clean-pyc: ## ๐งน Remove Python file artifacts
find ./mealie -name '*.pyc' -exec rm -f {} +
find ./mealie -name '*.pyo' -exec rm -f {} +
@@ -44,46 +74,38 @@ clean-test: ## ๐งน Remove test and coverage artifacts
rm -fr htmlcov/
rm -fr .pytest_cache
-test-all: lint-test test ## ๐งช Check Lint Format and Testing
+backend-clean: clean-pyc clean-test ## ๐งน Remove all build, test, coverage and Python artifacts
+ rm -fr .mypy_cache
-test: ## ๐งช Run tests quickly with the default Python
+backend-typecheck:
+ poetry run mypy mealie
+
+backend-test: ## ๐งช Run tests quickly with the default Python
poetry run pytest
-lint-test:
- poetry run black . --check
- poetry run isort . --check-only
- poetry run flake8 mealie tests
-
-lint: ## ๐งบ Format, Check and Flake8
+backend-format: ## ๐งบ Format, Check and Flake8
poetry run isort .
poetry run black .
+
+backend-lint:
poetry run flake8 mealie tests
-coverage: ## โ๏ธ Check code coverage quickly with the default Python
- poetry run pytest
+backend-all: backend-format backend-lint backend-typecheck backend-test ## ๐งช Runs all the backend checks and tests
+
+backend-coverage: ## โ๏ธ Check code coverage quickly with the default Python
+ poetry run pytest
poetry run coverage report -m
poetry run coveragepy-lcov
poetry run coverage html
$(BROWSER) htmlcov/index.html
-.PHONY: setup
-setup: ## ๐ Setup Development Instance
- poetry install && \
- cd frontend && \
- yarn install && \
- cd ..
-
- @echo Be sure to copy the template.env files
- @echo Testing the Natural Languuage Processor? Try `make setup-model` to get the most recent model
-
-setup-model: ## ๐ค Get the latest NLP CRF++ Model
- @echo Fetching NLP Model - CRF++ is still Required
- curl -L0 https://github.com/mealie-recipes/nlp-model/releases/download/v1.0.0/model.crfmodel --output ./mealie/services/parser_services/crfpp/model.crfmodel
-
backend: ## ๐ฌ Start Mealie Backend Development Server
poetry run python mealie/db/init_db.py && \
poetry run python mealie/app.py
+# -----------------------------------------------------------------------------
+# Frontend makefile
+
.PHONY: frontend
frontend: ## ๐ฌ Start Mealie Frontend Development Server
cd frontend && yarn run dev
@@ -97,10 +119,8 @@ frontend-generate: ## ๐ Generate Code for Frontend
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 && \
- cd docs && poetry run python -m mkdocs serve
+# -----------------------------------------------------------------------------
+# Docker makefile
docker-dev: ## ๐ณ Build and Start Docker Development Stack
docker-compose -f docker-compose.dev.yml -p dev-mealie down && \
@@ -108,7 +128,3 @@ docker-dev: ## ๐ณ Build and Start Docker Development Stack
docker-prod: ## ๐ณ Build and Start Docker Production Stack
docker-compose -f docker-compose.yml -p mealie up --build
-
-code-gen: ## ๐ค Run Code-Gen Scripts
- poetry run python dev/scripts/app_routes_gen.py
-
diff --git a/mealie/core/dependencies/dependencies.py b/mealie/core/dependencies/dependencies.py
index 03cd1f352b47..28cc3104719f 100644
--- a/mealie/core/dependencies/dependencies.py
+++ b/mealie/core/dependencies/dependencies.py
@@ -1,5 +1,6 @@
import shutil
import tempfile
+from collections.abc import AsyncGenerator, Callable, Generator
from pathlib import Path
from typing import Optional
from uuid import uuid4
@@ -94,10 +95,11 @@ def validate_long_live_token(session: Session, client_token: str, id: int) -> Pr
tokens: list[LongLiveTokenInDB] = repos.api_tokens.get(id, "user_id", limit=9999)
for token in tokens:
- token: LongLiveTokenInDB
if token.token == client_token:
return token.user
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid Token")
+
def validate_file_token(token: Optional[str] = None) -> Path:
credentials_exception = HTTPException(
@@ -133,7 +135,7 @@ def validate_recipe_token(token: Optional[str] = None) -> str:
return slug
-async def temporary_zip_path() -> Path:
+async def temporary_zip_path() -> AsyncGenerator[Path, None]:
app_dirs.TEMP_DIR.mkdir(exist_ok=True, parents=True)
temp_path = app_dirs.TEMP_DIR.joinpath("my_zip_archive.zip")
@@ -143,7 +145,7 @@ async def temporary_zip_path() -> Path:
temp_path.unlink(missing_ok=True)
-async def temporary_dir() -> Path:
+async def temporary_dir() -> AsyncGenerator[Path, None]:
temp_path = app_dirs.TEMP_DIR.joinpath(uuid4().hex)
temp_path.mkdir(exist_ok=True, parents=True)
@@ -153,12 +155,12 @@ async def temporary_dir() -> Path:
shutil.rmtree(temp_path)
-def temporary_file(ext: str = "") -> Path:
+def temporary_file(ext: str = "") -> Callable[[], Generator[tempfile._TemporaryFileWrapper, None, None]]:
"""
Returns a temporary file with the specified extension
"""
- def func() -> Path:
+ def func():
temp_path = app_dirs.TEMP_DIR.joinpath(uuid4().hex + ext)
temp_path.touch()
diff --git a/mealie/core/root_logger.py b/mealie/core/root_logger.py
index 5d2adea0909b..1e1e54e88ee7 100644
--- a/mealie/core/root_logger.py
+++ b/mealie/core/root_logger.py
@@ -20,7 +20,7 @@ class LoggerConfig:
format: str
date_format: str
logger_file: str
- level: str = logging.INFO
+ level: int = logging.INFO
@lru_cache
diff --git a/mealie/core/security.py b/mealie/core/security.py
index d6dd611bb0ef..14845321741d 100644
--- a/mealie/core/security.py
+++ b/mealie/core/security.py
@@ -36,7 +36,7 @@ def create_recipe_slug_token(file_path: str) -> str:
return create_access_token(token_data, expires_delta=timedelta(minutes=30))
-def user_from_ldap(db: AllRepositories, session, username: str, password: str) -> PrivateUser:
+def user_from_ldap(db: AllRepositories, session, username: str, password: str) -> PrivateUser | bool:
"""Given a username and password, tries to authenticate by BINDing to an
LDAP server
diff --git a/mealie/core/settings/db_providers.py b/mealie/core/settings/db_providers.py
index b894e595598d..08bfc38a8709 100644
--- a/mealie/core/settings/db_providers.py
+++ b/mealie/core/settings/db_providers.py
@@ -35,7 +35,7 @@ class PostgresProvider(AbstractDBProvider, BaseSettings):
POSTGRES_USER: str = "mealie"
POSTGRES_PASSWORD: str = "mealie"
POSTGRES_SERVER: str = "postgres"
- POSTGRES_PORT: str = 5432
+ POSTGRES_PORT: str = "5432"
POSTGRES_DB: str = "mealie"
@property
diff --git a/mealie/core/settings/settings.py b/mealie/core/settings/settings.py
index 60f3698ccb38..d32198a255f1 100644
--- a/mealie/core/settings/settings.py
+++ b/mealie/core/settings/settings.py
@@ -2,7 +2,7 @@ import secrets
from pathlib import Path
from typing import Optional
-from pydantic import BaseSettings
+from pydantic import BaseSettings, NoneStr
from .db_providers import AbstractDBProvider, db_provider_factory
@@ -33,26 +33,26 @@ class AppSettings(BaseSettings):
SECRET: str
@property
- def DOCS_URL(self) -> str:
+ def DOCS_URL(self) -> str | None:
return "/docs" if self.API_DOCS else None
@property
- def REDOC_URL(self) -> str:
+ def REDOC_URL(self) -> str | None:
return "/redoc" if self.API_DOCS else None
# ===============================================
# Database Configuration
DB_ENGINE: str = "sqlite" # Options: 'sqlite', 'postgres'
- DB_PROVIDER: AbstractDBProvider = None
+ DB_PROVIDER: Optional[AbstractDBProvider] = None
@property
- def DB_URL(self) -> str:
- return self.DB_PROVIDER.db_url
+ def DB_URL(self) -> str | None:
+ return self.DB_PROVIDER.db_url if self.DB_PROVIDER else None
@property
- def DB_URL_PUBLIC(self) -> str:
- return self.DB_PROVIDER.db_url_public
+ def DB_URL_PUBLIC(self) -> str | None:
+ return self.DB_PROVIDER.db_url_public if self.DB_PROVIDER else None
DEFAULT_GROUP: str = "Home"
DEFAULT_EMAIL: str = "changeme@email.com"
@@ -88,9 +88,9 @@ class AppSettings(BaseSettings):
# LDAP Configuration
LDAP_AUTH_ENABLED: bool = False
- LDAP_SERVER_URL: str = None
- LDAP_BIND_TEMPLATE: str = None
- LDAP_ADMIN_FILTER: str = None
+ LDAP_SERVER_URL: NoneStr = None
+ LDAP_BIND_TEMPLATE: NoneStr = None
+ LDAP_ADMIN_FILTER: NoneStr = None
@property
def LDAP_ENABLED(self) -> bool:
diff --git a/mealie/db/db_setup.py b/mealie/db/db_setup.py
index 448ea115e421..31a5b9692816 100644
--- a/mealie/db/db_setup.py
+++ b/mealie/db/db_setup.py
@@ -24,7 +24,7 @@ def sql_global_init(db_url: str):
return SessionLocal, engine
-SessionLocal, engine = sql_global_init(settings.DB_URL)
+SessionLocal, engine = sql_global_init(settings.DB_URL) # type: ignore
def create_session() -> Session:
diff --git a/mealie/db/init_db.py b/mealie/db/init_db.py
index 0c519013ff2d..4371eea8fe7b 100644
--- a/mealie/db/init_db.py
+++ b/mealie/db/init_db.py
@@ -1,5 +1,5 @@
+from collections.abc import Callable
from pathlib import Path
-from typing import Callable
from sqlalchemy import engine
diff --git a/mealie/db/models/_all_models.py b/mealie/db/models/_all_models.py
index 530396f95bee..d4f19ef02289 100644
--- a/mealie/db/models/_all_models.py
+++ b/mealie/db/models/_all_models.py
@@ -1,5 +1,5 @@
from .group import *
from .labels import *
-from .recipe.recipe import *
+from .recipe.recipe import * # type: ignore
from .server import *
from .users import *
diff --git a/mealie/db/models/_model_base.py b/mealie/db/models/_model_base.py
index fc8d93bfa39a..a8744e35c68e 100644
--- a/mealie/db/models/_model_base.py
+++ b/mealie/db/models/_model_base.py
@@ -24,7 +24,7 @@ class BaseMixins:
@classmethod
def get_ref(cls, match_value: str, match_attr: str = None, session: Session = None):
- match_attr = match_attr = cls.Config.get_attr
+ match_attr = match_attr or cls.Config.get_attr # type: ignore
if match_value is None or session is None:
return None
diff --git a/mealie/db/models/_model_utils/auto_init.py b/mealie/db/models/_model_utils/auto_init.py
index 22e0582a7399..5bf05991bbce 100644
--- a/mealie/db/models/_model_utils/auto_init.py
+++ b/mealie/db/models/_model_utils/auto_init.py
@@ -1,7 +1,7 @@
from functools import wraps
from uuid import UUID
-from pydantic import BaseModel, Field
+from pydantic import BaseModel, Field, NoneStr
from sqlalchemy.orm import MANYTOMANY, MANYTOONE, ONETOMANY, Session
from sqlalchemy.orm.decl_api import DeclarativeMeta
from sqlalchemy.orm.mapper import Mapper
@@ -21,7 +21,7 @@ class AutoInitConfig(BaseModel):
Config class for `auto_init` decorator.
"""
- get_attr: str = None
+ get_attr: NoneStr = None
exclude: set = Field(default_factory=_default_exclusion)
# auto_create: bool = False
@@ -83,12 +83,14 @@ def handle_one_to_many_list(session: Session, get_attr, relation_cls, all_elemen
elem_id = elem.get(get_attr, None) if isinstance(elem, dict) else elem
existing_elem = session.query(relation_cls).filter_by(**{get_attr: elem_id}).one_or_none()
- if existing_elem is None:
- elems_to_create.append(elem)
+ is_dict = isinstance(elem, dict)
+
+ if existing_elem is None and is_dict:
+ elems_to_create.append(elem) # type: ignore
continue
- elif isinstance(elem, dict):
- for key, value in elem.items():
+ elif is_dict:
+ for key, value in elem.items(): # type: ignore
if key not in cfg.exclude:
setattr(existing_elem, key, value)
diff --git a/mealie/db/models/_model_utils/helpers.py b/mealie/db/models/_model_utils/helpers.py
index f62a063f3f5d..b3a3dcfc5c53 100644
--- a/mealie/db/models/_model_utils/helpers.py
+++ b/mealie/db/models/_model_utils/helpers.py
@@ -1,5 +1,6 @@
import inspect
-from typing import Any, Callable
+from collections.abc import Callable
+from typing import Any
def get_valid_call(func: Callable, args_dict) -> dict:
@@ -8,7 +9,7 @@ def get_valid_call(func: Callable, args_dict) -> dict:
the original dictionary will be returned.
"""
- def get_valid_args(func: Callable) -> tuple:
+ def get_valid_args(func: Callable) -> list[str]:
"""
Returns a tuple of valid arguemnts for the supplied function.
"""
diff --git a/mealie/db/models/group/group.py b/mealie/db/models/group/group.py
index 8bff6a005521..d6c9079e959f 100644
--- a/mealie/db/models/group/group.py
+++ b/mealie/db/models/group/group.py
@@ -78,8 +78,8 @@ class Group(SqlAlchemyBase, BaseMixins):
def __init__(self, **_) -> None:
pass
- @staticmethod
- def get_ref(session: Session, name: str):
+ @staticmethod # TODO: Remove this
+ def get_ref(session: Session, name: str): # type: ignore
settings = get_app_settings()
item = session.query(Group).filter(Group.name == name).one_or_none()
diff --git a/mealie/db/models/recipe/category.py b/mealie/db/models/recipe/category.py
index f894d17a1b6a..f38cc914cb3f 100644
--- a/mealie/db/models/recipe/category.py
+++ b/mealie/db/models/recipe/category.py
@@ -63,8 +63,8 @@ class Category(SqlAlchemyBase, BaseMixins):
self.name = name.strip()
self.slug = slugify(name)
- @classmethod
- def get_ref(cls, match_value: str, session=None):
+ @classmethod # TODO: Remove this
+ def get_ref(cls, match_value: str, session=None): # type: ignore
if not session or not match_value:
return None
@@ -76,4 +76,4 @@ class Category(SqlAlchemyBase, BaseMixins):
return result
else:
logger.debug("Category doesn't exists, creating Category")
- return Category(name=match_value)
+ return Category(name=match_value) # type: ignore
diff --git a/mealie/db/models/recipe/comment.py b/mealie/db/models/recipe/comment.py
index 1a8ca7541f63..ac3e9b0f8662 100644
--- a/mealie/db/models/recipe/comment.py
+++ b/mealie/db/models/recipe/comment.py
@@ -22,5 +22,5 @@ class RecipeComment(SqlAlchemyBase, BaseMixins):
def __init__(self, **_) -> None:
pass
- def update(self, text, **_) -> None:
+ def update(self, text, **_) -> None: # type: ignore
self.text = text
diff --git a/mealie/db/models/recipe/recipe.py b/mealie/db/models/recipe/recipe.py
index 8e7e2d594f22..2b2c108d3717 100644
--- a/mealie/db/models/recipe/recipe.py
+++ b/mealie/db/models/recipe/recipe.py
@@ -1,5 +1,4 @@
import datetime
-from datetime import date
import sqlalchemy as sa
import sqlalchemy.orm as orm
@@ -107,7 +106,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
extras: list[ApiExtras] = orm.relationship("ApiExtras", cascade="all, delete-orphan")
# Time Stamp Properties
- date_added = sa.Column(sa.Date, default=date.today)
+ date_added = sa.Column(sa.Date, default=datetime.date.today)
date_updated = sa.Column(sa.DateTime)
# Shopping List Refs
diff --git a/mealie/db/models/recipe/tag.py b/mealie/db/models/recipe/tag.py
index 472676c6ed4f..4de8af92771f 100644
--- a/mealie/db/models/recipe/tag.py
+++ b/mealie/db/models/recipe/tag.py
@@ -50,8 +50,8 @@ class Tag(SqlAlchemyBase, BaseMixins):
self.name = name.strip()
self.slug = slugify(self.name)
- @classmethod
- def get_ref(cls, match_value: str, session=None):
+ @classmethod # TODO: Remove this
+ def get_ref(cls, match_value: str, session=None): # type: ignore
if not session or not match_value:
return None
@@ -62,4 +62,4 @@ class Tag(SqlAlchemyBase, BaseMixins):
return result
else:
logger.debug("Category doesn't exists, creating Category")
- return Tag(name=match_value)
+ return Tag(name=match_value) # type: ignore
diff --git a/mealie/db/models/users/users.py b/mealie/db/models/users/users.py
index b55758c9c3e4..ec01a43bc8fc 100644
--- a/mealie/db/models/users/users.py
+++ b/mealie/db/models/users/users.py
@@ -124,6 +124,6 @@ class User(SqlAlchemyBase, BaseMixins):
self.can_invite = can_invite
self.can_organize = can_organize
- @staticmethod
- def get_ref(session, id: str):
+ @staticmethod # TODO: Remove This
+ def get_ref(session, id: str): # type: ignore
return session.query(User).filter(User.id == id).one()
diff --git a/mealie/pkgs/img/minify.py b/mealie/pkgs/img/minify.py
index 6484db57d4ef..ceeb2a187fca 100644
--- a/mealie/pkgs/img/minify.py
+++ b/mealie/pkgs/img/minify.py
@@ -19,11 +19,11 @@ def get_format(image: Path) -> str:
def sizeof_fmt(file_path: Path, decimal_places=2):
if not file_path.exists():
return "(File Not Found)"
- size = file_path.stat().st_size
+ size: int | float = file_path.stat().st_size
for unit in ["B", "kB", "MB", "GB", "TB", "PB"]:
- if size < 1024.0 or unit == "PiB":
+ if size < 1024 or unit == "PiB":
break
- size /= 1024.0
+ size /= 1024
return f"{size:.{decimal_places}f} {unit}"
diff --git a/mealie/repos/repository_generic.py b/mealie/repos/repository_generic.py
index 530b54e3c918..4fc48155391e 100644
--- a/mealie/repos/repository_generic.py
+++ b/mealie/repos/repository_generic.py
@@ -1,5 +1,5 @@
-from typing import Any, Callable, Generic, TypeVar, Union
-from uuid import UUID
+from collections.abc import Callable
+from typing import Any, Generic, TypeVar, Union
from pydantic import UUID4, BaseModel
from sqlalchemy import func
@@ -18,7 +18,7 @@ class RepositoryGeneric(Generic[T, D]):
Generic ([D]): Represents the SqlAlchemyModel Model
"""
- def __init__(self, session: Session, primary_key: Union[str, int], sql_model: D, schema: T) -> None:
+ def __init__(self, session: Session, primary_key: str, sql_model: type[D], schema: type[T]) -> None:
self.session = session
self.primary_key = primary_key
self.sql_model = sql_model
@@ -26,10 +26,10 @@ class RepositoryGeneric(Generic[T, D]):
self.observers: list = []
self.limit_by_group = False
- self.user_id = None
+ self.user_id: UUID4 = None
self.limit_by_user = False
- self.group_id = None
+ self.group_id: UUID4 = None
def subscribe(self, func: Callable) -> None:
self.observers.append(func)
@@ -39,7 +39,7 @@ class RepositoryGeneric(Generic[T, D]):
self.user_id = user_id
return self
- def by_group(self, group_id: UUID) -> "RepositoryGeneric[T, D]":
+ def by_group(self, group_id: UUID4) -> "RepositoryGeneric[T, D]":
self.limit_by_group = True
self.group_id = group_id
return self
@@ -88,7 +88,7 @@ class RepositoryGeneric(Generic[T, D]):
def multi_query(
self,
- query_by: dict[str, str],
+ query_by: dict[str, str | bool | int | UUID4],
start=0,
limit: int = None,
override_schema=None,
@@ -152,7 +152,7 @@ class RepositoryGeneric(Generic[T, D]):
filter = self._filter_builder(**{match_key: match_value})
return self.session.query(self.sql_model).filter_by(**filter).one()
- def get_one(self, value: str | int | UUID4, key: str = None, any_case=False, override_schema=None) -> T:
+ def get_one(self, value: str | int | UUID4, key: str = None, any_case=False, override_schema=None) -> T | None:
key = key or self.primary_key
q = self.session.query(self.sql_model)
@@ -166,14 +166,14 @@ class RepositoryGeneric(Generic[T, D]):
result = q.one_or_none()
if not result:
- return
+ return None
eff_schema = override_schema or self.schema
return eff_schema.from_orm(result)
def get(
self, match_value: str | int | UUID4, match_key: str = None, limit=1, any_case=False, override_schema=None
- ) -> T | list[T]:
+ ) -> T | list[T] | None:
"""Retrieves an entry from the database by matching a key/value pair. If no
key is provided the class objects primary key will be used to match against.
@@ -193,7 +193,7 @@ class RepositoryGeneric(Generic[T, D]):
search_attr = getattr(self.sql_model, match_key)
result = (
self.session.query(self.sql_model)
- .filter(func.lower(search_attr) == match_value.lower())
+ .filter(func.lower(search_attr) == match_value.lower()) # type: ignore
.limit(limit)
.all()
)
@@ -210,7 +210,7 @@ class RepositoryGeneric(Generic[T, D]):
return [eff_schema.from_orm(x) for x in result]
- def create(self, document: T) -> T:
+ def create(self, document: T | BaseModel) -> T:
"""Creates a new database entry for the given SQL Alchemy Model.
Args:
@@ -221,7 +221,7 @@ class RepositoryGeneric(Generic[T, D]):
dict: A dictionary representation of the database entry
"""
document = document if isinstance(document, dict) else document.dict()
- new_document = self.sql_model(session=self.session, **document)
+ new_document = self.sql_model(session=self.session, **document) # type: ignore
self.session.add(new_document)
self.session.commit()
self.session.refresh(new_document)
@@ -231,7 +231,7 @@ class RepositoryGeneric(Generic[T, D]):
return self.schema.from_orm(new_document)
- def update(self, match_value: str | int | UUID4, new_data: dict) -> T:
+ def update(self, match_value: str | int | UUID4, new_data: dict | BaseModel) -> T:
"""Update a database entry.
Args:
session (Session): Database Session
@@ -244,7 +244,7 @@ class RepositoryGeneric(Generic[T, D]):
new_data = new_data if isinstance(new_data, dict) else new_data.dict()
entry = self._query_one(match_value=match_value)
- entry.update(session=self.session, **new_data)
+ entry.update(session=self.session, **new_data) # type: ignore
if self.observers:
self.update_observers()
@@ -252,13 +252,14 @@ class RepositoryGeneric(Generic[T, D]):
self.session.commit()
return self.schema.from_orm(entry)
- def patch(self, match_value: str | int | UUID4, new_data: dict) -> T:
+ def patch(self, match_value: str | int | UUID4, new_data: dict | BaseModel) -> T | None:
new_data = new_data if isinstance(new_data, dict) else new_data.dict()
entry = self._query_one(match_value=match_value)
if not entry:
- return
+ # TODO: Should raise exception
+ return None
entry_as_dict = self.schema.from_orm(entry).dict()
entry_as_dict.update(new_data)
@@ -300,7 +301,7 @@ class RepositoryGeneric(Generic[T, D]):
attr_match: str = None,
count=True,
override_schema=None,
- ) -> Union[int, T]:
+ ) -> Union[int, list[T]]:
eff_schema = override_schema or self.schema
# attr_filter = getattr(self.sql_model, attribute_name)
@@ -316,7 +317,7 @@ class RepositoryGeneric(Generic[T, D]):
new_documents = []
for document in documents:
document = document if isinstance(document, dict) else document.dict()
- new_document = self.sql_model(session=self.session, **document)
+ new_document = self.sql_model(session=self.session, **document) # type: ignore
new_documents.append(new_document)
self.session.add_all(new_documents)
diff --git a/mealie/repos/repository_meal_plan_rules.py b/mealie/repos/repository_meal_plan_rules.py
index a6f1951fac17..4c9e2a777700 100644
--- a/mealie/repos/repository_meal_plan_rules.py
+++ b/mealie/repos/repository_meal_plan_rules.py
@@ -10,7 +10,7 @@ from .repository_generic import RepositoryGeneric
class RepositoryMealPlanRules(RepositoryGeneric[PlanRulesOut, GroupMealPlanRules]):
def by_group(self, group_id: UUID) -> "RepositoryMealPlanRules":
- return super().by_group(group_id)
+ return super().by_group(group_id) # type: ignore
def get_rules(self, day: PlanRulesDay, entry_type: PlanRulesType) -> list[PlanRulesOut]:
qry = self.session.query(GroupMealPlanRules).filter(
diff --git a/mealie/repos/repository_meals.py b/mealie/repos/repository_meals.py
index 27bff973c461..27e32888ef29 100644
--- a/mealie/repos/repository_meals.py
+++ b/mealie/repos/repository_meals.py
@@ -9,10 +9,10 @@ from .repository_generic import RepositoryGeneric
class RepositoryMeals(RepositoryGeneric[ReadPlanEntry, GroupMealPlan]):
def get_slice(self, start: date, end: date, group_id: UUID) -> list[ReadPlanEntry]:
- start = start.strftime("%Y-%m-%d")
- end = end.strftime("%Y-%m-%d")
+ start_str = start.strftime("%Y-%m-%d")
+ end_str = end.strftime("%Y-%m-%d")
qry = self.session.query(GroupMealPlan).filter(
- GroupMealPlan.date.between(start, end),
+ GroupMealPlan.date.between(start_str, end_str),
GroupMealPlan.group_id == group_id,
)
diff --git a/mealie/repos/repository_recipes.py b/mealie/repos/repository_recipes.py
index 488cb8284efc..6fafa24de2c2 100644
--- a/mealie/repos/repository_recipes.py
+++ b/mealie/repos/repository_recipes.py
@@ -18,7 +18,7 @@ from .repository_generic import RepositoryGeneric
class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
def by_group(self, group_id: UUID) -> "RepositoryRecipes":
- return super().by_group(group_id)
+ return super().by_group(group_id) # type: ignore
def get_all_public(self, limit: int = None, order_by: str = None, start=0, override_schema=None):
eff_schema = override_schema or self.schema
@@ -47,14 +47,14 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
.all()
]
- def update_image(self, slug: str, _: str = None) -> str:
+ def update_image(self, slug: str, _: str = None) -> int:
entry: RecipeModel = self._query_one(match_value=slug)
entry.image = randint(0, 255)
self.session.commit()
return entry.image
- def count_uncategorized(self, count=True, override_schema=None) -> int:
+ def count_uncategorized(self, count=True, override_schema=None):
return self._count_attribute(
attribute_name=RecipeModel.recipe_category,
attr_match=None,
@@ -62,7 +62,7 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
override_schema=override_schema,
)
- def count_untagged(self, count=True, override_schema=None) -> int:
+ def count_untagged(self, count=True, override_schema=None):
return self._count_attribute(
attribute_name=RecipeModel.tags,
attr_match=None,
@@ -105,7 +105,9 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
.all()
]
- def get_random_by_categories_and_tags(self, categories: list[RecipeCategory], tags: list[RecipeTag]) -> Recipe:
+ def get_random_by_categories_and_tags(
+ self, categories: list[RecipeCategory], tags: list[RecipeTag]
+ ) -> list[Recipe]:
"""
get_random_by_categories returns a single random Recipe that contains every category provided
in the list. This uses a function built in to Postgres and SQLite to get a random row limited
diff --git a/mealie/repos/repository_shopping_list.py b/mealie/repos/repository_shopping_list.py
index 69f39bc95679..2322ddbd46fc 100644
--- a/mealie/repos/repository_shopping_list.py
+++ b/mealie/repos/repository_shopping_list.py
@@ -7,5 +7,5 @@ from .repository_generic import RepositoryGeneric
class RepositoryShoppingList(RepositoryGeneric[ShoppingListOut, ShoppingList]):
- def update(self, item_id: UUID4, data: ShoppingListUpdate) -> ShoppingListOut:
+ def update(self, item_id: UUID4, data: ShoppingListUpdate) -> ShoppingListOut: # type: ignore
return super().update(item_id, data)
diff --git a/mealie/repos/repository_users.py b/mealie/repos/repository_users.py
index 7dd5429cf79f..4189f94900ac 100644
--- a/mealie/repos/repository_users.py
+++ b/mealie/repos/repository_users.py
@@ -16,7 +16,7 @@ class RepositoryUsers(RepositoryGeneric[PrivateUser, User]):
return self.schema.from_orm(entry)
- def create(self, user: PrivateUser):
+ def create(self, user: PrivateUser | dict):
new_user = super().create(user)
# Select Random Image
diff --git a/mealie/repos/seed/seeders.py b/mealie/repos/seed/seeders.py
index 8767876448b1..349eb01c0488 100644
--- a/mealie/repos/seed/seeders.py
+++ b/mealie/repos/seed/seeders.py
@@ -1,5 +1,5 @@
import json
-from typing import Generator
+from collections.abc import Generator
from mealie.schema.labels import MultiPurposeLabelSave
from mealie.schema.recipe.recipe_ingredient import SaveIngredientFood, SaveIngredientUnit
diff --git a/mealie/routes/_base/abc_controller.py b/mealie/routes/_base/abc_controller.py
index b8ad87f8fe62..2256483146b2 100644
--- a/mealie/routes/_base/abc_controller.py
+++ b/mealie/routes/_base/abc_controller.py
@@ -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),
}
diff --git a/mealie/routes/_base/controller.py b/mealie/routes/_base/controller.py
index 8030378fe037..cec71d40b6b0 100644
--- a/mealie/routes/_base/controller.py
+++ b/mealie/routes/_base/controller.py
@@ -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] + [
diff --git a/mealie/routes/_base/mixins.py b/mealie/routes/_base/mixins.py
index 7639ecfbd569..53eda1b39017 100644
--- a/mealie/routes/_base/mixins.py
+++ b/mealie/routes/_base/mixins.py
@@ -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(
diff --git a/mealie/routes/_base/routers.py b/mealie/routes/_base/routers.py
index e0a5be5cf770..cf0d24250283 100644
--- a/mealie/routes/_base/routers.py
+++ b/mealie/routes/_base/routers.py
@@ -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)])
diff --git a/mealie/routes/auth/auth.py b/mealie/routes/auth/auth.py
index 7dd63e7e387b..61b5ff7d40cd 100644
--- a/mealie/routes/auth/auth.py
+++ b/mealie/routes/auth/auth.py
@@ -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)
diff --git a/mealie/routes/groups/controller_cookbooks.py b/mealie/routes/groups/controller_cookbooks.py
index 90a99c6f305e..b5ed2e839edc 100644
--- a/mealie/routes/groups/controller_cookbooks.py
+++ b/mealie/routes/groups/controller_cookbooks.py
@@ -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),
}
diff --git a/mealie/routes/groups/controller_group_reports.py b/mealie/routes/groups/controller_group_reports.py
index e2ef26624f7b..66b1c55e0340 100644
--- a/mealie/routes/groups/controller_group_reports.py
+++ b/mealie/routes/groups/controller_group_reports.py
@@ -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.")
diff --git a/mealie/routes/groups/controller_mealplan.py b/mealie/routes/groups/controller_mealplan.py
index 76e79a58d5d9..4bf6754d072a 100644
--- a/mealie/routes/groups/controller_mealplan.py
+++ b/mealie/routes/groups/controller_mealplan.py
@@ -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()
diff --git a/mealie/routes/groups/controller_migrations.py b/mealie/routes/groups/controller_migrations.py
index 2a0c59d57779..c19b1ee5e874 100644
--- a/mealie/routes/groups/controller_migrations.py
+++ b/mealie/routes/groups/controller_migrations.py
@@ -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)
diff --git a/mealie/routes/handlers.py b/mealie/routes/handlers.py
index 4f1bfaea31f7..209aeb6f8f47 100644
--- a/mealie/routes/handlers.py
+++ b/mealie/routes/handlers.py
@@ -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}
diff --git a/mealie/routes/recipe/recipe_crud_routes.py b/mealie/routes/recipe/recipe_crud_routes.py
index ce5cf87f7441..960f35fda939 100644
--- a/mealie/routes/recipe/recipe_crud_routes.py
+++ b/mealie/routes/recipe/recipe_crud_routes.py
@@ -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)
diff --git a/mealie/schema/_mealie/__init__.py b/mealie/schema/_mealie/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/mealie/schema/_mealie/types.py b/mealie/schema/_mealie/types.py
new file mode 100644
index 000000000000..211b44da017b
--- /dev/null
+++ b/mealie/schema/_mealie/types.py
@@ -0,0 +1,3 @@
+from typing import Optional
+
+NoneFloat = Optional[float]
diff --git a/mealie/schema/admin/backup.py b/mealie/schema/admin/backup.py
index 93bc62a1670b..03d7b58691b7 100644
--- a/mealie/schema/admin/backup.py
+++ b/mealie/schema/admin/backup.py
@@ -1,5 +1,5 @@
from datetime import datetime
-from typing import List, Optional
+from typing import Optional
from pydantic import BaseModel
@@ -22,7 +22,7 @@ class ImportJob(BackupOptions):
class CreateBackup(BaseModel):
tag: Optional[str]
options: BackupOptions
- templates: Optional[List[str]]
+ templates: Optional[list[str]]
class BackupFile(BaseModel):
@@ -32,5 +32,5 @@ class BackupFile(BaseModel):
class AllBackups(BaseModel):
- imports: List[BackupFile]
- templates: List[str]
+ imports: list[BackupFile]
+ templates: list[str]
diff --git a/mealie/schema/admin/migration.py b/mealie/schema/admin/migration.py
index 61081e6a1037..21674ad489fc 100644
--- a/mealie/schema/admin/migration.py
+++ b/mealie/schema/admin/migration.py
@@ -1,5 +1,4 @@
from datetime import datetime
-from typing import List
from pydantic.main import BaseModel
@@ -17,7 +16,7 @@ class MigrationFile(BaseModel):
class Migrations(BaseModel):
type: str
- files: List[MigrationFile] = []
+ files: list[MigrationFile] = []
class MigrationImport(RecipeImport):
diff --git a/mealie/schema/group/group_events.py b/mealie/schema/group/group_events.py
index 3e0bce863487..3e1582cbde1c 100644
--- a/mealie/schema/group/group_events.py
+++ b/mealie/schema/group/group_events.py
@@ -1,5 +1,5 @@
from fastapi_camelcase import CamelModel
-from pydantic import UUID4
+from pydantic import UUID4, NoneStr
# =============================================================================
# Group Events Notifier Options
@@ -68,7 +68,7 @@ class GroupEventNotifierSave(GroupEventNotifierCreate):
class GroupEventNotifierUpdate(GroupEventNotifierSave):
id: UUID4
- apprise_url: str = None
+ apprise_url: NoneStr = None
class GroupEventNotifierOut(CamelModel):
diff --git a/mealie/schema/group/invite_token.py b/mealie/schema/group/invite_token.py
index 387fcff20382..5a87a3535638 100644
--- a/mealie/schema/group/invite_token.py
+++ b/mealie/schema/group/invite_token.py
@@ -1,6 +1,7 @@
from uuid import UUID
from fastapi_camelcase import CamelModel
+from pydantic import NoneStr
class CreateInviteToken(CamelModel):
@@ -29,4 +30,4 @@ class EmailInvitation(CamelModel):
class EmailInitationResponse(CamelModel):
success: bool
- error: str = None
+ error: NoneStr = None
diff --git a/mealie/schema/mapper.py b/mealie/schema/mapper.py
index e2024126aec3..2ab47dda3aac 100644
--- a/mealie/schema/mapper.py
+++ b/mealie/schema/mapper.py
@@ -18,7 +18,7 @@ def mapper(source: U, dest: T, **_) -> T:
return dest
-def cast(source: U, dest: T, **kwargs) -> T:
+def cast(source: U, dest: type[T], **kwargs) -> T:
create_data = {field: getattr(source, field) for field in source.__fields__ if field in dest.__fields__}
create_data.update(kwargs or {})
return dest(**create_data)
diff --git a/mealie/schema/recipe/__init__.py b/mealie/schema/recipe/__init__.py
index 0dfd2a7dfb6d..d558cd4fdc45 100644
--- a/mealie/schema/recipe/__init__.py
+++ b/mealie/schema/recipe/__init__.py
@@ -3,13 +3,13 @@ from .recipe import *
from .recipe_asset import *
from .recipe_bulk_actions import *
from .recipe_category import *
-from .recipe_comments import *
+from .recipe_comments import * # type: ignore
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_share_token import * # type: ignore
from .recipe_step import *
from .recipe_tool import *
from .request_helpers import *
diff --git a/mealie/schema/recipe/recipe_ingredient.py b/mealie/schema/recipe/recipe_ingredient.py
index 03febdedbe58..3c5842d65dc8 100644
--- a/mealie/schema/recipe/recipe_ingredient.py
+++ b/mealie/schema/recipe/recipe_ingredient.py
@@ -7,6 +7,8 @@ from uuid import UUID, uuid4
from fastapi_camelcase import CamelModel
from pydantic import UUID4, Field
+from mealie.schema._mealie.types import NoneFloat
+
class UnitFoodBase(CamelModel):
name: str
@@ -23,7 +25,7 @@ class SaveIngredientFood(CreateIngredientFood):
class IngredientFood(CreateIngredientFood):
id: UUID4
- label: MultiPurposeLabelSummary = None
+ label: Optional[MultiPurposeLabelSummary] = None
class Config:
orm_mode = True
@@ -63,12 +65,12 @@ class RecipeIngredient(CamelModel):
class IngredientConfidence(CamelModel):
- average: float = None
- comment: float = None
- name: float = None
- unit: float = None
- quantity: float = None
- food: float = None
+ average: NoneFloat = None
+ comment: NoneFloat = None
+ name: NoneFloat = None
+ unit: NoneFloat = None
+ quantity: NoneFloat = None
+ food: NoneFloat = None
class ParsedIngredient(CamelModel):
diff --git a/mealie/schema/recipe/recipe_tool.py b/mealie/schema/recipe/recipe_tool.py
index 4be7b5df1f51..c5daa0cba629 100644
--- a/mealie/schema/recipe/recipe_tool.py
+++ b/mealie/schema/recipe/recipe_tool.py
@@ -1,4 +1,4 @@
-from typing import List
+import typing
from fastapi_camelcase import CamelModel
from pydantic import UUID4
@@ -22,7 +22,7 @@ class RecipeTool(RecipeToolCreate):
class RecipeToolResponse(RecipeTool):
- recipes: List["Recipe"] = []
+ recipes: typing.List["Recipe"] = []
class Config:
orm_mode = True
diff --git a/mealie/schema/user/auth.py b/mealie/schema/user/auth.py
index 92b1e9199345..cc3f648449e5 100644
--- a/mealie/schema/user/auth.py
+++ b/mealie/schema/user/auth.py
@@ -11,4 +11,4 @@ class Token(BaseModel):
class TokenData(BaseModel):
user_id: Optional[UUID4]
- username: Optional[constr(to_lower=True, strip_whitespace=True)] = None
+ username: Optional[constr(to_lower=True, strip_whitespace=True)] = None # type: ignore
diff --git a/mealie/schema/user/registration.py b/mealie/schema/user/registration.py
index 95a3ec825a2c..a790f776fa3a 100644
--- a/mealie/schema/user/registration.py
+++ b/mealie/schema/user/registration.py
@@ -1,13 +1,13 @@
from fastapi_camelcase import CamelModel
from pydantic import validator
-from pydantic.types import constr
+from pydantic.types import NoneStr, constr
class CreateUserRegistration(CamelModel):
- group: str = None
- group_token: str = None
- email: constr(to_lower=True, strip_whitespace=True)
- username: constr(to_lower=True, strip_whitespace=True)
+ group: NoneStr = None
+ group_token: NoneStr = None
+ email: constr(to_lower=True, strip_whitespace=True) # type: ignore
+ username: constr(to_lower=True, strip_whitespace=True) # type: ignore
password: str
password_confirm: str
advanced: bool = False
diff --git a/mealie/schema/user/user.py b/mealie/schema/user/user.py
index fe09bc2ae6aa..e41dd93bf055 100644
--- a/mealie/schema/user/user.py
+++ b/mealie/schema/user/user.py
@@ -53,7 +53,7 @@ class GroupBase(CamelModel):
class UserBase(CamelModel):
username: Optional[str]
full_name: Optional[str] = None
- email: constr(to_lower=True, strip_whitespace=True)
+ email: constr(to_lower=True, strip_whitespace=True) # type: ignore
admin: bool = False
group: Optional[str]
advanced: bool = False
@@ -107,7 +107,7 @@ class UserOut(UserBase):
class UserFavorites(UserBase):
- favorite_recipes: list[RecipeSummary] = []
+ favorite_recipes: list[RecipeSummary] = [] # type: ignore
class Config:
orm_mode = True
diff --git a/mealie/services/backups/exports.py b/mealie/services/backups/exports.py
index fe1f0fb173d8..af43abaf5ed6 100644
--- a/mealie/services/backups/exports.py
+++ b/mealie/services/backups/exports.py
@@ -39,7 +39,7 @@ class ExportDatabase:
try:
self.templates = [app_dirs.TEMPLATE_DIR.joinpath(x) for x in templates]
except Exception:
- self.templates = False
+ self.templates = []
logger.info("No Jinja2 Templates Registered for Export")
required_dirs = [
diff --git a/mealie/services/backups/imports.py b/mealie/services/backups/imports.py
index 18a420e97576..afbfb7f1a274 100644
--- a/mealie/services/backups/imports.py
+++ b/mealie/services/backups/imports.py
@@ -1,8 +1,8 @@
import json
import shutil
import zipfile
+from collections.abc import Callable
from pathlib import Path
-from typing import Callable
from pydantic.main import BaseModel
from sqlalchemy.orm.session import Session
@@ -140,7 +140,7 @@ class ImportDatabase:
if image_dir.exists(): # Migrate from before v0.5.0
for image in image_dir.iterdir():
- item: Recipe = successful_imports.get(image.stem)
+ item: Recipe = successful_imports.get(image.stem) # type: ignore
if item:
dest_dir = item.image_dir
@@ -294,7 +294,7 @@ def import_database(
settings_report = import_session.import_settings() if import_settings else []
group_report = import_session.import_groups() if import_groups else []
user_report = import_session.import_users() if import_users else []
- notification_report = []
+ notification_report: list = []
import_session.clean_up()
diff --git a/mealie/services/backups_v2/alchemy_exporter.py b/mealie/services/backups_v2/alchemy_exporter.py
index d199b54f822d..dcdd9e5dcade 100644
--- a/mealie/services/backups_v2/alchemy_exporter.py
+++ b/mealie/services/backups_v2/alchemy_exporter.py
@@ -6,7 +6,7 @@ from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel
from sqlalchemy import MetaData, create_engine
from sqlalchemy.engine import base
-from sqlalchemy.orm import Session, sessionmaker
+from sqlalchemy.orm import sessionmaker
from mealie.services._base_service import BaseService
@@ -122,8 +122,6 @@ class AlchemyExporter(BaseService):
"""Drops all data from the database"""
self.meta.reflect(bind=self.engine)
with self.session_maker() as session:
- session: Session
-
is_postgres = self.settings.DB_ENGINE == "postgres"
try:
diff --git a/mealie/services/email/email_senders.py b/mealie/services/email/email_senders.py
index 0b7bfe4c027f..ffd6686a7184 100644
--- a/mealie/services/email/email_senders.py
+++ b/mealie/services/email/email_senders.py
@@ -23,7 +23,7 @@ class DefaultEmailSender(ABCEmailSender, BaseService):
mail_from=(self.settings.SMTP_FROM_NAME, self.settings.SMTP_FROM_EMAIL),
)
- smtp_options = {"host": self.settings.SMTP_HOST, "port": self.settings.SMTP_PORT}
+ smtp_options: dict[str, str | bool] = {"host": self.settings.SMTP_HOST, "port": self.settings.SMTP_PORT}
if self.settings.SMTP_TLS:
smtp_options["tls"] = True
if self.settings.SMTP_USER:
diff --git a/mealie/services/exporter/_abc_exporter.py b/mealie/services/exporter/_abc_exporter.py
index f7f8f9fc9ed1..5511581cb134 100644
--- a/mealie/services/exporter/_abc_exporter.py
+++ b/mealie/services/exporter/_abc_exporter.py
@@ -1,8 +1,9 @@
import zipfile
from abc import abstractmethod, abstractproperty
+from collections.abc import Iterator
from dataclasses import dataclass
from pathlib import Path
-from typing import Callable, Iterator, Optional
+from typing import Callable, Optional
from uuid import UUID
from pydantic import BaseModel
@@ -27,7 +28,7 @@ class ExportedItem:
class ABCExporter(BaseService):
- write_dir_to_zip: Callable[[Path, str, Optional[list[str]]], None]
+ write_dir_to_zip: Callable[[Path, str, Optional[set[str]]], None] | None
def __init__(self, db: AllRepositories, group_id: UUID) -> None:
self.logger = get_logger()
@@ -47,8 +48,7 @@ class ABCExporter(BaseService):
def _post_export_hook(self, _: BaseModel) -> None:
pass
- @abstractmethod
- def export(self, zip: zipfile.ZipFile) -> list[ReportEntryCreate]:
+ def export(self, zip: zipfile.ZipFile) -> list[ReportEntryCreate]: # type: ignore
"""
Export takes in a zip file and exports the recipes to it. Note that the zip
file open/close is NOT handled by this method. You must handle it yourself.
@@ -57,7 +57,7 @@ class ABCExporter(BaseService):
zip (zipfile.ZipFile): Zip file destination
Returns:
- list[ReportEntryCreate]: [description] ???!?!
+ list[ReportEntryCreate]:
"""
self.write_dir_to_zip = self.write_dir_to_zip_func(zip)
diff --git a/mealie/services/exporter/recipe_exporter.py b/mealie/services/exporter/recipe_exporter.py
index 781da970b655..7512aabc6adb 100644
--- a/mealie/services/exporter/recipe_exporter.py
+++ b/mealie/services/exporter/recipe_exporter.py
@@ -1,4 +1,4 @@
-from typing import Iterator
+from collections.abc import Iterator
from uuid import UUID
from mealie.repos.all_repositories import AllRepositories
@@ -37,5 +37,5 @@ class RecipeExporter(ABCExporter):
"""Copy recipe directory contents into the zip folder"""
recipe_dir = item.directory
- if recipe_dir.exists():
+ if recipe_dir.exists() and self.write_dir_to_zip:
self.write_dir_to_zip(recipe_dir, f"{self.destination_dir}/{item.slug}", {".json"})
diff --git a/mealie/services/group_services/shopping_lists.py b/mealie/services/group_services/shopping_lists.py
index e2628019b059..2812d39ba783 100644
--- a/mealie/services/group_services/shopping_lists.py
+++ b/mealie/services/group_services/shopping_lists.py
@@ -168,7 +168,7 @@ class ShoppingListService:
found = False
for ref in item.recipe_references:
- remove_qty = 0
+ remove_qty = 0.0
if ref.recipe_id == recipe_id:
self.list_item_refs.delete(ref.id)
@@ -199,4 +199,4 @@ class ShoppingListService:
break
# Save Changes
- return self.shopping_lists.get(shopping_list.id)
+ return self.shopping_lists.get_one(shopping_list.id)
diff --git a/mealie/services/migrations/_migration_base.py b/mealie/services/migrations/_migration_base.py
index b4609bf34e2d..a1c9c5b54c83 100644
--- a/mealie/services/migrations/_migration_base.py
+++ b/mealie/services/migrations/_migration_base.py
@@ -1,5 +1,4 @@
from pathlib import Path
-from typing import Tuple
from uuid import UUID
from pydantic import UUID4
@@ -94,9 +93,10 @@ class BaseMigrator(BaseService):
self._create_report(report_name)
self._migrate()
self._save_all_entries()
- return self.db.group_reports.get(self.report_id)
- def import_recipes_to_database(self, validated_recipes: list[Recipe]) -> list[Tuple[str, UUID4, bool]]:
+ return self.db.group_reports.get_one(self.report_id)
+
+ def import_recipes_to_database(self, validated_recipes: list[Recipe]) -> list[tuple[str, UUID4, bool]]:
"""
Used as a single access point to process a list of Recipe objects into the
database in a predictable way. If an error occurs the session is rolled back
diff --git a/mealie/services/migrations/nextcloud.py b/mealie/services/migrations/nextcloud.py
index 739f5b7b2115..c84504822910 100644
--- a/mealie/services/migrations/nextcloud.py
+++ b/mealie/services/migrations/nextcloud.py
@@ -67,6 +67,6 @@ class NextcloudMigrator(BaseMigrator):
for slug, recipe_id, status in all_statuses:
if status:
- nc_dir: NextcloudDir = nextcloud_dirs[slug]
+ nc_dir = nextcloud_dirs[slug]
if nc_dir.image:
import_image(nc_dir.image, recipe_id)
diff --git a/mealie/services/migrations/utils/database_helpers.py b/mealie/services/migrations/utils/database_helpers.py
index 883688912979..a9b7fd2642e0 100644
--- a/mealie/services/migrations/utils/database_helpers.py
+++ b/mealie/services/migrations/utils/database_helpers.py
@@ -1,3 +1,4 @@
+from collections.abc import Iterable
from typing import TypeVar
from pydantic import UUID4, BaseModel
@@ -14,14 +15,14 @@ T = TypeVar("T", bound=BaseModel)
class DatabaseMigrationHelpers:
- def __init__(self, db: AllRepositories, session: Session, group_id: int, user_id: UUID4) -> None:
+ def __init__(self, db: AllRepositories, session: Session, group_id: UUID4, user_id: UUID4) -> None:
self.group_id = group_id
self.user_id = user_id
self.session = session
self.db = db
def _get_or_set_generic(
- self, accessor: RepositoryGeneric, items: list[str], create_model: T, out_model: T
+ self, accessor: RepositoryGeneric, items: Iterable[str], create_model: type[T], out_model: type[T]
) -> list[T]:
"""
Utility model for getting or setting categories or tags. This will only work for those two cases.
@@ -47,7 +48,7 @@ class DatabaseMigrationHelpers:
items_out.append(item_model.dict())
return items_out
- def get_or_set_category(self, categories: list[str]) -> list[RecipeCategory]:
+ def get_or_set_category(self, categories: Iterable[str]) -> list[RecipeCategory]:
return self._get_or_set_generic(
self.db.categories.by_group(self.group_id),
categories,
@@ -55,7 +56,7 @@ class DatabaseMigrationHelpers:
CategoryOut,
)
- def get_or_set_tags(self, tags: list[str]) -> list[RecipeTag]:
+ def get_or_set_tags(self, tags: Iterable[str]) -> list[RecipeTag]:
return self._get_or_set_generic(
self.db.tags.by_group(self.group_id),
tags,
diff --git a/mealie/services/migrations/utils/migration_alias.py b/mealie/services/migrations/utils/migration_alias.py
index 7103a129f73e..4e36f4027bb8 100644
--- a/mealie/services/migrations/utils/migration_alias.py
+++ b/mealie/services/migrations/utils/migration_alias.py
@@ -1,4 +1,5 @@
-from typing import Callable, Optional
+from collections.abc import Callable
+from typing import Optional
from pydantic import BaseModel
diff --git a/mealie/services/parser_services/_helpers/string_utils.py b/mealie/services/parser_services/_helpers/string_utils.py
index eb3c39a7c9fa..348aeefafa19 100644
--- a/mealie/services/parser_services/_helpers/string_utils.py
+++ b/mealie/services/parser_services/_helpers/string_utils.py
@@ -10,10 +10,10 @@ def move_parens_to_end(ing_str) -> str:
If no parentheses are found, the string is returned unchanged.
"""
if re.match(compiled_match, ing_str):
- match = re.search(compiled_search, ing_str)
- start = match.start()
- end = match.end()
- ing_str = ing_str[:start] + ing_str[end:] + " " + ing_str[start:end]
+ if match := re.search(compiled_search, ing_str):
+ start = match.start()
+ end = match.end()
+ ing_str = ing_str[:start] + ing_str[end:] + " " + ing_str[start:end]
return ing_str
diff --git a/mealie/services/parser_services/brute/process.py b/mealie/services/parser_services/brute/process.py
index 502f862d859d..0ef4f5e3e8a0 100644
--- a/mealie/services/parser_services/brute/process.py
+++ b/mealie/services/parser_services/brute/process.py
@@ -1,6 +1,5 @@
import string
import unicodedata
-from typing import Tuple
from pydantic import BaseModel
@@ -10,7 +9,7 @@ from .._helpers import check_char, move_parens_to_end
class BruteParsedIngredient(BaseModel):
food: str = ""
note: str = ""
- amount: float = ""
+ amount: float = 1.0
unit: str = ""
class Config:
@@ -31,7 +30,7 @@ def parse_fraction(x):
raise ValueError
-def parse_amount(ing_str) -> Tuple[float, str, str]:
+def parse_amount(ing_str) -> tuple[float, str, str]:
def keep_looping(ing_str, end) -> bool:
"""
Checks if:
@@ -48,7 +47,9 @@ def parse_amount(ing_str) -> Tuple[float, str, str]:
if check_char(ing_str[end], ".", ",", "/") and end + 1 < len(ing_str) and ing_str[end + 1] in string.digits:
return True
- amount = 0
+ return False
+
+ amount = 0.0
unit = ""
note = ""
@@ -87,7 +88,7 @@ def parse_amount(ing_str) -> Tuple[float, str, str]:
return amount, unit, note
-def parse_ingredient_with_comma(tokens) -> Tuple[str, str]:
+def parse_ingredient_with_comma(tokens) -> tuple[str, str]:
ingredient = ""
note = ""
start = 0
@@ -105,7 +106,7 @@ def parse_ingredient_with_comma(tokens) -> Tuple[str, str]:
return ingredient, note
-def parse_ingredient(tokens) -> Tuple[str, str]:
+def parse_ingredient(tokens) -> tuple[str, str]:
ingredient = ""
note = ""
if tokens[-1].endswith(")"):
@@ -132,7 +133,7 @@ def parse_ingredient(tokens) -> Tuple[str, str]:
def parse(ing_str) -> BruteParsedIngredient:
- amount = 0
+ amount = 0.0
unit = ""
ingredient = ""
note = ""
diff --git a/mealie/services/parser_services/crfpp/processor.py b/mealie/services/parser_services/crfpp/processor.py
index 3382845cac7b..615257553938 100644
--- a/mealie/services/parser_services/crfpp/processor.py
+++ b/mealie/services/parser_services/crfpp/processor.py
@@ -5,6 +5,8 @@ from pathlib import Path
from pydantic import BaseModel, validator
+from mealie.schema._mealie.types import NoneFloat
+
from . import utils
from .pre_processor import pre_process_string
@@ -14,10 +16,10 @@ MODEL_PATH = CWD / "model.crfmodel"
class CRFConfidence(BaseModel):
average: float = 0.0
- comment: float = None
- name: float = None
- unit: float = None
- qty: float = None
+ comment: NoneFloat = None
+ name: NoneFloat = None
+ unit: NoneFloat = None
+ qty: NoneFloat = None
class CRFIngredient(BaseModel):
diff --git a/mealie/services/parser_services/ingredient_parser.py b/mealie/services/parser_services/ingredient_parser.py
index bee87b3604fe..e7766b413864 100644
--- a/mealie/services/parser_services/ingredient_parser.py
+++ b/mealie/services/parser_services/ingredient_parser.py
@@ -99,7 +99,7 @@ class NLPParser(ABCIngredientParser):
return [self._crf_to_ingredient(crf_model) for crf_model in crf_models]
def parse_one(self, ingredient: str) -> ParsedIngredient:
- items = self.parse_one([ingredient])
+ items = self.parse([ingredient])
return items[0]
diff --git a/mealie/services/recipe/recipe_data_service.py b/mealie/services/recipe/recipe_data_service.py
index 1fb53a926921..912454848e43 100644
--- a/mealie/services/recipe/recipe_data_service.py
+++ b/mealie/services/recipe/recipe_data_service.py
@@ -38,7 +38,7 @@ class RecipeDataService(BaseService):
except Exception as e:
self.logger.exception(f"Failed to delete recipe data: {e}")
- def write_image(self, file_data: bytes, extension: str) -> Path:
+ def write_image(self, file_data: bytes | Path, extension: str) -> Path:
extension = extension.replace(".", "")
image_path = self.dir_image.joinpath(f"original.{extension}")
image_path.unlink(missing_ok=True)
@@ -91,8 +91,8 @@ class RecipeDataService(BaseService):
if ext not in img.IMAGE_EXTENSIONS:
ext = "jpg" # Guess the extension
- filename = str(self.recipe_id) + "." + ext
- filename = Recipe.directory_from_id(self.recipe_id).joinpath("images", filename)
+ file_name = f"{str(self.recipe_id)}.{ext}"
+ file_path = Recipe.directory_from_id(self.recipe_id).joinpath("images", file_name)
try:
r = requests.get(image_url, stream=True, headers={"User-Agent": _FIREFOX_UA})
@@ -102,7 +102,7 @@ class RecipeDataService(BaseService):
if r.status_code == 200:
r.raw.decode_content = True
- self.logger.info(f"File Name Suffix {filename.suffix}")
- self.write_image(r.raw, filename.suffix)
+ self.logger.info(f"File Name Suffix {file_path.suffix}")
+ self.write_image(r.raw, file_path.suffix)
- filename.unlink(missing_ok=True)
+ file_path.unlink(missing_ok=True)
diff --git a/mealie/services/recipe/recipe_service.py b/mealie/services/recipe/recipe_service.py
index 2186829c3122..2578c86bfa31 100644
--- a/mealie/services/recipe/recipe_service.py
+++ b/mealie/services/recipe/recipe_service.py
@@ -69,7 +69,6 @@ class RecipeService(BaseService):
all_asset_files = [x.file_name for x in recipe.assets]
for file in recipe.asset_dir.iterdir():
- file: Path
if file.is_dir():
continue
if file.name not in all_asset_files:
@@ -102,13 +101,13 @@ class RecipeService(BaseService):
def create_one(self, create_data: Union[Recipe, CreateRecipe]) -> Recipe:
- create_data: Recipe = self._recipe_creation_factory(
+ data: Recipe = self._recipe_creation_factory(
self.user,
name=create_data.name,
additional_attrs=create_data.dict(),
)
- create_data.settings = RecipeSettings(
+ data.settings = RecipeSettings(
public=self.group.preferences.recipe_public,
show_nutrition=self.group.preferences.recipe_show_nutrition,
show_assets=self.group.preferences.recipe_show_assets,
@@ -117,7 +116,7 @@ class RecipeService(BaseService):
disable_amount=self.group.preferences.recipe_disable_amount,
)
- return self.repos.recipes.create(create_data)
+ return self.repos.recipes.create(data)
def create_from_zip(self, archive: UploadFile, temp_path: Path) -> Recipe:
"""
diff --git a/mealie/services/recipe/template_service.py b/mealie/services/recipe/template_service.py
index 8fa1000f2224..36515eb884dd 100644
--- a/mealie/services/recipe/template_service.py
+++ b/mealie/services/recipe/template_service.py
@@ -27,7 +27,7 @@ class TemplateService(BaseService):
super().__init__()
@property
- def templates(self) -> list:
+ def templates(self) -> dict[str, list[str]]:
"""
Returns a list of all templates available to render.
"""
@@ -78,6 +78,8 @@ class TemplateService(BaseService):
if t_type == TemplateType.zip:
return self._render_zip(recipe)
+ raise ValueError(f"Template Type '{t_type}' not found.")
+
def _render_json(self, recipe: Recipe) -> Path:
"""
Renders a JSON file in a temporary directory and returns
@@ -98,18 +100,18 @@ class TemplateService(BaseService):
"""
self.__check_temp(self._render_jinja2)
- j2_template: Path = self.directories.TEMPLATE_DIR / j2_template
+ j2_path: Path = self.directories.TEMPLATE_DIR / j2_template
- if not j2_template.is_file():
- raise FileNotFoundError(f"Template '{j2_template}' not found.")
+ if not j2_path.is_file():
+ raise FileNotFoundError(f"Template '{j2_path}' not found.")
- with open(j2_template, "r") as f:
+ with open(j2_path, "r") as f:
template_text = f.read()
template = Template(template_text)
rendered_text = template.render(recipe=recipe.dict(by_alias=True))
- save_name = f"{recipe.slug}{j2_template.suffix}"
+ save_name = f"{recipe.slug}{j2_path.suffix}"
save_path = self.temp.joinpath(save_name)
diff --git a/mealie/services/scheduler/scheduled_func.py b/mealie/services/scheduler/scheduled_func.py
index 0e2f0115e2d8..d7159bab6363 100644
--- a/mealie/services/scheduler/scheduled_func.py
+++ b/mealie/services/scheduler/scheduled_func.py
@@ -1,5 +1,5 @@
+from collections.abc import Callable
from dataclasses import dataclass
-from typing import Callable, Tuple
from pydantic import BaseModel
@@ -17,7 +17,7 @@ class Cron:
@dataclass
class ScheduledFunc(BaseModel):
- id: Tuple[str, int]
+ id: tuple[str, int]
name: str
hour: int
minutes: int
diff --git a/mealie/services/scheduler/scheduler_registry.py b/mealie/services/scheduler/scheduler_registry.py
index a0ca65ee0dfe..f75e6012e73d 100644
--- a/mealie/services/scheduler/scheduler_registry.py
+++ b/mealie/services/scheduler/scheduler_registry.py
@@ -1,4 +1,4 @@
-from typing import Callable, Iterable
+from collections.abc import Callable, Iterable
from mealie.core import root_logger
diff --git a/mealie/services/scheduler/scheduler_service.py b/mealie/services/scheduler/scheduler_service.py
index 29b3c0e85892..75c92710323c 100644
--- a/mealie/services/scheduler/scheduler_service.py
+++ b/mealie/services/scheduler/scheduler_service.py
@@ -49,30 +49,26 @@ class SchedulerService:
@staticmethod
def add_cron_job(job_func: ScheduledFunc):
- SchedulerService.scheduler.add_job(
+ SchedulerService.scheduler.add_job( # type: ignore
job_func.callback,
trigger="cron",
name=job_func.id,
hour=job_func.hour,
minute=job_func.minutes,
- max_instances=job_func.max_instances,
+ max_instances=job_func.max_instances, # type: ignore
replace_existing=job_func.replace_existing,
args=job_func.args,
)
- # SchedulerService._job_store[job_func.id] = job_func
-
@staticmethod
def update_cron_job(job_func: ScheduledFunc):
- SchedulerService.scheduler.reschedule_job(
+ SchedulerService.scheduler.reschedule_job( # type: ignore
job_func.id,
trigger="cron",
hour=job_func.hour,
minute=job_func.minutes,
)
- # SchedulerService._job_store[job_func.id] = job_func
-
def _scheduled_task_wrapper(callable):
try:
diff --git a/mealie/services/scheduler/tasks/purge_group_exports.py b/mealie/services/scheduler/tasks/purge_group_exports.py
index 8ef4185fd6bb..1c34a126c417 100644
--- a/mealie/services/scheduler/tasks/purge_group_exports.py
+++ b/mealie/services/scheduler/tasks/purge_group_exports.py
@@ -39,7 +39,8 @@ def purge_excess_files() -> None:
limit = datetime.datetime.now() - datetime.timedelta(minutes=ONE_DAY_AS_MINUTES * 2)
for file in directories.GROUPS_DIR.glob("**/export/*.zip"):
- if file.stat().st_mtime < limit:
+ # TODO: fix comparison types
+ if file.stat().st_mtime < limit: # type: ignore
file.unlink()
logger.info(f"excess group file removed '{file}'")
diff --git a/mealie/services/scheduler/tasks/webhooks.py b/mealie/services/scheduler/tasks/webhooks.py
index e74459c1ab78..d9aff970fccd 100644
--- a/mealie/services/scheduler/tasks/webhooks.py
+++ b/mealie/services/scheduler/tasks/webhooks.py
@@ -28,7 +28,7 @@ def post_webhooks(webhook_id: int, session: Session = None):
if not todays_recipe:
return
- payload = json.loads([x.json(by_alias=True) for x in todays_recipe])
+ payload = json.loads([x.json(by_alias=True) for x in todays_recipe]) # type: ignore
response = requests.post(webhook.url, json=payload)
if response.status_code != 200:
diff --git a/mealie/services/scraper/cleaner.py b/mealie/services/scraper/cleaner.py
index 948d87d1d953..81a08e8a1ac9 100644
--- a/mealie/services/scraper/cleaner.py
+++ b/mealie/services/scraper/cleaner.py
@@ -2,7 +2,7 @@ import html
import json
import re
from datetime import datetime, timedelta
-from typing import List, Optional
+from typing import Optional
from slugify import slugify
@@ -33,7 +33,7 @@ def clean(recipe_data: dict, url=None) -> dict:
recipe_data["recipeIngredient"] = ingredient(recipe_data.get("recipeIngredient"))
recipe_data["recipeInstructions"] = instructions(recipe_data.get("recipeInstructions"))
recipe_data["image"] = image(recipe_data.get("image"))
- recipe_data["slug"] = slugify(recipe_data.get("name"))
+ recipe_data["slug"] = slugify(recipe_data.get("name")) # type: ignore
recipe_data["orgURL"] = url
return recipe_data
@@ -127,7 +127,7 @@ def image(image=None) -> str:
raise Exception(f"Unrecognised image URL format: {image}")
-def instructions(instructions) -> List[dict]:
+def instructions(instructions) -> list[dict]:
try:
instructions = json.loads(instructions)
except Exception:
@@ -162,7 +162,8 @@ def instructions(instructions) -> List[dict]:
sectionSteps = []
for step in instructions:
if step["@type"] == "HowToSection":
- [sectionSteps.append(item) for item in step["itemListElement"]]
+ for sectionStep in step["itemListElement"]:
+ sectionSteps.append(sectionStep)
if len(sectionSteps) > 0:
return [{"text": _instruction(step["text"])} for step in sectionSteps if step["@type"] == "HowToStep"]
@@ -183,6 +184,8 @@ def instructions(instructions) -> List[dict]:
else:
raise Exception(f"Unrecognised instruction format: {instructions}")
+ return []
+
def _instruction(line) -> str:
if isinstance(line, dict):
@@ -199,7 +202,7 @@ def _instruction(line) -> str:
return clean_line
-def ingredient(ingredients: list) -> str:
+def ingredient(ingredients: list | None) -> list[str]:
if ingredients:
return [clean_string(ing) for ing in ingredients]
else:
diff --git a/mealie/services/scraper/recipe_scraper.py b/mealie/services/scraper/recipe_scraper.py
index e55f9e355a75..e1c116952615 100644
--- a/mealie/services/scraper/recipe_scraper.py
+++ b/mealie/services/scraper/recipe_scraper.py
@@ -1,5 +1,3 @@
-from typing import Type
-
from mealie.schema.recipe.recipe import Recipe
from .scraper_strategies import ABCScraperStrategy, RecipeScraperOpenGraph, RecipeScraperPackage
@@ -11,9 +9,9 @@ class RecipeScraper:
"""
# List of recipe scrapers. Note that order matters
- scrapers: list[Type[ABCScraperStrategy]]
+ scrapers: list[type[ABCScraperStrategy]]
- def __init__(self, scrapers: list[Type[ABCScraperStrategy]] = None) -> None:
+ def __init__(self, scrapers: list[type[ABCScraperStrategy]] = None) -> None:
if scrapers is None:
scrapers = [
RecipeScraperPackage,
@@ -27,8 +25,8 @@ class RecipeScraper:
Scrapes a recipe from the web.
"""
- for scraper in self.scrapers:
- scraper = scraper(url)
+ for scraper_type in self.scrapers:
+ scraper = scraper_type(url)
recipe = scraper.parse()
if recipe is not None:
diff --git a/mealie/services/scraper/scraper_strategies.py b/mealie/services/scraper/scraper_strategies.py
index 3c609910fcc6..ad34b8009a38 100644
--- a/mealie/services/scraper/scraper_strategies.py
+++ b/mealie/services/scraper/scraper_strategies.py
@@ -1,5 +1,5 @@
from abc import ABC, abstractmethod
-from typing import Any, Callable, Tuple
+from typing import Any, Callable
import extruct
import requests
@@ -26,7 +26,7 @@ class ABCScraperStrategy(ABC):
self.url = url
@abstractmethod
- def parse(self, recipe_url: str) -> Recipe | None:
+ def parse(self) -> Recipe | None:
"""Parse a recipe from a web URL.
Args:
@@ -40,7 +40,7 @@ class ABCScraperStrategy(ABC):
class RecipeScraperPackage(ABCScraperStrategy):
def clean_scraper(self, scraped_data: SchemaScraperFactory.SchemaScraper, url: str) -> Recipe:
- def try_get_default(func_call: Callable, get_attr: str, default: Any, clean_func=None):
+ def try_get_default(func_call: Callable | None, get_attr: str, default: Any, clean_func=None):
value = default
try:
value = func_call()
@@ -143,7 +143,7 @@ class RecipeScraperOpenGraph(ABCScraperStrategy):
def get_html(self) -> str:
return requests.get(self.url).text
- def get_recipe_fields(self, html) -> dict:
+ def get_recipe_fields(self, html) -> dict | None:
"""
Get the recipe fields from the Open Graph data.
"""
@@ -151,7 +151,7 @@ class RecipeScraperOpenGraph(ABCScraperStrategy):
def og_field(properties: dict, field_name: str) -> str:
return next((val for name, val in properties if name == field_name), None)
- def og_fields(properties: list[Tuple[str, str]], field_name: str) -> list[str]:
+ def og_fields(properties: list[tuple[str, str]], field_name: str) -> list[str]:
return list({val for name, val in properties if name == field_name})
base_url = get_base_url(html, self.url)
@@ -159,7 +159,7 @@ class RecipeScraperOpenGraph(ABCScraperStrategy):
try:
properties = data["opengraph"][0]["properties"]
except Exception:
- return
+ return None
return {
"name": og_field(properties, "og:title"),
diff --git a/mealie/services/server_tasks/background_executory.py b/mealie/services/server_tasks/background_executory.py
index 51e10763d56b..62c338695318 100644
--- a/mealie/services/server_tasks/background_executory.py
+++ b/mealie/services/server_tasks/background_executory.py
@@ -1,6 +1,7 @@
+from collections.abc import Callable
from random import getrandbits
from time import sleep
-from typing import Any, Callable
+from typing import Any
from fastapi import BackgroundTasks
from pydantic import UUID4
diff --git a/mealie/services/user_services/password_reset_service.py b/mealie/services/user_services/password_reset_service.py
index f8b7187d4256..108d74d8d401 100644
--- a/mealie/services/user_services/password_reset_service.py
+++ b/mealie/services/user_services/password_reset_service.py
@@ -16,13 +16,13 @@ class PasswordResetService(BaseService):
self.db = get_repositories(session)
super().__init__()
- def generate_reset_token(self, email: str) -> SavePasswordResetToken:
+ def generate_reset_token(self, email: str) -> SavePasswordResetToken | None:
user = self.db.users.get_one(email, "email")
if user is None:
logger.error(f"failed to create password reset for {email=}: user doesn't exists")
# Do not raise exception here as we don't want to confirm to the client that the Email doens't exists
- return
+ return None
# Create Reset Token
token = url_safe_token()
diff --git a/mealie/services/user_services/registration_service.py b/mealie/services/user_services/registration_service.py
index 1c4fd634d009..12355db2dee6 100644
--- a/mealie/services/user_services/registration_service.py
+++ b/mealie/services/user_services/registration_service.py
@@ -66,7 +66,7 @@ class RegistrationService:
token_entry = self.repos.group_invite_tokens.get_one(registration.group_token)
if not token_entry:
raise HTTPException(status.HTTP_400_BAD_REQUEST, {"message": "Invalid group token"})
- group = self.repos.groups.get(token_entry.group_id)
+ group = self.repos.groups.get_one(token_entry.group_id)
else:
raise HTTPException(status.HTTP_400_BAD_REQUEST, {"message": "Missing group"})
diff --git a/poetry.lock b/poetry.lock
index fb1da4824187..088f1d9fba9c 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -214,6 +214,14 @@ python-versions = "*"
[package.dependencies]
pycparser = "*"
+[[package]]
+name = "cfgv"
+version = "3.3.1"
+description = "Validate configuration and produce human readable error messages."
+category = "dev"
+optional = false
+python-versions = ">=3.6.1"
+
[[package]]
name = "chardet"
version = "4.0.0"
@@ -303,6 +311,14 @@ python-versions = ">=3.6"
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)", "mock", "lxml", "cssselect", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources"]
+[[package]]
+name = "distlib"
+version = "0.3.4"
+description = "Distribution utilities"
+category = "dev"
+optional = false
+python-versions = "*"
+
[[package]]
name = "ecdsa"
version = "0.17.0"
@@ -386,6 +402,18 @@ python-versions = ">=3.6"
pydantic = "*"
pyhumps = "*"
+[[package]]
+name = "filelock"
+version = "3.6.0"
+description = "A platform independent file lock."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"]
+testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"]
+
[[package]]
name = "flake8"
version = "4.0.1"
@@ -499,6 +527,17 @@ python-versions = "*"
[package.extras]
test = ["Cython (==0.29.22)"]
+[[package]]
+name = "identify"
+version = "2.4.11"
+description = "File identification library for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+license = ["ukkonen"]
+
[[package]]
name = "idna"
version = "3.3"
@@ -710,6 +749,24 @@ category = "dev"
optional = false
python-versions = ">=3.6"
+[[package]]
+name = "mypy"
+version = "0.940"
+description = "Optional static typing for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+mypy-extensions = ">=0.4.3"
+tomli = ">=1.1.0"
+typing-extensions = ">=3.10"
+
+[package.extras]
+dmypy = ["psutil (>=4.0)"]
+python2 = ["typed-ast (>=1.4.0,<2)"]
+reports = ["lxml"]
+
[[package]]
name = "mypy-extensions"
version = "0.4.3"
@@ -718,6 +775,14 @@ category = "dev"
optional = false
python-versions = "*"
+[[package]]
+name = "nodeenv"
+version = "1.6.0"
+description = "Node.js virtual environment builder"
+category = "dev"
+optional = false
+python-versions = "*"
+
[[package]]
name = "oauthlib"
version = "3.1.1"
@@ -807,6 +872,22 @@ python-versions = ">=3.6"
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
+[[package]]
+name = "pre-commit"
+version = "2.17.0"
+description = "A framework for managing and maintaining multi-language pre-commit hooks."
+category = "dev"
+optional = false
+python-versions = ">=3.6.1"
+
+[package.dependencies]
+cfgv = ">=2.0.0"
+identify = ">=1.0.0"
+nodeenv = ">=0.11.1"
+pyyaml = ">=5.1"
+toml = "*"
+virtualenv = ">=20.0.8"
+
[[package]]
name = "premailer"
version = "3.10.0"
@@ -1336,6 +1417,41 @@ category = "dev"
optional = false
python-versions = ">=3.6"
+[[package]]
+name = "types-python-slugify"
+version = "5.0.3"
+description = "Typing stubs for python-slugify"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "types-pyyaml"
+version = "6.0.4"
+description = "Typing stubs for PyYAML"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "types-requests"
+version = "2.27.12"
+description = "Typing stubs for requests"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+types-urllib3 = "<1.27"
+
+[[package]]
+name = "types-urllib3"
+version = "1.26.11"
+description = "Typing stubs for urllib3"
+category = "dev"
+optional = false
+python-versions = "*"
+
[[package]]
name = "typing-extensions"
version = "4.0.1"
@@ -1416,6 +1532,24 @@ dev = ["Cython (>=0.29.24,<0.30.0)", "pytest (>=3.6.0)", "Sphinx (>=4.1.2,<4.2.0
docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)"]
test = ["aiohttp", "flake8 (>=3.9.2,<3.10.0)", "psutil", "pycodestyle (>=2.7.0,<2.8.0)", "pyOpenSSL (>=19.0.0,<19.1.0)", "mypy (>=0.800)"]
+[[package]]
+name = "virtualenv"
+version = "20.13.3"
+description = "Virtual Python Environment builder"
+category = "dev"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
+
+[package.dependencies]
+distlib = ">=0.3.1,<1"
+filelock = ">=3.2,<4"
+platformdirs = ">=2,<3"
+six = ">=1.9.0,<2"
+
+[package.extras]
+docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"]
+testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"]
+
[[package]]
name = "w3lib"
version = "1.22.0"
@@ -1488,7 +1622,7 @@ pgsql = ["psycopg2-binary"]
[metadata]
lock-version = "1.1"
python-versions = "^3.10"
-content-hash = "00e37f7569d999689984b41bb0085f86e0e902eb1a7cae32d408b079db0ae8d8"
+content-hash = "4fba071019a62f5d75e7c9a297a7815b2fed6486bb3616b5029a6fb08001761f"
[metadata.files]
aiofiles = [
@@ -1608,6 +1742,10 @@ cffi = [
{file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"},
{file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"},
]
+cfgv = [
+ {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"},
+ {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
+]
chardet = [
{file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"},
{file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"},
@@ -1694,6 +1832,10 @@ cssutils = [
{file = "cssutils-2.3.0-py3-none-any.whl", hash = "sha256:0cf1f6086b020dee18048ff3999339499f725934017ef9ae2cd5bb77f9ab5f46"},
{file = "cssutils-2.3.0.tar.gz", hash = "sha256:b2d3b16047caae82e5c590036935bafa1b621cf45c2f38885af4be4838f0fd00"},
]
+distlib = [
+ {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"},
+ {file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"},
+]
ecdsa = [
{file = "ecdsa-0.17.0-py2.py3-none-any.whl", hash = "sha256:5cf31d5b33743abe0dfc28999036c849a69d548f994b535e527ee3cb7f3ef676"},
{file = "ecdsa-0.17.0.tar.gz", hash = "sha256:b9f500bb439e4153d0330610f5d26baaf18d17b8ced1bc54410d189385ea68aa"},
@@ -1713,6 +1855,10 @@ fastapi = [
fastapi-camelcase = [
{file = "fastapi_camelcase-1.0.5.tar.gz", hash = "sha256:2cee005fb1b75649491b9f7cfccc640b12f028eb88084565f7d8cf415192026a"},
]
+filelock = [
+ {file = "filelock-3.6.0-py3-none-any.whl", hash = "sha256:f8314284bfffbdcfa0ff3d7992b023d4c628ced6feb957351d4c48d059f56bc0"},
+ {file = "filelock-3.6.0.tar.gz", hash = "sha256:9cd540a9352e432c7246a48fe4e8712b10acb1df2ad1f30e8c070b82ae1fed85"},
+]
flake8 = [
{file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"},
{file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"},
@@ -1810,6 +1956,10 @@ httptools = [
{file = "httptools-0.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:9abd788465aa46a0f288bd3a99e53edd184177d6379e2098fd6097bb359ad9d6"},
{file = "httptools-0.1.2.tar.gz", hash = "sha256:07659649fe6b3948b6490825f89abe5eb1cec79ebfaaa0b4bf30f3f33f3c2ba8"},
]
+identify = [
+ {file = "identify-2.4.11-py2.py3-none-any.whl", hash = "sha256:fd906823ed1db23c7a48f9b176a1d71cb8abede1e21ebe614bac7bdd688d9213"},
+ {file = "identify-2.4.11.tar.gz", hash = "sha256:2986942d3974c8f2e5019a190523b0b0e2a07cb8e89bf236727fb4b26f27f8fd"},
+]
idna = [
{file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
{file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
@@ -2040,10 +2190,39 @@ mkdocs-material-extensions = [
{file = "mkdocs-material-extensions-1.0.3.tar.gz", hash = "sha256:bfd24dfdef7b41c312ede42648f9eb83476ea168ec163b613f9abd12bbfddba2"},
{file = "mkdocs_material_extensions-1.0.3-py3-none-any.whl", hash = "sha256:a82b70e533ce060b2a5d9eb2bc2e1be201cf61f901f93704b4acf6e3d5983a44"},
]
+mypy = [
+ {file = "mypy-0.940-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0fdc9191a49c77ab5fa0439915d405e80a1118b163ab03cd2a530f346b12566a"},
+ {file = "mypy-0.940-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1903c92ff8642d521b4627e51a67e49f5be5aedb1fb03465b3aae4c3338ec491"},
+ {file = "mypy-0.940-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:471af97c35a32061883b0f8a3305ac17947fd42ce962ca9e2b0639eb9141492f"},
+ {file = "mypy-0.940-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:13677cb8b050f03b5bb2e8bf7b2668cd918b001d56c2435082bbfc9d5f730f42"},
+ {file = "mypy-0.940-cp310-cp310-win_amd64.whl", hash = "sha256:2efd76893fb8327eca7e942e21b373e6f3c5c083ff860fb1e82ddd0462d662bd"},
+ {file = "mypy-0.940-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8fe1bfab792e4300f80013edaf9949b34e4c056a7b2531b5ef3a0fb9d598ae2"},
+ {file = "mypy-0.940-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2dba92f58610d116f68ec1221fb2de2a346d081d17b24a784624389b17a4b3f9"},
+ {file = "mypy-0.940-cp36-cp36m-win_amd64.whl", hash = "sha256:712affcc456de637e774448c73e21c84dfa5a70bcda34e9b0be4fb898a9e8e07"},
+ {file = "mypy-0.940-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8aaf18d0f8bc3ffba56d32a85971dfbd371a5be5036da41ac16aefec440eff17"},
+ {file = "mypy-0.940-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:51be997c1922e2b7be514a5215d1e1799a40832c0a0dee325ba8794f2c48818f"},
+ {file = "mypy-0.940-cp37-cp37m-win_amd64.whl", hash = "sha256:628f5513268ebbc563750af672ccba5eef7f92d2d90154233edd498dfb98ca4e"},
+ {file = "mypy-0.940-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:68038d514ae59d5b2f326be502a359160158d886bd153fc2489dbf7a03c44c96"},
+ {file = "mypy-0.940-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b2fa5f2d597478ccfe1f274f8da2f50ea1e63da5a7ae2342c5b3b2f3e57ec340"},
+ {file = "mypy-0.940-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b1a116c451b41e35afc09618f454b5c2704ba7a4e36f9ff65014fef26bb6075b"},
+ {file = "mypy-0.940-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f66f2309cdbb07e95e60e83fb4a8272095bd4ea6ee58bf9a70d5fb304ec3e3f"},
+ {file = "mypy-0.940-cp38-cp38-win_amd64.whl", hash = "sha256:3ac14949677ae9cb1adc498c423b194ad4d25b13322f6fe889fb72b664c79121"},
+ {file = "mypy-0.940-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6eab2bcc2b9489b7df87d7c20743b66d13254ad4d6430e1dfe1a655d51f0933d"},
+ {file = "mypy-0.940-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0b52778a018559a256c819ee31b2e21e10b31ddca8705624317253d6d08dbc35"},
+ {file = "mypy-0.940-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d9d7647505bf427bc7931e8baf6cacf9be97e78a397724511f20ddec2a850752"},
+ {file = "mypy-0.940-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a0e5657ccaedeb5fdfda59918cc98fc6d8a8e83041bc0cec347a2ab6915f9998"},
+ {file = "mypy-0.940-cp39-cp39-win_amd64.whl", hash = "sha256:83f66190e3c32603217105913fbfe0a3ef154ab6bbc7ef2c989f5b2957b55840"},
+ {file = "mypy-0.940-py3-none-any.whl", hash = "sha256:a168da06eccf51875fdff5f305a47f021f23f300e2b89768abdac24538b1f8ec"},
+ {file = "mypy-0.940.tar.gz", hash = "sha256:71bec3d2782d0b1fecef7b1c436253544d81c1c0e9ca58190aed9befd8f081c5"},
+]
mypy-extensions = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
]
+nodeenv = [
+ {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"},
+ {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"},
+]
oauthlib = [
{file = "oauthlib-3.1.1-py2.py3-none-any.whl", hash = "sha256:42bf6354c2ed8c6acb54d971fce6f88193d97297e18602a3a886603f9d7730cc"},
{file = "oauthlib-3.1.1.tar.gz", hash = "sha256:8f0215fcc533dd8dd1bee6f4c412d4f0cd7297307d43ac61666389e3bc3198a3"},
@@ -2115,6 +2294,10 @@ pluggy = [
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
]
+pre-commit = [
+ {file = "pre_commit-2.17.0-py2.py3-none-any.whl", hash = "sha256:725fa7459782d7bec5ead072810e47351de01709be838c2ce1726b9591dad616"},
+ {file = "pre_commit-2.17.0.tar.gz", hash = "sha256:c1a8040ff15ad3d648c70cc3e55b93e4d2d5b687320955505587fd79bbaed06a"},
+]
premailer = [
{file = "premailer-3.10.0-py2.py3-none-any.whl", hash = "sha256:021b8196364d7df96d04f9ade51b794d0b77bcc19e998321c515633a2273be1a"},
{file = "premailer-3.10.0.tar.gz", hash = "sha256:d1875a8411f5dc92b53ef9f193db6c0f879dc378d618e0ad292723e388bfe4c2"},
@@ -2460,6 +2643,22 @@ tomli = [
{file = "tomli-1.2.3-py3-none-any.whl", hash = "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"},
{file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"},
]
+types-python-slugify = [
+ {file = "types-python-slugify-5.0.3.tar.gz", hash = "sha256:76169f4a6d40896fea76fb45a25c50ac7ba2ca03eee759ecac322a05fe4dd21c"},
+ {file = "types_python_slugify-5.0.3-py3-none-any.whl", hash = "sha256:a5761d3c55e949f8ace0694eb5be81210087084bb78df495de14ee5eaad6ac54"},
+]
+types-pyyaml = [
+ {file = "types-PyYAML-6.0.4.tar.gz", hash = "sha256:6252f62d785e730e454dfa0c9f0fb99d8dae254c5c3c686903cf878ea27c04b7"},
+ {file = "types_PyYAML-6.0.4-py3-none-any.whl", hash = "sha256:693b01c713464a6851f36ff41077f8adbc6e355eda929addfb4a97208aea9b4b"},
+]
+types-requests = [
+ {file = "types-requests-2.27.12.tar.gz", hash = "sha256:fd1382fa2e28eac848faedb0332840204f06f0cb517008e3c7b8282ca53e56d2"},
+ {file = "types_requests-2.27.12-py3-none-any.whl", hash = "sha256:120c949953b618e334bbe78de38e65aa261e1f48df021a05f0be833a848e4ba7"},
+]
+types-urllib3 = [
+ {file = "types-urllib3-1.26.11.tar.gz", hash = "sha256:24d64e441168851eb05f1d022de18ae31558f5649c8f1117e384c2e85e31315b"},
+ {file = "types_urllib3-1.26.11-py3-none-any.whl", hash = "sha256:bd0abc01e9fb963e4fddd561a56d21cc371b988d1245662195c90379077139cd"},
+]
typing-extensions = [
{file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"},
{file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"},
@@ -2498,6 +2697,10 @@ uvloop = [
{file = "uvloop-0.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e5f2e2ff51aefe6c19ee98af12b4ae61f5be456cd24396953244a30880ad861"},
{file = "uvloop-0.16.0.tar.gz", hash = "sha256:f74bc20c7b67d1c27c72601c78cf95be99d5c2cdd4514502b4f3eb0933ff1228"},
]
+virtualenv = [
+ {file = "virtualenv-20.13.3-py2.py3-none-any.whl", hash = "sha256:dd448d1ded9f14d1a4bfa6bfc0c5b96ae3be3f2d6c6c159b23ddcfd701baa021"},
+ {file = "virtualenv-20.13.3.tar.gz", hash = "sha256:e9dd1a1359d70137559034c0f5433b34caf504af2dc756367be86a5a32967134"},
+]
w3lib = [
{file = "w3lib-1.22.0-py2.py3-none-any.whl", hash = "sha256:0161d55537063e00d95a241663ede3395c4c6d7b777972ba2fd58bbab2001e53"},
{file = "w3lib-1.22.0.tar.gz", hash = "sha256:0ad6d0203157d61149fd45aaed2e24f53902989c32fc1dccc2e2bfba371560df"},
diff --git a/pyproject.toml b/pyproject.toml
index 6cc406c4d45d..2f87e128e73b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -55,6 +55,12 @@ isort = "^5.9.3"
flake8-print = "^4.0.0"
black = "^21.12b0"
coveragepy-lcov = "^0.1.1"
+mypy = "^0.940"
+types-python-slugify = "^5.0.3"
+types-PyYAML = "^6.0.4"
+types-requests = "^2.27.12"
+types-urllib3 = "^1.26.11"
+pre-commit = "^2.17.0"
[build-system]
requires = ["poetry-core>=1.0.0"]
@@ -76,7 +82,7 @@ sort_by_size = true
profile = "black"
line_length = 120
multi_line_output = 3
-
+
[tool.pytest.ini_options]
minversion = "6.0"
addopts = "-ra -q --cov=mealie"
@@ -91,4 +97,10 @@ testpaths = [
skip_empty = true
[tool.poetry.extras]
-pgsql = ["psycopg2-binary"]
\ No newline at end of file
+pgsql = ["psycopg2-binary"]
+
+[tool.mypy]
+python_version = "3.10"
+ignore_missing_imports = true
+follow_imports = "skip"
+strict_optional = false # TODO: Fix none type checks - temporary stop-gap to implement mypy
diff --git a/tests/fixtures/fixture_users.py b/tests/fixtures/fixture_users.py
index 1277ebfe9a5c..4f1ae3216083 100644
--- a/tests/fixtures/fixture_users.py
+++ b/tests/fixtures/fixture_users.py
@@ -1,5 +1,4 @@
import json
-from typing import Tuple
import requests
from pytest import fixture
@@ -104,7 +103,7 @@ def unique_user(api_client: TestClient, api_routes: utils.AppRoutes):
@fixture(scope="module")
-def user_tuple(admin_token, api_client: requests, api_routes: utils.AppRoutes) -> Tuple[utils.TestUser]:
+def user_tuple(admin_token, api_client: requests, api_routes: utils.AppRoutes) -> tuple[utils.TestUser]:
group_name = utils.random_string()
# Create the user
create_data_1 = {
diff --git a/tests/integration_tests/user_recipe_tests/test_recipe_crud.py b/tests/integration_tests/user_recipe_tests/test_recipe_crud.py
index fef2818c4ab5..576846f89b28 100644
--- a/tests/integration_tests/user_recipe_tests/test_recipe_crud.py
+++ b/tests/integration_tests/user_recipe_tests/test_recipe_crud.py
@@ -1,6 +1,6 @@
import json
from pathlib import Path
-from typing import Optional, Tuple, Union
+from typing import Optional, Union
import pytest
from bs4 import BeautifulSoup
@@ -31,7 +31,7 @@ def get_init(html_path: Path):
self,
url,
proxies: Optional[str] = None,
- timeout: Optional[Union[float, Tuple, None]] = None,
+ timeout: Optional[Union[float, tuple, None]] = None,
wild_mode: Optional[bool] = False,
**_,
):
diff --git a/tests/multitenant_tests/case_abc.py b/tests/multitenant_tests/case_abc.py
index a4f235599072..e27ec6213952 100644
--- a/tests/multitenant_tests/case_abc.py
+++ b/tests/multitenant_tests/case_abc.py
@@ -1,5 +1,4 @@
from abc import ABC, abstractmethod
-from typing import Tuple
from fastapi import Response
from fastapi.testclient import TestClient
@@ -17,7 +16,7 @@ class ABCMultiTenantTestCase(ABC):
def seed_action(repos: AllRepositories, group_id: str) -> set[int] | set[str]:
...
- def seed_multi(self, group1_id: str, group2_id: str) -> Tuple[set[int], set[int]]:
+ def seed_multi(self, group1_id: str, group2_id: str) -> tuple[set[int], set[int]]:
pass
@abstractmethod
diff --git a/tests/multitenant_tests/case_categories.py b/tests/multitenant_tests/case_categories.py
index 769c6ed9082f..f84b664deaf8 100644
--- a/tests/multitenant_tests/case_categories.py
+++ b/tests/multitenant_tests/case_categories.py
@@ -1,5 +1,3 @@
-from typing import Tuple
-
from requests import Response
from mealie.schema.recipe.recipe import RecipeCategory
@@ -27,7 +25,7 @@ class CategoryTestCase(ABCMultiTenantTestCase):
return category_ids
- def seed_multi(self, group1_id: str, group2_id: str) -> Tuple[set[str], set[str]]:
+ def seed_multi(self, group1_id: str, group2_id: str) -> tuple[set[str], set[str]]:
g1_item_ids = set()
g2_item_ids = set()
diff --git a/tests/multitenant_tests/case_foods.py b/tests/multitenant_tests/case_foods.py
index e28206f5f663..40b688276510 100644
--- a/tests/multitenant_tests/case_foods.py
+++ b/tests/multitenant_tests/case_foods.py
@@ -1,5 +1,3 @@
-from typing import Tuple
-
from requests import Response
from mealie.schema.recipe.recipe_ingredient import IngredientFood, SaveIngredientFood
@@ -26,7 +24,7 @@ class FoodsTestCase(ABCMultiTenantTestCase):
return food_ids
- def seed_multi(self, group1_id: str, group2_id: str) -> Tuple[set[str], set[str]]:
+ def seed_multi(self, group1_id: str, group2_id: str) -> tuple[set[str], set[str]]:
g1_item_ids = set()
g2_item_ids = set()
diff --git a/tests/multitenant_tests/case_tags.py b/tests/multitenant_tests/case_tags.py
index 3b2f4aab4285..6ab1f6fc118a 100644
--- a/tests/multitenant_tests/case_tags.py
+++ b/tests/multitenant_tests/case_tags.py
@@ -1,5 +1,3 @@
-from typing import Tuple
-
from requests import Response
from mealie.schema.recipe.recipe import RecipeTag
@@ -27,7 +25,7 @@ class TagsTestCase(ABCMultiTenantTestCase):
return tag_ids
- def seed_multi(self, group1_id: str, group2_id: str) -> Tuple[set[str], set[str]]:
+ def seed_multi(self, group1_id: str, group2_id: str) -> tuple[set[str], set[str]]:
g1_item_ids = set()
g2_item_ids = set()
diff --git a/tests/multitenant_tests/case_tools.py b/tests/multitenant_tests/case_tools.py
index f1939cc79bbb..ef417841d8a0 100644
--- a/tests/multitenant_tests/case_tools.py
+++ b/tests/multitenant_tests/case_tools.py
@@ -1,5 +1,3 @@
-from typing import Tuple
-
from requests import Response
from mealie.schema.recipe.recipe import RecipeTool
@@ -27,7 +25,7 @@ class ToolsTestCase(ABCMultiTenantTestCase):
return tool_ids
- def seed_multi(self, group1_id: str, group2_id: str) -> Tuple[set[int], set[int]]:
+ def seed_multi(self, group1_id: str, group2_id: str) -> tuple[set[int], set[int]]:
g1_item_ids = set()
g2_item_ids = set()
diff --git a/tests/multitenant_tests/case_units.py b/tests/multitenant_tests/case_units.py
index 4a0235cd7b1f..c40466d9116b 100644
--- a/tests/multitenant_tests/case_units.py
+++ b/tests/multitenant_tests/case_units.py
@@ -1,5 +1,3 @@
-from typing import Tuple
-
from requests import Response
from mealie.schema.recipe.recipe_ingredient import IngredientUnit, SaveIngredientUnit
@@ -26,7 +24,7 @@ class UnitsTestCase(ABCMultiTenantTestCase):
return unit_ids
- def seed_multi(self, group1_id: str, group2_id: str) -> Tuple[set[str], set[str]]:
+ def seed_multi(self, group1_id: str, group2_id: str) -> tuple[set[str], set[str]]:
g1_item_ids = set()
g2_item_ids = set()
diff --git a/tests/multitenant_tests/test_multitenant_cases.py b/tests/multitenant_tests/test_multitenant_cases.py
index a4bbc3a4a1c4..7d7dd64019a0 100644
--- a/tests/multitenant_tests/test_multitenant_cases.py
+++ b/tests/multitenant_tests/test_multitenant_cases.py
@@ -1,5 +1,3 @@
-from typing import Type
-
import pytest
from fastapi.testclient import TestClient
@@ -26,7 +24,7 @@ def test_multitenant_cases_get_all(
api_client: TestClient,
multitenants: MultiTenant,
database: AllRepositories,
- test_case: Type[ABCMultiTenantTestCase],
+ test_case: type[ABCMultiTenantTestCase],
):
"""
This test will run all the multitenant test cases and validate that they return only the data for their group.
@@ -63,7 +61,7 @@ def test_multitenant_cases_same_named_resources(
api_client: TestClient,
multitenants: MultiTenant,
database: AllRepositories,
- test_case: Type[ABCMultiTenantTestCase],
+ test_case: type[ABCMultiTenantTestCase],
):
"""
This test is used to ensure that the same resource can be created with the same values in different tenants.