mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-02-24 08:43:11 -05:00
Compare commits
45 Commits
v3.11.0
...
feat/stand
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21261bcd9f | ||
|
|
6c08b1cba4 | ||
|
|
eebff3f481 | ||
|
|
89b1629d68 | ||
|
|
0380baedb1 | ||
|
|
74c73f051d | ||
|
|
0fc4ff9c75 | ||
|
|
2c5ab9e40e | ||
|
|
efab33ccc5 | ||
|
|
6b8b929483 | ||
|
|
96c056adfd | ||
|
|
212560c822 | ||
|
|
787fdf5d74 | ||
|
|
a48a9fa10c | ||
|
|
1a32bcc1fd | ||
|
|
ee482afbd2 | ||
|
|
48cdf27ea9 | ||
|
|
fedd1d9eb6 | ||
|
|
3a01925e48 | ||
|
|
3af9b05bd8 | ||
|
|
fe9dadefea | ||
|
|
122ef2d867 | ||
|
|
5edd95ed6d | ||
|
|
6cd7cdff77 | ||
|
|
be92363538 | ||
|
|
74a0671c70 | ||
|
|
e772bb6834 | ||
|
|
6bf80adca1 | ||
|
|
492492939e | ||
|
|
d9b7f0a3a1 | ||
|
|
16e2386f5a | ||
|
|
bbfa105e99 | ||
|
|
c94c9940b2 | ||
|
|
29c6176d89 | ||
|
|
0c0d7d11a5 | ||
|
|
e75fc6d391 | ||
|
|
f308869154 | ||
|
|
af30b8bdfa | ||
|
|
de4f22c3f6 | ||
|
|
4c55b282d6 | ||
|
|
8d2b2eb581 | ||
|
|
e9daac5fc4 | ||
|
|
ee1205cfdc | ||
|
|
a165b707af | ||
|
|
564385eb83 |
@@ -25,16 +25,9 @@ dotenv:
|
||||
- .env
|
||||
- .dev.env
|
||||
tasks:
|
||||
docs:gen:
|
||||
desc: runs the API documentation generator
|
||||
cmds:
|
||||
- uv run python dev/code-generation/gen_docs_api.py
|
||||
|
||||
docs:
|
||||
desc: runs the documentation server
|
||||
dir: docs
|
||||
deps:
|
||||
- docs:gen
|
||||
cmds:
|
||||
- uv run python -m mkdocs serve
|
||||
|
||||
@@ -81,7 +74,6 @@ tasks:
|
||||
desc: run code generators
|
||||
cmds:
|
||||
- uv run python dev/code-generation/main.py {{ .CLI_ARGS }}
|
||||
- task: docs:gen
|
||||
- task: py:format
|
||||
|
||||
dev:services:
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
import json
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
from mealie.app import app
|
||||
from mealie.core.config import determine_data_dir
|
||||
|
||||
DATA_DIR = determine_data_dir()
|
||||
|
||||
"""Script to export the ReDoc documentation page into a standalone HTML file."""
|
||||
|
||||
HTML_TEMPLATE = """<!-- Custom HTML site displayed as the Home chapter -->
|
||||
{% extends "main.html" %}
|
||||
{% block tabs %}
|
||||
{{ super() }}
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<div id="redoc-container"></div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js"> </script>
|
||||
<script>
|
||||
var spec = MY_SPECIFIC_TEXT;
|
||||
Redoc.init(spec, {}, document.getElementById("redoc-container"));
|
||||
</script>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
{% block content %}{% endblock %}
|
||||
{% block footer %}{% endblock %}
|
||||
"""
|
||||
|
||||
HTML_PATH = DATA_DIR.parent.parent.joinpath("docs/docs/overrides/api.html")
|
||||
CONSTANT_DT = datetime(2025, 10, 24, 15, 53, 0, 0, tzinfo=UTC)
|
||||
|
||||
|
||||
def normalize_timestamps(s: dict[str, Any]) -> dict[str, Any]:
|
||||
field_format = s.get("format")
|
||||
is_timestamp = field_format in ["date-time", "date", "time"]
|
||||
has_default = s.get("default")
|
||||
|
||||
if not is_timestamp:
|
||||
for k, v in s.items():
|
||||
if isinstance(v, dict):
|
||||
s[k] = normalize_timestamps(v)
|
||||
elif isinstance(v, list):
|
||||
s[k] = [normalize_timestamps(i) if isinstance(i, dict) else i for i in v]
|
||||
|
||||
return s
|
||||
elif not has_default:
|
||||
return s
|
||||
|
||||
if field_format == "date-time":
|
||||
s["default"] = CONSTANT_DT.isoformat()
|
||||
elif field_format == "date":
|
||||
s["default"] = CONSTANT_DT.date().isoformat()
|
||||
elif field_format == "time":
|
||||
s["default"] = CONSTANT_DT.time().isoformat()
|
||||
|
||||
return s
|
||||
|
||||
|
||||
def generate_api_docs(my_app: FastAPI):
|
||||
openapi_schema = my_app.openapi()
|
||||
openapi_schema = normalize_timestamps(openapi_schema)
|
||||
|
||||
with open(HTML_PATH, "w") as fd:
|
||||
text = HTML_TEMPLATE.replace("MY_SPECIFIC_TEXT", json.dumps(openapi_schema))
|
||||
fd.write(text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
generate_api_docs(app)
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
title: API
|
||||
template: api.html
|
||||
---
|
||||
@@ -124,16 +124,16 @@ For custom mapping variables (e.g. OPENAI_CUSTOM_HEADERS) you should pass values
|
||||
|
||||
| Variables | Default | Description |
|
||||
|---------------------------------------------------|:-------:|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| OPENAI_BASE_URL<super>[†][secrets]</super> | None | The base URL for the OpenAI API. If you're not sure, leave this empty to use the standard OpenAI platform |
|
||||
| OPENAI_API_KEY<super>[†][secrets]</super> | None | Your OpenAI API Key. Enables OpenAI-related features |
|
||||
| OPENAI_MODEL | gpt-4o | Which OpenAI model to use. If you're not sure, leave this empty |
|
||||
| OPENAI_CUSTOM_HEADERS | None | Custom HTTP headers to add to all OpenAI requests. This should generally be left empty unless your custom service requires them |
|
||||
| OPENAI_CUSTOM_PARAMS | None | Custom HTTP query params to add to all OpenAI requests. This should generally be left empty unless your custom service requires them |
|
||||
| OPENAI_ENABLE_IMAGE_SERVICES | True | Whether to enable OpenAI image services, such as creating recipes via image. Leave this enabled unless your custom model doesn't support it, or you want to reduce costs |
|
||||
| OPENAI_WORKERS | 2 | Number of OpenAI workers per request. Higher values may increase processing speed, but will incur additional API costs |
|
||||
| OPENAI_SEND_DATABASE_DATA | True | Whether to send Mealie data to OpenAI to improve request accuracy. This will incur additional API costs |
|
||||
| OPENAI_REQUEST_TIMEOUT | 300 | The number of seconds to wait for an OpenAI request to complete before cancelling the request. Leave this empty unless you're running into timeout issues on slower hardware |
|
||||
| OPENAI_CUSTOM_PROMPT_DIR | None | Path to custom prompt files. Only existing files in your custom directory will override the defaults; any missing or empty custom files will automatically fall back to the system defaults. See https://github.com/mealie-recipes/mealie/tree/mealie-next/mealie/services/openai/prompts for expected file names. |
|
||||
| OPENAI_BASE_URL<super>[†][secrets]</super> | None | The base URL for the OpenAI API. If you're not sure, leave this empty to use the standard OpenAI platform |
|
||||
| OPENAI_API_KEY<super>[†][secrets]</super> | None | Your OpenAI API Key. Enables OpenAI-related features |
|
||||
| OPENAI_MODEL | gpt-4o | Which OpenAI model to use. If you're not sure, leave this empty |
|
||||
| OPENAI_CUSTOM_HEADERS <br/> :octicons-tag-24: v2.0.0 | None | Custom HTTP headers to add to all OpenAI requests. This should generally be left empty unless your custom service requires them |
|
||||
| OPENAI_CUSTOM_PARAMS <br/> :octicons-tag-24: v2.0.0 | None | Custom HTTP query params to add to all OpenAI requests. This should generally be left empty unless your custom service requires them |
|
||||
| OPENAI_ENABLE_IMAGE_SERVICES <br/> :octicons-tag-24: v1.12.0 | True | Whether to enable OpenAI image services, such as creating recipes via image. Leave this enabled unless your custom model doesn't support it, or you want to reduce costs |
|
||||
| OPENAI_WORKERS | 2 | Number of OpenAI workers per request. Higher values may increase processing speed, but will incur additional API costs |
|
||||
| OPENAI_SEND_DATABASE_DATA | True | Whether to send Mealie data to OpenAI to improve request accuracy. This will incur additional API costs |
|
||||
| OPENAI_REQUEST_TIMEOUT | 300 | The number of seconds to wait for an OpenAI request to complete before cancelling the request. Leave this empty unless you're running into timeout issues on slower hardware |
|
||||
| OPENAI_CUSTOM_PROMPT_DIR <br/> :octicons-tag-24: v3.10.0 | None | Path to custom prompt files. Only existing files in your custom directory will override the defaults; any missing or empty custom files will automatically fall back to the system defaults. See https://github.com/mealie-recipes/mealie/tree/mealie-next/mealie/services/openai/prompts for expected file names. |
|
||||
|
||||
### Theming
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -93,7 +93,7 @@ nav:
|
||||
- iOS Shortcut: "documentation/community-guide/ios-shortcut.md"
|
||||
- Reverse Proxy (SWAG): "documentation/community-guide/swag.md"
|
||||
|
||||
- API Reference: "api/redoc.md"
|
||||
- API Reference: "https://demo.mealie.io/docs"
|
||||
|
||||
- Contributors Guide:
|
||||
- Non-Code: "contributors/non-coders.md"
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
:items="foods"
|
||||
:label="$t('shopping-list.food')"
|
||||
:icon="$globals.icons.foods"
|
||||
:autofocus="autoFocus === 'food'"
|
||||
create
|
||||
@create="createAssignFood"
|
||||
/>
|
||||
@@ -41,7 +42,7 @@
|
||||
:label="$t('shopping-list.note')"
|
||||
rows="1"
|
||||
auto-grow
|
||||
autofocus
|
||||
:autofocus="autoFocus === 'note'"
|
||||
@keypress="handleNoteKeyPress"
|
||||
/>
|
||||
</div>
|
||||
@@ -165,6 +166,8 @@ export default defineNuxtComponent({
|
||||
},
|
||||
);
|
||||
|
||||
const autoFocus = !listItem.value.food && listItem.value.note ? "note" : "food";
|
||||
|
||||
async function createAssignFood(val: string) {
|
||||
// keep UI reactive
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
@@ -204,6 +207,7 @@ export default defineNuxtComponent({
|
||||
|
||||
return {
|
||||
listItem,
|
||||
autoFocus,
|
||||
createAssignFood,
|
||||
createAssignUnit,
|
||||
assignLabelToFood,
|
||||
|
||||
@@ -1423,8 +1423,8 @@
|
||||
"is-greater-than-or-equal-to": "er større end eller lig med (Automatic Translation)",
|
||||
"is-less-than": "er mindre end (Automatic Translation)",
|
||||
"is-less-than-or-equal-to": "er mindre end eller lig med (Automatic Translation)",
|
||||
"is-older-than": "is older than",
|
||||
"is-newer-than": "is newer than"
|
||||
"is-older-than": "er ældre end",
|
||||
"is-newer-than": "er nyere end"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "er",
|
||||
@@ -1436,7 +1436,7 @@
|
||||
"is-not-like": "er ikke som"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "days ago|day ago|days ago"
|
||||
"days-ago": "dage siden|dag siden|dage siden"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -1436,7 +1436,7 @@
|
||||
"is-not-like": "ist nicht wie"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "days ago|day ago|days ago"
|
||||
"days-ago": "vor Tagen|vor Tag|vor Tagen"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -1134,7 +1134,21 @@
|
||||
"example-unit-singular": "ex: Tablespoon",
|
||||
"example-unit-plural": "ex: Tablespoons",
|
||||
"example-unit-abbreviation-singular": "ex: Tbsp",
|
||||
"example-unit-abbreviation-plural": "ex: Tbsps"
|
||||
"example-unit-abbreviation-plural": "ex: Tbsps",
|
||||
"standardization": "Standardization",
|
||||
"standard-unit": "Standard Unit",
|
||||
"standard-quantity": "Standard Quantity",
|
||||
"unit-conversion": "Unit Conversion",
|
||||
"standard-unit-labels": {
|
||||
"fluid-ounce": "fluid ounce",
|
||||
"cup": "cup",
|
||||
"ounce": "ounce",
|
||||
"pound": "pound",
|
||||
"milliliter": "milliliter",
|
||||
"liter": "liter",
|
||||
"gram": "gram",
|
||||
"kilogram": "kilogram"
|
||||
}
|
||||
},
|
||||
"labels": {
|
||||
"seed-dialog-text": "Seed the database with common labels based on your local language.",
|
||||
|
||||
@@ -370,7 +370,7 @@
|
||||
"applies-to-all-days": "S'applique à tous les jours",
|
||||
"applies-on-days": "S'applique les {0}s",
|
||||
"meal-plan-settings": "Paramètres des menus",
|
||||
"add-all-to-list": "Ajouter tout à la liste",
|
||||
"add-all-to-list": "Tout ajouter a une liste",
|
||||
"add-day-to-list": "Ajouter un jour à la liste"
|
||||
},
|
||||
"migration": {
|
||||
@@ -1423,8 +1423,8 @@
|
||||
"is-greater-than-or-equal-to": "est plus grand que ou égal à",
|
||||
"is-less-than": "est inférieur à",
|
||||
"is-less-than-or-equal-to": "est inférieur ou égal à",
|
||||
"is-older-than": "is older than",
|
||||
"is-newer-than": "is newer than"
|
||||
"is-older-than": "est plus ancien que",
|
||||
"is-newer-than": "est plus récent que"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "est",
|
||||
@@ -1436,7 +1436,7 @@
|
||||
"is-not-like": "n'est pas similaire à"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "days ago|day ago|days ago"
|
||||
"days-ago": "jours|jour|jours"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -1423,8 +1423,8 @@
|
||||
"is-greater-than-or-equal-to": "þann eða eftir þann",
|
||||
"is-less-than": "fyrir þann",
|
||||
"is-less-than-or-equal-to": "fyrir þann eða þann",
|
||||
"is-older-than": "is older than",
|
||||
"is-newer-than": "is newer than"
|
||||
"is-older-than": "er eldra en",
|
||||
"is-newer-than": "er nýrra en"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "er",
|
||||
@@ -1436,7 +1436,7 @@
|
||||
"is-not-like": "is not like"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "days ago|day ago|days ago"
|
||||
"days-ago": "daga gamalt|dags gamalt|daga gamalt"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"about": {
|
||||
"about": "약/대략",
|
||||
"about-mealie": "Mealie에 대해",
|
||||
"about-mealie": "Mealie 정보",
|
||||
"api-docs": "API 문서",
|
||||
"api-port": "API 포트",
|
||||
"application-mode": "애플리케이션 모드",
|
||||
@@ -299,8 +299,8 @@
|
||||
"private-household-description": "귀하의 가구를 비공개로 설정하면 모든 공개 보기 옵션이 비활성화됩니다. 이는 개별 공개 보기 설정을 재정의합니다.",
|
||||
"lock-recipe-edits-from-other-households": "다른 가구의 레시피 편집 잠금",
|
||||
"lock-recipe-edits-from-other-households-description": "이 기능을 활성화하면 귀하의 가족 구성원만 귀하의 가족이 만든 요리법을 편집할 수 있습니다.",
|
||||
"household-recipe-preferences": "가정용 레시피 선호도",
|
||||
"default-recipe-preferences-description": "이는 가정에서 새로운 레시피를 만들 때의 기본 설정입니다. 레시피 설정 메뉴에서 개별 레시피에 대해 이를 변경할 수 있습니다.",
|
||||
"household-recipe-preferences": "가구 레시피 설정",
|
||||
"default-recipe-preferences-description": "이는 가구에서 새로운 레시피를 만들 때의 기본 설정입니다. 레시피 설정 메뉴에서 개별 레시피에 대해 이를 변경할 수 있습니다.",
|
||||
"allow-users-outside-of-your-household-to-see-your-recipes": "가족 외의 사용자에게도 요리법을 볼 수 있도록 허용",
|
||||
"allow-users-outside-of-your-household-to-see-your-recipes-description": "활성화하면 공개 공유 링크를 사용하여 사용자에게 권한을 부여하지 않고도 특정 레시피를 공유할 수 있습니다. 비활성화하면 가족 구성원 또는 사전 생성된 비공개 링크로만 레시피를 공유할 수 있습니다.",
|
||||
"household-preferences": "가구 설정"
|
||||
@@ -1423,8 +1423,8 @@
|
||||
"is-greater-than-or-equal-to": "다음보다 크거나 같음",
|
||||
"is-less-than": "다음보다 작음",
|
||||
"is-less-than-or-equal-to": "다음보다 작거나 같음",
|
||||
"is-older-than": "is older than",
|
||||
"is-newer-than": "is newer than"
|
||||
"is-older-than": "~보다 오래됨",
|
||||
"is-newer-than": "~보다 최근임"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "같음",
|
||||
@@ -1436,7 +1436,7 @@
|
||||
"is-not-like": "다음과 같지 않음"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "days ago|day ago|days ago"
|
||||
"days-ago": "일 전|일 전|일 전"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -1423,8 +1423,8 @@
|
||||
"is-greater-than-or-equal-to": "is groter dan of gelijk aan",
|
||||
"is-less-than": "is kleiner dan",
|
||||
"is-less-than-or-equal-to": "is kleiner dan of gelijk aan",
|
||||
"is-older-than": "is older than",
|
||||
"is-newer-than": "is newer than"
|
||||
"is-older-than": "is ouder dan",
|
||||
"is-newer-than": "is nieuwer dan"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "is",
|
||||
@@ -1436,7 +1436,7 @@
|
||||
"is-not-like": "is niet zoals"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "days ago|day ago|days ago"
|
||||
"days-ago": "dagen geleden|dag geleden|dagen geleden"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -329,6 +329,8 @@ export interface IngredientUnit {
|
||||
pluralAbbreviation?: string | null;
|
||||
useAbbreviation?: boolean;
|
||||
aliases?: IngredientUnitAlias[];
|
||||
standardQuantity?: number | null;
|
||||
standardUnit?: string | null;
|
||||
createdAt?: string | null;
|
||||
updatedAt?: string | null;
|
||||
}
|
||||
@@ -348,6 +350,8 @@ export interface CreateIngredientUnit {
|
||||
pluralAbbreviation?: string | null;
|
||||
useAbbreviation?: boolean;
|
||||
aliases?: CreateIngredientUnitAlias[];
|
||||
standardQuantity?: number | null;
|
||||
standardUnit?: string | null;
|
||||
}
|
||||
export interface CreateIngredientUnitAlias {
|
||||
name: string;
|
||||
|
||||
@@ -58,3 +58,13 @@ export interface QueryFilterJSONPart {
|
||||
relationalOperator?: RelationalKeyword | RelationalOperator | null;
|
||||
value?: string | string[] | null;
|
||||
}
|
||||
|
||||
export type StandardizedUnitType
|
||||
= | "fluid_ounce"
|
||||
| "cup"
|
||||
| "ounce"
|
||||
| "pound"
|
||||
| "milliliter"
|
||||
| "liter"
|
||||
| "gram"
|
||||
| "kilogram";
|
||||
|
||||
@@ -85,6 +85,8 @@ export interface CreateIngredientUnit {
|
||||
pluralAbbreviation?: string | null;
|
||||
useAbbreviation?: boolean;
|
||||
aliases?: CreateIngredientUnitAlias[];
|
||||
standardQuantity?: number | null;
|
||||
standardUnit?: string | null;
|
||||
}
|
||||
export interface CreateIngredientUnitAlias {
|
||||
name: string;
|
||||
@@ -174,6 +176,8 @@ export interface IngredientUnit {
|
||||
pluralAbbreviation?: string | null;
|
||||
useAbbreviation?: boolean;
|
||||
aliases?: IngredientUnitAlias[];
|
||||
standardQuantity?: number | null;
|
||||
standardUnit?: string | null;
|
||||
createdAt?: string | null;
|
||||
updatedAt?: string | null;
|
||||
}
|
||||
@@ -498,6 +502,8 @@ export interface SaveIngredientUnit {
|
||||
pluralAbbreviation?: string | null;
|
||||
useAbbreviation?: boolean;
|
||||
aliases?: CreateIngredientUnitAlias[];
|
||||
standardQuantity?: number | null;
|
||||
standardUnit?: string | null;
|
||||
groupId: string;
|
||||
}
|
||||
export interface ScrapeRecipe {
|
||||
|
||||
@@ -219,7 +219,7 @@ export default defineNuxtConfig({
|
||||
},
|
||||
workbox: {
|
||||
navigateFallback: "/",
|
||||
navigateFallbackAllowlist: [/^(?!\/api|\/docs|\/redoc)/],
|
||||
navigateFallbackAllowlist: [/^(?!\/api|\/docs)/],
|
||||
globPatterns: ["**/*.{js,css,html,png,svg,ico}"],
|
||||
globIgnores: ["404.html", "200.html"],
|
||||
cleanupOutdatedCaches: true,
|
||||
|
||||
@@ -88,6 +88,34 @@
|
||||
hide-details
|
||||
:label="$t('data-pages.units.use-abbreviation')"
|
||||
/>
|
||||
<v-divider />
|
||||
<v-card-text class="text-h6 mt-2 mb-3 pa-0">
|
||||
{{ $t('data-pages.units.standardization') }}
|
||||
</v-card-text>
|
||||
<v-card-text class="ma-0 pa-0">
|
||||
{{ $t('data-pages.units.unit-conversion') }}
|
||||
</v-card-text>
|
||||
<div class="d-flex flex-nowrap">
|
||||
<v-number-input
|
||||
v-model="createTarget.standardQuantity"
|
||||
variant="underlined"
|
||||
control-variant="hidden"
|
||||
density="compact"
|
||||
inset
|
||||
:min="0"
|
||||
:precision="null"
|
||||
hide-details
|
||||
class="mt-2"
|
||||
style="max-width: 125px;"
|
||||
/>
|
||||
<v-autocomplete
|
||||
v-model="createTarget.standardUnit"
|
||||
:items="standardUnitItems"
|
||||
clearable
|
||||
hide-details
|
||||
class="ml-2"
|
||||
/>
|
||||
</div>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
@@ -149,6 +177,34 @@
|
||||
hide-details
|
||||
:label="$t('data-pages.units.use-abbreviation')"
|
||||
/>
|
||||
<v-divider />
|
||||
<v-card-text class="text-h6 mt-2 mb-3 pa-0">
|
||||
{{ $t('data-pages.units.standardization') }}
|
||||
</v-card-text>
|
||||
<v-card-text class="ma-0 pa-0">
|
||||
{{ $t('data-pages.units.unit-conversion') }}
|
||||
</v-card-text>
|
||||
<div class="d-flex flex-nowrap">
|
||||
<v-number-input
|
||||
v-model="editTarget.standardQuantity"
|
||||
variant="underlined"
|
||||
control-variant="hidden"
|
||||
density="compact"
|
||||
inset
|
||||
:min="0"
|
||||
:precision="null"
|
||||
hide-details
|
||||
class="mt-2"
|
||||
style="max-width: 125px;"
|
||||
/>
|
||||
<v-autocomplete
|
||||
v-model="editTarget.standardUnit"
|
||||
:items="standardUnitItems"
|
||||
clearable
|
||||
hide-details
|
||||
class="ml-2"
|
||||
/>
|
||||
</div>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<template #custom-card-action>
|
||||
@@ -314,11 +370,17 @@ import RecipeDataAliasManagerDialog from "~/components/Domain/Recipe/RecipeDataA
|
||||
import { validators } from "~/composables/use-validators";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import type { CreateIngredientUnit, IngredientUnit, IngredientUnitAlias } from "~/lib/api/types/recipe";
|
||||
import type { StandardizedUnitType } from "~/lib/api/types/non-generated";
|
||||
import { useLocales } from "~/composables/use-locales";
|
||||
import { normalizeFilter } from "~/composables/use-utils";
|
||||
import { useUnitStore } from "~/composables/store";
|
||||
import type { VForm } from "~/types/auto-forms";
|
||||
|
||||
interface StandardUnitItem {
|
||||
title: string;
|
||||
value: StandardizedUnitType;
|
||||
};
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: { RecipeDataAliasManagerDialog },
|
||||
setup() {
|
||||
@@ -376,6 +438,16 @@ export default defineNuxtComponent({
|
||||
show: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
text: i18n.t("data-pages.units.standard-quantity"),
|
||||
value: "standardQuantity",
|
||||
show: false,
|
||||
},
|
||||
{
|
||||
text: i18n.t("data-pages.units.standard-unit"),
|
||||
value: "standardUnit",
|
||||
show: false,
|
||||
},
|
||||
{
|
||||
text: i18n.t("general.date-added"),
|
||||
value: "createdAt",
|
||||
@@ -384,6 +456,41 @@ export default defineNuxtComponent({
|
||||
},
|
||||
];
|
||||
|
||||
const standardUnitItems = computed<StandardUnitItem[]>(() => [
|
||||
{
|
||||
title: i18n.t("data-pages.units.standard-unit-labels.fluid-ounce"),
|
||||
value: "fluid_ounce",
|
||||
},
|
||||
{
|
||||
title: i18n.t("data-pages.units.standard-unit-labels.cup"),
|
||||
value: "cup",
|
||||
},
|
||||
{
|
||||
title: i18n.t("data-pages.units.standard-unit-labels.ounce"),
|
||||
value: "ounce",
|
||||
},
|
||||
{
|
||||
title: i18n.t("data-pages.units.standard-unit-labels.pound"),
|
||||
value: "pound",
|
||||
},
|
||||
{
|
||||
title: i18n.t("data-pages.units.standard-unit-labels.milliliter"),
|
||||
value: "milliliter",
|
||||
},
|
||||
{
|
||||
title: i18n.t("data-pages.units.standard-unit-labels.liter"),
|
||||
value: "liter",
|
||||
},
|
||||
{
|
||||
title: i18n.t("data-pages.units.standard-unit-labels.gram"),
|
||||
value: "gram",
|
||||
},
|
||||
{
|
||||
title: i18n.t("data-pages.units.standard-unit-labels.kilogram"),
|
||||
value: "kilogram",
|
||||
},
|
||||
]);
|
||||
|
||||
const { store, actions: unitActions } = useUnitStore();
|
||||
|
||||
// ============================================================
|
||||
@@ -536,6 +643,7 @@ export default defineNuxtComponent({
|
||||
return {
|
||||
tableConfig,
|
||||
tableHeaders,
|
||||
standardUnitItems,
|
||||
store,
|
||||
validators,
|
||||
normalizeFilter,
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
"""add unit standardization fields
|
||||
|
||||
Revision ID: a39c7f1826e3
|
||||
Revises: 1d9a002d7234
|
||||
Create Date: 2026-02-21 17:59:01.161812
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import orm
|
||||
from alembic import op
|
||||
from mealie.repos.repository_units import RepositoryUnit
|
||||
from mealie.core.root_logger import get_logger
|
||||
from mealie.db.models._model_utils.guid import GUID
|
||||
from mealie.repos.seed.seeders import IngredientUnitsSeeder
|
||||
from mealie.lang.locale_config import LOCALE_CONFIG
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "a39c7f1826e3"
|
||||
down_revision: str | None = "1d9a002d7234"
|
||||
branch_labels: str | tuple[str, ...] | None = None
|
||||
depends_on: str | tuple[str, ...] | None = None
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
class SqlAlchemyBase(orm.DeclarativeBase): ...
|
||||
|
||||
|
||||
class IngredientUnitModel(SqlAlchemyBase):
|
||||
__tablename__ = "ingredient_units"
|
||||
|
||||
id: orm.Mapped[GUID] = orm.mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
name: orm.Mapped[str | None] = orm.mapped_column(sa.String)
|
||||
plural_name: orm.Mapped[str | None] = orm.mapped_column(sa.String)
|
||||
abbreviation: orm.Mapped[str | None] = orm.mapped_column(sa.String)
|
||||
plural_abbreviation: orm.Mapped[str | None] = orm.mapped_column(sa.String)
|
||||
standard_quantity: orm.Mapped[float | None] = orm.mapped_column(sa.Float)
|
||||
standard_unit: orm.Mapped[str | None] = orm.mapped_column(sa.String)
|
||||
|
||||
|
||||
def populate_standards() -> None:
|
||||
bind = op.get_bind()
|
||||
|
||||
session = orm.Session(bind)
|
||||
|
||||
# We aren't using most of the functionality of this class, so we pass dummy args
|
||||
repo = RepositoryUnit(None, None, None, None, group_id=None) # type: ignore
|
||||
|
||||
stmt = sa.select(IngredientUnitModel)
|
||||
units = session.execute(stmt).scalars().all()
|
||||
if not units:
|
||||
return
|
||||
|
||||
# Manually build repo._standardized_unit_map with all locales
|
||||
repo._standardized_unit_map = {}
|
||||
for locale in LOCALE_CONFIG:
|
||||
locale_file = IngredientUnitsSeeder.get_file(locale)
|
||||
for unit_key, unit in IngredientUnitsSeeder.load_file(locale_file).items():
|
||||
for prop in ["name", "plural_name", "abbreviation"]:
|
||||
val = unit.get(prop)
|
||||
if val and isinstance(val, str):
|
||||
repo._standardized_unit_map[val.strip().lower()] = unit_key
|
||||
|
||||
for unit in units:
|
||||
unit_data = {
|
||||
"name": unit.name,
|
||||
"plural_name": unit.plural_name,
|
||||
"abbreviation": unit.abbreviation,
|
||||
"plural_abbreviation": unit.plural_abbreviation,
|
||||
}
|
||||
|
||||
standardized_data = repo._add_standardized_unit(unit_data)
|
||||
std_q = standardized_data.get("standard_quantity")
|
||||
std_u = standardized_data.get("standard_unit")
|
||||
if std_q and std_u:
|
||||
logger.info(f"Found unit '{unit.name}', which is standardized as '{std_q} * {std_u}'")
|
||||
unit.standard_quantity = std_q
|
||||
unit.standard_unit = std_u
|
||||
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("ingredient_units", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("standard_quantity", sa.Float(), nullable=True))
|
||||
batch_op.add_column(sa.Column("standard_unit", sa.String(), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
# Populate standardized units for existing records
|
||||
try:
|
||||
populate_standards()
|
||||
except Exception:
|
||||
logger.exception("Failed to populate unit standards, skipping...")
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("ingredient_units", schema=None) as batch_op:
|
||||
batch_op.drop_column("standard_unit")
|
||||
batch_op.drop_column("standard_quantity")
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@@ -102,7 +102,6 @@ app = FastAPI(
|
||||
description=description,
|
||||
version=APP_VERSION,
|
||||
docs_url=settings.DOCS_URL,
|
||||
redoc_url=settings.REDOC_URL,
|
||||
lifespan=lifespan_fn,
|
||||
)
|
||||
|
||||
|
||||
@@ -199,10 +199,6 @@ class AppSettings(AppLoggingSettings):
|
||||
def DOCS_URL(self) -> str | None:
|
||||
return "/docs" if self.API_DOCS else None
|
||||
|
||||
@property
|
||||
def REDOC_URL(self) -> str | None:
|
||||
return "/redoc" if self.API_DOCS else None
|
||||
|
||||
# ===============================================
|
||||
# Database Configuration
|
||||
|
||||
|
||||
@@ -52,6 +52,10 @@ class IngredientUnitModel(SqlAlchemyBase, BaseMixins):
|
||||
cascade="all, delete, delete-orphan",
|
||||
)
|
||||
|
||||
# Standardization
|
||||
standard_quantity: Mapped[float | None] = mapped_column(Float)
|
||||
standard_unit: Mapped[str | None] = mapped_column(String)
|
||||
|
||||
# Automatically updated by sqlalchemy event, do not write to this manually
|
||||
name_normalized: Mapped[str | None] = mapped_column(sa.String, index=True)
|
||||
plural_name_normalized: Mapped[str | None] = mapped_column(sa.String, index=True)
|
||||
|
||||
@@ -15,52 +15,63 @@ class LocalePluralFoodHandling(StrEnum):
|
||||
|
||||
@dataclass
|
||||
class LocaleConfig:
|
||||
key: str
|
||||
name: str
|
||||
dir: LocaleTextDirection = LocaleTextDirection.LTR
|
||||
plural_food_handling: LocalePluralFoodHandling = LocalePluralFoodHandling.ALWAYS
|
||||
|
||||
|
||||
LOCALE_CONFIG: dict[str, LocaleConfig] = {
|
||||
"af-ZA": LocaleConfig(name="Afrikaans (Afrikaans)"),
|
||||
"ar-SA": LocaleConfig(name="العربية (Arabic)", dir=LocaleTextDirection.RTL),
|
||||
"bg-BG": LocaleConfig(name="Български (Bulgarian)"),
|
||||
"ca-ES": LocaleConfig(name="Català (Catalan)"),
|
||||
"cs-CZ": LocaleConfig(name="Čeština (Czech)"),
|
||||
"da-DK": LocaleConfig(name="Dansk (Danish)"),
|
||||
"de-DE": LocaleConfig(name="Deutsch (German)"),
|
||||
"el-GR": LocaleConfig(name="Ελληνικά (Greek)"),
|
||||
"en-GB": LocaleConfig(name="British English", plural_food_handling=LocalePluralFoodHandling.WITHOUT_UNIT),
|
||||
"en-US": LocaleConfig(name="American English", plural_food_handling=LocalePluralFoodHandling.WITHOUT_UNIT),
|
||||
"es-ES": LocaleConfig(name="Español (Spanish)"),
|
||||
"et-EE": LocaleConfig(name="Eesti (Estonian)"),
|
||||
"fi-FI": LocaleConfig(name="Suomi (Finnish)"),
|
||||
"fr-BE": LocaleConfig(name="Belge (Belgian)"),
|
||||
"fr-CA": LocaleConfig(name="Français canadien (Canadian French)"),
|
||||
"fr-FR": LocaleConfig(name="Français (French)"),
|
||||
"gl-ES": LocaleConfig(name="Galego (Galician)"),
|
||||
"he-IL": LocaleConfig(name="עברית (Hebrew)", dir=LocaleTextDirection.RTL),
|
||||
"hr-HR": LocaleConfig(name="Hrvatski (Croatian)"),
|
||||
"hu-HU": LocaleConfig(name="Magyar (Hungarian)"),
|
||||
"is-IS": LocaleConfig(name="Íslenska (Icelandic)"),
|
||||
"it-IT": LocaleConfig(name="Italiano (Italian)"),
|
||||
"ja-JP": LocaleConfig(name="日本語 (Japanese)", plural_food_handling=LocalePluralFoodHandling.NEVER),
|
||||
"ko-KR": LocaleConfig(name="한국어 (Korean)", plural_food_handling=LocalePluralFoodHandling.NEVER),
|
||||
"lt-LT": LocaleConfig(name="Lietuvių (Lithuanian)"),
|
||||
"lv-LV": LocaleConfig(name="Latviešu (Latvian)"),
|
||||
"nl-NL": LocaleConfig(name="Nederlands (Dutch)"),
|
||||
"no-NO": LocaleConfig(name="Norsk (Norwegian)"),
|
||||
"pl-PL": LocaleConfig(name="Polski (Polish)"),
|
||||
"pt-BR": LocaleConfig(name="Português do Brasil (Brazilian Portuguese)"),
|
||||
"pt-PT": LocaleConfig(name="Português (Portuguese)"),
|
||||
"ro-RO": LocaleConfig(name="Română (Romanian)"),
|
||||
"ru-RU": LocaleConfig(name="Pусский (Russian)"),
|
||||
"sk-SK": LocaleConfig(name="Slovenčina (Slovak)"),
|
||||
"sl-SI": LocaleConfig(name="Slovenščina (Slovenian)"),
|
||||
"sr-SP": LocaleConfig(name="српски (Serbian)"),
|
||||
"sv-SE": LocaleConfig(name="Svenska (Swedish)"),
|
||||
"tr-TR": LocaleConfig(name="Türkçe (Turkish)", plural_food_handling=LocalePluralFoodHandling.NEVER),
|
||||
"uk-UA": LocaleConfig(name="Українська (Ukrainian)"),
|
||||
"vi-VN": LocaleConfig(name="Tiếng Việt (Vietnamese)", plural_food_handling=LocalePluralFoodHandling.NEVER),
|
||||
"zh-CN": LocaleConfig(name="简体中文 (Chinese simplified)", plural_food_handling=LocalePluralFoodHandling.NEVER),
|
||||
"zh-TW": LocaleConfig(name="繁體中文 (Chinese traditional)", plural_food_handling=LocalePluralFoodHandling.NEVER),
|
||||
"af-ZA": LocaleConfig(key="af-ZA", name="Afrikaans (Afrikaans)"),
|
||||
"ar-SA": LocaleConfig(key="ar-SA", name="العربية (Arabic)", dir=LocaleTextDirection.RTL),
|
||||
"bg-BG": LocaleConfig(key="bg-BG", name="Български (Bulgarian)"),
|
||||
"ca-ES": LocaleConfig(key="ca-ES", name="Català (Catalan)"),
|
||||
"cs-CZ": LocaleConfig(key="cs-CZ", name="Čeština (Czech)"),
|
||||
"da-DK": LocaleConfig(key="da-DK", name="Dansk (Danish)"),
|
||||
"de-DE": LocaleConfig(key="de-DE", name="Deutsch (German)"),
|
||||
"el-GR": LocaleConfig(key="el-GR", name="Ελληνικά (Greek)"),
|
||||
"en-GB": LocaleConfig(
|
||||
key="en-GB", name="British English", plural_food_handling=LocalePluralFoodHandling.WITHOUT_UNIT
|
||||
),
|
||||
"en-US": LocaleConfig(
|
||||
key="en-US", name="American English", plural_food_handling=LocalePluralFoodHandling.WITHOUT_UNIT
|
||||
),
|
||||
"es-ES": LocaleConfig(key="es-ES", name="Español (Spanish)"),
|
||||
"et-EE": LocaleConfig(key="et-EE", name="Eesti (Estonian)"),
|
||||
"fi-FI": LocaleConfig(key="fi-FI", name="Suomi (Finnish)"),
|
||||
"fr-BE": LocaleConfig(key="fr-BE", name="Belge (Belgian)"),
|
||||
"fr-CA": LocaleConfig(key="fr-CA", name="Français canadien (Canadian French)"),
|
||||
"fr-FR": LocaleConfig(key="fr-FR", name="Français (French)"),
|
||||
"gl-ES": LocaleConfig(key="gl-ES", name="Galego (Galician)"),
|
||||
"he-IL": LocaleConfig(key="he-IL", name="עברית (Hebrew)", dir=LocaleTextDirection.RTL),
|
||||
"hr-HR": LocaleConfig(key="hr-HR", name="Hrvatski (Croatian)"),
|
||||
"hu-HU": LocaleConfig(key="hu-HU", name="Magyar (Hungarian)"),
|
||||
"is-IS": LocaleConfig(key="is-IS", name="Íslenska (Icelandic)"),
|
||||
"it-IT": LocaleConfig(key="it-IT", name="Italiano (Italian)"),
|
||||
"ja-JP": LocaleConfig(key="ja-JP", name="日本語 (Japanese)", plural_food_handling=LocalePluralFoodHandling.NEVER),
|
||||
"ko-KR": LocaleConfig(key="ko-KR", name="한국어 (Korean)", plural_food_handling=LocalePluralFoodHandling.NEVER),
|
||||
"lt-LT": LocaleConfig(key="lt-LT", name="Lietuvių (Lithuanian)"),
|
||||
"lv-LV": LocaleConfig(key="lv-LV", name="Latviešu (Latvian)"),
|
||||
"nl-NL": LocaleConfig(key="nl-NL", name="Nederlands (Dutch)"),
|
||||
"no-NO": LocaleConfig(key="no-NO", name="Norsk (Norwegian)"),
|
||||
"pl-PL": LocaleConfig(key="pl-PL", name="Polski (Polish)"),
|
||||
"pt-BR": LocaleConfig(key="pt-BR", name="Português do Brasil (Brazilian Portuguese)"),
|
||||
"pt-PT": LocaleConfig(key="pt-PT", name="Português (Portuguese)"),
|
||||
"ro-RO": LocaleConfig(key="ro-RO", name="Română (Romanian)"),
|
||||
"ru-RU": LocaleConfig(key="ru-RU", name="Pусский (Russian)"),
|
||||
"sk-SK": LocaleConfig(key="sk-SK", name="Slovenčina (Slovak)"),
|
||||
"sl-SI": LocaleConfig(key="sl-SI", name="Slovenščina (Slovenian)"),
|
||||
"sr-SP": LocaleConfig(key="sr-SP", name="српски (Serbian)"),
|
||||
"sv-SE": LocaleConfig(key="sv-SE", name="Svenska (Swedish)"),
|
||||
"tr-TR": LocaleConfig(key="tr-TR", name="Türkçe (Turkish)", plural_food_handling=LocalePluralFoodHandling.NEVER),
|
||||
"uk-UA": LocaleConfig(key="uk-UA", name="Українська (Ukrainian)"),
|
||||
"vi-VN": LocaleConfig(
|
||||
key="vi-VN", name="Tiếng Việt (Vietnamese)", plural_food_handling=LocalePluralFoodHandling.NEVER
|
||||
),
|
||||
"zh-CN": LocaleConfig(
|
||||
key="zh-CN", name="简体中文 (Chinese simplified)", plural_food_handling=LocalePluralFoodHandling.NEVER
|
||||
),
|
||||
"zh-TW": LocaleConfig(
|
||||
key="zh-TW", name="繁體中文 (Chinese traditional)", plural_food_handling=LocalePluralFoodHandling.NEVER
|
||||
),
|
||||
}
|
||||
|
||||
@@ -1,17 +1,119 @@
|
||||
from pydantic import UUID4
|
||||
from collections.abc import Iterable
|
||||
|
||||
from pydantic import UUID4, BaseModel
|
||||
from sqlalchemy import select
|
||||
|
||||
from mealie.db.models.recipe.ingredient import IngredientUnitModel
|
||||
from mealie.schema.recipe.recipe_ingredient import IngredientUnit
|
||||
from mealie.lang.providers import get_locale_context
|
||||
from mealie.schema.recipe.recipe_ingredient import IngredientUnit, StandardizedUnitType
|
||||
|
||||
from .repository_generic import GroupRepositoryGeneric
|
||||
|
||||
|
||||
class RepositoryUnit(GroupRepositoryGeneric[IngredientUnit, IngredientUnitModel]):
|
||||
_standardized_unit_map: dict[str, str] | None = None
|
||||
|
||||
@property
|
||||
def standardized_unit_map(self) -> dict[str, str]:
|
||||
"""A map of potential known units to its standardized name in our seed data"""
|
||||
|
||||
if self._standardized_unit_map is None:
|
||||
from .seed.seeders import IngredientUnitsSeeder
|
||||
|
||||
ctx = get_locale_context()
|
||||
if ctx:
|
||||
locale = ctx[1].key
|
||||
else:
|
||||
locale = None
|
||||
|
||||
self._standardized_unit_map = {}
|
||||
locale_file = IngredientUnitsSeeder.get_file(locale=locale)
|
||||
for unit_key, unit in IngredientUnitsSeeder.load_file(locale_file).items():
|
||||
for prop in ["name", "plural_name", "abbreviation"]:
|
||||
val = unit.get(prop)
|
||||
if val and isinstance(val, str):
|
||||
self._standardized_unit_map[val.strip().lower()] = unit_key
|
||||
|
||||
return self._standardized_unit_map
|
||||
|
||||
def _get_unit(self, id: UUID4) -> IngredientUnitModel:
|
||||
stmt = select(self.model).filter_by(**self._filter_builder(**{"id": id}))
|
||||
return self.session.execute(stmt).scalars().one()
|
||||
|
||||
def _add_standardized_unit(self, data: BaseModel | dict) -> dict:
|
||||
if not isinstance(data, dict):
|
||||
data = data.model_dump()
|
||||
|
||||
# Don't overwrite user data if it exists
|
||||
if data.get("standard_quantity") is not None or data.get("standard_unit") is not None:
|
||||
return data
|
||||
|
||||
# Compare name attrs to translation files and see if there's a match to a known standard unit
|
||||
for prop in ["name", "plural_name", "abbreviation", "plural_abbreviation"]:
|
||||
val = data.get(prop)
|
||||
if not (val and isinstance(val, str)):
|
||||
continue
|
||||
|
||||
standardized_unit_key = self.standardized_unit_map.get(val.strip().lower())
|
||||
if not standardized_unit_key:
|
||||
continue
|
||||
|
||||
match standardized_unit_key:
|
||||
case "teaspoon":
|
||||
data["standard_quantity"] = 1 / 6
|
||||
data["standard_unit"] = StandardizedUnitType.FLUID_OUNCE
|
||||
case "tablespoon":
|
||||
data["standard_quantity"] = 1 / 2
|
||||
data["standard_unit"] = StandardizedUnitType.FLUID_OUNCE
|
||||
case "cup":
|
||||
data["standard_quantity"] = 1
|
||||
data["standard_unit"] = StandardizedUnitType.CUP
|
||||
case "fluid-ounce":
|
||||
data["standard_quantity"] = 1
|
||||
data["standard_unit"] = StandardizedUnitType.FLUID_OUNCE
|
||||
case "pint":
|
||||
data["standard_quantity"] = 2
|
||||
data["standard_unit"] = StandardizedUnitType.CUP
|
||||
case "quart":
|
||||
data["standard_quantity"] = 4
|
||||
data["standard_unit"] = StandardizedUnitType.CUP
|
||||
case "gallon":
|
||||
data["standard_quantity"] = 16
|
||||
data["standard_unit"] = StandardizedUnitType.CUP
|
||||
case "milliliter":
|
||||
data["standard_quantity"] = 1
|
||||
data["standard_unit"] = StandardizedUnitType.MILLILITER
|
||||
case "liter":
|
||||
data["standard_quantity"] = 1
|
||||
data["standard_unit"] = StandardizedUnitType.LITER
|
||||
case "pound":
|
||||
data["standard_quantity"] = 1
|
||||
data["standard_unit"] = StandardizedUnitType.POUND
|
||||
case "ounce":
|
||||
data["standard_quantity"] = 1
|
||||
data["standard_unit"] = StandardizedUnitType.OUNCE
|
||||
case "gram":
|
||||
data["standard_quantity"] = 1
|
||||
data["standard_unit"] = StandardizedUnitType.GRAM
|
||||
case "kilogram":
|
||||
data["standard_quantity"] = 1
|
||||
data["standard_unit"] = StandardizedUnitType.KILOGRAM
|
||||
case "milligram":
|
||||
data["standard_quantity"] = 1 / 1000
|
||||
data["standard_unit"] = StandardizedUnitType.GRAM
|
||||
case _:
|
||||
continue
|
||||
|
||||
return data
|
||||
|
||||
def create(self, data: IngredientUnit | dict) -> IngredientUnit:
|
||||
data = self._add_standardized_unit(data)
|
||||
return super().create(data)
|
||||
|
||||
def create_many(self, data: Iterable[IngredientUnit | dict]) -> list[IngredientUnit]:
|
||||
data = [self._add_standardized_unit(i) for i in data]
|
||||
return super().create_many(data)
|
||||
|
||||
def merge(self, from_unit: UUID4, to_unit: UUID4) -> IngredientUnit | None:
|
||||
from_model = self._get_unit(from_unit)
|
||||
to_model = self._get_unit(to_unit)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
from abc import ABC, abstractmethod
|
||||
from logging import Logger
|
||||
from pathlib import Path
|
||||
@@ -11,6 +12,8 @@ class AbstractSeeder(ABC):
|
||||
Abstract class for seeding data.
|
||||
"""
|
||||
|
||||
resources = Path(__file__).parent / "resources"
|
||||
|
||||
def __init__(self, db: AllRepositories, logger: Logger | None = None):
|
||||
"""
|
||||
Initialize the abstract seeder.
|
||||
@@ -19,7 +22,14 @@ class AbstractSeeder(ABC):
|
||||
"""
|
||||
self.repos = db
|
||||
self.logger = logger or get_logger("Data Seeder")
|
||||
self.resources = Path(__file__).parent / "resources"
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def get_file(self, locale: str | None = None) -> Path: ...
|
||||
|
||||
@classmethod
|
||||
def load_file(self, file: Path) -> dict[str, dict]:
|
||||
return json.loads(file.read_text(encoding="utf-8"))
|
||||
|
||||
@abstractmethod
|
||||
def seed(self, locale: str | None = None) -> None: ...
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -980,7 +980,7 @@
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "Kastanienpüree",
|
||||
"plural_name": "chestnut purée"
|
||||
"plural_name": "Kastanienpüree"
|
||||
},
|
||||
"prickly pear": {
|
||||
"aliases": [],
|
||||
@@ -1232,7 +1232,7 @@
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "Horngurke",
|
||||
"plural_name": "kiwanos"
|
||||
"plural_name": "Horn gurken"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1295,7 +1295,7 @@
|
||||
"black fungu": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "black fungus",
|
||||
"name": "Mu-Err-Pilze",
|
||||
"plural_name": "Mu-Err-Pilze"
|
||||
},
|
||||
"black truffle": {
|
||||
@@ -1361,7 +1361,7 @@
|
||||
"white fungu": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "white fungus",
|
||||
"name": "Tremella",
|
||||
"plural_name": "Weiße Pilze"
|
||||
},
|
||||
"pioppini": {
|
||||
@@ -1373,7 +1373,7 @@
|
||||
"snow fungu": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "snow fungus",
|
||||
"name": "Tremella",
|
||||
"plural_name": "Schneepilze"
|
||||
},
|
||||
"white beech mushroom": {
|
||||
@@ -1385,7 +1385,7 @@
|
||||
"boletu": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "boletus",
|
||||
"name": "Steinpilz",
|
||||
"plural_name": "Steinpilze"
|
||||
},
|
||||
"huitlacoche": {
|
||||
@@ -1404,7 +1404,7 @@
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "Nameko",
|
||||
"plural_name": "namekos"
|
||||
"plural_name": "Nameko Pilz"
|
||||
},
|
||||
"djon djon mushroom": {
|
||||
"aliases": [],
|
||||
@@ -1427,7 +1427,7 @@
|
||||
"honey fungu": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "honey fungus",
|
||||
"name": "Hallimasche",
|
||||
"plural_name": "Hallimasche"
|
||||
},
|
||||
"caesar's mushroom": {
|
||||
@@ -2017,16 +2017,16 @@
|
||||
"cream cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "cream cheese",
|
||||
"plural_name": "cream cheese"
|
||||
"name": "Frischkäse",
|
||||
"plural_name": "Frischkäse"
|
||||
},
|
||||
"sharp cheddar cheese": {
|
||||
"aliases": [
|
||||
"sharp cheddar"
|
||||
"Kräftiger Cheddar"
|
||||
],
|
||||
"description": "",
|
||||
"name": "sharp cheddar cheese",
|
||||
"plural_name": "sharp cheddar cheese"
|
||||
"name": "Kräftiger Cheddar",
|
||||
"plural_name": "Kräftiger Cheddar"
|
||||
},
|
||||
"cheese": {
|
||||
"aliases": [],
|
||||
@@ -2055,14 +2055,14 @@
|
||||
"cheddar-jack cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "cheddar-jack cheese",
|
||||
"plural_name": "cheddar-jack cheese"
|
||||
"name": "Cheddar-Käse",
|
||||
"plural_name": "Cheddar-Käse"
|
||||
},
|
||||
"monterey jack cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "monterey jack cheese",
|
||||
"plural_name": "monterey jack cheese"
|
||||
"name": "Monterey-Jack-Käse",
|
||||
"plural_name": "Monterey-Jack-Käse"
|
||||
},
|
||||
"blue cheese": {
|
||||
"aliases": [],
|
||||
@@ -2074,25 +2074,25 @@
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "Ziegenkäse",
|
||||
"plural_name": "goat cheese"
|
||||
"plural_name": "Ziegenkäse"
|
||||
},
|
||||
"fresh mozzarella cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "fresh mozzarella cheese",
|
||||
"plural_name": "fresh mozzarella cheese"
|
||||
"name": "Frischer Mozzarella",
|
||||
"plural_name": "Frischer Mozzarella"
|
||||
},
|
||||
"swis cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "swis cheese",
|
||||
"plural_name": "swis cheese"
|
||||
"name": "Schweizer Käse",
|
||||
"plural_name": "Schweizer Käse"
|
||||
},
|
||||
"pecorino cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "pecorino cheese",
|
||||
"plural_name": "pecorino cheese"
|
||||
"name": "Pecorino",
|
||||
"plural_name": "Pecorino"
|
||||
},
|
||||
"gruyere cheese": {
|
||||
"aliases": [],
|
||||
@@ -2103,8 +2103,8 @@
|
||||
"mascarpone cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "mascarpone cheese",
|
||||
"plural_name": "mascarpone cheese"
|
||||
"name": "Mascarpone",
|
||||
"plural_name": "Mascarpone"
|
||||
},
|
||||
"cottage cheese": {
|
||||
"aliases": [],
|
||||
@@ -2115,26 +2115,26 @@
|
||||
"american cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "american cheese",
|
||||
"plural_name": "american cheese"
|
||||
"name": "Schmelzkäse",
|
||||
"plural_name": "Schmelzkäse"
|
||||
},
|
||||
"provolone cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "Provolone-Käse",
|
||||
"plural_name": "provolone cheese"
|
||||
"plural_name": "Provolone"
|
||||
},
|
||||
"mexican cheese blend": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "mexican cheese blend",
|
||||
"plural_name": "mexican cheese blend"
|
||||
"name": "mexikanische Käsemischung",
|
||||
"plural_name": "mexikanische Käsemischung"
|
||||
},
|
||||
"pepper jack cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "pepper jack cheese",
|
||||
"plural_name": "pepper jack cheese"
|
||||
"name": "Pfefferkäse",
|
||||
"plural_name": "Pfefferkäse"
|
||||
},
|
||||
"brie cheese": {
|
||||
"aliases": [],
|
||||
@@ -2145,26 +2145,26 @@
|
||||
"paneer cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "paneer cheese",
|
||||
"plural_name": "paneer cheese"
|
||||
"name": "Paneer",
|
||||
"plural_name": "Paneer"
|
||||
},
|
||||
"fontina cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "fontina cheese",
|
||||
"plural_name": "fontina cheese"
|
||||
"name": "Fontina",
|
||||
"plural_name": "Fontina"
|
||||
},
|
||||
"queso fresco cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "queso fresco cheese",
|
||||
"plural_name": "queso fresco cheese"
|
||||
"name": "Cottage Käse",
|
||||
"plural_name": "Cottage Käse"
|
||||
},
|
||||
"quark cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "quark cheese",
|
||||
"plural_name": "quark cheese"
|
||||
"name": "Quark",
|
||||
"plural_name": "Quark"
|
||||
},
|
||||
"gouda cheese": {
|
||||
"aliases": [],
|
||||
@@ -2175,128 +2175,128 @@
|
||||
"cotija cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "cotija cheese",
|
||||
"plural_name": "cotija cheese"
|
||||
"name": "Cotija",
|
||||
"plural_name": "Cotija"
|
||||
},
|
||||
"asiago cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "asiago cheese",
|
||||
"plural_name": "asiago cheese"
|
||||
"name": "Asiago",
|
||||
"plural_name": "Asiago"
|
||||
},
|
||||
"smoked cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "smoked cheese",
|
||||
"plural_name": "smoked cheese"
|
||||
"name": "geräucherter Käse",
|
||||
"plural_name": "geräucherter Käse"
|
||||
},
|
||||
"halloumi cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "halloumi cheese",
|
||||
"plural_name": "halloumi cheese"
|
||||
"name": "Halloumi",
|
||||
"plural_name": "Halloumi"
|
||||
},
|
||||
"chevre cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "chevre cheese",
|
||||
"plural_name": "chevre cheese"
|
||||
"name": "Chèvre",
|
||||
"plural_name": "Chèvre"
|
||||
},
|
||||
"manchego cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "manchego cheese",
|
||||
"plural_name": "manchego cheese"
|
||||
"name": "Manchego Käse",
|
||||
"plural_name": "Manchego Käse"
|
||||
},
|
||||
"italian cheese blend": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "italian cheese blend",
|
||||
"plural_name": "italian cheese blend"
|
||||
"name": "italienische Käsemischung",
|
||||
"plural_name": "italienische Käsemischung"
|
||||
},
|
||||
"neufchatel cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "neufchatel cheese",
|
||||
"plural_name": "neufchatel cheese"
|
||||
"name": "Neufchâtel Käse",
|
||||
"plural_name": "Neufchâtel Käse"
|
||||
},
|
||||
"herb cream cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "herb cream cheese",
|
||||
"plural_name": "herb cream cheese"
|
||||
"name": "Kräuterfrischkäse",
|
||||
"plural_name": "Kräuterfrischkäse"
|
||||
},
|
||||
"burrata cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "burrata cheese",
|
||||
"plural_name": "burrata cheese"
|
||||
"name": "Burrata",
|
||||
"plural_name": "Burrata"
|
||||
},
|
||||
"havarti cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "havarti cheese",
|
||||
"plural_name": "havarti cheese"
|
||||
"name": "Havarti Käse",
|
||||
"plural_name": "Havarti Käse"
|
||||
},
|
||||
"colby cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "colby cheese",
|
||||
"plural_name": "colby cheese"
|
||||
"name": "Colby Käse",
|
||||
"plural_name": "Colby Käse"
|
||||
},
|
||||
"grana-padano cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "grana-padano cheese",
|
||||
"plural_name": "grana-padano cheese"
|
||||
"name": "Grana Padano",
|
||||
"plural_name": "Grana Padano"
|
||||
},
|
||||
"muenster cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "muenster cheese",
|
||||
"plural_name": "muenster cheese"
|
||||
"name": "Munster",
|
||||
"plural_name": "Munster"
|
||||
},
|
||||
"string cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "string cheese",
|
||||
"plural_name": "string cheese"
|
||||
"name": "Fadenkäse",
|
||||
"plural_name": "Fadenkäse"
|
||||
},
|
||||
"camembert cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "camembert cheese",
|
||||
"plural_name": "camembert cheese"
|
||||
"name": "Camembert",
|
||||
"plural_name": "Camembert"
|
||||
},
|
||||
"soft cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "soft cheese",
|
||||
"plural_name": "soft cheese"
|
||||
"name": "Weichkäse",
|
||||
"plural_name": "Weichkäse"
|
||||
},
|
||||
"stilton cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "stilton cheese",
|
||||
"plural_name": "stilton cheese"
|
||||
"name": "Stilton",
|
||||
"plural_name": "Stilton"
|
||||
},
|
||||
"raclette cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "raclette cheese",
|
||||
"plural_name": "raclette cheese"
|
||||
"name": "Raclettekäse",
|
||||
"plural_name": "Raclettekäse"
|
||||
},
|
||||
"colby-jack cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "colby-jack cheese",
|
||||
"plural_name": "colby-jack cheese"
|
||||
"name": "Colby-Jack Käse",
|
||||
"plural_name": "Colby-Jack Käse"
|
||||
},
|
||||
"jarlsberg cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "jarlsberg cheese",
|
||||
"plural_name": "jarlsberg cheese"
|
||||
"name": "Jarlsberg Käse",
|
||||
"plural_name": "Jarlsberg Käse"
|
||||
},
|
||||
"taleggio cheese": {
|
||||
"aliases": [],
|
||||
@@ -2307,14 +2307,14 @@
|
||||
"oaxaca cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "oaxaca cheese",
|
||||
"plural_name": "oaxaca cheese"
|
||||
"name": "Oaxaca Käse",
|
||||
"plural_name": "Oaxaca Käse"
|
||||
},
|
||||
"labneh cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "labneh cheese",
|
||||
"plural_name": "labneh cheese"
|
||||
"name": "Labneh",
|
||||
"plural_name": "Labneh"
|
||||
},
|
||||
"edam cheese": {
|
||||
"aliases": [],
|
||||
@@ -2325,104 +2325,104 @@
|
||||
"creamy cheese wedge": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "creamy cheese wedge",
|
||||
"plural_name": "creamy cheese wedges"
|
||||
"name": "Käseecke",
|
||||
"plural_name": "Käseecken"
|
||||
},
|
||||
"cheese powder cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "cheese powder cheese",
|
||||
"plural_name": "cheese powder cheese"
|
||||
"name": "Käsepulver",
|
||||
"plural_name": "Käsepulver"
|
||||
},
|
||||
"fromage blanc cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "fromage blanc cheese",
|
||||
"plural_name": "fromage blanc cheese"
|
||||
"name": "Fromage Blanc",
|
||||
"plural_name": "Fromage Blanc"
|
||||
},
|
||||
"asadero cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "asadero cheese",
|
||||
"plural_name": "asadero cheese"
|
||||
"name": "Asadero Käse",
|
||||
"plural_name": "Asadero Käse"
|
||||
},
|
||||
"marble cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "marble cheese",
|
||||
"plural_name": "marble cheese"
|
||||
"name": "Marmorkäse",
|
||||
"plural_name": "Marmorkäse"
|
||||
},
|
||||
"leicester cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "leicester cheese",
|
||||
"plural_name": "leicester cheese"
|
||||
"name": "Leicester Käse",
|
||||
"plural_name": "Leicester Käse"
|
||||
},
|
||||
"kefalotyri cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "kefalotyri cheese",
|
||||
"plural_name": "kefalotyri cheese"
|
||||
"name": "Kefalotyri Käse",
|
||||
"plural_name": "Kefalotyri Käse"
|
||||
},
|
||||
"mizithra cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "mizithra cheese",
|
||||
"plural_name": "mizithra cheese"
|
||||
"name": "Mizithra Käse",
|
||||
"plural_name": "Mizithra Käse"
|
||||
},
|
||||
"lancashire cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "lancashire cheese",
|
||||
"plural_name": "lancashire cheese"
|
||||
"name": "Lancashire Käse",
|
||||
"plural_name": "Lancashire Käse"
|
||||
},
|
||||
"kasseri cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "kasseri cheese",
|
||||
"plural_name": "kasseri cheese"
|
||||
"name": "Kasseri Käse",
|
||||
"plural_name": "Kasseri Käse"
|
||||
},
|
||||
"babybel cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "babybel cheese",
|
||||
"plural_name": "babybel cheese"
|
||||
"name": "Babybel",
|
||||
"plural_name": "Babybel"
|
||||
},
|
||||
"panela cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "panela cheese",
|
||||
"plural_name": "panela cheese"
|
||||
"name": "Panela Käse",
|
||||
"plural_name": "Panela Käse"
|
||||
},
|
||||
"longhorn cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "longhorn cheese",
|
||||
"plural_name": "longhorn cheese"
|
||||
"name": "Langhorn Käse",
|
||||
"plural_name": "Langhorn Käse"
|
||||
},
|
||||
"seasoned feta cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "seasoned feta cheese",
|
||||
"plural_name": "seasoned feta cheese"
|
||||
"name": "Gewürzter Feta",
|
||||
"plural_name": "Gewürzter Feta"
|
||||
},
|
||||
"comté cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "comté cheese",
|
||||
"plural_name": "comté cheese"
|
||||
"name": "Comté",
|
||||
"plural_name": "Comté"
|
||||
},
|
||||
"graviera cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "graviera cheese",
|
||||
"plural_name": "graviera cheese"
|
||||
"name": "Graviera",
|
||||
"plural_name": "Graviera"
|
||||
},
|
||||
"wensleydale cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "wensleydale cheese",
|
||||
"plural_name": "wensleydale cheese"
|
||||
"name": "Wensleydale Käse",
|
||||
"plural_name": "Wensleydale Käse"
|
||||
},
|
||||
"scamorza cheese": {
|
||||
"aliases": [],
|
||||
@@ -2433,38 +2433,38 @@
|
||||
"cambozola cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "cambozola cheese",
|
||||
"plural_name": "cambozola cheese"
|
||||
"name": "Cambozola",
|
||||
"plural_name": "Cambozola"
|
||||
},
|
||||
"cheshire cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "cheshire cheese",
|
||||
"plural_name": "cheshire cheese"
|
||||
"name": "Cheshire-Käse",
|
||||
"plural_name": "Cheshire-Käse"
|
||||
},
|
||||
"anthotyro cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "anthotyro cheese",
|
||||
"plural_name": "anthotyro cheese"
|
||||
"name": "Anthotyro Käse",
|
||||
"plural_name": "Anthotyro Käse"
|
||||
},
|
||||
"chenna cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "chenna cheese",
|
||||
"plural_name": "chenna cheese"
|
||||
"name": "Chhena Käse",
|
||||
"plural_name": "Chhena Käse"
|
||||
},
|
||||
"hard goat cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "hard goat cheese",
|
||||
"plural_name": "hard goat cheese"
|
||||
"name": "Ziegen-Hartkäse",
|
||||
"plural_name": "Ziegen-Hartkäse"
|
||||
},
|
||||
"kashkaval cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "kashkaval cheese",
|
||||
"plural_name": "kashkaval cheese"
|
||||
"name": "Kashkaval Käse",
|
||||
"plural_name": "Kashkaval Käse"
|
||||
},
|
||||
"sheep cheese": {
|
||||
"aliases": [],
|
||||
@@ -2475,25 +2475,25 @@
|
||||
"amul cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "amul cheese",
|
||||
"plural_name": "amul cheese"
|
||||
"name": "Amul Käse",
|
||||
"plural_name": "Amul Käse"
|
||||
},
|
||||
"reblochon cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "reblochon cheese",
|
||||
"plural_name": "reblochon cheese"
|
||||
"name": "Reblochons",
|
||||
"plural_name": "Reblochons"
|
||||
},
|
||||
"robiola cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "robiola cheese",
|
||||
"plural_name": "robiola cheese"
|
||||
"name": "Robiola Käse",
|
||||
"plural_name": "Robiola Käse"
|
||||
},
|
||||
"brick cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "brick cheese",
|
||||
"name": "Backstein Käse",
|
||||
"plural_name": "brick cheese"
|
||||
},
|
||||
"quick-melt cheese": {
|
||||
@@ -5772,7 +5772,7 @@
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "Ulva",
|
||||
"plural_name": "sea lettuce"
|
||||
"plural_name": "Ulva"
|
||||
},
|
||||
"korean seaweed": {
|
||||
"aliases": [],
|
||||
@@ -5830,7 +5830,7 @@
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "Koriander",
|
||||
"plural_name": "cilantro"
|
||||
"plural_name": "Koriander"
|
||||
},
|
||||
"cumin": {
|
||||
"aliases": [],
|
||||
@@ -6004,7 +6004,7 @@
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "Gemahlener Pfeffer",
|
||||
"plural_name": "cracked pepper"
|
||||
"plural_name": "Gemahlener Pfeffer"
|
||||
},
|
||||
"peppercorn": {
|
||||
"aliases": [],
|
||||
@@ -6274,13 +6274,13 @@
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "Getrockneter Koriander",
|
||||
"plural_name": "dried cilantro"
|
||||
"plural_name": "Getrockneter Koriander"
|
||||
},
|
||||
"lemon balm": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "Zitronenmelisse",
|
||||
"plural_name": "lemon balm"
|
||||
"plural_name": "Zitronenmelisse"
|
||||
},
|
||||
"dill seed": {
|
||||
"aliases": [],
|
||||
@@ -6304,7 +6304,7 @@
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "Wasabi Pulver",
|
||||
"plural_name": "wasabi powder"
|
||||
"plural_name": "Wasabi Pulver"
|
||||
},
|
||||
"achiote seed": {
|
||||
"aliases": [],
|
||||
@@ -6340,13 +6340,13 @@
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "vietnamesischer Zimt",
|
||||
"plural_name": "saigon cinnamon"
|
||||
"plural_name": "vietnamesischer Zimt"
|
||||
},
|
||||
"lemongrass paste": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "Zitronengras-Paste",
|
||||
"plural_name": "lemongrass paste"
|
||||
"plural_name": "Zitronengras-Paste"
|
||||
},
|
||||
"shiso": {
|
||||
"aliases": [],
|
||||
@@ -6358,7 +6358,7 @@
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "Sellerieknollenpulver",
|
||||
"plural_name": "celery powder"
|
||||
"plural_name": "Sellerieknollenpulver"
|
||||
},
|
||||
"black cumin": {
|
||||
"aliases": [],
|
||||
@@ -6476,26 +6476,26 @@
|
||||
"molasses": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "molass",
|
||||
"plural_name": "molasses"
|
||||
"name": "Melasse",
|
||||
"plural_name": "Melasse"
|
||||
},
|
||||
"stevia": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "Stevia",
|
||||
"plural_name": "stevia"
|
||||
"plural_name": "Stevia"
|
||||
},
|
||||
"agave nectar": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "Agavendicksaft",
|
||||
"plural_name": "agave nectar"
|
||||
"plural_name": "Agavendicksaft"
|
||||
},
|
||||
"sugar syrup": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "Zuckersirup",
|
||||
"plural_name": "sugar syrup"
|
||||
"plural_name": "Zuckersirup"
|
||||
},
|
||||
"isomalt": {
|
||||
"aliases": [],
|
||||
@@ -6507,31 +6507,31 @@
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "Erythrit",
|
||||
"plural_name": "erythritol"
|
||||
"plural_name": "Erythrit"
|
||||
},
|
||||
"vanilla sugar": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "Vanillezucker",
|
||||
"plural_name": "vanilla sugar"
|
||||
"plural_name": "Vanillezucker"
|
||||
},
|
||||
"demerara sugar": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "Demerara-Zucker",
|
||||
"plural_name": "demerara sugar"
|
||||
"plural_name": "Demerara-Zucker"
|
||||
},
|
||||
"caramel syrup": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "Karamellsirup",
|
||||
"plural_name": "caramel syrup"
|
||||
"plural_name": "Karamellsirup"
|
||||
},
|
||||
"chocolate syrup": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "Schokoladensirup",
|
||||
"plural_name": "chocolate syrup"
|
||||
"plural_name": "Schokoladensirup"
|
||||
},
|
||||
"jaggery": {
|
||||
"aliases": [],
|
||||
@@ -6543,61 +6543,61 @@
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "Rohzucker",
|
||||
"plural_name": "raw sugar"
|
||||
"plural_name": "Rohzucker"
|
||||
},
|
||||
"golden syrup": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "Zuckerrübensirup",
|
||||
"plural_name": "golden syrup"
|
||||
"plural_name": "Zuckerrübensirup"
|
||||
},
|
||||
"cinnamon sugar": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "Zimtzucker",
|
||||
"plural_name": "cinnamon sugar"
|
||||
"plural_name": "Zimtzucker"
|
||||
},
|
||||
"liquid stevia": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "Flüssiges Stevia",
|
||||
"plural_name": "liquid stevia"
|
||||
"plural_name": "Flüssiger Stevia"
|
||||
},
|
||||
"grenadine": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "Grenadine",
|
||||
"plural_name": "grenadine"
|
||||
"plural_name": "Grenadine"
|
||||
},
|
||||
"coarse sugar": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "Hagelzucker",
|
||||
"plural_name": "coarse sugar"
|
||||
"plural_name": "Hagelzucker"
|
||||
},
|
||||
"salted caramel syrup": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "Gesalzenes Karamellsirup",
|
||||
"plural_name": "salted caramel syrup"
|
||||
"plural_name": "Gesalzener Karamellsirup"
|
||||
},
|
||||
"sanding sugar": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "Puderzucker",
|
||||
"plural_name": "sanding sugar"
|
||||
"plural_name": "Puderzucker"
|
||||
},
|
||||
"dark corn syrup": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "Dunkler Maissirup",
|
||||
"plural_name": "dark corn syrup"
|
||||
"plural_name": "Dunkler Maissirup"
|
||||
},
|
||||
"sucralose": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "Sucralose",
|
||||
"plural_name": "sucralose"
|
||||
"plural_name": "Sucralose"
|
||||
},
|
||||
"monk fruit sweetener": {
|
||||
"aliases": [],
|
||||
@@ -6609,7 +6609,7 @@
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "Ahornzucker",
|
||||
"plural_name": "maple sugar"
|
||||
"plural_name": "Ahornzucker"
|
||||
},
|
||||
"blackstrap molass": {
|
||||
"aliases": [],
|
||||
@@ -6879,7 +6879,7 @@
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "Malzsirup",
|
||||
"plural_name": "malt syrup"
|
||||
"plural_name": "Malzsirup"
|
||||
},
|
||||
"hot honey": {
|
||||
"aliases": [],
|
||||
|
||||
@@ -7110,8 +7110,8 @@
|
||||
"himalayan salt": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "himalayan salt",
|
||||
"plural_name": "himalayan salt"
|
||||
"name": "Sal del Himalaya",
|
||||
"plural_name": "Sal del Himalaya"
|
||||
},
|
||||
"lemon & pepper seasoning": {
|
||||
"aliases": [],
|
||||
@@ -7326,8 +7326,8 @@
|
||||
"smoked salt": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "smoked salt",
|
||||
"plural_name": "smoked salt"
|
||||
"name": "Sal ahumada",
|
||||
"plural_name": "Sal ahumada"
|
||||
},
|
||||
"dash seasoning": {
|
||||
"aliases": [],
|
||||
@@ -7428,8 +7428,8 @@
|
||||
"truffle salt": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "truffle salt",
|
||||
"plural_name": "truffle salt"
|
||||
"name": "Sal de trufa",
|
||||
"plural_name": "Sal de trufa"
|
||||
},
|
||||
"biryani masala": {
|
||||
"aliases": [],
|
||||
@@ -10030,8 +10030,8 @@
|
||||
"bread": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "bread",
|
||||
"plural_name": "breads"
|
||||
"name": "Pan",
|
||||
"plural_name": "Panes"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -10052,20 +10052,20 @@
|
||||
"flour tortilla": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "flour tortilla",
|
||||
"plural_name": "flour tortillas"
|
||||
"name": "Tortilla de harina",
|
||||
"plural_name": "Tortillas de harina"
|
||||
},
|
||||
"almond flour tortilla": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "almond flour tortilla",
|
||||
"plural_name": "almond flour tortillas"
|
||||
"name": "Tortilla de harina de almendra",
|
||||
"plural_name": "Tortillas de harina de almendras"
|
||||
},
|
||||
"corn tortilla": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "corn tortilla",
|
||||
"plural_name": "corn tortillas"
|
||||
"name": "Tortilla de maíz",
|
||||
"plural_name": "Tortillas de maíz"
|
||||
},
|
||||
"cracker": {
|
||||
"aliases": [],
|
||||
@@ -10082,14 +10082,14 @@
|
||||
"tortilla chip": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "tortilla chip",
|
||||
"plural_name": "tortilla chips"
|
||||
"name": "totopo",
|
||||
"plural_name": "totopos"
|
||||
},
|
||||
"pita": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "pita",
|
||||
"plural_name": "pitas"
|
||||
"name": "Pan de pita",
|
||||
"plural_name": "Panes de pita"
|
||||
},
|
||||
"pretzel": {
|
||||
"aliases": [],
|
||||
@@ -10106,14 +10106,14 @@
|
||||
"rustic italian bread": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "rustic italian bread",
|
||||
"plural_name": "rustic italian breads"
|
||||
"name": "Pan italiano rústico",
|
||||
"plural_name": "Panes italianos rústicos"
|
||||
},
|
||||
"popcorn": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "popcorn",
|
||||
"plural_name": "popcorns"
|
||||
"name": "Palomita",
|
||||
"plural_name": "Palomitas"
|
||||
},
|
||||
"crouton": {
|
||||
"aliases": [],
|
||||
@@ -10124,8 +10124,8 @@
|
||||
"whole-wheat tortilla": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "whole-wheat tortilla",
|
||||
"plural_name": "whole-wheat tortillas"
|
||||
"name": "Tortilla de harina integral",
|
||||
"plural_name": "Tortillas de harina integral"
|
||||
},
|
||||
"english muffin": {
|
||||
"aliases": [],
|
||||
|
||||
@@ -169,7 +169,7 @@
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "rucola",
|
||||
"plural_name": "arugula"
|
||||
"plural_name": "rucolaa"
|
||||
},
|
||||
"leek": {
|
||||
"aliases": [],
|
||||
@@ -279,8 +279,8 @@
|
||||
"mixed vegetables": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "mixed vegetables",
|
||||
"plural_name": "mixed vegetables"
|
||||
"name": "sekakasvikset",
|
||||
"plural_name": "sekakasvikset"
|
||||
},
|
||||
"poblano pepper": {
|
||||
"aliases": [],
|
||||
@@ -419,8 +419,8 @@
|
||||
"corn on the cob": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "corn on the cob",
|
||||
"plural_name": "corn on the cob"
|
||||
"name": "tähkämaissi",
|
||||
"plural_name": "tähkämaissia"
|
||||
},
|
||||
"radicchio": {
|
||||
"aliases": [],
|
||||
@@ -488,13 +488,13 @@
|
||||
],
|
||||
"description": "",
|
||||
"name": "maize",
|
||||
"plural_name": "maize"
|
||||
"plural_name": "maissia"
|
||||
},
|
||||
"collard greens": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "collard greens",
|
||||
"plural_name": "collard greens"
|
||||
"name": "lehtikaali",
|
||||
"plural_name": "lehtikaalia"
|
||||
},
|
||||
"french-fried onion": {
|
||||
"aliases": [],
|
||||
@@ -1241,8 +1241,8 @@
|
||||
"button mushroom": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "button mushroom",
|
||||
"plural_name": "button mushrooms"
|
||||
"name": "pieni herkkusieni",
|
||||
"plural_name": "pieniä herkkusieniä"
|
||||
},
|
||||
"shiitake mushroom": {
|
||||
"aliases": [],
|
||||
@@ -1253,8 +1253,8 @@
|
||||
"portobello mushroom": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "portobello mushroom",
|
||||
"plural_name": "portobello mushrooms"
|
||||
"name": "herkkusieni",
|
||||
"plural_name": "herkkusieniä"
|
||||
},
|
||||
"wild mushroom": {
|
||||
"aliases": [],
|
||||
@@ -1503,8 +1503,8 @@
|
||||
"dried cherry": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "dried cherry",
|
||||
"plural_name": "dried cherries"
|
||||
"name": "kuivattu kirsikka",
|
||||
"plural_name": "kuivattuja kirsikoita"
|
||||
},
|
||||
"juniper berry": {
|
||||
"aliases": [],
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5425,14 +5425,14 @@
|
||||
"amberjack": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "amberjack",
|
||||
"plural_name": "amberjacks"
|
||||
"name": "sériole",
|
||||
"plural_name": "sérioles"
|
||||
},
|
||||
"korean fish cake": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "korean fish cake",
|
||||
"plural_name": "korean fish cakes"
|
||||
"name": "fish cake coréen",
|
||||
"plural_name": "fish cakes coréens"
|
||||
},
|
||||
"mullet": {
|
||||
"aliases": [],
|
||||
@@ -5467,8 +5467,8 @@
|
||||
"threadfin": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "threadfin",
|
||||
"plural_name": "threadfins"
|
||||
"name": "cohana",
|
||||
"plural_name": "cohanas"
|
||||
},
|
||||
"tiny fish": {
|
||||
"aliases": [],
|
||||
@@ -5479,8 +5479,8 @@
|
||||
"tuna belly": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "tuna belly",
|
||||
"plural_name": "tuna bellies"
|
||||
"name": "ventrèche de thon",
|
||||
"plural_name": "ventrèches de thon"
|
||||
},
|
||||
"beluga caviar": {
|
||||
"aliases": [],
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "jalapeño",
|
||||
"plural_name": "jalapeños"
|
||||
"plural_name": "jalapeño"
|
||||
},
|
||||
"avocado": {
|
||||
"aliases": [],
|
||||
@@ -132,8 +132,8 @@
|
||||
"baby greens": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "baby greens",
|
||||
"plural_name": "baby greens"
|
||||
"name": "verdura tenera",
|
||||
"plural_name": "verdure tenere"
|
||||
},
|
||||
"pumpkin": {
|
||||
"aliases": [],
|
||||
@@ -163,13 +163,13 @@
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "cavolo riccio",
|
||||
"plural_name": "kale"
|
||||
"plural_name": "cavoli"
|
||||
},
|
||||
"arugula": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "rucola",
|
||||
"plural_name": "arugula"
|
||||
"plural_name": "rucole"
|
||||
},
|
||||
"leek": {
|
||||
"aliases": [],
|
||||
@@ -199,7 +199,7 @@
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "lattuga romana",
|
||||
"plural_name": "romaine lettuce"
|
||||
"plural_name": "lattuga romana"
|
||||
},
|
||||
"beetroot": {
|
||||
"aliases": [],
|
||||
@@ -261,8 +261,8 @@
|
||||
"mixed greens": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "mixed greens",
|
||||
"plural_name": "mixed greens"
|
||||
"name": "verdura mista",
|
||||
"plural_name": "verdure miste"
|
||||
},
|
||||
"parsnip": {
|
||||
"aliases": [],
|
||||
@@ -279,8 +279,8 @@
|
||||
"mixed vegetables": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "mixed vegetables",
|
||||
"plural_name": "mixed vegetables"
|
||||
"name": "verdure miste",
|
||||
"plural_name": "verdure miste"
|
||||
},
|
||||
"poblano pepper": {
|
||||
"aliases": [],
|
||||
@@ -304,7 +304,7 @@
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "pepe di Caienna",
|
||||
"plural_name": "cayenne pepper"
|
||||
"plural_name": "peperoncini di Cayenna"
|
||||
},
|
||||
"green tomato": {
|
||||
"aliases": [],
|
||||
@@ -419,8 +419,8 @@
|
||||
"corn on the cob": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "corn on the cob",
|
||||
"plural_name": "corn on the cob"
|
||||
"name": "pannocchia",
|
||||
"plural_name": "pannocchie"
|
||||
},
|
||||
"radicchio": {
|
||||
"aliases": [],
|
||||
@@ -438,7 +438,7 @@
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "broccoli spinarolo",
|
||||
"plural_name": "tenderstem broccoli"
|
||||
"plural_name": "broccoletti"
|
||||
},
|
||||
"plantain": {
|
||||
"aliases": [],
|
||||
@@ -5730,7 +5730,7 @@
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "abalone",
|
||||
"plural_name": "abalones"
|
||||
"plural_name": "abaloni"
|
||||
},
|
||||
"seaweed salad": {
|
||||
"aliases": [],
|
||||
@@ -6309,8 +6309,8 @@
|
||||
"achiote seed": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "achiote seed",
|
||||
"plural_name": "achiote seeds"
|
||||
"name": "seme di achiote",
|
||||
"plural_name": "semi di achiote"
|
||||
},
|
||||
"savory herb": {
|
||||
"aliases": [],
|
||||
@@ -6405,8 +6405,8 @@
|
||||
"achiote paste": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "achiote paste",
|
||||
"plural_name": "achiote paste"
|
||||
"name": "pasta di achiote",
|
||||
"plural_name": "pasta di achiote"
|
||||
},
|
||||
"summer savory": {
|
||||
"aliases": [],
|
||||
@@ -9774,8 +9774,8 @@
|
||||
"acini di pepe": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "acini di pepe",
|
||||
"plural_name": "acini di pepes"
|
||||
"name": "acino di pepe",
|
||||
"plural_name": "acini di pepe"
|
||||
},
|
||||
"cavatelli": {
|
||||
"aliases": [],
|
||||
@@ -10687,7 +10687,7 @@
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "olio di semi di girasole",
|
||||
"plural_name": "sunflower oil"
|
||||
"plural_name": "olio di semi di girasole"
|
||||
},
|
||||
"avocado oil": {
|
||||
"aliases": [],
|
||||
@@ -10988,8 +10988,8 @@
|
||||
"achiote oil": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "achiote oil",
|
||||
"plural_name": "achiote oil"
|
||||
"name": "olio di achiote",
|
||||
"plural_name": "olio di achiote"
|
||||
},
|
||||
"jojoba oil": {
|
||||
"aliases": [],
|
||||
@@ -15676,7 +15676,7 @@
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "succo alla bacca di acai",
|
||||
"plural_name": "acai berry juice"
|
||||
"plural_name": "succo di acai"
|
||||
},
|
||||
"lemon crystal": {
|
||||
"aliases": [],
|
||||
@@ -15938,7 +15938,7 @@
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "polvere di bacca di acai",
|
||||
"plural_name": "acai powder"
|
||||
"plural_name": "polvere di acai"
|
||||
},
|
||||
"hemp protein": {
|
||||
"aliases": [],
|
||||
@@ -16105,8 +16105,8 @@
|
||||
"activated charcoal": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "activated charcoal",
|
||||
"plural_name": "activated charcoals"
|
||||
"name": "carbone attivo",
|
||||
"plural_name": "carboni attivi"
|
||||
},
|
||||
"egg powder": {
|
||||
"aliases": [],
|
||||
@@ -16231,7 +16231,7 @@
|
||||
"sunflower lecithin": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "sunflower lecithin",
|
||||
"name": "lecitina di girasole",
|
||||
"plural_name": "lecitina di girasole"
|
||||
},
|
||||
"thc": {
|
||||
@@ -16304,7 +16304,7 @@
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "maltodestrina",
|
||||
"plural_name": "maltodextrins"
|
||||
"plural_name": "maltodestrine"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2254,13 +2254,13 @@
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "muenster kaas",
|
||||
"plural_name": "muenster cheese"
|
||||
"plural_name": "Munster kaas"
|
||||
},
|
||||
"string cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "string cheese",
|
||||
"plural_name": "string cheese"
|
||||
"name": "stripkaas",
|
||||
"plural_name": "stripkaas"
|
||||
},
|
||||
"camembert cheese": {
|
||||
"aliases": [],
|
||||
@@ -2283,8 +2283,8 @@
|
||||
"raclette cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "raclette cheese",
|
||||
"plural_name": "raclette cheese"
|
||||
"name": "raclette kaas",
|
||||
"plural_name": "raclette kaas"
|
||||
},
|
||||
"colby-jack cheese": {
|
||||
"aliases": [],
|
||||
|
||||
@@ -484,7 +484,7 @@
|
||||
},
|
||||
"maize": {
|
||||
"aliases": [
|
||||
"corn husk"
|
||||
"łuska kukurydzy"
|
||||
],
|
||||
"description": "",
|
||||
"name": "maize",
|
||||
@@ -3469,8 +3469,8 @@
|
||||
"vegan chicken": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "vegan chicken",
|
||||
"plural_name": "vegan chickens"
|
||||
"name": "wegański kurczak",
|
||||
"plural_name": "wegański kurczak"
|
||||
},
|
||||
"coconut paste": {
|
||||
"aliases": [],
|
||||
@@ -3709,8 +3709,8 @@
|
||||
"vegan chicken nugget": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "vegan chicken nugget",
|
||||
"plural_name": "vegan chicken nuggets"
|
||||
"name": "wegański nuggets z kurczaka",
|
||||
"plural_name": "wegańskie nuggetsy z kurczaka"
|
||||
},
|
||||
"vegan starter culture": {
|
||||
"aliases": [],
|
||||
@@ -4353,20 +4353,20 @@
|
||||
"chicken breast": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "chicken breast",
|
||||
"plural_name": "chicken breasts"
|
||||
"name": "pierś z kurczaka",
|
||||
"plural_name": "piersi z kurczaka"
|
||||
},
|
||||
"chicken thigh": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "chicken thigh",
|
||||
"plural_name": "chicken thighs"
|
||||
"name": "udko z kurczaka",
|
||||
"plural_name": "udka z kurczaka"
|
||||
},
|
||||
"cooked chicken": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "cooked chicken",
|
||||
"plural_name": "cooked chickens"
|
||||
"name": "gotowany kurczak",
|
||||
"plural_name": "gotowany kurczak"
|
||||
},
|
||||
"ground turkey": {
|
||||
"aliases": [],
|
||||
@@ -4395,8 +4395,8 @@
|
||||
"chicken wing": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "chicken wing",
|
||||
"plural_name": "chicken wings"
|
||||
"name": "skrzydełko z kurczaka",
|
||||
"plural_name": "skrzydełka z kurczaka"
|
||||
},
|
||||
"turkey breast": {
|
||||
"aliases": [],
|
||||
@@ -4407,20 +4407,20 @@
|
||||
"ground chicken": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "ground chicken",
|
||||
"plural_name": "ground chickens"
|
||||
"name": "zmielone mięso z kurczaka",
|
||||
"plural_name": "zmielone mięso z kurczaka"
|
||||
},
|
||||
"rotisserie chicken": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "rotisserie chicken",
|
||||
"plural_name": "rotisserie chickens"
|
||||
"name": "zmielone mięso z kurczaka",
|
||||
"plural_name": "kurczak z rożna"
|
||||
},
|
||||
"chicken tender": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "chicken tender",
|
||||
"plural_name": "chicken tenders"
|
||||
"name": "polędwica z kurczaka",
|
||||
"plural_name": "polędwiczki z kurczaka"
|
||||
},
|
||||
"turkey sausage": {
|
||||
"aliases": [],
|
||||
@@ -4455,14 +4455,14 @@
|
||||
"boneless chicken": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "boneless chicken",
|
||||
"plural_name": "boneless chickens"
|
||||
"name": "kurczak bez kości",
|
||||
"plural_name": "kurczak bez kości"
|
||||
},
|
||||
"chicken liver": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "chicken liver",
|
||||
"plural_name": "chicken livers"
|
||||
"name": "wątroba z kurczaka",
|
||||
"plural_name": "wątroba z kurczaka"
|
||||
},
|
||||
"cornish hen": {
|
||||
"aliases": [],
|
||||
@@ -4491,8 +4491,8 @@
|
||||
"chicken quarter": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "chicken quarter",
|
||||
"plural_name": "chicken quarters"
|
||||
"name": "ćwiartka z kurczaka",
|
||||
"plural_name": "ćwiartka z kurczaka"
|
||||
},
|
||||
"ground turkey sausage": {
|
||||
"aliases": [],
|
||||
@@ -4557,8 +4557,8 @@
|
||||
"chicken bone": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "chicken bone",
|
||||
"plural_name": "chicken bones"
|
||||
"name": "kość z kurczaka",
|
||||
"plural_name": "kości z kurczaka"
|
||||
},
|
||||
"turkey meatball": {
|
||||
"aliases": [],
|
||||
@@ -4575,8 +4575,8 @@
|
||||
"chicken giblet": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "chicken giblet",
|
||||
"plural_name": "chicken giblets"
|
||||
"name": "podroby z kurczaka",
|
||||
"plural_name": "podroby z kurczaka"
|
||||
},
|
||||
"turkey wing": {
|
||||
"aliases": [],
|
||||
@@ -4599,8 +4599,8 @@
|
||||
"chicken nugget": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "chicken nugget",
|
||||
"plural_name": "chicken nuggets"
|
||||
"name": "nuggets z kurczaka",
|
||||
"plural_name": "nuggetsy z kurczaka"
|
||||
},
|
||||
"turkey burger": {
|
||||
"aliases": [],
|
||||
|
||||
@@ -487,7 +487,7 @@
|
||||
"corn husk"
|
||||
],
|
||||
"description": "",
|
||||
"name": "maize",
|
||||
"name": "milho",
|
||||
"plural_name": "maize"
|
||||
},
|
||||
"collard greens": {
|
||||
|
||||
@@ -18,13 +18,13 @@
|
||||
"abbreviation": "ч.ч."
|
||||
},
|
||||
"fluid-ounce": {
|
||||
"name": "fluid ounce",
|
||||
"name": "течни унции",
|
||||
"plural_name": "течни унции",
|
||||
"description": "",
|
||||
"abbreviation": "fl oz"
|
||||
"abbreviation": "течна унция"
|
||||
},
|
||||
"pint": {
|
||||
"name": "pint",
|
||||
"name": "пинта",
|
||||
"plural_name": "пинти",
|
||||
"description": "",
|
||||
"abbreviation": "pt"
|
||||
@@ -139,8 +139,8 @@
|
||||
"abbreviation": ""
|
||||
},
|
||||
"sprig": {
|
||||
"name": "sprig",
|
||||
"plural_name": "sprigs",
|
||||
"name": "клонче",
|
||||
"plural_name": "клончета",
|
||||
"description": "",
|
||||
"abbreviation": ""
|
||||
}
|
||||
|
||||
@@ -139,8 +139,8 @@
|
||||
"abbreviation": ""
|
||||
},
|
||||
"sprig": {
|
||||
"name": "sprig",
|
||||
"plural_name": "sprigs",
|
||||
"name": "rametto",
|
||||
"plural_name": "rametti",
|
||||
"description": "",
|
||||
"abbreviation": ""
|
||||
}
|
||||
|
||||
@@ -139,8 +139,8 @@
|
||||
"abbreviation": ""
|
||||
},
|
||||
"sprig": {
|
||||
"name": "sprig",
|
||||
"plural_name": "sprigs",
|
||||
"name": "веточка",
|
||||
"plural_name": "веточки",
|
||||
"description": "",
|
||||
"abbreviation": ""
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import json
|
||||
import pathlib
|
||||
from collections.abc import Generator
|
||||
from functools import cached_property
|
||||
@@ -21,9 +20,10 @@ class MultiPurposeLabelSeeder(AbstractSeeder):
|
||||
def service(self):
|
||||
return MultiPurposeLabelService(self.repos)
|
||||
|
||||
def get_file(self, locale: str | None = None) -> pathlib.Path:
|
||||
@classmethod
|
||||
def get_file(cls, locale: str | None = None) -> pathlib.Path:
|
||||
# Get the labels from the foods seed file now
|
||||
locale_path = self.resources / "foods" / "locales" / f"{locale}.json"
|
||||
locale_path = cls.resources / "foods" / "locales" / f"{locale}.json"
|
||||
return locale_path if locale_path.exists() else foods.en_US
|
||||
|
||||
def get_all_labels(self) -> list[MultiPurposeLabelOut]:
|
||||
@@ -34,7 +34,7 @@ class MultiPurposeLabelSeeder(AbstractSeeder):
|
||||
|
||||
current_label_names = {label.name for label in self.get_all_labels()}
|
||||
# load from the foods locale file and remove any empty strings
|
||||
seed_label_names = set(filter(None, json.loads(file.read_text(encoding="utf-8")).keys())) # type: set[str]
|
||||
seed_label_names = set(filter(None, self.load_file(file).keys())) # type: set[str]
|
||||
# only seed new labels
|
||||
to_seed_labels = seed_label_names - current_label_names
|
||||
for label in to_seed_labels:
|
||||
@@ -53,8 +53,9 @@ class MultiPurposeLabelSeeder(AbstractSeeder):
|
||||
|
||||
|
||||
class IngredientUnitsSeeder(AbstractSeeder):
|
||||
def get_file(self, locale: str | None = None) -> pathlib.Path:
|
||||
locale_path = self.resources / "units" / "locales" / f"{locale}.json"
|
||||
@classmethod
|
||||
def get_file(cls, locale: str | None = None) -> pathlib.Path:
|
||||
locale_path = cls.resources / "units" / "locales" / f"{locale}.json"
|
||||
return locale_path if locale_path.exists() else units.en_US
|
||||
|
||||
def get_all_units(self) -> list[IngredientUnit]:
|
||||
@@ -64,7 +65,7 @@ class IngredientUnitsSeeder(AbstractSeeder):
|
||||
file = self.get_file(locale)
|
||||
|
||||
seen_unit_names = {unit.name for unit in self.get_all_units()}
|
||||
for unit in json.loads(file.read_text(encoding="utf-8")).values():
|
||||
for unit in self.load_file(file).values():
|
||||
if unit["name"] in seen_unit_names:
|
||||
continue
|
||||
|
||||
@@ -88,8 +89,9 @@ class IngredientUnitsSeeder(AbstractSeeder):
|
||||
|
||||
|
||||
class IngredientFoodsSeeder(AbstractSeeder):
|
||||
def get_file(self, locale: str | None = None) -> pathlib.Path:
|
||||
locale_path = self.resources / "foods" / "locales" / f"{locale}.json"
|
||||
@classmethod
|
||||
def get_file(cls, locale: str | None = None) -> pathlib.Path:
|
||||
locale_path = cls.resources / "foods" / "locales" / f"{locale}.json"
|
||||
return locale_path if locale_path.exists() else foods.en_US
|
||||
|
||||
def get_label(self, value: str) -> MultiPurposeLabelOut | None:
|
||||
@@ -103,7 +105,7 @@ class IngredientFoodsSeeder(AbstractSeeder):
|
||||
|
||||
# get all current unique foods
|
||||
seen_foods_names = {food.name for food in self.get_all_foods()}
|
||||
for label, values in json.loads(file.read_text(encoding="utf-8")).items():
|
||||
for label, values in self.load_file(file).items():
|
||||
label_out = self.get_label(label)
|
||||
|
||||
for food_name, attributes in values["foods"].items():
|
||||
|
||||
@@ -67,6 +67,7 @@ from .recipe_ingredient import (
|
||||
RegisteredParser,
|
||||
SaveIngredientFood,
|
||||
SaveIngredientUnit,
|
||||
StandardizedUnitType,
|
||||
UnitFoodBase,
|
||||
)
|
||||
from .recipe_notes import RecipeNote
|
||||
@@ -159,6 +160,7 @@ __all__ = [
|
||||
"RegisteredParser",
|
||||
"SaveIngredientFood",
|
||||
"SaveIngredientUnit",
|
||||
"StandardizedUnitType",
|
||||
"UnitFoodBase",
|
||||
"RecipeSuggestionQuery",
|
||||
"RecipeSuggestionResponse",
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import enum
|
||||
from enum import StrEnum
|
||||
from fractions import Fraction
|
||||
from typing import ClassVar
|
||||
from uuid import UUID, uuid4
|
||||
@@ -34,6 +35,28 @@ def display_fraction(fraction: Fraction):
|
||||
)
|
||||
|
||||
|
||||
class StandardizedUnitType(StrEnum):
|
||||
"""
|
||||
An arbitrary list of standardized units supported by unit conversions.
|
||||
The backend doesn't really care what standardized unit you use, as long as it's recognized,
|
||||
but defining them here keeps it consistant with the frontend.
|
||||
"""
|
||||
|
||||
# Imperial
|
||||
FLUID_OUNCE = "fluid_ounce"
|
||||
CUP = "cup"
|
||||
|
||||
OUNCE = "ounce"
|
||||
POUND = "pound"
|
||||
|
||||
# Metric
|
||||
MILLILITER = "milliliter"
|
||||
LITER = "liter"
|
||||
|
||||
GRAM = "gram"
|
||||
KILOGRAM = "kilogram"
|
||||
|
||||
|
||||
class UnitFoodBase(MealieModel):
|
||||
id: UUID4 | None = None
|
||||
name: str
|
||||
@@ -109,9 +132,6 @@ class IngredientFood(CreateIngredientFood):
|
||||
except AttributeError:
|
||||
return v
|
||||
|
||||
def is_on_hand(self, household_slug: str) -> bool:
|
||||
return household_slug in self.households_with_tool
|
||||
|
||||
|
||||
class IngredientFoodPagination(PaginationBase):
|
||||
items: list[IngredientFood]
|
||||
@@ -130,7 +150,21 @@ class CreateIngredientUnit(UnitFoodBase):
|
||||
abbreviation: str = ""
|
||||
plural_abbreviation: str | None = ""
|
||||
use_abbreviation: bool = False
|
||||
|
||||
aliases: list[CreateIngredientUnitAlias] = []
|
||||
standard_quantity: float | None = None
|
||||
standard_unit: str | None = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_standardization_fields(self):
|
||||
# If one is set, the other must be set.
|
||||
# If quantity is <= 0, it's considered not set.
|
||||
if not self.standard_unit:
|
||||
self.standard_quantity = self.standard_unit = None
|
||||
elif not ((self.standard_quantity or 0) > 0):
|
||||
self.standard_quantity = self.standard_unit = None
|
||||
|
||||
return self
|
||||
|
||||
|
||||
class SaveIngredientUnit(CreateIngredientUnit):
|
||||
|
||||
@@ -32,9 +32,6 @@ class RecipeToolOut(RecipeToolCreate):
|
||||
except AttributeError:
|
||||
return v
|
||||
|
||||
def is_on_hand(self, household_slug: str) -> bool:
|
||||
return household_slug in self.households_with_tool
|
||||
|
||||
@classmethod
|
||||
def loader_options(cls) -> list[LoaderOption]:
|
||||
return [
|
||||
|
||||
@@ -28,6 +28,7 @@ from mealie.schema.recipe.recipe_ingredient import (
|
||||
)
|
||||
from mealie.schema.response.pagination import OrderDirection, PaginationQuery
|
||||
from mealie.services.parser_services._base import DataMatcher
|
||||
from mealie.services.parser_services.parser_utils import UnitConverter, merge_quantity_and_unit
|
||||
|
||||
|
||||
class ShoppingListService:
|
||||
@@ -41,8 +42,7 @@ class ShoppingListService:
|
||||
self.list_refs = repos.group_shopping_list_recipe_refs
|
||||
self.data_matcher = DataMatcher(self.repos, food_fuzzy_match_threshold=self.DEFAULT_FOOD_FUZZY_MATCH_THRESHOLD)
|
||||
|
||||
@staticmethod
|
||||
def can_merge(item1: ShoppingListItemBase, item2: ShoppingListItemBase) -> bool:
|
||||
def can_merge(self, item1: ShoppingListItemBase, item2: ShoppingListItemBase) -> bool:
|
||||
"""Check to see if this item can be merged with another item"""
|
||||
|
||||
if any(
|
||||
@@ -50,16 +50,28 @@ class ShoppingListService:
|
||||
item1.checked,
|
||||
item2.checked,
|
||||
item1.food_id != item2.food_id,
|
||||
item1.unit_id != item2.unit_id,
|
||||
]
|
||||
):
|
||||
return False
|
||||
|
||||
# check if units match or if they're compatable
|
||||
if item1.unit_id != item2.unit_id:
|
||||
item1_unit = item1.unit or self.data_matcher.units_by_id.get(item1.unit_id)
|
||||
item2_unit = item2.unit or self.data_matcher.units_by_id.get(item2.unit_id)
|
||||
if not (item1_unit and item1_unit.standard_unit):
|
||||
return False
|
||||
if not (item2_unit and item2_unit.standard_unit):
|
||||
return False
|
||||
|
||||
uc = UnitConverter()
|
||||
if not uc.can_convert(item1_unit.standard_unit, item2_unit.standard_unit):
|
||||
return False
|
||||
|
||||
# if foods match, we can merge, otherwise compare the notes
|
||||
return bool(item1.food_id) or item1.note == item2.note
|
||||
|
||||
@staticmethod
|
||||
def merge_items(
|
||||
self,
|
||||
from_item: ShoppingListItemCreate | ShoppingListItemUpdateBulk,
|
||||
to_item: ShoppingListItemCreate | ShoppingListItemUpdateBulk | ShoppingListItemOut,
|
||||
) -> ShoppingListItemUpdate:
|
||||
@@ -69,7 +81,20 @@ class ShoppingListService:
|
||||
Attributes of the `to_item` take priority over the `from_item`, except extras with overlapping keys
|
||||
"""
|
||||
|
||||
to_item.quantity += from_item.quantity
|
||||
to_item_unit = to_item.unit or self.data_matcher.units_by_id.get(to_item.unit_id)
|
||||
from_item_unit = from_item.unit or self.data_matcher.units_by_id.get(from_item.unit_id)
|
||||
if to_item_unit and to_item_unit.standard_unit and from_item_unit and from_item_unit.standard_unit:
|
||||
merged_qty, merged_unit = merge_quantity_and_unit(
|
||||
from_item.quantity or 0, from_item_unit, to_item.quantity or 0, to_item_unit
|
||||
)
|
||||
to_item.quantity = merged_qty
|
||||
to_item.unit_id = merged_unit.id
|
||||
to_item.unit = merged_unit
|
||||
|
||||
else:
|
||||
# No conversion needed, just sum the quantities
|
||||
to_item.quantity += from_item.quantity
|
||||
|
||||
if to_item.note != from_item.note:
|
||||
to_item.note = " | ".join([note for note in [to_item.note, from_item.note] if note])
|
||||
|
||||
|
||||
@@ -28,18 +28,38 @@ class DataMatcher:
|
||||
|
||||
self._food_fuzzy_match_threshold = food_fuzzy_match_threshold
|
||||
self._unit_fuzzy_match_threshold = unit_fuzzy_match_threshold
|
||||
|
||||
self._foods_by_id: dict[UUID4, IngredientFood] | None = None
|
||||
self._units_by_id: dict[UUID4, IngredientUnit] | None = None
|
||||
|
||||
self._foods_by_alias: dict[str, IngredientFood] | None = None
|
||||
self._units_by_alias: dict[str, IngredientUnit] | None = None
|
||||
|
||||
@property
|
||||
def foods_by_alias(self) -> dict[str, IngredientFood]:
|
||||
if self._foods_by_alias is None:
|
||||
def foods_by_id(self) -> dict[UUID4, IngredientFood]:
|
||||
if self._foods_by_id is None:
|
||||
foods_repo = self.repos.ingredient_foods
|
||||
query = PaginationQuery(page=1, per_page=-1)
|
||||
all_foods = foods_repo.page_all(query).items
|
||||
self._foods_by_id = {food.id: food for food in all_foods}
|
||||
|
||||
return self._foods_by_id
|
||||
|
||||
@property
|
||||
def units_by_id(self) -> dict[UUID4, IngredientUnit]:
|
||||
if self._units_by_id is None:
|
||||
units_repo = self.repos.ingredient_units
|
||||
query = PaginationQuery(page=1, per_page=-1)
|
||||
all_units = units_repo.page_all(query).items
|
||||
self._units_by_id = {unit.id: unit for unit in all_units}
|
||||
|
||||
return self._units_by_id
|
||||
|
||||
@property
|
||||
def foods_by_alias(self) -> dict[str, IngredientFood]:
|
||||
if self._foods_by_alias is None:
|
||||
foods_by_alias: dict[str, IngredientFood] = {}
|
||||
for food in all_foods:
|
||||
for food in self.foods_by_id.values():
|
||||
if food.name:
|
||||
foods_by_alias[IngredientFoodModel.normalize(food.name)] = food
|
||||
if food.plural_name:
|
||||
@@ -56,12 +76,8 @@ class DataMatcher:
|
||||
@property
|
||||
def units_by_alias(self) -> dict[str, IngredientUnit]:
|
||||
if self._units_by_alias is None:
|
||||
units_repo = self.repos.ingredient_units
|
||||
query = PaginationQuery(page=1, per_page=-1)
|
||||
all_units = units_repo.page_all(query).items
|
||||
|
||||
units_by_alias: dict[str, IngredientUnit] = {}
|
||||
for unit in all_units:
|
||||
for unit in self.units_by_id.values():
|
||||
if unit.name:
|
||||
units_by_alias[IngredientUnitModel.normalize(unit.name)] = unit
|
||||
if unit.plural_name:
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
from .string_utils import *
|
||||
from .unit_utils import *
|
||||
|
||||
146
mealie/services/parser_services/parser_utils/unit_utils.py
Normal file
146
mealie/services/parser_services/parser_utils/unit_utils.py
Normal file
@@ -0,0 +1,146 @@
|
||||
from typing import TYPE_CHECKING, Literal, overload
|
||||
|
||||
from pint import Quantity, Unit, UnitRegistry
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mealie.schema.recipe.recipe_ingredient import CreateIngredientUnit
|
||||
|
||||
|
||||
class UnitNotFound(Exception):
|
||||
"""Raised when trying to access a unit not found in the unit registry."""
|
||||
|
||||
def __init__(self, message: str = "Unit not found in unit registry"):
|
||||
self.message = message
|
||||
super().__init__(self.message)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.message}"
|
||||
|
||||
|
||||
class UnitConverter:
|
||||
def __init__(self):
|
||||
self.ureg = UnitRegistry()
|
||||
|
||||
def _resolve_ounce(self, unit_1: Unit, unit_2: Unit) -> tuple[Unit, Unit]:
|
||||
"""
|
||||
Often times "ounce" is used in place of "fluid ounce" in recipes.
|
||||
When trying to convert/combine ounces with a volume, we can assume it should have been a fluid ounce.
|
||||
This function will convert ounces to fluid ounces if the other unit is a volume.
|
||||
"""
|
||||
|
||||
OUNCE = self.ureg("ounce")
|
||||
FL_OUNCE = self.ureg("fluid_ounce")
|
||||
VOLUME = "[length] ** 3"
|
||||
|
||||
if unit_1 == OUNCE and unit_2.dimensionality == VOLUME:
|
||||
return FL_OUNCE, unit_2
|
||||
if unit_2 == OUNCE and unit_1.dimensionality == VOLUME:
|
||||
return unit_1, FL_OUNCE
|
||||
|
||||
return unit_1, unit_2
|
||||
|
||||
@overload
|
||||
def parse(self, unit: str | Unit, strict: Literal[False] = False) -> str | Unit: ...
|
||||
|
||||
@overload
|
||||
def parse(self, unit: str | Unit, strict: Literal[True]) -> Unit: ...
|
||||
|
||||
def parse(self, unit: str | Unit, strict: bool = False) -> str | Unit:
|
||||
"""
|
||||
Parse a string unit into a pint.Unit.
|
||||
|
||||
If strict is False (default), returns a pint.Unit if it exists, otherwise returns the original string.
|
||||
If strict is True, raises UnitNotFound instead of returning a string.
|
||||
If the input is already a parsed pint.Unit, returns it as-is.
|
||||
"""
|
||||
if isinstance(unit, Unit):
|
||||
return unit
|
||||
|
||||
try:
|
||||
return self.ureg(unit).units
|
||||
except Exception as e:
|
||||
if strict:
|
||||
raise UnitNotFound(f"Unit '{unit}' not found in unit registry") from e
|
||||
return unit
|
||||
|
||||
def can_convert(self, unit: str | Unit, to_unit: str | Unit) -> bool:
|
||||
"""Whether or not a given unit can be converted into another unit."""
|
||||
|
||||
unit = self.parse(unit)
|
||||
to_unit = self.parse(to_unit)
|
||||
|
||||
if not (isinstance(unit, Unit) and isinstance(to_unit, Unit)):
|
||||
return False
|
||||
|
||||
unit, to_unit = self._resolve_ounce(unit, to_unit)
|
||||
return unit.is_compatible_with(to_unit)
|
||||
|
||||
def convert(self, quantity: float, unit: str | Unit, to_unit: str | Unit) -> tuple[float, Unit]:
|
||||
"""
|
||||
Convert a quantity and a unit into another unit.
|
||||
|
||||
Returns tuple[quantity, unit]
|
||||
"""
|
||||
|
||||
unit = self.parse(unit, strict=True)
|
||||
to_unit = self.parse(to_unit, strict=True)
|
||||
unit, to_unit = self._resolve_ounce(unit, to_unit)
|
||||
|
||||
qty = quantity * unit
|
||||
converted = qty.to(to_unit)
|
||||
return float(converted.magnitude), converted.units
|
||||
|
||||
def merge(self, quantity_1: float, unit_1: str | Unit, quantity_2: float, unit_2: str | Unit) -> tuple[float, Unit]:
|
||||
"""Merge two quantities together"""
|
||||
|
||||
unit_1 = self.parse(unit_1, strict=True)
|
||||
unit_2 = self.parse(unit_2, strict=True)
|
||||
unit_1, unit_2 = self._resolve_ounce(unit_1, unit_2)
|
||||
|
||||
q1 = quantity_1 * unit_1
|
||||
q2 = quantity_2 * unit_2
|
||||
|
||||
out: Quantity = q1 + q2
|
||||
return float(out.magnitude), out.units
|
||||
|
||||
|
||||
def merge_quantity_and_unit[T: CreateIngredientUnit](
|
||||
qty_1: float, unit_1: T, qty_2: float, unit_2: T
|
||||
) -> tuple[float, T]:
|
||||
"""
|
||||
Merge a quantity and unit.
|
||||
|
||||
Returns tuple[quantity, unit]
|
||||
"""
|
||||
|
||||
if not (unit_1.standard_quantity and unit_1.standard_unit and unit_2.standard_quantity and unit_2.standard_unit):
|
||||
raise ValueError("Both units must contain standardized unit data")
|
||||
|
||||
PINT_UNIT_1_TXT = "_mealie_unit_1"
|
||||
PINT_UNIT_2_TXT = "_mealie_unit_2"
|
||||
|
||||
uc = UnitConverter()
|
||||
|
||||
# pre-process units to account for ounce -> fluid_ounce conversion
|
||||
unit_1_standard = uc.parse(unit_1.standard_unit, strict=True)
|
||||
unit_2_standard = uc.parse(unit_2.standard_unit, strict=True)
|
||||
unit_1_standard, unit_2_standard = uc._resolve_ounce(unit_1_standard, unit_2_standard)
|
||||
|
||||
# create custon unit definition so pint can handle them natively
|
||||
uc.ureg.define(f"{PINT_UNIT_1_TXT} = {unit_1.standard_quantity} * {unit_1_standard}")
|
||||
uc.ureg.define(f"{PINT_UNIT_2_TXT} = {unit_2.standard_quantity} * {unit_2_standard}")
|
||||
|
||||
pint_unit_1 = uc.parse(PINT_UNIT_1_TXT)
|
||||
pint_unit_2 = uc.parse(PINT_UNIT_2_TXT)
|
||||
|
||||
merged_q, merged_u = uc.merge(qty_1, pint_unit_1, qty_2, pint_unit_2)
|
||||
|
||||
# Convert to the bigger unit if quantity >= 1, else the smaller unit
|
||||
merged_q, merged_u = uc.convert(merged_q, merged_u, max(pint_unit_1, pint_unit_2))
|
||||
if abs(merged_q) < 1:
|
||||
merged_q, merged_u = uc.convert(merged_q, merged_u, min(pint_unit_1, pint_unit_2))
|
||||
|
||||
if str(merged_u) == PINT_UNIT_1_TXT:
|
||||
return merged_q, unit_1
|
||||
else:
|
||||
return merged_q, unit_2
|
||||
@@ -17,7 +17,7 @@ dependencies = [
|
||||
"apprise==1.9.7",
|
||||
"bcrypt==5.0.0",
|
||||
"extruct==0.18.0",
|
||||
"fastapi==0.129.0",
|
||||
"fastapi==0.129.1",
|
||||
"httpx==0.28.1",
|
||||
"lxml==6.0.2",
|
||||
"orjson==3.11.7",
|
||||
@@ -39,13 +39,14 @@ dependencies = [
|
||||
"authlib==1.6.8",
|
||||
"html2text==2025.4.15",
|
||||
"paho-mqtt==1.6.1",
|
||||
"pydantic-settings==2.13.0",
|
||||
"pillow-heif==1.2.0",
|
||||
"pydantic-settings==2.13.1",
|
||||
"pillow-heif==1.2.1",
|
||||
"pyjwt==2.11.0",
|
||||
"openai==2.21.0",
|
||||
"typing-extensions==4.15.0",
|
||||
"itsdangerous==2.2.0",
|
||||
"ingredient-parser-nlp==2.5.0",
|
||||
"pint>=0.25",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
@@ -58,19 +59,19 @@ pgsql = [
|
||||
|
||||
[dependency-groups]
|
||||
docs = [
|
||||
"mkdocs-material==9.7.1",
|
||||
"mkdocs-material==9.7.2",
|
||||
]
|
||||
dev = [
|
||||
"coverage==7.13.4",
|
||||
"coveragepy-lcov==0.1.2",
|
||||
"mkdocs-material==9.7.1",
|
||||
"mkdocs-material==9.7.2",
|
||||
"mypy==1.19.1",
|
||||
"pre-commit==4.5.1",
|
||||
"pylint==4.0.4",
|
||||
"pylint==4.0.5",
|
||||
"pytest==9.0.2",
|
||||
"pytest-asyncio==1.3.0",
|
||||
"rich==14.3.2",
|
||||
"ruff==0.15.1",
|
||||
"rich==14.3.3",
|
||||
"ruff==0.15.2",
|
||||
"types-PyYAML==6.0.12.20250915",
|
||||
"types-python-dateutil==2.9.0.20260124",
|
||||
"types-python-slugify==8.0.2.20240310",
|
||||
|
||||
@@ -4,6 +4,9 @@ CWD = Path(__file__).parent
|
||||
|
||||
locale_dir = CWD / "locale"
|
||||
|
||||
backup_version_1d9a002d7234_1 = CWD / "backups/backup-version-1d9a002d7234-1.zip"
|
||||
"""1d9a002d7234: add referenced_recipe to ingredients"""
|
||||
|
||||
backup_version_44e8d670719d_1 = CWD / "backups/backup-version-44e8d670719d-1.zip"
|
||||
"""44e8d670719d: add extras to shopping lists, list items, and ingredient foods"""
|
||||
|
||||
|
||||
BIN
tests/data/backups/backup-version-1d9a002d7234-1.zip
Normal file
BIN
tests/data/backups/backup-version-1d9a002d7234-1.zip
Normal file
Binary file not shown.
@@ -15,14 +15,12 @@ def test_seed_foods(api_client: TestClient, unique_user: TestUser):
|
||||
CREATED_FOODS = 2687
|
||||
database = unique_user.repos
|
||||
|
||||
# Check that the foods was created
|
||||
foods = database.ingredient_foods.page_all(PaginationQuery(page=1, per_page=-1)).items
|
||||
assert len(foods) == 0
|
||||
|
||||
resp = api_client.post(api_routes.groups_seeders_foods, json={"locale": "en-US"}, headers=unique_user.token)
|
||||
assert resp.status_code == 200
|
||||
|
||||
# Check that the foods was created
|
||||
foods = database.ingredient_foods.page_all(PaginationQuery(page=1, per_page=-1)).items
|
||||
assert len(foods) == CREATED_FOODS
|
||||
|
||||
@@ -31,29 +29,37 @@ def test_seed_units(api_client: TestClient, unique_user: TestUser):
|
||||
CREATED_UNITS = 24
|
||||
database = unique_user.repos
|
||||
|
||||
# Check that the foods was created
|
||||
units = database.ingredient_units.page_all(PaginationQuery(page=1, per_page=-1)).items
|
||||
assert len(units) == 0
|
||||
|
||||
resp = api_client.post(api_routes.groups_seeders_units, json={"locale": "en-US"}, headers=unique_user.token)
|
||||
assert resp.status_code == 200
|
||||
|
||||
# Check that the foods was created
|
||||
units = database.ingredient_units.page_all(PaginationQuery(page=1, per_page=-1)).items
|
||||
assert len(units) == CREATED_UNITS
|
||||
|
||||
# Check that the "pint" unit was created and includes standardized data
|
||||
pint_found = False
|
||||
for unit in units:
|
||||
if unit.name != "pint":
|
||||
continue
|
||||
|
||||
pint_found = True
|
||||
assert unit.standard_quantity == 2
|
||||
assert unit.standard_unit == "cup"
|
||||
|
||||
assert pint_found
|
||||
|
||||
|
||||
def test_seed_labels(api_client: TestClient, unique_user: TestUser):
|
||||
CREATED_LABELS = 32
|
||||
database = unique_user.repos
|
||||
|
||||
# Check that the foods was created
|
||||
labels = database.group_multi_purpose_labels.page_all(PaginationQuery(page=1, per_page=-1)).items
|
||||
assert len(labels) == 0
|
||||
|
||||
resp = api_client.post(api_routes.groups_seeders_labels, json={"locale": "en-US"}, headers=unique_user.token)
|
||||
assert resp.status_code == 200
|
||||
|
||||
# Check that the foods was created
|
||||
labels = database.group_multi_purpose_labels.page_all(PaginationQuery(page=1, per_page=-1)).items
|
||||
assert len(labels) == CREATED_LABELS
|
||||
|
||||
@@ -7,7 +7,7 @@ from fastapi.testclient import TestClient
|
||||
from pydantic import UUID4
|
||||
|
||||
from mealie.schema.household.group_shopping_list import ShoppingListItemOut, ShoppingListOut
|
||||
from mealie.schema.recipe.recipe_ingredient import SaveIngredientFood
|
||||
from mealie.schema.recipe.recipe_ingredient import IngredientUnit, SaveIngredientFood
|
||||
from tests import utils
|
||||
from tests.utils import api_routes
|
||||
from tests.utils.factories import random_int, random_string
|
||||
@@ -641,6 +641,96 @@ def test_shopping_list_items_with_zero_quantity(
|
||||
assert len(as_json["listItems"]) == len(normal_items + zero_qty_items) - 1
|
||||
|
||||
|
||||
def test_shopping_list_merge_standard_unit(
|
||||
api_client: TestClient, unique_user: TestUser, shopping_list: ShoppingListOut
|
||||
):
|
||||
unit_1_cup_data = {"name": random_string(), "standardQuantity": 1, "standardUnit": "cup"}
|
||||
unit_2_cup_data = {"name": random_string(), "standardQuantity": 2, "standardUnit": "cup"}
|
||||
unit_1_out = api_client.post(api_routes.units, json=unit_1_cup_data, headers=unique_user.token)
|
||||
unit_2_out = api_client.post(api_routes.units, json=unit_2_cup_data, headers=unique_user.token)
|
||||
|
||||
unit_1 = IngredientUnit.model_validate(unit_1_out.json())
|
||||
unit_2 = IngredientUnit.model_validate(unit_2_out.json())
|
||||
|
||||
list_item_1_data = create_item(shopping_list.id, unit_id=str(unit_1.id), note="mealie-food")
|
||||
list_item_2_data = create_item(shopping_list.id, unit_id=str(unit_2.id), note="mealie-food")
|
||||
response = api_client.post(
|
||||
api_routes.households_shopping_items_create_bulk,
|
||||
json=[list_item_1_data, list_item_2_data],
|
||||
headers=unique_user.token,
|
||||
)
|
||||
|
||||
as_json = utils.assert_deserialize(response, 201)
|
||||
assert len(as_json["createdItems"]) == 1
|
||||
|
||||
item_out = as_json["createdItems"][0]
|
||||
|
||||
# should use larger "2 cup" unit (a la "pint")
|
||||
assert item_out["unitId"] == str(unit_2.id)
|
||||
# calculate quantity by summing base "cup" amount and dividing by 2 (a la pints)
|
||||
assert item_out["quantity"] == (list_item_1_data["quantity"] + (list_item_2_data["quantity"] * 2)) / 2
|
||||
|
||||
|
||||
def test_shopping_list_merge_standard_unit_different_foods(
|
||||
api_client: TestClient, unique_user: TestUser, shopping_list: ShoppingListOut
|
||||
):
|
||||
unit_1_cup_data = {"name": random_string(), "standardQuantity": 1, "standardUnit": "cup"}
|
||||
unit_2_cup_data = {"name": random_string(), "standardQuantity": 2, "standardUnit": "cup"}
|
||||
unit_1_out = api_client.post(api_routes.units, json=unit_1_cup_data, headers=unique_user.token)
|
||||
unit_2_out = api_client.post(api_routes.units, json=unit_2_cup_data, headers=unique_user.token)
|
||||
|
||||
unit_1 = IngredientUnit.model_validate(unit_1_out.json())
|
||||
unit_2 = IngredientUnit.model_validate(unit_2_out.json())
|
||||
|
||||
list_item_1_data = create_item(shopping_list.id, unit_id=str(unit_1.id), note="mealie-food-1")
|
||||
list_item_2_data = create_item(shopping_list.id, unit_id=str(unit_2.id), note="mealie-food-2")
|
||||
response = api_client.post(
|
||||
api_routes.households_shopping_items_create_bulk,
|
||||
json=[list_item_1_data, list_item_2_data],
|
||||
headers=unique_user.token,
|
||||
)
|
||||
|
||||
as_json = utils.assert_deserialize(response, 201)
|
||||
assert len(as_json["createdItems"]) == 2
|
||||
for in_data, out_data in zip(
|
||||
[list_item_1_data, list_item_2_data], [as_json["createdItems"][0], as_json["createdItems"][1]], strict=True
|
||||
):
|
||||
assert in_data["quantity"] == out_data["quantity"]
|
||||
assert out_data["unit"]
|
||||
assert in_data["unit_id"] == out_data["unit"]["id"]
|
||||
assert in_data["note"] == out_data["note"]
|
||||
|
||||
|
||||
def test_shopping_list_merge_standard_unit_incompatible_units(
|
||||
api_client: TestClient, unique_user: TestUser, shopping_list: ShoppingListOut
|
||||
):
|
||||
unit_1_data = {"name": random_string(), "standardQuantity": 1, "standardUnit": "cup"}
|
||||
unit_2_data = {"name": random_string(), "standardQuantity": 2, "standardUnit": "gram"}
|
||||
unit_1_out = api_client.post(api_routes.units, json=unit_1_data, headers=unique_user.token)
|
||||
unit_2_out = api_client.post(api_routes.units, json=unit_2_data, headers=unique_user.token)
|
||||
|
||||
unit_1 = IngredientUnit.model_validate(unit_1_out.json())
|
||||
unit_2 = IngredientUnit.model_validate(unit_2_out.json())
|
||||
|
||||
list_item_1_data = create_item(shopping_list.id, unit_id=str(unit_1.id), note="mealie-food")
|
||||
list_item_2_data = create_item(shopping_list.id, unit_id=str(unit_2.id), note="mealie-food")
|
||||
response = api_client.post(
|
||||
api_routes.households_shopping_items_create_bulk,
|
||||
json=[list_item_1_data, list_item_2_data],
|
||||
headers=unique_user.token,
|
||||
)
|
||||
|
||||
as_json = utils.assert_deserialize(response, 201)
|
||||
assert len(as_json["createdItems"]) == 2
|
||||
for in_data, out_data in zip(
|
||||
[list_item_1_data, list_item_2_data], [as_json["createdItems"][0], as_json["createdItems"][1]], strict=True
|
||||
):
|
||||
assert in_data["quantity"] == out_data["quantity"]
|
||||
assert out_data["unit"]
|
||||
assert in_data["unit_id"] == out_data["unit"]["id"]
|
||||
assert in_data["note"] == out_data["note"]
|
||||
|
||||
|
||||
def test_shopping_list_item_extras(
|
||||
api_client: TestClient, unique_user: TestUser, shopping_list: ShoppingListOut
|
||||
) -> None:
|
||||
|
||||
@@ -2,7 +2,7 @@ import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from mealie.schema.recipe.recipe_ingredient import RegisteredParser
|
||||
from tests.unit_tests.test_ingredient_parser import TestIngredient
|
||||
from tests.unit_tests.ingredient_parser.test_ingredient_parser import TestIngredient
|
||||
from tests.utils import api_routes
|
||||
from tests.utils.fixture_schemas import TestUser
|
||||
|
||||
|
||||
309
tests/unit_tests/ingredient_parser/test_unit_utils.py
Normal file
309
tests/unit_tests/ingredient_parser/test_unit_utils.py
Normal file
@@ -0,0 +1,309 @@
|
||||
import pint
|
||||
import pytest
|
||||
|
||||
from mealie.schema.recipe.recipe_ingredient import CreateIngredientUnit
|
||||
from mealie.services.parser_services.parser_utils import UnitConverter, UnitNotFound, merge_quantity_and_unit
|
||||
from tests.utils import random_string
|
||||
|
||||
|
||||
def test_uc_parse_string():
|
||||
uc = UnitConverter()
|
||||
parsed = uc.parse("cup")
|
||||
|
||||
assert isinstance(parsed, pint.Unit)
|
||||
assert (str(parsed)) == "cup"
|
||||
|
||||
|
||||
def test_uc_parse_unit():
|
||||
uc = UnitConverter()
|
||||
parsed = uc.parse(uc.parse("cup"))
|
||||
|
||||
assert isinstance(parsed, pint.Unit)
|
||||
assert (str(parsed)) == "cup"
|
||||
|
||||
|
||||
def test_uc_parse_invalid():
|
||||
uc = UnitConverter()
|
||||
input_str = random_string()
|
||||
parsed = uc.parse(input_str)
|
||||
|
||||
assert not isinstance(parsed, pint.Unit)
|
||||
assert parsed == input_str
|
||||
|
||||
|
||||
def test_uc_parse_invalid_strict():
|
||||
uc = UnitConverter()
|
||||
input_str = random_string()
|
||||
|
||||
with pytest.raises(UnitNotFound):
|
||||
uc.parse(input_str, strict=True)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("pre_parse_1", [True, False])
|
||||
@pytest.mark.parametrize("pre_parse_2", [True, False])
|
||||
def test_can_convert(pre_parse_1: bool, pre_parse_2: bool):
|
||||
unit_1 = "cup"
|
||||
unit_2 = "pint"
|
||||
|
||||
uc = UnitConverter()
|
||||
if pre_parse_1:
|
||||
unit_1 = uc.parse(unit_1)
|
||||
if pre_parse_2:
|
||||
unit_2 = uc.parse(unit_2)
|
||||
|
||||
assert uc.can_convert(unit_1, unit_2)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("pre_parse_1", [True, False])
|
||||
@pytest.mark.parametrize("pre_parse_2", [True, False])
|
||||
def test_cannot_convert(pre_parse_1: bool, pre_parse_2: bool):
|
||||
unit_1 = "cup"
|
||||
unit_2 = "pound"
|
||||
|
||||
uc = UnitConverter()
|
||||
if pre_parse_1:
|
||||
unit_1 = uc.parse(unit_1)
|
||||
if pre_parse_2:
|
||||
unit_2 = uc.parse(unit_2)
|
||||
|
||||
assert not uc.can_convert(unit_1, unit_2)
|
||||
|
||||
|
||||
def test_cannot_convert_invalid_unit():
|
||||
uc = UnitConverter()
|
||||
assert not uc.can_convert("cup", random_string())
|
||||
assert not uc.can_convert(random_string(), "cup")
|
||||
|
||||
|
||||
def test_can_convert_same_unit():
|
||||
uc = UnitConverter()
|
||||
assert uc.can_convert("cup", "cup")
|
||||
|
||||
|
||||
def test_can_convert_volume_ounce():
|
||||
uc = UnitConverter()
|
||||
assert uc.can_convert("ounce", "cup")
|
||||
assert uc.can_convert("cup", "ounce")
|
||||
|
||||
|
||||
def test_convert_simple():
|
||||
uc = UnitConverter()
|
||||
quantity, unit = uc.convert(1, "cup", "pint")
|
||||
|
||||
assert isinstance(unit, pint.Unit)
|
||||
assert str(unit) == "pint"
|
||||
assert quantity == 1 / 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("pre_parse_1", [True, False])
|
||||
@pytest.mark.parametrize("pre_parse_2", [True, False])
|
||||
def test_convert_pre_parsed(pre_parse_1: bool, pre_parse_2: bool):
|
||||
unit_1 = "cup"
|
||||
unit_2 = "pint"
|
||||
|
||||
uc = UnitConverter()
|
||||
if pre_parse_1:
|
||||
unit_1 = uc.parse(unit_1)
|
||||
if pre_parse_2:
|
||||
unit_2 = uc.parse(unit_2)
|
||||
|
||||
quantity, unit = uc.convert(1, unit_1, unit_2)
|
||||
assert isinstance(unit, pint.Unit)
|
||||
assert str(unit) == "pint"
|
||||
assert quantity == 1 / 2
|
||||
|
||||
|
||||
def test_convert_weight():
|
||||
uc = UnitConverter()
|
||||
quantity, unit = uc.convert(16, "ounce", "pound")
|
||||
|
||||
assert isinstance(unit, pint.Unit)
|
||||
assert str(unit) == "pound"
|
||||
assert quantity == 1
|
||||
|
||||
|
||||
def test_convert_zero_quantity():
|
||||
uc = UnitConverter()
|
||||
quantity, unit = uc.convert(0, "cup", "pint")
|
||||
|
||||
assert isinstance(unit, pint.Unit)
|
||||
assert quantity == 0
|
||||
|
||||
|
||||
def test_convert_invalid_unit():
|
||||
uc = UnitConverter()
|
||||
|
||||
with pytest.raises(UnitNotFound):
|
||||
uc.convert(1, "pound", random_string())
|
||||
|
||||
|
||||
def test_convert_incompatible_units():
|
||||
uc = UnitConverter()
|
||||
|
||||
with pytest.raises(pint.errors.DimensionalityError):
|
||||
uc.convert(1, "pound", "cup")
|
||||
|
||||
|
||||
def test_convert_volume_ounce():
|
||||
uc = UnitConverter()
|
||||
quantity, unit = uc.convert(8, "ounce", "cup")
|
||||
|
||||
assert isinstance(unit, pint.Unit)
|
||||
assert str(unit) == "cup"
|
||||
assert quantity == 1
|
||||
|
||||
|
||||
def test_merge_same_unit():
|
||||
uc = UnitConverter()
|
||||
quantity, unit = uc.merge(1, "cup", 2, "cup")
|
||||
|
||||
assert isinstance(unit, pint.Unit)
|
||||
assert str(unit) == "cup"
|
||||
assert quantity == 3
|
||||
|
||||
|
||||
@pytest.mark.parametrize("pre_parse_1", [True, False])
|
||||
@pytest.mark.parametrize("pre_parse_2", [True, False])
|
||||
def test_merge_compatible_units(pre_parse_1: bool, pre_parse_2: bool):
|
||||
unit_1 = "cup"
|
||||
unit_2 = "pint"
|
||||
|
||||
uc = UnitConverter()
|
||||
if pre_parse_1:
|
||||
unit_1 = uc.parse(unit_1)
|
||||
if pre_parse_2:
|
||||
unit_2 = uc.parse(unit_2)
|
||||
|
||||
quantity, unit = uc.merge(1, unit_1, 1, unit_2)
|
||||
assert isinstance(unit, pint.Unit)
|
||||
# 1 cup + 1 pint = 1 cup + 2 cups = 3 cups
|
||||
assert quantity == 3
|
||||
|
||||
|
||||
def test_merge_weight_units():
|
||||
uc = UnitConverter()
|
||||
quantity, unit = uc.merge(8, "ounce", 8, "ounce")
|
||||
|
||||
assert isinstance(unit, pint.Unit)
|
||||
assert str(unit) == "ounce"
|
||||
assert quantity == 16
|
||||
|
||||
|
||||
def test_merge_different_weight_units():
|
||||
uc = UnitConverter()
|
||||
quantity, unit = uc.merge(1, "pound", 8, "ounce")
|
||||
|
||||
assert isinstance(unit, pint.Unit)
|
||||
# 1 pound + 8 ounces = 16 ounces + 8 ounces = 24 ounces
|
||||
assert str(unit) == "pound"
|
||||
assert quantity == 1.5
|
||||
|
||||
|
||||
def test_merge_zero_quantities():
|
||||
uc = UnitConverter()
|
||||
quantity, unit = uc.merge(0, "cup", 1, "cup")
|
||||
|
||||
assert isinstance(unit, pint.Unit)
|
||||
assert str(unit) == "cup"
|
||||
assert quantity == 1
|
||||
|
||||
|
||||
def test_merge_invalid_unit():
|
||||
uc = UnitConverter()
|
||||
|
||||
with pytest.raises(UnitNotFound):
|
||||
uc.merge(1, "pound", 1, random_string())
|
||||
|
||||
|
||||
def test_merge_incompatible_units():
|
||||
uc = UnitConverter()
|
||||
|
||||
with pytest.raises(pint.errors.DimensionalityError):
|
||||
uc.merge(1, "pound", 1, "cup")
|
||||
|
||||
|
||||
def test_merge_negative_quantity():
|
||||
uc = UnitConverter()
|
||||
quantity, unit = uc.merge(-1, "cup", 2, "cup")
|
||||
|
||||
assert isinstance(unit, pint.Unit)
|
||||
assert str(unit) == "cup"
|
||||
assert quantity == 1
|
||||
|
||||
|
||||
def test_merge_volume_ounce():
|
||||
uc = UnitConverter()
|
||||
quantity, unit = uc.merge(4, "ounce", 1, "cup")
|
||||
|
||||
assert isinstance(unit, pint.Unit)
|
||||
assert str(unit) == "fluid_ounce" # converted automatically from ounce
|
||||
assert quantity == 12
|
||||
|
||||
|
||||
def test_merge_quantity_and_unit_simple():
|
||||
unit_1 = CreateIngredientUnit(name="mealie_cup", standard_quantity=1, standard_unit="cup")
|
||||
unit_2 = CreateIngredientUnit(name="mealie_cup", standard_quantity=1, standard_unit="cup")
|
||||
|
||||
quantity, unit = merge_quantity_and_unit(1, unit_1, 2, unit_2)
|
||||
|
||||
assert quantity == 3
|
||||
assert unit.name == "mealie_cup"
|
||||
|
||||
|
||||
def test_merge_quantity_and_unit_invalid():
|
||||
unit_1 = CreateIngredientUnit(name="mealie_cup", standard_quantity=1, standard_unit="cup")
|
||||
unit_2 = CreateIngredientUnit(name="mealie_random", standard_quantity=1, standard_unit=random_string())
|
||||
|
||||
with pytest.raises(UnitNotFound):
|
||||
merge_quantity_and_unit(1, unit_1, 1, unit_2)
|
||||
|
||||
|
||||
def test_merge_quantity_and_unit_compatible():
|
||||
unit_1 = CreateIngredientUnit(name="mealie_pint", standard_quantity=1, standard_unit="pint")
|
||||
unit_2 = CreateIngredientUnit(name="mealie_cup", standard_quantity=1, standard_unit="cup")
|
||||
|
||||
quantity, unit = merge_quantity_and_unit(1, unit_1, 1, unit_2)
|
||||
|
||||
# 1 pint + 1 cup = 2 pints + 1 cup = 3 cups, converted to pint = 1.5 pint
|
||||
assert quantity == 1.5
|
||||
assert unit.name == "mealie_pint"
|
||||
|
||||
|
||||
def test_merge_quantity_and_unit_selects_larger_unit():
|
||||
unit_1 = CreateIngredientUnit(name="mealie_pint", standard_quantity=1, standard_unit="pint")
|
||||
unit_2 = CreateIngredientUnit(name="mealie_cup", standard_quantity=1, standard_unit="cup")
|
||||
|
||||
quantity, unit = merge_quantity_and_unit(2, unit_1, 4, unit_2)
|
||||
|
||||
# 2 pint + 4 cup = 4 cups + 4 cups = 8 cups, should be returned as pint (larger unit)
|
||||
assert quantity == 4
|
||||
assert unit.name == "mealie_pint"
|
||||
|
||||
|
||||
def test_merge_quantity_and_unit_selects_smaller_unit():
|
||||
unit_1 = CreateIngredientUnit(name="mealie_pint", standard_quantity=1, standard_unit="pint")
|
||||
unit_2 = CreateIngredientUnit(name="mealie_cup", standard_quantity=1, standard_unit="cup")
|
||||
|
||||
quantity, unit = merge_quantity_and_unit(0.125, unit_1, 0.5, unit_2)
|
||||
|
||||
# 0.125 pint + 0.5 cup = 0.25 cup + 0.5 cup = 0.75 cup, should be returned as cup (smaller for < 1)
|
||||
assert quantity == 0.75
|
||||
assert unit.name == "mealie_cup"
|
||||
|
||||
|
||||
def test_merge_quantity_and_unit_missing_standard_data():
|
||||
unit_1 = CreateIngredientUnit(name="mealie_cup", standard_quantity=1, standard_unit="cup")
|
||||
unit_2 = CreateIngredientUnit(name="mealie_cup_no_std", standard_quantity=None, standard_unit=None)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
merge_quantity_and_unit(1, unit_1, 1, unit_2)
|
||||
|
||||
|
||||
def test_merge_quantity_and_unit_volume_ounce():
|
||||
unit_1 = CreateIngredientUnit(name="mealie_oz", standard_quantity=1, standard_unit="ounce")
|
||||
unit_2 = CreateIngredientUnit(name="mealie_cup", standard_quantity=1, standard_unit="cup")
|
||||
|
||||
quantity, unit = merge_quantity_and_unit(8, unit_1, 1, unit_2)
|
||||
|
||||
assert quantity == 2
|
||||
assert unit.name == "mealie_cup"
|
||||
@@ -1,11 +1,26 @@
|
||||
from uuid import UUID
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from mealie.repos.all_repositories import AllRepositories, get_repositories
|
||||
from mealie.schema.recipe.recipe import Recipe
|
||||
from mealie.schema.recipe.recipe_ingredient import RecipeIngredient, SaveIngredientUnit
|
||||
from tests.utils.factories import random_string
|
||||
from mealie.schema.user.user import GroupBase
|
||||
from tests.utils.factories import random_int, random_string
|
||||
from tests.utils.fixture_schemas import TestUser
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def unique_local_group_id(unfiltered_database: AllRepositories) -> str:
|
||||
return str(unfiltered_database.groups.create(GroupBase(name=random_string())).id)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def unique_db(session: Session, unique_local_group_id: str) -> AllRepositories:
|
||||
return get_repositories(session, group_id=unique_local_group_id)
|
||||
|
||||
|
||||
def test_unit_merger(unique_user: TestUser):
|
||||
database = unique_user.repos
|
||||
recipe: Recipe | None = None
|
||||
@@ -51,3 +66,79 @@ def test_unit_merger(unique_user: TestUser):
|
||||
|
||||
for ingredient in recipe.recipe_ingredient:
|
||||
assert ingredient.unit.id == unit_1.id # type: ignore
|
||||
|
||||
|
||||
@pytest.mark.parametrize("standard_field", ["name", "plural_name", "abbreviation", "plural_abbreviation"])
|
||||
@pytest.mark.parametrize("use_bulk", [True, False])
|
||||
def test_auto_inject_standardization(unique_db: AllRepositories, standard_field: str, use_bulk: bool):
|
||||
unit_in = SaveIngredientUnit(name=random_string(), group_id=unique_db.group_id).model_dump()
|
||||
unit_in[standard_field] = "gallon"
|
||||
|
||||
if use_bulk:
|
||||
out_many = unique_db.ingredient_units.create_many([unit_in])
|
||||
assert len(out_many) == 1
|
||||
unit_out = out_many[0]
|
||||
else:
|
||||
unit_out = unique_db.ingredient_units.create(unit_in)
|
||||
|
||||
assert unit_out.standard_unit == "cup"
|
||||
assert unit_out.standard_quantity == 16
|
||||
|
||||
|
||||
def test_dont_auto_inject_random(unique_db: AllRepositories):
|
||||
unit_in = SaveIngredientUnit(name=random_string(), group_id=unique_db.group_id)
|
||||
unit_out = unique_db.ingredient_units.create(unit_in)
|
||||
|
||||
assert unit_out.standard_quantity is None
|
||||
assert unit_out.standard_unit is None
|
||||
|
||||
|
||||
def test_auto_inject_other_language(unique_db: AllRepositories):
|
||||
# Inject custom unit map
|
||||
GALLON = random_string()
|
||||
unique_db.ingredient_units._standardized_unit_map = {GALLON: "gallon"}
|
||||
|
||||
# Create unit with translated value
|
||||
unit_in = SaveIngredientUnit(name=GALLON, group_id=unique_db.group_id)
|
||||
unit_out = unique_db.ingredient_units.create(unit_in)
|
||||
|
||||
assert unit_out.standard_unit == "cup"
|
||||
assert unit_out.standard_quantity == 16
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", ["custom-mealie-unit", "gallon"])
|
||||
def test_user_standardization(unique_db: AllRepositories, name: str):
|
||||
unit_in = SaveIngredientUnit(
|
||||
name=name,
|
||||
group_id=unique_db.group_id,
|
||||
standard_quantity=random_int(1, 10),
|
||||
standard_unit=random_string(),
|
||||
)
|
||||
unit_out = unique_db.ingredient_units.create(unit_in)
|
||||
|
||||
assert unit_out.standard_quantity == unit_in.standard_quantity
|
||||
assert unit_out.standard_unit == unit_in.standard_unit
|
||||
|
||||
|
||||
def test_ignore_incomplete_standardization(unique_db: AllRepositories):
|
||||
unit_in = SaveIngredientUnit(
|
||||
name=random_string(),
|
||||
group_id=unique_db.group_id,
|
||||
standard_quantity=random_int(1, 10),
|
||||
standard_unit=None,
|
||||
)
|
||||
unit_out = unique_db.ingredient_units.create(unit_in)
|
||||
|
||||
assert unit_out.standard_quantity is None
|
||||
assert unit_out.standard_unit is None
|
||||
|
||||
unit_in = SaveIngredientUnit(
|
||||
name=random_string(),
|
||||
group_id=unique_db.group_id,
|
||||
standard_quantity=None,
|
||||
standard_unit=random_string(),
|
||||
)
|
||||
unit_out = unique_db.ingredient_units.create(unit_in)
|
||||
|
||||
assert unit_out.standard_quantity is None
|
||||
assert unit_out.standard_unit is None
|
||||
|
||||
@@ -217,6 +217,22 @@ def _b9e516e2d3b3_add_household_to_recipe_last_made_household_to_foods_and_tools
|
||||
assert not tool.households_with_tool
|
||||
|
||||
|
||||
def _a39c7f1826e3_add_unit_standardization_fields(session: Session):
|
||||
groups = session.query(Group).all()
|
||||
|
||||
for group in groups:
|
||||
# test_data.backup_version_1d9a002d7234_1 has a non-anonymized "pint" unit
|
||||
# and has not yet run the standardization migration.
|
||||
pint_units = (
|
||||
session.query(IngredientUnitModel)
|
||||
.filter(IngredientUnitModel.group_id == group.id, IngredientUnitModel.name == "pint")
|
||||
.all()
|
||||
)
|
||||
for unit in pint_units:
|
||||
assert unit.standard_quantity == 2
|
||||
assert unit.standard_unit == "cup"
|
||||
|
||||
|
||||
def test_database_restore_data():
|
||||
"""
|
||||
This tests real user backups to make sure the data is restored correctly. The data has been anonymized, but
|
||||
@@ -227,6 +243,7 @@ def test_database_restore_data():
|
||||
"""
|
||||
|
||||
backup_paths = [
|
||||
test_data.backup_version_1d9a002d7234_1,
|
||||
test_data.backup_version_44e8d670719d_1,
|
||||
test_data.backup_version_44e8d670719d_2,
|
||||
test_data.backup_version_44e8d670719d_3,
|
||||
@@ -245,6 +262,7 @@ def test_database_restore_data():
|
||||
_d7c6efd2de42_migrate_favorites_and_ratings_to_user_ratings,
|
||||
_86054b40fd06_added_query_filter_string_to_cookbook_and_mealplan,
|
||||
_b9e516e2d3b3_add_household_to_recipe_last_made_household_to_foods_and_tools,
|
||||
_a39c7f1826e3_add_unit_standardization_fields,
|
||||
]
|
||||
|
||||
settings = get_app_settings()
|
||||
|
||||
@@ -23,7 +23,6 @@ def test_non_default_settings(monkeypatch):
|
||||
assert app_settings.API_PORT == 8000
|
||||
assert app_settings.API_DOCS is False
|
||||
|
||||
assert app_settings.REDOC_URL is None
|
||||
assert app_settings.DOCS_URL is None
|
||||
|
||||
|
||||
|
||||
104
uv.lock
generated
104
uv.lock
generated
@@ -399,7 +399,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.129.0"
|
||||
version = "0.129.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-doc" },
|
||||
@@ -408,9 +408,9 @@ dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/48/47/75f6bea02e797abff1bca968d5997793898032d9923c1935ae2efdece642/fastapi-0.129.0.tar.gz", hash = "sha256:61315cebd2e65df5f97ec298c888f9de30430dd0612d59d6480beafbc10655af", size = 375450, upload-time = "2026-02-12T13:54:52.541Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f6/37/4aa7f6ce92745458b6ce0acd706dde2ac23a3bf341266b5311c904109f67/fastapi-0.129.1.tar.gz", hash = "sha256:6ccf0eca9644e0d6280115b4fc8281bf55ec5878d4d95572f7b2034ab15708ba", size = 369852, upload-time = "2026-02-21T13:10:03.335Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/dd/d0ee25348ac58245ee9f90b6f3cbb666bf01f69be7e0911f9851bddbda16/fastapi-0.129.0-py3-none-any.whl", hash = "sha256:b4946880e48f462692b31c083be0432275cbfb6e2274566b1be91479cc1a84ec", size = 102950, upload-time = "2026-02-12T13:54:54.528Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/f9/f15d92bd6035d4f83be8b82dc527a3e7abc87648fda62cf8d1df344410a7/fastapi-0.129.1-py3-none-any.whl", hash = "sha256:022462403bc385b791df418d8f088eb0e8f1fe7cb8f625d682f5e9da6157cc83", size = 103226, upload-time = "2026-02-21T13:10:05.058Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -850,6 +850,7 @@ dependencies = [
|
||||
{ name = "paho-mqtt" },
|
||||
{ name = "pillow" },
|
||||
{ name = "pillow-heif" },
|
||||
{ name = "pint" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "pyhumps" },
|
||||
@@ -910,7 +911,7 @@ requires-dist = [
|
||||
{ name = "bcrypt", specifier = "==5.0.0" },
|
||||
{ name = "beautifulsoup4", specifier = "==4.14.3" },
|
||||
{ name = "extruct", specifier = "==0.18.0" },
|
||||
{ name = "fastapi", specifier = "==0.129.0" },
|
||||
{ name = "fastapi", specifier = "==0.129.1" },
|
||||
{ name = "html2text", specifier = "==2025.4.15" },
|
||||
{ name = "httpx", specifier = "==0.28.1" },
|
||||
{ name = "ingredient-parser-nlp", specifier = "==2.5.0" },
|
||||
@@ -922,10 +923,11 @@ requires-dist = [
|
||||
{ name = "orjson", specifier = "==3.11.7" },
|
||||
{ name = "paho-mqtt", specifier = "==1.6.1" },
|
||||
{ name = "pillow", specifier = "==12.1.1" },
|
||||
{ name = "pillow-heif", specifier = "==1.2.0" },
|
||||
{ name = "pillow-heif", specifier = "==1.2.1" },
|
||||
{ name = "pint", specifier = ">=0.25" },
|
||||
{ name = "psycopg2-binary", marker = "extra == 'pgsql'", specifier = "==2.9.11" },
|
||||
{ name = "pydantic", specifier = "==2.12.5" },
|
||||
{ name = "pydantic-settings", specifier = "==2.13.0" },
|
||||
{ name = "pydantic-settings", specifier = "==2.13.1" },
|
||||
{ name = "pyhumps", specifier = "==3.8.0" },
|
||||
{ name = "pyjwt", specifier = "==2.11.0" },
|
||||
{ name = "python-dateutil", specifier = "==2.9.0.post0" },
|
||||
@@ -950,22 +952,22 @@ dev = [
|
||||
{ name = "coverage", specifier = "==7.13.4" },
|
||||
{ name = "coveragepy-lcov", specifier = "==0.1.2" },
|
||||
{ name = "freezegun", specifier = "==1.5.5" },
|
||||
{ name = "mkdocs-material", specifier = "==9.7.1" },
|
||||
{ name = "mkdocs-material", specifier = "==9.7.2" },
|
||||
{ name = "mypy", specifier = "==1.19.1" },
|
||||
{ name = "pre-commit", specifier = "==4.5.1" },
|
||||
{ name = "pydantic-to-typescript2", specifier = "==1.0.6" },
|
||||
{ name = "pylint", specifier = "==4.0.4" },
|
||||
{ name = "pylint", specifier = "==4.0.5" },
|
||||
{ name = "pytest", specifier = "==9.0.2" },
|
||||
{ name = "pytest-asyncio", specifier = "==1.3.0" },
|
||||
{ name = "rich", specifier = "==14.3.2" },
|
||||
{ name = "ruff", specifier = "==0.15.1" },
|
||||
{ name = "rich", specifier = "==14.3.3" },
|
||||
{ name = "ruff", specifier = "==0.15.2" },
|
||||
{ name = "types-python-dateutil", specifier = "==2.9.0.20260124" },
|
||||
{ name = "types-python-slugify", specifier = "==8.0.2.20240310" },
|
||||
{ name = "types-pyyaml", specifier = "==6.0.12.20250915" },
|
||||
{ name = "types-requests", specifier = "==2.32.4.20260107" },
|
||||
{ name = "types-urllib3", specifier = "==1.26.25.14" },
|
||||
]
|
||||
docs = [{ name = "mkdocs-material", specifier = "==9.7.1" }]
|
||||
docs = [{ name = "mkdocs-material", specifier = "==9.7.2" }]
|
||||
|
||||
[[package]]
|
||||
name = "mergedeep"
|
||||
@@ -1030,7 +1032,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "mkdocs-material"
|
||||
version = "9.7.1"
|
||||
version = "9.7.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "babel" },
|
||||
@@ -1045,9 +1047,9 @@ dependencies = [
|
||||
{ name = "pymdown-extensions" },
|
||||
{ name = "requests" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/27/e2/2ffc356cd72f1473d07c7719d82a8f2cbd261666828614ecb95b12169f41/mkdocs_material-9.7.1.tar.gz", hash = "sha256:89601b8f2c3e6c6ee0a918cc3566cb201d40bf37c3cd3c2067e26fadb8cce2b8", size = 4094392, upload-time = "2025-12-18T09:49:00.308Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/34/57/5d3c8c9e2ff9d66dc8f63aa052eb0bac5041fecff7761d8689fe65c39c13/mkdocs_material-9.7.2.tar.gz", hash = "sha256:6776256552290b9b7a7aa002780e25b1e04bc9c3a8516b6b153e82e16b8384bd", size = 4097818, upload-time = "2026-02-18T15:53:07.763Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/32/ed071cb721aca8c227718cffcf7bd539620e9799bbf2619e90c757bfd030/mkdocs_material-9.7.1-py3-none-any.whl", hash = "sha256:3f6100937d7d731f87f1e3e3b021c97f7239666b9ba1151ab476cabb96c60d5c", size = 9297166, upload-time = "2025-12-18T09:48:56.664Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/19/d194e75e82282b1d688f0720e21b5ac250ed64ddea333a228aaf83105f2e/mkdocs_material-9.7.2-py3-none-any.whl", hash = "sha256:9bf6f53452d4a4d527eac3cef3f92b7b6fc4931c55d57766a7d87890d47e1b92", size = 9305052, upload-time = "2026-02-18T15:53:05.221Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1237,20 +1239,20 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pillow-heif"
|
||||
version = "1.2.0"
|
||||
version = "1.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pillow" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/12/96/e4bf7bde1de9908bb509212c2861bff857eb4be845ebf80f6b0b02b8650d/pillow_heif-1.2.0.tar.gz", hash = "sha256:dd5c818dfb4ec39a5093127f8c07bbb32ca81dbbd29c4ebeffd23222ccc76aa9", size = 17128367, upload-time = "2026-01-23T07:35:24.755Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/22/f4/68bd0465dc0494c22e23334dde0a9c52dec5afe98cf5a40abb47f75e1b08/pillow_heif-1.2.1.tar.gz", hash = "sha256:29be44d636269e2d779b4aec629bc056ec7260b734a16b4d3bb284c49c200274", size = 17128668, upload-time = "2026-02-18T11:20:48.643Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/66/3c/76ef3ecb3c7dbfa72033eeded44c0d6dc963aada7f7fc5a6262e3381dc38/pillow_heif-1.2.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:9e2d26deeba5a2f31ee92ae7e21cd07f8001cd4b2075585eac2f74f926527210", size = 4818206, upload-time = "2026-01-23T07:34:37.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/8c/04fa8db3627c8d8255645eaead8f2900c0e389c20815d19fefbd8a342fde/pillow_heif-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b121733ea0f82c3de04b73958aa338acd98137ed3ed3bf7a16516e0bf8654782", size = 3503861, upload-time = "2026-01-23T07:34:38.916Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/b8/500bc420d193314add5b2d3e67e2568215378f025a1fc78374f9fec217f3/pillow_heif-1.2.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdba80502ea990e66e5d1bde97f58746e8002998f6d627c8b483ddff2bc262a7", size = 5843480, upload-time = "2026-01-23T07:34:40.795Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/60/84d73f681bae63062ac8c03b8d5cecc80ffc8604a2284573d06cd6b2718d/pillow_heif-1.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5f5fe73cbf68778a48ebf320f52b73b142e986e2b853b5f83d4b5d102d276d33", size = 5577848, upload-time = "2026-01-23T07:34:42.333Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/ca/dc6be91840ce5dc3607793fbeff382813bc5f641ef33abd2fb6f769856ea/pillow_heif-1.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:36516178cf3c1de0cbefa6461ca0a5c4f67380b7ee8e200106aefaf444f32613", size = 6885225, upload-time = "2026-01-23T07:34:44.622Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/c5/e86f47bb49214a3c7f7c09c6198db03048ddaf3c548f1fe58b25ff7d17da/pillow_heif-1.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c132994a444b8a5f2c5e64aa7d07b50405668198d8af5ba53af12732676e882d", size = 6510220, upload-time = "2026-01-23T07:34:46.871Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/ca/04a44838085df912fbdf5cef0146ef4026b47995f09c84a25bfc679d10d7/pillow_heif-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:8e06bb5e9ccd4f4a8be9c618e071ed47bcf8246fd0d1f00c25ac98c4ad9fd8ff", size = 5483151, upload-time = "2026-01-23T07:34:49.002Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/4a/8d674f384d1d9d2d84193dcb7f6b41eeb240ed127f45f07114770205c8a1/pillow_heif-1.2.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c5a3c8fec8cf63f6d9170f092a210e76d584beef5a5b0f5e8fbfa171eb27520a", size = 4666941, upload-time = "2026-02-18T11:20:03.957Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/4b/f08a3c535bb116ed74b7851eb5cc0ae105338f4d5e921d7547137322e68e/pillow_heif-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:add430cf7f5340eaa70c2e57af59655515fd415b2b93dde0baec87be48debd0f", size = 3392297, upload-time = "2026-02-18T11:20:06.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/eb/39f38534894c1e12cd4526683b7aec5a1d403cd8f76efc4efce762826658/pillow_heif-1.2.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9a6daa0f88fe5fa76b72c848615836368d0577a108059e3070615c1e50551dc", size = 5843458, upload-time = "2026-02-18T11:20:07.737Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/07/431be5fc0d34d67dedc918a1a0f9f9aa7f3973557698fb7d35b5d136c45c/pillow_heif-1.2.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:35a355df6024f09b0e46b56bb5805c275a8ca7dc67e1da2be245aee3a70c82ec", size = 5577826, upload-time = "2026-02-18T11:20:09.407Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/1b/ecdedae3225b75e870980bc6f505af2387e38f9ca85a110e59b4328e7263/pillow_heif-1.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:33d84eb1c40d9c63d2ea869e6290f5b59ebf4421ed16090796be60b8e3b2a061", size = 6885202, upload-time = "2026-02-18T11:20:11.421Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/27/a9a2ca1aa874166064ced4c8fbb03f106ef71b9eda0cc45688c3e12376d4/pillow_heif-1.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2522a54df26f996993189326208513a6c8458ac89de51644a89b19fcda712539", size = 6510193, upload-time = "2026-02-18T11:20:13.435Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/63/7d1f5358b6a4c1214e5ef25e2166992381e6a30c0da933ab56ce2c278ea9/pillow_heif-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:0c965277fde806c7c628b16f9a45f4a7b10c32c390ce7d70c0572499a5d8426f", size = 5483265, upload-time = "2026-02-18T11:20:15.741Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1397,16 +1399,16 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-settings"
|
||||
version = "2.13.0"
|
||||
version = "2.13.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/96/a1/ae859ffac5a3338a66b74c5e29e244fd3a3cc483c89feaf9f56c39898d75/pydantic_settings-2.13.0.tar.gz", hash = "sha256:95d875514610e8595672800a5c40b073e99e4aae467fa7c8f9c263061ea2e1fe", size = 222450, upload-time = "2026-02-15T12:11:23.476Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/1a/dd1b9d7e627486cf8e7523d09b70010e05a4bc41414f4ae6ce184cf0afb6/pydantic_settings-2.13.0-py3-none-any.whl", hash = "sha256:d67b576fff39cd086b595441bf9c75d4193ca9c0ed643b90360694d0f1240246", size = 58429, upload-time = "2026-02-15T12:11:22.133Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1450,7 +1452,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pylint"
|
||||
version = "4.0.4"
|
||||
version = "4.0.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "astroid" },
|
||||
@@ -1461,9 +1463,9 @@ dependencies = [
|
||||
{ name = "platformdirs" },
|
||||
{ name = "tomlkit" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5a/d2/b081da1a8930d00e3fc06352a1d449aaf815d4982319fab5d8cdb2e9ab35/pylint-4.0.4.tar.gz", hash = "sha256:d9b71674e19b1c36d79265b5887bf8e55278cbe236c9e95d22dc82cf044fdbd2", size = 1571735, upload-time = "2025-11-30T13:29:04.315Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e4/b6/74d9a8a68b8067efce8d07707fe6a236324ee1e7808d2eb3646ec8517c7d/pylint-4.0.5.tar.gz", hash = "sha256:8cd6a618df75deb013bd7eb98327a95f02a6fb839205a6bbf5456ef96afb317c", size = 1572474, upload-time = "2026-02-20T09:07:33.621Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/92/d40f5d937517cc489ad848fc4414ecccc7592e4686b9071e09e64f5e378e/pylint-4.0.4-py3-none-any.whl", hash = "sha256:63e06a37d5922555ee2c20963eb42559918c20bd2b21244e4ef426e7c43b92e0", size = 536425, upload-time = "2025-11-30T13:29:02.53Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/6f/9ac2548e290764781f9e7e2aaf0685b086379dabfb29ca38536985471eaf/pylint-4.0.5-py3-none-any.whl", hash = "sha256:00f51c9b14a3b3ae08cff6b2cdd43f28165c78b165b628692e428fb1f8dc2cf2", size = 536694, upload-time = "2026-02-20T09:07:31.028Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1727,40 +1729,40 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "14.3.2"
|
||||
version = "14.3.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown-it-py" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/74/99/a4cab2acbb884f80e558b0771e97e21e939c5dfb460f488d19df485e8298/rich-14.3.2.tar.gz", hash = "sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8", size = 230143, upload-time = "2026-02-01T16:20:47.908Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/45/615f5babd880b4bd7d405cc0dc348234c5ffb6ed1ea33e152ede08b2072d/rich-14.3.2-py3-none-any.whl", hash = "sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69", size = 309963, upload-time = "2026-02-01T16:20:46.078Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.15.1"
|
||||
version = "0.15.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/04/dc/4e6ac71b511b141cf626357a3946679abeba4cf67bc7cc5a17920f31e10d/ruff-0.15.1.tar.gz", hash = "sha256:c590fe13fb57c97141ae975c03a1aedb3d3156030cabd740d6ff0b0d601e203f", size = 4540855, upload-time = "2026-02-12T23:09:09.998Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/04/eab13a954e763b0606f460443fcbf6bb5a0faf06890ea3754ff16523dce5/ruff-0.15.2.tar.gz", hash = "sha256:14b965afee0969e68bb871eba625343b8673375f457af4abe98553e8bbb98342", size = 4558148, upload-time = "2026-02-19T22:32:20.271Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/23/bf/e6e4324238c17f9d9120a9d60aa99a7daaa21204c07fcd84e2ef03bb5fd1/ruff-0.15.1-py3-none-linux_armv6l.whl", hash = "sha256:b101ed7cf4615bda6ffe65bdb59f964e9f4a0d3f85cbf0e54f0ab76d7b90228a", size = 10367819, upload-time = "2026-02-12T23:09:03.598Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/ea/c8f89d32e7912269d38c58f3649e453ac32c528f93bb7f4219258be2e7ed/ruff-0.15.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:939c995e9277e63ea632cc8d3fae17aa758526f49a9a850d2e7e758bfef46602", size = 10798618, upload-time = "2026-02-12T23:09:22.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/0f/1d0d88bc862624247d82c20c10d4c0f6bb2f346559d8af281674cf327f15/ruff-0.15.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d83466455fdefe60b8d9c8df81d3c1bbb2115cede53549d3b522ce2bc703899", size = 10148518, upload-time = "2026-02-12T23:08:58.339Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/c8/291c49cefaa4a9248e986256df2ade7add79388fe179e0691be06fae6f37/ruff-0.15.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9457e3c3291024866222b96108ab2d8265b477e5b1534c7ddb1810904858d16", size = 10518811, upload-time = "2026-02-12T23:09:31.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/1a/f5707440e5ae43ffa5365cac8bbb91e9665f4a883f560893829cf16a606b/ruff-0.15.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92c92b003e9d4f7fbd33b1867bb15a1b785b1735069108dfc23821ba045b29bc", size = 10196169, upload-time = "2026-02-12T23:09:17.306Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/ff/26ddc8c4da04c8fd3ee65a89c9fb99eaa5c30394269d424461467be2271f/ruff-0.15.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fe5c41ab43e3a06778844c586251eb5a510f67125427625f9eb2b9526535779", size = 10990491, upload-time = "2026-02-12T23:09:25.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/00/50920cb385b89413f7cdb4bb9bc8fc59c1b0f30028d8bccc294189a54955/ruff-0.15.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66a6dd6df4d80dc382c6484f8ce1bcceb55c32e9f27a8b94c32f6c7331bf14fb", size = 11843280, upload-time = "2026-02-12T23:09:19.88Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/6d/2f5cad8380caf5632a15460c323ae326f1e1a2b5b90a6ee7519017a017ca/ruff-0.15.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a4a42cbb8af0bda9bcd7606b064d7c0bc311a88d141d02f78920be6acb5aa83", size = 11274336, upload-time = "2026-02-12T23:09:14.907Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/1d/5f56cae1d6c40b8a318513599b35ea4b075d7dc1cd1d04449578c29d1d75/ruff-0.15.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ab064052c31dddada35079901592dfba2e05f5b1e43af3954aafcbc1096a5b2", size = 11137288, upload-time = "2026-02-12T23:09:07.475Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/20/6f8d7d8f768c93b0382b33b9306b3b999918816da46537d5a61635514635/ruff-0.15.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5631c940fe9fe91f817a4c2ea4e81f47bee3ca4aa646134a24374f3c19ad9454", size = 11070681, upload-time = "2026-02-12T23:08:55.43Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/67/d640ac76069f64cdea59dba02af2e00b1fa30e2103c7f8d049c0cff4cafd/ruff-0.15.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68138a4ba184b4691ccdc39f7795c66b3c68160c586519e7e8444cf5a53e1b4c", size = 10486401, upload-time = "2026-02-12T23:09:27.927Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/3d/e1429f64a3ff89297497916b88c32a5cc88eeca7e9c787072d0e7f1d3e1e/ruff-0.15.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:518f9af03bfc33c03bdb4cb63fabc935341bb7f54af500f92ac309ecfbba6330", size = 10197452, upload-time = "2026-02-12T23:09:12.147Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/83/e2c3bade17dad63bf1e1c2ffaf11490603b760be149e1419b07049b36ef2/ruff-0.15.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:da79f4d6a826caaea95de0237a67e33b81e6ec2e25fc7e1993a4015dffca7c61", size = 10693900, upload-time = "2026-02-12T23:09:34.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/27/fdc0e11a813e6338e0706e8b39bb7a1d61ea5b36873b351acee7e524a72a/ruff-0.15.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3dd86dccb83cd7d4dcfac303ffc277e6048600dfc22e38158afa208e8bf94a1f", size = 11227302, upload-time = "2026-02-12T23:09:36.536Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/58/ac864a75067dcbd3b95be5ab4eb2b601d7fbc3d3d736a27e391a4f92a5c1/ruff-0.15.1-py3-none-win32.whl", hash = "sha256:660975d9cb49b5d5278b12b03bb9951d554543a90b74ed5d366b20e2c57c2098", size = 10462555, upload-time = "2026-02-12T23:09:29.899Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/5e/d4ccc8a27ecdb78116feac4935dfc39d1304536f4296168f91ed3ec00cd2/ruff-0.15.1-py3-none-win_amd64.whl", hash = "sha256:c820fef9dd5d4172a6570e5721704a96c6679b80cf7be41659ed439653f62336", size = 11599956, upload-time = "2026-02-12T23:09:01.157Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/07/5bda6a85b220c64c65686bc85bd0bbb23b29c62b3a9f9433fa55f17cda93/ruff-0.15.1-py3-none-win_arm64.whl", hash = "sha256:5ff7d5f0f88567850f45081fac8f4ec212be8d0b963e385c3f7d0d2eb4899416", size = 10874604, upload-time = "2026-02-12T23:09:05.515Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/70/3a4dc6d09b13cb3e695f28307e5d889b2e1a66b7af9c5e257e796695b0e6/ruff-0.15.2-py3-none-linux_armv6l.whl", hash = "sha256:120691a6fdae2f16d65435648160f5b81a9625288f75544dc40637436b5d3c0d", size = 10430565, upload-time = "2026-02-19T22:32:41.824Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/0b/bb8457b56185ece1305c666dc895832946d24055be90692381c31d57466d/ruff-0.15.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a89056d831256099658b6bba4037ac6dd06f49d194199215befe2bb10457ea5e", size = 10820354, upload-time = "2026-02-19T22:32:07.366Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/c1/e0532d7f9c9e0b14c46f61b14afd563298b8b83f337b6789ddd987e46121/ruff-0.15.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e36dee3a64be0ebd23c86ffa3aa3fd3ac9a712ff295e192243f814a830b6bd87", size = 10170767, upload-time = "2026-02-19T22:32:13.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/e8/da1aa341d3af017a21c7a62fb5ec31d4e7ad0a93ab80e3a508316efbcb23/ruff-0.15.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9fb47b6d9764677f8c0a193c0943ce9a05d6763523f132325af8a858eadc2b9", size = 10529591, upload-time = "2026-02-19T22:32:02.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/74/184fbf38e9f3510231fbc5e437e808f0b48c42d1df9434b208821efcd8d6/ruff-0.15.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f376990f9d0d6442ea9014b19621d8f2aaf2b8e39fdbfc79220b7f0c596c9b80", size = 10260771, upload-time = "2026-02-19T22:32:36.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/ac/605c20b8e059a0bc4b42360414baa4892ff278cec1c91fff4be0dceedefd/ruff-0.15.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2dcc987551952d73cbf5c88d9fdee815618d497e4df86cd4c4824cc59d5dd75f", size = 11045791, upload-time = "2026-02-19T22:32:31.642Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/52/db6e419908f45a894924d410ac77d64bdd98ff86901d833364251bd08e22/ruff-0.15.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42a47fd785cbe8c01b9ff45031af875d101b040ad8f4de7bbb716487c74c9a77", size = 11879271, upload-time = "2026-02-19T22:32:29.305Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/d8/7992b18f2008bdc9231d0f10b16df7dda964dbf639e2b8b4c1b4e91b83af/ruff-0.15.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbe9f49354866e575b4c6943856989f966421870e85cd2ac94dccb0a9dcb2fea", size = 11303707, upload-time = "2026-02-19T22:32:22.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/02/849b46184bcfdd4b64cde61752cc9a146c54759ed036edd11857e9b8443b/ruff-0.15.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7a672c82b5f9887576087d97be5ce439f04bbaf548ee987b92d3a7dede41d3a", size = 11149151, upload-time = "2026-02-19T22:32:44.234Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/04/f5284e388bab60d1d3b99614a5a9aeb03e0f333847e2429bebd2aaa1feec/ruff-0.15.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ecc64f46f7019e2bcc3cdc05d4a7da958b629a5ab7033195e11a438403d956", size = 11091132, upload-time = "2026-02-19T22:32:24.691Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/ae/88d844a21110e14d92cf73d57363fab59b727ebeabe78009b9ccb23500af/ruff-0.15.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:8dcf243b15b561c655c1ef2f2b0050e5d50db37fe90115507f6ff37d865dc8b4", size = 10504717, upload-time = "2026-02-19T22:32:26.75Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/27/867076a6ada7f2b9c8292884ab44d08fd2ba71bd2b5364d4136f3cd537e1/ruff-0.15.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dab6941c862c05739774677c6273166d2510d254dac0695c0e3f5efa1b5585de", size = 10263122, upload-time = "2026-02-19T22:32:10.036Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/ef/faf9321d550f8ebf0c6373696e70d1758e20ccdc3951ad7af00c0956be7c/ruff-0.15.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b9164f57fc36058e9a6806eb92af185b0697c9fe4c7c52caa431c6554521e5c", size = 10735295, upload-time = "2026-02-19T22:32:39.227Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/55/e8089fec62e050ba84d71b70e7834b97709ca9b7aba10c1a0b196e493f97/ruff-0.15.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:80d24fcae24d42659db7e335b9e1531697a7102c19185b8dc4a028b952865fd8", size = 11241641, upload-time = "2026-02-19T22:32:34.617Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/01/1c30526460f4d23222d0fabd5888868262fd0e2b71a00570ca26483cd993/ruff-0.15.2-py3-none-win32.whl", hash = "sha256:fd5ff9e5f519a7e1bd99cbe8daa324010a74f5e2ebc97c6242c08f26f3714f6f", size = 10507885, upload-time = "2026-02-19T22:32:15.635Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/10/3d18e3bbdf8fc50bbb4ac3cc45970aa5a9753c5cb51bf9ed9a3cd8b79fa3/ruff-0.15.2-py3-none-win_amd64.whl", hash = "sha256:d20014e3dfa400f3ff84830dfb5755ece2de45ab62ecea4af6b7262d0fb4f7c5", size = 11623725, upload-time = "2026-02-19T22:32:04.947Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/78/097c0798b1dab9f8affe73da9642bb4500e098cb27fd8dc9724816ac747b/ruff-0.15.2-py3-none-win_arm64.whl", hash = "sha256:cabddc5822acdc8f7b5527b36ceac55cc51eec7b1946e60181de8fe83ca8876e", size = 10941649, upload-time = "2026-02-19T22:32:18.108Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user