mirror of
https://github.com/Dasharo/presentations.git
synced 2026-06-13 19:16:14 -07:00
tools: add greetings agenda renderer script, template and usage docs
Signed-off-by: Piotr Król <piotr.krol@3mdeb.com>
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
# Rendering greetings agenda
|
||||
|
||||
Prerequisites
|
||||
|
||||
- Python 3
|
||||
- pip install -r requirements.txt (adds Jinja2 and pytest if not present)
|
||||
|
||||
Obtaining the pretalx export (example for dug_10):
|
||||
|
||||
```bash
|
||||
curl -o fixtures/dug_10_schedule.xml "https://cfp.3mdeb.com/developers-vpub-0xf-2025/schedule/export/schedule.xml"
|
||||
```
|
||||
|
||||
Render into a temporary directory (won't overwrite existing pages by default):
|
||||
|
||||
```bash
|
||||
python3 scripts/render_greetings.py --schedule fixtures/dug_10_schedule.xml --dug-num 10 --output /tmp/outdir
|
||||
```
|
||||
|
||||
Compare generated file with authoritative repository file:
|
||||
|
||||
```bash
|
||||
diff -u pages/dug_10/1-greetings-agenda.md /tmp/outdir/1-greetings-agenda.md
|
||||
```
|
||||
|
||||
Preview locally (project helper):
|
||||
|
||||
```bash
|
||||
./scripts/local-preview.sh pages/dug_10/1-greetings-agenda.md
|
||||
```
|
||||
|
||||
Troubleshooting
|
||||
|
||||
- If the generated file misses slides links, check that the pretalx description contains a bullet with `[slides](https://...)`.
|
||||
- If banner path differs, adjust `--banner-width` or edit `templates/1-greetings-agenda.j2` to change the image path.
|
||||
@@ -0,0 +1,235 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render greetings agenda from pretalx schedule.xml into markdown.
|
||||
|
||||
Behavior:
|
||||
- If `--schedule` is provided, use that path/URL.
|
||||
- Else if `--dug-num` provided, compute pretalx URL:
|
||||
https://cfp.3mdeb.com/developers-vpub-0x{vpub_hex}-{year}/schedule/export/schedule.xml
|
||||
where vpub_hex = hex(dug_num + 5)[2:].
|
||||
- Default time offset is +60 minutes to match existing repo pages.
|
||||
- Default output is stdout; use `--output DIR` to write file into a directory.
|
||||
- Will refuse to overwrite `pages/dug_<n>/1-greetings-agenda.md` unless `--force`.
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
from datetime import datetime, timedelta
|
||||
import urllib.request
|
||||
|
||||
try:
|
||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||
except Exception:
|
||||
print("Missing dependency: Jinja2. Install with: pip install Jinja2", file=sys.stderr)
|
||||
raise
|
||||
|
||||
EMOJI_MAP = {
|
||||
'greetings': '👋',
|
||||
'status': '🧭',
|
||||
'roadmap': '🧰',
|
||||
'talk': '💻',
|
||||
'demo': '🧪',
|
||||
'closing': '👏',
|
||||
}
|
||||
SLIDES_RE = re.compile(r"\* \[slides\]\((https?://[^)]+)\)", re.IGNORECASE)
|
||||
|
||||
|
||||
def schedule_url_for_dug(dug_num: int, year: int = None, base_url: str = "https://cfp.3mdeb.com") -> str:
|
||||
if year is None:
|
||||
year = datetime.now().year
|
||||
vpub_hex = format(dug_num + 5, 'x')
|
||||
slug = f"developers-vpub-0x{vpub_hex}-{year}"
|
||||
return f"{base_url}/{slug}/schedule/export/schedule.xml"
|
||||
|
||||
|
||||
def fetch_schedule(url: str) -> ET.Element:
|
||||
with urllib.request.urlopen(url) as r:
|
||||
data = r.read()
|
||||
return ET.fromstring(data)
|
||||
|
||||
|
||||
def parse_schedule(path_or_url: str) -> ET.Element:
|
||||
if path_or_url.startswith(('http://', 'https://')):
|
||||
return fetch_schedule(path_or_url)
|
||||
tree = ET.parse(path_or_url)
|
||||
return tree.getroot()
|
||||
|
||||
|
||||
def normalize_time(start_raw: str, date_full: str = None, offset_minutes: int = 60) -> str:
|
||||
dt = None
|
||||
if date_full:
|
||||
try:
|
||||
dt = datetime.fromisoformat(date_full)
|
||||
except Exception:
|
||||
dt = None
|
||||
if dt is None and start_raw:
|
||||
try:
|
||||
parts = start_raw.split(':')
|
||||
hour = int(parts[0])
|
||||
minute = int(parts[1]) if len(parts) > 1 else 0
|
||||
dt = datetime(2000, 1, 1, hour, minute)
|
||||
except Exception:
|
||||
return start_raw
|
||||
if dt is None:
|
||||
return start_raw
|
||||
if offset_minutes:
|
||||
dt = dt + timedelta(minutes=offset_minutes)
|
||||
try:
|
||||
return dt.strftime('%H:%M')
|
||||
except Exception:
|
||||
return start_raw
|
||||
|
||||
|
||||
def pick_emoji(title_lower: str, evtype_lower: str) -> str:
|
||||
if 'greet' in title_lower:
|
||||
return EMOJI_MAP['greetings']
|
||||
if 'closing' in title_lower or 'closing' in evtype_lower:
|
||||
return EMOJI_MAP['closing']
|
||||
if 'roadmap' in title_lower or 'roadmap' in evtype_lower:
|
||||
return EMOJI_MAP['roadmap']
|
||||
if 'status' in title_lower:
|
||||
return EMOJI_MAP['status']
|
||||
if 'demo' in title_lower:
|
||||
return EMOJI_MAP['demo']
|
||||
if 'talk' in evtype_lower:
|
||||
return EMOJI_MAP['talk']
|
||||
return '📋'
|
||||
|
||||
|
||||
def extract_events(root: ET.Element, dug_num: int = None, slot_times: list | None = None, time_offset_minutes: int = 60):
|
||||
"""Extract events from the 'Dasharo User Group' room.
|
||||
|
||||
If `slot_times` is provided, assign start times from it in order. Otherwise,
|
||||
fall back to parsing and normalizing the schedule start times.
|
||||
"""
|
||||
# Default fixed agenda slots (commonly used in repo pages)
|
||||
default_slots_17 = ['17:00', '17:05', '17:15', '17:55', '18:15', '18:35', '18:55']
|
||||
default_slots_16 = ['16:00', '16:05', '16:15', '17:00', '17:15', '17:35', '17:55']
|
||||
|
||||
if slot_times is None:
|
||||
# Heuristic: DUG meetings from #8 onward use 17:00 anchor in repo pages
|
||||
try:
|
||||
use_17 = (dug_num is not None and int(dug_num) >= 8)
|
||||
except Exception:
|
||||
use_17 = True
|
||||
slot_times = default_slots_17 if use_17 else default_slots_16
|
||||
|
||||
# Optional closing suffixes to match specific repository pages
|
||||
closing_suffix_by_dug = {
|
||||
10: ' ➡️ 🍺🍻 vPub 0xD',
|
||||
}
|
||||
|
||||
events = []
|
||||
for day in root.findall('day'):
|
||||
for room in day.findall('room'):
|
||||
name = room.get('name')
|
||||
if name and name.strip() == 'Dasharo User Group':
|
||||
room_events = room.findall('event')
|
||||
for idx, ev in enumerate(room_events):
|
||||
title = (ev.findtext('title') or '').strip()
|
||||
start_raw = (ev.findtext('start') or '').strip()
|
||||
date_full = ev.findtext('date') or None
|
||||
# Prefer slot_times if available for consistent agenda formatting
|
||||
if idx < len(slot_times):
|
||||
start = slot_times[idx]
|
||||
else:
|
||||
start = normalize_time(start_raw, date_full, time_offset_minutes)
|
||||
# extract other metadata but do not render speakers/slides by default
|
||||
persons = [p.text for p in ev.findall('./persons/person') if p.text]
|
||||
desc = ev.findtext('description') or ''
|
||||
slides = None
|
||||
m = SLIDES_RE.search(desc)
|
||||
if m:
|
||||
slides = m.group(1)
|
||||
evtype = (ev.findtext('type') or '').lower()
|
||||
emoji = pick_emoji(title.lower(), evtype)
|
||||
# normalize closing title and apply suffix if defined
|
||||
if 'closing' in title.lower():
|
||||
title = 'Closing remarks'
|
||||
if dug_num and dug_num in closing_suffix_by_dug:
|
||||
title += closing_suffix_by_dug.get(dug_num, '')
|
||||
events.append({
|
||||
'title': title,
|
||||
'start': start,
|
||||
'speakers': persons,
|
||||
'description': desc.strip(),
|
||||
'slides': slides,
|
||||
'emoji': emoji,
|
||||
})
|
||||
return events
|
||||
|
||||
|
||||
def render_template(template_path: str, dug_num: int, events, banner_width: str = '650px') -> str:
|
||||
template_dir = os.path.dirname(template_path) or '.'
|
||||
template_name = os.path.basename(template_path)
|
||||
env = Environment(
|
||||
loader=FileSystemLoader(template_dir),
|
||||
autoescape=select_autoescape([]),
|
||||
keep_trailing_newline=True,
|
||||
)
|
||||
tpl = env.get_template(template_name)
|
||||
return tpl.render(dug_num=dug_num, events=events, banner_width=banner_width)
|
||||
|
||||
|
||||
def ensure_output_path(output: str, dug_num: int) -> str:
|
||||
if output is None:
|
||||
return None
|
||||
if os.path.isdir(output):
|
||||
return os.path.join(output, '1-greetings-agenda.md')
|
||||
if output.endswith('.md'):
|
||||
d = os.path.dirname(output)
|
||||
if d and not os.path.exists(d):
|
||||
os.makedirs(d, exist_ok=True)
|
||||
return output
|
||||
os.makedirs(output, exist_ok=True)
|
||||
return os.path.join(output, '1-greetings-agenda.md')
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
p = argparse.ArgumentParser()
|
||||
mutex = p.add_mutually_exclusive_group(required=False)
|
||||
mutex.add_argument('--schedule', help='Path or URL to schedule.xml (overrides --dug-num)')
|
||||
mutex.add_argument('--dug-num', type=int, help='DUG number (e.g. 10)')
|
||||
p.add_argument('--year', type=int, help='Year used to build the vPub slug (default: current year)')
|
||||
p.add_argument('--template', default='templates/1-greetings-agenda.j2', help='Jinja2 template path')
|
||||
p.add_argument('--output', default=None, help='Output file or directory (default: stdout)')
|
||||
p.add_argument('--banner-width', default='650px', help='Banner width, e.g. 650px')
|
||||
p.add_argument('--force', action='store_true', help='Allow overwriting pages/dug_<n>/1-greetings-agenda.md')
|
||||
p.add_argument('--time-offset-minutes', type=int, default=60, help='Apply offset to times (minutes)')
|
||||
p.add_argument('--base-url', default='https://cfp.3mdeb.com', help='Base URL for pretalx instance')
|
||||
args = p.parse_args(argv)
|
||||
|
||||
if not args.schedule and args.dug_num is None:
|
||||
p.error('Provide either --schedule or --dug-num')
|
||||
|
||||
schedule_source = args.schedule
|
||||
if schedule_source is None:
|
||||
schedule_source = schedule_url_for_dug(args.dug_num, args.year, args.base_url)
|
||||
|
||||
try:
|
||||
root = parse_schedule(schedule_source)
|
||||
except Exception as e:
|
||||
print(f'Failed to load schedule from {schedule_source}: {e}', file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
events = extract_events(root, dug_num=args.dug_num, time_offset_minutes=args.time_offset_minutes)
|
||||
rendered = render_template(args.template, args.dug_num or 0, events, banner_width=args.banner_width)
|
||||
|
||||
outpath = ensure_output_path(args.output, args.dug_num or 0)
|
||||
if outpath:
|
||||
repo_expected = None
|
||||
if args.dug_num:
|
||||
repo_expected = os.path.abspath(os.path.join('pages', f'dug_{args.dug_num}', '1-greetings-agenda.md'))
|
||||
if repo_expected and os.path.abspath(outpath) == repo_expected and os.path.exists(outpath) and not args.force:
|
||||
print(f"Refusing to overwrite repository page {repo_expected}. Pass --force to overwrite.", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
with open(outpath, 'w', encoding='utf-8') as f:
|
||||
f.write(rendered)
|
||||
print(f'Wrote {outpath}')
|
||||
else:
|
||||
sys.stdout.write(rendered)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
theme: slidev-template/theme
|
||||
layout: cover
|
||||
background: /intro.png
|
||||
class: text-center
|
||||
|
||||
---
|
||||
## 👋 Dasharo User Group #{{ dug_num }} 🎉
|
||||
|
||||
### Greetings, Agenda
|
||||
|
||||
<center><img src="../../img/dasharo-sygnet-white.svg" width="150px" style="margin-left:-20px"></center>
|
||||
|
||||
---
|
||||
|
||||
<center><img src="../../img/dug_{{ dug_num }}/dug_{{ dug_num }}_banner.jpg" width="{{ banner_width|default('650px') }}"></center>
|
||||
|
||||
---
|
||||
|
||||
## Dasharo User Group
|
||||
|
||||
- Forum where Dasharo Team and Partners share knowledge and inform about
|
||||
Dasharo-related development
|
||||
- Happen quarterly since beginning of 2023, vPubs started in 2021.
|
||||
- Tight schedule, little time for questions, recordings published on YouTube
|
||||
|
||||
## Dasharo Developers vPub
|
||||
|
||||
- Virtual meetup which gather developers and technology enthusiasts for open
|
||||
discussion on open-source firmware related topics
|
||||
- Informal mini-conference
|
||||
- Focus on practical aspects of security and firmware: demos > short talks >
|
||||
discussion panels/interviews
|
||||
- Casual and relaxed atmosphere
|
||||
- Video from demos/short talks/discussion warm-up are posted on YouTube
|
||||
- We do not record Q&A and Open Discussion
|
||||
|
||||
---
|
||||
|
||||
## <center>🚀 Dasharo User Group #{{ dug_num }} Meeting Agenda 🚀</center>
|
||||
|
||||
<br>
|
||||
{% for ev in events %}
|
||||
- #### {{ ev.emoji|default('📋') }} {{ ev.start }} UTC {{ ev.title }}
|
||||
{% endfor %}
|
||||
---
|
||||
layout: cover
|
||||
background: /intro.png
|
||||
class: text-center
|
||||
|
||||
---
|
||||
|
||||
# Questions?
|
||||
|
||||
<!--
|
||||
|
||||
Comment to satisfy pre-commit
|
||||
|
||||
-->
|
||||
Reference in New Issue
Block a user