python-ulid 2.2.0__py3-none-any.whl → 2.4.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,7 +1,10 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: python-ulid
3
- Version: 2.2.0
3
+ Version: 2.4.0
4
4
  Summary: Universally unique lexicographically sortable identifier
5
+ Project-URL: Homepage, https://github.com/mdomke/python-ulid
6
+ Project-URL: Repository, https://github.com/mdomke/python-ulid
7
+ Project-URL: Documentation, https://python-ulid.readthedocs.io/
5
8
  Author-email: Martin Domke <mail@martindomke.net>
6
9
  License-Expression: MIT
7
10
  License-File: LICENSE
@@ -15,8 +18,11 @@ Classifier: Programming Language :: Python
15
18
  Classifier: Programming Language :: Python :: 3.9
16
19
  Classifier: Programming Language :: Python :: 3.10
17
20
  Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
18
22
  Classifier: Topic :: Software Development :: Libraries
19
23
  Requires-Python: >=3.9
24
+ Provides-Extra: pydantic
25
+ Requires-Dist: pydantic>=2.0; extra == 'pydantic'
20
26
  Description-Content-Type: text/x-rst
21
27
 
22
28
 
@@ -57,6 +63,12 @@ Use ``pip`` to install the library
57
63
 
58
64
  $ pip install python-ulid
59
65
 
66
+ to include Pydantic support install the optional dependency like so
67
+
68
+ .. code-block:: bash
69
+
70
+ $ pip install python-ulid[pydantic]
71
+
60
72
  .. installation-end
61
73
 
62
74
  .. usage-begin
@@ -82,8 +94,8 @@ or use one of the named constructors
82
94
  >>> ULID.from_datetime(datetime.datetime.now())
83
95
  ULID(01E75J2XBK390V2XRH44EHC10X)
84
96
 
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:
97
+ There are several options for encoding the ``ULID`` object
98
+ (e.g. string, hex, int, bytes, UUID):
87
99
 
88
100
  .. code-block:: pycon
89
101
 
@@ -91,15 +103,47 @@ as well as to access the timestamp attribute in different formats:
91
103
  '01BTGNYV6HRNK8K8VKZASZCFPE'
92
104
  >>> ulid.hex
93
105
  '015ea15f6cd1c56689a373fab3f63ece'
106
+ >>> int(ulid)
107
+ 1820576928786795198723644692628913870
108
+ >>> bytes(ulid)
109
+ b'\x01^\xa1_l\xd1\xc5f\x89\xa3s\xfa\xb3\xf6>\xce'
110
+ >>> ulid.to_uuid()
111
+ UUID('015ea15f-6cd1-c566-89a3-73fab3f63ece')
112
+
113
+ It is also possible to directly access the timestamp component of a ``ULID``,
114
+ either in UNIX epoch or as ``datetime.datetime``
115
+
116
+ .. code-block:: pycon
117
+
94
118
  >>> ulid.timestamp
95
119
  1505945939.153
96
120
  >>> ulid.datetime
97
121
  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
122
 
101
123
  .. usage-end
102
124
 
125
+ .. pydantic-begin
126
+
127
+ Pydantic integration
128
+ ---------------------
129
+
130
+ The ``ULID`` class can be directly used for the popular data validation library
131
+ `Pydantic <https://docs.pydantic.dev/latest/>`_ like so
132
+
133
+ .. code-block:: python
134
+
135
+ from pydantic import BaseModel
136
+ from ulid import ULID
137
+
138
+
139
+ class Model(BaseModel):
140
+ ulid: ULID
141
+
142
+ model = Model(ulid="DX89370400440532013000") # OK
143
+ model = Model(ulid="not-a-ulid") # Raises ValidationError
144
+
145
+ .. pydantic-end
146
+
103
147
  .. cli-begin
104
148
 
105
149
  Command line interface
@@ -124,6 +168,14 @@ and convert ULIDs, e.g.
124
168
  Timestamp: 1695219822.248
