Skip to content

Commit 8ded81b

Browse files
committed
v1.9.0 Random subclass, fetch timeout
1 parent 2c2725b commit 8ded81b

3 files changed

Lines changed: 142 additions & 19 deletions

File tree

quantumrandom/__init__.py

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,16 @@
3838
except ImportError:
3939
import simplejson as json
4040

41-
VERSION = '1.9.0'
41+
from quantumrandom.qrandom import QuantumRandom
42+
43+
VERSION = '1.10.0'
4244
URL = 'https://qrng.anu.edu.au/API/jsonI.php'
4345
DATA_TYPES = ['uint16', 'hex16']
4446
MAX_LEN = 1024
4547
INT_BITS = 16
4648

4749

48-
def get_data(data_type='uint16', array_length=1, block_size=1):
50+
def get_data(data_type='uint16', array_length=1, block_size=1, timeout=None):
4951
"""Fetch data from the ANU Quantum Random Numbers JSON API"""
5052
if data_type not in DATA_TYPES:
5153
raise Exception("data_type must be one of %s" % DATA_TYPES)
@@ -58,15 +60,15 @@ def get_data(data_type='uint16', array_length=1, block_size=1):
5860
'length': array_length,
5961
'size': block_size,
6062
})
61-
data = get_json(url)
63+
data = get_json(url, timeout=timeout)
6264
assert data['success'] is True, data
6365
assert data['length'] == array_length, data
6466
return data['data']
6567

6668

6769
if sys.version_info[0] == 2:
68-
def get_json(url):
69-
return json.loads(urlopen(url).read(), object_hook=_object_hook)
70+
def get_json(url, timeout=None):
71+
return json.loads(urlopen(url, timeout=timeout).read(), object_hook=_object_hook)
7072

7173
def _object_hook(obj):
7274
"""We are only dealing with ASCII characters"""
@@ -84,21 +86,21 @@ def next(it, default=_sentinel):
8486
raise
8587
return default
8688
else:
87-
def get_json(url):
88-
return json.loads(urlopen(url).read().decode('ascii'))
89+
def get_json(url, timeout=None):
90+
return json.loads(urlopen(url, timeout=timeout).read().decode('ascii'))
8991

9092

91-
def binary(array_length=100, block_size=100):
93+
def binary(array_length=100, block_size=100, **kwargs):
9294
"""Return a chunk of binary data"""
93-
return binascii.unhexlify(six.b(hex(array_length, block_size)))
95+
return binascii.unhexlify(six.b(hex(array_length, block_size, **kwargs)))
9496

9597

96-
def hex(array_length=100, block_size=100):
98+
def hex(array_length=100, block_size=100, **kwargs):
9799
"""Return a chunk of hex"""
98-
return ''.join(get_data('hex16', array_length, block_size))
100+
return ''.join(get_data('hex16', array_length, block_size, **kwargs))
99101

100102

101-
def randint(min=0, max=10, generator=None):
103+
def randint(min=0, max=10, generator=None, **kwargs):
102104
"""Return an int between min and max. If given, takes from generator instead.
103105
This can be useful to reuse the same cached_generator() instance over multiple calls."""
104106
rand_range = max - min
@@ -107,7 +109,7 @@ def randint(min=0, max=10, generator=None):
107109
return min
108110

109111
if generator is None:
110-
generator = cached_generator()
112+
generator = cached_generator(**kwargs)
111113

112114
source_bits = int(math.ceil(math.log(rand_range + 1, 2)))
113115
source_size = int(math.ceil(source_bits / float(INT_BITS)))
@@ -126,17 +128,17 @@ def randint(min=0, max=10, generator=None):
126128
return num / modulos + min
127129

128130

129-
def uint16(array_length=100):
131+
def uint16(array_length=100, **kwargs):
130132
"""Return a numpy array of uint16 numbers"""
131133
import numpy
132-
return numpy.array(get_data('uint16', array_length), dtype=numpy.uint16)
134+
return numpy.array(get_data('uint16', array_length, **kwargs), dtype=numpy.uint16)
133135

134136

135-
def cached_generator(data_type='uint16', cache_size=1024):
137+
def cached_generator(data_type='uint16', cache_size=1024, **kwargs):
136138
"""Returns numbers. Caches numbers to avoid latency."""
137139
while 1:
138-
for n in get_data(data_type, cache_size, cache_size):
140+
for n in get_data(data_type, cache_size, cache_size, **kwargs):
139141
yield n
140142

141143

