mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-22 14:33:33 -07:00
Merge branch 'mealie-next' into update-seeding
This commit is contained in:
commit
733ed3a637
91 changed files with 1249 additions and 2849 deletions
|
@ -12,7 +12,7 @@ repos:
|
|||
exclude: ^tests/data/
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.11.13
|
||||
rev: v0.12.1
|
||||
hooks:
|
||||
- id: ruff
|
||||
- id: ruff-format
|
||||
|
|
|
@ -156,8 +156,6 @@ Setting the following environmental variables will change the theme of the front
|
|||
|
||||
### Docker Secrets
|
||||
|
||||
### Docker Secrets
|
||||
|
||||
> <super>†</super> Starting in version `2.4.2`, any environment variable in the preceding lists with a dagger
|
||||
> symbol next to them support the Docker Compose secrets pattern, below.
|
||||
[Docker Compose secrets][docker-secrets] can be used to secure sensitive information regarding the Mealie implementation
|
||||
|
|
|
@ -64,3 +64,7 @@ a {
|
|||
.fill-height {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.vue-simple-handler {
|
||||
background-color: rgb(var(--v-theme-primary)) !important;
|
||||
}
|
||||
|
|
|
@ -24,8 +24,7 @@
|
|||
|
||||
<v-container
|
||||
v-if="book"
|
||||
fluid
|
||||
class="py-0 my-0"
|
||||
class="my-0"
|
||||
>
|
||||
<v-sheet
|
||||
color="transparent"
|
||||
|
@ -33,13 +32,12 @@
|
|||
elevation="0"
|
||||
>
|
||||
<div class="d-flex align-center w-100 mb-2">
|
||||
<v-toolbar-title class="headline mb-0">
|
||||
<v-icon size="large" class="mr-3">
|
||||
{{ $globals.icons.pages }}
|
||||
</v-icon>
|
||||
<v-toolbar-title class="headline mb-0">
|
||||
{{ book.name }}
|
||||
</v-toolbar-title>
|
||||
<v-spacer />
|
||||
<BaseButton
|
||||
v-if="canEdit"
|
||||
class="mx-1"
|
||||
|
@ -70,7 +68,8 @@
|
|||
<script lang="ts">
|
||||
import { useLazyRecipes } from "~/composables/recipes";
|
||||
import RecipeCardSection from "@/components/Domain/Recipe/RecipeCardSection.vue";
|
||||
import { useCookbook, useCookbooks } from "~/composables/use-group-cookbooks";
|
||||
import { useCookbookStore } from "~/composables/store/use-cookbook-store";
|
||||
import { useCookbook } from "~/composables/use-group-cookbooks";
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
import type { RecipeCookBook } from "~/lib/api/types/cookbook";
|
||||
import CookbookEditor from "~/components/Domain/Cookbook/CookbookEditor.vue";
|
||||
|
@ -87,7 +86,7 @@ export default defineNuxtComponent({
|
|||
const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes(isOwnGroup.value ? null : groupSlug.value);
|
||||
const slug = route.params.slug as string;
|
||||
const { getOne } = useCookbook(isOwnGroup.value ? null : groupSlug.value);
|
||||
const { actions } = useCookbooks();
|
||||
const { actions } = useCookbookStore();
|
||||
const router = useRouter();
|
||||
|
||||
const tab = ref(null);
|
||||
|
|
|
@ -69,22 +69,22 @@ export default defineNuxtComponent({
|
|||
const i18n = useI18n();
|
||||
|
||||
const MEAL_TYPE_OPTIONS = [
|
||||
{ text: i18n.t("meal-plan.breakfast"), value: "breakfast" },
|
||||
{ text: i18n.t("meal-plan.lunch"), value: "lunch" },
|
||||
{ text: i18n.t("meal-plan.dinner"), value: "dinner" },
|
||||
{ text: i18n.t("meal-plan.side"), value: "side" },
|
||||
{ text: i18n.t("meal-plan.type-any"), value: "unset" },
|
||||
{ title: i18n.t("meal-plan.breakfast"), value: "breakfast" },
|
||||
{ title: i18n.t("meal-plan.lunch"), value: "lunch" },
|
||||
{ title: i18n.t("meal-plan.dinner"), value: "dinner" },
|
||||
{ title: i18n.t("meal-plan.side"), value: "side" },
|
||||
{ title: i18n.t("meal-plan.type-any"), value: "unset" },
|
||||
];
|
||||
|
||||
const MEAL_DAY_OPTIONS = [
|
||||
{ text: i18n.t("general.monday"), value: "monday" },
|
||||
{ text: i18n.t("general.tuesday"), value: "tuesday" },
|
||||
{ text: i18n.t("general.wednesday"), value: "wednesday" },
|
||||
{ text: i18n.t("general.thursday"), value: "thursday" },
|
||||
{ text: i18n.t("general.friday"), value: "friday" },
|
||||
{ text: i18n.t("general.saturday"), value: "saturday" },
|
||||
{ text: i18n.t("general.sunday"), value: "sunday" },
|
||||
{ text: i18n.t("meal-plan.day-any"), value: "unset" },
|
||||
{ title: i18n.t("general.monday"), value: "monday" },
|
||||
{ title: i18n.t("general.tuesday"), value: "tuesday" },
|
||||
{ title: i18n.t("general.wednesday"), value: "wednesday" },
|
||||
{ title: i18n.t("general.thursday"), value: "thursday" },
|
||||
{ title: i18n.t("general.friday"), value: "friday" },
|
||||
{ title: i18n.t("general.saturday"), value: "saturday" },
|
||||
{ title: i18n.t("general.sunday"), value: "sunday" },
|
||||
{ title: i18n.t("meal-plan.day-any"), value: "unset" },
|
||||
];
|
||||
|
||||
const inputDay = computed({
|
||||
|
|
|
@ -189,6 +189,7 @@
|
|||
:show-add="false"
|
||||
:show-label="false"
|
||||
:show-icon="false"
|
||||
variant="underlined"
|
||||
@update:model-value="setOrganizerValues(field, index, $event)"
|
||||
/>
|
||||
<RecipeOrganizerSelector
|
||||
|
@ -198,6 +199,7 @@
|
|||
:show-add="false"
|
||||
:show-label="false"
|
||||
:show-icon="false"
|
||||
variant="underlined"
|
||||
@update:model-value="setOrganizerValues(field, index, $event)"
|
||||
/>
|
||||
<RecipeOrganizerSelector
|
||||
|
@ -207,6 +209,7 @@
|
|||
:show-add="false"
|
||||
:show-label="false"
|
||||
:show-icon="false"
|
||||
variant="underlined"
|
||||
@update:model-value="setOrganizerValues(field, index, $event)"
|
||||
/>
|
||||
<RecipeOrganizerSelector
|
||||
|
@ -216,6 +219,7 @@
|
|||
:show-add="false"
|
||||
:show-label="false"
|
||||
:show-icon="false"
|
||||
variant="underlined"
|
||||
@update:model-value="setOrganizerValues(field, index, $event)"
|
||||
/>
|
||||
<RecipeOrganizerSelector
|
||||
|
@ -225,6 +229,7 @@
|
|||
:show-add="false"
|
||||
:show-label="false"
|
||||
:show-icon="false"
|
||||
variant="underlined"
|
||||
@update:model-value="setOrganizerValues(field, index, $event)"
|
||||
/>
|
||||
</v-col>
|
||||
|
|
|
@ -74,6 +74,7 @@
|
|||
:size="$vuetify.display.xs ? 'small' : undefined"
|
||||
:color="btn.color"
|
||||
variant="elevated"
|
||||
:icon="$vuetify.display.xs"
|
||||
@click="emitHandler(btn.event)"
|
||||
>
|
||||
<v-icon :left="!$vuetify.display.xs">
|
||||
|
|
|
@ -1,101 +1,104 @@
|
|||
<template>
|
||||
<!-- Wrap v-hover with a div to provide a proper DOM element for the transition -->
|
||||
<v-lazy>
|
||||
<v-hover
|
||||
v-slot="{ isHovering, props }"
|
||||
:open-delay="50"
|
||||
>
|
||||
<v-card
|
||||
v-bind="props"
|
||||
:class="{ 'on-hover': isHovering }"
|
||||
:style="{ cursor }"
|
||||
:elevation="isHovering ? 12 : 2"
|
||||
:to="recipeRoute"
|
||||
:min-height="imageHeight + 75"
|
||||
@click.self="$emit('click')"
|
||||
<div>
|
||||
<v-hover
|
||||
v-slot="{ isHovering, props }"
|
||||
:open-delay="50"
|
||||
>
|
||||
<RecipeCardImage
|
||||
:icon-size="imageHeight"
|
||||
:height="imageHeight"
|
||||
:slug="slug"
|
||||
:recipe-id="recipeId"
|
||||
size="small"
|
||||
:image-version="image"
|
||||
<v-card
|
||||
v-bind="props"
|
||||
:class="{ 'on-hover': isHovering }"
|
||||
:style="{ cursor }"
|
||||
:elevation="isHovering ? 12 : 2"
|
||||
:to="recipeRoute"
|
||||
:min-height="imageHeight + 75"
|
||||
@click.self="$emit('click')"
|
||||
>
|
||||
<v-expand-transition v-if="description">
|
||||
<div
|
||||
v-if="isHovering"
|
||||
class="d-flex transition-fast-in-fast-out bg-secondary v-card--reveal"
|
||||
style="height: 100%"
|
||||
>
|
||||
<v-card-text class="v-card--text-show white--text">
|
||||
<div class="descriptionWrapper">
|
||||
<SafeMarkdown :source="description" />
|
||||
</div>
|
||||
</v-card-text>
|
||||
</div>
|
||||
</v-expand-transition>
|
||||
</RecipeCardImage>
|
||||
<v-card-title class="mb-n3 px-4">
|
||||
<div class="headerClass">
|
||||
{{ name }}
|
||||
</div>
|
||||
</v-card-title>
|
||||
|
||||
<slot name="actions">
|
||||
<v-card-actions
|
||||
v-if="showRecipeContent"
|
||||
class="px-1"
|
||||
<RecipeCardImage
|
||||
:icon-size="imageHeight"
|
||||
:height="imageHeight"
|
||||
:slug="slug"
|
||||
:recipe-id="recipeId"
|
||||
size="small"
|
||||
:image-version="image"
|
||||
>
|
||||
<RecipeFavoriteBadge
|
||||
v-if="isOwnGroup"
|
||||
class="absolute"
|
||||
:recipe-id="recipeId"
|
||||
show-always
|
||||
/>
|
||||
<div v-else class="px-1" /> <!-- Empty div to keep the layout consistent -->
|
||||
<v-expand-transition v-if="description">
|
||||
<div
|
||||
v-if="isHovering"
|
||||
class="d-flex transition-fast-in-fast-out bg-secondary v-card--reveal"
|
||||
style="height: 100%"
|
||||
>
|
||||
<v-card-text class="v-card--text-show white--text">
|
||||
<div class="descriptionWrapper">
|
||||
<SafeMarkdown :source="description" />
|
||||
</div>
|
||||
</v-card-text>
|
||||
</div>
|
||||
</v-expand-transition>
|
||||
</RecipeCardImage>
|
||||
<v-card-title class="mb-n3 px-4">
|
||||
<div class="headerClass">
|
||||
{{ name }}
|
||||
</div>
|
||||
</v-card-title>
|
||||
|
||||
<RecipeRating
|
||||
class="ml-n2"
|
||||
:value="rating"
|
||||
:recipe-id="recipeId"
|
||||
:slug="slug"
|
||||
small
|
||||
/>
|
||||
<v-spacer />
|
||||
<RecipeChips
|
||||
:truncate="true"
|
||||
:items="tags"
|
||||
:title="false"
|
||||
:limit="2"
|
||||
small
|
||||
url-prefix="tags"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
<slot name="actions">
|
||||
<v-card-actions
|
||||
v-if="showRecipeContent"
|
||||
class="px-1"
|
||||
>
|
||||
<RecipeFavoriteBadge
|
||||
v-if="isOwnGroup"
|
||||
class="absolute"
|
||||
:recipe-id="recipeId"
|
||||
show-always
|
||||
/>
|
||||
<div v-else class="px-1" /> <!-- Empty div to keep the layout consistent -->
|
||||
|
||||
<!-- If we're not logged-in, no items display, so we hide this menu -->
|
||||
<RecipeContextMenu
|
||||
v-if="isOwnGroup"
|
||||
color="grey-darken-2"
|
||||
:slug="slug"
|
||||
:name="name"
|
||||
:recipe-id="recipeId"
|
||||
:use-items="{
|
||||
delete: false,
|
||||
edit: false,
|
||||
download: true,
|
||||
mealplanner: true,
|
||||
shoppingList: true,
|
||||
print: false,
|
||||
printPreferences: false,
|
||||
share: true,
|
||||
}"
|
||||
@delete="$emit('delete', slug)"
|
||||
/>
|
||||
</v-card-actions>
|
||||
</slot>
|
||||
<slot />
|
||||
</v-card>
|
||||
</v-hover>
|
||||
<RecipeRating
|
||||
class="ml-n2"
|
||||
:value="rating"
|
||||
:recipe-id="recipeId"
|
||||
:slug="slug"
|
||||
small
|
||||
/>
|
||||
<v-spacer />
|
||||
<RecipeChips
|
||||
:truncate="true"
|
||||
:items="tags"
|
||||
:title="false"
|
||||
:limit="2"
|
||||
small
|
||||
url-prefix="tags"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
|
||||
<!-- If we're not logged-in, no items display, so we hide this menu -->
|
||||
<RecipeContextMenu
|
||||
v-if="isOwnGroup"
|
||||
color="grey-darken-2"
|
||||
:slug="slug"
|
||||
:name="name"
|
||||
:recipe-id="recipeId"
|
||||
:use-items="{
|
||||
delete: false,
|
||||
edit: false,
|
||||
download: true,
|
||||
mealplanner: true,
|
||||
shoppingList: true,
|
||||
print: false,
|
||||
printPreferences: false,
|
||||
share: true,
|
||||
}"
|
||||
@delete="$emit('delete', slug)"
|
||||
/>
|
||||
</v-card-actions>
|
||||
</slot>
|
||||
<slot />
|
||||
</v-card>
|
||||
</v-hover>
|
||||
</div>
|
||||
</v-lazy>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
v-for="category in items.slice(0, limit)"
|
||||
:key="category.name"
|
||||
label
|
||||
class="ma-1"
|
||||
class="mr-1 mt-1"
|
||||
color="accent"
|
||||
variant="flat"
|
||||
:size="small ? 'small' : 'default'"
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
id="arrow-search"
|
||||
v-model="search.query.value"
|
||||
autofocus
|
||||
variant="solo-filled"
|
||||
variant="solo"
|
||||
flat
|
||||
autocomplete="off"
|
||||
bg-color="primary-lighten-1"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<v-container
|
||||
fluid
|
||||
class="pa-0"
|
||||
class="px-0"
|
||||
>
|
||||
<div class="search-container pb-8">
|
||||
<form
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
clearable
|
||||
:messages="messages"
|
||||
>
|
||||
<template #append-outer>
|
||||
<template #append>
|
||||
<v-btn
|
||||
class="ml-2"
|
||||
color="primary"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="ma-0 pa-0 text-subtitle-1 dense-markdown ingredient-item">
|
||||
<div class="text-subtitle-1 dense-markdown ingredient-item">
|
||||
<SafeMarkdown
|
||||
v-if="parsedIng.quantity"
|
||||
class="d-inline"
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
</template>
|
||||
<v-list-item
|
||||
density="compact"
|
||||
class="pa-0"
|
||||
@click.stop="toggleChecked(index)"
|
||||
>
|
||||
<template #prepend>
|
||||
|
|
|
@ -59,7 +59,7 @@
|
|||
<v-app-bar
|
||||
color="transparent"
|
||||
flat
|
||||
class="mt-n1 rounded align-center px-4 position-relative w-100 left-0 top-0"
|
||||
class="mt-n1 rounded align-center position-relative w-100 left-0 top-0"
|
||||
>
|
||||
<v-icon
|
||||
size="large"
|
||||
|
|
|
@ -9,9 +9,9 @@
|
|||
closable-chips
|
||||
item-title="name"
|
||||
multiple
|
||||
variant="underlined"
|
||||
:variant="variant"
|
||||
:prepend-inner-icon="icon"
|
||||
:append-icon="$globals.icons.create"
|
||||
:append-icon="showAdd ? $globals.icons.create : undefined"
|
||||
return-object
|
||||
auto-select-first
|
||||
class="pa-0"
|
||||
|
@ -93,6 +93,10 @@ export default defineNuxtComponent({
|
|||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
variant: {
|
||||
type: String as () => "filled" | "underlined" | "outlined" | "plain" | "solo" | "solo-inverted" | "solo-filled",
|
||||
default: "outlined",
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-container v-show="!isCookMode" key="recipe-page" class="pt-0" :class="{ 'pa-0': $vuetify.display.smAndDown.value }">
|
||||
<v-container v-show="!isCookMode" key="recipe-page" class="px-0" :class="{ 'pa-0': $vuetify.display.smAndDown.value }">
|
||||
<v-card :flat="$vuetify.display.smAndDown.value" class="d-print-none">
|
||||
<RecipePageHeader
|
||||
:recipe="recipe"
|
||||
|
@ -87,11 +87,10 @@
|
|||
/>
|
||||
<RecipePrintContainer :recipe="recipe" :scale="scale" />
|
||||
</v-container>
|
||||
<!-- Cook mode displayes two columns with ingredients and instructions side by side, each being scrolled individually, allowing to view both at the same timer -->
|
||||
<!-- Cook mode displayes two columns with ingredients and instructions side by side, each being scrolled individually, allowing to view both at the same time -->
|
||||
<v-sheet
|
||||
v-show="isCookMode && !hasLinkedIngredients"
|
||||
key="cookmode"
|
||||
:style="{ height: $vuetify.display.smAndUp ? 'calc(100vh - 48px)' : '' }"
|
||||
>
|
||||
<!-- the calc is to account for the toolbar a more dynamic solution could be needed -->
|
||||
<v-row style="height: 100%" no-gutters class="overflow-hidden">
|
||||
|
@ -107,7 +106,12 @@
|
|||
/>
|
||||
<v-divider />
|
||||
</v-col>
|
||||
<v-col class="overflow-y-auto py-2" style="height: 100%" cols="12" sm="7">
|
||||
<v-col class="overflow-y-auto"
|
||||
:class="$vuetify.display.smAndDown.value ? 'py-2': 'py-6'"
|
||||
style="height: 100%" cols="12" sm="7">
|
||||
<h2 class="text-h5 px-4 font-weight-medium opacity-80">
|
||||
{{ $t('recipe.instructions') }}
|
||||
</h2>
|
||||
<RecipePageInstructions
|
||||
v-model="recipe.recipeInstructions"
|
||||
v-model:assets="recipe.assets"
|
||||
|
|
|
@ -1,31 +1,28 @@
|
|||
<template>
|
||||
<div class="d-flex justify-start align-top py-2">
|
||||
<div class="d-flex justify-start align-top flex-wrap">
|
||||
<RecipeImageUploadBtn
|
||||
class="my-1"
|
||||
class="my-2"
|
||||
:slug="recipe.slug"
|
||||
@upload="uploadImage"
|
||||
@refresh="imageKey++"
|
||||
/>
|
||||
<RecipeSettingsMenu
|
||||
v-model="recipe.settings"
|
||||
class="my-1 mx-1"
|
||||
class="my-2 mx-1"
|
||||
:is-owner="recipe.userId == user.id"
|
||||
@upload="uploadImage"
|
||||
/>
|
||||
<v-spacer />
|
||||
<v-container
|
||||
class="py-0"
|
||||
style="width: 40%;"
|
||||
>
|
||||
<v-select
|
||||
v-model="recipe.userId"
|
||||
class="my-2"
|
||||
max-width="300"
|
||||
:items="allUsers"
|
||||
item-title="fullName"
|
||||
item-value="id"
|
||||
:item-props="itemsProps"
|
||||
:label="$t('general.owner')"
|
||||
hide-details
|
||||
:disabled="!canEditOwner"
|
||||
variant="underlined"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
>
|
||||
<template #prepend>
|
||||
<UserAvatar
|
||||
|
@ -34,17 +31,7 @@
|
|||
/>
|
||||
</template>
|
||||
</v-select>
|
||||
<v-card-text
|
||||
v-if="ownerHousehold"
|
||||
class="pa-0 d-flex"
|
||||
style="align-items: flex-end;"
|
||||
>
|
||||
<v-spacer />
|
||||
<v-icon>{{ $globals.icons.household }}</v-icon>
|
||||
<span class="pl-1">{{ ownerHousehold.name }}</span>
|
||||
</v-card-text>
|
||||
</v-container>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
@ -71,13 +58,15 @@ const canEditOwner = computed(() => {
|
|||
|
||||
const { store: allUsers } = useUserStore();
|
||||
const { store: households } = useHouseholdStore();
|
||||
const ownerHousehold = computed(() => {
|
||||
const owner = allUsers.value.find(u => u.id === recipe.value.userId);
|
||||
if (!owner) {
|
||||
return null;
|
||||
}
|
||||
return households.value.find(h => h.id === owner.householdId);
|
||||
});
|
||||
|
||||
function itemsProps(item: any) {
|
||||
const owner = allUsers.value.find(u => u.id === item.id);
|
||||
return {
|
||||
value: item.id,
|
||||
title: item.fullName,
|
||||
subtitle: owner ? households.value.find(h => h.id === owner.householdId)?.name || "" : "",
|
||||
};
|
||||
}
|
||||
|
||||
async function uploadImage(fileObject: File) {
|
||||
if (!recipe.value || !recipe.value.slug) {
|
||||
|
|
|
@ -11,15 +11,17 @@
|
|||
class="d-flex flex-column justify-center align-center"
|
||||
>
|
||||
<v-card-text class="w-100">
|
||||
<v-card-title class="text-h5 font-weight-regular pa-0 d-flex flex-column align-center justify-center opacity-80">
|
||||
<div class="d-flex flex-column align-center">
|
||||
<v-card-title class="text-h5 font-weight-regular pa-0 text-wrap text-center opacity-80">
|
||||
{{ recipe.name }}
|
||||
<RecipeRating
|
||||
</v-card-title>
|
||||
<RecipeRating
|
||||
:key="recipe.slug"
|
||||
:value="recipe.rating"
|
||||
:recipe-id="recipe.id"
|
||||
:slug="recipe.slug"
|
||||
/>
|
||||
</v-card-title>
|
||||
</div>
|
||||
<v-divider class="my-2" />
|
||||
<SafeMarkdown :source="recipe.description" class="my-3" />
|
||||
<v-divider v-if="recipe.description" />
|
||||
|
@ -52,7 +54,7 @@
|
|||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
<div class="mx-6">
|
||||
<div v-if="recipe.prepTime || recipe.totalTime || recipe.performTime" class="mx-6">
|
||||
<RecipeTimeCard
|
||||
container-class="d-flex flex-wrap justify-center"
|
||||
:prep-time="recipe.prepTime"
|
||||
|
|
|
@ -15,12 +15,13 @@
|
|||
v-for="(tool, index) in recipe.tools"
|
||||
:key="index"
|
||||
density="compact"
|
||||
class="px-1"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-checkbox
|
||||
v-model="recipeTools[index].onHand"
|
||||
hide-details
|
||||
class="pt-0 my-auto py-auto"
|
||||
class="pt-0 py-auto"
|
||||
color="secondary"
|
||||
density="compact"
|
||||
@change="updateTool(index)"
|
||||
|
|
|
@ -356,8 +356,9 @@
|
|||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { VueDraggable } from "vue-draggable-plus";
|
||||
import { computed, nextTick, onMounted, ref, watch } from "vue";
|
||||
import RecipeIngredientHtml from "../../RecipeIngredientHtml.vue";
|
||||
import type { RecipeStep, IngredientReferences, RecipeIngredient, RecipeAsset, Recipe } from "~/lib/api/types/recipe";
|
||||
import { parseIngredientText } from "~/composables/recipes";
|
||||
|
@ -376,443 +377,345 @@ interface MergerHistory {
|
|||
sourceText: string;
|
||||
}
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
VueDraggable,
|
||||
RecipeIngredientHtml,
|
||||
DropZone,
|
||||
RecipeIngredients,
|
||||
const instructionList = defineModel<RecipeStep[]>("modelValue", { required: true, default: () => [] });
|
||||
const assets = defineModel<RecipeAsset[]>("assets", { required: true, default: () => [] });
|
||||
|
||||
const props = defineProps({
|
||||
recipe: {
|
||||
type: Object as () => NoUndefinedField<Recipe>,
|
||||
required: true,
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Array as () => RecipeStep[],
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
recipe: {
|
||||
type: Object as () => NoUndefinedField<Recipe>,
|
||||
required: true,
|
||||
},
|
||||
assets: {
|
||||
type: Array as () => RecipeAsset[],
|
||||
required: true,
|
||||
},
|
||||
scale: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue", "click-instruction-field", "update:assets"],
|
||||
|
||||
setup(props, context) {
|
||||
const i18n = useI18n();
|
||||
const BASE_URL = useRequestURL().origin;
|
||||
|
||||
const { isCookMode, toggleCookMode, isEditForm } = usePageState(props.recipe.slug);
|
||||
|
||||
const state = reactive({
|
||||
dialog: false,
|
||||
disabledSteps: [] as number[],
|
||||
unusedIngredients: [] as RecipeIngredient[],
|
||||
usedIngredients: [] as RecipeIngredient[],
|
||||
});
|
||||
|
||||
const showTitleEditor = ref<{ [key: string]: boolean }>({});
|
||||
|
||||
const actionEvents = [
|
||||
{
|
||||
text: i18n.t("recipe.toggle-section") as string,
|
||||
event: "toggle-section",
|
||||
},
|
||||
{
|
||||
text: i18n.t("recipe.link-ingredients") as string,
|
||||
event: "link-ingredients",
|
||||
},
|
||||
{
|
||||
text: i18n.t("recipe.merge-above") as string,
|
||||
event: "merge-above",
|
||||
},
|
||||
];
|
||||
|
||||
// ===============================================================
|
||||
// UI State Helpers
|
||||
|
||||
function hasSectionTitle(title: string | undefined) {
|
||||
return !(title === null || title === "" || title === undefined);
|
||||
}
|
||||
|
||||
watch(props.modelValue, (v) => {
|
||||
state.disabledSteps = [];
|
||||
|
||||
v.forEach((element: RecipeStep) => {
|
||||
if (element.id !== undefined) {
|
||||
showTitleEditor.value[element.id!] = hasSectionTitle(element.title!);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const showCookMode = ref(false);
|
||||
|
||||
// Eliminate state with an eager call to watcher?
|
||||
onMounted(() => {
|
||||
props.modelValue.forEach((element: RecipeStep) => {
|
||||
if (element.id !== undefined) {
|
||||
showTitleEditor.value[element.id!] = hasSectionTitle(element.title!);
|
||||
}
|
||||
|
||||
// showCookMode.value = false;
|
||||
if (showCookMode.value === false && element.ingredientReferences && element.ingredientReferences.length > 0) {
|
||||
showCookMode.value = true;
|
||||
}
|
||||
|
||||
showTitleEditor.value = { ...showTitleEditor.value };
|
||||
});
|
||||
});
|
||||
|
||||
function toggleDisabled(stepIndex: number) {
|
||||
if (isEditForm.value) {
|
||||
return;
|
||||
}
|
||||
if (state.disabledSteps.includes(stepIndex)) {
|
||||
const index = state.disabledSteps.indexOf(stepIndex);
|
||||
if (index !== -1) {
|
||||
state.disabledSteps.splice(index, 1);
|
||||
}
|
||||
}
|
||||
else {
|
||||
state.disabledSteps.push(stepIndex);
|
||||
}
|
||||
}
|
||||
|
||||
function isChecked(stepIndex: number) {
|
||||
if (state.disabledSteps.includes(stepIndex) && !isEditForm.value) {
|
||||
return "disabled-card";
|
||||
}
|
||||
}
|
||||
|
||||
function toggleShowTitle(id?: string) {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
||||
showTitleEditor.value[id] = !showTitleEditor.value[id];
|
||||
|
||||
const temp = { ...showTitleEditor.value };
|
||||
showTitleEditor.value = temp;
|
||||
}
|
||||
|
||||
const instructionList = ref<RecipeStep[]>([...props.modelValue]);
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
instructionList.value = [...newVal];
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
function onDragEnd() {
|
||||
context.emit("update:modelValue", [...instructionList.value]);
|
||||
drag.value = false;
|
||||
}
|
||||
|
||||
// ===============================================================
|
||||
// Ingredient Linker
|
||||
const activeRefs = ref<string[]>([]);
|
||||
const activeIndex = ref(0);
|
||||
const activeText = ref("");
|
||||
|
||||
function openDialog(idx: number, text: string, refs?: IngredientReferences[]) {
|
||||
if (!refs) {
|
||||
instructionList.value[idx].ingredientReferences = [];
|
||||
refs = props.modelValue[idx].ingredientReferences as IngredientReferences[];
|
||||
}
|
||||
|
||||
setUsedIngredients();
|
||||
activeText.value = text;
|
||||
activeIndex.value = idx;
|
||||
state.dialog = true;
|
||||
activeRefs.value = refs.map(ref => ref.referenceId ?? "");
|
||||
}
|
||||
|
||||
const availableNextStep = computed(() => activeIndex.value < props.modelValue.length - 1);
|
||||
|
||||
function setIngredientIds() {
|
||||
const instruction = props.modelValue[activeIndex.value];
|
||||
instruction.ingredientReferences = activeRefs.value.map((ref) => {
|
||||
return {
|
||||
referenceId: ref,
|
||||
};
|
||||
});
|
||||
|
||||
// Update the visibility of the cook mode button
|
||||
showCookMode.value = false;
|
||||
props.modelValue.forEach((element) => {
|
||||
if (showCookMode.value === false && element.ingredientReferences && element.ingredientReferences.length > 0) {
|
||||
showCookMode.value = true;
|
||||
}
|
||||
});
|
||||
state.dialog = false;
|
||||
}
|
||||
|
||||
function saveAndOpenNextLinkIngredients() {
|
||||
const currentStepIndex = activeIndex.value;
|
||||
|
||||
if (!availableNextStep.value) {
|
||||
return; // no next step, the button calling this function should not be shown
|
||||
}
|
||||
|
||||
setIngredientIds();
|
||||
const nextStep = props.modelValue[currentStepIndex + 1];
|
||||
// close dialog before opening to reset the scroll position
|
||||
nextTick(() => openDialog(currentStepIndex + 1, nextStep.text, nextStep.ingredientReferences));
|
||||
}
|
||||
|
||||
function setUsedIngredients() {
|
||||
const usedRefs: { [key: string]: boolean } = {};
|
||||
|
||||
props.modelValue.forEach((element) => {
|
||||
element.ingredientReferences?.forEach((ref) => {
|
||||
if (ref.referenceId !== undefined) {
|
||||
usedRefs[ref.referenceId!] = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
state.usedIngredients = props.recipe.recipeIngredient.filter((ing) => {
|
||||
return ing.referenceId !== undefined && ing.referenceId in usedRefs;
|
||||
});
|
||||
|
||||
state.unusedIngredients = props.recipe.recipeIngredient.filter((ing) => {
|
||||
return !(ing.referenceId !== undefined && ing.referenceId in usedRefs);
|
||||
});
|
||||
}
|
||||
|
||||
function autoSetReferences() {
|
||||
useExtractIngredientReferences(
|
||||
props.recipe.recipeIngredient,
|
||||
activeRefs.value,
|
||||
activeText.value,
|
||||
props.recipe.settings.disableAmount,
|
||||
).forEach((ingredient: string) => activeRefs.value.push(ingredient));
|
||||
}
|
||||
|
||||
const ingredientLookup = computed(() => {
|
||||
const results: { [key: string]: RecipeIngredient } = {};
|
||||
return props.recipe.recipeIngredient.reduce((prev, ing) => {
|
||||
if (ing.referenceId === undefined) {
|
||||
return prev;
|
||||
}
|
||||
prev[ing.referenceId] = ing;
|
||||
return prev;
|
||||
}, results);
|
||||
});
|
||||
|
||||
function getIngredientByRefId(refId: string | undefined) {
|
||||
if (refId === undefined) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const ing = ingredientLookup.value[refId];
|
||||
if (!ing) return "";
|
||||
return parseIngredientText(ing, props.recipe.settings.disableAmount, props.scale);
|
||||
}
|
||||
|
||||
// ===============================================================
|
||||
// Instruction Merger
|
||||
const mergeHistory = ref<MergerHistory[]>([]);
|
||||
|
||||
function mergeAbove(target: number, source: number) {
|
||||
if (target < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
mergeHistory.value.push({
|
||||
target,
|
||||
source,
|
||||
targetText: props.modelValue[target].text,
|
||||
sourceText: props.modelValue[source].text,
|
||||
});
|
||||
|
||||
instructionList.value[target].text += " " + props.modelValue[source].text;
|
||||
instructionList.value.splice(source, 1);
|
||||
}
|
||||
|
||||
function undoMerge(event: KeyboardEvent) {
|
||||
if (event.ctrlKey && event.code === "KeyZ") {
|
||||
if (!(mergeHistory.value?.length > 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastMerge = mergeHistory.value.pop();
|
||||
if (!lastMerge) {
|
||||
return;
|
||||
}
|
||||
|
||||
instructionList.value[lastMerge.target].text = lastMerge.targetText;
|
||||
instructionList.value.splice(lastMerge.source, 0, {
|
||||
id: uuid4(),
|
||||
title: "",
|
||||
text: lastMerge.sourceText,
|
||||
ingredientReferences: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function moveTo(dest: string, source: number) {
|
||||
if (dest === "top") {
|
||||
instructionList.value.unshift(instructionList.value.splice(source, 1)[0]);
|
||||
}
|
||||
else {
|
||||
instructionList.value.push(instructionList.value.splice(source, 1)[0]);
|
||||
}
|
||||
}
|
||||
|
||||
function insert(dest: number) {
|
||||
instructionList.value.splice(dest, 0, { id: uuid4(), text: "", title: "", ingredientReferences: [] });
|
||||
}
|
||||
|
||||
const previewStates = ref<boolean[]>([]);
|
||||
|
||||
function togglePreviewState(index: number) {
|
||||
const temp = [...previewStates.value];
|
||||
temp[index] = !temp[index];
|
||||
previewStates.value = temp;
|
||||
}
|
||||
|
||||
function toggleCollapseSection(index: number) {
|
||||
const sectionSteps: number[] = [];
|
||||
|
||||
for (let i = index; i < instructionList.value.length; i++) {
|
||||
if (!(i === index) && hasSectionTitle(instructionList.value[i].title!)) {
|
||||
break;
|
||||
}
|
||||
else {
|
||||
sectionSteps.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
const allCollapsed = sectionSteps.every(idx => state.disabledSteps.includes(idx));
|
||||
|
||||
if (allCollapsed) {
|
||||
state.disabledSteps = state.disabledSteps.filter(idx => !sectionSteps.includes(idx));
|
||||
}
|
||||
else {
|
||||
state.disabledSteps = [...state.disabledSteps, ...sectionSteps];
|
||||
}
|
||||
}
|
||||
|
||||
const drag = ref(false);
|
||||
|
||||
// ===============================================================
|
||||
// Image Uploader
|
||||
const api = useUserApi();
|
||||
const { recipeAssetPath } = useStaticRoutes();
|
||||
|
||||
const imageUploadMode = ref(false);
|
||||
|
||||
function toggleDragMode() {
|
||||
console.log("Toggling Drag Mode");
|
||||
imageUploadMode.value = !imageUploadMode.value;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.assets === undefined) {
|
||||
context.emit("update:assets", []);
|
||||
}
|
||||
});
|
||||
|
||||
const loadingStates = ref<{ [key: number]: boolean }>({});
|
||||
|
||||
async function handleImageDrop(index: number, files: File[]) {
|
||||
if (!files) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the file is an image
|
||||
const file = files[0];
|
||||
if (!file || !file.type.startsWith("image/")) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadingStates.value[index] = true;
|
||||
|
||||
const { data } = await api.recipes.createAsset(props.recipe.slug, {
|
||||
name: file.name,
|
||||
icon: "mdi-file-image",
|
||||
file,
|
||||
extension: file.name.split(".").pop() || "",
|
||||
});
|
||||
|
||||
loadingStates.value[index] = false;
|
||||
|
||||
if (!data) {
|
||||
return; // TODO: Handle error
|
||||
}
|
||||
|
||||
context.emit("update:assets", [...props.assets, data]);
|
||||
const assetUrl = BASE_URL + recipeAssetPath(props.recipe.id, data.fileName as string);
|
||||
const text = `<img src="${assetUrl}" height="100%" width="100%"/>`;
|
||||
instructionList.value[index].text += text;
|
||||
}
|
||||
|
||||
function openImageUpload(index: number) {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = "image/*";
|
||||
input.onchange = async () => {
|
||||
if (input.files) {
|
||||
await handleImageDrop(index, Array.from(input.files));
|
||||
input.remove();
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
}
|
||||
|
||||
const breakpoint = useDisplay();
|
||||
|
||||
return {
|
||||
// Image Uploader
|
||||
toggleDragMode,
|
||||
handleImageDrop,
|
||||
imageUploadMode,
|
||||
openImageUpload,
|
||||
loadingStates,
|
||||
|
||||
// Rest
|
||||
onDragEnd,
|
||||
drag,
|
||||
togglePreviewState,
|
||||
toggleCollapseSection,
|
||||
previewStates,
|
||||
...toRefs(state),
|
||||
actionEvents,
|
||||
activeRefs,
|
||||
activeText,
|
||||
getIngredientByRefId,
|
||||
showTitleEditor,
|
||||
mergeAbove,
|
||||
moveTo,
|
||||
openDialog,
|
||||
setIngredientIds,
|
||||
availableNextStep,
|
||||
saveAndOpenNextLinkIngredients,
|
||||
undoMerge,
|
||||
toggleDisabled,
|
||||
isChecked,
|
||||
toggleShowTitle,
|
||||
instructionList,
|
||||
autoSetReferences,
|
||||
parseIngredientText,
|
||||
toggleCookMode,
|
||||
showCookMode,
|
||||
isCookMode,
|
||||
isEditForm,
|
||||
insert,
|
||||
breakpoint,
|
||||
};
|
||||
scale: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["click-instruction-field", "update:assets"]);
|
||||
|
||||
const BASE_URL = useRequestURL().origin;
|
||||
|
||||
const { isCookMode, toggleCookMode, isEditForm } = usePageState(props.recipe.slug);
|
||||
|
||||
const dialog = ref(false);
|
||||
const disabledSteps = ref<number[]>([]);
|
||||
const unusedIngredients = ref<RecipeIngredient[]>([]);
|
||||
const usedIngredients = ref<RecipeIngredient[]>([]);
|
||||
|
||||
const showTitleEditor = ref<{ [key: string]: boolean }>({});
|
||||
|
||||
// ===============================================================
|
||||
// UI State Helpers
|
||||
|
||||
function hasSectionTitle(title: string | undefined) {
|
||||
return !(title === null || title === "" || title === undefined);
|
||||
}
|
||||
|
||||
watch(instructionList, (v) => {
|
||||
disabledSteps.value = [];
|
||||
|
||||
v.forEach((element: RecipeStep) => {
|
||||
if (element.id !== undefined) {
|
||||
showTitleEditor.value[element.id!] = hasSectionTitle(element.title!);
|
||||
}
|
||||
});
|
||||
}, { deep: true });
|
||||
|
||||
const showCookMode = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
instructionList.value.forEach((element: RecipeStep) => {
|
||||
if (element.id !== undefined) {
|
||||
showTitleEditor.value[element.id!] = hasSectionTitle(element.title!);
|
||||
}
|
||||
|
||||
if (showCookMode.value === false && element.ingredientReferences && element.ingredientReferences.length > 0) {
|
||||
showCookMode.value = true;
|
||||
}
|
||||
|
||||
showTitleEditor.value = { ...showTitleEditor.value };
|
||||
});
|
||||
|
||||
if (assets.value === undefined) {
|
||||
emit("update:assets", []);
|
||||
}
|
||||
});
|
||||
|
||||
function toggleDisabled(stepIndex: number) {
|
||||
if (isEditForm.value) {
|
||||
return;
|
||||
}
|
||||
if (disabledSteps.value.includes(stepIndex)) {
|
||||
const index = disabledSteps.value.indexOf(stepIndex);
|
||||
if (index !== -1) {
|
||||
disabledSteps.value.splice(index, 1);
|
||||
}
|
||||
}
|
||||
else {
|
||||
disabledSteps.value.push(stepIndex);
|
||||
}
|
||||
}
|
||||
|
||||
function isChecked(stepIndex: number) {
|
||||
if (disabledSteps.value.includes(stepIndex) && !isEditForm.value) {
|
||||
return "disabled-card";
|
||||
}
|
||||
}
|
||||
|
||||
function toggleShowTitle(id?: string) {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
||||
showTitleEditor.value[id] = !showTitleEditor.value[id];
|
||||
|
||||
const temp = { ...showTitleEditor.value };
|
||||
showTitleEditor.value = temp;
|
||||
}
|
||||
|
||||
function onDragEnd() {
|
||||
drag.value = false;
|
||||
}
|
||||
|
||||
// ===============================================================
|
||||
// Ingredient Linker
|
||||
const activeRefs = ref<string[]>([]);
|
||||
const activeIndex = ref(0);
|
||||
const activeText = ref("");
|
||||
|
||||
function openDialog(idx: number, text: string, refs?: IngredientReferences[]) {
|
||||
if (!refs) {
|
||||
instructionList.value[idx].ingredientReferences = [];
|
||||
refs = instructionList.value[idx].ingredientReferences as IngredientReferences[];
|
||||
}
|
||||
|
||||
setUsedIngredients();
|
||||
activeText.value = text;
|
||||
activeIndex.value = idx;
|
||||
dialog.value = true;
|
||||
activeRefs.value = refs.map(ref => ref.referenceId ?? "");
|
||||
}
|
||||
|
||||
const availableNextStep = computed(() => activeIndex.value < instructionList.value.length - 1);
|
||||
|
||||
function setIngredientIds() {
|
||||
const instruction = instructionList.value[activeIndex.value];
|
||||
instruction.ingredientReferences = activeRefs.value.map((ref) => {
|
||||
return {
|
||||
referenceId: ref,
|
||||
};
|
||||
});
|
||||
|
||||
// Update the visibility of the cook mode button
|
||||
showCookMode.value = false;
|
||||
instructionList.value.forEach((element) => {
|
||||
if (showCookMode.value === false && element.ingredientReferences && element.ingredientReferences.length > 0) {
|
||||
showCookMode.value = true;
|
||||
}
|
||||
});
|
||||
dialog.value = false;
|
||||
}
|
||||
|
||||
function saveAndOpenNextLinkIngredients() {
|
||||
const currentStepIndex = activeIndex.value;
|
||||
|
||||
if (!availableNextStep.value) {
|
||||
return; // no next step, the button calling this function should not be shown
|
||||
}
|
||||
|
||||
setIngredientIds();
|
||||
const nextStep = instructionList.value[currentStepIndex + 1];
|
||||
// close dialog before opening to reset the scroll position
|
||||
nextTick(() => openDialog(currentStepIndex + 1, nextStep.text, nextStep.ingredientReferences));
|
||||
}
|
||||
|
||||
function setUsedIngredients() {
|
||||
const usedRefs: { [key: string]: boolean } = {};
|
||||
|
||||
instructionList.value.forEach((element) => {
|
||||
element.ingredientReferences?.forEach((ref) => {
|
||||
if (ref.referenceId !== undefined) {
|
||||
usedRefs[ref.referenceId!] = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
usedIngredients.value = props.recipe.recipeIngredient.filter((ing) => {
|
||||
return ing.referenceId !== undefined && ing.referenceId in usedRefs;
|
||||
});
|
||||
|
||||
unusedIngredients.value = props.recipe.recipeIngredient.filter((ing) => {
|
||||
return !(ing.referenceId !== undefined && ing.referenceId in usedRefs);
|
||||
});
|
||||
}
|
||||
|
||||
function autoSetReferences() {
|
||||
useExtractIngredientReferences(
|
||||
props.recipe.recipeIngredient,
|
||||
activeRefs.value,
|
||||
activeText.value,
|
||||
props.recipe.settings.disableAmount,
|
||||
).forEach((ingredient: string) => activeRefs.value.push(ingredient));
|
||||
}
|
||||
|
||||
const ingredientLookup = computed(() => {
|
||||
const results: { [key: string]: RecipeIngredient } = {};
|
||||
return props.recipe.recipeIngredient.reduce((prev, ing) => {
|
||||
if (ing.referenceId === undefined) {
|
||||
return prev;
|
||||
}
|
||||
prev[ing.referenceId] = ing;
|
||||
return prev;
|
||||
}, results);
|
||||
});
|
||||
|
||||
function getIngredientByRefId(refId: string | undefined) {
|
||||
if (refId === undefined) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const ing = ingredientLookup.value[refId];
|
||||
if (!ing) return "";
|
||||
return parseIngredientText(ing, props.recipe.settings.disableAmount, props.scale);
|
||||
}
|
||||
|
||||
// ===============================================================
|
||||
// Instruction Merger
|
||||
const mergeHistory = ref<MergerHistory[]>([]);
|
||||
|
||||
function mergeAbove(target: number, source: number) {
|
||||
if (target < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
mergeHistory.value.push({
|
||||
target,
|
||||
source,
|
||||
targetText: instructionList.value[target].text,
|
||||
sourceText: instructionList.value[source].text,
|
||||
});
|
||||
|
||||
instructionList.value[target].text += " " + instructionList.value[source].text;
|
||||
instructionList.value.splice(source, 1);
|
||||
}
|
||||
|
||||
function undoMerge(event: KeyboardEvent) {
|
||||
if (event.ctrlKey && event.code === "KeyZ") {
|
||||
if (!(mergeHistory.value?.length > 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastMerge = mergeHistory.value.pop();
|
||||
if (!lastMerge) {
|
||||
return;
|
||||
}
|
||||
|
||||
instructionList.value[lastMerge.target].text = lastMerge.targetText;
|
||||
instructionList.value.splice(lastMerge.source, 0, {
|
||||
id: uuid4(),
|
||||
title: "",
|
||||
text: lastMerge.sourceText,
|
||||
ingredientReferences: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function moveTo(dest: string, source: number) {
|
||||
if (dest === "top") {
|
||||
instructionList.value.unshift(instructionList.value.splice(source, 1)[0]);
|
||||
}
|
||||
else {
|
||||
instructionList.value.push(instructionList.value.splice(source, 1)[0]);
|
||||
}
|
||||
}
|
||||
|
||||
function insert(dest: number) {
|
||||
instructionList.value.splice(dest, 0, { id: uuid4(), text: "", title: "", ingredientReferences: [] });
|
||||
}
|
||||
|
||||
const previewStates = ref<boolean[]>([]);
|
||||
|
||||
function togglePreviewState(index: number) {
|
||||
const temp = [...previewStates.value];
|
||||
temp[index] = !temp[index];
|
||||
previewStates.value = temp;
|
||||
}
|
||||
|
||||
function toggleCollapseSection(index: number) {
|
||||
const sectionSteps: number[] = [];
|
||||
|
||||
for (let i = index; i < instructionList.value.length; i++) {
|
||||
if (!(i === index) && hasSectionTitle(instructionList.value[i].title!)) {
|
||||
break;
|
||||
}
|
||||
else {
|
||||
sectionSteps.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
const allCollapsed = sectionSteps.every(idx => disabledSteps.value.includes(idx));
|
||||
|
||||
if (allCollapsed) {
|
||||
disabledSteps.value = disabledSteps.value.filter(idx => !sectionSteps.includes(idx));
|
||||
}
|
||||
else {
|
||||
disabledSteps.value = [...disabledSteps.value, ...sectionSteps];
|
||||
}
|
||||
}
|
||||
|
||||
const drag = ref(false);
|
||||
|
||||
// ===============================================================
|
||||
// Image Uploader
|
||||
const api = useUserApi();
|
||||
const { recipeAssetPath } = useStaticRoutes();
|
||||
|
||||
const loadingStates = ref<{ [key: number]: boolean }>({});
|
||||
|
||||
async function handleImageDrop(index: number, files: File[]) {
|
||||
if (!files) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the file is an image
|
||||
const file = files[0];
|
||||
if (!file || !file.type.startsWith("image/")) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadingStates.value[index] = true;
|
||||
|
||||
const { data } = await api.recipes.createAsset(props.recipe.slug, {
|
||||
name: file.name,
|
||||
icon: "mdi-file-image",
|
||||
file,
|
||||
extension: file.name.split(".").pop() || "",
|
||||
});
|
||||
|
||||
loadingStates.value[index] = false;
|
||||
|
||||
if (!data) {
|
||||
return; // TODO: Handle error
|
||||
}
|
||||
|
||||
emit("update:assets", [...assets.value, data]);
|
||||
const assetUrl = BASE_URL + recipeAssetPath(props.recipe.id, data.fileName as string);
|
||||
const text = `<img src="${assetUrl}" height="100%" width="100%"/>`;
|
||||
instructionList.value[index].text += text;
|
||||
}
|
||||
|
||||
function openImageUpload(index: number) {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = "image/*";
|
||||
input.onchange = async () => {
|
||||
if (input.files) {
|
||||
await handleImageDrop(index, Array.from(input.files));
|
||||
input.remove();
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
|
|
|
@ -60,7 +60,7 @@
|
|||
{{ $t('tool.required-tools') }}
|
||||
</v-card-title>
|
||||
<v-divider class="mx-2" />
|
||||
<v-card-text class="pt-0">
|
||||
<v-card-text>
|
||||
<RecipeOrganizerSelector
|
||||
v-model="recipe.tools"
|
||||
selector-type="tools"
|
||||
|
|
|
@ -141,6 +141,9 @@ export default defineNuxtComponent({
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
.text-center {
|
||||
font-size: smaller;
|
||||
}
|
||||
.time-card-flex {
|
||||
width: fit-content;
|
||||
}
|
||||
|
|
|
@ -69,7 +69,8 @@
|
|||
:style="maxHeight ? `max-height: ${maxHeight}; overflow-y: auto;` : ''"
|
||||
>
|
||||
<v-timeline
|
||||
:dense="$vuetify.display.smAndDown"
|
||||
:density="$vuetify.display.smAndDown ? ($vuetify.display.xs ? 'compact' : 'comfortable') : undefined"
|
||||
justify="center"
|
||||
class="timeline"
|
||||
>
|
||||
<RecipeTimelineItem
|
||||
|
@ -78,6 +79,7 @@
|
|||
:event="event"
|
||||
:recipe="recipes.get(event.recipeId)"
|
||||
:show-recipe-cards="showRecipeCards"
|
||||
:width="$vuetify.display.smAndDown ? '100%' : undefined"
|
||||
@update="updateTimelineEvent(index)"
|
||||
@delete="deleteTimelineEvent(index)"
|
||||
/>
|
||||
|
|
|
@ -125,6 +125,7 @@
|
|||
<v-alert
|
||||
type="info"
|
||||
:text="$t('search.no-results')"
|
||||
class="mb-0"
|
||||
/>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
|
|
@ -32,11 +32,11 @@
|
|||
<v-row>
|
||||
<v-col cols="9">
|
||||
<v-text-field
|
||||
v-model="generatedSignupLink"
|
||||
:label="$t('profile.invite-link')"
|
||||
type="text"
|
||||
readonly
|
||||
variant="filled"
|
||||
:value="generatedSignupLink"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
<div style="flex-basis: 500px">
|
||||
<strong> {{ $t("user.password-strength", { strength: pwStrength.strength.value }) }}</strong>
|
||||
<v-progress-linear
|
||||
:value="pwStrength.score.value"
|
||||
class="rounded-lg"
|
||||
v-model="pwStrength.score.value"
|
||||
rounded
|
||||
:color="pwStrength.color.value"
|
||||
height="15"
|
||||
/>
|
||||
|
@ -12,27 +12,11 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { usePasswordStrength } from "~/composables/use-passwords";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const asRef = toRef(props, "modelValue");
|
||||
const i18n = useI18n();
|
||||
const modelValue = defineModel<string>({ default: "" });
|
||||
const i18n = useI18n();
|
||||
|
||||
const pwStrength = usePasswordStrength(asRef, i18n);
|
||||
|
||||
return {
|
||||
pwStrength,
|
||||
};
|
||||
},
|
||||
});
|
||||
const pwStrength = usePasswordStrength(modelValue, i18n);
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
@click:append-inner="pwFields.togglePasswordShow"
|
||||
/>
|
||||
|
||||
<UserPasswordStrength :value="credentials.password1.value" />
|
||||
<UserPasswordStrength v-model="credentials.password1.value" />
|
||||
|
||||
<v-text-field
|
||||
v-model="credentials.password2.value"
|
||||
|
@ -86,9 +86,8 @@ import { usePasswordField } from "~/composables/use-passwords";
|
|||
import UserPasswordStrength from "~/components/Domain/User/UserPasswordStrength.vue";
|
||||
|
||||
const inputAttrs = {
|
||||
rounded: true,
|
||||
validateOnBlur: true,
|
||||
class: "rounded-lg pb-1",
|
||||
class: "pb-1",
|
||||
variant: "solo-filled" as any,
|
||||
};
|
||||
|
||||
|
|
|
@ -105,7 +105,7 @@
|
|||
</v-list-item>
|
||||
</template>
|
||||
</AppSidebar>
|
||||
<v-main class="pt-16">
|
||||
<v-main class="pt-12">
|
||||
<v-scroll-x-transition>
|
||||
<div>
|
||||
<NuxtPage />
|
||||
|
@ -119,8 +119,8 @@
|
|||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
import type { SideBarLink } from "~/types/application-types";
|
||||
import { useAppInfo } from "~/composables/api";
|
||||
import { useCookbooks, usePublicCookbooks } from "~/composables/use-group-cookbooks";
|
||||
import { useCookbookPreferences } from "~/composables/use-users/preferences";
|
||||
import { useCookbookStore, usePublicCookbookStore } from "~/composables/store/use-cookbook-store";
|
||||
import { useHouseholdStore, usePublicHouseholdStore } from "~/composables/store/use-household-store";
|
||||
import { useToggleDarkMode } from "~/composables/use-utils";
|
||||
import type { ReadCookBook } from "~/lib/api/types/cookbook";
|
||||
|
@ -136,9 +136,15 @@ export default defineNuxtComponent({
|
|||
const isAdmin = computed(() => $auth.user.value?.admin);
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
||||
const { cookbooks } = isOwnGroup.value ? useCookbooks() : usePublicCookbooks(groupSlug.value || "");
|
||||
|
||||
const cookbookPreferences = useCookbookPreferences();
|
||||
const { store: cookbooks, actions: cookbooksActions } = isOwnGroup.value ? useCookbookStore() : usePublicCookbookStore(groupSlug.value || "");
|
||||
onMounted(() => {
|
||||
if (!cookbooks.value.length) {
|
||||
cookbooksActions.refresh();
|
||||
}
|
||||
});
|
||||
|
||||
const { store: households } = isOwnGroup.value ? useHouseholdStore() : usePublicHouseholdStore(groupSlug.value || "");
|
||||
|
||||
const householdsById = computed(() => {
|
||||
|
@ -172,10 +178,6 @@ export default defineNuxtComponent({
|
|||
|
||||
const currentUserHouseholdId = computed(() => $auth.user.value?.householdId);
|
||||
const cookbookLinks = computed<SideBarLink[]>(() => {
|
||||
if (!cookbooks.value || !households.value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const sortedCookbooks = [...cookbooks.value].sort((a, b) => (a.position || 0) - (b.position || 0));
|
||||
|
||||
const ownLinks: SideBarLink[] = [];
|
||||
|
@ -229,7 +231,7 @@ export default defineNuxtComponent({
|
|||
{
|
||||
insertDivider: false,
|
||||
icon: $globals.icons.fileImage,
|
||||
title: i18n.t("recipe.create-from-image"),
|
||||
title: i18n.t("recipe.create-from-images"),
|
||||
subtitle: i18n.t("recipe.create-recipe-from-an-image"),
|
||||
to: `/g/${groupSlug.value}/r/create/image`,
|
||||
restricted: true,
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
<template>
|
||||
<v-form ref="file">
|
||||
<v-form ref="files">
|
||||
<input
|
||||
ref="uploader"
|
||||
class="d-none"
|
||||
type="file"
|
||||
:accept="accept"
|
||||
:multiple="multiple"
|
||||
@change="onFileChanged"
|
||||
>
|
||||
<slot v-bind="{ isSelecting, onButtonClick }">
|
||||
|
@ -72,9 +73,13 @@ export default defineNuxtComponent({
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
const file = ref<File | null>(null);
|
||||
const files = ref<File[]>([]);
|
||||
const uploader = ref<HTMLInputElement | null>(null);
|
||||
const isSelecting = ref(false);
|
||||
|
||||
|
@ -86,35 +91,52 @@ export default defineNuxtComponent({
|
|||
|
||||
const api = useUserApi();
|
||||
async function upload() {
|
||||
if (file.value != null) {
|
||||
isSelecting.value = true;
|
||||
|
||||
if (!props.post) {
|
||||
context.emit(UPLOAD_EVENT, file.value);
|
||||
isSelecting.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append(props.fileName, file.value);
|
||||
try {
|
||||
const response = await api.upload.file(props.url, formData);
|
||||
if (response) {
|
||||
context.emit(UPLOAD_EVENT, response);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
context.emit(UPLOAD_EVENT, null);
|
||||
}
|
||||
isSelecting.value = false;
|
||||
if (files.value.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
isSelecting.value = true;
|
||||
|
||||
if (!props.post) {
|
||||
// NOTE: To preserve behaviour for other parents of this component,
|
||||
// we emit a single File if !props.multiple.
|
||||
context.emit(UPLOAD_EVENT, props.multiple ? files.value : files.value[0]);
|
||||
isSelecting.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// WARN: My change is only for !props.post.
|
||||
// I have not added support for multiple files in the API.
|
||||
// Existing call-sites never passed the `multiple` prop,
|
||||
// so this case will only be hit if the prop is set to true.
|
||||
if (props.multiple && files.value.length > 1) {
|
||||
console.warn("Multiple file uploads are not supported by the API.");
|
||||
return;
|
||||
}
|
||||
|
||||
const file = files.value[0];
|
||||
const formData = new FormData();
|
||||
formData.append(props.fileName, file);
|
||||
|
||||
try {
|
||||
const response = await api.upload.file(props.url, formData);
|
||||
if (response) {
|
||||
context.emit(UPLOAD_EVENT, response);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
context.emit(UPLOAD_EVENT, null);
|
||||
}
|
||||
|
||||
isSelecting.value = false;
|
||||
}
|
||||
|
||||
function onFileChanged(e: Event) {
|
||||
const target = e.target as HTMLInputElement;
|
||||
if (target.files !== null && target.files.length > 0 && file.value !== null) {
|
||||
file.value = target.files[0];
|
||||
|
||||
if (target.files !== null && target.files.length > 0) {
|
||||
files.value = Array.from(target.files);
|
||||
upload();
|
||||
}
|
||||
}
|
||||
|
@ -132,7 +154,7 @@ export default defineNuxtComponent({
|
|||
}
|
||||
|
||||
return {
|
||||
file,
|
||||
files,
|
||||
uploader,
|
||||
isSelecting,
|
||||
effIcon,
|
||||
|
|
|
@ -10,7 +10,6 @@
|
|||
<v-col
|
||||
v-for="(inputField, index) in items"
|
||||
:key="index"
|
||||
class="py-0"
|
||||
cols="12"
|
||||
sm="12"
|
||||
>
|
||||
|
@ -75,7 +74,6 @@
|
|||
:disabled="(inputField.disableUpdate && updateMode) || (!updateMode && inputField.disableCreate) || (disabledFields && disabledFields.includes(inputField.varName))"
|
||||
variant="solo-filled"
|
||||
flat
|
||||
class="rounded-lg"
|
||||
rows="3"
|
||||
auto-grow
|
||||
density="comfortable"
|
||||
|
@ -95,7 +93,6 @@
|
|||
:disabled="(inputField.disableUpdate && updateMode) || (!updateMode && inputField.disableCreate) || (disabledFields && disabledFields.includes(inputField.varName))"
|
||||
variant="solo-filled"
|
||||
flat
|
||||
class="rounded-lg"
|
||||
:prepend-icon="inputField.icons ? modelValue[inputField.varName] : null"
|
||||
:label="inputField.label"
|
||||
:name="inputField.varName"
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
<v-card-title class="text-h5 pl-0 py-0" style="font-weight: normal;">
|
||||
<v-icon
|
||||
v-if="icon"
|
||||
size="small"
|
||||
start
|
||||
>
|
||||
{{ icon }}
|
||||
|
@ -24,7 +25,7 @@
|
|||
<slot />
|
||||
</p>
|
||||
</v-card-text>
|
||||
<v-divider class="mb-3" />
|
||||
<v-divider class="mt-1 mb-3" />
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -29,7 +29,6 @@
|
|||
<v-toolbar-title class="headline">
|
||||
{{ title }}
|
||||
</v-toolbar-title>
|
||||
<v-spacer />
|
||||
</v-toolbar>
|
||||
<v-progress-linear
|
||||
v-if="loading"
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
style="margin-bottom: 4rem"
|
||||
dark
|
||||
>
|
||||
<v-toolbar-title class="headline text-h4">
|
||||
<v-toolbar-title class="headline text-h4 text-center mx-0">
|
||||
Mealie
|
||||
</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
|
|
|
@ -1,48 +1,58 @@
|
|||
<template>
|
||||
<v-container class="pa-0">
|
||||
<v-row no-gutters>
|
||||
<v-col
|
||||
cols="8"
|
||||
align-self="center"
|
||||
>
|
||||
<Cropper
|
||||
ref="cropper"
|
||||
class="cropper"
|
||||
:src="img"
|
||||
:default-size="defaultSize"
|
||||
:style="`height: ${cropperHeight}; width: ${cropperWidth};`"
|
||||
<v-card class="ma-0 pt-2" :elevation="4">
|
||||
<v-card-text>
|
||||
<!-- Controls Row (Menu) -->
|
||||
<v-row class="mb-2 mx-1">
|
||||
<v-btn
|
||||
color="error"
|
||||
:icon="$globals.icons.delete"
|
||||
:disabled="submitted"
|
||||
@click="$emit('delete')"
|
||||
/>
|
||||
</v-col>
|
||||
<v-spacer />
|
||||
<v-col
|
||||
cols="2"
|
||||
align-self="center"
|
||||
>
|
||||
<v-container class="pa-0 mx-0">
|
||||
<v-row
|
||||
v-for="(row, keyRow) in controls"
|
||||
:key="keyRow"
|
||||
>
|
||||
<v-col
|
||||
v-for="(control, keyControl) in row"
|
||||
:key="keyControl"
|
||||
:cols="12 / row.length"
|
||||
class="py-2 mx-0"
|
||||
style="display: flex; align-items: center; justify-content: center;"
|
||||
>
|
||||
<v-btn
|
||||
icon
|
||||
:color="control.color"
|
||||
@click="control.callback()"
|
||||
>
|
||||
<v-icon> {{ control.icon }} </v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
v-if="changed"
|
||||
class="mr-2"
|
||||
color="success"
|
||||
:icon="$globals.icons.save"
|
||||
:disabled="submitted"
|
||||
@click="() => save()"
|
||||
/>
|
||||
<v-menu offset-y :close-on-content-click="false" location="bottom center">
|
||||
<template #activator="{ props }">
|
||||
<v-btn color="info" v-bind="props" :icon="$globals.icons.edit" :disabled="submitted" />
|
||||
</template>
|
||||
<v-list class="mt-1">
|
||||
<template v-for="(row, keyRow) in controls" :key="keyRow">
|
||||
<v-list-item-group>
|
||||
<v-list-item
|
||||
v-for="(control, keyControl) in row"
|
||||
:key="keyControl"
|
||||
:disabled="submitted"
|
||||
@click="control.callback()"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon :color="control.color" :icon="control.icon" />
|
||||
</v-list-item-icon>
|
||||
</v-list-item>
|
||||
</v-list-item-group>
|
||||
</template>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-row>
|
||||
|
||||
<!-- Image Row -->
|
||||
<Cropper
|
||||
ref="cropper"
|
||||
class="cropper"
|
||||
:src="img"
|
||||
:default-size="defaultSize"
|
||||
:style="`height: ${cropperHeight}; width: ${cropperWidth};`"
|
||||
@change="changed = changed + 1"
|
||||
@ready="changed = -1"
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -64,10 +74,15 @@ export default defineNuxtComponent({
|
|||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
submitted: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ["save"],
|
||||
emits: ["save", "delete"],
|
||||
setup(_, context) {
|
||||
const cropper = ref<Cropper>();
|
||||
const cropper = ref<any>();
|
||||
const changed = ref(0);
|
||||
const { $globals } = useNuxtApp();
|
||||
|
||||
interface Control {
|
||||
|
@ -101,41 +116,32 @@ export default defineNuxtComponent({
|
|||
callback: () => rotate(90),
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
color: "success",
|
||||
icon: $globals.icons.save,
|
||||
callback: () => save(),
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
function flip(hortizontal: boolean, vertical?: boolean) {
|
||||
if (!cropper.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
cropper.value.flip(hortizontal, vertical);
|
||||
changed.value = changed.value + 1;
|
||||
}
|
||||
|
||||
function rotate(angle: number) {
|
||||
if (!cropper.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
cropper.value.rotate(angle);
|
||||
changed.value = changed.value + 1;
|
||||
}
|
||||
|
||||
function save() {
|
||||
if (!cropper.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { canvas } = cropper.value.getResult();
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
context.emit("save", blob);
|
||||
|
@ -149,11 +155,11 @@ export default defineNuxtComponent({
|
|||
flip,
|
||||
rotate,
|
||||
save,
|
||||
changed,
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
// @ts-expect-error https://advanced-cropper.github.io/vue-advanced-cropper/guides/advanced-recipes.html
|
||||
defaultSize({ imageSize, visibleArea }) {
|
||||
return {
|
||||
width: (visibleArea || imageSize).width,
|
||||
|
|
|
@ -10,7 +10,6 @@
|
|||
:min="min"
|
||||
:max="max"
|
||||
type="number"
|
||||
class="rounded-xl"
|
||||
size="small"
|
||||
variant="plain"
|
||||
/>
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
<template>
|
||||
<MDC
|
||||
:value="value"
|
||||
tag="article"
|
||||
/>
|
||||
<!-- eslint-disable-next-line vue/no-v-html is safe here because all HTML is sanitized with DOMPurify in setup() -->
|
||||
<div v-html="value" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import DOMPurify from "isomorphic-dompurify";
|
||||
import { marked } from "marked";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
|
@ -40,7 +39,8 @@ export default defineNuxtComponent({
|
|||
}
|
||||
|
||||
const value = computed(() => {
|
||||
return sanitizeMarkdown(props.source) || "";
|
||||
const rawHtml = marked.parse(props.source || "", { async: false });
|
||||
return sanitizeMarkdown(rawHtml);
|
||||
});
|
||||
|
||||
return {
|
||||
|
@ -56,7 +56,8 @@ export default defineNuxtComponent({
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(th, td) {
|
||||
:deep(th),
|
||||
:deep(td) {
|
||||
border: 1px solid;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
|
@ -65,4 +66,10 @@ export default defineNuxtComponent({
|
|||
:deep(th) {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
:deep(ul),
|
||||
:deep(ol) {
|
||||
margin: 8px 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
import type { AxiosInstance, AxiosResponse } from "axios";
|
||||
import type { AxiosInstance, AxiosResponse, AxiosRequestConfig } from "axios";
|
||||
import type { ApiRequestInstance, RequestResponse } from "~/lib/api/types/non-generated";
|
||||
import { AdminAPI, PublicApi, UserApi } from "~/lib/api";
|
||||
import { PublicExploreApi } from "~/lib/api/client-public";
|
||||
|
||||
const request = {
|
||||
async safe<T, U>(
|
||||
funcCall: (url: string, data: U) => Promise<AxiosResponse<T>>,
|
||||
funcCall: (url: string, data: U, config?: AxiosRequestConfig) => Promise<AxiosResponse<T>>,
|
||||
url: string,
|
||||
data: U,
|
||||
config?: AxiosRequestConfig,
|
||||
): Promise<RequestResponse<T>> {
|
||||
let error = null;
|
||||
const response = await funcCall(url, data).catch(function (e) {
|
||||
const response = await funcCall(url, data, config).catch(function (e) {
|
||||
console.log(e);
|
||||
// Insert Generic Error Handling Here
|
||||
error = e;
|
||||
|
@ -22,9 +23,9 @@ const request = {
|
|||
|
||||
function getRequests(axiosInstance: AxiosInstance): ApiRequestInstance {
|
||||
return {
|
||||
async get<T>(url: string, params = {}): Promise<RequestResponse<T>> {
|
||||
async get<T>(url: string, params = {}, config?: AxiosRequestConfig): Promise<RequestResponse<T>> {
|
||||
let error = null;
|
||||
const response = await axiosInstance.get<T>(url, params).catch((e) => {
|
||||
const response = await axiosInstance.get<T>(url, { ...config, params }).catch((e) => {
|
||||
error = e;
|
||||
});
|
||||
if (response != null) {
|
||||
|
@ -33,20 +34,20 @@ function getRequests(axiosInstance: AxiosInstance): ApiRequestInstance {
|
|||
return { response: null, error, data: null };
|
||||
},
|
||||
|
||||
async post<T, U>(url: string, data: U) {
|
||||
return await request.safe<T, U>(axiosInstance.post, url, data);
|
||||
async post<T, U>(url: string, data: U, config?: AxiosRequestConfig) {
|
||||
return await request.safe<T, U>(axiosInstance.post, url, data, config);
|
||||
},
|
||||
|
||||
async put<T, U = T>(url: string, data: U) {
|
||||
return await request.safe<T, U>(axiosInstance.put, url, data);
|
||||
async put<T, U = T>(url: string, data: U, config?: AxiosRequestConfig) {
|
||||
return await request.safe<T, U>(axiosInstance.put, url, data, config);
|
||||
},
|
||||
|
||||
async patch<T, U = Partial<T>>(url: string, data: U) {
|
||||
return await request.safe<T, U>(axiosInstance.patch, url, data);
|
||||
async patch<T, U = Partial<T>>(url: string, data: U, config?: AxiosRequestConfig) {
|
||||
return await request.safe<T, U>(axiosInstance.patch, url, data, config);
|
||||
},
|
||||
|
||||
async delete<T>(url: string) {
|
||||
return await request.safe<T, undefined>(axiosInstance.delete, url, undefined);
|
||||
async delete<T>(url: string, config?: AxiosRequestConfig) {
|
||||
return await request.safe<T, undefined>(axiosInstance.delete, url, undefined, config);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
17
frontend/composables/store/use-cookbook-store.ts
Normal file
17
frontend/composables/store/use-cookbook-store.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { useReadOnlyStore, useStore } from "../partials/use-store-factory";
|
||||
import type { RecipeCookBook } from "~/lib/api/types/cookbook";
|
||||
import { usePublicExploreApi, useUserApi } from "~/composables/api";
|
||||
|
||||
const store: Ref<RecipeCookBook[]> = ref([]);
|
||||
const loading = ref(false);
|
||||
const publicLoading = ref(false);
|
||||
|
||||
export const useCookbookStore = function () {
|
||||
const api = useUserApi();
|
||||
return useStore<RecipeCookBook>(store, loading, api.cookbooks);
|
||||
};
|
||||
|
||||
export const usePublicCookbookStore = function (groupSlug: string) {
|
||||
const api = usePublicExploreApi(groupSlug).explore;
|
||||
return useReadOnlyStore<RecipeCookBook>(store, publicLoading, api.cookbooks);
|
||||
};
|
|
@ -1,10 +1,6 @@
|
|||
import { useAsyncKey } from "./use-utils";
|
||||
import { usePublicExploreApi } from "./api/api-client";
|
||||
import { useHouseholdSelf } from "./use-households";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import type { ReadCookBook, UpdateCookBook } from "~/lib/api/types/cookbook";
|
||||
|
||||
let cookbookStore: Ref<ReadCookBook[] | null> | null = null;
|
||||
|
||||
export const useCookbook = function (publicGroupSlug: string | null = null) {
|
||||
function getOne(id: string | number) {
|
||||
|
@ -22,149 +18,3 @@ export const useCookbook = function (publicGroupSlug: string | null = null) {
|
|||
|
||||
return { getOne };
|
||||
};
|
||||
|
||||
export const usePublicCookbooks = function (groupSlug: string) {
|
||||
const api = usePublicExploreApi(groupSlug).explore;
|
||||
const loading = ref(false);
|
||||
|
||||
const actions = {
|
||||
getAll() {
|
||||
loading.value = true;
|
||||
const { data: units } = useAsyncData(useAsyncKey(), async () => {
|
||||
const { data } = await api.cookbooks.getAll(1, -1, { orderBy: "position", orderDirection: "asc" });
|
||||
|
||||
if (data) {
|
||||
return data.items;
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
loading.value = false;
|
||||
return units;
|
||||
},
|
||||
async refreshAll() {
|
||||
loading.value = true;
|
||||
const { data } = await api.cookbooks.getAll(1, -1, { orderBy: "position", orderDirection: "asc" });
|
||||
|
||||
if (data && data.items && cookbookStore) {
|
||||
cookbookStore.value = data.items;
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
},
|
||||
flushStore() {
|
||||
cookbookStore = null;
|
||||
},
|
||||
};
|
||||
|
||||
if (!cookbookStore) {
|
||||
cookbookStore = actions.getAll();
|
||||
}
|
||||
|
||||
return { cookbooks: cookbookStore, actions };
|
||||
};
|
||||
|
||||
export const useCookbooks = function () {
|
||||
const api = useUserApi();
|
||||
const { household } = useHouseholdSelf();
|
||||
const loading = ref(false);
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
const actions = {
|
||||
getAll() {
|
||||
loading.value = true;
|
||||
const { data: units } = useAsyncData(useAsyncKey(), async () => {
|
||||
const { data } = await api.cookbooks.getAll(1, -1, { orderBy: "position", orderDirection: "asc" });
|
||||
|
||||
if (data) {
|
||||
return data.items;
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
loading.value = false;
|
||||
return units;
|
||||
},
|
||||
async refreshAll() {
|
||||
loading.value = true;
|
||||
const { data } = await api.cookbooks.getAll(1, -1, { orderBy: "position", orderDirection: "asc" });
|
||||
|
||||
if (data && data.items && cookbookStore) {
|
||||
cookbookStore.value = data.items;
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
},
|
||||
async createOne(name: string | null = null) {
|
||||
loading.value = true;
|
||||
const { data } = await api.cookbooks.createOne({
|
||||
name: name || i18n.t("cookbook.household-cookbook-name", [household.value?.name || "", String((cookbookStore?.value?.length ?? 0) + 1)]) as string,
|
||||
position: (cookbookStore?.value?.length ?? 0) + 1,
|
||||
queryFilterString: "",
|
||||
});
|
||||
if (data && cookbookStore?.value) {
|
||||
cookbookStore.value.push(data);
|
||||
}
|
||||
else {
|
||||
this.refreshAll();
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
return data;
|
||||
},
|
||||
async updateOne(updateData: UpdateCookBook) {
|
||||
if (!updateData.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
const { data } = await api.cookbooks.updateOne(updateData.id, updateData);
|
||||
if (data && cookbookStore?.value) {
|
||||
this.refreshAll();
|
||||
}
|
||||
loading.value = false;
|
||||
return data;
|
||||
},
|
||||
|
||||
async updateOrder(cookbooks: ReadCookBook[]) {
|
||||
if (!cookbooks?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
|
||||
cookbooks.forEach((element, index) => {
|
||||
element.position = index + 1;
|
||||
});
|
||||
|
||||
const { data } = await api.cookbooks.updateAll(cookbooks);
|
||||
|
||||
if (data && cookbookStore?.value) {
|
||||
this.refreshAll();
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
},
|
||||
async deleteOne(id: string | number) {
|
||||
loading.value = true;
|
||||
const { data } = await api.cookbooks.deleteOne(id);
|
||||
if (data && cookbookStore?.value) {
|
||||
this.refreshAll();
|
||||
}
|
||||
},
|
||||
flushStore() {
|
||||
cookbookStore = null;
|
||||
},
|
||||
};
|
||||
|
||||
if (!cookbookStore) {
|
||||
cookbookStore = actions.getAll();
|
||||
}
|
||||
|
||||
return { cookbooks: cookbookStore, actions };
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useUserApi } from "~/composables/api";
|
||||
import { useAdminApi } from "~/composables/api";
|
||||
import type { UserIn, UserOut } from "~/lib/api/types/user";
|
||||
|
||||
/*
|
||||
|
@ -8,7 +8,7 @@ to control whether the object is substantiated... but some of the others rely on
|
|||
*/
|
||||
|
||||
export const useAllUsers = function () {
|
||||
const api = useUserApi();
|
||||
const api = useAdminApi();
|
||||
const asyncKey = String(Date.now());
|
||||
const { data: users, refresh: refreshAllUsers } = useLazyAsyncData(asyncKey, async () => {
|
||||
const { data } = await api.users.getAll();
|
||||
|
@ -24,7 +24,7 @@ export const useAllUsers = function () {
|
|||
};
|
||||
|
||||
export const useUser = function (refreshFunc: CallableFunction | null = null) {
|
||||
const api = useUserApi();
|
||||
const api = useAdminApi();
|
||||
const loading = ref(false);
|
||||
|
||||
function getUser(id: string) {
|
||||
|
|
|
@ -596,12 +596,13 @@
|
|||
"create-recipe-description": "Create a new recipe from scratch.",
|
||||
"create-recipes": "Create Recipes",
|
||||
"import-with-zip": "Import with .zip",
|
||||
"create-recipe-from-an-image": "Create Recipe from an Image",
|
||||
"create-recipe-from-an-image-description": "Create a recipe by uploading an image of it. Mealie will attempt to extract the text from the image using AI and create a recipe from it.",
|
||||
"create-recipe-from-an-image": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "Create a recipe by uploading images of the recipe text. Mealie will attempt to extract the text from the images using AI and create a new recipe from it.",
|
||||
"crop-and-rotate-the-image": "Crop and rotate the image so that only the text is visible, and it's in the correct orientation.",
|
||||
"create-from-image": "Create from Image",
|
||||
"create-from-images": "Create from Images",
|
||||
"should-translate-description": "Translate the recipe into my language",
|
||||
"please-wait-image-procesing": "Please wait, the image is processing. This may take some time.",
|
||||
"please-wait-images-processing": "Please wait, the images are processing. This may take some time.",
|
||||
"bulk-url-import": "Bulk URL Import",
|
||||
"debug-scraper": "Debug Scraper",
|
||||
"create-a-recipe-by-providing-the-name-all-recipes-must-have-unique-names": "Create a recipe by providing the name. All recipes must have unique names.",
|
||||
|
@ -660,7 +661,10 @@
|
|||
"no-food": "No Food"
|
||||
},
|
||||
"reset-servings-count": "Reset Servings Count",
|
||||
"not-linked-ingredients": "Additional Ingredients"
|
||||
"not-linked-ingredients": "Additional Ingredients",
|
||||
"upload-another-image": "Upload another image",
|
||||
"upload-images": "Upload images",
|
||||
"upload-more-images": "Upload more images"
|
||||
},
|
||||
"recipe-finder": {
|
||||
"recipe-finder": "Recipe Finder",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { AxiosResponse } from "axios";
|
||||
import type { AxiosRequestConfig, AxiosResponse } from "axios";
|
||||
|
||||
export type NoUndefinedField<T> = { [P in keyof T]-?: NoUndefinedField<NonNullable<T[P]>> };
|
||||
|
||||
|
@ -9,11 +9,11 @@ export interface RequestResponse<T> {
|
|||
}
|
||||
|
||||
export interface ApiRequestInstance {
|
||||
get<T>(url: string, data?: unknown): Promise<RequestResponse<T>>;
|
||||
post<T>(url: string, data: unknown): Promise<RequestResponse<T>>;
|
||||
put<T, U = T>(url: string, data: U): Promise<RequestResponse<T>>;
|
||||
patch<T, U = Partial<T>>(url: string, data: U): Promise<RequestResponse<T>>;
|
||||
delete<T>(url: string): Promise<RequestResponse<T>>;
|
||||
get<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<RequestResponse<T>>;
|
||||
post<T>(url: string, data: unknown, config?: AxiosRequestConfig): Promise<RequestResponse<T>>;
|
||||
put<T, U = T>(url: string, data: U, config?: AxiosRequestConfig): Promise<RequestResponse<T>>;
|
||||
patch<T, U = Partial<T>>(url: string, data: U, config?: AxiosRequestConfig): Promise<RequestResponse<T>>;
|
||||
delete<T>(url: string, config?: AxiosRequestConfig): Promise<RequestResponse<T>>;
|
||||
}
|
||||
|
||||
export interface PaginationData<T> {
|
||||
|
|
|
@ -157,17 +157,19 @@ export class RecipeAPI extends BaseCRUDAPI<CreateRecipe, Recipe, Recipe> {
|
|||
return await this.requests.post<string>(routes.recipesCreateUrlBulk, payload);
|
||||
}
|
||||
|
||||
async createOneFromImage(fileObject: Blob | File, fileName: string, translateLanguage: string | null = null) {
|
||||
async createOneFromImages(fileObjects: (Blob | File)[], translateLanguage: string | null = null) {
|
||||
const formData = new FormData();
|
||||
formData.append("images", fileObject);
|
||||
formData.append("extension", fileName.split(".").pop() ?? "");
|
||||
|
||||
fileObjects.forEach((file) => {
|
||||
formData.append("images", file);
|
||||
});
|
||||
|
||||
let apiRoute = routes.recipesCreateFromImage;
|
||||
if (translateLanguage) {
|
||||
apiRoute = `${apiRoute}?translateLanguage=${translateLanguage}`;
|
||||
}
|
||||
|
||||
return await this.requests.post<string>(apiRoute, formData);
|
||||
return await this.requests.post<string>(apiRoute, formData, { timeout: 120000 });
|
||||
}
|
||||
|
||||
async parseIngredients(parser: Parser, ingredients: Array<string>) {
|
||||
|
|
|
@ -150,6 +150,7 @@ import {
|
|||
mdiCodeTags,
|
||||
mdiKnife,
|
||||
mdiCookie,
|
||||
mdiBellPlus,
|
||||
} from "@mdi/js";
|
||||
|
||||
export const icons = {
|
||||
|
@ -174,6 +175,7 @@ export const icons = {
|
|||
arrowUpDown: mdiDrag,
|
||||
backupRestore: mdiBackupRestore,
|
||||
bellAlert: mdiBellAlert,
|
||||
bellPlus: mdiBellPlus,
|
||||
broom: mdiBroom,
|
||||
calendar: mdiCalendar,
|
||||
calendarMinus: mdiCalendarMinus,
|
||||
|
|
10
frontend/middleware/pwa-share-target-redirect.global.ts
Normal file
10
frontend/middleware/pwa-share-target-redirect.global.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
export default defineNuxtRouteMiddleware((to) => {
|
||||
if (to.path === "/r/create/url") {
|
||||
const { user } = useMealieAuth();
|
||||
const groupSlug = user.value?.groupSlug;
|
||||
if (!groupSlug) {
|
||||
return navigateTo("/login", { redirectCode: 301 });
|
||||
}
|
||||
return navigateTo(`/g/${groupSlug}${to.fullPath}`, { redirectCode: 301 });
|
||||
}
|
||||
});
|
|
@ -13,7 +13,6 @@ export default defineNuxtConfig({
|
|||
"@sidebase/nuxt-auth",
|
||||
"@nuxtjs/google-fonts",
|
||||
"vuetify-nuxt-module",
|
||||
"@nuxtjs/mdc",
|
||||
"@nuxt/eslint",
|
||||
],
|
||||
ssr: false,
|
||||
|
|
|
@ -21,7 +21,6 @@
|
|||
"@mdi/js": "^7.4.47",
|
||||
"@nuxt/eslint": "1.2.0",
|
||||
"@nuxtjs/i18n": "^9.2.1",
|
||||
"@nuxtjs/mdc": "0.14.0",
|
||||
"@nuxtjs/proxy": "^2.1.0",
|
||||
"@sidebase/nuxt-auth": "0.10.0",
|
||||
"@vite-pwa/nuxt": "0.10.6",
|
||||
|
@ -31,6 +30,7 @@
|
|||
"fuse.js": "^7.1.0",
|
||||
"isomorphic-dompurify": "^2.22.0",
|
||||
"json-editor-vue": "^0.18.1",
|
||||
"marked": "^15.0.12",
|
||||
"next-auth": "~4.21.1",
|
||||
"nuxt": "^3.15.4",
|
||||
"typescript": "5.3",
|
||||
|
@ -41,7 +41,6 @@
|
|||
"vuetify-nuxt-module": "0.18.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "^7.26.8",
|
||||
"@nuxt/types": "^2.18.1",
|
||||
"@nuxtjs/eslint-config-typescript": "^12.1.0",
|
||||
"@nuxtjs/eslint-module": "^4.1.0",
|
||||
|
@ -54,7 +53,6 @@
|
|||
"eslint-plugin-nuxt": "^4.0.0",
|
||||
"eslint-plugin-prettier": "^5.2.3",
|
||||
"eslint-plugin-vue": "^9.32.0",
|
||||
"eslint-plugin-vuetify": "^2.5.1",
|
||||
"lint-staged": "^15.4.3",
|
||||
"prettier": "^3.5.2",
|
||||
"sass-embedded": "^1.85.1",
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
<section>
|
||||
<BaseCardSectionTitle class="pb-0" :icon="$globals.icons.wrench" :title="$t('admin.maintenance.summary-title')" />
|
||||
<div class="mb-6 ml-2 d-flex" style="gap: 0.3rem">
|
||||
<div class="mb-6 d-flex" style="gap: 0.3rem">
|
||||
<BaseButton color="info" @click="getSummary">
|
||||
<template #icon>
|
||||
{{ $globals.icons.tools }}
|
||||
|
@ -40,7 +40,7 @@
|
|||
{{ $t("admin.maintenance.button-label-open-details") }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
<v-card class="ma-2" :loading="state.fetchingInfo">
|
||||
<v-card class="" :loading="state.fetchingInfo">
|
||||
<template v-for="(value, idx) in info" :key="`item-${idx}`">
|
||||
<v-list-item>
|
||||
<v-list-item-title class="py-2">
|
||||
|
@ -67,21 +67,23 @@
|
|||
</template>
|
||||
</i18n-t>
|
||||
</BaseCardSectionTitle>
|
||||
<v-card class="ma-2" :loading="state.actionLoading">
|
||||
<v-card class="ma-0" flat :loading="state.actionLoading">
|
||||
<template v-for="(action, idx) in actions" :key="`item-${idx}`">
|
||||
<v-list-item class="py-1">
|
||||
<v-list-item class="py-2 px-0">
|
||||
<v-list-item-title>
|
||||
<div>{{ action.name }}</div>
|
||||
<v-list-item-subtitle class="wrap-word">
|
||||
{{ action.subtitle }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-title>
|
||||
<BaseButton color="info" @click="action.handler">
|
||||
<template #append>
|
||||
<BaseButton color="info" @click="action.handler">
|
||||
<template #icon>
|
||||
{{ $globals.icons.robot }}
|
||||
</template>
|
||||
{{ $t("general.run") }}
|
||||
</BaseButton>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-divider class="mx-2" />
|
||||
</template>
|
||||
|
|
|
@ -33,7 +33,6 @@
|
|||
:items="groups"
|
||||
variant="solo-filled"
|
||||
flat
|
||||
class="rounded-lg"
|
||||
item-title="name"
|
||||
item-value="id"
|
||||
:return-object="false"
|
||||
|
|
|
@ -12,8 +12,6 @@
|
|||
v-if="groups"
|
||||
v-model="createHouseholdForm.data.groupId"
|
||||
:items="groups"
|
||||
rounded
|
||||
class="rounded-lg"
|
||||
item-title="name"
|
||||
item-value="id"
|
||||
:return-object="false"
|
||||
|
|
|
@ -24,8 +24,6 @@
|
|||
v-if="groups"
|
||||
v-model="selectedGroupId"
|
||||
:items="groups"
|
||||
rounded
|
||||
class="rounded-lg"
|
||||
item-title="name"
|
||||
item-value="id"
|
||||
:return-object="false"
|
||||
|
@ -37,8 +35,6 @@
|
|||
v-model="newUserData.household"
|
||||
:disabled="!selectedGroupId"
|
||||
:items="households"
|
||||
rounded
|
||||
class="rounded-lg"
|
||||
item-title="name"
|
||||
item-value="name"
|
||||
:return-object="false"
|
||||
|
|
|
@ -17,10 +17,10 @@
|
|||
<v-form @submit.prevent="requestLink()">
|
||||
<v-text-field
|
||||
v-model="email"
|
||||
variant="filled"
|
||||
rounded
|
||||
:prepend-inner-icon="$globals.icons.email"
|
||||
variant="solo-filled"
|
||||
flat
|
||||
autofocus
|
||||
class="rounded-lg"
|
||||
name="login"
|
||||
:label="$t('user.email')"
|
||||
type="text"
|
||||
|
|
|
@ -139,10 +139,10 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { VueDraggable } from "vue-draggable-plus";
|
||||
import { useCookbooks } from "@/composables/use-group-cookbooks";
|
||||
import { useCookbookStore } from "~/composables/store/use-cookbook-store";
|
||||
import { useHouseholdSelf } from "@/composables/use-households";
|
||||
import CookbookEditor from "~/components/Domain/Cookbook/CookbookEditor.vue";
|
||||
import type { ReadCookBook } from "~/lib/api/types/cookbook";
|
||||
import type { CreateCookBook, ReadCookBook } from "~/lib/api/types/cookbook";
|
||||
import { useCookbookPreferences } from "~/composables/use-users/preferences";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
|
@ -162,7 +162,7 @@ export default defineNuxtComponent({
|
|||
});
|
||||
|
||||
const $auth = useMealieAuth();
|
||||
const { cookbooks: allCookbooks, actions } = useCookbooks();
|
||||
const { store: allCookbooks, actions } = useCookbookStore();
|
||||
|
||||
// Make a local reactive copy of myCookbooks
|
||||
const myCookbooks = ref<ReadCookBook[]>([]);
|
||||
|
@ -188,7 +188,9 @@ export default defineNuxtComponent({
|
|||
household.value?.name || "",
|
||||
String((myCookbooks.value?.length ?? 0) + 1),
|
||||
]) as string;
|
||||
await actions.createOne(name).then((cookbook) => {
|
||||
|
||||
const data = { name } as CreateCookBook;
|
||||
await actions.createOne(data).then((cookbook) => {
|
||||
if (!cookbook) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -80,7 +80,7 @@ export default defineNuxtComponent({
|
|||
},
|
||||
{
|
||||
icon: $globals.icons.fileImage,
|
||||
text: i18n.t("recipe.create-from-image"),
|
||||
text: i18n.t("recipe.create-from-images"),
|
||||
value: "image",
|
||||
hide: !enableOpenAIImages.value,
|
||||
},
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<v-card-text>
|
||||
{{ $t('recipe.recipe-bulk-importer-description') }}
|
||||
</v-card-text>
|
||||
</div>
|
||||
<div class="px-4">
|
||||
<section class="mt-2">
|
||||
<v-row
|
||||
v-for="(_, idx) in bulkUrls"
|
||||
|
@ -54,6 +54,7 @@
|
|||
cols="12"
|
||||
xs="12"
|
||||
sm="6"
|
||||
class="py-0"
|
||||
>
|
||||
<RecipeOrganizerSelector
|
||||
v-model="bulkUrls[idx].categories"
|
||||
|
@ -73,6 +74,7 @@
|
|||
cols="12"
|
||||
xs="12"
|
||||
sm="6"
|
||||
class="pt-0 pb-4"
|
||||
>
|
||||
<RecipeOrganizerSelector
|
||||
v-model="bulkUrls[idx].tags"
|
||||
|
@ -90,8 +92,9 @@
|
|||
</v-col>
|
||||
</template>
|
||||
</v-row>
|
||||
<v-card-actions class="justify-end flex-wrap mb-1">
|
||||
<v-card-actions class="justify-end flex-wrap mt-3 pa-0">
|
||||
<BaseButton
|
||||
class="mt-1 pr-4"
|
||||
delete
|
||||
@click="
|
||||
bulkUrls = [];
|
||||
|
@ -117,23 +120,26 @@
|
|||
@bulk-data="assignUrls"
|
||||
/>
|
||||
</v-card-actions>
|
||||
<div class="px-1">
|
||||
<div class="px-0">
|
||||
<v-checkbox
|
||||
v-model="showCatTags"
|
||||
hide-details
|
||||
:label="$t('recipe.set-categories-and-tags')"
|
||||
/>
|
||||
</div>
|
||||
<v-card-actions class="justify-end">
|
||||
<v-card-actions class="justify-center">
|
||||
<div style="width: 250px">
|
||||
<BaseButton
|
||||
:disabled="bulkUrls.length === 0 || lockBulkImport"
|
||||
rounded
|
||||
block
|
||||
@click="bulkCreate"
|
||||
>
|
||||
<template #icon>
|
||||
{{ $globals.icons.check }}
|
||||
</template>
|
||||
{{ $t('general.submit') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</v-card-actions>
|
||||
</section>
|
||||
<section class="mt-12">
|
||||
|
@ -144,6 +150,8 @@
|
|||
/>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
|
|
@ -61,7 +61,6 @@
|
|||
<div style="width: 250px">
|
||||
<BaseButton
|
||||
:disabled="!newRecipeData"
|
||||
large
|
||||
rounded
|
||||
block
|
||||
type="submit"
|
||||
|
|
|
@ -1,86 +1,55 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-form
|
||||
ref="domUrlForm"
|
||||
@submit.prevent="createRecipe"
|
||||
>
|
||||
<v-form ref="domUrlForm" @submit.prevent="createRecipe">
|
||||
<div>
|
||||
<v-card-title class="headline">
|
||||
{{ $t('recipe.create-recipe-from-an-image') }}
|
||||
{{ $t("recipe.create-recipe-from-an-image") }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<p>{{ $t('recipe.create-recipe-from-an-image-description') }}</p>
|
||||
<v-container class="pa-0">
|
||||
<v-row>
|
||||
<v-col
|
||||
cols="auto"
|
||||
align-self="center"
|
||||
>
|
||||
<AppButtonUpload
|
||||
v-if="!uploadedImage"
|
||||
class="ml-auto"
|
||||
url="none"
|
||||
file-name="image"
|
||||
accept="image/*"
|
||||
:text="$t('recipe.upload-image')"
|
||||
:text-btn="false"
|
||||
:post="false"
|
||||
@uploaded="uploadImage"
|
||||
/>
|
||||
<v-btn
|
||||
v-if="!!uploadedImage"
|
||||
color="error"
|
||||
@click="clearImage"
|
||||
>
|
||||
<v-icon start>
|
||||
{{ $globals.icons.close }}
|
||||
</v-icon>
|
||||
{{ $t('recipe.remove-image') }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-spacer />
|
||||
</v-row>
|
||||
|
||||
<div
|
||||
v-if="uploadedImage && uploadedImagePreviewUrl"
|
||||
class="mt-3"
|
||||
>
|
||||
<p>{{ $t("recipe.create-recipe-from-an-image-description") }}</p>
|
||||
<v-container class="px-0">
|
||||
<AppButtonUpload
|
||||
class="ml-auto"
|
||||
url="none"
|
||||
file-name="images"
|
||||
accept="image/*"
|
||||
:text="uploadedImages.length ? $t('recipe.upload-more-images') : $t('recipe.upload-images')"
|
||||
:text-btn="false"
|
||||
:post="false"
|
||||
:multiple="true"
|
||||
@uploaded="uploadImages"
|
||||
/>
|
||||
<div v-if="uploadedImages.length > 0" class="mt-3">
|
||||
<p class="my-2">
|
||||
{{ $t("recipe.crop-and-rotate-the-image") }}
|
||||
</p>
|
||||
<v-row>
|
||||
<v-col
|
||||
v-for="(imageUrl, index) in uploadedImagesPreviewUrls"
|
||||
:key="index"
|
||||
cols="12"
|
||||
class="pb-0"
|
||||
sm="6"
|
||||
lg="4"
|
||||
xl="3"
|
||||
>
|
||||
<v-card-text class="pa-0">
|
||||
<p class="mb-0">
|
||||
{{ $t('recipe.crop-and-rotate-the-image') }}
|
||||
</p>
|
||||
</v-card-text>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row style="max-width: 600px;">
|
||||
<v-spacer />
|
||||
<v-col cols="12">
|
||||
<ImageCropper
|
||||
:img="uploadedImagePreviewUrl"
|
||||
cropper-height="50vh"
|
||||
:img="imageUrl"
|
||||
cropper-height="100%"
|
||||
cropper-width="100%"
|
||||
@save="updateUploadedImage"
|
||||
:submitted="loading"
|
||||
class="mt-4"
|
||||
@save="(croppedImage) => updateUploadedImage(index, croppedImage)"
|
||||
@delete="clearImage(index)"
|
||||
/>
|
||||
</v-col>
|
||||
<v-spacer />
|
||||
</v-row>
|
||||
</div>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
<v-card-actions v-if="uploadedImage">
|
||||
<div>
|
||||
<v-card-actions v-if="uploadedImages.length">
|
||||
<div class="w-100 d-flex flex-column align-center">
|
||||
<p style="width: 250px">
|
||||
<BaseButton
|
||||
rounded
|
||||
block
|
||||
type="submit"
|
||||
:loading="loading"
|
||||
/>
|
||||
<BaseButton rounded block type="submit" :loading="loading" />
|
||||
</p>
|
||||
<p>
|
||||
<v-checkbox
|
||||
|
@ -90,11 +59,12 @@
|
|||
:disabled="loading"
|
||||
/>
|
||||
</p>
|
||||
<p
|
||||
v-if="loading"
|
||||
class="mb-0"
|
||||
>
|
||||
{{ $t('recipe.please-wait-image-procesing') }}
|
||||
<p v-if="loading" class="mb-0">
|
||||
{{
|
||||
uploadedImages.length > 1
|
||||
? $t("recipe.please-wait-images-processing")
|
||||
: $t("recipe.please-wait-image-procesing")
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</v-card-actions>
|
||||
|
@ -121,55 +91,60 @@ export default defineNuxtComponent({
|
|||
const groupSlug = computed(() => route.params.groupSlug || "");
|
||||
|
||||
const domUrlForm = ref<VForm | null>(null);
|
||||
const uploadedImage = ref<Blob | File>();
|
||||
const uploadedImageName = ref<string>("");
|
||||
const uploadedImagePreviewUrl = ref<string>();
|
||||
const uploadedImages = ref<(Blob | File)[]>([]);
|
||||
const uploadedImageNames = ref<string[]>([]);
|
||||
const uploadedImagesPreviewUrls = ref<string[]>([]);
|
||||
const shouldTranslate = ref(true);
|
||||
|
||||
function uploadImage(fileObject: File) {
|
||||
uploadedImage.value = fileObject;
|
||||
uploadedImageName.value = fileObject.name;
|
||||
uploadedImagePreviewUrl.value = URL.createObjectURL(fileObject);
|
||||
function uploadImages(files: File[]) {
|
||||
uploadedImages.value = [...uploadedImages.value, ...files];
|
||||
uploadedImageNames.value = [...uploadedImageNames.value, ...files.map(file => file.name)];
|
||||
uploadedImagesPreviewUrls.value = [
|
||||
...uploadedImagesPreviewUrls.value,
|
||||
...files.map(file => URL.createObjectURL(file)),
|
||||
];
|
||||
}
|
||||
|
||||
function updateUploadedImage(fileObject: Blob) {
|
||||
uploadedImage.value = fileObject;
|
||||
uploadedImagePreviewUrl.value = URL.createObjectURL(fileObject);
|
||||
}
|
||||
function clearImage(index: number) {
|
||||
URL.revokeObjectURL(uploadedImagesPreviewUrls.value[index]);
|
||||
|
||||
function clearImage() {
|
||||
uploadedImage.value = undefined;
|
||||
uploadedImageName.value = "";
|
||||
uploadedImagePreviewUrl.value = undefined;
|
||||
uploadedImages.value = uploadedImages.value.filter((_, i) => i !== index);
|
||||
uploadedImageNames.value = uploadedImageNames.value.filter((_, i) => i !== index);
|
||||
uploadedImagesPreviewUrls.value = uploadedImagesPreviewUrls.value.filter((_, i) => i !== index);
|
||||
}
|
||||
|
||||
async function createRecipe() {
|
||||
if (!uploadedImage.value) {
|
||||
if (uploadedImages.value.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.loading = true;
|
||||
const translateLanguage = shouldTranslate.value ? i18n.locale : undefined;
|
||||
const { data, error } = await api.recipes.createOneFromImage(uploadedImage.value, uploadedImageName.value, translateLanguage);
|
||||
const { data, error } = await api.recipes.createOneFromImages(uploadedImages.value, translateLanguage?.value);
|
||||
if (error || !data) {
|
||||
alert.error(i18n.t("events.something-went-wrong"));
|
||||
state.loading = false;
|
||||
}
|
||||
else {
|
||||
router.push(`/g/${groupSlug.value}/r/${data}`);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function updateUploadedImage(index: number, croppedImage: Blob) {
|
||||
uploadedImages.value[index] = croppedImage;
|
||||
uploadedImagesPreviewUrls.value[index] = URL.createObjectURL(croppedImage);
|
||||
}
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
domUrlForm,
|
||||
uploadedImage,
|
||||
uploadedImagePreviewUrl,
|
||||
uploadedImages,
|
||||
uploadedImagesPreviewUrls,
|
||||
shouldTranslate,
|
||||
uploadImage,
|
||||
updateUploadedImage,
|
||||
uploadImages,
|
||||
clearImage,
|
||||
createRecipe,
|
||||
updateUploadedImage,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -25,7 +25,6 @@
|
|||
<div style="width: 250px">
|
||||
<BaseButton
|
||||
:disabled="newRecipeZip === null"
|
||||
large
|
||||
rounded
|
||||
block
|
||||
:loading="loading"
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div>
|
||||
<BasePageTitle
|
||||
v-if="groupName"
|
||||
class="bg-grey-darken-4 mt-n4 pt-8"
|
||||
class="mt-n4 pt-8"
|
||||
>
|
||||
<template #header>
|
||||
<v-img
|
||||
|
|
|
@ -15,11 +15,12 @@
|
|||
</template>
|
||||
{{ $t('migration.recipe-data-migrations-explanation') }}
|
||||
</BasePageTitle>
|
||||
<v-container>
|
||||
<v-container :class="$vuetify.display.smAndDown ? 'px-0': ''">
|
||||
<BaseCardSectionTitle :title="$t('migration.new-migration')" />
|
||||
<v-card
|
||||
variant="outlined"
|
||||
:loading="loading"
|
||||
style="border-color: lightgrey;"
|
||||
>
|
||||
<v-card-title> {{ $t('migration.choose-migration-type') }} </v-card-title>
|
||||
<v-card-text
|
||||
|
@ -83,7 +84,7 @@
|
|||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-container>
|
||||
<v-container>
|
||||
<v-container class="$vuetify.display.smAndDown ? 'px-0': ''">
|
||||
<BaseCardSectionTitle :title="$t('migration.previous-migrations')" />
|
||||
<ReportTable
|
||||
:items="reports"
|
||||
|
|
|
@ -4,14 +4,14 @@
|
|||
<v-col v-for="(day, index) in plan" :key="index" cols="12" sm="12" md="4" lg="4" xl="2"
|
||||
class="col-borders my-1 d-flex flex-column">
|
||||
<v-card class="mb-2 border-left-primary rounded-sm px-2">
|
||||
<v-container class="px-0">
|
||||
<v-container class="px-0 d-flex align-center" height="56px">
|
||||
<v-row no-gutters style="width: 100%;">
|
||||
<v-col cols="10">
|
||||
<v-col cols="10" class="d-flex align-center">
|
||||
<p class="pl-2 my-1">
|
||||
{{ $d(day.date, "short") }}
|
||||
</p>
|
||||
</v-col>
|
||||
<v-col class="d-flex justify-top" cols="2">
|
||||
<v-col class="d-flex align-center" cols="2">
|
||||
<GroupMealPlanDayContextMenu v-if="day.recipes.length" :recipes="day.recipes" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
@ -38,82 +38,68 @@
|
|||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import type { MealsByDate } from "./types";
|
||||
import type { ReadPlanEntry } from "~/lib/api/types/meal-plan";
|
||||
import GroupMealPlanDayContextMenu from "~/components/Domain/Household/GroupMealPlanDayContextMenu.vue";
|
||||
import RecipeCardMobile from "~/components/Domain/Recipe/RecipeCardMobile.vue";
|
||||
import type { RecipeSummary } from "~/lib/api/types/recipe";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
GroupMealPlanDayContextMenu,
|
||||
RecipeCardMobile,
|
||||
},
|
||||
props: {
|
||||
mealplans: {
|
||||
type: Array as () => MealsByDate[],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
type DaySection = {
|
||||
title: string;
|
||||
meals: ReadPlanEntry[];
|
||||
const props = defineProps<{
|
||||
mealplans: MealsByDate[];
|
||||
}>();
|
||||
|
||||
type DaySection = {
|
||||
title: string;
|
||||
meals: ReadPlanEntry[];
|
||||
};
|
||||
|
||||
type Days = {
|
||||
date: Date;
|
||||
sections: DaySection[];
|
||||
recipes: RecipeSummary[];
|
||||
};
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
const plan = computed<Days[]>(() => {
|
||||
return props.mealplans.reduce((acc, day) => {
|
||||
const out: Days = {
|
||||
date: day.date,
|
||||
sections: [
|
||||
{ title: i18n.t("meal-plan.breakfast"), meals: [] },
|
||||
{ title: i18n.t("meal-plan.lunch"), meals: [] },
|
||||
{ title: i18n.t("meal-plan.dinner"), meals: [] },
|
||||
{ title: i18n.t("meal-plan.side"), meals: [] },
|
||||
],
|
||||
recipes: [],
|
||||
};
|
||||
|
||||
type Days = {
|
||||
date: Date;
|
||||
sections: DaySection[];
|
||||
recipes: RecipeSummary[];
|
||||
};
|
||||
for (const meal of day.meals) {
|
||||
if (meal.entryType === "breakfast") {
|
||||
out.sections[0].meals.push(meal);
|
||||
}
|
||||
else if (meal.entryType === "lunch") {
|
||||
out.sections[1].meals.push(meal);
|
||||
}
|
||||
else if (meal.entryType === "dinner") {
|
||||
out.sections[2].meals.push(meal);
|
||||
}
|
||||
else if (meal.entryType === "side") {
|
||||
out.sections[3].meals.push(meal);
|
||||
}
|
||||
|
||||
const i18n = useI18n();
|
||||
if (meal.recipe) {
|
||||
out.recipes.push(meal.recipe);
|
||||
}
|
||||
}
|
||||
|
||||
const plan = computed<Days[]>(() => {
|
||||
return props.mealplans.reduce((acc, day) => {
|
||||
const out: Days = {
|
||||
date: day.date,
|
||||
sections: [
|
||||
{ title: i18n.t("meal-plan.breakfast"), meals: [] },
|
||||
{ title: i18n.t("meal-plan.lunch"), meals: [] },
|
||||
{ title: i18n.t("meal-plan.dinner"), meals: [] },
|
||||
{ title: i18n.t("meal-plan.side"), meals: [] },
|
||||
],
|
||||
recipes: [],
|
||||
};
|
||||
// Drop empty sections
|
||||
out.sections = out.sections.filter(section => section.meals.length > 0);
|
||||
|
||||
for (const meal of day.meals) {
|
||||
if (meal.entryType === "breakfast") {
|
||||
out.sections[0].meals.push(meal);
|
||||
}
|
||||
else if (meal.entryType === "lunch") {
|
||||
out.sections[1].meals.push(meal);
|
||||
}
|
||||
else if (meal.entryType === "dinner") {
|
||||
out.sections[2].meals.push(meal);
|
||||
}
|
||||
else if (meal.entryType === "side") {
|
||||
out.sections[3].meals.push(meal);
|
||||
}
|
||||
acc.push(out);
|
||||
|
||||
if (meal.recipe) {
|
||||
out.recipes.push(meal.recipe);
|
||||
}
|
||||
}
|
||||
|
||||
// Drop empty sections
|
||||
out.sections = out.sections.filter(section => section.meals.length > 0);
|
||||
|
||||
acc.push(out);
|
||||
|
||||
return acc;
|
||||
}, [] as Days[]);
|
||||
});
|
||||
|
||||
return {
|
||||
plan,
|
||||
};
|
||||
},
|
||||
return acc;
|
||||
}, [] as Days[]);
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
<BaseDialog
|
||||
v-model="createDialog"
|
||||
:title="$t('events.new-notification')"
|
||||
:icon="$globals.icons.bellPlus"
|
||||
can-submit
|
||||
@submit="createNewNotifier"
|
||||
>
|
||||
|
@ -95,7 +96,7 @@
|
|||
>
|
||||
<v-expansion-panel-title
|
||||
disable-icon-rotate
|
||||
class="headline"
|
||||
class="text-h6"
|
||||
>
|
||||
<div class="d-flex align-center">
|
||||
{{ notifier.name }}
|
||||
|
@ -103,6 +104,7 @@
|
|||
<template #actions>
|
||||
<v-btn
|
||||
icon
|
||||
flat
|
||||
class="ml-2"
|
||||
>
|
||||
<v-icon>
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
<!-- Form Container -->
|
||||
<div class="d-flex justify-center grow items-center my-4">
|
||||
<template v-if="state.ctx.state === States.Initial">
|
||||
<div width="600px">
|
||||
<v-container>
|
||||
<v-card-title class="text-h5 my-4 mb-5 pb-0 text-center">
|
||||
{{ $t("user-registration.user-registration") }}
|
||||
</v-card-title>
|
||||
|
@ -60,7 +60,7 @@
|
|||
color="primary"
|
||||
dark
|
||||
hover
|
||||
width="300px"
|
||||
width="320px"
|
||||
@click="initial.joinGroup"
|
||||
>
|
||||
<v-card-title class="d-flex align-center justify-center py-3">
|
||||
|
@ -77,8 +77,8 @@
|
|||
color="primary"
|
||||
dark
|
||||
hover
|
||||
width="300px"
|
||||
@click="initial.joinGroup"
|
||||
width="320px"
|
||||
@click="initial.createGroup"
|
||||
>
|
||||
<v-card-title class="d-flex align-center justify-center py-3">
|
||||
<v-icon
|
||||
|
@ -92,7 +92,7 @@
|
|||
</v-card-title>
|
||||
</v-card>
|
||||
</div>
|
||||
</div>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<template v-else-if="state.ctx.state === States.ProvideToken">
|
||||
|
@ -333,9 +333,7 @@ import type { VForm } from "~/types/auto-forms";
|
|||
|
||||
const inputAttrs = {
|
||||
variant: "filled",
|
||||
rounded: true,
|
||||
validateOnBlur: true,
|
||||
class: "rounded-lg",
|
||||
};
|
||||
|
||||
export default defineNuxtComponent({
|
||||
|
|
|
@ -17,21 +17,19 @@
|
|||
<v-form @submit.prevent="requestLink()">
|
||||
<v-text-field
|
||||
v-model="email"
|
||||
:prepend-icon="$globals.icons.email"
|
||||
variant="filled"
|
||||
rounded
|
||||
:prepend-inner-icon="$globals.icons.email"
|
||||
variant="solo-filled"
|
||||
flat
|
||||
autofocus
|
||||
class="rounded-lg"
|
||||
name="login"
|
||||
:label="$t('user.email')"
|
||||
type="text"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="password"
|
||||
variant="filled"
|
||||
rounded
|
||||
class="rounded-lg"
|
||||
:prepend-icon="$globals.icons.lock"
|
||||
variant="solo-filled"
|
||||
flat
|
||||
:prepend-inner-icon="$globals.icons.lock"
|
||||
name="password"
|
||||
:label="$t('user.password')"
|
||||
type="password"
|
||||
|
@ -39,11 +37,10 @@
|
|||
/>
|
||||
<v-text-field
|
||||
v-model="passwordConfirm"
|
||||
variant="filled"
|
||||
rounded
|
||||
variant="solo-filled"
|
||||
flat
|
||||
validate-on="blur"
|
||||
class="rounded-lg"
|
||||
:prepend-icon="$globals.icons.lock"
|
||||
:prepend-inner-icon="$globals.icons.lock"
|
||||
name="password"
|
||||
:label="$t('user.confirm-password')"
|
||||
type="password"
|
||||
|
|
|
@ -17,9 +17,10 @@
|
|||
<section class="d-flex justify-center">
|
||||
<v-card
|
||||
class="mt-4"
|
||||
width="500px"
|
||||
width="100%"
|
||||
flat
|
||||
>
|
||||
<v-card-text>
|
||||
<v-card-text class="px-0">
|
||||
<v-form
|
||||
ref="domNewTokenForm"
|
||||
@submit.prevent
|
||||
|
@ -38,16 +39,16 @@
|
|||
readonly
|
||||
rows="3"
|
||||
/>
|
||||
<v-list-subheader class="text-center">
|
||||
<p>
|
||||
{{
|
||||
$t(
|
||||
"settings.token.copy-this-token-for-use-with-an-external-application-this-token-will-not-be-viewable-again",
|
||||
)
|
||||
}}
|
||||
</v-list-subheader>
|
||||
</p>
|
||||
</template>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-card-actions class="px-0">
|
||||
<BaseButton
|
||||
v-if="createdToken"
|
||||
cancel
|
||||
|
@ -78,14 +79,11 @@
|
|||
:title="$t('settings.token.active-tokens')"
|
||||
/>
|
||||
<section class="d-flex flex-column">
|
||||
<v-list>
|
||||
<div
|
||||
v-for="(token, index) in user.tokens"
|
||||
:key="index"
|
||||
>
|
||||
<v-card
|
||||
variant="outlined"
|
||||
class="mb-2"
|
||||
>
|
||||
<v-list-item>
|
||||
<v-list-item-title>
|
||||
{{ token.name }}
|
||||
|
@ -93,16 +91,17 @@
|
|||
<v-list-item-subtitle>
|
||||
{{ $t('general.created-on-date', [$d(new Date(token.createdAt!))]) }}
|
||||
</v-list-item-subtitle>
|
||||
<v-list-item-action>
|
||||
<template #append>
|
||||
<BaseButton
|
||||
delete
|
||||
small
|
||||
@click="deleteToken(token.id)"
|
||||
/>
|
||||
</v-list-item-action>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-card>
|
||||
</div>
|
||||
<v-divider class="mx-2 my-2" />
|
||||
</div>
|
||||
</v-list>
|
||||
</section>
|
||||
</v-container>
|
||||
</template>
|
||||
|
|
|
@ -149,7 +149,7 @@
|
|||
variant="underlined"
|
||||
@click:append="showPassword = !showPassword"
|
||||
/>
|
||||
<UserPasswordStrength :value="password.newOne" />
|
||||
<UserPasswordStrength v-model="password.newOne" />
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<v-container v-if="user">
|
||||
<v-container v-if="user" class="mb-8">
|
||||
<section class="d-flex flex-column align-center mt-4">
|
||||
<UserAvatar
|
||||
:tooltip="false"
|
||||
|
@ -7,7 +7,7 @@
|
|||
:user-id="user.id"
|
||||
/>
|
||||
|
||||
<h2 class="text-h4">
|
||||
<h2 class="text-h4 text-center">
|
||||
{{ $t('profile.welcome-user', [user.fullName]) }}
|
||||
</h2>
|
||||
<p class="subtitle-1 mb-0 text-center">
|
||||
|
|
|
@ -7,8 +7,7 @@ export default defineNuxtPlugin(() => {
|
|||
baseURL: "/", // api calls already pass with /api
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer " + useCookie(tokenName).value,
|
||||
Authorization: "Bearer " + useCookie(tokenName).value,
|
||||
},
|
||||
withCredentials: true,
|
||||
});
|
||||
|
|
1332
frontend/yarn.lock
1332
frontend/yarn.lock
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,5 @@
|
|||
import abc
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
import jwt
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
@ -13,10 +12,8 @@ ALGORITHM = "HS256"
|
|||
ISS = "mealie"
|
||||
remember_me_duration = timedelta(days=14)
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class AuthProvider(Generic[T], metaclass=abc.ABCMeta):
|
||||
class AuthProvider[T](metaclass=abc.ABCMeta):
|
||||
"""Base Authentication Provider interface"""
|
||||
|
||||
def __init__(self, session: Session, data: T) -> None:
|
||||
|
|
|
@ -408,7 +408,7 @@ class AppSettings(AppLoggingSettings):
|
|||
description = None
|
||||
if not self.OPENAI_API_KEY:
|
||||
description = "OPENAI_API_KEY is not set"
|
||||
elif self.OPENAI_MODEL:
|
||||
elif not self.OPENAI_MODEL:
|
||||
description = "OPENAI_MODEL is not set"
|
||||
|
||||
return FeatureDetails(
|
||||
|
@ -449,7 +449,7 @@ class AppSettings(AppLoggingSettings):
|
|||
def app_settings_constructor(data_dir: Path, production: bool, env_file: Path, env_encoding="utf-8") -> AppSettings:
|
||||
"""
|
||||
app_settings_constructor is a factory function that returns an AppSettings object. It is used to inject the
|
||||
required dependencies into the AppSettings object and nested child objects. AppSettings should not be substantiated
|
||||
required dependencies into the AppSettings object and nested child objects. AppSettings should not be instantiated
|
||||
directly, but rather through this factory function.
|
||||
"""
|
||||
secret_settings = {
|
||||
|
|
|
@ -4,7 +4,7 @@ import random
|
|||
from collections.abc import Iterable
|
||||
from datetime import UTC, datetime
|
||||
from math import ceil
|
||||
from typing import Any, Generic, TypeVar
|
||||
from typing import Any
|
||||
|
||||
from fastapi import HTTPException
|
||||
from pydantic import UUID4, BaseModel
|
||||
|
@ -28,18 +28,13 @@ from mealie.schema.response.query_search import SearchFilter
|
|||
|
||||
from ._utils import NOT_SET, NotSet
|
||||
|
||||
Schema = TypeVar("Schema", bound=MealieModel)
|
||||
Model = TypeVar("Model", bound=SqlAlchemyBase)
|
||||
|
||||
T = TypeVar("T", bound="RepositoryGeneric")
|
||||
|
||||
|
||||
class RepositoryGeneric(Generic[Schema, Model]):
|
||||
class RepositoryGeneric[Schema: MealieModel, Model: SqlAlchemyBase]:
|
||||
"""A Generic BaseAccess Model method to perform common operations on the database
|
||||
|
||||
Args:
|
||||
Generic ([Schema]): Represents the Pydantic Model
|
||||
Generic ([Model]): Represents the SqlAlchemyModel Model
|
||||
Schema: Represents the Pydantic Model
|
||||
Model: Represents the SqlAlchemyModel Model
|
||||
"""
|
||||
|
||||
session: Session
|
||||
|
@ -467,7 +462,7 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
|||
return search_filter.filter_query_by_search(query, schema, self.model)
|
||||
|
||||
|
||||
class GroupRepositoryGeneric(RepositoryGeneric[Schema, Model]):
|
||||
class GroupRepositoryGeneric[Schema: MealieModel, Model: SqlAlchemyBase](RepositoryGeneric[Schema, Model]):
|
||||
def __init__(
|
||||
self,
|
||||
session: Session,
|
||||
|
@ -483,7 +478,7 @@ class GroupRepositoryGeneric(RepositoryGeneric[Schema, Model]):
|
|||
self._group_id = group_id if group_id else None
|
||||
|
||||
|
||||
class HouseholdRepositoryGeneric(RepositoryGeneric[Schema, Model]):
|
||||
class HouseholdRepositoryGeneric[Schema: MealieModel, Model: SqlAlchemyBase](RepositoryGeneric[Schema, Model]):
|
||||
def __init__(
|
||||
self,
|
||||
session: Session,
|
||||
|
|
|
@ -6,20 +6,18 @@ See their repository for details -> https://github.com/dmontagu/fastapi-utils
|
|||
|
||||
import inspect
|
||||
from collections.abc import Callable
|
||||
from typing import Any, ClassVar, ForwardRef, TypeVar, cast, get_origin, get_type_hints
|
||||
from typing import Any, ClassVar, ForwardRef, cast, get_origin, get_type_hints
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi.routing import APIRoute
|
||||
from starlette.routing import Route, WebSocketRoute
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
CBV_CLASS_KEY = "__cbv_class__"
|
||||
INCLUDE_INIT_PARAMS_KEY = "__include_init_params__"
|
||||
RETURN_TYPES_FUNC_KEY = "__return_types_func__"
|
||||
|
||||
|
||||
def controller(router: APIRouter, *urls: str) -> Callable[[type[T]], type[T]]:
|
||||
def controller[T](router: APIRouter, *urls: str) -> Callable[[type[T]], type[T]]:
|
||||
"""
|
||||
This function returns a decorator that converts the decorated into a class-based view for the provided router.
|
||||
Any methods of the decorated class that are decorated as endpoints using the router provided to this function
|
||||
|
@ -36,7 +34,7 @@ def controller(router: APIRouter, *urls: str) -> Callable[[type[T]], type[T]]:
|
|||
return decorator
|
||||
|
||||
|
||||
def _cbv(router: APIRouter, cls: type[T], *urls: str, instance: Any | None = None) -> type[T]:
|
||||
def _cbv[T](router: APIRouter, cls: type[T], *urls: str, instance: Any | None = None) -> type[T]:
|
||||
"""
|
||||
Replaces any methods of the provided class `cls` that are endpoints of routes in `router` with updated
|
||||
function calls that will properly inject an instance of `cls`.
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
from collections.abc import Callable
|
||||
from logging import Logger
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
import sqlalchemy.exc
|
||||
from fastapi import HTTPException, status
|
||||
|
@ -9,12 +8,8 @@ from pydantic import UUID4, BaseModel
|
|||
from mealie.repos.repository_generic import RepositoryGeneric
|
||||
from mealie.schema.response import ErrorResponse
|
||||
|
||||
C = TypeVar("C", bound=BaseModel)
|
||||
R = TypeVar("R", bound=BaseModel)
|
||||
U = TypeVar("U", bound=BaseModel)
|
||||
|
||||
|
||||
class HttpRepo(Generic[C, R, U]):
|
||||
class HttpRepo[C: BaseModel, R: BaseModel, U: BaseModel]:
|
||||
"""
|
||||
The HttpRepo[C, R, U] class is a mixin class that provides a common set of methods for CRUD operations.
|
||||
This class is intended to be used in a composition pattern where a class has a mixin property. For example:
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from functools import cached_property
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import UUID4
|
||||
|
||||
from mealie.core import security
|
||||
|
@ -42,6 +42,11 @@ class AdminUserManagementRoutes(BaseAdminController):
|
|||
|
||||
@router.post("", response_model=UserOut, status_code=201)
|
||||
def create_one(self, data: UserIn):
|
||||
if self.repos.users.get_by_username(data.username):
|
||||
raise HTTPException(status.HTTP_409_CONFLICT, {"message": self.t("exceptions.username-conflict-error")})
|
||||
elif self.repos.users.get_one(data.email, "email"):
|
||||
raise HTTPException(status.HTTP_409_CONFLICT, {"message": self.t("exceptions.email-conflict-error")})
|
||||
|
||||
data.password = security.hash_password(data.password)
|
||||
return self.mixins.create_one(data)
|
||||
|
||||
|
|
|
@ -9,7 +9,6 @@ router = APIRouter()
|
|||
|
||||
router.include_router(registration.router, prefix=user_prefix, tags=["Users: Registration"])
|
||||
router.include_router(crud.user_router)
|
||||
router.include_router(crud.admin_router)
|
||||
router.include_router(forgot_password.router, prefix=user_prefix, tags=["Users: Passwords"])
|
||||
router.include_router(images.router, prefix=user_prefix, tags=["Users: Images"])
|
||||
router.include_router(api_tokens.router)
|
||||
|
|
|
@ -1,52 +1,17 @@
|
|||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi import HTTPException, status
|
||||
from pydantic import UUID4
|
||||
|
||||
from mealie.core.security import hash_password
|
||||
from mealie.core.security.providers.credentials_provider import CredentialsProvider
|
||||
from mealie.db.models.users.users import AuthMethod
|
||||
from mealie.routes._base import BaseAdminController, BaseUserController, controller
|
||||
from mealie.routes._base.mixins import HttpRepo
|
||||
from mealie.routes._base.routers import AdminAPIRouter, UserAPIRouter
|
||||
from mealie.routes._base import BaseUserController, controller
|
||||
from mealie.routes._base.routers import UserAPIRouter
|
||||
from mealie.routes.users._helpers import assert_user_change_allowed
|
||||
from mealie.schema.response import ErrorResponse, SuccessResponse
|
||||
from mealie.schema.response.pagination import PaginationQuery
|
||||
from mealie.schema.user import ChangePassword, UserBase, UserIn, UserOut
|
||||
from mealie.schema.user.user import UserPagination, UserRatings, UserRatingSummary
|
||||
from mealie.schema.user import ChangePassword, UserBase, UserOut
|
||||
from mealie.schema.user.user import UserRatings, UserRatingSummary
|
||||
|
||||
user_router = UserAPIRouter(prefix="/users", tags=["Users: CRUD"])
|
||||
admin_router = AdminAPIRouter(prefix="/users", tags=["Users: Admin CRUD"])
|
||||
|
||||
|
||||
@controller(admin_router)
|
||||
class AdminUserController(BaseAdminController):
|
||||
@property
|
||||
def mixins(self) -> HttpRepo:
|
||||
return HttpRepo[UserIn, UserOut, UserBase](self.repos.users, self.logger)
|
||||
|
||||
@admin_router.get("", response_model=UserPagination)
|
||||
def get_all(self, q: PaginationQuery = Depends(PaginationQuery)):
|
||||
"""Returns all users from all groups"""
|
||||
|
||||
response = self.repos.users.page_all(
|
||||
pagination=q,
|
||||
override=UserOut,
|
||||
)
|
||||
|
||||
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)
|
||||
def create_user(self, new_user: UserIn):
|
||||
new_user.password = hash_password(new_user.password)
|
||||
return self.mixins.create_one(new_user)
|
||||
|
||||
@admin_router.get("/{item_id}", response_model=UserOut)
|
||||
def get_user(self, item_id: UUID4):
|
||||
return self.mixins.get_one(item_id)
|
||||
|
||||
@admin_router.delete("/{item_id}")
|
||||
def delete_user(self, item_id: UUID4):
|
||||
self.mixins.delete_one(item_id)
|
||||
|
||||
|
||||
@controller(user_router)
|
||||
|
|
|
@ -4,7 +4,7 @@ import re
|
|||
from collections.abc import Sequence
|
||||
from datetime import UTC, datetime
|
||||
from enum import Enum
|
||||
from typing import ClassVar, Protocol, Self, TypeVar
|
||||
from typing import ClassVar, Protocol, Self
|
||||
|
||||
from humps.main import camelize
|
||||
from pydantic import UUID4, AliasChoices, BaseModel, ConfigDict, Field, model_validator
|
||||
|
@ -14,8 +14,6 @@ from sqlalchemy.orm.interfaces import LoaderOption
|
|||
|
||||
from mealie.db.models._model_base import SqlAlchemyBase
|
||||
|
||||
T = TypeVar("T", bound=BaseModel)
|
||||
|
||||
HOUR_ONLY_TZ_PATTERN = re.compile(r"[+-]\d{2}$")
|
||||
|
||||
|
||||
|
@ -56,7 +54,7 @@ class MealieModel(BaseModel):
|
|||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def fix_hour_only_tz(cls, data: T) -> T:
|
||||
def fix_hour_only_tz[T: BaseModel](cls, data: T) -> T:
|
||||
"""
|
||||
Fixes datetimes with timezones that only have the hour portion.
|
||||
|
||||
|
@ -82,7 +80,7 @@ class MealieModel(BaseModel):
|
|||
Adds UTC timezone information to all datetimes in the model.
|
||||
The server stores everything in UTC without timezone info.
|
||||
"""
|
||||
for field in self.model_fields:
|
||||
for field in self.__class__.model_fields:
|
||||
val = getattr(self, field)
|
||||
if not isinstance(val, datetime):
|
||||
continue
|
||||
|
@ -91,23 +89,25 @@ class MealieModel(BaseModel):
|
|||
|
||||
return self
|
||||
|
||||
def cast(self, cls: type[T], **kwargs) -> T:
|
||||
def cast[T: BaseModel](self, cls: type[T], **kwargs) -> T:
|
||||
"""
|
||||
Cast the current model to another with additional arguments. Useful for
|
||||
transforming DTOs into models that are saved to a database
|
||||
"""
|
||||
create_data = {field: getattr(self, field) for field in self.model_fields if field in cls.model_fields}
|
||||
create_data = {
|
||||
field: getattr(self, field) for field in self.__class__.model_fields if field in cls.model_fields
|
||||
}
|
||||
create_data.update(kwargs or {})
|
||||
return cls(**create_data)
|
||||
|
||||
def map_to(self, dest: T) -> T:
|
||||
def map_to[T: BaseModel](self, dest: T) -> T:
|
||||
"""
|
||||
Map matching values from the current model to another model. Model returned
|
||||
for method chaining.
|
||||
"""
|
||||
|
||||
for field in self.model_fields:
|
||||
if field in dest.model_fields:
|
||||
for field in self.__class__.model_fields:
|
||||
if field in dest.__class__.model_fields:
|
||||
setattr(dest, field, getattr(self, field))
|
||||
|
||||
return dest
|
||||
|
@ -117,18 +117,18 @@ class MealieModel(BaseModel):
|
|||
Map matching values from another model to the current model.
|
||||
"""
|
||||
|
||||
for field in src.model_fields:
|
||||
if field in self.model_fields:
|
||||
for field in src.__class__.model_fields:
|
||||
if field in self.__class__.model_fields:
|
||||
setattr(self, field, getattr(src, field))
|
||||
|
||||
def merge(self, src: T, replace_null=False):
|
||||
def merge[T: BaseModel](self, src: T, replace_null=False):
|
||||
"""
|
||||
Replace matching values from another instance to the current instance.
|
||||
"""
|
||||
|
||||
for field in src.model_fields:
|
||||
for field in src.__class__.model_fields:
|
||||
val = getattr(src, field)
|
||||
if field in self.model_fields and (val is not None or replace_null):
|
||||
if field in self.__class__.model_fields and (val is not None or replace_null):
|
||||
setattr(self, field, val)
|
||||
|
||||
@classmethod
|
||||
|
|
|
@ -1,24 +1,21 @@
|
|||
from typing import TypeVar
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
T = TypeVar("T", bound=BaseModel)
|
||||
U = TypeVar("U", bound=BaseModel)
|
||||
|
||||
|
||||
def mapper(source: U, dest: T, **_) -> T:
|
||||
def mapper[U: BaseModel, T: BaseModel](source: U, dest: T, **_) -> T:
|
||||
"""
|
||||
Map a source model to a destination model. Only top-level fields are mapped.
|
||||
"""
|
||||
|
||||
for field in source.model_fields:
|
||||
if field in dest.model_fields:
|
||||
for field in source.__class__.model_fields:
|
||||
if field in dest.__class__.model_fields:
|
||||
setattr(dest, field, getattr(source, field))
|
||||
|
||||
return dest
|
||||
|
||||
|
||||
def cast(source: U, dest: type[T], **kwargs) -> T:
|
||||
create_data = {field: getattr(source, field) for field in source.model_fields if field in dest.model_fields}
|
||||
def cast[U: BaseModel, T: BaseModel](source: U, dest: type[T], **kwargs) -> T:
|
||||
create_data = {
|
||||
field: getattr(source, field) for field in source.__class__.model_fields if field in dest.model_fields
|
||||
}
|
||||
create_data.update(kwargs or {})
|
||||
return dest(**create_data)
|
||||
|
|
|
@ -184,8 +184,9 @@ class OpenAIRecipe(OpenAIBase):
|
|||
[],
|
||||
description=dedent(
|
||||
"""
|
||||
A list of ingredients used in the recipe. Ingredients should be inserted in the order they appear in the
|
||||
recipe. If the recipe has no ingredients, you should return an empty list.
|
||||
A list of instructions for the recipe. Each instruction should represent one step in the recipe,
|
||||
and should be inserted in the order they appear in the recipe. If the recipe has no instructions,
|
||||
you should return an empty list.
|
||||
|
||||
Often times, but not always, instructions are separated by line breaks and/or separated by paragraphs.
|
||||
Use these as a guide to separate instructions. They also may be separated by numbers or words, such as
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import enum
|
||||
from typing import Annotated, Any, Generic, TypeVar
|
||||
from typing import Annotated, Any
|
||||
from urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit
|
||||
|
||||
from humps import camelize
|
||||
|
@ -8,8 +8,6 @@ from pydantic_core.core_schema import ValidationInfo
|
|||
|
||||
from mealie.schema._mealie import MealieModel
|
||||
|
||||
DataT = TypeVar("DataT", bound=BaseModel)
|
||||
|
||||
|
||||
class OrderDirection(str, enum.Enum):
|
||||
asc = "asc"
|
||||
|
@ -50,7 +48,7 @@ class PaginationQuery(RequestQuery):
|
|||
per_page: int = 50
|
||||
|
||||
|
||||
class PaginationBase(BaseModel, Generic[DataT]):
|
||||
class PaginationBase[DataT: BaseModel](BaseModel):
|
||||
page: int = 1
|
||||
per_page: int = 10
|
||||
total: int = 0
|
||||
|
|
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||
import re
|
||||
from collections import deque
|
||||
from enum import Enum
|
||||
from typing import Any, TypeVar, cast
|
||||
from typing import Any, cast
|
||||
from uuid import UUID
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
@ -19,8 +19,6 @@ from mealie.db.models._model_utils.datetime import NaiveDateTime
|
|||
from mealie.db.models._model_utils.guid import GUID
|
||||
from mealie.schema._mealie.mealie_model import MealieModel
|
||||
|
||||
Model = TypeVar("Model", bound=SqlAlchemyBase)
|
||||
|
||||
|
||||
class RelationalKeyword(Enum):
|
||||
IS = "IS"
|
||||
|
@ -274,7 +272,7 @@ class QueryFilterBuilder:
|
|||
return consolidated_group_builder.self_group()
|
||||
|
||||
@classmethod
|
||||
def get_model_and_model_attr_from_attr_string(
|
||||
def get_model_and_model_attr_from_attr_string[Model: SqlAlchemyBase](
|
||||
cls, attr_string: str, model: type[Model], *, query: sa.Select | None = None
|
||||
) -> tuple[SqlAlchemyBase, InstrumentedAttribute, sa.Select | None]:
|
||||
"""
|
||||
|
@ -343,7 +341,7 @@ class QueryFilterBuilder:
|
|||
return model_attr
|
||||
|
||||
@classmethod
|
||||
def _get_filter_element(
|
||||
def _get_filter_element[Model: SqlAlchemyBase](
|
||||
cls,
|
||||
query: sa.Select,
|
||||
component: QueryFilterBuilderComponent,
|
||||
|
@ -397,7 +395,7 @@ class QueryFilterBuilder:
|
|||
|
||||
return element
|
||||
|
||||
def filter_query(
|
||||
def filter_query[Model: SqlAlchemyBase](
|
||||
self, query: sa.Select, model: type[Model], column_aliases: dict[str, sa.ColumnElement] | None = None
|
||||
) -> sa.Select:
|
||||
"""
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from datetime import UTC, datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Any, Generic, TypeVar
|
||||
from typing import Annotated, Any
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import UUID4, BaseModel, ConfigDict, Field, StringConstraints, field_validator
|
||||
|
@ -20,7 +20,6 @@ from mealie.schema.response.pagination import PaginationBase
|
|||
from ...db.models.group import Group
|
||||
from ..recipe import CategoryBase
|
||||
|
||||
DataT = TypeVar("DataT", bound=BaseModel)
|
||||
DEFAULT_INTEGRATION_ID = "generic"
|
||||
settings = get_app_settings()
|
||||
|
||||
|
@ -102,7 +101,7 @@ class UserRatingOut(UserRatingCreate):
|
|||
]
|
||||
|
||||
|
||||
class UserRatings(BaseModel, Generic[DataT]):
|
||||
class UserRatings[DataT: BaseModel](BaseModel):
|
||||
ratings: list[DataT]
|
||||
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from pydantic import BaseModel
|
||||
from slugify import slugify
|
||||
|
@ -12,8 +12,6 @@ from mealie.schema.recipe import RecipeCategory
|
|||
from mealie.schema.recipe.recipe import RecipeTag
|
||||
from mealie.schema.recipe.recipe_category import CategoryOut, CategorySave, TagOut, TagSave
|
||||
|
||||
T = TypeVar("T", bound=BaseModel)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mealie.repos.repository_generic import RepositoryGeneric
|
||||
|
||||
|
@ -23,7 +21,7 @@ class DatabaseMigrationHelpers:
|
|||
self.session = session
|
||||
self.db = db
|
||||
|
||||
def _get_or_set_generic(
|
||||
def _get_or_set_generic[T: BaseModel](
|
||||
self, accessor: RepositoryGeneric, items: Iterable[str], create_model: type[T], out_model: type[T]
|
||||
) -> list[T]:
|
||||
"""
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from typing import TypeVar
|
||||
|
||||
from pydantic import UUID4, BaseModel
|
||||
from rapidfuzz import fuzz, process
|
||||
|
@ -17,8 +16,6 @@ from mealie.schema.recipe.recipe_ingredient import (
|
|||
)
|
||||
from mealie.schema.response.pagination import PaginationQuery
|
||||
|
||||
T = TypeVar("T", bound=BaseModel)
|
||||
|
||||
|
||||
class DataMatcher:
|
||||
def __init__(
|
||||
|
@ -83,7 +80,9 @@ class DataMatcher:
|
|||
return self._units_by_alias
|
||||
|
||||
@classmethod
|
||||
def find_match(cls, match_value: str, *, store_map: dict[str, T], fuzzy_match_threshold: int = 0) -> T | None:
|
||||
def find_match[T: BaseModel](
|
||||
cls, match_value: str, *, store_map: dict[str, T], fuzzy_match_threshold: int = 0
|
||||
) -> T | None:
|
||||
# check for literal matches
|
||||
if match_value in store_map:
|
||||
return store_map[match_value]
|
||||
|
|
507
poetry.lock
generated
507
poetry.lock
generated
|
@ -684,14 +684,14 @@ cli = ["requests"]
|
|||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.115.13"
|
||||
version = "0.115.14"
|
||||
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "fastapi-0.115.13-py3-none-any.whl", hash = "sha256:0a0cab59afa7bab22f5eb347f8c9864b681558c278395e94035a741fc10cd865"},
|
||||
{file = "fastapi-0.115.13.tar.gz", hash = "sha256:55d1d25c2e1e0a0a50aceb1c8705cd932def273c102bff0b1c1da88b3c6eb307"},
|
||||
{file = "fastapi-0.115.14-py3-none-any.whl", hash = "sha256:6c0c8bf9420bd58f565e585036d971872472b4f7d3f6c73b698e10cffdefb3ca"},
|
||||
{file = "fastapi-0.115.14.tar.gz", hash = "sha256:b1de15cdc1c499a4da47914db35d0e4ef8f1ce62b624e94e0e5824421df99739"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -1239,144 +1239,106 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "lxml"
|
||||
version = "5.4.0"
|
||||
version = "6.0.0"
|
||||
description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "lxml-5.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e7bc6df34d42322c5289e37e9971d6ed114e3776b45fa879f734bded9d1fea9c"},
|
||||
{file = "lxml-5.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6854f8bd8a1536f8a1d9a3655e6354faa6406621cf857dc27b681b69860645c7"},
|
||||
{file = "lxml-5.4.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:696ea9e87442467819ac22394ca36cb3d01848dad1be6fac3fb612d3bd5a12cf"},
|
||||
{file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ef80aeac414f33c24b3815ecd560cee272786c3adfa5f31316d8b349bfade28"},
|
||||
{file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b9c2754cef6963f3408ab381ea55f47dabc6f78f4b8ebb0f0b25cf1ac1f7609"},
|
||||
{file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7a62cc23d754bb449d63ff35334acc9f5c02e6dae830d78dab4dd12b78a524f4"},
|
||||
{file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f82125bc7203c5ae8633a7d5d20bcfdff0ba33e436e4ab0abc026a53a8960b7"},
|
||||
{file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b67319b4aef1a6c56576ff544b67a2a6fbd7eaee485b241cabf53115e8908b8f"},
|
||||
{file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:a8ef956fce64c8551221f395ba21d0724fed6b9b6242ca4f2f7beb4ce2f41997"},
|
||||
{file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:0a01ce7d8479dce84fc03324e3b0c9c90b1ece9a9bb6a1b6c9025e7e4520e78c"},
|
||||
{file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:91505d3ddebf268bb1588eb0f63821f738d20e1e7f05d3c647a5ca900288760b"},
|
||||
{file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a3bcdde35d82ff385f4ede021df801b5c4a5bcdfb61ea87caabcebfc4945dc1b"},
|
||||
{file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:aea7c06667b987787c7d1f5e1dfcd70419b711cdb47d6b4bb4ad4b76777a0563"},
|
||||
{file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a7fb111eef4d05909b82152721a59c1b14d0f365e2be4c742a473c5d7372f4f5"},
|
||||
{file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:43d549b876ce64aa18b2328faff70f5877f8c6dede415f80a2f799d31644d776"},
|
||||
{file = "lxml-5.4.0-cp310-cp310-win32.whl", hash = "sha256:75133890e40d229d6c5837b0312abbe5bac1c342452cf0e12523477cd3aa21e7"},
|
||||
{file = "lxml-5.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:de5b4e1088523e2b6f730d0509a9a813355b7f5659d70eb4f319c76beea2e250"},
|
||||
{file = "lxml-5.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:98a3912194c079ef37e716ed228ae0dcb960992100461b704aea4e93af6b0bb9"},
|
||||
{file = "lxml-5.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ea0252b51d296a75f6118ed0d8696888e7403408ad42345d7dfd0d1e93309a7"},
|
||||
{file = "lxml-5.4.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b92b69441d1bd39f4940f9eadfa417a25862242ca2c396b406f9272ef09cdcaa"},
|
||||
{file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20e16c08254b9b6466526bc1828d9370ee6c0d60a4b64836bc3ac2917d1e16df"},
|
||||
{file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7605c1c32c3d6e8c990dd28a0970a3cbbf1429d5b92279e37fda05fb0c92190e"},
|
||||
{file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ecf4c4b83f1ab3d5a7ace10bafcb6f11df6156857a3c418244cef41ca9fa3e44"},
|
||||
{file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cef4feae82709eed352cd7e97ae062ef6ae9c7b5dbe3663f104cd2c0e8d94ba"},
|
||||
{file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:df53330a3bff250f10472ce96a9af28628ff1f4efc51ccba351a8820bca2a8ba"},
|
||||
{file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:aefe1a7cb852fa61150fcb21a8c8fcea7b58c4cb11fbe59c97a0a4b31cae3c8c"},
|
||||
{file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ef5a7178fcc73b7d8c07229e89f8eb45b2908a9238eb90dcfc46571ccf0383b8"},
|
||||
{file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d2ed1b3cb9ff1c10e6e8b00941bb2e5bb568b307bfc6b17dffbbe8be5eecba86"},
|
||||
{file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:72ac9762a9f8ce74c9eed4a4e74306f2f18613a6b71fa065495a67ac227b3056"},
|
||||
{file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f5cb182f6396706dc6cc1896dd02b1c889d644c081b0cdec38747573db88a7d7"},
|
||||
{file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:3a3178b4873df8ef9457a4875703488eb1622632a9cee6d76464b60e90adbfcd"},
|
||||
{file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e094ec83694b59d263802ed03a8384594fcce477ce484b0cbcd0008a211ca751"},
|
||||
{file = "lxml-5.4.0-cp311-cp311-win32.whl", hash = "sha256:4329422de653cdb2b72afa39b0aa04252fca9071550044904b2e7036d9d97fe4"},
|
||||
{file = "lxml-5.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd3be6481ef54b8cfd0e1e953323b7aa9d9789b94842d0e5b142ef4bb7999539"},
|
||||
{file = "lxml-5.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b5aff6f3e818e6bdbbb38e5967520f174b18f539c2b9de867b1e7fde6f8d95a4"},
|
||||
{file = "lxml-5.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942a5d73f739ad7c452bf739a62a0f83e2578afd6b8e5406308731f4ce78b16d"},
|
||||
{file = "lxml-5.4.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:460508a4b07364d6abf53acaa0a90b6d370fafde5693ef37602566613a9b0779"},
|
||||
{file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529024ab3a505fed78fe3cc5ddc079464e709f6c892733e3f5842007cec8ac6e"},
|
||||
{file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ca56ebc2c474e8f3d5761debfd9283b8b18c76c4fc0967b74aeafba1f5647f9"},
|
||||
{file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a81e1196f0a5b4167a8dafe3a66aa67c4addac1b22dc47947abd5d5c7a3f24b5"},
|
||||
{file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00b8686694423ddae324cf614e1b9659c2edb754de617703c3d29ff568448df5"},
|
||||
{file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c5681160758d3f6ac5b4fea370495c48aac0989d6a0f01bb9a72ad8ef5ab75c4"},
|
||||
{file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:2dc191e60425ad70e75a68c9fd90ab284df64d9cd410ba8d2b641c0c45bc006e"},
|
||||
{file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:67f779374c6b9753ae0a0195a892a1c234ce8416e4448fe1e9f34746482070a7"},
|
||||
{file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:79d5bfa9c1b455336f52343130b2067164040604e41f6dc4d8313867ed540079"},
|
||||
{file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3d3c30ba1c9b48c68489dc1829a6eede9873f52edca1dda900066542528d6b20"},
|
||||
{file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1af80c6316ae68aded77e91cd9d80648f7dd40406cef73df841aa3c36f6907c8"},
|
||||
{file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4d885698f5019abe0de3d352caf9466d5de2baded00a06ef3f1216c1a58ae78f"},
|
||||
{file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea53d51859b6c64e7c51d522c03cc2c48b9b5d6172126854cc7f01aa11f52bc"},
|
||||
{file = "lxml-5.4.0-cp312-cp312-win32.whl", hash = "sha256:d90b729fd2732df28130c064aac9bb8aff14ba20baa4aee7bd0795ff1187545f"},
|
||||
{file = "lxml-5.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1dc4ca99e89c335a7ed47d38964abcb36c5910790f9bd106f2a8fa2ee0b909d2"},
|
||||
{file = "lxml-5.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:773e27b62920199c6197130632c18fb7ead3257fce1ffb7d286912e56ddb79e0"},
|
||||
{file = "lxml-5.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ce9c671845de9699904b1e9df95acfe8dfc183f2310f163cdaa91a3535af95de"},
|
||||
{file = "lxml-5.4.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9454b8d8200ec99a224df8854786262b1bd6461f4280064c807303c642c05e76"},
|
||||
{file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cccd007d5c95279e529c146d095f1d39ac05139de26c098166c4beb9374b0f4d"},
|
||||
{file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fce1294a0497edb034cb416ad3e77ecc89b313cff7adbee5334e4dc0d11f422"},
|
||||
{file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24974f774f3a78ac12b95e3a20ef0931795ff04dbb16db81a90c37f589819551"},
|
||||
{file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:497cab4d8254c2a90bf988f162ace2ddbfdd806fce3bda3f581b9d24c852e03c"},
|
||||
{file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e794f698ae4c5084414efea0f5cc9f4ac562ec02d66e1484ff822ef97c2cadff"},
|
||||
{file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:2c62891b1ea3094bb12097822b3d44b93fc6c325f2043c4d2736a8ff09e65f60"},
|
||||
{file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:142accb3e4d1edae4b392bd165a9abdee8a3c432a2cca193df995bc3886249c8"},
|
||||
{file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1a42b3a19346e5601d1b8296ff6ef3d76038058f311902edd574461e9c036982"},
|
||||
{file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4291d3c409a17febf817259cb37bc62cb7eb398bcc95c1356947e2871911ae61"},
|
||||
{file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4f5322cf38fe0e21c2d73901abf68e6329dc02a4994e483adbcf92b568a09a54"},
|
||||
{file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0be91891bdb06ebe65122aa6bf3fc94489960cf7e03033c6f83a90863b23c58b"},
|
||||
{file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:15a665ad90054a3d4f397bc40f73948d48e36e4c09f9bcffc7d90c87410e478a"},
|
||||
{file = "lxml-5.4.0-cp313-cp313-win32.whl", hash = "sha256:d5663bc1b471c79f5c833cffbc9b87d7bf13f87e055a5c86c363ccd2348d7e82"},
|
||||
{file = "lxml-5.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:bcb7a1096b4b6b24ce1ac24d4942ad98f983cd3810f9711bcd0293f43a9d8b9f"},
|
||||
{file = "lxml-5.4.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:7be701c24e7f843e6788353c055d806e8bd8466b52907bafe5d13ec6a6dbaecd"},
|
||||
{file = "lxml-5.4.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb54f7c6bafaa808f27166569b1511fc42701a7713858dddc08afdde9746849e"},
|
||||
{file = "lxml-5.4.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97dac543661e84a284502e0cf8a67b5c711b0ad5fb661d1bd505c02f8cf716d7"},
|
||||
{file = "lxml-5.4.0-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:c70e93fba207106cb16bf852e421c37bbded92acd5964390aad07cb50d60f5cf"},
|
||||
{file = "lxml-5.4.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9c886b481aefdf818ad44846145f6eaf373a20d200b5ce1a5c8e1bc2d8745410"},
|
||||
{file = "lxml-5.4.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:fa0e294046de09acd6146be0ed6727d1f42ded4ce3ea1e9a19c11b6774eea27c"},
|
||||
{file = "lxml-5.4.0-cp36-cp36m-win32.whl", hash = "sha256:61c7bbf432f09ee44b1ccaa24896d21075e533cd01477966a5ff5a71d88b2f56"},
|
||||
{file = "lxml-5.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:7ce1a171ec325192c6a636b64c94418e71a1964f56d002cc28122fceff0b6121"},
|
||||
{file = "lxml-5.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:795f61bcaf8770e1b37eec24edf9771b307df3af74d1d6f27d812e15a9ff3872"},
|
||||
{file = "lxml-5.4.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29f451a4b614a7b5b6c2e043d7b64a15bd8304d7e767055e8ab68387a8cacf4e"},
|
||||
{file = "lxml-5.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:891f7f991a68d20c75cb13c5c9142b2a3f9eb161f1f12a9489c82172d1f133c0"},
|
||||
{file = "lxml-5.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4aa412a82e460571fad592d0f93ce9935a20090029ba08eca05c614f99b0cc92"},
|
||||
{file = "lxml-5.4.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:ac7ba71f9561cd7d7b55e1ea5511543c0282e2b6450f122672a2694621d63b7e"},
|
||||
{file = "lxml-5.4.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:c5d32f5284012deaccd37da1e2cd42f081feaa76981f0eaa474351b68df813c5"},
|
||||
{file = "lxml-5.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:ce31158630a6ac85bddd6b830cffd46085ff90498b397bd0a259f59d27a12188"},
|
||||
{file = "lxml-5.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:31e63621e073e04697c1b2d23fcb89991790eef370ec37ce4d5d469f40924ed6"},
|
||||
{file = "lxml-5.4.0-cp37-cp37m-win32.whl", hash = "sha256:be2ba4c3c5b7900246a8f866580700ef0d538f2ca32535e991027bdaba944063"},
|
||||
{file = "lxml-5.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:09846782b1ef650b321484ad429217f5154da4d6e786636c38e434fa32e94e49"},
|
||||
{file = "lxml-5.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:eaf24066ad0b30917186420d51e2e3edf4b0e2ea68d8cd885b14dc8afdcf6556"},
|
||||
{file = "lxml-5.4.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b31a3a77501d86d8ade128abb01082724c0dfd9524f542f2f07d693c9f1175f"},
|
||||
{file = "lxml-5.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e108352e203c7afd0eb91d782582f00a0b16a948d204d4dec8565024fafeea5"},
|
||||
{file = "lxml-5.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a11a96c3b3f7551c8a8109aa65e8594e551d5a84c76bf950da33d0fb6dfafab7"},
|
||||
{file = "lxml-5.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:ca755eebf0d9e62d6cb013f1261e510317a41bf4650f22963474a663fdfe02aa"},
|
||||
{file = "lxml-5.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:4cd915c0fb1bed47b5e6d6edd424ac25856252f09120e3e8ba5154b6b921860e"},
|
||||
{file = "lxml-5.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:226046e386556a45ebc787871d6d2467b32c37ce76c2680f5c608e25823ffc84"},
|
||||
{file = "lxml-5.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:b108134b9667bcd71236c5a02aad5ddd073e372fb5d48ea74853e009fe38acb6"},
|
||||
{file = "lxml-5.4.0-cp38-cp38-win32.whl", hash = "sha256:1320091caa89805df7dcb9e908add28166113dcd062590668514dbd510798c88"},
|
||||
{file = "lxml-5.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:073eb6dcdf1f587d9b88c8c93528b57eccda40209cf9be549d469b942b41d70b"},
|
||||
{file = "lxml-5.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bda3ea44c39eb74e2488297bb39d47186ed01342f0022c8ff407c250ac3f498e"},
|
||||
{file = "lxml-5.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9ceaf423b50ecfc23ca00b7f50b64baba85fb3fb91c53e2c9d00bc86150c7e40"},
|
||||
{file = "lxml-5.4.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:664cdc733bc87449fe781dbb1f309090966c11cc0c0cd7b84af956a02a8a4729"},
|
||||
{file = "lxml-5.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67ed8a40665b84d161bae3181aa2763beea3747f748bca5874b4af4d75998f87"},
|
||||
{file = "lxml-5.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b4a3bd174cc9cdaa1afbc4620c049038b441d6ba07629d89a83b408e54c35cd"},
|
||||
{file = "lxml-5.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:b0989737a3ba6cf2a16efb857fb0dfa20bc5c542737fddb6d893fde48be45433"},
|
||||
{file = "lxml-5.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:dc0af80267edc68adf85f2a5d9be1cdf062f973db6790c1d065e45025fa26140"},
|
||||
{file = "lxml-5.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:639978bccb04c42677db43c79bdaa23785dc7f9b83bfd87570da8207872f1ce5"},
|
||||
{file = "lxml-5.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5a99d86351f9c15e4a901fc56404b485b1462039db59288b203f8c629260a142"},
|
||||
{file = "lxml-5.4.0-cp39-cp39-win32.whl", hash = "sha256:3e6d5557989cdc3ebb5302bbdc42b439733a841891762ded9514e74f60319ad6"},
|
||||
{file = "lxml-5.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:a8c9b7f16b63e65bbba889acb436a1034a82d34fa09752d754f88d708eca80e1"},
|
||||
{file = "lxml-5.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1b717b00a71b901b4667226bba282dd462c42ccf618ade12f9ba3674e1fabc55"},
|
||||
{file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27a9ded0f0b52098ff89dd4c418325b987feed2ea5cc86e8860b0f844285d740"},
|
||||
{file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b7ce10634113651d6f383aa712a194179dcd496bd8c41e191cec2099fa09de5"},
|
||||
{file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53370c26500d22b45182f98847243efb518d268374a9570409d2e2276232fd37"},
|
||||
{file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c6364038c519dffdbe07e3cf42e6a7f8b90c275d4d1617a69bb59734c1a2d571"},
|
||||
{file = "lxml-5.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b12cb6527599808ada9eb2cd6e0e7d3d8f13fe7bbb01c6311255a15ded4c7ab4"},
|
||||
{file = "lxml-5.4.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5f11a1526ebd0dee85e7b1e39e39a0cc0d9d03fb527f56d8457f6df48a10dc0c"},
|
||||
{file = "lxml-5.4.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48b4afaf38bf79109bb060d9016fad014a9a48fb244e11b94f74ae366a64d252"},
|
||||
{file = "lxml-5.4.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de6f6bb8a7840c7bf216fb83eec4e2f79f7325eca8858167b68708b929ab2172"},
|
||||
{file = "lxml-5.4.0-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5cca36a194a4eb4e2ed6be36923d3cffd03dcdf477515dea687185506583d4c9"},
|
||||
{file = "lxml-5.4.0-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b7c86884ad23d61b025989d99bfdd92a7351de956e01c61307cb87035960bcb1"},
|
||||
{file = "lxml-5.4.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:53d9469ab5460402c19553b56c3648746774ecd0681b1b27ea74d5d8a3ef5590"},
|
||||
{file = "lxml-5.4.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:56dbdbab0551532bb26c19c914848d7251d73edb507c3079d6805fa8bba5b706"},
|
||||
{file = "lxml-5.4.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14479c2ad1cb08b62bb941ba8e0e05938524ee3c3114644df905d2331c76cd57"},
|
||||
{file = "lxml-5.4.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32697d2ea994e0db19c1df9e40275ffe84973e4232b5c274f47e7c1ec9763cdd"},
|
||||
{file = "lxml-5.4.0-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:24f6df5f24fc3385f622c0c9d63fe34604893bc1a5bdbb2dbf5870f85f9a404a"},
|
||||
{file = "lxml-5.4.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:151d6c40bc9db11e960619d2bf2ec5829f0aaffb10b41dcf6ad2ce0f3c0b2325"},
|
||||
{file = "lxml-5.4.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4025bf2884ac4370a3243c5aa8d66d3cb9e15d3ddd0af2d796eccc5f0244390e"},
|
||||
{file = "lxml-5.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9459e6892f59ecea2e2584ee1058f5d8f629446eab52ba2305ae13a32a059530"},
|
||||
{file = "lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47fb24cc0f052f0576ea382872b3fc7e1f7e3028e53299ea751839418ade92a6"},
|
||||
{file = "lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50441c9de951a153c698b9b99992e806b71c1f36d14b154592580ff4a9d0d877"},
|
||||
{file = "lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:ab339536aa798b1e17750733663d272038bf28069761d5be57cb4a9b0137b4f8"},
|
||||
{file = "lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9776af1aad5a4b4a1317242ee2bea51da54b2a7b7b48674be736d463c999f37d"},
|
||||
{file = "lxml-5.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:63e7968ff83da2eb6fdda967483a7a023aa497d85ad8f05c3ad9b1f2e8c84987"},
|
||||
{file = "lxml-5.4.0.tar.gz", hash = "sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd"},
|
||||
{file = "lxml-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:35bc626eec405f745199200ccb5c6b36f202675d204aa29bb52e27ba2b71dea8"},
|
||||
{file = "lxml-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:246b40f8a4aec341cbbf52617cad8ab7c888d944bfe12a6abd2b1f6cfb6f6082"},
|
||||
{file = "lxml-6.0.0-cp310-cp310-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:2793a627e95d119e9f1e19720730472f5543a6d84c50ea33313ce328d870f2dd"},
|
||||
{file = "lxml-6.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:46b9ed911f36bfeb6338e0b482e7fe7c27d362c52fde29f221fddbc9ee2227e7"},
|
||||
{file = "lxml-6.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b4790b558bee331a933e08883c423f65bbcd07e278f91b2272489e31ab1e2b4"},
|
||||
{file = "lxml-6.0.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2030956cf4886b10be9a0285c6802e078ec2391e1dd7ff3eb509c2c95a69b76"},
|
||||
{file = "lxml-6.0.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d23854ecf381ab1facc8f353dcd9adeddef3652268ee75297c1164c987c11dc"},
|
||||
{file = "lxml-6.0.0-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:43fe5af2d590bf4691531b1d9a2495d7aab2090547eaacd224a3afec95706d76"},
|
||||
{file = "lxml-6.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74e748012f8c19b47f7d6321ac929a9a94ee92ef12bc4298c47e8b7219b26541"},
|
||||
{file = "lxml-6.0.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:43cfbb7db02b30ad3926e8fceaef260ba2fb7df787e38fa2df890c1ca7966c3b"},
|
||||
{file = "lxml-6.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:34190a1ec4f1e84af256495436b2d196529c3f2094f0af80202947567fdbf2e7"},
|
||||
{file = "lxml-6.0.0-cp310-cp310-win32.whl", hash = "sha256:5967fe415b1920a3877a4195e9a2b779249630ee49ece22021c690320ff07452"},
|
||||
{file = "lxml-6.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:f3389924581d9a770c6caa4df4e74b606180869043b9073e2cec324bad6e306e"},
|
||||
{file = "lxml-6.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:522fe7abb41309e9543b0d9b8b434f2b630c5fdaf6482bee642b34c8c70079c8"},
|
||||
{file = "lxml-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4ee56288d0df919e4aac43b539dd0e34bb55d6a12a6562038e8d6f3ed07f9e36"},
|
||||
{file = "lxml-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8dd6dd0e9c1992613ccda2bcb74fc9d49159dbe0f0ca4753f37527749885c25"},
|
||||
{file = "lxml-6.0.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:d7ae472f74afcc47320238b5dbfd363aba111a525943c8a34a1b657c6be934c3"},
|
||||
{file = "lxml-6.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5592401cdf3dc682194727c1ddaa8aa0f3ddc57ca64fd03226a430b955eab6f6"},
|
||||
{file = "lxml-6.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58ffd35bd5425c3c3b9692d078bf7ab851441434531a7e517c4984d5634cd65b"},
|
||||
{file = "lxml-6.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f720a14aa102a38907c6d5030e3d66b3b680c3e6f6bc95473931ea3c00c59967"},
|
||||
{file = "lxml-6.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2a5e8d207311a0170aca0eb6b160af91adc29ec121832e4ac151a57743a1e1e"},
|
||||
{file = "lxml-6.0.0-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:2dd1cc3ea7e60bfb31ff32cafe07e24839df573a5e7c2d33304082a5019bcd58"},
|
||||
{file = "lxml-6.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2cfcf84f1defed7e5798ef4f88aa25fcc52d279be731ce904789aa7ccfb7e8d2"},
|
||||
{file = "lxml-6.0.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:a52a4704811e2623b0324a18d41ad4b9fabf43ce5ff99b14e40a520e2190c851"},
|
||||
{file = "lxml-6.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c16304bba98f48a28ae10e32a8e75c349dd742c45156f297e16eeb1ba9287a1f"},
|
||||
{file = "lxml-6.0.0-cp311-cp311-win32.whl", hash = "sha256:f8d19565ae3eb956d84da3ef367aa7def14a2735d05bd275cd54c0301f0d0d6c"},
|
||||
{file = "lxml-6.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:b2d71cdefda9424adff9a3607ba5bbfc60ee972d73c21c7e3c19e71037574816"},
|
||||
{file = "lxml-6.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:8a2e76efbf8772add72d002d67a4c3d0958638696f541734304c7f28217a9cab"},
|
||||
{file = "lxml-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78718d8454a6e928470d511bf8ac93f469283a45c354995f7d19e77292f26108"},
|
||||
{file = "lxml-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:84ef591495ffd3f9dcabffd6391db7bb70d7230b5c35ef5148354a134f56f2be"},
|
||||
{file = "lxml-6.0.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:2930aa001a3776c3e2601cb8e0a15d21b8270528d89cc308be4843ade546b9ab"},
|
||||
{file = "lxml-6.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:219e0431ea8006e15005767f0351e3f7f9143e793e58519dc97fe9e07fae5563"},
|
||||
{file = "lxml-6.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bd5913b4972681ffc9718bc2d4c53cde39ef81415e1671ff93e9aa30b46595e7"},
|
||||
{file = "lxml-6.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:390240baeb9f415a82eefc2e13285016f9c8b5ad71ec80574ae8fa9605093cd7"},
|
||||
{file = "lxml-6.0.0-cp312-cp312-manylinux_2_27_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d6e200909a119626744dd81bae409fc44134389e03fbf1d68ed2a55a2fb10991"},
|
||||
{file = "lxml-6.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ca50bd612438258a91b5b3788c6621c1f05c8c478e7951899f492be42defc0da"},
|
||||
{file = "lxml-6.0.0-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:c24b8efd9c0f62bad0439283c2c795ef916c5a6b75f03c17799775c7ae3c0c9e"},
|
||||
{file = "lxml-6.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:afd27d8629ae94c5d863e32ab0e1d5590371d296b87dae0a751fb22bf3685741"},
|
||||
{file = "lxml-6.0.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:54c4855eabd9fc29707d30141be99e5cd1102e7d2258d2892314cf4c110726c3"},
|
||||
{file = "lxml-6.0.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c907516d49f77f6cd8ead1322198bdfd902003c3c330c77a1c5f3cc32a0e4d16"},
|
||||
{file = "lxml-6.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36531f81c8214e293097cd2b7873f178997dae33d3667caaae8bdfb9666b76c0"},
|
||||
{file = "lxml-6.0.0-cp312-cp312-win32.whl", hash = "sha256:690b20e3388a7ec98e899fd54c924e50ba6693874aa65ef9cb53de7f7de9d64a"},
|
||||
{file = "lxml-6.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:310b719b695b3dd442cdfbbe64936b2f2e231bb91d998e99e6f0daf991a3eba3"},
|
||||
{file = "lxml-6.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:8cb26f51c82d77483cdcd2b4a53cda55bbee29b3c2f3ddeb47182a2a9064e4eb"},
|
||||
{file = "lxml-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6da7cd4f405fd7db56e51e96bff0865b9853ae70df0e6720624049da76bde2da"},
|
||||
{file = "lxml-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b34339898bb556a2351a1830f88f751679f343eabf9cf05841c95b165152c9e7"},
|
||||
{file = "lxml-6.0.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:51a5e4c61a4541bd1cd3ba74766d0c9b6c12d6a1a4964ef60026832aac8e79b3"},
|
||||
{file = "lxml-6.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d18a25b19ca7307045581b18b3ec9ead2b1db5ccd8719c291f0cd0a5cec6cb81"},
|
||||
{file = "lxml-6.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d4f0c66df4386b75d2ab1e20a489f30dc7fd9a06a896d64980541506086be1f1"},
|
||||
{file = "lxml-6.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f4b481b6cc3a897adb4279216695150bbe7a44c03daba3c894f49d2037e0a24"},
|
||||
{file = "lxml-6.0.0-cp313-cp313-manylinux_2_27_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a78d6c9168f5bcb20971bf3329c2b83078611fbe1f807baadc64afc70523b3a"},
|
||||
{file = "lxml-6.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae06fbab4f1bb7db4f7c8ca9897dc8db4447d1a2b9bee78474ad403437bcc29"},
|
||||
{file = "lxml-6.0.0-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:1fa377b827ca2023244a06554c6e7dc6828a10aaf74ca41965c5d8a4925aebb4"},
|
||||
{file = "lxml-6.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1676b56d48048a62ef77a250428d1f31f610763636e0784ba67a9740823988ca"},
|
||||
{file = "lxml-6.0.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:0e32698462aacc5c1cf6bdfebc9c781821b7e74c79f13e5ffc8bfe27c42b1abf"},
|
||||
{file = "lxml-6.0.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4d6036c3a296707357efb375cfc24bb64cd955b9ec731abf11ebb1e40063949f"},
|
||||
{file = "lxml-6.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7488a43033c958637b1a08cddc9188eb06d3ad36582cebc7d4815980b47e27ef"},
|
||||
{file = "lxml-6.0.0-cp313-cp313-win32.whl", hash = "sha256:5fcd7d3b1d8ecb91445bd71b9c88bdbeae528fefee4f379895becfc72298d181"},
|
||||
{file = "lxml-6.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:2f34687222b78fff795feeb799a7d44eca2477c3d9d3a46ce17d51a4f383e32e"},
|
||||
{file = "lxml-6.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:21db1ec5525780fd07251636eb5f7acb84003e9382c72c18c542a87c416ade03"},
|
||||
{file = "lxml-6.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4eb114a0754fd00075c12648d991ec7a4357f9cb873042cc9a77bf3a7e30c9db"},
|
||||
{file = "lxml-6.0.0-cp38-cp38-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:7da298e1659e45d151b4028ad5c7974917e108afb48731f4ed785d02b6818994"},
|
||||
{file = "lxml-6.0.0-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7bf61bc4345c1895221357af8f3e89f8c103d93156ef326532d35c707e2fb19d"},
|
||||
{file = "lxml-6.0.0-cp38-cp38-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63b634facdfbad421d4b61c90735688465d4ab3a8853ac22c76ccac2baf98d97"},
|
||||
{file = "lxml-6.0.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:e380e85b93f148ad28ac15f8117e2fd8e5437aa7732d65e260134f83ce67911b"},
|
||||
{file = "lxml-6.0.0-cp38-cp38-win32.whl", hash = "sha256:185efc2fed89cdd97552585c624d3c908f0464090f4b91f7d92f8ed2f3b18f54"},
|
||||
{file = "lxml-6.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:f97487996a39cb18278ca33f7be98198f278d0bc3c5d0fd4d7b3d63646ca3c8a"},
|
||||
{file = "lxml-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:85b14a4689d5cff426c12eefe750738648706ea2753b20c2f973b2a000d3d261"},
|
||||
{file = "lxml-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f64ccf593916e93b8d36ed55401bb7fe9c7d5de3180ce2e10b08f82a8f397316"},
|
||||
{file = "lxml-6.0.0-cp39-cp39-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:b372d10d17a701b0945f67be58fae4664fd056b85e0ff0fbc1e6c951cdbc0512"},
|
||||
{file = "lxml-6.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a674c0948789e9136d69065cc28009c1b1874c6ea340253db58be7622ce6398f"},
|
||||
{file = "lxml-6.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:edf6e4c8fe14dfe316939711e3ece3f9a20760aabf686051b537a7562f4da91a"},
|
||||
{file = "lxml-6.0.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:048a930eb4572829604982e39a0c7289ab5dc8abc7fc9f5aabd6fbc08c154e93"},
|
||||
{file = "lxml-6.0.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0b5fa5eda84057a4f1bbb4bb77a8c28ff20ae7ce211588d698ae453e13c6281"},
|
||||
{file = "lxml-6.0.0-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:c352fc8f36f7e9727db17adbf93f82499457b3d7e5511368569b4c5bd155a922"},
|
||||
{file = "lxml-6.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8db5dc617cb937ae17ff3403c3a70a7de9df4852a046f93e71edaec678f721d0"},
|
||||
{file = "lxml-6.0.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:2181e4b1d07dde53986023482673c0f1fba5178ef800f9ab95ad791e8bdded6a"},
|
||||
{file = "lxml-6.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b3c98d5b24c6095e89e03d65d5c574705be3d49c0d8ca10c17a8a4b5201b72f5"},
|
||||
{file = "lxml-6.0.0-cp39-cp39-win32.whl", hash = "sha256:04d67ceee6db4bcb92987ccb16e53bef6b42ced872509f333c04fb58a3315256"},
|
||||
{file = "lxml-6.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:e0b1520ef900e9ef62e392dd3d7ae4f5fa224d1dd62897a792cf353eb20b6cae"},
|
||||
{file = "lxml-6.0.0-cp39-cp39-win_arm64.whl", hash = "sha256:e35e8aaaf3981489f42884b59726693de32dabfc438ac10ef4eb3409961fd402"},
|
||||
{file = "lxml-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:dbdd7679a6f4f08152818043dbb39491d1af3332128b3752c3ec5cebc0011a72"},
|
||||
{file = "lxml-6.0.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:40442e2a4456e9910875ac12951476d36c0870dcb38a68719f8c4686609897c4"},
|
||||
{file = "lxml-6.0.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db0efd6bae1c4730b9c863fc4f5f3c0fa3e8f05cae2c44ae141cb9dfc7d091dc"},
|
||||
{file = "lxml-6.0.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ab542c91f5a47aaa58abdd8ea84b498e8e49fe4b883d67800017757a3eb78e8"},
|
||||
{file = "lxml-6.0.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:013090383863b72c62a702d07678b658fa2567aa58d373d963cca245b017e065"},
|
||||
{file = "lxml-6.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c86df1c9af35d903d2b52d22ea3e66db8058d21dc0f59842ca5deb0595921141"},
|
||||
{file = "lxml-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4337e4aec93b7c011f7ee2e357b0d30562edd1955620fdd4aeab6aacd90d43c5"},
|
||||
{file = "lxml-6.0.0-pp39-pypy39_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ae74f7c762270196d2dda56f8dd7309411f08a4084ff2dfcc0b095a218df2e06"},
|
||||
{file = "lxml-6.0.0-pp39-pypy39_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:059c4cbf3973a621b62ea3132934ae737da2c132a788e6cfb9b08d63a0ef73f9"},
|
||||
{file = "lxml-6.0.0-pp39-pypy39_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17f090a9bc0ce8da51a5632092f98a7e7f84bca26f33d161a98b57f7fb0004ca"},
|
||||
{file = "lxml-6.0.0-pp39-pypy39_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9da022c14baeec36edfcc8daf0e281e2f55b950249a455776f0d1adeeada4734"},
|
||||
{file = "lxml-6.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a55da151d0b0c6ab176b4e761670ac0e2667817a1e0dadd04a01d0561a219349"},
|
||||
{file = "lxml-6.0.0.tar.gz", hash = "sha256:032e65120339d44cdc3efc326c9f660f5f7205f3a535c1fdbf898b29ea01fb72"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
|
@ -1384,7 +1346,6 @@ cssselect = ["cssselect (>=0.7)"]
|
|||
html-clean = ["lxml_html_clean"]
|
||||
html5 = ["html5lib"]
|
||||
htmlsoup = ["BeautifulSoup4"]
|
||||
source = ["Cython (>=3.0.11,<3.1.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "lxml-html-clean"
|
||||
|
@ -1624,14 +1585,14 @@ pyyaml = ">=5.1"
|
|||
|
||||
[[package]]
|
||||
name = "mkdocs-material"
|
||||
version = "9.6.14"
|
||||
version = "9.6.15"
|
||||
description = "Documentation that simply works"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "mkdocs_material-9.6.14-py3-none-any.whl", hash = "sha256:3b9cee6d3688551bf7a8e8f41afda97a3c39a12f0325436d76c86706114b721b"},
|
||||
{file = "mkdocs_material-9.6.14.tar.gz", hash = "sha256:39d795e90dce6b531387c255bd07e866e027828b7346d3eba5ac3de265053754"},
|
||||
{file = "mkdocs_material-9.6.15-py3-none-any.whl", hash = "sha256:ac969c94d4fe5eb7c924b6d2f43d7db41159ea91553d18a9afc4780c34f2717a"},
|
||||
{file = "mkdocs_material-9.6.15.tar.gz", hash = "sha256:64adf8fa8dba1a17905b6aee1894a5aafd966d4aeb44a11088519b0f5ca4f1b5"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -1855,14 +1816,14 @@ signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"]
|
|||
|
||||
[[package]]
|
||||
name = "openai"
|
||||
version = "1.88.0"
|
||||
version = "1.93.0"
|
||||
description = "The official Python library for the openai API"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "openai-1.88.0-py3-none-any.whl", hash = "sha256:7edd7826b3b83f5846562a6f310f040c79576278bf8e3687b30ba05bb5dff978"},
|
||||
{file = "openai-1.88.0.tar.gz", hash = "sha256:122d35e42998255cf1fc84560f6ee49a844e65c054cd05d3e42fda506b832bb1"},
|
||||
{file = "openai-1.93.0-py3-none-any.whl", hash = "sha256:3d746fe5498f0dd72e0d9ab706f26c91c0f646bf7459e5629af8ba7c9dbdf090"},
|
||||
{file = "openai-1.93.0.tar.gz", hash = "sha256:988f31ade95e1ff0585af11cc5a64510225e4f5cd392698c675d0a9265b8e337"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -1876,6 +1837,7 @@ tqdm = ">4"
|
|||
typing-extensions = ">=4.11,<5"
|
||||
|
||||
[package.extras]
|
||||
aiohttp = ["aiohttp", "httpx-aiohttp (>=0.1.6)"]
|
||||
datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"]
|
||||
realtime = ["websockets (>=13,<16)"]
|
||||
voice-helpers = ["numpy (>=2.0.2)", "sounddevice (>=0.5.1)"]
|
||||
|
@ -2013,101 +1975,126 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "11.2.1"
|
||||
version = "11.3.0"
|
||||
description = "Python Imaging Library (Fork)"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "pillow-11.2.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:d57a75d53922fc20c165016a20d9c44f73305e67c351bbc60d1adaf662e74047"},
|
||||
{file = "pillow-11.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:127bf6ac4a5b58b3d32fc8289656f77f80567d65660bc46f72c0d77e6600cc95"},
|
||||
{file = "pillow-11.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4ba4be812c7a40280629e55ae0b14a0aafa150dd6451297562e1764808bbe61"},
|
||||
{file = "pillow-11.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8bd62331e5032bc396a93609982a9ab6b411c05078a52f5fe3cc59234a3abd1"},
|
||||
{file = "pillow-11.2.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:562d11134c97a62fe3af29581f083033179f7ff435f78392565a1ad2d1c2c45c"},
|
||||
{file = "pillow-11.2.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:c97209e85b5be259994eb5b69ff50c5d20cca0f458ef9abd835e262d9d88b39d"},
|
||||
{file = "pillow-11.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0c3e6d0f59171dfa2e25d7116217543310908dfa2770aa64b8f87605f8cacc97"},
|
||||
{file = "pillow-11.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc1c3bc53befb6096b84165956e886b1729634a799e9d6329a0c512ab651e579"},
|
||||
{file = "pillow-11.2.1-cp310-cp310-win32.whl", hash = "sha256:312c77b7f07ab2139924d2639860e084ec2a13e72af54d4f08ac843a5fc9c79d"},
|
||||
{file = "pillow-11.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:9bc7ae48b8057a611e5fe9f853baa88093b9a76303937449397899385da06fad"},
|
||||
{file = "pillow-11.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:2728567e249cdd939f6cc3d1f049595c66e4187f3c34078cbc0a7d21c47482d2"},
|
||||
{file = "pillow-11.2.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35ca289f712ccfc699508c4658a1d14652e8033e9b69839edf83cbdd0ba39e70"},
|
||||
{file = "pillow-11.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0409af9f829f87a2dfb7e259f78f317a5351f2045158be321fd135973fff7bf"},
|
||||
{file = "pillow-11.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4e5c5edee874dce4f653dbe59db7c73a600119fbea8d31f53423586ee2aafd7"},
|
||||
{file = "pillow-11.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b93a07e76d13bff9444f1a029e0af2964e654bfc2e2c2d46bfd080df5ad5f3d8"},
|
||||
{file = "pillow-11.2.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:e6def7eed9e7fa90fde255afaf08060dc4b343bbe524a8f69bdd2a2f0018f600"},
|
||||
{file = "pillow-11.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8f4f3724c068be008c08257207210c138d5f3731af6c155a81c2b09a9eb3a788"},
|
||||
{file = "pillow-11.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0a6709b47019dff32e678bc12c63008311b82b9327613f534e496dacaefb71e"},
|
||||
{file = "pillow-11.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f6b0c664ccb879109ee3ca702a9272d877f4fcd21e5eb63c26422fd6e415365e"},
|
||||
{file = "pillow-11.2.1-cp311-cp311-win32.whl", hash = "sha256:cc5d875d56e49f112b6def6813c4e3d3036d269c008bf8aef72cd08d20ca6df6"},
|
||||
{file = "pillow-11.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:0f5c7eda47bf8e3c8a283762cab94e496ba977a420868cb819159980b6709193"},
|
||||
{file = "pillow-11.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:4d375eb838755f2528ac8cbc926c3e31cc49ca4ad0cf79cff48b20e30634a4a7"},
|
||||
{file = "pillow-11.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78afba22027b4accef10dbd5eed84425930ba41b3ea0a86fa8d20baaf19d807f"},
|
||||
{file = "pillow-11.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78092232a4ab376a35d68c4e6d5e00dfd73454bd12b230420025fbe178ee3b0b"},
|
||||
{file = "pillow-11.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a5f306095c6780c52e6bbb6109624b95c5b18e40aab1c3041da3e9e0cd3e2d"},
|
||||
{file = "pillow-11.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c7b29dbd4281923a2bfe562acb734cee96bbb129e96e6972d315ed9f232bef4"},
|
||||
{file = "pillow-11.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3e645b020f3209a0181a418bffe7b4a93171eef6c4ef6cc20980b30bebf17b7d"},
|
||||
{file = "pillow-11.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b2dbea1012ccb784a65349f57bbc93730b96e85b42e9bf7b01ef40443db720b4"},
|
||||
{file = "pillow-11.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da3104c57bbd72948d75f6a9389e6727d2ab6333c3617f0a89d72d4940aa0443"},
|
||||
{file = "pillow-11.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:598174aef4589af795f66f9caab87ba4ff860ce08cd5bb447c6fc553ffee603c"},
|
||||
{file = "pillow-11.2.1-cp312-cp312-win32.whl", hash = "sha256:1d535df14716e7f8776b9e7fee118576d65572b4aad3ed639be9e4fa88a1cad3"},
|
||||
{file = "pillow-11.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:14e33b28bf17c7a38eede290f77db7c664e4eb01f7869e37fa98a5aa95978941"},
|
||||
{file = "pillow-11.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:21e1470ac9e5739ff880c211fc3af01e3ae505859392bf65458c224d0bf283eb"},
|
||||
{file = "pillow-11.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28"},
|
||||
{file = "pillow-11.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830"},
|
||||
{file = "pillow-11.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0"},
|
||||
{file = "pillow-11.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1"},
|
||||
{file = "pillow-11.2.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f"},
|
||||
{file = "pillow-11.2.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155"},
|
||||
{file = "pillow-11.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14"},
|
||||
{file = "pillow-11.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b"},
|
||||
{file = "pillow-11.2.1-cp313-cp313-win32.whl", hash = "sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2"},
|
||||
{file = "pillow-11.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691"},
|
||||
{file = "pillow-11.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c"},
|
||||
{file = "pillow-11.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22"},
|
||||
{file = "pillow-11.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7"},
|
||||
{file = "pillow-11.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16"},
|
||||
{file = "pillow-11.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b"},
|
||||
{file = "pillow-11.2.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406"},
|
||||
{file = "pillow-11.2.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91"},
|
||||
{file = "pillow-11.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751"},
|
||||
{file = "pillow-11.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9"},
|
||||
{file = "pillow-11.2.1-cp313-cp313t-win32.whl", hash = "sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd"},
|
||||
{file = "pillow-11.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e"},
|
||||
{file = "pillow-11.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681"},
|
||||
{file = "pillow-11.2.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:7491cf8a79b8eb867d419648fff2f83cb0b3891c8b36da92cc7f1931d46108c8"},
|
||||
{file = "pillow-11.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b02d8f9cb83c52578a0b4beadba92e37d83a4ef11570a8688bbf43f4ca50909"},
|
||||
{file = "pillow-11.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:014ca0050c85003620526b0ac1ac53f56fc93af128f7546623cc8e31875ab928"},
|
||||
{file = "pillow-11.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3692b68c87096ac6308296d96354eddd25f98740c9d2ab54e1549d6c8aea9d79"},
|
||||
{file = "pillow-11.2.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:f781dcb0bc9929adc77bad571b8621ecb1e4cdef86e940fe2e5b5ee24fd33b35"},
|
||||
{file = "pillow-11.2.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:2b490402c96f907a166615e9a5afacf2519e28295f157ec3a2bb9bd57de638cb"},
|
||||
{file = "pillow-11.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dd6b20b93b3ccc9c1b597999209e4bc5cf2853f9ee66e3fc9a400a78733ffc9a"},
|
||||
{file = "pillow-11.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4b835d89c08a6c2ee7781b8dd0a30209a8012b5f09c0a665b65b0eb3560b6f36"},
|
||||
{file = "pillow-11.2.1-cp39-cp39-win32.whl", hash = "sha256:b10428b3416d4f9c61f94b494681280be7686bda15898a3a9e08eb66a6d92d67"},
|
||||
{file = "pillow-11.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:6ebce70c3f486acf7591a3d73431fa504a4e18a9b97ff27f5f47b7368e4b9dd1"},
|
||||
{file = "pillow-11.2.1-cp39-cp39-win_arm64.whl", hash = "sha256:c27476257b2fdcd7872d54cfd119b3a9ce4610fb85c8e32b70b42e3680a29a1e"},
|
||||
{file = "pillow-11.2.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9b7b0d4fd2635f54ad82785d56bc0d94f147096493a79985d0ab57aedd563156"},
|
||||
{file = "pillow-11.2.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:aa442755e31c64037aa7c1cb186e0b369f8416c567381852c63444dd666fb772"},
|
||||
{file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0d3348c95b766f54b76116d53d4cb171b52992a1027e7ca50c81b43b9d9e363"},
|
||||
{file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85d27ea4c889342f7e35f6d56e7e1cb345632ad592e8c51b693d7b7556043ce0"},
|
||||
{file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bf2c33d6791c598142f00c9c4c7d47f6476731c31081331664eb26d6ab583e01"},
|
||||
{file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e616e7154c37669fc1dfc14584f11e284e05d1c650e1c0f972f281c4ccc53193"},
|
||||
{file = "pillow-11.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:39ad2e0f424394e3aebc40168845fee52df1394a4673a6ee512d840d14ab3013"},
|
||||
{file = "pillow-11.2.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:80f1df8dbe9572b4b7abdfa17eb5d78dd620b1d55d9e25f834efdbee872d3aed"},
|
||||
{file = "pillow-11.2.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ea926cfbc3957090becbcbbb65ad177161a2ff2ad578b5a6ec9bb1e1cd78753c"},
|
||||
{file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:738db0e0941ca0376804d4de6a782c005245264edaa253ffce24e5a15cbdc7bd"},
|
||||
{file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db98ab6565c69082ec9b0d4e40dd9f6181dab0dd236d26f7a50b8b9bfbd5076"},
|
||||
{file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:036e53f4170e270ddb8797d4c590e6dd14d28e15c7da375c18978045f7e6c37b"},
|
||||
{file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:14f73f7c291279bd65fda51ee87affd7c1e097709f7fdd0188957a16c264601f"},
|
||||
{file = "pillow-11.2.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:208653868d5c9ecc2b327f9b9ef34e0e42a4cdd172c2988fd81d62d2bc9bc044"},
|
||||
{file = "pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6"},
|
||||
{file = "pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860"},
|
||||
{file = "pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad"},
|
||||
{file = "pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0"},
|
||||
{file = "pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b"},
|
||||
{file = "pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50"},
|
||||
{file = "pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae"},
|
||||
{file = "pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9"},
|
||||
{file = "pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e"},
|
||||
{file = "pillow-11.3.0-cp310-cp310-win32.whl", hash = "sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6"},
|
||||
{file = "pillow-11.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f"},
|
||||
{file = "pillow-11.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f"},
|
||||
{file = "pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722"},
|
||||
{file = "pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288"},
|
||||
{file = "pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d"},
|
||||
{file = "pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494"},
|
||||
{file = "pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58"},
|
||||
{file = "pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f"},
|
||||
{file = "pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e"},
|
||||
{file = "pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94"},
|
||||
{file = "pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0"},
|
||||
{file = "pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac"},
|
||||
{file = "pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd"},
|
||||
{file = "pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4"},
|
||||
{file = "pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69"},
|
||||
{file = "pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d"},
|
||||
{file = "pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6"},
|
||||
{file = "pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7"},
|
||||
{file = "pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024"},
|
||||
{file = "pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809"},
|
||||
{file = "pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d"},
|
||||
{file = "pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149"},
|
||||
{file = "pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d"},
|
||||
{file = "pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542"},
|
||||
{file = "pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd"},
|
||||
{file = "pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8"},
|
||||
{file = "pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f"},
|
||||
{file = "pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c"},
|
||||
{file = "pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd"},
|
||||
{file = "pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e"},
|
||||
{file = "pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1"},
|
||||
{file = "pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805"},
|
||||
{file = "pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8"},
|
||||
{file = "pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2"},
|
||||
{file = "pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b"},
|
||||
{file = "pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3"},
|
||||
{file = "pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51"},
|
||||
{file = "pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580"},
|
||||
{file = "pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e"},
|
||||
{file = "pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d"},
|
||||
{file = "pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced"},
|
||||
{file = "pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c"},
|
||||
{file = "pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8"},
|
||||
{file = "pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59"},
|
||||
{file = "pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe"},
|
||||
{file = "pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c"},
|
||||
{file = "pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788"},
|
||||
{file = "pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31"},
|
||||
{file = "pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e"},
|
||||
{file = "pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12"},
|
||||
{file = "pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a"},
|
||||
{file = "pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632"},
|
||||
{file = "pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673"},
|
||||
{file = "pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027"},
|
||||
{file = "pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77"},
|
||||
{file = "pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874"},
|
||||
{file = "pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a"},
|
||||
{file = "pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214"},
|
||||
{file = "pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635"},
|
||||
{file = "pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6"},
|
||||
{file = "pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae"},
|
||||
{file = "pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653"},
|
||||
{file = "pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6"},
|
||||
{file = "pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36"},
|
||||
{file = "pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b"},
|
||||
{file = "pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477"},
|
||||
{file = "pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50"},
|
||||
{file = "pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b"},
|
||||
{file = "pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12"},
|
||||
{file = "pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db"},
|
||||
{file = "pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa"},
|
||||
{file = "pillow-11.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:48d254f8a4c776de343051023eb61ffe818299eeac478da55227d96e241de53f"},
|
||||
{file = "pillow-11.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7aee118e30a4cf54fdd873bd3a29de51e29105ab11f9aad8c32123f58c8f8081"},
|
||||
{file = "pillow-11.3.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:23cff760a9049c502721bdb743a7cb3e03365fafcdfc2ef9784610714166e5a4"},
|
||||
{file = "pillow-11.3.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6359a3bc43f57d5b375d1ad54a0074318a0844d11b76abccf478c37c986d3cfc"},
|
||||
{file = "pillow-11.3.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:092c80c76635f5ecb10f3f83d76716165c96f5229addbd1ec2bdbbda7d496e06"},
|
||||
{file = "pillow-11.3.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cadc9e0ea0a2431124cde7e1697106471fc4c1da01530e679b2391c37d3fbb3a"},
|
||||
{file = "pillow-11.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6a418691000f2a418c9135a7cf0d797c1bb7d9a485e61fe8e7722845b95ef978"},
|
||||
{file = "pillow-11.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:97afb3a00b65cc0804d1c7abddbf090a81eaac02768af58cbdcaaa0a931e0b6d"},
|
||||
{file = "pillow-11.3.0-cp39-cp39-win32.whl", hash = "sha256:ea944117a7974ae78059fcc1800e5d3295172bb97035c0c1d9345fca1419da71"},
|
||||
{file = "pillow-11.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:e5c5858ad8ec655450a7c7df532e9842cf8df7cc349df7225c60d5d348c8aada"},
|
||||
{file = "pillow-11.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:6abdbfd3aea42be05702a8dd98832329c167ee84400a1d1f61ab11437f1717eb"},
|
||||
{file = "pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967"},
|
||||
{file = "pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe"},
|
||||
{file = "pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c"},
|
||||
{file = "pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25"},
|
||||
{file = "pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27"},
|
||||
{file = "pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a"},
|
||||
{file = "pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f"},
|
||||
{file = "pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6"},
|
||||
{file = "pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438"},
|
||||
{file = "pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3"},
|
||||
{file = "pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c"},
|
||||
{file = "pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361"},
|
||||
{file = "pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7"},
|
||||
{file = "pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8"},
|
||||
{file = "pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo", "olefile", "sphinx (>=8.2)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"]
|
||||
docs = ["furo", "olefile", "sphinx (>=8.2)", "sphinx-autobuild", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"]
|
||||
fpx = ["olefile"]
|
||||
mic = ["olefile"]
|
||||
test-arrow = ["pyarrow"]
|
||||
tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "trove-classifiers (>=2024.10.12)"]
|
||||
tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "trove-classifiers (>=2024.10.12)"]
|
||||
typing = ["typing-extensions ; python_version < \"3.10\""]
|
||||
xmp = ["defusedxml"]
|
||||
|
||||
|
@ -2511,14 +2498,14 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
|
|||
|
||||
[[package]]
|
||||
name = "pydantic-settings"
|
||||
version = "2.9.1"
|
||||
version = "2.10.1"
|
||||
description = "Settings management using Pydantic"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef"},
|
||||
{file = "pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268"},
|
||||
{file = "pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796"},
|
||||
{file = "pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -2675,14 +2662,14 @@ requests = ">=2.32.3"
|
|||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.4.0"
|
||||
version = "8.4.1"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pytest-8.4.0-py3-none-any.whl", hash = "sha256:f40f825768ad76c0977cbacdf1fd37c6f7a468e460ea6a0636078f8972d4517e"},
|
||||
{file = "pytest-8.4.0.tar.gz", hash = "sha256:14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6"},
|
||||
{file = "pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7"},
|
||||
{file = "pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -2799,14 +2786,14 @@ six = ">=1.5"
|
|||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
description = "Read key-value pairs from a .env file and set them as environment variables"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d"},
|
||||
{file = "python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5"},
|
||||
{file = "python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc"},
|
||||
{file = "python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
|
@ -3249,30 +3236,30 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
|
|||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.11.13"
|
||||
version = "0.12.2"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "ruff-0.11.13-py3-none-linux_armv6l.whl", hash = "sha256:4bdfbf1240533f40042ec00c9e09a3aade6f8c10b6414cf11b519488d2635d46"},
|
||||
{file = "ruff-0.11.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aef9c9ed1b5ca28bb15c7eac83b8670cf3b20b478195bd49c8d756ba0a36cf48"},
|
||||
{file = "ruff-0.11.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53b15a9dfdce029c842e9a5aebc3855e9ab7771395979ff85b7c1dedb53ddc2b"},
|
||||
{file = "ruff-0.11.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab153241400789138d13f362c43f7edecc0edfffce2afa6a68434000ecd8f69a"},
|
||||
{file = "ruff-0.11.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c51f93029d54a910d3d24f7dd0bb909e31b6cd989a5e4ac513f4eb41629f0dc"},
|
||||
{file = "ruff-0.11.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1808b3ed53e1a777c2ef733aca9051dc9bf7c99b26ece15cb59a0320fbdbd629"},
|
||||
{file = "ruff-0.11.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d28ce58b5ecf0f43c1b71edffabe6ed7f245d5336b17805803312ec9bc665933"},
|
||||
{file = "ruff-0.11.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55e4bc3a77842da33c16d55b32c6cac1ec5fb0fbec9c8c513bdce76c4f922165"},
|
||||
{file = "ruff-0.11.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:633bf2c6f35678c56ec73189ba6fa19ff1c5e4807a78bf60ef487b9dd272cc71"},
|
||||
{file = "ruff-0.11.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ffbc82d70424b275b089166310448051afdc6e914fdab90e08df66c43bb5ca9"},
|
||||
{file = "ruff-0.11.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a9ddd3ec62a9a89578c85842b836e4ac832d4a2e0bfaad3b02243f930ceafcc"},
|
||||
{file = "ruff-0.11.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d237a496e0778d719efb05058c64d28b757c77824e04ffe8796c7436e26712b7"},
|
||||
{file = "ruff-0.11.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26816a218ca6ef02142343fd24c70f7cd8c5aa6c203bca284407adf675984432"},
|
||||
{file = "ruff-0.11.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:51c3f95abd9331dc5b87c47ac7f376db5616041173826dfd556cfe3d4977f492"},
|
||||
{file = "ruff-0.11.13-py3-none-win32.whl", hash = "sha256:96c27935418e4e8e77a26bb05962817f28b8ef3843a6c6cc49d8783b5507f250"},
|
||||
{file = "ruff-0.11.13-py3-none-win_amd64.whl", hash = "sha256:29c3189895a8a6a657b7af4e97d330c8a3afd2c9c8f46c81e2fc5a31866517e3"},
|
||||
{file = "ruff-0.11.13-py3-none-win_arm64.whl", hash = "sha256:b4385285e9179d608ff1d2fb9922062663c658605819a6876d8beef0c30b7f3b"},
|
||||
{file = "ruff-0.11.13.tar.gz", hash = "sha256:26fa247dc68d1d4e72c179e08889a25ac0c7ba4d78aecfc835d49cbfd60bf514"},
|
||||
{file = "ruff-0.12.2-py3-none-linux_armv6l.whl", hash = "sha256:093ea2b221df1d2b8e7ad92fc6ffdca40a2cb10d8564477a987b44fd4008a7be"},
|
||||
{file = "ruff-0.12.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:09e4cf27cc10f96b1708100fa851e0daf21767e9709e1649175355280e0d950e"},
|
||||
{file = "ruff-0.12.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8ae64755b22f4ff85e9c52d1f82644abd0b6b6b6deedceb74bd71f35c24044cc"},
|
||||
{file = "ruff-0.12.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eb3a6b2db4d6e2c77e682f0b988d4d61aff06860158fdb413118ca133d57922"},
|
||||
{file = "ruff-0.12.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:73448de992d05517170fc37169cbca857dfeaeaa8c2b9be494d7bcb0d36c8f4b"},
|
||||
{file = "ruff-0.12.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b8b94317cbc2ae4a2771af641739f933934b03555e51515e6e021c64441532d"},
|
||||
{file = "ruff-0.12.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:45fc42c3bf1d30d2008023a0a9a0cfb06bf9835b147f11fe0679f21ae86d34b1"},
|
||||
{file = "ruff-0.12.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce48f675c394c37e958bf229fb5c1e843e20945a6d962cf3ea20b7a107dcd9f4"},
|
||||
{file = "ruff-0.12.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:793d8859445ea47591272021a81391350205a4af65a9392401f418a95dfb75c9"},
|
||||
{file = "ruff-0.12.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6932323db80484dda89153da3d8e58164d01d6da86857c79f1961934354992da"},
|
||||
{file = "ruff-0.12.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6aa7e623a3a11538108f61e859ebf016c4f14a7e6e4eba1980190cacb57714ce"},
|
||||
{file = "ruff-0.12.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2a4a20aeed74671b2def096bdf2eac610c7d8ffcbf4fb0e627c06947a1d7078d"},
|
||||
{file = "ruff-0.12.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:71a4c550195612f486c9d1f2b045a600aeba851b298c667807ae933478fcef04"},
|
||||
{file = "ruff-0.12.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:4987b8f4ceadf597c927beee65a5eaf994c6e2b631df963f86d8ad1bdea99342"},
|
||||
{file = "ruff-0.12.2-py3-none-win32.whl", hash = "sha256:369ffb69b70cd55b6c3fc453b9492d98aed98062db9fec828cdfd069555f5f1a"},
|
||||
{file = "ruff-0.12.2-py3-none-win_amd64.whl", hash = "sha256:dca8a3b6d6dc9810ed8f328d406516bf4d660c00caeaef36eb831cf4871b0639"},
|
||||
{file = "ruff-0.12.2-py3-none-win_arm64.whl", hash = "sha256:48d6c6bfb4761df68bc05ae630e24f506755e702d4fb08f08460be778c7ccb12"},
|
||||
{file = "ruff-0.12.2.tar.gz", hash = "sha256:d7b4f55cd6f325cb7621244f19c873c565a08aff5a4ba9c69aa7355f3f7afd3e"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3612,14 +3599,14 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
|
|||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.34.3"
|
||||
version = "0.35.0"
|
||||
description = "The lightning-fast ASGI server."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "uvicorn-0.34.3-py3-none-any.whl", hash = "sha256:16246631db62bdfbf069b0645177d6e8a77ba950cfedbfd093acef9444e4d885"},
|
||||
{file = "uvicorn-0.34.3.tar.gz", hash = "sha256:35919a9a979d7a59334b6b10e05d77c1d0d574c50e0fc98b8b1a0f165708b55a"},
|
||||
{file = "uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a"},
|
||||
{file = "uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -3884,4 +3871,4 @@ pgsql = ["psycopg2-binary"]
|
|||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.12,<3.13"
|
||||
content-hash = "2b8479e18ef741f5254b8c9d64566bf42d597cfb6564c1aa622f6a1afb117402"
|
||||
content-hash = "7c0755d30c7245d3c7f4a86ef90db901a61b3f8d7ed39f59d7eb10f794136e02"
|
||||
|
|
|
@ -26,7 +26,7 @@ bcrypt = "^4.0.1"
|
|||
extruct = "^0.18.0"
|
||||
fastapi = "^0.115.0"
|
||||
httpx = "^0.28.0"
|
||||
lxml = "^5.0.0"
|
||||
lxml = "^6.0.0"
|
||||
orjson = "^3.8.0"
|
||||
psycopg2-binary = { version = "^2.9.1", optional = true }
|
||||
pydantic = "^2.6.1"
|
||||
|
@ -40,7 +40,7 @@ python-slugify = "^8.0.0"
|
|||
recipe-scrapers = "^15.0.0"
|
||||
requests = "^2.31.0"
|
||||
tzdata = "^2024.1"
|
||||
uvicorn = { extras = ["standard"], version = "^0.34.0" }
|
||||
uvicorn = { extras = ["standard"], version = "^0.35.0" }
|
||||
beautifulsoup4 = "^4.11.2"
|
||||
isodate = "^0.7.0"
|
||||
text-unidecode = "^1.3"
|
||||
|
@ -69,7 +69,7 @@ pylint = "^3.0.0"
|
|||
pytest = "^8.0.0"
|
||||
pytest-asyncio = "^1.0.0"
|
||||
rich = "^14.0.0"
|
||||
ruff = "^0.11.0"
|
||||
ruff = "^0.12.0"
|
||||
types-PyYAML = "^6.0.4"
|
||||
types-python-dateutil = "^2.8.18"
|
||||
types-python-slugify = "^6.0.0"
|
||||
|
|
8
tests/fixtures/fixture_users.py
vendored
8
tests/fixtures/fixture_users.py
vendored
|
@ -76,7 +76,7 @@ def h2_user(session: Session, admin_token, api_client: TestClient, unique_user:
|
|||
"admin": False,
|
||||
"tokens": [],
|
||||
}
|
||||
response = api_client.post(api_routes.users, json=user_data, headers=admin_token)
|
||||
response = api_client.post(api_routes.admin_users, json=user_data, headers=admin_token)
|
||||
assert response.status_code == 201
|
||||
|
||||
# Log in as this user
|
||||
|
@ -135,7 +135,7 @@ def g2_user(session: Session, admin_token, api_client: TestClient):
|
|||
}
|
||||
|
||||
api_client.post(api_routes.admin_groups, json={"name": group}, headers=admin_token)
|
||||
response = api_client.post(api_routes.users, json=create_data, headers=admin_token)
|
||||
response = api_client.post(api_routes.admin_users, json=create_data, headers=admin_token)
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
|
@ -258,7 +258,7 @@ def user_tuple(session: Session, admin_token, api_client: TestClient) -> Generat
|
|||
users_out = []
|
||||
|
||||
for usr in [create_data_1, create_data_2]:
|
||||
response = api_client.post(api_routes.users, json=usr, headers=admin_token)
|
||||
response = api_client.post(api_routes.admin_users, json=usr, headers=admin_token)
|
||||
assert response.status_code == 201
|
||||
|
||||
# Log in as this user
|
||||
|
@ -312,7 +312,7 @@ def user_token(admin_token, api_client: TestClient):
|
|||
"tokens": [],
|
||||
}
|
||||
|
||||
response = api_client.post(api_routes.users, json=create_data, headers=admin_token)
|
||||
response = api_client.post(api_routes.admin_users, json=create_data, headers=admin_token)
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ def test_api_token_private(api_client: TestClient, admin_token):
|
|||
response = api_client.post(api_routes.users_api_tokens, json={"name": "Test API Token"}, headers=admin_token)
|
||||
assert response.status_code == 201
|
||||
|
||||
response = api_client.get(api_routes.users, headers=admin_token, params={"perPage": -1})
|
||||
response = api_client.get(api_routes.admin_users, headers=admin_token, params={"perPage": -1})
|
||||
assert response.status_code == 200
|
||||
for user in response.json()["items"]:
|
||||
for user_token in user["tokens"] or []:
|
||||
|
@ -39,7 +39,7 @@ def test_api_token_private(api_client: TestClient, admin_token):
|
|||
|
||||
|
||||
def test_use_token(api_client: TestClient, long_live_token):
|
||||
response = api_client.get(api_routes.users, headers=long_live_token)
|
||||
response = api_client.get(api_routes.admin_users, headers=long_live_token)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
|
|
|
@ -88,9 +88,11 @@ def test_user_update(api_client: TestClient, unique_user: TestUser, admin_user:
|
|||
|
||||
|
||||
def test_admin_updates(api_client: TestClient, admin_user: TestUser, unique_user: TestUser):
|
||||
response = api_client.get(api_routes.users_item_id(unique_user.user_id), headers=admin_user.token)
|
||||
response = api_client.get(api_routes.admin_users_item_id(unique_user.user_id), headers=admin_user.token)
|
||||
assert response.status_code == 200
|
||||
user = response.json()
|
||||
response = api_client.get(api_routes.users_item_id(admin_user.user_id), headers=admin_user.token)
|
||||
|
||||
response = api_client.get(api_routes.admin_users_item_id(admin_user.user_id), headers=admin_user.token)
|
||||
admin = response.json()
|
||||
|
||||
# admin updating themselves
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue