Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d96c36333b | ||
|
|
b220cd6431 | ||
|
|
598b0f3707 | ||
|
|
c18b9d3184 | ||
|
|
e64d070603 | ||
|
|
d843370c07 | ||
|
|
269be953ce | ||
|
|
9f7d74aecf | ||
|
|
4a0a8e8a5e | ||
|
|
75895cab79 | ||
|
|
be0cdee8b7 | ||
|
|
6024c8bc05 | ||
|
|
b3241d3e8b | ||
|
|
01b9987812 | ||
|
|
4e613e15f0 | ||
|
|
5298bdc90f | ||
|
|
2a6bb7d444 | ||
|
|
21f1d46b6d | ||
|
|
df15a9e74e | ||
|
|
f53cae7c7b | ||
|
|
219138fce1 | ||
|
|
4634ad5666 | ||
|
|
eab7c0d9e5 | ||
|
|
4afb767375 | ||
|
|
a49c32e663 | ||
|
|
5d55e4b4ff | ||
|
|
98c5d142eb | ||
|
|
c0db6ff3d1 | ||
|
|
b71310cdaf | ||
|
|
91fb750768 | ||
|
|
7f1139618d | ||
|
|
42b2bc7c15 |
13
README.md
@@ -1,10 +1,10 @@
|
||||
[![Latest Release][latest-release-shield]][latest-release-url]
|
||||
[![Contributors][contributors-shield]][contributors-url]
|
||||
[![Forks][forks-shield]][forks-url]
|
||||
[![Stargazers][stars-shield]][stars-url]
|
||||
[![Issues][issues-shield]][issues-url]
|
||||
[![AGPL License][license-shield]][license-url]
|
||||
[![Docker Pulls][docker-pull]][docker-pull]
|
||||
[![Docker Pulls][docker-pull]][docker-url]
|
||||
[![GHCR Pulls][ghcr-pulls]][ghcr-url]
|
||||
|
||||
<!-- PROJECT LOGO -->
|
||||
<br />
|
||||
@@ -88,16 +88,17 @@ Thanks to Depot for providing build instances for our Docker image builds.
|
||||
<!-- MARKDOWN LINKS & IMAGES -->
|
||||
<!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
|
||||
[contributors-shield]: https://img.shields.io/github/contributors/mealie-recipes/mealie.svg?style=flat-square
|
||||
[docker-pull]: https://img.shields.io/docker/pulls/hkotel/mealie
|
||||
[docker-pull]: https://img.shields.io/docker/pulls/hkotel/mealie?style=flat-square
|
||||
[docker-url]: https://hub.docker.com/r/hkotel/mealie
|
||||
[ghcr-pulls]: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fipitio%2Fghcr-pulls%2Fmaster%2Findex.json&query=%24%5B%3F(%40.owner%3D%3D%22mealie-recipes%22%20%26%26%20%40.repo%3D%3D%22mealie%22%20%26%26%20%40.image%3D%3D%22mealie%22)%5D.pulls&style=flat-square&label=ghcr%20pulls
|
||||
[ghcr-url]: https://github.com/mealie-recipes/mealie/pkgs/container/mealie
|
||||
[contributors-url]: https://github.com/mealie-recipes/mealie/graphs/contributors
|
||||
[forks-shield]: https://img.shields.io/github/forks/mealie-recipes/mealie.svg?style=flat-square
|
||||
[forks-url]: https://github.com/mealie-recipes/mealie/network/members
|
||||
[stars-shield]: https://img.shields.io/github/stars/mealie-recipes/mealie.svg?style=flat-square
|
||||
[stars-url]: https://github.com/mealie-recipes/mealie/stargazers
|
||||
[issues-shield]: https://img.shields.io/github/issues/mealie-recipes/mealie.svg?style=flat-square
|
||||
[issues-url]: https://github.com/mealie-recipes/mealie/issues
|
||||
[latest-release-shield]: https://img.shields.io/github/v/release/mealie-recipes/mealie?style=flat-square&label=latest%20release
|
||||
[latest-release-url]: https://img.shields.io/github/v/release/mealie-recipes/mealie
|
||||
[latest-release-url]: https://github.com/mealie-recipes/mealie/releases
|
||||
[license-shield]: https://img.shields.io/github/license/mealie-recipes/mealie.svg?style=flat-square
|
||||
[license-url]: https://github.com/mealie-recipes/mealie/blob/mealie-next/LICENSE
|
||||
[linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=flat-square&logo=linkedin&colorB=555
|
||||
|
||||
@@ -148,7 +148,7 @@ tasks:
|
||||
- poetry run python mealie/app.py
|
||||
|
||||
py:migrate:
|
||||
desc: generates a new migration file e.g. task py:migrate:generate "add new column"
|
||||
desc: generates a new database migration file e.g. task py:migrate "add new column"
|
||||
cmds:
|
||||
- poetry run alembic revision --autogenerate -m "{{ .CLI_ARGS }}"
|
||||
- task: py:format
|
||||
|
||||
BIN
docs/docs/assets/img/n8n/n8n-cred-app.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
docs/docs/assets/img/n8n/n8n-cred-connection.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
docs/docs/assets/img/n8n/n8n-mealie-backup.png
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
docs/docs/assets/img/n8n/n8n-workflow-auth.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
docs/docs/assets/img/n8n/n8n-workflow-import.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
386
docs/docs/assets/other/n8n/n8n-mealie-backup.json
Normal file
@@ -0,0 +1,386 @@
|
||||
{
|
||||
"name": "Mealie Backup",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"rule": {
|
||||
"interval": [
|
||||
{}
|
||||
]
|
||||
}
|
||||
},
|
||||
"id": "2ec440b4-0668-4bc0-aa66-4023d6379f28",
|
||||
"name": "Schedule Trigger",
|
||||
"type": "n8n-nodes-base.scheduleTrigger",
|
||||
"typeVersion": 1.1,
|
||||
"position": [
|
||||
240,
|
||||
660
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "https://mealie.example/api/admin/backups",
|
||||
"authentication": "genericCredentialType",
|
||||
"genericAuthType": "httpHeaderAuth",
|
||||
"options": {}
|
||||
},
|
||||
"id": "235f26f7-0f45-479e-a7e3-bf8cda7c8426",
|
||||
"name": "Run Backup ",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.1,
|
||||
"position": [
|
||||
520,
|
||||
520
|
||||
],
|
||||
"notesInFlow": false,
|
||||
"credentials": {
|
||||
"httpHeaderAuth": {
|
||||
"id": "GSL12tNi3MPvTZux",
|
||||
"name": "Mealie API"
|
||||
}
|
||||
},
|
||||
"notes": "Send an API call to run the backup"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "https://ntfy.example/backups",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Title",
|
||||
"value": "Meale Backup Failure"
|
||||
},
|
||||
{
|
||||
"name": "Priority",
|
||||
"value": "urgent"
|
||||
},
|
||||
{
|
||||
"name": "Tags",
|
||||
"value": "warning"
|
||||
},
|
||||
{
|
||||
"name": "Actions",
|
||||
"value": "view, Open Mealie, https://mealie.example/admin/backups; view, Open n8n, https://n8n.example"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"contentType": "raw",
|
||||
"body": "\"Full Panic!\"",
|
||||
"options": {}
|
||||
},
|
||||
"id": "40ba81a5-5741-4b15-98af-1a9e6b34f997",
|
||||
"name": "Ntfy Warning",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.1,
|
||||
"position": [
|
||||
1000,
|
||||
520
|
||||
],
|
||||
"onError": "continueRegularOutput"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"url": "https://mealie.example/api/admin/backups",
|
||||
"authentication": "genericCredentialType",
|
||||
"genericAuthType": "httpHeaderAuth",
|
||||
"options": {}
|
||||
},
|
||||
"id": "b75571d0-d926-440c-897f-55b89c6a5080",
|
||||
"name": "Get all backups",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.1,
|
||||
"position": [
|
||||
520,
|
||||
820
|
||||
],
|
||||
"credentials": {
|
||||
"httpHeaderAuth": {
|
||||
"id": "GSL12tNi3MPvTZux",
|
||||
"name": "Mealie API"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fieldToSplitOut": "imports",
|
||||
"options": {}
|
||||
},
|
||||
"id": "943d0e83-682b-4500-9faf-53284cfb02c6",
|
||||
"name": "Split Out",
|
||||
"type": "n8n-nodes-base.splitOut",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
720,
|
||||
820
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Get input data\nconst inputData = items.map(item => item.json);\n\n// Sort the data based on the 'date' field in descending order\ninputData.sort((a, b) => new Date(b.date) - new Date(a.date));\n\n// Get all records except the latest 7\nconst allExceptLatest7 = inputData.slice(7);\n\n// Map the output data back to the required format\nreturn allExceptLatest7.map(record => ({ json: record }));\n"
|
||||
},
|
||||
"id": "64eae81d-fdb6-44f7-9a2d-eff8d1763281",
|
||||
"name": "Code",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
860,
|
||||
820
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "DELETE",
|
||||
"url": "=https://mealie.example/api/admin/backups/{{ $json.name }}",
|
||||
"authentication": "genericCredentialType",
|
||||
"genericAuthType": "httpHeaderAuth",
|
||||
"options": {}
|
||||
},
|
||||
"id": "1148eeb8-4860-46df-8f61-0e85ea1e0e89",
|
||||
"name": "Delete Oldies",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.1,
|
||||
"position": [
|
||||
1040,
|
||||
820
|
||||
],
|
||||
"credentials": {
|
||||
"httpHeaderAuth": {
|
||||
"id": "GSL12tNi3MPvTZux",
|
||||
"name": "Mealie API"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "Sends API Call to run backup",
|
||||
"height": 225,
|
||||
"width": 226,
|
||||
"color": 4
|
||||
},
|
||||
"id": "cd2cb5db-87c1-40d8-a746-e61ace231987",
|
||||
"name": "Sticky Note",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
460,
|
||||
460
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "Is there an error?",
|
||||
"height": 225,
|
||||
"width": 231,
|
||||
"color": 3
|
||||
},
|
||||
"id": "0bebecbe-903e-4a69-bb1a-35619e68b540",
|
||||
"name": "Sticky Note1",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
700,
|
||||
460
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "Send alert to NTFY",
|
||||
"height": 225,
|
||||
"width": 229
|
||||
},
|
||||
"id": "0b732adb-8a84-456d-b26d-5fc5ee5a4cae",
|
||||
"name": "Sticky Note2",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
940,
|
||||
460
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "Gets all backups in Mealie",
|
||||
"height": 225,
|
||||
"width": 226,
|
||||
"color": 4
|
||||
},
|
||||
"id": "99c6886b-6a07-4b51-b395-d4bbcbde7d18",
|
||||
"name": "Sticky Note3",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
460,
|
||||
760
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "Splits the data, and parses the output",
|
||||
"height": 225,
|
||||
"width": 281
|
||||
},
|
||||
"id": "549555f8-0aed-42c0-9693-9c0d93902796",
|
||||
"name": "Sticky Note4",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
700,
|
||||
760
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "Deletes all but the last 7 backups",
|
||||
"height": 225,
|
||||
"width": 229
|
||||
},
|
||||
"id": "bcc5f0ba-73e9-42d7-b01b-c32f9f69f2f7",
|
||||
"name": "Sticky Note5",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
1000,
|
||||
760
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "Run every day a 01:00",
|
||||
"height": 225,
|
||||
"width": 226,
|
||||
"color": 4
|
||||
},
|
||||
"id": "ce797062-d727-43e3-a27f-e29b13ad3c9a",
|
||||
"name": "Sticky Note6",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
180,
|
||||
600
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "strict"
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "8b00bb85-827f-4f2f-813e-db0d25e927d3",
|
||||
"leftValue": "={{ $json.error }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "fefd3e8b-9b71-490a-82e3-25e5468a4135",
|
||||
"name": "Error?",
|
||||
"type": "n8n-nodes-base.filter",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
760,
|
||||
520
|
||||
]
|
||||
}
|
||||
],
|
||||
"pinData": {},
|
||||
"connections": {
|
||||
"Schedule Trigger": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Run Backup ",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Get all backups",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Run Backup ": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Error?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Get all backups": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Split Out",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Split Out": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Code",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Code": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Delete Oldies",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Error?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Ntfy Warning",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"versionId": "68e3e469-3ddb-4838-b09d-3c69fdd851f5",
|
||||
"meta": {
|
||||
"templateCredsSetupCompleted": true,
|
||||
"instanceId": "630eefaa8c490b9c5221d83a182af6450c2c3efaf4b580b8ac348631abfe1aeb"
|
||||
},
|
||||
"id": "whloxeXkdBWWi2Uj",
|
||||
"tags": []
|
||||
}
|
||||
85
docs/docs/contributors/developers-guide/database-changes.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# Development: Database Changes
|
||||
|
||||
This document is open to improvement; please share any insights you have/develop.
|
||||
|
||||
## Overview
|
||||
|
||||
When modifying the database, you will most likely need to change the files under `/mealie/db/models/`.
|
||||
How exactly you need to modify it is of course highly contextual to the change you're making.
|
||||
|
||||
## Using Alembic to generate upgrade script
|
||||
|
||||
In your dev container you can run something like (change the message) `task py:migrate "Add creation tag to group preferences"` to have Alembic generate an upgrade script for you.
|
||||
|
||||
The script Alembic generates, will be limited! (Perhaps there's a way to resolve that? Haven't looked into it yet)
|
||||
For example, Alembic generated a script _similar_ to this (it has been modified already to have accurate foreign key names, for instance):
|
||||
|
||||
```Python
|
||||
"""Add creation tag to group preferences
|
||||
|
||||
Revision ID: 0ea6eb8eaa44
|
||||
Revises: ba1e4a6cfe99
|
||||
Create Date: 2024-01-04 12:40:03.062671
|
||||
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
|
||||
import mealie.db.migration_types
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "0ea6eb8eaa44"
|
||||
down_revision = "ba1e4a6cfe99"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column(
|
||||
"group_preferences", sa.Column("recipe_creation_tag", mealie.db.migration_types.GUID(), nullable=True)
|
||||
)
|
||||
op.create_foreign_key("fk_groupprefs_tags", "group_preferences", "tags", ["recipe_creation_tag"], ["id"])
|
||||
### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_constraint("fk_groupprefs_tags", "group_preferences", type_="foreignkey")
|
||||
op.drop_column("group_preferences", "recipe_creation_tag")
|
||||
### end Alembic commands ###
|
||||
```
|
||||
|
||||
But when trying to actually use that upgrade script, it becomes clear that our SQLite database doesn't like them. The minor modification needed looks like:
|
||||
|
||||
```Python
|
||||
"""Add creation tag to group preferences
|
||||
|
||||
Revision ID: 0ea6eb8eaa44
|
||||
Revises: ba1e4a6cfe99
|
||||
Create Date: 2024-01-04 12:40:03.062671
|
||||
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
|
||||
import mealie.db.migration_types
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "0ea6eb8eaa44"
|
||||
down_revision = "ba1e4a6cfe99"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
with op.batch_alter_table("group_preferences", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("recipe_creation_tag", mealie.db.migration_types.GUID(), nullable=True))
|
||||
batch_op.create_foreign_key("fk_groupprefs_tags", "tags", ["recipe_creation_tag"], ["id"])
|
||||
|
||||
|
||||
def downgrade():
|
||||
with op.batch_alter_table("group_preferences", schema=None) as batch_op:
|
||||
batch_op.drop_constraint("fk_groupprefs_tags", type_="foreignkey")
|
||||
batch_op.drop_column("recipe_creation_tag")
|
||||
```
|
||||
@@ -0,0 +1,80 @@
|
||||
# Automating Backups with n8n
|
||||
|
||||
!!! info
|
||||
This guide was submitted by a community member. Find something wrong? Submit a PR to get it fixed!
|
||||
|
||||
> [n8n](https://github.com/n8n-io/n8n) is a free and source-available fair-code licensed workflow automation tool. Alternative to Zapier or Make, allowing you to use a UI to create automated workflows.
|
||||
|
||||
This example workflow:
|
||||
|
||||
1. Backups Mealie every morning via an API call
|
||||
2. Deletes all but the last 7 backups
|
||||
|
||||
> [!CAUTION]
|
||||
> This only automates the backup function, this does not backup your data to anywhere except your local instance. Please make sure you are backing up your data to an external source.
|
||||
|
||||
---
|
||||
|
||||

|
||||
|
||||
# Setup
|
||||
|
||||
## Deploying n8n
|
||||
|
||||
Follow the relevant guide in the [n8n Documentation](https://docs.n8n.io/)
|
||||
|
||||
## Importing n8n workflow
|
||||
|
||||
1. In n8n, add a new workflow
|
||||
2. In the top right hit the 3 dot menu and select 'Import from URL...'
|
||||
|
||||

|
||||
|
||||
3. Paste `https://github.com/mealie-recipes/mealie/blob/mealie-next/docs/docs/assets/other/n8n/n8n-mealie-backup.json` and click Import
|
||||
4. Click through the nodes and update the URLs for your environment
|
||||
|
||||
## API Credentials
|
||||
|
||||
#### Generate Mealie API Token
|
||||
|
||||
1. Head to https://mealie.example.com/user/profile/api-tokens
|
||||
> If you dont see this screen make sure that "Show advanced features" is checked under https://mealie.example.com/user/profile/edit
|
||||
2. Under token name, enter the name of the token i.e. 'n8n' and hit Generate
|
||||
3. Copy and keep this API Token somewhere safe, this is like your password!
|
||||
|
||||
> You can use your normal user for this, but assuming you're an admin you could also choose to create a user named n8n and generate the API key against that user.
|
||||
|
||||
#### Setup Credentials in n8n
|
||||
|
||||
> [n8n Docs](https://docs.n8n.io/credentials/add-edit-credentials/)
|
||||
|
||||
1. Create a new "Header Auth" Credential
|
||||
|
||||

|
||||
|
||||
2. In the connection screen set - Name as `Authorization` - Value as `Bearer {INSERT MEALIE API KEY}`
|
||||
|
||||

|
||||
|
||||
3. In the workflow you created, for the "Run Backup", "Get All backups", and "Delete Oldies" nodes, update:
|
||||
- Authentication to `Generic Credential Type`
|
||||
- Generic Auth Type to `Header Auth`
|
||||
- Header Auth to `Mealie API` or whatever you named your credentials
|
||||
|
||||

|
||||
|
||||
## Notification Node
|
||||
|
||||
> Please use error notifications of some kind. It's very easy to set and forget an automation, then have the worst happen and lose data.
|
||||
|
||||
[ntfy](https://github.com/binwiederhier/ntfy) is a great open source, self-hostable tool for sending notifications.
|
||||
|
||||
If you want to use ntfy, you will need to install it on your environment, or sign up for their service, and configure it with the webhook URL.
|
||||
|
||||
If you want to use another notification service, you can create a new node in n8n that sends the notification using whatever method you like.
|
||||
|
||||
- For example, if you want to send a push notification via [Pushover](https:/pushover.net/) you could create a new node that uses the Pushover API and sends the notification.
|
||||
- You can use the [Send Email](https://docs.n8n.io/integrations/builtincore-nodes/n8n-nodes-base.sendemail/) node in n8n as an example of how to create your own custom node.
|
||||
- You can send it off to InfluxDB, Slack, Discord etc. Go nuts.
|
||||
|
||||
If you're using another method for backups we'd love to hear about it. Pop in [Discord](https://discord.gg/QuStdQGSGK) and say hi!
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
:octicons-tag-24: v1.5.0
|
||||
|
||||
## Highlighs
|
||||
## Highlights
|
||||
|
||||
- Logs are written to `/app/data/mealie.log` by default in the container.
|
||||
- Logs are also written to stdout and stderr.
|
||||
@@ -13,4 +13,4 @@
|
||||
Starting in v1.5.0 logging is now highly configurable. Using the `LOG_CONFIG_OVERRIDE` you can provide the application with a custom configuration to log however you'd like. This configuration file is based off the [Python Logging Config](https://docs.python.org/3/library/logging.config.html#logging.config.fileConfig). It can be difficult to understand the configuration at first, so here are some resources to help get started.
|
||||
|
||||
- This [YouTube Video](https://www.youtube.com/watch?v=9L77QExPmI0) for a great walkthrough on the logging file format.
|
||||
- Our [Logging Config](https://github.com/mealie-recipes/mealie/blob/mealie-next/mealie/core/logger/logconf.prod.json)
|
||||
- Our [Logging Config](https://github.com/mealie-recipes/mealie/blob/mealie-next/mealie/core/logger/logconf.prod.json).
|
||||
|
||||
@@ -7,7 +7,7 @@ PostgreSQL might be considered if you need to support many concurrent users. In
|
||||
```yaml
|
||||
services:
|
||||
mealie:
|
||||
image: ghcr.io/mealie-recipes/mealie:v1.7.0 # (3)
|
||||
image: ghcr.io/mealie-recipes/mealie:v1.8.0 # (3)
|
||||
container_name: mealie
|
||||
restart: always
|
||||
ports:
|
||||
|
||||
@@ -11,7 +11,7 @@ SQLite is a popular, open source, self-contained, zero-configuration database th
|
||||
```yaml
|
||||
services:
|
||||
mealie:
|
||||
image: ghcr.io/mealie-recipes/mealie:v1.7.0 # (3)
|
||||
image: ghcr.io/mealie-recipes/mealie:v1.8.0 # (3)
|
||||
container_name: mealie
|
||||
restart: always
|
||||
ports:
|
||||
|
||||
@@ -79,8 +79,8 @@ nav:
|
||||
- Permissions and Public Access: "documentation/getting-started/usage/permissions-and-public-access.md"
|
||||
|
||||
- Authentication:
|
||||
- LDAP: "documentation/getting-started/authentication/ldap.md"
|
||||
- OpenID Connect: "documentation/getting-started/authentication/oidc.md"
|
||||
- LDAP: "documentation/getting-started/authentication/ldap.md"
|
||||
- OpenID Connect: "documentation/getting-started/authentication/oidc.md"
|
||||
|
||||
- Community Guides:
|
||||
- iOS Shortcuts: "documentation/community-guide/ios.md"
|
||||
@@ -88,6 +88,7 @@ nav:
|
||||
- Home Assistant: "documentation/community-guide/home-assistant.md"
|
||||
- Bulk Url Import: "documentation/community-guide/bulk-url-import.md"
|
||||
- Import Bookmarklet: "documentation/community-guide/import-recipe-bookmarklet.md"
|
||||
- Automate Backups with n8n: "documentation/community-guide/n8n-backup-automation.md"
|
||||
|
||||
- API Reference: "api/redoc.md"
|
||||
|
||||
|
||||
@@ -31,6 +31,13 @@
|
||||
<v-switch v-model="preferences.showNotes" hide-details :label="$tc('recipe.notes')" />
|
||||
</v-row>
|
||||
</v-col>
|
||||
<v-col cols="auto" align-self="start">
|
||||
<v-row no-gutters>
|
||||
<v-switch v-model="preferences.showNutrition" hide-details :label="$tc('recipe.nutrition')" />
|
||||
</v-row>
|
||||
<v-row no-gutters>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
<v-card
|
||||
|
||||
@@ -45,6 +45,22 @@ export default defineComponent({
|
||||
.d-inline {
|
||||
& > p {
|
||||
display: inline;
|
||||
&:has(>sub)>sup {
|
||||
letter-spacing: -0.05rem;
|
||||
}
|
||||
}
|
||||
&:has(sub) {
|
||||
&:after {
|
||||
letter-spacing: -0.2rem;
|
||||
}
|
||||
}
|
||||
sup {
|
||||
&+span{
|
||||
letter-spacing: -0.05rem;
|
||||
}
|
||||
&:before {
|
||||
letter-spacing: 0rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,14 +8,8 @@
|
||||
<v-card-text v-if="edit">
|
||||
<div v-for="(item, key, index) in value" :key="index">
|
||||
<v-text-field
|
||||
dense
|
||||
:value="value[key]"
|
||||
:label="labels[key].label"
|
||||
:suffix="labels[key].suffix"
|
||||
type="number"
|
||||
autocomplete="off"
|
||||
@input="updateValue(key, $event)"
|
||||
></v-text-field>
|
||||
dense :value="value[key]" :label="labels[key].label" :suffix="labels[key].suffix" type="number"
|
||||
autocomplete="off" @input="updateValue(key, $event)"></v-text-field>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-list v-if="showViewer" dense class="mt-0 pt-0">
|
||||
@@ -34,17 +28,10 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
|
||||
import { computed, defineComponent } from "@nuxtjs/composition-api";
|
||||
import { useNutritionLabels } from "~/composables/recipes";
|
||||
import { Nutrition } from "~/lib/api/types/recipe";
|
||||
|
||||
type NutritionLabelType = {
|
||||
[key: string]: {
|
||||
label: string;
|
||||
suffix: string;
|
||||
value?: string;
|
||||
};
|
||||
};
|
||||
|
||||
import { NutritionLabelType } from "~/composables/recipes/use-recipe-nutrition";
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
@@ -57,37 +44,7 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
const { i18n } = useContext();
|
||||
const labels = <NutritionLabelType>{
|
||||
calories: {
|
||||
label: i18n.tc("recipe.calories"),
|
||||
suffix: i18n.tc("recipe.calories-suffix"),
|
||||
},
|
||||
fatContent: {
|
||||
label: i18n.tc("recipe.fat-content"),
|
||||
suffix: i18n.tc("recipe.grams"),
|
||||
},
|
||||
fiberContent: {
|
||||
label: i18n.tc("recipe.fiber-content"),
|
||||
suffix: i18n.tc("recipe.grams"),
|
||||
},
|
||||
proteinContent: {
|
||||
label: i18n.tc("recipe.protein-content"),
|
||||
suffix: i18n.tc("recipe.grams"),
|
||||
},
|
||||
sodiumContent: {
|
||||
label: i18n.tc("recipe.sodium-content"),
|
||||
suffix: i18n.tc("recipe.milligrams"),
|
||||
},
|
||||
sugarContent: {
|
||||
label: i18n.tc("recipe.sugar-content"),
|
||||
suffix: i18n.tc("recipe.grams"),
|
||||
},
|
||||
carbohydrateContent: {
|
||||
label: i18n.tc("recipe.carbohydrate-content"),
|
||||
suffix: i18n.tc("recipe.grams"),
|
||||
},
|
||||
};
|
||||
const { labels } = useNutritionLabels();
|
||||
const valueNotNull = computed(() => {
|
||||
let key: keyof Nutrition;
|
||||
for (key in props.value) {
|
||||
|
||||
@@ -83,19 +83,42 @@
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Nutrition -->
|
||||
<div v-if="preferences.showNutrition">
|
||||
<v-card-title class="headline pl-0"> {{ $t("recipe.nutrition") }} </v-card-title>
|
||||
|
||||
|
||||
<section>
|
||||
<div class="print-section">
|
||||
<table class="nutrition-table">
|
||||
<tbody>
|
||||
<tr v-for="(value, key) in recipe.nutrition" :key="key">
|
||||
<template v-if="value">
|
||||
<td>{{ labels[key].label }}</td>
|
||||
<td>{{ value || '-' }}</td>
|
||||
</template>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed } from "@nuxtjs/composition-api";
|
||||
import { computed, defineComponent } from "@nuxtjs/composition-api";
|
||||
import RecipeTimeCard from "~/components/Domain/Recipe/RecipeTimeCard.vue";
|
||||
import { useStaticRoutes } from "~/composables/api";
|
||||
import { Recipe, RecipeIngredient, RecipeStep } from "~/lib/api/types/recipe";
|
||||
import { Recipe, RecipeIngredient, RecipeStep} from "~/lib/api/types/recipe";
|
||||
import { NoUndefinedField } from "~/lib/api/types/non-generated";
|
||||
import { ImagePosition, useUserPrintPreferences } from "~/composables/use-users/preferences";
|
||||
import { parseIngredientText } from "~/composables/recipes";
|
||||
import { parseIngredientText, useNutritionLabels } from "~/composables/recipes";
|
||||
import { usePageState } from "~/composables/recipe-page/shared-state";
|
||||
|
||||
|
||||
type IngredientSection = {
|
||||
sectionName: string;
|
||||
ingredients: RecipeIngredient[];
|
||||
@@ -129,6 +152,10 @@ export default defineComponent({
|
||||
const preferences = useUserPrintPreferences();
|
||||
const { recipeImage } = useStaticRoutes();
|
||||
const { imageKey } = usePageState(props.recipe.slug);
|
||||
const {labels} = useNutritionLabels();
|
||||
|
||||
|
||||
|
||||
|
||||
const recipeImageUrl = computed(() => {
|
||||
return recipeImage(props.recipe.id, props.recipe.image, imageKey.value);
|
||||
@@ -221,6 +248,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
return {
|
||||
labels,
|
||||
hasNotes,
|
||||
imageKey,
|
||||
ImagePosition,
|
||||
@@ -290,4 +318,16 @@ li {
|
||||
list-style-type: none;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.nutrition-table {
|
||||
width: 25%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.nutrition-table td {
|
||||
padding: 2px;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -2,4 +2,5 @@ export { useFraction } from "./use-fraction";
|
||||
export { useRecipe } from "./use-recipe";
|
||||
export { useRecipes, recentRecipes, allRecipes, useLazyRecipes } from "./use-recipes";
|
||||
export { parseIngredientText, useParsedIngredientText } from "./use-recipe-ingredients";
|
||||
export { useNutritionLabels } from "./use-recipe-nutrition";
|
||||
export { useTools } from "./use-recipe-tools";
|
||||
|
||||
@@ -31,13 +31,13 @@ describe(parseIngredientText.name, () => {
|
||||
test("ingredient text with fraction", () => {
|
||||
const ingredient = createRecipeIngredient({ quantity: 1.5, unit: { fraction: true, id: "1", name: "cup" } });
|
||||
|
||||
expect(parseIngredientText(ingredient, false, 1, true)).contain("1 <sup>1</sup>").and.to.contain("<sub>2</sub>");
|
||||
expect(parseIngredientText(ingredient, false, 1, true)).contain("1<sup>1</sup>").and.to.contain("<sub>2</sub>");
|
||||
});
|
||||
|
||||
test("ingredient text with fraction when unit is null", () => {
|
||||
const ingredient = createRecipeIngredient({ quantity: 1.5, unit: undefined });
|
||||
|
||||
expect(parseIngredientText(ingredient, false, 1, true)).contain("1 <sup>1</sup>").and.to.contain("<sub>2</sub>");
|
||||
expect(parseIngredientText(ingredient, false, 1, true)).contain("1<sup>1</sup>").and.to.contain("<sub>2</sub>");
|
||||
});
|
||||
|
||||
test("ingredient text with fraction no formatting", () => {
|
||||
|
||||
@@ -63,7 +63,7 @@ export function useParsedIngredientText(ingredient: RecipeIngredient, disableAmo
|
||||
|
||||
if (fraction[1] > 0) {
|
||||
returnQty += includeFormating ?
|
||||
` <sup>${fraction[1]}</sup>⁄<sub>${fraction[2]}</sub>` :
|
||||
`<sup>${fraction[1]}</sup><span>⁄</span><sub>${fraction[2]}</sub>` :
|
||||
` ${fraction[1]}/${fraction[2]}`;
|
||||
}
|
||||
}
|
||||
|
||||
47
frontend/composables/recipes/use-recipe-nutrition.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { useContext } from "@nuxtjs/composition-api";
|
||||
|
||||
|
||||
export interface NutritionLabelType {
|
||||
[key: string]: {
|
||||
label: string;
|
||||
suffix: string;
|
||||
value?: string;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
export function useNutritionLabels() {
|
||||
const { i18n } = useContext();
|
||||
const labels = <NutritionLabelType>{
|
||||
calories: {
|
||||
label: i18n.tc("recipe.calories"),
|
||||
suffix: i18n.tc("recipe.calories-suffix"),
|
||||
},
|
||||
fatContent: {
|
||||
label: i18n.tc("recipe.fat-content"),
|
||||
suffix: i18n.tc("recipe.grams"),
|
||||
},
|
||||
fiberContent: {
|
||||
label: i18n.tc("recipe.fiber-content"),
|
||||
suffix: i18n.tc("recipe.grams"),
|
||||
},
|
||||
proteinContent: {
|
||||
label: i18n.tc("recipe.protein-content"),
|
||||
suffix: i18n.tc("recipe.grams"),
|
||||
},
|
||||
sodiumContent: {
|
||||
label: i18n.tc("recipe.sodium-content"),
|
||||
suffix: i18n.tc("recipe.milligrams"),
|
||||
},
|
||||
sugarContent: {
|
||||
label: i18n.tc("recipe.sugar-content"),
|
||||
suffix: i18n.tc("recipe.grams"),
|
||||
},
|
||||
carbohydrateContent: {
|
||||
label: i18n.tc("recipe.carbohydrate-content"),
|
||||
suffix: i18n.tc("recipe.grams"),
|
||||
},
|
||||
};
|
||||
|
||||
return { labels }
|
||||
}
|
||||
@@ -6,6 +6,7 @@ export interface UserPrintPreferences {
|
||||
imagePosition: string;
|
||||
showDescription: boolean;
|
||||
showNotes: boolean;
|
||||
showNutrition: boolean;
|
||||
}
|
||||
|
||||
export interface UserSearchQuery {
|
||||
|
||||
@@ -583,6 +583,8 @@
|
||||
"report-deletion-failed": "Kon nie verslag uitvee nie",
|
||||
"recipe-debugger": "Resep debugger",
|
||||
"recipe-debugger-description": "Gryp die URL van die resep wat jy wil debug en plak dit hier. Die URL sal deur die resepskraper geskraap word en die resultate sal vertoon word. As jy nie enige data terugstuur sien nie, word die webwerf wat jy probeer skraap nie deur Mealie of sy skraperbiblioteek ondersteun nie.",
|
||||
"use-openai": "Use OpenAI",
|
||||
"recipe-debugger-use-openai-description": "Use OpenAI to parse the results instead of relying on the scraper library. When creating a recipe via URL, this is done automatically if the scraper library fails, but you may test it manually here.",
|
||||
"debug": "Debug",
|
||||
"tree-view": "Boomstruktuur",
|
||||
"recipe-yield": "Resep opbrengs",
|
||||
|
||||
@@ -583,6 +583,8 @@
|
||||
"report-deletion-failed": "Report deletion failed",
|
||||
"recipe-debugger": "Recipe Debugger",
|
||||
"recipe-debugger-description": "Grab the URL of the recipe you want to debug and paste it here. The URL will be scraped by the recipe scraper and the results will be displayed. If you don't see any data returned, the site you are trying to scrape is not supported by Mealie or its scraper library.",
|
||||
"use-openai": "Use OpenAI",
|
||||
"recipe-debugger-use-openai-description": "Use OpenAI to parse the results instead of relying on the scraper library. When creating a recipe via URL, this is done automatically if the scraper library fails, but you may test it manually here.",
|
||||
"debug": "Debug",
|
||||
"tree-view": "Tree View",
|
||||
"recipe-yield": "Recipe Yield",
|
||||
|
||||
@@ -583,6 +583,8 @@
|
||||
"report-deletion-failed": "Неуспешно изтриване на доклад",
|
||||
"recipe-debugger": "Debugger на рецепти",
|
||||
"recipe-debugger-description": "Вземете URL на рецептата, която желаете да проверите за грешки и го поставете тук. URL ще бъде обходен и резултатите ще бъдат визуализирани. Ако не виждате върнати данни, сайтът който се опитвате да обходите не се поддържа от Mealie или библиотеката за обхождане.",
|
||||
"use-openai": "Use OpenAI",
|
||||
"recipe-debugger-use-openai-description": "Use OpenAI to parse the results instead of relying on the scraper library. When creating a recipe via URL, this is done automatically if the scraper library fails, but you may test it manually here.",
|
||||
"debug": "Отстраняване на грешки",
|
||||
"tree-view": "Дървовиден изглед",
|
||||
"recipe-yield": "Добиване от рецепта",
|
||||
|
||||
@@ -583,6 +583,8 @@
|
||||
"report-deletion-failed": "No s'ha pogut suprimir l'informe",
|
||||
"recipe-debugger": "Depuradora de receptes",
|
||||
"recipe-debugger-description": "Agafa l'URL de la recepta que vols depurar i enganxa-la aquí. L'URL serà reastrejada pel rastrejador de receptes i es mostraran els resultats. Si no veieu cap dada retornada, el lloc que esteu provant de rastrejar no és compatible amb Mealie ni la seva biblioteca de rastreig.",
|
||||
"use-openai": "Use OpenAI",
|
||||
"recipe-debugger-use-openai-description": "Use OpenAI to parse the results instead of relying on the scraper library. When creating a recipe via URL, this is done automatically if the scraper library fails, but you may test it manually here.",
|
||||
"debug": "Depuració",
|
||||
"tree-view": "Vista en arbre",
|
||||
"recipe-yield": "Rendiment de la recepta",
|
||||
|
||||
@@ -583,6 +583,8 @@
|
||||
"report-deletion-failed": "Report deletion failed",
|
||||
"recipe-debugger": "Recipe Debugger",
|
||||
"recipe-debugger-description": "Grab the URL of the recipe you want to debug and paste it here. The URL will be scraped by the recipe scraper and the results will be displayed. If you don't see any data returned, the site you are trying to scrape is not supported by Mealie or its scraper library.",
|
||||
"use-openai": "Use OpenAI",
|
||||
"recipe-debugger-use-openai-description": "Use OpenAI to parse the results instead of relying on the scraper library. When creating a recipe via URL, this is done automatically if the scraper library fails, but you may test it manually here.",
|
||||
"debug": "Debug",
|
||||
"tree-view": "Stromové zobrazení",
|
||||
"recipe-yield": "Recipe Yield",
|
||||
|
||||
@@ -583,6 +583,8 @@
|
||||
"report-deletion-failed": "Sletning af rapport mislykkedes",
|
||||
"recipe-debugger": "Fejlsøgning af opskrifter",
|
||||
"recipe-debugger-description": "Indsæt URL'en på hjemmesiden, der indeholder den opskrift, du vil fejlsøge. URL-adressen vil blive læst og resultaterne vil blive vist. Hvis ingen data bliver vist, er indhentning af opskrifter fra hjemmesiden endnu ikke understøttet af Mealie.",
|
||||
"use-openai": "Use OpenAI",
|
||||
"recipe-debugger-use-openai-description": "Use OpenAI to parse the results instead of relying on the scraper library. When creating a recipe via URL, this is done automatically if the scraper library fails, but you may test it manually here.",
|
||||
"debug": "Fejlsøgning",
|
||||
"tree-view": "Træ visning",
|
||||
"recipe-yield": "Udbytte af opskrift",
|
||||
|
||||
@@ -583,6 +583,8 @@
|
||||
"report-deletion-failed": "Bericht löschen fehlgeschlagen",
|
||||
"recipe-debugger": "Rezept Debugger",
|
||||
"recipe-debugger-description": "Füge die URL des Rezepts, das du debuggen möchtest, hier ein. Die URL wird vom Scraper eingelesen und die Ergebnisse werden angezeigt. Wenn du keine Ausgabedaten sehen solltest, wird das Einlesen dieser Webseite nicht von Mealie oder dessen Scraper-Bibliothek unterstützt.",
|
||||
"use-openai": "Verwende OpenAI",
|
||||
"recipe-debugger-use-openai-description": "Verwende OpenAI anstelle der Scraper-Bibliothek, um die Einträge zu parsen. Wenn du ein Rezept über dessen URL erstellst und der Versuch über die Scraper-Bibliothek fehlschlägt, passiert das automatisch. Aber du kannst es hier auch manuell testen.",
|
||||
"debug": "Debug",
|
||||
"tree-view": "Strukturierte Ansicht",
|
||||
"recipe-yield": "Portionsangabe",
|
||||
|
||||
@@ -583,6 +583,8 @@
|
||||
"report-deletion-failed": "Report deletion failed",
|
||||
"recipe-debugger": "Recipe Debugger",
|
||||
"recipe-debugger-description": "Grab the URL of the recipe you want to debug and paste it here. The URL will be scraped by the recipe scraper and the results will be displayed. If you don't see any data returned, the site you are trying to scrape is not supported by Mealie or its scraper library.",
|
||||
"use-openai": "Χρήση OpenAI",
|
||||
"recipe-debugger-use-openai-description": "Χρησιμοποιήστε το OpenAI για να αναλύσετε τα αποτελέσματα αντί να βασιστείτε στη βιβλιοθήκη του scraper. Κατά τη δημιουργία μιας συνταγής μέσω URL, αυτό γίνεται αυτόματα αν η βιβλιοθήκη του scraper αποτύχει, αλλά μπορείτε να την δοκιμάσετε χειροκίνητα εδώ.",
|
||||
"debug": "Debug",
|
||||
"tree-view": "Tree View",
|
||||
"recipe-yield": "Recipe Yield",
|
||||
|
||||
@@ -583,6 +583,8 @@
|
||||
"report-deletion-failed": "Report deletion failed",
|
||||
"recipe-debugger": "Recipe Debugger",
|
||||
"recipe-debugger-description": "Grab the URL of the recipe you want to debug and paste it here. The URL will be scraped by the recipe scraper and the results will be displayed. If you don't see any data returned, the site you are trying to scrape is not supported by Mealie or its scraper library.",
|
||||
"use-openai": "Use OpenAI",
|
||||
"recipe-debugger-use-openai-description": "Use OpenAI to parse the results instead of relying on the scraper library. When creating a recipe via URL, this is done automatically if the scraper library fails, but you may test it manually here.",
|
||||
"debug": "Debug",
|
||||
"tree-view": "Tree View",
|
||||
"recipe-yield": "Recipe Yield",
|
||||
|
||||
@@ -583,6 +583,8 @@
|
||||
"report-deletion-failed": "Report deletion failed",
|
||||
"recipe-debugger": "Recipe Debugger",
|
||||
"recipe-debugger-description": "Grab the URL of the recipe you want to debug and paste it here. The URL will be scraped by the recipe scraper and the results will be displayed. If you don't see any data returned, the site you are trying to scrape is not supported by Mealie or its scraper library.",
|
||||
"use-openai": "Use OpenAI",
|
||||
"recipe-debugger-use-openai-description": "Use OpenAI to parse the results instead of relying on the scraper library. When creating a recipe via URL, this is done automatically if the scraper library fails, but you may test it manually here.",
|
||||
"debug": "Debug",
|
||||
"tree-view": "Tree View",
|
||||
"recipe-yield": "Recipe Yield",
|
||||
|
||||
@@ -583,6 +583,8 @@
|
||||
"report-deletion-failed": "Error al eliminar el reporte",
|
||||
"recipe-debugger": "Depurador de recetas",
|
||||
"recipe-debugger-description": "Obtenga la URL de la receta que desea depurar y pegala aquí. La URL será analizadada por el extractor de recetas y los resultados se mostrarán. Si no ves ningún dato devuelta, el sitio que estás intentando rascar no está soportado por Mealie o su biblioteca de analizadores.",
|
||||
"use-openai": "Use OpenAI",
|
||||
"recipe-debugger-use-openai-description": "Use OpenAI to parse the results instead of relying on the scraper library. When creating a recipe via URL, this is done automatically if the scraper library fails, but you may test it manually here.",
|
||||
"debug": "Depuración",
|
||||
"tree-view": "Vista en árbol",
|
||||
"recipe-yield": "Porciones",
|
||||
@@ -600,7 +602,7 @@
|
||||
"select-parser": "Seleccionar Analizador",
|
||||
"natural-language-processor": "Procesador de Lenguaje Natural",
|
||||
"brute-parser": "Analizador Bruto",
|
||||
"openai-parser": "OpenAI Parser",
|
||||
"openai-parser": "Analizador OpenAI",
|
||||
"parse-all": "Analizar Todo",
|
||||
"no-unit": "Sin unidad",
|
||||
"missing-unit": "Crear unidad faltante: {unit}",
|
||||
@@ -773,9 +775,9 @@
|
||||
"oidc-ready": "OIDC Listo",
|
||||
"oidc-ready-error-text": "No todos los valores OIDC están configurados. Puedes ignorar esto si no estás usando autenticación OIDC.",
|
||||
"oidc-ready-success-text": "Todas las variables OIDC requeridas están configuradas.",
|
||||
"openai-ready": "OpenAI Ready",
|
||||
"openai-ready-error-text": "Not all OpenAI Values are configured. This can be ignored if you are not using OpenAI features.",
|
||||
"openai-ready-success-text": "Required OpenAI variables are all set."
|
||||
"openai-ready": "OpenAI Listo",
|
||||
"openai-ready-error-text": "No todos los valores OpenAI están configurados. Puedes ignorar esto si no estás usando funciones de OpenAI.",
|
||||
"openai-ready-success-text": "Las variables OpenAI requeridas están todas definidas."
|
||||
},
|
||||
"shopping-list": {
|
||||
"all-lists": "Todas las listas",
|
||||
@@ -789,7 +791,7 @@
|
||||
"food": "Alimentos",
|
||||
"note": "Nota",
|
||||
"label": "Etiqueta",
|
||||
"save-label": "Save Label",
|
||||
"save-label": "Guardar etiqueta",
|
||||
"linked-item-warning": "Este elemento está vinculado a una o más recetas. Ajustar las unidades o los alimentos producirá resultados inesperados al añadir o quitar la receta de esta lista.",
|
||||
"toggle-food": "Mostrar nombre del alimento",
|
||||
"manage-labels": "Administrar etiquetas",
|
||||
|
||||
@@ -583,6 +583,8 @@
|
||||
"report-deletion-failed": "Raportin poistaminen epäonnistui",
|
||||
"recipe-debugger": "Reseptin vianhaku",
|
||||
"recipe-debugger-description": "Tartu sen reseptin URL-osoitteeseen, jonka virheenkorjausta haluat tehdä, ja liitä se tähän. Reseptikaappain hakee URL-osoitteen ja tulokset näytetään. Jos et näe palautettua dataa, Mealie tai sen kappauskirjasto ei tue sivua, jota yrität kaapata.",
|
||||
"use-openai": "Use OpenAI",
|
||||
"recipe-debugger-use-openai-description": "Use OpenAI to parse the results instead of relying on the scraper library. When creating a recipe via URL, this is done automatically if the scraper library fails, but you may test it manually here.",
|
||||
"debug": "Vianhaku",
|
||||
"tree-view": "Puunäkymä",
|
||||
"recipe-yield": "Reseptin tekijä",
|
||||
|
||||
@@ -583,6 +583,8 @@
|
||||
"report-deletion-failed": "La suppression du rapport a échoué",
|
||||
"recipe-debugger": "Débogueur de recette",
|
||||
"recipe-debugger-description": "Récupérez l'URL de la recette que vous voulez déboguer et collez-la ici. La recette sera analysée et les résultats seront affichés. Si vous ne voyez aucune donnée retournée, le site que vous essayez de récupérer n'est pas pris en charge par Mealie.",
|
||||
"use-openai": "Utiliser OpenAI",
|
||||
"recipe-debugger-use-openai-description": "Utilisez OpenAI pour analyser les résultats au lieu de la bibliothèque d’extraction. Lors de la création d'une recette via une URL, cela se fait automatiquement si la bibliothèque d’extraction échoue, mais vous pouvez le tester manuellement ici.",
|
||||
"debug": "Déboguer",
|
||||
"tree-view": "Vue en arborescence",
|
||||
"recipe-yield": "Nombre de parts",
|
||||
|
||||
@@ -583,6 +583,8 @@
|
||||
"report-deletion-failed": "La suppression du rapport a échoué",
|
||||
"recipe-debugger": "Débogueur de recette",
|
||||
"recipe-debugger-description": "Récupérez l'URL de la recette que vous voulez déboguer et collez-la ici. La recette sera analysée et les résultats seront affichés. Si vous ne voyez aucune donnée retournée, le site que vous essayez de récupérer n'est pas pris en charge par Mealie.",
|
||||
"use-openai": "Utiliser OpenAI",
|
||||
"recipe-debugger-use-openai-description": "Utilisez OpenAI pour analyser les résultats au lieu de la bibliothèque d’extraction. Lors de la création d'une recette via une URL, cela se fait automatiquement si la bibliothèque d’extraction échoue, mais vous pouvez le tester manuellement ici.",
|
||||
"debug": "Déboguer",
|
||||
"tree-view": "Vue en arborescence",
|
||||
"recipe-yield": "Nombre de parts",
|
||||
|
||||
@@ -583,6 +583,8 @@
|
||||
"report-deletion-failed": "Report deletion failed",
|
||||
"recipe-debugger": "Recipe Debugger",
|
||||
"recipe-debugger-description": "Grab the URL of the recipe you want to debug and paste it here. The URL will be scraped by the recipe scraper and the results will be displayed. If you don't see any data returned, the site you are trying to scrape is not supported by Mealie or its scraper library.",
|
||||
"use-openai": "Use OpenAI",
|
||||
"recipe-debugger-use-openai-description": "Use OpenAI to parse the results instead of relying on the scraper library. When creating a recipe via URL, this is done automatically if the scraper library fails, but you may test it manually here.",
|
||||
"debug": "Debug",
|
||||
"tree-view": "Tree View",
|
||||
"recipe-yield": "Recipe Yield",
|
||||
|
||||
@@ -583,6 +583,8 @@
|
||||
"report-deletion-failed": "מחיקת דוח נכשלה",
|
||||
"recipe-debugger": "דיבאגר למתכון",
|
||||
"recipe-debugger-description": "הדבק פה את קישור המתקון שברצונך לבצע לו דיבוג. הקישור יפוענך ע\"י מפענך המתכונים והתוצאה תוצג. אם לא חוזרת תוצאה, האתר אותו אתה מנסה להוסיף אינו נתמך ע\"י מילי או ספריית הפיענוך.",
|
||||
"use-openai": "Use OpenAI",
|
||||
"recipe-debugger-use-openai-description": "Use OpenAI to parse the results instead of relying on the scraper library. When creating a recipe via URL, this is done automatically if the scraper library fails, but you may test it manually here.",
|
||||
"debug": "דיבאג",
|
||||
"tree-view": "תצוגת עץ",
|
||||
"recipe-yield": "תשואת מתכון",
|
||||
|
||||
@@ -583,6 +583,8 @@
|
||||
"report-deletion-failed": "Brisanje nije uspjelo",
|
||||
"recipe-debugger": "Ispravljač Pogrešaka Recepta",
|
||||
"recipe-debugger-description": "Preuzmite URL recepta koji želite ispraviti i zalijepite ga ovdje. URL će biti obrađen od strane scraper-a za recepte i rezultati će biti prikazani. Ako ne vidite nikakve povratne podatke, to znači da web stranica koju pokušavate obraditi nije podržana od strane Mealie-a ili njegove biblioteke za scraper-e.",
|
||||
"use-openai": "Use OpenAI",
|
||||
"recipe-debugger-use-openai-description": "Use OpenAI to parse the results instead of relying on the scraper library. When creating a recipe via URL, this is done automatically if the scraper library fails, but you may test it manually here.",
|
||||
"debug": "Ispravljanje grešaka",
|
||||
"tree-view": "Prikaz Stabla",
|
||||
"recipe-yield": "Konačna Količina Recepta",
|
||||
|
||||
@@ -583,6 +583,8 @@
|
||||
"report-deletion-failed": "Jelentés törlése sikertelen",
|
||||
"recipe-debugger": "Recept hibakereső",
|
||||
"recipe-debugger-description": "Fogja meg a hibakereséshez szükséges recept URL-címét, és illessze be ide. Az URL-t a receptkinyerő átemeli, és az eredmények megjelennek. Ha nem lát semmilyen visszaadott adatot, akkor a Mealie vagy a receptkinyerő nem támogatja az oldal formátumát.",
|
||||
"use-openai": "OpenAI használata",
|
||||
"recipe-debugger-use-openai-description": "Használja az OpenAI-t az eredmények elemzésére, ahelyett, hogy a scraper könyvtárra hagyatkozna. Ha URL-címen keresztül hoz létre receptet, ez automatikusan megtörténik, ha a scraper könyvtár nem működik, ám itt manuálisan is tesztelheti.",
|
||||
"debug": "Hibakeresés",
|
||||
"tree-view": "Fa nézet",
|
||||
"recipe-yield": "Adagonkénti információk",
|
||||
|
||||
@@ -583,6 +583,8 @@
|
||||
"report-deletion-failed": "Report deletion failed",
|
||||
"recipe-debugger": "Recipe Debugger",
|
||||
"recipe-debugger-description": "Grab the URL of the recipe you want to debug and paste it here. The URL will be scraped by the recipe scraper and the results will be displayed. If you don't see any data returned, the site you are trying to scrape is not supported by Mealie or its scraper library.",
|
||||
"use-openai": "Use OpenAI",
|
||||
"recipe-debugger-use-openai-description": "Use OpenAI to parse the results instead of relying on the scraper library. When creating a recipe via URL, this is done automatically if the scraper library fails, but you may test it manually here.",
|
||||
"debug": "Debug",
|
||||
"tree-view": "Tree View",
|
||||
"recipe-yield": "Recipe Yield",
|
||||
|
||||
@@ -583,6 +583,8 @@
|
||||
"report-deletion-failed": "Eliminazione report fallita",
|
||||
"recipe-debugger": "Debugger Ricetta",
|
||||
"recipe-debugger-description": "Prendi l'URL della ricetta che vuoi fare il debug e incollalo qui. L'URL verrà recuperato dallo scraper di ricette e i risultati verranno visualizzati. Se non si vede alcun dato restituito, il sito che si sta cercando di analizzare non è supportato da Mealie o la sua libreria di scraping.",
|
||||
"use-openai": "Use OpenAI",
|
||||
"recipe-debugger-use-openai-description": "Use OpenAI to parse the results instead of relying on the scraper library. When creating a recipe via URL, this is done automatically if the scraper library fails, but you may test it manually here.",
|
||||
"debug": "Debug",
|
||||
"tree-view": "Visualizzazione ad Albero",
|
||||
"recipe-yield": "Resa Ricetta",
|
||||
|
||||
@@ -583,6 +583,8 @@
|
||||
"report-deletion-failed": "レポートの削除に失敗しました",
|
||||
"recipe-debugger": "レシピのデバッガー",
|
||||
"recipe-debugger-description": "デバッグしたいレシピのURLを取得し、ここに貼り付けます。 URLはレシピスクレーパーによって削除され、結果が表示されます。 データが返されていない場合、スクレイプしようとしているサイトはMealieまたはそのスクレイパーライブラリではサポートされていません。",
|
||||
"use-openai": "Use OpenAI",
|
||||
"recipe-debugger-use-openai-description": "Use OpenAI to parse the results instead of relying on the scraper library. When creating a recipe via URL, this is done automatically if the scraper library fails, but you may test it manually here.",
|
||||
"debug": "デバッグ",
|
||||
"tree-view": "ツリービュー",
|
||||
"recipe-yield": "レシピ収率",
|
||||
@@ -1204,7 +1206,7 @@
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"welcome-user": "👋 Welcome, {0}!",
|
||||
"welcome-user": "👋 ようこそ, {0}!",
|
||||
"description": "プロフィール、レシピ、グループ設定を管理します。",
|
||||
"get-invite-link": "招待リンクを取得",
|
||||
"get-public-link": "公開リンクを取得",
|
||||
|
||||
@@ -583,6 +583,8 @@
|
||||
"report-deletion-failed": "Report deletion failed",
|
||||
"recipe-debugger": "Recipe Debugger",
|
||||
"recipe-debugger-description": "Grab the URL of the recipe you want to debug and paste it here. The URL will be scraped by the recipe scraper and the results will be displayed. If you don't see any data returned, the site you are trying to scrape is not supported by Mealie or its scraper library.",
|
||||
"use-openai": "Use OpenAI",
|
||||
"recipe-debugger-use-openai-description": "Use OpenAI to parse the results instead of relying on the scraper library. When creating a recipe via URL, this is done automatically if the scraper library fails, but you may test it manually here.",
|
||||
"debug": "Debug",
|
||||
"tree-view": "Tree View",
|
||||
"recipe-yield": "Recipe Yield",
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
"something-went-wrong": "Įvyko klaida!",
|
||||
"subscribed-events": "Prenumeruojami įvykiai",
|
||||
"test-message-sent": "Testinė žinutė išsiųsta",
|
||||
"message-sent": "Message Sent",
|
||||
"message-sent": "Žinutė išsiųsta",
|
||||
"new-notification": "Naujas pranešimas",
|
||||
"event-notifiers": "Įvykių pranešimai",
|
||||
"apprise-url-skipped-if-blank": "Apprise URL (praleidžiama, jei tuščia)",
|
||||
@@ -81,7 +81,7 @@
|
||||
"recipe-events": "Recipe Events"
|
||||
},
|
||||
"general": {
|
||||
"add": "Add",
|
||||
"add": "Pridėti",
|
||||
"cancel": "Atšaukti",
|
||||
"clear": "Išvalyti",
|
||||
"close": "Uždaryti",
|
||||
@@ -117,9 +117,9 @@
|
||||
"json": "JSON",
|
||||
"keyword": "Raktažodis",
|
||||
"link-copied": "Nuoroda nukopijuota",
|
||||
"loading": "Loading",
|
||||
"loading": "Kraunasi",
|
||||
"loading-events": "Užkrovimo įvykiai",
|
||||
"loading-recipe": "Loading recipe...",
|
||||
"loading-recipe": "Receptai kraunasi...",
|
||||
"loading-ocr-data": "Loading OCR data...",
|
||||
"loading-recipes": "Receptai kraunasi",
|
||||
"message": "Pranešimas",
|
||||
@@ -145,18 +145,18 @@
|
||||
"save": "Išsaugoti",
|
||||
"settings": "Nustatymai",
|
||||
"share": "Dalintis",
|
||||
"show-all": "Show All",
|
||||
"show-all": "Rodyti viską",
|
||||
"shuffle": "Maišyti",
|
||||
"sort": "Rikiavimas",
|
||||
"sort-ascending": "Sort Ascending",
|
||||
"sort-descending": "Sort Descending",
|
||||
"sort-ascending": "Rūšiuoti didėjimo tvarka",
|
||||
"sort-descending": "Rūšiuoti mažėjančia tvarka",
|
||||
"sort-alphabetically": "Pagal abėcėlę",
|
||||
"status": "Būsena",
|
||||
"subject": "Tema",
|
||||
"submit": "Pateikti",
|
||||
"success-count": "Sėkmingų: {count}",
|
||||
"sunday": "Sekmadienis",
|
||||
"system": "System",
|
||||
"system": "Sistema",
|
||||
"templates": "Ruošiniai:",
|
||||
"test": "Tikrinti",
|
||||
"themes": "Temos",
|
||||
@@ -176,7 +176,7 @@
|
||||
"units": "Vienetai",
|
||||
"back": "Atgal",
|
||||
"next": "Kitas",
|
||||
"start": "Start",
|
||||
"start": "Pradėti",
|
||||
"toggle-view": "Perjungti vaizdą",
|
||||
"date": "Data",
|
||||
"id": "Id",
|
||||
@@ -377,7 +377,7 @@
|
||||
"description-long": "Mealie can import recipies from Plan to Eat."
|
||||
},
|
||||
"myrecipebox": {
|
||||
"title": "My Recipe Box",
|
||||
"title": "Mano receptų dėžutė",
|
||||
"description-long": "Mealie can import recipes from My Recipe Box. Export your recipes in CSV format, then upload the .csv file below."
|
||||
},
|
||||
"recipekeeper": {
|
||||
@@ -583,6 +583,8 @@
|
||||
"report-deletion-failed": "Ataskaitų trynimas nepavyko",
|
||||
"recipe-debugger": "Recepto klaidų radimas",
|
||||
"recipe-debugger-description": "Įkelkite neveikiančio recepto URL nuorodą. Iš nuorodos bus nuskaitytas receptas ir parodyti nuskaitymo rezultatai. Jei nematote jokių duomenų - ši svetainė nėra suderinama su Mealie.",
|
||||
"use-openai": "Naudoti 'OpenAI'",
|
||||
"recipe-debugger-use-openai-description": "Naudokite 'OpenAI', kad išanalizuoti rezultatus, o ne pasikliauti tinklalapių duomenų rinkiklio įrankiu. Kuriant receptą naudojant URL, tai padaroma automatiškai, jei duomenų rinkiklio įrankiui nepavyksta apdoroti rezultatų, tačiau čia jį galite išbandyti rankiniu būdu.",
|
||||
"debug": "Šalinti klaidas",
|
||||
"tree-view": "Medžio struktūra",
|
||||
"recipe-yield": "Recepto išeiga",
|
||||
|
||||
@@ -583,6 +583,8 @@
|
||||
"report-deletion-failed": "Report deletion failed",
|
||||
"recipe-debugger": "Recipe Debugger",
|
||||
"recipe-debugger-description": "Grab the URL of the recipe you want to debug and paste it here. The URL will be scraped by the recipe scraper and the results will be displayed. If you don't see any data returned, the site you are trying to scrape is not supported by Mealie or its scraper library.",
|
||||
"use-openai": "Use OpenAI",
|
||||
"recipe-debugger-use-openai-description": "Use OpenAI to parse the results instead of relying on the scraper library. When creating a recipe via URL, this is done automatically if the scraper library fails, but you may test it manually here.",
|
||||
"debug": "Debug",
|
||||
"tree-view": "Tree View",
|
||||
"recipe-yield": "Recipe Yield",
|
||||
|
||||
@@ -564,7 +564,7 @@
|
||||
"create-a-recipe-by-providing-the-name-all-recipes-must-have-unique-names": "Maak een recept door het een naam te geven. Alle recepten moeten unieke namen hebben.",
|
||||
"new-recipe-names-must-be-unique": "Nieuwe receptnamen moeten uniek zijn",
|
||||
"scrape-recipe": "Scrape recept",
|
||||
"scrape-recipe-description": "Voeg een recept toe via een url. Geef de url op van de site welke je wil scannen voor een recept., en Mealie zal proberen het recept vanaf die plek te scannen en aan je collectie toe te voegen.",
|
||||
"scrape-recipe-description": "Voeg een recept toe via een URL. Geef de URL op van de site die je wil scannen voor een recept en Mealie zal proberen het recept vanaf die plek te scannen en aan je collectie toe te voegen.",
|
||||
"scrape-recipe-have-a-lot-of-recipes": "Heb je veel recepten die je in 1 keer wil importeren?",
|
||||
"scrape-recipe-suggest-bulk-importer": "Probeer de bulk importer uit",
|
||||
"import-original-keywords-as-tags": "Importeer oorspronkelijke trefwoorden als tags",
|
||||
@@ -583,6 +583,8 @@
|
||||
"report-deletion-failed": "Rapport verwijderen is mislukt",
|
||||
"recipe-debugger": "Recept debugger",
|
||||
"recipe-debugger-description": "Pak de URL van het recept dat u wilt debuggen en plak die hier. De URL zal door de receptenscraper worden gescrapt en de resultaten zullen worden weergegeven. Als u geen gegevens ziet, wordt de site die u probeert te scrapen niet ondersteund door Mealie of zijn scraperbibliotheek.",
|
||||
"use-openai": "Gebruik OpenAI",
|
||||
"recipe-debugger-use-openai-description": "Gebruik OpenAI om de resultaten te verwerken in plaats van te vertrouwen op de scraper-bibliotheek. Bij het maken van een recept via een URL wordt dit automatisch gedaan als de scraper-bibliotheek mislukt, maar u kunt het hier handmatig testen.",
|
||||
"debug": "Debug",
|
||||
"tree-view": "Boomstructuurweergave",
|
||||
"recipe-yield": "Recept Opbrengst",
|
||||
@@ -594,17 +596,17 @@
|
||||
"recipe-actions": "Acties met recepten ",
|
||||
"parser": {
|
||||
"experimental-alert-text": "Mealie gebruikt natuurlijke taalverwerking om te ontleden en maakt eenheden en levensmiddelen voor de ingrediënten van je recept. Deze functie is experimenteel en werkt misschien niet altijd zoals verwacht. Als u liever niet de bewerkte resultaten gebruikt, kunt u 'Annuleren' selecteren en de wijzigingen worden niet opgeslagen.",
|
||||
"ingredient-parser": "Ingrediënt Parser",
|
||||
"explanation": "To use the ingredient parser, click the 'Parse All' button to start the process. Once the processed ingredients are available, you can review the items and verify that they were parsed correctly. The model's confidence score is displayed on the right of the item title. This score is an average of all the individual scores and may not always be completely accurate.",
|
||||
"alerts-explainer": "Alerts will be displayed if a matching foods or unit is found but does not exists in the database.",
|
||||
"select-parser": "Selecteer parser",
|
||||
"natural-language-processor": "Natural Language Processor",
|
||||
"brute-parser": "Brute Parser",
|
||||
"openai-parser": "OpenAI parser",
|
||||
"parse-all": "Alles verwerken",
|
||||
"ingredient-parser": "Ingrediëntenontleder",
|
||||
"explanation": "Om de ingrediëntenontleder te gebruiken, klik op de knop 'Alles ontleden' om het proces te starten. Zodra de verwerkte ingrediënten beschikbaar zijn, kan je de items bekijken en controleren of ze correct verwerkt zijn. De vertrouwensscore van het model wordt weergegeven aan de rechterkant van het item titel. Deze score is een gemiddelde van alle afzonderlijke scores en is mogelijk niet altijd volledig.",
|
||||
"alerts-explainer": "Waarschuwingen zullen worden getoond als er een overeenkomend voedsel of eenheid is gevonden, maar niet bestaat in de database.",
|
||||
"select-parser": "Selecteer ontleder",
|
||||
"natural-language-processor": "Natuurlijke taalverwerker",
|
||||
"brute-parser": "Ruwe ontleder",
|
||||
"openai-parser": "OpenAI ontleder",
|
||||
"parse-all": "Alles ontleden",
|
||||
"no-unit": "Geen eenheid",
|
||||
"missing-unit": "Create missing unit: {unit}",
|
||||
"missing-food": "Create missing food: {food}",
|
||||
"missing-unit": "Maak ontbrekende eenheid aan: {unit}",
|
||||
"missing-food": "Ontbrekende voedsel maken: {food}",
|
||||
"no-food": "Geen voedsel"
|
||||
}
|
||||
},
|
||||
@@ -771,8 +773,8 @@
|
||||
"build": "Bouw",
|
||||
"recipe-scraper-version": "Versie van de receptenscraper",
|
||||
"oidc-ready": "OIDC klaar",
|
||||
"oidc-ready-error-text": "Not all OIDC Values are configured. This can be ignored if you are not using OIDC Authentication.",
|
||||
"oidc-ready-success-text": "Required OIDC variables are all set.",
|
||||
"oidc-ready-error-text": "Niet alle OIDC-waarden zijn geconfigureerd. Dit kan worden genegeerd als je geen OIDC-authenticatie gebruikt.",
|
||||
"oidc-ready-success-text": "Vereiste OIDC variabelen zijn allemaal ingesteld.",
|
||||
"openai-ready": "OpenAI staat klaar",
|
||||
"openai-ready-error-text": "Niet alle tekstvakken voor OpenAI zijn ingevuld. Als je geen OpenAI gebruikt kun je dit leeg laten.",
|
||||
"openai-ready-success-text": "Verplichte tekstvakken voor OpenAI zijn ingevuld."
|
||||
@@ -823,7 +825,7 @@
|
||||
"language": "Taal",
|
||||
"maintenance": "Onderhoud",
|
||||
"background-tasks": "Achtergrondtaken",
|
||||
"parser": "Parser",
|
||||
"parser": "Ontleder",
|
||||
"developer": "Ontwikkelaar",
|
||||
"cookbook": "Kookboek",
|
||||
"create-cookbook": "Maak een nieuw kookboek aan"
|
||||
|
||||
@@ -583,6 +583,8 @@
|
||||
"report-deletion-failed": "Sletting av rapport mislyktes",
|
||||
"recipe-debugger": "Oppskriftsfeilsøker",
|
||||
"recipe-debugger-description": "Hent nettadressen til oppskriften du vil feilsøke og lim den inn her. Nettsiden vil bli skrapt og resultatene vil bli vist. Hvis du ikke ser noen data returnert, er ikke nettstedet du prøver å skrape støttet av Mealie eller skraper-biblioteket.",
|
||||
"use-openai": "Use OpenAI",
|
||||
"recipe-debugger-use-openai-description": "Use OpenAI to parse the results instead of relying on the scraper library. When creating a recipe via URL, this is done automatically if the scraper library fails, but you may test it manually here.",
|
||||
"debug": "Feilsøk",
|
||||
"tree-view": "Trevisning",
|
||||
"recipe-yield": "Utbytte av oppskrift",
|
||||
|
||||
@@ -583,6 +583,8 @@
|
||||
"report-deletion-failed": "Nie udało się usunąć raportu",
|
||||
"recipe-debugger": "Debugger przepisów",
|
||||
"recipe-debugger-description": "Skopiuj link do przepisu, który chcesz debugować i wklej go tutaj. Strona zostanie obskrobana przez skrobarkę przepisów i jej wynik zostanie wyświetlony. Jeśli nic nie zostało zwrócone, strona, którą próbujesz obskrobać, nie jest wspierana przez Mealie i jej bibliotekę skrobania.",
|
||||
"use-openai": "Use OpenAI",
|
||||
"recipe-debugger-use-openai-description": "Use OpenAI to parse the results instead of relying on the scraper library. When creating a recipe via URL, this is done automatically if the scraper library fails, but you may test it manually here.",
|
||||
"debug": "Debuguj",
|
||||
"tree-view": "Widok drzewa",
|
||||
"recipe-yield": "Wydajność przepisu",
|
||||
|
||||
@@ -583,6 +583,8 @@
|
||||
"report-deletion-failed": "Relatório de exclusão falhou",
|
||||
"recipe-debugger": "Depurador de Receita",
|
||||
"recipe-debugger-description": "Pegue a URL da receita que deseja depurar e cole aqui. A URL será encontrada pelo scraper das receitas e os resultados serão exibidos. Se não ver nenhum dado retornado, o site que você está tentando criar um scrape não é suportado pelo Mealie ou pela sua biblioteca de scraper.",
|
||||
"use-openai": "Use OpenAI",
|
||||
"recipe-debugger-use-openai-description": "Use OpenAI to parse the results instead of relying on the scraper library. When creating a recipe via URL, this is done automatically if the scraper library fails, but you may test it manually here.",
|
||||
"debug": "Depurar",
|
||||
"tree-view": "Visualização em árvore",
|
||||
"recipe-yield": "Rendimento da Receita",
|
||||
|
||||
@@ -583,6 +583,8 @@
|
||||
"report-deletion-failed": "Erro ao eliminar relatório",
|
||||
"recipe-debugger": "Depurador de Receitas",
|
||||
"recipe-debugger-description": "Copie o URL da receita que quer depurar e cole-o aqui. O URL será lido pelo leitor de receitas e os resultados serão apresentados. Se nenhuma informação for devolvida, a página que está a tentar ler não é suportada pelo Mealie ou pela sua biblioteca de 'scrapping'.",
|
||||
"use-openai": "Utilizar OpenAI",
|
||||
"recipe-debugger-use-openai-description": "Utilize o OpenAI para analisar os resultados em vez de depender da biblioteca de scrapers. Ao criar uma receita através de um URL, isto é feito automaticamente se a biblioteca de scrapers falhar, mas pode testá-la manualmente aqui.",
|
||||
"debug": "Depurar",
|
||||
"tree-view": "Vista em árvore",
|
||||
"recipe-yield": "Rendimento da receita",
|
||||
|
||||
@@ -583,6 +583,8 @@
|
||||
"report-deletion-failed": "Ștergerea raportului a eșuat",
|
||||
"recipe-debugger": "Depanare rețetă",
|
||||
"recipe-debugger-description": "Copiază URL-ul rețetei pe care vrei să o depanezi și lipește-l aici. URL-ul va fi importat și rezultatele vor fi afișate ulterior. Dacă nu vedeți date returnate, site-ul pe care încerci să îl procesezi nu este suportat de Mealie sau biblioteca sa de import.",
|
||||
"use-openai": "Use OpenAI",
|
||||
"recipe-debugger-use-openai-description": "Use OpenAI to parse the results instead of relying on the scraper library. When creating a recipe via URL, this is done automatically if the scraper library fails, but you may test it manually here.",
|
||||
"debug": "Depanare",
|
||||
"tree-view": "Vizualizare Ierarhică",
|
||||
"recipe-yield": "Producere rețetă",
|
||||
|
||||
@@ -583,6 +583,8 @@
|
||||
"report-deletion-failed": "Не удалось удалить отчёт",
|
||||
"recipe-debugger": "Отладчик рецептов",
|
||||
"recipe-debugger-description": "Вставьте сюда URL рецепта, который вы хотите отладить. Рецепт по указанному URL-адресу будет отсканирован и результаты сканирования будут указаны ниже. Если вы не видите никаких возвращенных данных, то сайт, который вы пытаетесь обработать, не поддерживается Mealie или библиотекой сканера.",
|
||||
"use-openai": "Use OpenAI",
|
||||
"recipe-debugger-use-openai-description": "Use OpenAI to parse the results instead of relying on the scraper library. When creating a recipe via URL, this is done automatically if the scraper library fails, but you may test it manually here.",
|
||||
"debug": "Отладка",
|
||||
"tree-view": "В виде дерева",
|
||||
"recipe-yield": "Количество порций",
|
||||
|
||||
@@ -583,6 +583,8 @@
|
||||
"report-deletion-failed": "Vymazanie reportov zlyhalo",
|
||||
"recipe-debugger": "Debugger receptov",
|
||||
"recipe-debugger-description": "Vezmite URL odkaz receptu, ktorý chcete opraviť a vložte ju sem. Recept bude stiahnutý z URL odkazu pomocou scrapera receptov a zobrazí sa výsledok sťahovania. V prípade, ak nevidíte žiadne zobrazené dáta, stránka z URL odkazu nie je podporovaná Mealie alebo jej scrapovacou knižnicou.",
|
||||
"use-openai": "Use OpenAI",
|
||||
"recipe-debugger-use-openai-description": "Use OpenAI to parse the results instead of relying on the scraper library. When creating a recipe via URL, this is done automatically if the scraper library fails, but you may test it manually here.",
|
||||
"debug": "Debugovať",
|
||||
"tree-view": "Stromový pohľad",
|
||||
"recipe-yield": "Počet porcií",
|
||||
|
||||
@@ -583,6 +583,8 @@
|
||||
"report-deletion-failed": "Brisanje poročila ni bilo uspešno",
|
||||
"recipe-debugger": "Odpravljanje težav v strganju recepta",
|
||||
"recipe-debugger-description": "Prilepi povezavo do recepta, ki ga želiš postrgati. Strgalnik receptov bo postrgal spletno stran in prikazal rezultate. Če ne vidiš nobenih podatkov, potem spletna stran, ki jo želiš strgati ni podprta v Mealie oz. v strgalniku, ki ga uporablja.",
|
||||
"use-openai": "Use OpenAI",
|
||||
"recipe-debugger-use-openai-description": "Use OpenAI to parse the results instead of relying on the scraper library. When creating a recipe via URL, this is done automatically if the scraper library fails, but you may test it manually here.",
|
||||
"debug": "Debug",
|
||||
"tree-view": "Drevesni prikaz",
|
||||
"recipe-yield": "Število porcij",
|
||||
|
||||
@@ -583,6 +583,8 @@
|
||||
"report-deletion-failed": "Report deletion failed",
|
||||
"recipe-debugger": "Recipe Debugger",
|
||||
"recipe-debugger-description": "Grab the URL of the recipe you want to debug and paste it here. The URL will be scraped by the recipe scraper and the results will be displayed. If you don't see any data returned, the site you are trying to scrape is not supported by Mealie or its scraper library.",
|
||||
"use-openai": "Use OpenAI",
|
||||
"recipe-debugger-use-openai-description": "Use OpenAI to parse the results instead of relying on the scraper library. When creating a recipe via URL, this is done automatically if the scraper library fails, but you may test it manually here.",
|
||||
"debug": "Debug",
|
||||
"tree-view": "Tree View",
|
||||
"recipe-yield": "Recipe Yield",
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
"clear": "Rensa",
|
||||
"close": "Stäng",
|
||||
"confirm": "Bekräfta",
|
||||
"confirm-how-does-everything-look": "How does everything look?",
|
||||
"confirm-how-does-everything-look": "Hur ser det ut?",
|
||||
"confirm-delete-generic": "Är du säker på att du vill radera detta?",
|
||||
"copied_message": "Kopierad!",
|
||||
"create": "Skapa",
|
||||
@@ -291,8 +291,8 @@
|
||||
"mealplan-updated": "Måltidsplan uppdaterad",
|
||||
"no-meal-plan-defined-yet": "Ingen måltidsplan definierad ännu",
|
||||
"no-meal-planned-for-today": "Ingen måltidsplan för idag",
|
||||
"numberOfDays-hint": "Number of days on page load",
|
||||
"numberOfDays-label": "Default Days",
|
||||
"numberOfDays-hint": "Antal dagar vid sidhämtning",
|
||||
"numberOfDays-label": "Förvalda dagar",
|
||||
"only-recipes-with-these-categories-will-be-used-in-meal-plans": "Endast recept med dessa kategorier kommer att användas i måltidsplaner",
|
||||
"planner": "Planeringkalender",
|
||||
"quick-week": "Snabb vecka",
|
||||
@@ -367,7 +367,7 @@
|
||||
"choose-migration-type": "Välj migrationstyp",
|
||||
"tag-all-recipes": "Tagga alla recept med {tag-name} tagg",
|
||||
"nextcloud-text": "Nextcloud-recept kan importeras från en zip-fil som innehåller data som lagras i Nextcloud. Se exempelmappens struktur nedan för att säkerställa att dina recept kan importeras.",
|
||||
"chowdown-text": "Mealie natively supports the chowdown repository format. Download the code repository as a .zip file and upload it below.",
|
||||
"chowdown-text": "Mealie har inbyggt stöd för chowdowns repository-format. Ladda ner kodförrådet som en .zip-fil och ladda upp det nedan.",
|
||||
"recipe-1": "Recept 1",
|
||||
"recipe-2": "Recept 2",
|
||||
"paprika-text": "Mealie kan importera recept från Paprika-applikationen. Exportera dina recept från Paprika, byt namn på filnamnstillägget på exporten till .zip och ladda upp det nedan.",
|
||||
@@ -382,7 +382,7 @@
|
||||
},
|
||||
"recipekeeper": {
|
||||
"title": "Recipe Keeper",
|
||||
"description-long": "Mealie can import recipes from Recipe Keeper. Export your recipes in zip format, then upload the .zip file below."
|
||||
"description-long": "Mealie kan importera recept från Recept Keeper. Exportera dina recept i zip-format, ladda sedan upp .zip-filen här under."
|
||||
}
|
||||
},
|
||||
"new-recipe": {
|
||||
@@ -583,6 +583,8 @@
|
||||
"report-deletion-failed": "Rapporten gick inte att radera",
|
||||
"recipe-debugger": "Receptfelsökning",
|
||||
"recipe-debugger-description": "Ta tag i URL: en till det recept du vill felsöka och klistra in det här. URL-adressen kommer att skrapas av receptskrapan och resultaten kommer att visas. Om du inte ser några data returnerade, stödjs inte webbplatsen du försöker skrapa av Mealie eller dess skrapbibliotek.",
|
||||
"use-openai": "Använd OpenAI",
|
||||
"recipe-debugger-use-openai-description": "Använd OpenAI för att tolka resultaten istället för att förlita sig på skrapans bibliotek. När du skapar ett recept via URL görs detta automatiskt om skrapbiblioteket misslyckas, men du kan testa det manuellt här.",
|
||||
"debug": "Felsök",
|
||||
"tree-view": "Trädvy",
|
||||
"recipe-yield": "Receptutfall",
|
||||
@@ -591,7 +593,7 @@
|
||||
"screen-awake": "Håll skärmen vaken",
|
||||
"remove-image": "Ta bort bild",
|
||||
"nextStep": "Nästa steg",
|
||||
"recipe-actions": "Recipe Actions",
|
||||
"recipe-actions": "Recept åtgärder",
|
||||
"parser": {
|
||||
"experimental-alert-text": "Mealie använder naturligt språk för att tolka enheter och livsmedel som behövs för dina recept. Denna funktion är experimentell och kanske inte alltid funkar som förväntat. Om du föredrar att inte använda de tolkade resultatet, kan du välja 'Avbryt' och förändringarna kommer då inte sparas.",
|
||||
"ingredient-parser": "Ingrediensanalysator",
|
||||
@@ -605,7 +607,7 @@
|
||||
"no-unit": "Ingen enhet",
|
||||
"missing-unit": "Skapa saknad enhet: {unit}",
|
||||
"missing-food": "Skapa saknad ingrediens: {food}",
|
||||
"no-food": "No Food"
|
||||
"no-food": "Ingen mat"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
@@ -635,7 +637,7 @@
|
||||
"backup-created-at-response-export_path": "Backup skapad {path}",
|
||||
"backup-deleted": "Backup raderad",
|
||||
"restore-success": "Återställning slutförd",
|
||||
"restore-fail": "Restore failed. Check your server logs for more details",
|
||||
"restore-fail": "Återställning misslyckades. Kontrollera dina serverloggar för mer information",
|
||||
"backup-tag": "Backup tagg",
|
||||
"create-heading": "Skapa en säkerhetskopia",
|
||||
"delete-backup": "Ta bort säkerhetskopian",
|
||||
@@ -773,9 +775,9 @@
|
||||
"oidc-ready": "OIDC Klar",
|
||||
"oidc-ready-error-text": "Alla OIDC-värden är inte konfigurerade. Detta kan ignoreras om du inte använder OIDC-autentisering.",
|
||||
"oidc-ready-success-text": "Alla obligatoriska OIDC-variabler är satta.",
|
||||
"openai-ready": "OpenAI Ready",
|
||||
"openai-ready-error-text": "Not all OpenAI Values are configured. This can be ignored if you are not using OpenAI features.",
|
||||
"openai-ready-success-text": "Required OpenAI variables are all set."
|
||||
"openai-ready": "OpenAI redo",
|
||||
"openai-ready-error-text": "Alla OpenAI-värden är inte konfigurerade. Detta kan ignoreras om du inte använder OpenAI-funktioner.",
|
||||
"openai-ready-success-text": "Alla obligatoriska OpenAI-variabler är satta."
|
||||
},
|
||||
"shopping-list": {
|
||||
"all-lists": "Visa alla listor",
|
||||
@@ -789,7 +791,7 @@
|
||||
"food": "Mat",
|
||||
"note": "Anteckning",
|
||||
"label": "Etikett",
|
||||
"save-label": "Save Label",
|
||||
"save-label": "Spara etikett",
|
||||
"linked-item-warning": "Denna artikel är länkad till ett eller flera recept. Justering av enheter eller livsmedel ger oväntade resultat när du lägger till eller tar bort receptet från denna lista.",
|
||||
"toggle-food": "Växla mat",
|
||||
"manage-labels": "Hantera etiketter",
|
||||
@@ -1031,10 +1033,10 @@
|
||||
"source-unit-will-be-deleted": "Källenheten kommer att raderas"
|
||||
},
|
||||
"recipe-actions": {
|
||||
"recipe-actions-data": "Recipe Actions Data",
|
||||
"new-recipe-action": "New Recipe Action",
|
||||
"edit-recipe-action": "Edit Recipe Action",
|
||||
"action-type": "Action Type"
|
||||
"recipe-actions-data": "Data för receptåtgärder",
|
||||
"new-recipe-action": "Ny receptåtgärd",
|
||||
"edit-recipe-action": "Redigera receptåtgärd",
|
||||
"action-type": "Åtgärdstyp"
|
||||
},
|
||||
"create-alias": "Skapa alias",
|
||||
"manage-aliases": "Hantera alias",
|
||||
@@ -1193,14 +1195,14 @@
|
||||
"no-logs-found": "Inga loggar hittade",
|
||||
"tasks": "Uppgifter",
|
||||
"setup": {
|
||||
"first-time-setup": "First Time Setup",
|
||||
"welcome-to-mealie-get-started": "Welcome to Mealie! Let's get started",
|
||||
"already-set-up-bring-to-homepage": "I'm already set up, just bring me to the homepage",
|
||||
"common-settings-for-new-sites": "Here are some common settings for new sites",
|
||||
"setup-complete": "Setup Complete!",
|
||||
"here-are-a-few-things-to-help-you-get-started": "Here are a few things to help you get started with Mealie",
|
||||
"restore-from-v1-backup": "Have a backup from a previous instance of Mealie v1? You can restore it here.",
|
||||
"manage-profile-or-get-invite-link": "Manage your own profile, or grab an invite link to share with others."
|
||||
"first-time-setup": "Första installationen",
|
||||
"welcome-to-mealie-get-started": "Välkommen till Mealie! Låt oss komma igång",
|
||||
"already-set-up-bring-to-homepage": "Jag har redan gjort inställningarna, ta mig bara till hemsidan",
|
||||
"common-settings-for-new-sites": "Här är några vanliga inställningar för nya webbplatser",
|
||||
"setup-complete": "Konfigurationen slutförd!",
|
||||
"here-are-a-few-things-to-help-you-get-started": "Här är några saker som hjälper dig att komma igång med Mealie",
|
||||
"restore-from-v1-backup": "Har du en säkerhetskopia från en tidigare instans av Mealie v1? Du kan återställa den här.",
|
||||
"manage-profile-or-get-invite-link": "Hantera din egen profil eller hämta en inbjudningslänk för att dela med andra."
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
@@ -1209,16 +1211,16 @@
|
||||
"get-invite-link": "Få inbjudningslänk",
|
||||
"get-public-link": "Få offentlig länk",
|
||||
"account-summary": "Kontosammanfattning",
|
||||
"account-summary-description": "Here's a summary of your group's information.",
|
||||
"account-summary-description": "Här är en sammanfattning av din grupps information.",
|
||||
"group-statistics": "Gruppstatistik",
|
||||
"group-statistics-description": "Din gruppstatistik ger dig en inblick i hur du använder Mealie.",
|
||||
"storage-capacity": "Lagringskapacitet",
|
||||
"storage-capacity-description": "Din lagringskapacitet är en beräkning av de bilder och tillgångar du har laddat upp.",
|
||||
"personal": "Personligt",
|
||||
"personal-description": "These are settings that are personal to you. Changes here won't affect other users.",
|
||||
"personal-description": "Det här är inställningar som är personliga för dig. Ändringar här påverkar inte andra användare.",
|
||||
"user-settings": "Användarinställningar",
|
||||
"user-settings-description": "Manage your preferences, change your password, and update your email.",
|
||||
"api-tokens-description": "Manage your API Tokens for access from external applications.",
|
||||
"user-settings-description": "Hantera dina inställningar, ändra ditt lösenord och uppdatera din e-post.",
|
||||
"api-tokens-description": "Hantera dina API-Tokens för åtkomst från externa program.",
|
||||
"group-description": "Dessa objekt delas inom din grupp. Att redigera en av dem kommer att ändra den för hela gruppen!",
|
||||
"group-settings": "Gruppinställningar",
|
||||
"group-settings-description": "Hantera dina gemensamma gruppinställningar som måltidsplan och sekretessinställningar.",
|
||||
@@ -1229,9 +1231,9 @@
|
||||
"notifiers": "Notifierare",
|
||||
"notifiers-description": "Setup email and push notifications that trigger on specific events.",
|
||||
"manage-data": "Hantera data",
|
||||
"manage-data-description": "Manage your Mealie data; Foods, Units, Categories, Tags and more.",
|
||||
"manage-data-description": "Hantera din Mealie data; Livsmedel, Enheter, Kategorier, Taggar och mer.",
|
||||
"data-migrations": "Data migreringar",
|
||||
"data-migrations-description": "Migrate your existing data from other applications like Nextcloud Recipes and Chowdown.",
|
||||
"data-migrations-description": "Migrera befintliga data från andra program som Nextcloud Recipes och Chowdown.",
|
||||
"email-sent": "E-post skickades",
|
||||
"error-sending-email": "Fel vid sändning av e-post",
|
||||
"personal-information": "Personlig information",
|
||||
|
||||
@@ -583,6 +583,8 @@
|
||||
"report-deletion-failed": "Report deletion failed",
|
||||
"recipe-debugger": "Recipe Debugger",
|
||||
"recipe-debugger-description": "Grab the URL of the recipe you want to debug and paste it here. The URL will be scraped by the recipe scraper and the results will be displayed. If you don't see any data returned, the site you are trying to scrape is not supported by Mealie or its scraper library.",
|
||||
"use-openai": "Use OpenAI",
|
||||
"recipe-debugger-use-openai-description": "Use OpenAI to parse the results instead of relying on the scraper library. When creating a recipe via URL, this is done automatically if the scraper library fails, but you may test it manually here.",
|
||||
"debug": "Hata ayıklama",
|
||||
"tree-view": "Ağaç Görünümü",
|
||||
"recipe-yield": "Recipe Yield",
|
||||
|
||||
@@ -291,8 +291,8 @@
|
||||
"mealplan-updated": "План харчування оновлено",
|
||||
"no-meal-plan-defined-yet": "Не створено жодного плану харчування",
|
||||
"no-meal-planned-for-today": "Не заплановано харчування на сьогодні",
|
||||
"numberOfDays-hint": "Number of days on page load",
|
||||
"numberOfDays-label": "Default Days",
|
||||
"numberOfDays-hint": "Скільки днів завантажувати на сторінку",
|
||||
"numberOfDays-label": "Дні за замовчуванням",
|
||||
"only-recipes-with-these-categories-will-be-used-in-meal-plans": "Лише рецепти з цими категоріями будуть використані в планах харчування",
|
||||
"planner": "Планувальник",
|
||||
"quick-week": "Швидкий тиждень",
|
||||
@@ -382,7 +382,7 @@
|
||||
},
|
||||
"recipekeeper": {
|
||||
"title": "Recipe Keeper",
|
||||
"description-long": "Mealie can import recipes from Recipe Keeper. Export your recipes in zip format, then upload the .zip file below."
|
||||
"description-long": "Mealie може імпортувати рецепти з Recipe Keeper. Експортувати ваші рецепти у форматі zip, а потім відвантажте файл .zip нижче."
|
||||
}
|
||||
},
|
||||
"new-recipe": {
|
||||
@@ -583,6 +583,8 @@
|
||||
"report-deletion-failed": "Не вдалося видалити звіт",
|
||||
"recipe-debugger": "Дебаггер рецептів",
|
||||
"recipe-debugger-description": "Вставте URL-адресу рецепта, який ви хочете дебажити сюди. URL буде розпарсено парсером рецептів й результати будуть відображені. Якщо ви не бачите жодних даних - значить Mealie або парсер рецептів не підтримує сайт який ви намагаєтеся використати.",
|
||||
"use-openai": "Використовувати OpenAI",
|
||||
"recipe-debugger-use-openai-description": "Використовуйте OpenAI для аналізу результатів замість використання бібліотеки парсера. Під час створення рецепта через URL це робиться автоматично, якщо бібліотека парсера не впоралася, але ви можете перевірити це тут вручну.",
|
||||
"debug": "Дебажити",
|
||||
"tree-view": "У вигляді дерева",
|
||||
"recipe-yield": "Вихід рецепту",
|
||||
@@ -635,7 +637,7 @@
|
||||
"backup-created-at-response-export_path": "Резервна копія створена {path}",
|
||||
"backup-deleted": "Резервна копія видалена",
|
||||
"restore-success": "Відновлення успішне",
|
||||
"restore-fail": "Restore failed. Check your server logs for more details",
|
||||
"restore-fail": "Не вдалося відновити. Перевірте журнали вашого сервера для більш докладної інформації",
|
||||
"backup-tag": "Мітка резервної копії",
|
||||
"create-heading": "Створити резервну копію",
|
||||
"delete-backup": "Видалити резервну копію",
|
||||
|
||||
@@ -583,6 +583,8 @@
|
||||
"report-deletion-failed": "Report deletion failed",
|
||||
"recipe-debugger": "Recipe Debugger",
|
||||
"recipe-debugger-description": "Grab the URL of the recipe you want to debug and paste it here. The URL will be scraped by the recipe scraper and the results will be displayed. If you don't see any data returned, the site you are trying to scrape is not supported by Mealie or its scraper library.",
|
||||
"use-openai": "Use OpenAI",
|
||||
"recipe-debugger-use-openai-description": "Use OpenAI to parse the results instead of relying on the scraper library. When creating a recipe via URL, this is done automatically if the scraper library fails, but you may test it manually here.",
|
||||
"debug": "Debug",
|
||||
"tree-view": "Tree View",
|
||||
"recipe-yield": "Recipe Yield",
|
||||
|
||||
@@ -583,6 +583,8 @@
|
||||
"report-deletion-failed": "删除报告失败",
|
||||
"recipe-debugger": "食谱调试器",
|
||||
"recipe-debugger-description": "抓取你想要的食谱的URL并粘贴在此。食谱刮削器将尝试刮削该URL并显示结果。如果你没看到任何返回数据,则说明对应的网站不支持Mealie或它的刮削库。",
|
||||
"use-openai": "Use OpenAI",
|
||||
"recipe-debugger-use-openai-description": "Use OpenAI to parse the results instead of relying on the scraper library. When creating a recipe via URL, this is done automatically if the scraper library fails, but you may test it manually here.",
|
||||
"debug": "调试",
|
||||
"tree-view": "树状图",
|
||||
"recipe-yield": "食谱菜量",
|
||||
|
||||
@@ -583,6 +583,8 @@
|
||||
"report-deletion-failed": "Report deletion failed",
|
||||
"recipe-debugger": "Recipe Debugger",
|
||||
"recipe-debugger-description": "Grab the URL of the recipe you want to debug and paste it here. The URL will be scraped by the recipe scraper and the results will be displayed. If you don't see any data returned, the site you are trying to scrape is not supported by Mealie or its scraper library.",
|
||||
"use-openai": "Use OpenAI",
|
||||
"recipe-debugger-use-openai-description": "Use OpenAI to parse the results instead of relying on the scraper library. When creating a recipe via URL, this is done automatically if the scraper library fails, but you may test it manually here.",
|
||||
"debug": "Debug",
|
||||
"tree-view": "Tree View",
|
||||
"recipe-yield": "Recipe Yield",
|
||||
|
||||
@@ -128,8 +128,8 @@ export class RecipeAPI extends BaseCRUDAPI<CreateRecipe, Recipe, Recipe> {
|
||||
return this.requests.post<UpdateImageResponse>(routes.recipesRecipeSlugImage(slug), { url });
|
||||
}
|
||||
|
||||
async testCreateOneUrl(url: string) {
|
||||
return await this.requests.post<Recipe | null>(routes.recipesTestScrapeUrl, { url });
|
||||
async testCreateOneUrl(url: string, useOpenAI = false) {
|
||||
return await this.requests.post<Recipe | null>(routes.recipesTestScrapeUrl, { url, useOpenAI });
|
||||
}
|
||||
|
||||
async createOneByUrl(url: string, includeTags: boolean) {
|
||||
|
||||
@@ -401,12 +401,24 @@ export default {
|
||||
"short_name": "Shopping Lists",
|
||||
"description": "Open the shopping lists",
|
||||
"url": "/shopping-lists",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/svgs/mdiFormatListChecks.svg",
|
||||
"sizes": "256x256",
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Meal Planner",
|
||||
"short_name": "Meal Planner",
|
||||
"description": "Open the meal planner",
|
||||
"url": "/group/mealplan/planner/view",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/svgs/mdiCalendarMultiselect.svg",
|
||||
"sizes": "256x256",
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -18,7 +18,11 @@
|
||||
:rules="[validators.url]"
|
||||
:hint="$t('new-recipe.url-form-hint')"
|
||||
persistent-hint
|
||||
></v-text-field>
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-text v-if="appInfo && appInfo.enableOpenai">
|
||||
{{ $t('recipe.recipe-debugger-use-openai-description') }}
|
||||
<v-checkbox v-model="useOpenAI" :label="$t('recipe.use-openai')"></v-checkbox>
|
||||
</v-card-text>
|
||||
<v-card-actions class="justify-center">
|
||||
<div style="width: 250px">
|
||||
@@ -51,7 +55,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, toRefs, ref, useRouter, computed, useRoute } from "@nuxtjs/composition-api";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { useAppInfo, useUserApi } from "~/composables/api";
|
||||
import { validators } from "~/composables/use-validators";
|
||||
import { Recipe } from "~/lib/api/types/recipe";
|
||||
|
||||
@@ -60,11 +64,13 @@ export default defineComponent({
|
||||
const state = reactive({
|
||||
error: false,
|
||||
loading: false,
|
||||
useOpenAI: false,
|
||||
});
|
||||
|
||||
const api = useUserApi();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const appInfo = useAppInfo();
|
||||
|
||||
const recipeUrl = computed({
|
||||
set(recipe_import_url: string | null) {
|
||||
@@ -89,13 +95,14 @@ export default defineComponent({
|
||||
|
||||
state.loading = true;
|
||||
|
||||
const { data } = await api.recipes.testCreateOneUrl(url);
|
||||
const { data } = await api.recipes.testCreateOneUrl(url, state.useOpenAI);
|
||||
|
||||
state.loading = false;
|
||||
debugData.value = data;
|
||||
}
|
||||
|
||||
return {
|
||||
appInfo,
|
||||
recipeUrl,
|
||||
debugTreeView,
|
||||
debugUrl,
|
||||
|
||||
@@ -132,14 +132,14 @@ export default defineComponent({
|
||||
text: i18n.tc("migration.plantoeat.title"),
|
||||
value: MIGRATIONS.plantoeat,
|
||||
},
|
||||
{
|
||||
text: i18n.tc("migration.tandoor.title"),
|
||||
value: MIGRATIONS.tandoor,
|
||||
},
|
||||
{
|
||||
text: i18n.tc("migration.recipekeeper.title"),
|
||||
value: MIGRATIONS.recipekeeper,
|
||||
},
|
||||
{
|
||||
text: i18n.tc("migration.tandoor.title"),
|
||||
value: MIGRATIONS.tandoor,
|
||||
},
|
||||
];
|
||||
const _content = {
|
||||
[MIGRATIONS.mealie]: {
|
||||
@@ -312,6 +312,26 @@ export default defineComponent({
|
||||
}
|
||||
],
|
||||
},
|
||||
[MIGRATIONS.recipekeeper]: {
|
||||
text: i18n.tc("migration.recipekeeper.description-long"),
|
||||
acceptedFileType: ".zip",
|
||||
tree: [
|
||||
{
|
||||
id: 1,
|
||||
icon: $globals.icons.zip,
|
||||
name: "recipekeeperhtml.zip",
|
||||
children: [
|
||||
{ id: 2, name: "recipes.html", icon: $globals.icons.codeJson },
|
||||
{ id: 3, name: "images", icon: $globals.icons.folderOutline,
|
||||
children: [
|
||||
{ id: 4, name: "image1.jpg", icon: $globals.icons.fileImage },
|
||||
{ id: 5, name: "image2.jpg", icon: $globals.icons.fileImage },
|
||||
]
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
},
|
||||
[MIGRATIONS.tandoor]: {
|
||||
text: i18n.tc("migration.tandoor.description-long"),
|
||||
acceptedFileType: ".zip",
|
||||
@@ -352,26 +372,6 @@ export default defineComponent({
|
||||
}
|
||||
],
|
||||
},
|
||||
[MIGRATIONS.recipekeeper]: {
|
||||
text: i18n.tc("migration.recipekeeper.description-long"),
|
||||
acceptedFileType: ".zip",
|
||||
tree: [
|
||||
{
|
||||
id: 1,
|
||||
icon: $globals.icons.zip,
|
||||
name: "recipekeeperhtml.zip",
|
||||
children: [
|
||||
{ id: 2, name: "recipes.html", icon: $globals.icons.codeJson },
|
||||
{ id: 3, name: "images", icon: $globals.icons.folderOutline,
|
||||
children: [
|
||||
{ id: 4, name: "image1.jpeg", icon: $globals.icons.fileImage },
|
||||
{ id: 5, name: "image2.jpeg", icon: $globals.icons.fileImage },
|
||||
]
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
function setFileObject(fileObject: File) {
|
||||
|
||||
1
frontend/static/svgs/mdiCalendarMultiselect.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="mdi-calendar-multiselect" viewBox="0 0 24 24"><path d="M19,19V8H5V19H19M16,1H18V3H19A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5C3.89,21 3,20.1 3,19V5C3,3.89 3.89,3 5,3H6V1H8V3H16V1M7,10H9V12H7V10M15,10H17V12H15V10M11,14H13V16H11V14M15,14H17V16H15V14Z" /></svg>
|
||||
|
After Width: | Height: | Size: 297 B |
1
frontend/static/svgs/mdiFormatListChecks.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="mdi-format-list-checks" viewBox="0 0 24 24"><path d="M3,5H9V11H3V5M5,7V9H7V7H5M11,7H21V9H11V7M11,15H21V17H11V15M5,20L1.5,16.5L2.91,15.09L5,17.17L9.59,12.59L11,14L5,20Z" /></svg>
|
||||
|
After Width: | Height: | Size: 221 B |
@@ -5,7 +5,17 @@ from zipfile import ZipFile
|
||||
|
||||
import orjson
|
||||
import sqlalchemy
|
||||
from fastapi import BackgroundTasks, Depends, File, Form, HTTPException, Path, Query, Request, status
|
||||
from fastapi import (
|
||||
BackgroundTasks,
|
||||
Depends,
|
||||
File,
|
||||
Form,
|
||||
HTTPException,
|
||||
Path,
|
||||
Query,
|
||||
Request,
|
||||
status,
|
||||
)
|
||||
from fastapi.datastructures import UploadFile
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import UUID4, BaseModel, Field
|
||||
@@ -14,7 +24,11 @@ from starlette.background import BackgroundTask
|
||||
from starlette.responses import FileResponse
|
||||
|
||||
from mealie.core import exceptions
|
||||
from mealie.core.dependencies import get_temporary_path, get_temporary_zip_path, validate_recipe_token
|
||||
from mealie.core.dependencies import (
|
||||
get_temporary_path,
|
||||
get_temporary_zip_path,
|
||||
validate_recipe_token,
|
||||
)
|
||||
from mealie.core.security import create_recipe_slug_token
|
||||
from mealie.db.models.group.cookbook import CookBook
|
||||
from mealie.pkgs import cache
|
||||
@@ -26,10 +40,19 @@ from mealie.routes._base.routers import MealieCrudRoute, UserAPIRouter
|
||||
from mealie.schema.cookbook.cookbook import ReadCookBook
|
||||
from mealie.schema.make_dependable import make_dependable
|
||||
from mealie.schema.recipe import Recipe, RecipeImageTypes, ScrapeRecipe
|
||||
from mealie.schema.recipe.recipe import CreateRecipe, CreateRecipeByUrlBulk, RecipeLastMade, RecipeSummary
|
||||
from mealie.schema.recipe.recipe import (
|
||||
CreateRecipe,
|
||||
CreateRecipeByUrlBulk,
|
||||
RecipeLastMade,
|
||||
RecipeSummary,
|
||||
)
|
||||
from mealie.schema.recipe.recipe_asset import RecipeAsset
|
||||
from mealie.schema.recipe.recipe_scraper import ScrapeRecipeTest
|
||||
from mealie.schema.recipe.request_helpers import RecipeDuplicate, RecipeZipTokenResponse, UpdateImageResponse
|
||||
from mealie.schema.recipe.request_helpers import (
|
||||
RecipeDuplicate,
|
||||
RecipeZipTokenResponse,
|
||||
UpdateImageResponse,
|
||||
)
|
||||
from mealie.schema.response import PaginationBase, PaginationQuery
|
||||
from mealie.schema.response.pagination import RecipeSearchQuery
|
||||
from mealie.schema.response.responses import ErrorResponse
|
||||
@@ -40,13 +63,21 @@ from mealie.services.event_bus_service.event_types import (
|
||||
EventRecipeData,
|
||||
EventTypes,
|
||||
)
|
||||
from mealie.services.recipe.recipe_data_service import InvalidDomainError, NotAnImageError, RecipeDataService
|
||||
from mealie.services.recipe.recipe_data_service import (
|
||||
InvalidDomainError,
|
||||
NotAnImageError,
|
||||
RecipeDataService,
|
||||
)
|
||||
from mealie.services.recipe.recipe_service import RecipeService
|
||||
from mealie.services.recipe.template_service import TemplateService
|
||||
from mealie.services.scraper.recipe_bulk_scraper import RecipeBulkScraperService
|
||||
from mealie.services.scraper.scraped_extras import ScraperContext
|
||||
from mealie.services.scraper.scraper import create_from_url
|
||||
from mealie.services.scraper.scraper_strategies import ForceTimeoutException, RecipeScraperPackage
|
||||
from mealie.services.scraper.scraper_strategies import (
|
||||
ForceTimeoutException,
|
||||
RecipeScraperOpenAI,
|
||||
RecipeScraperPackage,
|
||||
)
|
||||
|
||||
|
||||
class JSONBytes(JSONResponse):
|
||||
@@ -210,10 +241,11 @@ class RecipeController(BaseRecipeController):
|
||||
return {"reportId": report_id}
|
||||
|
||||
@router.post("/test-scrape-url")
|
||||
async def test_parse_recipe_url(self, url: ScrapeRecipeTest):
|
||||
async def test_parse_recipe_url(self, data: ScrapeRecipeTest):
|
||||
# Debugger should produce the same result as the scraper sees before cleaning
|
||||
ScraperClass = RecipeScraperOpenAI if data.use_openai else RecipeScraperPackage
|
||||
try:
|
||||
if scraped_data := await RecipeScraperPackage(url.url, self.translator).scrape_url():
|
||||
if scraped_data := await ScraperClass(data.url, self.translator).scrape_url():
|
||||
return scraped_data.schema.data
|
||||
except ForceTimeoutException as e:
|
||||
raise HTTPException(
|
||||
|
||||
0
mealie/schema/openai/__init__.py
Normal file
10
mealie/schema/openai/_base.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class OpenAIBase(BaseModel):
|
||||
"""
|
||||
This class defines the JSON schema sent to OpenAI. Its schema is
|
||||
injected directly into the OpenAI prompt.
|
||||
"""
|
||||
|
||||
__doc__ = "" # we don't want to include the docstring in the JSON schema
|
||||
92
mealie/schema/openai/recipe_ingredient.py
Normal file
@@ -0,0 +1,92 @@
|
||||
from textwrap import dedent
|
||||
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
from ._base import OpenAIBase
|
||||
|
||||
|
||||
class OpenAIIngredient(OpenAIBase):
|
||||
input: str = Field(
|
||||
...,
|
||||
description=dedent(
|
||||
"""
|
||||
The input is simply the ingredient string you are processing as-is. It is forbidden to
|
||||
modify this at all, you must provide the input exactly as you received it.
|
||||
"""
|
||||
),
|
||||
)
|
||||
confidence: float | None = Field(
|
||||
None,
|
||||
description=dedent(
|
||||
"""
|
||||
This value is a float between 0 - 100, where 100 is full confidence that the result is correct,
|
||||
and 0 is no confidence that the result is correct. If you're unable to parse anything,
|
||||
and you put the entire string in the notes, you should return 0 confidence. If you can easily
|
||||
parse the string into each component, then you should return a confidence of 100. If you have to
|
||||
guess which part is the unit and which part is the food, your confidence should be lower, such as 60.
|
||||
Even if there is no unit or note, if you're able to determine the food, you may use a higher confidence.
|
||||
If the entire ingredient consists of only a food, you can use a confidence of 100.
|
||||
"""
|
||||
),
|
||||
)
|
||||
|
||||
quantity: float | None = Field(
|
||||
0,
|
||||
description=dedent(
|
||||
"""
|
||||
The numerical representation of how much of this ingredient. For instance, if you receive
|
||||
"3 1/2 grams of minced garlic", the quantity is "3 1/2". Quantity may be represented as a whole number
|
||||
(integer), a float or decimal, or a fraction. You should output quantity in only whole numbers or
|
||||
floats, converting fractions into floats. Floats longer than 10 decimal places should be
|
||||
rounded to 10 decimal places.
|
||||
"""
|
||||
),
|
||||
)
|
||||
unit: str | None = Field(
|
||||
None,
|
||||
description=dedent(
|
||||
"""
|
||||
The unit of measurement for this ingredient. For instance, if you receive
|
||||
"2 lbs chicken breast", the unit is "lbs" (short for "pounds").
|
||||
"""
|
||||
),
|
||||
)
|
||||
food: str | None = Field(
|
||||
None,
|
||||
description=dedent(
|
||||
"""
|
||||
The actual physical ingredient used in the recipe. For instance, if you receive
|
||||
"3 cups of onions, chopped", the food is "onions".
|
||||
"""
|
||||
),
|
||||
)
|
||||
note: str | None = Field(
|
||||
None,
|
||||
description=dedent(
|
||||
"""
|
||||
The rest of the text that represents more detail on how to prepare the ingredient.
|
||||
Anything that is not one of the above should be the note. For instance, if you receive
|
||||
"one can of butter beans, drained" the note would be "drained". If you receive
|
||||
"3 cloves of garlic peeled and finely chopped", the note would be "peeled and finely chopped".
|
||||
"""
|
||||
),
|
||||
)
|
||||
|
||||
@field_validator("quantity")
|
||||
def coerce_none_qty(cls, v: float | None) -> float:
|
||||
return v or 0
|
||||
|
||||
@field_validator("confidence")
|
||||
def validate_confidence(cls, v: float | None) -> float:
|
||||
v = v or 0
|
||||
|
||||
if v < 0:
|
||||
v = 0
|
||||
elif v > 100:
|
||||
v = 100
|
||||
|
||||
return v / 100
|
||||
|
||||
|
||||
class OpenAIIngredients(OpenAIBase):
|
||||
ingredients: list[OpenAIIngredient] = []
|
||||
@@ -1,10 +1,11 @@
|
||||
from pydantic import ConfigDict
|
||||
from pydantic import ConfigDict, Field
|
||||
|
||||
from mealie.schema._mealie.mealie_model import MealieModel
|
||||
|
||||
|
||||
class ScrapeRecipeTest(MealieModel):
|
||||
url: str
|
||||
use_openai: bool = Field(False, alias="useOpenAI")
|
||||
|
||||
|
||||
class ScrapeRecipe(MealieModel):
|
||||
|
||||
@@ -74,6 +74,28 @@ class BaseMigrator(BaseService):
|
||||
|
||||
super().__init__()
|
||||
|
||||
@classmethod
|
||||
def get_zip_base_path(cls, path: Path) -> Path:
|
||||
# Safari mangles our ZIP structure and adds a "__MACOSX" directory at the root along with
|
||||
# an arbitrarily-named directory containing the actual contents. So, if we find a dunder directory
|
||||
# at the root (i.e. __MACOSX) we traverse down the first non-dunder directory and assume this is the base.
|
||||
# We assume migration exports never contain a directory that starts with "__".
|
||||
normal_dirs: list[Path] = []
|
||||
dunder_dirs: list[Path] = []
|
||||
for dir in path.iterdir():
|
||||
if not dir.is_dir():
|
||||
continue
|
||||
|
||||
if dir.name.startswith("__"):
|
||||
dunder_dirs.append(dir)
|
||||
else:
|
||||
normal_dirs.append(dir)
|
||||
|
||||
if len(normal_dirs) == 1 and len(dunder_dirs) == 1:
|
||||
return normal_dirs[0]
|
||||
else:
|
||||
return path
|
||||
|
||||
def _migrate(self) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@@ -20,12 +20,24 @@ class ChowdownMigrator(BaseMigrator):
|
||||
MigrationAlias(key="tags", alias="tags", func=split_by_comma),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_zip_base_path(cls, path: Path) -> Path:
|
||||
potential_path = super().get_zip_base_path(path)
|
||||
if path == potential_path:
|
||||
return path
|
||||
|
||||
# make sure we didn't accidentally open a recipe dir
|
||||
if (potential_path / "recipe.json").exists():
|
||||
return path
|
||||
else:
|
||||
return potential_path
|
||||
|
||||
def _migrate(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
with zipfile.ZipFile(self.archive) as zip_file:
|
||||
zip_file.extractall(tmpdir)
|
||||
|
||||
temp_path = Path(tmpdir)
|
||||
temp_path = self.get_zip_base_path(Path(tmpdir))
|
||||
|
||||
chow_dir = next(temp_path.iterdir())
|
||||
image_dir = temp_path.joinpath(chow_dir, "images")
|
||||
|
||||
@@ -86,7 +86,7 @@ class CopyMeThatMigrator(BaseMigrator):
|
||||
with zipfile.ZipFile(self.archive) as zip_file:
|
||||
zip_file.extractall(tmpdir)
|
||||
|
||||
source_dir = Path(tmpdir)
|
||||
source_dir = self.get_zip_base_path(Path(tmpdir))
|
||||
|
||||
recipes_as_dicts: list[dict] = []
|
||||
for recipes_data_file in source_dir.glob("*.html"):
|
||||
|
||||
@@ -25,6 +25,18 @@ class MealieAlphaMigrator(BaseMigrator):
|
||||
MigrationAlias(key="tags", alias="tags", func=split_by_comma),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_zip_base_path(cls, path: Path) -> Path:
|
||||
potential_path = super().get_zip_base_path(path)
|
||||
if path == potential_path:
|
||||
return path
|
||||
|
||||
# make sure we didn't accidentally open the "recipes" dir
|
||||
if potential_path.name == "recipes":
|
||||
return path
|
||||
else:
|
||||
return potential_path
|
||||
|
||||
def _convert_to_new_schema(self, recipe: dict) -> Recipe:
|
||||
if recipe.get("categories", False):
|
||||
recipe["recipeCategory"] = recipe.get("categories")
|
||||
@@ -55,7 +67,7 @@ class MealieAlphaMigrator(BaseMigrator):
|
||||
with zipfile.ZipFile(self.archive) as zip_file:
|
||||
zip_file.extractall(tmpdir)
|
||||
|
||||
temp_path = Path(tmpdir)
|
||||
temp_path = self.get_zip_base_path(Path(tmpdir))
|
||||
recipe_lookup: dict[str, Path] = {}
|
||||
|
||||
recipes: list[Recipe] = []
|
||||
|
||||
@@ -57,6 +57,18 @@ class NextcloudMigrator(BaseMigrator):
|
||||
MigrationAlias(key="performTime", alias="cookTime", func=parse_iso8601_duration),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_zip_base_path(cls, path: Path) -> Path:
|
||||
potential_path = super().get_zip_base_path(path)
|
||||
if path == potential_path:
|
||||
return path
|
||||
|
||||
# make sure we didn't accidentally open a recipe dir
|
||||
if (potential_path / "recipe.json").exists():
|
||||
return path
|
||||
else:
|
||||
return potential_path
|
||||
|
||||
def _migrate(self) -> None:
|
||||
# Unzip File into temp directory
|
||||
|
||||
@@ -65,7 +77,8 @@ class NextcloudMigrator(BaseMigrator):
|
||||
with zipfile.ZipFile(self.archive) as zip_file:
|
||||
zip_file.extractall(tmpdir)
|
||||
|
||||
potential_recipe_dirs = glob_walker(Path(tmpdir), glob_str="**/[!.]*.json", return_parent=True)
|
||||
base_dir = self.get_zip_base_path(Path(tmpdir))
|
||||
potential_recipe_dirs = glob_walker(base_dir, glob_str="**/[!.]*.json", return_parent=True)
|
||||
nextcloud_dirs = {y.slug: y for x in potential_recipe_dirs if (y := NextcloudDir.from_dir(x))}
|
||||
|
||||
all_recipes = []
|
||||
|
||||
@@ -11,6 +11,17 @@ from .utils.migration_alias import MigrationAlias
|
||||
from .utils.migration_helpers import import_image, parse_iso8601_duration
|
||||
|
||||
|
||||
def clean_instructions(instructions: list[str]) -> list[str]:
|
||||
try:
|
||||
for i, instruction in enumerate(instructions):
|
||||
if instruction.startswith(f"{i + 1}. "):
|
||||
instructions[i] = instruction.removeprefix(f"{i + 1}. ")
|
||||
|
||||
return instructions
|
||||
except Exception:
|
||||
return instructions
|
||||
|
||||
|
||||
def parse_recipe_div(recipe, image_path):
|
||||
meta = {}
|
||||
for item in recipe.find_all(lambda x: x.has_attr("itemprop")):
|
||||
@@ -59,7 +70,7 @@ class RecipeKeeperMigrator(BaseMigrator):
|
||||
key="recipeIngredient",
|
||||
alias="recipeIngredients",
|
||||
),
|
||||
MigrationAlias(key="recipeInstructions", alias="recipeDirections"),
|
||||
MigrationAlias(key="recipeInstructions", alias="recipeDirections", func=clean_instructions),
|
||||
MigrationAlias(key="performTime", alias="cookTime", func=parse_iso8601_duration),
|
||||
MigrationAlias(key="prepTime", alias="prepTime", func=parse_iso8601_duration),
|
||||
MigrationAlias(key="image", alias="photo0"),
|
||||
@@ -77,7 +88,7 @@ class RecipeKeeperMigrator(BaseMigrator):
|
||||
with zipfile.ZipFile(self.archive) as zip_file:
|
||||
zip_file.extractall(tmpdir)
|
||||
|
||||
source_dir = Path(tmpdir) / "recipekeeperhtml"
|
||||
source_dir = self.get_zip_base_path(Path(tmpdir))
|
||||
|
||||
recipes_as_dicts: list[dict] = []
|
||||
with open(source_dir / "recipes.html") as fp:
|
||||
|
||||
@@ -109,7 +109,7 @@ class TandoorMigrator(BaseMigrator):
|
||||
with zipfile.ZipFile(self.archive) as zip_file:
|
||||
zip_file.extractall(tmpdir)
|
||||
|
||||
source_dir = Path(tmpdir)
|
||||
source_dir = self.get_zip_base_path(Path(tmpdir))
|
||||
|
||||
recipes_as_dicts: list[dict] = []
|
||||
for i, recipe_zip_file in enumerate(source_dir.glob("*.zip")):
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
You are a bot that parses user input into recipe ingredients. You will receive a list of one or more ingredients, each containing one or more of the following components:
|
||||
- Food: the actual physical ingredient used in the recipe. For instance, if you receive "3 cups of onions, chopped", the food is "onions"
|
||||
- Unit: the unit of measurement for this ingredient. For instance, if you receive "2 lbs chicken breast", the unit is "lbs" (short for "pounds")
|
||||
- Quantity: the numerical representation of how much of this ingredient. For instance, if you receive "3 1/2 grams of minced garlic", the quantity is "3 1/2". Quantity may be represented as a whole number (integer), a float or decimal, or a fraction. You should output quantity in only whole numbers or floats, converting fractions into floats. Floats longer than 10 decimal places should be rounded to 10 decimal places.
|
||||
- Note: the rest of the text that represents more detail on how to prepare the ingredient. Anything that is not one of the above should be the note. For instance, if you receive "one can of butter beans, drained" the note would be "drained". If you receive "3 cloves of garlic peeled and finely chopped", the note would be "peeled and finely chopped"
|
||||
- Input: The input is simply the ingredient string you are processing as-is. It is forbidden to modify this at all, you must provide the input exactly as you received it
|
||||
|
||||
While parsing the ingredients, there are some things to keep in mind:
|
||||
You are a bot that parses user input into recipe ingredients. You will receive a list of one or more ingredients, each containing one or more of the following components: quantity, unit, food, and note. Their definitions are stated in the JSON schema below. While parsing the ingredients, there are some things to keep in mind:
|
||||
- If you cannot accurately determine the quantity, unit, food, or note, you should place everything into the note field and leave everything else empty. It's better to err on the side of putting everything in the note field than being wrong
|
||||
- You may receive recipe ingredients from multiple different languages. You should adhere to the grammar rules of the input language when trying to parse the ingredient string
|
||||
- Sometimes foods or units will be in their singular, plural, or other grammatical forms. You must interpret all of them appropriately
|
||||
@@ -17,8 +10,6 @@ While parsing the ingredients, there are some things to keep in mind:
|
||||
|
||||
It is imperative that you do not create any data or otherwise make up any information. Failure to adhere to this rule is illegal and will result in harsh punishment. If you are unsure, place the entire string into the note section of the response. Do not make things up.
|
||||
|
||||
In addition to calculating the recipe ingredient fields, you are also responsible for including a confidence value. This value is a float between 0 - 1, where 1 is full confidence that the result is correct, and 0 is no confidence that the result is correct. If you're unable to parse anything, and you put the entire string in the notes, you should return 0 confidence. If you can easily parse the string into each component, then you should return a confidence of 1. If you have to guess which part is the unit and which part is the food, your confidence should be lower, such as 0.6. Even if there is no unit or note, if you're able to determine the food, you may use a higher confidence. If the entire ingredient consists of only a food, you can use a confidence of 1.
|
||||
|
||||
Below you will receive the JSON schema for your response. Your response must be in valid JSON in the below schema as provided. You must respond in this JSON schema; failure to do so is illegal. It is imperative that you follow the schema precisely to avoid punishment. You must follow the JSON schema.
|
||||
|
||||
The user message that you receive will be the list of one or more recipe ingredients for you to parse. Your response should have exactly one item for each item provided. For instance, if you receive 12 items to parse, then your response should be an array of 12 parsed items.
|
||||
|
||||
7
mealie/services/openai/prompts/recipes/scrape-recipe.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
You are a bot that reads website data and parses it into recipe JSON. You will receive the contents of a webpage (such as its HTML) and you need to extract the recipe data and return its JSON in valid schema. The recipe schema is the standard schema.org schema, which is defined at "https://schema.org/Recipe".
|
||||
|
||||
It is imperative that you do not create any data or otherwise make up any information. Failure to adhere to this rule is illegal and will result in harsh punishment. If you are unable to extract data due to insufficient input, you may reply with a completely empty JSON object (represented by two brackets: {}).
|
||||
|
||||
Your response must be in valid JSON in the schema.org Recipe definition. You must respond in this JSON schema; failure to do so is illegal. It is imperative that you follow the schema precisely to avoid punishment. You must follow the JSON schema.
|
||||
|
||||
The user message that you receive will be the webpage contents, including (but not necessarily limited to) text extracted from the HTML.
|
||||
@@ -2,8 +2,7 @@ import asyncio
|
||||
import json
|
||||
from collections.abc import Awaitable
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from mealie.schema.openai.recipe_ingredient import OpenAIIngredient, OpenAIIngredients
|
||||
from mealie.schema.recipe.recipe_ingredient import (
|
||||
CreateIngredientFood,
|
||||
CreateIngredientUnit,
|
||||
@@ -16,27 +15,6 @@ from mealie.services.openai import OpenAIDataInjection, OpenAIService
|
||||
from .._base import ABCIngredientParser
|
||||
|
||||
|
||||
class OpenAIIngredient(BaseModel):
|
||||
"""
|
||||
This class defines the JSON schema sent to OpenAI. Its schema is
|
||||
injected directly into the OpenAI prompt.
|
||||
"""
|
||||
|
||||
__doc__ = "" # we don't want to include the docstring in the JSON schema
|
||||
|
||||
input: str
|
||||
confidence: float | None = None
|
||||
|
||||
quantity: float | None = 0
|
||||
unit: str | None = None
|
||||
food: str | None = None
|
||||
note: str | None = None
|
||||
|
||||
|
||||
class OpenAIIngredients(BaseModel):
|
||||
ingredients: list[OpenAIIngredient] = []
|
||||
|
||||
|
||||
class OpenAIParser(ABCIngredientParser):
|
||||
def _convert_ingredient(self, openai_ing: OpenAIIngredient) -> ParsedIngredient:
|
||||
ingredient = RecipeIngredient(
|
||||
|
||||
@@ -2,9 +2,19 @@ from mealie.lang.providers import Translator
|
||||
from mealie.schema.recipe.recipe import Recipe
|
||||
from mealie.services.scraper.scraped_extras import ScrapedExtras
|
||||
|
||||
from .scraper_strategies import ABCScraperStrategy, RecipeScraperOpenGraph, RecipeScraperPackage
|
||||
from .scraper_strategies import (
|
||||
ABCScraperStrategy,
|
||||
RecipeScraperOpenAI,
|
||||
RecipeScraperOpenGraph,
|
||||
RecipeScraperPackage,
|
||||
safe_scrape_html,
|
||||
)
|
||||
|
||||
DEFAULT_SCRAPER_STRATEGIES: list[type[ABCScraperStrategy]] = [RecipeScraperPackage, RecipeScraperOpenGraph]
|
||||
DEFAULT_SCRAPER_STRATEGIES: list[type[ABCScraperStrategy]] = [
|
||||
RecipeScraperPackage,
|
||||
RecipeScraperOpenAI,
|
||||
RecipeScraperOpenGraph,
|
||||
]
|
||||
|
||||
|
||||
class RecipeScraper:
|
||||
@@ -27,8 +37,9 @@ class RecipeScraper:
|
||||
Scrapes a recipe from the web.
|
||||
"""
|
||||
|
||||
raw_html = await safe_scrape_html(url)
|
||||
for scraper_type in self.scrapers:
|
||||
scraper = scraper_type(url, self.translator)
|
||||
scraper = scraper_type(url, self.translator, raw_html=raw_html)
|
||||
result = await scraper.parse()
|
||||
|
||||
if result is not None:
|
||||
|
||||
@@ -3,6 +3,7 @@ from abc import ABC, abstractmethod
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
import bs4
|
||||
import extruct
|
||||
from fastapi import HTTPException, status
|
||||
from httpx import AsyncClient
|
||||
@@ -10,10 +11,12 @@ from recipe_scrapers import NoSchemaFoundInWildMode, SchemaScraperFactory, scrap
|
||||
from slugify import slugify
|
||||
from w3lib.html import get_base_url
|
||||
|
||||
from mealie.core.config import get_app_settings
|
||||
from mealie.core.root_logger import get_logger
|
||||
from mealie.lang.providers import Translator
|
||||
from mealie.pkgs import safehttp
|
||||
from mealie.schema.recipe.recipe import Recipe, RecipeStep
|
||||
from mealie.services.openai import OpenAIService
|
||||
from mealie.services.scraper.scraped_extras import ScrapedExtras
|
||||
|
||||
from . import cleaner
|
||||
@@ -86,9 +89,15 @@ class ABCScraperStrategy(ABC):
|
||||
|
||||
url: str
|
||||
|
||||
def __init__(self, url: str, translator: Translator) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
url: str,
|
||||
translator: Translator,
|
||||
raw_html: str | None = None,
|
||||
) -> None:
|
||||
self.logger = get_logger()
|
||||
self.url = url
|
||||
self.raw_html = raw_html
|
||||
self.translator = translator
|
||||
|
||||
@abstractmethod
|
||||
@@ -109,7 +118,7 @@ class ABCScraperStrategy(ABC):
|
||||
|
||||
class RecipeScraperPackage(ABCScraperStrategy):
|
||||
async def get_html(self, url: str) -> str:
|
||||
return await safe_scrape_html(url)
|
||||
return self.raw_html or await safe_scrape_html(url)
|
||||
|
||||
def clean_scraper(self, scraped_data: SchemaScraperFactory.SchemaScraper, url: str) -> tuple[Recipe, ScrapedExtras]:
|
||||
def try_get_default(
|
||||
@@ -227,9 +236,75 @@ class RecipeScraperPackage(ABCScraperStrategy):
|
||||
return self.clean_scraper(scraped_data, self.url)
|
||||
|
||||
|
||||
class RecipeScraperOpenAI(RecipeScraperPackage):
|
||||
"""
|
||||
A wrapper around the `RecipeScraperPackage` class that uses OpenAI to extract the recipe from the URL,
|
||||
rather than trying to scrape it directly.
|
||||
"""
|
||||
|
||||
def find_image(self, soup: bs4.BeautifulSoup) -> str | None:
|
||||
# find the open graph image tag
|
||||
og_image = soup.find("meta", property="og:image")
|
||||
if og_image and og_image.get("content"):
|
||||
return og_image["content"]
|
||||
|
||||
# find the largest image on the page
|
||||
largest_img = None
|
||||
max_size = 0
|
||||
for img in soup.find_all("img"):
|
||||
width = img.get("width", 0)
|
||||
height = img.get("height", 0)
|
||||
if not width or not height:
|
||||
continue
|
||||
|
||||
size = int(width) * int(height)
|
||||
if size > max_size:
|
||||
max_size = size
|
||||
largest_img = img
|
||||
|
||||
if largest_img:
|
||||
return largest_img.get("src")
|
||||
|
||||
return None
|
||||
|
||||
def format_html_to_text(self, html: str) -> str:
|
||||
soup = bs4.BeautifulSoup(html, "lxml")
|
||||
|
||||
text = soup.get_text(separator="\n", strip=True)
|
||||
if not text:
|
||||
raise Exception("No text found in HTML")
|
||||
image = self.find_image(soup)
|
||||
|
||||
components = [f"Convert this content to JSON: {text}"]
|
||||
if image:
|
||||
components.append(f"Recipe Image: {image}")
|
||||
return "\n".join(components)
|
||||
|
||||
async def get_html(self, url: str) -> str:
|
||||
settings = get_app_settings()
|
||||
if not settings.OPENAI_ENABLED:
|
||||
return ""
|
||||
|
||||
html = self.raw_html or await safe_scrape_html(url)
|
||||
text = self.format_html_to_text(html)
|
||||
try:
|
||||
service = OpenAIService()
|
||||
prompt = service.get_prompt("recipes.scrape-recipe")
|
||||
|
||||
response_json = await service.get_response(prompt, text, force_json_response=True)
|
||||
return (
|
||||
"<!DOCTYPE html><html><head>"
|
||||
f'<script type="application/ld+json">{response_json}</script>'
|
||||
"</head><body></body></html>"
|
||||
)
|
||||
except Exception:
|
||||
self.logger.exception(f"OpenAI was unable to extract a recipe from {url}")
|
||||
return ""
|
||||
|
||||
|
||||
class RecipeScraperOpenGraph(ABCScraperStrategy):
|
||||
async def get_html(self, url: str) -> str:
|
||||
return await safe_scrape_html(url)
|
||||
return self.raw_html or await safe_scrape_html(url)
|
||||
|
||||
def get_recipe_fields(self, html) -> dict | None:
|
||||
"""
|
||||
|
||||
160
poetry.lock
generated
@@ -1455,13 +1455,13 @@ pyyaml = ">=5.1"
|
||||
|
||||
[[package]]
|
||||
name = "mkdocs-material"
|
||||
version = "9.5.25"
|
||||
version = "9.5.27"
|
||||
description = "Documentation that simply works"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "mkdocs_material-9.5.25-py3-none-any.whl", hash = "sha256:68fdab047a0b9bfbefe79ce267e8a7daaf5128bcf7867065fcd201ee335fece1"},
|
||||
{file = "mkdocs_material-9.5.25.tar.gz", hash = "sha256:d0662561efb725b712207e0ee01f035ca15633f29a64628e24f01ec99d7078f4"},
|
||||
{file = "mkdocs_material-9.5.27-py3-none-any.whl", hash = "sha256:af8cc263fafa98bb79e9e15a8c966204abf15164987569bd1175fd66a7705182"},
|
||||
{file = "mkdocs_material-9.5.27.tar.gz", hash = "sha256:a7d4a35f6d4a62b0c43a0cfe7e987da0980c13587b5bc3c26e690ad494427ec0"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1583,13 +1583,13 @@ signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"]
|
||||
|
||||
[[package]]
|
||||
name = "openai"
|
||||
version = "1.31.0"
|
||||
version = "1.34.0"
|
||||
description = "The official Python library for the openai API"
|
||||
optional = false
|
||||
python-versions = ">=3.7.1"
|
||||
files = [
|
||||
{file = "openai-1.31.0-py3-none-any.whl", hash = "sha256:82044ee3122113f2a468a1f308a8882324d09556ba5348687c535d3655ee331c"},
|
||||
{file = "openai-1.31.0.tar.gz", hash = "sha256:54ae0625b005d6a3b895db2b8438dae1059cffff0cd262a26e9015c13a29ab06"},
|
||||
{file = "openai-1.34.0-py3-none-any.whl", hash = "sha256:018623c2f795424044675c6230fa3bfbf98d9e0aab45d8fd116f2efb2cfb6b7e"},
|
||||
{file = "openai-1.34.0.tar.gz", hash = "sha256:95c8e2da4acd6958e626186957d656597613587195abd0fb2527566a93e76770"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1606,57 +1606,57 @@ datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"]
|
||||
|
||||
[[package]]
|
||||
name = "orjson"
|
||||
version = "3.10.3"
|
||||
version = "3.10.5"
|
||||
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "orjson-3.10.3-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9fb6c3f9f5490a3eb4ddd46fc1b6eadb0d6fc16fb3f07320149c3286a1409dd8"},
|
||||
{file = "orjson-3.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:252124b198662eee80428f1af8c63f7ff077c88723fe206a25df8dc57a57b1fa"},
|
||||
{file = "orjson-3.10.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9f3e87733823089a338ef9bbf363ef4de45e5c599a9bf50a7a9b82e86d0228da"},
|
||||
{file = "orjson-3.10.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8334c0d87103bb9fbbe59b78129f1f40d1d1e8355bbed2ca71853af15fa4ed3"},
|
||||
{file = "orjson-3.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1952c03439e4dce23482ac846e7961f9d4ec62086eb98ae76d97bd41d72644d7"},
|
||||
{file = "orjson-3.10.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c0403ed9c706dcd2809f1600ed18f4aae50be263bd7112e54b50e2c2bc3ebd6d"},
|
||||
{file = "orjson-3.10.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:382e52aa4270a037d41f325e7d1dfa395b7de0c367800b6f337d8157367bf3a7"},
|
||||
{file = "orjson-3.10.3-cp310-none-win32.whl", hash = "sha256:be2aab54313752c04f2cbaab4515291ef5af8c2256ce22abc007f89f42f49109"},
|
||||
{file = "orjson-3.10.3-cp310-none-win_amd64.whl", hash = "sha256:416b195f78ae461601893f482287cee1e3059ec49b4f99479aedf22a20b1098b"},
|
||||
{file = "orjson-3.10.3-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:73100d9abbbe730331f2242c1fc0bcb46a3ea3b4ae3348847e5a141265479700"},
|
||||
{file = "orjson-3.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:544a12eee96e3ab828dbfcb4d5a0023aa971b27143a1d35dc214c176fdfb29b3"},
|
||||
{file = "orjson-3.10.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:520de5e2ef0b4ae546bea25129d6c7c74edb43fc6cf5213f511a927f2b28148b"},
|
||||
{file = "orjson-3.10.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ccaa0a401fc02e8828a5bedfd80f8cd389d24f65e5ca3954d72c6582495b4bcf"},
|
||||
{file = "orjson-3.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7bc9e8bc11bac40f905640acd41cbeaa87209e7e1f57ade386da658092dc16"},
|
||||
{file = "orjson-3.10.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3582b34b70543a1ed6944aca75e219e1192661a63da4d039d088a09c67543b08"},
|
||||
{file = "orjson-3.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c23dfa91481de880890d17aa7b91d586a4746a4c2aa9a145bebdbaf233768d5"},
|
||||
{file = "orjson-3.10.3-cp311-none-win32.whl", hash = "sha256:1770e2a0eae728b050705206d84eda8b074b65ee835e7f85c919f5705b006c9b"},
|
||||
{file = "orjson-3.10.3-cp311-none-win_amd64.whl", hash = "sha256:93433b3c1f852660eb5abdc1f4dd0ced2be031ba30900433223b28ee0140cde5"},
|
||||
{file = "orjson-3.10.3-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a39aa73e53bec8d410875683bfa3a8edf61e5a1c7bb4014f65f81d36467ea098"},
|
||||
{file = "orjson-3.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0943a96b3fa09bee1afdfccc2cb236c9c64715afa375b2af296c73d91c23eab2"},
|
||||
{file = "orjson-3.10.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e852baafceff8da3c9defae29414cc8513a1586ad93e45f27b89a639c68e8176"},
|
||||
{file = "orjson-3.10.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18566beb5acd76f3769c1d1a7ec06cdb81edc4d55d2765fb677e3eaa10fa99e0"},
|
||||
{file = "orjson-3.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bd2218d5a3aa43060efe649ec564ebedec8ce6ae0a43654b81376216d5ebd42"},
|
||||
{file = "orjson-3.10.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cf20465e74c6e17a104ecf01bf8cd3b7b252565b4ccee4548f18b012ff2f8069"},
|
||||
{file = "orjson-3.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba7f67aa7f983c4345eeda16054a4677289011a478ca947cd69c0a86ea45e534"},
|
||||
{file = "orjson-3.10.3-cp312-none-win32.whl", hash = "sha256:17e0713fc159abc261eea0f4feda611d32eabc35708b74bef6ad44f6c78d5ea0"},
|
||||
{file = "orjson-3.10.3-cp312-none-win_amd64.whl", hash = "sha256:4c895383b1ec42b017dd2c75ae8a5b862fc489006afde06f14afbdd0309b2af0"},
|
||||
{file = "orjson-3.10.3-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:be2719e5041e9fb76c8c2c06b9600fe8e8584e6980061ff88dcbc2691a16d20d"},
|
||||
{file = "orjson-3.10.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0175a5798bdc878956099f5c54b9837cb62cfbf5d0b86ba6d77e43861bcec2"},
|
||||
{file = "orjson-3.10.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:978be58a68ade24f1af7758626806e13cff7748a677faf95fbb298359aa1e20d"},
|
||||
{file = "orjson-3.10.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16bda83b5c61586f6f788333d3cf3ed19015e3b9019188c56983b5a299210eb5"},
|
||||
{file = "orjson-3.10.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ad1f26bea425041e0a1adad34630c4825a9e3adec49079b1fb6ac8d36f8b754"},
|
||||
{file = "orjson-3.10.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:9e253498bee561fe85d6325ba55ff2ff08fb5e7184cd6a4d7754133bd19c9195"},
|
||||
{file = "orjson-3.10.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0a62f9968bab8a676a164263e485f30a0b748255ee2f4ae49a0224be95f4532b"},
|
||||
{file = "orjson-3.10.3-cp38-none-win32.whl", hash = "sha256:8d0b84403d287d4bfa9bf7d1dc298d5c1c5d9f444f3737929a66f2fe4fb8f134"},
|
||||
{file = "orjson-3.10.3-cp38-none-win_amd64.whl", hash = "sha256:8bc7a4df90da5d535e18157220d7915780d07198b54f4de0110eca6b6c11e290"},
|
||||
{file = "orjson-3.10.3-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9059d15c30e675a58fdcd6f95465c1522b8426e092de9fff20edebfdc15e1cb0"},
|
||||
{file = "orjson-3.10.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d40c7f7938c9c2b934b297412c067936d0b54e4b8ab916fd1a9eb8f54c02294"},
|
||||
{file = "orjson-3.10.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d4a654ec1de8fdaae1d80d55cee65893cb06494e124681ab335218be6a0691e7"},
|
||||
{file = "orjson-3.10.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:831c6ef73f9aa53c5f40ae8f949ff7681b38eaddb6904aab89dca4d85099cb78"},
|
||||
{file = "orjson-3.10.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99b880d7e34542db89f48d14ddecbd26f06838b12427d5a25d71baceb5ba119d"},
|
||||
{file = "orjson-3.10.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2e5e176c994ce4bd434d7aafb9ecc893c15f347d3d2bbd8e7ce0b63071c52e25"},
|
||||
{file = "orjson-3.10.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b69a58a37dab856491bf2d3bbf259775fdce262b727f96aafbda359cb1d114d8"},
|
||||
{file = "orjson-3.10.3-cp39-none-win32.whl", hash = "sha256:b8d4d1a6868cde356f1402c8faeb50d62cee765a1f7ffcfd6de732ab0581e063"},
|
||||
{file = "orjson-3.10.3-cp39-none-win_amd64.whl", hash = "sha256:5102f50c5fc46d94f2033fe00d392588564378260d64377aec702f21a7a22912"},
|
||||
{file = "orjson-3.10.3.tar.gz", hash = "sha256:2b166507acae7ba2f7c315dcf185a9111ad5e992ac81f2d507aac39193c2c818"},
|
||||
{file = "orjson-3.10.5-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:545d493c1f560d5ccfc134803ceb8955a14c3fcb47bbb4b2fee0232646d0b932"},
|
||||
{file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4324929c2dd917598212bfd554757feca3e5e0fa60da08be11b4aa8b90013c1"},
|
||||
{file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c13ca5e2ddded0ce6a927ea5a9f27cae77eee4c75547b4297252cb20c4d30e6"},
|
||||
{file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6c8e30adfa52c025f042a87f450a6b9ea29649d828e0fec4858ed5e6caecf63"},
|
||||
{file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:338fd4f071b242f26e9ca802f443edc588fa4ab60bfa81f38beaedf42eda226c"},
|
||||
{file = "orjson-3.10.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6970ed7a3126cfed873c5d21ece1cd5d6f83ca6c9afb71bbae21a0b034588d96"},
|
||||
{file = "orjson-3.10.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:235dadefb793ad12f7fa11e98a480db1f7c6469ff9e3da5e73c7809c700d746b"},
|
||||
{file = "orjson-3.10.5-cp310-none-win32.whl", hash = "sha256:be79e2393679eda6a590638abda16d167754393f5d0850dcbca2d0c3735cebe2"},
|
||||
{file = "orjson-3.10.5-cp310-none-win_amd64.whl", hash = "sha256:c4a65310ccb5c9910c47b078ba78e2787cb3878cdded1702ac3d0da71ddc5228"},
|
||||
{file = "orjson-3.10.5-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:cdf7365063e80899ae3a697def1277c17a7df7ccfc979990a403dfe77bb54d40"},
|
||||
{file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b68742c469745d0e6ca5724506858f75e2f1e5b59a4315861f9e2b1df77775a"},
|
||||
{file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7d10cc1b594951522e35a3463da19e899abe6ca95f3c84c69e9e901e0bd93d38"},
|
||||
{file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcbe82b35d1ac43b0d84072408330fd3295c2896973112d495e7234f7e3da2e1"},
|
||||
{file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c0eb7e0c75e1e486c7563fe231b40fdd658a035ae125c6ba651ca3b07936f5"},
|
||||
{file = "orjson-3.10.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:53ed1c879b10de56f35daf06dbc4a0d9a5db98f6ee853c2dbd3ee9d13e6f302f"},
|
||||
{file = "orjson-3.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:099e81a5975237fda3100f918839af95f42f981447ba8f47adb7b6a3cdb078fa"},
|
||||
{file = "orjson-3.10.5-cp311-none-win32.whl", hash = "sha256:1146bf85ea37ac421594107195db8bc77104f74bc83e8ee21a2e58596bfb2f04"},
|
||||
{file = "orjson-3.10.5-cp311-none-win_amd64.whl", hash = "sha256:36a10f43c5f3a55c2f680efe07aa93ef4a342d2960dd2b1b7ea2dd764fe4a37c"},
|
||||
{file = "orjson-3.10.5-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:68f85ecae7af14a585a563ac741b0547a3f291de81cd1e20903e79f25170458f"},
|
||||
{file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28afa96f496474ce60d3340fe8d9a263aa93ea01201cd2bad844c45cd21f5268"},
|
||||
{file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cd684927af3e11b6e754df80b9ffafd9fb6adcaa9d3e8fdd5891be5a5cad51e"},
|
||||
{file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d21b9983da032505f7050795e98b5d9eee0df903258951566ecc358f6696969"},
|
||||
{file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ad1de7fef79736dde8c3554e75361ec351158a906d747bd901a52a5c9c8d24b"},
|
||||
{file = "orjson-3.10.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d97531cdfe9bdd76d492e69800afd97e5930cb0da6a825646667b2c6c6c0211"},
|
||||
{file = "orjson-3.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d69858c32f09c3e1ce44b617b3ebba1aba030e777000ebdf72b0d8e365d0b2b3"},
|
||||
{file = "orjson-3.10.5-cp312-none-win32.whl", hash = "sha256:64c9cc089f127e5875901ac05e5c25aa13cfa5dbbbd9602bda51e5c611d6e3e2"},
|
||||
{file = "orjson-3.10.5-cp312-none-win_amd64.whl", hash = "sha256:b2efbd67feff8c1f7728937c0d7f6ca8c25ec81373dc8db4ef394c1d93d13dc5"},
|
||||
{file = "orjson-3.10.5-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:03b565c3b93f5d6e001db48b747d31ea3819b89abf041ee10ac6988886d18e01"},
|
||||
{file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:584c902ec19ab7928fd5add1783c909094cc53f31ac7acfada817b0847975f26"},
|
||||
{file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a35455cc0b0b3a1eaf67224035f5388591ec72b9b6136d66b49a553ce9eb1e6"},
|
||||
{file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1670fe88b116c2745a3a30b0f099b699a02bb3482c2591514baf5433819e4f4d"},
|
||||
{file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:185c394ef45b18b9a7d8e8f333606e2e8194a50c6e3c664215aae8cf42c5385e"},
|
||||
{file = "orjson-3.10.5-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ca0b3a94ac8d3886c9581b9f9de3ce858263865fdaa383fbc31c310b9eac07c9"},
|
||||
{file = "orjson-3.10.5-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dfc91d4720d48e2a709e9c368d5125b4b5899dced34b5400c3837dadc7d6271b"},
|
||||
{file = "orjson-3.10.5-cp38-none-win32.whl", hash = "sha256:c05f16701ab2a4ca146d0bca950af254cb7c02f3c01fca8efbbad82d23b3d9d4"},
|
||||
{file = "orjson-3.10.5-cp38-none-win_amd64.whl", hash = "sha256:8a11d459338f96a9aa7f232ba95679fc0c7cedbd1b990d736467894210205c09"},
|
||||
{file = "orjson-3.10.5-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:85c89131d7b3218db1b24c4abecea92fd6c7f9fab87441cfc342d3acc725d807"},
|
||||
{file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb66215277a230c456f9038d5e2d84778141643207f85336ef8d2a9da26bd7ca"},
|
||||
{file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:51bbcdea96cdefa4a9b4461e690c75ad4e33796530d182bdd5c38980202c134a"},
|
||||
{file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbead71dbe65f959b7bd8cf91e0e11d5338033eba34c114f69078d59827ee139"},
|
||||
{file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5df58d206e78c40da118a8c14fc189207fffdcb1f21b3b4c9c0c18e839b5a214"},
|
||||
{file = "orjson-3.10.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c4057c3b511bb8aef605616bd3f1f002a697c7e4da6adf095ca5b84c0fd43595"},
|
||||
{file = "orjson-3.10.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b39e006b00c57125ab974362e740c14a0c6a66ff695bff44615dcf4a70ce2b86"},
|
||||
{file = "orjson-3.10.5-cp39-none-win32.whl", hash = "sha256:eded5138cc565a9d618e111c6d5c2547bbdd951114eb822f7f6309e04db0fb47"},
|
||||
{file = "orjson-3.10.5-cp39-none-win_amd64.whl", hash = "sha256:cc28e90a7cae7fcba2493953cff61da5a52950e78dc2dacfe931a317ee3d8de7"},
|
||||
{file = "orjson-3.10.5.tar.gz", hash = "sha256:7a5baef8a4284405d96c90c7c62b755e9ef1ada84c2406c24a9ebec86b89f46d"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2026,13 +2026,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.7.3"
|
||||
version = "2.7.4"
|
||||
description = "Data validation using Python type hints"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pydantic-2.7.3-py3-none-any.whl", hash = "sha256:ea91b002777bf643bb20dd717c028ec43216b24a6001a280f83877fd2655d0b4"},
|
||||
{file = "pydantic-2.7.3.tar.gz", hash = "sha256:c46c76a40bb1296728d7a8b99aa73dd70a48c3510111ff290034f860c99c419e"},
|
||||
{file = "pydantic-2.7.4-py3-none-any.whl", hash = "sha256:ee8538d41ccb9c0a9ad3e0e5f07bf15ed8015b481ced539a1759d8cc89ae90d0"},
|
||||
{file = "pydantic-2.7.4.tar.gz", hash = "sha256:0c84efd9548d545f63ac0060c1e4d39bb9b14db8b3c0652338aecc07b5adec52"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2136,13 +2136,13 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-settings"
|
||||
version = "2.3.0"
|
||||
version = "2.3.3"
|
||||
description = "Settings management using Pydantic"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pydantic_settings-2.3.0-py3-none-any.whl", hash = "sha256:26eeed27370a9c5e3f64e4a7d6602573cbedf05ed940f1d5b11c3f178427af7a"},
|
||||
{file = "pydantic_settings-2.3.0.tar.gz", hash = "sha256:78db28855a71503cfe47f39500a1dece523c640afd5280edb5c5c9c9cfa534c9"},
|
||||
{file = "pydantic_settings-2.3.3-py3-none-any.whl", hash = "sha256:e4ed62ad851670975ec11285141db888fd24947f9440bd4380d7d8788d4965de"},
|
||||
{file = "pydantic_settings-2.3.3.tar.gz", hash = "sha256:87fda838b64b5039b970cd47c3e8a1ee460ce136278ff672980af21516f6e6ce"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2215,13 +2215,13 @@ tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "pylint"
|
||||
version = "3.2.2"
|
||||
version = "3.2.3"
|
||||
description = "python code static checker"
|
||||
optional = false
|
||||
python-versions = ">=3.8.0"
|
||||
files = [
|
||||
{file = "pylint-3.2.2-py3-none-any.whl", hash = "sha256:3f8788ab20bb8383e06dd2233e50f8e08949cfd9574804564803441a4946eab4"},
|
||||
{file = "pylint-3.2.2.tar.gz", hash = "sha256:d068ca1dfd735fb92a07d33cb8f288adc0f6bc1287a139ca2425366f7cbe38f8"},
|
||||
{file = "pylint-3.2.3-py3-none-any.whl", hash = "sha256:b3d7d2708a3e04b4679e02d99e72329a8b7ee8afb8d04110682278781f889fa8"},
|
||||
{file = "pylint-3.2.3.tar.gz", hash = "sha256:02f6c562b215582386068d52a30f520d84fdbcf2a95fc7e855b816060d048b60"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2778,28 +2778,28 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.4.7"
|
||||
version = "0.4.9"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "ruff-0.4.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e089371c67892a73b6bb1525608e89a2aca1b77b5440acf7a71dda5dac958f9e"},
|
||||
{file = "ruff-0.4.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:10f973d521d910e5f9c72ab27e409e839089f955be8a4c8826601a6323a89753"},
|
||||
{file = "ruff-0.4.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59c3d110970001dfa494bcd95478e62286c751126dfb15c3c46e7915fc49694f"},
|
||||
{file = "ruff-0.4.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa9773c6c00f4958f73b317bc0fd125295110c3776089f6ef318f4b775f0abe4"},
|
||||
{file = "ruff-0.4.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07fc80bbb61e42b3b23b10fda6a2a0f5a067f810180a3760c5ef1b456c21b9db"},
|
||||
{file = "ruff-0.4.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:fa4dafe3fe66d90e2e2b63fa1591dd6e3f090ca2128daa0be33db894e6c18648"},
|
||||
{file = "ruff-0.4.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7c0083febdec17571455903b184a10026603a1de078428ba155e7ce9358c5f6"},
|
||||
{file = "ruff-0.4.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ad1b20e66a44057c326168437d680a2166c177c939346b19c0d6b08a62a37589"},
|
||||
{file = "ruff-0.4.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbf5d818553add7511c38b05532d94a407f499d1a76ebb0cad0374e32bc67202"},
|
||||
{file = "ruff-0.4.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:50e9651578b629baec3d1513b2534de0ac7ed7753e1382272b8d609997e27e83"},
|
||||
{file = "ruff-0.4.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8874a9df7766cb956b218a0a239e0a5d23d9e843e4da1e113ae1d27ee420877a"},
|
||||
{file = "ruff-0.4.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b9de9a6e49f7d529decd09381c0860c3f82fa0b0ea00ea78409b785d2308a567"},
|
||||
{file = "ruff-0.4.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:13a1768b0691619822ae6d446132dbdfd568b700ecd3652b20d4e8bc1e498f78"},
|
||||
{file = "ruff-0.4.7-py3-none-win32.whl", hash = "sha256:769e5a51df61e07e887b81e6f039e7ed3573316ab7dd9f635c5afaa310e4030e"},
|
||||
{file = "ruff-0.4.7-py3-none-win_amd64.whl", hash = "sha256:9e3ab684ad403a9ed1226894c32c3ab9c2e0718440f6f50c7c5829932bc9e054"},
|
||||
{file = "ruff-0.4.7-py3-none-win_arm64.whl", hash = "sha256:10f2204b9a613988e3484194c2c9e96a22079206b22b787605c255f130db5ed7"},
|
||||
{file = "ruff-0.4.7.tar.gz", hash = "sha256:2331d2b051dc77a289a653fcc6a42cce357087c5975738157cd966590b18b5e1"},
|
||||
{file = "ruff-0.4.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b262ed08d036ebe162123170b35703aaf9daffecb698cd367a8d585157732991"},
|
||||
{file = "ruff-0.4.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:98ec2775fd2d856dc405635e5ee4ff177920f2141b8e2d9eb5bd6efd50e80317"},
|
||||
{file = "ruff-0.4.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4555056049d46d8a381f746680db1c46e67ac3b00d714606304077682832998e"},
|
||||
{file = "ruff-0.4.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e91175fbe48f8a2174c9aad70438fe9cb0a5732c4159b2a10a3565fea2d94cde"},
|
||||
{file = "ruff-0.4.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e8e7b95673f22e0efd3571fb5b0cf71a5eaaa3cc8a776584f3b2cc878e46bff"},
|
||||
{file = "ruff-0.4.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2d45ddc6d82e1190ea737341326ecbc9a61447ba331b0a8962869fcada758505"},
|
||||
{file = "ruff-0.4.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:78de3fdb95c4af084087628132336772b1c5044f6e710739d440fc0bccf4d321"},
|
||||
{file = "ruff-0.4.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:06b60f91bfa5514bb689b500a25ba48e897d18fea14dce14b48a0c40d1635893"},
|
||||
{file = "ruff-0.4.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88bffe9c6a454bf8529f9ab9091c99490578a593cc9f9822b7fc065ee0712a06"},
|
||||
{file = "ruff-0.4.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:673bddb893f21ab47a8334c8e0ea7fd6598ecc8e698da75bcd12a7b9d0a3206e"},
|
||||
{file = "ruff-0.4.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8c1aff58c31948cc66d0b22951aa19edb5af0a3af40c936340cd32a8b1ab7438"},
|
||||
{file = "ruff-0.4.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:784d3ec9bd6493c3b720a0b76f741e6c2d7d44f6b2be87f5eef1ae8cc1d54c84"},
|
||||
{file = "ruff-0.4.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:732dd550bfa5d85af8c3c6cbc47ba5b67c6aed8a89e2f011b908fc88f87649db"},
|
||||
{file = "ruff-0.4.9-py3-none-win32.whl", hash = "sha256:8064590fd1a50dcf4909c268b0e7c2498253273309ad3d97e4a752bb9df4f521"},
|
||||
{file = "ruff-0.4.9-py3-none-win_amd64.whl", hash = "sha256:e0a22c4157e53d006530c902107c7f550b9233e9706313ab57b892d7197d8e52"},
|
||||
{file = "ruff-0.4.9-py3-none-win_arm64.whl", hash = "sha256:5d5460f789ccf4efd43f265a58538a2c24dbce15dbf560676e430375f20a8198"},
|
||||
{file = "ruff-0.4.9.tar.gz", hash = "sha256:f1cb0828ac9533ba0135d148d214e284711ede33640465e706772645483427e3"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -9,6 +9,7 @@ from pydantic import UUID4
|
||||
|
||||
from mealie.db.db_setup import session_context
|
||||
from mealie.repos.repository_factory import AllRepositories
|
||||
from mealie.schema.openai.recipe_ingredient import OpenAIIngredient, OpenAIIngredients
|
||||
from mealie.schema.recipe.recipe_ingredient import (
|
||||
CreateIngredientFood,
|
||||
CreateIngredientFoodAlias,
|
||||
@@ -24,8 +25,10 @@ from mealie.schema.recipe.recipe_ingredient import (
|
||||
from mealie.schema.user.user import GroupBase
|
||||
from mealie.services.openai import OpenAIService
|
||||
from mealie.services.parser_services import RegisteredParser, get_parser
|
||||
from mealie.services.parser_services.crfpp.processor import CRFIngredient, convert_list_to_crf_model
|
||||
from mealie.services.parser_services.openai.parser import OpenAIIngredient, OpenAIIngredients
|
||||
from mealie.services.parser_services.crfpp.processor import (
|
||||
CRFIngredient,
|
||||
convert_list_to_crf_model,
|
||||
)
|
||||
from tests.utils.factories import random_int, random_string
|
||||
|
||||
|
||||
|
||||