Skip to main content

dtrexp-py

dtrexp-py

Python implementation of DTRExp (read: "DTR Expression") — a compact string expression for date-time ranges and recurrence, evaluated by coverage rather than enumeration.

T0900:1800 E1:5          Mon–Fri, 09:00–18:00
E7#-1 M4                 last Sunday of April, every year
20200106/10D             every 10 days from 2020-01-06 (cron can't say this)
M!7                      every month except July

Scope: parsing, validation and coverage evaluation — the spec's core interface. Rendering, description and RRULE export are out of scope; the reference implementation has them.

Install

pip install dtrexp

Python 3.11+, stdlib only (zoneinfo for IANA zones).

Usage

from datetime import datetime, timezone
import dtrexp

dtr = dtrexp.parse("T0900:1800 E1:5")   # business hours, Mon–Fri

dtr.covers(datetime(2026, 7, 7, 10, 0, tzinfo=timezone.utc))
# —> True (UTC — the default)
dtr.covers(datetime(2026, 7, 7, 7, 30, tzinfo=timezone.utc), tz="Europe/Berlin")
# —> True (09:30 Berlin local time)

berlin = dtr.with_zone("Europe/Berlin")   # a copy; dtr is left as it is
berlin.covers(datetime(2026, 7, 7, 7, 30, tzinfo=timezone.utc))
# —> True (the bound zone)
berlin.covers(datetime(2026, 7, 7, 7, 30, tzinfo=timezone.utc), tz="UTC")
# —> False (a tz passed to the call wins)

Note that you parse once (at write/config time) and evaluate many; Expression objects are meant to be shared, not changed (with_zone returns a copy rather than changing the one you have), and covers is a single calendar-field extraction followed by integer comparisons (no occurrence iteration). The zone is an evaluation parameter, never part of the expression. covers also accepts an ISO 8601 string for the instant.

  • covers(instant, tz=None): tz is an IANA zone name; None means the bound zone, else "UTC". A tz passed to the call, "UTC" included, wins over the bound zone. An unknown zone raises ZoneInfoNotFoundError.
  • with_zone(tz): a copy whose evaluation defaults to tz, readable as zone (None when unbound). The copy's text and warnings are the original's. The zone is checked at once: an unknown zone raises ZoneInfoNotFoundError, an empty name ValueError, and a non-str (None included) TypeError; binding never falls back to UTC or the host's zone.

Which to use: bind the zone with with_zone() when it belongs to what the expression describes (a store's hours, a venue's schedule), and pass tz per call when it belongs to whoever is asking (a rule shown to each user in their own zone). A bound copy shares its text with the original, so a cache of parsed expressions keys on text and zone together.

Errors and Warnings

Both carry a position; the 0-based character offset into the source:

dtrexp.parse("Y*/3")
# raises DTRExpSyntaxError: "anchorless stride — an explicit start is required (at 1)"
# error.position —> 1

res = dtrexp.validate("D30 M2")   # never raises
res.valid                         # True — it parses
res.warnings                      # (DTRExpWarning(message="unsatisfiable …", position=0),)
  • parse(text) raises DTRExpSyntaxError (a ValueError) on invalid input; position points at the offending character and is appended to the rendered message.
  • validate(text) never raises; typo-shaped input comes back as data. Returns a frozen ValidationResult with valid bool, errors (parsing stops at the first syntax error, so at most one) and warnings.
  • Warnings are the spec's §9.1 unsatisfiability lint: expressions that parse but can never match. They are DTRExpWarning objects (message str, position int | None); str(warning) is the message. Expression.warnings and validate(text).warnings carry the same content.

Conformance & Quality

  • The test suite is driven by the shared vectors.json from the spec repo (draft 2.8): every coverage, rejection, warning and quiet vector, including the calendar traps (Feb 29 across 2000/2024/2100, W53 existence, DST gap/overlap in Europe/Berlin). See VECTORS.md for how the suite works.
  • 100% line + branch coverage, enforced (uv run poe cover); mutation-tested with mutmut (uv run poe mutation).
  • Zero dependencies.

License

© 2026, Onur Yıldırım. MIT License.

Metadata

Release files for dtrexp 1.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for dtrexp 1.1.0
File Size Uploaded Provenance Actions
dtrexp-1.1.0.tar.gz 86.5 kB Without provenance Details for dtrexp-1.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for dtrexp 1.1.0
File Interpreter ABI Platform Actions
dtrexp-1.1.0-py3-none-any.whl Python 3 none any Details for dtrexp-1.1.0-py3-none-any.whl

Total release size: 106.1 kB

Release files / dtrexp-1.1.0.tar.gz

Download URL dtrexp-1.1.0.tar.gz
Size 86.5 kB
Tags Source
SHA-256 checksum
How to use checksums
b27474087c11da10d2843a41c0aceb6d2ba366999d1a5276bf2ead0d7195e883
BLAKE2b-256 checksum
How to use checksums
6cc58ce2c9d555d52e322108bfd0eba41b7844e1897ac2f4a1541ccc1668235c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.27 {"installer":{"name":"uv","version":"0.11.27","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / dtrexp-1.1.0-py3-none-any.whl

Download URL dtrexp-1.1.0-py3-none-any.whl
Size 19.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
78e48a08d3d073d38fd02cafed542a74c8895a1f1eb9b7cae9f8ad259db90728
BLAKE2b-256 checksum
How to use checksums
62c243fd82f7e6f7e13d8a139f614bc7b3661957c983f5c6dbfb5675242750f8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.27 {"installer":{"name":"uv","version":"0.11.27","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Security for dtrexp 1.1.0

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.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page