whatamithinking-idlib 1.0.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.
- whatamithinking/idlib/__init__.py +491 -0
- whatamithinking_idlib-1.0.0.dist-info/METADATA +182 -0
- whatamithinking_idlib-1.0.0.dist-info/RECORD +6 -0
- whatamithinking_idlib-1.0.0.dist-info/WHEEL +5 -0
- whatamithinking_idlib-1.0.0.dist-info/licenses/LICENSE +21 -0
- whatamithinking_idlib-1.0.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
__version__ = "1.0.0"
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
from functools import total_ordering
|
|
5
|
+
from typing import Any, Optional, TypeVar, Union
|
|
6
|
+
import secrets
|
|
7
|
+
import time
|
|
8
|
+
import datetime
|
|
9
|
+
from contextvars import ContextVar
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
from whatamithinking.hostutil import (
|
|
13
|
+
normalize,
|
|
14
|
+
is_valid_hostname,
|
|
15
|
+
get_hostname,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
T_UOID = TypeVar("T_UOID", bound="UOID")
|
|
20
|
+
T_UOHID = TypeVar("T_UOHID", bound="UOHID")
|
|
21
|
+
T_UOPHID = TypeVar("T_UOPHID", bound="UOPHID")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@total_ordering
|
|
25
|
+
class UOID:
|
|
26
|
+
"""Unique ordered ID: time-random_bytes
|
|
27
|
+
|
|
28
|
+
Length = 16 bytes (32 chars)
|
|
29
|
+
|
|
30
|
+
This unique id format was designed to be sequential, so that all
|
|
31
|
+
subsequent ID's generated will be greater than the previous ones
|
|
32
|
+
in lexicographic order.
|
|
33
|
+
|
|
34
|
+
Not threadsafe.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
__slots__ = "_bytes", "_str", "_hash", "__weakref__"
|
|
38
|
+
_last_id: Optional[bytes] = None
|
|
39
|
+
_last_time_ns: Optional[int] = None
|
|
40
|
+
_last_rand_bytes: Optional[bytearray] = None
|
|
41
|
+
_time_byte_length = 10
|
|
42
|
+
_rand_byte_length = 6
|
|
43
|
+
_MIN_BYTE_LENGTH = _time_byte_length + _rand_byte_length
|
|
44
|
+
_MAX_BYTE_LENGTH = _time_byte_length + _rand_byte_length
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
value: Union[None, bytes, bytearray, str, T_UOID] = None,
|
|
49
|
+
timeout: float = 0.030,
|
|
50
|
+
**kwargs,
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Init
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
value: Optional. id bytes or encoded string or instance of this object.
|
|
56
|
+
If None, generates a new id. Defaults to None.
|
|
57
|
+
timeout: Optional. Max number of seconds to spend trying go create a new
|
|
58
|
+
id before timing out. Defaults to 0.030 seconds.
|
|
59
|
+
"""
|
|
60
|
+
self._bytes = value
|
|
61
|
+
if self._bytes is None:
|
|
62
|
+
self._bytes = self.new(timeout=timeout, **kwargs)
|
|
63
|
+
elif isinstance(value, str):
|
|
64
|
+
self._bytes = self._decode(value)
|
|
65
|
+
elif isinstance(value, type(self)):
|
|
66
|
+
self._bytes = bytes(value)
|
|
67
|
+
if len(self._bytes) > self._MAX_BYTE_LENGTH:
|
|
68
|
+
raise ValueError(
|
|
69
|
+
f"{type(self).__name__} bytes length, {len(self._bytes)}, "
|
|
70
|
+
+ f"exceeded max, {self._MAX_BYTE_LENGTH}."
|
|
71
|
+
)
|
|
72
|
+
if len(self._bytes) < self._MIN_BYTE_LENGTH:
|
|
73
|
+
raise ValueError(
|
|
74
|
+
f"{type(self).__name__} bytes length, {len(self._bytes)}, "
|
|
75
|
+
+ f"exceeded min, {self._MIN_BYTE_LENGTH}."
|
|
76
|
+
)
|
|
77
|
+
self._str: Optional[str] = None
|
|
78
|
+
self._hash: Optional[int] = None
|
|
79
|
+
# test we can parse all fields
|
|
80
|
+
self.time
|
|
81
|
+
self.rand
|
|
82
|
+
|
|
83
|
+
def _encode(self, value: bytes) -> str:
|
|
84
|
+
return base64.b32hexencode(value).decode("ascii")
|
|
85
|
+
|
|
86
|
+
def _decode(self, value: bytes | str) -> bytes:
|
|
87
|
+
return base64.b32hexdecode(value)
|
|
88
|
+
|
|
89
|
+
@classmethod
|
|
90
|
+
def new(cls, timeout: float = 0.030) -> bytes:
|
|
91
|
+
"""Generate and return the bytes for a new id.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
timeout: Optional. Max number of seconds to take to generate
|
|
95
|
+
a new id. This is only required in edge cases where there
|
|
96
|
+
is clock drift and the clock went backwards from the last
|
|
97
|
+
id generated in which case this function will sleep for
|
|
98
|
+
a short duration and then try again.
|
|
99
|
+
"""
|
|
100
|
+
deadline = time.monotonic() + timeout
|
|
101
|
+
while True:
|
|
102
|
+
time_ns = time.time_ns()
|
|
103
|
+
if cls._last_time_ns is None or time_ns > cls._last_time_ns:
|
|
104
|
+
rand_bytes = secrets.token_bytes(cls._rand_byte_length)
|
|
105
|
+
else:
|
|
106
|
+
# auto-increment the random bytes if inside same timestamp
|
|
107
|
+
# so ids are still sequential even when generation happening
|
|
108
|
+
# faster than clock changing
|
|
109
|
+
rand_int = int.from_bytes(cls._last_rand_bytes, "big") + 1
|
|
110
|
+
if (
|
|
111
|
+
rand_int.bit_length()
|
|
112
|
+
> cls._rand_byte_length * cls._time_byte_length
|
|
113
|
+
):
|
|
114
|
+
raise RuntimeError(
|
|
115
|
+
"Random bytes overflow from too many "
|
|
116
|
+
+ "ids being generated for the same timestamp."
|
|
117
|
+
)
|
|
118
|
+
rand_bytes = rand_int.to_bytes(cls._rand_byte_length, "big")
|
|
119
|
+
cls._last_time_ns = time_ns
|
|
120
|
+
cls._last_rand_bytes = rand_bytes
|
|
121
|
+
time_ns_bytes = time_ns.to_bytes(cls._time_byte_length, "big")
|
|
122
|
+
id_bytes = b"".join([time_ns_bytes, rand_bytes])
|
|
123
|
+
if cls._last_id is not None and id_bytes < cls._last_id:
|
|
124
|
+
if time.monotonic() > deadline:
|
|
125
|
+
raise RuntimeError(
|
|
126
|
+
"Clock drift prevented an incremental id from "
|
|
127
|
+
"being generated within the timeout."
|
|
128
|
+
)
|
|
129
|
+
time.sleep(0.000005)
|
|
130
|
+
continue
|
|
131
|
+
cls._last_id = id_bytes
|
|
132
|
+
return id_bytes
|
|
133
|
+
|
|
134
|
+
def __eq__(self, other: Any) -> bool:
|
|
135
|
+
if other is None:
|
|
136
|
+
return False
|
|
137
|
+
if isinstance(other, str):
|
|
138
|
+
return str(self) == str(other)
|
|
139
|
+
return bytes(self) == bytes(other)
|
|
140
|
+
|
|
141
|
+
def __lt__(self, other: Any) -> bool:
|
|
142
|
+
if other is None:
|
|
143
|
+
return False
|
|
144
|
+
if isinstance(other, str):
|
|
145
|
+
return str(self) < str(other)
|
|
146
|
+
return bytes(self) < bytes(other)
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def time(self) -> int:
|
|
150
|
+
"""Return nanosecond epoch time encoded in the id."""
|
|
151
|
+
return int.from_bytes(self._bytes[0 : self._time_byte_length], "big")
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def rand(self) -> bytes:
|
|
155
|
+
"""Return the random bytes part of id."""
|
|
156
|
+
return self._bytes[
|
|
157
|
+
self._time_byte_length : self._time_byte_length + self._rand_byte_length
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
def __repr__(self) -> str:
|
|
161
|
+
dec_time = datetime.datetime.fromtimestamp(self.time / 1_000_000_000).strftime(
|
|
162
|
+
"%Y-%m-%d %H:%M:%S"
|
|
163
|
+
)
|
|
164
|
+
return f"<{type(self).__name__} time={dec_time}.{self.time%1_000_000_000} rand={self._encode(self.rand)}>"
|
|
165
|
+
|
|
166
|
+
def __str__(self) -> str:
|
|
167
|
+
"""32-character b16 encoded version of the resource id,
|
|
168
|
+
which maintains the same sort order as the bytes."""
|
|
169
|
+
if self._str is None:
|
|
170
|
+
self._str = self._encode(self._bytes)
|
|
171
|
+
return self._str
|
|
172
|
+
|
|
173
|
+
def __bytes__(self) -> bytes:
|
|
174
|
+
return self._bytes
|
|
175
|
+
|
|
176
|
+
def __hash__(self) -> int:
|
|
177
|
+
"""Return the hash of the string of this id."""
|
|
178
|
+
if self._hash is None:
|
|
179
|
+
self._hash = hash(str(self))
|
|
180
|
+
return self._hash
|
|
181
|
+
|
|
182
|
+
def replace(
|
|
183
|
+
self, time: Optional[int] = None, rand: Optional[bytearray] = None
|
|
184
|
+
) -> T_UOID:
|
|
185
|
+
"""Replace parts of the UOID and return a new instance with those
|
|
186
|
+
replaced parts.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
time: Optional. nanoseconds since epoch integer.
|
|
190
|
+
rand: Optional. random bytes.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
A new instance of this class
|
|
194
|
+
"""
|
|
195
|
+
parts = []
|
|
196
|
+
if time is None:
|
|
197
|
+
parts.append(self._bytes[: self._time_byte_length])
|
|
198
|
+
else:
|
|
199
|
+
parts.append(time.to_bytes(self._time_byte_length, "big"))
|
|
200
|
+
if rand is None:
|
|
201
|
+
parts.append(
|
|
202
|
+
self._bytes[
|
|
203
|
+
self._time_byte_length : self._time_byte_length
|
|
204
|
+
+ self._rand_byte_length
|
|
205
|
+
]
|
|
206
|
+
)
|
|
207
|
+
else:
|
|
208
|
+
parts.append(rand)
|
|
209
|
+
return type(self)(b"".join(parts))
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
_encoded_localhost = get_hostname().encode("ascii")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class UOHID(UOID):
|
|
216
|
+
"""Unique ordered host ID: time-random_bytes-hostname
|
|
217
|
+
|
|
218
|
+
Min Length (10 (time) + 6 (random) + 1 (hostname)) = 17 bytes (32 chars)
|
|
219
|
+
Max Length (10 (time) + 6 (random) + 64 (hostname)) = 80 bytes (128 chars)
|
|
220
|
+
|
|
221
|
+
This unique id format was designed to be sequential, so that all
|
|
222
|
+
subsequent ID's generated will be greater than the previous ones
|
|
223
|
+
in lexicographic order.
|
|
224
|
+
|
|
225
|
+
Additionally, this encodes the hostname of the machine it was generated
|
|
226
|
+
on. This was done on purpose to simplify sharing resources across services
|
|
227
|
+
by allowing one service to proxy to another using the hostname encoded
|
|
228
|
+
in the resource id. When converted to a string, the ID is base32hex encoded
|
|
229
|
+
to avoid leaking this implementation detail to clients. base32hex maintains
|
|
230
|
+
the sort order of the data it encodes.
|
|
231
|
+
|
|
232
|
+
WARNING: This ID should not be used in cases where security is important,
|
|
233
|
+
because it leaks the hostnames of the machines hosting the services.
|
|
234
|
+
|
|
235
|
+
Not threadsafe.
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
__slots__ = []
|
|
239
|
+
_min_hostname_byte_length = 1 # based on limits from linux and windows machines
|
|
240
|
+
_max_hostname_byte_length = 64 # based on limits from linux and windows machines
|
|
241
|
+
_MIN_BYTE_LENGTH = (
|
|
242
|
+
UOID._time_byte_length + UOID._rand_byte_length + _min_hostname_byte_length
|
|
243
|
+
)
|
|
244
|
+
_MAX_BYTE_LENGTH = (
|
|
245
|
+
UOID._time_byte_length + UOID._rand_byte_length + _max_hostname_byte_length
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
def __init__(
|
|
249
|
+
self, value: Union[None, bytes, bytearray, str, T_UOHID] = None, **kwargs
|
|
250
|
+
) -> None:
|
|
251
|
+
"""Init
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
value: id bytes or encoded string or instance of this object
|
|
255
|
+
timeout: Optional. Max number of seconds to spend trying go create a new
|
|
256
|
+
id before timing out. Defaults to 0.030 seconds.
|
|
257
|
+
"""
|
|
258
|
+
super().__init__(value, **kwargs)
|
|
259
|
+
if not is_valid_hostname(self.hostname):
|
|
260
|
+
raise ValueError(f"Invalid hostname format given.")
|
|
261
|
+
|
|
262
|
+
def __repr__(self) -> str:
|
|
263
|
+
dec_time = datetime.datetime.fromtimestamp(self.time / 1_000_000_000).strftime(
|
|
264
|
+
"%Y-%m-%d %H:%M:%S"
|
|
265
|
+
)
|
|
266
|
+
return (
|
|
267
|
+
f"<{type(self).__name__} time={dec_time}.{self.time%1_000_000_000} rand={self._encode(self.rand)} "
|
|
268
|
+
f"hostname={self.hostname}>"
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
@classmethod
|
|
272
|
+
def new(cls, timeout: float = 0.030) -> bytes:
|
|
273
|
+
"""Generate and return the bytes for a new id
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
timeout: Optional. Max number of seconds to take to generate
|
|
277
|
+
a new id. This is only required in edge cases where there
|
|
278
|
+
is clock drift and the clock went backwards from the last
|
|
279
|
+
id generated in which case this function will sleep for
|
|
280
|
+
a short duration and then try again.
|
|
281
|
+
"""
|
|
282
|
+
global _encoded_localhost
|
|
283
|
+
|
|
284
|
+
id_bytes = b"".join(
|
|
285
|
+
[
|
|
286
|
+
UOID.new(timeout=timeout),
|
|
287
|
+
_encoded_localhost,
|
|
288
|
+
]
|
|
289
|
+
)
|
|
290
|
+
return id_bytes
|
|
291
|
+
|
|
292
|
+
def __str__(self) -> str:
|
|
293
|
+
"""32 to 128 character b32hex encoded version of the id,
|
|
294
|
+
which maintains the same sort order as the bytes."""
|
|
295
|
+
return super().__str__()
|
|
296
|
+
|
|
297
|
+
@property
|
|
298
|
+
def hostname(self) -> str:
|
|
299
|
+
"""The hostname of a node, usually the computer where the id was generated."""
|
|
300
|
+
return self._bytes[self._time_byte_length + self._rand_byte_length :].decode(
|
|
301
|
+
"ascii"
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
def replace(
|
|
305
|
+
self,
|
|
306
|
+
time: Optional[int] = None,
|
|
307
|
+
rand: Optional[bytearray] = None,
|
|
308
|
+
hostname: Optional[str] = None,
|
|
309
|
+
) -> T_UOHID:
|
|
310
|
+
"""Replace parts of the UOHID and return a new instance with those
|
|
311
|
+
replaced parts.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
time: Optional. nanoseconds since epoch integer.
|
|
315
|
+
rand: Optional. random bytes.
|
|
316
|
+
hostname: Optional. Hostname of a computer.
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
UOHID
|
|
320
|
+
"""
|
|
321
|
+
parts = []
|
|
322
|
+
if time is None:
|
|
323
|
+
parts.append(self._bytes[: self._time_byte_length])
|
|
324
|
+
else:
|
|
325
|
+
parts.append(time.to_bytes(self._time_byte_length, "big"))
|
|
326
|
+
if rand is None:
|
|
327
|
+
parts.append(
|
|
328
|
+
self._bytes[
|
|
329
|
+
self._time_byte_length : self._time_byte_length
|
|
330
|
+
+ self._rand_byte_length
|
|
331
|
+
]
|
|
332
|
+
)
|
|
333
|
+
else:
|
|
334
|
+
parts.append(rand)
|
|
335
|
+
if hostname is None:
|
|
336
|
+
parts.append(self._bytes[self._time_byte_length + self._rand_byte_length :])
|
|
337
|
+
else:
|
|
338
|
+
parts.append(normalize(hostname).encode("ascii"))
|
|
339
|
+
return type(self)(b"".join(parts))
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
class UOPHID(UOHID):
|
|
343
|
+
"""Unique ordered port-host ID: time-random_bytes-port-hostname
|
|
344
|
+
|
|
345
|
+
Min Length (10 (time) + 6 (random) + 2 (port) + 1 (hostname)) = 19 bytes (32 chars)
|
|
346
|
+
Max Length (10 (time) + 6 (random) + 2 (port) + 64 (hostname)) = 82 bytes (136 chars)
|
|
347
|
+
|
|
348
|
+
Similar to UOHID, but it encodes the port number of the service the resource
|
|
349
|
+
is hosted on allowing for proxying services to work based on the info encoded in the
|
|
350
|
+
resource id.
|
|
351
|
+
"""
|
|
352
|
+
|
|
353
|
+
__slots__ = []
|
|
354
|
+
_port_byte_length = 2
|
|
355
|
+
_MIN_BYTE_LENGTH = (
|
|
356
|
+
UOID._time_byte_length
|
|
357
|
+
+ UOID._rand_byte_length
|
|
358
|
+
+ _port_byte_length
|
|
359
|
+
+ UOHID._min_hostname_byte_length
|
|
360
|
+
)
|
|
361
|
+
_MAX_BYTE_LENGTH = (
|
|
362
|
+
UOID._time_byte_length
|
|
363
|
+
+ UOID._rand_byte_length
|
|
364
|
+
+ _port_byte_length
|
|
365
|
+
+ UOHID._max_hostname_byte_length
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
def __init__(
|
|
369
|
+
self,
|
|
370
|
+
value: Union[None, bytes, bytearray, str, T_UOHID] = None,
|
|
371
|
+
*,
|
|
372
|
+
port: int | None = None,
|
|
373
|
+
**kwargs,
|
|
374
|
+
) -> None:
|
|
375
|
+
"""Init
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
value: id bytes or encoded string or instance of this object
|
|
379
|
+
timeout: Optional. Max number of seconds to spend trying go create a new
|
|
380
|
+
id before timing out. Defaults to 0.030 seconds.
|
|
381
|
+
port: Optional if value not given. Port number the service is hosted on
|
|
382
|
+
for where this resource exists.
|
|
383
|
+
"""
|
|
384
|
+
super().__init__(value, port=port, **kwargs)
|
|
385
|
+
if self.port < 0 or self.port > 65535:
|
|
386
|
+
raise ValueError(
|
|
387
|
+
"The port is outside the acceptable range between 0 and 65535."
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
def __repr__(self) -> str:
|
|
391
|
+
dec_time = datetime.datetime.fromtimestamp(self.time / 1_000_000_000).strftime(
|
|
392
|
+
"%Y-%m-%d %H:%M:%S"
|
|
393
|
+
)
|
|
394
|
+
return (
|
|
395
|
+
f"<{type(self).__name__} time={dec_time}.{self.time%1_000_000_000} rand={self._encode(self.rand)} "
|
|
396
|
+
f"port={self.port} hostname={self.hostname}>"
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
@classmethod
|
|
400
|
+
def new(cls, port: int, timeout: float = 0.030) -> bytes:
|
|
401
|
+
"""Generate a bytes for a UOHID object, filling in the port based on
|
|
402
|
+
current_service_info and the hostname from that of the local machine.
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
port: Port number of the service this resource exists on.
|
|
406
|
+
timeout: Optional. Max number of seconds to take to generate
|
|
407
|
+
a new id. This is only required in edge cases where there
|
|
408
|
+
is clock drift and the clock went backwards from the last
|
|
409
|
+
id generated in which case this function will sleep for
|
|
410
|
+
a short duration and then try again.
|
|
411
|
+
"""
|
|
412
|
+
global _encoded_localhost
|
|
413
|
+
|
|
414
|
+
id_bytes = b"".join(
|
|
415
|
+
[
|
|
416
|
+
UOID.new(timeout=timeout),
|
|
417
|
+
port.to_bytes(2, "big", signed=False),
|
|
418
|
+
_encoded_localhost,
|
|
419
|
+
]
|
|
420
|
+
)
|
|
421
|
+
return id_bytes
|
|
422
|
+
|
|
423
|
+
def __str__(self) -> str:
|
|
424
|
+
"""32 to 136 character b32hex encoded version of the id,
|
|
425
|
+
which maintains the same sort order as the bytes."""
|
|
426
|
+
return super().__str__()
|
|
427
|
+
|
|
428
|
+
@property
|
|
429
|
+
def port(self) -> int:
|
|
430
|
+
"""The port a service is running on for a node."""
|
|
431
|
+
i = self._time_byte_length + self._rand_byte_length
|
|
432
|
+
return int.from_bytes(
|
|
433
|
+
self._bytes[i : i + self._port_byte_length], "big", signed=False
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
@property
|
|
437
|
+
def hostname(self) -> str:
|
|
438
|
+
"""The hostname of a node, usually the computer where the id was generated."""
|
|
439
|
+
return self._bytes[
|
|
440
|
+
self._time_byte_length + self._rand_byte_length + self._port_byte_length :
|
|
441
|
+
].decode("ascii")
|
|
442
|
+
|
|
443
|
+
def replace(
|
|
444
|
+
self,
|
|
445
|
+
time: Optional[int] = None,
|
|
446
|
+
rand: Optional[bytearray] = None,
|
|
447
|
+
hostname: Optional[str] = None,
|
|
448
|
+
port: Optional[int] = None,
|
|
449
|
+
) -> T_UOHID:
|
|
450
|
+
"""Replace parts of the UOHID and return a new instance with those
|
|
451
|
+
replaced parts.
|
|
452
|
+
|
|
453
|
+
Args:
|
|
454
|
+
time: Optional. nanoseconds since epoch integer.
|
|
455
|
+
rand: Optional. random bytes.
|
|
456
|
+
hostname: Optional. Hostname of a computer.
|
|
457
|
+
port: Optional. Port a service is running on.
|
|
458
|
+
|
|
459
|
+
Returns:
|
|
460
|
+
UOHID
|
|
461
|
+
"""
|
|
462
|
+
parts = []
|
|
463
|
+
if time is None:
|
|
464
|
+
parts.append(self._bytes[: self._time_byte_length])
|
|
465
|
+
else:
|
|
466
|
+
parts.append(time.to_bytes(self._time_byte_length, "big"))
|
|
467
|
+
if rand is None:
|
|
468
|
+
parts.append(
|
|
469
|
+
self._bytes[
|
|
470
|
+
self._time_byte_length : self._time_byte_length
|
|
471
|
+
+ self._rand_byte_length
|
|
472
|
+
]
|
|
473
|
+
)
|
|
474
|
+
else:
|
|
475
|
+
parts.append(rand)
|
|
476
|
+
if port is None:
|
|
477
|
+
i = self._time_byte_length + self._rand_byte_length
|
|
478
|
+
parts.append(self._bytes[i : i + self._port_byte_length])
|
|
479
|
+
else:
|
|
480
|
+
parts.append(port.to_bytes(2, "big", signed=False))
|
|
481
|
+
if hostname is None:
|
|
482
|
+
parts.append(
|
|
483
|
+
self._bytes[
|
|
484
|
+
self._time_byte_length
|
|
485
|
+
+ self._rand_byte_length
|
|
486
|
+
+ self._port_byte_length :
|
|
487
|
+
]
|
|
488
|
+
)
|
|
489
|
+
else:
|
|
490
|
+
parts.append(normalize(hostname).encode("ascii"))
|
|
491
|
+
return type(self)(b"".join(parts))
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: whatamithinking-idlib
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Generate unique IDs to identify resources in apps
|
|
5
|
+
Author-email: Connor Sherwood Maynes <connormaynes@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 whatamithinking
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/whatamithinking/idlib
|
|
29
|
+
Project-URL: Source, https://github.com/whatamithinking/idlib
|
|
30
|
+
Keywords: id,unique-id,uuid,resource-id
|
|
31
|
+
Classifier: Programming Language :: Python :: 3
|
|
32
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
33
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
34
|
+
Classifier: Operating System :: OS Independent
|
|
35
|
+
Requires-Python: >=3.12
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
License-File: LICENSE
|
|
38
|
+
Requires-Dist: whatamithinking-hostutil
|
|
39
|
+
Provides-Extra: dev
|
|
40
|
+
Requires-Dist: black; extra == "dev"
|
|
41
|
+
Dynamic: license-file
|
|
42
|
+
|
|
43
|
+
# idlib
|
|
44
|
+
|
|
45
|
+
Time-ordered unique IDs for identifying resources in applications.
|
|
46
|
+
|
|
47
|
+
`idlib` generates unique IDs that are **sequential**: every ID generated will be lexicographically greater than all previously generated IDs. This makes them ideal as primary keys in databases, since they naturally sort in insertion order without a secondary timestamp column.
|
|
48
|
+
|
|
49
|
+
IDs are encoded with [base32hex](https://www.rfc-editor.org/rfc/rfc4648#section-7), which preserves the sort order of the underlying bytes while remaining safe to use in URLs and filenames.
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
pip install whatamithinking-idlib
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## ID Types
|
|
58
|
+
|
|
59
|
+
### `UOID` — Unique Ordered ID
|
|
60
|
+
|
|
61
|
+
**Structure:** `time (10 bytes) + random (6 bytes)` → 16 bytes → **32-character string**
|
|
62
|
+
|
|
63
|
+
The base type. Encodes a nanosecond-precision timestamp and random bytes.
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from whatamithinking.idlib import UOID
|
|
67
|
+
|
|
68
|
+
id1 = UOID()
|
|
69
|
+
id2 = UOID()
|
|
70
|
+
|
|
71
|
+
print(str(id1)) # e.g. "0O0000CGFS0000ABCDEFGH123456"
|
|
72
|
+
print(str(id2)) # always lexicographically greater than id1
|
|
73
|
+
|
|
74
|
+
assert id1 < id2 # guaranteed ordering
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Properties:**
|
|
78
|
+
|
|
79
|
+
| Property | Description |
|
|
80
|
+
|----------|-------------|
|
|
81
|
+
| `.time` | Nanosecond epoch timestamp encoded in the ID (`int`) |
|
|
82
|
+
| `.rand` | Random bytes component (`bytes`) |
|
|
83
|
+
|
|
84
|
+
**Reconstruction from string or bytes:**
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
id_str = str(id1)
|
|
88
|
+
restored = UOID(id_str) # from string
|
|
89
|
+
restored = UOID(bytes(id1)) # from bytes
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
### `UOHID` — Unique Ordered Host ID
|
|
95
|
+
|
|
96
|
+
**Structure:** `time (10 bytes) + random (6 bytes) + hostname (1–64 bytes)` → 17–80 bytes → **32–128-character string**
|
|
97
|
+
|
|
98
|
+
Extends `UOID` by encoding the hostname of the machine that generated the ID. Useful for systems where resources need to be routed back to the originating service by hostname.
|
|
99
|
+
|
|
100
|
+
> **Warning:** This ID leaks the hostname of the generating machine. Do not use it when that information is sensitive.
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from whatamithinking.idlib import UOHID
|
|
104
|
+
|
|
105
|
+
id1 = UOHID()
|
|
106
|
+
|
|
107
|
+
print(str(id1)) # base32hex-encoded string
|
|
108
|
+
print(id1.hostname) # e.g. "my-server"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Properties:**
|
|
112
|
+
|
|
113
|
+
| Property | Description |
|
|
114
|
+
|-------------|-------------|
|
|
115
|
+
| `.time` | Nanosecond epoch timestamp (`int`) |
|
|
116
|
+
| `.rand` | Random bytes component (`bytes`) |
|
|
117
|
+
| `.hostname` | Hostname of the generating machine (`str`) |
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
### `UOPHID` — Unique Ordered Port-Host ID
|
|
122
|
+
|
|
123
|
+
**Structure:** `time (10 bytes) + random (6 bytes) + port (2 bytes) + hostname (1–64 bytes)` → 19–82 bytes → **32–136-character string**
|
|
124
|
+
|
|
125
|
+
Extends `UOHID` by also encoding the port number of the service that created the ID. Useful for systems where resources need to be routed to a specific port on the originating host.
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from whatamithinking.idlib import UOPHID
|
|
129
|
+
|
|
130
|
+
id1 = UOPHID(port=9100)
|
|
131
|
+
|
|
132
|
+
print(str(id1)) # base32hex-encoded string
|
|
133
|
+
print(id1.port) # 9100
|
|
134
|
+
print(id1.hostname) # e.g. "my-server"
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**Properties:**
|
|
138
|
+
|
|
139
|
+
| Property | Description |
|
|
140
|
+
|-------------|-------------|
|
|
141
|
+
| `.time` | Nanosecond epoch timestamp (`int`) |
|
|
142
|
+
| `.rand` | Random bytes component (`bytes`) |
|
|
143
|
+
| `.port` | Service port number (`int`, 0–65535) |
|
|
144
|
+
| `.hostname` | Hostname of the generating machine (`str`) |
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Common API
|
|
149
|
+
|
|
150
|
+
All three types share this interface:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
# Create new
|
|
154
|
+
id1 = UOID()
|
|
155
|
+
|
|
156
|
+
# Restore from string
|
|
157
|
+
id2 = UOID("0O0000CGFS0000ABCDEFGH123456")
|
|
158
|
+
|
|
159
|
+
# Restore from bytes
|
|
160
|
+
id3 = UOID(bytes(id1))
|
|
161
|
+
|
|
162
|
+
# String representation (base32hex, sortable)
|
|
163
|
+
s = str(id1)
|
|
164
|
+
|
|
165
|
+
# Raw bytes
|
|
166
|
+
b = bytes(id1)
|
|
167
|
+
|
|
168
|
+
# Comparison (all ordering operators supported)
|
|
169
|
+
assert id1 < id2
|
|
170
|
+
assert id1 == id1
|
|
171
|
+
|
|
172
|
+
# Replace individual fields and get a new instance
|
|
173
|
+
new_id = id1.replace(rand=b"\x00" * 6)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Ordering Guarantees
|
|
177
|
+
|
|
178
|
+
IDs are guaranteed to be strictly increasing as long as they are generated on the same thread. If multiple IDs are generated within the same nanosecond, the random component is auto-incremented to maintain order. If clock drift is detected (clock moved backwards), generation will retry for up to `timeout` seconds (default: 30 ms) before raising a `RuntimeError`.
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
MIT
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
whatamithinking/idlib/__init__.py,sha256=ZvPJhtly3TNWGQPwxBnOfj8eUfGcYM_p51LGfSeDyuM,17880
|
|
2
|
+
whatamithinking_idlib-1.0.0.dist-info/licenses/LICENSE,sha256=s5f4BFsESm1KgGwAQEsNTR4lLEH14kj7-j5SnAkJzn4,1093
|
|
3
|
+
whatamithinking_idlib-1.0.0.dist-info/METADATA,sha256=eG2pmAFjnUsZcS0xnSJi0uwXkbyMFx1SNjaGiVwtL-8,6235
|
|
4
|
+
whatamithinking_idlib-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
5
|
+
whatamithinking_idlib-1.0.0.dist-info/top_level.txt,sha256=ziC0QeEOyPmt6QrFH6vunXPacrmYzVVGH09o9lxRNbI,21
|
|
6
|
+
whatamithinking_idlib-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 whatamithinking
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|