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 argparse
import datetime import datetime
import shutil
import sys
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Any
from typing import Dict
from typing import NamedTuple from typing import NamedTuple
from typing import Optional from typing import Optional
from typing import Sequence from typing import Sequence
@ -18,21 +22,25 @@ yaml = ruamel.yaml.YAML(typ="safe")
@dataclass @dataclass
class MediaContainer: class MediaContainer:
title: str title: str
asset: str source: str
preview: Optional[str] = None
anchor: Optional[Union[str, int]] = None anchor: Optional[Union[str, int]] = None
source: Optional[str] = None
content: Optional[str] = None content: Optional[str] = None
hide_source: bool = False hide_source: bool = False
class MediaSerializer(msh.Schema): class MediaSerializer(msh.Schema):
title = msh.fields.String() title = msh.fields.String(required=True)
asset = msh.fields.URL() source = msh.fields.String(required=True)
preview = msh.fields.String(required=False)
anchor = msh.fields.String(required=False) anchor = msh.fields.String(required=False)
source = msh.fields.URL(required=False)
content = msh.fields.String(required=False) content = msh.fields.String(required=False)
hide_source = msh.fields.Boolean(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 @dataclass
class LinkContainer: class LinkContainer:
@ -42,10 +50,14 @@ class LinkContainer:
class LinkSerializer(msh.Schema): class LinkSerializer(msh.Schema):
link = msh.fields.URL() link = msh.fields.URL(required=True)
title = msh.fields.String(required=False) title = msh.fields.String(required=False)
icon = 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): class Location(NamedTuple):
title: str title: str
@ -53,8 +65,12 @@ class Location(NamedTuple):
class LocationSeralizer(msh.Schema): class LocationSeralizer(msh.Schema):
title = msh.fields.String() title = msh.fields.String(required=True)
link = msh.fields.URL() link = msh.fields.URL(required=True)
@msh.post_load
def _make_dataclass(self, data: Dict[str, Any], *args, **kwargs) -> Location:
return Location(**data)
@dataclass @dataclass
@ -70,47 +86,93 @@ class PostContainer:
class PostSerializer(msh.Schema): class PostSerializer(msh.Schema):
title = msh.fields.String() title = msh.fields.String(required=True)
location = msh.fields.Nested(LocationSeralizer) location = msh.fields.Nested(LocationSeralizer, required=True)
date = msh.fields.Date() date = msh.fields.Date("%Y-%m-%d", required=True)
banner = msh.fields.URL() banner = msh.fields.URL(required=True)
slug = msh.fields.String(required=False) slug = msh.fields.String(required=False)
links = msh.fields.List(msh.fields.Nested(LinkSerializer), 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: def get_args() -> argparse.Namespace:
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument(
"--config", help="Path to the config file", default=(Path.cwd() / "config.yaml")
)
parser.add_argument( parser.add_argument(
"-c", "--check", action="store_true", help="Check the config without building" "-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() return parser.parse_args()
def main(): def main():
cwd = Path.cwd().resolve() args = get_args()
output = cwd / "explore"
with (cwd / "config.yaml").open() as infile: cwd = Path.cwd().resolve()
config = yaml.load(infile) 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( env = jinja2.Environment(
loader=jinja2.FileSystemLoader(str(cwd / "templates")), loader=jinja2.FileSystemLoader(str(cwd / "templates")),
autoescape=jinja2.select_autoescape(["html", "xml"]), autoescape=jinja2.select_autoescape(["html", "xml"]),
) )
if not output.exists(): output.mkdir(exist_ok=True)
output.mkdir() explore.mkdir(exist_ok=True)
index = env.get_template("index.html.j2").render(config=config) index = env.get_template("index.html.j2").render(config=config)
with (explore / "index.html").open("w") as outfile:
with (output / "index.html").open("w") as outfile:
outfile.write(index) 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") post_template = env.get_template("post.html.j2")
for post_data in config["posts"]: for post_data in config["posts"]:
post = post_template.render(post=post_data) 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) outfile.write(post)
nginx = env.get_template("nginx.conf.d.j2").render(config=config) nginx = env.get_template("nginx.conf.d.j2").render(config=config)
@ -119,4 +181,4 @@ def main():
if __name__ == "__main__": if __name__ == "__main__":
main() sys.exit(main())