Compare commits

...
Sign in to create a new pull request.

17 commits

Author SHA1 Message Date
2e5859342d Dockerfile: fix entrypoint
All checks were successful
ci/woodpecker/push/containers Pipeline was successful
2024-10-12 00:32:22 +02:00
ae3ee28ab4 README.md: update 2024-10-12 00:20:05 +02:00
8a9ae16ad3 .woodpecker: add 2024-10-12 00:12:20 +02:00
cc9206306a parsing: also accept short ingredients without amounts
for example 'oil' will now be allowed, previously it would have to be `1
spoon oil` when sometimes people don't know the amount
2024-10-12 00:12:09 +02:00
3e3de7ea59 pyproject.toml: add dependency required with newer lxml 2024-10-12 00:04:40 +02:00
e35bc3852b update URLs 2024-10-11 22:21:30 +02:00
7b3aa25c95 Dockerfile: use python image instead of alpine, update 2024-10-11 22:20:08 +02:00
88f801cadb show full recipe price every time with a clarification if necessary 2024-05-12 22:28:42 +00:00
8046f0d237 responsive website 2023-12-12 02:33:12 +00:00
08fce0ae16 update package name in dockerfile 2023-12-12 02:32:04 +00:00
67cb69000b fix generate-* subcommands from other directories 2023-11-30 03:09:36 +00:00
7a9a39f2fb make pricedb currency mandatory
now, if there is a price entry for an ingredient, it either has to have a currency specified or default_currency has to be set in settings.yaml
2023-11-30 03:09:36 +00:00
70432d867e update urls in pyproject.toml 2023-11-30 03:09:35 +00:00
d8569aa43e Add README.md 2023-11-30 03:09:35 +00:00
816e65a00c add warning about serve not being suitable for production 2023-11-30 03:09:34 +00:00
67bb8c2266 rename project from recipes to comfy-recipes 2023-11-30 03:09:34 +00:00
69e578dba4 allow using a single string as an ingredient instead of a dict 2023-11-28 14:26:34 +00:00
12 changed files with 179 additions and 39 deletions

View file

@ -0,0 +1,15 @@
when:
- event: push
branch: main
steps:
- name: publish
image: woodpeckerci/plugin-docker-buildx
settings:
platforms: linux/amd64,linux/arm64/v8
repo: git.comfy.city/emi/comfy-recipes
registry: git.comfy.city
tags: latest
username: ${CI_REPO_OWNER}
password:
from_secret: registry_token

View file

@ -1,12 +1,11 @@
FROM alpine:3.18 AS build FROM python:3.13-alpine AS build
COPY . /build COPY . /build
WORKDIR /build WORKDIR /build
RUN apk add --no-cache python3 py3-build py3-hatchling && \ RUN pip install build hatchling && \
python3 -m build . python3 -m build .
FROM alpine:3.18 FROM python:3.13-alpine
COPY --from=build /build/dist/recipes-*-py3-none-any.whl / COPY --from=build /build/dist/ /mnt
RUN apk add --no-cache python3 py3-pip && \ RUN pip install /mnt/comfy_recipes-*-py3-none-any.whl && \
pip install /recipes-*-py3-none-any.whl && \ rm -r /mnt
apk del py3-pip ENTRYPOINT ["/usr/local/bin/recipes-cli"]
ENTRYPOINT ["/usr/bin/recipes-cli"]

43
README.md Normal file
View file

@ -0,0 +1,43 @@
# Comfy Recipes
Comfy Recipes is a simple application that renders your recipes into HTML.
Documentation is available at <https://comfy.city/comfy-recipes/docs>
## Running
```sh
docker run -v ./recipes:/recipes git.comfy.city/Emi/comfy-recipes:latest build /recipes
```
## Building
### Building the python package
Make sure you have hatchling and build modules installed.
```sh
python3 -m build
```
The package can now be installed with
```sh
pip install dist/comfy-recipes-*-py3-none-any.whl
```
### Building the docker container
```sh
docker buildx build -t git.comfy.city/Emi/comfy-recipes:local .
```
The container can now be ran
```sh
docker run -v ./recipes:/recipes git.comfy.city/Emi/comfy-recipes:local build /recipes
```
### Building the documentation
```sh
cd docs
mdbook build
```
Documentation can now be served with
```sh
mdbook serve
```

View file

@ -139,7 +139,8 @@ class Builder:
def collect_unitnames(rec: parsing.Recipe) -> List[str]: def collect_unitnames(rec: parsing.Recipe) -> List[str]:
results: List[str] = [] results: List[str] = []
for ing in rec.ingredients: for ing in rec.ingredients:
results.append(ing.unit.name) if ing.unit is not None:
results.append(ing.unit.name)
return results return results
unitnamelists = self.foreach_recipe(collect_unitnames) unitnamelists = self.foreach_recipe(collect_unitnames)

View file

@ -44,12 +44,14 @@ def main() -> None:
(args.address, args.port), http.server.SimpleHTTPRequestHandler (args.address, args.port), http.server.SimpleHTTPRequestHandler
) )
print(f"serving at http://{args.address}:{args.port}") print(f"serving at http://{args.address}:{args.port}")
print("THIS WEB SERVER IS ONLY MEANT FOR LOCAL TESTING, IT IS NOT SUITABLE FOR PRODUCTION")
try: try:
httpd.serve_forever() httpd.serve_forever()
except KeyboardInterrupt: except KeyboardInterrupt:
pass pass
elif args.subcommand == "generate-units": elif args.subcommand == "generate-units":
if not args.force and os.path.isfile(args.directory + "/units.yaml"): os.chdir(args.directory)
if not args.force and os.path.isfile("units.yaml"):
print( print(
"units.yaml already exists, pass --force if you want to overwrite it", "units.yaml already exists, pass --force if you want to overwrite it",
file=sys.stderr, file=sys.stderr,
@ -58,7 +60,8 @@ def main() -> None:
else: else:
builder.generate_units() builder.generate_units()
elif args.subcommand == "generate-ingredients": elif args.subcommand == "generate-ingredients":
if not args.force and os.path.isfile(args.directory + "/ingredients.yaml"): os.chdir(args.directory)
if not args.force and os.path.isfile("ingredients.yaml"):
print( print(
"ingredients.yaml already exists, pass --force if you want to overwrite it", "ingredients.yaml already exists, pass --force if you want to overwrite it",
file=sys.stderr, file=sys.stderr,

View file

@ -1,5 +1,6 @@
from abc import abstractmethod from abc import abstractmethod
import collections import collections
import re
from typing import Any, Dict, List, Optional, Self from typing import Any, Dict, List, Optional, Self
import comfyrecipes.settings as settings import comfyrecipes.settings as settings
@ -178,8 +179,8 @@ class IngredientInstance(Element):
self, self,
ctx: Context, ctx: Context,
name: str, name: str,
amount: float, amount: Optional[float],
unit: Unit, unit: Optional[Unit],
alternatives: List["IngredientInstance"], alternatives: List["IngredientInstance"],
note: str, note: str,
price: Optional["PriceDB"], price: Optional["PriceDB"],
@ -193,7 +194,38 @@ class IngredientInstance(Element):
self.price = price self.price = price
@classmethod @classmethod
def from_dict(cls, ctx: Context, dct: Dict[str, Any]) -> Self: def from_dict(cls, ctx: Context, dct: str | Dict[str, Any]) -> Self:
if isinstance(dct, str):
string = dct.strip()
p = re.compile(r"^(?:([0-9\.]+) ([a-zA-Z]+) )+([\w ]+)(?: \((.*)\))?$")
match = p.match(string)
amount = float(1)
unit = ctx.default_unit
name = string
note = None
if match is not None:
amount = float(match.group(1))
unitstr = match.group(2)
if unit is not None:
unitx = ctx.units.get(unitstr)
if unitx is None:
ctx.issues.error(issues.ISSUE_UNKNOWN_UNIT, f"unknown unit {unitstr}")
else:
unit = unitx
name = match.group(3)
note = match.group(4)
if note is None:
note = ""
return cls(
ctx=ctx,
name=name,
amount=amount,
unit=unit,
alternatives=[],
note=note,
price=None,
)
name = dct["name"] name = dct["name"]
ingredient = ctx.ingredients.get(name) ingredient = ctx.ingredients.get(name)
@ -258,7 +290,7 @@ class Recipe(Element):
source: Optional[SafeHTML], source: Optional[SafeHTML],
ingredients: List[IngredientInstance], ingredients: List[IngredientInstance],
subrecipes: List["Recipe"], subrecipes: List["Recipe"],
price: Optional["PriceDB"], price: Optional["MultiPriceDB"],
stepsections: List[StepSection], stepsections: List[StepSection],
) -> None: ) -> None:
super().__init__(ctx) super().__init__(ctx)
@ -286,7 +318,7 @@ class Recipe(Element):
rp = Recipe.from_dict(ctx, partdct) rp = Recipe.from_dict(ctx, partdct)
subrecipes.append(rp) subrecipes.append(rp)
price: Optional[PriceDB] = None price: Optional[MultiPriceDB] = None
pricex: float = 0 pricex: float = 0
ingswithprice = 0 ingswithprice = 0
ingswithoutprice = 0 ingswithoutprice = 0
@ -304,16 +336,22 @@ class Recipe(Element):
# we don't know how to convert currencies yet # we don't know how to convert currencies yet
currency = None currency = None
break break
if currency is None or ingswithoutprice != 0 or len(ingredients) == 0: if currency is None or len(ingredients) == 0:
price = None price = None
else: else:
price = PriceDB( pricedb = PriceDB(
ctx=ctx, ctx=ctx,
price=pricex, price=pricex,
amount=1, amount=1,
unit=ctx.default_unit, unit=ctx.default_unit,
currency=currency, currency=currency,
) )
price = MultiPriceDB(
ctx=ctx,
pricedb=pricedb,
item_count=len(ingredients),
item_prices_missing=ingswithoutprice,
)
stepsections: List[StepSection] = [] stepsections: List[StepSection] = []
if "steps" in dct: if "steps" in dct:
@ -533,7 +571,7 @@ class PriceDB(Element):
price: float, price: float,
amount: float, amount: float,
unit: Unit, unit: Unit,
currency: Optional[str], currency: str,
) -> None: ) -> None:
super().__init__(ctx) super().__init__(ctx)
self.price = price self.price = price
@ -560,4 +598,19 @@ class PriceDB(Element):
currency = ctx.settings.default_currency currency = ctx.settings.default_currency
if "currency" in dct: if "currency" in dct:
currency = dct["currency"] currency = dct["currency"]
if currency is None:
raise RuntimeError("currency not specified and default_currency is also not set")
return cls(ctx=ctx, price=price, amount=amount, unit=unit, currency=currency) return cls(ctx=ctx, price=price, amount=amount, unit=unit, currency=currency)
class MultiPriceDB(Element):
def __init__(
self,
ctx: Context,
pricedb: PriceDB,
item_count: int,
item_prices_missing: int,
) -> None:
super().__init__(ctx)
self.pricedb = pricedb
self.item_count = item_count
self.item_prices_missing = item_prices_missing

