Skip to content

Commit cebd25b

Browse files
committed
Add pure python fallback implementation
1 parent 34b2e36 commit cebd25b

File tree

6 files changed

+397
-125
lines changed

6 files changed

+397
-125
lines changed

setup.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
#!/usr/bin/env python
2+
import os
3+
import sys
24
import sysconfig
35
from typing import Any, Dict, List, Optional, Tuple
46

57
from setuptools import Extension, setup
68

9+
build: bool = os.environ.get("SONYFLAKE_TURBO_BUILD", "1").lower() in ("1", "true")
710
options: Dict[str, Any] = {}
811
define_macros: List[Tuple[str, Optional[str]]] = []
912
py_limited_api: bool = not sysconfig.get_config_var("Py_GIL_DISABLED")
1013
cflags: List[str] = []
1114

15+
if sys.implementation.name != "cpython":
16+
build = False
17+
1218
if sysconfig.get_platform().startswith("win"):
1319
cflags.append("/utf-8")
1420
cflags.append("/std:c17")
@@ -24,9 +30,10 @@
2430
options["bdist_wheel"] = {"py_limited_api": "cp310"}
2531
define_macros.append(("Py_LIMITED_API", "0x030a00f0"))
2632

27-
setup(
28-
options=options,
29-
ext_modules=[
33+
34+
setup_kwargs = {
35+
"options": options,
36+
"ext_modules": [
3037
Extension(
3138
"sonyflake_turbo._sonyflake",
3239
sources=[
@@ -39,4 +46,9 @@
3946
extra_compile_args=cflags,
4047
),
4148
],
42-
)
49+
}
50+
51+
if not build:
52+
setup_kwargs = {}
53+
54+
setup(**setup_kwargs)

src/sonyflake_turbo/__init__.py

Lines changed: 31 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,27 @@
1-
from typing import (
2-
Any,
3-
AsyncIterable,
4-
AsyncIterator,
5-
Awaitable,
6-
Callable,
7-
ClassVar,
8-
Generator,
9-
TypeAlias,
10-
)
11-
12-
from ._sonyflake import (
13-
SONYFLAKE_EPOCH,
14-
SONYFLAKE_MACHINE_ID_BITS,
15-
SONYFLAKE_MACHINE_ID_MAX,
16-
SONYFLAKE_MACHINE_ID_OFFSET,
17-
SONYFLAKE_SEQUENCE_BITS,
18-
SONYFLAKE_SEQUENCE_MAX,
19-
SONYFLAKE_TIME_OFFSET,
20-
MachineIDLCG,
21-
SonyFlake,
22-
)
23-
24-
AsyncSleep: TypeAlias = Callable[[float], Awaitable[None]]
1+
try:
2+
from ._sonyflake import (
3+
SONYFLAKE_EPOCH,
4+
SONYFLAKE_MACHINE_ID_BITS,
5+
SONYFLAKE_MACHINE_ID_MAX,
6+
SONYFLAKE_MACHINE_ID_OFFSET,
7+
SONYFLAKE_SEQUENCE_BITS,
8+
SONYFLAKE_SEQUENCE_MAX,
9+
SONYFLAKE_TIME_OFFSET,
10+
MachineIDLCG,
11+
SonyFlake,
12+
)
13+
except ImportError: # pragma: no cover
14+
from .pure import (
15+
SONYFLAKE_EPOCH,
16+
SONYFLAKE_MACHINE_ID_BITS,
17+
SONYFLAKE_MACHINE_ID_MAX,
18+
SONYFLAKE_MACHINE_ID_OFFSET,
19+
SONYFLAKE_SEQUENCE_BITS,
20+
SONYFLAKE_SEQUENCE_MAX,
21+
SONYFLAKE_TIME_OFFSET,
22+
MachineIDLCG,
23+
SonyFlake,
24+
)
2525

2626
__all__ = [
2727
"SONYFLAKE_EPOCH",
@@ -37,7 +37,7 @@
3737
]
3838

3939

40-
class AsyncSonyFlake(Awaitable[int], AsyncIterable[int]):
40+
class AsyncSonyFlake:
4141
"""Async wrapper for :class:`SonyFlake`.
4242
4343
Usage:
@@ -57,10 +57,9 @@ class AsyncSonyFlake(Awaitable[int], AsyncIterable[int]):
5757
break
5858
"""
5959

60-
__slots__: ClassVar[tuple[str, ...]] = ("sf", "sleep")
61-
sleep: AsyncSleep
60+
__slots__ = ("sf", "sleep")
6261

63-
def __init__(self, sf: SonyFlake, sleep: AsyncSleep | None = None) -> None:
62+
def __init__(self, sf: SonyFlake, sleep=None):
6463
"""Initialize AsyncSonyFlake ID generator.
6564
6665
Args:
@@ -76,7 +75,7 @@ def __init__(self, sf: SonyFlake, sleep: AsyncSleep | None = None) -> None:
7675
self.sf = sf
7776
self.sleep = sleep
7877

79-
async def __call__(self, n: int) -> list[int]:
78+
async def __call__(self, n):
8079
"""Generate multiple SonyFlake IDs at once.
8180
8281
Roughly equivalent to ``[await asf for _ in range(n)]``, but more
@@ -96,7 +95,7 @@ async def __call__(self, n: int) -> list[int]:
9695

9796
return ids
9897

99-
def __await__(self) -> Generator[Any, Any, int]:
98+
def __await__(self):
10099
"""Produce a SonyFlake ID."""
101100

102101
id_, to_sleep = self.sf._raw(None)
@@ -105,12 +104,12 @@ def __await__(self) -> Generator[Any, Any, int]:
105104

106105
return id_
107106

108-
def __aiter__(self) -> AsyncIterator[int]:
107+
def __aiter__(self):
109108
"""Return an infinite SonyFlake ID async iterator."""
110109

111110
return self._gen().__aiter__()
112111

113-
async def _gen(self) -> AsyncIterator[int]:
112+
async def _gen(self):
114113
"""Infinite SonyFlake ID async generator."""
115114

116115
while True:

src/sonyflake_turbo/__init__.pyi

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from collections.abc import AsyncIterable, AsyncIterator, Awaitable
2+
from typing import (
3+
Any,
4+
Callable,
5+
ClassVar,
6+
Generator,
7+
TypeAlias,
8+
)
9+
10+
from ._sonyflake import (
11+
SONYFLAKE_EPOCH,
12+
SONYFLAKE_MACHINE_ID_BITS,
13+
SONYFLAKE_MACHINE_ID_MAX,
14+
SONYFLAKE_MACHINE_ID_OFFSET,
15+
SONYFLAKE_SEQUENCE_BITS,
16+
SONYFLAKE_SEQUENCE_MAX,
17+
SONYFLAKE_TIME_OFFSET,
18+
MachineIDLCG,
19+
SonyFlake,
20+
)
21+
22+
__all__ = [
23+
"SONYFLAKE_EPOCH",
24+
"SONYFLAKE_MACHINE_ID_BITS",
25+
"SONYFLAKE_MACHINE_ID_MAX",
26+
"SONYFLAKE_MACHINE_ID_OFFSET",
27+
"SONYFLAKE_SEQUENCE_BITS",
28+
"SONYFLAKE_SEQUENCE_MAX",
29+
"SONYFLAKE_TIME_OFFSET",
30+
"AsyncSonyFlake",
31+
"MachineIDLCG",
32+
"SonyFlake",
33+
]
34+
35+
AsyncSleep: TypeAlias = Callable[[float], Awaitable[None]]
36+
37+
class AsyncSonyFlake(Awaitable[int], AsyncIterable[int]):
38+
"""Async wrapper for :class:`SonyFlake`.
39+
40+
Usage:
41+
42+
.. code-block:: python
43+
44+
import asyncio
45+
46+
sf = SonyFlake(0x1337, 0xCAFE, start_time=1749081600)
47+
asf = AsyncSonyFlake(sf, asyncio.sleep)
48+
49+
print(await asf)
50+
print(await asf(5))
51+
52+
async for id_ in asf:
53+
print(id_)
54+
break
55+
"""
56+
57+
__slots__: ClassVar[tuple[str, ...]]
58+
sleep: AsyncSleep
59+
60+
def __init__(self, sf: SonyFlake, sleep: AsyncSleep | None = None) -> None: ...
61+
async def __call__(self, n: int) -> list[int]: ...
62+
def __await__(self) -> Generator[Any, Any, int]: ...
63+
def __aiter__(self) -> AsyncIterator[int]: ...
64+
async def _gen(self) -> AsyncIterator[int]: ...

src/sonyflake_turbo/pure.py

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
from threading import Lock
2+
from time import sleep, time_ns
3+
from typing import Self, overload
4+
5+
SONYFLAKE_EPOCH = 1409529600 # 2014-09-01 00:00:00 UTC
6+
SONYFLAKE_SEQUENCE_BITS = 8
7+
SONYFLAKE_SEQUENCE_COUNT = 1 << SONYFLAKE_SEQUENCE_BITS
8+
SONYFLAKE_SEQUENCE_MAX = SONYFLAKE_SEQUENCE_COUNT - 1
9+
SONYFLAKE_MACHINE_ID_BITS = 16
10+
SONYFLAKE_MACHINE_ID_MAX = (1 << SONYFLAKE_MACHINE_ID_BITS) - 1
11+
SONYFLAKE_MACHINE_ID_OFFSET = SONYFLAKE_SEQUENCE_BITS
12+
SONYFLAKE_TIME_OFFSET = SONYFLAKE_MACHINE_ID_BITS + SONYFLAKE_SEQUENCE_BITS
13+
14+
15+
def sf_to_ns(start_time: int, sf: int) -> int:
16+
return (start_time + sf) * 10_000_000
17+
18+
19+
def ns_to_sf(start_time: int, ns: int) -> int:
20+
return ns // 10_000_000 - start_time
21+
22+
23+
def compose(i: int, elapsed: int, machine_ids: list[int]) -> int:
24+
t = elapsed << SONYFLAKE_TIME_OFFSET
25+
m = machine_ids[i >> SONYFLAKE_SEQUENCE_BITS] << SONYFLAKE_MACHINE_ID_OFFSET
26+
c = i & SONYFLAKE_SEQUENCE_MAX
27+
return t | m | c
28+
29+
30+
def diff(start_time: int, elapsed: int, current_ns: int) -> float:
31+
d = sf_to_ns(start_time, elapsed) - current_ns
32+
if d > 0:
33+
return d / 1_000_000_000
34+
return 0
35+
36+
37+
def machine_id_lcg(x: int) -> int:
38+
return (32309 * x + 13799) % 65536
39+
40+
41+
class SonyFlake:
42+
__slots__ = ("_machine_ids", "_start_time", "_elapsed", "_max_i", "_i", "_lock")
43+
44+
_start_time: int
45+
_elapsed: int
46+
_machine_ids: list[int]
47+
_max_i: int
48+
_i: int
49+
_lock: Lock
50+
51+
def __init__(self, *machine_ids: int, start_time: int = SONYFLAKE_EPOCH):
52+
machine_ids_set = set(machine_ids)
53+
54+
if len(machine_ids_set) == 0:
55+
raise ValueError("At least one machine ID must be provided")
56+
57+
if len(machine_ids_set) > 65536:
58+
raise ValueError("Too many machine IDs, maximum is 65536")
59+
60+
if len(machine_ids_set) != len(machine_ids):
61+
raise ValueError("Duplicate machine IDs are not allowed")
62+
63+
if not all(map(lambda x: isinstance(x, int), machine_ids_set)):
64+
raise TypeError("Machine IDs must be integers")
65+
66+
if min(machine_ids_set) < 0 or max(machine_ids_set) > 0xFFFF:
67+
raise ValueError("Machine IDs must be in range [0, 65535]")
68+
69+
if not isinstance(start_time, int):
70+
raise TypeError("start_time must be an integer")
71+
72+
self._machine_ids = sorted(machine_ids_set)
73+
self._max_i = len(machine_ids_set) * SONYFLAKE_SEQUENCE_COUNT
74+
self._start_time = start_time * 100
75+
self._elapsed = 0
76+
self._i = 0
77+
self._lock = Lock()
78+
79+
def __iter__(self) -> Self:
80+
return self
81+
82+
def __next__(self) -> int:
83+
id_, to_sleep = self._next()
84+
if to_sleep > 0:
85+
sleep(to_sleep)
86+
return id_
87+
88+
def __call__(self, n: int) -> list[int]:
89+
ids, to_sleep = self._next_n(n)
90+
sleep(to_sleep)
91+
return ids
92+
93+
def __repr__(self) -> str:
94+
cls = self.__class__.__name__
95+
machine_ids = ", ".join(map(str, sorted(self._machine_ids)))
96+
return f"{cls}({machine_ids}, start_time={self._start_time // 100})"
97+
98+
@overload
99+
def _raw(self, n: int) -> tuple[list[int], float]: ...
100+
@overload
101+
def _raw(self, n: None) -> tuple[int, float]: ...
102+
103+
def _raw(self, n: int | None) -> tuple[int | list[int], float]:
104+
if n is None:
105+
return self._next()
106+
return self._next_n(n)
107+
108+
def _next(self) -> tuple[int, float]:
109+
start_time = self._start_time
110+
machine_ids = self._machine_ids
111+
112+
with self._lock:
113+
current_ns = time_ns()
114+
current = ns_to_sf(start_time, current_ns)
115+
elapsed = self._elapsed
116+
117+
if elapsed < current:
118+
self._elapsed = elapsed = current
119+
self._i = i = 0
120+
else:
121+
self._i = i = (self._i + 1) % self._max_i
122+
123+
if i == 0:
124+
self._elapsed = elapsed = elapsed + 1
125+
126+
return (
127+
compose(i, elapsed, machine_ids),
128+
diff(start_time, elapsed, current_ns),
129+
)
130+
131+
def _next_n(self, n: int) -> tuple[list[int], float]:
132+
if n < 1:
133+
return [], 0
134+
135+
start_time = self._start_time
136+
machine_ids = self._machine_ids
137+
max_i = self._max_i
138+
result: list[int] = []
139+
140+
with self._lock:
141+
current_ns = time_ns()
142+
current = ns_to_sf(start_time, current_ns)
143+
elapsed = self._elapsed
144+
145+
if elapsed < current:
146+
elapsed = current
147+
i = 0
148+
else:
149+
i = self._i
150+
151+
while len(result) < n:
152+
result.append(compose(i, elapsed, machine_ids))
153+
154+
i = (i + 1) % max_i
155+
156+
if i == 0:
157+
elapsed += 1
158+
159+
self._elapsed = elapsed
160+
self._i = i
161+
162+
return result, diff(start_time, elapsed, current_ns)
163+
164+
165+
class MachineIDLCG:
166+
def __init__(self, seed: int = 0):
167+
self._seed = seed
168+
self._lock = Lock()
169+
170+
def __next__(self) -> int:
171+
with self._lock:
172+
self._seed = x = machine_id_lcg(self._seed)
173+
return x
174+
175+
def __repr__(self) -> str:
176+
return f"{self.__class__.__name__}({self._seed})"

0 commit comments

Comments
 (0)