Developers’ Reference

Architecture, build/test/release workflow, documentation pipeline, and contribution rules for Time To Align!

This page is the canonical reference for contributing to Time To Align!. The conceptual model lives in Concepts and the per-class API lives under API Reference; everything you need to build, test, document, and release the library is here.

The package is named timetoalign for machines (imports, PyPI) and written Time To Align! for humans (prose, headings, comments).


1. Repository Layout

The git repository is a multi-project workspace; the library itself lives in timetoalign/.

tta/                                 # Repository root
├── timetoalign/                     # The Python library — the focus of this doc
│   ├── timetoalign/                 # Importable package
│   ├── tests/                       # pytest suite (mirrors the package layout)
│   ├── docs/                        # Documentation source
│   │   ├── page/                    # Quarto site (this site)
│   │   ├── tuto-notebooks/          # Tutorial notebooks (jupytext .py + paired .ipynb)
│   │   └── howto-notebooks/         # How-to notebooks (jupytext .py + paired .ipynb)
│   ├── pyproject.toml               # PEP 621 project metadata + ruff/coverage/pytest config
│   ├── tox.ini                      # Build/test/lint/publish orchestrator
│   ├── .pre-commit-config.yaml      # Pinned formatter/linter versions
│   ├── .github/workflows/           # CI (release-please)
│   └── CHANGELOG.md                 # Auto-generated by release-please — DO NOT edit by hand
├── tta_article/                     # TISMIR manuscript (LaTeX, submitted) — out of scope here
├── pyMeasureMap/                    # Sibling project; standalone
└── dashboard/                       # Sibling project; standalone

Sibling projects (tta_article/, pyMeasureMap/, dashboard/) have their own build infrastructure and are not governed by the rules below.


2. Architecture Overview

2.1 Subpackages

The importable package timetoalign/ is organised into seven subpackages, each with a single responsibility:

Subpackage Responsibility
core/ Foundational types: enums (Domain, TimeUnit, …), Coordinate / IdCoordinate, TimeStamp, IdGenerator.
fields/ SemanticField definitions (Layer 2 of the EventData stack).
display/ ASCII rendering helpers for timelines, groups, bundles, flows.
maps/ ConversionMap family (LinearMap, TableMap, ChainMap, PiecewiseMap, …).
timelines/ Timeline classes for all six domain × modality combinations, regions, flow control, beat grids.
alignment/ MatchClaim, AlignmentAnchor, TimelineGroup, AlignmentBundle, MatchGraph, MatchLine, WarpMap.
loader/ File-format-specific ingestion + the PyArrow-backed EventData / EventStore storage primitives.

2.2 Import Direction (MANDATORY)

Subpackages are arranged in strict layers. Imports may only flow downward, from a higher-numbered layer to a lower-numbered one. This is a hard rule: violations create circular-import landmines and are refused at review.

                    ┌──────────────────────────────┐
       Layer 3 ──▶  │            loader            │
                    └──────────────┬───────────────┘
                                   │ may import from layers 0–2
                    ┌──────────────▼───────────────┐
       Layer 2 ──▶  │     timelines     alignment  │
                    └──────────────┬───────────────┘
                                   │ may import from layers 0–1
                    ┌──────────────▼───────────────┐
       Layer 1 ──▶  │             maps             │
                    └──────────────┬───────────────┘
                                   │ may import from layer 0
                    ┌──────────────▼───────────────┐
       Layer 0 ──▶  │   core    fields    display  │
                    └──────────────────────────────┘
                       (no internal dependencies)

Equivalently:

Subpackage May import from
core/ nothing internal
fields/ nothing internal
display/ nothing internal at runtime (TYPE_CHECKING only)
maps/ core
timelines/ core, maps (+ documented exception below)
alignment/ core, maps
loader/ core, maps, timelines, alignment

One documented exception: timelines ↔︎ loader

Timeline instances own an EventData (PyArrow-backed event storage). For historical reasons, EventData and EventStore live under loader/ because loaders are their primary producer. As a result timelines/base.py performs one runtime import from loader/:

from timetoalign.loader import EventData

All other timelines → loader references are deferred (function-local) or guarded by if TYPE_CHECKING: to avoid the circular import at module load. Do not introduce any new runtime timelines → loader imports. If you need additional types from the loader namespace inside a timeline module, either:

  1. Put the import inside the function that needs it (deferred), or
  2. Guard it with if TYPE_CHECKING: and use a string annotation.

2.3 Stable Architectural Invariants

