API reference
This page documents the public modules. The everyday names, such as
parse_text, generate_schedule, build_ics, and render_week_pdf, are
re-exported from the top-level writing_schedule package, and their canonical
definitions are shown below under the module that owns them.
Parsing
Read an org table from text into rows of cells.
This is the Python stand-in for elisp org-table-to-lisp. It finds the
first contiguous run of table lines (those starting, after optional leading
whitespace, with |) and returns a list whose elements are either the
sentinel HLINE for a horizontal rule or a list of trimmed cell
strings for a data row.
A horizontal rule is any table line whose first character after the leading
| is - or +, matching org’s org-table-hline-p.
- writing_schedule.orgtable.HLINE = '__HLINE__'
Sentinel marking a horizontal rule row (elisp returns the symbol
hline).
- writing_schedule.orgtable.split_row(line: str) List[str][source]
Split one
| a | b |line into trimmed cells["a", "b"].
- writing_schedule.orgtable.parse_org_table(text: str) List[str | List[str]][source]
Return the first org table in
textas a list of rows.Each row is either
HLINEor a list of trimmed cell strings. Lines outside the first table run are ignored, so a table embedded in a larger org document parses cleanly.
Parse a weekly block table into a ParsedTable.
This is a direct port of writing-schedule--parse plus its helpers. The
four row kinds (header, legend, time-block, section header) are tested in that
order and the first match wins, exactly as in the reference. See the format
spec sections “The weekly block table”.
- writing_schedule.parser.day_offset(cell: str | None) int | None[source]
Return the Monday offset for
cellwhen it names a day, else None.
- writing_schedule.parser.parse_time(cell: str | None) Tuple[str, str] | None[source]
Return (start, end) as zero-padded
HH:MMstrings, or None.The range may appear anywhere inside the cell. No range checking is done, matching the reference; callers may validate separately.
- writing_schedule.parser.minutes_between(start: str, end: str) int[source]
Return the number of minutes between two
HH:MMstrings.
- writing_schedule.parser.parse_table(rows: List[str | List[str]]) ParsedTable[source]
Parse table
rows(fromparse_org_table()) into a ParsedTable.
- writing_schedule.parser.parse_text(text: str) ParsedTable[source]
Convenience: read the first org table in
textand parse it.
Data model for the writing-schedule Python port.
The types here mirror the plists produced by writing-schedule.el:
An
Eventcorresponds to a parsed time block. Its five fields match the reference plist keys:section,:offset,:start,:end, and:letter(see the format spec, tabletab:event).A
ParsedTablecorresponds to the plist returned bywriting-schedule--parsewith keys:events,:legend,:letters, and:columns.A
MapEntrycorresponds to the mapping plists with keys:letter,:code, and:desc.
- class writing_schedule.model.Event(section: str, offset: int, start: str, end: str, letter: str)[source]
Bases:
objectOne time block on one day.
- Variables:
section (str) – The most recent section header, or
"Writing"before any header.offset (int) – Days from Monday (0 = Monday … 6 = Sunday).
end (start,) – Zero-padded
HH:MMstrings.letter (str) – The upper-cased code taken from the day cell.
- section: str
- offset: int
- start: str
- end: str
- letter: str
- class writing_schedule.model.ParsedTable(events: List[Event] = <factory>, legend: Tuple[str, str]]=<factory>, letters: List[str] = <factory>, columns: Tuple[int, int]]=<factory>)[source]
Bases:
objectThe result of parsing a weekly block table.
- legend: List[Tuple[str, str]]
- letters: List[str]
- columns: List[Tuple[int, int]]
Dates
Date helpers.
These use datetime.date rather than the reference’s absolute day
numbers, which is cleaner in Python and gives the same results. English
weekday names are emitted for predictability across machines, as the format
spec recommends.
- writing_schedule.week.to_date(value: date | str) date[source]
Coerce an ISO date string or
datetime.dateto a date.
- writing_schedule.week.resolve_date(value: date | str) date[source]
Return a concrete date for
value.valueis an ISO date string, adatetime.date, or the wordtoday(any case, surrounding space tolerated), which resolves to the current local date. This lets a caller name a specific day or ask for today, which is what the day-scoped sheet commands accept.
- writing_schedule.week.week_monday(value: date | str) date[source]
Return the Monday on or before
value.Any day in a week therefore selects that week.
Schedule and calendar
Generate the dated schedule .org file and its Summary section.
Ports writing-schedule--build-org and writing-schedule--summary plus
the batch mapping helper. See the format spec, “The generated schedule file”.
- writing_schedule.schedule.legend_mapping(letters: List[str], legend: List[Tuple[str, str]]) List[MapEntry][source]
Build a non-interactive mapping for
lettersfromlegend.Each entry has an empty code and a description taken from the legend, so batch generation needs no prompts. Mirrors
writing-schedule--legend-mapping.
- writing_schedule.schedule.build_org(events: List[Event], mapping: List[MapEntry], monday: date, title: str, config: Config | None = None) str[source]
Return the schedule org file body (without the Summary section).
- writing_schedule.schedule.summary(events: List[Event], mapping: List[MapEntry]) str[source]
Return an org Summary section totalling weekly hours per code.
The section carries no timestamp, so a calendar exporter skips it.
- writing_schedule.schedule.schedule_title(monday: date) str[source]
The default schedule title, e.g.
Writing Schedule (week of ...).
- writing_schedule.schedule.generate_schedule(parsed: ParsedTable, week, config: Config | None = None, title: str | None = None) Tuple[date, str, str][source]
Return
(monday, title, body)for the schedule file.weekis any date (or ISO string) inside the target week; it snaps to the Monday.bodyis the full file text including the Summary section.
- writing_schedule.schedule.schedule_filename(monday: date) str[source]
Return the archival file name for the week beginning
monday.
- writing_schedule.schedule.filter_day(parsed: ParsedTable, monday: date, day: date) ParsedTable[source]
Return a copy of
parsedholding onlyday’s blocks.The legend is kept whole, so the key and mapping still describe every code, while events, letters, and columns are narrowed to the one day.
- writing_schedule.schedule.day_schedule_title(day: date) str[source]
The default single-day schedule title, e.g.
... (2026-01-21 Wednesday).
- writing_schedule.schedule.day_schedule_filename(day: date) str[source]
Return the file name for a single day’s schedule,
day-<ISO>.org.The
day-prefix keeps these out of the weekly archive, which lists onlywriting-<Monday>.orgfiles, so a reprinted day never shadows or overwrites the week it belongs to.
- writing_schedule.schedule.generate_day_schedule(parsed: ParsedTable, day, config: Config | None = None, title: str | None = None) Tuple[date, str, str, ParsedTable][source]
Return
(monday, title, body, day_parsed)for one day’s schedule.dayis a date, an ISO string, or the wordtoday.day_parsedis the table narrowed to that day, ready forbuild_ics().bodyis the full file text including the Summary section, with each event stamped on its real date, exactly as in the weekly schedule.
Emit an iCalendar (RFC 5545) file directly, without a third-party library.
The reference implementation hands the generated org file to org’s own
exporter. A second implementation is expected to write the small required
subset itself, because a dependency-free program is easier to install. This
module produces one VEVENT per block, wrapped in a VCALENDAR with a
VTIMEZONE built from zoneinfo (see writing_schedule.vtimezone).
Times are written as local wall-clock values with a TZID parameter, so a
04:00 block stays at 04:00 in every week, and the VTIMEZONE supplies the
offset a client needs to compute the absolute instant. When no zone is
configured the exporter falls back to the reference’s floating-time behaviour.
- writing_schedule.ics.fold_line(line: str) str[source]
Fold a content line at 75 octets with CRLF + single-space continuation.
- writing_schedule.ics.build_ics(parsed: ParsedTable, monday: date, title: str, config: Config | None = None, calname: str | None = None, dtstamp: datetime | None = None) str[source]
Return the iCalendar text for
parsedanchored atmonday.dtstampmay be supplied for reproducible output; it defaults to the current time in UTC.
Build an RFC 5545 VTIMEZONE component from a named zone via zoneinfo.
The reference implementation writes floating local times and names the zone
only through the X-WR-TIMEZONE vendor property. The format spec recommends
attaching an explicit TZID to each DTSTART/DTEND and shipping a
VTIMEZONE so the times are unambiguous. This module derives that
VTIMEZONE from the standard-library zoneinfo database, so daylight
saving is correct without hard-coded offsets.
The daylight-saving transitions for the given year are located by probing the
zone, and each observance carries a RRULE derived from the transition’s
month and weekday position (for example the second Sunday of March), which is
how nearly every civil zone expresses its rule.
Sheets
Draw the printable time-block sheet with ReportLab (the default engine).
This replaces the TeX dependency with a pure-Python one. The sheet is a rectangular grid, so drawing lines and text at absolute coordinates is a natural fit: each planned block is a box that spans its exact time range, rather than snapping to the five-row-per-hour grid the LaTeX/tabular model forces. The sub-rows survive only as light guide lines, which is the improvement the format spec recommends for a canvas renderer.
Layout follows the spec’s geometry: a code key across the top, a Date row and two blank rows, hour labels down a narrow left column, planned blocks drawn as heavy outlined boxes in the first wide (plan) column, and the remaining plan columns left blank for revisions. Each day is two pages, split at the same hour the reference uses; a block reaching a page edge is left open there.
- writing_schedule.sheet.block_box(start: str, end: str, page_lo: int, page_hi: int, grid_top: float, grid_bottom: float)[source]
Return the block’s box geometry on a page, or None if it is off-page.
Pure geometry, factored out so tests can check that a block from t1 to t2 maps to y positions proportional to its time range. Returns a dict with
y_top/y_bottom(points), the clippedstart/end(hours), andopen_top/open_bottomflags for blocks that run past a page edge.
- writing_schedule.sheet.render_week_pdf(parsed: ParsedTable, monday: date, path: str, config: Config | None = None) str[source]
Draw the whole week (two pages per day) into one PDF at
path.
- writing_schedule.sheet.render_day_pdf(parsed: ParsedTable, monday: date, day_date: date, path: str, config: Config | None = None) str[source]
Draw a single day (two pages) into a PDF at
path.A day with no column in the table draws as a blank sheet, so any date, including today, produces a usable plan to fill in by hand.
Shared geometry for the printable time-block sheet.
Both the ReportLab drawer and the LaTeX emitter use these helpers so the two engines agree on which days appear, in what order, and where each block sits.
- writing_schedule.sheet_common.tidy(hhmm: str) str[source]
Drop a leading zero on the hour, so
04:00becomes4:00.
- writing_schedule.sheet_common.page_split(lo: int, hi: int) int[source]
Return the last hour of page one.
Computed as
lo + ceil(total/2) - 1wheretotal = hi - lo + 1, matching the reference. With the defaults (4..23) this gives 13.
- writing_schedule.sheet_common.block_rows(start: str, end: str, subrows: int) Tuple[int, int][source]
Return the (start_row, end_row) grid span for a block.
Ports the reference’s row math. A block occupies rows
start_rowup to but not includingend_row; the guard keeps a sub-row-short block at one row.
- writing_schedule.sheet_common.block_hours(start: str, end: str) Tuple[float, float][source]
Return (start, end) as fractional hours for exact canvas placement.
An end at or before the start is treated as crossing midnight and extends to 24:00, so an overnight block reads as one open-ended box.
- writing_schedule.sheet_common.iter_days(parsed: ParsedTable, monday: date, only: date | None = None) List[Tuple[date, str, List[Event]]][source]
Return (date, “ISO (Weekday)”, events) for each day column, in order.
When
onlyis a date, return just that one day, even when the table has no column for it, in which case the day carries no events and reads as a blank sheet. This is what the day-scoped commands use to print the plan for a single day, which may be today.
- writing_schedule.sheet_common.key_pairs(parsed: ParsedTable, config: Config | None = None) List[Tuple[str, str]][source]
Return (code, description) for the sheet key, using the merged legend.
LaTeX time-block sheet emitter.
A faithful port of the writing-schedule--timeblock-* LaTeX family, kept as
an optional engine (--engine latex) for users who have TeX installed and
want output that is byte-comparable with the Emacs version. The default
engine is ReportLab (writing_schedule.sheet).
- writing_schedule.sheet_latex.render_latex(parsed: ParsedTable, monday: date, config: Config | None = None, day: date | None = None) str[source]
Return a full LaTeX document with a two-page sheet per day.
When
dayis a date, emit only that one day, which is how the day-scoped sheet command prints the plan for a single day.
Write the editable week org file, one booktabs table per day.
Ports writing-schedule--timeblock-org-document. See the format spec,
“The editable week export”. Days appear in day-column order and blocks within
a day are sorted by start time; the Revision column is always empty.
- writing_schedule.sheet_org.build_week_org(parsed: ParsedTable, monday: date, config: Config | None = None, day: date | None = None) str[source]
Return the editable org document as a string.
By default the document covers the whole week, one section per day. When
dayis a date, the document covers only that day, so you can edit and print the plan for a single day.
Configuration and utilities
Configuration defaults for the writing-schedule Python port.
Each field mirrors a defcustom in writing-schedule.el. Grouping them
in one dataclass keeps the generators pure: they take a Config rather
than reading globals, which makes them easy to test.
- class writing_schedule.config.Config(use_todo: bool = True, todo_keyword: str = 'TODO', timezone: str = 'America/Chicago', prodid: str = '-//Blaine Mooers//writing-schedule.py//EN', timeblock_start_hour: int = 4, timeblock_end_hour: int = 23, timeblock_columns: int = 4, timeblock_subrows: int = 5, code_descriptions: Dict[str, str]=<factory>)[source]
Bases:
objectRuntime configuration for schedule, calendar, and sheet generation.
- use_todo: bool = True
When True, each generated event headline carries the TODO keyword.
- todo_keyword: str = 'TODO'
The keyword placed before each event when
use_todois True.
- timezone: str = 'America/Chicago'
IANA time zone used for the calendar. An empty string selects the floating-time behaviour of the reference implementation.
- prodid: str = '-//Blaine Mooers//writing-schedule.py//EN'
PRODID advertised by the exporter.
- timeblock_start_hour: int = 4
- timeblock_end_hour: int = 23
- timeblock_columns: int = 4
One planned column plus (columns - 1) revision columns.
- timeblock_subrows: int = 5
Writing rows per hour.
- code_descriptions: Dict[str, str]
Standing dictionary of code -> description, merged under a table’s own legend (the legend wins). Used by the sheets, not the schedule.
Blank weekly-table scaffold.
Ports writing-schedule--template-string. The scaffold uses single-letter
codes; a user may rename the legend rows and use their own short uppercase
codes (such as EM or EX) in the cells.
- writing_schedule.template.template_string(n: int) str[source]
Return a blank weekly schedule template for
nprojects (1..26).
List archived weekly schedule files, newest first.
Ports writing-schedule--archived-weeks. ISO dates sort lexically, so a
plain descending string sort orders the weeks from most to least recent.
Only writing-YYYY-MM-DD.org names are matched, so calendar exports and
unrelated org files are ignored.
Command line
Command-line front end.
Subcommands mirror the shell script and the elisp entry points:
generate parse a table and write the dated schedule .org (and .ics) export parse a table and write only the .ics sheets draw the printable time-block sheet (ReportLab or LaTeX) and/or org template print or write a blank weekly table for N projects weeks list archived weekly schedule files, newest first