Skip to content

Commit

Permalink
Merge branch 'main' into categories_in_project
Browse files Browse the repository at this point in the history
  • Loading branch information
maximlt authored Oct 23, 2024
2 parents 3a6d590 + 9f50843 commit bae6807
Show file tree
Hide file tree
Showing 5 changed files with 222 additions and 59 deletions.
1 change: 0 additions & 1 deletion _extensions/gallery.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import yaml
import glob
from pathlib import Path
import nbformat
import sphinx.util
import re

Expand Down
165 changes: 150 additions & 15 deletions _extensions/nbheader.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,107 @@
import glob
import os

from datetime import datetime
from pathlib import Path

import nbformat
import sphinx.util
import yaml

from dodo import deployment_cmd_to_endpoint

logger = sphinx.util.logging.getLogger('nbheader-extension')
TITLE_TAG = "hv-nbheader-title"


def transform_date(date_obj):
if date_obj == 'NA':
return date_obj
if isinstance(date_obj, str):
date_obj = datetime.strptime(date_obj, '%Y-%m-%d')
return date_obj.strftime('%B %-d, %Y')


def load_authors_mapping(srcdir):
with open(os.path.join(srcdir, 'authors.yml'), 'r') as file:
return yaml.safe_load(file)


def insert_prolog(nb_path, prolog):
def create_header_html(
authors: list[dict[str, str]],
actions: list[dict[str, str]],
created_date,
updated_date,
):
# Title not included in the header HTML but as an extra Markdown
# cell, as otherwise the next-level headings (`## Foo`) are displayed
# as if they were first-level titles.
created_date = transform_date(created_date)
updated_date = transform_date(updated_date)
if updated_date == created_date:
updated_date = None
authors_html = ''.join([
f'''
<div class="d-flex align-items-center">
<a href="{author['github']}" class="hv-nbheader-author-name">
<img src="{author['picture']}" alt="profile" class="hv-nbheader-author-image rounded-circle me-2"><span>{author['name']}</span>
</a>
</div>
'''
for author in authors
])

actions_html = ''.join([
f"""
<a class="mr-1" href="{action["url"]}"{'target="_blank"' if 'download' not in action["text"].lower() else ''}>
<i class="{action["icon"]} me-1"></i>{action["text"]}
</a>
"""
for action in actions
])

return f'''
<div class="hv-nbheader container mb-5">
<div class="hv-nbheader-authors-container mb-2">
{authors_html}
</div>
<div class= "mb-2 opacity-75">
Published: {created_date}{f" · Updated: {updated_date}" if updated_date else ""}
</div>
<hr />
<div class=" hv-nbheader-actions mb-2 mt-2">
{actions_html}
</div>
<hr />
</div>
'''


def insert_html_header(nb_path, html_header):
nb = nbformat.read(nb_path, as_version=4)
first_cell = nb['cells'][0]
prolog = "```{eval-rst}\n" + prolog + "\n```"
prolog_cell = nbformat.v4.new_markdown_cell(source=prolog)
if "```{eval-rst}" in first_cell['source']:
nb['cells'][0] = prolog_cell
tags = first_cell['metadata'].get('tags', [])

if not (title_processed := TITLE_TAG in tags):
parts = first_cell['source'].split('\n')
title = parts[0]
first_cell['source'] = title
tags.append(TITLE_TAG)
first_cell['metadata']['tags'] = tags
if ''.join([s.strip() for s in parts]):
body_cell = nbformat.v4.new_markdown_cell(source='\n'.join(parts[1:]))
nb['cells'].insert(1, body_cell)

html_header = f'```{{raw}} html\n{html_header}\n```'
html_cell = nbformat.v4.new_markdown_cell(source=html_header)

# To handle when the extension is run multiple times.
if not title_processed:
html_cell = nbformat.v4.new_markdown_cell(source=html_header)
nb['cells'].insert(1, html_cell)
else:
nb['cells'].insert(0, prolog_cell)
nb['cells'][1]['source'] = html_header

nbformat.write(nb, nb_path, version=nbformat.NO_CONVERT)


Expand All @@ -29,24 +113,75 @@ def add_nbheader(app):

logger.info('Adding notebook headers...', color='white')

AUTHORS_MAPPING = load_authors_mapping(app.builder.srcdir)
# Get config
gallery_conf = app.config.gallery_conf
sections = gallery_conf['sections']
doc_dir = Path(app.builder.srcdir)
gallery_path = doc_dir / gallery_conf['path']
for section in sections:
prolog = section['prolog']
project_path = gallery_path / section['path']
project_name = section['path']
header_data = section['header']

authors_full = []
authors = header_data['authors']
for author in authors:
author_mapping = AUTHORS_MAPPING.get(author, {})
author_data = {
'name': author_mapping.get('name', author),
'picture': f'https://avatars.githubusercontent.com/{author}?size=48',
'github': f'https://github.com/{author}',
}
authors_full.append(author_data)

app.config.html_static_path.append(
f'gallery/{project_name}/_archive'
)

project_path = gallery_path / project_name
nb_files = glob.glob(os.path.join(project_path, '*.ipynb'))
for nb_file in nb_files:
nb_file = Path(nb_file)
# Used by examples.holoviz.org to link to the viewed notebook
nb_prolog = prolog
if '/notebooks/{template_notebook_filename}' in prolog:
nb_prolog = prolog.format(
template_notebook_filename=nb_file.name,
)
insert_prolog(nb_file, nb_prolog)
deployments = header_data['deployments']
actions = []
for depl in deployments:
if depl['command'] == 'notebook':
text = 'Run notebook'
fa_icon = 'fas fa-play'
url = deployment_cmd_to_endpoint(depl['command'], project_name)
url += f'/notebooks/{nb_file.name}'
elif depl['command'] == 'dashboard':
text = 'Open app(s)'
fa_icon = 'fa-solid fa-table-cells-large'
url = deployment_cmd_to_endpoint(depl['command'], project_name)
else:
continue
ddata = {
'text': text,
'icon': fa_icon,
'url': url,
}
actions.append(ddata)