These invariants are tested at the unit level and enforced at review. If you find yourself wanting to break one, raise it as a discussion before coding.

  • TimeStamp is the one and only return type for coordinate resolution. No parallel “GroupTimestamp” / “BundleTimestamp” / etc. classes.
  • Coordinate and IdCoordinate are the canonical input types for every method that takes a coordinate value. Raw numbers are accepted for ergonomics; raw numbers are not acceptable as return types — every getter returns a Coordinate.
  • ConversionMap properties belong to a single Timeline. They are not graph edges between timelines.
  • MatchGraph is on-demand and analytical. It is built from claims when needed, not maintained as a system-wide structure.
  • Loaders use a strict two-phase contract. loader.load(*sources) ingests files; loader.create_*() produces domain objects and never takes file paths. The from_file() classmethod is sugar for the common case.
  • All enums live in core/enums.py and subclass FancyStrEnum. Member names are lowercase. The single documented exception is NumberType, whose values are Python type objects.
  • Timeline IDs are systematic: clt/dlt/cpt/dpt/cgt/dgt prefixes for the six domain × modality combinations, with optional role prefixes (score:clt1, perf:dlt1).

For the full conceptual rationale see Concepts and the Glossary.


3. Environment & Installation

3.1 Python

  • Supported: Python 3.11, 3.12, 3.13 (CI matrix).
  • Required: >=3.11 per pyproject.toml.

3.2 Install

From the timetoalign/ directory:

pip install -e ".[dev]"

dev is the convenience extra that pulls in everything: all loaders, plotting, the tutorial Jupyter stack, the docs build chain, the test stack, and the formatter/linter tooling. See pyproject.toml for the full extras hierarchy (midi, partitura, music21, ms3, audio, graphical, plot, scores, loaders, tutorial, docs, testing, all, dev).

3.3 Pre-commit Hook

After installing, register the git hook once per clone:

pre-commit install

The pinned hook versions in .pre-commit-config.yaml are:

