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
24 changed files with 295 additions and 65 deletions
|
@ -1,4 +1,6 @@
|
|||
import { BaseCRUDAPI } from "../_base";
|
||||
import { Category } from "./categories";
|
||||
import { Tag } from "./tags";
|
||||
import { Recipe, CreateRecipe } from "~/types/api-types/recipe";
|
||||
|
||||
const prefix = "/api";
|
||||
|
@ -8,6 +10,7 @@ const routes = {
|
|||
recipesBase: `${prefix}/recipes`,
|
||||
recipesTestScrapeUrl: `${prefix}/recipes/test-scrape-url`,
|
||||
recipesCreateUrl: `${prefix}/recipes/create-url`,
|
||||
recipesCreateUrlBulk: `${prefix}/recipes/create-url/bulk`,
|
||||
recipesCreateFromZip: `${prefix}/recipes/create-from-zip`,
|
||||
recipesCategory: `${prefix}/recipes/category`,
|
||||
recipesParseIngredient: `${prefix}/parser/ingredient`,
|
||||
|
@ -59,6 +62,16 @@ export interface ParsedIngredient {
|
|||
ingredient: Ingredient;
|
||||
}
|
||||
|
||||
export interface BulkCreateRecipe {
|
||||
url: string;
|
||||
categories: Category[];
|
||||
tags: Tag[];
|
||||
}
|
||||
|
||||
export interface BulkCreatePayload {
|
||||
imports: BulkCreateRecipe[];
|
||||
}
|
||||
|
||||
export class RecipeAPI extends BaseCRUDAPI<Recipe, CreateRecipe> {
|
||||
baseRoute: string = routes.recipesBase;
|
||||
itemRoute = routes.recipesRecipeSlug;
|
||||
|
@ -90,6 +103,10 @@ export class RecipeAPI extends BaseCRUDAPI<Recipe, CreateRecipe> {
|
|||
return await this.requests.post(routes.recipesCreateUrl, { url });
|
||||
}
|
||||
|
||||
async createManyByUrl(payload: BulkCreatePayload) {
|
||||
return await this.requests.post(routes.recipesCreateUrlBulk, payload);
|
||||
}
|
||||
|
||||
// Recipe Comments
|
||||
|
||||
// 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>
|
||||
<v-autocomplete
|
||||
v-model="selected"
|
||||
|
@ -14,12 +16,14 @@
|
|||
:solo="solo"
|
||||
:return-object="returnObject"
|
||||
:flat="flat"
|
||||
v-bind="$attrs"
|
||||
@input="emitChange"
|
||||
>
|
||||
<template #selection="data">
|
||||
<v-chip
|
||||
v-if="showSelected"
|
||||
:key="data.index"
|
||||
:small="dense"
|
||||
class="ma-1"
|
||||
:input-value="data.selected"
|
||||
close
|
||||
|
|
|
@ -26,9 +26,7 @@
|
|||
</v-card>
|
||||
|
||||
<div v-if="edit" class="d-flex justify-end">
|
||||
<v-btn class="mt-1" color="secondary" dark @click="addNote">
|
||||
<v-icon>{{ $globals.icons.create }}</v-icon>
|
||||
</v-btn>
|
||||
<BaseButton class="ml-auto my-2" @click="addNote"> {{ $t("general.new") }}</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -92,7 +92,7 @@
|
|||
@end="onMoveCallback"
|
||||
>
|
||||
<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">
|
||||
<RecipeCardImage v-if="mealplan.recipe" tiny icon-size="25" :slug="mealplan.recipe.slug" />
|
||||
<v-icon v-else>
|
||||
|
@ -108,8 +108,8 @@
|
|||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-divider class="mx-2"></v-divider>
|
||||
<v-card-actions>
|
||||
<v-divider v-if="edit" class="mx-2"></v-divider>
|
||||
<v-card-actions v-if="edit">
|
||||
<v-btn color="error" icon @click="actions.deleteOne(mealplan.id)">
|
||||
<v-icon>{{ $globals.icons.delete }}</v-icon>
|
||||
</v-btn>
|
||||
|
|
|
@ -110,8 +110,8 @@
|
|||
/>
|
||||
</draggable>
|
||||
<div class="d-flex justify-end mt-2">
|
||||
<RecipeIngredientParserMenu class="mr-1" :slug="recipe.slug" :ingredients="recipe.recipeIngredient" />
|
||||
<RecipeDialogBulkAdd class="mr-1" @bulk-data="addIngredient" />
|
||||
<RecipeIngredientParserMenu :slug="recipe.slug" :ingredients="recipe.recipeIngredient" />
|
||||
<RecipeDialogBulkAdd class="ml-1 mr-1" @bulk-data="addIngredient" />
|
||||
<BaseButton @click="addIngredient"> {{ $t("general.new") }} </BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -228,6 +228,30 @@
|
|||
<RecipeNotes v-model="recipe.notes" :edit="form" />
|
||||
</v-col>
|
||||
</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>
|
||||
</div>
|
||||
</v-card>
|
||||
|
|
|
@ -142,7 +142,7 @@
|
|||
<v-tab-item value="debug" eager>
|
||||
<v-form ref="domUrlForm" @submit.prevent="debugUrl(recipeUrl)">
|
||||
<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>
|
||||
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
|
||||
|
@ -174,6 +174,17 @@
|
|||
</v-card>
|
||||
</v-form>
|
||||
</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>
|
||||
</section>
|
||||
<v-divider class="mt-5"></v-divider>
|
||||
|
@ -195,6 +206,74 @@
|
|||
height="700px"
|
||||
/>
|
||||
</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>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -204,10 +283,12 @@ import { defineComponent, reactive, toRefs, ref, useRouter, useContext } from "@
|
|||
// @ts-ignore No Types for v-jsoneditor
|
||||
import VJsoneditor from "v-jsoneditor";
|
||||
import { useApiSingleton } from "~/composables/use-api";
|
||||
import RecipeCategoryTagSelector from "~/components/Domain/Recipe/RecipeCategoryTagSelector.vue";
|
||||
import { validators } from "~/composables/use-validators";
|
||||
import { Recipe } from "~/types/api-types/recipe";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
export default defineComponent({
|
||||
components: { VJsoneditor },
|
||||
components: { VJsoneditor, RecipeCategoryTagSelector },
|
||||
setup() {
|
||||
const state = reactive({
|
||||
error: false,
|
||||
|
@ -233,6 +314,11 @@ export default defineComponent({
|
|||
text: "Import with .zip",
|
||||
value: "zip",
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.link,
|
||||
text: "Bulk URL Import",
|
||||
value: "bulk",
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.robot,
|
||||
text: "Debug Scraper",
|
||||
|
@ -249,7 +335,6 @@ export default defineComponent({
|
|||
state.loading = false;
|
||||
return;
|
||||
}
|
||||
console.log(response);
|
||||
router.push(`/recipe/${response.data}`);
|
||||
}
|
||||
|
||||
|
@ -300,7 +385,6 @@ export default defineComponent({
|
|||
return;
|
||||
}
|
||||
const { response } = await api.recipes.createOne({ name });
|
||||
console.log("Create By Name Func", response);
|
||||
handleResponse(response);
|
||||
}
|
||||
|
||||
|
@ -318,11 +402,31 @@ export default defineComponent({
|
|||
formData.append(newRecipeZipFileName, newRecipeZip.value);
|
||||
|
||||
const { response } = await api.upload.file("/api/recipes/create-from-zip", formData);
|
||||
console.log(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 {
|
||||
bulkCreate,
|
||||
bulkUrls,
|
||||
debugTreeView,
|
||||
tabs,
|
||||
domCreateByName,
|
||||
|
|
|
@ -21,7 +21,7 @@ import { useLazyRecipes } from "~/composables/use-recipes";
|
|||
export default defineComponent({
|
||||
components: { RecipeCardSection },
|
||||
setup() {
|
||||
const start = ref(1);
|
||||
const start = ref(0);
|
||||
const limit = ref(30);
|
||||
const increment = ref(30);
|
||||
const ready = ref(false);
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from mealie.core.root_logger import get_logger
|
||||
from mealie.db.data_access_layer.access_model_factory import Database
|
||||
|
||||
CWD = Path(__file__).parent
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def get_default_foods():
|
||||
|
@ -23,10 +25,10 @@ def default_recipe_unit_init(db: Database) -> None:
|
|||
try:
|
||||
db.ingredient_units.create(unit)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
logger.error(e)
|
||||
|
||||
for food in get_default_foods():
|
||||
try:
|
||||
db.ingredient_foods.create(food)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
logger.error(e)
|
||||
|
|
|
@ -33,7 +33,6 @@ async def check_email_config():
|
|||
|
||||
@router.post("", response_model=EmailSuccess)
|
||||
async def send_test_email(data: EmailTest):
|
||||
print(data)
|
||||
service = EmailService()
|
||||
status = False
|
||||
error = None
|
||||
|
|
|
@ -8,17 +8,14 @@ from sqlalchemy.orm.session import Session
|
|||
from mealie.db.database import get_database
|
||||
from mealie.db.db_setup import generate_session
|
||||
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
|
||||
|
||||
user_router = UserAPIRouter()
|
||||
|
||||
|
||||
@user_router.post("/{slug}/image")
|
||||
def scrape_image_url(
|
||||
slug: str,
|
||||
url: CreateRecipeByURL,
|
||||
):
|
||||
def scrape_image_url(slug: str, url: CreateRecipeByUrl):
|
||||
""" Removes an existing image and replaces it with the incoming file. """
|
||||
|
||||
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.db_setup import generate_session
|
||||
from mealie.routes.routers import UserAPIRouter
|
||||
from mealie.schema.recipe import CreateRecipeByURL, Recipe, RecipeImageTypes
|
||||
from mealie.schema.recipe.recipe import CreateRecipe, RecipeSummary
|
||||
from mealie.schema.recipe import CreateRecipeByUrl, Recipe, RecipeImageTypes
|
||||
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.scraper.scraper import create_from_url, scrape_from_url
|
||||
from mealie.services.server_tasks.background_executory import BackgroundExecutor
|
||||
|
||||
user_router = UserAPIRouter()
|
||||
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)
|
||||
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 """
|
||||
|
||||
recipe = create_from_url(url.url)
|
||||
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")
|
||||
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
|
||||
scraped_data = scrape_from_url(url.url)
|
||||
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 """
|
||||
db = get_database(session)
|
||||
|
||||
recipe: Recipe = db.recipes.get(slug)
|
||||
|
||||
image_asset = recipe.image_dir.joinpath(RecipeImageTypes.original.value)
|
||||
|
||||
with ZipFile(temp_path, "w") as myzip:
|
||||
myzip.writestr(f"{slug}.json", recipe.json())
|
||||
|
||||
|
|
|
@ -25,7 +25,6 @@ class CreatePlanEntry(CamelModel):
|
|||
@validator("recipe_id", always=True)
|
||||
@classmethod
|
||||
def id_or_title(cls, value, values):
|
||||
print(value, values)
|
||||
if bool(value) is False and bool(values["title"]) is False:
|
||||
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()
|
||||
|
||||
|
||||
class CreateRecipeByURL(BaseModel):
|
||||
url: str
|
||||
|
||||
class Config:
|
||||
schema_extra = {"example": {"url": "https://myfavoriterecipes.com/recipes"}}
|
||||
|
||||
|
||||
class CreateRecipe(CamelModel):
|
||||
name: str
|
||||
|
||||
|
||||
class RecipeTag(CamelModel):
|
||||
name: str
|
||||
slug: str
|
||||
|
@ -44,6 +33,27 @@ class RecipeCategory(RecipeTag):
|
|||
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):
|
||||
id: Optional[int]
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ from pydantic import Field
|
|||
class ServerTaskNames(str, enum.Enum):
|
||||
default = "Background Task"
|
||||
backup_task = "Database Backup"
|
||||
bulk_recipe_import = "Bulk Recipe Import"
|
||||
|
||||
|
||||
class ServerTaskStatus(str, enum.Enum):
|
||||
|
|
|
@ -37,6 +37,7 @@ class CrudHttpMixins(Generic[C, R, U], ABC):
|
|||
self.item = self.dal.create(data)
|
||||
except Exception as ex:
|
||||
logger.exception(ex)
|
||||
self.session.rollback()
|
||||
|
||||
msg = default_msg
|
||||
if exception_msgs:
|
||||
|
|
|
@ -73,14 +73,3 @@ class EmailService(BaseService):
|
|||
button_text="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:
|
||||
|
||||
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)
|
||||
|
||||
return string
|
||||
|
|
|
@ -43,13 +43,9 @@ def clean_string(text: str) -> str:
|
|||
if isinstance(text, list):
|
||||
text = text[0]
|
||||
|
||||
print(type(text))
|
||||
|
||||
if text == "" or text is None:
|
||||
return ""
|
||||
|
||||
print(text)
|
||||
|
||||
cleaned_text = html.unescape(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:
|
||||
return None
|
||||
elif isinstance(time_entry, timedelta):
|
||||
pretty_print_timedelta(time_entry)
|
||||
return pretty_print_timedelta(time_entry)
|
||||
elif isinstance(time_entry, datetime):
|
||||
print(time_entry)
|
||||
pass
|
||||
# print(time_entry)
|
||||
elif isinstance(time_entry, str):
|
||||
try:
|
||||
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"
|
||||
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]]
|
||||
name = "ghp-import"
|
||||
version = "2.0.2"
|
||||
|
@ -1381,7 +1394,7 @@ pgsql = ["psycopg2-binary"]
|
|||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.9"
|
||||
content-hash = "89271346f576de3d209ae69639ab7227c03bb8512a1671905a48407d76371ba9"
|
||||
content-hash = "31d3ee104998ad61b18322584c0cc84de32dbad0dc7657c9f7b7ae8214dae9c3"
|
||||
|
||||
[metadata.files]
|
||||
aiofiles = [
|
||||
|
@ -1619,6 +1632,10 @@ flake8 = [
|
|||
{file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"},
|
||||
{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 = [
|
||||
{file = "ghp-import-2.0.2.tar.gz", hash = "sha256:947b3771f11be850c852c64b561c600fdddf794bab363060854c1ee7ad05e071"},
|
||||
{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"
|
||||
isort = "^5.9.3"
|
||||
regex = "2021.9.30" # TODO: Remove during Upgrade -> https://github.com/psf/black/issues/2524
|
||||
flake8-print = "^4.0.0"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
|
|
|
@ -47,7 +47,6 @@ def register_user(api_client, invite):
|
|||
registration.group_token = invite
|
||||
|
||||
response = api_client.post(Routes.register, json=registration.dict(by_alias=True))
|
||||
print(response.json())
|
||||
return registration, response
|
||||
|
||||
|
||||
|
|
|
@ -28,8 +28,6 @@ def test_read_webhook(api_client: TestClient, unique_user: TestUser, webhook_dat
|
|||
|
||||
webhook = response.json()
|
||||
|
||||
print(webhook)
|
||||
|
||||
assert webhook["id"]
|
||||
assert webhook["name"] == webhook_data["name"]
|
||||
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
|
||||
|
||||
response = api_client.get(Routes.item(id), headers=unique_user.token)
|
||||
print(response.json())
|
||||
assert response.status_code == 404
|
||||
|
|
Loading…
Reference in a new issue