mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-02-24 08:43:11 -05:00
Compare commits
206 Commits
v1.0.0beta
...
v1.0.0beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee121a12f8 | ||
|
|
5a41e1d02d | ||
|
|
3f5aab6fae | ||
|
|
84c23765cd | ||
|
|
97d9e2a109 | ||
|
|
46161ef12b | ||
|
|
d6e8602db5 | ||
|
|
83a076bd8b | ||
|
|
c5613694d9 | ||
|
|
2e11e57e0a | ||
|
|
ce4315f971 | ||
|
|
fcc5d99d40 | ||
|
|
9f6bcc83d5 | ||
|
|
0d953da5b0 | ||
|
|
a56bedb022 | ||
|
|
558789cd02 | ||
|
|
bc99884438 | ||
|
|
0bb1b6500f | ||
|
|
d53e78317d | ||
|
|
c9929745f8 | ||
|
|
a59d1ae2f9 | ||
|
|
9ecef4c25f | ||
|
|
a8f0fb14a7 | ||
|
|
e516a2e801 | ||
|
|
a3904c45d8 | ||
|
|
61ba16ad26 | ||
|
|
2914553084 | ||
|
|
1062ad9d97 | ||
|
|
ed7849d92d | ||
|
|
07b2692927 | ||
|
|
03be764ce7 | ||
|
|
5cfa276475 | ||
|
|
f5440652a8 | ||
|
|
e088719ddc | ||
|
|
332870f608 | ||
|
|
8280099dc5 | ||
|
|
52e07e4f21 | ||
|
|
a3a66055e0 | ||
|
|
eca54e6474 | ||
|
|
6159b55be9 | ||
|
|
47e7783ef5 | ||
|
|
2c04393501 | ||
|
|
7aa7945647 | ||
|
|
ef481a6a9c | ||
|
|
796e55b7d5 | ||
|
|
025f1bc603 | ||
|
|
8271c3001e | ||
|
|
db70a210a2 | ||
|
|
1d204e9f48 | ||
|
|
d3da551dae | ||
|
|
124ec3743a | ||
|
|
2e6b877ba9 | ||
|
|
33dad80eff | ||
|
|
77622e0257 | ||
|
|
6f301276a7 | ||
|
|
39adea4ee3 | ||
|
|
a8f3922907 | ||
|
|
0dab4012d2 | ||
|
|
11eeab1b51 | ||
|
|
21161321e4 | ||
|
|
38900e6773 | ||
|
|
fad02757d9 | ||
|
|
86d25691d2 | ||
|
|
1488b75b65 | ||
|
|
b56b987f5a | ||
|
|
5337eaf1aa | ||
|
|
0e7dc75557 | ||
|
|
24cf21677c | ||
|
|
c3459d540b | ||
|
|
5105b13219 | ||
|
|
ed1146f5fb | ||
|
|
9731317082 | ||
|
|
80639d6968 | ||
|
|
a5472d7274 | ||
|
|
6d818fe5bc | ||
|
|
7231c842c0 | ||
|
|
e989651336 | ||
|
|
2df791b80b | ||
|
|
c65c00f3a8 | ||
|
|
87bd0dda6d | ||
|
|
5399761dfd | ||
|
|
1c938cb835 | ||
|
|
25c40b8abf | ||
|
|
eacab576b4 | ||
|
|
eb9975a392 | ||
|
|
5829ebec91 | ||
|
|
2007bcfe28 | ||
|
|
d26cb570ba | ||
|
|
18b2c92a76 | ||
|
|
9ea5e6584f | ||
|
|
4a0fb56d18 | ||
|
|
789ab27eef | ||
|
|
3d099ec7c6 | ||
|
|
23c039b42d | ||
|
|
caa9e03050 | ||
|
|
a8da1a7594 | ||
|
|
fd4776842e | ||
|
|
31793693f2 | ||
|
|
98936e4ce2 | ||
|
|
6859ba346c | ||
|
|
e0b34457b5 | ||
|
|
a25eb2e1bb | ||
|
|
2865bcbb04 | ||
|
|
007b861ad6 | ||
|
|
74548e9152 | ||
|
|
aaeb162dd5 | ||
|
|
85448b8a18 | ||
|
|
5c24949e19 | ||
|
|
f231109194 | ||
|
|
692d91e338 | ||
|
|
3c2497bccf | ||
|
|
f4278737fb | ||
|
|
b092654b43 | ||
|
|
dc41da3c7f | ||
|
|
6858152acb | ||
|
|
ba15006bb1 | ||
|
|
7af48d51be | ||
|
|
ad9ede20a2 | ||
|
|
3985713cbd | ||
|
|
7adcc86d03 | ||
|
|
5cfff75dbe | ||
|
|
3b45da274c | ||
|
|
238f555f5e | ||
|
|
54c4f19a5c | ||
|
|
b3c41a4bd0 | ||
|
|
ca64584fd1 | ||
|
|
553325ed09 | ||
|
|
ff2334a489 | ||
|
|
71d3db7aef | ||
|
|
8a98288248 | ||
|
|
03147c8723 | ||
|
|
32244988d2 | ||
|
|
e5bf7bce17 | ||
|
|
ef24705cfa | ||
|
|
f45e2587a0 | ||
|
|
e82e7d0fb3 | ||
|
|
a7c6e89dfa | ||
|
|
7ce02c31d5 | ||
|
|
95b1b8bbdc | ||
|
|
f5040af8bb | ||
|
|
5fd79457dd | ||
|
|
75d4d9658c | ||
|
|
9907b9d625 | ||
|
|
60ecba2f92 | ||
|
|
34cd6eb687 | ||
|
|
505e594758 | ||
|
|
ea42350244 | ||
|
|
11478134a1 | ||
|
|
6649ccf224 | ||
|
|
5fca94dd45 | ||
|
|
13850cda1f | ||
|
|
483f789b8e | ||
|
|
1b83c82997 | ||
|
|
34f52c06a6 | ||
|
|
07fef8af9f | ||
|
|
703ee32653 | ||
|
|
3d4e5441dd | ||
|
|
f00280e32b | ||
|
|
9e6a720cf1 | ||
|
|
7f50071312 | ||
|
|
c64da1fdb7 | ||
|
|
2809cef3b1 | ||
|
|
2f7ff6d178 | ||
|
|
c05e048b65 | ||
|
|
157bad0e29 | ||
|
|
f96a584a5d | ||
|
|
151e20489a | ||
|
|
7dbb0858bd | ||
|
|
b921e95163 | ||
|
|
cb15db2d27 | ||
|
|
c158672d12 | ||
|
|
292bf7068a | ||
|
|
5db4dedc3f | ||
|
|
f122c382e9 | ||
|
|
c865bc7769 | ||
|
|
efffe26a19 | ||
|
|
8b054fd945 | ||
|
|
bb1fa52d10 | ||
|
|
d4b92a8ade | ||
|
|
85d514eb1a | ||
|
|
8878f78ab1 | ||
|
|
d315ad63d2 | ||
|
|
48053b55b9 | ||
|
|
78c7399ff7 | ||
|
|
f70fc18222 | ||
|
|
6f83b0f522 | ||
|
|
5a053cdcd6 | ||
|
|
b1256f4ad2 | ||
|
|
525842e9a1 | ||
|
|
9e261f5235 | ||
|
|
3f808f8f00 | ||
|
|
394df6c210 | ||
|
|
754e77c9cb | ||
|
|
3030e3e7f4 | ||
|
|
f6c18ec73d | ||
|
|
84dc60d7bf | ||
|
|
7541175b75 | ||
|
|
932f4a72df | ||
|
|
b904b161eb | ||
|
|
504bf41b9c | ||
|
|
92ccbae657 | ||
|
|
c0d59db83d | ||
|
|
511ce91630 | ||
|
|
5f5eb2c46d | ||
|
|
4662253d0e | ||
|
|
8836a258bd |
@@ -35,16 +35,22 @@
|
||||
},
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"matangover.mypy",
|
||||
"ms-python.black-formatter",
|
||||
"ms-python.isort",
|
||||
"ms-python.python",
|
||||
"ms-python.vscode-pylance"
|
||||
"ms-python.vscode-pylance",
|
||||
"Vue.volar"
|
||||
],
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
"forwardPorts": [
|
||||
3000,
|
||||
9000
|
||||
],
|
||||
// Use 'onCreateCommand' to run commands at the end of container creation.
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postCreateCommand": "sudo chown -R vscode:vscode /workspaces/mealie/frontend/node_modules && make setup",
|
||||
"onCreateCommand": "sudo chown -R vscode:vscode /workspaces/mealie/frontend/node_modules && make setup",
|
||||
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||
"remoteUser": "vscode",
|
||||
// "features": {
|
||||
|
||||
14
.github/dependabot.yml
vendored
14
.github/dependabot.yml
vendored
@@ -30,3 +30,17 @@ updates:
|
||||
prefix: fix
|
||||
prefix-development: chore
|
||||
include: scope
|
||||
- package-ecosystem: pip
|
||||
directory: "/mealie"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "00:00"
|
||||
open-pull-requests-limit: 10
|
||||
reviewers:
|
||||
- hay-kot
|
||||
assignees:
|
||||
- hay-kot
|
||||
commit-message:
|
||||
prefix: fix
|
||||
prefix-development: chore
|
||||
include: scope
|
||||
|
||||
72
.github/pull_request_template.md
vendored
Normal file
72
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
<!--
|
||||
This template provides some ideas of things to include in your PR description.
|
||||
To start, try providing a short summary of your changes in the Title above.
|
||||
If a section of the PR template does not apply to this PR, then delete that section.
|
||||
-->
|
||||
|
||||
## What type of PR is this?
|
||||
|
||||
_(REQUIRED)_
|
||||
|
||||
<!--
|
||||
Delete any of the following that do not apply:
|
||||
-->
|
||||
|
||||
- bug
|
||||
- cleanup
|
||||
- documentation
|
||||
- feature
|
||||
|
||||
## What this PR does / why we need it:
|
||||
|
||||
_(REQUIRED)_
|
||||
|
||||
<!--
|
||||
What goal is this change working towards?
|
||||
Provide a bullet pointed summary of how each file was changed.
|
||||
Briefly explain any decisions you made with respect to the changes.
|
||||
Include anything here that you didn't include in *Release Notes*
|
||||
above, such as changes to CI or changes to internal methods.
|
||||
-->
|
||||
|
||||
## Which issue(s) this PR fixes:
|
||||
|
||||
_(REQUIRED)_
|
||||
|
||||
<!--
|
||||
If this PR fixes one of more issues, list them here.
|
||||
One line each, like so:
|
||||
Fixes #123
|
||||
Fixes #39
|
||||
-->
|
||||
|
||||
## Special notes for your reviewer:
|
||||
|
||||
_(fill-in or delete this section)_
|
||||
|
||||
<!--
|
||||
Is there any particular feedback you would / wouldn't like?
|
||||
Which parts of the code should reviewers focus on?
|
||||
-->
|
||||
|
||||
## Testing
|
||||
|
||||
_(fill-in or delete this section)_
|
||||
|
||||
<!--
|
||||
Describe how you tested this change.
|
||||
-->
|
||||
|
||||
## Release Notes
|
||||
|
||||
_(REQUIRED)_
|
||||
<!--
|
||||
If this PR makes user facing changes, please describe them here. This
|
||||
description will be copied into the release notes/changelog, whenever the
|
||||
next version is released. Keep this section short, and focus on high level
|
||||
changes.
|
||||
Put your text between the block. To omit notes, use NONE within the block.
|
||||
-->
|
||||
|
||||
```release-note
|
||||
```
|
||||
63
.github/workflows/backend-docker-nightly.yml
vendored
63
.github/workflows/backend-docker-nightly.yml
vendored
@@ -1,63 +0,0 @@
|
||||
name: Backend - Nightly Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- mealie-next
|
||||
|
||||
concurrency:
|
||||
group: backend-nightly-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
#
|
||||
# Checkout
|
||||
#
|
||||
- name: checkout code
|
||||
uses: actions/checkout@v2
|
||||
#
|
||||
# Setup QEMU
|
||||
#
|
||||
- name: Set up QEMU
|
||||
id: qemu
|
||||
uses: docker/setup-qemu-action@v1
|
||||
with:
|
||||
image: tonistiigi/binfmt:latest
|
||||
platforms: all
|
||||
#
|
||||
# Setup Buildx
|
||||
#
|
||||
- name: install buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
with:
|
||||
install: true
|
||||
#
|
||||
# Login to Docker Hub
|
||||
#
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
#
|
||||
# Build
|
||||
#
|
||||
- name: build the image
|
||||
run: |
|
||||
docker build --push --no-cache \
|
||||
--tag hkotel/mealie:api-nightly \
|
||||
--build-arg COMMIT=$(git rev-parse HEAD) \
|
||||
--platform linux/amd64,linux/arm64 .
|
||||
#
|
||||
# Build Discord Notification
|
||||
#
|
||||
- name: Discord notification
|
||||
env:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_NIGHTLY_WEBHOOK }}
|
||||
uses: Ilshidur/action-discord@0.3.2
|
||||
with:
|
||||
args: "🚀 A New build of mealie:api-nightly is available"
|
||||
75
.github/workflows/beta-release.yml
vendored
75
.github/workflows/beta-release.yml
vendored
@@ -1,75 +0,0 @@
|
||||
name: Docker Build Production
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
#
|
||||
# Get Release Version
|
||||
#
|
||||
- uses: oprypin/find-latest-tag@v1
|
||||
with:
|
||||
repository: hay-kot/mealie # The repository to scan.
|
||||
releases-only: true # We know that all relevant tags have a GitHub release for them.
|
||||
id: mealie_version # The step ID to refer to later.
|
||||
#
|
||||
# Checkout
|
||||
#
|
||||
- name: checkout code
|
||||
uses: actions/checkout@v2
|
||||
#
|
||||
# Setup QEMU
|
||||
#
|
||||
- name: Set up QEMU
|
||||
id: qemu
|
||||
uses: docker/setup-qemu-action@v1
|
||||
with:
|
||||
image: tonistiigi/binfmt:latest
|
||||
platforms: all
|
||||
#
|
||||
# Setup Buildx
|
||||
#
|
||||
- name: install buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
with:
|
||||
install: true
|
||||
#
|
||||
# Login to Docker Hub
|
||||
#
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
#
|
||||
# Build Backend
|
||||
#
|
||||
- name: build the image
|
||||
run: |
|
||||
docker build --push --no-cache \
|
||||
--tag hkotel/mealie:api-${{ steps.mealie_version.outputs.tag }} \
|
||||
--build-arg COMMIT=$(git rev-parse HEAD) \
|
||||
--platform linux/amd64,linux/arm64 .
|
||||
#
|
||||
# Build Frontend
|
||||
#
|
||||
- name: build the image
|
||||
working-directory: "frontend"
|
||||
run: |
|
||||
docker build --push --no-cache \
|
||||
--tag hkotel/mealie:frontend-${{ steps.mealie_version.outputs.tag }} \
|
||||
--platform linux/amd64,linux/arm64 .
|
||||
#
|
||||
# Release Discord Notification
|
||||
#
|
||||
- name: Discord notification
|
||||
env:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_RELEASE_WEBHOOK }}
|
||||
uses: Ilshidur/action-discord@0.3.2
|
||||
with:
|
||||
args: '🚀 Version {{ EVENT_PAYLOAD.release.tag_name }} of Mealie has been released. See the release notes https://github.com/hay-kot/mealie/releases/tag/{{ EVENT_PAYLOAD.release.tag_name }}'
|
||||
63
.github/workflows/frontend-docker-nightly.yml
vendored
63
.github/workflows/frontend-docker-nightly.yml
vendored
@@ -1,63 +0,0 @@
|
||||
name: Frontend - Nightly Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- mealie-next
|
||||
|
||||
concurrency:
|
||||
group: frontend-nightly-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
#
|
||||
# Checkout
|
||||
#
|
||||
- name: checkout code
|
||||
uses: actions/checkout@v2
|
||||
#
|
||||
# Setup QEMU
|
||||
#
|
||||
- name: Set up QEMU
|
||||
id: qemu
|
||||
uses: docker/setup-qemu-action@v1
|
||||
with:
|
||||
image: tonistiigi/binfmt:latest
|
||||
platforms: all
|
||||
#
|
||||
# Setup Buildx
|
||||
#
|
||||
- name: install buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
with:
|
||||
install: true
|
||||
#
|
||||
# Login to Docker Hub
|
||||
#
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
#
|
||||
# Build
|
||||
#
|
||||
- name: build the image
|
||||
working-directory: "frontend"
|
||||
run: |
|
||||
docker build --push --no-cache \
|
||||
--tag hkotel/mealie:frontend-nightly \
|
||||
--platform linux/amd64,linux/arm64 .
|
||||
#
|
||||
# Build Discord Notification
|
||||
#
|
||||
- name: Discord notification
|
||||
env:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_NIGHTLY_WEBHOOK }}
|
||||
uses: Ilshidur/action-discord@0.3.2
|
||||
with:
|
||||
args: "🚀 A New build of mealie:frontend-nightly is available"
|
||||
49
.github/workflows/frontend-lint.yml
vendored
49
.github/workflows/frontend-lint.yml
vendored
@@ -1,49 +0,0 @@
|
||||
name: Frontend Lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- mealie-next
|
||||
pull_request:
|
||||
branches:
|
||||
- mealie-next
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
node: [16]
|
||||
|
||||
steps:
|
||||
- name: Checkout 🛎
|
||||
uses: actions/checkout@master
|
||||
|
||||
- name: Setup node env 🏗
|
||||
uses: actions/setup-node@v2.1.5
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
check-latest: true
|
||||
|
||||
- name: Get yarn cache directory path 🛠
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- name: Cache node_modules 📦
|
||||
uses: actions/cache@v2.1.4
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Install dependencies 👨🏻💻
|
||||
run: yarn
|
||||
working-directory: "frontend"
|
||||
|
||||
- name: Run linter 👀
|
||||
run: yarn lint
|
||||
working-directory: "frontend"
|
||||
64
.github/workflows/nightly.yml
vendored
Normal file
64
.github/workflows/nightly.yml
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
name: Docker Nightly Production
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- mealie-next
|
||||
|
||||
concurrency:
|
||||
group: nightly-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
backend-tests:
|
||||
name: "Backend Server Tests"
|
||||
uses: ./.github/workflows/partial-backend.yml
|
||||
|
||||
frontend-tests:
|
||||
name: "Frontend and End-to-End Tests"
|
||||
uses: ./.github/workflows/partial-frontend.yml
|
||||
|
||||
build-release:
|
||||
name: Build Tagged Release
|
||||
uses: ./.github/workflows/partial-builder.yml
|
||||
needs:
|
||||
- frontend-tests
|
||||
- backend-tests
|
||||
with:
|
||||
tag: nightly
|
||||
secrets:
|
||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
notify-discord:
|
||||
name: Notify Discord
|
||||
needs:
|
||||
- build-release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Discord notification
|
||||
env:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_NIGHTLY_WEBHOOK }}
|
||||
uses: Ilshidur/action-discord@0.3.2
|
||||
with:
|
||||
args: "🚀 A New build of mealie:api-nightly and mealie:frontend-nightly is available"
|
||||
|
||||
deploy-demo:
|
||||
runs-on: ubuntu-latest
|
||||
name: Deploy Demo
|
||||
needs:
|
||||
- build-release
|
||||
steps:
|
||||
- name: Clean and Deploy Demo
|
||||
uses: appleboy/ssh-action@master
|
||||
with:
|
||||
host: ${{ secrets.DEMO_SERVER_IP }}
|
||||
username: ${{ secrets.DEMO_SERVER_USER }}
|
||||
key: ${{ secrets.DEMO_SERVER_SSH_KEY }}
|
||||
port: ${{ secrets.DEMO_SERVER_PORT }}
|
||||
script_stop: true
|
||||
script: |
|
||||
cd ~/docker/mealie-next
|
||||
docker-compose pull
|
||||
docker-compose down -v
|
||||
docker-compose up -d
|
||||
@@ -1,12 +1,7 @@
|
||||
name: Backend Tests
|
||||
name: Backend Test/Lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- mealie-next
|
||||
pull_request:
|
||||
branches:
|
||||
- mealie-next
|
||||
types: [synchronize, opened, reopened, ready_for_review]
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
@@ -34,56 +29,56 @@ jobs:
|
||||
- 5432:5432
|
||||
# Steps
|
||||
steps:
|
||||
#----------------------------------------------
|
||||
# check-out repo and set-up python
|
||||
#----------------------------------------------
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: "3.10"
|
||||
#----------------------------------------------
|
||||
# ----- install & configure poetry -----
|
||||
#----------------------------------------------
|
||||
|
||||
- name: Install Poetry
|
||||
uses: snok/install-poetry@v1
|
||||
with:
|
||||
virtualenvs-create: true
|
||||
virtualenvs-in-project: true
|
||||
#----------------------------------------------
|
||||
# load cached venv if cache exists
|
||||
#----------------------------------------------
|
||||
|
||||
- name: Load cached venv
|
||||
id: cached-poetry-dependencies
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: .venv
|
||||
key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }}
|
||||
#----------------------------------------------
|
||||
# install dependencies if cache does not exist
|
||||
#----------------------------------------------
|
||||
|
||||
- name: Check venv cache
|
||||
id: cache-validate
|
||||
if: steps.cached-poetry-dependencies.outputs.cache-hit == 'true'
|
||||
run: |
|
||||
echo "print('venv good?')" > test.py && poetry run python test.py && echo ::set-output name=cache-hit-success::true
|
||||
rm test.py
|
||||
continue-on-error: true
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libsasl2-dev libldap2-dev libssl-dev
|
||||
sudo apt-get install libsasl2-dev libldap2-dev libssl-dev tesseract-ocr-all
|
||||
poetry install
|
||||
poetry add "psycopg2-binary==2.8.6"
|
||||
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
|
||||
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' || steps.cache-validate.outputs.cache-hit-success != 'true'
|
||||
|
||||
#----------------------------------------------
|
||||
# run test suite
|
||||
#----------------------------------------------
|
||||
- name: Formatting (Black & isort)
|
||||
run: |
|
||||
poetry run black . --check
|
||||
poetry run isort . --check-only
|
||||
|
||||
- name: Lint (Flake8)
|
||||
run: |
|
||||
make backend-lint
|
||||
|
||||
- name: Mypy Typecheck
|
||||
run: |
|
||||
make backend-typecheck
|
||||
|
||||
- name: Pytest
|
||||
env:
|
||||
DB_ENGINE: ${{ matrix.Database }}
|
||||
80
.github/workflows/partial-builder.yml
vendored
Normal file
80
.github/workflows/partial-builder.yml
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
name: Build Containers
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
tag:
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
DOCKERHUB_USERNAME:
|
||||
required: true
|
||||
DOCKERHUB_TOKEN:
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
build-frontend:
|
||||
runs-on: ubuntu-latest
|
||||
name: Build Frontend
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up QEMU
|
||||
id: qemu
|
||||
uses: docker/setup-qemu-action@v1
|
||||
with:
|
||||
image: tonistiigi/binfmt:latest
|
||||
platforms: all
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
with:
|
||||
install: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build Frontend Image
|
||||
working-directory: "frontend"
|
||||
run: |
|
||||
docker build --push --no-cache \
|
||||
--tag hkotel/mealie:frontend-${{ inputs.tag }} \
|
||||
--platform linux/amd64,linux/arm64 .
|
||||
|
||||
build-backend:
|
||||
runs-on: ubuntu-latest
|
||||
name: Build Backend
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up QEMU
|
||||
id: qemu
|
||||
uses: docker/setup-qemu-action@v1
|
||||
with:
|
||||
image: tonistiigi/binfmt:latest
|
||||
platforms: all
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
with:
|
||||
install: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build Backend Image
|
||||
run: |
|
||||
docker build --push --no-cache \
|
||||
--tag hkotel/mealie:api-${{ inputs.tag }} \
|
||||
--build-arg COMMIT=$(git rev-parse HEAD) \
|
||||
--platform linux/amd64,linux/arm64 .
|
||||
77
.github/workflows/partial-frontend.yml
vendored
Normal file
77
.github/workflows/partial-frontend.yml
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
name: Frontend Build/Lin
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout 🛎
|
||||
uses: actions/checkout@master
|
||||
|
||||
- name: Setup node env 🏗
|
||||
uses: actions/setup-node@v2.1.5
|
||||
with:
|
||||
node-version: 16
|
||||
check-latest: true
|
||||
|
||||
- name: Get yarn cache directory path 🛠
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- name: Cache node_modules 📦
|
||||
uses: actions/cache@v2.1.4
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Install dependencies 👨🏻💻
|
||||
run: yarn
|
||||
working-directory: "frontend"
|
||||
|
||||
- name: Run linter 👀
|
||||
run: yarn lint
|
||||
working-directory: "frontend"
|
||||
|
||||
- name: Run tests 🧪
|
||||
run: yarn test:ci
|
||||
working-directory: "frontend"
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout 🛎
|
||||
uses: actions/checkout@master
|
||||
|
||||
- name: Setup node env 🏗
|
||||
uses: actions/setup-node@v2.1.5
|
||||
with:
|
||||
node-version: 16
|
||||
check-latest: true
|
||||
|
||||
- name: Get yarn cache directory path 🛠
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- name: Cache node_modules 📦
|
||||
uses: actions/cache@v2.1.4
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Install dependencies 👨🏻💻
|
||||
run: yarn
|
||||
working-directory: "frontend"
|
||||
|
||||
- name: Run Build 🚚
|
||||
run: yarn build
|
||||
working-directory: "frontend"
|
||||
15
.github/workflows/pull-requests.yml
vendored
Normal file
15
.github/workflows/pull-requests.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
name: PR CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- mealie-next
|
||||
|
||||
jobs:
|
||||
backend-tests:
|
||||
name: "Backend Server Tests"
|
||||
uses: ./.github/workflows/partial-backend.yml
|
||||
|
||||
frontend-tests:
|
||||
name: "Frontend and End-to-End Tests"
|
||||
uses: ./.github/workflows/partial-frontend.yml
|
||||
55
.github/workflows/release.yml
vendored
Normal file
55
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
name: Docker Build Production
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
backend-tests:
|
||||
name: "Backend Server Tests"
|
||||
uses: ./.github/workflows/partial-backend.yml
|
||||
|
||||
frontend-tests:
|
||||
name: "Frontend and End-to-End Tests"
|
||||
uses: ./.github/workflows/partial-frontend.yml
|
||||
|
||||
build-release:
|
||||
name: Build Tagged Release
|
||||
uses: ./.github/workflows/partial-builder.yml
|
||||
needs:
|
||||
- backend-tests
|
||||
- frontend-tests
|
||||
with:
|
||||
tag: ${{ github.event.release.tag_name }}
|
||||
secrets:
|
||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
publish-docs:
|
||||
name: Deploy docs
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build-release
|
||||
steps:
|
||||
- name: Checkout main
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Deploy docs
|
||||
uses: mhausenblas/mkdocs-deploy-gh-pages@master
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CONFIG_FILE: docs/mkdocs.yml
|
||||
EXTRA_PACKAGES: build-base
|
||||
|
||||
notify-discord:
|
||||
name: Notify Discord
|
||||
needs:
|
||||
- build-release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Discord notification
|
||||
env:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_RELEASE_WEBHOOK }}
|
||||
uses: Ilshidur/action-discord@0.3.2
|
||||
with:
|
||||
args: "🚀 Version {{ EVENT_PAYLOAD.release.tag_name }} of Mealie has been released. See the release notes https://github.com/hay-kot/mealie/releases/tag/{{ EVENT_PAYLOAD.release.tag_name }}"
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -55,7 +55,6 @@ develop-eggs/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
!frontend/src/components/Recipe/Parts/
|
||||
@@ -158,3 +157,4 @@ dev/code-generation/generated/openapi.json
|
||||
dev/code-generation/generated/test_routes.py
|
||||
mealie/services/parser_services/crfpp/model.crfmodel
|
||||
lcov.info
|
||||
dev/code-generation/openapi.json
|
||||
|
||||
@@ -9,10 +9,11 @@ repos:
|
||||
- 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
|
||||
exclude: ^tests/data/
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.1.0
|
||||
hooks:
|
||||
- id: upgrade-type-hints
|
||||
- id: pyupgrade
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 5.10.1
|
||||
hooks:
|
||||
|
||||
8
.vscode/settings.json
vendored
8
.vscode/settings.json
vendored
@@ -1,10 +1,4 @@
|
||||
{
|
||||
"conventionalCommits.scopes": [
|
||||
"frontend",
|
||||
"docs",
|
||||
"backend",
|
||||
"code-generation"
|
||||
],
|
||||
"cSpell.enableFiletypes": ["!javascript", "!python", "!yaml"],
|
||||
"cSpell.words": [
|
||||
"chowdown",
|
||||
@@ -44,7 +38,7 @@
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.analysis.typeCheckingMode": "off",
|
||||
"python.linting.mypyEnabled": true,
|
||||
"python.sortImports.path": "${workspaceFolder}/.venv/bin/isort",
|
||||
"isort.path": ["${workspaceFolder}/.venv/bin/isort"],
|
||||
"search.mode": "reuseEditor",
|
||||
"python.testing.unittestArgs": ["-v", "-s", "./tests", "-p", "test_*.py"],
|
||||
"explorer.fileNesting.enabled": true,
|
||||
|
||||
16
Dockerfile
16
Dockerfile
@@ -34,14 +34,15 @@ RUN apt-get update \
|
||||
build-essential \
|
||||
libpq-dev \
|
||||
libwebp-dev \
|
||||
tesseract-ocr-all \
|
||||
# LDAP Dependencies
|
||||
libsasl2-dev libldap2-dev libssl-dev \
|
||||
gnupg gnupg2 gnupg1 \
|
||||
&& pip install -U --no-cache-dir pip
|
||||
|
||||
# install poetry - respects $POETRY_VERSION & $POETRY_HOME
|
||||
ENV POETRY_VERSION=1.1.6
|
||||
RUN curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py | python -
|
||||
ENV POETRY_VERSION=1.2.1
|
||||
RUN curl -sSL https://install.python-poetry.org | python3 -
|
||||
|
||||
# copy project requirement files here to ensure they will be cached.
|
||||
WORKDIR $PYSETUP_PATH
|
||||
@@ -94,10 +95,10 @@ ENV TESTING=false
|
||||
ARG COMMIT
|
||||
ENV GIT_COMMIT_HASH=$COMMIT
|
||||
|
||||
# curl for used by healthcheck
|
||||
RUN apt-get update \
|
||||
&& apt-get install --no-install-recommends -y \
|
||||
curl \
|
||||
gosu \
|
||||
tesseract-ocr-all \
|
||||
&& apt-get autoremove \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
@@ -105,9 +106,6 @@ RUN apt-get update \
|
||||
COPY --from=builder-base $POETRY_HOME $POETRY_HOME
|
||||
COPY --from=builder-base $PYSETUP_PATH $PYSETUP_PATH
|
||||
|
||||
# copy CRF++ Binary from crfpp
|
||||
ENV CRF_MODEL_URL=https://github.com/mealie-recipes/nlp-model/releases/download/v1.0.0/model.crfmodel
|
||||
|
||||
ENV LD_LIBRARY_PATH=/usr/local/lib
|
||||
COPY --from=crfpp /usr/local/lib/ /usr/local/lib
|
||||
COPY --from=crfpp /usr/local/bin/crf_learn /usr/local/bin/crf_learn
|
||||
@@ -128,14 +126,14 @@ RUN . $VENV_PATH/bin/activate && poetry install -E pgsql --no-dev
|
||||
WORKDIR /
|
||||
|
||||
# Grab CRF++ Model Release
|
||||
RUN curl -L0 $CRF_MODEL_URL --output $MEALIE_HOME/mealie/services/parser_services/crfpp/model.crfmodel
|
||||
RUN python $MEALIE_HOME/mealie/scripts/install_model.py
|
||||
|
||||
VOLUME [ "$MEALIE_HOME/data/" ]
|
||||
ENV APP_PORT=9000
|
||||
|
||||
EXPOSE ${APP_PORT}
|
||||
|
||||
HEALTHCHECK CMD curl -f http://localhost:${APP_PORT}/docs || exit 1
|
||||
HEALTHCHECK CMD python $MEALIE_HOME/mealie/scripts/healthcheck.py || exit 1
|
||||
|
||||
RUN chmod +x $MEALIE_HOME/mealie/run.sh
|
||||
ENTRYPOINT $MEALIE_HOME/mealie/run.sh
|
||||
|
||||
10
README.md
10
README.md
@@ -42,18 +42,18 @@
|
||||
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)!
|
||||
- [Documentation](https://docs.mealie.io)
|
||||
- [Documentation](https://nightly.mealie.io)
|
||||
|
||||
|
||||
<!-- 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**. 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.
|
||||
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 Containers](https://code.visualstudio.com/docs/remote/containers) 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.
|
||||
|
||||
<a href="https://www.buymeacoffee.com/haykot" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-green.png" alt="Buy Me A Coffee" style="height: 30px !important;width: 107px !important;" ></a>
|
||||
|
||||
@@ -64,7 +64,7 @@ Distributed under the MIT License. See `LICENSE` for more information.
|
||||
|
||||
## Sponsors
|
||||
|
||||
Huge thanks to all the sponsors of this project on [Github Sponsors](https://github.com/sponsors/hay-kot) and Buy Me a Coffee. Without you this project would surely not be possible.
|
||||
Huge thanks to all the sponsors of this project on [Github Sponsors](https://github.com/sponsors/hay-kot) and Buy Me a Coffee. Without you, this project would surely not be possible.
|
||||
|
||||
Thanks to Linode for providing Hosting for the Demo, Beta, and Documentation sites! Another big thanks to JetBrains for providing their IDEs for development.
|
||||
|
||||
@@ -87,7 +87,7 @@ Thanks to Linode for providing Hosting for the Demo, Beta, and Documentation sit
|
||||
[issues-shield]: https://img.shields.io/github/issues/hay-kot/mealie.svg?style=flat-square
|
||||
[issues-url]: https://github.com/hay-kot/mealie/issues
|
||||
[license-shield]: https://img.shields.io/github/license/hay-kot/mealie.svg?style=flat-square
|
||||
[license-url]: https://github.com/hay-kot/mealie/blob/master/LICENSE.txt
|
||||
[license-url]: https://github.com/hay-kot/mealie/blob/mealie-next/LICENSE
|
||||
[linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=flat-square&logo=linkedin&colorB=555
|
||||
[linkedin-url]: https://linkedin.com/in/hay-kot
|
||||
[product-screenshot]: docs/docs/assets/img/home_screenshot.png
|
||||
|
||||
@@ -23,8 +23,8 @@ def is_postgres():
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
|
||||
# SQLite doesn't require migration as types are not inforced.
|
||||
# Postgres Specifc Migration
|
||||
# SQLite doesn't require migration as types are not enforced.
|
||||
# Postgres Specific Migration
|
||||
if is_postgres():
|
||||
op.alter_column(
|
||||
"recipes_ingredients",
|
||||
@@ -38,8 +38,8 @@ def upgrade():
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
|
||||
# SQLite doesn't require migration as types are not inforced.
|
||||
# Postgres Specifc Migration
|
||||
# SQLite doesn't require migration as types are not enforced.
|
||||
# Postgres Specific Migration
|
||||
if is_postgres():
|
||||
op.alter_column(
|
||||
"recipes_ingredients",
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
"""add new webhook fields
|
||||
|
||||
|
||||
Revision ID: f30cf048c228
|
||||
Revises: ab0bae02578f
|
||||
Create Date: 2022-06-15 21:05:34.851857
|
||||
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "f30cf048c228"
|
||||
down_revision = "ab0bae02578f"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column("webhook_urls", sa.Column("webhook_type", sa.String(), nullable=True))
|
||||
op.add_column("webhook_urls", sa.Column("scheduled_time", sa.Time(), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column("webhook_urls", "scheduled_time")
|
||||
op.drop_column("webhook_urls", "webhook_type")
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,27 @@
|
||||
"""Add is_ocr_recipe column to recipes
|
||||
|
||||
Revision ID: 089bfa50d0ed
|
||||
Revises: f30cf048c228
|
||||
Create Date: 2022-08-05 17:07:07.389271
|
||||
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "089bfa50d0ed"
|
||||
down_revision = "188374910655"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column("recipes", sa.Column("is_ocr_recipe", sa.Boolean(), default=False, nullable=True))
|
||||
op.execute("UPDATE recipes SET is_ocr_recipe = FALSE")
|
||||
# SQLITE does not support ALTER COLUMN, so the column will stay nullable to prevent making this migration a mess
|
||||
# The Recipe pydantic model and the SQL server use False as default value anyway for this column so Null should be a very rare sight
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_column("recipes", "is_ocr_recipe")
|
||||
@@ -0,0 +1,26 @@
|
||||
"""add login_attemps and locked_at field to user table
|
||||
|
||||
Revision ID: 188374910655
|
||||
Revises: f30cf048c228
|
||||
Create Date: 2022-08-12 19:05:59.776361
|
||||
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "188374910655"
|
||||
down_revision = "f30cf048c228"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column("users", sa.Column("login_attemps", sa.Integer(), nullable=True))
|
||||
op.add_column("users", sa.Column("locked_at", sa.DateTime(), nullable=True))
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_column("users", "locked_at")
|
||||
op.drop_column("users", "login_attemps")
|
||||
@@ -0,0 +1,72 @@
|
||||
"""add extras to shopping lists, list items, and ingredient foods
|
||||
|
||||
Revision ID: 44e8d670719d
|
||||
Revises: 188374910655
|
||||
Create Date: 2022-08-29 13:57:40.452245
|
||||
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
|
||||
import mealie.db.migration_types
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "44e8d670719d"
|
||||
down_revision = "089bfa50d0ed"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"shopping_list_extras",
|
||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("update_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("key_name", sa.String(), nullable=True),
|
||||
sa.Column("value", sa.String(), nullable=True),
|
||||
sa.Column("shopping_list_id", mealie.db.migration_types.GUID(), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["shopping_list_id"],
|
||||
["shopping_lists.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_table(
|
||||
"ingredient_food_extras",
|
||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("update_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("key_name", sa.String(), nullable=True),
|
||||
sa.Column("value", sa.String(), nullable=True),
|
||||
sa.Column("ingredient_food_id", mealie.db.migration_types.GUID(), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["ingredient_food_id"],
|
||||
["ingredient_foods.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_table(
|
||||
"shopping_list_item_extras",
|
||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("update_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("key_name", sa.String(), nullable=True),
|
||||
sa.Column("value", sa.String(), nullable=True),
|
||||
sa.Column("shopping_list_item_id", mealie.db.migration_types.GUID(), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["shopping_list_item_id"],
|
||||
["shopping_list_items.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table("shopping_list_item_extras")
|
||||
op.drop_table("ingredient_food_extras")
|
||||
op.drop_table("shopping_list_extras")
|
||||
# ### end Alembic commands ###
|
||||
@@ -2,8 +2,6 @@ preserve_hierarchy: false
|
||||
files:
|
||||
- source: /frontend/lang/messages/en-US.json
|
||||
translation: /frontend/lang/messages/%locale%.json
|
||||
- source: /frontend/lang/dateTimeFormats/en-US.json
|
||||
translation: /frontend/lang/dateTimeFormats/%locale%.json
|
||||
- source: /mealie/lang/messages/en-US.json
|
||||
translation: /mealie/lang/messages/%locale%.json
|
||||
- source: /mealie/repos/seed/resources/foods/locales/en-US.json
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
from pathlib import Path
|
||||
|
||||
CWD = Path(__file__).parent
|
||||
PROJECT_DIR = Path(__file__).parent.parent.parent
|
||||
|
||||
|
||||
class Directories:
|
||||
out_dir = CWD / "generated"
|
||||
|
||||
|
||||
class CodeTemplates:
|
||||
interface = CWD / "templates" / "interface.js"
|
||||
pytest_routes = CWD / "templates" / "test_routes.py.j2"
|
||||
|
||||
|
||||
class CodeDest:
|
||||
interface = CWD / "generated" / "interface.js"
|
||||
pytest_routes = CWD / "generated" / "test_routes.py"
|
||||
use_locales = PROJECT_DIR / "frontend" / "composables" / "use-locales" / "available-locales.ts"
|
||||
|
||||
|
||||
class CodeKeys:
|
||||
"""Hard coded comment IDs that are used to generate code"""
|
||||
|
||||
nuxt_local_messages = "MESSAGE_LOCALES"
|
||||
nuxt_local_dates = "DATE_LOCALES"
|
||||
@@ -1,39 +0,0 @@
|
||||
from pathlib import Path
|
||||
|
||||
from _gen_utils import inject_inline
|
||||
from _static import CodeKeys
|
||||
|
||||
PROJECT_DIR = Path(__file__).parent.parent.parent
|
||||
|
||||
|
||||
datetime_dir = PROJECT_DIR / "frontend" / "lang" / "dateTimeFormats"
|
||||
locales_dir = PROJECT_DIR / "frontend" / "lang" / "messages"
|
||||
nuxt_config = PROJECT_DIR / "frontend" / "nuxt.config.js"
|
||||
|
||||
"""
|
||||
This snippet walks the message and dat locales directories and generates the import information
|
||||
for the nuxt.config.js file and automatically injects it into the nuxt.config.js file. Note that
|
||||
the code generation ID is hardcoded into the script and required in the nuxt config.
|
||||
"""
|
||||
|
||||
|
||||
def main(): # sourcery skip: list-comprehension
|
||||
print("Starting...")
|
||||
|
||||
all_date_locales = []
|
||||
for match in datetime_dir.glob("*.json"):
|
||||
all_date_locales.append(f'"{match.stem}": require("./lang/dateTimeFormats/{match.name}"),')
|
||||
|
||||
all_langs = []
|
||||
for match in locales_dir.glob("*.json"):
|
||||
lang_string = f'{{ code: "{match.stem}", file: "{match.name}" }},'
|
||||
all_langs.append(lang_string)
|
||||
|
||||
inject_inline(nuxt_config, CodeKeys.nuxt_local_messages, all_langs)
|
||||
inject_inline(nuxt_config, CodeKeys.nuxt_local_dates, all_date_locales)
|
||||
|
||||
print("Finished...")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,15 +1,13 @@
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from _gen_utils import render_python_template
|
||||
from slugify import slugify
|
||||
from utils import render_python_template
|
||||
|
||||
CWD = Path(__file__).parent
|
||||
|
||||
TEMPLATE = CWD / "templates" / "test_data.py.j2"
|
||||
|
||||
TEST_DATA = CWD.parent.parent / "tests" / "data"
|
||||
|
||||
GENERATED = CWD / "generated"
|
||||
|
||||
|
||||
@@ -27,9 +25,7 @@ class TestDataPath:
|
||||
|
||||
# Remove any file extension
|
||||
var = var.split(".")[0]
|
||||
|
||||
var = var.replace("'", "")
|
||||
|
||||
var = slugify(var, separator="_")
|
||||
|
||||
return cls(var, rel_path)
|
||||
@@ -99,8 +95,6 @@ def rename_non_compliant_paths():
|
||||
|
||||
|
||||
def main():
|
||||
print("Starting Template Generation")
|
||||
|
||||
rename_non_compliant_paths()
|
||||
|
||||
GENERATED.mkdir(exist_ok=True)
|
||||
@@ -117,8 +111,6 @@ def main():
|
||||
{"children": all_children},
|
||||
)
|
||||
|
||||
print("Finished Template Generation")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
84
dev/code-generation/gen_py_pytest_routes.py
Normal file
84
dev/code-generation/gen_py_pytest_routes.py
Normal file
@@ -0,0 +1,84 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI
|
||||
from jinja2 import Template
|
||||
from pydantic import BaseModel
|
||||
from utils import PROJECT_DIR, CodeTemplates, HTTPRequest, RouteObject
|
||||
|
||||
CWD = Path(__file__).parent
|
||||
|
||||
OUTFILE = PROJECT_DIR / "tests" / "utils" / "api_routes" / "__init__.py"
|
||||
|
||||
|
||||
class PathObject(BaseModel):
|
||||
route_object: RouteObject
|
||||
http_verbs: list[HTTPRequest]
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
|
||||
def get_path_objects(app: FastAPI):
|
||||
paths = []
|
||||
|
||||
for key, value in app.openapi().items():
|
||||
if key == "paths":
|
||||
for key, value in value.items():
|
||||
|
||||
paths.append(
|
||||
PathObject(
|
||||
route_object=RouteObject(key),
|
||||
http_verbs=[HTTPRequest(request_type=k, **v) for k, v in value.items()],
|
||||
)
|
||||
)
|
||||
|
||||
return paths
|
||||
|
||||
|
||||
def dump_open_api(app: FastAPI):
|
||||
"""Writes the Open API as JSON to a json file"""
|
||||
OPEN_API_FILE = CWD / "openapi.json"
|
||||
|
||||
with open(OPEN_API_FILE, "w") as f:
|
||||
f.write(json.dumps(app.openapi()))
|
||||
|
||||
|
||||
def read_template(file: Path):
|
||||
with open(file) as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def generate_python_templates(static_paths: list[PathObject], function_paths: list[PathObject]):
|
||||
|
||||
template = Template(read_template(CodeTemplates.pytest_routes))
|
||||
content = template.render(
|
||||
paths={
|
||||
"prefix": "/api",
|
||||
"static_paths": static_paths,
|
||||
"function_paths": function_paths,
|
||||
}
|
||||
)
|
||||
with open(OUTFILE, "w") as f:
|
||||
f.write(content)
|
||||
|
||||
return
|
||||
|
||||
|
||||
def main():
|
||||
from mealie.app import app
|
||||
|
||||
dump_open_api(app)
|
||||
paths = get_path_objects(app)
|
||||
|
||||
static_paths = [x.route_object for x in paths if not x.route_object.is_function]
|
||||
function_paths = [x.route_object for x in paths if x.route_object.is_function]
|
||||
|
||||
static_paths.sort(key=lambda x: x.router_slug)
|
||||
function_paths.sort(key=lambda x: x.router_slug)
|
||||
|
||||
generate_python_templates(static_paths, function_paths)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
102
dev/code-generation/gen_py_schema_exports.py
Normal file
102
dev/code-generation/gen_py_schema_exports.py
Normal file
@@ -0,0 +1,102 @@
|
||||
import pathlib
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from utils import PROJECT_DIR, log, render_python_template
|
||||
|
||||
template = """# This file is auto-generated by gen_schema_exports.py
|
||||
{% for file in data.module.files %}{{ file.import_str() }}
|
||||
{% endfor %}
|
||||
|
||||
__all__ = [
|
||||
{% for file in data.module.files %}
|
||||
{%- for class in file.classes -%}
|
||||
"{{ class }}",
|
||||
{%- endfor -%}
|
||||
{%- endfor %}
|
||||
]
|
||||
|
||||
"""
|
||||
|
||||
SCHEMA_PATH = PROJECT_DIR / "mealie" / "schema"
|
||||
|
||||
SKIP = {"static", "__pycache__"}
|
||||
|
||||
|
||||
class PyFile:
|
||||
import_path: str
|
||||
"""The import path of the file"""
|
||||
|
||||
classes: list[str]
|
||||
"""A list of classes in the file"""
|
||||
|
||||
def __init__(self, path: pathlib.Path):
|
||||
self.import_path = path.stem
|
||||
self.classes = []
|
||||
|
||||
self.classes = PyFile.extract_classes(path)
|
||||
self.classes.sort()
|
||||
|
||||
def import_str(self) -> str:
|
||||
"""Returns a string that can be used to import the file"""
|
||||
return f"from .{self.import_path} import {', '.join(self.classes)}"
|
||||
|
||||
@staticmethod
|
||||
def extract_classes(file_path: pathlib.Path) -> list[str]:
|
||||
name = file_path.stem
|
||||
|
||||
if name == "__init__" or name.startswith("_"):
|
||||
return []
|
||||
|
||||
classes = re.findall(r"(?m)^class\s(\w+)", file_path.read_text())
|
||||
return classes
|
||||
|
||||
|
||||
@dataclass
|
||||
class Modules:
|
||||
directory: pathlib.Path
|
||||
"""The directory to search for modules"""
|
||||
|
||||
files: list[PyFile] = field(default_factory=list)
|
||||
"""A list of files in the directory"""
|
||||
|
||||
def __post_init__(self):
|
||||
for file in self.directory.glob("*.py"):
|
||||
if file.name.startswith("_"):
|
||||
continue
|
||||
|
||||
pfile = PyFile(file)
|
||||
|
||||
if len(pfile.classes) > 0:
|
||||
self.files.append(pfile)
|
||||
|
||||
else:
|
||||
log.debug(f"Skipping {file.name} as it has no classes")
|
||||
|
||||
|
||||
def find_modules(root: pathlib.Path) -> list[Modules]:
|
||||
"""Finds all the top level modules in the provided folder"""
|
||||
modules: list[Modules] = []
|
||||
for file in root.iterdir():
|
||||
if file.is_dir() and file.name not in SKIP:
|
||||
|
||||
modules.append(Modules(directory=file))
|
||||
|
||||
return modules
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
modules = find_modules(SCHEMA_PATH)
|
||||
|
||||
for module in modules:
|
||||
log.debug(f"Module: {module.directory.name}")
|
||||
for file in module.files:
|
||||
log.debug(f" File: {file.import_path}")
|
||||
log.debug(f" Classes: [{', '.join(file.classes)}]")
|
||||
|
||||
render_python_template(template, module.directory / "__init__.py", {"module": module})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,53 +0,0 @@
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from _gen_utils import render_python_template
|
||||
from _open_api_parser import OpenAPIParser
|
||||
from _static import CodeDest, CodeTemplates
|
||||
from rich.console import Console
|
||||
|
||||
from mealie.app import app
|
||||
|
||||
"""
|
||||
This code is used for generating route objects for each route in the OpenAPI Specification.
|
||||
Currently, they are NOT automatically injected into the test suite. As such, you'll need to copy
|
||||
the relavent contents of the generated file into the test suite where applicable. I am slowly
|
||||
migrating the test suite to use this new generated file and this process will be "automated" in the
|
||||
future.
|
||||
"""
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
def write_dict_to_file(file_name: str, data: dict[str, Any]):
|
||||
with open(file_name, "w") as f:
|
||||
f.write(json.dumps(data, indent=4))
|
||||
|
||||
|
||||
def main():
|
||||
print("Starting...")
|
||||
open_api = OpenAPIParser(app)
|
||||
modules = open_api.get_by_module()
|
||||
|
||||
mods = []
|
||||
|
||||
for mod, value in modules.items():
|
||||
|
||||
routes = []
|
||||
existings = set()
|
||||
# Reduce routes by unique py_route attribute
|
||||
for route in value:
|
||||
if route.py_route not in existings:
|
||||
existings.add(route.py_route)
|
||||
routes.append(route)
|
||||
|
||||
module = {"name": mod, "routes": routes}
|
||||
mods.append(module)
|
||||
|
||||
render_python_template(CodeTemplates.pytest_routes, CodeDest.pytest_routes, {"mods": mods})
|
||||
|
||||
print("Finished...")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,35 +0,0 @@
|
||||
from _gen_utils import render_python_template
|
||||
from _static import PROJECT_DIR
|
||||
|
||||
template = """# GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
{% for file in data.files %}from .{{ file }} import *
|
||||
{% endfor %}
|
||||
"""
|
||||
|
||||
SCHEMA_PATH = PROJECT_DIR / "mealie" / "schema"
|
||||
|
||||
|
||||
def generate_init_files() -> None:
|
||||
|
||||
for schema in SCHEMA_PATH.iterdir():
|
||||
if not schema.is_dir():
|
||||
print(f"Skipping {schema}")
|
||||
continue
|
||||
|
||||
print(f"Generating {schema}")
|
||||
init_file = schema.joinpath("__init__.py")
|
||||
|
||||
module_files = [
|
||||
f.stem for f in schema.iterdir() if f.is_file() and f.suffix == ".py" and not f.stem.startswith("_")
|
||||
]
|
||||
render_python_template(template, init_file, {"files": module_files})
|
||||
|
||||
|
||||
def main():
|
||||
print("Starting...")
|
||||
generate_init_files()
|
||||
print("Finished...")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,11 +1,12 @@
|
||||
import pathlib
|
||||
from pathlib import Path
|
||||
|
||||
import _static
|
||||
import dotenv
|
||||
import requests
|
||||
from jinja2 import Template
|
||||
from pydantic import Extra
|
||||
from requests import Response
|
||||
from rich import print
|
||||
from utils import CodeDest, CodeKeys, inject_inline, log
|
||||
|
||||
from mealie.schema._mealie import MealieModel
|
||||
|
||||
@@ -13,6 +14,7 @@ BASE = pathlib.Path(__file__).parent.parent.parent
|
||||
|
||||
API_KEY = dotenv.get_key(BASE / ".env", "CROWDIN_API_KEY")
|
||||
|
||||
|
||||
NAMES = {
|
||||
"en-US": "American English",
|
||||
"en-GB": "British English",
|
||||
@@ -35,7 +37,7 @@ NAMES = {
|
||||
"nl-NL": "Nederlands (Dutch)",
|
||||
"pl-PL": "Polski (Polish)",
|
||||
"pt-BR": "Português do Brasil (Brazilian Portuguese)",
|
||||
"pt-PT": "Português (Portugese)",
|
||||
"pt-PT": "Português (Portuguese)",
|
||||
"ro-RO": "Română (Romanian)",
|
||||
"ru-RU": "Pусский (Russian)",
|
||||
"sr-SP": "српски (Serbian)",
|
||||
@@ -55,6 +57,7 @@ export const LOCALES = [{% for locale in locales %}
|
||||
progress: {{ locale.progress }},
|
||||
},{% endfor %}
|
||||
]
|
||||
|
||||
"""
|
||||
|
||||
|
||||
@@ -66,6 +69,9 @@ class TargetLanguage(MealieModel):
|
||||
twoLettersCode: str
|
||||
progress: float = 0.0
|
||||
|
||||
class Config:
|
||||
extra = Extra.allow
|
||||
|
||||
|
||||
class CrowdinApi:
|
||||
project_name = "Mealie"
|
||||
@@ -122,22 +128,53 @@ class CrowdinApi:
|
||||
return response.json()
|
||||
|
||||
|
||||
def main():
|
||||
print("Starting...") # noqa
|
||||
PROJECT_DIR = Path(__file__).parent.parent.parent
|
||||
|
||||
if API_KEY is None:
|
||||
print("CROWDIN_API_KEY is not set") # noqa
|
||||
return
|
||||
|
||||
datetime_dir = PROJECT_DIR / "frontend" / "lang" / "dateTimeFormats"
|
||||
locales_dir = PROJECT_DIR / "frontend" / "lang" / "messages"
|
||||
nuxt_config = PROJECT_DIR / "frontend" / "nuxt.config.js"
|
||||
|
||||
"""
|
||||
This snippet walks the message and dat locales directories and generates the import information
|
||||
for the nuxt.config.js file and automatically injects it into the nuxt.config.js file. Note that
|
||||
the code generation ID is hardcoded into the script and required in the nuxt config.
|
||||
"""
|
||||
|
||||
|
||||
def inject_nuxt_values():
|
||||
all_date_locales = [
|
||||
f'"{match.stem}": require("./lang/dateTimeFormats/{match.name}"),' for match in datetime_dir.glob("*.json")
|
||||
]
|
||||
|
||||
all_langs = []
|
||||
for match in locales_dir.glob("*.json"):
|
||||
lang_string = f'{{ code: "{match.stem}", file: "{match.name}" }},'
|
||||
all_langs.append(lang_string)
|
||||
|
||||
log.debug(f"injecting locales into nuxt config -> {nuxt_config}")
|
||||
inject_inline(nuxt_config, CodeKeys.nuxt_local_messages, all_langs)
|
||||
inject_inline(nuxt_config, CodeKeys.nuxt_local_dates, all_date_locales)
|
||||
|
||||
|
||||
def generate_locales_ts_file():
|
||||
api = CrowdinApi("")
|
||||
models = api.get_languages()
|
||||
tmpl = Template(LOCALE_TEMPLATE)
|
||||
rendered = tmpl.render(locales=models)
|
||||
|
||||
with open(_static.CodeDest.use_locales, "w") as f:
|
||||
log.debug(f"generating locales ts file -> {CodeDest.use_locales}")
|
||||
with open(CodeDest.use_locales, "w") as f:
|
||||
f.write(rendered) # type:ignore
|
||||
|
||||
print("Finished...") # noqa
|
||||
|
||||
def main():
|
||||
if API_KEY is None or API_KEY == "":
|
||||
log.error("CROWDIN_API_KEY is not set")
|
||||
return
|
||||
|
||||
generate_locales_ts_file()
|
||||
inject_nuxt_values()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -2,28 +2,28 @@ from pathlib import Path
|
||||
|
||||
from jinja2 import Template
|
||||
from pydantic2ts import generate_typescript_defs
|
||||
from rich import print
|
||||
from utils import log
|
||||
|
||||
# ============================================================
|
||||
# Global Compoenents Generator
|
||||
|
||||
template = """// This Code is auto generated by gen_global_components.py
|
||||
{% for name in global %} import {{ name }} from "@/components/global/{{ name }}.vue";
|
||||
{% for name in global %}import {{ name }} from "@/components/global/{{ name }}.vue";
|
||||
{% endfor %}{% for name in layout %}import {{ name }} from "@/components/layout/{{ name }}.vue";
|
||||
{% endfor %}
|
||||
{% for name in layout %} import {{ name }} from "@/components/layout/{{ name }}.vue";
|
||||
{% endfor %}
|
||||
|
||||
declare module "vue" {
|
||||
export interface GlobalComponents {
|
||||
// Global Components
|
||||
{% for name in global %} {{ name }}: typeof {{ name }};
|
||||
{% endfor %} // Layout Components
|
||||
{% for name in layout %} {{ name }}: typeof {{ name }};
|
||||
{% endfor %}
|
||||
}
|
||||
{% for name in global -%}
|
||||
{{ " " }}{{ name }}: typeof {{ name }};
|
||||
{% endfor -%}
|
||||
{{ " " }}// Layout Components
|
||||
{% for name in layout -%}
|
||||
{{ " " }}{{ name }}: typeof {{ name }};
|
||||
{% endfor -%}{{ " }"}}
|
||||
}
|
||||
|
||||
export {};
|
||||
|
||||
"""
|
||||
|
||||
CWD = Path(__file__).parent
|
||||
@@ -46,6 +46,7 @@ def generate_global_components_types() -> None:
|
||||
data = {}
|
||||
for name, path in component_paths.items():
|
||||
components = [component.stem for component in path.glob("*.vue")]
|
||||
components.sort()
|
||||
data[name] = components
|
||||
|
||||
return data
|
||||
@@ -74,7 +75,7 @@ def generate_typescript_types() -> None:
|
||||
return str_path
|
||||
|
||||
schema_path = PROJECT_DIR / "mealie" / "schema"
|
||||
types_dir = PROJECT_DIR / "frontend" / "types" / "api-types"
|
||||
types_dir = PROJECT_DIR / "frontend" / "lib" / "api" / "types"
|
||||
|
||||
ignore_dirs = ["__pycache__", "static", "_mealie"]
|
||||
|
||||
@@ -99,26 +100,29 @@ def generate_typescript_types() -> None:
|
||||
generate_typescript_defs(path_as_module, str(out_path), exclude=("MealieModel")) # type: ignore
|
||||
except Exception as e:
|
||||
failed_modules.append(module)
|
||||
print("\nModule Errors:", module, "-----------------") # noqa
|
||||
print(e) # noqa
|
||||
print("Finished Module Errors:", module, "-----------------\n") # noqa
|
||||
log.error(f"Module Error: {e}")
|
||||
|
||||
print("\n📁 Skipped Directories:") # noqa
|
||||
log.debug("\n📁 Skipped Directories:")
|
||||
for skipped_dir in skipped_dirs:
|
||||
print(" 📁", skipped_dir.name) # noqa
|
||||
log.debug(f" 📁 {skipped_dir.name}")
|
||||
|
||||
print("📄 Skipped Files:") # noqa
|
||||
log.debug("📄 Skipped Files:")
|
||||
for f in skipped_files:
|
||||
print(" 📄", f.name) # noqa
|
||||
log.debug(f" 📄 {f.name}")
|
||||
|
||||
print("❌ Failed Modules:") # noqa
|
||||
for f in failed_modules:
|
||||
print(" ❌", f.name) # noqa
|
||||
if len(failed_modules) > 0:
|
||||
log.error("❌ Failed Modules:")
|
||||
for f in failed_modules:
|
||||
log.error(f" ❌ {f.name}")
|
||||
|
||||
|
||||
def main():
|
||||
log.debug("\n-- Starting Global Components Generator --")
|
||||
generate_global_components_types()
|
||||
|
||||
log.debug("\n-- Starting Pydantic To Typescript Generator --")
|
||||
generate_typescript_types()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("\n-- Starting Global Components Generator --") # noqa
|
||||
generate_global_components_types()
|
||||
|
||||
print("\n-- Starting Pydantic To Typescript Generator --") # noqa
|
||||
generate_typescript_types()
|
||||
main()
|
||||
28
dev/code-generation/main.py
Normal file
28
dev/code-generation/main.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from pathlib import Path
|
||||
|
||||
import gen_py_pytest_data_paths
|
||||
import gen_py_pytest_routes
|
||||
import gen_py_schema_exports
|
||||
import gen_ts_locales
|
||||
import gen_ts_types
|
||||
from utils import log
|
||||
|
||||
CWD = Path(__file__).parent
|
||||
|
||||
|
||||
def main():
|
||||
items = [
|
||||
(gen_py_schema_exports.main, "schema exports"),
|
||||
(gen_ts_types.main, "frontend types"),
|
||||
(gen_ts_locales.main, "locales"),
|
||||
(gen_py_pytest_data_paths.main, "test data paths"),
|
||||
(gen_py_pytest_routes.main, "pytest routes"),
|
||||
]
|
||||
|
||||
for func, name in items:
|
||||
log.info(f"Generating {name}...")
|
||||
func()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,9 +1,11 @@
|
||||
{% for mod in mods %}
|
||||
class {{mod.name}}Routes:{% for route in mod.routes %}{% if not route.path_is_func %}
|
||||
{{route.name_snake}} = "{{ route.py_route }}"{% endif %}{% endfor %}{% for route in mod.routes %}
|
||||
{% if route.path_is_func %}
|
||||
@staticmethod
|
||||
def {{route.name_snake}}({{ route.path_vars|join(", ") }}):
|
||||
return f"{{route.py_route}}"
|
||||
{% endif %}{% endfor %}
|
||||
{% endfor %}
|
||||
# This Content is Auto Generated for Pytest
|
||||
prefix = "{{paths.prefix}}"
|
||||
{% for path in paths.static_paths %}
|
||||
{{ path.router_slug }} = "{{path.prefix}}{{ path.route }}"
|
||||
"""`{{path.prefix}}{{ path.route }}`"""{% endfor %}
|
||||
{% for path in paths.function_paths %}
|
||||
|
||||
def {{path.router_slug}}({{path.var|join(", ")}}):
|
||||
"""`{{ paths.prefix }}{{ path.route }}`"""
|
||||
return f"{prefix}{{ path.route }}"
|
||||
{% endfor %}
|
||||
|
||||
25
dev/code-generation/utils/__init__.py
Normal file
25
dev/code-generation/utils/__init__.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from .open_api_parser import OpenAPIParser
|
||||
from .route import HTTPRequest, ParameterIn, RequestBody, RequestType, RouteObject, RouterParameter
|
||||
from .static import PROJECT_DIR, CodeDest, CodeKeys, CodeTemplates, Directories
|
||||
from .template import CodeSlicer, find_start_end, get_indentation_of_string, inject_inline, log, render_python_template
|
||||
|
||||
__all__ = [
|
||||
"CodeDest",
|
||||
"CodeKeys",
|
||||
"CodeSlicer",
|
||||
"CodeTemplates",
|
||||
"Directories",
|
||||
"find_start_end",
|
||||
"get_indentation_of_string",
|
||||
"HTTPRequest",
|
||||
"inject_inline",
|
||||
"log",
|
||||
"OpenAPIParser",
|
||||
"ParameterIn",
|
||||
"PROJECT_DIR",
|
||||
"render_python_template",
|
||||
"RequestBody",
|
||||
"RequestType",
|
||||
"RouteObject",
|
||||
"RouterParameter",
|
||||
]
|
||||
@@ -3,11 +3,12 @@ import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from _static import Directories
|
||||
from fastapi import FastAPI
|
||||
from humps import camelize
|
||||
from slugify import slugify
|
||||
|
||||
from .static import Directories
|
||||
|
||||
|
||||
def get_openapi_spec_by_ref(app, type_reference: str) -> dict:
|
||||
if not type_reference:
|
||||
@@ -3,7 +3,7 @@ from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
from humps import camelize
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel, Extra, Field
|
||||
from slugify import slugify
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ class RequestType(str, Enum):
|
||||
class ParameterIn(str, Enum):
|
||||
query = "query"
|
||||
path = "path"
|
||||
header = "header"
|
||||
|
||||
|
||||
class RouterParameter(BaseModel):
|
||||
@@ -37,10 +38,16 @@ class RouterParameter(BaseModel):
|
||||
name: str
|
||||
location: ParameterIn = Field(..., alias="in")
|
||||
|
||||
class Config:
|
||||
extra = Extra.allow
|
||||
|
||||
|
||||
class RequestBody(BaseModel):
|
||||
required: bool = False
|
||||
|
||||
class Config:
|
||||
extra = Extra.allow
|
||||
|
||||
|
||||
class HTTPRequest(BaseModel):
|
||||
request_type: RequestType
|
||||
@@ -49,7 +56,10 @@ class HTTPRequest(BaseModel):
|
||||
requestBody: Optional[RequestBody]
|
||||
|
||||
parameters: list[RouterParameter] = []
|
||||
tags: list[str]
|
||||
tags: list[str] | None = []
|
||||
|
||||
class Config:
|
||||
extra = Extra.allow
|
||||
|
||||
def list_as_js_object_string(self, parameters, braces=True):
|
||||
if len(parameters) == 0:
|
||||
26
dev/code-generation/utils/static.py
Normal file
26
dev/code-generation/utils/static.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from pathlib import Path
|
||||
|
||||
PARENT = Path(__file__).parent.parent
|
||||
PROJECT_DIR = Path(__file__).parent.parent.parent.parent
|
||||
|
||||
|
||||
class Directories:
|
||||
out_dir = PARENT / "generated"
|
||||
|
||||
|
||||
class CodeTemplates:
|
||||
interface = PARENT / "templates" / "interface.js"
|
||||
pytest_routes = PARENT / "templates" / "test_routes.py.j2"
|
||||
|
||||
|
||||
class CodeDest:
|
||||
interface = PARENT / "generated" / "interface.js"
|
||||
pytest_routes = PARENT / "generated" / "test_routes.py"
|
||||
use_locales = PROJECT_DIR / "frontend" / "composables" / "use-locales" / "available-locales.ts"
|
||||
|
||||
|
||||
class CodeKeys:
|
||||
"""Hard coded comment IDs that are used to generate code"""
|
||||
|
||||
nuxt_local_messages = "MESSAGE_LOCALES"
|
||||
nuxt_local_dates = "DATE_LOCALES"
|
||||
@@ -1,3 +1,4 @@
|
||||
import logging
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
@@ -5,9 +6,15 @@ from pathlib import Path
|
||||
import black
|
||||
import isort
|
||||
from jinja2 import Template
|
||||
from rich.logging import RichHandler
|
||||
|
||||
FORMAT = "%(message)s"
|
||||
logging.basicConfig(level=logging.INFO, format=FORMAT, datefmt="[%X]", handlers=[RichHandler()])
|
||||
|
||||
log = logging.getLogger("rich")
|
||||
|
||||
|
||||
def render_python_template(template_file: Path | str, dest: Path, data: dict) -> str:
|
||||
def render_python_template(template_file: Path | str, dest: Path, data: dict):
|
||||
"""Render and Format a Jinja2 Template for Python Code"""
|
||||
if isinstance(template_file, Path):
|
||||
tplt = Template(template_file.read_text())
|
||||
@@ -15,7 +22,9 @@ def render_python_template(template_file: Path | str, dest: Path, data: dict) ->
|
||||
tplt = Template(template_file)
|
||||
|
||||
text = tplt.render(data=data)
|
||||
|
||||
text = black.format_str(text, mode=black.FileMode())
|
||||
|
||||
dest.write_text(text)
|
||||
isort.file(dest)
|
||||
|
||||
@@ -37,7 +46,6 @@ class CodeSlicer:
|
||||
|
||||
def push_line(self, string: str) -> None:
|
||||
self._next_line = self._next_line or self.start + 1
|
||||
print(self.indentation)
|
||||
self.text.insert(self._next_line, self.indentation + string + "\n")
|
||||
self._next_line += 1
|
||||
|
||||
@@ -46,7 +54,7 @@ def get_indentation_of_string(line: str, comment_char: str = "//") -> str:
|
||||
return re.sub(rf"{comment_char}.*", "", line).removesuffix("\n")
|
||||
|
||||
|
||||
def find_start_end(file_text: list[str], gen_id: str) -> tuple[int, int]:
|
||||
def find_start_end(file_text: list[str], gen_id: str) -> tuple[int, int, str]:
|
||||
start = None
|
||||
end = None
|
||||
indentation = None
|
||||
@@ -84,7 +92,7 @@ def inject_inline(file_path: Path, key: str, code: list[str]) -> None:
|
||||
|
||||
"""
|
||||
|
||||
with open(file_path, "r") as f:
|
||||
with open(file_path) as f:
|
||||
file_text = f.readlines()
|
||||
|
||||
start, end, indentation = find_start_end(file_text, key)
|
||||
@@ -2,8 +2,13 @@ import json
|
||||
import random
|
||||
import string
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
|
||||
import requests
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
def random_string(length: int) -> str:
|
||||
@@ -14,6 +19,218 @@ def payload_factory() -> dict:
|
||||
return {"name": random_string(15)}
|
||||
|
||||
|
||||
def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dict:
|
||||
return {
|
||||
"id": id,
|
||||
"userId": userId,
|
||||
"groupId": groupId,
|
||||
"name": name,
|
||||
"slug": slug,
|
||||
"image": "tNRG",
|
||||
"recipeYield": "9 servings",
|
||||
"totalTime": "33 Minutes",
|
||||
"prepTime": "20 Minutes",
|
||||
"cookTime": None,
|
||||
"performTime": "13 Minutes",
|
||||
"description": "These Levain Bakery-Style Peanut Butter Cookies are the ULTIMATE for serious PB lovers! Supremely thick and chewy with gooey centers and a soft texture, they're packed with peanut butter flavor and Reese's Pieces for the most amazing cookie ever!",
|
||||
"recipeCategory": [],
|
||||
"tags": [],
|
||||
"tools": [],
|
||||
"rating": None,
|
||||
"orgURL": "https://thedomesticrebel.com/2021/04/28/levain-bakery-style-ultimate-peanut-butter-cookies/",
|
||||
"recipeIngredient": [
|
||||
{
|
||||
"title": None,
|
||||
"note": "1 cup unsalted butter, cut into cubes",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"referenceId": "ea3b6702-9532-4fbc-a40b-f99917831c26",
|
||||
},
|
||||
{
|
||||
"title": None,
|
||||
"note": "1 cup light brown sugar",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"referenceId": "c5bbfefb-1e23-4ffd-af88-c0363a0fae82",
|
||||
},
|
||||
{
|
||||
"title": None,
|
||||
"note": "1/2 cup granulated white sugar",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"referenceId": "034f481b-c426-4a17-b983-5aea9be4974b",
|
||||
},
|
||||
{
|
||||
"title": None,
|
||||
"note": "2 large eggs",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"referenceId": "37c1f796-3bdb-4856-859f-dbec90bc27e4",
|
||||
},
|
||||
{
|
||||
"title": None,
|
||||
"note": "2 tsp vanilla extract",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"referenceId": "85561ace-f249-401d-834c-e600a2f6280e",
|
||||
},
|
||||
{
|
||||
"title": None,
|
||||
"note": "1/2 cup creamy peanut butter",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"referenceId": "ac91bda0-e8a8-491a-976a-ae4e72418cfd",
|
||||
},
|
||||
{
|
||||
"title": None,
|
||||
"note": "1 tsp cornstarch",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"referenceId": "4d1256b3-115e-4475-83cd-464fbc304cb0",
|
||||
},
|
||||
{
|
||||
"title": None,
|
||||
"note": "1 tsp baking soda",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"referenceId": "64627441-39f9-4ee3-8494-bafe36451d12",
|
||||
},
|
||||
{
|
||||
"title": None,
|
||||
"note": "1/2 tsp salt",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"referenceId": "7ae212d0-3cd1-44b0-899e-ec5bd91fd384",
|
||||
},
|
||||
{
|
||||
"title": None,
|
||||
"note": "1 cup cake flour",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"referenceId": "06967994-8548-4952-a8cc-16e8db228ebd",
|
||||
},
|
||||
{
|
||||
"title": None,
|
||||
"note": "2 cups all-purpose flour",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"referenceId": "bdb33b23-c767-4465-acf8-3b8e79eb5691",
|
||||
},
|
||||
{
|
||||
"title": None,
|
||||
"note": "2 cups peanut butter chips",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"referenceId": "12ba0af8-affd-4fb2-9cca-6f1b3e8d3aef",
|
||||
},
|
||||
{
|
||||
"title": None,
|
||||
"note": "1½ cups Reese's Pieces candies",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"referenceId": "4bdc0598-a3eb-41ee-8af0-4da9348fbfe2",
|
||||
},
|
||||
],
|
||||
"dateAdded": "2022-09-03",
|
||||
"dateUpdated": "2022-09-10T15:18:19.866085",
|
||||
"createdAt": "2022-09-03T18:31:17.488118",
|
||||
"updateAt": "2022-09-10T15:18:19.869630",
|
||||
"recipeInstructions": [
|
||||
{
|
||||
"id": "60ae53a3-b3ff-40ee-bae3-89fea0b1c637",
|
||||
"title": "",
|
||||
"text": "Preheat oven to 410° degrees F. Line 2 baking sheets with parchment paper or silicone liners; set aside.",
|
||||
"ingredientReferences": [],
|
||||
},
|
||||
{
|
||||
"id": "4e1c30c2-2e96-4a0a-b750-23c9ea3640f8",
|
||||
"title": "",
|
||||
"text": "In the bowl of a stand mixer, cream together the cubed butter, brown sugar and granulated sugar with the paddle attachment for 30 seconds on low speed. Increase speed to medium and beat for another 30 seconds, then increase to medium-high speed and beat for another 30 seconds until mixture is creamy and smooth. Beat in the eggs, one at a time, followed by the vanilla extract and peanut butter, scraping down the sides and bottom of the bowl as needed.",
|
||||
"ingredientReferences": [],
|
||||
},
|
||||
{
|
||||
"id": "9fb8e2a2-d410-445c-bafc-c059203e6f4b",
|
||||
"title": "",
|
||||
"text": "Add in the cornstarch, baking soda, salt, cake flour, and all-purpose flour and mix on low speed until just combined. Fold in the peanut butter chips and Reese's Pieces candies by hand until fully incorporated. Chill the dough uncovered in the fridge for 15 minutes.",
|
||||
"ingredientReferences": [],
|
||||
},
|
||||
{
|
||||
"id": "1ceb9aa4-49f7-4d4a-996f-3c715eb74642",
|
||||
"title": "",
|
||||
"text": 'Using a digital kitchen scale for accuracy, weigh out 6 ounces of cookie dough in a loose, rough textured ball. I like to make my cookie dough balls kind of tall as well. You do not want the dough balls to be smooth and compacted. Place on the baking sheet. Repeat with remaining dough balls, staggering on the baking sheet at least 3" apart from one another, and only placing 4 dough balls per baking sheet.',
|
||||
"ingredientReferences": [],
|
||||
},
|
||||
{
|
||||
"id": "591993fc-72bb-4091-8a12-84640c523fc1",
|
||||
"title": "",
|
||||
"text": "Bake one baking sheet at a time in the center rack of the oven for 10-13 minutes or until the tops are light golden brown and the exterior is dry and dull looking. Centers will be slightly underdone and gooey; this is okay and the cookies will finish cooking some once removed from the oven. Let stand on the baking sheets for at least 30 minutes before serving; the cookies are very delicate and fragile once removed from the oven and need time to set before being moved. Keep remaining dough refrigerated while other cookies bake.",
|
||||
"ingredientReferences": [],
|
||||
},
|
||||
],
|
||||
"nutrition": {
|
||||
"calories": None,
|
||||
"fatContent": None,
|
||||
"proteinContent": None,
|
||||
"carbohydrateContent": None,
|
||||
"fiberContent": None,
|
||||
"sodiumContent": None,
|
||||
"sugarContent": None,
|
||||
},
|
||||
"settings": {
|
||||
"public": True,
|
||||
"showNutrition": False,
|
||||
"showAssets": False,
|
||||
"landscapeView": False,
|
||||
"disableComments": False,
|
||||
"disableAmount": True,
|
||||
"locked": False,
|
||||
},
|
||||
"assets": [],
|
||||
"notes": [],
|
||||
"extras": {},
|
||||
"comments": [],
|
||||
}
|
||||
|
||||
|
||||
def login(username="changeme@email.com", password="MyPassword"):
|
||||
|
||||
payload = {"username": username, "password": password}
|
||||
@@ -30,32 +247,90 @@ def populate_data(token):
|
||||
r = requests.post("http://localhost:9000/api/recipes", json=payload, headers=token)
|
||||
|
||||
if r.status_code != 201:
|
||||
print(f"Error: {r.status_code}")
|
||||
print(r.text)
|
||||
console.print(f"Error: {r.status_code}")
|
||||
console.print(r.text)
|
||||
exit()
|
||||
|
||||
else:
|
||||
print(f"Created recipe: {payload}")
|
||||
recipe_json = requests.get(f"http://localhost:9000/api/recipes/{payload['name']}", headers=token)
|
||||
|
||||
if recipe_json.status_code != 200:
|
||||
console.print(f"Error: {recipe_json.status_code}")
|
||||
console.print(recipe_json.text)
|
||||
exit()
|
||||
|
||||
recipe = json.loads(recipe_json.text)
|
||||
update_data = recipe_data(recipe["name"], recipe["slug"], recipe["id"], recipe["userId"], recipe["groupId"])
|
||||
|
||||
r = requests.put(f"http://localhost:9000/api/recipes/{update_data['slug']}", json=update_data, headers=token)
|
||||
if r.status_code != 200:
|
||||
console.print(f"Error: {r.status_code}")
|
||||
console.print(r.text)
|
||||
exit()
|
||||
|
||||
|
||||
def time_request(url, headers):
|
||||
@dataclass(slots=True)
|
||||
class Result:
|
||||
recipes: int
|
||||
time: float
|
||||
|
||||
|
||||
def time_request(url, headers) -> Result:
|
||||
start = time.time()
|
||||
r = requests.get(url, headers=headers)
|
||||
|
||||
print(f"Total Recipes {len(r.json())}")
|
||||
end = time.time()
|
||||
print(end - start)
|
||||
|
||||
return Result(len(r.json()["items"]), end - start)
|
||||
|
||||
|
||||
def main():
|
||||
print("Starting...")
|
||||
token = login()
|
||||
# populate_data(token)
|
||||
|
||||
for _ in range(10):
|
||||
time_request("http://localhost:9000/api/recipes", token)
|
||||
results: list[Result] = []
|
||||
|
||||
print("Finished...")
|
||||
for _ in range(10):
|
||||
result = time_request("http://localhost:9000/api/recipes?perPage=-1&page=1&loadFood=true", token)
|
||||
results.append(result)
|
||||
|
||||
min, max, average = 99, 0, 0
|
||||
|
||||
for result in results:
|
||||
if result.time < min:
|
||||
min = result.time
|
||||
|
||||
if result.time > max:
|
||||
max = result.time
|
||||
|
||||
average += result.time
|
||||
|
||||
tbl1 = Table(title="Requests")
|
||||
|
||||
tbl1.add_column("Recipes", justify="right", style="cyan", no_wrap=True)
|
||||
tbl1.add_column("Time", justify="right", style="magenta")
|
||||
|
||||
for result in results:
|
||||
tbl1.add_row(
|
||||
str(result.recipes),
|
||||
str(result.time),
|
||||
)
|
||||
|
||||
tbl2 = Table(title="Summary")
|
||||
|
||||
tbl2.add_column("Min", justify="right", style="green")
|
||||
tbl2.add_column("Max", justify="right", style="green")
|
||||
tbl2.add_column("Average", justify="right", style="green")
|
||||
|
||||
tbl2.add_row(
|
||||
str(round(min * 1000)) + "ms",
|
||||
str(round(max * 1000)) + "ms",
|
||||
str(round((average / len(results)) * 1000)) + "ms",
|
||||
)
|
||||
|
||||
console = Console()
|
||||
console.print(tbl1)
|
||||
console.print(tbl2)
|
||||
|
||||
# Best Time 289 / 405/ 247
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,214 +0,0 @@
|
||||
import json
|
||||
import re
|
||||
from enum import Enum
|
||||
from itertools import groupby
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import FastAPI
|
||||
from humps import camelize
|
||||
from jinja2 import Template
|
||||
from pydantic import BaseModel, Field
|
||||
from slugify import slugify
|
||||
|
||||
from mealie.app import app
|
||||
|
||||
CWD = Path(__file__).parent
|
||||
OUT_DIR = CWD / "output"
|
||||
TEMPLATES_DIR = CWD / "templates"
|
||||
|
||||
JS_DIR = OUT_DIR / "javascriptAPI"
|
||||
JS_DIR.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
|
||||
class RouteObject:
|
||||
def __init__(self, route_string) -> None:
|
||||
self.prefix = "/" + route_string.split("/")[1]
|
||||
self.route = "/" + route_string.split("/", 2)[2]
|
||||
self.js_route = self.route.replace("{", "${")
|
||||
self.parts = route_string.split("/")[1:]
|
||||
self.var = re.findall(r"\{(.*?)\}", route_string)
|
||||
self.is_function = "{" in self.route
|
||||
self.router_slug = slugify("_".join(self.parts[1:]), separator="_")
|
||||
self.router_camel = camelize(self.router_slug)
|
||||
|
||||
|
||||
class RequestType(str, Enum):
|
||||
get = "get"
|
||||
put = "put"
|
||||
post = "post"
|
||||
patch = "patch"
|
||||
delete = "delete"
|
||||
|
||||
|
||||
class ParameterIn(str, Enum):
|
||||
query = "query"
|
||||
path = "path"
|
||||
|
||||
|
||||
class RouterParameter(BaseModel):
|
||||
required: bool = False
|
||||
name: str
|
||||
location: ParameterIn = Field(..., alias="in")
|
||||
|
||||
|
||||
class RequestBody(BaseModel):
|
||||
required: bool = False
|
||||
|
||||
|
||||
class HTTPRequest(BaseModel):
|
||||
request_type: RequestType
|
||||
description: str = ""
|
||||
summary: str
|
||||
requestBody: Optional[RequestBody]
|
||||
|
||||
parameters: list[RouterParameter] = []
|
||||
tags: list[str]
|
||||
|
||||
def list_as_js_object_string(self, parameters, braces=True):
|
||||
if len(parameters) == 0:
|
||||
return ""
|
||||
|
||||
if braces:
|
||||
return "{" + ", ".join(parameters) + "}"
|
||||
else:
|
||||
return ", ".join(parameters)
|
||||
|
||||
def payload(self):
|
||||
return "payload" if self.requestBody else ""
|
||||
|
||||
def function_args(self):
|
||||
all_params = [p.name for p in self.parameters]
|
||||
if self.requestBody:
|
||||
all_params.append("payload")
|
||||
return self.list_as_js_object_string(all_params)
|
||||
|
||||
def query_params(self):
|
||||
params = [param.name for param in self.parameters if param.location == ParameterIn.query]
|
||||
return self.list_as_js_object_string(params)
|
||||
|
||||
def path_params(self):
|
||||
params = [param.name for param in self.parameters if param.location == ParameterIn.path]
|
||||
return self.list_as_js_object_string(parameters=params, braces=False)
|
||||
|
||||
@property
|
||||
def summary_camel(self):
|
||||
return camelize(slugify(self.summary))
|
||||
|
||||
@property
|
||||
def js_docs(self):
|
||||
return self.description.replace("\n", " \n * ")
|
||||
|
||||
|
||||
class PathObject(BaseModel):
|
||||
route_object: RouteObject
|
||||
http_verbs: list[HTTPRequest]
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
|
||||
def get_path_objects(app: FastAPI):
|
||||
paths = []
|
||||
|
||||
for key, value in app.openapi().items():
|
||||
if key == "paths":
|
||||
for key, value in value.items():
|
||||
|
||||
paths.append(
|
||||
PathObject(
|
||||
route_object=RouteObject(key),
|
||||
http_verbs=[HTTPRequest(request_type=k, **v) for k, v in value.items()],
|
||||
)
|
||||
)
|
||||
|
||||
return paths
|
||||
|
||||
|
||||
def dump_open_api(app: FastAPI):
|
||||
"""Writes the Open API as JSON to a json file"""
|
||||
OPEN_API_FILE = CWD / "openapi.json"
|
||||
|
||||
with open(OPEN_API_FILE, "w") as f:
|
||||
f.write(json.dumps(app.openapi()))
|
||||
|
||||
|
||||
def read_template(file: Path):
|
||||
with open(file, "r") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def generate_python_templates(static_paths: list[PathObject], function_paths: list[PathObject]):
|
||||
PYTEST_TEMPLATE = TEMPLATES_DIR / "pytest_routes.j2"
|
||||
PYTHON_OUT_FILE = OUT_DIR / "app_routes.py"
|
||||
|
||||
template = Template(read_template(PYTEST_TEMPLATE))
|
||||
content = template.render(
|
||||
paths={
|
||||
"prefix": "/api",
|
||||
"static_paths": static_paths,
|
||||
"function_paths": function_paths,
|
||||
}
|
||||
)
|
||||
with open(PYTHON_OUT_FILE, "w") as f:
|
||||
f.write(content)
|
||||
|
||||
return
|
||||
|
||||
|
||||
def generate_js_templates(paths: list[PathObject]):
|
||||
# Template Path
|
||||
JS_API_INTERFACE = TEMPLATES_DIR / "js_api_interface.j2"
|
||||
JS_INDEX = TEMPLATES_DIR / "js_index.j2"
|
||||
|
||||
INTERFACES_DIR = JS_DIR / "interfaces"
|
||||
INTERFACES_DIR.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
all_tags = []
|
||||
for tag, tag_paths in groupby(paths, lambda x: x.http_verbs[0].tags[0]):
|
||||
file_name = slugify(tag, separator="-")
|
||||
|
||||
tag = camelize(tag)
|
||||
|
||||
tag_paths: list[PathObject] = list(tag_paths)
|
||||
|
||||
template = Template(read_template(JS_API_INTERFACE))
|
||||
content = template.render(
|
||||
paths={
|
||||
"prefix": "/api",
|
||||
"static_paths": [x.route_object for x in tag_paths if not x.route_object.is_function],
|
||||
"function_paths": [x.route_object for x in tag_paths if x.route_object.is_function],
|
||||
"all_paths": tag_paths,
|
||||
"export_name": tag,
|
||||
}
|
||||
)
|
||||
|
||||
tag: dict = {"camel": camelize(tag), "slug": file_name}
|
||||
all_tags.append(tag)
|
||||
|
||||
with open(INTERFACES_DIR.joinpath(file_name + ".ts"), "w") as f:
|
||||
f.write(content)
|
||||
|
||||
template = Template(read_template(JS_INDEX))
|
||||
content = template.render(files={"files": all_tags})
|
||||
|
||||
with open(JS_DIR.joinpath("index.js"), "w") as f:
|
||||
f.write(content)
|
||||
|
||||
|
||||
def generate_template(app):
|
||||
dump_open_api(app)
|
||||
paths = get_path_objects(app)
|
||||
|
||||
static_paths = [x.route_object for x in paths if not x.route_object.is_function]
|
||||
function_paths = [x.route_object for x in paths if x.route_object.is_function]
|
||||
|
||||
static_paths.sort(key=lambda x: x.router_slug)
|
||||
function_paths.sort(key=lambda x: x.router_slug)
|
||||
|
||||
generate_python_templates(static_paths, function_paths)
|
||||
generate_js_templates(paths)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
generate_template(app)
|
||||
@@ -1,26 +0,0 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
|
||||
CWD = Path(__file__).parent
|
||||
|
||||
|
||||
def login(username="changeme@email.com", password="MyPassword"):
|
||||
|
||||
payload = {"username": username, "password": password}
|
||||
r = requests.post("http://localhost:9000/api/auth/token", payload)
|
||||
|
||||
# Bearer
|
||||
token = json.loads(r.text).get("access_token")
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
|
||||
def main():
|
||||
print("Starting...")
|
||||
|
||||
print("Finished...")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,159 +0,0 @@
|
||||
import json
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from slugify import slugify
|
||||
|
||||
CWD = Path(__file__).parent
|
||||
|
||||
PROJECT_BASE = CWD.parent.parent
|
||||
|
||||
server_side_msgs = PROJECT_BASE / "mealie" / "utils" / "error_messages.py"
|
||||
en_us_msgs = PROJECT_BASE / "frontend" / "lang" / "errors" / "en-US.json"
|
||||
client_side_msgs = PROJECT_BASE / "frontend" / "utils" / "error-messages.ts"
|
||||
|
||||
GENERATE_MESSAGES = [
|
||||
# User Related
|
||||
"user",
|
||||
"webhook",
|
||||
"token",
|
||||
# Group Related
|
||||
"group",
|
||||
"cookbook",
|
||||
"mealplan",
|
||||
# Recipe Related
|
||||
"scraper",
|
||||
"recipe",
|
||||
"ingredient",
|
||||
"food",
|
||||
"unit",
|
||||
# Admin Related
|
||||
"backup",
|
||||
"migration",
|
||||
"event",
|
||||
]
|
||||
|
||||
|
||||
class ErrorMessage:
|
||||
def __init__(self, prefix, verb) -> None:
|
||||
self.message = f"{prefix.title()} {verb.title()} Failed"
|
||||
self.snake = slugify(self.message, separator="_")
|
||||
self.kabab = slugify(self.message, separator="-")
|
||||
|
||||
def factory(prefix) -> list["ErrorMessage"]:
|
||||
verbs = ["Create", "Update", "Delete"]
|
||||
return [ErrorMessage(prefix, verb) for verb in verbs]
|
||||
|
||||
|
||||
@dataclass
|
||||
class CodeGenLines:
|
||||
start: int
|
||||
end: int
|
||||
|
||||
indentation: str
|
||||
text: list[str]
|
||||
|
||||
_next_line = None
|
||||
|
||||
def purge_lines(self) -> None:
|
||||
start = self.start + 1
|
||||
end = self.end
|
||||
del self.text[start:end]
|
||||
|
||||
def push_line(self, string: str) -> None:
|
||||
self._next_line = self._next_line or self.start + 1
|
||||
self.text.insert(self._next_line, self.indentation + string)
|
||||
self._next_line += 1
|
||||
|
||||
|
||||
def find_start(file_text: list[str], gen_id: str):
|
||||
for x, line in enumerate(file_text):
|
||||
if "CODE_GEN_ID:" in line and gen_id in line:
|
||||
return x, line
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def find_end(file_text: list[str], gen_id: str):
|
||||
for x, line in enumerate(file_text):
|
||||
if f"END {gen_id}" in line:
|
||||
return x, line
|
||||
return None
|
||||
|
||||
|
||||
def get_indentation_of_string(line: str):
|
||||
return re.sub(r"#.*", "", line).removesuffix("\n")
|
||||
|
||||
|
||||
def get_messages(message_prefix: str) -> str:
|
||||
prefix = message_prefix.lower()
|
||||
|
||||
return [
|
||||
f'{prefix}_create_failure = "{prefix}-create-failure"\n',
|
||||
f'{prefix}_update_failure = "{prefix}-update-failure"\n',
|
||||
f'{prefix}_delete_failure = "{prefix}-delete-failure"\n',
|
||||
]
|
||||
|
||||
|
||||
def code_gen_factory(file_path: Path) -> CodeGenLines:
|
||||
with open(file_path, "r") as file:
|
||||
text = file.readlines()
|
||||
start_num, line = find_start(text, "ERROR_MESSAGE_ENUMS")
|
||||
indentation = get_indentation_of_string(line)
|
||||
end_num, line = find_end(text, "ERROR_MESSAGE_ENUMS")
|
||||
|
||||
return CodeGenLines(
|
||||
start=start_num,
|
||||
end=end_num,
|
||||
indentation=indentation,
|
||||
text=text,
|
||||
)
|
||||
|
||||
|
||||
def write_to_locals(messages: list[ErrorMessage]) -> None:
|
||||
with open(en_us_msgs, "r") as f:
|
||||
existing_msg = json.loads(f.read())
|
||||
|
||||
for msg in messages:
|
||||
if msg.kabab in existing_msg:
|
||||
continue
|
||||
|
||||
existing_msg[msg.kabab] = msg.message
|
||||
print(f"Added Key {msg.kabab} to 'en-US.json'")
|
||||
|
||||
with open(en_us_msgs, "w") as f:
|
||||
f.write(json.dumps(existing_msg, indent=4))
|
||||
|
||||
|
||||
def main():
|
||||
print("Starting...")
|
||||
GENERATE_MESSAGES.sort()
|
||||
|
||||
code_gen = code_gen_factory(server_side_msgs)
|
||||
code_gen.purge_lines()
|
||||
|
||||
messages = []
|
||||
for msg_type in GENERATE_MESSAGES:
|
||||
messages += get_messages(msg_type)
|
||||
messages.append("\n")
|
||||
|
||||
for msg in messages:
|
||||
code_gen.push_line(msg)
|
||||
|
||||
with open(server_side_msgs, "w") as file:
|
||||
file.writelines(code_gen.text)
|
||||
|
||||
# Locals
|
||||
|
||||
local_msgs = []
|
||||
for msg_type in GENERATE_MESSAGES:
|
||||
local_msgs += ErrorMessage.factory(msg_type)
|
||||
|
||||
write_to_locals(local_msgs)
|
||||
|
||||
print("Done!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,32 +0,0 @@
|
||||
import json
|
||||
|
||||
import requests
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class GithubIssue(BaseModel):
|
||||
url: str
|
||||
number: int
|
||||
title: str
|
||||
|
||||
|
||||
def get_issues_by_label(label="fixed-pending-release") -> list[GithubIssue]:
|
||||
|
||||
issues_url = f"https://api.github.com/repos/hay-kot/mealie/issues?labels={label}"
|
||||
|
||||
response = requests.get(issues_url)
|
||||
issues = json.loads(response.text)
|
||||
return [GithubIssue(**issue) for issue in issues]
|
||||
|
||||
|
||||
def format_markdown_list(issues: list[GithubIssue]) -> str:
|
||||
return "\n".join(f"- [{issue.number}]({issue.url}) - {issue.title}" for issue in issues)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
issues = get_issues_by_label()
|
||||
print(format_markdown_list(issues))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,156 +0,0 @@
|
||||
# This Content is Auto Generated for Pytest
|
||||
|
||||
|
||||
class AppRoutes:
|
||||
def __init__(self) -> None:
|
||||
self.prefix = '/api'
|
||||
|
||||
self.about_events = "/api/about/events"
|
||||
self.about_events_notifications = "/api/about/events/notifications"
|
||||
self.about_events_notifications_test = "/api/about/events/notifications/test"
|
||||
self.about_recipes_defaults = "/api/about/recipes/defaults"
|
||||
self.auth_refresh = "/api/auth/refresh"
|
||||
self.auth_token = "/api/auth/token"
|
||||
self.auth_token_long = "/api/auth/token/long"
|
||||
self.backups_available = "/api/backups/available"
|
||||
self.backups_export_database = "/api/backups/export/database"
|
||||
self.backups_upload = "/api/backups/upload"
|
||||
self.categories = "/api/categories"
|
||||
self.categories_empty = "/api/categories/empty"
|
||||
self.debug = "/api/debug"
|
||||
self.debug_last_recipe_json = "/api/debug/last-recipe-json"
|
||||
self.debug_log = "/api/debug/log"
|
||||
self.debug_statistics = "/api/debug/statistics"
|
||||
self.debug_version = "/api/debug/version"
|
||||
self.groups = "/api/groups"
|
||||
self.groups_self = "/api/groups/self"
|
||||
self.meal_plans_all = "/api/meal-plans/all"
|
||||
self.meal_plans_create = "/api/meal-plans/create"
|
||||
self.meal_plans_this_week = "/api/meal-plans/this-week"
|
||||
self.meal_plans_today = "/api/meal-plans/today"
|
||||
self.meal_plans_today_image = "/api/meal-plans/today/image"
|
||||
self.migrations = "/api/migrations"
|
||||
self.recipes = "/api/recipes"
|
||||
self.recipes_category = "/api/recipes/category"
|
||||
self.recipes_create = "/api/recipes/create"
|
||||
self.recipes_create_from_zip = "/api/recipes/create-from-zip"
|
||||
self.recipes_create_url = "/api/recipes/create-url"
|
||||
self.recipes_summary_uncategorized = "/api/recipes/summary/uncategorized"
|
||||
self.recipes_summary_untagged = "/api/recipes/summary/untagged"
|
||||
self.recipes_tag = "/api/recipes/tag"
|
||||
self.recipes_test_scrape_url = "/api/recipes/test-scrape-url"
|
||||
self.shopping_lists = "/api/shopping-lists"
|
||||
self.site_settings = "/api/site-settings"
|
||||
self.site_settings_custom_pages = "/api/site-settings/custom-pages"
|
||||
self.site_settings_webhooks_test = "/api/site-settings/webhooks/test"
|
||||
self.tags = "/api/tags"
|
||||
self.tags_empty = "/api/tags/empty"
|
||||
self.themes = "/api/themes"
|
||||
self.themes_create = "/api/themes/create"
|
||||
self.users = "/api/users"
|
||||
self.users_api_tokens = "/api/users/api-tokens"
|
||||
self.users_self = "/api/users/self"
|
||||
self.users_sign_ups = "/api/users/sign-ups"
|
||||
self.utils_download = "/api/utils/download"
|
||||
|
||||
def about_events_id(self, id):
|
||||
return f"{self.prefix}/about/events/{id}"
|
||||
|
||||
def about_events_notifications_id(self, id):
|
||||
return f"{self.prefix}/about/events/notifications/{id}"
|
||||
|
||||
def backups_file_name_delete(self, file_name):
|
||||
return f"{self.prefix}/backups/{file_name}/delete"
|
||||
|
||||
def backups_file_name_download(self, file_name):
|
||||
return f"{self.prefix}/backups/{file_name}/download"
|
||||
|
||||
def backups_file_name_import(self, file_name):
|
||||
return f"{self.prefix}/backups/{file_name}/import"
|
||||
|
||||
def categories_category(self, category):
|
||||
return f"{self.prefix}/categories/{category}"
|
||||
|
||||
def debug_log_num(self, num):
|
||||
return f"{self.prefix}/debug/log/{num}"
|
||||
|
||||
def groups_id(self, id):
|
||||
return f"{self.prefix}/groups/{id}"
|
||||
|
||||
def meal_plans_id(self, id):
|
||||
return f"{self.prefix}/meal-plans/{id}"
|
||||
|
||||
def meal_plans_id_shopping_list(self, id):
|
||||
return f"{self.prefix}/meal-plans/{id}/shopping-list"
|
||||
|
||||
def meal_plans_plan_id(self, plan_id):
|
||||
return f"{self.prefix}/meal-plans/{plan_id}"
|
||||
|
||||
def media_recipes_recipe_slug_assets_file_name(self, recipe_slug, file_name):
|
||||
return f"{self.prefix}/media/recipes/{recipe_slug}/assets/{file_name}"
|
||||
|
||||
def media_recipes_recipe_slug_images_file_name(self, recipe_slug, file_name):
|
||||
return f"{self.prefix}/media/recipes/{recipe_slug}/images/{file_name}"
|
||||
|
||||
def migrations_import_type_file_name_delete(self, import_type, file_name):
|
||||
return f"{self.prefix}/migrations/{import_type}/{file_name}/delete"
|
||||
|
||||
def migrations_import_type_file_name_import(self, import_type, file_name):
|
||||
return f"{self.prefix}/migrations/{import_type}/{file_name}/import"
|
||||
|
||||
def migrations_import_type_upload(self, import_type):
|
||||
return f"{self.prefix}/migrations/{import_type}/upload"
|
||||
|
||||
def recipes_recipe_slug(self, recipe_slug):
|
||||
return f"{self.prefix}/recipes/{recipe_slug}"
|
||||
|
||||
def recipes_recipe_slug_assets(self, recipe_slug):
|
||||
return f"{self.prefix}/recipes/{recipe_slug}/assets"
|
||||
|
||||
def recipes_recipe_slug_image(self, recipe_slug):
|
||||
return f"{self.prefix}/recipes/{recipe_slug}/image"
|
||||
|
||||
def recipes_recipe_slug_zip(self, recipe_slug):
|
||||
return f"{self.prefix}/recipes/{recipe_slug}/zip"
|
||||
|
||||
def recipes_slug_comments(self, slug):
|
||||
return f"{self.prefix}/recipes/{slug}/comments"
|
||||
|
||||
def recipes_slug_comments_id(self, slug, id):
|
||||
return f"{self.prefix}/recipes/{slug}/comments/{id}"
|
||||
|
||||
def shopping_lists_id(self, id):
|
||||
return f"{self.prefix}/shopping-lists/{id}"
|
||||
|
||||
def site_settings_custom_pages_id(self, id):
|
||||
return f"{self.prefix}/site-settings/custom-pages/{id}"
|
||||
|
||||
def tags_tag(self, tag):
|
||||
return f"{self.prefix}/tags/{tag}"
|
||||
|
||||
def themes_id(self, id):
|
||||
return f"{self.prefix}/themes/{id}"
|
||||
|
||||
def users_api_tokens_token_id(self, token_id):
|
||||
return f"{self.prefix}/users/api-tokens/{token_id}"
|
||||
|
||||
def users_id(self, id):
|
||||
return f"{self.prefix}/users/{id}"
|
||||
|
||||
def users_id_favorites(self, id):
|
||||
return f"{self.prefix}/users/{id}/favorites"
|
||||
|
||||
def users_id_favorites_slug(self, id, slug):
|
||||
return f"{self.prefix}/users/{id}/favorites/{slug}"
|
||||
|
||||
def users_id_image(self, id):
|
||||
return f"{self.prefix}/users/{id}/image"
|
||||
|
||||
def users_id_password(self, id):
|
||||
return f"{self.prefix}/users/{id}/password"
|
||||
|
||||
def users_id_reset_password(self, id):
|
||||
return f"{self.prefix}/users/{id}/reset-password"
|
||||
|
||||
def users_sign_ups_token(self, token):
|
||||
return f"{self.prefix}/users/sign-ups/{token}"
|
||||
@@ -1,11 +0,0 @@
|
||||
git checkout dev
|
||||
git merge --strategy=ours master # keep the content of this branch, but record a merge
|
||||
git checkout master
|
||||
git merge dev # fast-forward master up to the merge
|
||||
|
||||
|
||||
## TODOs
|
||||
|
||||
# Create New Branch v0.x.x
|
||||
# Push Branch Version to Github
|
||||
# Create Pull Request
|
||||
@@ -1,17 +0,0 @@
|
||||
import { requests } from "../requests";
|
||||
|
||||
const prefix = '{{paths.prefix}}'
|
||||
|
||||
const routes = { {% for path in paths.static_paths %}
|
||||
{{ path.router_camel }}: `${prefix}{{ path.route }}`,{% endfor %}
|
||||
{% for path in paths.function_paths %}
|
||||
{{path.router_camel}}: ({{path.var|join(", ")}}) => `${prefix}{{ path.js_route }}`,{% endfor %}
|
||||
}
|
||||
|
||||
export const {{paths.export_name}}API = { {% for path in paths.all_paths %} {% for verb in path.http_verbs %}
|
||||
{% if verb.js_docs %}/** {{ verb.js_docs }}
|
||||
*/ {% endif %}
|
||||
async {{ verb.summary_camel }}({{ verb.function_args() }}) {
|
||||
return await requests.{{ verb.request_type.value }}(routes.{{ path.route_object.router_camel }}{% if path.route_object.is_function %}({{verb.path_params()}}){% endif %}, {{ verb.query_params() }} {{ verb.payload() }})
|
||||
}, {% endfor %} {% endfor %}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{% for api in files.files %}
|
||||
import { {{ api.camel }}API } from "./interfaces/{{ api.slug }}" {% endfor %}
|
||||
|
||||
export const api = {
|
||||
{% for api in files.files %}
|
||||
{{api.camel}}: {{api.camel}}API, {% endfor %}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
# This Content is Auto Generated for Pytest
|
||||
|
||||
|
||||
class AppRoutes:
|
||||
def __init__(self) -> None:
|
||||
self.prefix = '{{paths.prefix}}'
|
||||
{% for path in paths.static_paths %}
|
||||
self.{{ path.router_slug }} = "{{path.prefix}}{{ path.route }}"{% endfor %}
|
||||
{% for path in paths.function_paths %}
|
||||
def {{path.router_slug}}(self, {{path.var|join(", ")}}):
|
||||
return f"{self.prefix}{{ path.route }}"
|
||||
{% endfor %}
|
||||
@@ -3,6 +3,10 @@ services:
|
||||
mealie-frontend:
|
||||
container_name: mealie-frontend
|
||||
image: mealie-frontend:dev
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 500M
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
@@ -34,6 +38,10 @@ services:
|
||||
- THEME_DARK_ERROR=#EF5350
|
||||
mealie:
|
||||
container_name: mealie-api
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 1000M
|
||||
build:
|
||||
context: ./
|
||||
target: production
|
||||
@@ -57,6 +65,7 @@ services:
|
||||
|
||||
# =====================================
|
||||
# Web Concurrency
|
||||
WEB_GUNICORN: true
|
||||
WORKERS_PER_CORE: 0.5
|
||||
MAX_WORKERS: 1
|
||||
WEB_CONCURRENCY: 1
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
# vx.x.x COOL TITLE GOES HERE
|
||||
|
||||
**App Version: vx.x.x**
|
||||
|
||||
**Database Version: vx.x.x**
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
!!! error "Breaking Changes"
|
||||
|
||||
#### Database
|
||||
|
||||
#### ENV Variables
|
||||
|
||||
|
||||
## Bug Fixes
|
||||
- Fixed ...
|
||||
|
||||
## Features and Improvements
|
||||
|
||||
### General
|
||||
- New Thing 1
|
||||
|
||||
|
||||
### UI Improvements
|
||||
-
|
||||
|
||||
|
||||
### Behind the Scenes
|
||||
- Refactoring...
|
||||
@@ -67,6 +67,6 @@ This is, what I think, is a big release! Tons of new features and some great qua
|
||||
### Breaking Changes
|
||||
|
||||
!!! error "Breaking Changes"
|
||||
- API endpoints have been refactored to adhear to a more consistent standard. This is a WIP and more changes are likely to occur.
|
||||
- API endpoints have been refactored to adhere to a more consistent standard. This is a WIP and more changes are likely to occur.
|
||||
- Officially Dropped MongoDB Support
|
||||
- Database Breaks! We have not yet implemented a database migration service. As such, upgrades cannot be done by simply pulling the image. You must first export your recipes, update your deployment, and then import your recipes. This pattern is likely to be how upgrades take place prior to v1.0. After v1.0 migrations will be done automatically.
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
- See what's for dinner
|
||||
- Manage Long Live API Tokens (New)
|
||||
- New Admin Dashboard! 🎉
|
||||
- Now you can get some insight on your application with application statics and events.
|
||||
- Now you can get some insight on your application with application statistics and events.
|
||||
- See uncategorized/untagged recipes and organize them!
|
||||
- Backup/Restore right from your dashboard
|
||||
- See server side events. Now you can know who deleted your favorite recipe!
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
|
||||
- Site Settings has been completely revamped. All site-wide settings at defined on the server as ENV variables. The site settings page now only shows you the non-secret values for reference. It also has some helpers to let you know if something isn't configured correctly.
|
||||
- Server Side Bare URL will let you know if the BASE_URL env variable has been set
|
||||
- Secure Site let's you know if you're serving via HTTPS or accessing by localhost. accessing without a secure site will render some of the features unusable.
|
||||
- Secure Site let's you know if you're serving via HTTPS or accessing by localhost. Accessing without a secure site will render some of the features unusable.
|
||||
- Email Configuration Status will let you know if all the email settings have been provided and offer a way to send test emails.
|
||||
|
||||
#### 👨👩👧👦 Users and Groups
|
||||
|
||||
126
docs/docs/changelog/v1.0.0beta-4.md
Normal file
126
docs/docs/changelog/v1.0.0beta-4.md
Normal file
@@ -0,0 +1,126 @@
|
||||
### Security
|
||||
|
||||
#### v1.0.0beta-3 and Under - Recipe Scraper: Server Side Request Forgery Lead To Denial Of Service
|
||||
|
||||
!!! error "CWE-918: Server-Side Request Forgery (SSRF)"
|
||||
In this case if a attacker try to load a huge file then server will try to load the file and eventually server use its all memory which will dos the server
|
||||
|
||||
##### Mitigation
|
||||
|
||||
HTML is now scraped via a Stream and canceled after a 15 second timeout to prevent arbitrary data from being loaded into the server.
|
||||
|
||||
#### v1.0.0beta-3 and Under - Recipe Assets: Remote Code Execution
|
||||
|
||||
!!! error "CWE-1336: Improper Neutralization of Special Elements Used in a Template Engine"
|
||||
As a low privileged user, Create a new recipe and click on the "+" to add a New Asset.
|
||||
Select a file, then proxy the request that will create the asset.
|
||||
|
||||
Since mealie/routes/recipe/recipe_crud_routes.py:306 is calling slugify on the name POST parameter, we use $ which slugify() will remove completely.
|
||||
|
||||
Since mealie/routes/recipe/recipe_crud_routes.py:306 is concatenating raw user input from the extension POST parameter into the variable file_name, which ultimately gets used when writing to disk, we can use a directory traversal attack in the extension (e.g. ./../../../tmp/pwn.txt) to write the file to arbitrary location on the server.
|
||||
|
||||
As an attacker, now that we have a strong attack primitive, we can start getting creative to get RCE. Since the files were being created by root, we could add an entry to /etc/passwd, create a crontab, etc. but since there was templating functionality in the application that peaked my interest. The PoC in the HTTP request above creates a Jinja2 template at /app/data/template/pwn.html. Since Jinja2 templates execute Python code when rendered, all we have to do now to get code execution is render the malicious template. This was easy enough.
|
||||
|
||||
##### Mitigation
|
||||
|
||||
We've added proper path sanitization to ensure that the user is not allowed to write to arbitrary locations on the server.
|
||||
|
||||
!!! warning "Breaking Change Incoming"
|
||||
As this has shown a significant area of exposure in the templates that Mealie was provided for exporting recipes, we'll be removing this feature in the next Beta release and will instead rely on the community to provide tooling around transforming recipes using templates. This will significantly limit the possible exposure of users injecting malicious templates into the application. The template functionality will be completely removed in the next beta release v1.0.0beta-5
|
||||
|
||||
#### All version Markdown Editor: Cross Site Scripting
|
||||
|
||||
!!! error "CWE-79: Cross-site Scripting (XSS) - Stored"
|
||||
A low privilege user can insert malicious JavaScript code into the Recipe Instructions which will execute in another person's browser that visits the recipe.
|
||||
|
||||
`<img src=x onerror=alert(document.domain)>`
|
||||
|
||||
##### Mitigation
|
||||
|
||||
This issues is present on all pages that allow markdown input. This error has been mitigated by wrapping the 3rd Party Markdown component and using the `domPurify` library to strip out the dangerous HTML.
|
||||
|
||||
#### v1.0.0beta-3 and Under - Image Scraper: Server-Side Request Forgery
|
||||
|
||||
!!! error "CWE-918: Server-Side Request Forgery (SSRF)"
|
||||
In the recipe edit page, is possible to upload an image directly or via an URL provided by the user. The function that handles the fetching and saving of the image via the URL doesn't have any URL verification, which allows to fetch internal services.
|
||||
|
||||
Furthermore, after the resource is fetch, there is no MIME type validation, which would ensure that the resource is indeed an image. After this, because there is no extension in the provided URL, the application will fallback to jpg, and original for the image name.
|
||||
|
||||
Then the result is saved to disk with the original.jpg name, that can be retrieved from the following URL: http://<domain>/api/media/recipes/<recipe-uid>/images/original.jpg. This file will contain the full response of the provided URL.
|
||||
|
||||
**Impact**
|
||||
|
||||
An attacker can get sensitive information of any internal-only services running. For example, if the application is hosted on Amazon Web Services (AWS) platform, its possible to fetch the AWS API endpoint, https://169.254.169.254, which returns API keys and other sensitive metadata.
|
||||
|
||||
##### Mitigation
|
||||
|
||||
Two actions were taken to reduce exposure to SSRF in this case.
|
||||
|
||||
1. The application will not prevent requests being made to local resources by checking for localhost or 127.0.0.1 domain names.
|
||||
2. The mime-type of the response is now checked prior to writing to disk.
|
||||
|
||||
If either of the above actions prevent the user from uploading images, the application will alert the user of what error occurred.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- For erroneously-translated datetime config ([#1362](https://github.com/hay-kot/mealie/issues/1362))
|
||||
- Fixed text color on RecipeCard in RecipePrintView and implemented ingredient sections ([#1351](https://github.com/hay-kot/mealie/issues/1351))
|
||||
- Ingredient sections lost after parsing ([#1368](https://github.com/hay-kot/mealie/issues/1368))
|
||||
- Increased float rounding precision for CRF parser ([#1369](https://github.com/hay-kot/mealie/issues/1369))
|
||||
- Infinite scroll bug on all recipes page ([#1393](https://github.com/hay-kot/mealie/issues/1393))
|
||||
- Fast fail of bulk importer ([#1394](https://github.com/hay-kot/mealie/issues/1394))
|
||||
- Bump @mdi/js from 5.9.55 to 6.7.96 in /frontend ([#1279](https://github.com/hay-kot/mealie/issues/1279))
|
||||
- Bump @nuxtjs/i18n from 7.0.3 to 7.2.2 in /frontend ([#1288](https://github.com/hay-kot/mealie/issues/1288))
|
||||
- Bump date-fns from 2.23.0 to 2.28.0 in /frontend ([#1293](https://github.com/hay-kot/mealie/issues/1293))
|
||||
- Bump fuse.js from 6.5.3 to 6.6.2 in /frontend ([#1325](https://github.com/hay-kot/mealie/issues/1325))
|
||||
- Bump core-js from 3.17.2 to 3.23.1 in /frontend ([#1383](https://github.com/hay-kot/mealie/issues/1383))
|
||||
- All-recipes page now sorts alphabetically ([#1405](https://github.com/hay-kot/mealie/issues/1405))
|
||||
- Sort recent recipes by created_at instead of date_added ([#1417](https://github.com/hay-kot/mealie/issues/1417))
|
||||
- Only show scaler when ingredients amounts enabled ([#1426](https://github.com/hay-kot/mealie/issues/1426))
|
||||
- Add missing types for API token deletion ([#1428](https://github.com/hay-kot/mealie/issues/1428))
|
||||
- Entry nutrition checker ([#1448](https://github.com/hay-kot/mealie/issues/1448))
|
||||
- Use == operator instead of is_ for sql queries ([#1453](https://github.com/hay-kot/mealie/issues/1453))
|
||||
- Use `mtime` instead of `ctime` for backup dates ([#1461](https://github.com/hay-kot/mealie/issues/1461))
|
||||
- Mealplan pagination ([#1464](https://github.com/hay-kot/mealie/issues/1464))
|
||||
- Properly use pagination for group event notifies ([#1512](https://github.com/hay-kot/mealie/pull/1512))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add go bulk import example ([#1388](https://github.com/hay-kot/mealie/issues/1388))
|
||||
- Fix old link
|
||||
- Pagination and filtering, and fixed a few broken links ([#1488](https://github.com/hay-kot/mealie/issues/1488))
|
||||
|
||||
### Features
|
||||
|
||||
- Toggle display of ingredient references in recipe instructions ([#1268](https://github.com/hay-kot/mealie/issues/1268))
|
||||
- Add custom scaling option ([#1345](https://github.com/hay-kot/mealie/issues/1345))
|
||||
- Implemented "order by" API parameters for recipe, food, and unit queries ([#1356](https://github.com/hay-kot/mealie/issues/1356))
|
||||
- Implement user favorites page ([#1376](https://github.com/hay-kot/mealie/issues/1376))
|
||||
- Extend Apprise JSON notification functionality with programmatic data ([#1355](https://github.com/hay-kot/mealie/issues/1355))
|
||||
- Mealplan-webhooks ([#1403](https://github.com/hay-kot/mealie/issues/1403))
|
||||
- Added "last-modified" header to supported record types ([#1379](https://github.com/hay-kot/mealie/issues/1379))
|
||||
- Re-write get all routes to use pagination ([#1424](https://github.com/hay-kot/mealie/issues/1424))
|
||||
- Advanced filtering API ([#1468](https://github.com/hay-kot/mealie/issues/1468))
|
||||
- Restore frontend sorting for all recipes ([#1497](https://github.com/hay-kot/mealie/issues/1497))
|
||||
- Implemented local storage for sorting and dynamic sort icons on the new recipe sort card ([1506](https://github.com/hay-kot/mealie/pull/1506))
|
||||
- create new foods and units from their Data Management pages ([#1511](https://github.com/hay-kot/mealie/pull/1511))
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Bump dev deps ([#1418](https://github.com/hay-kot/mealie/issues/1418))
|
||||
- Bump @vue/runtime-dom in /frontend ([#1423](https://github.com/hay-kot/mealie/issues/1423))
|
||||
- Backend page_all route cleanup ([#1483](https://github.com/hay-kot/mealie/issues/1483))
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove depreciated repo call ([#1370](https://github.com/hay-kot/mealie/issues/1370))
|
||||
|
||||
### Hotfix
|
||||
|
||||
- Tame typescript beast
|
||||
|
||||
### UI
|
||||
|
||||
- Improve parser ui text display ([#1437](https://github.com/hay-kot/mealie/issues/1437))
|
||||
|
||||
<!-- generated by git-cliff -->
|
||||
3
docs/docs/changelog/v1.0.0beta-5.md
Normal file
3
docs/docs/changelog/v1.0.0beta-5.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## NOTICE:
|
||||
|
||||
Release changelogs are now published on github releases. This file is kept for historical purposes.
|
||||
@@ -36,7 +36,7 @@ Checkout the makefile for all of the available commands.
|
||||
|
||||
Once the prerequisites are installed you can cd into the project base directory and run `make setup` to install the python and node dependencies.
|
||||
|
||||
=== "Linux / MacOs"
|
||||
=== "Linux / macOS"
|
||||
|
||||
```bash
|
||||
# Naviate To The Root Directory
|
||||
@@ -66,7 +66,7 @@ Before you start the server you MUST copy the `template.env` and `frontend/templ
|
||||
|
||||
Once that is complete you're ready to start the servers. You'll need two shells open, One for the server and one for the frontend.
|
||||
|
||||
=== "Linux / MacOs"
|
||||
=== "Linux / macOS"
|
||||
|
||||
```bash
|
||||
# Terminal #1
|
||||
|
||||
@@ -4,7 +4,7 @@ Mealie uses Conditional Random Fields (CRFs) for parsing and processing ingredie
|
||||
|
||||
## Improving The CRF Parser
|
||||
|
||||
To improve results with the model, you'll likely need to focus on improving the tokenization and parsing of the original string to aid the model in determine what the ingredient is. Datascience is not my forte, but I have done some tokenization to improve the model. You can find that code under `/mealie/services/parser_services/crfpp` along with some other utility functions to aid in the tokenization and processing of ingredient strings.
|
||||
To improve results with the model, you'll likely need to focus on improving the tokenization and parsing of the original string to aid the model in determine what the ingredient is. Data science is not my forte, but I have done some tokenization to improve the model. You can find that code under `/mealie/services/parser_services/crfpp` along with some other utility functions to aid in the tokenization and processing of ingredient strings.
|
||||
|
||||
The best way to test on improving the parser is to register additional test cases in `/mealie/tests/unit_tests/test_crfpp_parser.py` and run the test after making changes to the tokenizer. Note that the test cases DO NOT run in the CI environment, therefore you will need to have CRF++ installed on your machine. If you're using a Mac the easiest way to do this is through brew.
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ Recipes can be imported in bulk from a file containing a list of URLs. This can
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
function authentification () {
|
||||
function authentication () {
|
||||
auth=$(curl -X 'POST' \
|
||||
"$3/api/auth/token" \
|
||||
-H 'accept: application/json' \
|
||||
@@ -38,17 +38,20 @@ password="MyPassword"
|
||||
mealie_url=http://localhost:9000
|
||||
|
||||
|
||||
token=$(authentification $mail $password $mealie_url)
|
||||
token=$(authentication $mail $password $mealie_url)
|
||||
import_from_file $input $token $mealie_url
|
||||
|
||||
```
|
||||
|
||||
#### Go
|
||||
See <a href="https://github.com/Jleagle/mealie-importer" target="_blank">Jleagle/mealie-importer</a>.
|
||||
|
||||
#### Python
|
||||
```python
|
||||
import requests
|
||||
import re
|
||||
|
||||
def authentification(mail, password, mealie_url):
|
||||
def authentication(mail, password, mealie_url):
|
||||
headers = {
|
||||
'accept': 'application/json',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
@@ -87,7 +90,7 @@ password="MyPassword"
|
||||
mealie_url="http://localhost:9000"
|
||||
|
||||
|
||||
token = authentification(mail, password, mealie_url)
|
||||
token = authentication(mail, password, mealie_url)
|
||||
import_from_file(input_file, token, mealie_url)
|
||||
```
|
||||
|
||||
|
||||
@@ -8,10 +8,85 @@ Mealie supports long-live api tokens in the user frontend. See [user settings pa
|
||||
## Key Components
|
||||
|
||||
### Exploring Your Local API
|
||||
On your local installation you can access interactive API documentation that provides `curl` examples and expected results. This allows you to easily test and interact with your API to identify places to include your own functionality. You can visit the documentation at `http://mealie.yourdomain.com/docs` or see the example at the [Demo Site](https://mealie-demo.hay-kot.dev/docs)
|
||||
On your local installation you can access interactive API documentation that provides `curl` examples and expected results. This allows you to easily test and interact with your API to identify places to include your own functionality. You can visit the documentation at `http://mealie.yourdomain.com/docs` or see the example at the [Demo Site](https://demo.mealie.io/docs).
|
||||
|
||||
### Recipe Extras
|
||||
Recipes extras are a key feature of the Mealie API. They allow you to create custom json key/value pairs within a recipe to reference from 3rd part applications. You can use these keys to contain information to trigger automation or custom messages to relay to your desired device.
|
||||
### Extras
|
||||
#### Recipe Extras
|
||||
Recipes extras are a key feature of the Mealie API. They allow you to create custom json key/value pairs within a recipe to reference from 3rd part applications. You can use these keys to contain information to trigger automation or custom messages to relay to your desired device.
|
||||
|
||||
For example you could add `{"message": "Remember to thaw the chicken"}` to a recipe and use the webhooks built into mealie to send that message payload to a destination to be processed.
|
||||
|
||||
#### Shopping List and Food Extras
|
||||
Similarly to recipes, extras are supported on shopping lists, shopping list items, and foods. At this time they are only accessible through the API. Extras for these objects allow for rich integrations between the Mealie shopping list and your favorite list manager, such as Alexa, ToDoist, Trello, or any other list manager with an API.
|
||||
|
||||
To keep shopping lists in sync, for instance, you can store your Trello list id on your Mealie shopping list: <br />
|
||||
`{"trello_list_id": "5abbe4b7ddc1b351ef961414"}`
|
||||
|
||||
Now if an update is made to your shopping list, you know which Trello list also needs to be updated. Similarly, you can also keep track of individual cards on your Trello list by storing data on shopping list items: <br />
|
||||
`{"trello_card_id": "bab414bde415cd715efb9361"}`
|
||||
|
||||
Sometimes you may want to exclude certain foods from syncing to your external list, such as water, so you can add a custom property to your "water" food: <br />
|
||||
`{"trello_exclude_food": "true"}`
|
||||
|
||||
You can combine your custom data definitions with our Event Subscriptions API, enabling you to keep your external list up-to-date in real time.
|
||||
|
||||
### Pagination and Filtering
|
||||
Most document types share a uniform pagination and filtering API (e.g. `GET /api/recipes`). These allow you to filter by an arbitrary combination of criteria and return only a certain number of documents (i.e. a single "page" of documents).
|
||||
|
||||
#### Pagination
|
||||
The pagination API allows you to limit how many documents you return in each call. This is important when serving data to an application, as you don't want to wait for a huge payload every time you load a page. You may also not want to render all documents at once, opting to render only a few at a time.
|
||||
|
||||
The `perPage` parameter tells Mealie how many documents to return (this is similar to `LIMIT` in SQL). If you want to keep fetching more data in batches, first determine your batch size (in other words: how many documents you want per-page), then make additional calls by changing the `page` parameter. If your `perPage` size is 30, then page 1 will return the first 30 documents, page 2 will return the next 30 documents, etc.
|
||||
|
||||
Many applications will keep track of the query and adjust the page parameter appropriately, but some applications can't do this, or a particular implementation may make this difficult. The response includes pagination guides to help you find the next page and previous page. Here is a sample response:
|
||||
```json
|
||||
{
|
||||
"page": 2,
|
||||
"per_page": 5,
|
||||
"total": 23,
|
||||
"total_pages": 5,
|
||||
"data": [...],
|
||||
"next": "/recipes?page=3&per_page=5&order_by=name&order_direction=asc",
|
||||
"previous": "/recipes?page=1&per_page=5&order_by=name&order_direction=asc"
|
||||
}
|
||||
```
|
||||
Notice that the route does not contain the baseurl (e.g. `https://mymealieapplication.com/api`).
|
||||
|
||||
There are a few shorthands available to reduce the number of calls for certain common requests:
|
||||
- if you want to return _all_ results, effectively disabling pagination, set `perPage = -1` (and fetch the first page)
|
||||
- if you want to fetch the _last_ page, set `page = -1`
|
||||
|
||||
#### Filtering
|
||||
The `queryFilter` parameter enables fine-grained control over your query. You can filter by any combination of attributes connected by logical operators (`AND`, `OR`). You can also group attributes together using parenthesis. For string, date, or datetime literals, you should surround them in double quotes (e.g. `"Pasta Fagioli"`). If there are no spaces in your literal (such as dates) the API will probably parse it correctly, but it's recommended that you use quotes anyway.
|
||||
|
||||
Here are several examples of filters. These filter strings are not surrounded in quotes for ease of reading, but they are _strings_, so they will probably be in quotes in your language.
|
||||
|
||||
##### Simple Filters
|
||||
Here is an example of a filter to find a recipe with the name "Pasta Fagioli": <br>
|
||||
`name = "Pasta Fagioli"`
|
||||
|
||||
This filter will find all recipes created on or after a particular date: <br>
|
||||
`createdAt >= "2021-02-22"`
|
||||
|
||||
> **_NOTE:_** The API uses Python's [dateutil parser](https://dateutil.readthedocs.io/en/stable/parser.html), which parses many different date/datetime formats.
|
||||
|
||||
This filter will find all units that have `useAbbreviation` disabled: <br>
|
||||
`useAbbreviation = false`
|
||||
|
||||
##### Compound Filters
|
||||
You can combine multiple filter statements using logical operators (`AND`, `OR`).
|
||||
|
||||
This filter will only return recipes named "Pasta Fagioli" or "Grandma's Brisket": <br>
|
||||
`name = "Pasta Fagioli" OR name = "Grandma's Brisket"`
|
||||
|
||||
This filter will return all recipes created before a particular date, except for the one named "Ultimate Vegan Ramen Recipe With Miso Broth": <br>
|
||||
`createdAt < "January 2nd, 2014" AND name <> "Ultimate Vegan Ramen Recipe With Miso Broth"`
|
||||
|
||||
This filter will return three particular recipes: <br>
|
||||
`name = "Pasta Fagioli" OR name = "Grandma's Brisket" OR name = "Ultimate Vegan Ramen Recipe With Miso Broth"`
|
||||
|
||||
##### Advanced Filters
|
||||
You can have multiple filter groups combined by logical operators. You can define a filter group with parenthesis.
|
||||
|
||||
Here's a filter that will find all recipes updated between two particular times, but exclude the "Pasta Fagioli" recipe: <br>
|
||||
`(updatedAt > "2022-07-17T15:47:00Z" AND updatedAt < "2022-07-17T15:50:00Z") AND name <> "Pasta Fagioli"`
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
Mealie offers two main ways to create recipes. You can use the integrated recipe-scraper to create recipes from hundreds of websites, or you can create recipes manually using the recipe editor.
|
||||
|
||||
[Demo](https://beta.mealie.io/recipe/create/url){ .md-button .md-button--primary .align-right }
|
||||
[Demo](https://demo.mealie.io/recipe/create/url){ .md-button .md-button--primary .align-right }
|
||||
|
||||
### Importing Recipes
|
||||
|
||||
@@ -22,7 +22,7 @@ Mealie supports importing recipes from a few other sources besides websites. Cur
|
||||
You can access these options on your installation at the `/group/migrations` page on your installation. If you'd like to see another source added, feel free to request so on Github.
|
||||
|
||||
|
||||
[Demo](https://beta.mealie.io/group/data/foods){ .md-button .md-button--primary }
|
||||
[Demo](https://demo.mealie.io/group/data/foods){ .md-button .md-button--primary }
|
||||
|
||||
|
||||
### Organizing Recipes
|
||||
@@ -34,13 +34,13 @@ Mealie has a robust and flexible recipe organization system with a few different
|
||||
|
||||
Categories are the overarching organizer for recipes. You can assign as many categories as you'd like to a recipe, but we recommend that you try to limit the categories you assign to a recipe to one or two. This helps keep categories as focused as possible while still allowing you to find recipes that are related to each other. For example, you might assign a recipe to the category **Breakfast**, **Lunch**, **Dinner**, or **Side**.
|
||||
|
||||
[Demo](https://beta.mealie.io/recipes/categories){ .md-button .md-button--primary }
|
||||
[Demo](https://demo.mealie.io/recipes/categories){ .md-button .md-button--primary }
|
||||
|
||||
#### Tags
|
||||
|
||||
Tags, are nearly identical to categories in function but play a secondary role in some cases. As such, we recommend that you use tags freely to help you organize your recipes by more specific topics. For example, if a recipe can be frozen or is a great left-over meal, you could assign the tags **frozen** and **left-over** and easily filter for those at a later time.
|
||||
|
||||
[Demo](https://beta.mealie.io/recipes/tags){ .md-button .md-button--primary }
|
||||
[Demo](https://demo.mealie.io/recipes/tags){ .md-button .md-button--primary }
|
||||
|
||||
#### Tools
|
||||
|
||||
@@ -48,11 +48,11 @@ Tools, are another way that some users like to organize their recipes. If a reci
|
||||
|
||||
Each of the above organizers can be filtered in searches, and have their own pages where you can view all the recipes that are associated with those organizers.
|
||||
|
||||
[Demo](https://beta.mealie.io/recipes/tools){ .md-button .md-button--primary }
|
||||
[Demo](https://demo.mealie.io/recipes/tools){ .md-button .md-button--primary }
|
||||
|
||||
#### Cookbooks
|
||||
|
||||
Mealie also has the concept of cookbooks. These can be created inside of a group and can use a cross section of Categories, Tags, and Tools to filter recipes and view them in one specific page. Cookbooks are a great way to keep a supset of recipes easily accessible to you. You can think of them as a saved search results page. While most examples are simple, you can use as many organizers to filter a cookbook as you'd like.
|
||||
Mealie also has the concept of cookbooks. These can be created inside of a group and can use a cross section of Categories, Tags, and Tools to filter recipes and view them in one specific page. Cookbooks are a great way to keep a subset of recipes easily accessible to you. You can think of them as a saved search results page. While most examples are simple, you can use as many organizers to filter a cookbook as you'd like.
|
||||
|
||||
#### Examples:
|
||||
|
||||
@@ -60,7 +60,7 @@ Mealie also has the concept of cookbooks. These can be created inside of a group
|
||||
- Pasta Sides: Recipes that have both the **Side** category and the **Pasta** tag
|
||||
- Dessert Breads: Recipes that have both the **Bread** category and the **Dessert** tag
|
||||
|
||||
[Demo](https://beta.mealie.io/group/cookbooks){ .md-button .md-button--primary }
|
||||
[Demo](https://demo.mealie.io/group/cookbooks){ .md-button .md-button--primary }
|
||||
|
||||
## Meal Planning
|
||||
|
||||
@@ -69,13 +69,13 @@ Mealie uses a calendar like view to help you plan your meals. It shows you the p
|
||||
!!! tip
|
||||
You can also add a "Note" type entry to your meal-plan when you want to include something that might not have a specific recipes. This is great for leftovers, or for ordering out.
|
||||
|
||||
[Demo](https://beta.mealie.io/group/mealplan/planner){ .md-button .md-button--primary }
|
||||
[Demo](https://demo.mealie.io/group/mealplan/planner){ .md-button .md-button--primary }
|
||||
|
||||
### Planner Rules
|
||||
|
||||
The meal planner has the concept of plan rules. These offer a flexible way to use your organizers to customize how a random recipe is inserted into your meal plan. You can set rules to restrict the pool of recipes based on the Tags and/or Categories of a recipe. Additionally, since meal plans have a Breakfast, Lunch, Dinner, and Snack labels you can specifically set a rule to be active for a **specific meal type** or even a **specific day of the week.**
|
||||
|
||||
[Demo](https://beta.mealie.io/group/mealplan/settings){ .md-button .md-button--primary }
|
||||
[Demo](https://demo.mealie.io/group/mealplan/settings){ .md-button .md-button--primary }
|
||||
|
||||
## Shopping Lists
|
||||
|
||||
@@ -85,7 +85,7 @@ The shopping lists feature is a great way to keep track of what you need to buy
|
||||
At this time there isn't a tight integration between meal-plans and shopping lists, however it's something we have planned for the future.
|
||||
|
||||
|
||||
[Demo](https://beta.mealie.io/shopping-lists){ .md-button .md-button--primary }
|
||||
[Demo](https://demo.mealie.io/shopping-lists){ .md-button .md-button--primary }
|
||||
|
||||
|
||||
## Data Management
|
||||
@@ -107,31 +107,31 @@ Managing a robust collection of recipes inevitable requires a lot of data. Meali
|
||||
- Merge Units into a single unit entry
|
||||
- Export as JSON
|
||||
|
||||
[Demo](https://beta.mealie.io/group/data/foods){ .md-button .md-button--primary }
|
||||
[Demo](https://demo.mealie.io/group/data/foods){ .md-button .md-button--primary }
|
||||
|
||||
## Server Administration
|
||||
|
||||
### Site Settings
|
||||
|
||||
The site settings page contains general information about your installtion like the application version, some configuration details, and some utilities to help you confirm your installation is working as expected. For example, you can use the Email Configuration section to validate that your email credentials are setup correctly and that the email service is working as expected. Additionally, there is a docker-volume utility that will confirm your volumes are configured and shared correctly between the front and backend of the application.
|
||||
The site settings page contains general information about your installation like the application version, some configuration details, and some utilities to help you confirm your installation is working as expected. For example, you can use the Email Configuration section to validate that your email credentials are setup correctly and that the email service is working as expected. Additionally, there is a docker-volume utility that will confirm your volumes are configured and shared correctly between the front and backend of the application.
|
||||
|
||||
[Demo](https://beta.mealie.io/admin/site-settings){ .md-button .md-button--primary }
|
||||
[Demo](https://demo.mealie.io/admin/site-settings){ .md-button .md-button--primary }
|
||||
|
||||
### Users and Group
|
||||
|
||||
There is a small management area for users and groups that allows you to create, edit, and delete users and groups.
|
||||
|
||||
[Demo](https://beta.mealie.io/admin/manage/users){ .md-button .md-button--primary }
|
||||
[Demo](https://demo.mealie.io/admin/manage/users){ .md-button .md-button--primary }
|
||||
|
||||
### Backups
|
||||
|
||||
The backups page provides a full system backup of your installation including all assets and images related to recipes. These are archived into a zip file and stored on the server but can also be downloaded through the UI. Due to some issues in the past Mealie no longer performs automatic backups, **it is advised that during setup you also setup a backup strategy to ensure your data is not lost.**
|
||||
|
||||
|
||||
[Demo](https://beta.mealie.io/admin/backups){ .md-button .md-button--primary }
|
||||
[Demo](https://demo.mealie.io/admin/backups){ .md-button .md-button--primary }
|
||||
|
||||
|
||||
!!! note
|
||||
This is **NOT** the same as backups in v0.5.4. We've greatly simplified how backups are managed at the database level and we are now taking a full snapshot of the database and restoring it. If you're looking to export your recipes to move to an alternative service, this is likely not the way you'll want to export that data. You'll likely want to handle that through the group data exports page or through the API itself.
|
||||
|
||||
[Group Data Exports](https://beta.mealie.io/group/data/recipes){ .md-button .md-button--primary }
|
||||
[Group Data Exports](https://demo.mealie.io/group/data/recipes){ .md-button .md-button--primary }
|
||||
|
||||
@@ -18,7 +18,12 @@
|
||||
| ALLOW_SIGNUP | true | Allow user sign-up without token (should match frontend env) |
|
||||
|
||||
|
||||
### Security
|
||||
|
||||
| Variables | Default | Description |
|
||||
| --------------------------- | :-----: | ----------------------------------------------------------------------------------- |
|
||||
| SECURITY_MAX_LOGIN_ATTEMPTS | 5 | Maximum times a user can provide an invalid password before their account is locked |
|
||||
| SECURITY_USER_LOCKOUT_TIME | 24 | Time in hours for how long a users account is locked |
|
||||
|
||||
### Database
|
||||
|
||||
@@ -39,7 +44,7 @@
|
||||
| SMTP_HOST | None | Required For email |
|
||||
| SMTP_PORT | 587 | Required For email |
|
||||
| SMTP_FROM_NAME | Mealie | Required For email |
|
||||
| SMTP_AUTH_STRATEGY | TLS | Required For email, Options: 'TLS', 'SSL', 'NONE' |
|
||||
| SMTP_AUTH_STRATEGY | TLS | Required For email, Options: 'TLS', 'SSL', 'NONE' |
|
||||
| SMTP_FROM_EMAIL | None | Required For email |
|
||||
| SMTP_USER | None | Required if SMTP_AUTH_STRATEGY is 'TLS' or 'SSL' |
|
||||
| SMTP_PASSWORD | None | Required if SMTP_AUTH_STRATEGY is 'TLS' or 'SSL' |
|
||||
@@ -49,6 +54,7 @@ Changing the webworker settings may cause unforeseen memory leak issues with Mea
|
||||
|
||||
| Variables | Default | Description |
|
||||
| ---------------- | :-----: | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| WEB_GUNICORN | false | Enables Gunicorn to manage Uvicorn web for multiple works |
|
||||
| WORKERS_PER_CORE | 1 | Set the number of workers to the number of CPU cores multiplied by this value (Value \* CPUs). More info [here][workers_per_core] |
|
||||
| MAX_WORKERS | 1 | Set the maximum number of workers to use. Default is not set meaning unlimited. More info [here][max_workers] |
|
||||
| WEB_CONCURRENCY | 1 | Override the automatic definition of number of workers. More info [here][web_concurrency] |
|
||||
@@ -56,9 +62,12 @@ Changing the webworker settings may cause unforeseen memory leak issues with Mea
|
||||
|
||||
### LDAP
|
||||
|
||||
| Variables | Default | Description |
|
||||
| ------------------ | :-----: | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| LDAP_AUTH_ENABLED | False | Authenticate via an external LDAP server in addidion to built-in Mealie auth |
|
||||
| LDAP_SERVER_URL | None | LDAP server URL (e.g. ldap://ldap.example.com) |
|
||||
| LDAP_BIND_TEMPLATE | None | Templated DN for users, `{}` will be replaced with the username (e.g. `cn={},dc=example,dc=com`) |
|
||||
| LDAP_ADMIN_FILTER | None | Optional LDAP filter, which tells Mealie the LDAP user is an admin (e.g. `(memberOf=cn=admins,dc=example,dc=com)`) |
|
||||
| Variables | Default | Description |
|
||||
| ------------------- | :-----: | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| LDAP_AUTH_ENABLED | False | Authenticate via an external LDAP server in addidion to built-in Mealie auth |
|
||||
| LDAP_SERVER_URL | None | LDAP server URL (e.g. ldap://ldap.example.com) |
|
||||
| LDAP_TLS_INSECURE | False | Do not verify server certificate when using secure LDAP |
|
||||
| LDAP_TLS_CACERTFILE | None | File path to Certificate Authority used to verify server certificate (e.g. `/path/to/ca.crt`) |
|
||||
| LDAP_BIND_TEMPLATE | None | Templated DN for users, `{}` will be replaced with the username (e.g. `cn={},dc=example,dc=com`, `{}@example.com`) |
|
||||
| LDAP_BASE_DN | None | Starting point when searching for users authentication (e.g. `CN=Users,DC=xx,DC=yy,DC=de`) |
|
||||
| LDAP_ADMIN_FILTER | None | Optional LDAP filter, which tells Mealie the LDAP user is an admin (e.g. `(memberOf=cn=admins,dc=example,dc=com)`) |
|
||||
|
||||
@@ -20,10 +20,10 @@ Setting the following environmental variables will change the theme of the front
|
||||
| THEME_LIGHT_INFO | #1976D2 | Light Theme Config Variable |
|
||||
| THEME_LIGHT_WARNING | #FF6D00 | Light Theme Config Variable |
|
||||
| THEME_LIGHT_ERROR | #EF5350 | Light Theme Config Variable |
|
||||
| DARK_LIGHT_PRIMARY | #E58325 | Dark Theme Config Variable |
|
||||
| DARK_LIGHT_ACCENT | #007A99 | Dark Theme Config Variable |
|
||||
| DARK_LIGHT_SECONDARY | #973542 | Dark Theme Config Variable |
|
||||
| DARK_LIGHT_SUCCESS | #43A047 | Dark Theme Config Variable |
|
||||
| DARK_LIGHT_INFO | #1976D2 | Dark Theme Config Variable |
|
||||
| DARK_LIGHT_WARNING | #FF6D00 | Dark Theme Config Variable |
|
||||
| DARK_LIGHT_ERROR | #EF5350 | Dark Theme Config Variable |
|
||||
| THEME_DARK_PRIMARY | #E58325 | Dark Theme Config Variable |
|
||||
| THEME_DARK_ACCENT | #007A99 | Dark Theme Config Variable |
|
||||
| THEME_DARK_SECONDARY | #973542 | Dark Theme Config Variable |
|
||||
| THEME_DARK_SUCCESS | #43A047 | Dark Theme Config Variable |
|
||||
| THEME_DARK_INFO | #1976D2 | Dark Theme Config Variable |
|
||||
| THEME_DARK_WARNING | #FF6D00 | Dark Theme Config Variable |
|
||||
| THEME_DARK_ERROR | #EF5350 | Dark Theme Config Variable |
|
||||
|
||||
@@ -63,7 +63,7 @@ After you've configured your database, and updated the `docker-compose.yaml` fil
|
||||
$ docker-compose up -d
|
||||
```
|
||||
|
||||
You should see the containers start up without error. You should now be able to access the Mealie frontend at [http://localhost:9925](http://locahost:9925).
|
||||
You should see the containers start up without error. You should now be able to access the Mealie frontend at [http://localhost:9925](http://localhost:9925).
|
||||
|
||||
!!! tip "Default Credentials"
|
||||
|
||||
@@ -102,8 +102,8 @@ While the docker-compose file should work without modification, some users want
|
||||
|
||||

|
||||
|
||||
In the diagram above there's a few crutial things to note.
|
||||
In the diagram above there's a few crucial things to note.
|
||||
|
||||
1. Port 9925 is the host port, this can be anything you want. The important part is that it's mapped to the mealie-frontend container at port 3000.
|
||||
2. The mealie-frontend container communicated with the mealie-api container through the INTERNAL docker network. This requires that the two containers are on the same network and that the network supports name resolution (anything but the default bridge network). The resolution URL can be specified in the docker-compose as the `API_URL` environment variable.
|
||||
3. The mealie-data volume is mounted to BOTH the mealie-frontend and mealie-api containers. This is REQUIRED to ensure that images and assets are severed up correctly. While the default configuration is a docker-volume, that same can be accomplished by using a local directory mounted to the containers.
|
||||
3. The mealie-data volume is mounted to BOTH the mealie-frontend and mealie-api containers. This is REQUIRED to ensure that images and assets are served up correctly. While the default configuration is a docker-volume, that same can be accomplished by using a local directory mounted to the containers.
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
|
||||
**For Environmental Variable Configuration See:**
|
||||
|
||||
- [Frontend Configuration](/mealie/documentation/getting-started/installation/frontend-config/)
|
||||
- [Backend Configuration](/mealie/documentation/getting-started/installation/backend-config/)
|
||||
- [Frontend Configuration](./frontend-config.md)
|
||||
- [Backend Configuration](./backend-config.md)
|
||||
|
||||
```yaml
|
||||
---
|
||||
version: "3.7"
|
||||
services:
|
||||
mealie-frontend:
|
||||
image: hkotel/mealie:frontend-v1.0.0beta-3
|
||||
image: hkotel/mealie:frontend-v1.0.0beta-4
|
||||
container_name: mealie-frontend
|
||||
depends_on:
|
||||
- mealie-api
|
||||
@@ -23,8 +23,12 @@ services:
|
||||
volumes:
|
||||
- mealie-data:/app/data/ # (3)
|
||||
mealie-api:
|
||||
image: hkotel/mealie:api-v1.0.0beta-3
|
||||
image: hkotel/mealie:api-v1.0.0beta-4
|
||||
container_name: mealie-api
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 1000M # (4)
|
||||
depends_on:
|
||||
- postgres
|
||||
volumes:
|
||||
@@ -66,3 +70,4 @@ volumes:
|
||||
<br/> <br/> **Note** that both containers must be on the same docker-network for this to work.
|
||||
2. To access the mealie interface you only need to expose port 3000 on the mealie-frontend container. Here we expose port 9925 on the host, feel free to change this to any port you like.
|
||||
3. Mounting the data directory to the frontend is now required to access the images/assets directory. This can be mounted read-only. Internally the frontend containers runs a Caddy proxy server that serves the assets requested to reduce load on the backend API.
|
||||
4. Setting an explicit memory limit is recommended. Python can pre-allocate larger amounts of memory than is necessary if you have a machine with a lot of RAM. This can cause the container to idle at a high memory usage. Setting a memory limit will improve idle performance.
|
||||
|
||||
@@ -4,15 +4,15 @@ SQLite is a popular, open source, self-contained, zero-configuration database th
|
||||
|
||||
**For Environmental Variable Configuration See:**
|
||||
|
||||
- [Frontend Configuration](/mealie/documentation/getting-started/installation/frontend-config/)
|
||||
- [Backend Configuration](/mealie/documentation/getting-started/installation/backend-config/)
|
||||
- [Frontend Configuration](./frontend-config.md)
|
||||
- [Backend Configuration](./backend-config.md)
|
||||
|
||||
```yaml
|
||||
---
|
||||
version: "3.7"
|
||||
services:
|
||||
mealie-frontend:
|
||||
image: hkotel/mealie:frontend-v1.0.0beta-3
|
||||
image: hkotel/mealie:frontend-v1.0.0beta-4
|
||||
container_name: mealie-frontend
|
||||
environment:
|
||||
# Set Frontend ENV Variables Here
|
||||
@@ -23,8 +23,12 @@ services:
|
||||
volumes:
|
||||
- mealie-data:/app/data/ # (3)
|
||||
mealie-api:
|
||||
image: hkotel/mealie:api-v1.0.0beta-3
|
||||
image: hkotel/mealie:api-v1.0.0beta-4
|
||||
container_name: mealie-api
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 1000M # (4)
|
||||
volumes:
|
||||
- mealie-data:/app/data/
|
||||
environment:
|
||||
@@ -49,3 +53,4 @@ volumes:
|
||||
<br/> <br/> **Note** that both containers must be on the same docker-network for this to work.
|
||||
2. To access the mealie interface you only need to expose port 3000 on the mealie-frontend container. Here we expose port 9925 on the host, feel free to change this to any port you like.
|
||||
3. Mounting the data directory to the frontend is now required to access the images/assets directory. This can be mounted read-only. Internally the frontend containers runs a Caddy proxy server that serves the assets requested to reduce load on the backend API.
|
||||
4. Setting an explicit memory limit is recommended. Python can pre-allocate larger amounts of memory than is necessary if you have a machine with a lot of RAM. This can cause the container to idle at a high memory usage. Setting a memory limit will improve idle performance.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# About The Project
|
||||
|
||||
!!! warning "Mealie v1 Beta Release"
|
||||
This documentation if for the Mealie v1 Beta release and is not final. As such, it may contain incomplete or incorrect information. You should understand that installing Mealie v1 Beta is a work in progress and while we've committed to maintaining the database schema and provided migrations, we are still in the process of adding new features, and robust testing to ensure the application works as expected.
|
||||
This documentation is for the Mealie v1 Beta release and is not final. As such, it may contain incomplete or incorrect information. You should understand that installing Mealie v1 Beta is a work in progress and while we've committed to maintaining the database schema and provided migrations, we are still in the process of adding new features, and robust testing to ensure the application works as expected.
|
||||
|
||||
You should likely find bugs, errors, and unfinished pages within the application. To find the current status of the release you can checkout the [project on github](https://github.com/hay-kot/mealie/projects/7) or reach out on discord.
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ This can be a plus or a minus depending on your use case. If you relied on the o
|
||||
|
||||
## Step 1: Setting Up The New Application
|
||||
|
||||
Given the nature of the upgrade, it is highly recommended that you standup a new instance of mealie along side your current instance. This will allow you to migrate your data safely and quickly without any issues. Follow the instructions in the [Installation Checklist](../getting-started/installation/installation-checklist.md) to get started. Once that's complete and you can login, continue here with step 2.
|
||||
Given the nature of the upgrade, it is highly recommended that you stand up a new instance of mealie along side your current instance. This will allow you to migrate your data safely and quickly without any issues. Follow the instructions in the [Installation Checklist](../getting-started/installation/installation-checklist.md) to get started. Once that's complete and you can login, continue here with step 2.
|
||||
|
||||
## Step 2: Exporting Your Data from Pre-v1
|
||||
|
||||
@@ -42,7 +42,7 @@ In your instance of Mealie prior to v1, perform an export of your data in the Ad
|
||||
|
||||
## Step 3: Using the Migration Tool
|
||||
|
||||
In your new v1 instance, navigate to `/group/migrations` and select "Mealie" from the dropdown selector. Upload the exported data from your pre-v1 instance. Optionally, you can tag all the recipes you've imported with the `mealie_alpha` tag to help you identify them. Once the upload has finished, submit the form and your migration will begin. This may take some time, but when it's complete you'll be provided a new entry in hte "Previous Migrations" table below. Be sure to review the migration report to make sure everything was successful. There may be instances where some of the recipes were not imported, but the migration will still import all the successful recipes.
|
||||
In your new v1 instance, navigate to `/group/migrations` and select "Mealie" from the dropdown selector. Upload the exported data from your pre-v1 instance. Optionally, you can tag all the recipes you've imported with the `mealie_alpha` tag to help you identify them. Once the upload has finished, submit the form and your migration will begin. This may take some time, but when it's complete you'll be provided a new entry in the "Previous Migrations" table below. Be sure to review the migration report to make sure everything was successful. There may be instances where some of the recipes were not imported, but the migration will still import all the successful recipes.
|
||||
|
||||
In most cases, it's faster to manually migrate the recipes that didn't take instead of trying to identify why the recipes failed to import. If you're experiencing issues with the migration tool, please open an issue on GitHub.
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# Backups and Restoring
|
||||
|
||||
Mealie provides an integrated mechanics for doing full installation backups of the database. Navigate to `/admin/backups` to
|
||||
|
||||
- See a list of available backups
|
||||
- Perform a backups
|
||||
- Restore a backup
|
||||
|
||||
!!! tip
|
||||
If you're using Mealie with SQLite all your data is stored in the /app/data/ folder in the container. You can easily perform entire site backups by stopping the container, and backing up this folder with your chosen tool. This is the **best** way to backup your data.
|
||||
|
||||
## Restoring from a Backup
|
||||
|
||||
To restore from a backup it needs to be uploaded to your instance, this can be done through the web portal. On the lower left hand corner of the backups data table you'll see an upload button. Click this button and select the backup file you want to upload and it will be available to import shortly.
|
||||
|
||||
Before importing it's critical that you understand the following:
|
||||
|
||||
- This is a destructive action and will delete all data in the database
|
||||
- This action cannot be undone
|
||||
- If this action is successful you will be logged out and you will need to log back in to complete the restore
|
||||
|
||||
!!! warning
|
||||
Prior to beta-v5 using a mis-matched version of the database backup will result in an error that will prevent you from using the instance of Mealie requiring you to remove all data and reinstall. Post beta-v5 performing a mismatched restore will throw an error and alert the user of the issue.
|
||||
|
||||
### Postgres Note
|
||||
|
||||
Restoring the Database when using Postgres requires Mealie to be configured with a postgres **superuser** account. This is due to our usage of massive deleting of data in the database and temporarily setting roles to perform the restore. To perform a restoration on Postgres you will need to _temporarily_ set the Mealie user to a superuser account.
|
||||
|
||||
```sql
|
||||
ALTER USER mealie WITH SUPERUSER;
|
||||
|
||||
# Run restore from Mealie
|
||||
|
||||
ALTER USER mealie WITH NOSUPERUSER;
|
||||
```
|
||||
|
||||
For more information see [GitHub Issue #1500](https://github.com/hay-kot/mealie/issues/1500)
|
||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
||||
site_name: Mealie
|
||||
#demo_url: https://mealie-demo.hay-kot.dev/
|
||||
demo_url: https://demo.mealie.io
|
||||
site_url: https://hay-kot.github.io/mealie/
|
||||
use_directory_urls: true
|
||||
theme:
|
||||
@@ -69,6 +69,8 @@ nav:
|
||||
- PostgreSQL: "documentation/getting-started/installation/postgres.md"
|
||||
- Frontend Configuration: "documentation/getting-started/installation/frontend-config.md"
|
||||
- Backend Configuration: "documentation/getting-started/installation/backend-config.md"
|
||||
- Usage:
|
||||
- Backup and Restoring: "documentation/getting-started/usage/backups-and-restoring.md"
|
||||
|
||||
- Community Guides:
|
||||
- iOS Shortcuts: "documentation/community-guide/ios.md"
|
||||
@@ -88,6 +90,7 @@ nav:
|
||||
- Improving Ingredient Parser: "contributors/guides/ingredient-parser.md"
|
||||
|
||||
- Change Log:
|
||||
- v1.0.0beta-4: "changelog/v1.0.0beta-4.md"
|
||||
- v1.0.0beta-3: "changelog/v1.0.0beta-3.md"
|
||||
- v1.0.0beta-2: "changelog/v1.0.0beta-2.md"
|
||||
- v1.0.0 Beta: "changelog/v1.0.0.md"
|
||||
|
||||
@@ -52,7 +52,10 @@ module.exports = {
|
||||
"ts-ignore": "allow-with-description",
|
||||
},
|
||||
],
|
||||
"no-restricted-imports": ["error", { paths: ["@vue/reactivity", "@vue/runtime-dom", "@vue/composition-api"] }],
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{ paths: ["@vue/reactivity", "@vue/runtime-dom", "@vue/composition-api", "vue-demi"] },
|
||||
],
|
||||
|
||||
// TODO Gradually activate all rules
|
||||
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||
|
||||
1
frontend/.husky/.gitignore
vendored
1
frontend/.husky/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
_
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
handle @apidocs {
|
||||
uri strip_suffix /
|
||||
reverse_proxy http://mealie-api:9000
|
||||
reverse_proxy {$API_URL}
|
||||
}
|
||||
|
||||
handle {
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { BaseCRUDAPI } from "../_base";
|
||||
import { UserIn, UserOut } from "~/types/api-types/user";
|
||||
|
||||
const prefix = "/api";
|
||||
|
||||
const routes = {
|
||||
adminUsers: `${prefix}/admin/users`,
|
||||
adminUsersId: (tag: string) => `${prefix}/admin/users/${tag}`,
|
||||
};
|
||||
|
||||
export class AdminUsersApi extends BaseCRUDAPI<UserIn, UserOut, UserOut> {
|
||||
baseRoute: string = routes.adminUsers;
|
||||
itemRoute = routes.adminUsersId;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { ValidatorsApi } from "./public/validators";
|
||||
import { ApiRequestInstance } from "~/types/api";
|
||||
|
||||
export class PublicApi {
|
||||
public validators: ValidatorsApi;
|
||||
|
||||
constructor(requests: ApiRequestInstance) {
|
||||
this.validators = new ValidatorsApi(requests);
|
||||
|
||||
Object.freeze(this);
|
||||
}
|
||||
}
|
||||
378
frontend/assets/css/fonts.css
Normal file
378
frontend/assets/css/fonts.css
Normal file
@@ -0,0 +1,378 @@
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-100-cyrillic-ext1.woff2') format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-100-cyrillic2.woff2') format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-100-greek-ext3.woff2') format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-100-greek4.woff2') format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-100-vietnamese5.woff2') format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-100-latin-ext6.woff2') format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-100-latin7.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-300-cyrillic-ext8.woff2') format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-300-cyrillic9.woff2') format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-300-greek-ext10.woff2') format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-300-greek11.woff2') format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-300-vietnamese12.woff2') format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-300-latin-ext13.woff2') format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-300-latin14.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-400-cyrillic-ext15.woff2') format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-400-cyrillic16.woff2') format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-400-greek-ext17.woff2') format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-400-greek18.woff2') format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-400-vietnamese19.woff2') format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-400-latin-ext20.woff2') format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-400-latin21.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-500-cyrillic-ext22.woff2') format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-500-cyrillic23.woff2') format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-500-greek-ext24.woff2') format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-500-greek25.woff2') format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-500-vietnamese26.woff2') format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-500-latin-ext27.woff2') format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-500-latin28.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-700-cyrillic-ext29.woff2') format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-700-cyrillic30.woff2') format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-700-greek-ext31.woff2') format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-700-greek32.woff2') format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-700-vietnamese33.woff2') format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-700-latin-ext34.woff2') format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-700-latin35.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-900-cyrillic-ext36.woff2') format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-900-cyrillic37.woff2') format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-900-greek-ext38.woff2') format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-900-greek39.woff2') format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-900-vietnamese40.woff2') format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-900-latin-ext41.woff2') format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-900-latin42.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
BIN
frontend/assets/fonts/Roboto-100-cyrillic-ext1.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-100-cyrillic-ext1.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-100-cyrillic2.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-100-cyrillic2.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-100-greek-ext3.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-100-greek-ext3.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-100-greek4.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-100-greek4.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-100-latin-ext6.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-100-latin-ext6.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-100-latin7.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-100-latin7.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-100-vietnamese5.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-100-vietnamese5.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-300-cyrillic-ext8.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-300-cyrillic-ext8.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-300-cyrillic9.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-300-cyrillic9.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-300-greek-ext10.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-300-greek-ext10.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-300-greek11.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-300-greek11.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-300-latin-ext13.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-300-latin-ext13.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-300-latin14.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-300-latin14.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-300-vietnamese12.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-300-vietnamese12.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-400-cyrillic-ext15.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-400-cyrillic-ext15.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-400-cyrillic16.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-400-cyrillic16.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-400-greek-ext17.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-400-greek-ext17.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-400-greek18.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-400-greek18.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-400-latin-ext20.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-400-latin-ext20.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-400-latin21.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-400-latin21.woff2
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user