python-ulid 2.1.0__py3-none-any.whl → 2.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: python-ulid
3
- Version: 2.1.0
3
+ Version: 2.3.0
4
4
  Summary: Universally unique lexicographically sortable identifier
5
5
  Author-email: Martin Domke <mail@martindomke.net>
6
6
  License-Expression: MIT
@@ -15,8 +15,11 @@ Classifier: Programming Language :: Python
15
15
  Classifier: Programming Language :: Python :: 3.9
16
16
  Classifier: Programming Language :: Python :: 3.10
17
17
  Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
18
19
  Classifier: Topic :: Software Development :: Libraries
19
20
  Requires-Python: >=3.9
21
+ Provides-Extra: pydantic
22
+ Requires-Dist: pydantic>=2.0; extra == 'pydantic'
20
23
  Description-Content-Type: text/x-rst
21
24
 
22
25
 
@@ -57,6 +60,12 @@ Use ``pip`` to install the library
57
60
 
58
61
  $ pip install python-ulid
59
62
 
63
+ to include Pydantic support install the optional dependency like so
64
+
65
+ .. code-block:: bash
66
+
67
+ $ pip install python-ulid[pydantic]
68
+
60
69
  .. installation-end
61
70
 
62
71
  .. usage-begin
@@ -82,8 +91,8 @@ or use one of the named constructors
82
91
  >>> ULID.from_datetime(datetime.datetime.now())
83
92
  ULID(01E75J2XBK390V2XRH44EHC10X)
84
93
 
85
- There are several options for encoding the ``ULID`` object (e.g. string, hex, int),
86
- as well as to access the timestamp attribute in different formats:
94
+ There are several options for encoding the ``ULID`` object
95
+ (e.g. string, hex, int, bytes, UUID):
87
96
 
88
97
  .. code-block:: pycon
89
98
 
@@ -91,22 +100,54 @@ as well as to access the timestamp attribute in different formats:
91
100
  '01BTGNYV6HRNK8K8VKZASZCFPE'
92
101
  >>> ulid.hex
93
102
  '015ea15f6cd1c56689a373fab3f63ece'
103
+ >>> int(ulid)
104
+ 1820576928786795198723644692628913870
105
+ >>> bytes(ulid)
106
+ b'\x01^\xa1_l\xd1\xc5f\x89\xa3s\xfa\xb3\xf6>\xce'
107
+ >>> ulid.to_uuid()
108
+ UUID('015ea15f-6cd1-c566-89a3-73fab3f63ece')
109
+
110
+ It is also possible to directly access the timestamp component of a ``ULID``,
111
+ either in UNIX epoch or as ``datetime.datetime``
112
+
113
+ .. code-block:: pycon
114
+
94
115
  >>> ulid.timestamp
95
116
  1505945939.153
96
117
  >>> ulid.datetime
97
118
  datetime.datetime(2017, 9, 20, 22, 18, 59, 153000, tzinfo=datetime.timezone.utc)
98
- >>> ulid.to_uuid()
99
- UUID('015ea15f-6cd1-c566-89a3-73fab3f63ece')
100
119
 
101
120
  .. usage-end
102
121
 
122
+ .. pydantic-begin
123
+
124
+ Pydantic integration
125
+ ---------------------
126
+
127
+ The ``ULID`` class can be directly used for the popular data validation library
128
+ `Pydantic <https://docs.pydantic.dev/latest/>`_ like so
129
+
130
+ .. code-block:: python
131
+
132
+ from pydantic import BaseModel
133
+ from ulid import ULID
134
+
135
+
136
+ class Model(BaseModel):
137
+ ulid: ULID
138
+
139
+ model = Model(ulid="DX89370400440532013000") # OK
140
+ model = Model(ulid="not-a-ulid") # Raises ValidationError
141
+
142
+ .. pydantic-end
143
+
103
144
  .. cli-begin
104
145
 
105
146
  Command line interface
106
147
  -----------------------
