Rendering Planners
The Planner class ties a Jinja2/HTML template together with
a Calendar and template parameters, then renders the result
to HTML or PDF.
Creating a Planner
from pyplanner import Calendar, Planner
cal = Calendar()
planner = Planner("planners/demo/demo.html", calendar=cal)
The template path must point to a Jinja2/HTML file. The Jinja2 environment is configured with:
%%as the line statement prefix##as the line comment prefixtrim_blocksandlstrip_blocksenabledAutoescaping for HTML
The template directory (parent of the template file) is used as the Jinja2 file-system loader root.
Template parameters
Templates may accept parameters defined in a params.xml file next to the
template. Params loads the schema and produces a
SimpleNamespace tree:
from pyplanner import Params
params = Params.load_xml("planners/demo/params.xml")
ns = params.apply()
print(ns.year) # 2026
print(ns.month) # 1
print(ns.day_off_color) # #C00000
Override individual values with -D-style strings:
ns = params.apply(["year=2027", "day_off_color=#0000FF"])
print(ns.year) # 2027
Nested parameters use dot notation:
# Given a params.xml with <colors><primary>#FFF</primary></colors>
ns = params.apply(["colors.primary=#000"])
print(ns.colors.primary) # #000
The params.xml format
The XML schema uses <params> as the root element. Leaf parameters have a
type attribute (str, int, float or bool) and their text
content is the default value. Namespace elements group related parameters under
a nested namespace:
<?xml version="1.0" encoding="UTF-8"?>
<params>
<year type="int" help="Planner year">2026</year>
<accent help="Primary accent color">#4A90D9</accent>
<show_notes type="bool" help="Include notes">yes</show_notes>
<colors help="Brand colors">
<primary help="Primary brand color">#4A90D9</primary>
<weekend help="Weekend highlight">#FDD</weekend>
</colors>
</params>
Rules:
Element names must be valid Python identifiers (use underscores, not hyphens).
Omitting
typedefaults tostr.Boolean values accept
true/false,yes/no,y/n,on/off,1/0(case-insensitive).Use
<![CDATA[...]]>for values containing XML special characters (e.g. inline SVG).
Programmatic schema
You can also build a Params from a dict without an XML file:
from pyplanner import Params
params = Params({
"year": {"type": "int", "default": 2026},
"accent": {"type": "str", "default": "#4A90D9"},
})
ns = params.apply(["year=2027"])
Rendering to HTML
Planner.html() renders the template and returns
an HTML string:
html = planner.html(base="planners/demo")
with open("demo.html", "w", encoding="utf-8") as f:
f.write(html)
The base parameter controls how asset paths (images, CSS, fonts) are
resolved. Templates reference assets as {{ base }}/assets/.... When base
is a relative path, assets resolve relative to wherever the output HTML is
saved. When base is None, it defaults to the template directory’s
file:// URI.
base is a render-time parameter rather than a constructor argument because
the correct value depends on where the output is written, not on the template
location. When generating HTML to a file, base should be a relative path
from the output to the template directory. When generating a PDF, it should be a
file:// URI. The --watch mode needs yet another value (relative to the
livereload server root). Making it a render-time parameter lets the same
Planner instance produce output for different contexts without
reconstruction.
Rendering to PDF
Planner.pdf() launches a headless Chromium
browser via Playwright, loads the rendered HTML and prints it to PDF:
pdf_bytes = planner.pdf()
with open("demo.pdf", "wb") as f:
f.write(pdf_bytes)
The PDF renderer:
Routes
file://requests to the local file system so images and fonts load correctly.Waits for web fonts to finish loading before printing.
Respects CSS
@pagerules for page size and margins.Extracts
.pageelement IDs and adds year/month bookmarks to the PDF outline automatically.
Page size and margins are controlled entirely by CSS @page rules in the
template’s stylesheet, not by API parameters. An earlier version accepted
margin_top, margin_bottom, etc. as parameters to pdf(). This created
two competing sources of truth: the CSS and the Python API. Removing the API
parameters makes CSS the single source for page geometry, which is what
designers expect. Playwright’s prefer_css_page_size=True flag ensures the
browser respects the template’s @page declarations.
Template context variables
Every template receives four variables:
baseBase URL string for resolving asset paths.
calendarThe
Calendarinstance. Callcalendar.year(n)to build a year. Accesscalendar.weekdaysfor the ordered weekday list.langLanguage code string (e.g.
"en").paramsThe
SimpleNamespacetree fromparams.xml.
Live preview
For interactive design work, the watch() function starts a
livereload server that rebuilds the HTML on every file change:
from pyplanner import Planner, watch
planner = Planner("planners/demo/demo.html")
watch(planner, "demo.html")
This is equivalent to running pyplanner planners/demo --watch on the command
line.
Warning
The livereload package is imported lazily inside watch() rather than
at module level. Importing it triggers logging.basicConfig() which
alters the global logging state of the host application. By deferring the
import to call time, the side effect is confined to the moment the user
explicitly opts into live preview, leaving the logging setup untouched for
all other code paths. If you use pyplanner as a library, be aware that
calling watch() will trigger that side effect and may
affect your log handlers.
API reference
- class pyplanner.Planner(path: str | PathLike[str], calendar: Calendar | None = None, params: SimpleNamespace | None = None)
Render a Jinja2/HTML planner template into HTML or PDF.
- Parameters:
path – Path to the Jinja2/HTML template file.
calendar –
Calendarinstance used for template rendering.params – Template parameters namespace. Passed to templates as
params. Defaults to an empty namespace.
- html(base: str | None = None) str
Render the template and return the resulting HTML string.
- Parameters:
base – Base URL used to resolve assets paths. If not provided, the planner directory is used. During live reloading, browser doesn’t generate requests for file:// URLs. This parameter is used to provide a base URL relative to the output directory. In this case preview in browser works correctly.
- Returns:
Rendered HTML.
- class pyplanner.Params(schema: dict[str, Any])
Typed parameter schema with defaults.
Use
load_xml()to construct from an XML file or pass a schema dict directly for programmatic / test usage.A schema is a nested dict where each leaf is a dict with keys
"type"(str) and"default"(parsed value orNone), and each namespace is a dict whose values are either leaves or nested namespace dicts.- Parameters:
schema – Nested parameter definition dict.
- static load_xml(path: str | PathLike[str]) Params
Construct a
Paramsfrom an XML file.The XML schema uses
<params>as the root element. Each child element defines a parameter or a namespace:Leaf parameter - has a
typeorhelpattribute and no child elements. The element’s text content is the default value (use CDATA for values containing XML special characters). Thehelpattribute is a human-readable description. Omittingtypedefaults tostr. Empty or absent text content defaults toNone.Namespace - has child elements and no
typeattribute. An optionalhelpattribute is allowed for documentation. Groups related parameters under a nestedSimpleNamespace.
Allowed types:
str,int,float,bool. Element names must be valid Python identifiers (no hyphens - use underscores).Example
params.xml:<?xml version="1.0" encoding="UTF-8"?> <params> <year type="int" help="Planner year"> 2026 </year> <accent help="Primary accent color"> #4A90D9 </accent> <show_notes type="bool" help="Include notes"> yes </show_notes> <colors help="Brand colors"> <primary help="Primary brand color"> #4A90D9 </primary> <weekend help="Weekend highlight"> #FDD </weekend> </colors> </params>- Parameters:
path – Path to a
params.xmlfile.- Returns:
New
Paramsinstance.- Raises:
ValueError – On malformed XML structure or invalid parameter names.
- apply(defines: list[str] | None = None) SimpleNamespace
Build a namespace from defaults and provided defines.
- Parameters:
defines – List of
KEY=VALUEstrings. Dot notation addresses nested namespaces (e.g.colors.primary=#FFF).- Returns:
SimpleNamespacetree with all parameters resolved.- Raises:
ValueError – On unknown keys or type mismatches.
- pyplanner.params.parse_bool(value: str) bool
Parse a boolean string (case-insensitive).
Accepted truthy values:
true,yes,y,on,1. Accepted falsy values:false,no,n,off,0.- Parameters:
value – String to parse.
- Returns:
Parsed boolean.
- Raises:
ValueError – If value is not a recognized boolean string.