allaroundhere.org/build.py

185 lines
5.5 KiB
Python

import argparse
import datetime
import shutil
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from typing import Dict
from typing import NamedTuple
from typing import Optional
from typing import Sequence
from typing import Union
import jinja2
import marshmallow as msh
import ruamel.yaml
yaml = ruamel.yaml.YAML(typ="safe")
@dataclass
class MediaContainer:
title: str
source: str
preview: Optional[str] = None
anchor: Optional[Union[str, int]] = None
content: Optional[str] = None
hide_source: bool = False
class MediaSerializer(msh.Schema):
title = msh.fields.String(required=True)
source = msh.fields.String(required=True)
preview = msh.fields.String(required=False)
anchor = msh.fields.String(required=False)
content = msh.fields.String(required=False)
hide_source = msh.fields.Boolean(required=False)
@msh.post_load
def _make_dataclass(self, data: Dict[str, Any], *args, **kwargs) -> MediaContainer:
return MediaContainer(**data)
@dataclass
class LinkContainer:
link: str
title: Optional[str] = None
icon: Optional[str] = None
class LinkSerializer(msh.Schema):
link = msh.fields.URL(required=True)
title = msh.fields.String(required=False)
icon = msh.fields.String(required=False)
@msh.post_load
def _make_dataclass(self, data: Dict[str, Any], *args, **kwargs) -> LinkContainer:
return LinkContainer(**data)
class Location(NamedTuple):
title: str
link: str
class LocationSeralizer(msh.Schema):
title = msh.fields.String(required=True)
link = msh.fields.URL(required=True)
@msh.post_load
def _make_dataclass(self, data: Dict[str, Any], *args, **kwargs) -> Location:
return Location(**data)
@dataclass
class PostContainer:
title: str
location: Location
date: datetime.date
banner: str
media: Sequence[MediaContainer]
links: Sequence[LinkContainer] = ()
slug: Optional[str] = None
class PostSerializer(msh.Schema):
title = msh.fields.String(required=True)
location = msh.fields.Nested(LocationSeralizer, required=True)
date = msh.fields.Date("%Y-%m-%d", required=True)
banner = msh.fields.URL(required=True)
slug = msh.fields.String(required=False)
links = msh.fields.List(msh.fields.Nested(LinkSerializer), required=False)
media = msh.fields.List(msh.fields.Nested(MediaSerializer), required=True)
@msh.validates_schema
def _unique_anchors(self, data: Dict[str, Any], **kwargs):
anchors = [item.anchor for item in data["media"] if item.anchor is not None]
if len(anchors) != len(set(anchors)):
raise msh.ValidationError(
f"Media anchors used multiple times: {set([item for item in anchors if anchors.count(item) > 1])}"
)
@msh.post_load
def _make_dataclass(self, data: Dict[str, Any], *args, **kwargs) -> PostContainer:
for index, item in enumerate(data["media"]):
item.anchor = item.anchor or index
data["media"][index] = item
return PostContainer(**data)
class ConfigSerializer(msh.Schema):
static = msh.fields.List(msh.fields.String(), required=False)
posts = msh.fields.List(msh.fields.Nested(PostSerializer), required=True)
@msh.validates_schema
def _unique_slugs(self, data: Dict[str, Any], **kwargs):
slugs = [item.slug for item in data["posts"] if item.slug is not None]
if len(slugs) != len(set(slugs)):
raise msh.ValidationError(
f"Post slugs used multiple times: {set([item for item in slugs if slugs.count(item) > 1])}"
)
def get_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument(
"--config", help="Path to the config file", default=(Path.cwd() / "config.yaml")
)
parser.add_argument(
"-c", "--check", action="store_true", help="Check the config without building"
)
parser.add_argument("-p", "--publish", action="store_true", help="Publish the site")
return parser.parse_args()
def main():
args = get_args()
cwd = Path.cwd().resolve()
output = cwd / "build"
explore = output / "explore"
with Path(args.config).resolve().open() as infile:
config = ConfigSerializer().load(yaml.load(infile))
if args.check:
return 0
env = jinja2.Environment(
loader=jinja2.FileSystemLoader(str(cwd / "templates")),
autoescape=jinja2.select_autoescape(["html", "xml"]),
)
output.mkdir(exist_ok=True)
explore.mkdir(exist_ok=True)
index = env.get_template("index.html.j2").render(config=config)
with (explore / "index.html").open("w") as outfile:
outfile.write(index)
sitemap = env.get_template("sitemap.xml.j2").render(config=config)
with (output / "sitemap.xml").open("w") as outfile:
outfile.write(sitemap)
for static in config["static"]:
dest = Path(output / static).resolve()
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(static, str(output / static), follow_symlinks=True)
post_template = env.get_template("post.html.j2")
for post_data in config["posts"]:
post = post_template.render(post=post_data)
with (explore / f"{post_data.slug}.html").open("w") as outfile:
outfile.write(post)
nginx = env.get_template("nginx.conf.d.j2").render(config=config)
with (cwd / "nginx.conf").open("w") as outfile:
outfile.write(nginx)
if __name__ == "__main__":
sys.exit(main())