107
148
 
108
149
  The package comes with a CLI interface that can be invoked either by the script name
109
- `ulid` or as python module `python -m ulid`. The CLI allows you to generate, inspect
150
+ ``ulid`` or as python module ``python -m ulid``. The CLI allows you to generate, inspect
110
151
  and convert ULIDs, e.g.
111
152
 
112
153
  .. code-block:: bash
@@ -124,8 +165,26 @@ and convert ULIDs, e.g.
124
165
  Timestamp: 1695219822.248
125
166
  Datetime: 2023-09-20 14:23:42.248000+00:00
126
167
 
168
+ There are several flags to select specific output formats for the ``show`` command, e.g.
169
+
170
+
171
+ .. code-block:: bash
172
+
173
+ $ ulid show --datetime 01HASFKBN8SKZTSVVS03K5AMMS
174
+ 2023-09-20 14:23:42.248000+00:00
175
+
176
+ The special character ``-`` allows to read values from ``stdin`` so that they can be piped. E.g.
177
+
178
+ .. code-block:: bash
179
+
127
180
  $ echo 01HASFKBN8SKZTSVVS03K5AMMS | ulid show --uuid -
128
- 018ab2f9-aea8-ccff-acef-7900e6555299
181
+ 018ab2f9-aea8-4cff-acef-7900e6555299
182
+
183
+ $ date --iso-8601 | python -m ulid build --from-datetime -
184
+ 01HAT9PVR02T3S13XB48S7GEHE
185
+
186
+ For a full overview of flags for the ``build`` and ``show`` commands use the ``--help`` option
187
+ (e.g. ``ulid show --help``).
129
188
 
130
189
  .. cli-end
131
190
 
@@ -135,3 +194,7 @@ Other implementations
135
194
  * `ahawker/ulid <https://github.com/ahawker/ulid>`_
136
195
  * `valohai/ulid2 <https://github.com/valohai/ulid2>`_
137
196
  * `mdipierro/ulid <https://github.com/mdipierro/ulid>`_
197
+ * `oklog/ulid <https://github.com/oklog/ulid>`_
198
+ * `ulid/javascript <https://github.com/ulid/javascript>`_
199
+ * `RobThree/NUlid <https://github.com/RobThree/NUlid>`_
200
+ * `imdario/go-ulid <https://github.com/imdario/go-ulid>`_
@@ -0,0 +1,10 @@
1
+ ulid/__init__.py,sha256=0mApsC1fFWyQnZgbJqU-fX0gV-Qj2l_jX3IEwbwKzxw,9114
2
+ ulid/__main__.py,sha256=Cmg0NEz3GUZ2WhsZVpY3-Kq-JOkxJFaLsRNgj3Aosyw,5341
3
+ ulid/base32.py,sha256=OkU4FBqohJgV_ZRc3dTbjPlUTSSeC9ddFlPXD2vA8vc,5832
4
+ ulid/constants.py,sha256=N_nw4W2ciXO8C1IeuO3YK3j4_x9QW-0KyvrSQUuM-G4,241
5
+ ulid/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ python_ulid-2.3.0.dist-info/METADATA,sha256=91xx1_CVxuIoS3Wj7vXRyxA0Z5nF8gxEZtLy4Egt1zg,5382
7
+ python_ulid-2.3.0.dist-info/WHEEL,sha256=bq9SyP5NxIRA9EpQgMCd-9RmPHWvbH-4lTDGwxgIR64,87
8
+ python_ulid-2.3.0.dist-info/entry_points.txt,sha256=9214ghzfSgS8a-TjjlWDF7bI7KSaeewOCBKn5uz7bdU,50
9
+ python_ulid-2.3.0.dist-info/licenses/LICENSE,sha256=DiDPNhW5yUHIqaKowvX0c1c9xWiI0xW4ji8R9N2NJbw,1056
10
+ python_ulid-2.3.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.18.0
2
+ Generator: hatchling 1.22.3
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
ulid/__init__.py CHANGED
@@ -9,15 +9,21 @@ from datetime import datetime
9
9
  from datetime import timezone
