Compare commits
17 commits
single-str
...
main
Author | SHA1 | Date | |
---|---|---|---|
2e5859342d | |||
ae3ee28ab4 | |||
8a9ae16ad3 | |||
cc9206306a | |||
3e3de7ea59 | |||
e35bc3852b | |||
7b3aa25c95 | |||
88f801cadb | |||
8046f0d237 | |||
08fce0ae16 | |||
67cb69000b | |||
7a9a39f2fb | |||
70432d867e | |||
d8569aa43e | |||
816e65a00c | |||
67bb8c2266 | |||
69e578dba4 |
9 changed files with 137 additions and 39 deletions
15
.woodpecker/containers.yaml
Normal file
15
.woodpecker/containers.yaml
Normal 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
|
15
Dockerfile
15
Dockerfile
|
@ -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
43
README.md
Normal 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
|
||||||
|
```
|
|
@ -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,
|
||||||
|
|
|
@ -197,19 +197,20 @@ class IngredientInstance(Element):
|
||||||
def from_dict(cls, ctx: Context, dct: str | Dict[str, Any]) -> Self:
|
def from_dict(cls, ctx: Context, dct: str | Dict[str, Any]) -> Self:
|
||||||
if isinstance(dct, str):
|
if isinstance(dct, str):
|
||||||
string = dct.strip()
|
string = dct.strip()
|
||||||
p = re.compile(r"^(?:([0-9\.]+) ([a-zA-Z]+) )?([\w ]+)(?: \((.*)\))?$")
|
p = re.compile(r"^(?:([0-9\.]+) ([a-zA-Z]+) )+([\w ]+)(?: \((.*)\))?$")
|
||||||
match = p.match(string)
|
match = p.match(string)
|
||||||
if match is None:
|
|
||||||
raise RuntimeError(
|
amount = float(1)
|
||||||
"ingredient {string} regex not matched, it should be in the format [amount(num) unit(string, one word)] name(string, any number of words) [(note(string))]"
|
unit = ctx.default_unit
|
||||||
)
|
name = string
|
||||||
|
note = None
|
||||||
|
if match is not None:
|
||||||
amount = float(match.group(1))
|
amount = float(match.group(1))
|
||||||
unitstr = match.group(2)
|
unitstr = match.group(2)
|
||||||
unit = ctx.default_unit
|
|
||||||
if unit is not None:
|
if unit is not None:
|
||||||
unitx = ctx.units.get(unitstr)
|
unitx = ctx.units.get(unitstr)
|
||||||
if unitx is None:
|
if unitx is None:
|
||||||
ctx.issues.error(issues.ISSUE_UNKNOWN_UNIT, "unknown unit {unitstr}")
|
ctx.issues.error(issues.ISSUE_UNKNOWN_UNIT, f"unknown unit {unitstr}")
|
||||||
else:
|
else:
|
||||||
unit = unitx
|
unit = unitx
|
||||||
name = match.group(3)
|
name = match.group(3)
|
||||||
|
@ -218,7 +219,7 @@ class IngredientInstance(Element):
|
||||||
note = ""
|
note = ""
|
||||||
return cls(
|
return cls(
|
||||||
ctx=ctx,
|
ctx=ctx,
|
||||||
name=dct,
|
name=name,
|
||||||
amount=amount,
|
amount=amount,
|
||||||
unit=unit,
|
unit=unit,
|
||||||
alternatives=[],
|
alternatives=[],
|
||||||
|
@ -289,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)
|
||||||
|
@ -317,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
|
||||||
|
@ -335,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:
|
||||||
|
@ -564,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
|
||||||
|
@ -591,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
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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%}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% macro ingredientpart(ing) -%}
|
{% macro ingredientpart(ing) -%}
|
||||||
{% if recipe.price != None %}{{price(ing.price)}} {%endif%}
|
{% if ing.price != None %}{{price(ing.price)}} {%endif%}
|
||||||
{% if ing.amount != None %} {{ing.amount|amountprint}}
|
{% if ing.amount != None %} {{ing.amount|amountprint}}
|
||||||
{% if ing.unit != None %} {{ing.unit.name}}{% endif %}
|
{% if ing.unit != None %} {{ing.unit.name}}{% endif %}
|
||||||
{%endif%}
|
{%endif%}
|
||||||
|
@ -29,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>
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue