Skip to content

Commit

Permalink
Merge pull request #58 from lkluft/refactor-extensions
Browse files Browse the repository at this point in the history
Refactor extensions
  • Loading branch information
lkluft authored Aug 1, 2024
2 parents 7cf59b5 + a71b8ab commit 7628114
Show file tree
Hide file tree
Showing 8 changed files with 241 additions and 110 deletions.
3 changes: 3 additions & 0 deletions orcestra_book/_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ sphinx:
local_extensions:
apastyle: _ext/
bracket_citation_style: _ext/
campaign_logos: _ext/
flight_categories: _ext/
front: _ext/
reports: _ext/
config:
bibtex_default_style: myapastyle
Expand Down
34 changes: 34 additions & 0 deletions orcestra_book/_ext/campaign_logos.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import pathlib
from docutils import nodes

from sphinx.util.docutils import SphinxRole


class LogoRole(SphinxRole):
"""Add a small campaign logo to the upper-right corner of a page."""

def run(self):
src = pathlib.Path(self.env.srcdir)

logo_path = list((src / "logos").glob(f"*_{self.text}.svg"))
doc_path = pathlib.Path(self.env.doc2path(self.env.docname))

if len(logo_path) == 0:
raise Exception(f"No logo found for {self.text}")
else:
rel_path = logo_path[0].relative_to(doc_path.parent, walk_up=True)

node = nodes.image(uri=rel_path.as_posix(), alt=f"{self.text} logo")
node["classes"].append("campaign-logo")

return [node], []


def setup(app):
app.add_role("logo", LogoRole())

return {
"version": "0.1",
"parallel_read_safe": True,
"parallel_write_safe": True,
}
71 changes: 71 additions & 0 deletions orcestra_book/_ext/flight_categories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import pathlib
from docutils import nodes
from functools import lru_cache

import yaml
from sphinx.util.docutils import SphinxRole


@lru_cache
def load_frontmatter(path):
"""Load and return the front matter section of a YAML file."""
with open(path, "r") as fp:
frontmatter = next(yaml.safe_load_all(fp))

return frontmatter


@lru_cache
def create_flight_badge(src, cat_id):
"""Return an HTML node based on a flight category id."""
with open(src / "reports" / "flight_categories.yaml", "r") as fp:
cat_tier = {
key: attrs["tier"]
for key, attrs in yaml.safe_load(fp)["categories"].items()
}.get(cat_id, "unknown")

span = f'<span class="badge cat-{cat_tier} cat-{cat_id}">{cat_id}</span>'
href = f'<a href="https://orcestra-campaign.org/search.html?q={cat_id}">{span}</a>'
node = nodes.raw(
text=href,
format="html",
)

return node


class FlightCategoryRole(SphinxRole):
"""Create a badge based on a given flight category."""
def run(self):
src = pathlib.Path(self.env.srcdir)
node = create_flight_badge(src, self.text)

return [node], []


class BadgesRole(SphinxRole):
"""Create a list of badges based on all categories defined in YAML fron matter."""
def run(self):
src = pathlib.Path(self.env.srcdir)
fm = load_frontmatter(self.env.doc2path(self.env.docname))
categories = fm.get("categories", [])

node_list = [create_flight_badge(src, cat_id) for cat_id in categories]

node = nodes.raw(
text=" ".join(n.astext() for n in node_list),
format="html",
)

return [node], []


def setup(app):
app.add_role("flight-cat", FlightCategoryRole())
app.add_role("badges", BadgesRole())

return {
"version": "0.1",
"parallel_read_safe": True,
"parallel_write_safe": True,
}
32 changes: 32 additions & 0 deletions orcestra_book/_ext/front.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from docutils import nodes
from functools import lru_cache

import yaml
from sphinx.util.docutils import SphinxRole


@lru_cache
def load_frontmatter(path):
"""Load and return the front matter section of a YAML file."""
with open(path, "r") as fp:
frontmatter = next(yaml.safe_load_all(fp))

return frontmatter


class FrontmatterRole(SphinxRole):
def run(self):
"""Access variables defined in document front matter."""
fm = load_frontmatter(self.env.doc2path(self.env.docname))

return nodes.raw(text=fm[self.text]), []


def setup(app):
app.add_role("front", FrontmatterRole())