10
10
  from typing import Any
11
11
  from typing import Generic
12
+ from typing import TYPE_CHECKING
12
13
  from typing import TypeVar
13
14
 
14
15
  from ulid import base32
15
16
  from ulid import constants
16
17
 
17
18
 
19
+ if TYPE_CHECKING: # pragma: no cover
20
+ from pydantic import GetCoreSchemaHandler
21
+ from pydantic import ValidatorFunctionWrapHandler
22
+ from pydantic_core import CoreSchema
23
+
18
24
  try:
19
25
  from importlib.metadata import version
20
- except ImportError:
26
+ except ImportError: # pragma: no cover
21
27
  from importlib_metadata import version # type: ignore
22
28
 
23
29
 
@@ -44,6 +50,9 @@ class validate_type(Generic[T]): # noqa: N801
44
50
  return wrapped
45
51
 
46
52
 
53
+ U = TypeVar("U", bound="ULID")
54
+
55
+
47
56
  @functools.total_ordering
48
57
  class ULID:
49
58
  """The :class:`ULID` object consists of a timestamp part of 48 bits and of 80 random bits.
@@ -67,11 +76,13 @@ class ULID:
67
76
  def __init__(self, value: bytes | None = None) -> None:
68
77
  if value is not None and len(value) != constants.BYTES_LEN:
69
78
  raise ValueError("ULID has to be exactly 16 bytes long.")
70
- self.bytes: bytes = value or ULID.from_timestamp(time.time()).bytes
79
+ self.bytes: bytes = (
80
+ value or ULID.from_timestamp(time.time_ns() // constants.NANOSECS_IN_MILLISECS).bytes
81
+ )
71
82
 
72
83
  @classmethod
73
84
  @validate_type(datetime)
74
- def from_datetime(cls, value: datetime) -> ULID:
85
+ def from_datetime(cls: type[U], value: datetime) -> U:
75
86
  """Create a new :class:`ULID`-object from a :class:`datetime`. The timestamp part of the
76
87
  `ULID` will be set to the corresponding timestamp of the datetime.
77
88
 
@@ -85,7 +96,7 @@ class ULID:
85
96
 
86
97
  @classmethod
87
98
  @validate_type(int, float)
88
- def from_timestamp(cls, value: int | float) -> ULID:
99
+ def from_timestamp(cls: type[U], value: int | float) -> U:
89
100
  """Create a new :class:`ULID`-object from a timestamp. The timestamp can be either a
90
101
  `float` representing the time in seconds (as it would be returned by :func:`time.time()`)
91
102
  or an `int` in milliseconds.
@@ -104,7 +115,7 @@ class ULID:
104
115
 
105
116
  @classmethod
106
117
  @validate_type(uuid.UUID)
107
- def from_uuid(cls, value: uuid.UUID) -> ULID:
118
+ def from_uuid(cls: type[U], value: uuid.UUID) -> U:
108
119
  """Create a new :class:`ULID`-object from a :class:`uuid.UUID`. The timestamp part will be
109
120
  random in that case.
110
121
 
@@ -118,25 +129,25 @@ class ULID:
118
129
 
119
130
  @classmethod
120
131
  @validate_type(bytes)
121
- def from_bytes(cls, bytes_: bytes) -> ULID:
132
+ def from_bytes(cls: type[U], bytes_: bytes) -> U:
122
133
  """Create a new :class:`ULID`-object from sequence of 16 bytes."""
123
134
  return cls(bytes_)
124
135
 
125
136
  @classmethod
126
137
  @validate_type(str)
127
- def from_hex(cls, value: str) -> ULID:
138
+ def from_hex(cls: type[U], value: str) -> U:
128
139
  """Create a new :class:`ULID`-object from 32 character string of hex values."""
129
140
  return cls.from_bytes(bytes.fromhex(value))
