neomundi 2.0.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.
- neomundi/__init__.py +165 -0
- neomundi/_retry.py +114 -0
- neomundi/client.py +637 -0
- neomundi/errors.py +357 -0
- neomundi/models.py +1275 -0
- neomundi/py.typed +0 -0
- neomundi/streaming.py +501 -0
- neomundi-2.0.0.dist-info/METADATA +188 -0
- neomundi-2.0.0.dist-info/RECORD +13 -0
- neomundi-2.0.0.dist-info/WHEEL +5 -0
- neomundi-2.0.0.dist-info/licenses/LICENSE +201 -0
- neomundi-2.0.0.dist-info/licenses/LICENSE_NOTICE.md +17 -0
- neomundi-2.0.0.dist-info/top_level.txt +1 -0
neomundi/__init__.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""SDK Python NeoMundi — client de l'API de mesure du comportement des IA.
|
|
2
|
+
|
|
3
|
+
SDK v2 : REST post-call et proxy de génération SSE.
|
|
4
|
+
|
|
5
|
+
Principe directeur, qui explique chaque choix de ce paquet :
|
|
6
|
+
|
|
7
|
+
Le SDK transporte et expose les mesures. Il ne les interprète pas.
|
|
8
|
+
|
|
9
|
+
Concrètement :
|
|
10
|
+
|
|
11
|
+
* aucun seuil n'est embarqué — pas de `is_safe()`, pas de niveau de
|
|
12
|
+
qualité dérivé ;
|
|
13
|
+
* `null` reste `None` : aucune valeur par défaut numérique n'existe dans
|
|
14
|
+
le code source ;
|
|
15
|
+
* un champ dont la sémantique est ambiguë est exposé sous son chemin
|
|
16
|
+
complet, avec l'ambiguïté documentée, plutôt que converti.
|
|
17
|
+
|
|
18
|
+
Lire `docs/LIMITES.md` avant d'interpréter un score.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
__version__ = "2.0.0"
|
|
24
|
+
|
|
25
|
+
#: Le contrat backend auquel ce SDK se réfère.
|
|
26
|
+
#:
|
|
27
|
+
#: `backend_commit` est le commit où les modèles de requête et de réponse
|
|
28
|
+
#: ont été relevés. Il est inscrit ici parce qu'aucun OpenAPI figé
|
|
29
|
+
#: n'existe côté serveur : nommer le commit est le seul moyen de dire à
|
|
30
|
+
#: quoi ce SDK correspond.
|
|
31
|
+
#:
|
|
32
|
+
#: `measurement_version` est le seul numéro qui ait un sens scientifique :
|
|
33
|
+
#: deux résultats produits sous deux valeurs différentes ne sont pas
|
|
34
|
+
#: comparables pour une même entrée. Comparer la valeur ci-dessous à
|
|
35
|
+
#: `result.audit.measurement_version` avertit d'un écart.
|
|
36
|
+
#:
|
|
37
|
+
#: `verified_measurement_versions` est une liste **plus large**, et la
|
|
38
|
+
#: distinction compte. `measurement_version` dit de quel commit ce SDK a
|
|
39
|
+
#: été dérivé ; `verified_measurement_versions` dit sous quelles versions
|
|
40
|
+
#: de mesure la surface REST a été vérifiée identique à celle-là. Un
|
|
41
|
+
#: écart entre les deux n'est donc pas une incompatibilité tant que la
|
|
42
|
+
#: version servie figure dans cette liste — mais il reste un écart, et il
|
|
43
|
+
#: reste décisif pour la **comparabilité** de deux séries.
|
|
44
|
+
#:
|
|
45
|
+
#: `3.3.1` y figure depuis le 2026-10-22, sur deux preuves énoncées en
|
|
46
|
+
#: entier dans `KNOWN_LIMITATIONS.md` §6 : les fichiers de contrat du
|
|
47
|
+
#: backend sont **inchangés** entre `68d6057` et le commit déployé
|
|
48
|
+
#: `aeff13c`, et une réponse réellement émise par la production sous
|
|
49
|
+
#: `3.3.1` a été rejouée sans avertissement ni champ inconnu. **Aucune
|
|
50
|
+
#: des deux n'est un appel de ce SDK contre le service hébergé.**
|
|
51
|
+
API_CONTRACT = {
|
|
52
|
+
"backend_commit": "68d6057cf620e430e6488da3cfcec9c471c75ed6",
|
|
53
|
+
"measurement_version": "3.3.0",
|
|
54
|
+
"verified_measurement_versions": ("3.3.0", "3.3.1"),
|
|
55
|
+
"normalizer_version": "1.0.0",
|
|
56
|
+
"route": "POST /v1/govern",
|
|
57
|
+
"stream_route": "POST /v1/govern/stream",
|
|
58
|
+
"stream_backend_commit": "aeff13cca717205c9ebbc83c5755ba6a22b284bb",
|
|
59
|
+
"stream_verification": "local_backend_source_and_offline_tests_only",
|
|
60
|
+
"contract_source": "govern-v3/app/models/responses.py, requests.py",
|
|
61
|
+
"openapi_frozen": False,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
from ._retry import NO_RETRY, RetryPolicy
|
|
65
|
+
from .streaming import (
|
|
66
|
+
GOVERN_STREAM_PATH,
|
|
67
|
+
GovernStream,
|
|
68
|
+
StreamError,
|
|
69
|
+
StreamEvent,
|
|
70
|
+
StreamInterruptedError,
|
|
71
|
+
StreamProtocolError,
|
|
72
|
+
StreamResult,
|
|
73
|
+
StreamServerError,
|
|
74
|
+
)
|
|
75
|
+
from .client import GOVERN_PATH, KEY_PREFIXES, NeoMundiClient
|
|
76
|
+
from .errors import (
|
|
77
|
+
AggregateWithheldError,
|
|
78
|
+
APIError,
|
|
79
|
+
AuthenticationError,
|
|
80
|
+
ConfigurationError,
|
|
81
|
+
InvalidResponseError,
|
|
82
|
+
NeoMundiError,
|
|
83
|
+
NetworkError,
|
|
84
|
+
PaymentRequiredError,
|
|
85
|
+
PayloadTooLargeError,
|
|
86
|
+
RateLimitError,
|
|
87
|
+
ServerError,
|
|
88
|
+
TimeoutError,
|
|
89
|
+
TransportError,
|
|
90
|
+
ValidationError,
|
|
91
|
+
)
|
|
92
|
+
from .models import (
|
|
93
|
+
AGGREGATE_FIELDS,
|
|
94
|
+
COVERAGE_SIGNALS,
|
|
95
|
+
Audit,
|
|
96
|
+
Baseline,
|
|
97
|
+
Citation,
|
|
98
|
+
Density,
|
|
99
|
+
Esi,
|
|
100
|
+
Governance,
|
|
101
|
+
GovernResult,
|
|
102
|
+
Measurement,
|
|
103
|
+
MeasurementState,
|
|
104
|
+
Metrics,
|
|
105
|
+
ParseWarning,
|
|
106
|
+
Quality,
|
|
107
|
+
RagGrounding,
|
|
108
|
+
Runtime,
|
|
109
|
+
SuspectPhrase,
|
|
110
|
+
UngroundedSnippet,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
__all__ = [
|
|
114
|
+
"GOVERN_STREAM_PATH",
|
|
115
|
+
"GovernStream",
|
|
116
|
+
"StreamEvent",
|
|
117
|
+
"StreamResult",
|
|
118
|
+
"StreamError",
|
|
119
|
+
"StreamInterruptedError",
|
|
120
|
+
"StreamProtocolError",
|
|
121
|
+
"StreamServerError",
|
|
122
|
+
"__version__",
|
|
123
|
+
"API_CONTRACT",
|
|
124
|
+
# client
|
|
125
|
+
"NeoMundiClient",
|
|
126
|
+
"GOVERN_PATH",
|
|
127
|
+
"KEY_PREFIXES",
|
|
128
|
+
# retry
|
|
129
|
+
"RetryPolicy",
|
|
130
|
+
"NO_RETRY",
|
|
131
|
+
# modèles
|
|
132
|
+
"GovernResult",
|
|
133
|
+
"MeasurementState",
|
|
134
|
+
"COVERAGE_SIGNALS",
|
|
135
|
+
"AGGREGATE_FIELDS",
|
|
136
|
+
"ParseWarning",
|
|
137
|
+
"Governance",
|
|
138
|
+
"Measurement",
|
|
139
|
+
"Quality",
|
|
140
|
+
"Runtime",
|
|
141
|
+
"Metrics",
|
|
142
|
+
"Baseline",
|
|
143
|
+
"Esi",
|
|
144
|
+
"Density",
|
|
145
|
+
"RagGrounding",
|
|
146
|
+
"Audit",
|
|
147
|
+
"SuspectPhrase",
|
|
148
|
+
"Citation",
|
|
149
|
+
"UngroundedSnippet",
|
|
150
|
+
# erreurs
|
|
151
|
+
"NeoMundiError",
|
|
152
|
+
"ConfigurationError",
|
|
153
|
+
"TransportError",
|
|
154
|
+
"TimeoutError",
|
|
155
|
+
"NetworkError",
|
|
156
|
+
"InvalidResponseError",
|
|
157
|
+
"AggregateWithheldError",
|
|
158
|
+
"APIError",
|
|
159
|
+
"AuthenticationError",
|
|
160
|
+
"PaymentRequiredError",
|
|
161
|
+
"PayloadTooLargeError",
|
|
162
|
+
"ValidationError",
|
|
163
|
+
"RateLimitError",
|
|
164
|
+
"ServerError",
|
|
165
|
+
]
|
neomundi/_retry.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Politique de réessai — **désactivée par défaut**, et c'est une décision de fond.
|
|
2
|
+
|
|
3
|
+
`POST /v1/govern` n'est pas idempotent. Relevé dans le backend au commit
|
|
4
|
+
`68d6057` :
|
|
5
|
+
|
|
6
|
+
* une **nouvelle observation** est créée à chaque appel —
|
|
7
|
+
`request_id = str(uuid.uuid4())`, `runtime_engine.py:162` ;
|
|
8
|
+
* le **quota ou le portefeuille est débité** avant le travail —
|
|
9
|
+
`enforce_trial_quota(client)`, `governance.py:172` ;
|
|
10
|
+
* une **ligne de métrologie** est écrite — `usage_events`,
|
|
11
|
+
`governance.py:586-626` ;
|
|
12
|
+
* la **référence `delta_g` du compte est écrasée** dans Redis —
|
|
13
|
+
`governance.py:2088`.
|
|
14
|
+
|
|
15
|
+
Aucun en-tête d'idempotence n'existe sur cette route : la recherche de
|
|
16
|
+
`Idempotency-Key` dans `govern-v3/` ne renvoie aucune occurrence.
|
|
17
|
+
|
|
18
|
+
Conséquence : un réessai automatique produirait une **seconde mesure
|
|
19
|
+
facturée** et faussserait le `delta_g` de l'appel suivant. Le SDK ne
|
|
20
|
+
réessaie donc rien de lui-même. Il expose `Retry-After` sur
|
|
21
|
+
`RateLimitError` et laisse l'appelant décider.
|
|
22
|
+
|
|
23
|
+
Une politique explicite reste disponible pour qui l'assume, avec un
|
|
24
|
+
garde-fou qui n'est pas négociable : **un délai dépassé n'est jamais
|
|
25
|
+
réessayé**. C'est le seul cas où le SDK ne peut pas savoir si la requête
|
|
26
|
+
a été exécutée côté serveur. Un `503` *reçu*, lui, prouve qu'elle n'a pas
|
|
27
|
+
abouti.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
from dataclasses import dataclass, field
|
|
33
|
+
from typing import Tuple
|
|
34
|
+
|
|
35
|
+
__all__ = ["RetryPolicy", "NO_RETRY"]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class RetryPolicy:
|
|
40
|
+
"""Réessais sur les statuts HTTP que l'appelant juge sûrs.
|
|
41
|
+
|
|
42
|
+
À construire **explicitement**. Il n'existe pas de `retry=True` ni
|
|
43
|
+
d'entier magique : activer des réessais sur une route non idempotente
|
|
44
|
+
est une décision, pas une option de confort.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
max_attempts: nombre total de tentatives, réessais inclus. `1`
|
|
48
|
+
signifie « aucun réessai ». Doit être ≥ 1.
|
|
49
|
+
on: statuts HTTP réessayables. Par défaut `(503,)` — un service
|
|
50
|
+
indisponible a répondu, donc il n'a pas traité la requête.
|
|
51
|
+
**429 n'y est pas par défaut** : une limite de débit atteinte
|
|
52
|
+
demande d'attendre `Retry-After`, ce que l'appelant maîtrise
|
|
53
|
+
mieux que le SDK.
|
|
54
|
+
backoff_initial: délai de la première attente, en secondes.
|
|
55
|
+
backoff_multiplier: facteur appliqué à chaque nouvelle attente.
|
|
56
|
+
backoff_max: plafond d'une attente, en secondes.
|
|
57
|
+
respect_retry_after: quand le serveur envoie `Retry-After`, ce
|
|
58
|
+
délai prime sur le calcul exponentiel.
|
|
59
|
+
|
|
60
|
+
Jamais réessayé, quelle que soit la politique :
|
|
61
|
+
|
|
62
|
+
* un **délai dépassé** (`TimeoutError`) — la requête a peut-être été
|
|
63
|
+
exécutée et facturée ;
|
|
64
|
+
* une **erreur réseau** (`NetworkError`) — même raison ;
|
|
65
|
+
* tout statut absent de `on`.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
max_attempts: int = 1
|
|
69
|
+
on: Tuple[int, ...] = field(default=(503,))
|
|
70
|
+
backoff_initial: float = 0.5
|
|
71
|
+
backoff_multiplier: float = 2.0
|
|
72
|
+
backoff_max: float = 8.0
|
|
73
|
+
respect_retry_after: bool = True
|
|
74
|
+
|
|
75
|
+
def __post_init__(self) -> None:
|
|
76
|
+
if self.max_attempts < 1:
|
|
77
|
+
raise ValueError("max_attempts doit être >= 1 (1 = aucun réessai).")
|
|
78
|
+
if self.backoff_initial < 0 or self.backoff_max < 0:
|
|
79
|
+
raise ValueError("Les délais de backoff doivent être positifs.")
|
|
80
|
+
if self.backoff_multiplier < 1:
|
|
81
|
+
raise ValueError("backoff_multiplier doit être >= 1.")
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def enabled(self) -> bool:
|
|
85
|
+
"""`True` seulement si plus d'une tentative est autorisée."""
|
|
86
|
+
return self.max_attempts > 1
|
|
87
|
+
|
|
88
|
+
def should_retry(self, status_code: int, attempt: int) -> bool:
|
|
89
|
+
"""`True` si un statut peut être réessayé à cette tentative.
|
|
90
|
+
|
|
91
|
+
`attempt` est le numéro de la tentative qui vient d'échouer, à
|
|
92
|
+
partir de 1.
|
|
93
|
+
"""
|
|
94
|
+
if not self.enabled:
|
|
95
|
+
return False
|
|
96
|
+
if attempt >= self.max_attempts:
|
|
97
|
+
return False
|
|
98
|
+
return status_code in self.on
|
|
99
|
+
|
|
100
|
+
def delay_for(self, attempt: int, retry_after: "float | None" = None) -> float:
|
|
101
|
+
"""Le délai à attendre après la tentative `attempt`.
|
|
102
|
+
|
|
103
|
+
`retry_after`, quand le serveur le fournit et que
|
|
104
|
+
`respect_retry_after` est actif, prime : le serveur sait mieux que
|
|
105
|
+
nous quand il acceptera la requête suivante.
|
|
106
|
+
"""
|
|
107
|
+
if retry_after is not None and self.respect_retry_after:
|
|
108
|
+
return max(0.0, float(retry_after))
|
|
109
|
+
delay = self.backoff_initial * (self.backoff_multiplier ** max(0, attempt - 1))
|
|
110
|
+
return min(delay, self.backoff_max)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
#: La politique par défaut : aucune tentative supplémentaire.
|
|
114
|
+
NO_RETRY = RetryPolicy(max_attempts=1)
|