Hook Version Settings
black 26.1.0 language_version: python3.11
isort 7.0.0 --profile black --filter-files
flake8 7.3.0 max-line-length=120, __init__.py:F401 ignored, docs/howto-notebooks/*.py:E402 ignored
seed-isort-config 2.2.0
pre-commit-hooks 6.0.0 trailing-whitespace, check-ast/json/xml/yaml, end-of-file-fixer, mixed-line-ending (auto), debug-statements, requirements-txt-fixer

These are pinned deliberately — bump them in a dedicated chore: commit so the diff is isolated.


4. Build, Test, Publish (tox)

tox is the canonical entry point for everything CI-relevant. It uses tox-uv for fast environment creation. All commands run from timetoalign/.

Command Effect
tox Runs the full pytest suite under py311, py312, py313 (skips missing interpreters).
tox -e lint pre-commit run --all-files --show-diff-on-failure.
tox -e build PEP 517 sdist + wheel into ./dist/ via python -m build.
tox -e clean Removes ./build/ and ./dist/.
tox -e publish twine check dist/* then twine upload dist/* (testpypi by default).

To publish to production PyPI explicitly:

tox -e publish -- --repository pypi

The publish env passes TWINE_USERNAME / TWINE_PASSWORD / TWINE_REPOSITORY / TWINE_REPOSITORY_URL through from the environment.

There is no tox -e docs env; documentation is built directly with quartodoc + quarto (see §6).

4.1 Direct pytest for Iteration

Inside the active venv, plain pytest is faster than tox for tight edit-test loops:

pytest                              # full suite, parallel via pytest-xdist (-n auto)
pytest -n 0                         # serial — debugging only
pytest tests/timelines              # one subtree
pytest tests/timelines/test_base.py::test_lock

Coverage is on by default (configured in pyproject.toml [tool.pytest.ini_options]).

4.2 Test Discipline (MANDATORY)

  • Parallel-safe. No global mutable state, no inter-test ordering, use tmp_path not hardcoded /tmp/ paths, no port collisions.
  • Exact assertions. Use exact gold-standard counts, never >=, never “approximate”. If floating-point tolerance is required, document the mathematical reason in the test and the test data README.
  • Cover happy path + edge cases. Boundaries, empty inputs, type mismatches.
  • Property tests for math. ConversionMap-style code uses hypothesis to verify invariants such as inverse(forward(x)) == x.
  • Test data needs a README. Every directory under tests/data/ documents provenance, validation logic, and known discrepancies between loaders.

5. Coding Standards

5.1 Style and Formatting

  • Line length 120 (enforced by black + flake8).
  • from __future__ import annotations is the first non-comment line.
  • Imports grouped: standard library, third-party, local — handled by isort with the black profile.
  • Module logger: module_logger = logging.getLogger(__name__) immediately after imports.
  • Use # region <name> / # endregion <name> to group related class / function definitions inside a module.

5.2 Class Member Order

Inside a class, declare members in this order:

  1. Class variables.
  2. @classmethod @property accessors.
  3. @classmethod factories / helpers.
  4. Nested classes (especially Schema).
  5. __init__.
  6. Magic methods (__eq__, __hash__, __repr__, __str__).
  7. @property accessors.
  8. Public and private methods.

5.3 Typing

  • Every public function signature and class attribute is type-hinted.
  • Use modern generics (list[str], dict[str, int], X | None) — the __future__ annotations import makes these free at runtime on 3.11+.
  • Use typing_extensions.Self for methods returning the instance type.
  • Break circular dependencies with if TYPE_CHECKING: blocks and string annotations, not by collapsing modules.

5.4 Docstrings (Google Style)

Every public module, class, function, and method needs a Google-style docstring. Quartodoc renders them into the API reference site.

def lookup(self, coord: CoordinateSpec) -> TimeStamp:
    """Return the timestamp at ``coord``.

    Args:
        coord: A raw value, ``Coordinate``, or ``IdCoordinate``.

    Returns:
        A `timetoalign.TimeStamp` populated with all synchronous child
        coordinates and conversion-map results.

    Raises:
        ValueError: If ``coord`` is outside the timeline length.
    """

Cross-linking

  • In docstrings: wrap fully-qualified names in backticks (`timetoalign.ConversionMap`); the interlinks filter resolves them.
  • In .qmd pages and notebook markdown: every model term goes through the glossary shortcode: . Bare uses without the shortcode are documentation bugs.
  • When you add a new concept: update glossary.yml and glossary.qmd in the same commit as the implementation.

5.5 Error Handling

  • Never use assert for runtime validation — it disappears under -O.
  • ValueError for invalid arguments.
  • TypeError for unit / type mismatches.
  • RuntimeError (or a custom subclass) for invalid state, e.g. mutating a locked timeline.

5.6 Pitch Spelling

Normalise all input to canonical Unicode characters:

  • Sharp: (U+266F)
  • Flat: (U+266D)

#, b, -, etc. are converted at the boundary.


6. Documentation Site

The docs site is built with Quarto + quartodoc and lives at https://timetoalign.github.io/.

6.1 Building Locally

cd docs/page
quartodoc build           # regenerate the API reference pages from docstrings
quarto render             # build the full site into _site/

The Quarto pre-render hook automatically runs sync_notebooks.py (see §6.3) to refresh tutorial and how-to notebooks before rendering.

6.2 Site Structure (Diátaxis)

The site follows the Diátaxis framework:

  • Tutorials (tutorials.qmd + tutorials/): learning-oriented, step-by-step.
  • How-to Guides (howto.qmd + howto/): task-oriented, focused recipes.
  • Explanation (concepts.qmd, glossary.qmd): understanding-oriented.
  • API Reference (reference.qmd + auto-generated reference/): information-oriented; this Developers’ Reference lives here too.

The navigation (top navbar + per-section sidebar) is configured in _quarto.yml.

6.3 Notebook Integration Pipeline

Tutorial and how-to notebooks live as jupytext py:percent files in docs/tuto-notebooks/ and docs/howto-notebooks/. The paired .ipynb files are regenerated mechanically; the .py is the source of truth in git.

The pipeline is driven by docs/page/notebooks.csv, with one row per notebook:

Column Meaning
section Either tutorials or howto. Selects the source directory and target subdirectory of the rendered site.
source Filename of the .py jupytext source.
slug Stem used for the rendered .ipynb (without extension).
title Title injected as Quarto YAML front matter.
description Short description used in listing pages.

docs/page/sync_notebooks.py (run as the Quarto pre-render step) does three things per row:

  1. Runs jupytext --sync --execute on the .py source, regenerating the paired .ipynb with fresh outputs.
  2. Copies the resulting .ipynb into docs/page/{tutorials,howto}/<slug>.ipynb.
  3. Replaces the notebook metadata with a Quarto-friendly YAML cell built from the CSV row.

A SHA-256 cache (docs/page/.sync_state.json) skips notebooks whose .py source has not changed.

Adding a new how-to / tutorial notebook

  1. Drop the new .py (jupytext py:percent) into docs/tuto-notebooks/ or docs/howto-notebooks/.
  2. Add a row to docs/page/notebooks.csv.
  3. Add a sidebar entry under the appropriate section in docs/page/_quarto.yml.
  4. If the notebook introduces a new model term, also update glossary.yml and glossary.qmd.
  5. Run cd docs/page && python sync_notebooks.py --verbose once locally to confirm it executes cleanly.
  6. Run quartodoc build && quarto render to confirm it renders.

Notebook style rules

  • Markdown cells use the shortcode for every model term. A bare “Timeline” / “ConversionMap” / etc. is a documentation bug.
  • Use Loader.from_file() rather than the two-phase load() + create_*() pattern in tutorials. The two-phase form is library-internal.
  • Never demonstrate AlignmentBundle.transfer(). The user-facing answer to “how do I convert coordinates?” is always a TimeStamp or MatchStamp.

6.4 Jupytext and Quarto: Upgrade-Only

Jupytext and Quarto versions may only ever be upgraded, never downgraded.

Both tools rewrite notebook structure on save; downgrading silently introduces incompatible changes that break the diff history and may corrupt outputs across the entire notebook corpus. If a contributor’s local install is older than the version that last touched the notebooks, they must upgrade before running sync_notebooks.py.

This rule applies equally to CI environments — pin versions upward only.

6.5 quartodoc API Reference

The API reference under docs/page/reference/ is fully generated from the live docstrings. Do not hand-edit those files; they are overwritten by quartodoc build. To add a class to the reference:

  1. Make sure the class is exported from timetoalign/__init__.py (or list it with an explicit name: / package: entry).
  2. Add it to the appropriate contents: list under quartodoc: in docs/page/_quarto.yml.
  3. quartodoc build regenerates the page.

7. Conventional Commits & Releases

The library is released by release-please, configured in .github/workflows/release-please.yml with release-type: python and package-name: timetoalign. The action runs on every push to main and:

  1. Reads commit messages since the last release tag.
  2. Computes the next semantic version from those messages.
  3. Opens (or updates) a release PR that bumps pyproject.toml’s version and rewrites CHANGELOG.md.
  4. When the release PR is merged, tags the commit and creates a GitHub release.

Because the version bump is computed from commit messages, every commit that lands on main must follow Conventional Commits.

7.1 Commit Types

Prefix Effect on version Use for
feat: minor bump New user-facing capability.
fix: patch bump Bug fix.
perf: patch bump Performance improvement (no behaviour change).
docs: none Documentation only.
style: none Formatting / whitespace.
refactor: none Restructuring with no behaviour change.
test: none Adding or correcting tests.
build: none Build system or dependency changes.
ci: none CI configuration changes.
chore: none Repo housekeeping with no src/test impact.
revert: depends on reverted Revert of a previous commit.

Optional scopes are encouraged for clarity: feat(loader): …, fix(maps): ….

7.2 Breaking Changes (MAJOR Bump)

A breaking change triggers a major version bump. Mark it both ways:

  1. Add ! after the type, e.g. refactor!:, feat!:.
  2. Include a BREAKING CHANGE: footer describing the migration.
refactor!: replace TimelineGroup.get_timestamp_at return type

BREAKING CHANGE: get_timestamp_at now returns TimeStamp instead of the
removed GroupTimestamp class. Callers using `.coordinates` should switch
to subscript access (`ts["clt1"]`) or `ts.get(timeline_id)`.

The ! is what release-please reads to compute the bump; the BREAKING CHANGE: footer is what readers of the changelog rely on. Use both.

7.3 Manual Edits Are Forbidden

  • Do not edit pyproject.toml’s version by hand.
  • Do not edit CHANGELOG.md by hand.

Both are owned by release-please. Manual edits will be overwritten or, worse, will fight the next release PR.


8. Branching and Pull Requests

  • main is the release branch; release-please runs against it.
  • Feature work happens on topic branches; squash-merge into main with a Conventional Commit message that becomes the merge commit.
  • The PR title also follows Conventional Commits — release-please reads the merge commit, but a consistent title makes the PR list scannable.
  • Run tox -e lint and tox (or at least targeted pytest) locally before opening a PR.

9. IDE

PyCharm is the preferred IDE. The repo is set up to work cleanly with it out of the box:

  • Import the timetoalign/ directory as a PyCharm project (not the repo root — sibling projects have their own settings).
  • Configure the project interpreter to a venv with pip install -e ".[dev]" applied.
  • Enable the Black and isort integrations and point them at the project tools (PyCharm reads .pre-commit-config.yaml for the pinned versions; mirror them in Settings → Tools → Black and Settings → Editor → Code Style → Python → Imports).
  • Enable Settings → Editor → Inspections → Python → Type checker with strict mode if you want the same level of typing the codebase already enforces.
  • The flake8 line length is 120; set Settings → Editor → Code Style → Python → Hard wrap at to 120 to match.

VS Code, Neovim, and other editors all work — the formatters and linters are externally driven by pre-commit and tox, so editor choice is not load-bearing — but PyCharm is what the maintainers use day-to-day.


10. Where to Look Next