Merge branch 'mealie-next' into remove-duplicate-docs-headline

This commit is contained in:
Michael Genson 2025-06-26 15:12:37 -05:00 committed by GitHub
commit d89b53a5af
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 539 additions and 2058 deletions

View file

@ -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.0
hooks:
- id: ruff
- id: ruff-format

View file

@ -70,7 +70,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 +88,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);

View file

@ -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,64 +377,33 @@ interface MergerHistory {
sourceText: string;
}
export default defineNuxtComponent({
components: {
VueDraggable,
RecipeIngredientHtml,
DropZone,
RecipeIngredients,
},
props: {
modelValue: {
type: Array as () => RecipeStep[],
required: false,
default: () => [],
},
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,
},
assets: {
type: Array as () => RecipeAsset[],
required: true,
},
scale: {
type: Number,
default: 1,
},
},
emits: ["update:modelValue", "click-instruction-field", "update:assets"],
});
const emit = defineEmits(["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 dialog = ref(false);
const disabledSteps = ref<number[]>([]);
const unusedIngredients = ref<RecipeIngredient[]>([]);
const usedIngredients = ref<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
@ -441,51 +411,53 @@ export default defineNuxtComponent({
return !(title === null || title === "" || title === undefined);
}
watch(props.modelValue, (v) => {
state.disabledSteps = [];
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);
// Eliminate state with an eager call to watcher?
onMounted(() => {
props.modelValue.forEach((element: RecipeStep) => {
instructionList.value.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 };
});
if (assets.value === undefined) {
emit("update:assets", []);
}
});
function toggleDisabled(stepIndex: number) {
if (isEditForm.value) {
return;
}
if (state.disabledSteps.includes(stepIndex)) {
const index = state.disabledSteps.indexOf(stepIndex);
if (disabledSteps.value.includes(stepIndex)) {
const index = disabledSteps.value.indexOf(stepIndex);
if (index !== -1) {
state.disabledSteps.splice(index, 1);
disabledSteps.value.splice(index, 1);
}
}
else {
state.disabledSteps.push(stepIndex);
disabledSteps.value.push(stepIndex);
}
}
function isChecked(stepIndex: number) {
if (state.disabledSteps.includes(stepIndex) && !isEditForm.value) {
if (disabledSteps.value.includes(stepIndex) && !isEditForm.value) {
return "disabled-card";
}
}
@ -501,18 +473,7 @@ export default defineNuxtComponent({
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;
}
@ -525,20 +486,20 @@ export default defineNuxtComponent({
function openDialog(idx: number, text: string, refs?: IngredientReferences[]) {
if (!refs) {
instructionList.value[idx].ingredientReferences = [];
refs = props.modelValue[idx].ingredientReferences as IngredientReferences[];
refs = instructionList.value[idx].ingredientReferences as IngredientReferences[];
}
setUsedIngredients();
activeText.value = text;
activeIndex.value = idx;
state.dialog = true;
dialog.value = true;
activeRefs.value = refs.map(ref => ref.referenceId ?? "");
}
const availableNextStep = computed(() => activeIndex.value < props.modelValue.length - 1);
const availableNextStep = computed(() => activeIndex.value < instructionList.value.length - 1);
function setIngredientIds() {
const instruction = props.modelValue[activeIndex.value];
const instruction = instructionList.value[activeIndex.value];
instruction.ingredientReferences = activeRefs.value.map((ref) => {
return {
referenceId: ref,
@ -547,12 +508,12 @@ export default defineNuxtComponent({
// Update the visibility of the cook mode button
showCookMode.value = false;
props.modelValue.forEach((element) => {
instructionList.value.forEach((element) => {
if (showCookMode.value === false && element.ingredientReferences && element.ingredientReferences.length > 0) {
showCookMode.value = true;
}
});
state.dialog = false;
dialog.value = false;
}
function saveAndOpenNextLinkIngredients() {
@ -563,7 +524,7 @@ export default defineNuxtComponent({
}
setIngredientIds();
const nextStep = props.modelValue[currentStepIndex + 1];
const nextStep = instructionList.value[currentStepIndex + 1];
// close dialog before opening to reset the scroll position
nextTick(() => openDialog(currentStepIndex + 1, nextStep.text, nextStep.ingredientReferences));
}
@ -571,7 +532,7 @@ export default defineNuxtComponent({
function setUsedIngredients() {
const usedRefs: { [key: string]: boolean } = {};
props.modelValue.forEach((element) => {
instructionList.value.forEach((element) => {
element.ingredientReferences?.forEach((ref) => {
if (ref.referenceId !== undefined) {
usedRefs[ref.referenceId!] = true;
@ -579,11 +540,11 @@ export default defineNuxtComponent({
});
});
state.usedIngredients = props.recipe.recipeIngredient.filter((ing) => {
usedIngredients.value = props.recipe.recipeIngredient.filter((ing) => {
return ing.referenceId !== undefined && ing.referenceId in usedRefs;
});
state.unusedIngredients = props.recipe.recipeIngredient.filter((ing) => {
unusedIngredients.value = props.recipe.recipeIngredient.filter((ing) => {
return !(ing.referenceId !== undefined && ing.referenceId in usedRefs);
});
}
@ -630,11 +591,11 @@ export default defineNuxtComponent({
mergeHistory.value.push({
target,
source,
targetText: props.modelValue[target].text,
sourceText: props.modelValue[source].text,
targetText: instructionList.value[target].text,
sourceText: instructionList.value[source].text,
});
instructionList.value[target].text += " " + props.modelValue[source].text;
instructionList.value[target].text += " " + instructionList.value[source].text;
instructionList.value.splice(source, 1);
}
@ -692,13 +653,13 @@ export default defineNuxtComponent({
}
}
const allCollapsed = sectionSteps.every(idx => state.disabledSteps.includes(idx));
const allCollapsed = sectionSteps.every(idx => disabledSteps.value.includes(idx));
if (allCollapsed) {
state.disabledSteps = state.disabledSteps.filter(idx => !sectionSteps.includes(idx));
disabledSteps.value = disabledSteps.value.filter(idx => !sectionSteps.includes(idx));
}
else {
state.disabledSteps = [...state.disabledSteps, ...sectionSteps];
disabledSteps.value = [...disabledSteps.value, ...sectionSteps];
}
}
@ -709,19 +670,6 @@ export default defineNuxtComponent({
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[]) {
@ -750,7 +698,7 @@ export default defineNuxtComponent({
return; // TODO: Handle error
}
context.emit("update:assets", [...props.assets, data]);
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;
@ -768,51 +716,6 @@ export default defineNuxtComponent({
};
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,
};
},
});
</script>
<style lang="css" scoped>

View file

@ -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[] = [];

View file

@ -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>

View 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);
};

View file

@ -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 };
};

View 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 });
}
});

View file

@ -13,7 +13,6 @@ export default defineNuxtConfig({
"@sidebase/nuxt-auth",
"@nuxtjs/google-fonts",
"vuetify-nuxt-module",
"@nuxtjs/mdc",
"@nuxt/eslint",
],
ssr: false,

View file

@ -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",

View file

@ -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;
}

View file

@ -150,7 +150,7 @@ export default defineNuxtComponent({
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.createOneFromImage(uploadedImage.value, uploadedImageName.value, translateLanguage?.value);
if (error || !data) {
alert.error(i18n.t("events.something-went-wrong"));
state.loading = false;

View file

@ -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,25 +38,17 @@
</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) {
const props = defineProps<{
mealplans: MealsByDate[];
}>();
type DaySection = {
title: string;
meals: ReadPlanEntry[];
@ -110,10 +102,4 @@ export default defineNuxtComponent({
return acc;
}, [] as Days[]);
});
return {
plan,
};
},
});
</script>

View file

@ -78,7 +78,7 @@
dark
hover
width="300px"
@click="initial.joinGroup"
@click="initial.createGroup"
>
<v-card-title class="d-flex align-center justify-center py-3">
<v-icon

View file

@ -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,
});

File diff suppressed because it is too large Load diff

View file

@ -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:

View file

@ -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,

View file

@ -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`.

View file

@ -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:

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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:
"""

View file

@ -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]

View file

@ -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]:
"""

View file

@ -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]

59
poetry.lock generated
View file

@ -1855,14 +1855,14 @@ signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"]
[[package]]
name = "openai"
version = "1.88.0"
version = "1.91.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.91.0-py3-none-any.whl", hash = "sha256:207f87aa3bc49365e014fac2f7e291b99929f4fe126c4654143440e0ad446a5f"},
{file = "openai-1.91.0.tar.gz", hash = "sha256:d6b07730d2f7c6745d0991997c16f85cddfc90ddcde8d569c862c30716b9fc90"},
]
[package.dependencies]
@ -1876,6 +1876,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)"]
@ -2675,14 +2676,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 +2800,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 +3250,30 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]]
name = "ruff"
version = "0.11.13"
version = "0.12.0"
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.0-py3-none-linux_armv6l.whl", hash = "sha256:5652a9ecdb308a1754d96a68827755f28d5dfb416b06f60fd9e13f26191a8848"},
{file = "ruff-0.12.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:05ed0c914fabc602fc1f3b42c53aa219e5736cb030cdd85640c32dbc73da74a6"},
{file = "ruff-0.12.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:07a7aa9b69ac3fcfda3c507916d5d1bca10821fe3797d46bad10f2c6de1edda0"},
{file = "ruff-0.12.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7731c3eec50af71597243bace7ec6104616ca56dda2b99c89935fe926bdcd48"},
{file = "ruff-0.12.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:952d0630eae628250ab1c70a7fffb641b03e6b4a2d3f3ec6c1d19b4ab6c6c807"},
{file = "ruff-0.12.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c021f04ea06966b02614d442e94071781c424ab8e02ec7af2f037b4c1e01cc82"},
{file = "ruff-0.12.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d235618283718ee2fe14db07f954f9b2423700919dc688eacf3f8797a11315c"},
{file = "ruff-0.12.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0758038f81beec8cc52ca22de9685b8ae7f7cc18c013ec2050012862cc9165"},
{file = "ruff-0.12.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:139b3d28027987b78fc8d6cfb61165447bdf3740e650b7c480744873688808c2"},
{file = "ruff-0.12.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68853e8517b17bba004152aebd9dd77d5213e503a5f2789395b25f26acac0da4"},
{file = "ruff-0.12.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3a9512af224b9ac4757f7010843771da6b2b0935a9e5e76bb407caa901a1a514"},
{file = "ruff-0.12.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b08df3d96db798e5beb488d4df03011874aff919a97dcc2dd8539bb2be5d6a88"},
{file = "ruff-0.12.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6a315992297a7435a66259073681bb0d8647a826b7a6de45c6934b2ca3a9ed51"},
{file = "ruff-0.12.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e55e44e770e061f55a7dbc6e9aed47feea07731d809a3710feda2262d2d4d8a"},
{file = "ruff-0.12.0-py3-none-win32.whl", hash = "sha256:7162a4c816f8d1555eb195c46ae0bd819834d2a3f18f98cc63819a7b46f474fb"},
{file = "ruff-0.12.0-py3-none-win_amd64.whl", hash = "sha256:d00b7a157b8fb6d3827b49d3324da34a1e3f93492c1f97b08e222ad7e9b291e0"},
{file = "ruff-0.12.0-py3-none-win_arm64.whl", hash = "sha256:8cd24580405ad8c1cc64d61725bca091d6b6da7eb3d36f72cc605467069d7e8b"},
{file = "ruff-0.12.0.tar.gz", hash = "sha256:4d047db3662418d4a848a3fdbfaf17488b34b62f527ed6f10cb8afd78135bc5c"},
]
[[package]]
@ -3884,4 +3885,4 @@ pgsql = ["psycopg2-binary"]
[metadata]
lock-version = "2.1"
python-versions = ">=3.12,<3.13"
content-hash = "2b8479e18ef741f5254b8c9d64566bf42d597cfb6564c1aa622f6a1afb117402"
content-hash = "632cd8ef199c2668bc799a1cf4f370161dc13ff7dcf76ed40f3c94a0896e304f"

View file

@ -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"