permid64 0.1.0__tar.gz

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,267 @@
1
+ Metadata-Version: 2.4
2
+ Name: permid64
3
+ Version: 0.1.0
4
+ Summary: Clock-free, persistent, reversible-permutation 64-bit ID generation
5
+ License: MIT
6
+ Project-URL: Repository, https://github.com/erickh826/permid64
7
+ Project-URL: Issues, https://github.com/erickh826/permid64/issues
8
+ Keywords: id,uuid,unique-id,feistel,permutation,counter,clock-free
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development :: Libraries
19
+ Requires-Python: >=3.8
20
+ Description-Content-Type: text/markdown
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest>=8.0; extra == "dev"
23
+ Requires-Dist: pytest-xdist; extra == "dev"
24
+ Requires-Dist: hypothesis; extra == "dev"
25
+ Requires-Dist: ruff; extra == "dev"
26
+ Requires-Dist: mypy; extra == "dev"
27
+
28
+ # permid64
29
+
30
+ **Clock-free, persistent, reversible-permutation 64-bit ID generation.**
31
+
32
+ > *Counter in, permutation out.*
33
+
34
+ permid64 generates unique 64-bit integer IDs without relying on wall-clock time. It combines a crash-safe persistent counter with an invertible permutation to produce IDs that look random but carry recoverable metadata.
35
+
36
+ ```
37
+ # Raw counter (leaks business volume at a glance)
38
+ 1001, 1002, 1003 ...
39
+
40
+ # permid64 (shuffled surface, recoverable structure)
41
+ 12609531668580943872, 7349201938475629, 3847291038012847 ...
42
+ # decode(12609531668580943872) → instance_id=42, sequence=0
43
+ ```
44
+
45
+ ---
46
+
47
+ ## What it is
48
+
49
+ - A **clock-free** 64-bit ID generator — no timestamp, no NTP dependency
50
+ - IDs are **unique** because the source is a monotonically increasing counter
51
+ - IDs **look shuffled** because they pass through a reversible permutation
52
+ - The permutation is **invertible** — `decode()` recovers the original metadata
53
+
54
+ ## What it is not
55
+
56
+ - **Not a timestamp-based scheme** — there is no time component in the ID
57
+ - **Not a UUID replacement for every scenario** — if you need a globally unique random token with no infrastructure at all, UUID v4 is simpler
58
+ - **Not cryptographic encryption** — the permutation is an obfuscation layer, not authenticated encryption; do not use IDs as secrets or security tokens
59
+ - **Not safe for multiple processes sharing one state file** — `PersistentCounterSource` is single-process only; concurrent writes from multiple processes to the same state file will cause duplicates (see [Limitations](#limitations))
60
+
61
+ ---
62
+
63
+ ## Design
64
+
65
+ ```
66
+ seq = source.next() # monotonic counter (persistent)
67
+ raw = layout.compose(instance_id, seq) # pack 16-bit shard + 48-bit seq
68
+ id64 = permutation.forward(raw) # obfuscate with invertible bijection
69
+ ```
70
+
71
+ **Layout** — default 64-bit split:
72
+
73
+ ```
74
+ [ instance_id : 16 bits ][ sequence : 48 bits ]
75
+ ```
76
+
77
+ - Up to **65 535** independent shards
78
+ - Up to **281 trillion** IDs per shard
79
+
80
+ **Permutations** — both are bijections over `[0, 2^64)`:
81
+
82
+ | Mode | Formula | Speed | Mixing |
83
+ |---|---|---|---|
84
+ | `multiplicative` | `f(x) = (a·x + b) mod 2^64` | ~500 M/s | Good |
85
+ | `feistel` | 64-bit Feistel network | ~150 M/s | Excellent |
86
+
87
+ **Persistence** — block reservation strategy:
88
+ 1. On startup, read high-water mark from state file.
89
+ 2. Reserve a block of N sequence numbers, write new high-water mark.
90
+ 3. Serve IDs from memory until block exhausted.
91
+ 4. If the process crashes, the unused block is lost (gap), but **no duplicate is ever issued**.
92
+
93
+ ---
94
+
95
+ ## Quick start
96
+
97
+ ```python
98
+ from permid64 import Id64
99
+
100
+ # Multiplicative (fastest)
101
+ gen = Id64.multiplicative(
102
+ instance_id=42,
103
+ state_file="permid64.state",
104
+ block_size=4096,
105
+ )
106
+
107
+ uid = gen.next_u64() # e.g. 12609531668580943872
108
+ meta = gen.decode(uid)
109
+ # DecodedId(raw=2748779069440, instance_id=42, sequence=0)
110
+ print(meta.instance_id, meta.sequence)
111
+
112
+ # Feistel (better statistical mixing)
113
+ gen2 = Id64.feistel(
114
+ instance_id=42,
115
+ state_file="permid64.state",
116
+ block_size=4096,
117
+ key=0xDEADBEEFCAFEBABE,
118
+ rounds=6,
119
+ )
120
+ ```
121
+
122
+ ### Why decode() matters
123
+
124
+ In production, when an anomalous ID appears in a log or alert, you can decode it instantly — no DB lookup needed:
125
+
126
+ ```python
127
+ meta = gen.decode(12609531668580943872)
128
+ print(f"Issued by instance {meta.instance_id}, sequence #{meta.sequence}")
129
+ # Issued by instance 42, sequence #0
130
+ ```
131
+
132
+ This makes incident tracing dramatically faster: you immediately know which shard issued the ID and its approximate position in the issuance history.
133
+
134
+ ### Assigning instance_id
135
+
136
+ Assign each process or deployment unit a distinct `instance_id`. Common patterns:
137
+
138
+ ```python
139
+ import os
140
+
141
+ # From environment variable (works in Docker / K8s)
142
+ instance_id = int(os.environ.get("INSTANCE_ID", "0"))
143
+
144
+ # From K8s StatefulSet pod name (e.g. "worker-3" -> 3)
145
+ import re
146
+ pod_name = os.environ.get("POD_NAME", "worker-0")
147
+ instance_id = int(re.search(r"(\d+)$", pod_name).group(1))
148
+ ```
149
+
150
+ Each `instance_id` gets its own independent sequence space — no coordination needed between shards.
151
+
152
+ ---
153
+
154
+ ## Installation
155
+
156
+ ```bash
157
+ pip install permid64 # once published to PyPI
158
+ # or from source:
159
+ pip install -e ".[dev]"
160
+ ```
161
+
162
+ ---
163
+
164
+ ## Running tests
165
+
166
+ ```bash
167
+ pytest
168
+ ```
169
+
170
+ Five acceptance criteria are checked:
171
+
172
+ 1. **Uniqueness** — 1 million IDs, zero duplicates
173
+ 2. **Invertibility** — `decode(next_u64())` recovers `instance_id` and `sequence`
174
+ 3. **Restart safety** — sequence never resets across process restarts
175
+ 4. **Gap tolerance** — crash causes a gap, never a duplicate
176
+ 5. **Thread safety** — concurrent generation remains unique
177
+
178
+ ---
179
+
180
+ ## Benchmark
181
+
182
+ ```bash
183
+ python benchmarks/bench_id64.py
184
+ ```
185
+
186
+ Sample output (Apple M2):
187
+
188
+ ```
189
+ [Permutation comparison — block_size=4096]
190
+ multiplicative (default keys) ~480,000,000 IDs/sec
191
+ feistel (6 rounds) ~140,000,000 IDs/sec
192
+ feistel (12 rounds) ~80,000,000 IDs/sec
193
+ ```
194
+
195
+ ---
196
+
197
+ ## Guarantees
198
+
199
+ | Guarantee | Notes |
200
+ |---|---|
201
+ | No duplicate IDs within a shard | Strict |
202
+ | No duplicates across restarts | Strict — state file must be on durable storage |
203
+ | Decodable | Only with the same permutation key / params |
204
+ | Gaps allowed | After a crash, some sequence numbers are skipped |
205
+ | No global coordination | Each `instance_id` is fully independent |
206
+
207
+ ---
208
+
209
+ ## Limitations
210
+
211
+ ### Single-process only
212
+
213
+ `PersistentCounterSource` is **not safe for concurrent use across multiple processes** sharing the same state file. A best-effort `fcntl.flock` advisory lock is applied during block reservation on POSIX systems, but this is not a hard guarantee — do not rely on it as a substitute for proper shard isolation.
214
+
215
+ The correct pattern for multiple processes is to assign each a **distinct `instance_id`** and a **distinct state file**. Multi-process coordination via a central allocator is planned for v0.3.
216
+
217
+ ### Feistel is obfuscation, not encryption
218
+
219
+ The Feistel permutation provides strong mixing and is reversible, but it is not a formally audited cryptographic primitive. Do not rely on it for access control, token authentication, or any security-sensitive use case.
220
+
221
+ ### instance_id must be assigned manually
222
+
223
+ There is no automatic shard coordination. Assign `instance_id` values via config or environment variables and ensure they are unique across your deployment.
224
+
225
+ ### Sequence space is large but finite
226
+
227
+ The default 48-bit sequence space supports ~281 trillion IDs per shard. This is enough for virtually all workloads, but it is not infinite.
228
+
229
+ ---
230
+
231
+ ## Architecture
232
+
233
+ ```
234
+ permid64/
235
+ __init__.py # public exports: Id64, DecodedId
236
+ generator.py # Id64 façade
237
+ source.py # PersistentCounterSource
238
+ layout.py # Layout64 — pack/unpack 64-bit raw value
239
+ permutation.py # MultiplyOddPermutation, Feistel64Permutation
240
+ types.py # DecodedId dataclass
241
+
242
+ tests/
243
+ test_counter.py
244
+ test_layout.py
245
+ test_permutation.py
246
+ test_id64_e2e.py # the 5 MVP acceptance tests
247
+
248
+ benchmarks/
249
+ bench_id64.py
250
+ ```
251
+
252
+ ---
253
+
254
+ ## Roadmap
255
+
256
+ | Version | Focus |
257
+ |---|---|
258
+ | v0.1 (current) | Core: counter + permutation + decode |
259
+ | v0.2 | `IdentityPermutation`, Base32/Base62 encoding, `Id64Config` |
260
+ | v0.3 | Multi-process file locking, `ReservedBlockSource` (central allocator) |
261
+ | v0.4+ | Rust/Go reference implementations, formal cross-language spec |
262
+
263
+ ---
264
+
265
+ ## License
266
+
267
+ MIT
@@ -0,0 +1,240 @@
1
+ # permid64
2
+
3
+ **Clock-free, persistent, reversible-permutation 64-bit ID generation.**
4
+
5
+ > *Counter in, permutation out.*
6
+
7
+ permid64 generates unique 64-bit integer IDs without relying on wall-clock time. It combines a crash-safe persistent counter with an invertible permutation to produce IDs that look random but carry recoverable metadata.
8
+
9
+ ```
10
+ # Raw counter (leaks business volume at a glance)
11
+ 1001, 1002, 1003 ...
12
+
13
+ # permid64 (shuffled surface, recoverable structure)
14
+ 12609531668580943872, 7349201938475629, 3847291038012847 ...
15
+ # decode(12609531668580943872) → instance_id=42, sequence=0
16
+ ```
17
+
18
+ ---
19
+
20
+ ## What it is
21
+
22
+ - A **clock-free** 64-bit ID generator — no timestamp, no NTP dependency
23
+ - IDs are **unique** because the source is a monotonically increasing counter
24
+ - IDs **look shuffled** because they pass through a reversible permutation
25
+ - The permutation is **invertible** — `decode()` recovers the original metadata
26
+
27
+ ## What it is not
28
+
29
+ - **Not a timestamp-based scheme** — there is no time component in the ID
30
+ - **Not a UUID replacement for every scenario** — if you need a globally unique random token with no infrastructure at all, UUID v4 is simpler
31
+ - **Not cryptographic encryption** — the permutation is an obfuscation layer, not authenticated encryption; do not use IDs as secrets or security tokens
32
+ - **Not safe for multiple processes sharing one state file** — `PersistentCounterSource` is single-process only; concurrent writes from multiple processes to the same state file will cause duplicates (see [Limitations](#limitations))
33
+
34
+ ---
35
+
36
+ ## Design
37
+
38
+ ```
39
+ seq = source.next() # monotonic counter (persistent)
40
+ raw = layout.compose(instance_id, seq) # pack 16-bit shard + 48-bit seq
41
+ id64 = permutation.forward(raw) # obfuscate with invertible bijection
42
+ ```
43
+
44
+ **Layout** — default 64-bit split:
45
+
46
+ ```
47
+ [ instance_id : 16 bits ][ sequence : 48 bits ]
48
+ ```
49
+
50
+ - Up to **65 535** independent shards
51
+ - Up to **281 trillion** IDs per shard
52
+
53
+ **Permutations** — both are bijections over `[0, 2^64)`:
54
+
55
+ | Mode | Formula | Speed | Mixing |
56
+ |---|---|---|---|
57
+ | `multiplicative` | `f(x) = (a·x + b) mod 2^64` | ~500 M/s | Good |
58
+ | `feistel` | 64-bit Feistel network | ~150 M/s | Excellent |
59
+
60
+ **Persistence** — block reservation strategy:
61
+ 1. On startup, read high-water mark from state file.
62
+ 2. Reserve a block of N sequence numbers, write new high-water mark.
63
+ 3. Serve IDs from memory until block exhausted.
64
+ 4. If the process crashes, the unused block is lost (gap), but **no duplicate is ever issued**.
65
+
66
+ ---
67
+
68
+ ## Quick start
69
+
70
+ ```python
71
+ from permid64 import Id64
72
+
73
+ # Multiplicative (fastest)
74
+ gen = Id64.multiplicative(
75
+ instance_id=42,
76
+ state_file="permid64.state",
77
+ block_size=4096,
78
+ )
79
+
80
+ uid = gen.next_u64() # e.g. 12609531668580943872
81
+ meta = gen.decode(uid)
82
+ # DecodedId(raw=2748779069440, instance_id=42, sequence=0)
83
+ print(meta.instance_id, meta.sequence)
84
+
85
+ # Feistel (better statistical mixing)
86
+ gen2 = Id64.feistel(
87
+ instance_id=42,
88
+ state_file="permid64.state",
89
+ block_size=4096,
90
+ key=0xDEADBEEFCAFEBABE,
91
+ rounds=6,
92
+ )
93
+ ```
94
+
95
+ ### Why decode() matters
96
+
97
+ In production, when an anomalous ID appears in a log or alert, you can decode it instantly — no DB lookup needed:
98
+
99
+ ```python
100
+ meta = gen.decode(12609531668580943872)
101
+ print(f"Issued by instance {meta.instance_id}, sequence #{meta.sequence}")
102
+ # Issued by instance 42, sequence #0
103
+ ```
104
+
105
+ This makes incident tracing dramatically faster: you immediately know which shard issued the ID and its approximate position in the issuance history.
106
+
107
+ ### Assigning instance_id
108
+
109
+ Assign each process or deployment unit a distinct `instance_id`. Common patterns:
110
+
111
+ ```python
112
+ import os
113
+
114
+ # From environment variable (works in Docker / K8s)
115
+ instance_id = int(os.environ.get("INSTANCE_ID", "0"))
116
+
117
+ # From K8s StatefulSet pod name (e.g. "worker-3" -> 3)
118
+ import re
119
+ pod_name = os.environ.get("POD_NAME", "worker-0")
120
+ instance_id = int(re.search(r"(\d+)$", pod_name).group(1))
121
+ ```
122
+
123
+ Each `instance_id` gets its own independent sequence space — no coordination needed between shards.
124
+
125
+ ---
126
+
127
+ ## Installation
128
+
129
+ ```bash
130
+ pip install permid64 # once published to PyPI
131
+ # or from source:
132
+ pip install -e ".[dev]"
133
+ ```
134
+
135
+ ---
136
+
137
+ ## Running tests
138
+
139
+ ```bash
140
+ pytest
141
+ ```
142
+
143
+ Five acceptance criteria are checked:
144
+
145
+ 1. **Uniqueness** — 1 million IDs, zero duplicates
146
+ 2. **Invertibility** — `decode(next_u64())` recovers `instance_id` and `sequence`
147
+ 3. **Restart safety** — sequence never resets across process restarts
148
+ 4. **Gap tolerance** — crash causes a gap, never a duplicate
149
+ 5. **Thread safety** — concurrent generation remains unique
150
+
151
+ ---
152
+
153
+ ## Benchmark
154
+
155
+ ```bash
156
+ python benchmarks/bench_id64.py
157
+ ```
158
+
159
+ Sample output (Apple M2):
160
+
161
+ ```
162
+ [Permutation comparison — block_size=4096]
163
+ multiplicative (default keys) ~480,000,000 IDs/sec
164
+ feistel (6 rounds) ~140,000,000 IDs/sec
165
+ feistel (12 rounds) ~80,000,000 IDs/sec
166
+ ```
167
+
168
+ ---
169
+
170
+ ## Guarantees
171
+
172
+ | Guarantee | Notes |
173
+ |---|---|
174
+ | No duplicate IDs within a shard | Strict |
175
+ | No duplicates across restarts | Strict — state file must be on durable storage |
176
+ | Decodable | Only with the same permutation key / params |
177
+ | Gaps allowed | After a crash, some sequence numbers are skipped |
178
+ | No global coordination | Each `instance_id` is fully independent |
179
+
180
+ ---
181
+
182
+ ## Limitations
183
+
184
+ ### Single-process only
185
+
186
+ `PersistentCounterSource` is **not safe for concurrent use across multiple processes** sharing the same state file. A best-effort `fcntl.flock` advisory lock is applied during block reservation on POSIX systems, but this is not a hard guarantee — do not rely on it as a substitute for proper shard isolation.
187
+
188
+ The correct pattern for multiple processes is to assign each a **distinct `instance_id`** and a **distinct state file**. Multi-process coordination via a central allocator is planned for v0.3.
189
+
190
+ ### Feistel is obfuscation, not encryption
191
+
192
+ The Feistel permutation provides strong mixing and is reversible, but it is not a formally audited cryptographic primitive. Do not rely on it for access control, token authentication, or any security-sensitive use case.
193
+
194
+ ### instance_id must be assigned manually
195
+
196
+ There is no automatic shard coordination. Assign `instance_id` values via config or environment variables and ensure they are unique across your deployment.
197
+
198
+ ### Sequence space is large but finite
199
+
200
+ The default 48-bit sequence space supports ~281 trillion IDs per shard. This is enough for virtually all workloads, but it is not infinite.
201
+
202
+ ---
203
+
204
+ ## Architecture
205
+
206
+ ```
207
+ permid64/
208
+ __init__.py # public exports: Id64, DecodedId
209
+ generator.py # Id64 façade
210
+ source.py # PersistentCounterSource
211
+ layout.py # Layout64 — pack/unpack 64-bit raw value
212
+ permutation.py # MultiplyOddPermutation, Feistel64Permutation
213
+ types.py # DecodedId dataclass
214
+
215
+ tests/
216
+ test_counter.py
217
+ test_layout.py
218
+ test_permutation.py
219
+ test_id64_e2e.py # the 5 MVP acceptance tests
220
+
221
+ benchmarks/
222
+ bench_id64.py
223
+ ```
224
+
225
+ ---
226
+
227
+ ## Roadmap
228
+
229
+ | Version | Focus |
230
+ |---|---|
231
+ | v0.1 (current) | Core: counter + permutation + decode |
232
+ | v0.2 | `IdentityPermutation`, Base32/Base62 encoding, `Id64Config` |
233
+ | v0.3 | Multi-process file locking, `ReservedBlockSource` (central allocator) |
234
+ | v0.4+ | Rust/Go reference implementations, formal cross-language spec |
235
+
236
+ ---
237
+
238
+ ## License
239
+
240
+ MIT
@@ -0,0 +1,17 @@
1
+ """
2
+ permid64 — Clock-free, persistent, obfuscated 64-bit ID generation.
3
+
4
+ Public API
5
+ ----------
6
+ from permid64 import Id64, DecodedId
7
+
8
+ gen = Id64.multiplicative(instance_id=1, state_file="id64.state")
9
+ uid = gen.next_u64()
10
+ meta = gen.decode(uid) # DecodedId(raw=..., instance_id=1, sequence=0)
11
+ """
12
+ from .generator import Id64
13
+ from .permutation import Permutation64Protocol
14
+ from .types import DecodedId
15
+
16
+ __all__ = ["Id64", "DecodedId", "Permutation64Protocol"]
17
+ __version__ = "0.1.0"
@@ -0,0 +1,122 @@
1
+ """
2
+ generator.py — Id64: the main public façade.
3
+
4
+ Usage
5
+ -----
6
+ # Multiplicative (fast, simpler)
7
+ gen = Id64.multiplicative(
8
+ instance_id=42,
9
+ state_file="id64.state",
10
+ block_size=4096,
11
+ a=0x9E3779B185EBCA87,
12
+ b=0x6A09E667F3BCC909,
13
+ )
14
+
15
+ # Feistel (better statistical mixing)
16
+ gen = Id64.feistel(
17
+ instance_id=42,
18
+ state_file="id64.state",
19
+ block_size=4096,
20
+ key=0xDEADBEEFCAFEBABE,
21
+ rounds=6,
22
+ )
23
+
24
+ id_val = gen.next_u64() # -> int (unsigned 64-bit)
25
+ meta = gen.decode(id_val) # -> DecodedId(raw, instance_id, sequence)
26
+ """
27
+ from __future__ import annotations
28
+
29
+ from .layout import Layout64
30
+ from .permutation import Feistel64Permutation, MultiplyOddPermutation, Permutation64Protocol
31
+ from .source import PersistentCounterSource
32
+ from .types import DecodedId
33
+
34
+
35
+ class Id64:
36
+ """
37
+ Clock-free 64-bit ID generator.
38
+
39
+ Architecture
40
+ ------------
41
+ seq = source.next() # monotonic counter (persistent)
42
+ raw = layout.compose(instance_id, seq) # pack bits
43
+ id64 = permutation.forward(raw) # obfuscate
44
+
45
+ Decode
46
+ ------
47
+ raw = permutation.inverse(id64)
48
+ meta = layout.decompose(raw)
49
+ """
50
+
51
+ def __init__(
52
+ self,
53
+ instance_id: int,
54
+ source: PersistentCounterSource,
55
+ permutation: Permutation64Protocol,
56
+ layout: Layout64 | None = None,
57
+ ) -> None:
58
+ self.instance_id = instance_id
59
+ self.source = source
60
+ self.permutation = permutation
61
+ self.layout = layout or Layout64()
62
+
63
+ # ------------------------------------------------------------------
64
+ # Factory constructors
65
+ # ------------------------------------------------------------------
66
+
67
+ @classmethod
68
+ def multiplicative(
69
+ cls,
70
+ instance_id: int,
71
+ state_file: str,
72
+ block_size: int = 4096,
73
+ a: int = 0x9E3779B185EBCA87,
74
+ b: int = 0x6A09E667F3BCC909,
75
+ ) -> "Id64":
76
+ """
77
+ Create a generator backed by a multiply-odd (affine) permutation.
78
+
79
+ ``a`` defaults to the 64-bit golden-ratio constant; ``b`` adds a
80
+ second independent mixing constant. Both can be overridden.
81
+ """
82
+ return cls(
83
+ instance_id=instance_id,
84
+ source=PersistentCounterSource(state_file, block_size),
85
+ permutation=MultiplyOddPermutation(a=a, b=b),
86
+ )
87
+
88
+ @classmethod
89
+ def feistel(
90
+ cls,
91
+ instance_id: int,
92
+ state_file: str,
93
+ block_size: int = 4096,
94
+ key: int = 0xDEADBEEFCAFEBABE,
95
+ rounds: int = 6,
96
+ ) -> "Id64":
97
+ """
98
+ Create a generator backed by a 64-bit Feistel-network permutation.
99
+
100
+ ``key`` is a 64-bit seed from which round keys are derived.
101
+ ``rounds`` defaults to 6 (good mixing / speed balance).
102
+ """
103
+ return cls(
104
+ instance_id=instance_id,
105
+ source=PersistentCounterSource(state_file, block_size),
106
+ permutation=Feistel64Permutation(key=key, rounds=rounds),
107
+ )
108
+
109
+ # ------------------------------------------------------------------
110
+ # Core API
111
+ # ------------------------------------------------------------------
112
+
113
+ def next_u64(self) -> int:
114
+ """Return the next unique, obfuscated 64-bit ID."""
115
+ seq = self.source.next()
116
+ raw = self.layout.compose(self.instance_id, seq)
117
+ return self.permutation.forward(raw)
118
+
119
+ def decode(self, id64: int) -> DecodedId:
120
+ """Reverse a previously generated ID back to its metadata."""
121
+ raw = self.permutation.inverse(id64)
122
+ return self.layout.decompose(raw)
@@ -0,0 +1,54 @@
1
+ """
2
+ layout.py — Pack / unpack instance_id + sequence into a single 64-bit integer.
3
+
4
+ Default split: 16 bits for instance_id (up to 65 535 shards)
5
+ 48 bits for sequence (up to 281 trillion IDs per shard)
6
+ """
7
+ from .types import DecodedId
8
+
9
+ MASK64 = 0xFFFFFFFFFFFFFFFF
10
+
11
+
12
+ class Layout64:
13
+ """
14
+ Bit-layout for the 64-bit raw value.
15
+
16
+ instance_id occupies the top `instance_bits` bits.
17
+ sequence occupies the bottom `sequence_bits` bits.
18
+ """
19
+
20
+ def __init__(self, instance_bits: int = 16, sequence_bits: int = 48) -> None:
21
+ if instance_bits + sequence_bits != 64:
22
+ raise ValueError(
23
+ f"instance_bits ({instance_bits}) + sequence_bits ({sequence_bits}) must equal 64"
24
+ )
25
+ self.instance_bits = instance_bits
26
+ self.sequence_bits = sequence_bits
27
+ self.sequence_mask = (1 << sequence_bits) - 1
28
+ self.instance_mask = (1 << instance_bits) - 1
29
+
30
+ def compose(self, instance_id: int, sequence: int) -> int:
31
+ """
32
+ Pack instance_id and sequence into a single 64-bit integer.
33
+
34
+ Both values are silently masked to their configured bit widths.
35
+ Values exceeding the field width (e.g. instance_id >= 2^instance_bits)
36
+ will have their high bits truncated without raising an error.
37
+ Use ``instance_mask`` / ``sequence_mask`` to validate inputs if
38
+ strict overflow detection is required.
39
+ """
40
+ if sequence > self.sequence_mask:
41
+ raise OverflowError(
42
+ f"sequence {sequence} exceeds {self.sequence_bits}-bit maximum "
43
+ f"({self.sequence_mask}). The ID space for this shard is exhausted."
44
+ )
45
+ return (
46
+ ((instance_id & self.instance_mask) << self.sequence_bits)
47
+ | (sequence & self.sequence_mask)
48
+ ) & MASK64
49
+
50
+ def decompose(self, raw: int) -> DecodedId:
51
+ """Unpack a raw 64-bit integer back into instance_id and sequence."""
52
+ seq = raw & self.sequence_mask
53
+ instance_id = (raw >> self.sequence_bits) & self.instance_mask
54
+ return DecodedId(raw=raw, instance_id=instance_id, sequence=seq)