Skip to content

Commit df6a12b

Browse files
committed
infrastructure framework
1 parent 9cf898b commit df6a12b

File tree

6 files changed

+182
-5
lines changed

6 files changed

+182
-5
lines changed

.pre-commit-config.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ repos:
77
- id: check-docstring-first
88
- id: check-json
99
- id: check-yaml
10-
- id: double-quote-string-fixer
1110

1211
- repo: https://github.com/psf/black
1312
rev: 25.1.0

environment.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ dependencies:
1313
- numpy
1414
- matplotlib
1515
- google-api-python-client
16+
- feedgen
1617
- pip
1718
- pip:
1819
- google-analytics-data

portal/myst.yml

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,34 @@ project:
66
id: 770e49e5-344a-4c46-adaa-3afb060b2085
77
authors: Project Pythia Community
88
github: https://github.com/projectpythia/projectpythia.github.io
9+
plugins:
10+
- type: executable
11+
path: src/blogpost.py
912

1013
toc:
1114
- file: index.md
1215
- file: about.md
13-
- title: Blog
16+
- file: posts/blog.md
1417
children:
15-
# - pattern: posts/*.md
16-
# Temporary until we have blog infrastructure: explicit list of posts by date (newest first)
18+
- title: "2025"
19+
children:
1720
- file: posts/2025/mystification.md
1821
- file: posts/2025/cookoff2025-website.md
1922
- file: posts/2025/binderhub_status.md
2023
- file: posts/2025/new-cookbooks.md
24+
- title: "2024"
25+
children:
2126
- file: posts/2024/cookoff2024-website.md
27+
- title: "2023"
28+
children:
2229
- file: posts/2023/cookoff2024-savethedate.md
2330
- file: posts/2023/fundraiser.md
2431
- file: posts/2023/cookoff2023.md
2532
- file: contributing.md
2633
- file: cookbook-guide.md
2734
- file: quick-cookbook-guide.md
2835
- file: metrics.md
36+
2937
site:
3038
domains: []
3139
options:

portal/posts/blog.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Blog
2+
3+
Below are a few of the latest posts in my blog.
4+
You can see a full list by year to the left.
5+
6+
:::{postlist}
7+
:number: 25
8+
:::

