mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-04-21 04:15:34 -04:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7a08b6b11 | ||
|
|
bd296c3eaf | ||
|
|
8aa016e57b | ||
|
|
480574eb3d | ||
|
|
0573d6fc9c | ||
|
|
f8d08c6785 | ||
|
|
e6368174f0 | ||
|
|
2252875050 | ||
|
|
54c62ec491 | ||
|
|
af79a751fb | ||
|
|
6e2c849412 |
@@ -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:v3.15.0`
|
||||
2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:v3.15.2`
|
||||
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
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ PostgreSQL might be considered if you need to support many concurrent users. In
|
||||
```yaml
|
||||
services:
|
||||
mealie:
|
||||
image: ghcr.io/mealie-recipes/mealie:v3.15.0 # (3)
|
||||
image: ghcr.io/mealie-recipes/mealie:v3.15.2 # (3)
|
||||
container_name: mealie
|
||||
restart: always
|
||||
ports:
|
||||
|
||||
@@ -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:v3.15.0 # (3)
|
||||
image: ghcr.io/mealie-recipes/mealie:v3.15.2 # (3)
|
||||
container_name: mealie
|
||||
restart: always
|
||||
ports:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<BaseCardSectionTitle :title="$t('group.group-preferences')" />
|
||||
<div class="mb-6">
|
||||
<v-checkbox
|
||||
v-model="preferences.privateGroup"
|
||||
v-model="local.privateGroup"
|
||||
hide-details
|
||||
density="compact"
|
||||
color="primary"
|
||||
@@ -21,7 +21,7 @@
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<v-checkbox
|
||||
v-model="preferences.showAnnouncements"
|
||||
v-model="local.showAnnouncements"
|
||||
hide-details
|
||||
density="compact"
|
||||
color="primary"
|
||||
@@ -40,4 +40,6 @@
|
||||
import type { ReadGroupPreferences } from "~/lib/api/types/user";
|
||||
|
||||
const preferences = defineModel<ReadGroupPreferences>({ required: true });
|
||||
const local = reactive({ ...preferences.value });
|
||||
watch(local, (newVal) => { preferences.value = { ...newVal }; });
|
||||
</script>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div v-if="preferences">
|
||||
<BaseCardSectionTitle :title="$t('household.household-preferences')" />
|
||||
<div class="mb-6">
|
||||
<v-checkbox v-model="preferences.privateHousehold" hide-details density="compact" :label="$t('household.private-household')" color="primary" />
|
||||
<v-checkbox v-model="local.privateHousehold" hide-details density="compact" :label="$t('household.private-household')" color="primary" />
|
||||
<div class="ml-8">
|
||||
<p class="text-subtitle-2 my-0 py-0">
|
||||
{{ $t("household.private-household-description") }}
|
||||
@@ -11,7 +11,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<v-checkbox v-model="preferences.lockRecipeEditsFromOtherHouseholds" hide-details density="compact" :label="$t('household.lock-recipe-edits-from-other-households')" color="primary" />
|
||||
<v-checkbox v-model="local.lockRecipeEditsFromOtherHouseholds" hide-details density="compact" :label="$t('household.lock-recipe-edits-from-other-households')" color="primary" />
|
||||
<div class="ml-8">
|
||||
<p class="text-subtitle-2 my-0 py-0">
|
||||
{{ $t("household.lock-recipe-edits-from-other-households-description") }}
|
||||
@@ -20,7 +20,7 @@
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<v-checkbox
|
||||
v-model="preferences.showAnnouncements"
|
||||
v-model="local.showAnnouncements"
|
||||
hide-details
|
||||
density="compact"
|
||||
color="primary"
|
||||
@@ -33,7 +33,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<v-select
|
||||
v-model="preferences.firstDayOfWeek"
|
||||
v-model="local.firstDayOfWeek"
|
||||
:prepend-icon="$globals.icons.calendarWeekBegin"
|
||||
:items="allDays"
|
||||
item-title="name"
|
||||
@@ -48,7 +48,7 @@
|
||||
</BaseCardSectionTitle>
|
||||
<div class="preference-container">
|
||||
<div v-for="p in recipePreferences" :key="p.key">
|
||||
<v-checkbox v-model="preferences[p.key]" hide-details density="compact" :label="p.label" color="primary" />
|
||||
<v-checkbox v-model="local[p.key]" hide-details density="compact" :label="p.label" color="primary" />
|
||||
<p class="ml-8 text-subtitle-2 my-0 py-0">
|
||||
{{ p.description }}
|
||||
</p>
|
||||
@@ -61,6 +61,9 @@
|
||||
import type { ReadHouseholdPreferences } from "~/lib/api/types/household";
|
||||
|
||||
const preferences = defineModel<ReadHouseholdPreferences>({ required: true });
|
||||
const local = reactive({ ...preferences.value });
|
||||
watch(local, (newVal) => { preferences.value = { ...newVal }; });
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
type Preference = {
|
||||
|
||||
@@ -457,13 +457,8 @@ async function seedData() {
|
||||
return;
|
||||
}
|
||||
|
||||
const tasks = [
|
||||
seedFoods(),
|
||||
seedUnits(),
|
||||
seedLabels(),
|
||||
];
|
||||
|
||||
await Promise.all(tasks);
|
||||
await seedLabels();
|
||||
await Promise.all([seedFoods(), seedUnits()]);
|
||||
}
|
||||
|
||||
async function submitCommonSettings() {
|
||||
|
||||
@@ -50,6 +50,12 @@ export default defineNuxtConfig({
|
||||
content: "Mealie is a recipe management app for your kitchen.",
|
||||
},
|
||||
],
|
||||
script: [
|
||||
{
|
||||
innerHTML: `(function(){try{var d=localStorage.getItem('vueuse-color-scheme');var m=d==='dark'||(d!=='light'&&matchMedia('(prefers-color-scheme:dark)').matches);document.documentElement.style.backgroundColor=m?'#1E1E1E':'#FFFFFF'}catch(e){}})()`,
|
||||
type: "text/javascript",
|
||||
},
|
||||
],
|
||||
link: [
|
||||
{ rel: "icon", type: "image/x-icon", href: "/favicon.ico" },
|
||||
{ rel: "shortcut icon", type: "image/png", href: "/icons/icon-x64.png" },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mealie",
|
||||
"version": "3.15.0",
|
||||
"version": "3.15.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "nuxt dev",
|
||||
|
||||
@@ -19,8 +19,12 @@ router = APIRouter(prefix="/backups")
|
||||
|
||||
@controller(router)
|
||||
class AdminBackupController(BaseAdminController):
|
||||
def _backup_path(self, name) -> Path:
|
||||
return get_app_dirs().BACKUP_DIR / name
|
||||
def _backup_path(self, name: str) -> Path:
|
||||
backup_dir = get_app_dirs().BACKUP_DIR
|
||||
candidate = (backup_dir / name).resolve()
|
||||
if not candidate.is_relative_to(backup_dir.resolve()):
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
||||
return candidate
|
||||
|
||||
@router.get("", response_model=AllBackups)
|
||||
def get_all(self):
|
||||
@@ -86,7 +90,7 @@ class AdminBackupController(BaseAdminController):
|
||||
app_dirs = get_app_dirs()
|
||||
dest = app_dirs.BACKUP_DIR.joinpath(f"{name}.zip")
|
||||
|
||||
if dest.absolute().parent != app_dirs.BACKUP_DIR:
|
||||
if dest.resolve().parent != app_dirs.BACKUP_DIR.resolve():
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
with dest.open("wb") as buffer:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, File, UploadFile
|
||||
|
||||
@@ -25,9 +26,12 @@ class AdminDebugController(BaseAdminController):
|
||||
|
||||
with get_temporary_path() as temp_path:
|
||||
if image:
|
||||
with temp_path.joinpath(image.filename).open("wb") as buffer:
|
||||
if not image.filename:
|
||||
return DebugResponse(success=False, response="Invalid image filename")
|
||||
safe_filename = Path(image.filename).name
|
||||
local_image_path = temp_path.joinpath(safe_filename)
|
||||
with local_image_path.open("wb") as buffer:
|
||||
shutil.copyfileobj(image.file, buffer)
|
||||
local_image_path = temp_path.joinpath(image.filename)
|
||||
local_images = [OpenAILocalImage(filename=os.path.basename(local_image_path), path=local_image_path)]
|
||||
else:
|
||||
local_images = None
|
||||
|
||||
@@ -17,7 +17,7 @@ class ImageType(StrEnum):
|
||||
|
||||
|
||||
@router.get("/{recipe_id}/images/{file_name}")
|
||||
async def get_recipe_img(recipe_id: str, file_name: ImageType = ImageType.original):
|
||||
async def get_recipe_img(recipe_id: UUID4, file_name: ImageType = ImageType.original):
|
||||
"""
|
||||
Takes in a recipe id, returns the static image. This route is proxied in the docker image
|
||||
and should not hit the API in production
|
||||
@@ -32,7 +32,7 @@ async def get_recipe_img(recipe_id: str, file_name: ImageType = ImageType.origin
|
||||
|
||||
@router.get("/{recipe_id}/images/timeline/{timeline_event_id}/{file_name}")
|
||||
async def get_recipe_timeline_event_img(
|
||||
recipe_id: str, timeline_event_id: str, file_name: ImageType = ImageType.original
|
||||
recipe_id: UUID4, timeline_event_id: UUID4, file_name: ImageType = ImageType.original
|
||||
):
|
||||
"""
|
||||
Takes in a recipe id and event timeline id, returns the static image. This route is proxied in the docker image
|
||||
@@ -51,7 +51,11 @@ async def get_recipe_timeline_event_img(
|
||||
@router.get("/{recipe_id}/assets/{file_name}")
|
||||
async def get_recipe_asset(recipe_id: UUID4, file_name: str):
|
||||
"""Returns a recipe asset"""
|
||||
file = Recipe.directory_from_id(recipe_id).joinpath("assets", file_name)
|
||||
asset_dir = Recipe.directory_from_id(recipe_id).joinpath("assets")
|
||||
file = asset_dir.joinpath(file_name).resolve()
|
||||
|
||||
if not file.is_relative_to(asset_dir.resolve()):
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if file.exists():
|
||||
return FileResponse(file)
|
||||
|
||||
@@ -11,7 +11,11 @@ router = APIRouter(prefix="/users")
|
||||
async def get_user_image(user_id: UUID4, file_name: str):
|
||||
"""Takes in a recipe slug, returns the static image. This route is proxied in the docker image
|
||||
and should not hit the API in production"""
|
||||
recipe_image = PrivateUser.get_directory(user_id) / file_name
|
||||
user_dir = PrivateUser.get_directory(user_id)
|
||||
recipe_image = (user_dir / file_name).resolve()
|
||||
|
||||
if not recipe_image.is_relative_to(user_dir.resolve()):
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if recipe_image.exists():
|
||||
return FileResponse(recipe_image, media_type="image/webp")
|
||||
|
||||
@@ -272,8 +272,8 @@ class BaseMigrator(BaseService):
|
||||
recipe = cleaner.clean(recipe_dict, self.translator, url=recipe_dict.get("org_url", None))
|
||||
return recipe
|
||||
|
||||
def import_image(self, slug: str, src: str | Path, recipe_id: UUID4):
|
||||
def import_image(self, slug: str, src: str | Path, recipe_id: UUID4, extraction_root: Path | None = None):
|
||||
try:
|
||||
import_image(src, recipe_id)
|
||||
import_image(src, recipe_id, extraction_root=extraction_root)
|
||||
except UnidentifiedImageError as e:
|
||||
self.logger.error(f"Failed to import image for {slug}: {e}")
|
||||
|
||||
@@ -4,7 +4,7 @@ from pathlib import Path
|
||||
|
||||
from ._migration_base import BaseMigrator
|
||||
from .utils.migration_alias import MigrationAlias
|
||||
from .utils.migration_helpers import MigrationReaders, split_by_comma
|
||||
from .utils.migration_helpers import MigrationReaders, safe_local_path, split_by_comma
|
||||
|
||||
|
||||
class ChowdownMigrator(BaseMigrator):
|
||||
@@ -60,8 +60,10 @@ class ChowdownMigrator(BaseMigrator):
|
||||
continue
|
||||
|
||||
if r.image:
|
||||
cd_image = image_dir.joinpath(r.image)
|
||||
cd_image = safe_local_path(image_dir.joinpath(r.image), image_dir)
|
||||
else:
|
||||
cd_image = None
|
||||
except StopIteration:
|
||||
continue
|
||||
if cd_image:
|
||||
self.import_image(slug, cd_image, recipe_id)
|
||||
self.import_image(slug, cd_image, recipe_id, extraction_root=image_dir)
|
||||
|
||||
@@ -11,7 +11,7 @@ from mealie.services.parser_services._base import DataMatcher
|
||||
from mealie.services.parser_services.parser_utils.string_utils import extract_quantity_from_string
|
||||
|
||||
from ._migration_base import BaseMigrator
|
||||
from .utils.migration_helpers import format_time
|
||||
from .utils.migration_helpers import format_time, safe_local_path
|
||||
|
||||
|
||||
class DSVParser:
|
||||
@@ -157,15 +157,21 @@ class CooknMigrator(BaseMigrator):
|
||||
if _media_type != "":
|
||||
# Determine file extension based on media type
|
||||
_extension = _media_type.split("/")[-1]
|
||||
_old_image_path = os.path.join(db.directory, str(_media_id))
|
||||
new_image_path = f"{_old_image_path}.{_extension}"
|
||||
_old_image_path = Path(db.directory) / str(_media_id)
|
||||
new_image_path = _old_image_path.with_suffix(f".{_extension}")
|
||||
if safe_local_path(_old_image_path, db.directory) is None:
|
||||
return None
|
||||
if safe_local_path(new_image_path, db.directory) is None:
|
||||
return None
|
||||
# Rename the file if it exists and has no extension
|
||||
if os.path.exists(_old_image_path) and not os.path.exists(new_image_path):
|
||||
if _old_image_path.exists() and not new_image_path.exists():
|
||||
os.rename(_old_image_path, new_image_path)
|
||||
if Path(new_image_path).exists():
|
||||
return new_image_path
|
||||
if new_image_path.exists():
|
||||
return str(new_image_path)
|
||||
else:
|
||||
return os.path.join(db.directory, str(_media_id))
|
||||
candidate = Path(db.directory) / str(_media_id)
|
||||
if safe_local_path(candidate, db.directory) is not None:
|
||||
return str(candidate)
|
||||
return None
|
||||
|
||||
def _parse_ingredients(self, _recipe_id: str, db: DSVParser) -> list[RecipeIngredient]:
|
||||
@@ -388,14 +394,14 @@ class CooknMigrator(BaseMigrator):
|
||||
recipe = recipe_lookup.get(slug)
|
||||
if recipe:
|
||||
if recipe.image:
|
||||
self.import_image(slug, recipe.image, recipe_id)
|
||||
self.import_image(slug, recipe.image, recipe_id, extraction_root=db.directory)
|
||||
else:
|
||||
index_len = len(slug.split("-")[-1])
|
||||
recipe = recipe_lookup.get(slug[: -(index_len + 1)])
|
||||
if recipe:
|
||||
self.logger.warning("Duplicate recipe (%s) found! Saved as copy...", recipe.name)
|
||||
if recipe.image:
|
||||
self.import_image(slug, recipe.image, recipe_id)
|
||||
self.import_image(slug, recipe.image, recipe_id, extraction_root=db.directory)
|
||||
else:
|
||||
self.logger.warning("Failed to lookup recipe! (%s)", slug)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ from mealie.schema.reports.reports import ReportEntryCreate
|
||||
|
||||
from ._migration_base import BaseMigrator
|
||||
from .utils.migration_alias import MigrationAlias
|
||||
from .utils.migration_helpers import import_image
|
||||
from .utils.migration_helpers import import_image, safe_local_path
|
||||
|
||||
|
||||
def parse_recipe_tags(tags: list) -> list[str]:
|
||||
@@ -52,7 +52,9 @@ class CopyMeThatMigrator(BaseMigrator):
|
||||
# the recipe image tag has no id, so we parse it directly
|
||||
if tag.name == "img" and "recipeImage" in tag.get("class", []):
|
||||
if image_path := tag.get("src"):
|
||||
recipe_dict["image"] = str(source_dir.joinpath(image_path))
|
||||
safe = safe_local_path(source_dir.joinpath(image_path), source_dir)
|
||||
if safe is not None:
|
||||
recipe_dict["image"] = str(safe)
|
||||
|
||||
continue
|
||||
|
||||
@@ -120,4 +122,4 @@ class CopyMeThatMigrator(BaseMigrator):
|
||||
except StopIteration:
|
||||
continue
|
||||
|
||||
import_image(r.image, recipe_id)
|
||||
import_image(r.image, recipe_id, extraction_root=source_dir)
|
||||
|
||||
@@ -97,4 +97,4 @@ class NextcloudMigrator(BaseMigrator):
|
||||
if status:
|
||||
nc_dir = nextcloud_dirs[slug]
|
||||
if nc_dir.image:
|
||||
self.import_image(slug, nc_dir.image, recipe_id)
|
||||
self.import_image(slug, nc_dir.image, recipe_id, extraction_root=base_dir)
|
||||
|
||||
@@ -84,6 +84,6 @@ class PaprikaMigrator(BaseMigrator):
|
||||
temp_file.write(image.read())
|
||||
temp_file.flush()
|
||||
path = Path(temp_file.name)
|
||||
self.import_image(slug, path, recipe_id)
|
||||
self.import_image(slug, path, recipe_id, extraction_root=path.parent)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to import image for {slug}: {e}")
|
||||
|
||||
@@ -8,7 +8,7 @@ from mealie.services.scraper import cleaner
|
||||
|
||||
from ._migration_base import BaseMigrator
|
||||
from .utils.migration_alias import MigrationAlias
|
||||
from .utils.migration_helpers import parse_iso8601_duration
|
||||
from .utils.migration_helpers import parse_iso8601_duration, safe_local_path
|
||||
|
||||
|
||||
def clean_instructions(instructions: list[str]) -> list[str]:
|
||||
@@ -30,7 +30,9 @@ def parse_recipe_div(recipe, image_path):
|
||||
elif item.name == "div":
|
||||
meta[item["itemprop"]] = list(item.stripped_strings)
|
||||
elif item.name == "img":
|
||||
meta[item["itemprop"]] = str(image_path / item["src"])
|
||||
safe = safe_local_path(image_path / item["src"], image_path)
|
||||
if safe is not None:
|
||||
meta[item["itemprop"]] = str(safe)
|
||||
else:
|
||||
meta[item["itemprop"]] = item.string
|
||||
# merge nutrition keys into their own dict.
|
||||
@@ -107,4 +109,4 @@ class RecipeKeeperMigrator(BaseMigrator):
|
||||
except StopIteration:
|
||||
continue
|
||||
|
||||
self.import_image(slug, recipe.image, recipe_id)
|
||||
self.import_image(slug, recipe.image, recipe_id, extraction_root=source_dir)
|
||||
|
||||
@@ -132,4 +132,4 @@ class TandoorMigrator(BaseMigrator):
|
||||
except StopIteration:
|
||||
continue
|
||||
|
||||
self.import_image(slug, r.image, recipe_id)
|
||||
self.import_image(slug, r.image, recipe_id, extraction_root=source_dir)
|
||||
|
||||
@@ -8,6 +8,7 @@ import yaml
|
||||
from PIL import UnidentifiedImageError
|
||||
from pydantic import UUID4
|
||||
|
||||
from mealie.core import root_logger
|
||||
from mealie.services.recipe.recipe_data_service import RecipeDataService
|
||||
|
||||
|
||||
@@ -100,16 +101,45 @@ def glob_walker(directory: Path, glob_str: str, return_parent=True) -> list[Path
|
||||
return matches
|
||||
|
||||
|
||||
def import_image(src: str | Path, recipe_id: UUID4):
|
||||
"""Read the successful migrations attribute and for each import the image
|
||||
appropriately into the image directory. Minification is done in mass
|
||||
after the migration occurs.
|
||||
def safe_local_path(candidate: str | Path, root: Path) -> Path | None:
|
||||
"""
|
||||
Returns the resolved path only if it is safely contained within root.
|
||||
|
||||
Returns ``None`` for any path that would escape the root directory,
|
||||
including ``../../`` traversal sequences and absolute paths outside root.
|
||||
Symlinks are followed by ``resolve()``, so a symlink pointing outside root
|
||||
is also rejected.
|
||||
"""
|
||||
try:
|
||||
# OSError: symlink resolution failure; ValueError: null bytes on some platforms
|
||||
resolved = Path(candidate).resolve()
|
||||
if resolved.is_relative_to(root.resolve()):
|
||||
return resolved
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def import_image(src: str | Path, recipe_id: UUID4, extraction_root: Path | None = None):
|
||||
"""Import a local image file into the recipe image directory.
|
||||
|
||||
May raise an UnidentifiedImageError if the file is not a recognised format.
|
||||
|
||||
If extraction_root is provided, the src path must be contained within it.
|
||||
Paths that escape the extraction_root are silently rejected to prevent
|
||||
arbitrary local file reads via archive-controlled image paths.
|
||||
"""
|
||||
|
||||
if isinstance(src, str):
|
||||
src = Path(src)
|
||||
|
||||
if extraction_root is not None:
|
||||
if safe_local_path(src, extraction_root) is None:
|
||||
root_logger.get_logger().warning(
|
||||
"Rejected image path outside extraction root: %s (root: %s)", src, extraction_root
|
||||
)
|
||||
return
|
||||
|
||||
if not src.exists():
|
||||
return
|
||||
|
||||
|
||||
@@ -129,19 +129,24 @@ class OpenAIService(BaseService):
|
||||
"""
|
||||
tree = name.split(".")
|
||||
relative_path = Path(*tree[:-1], tree[-1] + ".txt")
|
||||
default_prompt_file = Path(self.PROMPTS_DIR, relative_path)
|
||||
|
||||
default_prompt_file = (self.PROMPTS_DIR / relative_path).resolve()
|
||||
if not default_prompt_file.is_relative_to(self.PROMPTS_DIR.resolve()):
|
||||
raise ValueError(f"Invalid prompt name '{name}': resolves outside prompts directory")
|
||||
|
||||
try:
|
||||
# Only include custom files if the custom_dir is configured, is a directory, and the prompt file exists
|
||||
custom_dir = Path(self.custom_prompt_dir) if self.custom_prompt_dir else None
|
||||
custom_dir = Path(self.custom_prompt_dir).resolve() if self.custom_prompt_dir else None
|
||||
if custom_dir and not custom_dir.is_dir():
|
||||
custom_dir = None
|
||||
except Exception:
|
||||
custom_dir = None
|
||||
|
||||
if custom_dir:
|
||||
custom_prompt_file = Path(custom_dir, relative_path)
|
||||
if custom_prompt_file.exists():
|
||||
custom_prompt_file = (custom_dir / relative_path).resolve()
|
||||
if not custom_prompt_file.is_relative_to(custom_dir):
|
||||
logger.warning(f"Custom prompt file resolves outside custom dir, skipping: {custom_prompt_file}")
|
||||
elif custom_prompt_file.exists():
|
||||
logger.debug(f"Found valid custom prompt file: {custom_prompt_file}")
|
||||
return [custom_prompt_file, default_prompt_file]
|
||||
else:
|
||||
|
||||
@@ -99,7 +99,12 @@ class RecipeDataService(BaseService):
|
||||
with open(image_path, "ab") as f:
|
||||
shutil.copyfileobj(file_data, f)
|
||||
|
||||
self.minifier.minify(image_path)
|
||||
try:
|
||||
self.minifier.minify(image_path)
|
||||
except Exception:
|
||||
# Remove the partially-written file so corrupt images don't persist on disk.
|
||||
image_path.unlink(missing_ok=True)
|
||||
raise
|
||||
|
||||
return image_path
|
||||
|
||||
|
||||
@@ -325,9 +325,11 @@ class RecipeService(RecipeServiceBase):
|
||||
with get_temporary_path() as temp_path:
|
||||
local_images: list[Path] = []
|
||||
for image in images:
|
||||
with temp_path.joinpath(image.filename).open("wb") as buffer:
|
||||
safe_filename = Path(image.filename).name
|
||||
image_path = temp_path.joinpath(safe_filename)
|
||||
with image_path.open("wb") as buffer:
|
||||
shutil.copyfileobj(image.file, buffer)
|
||||
local_images.append(temp_path.joinpath(image.filename))
|
||||
local_images.append(image_path)
|
||||
|
||||
recipe_data = await openai_recipe_service.build_recipe_from_images(
|
||||
local_images, translate_language=translate_language
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "mealie"
|
||||
version = "3.15.0"
|
||||
version = "3.15.2"
|
||||
description = "A Recipe Manager"
|
||||
authors = [{ name = "Hayden", email = "hay-kot@pm.me" }]
|
||||
license = "AGPL-3.0-only"
|
||||
@@ -19,14 +19,14 @@ dependencies = [
|
||||
"extruct==0.18.0",
|
||||
"fastapi==0.135.3",
|
||||
"httpx==0.28.1",
|
||||
"lxml==6.0.2",
|
||||
"lxml==6.0.3",
|
||||
"orjson==3.11.8",
|
||||
"pydantic==2.12.5",
|
||||
"pyhumps==3.8.0",
|
||||
"python-dateutil==2.9.0.post0",
|
||||
"python-dotenv==1.2.2",
|
||||
"python-ldap==3.4.5",
|
||||
"python-multipart==0.0.24",
|
||||
"python-multipart==0.0.26",
|
||||
"python-slugify==8.0.4",
|
||||
"recipe-scrapers==15.11.0",
|
||||
"requests==2.33.1",
|
||||
@@ -42,7 +42,7 @@ dependencies = [
|
||||
"pydantic-settings==2.13.1",
|
||||
"pillow-heif==1.3.0",
|
||||
"pyjwt==2.12.1",
|
||||
"openai==2.30.0",
|
||||
"openai==2.31.0",
|
||||
"typing-extensions==4.15.0",
|
||||
"itsdangerous==2.2.0",
|
||||
"yt-dlp==2026.3.17",
|
||||
@@ -73,7 +73,7 @@ dev = [
|
||||
"pytest==9.0.3",
|
||||
"pytest-asyncio==1.3.0",
|
||||
"rich==15.0.0",
|
||||
"ruff==0.15.9",
|
||||
"ruff==0.15.10",
|
||||
"types-PyYAML==6.0.12.20260408",
|
||||
"types-python-dateutil==2.9.0.20260408",
|
||||
"types-python-slugify==8.0.2.20240310",
|
||||
|
||||
100
uv.lock
generated
100
uv.lock
generated
@@ -756,28 +756,28 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "lxml"
|
||||
version = "6.0.2"
|
||||
version = "6.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/43/42/149c7747977db9d68faee960c1a3391eb25e94d4bb677f8e2df8328e4098/lxml-6.0.3.tar.gz", hash = "sha256:a1664c5139755df44cab3834f4400b331b02205d62d3fdcb1554f63439bf3372", size = 4237567, upload-time = "2026-04-09T14:39:09.664Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/c8/8ff2bc6b920c84355146cd1ab7d181bc543b89241cfb1ebee824a7c81457/lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456", size = 8661887, upload-time = "2025-09-22T04:01:17.265Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/6f/9aae1008083bb501ef63284220ce81638332f9ccbfa53765b2b7502203cf/lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924", size = 4667818, upload-time = "2025-09-22T04:01:19.688Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/ca/31fb37f99f37f1536c133476674c10b577e409c0a624384147653e38baf2/lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f", size = 4950807, upload-time = "2025-09-22T04:01:21.487Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/87/f6cb9442e4bada8aab5ae7e1046264f62fdbeaa6e3f6211b93f4c0dd97f1/lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534", size = 5109179, upload-time = "2025-09-22T04:01:23.32Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/20/a7760713e65888db79bbae4f6146a6ae5c04e4a204a3c48896c408cd6ed2/lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564", size = 5023044, upload-time = "2025-09-22T04:01:25.118Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/b0/7e64e0460fcb36471899f75831509098f3fd7cd02a3833ac517433cb4f8f/lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f", size = 5359685, upload-time = "2025-09-22T04:01:27.398Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/e1/e5df362e9ca4e2f48ed6411bd4b3a0ae737cc842e96877f5bf9428055ab4/lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0", size = 5654127, upload-time = "2025-09-22T04:01:29.629Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/d1/232b3309a02d60f11e71857778bfcd4acbdb86c07db8260caf7d008b08f8/lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192", size = 5253958, upload-time = "2025-09-22T04:01:31.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/35/d955a070994725c4f7d80583a96cab9c107c57a125b20bb5f708fe941011/lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0", size = 4711541, upload-time = "2025-09-22T04:01:33.801Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/be/667d17363b38a78c4bd63cfd4b4632029fd68d2c2dc81f25ce9eb5224dd5/lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092", size = 5267426, upload-time = "2025-09-22T04:01:35.639Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/47/62c70aa4a1c26569bc958c9ca86af2bb4e1f614e8c04fb2989833874f7ae/lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f", size = 5064917, upload-time = "2025-09-22T04:01:37.448Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/55/6ceddaca353ebd0f1908ef712c597f8570cc9c58130dbb89903198e441fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8", size = 4788795, upload-time = "2025-09-22T04:01:39.165Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/e8/fd63e15da5e3fd4c2146f8bbb3c14e94ab850589beab88e547b2dbce22e1/lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f", size = 5676759, upload-time = "2025-09-22T04:01:41.506Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/47/b3ec58dc5c374697f5ba37412cd2728f427d056315d124dd4b61da381877/lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6", size = 5255666, upload-time = "2025-09-22T04:01:43.363Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/93/03ba725df4c3d72afd9596eef4a37a837ce8e4806010569bedfcd2cb68fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322", size = 5277989, upload-time = "2025-09-22T04:01:45.215Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/80/c06de80bfce881d0ad738576f243911fccf992687ae09fd80b734712b39c/lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849", size = 3611456, upload-time = "2025-09-22T04:01:48.243Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/d7/0cdfb6c3e30893463fb3d1e52bc5f5f99684a03c29a0b6b605cfae879cd5/lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f", size = 4011793, upload-time = "2025-09-22T04:01:50.042Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/7b/93c73c67db235931527301ed3785f849c78991e2e34f3fd9a6663ffda4c5/lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6", size = 3672836, upload-time = "2025-09-22T04:01:52.145Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/4c/552571c619edd607432cbbf25e312a5d02859f2a7de421494a644b48451e/lxml-6.0.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ad6952810349cbfb843fe15e8afc580b2712359ae42b1d2b05d097bd48c4aea4", size = 8570109, upload-time = "2026-04-09T14:34:50.969Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/49/cf08843a6a923cd1eef40797a31e61424ac257c43634b5c9cff3bee93696/lxml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b81ec1ecac3be8c1ff1e00ca1c1baf8122e87db9000cd2549963847bd5e3b41", size = 4623404, upload-time = "2026-04-09T14:34:53.79Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/59/ffde0037a781b10c854abdf9e34fbf60d8f375ce8026551982b9f26695cc/lxml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:448e69211e59c39f398990753d15ba49f7218ec128f64ac8012ef16762e509a3", size = 4929662, upload-time = "2026-04-09T14:34:55.763Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/29/c468055e45954a93e1bc043a964d327d6784552d6551dc2364a1f83c53a1/lxml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6289cb9145fbbc5b0e159c9fcd7fc09446dadc6b60b72c4d1012e80c7c727970", size = 5092106, upload-time = "2026-04-09T14:34:58.522Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/a3/8400c79a6defe609e24ce7b580f48d53f08acbf4c998eede0083a89f16f0/lxml-6.0.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b68c29aac4788438b07d768057836de47589c7deaa3ad8dc4af488dfc27be388", size = 5004214, upload-time = "2026-04-09T14:35:00.531Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/b5/797246619cd0831c8d239f91fd4683683abbe7144854c6f33c68a6ea9f42/lxml-6.0.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:50293e024afe5e2c25da2be68c8ceca8618912a0701a73f75b488317c8081aa6", size = 5630889, upload-time = "2026-04-09T14:35:02.89Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/fa/b86302385dc896d02ebb2803e4522a923acaa30e6cb35223492257ee24ab/lxml-6.0.3-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac65c08ba1bd90f662cb1d5c79f7ae4c53b1c100f0bb6ec5df1f40ac29028a7e", size = 5237728, upload-time = "2026-04-09T14:35:05.827Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/7d/812c054b7d15f4dfb3a6fc877c2936023fcd8ac8b53807f996c8c60c4f57/lxml-6.0.3-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:16fbcf06ae534b2fa5bcdc19fcf6abd9df2e74fe8563147d1c5a687a130efed4", size = 5349527, upload-time = "2026-04-09T14:35:08.121Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/4a/33a572874924809928747cd156b172b04cd19c1ec1d10925fc77dfeb676d/lxml-6.0.3-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:3a0484bd1e84f82766befcbd71cccd7307dacfe08071e4dbc1d9a9b498d321e8", size = 4693177, upload-time = "2026-04-09T14:35:10.4Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/d5/71842813ca0c43718f641e770195e278832f8c01870eaac857a3de34448a/lxml-6.0.3-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c137f8c8419c3de93e2998131d94628805f148e52b34da6d7533454e4d78bc2a", size = 5243928, upload-time = "2026-04-09T14:35:12.393Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/a7/330845ae467c6086ef35977c335bb252fa11490082335f9ccfd0465bdfb7/lxml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:775266571f7027b1d77f5fce18a247b24f51a4404bdc1b90ec56be9b1e3801b9", size = 5046937, upload-time = "2026-04-09T14:35:15.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/3d/b58b0aee0cf7e0b7eb5d24795a129c634c6d07f032d8b902bb0859319d13/lxml-6.0.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:aa18653b795d2c273b8676f7ad2ca916d846d15864e335f746658e4c28eb5168", size = 4776758, upload-time = "2026-04-09T14:35:17.758Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/4c/f421b50f08c1b724a24c4a778db8888d0a2d948b4dd08b80f4f05a0804ff/lxml-6.0.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:cbffd22fc8e4d80454efa968b0c93440a00b8b8a817ce0c29d2c6cb5ad324362", size = 5644912, upload-time = "2026-04-09T14:35:20.438Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/99/eabfedb111ca1f26c8fe890413eabc7e2b0010f075fdf5bceb42737c3894/lxml-6.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7373ede7ccb89e6f6e39c1423b3a4d4ee48035d3b4619a6addced5c8b48d0ecc", size = 5233509, upload-time = "2026-04-09T14:35:23.137Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/17/050a105ca1154025b68c19901d45292cbdcee6f25bd056c178ad6b55e534/lxml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e759ff1b244725fef428c6b54f3dab4954c293b2d242a5f2e79db5cc3873de51", size = 5260150, upload-time = "2026-04-09T14:35:25.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/a0/ed83517d12e9fe00101a21fe08a168fd69f57875d9416353e2a38c401df7/lxml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:f179bae37ad673f57756b59f26833b7922230bef471fdb29492428f152bae8c6", size = 3595160, upload-time = "2026-04-09T14:35:27.519Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/d3/101726831f45951fe3ddd03cffbd2a4ac6261fc63ada399e6f7051d43af6/lxml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:8eeec925ad7f81886d413b3a1f8715551f75543519229a9b35e957771e1826d5", size = 3996108, upload-time = "2026-04-09T14:35:29.608Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/9f/ab1c58ad55bfcd4b55bafd98f19ff24f34315441f13aa787d5220def0702/lxml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:f96bba9a26a064ce9e11099bad12fb08384b64d3acc0acf94bf386ca5cf4f95f", size = 3658906, upload-time = "2026-04-09T14:35:32.451Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -864,7 +864,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "mealie"
|
||||
version = "3.15.0"
|
||||
version = "3.15.2"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiofiles" },
|
||||
@@ -960,8 +960,8 @@ requires-dist = [
|
||||
{ name = "isodate", specifier = "==0.7.2" },
|
||||
{ name = "itsdangerous", specifier = "==2.2.0" },
|
||||
{ name = "jinja2", specifier = "==3.1.6" },
|
||||
{ name = "lxml", specifier = "==6.0.2" },
|
||||
{ name = "openai", specifier = "==2.30.0" },
|
||||
{ name = "lxml", specifier = "==6.0.3" },
|
||||
{ name = "openai", specifier = "==2.31.0" },
|
||||
{ name = "orjson", specifier = "==3.11.8" },
|
||||
{ name = "paho-mqtt", specifier = "==1.6.1" },
|
||||
{ name = "pillow", specifier = "==12.2.0" },
|
||||
@@ -975,7 +975,7 @@ requires-dist = [
|
||||
{ name = "python-dateutil", specifier = "==2.9.0.post0" },
|
||||
{ name = "python-dotenv", specifier = "==1.2.2" },
|
||||
{ name = "python-ldap", specifier = "==3.4.5" },
|
||||
{ name = "python-multipart", specifier = "==0.0.24" },
|
||||
{ name = "python-multipart", specifier = "==0.0.26" },
|
||||
{ name = "python-slugify", specifier = "==8.0.4" },
|
||||
{ name = "pyyaml", specifier = "==6.0.3" },
|
||||
{ name = "rapidfuzz", specifier = "==3.14.5" },
|
||||
@@ -1003,7 +1003,7 @@ dev = [
|
||||
{ name = "pytest", specifier = "==9.0.3" },
|
||||
{ name = "pytest-asyncio", specifier = "==1.3.0" },
|
||||
{ name = "rich", specifier = "==15.0.0" },
|
||||
{ name = "ruff", specifier = "==0.15.9" },
|
||||
{ name = "ruff", specifier = "==0.15.10" },
|
||||
{ name = "types-python-dateutil", specifier = "==2.9.0.20260408" },
|
||||
{ name = "types-python-slugify", specifier = "==8.0.2.20240310" },
|
||||
{ name = "types-pyyaml", specifier = "==6.0.12.20260408" },
|
||||
@@ -1189,7 +1189,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "openai"
|
||||
version = "2.30.0"
|
||||
version = "2.31.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
@@ -1201,9 +1201,9 @@ dependencies = [
|
||||
{ name = "tqdm" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/88/15/52580c8fbc16d0675d516e8749806eda679b16de1e4434ea06fb6feaa610/openai-2.30.0.tar.gz", hash = "sha256:92f7661c990bda4b22a941806c83eabe4896c3094465030dd882a71abe80c885", size = 676084, upload-time = "2026-03-25T22:08:59.96Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/94/fe/64b3d035780b3188f86c4f6f1bc202e7bb74757ef028802112273b9dcacf/openai-2.31.0.tar.gz", hash = "sha256:43ca59a88fc973ad1848d86b98d7fac207e265ebbd1828b5e4bdfc85f79427a5", size = 684772, upload-time = "2026-04-08T21:01:41.797Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/9e/5bfa2270f902d5b92ab7d41ce0475b8630572e71e349b2a4996d14bdda93/openai-2.30.0-py3-none-any.whl", hash = "sha256:9a5ae616888eb2748ec5e0c5b955a51592e0b201a11f4262db920f2a78c5231d", size = 1146656, upload-time = "2026-03-25T22:08:58.2Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/bc/a8f7c3aa03452fedbb9af8be83e959adba96a6b4a35e416faffcc959c568/openai-2.31.0-py3-none-any.whl", hash = "sha256:44e1344d87e56a493d649b17e2fac519d1368cbb0745f59f1957c4c26de50a0a", size = 1153479, upload-time = "2026-04-08T21:01:39.217Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1627,11 +1627,11 @@ sdist = { url = "https://files.pythonhosted.org/packages/0c/88/8d2797decc42e1c1c
|
||||
|
||||
[[package]]
|
||||
name = "python-multipart"
|
||||
version = "0.0.24"
|
||||
version = "0.0.26"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8a/45/e23b5dc14ddb9918ae4a625379506b17b6f8fc56ca1d82db62462f59aea6/python_multipart-0.0.24.tar.gz", hash = "sha256:9574c97e1c026e00bc30340ef7c7d76739512ab4dfd428fec8c330fa6a5cc3c8", size = 37695, upload-time = "2026-04-05T20:49:13.829Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/88/71/b145a380824a960ebd60e1014256dbb7d2253f2316ff2d73dfd8928ec2c3/python_multipart-0.0.26.tar.gz", hash = "sha256:08fadc45918cd615e26846437f50c5d6d23304da32c341f289a617127b081f17", size = 43501, upload-time = "2026-04-10T14:09:59.473Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/73/89930efabd4da63cea44a3f438aeb753d600123570e6d6264e763617a9ce/python_multipart-0.0.24-py3-none-any.whl", hash = "sha256:9b110a98db707df01a53c194f0af075e736a770dc5058089650d70b4a182f950", size = 24420, upload-time = "2026-04-05T20:49:12.555Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-py3-none-any.whl", hash = "sha256:c0b169f8c4484c13b0dcf2ef0ec3a4adb255c4b7d18d8e420477d2b1dd03f185", size = 28847, upload-time = "2026-04-10T14:09:58.131Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1786,27 +1786,27 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.15.9"
|
||||
version = "0.15.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e6/97/e9f1ca355108ef7194e38c812ef40ba98c7208f47b13ad78d023caa583da/ruff-0.15.9.tar.gz", hash = "sha256:29cbb1255a9797903f6dde5ba0188c707907ff44a9006eb273b5a17bfa0739a2", size = 4617361, upload-time = "2026-04-02T18:17:20.829Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e7/d9/aa3f7d59a10ef6b14fe3431706f854dbf03c5976be614a9796d36326810c/ruff-0.15.10.tar.gz", hash = "sha256:d1f86e67ebfdef88e00faefa1552b5e510e1d35f3be7d423dc7e84e63788c94e", size = 4631728, upload-time = "2026-04-09T14:06:09.884Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/1f/9cdfd0ac4b9d1e5a6cf09bedabdf0b56306ab5e333c85c87281273e7b041/ruff-0.15.9-py3-none-linux_armv6l.whl", hash = "sha256:6efbe303983441c51975c243e26dff328aca11f94b70992f35b093c2e71801e1", size = 10511206, upload-time = "2026-04-02T18:16:41.574Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/f6/32bfe3e9c136b35f02e489778d94384118bb80fd92c6d92e7ccd97db12ce/ruff-0.15.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4965bac6ac9ea86772f4e23587746f0b7a395eccabb823eb8bfacc3fa06069f7", size = 10923307, upload-time = "2026-04-02T18:17:08.645Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/25/de55f52ab5535d12e7aaba1de37a84be6179fb20bddcbe71ec091b4a3243/ruff-0.15.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf05aad70ca5b5a0a4b0e080df3a6b699803916d88f006efd1f5b46302daab8", size = 10316722, upload-time = "2026-04-02T18:16:44.206Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/11/690d75f3fd6278fe55fff7c9eb429c92d207e14b25d1cae4064a32677029/ruff-0.15.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9439a342adb8725f32f92732e2bafb6d5246bd7a5021101166b223d312e8fc59", size = 10623674, upload-time = "2026-04-02T18:16:50.951Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/ec/176f6987be248fc5404199255522f57af1b4a5a1b57727e942479fec98ad/ruff-0.15.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c5e6faf9d97c8edc43877c3f406f47446fc48c40e1442d58cfcdaba2acea745", size = 10351516, upload-time = "2026-04-02T18:16:57.206Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/fc/51cffbd2b3f240accc380171d51446a32aa2ea43a40d4a45ada67368fbd2/ruff-0.15.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b34a9766aeec27a222373d0b055722900fbc0582b24f39661aa96f3fe6ad901", size = 11150202, upload-time = "2026-04-02T18:17:06.452Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/d4/25292a6dfc125f6b6528fe6af31f5e996e19bf73ca8e3ce6eb7fa5b95885/ruff-0.15.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89dd695bc72ae76ff484ae54b7e8b0f6b50f49046e198355e44ea656e521fef9", size = 11988891, upload-time = "2026-04-02T18:17:18.575Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/e1/1eebcb885c10e19f969dcb93d8413dfee8172578709d7ee933640f5e7147/ruff-0.15.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce187224ef1de1bd225bc9a152ac7102a6171107f026e81f317e4257052916d5", size = 11480576, upload-time = "2026-04-02T18:16:52.986Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/6b/a1548ac378a78332a4c3dcf4a134c2475a36d2a22ddfa272acd574140b50/ruff-0.15.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b0c7c341f68adb01c488c3b7d4b49aa8ea97409eae6462d860a79cf55f431b6", size = 11254525, upload-time = "2026-04-02T18:17:02.041Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/aa/4bb3af8e61acd9b1281db2ab77e8b2c3c5e5599bf2a29d4a942f1c62b8d6/ruff-0.15.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:55cc15eee27dc0eebdfcb0d185a6153420efbedc15eb1d38fe5e685657b0f840", size = 11204072, upload-time = "2026-04-02T18:17:13.581Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/48/d550dc2aa6e423ea0bcc1d0ff0699325ffe8a811e2dba156bd80750b86dc/ruff-0.15.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a6537f6eed5cda688c81073d46ffdfb962a5f29ecb6f7e770b2dc920598997ed", size = 10594998, upload-time = "2026-04-02T18:16:46.369Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/47/321167e17f5344ed5ec6b0aa2cff64efef5f9e985af8f5622cfa6536043f/ruff-0.15.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6d3fcbca7388b066139c523bda744c822258ebdcfbba7d24410c3f454cc9af71", size = 10359769, upload-time = "2026-04-02T18:17:10.994Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/5e/074f00b9785d1d2c6f8c22a21e023d0c2c1817838cfca4c8243200a1fa87/ruff-0.15.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:058d8e99e1bfe79d8a0def0b481c56059ee6716214f7e425d8e737e412d69677", size = 10850236, upload-time = "2026-04-02T18:16:48.749Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/37/804c4135a2a2caf042925d30d5f68181bdbd4461fd0d7739da28305df593/ruff-0.15.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8e1ddb11dbd61d5983fa2d7d6370ef3eb210951e443cace19594c01c72abab4c", size = 11358343, upload-time = "2026-04-02T18:16:55.068Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/3d/1364fcde8656962782aa9ea93c92d98682b1ecec2f184e625a965ad3b4a6/ruff-0.15.9-py3-none-win32.whl", hash = "sha256:bde6ff36eaf72b700f32b7196088970bf8fdb2b917b7accd8c371bfc0fd573ec", size = 10583382, upload-time = "2026-04-02T18:17:04.261Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/56/5c7084299bd2cacaa07ae63a91c6f4ba66edc08bf28f356b24f6b717c799/ruff-0.15.9-py3-none-win_amd64.whl", hash = "sha256:45a70921b80e1c10cf0b734ef09421f71b5aa11d27404edc89d7e8a69505e43d", size = 11744969, upload-time = "2026-04-02T18:16:59.611Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/36/76704c4f312257d6dbaae3c959add2a622f63fcca9d864659ce6d8d97d3d/ruff-0.15.9-py3-none-win_arm64.whl", hash = "sha256:0694e601c028fd97dc5c6ee244675bc241aeefced7ef80cd9c6935a871078f53", size = 11005870, upload-time = "2026-04-02T18:17:15.773Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/00/a1c2fdc9939b2c03691edbda290afcd297f1f389196172826b03d6b6a595/ruff-0.15.10-py3-none-linux_armv6l.whl", hash = "sha256:0744e31482f8f7d0d10a11fcbf897af272fefdfcb10f5af907b18c2813ff4d5f", size = 10563362, upload-time = "2026-04-09T14:06:21.189Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/15/006990029aea0bebe9d33c73c3e28c80c391ebdba408d1b08496f00d422d/ruff-0.15.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b1e7c16ea0ff5a53b7c2df52d947e685973049be1cdfe2b59a9c43601897b22e", size = 10951122, upload-time = "2026-04-09T14:06:02.236Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/c0/4ac978fe874d0618c7da647862afe697b281c2806f13ce904ad652fa87e4/ruff-0.15.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93cc06a19e5155b4441dd72808fdf84290d84ad8a39ca3b0f994363ade4cebb1", size = 10314005, upload-time = "2026-04-09T14:06:00.026Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/73/c209138a5c98c0d321266372fc4e33ad43d506d7e5dd817dd89b60a8548f/ruff-0.15.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83e1dd04312997c99ea6965df66a14fb4f03ba978564574ffc68b0d61fd3989e", size = 10643450, upload-time = "2026-04-09T14:05:42.137Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/76/0deec355d8ec10709653635b1f90856735302cb8e149acfdf6f82a5feb70/ruff-0.15.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8154d43684e4333360fedd11aaa40b1b08a4e37d8ffa9d95fee6fa5b37b6fab1", size = 10379597, upload-time = "2026-04-09T14:05:49.984Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/be/86bba8fc8798c081e28a4b3bb6d143ccad3fd5f6f024f02002b8f08a9fa3/ruff-0.15.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ab88715f3a6deb6bde6c227f3a123410bec7b855c3ae331b4c006189e895cef", size = 11146645, upload-time = "2026-04-09T14:06:12.246Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/89/140025e65911b281c57be1d385ba1d932c2366ca88ae6663685aed8d4881/ruff-0.15.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a768ff5969b4f44c349d48edf4ab4f91eddb27fd9d77799598e130fb628aa158", size = 12030289, upload-time = "2026-04-09T14:06:04.776Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/de/ddacca9545a5e01332567db01d44bd8cf725f2db3b3d61a80550b48308ea/ruff-0.15.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ee3ef42dab7078bda5ff6a1bcba8539e9857deb447132ad5566a038674540d0", size = 11496266, upload-time = "2026-04-09T14:05:55.485Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/bb/7ddb00a83760ff4a83c4e2fc231fd63937cc7317c10c82f583302e0f6586/ruff-0.15.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51cb8cc943e891ba99989dd92d61e29b1d231e14811db9be6440ecf25d5c1609", size = 11256418, upload-time = "2026-04-09T14:05:57.69Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/8d/55de0d35aacf6cd50b6ee91ee0f291672080021896543776f4170fc5c454/ruff-0.15.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e59c9bdc056a320fb9ea1700a8d591718b8faf78af065484e801258d3a76bc3f", size = 11288416, upload-time = "2026-04-09T14:05:44.695Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/cf/9438b1a27426ec46a80e0a718093c7f958ef72f43eb3111862949ead3cc1/ruff-0.15.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:136c00ca2f47b0018b073f28cb5c1506642a830ea941a60354b0e8bc8076b151", size = 10621053, upload-time = "2026-04-09T14:05:52.782Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/50/e29be6e2c135e9cd4cb15fbade49d6a2717e009dff3766dd080fcb82e251/ruff-0.15.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8b80a2f3c9c8a950d6237f2ca12b206bccff626139be9fa005f14feb881a1ae8", size = 10378302, upload-time = "2026-04-09T14:06:14.361Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/2f/e0b36a6f99c51bb89f3a30239bc7bf97e87a37ae80aa2d6542d6e5150364/ruff-0.15.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e3e53c588164dc025b671c9df2462429d60357ea91af7e92e9d56c565a9f1b07", size = 10850074, upload-time = "2026-04-09T14:06:16.581Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/08/874da392558ce087a0f9b709dc6ec0d60cbc694c1c772dab8d5f31efe8cb/ruff-0.15.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b0c52744cf9f143a393e284125d2576140b68264a93c6716464e129a3e9adb48", size = 11358051, upload-time = "2026-04-09T14:06:18.948Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/46/602938f030adfa043e67112b73821024dc79f3ab4df5474c25fa4c1d2d14/ruff-0.15.10-py3-none-win32.whl", hash = "sha256:d4272e87e801e9a27a2e8df7b21011c909d9ddd82f4f3281d269b6ba19789ca5", size = 10588964, upload-time = "2026-04-09T14:06:07.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/b6/261225b875d7a13b33a6d02508c39c28450b2041bb01d0f7f1a83d569512/ruff-0.15.10-py3-none-win_amd64.whl", hash = "sha256:28cb32d53203242d403d819fd6983152489b12e4a3ae44993543d6fe62ab42ed", size = 11745044, upload-time = "2026-04-09T14:05:39.473Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/ed/dea90a65b7d9e69888890fb14c90d7f51bf0c1e82ad800aeb0160e4bacfd/ruff-0.15.10-py3-none-win_arm64.whl", hash = "sha256:601d1610a9e1f1c2165a4f561eeaa2e2ea1e97f3287c5aa258d3dab8b57c6188", size = 11035607, upload-time = "2026-04-09T14:05:47.593Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user