Many small refactors, mainly moving the JSON import/export to one file.
This commit is contained in:
parent
1017f3b68c
commit
ba139706bb
53
README.md
53
README.md
@ -1,16 +1,32 @@
|
|||||||
# Recipes for Engineers
|
# Recipes for Engineers
|
||||||
|
|
||||||
Store recipes in a nerdy JSON tree format and use that to generate [Cooking for Engineers](https://www.cookingforengineers.com)-style tabular recipe cards in HTML.
|
Store recipes in a nerdy JSON tree format and use that to generate [Cooking for
|
||||||
|
Engineers](https://www.cookingforengineers.com)-style tabular recipe cards in
|
||||||
|
HTML.
|
||||||
|
|
||||||
Right now this project is just a set of scripts.
|
I used these scripts to create my [recipes
|
||||||
|
site](https://tilde.town/~gamerdonkey/recipes/) here on town. Fun fact: you can
|
||||||
|
replace the `html` with `json` in the URLs to get my recipes in their raw JSON
|
||||||
|
format.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
These scripts require the `anytree` Python library.
|
Requires the [anytree Python
|
||||||
|
library](https://anytree.readthedocs.io/en/latest/).
|
||||||
|
|
||||||
## `create_recipe_json.py`
|
## Main Scripts
|
||||||
|
|
||||||
This is a fairly naive script that walks you through the process of turning a recipe into a tree structure. It has you list out ingredients, and then add tasks to perform on those ingredients. You end up with a tree that is something like this:
|
### `create_recipe_json.py`
|
||||||
|
|
||||||
|
This is a fairly naive script that walks you through the process of turning a
|
||||||
|
recipe into a tree structure. It has you list out ingredients, and then add
|
||||||
|
tasks to perform on those ingredients.
|
||||||
|
|
||||||
|
You can add ingredients and then add tasks to perform on those ingredients.
|
||||||
|
The results of those tasks can have more tasks applied to them, until you end
|
||||||
|
up with your finished product.
|
||||||
|
|
||||||
|
The tree you build has a structure like this, with the last step at the root:
|
||||||
|
|
||||||
```
|
```
|
||||||
shake and grill for 20 more mins
|
shake and grill for 20 more mins
|
||||||
@ -28,12 +44,31 @@ shake and grill for 20 more mins
|
|||||||
+-- red potatoes
|
+-- red potatoes
|
||||||
```
|
```
|
||||||
|
|
||||||
Leaves are raw ingredients, other nodes are tasks. The tree is output to a JSON file.
|
Leaves are raw ingredients, other nodes are tasks. A recipe can have multiple
|
||||||
|
tree roots. Once you are done adding ingredients and tasks, all trees are
|
||||||
|
output to a JSON file based on the name you give to the recipe.
|
||||||
|
|
||||||
## `recipe_json_to_html.py`
|
### `recipes2html.py`
|
||||||
|
|
||||||
This script takes that JSON file and does a recursive depth-first search to render a webpage with an HTML-table-based recipe card.
|
***WARNING: These scripts do not sanitize most inputs!*** Parts of the JSON can
|
||||||
|
have HTML which will be rendered out in the browser, which was very convenient
|
||||||
|
for me but a terrible idea for security. **DO NOT RUN THIS CODE ON JSON YOU
|
||||||
|
DID NOT CREATE YOURSELF!**
|
||||||
|
|
||||||
|
This script takes an argument pointing to a directory of recipe JSON files, in
|
||||||
|
the format created by the `create_recipe_json.py` script. It then does a
|
||||||
|
recursive depth-first search on each tree in that file to generate an HTML
|
||||||
|
file with a traditional recipe and a tabular-style recipe card. It also takes
|
||||||
|
an optional argument pointing to a file and, if given, the contents of that
|
||||||
|
file will be inserted into the header section of the generated `index.html`.
|
||||||
|
|
||||||
## Known Issues
|
## Known Issues
|
||||||
|
|
||||||
- A single node cannot have multiple tasks done on it (nerdy reason: because a node cannot have two parents). So this cannot represent, for example, mixing cinnamon and sugar, then splitting that mixture up and doing separate subsequent operations with the two batches.
|
- A single node cannot have multiple tasks done on it (nerdy reason: because a
|
||||||
|
node cannot have two parents). So this cannot perfectly represent, for example,
|
||||||
|
mixing cinnamon and sugar, then splitting that mixture up and doing separate
|
||||||
|
subsequent operations with the two batches.
|
||||||
|
This has come up once in my [zucchini bread
|
||||||
|
recipe](https://tilde.town/~gamerdonkey/recipes/lemony_olive_oil_zucchini_bread.html),
|
||||||
|
and I worked around the issue by just adding an instruction to set the
|
||||||
|
ingredients aside and use them later.
|
||||||
|
@ -2,6 +2,7 @@ import json
|
|||||||
|
|
||||||
from anytree import Node, RenderTree
|
from anytree import Node, RenderTree
|
||||||
from anytree.exporter import DictExporter
|
from anytree.exporter import DictExporter
|
||||||
|
from recipe import Recipe
|
||||||
|
|
||||||
|
|
||||||
def print_nodes():
|
def print_nodes():
|
||||||
@ -15,42 +16,61 @@ node = "start"
|
|||||||
while node:
|
while node:
|
||||||
print_nodes()
|
print_nodes()
|
||||||
|
|
||||||
node = input("Add node: ")
|
node = input("Add ingredient/task: ")
|
||||||
|
|
||||||
if node:
|
if node:
|
||||||
needs = input("List needs (comma-separated): ")
|
needs = ""
|
||||||
|
if nodes:
|
||||||
|
needs = input("List needs (comma-separated): ")
|
||||||
|
|
||||||
description = input("Detailed description: ")
|
description = input("Detailed description: ")
|
||||||
|
|
||||||
if needs:
|
if needs:
|
||||||
result = input("Result of node (e.g. chopped carrots): ")
|
result = input("Result of node (e.g. chopped carrots): ")
|
||||||
new_node = Node(node, description=description, result=result)
|
new_node = Node(node, description=description, result=result)
|
||||||
|
|
||||||
for e in [int(key.strip()) for key in needs.split(",")]:
|
for key in needs.split(","):
|
||||||
nodes.pop(e).parent = new_node
|
try:
|
||||||
|
nodes.pop(int(key.strip())).parent = new_node
|
||||||
|
except KeyError:
|
||||||
|
print(f"<{key.strip()}> was not a valid ingredient or task")
|
||||||
else:
|
else:
|
||||||
new_node = Node(node, description=description, result=node)
|
new_node = Node(node, description=description, result=node)
|
||||||
|
|
||||||
nodes[i] = new_node
|
nodes[i] = new_node
|
||||||
i += 1
|
i += 1
|
||||||
|
else:
|
||||||
|
for key, tree_node in nodes.items():
|
||||||
|
print(key, end=" ")
|
||||||
|
for pre, _, render_node in RenderTree(tree_node):
|
||||||
|
print(f"{pre}{render_node.name}")
|
||||||
|
|
||||||
root_keys = nodes.keys()
|
confirm = input("Done adding ingredients and tasks? (y/N): ")
|
||||||
|
|
||||||
for key in root_keys:
|
if not confirm or not confirm.lower().startswith("y"):
|
||||||
for pre, _, node in RenderTree(nodes[key]):
|
node = "continue"
|
||||||
print(f"{pre}{node.name}")
|
|
||||||
|
name = ""
|
||||||
|
try:
|
||||||
|
while not name:
|
||||||
|
name = input("Recipe name (required to save, ctrl-c to exit): ")
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("Exiting...")
|
||||||
|
exit(0)
|
||||||
|
|
||||||
name = input("Recipe name: ")
|
|
||||||
description = input("Recipe description: ")
|
description = input("Recipe description: ")
|
||||||
if name:
|
servings = input("Servings: ")
|
||||||
sub_recipes = []
|
prep_time = input("Prep time: ")
|
||||||
exporter = DictExporter()
|
cook_time = input("Cook time: ")
|
||||||
for key in root_keys:
|
|
||||||
sub_recipes.append(exporter.export(nodes[key]))
|
|
||||||
|
|
||||||
recipe = {"name": name, "description": description, "sub_recipes": sub_recipes}
|
recipe = Recipe(name, description, servings, prep_time, cook_time)
|
||||||
|
for _, node in nodes.items():
|
||||||
|
recipe.add_sub_recipe(node)
|
||||||
|
|
||||||
filename = name.replace(" ", "_").lower()
|
filename = f"{name.replace(' ', '_').lower()}.json"
|
||||||
|
|
||||||
with open(f"{filename}.json", 'w') as f:
|
print(f"Writing to {filename}...")
|
||||||
f.write(json.dumps(recipe, indent=2))
|
with open(filename, 'w') as f:
|
||||||
|
f.write(recipe.to_json())
|
||||||
|
print("Done!")
|
||||||
|
|
||||||
|
75
recipe.py
75
recipe.py
@ -1,6 +1,9 @@
|
|||||||
|
import html
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
from anytree import Node
|
||||||
from anytree.importer import DictImporter
|
from anytree.importer import DictImporter
|
||||||
|
from anytree.exporter import DictExporter
|
||||||
from sys import argv
|
from sys import argv
|
||||||
|
|
||||||
|
|
||||||
@ -14,29 +17,32 @@ class TableCell:
|
|||||||
|
|
||||||
def to_html(self):
|
def to_html(self):
|
||||||
if self.is_header:
|
if self.is_header:
|
||||||
html = f"<th title='{self.description}'>{self.name}</th>"
|
output = f"<th title='{html.escape(self.description)}'>{self.name}</th>"
|
||||||
elif not self.name:
|
elif not self.name:
|
||||||
html = f'<td colspan="{self.colspan}" class="filler"></td>'
|
output = f'<td colspan="{self.colspan}" class="filler"></td>'
|
||||||
else:
|
else:
|
||||||
html = f"<td rowspan='{self.rowspan}' colspan='{self.colspan}' title='{self.description}'>{self.name}</td>"
|
output = f"<td rowspan='{self.rowspan}' colspan='{self.colspan}' title='{html.escape(self.description)}'>{self.name}</td>"
|
||||||
|
|
||||||
return html
|
return output
|
||||||
|
|
||||||
|
|
||||||
class Recipe:
|
class Recipe:
|
||||||
def __init__(self, json_filename: str):
|
def __init__(self, name: str, desc: str, servings: str, prep_time: str, cook_time: str, sub_recipes: list[Node]=[]):
|
||||||
with open(json_filename, 'r') as f:
|
self.name = name
|
||||||
recipe_dict = json.loads(f.read())
|
self.desc = desc
|
||||||
|
self.servings = servings
|
||||||
|
self.prep_time = prep_time
|
||||||
|
self.cook_time = cook_time
|
||||||
|
|
||||||
self.name = recipe_dict.get("name")
|
self._sub_recipes = sub_recipes
|
||||||
self.desc = recipe_dict.get("description")
|
|
||||||
importer = DictImporter()
|
def add_sub_recipe(self, tree_root: Node):
|
||||||
self.sub_recipes = [importer.import_(data) for data in recipe_dict["sub_recipes"]]
|
self._sub_recipes.append(tree_root)
|
||||||
|
|
||||||
def generate_ingredient_list(self):
|
def generate_ingredient_list(self):
|
||||||
ingredients = []
|
ingredients = []
|
||||||
|
|
||||||
for sub_recipe in self.sub_recipes:
|
for sub_recipe in self._sub_recipes:
|
||||||
for ingredient in sub_recipe.leaves:
|
for ingredient in sub_recipe.leaves:
|
||||||
if ingredient.description:
|
if ingredient.description:
|
||||||
ingredients.append(f"{ingredient.name} -- {ingredient.description}")
|
ingredients.append(f"{ingredient.name} -- {ingredient.description}")
|
||||||
@ -48,7 +54,7 @@ class Recipe:
|
|||||||
def generate_task_list(self):
|
def generate_task_list(self):
|
||||||
tasks = []
|
tasks = []
|
||||||
|
|
||||||
for sub_recipe in self.sub_recipes:
|
for sub_recipe in self._sub_recipes:
|
||||||
tasks.extend(self.generate_steps_depth_first(sub_recipe, steps=[]))
|
tasks.extend(self.generate_steps_depth_first(sub_recipe, steps=[]))
|
||||||
|
|
||||||
return tasks
|
return tasks
|
||||||
@ -65,7 +71,7 @@ class Recipe:
|
|||||||
def generate_tables(self):
|
def generate_tables(self):
|
||||||
tables = []
|
tables = []
|
||||||
|
|
||||||
for sub_recipe in self.sub_recipes:
|
for sub_recipe in self._sub_recipes:
|
||||||
tables.append(self.build_table_rows_depth_first(sub_recipe))
|
tables.append(self.build_table_rows_depth_first(sub_recipe))
|
||||||
|
|
||||||
return tables
|
return tables
|
||||||
@ -97,6 +103,38 @@ class Recipe:
|
|||||||
|
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_json(cls, json_string: str):
|
||||||
|
recipe_dict = json.loads(json_string)
|
||||||
|
|
||||||
|
name = recipe_dict["name"]
|
||||||
|
desc = recipe_dict.get("description")
|
||||||
|
servings = recipe_dict.get("servings")
|
||||||
|
prep_time = recipe_dict.get("prep_time")
|
||||||
|
cook_time = recipe_dict.get("cook_time")
|
||||||
|
importer = DictImporter()
|
||||||
|
sub_recipes = [importer.import_(data) for data in recipe_dict["sub_recipes"]]
|
||||||
|
|
||||||
|
return cls(name, desc, servings, prep_time, cook_time, sub_recipes)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
sub_recipes_dict = []
|
||||||
|
exporter = DictExporter()
|
||||||
|
for sub_recipe in self._sub_recipes:
|
||||||
|
sub_recipes_dict.append(exporter.export(sub_recipe))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"name": self.name,
|
||||||
|
"description": self.desc,
|
||||||
|
"servings": self.servings,
|
||||||
|
"prep_time": self.prep_time,
|
||||||
|
"cook_time": self.cook_time,
|
||||||
|
"sub_recipes": sub_recipes_dict
|
||||||
|
}
|
||||||
|
|
||||||
|
def to_json(self):
|
||||||
|
return json.dumps(self.to_dict(), indent=2)
|
||||||
|
|
||||||
def to_html(self):
|
def to_html(self):
|
||||||
return f"""<!DOCTYPE html>
|
return f"""<!DOCTYPE html>
|
||||||
<html lang='en'>
|
<html lang='en'>
|
||||||
@ -110,6 +148,11 @@ class Recipe:
|
|||||||
<nav><a href='./'>Recipe Index</a></nav>
|
<nav><a href='./'>Recipe Index</a></nav>
|
||||||
<h1>{self.name}</h1>
|
<h1>{self.name}</h1>
|
||||||
<p>{self.desc}</p>
|
<p>{self.desc}</p>
|
||||||
|
<p>
|
||||||
|
{f"<span><strong>Servings: </strong>{self.servings}</span><br>" if self.servings else ""}
|
||||||
|
{f"<span><strong>Preparation Time: </strong>{self.prep_time}</span><br>" if self.prep_time else ""}
|
||||||
|
{f"<span><strong>Cook Time: </strong>{self.cook_time}</span><br>" if self.cook_time else ""}
|
||||||
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
@ -149,5 +192,7 @@ if __name__ == "__main__":
|
|||||||
print(f"usage: {argv[0]} <json file>")
|
print(f"usage: {argv[0]} <json file>")
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
print(Recipe(argv[1]).to_html())
|
|
||||||
|
with open(argv[1], 'r') as f:
|
||||||
|
print(Recipe.from_json(f.read()).to_html())
|
||||||
|
|
||||||
|
@ -31,7 +31,8 @@ recipe_names = {}
|
|||||||
for dirpath, dirnames, filenames in walk(recipe_dir):
|
for dirpath, dirnames, filenames in walk(recipe_dir):
|
||||||
for filename in filenames:
|
for filename in filenames:
|
||||||
if filename.endswith("json"):
|
if filename.endswith("json"):
|
||||||
recipe = Recipe(path.join(dirpath, filename))
|
with open(path.join(dirpath, filename), 'r') as json_file:
|
||||||
|
recipe = Recipe.from_json(json_file.read())
|
||||||
|
|
||||||
html = recipe.to_html()
|
html = recipe.to_html()
|
||||||
html_filepath = path.join(dirpath, f"{filename.rsplit('.', maxsplit=1)[0]}.html")
|
html_filepath = path.join(dirpath, f"{filename.rsplit('.', maxsplit=1)[0]}.html")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user