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 text as a list of rows.

Each row is either HLINE or 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 cell when 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:MM strings, 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:MM strings.

writing_schedule.parser.parse_table(rows: List[str | List[str]]) ParsedTable[source]

Parse table rows (from parse_org_table()) into a ParsedTable.

writing_schedule.parser.parse_text(text: str) ParsedTable[source]

Convenience: read the first org table in text and parse it.

Data model for the writing-schedule Python port.

The types here mirror the plists produced by writing-schedule.el:

  • An Event corresponds to a parsed time block. Its five fields match the reference plist keys :section, :offset, :start, :end, and :letter (see the format spec, table tab:event).

  • A ParsedTable corresponds to the plist returned by writing-schedule--parse with keys :events, :legend, :letters, and :columns.

  • A MapEntry corresponds 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: object

One 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:MM strings.

  • 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: object

The result of parsing a weekly block table.

events: List[Event]
legend: List[Tuple[str, str]]
letters: List[str]
columns: List[Tuple[int, int]]
legend_lookup(code: str) str | None[source]

Return the first description for code in the legend, or None.

This mirrors elisp assoc, which returns the first matching entry.

class writing_schedule.model.MapEntry(letter: str, code: str = '', desc: str = '')[source]

Bases: object

Assignment of a project to a code.

letter: str
code: str = ''
desc: str = ''
writing_schedule.model.map_get(mapping: List[MapEntry], letter: str) MapEntry | None[source]

Return the mapping entry for letter, or None.

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.date to a date.

writing_schedule.week.resolve_date(value: date | str) date[source]

Return a concrete date for value.

value is an ISO date string, a datetime.date, or the word today (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.

writing_schedule.week.iso_date(d: date) str[source]

Return the ISO date string, e.g. 2026-01-19.

writing_schedule.week.day_of_week(monday: date, offset: int) date[source]

Return the date offset days after monday.

writing_schedule.week.timestamp(monday: date, offset: int, start: str, end: str) str[source]

Build an org active timestamp for a block.

Form: <YYYY-MM-DD Dow HH:MM-HH:MM>.

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 letters from legend.

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.

week is any date (or ISO string) inside the target week; it snaps to the Monday. body is 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 parsed holding only day’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 only writing-<Monday>.org files, 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.

day is a date, an ISO string, or the word today. day_parsed is the table narrowed to that day, ready for build_ics(). body is 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 parsed anchored at monday.

dtstamp may 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.

writing_schedule.vtimezone.build_vtimezone(tzname: str, year: int) List[str] | None[source]

Return the VTIMEZONE as unfolded content lines, or None if unavailable.

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 clipped start/end (hours), and open_top/open_bottom flags 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:00 becomes 4: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) - 1 where total = 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_row up to but not including end_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 only is 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.latex_escape(s: str | None) str[source]
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 day is 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 day is 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: object

Runtime 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_todo is 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.

effective_legend(legend: List[Tuple[str, str]]) List[Tuple[str, str]][source]

Merge code_descriptions under legend.

Entries in legend take precedence because they are specific to the week; the configured descriptions fill in any code the legend omits. Mirrors writing-schedule--effective-legend.

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 n projects (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.

writing_schedule.archive.archived_weeks(directory: str) List[Tuple[str, str]][source]

Return (ISO-date, path) for each archived week, newest first.

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

writing_schedule.cli.build_parser() ArgumentParser[source]
writing_schedule.cli.main(argv: List[str] | None = None) int[source]