python-ulid 3.0.0__py3-none-any.whl → 3.1.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.3
1
+ Metadata-Version: 2.4
2
2
  Name: python-ulid
3
- Version: 3.0.0
3
+ Version: 3.1.0
4
4
  Summary: Universally unique lexicographically sortable identifier
5
5
  Project-URL: Homepage, https://github.com/mdomke/python-ulid
6
6
  Project-URL: Documentation, https://python-ulid.readthedocs.io
@@ -22,6 +22,7 @@ Classifier: Programming Language :: Python :: 3.9
22
22
  Classifier: Programming Language :: Python :: 3.10
23
23
  Classifier: Programming Language :: Python :: 3.11
24
24
  Classifier: Programming Language :: Python :: 3.12
25
+ Classifier: Programming Language :: Python :: 3.13
25
26
  Classifier: Topic :: Software Development :: Libraries
26
27
  Requires-Python: >=3.9
27
28
  Provides-Extra: pydantic
@@ -0,0 +1,10 @@
1
+ ulid/__init__.py,sha256=g9Fb4nyq2mt3wGtLdmv3h9HutFJXLsrv_3HiVM-Qph8,12419
2
+ ulid/__main__.py,sha256=TUo3dkWkJLfzRJyd55nfqGqvv1NcG7fBpsZHIi_p4rw,5421
3
+ ulid/base32.py,sha256=iUE5BGPW5Dul-Eu3ITvcymk8acNkIFAVTlrL12YIPKI,5961
4
+ ulid/constants.py,sha256=lbCLRNQXq8qnOdz_w-nJf_j5dC2X7Gjv-8GJUbs5jX8,503
5
+ ulid/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ python_ulid-3.1.0.dist-info/METADATA,sha256=TM_dpKEq830WTj6mahb6qF_5wiVnL6-Rau-6REqSoSQ,5833
7
+ python_ulid-3.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
8
+ python_ulid-3.1.0.dist-info/entry_points.txt,sha256=9214ghzfSgS8a-TjjlWDF7bI7KSaeewOCBKn5uz7bdU,50
9
+ python_ulid-3.1.0.dist-info/licenses/LICENSE,sha256=DiDPNhW5yUHIqaKowvX0c1c9xWiI0xW4ji8R9N2NJbw,1056
10
+ python_ulid-3.1.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.25.0
2
+ Generator: hatchling 1.27.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
ulid/__init__.py CHANGED
@@ -6,12 +6,15 @@ import time
6
6
  import uuid
7
7
  from datetime import datetime
8
8
  from datetime import timezone
9
+ from threading import Lock
9
10
  from typing import Any
10
11
  from typing import cast
11
12
  from typing import Generic
12
13
  from typing import TYPE_CHECKING
13
14
  from typing import TypeVar
14
15
 
16
+ from typing_extensions import Self
17
+
15
18
  from ulid import base32
16
19
  from ulid import constants
17
20
 
@@ -51,11 +54,44 @@ class validate_type(Generic[T]): # noqa: N801
51
54
  return wrapped
52
55
 
53
56
 
54
- U = TypeVar("U", bound="ULID")
57
+ class ValueProvider:
58
+ def __init__(self) -> None:
59
+ self.lock = Lock()
60
+ self.prev_timestamp = constants.MIN_TIMESTAMP
61
+ self.prev_randomness = constants.MIN_RANDOMNESS
62
+
63
+ def timestamp(self, value: float | None = None) -> int:
64
+ if value is None:
65
+ value = time.time_ns() // constants.NANOSECS_IN_MILLISECS
66
+ elif isinstance(value, float):
67
+ value = int(value * constants.MILLISECS_IN_SECS)
68
+ if value > constants.MAX_TIMESTAMP:
69
+ raise ValueError("Value exceeds maximum possible timestamp")
70
+ return value
71
+
72
+ def randomness(self) -> bytes:
73
+ with self.lock:
74
+ current_timestamp = self.timestamp()
75
+ if current_timestamp == self.prev_timestamp:
76
+ if self.prev_randomness == constants.MAX_RANDOMNESS:
77
+ raise ValueError("Randomness within same millisecond exhausted")
78
+ randomness = self.increment_bytes(self.prev_randomness)
79
+ else:
80
+ randomness = os.urandom(constants.RANDOMNESS_LEN)
81
+
82
+ self.prev_randomness = randomness
83
+ self.prev_timestamp = current_timestamp
84
+ return randomness
85
+
86
+ def increment_bytes(self, value: bytes) -> bytes:
87
+ length = len(value)
88
+ return (int.from_bytes(value, byteorder="big") + 1).to_bytes(length, byteorder="big")
55
89
 
56
90
 
57
91
  @functools.total_ordering
58
92
  class ULID:
