Implement data model for build script

This commit is contained in:
Ethan Paul 2021-04-24 21:01:25 -04:00
parent c14ff1066c
commit 15cb22b955
No known key found for this signature in database
GPG Key ID: D0E2CBF1245E92BF

108
build.py
View File

@ -1,7 +1,11 @@
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
@ -18,21 +22,25 @@ yaml = ruamel.yaml.YAML(typ="safe")
@dataclass
class MediaContainer:
title: str
asset: str
source: str
preview: Optional[str] = None
anchor: Optional[Union[str, int]] = None
source: Optional[str] = None
content: Optional[str] = None
hide_source: bool = False
class MediaSerializer(msh.Schema):
title = msh.fields.String()
asset = msh.fields.URL()
title = msh.fields.String(required=True)
source = msh.fields.String(required=True)
preview = msh.fields.String(required=False)
anchor = msh.fields.String(required=False)
source = msh.fields.URL(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:
@ -42,10 +50,14 @@ class LinkContainer:
class LinkSerializer(msh.Schema):
link = msh.fields.URL()
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
@ -53,8 +65,12 @@ class Location(NamedTuple):
class LocationSeralizer(msh.Schema):
title = msh.fields.String()
link = msh.fields.URL()
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
@ -70,47 +86,93 @@ class PostContainer:
class PostSerializer(msh.Schema):
title = msh.fields.String()
location = msh.fields.Nested(LocationSeralizer)
date = msh.fields.Date()
banner = msh.fields.URL()
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()
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():
cwd = Path.cwd().resolve()
output = cwd / "explore"
args = get_args()
with (cwd / "config.yaml").open() as infile:
config = yaml.load(infile)
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"]),
)
if not output.exists():
output.mkdir()
output.mkdir(exist_ok=True)
explore.mkdir(exist_ok=True)
index = env.get_template("index.html.j2").render(config=config)
with (output / "index.html").open("w") as outfile:
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 (output / f"{post_data['slug']}.html").open("w") as outfile:
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)
@ -119,4 +181,4 @@ def main():
if __name__ == "__main__":
main()
sys.exit(main())