ethoryx 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ethoryx-0.1.0/.gitignore +24 -0
- ethoryx-0.1.0/LICENSE +21 -0
- ethoryx-0.1.0/PKG-INFO +114 -0
- ethoryx-0.1.0/README.md +90 -0
- ethoryx-0.1.0/pyproject.toml +36 -0
- ethoryx-0.1.0/src/ethoryx/__init__.py +57 -0
- ethoryx-0.1.0/src/ethoryx/client.py +155 -0
- ethoryx-0.1.0/src/ethoryx/errors.py +22 -0
- ethoryx-0.1.0/src/ethoryx/version.py +1 -0
- ethoryx-0.1.0/tests/test_client.py +82 -0
ethoryx-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Build / packaging
|
|
2
|
+
build/
|
|
3
|
+
dist/
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
*.egg
|
|
7
|
+
|
|
8
|
+
# Python
|
|
9
|
+
__pycache__/
|
|
10
|
+
*.py[cod]
|
|
11
|
+
.venv/
|
|
12
|
+
venv/
|
|
13
|
+
.env
|
|
14
|
+
|
|
15
|
+
# Tooling
|
|
16
|
+
.pytest_cache/
|
|
17
|
+
.mypy_cache/
|
|
18
|
+
.ruff_cache/
|
|
19
|
+
.tox/
|
|
20
|
+
|
|
21
|
+
# OS / editor
|
|
22
|
+
.DS_Store
|
|
23
|
+
.idea/
|
|
24
|
+
.vscode/
|
ethoryx-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ethoryx Research (Tesfaye Dereje)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
ethoryx-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ethoryx
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python client for the Ethoryx prime-generation API (RSA, FHE, zk-SNARK, and NTT primes).
|
|
5
|
+
Project-URL: Homepage, https://ethoryx.io
|
|
6
|
+
Project-URL: Documentation, https://ethoryx.io/docs
|
|
7
|
+
Project-URL: Source, https://github.com/Teshgty/ethoryx-python
|
|
8
|
+
Project-URL: API Status, https://api.ethoryx.io/v1/status
|
|
9
|
+
Author-email: Ethoryx Research <research@ethoryx.io>
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: cryptography,ethoryx,fhe,ntt,number-theory,prime,primes,rsa,zk-snark
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
20
|
+
Classifier: Topic :: Security :: Cryptography
|
|
21
|
+
Requires-Python: >=3.8
|
|
22
|
+
Requires-Dist: requests>=2.20
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# ethoryx
|
|
26
|
+
|
|
27
|
+
Official Python client for the [Ethoryx](https://ethoryx.io) prime-generation API —
|
|
28
|
+
structurally-guided primes for **RSA, FHE, zk-SNARKs, and NTT**, backed by the Ethoryx
|
|
29
|
+
C/GMP core. Fewer Miller–Rabin tests, fewer candidates, same cryptographic strength.
|
|
30
|
+
|
|
31
|
+
- API: `https://api.ethoryx.io` · [Docs](https://ethoryx.io/docs) · [Status](https://api.ethoryx.io/v1/status)
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install ethoryx
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick start
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
import ethoryx
|
|
43
|
+
|
|
44
|
+
client = ethoryx.Client(api_key="tg_your_key") # or set ETHORYX_API_KEY
|
|
45
|
+
|
|
46
|
+
# Generate primes
|
|
47
|
+
prime = client.generate(bits=2048)
|
|
48
|
+
rsa = client.generate_rsa(bits=2048)
|
|
49
|
+
pair = client.generate_rsa_pair(bits=2048) # matched (p, q)
|
|
50
|
+
ntt = client.generate_ntt(bits=256)
|
|
51
|
+
fhe = client.generate_fhe(bits=2048) # Starter tier or above
|
|
52
|
+
|
|
53
|
+
# Public endpoints — no API key required
|
|
54
|
+
ethoryx.verify(999983) # {"is_prime": true, ...}
|
|
55
|
+
ethoryx.optimize_next(prime=999983, gap=6) # next-prime prediction
|
|
56
|
+
ethoryx.status() # version, benchmarks
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Get a free key (100 calls/month) at <https://ethoryx.io/register>.
|
|
60
|
+
|
|
61
|
+
## Authentication
|
|
62
|
+
|
|
63
|
+
Pass your key to the client, or set it once in the environment:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
export ETHORYX_API_KEY="tg_your_key"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
client = ethoryx.Client() # picks up ETHORYX_API_KEY
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The public endpoints (`verify`, `optimize_next`, `predict_gap`, `status`) work without a
|
|
74
|
+
key. Everything under `generate*` and `account*` requires one.
|
|
75
|
+
|
|
76
|
+
## API
|
|
77
|
+
|
|
78
|
+
| Method | Endpoint | Auth |
|
|
79
|
+
|--------|----------|------|
|
|
80
|
+
| `client.generate(bits=, method=)` | `GET /v1/generate` | 🔑 |
|
|
81
|
+
| `client.generate_rsa(bits=)` | `GET /v1/generate/rsa` | 🔑 |
|
|
82
|
+
| `client.generate_rsa_pair(bits=)` | `GET /v1/generate/rsa-pair` | 🔑 |
|
|
83
|
+
| `client.generate_ntt(bits=)` | `GET /v1/generate/ntt` | 🔑 |
|
|
84
|
+
| `client.generate_fhe(bits=)` | `GET /v1/generate/fhe` | 🔑 Starter+ |
|
|
85
|
+
| `client.generate_ecc(bits=)` | `GET /v1/generate/ecc` | 🔑 |
|
|
86
|
+
| `client.verify(n)` | `GET /v1/verify` | public |
|
|
87
|
+
| `client.optimize_next(prime=, gap=)` | `GET /v1/optimize/next` | public |
|
|
88
|
+
| `client.predict_gap(...)` | `GET /v1/predict/gap` | public |
|
|
89
|
+
| `client.account()` | `GET /v1/account` | 🔑 |
|
|
90
|
+
| `client.usage_history()` | `GET /v1/usage/history` | 🔑 |
|
|
91
|
+
| `client.status()` | `GET /v1/status` | public |
|
|
92
|
+
|
|
93
|
+
Every method returns the parsed JSON response as a `dict`.
|
|
94
|
+
|
|
95
|
+
## Errors
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from ethoryx import EthoryxError, AuthError, RateLimitError, APIError
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
client.generate(bits=2048)
|
|
102
|
+
except AuthError:
|
|
103
|
+
... # missing/invalid key, or tier too low (401/403)
|
|
104
|
+
except RateLimitError:
|
|
105
|
+
... # 429 — back off and retry
|
|
106
|
+
except APIError as e:
|
|
107
|
+
print(e.status, e.response)
|
|
108
|
+
except EthoryxError:
|
|
109
|
+
... # network/other
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
MIT © Ethoryx Research
|
ethoryx-0.1.0/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# ethoryx
|
|
2
|
+
|
|
3
|
+
Official Python client for the [Ethoryx](https://ethoryx.io) prime-generation API —
|
|
4
|
+
structurally-guided primes for **RSA, FHE, zk-SNARKs, and NTT**, backed by the Ethoryx
|
|
5
|
+
C/GMP core. Fewer Miller–Rabin tests, fewer candidates, same cryptographic strength.
|
|
6
|
+
|
|
7
|
+
- API: `https://api.ethoryx.io` · [Docs](https://ethoryx.io/docs) · [Status](https://api.ethoryx.io/v1/status)
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install ethoryx
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
import ethoryx
|
|
19
|
+
|
|
20
|
+
client = ethoryx.Client(api_key="tg_your_key") # or set ETHORYX_API_KEY
|
|
21
|
+
|
|
22
|
+
# Generate primes
|
|
23
|
+
prime = client.generate(bits=2048)
|
|
24
|
+
rsa = client.generate_rsa(bits=2048)
|
|
25
|
+
pair = client.generate_rsa_pair(bits=2048) # matched (p, q)
|
|
26
|
+
ntt = client.generate_ntt(bits=256)
|
|
27
|
+
fhe = client.generate_fhe(bits=2048) # Starter tier or above
|
|
28
|
+
|
|
29
|
+
# Public endpoints — no API key required
|
|
30
|
+
ethoryx.verify(999983) # {"is_prime": true, ...}
|
|
31
|
+
ethoryx.optimize_next(prime=999983, gap=6) # next-prime prediction
|
|
32
|
+
ethoryx.status() # version, benchmarks
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Get a free key (100 calls/month) at <https://ethoryx.io/register>.
|
|
36
|
+
|
|
37
|
+
## Authentication
|
|
38
|
+
|
|
39
|
+
Pass your key to the client, or set it once in the environment:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
export ETHORYX_API_KEY="tg_your_key"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
client = ethoryx.Client() # picks up ETHORYX_API_KEY
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The public endpoints (`verify`, `optimize_next`, `predict_gap`, `status`) work without a
|
|
50
|
+
key. Everything under `generate*` and `account*` requires one.
|
|
51
|
+
|
|
52
|
+
## API
|
|
53
|
+
|
|
54
|
+
| Method | Endpoint | Auth |
|
|
55
|
+
|--------|----------|------|
|
|
56
|
+
| `client.generate(bits=, method=)` | `GET /v1/generate` | 🔑 |
|
|
57
|
+
| `client.generate_rsa(bits=)` | `GET /v1/generate/rsa` | 🔑 |
|
|
58
|
+
| `client.generate_rsa_pair(bits=)` | `GET /v1/generate/rsa-pair` | 🔑 |
|
|
59
|
+
| `client.generate_ntt(bits=)` | `GET /v1/generate/ntt` | 🔑 |
|
|
60
|
+
| `client.generate_fhe(bits=)` | `GET /v1/generate/fhe` | 🔑 Starter+ |
|
|
61
|
+
| `client.generate_ecc(bits=)` | `GET /v1/generate/ecc` | 🔑 |
|
|
62
|
+
| `client.verify(n)` | `GET /v1/verify` | public |
|
|
63
|
+
| `client.optimize_next(prime=, gap=)` | `GET /v1/optimize/next` | public |
|
|
64
|
+
| `client.predict_gap(...)` | `GET /v1/predict/gap` | public |
|
|
65
|
+
| `client.account()` | `GET /v1/account` | 🔑 |
|
|
66
|
+
| `client.usage_history()` | `GET /v1/usage/history` | 🔑 |
|
|
67
|
+
| `client.status()` | `GET /v1/status` | public |
|
|
68
|
+
|
|
69
|
+
Every method returns the parsed JSON response as a `dict`.
|
|
70
|
+
|
|
71
|
+
## Errors
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from ethoryx import EthoryxError, AuthError, RateLimitError, APIError
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
client.generate(bits=2048)
|
|
78
|
+
except AuthError:
|
|
79
|
+
... # missing/invalid key, or tier too low (401/403)
|
|
80
|
+
except RateLimitError:
|
|
81
|
+
... # 429 — back off and retry
|
|
82
|
+
except APIError as e:
|
|
83
|
+
print(e.status, e.response)
|
|
84
|
+
except EthoryxError:
|
|
85
|
+
... # network/other
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## License
|
|
89
|
+
|
|
90
|
+
MIT © Ethoryx Research
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ethoryx"
|
|
7
|
+
description = "Official Python client for the Ethoryx prime-generation API (RSA, FHE, zk-SNARK, and NTT primes)."
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.8"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
authors = [{ name = "Ethoryx Research", email = "research@ethoryx.io" }]
|
|
12
|
+
keywords = ["ethoryx", "prime", "rsa", "fhe", "zk-snark", "cryptography", "ntt", "number-theory", "primes"]
|
|
13
|
+
dependencies = ["requests>=2.20"]
|
|
14
|
+
dynamic = ["version"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
22
|
+
"Topic :: Security :: Cryptography",
|
|
23
|
+
"Topic :: Scientific/Engineering :: Mathematics",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://ethoryx.io"
|
|
28
|
+
Documentation = "https://ethoryx.io/docs"
|
|
29
|
+
Source = "https://github.com/Teshgty/ethoryx-python"
|
|
30
|
+
"API Status" = "https://api.ethoryx.io/v1/status"
|
|
31
|
+
|
|
32
|
+
[tool.hatch.version]
|
|
33
|
+
path = "src/ethoryx/version.py"
|
|
34
|
+
|
|
35
|
+
[tool.hatch.build.targets.wheel]
|
|
36
|
+
packages = ["src/ethoryx"]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ethoryx — official Python client for the Ethoryx prime-generation API.
|
|
3
|
+
|
|
4
|
+
import ethoryx
|
|
5
|
+
|
|
6
|
+
client = ethoryx.Client(api_key="tg_...") # or set ETHORYX_API_KEY
|
|
7
|
+
prime = client.generate(bits=2048)
|
|
8
|
+
rsa = client.generate_rsa(bits=2048)
|
|
9
|
+
|
|
10
|
+
# Public endpoints need no key:
|
|
11
|
+
ethoryx.verify(999983)
|
|
12
|
+
ethoryx.status()
|
|
13
|
+
|
|
14
|
+
Docs: https://ethoryx.io/docs
|
|
15
|
+
"""
|
|
16
|
+
from .client import DEFAULT_BASE_URL, Client
|
|
17
|
+
from .errors import APIError, AuthError, EthoryxError, RateLimitError
|
|
18
|
+
from .version import __version__
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"Client",
|
|
22
|
+
"DEFAULT_BASE_URL",
|
|
23
|
+
"EthoryxError",
|
|
24
|
+
"APIError",
|
|
25
|
+
"AuthError",
|
|
26
|
+
"RateLimitError",
|
|
27
|
+
"status",
|
|
28
|
+
"verify",
|
|
29
|
+
"optimize_next",
|
|
30
|
+
"predict_gap",
|
|
31
|
+
"__version__",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _public():
|
|
36
|
+
"""A keyless client for the free/public endpoints."""
|
|
37
|
+
return Client(api_key=None)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def status():
|
|
41
|
+
"""API status, version, and benchmarks (public)."""
|
|
42
|
+
return _public().status()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def verify(n):
|
|
46
|
+
"""Check whether ``n`` is prime (public)."""
|
|
47
|
+
return _public().verify(n)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def optimize_next(prime=None, gap=None, **params):
|
|
51
|
+
"""Predict the next prime from the last prime + preceding gap (public)."""
|
|
52
|
+
return _public().optimize_next(prime=prime, gap=gap, **params)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def predict_gap(**params):
|
|
56
|
+
"""Predict the gap to the next prime (public)."""
|
|
57
|
+
return _public().predict_gap(**params)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ethoryx API client.
|
|
3
|
+
|
|
4
|
+
A thin, well-behaved wrapper over the Ethoryx prime-generation API
|
|
5
|
+
(https://api.ethoryx.io). Every method returns the parsed JSON response as a
|
|
6
|
+
dict; failures raise a typed :class:`~ethoryx.errors.EthoryxError`.
|
|
7
|
+
|
|
8
|
+
import ethoryx
|
|
9
|
+
client = ethoryx.Client(api_key="tg_...") # or set ETHORYX_API_KEY
|
|
10
|
+
prime = client.generate(bits=2048)
|
|
11
|
+
"""
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
import requests
|
|
15
|
+
|
|
16
|
+
from .errors import APIError, AuthError, EthoryxError, RateLimitError
|
|
17
|
+
from .version import __version__
|
|
18
|
+
|
|
19
|
+
DEFAULT_BASE_URL = "https://api.ethoryx.io"
|
|
20
|
+
|
|
21
|
+
__all__ = ["Client", "DEFAULT_BASE_URL"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Client:
|
|
25
|
+
"""Client for the Ethoryx API.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
api_key: Your ``tg_...`` API key. Falls back to the ``ETHORYX_API_KEY``
|
|
29
|
+
environment variable. Not required for the public endpoints
|
|
30
|
+
(:meth:`verify`, :meth:`optimize_next`, :meth:`predict_gap`,
|
|
31
|
+
:meth:`status`).
|
|
32
|
+
base_url: Override the API host (default ``https://api.ethoryx.io``).
|
|
33
|
+
timeout: Per-request timeout in seconds (default 30).
|
|
34
|
+
session: An optional pre-configured ``requests.Session``.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, api_key=None, base_url=DEFAULT_BASE_URL, timeout=30, session=None):
|
|
38
|
+
self.api_key = api_key or os.environ.get("ETHORYX_API_KEY")
|
|
39
|
+
self.base_url = base_url.rstrip("/")
|
|
40
|
+
self.timeout = timeout
|
|
41
|
+
self._session = session or requests.Session()
|
|
42
|
+
self._session.headers.setdefault("User-Agent", "ethoryx-python/%s" % __version__)
|
|
43
|
+
|
|
44
|
+
# ── transport ────────────────────────────────────────────────────────────
|
|
45
|
+
def _request(self, method, path, params=None, json=None, auth=True):
|
|
46
|
+
headers = {}
|
|
47
|
+
if self.api_key: # always send the key when we have one
|
|
48
|
+
headers["X-API-Key"] = self.api_key
|
|
49
|
+
elif auth: # required but absent → fail clearly
|
|
50
|
+
raise AuthError(
|
|
51
|
+
"No API key. Pass api_key= or set ETHORYX_API_KEY. "
|
|
52
|
+
"Get one at https://ethoryx.io/register"
|
|
53
|
+
)
|
|
54
|
+
try:
|
|
55
|
+
resp = self._session.request(
|
|
56
|
+
method, self.base_url + path, params=params, json=json,
|
|
57
|
+
headers=headers, timeout=self.timeout,
|
|
58
|
+
)
|
|
59
|
+
except requests.RequestException as exc:
|
|
60
|
+
raise EthoryxError("request to %s failed: %s" % (path, exc)) from exc
|
|
61
|
+
return self._handle(resp)
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
def _handle(resp):
|
|
65
|
+
try:
|
|
66
|
+
data = resp.json()
|
|
67
|
+
except ValueError:
|
|
68
|
+
data = {"error": resp.text}
|
|
69
|
+
if resp.status_code >= 400:
|
|
70
|
+
msg = (data.get("error") or data.get("message") or resp.reason
|
|
71
|
+
if isinstance(data, dict) else resp.reason)
|
|
72
|
+
if resp.status_code in (401, 403):
|
|
73
|
+
raise AuthError(msg, status=resp.status_code, response=data)
|
|
74
|
+
if resp.status_code == 429:
|
|
75
|
+
raise RateLimitError(msg, status=resp.status_code, response=data)
|
|
76
|
+
raise APIError(msg, status=resp.status_code, response=data)
|
|
77
|
+
return data
|
|
78
|
+
|
|
79
|
+
def _get(self, path, params=None, auth=True):
|
|
80
|
+
return self._request("GET", path, params=params, auth=auth)
|
|
81
|
+
|
|
82
|
+
def _post(self, path, json=None, auth=True):
|
|
83
|
+
return self._request("POST", path, json=json, auth=auth)
|
|
84
|
+
|
|
85
|
+
# ── prime generation (X-API-Key required) ────────────────────────────────
|
|
86
|
+
def generate(self, bits=2048, method=None, **params):
|
|
87
|
+
"""Generate a prime of ``bits`` bits. ``GET /v1/generate``."""
|
|
88
|
+
q = {"bits": bits, **params}
|
|
89
|
+
if method is not None:
|
|
90
|
+
q["method"] = method
|
|
91
|
+
return self._get("/v1/generate", q)
|
|
92
|
+
|
|
93
|
+
def generate_rsa(self, bits=2048, **params):
|
|
94
|
+
"""Generate an RSA-suitable prime. ``GET /v1/generate/rsa``."""
|
|
95
|
+
return self._get("/v1/generate/rsa", {"bits": bits, **params})
|
|
96
|
+
|
|
97
|
+
def generate_rsa_pair(self, bits=2048, **params):
|
|
98
|
+
"""Generate a matched RSA prime pair (p, q). ``GET /v1/generate/rsa-pair``."""
|
|
99
|
+
return self._get("/v1/generate/rsa-pair", {"bits": bits, **params})
|
|
100
|
+
|
|
101
|
+
def generate_ntt(self, bits=256, **params):
|
|
102
|
+
"""Generate an NTT-friendly prime. ``GET /v1/generate/ntt``."""
|
|
103
|
+
return self._get("/v1/generate/ntt", {"bits": bits, **params})
|
|
104
|
+
|
|
105
|
+
def generate_fhe(self, bits=None, **params):
|
|
106
|
+
"""Generate an FHE-suitable prime. ``GET /v1/generate/fhe`` (Starter tier+)."""
|
|
107
|
+
q = dict(params)
|
|
108
|
+
if bits is not None:
|
|
109
|
+
q["bits"] = bits
|
|
110
|
+
return self._get("/v1/generate/fhe", q)
|
|
111
|
+
|
|
112
|
+
def generate_ecc(self, bits=None, **params):
|
|
113
|
+
"""Generate an ECC-suitable prime. ``GET /v1/generate/ecc``."""
|
|
114
|
+
q = dict(params)
|
|
115
|
+
if bits is not None:
|
|
116
|
+
q["bits"] = bits
|
|
117
|
+
return self._get("/v1/generate/ecc", q)
|
|
118
|
+
|
|
119
|
+
# ── optimization / prediction (free, public) ─────────────────────────────
|
|
120
|
+
def optimize_next(self, prime=None, gap=None, **params):
|
|
121
|
+
"""Predict the next prime from the last prime + preceding gap.
|
|
122
|
+
``GET /v1/optimize/next`` — free and public."""
|
|
123
|
+
q = dict(params)
|
|
124
|
+
if prime is not None:
|
|
125
|
+
q["prime"] = prime
|
|
126
|
+
if gap is not None:
|
|
127
|
+
q["gap"] = gap
|
|
128
|
+
return self._get("/v1/optimize/next", q, auth=False)
|
|
129
|
+
|
|
130
|
+
def predict_gap(self, **params):
|
|
131
|
+
"""Predict the gap to the next prime. ``GET /v1/predict/gap``."""
|
|
132
|
+
return self._get("/v1/predict/gap", params, auth=False)
|
|
133
|
+
|
|
134
|
+
# ── verification (public) ────────────────────────────────────────────────
|
|
135
|
+
def verify(self, n):
|
|
136
|
+
"""Check whether ``n`` is prime. ``GET /v1/verify`` — public, no key."""
|
|
137
|
+
return self._get("/v1/verify", {"n": str(n)}, auth=False)
|
|
138
|
+
|
|
139
|
+
# ── account ──────────────────────────────────────────────────────────────
|
|
140
|
+
def account(self):
|
|
141
|
+
"""Your account, tier, and quota. ``GET /v1/account``."""
|
|
142
|
+
return self._get("/v1/account")
|
|
143
|
+
|
|
144
|
+
def account_profile(self):
|
|
145
|
+
"""Account profile details. ``GET /v1/account/profile``."""
|
|
146
|
+
return self._get("/v1/account/profile")
|
|
147
|
+
|
|
148
|
+
def usage_history(self):
|
|
149
|
+
"""Your recent API usage. ``GET /v1/usage/history``."""
|
|
150
|
+
return self._get("/v1/usage/history")
|
|
151
|
+
|
|
152
|
+
# ── status (public) ──────────────────────────────────────────────────────
|
|
153
|
+
def status(self):
|
|
154
|
+
"""API status, version, and benchmarks. ``GET /v1/status`` — public."""
|
|
155
|
+
return self._get("/v1/status", auth=False)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Exceptions raised by the Ethoryx client."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class EthoryxError(Exception):
|
|
5
|
+
"""Base class for all Ethoryx client errors."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, message, status=None, response=None):
|
|
8
|
+
super().__init__(message)
|
|
9
|
+
self.status = status # HTTP status code, when available
|
|
10
|
+
self.response = response # parsed JSON body, when available
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class APIError(EthoryxError):
|
|
14
|
+
"""The API returned a 4xx/5xx that isn't auth or rate-limit related."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AuthError(EthoryxError):
|
|
18
|
+
"""Missing/invalid API key, or the endpoint needs a higher tier (401/403)."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class RateLimitError(EthoryxError):
|
|
22
|
+
"""Too many requests (429). Back off and retry."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Offline tests — a fake session verifies URL/params/headers without any network."""
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
import ethoryx
|
|
7
|
+
from ethoryx import AuthError, RateLimitError, APIError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class _Resp:
|
|
11
|
+
def __init__(self, status=200, body=None, text=""):
|
|
12
|
+
self.status_code = status
|
|
13
|
+
self._body = body
|
|
14
|
+
self.text = text
|
|
15
|
+
self.reason = "err"
|
|
16
|
+
|
|
17
|
+
def json(self):
|
|
18
|
+
if self._body is None:
|
|
19
|
+
raise ValueError("no json")
|
|
20
|
+
return self._body
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _FakeSession:
|
|
24
|
+
"""Records the last request and returns a queued response."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, resp):
|
|
27
|
+
self.headers = {}
|
|
28
|
+
self.resp = resp
|
|
29
|
+
self.calls = []
|
|
30
|
+
|
|
31
|
+
def request(self, method, url, params=None, json=None, headers=None, timeout=None):
|
|
32
|
+
self.calls.append(dict(method=method, url=url, params=params,
|
|
33
|
+
json=json, headers=headers))
|
|
34
|
+
return self.resp
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _client(resp, **kw):
|
|
38
|
+
sess = _FakeSession(resp)
|
|
39
|
+
c = ethoryx.Client(session=sess, **kw)
|
|
40
|
+
return c, sess
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_generate_builds_url_and_sends_key():
|
|
44
|
+
c, s = _client(_Resp(body={"prime": "123"}), api_key="tg_test")
|
|
45
|
+
out = c.generate(bits=2048, method="standard")
|
|
46
|
+
assert out == {"prime": "123"}
|
|
47
|
+
call = s.calls[-1]
|
|
48
|
+
assert call["method"] == "GET"
|
|
49
|
+
assert call["url"].endswith("/v1/generate")
|
|
50
|
+
assert call["params"] == {"bits": 2048, "method": "standard"}
|
|
51
|
+
assert call["headers"]["X-API-Key"] == "tg_test"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_authed_endpoint_without_key_raises():
|
|
55
|
+
c, _ = _client(_Resp(body={})) # no api_key
|
|
56
|
+
with pytest.raises(AuthError):
|
|
57
|
+
c.generate(bits=512)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_public_endpoint_needs_no_key():
|
|
61
|
+
c, s = _client(_Resp(body={"is_prime": True}))
|
|
62
|
+
assert c.verify(999983) == {"is_prime": True}
|
|
63
|
+
assert s.calls[-1]["params"] == {"n": "999983"}
|
|
64
|
+
assert "X-API-Key" not in s.calls[-1]["headers"]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_error_mapping():
|
|
68
|
+
c, _ = _client(_Resp(status=429, body={"error": "slow down"}), api_key="tg")
|
|
69
|
+
with pytest.raises(RateLimitError):
|
|
70
|
+
c.generate()
|
|
71
|
+
|
|
72
|
+
c2, _ = _client(_Resp(status=403, body={"error": "tier too low"}), api_key="tg")
|
|
73
|
+
with pytest.raises(AuthError):
|
|
74
|
+
c2.generate_fhe(bits=2048)
|
|
75
|
+
|
|
76
|
+
c3, _ = _client(_Resp(status=500, body={"error": "boom"}), api_key="tg")
|
|
77
|
+
with pytest.raises(APIError):
|
|
78
|
+
c3.generate()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_module_version():
|
|
82
|
+
assert isinstance(ethoryx.__version__, str) and ethoryx.__version__
|