migration changes
@ -30,19 +30,6 @@ Feature placement is not set in stone. This is much more of a guideline than any
|
||||
|
||||
|
||||
### Backend
|
||||
|
||||
## v0.1.0 - Initial Release
|
||||
|
||||
## Frontend Tasks
|
||||
- [ ] General
|
||||
* [ ] Recipe Category Handling
|
||||
- [x] Meal Plan
|
||||
* [ ] Include Lunch / Dinner / Breaksfast Categories Option
|
||||
|
||||
### Backend Tasks
|
||||
- [ ] Backup Options
|
||||
* [ ] Force Update
|
||||
* [ ] Rebuild
|
||||
- [ ] Recipe Data
|
||||
* [ ] Better Scraper
|
||||
* [ ] Image Minification
|
||||
@ -50,6 +37,11 @@ Feature placement is not set in stone. This is much more of a guideline than any
|
||||
- [ ] Category Management
|
||||
* [ ] Lunch / Dinner / Breakfast <- Meal Generation
|
||||
* [ ] Dessert / Side / Appetizer / Bread / Drinks /
|
||||
- [ ] Backup Options
|
||||
* [ ] Force Update
|
||||
* [ ] Rebuild
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -3,6 +3,7 @@ import recipe from "./api/recipe";
|
||||
import mealplan from "./api/mealplan";
|
||||
import settings from "./api/settings";
|
||||
import themes from "./api/themes";
|
||||
import migration from "./api/migration";
|
||||
|
||||
// import api from "../api";
|
||||
|
||||
@ -12,4 +13,5 @@ export default {
|
||||
mealPlans: mealplan,
|
||||
settings: settings,
|
||||
themes: themes,
|
||||
migrations: migration,
|
||||
};
|
||||
|
@ -2,13 +2,17 @@ const baseURL = "/api/";
|
||||
import axios from "axios";
|
||||
import store from "../store/store";
|
||||
|
||||
// look for data.snackbar in response
|
||||
function processResponse(response) {
|
||||
if (("data" in response) & ("snackbar" in response.data)) {
|
||||
try {
|
||||
store.commit("setSnackBar", {
|
||||
text: response.data.snackbar.text,
|
||||
type: response.data.snackbar.type,
|
||||
});
|
||||
} else return;
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const apiReq = {
|
||||
|
19
frontend/src/api/migration.js
Normal file
@ -0,0 +1,19 @@
|
||||
import { baseURL } from "./api-utils";
|
||||
import { apiReq } from "./api-utils";
|
||||
import { store } from "../store/store";
|
||||
|
||||
const migrationBase = baseURL + "migration/";
|
||||
|
||||
const migrationURLs = {
|
||||
chowdownURL: migrationBase + "chowdown/repo/",
|
||||
};
|
||||
|
||||
export default {
|
||||
async migrateChowdown(repoURL) {
|
||||
let postBody = { url: repoURL };
|
||||
let response = await apiReq.post(migrationURLs.chowdownURL, postBody);
|
||||
console.log(response);
|
||||
store.dispatch("requestRecentRecipes");
|
||||
return response.data;
|
||||
},
|
||||
};
|
@ -20,7 +20,10 @@ const recipeURLs = {
|
||||
|
||||
export default {
|
||||
async createByURL(recipeURL) {
|
||||
let response = await apiReq.post(recipeURLs.createByURL, { url: recipeURL });
|
||||
let response = await apiReq.post(recipeURLs.createByURL, {
|
||||
url: recipeURL,
|
||||
});
|
||||
console.log(response);
|
||||
let recipeSlug = response.data;
|
||||
store.dispatch("requestRecentRecipes");
|
||||
router.push(`/recipe/${recipeSlug}`);
|
||||
|
@ -3,6 +3,7 @@
|
||||
<Theme />
|
||||
<Backup />
|
||||
<Webhooks />
|
||||
<Migration />
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
@ -10,11 +11,13 @@
|
||||
import Backup from "./Backup";
|
||||
import Webhooks from "./Webhooks";
|
||||
import Theme from "./Theme";
|
||||
import Migration from "./Migration";
|
||||
export default {
|
||||
components: {
|
||||
Backup,
|
||||
Webhooks,
|
||||
Theme,
|
||||
Migration,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
69
frontend/src/components/Admin/Migration.vue
Normal file
@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<v-card :loading="loading">
|
||||
<v-card-title class="secondary white--text mt-1">
|
||||
Recipe Migration
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<p>
|
||||
Currently Chowdown via public Repo URL is the only supported type of
|
||||
migration
|
||||
</p>
|
||||
<v-form>
|
||||
<v-row dense align="center">
|
||||
<v-col cols="12" md="5" sm="5">
|
||||
<v-text-field v-model="repo" label="Chowdown Repo URL">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4" sm="5">
|
||||
<v-btn text color="info" @click="importRepo"> Migrate </v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
<v-alert v-if="failedRecipes[1]" outlined dense type="error">
|
||||
<h4>Failed Recipes</h4>
|
||||
<v-list dense>
|
||||
<v-list-item v-for="fail in this.failedRecipes" :key="fail">
|
||||
{{ fail }}
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-alert>
|
||||
<v-alert v-if="failedImages[1]" outlined dense type="error">
|
||||
<h4>Failed Images</h4>
|
||||
<v-list dense>
|
||||
<v-list-item v-for="fail in this.failedImages" :key="fail">
|
||||
{{ fail }}
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from "../../api";
|
||||
// import TimePicker from "./Webhooks/TimePicker";
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
processRan: false,
|
||||
loading: false,
|
||||
failedImages: [],
|
||||
failedRecipes: [],
|
||||
repo: "",
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async importRepo() {
|
||||
this.loading = true;
|
||||
let response = await api.migrations.migrateChowdown(this.repo);
|
||||
this.failedImages = response.failedImages;
|
||||
this.failedRecipes = response.failedRecipes;
|
||||
this.loading = false;
|
||||
this.processRan = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
@ -7,6 +7,7 @@ from fastapi.staticfiles import StaticFiles
|
||||
from routes import (
|
||||
backup_routes,
|
||||
meal_routes,
|
||||
migration_routes,
|
||||
recipe_routes,
|
||||
setting_routes,
|
||||
static_routes,
|
||||
@ -31,6 +32,7 @@ app.include_router(meal_routes.router)
|
||||
app.include_router(setting_routes.router)
|
||||
app.include_router(backup_routes.router)
|
||||
app.include_router(user_routes.router)
|
||||
app.include_router(migration_routes.router)
|
||||
|
||||
# API 404 Catch all CALL AFTER ROUTERS
|
||||
@app.get("/api/{full_path:path}", status_code=404, include_in_schema=False)
|
||||
|
BIN
mealie/data/img/banana-bread.jpg
Normal file
After Width: | Height: | Size: 2.0 MiB |
BIN
mealie/data/img/broccoli-beer-cheese-soup.jpg
Normal file
After Width: | Height: | Size: 519 KiB |
BIN
mealie/data/img/cauliflower-cacciatore.jpg
Normal file
After Width: | Height: | Size: 1.1 MiB |
Before Width: | Height: | Size: 363 KiB |
BIN
mealie/data/img/crispy-carrots.jpg
Normal file
After Width: | Height: | Size: 794 KiB |
BIN
mealie/data/img/crockpot-buffalo-chicken.jpg
Normal file
After Width: | Height: | Size: 572 KiB |
BIN
mealie/data/img/downtown-marinade.jpg
Normal file
After Width: | Height: | Size: 528 KiB |
BIN
mealie/data/img/falafel-hummus-plate.jpg
Normal file
After Width: | Height: | Size: 452 KiB |
BIN
mealie/data/img/french-toast.jpg
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
mealie/data/img/graham-cracker-crust.jpg
Normal file
After Width: | Height: | Size: 997 KiB |
BIN
mealie/data/img/green-chile-stew.jpg
Normal file
After Width: | Height: | Size: 556 KiB |
BIN
mealie/data/img/green-spaghetti.jpg
Normal file
After Width: | Height: | Size: 317 KiB |
BIN
mealie/data/img/jalapeno-cornbread.jpg
Normal file
After Width: | Height: | Size: 1.3 MiB |
BIN
mealie/data/img/mississippi-pot-roast.jpg
Normal file
After Width: | Height: | Size: 622 KiB |
BIN
mealie/data/img/mongolian-beef.jpg
Normal file
After Width: | Height: | Size: 812 KiB |
BIN
mealie/data/img/mushroom-risotto.jpg
Normal file
After Width: | Height: | Size: 650 KiB |
BIN
mealie/data/img/new-york-strip.jpg
Normal file
After Width: | Height: | Size: 2.2 MiB |
BIN
mealie/data/img/nilla-wafer-french-toast.jpg
Normal file
After Width: | Height: | Size: 889 KiB |
BIN
mealie/data/img/one-minute-muffin.jpg
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
mealie/data/img/pace-pork.jpg
Normal file
After Width: | Height: | Size: 602 KiB |
Before Width: | Height: | Size: 74 KiB |
BIN
mealie/data/img/pork-steaks.jpg
Normal file
After Width: | Height: | Size: 788 KiB |
BIN
mealie/data/img/red-berry-tart.jpg
Normal file
After Width: | Height: | Size: 137 KiB |
BIN
mealie/data/img/red-berry-topping.jpg
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
mealie/data/img/red-lentil-salad.jpg
Normal file
After Width: | Height: | Size: 312 KiB |
BIN
mealie/data/img/roasted-brussels-sprouts.jpg
Normal file
After Width: | Height: | Size: 664 KiB |
BIN
mealie/data/img/roasted-okra.jpg
Normal file
After Width: | Height: | Size: 985 KiB |
BIN
mealie/data/img/salt-vinegar-potatoes.jpg
Normal file
After Width: | Height: | Size: 154 KiB |
BIN
mealie/data/img/smashed-carrots.jpg
Normal file
After Width: | Height: | Size: 413 KiB |
Before Width: | Height: | Size: 7.4 MiB |
Before Width: | Height: | Size: 115 KiB |
BIN
mealie/data/img/stuffed-peppers.jpg
Normal file
After Width: | Height: | Size: 1.0 MiB |
BIN
mealie/data/img/sweet-potato-cakes-with-poached-eggs.jpg
Normal file
After Width: | Height: | Size: 324 KiB |
BIN
mealie/data/img/tequila-beer-and-citrus-cocktail.jpg
Normal file
After Width: | Height: | Size: 184 KiB |
BIN
mealie/data/img/vanilla-custard.jpg
Normal file
After Width: | Height: | Size: 1.7 MiB |
24
mealie/routes/migration_routes.py
Normal file
@ -0,0 +1,24 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from models.backup_models import BackupJob
|
||||
from services.migrations.chowdown import chowdown_migrate as chowdow_migrate
|
||||
from utils.snackbar import SnackResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/api/migration/chowdown/repo/", tags=["Migration"])
|
||||
async def import_chowdown_recipes(repo: dict):
|
||||
""" Import Chowsdown Recipes from Repo URL """
|
||||
try:
|
||||
report = chowdow_migrate(repo.get("url"))
|
||||
return SnackResponse.success(
|
||||
"Recipes Imported from Git Repo, see report for failures.",
|
||||
additional_data=report,
|
||||
)
|
||||
except:
|
||||
return HTTPException(
|
||||
status_code=400,
|
||||
detail=SnackResponse.error(
|
||||
"Unable to Migrate Recipes. See Log for Details"
|
||||
),
|
||||
)
|
@ -24,6 +24,7 @@ async def get_all_recipes(
|
||||
async def get_recipe(recipe_slug: str):
|
||||
""" Takes in a recipe slug, returns all data for a recipe """
|
||||
recipe = Recipe.get_by_slug(recipe_slug)
|
||||
|
||||
return recipe
|
||||
|
||||
|
||||
|
@ -1,26 +1,106 @@
|
||||
import collections
|
||||
import shutil
|
||||
from os.path import join
|
||||
from pathlib import Path
|
||||
from pprint import pprint
|
||||
|
||||
import pdfkit
|
||||
import requests
|
||||
import git
|
||||
import yaml
|
||||
from git.util import join_path
|
||||
|
||||
from db.mongo_setup import global_init
|
||||
from services.image_services import IMG_DIR
|
||||
from services.recipe_services import Recipe
|
||||
|
||||
Cron = collections.namedtuple("Cron", "hours minutes")
|
||||
try:
|
||||
from yaml import CDumper as Dumper
|
||||
from yaml import CLoader as Loader
|
||||
except ImportError:
|
||||
from yaml import Dumper, Loader
|
||||
|
||||
CWD = Path(__file__).parent
|
||||
|
||||
file = f"/home/hayden/Projects/mealie-fastAPI/mealie/chowdown.md"
|
||||
|
||||
repo = "https://github.com/clarklab/chowdown"
|
||||
|
||||
|
||||
# def cron_parser(time_str: str) -> Cron:
|
||||
# time = time_str.split(":")
|
||||
# cron = Cron(hours=time[0], minutes=time[1])
|
||||
def pull_repo(repo):
|
||||
dest_dir = CWD.joinpath("data/temp/migration/git_pull")
|
||||
if dest_dir.exists():
|
||||
shutil.rmtree(dest_dir)
|
||||
dest_dir.mkdir(parents=True, exist_ok=True)
|
||||
git.Git(dest_dir).clone(repo)
|
||||
|
||||
# print(cron.hours, cron.minutes)
|
||||
repo_name = repo.split("/")[-1]
|
||||
recipe_dir = dest_dir.joinpath(repo_name, "_recipes")
|
||||
image_dir = dest_dir.joinpath(repo_name, "images")
|
||||
|
||||
return recipe_dir, image_dir
|
||||
|
||||
|
||||
# cron_parser("12:45")
|
||||
def read_chowdown_file(recipe_file: Path) -> Recipe:
|
||||
with open(recipe_file, "r") as stream:
|
||||
recipe_description: str = str
|
||||
recipe_data: dict = {}
|
||||
try:
|
||||
for x, item in enumerate(yaml.load_all(stream, Loader=Loader)):
|
||||
print(item)
|
||||
if x == 0:
|
||||
recipe_data = item
|
||||
|
||||
URL = "https://home.homelabhome.com/api/webhook/test_msg"
|
||||
from services.meal_services import MealPlan
|
||||
elif x == 1:
|
||||
recipe_description = str(item)
|
||||
|
||||
global_init()
|
||||
todays_meal = MealPlan.today()
|
||||
except yaml.YAMLError as exc:
|
||||
print(exc)
|
||||
return
|
||||
|
||||
requests.post(URL, todays_meal)
|
||||
reformat_data = {
|
||||
"name": recipe_data.get("title"),
|
||||
"description": recipe_description,
|
||||
"image": recipe_data.get("image", ""),
|
||||
"recipeIngredient": recipe_data.get("ingredients"),
|
||||
"recipeInstructions": recipe_data.get("directions"),
|
||||
"tags": recipe_data.get("tags").split(","),
|
||||
}
|
||||
|
||||
pprint(reformat_data)
|
||||
new_recipe = Recipe(**reformat_data)
|
||||
|
||||
reformated_list = []
|
||||
for instruction in new_recipe.recipeInstructions:
|
||||
reformated_list.append({"text": instruction})
|
||||
|
||||
new_recipe.recipeInstructions = reformated_list
|
||||
|
||||
return new_recipe
|
||||
|
||||
|
||||
def main():
|
||||
from db.mongo_setup import global_init
|
||||
|
||||
global_init()
|
||||
recipe_dir, image_dir = pull_repo(repo)
|
||||
|
||||
failed_images = []
|
||||
for image in image_dir.iterdir():
|
||||
try:
|
||||
image.rename(IMG_DIR.joinpath(image.name))
|
||||
except:
|
||||
failed_images.append(image.name)
|
||||
|
||||
failed_recipes = []
|
||||
for recipe in recipe_dir.glob("*.md"):
|
||||
print(recipe.name)
|
||||
try:
|
||||
new_recipe = read_chowdown_file(recipe)
|
||||
new_recipe.save_to_db()
|
||||
|
||||
except:
|
||||
failed_recipes.append(recipe.name)
|
||||
|
||||
report = {"failedImages": failed_images, "failedRecipes": failed_recipes}
|
||||
|
||||
print(report)
|
||||
|
||||
|
||||
main()
|
||||
|
91
mealie/services/migrations/chowdown.py
Normal file
@ -0,0 +1,91 @@
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
import git
|
||||
import yaml
|
||||
from services.image_services import IMG_DIR
|
||||
from services.recipe_services import Recipe
|
||||
|
||||
try:
|
||||
from yaml import CLoader as Loader
|
||||
except ImportError:
|
||||
from yaml import Loader
|
||||
|
||||
TEMP_DIR = Path(__file__).parent.parent.parent.joinpath("temp")
|
||||
|
||||
|
||||
def pull_repo(repo):
|
||||
dest_dir = TEMP_DIR.joinpath("/migration/git_pull")
|
||||
if dest_dir.exists():
|
||||
shutil.rmtree(dest_dir)
|
||||
dest_dir.mkdir(parents=True, exist_ok=True)
|
||||
git.Git(dest_dir).clone(repo)
|
||||
|
||||
repo_name = repo.split("/")[-1]
|
||||
recipe_dir = dest_dir.joinpath(repo_name, "_recipes")
|
||||
image_dir = dest_dir.joinpath(repo_name, "images")
|
||||
|
||||
return recipe_dir, image_dir
|
||||
|
||||
|
||||
def read_chowdown_file(recipe_file: Path) -> Recipe:
|
||||
with open(recipe_file, "r") as stream:
|
||||
recipe_description: str = str
|
||||
recipe_data: dict = {}
|
||||
try:
|
||||
for x, item in enumerate(yaml.load_all(stream, Loader=Loader)):
|
||||
print(item)
|
||||
if x == 0:
|
||||
recipe_data = item
|
||||
|
||||
elif x == 1:
|
||||
recipe_description = str(item)
|
||||
|
||||
except yaml.YAMLError as exc:
|
||||
print(exc)
|
||||
return
|
||||
|
||||
reformat_data = {
|
||||
"name": recipe_data.get("title"),
|
||||
"description": recipe_description,
|
||||
"image": recipe_data.get("image", ""),
|
||||
"recipeIngredient": recipe_data.get("ingredients"),
|
||||
"recipeInstructions": recipe_data.get("directions"),
|
||||
"tags": recipe_data.get("tags").split(","),
|
||||
}
|
||||
|
||||
new_recipe = Recipe(**reformat_data)
|
||||
|
||||
reformated_list = []
|
||||
for instruction in new_recipe.recipeInstructions:
|
||||
reformated_list.append({"text": instruction})
|
||||
|
||||
new_recipe.recipeInstructions = reformated_list
|
||||
|
||||
return new_recipe
|
||||
|
||||
|
||||
def chowdown_migrate(repo):
|
||||
recipe_dir, image_dir = pull_repo(repo)
|
||||
|
||||
failed_images = []
|
||||
for image in image_dir.iterdir():
|
||||
try:
|
||||
shutil.copy(image, IMG_DIR.joinpath(image.name))
|
||||
except:
|
||||
failed_images.append(image.name)
|
||||
|
||||
failed_recipes = []
|
||||
for recipe in recipe_dir.glob("*.md"):
|
||||
print(recipe.name)
|
||||
try:
|
||||
new_recipe = read_chowdown_file(recipe)
|
||||
new_recipe.save_to_db()
|
||||
|
||||
except:
|
||||
failed_recipes.append(recipe.name)
|
||||
|
||||
|
||||
report = {"failedImages": failed_images, "failedRecipes": failed_recipes}
|
||||
|
||||
return report
|
@ -1,32 +1,39 @@
|
||||
class SnackResponse:
|
||||
@staticmethod
|
||||
def _create_response(message: str, type: str) -> dict:
|
||||
return {"snackbar": {"text": message, "type": type}}
|
||||
def _create_response(message: str, type: str, additional_data: dict = None) -> dict:
|
||||
|
||||
snackbar = {"snackbar": {"text": message, "type": type}}
|
||||
|
||||
if additional_data:
|
||||
snackbar.update(additional_data)
|
||||
print(snackbar)
|
||||
|
||||
return snackbar
|
||||
|
||||
@staticmethod
|
||||
def primary(message: str) -> dict:
|
||||
return SnackResponse._create_response(message, "primary")
|
||||
def primary(message: str, additional_data: dict = None) -> dict:
|
||||
return SnackResponse._create_response(message, "primary", additional_data)
|
||||
|
||||
@staticmethod
|
||||
def accent(message: str) -> dict:
|
||||
return SnackResponse._create_response(message, "accent")
|
||||
def accent(message: str, additional_data: dict = None) -> dict:
|
||||
return SnackResponse._create_response(message, "accent", additional_data)
|
||||
|
||||
@staticmethod
|
||||
def secondary(message: str) -> dict:
|
||||
return SnackResponse._create_response(message, "secondary")
|
||||
def secondary(message: str, additional_data: dict = None) -> dict:
|
||||
return SnackResponse._create_response(message, "secondary", additional_data)
|
||||
|
||||
@staticmethod
|
||||
def success(message: str) -> dict:
|
||||
return SnackResponse._create_response(message, "success")
|
||||
def success(message: str, additional_data: dict = None) -> dict:
|
||||
return SnackResponse._create_response(message, "success", additional_data)
|
||||
|
||||
@staticmethod
|
||||
def info(message: str) -> dict:
|
||||
return SnackResponse._create_response(message, "info")
|
||||
def info(message: str, additional_data: dict = None) -> dict:
|
||||
return SnackResponse._create_response(message, "info", additional_data)
|
||||
|
||||
@staticmethod
|
||||
def warning(message: str) -> dict:
|
||||
return SnackResponse._create_response(message, "warning")
|
||||
def warning(message: str, additional_data: dict = None) -> dict:
|
||||
return SnackResponse._create_response(message, "warning", additional_data)
|
||||
|
||||
@staticmethod
|
||||
def error(message: str) -> dict:
|
||||
return SnackResponse._create_response(message, "error")
|
||||
def error(message: str, additional_data: dict = None) -> dict:
|
||||
return SnackResponse._create_response(message, "error", additional_data)
|
||||
|
@ -1,5 +0,0 @@
|
||||
const config = (() => {
|
||||
return {
|
||||
"VUE_APP_API_BASE_URL": "REPLACE_ME",
|
||||
};
|
||||
})();
|
@ -1 +0,0 @@
|
||||
.card-btn{margin-top:-10px}.disabled-card{opacity:1%}.img-input{position:absolute;bottom:0}
|
Before Width: | Height: | Size: 4.2 KiB |
@ -1 +0,0 @@
|
||||
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/static/favicon.ico"><title>frontend</title><link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css"><link href="/static/css/app.e50b23f1.css" rel="preload" as="style"><link href="/static/css/chunk-vendors.e0416589.css" rel="preload" as="style"><link href="/static/js/app.b457c0af.js" rel="preload" as="script"><link href="/static/js/chunk-vendors.a435ad20.js" rel="preload" as="script"><link href="/static/css/chunk-vendors.e0416589.css" rel="stylesheet"><link href="/static/css/app.e50b23f1.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but frontend doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="/static/js/chunk-vendors.a435ad20.js"></script><script src="/static/js/app.b457c0af.js"></script></body></html>
|
@ -16,6 +16,10 @@ dnspython==2.0.0
|
||||
email-validator==1.1.1
|
||||
extruct==0.10.0
|
||||
fastapi==0.61.1
|
||||
fastapi-login==1.5.1
|
||||
future==0.18.2
|
||||
gitdb==4.0.5
|
||||
GitPython==3.1.11
|
||||
graphene==2.1.8
|
||||
graphql-core==2.3.2
|
||||
graphql-relay==2.0.1
|
||||
@ -28,18 +32,31 @@ isodate==0.6.0
|
||||
isort==5.4.2
|
||||
itsdangerous==1.1.0
|
||||
Jinja2==2.11.2
|
||||
joblib==1.0.0
|
||||
jstyleson==0.0.2
|
||||
lazy-object-proxy==1.4.3
|
||||
livereload==2.6.3
|
||||
lunr==0.5.8
|
||||
lxml==4.5.2
|
||||
Markdown==3.3.3
|
||||
MarkupSafe==1.1.1
|
||||
mccabe==0.6.1
|
||||
mf2py==1.1.2
|
||||
mkdocs==1.1.2
|
||||
mkdocs-material==6.1.7
|
||||
mkdocs-material-extensions==1.0.1
|
||||
mongoengine==0.21.0
|
||||
mypy-extensions==0.4.3
|
||||
nltk==3.5
|
||||
passlib==1.7.4
|
||||
pathspec==0.8.0
|
||||
pdfkit==0.6.1
|
||||
promise==2.3
|
||||
pydantic==1.6.1
|
||||
Pygments==2.7.3
|
||||
PyJWT==1.7.1
|
||||
pylint==2.6.0
|
||||
pymdown-extensions==8.0.1
|
||||
pymongo==3.11.1
|
||||
pyparsing==2.4.7
|
||||
python-dateutil==2.8.1
|
||||
@ -55,10 +72,13 @@ requests==2.24.0
|
||||
Rx==1.6.1
|
||||
scrape-schema-recipe==0.1.1
|
||||
six==1.15.0
|
||||
smmap==3.0.4
|
||||
soupsieve==2.0.1
|
||||
starlette==0.13.6
|
||||
text-unidecode==1.3
|
||||
toml==0.10.1
|
||||
tornado==6.1
|
||||
tqdm==4.54.1
|
||||
typed-ast==1.4.1
|
||||
typing-extensions==3.7.4.3
|
||||
tzlocal==2.1
|
||||
|
16
scratch.json
@ -1,12 +1,4 @@
|
||||
{'name': 'Marranitos Enfiestados', 'description': 'Rick Martinez’s take on the popular Mexican pig-shaped cookie goes all out with a ginger-spiced dough and tons of sprinkles.', 'image': 'marranitos-enfiestados.jpg', 'recipeYield': '', 'recipeIngredient': ['1½ cups (210 g) whole wheat flour', '1 Tbsp. ground cinnamon', '1 Tbsp. ground ginger', '1 tsp. ground allspice', '¼ tsp. ground cloves', '1 tsp. baking powder', '½ tsp. baking soda', '3¾ cups (469 g) all-purpose flour, plus more for dusting', '12 Tbsp. (1½ sticks) unsalted butter, room temperature, divided', '½ cup (100 g) plus 1 tsp. granulated sugar', '1¼ tsp. Diamond Crystal or ¾ tsp. Morton kosher salt, divided', '3 large eggs, room temperature', '2 tsp. vanilla extract, divided', '⅓ cup light agave syrup (nectar), honey, or light corn syrup', '½ cup (100 g) grated or granulated piloncillo or (packed) dark brown sugar', '⅓ cup robust-flavored (dark) molasses', 'Nonstick vegetable oil spray', 'Sanding sugar or sprinkles (for decorating)', 'A 4" pig-shaped cookie cutter'], 'recipeInstructions': [{'@type': 'HowToStep', 'text': 'Whisk whole wheat flour, cinnamon, ginger, allspice, cloves, baking powder, baking soda, and 3¾ cups (469 g) all-purpose flour in a medium bowl. Using an electric mixer on medium-high speed, beat 6 Tbsp. butter, ½ cup (100 g) granulated sugar, and half of salt in a large bowl, scraping down sides of bowl as needed, until light and creamy, about 3 minutes. Add 1 egg and 1 tsp. vanilla; beat to combine. Add agave and beat just until smooth. Reduce speed to low, add half of dry ingredients (if you have a scale, use it!), and beat to combine, scraping down sides of bowl as needed. Dough will be slightly sticky. Wrap in plastic; pat into a square about ¾" thick. Chill at least 3 hours and up to 1 day.'
|
||||
},
|
||||
{'@type': 'HowToStep', 'text': 'Clean bowl and beaters. With mixer on medium-high speed, beat piloncillo, remaining salt, and remaining 6 Tbsp. butter, scraping down sides of bowl as needed, until light and creamy, about 3 minutes. Add 1 egg and remaining 1 tsp. vanilla; beat until combined. Add molasses and beat until smooth. Reduce speed to low; beat in remaining dry ingredients, scraping down sides of bowl as needed. Wrap in plastic; pat into a square about ¾" thick. Chill at least 3 hours and up to 1 day.'}, {'@type': 'HowToStep', 'text': 'Place racks in upper and lower thirds of oven; preheat to 350°. Line 3 baking sheets with parchment paper and lightly coat with nonstick spray. Cut both doughs into about ¾" pieces. (Don’t worry about being super precise; spots will look better if pieces are different shapes and sizes.) Arrange about half of brown and white dough pieces, touching and alternating colors, in an even layer on a lightly floured piece of parchment. (Chill remaining dough while you work.) Roll out ¼" thick. Punch out cookies with lightly floured cutter and transfer to prepared baking sheets, spacing ¾" apart. Arrange scraps in a single layer so they are touching and cover with plastic; chill 10 minutes if soft. Roll out scraps and cut out more pigs. Repeat with remaining dough and scraps.'
|
||||
},
|
||||
{'@type': 'HowToStep', 'text': 'Beat remaining egg and remaining 1 tsp. granulated sugar in a small bowl. Using your finger, rub egg on brown spots only (egg will darken the spots) and along edges of cookies. Sprinkle sanding sugar or sprinkles along the edges of cookies, gently pressing into dough to adhere. Chill 10 minutes.'
|
||||
},
|
||||
{'@type': 'HowToStep', 'text': 'Working in batches, bake cookies, rotating baking sheets top to bottom and front to back halfway through, until puffed and light spots are golden,
|
||||
10–12 minutes. Let cookies cool 10 minutes on baking sheets, then transfer to a wire rack. Let cool completely.\nDo ahead: Cookies can be made 2 days ahead. Store airtight at room temperature.'
|
||||
}
|
||||
], 'totalTime': None, 'slug': 'marranitos-enfiestados', 'categories': [], 'tags': [], 'dateAdded': datetime.date(2020,
|
||||
12,
|
||||
20), 'notes': [], 'rating': 4, 'orgURL':
|
||||
{'name': 'Broccoli Beer Cheese Soup', 'description': "This recipe is inspired by one of my favorites, Gourmand's Beer Cheese Soup, which uses Shiner Bock. Feel free to use whatever you want, then go to [Gourmand's](http://lovethysandwich.com) to have the real thing.", 'image': 'broccoli-beer-cheese-soup.jpg', 'recipeYield': None, 'recipeIngredient': ['4 tablespoons butter', '1 cup diced onion', '1/2 cup shredded carrot', '1/2 cup diced celery', '1 tablespoon garlic', '1/4 cup flour', '1 quart chicken broth', '1 cup heavy cream', '10 ounces muenster cheese', '1 cup white white wine', '1 cup pale beer', '1 teaspoon Worcestershire sauce', '1/2 teaspoon hot sauce'
|
||||
], 'recipeInstructions': ['Start with butter, onions, carrots, celery, garlic until cooked down', 'Add flour, stir well, cook for 4-5 mins', 'Add chicken broth, bring to a boil', 'Add wine and reduce to a simmer', 'Add cream, cheese, Worcestershire, and hot sauce', 'Serve with croutons'
|
||||
], 'totalTime': None, 'slug': 'broccoli-beer-cheese-soup', 'categories': None, 'tags': None, 'dateAdded': None, 'notes': None, 'rating': None, 'orgURL': None, 'extras': None
|
||||
}
|