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 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)