142-
__all__ = ['get_data', 'binary', 'hex', 'uint16', 'cached_generator', 'randint']
144+
__all__ = ['get_data', 'binary', 'hex', 'uint16', 'cached_generator', 'randint', 'QuantumRandom']

quantumrandom/qrandom.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
from binascii import hexlify as _hexlify
2+
from random import Random, RECIP_BPF
3+
import quantumrandom
4+
import six
5+
import threading
6+
7+
8+
_longint = int if six.PY3 else long
9+
10+
11+
class _QRBackgroundFetchThread(threading.Thread):
12+
def __init__(self, qr):
13+
self.qr = qr
14+
self.should_fetch = threading.Event()
15+
self.idle = threading.Event()
16+
threading.Thread.__init__(self)
17+
self.daemon = True
18+
self.start()
19+
20+
def run(self):
21+
while self.should_fetch.wait():
22+
self.idle.clear()
23+
try:
24+
self.qr._refresh()
25+
finally:
26+
self.idle.set()
27+
self.should_fetch.clear()
28+
29+
30+
class QuantumRandom(Random):
31+
"An implementation of random.Random that uses the ANU quantum random API"
32+
def __init__(self, x=None, cached_bytes=1024, autofetch_at=1024-64, fetch_timeout=None):
33+
self._fetcher = None
34+
self._autofetch_at = autofetch_at
35+
self._fetch_timeout = fetch_timeout
36+
37+
if cached_bytes:
38+
self._buf_idx = 1024 # start uninitialized
39+
self._buf_len = cached_bytes
40+
self._buf_lock = threading.RLock()
41+
self._cache_buf = bytearray(cached_bytes)
42+
43+
if autofetch_at:
44+
self._fetcher = _QRBackgroundFetchThread(self)
45+
self._fetcher.should_fetch.set()
46+
else:
47+
self._autofetch_at = None
48+
self._cache_buf = None
49+
50+
Random.__init__(self, x)
51+
52+
def _fetch_qr(self, b):
53+
if b > 1024:
54+
blocks = (b + 1023) // 1024
55+
block_size = 1024
56+
else:
57+
blocks = 1
58+
block_size = b
59+
60+
return quantumrandom.binary(blocks, block_size, timeout=self._fetch_timeout)
61+
62+
def _refresh(self, over=0):
63+
refresh = self._fetch_qr(self._buf_len + over)
64+
65+
with self._buf_lock:
66+
self._cache_buf[:] = refresh[over:]
67+
self._buf_idx = 0
68+
69+
if over:
70+
return refresh[:over]
71+
72+
def _qrandom(self, b):
73+
if self._cache_buf:
74+
with self._buf_lock:
75+
ret = self._cache_buf[self._buf_idx : self._buf_idx + b]
76+
over = self._buf_idx + b - self._buf_len
77+
78+
if over > 0:
79+
if self._fetcher and self._fetcher.idle.is_set():
80+
ret += self._refresh(over)
81+
else:
82+
self._fetcher and self._fetcher.idle.wait()
83+
ret += self._qrandom(over)
84+
else:
85+
self._buf_idx += b
86+
87+
# notify the background thread that we need more data
88+
if (self._autofetch_at and self._buf_idx > self._autofetch_at
89+
and not self._fetcher.should_fetch.is_set()):
90+
self._fetcher.should_fetch.set()
91+
92+
return ret
93+
else:
94+
return self._fetch_qr(b)
95+
96+
def random(self):
97+
intstr = _hexlify(self._qrandom(7))
98+
return (_longint(intstr, 16) >> 3) * RECIP_BPF
99+
100+
def getrandbits(self, k):
101+
if k <= 0:
102+
raise ValueError('number of bits must be greater than zero')
103+
if k != int(k):
104+
raise TypeError('number of bits should be an integer')
105+
106+
# bits / 8 and rounded up
107+
numbytes = (k + 7) // 8
108+
x = _longint(_hexlify(self._qrandom(numbytes)), 16)
109+
# trim excess bits
110+
return x >> (numbytes * 8 - k)
111+
112+
def seed(self, *args, **kwds):
113+
return None
114+
115+
def _notimplemented(self, *args, **kwds):
116+
raise NotImplementedError('Quantum entropy source does not have state.')
117+
118+
getstate = setstate = _notimplemented
119+
120+
121+
__all__ = ['QuantumRandom']

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from setuptools import setup, find_packages
22
import sys
33

4-
version = '1.9.0'
4+
version = '1.10.0'
55

66
f = open('README.rst')
77
long_description = f.read()

0 commit comments

Comments
 (0)