return {
"version": "0.1",
"parallel_read_safe": True,
"parallel_write_safe": True,
}
167 changes: 60 additions & 107 deletions orcestra_book/_ext/reports.py
Original file line number Diff line number Diff line change
@@ -1,155 +1,108 @@
#!/usr/bin/env python3
import os
import pathlib
import subprocess
from datetime import datetime
from docutils import nodes
from functools import lru_cache, partial
import re
from collections import defaultdict
from functools import lru_cache

import yaml
from jinja2 import Template
from sphinx.util.docutils import SphinxRole


@lru_cache
def load_frontmatter(path, derive_flight=False):
def load_frontmatter(path):
"""Load and return the front matter section of a YAML file."""
print(path)
with open(path, "r") as fp:
frontmatter = next(yaml.safe_load_all(fp))

if derive_flight:
takeoff = frontmatter["takeoff"]
landing = frontmatter["landing"]

frontmatter["expr_date"] = takeoff.strftime("%d %B %Y")
frontmatter["expr_takeoff"] = takeoff.strftime("%X")
frontmatter["expr_landing"] = landing.strftime("%X")
frontmatter["expr_categories"] = map(
lambda s: f"{{flight-cat}}`{s}`", frontmatter.get("categories", [])
)
frontmatter["filepath"] = path.as_posix()

return frontmatter


@lru_cache
def create_flight_badge(src, cat_id):
"""Return an HTML node based on a flight category id."""
with open(src / "reports" / "flight_categories.yaml", "r") as fp:
cat_tier = {
key: attrs["tier"]
for key, attrs in yaml.safe_load(fp)["categories"].items()
}.get(cat_id, "unknown")

span = f'<span class="badge cat-{cat_tier} cat-{cat_id}">{cat_id}</span>'
href = f'<a href="https://orcestra-campaign.org/search.html?q={cat_id}">{span}</a>'
node = nodes.raw(
text=href,
format="html",
)

return node


class FlightCategoryRole(SphinxRole):
def run(self):
src = pathlib.Path(self.env.srcdir)
node = create_flight_badge(src, self.text)

return [node], []

def fpath2id(fpath):
"""Extract the ID (e.g. HALO-20240801a) from a given file path."""
return pathlib.Path(fpath).stem

class BadgesRole(SphinxRole):
def run(self):
src = pathlib.Path(self.env.srcdir)
fm = load_frontmatter(self.env.doc2path(self.env.docname))
categories = fm.get("categories", [])

node_list = [create_flight_badge(src, cat_id) for cat_id in categories]
def collect_all_metadata(src, subdirs=("plans", "reports"), pattern="*.md"):
"""Collect front matter from Markdown files in various given locations."""
metadata = defaultdict(dict)
for subdir in subdirs:
for src_file in (src / subdir).glob(pattern):
subdir = (
subdir[:-1] if subdir.endswith("s") else subdir
) # e.g. plans -> plan
metadata[fpath2id(src_file)][subdir] = load_frontmatter(src_file)

node = nodes.raw(
text=" ".join(n.astext() for n in node_list),
format="html",
)
return metadata

return [node], []

def consolidate_metadata(src, metadata):
"""Merge duplicated data from flight plans and reports."""
latest_source = "report" if "report" in metadata else "plan"

class FrontmatterRole(SphinxRole):
def run(self):
"""Access variables defined in document front matter."""
fm = load_frontmatter(self.env.doc2path(self.env.docname))
for key in ("takeoff", "landing", "crew", "categories", "nickname"):
metadata[key] = metadata[latest_source][key]

return nodes.raw(text=fm[self.text]), []
metadata["pi"] = [
member["name"] for member in metadata["crew"] if member["job"].lower() == "pi"
][0]

# Collect relative Sphinx links to flight plans and reports
refs = []
for k, v in metadata.items():
if isinstance(v, dict) and "filepath" in v:
relpath = pathlib.Path(v["filepath"]).relative_to(
src / "operation", walk_up=True
)
refs.append(f"[{k}]({relpath})")

class LogoRole(SphinxRole):
"""Add a small campaign logo to the upper-right corner of a page."""
def run(self):
src = pathlib.Path(self.env.srcdir)
metadata["refs"] = refs

logo_path = list((src / "logos").glob(f"*_{self.text}.svg"))
doc_path = pathlib.Path(self.env.doc2path(self.env.docname))
return metadata

if len(logo_path) == 0:
raise Exception(f"No logo found for {self.text}")
else:
rel_path = logo_path[0].relative_to(doc_path.parent, walk_up=True)