125
169
  Datetime: 2023-09-20 14:23:42.248000+00:00
126
170
 
171
+ There are several flags to select specific output formats for the ``show`` command, e.g.
172
+
173
+
174
+ .. code-block:: bash
175
+
176
+ $ ulid show --datetime 01HASFKBN8SKZTSVVS03K5AMMS
177
+ 2023-09-20 14:23:42.248000+00:00
178
+
127
179
  The special character ``-`` allows to read values from ``stdin`` so that they can be piped. E.g.
128
180
 
129
181
  .. code-block:: bash
@@ -134,6 +186,9 @@ The special character ``-`` allows to read values from ``stdin`` so that they ca
134
186
  $ date --iso-8601 | python -m ulid build --from-datetime -
135
187
  01HAT9PVR02T3S13XB48S7GEHE
136
188
 
189
+ For a full overview of flags for the ``build`` and ``show`` commands use the ``--help`` option
190
+ (e.g. ``ulid show --help``).
191
+
137
192
  .. cli-end
138
193
 
139
194
  Other implementations
@@ -142,3 +197,7 @@ Other implementations
142
197
  * `ahawker/ulid <https://github.com/ahawker/ulid>`_
143
198
  * `valohai/ulid2 <https://github.com/valohai/ulid2>`_
144
199
  * `mdipierro/ulid <https://github.com/mdipierro/ulid>`_
200
+ * `oklog/ulid <https://github.com/oklog/ulid>`_
201
+ * `ulid/javascript <https://github.com/ulid/javascript>`_
202
+ * `RobThree/NUlid <https://github.com/RobThree/NUlid>`_
203
+ * `imdario/go-ulid <https://github.com/imdario/go-ulid>`_
@@ -0,0 +1,10 @@
1
+ ulid/__init__.py,sha256=7wY5wUQGwcUhreFESFX6MCrimPbaJOjXUY0XEZvU9Cg,9235
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.4.0.dist-info/METADATA,sha256=ET_sWGfqWfhjEiThmFtNrpHet16cPGEsJe4NKVOgKbU,5570
7
+ python_ulid-2.4.0.dist-info/WHEEL,sha256=uNdcs2TADwSd5pVaP0Z_kcjcvvTUklh2S7bxZMF8Uj0,87
8
+ python_ulid-2.4.0.dist-info/entry_points.txt,sha256=9214ghzfSgS8a-TjjlWDF7bI7KSaeewOCBKn5uz7bdU,50
9
+ python_ulid-2.4.0.dist-info/licenses/LICENSE,sha256=DiDPNhW5yUHIqaKowvX0c1c9xWiI0xW4ji8R9N2NJbw,1056
10
+ python_ulid-2.4.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.4
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,37 @@ 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
+ serialization=core_schema.to_string_ser_schema(
265
+ when_used="json-unless-none",
266
+ ),
267
+ )
268
+
269
+ @classmethod
270
+ def _pydantic_validate(cls, value: Any, handler: ValidatorFunctionWrapHandler) -> Any:
271
+ from pydantic_core import PydanticCustomError
272
+
273
+ try:
274
+ if isinstance(value, int):
275
+ ulid = cls.from_int(value)
276
+ elif isinstance(value, str):
277
+ ulid = cls.from_str(value)
278
+ elif isinstance(value, ULID):
279
+ ulid = value
280
+ else:
281
+ ulid = cls.from_bytes(value)
282
+ except ValueError as err:
283
+ raise PydanticCustomError("ulid_format", "Unrecognized format") from err
284
+ return handler(ulid)
ulid/__main__.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import argparse
2
4
  import shutil
3
5
  import sys
@@ -7,7 +9,6 @@ from collections.abc import Sequence
7
9
  from datetime import datetime
8
10
  from functools import partial
9
11
  from typing import Any
10
- from typing import Optional
11
12
  from uuid import UUID
12
13
 
13
14
  import ulid
@@ -92,12 +93,12 @@ def make_parser(prog: str | None = None) -> argparse.ArgumentParser:
92
93
  return parser
