diff --git a/quantumrandom/__init__.py b/quantumrandom/__init__.py index 7c4bc86..152b1ce 100644 --- a/quantumrandom/__init__.py +++ b/quantumrandom/__init__.py @@ -38,14 +38,16 @@ except ImportError: import simplejson as json -VERSION = '1.9.0' +from quantumrandom.qrandom import QuantumRandom + +VERSION = '1.10.0' URL = 'https://qrng.anu.edu.au/API/jsonI.php' DATA_TYPES = ['uint16', 'hex16'] MAX_LEN = 1024 INT_BITS = 16 -def get_data(data_type='uint16', array_length=1, block_size=1): +def get_data(data_type='uint16', array_length=1, block_size=1, timeout=None): """Fetch data from the ANU Quantum Random Numbers JSON API""" if data_type not in DATA_TYPES: 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): 'length': array_length, 'size': block_size, }) - data = get_json(url) + data = get_json(url, timeout=timeout) assert data['success'] is True, data assert data['length'] == array_length, data return data['data'] if sys.version_info[0] == 2: - def get_json(url): - return json.loads(urlopen(url).read(), object_hook=_object_hook) + def get_json(url, timeout=None): + return json.loads(urlopen(url, timeout=timeout).read(), object_hook=_object_hook) def _object_hook(obj): """We are only dealing with ASCII characters""" @@ -84,21 +86,21 @@ def next(it, default=_sentinel): raise return default else: - def get_json(url): - return json.loads(urlopen(url).read().decode('ascii')) + def get_json(url, timeout=None): + return json.loads(urlopen(url, timeout=timeout).read().decode('ascii')) -def binary(array_length=100, block_size=100): +def binary(array_length=100, block_size=100, **kwargs): """Return a chunk of binary data""" - return binascii.unhexlify(six.b(hex(array_length, block_size))) + return binascii.unhexlify(six.b(hex(array_length, block_size, **kwargs))) -def hex(array_length=100, block_size=100): +def hex(array_length=100, block_size=100, **kwargs): """Return a chunk of hex""" - return ''.join(get_data('hex16', array_length, block_size)) + return ''.join(get_data('hex16', array_length, block_size, **kwargs)) -def randint(min=0, max=10, generator=None): +def randint(min=0, max=10, generator=None, **kwargs): """Return an int between min and max. If given, takes from generator instead. This can be useful to reuse the same cached_generator() instance over multiple calls.""" rand_range = max - min @@ -107,7 +109,7 @@ def randint(min=0, max=10, generator=None): return min if generator is None: - generator = cached_generator() + generator = cached_generator(**kwargs) source_bits = int(math.ceil(math.log(rand_range + 1, 2))) source_size = int(math.ceil(source_bits / float(INT_BITS))) @@ -126,17 +128,17 @@ def randint(min=0, max=10, generator=None): return num / modulos + min -def uint16(array_length=100): +def uint16(array_length=100, **kwargs): """Return a numpy array of uint16 numbers""" import numpy - return numpy.array(get_data('uint16', array_length), dtype=numpy.uint16) + return numpy.array(get_data('uint16', array_length, **kwargs), dtype=numpy.uint16) -def cached_generator(data_type='uint16', cache_size=1024): +def cached_generator(data_type='uint16', cache_size=1024, **kwargs): """Returns numbers. Caches numbers to avoid latency.""" while 1: - for n in get_data(data_type, cache_size, cache_size): + for n in get_data(data_type, cache_size, cache_size, **kwargs): yield n -__all__ = ['get_data', 'binary', 'hex', 'uint16', 'cached_generator', 'randint'] +__all__ = ['get_data', 'binary', 'hex', 'uint16', 'cached_generator', 'randint', 'QuantumRandom'] diff --git a/quantumrandom/qrandom.py b/quantumrandom/qrandom.py new file mode 100644 index 0000000..c12f9cd --- /dev/null +++ b/quantumrandom/qrandom.py @@ -0,0 +1,121 @@ +from binascii import hexlify as _hexlify +from random import Random, RECIP_BPF +import quantumrandom +import six +import threading + + +_longint = int if six.PY3 else long + + +class _QRBackgroundFetchThread(threading.Thread): + def __init__(self, qr): + self.qr = qr + self.should_fetch = threading.Event() + self.idle = threading.Event() + threading.Thread.__init__(self) + self.daemon = True + self.start() + + def run(self): + while self.should_fetch.wait(): + self.idle.clear() + try: + self.qr._refresh() + finally: + self.idle.set() + self.should_fetch.clear() + + +class QuantumRandom(Random): + "An implementation of random.Random that uses the ANU quantum random API" + def __init__(self, x=None, cached_bytes=1024, autofetch_at=1024-64, fetch_timeout=None): + self._fetcher = None + self._autofetch_at = autofetch_at + self._fetch_timeout = fetch_timeout + + if cached_bytes: + self._buf_idx = 1024 # start uninitialized + self._buf_len = cached_bytes + self._buf_lock = threading.RLock() + self._cache_buf = bytearray(cached_bytes) + + if autofetch_at: + self._fetcher = _QRBackgroundFetchThread(self) + self._fetcher.should_fetch.set() + else: + self._autofetch_at = None + self._cache_buf = None + + Random.__init__(self, x) + + def _fetch_qr(self, b): + if b > 1024: + blocks = (b + 1023) // 1024 + block_size = 1024 + else: + blocks = 1 + block_size = b + + return quantumrandom.binary(blocks, block_size, timeout=self._fetch_timeout) + + def _refresh(self, over=0): + refresh = self._fetch_qr(self._buf_len + over) + + with self._buf_lock: + self._cache_buf[:] = refresh[over:] + self._buf_idx = 0 + + if over: + return refresh[:over] + + def _qrandom(self, b): + if self._cache_buf: + with self._buf_lock: + ret = self._cache_buf[self._buf_idx : self._buf_idx + b] + over = self._buf_idx + b - self._buf_len + + if over > 0: + if self._fetcher and self._fetcher.idle.is_set(): + ret += self._refresh(over) + else: + self._fetcher and self._fetcher.idle.wait() + ret += self._qrandom(over) + else: + self._buf_idx += b + + # notify the background thread that we need more data + if (self._autofetch_at and self._buf_idx > self._autofetch_at + and not self._fetcher.should_fetch.is_set()): + self._fetcher.should_fetch.set() + + return ret + else: + return self._fetch_qr(b) + + def random(self): + intstr = _hexlify(self._qrandom(7)) + return (_longint(intstr, 16) >> 3) * RECIP_BPF + + def getrandbits(self, k): + if k <= 0: + raise ValueError('number of bits must be greater than zero') + if k != int(k): + raise TypeError('number of bits should be an integer') + + # bits / 8 and rounded up + numbytes = (k + 7) // 8 + x = _longint(_hexlify(self._qrandom(numbytes)), 16) + # trim excess bits + return x >> (numbytes * 8 - k) + + def seed(self, *args, **kwds): + return None + + def _notimplemented(self, *args, **kwds): + raise NotImplementedError('Quantum entropy source does not have state.') + + getstate = setstate = _notimplemented + + +__all__ = ['QuantumRandom'] diff --git a/setup.py b/setup.py index f6d0202..5f05e18 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages import sys -version = '1.9.0' +version = '1.10.0' f = open('README.rst') long_description = f.read()