130
141
 
131
142
  @classmethod
132
143
  @validate_type(str)
133
- def from_str(cls, string: str) -> ULID:
144
+ def from_str(cls: type[U], string: str) -> U:
134
145
  """Create a new :class:`ULID`-object from a 26 char long string representation."""
135
146
  return cls(base32.decode(string))
136
147
 
137
148
  @classmethod
138
149
  @validate_type(int)
139
- def from_int(cls, value: int) -> ULID:
150
+ def from_int(cls: type[U], value: int) -> U:
140
151
  """Create a new :class:`ULID`-object from an `int`."""
141
152
  return cls(int.to_bytes(value, constants.BYTES_LEN, "big"))
142
153
 
@@ -209,6 +220,10 @@ class ULID:
209
220
  """Encode this object as an integer."""
210
221
  return int.from_bytes(self.bytes, byteorder="big")
211
222
 
223
+ def __bytes__(self) -> bytes:
224
+ """Encode this object as byte sequence."""
225
+ return self.bytes
226
+
212
227
  def __lt__(self, other: Any) -> bool:
213
228
  if isinstance(other, ULID):
214
229
  return self.bytes < other.bytes
@@ -233,3 +248,34 @@ class ULID:
233
248
 
234
249
  def __hash__(self) -> int:
235
250
  return hash(self.bytes)
251
+
252
+ @classmethod
253
+ def __get_pydantic_core_schema__(cls, source: Any, handler: GetCoreSchemaHandler) -> CoreSchema:
254
+ from pydantic_core import core_schema
255
+
256
+ return core_schema.no_info_wrap_validator_function(
257
+ cls._pydantic_validate,
258
+ core_schema.union_schema(
259
+ [
260
+ core_schema.is_instance_schema(ULID),
261
+ core_schema.no_info_plain_validator_function(ULID),
262
+ ]
263
+ ),
264
+ )
265
+
266
+ @classmethod
267
+ def _pydantic_validate(cls, value: Any, handler: ValidatorFunctionWrapHandler) -> Any:
268
+ from pydantic_core import PydanticCustomError
269
+
270
+ try:
271
+ if isinstance(value, int):
272
+ ulid = cls.from_int(value)
273
+ elif isinstance(value, str):
274
+ ulid = cls.from_str(value)
275
+ elif isinstance(value, ULID):
276
+ ulid = value
277
+ else:
278
+ ulid = cls.from_bytes(value)
279
+ except ValueError as err:
280
+ raise PydanticCustomError("ulid_format", "Unrecognized format") from err
281
+ return handler(ulid)
ulid/__main__.py CHANGED
@@ -1,10 +1,14 @@
1
+ from __future__ import annotations
2
+
1
3
  import argparse
2
4
  import shutil
3
5
  import sys
4
6
  import textwrap
7
+ from collections.abc import Callable
5
8
  from collections.abc import Sequence
6
9
  from datetime import datetime
7
10
  from functools import partial
11
+ from typing import Any
8
12
  from uuid import UUID
9
13
 
10
14
  import ulid
@@ -47,37 +51,31 @@ def make_parser(prog: str | None = None) -> argparse.ArgumentParser:
47
51
  )
48
52
  b.add_argument(
49
53
  "--from-int",
50
- type=int,
51
54
  metavar="<int>",
52
55
  help="create from integer",
53
56
  )
54
57
  b.add_argument(
55
58
  "--from-hex",
56
- type=str,
57
59
  metavar="<str>",
58
60
  help="create from 32 character hex value",
59
61
  )
60
62
  b.add_argument(
61
63
  "--from-str",
62
- type=str,
63
64
  metavar="<str>",
64
65
  help="create from base32 encoded string of length 26",
65
66
  )
66
67
  b.add_argument(
67
68
  "--from-timestamp",
68
- type=parse_numeric,
69
69
  metavar="<int|float>",
70
70
  help="create from timestamp either as float in secs or int as millis",
71
71
  )
