diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3257bed..a814ec3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,6 +13,6 @@ repos: hooks: - id: ruff - repo: https://github.com/pycqa/doc8 - rev: 0.10.1 + rev: v1.1.1 hooks: - id: doc8 diff --git a/README.rst b/README.rst index de7f716..0fac75b 100644 --- a/README.rst +++ b/README.rst @@ -109,6 +109,34 @@ as well as to access the timestamp attribute in different formats: .. usage-end +.. cli-begin + +Command line interface +----------------------- + +The package comes with a CLI interface that can be invoked either by the script name +`ulid` or as python module `python -m ulid`. The CLI allows you to generate, inspect +and convert ULIDs, e.g. + +.. code-block:: bash + + $ ulid build + 01HASFKBN8SKZTSVVS03K5AMMS + + $ ulid build --from-datetime=2023-09-23T10:20:30 + 01HB0J0F5GCKEXNSWVAD5PEAC1 + + $ ulid show 01HASFKBN8SKZTSVVS03K5AMMS + ULID: 01HASFKBN8SKZTSVVS03K5AMMS + Hex: 018ab2f9aea8ccffacef7900e6555299 + Int: 2049395013039097460549394558635823769 + Timestamp: 1695219822.248 + Datetime: 2023-09-20 14:23:42.248000+00:00 + + $ echo 01HASFKBN8SKZTSVVS03K5AMMS | ulid show --uuid - + 018ab2f9-aea8-ccff-acef-7900e6555299 + +.. cli-end Other implementations --------------------- diff --git a/docs/index.rst b/docs/index.rst index b2f4fde..e6b1169 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -16,6 +16,9 @@ Release v\ |release| (:ref:`What's new `) :start-after: usage-begin :end-before: usage-end +.. include:: ../README.rst + :start-after: cli-begin + :end-before: cli-end API documentation ----------------- diff --git a/pyproject.toml b/pyproject.toml index b851e97..08079e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,9 @@ classifiers = [ "Topic :: Software Development :: Libraries", ] +[project.scripts] +ulid = "ulid.__main__:entrypoint" + [tool.hatch.version] source = "vcs" @@ -66,3 +69,6 @@ line_length = 100 branch = true parallel = true source = ["ulid"] + +[tool.doc8] +max-line-length = 100 diff --git a/ulid/__main__.py b/ulid/__main__.py new file mode 100644 index 0000000..418570a --- /dev/null +++ b/ulid/__main__.py @@ -0,0 +1,159 @@ +import argparse +import shutil +import sys +import textwrap +from collections.abc import Sequence +from datetime import datetime +from datetime import timezone +from functools import partial +from uuid import UUID + +from ulid import ULID + + +def make_parser(prog: str | None = None) -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog=prog, + description=textwrap.indent( + textwrap.dedent( + """ + Create or inspect ULIDs + + A ULID is a universally unique lexicographically sortable identifier + with the following structure + + 01AN4Z07BY 79KA1307SR9X4MV3 + |----------| |----------------| + Timestamp Randomness + 48bits 80bits + """ + ).strip(), + " ", + ), + formatter_class=partial( + argparse.RawDescriptionHelpFormatter, + # Prevent argparse from taking up the entire width of the terminal window + # which impedes readability. + width=min(shutil.get_terminal_size().columns - 2, 127), + ), + ) + parser.set_defaults(func=lambda _: parser.print_help()) + + subparsers = parser.add_subparsers(title="subcommands") + b = subparsers.add_parser( + "build", + help="generate ULIDs from different sources", + ) + b.add_argument( + "--from-int", + type=int, + metavar="", + help="create from integer", + ) + b.add_argument( + "--from-hex", + type=str, + metavar="", + help="create from 32 character hex value", + ) + b.add_argument( + "--from-str", + type=str, + metavar="", + help="create from base32 encoded string of length 26", + ) + b.add_argument( + "--from-timestamp", + type=parse_numeric, + metavar="", + help="create from timestamp either as float in secs or int as millis", + ) + b.add_argument( + "--from-datetime", + type=datetime.fromisoformat, + metavar="", + help="create from datetime. The timestamp part of the ULID will be taken from the datetime", + ) + b.add_argument( + "--from-uuid", + type=UUID, + metavar="", + help="create from given UUID. The timestamp part will be random.", + ) + b.set_defaults(func=build) + + s = subparsers.add_parser("show", help="show properties of a ULID") + s.add_argument("ulid", help="the ULID to inspect. The special value - reads from stdin") + s.add_argument("--uuid", action="store_true", help="convert to UUID") + s.add_argument("--hex", action="store_true", help="convert to hex") + s.add_argument("--int", action="store_true", help="convert to int") + s.add_argument("--timestamp", "--ts", action="store_true", help="show timestamp") + s.add_argument("--datetime", "--dt", action="store_true", help="show datetime") + s.set_defaults(func=show) + return parser + + +def parse_numeric(s: str) -> int | float: + try: + return int(s) + except ValueError: + return float(s) + + +def main(argv: Sequence[str], prog: str | None = None) -> None: + args = make_parser(prog).parse_args(argv) + args.func(args) + + +def build(args: argparse.Namespace) -> None: + ulid: ULID + if args.from_int is not None: + ulid = ULID.from_int(args.from_int) + elif args.from_hex is not None: + ulid = ULID.from_hex(args.from_hex) + elif args.from_str is not None: + ulid = ULID.from_str(args.from_str) + elif args.from_timestamp is not None: + ulid = ULID.from_timestamp(args.from_timestamp) + elif args.from_datetime is not None: + ulid = ULID.from_datetime(args.from_datetime) + elif args.from_uuid is not None: + ulid = ULID.from_uuid(args.from_uuid) + else: + ulid = ULID.from_datetime(datetime.now(timezone.utc)) + print(ulid) + + +def show(args: argparse.Namespace) -> None: + value = sys.stdin.readline().strip() if args.ulid == "-" else args.ulid + ulid: ULID = ULID.from_str(value) + if args.uuid: + print(ulid.to_uuid()) + elif args.hex: + print(ulid.hex) + elif args.int: + print(int(ulid)) + elif args.timestamp: + print(ulid.timestamp) + elif args.datetime: + print(ulid.datetime) + else: + print( + textwrap.dedent( + f""" + ULID: {ulid!s} + Hex: {ulid.hex} + Int: {int(ulid)} + Timestamp: {ulid.timestamp} + Datetime: {ulid.datetime} + """ + ).strip() + ) + + +def entrypoint() -> None: + main(sys.argv[1:]) + + +if __name__ == "__main__": + main(sys.argv[1:], "python -m ulid")