Skip to content

Commit

Permalink
Merge pull request #3 from sbalian/api-key
Browse files Browse the repository at this point in the history
API key
  • Loading branch information
sbalian authored Apr 6, 2022
2 parents f0d4866 + e20fe8e commit 2d74116
Show file tree
Hide file tree
Showing 14 changed files with 215 additions and 43 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,4 @@ jobs:
- name: Test
run: |
source $HOME/.poetry/env
poetry run pytest
QRANDOM_API_KEY=key poetry run pytest
23 changes: 21 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,23 @@ pip install quantum-random[numpy]
Note that the NumPy integration is not well-tested and is not available
for Python 3.10.

## Setup: passing your API key

ANU now requires you to use an API key. You can get a free (trial) or paid key
from [here][anupricing].

You can pass your key to `qrandom` in three ways:

1. By setting the environment variable `QRANDOM_API_KEY`.
2. By running `qrandom-init` to save your key in an INI config file that is
stored in a subdirectory of your default home config directory (as specified
by XDG, e.g., `/home/<your-username>/.config/qrandom/`).
3. By running `qrandom-init` to save your key in an INI file in a directory
of your choice set by `QRANDOM_CONFIG_DIR`.

`qrandom` will look for the key in the order above. The `qrandom-init` utility
is interactive and comes installed with `qrandom`.

## Usage

