refactor #1

Merged
jim merged 20 commits from refactor into main 2026-02-03 01:08:46 -05:00
43 changed files with 129 additions and 56 deletions
Showing only changes of commit bf0ca26a71 - Show all commits

View File

@ -8,9 +8,9 @@ site_defaults:
pages: ./templates/pages pages: ./templates/pages
sites: sites:
main: main:
build_cache: ./build build_cache: ./site
assets: assets:
- ./assets - /assets
articles: articles:
- ./pages/*.md - ./pages/*.md
resume: resume:
@ -24,7 +24,7 @@ sites:
build_cache: ./build/dogma-jimfinium build_cache: ./build/dogma-jimfinium
web_root: ./dist/dogma-jimfinium web_root: ./dist/dogma-jimfinium
assets: assets:
- '{build_cache}/assets' - assets
articles: articles:
- '{build_cache}/*.md' - '*.md'

55
main.py
View File

@ -1,4 +1,6 @@
import os import os
import glob
import shutil
import subprocess import subprocess
import markdown import markdown
import yaml import yaml
@ -30,7 +32,7 @@ class ArticleMetadata(pydantic.BaseModel):
title: str title: str
author: Optional[str] = config.get('author') author: Optional[str] = config.get('author')
date: date date: date
lastmod: Optional[date] lastmod: Optional[date] = None
published: bool published: bool
tags: list tags: list
thumbnail: Optional[str] = None thumbnail: Optional[str] = None
@ -73,20 +75,13 @@ def format_html_template(template: str, **kwargs) -> str:
return html return html
REPOS = [
'ssh://gitea/jim/resume.git',
'ssh://gitea/jim/dogma-jimfinium.git'
]
run = lambda cmd: subprocess.run(cmd.split(' '), stdout = subprocess.PIPE, stderr = subprocess.PIPE) run = lambda cmd: subprocess.run(cmd.split(' '), stdout = subprocess.PIPE, stderr = subprocess.PIPE)
def update_git_repos(repos: list) -> None: def pull_git_repo(repo: str, build_cache: str) -> None:
'''Pulls updates to repos in the build directory, or clones them if they don't exist.''' '''Pulls/clones a repo into the build cache directory.'''
for repo in repos: if os.path.exists(f'{build_cache}/.git'):
local_path = 'build/'+repo.split('/')[-1].strip('.git') run(f'git -C {build_cache} pull origin')
print(local_path) else:
if os.path.exists(f'{local_path}/.git'): run(f'git clone {repo} {build_cache}')
run(f'git -C {local_path} pull origin')
else:
run(f'git clone {repo} {local_path}')
def load_partials() -> dict: def load_partials() -> dict:
@ -147,7 +142,7 @@ def build_blog_archive(
# Add each article as a list item to an unordered list. # Add each article as a list item to an unordered list.
archive_html_content = '<ul>' archive_html_content = '<ul>'
for article, (metadata, contents) in sorted(index.items(), key = lambda item: item[1][0]['date'])[::-1]: for article, (metadata, contents) in sorted(index.items(), key = lambda item: item[1][0].date)[::-1]:
# Generate HTML for the article (including metadata tags). # Generate HTML for the article (including metadata tags).
archive_html_content += format_html_template( archive_html_content += format_html_template(
@ -168,5 +163,35 @@ def build_blog_archive(
return archive_html_page return archive_html_page
def copy_assets(site: SiteConfig):
'''Copies the list of site assets from the build cache to the web root.'''
# Expand any globbed expressions.
expanded_asset_list = []
for a in site.assets:
expanded_asset_list.extend(
# Assets are defined relative to the build cache; construct the full path.
glob.glob(f'{site.build_cache}/{a.lstrip("/")}')
)
for asset in expanded_asset_list:
# Construct the destination path analogous to the source path
# but in the web root instead of the build cache.
destination = f'{site.web_root}/{a.lstrip("/")}'
# Delete existing files.
shutil.rmtree(destination, ignore_errors=True)
# Copy the asset.
if os.path.isdir(asset):
shutil.copytree(asset, destination)
elif os.path.isfile(asset):
shutil.copyfile(asset, destination)
else:
continue
return None
if __name__ == '__main__': if __name__ == '__main__':
pass pass

View File

@ -27,7 +27,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": 11,
"id": "207d2510", "id": "207d2510",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
@ -39,6 +39,7 @@
"import subprocess\n", "import subprocess\n",
"import rfeed\n", "import rfeed\n",
"import pydantic\n", "import pydantic\n",
"import glob\n",
"from typing import Optional, Union, Literal, BinaryIO, Any\n", "from typing import Optional, Union, Literal, BinaryIO, Any\n",
"\n", "\n",
"\n", "\n",
@ -49,7 +50,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": 2,
"id": "4b17a3ed", "id": "4b17a3ed",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@ -72,24 +73,10 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": 3,
"id": "d2361c42", "id": "d2361c42",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [],
{
"ename": "ValidationError",
"evalue": "2 validation errors for ArticleMetadata\ndate\n Input should be a valid string [type=string_type, input_value=datetime.date(2026, 1, 30), input_type=date]\n For further information visit https://errors.pydantic.dev/2.12/v/string_type\nlastmod\n Field required [type=missing, input_value={'title': 'Welcome', 'dat...shed': True, 'tags': []}, input_type=dict]\n For further information visit https://errors.pydantic.dev/2.12/v/missing",
"output_type": "error",
"traceback": [
"\u001b[31m---------------------------------------------------------------------------\u001b[39m",
"\u001b[31mValidationError\u001b[39m Traceback (most recent call last)",
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[3]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m metadata, content = \u001b[43mload_markdown\u001b[49m\u001b[43m(\u001b[49m\u001b[33;43m'\u001b[39;49m\u001b[33;43mpages/home.md\u001b[39;49m\u001b[33;43m'\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[32m 2\u001b[39m \u001b[38;5;66;03m# content = content.replace('src=\"assets', 'src=\"../tmp/dogma-jimfinium/assets')\u001b[39;00m\n",
"\u001b[36mFile \u001b[39m\u001b[32m~/projects/shepich.com/main.py:59\u001b[39m, in \u001b[36mload_markdown\u001b[39m\u001b[34m(md)\u001b[39m\n\u001b[32m 56\u001b[39m \u001b[38;5;66;03m# Convert the contents to a HTML string.\u001b[39;00m\n\u001b[32m 57\u001b[39m content = markdown.markdown(raw_article)\n\u001b[32m---> \u001b[39m\u001b[32m59\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mArticleMetadata\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mmetadata\u001b[49m\u001b[43m)\u001b[49m, content\n",
"\u001b[36mFile \u001b[39m\u001b[32m~/projects/shepich.com/.venv/lib/python3.12/site-packages/pydantic/main.py:250\u001b[39m, in \u001b[36mBaseModel.__init__\u001b[39m\u001b[34m(self, **data)\u001b[39m\n\u001b[32m 248\u001b[39m \u001b[38;5;66;03m# `__tracebackhide__` tells pytest and some other tools to omit this function from tracebacks\u001b[39;00m\n\u001b[32m 249\u001b[39m __tracebackhide__ = \u001b[38;5;28;01mTrue\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m250\u001b[39m validated_self = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m__pydantic_validator__\u001b[49m\u001b[43m.\u001b[49m\u001b[43mvalidate_python\u001b[49m\u001b[43m(\u001b[49m\u001b[43mdata\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mself_instance\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[32m 251\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m validated_self:\n\u001b[32m 252\u001b[39m warnings.warn(\n\u001b[32m 253\u001b[39m \u001b[33m'\u001b[39m\u001b[33mA custom validator is returning a value other than `self`.\u001b[39m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[33m'\u001b[39m\n\u001b[32m 254\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mReturning anything other than `self` from a top level model validator isn\u001b[39m\u001b[33m'\u001b[39m\u001b[33mt supported when validating via `__init__`.\u001b[39m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[33m\"\u001b[39m\n\u001b[32m 255\u001b[39m \u001b[33m'\u001b[39m\u001b[33mSee the `model_validator` docs (https://docs.pydantic.dev/latest/concepts/validators/#model-validators) for more details.\u001b[39m\u001b[33m'\u001b[39m,\n\u001b[32m 256\u001b[39m stacklevel=\u001b[32m2\u001b[39m,\n\u001b[32m 257\u001b[39m )\n",
"\u001b[31mValidationError\u001b[39m: 2 validation errors for ArticleMetadata\ndate\n Input should be a valid string [type=string_type, input_value=datetime.date(2026, 1, 30), input_type=date]\n For further information visit https://errors.pydantic.dev/2.12/v/string_type\nlastmod\n Field required [type=missing, input_value={'title': 'Welcome', 'dat...shed': True, 'tags': []}, input_type=dict]\n For further information visit https://errors.pydantic.dev/2.12/v/missing"
]
}
],
"source": [ "source": [
"metadata, content = load_markdown('pages/home.md')\n", "metadata, content = load_markdown('pages/home.md')\n",
"# content = content.replace('src=\"assets', 'src=\"../tmp/dogma-jimfinium/assets')" "# content = content.replace('src=\"assets', 'src=\"../tmp/dogma-jimfinium/assets')"
@ -97,20 +84,12 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": 5,
"id": "0bb15524",
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"id": "ed7b3b2f", "id": "ed7b3b2f",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"html = format_html_template('templates/pages/default.html', content = content, **{'metadata.'+k:v for k,v in metadata.items()}, **PARTIALS)\n", "html = format_html_template('templates/pages/default.html', content = content, metadata = metadata, **PARTIALS)\n",
"with open('dist/home.html', 'w') as f:\n", "with open('dist/home.html', 'w') as f:\n",
" f.write(html)" " f.write(html)"
] ]
@ -151,10 +130,76 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": 12,
"id": "dafd924b",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"['build/dogma-jimfinium/superlock.md',\n",
" 'build/dogma-jimfinium/sustainable-living.md',\n",
" 'build/dogma-jimfinium/stocking-up.md',\n",
" 'build/dogma-jimfinium/set-up-the-toys.md',\n",
" 'build/dogma-jimfinium/babies-love-trash.md',\n",
" 'build/dogma-jimfinium/do-what-you-love.md',\n",
" 'build/dogma-jimfinium/self-care-is-not-selfish.md',\n",
" 'build/dogma-jimfinium/temptation.md',\n",
" 'build/dogma-jimfinium/blowouts.md',\n",
" 'build/dogma-jimfinium/vitamins.md',\n",
" 'build/dogma-jimfinium/gear-for-new-parents.md']"
]
},
"execution_count": 12,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"glob.glob('build/dogma-jimfinium/*.md')"
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "cced61c4",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"'gear-for-new-parents.md'"
]
},
"execution_count": 13,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"os.path.basename( 'build/dogma-jimfinium/gear-for-new-parents.md')"
]
},
{
"cell_type": "code",
"execution_count": 14,
"id": "944a5efd", "id": "944a5efd",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [
{
"ename": "TypeError",
"evalue": "'ArticleMetadata' object is not subscriptable",
"output_type": "error",
"traceback": [
"\u001b[31m---------------------------------------------------------------------------\u001b[39m",
"\u001b[31mTypeError\u001b[39m Traceback (most recent call last)",
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[14]\u001b[39m\u001b[32m, line 36\u001b[39m\n\u001b[32m 32\u001b[39m \u001b[38;5;28;01mwith\u001b[39;00m \u001b[38;5;28mopen\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m'\u001b[39m\u001b[33mdist/dogma-jimfinium/\u001b[39m\u001b[38;5;132;01m{\u001b[39;00marticle_filestem\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m.html\u001b[39m\u001b[33m'\u001b[39m, \u001b[33m'\u001b[39m\u001b[33mw\u001b[39m\u001b[33m'\u001b[39m) \u001b[38;5;28;01mas\u001b[39;00m f:\n\u001b[32m 33\u001b[39m f.write(html)\n\u001b[32m---> \u001b[39m\u001b[32m36\u001b[39m index_html = \u001b[43mbuild_blog_archive\u001b[49m\u001b[43m(\u001b[49m\u001b[43mindex\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mPARTIALS\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 37\u001b[39m \u001b[38;5;66;03m# Write the HTML file to /dist/dogma-jimfinium.\u001b[39;00m\n\u001b[32m 38\u001b[39m \u001b[38;5;28;01mwith\u001b[39;00m \u001b[38;5;28mopen\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m'\u001b[39m\u001b[33mdist/dogma-jimfinium/index.html\u001b[39m\u001b[33m'\u001b[39m, \u001b[33m'\u001b[39m\u001b[33mw\u001b[39m\u001b[33m'\u001b[39m) \u001b[38;5;28;01mas\u001b[39;00m f:\n",
"\u001b[36mFile \u001b[39m\u001b[32m~/projects/shepich.com/main.py:150\u001b[39m, in \u001b[36mbuild_blog_archive\u001b[39m\u001b[34m(index, page_template, li_template, **kwargs)\u001b[39m\n\u001b[32m 148\u001b[39m \u001b[38;5;66;03m# Add each article as a list item to an unordered list.\u001b[39;00m\n\u001b[32m 149\u001b[39m archive_html_content = \u001b[33m'\u001b[39m\u001b[33m<ul>\u001b[39m\u001b[33m'\u001b[39m\n\u001b[32m--> \u001b[39m\u001b[32m150\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m article, (metadata, contents) \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28;43msorted\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mindex\u001b[49m\u001b[43m.\u001b[49m\u001b[43mitems\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mkey\u001b[49m\u001b[43m \u001b[49m\u001b[43m=\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mlambda\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mitem\u001b[49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43mitem\u001b[49m\u001b[43m[\u001b[49m\u001b[32;43m1\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m[\u001b[49m\u001b[32;43m0\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m[\u001b[49m\u001b[33;43m'\u001b[39;49m\u001b[33;43mdate\u001b[39;49m\u001b[33;43m'\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m)\u001b[49m[::-\u001b[32m1\u001b[39m]:\n\u001b[32m 151\u001b[39m \n\u001b[32m 152\u001b[39m \u001b[38;5;66;03m# Generate HTML for the article (including metadata tags).\u001b[39;00m\n\u001b[32m 153\u001b[39m archive_html_content += format_html_template(\n\u001b[32m 154\u001b[39m li_template,\n\u001b[32m 155\u001b[39m article_filestem = article,\n\u001b[32m (...)\u001b[39m\u001b[32m 158\u001b[39m \n\u001b[32m 159\u001b[39m )\n\u001b[32m 160\u001b[39m archive_html_content +=\u001b[33m'\u001b[39m\u001b[33m</ul>\u001b[39m\u001b[33m'\u001b[39m\n",
"\u001b[36mFile \u001b[39m\u001b[32m~/projects/shepich.com/main.py:150\u001b[39m, in \u001b[36mbuild_blog_archive.<locals>.<lambda>\u001b[39m\u001b[34m(item)\u001b[39m\n\u001b[32m 148\u001b[39m \u001b[38;5;66;03m# Add each article as a list item to an unordered list.\u001b[39;00m\n\u001b[32m 149\u001b[39m archive_html_content = \u001b[33m'\u001b[39m\u001b[33m<ul>\u001b[39m\u001b[33m'\u001b[39m\n\u001b[32m--> \u001b[39m\u001b[32m150\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m article, (metadata, contents) \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28msorted\u001b[39m(index.items(), key = \u001b[38;5;28;01mlambda\u001b[39;00m item: \u001b[43mitem\u001b[49m\u001b[43m[\u001b[49m\u001b[32;43m1\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m[\u001b[49m\u001b[32;43m0\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m[\u001b[49m\u001b[33;43m'\u001b[39;49m\u001b[33;43mdate\u001b[39;49m\u001b[33;43m'\u001b[39;49m\u001b[43m]\u001b[49m)[::-\u001b[32m1\u001b[39m]:\n\u001b[32m 151\u001b[39m \n\u001b[32m 152\u001b[39m \u001b[38;5;66;03m# Generate HTML for the article (including metadata tags).\u001b[39;00m\n\u001b[32m 153\u001b[39m archive_html_content += format_html_template(\n\u001b[32m 154\u001b[39m li_template,\n\u001b[32m 155\u001b[39m article_filestem = article,\n\u001b[32m (...)\u001b[39m\u001b[32m 158\u001b[39m \n\u001b[32m 159\u001b[39m )\n\u001b[32m 160\u001b[39m archive_html_content +=\u001b[33m'\u001b[39m\u001b[33m</ul>\u001b[39m\u001b[33m'\u001b[39m\n",
"\u001b[31mTypeError\u001b[39m: 'ArticleMetadata' object is not subscriptable"
]
}
],
"source": [ "source": [
"shutil.rmtree('dist/dogma-jimfinium', ignore_errors=True)\n", "shutil.rmtree('dist/dogma-jimfinium', ignore_errors=True)\n",
"os.makedirs('dist/dogma-jimfinium', exist_ok=True)\n", "os.makedirs('dist/dogma-jimfinium', exist_ok=True)\n",
@ -162,14 +207,17 @@
"\n", "\n",
"index = {}\n", "index = {}\n",
"\n", "\n",
"for article in os.listdir('build/dogma-jimfinium'):\n", "for article in glob.glob('build/dogma-jimfinium/*.md'):\n",
" metadata, content = load_markdown(f'build/dogma-jimfinium/{article}')\n", " metadata, content = load_markdown(article)\n",
"\n",
" if metadata is None:\n",
" print(article)\n",
"\n", "\n",
" # Skip unpublished articles.\n", " # Skip unpublished articles.\n",
" if not metadata.get('published'):\n", " if not metadata.published:\n",
" continue\n", " continue\n",
"\n", "\n",
" article_filestem = os.path.splitext(article)[0]\n", " article_filestem = os.path.splitext(os.path.basename(article))[0]\n",
"\n", "\n",
" # Add the article to the index.\n", " # Add the article to the index.\n",
" index[article_filestem] = (metadata, content)\n", " index[article_filestem] = (metadata, content)\n",
@ -178,8 +226,8 @@
" article_html = format_html_template(\n", " article_html = format_html_template(\n",
" 'templates/components/blog_article.html',\n", " 'templates/components/blog_article.html',\n",
" content = content,\n", " content = content,\n",
" blog_tags = ' '.join(format_blog_tags(metadata['tags'])),\n", " blog_tags = ' '.join(format_blog_tags(metadata.tags)),\n",
" **{'metadata.'+k:v for k,v in metadata.items()}\n", " metadata = metadata\n",
" )\n", " )\n",
" html = format_html_template('templates/pages/default.html', content = article_html, **PARTIALS)\n", " html = format_html_template('templates/pages/default.html', content = article_html, **PARTIALS)\n",
" \n", " \n",
@ -188,7 +236,7 @@
" f.write(html)\n", " f.write(html)\n",
"\n", "\n",
"\n", "\n",
"index_html = build_blog_archive(index, metadata.title = 'Dogma Jimfinium | Index', **PARTIALS)\n", "index_html = build_blog_archive(index, **PARTIALS)\n",
"# Write the HTML file to /dist/dogma-jimfinium.\n", "# Write the HTML file to /dist/dogma-jimfinium.\n",
"with open(f'dist/dogma-jimfinium/index.html', 'w') as f:\n", "with open(f'dist/dogma-jimfinium/index.html', 'w') as f:\n",
" f.write(index_html)" " f.write(index_html)"