alea-q 0.2.0__py3-none-any.whl
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.
Potentially problematic release.
This version of alea-q might be problematic. Click here for more details.
- alea_q/__init__.py +83 -0
- alea_q/_http.py +220 -0
- alea_q/client.py +181 -0
- alea_q/exceptions.py +120 -0
- alea_q/models.py +412 -0
- alea_q/pqc_crypto.py +143 -0
- alea_q/tiers/__init__.py +0 -0
- alea_q/tiers/ivory.py +98 -0
- alea_q/tiers/platinum.py +129 -0
- alea_q/tiers/pqc.py +273 -0
- alea_q/tiers/silver.py +216 -0
- alea_q/verify.py +200 -0
- alea_q-0.2.0.dist-info/METADATA +349 -0
- alea_q-0.2.0.dist-info/RECORD +15 -0
- alea_q-0.2.0.dist-info/WHEEL +4 -0
alea_q/__init__.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# alea_q/__init__.py
|
|
2
|
+
"""
|
|
3
|
+
alea-q — SDK Python pour l'API ALEA-Q / CERTROPI.
|
|
4
|
+
|
|
5
|
+
Génération d'entiers aléatoires certifiés Device-Independent
|
|
6
|
+
depuis des QPU réels (IBM, Quandela, Pasqal).
|
|
7
|
+
|
|
8
|
+
Usage rapide :
|
|
9
|
+
from alea_q import AleaQClient
|
|
10
|
+
|
|
11
|
+
client = AleaQClient(api_key="sk_plat_xxx")
|
|
12
|
+
result = client.platinum.generate(n_bits=256)
|
|
13
|
+
print(result.as_hex())
|
|
14
|
+
|
|
15
|
+
Documentation : https://docs.alea-q.com
|
|
16
|
+
"""
|
|
17
|
+
from alea_q.client import AleaQClient, AsyncAleaQClient
|
|
18
|
+
from alea_q.exceptions import (
|
|
19
|
+
AleaQError,
|
|
20
|
+
AuthenticationError,
|
|
21
|
+
BackendUnavailable,
|
|
22
|
+
CertificateError,
|
|
23
|
+
InsufficientBalance,
|
|
24
|
+
PermissionError,
|
|
25
|
+
QuotaExceeded,
|
|
26
|
+
RateLimitError,
|
|
27
|
+
ServerError,
|
|
28
|
+
ValidationError,
|
|
29
|
+
)
|
|
30
|
+
from alea_q.models import (
|
|
31
|
+
AccountInfo,
|
|
32
|
+
BatchResult,
|
|
33
|
+
GenerateResult,
|
|
34
|
+
HealthStatus,
|
|
35
|
+
MLKemEncapsResult,
|
|
36
|
+
MLKemKeyPair,
|
|
37
|
+
PoolStatus,
|
|
38
|
+
QuotaInfo,
|
|
39
|
+
SLHDSAKeyPair,
|
|
40
|
+
SLHDSASignResult,
|
|
41
|
+
SLHDSAVerifyResult,
|
|
42
|
+
UsageStats,
|
|
43
|
+
)
|
|
44
|
+
from alea_q.pqc_crypto import DecapsulationError, mlkem_decaps
|
|
45
|
+
from alea_q.verify import verify_certificate, verify_certificate_dict
|
|
46
|
+
|
|
47
|
+
__version__ = "0.2.0"
|
|
48
|
+
__all__ = [
|
|
49
|
+
# Clients
|
|
50
|
+
"AleaQClient",
|
|
51
|
+
"AsyncAleaQClient",
|
|
52
|
+
# Modèles
|
|
53
|
+
"GenerateResult",
|
|
54
|
+
"BatchResult",
|
|
55
|
+
"PoolStatus",
|
|
56
|
+
"HealthStatus",
|
|
57
|
+
"AccountInfo",
|
|
58
|
+
"QuotaInfo",
|
|
59
|
+
"UsageStats",
|
|
60
|
+
"MLKemKeyPair",
|
|
61
|
+
"MLKemEncapsResult",
|
|
62
|
+
"SLHDSAKeyPair",
|
|
63
|
+
"SLHDSASignResult",
|
|
64
|
+
"SLHDSAVerifyResult",
|
|
65
|
+
# Exceptions
|
|
66
|
+
"AleaQError",
|
|
67
|
+
"AuthenticationError",
|
|
68
|
+
"PermissionError",
|
|
69
|
+
"QuotaExceeded",
|
|
70
|
+
"BackendUnavailable",
|
|
71
|
+
"RateLimitError",
|
|
72
|
+
"ServerError",
|
|
73
|
+
"CertificateError",
|
|
74
|
+
"ValidationError",
|
|
75
|
+
"InsufficientBalance",
|
|
76
|
+
# Vérification certificat
|
|
77
|
+
"verify_certificate",
|
|
78
|
+
"verify_certificate_dict",
|
|
79
|
+
# Décapsulation ML-KEM locale (#438)
|
|
80
|
+
"mlkem_decaps",
|
|
81
|
+
"DecapsulationError",
|
|
82
|
+
]
|
|
83
|
+
|
alea_q/_http.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# alea_q/_http.py
|
|
2
|
+
"""
|
|
3
|
+
Couche HTTP du SDK alea-q — gestion centralisée des requêtes et des erreurs.
|
|
4
|
+
|
|
5
|
+
Utilisé par AleaQClient (sync) et AsyncAleaQClient (async).
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from typing import Any, Dict, Optional
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
|
|
14
|
+
from alea_q.exceptions import (
|
|
15
|
+
AleaQError,
|
|
16
|
+
AuthenticationError,
|
|
17
|
+
BackendUnavailable,
|
|
18
|
+
InsufficientBalance,
|
|
19
|
+
PermissionError,
|
|
20
|
+
QuotaExceeded,
|
|
21
|
+
RateLimitError,
|
|
22
|
+
ServerError,
|
|
23
|
+
ValidationError,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Version du SDK — mise à jour à chaque release
|
|
27
|
+
SDK_VERSION = "0.2.0"
|
|
28
|
+
|
|
29
|
+
DEFAULT_BASE_URL = os.getenv("ALEA_Q_BASE_URL", "https://api.alea-q.com")
|
|
30
|
+
DEFAULT_TIMEOUT = float(os.getenv("ALEA_Q_TIMEOUT", "60"))
|
|
31
|
+
DEFAULT_RETRIES = int(os.getenv("ALEA_Q_MAX_RETRIES", "2"))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _resolve_api_key(api_key: Optional[str]) -> str:
|
|
35
|
+
"""
|
|
36
|
+
Résout la clé API depuis le paramètre ou la variable d'environnement.
|
|
37
|
+
Lève ValidationError si aucune clé n'est disponible.
|
|
38
|
+
"""
|
|
39
|
+
key = api_key or os.getenv("ALEA_Q_API_KEY", "")
|
|
40
|
+
if not key:
|
|
41
|
+
raise ValidationError(
|
|
42
|
+
"Clé API manquante. Passez api_key= au constructeur ou "
|
|
43
|
+
"définissez la variable d'environnement ALEA_Q_API_KEY."
|
|
44
|
+
)
|
|
45
|
+
return key
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# Préfixes réels des clés API (api/auth.py côté serveur) — sk_ivor_, pas
|
|
49
|
+
# sk_ivory_ (#116, faute de frappe corrigée dans la doc du SDK au passage).
|
|
50
|
+
_TIER_PREFIXES = {
|
|
51
|
+
"sk_ivor_": "ivory",
|
|
52
|
+
"sk_silv_": "silver",
|
|
53
|
+
"sk_plat_": "platinum",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _detect_tier(api_key: str) -> Optional[str]:
|
|
58
|
+
"""
|
|
59
|
+
Détecte le tier ('ivory' | 'silver' | 'platinum') depuis le préfixe
|
|
60
|
+
de la clé API (#116) — best-effort, informationnel.
|
|
61
|
+
|
|
62
|
+
Retourne None si le préfixe n'est pas reconnu, plutôt que de lever :
|
|
63
|
+
la validation qui compte réellement se fait côté serveur (401 sur
|
|
64
|
+
une clé invalide, via _raise_for_status ci-dessous) — bloquer la
|
|
65
|
+
construction du client sur un préfixe inconnu casserait des clés
|
|
66
|
+
de test/staging légitimes qui ne suivent pas cette convention,
|
|
67
|
+
pour une garantie que le serveur apporte de toute façon.
|
|
68
|
+
"""
|
|
69
|
+
for prefix, tier in _TIER_PREFIXES.items():
|
|
70
|
+
if api_key.startswith(prefix):
|
|
71
|
+
return tier
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _build_headers(api_key: str) -> Dict[str, str]:
|
|
76
|
+
return {
|
|
77
|
+
"X-API-Key": api_key,
|
|
78
|
+
"User-Agent": f"alea-q-python/{SDK_VERSION}",
|
|
79
|
+
"Accept": "application/json",
|
|
80
|
+
"Content-Type": "application/json",
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _raise_for_status(response: httpx.Response) -> None:
|
|
85
|
+
"""
|
|
86
|
+
Convertit les erreurs HTTP en exceptions typées alea-q.
|
|
87
|
+
Extrait request_id et retry_after si disponibles.
|
|
88
|
+
"""
|
|
89
|
+
if response.status_code < 400:
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
request_id = response.headers.get("x-request-id")
|
|
93
|
+
retry_after_raw = response.headers.get("retry-after")
|
|
94
|
+
retry_after = int(retry_after_raw) if retry_after_raw and retry_after_raw.isdigit() else None
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
body = response.json()
|
|
98
|
+
detail = body.get("detail", str(response.status_code))
|
|
99
|
+
reason = body.get("reason")
|
|
100
|
+
except Exception:
|
|
101
|
+
detail = response.text or str(response.status_code)
|
|
102
|
+
reason = None
|
|
103
|
+
|
|
104
|
+
sc = response.status_code
|
|
105
|
+
|
|
106
|
+
if sc == 401:
|
|
107
|
+
raise AuthenticationError(detail, status_code=sc, request_id=request_id)
|
|
108
|
+
if sc == 403:
|
|
109
|
+
raise PermissionError(detail, status_code=sc, request_id=request_id)
|
|
110
|
+
if sc == 422:
|
|
111
|
+
raise ValidationError(detail, status_code=sc, request_id=request_id)
|
|
112
|
+
if sc == 402:
|
|
113
|
+
raise InsufficientBalance(detail, status_code=sc, request_id=request_id)
|
|
114
|
+
if sc == 429:
|
|
115
|
+
# Distinguer rate-limit (Caddy) et quota (applicatif)
|
|
116
|
+
if reason or "quota" in detail.lower():
|
|
117
|
+
raise QuotaExceeded(
|
|
118
|
+
detail, retry_after=retry_after,
|
|
119
|
+
status_code=sc, request_id=request_id,
|
|
120
|
+
)
|
|
121
|
+
raise RateLimitError(
|
|
122
|
+
detail, retry_after=retry_after,
|
|
123
|
+
status_code=sc, request_id=request_id,
|
|
124
|
+
)
|
|
125
|
+
if sc == 503:
|
|
126
|
+
raise BackendUnavailable(
|
|
127
|
+
detail, retry_after=retry_after, reason=reason,
|
|
128
|
+
status_code=sc, request_id=request_id,
|
|
129
|
+
)
|
|
130
|
+
if sc >= 500:
|
|
131
|
+
raise ServerError(detail, status_code=sc, request_id=request_id)
|
|
132
|
+
|
|
133
|
+
raise AleaQError(detail, status_code=sc, request_id=request_id)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ── Client synchrone ──────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
class HttpClient:
|
|
139
|
+
"""Client HTTP synchrone (httpx)."""
|
|
140
|
+
|
|
141
|
+
def __init__(
|
|
142
|
+
self,
|
|
143
|
+
api_key: str,
|
|
144
|
+
base_url: str,
|
|
145
|
+
timeout: float,
|
|
146
|
+
) -> None:
|
|
147
|
+
self._headers = _build_headers(api_key)
|
|
148
|
+
self._client = httpx.Client(
|
|
149
|
+
base_url = base_url,
|
|
150
|
+
headers = self._headers,
|
|
151
|
+
timeout = timeout,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def post(self, path: str, json: Dict[str, Any]) -> Dict[str, Any]:
|
|
155
|
+
response = self._client.post(path, json=json)
|
|
156
|
+
_raise_for_status(response)
|
|
157
|
+
return response.json()
|
|
158
|
+
|
|
159
|
+
def get(self, path: str, params: Optional[Dict] = None) -> Dict[str, Any]:
|
|
160
|
+
response = self._client.get(path, params=params)
|
|
161
|
+
_raise_for_status(response)
|
|
162
|
+
return response.json()
|
|
163
|
+
|
|
164
|
+
def get_bytes(self, path: str) -> bytes:
|
|
165
|
+
response = self._client.get(path)
|
|
166
|
+
_raise_for_status(response)
|
|
167
|
+
return response.content
|
|
168
|
+
|
|
169
|
+
def close(self) -> None:
|
|
170
|
+
self._client.close()
|
|
171
|
+
|
|
172
|
+
def __enter__(self) -> "HttpClient":
|
|
173
|
+
return self
|
|
174
|
+
|
|
175
|
+
def __exit__(self, *args) -> None:
|
|
176
|
+
self.close()
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# ── Client asynchrone ─────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
class AsyncHttpClient:
|
|
182
|
+
"""Client HTTP asynchrone (httpx.AsyncClient)."""
|
|
183
|
+
|
|
184
|
+
def __init__(
|
|
185
|
+
self,
|
|
186
|
+
api_key: str,
|
|
187
|
+
base_url: str,
|
|
188
|
+
timeout: float,
|
|
189
|
+
) -> None:
|
|
190
|
+
self._headers = _build_headers(api_key)
|
|
191
|
+
self._client = httpx.AsyncClient(
|
|
192
|
+
base_url = base_url,
|
|
193
|
+
headers = self._headers,
|
|
194
|
+
timeout = timeout,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
async def post(self, path: str, json: Dict[str, Any]) -> Dict[str, Any]:
|
|
198
|
+
response = await self._client.post(path, json=json)
|
|
199
|
+
_raise_for_status(response)
|
|
200
|
+
return response.json()
|
|
201
|
+
|
|
202
|
+
async def get(self, path: str, params: Optional[Dict] = None) -> Dict[str, Any]:
|
|
203
|
+
response = await self._client.get(path, params=params)
|
|
204
|
+
_raise_for_status(response)
|
|
205
|
+
return response.json()
|
|
206
|
+
|
|
207
|
+
async def get_bytes(self, path: str) -> bytes:
|
|
208
|
+
response = await self._client.get(path)
|
|
209
|
+
_raise_for_status(response)
|
|
210
|
+
return response.content
|
|
211
|
+
|
|
212
|
+
async def close(self) -> None:
|
|
213
|
+
await self._client.aclose()
|
|
214
|
+
|
|
215
|
+
async def __aenter__(self) -> "AsyncHttpClient":
|
|
216
|
+
return self
|
|
217
|
+
|
|
218
|
+
async def __aexit__(self, *args) -> None:
|
|
219
|
+
await self.close()
|
|
220
|
+
|
alea_q/client.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# alea_q/client.py
|
|
2
|
+
"""
|
|
3
|
+
Clients principaux du SDK alea-q.
|
|
4
|
+
|
|
5
|
+
Usage sync :
|
|
6
|
+
from alea_q import AleaQClient
|
|
7
|
+
|
|
8
|
+
client = AleaQClient(api_key="sk_plat_xxx")
|
|
9
|
+
|
|
10
|
+
# Platinum
|
|
11
|
+
result = client.platinum.generate(n_bits=256)
|
|
12
|
+
print(result.as_hex())
|
|
13
|
+
|
|
14
|
+
# Silver
|
|
15
|
+
result = client.silver.generate(n_bits=128)
|
|
16
|
+
|
|
17
|
+
# Ivory batch
|
|
18
|
+
batch = client.ivory.batch(size=1000, n_bits=32)
|
|
19
|
+
|
|
20
|
+
# Vérification certificat
|
|
21
|
+
client.platinum.generate(n_bits=256, verify=True, pubkey_path="certropi_public.pem")
|
|
22
|
+
|
|
23
|
+
# Context manager
|
|
24
|
+
with AleaQClient() as client:
|
|
25
|
+
result = client.platinum.generate()
|
|
26
|
+
|
|
27
|
+
Usage async :
|
|
28
|
+
from alea_q import AsyncAleaQClient
|
|
29
|
+
|
|
30
|
+
async with AsyncAleaQClient(api_key="sk_plat_xxx") as client:
|
|
31
|
+
result = await client.platinum.generate(n_bits=256)
|
|
32
|
+
"""
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
from typing import Optional
|
|
36
|
+
|
|
37
|
+
from alea_q._http import (
|
|
38
|
+
DEFAULT_BASE_URL,
|
|
39
|
+
DEFAULT_TIMEOUT,
|
|
40
|
+
AsyncHttpClient,
|
|
41
|
+
HttpClient,
|
|
42
|
+
_detect_tier,
|
|
43
|
+
_resolve_api_key,
|
|
44
|
+
)
|
|
45
|
+
from alea_q.models import AccountInfo, HealthStatus
|
|
46
|
+
from alea_q.tiers.ivory import AsyncIvoryTier, IvoryTier
|
|
47
|
+
from alea_q.tiers.platinum import AsyncPlatinumTier, PlatinumTier
|
|
48
|
+
from alea_q.tiers.silver import AsyncSilverTier, SilverTier
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class AleaQClient:
|
|
52
|
+
"""
|
|
53
|
+
Client synchrone ALEA-Q / CERTROPI.
|
|
54
|
+
|
|
55
|
+
Paramètres
|
|
56
|
+
----------
|
|
57
|
+
api_key : clé API (sk_plat_xxx, sk_silv_xxx, sk_ivor_xxx).
|
|
58
|
+
Si absent, lit ALEA_Q_API_KEY depuis l'environnement.
|
|
59
|
+
base_url : URL de base de l'API (défaut : https://api.alea-q.com).
|
|
60
|
+
Surcharger avec ALEA_Q_BASE_URL pour staging/dev.
|
|
61
|
+
timeout : timeout HTTP en secondes (défaut : 60).
|
|
62
|
+
|
|
63
|
+
Attributs
|
|
64
|
+
---------
|
|
65
|
+
tier : tier détecté depuis le préfixe de la clé (#116) —
|
|
66
|
+
'ivory' | 'silver' | 'platinum', ou None si le préfixe
|
|
67
|
+
n'est pas reconnu (informationnel ; la validation
|
|
68
|
+
réelle de la clé se fait côté serveur).
|
|
69
|
+
platinum : sous-client Platinum (génération DI-certifiée)
|
|
70
|
+
silver : sous-client Silver (DRBG seedé QPU)
|
|
71
|
+
ivory : sous-client Ivory (simulateur Toeplitz)
|
|
72
|
+
|
|
73
|
+
Exemple
|
|
74
|
+
-------
|
|
75
|
+
>>> client = AleaQClient(api_key="sk_plat_xxx")
|
|
76
|
+
>>> result = client.platinum.generate(n_bits=256)
|
|
77
|
+
>>> print(result.as_hex())
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
api_key: Optional[str] = None,
|
|
83
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
84
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
85
|
+
) -> None:
|
|
86
|
+
resolved_key = _resolve_api_key(api_key)
|
|
87
|
+
self.tier = _detect_tier(resolved_key)
|
|
88
|
+
self._http = HttpClient(
|
|
89
|
+
api_key = resolved_key,
|
|
90
|
+
base_url = base_url,
|
|
91
|
+
timeout = timeout,
|
|
92
|
+
)
|
|
93
|
+
self.platinum = PlatinumTier(self._http)
|
|
94
|
+
self.silver = SilverTier(self._http)
|
|
95
|
+
self.ivory = IvoryTier(self._http)
|
|
96
|
+
|
|
97
|
+
def health(self) -> HealthStatus:
|
|
98
|
+
"""
|
|
99
|
+
Retourne l'état du service ALEA-Q.
|
|
100
|
+
Requiert une clé admin pour la réponse complète.
|
|
101
|
+
"""
|
|
102
|
+
data = self._http.get("/health")
|
|
103
|
+
return HealthStatus.from_dict(data)
|
|
104
|
+
|
|
105
|
+
def me(self) -> AccountInfo:
|
|
106
|
+
"""
|
|
107
|
+
Informations du compte authentifié (#116) — tier, quota
|
|
108
|
+
journalier restant et statistiques d'usage cumulées.
|
|
109
|
+
"""
|
|
110
|
+
data = self._http.get("/v1/me")
|
|
111
|
+
return AccountInfo.from_dict(data)
|
|
112
|
+
|
|
113
|
+
def close(self) -> None:
|
|
114
|
+
"""Ferme le client HTTP et libère les ressources."""
|
|
115
|
+
self._http.close()
|
|
116
|
+
|
|
117
|
+
def __enter__(self) -> "AleaQClient":
|
|
118
|
+
return self
|
|
119
|
+
|
|
120
|
+
def __exit__(self, *args) -> None:
|
|
121
|
+
self.close()
|
|
122
|
+
|
|
123
|
+
def __repr__(self) -> str:
|
|
124
|
+
return f"AleaQClient(base_url={self._http._client.base_url!r})"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class AsyncAleaQClient:
|
|
128
|
+
"""
|
|
129
|
+
Client asynchrone ALEA-Q / CERTROPI.
|
|
130
|
+
|
|
131
|
+
Paramètres
|
|
132
|
+
----------
|
|
133
|
+
api_key : clé API. Si absent, lit ALEA_Q_API_KEY depuis l'environnement.
|
|
134
|
+
base_url : URL de base de l'API.
|
|
135
|
+
timeout : timeout HTTP en secondes.
|
|
136
|
+
|
|
137
|
+
Exemple
|
|
138
|
+
-------
|
|
139
|
+
>>> async with AsyncAleaQClient(api_key="sk_plat_xxx") as client:
|
|
140
|
+
... result = await client.platinum.generate(n_bits=256)
|
|
141
|
+
... print(result.as_hex())
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
def __init__(
|
|
145
|
+
self,
|
|
146
|
+
api_key: Optional[str] = None,
|
|
147
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
148
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
149
|
+
) -> None:
|
|
150
|
+
resolved_key = _resolve_api_key(api_key)
|
|
151
|
+
self.tier = _detect_tier(resolved_key)
|
|
152
|
+
self._http = AsyncHttpClient(
|
|
153
|
+
api_key = resolved_key,
|
|
154
|
+
base_url = base_url,
|
|
155
|
+
timeout = timeout,
|
|
156
|
+
)
|
|
157
|
+
self.platinum = AsyncPlatinumTier(self._http)
|
|
158
|
+
self.silver = AsyncSilverTier(self._http)
|
|
159
|
+
self.ivory = AsyncIvoryTier(self._http)
|
|
160
|
+
|
|
161
|
+
async def health(self) -> HealthStatus:
|
|
162
|
+
data = await self._http.get("/health")
|
|
163
|
+
return HealthStatus.from_dict(data)
|
|
164
|
+
|
|
165
|
+
async def me(self) -> AccountInfo:
|
|
166
|
+
"""Informations du compte authentifié (#116) — voir AleaQClient.me()."""
|
|
167
|
+
data = await self._http.get("/v1/me")
|
|
168
|
+
return AccountInfo.from_dict(data)
|
|
169
|
+
|
|
170
|
+
async def close(self) -> None:
|
|
171
|
+
await self._http.close()
|
|
172
|
+
|
|
173
|
+
async def __aenter__(self) -> "AsyncAleaQClient":
|
|
174
|
+
return self
|
|
175
|
+
|
|
176
|
+
async def __aexit__(self, *args) -> None:
|
|
177
|
+
await self.close()
|
|
178
|
+
|
|
179
|
+
def __repr__(self) -> str:
|
|
180
|
+
return f"AsyncAleaQClient(base_url={self._http._client.base_url!r})"
|
|
181
|
+
|
alea_q/exceptions.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# alea_q/exceptions.py
|
|
2
|
+
"""
|
|
3
|
+
Exceptions typées du SDK alea-q.
|
|
4
|
+
|
|
5
|
+
Hiérarchie :
|
|
6
|
+
AleaQError — base commune
|
|
7
|
+
AuthenticationError — clé API invalide ou absente (401)
|
|
8
|
+
PermissionError — tier insuffisant (403)
|
|
9
|
+
QuotaExceeded — quota de complexité dépassé (429)
|
|
10
|
+
BackendUnavailable — backend QPU indisponible (503)
|
|
11
|
+
RateLimitError — trop de requêtes (429 rate-limit)
|
|
12
|
+
ServerError — erreur interne serveur (500)
|
|
13
|
+
CertificateError — erreur de vérification d'attestation
|
|
14
|
+
ValidationError — paramètres invalides côté client
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import Optional
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AleaQError(Exception):
|
|
22
|
+
"""Exception de base du SDK alea-q."""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
message: str,
|
|
27
|
+
status_code: Optional[int] = None,
|
|
28
|
+
request_id: Optional[str] = None,
|
|
29
|
+
) -> None:
|
|
30
|
+
super().__init__(message)
|
|
31
|
+
self.status_code = status_code
|
|
32
|
+
self.request_id = request_id
|
|
33
|
+
|
|
34
|
+
def __repr__(self) -> str:
|
|
35
|
+
return (
|
|
36
|
+
f"{self.__class__.__name__}("
|
|
37
|
+
f"message={str(self)!r}, "
|
|
38
|
+
f"status_code={self.status_code}, "
|
|
39
|
+
f"request_id={self.request_id!r})"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class AuthenticationError(AleaQError):
|
|
44
|
+
"""Clé API invalide, expirée ou absente. (HTTP 401)"""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class PermissionError(AleaQError):
|
|
48
|
+
"""Tier de la clé insuffisant pour cette opération. (HTTP 403)"""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class QuotaExceeded(AleaQError):
|
|
52
|
+
"""Quota de complexité ou de requêtes dépassé. (HTTP 429)"""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
message: str,
|
|
57
|
+
retry_after: Optional[int] = None,
|
|
58
|
+
**kwargs,
|
|
59
|
+
) -> None:
|
|
60
|
+
super().__init__(message, **kwargs)
|
|
61
|
+
self.retry_after = retry_after
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class BackendUnavailable(AleaQError):
|
|
65
|
+
"""
|
|
66
|
+
Backend QPU temporairement indisponible — pool vide ou en maintenance.
|
|
67
|
+
(HTTP 503)
|
|
68
|
+
|
|
69
|
+
Attributs
|
|
70
|
+
---------
|
|
71
|
+
retry_after : secondes avant de réessayer (depuis l'en-tête Retry-After)
|
|
72
|
+
reason : cause technique ('pool_empty_no_backend', 'backend_unavailable', etc.)
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
message: str,
|
|
78
|
+
retry_after: Optional[int] = None,
|
|
79
|
+
reason: Optional[str] = None,
|
|
80
|
+
**kwargs,
|
|
81
|
+
) -> None:
|
|
82
|
+
super().__init__(message, **kwargs)
|
|
83
|
+
self.retry_after = retry_after
|
|
84
|
+
self.reason = reason
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class RateLimitError(AleaQError):
|
|
88
|
+
"""Trop de requêtes — rate-limit atteint. (HTTP 429)"""
|
|
89
|
+
|
|
90
|
+
def __init__(
|
|
91
|
+
self,
|
|
92
|
+
message: str,
|
|
93
|
+
retry_after: Optional[int] = None,
|
|
94
|
+
**kwargs,
|
|
95
|
+
) -> None:
|
|
96
|
+
super().__init__(message, **kwargs)
|
|
97
|
+
self.retry_after = retry_after
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class ServerError(AleaQError):
|
|
101
|
+
"""Erreur interne serveur ALEA-Q. (HTTP 500)"""
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class InsufficientBalance(AleaQError):
|
|
105
|
+
"""
|
|
106
|
+
Solde prépayé insuffisant pour honorer la requête. (HTTP 402)
|
|
107
|
+
|
|
108
|
+
#178/#179 — jamais câblé dans le SDK avant cette découverte en
|
|
109
|
+
travaillant sur le streaming Silver : le mode pay-as-you-go existe
|
|
110
|
+
côté API depuis #178 mais ne levait qu'une AleaQError générique.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class CertificateError(AleaQError):
|
|
115
|
+
"""Erreur lors de la vérification d'un certificat d'attestation Platinum."""
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class ValidationError(AleaQError):
|
|
119
|
+
"""Paramètres de requête invalides (vérification côté client avant envoi)."""
|
|
120
|
+
|