72
72
  b.add_argument(
73
73
  "--from-datetime",
74
- type=datetime.fromisoformat,
75
74
  metavar="<iso8601>",
76
75
  help="create from datetime. The timestamp part of the ULID will be taken from the datetime",
77
76
  )
78
77
  b.add_argument(
79
78
  "--from-uuid",
80
- type=UUID,
81
79
  metavar="<uuid>",
82
80
  help="create from given UUID. The timestamp part will be random.",
83
81
  )
@@ -85,7 +83,8 @@ def make_parser(prog: str | None = None) -> argparse.ArgumentParser:
85
83
 
86
84
  s = subparsers.add_parser("show", help="show properties of a ULID")
87
85
  s.add_argument("ulid", help="the ULID to inspect. The special value - reads from stdin")
88
- s.add_argument("--uuid", action="store_true", help="convert to UUID")
86
+ s.add_argument("--uuid", action="store_true", help="convert to fully random UUID")
87
+ s.add_argument("--uuid4", action="store_true", help="convert to RFC 4122 compliant UUIDv4")
89
88
  s.add_argument("--hex", action="store_true", help="convert to hex")
90
89
  s.add_argument("--int", action="store_true", help="convert to int")
91
90
  s.add_argument("--timestamp", "--ts", action="store_true", help="show timestamp")
@@ -94,6 +93,18 @@ def make_parser(prog: str | None = None) -> argparse.ArgumentParser:
94
93
  return parser
95
94
 
96
95
 
96
+ def main(argv: Sequence[str], prog: str | None = None) -> str:
97
+ args = make_parser(prog).parse_args(argv)
98
+ return args.func(args)
99
+
100
+
101
+ def from_value_or_stdin(value: str, convert: Callable[[str], Any] | None = None) -> Any:
102
+ value = sys.stdin.readline().strip() if value == "-" else value
103
+ if convert is not None:
104
+ return convert(value)
105
+ return value
106
+
107
+
97
108
  def parse_numeric(s: str) -> int | float:
98
109
  try:
99
110
  return int(s)
@@ -101,60 +112,54 @@ def parse_numeric(s: str) -> int | float:
101
112
  return float(s)
102
113
 
103
114
 
104
- def main(argv: Sequence[str], prog: str | None = None) -> None:
105
- args = make_parser(prog).parse_args(argv)
106
- args.func(args)
107
-
108
-
109
- def build(args: argparse.Namespace) -> None:
115
+ def build(args: argparse.Namespace) -> str:
110
116
  ulid: ULID
111
117
  if args.from_int is not None:
112
- ulid = ULID.from_int(args.from_int)
118
+ ulid = ULID.from_int(from_value_or_stdin(args.from_int, int))
113
119
  elif args.from_hex is not None:
114
- ulid = ULID.from_hex(args.from_hex)
120
+ ulid = ULID.from_hex(from_value_or_stdin(args.from_hex))
115
121
  elif args.from_str is not None:
116
- ulid = ULID.from_str(args.from_str)
122
+ ulid = ULID.from_str(from_value_or_stdin(args.from_str))
117
123
  elif args.from_timestamp is not None:
118
- ulid = ULID.from_timestamp(args.from_timestamp)
124
+ ulid = ULID.from_timestamp(from_value_or_stdin(args.from_timestamp, parse_numeric))
119
125
  elif args.from_datetime is not None:
120
- ulid = ULID.from_datetime(args.from_datetime)
126
+ ulid = ULID.from_datetime(from_value_or_stdin(args.from_datetime, datetime.fromisoformat))
121
127
  elif args.from_uuid is not None:
122
- ulid = ULID.from_uuid(args.from_uuid)
128
+ ulid = ULID.from_uuid(from_value_or_stdin(args.from_uuid, UUID))
123
129
  else:
124
130
  ulid = ULID()
125
- print(ulid)
131
+ return str(ulid)
126
132
 
