Skip to content

Commit

Permalink
Add command line interface
Browse files Browse the repository at this point in the history
  • Loading branch information
mdomke committed Sep 20, 2023
1 parent d9bd31f commit ffe282a
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
28 changes: 28 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
---------------------
Expand Down
3 changes: 3 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ Release v\ |release| (:ref:`What's new <changelog>`)
:start-after: usage-begin
:end-before: usage-end

.. include:: ../README.rst
:start-after: cli-begin
:end-before: cli-end

API documentation
-----------------
Expand Down
6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ classifiers = [
"Topic :: Software Development :: Libraries",
]

[project.scripts]
ulid = "ulid.__main__:entrypoint"

[tool.hatch.version]
source = "vcs"

Expand Down Expand Up @@ -66,3 +69,6 @@ line_length = 100
branch = true
parallel = true
source = ["ulid"]

[tool.doc8]
max-line-length = 100
159 changes: 159 additions & 0 deletions ulid/__main__.py
Original file line number Diff line number Diff line change
@@ -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="<int>",
help="create from integer",
)
b.add_argument(
"--from-hex",
type=str,
metavar="<str>",
help="create from 32 character hex value",
)
b.add_argument(
"--from-str",
type=str,
metavar="<str>",
help="create from base32 encoded string of length 26",
)
b.add_argument(
"--from-timestamp",
type=parse_numeric,
metavar="<int|float>",
help="create from timestamp either as float in secs or int as millis",
)
b.add_argument(
"--from-datetime",
type=datetime.fromisoformat,
metavar="<iso8601>",
help="create from datetime. The timestamp part of the ULID will be taken from the datetime",
)
b.add_argument(
"--from-uuid",
type=UUID,
metavar="<uuid>",
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")

0 comments on commit ffe282a

Please sign in to comment.