93
94
 
94
95
 
95
- def main(argv: Sequence[str], prog: str | None = None) -> None:
96
+ def main(argv: Sequence[str], prog: str | None = None) -> str:
96
97
  args = make_parser(prog).parse_args(argv)
97
- args.func(args)
98
+ return args.func(args)
98
99
 
99
100
 
100
- def from_value_or_stdin(value: str, convert: Optional[Callable[[str], Any]] = None) -> Any:
101
+ def from_value_or_stdin(value: str, convert: Callable[[str], Any] | None = None) -> Any:
101
102
  value = sys.stdin.readline().strip() if value == "-" else value
102
103
  if convert is not None:
103
104
  return convert(value)
@@ -111,7 +112,7 @@ def parse_numeric(s: str) -> int | float:
111
112
  return float(s)
112
113
 
113
114
 
114
- def build(args: argparse.Namespace) -> None:
115
+ def build(args: argparse.Namespace) -> str:
115
116
  ulid: ULID
116
117
  if args.from_int is not None:
117
118
  ulid = ULID.from_int(from_value_or_stdin(args.from_int, int))
@@ -127,40 +128,38 @@ def build(args: argparse.Namespace) -> None:
127
128
  ulid = ULID.from_uuid(from_value_or_stdin(args.from_uuid, UUID))
128
129
  else:
129
130
  ulid = ULID()
130
- print(ulid)
131
+ return str(ulid)
131
132
 
132
133
 
133
- def show(args: argparse.Namespace) -> None:
134
+ def show(args: argparse.Namespace) -> str:
134
135
  ulid: ULID = ULID.from_str(from_value_or_stdin(args.ulid))
135
136
  if args.uuid:
136
- print(ulid.to_uuid())
137
+ return str(ulid.to_uuid())
137
138
  elif args.uuid4:
138
- print(ulid.to_uuid4())
139
+ return str(ulid.to_uuid4())
139
140
  elif args.hex:
140
- print(ulid.hex)
141
+ return ulid.hex
141
142
  elif args.int:
142
- print(int(ulid))
143
+ return str(int(ulid))
143
144
  elif args.timestamp:
144
- print(ulid.timestamp)
145
+ return str(ulid.timestamp)
145
146
  elif args.datetime:
146
- print(ulid.datetime)
147
+ return ulid.datetime.isoformat()
147
148
  else:
148
- print(
149
- textwrap.dedent(
150
- f"""
151
- ULID: {ulid!s}
152
- Hex: {ulid.hex}
153
- Int: {int(ulid)}
154
- Timestamp: {ulid.timestamp}
155
- Datetime: {ulid.datetime}
156
- """
157
- ).strip()
158
- )
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()
159
158
 
160
159
 
161
- def entrypoint() -> None:
162
- main(sys.argv[1:])
160
+ def entrypoint() -> None: # pragma: no cover
161
+ print(main(sys.argv[1:]))
163
162
 
164
163
 
165
- if __name__ == "__main__":
164
+ if __name__ == "__main__": # pragma: no cover
166
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=HXSnwi6_a_2RNSKJMyZ2ndX_LwyCcxNCW2l2uiflmSk,5290
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.2.0.dist-info/METADATA,sha256=8dBkMNE00iscfhhgMeM1fEzriT6ynyJ-6nXrO1lN66g,3893
7
- python_ulid-2.2.0.dist-info/WHEEL,sha256=9QBuHhg6FNW7lppboF2vKVbCGTVzsFykgRQjjlajrhA,87
8
- python_ulid-2.2.0.dist-info/entry_points.txt,sha256=9214ghzfSgS8a-TjjlWDF7bI7KSaeewOCBKn5uz7bdU,50
9
- python_ulid-2.2.0.dist-info/licenses/LICENSE,sha256=DiDPNhW5yUHIqaKowvX0c1c9xWiI0xW4ji8R9N2NJbw,1056
10
- python_ulid-2.2.0.dist-info/RECORD,,