mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-07-09 03:04:54 -04:00
Merge branch 'mealie-next' into feat-frontend-access-controll
This commit is contained in:
commit
058d968833
@ -49,7 +49,9 @@
|
||||
"onCreateCommand": "sudo chown -R vscode:vscode /workspaces/mealie/frontend/node_modules && task setup",
|
||||
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||
"remoteUser": "vscode",
|
||||
// "features": {
|
||||
// "git": "latest"
|
||||
// }
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {
|
||||
"dockerDashComposeVersion": "v2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
18
.github/workflows/release.yml
vendored
18
.github/workflows/release.yml
vendored
@ -49,6 +49,9 @@ jobs:
|
||||
needs:
|
||||
- build-release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout 🛎
|
||||
uses: actions/checkout@v4
|
||||
@ -58,11 +61,12 @@ jobs:
|
||||
sed -i 's/:v[0-9]*.[0-9]*.[0-9]*/:${{ github.event.release.tag_name }}/' docs/docs/documentation/getting-started/installation/sqlite.md
|
||||
sed -i 's/:v[0-9]*.[0-9]*.[0-9]*/:${{ github.event.release.tag_name }}/' docs/docs/documentation/getting-started/installation/postgres.md
|
||||
|
||||
- name: Commit updates
|
||||
uses: test-room-7/action-update-file@v1
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
file-path: |
|
||||
docs/docs/documentation/getting-started/installation/sqlite.md
|
||||
docs/docs/documentation/getting-started/installation/postgres.md
|
||||
commit-msg: "Change image tag, for release ${{ github.event.release.tag_name }}"
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: "Update image tag, for release ${{ github.event.release.tag_name }}"
|
||||
branch: "docs/newrelease-update-version-${{ github.event.release.tag_name }}"
|
||||
delete-branch: true
|
||||
base: mealie-next
|
||||
title: "docs(auto): Update image tag, for release ${{ github.event.release.tag_name }}"
|
||||
body: "Auto-generated by `.github/workflows/release.yml`, on publish of release ${{ github.event.release.tag_name }}"
|
||||
|
@ -14,7 +14,9 @@ env:
|
||||
SMTP_HOST: localhost
|
||||
SMTP_PORT: 1025
|
||||
SMTP_FROM_NAME: MealieDev
|
||||
SMTP_FROM_EMAIL: mealie@example.com
|
||||
SMTP_AUTH_STRATEGY: NONE
|
||||
BASE_URL: http://localhost:3000
|
||||
LANG: en-US
|
||||
|
||||
# loads .env file if it exists
|
||||
|
@ -54,8 +54,8 @@ Changing the webworker settings may cause unforeseen memory leak issues with Mea
|
||||
| ---------------- | :-----: | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| WEB_GUNICORN | false | Enables Gunicorn to manage Uvicorn web for multiple works |
|
||||
| WORKERS_PER_CORE | 1 | Set the number of workers to the number of CPU cores multiplied by this value (Value \* CPUs). More info [here][workers_per_core] |
|
||||
| MAX_WORKERS | 1 | Set the maximum number of workers to use. Default is not set meaning unlimited. More info [here][max_workers] |
|
||||
| WEB_CONCURRENCY | 1 | Override the automatic definition of number of workers. More info [here][web_concurrency] |
|
||||
| MAX_WORKERS | None | Set the maximum number of workers to use. Default is not set meaning unlimited. More info [here][max_workers] |
|
||||
| WEB_CONCURRENCY | 2 | Override the automatic definition of number of workers. More info [here][web_concurrency] |
|
||||
|
||||
### LDAP
|
||||
|
||||
@ -95,3 +95,8 @@ Setting the following environmental variables will change the theme of the front
|
||||
| THEME_DARK_INFO | #1976D2 | Dark Theme Config Variable |
|
||||
| THEME_DARK_WARNING | #FF6D00 | Dark Theme Config Variable |
|
||||
| THEME_DARK_ERROR | #EF5350 | Dark Theme Config Variable |
|
||||
|
||||
|
||||
[workers_per_core]: https://github.com/tiangolo/uvicorn-gunicorn-docker/blob/2daa3e3873c837d5781feb4ff6a40a89f791f81b/README.md#workers_per_core
|
||||
[max_workers]: https://github.com/tiangolo/uvicorn-gunicorn-docker/blob/2daa3e3873c837d5781feb4ff6a40a89f791f81b/README.md#max_workers
|
||||
[web_concurrency]: https://github.com/tiangolo/uvicorn-gunicorn-docker/blob/2daa3e3873c837d5781feb4ff6a40a89f791f81b/README.md#web_concurrency
|
||||
|
File diff suppressed because one or more lines are too long
@ -19,11 +19,11 @@
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-list v-if="showViewer" dense class="mt-0 pt-0">
|
||||
<v-list-item v-for="(item, key, index) in labels" :key="index" style="min-height: 25px" dense>
|
||||
<v-list-item v-for="(item, key, index) in renderedList" :key="index" style="min-height: 25px" dense>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="pl-4 caption flex row">
|
||||
<div>{{ item.label }}</div>
|
||||
<div class="ml-auto mr-1">{{ value[key] }}</div>
|
||||
<div class="ml-auto mr-1">{{ item.value }}</div>
|
||||
<div>{{ item.suffix }}</div>
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
@ -37,6 +37,14 @@
|
||||
import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
|
||||
import { Nutrition } from "~/lib/api/types/recipe";
|
||||
|
||||
type NutritionLabelType = {
|
||||
[key: string]: {
|
||||
label: string;
|
||||
suffix: string;
|
||||
value?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
@ -50,34 +58,34 @@ export default defineComponent({
|
||||
},
|
||||
setup(props, context) {
|
||||
const { i18n } = useContext();
|
||||
const labels = {
|
||||
const labels = <NutritionLabelType>{
|
||||
calories: {
|
||||
label: i18n.t("recipe.calories"),
|
||||
suffix: i18n.t("recipe.calories-suffix"),
|
||||
label: i18n.tc("recipe.calories"),
|
||||
suffix: i18n.tc("recipe.calories-suffix"),
|
||||
},
|
||||
fatContent: {
|
||||
label: i18n.t("recipe.fat-content"),
|
||||
suffix: i18n.t("recipe.grams"),
|
||||
label: i18n.tc("recipe.fat-content"),
|
||||
suffix: i18n.tc("recipe.grams"),
|
||||
},
|
||||
fiberContent: {
|
||||
label: i18n.t("recipe.fiber-content"),
|
||||
suffix: i18n.t("recipe.grams"),
|
||||
label: i18n.tc("recipe.fiber-content"),
|
||||
suffix: i18n.tc("recipe.grams"),
|
||||
},
|
||||
proteinContent: {
|
||||
label: i18n.t("recipe.protein-content"),
|
||||
suffix: i18n.t("recipe.grams"),
|
||||
label: i18n.tc("recipe.protein-content"),
|
||||
suffix: i18n.tc("recipe.grams"),
|
||||
},
|
||||
sodiumContent: {
|
||||
label: i18n.t("recipe.sodium-content"),
|
||||
suffix: i18n.t("recipe.milligrams"),
|
||||
label: i18n.tc("recipe.sodium-content"),
|
||||
suffix: i18n.tc("recipe.milligrams"),
|
||||
},
|
||||
sugarContent: {
|
||||
label: i18n.t("recipe.sugar-content"),
|
||||
suffix: i18n.t("recipe.grams"),
|
||||
label: i18n.tc("recipe.sugar-content"),
|
||||
suffix: i18n.tc("recipe.grams"),
|
||||
},
|
||||
carbohydrateContent: {
|
||||
label: i18n.t("recipe.carbohydrate-content"),
|
||||
suffix: i18n.t("recipe.grams"),
|
||||
label: i18n.tc("recipe.carbohydrate-content"),
|
||||
suffix: i18n.tc("recipe.grams"),
|
||||
},
|
||||
};
|
||||
const valueNotNull = computed(() => {
|
||||
@ -96,11 +104,25 @@ export default defineComponent({
|
||||
context.emit("input", { ...props.value, [key]: event });
|
||||
}
|
||||
|
||||
// Build a new list that only contains nutritional information that has a value
|
||||
const renderedList = computed(() => {
|
||||
return Object.entries(labels).reduce((item: NutritionLabelType, [key, label]) => {
|
||||
if (props.value[key]?.trim()) {
|
||||
item[key] = {
|
||||
...label,
|
||||
value: props.value[key],
|
||||
};
|
||||
}
|
||||
return item;
|
||||
}, {});
|
||||
});
|
||||
|
||||
return {
|
||||
labels,
|
||||
valueNotNull,
|
||||
showViewer,
|
||||
updateValue,
|
||||
renderedList,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
@ -200,7 +200,7 @@
|
||||
"created-on-date": "Създадено на {0}",
|
||||
"unsaved-changes": "Имате незапазени промени. Желаете ли да ги запазите преди да излезете? Натиснете Ок за запазване и Отказ за отхвърляне на промените.",
|
||||
"clipboard-copy-failure": "Линкът към рецептата е копиран в клипборда.",
|
||||
"confirm-delete-generic-items": "Are you sure you want to delete the following items?"
|
||||
"confirm-delete-generic-items": "Сигурни ли сте, че желаете да изтриете следните елементи?"
|
||||
},
|
||||
"group": {
|
||||
"are-you-sure-you-want-to-delete-the-group": "Сигурни ли сте, че искате да изтриете <b>{groupName}<b/>?",
|
||||
@ -259,7 +259,7 @@
|
||||
},
|
||||
"meal-plan": {
|
||||
"create-a-new-meal-plan": "Създаване на нов хранителен план",
|
||||
"update-this-meal-plan": "Update this Meal Plan",
|
||||
"update-this-meal-plan": "Обнови този План за хранене",
|
||||
"dinner-this-week": "Вечеря тази седмица",
|
||||
"dinner-today": "Вечеря Днес",
|
||||
"dinner-tonight": "Вечеря ТАЗИ ВЕЧЕР",
|
||||
@ -474,11 +474,11 @@
|
||||
"add-to-timeline": "Добави към времевата линия",
|
||||
"recipe-added-to-list": "Рецептата е добавена към списъка",
|
||||
"recipes-added-to-list": "Рецептите са добавени към списъка",
|
||||
"successfully-added-to-list": "Successfully added to list",
|
||||
"successfully-added-to-list": "Успешно добавено в списъка",
|
||||
"recipe-added-to-mealplan": "Рецептата е добавена към хранителния план",
|
||||
"failed-to-add-recipes-to-list": "Неуспешно добавяне на рецепта към списъка",
|
||||
"failed-to-add-recipe-to-mealplan": "Рецептата не беше добавена към хранителния план",
|
||||
"failed-to-add-to-list": "Failed to add to list",
|
||||
"failed-to-add-to-list": "Неуспешно добавяне към списъка",
|
||||
"yield": "Добив",
|
||||
"quantity": "Количество",
|
||||
"choose-unit": "Избери единица",
|
||||
@ -537,8 +537,8 @@
|
||||
"new-recipe-names-must-be-unique": "Името на рецептата трябва да бъде уникално",
|
||||
"scrape-recipe": "Обхождане на рецепта",
|
||||
"scrape-recipe-description": "Обходи рецепта по линк. Предоставете линк за сайт, който искате да бъде обходен. Mealie ще опита да обходи рецептата от този сайт и да я добави във Вашата колекция.",
|
||||
"scrape-recipe-have-a-lot-of-recipes": "Have a lot of recipes you want to scrape at once?",
|
||||
"scrape-recipe-suggest-bulk-importer": "Try out the bulk importer",
|
||||
"scrape-recipe-have-a-lot-of-recipes": "Имате много рецепти, които искате да обходите наведнъж?",
|
||||
"scrape-recipe-suggest-bulk-importer": "Пробвайте масовото импорторане",
|
||||
"import-original-keywords-as-tags": "Импортирай оригиналните ключови думи като тагове",
|
||||
"stay-in-edit-mode": "Остани в режим на редакция",
|
||||
"import-from-zip": "Импортирай от Zip",
|
||||
@ -562,7 +562,7 @@
|
||||
"upload-image": "Качване на изображение",
|
||||
"screen-awake": "Запази екрана активен",
|
||||
"remove-image": "Премахване на изображение",
|
||||
"nextStep": "Next step"
|
||||
"nextStep": "Следваща стъпка"
|
||||
},
|
||||
"search": {
|
||||
"advanced-search": "Разширено търсене",
|
||||
@ -1187,7 +1187,7 @@
|
||||
"require-all-tools": "Изискване на всички инструменти",
|
||||
"cookbook-name": "Име на книгата с рецепти",
|
||||
"cookbook-with-name": "Книга с рецепти {0}",
|
||||
"create-a-cookbook": "Create a Cookbook",
|
||||
"cookbook": "Cookbook"
|
||||
"create-a-cookbook": "Създай Готварска книга",
|
||||
"cookbook": "Готварска книга"
|
||||
}
|
||||
}
|
||||
|
@ -200,7 +200,7 @@
|
||||
"created-on-date": "נוצר ב-{0}",
|
||||
"unsaved-changes": "יש שינויים שלא נשמרו. לצאת לפני שמירה? אשר לשמירה, בטל למחיקת שינויים.",
|
||||
"clipboard-copy-failure": "כשלון בהעתקה ללוח ההדבקה.",
|
||||
"confirm-delete-generic-items": "Are you sure you want to delete the following items?"
|
||||
"confirm-delete-generic-items": "האם אתה בטוח שברצונך למחוק את הפריטים הנבחרים?"
|
||||
},
|
||||
"group": {
|
||||
"are-you-sure-you-want-to-delete-the-group": "האם את/ה בטוח/ה שברצונך למחוק את <b>{groupName}<b/>?",
|
||||
@ -259,7 +259,7 @@
|
||||
},
|
||||
"meal-plan": {
|
||||
"create-a-new-meal-plan": "יצירת תכנית ארוחות חדשה",
|
||||
"update-this-meal-plan": "Update this Meal Plan",
|
||||
"update-this-meal-plan": "עדכן את תכנון הארוחות",
|
||||
"dinner-this-week": "ארוחות ערב השבוע",
|
||||
"dinner-today": "ארוחת ערב היום",
|
||||
"dinner-tonight": "ארוחת ערב היום",
|
||||
@ -474,11 +474,11 @@
|
||||
"add-to-timeline": "הוסף לציר הזמן",
|
||||
"recipe-added-to-list": "מתכון נוסף לרשימה",
|
||||
"recipes-added-to-list": "מתכונים הוספו לרשימה",
|
||||
"successfully-added-to-list": "Successfully added to list",
|
||||
"successfully-added-to-list": "נוסף לרשימה בהצלחה",
|
||||
"recipe-added-to-mealplan": "מתכון נוסף לתכנון ארוחות",
|
||||
"failed-to-add-recipes-to-list": "כשלון בהוספת מתכון לרשימה",
|
||||
"failed-to-add-recipe-to-mealplan": "הוספת מתכון לתכנון ארוחות נכשלה",
|
||||
"failed-to-add-to-list": "Failed to add to list",
|
||||
"failed-to-add-to-list": "כשלון בהוספה לרשימה",
|
||||
"yield": "תשואה",
|
||||
"quantity": "כמות",
|
||||
"choose-unit": "בחירת יחידת מידה",
|
||||
@ -515,7 +515,7 @@
|
||||
"how-did-it-turn-out": "איך יצא?",
|
||||
"user-made-this": "{user} הכין את זה",
|
||||
"last-made-date": "נעשה לאחרונה ב{date}",
|
||||
"api-extras-description": "Recipes extras are a key feature of the Mealie API. They allow you to create custom JSON key/value pairs within a recipe, to reference from 3rd party applications. You can use these keys to provide information, for example to trigger automations or custom messages to relay to your desired device.",
|
||||
"api-extras-description": "מתכונים נוספים הם יכולת מפתח של Mealie API. הם מאפשרים ליצור צמדי key/value בצורת JSON על מנת לקרוא אותם בתוכנת צד שלישית. תוכלו להשתמש בצמדים האלה כדי לספק מידע, לדוגמא להפעיל אוטומציות או הודעות מותאמות אישית למכשירים מסויימים.",
|
||||
"message-key": "מפתח הודעה",
|
||||
"parse": "ניתוח",
|
||||
"attach-images-hint": "הוסף תמונות ע\"י גרירה ושחרור אל תוך העורך",
|
||||
@ -537,8 +537,8 @@
|
||||
"new-recipe-names-must-be-unique": "שם מתכון חדש חייב להיות ייחודי",
|
||||
"scrape-recipe": "קריאת מתכון",
|
||||
"scrape-recipe-description": "קריאת מתכון בעזרת לינק. ספק את הלינק של האתר שברצונך לקרוא, ומילי תנסה לקרוא את המתכון מהאתר ולהוסיף אותו לאוסף.",
|
||||
"scrape-recipe-have-a-lot-of-recipes": "Have a lot of recipes you want to scrape at once?",
|
||||
"scrape-recipe-suggest-bulk-importer": "Try out the bulk importer",
|
||||
"scrape-recipe-have-a-lot-of-recipes": "יש לך הרבה מתכונים שאתה רוצה לקרוא בבת אחת?",
|
||||
"scrape-recipe-suggest-bulk-importer": "נסה את יכולת קריאת רשימה",
|
||||
"import-original-keywords-as-tags": "ייבא שמות מפתח מקוריות כתגיות",
|
||||
"stay-in-edit-mode": "השאר במצב עריכה",
|
||||
"import-from-zip": "ייבא מקובץ",
|
||||
@ -562,7 +562,7 @@
|
||||
"upload-image": "העלה תמונה",
|
||||
"screen-awake": "השאר את המסך פעיל",
|
||||
"remove-image": "האם למחוק את התמונה?",
|
||||
"nextStep": "Next step"
|
||||
"nextStep": "השלב הבא"
|
||||
},
|
||||
"search": {
|
||||
"advanced-search": "חיפוש מתקדם",
|
||||
@ -797,7 +797,7 @@
|
||||
"untagged-count": "לא מתוייג {count}",
|
||||
"create-a-tag": "צור תגית",
|
||||
"tag-name": "שם תגית",
|
||||
"tag": "Tag"
|
||||
"tag": "תגית"
|
||||
},
|
||||
"tool": {
|
||||
"tools": "כלים",
|
||||
@ -807,7 +807,7 @@
|
||||
"create-new-tool": "יצירת כלי חדש",
|
||||
"on-hand-checkbox-label": "הראה מה יש לי במטבח",
|
||||
"required-tools": "צריך כלים",
|
||||
"tool": "Tool"
|
||||
"tool": "כלי"
|
||||
},
|
||||
"user": {
|
||||
"admin": "אדמין",
|
||||
@ -898,10 +898,10 @@
|
||||
"user-can-organize-group-data": "משתמש יכול לשנות מידע של קבוצה",
|
||||
"enable-advanced-features": "אפשר אפשרויות מתקדמות",
|
||||
"it-looks-like-this-is-your-first-time-logging-in": "נראה שזו ההתחברות הראשונה שלך.",
|
||||
"dont-want-to-see-this-anymore-be-sure-to-change-your-email": "Don't want to see this anymore? Be sure to change your email in your user settings!",
|
||||
"dont-want-to-see-this-anymore-be-sure-to-change-your-email": "לא רוצה לראות את זה יותר? דאג לשנות את המייל של בהגדרות המשתמש!",
|
||||
"forgot-password": "שכחתי סיסמא",
|
||||
"forgot-password-text": "Please enter your email address and we will send you a link to reset your password.",
|
||||
"changes-reflected-immediately": "Changes to this user will be reflected immediately."
|
||||
"forgot-password-text": "נא לספק כתובת דוא\"ל. אנו נשלח לך הודעת דוא\"ל לצורך איפוס הסיסמה שלך.",
|
||||
"changes-reflected-immediately": "השינויים למשתמש זה יבוצעו מיידית."
|
||||
},
|
||||
"language-dialog": {
|
||||
"translated": "תורגם",
|
||||
@ -923,8 +923,8 @@
|
||||
"food-label": "תוית אוכל",
|
||||
"edit-food": "עריכת מזון",
|
||||
"food-data": "נתוני אוכל",
|
||||
"example-food-singular": "ex: Onion",
|
||||
"example-food-plural": "ex: Onions"
|
||||
"example-food-singular": "דוגמא: בצל",
|
||||
"example-food-plural": "דוגמא: בצלים"
|
||||
},
|
||||
"units": {
|
||||
"seed-dialog-text": "אכלס את מסד הנתונים עם יחידות מדידה בהתאם לשפה המקומית שלך.",
|
||||
@ -935,7 +935,7 @@
|
||||
"merging-unit-into-unit": "ממזג את {0} לתוך {1}",
|
||||
"create-unit": "יצירת יחידה",
|
||||
"abbreviation": "קיצור",
|
||||
"plural-abbreviation": "Plural Abbreviation",
|
||||
"plural-abbreviation": "צורת הרבית",
|
||||
"description": "תיאור",
|
||||
"display-as-fraction": "הצגה כשבר",
|
||||
"use-abbreviation": "השתמש בקיצור",
|
||||
@ -943,10 +943,10 @@
|
||||
"unit-data": "נתוני יחידה",
|
||||
"use-abbv": "השתמש בקיצור",
|
||||
"fraction": "שבר",
|
||||
"example-unit-singular": "ex: Tablespoon",
|
||||
"example-unit-plural": "ex: Tablespoons",
|
||||
"example-unit-abbreviation-singular": "ex: Tbsp",
|
||||
"example-unit-abbreviation-plural": "ex: Tbsps"
|
||||
"example-unit-singular": "דוגמא: כפית",
|
||||
"example-unit-plural": "דוגמא: כפיות",
|
||||
"example-unit-abbreviation-singular": "דוגמא: כף",
|
||||
"example-unit-abbreviation-plural": "דוגמא: כפות"
|
||||
},
|
||||
"labels": {
|
||||
"seed-dialog-text": "אכלס את מסד הנתונים בתגיות נפוצות בהתאם לשפה המקומית שלך.",
|
||||
@ -1187,7 +1187,7 @@
|
||||
"require-all-tools": "זקוק לכל הכלים",
|
||||
"cookbook-name": "שם ספר בישול",
|
||||
"cookbook-with-name": "ספר בישול {0}",
|
||||
"create-a-cookbook": "Create a Cookbook",
|
||||
"cookbook": "Cookbook"
|
||||
"create-a-cookbook": "צור ספר בישול חדש",
|
||||
"cookbook": "ספר בישול"
|
||||
}
|
||||
}
|
||||
|
@ -200,7 +200,7 @@
|
||||
"created-on-date": "Gemaakt op {0}",
|
||||
"unsaved-changes": "Er zijn niet-opgeslagen wijzigingen. Wil je eerst opslaan voordat je vertrekt? Okay om op te slaan, Annuleren om wijzigingen ongedaan te maken.",
|
||||
"clipboard-copy-failure": "Kopiëren naar klembord mislukt.",
|
||||
"confirm-delete-generic-items": "Are you sure you want to delete the following items?"
|
||||
"confirm-delete-generic-items": "Weet u zeker dat u de volgende items wilt verwijderen?"
|
||||
},
|
||||
"group": {
|
||||
"are-you-sure-you-want-to-delete-the-group": "Weet je zeker dat je <b>{groupName}<b/> wil verwijderen?",
|
||||
|
@ -707,7 +707,7 @@
|
||||
"email-configured": "Email настроен",
|
||||
"email-test-results": "Результаты теста Email",
|
||||
"ready": "Готово",
|
||||
"not-ready": "Не готово - Проверьте переменные окружающей среды",
|
||||
"not-ready": "Не готово - Проверьте переменные окружения",
|
||||
"succeeded": "Выполнено успешно",
|
||||
"failed": "Ошибка",
|
||||
"general-about": "Общая информация",
|
||||
|
@ -31,7 +31,24 @@
|
||||
<BaseButton type="button" :loading="generatingToken" create @click.prevent="handlePasswordReset">
|
||||
{{ $t("user.generate-password-reset-link") }}
|
||||
</BaseButton>
|
||||
<AppButtonCopy v-if="resetUrl" :copy-text="resetUrl"></AppButtonCopy>
|
||||
</div>
|
||||
<div v-if="resetUrl" class="mb-2">
|
||||
<v-card-text>
|
||||
<p class="text-center pb-0">
|
||||
{{ resetUrl }}
|
||||
</p>
|
||||
</v-card-text>
|
||||
<v-card-actions class="align-center pt-0" style="gap: 4px">
|
||||
<BaseButton cancel @click="resetUrl = ''"> {{ $t("general.close") }} </BaseButton>
|
||||
<v-spacer></v-spacer>
|
||||
<BaseButton v-if="user.email" color="info" class="mr-1" @click="sendResetEmail">
|
||||
<template #icon>
|
||||
{{ $globals.icons.email }}
|
||||
</template>
|
||||
{{ $t("user.email") }}
|
||||
</BaseButton>
|
||||
<AppButtonCopy :icon="false" color="info" :copy-text="resetUrl" />
|
||||
</v-card-actions>
|
||||
</div>
|
||||
|
||||
<AutoForm v-model="user" :items="userForm" update-mode :disabled-fields="disabledFields" />
|
||||
@ -46,7 +63,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, useRoute, onMounted, ref, useContext } from "@nuxtjs/composition-api";
|
||||
import { useAdminApi } from "~/composables/api";
|
||||
import { useAdminApi, useUserApi } from "~/composables/api";
|
||||
import { useGroups } from "~/composables/use-groups";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
import { useUserForm } from "~/composables/use-users";
|
||||
@ -118,6 +135,17 @@ export default defineComponent({
|
||||
generatingToken.value = false;
|
||||
}
|
||||
|
||||
const userApi = useUserApi();
|
||||
async function sendResetEmail() {
|
||||
if (!user.value?.email) return;
|
||||
const { response } = await userApi.email.sendForgotPassword({ email: user.value.email });
|
||||
if (response && response.status === 200) {
|
||||
alert.success(i18n.tc("profile.email-sent"));
|
||||
} else {
|
||||
alert.error(i18n.tc("profile.error-sending-email"));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
disabledFields,
|
||||
@ -130,6 +158,7 @@ export default defineComponent({
|
||||
handlePasswordReset,
|
||||
resetUrl,
|
||||
generatingToken,
|
||||
sendResetEmail,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
@ -112,7 +112,7 @@ async def system_startup():
|
||||
logger.info("-----SYSTEM STARTUP----- \n")
|
||||
logger.info("------APP SETTINGS------")
|
||||
logger.info(
|
||||
settings.json(
|
||||
settings.model_dump_json(
|
||||
indent=4,
|
||||
exclude={
|
||||
"SECRET",
|
||||
|
@ -1,7 +1,9 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from pathlib import Path
|
||||
from urllib import parse as urlparse
|
||||
|
||||
from pydantic import BaseModel, BaseSettings, PostgresDsn
|
||||
from pydantic import BaseModel, PostgresDsn
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class AbstractDBProvider(ABC):
|
||||
@ -38,15 +40,19 @@ class PostgresProvider(AbstractDBProvider, BaseSettings):
|
||||
POSTGRES_PORT: str = "5432"
|
||||
POSTGRES_DB: str = "mealie"
|
||||
|
||||
model_config = SettingsConfigDict(arbitrary_types_allowed=True, extra="allow")
|
||||
|
||||
@property
|
||||
def db_url(self) -> str:
|
||||
host = f"{self.POSTGRES_SERVER}:{self.POSTGRES_PORT}"
|
||||
return PostgresDsn.build(
|
||||
scheme="postgresql",
|
||||
user=self.POSTGRES_USER,
|
||||
password=self.POSTGRES_PASSWORD,
|
||||
host=host,
|
||||
path=f"/{self.POSTGRES_DB or ''}",
|
||||
return str(
|
||||
PostgresDsn.build(
|
||||
scheme="postgresql",
|
||||
username=self.POSTGRES_USER,
|
||||
password=urlparse.quote_plus(self.POSTGRES_PASSWORD),
|
||||
host=host,
|
||||
path=f"{self.POSTGRES_DB or ''}",
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
|
@ -1,7 +1,8 @@
|
||||
import secrets
|
||||
from pathlib import Path
|
||||
|
||||
from pydantic import BaseSettings, NoneStr, validator
|
||||
from pydantic import field_validator
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
from mealie.core.settings.themes import Theme
|
||||
|
||||
@ -55,7 +56,8 @@ class AppSettings(BaseSettings):
|
||||
SECURITY_USER_LOCKOUT_TIME: int = 24
|
||||
"time in hours"
|
||||
|
||||
@validator("BASE_URL")
|
||||
@field_validator("BASE_URL")
|
||||
@classmethod
|
||||
def remove_trailing_slash(cls, v: str) -> str:
|
||||
if v and v[-1] == "/":
|
||||
return v[:-1]
|
||||
@ -100,12 +102,12 @@ class AppSettings(BaseSettings):
|
||||
# ===============================================
|
||||
# Email Configuration
|
||||
|
||||
SMTP_HOST: str | None
|
||||
SMTP_HOST: str | None = None
|
||||
SMTP_PORT: str | None = "587"
|
||||
SMTP_FROM_NAME: str | None = "Mealie"
|
||||
SMTP_FROM_EMAIL: str | None
|
||||
SMTP_USER: str | None
|
||||
SMTP_PASSWORD: str | None
|
||||
SMTP_FROM_EMAIL: str | None = None
|
||||
SMTP_USER: str | None = None
|
||||
SMTP_PASSWORD: str | None = None
|
||||
SMTP_AUTH_STRATEGY: str | None = "TLS" # Options: 'TLS', 'SSL', 'NONE'
|
||||
|
||||
@property
|
||||
@ -122,11 +124,11 @@ class AppSettings(BaseSettings):
|
||||
|
||||
@staticmethod
|
||||
def validate_smtp(
|
||||
host: str | None,
|
||||
port: str | None,
|
||||
from_name: str | None,
|
||||
from_email: str | None,
|
||||
strategy: str | None,
|
||||
host: str | None = None,
|
||||
port: str | None = None,
|
||||
from_name: str | None = None,
|
||||
from_email: str | None = None,
|
||||
strategy: str | None = None,
|
||||
user: str | None = None,
|
||||
password: str | None = None,
|
||||
) -> bool:
|
||||
@ -143,15 +145,15 @@ class AppSettings(BaseSettings):
|
||||
# LDAP Configuration
|
||||
|
||||
LDAP_AUTH_ENABLED: bool = False
|
||||
LDAP_SERVER_URL: NoneStr = None
|
||||
LDAP_SERVER_URL: str | None = None
|
||||
LDAP_TLS_INSECURE: bool = False
|
||||
LDAP_TLS_CACERTFILE: NoneStr = None
|
||||
LDAP_TLS_CACERTFILE: str | None = None
|
||||
LDAP_ENABLE_STARTTLS: bool = False
|
||||
LDAP_BASE_DN: NoneStr = None
|
||||
LDAP_QUERY_BIND: NoneStr = None
|
||||
LDAP_QUERY_PASSWORD: NoneStr = None
|
||||
LDAP_USER_FILTER: NoneStr = None
|
||||
LDAP_ADMIN_FILTER: NoneStr = None
|
||||
LDAP_BASE_DN: str | None = None
|
||||
LDAP_QUERY_BIND: str | None = None
|
||||
LDAP_QUERY_PASSWORD: str | None = None
|
||||
LDAP_USER_FILTER: str | None = None
|
||||
LDAP_ADMIN_FILTER: str | None = None
|
||||
LDAP_ID_ATTRIBUTE: str = "uid"
|
||||
LDAP_MAIL_ATTRIBUTE: str = "mail"
|
||||
LDAP_NAME_ATTRIBUTE: str = "name"
|
||||
@ -173,9 +175,7 @@ class AppSettings(BaseSettings):
|
||||
# Testing Config
|
||||
|
||||
TESTING: bool = False
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
model_config = SettingsConfigDict(arbitrary_types_allowed=True, extra="allow")
|
||||
|
||||
|
||||
def app_settings_constructor(data_dir: Path, production: bool, env_file: Path, env_encoding="utf-8") -> AppSettings:
|
||||
|
@ -1,4 +1,4 @@
|
||||
from pydantic import BaseSettings
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class Theme(BaseSettings):
|
||||
@ -17,6 +17,4 @@ class Theme(BaseSettings):
|
||||
dark_info: str = "#1976D2"
|
||||
dark_warning: str = "#FF6D00"
|
||||
dark_error: str = "#EF5350"
|
||||
|
||||
class Config:
|
||||
env_prefix = "theme_"
|
||||
model_config = SettingsConfigDict(env_prefix="theme_", extra="allow")
|
||||
|
@ -1,7 +1,7 @@
|
||||
from functools import wraps
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, Field, NoneStr
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import MANYTOMANY, MANYTOONE, ONETOMANY, Session
|
||||
from sqlalchemy.orm.mapper import Mapper
|
||||
@ -21,7 +21,7 @@ class AutoInitConfig(BaseModel):
|
||||
Config class for `auto_init` decorator.
|
||||
"""
|
||||
|
||||
get_attr: NoneStr = None
|
||||
get_attr: str | None = None
|
||||
exclude: set = Field(default_factory=_default_exclusion)
|
||||
# auto_create: bool = False
|
||||
|
||||
@ -31,16 +31,16 @@ def _get_config(relation_cls: type[SqlAlchemyBase]) -> AutoInitConfig:
|
||||
Returns the config for the given class.
|
||||
"""
|
||||
cfg = AutoInitConfig()
|
||||
cfgKeys = cfg.dict().keys()
|
||||
cfgKeys = cfg.model_dump().keys()
|
||||
# Get the config for the class
|
||||
try:
|
||||
class_config: AutoInitConfig = relation_cls.Config
|
||||
class_config: ConfigDict = relation_cls.model_config
|
||||
except AttributeError:
|
||||
return cfg
|
||||
# Map all matching attributes in Config to all AutoInitConfig attributes
|
||||
for attr in dir(class_config):
|
||||
for attr in class_config:
|
||||
if attr in cfgKeys:
|
||||
setattr(cfg, attr, getattr(class_config, attr))
|
||||
setattr(cfg, attr, class_config[attr])
|
||||
|
||||
return cfg
|
||||
|
||||
@ -97,7 +97,7 @@ def handle_one_to_many_list(
|
||||
|
||||
updated_elems.append(existing_elem)
|
||||
|
||||
new_elems = [safe_call(relation_cls, elem, session=session) for elem in elems_to_create]
|
||||
new_elems = [safe_call(relation_cls, elem.copy(), session=session) for elem in elems_to_create]
|
||||
return new_elems + updated_elems
|
||||
|
||||
|
||||
@ -164,7 +164,7 @@ def auto_init(): # sourcery no-metrics
|
||||
setattr(self, key, instances)
|
||||
|
||||
elif relation_dir == ONETOMANY:
|
||||
instance = safe_call(relation_cls, val, session=session)
|
||||
instance = safe_call(relation_cls, val.copy() if val else None, session=session)
|
||||
setattr(self, key, instance)
|
||||
|
||||
elif relation_dir == MANYTOONE and not use_list:
|
||||
|
@ -29,12 +29,15 @@ def get_valid_call(func: Callable, args_dict) -> dict:
|
||||
return {k: v for k, v in args_dict.items() if k in valid_args}
|
||||
|
||||
|
||||
def safe_call(func, dict_args: dict, **kwargs) -> Any:
|
||||
def safe_call(func, dict_args: dict | None, **kwargs) -> Any:
|
||||
"""
|
||||
Safely calls the supplied function with the supplied dictionary of arguments.
|
||||
by removing any invalid arguments.
|
||||
"""
|
||||
|
||||
if dict_args is None:
|
||||
dict_args = {}
|
||||
|
||||
if kwargs:
|
||||
dict_args.update(kwargs)
|
||||
|
||||
|
@ -2,6 +2,7 @@ from typing import TYPE_CHECKING, Optional
|
||||
|
||||
import sqlalchemy as sa
|
||||
import sqlalchemy.orm as orm
|
||||
from pydantic import ConfigDict
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from sqlalchemy.orm.session import Session
|
||||
@ -79,9 +80,8 @@ class Group(SqlAlchemyBase, BaseMixins):
|
||||
ingredient_foods: Mapped[list["IngredientFoodModel"]] = orm.relationship("IngredientFoodModel", **common_args)
|
||||
tools: Mapped[list["Tool"]] = orm.relationship("Tool", **common_args)
|
||||
tags: Mapped[list["Tag"]] = orm.relationship("Tag", **common_args)
|
||||
|
||||
class Config:
|
||||
exclude = {
|
||||
model_config = ConfigDict(
|
||||
exclude={
|
||||
"users",
|
||||
"webhooks",
|
||||
"shopping_lists",
|
||||
@ -91,6 +91,7 @@ class Group(SqlAlchemyBase, BaseMixins):
|
||||
"mealplans",
|
||||
"data_exports",
|
||||
}
|
||||
)
|
||||
|
||||
@auto_init()
|
||||
def __init__(self, **_) -> None:
|
||||
|
@ -1,6 +1,7 @@
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from pydantic import ConfigDict
|
||||
from sqlalchemy import ForeignKey, orm
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from sqlalchemy.sql.sqltypes import Boolean, DateTime, String
|
||||
@ -47,9 +48,7 @@ class ReportModel(SqlAlchemyBase, BaseMixins):
|
||||
# Relationships
|
||||
group_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
|
||||
group: Mapped["Group"] = orm.relationship("Group", back_populates="group_reports", single_parent=True)
|
||||
|
||||
class Config:
|
||||
exclude = ["entries"]
|
||||
model_config = ConfigDict(exclude=["entries"])
|
||||
|
||||
@auto_init()
|
||||
def __init__(self, **_) -> None:
|
||||
|
@ -1,5 +1,6 @@
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from pydantic import ConfigDict
|
||||
from sqlalchemy import Boolean, Float, ForeignKey, Integer, String, UniqueConstraint, orm
|
||||
from sqlalchemy.ext.orderinglist import ordering_list
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
@ -69,9 +70,7 @@ class ShoppingListItem(SqlAlchemyBase, BaseMixins):
|
||||
recipe_references: Mapped[list[ShoppingListItemRecipeReference]] = orm.relationship(
|
||||
ShoppingListItemRecipeReference, cascade="all, delete, delete-orphan"
|
||||
)
|
||||
|
||||
class Config:
|
||||
exclude = {"id", "label", "food", "unit"}
|
||||
model_config = ConfigDict(exclude={"id", "label", "food", "unit"})
|
||||
|
||||
@api_extras
|
||||
@auto_init()
|
||||
@ -91,9 +90,7 @@ class ShoppingListRecipeReference(BaseMixins, SqlAlchemyBase):
|
||||
)
|
||||
|
||||
recipe_quantity: Mapped[float] = mapped_column(Float, nullable=False)
|
||||
|
||||
class Config:
|
||||
exclude = {"id", "recipe"}
|
||||
model_config = ConfigDict(exclude={"id", "recipe"})
|
||||
|
||||
@auto_init()
|
||||
def __init__(self, **_) -> None:
|
||||
@ -112,9 +109,7 @@ class ShoppingListMultiPurposeLabel(SqlAlchemyBase, BaseMixins):
|
||||
"MultiPurposeLabel", back_populates="shopping_lists_label_settings"
|
||||
)
|
||||
position: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
|
||||
class Config:
|
||||
exclude = {"label"}
|
||||
model_config = ConfigDict(exclude={"label"})
|
||||
|
||||
@auto_init()
|
||||
def __init__(self, **_) -> None:
|
||||
@ -146,9 +141,7 @@ class ShoppingList(SqlAlchemyBase, BaseMixins):
|
||||
collection_class=ordering_list("position"),
|
||||
)
|
||||
extras: Mapped[list[ShoppingListExtras]] = orm.relationship("ShoppingListExtras", cascade="all, delete-orphan")
|
||||
|
||||
class Config:
|
||||
exclude = {"id", "list_items"}
|
||||
model_config = ConfigDict(exclude={"id", "list_items"})
|
||||
|
||||
@api_extras
|
||||
@auto_init()
|
||||
|
@ -1,3 +1,4 @@
|
||||
from pydantic import ConfigDict
|
||||
from sqlalchemy import ForeignKey, Integer, String, orm
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
@ -28,12 +29,12 @@ class RecipeInstruction(SqlAlchemyBase):
|
||||
ingredient_references: Mapped[list[RecipeIngredientRefLink]] = orm.relationship(
|
||||
RecipeIngredientRefLink, cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
class Config:
|
||||
exclude = {
|
||||
model_config = ConfigDict(
|
||||
exclude={
|
||||
"id",
|
||||
"ingredient_references",
|
||||
}
|
||||
)
|
||||
|
||||
@auto_init()
|
||||
def __init__(self, ingredient_references, session, **_) -> None:
|
||||
|
@ -3,6 +3,7 @@ from typing import TYPE_CHECKING
|
||||
|
||||
import sqlalchemy as sa
|
||||
import sqlalchemy.orm as orm
|
||||
from pydantic import ConfigDict
|
||||
from sqlalchemy import event
|
||||
from sqlalchemy.ext.orderinglist import ordering_list
|
||||
from sqlalchemy.orm import Mapped, mapped_column, validates
|
||||
@ -134,10 +135,9 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||
# Automatically updated by sqlalchemy event, do not write to this manually
|
||||
name_normalized: Mapped[str] = mapped_column(sa.String, nullable=False, index=True)
|
||||
description_normalized: Mapped[str | None] = mapped_column(sa.String, index=True)
|
||||
|
||||
class Config:
|
||||
get_attr = "slug"
|
||||
exclude = {
|
||||
model_config = ConfigDict(
|
||||
get_attr="slug",
|
||||
exclude={
|
||||
"assets",
|
||||
"notes",
|
||||
"nutrition",
|
||||
@ -146,7 +146,8 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||
"settings",
|
||||
"comments",
|
||||
"timeline_events",
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@validates("name")
|
||||
def validate_name(self, _, name):
|
||||
|
@ -2,6 +2,7 @@ import enum
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from pydantic import ConfigDict
|
||||
from sqlalchemy import Boolean, DateTime, Enum, ForeignKey, Integer, String, orm
|
||||
from sqlalchemy.ext.hybrid import hybrid_property
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
@ -84,9 +85,8 @@ class User(SqlAlchemyBase, BaseMixins):
|
||||
favorite_recipes: Mapped[list["RecipeModel"]] = orm.relationship(
|
||||
"RecipeModel", secondary=users_to_favorites, back_populates="favorited_by"
|
||||
)
|
||||
|
||||
class Config:
|
||||
exclude = {
|
||||
model_config = ConfigDict(
|
||||
exclude={
|
||||
"password",
|
||||
"admin",
|
||||
"can_manage",
|
||||
@ -94,6 +94,7 @@ class User(SqlAlchemyBase, BaseMixins):
|
||||
"can_organize",
|
||||
"group",
|
||||
}
|
||||
)
|
||||
|
||||
@hybrid_property
|
||||
def group_slug(self) -> str:
|
||||
|
@ -31,5 +31,14 @@
|
||||
"generic-updated-with-url": "{name} is opgedateer, {url}",
|
||||
"generic-duplicated": "{name} is gekopieer",
|
||||
"generic-deleted": "{name} is verwyder"
|
||||
},
|
||||
"datetime": {
|
||||
"year": "year|years",
|
||||
"day": "day|days",
|
||||
"hour": "hour|hours",
|
||||
"minute": "minute|minutes",
|
||||
"second": "second|seconds",
|
||||
"millisecond": "millisecond|milliseconds",
|
||||
"microsecond": "microsecond|microseconds"
|
||||
}
|
||||
}
|
||||
|
@ -31,5 +31,14 @@
|
||||
"generic-updated-with-url": "{name} تم تحديثه، {url}",
|
||||
"generic-duplicated": "تم تكرار {name}",
|
||||
"generic-deleted": "تم حذف {name}"
|
||||
},
|
||||
"datetime": {
|
||||
"year": "year|years",
|
||||
"day": "day|days",
|
||||
"hour": "hour|hours",
|
||||
"minute": "minute|minutes",
|
||||
"second": "second|seconds",
|
||||
"millisecond": "millisecond|milliseconds",
|
||||
"microsecond": "microsecond|microseconds"
|
||||
}
|
||||
}
|
||||
|
@ -31,5 +31,14 @@
|
||||
"generic-updated-with-url": "{name} беше актуализирано, {url}",
|
||||
"generic-duplicated": "{name} е дублицирано",
|
||||
"generic-deleted": "{name} беше изтрито"
|
||||
},
|
||||
"datetime": {
|
||||
"year": "година|години",
|
||||
"day": "ден|дни",
|
||||
"hour": "час|часове",
|
||||
"minute": "минута|минути",
|
||||
"second": "секунда|секунди",
|
||||
"millisecond": "милисекунда|милисекунди",
|
||||
"microsecond": "микросекунда|микросекунди"
|
||||
}
|
||||
}
|
||||
|
@ -31,5 +31,14 @@
|
||||
"generic-updated-with-url": "{name} ha estat actualitzat, {url}",
|
||||
"generic-duplicated": "S'ha duplicat {name}",
|
||||
"generic-deleted": "{name} ha estat eliminat"
|
||||
},
|
||||
"datetime": {
|
||||
"year": "year|years",
|
||||
"day": "day|days",
|
||||
"hour": "hour|hours",
|
||||
"minute": "minute|minutes",
|
||||
"second": "second|seconds",
|
||||
"millisecond": "millisecond|milliseconds",
|
||||
"microsecond": "microsecond|microseconds"
|
||||
}
|
||||
}
|
||||
|
@ -31,5 +31,14 @@
|
||||
"generic-updated-with-url": "{name} byl aktualizován, {url}",
|
||||
"generic-duplicated": "{name} byl duplikován",
|
||||
"generic-deleted": "{name} byl odstraněn"
|
||||
},
|
||||
"datetime": {
|
||||
"year": "year|years",
|
||||
"day": "day|days",
|
||||
"hour": "hour|hours",
|
||||
"minute": "minute|minutes",
|
||||
"second": "second|seconds",
|
||||
"millisecond": "millisecond|milliseconds",
|
||||
"microsecond": "microsecond|microseconds"
|
||||
}
|
||||
}
|
||||
|
@ -31,5 +31,14 @@
|
||||
"generic-updated-with-url": "{name} er blevet opdateret, {url}",
|
||||
"generic-duplicated": "{name} er blevet dublikeret",
|
||||
"generic-deleted": "{name} er blevet slettet"
|
||||
},
|
||||
"datetime": {
|
||||
"year": "år|år",
|
||||
"day": "dag|dage",
|
||||
"hour": "time|timer",
|
||||
"minute": "minut|minutter",
|
||||
"second": "sekund|sekunder",
|
||||
"millisecond": "millisekund|millisekunder",
|
||||
"microsecond": "mikrosekund|mikrosekunder"
|
||||
}
|
||||
}
|
||||
|
@ -31,5 +31,14 @@
|
||||
"generic-updated-with-url": "{name} wurde aktualisiert, {url}",
|
||||
"generic-duplicated": "{name} wurde dupliziert",
|
||||
"generic-deleted": "{name} wurde gelöscht"
|
||||
},
|
||||
"datetime": {
|
||||
"year": "year|years",
|
||||
"day": "day|days",
|
||||
"hour": "hour|hours",
|
||||
"minute": "minute|minutes",
|
||||
"second": "second|seconds",
|
||||
"millisecond": "millisecond|milliseconds",
|
||||
"microsecond": "microsecond|microseconds"
|
||||
}
|
||||
}
|
||||
|
@ -31,5 +31,14 @@
|
||||
"generic-updated-with-url": "{name} has been updated, {url}",
|
||||
"generic-duplicated": "{name} has been duplicated",
|
||||
"generic-deleted": "{name} has been deleted"
|
||||
},
|
||||
"datetime": {
|
||||
"year": "year|years",
|
||||
"day": "day|days",
|
||||
"hour": "hour|hours",
|
||||
"minute": "minute|minutes",
|
||||
"second": "second|seconds",
|
||||
"millisecond": "millisecond|milliseconds",
|
||||
"microsecond": "microsecond|microseconds"
|
||||
}
|
||||
}
|
||||
|
@ -31,5 +31,14 @@
|
||||
"generic-updated-with-url": "{name} has been updated, {url}",
|
||||
"generic-duplicated": "{name} has been duplicated",
|
||||
"generic-deleted": "{name} has been deleted"
|
||||
},
|
||||
"datetime": {
|
||||
"year": "year|years",
|
||||
"day": "day|days",
|
||||
"hour": "hour|hours",
|
||||
"minute": "minute|minutes",
|
||||
"second": "second|seconds",
|
||||
"millisecond": "millisecond|milliseconds",
|
||||
"microsecond": "microsecond|microseconds"
|
||||
}
|
||||
}
|
||||
|
@ -31,5 +31,14 @@
|
||||
"generic-updated-with-url": "{name} has been updated, {url}",
|
||||
"generic-duplicated": "{name} has been duplicated",
|
||||
"generic-deleted": "{name} has been deleted"
|
||||
},
|
||||
"datetime": {
|
||||
"year": "year|years",
|
||||
"day": "day|days",
|
||||
"hour": "hour|hours",
|
||||
"minute": "minute|minutes",
|
||||
"second": "second|seconds",
|
||||
"millisecond": "millisecond|milliseconds",
|
||||
"microsecond": "microsecond|microseconds"
|
||||
}
|
||||
}
|
||||
|
@ -31,5 +31,14 @@
|
||||
"generic-updated-with-url": "Se ha actualizado {name}, {url}",
|
||||
"generic-duplicated": "Se ha duplicado {name}",
|
||||
"generic-deleted": "Se ha eliminado {name}"
|
||||
},
|
||||
"datetime": {
|
||||
"year": "year|years",
|
||||
"day": "day|days",
|
||||
"hour": "hour|hours",
|
||||
"minute": "minute|minutes",
|
||||
"second": "second|seconds",
|
||||
"millisecond": "millisecond|milliseconds",
|
||||
"microsecond": "microsecond|microseconds"
|
||||
}
|
||||
}
|
||||
|
@ -31,5 +31,14 @@
|
||||
"generic-updated-with-url": "{name} on päivitetty, {url}",
|
||||
"generic-duplicated": "{name} on kahdennettu",
|
||||
"generic-deleted": "{name} on poistettu"
|
||||
},
|
||||
"datetime": {
|
||||
"year": "year|years",
|
||||
"day": "day|days",
|
||||
"hour": "hour|hours",
|
||||
"minute": "minute|minutes",
|
||||
"second": "second|seconds",
|
||||
"millisecond": "millisecond|milliseconds",
|
||||
"microsecond": "microsecond|microseconds"
|
||||
}
|
||||
}
|
||||
|
@ -31,5 +31,14 @@
|
||||
"generic-updated-with-url": "{name} a été mis à jour, {url}",
|
||||
"generic-duplicated": "{name} a été dupliqué",
|
||||
"generic-deleted": "{name} a été supprimé"
|
||||
},
|
||||
"datetime": {
|
||||
"year": "year|years",
|
||||
"day": "day|days",
|
||||
"hour": "hour|hours",
|
||||
"minute": "minute|minutes",
|
||||
"second": "second|seconds",
|
||||
"millisecond": "millisecond|milliseconds",
|
||||
"microsecond": "microsecond|microseconds"
|
||||
}
|
||||
}
|
||||
|
@ -31,5 +31,14 @@
|
||||
"generic-updated-with-url": "{name} a été mis à jour, {url}",
|
||||
"generic-duplicated": "{name} a été dupliqué",
|
||||
"generic-deleted": "{name} a été supprimé"
|
||||
},
|
||||
"datetime": {
|
||||
"year": "année|années",
|
||||
"day": "jour|jours",
|
||||
"hour": "heure|heures",
|
||||
"minute": "minute|minutes",
|
||||
"second": "seconde|secondes",
|
||||
"millisecond": "milliseconde|millisecondes",
|
||||
"microsecond": "microseconde|microsecondes"
|
||||
}
|
||||
}
|
||||
|
@ -31,5 +31,14 @@
|
||||
"generic-updated-with-url": "{name} has been updated, {url}",
|
||||
"generic-duplicated": "{name} has been duplicated",
|
||||
"generic-deleted": "{name} has been deleted"
|
||||
},
|
||||
"datetime": {
|
||||
"year": "year|years",
|
||||
"day": "day|days",
|
||||
"hour": "hour|hours",
|
||||
"minute": "minute|minutes",
|
||||
"second": "second|seconds",
|
||||
"millisecond": "millisecond|milliseconds",
|
||||
"microsecond": "microsecond|microseconds"
|
||||
}
|
||||
}
|
||||
|
@ -31,5 +31,14 @@
|
||||
"generic-updated-with-url": "{name} עודכן, {url}",
|
||||
"generic-duplicated": "{name} שוכפל",
|
||||
"generic-deleted": "{name} נמחק"
|
||||
},
|
||||
"datetime": {
|
||||
"year": "year|years",
|
||||
"day": "day|days",
|
||||
"hour": "hour|hours",
|
||||
"minute": "minute|minutes",
|
||||
"second": "second|seconds",
|
||||
"millisecond": "millisecond|milliseconds",
|
||||
"microsecond": "microsecond|microseconds"
|
||||
}
|
||||
}
|
||||
|
@ -31,5 +31,14 @@
|
||||
"generic-updated-with-url": "{name} je ažuriran, {url}",
|
||||
"generic-duplicated": "{name} je dupliciran",
|
||||
"generic-deleted": "{name} je obrisan"
|
||||
},
|
||||
"datetime": {
|
||||
"year": "year|years",
|
||||
"day": "day|days",
|
||||
"hour": "hour|hours",
|
||||
"minute": "minute|minutes",
|
||||
"second": "second|seconds",
|
||||
"millisecond": "millisecond|milliseconds",
|
||||
"microsecond": "microsecond|microseconds"
|
||||
}
|
||||
}
|
||||
|
@ -31,5 +31,14 @@
|
||||
"generic-updated-with-url": "{name} frissítve, {url}",
|
||||
"generic-duplicated": "{name} duplikálva",
|
||||
"generic-deleted": "{name} törölve lett"
|
||||
},
|
||||
"datetime": {
|
||||
"year": "év",
|
||||
"day": "nap",
|
||||
"hour": "óra",
|
||||
"minute": "perc",
|
||||
"second": "másodperc",
|
||||
"millisecond": "ezredmásodperc",
|
||||
"microsecond": "mikroszekundum"
|
||||
}
|
||||
}
|
||||
|
@ -31,5 +31,14 @@
|
||||
"generic-updated-with-url": "{name} has been updated, {url}",
|
||||
"generic-duplicated": "{name} has been duplicated",
|
||||
"generic-deleted": "{name} has been deleted"
|
||||
},
|
||||
"datetime": {
|
||||
"year": "year|years",
|
||||
"day": "day|days",
|
||||
"hour": "hour|hours",
|
||||
"minute": "minute|minutes",
|
||||
"second": "second|seconds",
|
||||
"millisecond": "millisecond|milliseconds",
|
||||
"microsecond": "microsecond|microseconds"
|
||||
}
|
||||
}
|
||||
|
@ -31,5 +31,14 @@
|
||||
"generic-updated-with-url": "{name} è stato aggiornato, {url}",
|
||||
"generic-duplicated": "{name} è stato duplicato",
|
||||
"generic-deleted": "{name} è stato eliminato"
|
||||
},
|
||||
"datetime": {
|
||||
"year": "anno|anni",
|
||||
"day": "giorno|giorni",
|
||||
"hour": "ora|ore",
|
||||
"minute": "minuto|minuti",
|
||||
"second": "secondo|secondi",
|
||||
"millisecond": "millisecondo|millisecondi",
|
||||
"microsecond": "microsecondo|microsecondi"
|
||||
}
|
||||
}
|
||||
|
@ -31,5 +31,14 @@
|
||||
"generic-updated-with-url": "{name} has been updated, {url}",
|
||||
"generic-duplicated": "{name} has been duplicated",
|
||||
"generic-deleted": "{name} has been deleted"
|
||||
},
|
||||
"datetime": {
|
||||
"year": "year|years",
|
||||
"day": "day|days",
|
||||
"hour": "hour|hours",
|
||||
"minute": "minute|minutes",
|
||||
"second": "second|seconds",
|
||||
"millisecond": "millisecond|milliseconds",
|
||||
"microsecond": "microsecond|microseconds"
|
||||
}
|
||||
}
|
||||
|
@ -31,5 +31,14 @@
|
||||
"generic-updated-with-url": "{name} has been updated, {url}",
|
||||
"generic-duplicated": "{name} has been duplicated",
|
||||
"generic-deleted": "{name} has been deleted"
|
||||
},
|
||||
"datetime": {
|
||||
"year": "year|years",
|
||||
"day": "day|days",
|
||||
"hour": "hour|hours",
|
||||
"minute": "minute|minutes",
|
||||
"second": "second|seconds",
|
||||
"millisecond": "millisecond|milliseconds",
|
||||
"microsecond": "microsecond|microseconds"
|
||||
}
|
||||
}
|
||||
|
@ -31,5 +31,14 @@
|
||||
"generic-updated-with-url": "{name} atnaujintas, {url}",
|
||||
"generic-duplicated": "{name} buvo nukopijuotas",
|
||||
"generic-deleted": "{name} ištrintas"
|
||||
},
|
||||
"datetime": {
|
||||
"year": "year|years",
|
||||
"day": "day|days",
|
||||
"hour": "hour|hours",
|
||||
"minute": "minute|minutes",
|
||||
"second": "second|seconds",
|
||||
"millisecond": "millisecond|milliseconds",
|
||||
"microsecond": "microsecond|microseconds"
|
||||
}
|
||||
}
|
||||
|
@ -31,5 +31,14 @@
|
||||
"generic-updated-with-url": "{name} has been updated, {url}",
|
||||
"generic-duplicated": "{name} has been duplicated",
|
||||
"generic-deleted": "{name} has been deleted"
|
||||
},
|
||||
"datetime": {
|
||||
"year": "year|years",
|
||||
"day": "day|days",
|
||||
"hour": "hour|hours",
|
||||
"minute": "minute|minutes",
|
||||
"second": "second|seconds",
|
||||
"millisecond": "millisecond|milliseconds",
|
||||
"microsecond": "microsecond|microseconds"
|
||||
}
|
||||
}
|
||||
|
@ -31,5 +31,14 @@
|
||||
"generic-updated-with-url": "{name} is bijgewerkt, {url}",
|
||||
"generic-duplicated": "(naam) is gekopieerd",
|
||||
"generic-deleted": "{name} is verwijderd"
|
||||
},
|
||||
"datetime": {
|
||||
"year": "jaar|jaren",
|
||||
"day": "dag|dagen",
|
||||
"hour": "uur|uren",
|
||||
"minute": "minuut|minuten",
|
||||
"second": "seconde|seconden",
|
||||
"millisecond": "milliseconde milliseconden",
|
||||
"microsecond": "microseconde microseconden"
|
||||
}
|
||||
}
|
||||
|
@ -31,5 +31,14 @@
|
||||
"generic-updated-with-url": "{name} har blitt oppdatert, {url}",
|
||||
"generic-duplicated": "{name} har blitt duplisert",
|
||||
"generic-deleted": "{name} har blitt slettet"
|
||||
},
|
||||
"datetime": {
|
||||
"year": "year|years",
|
||||
"day": "day|days",
|
||||
"hour": "hour|hours",
|
||||
"minute": "minute|minutes",
|
||||
"second": "second|seconds",
|
||||
"millisecond": "millisecond|milliseconds",
|
||||
"microsecond": "microsecond|microseconds"
|
||||
}
|
||||
}
|
||||
|
@ -31,5 +31,14 @@
|
||||
"generic-updated-with-url": "{name} został zaktualizowany. {url}",
|
||||
"generic-duplicated": "{name} został zduplikowany",
|
||||
"generic-deleted": "{name} został usunięty"
|
||||
},
|
||||
"datetime": {
|
||||
"year": "year|years",
|
||||
"day": "day|days",
|
||||
"hour": "hour|hours",
|
||||
"minute": "minute|minutes",
|
||||
"second": "second|seconds",
|
||||
"millisecond": "millisecond|milliseconds",
|
||||
"microsecond": "microsecond|microseconds"
|
||||
}
|
||||
}
|
||||
|
@ -31,5 +31,14 @@
|
||||
"generic-updated-with-url": "{name} foi atualizado, {url}",
|
||||
"generic-duplicated": "{name} foi duplicada",
|
||||
"generic-deleted": "{name} foi excluído"
|
||||
},
|
||||
"datetime": {
|
||||
"year": "year|years",
|
||||
"day": "day|days",
|
||||
"hour": "hour|hours",
|
||||
"minute": "minute|minutes",
|
||||
"second": "second|seconds",
|
||||
"millisecond": "millisecond|milliseconds",
|
||||
"microsecond": "microsecond|microseconds"
|
||||
}
|
||||
}
|
||||
|
@ -31,5 +31,14 @@
|
||||
"generic-updated-with-url": "{name} foi atualizado, {url}",
|
||||
"generic-duplicated": "{name} foi duplicado",
|
||||
"generic-deleted": "{name} foi removido"
|
||||
},
|
||||
"datetime": {
|
||||
"year": "ano|anos",
|
||||
"day": "dia|dias",
|
||||
"hour": "hora|horas",
|
||||
"minute": "minuto|minutos",
|
||||
"second": "segundo|segundos",
|
||||
"millisecond": "milissegundo|milissegundos",
|
||||
"microsecond": "microssegundo|microssegundos"
|
||||
}
|
||||
}
|
||||
|
@ -31,5 +31,14 @@
|
||||
"generic-updated-with-url": "{name} a fost actualizat, {url}",
|
||||
"generic-duplicated": "{name} a fost duplicat",
|
||||
"generic-deleted": "{name} a fost șters"
|
||||
},
|
||||
"datetime": {
|
||||
"year": "year|years",
|
||||
"day": "day|days",
|
||||
"hour": "hour|hours",
|
||||
"minute": "minute|minutes",
|
||||
"second": "second|seconds",
|
||||
"millisecond": "millisecond|milliseconds",
|
||||
"microsecond": "microsecond|microseconds"
|
||||
}
|
||||
}
|
||||
|
@ -31,5 +31,14 @@
|
||||
"generic-updated-with-url": "{name} был обновлен, {url}",
|
||||
"generic-duplicated": "Копия {name} была создана",
|
||||
"generic-deleted": "{name} был удален"
|
||||
},
|
||||
"datetime": {
|
||||
"year": "year|years",
|
||||
"day": "day|days",
|
||||
"hour": "hour|hours",
|
||||
"minute": "minute|minutes",
|
||||
"second": "second|seconds",
|
||||
"millisecond": "millisecond|milliseconds",
|
||||
"microsecond": "microsecond|microseconds"
|
||||
}
|
||||
}
|
||||
|
@ -31,5 +31,14 @@
|
||||
"generic-updated-with-url": "{name} bol aktualizovaný, {url}",
|
||||
"generic-duplicated": "{name} bol duplikovaný",
|
||||
"generic-deleted": "{name} bol vymazaný"
|
||||
},
|
||||
"datetime": {
|
||||
"year": "year|years",
|
||||
"day": "day|days",
|
||||
"hour": "hour|hours",
|
||||
"minute": "minute|minutes",
|
||||
"second": "second|seconds",
|
||||
"millisecond": "millisecond|milliseconds",
|
||||
"microsecond": "microsecond|microseconds"
|
||||
}
|
||||
}
|
||||
|
@ -31,5 +31,14 @@
|
||||
"generic-updated-with-url": "{name} je bil posodobljen, {url}",
|
||||
"generic-duplicated": "{name} je bilo podvojeno",
|
||||
"generic-deleted": "{name} je bil izbrisan"
|
||||
},
|
||||
"datetime": {
|
||||
"year": "year|years",
|
||||
"day": "day|days",
|
||||
"hour": "hour|hours",
|
||||
"minute": "minute|minutes",
|
||||
"second": "second|seconds",
|
||||
"millisecond": "millisecond|milliseconds",
|
||||
"microsecond": "microsecond|microseconds"
|
||||
}
|
||||
}
|
||||
|
@ -31,5 +31,14 @@
|
||||
"generic-updated-with-url": "{name} је ажурирано, {url}",
|
||||
"generic-duplicated": "{name} је дуплиран",
|
||||
"generic-deleted": "{name} је обрисан"
|
||||
},
|
||||
"datetime": {
|
||||
"year": "year|years",
|
||||
"day": "day|days",
|
||||
"hour": "hour|hours",
|
||||
"minute": "minute|minutes",
|
||||
"second": "second|seconds",
|
||||
"millisecond": "millisecond|milliseconds",
|
||||
"microsecond": "microsecond|microseconds"
|
||||
}
|
||||
}
|
||||
|
@ -31,5 +31,14 @@
|
||||
"generic-updated-with-url": "{name} har uppdaterats, {url}",
|
||||
"generic-duplicated": "{name} har duplicerats",
|
||||
"generic-deleted": "{name} har tagits bort"
|
||||
},
|
||||
"datetime": {
|
||||
"year": "år|år",
|
||||
"day": "dag|dagar",
|
||||
"hour": "timme|timmar",
|
||||
"minute": "minut|minuter",
|
||||
"second": "sekund|sekunder",
|
||||
"millisecond": "millisecond|milliseconds",
|
||||
"microsecond": "microsecond|microseconds"
|
||||
}
|
||||
}
|
||||
|
@ -31,5 +31,14 @@
|
||||
"generic-updated-with-url": "{name} güncellendi, {url}",
|
||||
"generic-duplicated": "{name} yinelendi",
|
||||
"generic-deleted": "{name} silindi"
|
||||
},
|
||||
"datetime": {
|
||||
"year": "yıl|yıllar",
|
||||
"day": "gün|günler",
|
||||
"hour": "saat|saatler",
|
||||
"minute": "dakika|dakikalar",
|
||||
"second": "saniye|saniyeler",
|
||||
"millisecond": "milisaniye|milisaniyeler",
|
||||
"microsecond": "mikrosaniye|mikrosaniyeler"
|
||||
}
|
||||
}
|
||||
|
@ -31,5 +31,14 @@
|
||||
"generic-updated-with-url": "{name} оновлено, {url}",
|
||||
"generic-duplicated": "{name} дубльовано",
|
||||
"generic-deleted": "{name} видалено"
|
||||
},
|
||||
"datetime": {
|
||||
"year": "рік|роки",
|
||||
"day": "день|дні",
|
||||
"hour": "година|години",
|
||||
"minute": "хвилина|хвилини",
|
||||
"second": "секунда|секунди",
|
||||
"millisecond": "мілісекунда|мілісекунди",
|
||||
"microsecond": "мікросекунда|мікросекунди"
|
||||
}
|
||||
}
|
||||
|
@ -31,5 +31,14 @@
|
||||
"generic-updated-with-url": "{name} has been updated, {url}",
|
||||
"generic-duplicated": "{name} has been duplicated",
|
||||
"generic-deleted": "{name} has been deleted"
|
||||
},
|
||||
"datetime": {
|
||||
"year": "year|years",
|
||||
"day": "day|days",
|
||||
"hour": "hour|hours",
|
||||
"minute": "minute|minutes",
|
||||
"second": "second|seconds",
|
||||
"millisecond": "millisecond|milliseconds",
|
||||
"microsecond": "microsecond|microseconds"
|
||||
}
|
||||
}
|
||||
|
@ -31,5 +31,14 @@
|
||||
"generic-updated-with-url": "{name} 已创建, {url}",
|
||||
"generic-duplicated": "{name} 已复制",
|
||||
"generic-deleted": "{name} 已删除"
|
||||
},
|
||||
"datetime": {
|
||||
"year": "year|years",
|
||||
"day": "day|days",
|
||||
"hour": "hour|hours",
|
||||
"minute": "minute|minutes",
|
||||
"second": "second|seconds",
|
||||
"millisecond": "millisecond|milliseconds",
|
||||
"microsecond": "microsecond|microseconds"
|
||||
}
|
||||
}
|
||||
|
@ -31,5 +31,14 @@
|
||||
"generic-updated-with-url": "{name} has been updated, {url}",
|
||||
"generic-duplicated": "{name} has been duplicated",
|
||||
"generic-deleted": "{name} has been deleted"
|
||||
},
|
||||
"datetime": {
|
||||
"year": "year|years",
|
||||
"day": "day|days",
|
||||
"hour": "hour|hours",
|
||||
"minute": "minute|minutes",
|
||||
"second": "second|seconds",
|
||||
"millisecond": "millisecond|milliseconds",
|
||||
"microsecond": "microsecond|microseconds"
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
@ -13,6 +14,22 @@ class JsonProvider:
|
||||
else:
|
||||
self.translations = path
|
||||
|
||||
def _parse_plurals(self, value: str, count: float):
|
||||
# based off of: https://kazupon.github.io/vue-i18n/guide/pluralization.html
|
||||
|
||||
values = [v.strip() for v in value.split("|")]
|
||||
if len(values) == 1:
|
||||
return value
|
||||
elif len(values) == 2:
|
||||
return values[0] if count == 1 else values[1]
|
||||
elif len(values) == 3:
|
||||
if count == 0:
|
||||
return values[0]
|
||||
else:
|
||||
return values[1] if count == 1 else values[2]
|
||||
else:
|
||||
return values[0]
|
||||
|
||||
def t(self, key: str, default=None, **kwargs) -> str:
|
||||
keys = key.split(".")
|
||||
|
||||
@ -30,9 +47,12 @@ class JsonProvider:
|
||||
|
||||
if i == last:
|
||||
for key, value in kwargs.items():
|
||||
if not value:
|
||||
translation_value = cast(str, translation_value)
|
||||
if value is None:
|
||||
value = ""
|
||||
translation_value = translation_value.replace("{" + key + "}", value)
|
||||
return translation_value
|
||||
if key == "count":
|
||||
translation_value = self._parse_plurals(translation_value, float(value))
|
||||
translation_value = translation_value.replace("{" + key + "}", str(value)) # type: ignore
|
||||
return translation_value # type: ignore
|
||||
|
||||
return default or key
|
||||
|
@ -106,7 +106,7 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
||||
except AttributeError:
|
||||
self.logger.info(f'Attempted to sort by unknown sort property "{order_by}"; ignoring')
|
||||
result = self.session.execute(q.offset(start).limit(limit)).unique().scalars().all()
|
||||
return [eff_schema.from_orm(x) for x in result]
|
||||
return [eff_schema.model_validate(x) for x in result]
|
||||
|
||||
def multi_query(
|
||||
self,
|
||||
@ -129,7 +129,7 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
||||
|
||||
q = q.offset(start).limit(limit)
|
||||
result = self.session.execute(q).unique().scalars().all()
|
||||
return [eff_schema.from_orm(x) for x in result]
|
||||
return [eff_schema.model_validate(x) for x in result]
|
||||
|
||||
def _query_one(self, match_value: str | int | UUID4, match_key: str | None = None) -> Model:
|
||||
"""
|
||||
@ -161,11 +161,11 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
||||
if not result:
|
||||
return None
|
||||
|
||||
return eff_schema.from_orm(result)
|
||||
return eff_schema.model_validate(result)
|
||||
|
||||
def create(self, data: Schema | BaseModel | dict) -> Schema:
|
||||
try:
|
||||
data = data if isinstance(data, dict) else data.dict()
|
||||
data = data if isinstance(data, dict) else data.model_dump()
|
||||
new_document = self.model(session=self.session, **data)
|
||||
self.session.add(new_document)
|
||||
self.session.commit()
|
||||
@ -175,12 +175,12 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
||||
|
||||
self.session.refresh(new_document)
|
||||
|
||||
return self.schema.from_orm(new_document)
|
||||
return self.schema.model_validate(new_document)
|
||||
|
||||
def create_many(self, data: Iterable[Schema | dict]) -> list[Schema]:
|
||||
new_documents = []
|
||||
for document in data:
|
||||
document = document if isinstance(document, dict) else document.dict()
|
||||
document = document if isinstance(document, dict) else document.model_dump()
|
||||
new_document = self.model(session=self.session, **document)
|
||||
new_documents.append(new_document)
|
||||
|
||||
@ -190,7 +190,7 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
||||
for created_document in new_documents:
|
||||
self.session.refresh(created_document)
|
||||
|
||||
return [self.schema.from_orm(x) for x in new_documents]
|
||||
return [self.schema.model_validate(x) for x in new_documents]
|
||||
|
||||
def update(self, match_value: str | int | UUID4, new_data: dict | BaseModel) -> Schema:
|
||||
"""Update a database entry.
|
||||
@ -202,18 +202,18 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
||||
Returns:
|
||||
dict: Returns a dictionary representation of the database entry
|
||||
"""
|
||||
new_data = new_data if isinstance(new_data, dict) else new_data.dict()
|
||||
new_data = new_data if isinstance(new_data, dict) else new_data.model_dump()
|
||||
|
||||
entry = self._query_one(match_value=match_value)
|
||||
entry.update(session=self.session, **new_data)
|
||||
|
||||
self.session.commit()
|
||||
return self.schema.from_orm(entry)
|
||||
return self.schema.model_validate(entry)
|
||||
|
||||
def update_many(self, data: Iterable[Schema | dict]) -> list[Schema]:
|
||||
document_data_by_id: dict[str, dict] = {}
|
||||
for document in data:
|
||||
document_data = document if isinstance(document, dict) else document.dict()
|
||||
document_data = document if isinstance(document, dict) else document.model_dump()
|
||||
document_data_by_id[document_data["id"]] = document_data
|
||||
|
||||
documents_to_update_query = self._query().filter(self.model.id.in_(list(document_data_by_id.keys())))
|
||||
@ -226,14 +226,14 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
||||
updated_documents.append(document_to_update)
|
||||
|
||||
self.session.commit()
|
||||
return [self.schema.from_orm(x) for x in updated_documents]
|
||||
return [self.schema.model_validate(x) for x in updated_documents]
|
||||
|
||||
def patch(self, match_value: str | int | UUID4, new_data: dict | BaseModel) -> Schema:
|
||||
new_data = new_data if isinstance(new_data, dict) else new_data.dict()
|
||||
new_data = new_data if isinstance(new_data, dict) else new_data.model_dump()
|
||||
|
||||
entry = self._query_one(match_value=match_value)
|
||||
|
||||
entry_as_dict = self.schema.from_orm(entry).dict()
|
||||
entry_as_dict = self.schema.model_validate(entry).model_dump()
|
||||
entry_as_dict.update(new_data)
|
||||
|
||||
return self.update(match_value, entry_as_dict)
|
||||
@ -242,7 +242,7 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
||||
match_key = match_key or self.primary_key
|
||||
|
||||
result = self._query_one(value, match_key)
|
||||
results_as_model = self.schema.from_orm(result)
|
||||
results_as_model = self.schema.model_validate(result)
|
||||
|
||||
try:
|
||||
self.session.delete(result)
|
||||
@ -256,7 +256,7 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
||||
def delete_many(self, values: Iterable) -> Schema:
|
||||
query = self._query().filter(self.model.id.in_(values)) # type: ignore
|
||||
results = self.session.execute(query).unique().scalars().all()
|
||||
results_as_model = [self.schema.from_orm(result) for result in results]
|
||||
results_as_model = [self.schema.model_validate(result) for result in results]
|
||||
|
||||
try:
|
||||
# we create a delete statement for each row
|
||||
@ -295,7 +295,7 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
||||
return self.session.scalar(q)
|
||||
else:
|
||||
q = self._query(override_schema=eff_schema).filter(attribute_name == attr_match)
|
||||
return [eff_schema.from_orm(x) for x in self.session.execute(q).scalars().all()]
|
||||
return [eff_schema.model_validate(x) for x in self.session.execute(q).scalars().all()]
|
||||
|
||||
def page_all(self, pagination: PaginationQuery, override=None, search: str | None = None) -> PaginationBase[Schema]:
|
||||
"""
|
||||
@ -309,7 +309,7 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
||||
"""
|
||||
eff_schema = override or self.schema
|
||||
# Copy this, because calling methods (e.g. tests) might rely on it not getting mutated
|
||||
pagination_result = pagination.copy()
|
||||
pagination_result = pagination.model_copy()
|
||||
q = self._query(override_schema=eff_schema, with_options=False)
|
||||
|
||||
fltr = self._filter_builder()
|
||||
@ -336,7 +336,7 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
||||
per_page=pagination_result.per_page,
|
||||
total=count,
|
||||
total_pages=total_pages,
|
||||
items=[eff_schema.from_orm(s) for s in data],
|
||||
items=[eff_schema.model_validate(s) for s in data],
|
||||
)
|
||||
|
||||
def add_pagination_to_query(self, query: Select, pagination: PaginationQuery) -> tuple[Select, int, int]:
|
||||
|
@ -23,7 +23,7 @@ from .repository_generic import RepositoryGeneric
|
||||
class RepositoryGroup(RepositoryGeneric[GroupInDB, Group]):
|
||||
def create(self, data: GroupBase | dict) -> GroupInDB:
|
||||
if isinstance(data, GroupBase):
|
||||
data = data.dict()
|
||||
data = data.model_dump()
|
||||
|
||||
max_attempts = 10
|
||||
original_name = cast(str, data["name"])
|
||||
@ -61,7 +61,7 @@ class RepositoryGroup(RepositoryGeneric[GroupInDB, Group]):
|
||||
dbgroup = self.session.execute(select(self.model).filter_by(name=name)).scalars().one_or_none()
|
||||
if dbgroup is None:
|
||||
return None
|
||||
return self.schema.from_orm(dbgroup)
|
||||
return self.schema.model_validate(dbgroup)
|
||||
|
||||
def get_by_slug_or_id(self, slug_or_id: str | UUID) -> GroupInDB | None:
|
||||
if isinstance(slug_or_id, str):
|
||||
|
@ -28,4 +28,4 @@ class RepositoryMealPlanRules(RepositoryGeneric[PlanRulesOut, GroupMealPlanRules
|
||||
|
||||
rules = self.session.execute(stmt).scalars().all()
|
||||
|
||||
return [self.schema.from_orm(x) for x in rules]
|
||||
return [self.schema.model_validate(x) for x in rules]
|
||||
|
@ -17,4 +17,4 @@ class RepositoryMeals(RepositoryGeneric[ReadPlanEntry, GroupMealPlan]):
|
||||
today = date.today()
|
||||
stmt = select(GroupMealPlan).filter(GroupMealPlan.date == today, GroupMealPlan.group_id == group_id)
|
||||
plans = self.session.execute(stmt).scalars().all()
|
||||
return [self.schema.from_orm(x) for x in plans]
|
||||
return [self.schema.model_validate(x) for x in plans]
|
||||
|
@ -58,7 +58,7 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
||||
.offset(start)
|
||||
.limit(limit)
|
||||
)
|
||||
return [eff_schema.from_orm(x) for x in self.session.execute(stmt).scalars().all()]
|
||||
return [eff_schema.model_validate(x) for x in self.session.execute(stmt).scalars().all()]
|
||||
|
||||
stmt = (
|
||||
select(self.model)
|
||||
@ -67,7 +67,7 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
||||
.offset(start)
|
||||
.limit(limit)
|
||||
)
|
||||
return [eff_schema.from_orm(x) for x in self.session.execute(stmt).scalars().all()]
|
||||
return [eff_schema.model_validate(x) for x in self.session.execute(stmt).scalars().all()]
|
||||
|
||||
def update_image(self, slug: str, _: str | None = None) -> int:
|
||||
entry: RecipeModel = self._query_one(match_value=slug)
|
||||
@ -160,7 +160,7 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
||||
search: str | None = None,
|
||||
) -> RecipePagination:
|
||||
# Copy this, because calling methods (e.g. tests) might rely on it not getting mutated
|
||||
pagination_result = pagination.copy()
|
||||
pagination_result = pagination.model_copy()
|
||||
q = select(self.model)
|
||||
|
||||
args = [
|
||||
@ -216,7 +216,7 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
||||
self.session.rollback()
|
||||
raise e
|
||||
|
||||
items = [RecipeSummary.from_orm(item) for item in data]
|
||||
items = [RecipeSummary.model_validate(item) for item in data]
|
||||
return RecipePagination(
|
||||
page=pagination_result.page,
|
||||
per_page=pagination_result.per_page,
|
||||
@ -236,7 +236,7 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
||||
.join(RecipeModel.recipe_category)
|
||||
.filter(RecipeModel.recipe_category.any(Category.id.in_(ids)))
|
||||
)
|
||||
return [RecipeSummary.from_orm(x) for x in self.session.execute(stmt).unique().scalars().all()]
|
||||
return [RecipeSummary.model_validate(x) for x in self.session.execute(stmt).unique().scalars().all()]
|
||||
|
||||
def _build_recipe_filter(
|
||||
self,
|
||||
@ -298,7 +298,7 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
||||
require_all_tools=require_all_tools,
|
||||
)
|
||||
stmt = select(RecipeModel).filter(*fltr)
|
||||
return [self.schema.from_orm(x) for x in self.session.execute(stmt).scalars().all()]
|
||||
return [self.schema.model_validate(x) for x in self.session.execute(stmt).scalars().all()]
|
||||
|
||||
def get_random_by_categories_and_tags(
|
||||
self, categories: list[RecipeCategory], tags: list[RecipeTag]
|
||||
@ -316,7 +316,7 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
||||
stmt = (
|
||||
select(RecipeModel).filter(and_(*filters)).order_by(func.random()).limit(1) # Postgres and SQLite specific
|
||||
)
|
||||
return [self.schema.from_orm(x) for x in self.session.execute(stmt).scalars().all()]
|
||||
return [self.schema.model_validate(x) for x in self.session.execute(stmt).scalars().all()]
|
||||
|
||||
def get_random(self, limit=1) -> list[Recipe]:
|
||||
stmt = (
|
||||
@ -325,14 +325,14 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
||||
.order_by(func.random()) # Postgres and SQLite specific
|
||||
.limit(limit)
|
||||
)
|
||||
return [self.schema.from_orm(x) for x in self.session.execute(stmt).scalars().all()]
|
||||
return [self.schema.model_validate(x) for x in self.session.execute(stmt).scalars().all()]
|
||||
|
||||
def get_by_slug(self, group_id: UUID4, slug: str, limit=1) -> Recipe | None:
|
||||
stmt = select(RecipeModel).filter(RecipeModel.group_id == group_id, RecipeModel.slug == slug)
|
||||
dbrecipe = self.session.execute(stmt).scalars().one_or_none()
|
||||
if dbrecipe is None:
|
||||
return None
|
||||
return self.schema.from_orm(dbrecipe)
|
||||
return self.schema.model_validate(dbrecipe)
|
||||
|
||||
def all_ids(self, group_id: UUID4) -> Sequence[UUID4]:
|
||||
stmt = select(RecipeModel.id).filter(RecipeModel.group_id == group_id)
|
||||
|
@ -18,7 +18,7 @@ class RepositoryUsers(RepositoryGeneric[PrivateUser, User]):
|
||||
def update_password(self, id, password: str):
|
||||
entry = self._query_one(match_value=id)
|
||||
if settings.IS_DEMO:
|
||||
user_to_update = self.schema.from_orm(entry)
|
||||
user_to_update = self.schema.model_validate(entry)
|
||||
if user_to_update.is_default_user:
|
||||
# do not update the default user in demo mode
|
||||
return user_to_update
|
||||
@ -26,7 +26,7 @@ class RepositoryUsers(RepositoryGeneric[PrivateUser, User]):
|
||||
entry.update_password(password)
|
||||
self.session.commit()
|
||||
|
||||
return self.schema.from_orm(entry)
|
||||
return self.schema.model_validate(entry)
|
||||
|
||||
def create(self, user: PrivateUser | dict): # type: ignore
|
||||
new_user = super().create(user)
|
||||
@ -66,9 +66,9 @@ class RepositoryUsers(RepositoryGeneric[PrivateUser, User]):
|
||||
def get_by_username(self, username: str) -> PrivateUser | None:
|
||||
stmt = select(User).filter(User.username == username)
|
||||
dbuser = self.session.execute(stmt).scalars().one_or_none()
|
||||
return None if dbuser is None else self.schema.from_orm(dbuser)
|
||||
return None if dbuser is None else self.schema.model_validate(dbuser)
|
||||
|
||||
def get_locked_users(self) -> list[PrivateUser]:
|
||||
stmt = select(User).filter(User.locked_at != None) # noqa E711
|
||||
results = self.session.execute(stmt).scalars().all()
|
||||
return [self.schema.from_orm(x) for x in results]
|
||||
return [self.schema.model_validate(x) for x in results]
|
||||
|
@ -2,7 +2,7 @@ from abc import ABC
|
||||
from logging import Logger
|
||||
|
||||
from fastapi import Depends
|
||||
from pydantic import UUID4
|
||||
from pydantic import UUID4, ConfigDict
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from mealie.core.config import get_app_dirs, get_app_settings
|
||||
@ -25,10 +25,10 @@ class _BaseController(ABC):
|
||||
session: Session = Depends(generate_session)
|
||||
translator: Translator = Depends(local_provider)
|
||||
|
||||
_repos: AllRepositories | None
|
||||
_logger: Logger | None
|
||||
_settings: AppSettings | None
|
||||
_folders: AppDirectories | None
|
||||
_repos: AllRepositories | None = None
|
||||
_logger: Logger | None = None
|
||||
_settings: AppSettings | None = None
|
||||
_folders: AppDirectories | None = None
|
||||
|
||||
@property
|
||||
def t(self):
|
||||
@ -58,8 +58,7 @@ class _BaseController(ABC):
|
||||
self._folders = get_app_dirs()
|
||||
return self._folders
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
|
||||
|
||||
class BasePublicController(_BaseController):
|
||||
|
@ -6,11 +6,10 @@ See their repository for details -> https://github.com/dmontagu/fastapi-utils
|
||||
|
||||
import inspect
|
||||
from collections.abc import Callable
|
||||
from typing import Any, TypeVar, cast, get_type_hints
|
||||
from typing import Any, ClassVar, ForwardRef, TypeVar, cast, get_origin, get_type_hints
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi.routing import APIRoute
|
||||
from pydantic.typing import is_classvar
|
||||
from starlette.routing import Route, WebSocketRoute
|
||||
|
||||
T = TypeVar("T")
|
||||
@ -47,6 +46,25 @@ def _cbv(router: APIRouter, cls: type[T], *urls: str, instance: Any | None = Non
|
||||
return cls
|
||||
|
||||
|
||||
# copied from Pydantic V1 Source: https://github.com/pydantic/pydantic/blob/1c91c8627b541b22354b9ed56b9ef1bb21ac6fbd/pydantic/v1/typing.py
|
||||
def _check_classvar(v: type[Any] | None) -> bool:
|
||||
if v is None:
|
||||
return False
|
||||
|
||||
return v.__class__ == ClassVar.__class__ and getattr(v, "_name", None) == "ClassVar"
|
||||
|
||||
|
||||
# copied from Pydantic V1 Source: https://github.com/pydantic/pydantic/blob/1c91c8627b541b22354b9ed56b9ef1bb21ac6fbd/pydantic/v1/typing.py
|
||||
def _is_classvar(ann_type: type[Any]) -> bool:
|
||||
if _check_classvar(ann_type) or _check_classvar(get_origin(ann_type)):
|
||||
return True
|
||||
|
||||
if ann_type.__class__ == ForwardRef and ann_type.__forward_arg__.startswith("ClassVar["): # type: ignore
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _init_cbv(cls: type[Any], instance: Any | None = None) -> None:
|
||||
"""
|
||||
Idempotently modifies the provided `cls`, performing the following modifications:
|
||||
@ -67,7 +85,7 @@ def _init_cbv(cls: type[Any], instance: Any | None = None) -> None:
|
||||
|
||||
dependency_names: list[str] = []
|
||||
for name, hint in get_type_hints(cls).items():
|
||||
if is_classvar(hint):
|
||||
if _is_classvar(hint):
|
||||
continue
|
||||
|
||||
if name.startswith("_"):
|
||||
|
@ -108,7 +108,7 @@ class HttpRepo(Generic[C, R, U]):
|
||||
)
|
||||
|
||||
try:
|
||||
item = self.repo.patch(item_id, data.dict(exclude_unset=True, exclude_defaults=True))
|
||||
item = self.repo.patch(item_id, data.model_dump(exclude_unset=True, exclude_defaults=True))
|
||||
except Exception as ex:
|
||||
self.handle_exception(ex)
|
||||
|
||||
|
@ -43,7 +43,7 @@ class AdminUserManagementRoutes(BaseAdminController):
|
||||
override=GroupInDB,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.model_dump())
|
||||
return response
|
||||
|
||||
@router.post("", response_model=GroupInDB, status_code=status.HTTP_201_CREATED)
|
||||
|
@ -37,7 +37,7 @@ class AdminUserManagementRoutes(BaseAdminController):
|
||||
override=UserOut,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.model_dump())
|
||||
return response
|
||||
|
||||
@router.post("", response_model=UserOut, status_code=201)
|
||||
|
@ -18,7 +18,7 @@ class AdminServerTasksController(BaseAdminController):
|
||||
override=ServerTask,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.model_dump())
|
||||
return response
|
||||
|
||||
@router.post("/server-tasks", response_model=ServerTask, status_code=201)
|
||||
|
@ -53,4 +53,4 @@ def get_app_theme(resp: Response):
|
||||
settings = get_app_settings()
|
||||
|
||||
resp.headers["Cache-Control"] = "public, max-age=604800"
|
||||
return AppTheme(**settings.theme.dict())
|
||||
return AppTheme(**settings.theme.model_dump())
|
||||
|
@ -48,7 +48,7 @@ class MealieAuthToken(BaseModel):
|
||||
|
||||
@classmethod
|
||||
def respond(cls, token: str, token_type: str = "bearer") -> dict:
|
||||
return cls(access_token=token, token_type=token_type).dict()
|
||||
return cls(access_token=token, token_type=token_type).model_dump()
|
||||
|
||||
|
||||
@public_router.post("/token")
|
||||
|
@ -47,7 +47,7 @@ class RecipeCommentRoutes(BaseUserController):
|
||||
override=RecipeCommentOut,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.model_dump())
|
||||
return response
|
||||
|
||||
@router.post("", response_model=RecipeCommentOut, status_code=201)
|
||||
|
@ -1,3 +1,5 @@
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import UUID4
|
||||
|
||||
@ -36,12 +38,19 @@ class PublicCookbooksController(BasePublicExploreController):
|
||||
search=search,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(router.url_path_for("get_all", group_slug=self.group.slug), q.dict())
|
||||
response.set_pagination_guides(router.url_path_for("get_all", group_slug=self.group.slug), q.model_dump())
|
||||
return response
|
||||
|
||||
@router.get("/{item_id}", response_model=RecipeCookBook)
|
||||
def get_one(self, item_id: UUID4 | str) -> RecipeCookBook:
|
||||
match_attr = "slug" if isinstance(item_id, str) else "id"
|
||||
if isinstance(item_id, UUID):
|
||||
match_attr = "id"
|
||||
else:
|
||||
try:
|
||||
UUID(item_id)
|
||||
match_attr = "id"
|
||||
except ValueError:
|
||||
match_attr = "slug"
|
||||
cookbook = self.cookbooks.get_one(item_id, match_attr)
|
||||
|
||||
if not cookbook or not cookbook.public:
|
||||
|
@ -26,7 +26,7 @@ class PublicFoodsController(BasePublicExploreController):
|
||||
search=search,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(router.url_path_for("get_all", group_slug=self.group.slug), q.dict())
|
||||
response.set_pagination_guides(router.url_path_for("get_all", group_slug=self.group.slug), q.model_dump())
|
||||
return response
|
||||
|
||||
@router.get("/{item_id}", response_model=IngredientFood)
|
||||
|
@ -31,7 +31,9 @@ class PublicCategoriesController(BasePublicExploreController):
|
||||
search=search,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(categories_router.url_path_for("get_all", group_slug=self.group.slug), q.dict())
|
||||
response.set_pagination_guides(
|
||||
categories_router.url_path_for("get_all", group_slug=self.group.slug), q.model_dump()
|
||||
)
|
||||
return response
|
||||
|
||||
@categories_router.get("/{item_id}", response_model=CategoryOut)
|
||||
@ -59,7 +61,7 @@ class PublicTagsController(BasePublicExploreController):
|
||||
search=search,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(tags_router.url_path_for("get_all", group_slug=self.group.slug), q.dict())
|
||||
response.set_pagination_guides(tags_router.url_path_for("get_all", group_slug=self.group.slug), q.model_dump())
|
||||
return response
|
||||
|
||||
@tags_router.get("/{item_id}", response_model=TagOut)
|
||||
@ -87,7 +89,7 @@ class PublicToolsController(BasePublicExploreController):
|
||||
search=search,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(tools_router.url_path_for("get_all", group_slug=self.group.slug), q.dict())
|
||||
response.set_pagination_guides(tools_router.url_path_for("get_all", group_slug=self.group.slug), q.model_dump())
|
||||
return response
|
||||
|
||||
@tools_router.get("/{item_id}", response_model=RecipeToolOut)
|
||||
|
@ -1,3 +1,5 @@
|
||||
from uuid import UUID
|
||||
|
||||
import orjson
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||||
from pydantic import UUID4
|
||||
@ -37,7 +39,14 @@ class PublicRecipesController(BasePublicExploreController):
|
||||
) -> PaginationBase[RecipeSummary]:
|
||||
cookbook_data: ReadCookBook | None = None
|
||||
if search_query.cookbook:
|
||||
cb_match_attr = "slug" if isinstance(search_query.cookbook, str) else "id"
|
||||
if isinstance(search_query.cookbook, UUID):
|
||||
cb_match_attr = "id"
|
||||
else:
|
||||
try:
|
||||
UUID(search_query.cookbook)
|
||||
cb_match_attr = "id"
|
||||
except ValueError:
|
||||
cb_match_attr = "slug"
|
||||
cookbook_data = self.cookbooks.get_one(search_query.cookbook, cb_match_attr)
|
||||
|
||||
if cookbook_data is None or not cookbook_data.public:
|
||||
@ -64,13 +73,13 @@ class PublicRecipesController(BasePublicExploreController):
|
||||
)
|
||||
|
||||
# merge default pagination with the request's query params
|
||||
query_params = q.dict() | {**request.query_params}
|
||||
query_params = q.model_dump() | {**request.query_params}
|
||||
pagination_response.set_pagination_guides(
|
||||
router.url_path_for("get_all", group_slug=self.group.slug),
|
||||
{k: v for k, v in query_params.items() if v is not None},
|
||||
)
|
||||
|
||||
json_compatible_response = orjson.dumps(pagination_response.dict(by_alias=True))
|
||||
json_compatible_response = orjson.dumps(pagination_response.model_dump(by_alias=True))
|
||||
|
||||
# Response is returned directly, to avoid validation and improve performance
|
||||
return JSONBytes(content=json_compatible_response)
|
||||
|
@ -1,4 +1,5 @@
|
||||
from functools import cached_property
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import UUID4
|
||||
@ -48,7 +49,7 @@ class GroupCookbookController(BaseCrudController):
|
||||
override=ReadCookBook,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.model_dump())
|
||||
return response
|
||||
|
||||
@router.post("", response_model=ReadCookBook, status_code=201)
|
||||
@ -85,7 +86,15 @@ class GroupCookbookController(BaseCrudController):
|
||||
|
||||
@router.get("/{item_id}", response_model=RecipeCookBook)
|
||||
def get_one(self, item_id: UUID4 | str):
|
||||
match_attr = "slug" if isinstance(item_id, str) else "id"
|
||||
if isinstance(item_id, UUID):
|
||||
match_attr = "id"
|
||||
else:
|
||||
try:
|
||||
UUID(item_id)
|
||||
match_attr = "id"
|
||||
except ValueError:
|
||||
match_attr = "slug"
|
||||
|
||||
cookbook = self.repo.get_one(item_id, match_attr)
|
||||
|
||||
if cookbook is None:
|
||||
|
@ -58,7 +58,7 @@ class GroupEventsNotifierController(BaseUserController):
|
||||
override=GroupEventNotifierOut,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.model_dump())
|
||||
return response
|
||||
|
||||
@router.post("", response_model=GroupEventNotifierOut, status_code=201)
|
||||
|
@ -48,7 +48,7 @@ class MultiPurposeLabelsController(BaseUserController):
|
||||
search=search,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.model_dump())
|
||||
return response
|
||||
|
||||
@router.post("", response_model=MultiPurposeLabelOut)
|
||||
|
@ -31,7 +31,7 @@ class GroupMealplanConfigController(BaseUserController):
|
||||
override=PlanRulesOut,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.model_dump())
|
||||
return response
|
||||
|
||||
@router.post("", response_model=PlanRulesOut, status_code=201)
|
||||
|
@ -44,6 +44,7 @@ class GroupMigrationController(BaseUserController):
|
||||
"user_id": self.user.id,
|
||||
"group_id": self.group_id,
|
||||
"add_migration_tag": add_migration_tag,
|
||||
"translator": self.translator,
|
||||
}
|
||||
|
||||
table: dict[SupportedMigrations, type[BaseMigrator]] = {
|
||||
|
@ -105,7 +105,7 @@ class ShoppingListItemController(BaseCrudController):
|
||||
@item_router.get("", response_model=ShoppingListItemPagination)
|
||||
def get_all(self, q: PaginationQuery = Depends()):
|
||||
response = self.repo.page_all(pagination=q, override=ShoppingListItemOut)
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.model_dump())
|
||||
return response
|
||||
|
||||
@item_router.post("/create-bulk", response_model=ShoppingListItemsCollectionOut, status_code=201)
|
||||
@ -174,7 +174,7 @@ class ShoppingListController(BaseCrudController):
|
||||
override=ShoppingListSummary,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.model_dump())
|
||||
return response
|
||||
|
||||
@router.post("", response_model=ShoppingListOut, status_code=201)
|
||||
|
@ -32,7 +32,7 @@ class ReadWebhookController(BaseUserController):
|
||||
override=ReadWebhook,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.model_dump())
|
||||
return response
|
||||
|
||||
@router.post("", response_model=ReadWebhook, status_code=201)
|
||||
|
@ -1,7 +1,7 @@
|
||||
from functools import cached_property
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import UUID4, BaseModel
|
||||
from pydantic import UUID4, BaseModel, ConfigDict
|
||||
|
||||
from mealie.routes._base import BaseCrudController, controller
|
||||
from mealie.routes._base.mixins import HttpRepo
|
||||
@ -20,9 +20,7 @@ class CategorySummary(BaseModel):
|
||||
id: UUID4
|
||||
slug: str
|
||||
name: str
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
@controller(router)
|
||||
@ -46,7 +44,7 @@ class RecipeCategoryController(BaseCrudController):
|
||||
search=search,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.model_dump())
|
||||
return response
|
||||
|
||||
@router.post("", status_code=201)
|
||||
@ -71,7 +69,7 @@ class RecipeCategoryController(BaseCrudController):
|
||||
def get_one(self, item_id: UUID4):
|
||||
"""Returns a list of recipes associated with the provided category."""
|
||||
category_obj = self.mixins.get_one(item_id)
|
||||
category_obj = CategorySummary.from_orm(category_obj)
|
||||
category_obj = CategorySummary.model_validate(category_obj)
|
||||
return category_obj
|
||||
|
||||
@router.put("/{item_id}", response_model=CategorySummary)
|
||||
@ -119,7 +117,7 @@ class RecipeCategoryController(BaseCrudController):
|
||||
def get_one_by_slug(self, category_slug: str):
|
||||
"""Returns a category object with the associated recieps relating to the category"""
|
||||
category: RecipeCategory = self.mixins.get_one(category_slug, "slug")
|
||||
return RecipeCategoryResponse.construct(
|
||||
return RecipeCategoryResponse.model_construct(
|
||||
id=category.id,
|
||||
slug=category.slug,
|
||||
name=category.name,
|
||||
|
@ -35,7 +35,7 @@ class TagController(BaseCrudController):
|
||||
search=search,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.model_dump())
|
||||
return response
|
||||
|
||||
@router.get("/empty")
|
||||
|
@ -32,7 +32,7 @@ class RecipeToolController(BaseUserController):
|
||||
search=search,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.model_dump())
|
||||
return response
|
||||
|
||||
@router.post("", response_model=RecipeTool, status_code=201)
|
||||
|
@ -1,5 +1,6 @@
|
||||
from functools import cached_property
|
||||
from shutil import copyfileobj
|
||||
from uuid import UUID
|
||||
from zipfile import ZipFile
|
||||
|
||||
import orjson
|
||||
@ -125,7 +126,7 @@ class RecipeExportController(BaseRecipeController):
|
||||
recipe: Recipe = self.mixins.get_one(slug)
|
||||
image_asset = recipe.image_dir.joinpath(RecipeImageTypes.original.value)
|
||||
with ZipFile(temp_path, "w") as myzip:
|
||||
myzip.writestr(f"{slug}.json", recipe.json())
|
||||
myzip.writestr(f"{slug}.json", recipe.model_dump_json())
|
||||
|
||||
if image_asset.is_file():
|
||||
myzip.write(image_asset, arcname=image_asset.name)
|
||||
@ -164,7 +165,7 @@ class RecipeController(BaseRecipeController):
|
||||
async def parse_recipe_url(self, req: ScrapeRecipe):
|
||||
"""Takes in a URL and attempts to scrape data and load it into the database"""
|
||||
try:
|
||||
recipe, extras = await create_from_url(req.url)
|
||||
recipe, extras = await create_from_url(req.url, self.translator)
|
||||
except ForceTimeoutException as e:
|
||||
raise HTTPException(
|
||||
status_code=408, detail=ErrorResponse.respond(message="Recipe Scraping Timed Out")
|
||||
@ -193,7 +194,7 @@ class RecipeController(BaseRecipeController):
|
||||
@router.post("/create-url/bulk", status_code=202)
|
||||
def parse_recipe_url_bulk(self, bulk: CreateRecipeByUrlBulk, bg_tasks: BackgroundTasks):
|
||||
"""Takes in a URL and attempts to scrape data and load it into the database"""
|
||||
bulk_scraper = RecipeBulkScraperService(self.service, self.repos, self.group)
|
||||
bulk_scraper = RecipeBulkScraperService(self.service, self.repos, self.group, self.translator)
|
||||
report_id = bulk_scraper.get_report_id()
|
||||
bg_tasks.add_task(bulk_scraper.scrape, bulk)
|
||||
|
||||
@ -208,7 +209,7 @@ class RecipeController(BaseRecipeController):
|
||||
async def test_parse_recipe_url(self, url: ScrapeRecipeTest):
|
||||
# Debugger should produce the same result as the scraper sees before cleaning
|
||||
try:
|
||||
if scraped_data := await RecipeScraperPackage(url.url).scrape_url():
|
||||
if scraped_data := await RecipeScraperPackage(url.url, self.translator).scrape_url():
|
||||
return scraped_data.schema.data
|
||||
except ForceTimeoutException as e:
|
||||
raise HTTPException(
|
||||
@ -244,7 +245,14 @@ class RecipeController(BaseRecipeController):
|
||||
):
|
||||
cookbook_data: ReadCookBook | None = None
|
||||
if search_query.cookbook:
|
||||
cb_match_attr = "slug" if isinstance(search_query.cookbook, str) else "id"
|
||||
if isinstance(search_query.cookbook, UUID):
|
||||
cb_match_attr = "id"
|
||||
else:
|
||||
try:
|
||||
UUID(search_query.cookbook)
|
||||
cb_match_attr = "id"
|
||||
except ValueError:
|
||||
cb_match_attr = "slug"
|
||||
cookbook_data = self.cookbooks_repo.get_one(search_query.cookbook, cb_match_attr)
|
||||
|
||||
if cookbook_data is None:
|
||||
@ -265,13 +273,13 @@ class RecipeController(BaseRecipeController):
|
||||
)
|
||||
|
||||
# merge default pagination with the request's query params
|
||||
query_params = q.dict() | {**request.query_params}
|
||||
query_params = q.model_dump() | {**request.query_params}
|
||||
pagination_response.set_pagination_guides(
|
||||
router.url_path_for("get_all"),
|
||||
{k: v for k, v in query_params.items() if v is not None},
|
||||
)
|
||||
|
||||
json_compatible_response = orjson.dumps(pagination_response.dict(by_alias=True))
|
||||
json_compatible_response = orjson.dumps(pagination_response.model_dump(by_alias=True))
|
||||
|
||||
# Response is returned directly, to avoid validation and improve performance
|
||||
return JSONBytes(content=json_compatible_response)
|
||||
|
@ -49,7 +49,7 @@ class RecipeTimelineEventsController(BaseCrudController):
|
||||
override=RecipeTimelineEventOut,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(events_router.url_path_for("get_all"), q.dict())
|
||||
response.set_pagination_guides(events_router.url_path_for("get_all"), q.model_dump())
|
||||
return response
|
||||
|
||||
@events_router.post("", response_model=RecipeTimelineEventOut, status_code=201)
|
||||
|
@ -30,7 +30,7 @@ class RecipeSharedController(BaseUserController):
|
||||
|
||||
@router.post("", response_model=RecipeShareToken, status_code=201)
|
||||
def create_one(self, data: RecipeShareTokenCreate) -> RecipeShareToken:
|
||||
save_data = RecipeShareTokenSave(**data.dict(), group_id=self.group_id)
|
||||
save_data = RecipeShareTokenSave(**data.model_dump(), group_id=self.group_id)
|
||||
return self.mixins.create_one(save_data)
|
||||
|
||||
@router.get("/{item_id}", response_model=RecipeShareToken)
|
||||
|
@ -52,7 +52,7 @@ class IngredientFoodsController(BaseUserController):
|
||||
search=search,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.model_dump())
|
||||
return response
|
||||
|
||||
@router.post("", response_model=IngredientFood, status_code=201)
|
||||
|
@ -52,7 +52,7 @@ class IngredientUnitsController(BaseUserController):
|
||||
search=search,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.model_dump())
|
||||
return response
|
||||
|
||||
@router.post("", response_model=IngredientUnit, status_code=201)
|
||||
|
@ -29,7 +29,7 @@ class AdminUserController(BaseAdminController):
|
||||
override=UserOut,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(admin_router.url_path_for("get_all"), q.dict())
|
||||
response.set_pagination_guides(admin_router.url_path_for("get_all"), q.model_dump())
|
||||
return response
|
||||
|
||||
@admin_router.post("", response_model=UserOut, status_code=201)
|
||||
@ -103,7 +103,7 @@ class UserController(BaseUserController):
|
||||
)
|
||||
|
||||
try:
|
||||
self.repos.users.update(item_id, new_data.dict())
|
||||
self.repos.users.update(item_id, new_data.model_dump())
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
|
252
mealie/schema/_mealie/datetime_parse.py
Normal file
252
mealie/schema/_mealie/datetime_parse.py
Normal file
@ -0,0 +1,252 @@
|
||||
"""
|
||||
From Pydantic V1: https://github.com/pydantic/pydantic/blob/abcf81ec104d2da70894ac0402ae11a7186c5e47/pydantic/datetime_parse.py
|
||||
"""
|
||||
|
||||
import re
|
||||
from datetime import date, datetime, time, timedelta, timezone
|
||||
|
||||
date_expr = r"(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})"
|
||||
time_expr = (
|
||||
r"(?P<hour>\d{1,2}):(?P<minute>\d{1,2})"
|
||||
r"(?::(?P<second>\d{1,2})(?:\.(?P<microsecond>\d{1,6})\d{0,6})?)?"
|
||||
r"(?P<tzinfo>Z|[+-]\d{2}(?::?\d{2})?)?$"
|
||||
)
|
||||
|
||||
date_re = re.compile(f"{date_expr}$")
|
||||
time_re = re.compile(time_expr)
|
||||
datetime_re = re.compile(f"{date_expr}[T ]{time_expr}")
|
||||
|
||||
standard_duration_re = re.compile(
|
||||
r"^"
|
||||
r"(?:(?P<days>-?\d+) (days?, )?)?"
|
||||
r"((?:(?P<hours>-?\d+):)(?=\d+:\d+))?"
|
||||
r"(?:(?P<minutes>-?\d+):)?"
|
||||
r"(?P<seconds>-?\d+)"
|
||||
r"(?:\.(?P<microseconds>\d{1,6})\d{0,6})?"
|
||||
r"$"
|
||||
)
|
||||
|
||||
# Support the sections of ISO 8601 date representation that are accepted by timedelta
|
||||
iso8601_duration_re = re.compile(
|
||||
r"^(?P<sign>[-+]?)"
|
||||
r"P"
|
||||
r"(?:(?P<days>\d+(.\d+)?)D)?"
|
||||
r"(?:T"
|
||||
r"(?:(?P<hours>\d+(.\d+)?)H)?"
|
||||
r"(?:(?P<minutes>\d+(.\d+)?)M)?"
|
||||
r"(?:(?P<seconds>\d+(.\d+)?)S)?"
|
||||
r")?"
|
||||
r"$"
|
||||
)
|
||||
|
||||
EPOCH = datetime(1970, 1, 1)
|
||||
# if greater than this, the number is in ms, if less than or equal it's in seconds
|
||||
# (in seconds this is 11th October 2603, in ms it's 20th August 1970)
|
||||
MS_WATERSHED = int(2e10)
|
||||
# slightly more than datetime.max in ns - (datetime.max - EPOCH).total_seconds() * 1e9
|
||||
MAX_NUMBER = int(3e20)
|
||||
|
||||
|
||||
class DateError(ValueError):
|
||||
def __init__(self, *args: object) -> None:
|
||||
super().__init__("invalid date format")
|
||||
|
||||
|
||||
class TimeError(ValueError):
|
||||
def __init__(self, *args: object) -> None:
|
||||
super().__init__("invalid time format")
|
||||
|
||||
|
||||
class DateTimeError(ValueError):
|
||||
def __init__(self, *args: object) -> None:
|
||||
super().__init__("invalid datetime format")
|
||||
|
||||
|
||||
class DurationError(ValueError):
|
||||
def __init__(self, *args: object) -> None:
|
||||
super().__init__("invalid duration format")
|
||||
|
||||
|
||||
def get_numeric(value: str | bytes | int | float, native_expected_type: str) -> None | int | float:
|
||||
if isinstance(value, int | float):
|
||||
return value
|
||||
try:
|
||||
return float(value)
|
||||
except ValueError:
|
||||
return None
|
||||
except TypeError as e:
|
||||
raise TypeError(f"invalid type; expected {native_expected_type}, string, bytes, int or float") from e
|
||||
|
||||
|
||||
def from_unix_seconds(seconds: int | float) -> datetime:
|
||||
if seconds > MAX_NUMBER:
|
||||
return datetime.max
|
||||
elif seconds < -MAX_NUMBER:
|
||||
return datetime.min
|
||||
|
||||
while abs(seconds) > MS_WATERSHED:
|
||||
seconds /= 1000
|
||||
dt = EPOCH + timedelta(seconds=seconds)
|
||||
return dt.replace(tzinfo=timezone.utc)
|
||||
|
||||
|
||||
def _parse_timezone(value: str | None, error: type[Exception]) -> None | int | timezone:
|
||||
if value == "Z":
|
||||
return timezone.utc
|
||||
elif value is not None:
|
||||
offset_mins = int(value[-2:]) if len(value) > 3 else 0
|
||||
offset = 60 * int(value[1:3]) + offset_mins
|
||||
if value[0] == "-":
|
||||
offset = -offset
|
||||
try:
|
||||
return timezone(timedelta(minutes=offset))
|
||||
except ValueError as e:
|
||||
raise error() from e
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def parse_date(value: date | str | bytes | int | float) -> date:
|
||||
"""
|
||||
Parse a date/int/float/string and return a datetime.date.
|
||||
|
||||
Raise ValueError if the input is well formatted but not a valid date.
|
||||
Raise ValueError if the input isn't well formatted.
|
||||
"""
|
||||
if isinstance(value, date):
|
||||
if isinstance(value, datetime):
|
||||
return value.date()
|
||||
else:
|
||||
return value
|
||||
|
||||
number = get_numeric(value, "date")
|
||||
if number is not None:
|
||||
return from_unix_seconds(number).date()
|
||||
|
||||
if isinstance(value, bytes):
|
||||
value = value.decode()
|
||||
|
||||
match = date_re.match(value) # type: ignore
|
||||
if match is None:
|
||||
raise DateError()
|
||||
|
||||
kw = {k: int(v) for k, v in match.groupdict().items()}
|
||||
|
||||
try:
|
||||
return date(**kw)
|
||||
except ValueError as e:
|
||||
raise DateError() from e
|
||||
|
||||
|
||||
def parse_time(value: time | str | bytes | int | float) -> time:
|
||||
"""
|
||||
Parse a time/string and return a datetime.time.
|
||||
|
||||
Raise ValueError if the input is well formatted but not a valid time.
|
||||
Raise ValueError if the input isn't well formatted, in particular if it contains an offset.
|
||||
"""
|
||||
if isinstance(value, time):
|
||||
return value
|
||||
|
||||
number = get_numeric(value, "time")
|
||||
if number is not None:
|
||||
if number >= 86400:
|
||||
# doesn't make sense since the time time loop back around to 0
|
||||
raise TimeError()
|
||||
return (datetime.min + timedelta(seconds=number)).time()
|
||||
|
||||
if isinstance(value, bytes):
|
||||
value = value.decode()
|
||||
|
||||
match = time_re.match(value) # type: ignore
|
||||
if match is None:
|
||||
raise TimeError()
|
||||
|
||||
kw = match.groupdict()
|
||||
if kw["microsecond"]:
|
||||
kw["microsecond"] = kw["microsecond"].ljust(6, "0")
|
||||
|
||||
tzinfo = _parse_timezone(kw.pop("tzinfo"), TimeError)
|
||||
kw_: dict[str, None | int | timezone] = {k: int(v) for k, v in kw.items() if v is not None}
|
||||
kw_["tzinfo"] = tzinfo
|
||||
|
||||
try:
|
||||
return time(**kw_) # type: ignore
|
||||
except ValueError as e:
|
||||
raise TimeError() from e
|
||||
|
||||
|
||||
def parse_datetime(value: datetime | str | bytes | int | float) -> datetime:
|
||||
"""
|
||||
Parse a datetime/int/float/string and return a datetime.datetime.
|
||||
|
||||
This function supports time zone offsets. When the input contains one,
|
||||
the output uses a timezone with a fixed offset from UTC.
|
||||
|
||||
Raise ValueError if the input is well formatted but not a valid datetime.
|
||||
Raise ValueError if the input isn't well formatted.
|
||||
"""
|
||||
if isinstance(value, datetime):
|
||||
return value
|
||||
|
||||
number = get_numeric(value, "datetime")
|
||||
if number is not None:
|
||||
return from_unix_seconds(number)
|
||||
|
||||
if isinstance(value, bytes):
|
||||
value = value.decode()
|
||||
|
||||
match = datetime_re.match(value) # type: ignore
|
||||
if match is None:
|
||||
raise DateTimeError()
|
||||
|
||||
kw = match.groupdict()
|
||||
if kw["microsecond"]:
|
||||
kw["microsecond"] = kw["microsecond"].ljust(6, "0")
|
||||
|
||||
tzinfo = _parse_timezone(kw.pop("tzinfo"), DateTimeError)
|
||||
kw_: dict[str, None | int | timezone] = {k: int(v) for k, v in kw.items() if v is not None}
|
||||
kw_["tzinfo"] = tzinfo
|
||||
|
||||
try:
|
||||
return datetime(**kw_) # type: ignore
|
||||
except ValueError as e:
|
||||
raise DateTimeError() from e
|
||||
|
||||
|
||||
def parse_duration(value: str | bytes | int | float) -> timedelta:
|
||||
"""
|
||||
Parse a duration int/float/string and return a datetime.timedelta.
|
||||
|
||||
The preferred format for durations in Django is '%d %H:%M:%S.%f'.
|
||||
|
||||
Also supports ISO 8601 representation.
|
||||
"""
|
||||
if isinstance(value, timedelta):
|
||||
return value
|
||||
|
||||
if isinstance(value, int | float):
|
||||
# below code requires a string
|
||||
value = f"{value:f}"
|
||||
elif isinstance(value, bytes):
|
||||
value = value.decode()
|
||||
|
||||
try:
|
||||
match = standard_duration_re.match(value) or iso8601_duration_re.match(value)
|
||||
except TypeError as e:
|
||||
raise TypeError("invalid type; expected timedelta, string, bytes, int or float") from e
|
||||
|
||||
if not match:
|
||||
raise DurationError()
|
||||
|
||||
kw = match.groupdict()
|
||||
sign = -1 if kw.pop("sign", "+") == "-" else 1
|
||||
if kw.get("microseconds"):
|
||||
kw["microseconds"] = kw["microseconds"].ljust(6, "0")
|
||||
|
||||
if kw.get("seconds") and kw.get("microseconds") and kw["seconds"].startswith("-"):
|
||||
kw["microseconds"] = "-" + kw["microseconds"]
|
||||
|
||||
kw_ = {k: float(v) for k, v in kw.items() if v is not None}
|
||||
|
||||
return sign * timedelta(**kw_)
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user