mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-05-24 01:12:54 -04:00
improve developer tooling (backend) (#1051)
* add basic pre-commit file * add flake8 * add isort * add pep585-upgrade (typing upgrades) * use namespace for import * add mypy * update ci for backend * flake8 scope * fix version format * update makefile * disable strict option (temporary) * fix mypy issues * upgrade type hints (pre-commit) * add vscode typing check * add types to dev deps * remote container draft * update setup script * update compose version * run setup on create * dev containers update * remove unused pages * update setup tips * expose ports * Update pre-commit to include flask8-print (#1053) * Add in flake8-print to pre-commit * pin version of flake8-print * formatting * update getting strated docs * add mypy to pre-commit * purge .mypy_cache on clean * drop mypy Co-authored-by: zackbcom <zackbcom@users.noreply.github.com>
This commit is contained in:
parent
e109391e9a
commit
3c2744a3da
38
.devcontainer/Dockerfile
Normal file
38
.devcontainer/Dockerfile
Normal file
@ -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
|
48
.devcontainer/devcontainer.json
Normal file
48
.devcontainer/devcontainer.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
16
.github/workflows/backend-tests.yml
vendored
16
.github/workflows/backend-tests.yml
vendored
@ -69,13 +69,23 @@ jobs:
|
|||||||
sudo apt-get install libsasl2-dev libldap2-dev libssl-dev
|
sudo apt-get install libsasl2-dev libldap2-dev libssl-dev
|
||||||
poetry install
|
poetry install
|
||||||
poetry add "psycopg2-binary==2.8.6"
|
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
|
# 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:
|
env:
|
||||||
DB_ENGINE: ${{ matrix.Database }}
|
DB_ENGINE: ${{ matrix.Database }}
|
||||||
POSTGRES_SERVER: localhost
|
POSTGRES_SERVER: localhost
|
||||||
run: |
|
run: |
|
||||||
make test-all
|
make backend-test
|
||||||
|
30
.pre-commit-config.yaml
Normal file
30
.pre-commit-config.yaml
Normal file
@ -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"
|
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@ -42,9 +42,10 @@
|
|||||||
"python.testing.pytestArgs": ["tests"],
|
"python.testing.pytestArgs": ["tests"],
|
||||||
"python.testing.pytestEnabled": true,
|
"python.testing.pytestEnabled": true,
|
||||||
"python.testing.unittestEnabled": false,
|
"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",
|
"search.mode": "reuseEditor",
|
||||||
"vetur.validation.template": false,
|
"vetur.validation.template": false,
|
||||||
"python.sortImports.path": "${workspaceFolder}/.venv/bin/isort",
|
|
||||||
"coverage-gutters.lcovname": "${workspaceFolder}/.coverage"
|
"coverage-gutters.lcovname": "${workspaceFolder}/.coverage"
|
||||||
}
|
}
|
||||||
|
@ -65,8 +65,9 @@ COPY --from=builder-base $PYSETUP_PATH $PYSETUP_PATH
|
|||||||
COPY ./mealie $MEALIE_HOME/mealie
|
COPY ./mealie $MEALIE_HOME/mealie
|
||||||
COPY ./poetry.lock ./pyproject.toml $MEALIE_HOME/
|
COPY ./poetry.lock ./pyproject.toml $MEALIE_HOME/
|
||||||
|
|
||||||
#! Future
|
# Alembic
|
||||||
# COPY ./alembic ./alembic.ini $MEALIE_HOME/
|
COPY ./alembic $MEALIE_HOME/alembic
|
||||||
|
COPY ./alembic.ini $MEALIE_HOME/
|
||||||
|
|
||||||
# venv already has runtime deps installed we get a quicker install
|
# venv already has runtime deps installed we get a quicker install
|
||||||
WORKDIR $MEALIE_HOME
|
WORKDIR $MEALIE_HOME
|
||||||
@ -114,7 +115,7 @@ COPY ./mealie $MEALIE_HOME/mealie
|
|||||||
COPY ./poetry.lock ./pyproject.toml $MEALIE_HOME/
|
COPY ./poetry.lock ./pyproject.toml $MEALIE_HOME/
|
||||||
COPY ./gunicorn_conf.py $MEALIE_HOME
|
COPY ./gunicorn_conf.py $MEALIE_HOME
|
||||||
|
|
||||||
#! Future
|
# Alembic
|
||||||
COPY ./alembic $MEALIE_HOME/alembic
|
COPY ./alembic $MEALIE_HOME/alembic
|
||||||
COPY ./alembic.ini $MEALIE_HOME/
|
COPY ./alembic.ini $MEALIE_HOME/
|
||||||
|
|
||||||
|
53
README.md
53
README.md
@ -47,58 +47,23 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[![Product Name Screen Shot][product-screenshot]](https://example.com)
|
[![Product Name Screen Shot][product-screenshot]](https://docs.mealie.io)
|
||||||
|
|
||||||
# About The Project
|
# 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)!
|
- [Remember to join the Discord](https://discord.gg/QuStdQGSGK)!
|
||||||
|
- [Documentation](https://docs.mealie.io)
|
||||||
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
||||||
|
|
||||||
<!-- CONTRIBUTING -->
|
<!-- CONTRIBUTING -->
|
||||||
## Contributing
|
## 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.
|
||||||
|
|
||||||
|
- 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.
|
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.
|
Thanks to Linode for providing Hosting for the Demo, Beta, and Documentation sites! Another big thanks to JetBrains for providing their IDEs for development.
|
||||||
|
|
||||||
<div align='center'>
|
<div align='center'>
|
||||||
<img height="200" src="docs/docs/assets/img/sponsors-linode.svg" />
|
<img height="100" src="docs/docs/assets/img/sponsors-linode.svg" />
|
||||||
<img height="200" src="docs/docs/assets/img/sponsors-jetbrains.png" />
|
<img height="100" src="docs/docs/assets/img/sponsors-jetbrains.png" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import re
|
import re
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Tuple
|
|
||||||
|
|
||||||
import black
|
import black
|
||||||
import isort
|
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")
|
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
|
start = None
|
||||||
end = None
|
end = None
|
||||||
indentation = None
|
indentation = None
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# Use root/example as user/password credentials
|
# Use root/example as user/password credentials
|
||||||
version: "3.1"
|
version: "3.4"
|
||||||
services:
|
services:
|
||||||
# Vue Frontend
|
# Vue Frontend
|
||||||
mealie-frontend:
|
mealie-frontend:
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
version: "3.1"
|
version: "3.4"
|
||||||
services:
|
services:
|
||||||
mealie-frontend:
|
mealie-frontend:
|
||||||
container_name: mealie-frontend
|
container_name: mealie-frontend
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
# Guidelines
|
|
||||||
|
|
||||||
## Python
|
|
||||||
|
|
||||||
## Vue
|
|
||||||
|
|
||||||
[See The Style Guide](../developers-guide/style-guide.md)
|
|
@ -6,21 +6,25 @@
|
|||||||
|
|
||||||
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.
|
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
|
## With VS Code Dev Containers
|
||||||
|
|
||||||
!!! 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)
|
|
||||||
|
|
||||||
Prerequisites
|
Prerequisites
|
||||||
|
|
||||||
- Docker
|
- 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
|
### Prerequisites
|
||||||
|
|
||||||
- [Python 3.10](https://www.python.org/downloads/)
|
- [Python 3.10](https://www.python.org/downloads/)
|
||||||
@ -89,30 +93,24 @@ Once that is complete you're ready to start the servers. You'll need two shells
|
|||||||
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.
|
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
|
docs 📄 Start Mkdocs Development Server
|
||||||
clean 🧹 Remove all build, test, coverage and Python artifacts
|
code-gen 🤖 Run Code-Gen Scripts
|
||||||
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
|
|
||||||
setup 🏗 Setup Development Instance
|
setup 🏗 Setup Development Instance
|
||||||
setup-model 🤖 Get the latest NLP CRF++ Model
|
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
|
backend 🎬 Start Mealie Backend Development Server
|
||||||
frontend 🎬 Start Mealie Frontend Development Server
|
frontend 🎬 Start Mealie Frontend Development Server
|
||||||
frontend-build 🏗 Build Frontend in frontend/dist
|
frontend-build 🏗 Build Frontend in frontend/dist
|
||||||
frontend-generate 🏗 Generate Code for Frontend
|
frontend-generate 🏗 Generate Code for Frontend
|
||||||
frontend-lint 🧺 Run yarn lint
|
frontend-lint 🧺 Run yarn lint
|
||||||
docs 📄 Start Mkdocs Development Server
|
|
||||||
docker-dev 🐳 Build and Start Docker Development Stack
|
docker-dev 🐳 Build and Start Docker Development Stack
|
||||||
docker-prod 🐳 Build and Start Docker Production 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)
|
|
@ -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
|
|
||||||
<v-btn color="primary">
|
|
||||||
<v-icon left> mdi-plus </v-icon>
|
|
||||||
Primary Button
|
|
||||||
</v-btn>
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
File diff suppressed because one or more lines are too long
@ -82,8 +82,6 @@ nav:
|
|||||||
- Developers Guide:
|
- Developers Guide:
|
||||||
- Code Contributions: "contributors/developers-guide/code-contributions.md"
|
- Code Contributions: "contributors/developers-guide/code-contributions.md"
|
||||||
- Dev Getting Started: "contributors/developers-guide/starting-dev-server.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:
|
- Guides:
|
||||||
- Improving Ingredient Parser: "contributors/guides/ingredient-parser.md"
|
- Improving Ingredient Parser: "contributors/guides/ingredient-parser.md"
|
||||||
|
|
||||||
|
84
makefile
84
makefile
@ -23,15 +23,45 @@ BROWSER := python -c "$$BROWSER_PYSCRIPT"
|
|||||||
help:
|
help:
|
||||||
@python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST)
|
@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/recipes/
|
||||||
rm -r ./dev/data/users/
|
rm -r ./dev/data/users/
|
||||||
rm -f ./dev/data/mealie*.db
|
rm -f ./dev/data/mealie*.db
|
||||||
rm -f ./dev/data/mealie.log
|
rm -f ./dev/data/mealie.log
|
||||||
rm -f ./dev/data/.secret
|
rm -f ./dev/data/.secret
|
||||||
|
|
||||||
clean: clean-pyc clean-test ## 🧹 Remove all build, test, coverage and Python artifacts
|
|
||||||
|
|
||||||
clean-pyc: ## 🧹 Remove Python file artifacts
|
clean-pyc: ## 🧹 Remove Python file artifacts
|
||||||
find ./mealie -name '*.pyc' -exec rm -f {} +
|
find ./mealie -name '*.pyc' -exec rm -f {} +
|
||||||
find ./mealie -name '*.pyo' -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 htmlcov/
|
||||||
rm -fr .pytest_cache
|
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
|
poetry run pytest
|
||||||
|
|
||||||
lint-test:
|
backend-format: ## 🧺 Format, Check and Flake8
|
||||||
poetry run black . --check
|
|
||||||
poetry run isort . --check-only
|
|
||||||
poetry run flake8 mealie tests
|
|
||||||
|
|
||||||
lint: ## 🧺 Format, Check and Flake8
|
|
||||||
poetry run isort .
|
poetry run isort .
|
||||||
poetry run black .
|
poetry run black .
|
||||||
|
|
||||||
|
backend-lint:
|
||||||
poetry run flake8 mealie tests
|
poetry run flake8 mealie tests
|
||||||
|
|
||||||
coverage: ## ☂️ Check code coverage quickly with the default Python
|
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 pytest
|
||||||
poetry run coverage report -m
|
poetry run coverage report -m
|
||||||
poetry run coveragepy-lcov
|
poetry run coveragepy-lcov
|
||||||
poetry run coverage html
|
poetry run coverage html
|
||||||
$(BROWSER) htmlcov/index.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
|
backend: ## 🎬 Start Mealie Backend Development Server
|
||||||
poetry run python mealie/db/init_db.py && \
|
poetry run python mealie/db/init_db.py && \
|
||||||
poetry run python mealie/app.py
|
poetry run python mealie/app.py
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Frontend makefile
|
||||||
|
|
||||||
.PHONY: frontend
|
.PHONY: frontend
|
||||||
frontend: ## 🎬 Start Mealie Frontend Development Server
|
frontend: ## 🎬 Start Mealie Frontend Development Server
|
||||||
cd frontend && yarn run dev
|
cd frontend && yarn run dev
|
||||||
@ -97,10 +119,8 @@ frontend-generate: ## 🏗 Generate Code for Frontend
|
|||||||
frontend-lint: ## 🧺 Run yarn lint
|
frontend-lint: ## 🧺 Run yarn lint
|
||||||
cd frontend && yarn lint
|
cd frontend && yarn lint
|
||||||
|
|
||||||
.PHONY: docs
|
# -----------------------------------------------------------------------------
|
||||||
docs: ## 📄 Start Mkdocs Development Server
|
# Docker makefile
|
||||||
poetry run python dev/scripts/api_docs_gen.py && \
|
|
||||||
cd docs && poetry run python -m mkdocs serve
|
|
||||||
|
|
||||||
docker-dev: ## 🐳 Build and Start Docker Development Stack
|
docker-dev: ## 🐳 Build and Start Docker Development Stack
|
||||||
docker-compose -f docker-compose.dev.yml -p dev-mealie down && \
|
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-prod: ## 🐳 Build and Start Docker Production Stack
|
||||||
docker-compose -f docker-compose.yml -p mealie up --build
|
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
|
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
|
from collections.abc import AsyncGenerator, Callable, Generator
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from uuid import uuid4
|
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)
|
tokens: list[LongLiveTokenInDB] = repos.api_tokens.get(id, "user_id", limit=9999)
|
||||||
|
|
||||||
for token in tokens:
|
for token in tokens:
|
||||||
token: LongLiveTokenInDB
|
|
||||||
if token.token == client_token:
|
if token.token == client_token:
|
||||||
return token.user
|
return token.user
|
||||||
|
|
||||||
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid Token")
|
||||||
|
|
||||||
|
|
||||||
def validate_file_token(token: Optional[str] = None) -> Path:
|
def validate_file_token(token: Optional[str] = None) -> Path:
|
||||||
credentials_exception = HTTPException(
|
credentials_exception = HTTPException(
|
||||||
@ -133,7 +135,7 @@ def validate_recipe_token(token: Optional[str] = None) -> str:
|
|||||||
return slug
|
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)
|
app_dirs.TEMP_DIR.mkdir(exist_ok=True, parents=True)
|
||||||
temp_path = app_dirs.TEMP_DIR.joinpath("my_zip_archive.zip")
|
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)
|
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 = app_dirs.TEMP_DIR.joinpath(uuid4().hex)
|
||||||
temp_path.mkdir(exist_ok=True, parents=True)
|
temp_path.mkdir(exist_ok=True, parents=True)
|
||||||
|
|
||||||
@ -153,12 +155,12 @@ async def temporary_dir() -> Path:
|
|||||||
shutil.rmtree(temp_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
|
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 = app_dirs.TEMP_DIR.joinpath(uuid4().hex + ext)
|
||||||
temp_path.touch()
|
temp_path.touch()
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ class LoggerConfig:
|
|||||||
format: str
|
format: str
|
||||||
date_format: str
|
date_format: str
|
||||||
logger_file: str
|
logger_file: str
|
||||||
level: str = logging.INFO
|
level: int = logging.INFO
|
||||||
|
|
||||||
|
|
||||||
@lru_cache
|
@lru_cache
|
||||||
|
@ -36,7 +36,7 @@ def create_recipe_slug_token(file_path: str) -> str:
|
|||||||
return create_access_token(token_data, expires_delta=timedelta(minutes=30))
|
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
|
"""Given a username and password, tries to authenticate by BINDing to an
|
||||||
LDAP server
|
LDAP server
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ class PostgresProvider(AbstractDBProvider, BaseSettings):
|
|||||||
POSTGRES_USER: str = "mealie"
|
POSTGRES_USER: str = "mealie"
|
||||||
POSTGRES_PASSWORD: str = "mealie"
|
POSTGRES_PASSWORD: str = "mealie"
|
||||||
POSTGRES_SERVER: str = "postgres"
|
POSTGRES_SERVER: str = "postgres"
|
||||||
POSTGRES_PORT: str = 5432
|
POSTGRES_PORT: str = "5432"
|
||||||
POSTGRES_DB: str = "mealie"
|
POSTGRES_DB: str = "mealie"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -2,7 +2,7 @@ import secrets
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from pydantic import BaseSettings
|
from pydantic import BaseSettings, NoneStr
|
||||||
|
|
||||||
from .db_providers import AbstractDBProvider, db_provider_factory
|
from .db_providers import AbstractDBProvider, db_provider_factory
|
||||||
|
|
||||||
@ -33,26 +33,26 @@ class AppSettings(BaseSettings):
|
|||||||
SECRET: str
|
SECRET: str
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def DOCS_URL(self) -> str:
|
def DOCS_URL(self) -> str | None:
|
||||||
return "/docs" if self.API_DOCS else None
|
return "/docs" if self.API_DOCS else None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def REDOC_URL(self) -> str:
|
def REDOC_URL(self) -> str | None:
|
||||||
return "/redoc" if self.API_DOCS else None
|
return "/redoc" if self.API_DOCS else None
|
||||||
|
|
||||||
# ===============================================
|
# ===============================================
|
||||||
# Database Configuration
|
# Database Configuration
|
||||||
|
|
||||||
DB_ENGINE: str = "sqlite" # Options: 'sqlite', 'postgres'
|
DB_ENGINE: str = "sqlite" # Options: 'sqlite', 'postgres'
|
||||||
DB_PROVIDER: AbstractDBProvider = None
|
DB_PROVIDER: Optional[AbstractDBProvider] = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def DB_URL(self) -> str:
|
def DB_URL(self) -> str | None:
|
||||||
return self.DB_PROVIDER.db_url
|
return self.DB_PROVIDER.db_url if self.DB_PROVIDER else None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def DB_URL_PUBLIC(self) -> str:
|
def DB_URL_PUBLIC(self) -> str | None:
|
||||||
return self.DB_PROVIDER.db_url_public
|
return self.DB_PROVIDER.db_url_public if self.DB_PROVIDER else None
|
||||||
|
|
||||||
DEFAULT_GROUP: str = "Home"
|
DEFAULT_GROUP: str = "Home"
|
||||||
DEFAULT_EMAIL: str = "changeme@email.com"
|
DEFAULT_EMAIL: str = "changeme@email.com"
|
||||||
@ -88,9 +88,9 @@ class AppSettings(BaseSettings):
|
|||||||
# LDAP Configuration
|
# LDAP Configuration
|
||||||
|
|
||||||
LDAP_AUTH_ENABLED: bool = False
|
LDAP_AUTH_ENABLED: bool = False
|
||||||
LDAP_SERVER_URL: str = None
|
LDAP_SERVER_URL: NoneStr = None
|
||||||
LDAP_BIND_TEMPLATE: str = None
|
LDAP_BIND_TEMPLATE: NoneStr = None
|
||||||
LDAP_ADMIN_FILTER: str = None
|
LDAP_ADMIN_FILTER: NoneStr = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def LDAP_ENABLED(self) -> bool:
|
def LDAP_ENABLED(self) -> bool:
|
||||||
|
@ -24,7 +24,7 @@ def sql_global_init(db_url: str):
|
|||||||
return SessionLocal, engine
|
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:
|
def create_session() -> Session:
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
|
from collections.abc import Callable
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Callable
|
|
||||||
|
|
||||||
from sqlalchemy import engine
|
from sqlalchemy import engine
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from .group import *
|
from .group import *
|
||||||
from .labels import *
|
from .labels import *
|
||||||
from .recipe.recipe import *
|
from .recipe.recipe import * # type: ignore
|
||||||
from .server import *
|
from .server import *
|
||||||
from .users import *
|
from .users import *
|
||||||
|
@ -24,7 +24,7 @@ class BaseMixins:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_ref(cls, match_value: str, match_attr: str = None, session: Session = None):
|
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:
|
if match_value is None or session is None:
|
||||||
return None
|
return None
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from functools import wraps
|
from functools import wraps
|
||||||
from uuid import UUID
|
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 import MANYTOMANY, MANYTOONE, ONETOMANY, Session
|
||||||
from sqlalchemy.orm.decl_api import DeclarativeMeta
|
from sqlalchemy.orm.decl_api import DeclarativeMeta
|
||||||
from sqlalchemy.orm.mapper import Mapper
|
from sqlalchemy.orm.mapper import Mapper
|
||||||
@ -21,7 +21,7 @@ class AutoInitConfig(BaseModel):
|
|||||||
Config class for `auto_init` decorator.
|
Config class for `auto_init` decorator.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
get_attr: str = None
|
get_attr: NoneStr = None
|
||||||
exclude: set = Field(default_factory=_default_exclusion)
|
exclude: set = Field(default_factory=_default_exclusion)
|
||||||
# auto_create: bool = False
|
# 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
|
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()
|
existing_elem = session.query(relation_cls).filter_by(**{get_attr: elem_id}).one_or_none()
|
||||||
|
|
||||||
if existing_elem is None:
|
is_dict = isinstance(elem, dict)
|
||||||
elems_to_create.append(elem)
|
|
||||||
|
if existing_elem is None and is_dict:
|
||||||
|
elems_to_create.append(elem) # type: ignore
|
||||||
continue
|
continue
|
||||||
|
|
||||||
elif isinstance(elem, dict):
|
elif is_dict:
|
||||||
for key, value in elem.items():
|
for key, value in elem.items(): # type: ignore
|
||||||
if key not in cfg.exclude:
|
if key not in cfg.exclude:
|
||||||
setattr(existing_elem, key, value)
|
setattr(existing_elem, key, value)
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import inspect
|
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:
|
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.
|
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.
|
Returns a tuple of valid arguemnts for the supplied function.
|
||||||
"""
|
"""
|
||||||
|
@ -78,8 +78,8 @@ class Group(SqlAlchemyBase, BaseMixins):
|
|||||||
def __init__(self, **_) -> None:
|
def __init__(self, **_) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod # TODO: Remove this
|
||||||
def get_ref(session: Session, name: str):
|
def get_ref(session: Session, name: str): # type: ignore
|
||||||
settings = get_app_settings()
|
settings = get_app_settings()
|
||||||
|
|
||||||
item = session.query(Group).filter(Group.name == name).one_or_none()
|
item = session.query(Group).filter(Group.name == name).one_or_none()
|
||||||
|
@ -63,8 +63,8 @@ class Category(SqlAlchemyBase, BaseMixins):
|
|||||||
self.name = name.strip()
|
self.name = name.strip()
|
||||||
self.slug = slugify(name)
|
self.slug = slugify(name)
|
||||||
|
|
||||||
@classmethod
|
@classmethod # TODO: Remove this
|
||||||
def get_ref(cls, match_value: str, session=None):
|
def get_ref(cls, match_value: str, session=None): # type: ignore
|
||||||
if not session or not match_value:
|
if not session or not match_value:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -76,4 +76,4 @@ class Category(SqlAlchemyBase, BaseMixins):
|
|||||||
return result
|
return result
|
||||||
else:
|
else:
|
||||||
logger.debug("Category doesn't exists, creating Category")
|
logger.debug("Category doesn't exists, creating Category")
|
||||||
return Category(name=match_value)
|
return Category(name=match_value) # type: ignore
|
||||||
|
@ -22,5 +22,5 @@ class RecipeComment(SqlAlchemyBase, BaseMixins):
|
|||||||
def __init__(self, **_) -> None:
|
def __init__(self, **_) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def update(self, text, **_) -> None:
|
def update(self, text, **_) -> None: # type: ignore
|
||||||
self.text = text
|
self.text = text
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import datetime
|
import datetime
|
||||||
from datetime import date
|
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
import sqlalchemy.orm as orm
|
import sqlalchemy.orm as orm
|
||||||
@ -107,7 +106,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
|||||||
extras: list[ApiExtras] = orm.relationship("ApiExtras", cascade="all, delete-orphan")
|
extras: list[ApiExtras] = orm.relationship("ApiExtras", cascade="all, delete-orphan")
|
||||||
|
|
||||||
# Time Stamp Properties
|
# 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)
|
date_updated = sa.Column(sa.DateTime)
|
||||||
|
|
||||||
# Shopping List Refs
|
# Shopping List Refs
|
||||||
|
@ -50,8 +50,8 @@ class Tag(SqlAlchemyBase, BaseMixins):
|
|||||||
self.name = name.strip()
|
self.name = name.strip()
|
||||||
self.slug = slugify(self.name)
|
self.slug = slugify(self.name)
|
||||||
|
|
||||||
@classmethod
|
@classmethod # TODO: Remove this
|
||||||
def get_ref(cls, match_value: str, session=None):
|
def get_ref(cls, match_value: str, session=None): # type: ignore
|
||||||
if not session or not match_value:
|
if not session or not match_value:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -62,4 +62,4 @@ class Tag(SqlAlchemyBase, BaseMixins):
|
|||||||
return result
|
return result
|
||||||
else:
|
else:
|
||||||
logger.debug("Category doesn't exists, creating Category")
|
logger.debug("Category doesn't exists, creating Category")
|
||||||
return Tag(name=match_value)
|
return Tag(name=match_value) # type: ignore
|
||||||
|
@ -124,6 +124,6 @@ class User(SqlAlchemyBase, BaseMixins):
|
|||||||
self.can_invite = can_invite
|
self.can_invite = can_invite
|
||||||
self.can_organize = can_organize
|
self.can_organize = can_organize
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod # TODO: Remove This
|
||||||
def get_ref(session, id: str):
|
def get_ref(session, id: str): # type: ignore
|
||||||
return session.query(User).filter(User.id == id).one()
|
return session.query(User).filter(User.id == id).one()
|
||||||
|
@ -19,11 +19,11 @@ def get_format(image: Path) -> str:
|
|||||||
def sizeof_fmt(file_path: Path, decimal_places=2):
|
def sizeof_fmt(file_path: Path, decimal_places=2):
|
||||||
if not file_path.exists():
|
if not file_path.exists():
|
||||||
return "(File Not Found)"
|
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"]:
|
for unit in ["B", "kB", "MB", "GB", "TB", "PB"]:
|
||||||
if size < 1024.0 or unit == "PiB":
|
if size < 1024 or unit == "PiB":
|
||||||
break
|
break
|
||||||
size /= 1024.0
|
size /= 1024
|
||||||
return f"{size:.{decimal_places}f} {unit}"
|
return f"{size:.{decimal_places}f} {unit}"
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from typing import Any, Callable, Generic, TypeVar, Union
|
from collections.abc import Callable
|
||||||
from uuid import UUID
|
from typing import Any, Generic, TypeVar, Union
|
||||||
|
|
||||||
from pydantic import UUID4, BaseModel
|
from pydantic import UUID4, BaseModel
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
@ -18,7 +18,7 @@ class RepositoryGeneric(Generic[T, D]):
|
|||||||
Generic ([D]): Represents the SqlAlchemyModel Model
|
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.session = session
|
||||||
self.primary_key = primary_key
|
self.primary_key = primary_key
|
||||||
self.sql_model = sql_model
|
self.sql_model = sql_model
|
||||||
@ -26,10 +26,10 @@ class RepositoryGeneric(Generic[T, D]):
|
|||||||
self.observers: list = []
|
self.observers: list = []
|
||||||
|
|
||||||
self.limit_by_group = False
|
self.limit_by_group = False
|
||||||
self.user_id = None
|
self.user_id: UUID4 = None
|
||||||
|
|
||||||
self.limit_by_user = False
|
self.limit_by_user = False
|
||||||
self.group_id = None
|
self.group_id: UUID4 = None
|
||||||
|
|
||||||
def subscribe(self, func: Callable) -> None:
|
def subscribe(self, func: Callable) -> None:
|
||||||
self.observers.append(func)
|
self.observers.append(func)
|
||||||
@ -39,7 +39,7 @@ class RepositoryGeneric(Generic[T, D]):
|
|||||||
self.user_id = user_id
|
self.user_id = user_id
|
||||||
return self
|
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.limit_by_group = True
|
||||||
self.group_id = group_id
|
self.group_id = group_id
|
||||||
return self
|
return self
|
||||||
@ -88,7 +88,7 @@ class RepositoryGeneric(Generic[T, D]):
|
|||||||
|
|
||||||
def multi_query(
|
def multi_query(
|
||||||
self,
|
self,
|
||||||
query_by: dict[str, str],
|
query_by: dict[str, str | bool | int | UUID4],
|
||||||
start=0,
|
start=0,
|
||||||
limit: int = None,
|
limit: int = None,
|
||||||
override_schema=None,
|
override_schema=None,
|
||||||
@ -152,7 +152,7 @@ class RepositoryGeneric(Generic[T, D]):
|
|||||||
filter = self._filter_builder(**{match_key: match_value})
|
filter = self._filter_builder(**{match_key: match_value})
|
||||||
return self.session.query(self.sql_model).filter_by(**filter).one()
|
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
|
key = key or self.primary_key
|
||||||
|
|
||||||
q = self.session.query(self.sql_model)
|
q = self.session.query(self.sql_model)
|
||||||
@ -166,14 +166,14 @@ class RepositoryGeneric(Generic[T, D]):
|
|||||||
result = q.one_or_none()
|
result = q.one_or_none()
|
||||||
|
|
||||||
if not result:
|
if not result:
|
||||||
return
|
return None
|
||||||
|
|
||||||
eff_schema = override_schema or self.schema
|
eff_schema = override_schema or self.schema
|
||||||
return eff_schema.from_orm(result)
|
return eff_schema.from_orm(result)
|
||||||
|
|
||||||
def get(
|
def get(
|
||||||
self, match_value: str | int | UUID4, match_key: str = None, limit=1, any_case=False, override_schema=None
|
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
|
"""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.
|
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)
|
search_attr = getattr(self.sql_model, match_key)
|
||||||
result = (
|
result = (
|
||||||
self.session.query(self.sql_model)
|
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)
|
.limit(limit)
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
@ -210,7 +210,7 @@ class RepositoryGeneric(Generic[T, D]):
|
|||||||
|
|
||||||
return [eff_schema.from_orm(x) for x in result]
|
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.
|
"""Creates a new database entry for the given SQL Alchemy Model.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -221,7 +221,7 @@ class RepositoryGeneric(Generic[T, D]):
|
|||||||
dict: A dictionary representation of the database entry
|
dict: A dictionary representation of the database entry
|
||||||
"""
|
"""
|
||||||
document = document if isinstance(document, dict) else document.dict()
|
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.add(new_document)
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
self.session.refresh(new_document)
|
self.session.refresh(new_document)
|
||||||
@ -231,7 +231,7 @@ class RepositoryGeneric(Generic[T, D]):
|
|||||||
|
|
||||||
return self.schema.from_orm(new_document)
|
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.
|
"""Update a database entry.
|
||||||
Args:
|
Args:
|
||||||
session (Session): Database Session
|
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()
|
new_data = new_data if isinstance(new_data, dict) else new_data.dict()
|
||||||
|
|
||||||
entry = self._query_one(match_value=match_value)
|
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:
|
if self.observers:
|
||||||
self.update_observers()
|
self.update_observers()
|
||||||
@ -252,13 +252,14 @@ class RepositoryGeneric(Generic[T, D]):
|
|||||||
self.session.commit()
|
self.session.commit()
|
||||||
return self.schema.from_orm(entry)
|
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()
|
new_data = new_data if isinstance(new_data, dict) else new_data.dict()
|
||||||
|
|
||||||
entry = self._query_one(match_value=match_value)
|
entry = self._query_one(match_value=match_value)
|
||||||
|
|
||||||
if not entry:
|
if not entry:
|
||||||
return
|
# TODO: Should raise exception
|
||||||
|
return None
|
||||||
|
|
||||||
entry_as_dict = self.schema.from_orm(entry).dict()
|
entry_as_dict = self.schema.from_orm(entry).dict()
|
||||||
entry_as_dict.update(new_data)
|
entry_as_dict.update(new_data)
|
||||||
@ -300,7 +301,7 @@ class RepositoryGeneric(Generic[T, D]):
|
|||||||
attr_match: str = None,
|
attr_match: str = None,
|
||||||
count=True,
|
count=True,
|
||||||
override_schema=None,
|
override_schema=None,
|
||||||
) -> Union[int, T]:
|
) -> Union[int, list[T]]:
|
||||||
eff_schema = override_schema or self.schema
|
eff_schema = override_schema or self.schema
|
||||||
# attr_filter = getattr(self.sql_model, attribute_name)
|
# attr_filter = getattr(self.sql_model, attribute_name)
|
||||||
|
|
||||||
@ -316,7 +317,7 @@ class RepositoryGeneric(Generic[T, D]):
|
|||||||
new_documents = []
|
new_documents = []
|
||||||
for document in documents:
|
for document in documents:
|
||||||
document = document if isinstance(document, dict) else document.dict()
|
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)
|
new_documents.append(new_document)
|
||||||
|
|
||||||
self.session.add_all(new_documents)
|
self.session.add_all(new_documents)
|
||||||
|
@ -10,7 +10,7 @@ from .repository_generic import RepositoryGeneric
|
|||||||
|
|
||||||
class RepositoryMealPlanRules(RepositoryGeneric[PlanRulesOut, GroupMealPlanRules]):
|
class RepositoryMealPlanRules(RepositoryGeneric[PlanRulesOut, GroupMealPlanRules]):
|
||||||
def by_group(self, group_id: UUID) -> "RepositoryMealPlanRules":
|
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]:
|
def get_rules(self, day: PlanRulesDay, entry_type: PlanRulesType) -> list[PlanRulesOut]:
|
||||||
qry = self.session.query(GroupMealPlanRules).filter(
|
qry = self.session.query(GroupMealPlanRules).filter(
|
||||||
|
@ -9,10 +9,10 @@ from .repository_generic import RepositoryGeneric
|
|||||||
|
|
||||||
class RepositoryMeals(RepositoryGeneric[ReadPlanEntry, GroupMealPlan]):
|
class RepositoryMeals(RepositoryGeneric[ReadPlanEntry, GroupMealPlan]):
|
||||||
def get_slice(self, start: date, end: date, group_id: UUID) -> list[ReadPlanEntry]:
|
def get_slice(self, start: date, end: date, group_id: UUID) -> list[ReadPlanEntry]:
|
||||||
start = start.strftime("%Y-%m-%d")
|
start_str = start.strftime("%Y-%m-%d")
|
||||||
end = end.strftime("%Y-%m-%d")
|
end_str = end.strftime("%Y-%m-%d")
|
||||||
qry = self.session.query(GroupMealPlan).filter(
|
qry = self.session.query(GroupMealPlan).filter(
|
||||||
GroupMealPlan.date.between(start, end),
|
GroupMealPlan.date.between(start_str, end_str),
|
||||||
GroupMealPlan.group_id == group_id,
|
GroupMealPlan.group_id == group_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ from .repository_generic import RepositoryGeneric
|
|||||||
|
|
||||||
class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
||||||
def by_group(self, group_id: UUID) -> "RepositoryRecipes":
|
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):
|
def get_all_public(self, limit: int = None, order_by: str = None, start=0, override_schema=None):
|
||||||
eff_schema = override_schema or self.schema
|
eff_schema = override_schema or self.schema
|
||||||
@ -47,14 +47,14 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
|||||||
.all()
|
.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: RecipeModel = self._query_one(match_value=slug)
|
||||||
entry.image = randint(0, 255)
|
entry.image = randint(0, 255)
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
|
|
||||||
return entry.image
|
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(
|
return self._count_attribute(
|
||||||
attribute_name=RecipeModel.recipe_category,
|
attribute_name=RecipeModel.recipe_category,
|
||||||
attr_match=None,
|
attr_match=None,
|
||||||
@ -62,7 +62,7 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
|||||||
override_schema=override_schema,
|
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(
|
return self._count_attribute(
|
||||||
attribute_name=RecipeModel.tags,
|
attribute_name=RecipeModel.tags,
|
||||||
attr_match=None,
|
attr_match=None,
|
||||||
@ -105,7 +105,9 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
|||||||
.all()
|
.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
|
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
|
in the list. This uses a function built in to Postgres and SQLite to get a random row limited
|
||||||
|
@ -7,5 +7,5 @@ from .repository_generic import RepositoryGeneric
|
|||||||
|
|
||||||
|
|
||||||
class RepositoryShoppingList(RepositoryGeneric[ShoppingListOut, ShoppingList]):
|
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)
|
return super().update(item_id, data)
|
||||||
|
@ -16,7 +16,7 @@ class RepositoryUsers(RepositoryGeneric[PrivateUser, User]):
|
|||||||
|
|
||||||
return self.schema.from_orm(entry)
|
return self.schema.from_orm(entry)
|
||||||
|
|
||||||
def create(self, user: PrivateUser):
|
def create(self, user: PrivateUser | dict):
|
||||||
new_user = super().create(user)
|
new_user = super().create(user)
|
||||||
|
|
||||||
# Select Random Image
|
# Select Random Image
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import json
|
import json
|
||||||
from typing import Generator
|
from collections.abc import Generator
|
||||||
|
|
||||||
from mealie.schema.labels import MultiPurposeLabelSave
|
from mealie.schema.labels import MultiPurposeLabelSave
|
||||||
from mealie.schema.recipe.recipe_ingredient import SaveIngredientFood, SaveIngredientUnit
|
from mealie.schema.recipe.recipe_ingredient import SaveIngredientFood, SaveIngredientUnit
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
from abc import ABC
|
from abc import ABC
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from typing import Type
|
|
||||||
|
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
|
|
||||||
@ -29,7 +28,7 @@ class BaseUserController(ABC):
|
|||||||
|
|
||||||
deps: SharedDependencies = Depends(SharedDependencies.user)
|
deps: SharedDependencies = Depends(SharedDependencies.user)
|
||||||
|
|
||||||
def registered_exceptions(self, ex: Type[Exception]) -> str:
|
def registered_exceptions(self, ex: type[Exception]) -> str:
|
||||||
registered = {
|
registered = {
|
||||||
**mealie_registered_exceptions(self.deps.t),
|
**mealie_registered_exceptions(self.deps.t),
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,8 @@ This file contains code taken from fastapi-utils project. The code is licensed u
|
|||||||
See their repository for details -> https://github.com/dmontagu/fastapi-utils
|
See their repository for details -> https://github.com/dmontagu/fastapi-utils
|
||||||
"""
|
"""
|
||||||
import inspect
|
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 import APIRouter, Depends
|
||||||
from fastapi.routing import APIRoute
|
from fastapi.routing import APIRoute
|
||||||
@ -18,7 +19,7 @@ INCLUDE_INIT_PARAMS_KEY = "__include_init_params__"
|
|||||||
RETURN_TYPES_FUNC_KEY = "__return_types_func__"
|
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.
|
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
|
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
|
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
|
# Define cls as cbv class exclusively when using the decorator
|
||||||
return _cbv(router, cls, *urls)
|
return _cbv(router, cls, *urls)
|
||||||
|
|
||||||
return decorator
|
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
|
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`.
|
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
|
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:
|
Idempotently modifies the provided `cls`, performing the following modifications:
|
||||||
* The `__init__` function is updated to set any class-annotated dependencies as instance attributes
|
* 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)
|
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():
|
for name, hint in get_type_hints(cls).items():
|
||||||
if is_classvar(hint):
|
if is_classvar(hint):
|
||||||
continue
|
continue
|
||||||
@ -88,7 +89,7 @@ def _init_cbv(cls: Type[Any], instance: Any = None) -> None:
|
|||||||
setattr(cls, CBV_CLASS_KEY, True)
|
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()
|
cbv_router = APIRouter()
|
||||||
function_members = inspect.getmembers(cls, inspect.isfunction)
|
function_members = inspect.getmembers(cls, inspect.isfunction)
|
||||||
for url in urls:
|
for url in urls:
|
||||||
@ -97,7 +98,7 @@ def _register_endpoints(router: APIRouter, cls: Type[Any], *urls: str) -> None:
|
|||||||
for route in router.routes:
|
for route in router.routes:
|
||||||
assert isinstance(route, APIRoute)
|
assert isinstance(route, APIRoute)
|
||||||
route_methods: Any = route.methods
|
route_methods: Any = route.methods
|
||||||
cast(Tuple[Any], route_methods)
|
cast(tuple[Any], route_methods)
|
||||||
router_roles.append((route.path, tuple(route_methods)))
|
router_roles.append((route.path, tuple(route_methods)))
|
||||||
|
|
||||||
if len(set(router_roles)) != len(router_roles):
|
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)
|
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:
|
for _, func in function_members:
|
||||||
index_route = numbered_routes_by_endpoint.get(func)
|
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)
|
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
|
# 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)
|
(route.endpoint, route.path) for route in router.routes if isinstance(route, APIRoute)
|
||||||
]
|
]
|
||||||
for name, func in function_members:
|
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)
|
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.
|
Fixes the endpoint signature for a cbv route to ensure FastAPI performs dependency injection properly.
|
||||||
"""
|
"""
|
||||||
old_endpoint = route.endpoint
|
old_endpoint = route.endpoint
|
||||||
old_signature = inspect.signature(old_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]
|
old_first_parameter = old_parameters[0]
|
||||||
new_first_parameter = old_first_parameter.replace(default=Depends(cls))
|
new_first_parameter = old_first_parameter.replace(default=Depends(cls))
|
||||||
new_parameters = [new_first_parameter] + [
|
new_parameters = [new_first_parameter] + [
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
|
from collections.abc import Callable
|
||||||
from logging import Logger
|
from logging import Logger
|
||||||
from typing import Callable, Generic, Type, TypeVar
|
from typing import Generic, TypeVar
|
||||||
|
|
||||||
from fastapi import HTTPException, status
|
from fastapi import HTTPException, status
|
||||||
from pydantic import UUID4, BaseModel
|
from pydantic import UUID4, BaseModel
|
||||||
@ -26,14 +27,14 @@ class CrudMixins(Generic[C, R, U]):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
repo: RepositoryGeneric
|
repo: RepositoryGeneric
|
||||||
exception_msgs: Callable[[Type[Exception]], str] | None
|
exception_msgs: Callable[[type[Exception]], str] | None
|
||||||
default_message: str = "An unexpected error occurred."
|
default_message: str = "An unexpected error occurred."
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
repo: RepositoryGeneric,
|
repo: RepositoryGeneric,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
exception_msgs: Callable[[Type[Exception]], str] = None,
|
exception_msgs: Callable[[type[Exception]], str] = None,
|
||||||
default_message: str = None,
|
default_message: str = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
@ -83,7 +84,7 @@ class CrudMixins(Generic[C, R, U]):
|
|||||||
return item
|
return item
|
||||||
|
|
||||||
def update_one(self, data: U, item_id: int | str | UUID4) -> R:
|
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:
|
if not item:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from typing import List, Optional
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
@ -10,7 +10,7 @@ class AdminAPIRouter(APIRouter):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
tags: Optional[List[str]] = None,
|
tags: Optional[list[str]] = None,
|
||||||
prefix: str = "",
|
prefix: str = "",
|
||||||
):
|
):
|
||||||
super().__init__(tags=tags, prefix=prefix, dependencies=[Depends(get_admin_user)])
|
super().__init__(tags=tags, prefix=prefix, dependencies=[Depends(get_admin_user)])
|
||||||
@ -21,7 +21,7 @@ class UserAPIRouter(APIRouter):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
tags: Optional[List[str]] = None,
|
tags: Optional[list[str]] = None,
|
||||||
prefix: str = "",
|
prefix: str = "",
|
||||||
):
|
):
|
||||||
super().__init__(tags=tags, prefix=prefix, dependencies=[Depends(get_current_user)])
|
super().__init__(tags=tags, prefix=prefix, dependencies=[Depends(get_current_user)])
|
||||||
|
@ -52,16 +52,15 @@ def get_token(data: CustomOAuth2Form = Depends(), session: Session = Depends(gen
|
|||||||
email = data.username
|
email = data.username
|
||||||
password = data.password
|
password = data.password
|
||||||
|
|
||||||
user: PrivateUser = authenticate_user(session, email, password)
|
user = authenticate_user(session, email, password) # type: ignore
|
||||||
|
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
headers={"WWW-Authenticate": "Bearer"},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
duration = timedelta(days=14) if data.remember_me else None
|
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)
|
return MealieAuthToken.respond(access_token)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from typing import Type
|
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException
|
||||||
from pydantic import UUID4
|
from pydantic import UUID4
|
||||||
@ -24,7 +23,7 @@ class GroupCookbookController(BaseUserController):
|
|||||||
def repo(self):
|
def repo(self):
|
||||||
return self.deps.repos.cookbooks.by_group(self.group_id)
|
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 = {
|
registered = {
|
||||||
**mealie_registered_exceptions(self.deps.t),
|
**mealie_registered_exceptions(self.deps.t),
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from typing import Type
|
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from pydantic import UUID4
|
from pydantic import UUID4
|
||||||
@ -19,7 +18,7 @@ class GroupReportsController(BaseUserController):
|
|||||||
def repo(self):
|
def repo(self):
|
||||||
return self.deps.repos.group_reports.by_group(self.deps.acting_user.group_id)
|
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 {
|
return {
|
||||||
**mealie_registered_exceptions(self.deps.t),
|
**mealie_registered_exceptions(self.deps.t),
|
||||||
}.get(ex, "An unexpected error occurred.")
|
}.get(ex, "An unexpected error occurred.")
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
from datetime import date, timedelta
|
from datetime import date, timedelta
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from typing import Type
|
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException
|
||||||
|
|
||||||
@ -24,7 +23,7 @@ class GroupMealplanController(BaseUserController):
|
|||||||
def repo(self) -> RepositoryMeals:
|
def repo(self) -> RepositoryMeals:
|
||||||
return self.repos.meals.by_group(self.group_id)
|
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 = {
|
registered = {
|
||||||
**mealie_registered_exceptions(self.deps.t),
|
**mealie_registered_exceptions(self.deps.t),
|
||||||
}
|
}
|
||||||
@ -58,7 +57,7 @@ class GroupMealplanController(BaseUserController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
recipe_repo = self.repos.recipes.by_group(self.group_id)
|
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
|
if not rules: # If no rules are set, return any random recipe from the group
|
||||||
random_recipes = recipe_repo.get_random()
|
random_recipes = recipe_repo.get_random()
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import shutil
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from fastapi import Depends, File, Form
|
from fastapi import Depends, File, Form
|
||||||
from fastapi.datastructures import UploadFile
|
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.routes._base.routers import UserAPIRouter
|
||||||
from mealie.schema.group.group_migration import SupportedMigrations
|
from mealie.schema.group.group_migration import SupportedMigrations
|
||||||
from mealie.schema.reports.reports import ReportSummary
|
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"])
|
router = UserAPIRouter(prefix="/groups/migrations", tags=["Group: Migrations"])
|
||||||
|
|
||||||
@ -21,7 +28,7 @@ class GroupMigrationController(BaseUserController):
|
|||||||
add_migration_tag: bool = Form(False),
|
add_migration_tag: bool = Form(False),
|
||||||
migration_type: SupportedMigrations = Form(...),
|
migration_type: SupportedMigrations = Form(...),
|
||||||
archive: UploadFile = File(...),
|
archive: UploadFile = File(...),
|
||||||
temp_path: str = Depends(temporary_zip_path),
|
temp_path: Path = Depends(temporary_zip_path),
|
||||||
):
|
):
|
||||||
# Save archive to temp_path
|
# Save archive to temp_path
|
||||||
with temp_path.open("wb") as buffer:
|
with temp_path.open("wb") as buffer:
|
||||||
@ -36,6 +43,8 @@ class GroupMigrationController(BaseUserController):
|
|||||||
"add_migration_tag": add_migration_tag,
|
"add_migration_tag": add_migration_tag,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
migrator: BaseMigrator
|
||||||
|
|
||||||
match migration_type:
|
match migration_type:
|
||||||
case SupportedMigrations.chowdown:
|
case SupportedMigrations.chowdown:
|
||||||
migrator = ChowdownMigrator(**args)
|
migrator = ChowdownMigrator(**args)
|
||||||
|
@ -23,7 +23,6 @@ def register_debug_handler(app: FastAPI):
|
|||||||
|
|
||||||
@app.exception_handler(RequestValidationError)
|
@app.exception_handler(RequestValidationError)
|
||||||
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
||||||
|
|
||||||
exc_str = f"{exc}".replace("\n", " ").replace(" ", " ")
|
exc_str = f"{exc}".replace("\n", " ").replace(" ", " ")
|
||||||
log_wrapper(request, exc)
|
log_wrapper(request, exc)
|
||||||
content = {"status_code": status.HTTP_422_UNPROCESSABLE_ENTITY, "message": exc_str, "data": None}
|
content = {"status_code": status.HTTP_422_UNPROCESSABLE_ENTITY, "message": exc_str, "data": None}
|
||||||
|
@ -173,7 +173,7 @@ class RecipeController(BaseRecipeController):
|
|||||||
task.append_log(f"Error: Failed to create recipe from url: {b.url}")
|
task.append_log(f"Error: Failed to create recipe from url: {b.url}")
|
||||||
task.append_log(f"Error: {e}")
|
task.append_log(f"Error: {e}")
|
||||||
self.deps.logger.error(f"Failed to create recipe from url: {b.url}")
|
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)
|
database.server_tasks.update(task.id, task)
|
||||||
|
|
||||||
task.set_finished()
|
task.set_finished()
|
||||||
@ -225,12 +225,13 @@ class RecipeController(BaseRecipeController):
|
|||||||
return self.mixins.get_one(slug)
|
return self.mixins.get_one(slug)
|
||||||
|
|
||||||
@router.post("", status_code=201, response_model=str)
|
@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"""
|
"""Takes in a JSON string and loads data into the database as a new entry"""
|
||||||
try:
|
try:
|
||||||
return self.service.create_one(data).slug
|
return self.service.create_one(data).slug
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.handle_exceptions(e)
|
self.handle_exceptions(e)
|
||||||
|
return None
|
||||||
|
|
||||||
@router.put("/{slug}")
|
@router.put("/{slug}")
|
||||||
def update_one(self, slug: str, data: Recipe):
|
def update_one(self, slug: str, data: Recipe):
|
||||||
@ -263,7 +264,7 @@ class RecipeController(BaseRecipeController):
|
|||||||
# Image and Assets
|
# Image and Assets
|
||||||
|
|
||||||
@router.post("/{slug}/image", tags=["Recipe: Images 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)
|
recipe = self.mixins.get_one(slug)
|
||||||
data_service = RecipeDataService(recipe.id)
|
data_service = RecipeDataService(recipe.id)
|
||||||
data_service.scrape_image(url.url)
|
data_service.scrape_image(url.url)
|
||||||
@ -303,7 +304,7 @@ class RecipeController(BaseRecipeController):
|
|||||||
if not dest.is_file():
|
if not dest.is_file():
|
||||||
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)
|
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)
|
recipe.assets.append(asset_in)
|
||||||
|
|
||||||
self.mixins.update_one(recipe, slug)
|
self.mixins.update_one(recipe, slug)
|
||||||
|
0
mealie/schema/_mealie/__init__.py
Normal file
0
mealie/schema/_mealie/__init__.py
Normal file
3
mealie/schema/_mealie/types.py
Normal file
3
mealie/schema/_mealie/types.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
NoneFloat = Optional[float]
|
@ -1,5 +1,5 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import List, Optional
|
from typing import Optional
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
@ -22,7 +22,7 @@ class ImportJob(BackupOptions):
|
|||||||
class CreateBackup(BaseModel):
|
class CreateBackup(BaseModel):
|
||||||
tag: Optional[str]
|
tag: Optional[str]
|
||||||
options: BackupOptions
|
options: BackupOptions
|
||||||
templates: Optional[List[str]]
|
templates: Optional[list[str]]
|
||||||
|
|
||||||
|
|
||||||
class BackupFile(BaseModel):
|
class BackupFile(BaseModel):
|
||||||
@ -32,5 +32,5 @@ class BackupFile(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class AllBackups(BaseModel):
|
class AllBackups(BaseModel):
|
||||||
imports: List[BackupFile]
|
imports: list[BackupFile]
|
||||||
templates: List[str]
|
templates: list[str]
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from pydantic.main import BaseModel
|
from pydantic.main import BaseModel
|
||||||
|
|
||||||
@ -17,7 +16,7 @@ class MigrationFile(BaseModel):
|
|||||||
|
|
||||||
class Migrations(BaseModel):
|
class Migrations(BaseModel):
|
||||||
type: str
|
type: str
|
||||||
files: List[MigrationFile] = []
|
files: list[MigrationFile] = []
|
||||||
|
|
||||||
|
|
||||||
class MigrationImport(RecipeImport):
|
class MigrationImport(RecipeImport):
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from fastapi_camelcase import CamelModel
|
from fastapi_camelcase import CamelModel
|
||||||
from pydantic import UUID4
|
from pydantic import UUID4, NoneStr
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Group Events Notifier Options
|
# Group Events Notifier Options
|
||||||
@ -68,7 +68,7 @@ class GroupEventNotifierSave(GroupEventNotifierCreate):
|
|||||||
|
|
||||||
class GroupEventNotifierUpdate(GroupEventNotifierSave):
|
class GroupEventNotifierUpdate(GroupEventNotifierSave):
|
||||||
id: UUID4
|
id: UUID4
|
||||||
apprise_url: str = None
|
apprise_url: NoneStr = None
|
||||||
|
|
||||||
|
|
||||||
class GroupEventNotifierOut(CamelModel):
|
class GroupEventNotifierOut(CamelModel):
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from fastapi_camelcase import CamelModel
|
from fastapi_camelcase import CamelModel
|
||||||
|
from pydantic import NoneStr
|
||||||
|
|
||||||
|
|
||||||
class CreateInviteToken(CamelModel):
|
class CreateInviteToken(CamelModel):
|
||||||
@ -29,4 +30,4 @@ class EmailInvitation(CamelModel):
|
|||||||
|
|
||||||
class EmailInitationResponse(CamelModel):
|
class EmailInitationResponse(CamelModel):
|
||||||
success: bool
|
success: bool
|
||||||
error: str = None
|
error: NoneStr = None
|
||||||
|
@ -18,7 +18,7 @@ def mapper(source: U, dest: T, **_) -> T:
|
|||||||
return dest
|
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 = {field: getattr(source, field) for field in source.__fields__ if field in dest.__fields__}
|
||||||
create_data.update(kwargs or {})
|
create_data.update(kwargs or {})
|
||||||
return dest(**create_data)
|
return dest(**create_data)
|
||||||
|
@ -3,13 +3,13 @@ from .recipe import *
|
|||||||
from .recipe_asset import *
|
from .recipe_asset import *
|
||||||
from .recipe_bulk_actions import *
|
from .recipe_bulk_actions import *
|
||||||
from .recipe_category import *
|
from .recipe_category import *
|
||||||
from .recipe_comments import *
|
from .recipe_comments import * # type: ignore
|
||||||
from .recipe_image_types import *
|
from .recipe_image_types import *
|
||||||
from .recipe_ingredient import *
|
from .recipe_ingredient import *
|
||||||
from .recipe_notes import *
|
from .recipe_notes import *
|
||||||
from .recipe_nutrition import *
|
from .recipe_nutrition import *
|
||||||
from .recipe_settings import *
|
from .recipe_settings import *
|
||||||
from .recipe_share_token import *
|
from .recipe_share_token import * # type: ignore
|
||||||
from .recipe_step import *
|
from .recipe_step import *
|
||||||
from .recipe_tool import *
|
from .recipe_tool import *
|
||||||
from .request_helpers import *
|
from .request_helpers import *
|
||||||
|
@ -7,6 +7,8 @@ from uuid import UUID, uuid4
|
|||||||
from fastapi_camelcase import CamelModel
|
from fastapi_camelcase import CamelModel
|
||||||
from pydantic import UUID4, Field
|
from pydantic import UUID4, Field
|
||||||
|
|
||||||
|
from mealie.schema._mealie.types import NoneFloat
|
||||||
|
|
||||||
|
|
||||||
class UnitFoodBase(CamelModel):
|
class UnitFoodBase(CamelModel):
|
||||||
name: str
|
name: str
|
||||||
@ -23,7 +25,7 @@ class SaveIngredientFood(CreateIngredientFood):
|
|||||||
|
|
||||||
class IngredientFood(CreateIngredientFood):
|
class IngredientFood(CreateIngredientFood):
|
||||||
id: UUID4
|
id: UUID4
|
||||||
label: MultiPurposeLabelSummary = None
|
label: Optional[MultiPurposeLabelSummary] = None
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
orm_mode = True
|
orm_mode = True
|
||||||
@ -63,12 +65,12 @@ class RecipeIngredient(CamelModel):
|
|||||||
|
|
||||||
|
|
||||||
class IngredientConfidence(CamelModel):
|
class IngredientConfidence(CamelModel):
|
||||||
average: float = None
|
average: NoneFloat = None
|
||||||
comment: float = None
|
comment: NoneFloat = None
|
||||||
name: float = None
|
name: NoneFloat = None
|
||||||
unit: float = None
|
unit: NoneFloat = None
|
||||||
quantity: float = None
|
quantity: NoneFloat = None
|
||||||
food: float = None
|
food: NoneFloat = None
|
||||||
|
|
||||||
|
|
||||||
class ParsedIngredient(CamelModel):
|
class ParsedIngredient(CamelModel):
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from typing import List
|
import typing
|
||||||
|
|
||||||
from fastapi_camelcase import CamelModel
|
from fastapi_camelcase import CamelModel
|
||||||
from pydantic import UUID4
|
from pydantic import UUID4
|
||||||
@ -22,7 +22,7 @@ class RecipeTool(RecipeToolCreate):
|
|||||||
|
|
||||||
|
|
||||||
class RecipeToolResponse(RecipeTool):
|
class RecipeToolResponse(RecipeTool):
|
||||||
recipes: List["Recipe"] = []
|
recipes: typing.List["Recipe"] = []
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
orm_mode = True
|
orm_mode = True
|
||||||
|
@ -11,4 +11,4 @@ class Token(BaseModel):
|
|||||||
|
|
||||||
class TokenData(BaseModel):
|
class TokenData(BaseModel):
|
||||||
user_id: Optional[UUID4]
|
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
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
from fastapi_camelcase import CamelModel
|
from fastapi_camelcase import CamelModel
|
||||||
from pydantic import validator
|
from pydantic import validator
|
||||||
from pydantic.types import constr
|
from pydantic.types import NoneStr, constr
|
||||||
|
|
||||||
|
|
||||||
class CreateUserRegistration(CamelModel):
|
class CreateUserRegistration(CamelModel):
|
||||||
group: str = None
|
group: NoneStr = None
|
||||||
group_token: str = None
|
group_token: NoneStr = None
|
||||||
email: constr(to_lower=True, strip_whitespace=True)
|
email: constr(to_lower=True, strip_whitespace=True) # type: ignore
|
||||||
username: constr(to_lower=True, strip_whitespace=True)
|
username: constr(to_lower=True, strip_whitespace=True) # type: ignore
|
||||||
password: str
|
password: str
|
||||||
password_confirm: str
|
password_confirm: str
|
||||||
advanced: bool = False
|
advanced: bool = False
|
||||||
|
@ -53,7 +53,7 @@ class GroupBase(CamelModel):
|
|||||||
class UserBase(CamelModel):
|
class UserBase(CamelModel):
|
||||||
username: Optional[str]
|
username: Optional[str]
|
||||||
full_name: Optional[str] = None
|
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
|
admin: bool = False
|
||||||
group: Optional[str]
|
group: Optional[str]
|
||||||
advanced: bool = False
|
advanced: bool = False
|
||||||
@ -107,7 +107,7 @@ class UserOut(UserBase):
|
|||||||
|
|
||||||
|
|
||||||
class UserFavorites(UserBase):
|
class UserFavorites(UserBase):
|
||||||
favorite_recipes: list[RecipeSummary] = []
|
favorite_recipes: list[RecipeSummary] = [] # type: ignore
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
orm_mode = True
|
orm_mode = True
|
||||||
|
@ -39,7 +39,7 @@ class ExportDatabase:
|
|||||||
try:
|
try:
|
||||||
self.templates = [app_dirs.TEMPLATE_DIR.joinpath(x) for x in templates]
|
self.templates = [app_dirs.TEMPLATE_DIR.joinpath(x) for x in templates]
|
||||||
except Exception:
|
except Exception:
|
||||||
self.templates = False
|
self.templates = []
|
||||||
logger.info("No Jinja2 Templates Registered for Export")
|
logger.info("No Jinja2 Templates Registered for Export")
|
||||||
|
|
||||||
required_dirs = [
|
required_dirs = [
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import json
|
import json
|
||||||
import shutil
|
import shutil
|
||||||
import zipfile
|
import zipfile
|
||||||
|
from collections.abc import Callable
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Callable
|
|
||||||
|
|
||||||
from pydantic.main import BaseModel
|
from pydantic.main import BaseModel
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
@ -140,7 +140,7 @@ class ImportDatabase:
|
|||||||
|
|
||||||
if image_dir.exists(): # Migrate from before v0.5.0
|
if image_dir.exists(): # Migrate from before v0.5.0
|
||||||
for image in image_dir.iterdir():
|
for image in image_dir.iterdir():
|
||||||
item: Recipe = successful_imports.get(image.stem)
|
item: Recipe = successful_imports.get(image.stem) # type: ignore
|
||||||
|
|
||||||
if item:
|
if item:
|
||||||
dest_dir = item.image_dir
|
dest_dir = item.image_dir
|
||||||
@ -294,7 +294,7 @@ def import_database(
|
|||||||
settings_report = import_session.import_settings() if import_settings else []
|
settings_report = import_session.import_settings() if import_settings else []
|
||||||
group_report = import_session.import_groups() if import_groups else []
|
group_report = import_session.import_groups() if import_groups else []
|
||||||
user_report = import_session.import_users() if import_users else []
|
user_report = import_session.import_users() if import_users else []
|
||||||
notification_report = []
|
notification_report: list = []
|
||||||
|
|
||||||
import_session.clean_up()
|
import_session.clean_up()
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ from fastapi.encoders import jsonable_encoder
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from sqlalchemy import MetaData, create_engine
|
from sqlalchemy import MetaData, create_engine
|
||||||
from sqlalchemy.engine import base
|
from sqlalchemy.engine import base
|
||||||
from sqlalchemy.orm import Session, sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
from mealie.services._base_service import BaseService
|
from mealie.services._base_service import BaseService
|
||||||
|
|
||||||
@ -122,8 +122,6 @@ class AlchemyExporter(BaseService):
|
|||||||
"""Drops all data from the database"""
|
"""Drops all data from the database"""
|
||||||
self.meta.reflect(bind=self.engine)
|
self.meta.reflect(bind=self.engine)
|
||||||
with self.session_maker() as session:
|
with self.session_maker() as session:
|
||||||
session: Session
|
|
||||||
|
|
||||||
is_postgres = self.settings.DB_ENGINE == "postgres"
|
is_postgres = self.settings.DB_ENGINE == "postgres"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -23,7 +23,7 @@ class DefaultEmailSender(ABCEmailSender, BaseService):
|
|||||||
mail_from=(self.settings.SMTP_FROM_NAME, self.settings.SMTP_FROM_EMAIL),
|
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:
|
if self.settings.SMTP_TLS:
|
||||||
smtp_options["tls"] = True
|
smtp_options["tls"] = True
|
||||||
if self.settings.SMTP_USER:
|
if self.settings.SMTP_USER:
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import zipfile
|
import zipfile
|
||||||
from abc import abstractmethod, abstractproperty
|
from abc import abstractmethod, abstractproperty
|
||||||
|
from collections.abc import Iterator
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Callable, Iterator, Optional
|
from typing import Callable, Optional
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
@ -27,7 +28,7 @@ class ExportedItem:
|
|||||||
|
|
||||||
|
|
||||||
class ABCExporter(BaseService):
|
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:
|
def __init__(self, db: AllRepositories, group_id: UUID) -> None:
|
||||||
self.logger = get_logger()
|
self.logger = get_logger()
|
||||||
@ -47,8 +48,7 @@ class ABCExporter(BaseService):
|
|||||||
def _post_export_hook(self, _: BaseModel) -> None:
|
def _post_export_hook(self, _: BaseModel) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
def export(self, zip: zipfile.ZipFile) -> list[ReportEntryCreate]: # type: ignore
|
||||||
def export(self, zip: zipfile.ZipFile) -> list[ReportEntryCreate]:
|
|
||||||
"""
|
"""
|
||||||
Export takes in a zip file and exports the recipes to it. Note that the zip
|
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.
|
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
|
zip (zipfile.ZipFile): Zip file destination
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list[ReportEntryCreate]: [description] ???!?!
|
list[ReportEntryCreate]:
|
||||||
"""
|
"""
|
||||||
self.write_dir_to_zip = self.write_dir_to_zip_func(zip)
|
self.write_dir_to_zip = self.write_dir_to_zip_func(zip)
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from typing import Iterator
|
from collections.abc import Iterator
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from mealie.repos.all_repositories import AllRepositories
|
from mealie.repos.all_repositories import AllRepositories
|
||||||
@ -37,5 +37,5 @@ class RecipeExporter(ABCExporter):
|
|||||||
"""Copy recipe directory contents into the zip folder"""
|
"""Copy recipe directory contents into the zip folder"""
|
||||||
recipe_dir = item.directory
|
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"})
|
self.write_dir_to_zip(recipe_dir, f"{self.destination_dir}/{item.slug}", {".json"})
|
||||||
|
@ -168,7 +168,7 @@ class ShoppingListService:
|
|||||||
found = False
|
found = False
|
||||||
|
|
||||||
for ref in item.recipe_references:
|
for ref in item.recipe_references:
|
||||||
remove_qty = 0
|
remove_qty = 0.0
|
||||||
|
|
||||||
if ref.recipe_id == recipe_id:
|
if ref.recipe_id == recipe_id:
|
||||||
self.list_item_refs.delete(ref.id)
|
self.list_item_refs.delete(ref.id)
|
||||||
@ -199,4 +199,4 @@ class ShoppingListService:
|
|||||||
break
|
break
|
||||||
|
|
||||||
# Save Changes
|
# Save Changes
|
||||||
return self.shopping_lists.get(shopping_list.id)
|
return self.shopping_lists.get_one(shopping_list.id)
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Tuple
|
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from pydantic import UUID4
|
from pydantic import UUID4
|
||||||
@ -94,9 +93,10 @@ class BaseMigrator(BaseService):
|
|||||||
self._create_report(report_name)
|
self._create_report(report_name)
|
||||||
self._migrate()
|
self._migrate()
|
||||||
self._save_all_entries()
|
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
|
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
|
database in a predictable way. If an error occurs the session is rolled back
|
||||||
|
@ -67,6 +67,6 @@ class NextcloudMigrator(BaseMigrator):
|
|||||||
|
|
||||||
for slug, recipe_id, status in all_statuses:
|
for slug, recipe_id, status in all_statuses:
|
||||||
if status:
|
if status:
|
||||||
nc_dir: NextcloudDir = nextcloud_dirs[slug]
|
nc_dir = nextcloud_dirs[slug]
|
||||||
if nc_dir.image:
|
if nc_dir.image:
|
||||||
import_image(nc_dir.image, recipe_id)
|
import_image(nc_dir.image, recipe_id)
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from collections.abc import Iterable
|
||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
|
|
||||||
from pydantic import UUID4, BaseModel
|
from pydantic import UUID4, BaseModel
|
||||||
@ -14,14 +15,14 @@ T = TypeVar("T", bound=BaseModel)
|
|||||||
|
|
||||||
|
|
||||||
class DatabaseMigrationHelpers:
|
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.group_id = group_id
|
||||||
self.user_id = user_id
|
self.user_id = user_id
|
||||||
self.session = session
|
self.session = session
|
||||||
self.db = db
|
self.db = db
|
||||||
|
|
||||||
def _get_or_set_generic(
|
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]:
|
) -> list[T]:
|
||||||
"""
|
"""
|
||||||
Utility model for getting or setting categories or tags. This will only work for those two cases.
|
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())
|
items_out.append(item_model.dict())
|
||||||
return items_out
|
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(
|
return self._get_or_set_generic(
|
||||||
self.db.categories.by_group(self.group_id),
|
self.db.categories.by_group(self.group_id),
|
||||||
categories,
|
categories,
|
||||||
@ -55,7 +56,7 @@ class DatabaseMigrationHelpers:
|
|||||||
CategoryOut,
|
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(
|
return self._get_or_set_generic(
|
||||||
self.db.tags.by_group(self.group_id),
|
self.db.tags.by_group(self.group_id),
|
||||||
tags,
|
tags,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from typing import Callable, Optional
|
from collections.abc import Callable
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ def move_parens_to_end(ing_str) -> str:
|
|||||||
If no parentheses are found, the string is returned unchanged.
|
If no parentheses are found, the string is returned unchanged.
|
||||||
"""
|
"""
|
||||||
if re.match(compiled_match, ing_str):
|
if re.match(compiled_match, ing_str):
|
||||||
match = re.search(compiled_search, ing_str)
|
if match := re.search(compiled_search, ing_str):
|
||||||
start = match.start()
|
start = match.start()
|
||||||
end = match.end()
|
end = match.end()
|
||||||
ing_str = ing_str[:start] + ing_str[end:] + " " + ing_str[start:end]
|
ing_str = ing_str[:start] + ing_str[end:] + " " + ing_str[start:end]
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import string
|
import string
|
||||||
import unicodedata
|
import unicodedata
|
||||||
from typing import Tuple
|
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
@ -10,7 +9,7 @@ from .._helpers import check_char, move_parens_to_end
|
|||||||
class BruteParsedIngredient(BaseModel):
|
class BruteParsedIngredient(BaseModel):
|
||||||
food: str = ""
|
food: str = ""
|
||||||
note: str = ""
|
note: str = ""
|
||||||
amount: float = ""
|
amount: float = 1.0
|
||||||
unit: str = ""
|
unit: str = ""
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
@ -31,7 +30,7 @@ def parse_fraction(x):
|
|||||||
raise ValueError
|
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:
|
def keep_looping(ing_str, end) -> bool:
|
||||||
"""
|
"""
|
||||||
Checks if:
|
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:
|
if check_char(ing_str[end], ".", ",", "/") and end + 1 < len(ing_str) and ing_str[end + 1] in string.digits:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
amount = 0
|
return False
|
||||||
|
|
||||||
|
amount = 0.0
|
||||||
unit = ""
|
unit = ""
|
||||||
note = ""
|
note = ""
|
||||||
|
|
||||||
@ -87,7 +88,7 @@ def parse_amount(ing_str) -> Tuple[float, str, str]:
|
|||||||
return amount, unit, note
|
return amount, unit, note
|
||||||
|
|
||||||
|
|
||||||
def parse_ingredient_with_comma(tokens) -> Tuple[str, str]:
|
def parse_ingredient_with_comma(tokens) -> tuple[str, str]:
|
||||||
ingredient = ""
|
ingredient = ""
|
||||||
note = ""
|
note = ""
|
||||||
start = 0
|
start = 0
|
||||||
@ -105,7 +106,7 @@ def parse_ingredient_with_comma(tokens) -> Tuple[str, str]:
|
|||||||
return ingredient, note
|
return ingredient, note
|
||||||
|
|
||||||
|
|
||||||
def parse_ingredient(tokens) -> Tuple[str, str]:
|
def parse_ingredient(tokens) -> tuple[str, str]:
|
||||||
ingredient = ""
|
ingredient = ""
|
||||||
note = ""
|
note = ""
|
||||||
if tokens[-1].endswith(")"):
|
if tokens[-1].endswith(")"):
|
||||||
@ -132,7 +133,7 @@ def parse_ingredient(tokens) -> Tuple[str, str]:
|
|||||||
|
|
||||||
|
|
||||||
def parse(ing_str) -> BruteParsedIngredient:
|
def parse(ing_str) -> BruteParsedIngredient:
|
||||||
amount = 0
|
amount = 0.0
|
||||||
unit = ""
|
unit = ""
|
||||||
ingredient = ""
|
ingredient = ""
|
||||||
note = ""
|
note = ""
|
||||||
|
@ -5,6 +5,8 @@ from pathlib import Path
|
|||||||
|
|
||||||
from pydantic import BaseModel, validator
|
from pydantic import BaseModel, validator
|
||||||
|
|
||||||
|
from mealie.schema._mealie.types import NoneFloat
|
||||||
|
|
||||||
from . import utils
|
from . import utils
|
||||||
from .pre_processor import pre_process_string
|
from .pre_processor import pre_process_string
|
||||||
|
|
||||||
@ -14,10 +16,10 @@ MODEL_PATH = CWD / "model.crfmodel"
|
|||||||
|
|
||||||
class CRFConfidence(BaseModel):
|
class CRFConfidence(BaseModel):
|
||||||
average: float = 0.0
|
average: float = 0.0
|
||||||
comment: float = None
|
comment: NoneFloat = None
|
||||||
name: float = None
|
name: NoneFloat = None
|
||||||
unit: float = None
|
unit: NoneFloat = None
|
||||||
qty: float = None
|
qty: NoneFloat = None
|
||||||
|
|
||||||
|
|
||||||
class CRFIngredient(BaseModel):
|
class CRFIngredient(BaseModel):
|
||||||
|
@ -99,7 +99,7 @@ class NLPParser(ABCIngredientParser):
|
|||||||
return [self._crf_to_ingredient(crf_model) for crf_model in crf_models]
|
return [self._crf_to_ingredient(crf_model) for crf_model in crf_models]
|
||||||
|
|
||||||
def parse_one(self, ingredient: str) -> ParsedIngredient:
|
def parse_one(self, ingredient: str) -> ParsedIngredient:
|
||||||
items = self.parse_one([ingredient])
|
items = self.parse([ingredient])
|
||||||
return items[0]
|
return items[0]
|
||||||
|
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ class RecipeDataService(BaseService):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.exception(f"Failed to delete recipe data: {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(".", "")
|
extension = extension.replace(".", "")
|
||||||
image_path = self.dir_image.joinpath(f"original.{extension}")
|
image_path = self.dir_image.joinpath(f"original.{extension}")
|
||||||
image_path.unlink(missing_ok=True)
|
image_path.unlink(missing_ok=True)
|
||||||
@ -91,8 +91,8 @@ class RecipeDataService(BaseService):
|
|||||||
if ext not in img.IMAGE_EXTENSIONS:
|
if ext not in img.IMAGE_EXTENSIONS:
|
||||||
ext = "jpg" # Guess the extension
|
ext = "jpg" # Guess the extension
|
||||||
|
|
||||||
filename = str(self.recipe_id) + "." + ext
|
file_name = f"{str(self.recipe_id)}.{ext}"
|
||||||
filename = Recipe.directory_from_id(self.recipe_id).joinpath("images", filename)
|
file_path = Recipe.directory_from_id(self.recipe_id).joinpath("images", file_name)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r = requests.get(image_url, stream=True, headers={"User-Agent": _FIREFOX_UA})
|
r = requests.get(image_url, stream=True, headers={"User-Agent": _FIREFOX_UA})
|
||||||
@ -102,7 +102,7 @@ class RecipeDataService(BaseService):
|
|||||||
|
|
||||||
if r.status_code == 200:
|
if r.status_code == 200:
|
||||||
r.raw.decode_content = True
|
r.raw.decode_content = True
|
||||||
self.logger.info(f"File Name Suffix {filename.suffix}")
|
self.logger.info(f"File Name Suffix {file_path.suffix}")
|
||||||
self.write_image(r.raw, filename.suffix)
|
self.write_image(r.raw, file_path.suffix)
|
||||||
|
|
||||||
filename.unlink(missing_ok=True)
|
file_path.unlink(missing_ok=True)
|
||||||
|
@ -69,7 +69,6 @@ class RecipeService(BaseService):
|
|||||||
all_asset_files = [x.file_name for x in recipe.assets]
|
all_asset_files = [x.file_name for x in recipe.assets]
|
||||||
|
|
||||||
for file in recipe.asset_dir.iterdir():
|
for file in recipe.asset_dir.iterdir():
|
||||||
file: Path
|
|
||||||
if file.is_dir():
|
if file.is_dir():
|
||||||
continue
|
continue
|
||||||
if file.name not in all_asset_files:
|
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:
|
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,
|
self.user,
|
||||||
name=create_data.name,
|
name=create_data.name,
|
||||||
additional_attrs=create_data.dict(),
|
additional_attrs=create_data.dict(),
|
||||||
)
|
)
|
||||||
|
|
||||||
create_data.settings = RecipeSettings(
|
data.settings = RecipeSettings(
|
||||||
public=self.group.preferences.recipe_public,
|
public=self.group.preferences.recipe_public,
|
||||||
show_nutrition=self.group.preferences.recipe_show_nutrition,
|
show_nutrition=self.group.preferences.recipe_show_nutrition,
|
||||||
show_assets=self.group.preferences.recipe_show_assets,
|
show_assets=self.group.preferences.recipe_show_assets,
|
||||||
@ -117,7 +116,7 @@ class RecipeService(BaseService):
|
|||||||
disable_amount=self.group.preferences.recipe_disable_amount,
|
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:
|
def create_from_zip(self, archive: UploadFile, temp_path: Path) -> Recipe:
|
||||||
"""
|
"""
|
||||||
|
@ -27,7 +27,7 @@ class TemplateService(BaseService):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def templates(self) -> list:
|
def templates(self) -> dict[str, list[str]]:
|
||||||
"""
|
"""
|
||||||
Returns a list of all templates available to render.
|
Returns a list of all templates available to render.
|
||||||
"""
|
"""
|
||||||
@ -78,6 +78,8 @@ class TemplateService(BaseService):
|
|||||||
if t_type == TemplateType.zip:
|
if t_type == TemplateType.zip:
|
||||||
return self._render_zip(recipe)
|
return self._render_zip(recipe)
|
||||||
|
|
||||||
|
raise ValueError(f"Template Type '{t_type}' not found.")
|
||||||
|
|
||||||
def _render_json(self, recipe: Recipe) -> Path:
|
def _render_json(self, recipe: Recipe) -> Path:
|
||||||
"""
|
"""
|
||||||
Renders a JSON file in a temporary directory and returns
|
Renders a JSON file in a temporary directory and returns
|
||||||
@ -98,18 +100,18 @@ class TemplateService(BaseService):
|
|||||||
"""
|
"""
|
||||||
self.__check_temp(self._render_jinja2)
|
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():
|
if not j2_path.is_file():
|
||||||
raise FileNotFoundError(f"Template '{j2_template}' not found.")
|
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_text = f.read()
|
||||||
|
|
||||||
template = Template(template_text)
|
template = Template(template_text)
|
||||||
rendered_text = template.render(recipe=recipe.dict(by_alias=True))
|
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)
|
save_path = self.temp.joinpath(save_name)
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Callable, Tuple
|
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ class Cron:
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ScheduledFunc(BaseModel):
|
class ScheduledFunc(BaseModel):
|
||||||
id: Tuple[str, int]
|
id: tuple[str, int]
|
||||||
name: str
|
name: str
|
||||||
hour: int
|
hour: int
|
||||||
minutes: int
|
minutes: int
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from typing import Callable, Iterable
|
from collections.abc import Callable, Iterable
|
||||||
|
|
||||||
from mealie.core import root_logger
|
from mealie.core import root_logger
|
||||||
|
|
||||||
|
@ -49,30 +49,26 @@ class SchedulerService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def add_cron_job(job_func: ScheduledFunc):
|
def add_cron_job(job_func: ScheduledFunc):
|
||||||
SchedulerService.scheduler.add_job(
|
SchedulerService.scheduler.add_job( # type: ignore
|
||||||
job_func.callback,
|
job_func.callback,
|
||||||
trigger="cron",
|
trigger="cron",
|
||||||
name=job_func.id,
|
name=job_func.id,
|
||||||
hour=job_func.hour,
|
hour=job_func.hour,
|
||||||
minute=job_func.minutes,
|
minute=job_func.minutes,
|
||||||
max_instances=job_func.max_instances,
|
max_instances=job_func.max_instances, # type: ignore
|
||||||
replace_existing=job_func.replace_existing,
|
replace_existing=job_func.replace_existing,
|
||||||
args=job_func.args,
|
args=job_func.args,
|
||||||
)
|
)
|
||||||
|
|
||||||
# SchedulerService._job_store[job_func.id] = job_func
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def update_cron_job(job_func: ScheduledFunc):
|
def update_cron_job(job_func: ScheduledFunc):
|
||||||
SchedulerService.scheduler.reschedule_job(
|
SchedulerService.scheduler.reschedule_job( # type: ignore
|
||||||
job_func.id,
|
job_func.id,
|
||||||
trigger="cron",
|
trigger="cron",
|
||||||
hour=job_func.hour,
|
hour=job_func.hour,
|
||||||
minute=job_func.minutes,
|
minute=job_func.minutes,
|
||||||
)
|
)
|
||||||
|
|
||||||
# SchedulerService._job_store[job_func.id] = job_func
|
|
||||||
|
|
||||||
|
|
||||||
def _scheduled_task_wrapper(callable):
|
def _scheduled_task_wrapper(callable):
|
||||||
try:
|
try:
|
||||||
|
@ -39,7 +39,8 @@ def purge_excess_files() -> None:
|
|||||||
limit = datetime.datetime.now() - datetime.timedelta(minutes=ONE_DAY_AS_MINUTES * 2)
|
limit = datetime.datetime.now() - datetime.timedelta(minutes=ONE_DAY_AS_MINUTES * 2)
|
||||||
|
|
||||||
for file in directories.GROUPS_DIR.glob("**/export/*.zip"):
|
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()
|
file.unlink()
|
||||||
logger.info(f"excess group file removed '{file}'")
|
logger.info(f"excess group file removed '{file}'")
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ def post_webhooks(webhook_id: int, session: Session = None):
|
|||||||
if not todays_recipe:
|
if not todays_recipe:
|
||||||
return
|
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)
|
response = requests.post(webhook.url, json=payload)
|
||||||
|
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
|
@ -2,7 +2,7 @@ import html
|
|||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import List, Optional
|
from typing import Optional
|
||||||
|
|
||||||
from slugify import slugify
|
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["recipeIngredient"] = ingredient(recipe_data.get("recipeIngredient"))
|
||||||
recipe_data["recipeInstructions"] = instructions(recipe_data.get("recipeInstructions"))
|
recipe_data["recipeInstructions"] = instructions(recipe_data.get("recipeInstructions"))
|
||||||
recipe_data["image"] = image(recipe_data.get("image"))
|
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
|
recipe_data["orgURL"] = url
|
||||||
|
|
||||||
return recipe_data
|
return recipe_data
|
||||||
@ -127,7 +127,7 @@ def image(image=None) -> str:
|
|||||||
raise Exception(f"Unrecognised image URL format: {image}")
|
raise Exception(f"Unrecognised image URL format: {image}")
|
||||||
|
|
||||||
|
|
||||||
def instructions(instructions) -> List[dict]:
|
def instructions(instructions) -> list[dict]:
|
||||||
try:
|
try:
|
||||||
instructions = json.loads(instructions)
|
instructions = json.loads(instructions)
|
||||||
except Exception:
|
except Exception:
|
||||||
@ -162,7 +162,8 @@ def instructions(instructions) -> List[dict]:
|
|||||||
sectionSteps = []
|
sectionSteps = []
|
||||||
for step in instructions:
|
for step in instructions:
|
||||||
if step["@type"] == "HowToSection":
|
if step["@type"] == "HowToSection":
|
||||||
[sectionSteps.append(item) for item in step["itemListElement"]]
|
for sectionStep in step["itemListElement"]:
|
||||||
|
sectionSteps.append(sectionStep)
|
||||||
|
|
||||||
if len(sectionSteps) > 0:
|
if len(sectionSteps) > 0:
|
||||||
return [{"text": _instruction(step["text"])} for step in sectionSteps if step["@type"] == "HowToStep"]
|
return [{"text": _instruction(step["text"])} for step in sectionSteps if step["@type"] == "HowToStep"]
|
||||||
@ -183,6 +184,8 @@ def instructions(instructions) -> List[dict]:
|
|||||||
else:
|
else:
|
||||||
raise Exception(f"Unrecognised instruction format: {instructions}")
|
raise Exception(f"Unrecognised instruction format: {instructions}")
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
def _instruction(line) -> str:
|
def _instruction(line) -> str:
|
||||||
if isinstance(line, dict):
|
if isinstance(line, dict):
|
||||||
@ -199,7 +202,7 @@ def _instruction(line) -> str:
|
|||||||
return clean_line
|
return clean_line
|
||||||
|
|
||||||
|
|
||||||
def ingredient(ingredients: list) -> str:
|
def ingredient(ingredients: list | None) -> list[str]:
|
||||||
if ingredients:
|
if ingredients:
|
||||||
return [clean_string(ing) for ing in ingredients]
|
return [clean_string(ing) for ing in ingredients]
|
||||||
else:
|
else:
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
from typing import Type
|
|
||||||
|
|
||||||
from mealie.schema.recipe.recipe import Recipe
|
from mealie.schema.recipe.recipe import Recipe
|
||||||
|
|
||||||
from .scraper_strategies import ABCScraperStrategy, RecipeScraperOpenGraph, RecipeScraperPackage
|
from .scraper_strategies import ABCScraperStrategy, RecipeScraperOpenGraph, RecipeScraperPackage
|
||||||
@ -11,9 +9,9 @@ class RecipeScraper:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# List of recipe scrapers. Note that order matters
|
# 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:
|
if scrapers is None:
|
||||||
scrapers = [
|
scrapers = [
|
||||||
RecipeScraperPackage,
|
RecipeScraperPackage,
|
||||||
@ -27,8 +25,8 @@ class RecipeScraper:
|
|||||||
Scrapes a recipe from the web.
|
Scrapes a recipe from the web.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
for scraper in self.scrapers:
|
for scraper_type in self.scrapers:
|
||||||
scraper = scraper(url)
|
scraper = scraper_type(url)
|
||||||
recipe = scraper.parse()
|
recipe = scraper.parse()
|
||||||
|
|
||||||
if recipe is not None:
|
if recipe is not None:
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Any, Callable, Tuple
|
from typing import Any, Callable
|
||||||
|
|
||||||
import extruct
|
import extruct
|
||||||
import requests
|
import requests
|
||||||
@ -26,7 +26,7 @@ class ABCScraperStrategy(ABC):
|
|||||||
self.url = url
|
self.url = url
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def parse(self, recipe_url: str) -> Recipe | None:
|
def parse(self) -> Recipe | None:
|
||||||
"""Parse a recipe from a web URL.
|
"""Parse a recipe from a web URL.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -40,7 +40,7 @@ class ABCScraperStrategy(ABC):
|
|||||||
|
|
||||||
class RecipeScraperPackage(ABCScraperStrategy):
|
class RecipeScraperPackage(ABCScraperStrategy):
|
||||||
def clean_scraper(self, scraped_data: SchemaScraperFactory.SchemaScraper, url: str) -> Recipe:
|
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
|
value = default
|
||||||
try:
|
try:
|
||||||
value = func_call()
|
value = func_call()
|
||||||
@ -143,7 +143,7 @@ class RecipeScraperOpenGraph(ABCScraperStrategy):
|
|||||||
def get_html(self) -> str:
|
def get_html(self) -> str:
|
||||||
return requests.get(self.url).text
|
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.
|
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:
|
def og_field(properties: dict, field_name: str) -> str:
|
||||||
return next((val for name, val in properties if name == field_name), None)
|
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})
|
return list({val for name, val in properties if name == field_name})
|
||||||
|
|
||||||
base_url = get_base_url(html, self.url)
|
base_url = get_base_url(html, self.url)
|
||||||
@ -159,7 +159,7 @@ class RecipeScraperOpenGraph(ABCScraperStrategy):
|
|||||||
try:
|
try:
|
||||||
properties = data["opengraph"][0]["properties"]
|
properties = data["opengraph"][0]["properties"]
|
||||||
except Exception:
|
except Exception:
|
||||||
return
|
return None
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"name": og_field(properties, "og:title"),
|
"name": og_field(properties, "og:title"),
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
|
from collections.abc import Callable
|
||||||
from random import getrandbits
|
from random import getrandbits
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from typing import Any, Callable
|
from typing import Any
|
||||||
|
|
||||||
from fastapi import BackgroundTasks
|
from fastapi import BackgroundTasks
|
||||||
from pydantic import UUID4
|
from pydantic import UUID4
|
||||||
|
@ -16,13 +16,13 @@ class PasswordResetService(BaseService):
|
|||||||
self.db = get_repositories(session)
|
self.db = get_repositories(session)
|
||||||
super().__init__()
|
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")
|
user = self.db.users.get_one(email, "email")
|
||||||
|
|
||||||
if user is None:
|
if user is None:
|
||||||
logger.error(f"failed to create password reset for {email=}: user doesn't exists")
|
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
|
# 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
|
# Create Reset Token
|
||||||
token = url_safe_token()
|
token = url_safe_token()
|
||||||
|
@ -66,7 +66,7 @@ class RegistrationService:
|
|||||||
token_entry = self.repos.group_invite_tokens.get_one(registration.group_token)
|
token_entry = self.repos.group_invite_tokens.get_one(registration.group_token)
|
||||||
if not token_entry:
|
if not token_entry:
|
||||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, {"message": "Invalid group token"})
|
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:
|
else:
|
||||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, {"message": "Missing group"})
|
raise HTTPException(status.HTTP_400_BAD_REQUEST, {"message": "Missing group"})
|
||||||
|
|
||||||
|
205
poetry.lock
generated
205
poetry.lock
generated
@ -214,6 +214,14 @@ python-versions = "*"
|
|||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
pycparser = "*"
|
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]]
|
[[package]]
|
||||||
name = "chardet"
|
name = "chardet"
|
||||||
version = "4.0.0"
|
version = "4.0.0"
|
||||||
@ -303,6 +311,14 @@ python-versions = ">=3.6"
|
|||||||
docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
|
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"]
|
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]]
|
[[package]]
|
||||||
name = "ecdsa"
|
name = "ecdsa"
|
||||||
version = "0.17.0"
|
version = "0.17.0"
|
||||||
@ -386,6 +402,18 @@ python-versions = ">=3.6"
|
|||||||
pydantic = "*"
|
pydantic = "*"
|
||||||
pyhumps = "*"
|
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]]
|
[[package]]
|
||||||
name = "flake8"
|
name = "flake8"
|
||||||
version = "4.0.1"
|
version = "4.0.1"
|
||||||
@ -499,6 +527,17 @@ python-versions = "*"
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
test = ["Cython (==0.29.22)"]
|
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]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "3.3"
|
version = "3.3"
|
||||||
@ -710,6 +749,24 @@ category = "dev"
|
|||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
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]]
|
[[package]]
|
||||||
name = "mypy-extensions"
|
name = "mypy-extensions"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
@ -718,6 +775,14 @@ category = "dev"
|
|||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nodeenv"
|
||||||
|
version = "1.6.0"
|
||||||
|
description = "Node.js virtual environment builder"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oauthlib"
|
name = "oauthlib"
|
||||||
version = "3.1.1"
|
version = "3.1.1"
|
||||||
@ -807,6 +872,22 @@ python-versions = ">=3.6"
|
|||||||
dev = ["pre-commit", "tox"]
|
dev = ["pre-commit", "tox"]
|
||||||
testing = ["pytest", "pytest-benchmark"]
|
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]]
|
[[package]]
|
||||||
name = "premailer"
|
name = "premailer"
|
||||||
version = "3.10.0"
|
version = "3.10.0"
|
||||||
@ -1336,6 +1417,41 @@ category = "dev"
|
|||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
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]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
version = "4.0.1"
|
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)"]
|
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)"]
|
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]]
|
[[package]]
|
||||||
name = "w3lib"
|
name = "w3lib"
|
||||||
version = "1.22.0"
|
version = "1.22.0"
|
||||||
@ -1488,7 +1622,7 @@ pgsql = ["psycopg2-binary"]
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "1.1"
|
||||||
python-versions = "^3.10"
|
python-versions = "^3.10"
|
||||||
content-hash = "00e37f7569d999689984b41bb0085f86e0e902eb1a7cae32d408b079db0ae8d8"
|
content-hash = "4fba071019a62f5d75e7c9a297a7815b2fed6486bb3616b5029a6fb08001761f"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
aiofiles = [
|
aiofiles = [
|
||||||
@ -1608,6 +1742,10 @@ cffi = [
|
|||||||
{file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"},
|
{file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"},
|
||||||
{file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"},
|
{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 = [
|
chardet = [
|
||||||
{file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"},
|
{file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"},
|
||||||
{file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"},
|
{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-py3-none-any.whl", hash = "sha256:0cf1f6086b020dee18048ff3999339499f725934017ef9ae2cd5bb77f9ab5f46"},
|
||||||
{file = "cssutils-2.3.0.tar.gz", hash = "sha256:b2d3b16047caae82e5c590036935bafa1b621cf45c2f38885af4be4838f0fd00"},
|
{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 = [
|
ecdsa = [
|
||||||
{file = "ecdsa-0.17.0-py2.py3-none-any.whl", hash = "sha256:5cf31d5b33743abe0dfc28999036c849a69d548f994b535e527ee3cb7f3ef676"},
|
{file = "ecdsa-0.17.0-py2.py3-none-any.whl", hash = "sha256:5cf31d5b33743abe0dfc28999036c849a69d548f994b535e527ee3cb7f3ef676"},
|
||||||
{file = "ecdsa-0.17.0.tar.gz", hash = "sha256:b9f500bb439e4153d0330610f5d26baaf18d17b8ced1bc54410d189385ea68aa"},
|
{file = "ecdsa-0.17.0.tar.gz", hash = "sha256:b9f500bb439e4153d0330610f5d26baaf18d17b8ced1bc54410d189385ea68aa"},
|
||||||
@ -1713,6 +1855,10 @@ fastapi = [
|
|||||||
fastapi-camelcase = [
|
fastapi-camelcase = [
|
||||||
{file = "fastapi_camelcase-1.0.5.tar.gz", hash = "sha256:2cee005fb1b75649491b9f7cfccc640b12f028eb88084565f7d8cf415192026a"},
|
{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 = [
|
flake8 = [
|
||||||
{file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"},
|
{file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"},
|
||||||
{file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"},
|
{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-cp39-cp39-win_amd64.whl", hash = "sha256:9abd788465aa46a0f288bd3a99e53edd184177d6379e2098fd6097bb359ad9d6"},
|
||||||
{file = "httptools-0.1.2.tar.gz", hash = "sha256:07659649fe6b3948b6490825f89abe5eb1cec79ebfaaa0b4bf30f3f33f3c2ba8"},
|
{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 = [
|
idna = [
|
||||||
{file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
|
{file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
|
||||||
{file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
|
{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.tar.gz", hash = "sha256:bfd24dfdef7b41c312ede42648f9eb83476ea168ec163b613f9abd12bbfddba2"},
|
||||||
{file = "mkdocs_material_extensions-1.0.3-py3-none-any.whl", hash = "sha256:a82b70e533ce060b2a5d9eb2bc2e1be201cf61f901f93704b4acf6e3d5983a44"},
|
{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 = [
|
mypy-extensions = [
|
||||||
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
|
{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"},
|
{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 = [
|
oauthlib = [
|
||||||
{file = "oauthlib-3.1.1-py2.py3-none-any.whl", hash = "sha256:42bf6354c2ed8c6acb54d971fce6f88193d97297e18602a3a886603f9d7730cc"},
|
{file = "oauthlib-3.1.1-py2.py3-none-any.whl", hash = "sha256:42bf6354c2ed8c6acb54d971fce6f88193d97297e18602a3a886603f9d7730cc"},
|
||||||
{file = "oauthlib-3.1.1.tar.gz", hash = "sha256:8f0215fcc533dd8dd1bee6f4c412d4f0cd7297307d43ac61666389e3bc3198a3"},
|
{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-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
|
||||||
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
|
{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 = [
|
premailer = [
|
||||||
{file = "premailer-3.10.0-py2.py3-none-any.whl", hash = "sha256:021b8196364d7df96d04f9ade51b794d0b77bcc19e998321c515633a2273be1a"},
|
{file = "premailer-3.10.0-py2.py3-none-any.whl", hash = "sha256:021b8196364d7df96d04f9ade51b794d0b77bcc19e998321c515633a2273be1a"},
|
||||||
{file = "premailer-3.10.0.tar.gz", hash = "sha256:d1875a8411f5dc92b53ef9f193db6c0f879dc378d618e0ad292723e388bfe4c2"},
|
{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-py3-none-any.whl", hash = "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"},
|
||||||
{file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"},
|
{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 = [
|
typing-extensions = [
|
||||||
{file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"},
|
{file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"},
|
||||||
{file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"},
|
{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-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e5f2e2ff51aefe6c19ee98af12b4ae61f5be456cd24396953244a30880ad861"},
|
||||||
{file = "uvloop-0.16.0.tar.gz", hash = "sha256:f74bc20c7b67d1c27c72601c78cf95be99d5c2cdd4514502b4f3eb0933ff1228"},
|
{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 = [
|
w3lib = [
|
||||||
{file = "w3lib-1.22.0-py2.py3-none-any.whl", hash = "sha256:0161d55537063e00d95a241663ede3395c4c6d7b777972ba2fd58bbab2001e53"},
|
{file = "w3lib-1.22.0-py2.py3-none-any.whl", hash = "sha256:0161d55537063e00d95a241663ede3395c4c6d7b777972ba2fd58bbab2001e53"},
|
||||||
{file = "w3lib-1.22.0.tar.gz", hash = "sha256:0ad6d0203157d61149fd45aaed2e24f53902989c32fc1dccc2e2bfba371560df"},
|
{file = "w3lib-1.22.0.tar.gz", hash = "sha256:0ad6d0203157d61149fd45aaed2e24f53902989c32fc1dccc2e2bfba371560df"},
|
||||||
|
@ -55,6 +55,12 @@ isort = "^5.9.3"
|
|||||||
flake8-print = "^4.0.0"
|
flake8-print = "^4.0.0"
|
||||||
black = "^21.12b0"
|
black = "^21.12b0"
|
||||||
coveragepy-lcov = "^0.1.1"
|
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]
|
[build-system]
|
||||||
requires = ["poetry-core>=1.0.0"]
|
requires = ["poetry-core>=1.0.0"]
|
||||||
@ -92,3 +98,9 @@ skip_empty = true
|
|||||||
|
|
||||||
[tool.poetry.extras]
|
[tool.poetry.extras]
|
||||||
pgsql = ["psycopg2-binary"]
|
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
|
||||||
|
3
tests/fixtures/fixture_users.py
vendored
3
tests/fixtures/fixture_users.py
vendored
@ -1,5 +1,4 @@
|
|||||||
import json
|
import json
|
||||||
from typing import Tuple
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from pytest import fixture
|
from pytest import fixture
|
||||||
@ -104,7 +103,7 @@ def unique_user(api_client: TestClient, api_routes: utils.AppRoutes):
|
|||||||
|
|
||||||
|
|
||||||
@fixture(scope="module")
|
@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()
|
group_name = utils.random_string()
|
||||||
# Create the user
|
# Create the user
|
||||||
create_data_1 = {
|
create_data_1 = {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, Tuple, Union
|
from typing import Optional, Union
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
@ -31,7 +31,7 @@ def get_init(html_path: Path):
|
|||||||
self,
|
self,
|
||||||
url,
|
url,
|
||||||
proxies: Optional[str] = None,
|
proxies: Optional[str] = None,
|
||||||
timeout: Optional[Union[float, Tuple, None]] = None,
|
timeout: Optional[Union[float, tuple, None]] = None,
|
||||||
wild_mode: Optional[bool] = False,
|
wild_mode: Optional[bool] = False,
|
||||||
**_,
|
**_,
|
||||||
):
|
):
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Tuple
|
|
||||||
|
|
||||||
from fastapi import Response
|
from fastapi import Response
|
||||||
from fastapi.testclient import TestClient
|
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_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
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
from typing import Tuple
|
|
||||||
|
|
||||||
from requests import Response
|
from requests import Response
|
||||||
|
|
||||||
from mealie.schema.recipe.recipe import RecipeCategory
|
from mealie.schema.recipe.recipe import RecipeCategory
|
||||||
@ -27,7 +25,7 @@ class CategoryTestCase(ABCMultiTenantTestCase):
|
|||||||
|
|
||||||
return category_ids
|
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()
|
g1_item_ids = set()
|
||||||
g2_item_ids = set()
|
g2_item_ids = set()
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user