node = nodes.image(uri=rel_path.as_posix(), alt=f"{self.text} logo")
node['classes'].append('campaign-logo') # Ad

return [node], []


def collect_halo_refs(src, flight_id):
refs = ", ".join(
f"[{t}](../{t}s/{flight_id})" for t in ("plan", "report")
if (src / f"{t}s" / f"{flight_id}.md").is_file()
)

return f"{flight_id} ({refs})"


def write_flight_table(app):
def write_ship_table(app):
"""Collect all reports from RV METEOR and create an overview table."""
src = pathlib.Path(app.srcdir)
flights = (src / "reports").glob("HALO-[0-9]*[a-z].md")

func = partial(load_frontmatter, derive_flight=True)
frontmatters = {fm["flight_id"]: fm for fm in map(func, sorted(flights))}
metadata = collect_all_metadata(src)

for flight_id in frontmatters:
frontmatters[flight_id]["expr_refs"] = collect_halo_refs(src, flight_id)
regex = re.compile("METEOR-[0-9]*")
# For now, only shipd **reports** are expected. May need to be adapted in future.
frontmatters = {k: v["report"] for k, v in metadata.items() if regex.match(k)}

with open(src / "_templates" / "operation_halo.md", "r") as fp:
with open(src / "_templates" / "operation_rvmeteor.md", "r") as fp:
templ = fp.read()

with open(src / "operation" / "halo.md", "w") as fp:
with open(src / "operation" / "rvmeteor.md", "w") as fp:
t = Template(templ)
fp.write(t.render(flights=frontmatters))
fp.write(t.render(reports=frontmatters))


def write_ship_table(app):
src = pathlib.Path(app.srcdir)
reports = (src / "reports").glob("METEOR-[0-9]*.md")
def write_flight_table(app=None):
"""Collect all reports from HALO and create an overview table."""
# src = pathlib.Path(app.srcdir)
src = pathlib.Path("orcestra_book/")
metadata = collect_all_metadata(src)

frontmatters = {fm["report_id"]: fm for fm in map(load_frontmatter, sorted(reports))}
regex = re.compile("HALO-[0-9]*[a-z]")
frontmatters = {
k: consolidate_metadata(src, v) for k, v in metadata.items() if regex.match(k)
}
print(frontmatters["HALO-20240810a"]["refs"])

with open(src / "_templates" / "operation_rvmeteor.md", "r") as fp:
with open(src / "_templates" / "operation_halo.md", "r") as fp:
templ = fp.read()

with open(src / "operation" / "rvmeteor.md", "w") as fp:
with open(src / "operation" / "halo.md", "w") as fp:
t = Template(templ)
fp.write(t.render(reports=frontmatters))
fp.write(t.render(flights=frontmatters))


def setup(app):
app.connect("builder-inited", write_flight_table)
app.connect("builder-inited", write_ship_table)

app.add_role("flight-cat", FlightCategoryRole())
app.add_role("badges", BadgesRole())
app.add_role("front", FrontmatterRole())
app.add_role("logo", LogoRole())

return {
"version": "0.1",
"parallel_read_safe": True,
Expand Down
8 changes: 7 additions & 1 deletion orcestra_book/_templates/operation_halo.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,11 @@
Flight-ID | Date | Takeoff | Landing | PI | Nickname | Categories
--- | --- | --- | --- | --- | --- | ---
{% for k, v in flights.items() -%}
{{ v["expr_refs"] }} | {{ v["expr_date"] }} | {{ v["expr_takeoff"] }} | {{ v["expr_landing"] }} | {{ v["pi"] }} | {{ v["nickname"] }} | {{ v["expr_categories"]|join(' ') }}
{% set c1 = v["refs"]|join(", ") -%}
{% set c2 = v["takeoff"].strftime("%d %B %Y") -%}
{% set c3 = v["takeoff"].strftime("%X") -%}
{% set c4 = v["landing"].strftime("%X") -%}
{% set c5 = v["pi"] -%}
{% set c6 = v["nickname"] -%}
{{ k }} ({{ c1 }}) | {{ c2 }} | {{ c3 }} | {{ c4 }} | {{ c5 }} | {{ c6 }} | {% for cat in v["categories"] %}{flight-cat}`{{cat}}` {% endfor %}
{% endfor -%}
Loading

0 comments on commit 7628114

Please sign in to comment.