Project Layout
Source layout
The project uses the src/ layout recommended by the Python Packaging
Authority. The package source lives under src/pyplanner/ and is never
importable directly from the repository root - you must install the package
first (pip install -e .). This prevents accidental imports of the
development tree in tests or scripts and ensures the installed package is always
what gets tested.
feather-flow/
pyproject.toml Build metadata and tool config
README.rst Project README (included in Sphinx docs)
src/
pyplanner/
__init__.py Public API exports
__main__.py CLI entry point
calendar.py Calendar, Year, Month, Day
dayinfo.py DayInfo, DayInfoProvider
lang.py Lang registry
liveserver.py watch() live-reload server
params.py Params XML schema and -D overrides
pdfbookmarks.py PDF outline/bookmarks
pdfopt.py PDF optimization (image dedup)
planner.py Planner (Jinja2 + Playwright)
weekday.py WeekDay and country rules
providers/
__init__.py Re-exports built-in providers
isdayoff.py IsDayOffProvider (RU, BY, KZ, UZ, GE)
nagerdate.py NagerDateProvider (100+ countries)
tracker/
__init__.py setup_tracker(), tracker() singleton
protocol.py ProgressTracker protocol
base.py BaseTracker with refresh thread
simple.py SimpleProgressTracker (non-TTY)
tqdm.py TqdmTracker (TTY progress bar)
quiet.py QuietTracker (no-op)
tests/ One test file per source module
planners/ Self-contained planner templates
docs/ Sphinx documentation
Self-contained planner directories
Each planner lives in its own directory under planners/ with the template,
params.xml and an assets/ folder:
planners/ff-2026/
ff-2026.html
params.xml
assets/
ff-2026.css
cover.png
...
Earlier versions kept templates and assets in separate top-level directories. This made it hard to copy or share a planner as a unit. Moving to self-contained directories means you can zip a folder and hand it to someone - everything needed to render the planner is inside. In watch mode whole planner directory is watched for changes and the HTML is regenerated automatically.
The trade-off is that asset paths in templates must use
{{ base }}/assets/... instead of relative paths. The base variable is
injected at render time and points to the template directory.
Dependencies
All runtime dependencies (jinja2, livereload, pikepdf, playwright, tqdm) are
listed under [project.dependencies] in pyproject.toml, not as optional
extras.
An earlier version split them into [project.optional-dependencies] groups
(full, pdf, etc.). This caused silent degradation when users forgot to
install the right extra - import pikepdf would fail at runtime with a
confusing ModuleNotFoundError deep inside a PDF generation call.
Making everything mandatory means pip install pyplanner gives you a fully
working package. The install size increase is acceptable because all
dependencies are needed for the primary use case (PDF generation).
Development tools (pytest, ruff, mypy, Sphinx, pre-commit) live in the [dev]
optional extra because they are needed by package developers only.
Module dependency graph
The modules have a clear dependency hierarchy. Leaf modules at the bottom have no internal dependencies; higher modules compose them.
__main__
|
+-- Planner -----+-- Calendar --+-- DayInfoProvider
| | | (+ DayInfo)
| | +-- Lang
| | +-- WeekDay
| |
| +-- pdfbookmarks
| +-- tracker
|
+-- Params
+-- pdfopt
+-- liveserver --+-- Planner
+-- Params
__main__ is the CLI orchestrator. It builds a Calendar,
a Planner and wires them together based on command-line
arguments.
planner depends on calendar, lang and pdfbookmarks but does
not depend on pdfopt - optimization is applied by the caller (__main__
or user code), keeping the rendering path clean. PDF bookmarks are generated
together with the PDF, because they are extracted from the HTML page IDs.
liveserver imports Planner and
Params because it rebuilds the HTML on file changes and
needs to re-parse params.xml each time.
The tracker sub-package is used throughout but has no dependencies on the
rest of pyplanner.
What each module does
- calendar.py
Wraps the stdlib
calendarmodule to buildYear,MonthandDayobjects enriched with localized names and optional holiday data from aDayInfoProvider.- dayinfo.py
Defines the
DayInfodataclass and theDayInfoProviderabstract base class. Also provides the plugin loader that discovers providers by duck typing from arbitrary modules.- weekday.py
WeekDaycarries a day’s localized name, abbreviation and off-day flag. Country lookup tables determine first-weekday and weekend rules for 100+ countries.- lang.py
Langis a frozen dataclass with a static registry. Built-in languages (en, ru, kr) are registered at import time. Alias support (ko->kr) avoids duplicating data.- params.py
Paramsloads a typed XML schema, validates names and types, and produces aSimpleNamespacetree.-D KEY=VALUEoverrides walk the dot-path and set values with type coercion.- planner.py
Plannersets up a Jinja2 environment with custom delimiters (%%/##), renders templates withbase,calendar,langandparamsas context, and optionally prints to PDF via Playwright. PDF bookmarks are extracted from.pageelement IDs.- pdfopt.py
Post-processing for Chromium-generated PDFs. Deduplicates Image XObjects by SHA-256 hash, strips obsolete
/ProcSetarrays, deduplicates Form XObjects, and recompresses with object streams.- pdfbookmarks.py
Inserts PDF outline entries via pikepdf. Supports multi-level bookmarks by specifying a parent path.
- liveserver.py
Wraps the livereload library to watch the template directory and rebuild HTML on changes. Imports livereload lazily to avoid its
logging.basicConfig()side effect.- providers/isdayoff.py
Fetches a binary workday/off-day string from isdayoff.ru for Russia, Belarus, Kazakhstan, Uzbekistan and Georgia.
- providers/nagerdate.py
Fetches public holiday JSON from the Nager.Date API for 100+ countries.
- tracker/
A
ProgressTrackerprotocol with three implementations (quiet, simple, tqdm) and a module-level singleton accessed viatracker().