portal/src/blogpost.py

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
#!/usr/bin/env python
2+
import argparse
3+
import json
4+
import sys
5+
from pathlib import Path
6+
7+
import pandas as pd
8+
import unist as u
9+
from feedgen.feed import FeedGenerator
10+
from yaml import safe_load
11+
12+
DEFAULTS = {"number": 10}
13+
14+
root = Path(__file__).parent.parent
15+
16+
# Aggregate all posts from the markdown and ipynb files
17+
posts = []
18+
for ifile in root.rglob("posts/**/*.md"):
19+
if "drafts" in str(ifile):
20+
continue
21+
22+
text = ifile.read_text()
23+
try:
24+
_, meta, content = text.split("---", 2)
25+
except Exception:
26+
print(f"Skipping file with error: {ifile}", file=sys.stderr)
27+
continue
28+
29+
# Load in YAML metadata
30+
meta = safe_load(meta)
31+
meta["path"] = ifile.relative_to(root).with_suffix("")
32+
if "title" not in meta:
33+
lines = text.splitlines()
34+
for ii in lines:
35+
if ii.strip().startswith("#"):
36+
meta["title"] = ii.replace("#", "").strip()
37+
break
38+
39+
# Summarize content
40+
skip_lines = ["#", "--", "%", "++"]
41+
content = "\n".join(
42+
ii
43+
for ii in content.splitlines()
44+
if not any(ii.startswith(char) for char in skip_lines)
45+
)
46+
N_WORDS = 50
47+
words = " ".join(content.split(" ")[:N_WORDS])
48+
meta["content"] = meta.get("description", words)
49+
posts.append(meta)
50+
posts = pd.DataFrame(posts)
51+
posts["date"] = pd.to_datetime(posts["date"]).dt.tz_localize("US/Pacific")
52+
posts = posts.dropna(subset=["date"])
53+
posts = posts.sort_values("date", ascending=False)
54+
55+
# Generate an RSS feed
56+
fg = FeedGenerator()
57+
fg.id("https://projectpythia.org/")
58+
fg.title("Project Pythia blog")
59+
fg.author({"name": "Project Pythia Team", "email": "projectpythia@ucar.edu"})
60+
fg.link(href="https://projectpythia.org/", rel="alternate")
61+
fg.logo("_static/images/logos/pythia_logo-blue-btext.svg")
62+
fg.subtitle("")
63+
fg.link(href="http://chrisholdgraf.com/rss.xml", rel="self")
64+
fg.language("en")
65+
66+
# Add all my posts to it
67+
for ix, irow in posts.iterrows():
68+
fe = fg.add_entry()
69+
fe.id(f'https://projectpythia.org/{irow["path"]}')
70+
fe.published(irow["date"])
71+
fe.title(irow["title"])
72+
fe.link(href=f'https://projectpythia.org/{irow["path"]}')
73+
fe.content(content=irow["content"])
74+
75+
# Write an RSS feed with latest posts
76+
fg.atom_file(root / "atom.xml", pretty=True)
77+
fg.rss_file(root / "rss.xml", pretty=True)
78+
79+
plugin = {
80+
"name": "Blog Post list",
81+
"directives": [
82+
{
83+
"name": "postlist",
84+
"doc": "An example directive for showing a nice random image at a custom size.",
85+
"alias": ["bloglist"],
86+
"arg": {},
87+
"options": {
88+
"number": {
89+
"type": "int",
90+
"doc": "The number of posts to include",
91+
}
92+
},
93+
}
94+
],
95+
}
96+
97+
children = []
98+
for ix, irow in posts.iterrows():
99+
children.append(
100+
{
101+
"type": "card",
102+
"url": f'/{irow["path"].with_suffix("")}',
103+
"children": [
104+
{"type": "cardTitle", "children": [u.text(irow["title"])]},
105+
{"type": "paragraph", "children": [u.text(irow["content"])]},
106+
{
107+
"type": "footer",
108+
"children": [
109+
u.strong([u.text("Date: ")]),
110+
u.text(f'{irow["date"]:%B %d, %Y} | '),
111+
u.strong([u.text("Author: ")]),
112+
u.text(f'{irow["author"]}'),
113+
],
114+
},
115+
],
116+
}
117+
)
118+
119+
120+
def declare_result(content):
121+
"""Declare result as JSON to stdout
122+
123+
:param content: content to declare as the result
124+
"""
125+
126+
# Format result and write to stdout
127+
json.dump(content, sys.stdout, indent=2)
128+
# Successfully exit
129+
raise SystemExit(0)
130+
131+
132+
def run_directive(name, data):
133+
"""Execute a directive with the given name and data
134+
135+
:param name: name of the directive to run
136+
:param data: data of the directive to run
137+
"""
138+
assert name == "postlist"
139+
opts = data["node"].get("options", {})
140+
number = int(opts.get("number", DEFAULTS["number"]))
141+
output = children[:number]
142+
return output
143+
144+
145+
if __name__ == "__main__":
146+
parser = argparse.ArgumentParser()
147+
group = parser.add_mutually_exclusive_group()
148+
group.add_argument("--role")
149+
group.add_argument("--directive")
150+
group.add_argument("--transform")
151+
args = parser.parse_args()
152+
153+
if args.directive:
154+
data = json.load(sys.stdin)
155+
declare_result(run_directive(args.directive, data))
156+
elif args.transform:
157+
raise NotImplementedError
158+
elif args.role:
159+
raise NotImplementedError
160+
else:
161+
declare_result(plugin)

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ select = B,C,E,F,W,T4,B9
77

88
[isort]
99
known_first_party=
10-
known_third_party=
10+
known_third_party=feedgen,pandas,unist,yaml
1111
multi_line_output=3
1212
include_trailing_comma=True
1313
force_grid_wrap=0

0 commit comments

Comments
 (0)