mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-22 14:33:33 -07:00
Merge branch 'mealie-next' into fix/update-pre-commit-hooks
This commit is contained in:
commit
3b4ca33573
19 changed files with 108 additions and 262 deletions
|
@ -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);
|
||||
|
|
|
@ -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[] = [];
|
||||
|
|
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 };
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]
|
||||
|
|
52
poetry.lock
generated
52
poetry.lock
generated
|
@ -1855,14 +1855,14 @@ signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"]
|
|||
|
||||
[[package]]
|
||||
name = "openai"
|
||||
version = "1.90.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.90.0-py3-none-any.whl", hash = "sha256:e5dcb5498ea6b42fec47546d10f1bcc05fb854219a7d953a5ba766718b212a02"},
|
||||
{file = "openai-1.90.0.tar.gz", hash = "sha256:9771982cdd5b6631af68c6a603da72ed44cd2caf73b49f717a72b71374bc565b"},
|
||||
{file = "openai-1.91.0-py3-none-any.whl", hash = "sha256:207f87aa3bc49365e014fac2f7e291b99929f4fe126c4654143440e0ad446a5f"},
|
||||
{file = "openai-1.91.0.tar.gz", hash = "sha256:d6b07730d2f7c6745d0991997c16f85cddfc90ddcde8d569c862c30716b9fc90"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -2800,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]
|
||||
|
@ -3250,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]]
|
||||
|
@ -3885,4 +3885,4 @@ pgsql = ["psycopg2-binary"]
|
|||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.12,<3.13"
|
||||
content-hash = "2b8479e18ef741f5254b8c9d64566bf42d597cfb6564c1aa622f6a1afb117402"
|
||||
content-hash = "632cd8ef199c2668bc799a1cf4f370161dc13ff7dcf76ed40f3c94a0896e304f"
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue