Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 20 additions & 18 deletions quantumrandom/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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"""
Expand All @@ -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
Expand All @@ -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)))
Expand All @@ -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']
121 changes: 121 additions & 0 deletions quantumrandom/qrandom.py
Original file line number Diff line number Diff line change
@@ -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']
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
@@ -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()
Expand Down