diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
new file mode 100644
index 000000000..d9127a9ec
--- /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 000000000..9f13d65ac
--- /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 9a5ecfe26..fdbde1270 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 000000000..92273046e
--- /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 68e5e36cd..1b51f1eed 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 8019c89dc..72f51b7f3 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 a0013defe..028126eb8 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 b51f0dc92..8c6c6e2b3 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 87a1d51f1..32b6ced12 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 da0ab8e5d..c2d4aee3c 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 24ab52cdf..000000000
--- 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 0d419a73f..d3c1dd50f 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 342374c64..000000000
--- 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 0f34d0636..282b77ae1 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 85ee8018e..c5b57d321 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 cd09f2605..dd178114e 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 03cd1f352..28cc31047 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 5d2adea09..1e1e54e88 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 d6dd611bb..148453217 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 b894e5955..08bfc38a8 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 60f3698cc..d32198a25 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 448ea115e..31a5b9692 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 0c519013f..4371eea8f 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 530396f95..d4f19ef02 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 fc8d93bfa..a8744e35c 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 22e0582a7..5bf05991b 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 f62a063f3..b3a3dcfc5 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 8bff6a005..d6c9079e9 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 f894d17a1..f38cc914c 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 1a8ca7541..ac3e9b0f8 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 8e7e2d594..2b2c108d3 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 472676c6e..4de8af927 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 b55758c9c..ec01a43bc 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 6484db57d..ceeb2a187 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 530b54e3c..4fc481553 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 a6f1951fa..4c9e2a777 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 27bff973c..27e32888e 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 488cb8284..6fafa24de 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 69f39bc95..2322ddbd4 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 7dd5429cf..4189f9490 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 876787644..349eb01c0 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 b8ad87f8f..225648314 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 8030378fe..cec71d40b 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 7639ecfbd..53eda1b39 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 e0a5be5cf..cf0d24250 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 7dd63e7e3..61b5ff7d4 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 90a99c6f3..b5ed2e839 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 e2ef26624..66b1c55e0 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 76e79a58d..4bf6754d0 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 2a0c59d57..c19b1ee5e 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 4f1bfaea3..209aeb6f8 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 ce5cf87f7..960f35fda 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 000000000..e69de29bb
diff --git a/mealie/schema/_mealie/types.py b/mealie/schema/_mealie/types.py
new file mode 100644
index 000000000..211b44da0
--- /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 93bc62a16..03d7b5869 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 61081e6a1..21674ad48 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 3e0bce863..3e1582cbd 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 387fcff20..5a87a3535 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 e2024126a..2ab47dda3 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 0dfd2a7df..d558cd4fd 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 03febdedb..3c5842d65 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 4be7b5df1..c5daa0cba 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 92b1e9199..cc3f64844 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 95a3ec825..a790f776f 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 fe09bc2ae..e41dd93bf 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 fe1f0fb17..af43abaf5 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 18a420e97..afbfb7f1a 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 d199b54f8..dcdd9e5dc 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 0b7bfe4c0..ffd6686a7 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 f7f8f9fc9..5511581cb 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 781da970b..7512aabc6 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 e2628019b..2812d39ba 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 b4609bf34..a1c9c5b54 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 739f5b7b2..c84504822 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 883688912..a9b7fd264 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 7103a129f..4e36f4027 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 eb3c39a7c..348aeefaf 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 502f862d8..0ef4f5e3e 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 3382845ca..615257553 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 bee87b360..e7766b413 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 1fb53a926..912454848 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 2186829c3..2578c86bf 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 8fa1000f2..36515eb88 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 0e2f0115e..d7159bab6 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 a0ca65ee0..f75e6012e 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 29b3c0e85..75c927103 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 8ef4185fd..1c34a126c 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 e74459c1a..d9aff970f 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 948d87d1d..81a08e8a1 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 e55f9e355..e1c116952 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 3c609910f..ad34b8009 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 51e10763d..62c338695 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 f8b7187d4..108d74d8d 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 1c4fd634d..12355db2d 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 fb1da4824..088f1d9fb 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 6cc406c4d..2f87e128e 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 1277ebfe9..4f1ae3216 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 fef2818c4..576846f89 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 a4f235599..e27ec6213 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 769c6ed90..f84b664de 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 e28206f5f..40b688276 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 3b2f4aab4..6ab1f6fc1 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 f1939cc79..ef417841d 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 4a0235cd7..c40466d91 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 a4bbc3a4a..7d7dd6401 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.