diff --git a/.github/workflows/wheel.yml b/.github/workflows/wheel.yml index e5cc5f5..bd5c2ff 100644 --- a/.github/workflows/wheel.yml +++ b/.github/workflows/wheel.yml @@ -29,8 +29,8 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@v5 - - name: Build wheel - run: uv build --wheel --out-dir wheelhouse + - name: Build package + run: make wheel OUT_DIR=wheelhouse # https://github.com/actions/upload-artifact - uses: actions/upload-artifact@v6 diff --git a/.gitignore b/.gitignore index e067d04..65dd86e 100644 --- a/.gitignore +++ b/.gitignore @@ -11,5 +11,6 @@ build cruft dist docs +man/*.1 src/*.egg-info venv diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6f156ae --- /dev/null +++ b/Makefile @@ -0,0 +1,34 @@ +OUT_DIR ?= dist + +.PHONY: all +all: wheel + +.PHONY: test +test: + @echo "==> $@" + uv run pytest + +.PHONY: clean +clean: + @echo "==> $@" + rm -rf dist/ build/ *.egg-info man/datetimecalc.1 + +.PHONY: wheel +wheel: manpages + @echo "==> $@" + uv build --wheel --out-dir $(OUT_DIR) + +.PHONY: manpages +manpages: man/datetimecalc.1 + +man/datetimecalc.1: man/extra-sections.man src/datetimecalc/__main__.py + @echo "==> $@" + uv run argparse-manpage \ + --module datetimecalc.__main__ \ + --function get_parser \ + --project-name datetimecalc \ + --description "parse and compute with natural language datetime expressions" \ + --author "Backplane " \ + --url "https://github.com/backplane/datetimecalc" \ + --include "$<" \ + --output "$@" diff --git a/man/extra-sections.man b/man/extra-sections.man new file mode 100644 index 0000000..2f5c2af --- /dev/null +++ b/man/extra-sections.man @@ -0,0 +1,108 @@ +[EXPRESSIONS] +Expressions consist of temporal values (datetimes, timedeltas, or timezones) +combined with operators. + +.SS Operators +.TP +.B + +Add a timedelta to a datetime, or add two timedeltas. +.TP +.B \- +Subtract a timedelta from a datetime, subtract two datetimes to get a +timedelta, or subtract two timedeltas. +.TP +.B @ +Convert a datetime to a different timezone. +.TP +.BR < ", " <= ", " > ", " >= ", " == ", " != +Compare two datetimes or two timedeltas. + +.SS Datetime Formats +Datetimes can be specified in various formats: +.IP \(bu 2 +ISO format: 2024-01-15, 2024-01-15 14:30 +.IP \(bu 2 +Natural language: tomorrow, next Friday, yesterday at 3pm +.IP \(bu 2 +With timezone: 2024-01-15 14:30 UTC, tomorrow America/New_York +.IP \(bu 2 +With UTC offset: 2024-01-15 +05:30 + +.SS Timedelta Formats +Durations support various units and formats: +.IP \(bu 2 +Full words: 1 day, 2 hours, 30 minutes +.IP \(bu 2 +Abbreviations: 1d, 2h, 30m, 15s +.IP \(bu 2 +Combined: 1 day 2 hours 30 minutes, 1d2h30m +.IP \(bu 2 +Fractional: 1.5 hours, 2.5 days +.PP +Supported units: years (y), months (mo), weeks (w), days (d), hours (h), +minutes (m), seconds (s), milliseconds (ms), microseconds (us). +.PP +.B Note: +Years and months use fixed approximations (1 year = 365 days, +1 month = 30 days). + +.SS Timezone Formats +Timezones can be specified as: +.IP \(bu 2 +IANA names: America/New_York, Europe/London, Asia/Tokyo +.IP \(bu 2 +Abbreviations: UTC, EST, PST (where unambiguous) +.IP \(bu 2 +UTC offsets: +05:30, -08:00 + +[EXAMPLES] +.TP +Add one week to a date: +.B datetimecalc \(dq2024-01-01 + 1 week\(dq +.TP +Calculate days until a date: +.B datetimecalc \(dq2025-01-01 - now\(dq +.TP +Convert timezone: +.B datetimecalc \(dq2024-01-01 12:00 America/New_York @ UTC\(dq +.TP +Compare durations: +.B datetimecalc \(dq1 day == 24 hours\(dq +.TP +Natural language with multiple arguments (no quotes needed): +.B datetimecalc tomorrow at 3pm + 2 hours +.TP +Get Python repr output: +.B datetimecalc \-\-repr \(dq2025-01-01 - 2024-01-01\(dq + +[OUTPUT] +By default, datetimes are formatted in ISO 8601 format and timedeltas +are formatted in a human-readable localized form based on system locale. +.PP +With +.BR \-\-repr , +output uses Python's repr() format (e.g., datetime.timedelta(days=7)). + +[LOCALIZATION] +Timedelta output is automatically localized based on the system locale. +Supported languages: English, Spanish, Chinese, Hindi, Portuguese, +Bengali, Russian, Japanese, Vietnamese, Turkish, Marathi. + +[EXIT STATUS] +.TP +.B 0 +Success. +.TP +.B 1 +Error parsing expression or invalid input. + +[SEE ALSO] +.BR date (1), +.BR python3 (1) + +[BUGS] +Only single binary operations are supported. Chained expressions like +"a + b + c" do not work; use separate invocations or parentheses in a shell. + +[COPYRIGHT] +Copyright \(co 2024-2026 Backplane. Licensed under the Apache License 2.0. diff --git a/pyproject.toml b/pyproject.toml index d0cb2f5..070edbf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,4 +46,5 @@ build-backend = "uv_build" dev = [ "pytest>=8.4.2", "pdoc3>=0.11.1", + "argparse-manpage>=4.7", ] diff --git a/src/datetimecalc/__main__.py b/src/datetimecalc/__main__.py index df71dc7..bea7f5b 100755 --- a/src/datetimecalc/__main__.py +++ b/src/datetimecalc/__main__.py @@ -13,22 +13,29 @@ import argparse import logging import sys +from importlib.metadata import version from .functions import format_temporal_object, parse_temporal_expr -def main() -> int: +def get_parser() -> argparse.ArgumentParser: """ - entrypoint for direct execution; returns an integer suitable for use with - sys.exit + Build and return the argument parser for datetimecalc. + + This function is also used by argparse-manpage to generate the manpage. """ argp = argparse.ArgumentParser( - prog=__package__, + prog="datetimecalc", description=( "program which parses natural language datetime and timedelta expressions" ), formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) + argp.add_argument( + "--version", + action="version", + version=f"%(prog)s {version('datetimecalc')}", + ) argp.add_argument( "--debug", action="store_true", @@ -45,6 +52,15 @@ def main() -> int: nargs="+", help="a natural language date and time operation expression", ) + return argp + + +def main() -> int: + """ + entrypoint for direct execution; returns an integer suitable for use with + sys.exit + """ + argp = get_parser() args = argp.parse_args() logging.basicConfig( diff --git a/uv.lock b/uv.lock index 6db0da6..5f3da56 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,12 @@ version = 1 revision = 3 requires-python = ">=3.12" +[[package]] +name = "argparse-manpage" +version = "4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/6e/2907db04890c23728eecfcb04c37f66cba24d7903fe0ff6b8dc84d943ca5/argparse_manpage-4.7.tar.gz", hash = "sha256:1deab76b212ac8753cbb67b9d2d2bc0949bbc338bb1cc3547f0890cb34108b32", size = 58871, upload-time = "2025-08-15T10:24:20.601Z" } + [[package]] name = "colorama" version = "0.4.6" @@ -21,6 +27,7 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "argparse-manpage" }, { name = "pdoc3" }, { name = "pytest" }, ] @@ -30,6 +37,7 @@ requires-dist = [{ name = "parsedatetime", specifier = ">=2.6" }] [package.metadata.requires-dev] dev = [ + { name = "argparse-manpage", specifier = ">=4.7" }, { name = "pdoc3", specifier = ">=0.11.1" }, { name = "pytest", specifier = ">=8.4.2" }, ]