Compare commits

..

31 Commits

Author SHA1 Message Date
renovate[bot]
65c35adc9d fix(deps): update dependency extruct to ^0.18.0 (#4524)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-10 18:05:38 +00:00
renovate[bot]
83b4846f0c chore(deps): update dependency ruff to v0.7.3 (#4522)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-10 11:55:43 -06:00
Carter
6bc7ada20a feat: OIDC: add the ability to override the requested scopes (#4530) 2024-11-09 10:52:12 -06:00
Carter
8ce6f9038a feat: adds descriptions to feature checks and add them to logs (#4504) 2024-11-08 05:37:53 +00:00
Michael Genson
e3c6d4c66c fix: JSON Mode Resets Page State (#4519) 2024-11-07 17:43:07 +00:00
renovate[bot]
381a698220 fix(deps): update dependency openai to v1.54.3 (#4520)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-06 22:55:41 +00:00
renovate[bot]
c866557d58 fix(deps): update dependency openai to v1.54.2 (#4518)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-06 18:28:57 +00:00
renovate[bot]
bb5da2cb54 fix(deps): update dependency alembic to v1.14.0 (#4512)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-06 09:38:11 -06:00
Michael Genson
0fed5f54f6 fix: Prevent Users From Being Created With Missing Group/Household (#4500) 2024-11-05 23:52:33 +00:00
Michael Genson
f4bde93960 fix: "No Label" on Shopping List can't be toggled (#4513)
Co-authored-by: Kuchenpirat <24235032+Kuchenpirat@users.noreply.github.com>
2024-11-05 23:12:52 +00:00
renovate[bot]
62300deea0 fix(deps): update dependency orjson to v3.10.11 (#4495)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-05 21:32:37 +01:00
Michael Genson
87f4b23711 feat: Show Cookbooks from Other Households (#4452) 2024-11-05 13:57:30 -06:00
renovate[bot]
8983745106 chore(deps): update dependency mkdocs-material to v9.5.44 (#4516)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-05 20:17:57 +01:00
renovate[bot]
8872fd52cd fix(deps): update dependency openai to v1.54.1 (#4515)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-05 19:48:34 +01:00
renovate[bot]
b81b97d934 fix(deps): update dependency openai to v1.54.0 (#4510)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-05 19:31:14 +01:00
renovate[bot]
f798fafb3e chore(deps): update dependency rich to v13.9.4 (#4491)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-05 16:32:54 +01:00
github-actions[bot]
dbbbe06a23 chore(auto): Update pre-commit hooks (#4506)
Co-authored-by: boc-the-git <3479092+boc-the-git@users.noreply.github.com>
2024-11-04 17:08:17 +00:00
Wim de Groot
4b9eb5077a feat: implement the possibility to add tls (#4456)
Signed-off-by: Wim de Groot <34519486+wim-de-groot@users.noreply.github.com>
2024-11-04 16:17:08 +00:00
Michael Genson
ff6db2374d fix: Scripts Missing Repo Params (#4487)
Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
2024-11-03 18:17:01 +00:00
renovate[bot]
3e69ea94d5 chore(deps): update dependency ruff to v0.7.2 (#4488)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-01 23:56:03 -05:00
renovate[bot]
2e114cfa69 fix(deps): update dependency pydantic-settings to v2.6.1 (#4486)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-01 20:00:10 -05:00
Shlok Sheth
eb34ef0156 fix: Added Nutrients Suffix to the PrintView and some formatting to that (#4493) 2024-11-01 22:12:25 +00:00
Brian Choromanski
446755f678 feat: vrslev/pre-commit-autoupdate archived (#4421) 2024-11-01 21:55:17 +00:00
renovate[bot]
08fe2d32b0 chore(deps): update dependency mkdocs-material to v9.5.43 (#4481)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-31 22:04:52 +01:00
renovate[bot]
fb653ee2f6 fix(deps): update dependency python-multipart to ^0.0.17 (#4479)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-31 07:52:36 -05:00
renovate[bot]
a326a8c717 fix(deps): update dependency fastapi to v0.115.4 (#4459)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-30 15:42:56 -05:00
renovate[bot]
6e7cb5fb86 fix(deps): update dependency python-multipart to ^0.0.16 (#4400)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-30 20:28:14 +00:00
renovate[bot]
9289bd8e05 fix(deps): update dependency openai to v1.53.0 (#4475)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-30 15:16:37 -05:00
Kuchenpirat
985b5634b7 fix: round ingredient amounts when not using fractions (#4470) 2024-10-30 15:12:45 +00:00
github-actions[bot]
2b2bc041bd docs(auto): Update image tag, for release v2.1.0 (#4471)
Co-authored-by: boc-the-git <3479092+boc-the-git@users.noreply.github.com>
2024-10-30 13:35:39 +00:00
Kuchenpirat
6e16d4cc91 fix: set useFractions on Unit creation to true by default (#4469) 2024-10-30 08:27:02 -05:00
39 changed files with 905 additions and 286 deletions

View File

@@ -15,8 +15,30 @@ jobs:
- name: Checkout 🛎
uses: actions/checkout@v4
- name: Update pre-commit Hooks
uses: vrslev/pre-commit-autoupdate@v1.0.0
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Set PY
shell: bash
run: echo "PY=$(python -VV | sha256sum | cut -d' ' -f1)" >> $GITHUB_ENV
- name: Cache
uses: actions/cache@v4
with:
path: |
~/.cache/pre-commit
~/.cache/pip
key: pre-commit-${{ env.PY }}-${{ hashFiles('.pre-commit-config.yaml') }}
- name: Install pre-commit
shell: bash
run: pip install -U pre-commit
- name: Run `pre-commit autoupdate`
shell: bash
run: pre-commit autoupdate --color=always
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6

View File

@@ -12,7 +12,7 @@ repos:
exclude: ^tests/data/
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.7.1
rev: v0.7.2
hooks:
- id: ruff
- id: ruff-format

View File

@@ -4,21 +4,21 @@
### General
| Variables | Default | Description |
| ----------------------------- | :-------------------: | --------------------------------------------------------------------------------------------------------- |
| PUID | 911 | UserID permissions between host OS and container |
| PGID | 911 | GroupID permissions between host OS and container |
| DEFAULT_GROUP | Home | The default group for users |
| DEFAULT_HOUSEHOLD | Family | The default household for users in each group |
| BASE_URL | http://localhost:8080 | Used for Notifications |
| TOKEN_TIME | 48 | The time in hours that a login/auth token is valid |
| API_PORT | 9000 | The port exposed by backend API. **Do not change this if you're running in Docker** |
| API_DOCS | True | Turns on/off access to the API documentation locally |
| TZ | UTC | Must be set to get correct date/time on the server |
| ALLOW_SIGNUP<super>\*</super> | false | Allow user sign-up without token |
| LOG_CONFIG_OVERRIDE | | Override the config for logging with a custom path |
| LOG_LEVEL | info | Logging level (e.g. critical, error, warning, info, debug) |
| DAILY_SCHEDULE_TIME | 23:45 | The time of day to run daily server tasks, in HH:MM format. Use the server's local time, *not* UTC |
| Variables | Default | Description |
| ----------------------------- | :-------------------: | -------------------------------------------------------------------------------------------------- |
| PUID | 911 | UserID permissions between host OS and container |
| PGID | 911 | GroupID permissions between host OS and container |
| DEFAULT_GROUP | Home | The default group for users |
| DEFAULT_HOUSEHOLD | Family | The default household for users in each group |
| BASE_URL | http://localhost:8080 | Used for Notifications |
| TOKEN_TIME | 48 | The time in hours that a login/auth token is valid |
| API_PORT | 9000 | The port exposed by backend API. **Do not change this if you're running in Docker** |
| API_DOCS | True | Turns on/off access to the API documentation locally |
| TZ | UTC | Must be set to get correct date/time on the server |
| ALLOW_SIGNUP<super>\*</super> | false | Allow user sign-up without token |
| LOG_CONFIG_OVERRIDE | | Override the config for logging with a custom path |
| LOG_LEVEL | info | Logging level (e.g. critical, error, warning, info, debug) |
| DAILY_SCHEDULE_TIME | 23:45 | The time of day to run daily server tasks, in HH:MM format. Use the server's local time, *not* UTC |
<super>\*</super> Starting in v1.4.0 this was changed to default to `false` as part of a security review of the application.
@@ -57,10 +57,19 @@
Changing the webworker settings may cause unforeseen memory leak issues with Mealie. It's best to leave these at the defaults unless you begin to experience issues with multiple users. Exercise caution when changing these settings
| Variables | Default | Description |
| --------------- | :-----: | ----------------------------------------------------------------------------- |
| Variables | Default | Description |
| --------------- | :-----: | -------------------------------------------------------------------------------- |
| UVICORN_WORKERS | 1 | Sets the number of workers for the web server. [More info here][unicorn_workers] |
### TLS
Use this only when mealie is run without a webserver or reverse proxy.
| Variables | Default | Description |
| -------------------- | :-----: | ------------------------ |
| TLS_CERTIFICATE_PATH | None | File path to Certificate |
| TLS_PRIVATE_KEY_PATH | None | File path to private key |
### LDAP
| Variables | Default | Description |
@@ -85,21 +94,22 @@ Changing the webworker settings may cause unforeseen memory leak issues with Mea
For usage, see [Usage - OpenID Connect](../authentication/oidc-v2.md)
| Variables | Default | Description |
| ---------------------- | :-----: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| OIDC_AUTH_ENABLED | False | Enables authentication via OpenID Connect |
| OIDC_SIGNUP_ENABLED | True | Enables new users to be created when signing in for the first time with OIDC |
| OIDC_CONFIGURATION_URL | None | The URL to the OIDC configuration of your provider. This is usually something like https://auth.example.com/.well-known/openid-configuration |
| OIDC_CLIENT_ID | None | The client id of your configured client in your provider |
| OIDC_CLIENT_SECRET <br/> :octicons-tag-24: v2.0.0 | None | The client secret of your configured client in your provider|
| OIDC_USER_GROUP | None | If specified, only users belonging to this group will be able to successfully authenticate. For more information see [this page](../authentication/oidc-v2.md#groups) |
| OIDC_ADMIN_GROUP | None | If specified, users belonging to this group will be able to successfully authenticate *and* be made an admin. For more information see [this page](../authentication/oidc-v2.md#groups) |
| OIDC_AUTO_REDIRECT | False | If `True`, then the login page will be bypassed and you will be sent directly to your Identity Provider. You can still get to the login page by adding `?direct=1` to the login URL |
| OIDC_PROVIDER_NAME | OAuth | The provider name is shown in SSO login button. "Login with <OIDC_PROVIDER_NAME\>" |
| OIDC_REMEMBER_ME | False | Because redirects bypass the login screen, you cant extend your session by clicking the "Remember Me" checkbox. By setting this value to true, a session will be extended as if "Remember Me" was checked |
| OIDC_USER_CLAIM | email | This is the claim which Mealie will use to look up an existing user by (e.g. "email", "preferred_username") |
| OIDC_GROUPS_CLAIM | groups | Optional if not using `OIDC_USER_GROUP` or `OIDC_ADMIN_GROUP`. This is the claim Mealie will request from your IdP and will use to compare to `OIDC_USER_GROUP` or `OIDC_ADMIN_GROUP` to allow the user to log in to Mealie or is set as an admin. **Your IdP must be configured to grant this claim** |
| OIDC_TLS_CACERTFILE | None | File path to Certificate Authority used to verify server certificate (e.g. `/path/to/ca.crt`) |
| Variables | Default | Description |
| ------------------------------------------------- | :-----: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| OIDC_AUTH_ENABLED | False | Enables authentication via OpenID Connect |
| OIDC_SIGNUP_ENABLED | True | Enables new users to be created when signing in for the first time with OIDC |
| OIDC_CONFIGURATION_URL | None | The URL to the OIDC configuration of your provider. This is usually something like https://auth.example.com/.well-known/openid-configuration |
| OIDC_CLIENT_ID | None | The client id of your configured client in your provider |
| OIDC_CLIENT_SECRET <br/> :octicons-tag-24: v2.0.0 | None | The client secret of your configured client in your provider |
| OIDC_USER_GROUP | None | If specified, only users belonging to this group will be able to successfully authenticate. For more information see [this page](../authentication/oidc-v2.md#groups) |
| OIDC_ADMIN_GROUP | None | If specified, users belonging to this group will be able to successfully authenticate *and* be made an admin. For more information see [this page](../authentication/oidc-v2.md#groups) |
| OIDC_AUTO_REDIRECT | False | If `True`, then the login page will be bypassed and you will be sent directly to your Identity Provider. You can still get to the login page by adding `?direct=1` to the login URL |
| OIDC_PROVIDER_NAME | OAuth | The provider name is shown in SSO login button. "Login with <OIDC_PROVIDER_NAME\>" |
| OIDC_REMEMBER_ME | False | Because redirects bypass the login screen, you cant extend your session by clicking the "Remember Me" checkbox. By setting this value to true, a session will be extended as if "Remember Me" was checked |
| OIDC_USER_CLAIM | email | This is the claim which Mealie will use to look up an existing user by (e.g. "email", "preferred_username") |
| OIDC_GROUPS_CLAIM | groups | Optional if not using `OIDC_USER_GROUP` or `OIDC_ADMIN_GROUP`. This is the claim Mealie will request from your IdP and will use to compare to `OIDC_USER_GROUP` or `OIDC_ADMIN_GROUP` to allow the user to log in to Mealie or is set as an admin. **Your IdP must be configured to grant this claim** |
| OIDC_SCOPES_OVERRIDE | None | Advanced configuration used to override the scopes requested from the IdP. **Most users won't need to change this**. At a minimum, 'openid profile email' are required. |
| OIDC_TLS_CACERTFILE | None | File path to Certificate Authority used to verify server certificate (e.g. `/path/to/ca.crt`) |
### OpenAI
@@ -118,7 +128,7 @@ For custom mapping variables (e.g. OPENAI_CUSTOM_HEADERS) you should pass values
| OPENAI_ENABLE_IMAGE_SERVICES | True | Whether to enable OpenAI image services, such as creating recipes via image. Leave this enabled unless your custom model doesn't support it, or you want to reduce costs |
| OPENAI_WORKERS | 2 | Number of OpenAI workers per request. Higher values may increase processing speed, but will incur additional API costs |
| OPENAI_SEND_DATABASE_DATA | True | Whether to send Mealie data to OpenAI to improve request accuracy. This will incur additional API costs |
| OPENAI_REQUEST_TIMEOUT | 60 | The number of seconds to wait for an OpenAI request to complete before cancelling the request. Leave this empty unless you're running into timeout issues on slower hardware |
| OPENAI_REQUEST_TIMEOUT | 60 | The number of seconds to wait for an OpenAI request to complete before cancelling the request. Leave this empty unless you're running into timeout issues on slower hardware |
### Theming

View File

@@ -31,7 +31,7 @@ To deploy mealie on your local network, it is highly recommended to use Docker t
We've gone through a few versions of Mealie v1 deployment targets. We have settled on a single container deployment, and we've begun publishing the nightly container on github containers. If you're looking to move from the old nightly (split containers _or_ the omni image) to the new nightly, there are a few things you need to do:
1. Take a backup just in case!
2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:v2.0.0`
2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:v2.1.0`
3. Take the external port from the frontend container and set that as the port mapped to port `9000` on the new container. The frontend is now served on port 9000 from the new container, so it will need to be mapped for you to have access.
4. Restart the container

View File

@@ -7,7 +7,7 @@ PostgreSQL might be considered if you need to support many concurrent users. In
```yaml
services:
mealie:
image: ghcr.io/mealie-recipes/mealie:v2.0.0 # (3)
image: ghcr.io/mealie-recipes/mealie:v2.1.0 # (3)
container_name: mealie
restart: always
ports:

View File

@@ -11,7 +11,7 @@ SQLite is a popular, open source, self-contained, zero-configuration database th
```yaml
services:
mealie:
image: ghcr.io/mealie-recipes/mealie:v2.0.0 # (3)
image: ghcr.io/mealie-recipes/mealie:v2.1.0 # (3)
container_name: mealie
restart: always
ports:

View File

File diff suppressed because one or more lines are too long

View File

@@ -7,7 +7,7 @@
width="100%"
max-width="1100px"
:icon="$globals.icons.pages"
:title="$t('general.edit')"
:title="$tc('general.edit')"
:submit-icon="$globals.icons.save"
:submit-text="$tc('general.save')"
:submit-disabled="!editTarget.queryFilterString"
@@ -25,7 +25,7 @@
<v-toolbar-title class="headline"> {{ book.name }} </v-toolbar-title>
<v-spacer></v-spacer>
<BaseButton
v-if="isOwnGroup"
v-if="canEdit"
class="mx-1"
:edit="true"
@click="handleEditCookbook"
@@ -79,6 +79,15 @@
const tab = ref(null);
const book = getOne(slug);
const isOwnHousehold = computed(() => {
if (!($auth.user && book.value?.householdId)) {
return false;
}
return $auth.user.householdId === book.value.householdId;
})
const canEdit = computed(() => isOwnGroup.value && isOwnHousehold.value);
const dialogStates = reactive({
edit: false,
});
@@ -118,7 +127,7 @@
recipes,
removeRecipe,
replaceRecipes,
isOwnGroup,
canEdit,
dialogStates,
editTarget,
handleEditCookbook,

View File

@@ -96,7 +96,13 @@ import RecipePageTitleContent from "./RecipePageParts/RecipePageTitleContent.vue
import RecipePageComments from "./RecipePageParts/RecipePageComments.vue";
import { useLoggedInState } from "~/composables/use-logged-in-state";
import RecipePrintContainer from "~/components/Domain/Recipe/RecipePrintContainer.vue";
import { EditorMode, PageMode, usePageState, usePageUser } from "~/composables/recipe-page/shared-state";
import {
clearPageState,
EditorMode,
PageMode,
usePageState,
usePageUser,
} from "~/composables/recipe-page/shared-state";
import { NoUndefinedField } from "~/lib/api/types/non-generated";
import { Recipe } from "~/lib/api/types/recipe";
import { useRouteQuery } from "~/composables/use-router";
@@ -170,6 +176,9 @@ export default defineComponent({
}
}
deactivateNavigationWarning();
clearPageState(props.recipe.slug || "");
console.debug("reset RecipePage state during unmount");
});
/** =============================================================

View File

@@ -32,8 +32,8 @@
</template>
<script lang="ts">
import { computed, defineComponent, onUnmounted } from "@nuxtjs/composition-api";
import { clearPageState, usePageState, usePageUser } from "~/composables/recipe-page/shared-state";
import { computed, defineComponent } from "@nuxtjs/composition-api";
import { usePageState, usePageUser } from "~/composables/recipe-page/shared-state";
import { NoUndefinedField } from "~/lib/api/types/non-generated";
import { Recipe } from "~/lib/api/types/recipe";
import { useUserApi } from "~/composables/api";
@@ -75,10 +75,6 @@ export default defineComponent({
return households.value.find((h) => h.id === owner.householdId);
});
onUnmounted(() => {
clearPageState(props.recipe.slug);
console.debug("reset RecipePage state during unmount");
});
async function uploadImage(fileObject: File) {
if (!props.recipe || !props.recipe.slug) {
return;

View File

@@ -98,7 +98,7 @@
<tr v-for="(value, key) in recipe.nutrition" :key="key">
<template v-if="value">
<td>{{ labels[key].label }}</td>
<td>{{ value || '-' }}</td>
<td>{{ value ? (labels[key].suffix ? `${value} ${labels[key].suffix}` : value) : '-' }}</td>
</template>
</tr>
</tbody>
@@ -322,10 +322,32 @@ li {
}
.nutrition-table {
width: 25%;
max-width: 80%;
border-collapse: collapse;
}
.nutrition-table th,
.nutrition-table td {
padding: 6px 10px;
text-align: left;
vertical-align: top;
font-size: 14px;
}
.nutrition-table th {
font-weight: bold;
padding-bottom: 10px;
}
.nutrition-table td:first-child {
width: 70%;
font-weight: bold;
}
.nutrition-table td:last-child {
width: 30%;
text-align: right;
}
.nutrition-table td {
padding: 2px;
text-align: left;

View File

@@ -82,12 +82,17 @@ import { computed, defineComponent, onMounted, ref, useContext, useRoute } from
import { useLoggedInState } from "~/composables/use-logged-in-state";
import AppHeader from "@/components/Layout/LayoutParts/AppHeader.vue";
import AppSidebar from "@/components/Layout/LayoutParts/AppSidebar.vue";
import { SidebarLinks } from "~/types/application-types";
import { SideBarLink } from "~/types/application-types";
import LanguageDialog from "~/components/global/LanguageDialog.vue";
import TheSnackbar from "@/components/Layout/LayoutParts/TheSnackbar.vue";
import { useAppInfo } from "~/composables/api";
import { useCookbooks, usePublicCookbooks } from "~/composables/use-group-cookbooks";
import { useCookbookPreferences } from "~/composables/use-users/preferences";
import { useHouseholdStore, usePublicHouseholdStore } from "~/composables/store/use-household-store";
import { useToggleDarkMode } from "~/composables/use-utils";
import { ReadCookBook } from "~/lib/api/types/cookbook";
import { HouseholdSummary } from "~/lib/api/types/household";
export default defineComponent({
components: { AppHeader, AppSidebar, LanguageDialog, TheSnackbar },
@@ -99,6 +104,15 @@ export default defineComponent({
const route = useRoute();
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
const { cookbooks } = isOwnGroup.value ? useCookbooks() : usePublicCookbooks(groupSlug.value || "");
const cookbookPreferences = useCookbookPreferences();
const { store: households } = isOwnGroup.value ? useHouseholdStore() : usePublicHouseholdStore(groupSlug.value || "");
const householdsById = computed(() => {
return households.value.reduce((acc, household) => {
acc[household.id] = household;
return acc;
}, {} as { [key: string]: HouseholdSummary });
});
const appInfo = useAppInfo();
const showImageImport = computed(() => appInfo.value?.enableOpenaiImageServices);
@@ -113,29 +127,57 @@ export default defineComponent({
sidebar.value = !$vuetify.breakpoint.md;
});
const cookbookLinks = computed(() => {
if (!cookbooks.value) return [];
return cookbooks.value.map((cookbook) => {
return {
key: cookbook.slug,
icon: $globals.icons.pages,
title: cookbook.name,
to: `/g/${groupSlug.value}/cookbooks/${cookbook.slug as string}`,
};
});
});
interface Link {
insertDivider: boolean;
icon: string;
title: string;
subtitle: string | null;
to: string;
restricted: boolean;
hide: boolean;
function cookbookAsLink(cookbook: ReadCookBook): SideBarLink {
return {
key: cookbook.slug || "",
icon: $globals.icons.pages,
title: cookbook.name,
to: `/g/${groupSlug.value}/cookbooks/${cookbook.slug || ""}`,
restricted: false,
};
}
const createLinks = computed<Link[]>(() => [
const currentUserHouseholdId = computed(() => $auth.user?.householdId);
const cookbookLinks = computed<SideBarLink[]>(() => {
if (!cookbooks.value) {
return [];
}
cookbooks.value.sort((a, b) => (a.position || 0) - (b.position || 0));
const ownLinks: SideBarLink[] = [];
const links: SideBarLink[] = [];
const cookbooksByHousehold = cookbooks.value.reduce((acc, cookbook) => {
const householdName = householdsById.value[cookbook.householdId]?.name || "";
if (!acc[householdName]) {
acc[householdName] = [];
}
acc[householdName].push(cookbook);
return acc;
}, {} as Record<string, ReadCookBook[]>);
Object.entries(cookbooksByHousehold).forEach(([householdName, cookbooks]) => {
if (cookbooks[0].householdId === currentUserHouseholdId.value) {
ownLinks.push(...cookbooks.map(cookbookAsLink));
} else {
links.push({
key: householdName,
icon: $globals.icons.book,
title: householdName,
children: cookbooks.map(cookbookAsLink),
restricted: false,
});
}
});
links.sort((a, b) => a.title.localeCompare(b.title));
if ($auth.user && cookbookPreferences.value.hideOtherHouseholds) {
return ownLinks;
} else {
return [...ownLinks, ...links];
}
});
const createLinks = computed<SideBarLink[]>(() => [
{
insertDivider: false,
icon: $globals.icons.link,
@@ -165,7 +207,7 @@ export default defineComponent({
},
]);
const bottomLinks = computed<SidebarLinks>(() => [
const bottomLinks = computed<SideBarLink[]>(() => [
{
icon: $globals.icons.cog,
title: i18n.tc("general.settings"),
@@ -174,7 +216,7 @@ export default defineComponent({
},
]);
const topLinks = computed<SidebarLinks>(() => [
const topLinks = computed<SideBarLink[]>(() => [
{
icon: $globals.icons.silverwareForkKnife,
to: `/g/${groupSlug.value}`,

View File

@@ -135,7 +135,7 @@
</template>
<script lang="ts">
import { computed, defineComponent, reactive, toRefs, useContext } from "@nuxtjs/composition-api";
import { computed, defineComponent, reactive, toRefs, useContext, watch } from "@nuxtjs/composition-api";
import { useLoggedInState } from "~/composables/use-logged-in-state";
import { SidebarLinks } from "~/types/application-types";
import UserAvatar from "~/components/Domain/User/UserAvatar.vue";
@@ -192,13 +192,29 @@ export default defineComponent({
const userProfileLink = computed(() => $auth.user ? "/user/profile" : undefined);
const state = reactive({
dropDowns: {},
dropDowns: {} as Record<string, boolean>,
topSelected: null as string[] | null,
secondarySelected: null as string[] | null,
bottomSelected: null as string[] | null,
hasOpenedBefore: false as boolean,
});
const allLinks = computed(() => [...props.topLink, ...(props.secondaryLinks || []), ...(props.bottomLinks || [])]);
function initDropdowns() {
allLinks.value.forEach((link) => {
state.dropDowns[link.title] = link.childrenStartExpanded || false;
})
}
watch(
() => allLinks,
() => {
initDropdowns();
},
{
deep: true,
}
);
return {
...toRefs(state),
userFavoritesLink,

View File

@@ -54,7 +54,7 @@ export function useParsedIngredientText(ingredient: RecipeIngredient, disableAmo
// casting to number is required as sometimes quantity is a string
if (quantity && Number(quantity) !== 0) {
if (unit && !unit.fraction) {
returnQty = (quantity * scale).toString();
returnQty = Number((quantity * scale).toPrecision(3)).toString();
} else {
const fraction = frac(quantity * scale, 10, true);
if (fraction[0] !== undefined && fraction[0] > 0) {

View File

@@ -99,10 +99,10 @@ export const useCookbooks = function () {
loading.value = false;
},
async createOne() {
async createOne(name: string | null = null) {
loading.value = true;
const { data } = await api.cookbooks.createOne({
name: i18n.t("cookbook.household-cookbook-name", [household.value?.name || "", String((cookbookStore?.value?.length ?? 0) + 1)]) as string,
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: "",
});
@@ -129,18 +129,18 @@ export const useCookbooks = function () {
return data;
},
async updateOrder() {
if (!cookbookStore?.value) {
async updateOrder(cookbooks: ReadCookBook[]) {
if (!cookbooks?.length) {
return;
}
loading.value = true;
cookbookStore.value.forEach((element, index) => {
cookbooks.forEach((element, index) => {
element.position = index + 1;
});
const { data } = await api.cookbooks.updateAll(cookbookStore.value);
const { data } = await api.cookbooks.updateAll(cookbooks);
if (data && cookbookStore?.value) {
this.refreshAll();

View File

@@ -45,6 +45,10 @@ export interface UserParsingPreferences {
parser: RegisteredParser;
}
export interface UserCookbooksPreferences {
hideOtherHouseholds: boolean;
}
export function useUserMealPlanPreferences(): Ref<UserMealPlanPreferences> {
const fromStorage = useLocalStorage(
"meal-planner-preferences",
@@ -153,3 +157,17 @@ export function useParsingPreferences(): Ref<UserParsingPreferences> {
return fromStorage;
}
export function useCookbookPreferences(): Ref<UserCookbooksPreferences> {
const fromStorage = useLocalStorage(
"cookbook-preferences",
{
hideOtherHouseholds: false,
},
{ mergeDefaults: true }
// we cast to a Ref because by default it will return an optional type ref
// but since we pass defaults we know all properties are set.
) as unknown as Ref<UserCookbooksPreferences>;
return fromStorage;
}

View File

@@ -1327,6 +1327,8 @@
"cookbook": {
"cookbooks": "Cookbooks",
"description": "Cookbooks are another way to organize recipes by creating cross sections of recipes, organizers, and other filters. Creating a cookbook will add an entry to the side-bar and all the recipes with the filters chosen will be displayed in the cookbook.",
"hide-cookbooks-from-other-households": "Hide Cookbooks from Other Households",
"hide-cookbooks-from-other-households-description": "When enabled, only cookbooks from your household will appear on the sidebar",
"public-cookbook": "Public Cookbook",
"public-cookbook-description": "Public Cookbooks can be shared with non-mealie users and will be displayed on your groups page.",
"filter-options": "Filter Options",

View File

@@ -1,6 +1,6 @@
{
"name": "mealie",
"version": "2.0.0",
"version": "2.1.0",
"private": true,
"scripts": {
"dev": "nuxt",

View File

@@ -48,20 +48,33 @@
{{ $t('cookbook.description') }}
</BasePageTitle>
<div class="my-6">
<v-checkbox
v-model="cookbookPreferences.hideOtherHouseholds"
:label="$tc('cookbook.hide-cookbooks-from-other-households')"
hide-details
/>
<div class="ml-8">
<p class="text-subtitle-2 my-0 py-0">
{{ $tc("cookbook.hide-cookbooks-from-other-households-description") }}
</p>
</div>
</div>
<!-- Create New -->
<BaseButton create @click="createCookbook" />
<!-- Cookbook List -->
<v-expansion-panels class="mt-2">
<draggable
v-model="cookbooks"
v-model="myCookbooks"
handle=".handle"
delay="250"
:delay-on-touch-only="true"
style="width: 100%"
@change="actions.updateOrder()"
@change="actions.updateOrder(myCookbooks)"
>
<v-expansion-panel v-for="cookbook in cookbooks" :key="cookbook.id" class="my-2 left-border rounded">
<v-expansion-panel v-for="cookbook in myCookbooks" :key="cookbook.id" class="my-2 left-border rounded">
<v-expansion-panel-header disable-icon-rotate class="headline">
<div class="d-flex align-center">
<v-icon large left>
@@ -110,11 +123,13 @@
<script lang="ts">
import { defineComponent, onBeforeUnmount, onMounted, reactive, ref } from "@nuxtjs/composition-api";
import { computed, defineComponent, onBeforeUnmount, onMounted, reactive, ref, useContext } from "@nuxtjs/composition-api";
import draggable from "vuedraggable";
import { useCookbooks } from "@/composables/use-group-cookbooks";
import { useHouseholdSelf } from "@/composables/use-households";
import CookbookEditor from "~/components/Domain/Cookbook/CookbookEditor.vue";
import { ReadCookBook } from "~/lib/api/types/cookbook";
import { useCookbookPreferences } from "~/composables/use-users/preferences";
export default defineComponent({
components: { CookbookEditor, draggable },
@@ -124,13 +139,28 @@ export default defineComponent({
create: false,
delete: false,
});
const { cookbooks, actions } = useCookbooks();
const { $auth, i18n } = useContext();
const { cookbooks: allCookbooks, actions } = useCookbooks();
const myCookbooks = computed<ReadCookBook[]>({
get: () => {
return allCookbooks.value?.filter((cookbook) => {
return cookbook.householdId === $auth.user?.householdId;
}) || [];
},
set: (value: ReadCookBook[]) => {
actions.updateOrder(value);
},
});
const { household } = useHouseholdSelf();
const cookbookPreferences = useCookbookPreferences()
// create
const createTargetKey = ref(0);
const createTarget = ref<ReadCookBook | null>(null);
async function createCookbook() {
await actions.createOne().then((cookbook) => {
const name = i18n.t("cookbook.household-cookbook-name", [household.value?.name || "", String((myCookbooks.value?.length ?? 0) + 1)]) as string
await actions.createOne(name).then((cookbook) => {
createTarget.value = cookbook as ReadCookBook;
createTargetKey.value++;
});
@@ -177,7 +207,8 @@ export default defineComponent({
});
return {
cookbooks,
myCookbooks,
cookbookPreferences,
actions,
dialogStates,
// create

View File

@@ -323,7 +323,7 @@ export default defineComponent({
// we explicitly set booleans to false since forms don't POST unchecked boxes
const createTarget = ref<CreateIngredientUnit>({
name: "",
fraction: false,
fraction: true,
useAbbreviation: false,
});

View File

@@ -56,7 +56,7 @@
<!-- View By Label -->
<div v-else>
<div v-for="(value, key) in itemsByLabel" :key="key" class="mb-6">
<div v-for="(value, key) in itemsByLabel" :key="key" class="pb-4">
<v-btn
:color="getLabelColor(value[0]) ? getLabelColor(value[0]) : '#959595'"
:style="{
@@ -73,20 +73,20 @@
<v-divider/>
<v-expand-transition group>
<div v-show="labelOpenState[key]">
<draggable :value="value" handle=".handle" delay="250" :delay-on-touch-only="true" @start="loadingCounter += 1" @end="loadingCounter -= 1" @input="updateIndexUncheckedByLabel(key, $event)">
<v-lazy v-for="(item, index) in value" :key="item.id" class="ml-2 my-2">
<ShoppingListItem
v-model="value[index]"
:show-label=false
:labels="allLabels || []"
:units="allUnits || []"
:foods="allFoods || []"
:recipes="recipeMap"
@checked="saveListItem"
@save="saveListItem"
@delete="deleteListItem(item)"
/>
</v-lazy>
<draggable :value="value" handle=".handle" delay="250" :delay-on-touch-only="true" @start="loadingCounter += 1" @end="loadingCounter -= 1" @input="updateIndexUncheckedByLabel(key, $event)">
<v-lazy v-for="(item, index) in value" :key="item.id" class="ml-2 my-2">
<ShoppingListItem
v-model="value[index]"
:show-label=false
:labels="allLabels || []"
:units="allUnits || []"
:foods="allFoods || []"
:recipes="recipeMap"
@checked="saveListItem"
@save="saveListItem"
@delete="deleteListItem(item)"
/>
</v-lazy>
</draggable>
</div>
</v-expand-transition>
@@ -470,7 +470,7 @@ export default defineComponent({
});
// =====================================
// Collapsables
// Collapsable Labels
const labelOpenState = ref<{ [key: string]: boolean }>({});
const initializeLabelOpenStates = () => {
@@ -480,8 +480,8 @@ export default defineComponent({
let hasChanges = false;
for (const item of shoppingList.value.listItems) {
const labelName = item.label?.name;
if (labelName && !existingLabels.has(labelName) && !(labelName in labelOpenState.value)) {
const labelName = item.label?.name || i18n.tc("shopping-list.no-label");
if (!existingLabels.has(labelName) && !(labelName in labelOpenState.value)) {
labelOpenState.value[labelName] = true;
hasChanges = true;
}
@@ -492,9 +492,13 @@ export default defineComponent({
}
};
const labelNames = computed(() =>
new Set(shoppingList.value?.listItems?.map(item => item.label?.name).filter(Boolean) ?? [])
);
const labelNames = computed(() => {
return new Set(
shoppingList.value?.listItems
?.map(item => item.label?.name || i18n.tc("shopping-list.no-label"))
.filter(Boolean) ?? []
);
});
watch(labelNames, initializeLabelOpenStates, { immediate: true });

View File

@@ -5,6 +5,7 @@ export interface SideBarLink {
href?: string;
title: string;
children?: SideBarLink[];
childrenStartExpanded?: boolean;
restricted: boolean;
}

View File

@@ -64,20 +64,23 @@ async def lifespan_fn(_: FastAPI) -> AsyncGenerator[None, None]:
settings.model_dump_json(
indent=4,
exclude={
"LDAP_QUERY_PASSWORD",
"OPENAI_API_KEY",
"SECRET",
"SESSION_SECRET",
"SFTP_PASSWORD",
"SFTP_USERNAME",
"DB_URL", # replace by DB_URL_PUBLIC for logs
"DB_PROVIDER",
"SMTP_USER",
"SMTP_PASSWORD",
"OIDC_CLIENT_SECRET",
},
)
)
logger.info("------APP FEATURES------")
logger.info("--------==SMTP==--------")
logger.info(settings.SMTP_FEATURE)
logger.info("--------==LDAP==--------")
logger.info(settings.LDAP_FEATURE)
logger.info("--------==OIDC==--------")
logger.info(settings.OIDC_FEATURE)
logger.info("-------==OPENAI==-------")
logger.info(settings.OPENAI_FEATURE)
logger.info("------------------------")
yield

View File

@@ -3,10 +3,10 @@ import os
import secrets
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, NamedTuple
from typing import Annotated, Any, NamedTuple
from dateutil.tz import tzlocal
from pydantic import field_validator
from pydantic import PlainSerializer, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from mealie.core.settings.themes import Theme
@@ -19,6 +19,29 @@ class ScheduleTime(NamedTuple):
minute: int
class FeatureDetails(NamedTuple):
enabled: bool
"""Indicates if the feature is enabled or not"""
description: str | None
"""Short description describing why the feature is not ready"""
def __str__(self):
s = f"Enabled: {self.enabled}"
if not self.enabled and self.description:
s += f"\nReason: {self.description}"
return s
MaskedNoneString = Annotated[
str | None,
PlainSerializer(lambda x: None if x is None else "*****", return_type=str | None),
]
"""
Custom serializer for sensitive settings. If the setting is None, then will serialize as null, otherwise,
the secret will be serialized as '*****'
"""
def determine_secrets(data_dir: Path, secret: str, production: bool) -> str:
if not production:
return "shh-secret-test-key"
@@ -200,12 +223,16 @@ class AppSettings(AppLoggingSettings):
SMTP_PORT: str | None = "587"
SMTP_FROM_NAME: str | None = "Mealie"
SMTP_FROM_EMAIL: str | None = None
SMTP_USER: str | None = None
SMTP_PASSWORD: str | None = None
SMTP_USER: MaskedNoneString = None
SMTP_PASSWORD: MaskedNoneString = None
SMTP_AUTH_STRATEGY: str | None = "TLS" # Options: 'TLS', 'SSL', 'NONE'
@property
def SMTP_ENABLE(self) -> bool:
return self.SMTP_FEATURE.enabled
@property
def SMTP_FEATURE(self) -> FeatureDetails:
return AppSettings.validate_smtp(
self.SMTP_HOST,
self.SMTP_PORT,
@@ -225,15 +252,30 @@ class AppSettings(AppLoggingSettings):
strategy: str | None = None,
user: str | None = None,
password: str | None = None,
) -> bool:
) -> FeatureDetails:
"""Validates all SMTP variables are set"""
required = {host, port, from_name, from_email, strategy}
description = None
required = {
"SMTP_HOST": host,
"SMTP_PORT": port,
"SMTP_FROM_NAME": from_name,
"SMTP_FROM_EMAIL": from_email,
"SMTP_AUTH_STRATEGY": strategy,
}
missing_values = [key for (key, value) in required.items() if value is None]
if missing_values:
description = f"Missing required values for {missing_values}"
if strategy and strategy.upper() in {"TLS", "SSL"}:
required.add(user)
required.add(password)
required["SMTP_USER"] = user
required["SMTP_PASSWORD"] = password
if not description:
missing_values = [key for (key, value) in required.items() if value is None]
description = f"Missing required values for {missing_values} because SMTP_AUTH_STRATEGY is not None"
return "" not in required and None not in required
not_none = "" not in required.values() and None not in required.values()
return FeatureDetails(enabled=not_none, description=description)
# ===============================================
# LDAP Configuration
@@ -245,31 +287,43 @@ class AppSettings(AppLoggingSettings):
LDAP_ENABLE_STARTTLS: bool = False
LDAP_BASE_DN: str | None = None
LDAP_QUERY_BIND: str | None = None
LDAP_QUERY_PASSWORD: str | None = None
LDAP_QUERY_PASSWORD: MaskedNoneString = None
LDAP_USER_FILTER: str | None = None
LDAP_ADMIN_FILTER: str | None = None
LDAP_ID_ATTRIBUTE: str = "uid"
LDAP_MAIL_ATTRIBUTE: str = "mail"
LDAP_NAME_ATTRIBUTE: str = "name"
@property
def LDAP_FEATURE(self) -> FeatureDetails:
description = None if self.LDAP_AUTH_ENABLED else "LDAP_AUTH_ENABLED is false"
required = {
"LDAP_SERVER_URL": self.LDAP_SERVER_URL,
"LDAP_BASE_DN": self.LDAP_BASE_DN,
"LDAP_ID_ATTRIBUTE": self.LDAP_ID_ATTRIBUTE,
"LDAP_MAIL_ATTRIBUTE": self.LDAP_MAIL_ATTRIBUTE,
"LDAP_NAME_ATTRIBUTE": self.LDAP_NAME_ATTRIBUTE,
}
not_none = None not in required.values()
if not not_none and not description:
missing_values = [key for (key, value) in required.items() if value is None]
description = f"Missing required values for {missing_values}"
return FeatureDetails(
enabled=self.LDAP_AUTH_ENABLED and not_none,
description=description,
)
@property
def LDAP_ENABLED(self) -> bool:
"""Validates LDAP settings are all set"""
required = {
self.LDAP_SERVER_URL,
self.LDAP_BASE_DN,
self.LDAP_ID_ATTRIBUTE,
self.LDAP_MAIL_ATTRIBUTE,
self.LDAP_NAME_ATTRIBUTE,
}
not_none = None not in required
return self.LDAP_AUTH_ENABLED and not_none
return self.LDAP_FEATURE.enabled
# ===============================================
# OIDC Configuration
OIDC_AUTH_ENABLED: bool = False
OIDC_CLIENT_ID: str | None = None
OIDC_CLIENT_SECRET: str | None = None
OIDC_CLIENT_SECRET: MaskedNoneString = None
OIDC_CONFIGURATION_URL: str | None = None
OIDC_SIGNUP_ENABLED: bool = True
OIDC_USER_GROUP: str | None = None
@@ -279,6 +333,7 @@ class AppSettings(AppLoggingSettings):
OIDC_REMEMBER_ME: bool = False
OIDC_USER_CLAIM: str = "email"
OIDC_GROUPS_CLAIM: str | None = "groups"
OIDC_SCOPES_OVERRIDE: str | None = None
OIDC_TLS_CACERTFILE: str | None = None
@property
@@ -286,29 +341,41 @@ class AppSettings(AppLoggingSettings):
return self.OIDC_USER_GROUP is not None or self.OIDC_ADMIN_GROUP is not None
@property
def OIDC_READY(self) -> bool:
"""Validates OIDC settings are all set"""
def OIDC_FEATURE(self) -> FeatureDetails:
description = None if self.OIDC_AUTH_ENABLED else "OIDC_AUTH_ENABLED is false"
required = {
self.OIDC_CLIENT_ID,
self.OIDC_CLIENT_SECRET,
self.OIDC_CONFIGURATION_URL,
self.OIDC_USER_CLAIM,
"OIDC_CLIENT_ID": self.OIDC_CLIENT_ID,
"OIDC_CLIENT_SECRET": self.OIDC_CLIENT_SECRET,
"OIDC_CONFIGURATION_URL": self.OIDC_CONFIGURATION_URL,
"OIDC_USER_CLAIM": self.OIDC_USER_CLAIM,
}
not_none = None not in required
valid_group_claim = True
not_none = None not in required.values()
if not not_none and not description:
missing_values = [key for (key, value) in required.items() if value is None]
description = f"Missing required values for {missing_values}"
valid_group_claim = True
if self.OIDC_REQUIRES_GROUP_CLAIM and self.OIDC_GROUPS_CLAIM is None:
if not description:
description = "OIDC_GROUPS_CLAIM is required when OIDC_USER_GROUP or OIDC_ADMIN_GROUP are provided"
valid_group_claim = False
return self.OIDC_AUTH_ENABLED and not_none and valid_group_claim
return FeatureDetails(
enabled=self.OIDC_AUTH_ENABLED and not_none and valid_group_claim,
description=description,
)
@property
def OIDC_READY(self) -> bool:
"""Validates OIDC settings are all set"""
return self.OIDC_FEATURE.enabled
# ===============================================
# OpenAI Configuration
OPENAI_BASE_URL: str | None = None
"""The base URL for the OpenAI API. Leave this unset for most usecases"""
OPENAI_API_KEY: str | None = None
OPENAI_API_KEY: MaskedNoneString = None
"""Your OpenAI API key. Required to enable OpenAI features"""
OPENAI_MODEL: str = "gpt-4o"
"""Which OpenAI model to send requests to. Leave this unset for most usecases"""
@@ -333,6 +400,24 @@ class AppSettings(AppLoggingSettings):
The number of seconds to wait for an OpenAI request to complete before cancelling the request
"""
@property
def OPENAI_FEATURE(self) -> FeatureDetails:
description = None
if not self.OPENAI_API_KEY:
description = "OPENAI_API_KEY is not set"
elif self.OPENAI_MODEL:
description = "OPENAI_MODEL is not set"
return FeatureDetails(
enabled=bool(self.OPENAI_API_KEY and self.OPENAI_MODEL),
description=description,
)
@property
def OPENAI_ENABLED(self) -> bool:
"""Validates OpenAI settings are all set"""
return self.OPENAI_FEATURE.enabled
# ===============================================
# Web Concurrency
@@ -346,13 +431,17 @@ class AppSettings(AppLoggingSettings):
def WORKERS(self) -> int:
return max(1, self.WORKER_PER_CORE * self.UVICORN_WORKERS)
@property
def OPENAI_ENABLED(self) -> bool:
"""Validates OpenAI settings are all set"""
return bool(self.OPENAI_API_KEY and self.OPENAI_MODEL)
model_config = SettingsConfigDict(arbitrary_types_allowed=True, extra="allow")
# ===============================================
# TLS
TLS_CERTIFICATE_PATH: str | os.PathLike[str] | None = None
"""Path where the certificate resides."""
TLS_PRIVATE_KEY_PATH: str | os.PathLike[str] | None = None
"""Path where the private key resides."""
def app_settings_constructor(data_dir: Path, production: bool, env_file: Path, env_encoding="utf-8") -> AppSettings:
"""

View File

@@ -151,6 +151,14 @@ class User(SqlAlchemyBase, BaseMixins):
else:
self.household = None
if self.group is None:
raise ValueError(f"Group {group} does not exist; cannot create user")
if self.household is None:
raise ValueError(
f'Household "{household}" does not exist on group '
f'"{self.group.name}" ({self.group.id}); cannot create user'
)
self.rated_recipes = []
self.password = password

View File

@@ -13,6 +13,8 @@ def main():
log_config=log_config(),
workers=settings.WORKERS,
forwarded_allow_ips=settings.HOST_IP,
ssl_keyfile=settings.TLS_PRIVATE_KEY_PATH,
ssl_certfile=settings.TLS_CERTIFICATE_PATH,
)

View File

@@ -2,6 +2,7 @@ from collections.abc import Callable
from logging import Logger
from typing import Generic, TypeVar
import sqlalchemy.exc
from fastapi import HTTPException, status
from pydantic import UUID4, BaseModel
@@ -57,10 +58,16 @@ class HttpRepo(Generic[C, R, U]):
# Respond
msg = self.get_exception_message(ex)
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail=ErrorResponse.respond(message=msg, exception=str(ex)),
)
if isinstance(ex, sqlalchemy.exc.NoResultFound):
raise HTTPException(
status.HTTP_404_NOT_FOUND,
detail=ErrorResponse.respond(message=msg, exception=str(ex)),
)
else:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail=ErrorResponse.respond(message=msg, exception=str(ex)),
)
def create_one(self, data: C) -> R | None:
item: R | None = None

View File

@@ -28,8 +28,12 @@ remember_me_duration = timedelta(days=14)
settings = get_app_settings()
if settings.OIDC_READY:
oauth = OAuth()
groups_claim = settings.OIDC_GROUPS_CLAIM if settings.OIDC_REQUIRES_GROUP_CLAIM else ""
scope = f"openid email profile {groups_claim}"
scope = None
if settings.OIDC_SCOPES_OVERRIDE:
scope = settings.OIDC_SCOPES_OVERRIDE
else:
groups_claim = settings.OIDC_GROUPS_CLAIM if settings.OIDC_REQUIRES_GROUP_CLAIM else ""
scope = f"openid email profile {groups_claim}"
client_args = {"scope": scope.rstrip()}
if settings.OIDC_TLS_CACERTFILE:
client_args["verify"] = settings.OIDC_TLS_CACERTFILE

View File

@@ -6,6 +6,7 @@ from fastapi import APIRouter, Depends, HTTPException
from pydantic import UUID4
from mealie.core.exceptions import mealie_registered_exceptions
from mealie.repos.all_repositories import get_repositories
from mealie.routes._base import BaseCrudController, controller
from mealie.routes._base.mixins import HttpRepo
from mealie.routes._base.routers import MealieCrudRoute
@@ -26,9 +27,13 @@ router = APIRouter(prefix="/households/cookbooks", tags=["Households: Cookbooks"
@controller(router)
class GroupCookbookController(BaseCrudController):
@cached_property
def repo(self):
def cookbooks(self):
return self.repos.cookbooks
@cached_property
def group_cookbooks(self):
return get_repositories(self.session, group_id=self.group_id, household_id=None).cookbooks
def registered_exceptions(self, ex: type[Exception]) -> str:
registered = {
**mealie_registered_exceptions(self.translator),
@@ -38,14 +43,15 @@ class GroupCookbookController(BaseCrudController):
@cached_property
def mixins(self):
return HttpRepo[CreateCookBook, ReadCookBook, UpdateCookBook](
self.repo,
self.cookbooks,
self.logger,
self.registered_exceptions,
)
@router.get("", response_model=CookBookPagination)
def get_all(self, q: PaginationQuery = Depends(PaginationQuery)):
response = self.repo.page_all(
# Fetch all cookbooks for the group, rather than the household
response = self.group_cookbooks.page_all(
pagination=q,
override=ReadCookBook,
)
@@ -106,7 +112,8 @@ class GroupCookbookController(BaseCrudController):
except ValueError:
match_attr = "slug"
cookbook = self.repo.get_one(item_id, match_attr)
# Allow fetching other households' cookbooks
cookbook = self.group_cookbooks.get_one(item_id, match_attr)
if cookbook is None:
raise HTTPException(status_code=404)

View File

@@ -105,8 +105,8 @@ class BaseRecipeController(BaseCrudController):
return get_repositories(self.session, group_id=self.group_id, household_id=None).recipes
@cached_property
def cookbooks_repo(self) -> RepositoryGeneric[ReadCookBook, CookBook]:
return self.repos.cookbooks
def group_cookbooks(self) -> RepositoryGeneric[ReadCookBook, CookBook]:
return get_repositories(self.session, group_id=self.group_id, household_id=None).cookbooks
@cached_property
def service(self) -> RecipeService:
@@ -354,7 +354,7 @@ class RecipeController(BaseRecipeController):
cb_match_attr = "id"
except ValueError:
cb_match_attr = "slug"
cookbook_data = self.cookbooks_repo.get_one(search_query.cookbook, cb_match_attr)
cookbook_data = self.group_cookbooks.get_one(search_query.cookbook, cb_match_attr)
if cookbook_data is None:
raise HTTPException(status_code=404, detail="cookbook not found")

View File

@@ -14,7 +14,7 @@ def main():
logger = root_logger.get_logger()
with session_context() as session:
repos = AllRepositories(session)
repos = AllRepositories(session, group_id=None, household_id=None)
user = repos.users.get_one(confirmed, "email")

View File

@@ -14,7 +14,7 @@ def main():
logger = root_logger.get_logger()
with session_context() as session:
repos = AllRepositories(session)
repos = AllRepositories(session, group_id=None, household_id=None)
user_service = UserService(repos)
locked_users = user_service.get_locked_users()

208
poetry.lock generated
View File

@@ -13,13 +13,13 @@ files = [
[[package]]
name = "alembic"
version = "1.13.3"
version = "1.14.0"
description = "A database migration tool for SQLAlchemy."
optional = false
python-versions = ">=3.8"
files = [
{file = "alembic-1.13.3-py3-none-any.whl", hash = "sha256:908e905976d15235fae59c9ac42c4c5b75cfcefe3d27c0fbf7ae15a37715d80e"},
{file = "alembic-1.13.3.tar.gz", hash = "sha256:203503117415561e203aa14541740643a611f641517f0209fcae63e9fa09f1a2"},
{file = "alembic-1.14.0-py3-none-any.whl", hash = "sha256:99bd884ca390466db5e27ffccff1d179ec5c05c965cfefc0607e69f9e411cb25"},
{file = "alembic-1.14.0.tar.gz", hash = "sha256:b00892b53b3642d0b8dbedba234dbf1924b69be83a9a769d5a624b01094e304b"},
]
[package.dependencies]
@@ -603,13 +603,13 @@ test = ["pytest (>=6)"]
[[package]]
name = "extruct"
version = "0.17.0"
version = "0.18.0"
description = "Extract embedded metadata from HTML markup"
optional = false
python-versions = ">=3.8"
files = [
{file = "extruct-0.17.0-py2.py3-none-any.whl", hash = "sha256:5f1d8e307fbb0c41f64ce486ddfaf16dc67e4b8f6e9570c57b123409ee37a307"},
{file = "extruct-0.17.0.tar.gz", hash = "sha256:a94c0be5b5fd95a8370204ecc02687bd27845d536055d8d1c69a0a30da0420c7"},
{file = "extruct-0.18.0-py2.py3-none-any.whl", hash = "sha256:1e739985da705c3348c7614dc169e7780caf20908338fa5f4c6e48576df6f000"},
{file = "extruct-0.18.0.tar.gz", hash = "sha256:b5b48d459003b27c05ee91527b14a5a31735231aaf85d2b1f331d4db879318dd"},
]
[package.dependencies]
@@ -627,13 +627,13 @@ cli = ["requests"]
[[package]]
name = "fastapi"
version = "0.115.3"
version = "0.115.4"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
optional = false
python-versions = ">=3.8"
files = [
{file = "fastapi-0.115.3-py3-none-any.whl", hash = "sha256:8035e8f9a2b0aa89cea03b6c77721178ed5358e1aea4cd8570d9466895c0638c"},
{file = "fastapi-0.115.3.tar.gz", hash = "sha256:c091c6a35599c036d676fa24bd4a6e19fa30058d93d950216cdc672881f6f7db"},
{file = "fastapi-0.115.4-py3-none-any.whl", hash = "sha256:0b504a063ffb3cf96a5e27dc1bc32c80ca743a2528574f9cdc77daa2d31b4742"},
{file = "fastapi-0.115.4.tar.gz", hash = "sha256:db653475586b091cb8b2fec2ac54a680ac6a158e07406e1abae31679e8826349"},
]
[package.dependencies]
@@ -1464,13 +1464,13 @@ pyyaml = ">=5.1"
[[package]]
name = "mkdocs-material"
version = "9.5.42"
version = "9.5.44"
description = "Documentation that simply works"
optional = false
python-versions = ">=3.8"
files = [
{file = "mkdocs_material-9.5.42-py3-none-any.whl", hash = "sha256:452a7c5d21284b373f36b981a2cbebfff59263feebeede1bc28652e9c5bbe316"},
{file = "mkdocs_material-9.5.42.tar.gz", hash = "sha256:92779b5e9b5934540c574c11647131d217dc540dce72b05feeda088c8eb1b8f2"},
{file = "mkdocs_material-9.5.44-py3-none-any.whl", hash = "sha256:47015f9c167d58a5ff5e682da37441fc4d66a1c79334bfc08d774763cacf69ca"},
{file = "mkdocs_material-9.5.44.tar.gz", hash = "sha256:f3a6c968e524166b3f3ed1fb97d3ed3e0091183b0545cedf7156a2a6804c56c0"},
]
[package.dependencies]
@@ -1598,13 +1598,13 @@ signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"]
[[package]]
name = "openai"
version = "1.52.2"
version = "1.54.3"
description = "The official Python library for the openai API"
optional = false
python-versions = ">=3.7.1"
python-versions = ">=3.8"
files = [
{file = "openai-1.52.2-py3-none-any.whl", hash = "sha256:57e9e37bc407f39bb6ec3a27d7e8fb9728b2779936daa1fcf95df17d3edfaccc"},
{file = "openai-1.52.2.tar.gz", hash = "sha256:87b7d0f69d85f5641678d414b7ee3082363647a5c66a462ed7f3ccb59582da0d"},
{file = "openai-1.54.3-py3-none-any.whl", hash = "sha256:f18dbaf09c50d70c4185b892a2a553f80681d1d866323a2da7f7be2f688615d5"},
{file = "openai-1.54.3.tar.gz", hash = "sha256:7511b74eeb894ac0b0253dc71f087a15d2e4d71d22d0088767205143d880cca6"},
]
[package.dependencies]
@@ -1622,69 +1622,69 @@ datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"]
[[package]]
name = "orjson"
version = "3.10.10"
version = "3.10.11"
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
optional = false
python-versions = ">=3.8"
files = [
{file = "orjson-3.10.10-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b788a579b113acf1c57e0a68e558be71d5d09aa67f62ca1f68e01117e550a998"},
{file = "orjson-3.10.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:804b18e2b88022c8905bb79bd2cbe59c0cd014b9328f43da8d3b28441995cda4"},
{file = "orjson-3.10.10-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9972572a1d042ec9ee421b6da69f7cc823da5962237563fa548ab17f152f0b9b"},
{file = "orjson-3.10.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc6993ab1c2ae7dd0711161e303f1db69062955ac2668181bfdf2dd410e65258"},
{file = "orjson-3.10.10-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d78e4cacced5781b01d9bc0f0cd8b70b906a0e109825cb41c1b03f9c41e4ce86"},
{file = "orjson-3.10.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6eb2598df518281ba0cbc30d24c5b06124ccf7e19169e883c14e0831217a0bc"},
{file = "orjson-3.10.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:23776265c5215ec532de6238a52707048401a568f0fa0d938008e92a147fe2c7"},
{file = "orjson-3.10.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8cc2a654c08755cef90b468ff17c102e2def0edd62898b2486767204a7f5cc9c"},
{file = "orjson-3.10.10-cp310-none-win32.whl", hash = "sha256:081b3fc6a86d72efeb67c13d0ea7c030017bd95f9868b1e329a376edc456153b"},
{file = "orjson-3.10.10-cp310-none-win_amd64.whl", hash = "sha256:ff38c5fb749347768a603be1fb8a31856458af839f31f064c5aa74aca5be9efe"},
{file = "orjson-3.10.10-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:879e99486c0fbb256266c7c6a67ff84f46035e4f8749ac6317cc83dacd7f993a"},
{file = "orjson-3.10.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:019481fa9ea5ff13b5d5d95e6fd5ab25ded0810c80b150c2c7b1cc8660b662a7"},
{file = "orjson-3.10.10-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0dd57eff09894938b4c86d4b871a479260f9e156fa7f12f8cad4b39ea8028bb5"},
{file = "orjson-3.10.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dbde6d70cd95ab4d11ea8ac5e738e30764e510fc54d777336eec09bb93b8576c"},
{file = "orjson-3.10.10-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2625cb37b8fb42e2147404e5ff7ef08712099197a9cd38895006d7053e69d6"},
{file = "orjson-3.10.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbf3c20c6a7db69df58672a0d5815647ecf78c8e62a4d9bd284e8621c1fe5ccb"},
{file = "orjson-3.10.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:75c38f5647e02d423807d252ce4528bf6a95bd776af999cb1fb48867ed01d1f6"},
{file = "orjson-3.10.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:23458d31fa50ec18e0ec4b0b4343730928296b11111df5f547c75913714116b2"},
{file = "orjson-3.10.10-cp311-none-win32.whl", hash = "sha256:2787cd9dedc591c989f3facd7e3e86508eafdc9536a26ec277699c0aa63c685b"},
{file = "orjson-3.10.10-cp311-none-win_amd64.whl", hash = "sha256:6514449d2c202a75183f807bc755167713297c69f1db57a89a1ef4a0170ee269"},
{file = "orjson-3.10.10-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:8564f48f3620861f5ef1e080ce7cd122ee89d7d6dacf25fcae675ff63b4d6e05"},
{file = "orjson-3.10.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5bf161a32b479034098c5b81f2608f09167ad2fa1c06abd4e527ea6bf4837a9"},
{file = "orjson-3.10.10-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:68b65c93617bcafa7f04b74ae8bc2cc214bd5cb45168a953256ff83015c6747d"},
{file = "orjson-3.10.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8e28406f97fc2ea0c6150f4c1b6e8261453318930b334abc419214c82314f85"},
{file = "orjson-3.10.10-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4d0d9fe174cc7a5bdce2e6c378bcdb4c49b2bf522a8f996aa586020e1b96cee"},
{file = "orjson-3.10.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3be81c42f1242cbed03cbb3973501fcaa2675a0af638f8be494eaf37143d999"},
{file = "orjson-3.10.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:65f9886d3bae65be026219c0a5f32dbbe91a9e6272f56d092ab22561ad0ea33b"},
{file = "orjson-3.10.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:730ed5350147db7beb23ddaf072f490329e90a1d059711d364b49fe352ec987b"},
{file = "orjson-3.10.10-cp312-none-win32.whl", hash = "sha256:a8f4bf5f1c85bea2170800020d53a8877812892697f9c2de73d576c9307a8a5f"},
{file = "orjson-3.10.10-cp312-none-win_amd64.whl", hash = "sha256:384cd13579a1b4cd689d218e329f459eb9ddc504fa48c5a83ef4889db7fd7a4f"},
{file = "orjson-3.10.10-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:44bffae68c291f94ff5a9b4149fe9d1bdd4cd0ff0fb575bcea8351d48db629a1"},
{file = "orjson-3.10.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e27b4c6437315df3024f0835887127dac2a0a3ff643500ec27088d2588fa5ae1"},
{file = "orjson-3.10.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca84df16d6b49325a4084fd8b2fe2229cb415e15c46c529f868c3387bb1339d"},
{file = "orjson-3.10.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c14ce70e8f39bd71f9f80423801b5d10bf93d1dceffdecd04df0f64d2c69bc01"},
{file = "orjson-3.10.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:24ac62336da9bda1bd93c0491eff0613003b48d3cb5d01470842e7b52a40d5b4"},
{file = "orjson-3.10.10-cp313-none-win32.whl", hash = "sha256:eb0a42831372ec2b05acc9ee45af77bcaccbd91257345f93780a8e654efc75db"},
{file = "orjson-3.10.10-cp313-none-win_amd64.whl", hash = "sha256:f0c4f37f8bf3f1075c6cc8dd8a9f843689a4b618628f8812d0a71e6968b95ffd"},
{file = "orjson-3.10.10-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:829700cc18503efc0cf502d630f612884258020d98a317679cd2054af0259568"},
{file = "orjson-3.10.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0ceb5e0e8c4f010ac787d29ae6299846935044686509e2f0f06ed441c1ca949"},
{file = "orjson-3.10.10-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c25908eb86968613216f3db4d3003f1c45d78eb9046b71056ca327ff92bdbd4"},
{file = "orjson-3.10.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:218cb0bc03340144b6328a9ff78f0932e642199ac184dd74b01ad691f42f93ff"},
{file = "orjson-3.10.10-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e2277ec2cea3775640dc81ab5195bb5b2ada2fe0ea6eee4677474edc75ea6785"},
{file = "orjson-3.10.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:848ea3b55ab5ccc9d7bbd420d69432628b691fba3ca8ae3148c35156cbd282aa"},
{file = "orjson-3.10.10-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:e3e67b537ac0c835b25b5f7d40d83816abd2d3f4c0b0866ee981a045287a54f3"},
{file = "orjson-3.10.10-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:7948cfb909353fce2135dcdbe4521a5e7e1159484e0bb024c1722f272488f2b8"},
{file = "orjson-3.10.10-cp38-none-win32.whl", hash = "sha256:78bee66a988f1a333dc0b6257503d63553b1957889c17b2c4ed72385cd1b96ae"},
{file = "orjson-3.10.10-cp38-none-win_amd64.whl", hash = "sha256:f1d647ca8d62afeb774340a343c7fc023efacfd3a39f70c798991063f0c681dd"},
{file = "orjson-3.10.10-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:5a059afddbaa6dd733b5a2d76a90dbc8af790b993b1b5cb97a1176ca713b5df8"},
{file = "orjson-3.10.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f9b5c59f7e2a1a410f971c5ebc68f1995822837cd10905ee255f96074537ee6"},
{file = "orjson-3.10.10-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d5ef198bafdef4aa9d49a4165ba53ffdc0a9e1c7b6f76178572ab33118afea25"},
{file = "orjson-3.10.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aaf29ce0bb5d3320824ec3d1508652421000ba466abd63bdd52c64bcce9eb1fa"},
{file = "orjson-3.10.10-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dddd5516bcc93e723d029c1633ae79c4417477b4f57dad9bfeeb6bc0315e654a"},
{file = "orjson-3.10.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12f2003695b10817f0fa8b8fca982ed7f5761dcb0d93cff4f2f9f6709903fd7"},
{file = "orjson-3.10.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:672f9874a8a8fb9bb1b771331d31ba27f57702c8106cdbadad8bda5d10bc1019"},
{file = "orjson-3.10.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1dcbb0ca5fafb2b378b2c74419480ab2486326974826bbf6588f4dc62137570a"},
{file = "orjson-3.10.10-cp39-none-win32.whl", hash = "sha256:d9bbd3a4b92256875cb058c3381b782649b9a3c68a4aa9a2fff020c2f9cfc1be"},
{file = "orjson-3.10.10-cp39-none-win_amd64.whl", hash = "sha256:766f21487a53aee8524b97ca9582d5c6541b03ab6210fbaf10142ae2f3ced2aa"},
{file = "orjson-3.10.10.tar.gz", hash = "sha256:37949383c4df7b4337ce82ee35b6d7471e55195efa7dcb45ab8226ceadb0fe3b"},
{file = "orjson-3.10.11-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6dade64687f2bd7c090281652fe18f1151292d567a9302b34c2dbb92a3872f1f"},
{file = "orjson-3.10.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82f07c550a6ccd2b9290849b22316a609023ed851a87ea888c0456485a7d196a"},
{file = "orjson-3.10.11-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd9a187742d3ead9df2e49240234d728c67c356516cf4db018833a86f20ec18c"},
{file = "orjson-3.10.11-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77b0fed6f209d76c1c39f032a70df2d7acf24b1812ca3e6078fd04e8972685a3"},
{file = "orjson-3.10.11-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63fc9d5fe1d4e8868f6aae547a7b8ba0a2e592929245fff61d633f4caccdcdd6"},
{file = "orjson-3.10.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65cd3e3bb4fbb4eddc3c1e8dce10dc0b73e808fcb875f9fab40c81903dd9323e"},
{file = "orjson-3.10.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6f67c570602300c4befbda12d153113b8974a3340fdcf3d6de095ede86c06d92"},
{file = "orjson-3.10.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1f39728c7f7d766f1f5a769ce4d54b5aaa4c3f92d5b84817053cc9995b977acc"},
{file = "orjson-3.10.11-cp310-none-win32.whl", hash = "sha256:1789d9db7968d805f3d94aae2c25d04014aae3a2fa65b1443117cd462c6da647"},
{file = "orjson-3.10.11-cp310-none-win_amd64.whl", hash = "sha256:5576b1e5a53a5ba8f8df81872bb0878a112b3ebb1d392155f00f54dd86c83ff6"},
{file = "orjson-3.10.11-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1444f9cb7c14055d595de1036f74ecd6ce15f04a715e73f33bb6326c9cef01b6"},
{file = "orjson-3.10.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdec57fe3b4bdebcc08a946db3365630332dbe575125ff3d80a3272ebd0ddafe"},
{file = "orjson-3.10.11-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4eed32f33a0ea6ef36ccc1d37f8d17f28a1d6e8eefae5928f76aff8f1df85e67"},
{file = "orjson-3.10.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80df27dd8697242b904f4ea54820e2d98d3f51f91e97e358fc13359721233e4b"},
{file = "orjson-3.10.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:705f03cee0cb797256d54de6695ef219e5bc8c8120b6654dd460848d57a9af3d"},
{file = "orjson-3.10.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03246774131701de8e7059b2e382597da43144a9a7400f178b2a32feafc54bd5"},
{file = "orjson-3.10.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8b5759063a6c940a69c728ea70d7c33583991c6982915a839c8da5f957e0103a"},
{file = "orjson-3.10.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:677f23e32491520eebb19c99bb34675daf5410c449c13416f7f0d93e2cf5f981"},
{file = "orjson-3.10.11-cp311-none-win32.whl", hash = "sha256:a11225d7b30468dcb099498296ffac36b4673a8398ca30fdaec1e6c20df6aa55"},
{file = "orjson-3.10.11-cp311-none-win_amd64.whl", hash = "sha256:df8c677df2f9f385fcc85ab859704045fa88d4668bc9991a527c86e710392bec"},
{file = "orjson-3.10.11-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:360a4e2c0943da7c21505e47cf6bd725588962ff1d739b99b14e2f7f3545ba51"},
{file = "orjson-3.10.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:496e2cb45de21c369079ef2d662670a4892c81573bcc143c4205cae98282ba97"},
{file = "orjson-3.10.11-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7dfa8db55c9792d53c5952900c6a919cfa377b4f4534c7a786484a6a4a350c19"},
{file = "orjson-3.10.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:51f3382415747e0dbda9dade6f1e1a01a9d37f630d8c9049a8ed0e385b7a90c0"},
{file = "orjson-3.10.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f35a1b9f50a219f470e0e497ca30b285c9f34948d3c8160d5ad3a755d9299433"},
{file = "orjson-3.10.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f3b7c5803138e67028dde33450e054c87e0703afbe730c105f1fcd873496d5"},
{file = "orjson-3.10.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f91d9eb554310472bd09f5347950b24442600594c2edc1421403d7610a0998fd"},
{file = "orjson-3.10.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dfbb2d460a855c9744bbc8e36f9c3a997c4b27d842f3d5559ed54326e6911f9b"},
{file = "orjson-3.10.11-cp312-none-win32.whl", hash = "sha256:d4a62c49c506d4d73f59514986cadebb7e8d186ad510c518f439176cf8d5359d"},
{file = "orjson-3.10.11-cp312-none-win_amd64.whl", hash = "sha256:f1eec3421a558ff7a9b010a6c7effcfa0ade65327a71bb9b02a1c3b77a247284"},
{file = "orjson-3.10.11-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c46294faa4e4d0eb73ab68f1a794d2cbf7bab33b1dda2ac2959ffb7c61591899"},
{file = "orjson-3.10.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52e5834d7d6e58a36846e059d00559cb9ed20410664f3ad156cd2cc239a11230"},
{file = "orjson-3.10.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2fc947e5350fdce548bfc94f434e8760d5cafa97fb9c495d2fef6757aa02ec0"},
{file = "orjson-3.10.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0efabbf839388a1dab5b72b5d3baedbd6039ac83f3b55736eb9934ea5494d258"},
{file = "orjson-3.10.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a3f29634260708c200c4fe148e42b4aae97d7b9fee417fbdd74f8cfc265f15b0"},
{file = "orjson-3.10.11-cp313-none-win32.whl", hash = "sha256:1a1222ffcee8a09476bbdd5d4f6f33d06d0d6642df2a3d78b7a195ca880d669b"},
{file = "orjson-3.10.11-cp313-none-win_amd64.whl", hash = "sha256:bc274ac261cc69260913b2d1610760e55d3c0801bb3457ba7b9004420b6b4270"},
{file = "orjson-3.10.11-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:19b3763e8bbf8ad797df6b6b5e0fc7c843ec2e2fc0621398534e0c6400098f87"},
{file = "orjson-3.10.11-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1be83a13312e5e58d633580c5eb8d0495ae61f180da2722f20562974188af205"},
{file = "orjson-3.10.11-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:afacfd1ab81f46dedd7f6001b6d4e8de23396e4884cd3c3436bd05defb1a6446"},
{file = "orjson-3.10.11-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cb4d0bea56bba596723d73f074c420aec3b2e5d7d30698bc56e6048066bd560c"},
{file = "orjson-3.10.11-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96ed1de70fcb15d5fed529a656df29f768187628727ee2788344e8a51e1c1350"},
{file = "orjson-3.10.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4bfb30c891b530f3f80e801e3ad82ef150b964e5c38e1fb8482441c69c35c61c"},
{file = "orjson-3.10.11-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d496c74fc2b61341e3cefda7eec21b7854c5f672ee350bc55d9a4997a8a95204"},
{file = "orjson-3.10.11-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:655a493bac606655db9a47fe94d3d84fc7f3ad766d894197c94ccf0c5408e7d3"},
{file = "orjson-3.10.11-cp38-none-win32.whl", hash = "sha256:b9546b278c9fb5d45380f4809e11b4dd9844ca7aaf1134024503e134ed226161"},
{file = "orjson-3.10.11-cp38-none-win_amd64.whl", hash = "sha256:b592597fe551d518f42c5a2eb07422eb475aa8cfdc8c51e6da7054b836b26782"},
{file = "orjson-3.10.11-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c95f2ecafe709b4e5c733b5e2768ac569bed308623c85806c395d9cca00e08af"},
{file = "orjson-3.10.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80c00d4acded0c51c98754fe8218cb49cb854f0f7eb39ea4641b7f71732d2cb7"},
{file = "orjson-3.10.11-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:461311b693d3d0a060439aa669c74f3603264d4e7a08faa68c47ae5a863f352d"},
{file = "orjson-3.10.11-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52ca832f17d86a78cbab86cdc25f8c13756ebe182b6fc1a97d534051c18a08de"},
{file = "orjson-3.10.11-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c57ea78a753812f528178aa2f1c57da633754c91d2124cb28991dab4c79a54"},
{file = "orjson-3.10.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7fcfc6f7ca046383fb954ba528587e0f9336828b568282b27579c49f8e16aad"},
{file = "orjson-3.10.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:86b9dd983857970c29e4c71bb3e95ff085c07d3e83e7c46ebe959bac07ebd80b"},
{file = "orjson-3.10.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4d83f87582d223e54efb2242a79547611ba4ebae3af8bae1e80fa9a0af83bb7f"},
{file = "orjson-3.10.11-cp39-none-win32.whl", hash = "sha256:9fd0ad1c129bc9beb1154c2655f177620b5beaf9a11e0d10bac63ef3fce96950"},
{file = "orjson-3.10.11-cp39-none-win_amd64.whl", hash = "sha256:10f416b2a017c8bd17f325fb9dee1fb5cdd7a54e814284896b7c3f2763faa017"},
{file = "orjson-3.10.11.tar.gz", hash = "sha256:e35b6d730de6384d5b2dab5fd23f0d76fae8bbc8c353c2f78210aa5fa4beb3ef"},
]
[[package]]
@@ -2174,13 +2174,13 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
[[package]]
name = "pydantic-settings"
version = "2.6.0"
version = "2.6.1"
description = "Settings management using Pydantic"
optional = false
python-versions = ">=3.8"
files = [
{file = "pydantic_settings-2.6.0-py3-none-any.whl", hash = "sha256:4a819166f119b74d7f8c765196b165f95cc7487ce58ea27dec8a5a26be0970e0"},
{file = "pydantic_settings-2.6.0.tar.gz", hash = "sha256:44a1804abffac9e6a30372bb45f6cafab945ef5af25e66b1c634c01dd39e0188"},
{file = "pydantic_settings-2.6.1-py3-none-any.whl", hash = "sha256:7fb0637c786a558d3103436278a7c4f1cfd29ba8973238a50c5bb9a55387da87"},
{file = "pydantic_settings-2.6.1.tar.gz", hash = "sha256:e0f92546d8a9923cb8941689abf85d6601a8c19a23e97a34b2964a2e3f813ca0"},
]
[package.dependencies]
@@ -2413,13 +2413,13 @@ pyasn1_modules = ">=0.1.5"
[[package]]
name = "python-multipart"
version = "0.0.12"
version = "0.0.17"
description = "A streaming multipart parser for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "python_multipart-0.0.12-py3-none-any.whl", hash = "sha256:43dcf96cf65888a9cd3423544dd0d75ac10f7aa0c3c28a175bbcd00c9ce1aebf"},
{file = "python_multipart-0.0.12.tar.gz", hash = "sha256:045e1f98d719c1ce085ed7f7e1ef9d8ccc8c02ba02b5566d5f7521410ced58cb"},
{file = "python_multipart-0.0.17-py3-none-any.whl", hash = "sha256:15dc4f487e0a9476cc1201261188ee0940165cffc94429b6fc565c4d3045cb5d"},
{file = "python_multipart-0.0.17.tar.gz", hash = "sha256:41330d831cae6e2f22902704ead2826ea038d0419530eadff3ea80175aec5538"},
]
[[package]]
@@ -2796,13 +2796,13 @@ rsa = ["oauthlib[signedtoken] (>=3.0.0)"]
[[package]]
name = "rich"
version = "13.9.3"
version = "13.9.4"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
optional = false
python-versions = ">=3.8.0"
files = [
{file = "rich-13.9.3-py3-none-any.whl", hash = "sha256:9836f5096eb2172c9e77df411c1b009bace4193d6a481d534fea75ebba758283"},
{file = "rich-13.9.3.tar.gz", hash = "sha256:bc1e01b899537598cf02579d2b9f4a415104d3fc439313a7a2c165d76557a08e"},
{file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"},
{file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"},
]
[package.dependencies]
@@ -2815,29 +2815,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]]
name = "ruff"
version = "0.7.1"
version = "0.7.3"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.7.1-py3-none-linux_armv6l.whl", hash = "sha256:cb1bc5ed9403daa7da05475d615739cc0212e861b7306f314379d958592aaa89"},
{file = "ruff-0.7.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:27c1c52a8d199a257ff1e5582d078eab7145129aa02721815ca8fa4f9612dc35"},
{file = "ruff-0.7.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:588a34e1ef2ea55b4ddfec26bbe76bc866e92523d8c6cdec5e8aceefeff02d99"},
{file = "ruff-0.7.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94fc32f9cdf72dc75c451e5f072758b118ab8100727168a3df58502b43a599ca"},
{file = "ruff-0.7.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:985818742b833bffa543a84d1cc11b5e6871de1b4e0ac3060a59a2bae3969250"},
{file = "ruff-0.7.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32f1e8a192e261366c702c5fb2ece9f68d26625f198a25c408861c16dc2dea9c"},
{file = "ruff-0.7.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:699085bf05819588551b11751eff33e9ca58b1b86a6843e1b082a7de40da1565"},
{file = "ruff-0.7.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:344cc2b0814047dc8c3a8ff2cd1f3d808bb23c6658db830d25147339d9bf9ea7"},
{file = "ruff-0.7.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4316bbf69d5a859cc937890c7ac7a6551252b6a01b1d2c97e8fc96e45a7c8b4a"},
{file = "ruff-0.7.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79d3af9dca4c56043e738a4d6dd1e9444b6d6c10598ac52d146e331eb155a8ad"},
{file = "ruff-0.7.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c5c121b46abde94a505175524e51891f829414e093cd8326d6e741ecfc0a9112"},
{file = "ruff-0.7.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8422104078324ea250886954e48f1373a8fe7de59283d747c3a7eca050b4e378"},
{file = "ruff-0.7.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:56aad830af8a9db644e80098fe4984a948e2b6fc2e73891538f43bbe478461b8"},
{file = "ruff-0.7.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:658304f02f68d3a83c998ad8bf91f9b4f53e93e5412b8f2388359d55869727fd"},
{file = "ruff-0.7.1-py3-none-win32.whl", hash = "sha256:b517a2011333eb7ce2d402652ecaa0ac1a30c114fbbd55c6b8ee466a7f600ee9"},
{file = "ruff-0.7.1-py3-none-win_amd64.whl", hash = "sha256:f38c41fcde1728736b4eb2b18850f6d1e3eedd9678c914dede554a70d5241307"},
{file = "ruff-0.7.1-py3-none-win_arm64.whl", hash = "sha256:19aa200ec824c0f36d0c9114c8ec0087082021732979a359d6f3c390a6ff2a37"},
{file = "ruff-0.7.1.tar.gz", hash = "sha256:9d8a41d4aa2dad1575adb98a82870cf5db5f76b2938cf2206c22c940034a36f4"},
{file = "ruff-0.7.3-py3-none-linux_armv6l.whl", hash = "sha256:34f2339dc22687ec7e7002792d1f50712bf84a13d5152e75712ac08be565d344"},
{file = "ruff-0.7.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fb397332a1879b9764a3455a0bb1087bda876c2db8aca3a3cbb67b3dbce8cda0"},
{file = "ruff-0.7.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:37d0b619546103274e7f62643d14e1adcbccb242efda4e4bdb9544d7764782e9"},
{file = "ruff-0.7.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d59f0c3ee4d1a6787614e7135b72e21024875266101142a09a61439cb6e38a5"},
{file = "ruff-0.7.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:44eb93c2499a169d49fafd07bc62ac89b1bc800b197e50ff4633aed212569299"},
{file = "ruff-0.7.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6d0242ce53f3a576c35ee32d907475a8d569944c0407f91d207c8af5be5dae4e"},
{file = "ruff-0.7.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6b6224af8b5e09772c2ecb8dc9f3f344c1aa48201c7f07e7315367f6dd90ac29"},
{file = "ruff-0.7.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c50f95a82b94421c964fae4c27c0242890a20fe67d203d127e84fbb8013855f5"},
{file = "ruff-0.7.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7f3eff9961b5d2644bcf1616c606e93baa2d6b349e8aa8b035f654df252c8c67"},
{file = "ruff-0.7.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8963cab06d130c4df2fd52c84e9f10d297826d2e8169ae0c798b6221be1d1d2"},
{file = "ruff-0.7.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:61b46049d6edc0e4317fb14b33bd693245281a3007288b68a3f5b74a22a0746d"},
{file = "ruff-0.7.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:10ebce7696afe4644e8c1a23b3cf8c0f2193a310c18387c06e583ae9ef284de2"},
{file = "ruff-0.7.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3f36d56326b3aef8eeee150b700e519880d1aab92f471eefdef656fd57492aa2"},
{file = "ruff-0.7.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5d024301109a0007b78d57ab0ba190087b43dce852e552734ebf0b0b85e4fb16"},
{file = "ruff-0.7.3-py3-none-win32.whl", hash = "sha256:4ba81a5f0c5478aa61674c5a2194de8b02652f17addf8dfc40c8937e6e7d79fc"},
{file = "ruff-0.7.3-py3-none-win_amd64.whl", hash = "sha256:588a9ff2fecf01025ed065fe28809cd5a53b43505f48b69a1ac7707b1b7e4088"},
{file = "ruff-0.7.3-py3-none-win_arm64.whl", hash = "sha256:1713e2c5545863cdbfe2cbce21f69ffaf37b813bfd1fb3b90dc9a6f1963f5a8c"},
{file = "ruff-0.7.3.tar.gz", hash = "sha256:e1d1ba2e40b6e71a61b063354d04be669ab0d39c352461f3d789cac68b54a313"},
]
[[package]]
@@ -3416,4 +3416,4 @@ pgsql = ["psycopg2-binary"]
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "455f4f29104e6614f7ff7e899cf2d63302cd84dd693b59ee17d48ca05fd39543"
content-hash = "2a3b97688c700f6c01241c0559afa48bdf039399261e7cdd68eebad96dadb44f"

View File

@@ -3,7 +3,7 @@ authors = ["Hayden <hay-kot@pm.me>"]
description = "A Recipe Manager"
license = "AGPL"
name = "mealie"
version = "2.0.0"
version = "2.1.0"
[tool.poetry.scripts]
start = "mealie.app:main"
@@ -19,7 +19,7 @@ aniso8601 = "9.0.1"
appdirs = "1.4.4"
apprise = "^1.4.5"
bcrypt = "^4.0.1"
extruct = "^0.17.0"
extruct = "^0.18.0"
fastapi = "^0.115.0"
httpx = "^0.27.0"
lxml = "^5.0.0"
@@ -31,7 +31,7 @@ python = "^3.10"
python-dateutil = "^2.8.2"
python-dotenv = "^1.0.0"
python-ldap = "^3.3.1"
python-multipart = "^0.0.12"
python-multipart = "^0.0.17"
python-slugify = "^8.0.0"
recipe-scrapers = "^15.0.0"
requests = "^2.31.0"

View File

@@ -60,6 +60,8 @@ def test_create_cookbook(api_client: TestClient, unique_user: TestUser):
page_data = get_page_data(unique_user.group_id, unique_user.household_id)
response = api_client.post(api_routes.households_cookbooks, json=page_data, headers=unique_user.token)
assert response.status_code == 201
assert response.json()["groupId"] == unique_user.group_id
assert response.json()["householdId"] == unique_user.household_id
@pytest.mark.parametrize("name_input", ["", " ", "@"])
@@ -78,9 +80,22 @@ def test_create_cookbook_bad_name(api_client: TestClient, unique_user: TestUser,
assert response.status_code == 422
def test_read_cookbook(api_client: TestClient, unique_user: TestUser, cookbooks: list[TestCookbook]):
@pytest.mark.parametrize("use_other_household", [True, False])
def test_read_cookbook(
api_client: TestClient,
unique_user: TestUser,
h2_user: TestUser,
cookbooks: list[TestCookbook],
use_other_household: bool,
):
sample = random.choice(cookbooks)
response = api_client.get(api_routes.households_cookbooks_item_id(sample.id), headers=unique_user.token)
if use_other_household:
headers = h2_user.token
else:
headers = unique_user.token
# all households should be able to fetch all cookbooks
response = api_client.get(api_routes.households_cookbooks_item_id(sample.id), headers=headers)
assert response.status_code == 200
page_data = response.json()
@@ -111,6 +126,28 @@ def test_update_cookbook(api_client: TestClient, unique_user: TestUser, cookbook
assert page_data["slug"] == update_data["name"]
def test_update_cookbook_other_household(
api_client: TestClient, unique_user: TestUser, h2_user: TestUser, cookbooks: list[TestCookbook]
):
cookbook = random.choice(cookbooks)
update_data = get_page_data(unique_user.group_id, unique_user.household_id)
update_data["name"] = random_string(10)
response = api_client.put(
api_routes.households_cookbooks_item_id(cookbook.id), json=update_data, headers=h2_user.token
)
assert response.status_code == 404
response = api_client.get(api_routes.households_cookbooks_item_id(cookbook.id), headers=unique_user.token)
assert response.status_code == 200
page_data = response.json()
assert page_data["name"] != update_data["name"]
assert page_data["slug"] != update_data["name"]
def test_update_cookbooks_many(api_client: TestClient, unique_user: TestUser, cookbooks: list[TestCookbook]):
pages = [x.data for x in cookbooks]
@@ -135,6 +172,20 @@ def test_update_cookbooks_many(api_client: TestClient, unique_user: TestUser, co
assert str(know) in server_ids
def test_update_cookbooks_many_other_household(
api_client: TestClient, unique_user: TestUser, h2_user: TestUser, cookbooks: list[TestCookbook]
):
pages = [x.data for x in cookbooks]
reverse_order = sorted(pages, key=lambda x: x["position"], reverse=True)
for x, page in enumerate(reverse_order):
page["position"] = x
page["group_id"] = str(unique_user.group_id)
response = api_client.put(api_routes.households_cookbooks, json=utils.jsonify(reverse_order), headers=h2_user.token)
assert response.status_code == 404
def test_delete_cookbook(api_client: TestClient, unique_user: TestUser, cookbooks: list[TestCookbook]):
sample = random.choice(cookbooks)
response = api_client.delete(api_routes.households_cookbooks_item_id(sample.id), headers=unique_user.token)
@@ -145,6 +196,18 @@ def test_delete_cookbook(api_client: TestClient, unique_user: TestUser, cookbook
assert response.status_code == 404
def test_delete_cookbook_other_household(
api_client: TestClient, unique_user: TestUser, h2_user: TestUser, cookbooks: list[TestCookbook]
):
sample = random.choice(cookbooks)
response = api_client.delete(api_routes.households_cookbooks_item_id(sample.id), headers=h2_user.token)
assert response.status_code == 404
response = api_client.get(api_routes.households_cookbooks_item_id(sample.slug), headers=unique_user.token)
assert response.status_code == 200
@pytest.mark.parametrize(
"qf_string, expected_code",
[

View File

@@ -299,3 +299,16 @@ def test_cookbook_recipes_includes_all_households(api_client: TestClient, unique
assert recipe.id in fetched_recipe_ids
for recipe in other_recipes:
assert recipe.id in fetched_recipe_ids
def test_cookbooks_from_other_households(api_client: TestClient, unique_user: TestUser, h2_user: TestUser):
h2_cookbook = h2_user.repos.cookbooks.create(
SaveCookBook(
name=random_string(),
group_id=h2_user.group_id,
household_id=h2_user.household_id,
)
)
response = api_client.get(api_routes.recipes, params={"cookbook": h2_cookbook.slug}, headers=unique_user.token)
assert response.status_code == 200

View File

@@ -1,8 +1,10 @@
import pytest
from pytest import MonkeyPatch, Session
from mealie.core.config import get_app_settings
from mealie.core.security.providers.openid_provider import OpenIDProvider
from mealie.repos.all_repositories import get_repositories
from tests.utils.factories import random_email, random_string
from tests.utils.fixture_schemas import TestUser
@@ -125,3 +127,38 @@ def test_has_admin_group_new_user(monkeypatch: MonkeyPatch, session: Session):
user = db.users.get_one("dude2", "username")
assert user is not None
assert user.admin
@pytest.mark.parametrize("valid_group", [True, False])
@pytest.mark.parametrize("valid_household", [True, False])
def test_ldap_user_creation_invalid_group_or_household(
monkeypatch: MonkeyPatch, session: Session, valid_group: bool, valid_household: bool
):
monkeypatch.setenv("OIDC_USER_GROUP", "mealie_user")
monkeypatch.setenv("OIDC_ADMIN_GROUP", "mealie_admin")
if not valid_group:
monkeypatch.setenv("DEFAULT_GROUP", random_string())
if not valid_household:
monkeypatch.setenv("DEFAULT_HOUSEHOLD", random_string())
get_app_settings.cache_clear()
data = {
"preferred_username": random_string(),
"email": random_email(),
"name": random_string(),
"groups": ["mealie_user"],
}
auth_provider = OpenIDProvider(session, data)
if valid_group and valid_household:
assert auth_provider.authenticate() is not None
else:
assert auth_provider.authenticate() is None
db = get_repositories(session, group_id=None, household_id=None)
user = db.users.get_one(data["preferred_username"], "username")
if valid_group and valid_household:
assert user is not None
else:
assert user is None

View File

@@ -1,3 +1,4 @@
import json
import re
from dataclasses import dataclass
@@ -126,13 +127,27 @@ smtp_validation_cases = [
(
"good_data_tls",
SMTPValidationCase(
"email.mealie.io", "587", "tls", "Mealie", "mealie@mealie.io", "mealie@mealie.io", "mealie-password", True
"email.mealie.io",
"587",
"tls",
"Mealie",
"mealie@mealie.io",
"mealie@mealie.io",
"mealie-password",
True,
),
),
(
"good_data_ssl",
SMTPValidationCase(
"email.mealie.io", "465", "tls", "Mealie", "mealie@mealie.io", "mealie@mealie.io", "mealie-password", True
"email.mealie.io",
"465",
"tls",
"Mealie",
"mealie@mealie.io",
"mealie@mealie.io",
"mealie-password",
True,
),
),
]
@@ -151,6 +166,149 @@ def test_smtp_enable_with_bad_data_tls(data: SMTPValidationCase):
data.auth_strategy,
data.user,
data.password,
)
).enabled
assert is_valid is data.is_valid
@dataclass(slots=True)
class EnvVar:
name: str
value: any
class LDAPValidationCase:
settings = list[EnvVar]
is_valid: bool
def __init__(
self,
enabled: bool,
server_url: str | None,
base_dn: str | None,
is_valid: bool,
):
self.settings = [
EnvVar("LDAP_AUTH_ENABLED", enabled),
EnvVar("LDAP_SERVER_URL", server_url),
EnvVar("LDAP_BASE_DN", base_dn),
]
self.is_valid = is_valid
ldap_validation_cases = [
("not enabled", LDAPValidationCase(False, None, None, False)),
("missing url", LDAPValidationCase(True, None, "dn", False)),
("missing base dn", LDAPValidationCase(True, "url", None, False)),
("all good", LDAPValidationCase(True, "url", "dn", True)),
]
ldap_cases = [x[1] for x in ldap_validation_cases]
ldap_cases_ids = [x[0] for x in ldap_validation_cases]
@pytest.mark.parametrize("data", ldap_cases, ids=ldap_cases_ids)
def test_ldap_settings_validation(data: LDAPValidationCase, monkeypatch: pytest.MonkeyPatch):
for setting in data.settings:
if setting.value is not None:
monkeypatch.setenv(setting.name, setting.value)
else:
monkeypatch.delenv(setting.name, raising=False)
get_app_settings.cache_clear()
app_settings = get_app_settings()
assert app_settings.LDAP_ENABLED is data.is_valid
class OIDCValidationCase:
settings = list[EnvVar]
is_valid: bool
def __init__(
self,
enabled: bool,
client_id: str | None,
client_secret: str | None,
configuration_url: str | None,
groups_claim: str | None,
user_group: str | None,
admin_group: str | None,
is_valid: bool,
):
self.settings = [
EnvVar("OIDC_AUTH_ENABLED", enabled),
EnvVar("OIDC_CLIENT_ID", client_id),
EnvVar("OIDC_CLIENT_SECRET", client_secret),
EnvVar("OIDC_CONFIGURATION_URL", configuration_url),
EnvVar("OIDC_GROUPS_CLAIM", groups_claim),
EnvVar("OIDC_USER_GROUP", user_group),
EnvVar("OIDC_ADMIN_GROUP", admin_group),
]
self.is_valid = is_valid
oidc_validation_cases = [
(
"not enabled",
OIDCValidationCase(False, None, None, None, None, None, None, False),
),
(
"missing client id",
OIDCValidationCase(True, None, "secret", "url", "groups", "user", "admin", False),
),
(
"missing client secret",
OIDCValidationCase(True, "id", None, "url", "groups", "user", "admin", False),
),
(
"missing url",
OIDCValidationCase(True, "id", "secret", None, "groups", "user", "admin", False),
),
(
"all good no groups",
OIDCValidationCase(True, "id", "secret", "url", None, None, None, True),
),
(
"all good with groups",
OIDCValidationCase(True, "id", "secret", "url", "groups", "user", "admin", True),
),
]
oidc_cases = [x[1] for x in oidc_validation_cases]
oidc_cases_ids = [x[0] for x in oidc_validation_cases]
@pytest.mark.parametrize("data", oidc_cases, ids=oidc_cases_ids)
def test_oidc_settings_validation(data: OIDCValidationCase, monkeypatch: pytest.MonkeyPatch):
for setting in data.settings:
if setting.value is not None:
monkeypatch.setenv(setting.name, setting.value)
else:
monkeypatch.delenv(setting.name, raising=False)
get_app_settings.cache_clear()
app_settings = get_app_settings()
assert app_settings.OIDC_READY is data.is_valid
def test_sensitive_settings_mask(monkeypatch: pytest.MonkeyPatch):
sensitive_settings = [
"LDAP_QUERY_PASSWORD",
"OPENAI_API_KEY",
"SMTP_USER",
"SMTP_PASSWORD",
"OIDC_CLIENT_SECRET",
]
for setting in sensitive_settings:
monkeypatch.setenv(setting, "super_secret")
get_app_settings.cache_clear()
app_settings = get_app_settings()
settings = app_settings.model_dump()
settings_json = json.loads(app_settings.model_dump_json())
for setting in sensitive_settings:
assert settings[setting] == "*****"
assert settings_json[setting] == "*****"

View File

@@ -1,6 +1,7 @@
from pathlib import Path
import ldap
import pytest
from pytest import MonkeyPatch
from mealie.core import security
@@ -13,6 +14,7 @@ from mealie.core.security.providers.credentials_provider import (
from mealie.core.security.providers.ldap_provider import LDAPProvider
from mealie.db.db_setup import session_context
from mealie.db.models.users.users import AuthMethod
from mealie.repos.repository_factory import AllRepositories
from mealie.schema.user.auth import CredentialsRequestForm
from mealie.schema.user.user import PrivateUser
from tests.utils import random_string
@@ -92,7 +94,7 @@ class LdapConnMock:
pass
def setup_env(monkeypatch: MonkeyPatch):
def setup_env(monkeypatch: MonkeyPatch, **kwargs):
user = random_string(10)
mail = random_string(10)
name = random_string(10)
@@ -140,11 +142,55 @@ def test_ldap_user_creation(monkeypatch: MonkeyPatch):
provider = get_provider(session, user, password)
result = provider.get_user()
app_settings = get_app_settings()
assert result
assert result.username == user
assert result.email == mail
assert result.full_name == name
assert result.admin is False
assert result.group == app_settings.DEFAULT_GROUP
assert result.household == app_settings.DEFAULT_HOUSEHOLD
assert result.auth_method == AuthMethod.LDAP
@pytest.mark.parametrize("valid_group", [True, False])
@pytest.mark.parametrize("valid_household", [True, False])
def test_ldap_user_creation_invalid_group_or_household(
unfiltered_database: AllRepositories, monkeypatch: MonkeyPatch, valid_group: bool, valid_household: bool
):
user, mail, name, password, query_bind, query_password = setup_env(monkeypatch)
if not valid_group:
monkeypatch.setenv("DEFAULT_GROUP", random_string())
if not valid_household:
monkeypatch.setenv("DEFAULT_HOUSEHOLD", random_string())
def ldap_initialize_mock(url):
assert url == ""
return LdapConnMock(user, password, False, query_bind, query_password, mail, name)
monkeypatch.setattr(ldap, "initialize", ldap_initialize_mock)
get_app_settings.cache_clear()
with session_context() as session:
provider = get_provider(session, user, password)
try:
result = provider.get_user()
except ValueError:
result = None
if valid_group and valid_household:
assert result
else:
assert not result
# check if the user exists in the db
user = unfiltered_database.users.get_by_username(user)
if valid_group and valid_household:
assert user
else:
assert not user
def test_ldap_user_creation_fail(monkeypatch: MonkeyPatch):