View file

@ -13,17 +13,22 @@
"ingredients": { "ingredients": {
"type": "array", "type": "array",
"items": { "items": {
"$id": "https://example.com/ingredient.json", "oneOf": [
"type": "object", { "type": "string" },
"additionalProperties": false, {
"required": [ "name" ], "$id": "https://example.com/ingredient.json",
"properties": { "type": "object",
"name": { "type": "string" }, "additionalProperties": false,
"amount": { "type": "number" }, "required": [ "name" ],
"unit": { "type": "string" }, "properties": {
"or": { "items": { "$ref": "/ingredient.json" } }, "name": { "type": "string" },
"note": { "type": "string" } "amount": { "type": "number" },
} "unit": { "type": "string" },
"or": { "items": { "$ref": "/ingredient.json" } },
"note": { "type": "string" }
}
}
]
} }
}, },
"steps": { "steps": {

View file

@ -1,12 +1,24 @@
{% macro price(price) -%} {% macro price(price) -%}
{% if price != None and price is defined and price.price is defined %}{{price.price|round(1)|numprint}}{%else%}?{% endif %} {{price.currency}} {% if price != None and price is defined and price.price is defined %}{{price.price|round(1)|numprint}}{%else%}?{% endif %} {{price.currency}}
{%- endmacro %} {%- endmacro %}
{% macro multiprice(multiprice, shortform) -%}
{% if multiprice != None and multiprice.pricedb != None and multiprice.pricedb is defined and multiprice.pricedb.price is defined -%}
{{multiprice.pricedb.price|round(1)|numprint}}
{%else%}
?
{%- endif %}
{{multiprice.pricedb.currency}}
{%if multiprice.item_prices_missing != 0 and not shortform%}
({{multiprice.item_prices_missing}}/{{multiprice.item_count}} prices missing)
{%endif%}
{%- endmacro %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<title>{% block title %}{% endblock %}</title> <title>{% block title %}{% endblock %}</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/water.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/water.css">
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head> </head>
<body> <body>
{% block body %}{% endblock %} {% block body %}{% endblock %}

View file

@ -3,6 +3,6 @@
{%block body %} {%block body %}
<h1>Recipes</h1> <h1>Recipes</h1>
{% for recipe in recipes %} {% for recipe in recipes %}
<li>{% if recipe.price != None %}{{price(recipe.price)}} {%endif%}<a href="{{ recipe.outpath }}">{{ recipe.title }}</a></li> <li>{% if recipe.price != None %}{{multiprice(recipe.price, true)}} {%endif%}<a href="{{ recipe.outpath }}">{{ recipe.title }}</a></li>
{% endfor %} {% endfor %}
{%endblock%} {%endblock%}

View file

@ -1,6 +1,11 @@
{% extends "base.html" %} {% extends "base.html" %}
{% macro ingredientpart(ing) -%} {% macro ingredientpart(ing) -%}
{% if recipe.price != None %}{{price(ing.price)}} {%endif%}{{ing.amount|amountprint}} {{ing["unit"].name}} {{ ing.name }} {{ing.note}} {% if ing.price != None %}{{price(ing.price)}} {%endif%}
{% if ing.amount != None %} {{ing.amount|amountprint}}
{% if ing.unit != None %} {{ing.unit.name}}{% endif %}
{%endif%}
{{ ing.name }}
{% if ing.note != "" %} {{ing.note}}{% endif %}
{%- endmacro %} {%- endmacro %}
{% macro ingredient(ing) -%} {% macro ingredient(ing) -%}
@ -24,7 +29,10 @@
{% for ing in rec.ingredients %} {% for ing in rec.ingredients %}
<li>{{ingredient(ing)}}</li> <li>{{ingredient(ing)}}</li>
{% endfor %} {% endfor %}
{% if rec.price != None %}price: {{price(rec.price)}}{%endif%} {% if rec.price != None %}
<br>
price: {{multiprice(rec.price)}}
{%endif%}
{% endif %} {% endif %}
{% if rec.stepsections|length != 0 %} {% if rec.stepsections|length != 0 %}
<h3>Steps</h3> <h3>Steps</h3>

View file

@ -3,13 +3,13 @@ requires = ["hatchling"]
build-backend = "hatchling.build" build-backend = "hatchling.build"
[project] [project]
name = "recipes" name = "comfy-recipes"
version = "0.0.1" version = "0.0.1"
description = "Smart recipe static site generator" description = "Smart recipe static site generator"
authors = [{ name = "Emi Vasilek", email = "emi.vasilek@gmail.com" }] authors = [{ name = "Emi Vasilek", email = "emi.vasilek@gmail.com" }]
keywords = ["recipes", "recipe", "static site generator"] keywords = ["recipes", "recipe", "static site generator"]
requires-python = ">= 3.8" requires-python = ">= 3.8"
dependencies = ["jsonschema", "jinja2", "PyYAML", "mistune", "lxml"] dependencies = ["jsonschema", "jinja2", "PyYAML", "mistune", "lxml", "lxml_html_clean"]
classifiers = [ classifiers = [
"Development Status :: 3 - Alpha", "Development Status :: 3 - Alpha",
"Programming Language :: Python", "Programming Language :: Python",
@ -17,9 +17,10 @@ classifiers = [
license = { file = "LICENSE" } license = { file = "LICENSE" }
[project.urls] [project.urls]
Homepage = "https://codeberg.org/comfy.city/recipes" Homepage = "https://git.comfy.city/Emi/comfy-recipes"
Repository = "https://codeberg.org/comfy.city/recipes.git" Repository = "https://git.comfy.city/Emi/comfy-recipes.git"
Issues = "https://codeberg.org/comfy.city/recipes/issues" Issues = "https://git.comfy.city/Emi/comfy-recipes/issues"
Documentation = "https://comfy.city/comfy-recipes/docs"
[project.scripts] [project.scripts]
recipes-cli = "comfyrecipes.cli:main" recipes-cli = "comfyrecipes.cli:main"