93
+ provider = ValueProvider()
94
+
59
95
  """The :class:`ULID` object consists of a timestamp part of 48 bits and of 80 random bits.
60
96
 
61
97
  .. code-block:: text
@@ -83,13 +119,11 @@ class ULID:
83
119
  def __init__(self, value: bytes | None = None) -> None:
84
120
  if value is not None and len(value) != constants.BYTES_LEN:
85
121
  raise ValueError("ULID has to be exactly 16 bytes long.")
86
- self.bytes: bytes = (
87
- value or ULID.from_timestamp(time.time_ns() // constants.NANOSECS_IN_MILLISECS).bytes
88
- )
122
+ self.bytes: bytes = value or ULID.from_timestamp(self.provider.timestamp()).bytes
89
123
 
90
124
  @classmethod
91
125
  @validate_type(datetime)
92
- def from_datetime(cls: type[U], value: datetime) -> U:
126
+ def from_datetime(cls, value: datetime) -> Self:
93
127
  """Create a new :class:`ULID`-object from a :class:`datetime`. The timestamp part of the
94
128
  `ULID` will be set to the corresponding timestamp of the datetime.
95
129
 
@@ -103,7 +137,7 @@ class ULID:
103
137
 
104
138
  @classmethod
105
139
  @validate_type(int, float)
106
- def from_timestamp(cls: type[U], value: float) -> U:
140
+ def from_timestamp(cls, value: float) -> Self:
107
141
  """Create a new :class:`ULID`-object from a timestamp. The timestamp can be either a
108
142
  `float` representing the time in seconds (as it would be returned by :func:`time.time()`)
109
143
  or an `int` in milliseconds.
@@ -114,15 +148,13 @@ class ULID:
114
148
  >>> ULID.from_timestamp(time.time())
115
149
  ULID(01E75QWN5HKQ0JAVX9FG1K4YP4)
116
150
  """
117
- if isinstance(value, float):
118
- value = int(value * constants.MILLISECS_IN_SECS)
119
- timestamp = int.to_bytes(value, constants.TIMESTAMP_LEN, "big")
120
- randomness = os.urandom(constants.RANDOMNESS_LEN)
151
+ timestamp = int.to_bytes(cls.provider.timestamp(value), constants.TIMESTAMP_LEN, "big")
152
+ randomness = cls.provider.randomness()
121
153
  return cls.from_bytes(timestamp + randomness)
122
154
 
123
155
  @classmethod
124
156
  @validate_type(uuid.UUID)
125
- def from_uuid(cls: type[U], value: uuid.UUID) -> U:
157
+ def from_uuid(cls, value: uuid.UUID) -> Self:
126
158
  """Create a new :class:`ULID`-object from a :class:`uuid.UUID`. The timestamp part will be
127
159
  random in that case.
128
160
 
@@ -136,37 +168,38 @@ class ULID:
136
168
 
137
169
  @classmethod
138
170
  @validate_type(bytes)
139
- def from_bytes(cls: type[U], bytes_: bytes) -> U:
171
+ def from_bytes(cls, bytes_: bytes) -> Self:
140
172
  """Create a new :class:`ULID`-object from sequence of 16 bytes."""
141
173
  return cls(bytes_)
142
174
 
143
175
  @classmethod
144
176
  @validate_type(str)
145
- def from_hex(cls: type[U], value: str) -> U:
177
+ def from_hex(cls, value: str) -> Self:
146
178
  """Create a new :class:`ULID`-object from 32 character string of hex values."""
147
179
  return cls.from_bytes(bytes.fromhex(value))
148
180
 
149
181
  @classmethod
150
182
  @validate_type(str)
151
- def from_str(cls: type[U], string: str) -> U:
183
+ def from_str(cls, string: str) -> Self:
152
184
  """Create a new :class:`ULID`-object from a 26 char long string representation."""
153
185
  return cls(base32.decode(string))
154
186
 
155
187
  @classmethod
156
188
  @validate_type(int)
157
- def from_int(cls: type[U], value: int) -> U:
189
+ def from_int(cls, value: int) -> Self:
158
190
  """Create a new :class:`ULID`-object from an `int`."""
159
191
  return cls(int.to_bytes(value, constants.BYTES_LEN, "big"))
160
192
 
161
193
  @classmethod
162
- def parse(cls: type[U], value: Any) -> U:
194
+ def parse(cls, value: Any) -> Self:
163
195
  """Create a new :class:`ULID`-object from a given value.
164
196
 
