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.
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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.
@@ -0,0 +1,2 @@
1
+ dist
2
+ whatamithinking