migration changes

This commit is contained in:
Hayden 2021-01-01 16:51:55 -09:00
parent e5304f0589
commit e313741a25
59 changed files with 368 additions and 22124 deletions

View File

@ -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

View File

@ -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,
};

View File

@ -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 = {

View 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;
},
};

View File

@ -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}`);

View File

@ -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>

View 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>

View File

@ -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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 519 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 363 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 794 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 572 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 997 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 556 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 812 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 650 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 889 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 788 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 985 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

View 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"
),
)

View File

@ -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

View File

@ -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()

View 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

View File

@ -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)

View File

@ -1,5 +0,0 @@
const config = (() => {
return {
"VUE_APP_API_BASE_URL": "REPLACE_ME",
};
})();

View File

@ -1 +0,0 @@
.card-btn{margin-top:-10px}.disabled-card{opacity:1%}.img-input{position:absolute;bottom:0}

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -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>

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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

View File

@ -1,12 +1,4 @@
{'name': 'Marranitos Enfiestados', 'description': 'Rick Martinezs 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. (Dont 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,
1012 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
}