# App -> Notebook -> Download
if len(actions) == 2 and 'notebook' in actions[0]['text'].lower():
actions = actions[::-1]

download = {
'text': 'Download project',
'icon': 'fas fa-download',
'url': f'../../_static/{project_name}.zip',
}
actions.append(download)

html_header = create_header_html(
authors_full,
actions,
header_data['created'],
header_data['last_updated'],
)

insert_html_header(nb_file, html_header)


def setup(app):
Expand Down
22 changes: 22 additions & 0 deletions doc/_static/css/header.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.hv-nbheader-author-image {
width: 48px;
height: 48px;
}

.hv-nbheader-actions a {
margin-right: 1rem;
}

.hv-nbheader-authors-container {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}

.hv-nbheader {
padding-left: 0px
}

.hv-nbheader a {
text-decoration: none;
}
26 changes: 26 additions & 0 deletions doc/authors.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
jbednar:
name: Jim Bednar
philippjfr:
name: Philipp Rudiger
jlstevens:
name: Jean-Luc Stevens
jsignell:
name: Julia Signell
ablythed:
name: Blythe Davis
scottire:
name: Scott Condron
djfrancesco:
name: François Pacull
TomAugspurger:
name: Tom Augspurger
jbcrail:
name: Joseph Crail
PeterDSteinberg:
name: Peter Steinberg
azaya89:
name: Isaiah Akorita
droumis:
name: Demetris Roumis
maximlt:
name: Maxime Liquet
67 changes: 24 additions & 43 deletions doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@
version = release = '0.1.0'

html_static_path += ['_static']
html_js_files = ['js/filter.js',]
html_theme = 'pydata_sphinx_theme'
html_logo = "_static/holoviz-logo-unstacked.svg"
html_favicon = "_static/favicon.ico"

html_css_files += [
'css/custom.css',
'css/header.css',
]

templates_path.insert(0, '_templates')
Expand Down Expand Up @@ -153,56 +153,27 @@ def gallery_spec(name):
categories = examples_config.get('categories', [])
# TODO: isn't optional
labels = examples_config.get('labels', [])
# TODO: isn't optional
created = examples_config.get('created', 'NA')
# TODO: isn't optional
authors = examples_config.get('maintainers', '')
# TODO: is optional, if not provided is computed
authors = examples_config['maintainers']
# Optional, computed if not provided.
last_updated = examples_config.get('last_updated', '')
if not last_updated:
last_updated = last_commit_date(name, root='..', verbose=False)
title = examples_config.get('title', '') or projname_to_title(spec['name'])
# Default is empty string as deployments is injected into PROLOG_TEMPLATE
deployments = examples_config.get('deployments', '')

if authors:
authors = [AUTHOR_TEMPLATE.format(author=author) for author in authors]
authors = ', '.join(authors)

if deployments:
_formatted_deployments = []
for depl in deployments:
if depl['command'] == 'notebook':
text = 'Run notebook'
material_icon = 'smart_display'
endpoint = deployment_cmd_to_endpoint(depl['command'], name)
# nbsite will look for "/notebooks/{template_notebook_filename}"
# and replace {template_notebook_filename} by the notebook
# filename where the metadata prolog is injected.
endpoint += '/notebooks/{template_notebook_filename}'
elif depl['command'] == 'dashboard':
text = 'Open app(s)'
material_icon = 'dashboard'
endpoint = deployment_cmd_to_endpoint(depl['command'], name)
formatted_depl = DEPLOYMENT_TEMPLATE.format(
text=text, material_icon=material_icon, endpoint=endpoint
)
_formatted_deployments.append(formatted_depl)
deployments = '\n\n'.join(_formatted_deployments)

prolog = PROLOG_TEMPLATE.format(
created=created, authors=authors, last_updated=last_updated,
projectname=name, deployments=deployments,
)

deployments = examples_config.get('deployments', [])
skip = examples_config.get('skip', False)

return {
'path': name,
'title': title,
'description': description,
'labels': labels,
'prolog': prolog,
'header': {
'authors': authors,
'created': created,
'last_updated': last_updated,
'deployments': deployments,
},
'skip': skip,
'last_updated': last_updated,
'categories': categories,
Expand Down Expand Up @@ -354,15 +325,25 @@ def to_gallery_redirects():
],
}

def setup(app):
from nbsite import nbbuild
nbbuild.setup(app)

app.connect("builder-inited", remove_mystnb_static)
def add_filter_js_gallery_index(app, pagename, templatename, context, doctree):
# Only add filter.js to the gallery index page
if pagename != "gallery/index":
return
app.add_js_file("js/filter.js")


def remove_mystnb_static(app):
# Ensure our myst_nb.css is loaded by removing myst_nb static_path
# from config
app.config.html_static_path = [
p for p in app.config.html_static_path if 'myst_nb' not in p
]


def setup(app):
from nbsite import nbbuild
nbbuild.setup(app)

app.connect("builder-inited", remove_mystnb_static)
app.connect("html-page-context", add_filter_js_gallery_index)

0 comments on commit bae6807

Please sign in to comment.