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 prefix

  • trim_blocks and lstrip_blocks enabled

  • Autoescaping 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 type defaults to str.

  • 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 @page rules for page size and margins.

  • Extracts .page element 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:

base

Base URL string for resolving asset paths.

calendar

The Calendar instance. Call calendar.year(n) to build a year. Access calendar.weekdays for the ordered weekday list.

lang

Language code string (e.g. "en").

params

The SimpleNamespace tree from params.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.

  • calendarCalendar instance 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.

pdf(base: str | None = None) bytes

Render the template and return a PDF as raw bytes.

Parameters:

base – Base URL used to resolve assets paths. If not provided, the planner directory is used.

Returns:

PDF file content as bytes.

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 or None), 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 Params from 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 type or help attribute and no child elements. The element’s text content is the default value (use CDATA for values containing XML special characters). The help attribute is a human-readable description. Omitting type defaults to str. Empty or absent text content defaults to None.

  • Namespace - has child elements and no type attribute. An optional help attribute is allowed for documentation. Groups related parameters under a nested SimpleNamespace.

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.xml file.

Returns:

New Params instance.

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=VALUE strings. Dot notation addresses nested namespaces (e.g. colors.primary=#FFF).

Returns:

SimpleNamespace tree 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.