mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-07-09 03:04:54 -04:00
feat(backend): ✨ Minor linting, bulk URL import, and improve BG tasks (#760)
* Fixes #751 * Fixes not showing original URL * start slice at 0 instead of 1 * remove print statements * add linter for print statements and remove print * hide all buttons when edit disabled * add bulk import API * update attribute bindings * unify button styles * bulk add recipe feature * thanks linter! * uncomment code Co-authored-by: Hayden <hay-kot@pm.me>
This commit is contained in:
parent
1e5ef28f91
commit
2afaf70a03
@ -1,4 +1,6 @@
|
|||||||
import { BaseCRUDAPI } from "../_base";
|
import { BaseCRUDAPI } from "../_base";
|
||||||
|
import { Category } from "./categories";
|
||||||
|
import { Tag } from "./tags";
|
||||||
import { Recipe, CreateRecipe } from "~/types/api-types/recipe";
|
import { Recipe, CreateRecipe } from "~/types/api-types/recipe";
|
||||||
|
|
||||||
const prefix = "/api";
|
const prefix = "/api";
|
||||||
@ -8,6 +10,7 @@ const routes = {
|
|||||||
recipesBase: `${prefix}/recipes`,
|
recipesBase: `${prefix}/recipes`,
|
||||||
recipesTestScrapeUrl: `${prefix}/recipes/test-scrape-url`,
|
recipesTestScrapeUrl: `${prefix}/recipes/test-scrape-url`,
|
||||||
recipesCreateUrl: `${prefix}/recipes/create-url`,
|
recipesCreateUrl: `${prefix}/recipes/create-url`,
|
||||||
|
recipesCreateUrlBulk: `${prefix}/recipes/create-url/bulk`,
|
||||||
recipesCreateFromZip: `${prefix}/recipes/create-from-zip`,
|
recipesCreateFromZip: `${prefix}/recipes/create-from-zip`,
|
||||||
recipesCategory: `${prefix}/recipes/category`,
|
recipesCategory: `${prefix}/recipes/category`,
|
||||||
recipesParseIngredient: `${prefix}/parser/ingredient`,
|
recipesParseIngredient: `${prefix}/parser/ingredient`,
|
||||||
@ -59,6 +62,16 @@ export interface ParsedIngredient {
|
|||||||
ingredient: Ingredient;
|
ingredient: Ingredient;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BulkCreateRecipe {
|
||||||
|
url: string;
|
||||||
|
categories: Category[];
|
||||||
|
tags: Tag[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BulkCreatePayload {
|
||||||
|
imports: BulkCreateRecipe[];
|
||||||
|
}
|
||||||
|
|
||||||
export class RecipeAPI extends BaseCRUDAPI<Recipe, CreateRecipe> {
|
export class RecipeAPI extends BaseCRUDAPI<Recipe, CreateRecipe> {
|
||||||
baseRoute: string = routes.recipesBase;
|
baseRoute: string = routes.recipesBase;
|
||||||
itemRoute = routes.recipesRecipeSlug;
|
itemRoute = routes.recipesRecipeSlug;
|
||||||
@ -90,6 +103,10 @@ export class RecipeAPI extends BaseCRUDAPI<Recipe, CreateRecipe> {
|
|||||||
return await this.requests.post(routes.recipesCreateUrl, { url });
|
return await this.requests.post(routes.recipesCreateUrl, { url });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createManyByUrl(payload: BulkCreatePayload) {
|
||||||
|
return await this.requests.post(routes.recipesCreateUrlBulk, payload);
|
||||||
|
}
|
||||||
|
|
||||||
// Recipe Comments
|
// Recipe Comments
|
||||||
|
|
||||||
// Methods to Generate reference urls for assets/images *
|
// Methods to Generate reference urls for assets/images *
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
//TODO: Prevent fetching Categories/Tags multiple time when selector is on page multiple times
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<v-autocomplete
|
<v-autocomplete
|
||||||
v-model="selected"
|
v-model="selected"
|
||||||
@ -14,12 +16,14 @@
|
|||||||
:solo="solo"
|
:solo="solo"
|
||||||
:return-object="returnObject"
|
:return-object="returnObject"
|
||||||
:flat="flat"
|
:flat="flat"
|
||||||
|
v-bind="$attrs"
|
||||||
@input="emitChange"
|
@input="emitChange"
|
||||||
>
|
>
|
||||||
<template #selection="data">
|
<template #selection="data">
|
||||||
<v-chip
|
<v-chip
|
||||||
v-if="showSelected"
|
v-if="showSelected"
|
||||||
:key="data.index"
|
:key="data.index"
|
||||||
|
:small="dense"
|
||||||
class="ma-1"
|
class="ma-1"
|
||||||
:input-value="data.selected"
|
:input-value="data.selected"
|
||||||
close
|
close
|
||||||
|
@ -26,9 +26,7 @@
|
|||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
<div v-if="edit" class="d-flex justify-end">
|
<div v-if="edit" class="d-flex justify-end">
|
||||||
<v-btn class="mt-1" color="secondary" dark @click="addNote">
|
<BaseButton class="ml-auto my-2" @click="addNote"> {{ $t("general.new") }}</BaseButton>
|
||||||
<v-icon>{{ $globals.icons.create }}</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -92,7 +92,7 @@
|
|||||||
@end="onMoveCallback"
|
@end="onMoveCallback"
|
||||||
>
|
>
|
||||||
<v-card v-for="mealplan in plan.meals" :key="mealplan.id" v-model="hover[mealplan.id]" class="my-1">
|
<v-card v-for="mealplan in plan.meals" :key="mealplan.id" v-model="hover[mealplan.id]" class="my-1">
|
||||||
<v-list-item>
|
<v-list-item :to="edit ? null : `/recipe/${mealplan.recipe.slug}`">
|
||||||
<v-list-item-avatar :rounded="false">
|
<v-list-item-avatar :rounded="false">
|
||||||
<RecipeCardImage v-if="mealplan.recipe" tiny icon-size="25" :slug="mealplan.recipe.slug" />
|
<RecipeCardImage v-if="mealplan.recipe" tiny icon-size="25" :slug="mealplan.recipe.slug" />
|
||||||
<v-icon v-else>
|
<v-icon v-else>
|
||||||
@ -108,8 +108,8 @@
|
|||||||
</v-list-item-subtitle>
|
</v-list-item-subtitle>
|
||||||
</v-list-item-content>
|
</v-list-item-content>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
<v-divider class="mx-2"></v-divider>
|
<v-divider v-if="edit" class="mx-2"></v-divider>
|
||||||
<v-card-actions>
|
<v-card-actions v-if="edit">
|
||||||
<v-btn color="error" icon @click="actions.deleteOne(mealplan.id)">
|
<v-btn color="error" icon @click="actions.deleteOne(mealplan.id)">
|
||||||
<v-icon>{{ $globals.icons.delete }}</v-icon>
|
<v-icon>{{ $globals.icons.delete }}</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
@ -110,8 +110,8 @@
|
|||||||
/>
|
/>
|
||||||
</draggable>
|
</draggable>
|
||||||
<div class="d-flex justify-end mt-2">
|
<div class="d-flex justify-end mt-2">
|
||||||
<RecipeIngredientParserMenu class="mr-1" :slug="recipe.slug" :ingredients="recipe.recipeIngredient" />
|
<RecipeIngredientParserMenu :slug="recipe.slug" :ingredients="recipe.recipeIngredient" />
|
||||||
<RecipeDialogBulkAdd class="mr-1" @bulk-data="addIngredient" />
|
<RecipeDialogBulkAdd class="ml-1 mr-1" @bulk-data="addIngredient" />
|
||||||
<BaseButton @click="addIngredient"> {{ $t("general.new") }} </BaseButton>
|
<BaseButton @click="addIngredient"> {{ $t("general.new") }} </BaseButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -228,6 +228,30 @@
|
|||||||
<RecipeNotes v-model="recipe.notes" :edit="form" />
|
<RecipeNotes v-model="recipe.notes" :edit="form" />
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
|
<v-card-actions class="justify-end">
|
||||||
|
<v-text-field
|
||||||
|
v-if="form"
|
||||||
|
v-model="recipe.orgURL"
|
||||||
|
class="mt-10"
|
||||||
|
:label="$t('recipe.original-url')"
|
||||||
|
></v-text-field>
|
||||||
|
<v-btn
|
||||||
|
v-else-if="recipe.orgURL"
|
||||||
|
dense
|
||||||
|
small
|
||||||
|
:hover="false"
|
||||||
|
type="label"
|
||||||
|
:ripple="false"
|
||||||
|
elevation="0"
|
||||||
|
:href="recipe.orgURL"
|
||||||
|
color="secondary darken-1"
|
||||||
|
target="_blank"
|
||||||
|
class="rounded-sm mr-4"
|
||||||
|
>
|
||||||
|
{{ $t("recipe.original-url") }}
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</div>
|
</div>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
@ -142,7 +142,7 @@
|
|||||||
<v-tab-item value="debug" eager>
|
<v-tab-item value="debug" eager>
|
||||||
<v-form ref="domUrlForm" @submit.prevent="debugUrl(recipeUrl)">
|
<v-form ref="domUrlForm" @submit.prevent="debugUrl(recipeUrl)">
|
||||||
<v-card flat>
|
<v-card flat>
|
||||||
<v-card-title class="headline"> Recipe Debugger</v-card-title>
|
<v-card-title class="headline"> Recipe Importer </v-card-title>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
Grab the URL of the recipe you want to debug and paste it here. The URL will be scraped by the recipe
|
Grab the URL of the recipe you want to debug and paste it here. The URL will be scraped by the recipe
|
||||||
scraper and the results will be displayed. If you don't see any data returned, the site you are trying
|
scraper and the results will be displayed. If you don't see any data returned, the site you are trying
|
||||||
@ -174,6 +174,17 @@
|
|||||||
</v-card>
|
</v-card>
|
||||||
</v-form>
|
</v-form>
|
||||||
</v-tab-item>
|
</v-tab-item>
|
||||||
|
|
||||||
|
<v-tab-item value="bulk" eager>
|
||||||
|
<v-card flat>
|
||||||
|
<v-card-title class="headline"> Recipe Bulk Importer </v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
The Bulk recipe importer allows you to import multiple recipes at once by queing the sites on the
|
||||||
|
backend and running the task in the background. This can be useful when initially migrating to Mealie,
|
||||||
|
or when you want to import a large number of recipes.
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-tab-item>
|
||||||
</v-tabs-items>
|
</v-tabs-items>
|
||||||
</section>
|
</section>
|
||||||
<v-divider class="mt-5"></v-divider>
|
<v-divider class="mt-5"></v-divider>
|
||||||
@ -195,6 +206,74 @@
|
|||||||
height="700px"
|
height="700px"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
<!-- Debug Extras -->
|
||||||
|
<section v-else-if="tab === 'bulk'" class="mt-2">
|
||||||
|
<v-row v-for="(bulkUrl, idx) in bulkUrls" :key="'bulk-url' + idx" class="my-1" dense>
|
||||||
|
<v-col cols="12" xs="12" sm="12" md="12">
|
||||||
|
<v-text-field
|
||||||
|
v-model="bulkUrls[idx].url"
|
||||||
|
:label="$t('new-recipe.recipe-url')"
|
||||||
|
dense
|
||||||
|
single-line
|
||||||
|
validate-on-blur
|
||||||
|
autofocus
|
||||||
|
filled
|
||||||
|
hide-details
|
||||||
|
clearable
|
||||||
|
:prepend-inner-icon="$globals.icons.link"
|
||||||
|
rounded
|
||||||
|
class="rounded-lg"
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<v-btn color="error" icon x-small @click="bulkUrls.splice(idx, 1)">
|
||||||
|
<v-icon>
|
||||||
|
{{ $globals.icons.delete }}
|
||||||
|
</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
</v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" xs="12" sm="6">
|
||||||
|
<RecipeCategoryTagSelector
|
||||||
|
v-model="bulkUrls[idx].categories"
|
||||||
|
validate-on-blur
|
||||||
|
autofocus
|
||||||
|
single-line
|
||||||
|
filled
|
||||||
|
hide-details
|
||||||
|
dense
|
||||||
|
clearable
|
||||||
|
rounded
|
||||||
|
class="rounded-lg"
|
||||||
|
></RecipeCategoryTagSelector>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" xs="12" sm="6">
|
||||||
|
<RecipeCategoryTagSelector
|
||||||
|
v-model="bulkUrls[idx].tags"
|
||||||
|
validate-on-blur
|
||||||
|
autofocus
|
||||||
|
tag-selector
|
||||||
|
hide-details
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
single-line
|
||||||
|
clearable
|
||||||
|
rounded
|
||||||
|
class="rounded-lg"
|
||||||
|
></RecipeCategoryTagSelector>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-card-actions class="justify-end">
|
||||||
|
<BaseButton delete @click="bulkUrls = []"> Clear </BaseButton>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<BaseButton color="info" @click="bulkUrls.push({ url: '', categories: [], tags: [] })">
|
||||||
|
<template #icon> {{ $globals.icons.createAlt }} </template> New
|
||||||
|
</BaseButton>
|
||||||
|
<BaseButton :disabled="bulkUrls.length === 0" @click="bulkCreate">
|
||||||
|
<template #icon> {{ $globals.icons.check }} </template> Submit
|
||||||
|
</BaseButton>
|
||||||
|
</v-card-actions>
|
||||||
|
</section>
|
||||||
</v-container>
|
</v-container>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -204,10 +283,12 @@ import { defineComponent, reactive, toRefs, ref, useRouter, useContext } from "@
|
|||||||
// @ts-ignore No Types for v-jsoneditor
|
// @ts-ignore No Types for v-jsoneditor
|
||||||
import VJsoneditor from "v-jsoneditor";
|
import VJsoneditor from "v-jsoneditor";
|
||||||
import { useApiSingleton } from "~/composables/use-api";
|
import { useApiSingleton } from "~/composables/use-api";
|
||||||
|
import RecipeCategoryTagSelector from "~/components/Domain/Recipe/RecipeCategoryTagSelector.vue";
|
||||||
import { validators } from "~/composables/use-validators";
|
import { validators } from "~/composables/use-validators";
|
||||||
import { Recipe } from "~/types/api-types/recipe";
|
import { Recipe } from "~/types/api-types/recipe";
|
||||||
|
import { alert } from "~/composables/use-toast";
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: { VJsoneditor },
|
components: { VJsoneditor, RecipeCategoryTagSelector },
|
||||||
setup() {
|
setup() {
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
error: false,
|
error: false,
|
||||||
@ -233,6 +314,11 @@ export default defineComponent({
|
|||||||
text: "Import with .zip",
|
text: "Import with .zip",
|
||||||
value: "zip",
|
value: "zip",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: $globals.icons.link,
|
||||||
|
text: "Bulk URL Import",
|
||||||
|
value: "bulk",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: $globals.icons.robot,
|
icon: $globals.icons.robot,
|
||||||
text: "Debug Scraper",
|
text: "Debug Scraper",
|
||||||
@ -249,7 +335,6 @@ export default defineComponent({
|
|||||||
state.loading = false;
|
state.loading = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log(response);
|
|
||||||
router.push(`/recipe/${response.data}`);
|
router.push(`/recipe/${response.data}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -300,7 +385,6 @@ export default defineComponent({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { response } = await api.recipes.createOne({ name });
|
const { response } = await api.recipes.createOne({ name });
|
||||||
console.log("Create By Name Func", response);
|
|
||||||
handleResponse(response);
|
handleResponse(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -318,11 +402,31 @@ export default defineComponent({
|
|||||||
formData.append(newRecipeZipFileName, newRecipeZip.value);
|
formData.append(newRecipeZipFileName, newRecipeZip.value);
|
||||||
|
|
||||||
const { response } = await api.upload.file("/api/recipes/create-from-zip", formData);
|
const { response } = await api.upload.file("/api/recipes/create-from-zip", formData);
|
||||||
console.log(response);
|
|
||||||
handleResponse(response);
|
handleResponse(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===================================================
|
||||||
|
// Bulk Importer
|
||||||
|
|
||||||
|
const bulkUrls = ref([{ url: "", categories: [], tags: [] }]);
|
||||||
|
|
||||||
|
async function bulkCreate() {
|
||||||
|
if (bulkUrls.value.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { response } = await api.recipes.createManyByUrl({ imports: bulkUrls.value });
|
||||||
|
|
||||||
|
if (response?.status === 202) {
|
||||||
|
alert.success("Bulk Import process has started");
|
||||||
|
} else {
|
||||||
|
alert.error("Bulk import process has failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
bulkCreate,
|
||||||
|
bulkUrls,
|
||||||
debugTreeView,
|
debugTreeView,
|
||||||
tabs,
|
tabs,
|
||||||
domCreateByName,
|
domCreateByName,
|
||||||
|
@ -21,7 +21,7 @@ import { useLazyRecipes } from "~/composables/use-recipes";
|
|||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: { RecipeCardSection },
|
components: { RecipeCardSection },
|
||||||
setup() {
|
setup() {
|
||||||
const start = ref(1);
|
const start = ref(0);
|
||||||
const limit = ref(30);
|
const limit = ref(30);
|
||||||
const increment = ref(30);
|
const increment = ref(30);
|
||||||
const ready = ref(false);
|
const ready = ref(false);
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from mealie.core.root_logger import get_logger
|
||||||
from mealie.db.data_access_layer.access_model_factory import Database
|
from mealie.db.data_access_layer.access_model_factory import Database
|
||||||
|
|
||||||
CWD = Path(__file__).parent
|
CWD = Path(__file__).parent
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def get_default_foods():
|
def get_default_foods():
|
||||||
@ -23,10 +25,10 @@ def default_recipe_unit_init(db: Database) -> None:
|
|||||||
try:
|
try:
|
||||||
db.ingredient_units.create(unit)
|
db.ingredient_units.create(unit)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
logger.error(e)
|
||||||
|
|
||||||
for food in get_default_foods():
|
for food in get_default_foods():
|
||||||
try:
|
try:
|
||||||
db.ingredient_foods.create(food)
|
db.ingredient_foods.create(food)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
logger.error(e)
|
||||||
|
@ -33,7 +33,6 @@ async def check_email_config():
|
|||||||
|
|
||||||
@router.post("", response_model=EmailSuccess)
|
@router.post("", response_model=EmailSuccess)
|
||||||
async def send_test_email(data: EmailTest):
|
async def send_test_email(data: EmailTest):
|
||||||
print(data)
|
|
||||||
service = EmailService()
|
service = EmailService()
|
||||||
status = False
|
status = False
|
||||||
error = None
|
error = None
|
||||||
|
@ -8,17 +8,14 @@ from sqlalchemy.orm.session import Session
|
|||||||
from mealie.db.database import get_database
|
from mealie.db.database import get_database
|
||||||
from mealie.db.db_setup import generate_session
|
from mealie.db.db_setup import generate_session
|
||||||
from mealie.routes.routers import UserAPIRouter
|
from mealie.routes.routers import UserAPIRouter
|
||||||
from mealie.schema.recipe import CreateRecipeByURL, Recipe, RecipeAsset
|
from mealie.schema.recipe import CreateRecipeByUrl, Recipe, RecipeAsset
|
||||||
from mealie.services.image.image import scrape_image, write_image
|
from mealie.services.image.image import scrape_image, write_image
|
||||||
|
|
||||||
user_router = UserAPIRouter()
|
user_router = UserAPIRouter()
|
||||||
|
|
||||||
|
|
||||||
@user_router.post("/{slug}/image")
|
@user_router.post("/{slug}/image")
|
||||||
def scrape_image_url(
|
def scrape_image_url(slug: str, url: CreateRecipeByUrl):
|
||||||
slug: str,
|
|
||||||
url: CreateRecipeByURL,
|
|
||||||
):
|
|
||||||
""" Removes an existing image and replaces it with the incoming file. """
|
""" Removes an existing image and replaces it with the incoming file. """
|
||||||
|
|
||||||
scrape_image(url.url, slug)
|
scrape_image(url.url, slug)
|
||||||
|
@ -12,10 +12,12 @@ from mealie.core.root_logger import get_logger
|
|||||||
from mealie.db.database import get_database
|
from mealie.db.database import get_database
|
||||||
from mealie.db.db_setup import generate_session
|
from mealie.db.db_setup import generate_session
|
||||||
from mealie.routes.routers import UserAPIRouter
|
from mealie.routes.routers import UserAPIRouter
|
||||||
from mealie.schema.recipe import CreateRecipeByURL, Recipe, RecipeImageTypes
|
from mealie.schema.recipe import CreateRecipeByUrl, Recipe, RecipeImageTypes
|
||||||
from mealie.schema.recipe.recipe import CreateRecipe, RecipeSummary
|
from mealie.schema.recipe.recipe import CreateRecipe, CreateRecipeByUrlBulk, RecipeSummary
|
||||||
|
from mealie.schema.server.tasks import ServerTaskNames
|
||||||
from mealie.services.recipe.recipe_service import RecipeService
|
from mealie.services.recipe.recipe_service import RecipeService
|
||||||
from mealie.services.scraper.scraper import create_from_url, scrape_from_url
|
from mealie.services.scraper.scraper import create_from_url, scrape_from_url
|
||||||
|
from mealie.services.server_tasks.background_executory import BackgroundExecutor
|
||||||
|
|
||||||
user_router = UserAPIRouter()
|
user_router = UserAPIRouter()
|
||||||
logger = get_logger()
|
logger = get_logger()
|
||||||
@ -34,15 +36,55 @@ def create_from_name(data: CreateRecipe, recipe_service: RecipeService = Depends
|
|||||||
|
|
||||||
|
|
||||||
@user_router.post("/create-url", status_code=201, response_model=str)
|
@user_router.post("/create-url", status_code=201, response_model=str)
|
||||||
def parse_recipe_url(url: CreateRecipeByURL, recipe_service: RecipeService = Depends(RecipeService.private)):
|
def parse_recipe_url(url: CreateRecipeByUrl, recipe_service: RecipeService = Depends(RecipeService.private)):
|
||||||
""" Takes in a URL and attempts to scrape data and load it into the database """
|
""" Takes in a URL and attempts to scrape data and load it into the database """
|
||||||
|
|
||||||
recipe = create_from_url(url.url)
|
recipe = create_from_url(url.url)
|
||||||
return recipe_service.create_one(recipe).slug
|
return recipe_service.create_one(recipe).slug
|
||||||
|
|
||||||
|
|
||||||
|
@user_router.post("/create-url/bulk", status_code=202)
|
||||||
|
def parse_recipe_url_bulk(
|
||||||
|
bulk: CreateRecipeByUrlBulk,
|
||||||
|
recipe_service: RecipeService = Depends(RecipeService.private),
|
||||||
|
bg_service: BackgroundExecutor = Depends(BackgroundExecutor.private),
|
||||||
|
):
|
||||||
|
""" Takes in a URL and attempts to scrape data and load it into the database """
|
||||||
|
|
||||||
|
def bulk_import_func(task_id: int, session: Session) -> None:
|
||||||
|
database = get_database(session)
|
||||||
|
task = database.server_tasks.get_one(task_id)
|
||||||
|
|
||||||
|
task.append_log("test task has started")
|
||||||
|
|
||||||
|
for b in bulk.imports:
|
||||||
|
try:
|
||||||
|
recipe = create_from_url(b.url)
|
||||||
|
|
||||||
|
if b.tags:
|
||||||
|
recipe.tags = b.tags
|
||||||
|
|
||||||
|
if b.categories:
|
||||||
|
recipe.recipe_category = b.categories
|
||||||
|
|
||||||
|
recipe_service.create_one(recipe)
|
||||||
|
task.append_log(f"INFO: Created recipe from url: {b.url}")
|
||||||
|
except Exception as e:
|
||||||
|
task.append_log(f"Error: Failed to create recipe from url: {b.url}")
|
||||||
|
task.append_log(f"Error: {e}")
|
||||||
|
logger.error(f"Failed to create recipe from url: {b.url}")
|
||||||
|
logger.error(e)
|
||||||
|
database.server_tasks.update(task.id, task)
|
||||||
|
|
||||||
|
task.set_finished()
|
||||||
|
database.server_tasks.update(task.id, task)
|
||||||
|
|
||||||
|
bg_service.dispatch(ServerTaskNames.bulk_recipe_import, bulk_import_func)
|
||||||
|
|
||||||
|
return {"details": "task has been started"}
|
||||||
|
|
||||||
|
|
||||||
@user_router.post("/test-scrape-url")
|
@user_router.post("/test-scrape-url")
|
||||||
def test_parse_recipe_url(url: CreateRecipeByURL):
|
def test_parse_recipe_url(url: CreateRecipeByUrl):
|
||||||
# Debugger should produce the same result as the scraper sees before cleaning
|
# Debugger should produce the same result as the scraper sees before cleaning
|
||||||
scraped_data = scrape_from_url(url.url)
|
scraped_data = scrape_from_url(url.url)
|
||||||
if scraped_data:
|
if scraped_data:
|
||||||
@ -73,11 +115,8 @@ async def get_recipe_as_zip(
|
|||||||
):
|
):
|
||||||
""" Get a Recipe and It's Original Image as a Zip File """
|
""" Get a Recipe and It's Original Image as a Zip File """
|
||||||
db = get_database(session)
|
db = get_database(session)
|
||||||
|
|
||||||
recipe: Recipe = db.recipes.get(slug)
|
recipe: Recipe = db.recipes.get(slug)
|
||||||
|
|
||||||
image_asset = recipe.image_dir.joinpath(RecipeImageTypes.original.value)
|
image_asset = recipe.image_dir.joinpath(RecipeImageTypes.original.value)
|
||||||
|
|
||||||
with ZipFile(temp_path, "w") as myzip:
|
with ZipFile(temp_path, "w") as myzip:
|
||||||
myzip.writestr(f"{slug}.json", recipe.json())
|
myzip.writestr(f"{slug}.json", recipe.json())
|
||||||
|
|
||||||
|
@ -25,7 +25,6 @@ class CreatePlanEntry(CamelModel):
|
|||||||
@validator("recipe_id", always=True)
|
@validator("recipe_id", always=True)
|
||||||
@classmethod
|
@classmethod
|
||||||
def id_or_title(cls, value, values):
|
def id_or_title(cls, value, values):
|
||||||
print(value, values)
|
|
||||||
if bool(value) is False and bool(values["title"]) is False:
|
if bool(value) is False and bool(values["title"]) is False:
|
||||||
raise ValueError(f"`recipe_id={value}` or `title={values['title']}` must be provided")
|
raise ValueError(f"`recipe_id={value}` or `title={values['title']}` must be provided")
|
||||||
|
|
||||||
|
@ -21,17 +21,6 @@ from .recipe_step import RecipeStep
|
|||||||
app_dirs = get_app_dirs()
|
app_dirs = get_app_dirs()
|
||||||
|
|
||||||
|
|
||||||
class CreateRecipeByURL(BaseModel):
|
|
||||||
url: str
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
schema_extra = {"example": {"url": "https://myfavoriterecipes.com/recipes"}}
|
|
||||||
|
|
||||||
|
|
||||||
class CreateRecipe(CamelModel):
|
|
||||||
name: str
|
|
||||||
|
|
||||||
|
|
||||||
class RecipeTag(CamelModel):
|
class RecipeTag(CamelModel):
|
||||||
name: str
|
name: str
|
||||||
slug: str
|
slug: str
|
||||||
@ -44,6 +33,27 @@ class RecipeCategory(RecipeTag):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CreateRecipeByUrl(BaseModel):
|
||||||
|
url: str
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
schema_extra = {"example": {"url": "https://myfavoriterecipes.com/recipes"}}
|
||||||
|
|
||||||
|
|
||||||
|
class CreateRecipeBulk(BaseModel):
|
||||||
|
url: str
|
||||||
|
categories: list[RecipeCategory] = None
|
||||||
|
tags: list[RecipeTag] = None
|
||||||
|
|
||||||
|
|
||||||
|
class CreateRecipeByUrlBulk(BaseModel):
|
||||||
|
imports: list[CreateRecipeBulk]
|
||||||
|
|
||||||
|
|
||||||
|
class CreateRecipe(CamelModel):
|
||||||
|
name: str
|
||||||
|
|
||||||
|
|
||||||
class RecipeSummary(CamelModel):
|
class RecipeSummary(CamelModel):
|
||||||
id: Optional[int]
|
id: Optional[int]
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ from pydantic import Field
|
|||||||
class ServerTaskNames(str, enum.Enum):
|
class ServerTaskNames(str, enum.Enum):
|
||||||
default = "Background Task"
|
default = "Background Task"
|
||||||
backup_task = "Database Backup"
|
backup_task = "Database Backup"
|
||||||
|
bulk_recipe_import = "Bulk Recipe Import"
|
||||||
|
|
||||||
|
|
||||||
class ServerTaskStatus(str, enum.Enum):
|
class ServerTaskStatus(str, enum.Enum):
|
||||||
|
@ -37,6 +37,7 @@ class CrudHttpMixins(Generic[C, R, U], ABC):
|
|||||||
self.item = self.dal.create(data)
|
self.item = self.dal.create(data)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.exception(ex)
|
logger.exception(ex)
|
||||||
|
self.session.rollback()
|
||||||
|
|
||||||
msg = default_msg
|
msg = default_msg
|
||||||
if exception_msgs:
|
if exception_msgs:
|
||||||
|
@ -73,14 +73,3 @@ class EmailService(BaseService):
|
|||||||
button_text="Test Email",
|
button_text="Test Email",
|
||||||
)
|
)
|
||||||
return self.send_email(address, test_email)
|
return self.send_email(address, test_email)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
print("Starting...")
|
|
||||||
service = EmailService()
|
|
||||||
service.send_test_email("hay-kot@pm.me")
|
|
||||||
print("Finished...")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
@ -19,7 +19,7 @@ replace_abbreviations = {
|
|||||||
def replace_common_abbreviations(string: str) -> str:
|
def replace_common_abbreviations(string: str) -> str:
|
||||||
|
|
||||||
for k, v in replace_abbreviations.items():
|
for k, v in replace_abbreviations.items():
|
||||||
regex = rf"(?<=\d)\s?({k}s?)"
|
regex = rf"(?<=\d)\s?({k}\bs?)"
|
||||||
string = re.sub(regex, v, string)
|
string = re.sub(regex, v, string)
|
||||||
|
|
||||||
return string
|
return string
|
||||||
|
@ -43,13 +43,9 @@ def clean_string(text: str) -> str:
|
|||||||
if isinstance(text, list):
|
if isinstance(text, list):
|
||||||
text = text[0]
|
text = text[0]
|
||||||
|
|
||||||
print(type(text))
|
|
||||||
|
|
||||||
if text == "" or text is None:
|
if text == "" or text is None:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
print(text)
|
|
||||||
|
|
||||||
cleaned_text = html.unescape(text)
|
cleaned_text = html.unescape(text)
|
||||||
cleaned_text = re.sub("<[^<]+?>", "", cleaned_text)
|
cleaned_text = re.sub("<[^<]+?>", "", cleaned_text)
|
||||||
cleaned_text = re.sub(" +", " ", cleaned_text)
|
cleaned_text = re.sub(" +", " ", cleaned_text)
|
||||||
@ -201,9 +197,10 @@ def clean_time(time_entry):
|
|||||||
if time_entry is None:
|
if time_entry is None:
|
||||||
return None
|
return None
|
||||||
elif isinstance(time_entry, timedelta):
|
elif isinstance(time_entry, timedelta):
|
||||||
pretty_print_timedelta(time_entry)
|
return pretty_print_timedelta(time_entry)
|
||||||
elif isinstance(time_entry, datetime):
|
elif isinstance(time_entry, datetime):
|
||||||
print(time_entry)
|
pass
|
||||||
|
# print(time_entry)
|
||||||
elif isinstance(time_entry, str):
|
elif isinstance(time_entry, str):
|
||||||
try:
|
try:
|
||||||
time_delta_object = parse_duration(time_entry)
|
time_delta_object = parse_duration(time_entry)
|
||||||
|
19
poetry.lock
generated
19
poetry.lock
generated
@ -372,6 +372,19 @@ mccabe = ">=0.6.0,<0.7.0"
|
|||||||
pycodestyle = ">=2.7.0,<2.8.0"
|
pycodestyle = ">=2.7.0,<2.8.0"
|
||||||
pyflakes = ">=2.3.0,<2.4.0"
|
pyflakes = ">=2.3.0,<2.4.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flake8-print"
|
||||||
|
version = "4.0.0"
|
||||||
|
description = "print statement checker plugin for flake8"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
flake8 = ">=3.0"
|
||||||
|
pycodestyle = "*"
|
||||||
|
six = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ghp-import"
|
name = "ghp-import"
|
||||||
version = "2.0.2"
|
version = "2.0.2"
|
||||||
@ -1381,7 +1394,7 @@ pgsql = ["psycopg2-binary"]
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "1.1"
|
||||||
python-versions = "^3.9"
|
python-versions = "^3.9"
|
||||||
content-hash = "89271346f576de3d209ae69639ab7227c03bb8512a1671905a48407d76371ba9"
|
content-hash = "31d3ee104998ad61b18322584c0cc84de32dbad0dc7657c9f7b7ae8214dae9c3"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
aiofiles = [
|
aiofiles = [
|
||||||
@ -1619,6 +1632,10 @@ flake8 = [
|
|||||||
{file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"},
|
{file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"},
|
||||||
{file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"},
|
{file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"},
|
||||||
]
|
]
|
||||||
|
flake8-print = [
|
||||||
|
{file = "flake8-print-4.0.0.tar.gz", hash = "sha256:5afac374b7dc49aac2c36d04b5eb1d746d72e6f5df75a6ecaecd99e9f79c6516"},
|
||||||
|
{file = "flake8_print-4.0.0-py3-none-any.whl", hash = "sha256:6c0efce658513169f96d7a24cf136c434dc711eb00ebd0a985eb1120103fe584"},
|
||||||
|
]
|
||||||
ghp-import = [
|
ghp-import = [
|
||||||
{file = "ghp-import-2.0.2.tar.gz", hash = "sha256:947b3771f11be850c852c64b561c600fdddf794bab363060854c1ee7ad05e071"},
|
{file = "ghp-import-2.0.2.tar.gz", hash = "sha256:947b3771f11be850c852c64b561c600fdddf794bab363060854c1ee7ad05e071"},
|
||||||
{file = "ghp_import-2.0.2-py3-none-any.whl", hash = "sha256:5f8962b30b20652cdffa9c5a9812f7de6bcb56ec475acac579807719bf242c46"},
|
{file = "ghp_import-2.0.2-py3-none-any.whl", hash = "sha256:5f8962b30b20652cdffa9c5a9812f7de6bcb56ec475acac579807719bf242c46"},
|
||||||
|
@ -50,6 +50,7 @@ pydantic-to-typescript = "^1.0.7"
|
|||||||
rich = "^10.7.0"
|
rich = "^10.7.0"
|
||||||
isort = "^5.9.3"
|
isort = "^5.9.3"
|
||||||
regex = "2021.9.30" # TODO: Remove during Upgrade -> https://github.com/psf/black/issues/2524
|
regex = "2021.9.30" # TODO: Remove during Upgrade -> https://github.com/psf/black/issues/2524
|
||||||
|
flake8-print = "^4.0.0"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core>=1.0.0"]
|
requires = ["poetry-core>=1.0.0"]
|
||||||
|
@ -47,7 +47,6 @@ def register_user(api_client, invite):
|
|||||||
registration.group_token = invite
|
registration.group_token = invite
|
||||||
|
|
||||||
response = api_client.post(Routes.register, json=registration.dict(by_alias=True))
|
response = api_client.post(Routes.register, json=registration.dict(by_alias=True))
|
||||||
print(response.json())
|
|
||||||
return registration, response
|
return registration, response
|
||||||
|
|
||||||
|
|
||||||
|
@ -28,8 +28,6 @@ def test_read_webhook(api_client: TestClient, unique_user: TestUser, webhook_dat
|
|||||||
|
|
||||||
webhook = response.json()
|
webhook = response.json()
|
||||||
|
|
||||||
print(webhook)
|
|
||||||
|
|
||||||
assert webhook["id"]
|
assert webhook["id"]
|
||||||
assert webhook["name"] == webhook_data["name"]
|
assert webhook["name"] == webhook_data["name"]
|
||||||
assert webhook["url"] == webhook_data["url"]
|
assert webhook["url"] == webhook_data["url"]
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
import pytest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from tests.utils.fixture_schemas import TestUser
|
||||||
|
|
||||||
|
|
||||||
|
class Routes:
|
||||||
|
base = "/api/recipes"
|
||||||
|
bulk = "/api/recipes/create-url/bulk"
|
||||||
|
|
||||||
|
def item(item_id: str) -> str:
|
||||||
|
return f"{Routes.base}/{item_id}"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip("Long Running Scraper")
|
||||||
|
def test_bulk_import(api_client: TestClient, unique_user: TestUser):
|
||||||
|
recipes = {
|
||||||
|
"imports": [
|
||||||
|
{"url": "https://www.bonappetit.com/recipe/caramel-crunch-chocolate-chunklet-cookies"},
|
||||||
|
{"url": "https://www.allrecipes.com/recipe/10813/best-chocolate-chip-cookies/"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
slugs = [
|
||||||
|
"caramel-crunch-chocolate-chunklet-cookies",
|
||||||
|
"best-chocolate-chip-cookies",
|
||||||
|
]
|
||||||
|
|
||||||
|
response = api_client.post(Routes.bulk, json=recipes, headers=unique_user.token)
|
||||||
|
|
||||||
|
assert response.status_code == 201
|
||||||
|
|
||||||
|
for slug in slugs:
|
||||||
|
response = api_client.get(Routes.item(slug), headers=unique_user.token)
|
||||||
|
assert response.status_code == 200
|
@ -73,5 +73,4 @@ def test_delete_food(api_client: TestClient, food: dict, unique_user: TestUser):
|
|||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
response = api_client.get(Routes.item(id), headers=unique_user.token)
|
response = api_client.get(Routes.item(id), headers=unique_user.token)
|
||||||
print(response.json())
|
|
||||||
assert response.status_code == 404
|
assert response.status_code == 404
|
||||||
|
Loading…
x
Reference in New Issue
Block a user