127
133
 
128
- def show(args: argparse.Namespace) -> None:
129
- value = sys.stdin.readline().strip() if args.ulid == "-" else args.ulid
130
- ulid: ULID = ULID.from_str(value)
134
+ def show(args: argparse.Namespace) -> str:
135
+ ulid: ULID = ULID.from_str(from_value_or_stdin(args.ulid))
131
136
  if args.uuid:
132
- print(ulid.to_uuid())
137
+ return str(ulid.to_uuid())
138
+ elif args.uuid4:
139
+ return str(ulid.to_uuid4())
133
140
  elif args.hex:
134
- print(ulid.hex)
141
+ return ulid.hex
135
142
  elif args.int:
136
- print(int(ulid))
143
+ return str(int(ulid))
137
144
  elif args.timestamp:
138
- print(ulid.timestamp)
145
+ return str(ulid.timestamp)
139
146
  elif args.datetime:
140
- print(ulid.datetime)
147
+ return ulid.datetime.isoformat()
141
148
  else:
142
- print(
143
- textwrap.dedent(
144
- f"""
145
- ULID: {ulid!s}
146
- Hex: {ulid.hex}
147
- Int: {int(ulid)}
148
- Timestamp: {ulid.timestamp}
149
- Datetime: {ulid.datetime}
150
- """
151
- ).strip()
152
- )
149
+ return textwrap.dedent(
150
+ f"""
151
+ ULID: {ulid!s}
152
+ Hex: {ulid.hex}
153
+ Int: {int(ulid)}
154
+ Timestamp: {ulid.timestamp}
155
+ Datetime: {ulid.datetime.isoformat()}
156
+ """
157
+ ).strip()
153
158
 
154
159
 
155
- def entrypoint() -> None:
156
- main(sys.argv[1:])
160
+ def entrypoint() -> None: # pragma: no cover
161
+ print(main(sys.argv[1:]))
157
162
 
158
163
 
159
- if __name__ == "__main__":
164
+ if __name__ == "__main__": # pragma: no cover
160
165
  main(sys.argv[1:], "python -m ulid")
ulid/constants.py CHANGED
@@ -1,4 +1,5 @@
1
1
  MILLISECS_IN_SECS = 1000
2
+ NANOSECS_IN_MILLISECS = 1000000
2
3
 
3
4
  TIMESTAMP_LEN = 6
4
5
  RANDOMNESS_LEN = 10
@@ -1,10 +0,0 @@
1
- ulid/__init__.py,sha256=QctfUYCnmrMlSxlxFVol47UOC5wzc5kPR0CAMUXxdVI,7516
2
- ulid/__main__.py,sha256=RXiQNtc31xZOxqPmsnRHJMSnbvIiLHyBIFHcWC-Pka8,4817
3
- ulid/base32.py,sha256=OkU4FBqohJgV_ZRc3dTbjPlUTSSeC9ddFlPXD2vA8vc,5832
4
- ulid/constants.py,sha256=LHZ167q6iOJJ3giBcbNEwM-ahJYIrSvQ-1hHhpvLhHE,209
5
- ulid/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- python_ulid-2.1.0.dist-info/METADATA,sha256=i2Y5w0DtmNSfTf4f0PU5gXxUKIwu9OkP_ViKrgrLfiw,3676
7
- python_ulid-2.1.0.dist-info/WHEEL,sha256=9QBuHhg6FNW7lppboF2vKVbCGTVzsFykgRQjjlajrhA,87
8
- python_ulid-2.1.0.dist-info/entry_points.txt,sha256=9214ghzfSgS8a-TjjlWDF7bI7KSaeewOCBKn5uz7bdU,50
9
- python_ulid-2.1.0.dist-info/licenses/LICENSE,sha256=DiDPNhW5yUHIqaKowvX0c1c9xWiI0xW4ji8R9N2NJbw,1056
10
- python_ulid-2.1.0.dist-info/RECORD,,