pyxctsk
A Python implementation of XCTrack's task format for paragliding and hang gliding competitions. This library enables parsing, manipulation, and visualization of XCTrack task files used to define competition routes.
pyxctsk provides a comprehensive toolkit for working with the XCTrack task specification, including reading/writing task files, generating QR codes for task sharing, and performing distance calculations on competition routes.
It also aims to be an open-source reference implementation of the FAI Sporting Code S7F distance calculations, so that a task's published distance is something anyone can reproduce rather than something that depends on which device the task committee used. The findings — including that S7F defines the optimized distance but not the "distance through centres" that task boards publish beside it — are in docs/s7f-distance-reference.md.
$ pyxctsk distances task.xctsk --format text
task distance (§7.2) 92.002 km
speed section (§7.2) 86.761 km
through centres 321.440 km [LAUNCH_TO_GOAL]
pyxctsk distances is the command another implementation should diff against: drop
--format text for JSON carrying the library version, the S7F edition, every reading
of the undefined centre distance, and the optimized crossing point for each turnpoint.
The library implements the XCTrack Competition Interfaces specification: both task formats, both QR schemes (XCTSK: and the compressed XCTSKZ:), manufacturer extensions, and all documented fields. All 25 reference tasks generated by tools.xcontest.org round-trip byte-identically. The conformance review behind that claim, including the design decisions it produced, is in docs/arch-review/.
Technical Highlights
- Typed Data Model: Core domain objects are dataclasses with constrained values modelled as enums, so unknown task/turnpoint/goal types are rejected at parse time.
Task.validate()additionally reports violations of the spec's structural rules — TAKEOFF only first, SSS and ESS exactly once, SSS before ESS — andparse_task(data, strict=True)turns them into an error (model/task.py). Lenient reading covers only that structure: a value of the wrong kind — a boolean latitude, a latitude off the earth, a QRzthat is not a polyline — is refused on every read as aMalformedPayloadErrornaming where, such asturnpoints[1].waypoint.lat - S7F-conformant Distance Calculation: The Ding–Xie–Jiang path finder in a localized Transverse Mercator plane, converged to the spec's ε = 0.1 m and multi-started so the answer is the task's shortest path rather than the projection's local optimum; the two-pass task-area centre of §7.1.6; and both of §7.2's distances, the task's and the speed section's (distance/, route_optimization.py)
- Efficient QR Code Representation: Implements XCTrack's compact QR code format with polyline compression for efficient task sharing via small QR codes that work well in direct sunlight (qrcode/task.py)
- Flexible Parsing Pipeline: Single entry point that intelligently detects and parses multiple input formats (JSON, URL, QR code image) (parser.py)
- Type Safety: Comprehensive type hints throughout the codebase with strict mypy enforcement
Dependencies and Libraries
Core
- click: Command-line interface framework for the CLI tools
- pyproj: Projection calculations for accurate distance measurements
- scipy: Scientific computing library used for route optimization algorithms
- simplekml: KML document generation
Optional — the qr extra
QR images only. Reading and writing the XCTSK: and XCTSKZ: strings
themselves needs nothing beyond the core; rendering one as a PNG, or reading one
back out of an image, needs these:
- Pillow: Image processing for QR code generation and parsing
- qrcode: QR code generation
- zxing-cpp: QR code parsing from images (ships binary wheels; no system library needed)
Without them, parse_task on an image and pyxctsk convert --format png fail
with a message naming the extra; everything else works.
Development
- pytest: Testing framework
- ruff: Linting and formatting
- mypy: Static type checking
- lefthook: Git hook manager
Project Structure
.
├── src/
│ └── pyxctsk/ # Core package implementation
│ ├── __init__.py # Public API — everything most callers need
│ ├── parser.py # Single entry point for every input format
│ ├── cli.py # `pyxctsk convert`
│ ├── exceptions.py
│ ├── model/ # The task itself: dataclasses, enums, validation
│ ├── qrcode/ # The compact QR format, and conversion to the model
│ ├── distance/ # Route optimization, task distances, goal-line geometry
│ └── export/ # KML and GeoJSON writers
├── tests/ # Test suite, mirroring the package layout
│ ├── conftest.py # Shared fixtures
│ ├── paths.py # Where the reference data lives
│ ├── model/ qrcode/ distance/ export/
│ ├── conformance/ # Spec-audit regressions, cutting across packages
│ └── data/ # Reference tasks and generated output
├── scripts/ # Utility scripts
├── pyproject.toml # Project configuration
└── README.md
Dependencies between the packages run one way — model → qrcode and
model → distance → export — so a change to an export format cannot reach the
domain model, and the model does not know how distances are computed. Each
package's __init__.py is its interface and describes what it holds.
src/pyxctsk/: Core library implementation with immutable data models and parsing logictests/: Test suite; each subpackage covers the source package of the same namescripts/: Utility scripts for automation and testing
Installation
Core Library Only
pip install pyxctsk
Development Installation
This project uses uv for dependency management.
git clone https://github.com/simonsteiner/pyxctsk.git
cd pyxctsk
# Create the virtual environment and install the project (editable), the dev
# dependency group, and the optional QR image support.
uv sync --all-extras
# Also install dependencies for the repository-only utilities under scripts/.
uv sync --all-extras --group tools
# Run tests
uv run pytest
# Run single test with parameter
# -s: disables output capturing, allowing print statements and other outputs to be shown in the terminal.
# -vv: increases verbosity, providing more detailed test results.
uv run pytest -s tests/qrcode/test_codec.py -vv
# (Optional) To check QR code dependencies, run:
uv run python scripts/check_qr_deps.py
Code Quality & Formatting
The project uses lefthook to run ruff (lint + format), mypy, and cspell on commit:
# Install the git hooks (one-time)
uv run lefthook install
# Run the pre-commit hooks against staged files
uv run lefthook run pre-commit
# Or run the tools directly
uv run ruff check --fix src/ tests/ scripts/
uv run ruff format src/ tests/ scripts/
uv run mypy src/ tests/
Usage Examples
from pyxctsk import load_task, parse_task, Task, TaskType, Turnpoint, Waypoint
# Parse from file, URL, or QR code
task = load_task('task.xctsk') # From a .xctsk file (or a QR image file)
task = parse_task('XCTSK:{...}') # From XCTrack URL
task = parse_task(open('qr.png', 'rb').read()) # From QR code image bytes
# Access task data
print(f"Task type: {task.task_type}")
print(f"Number of turnpoints: {len(task.turnpoints)}")
# Create a new task
task = Task(
task_type=TaskType.CLASSIC,
version=1,
turnpoints=[
Turnpoint(
radius=1000,
waypoint=Waypoint(
name="TP01", lat=46.5, lon=8.0, alt_smoothed=1000
)
)
]
)
# Save as JSON
with open('task.xctsk', 'w', encoding='utf-8') as f:
f.write(task.to_json())
# Generate QR code
from pyxctsk.qrcode.image import generate_qrcode_image
qr_task = task.to_qr_code_task()
qr_image = generate_qrcode_image(qr_task.to_string())
qr_image.save('task_qr.png')
Command Line Interface
The pyxctsk CLI enables conversion and inspection of XCTrack task files in multiple formats, with strict error handling and clear messaging.
Parameter Options:
--format [json|kml|geojson|png|qrcode-json]
Output format.json: Standard JSON representationkml: KML for mapping toolsgeojson: GeoJSON FeatureCollection of the same mappng: QR code image (PNG)qrcode-json: Compact QR string (XCTSK: URL)
Default:json
--output, -o <file>
Output file (default: stdout). Forpng, writes a PNG image; for others, writes text.--compressed, -z
Emit the compressedXCTSKZ:encoding (pngandqrcode-jsononly).--strict
Reject a task that breaks the spec's structural rules instead of converting it. A malformed value is refused with or without it.<input_file>
Input file (optional). If omitted, reads from stdin. Accepts.xctskfiles or QR code images.
Examples:
# Convert task to different formats
pyxctsk convert task.xctsk --format json # JSON output
pyxctsk convert task.xctsk --format kml -o task.kml # KML output
pyxctsk convert task.xctsk --format geojson -o task.geojson # GeoJSON output
pyxctsk convert task.xctsk --format png -o qr.png # QR code image
pyxctsk convert task.xctsk --format qrcode-json # XCTSK: URL string
# Parse from different inputs
pyxctsk convert qr_code.png --format json # From QR image
cat task.xctsk | pyxctsk convert --format kml # From stdin
Supported formats:
- Input:
.xctsk(XCTrack task files), QR code image (PNG) - Output: JSON, KML, GeoJSON, QR code (PNG or XCTSK: URL string)
See the CLI startup message (pyxctsk --help or running the CLI with no arguments) for a quick summary of options and supported formats.
Requirements
- Python 3.11+ (CI verifies 3.11 through 3.14 on every pull request and push to
main, and again before each release) - Install
pyxctsk[qr]for QR code images (PNG output and QR image input). - Development tooling lives in the
devdependency group and is installed byuv sync;uv sync --all-extrasalso installs QR image support. - Utilities under
scripts/are source-checkout tools, not part of the published package. Install their dependencies withuv sync --group tools.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Original Implementation
This Python library is based on the Go implementation by Tom Payne: https://github.com/twpayne/go-xctrack
Metadata
Release files for pyxctsk 0.7.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pyxctsk-0.7.0.tar.gz | 1.5 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pyxctsk-0.7.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.7 MB
Release files / pyxctsk-0.7.0.tar.gz
| Download URL | pyxctsk-0.7.0.tar.gz |
|---|---|
| Size | 1.5 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6f6e61b1d02872a6ea7acde1f6b377fdc885a689e22405af0f8d7f650c1a1b39
|
|
BLAKE2b-256 checksum How to use checksums |
07da3353dee34519cf02c0dc9feae15c8717665eb7de14b9eeb100204c73c189
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.
Transparency logRelease files / pyxctsk-0.7.0-py3-none-any.whl
| Download URL | pyxctsk-0.7.0-py3-none-any.whl |
|---|---|
| Size | 130.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
365ffd6370fffcf08b361856ea8ea1844e6b293c1de00da278dbc1ed1a979f9d
|
|
BLAKE2b-256 checksum How to use checksums |
3ae0d432a80e37638f2d64bddd79403713543d3a0df9eb352d5f2b16f60b8d60
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.
Transparency log