165
- .. note:: This method should only be used when the caller is trying to parse a ULID from
166
- a value when they're unsure what format/primitive type it will be given in.
197
+ .. note::
198
+ This method should only be used when the caller is trying to parse a ULID from
199
+ a value when they're unsure what format/primitive type it will be given in.
167
200
  """
168
201
  if isinstance(value, ULID):
169
- return cast(U, value)
202
+ return cast(Self, value)
170
203
  if isinstance(value, uuid.UUID):
171
204
  return cls.from_uuid(value)
172
205
  if isinstance(value, str):
@@ -190,7 +223,7 @@ class ULID:
190
223
  return cls.from_bytes(value)
191
224
  raise TypeError(f"Cannot parse ULID from type {type(value)}")
192
225
 
193
- @property
226
+ @functools.cached_property
194
227
  def milliseconds(self) -> int:
195
228
  """The timestamp part as epoch time in milliseconds.
196
229
 
@@ -201,7 +234,7 @@ class ULID:
201
234
  """
202
235
  return int.from_bytes(self.bytes[: constants.TIMESTAMP_LEN], byteorder="big")
203
236
 
204
- @property
237
+ @functools.cached_property
205
238
  def timestamp(self) -> float:
206
239
  """The timestamp part as epoch time in seconds.
207
240
 
@@ -212,7 +245,7 @@ class ULID:
212
245
  """
213
246
  return self.milliseconds / constants.MILLISECS_IN_SECS
214
247
 
215
- @property
248
+ @functools.cached_property
216
249
  def datetime(self) -> datetime:
217
250
  """Return the timestamp part as timezone-aware :class:`datetime` in UTC.
218
251
 
@@ -223,7 +256,7 @@ class ULID:
223
256
  """
224
257
  return datetime.fromtimestamp(self.timestamp, timezone.utc)
225
258
 
226
- @property
259
+ @functools.cached_property
227
260
  def hex(self) -> str:
228
261
  """Encode the :class:`ULID`-object as a 32 char sequence of hex values."""
229
262
  return self.bytes.hex()
@@ -297,7 +330,11 @@ class ULID:
297
330
  core_schema.union_schema([
298
331
  core_schema.is_instance_schema(ULID),
299
332
  core_schema.no_info_plain_validator_function(ULID),
300
- core_schema.str_schema(pattern=r"[A-Z0-9]{26}", min_length=26, max_length=26),
333
+ core_schema.str_schema(
334
+ pattern=rf"[0-7][{base32.ENCODE}]{{25}}",
335
+ min_length=26,
336
+ max_length=26,
337
+ ),
301
338
  core_schema.bytes_schema(min_length=16, max_length=16),
302
339
  ]),
303
340
  serialization=core_schema.to_string_ser_schema(
@@ -309,6 +346,7 @@ class ULID:
309
346
  def _pydantic_validate(cls, value: Any, handler: ValidatorFunctionWrapHandler) -> Any:
310
347
  from pydantic_core import PydanticCustomError
311
348
 
349
+ ulid: ULID
312
350
  try:
313
351
  if isinstance(value, int):
314
352
  ulid = cls.from_int(value)
ulid/constants.py CHANGED
@@ -1,6 +1,12 @@
1
1
  MILLISECS_IN_SECS = 1000
2
2
  NANOSECS_IN_MILLISECS = 1000000
3
3
 
4
+ MIN_RANDOMNESS = b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
5
+ MAX_RANDOMNESS = b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"
6
+
7
+ MIN_TIMESTAMP = 0
8
+ MAX_TIMESTAMP = 2**48 - 1
9
+
4
10
  TIMESTAMP_LEN = 6
5
11
  RANDOMNESS_LEN = 10
6
12
  BYTES_LEN = TIMESTAMP_LEN + RANDOMNESS_LEN
@@ -1,10 +0,0 @@
1
- ulid/__init__.py,sha256=yoBtXpTz-t8xnfOHtM70-hYIeYKk75ya7t6B6uWILhg,10971
2
- ulid/__main__.py,sha256=TUo3dkWkJLfzRJyd55nfqGqvv1NcG7fBpsZHIi_p4rw,5421
3
- ulid/base32.py,sha256=iUE5BGPW5Dul-Eu3ITvcymk8acNkIFAVTlrL12YIPKI,5961
4
- ulid/constants.py,sha256=tJByGuryoTT_xfECvpMNPNwRvZMp2K_ZlZ7tOLeaXJs,335
5
- ulid/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- python_ulid-3.0.0.dist-info/METADATA,sha256=82s8fmElXLy5Um_bjqhtZfiXd54YF0TDI2iOlKliLMs,5782
7
- python_ulid-3.0.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
8
- python_ulid-3.0.0.dist-info/entry_points.txt,sha256=9214ghzfSgS8a-TjjlWDF7bI7KSaeewOCBKn5uz7bdU,50
9
- python_ulid-3.0.0.dist-info/licenses/LICENSE,sha256=DiDPNhW5yUHIqaKowvX0c1c9xWiI0xW4ji8R9N2NJbw,1056
10
- python_ulid-3.0.0.dist-info/RECORD,,