Just import `qrandom` and use it like you'd use the
Expand Down Expand Up @@ -64,7 +81,8 @@ array([[0.37220278, 0.24337193, 0.67534826],

To run the tests locally, you will need [poetry][poetry] and Python 3.7-3.10.
One way of having multiple Python versions is to use [pyenv][pyenv] and list
the versions in `.python-version`.
the versions in `.python-version`. First, make sure you have set up your key.
Then,

```bash
poetry install
Expand Down Expand Up @@ -92,7 +110,8 @@ NumPy support is provided using [RandomGen][randomgen].

See [LICENCE](./LICENSE).

[anu]: https://qrng.anu.edu.au
[anu]: https://quantumnumbers.anu.edu.au
[anupricing]: https://quantumnumbers.anu.edu.au/pricing
[pyrandom]: https://docs.python.org/3.9/library/random.html
[poetry]: https://python-poetry.org
[pyenv]: https://github.com/pyenv/pyenv
Expand Down
2 changes: 0 additions & 2 deletions docs/generate_plot.py
100755 → 100644
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
#!/usr/bin/env python

import random

import matplotlib.pyplot as plt
Expand Down
Binary file modified docs/random.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 10 additions & 2 deletions docs/uniform.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,16 @@ Calling `qrandom.random()` 10,000 times, and comparing it to `random.random()`:
![Random](./random.png)

The [Kolmogorov–Smirnov statistics][kstest] with the reference distribution
`scipy.uniform` are 0.007 for both the quantum and standard Python samples
(using [`scipy.stats.kstest`][scipy-kstest]).
`scipy.uniform` gives the following results.

| Trial | Quantum statistic | Quantum p-value | Standard statistic | Standard p-value |
| ----- | ----------------- | --------------- | ------------------ | ---------------- |
| 1 | 0.01 | 0.2 | 0.005 | 0.9 |
| 2 | 0.007 | 0.7 | 0.005 | 1.0 |
| 3 | 0.006 | 0.9 | 0.008 | 0.6 |

Each trial is a different run of [`scipy.stats.kstest`][scipy-kstest] with
10,000 numbers. The plot shows the numbers for the last trial.

[kstest]: https://en.wikipedia.org/wiki/Kolmogorov%E2%80%93Smirnov_test
[scipy-kstest]: https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.kstest.html
14 changes: 13 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "quantum-random"
version = "1.1.1"
version = "1.2.2"
description = "Quantum random numbers"
authors = ["Seto Balian <[email protected]>"]
packages = [{ include = "qrandom" }]
Expand Down Expand Up @@ -28,6 +28,7 @@ requests = "^2.25.1"

numpy = { version = "^1.17", optional = true, python = ">=3.7,<3.10" }
randomgen = { version = "^1.21.2", optional = true, python = ">=3.7,<3.10" }
xdg = "^5.1.1"

[tool.poetry.dev-dependencies]
isort = "^5.7.0"
Expand All @@ -44,6 +45,9 @@ scipy = "<1.8"
[tool.poetry.extras]
numpy = ["numpy", "randomgen"]

[tool.poetry.scripts]
qrandom-init = 'qrandom._cli:init'

[tool.isort]
line_length = 79
known_first_party = "qrandom"
Expand Down
49 changes: 32 additions & 17 deletions qrandom/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Implements a quantum random number generator as a subclass of random.Random
as described on https://docs.python.org/. The numbers come from the
ANU Quantum Random Number Generator at The Australian National University
(https://qrng.anu.edu.au/).
(https://quantumnumbers.anu.edu.au/).
You can use it just like the standard random module (this module replaces the
default Mersenne Twister). But seeding is ignored and getstate() and setstate()
Expand All @@ -20,7 +20,9 @@

import requests

__version__ = "1.1.1"
from . import _key

__version__ = "1.2.2"

__all__ = [
"betavariate",
Expand All @@ -47,35 +49,48 @@
"fill",
]

_ANU_URL = "https://qrng.anu.edu.au/API/jsonI.php"
_ANU_URL = "https://api.quantumnumbers.anu.edu.au"


def _get_qrand_int64(size: int = 1024) -> List[int]:
def _get_qrand_int64(
size: int = 1024, raw: bool = False
) -> Union[Dict[str, Union[bool, str, Dict[str, List[str]]]], List[int]]:
"""Gets quantum random int64s from the ANU API.
size is the number of int64s fetched (1024 by default).
Raises RuntimeError if the ANU API call is not successful. This includes
the case of size > 1024.
raw = False (default) outputs a list of integers in base 10. Otherwise,
the output is the raw JSON from the API (with the results nested and
as hex strings).
Raises HTTPError if the ANU API call is not successful.
This includes the case of size > 1024.
"""
params: Dict[str, Union[int, str]] = {
"length": size,
"type": "hex16",
"size": 8,
"size": 4,
}
response = requests.get(_ANU_URL, params)
response.raise_for_status()
headers: Dict[str, str] = {"x-api-key": _key.get_api_key()}
response = requests.get(_ANU_URL, params, headers=headers)
try:
response.raise_for_status()
except requests.HTTPError as http_error:
print("JSON response received:")
print(response.json())
raise http_error
r_json = response.json()

if r_json["success"]:
return [int(number, 16) for number in r_json["data"]]
else:
raise RuntimeError(
"The 'success' field in the ANU response was False."
if not r_json["success"]:
# This used to happen with the old API so keeping it here just in case.
raise requests.HTTPError(
"The 'success' field in the ANU response was False even "
f"though the status code was {response.status_code}."
)
# The status code is 200 when this happens
return
if raw:
return r_json
else:
return [int(number, 16) for number in r_json["data"]]


class QuantumRandom(pyrandom.Random):
Expand Down
47 changes: 47 additions & 0 deletions qrandom/_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import configparser
import os
import pathlib
import sys

import xdg

DEFAULT_DIR = xdg.xdg_config_home() / "qrandom"


def init():
print("This utility will help you set the API key for qrandom.")
print("You can get a key from https://quantumnumbers.anu.edu.au/.")
print("Where would you like to store the key?")
print("[Type in a directory path and press enter or just press enter to ")
print(f"use the default path ({DEFAULT_DIR})]:")
user_input_dir = input().strip()
if user_input_dir in ["", DEFAULT_DIR]:
config_dir = DEFAULT_DIR
os.makedirs(config_dir, exist_ok=True)
config_path = config_dir / "qrandom.ini"
else:
config_dir = pathlib.Path(user_input_dir).expanduser().resolve()
if config_dir.exists() and config_dir.is_file():
print(f"{config_dir} must be a directory.")
sys.exit(1)
os.makedirs(config_dir, exist_ok=True)
config_path = config_dir / "qrandom.ini"
if config_path.exists():
print(f"{config_path} exists. Would you like to overwrite it? [Y/n]:")
if input().strip() != "Y":
print("Aborted.")
sys.exit(1)
config = configparser.ConfigParser()
config.add_section("default")
print("Enter or paste your API key:")
api_key = input().strip()
config["default"]["key"] = api_key
with open(config_path, "w") as f:
config.write(f)
print(f"Stored API key in {config_path}.")
if config_dir != DEFAULT_DIR:
print(
"Since you did not write to the default path, do not forget to "
f"set QRANDOM_CONFIG_DIR to {config_dir} when using qrandom."
)
return
79 changes: 79 additions & 0 deletions qrandom/_key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import configparser
import os
import pathlib

import xdg

INIT_MSG = "initialise qrandom.ini by running qrandom-init"


class ApiKeyNotFoundInEnvError(Exception):
pass


class CustomConfigDirNotFoundError(Exception):
pass


def get_from_env() -> str:
try:
return os.environ["QRANDOM_API_KEY"]
except KeyError:
raise ApiKeyNotFoundInEnvError


def get_custom_dir() -> pathlib.Path:
try:
config_dir = (
pathlib.Path(os.environ["QRANDOM_CONFIG_DIR"])
.expanduser()
.resolve()
)
if not config_dir.exists():
raise IOError(
f"{config_dir} does not exist. {INIT_MSG.capitalize()}."
)
if config_dir.is_file():
raise IOError(
f"{config_dir} must be a directory. {INIT_MSG.capitalize()}."
)
return config_dir
except KeyError:
raise CustomConfigDirNotFoundError


def get_from_file(config_dir: pathlib.Path) -> str:
config_path = config_dir / "qrandom.ini"
if not config_path.exists():
raise FileNotFoundError(
f"{config_path} does not exist. {INIT_MSG.capitalize()}."
)
if config_path.is_dir():
raise IsADirectoryError(
f"{config_path} cannot be a directory.{INIT_MSG.capitalize()}."
)
config = configparser.ConfigParser()
config.read(config_path)
return config["default"]["key"]


def get_api_key() -> str:
try:
return get_from_env()
except ApiKeyNotFoundInEnvError:
try:
config_dir = get_custom_dir()
except CustomConfigDirNotFoundError:
config_dir = xdg.xdg_config_home() / "qrandom"
config_path = config_dir / "qrandom.ini"
if not config_path.exists():
raise FileNotFoundError(
f"{config_path} does not exist. {INIT_MSG.capitalize()}."
)
if config_path.is_dir():
raise IsADirectoryError(
f"{config_path} cannot be a directory.{INIT_MSG.capitalize()}."
)
config = configparser.ConfigParser()
config.read(config_path)
return config["default"]["key"]
4 changes: 3 additions & 1 deletion qrandom/numpy.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Adds numpy support to qrandom."""

from typing import Any

import numpy.random
import randomgen # type: ignore

Expand All @@ -10,7 +12,7 @@ class ANUQRNG(QuantumRandom):
def __init__(self):
super().__init__()

def random_raw(self, voidp) -> int:
def random_raw(self, voidp: Any) -> int:
return self._get_rand_int64()


Expand Down
2 changes: 1 addition & 1 deletion tests/data/responses.json

Large diffs are not rendered by default.

15 changes: 1 addition & 14 deletions tests/get_responses.py
100755 → 100644
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
#!/usr/bin/env python

import json
from typing import Dict, Union

import requests

import qrandom

Expand All @@ -13,15 +8,7 @@ def main():
num_hits = 10
for hit in range(num_hits):
print(f"Getting {hit+1} of {num_hits} ...")
params: Dict[str, Union[int, str]] = {
"length": 1024,
"type": "hex16",
"size": 8,
}
response = requests.get(qrandom._ANU_URL, params)
response.raise_for_status()
json_r = response.json()
json_responses.append(json_r)
json_responses.append(qrandom._get_qrand_int64(size=1024, raw=True))

path = "data/responses.json"
with open(path, "w") as f:
Expand Down
Loading

0 comments on commit 2d74116

Please sign in to comment.