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.
@@ -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
@@ -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__