brio-sdk 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.
- brio_sdk-0.1.0/.gitignore +9 -0
- brio_sdk-0.1.0/CHANGELOG.md +8 -0
- brio_sdk-0.1.0/PKG-INFO +195 -0
- brio_sdk-0.1.0/README.md +176 -0
- brio_sdk-0.1.0/contract-snapshot.json +222 -0
- brio_sdk-0.1.0/pyproject.toml +78 -0
- brio_sdk-0.1.0/src/brio_sdk/__init__.py +107 -0
- brio_sdk-0.1.0/src/brio_sdk/_clock.py +59 -0
- brio_sdk-0.1.0/src/brio_sdk/_config.py +98 -0
- brio_sdk-0.1.0/src/brio_sdk/_constants.py +73 -0
- brio_sdk-0.1.0/src/brio_sdk/_http.py +145 -0
- brio_sdk-0.1.0/src/brio_sdk/_payloads.py +58 -0
- brio_sdk-0.1.0/src/brio_sdk/_upload.py +91 -0
- brio_sdk-0.1.0/src/brio_sdk/async_client.py +303 -0
- brio_sdk-0.1.0/src/brio_sdk/client.py +303 -0
- brio_sdk-0.1.0/src/brio_sdk/exceptions.py +223 -0
- brio_sdk-0.1.0/src/brio_sdk/models.py +165 -0
- brio_sdk-0.1.0/src/brio_sdk/py.typed +0 -0
- brio_sdk-0.1.0/src/brio_sdk/services.py +161 -0
- brio_sdk-0.1.0/tests/__init__.py +1 -0
- brio_sdk-0.1.0/tests/conftest.py +119 -0
- brio_sdk-0.1.0/tests/test_clients.py +448 -0
- brio_sdk-0.1.0/tests/test_config.py +54 -0
- brio_sdk-0.1.0/tests/test_exceptions.py +121 -0
- brio_sdk-0.1.0/tests/test_http.py +100 -0
- brio_sdk-0.1.0/tests/test_models.py +103 -0
- brio_sdk-0.1.0/tests/test_package.py +16 -0
- brio_sdk-0.1.0/tests/test_services.py +116 -0
- brio_sdk-0.1.0/tests/test_symmetry.py +67 -0
- brio_sdk-0.1.0/uv.lock +346 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0](https://github.com/IA-Generative/async-api/compare/brio-sdk-v0.0.0...brio-sdk-v0.1.0) (2026-07-30)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* SDK ([#515](https://github.com/IA-Generative/async-api/issues/515)) ([9ff7596](https://github.com/IA-Generative/async-api/commit/9ff7596d41e9b3f9bf2570d8150debcd63a282b2))
|
brio_sdk-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: brio-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Client SDK (sync + async) des services IA BRIO — soumission de tâches, polling, fichiers.
|
|
5
|
+
Project-URL: Repository, https://github.com/IA-Generative/async-api
|
|
6
|
+
Project-URL: Documentation, https://github.com/IA-Generative/async-api/tree/main/clients/python/brio-sdk
|
|
7
|
+
License: MIT
|
|
8
|
+
Keywords: async-api,brio,minint,sdk
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Requires-Python: >=3.11
|
|
16
|
+
Requires-Dist: httpx>=0.28
|
|
17
|
+
Requires-Dist: pydantic>=2.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# brio-sdk
|
|
21
|
+
|
|
22
|
+
Client Python des services IA **BRIO** : soumettre une tâche, attendre son résultat, envoyer et
|
|
23
|
+
récupérer des fichiers — sans réécrire l'auth, le polling ni le mapping d'erreurs.
|
|
24
|
+
|
|
25
|
+
Deux clients à **surface identique** : `BrioClient` (synchrone, pour les scripts) et
|
|
26
|
+
`AsyncBrioClient` (asynchrone, pour un service FastAPI ou un worker). Mêmes méthodes, mêmes
|
|
27
|
+
signatures, mêmes types de retour.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install brio-sdk
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Python ≥ 3.11.
|
|
34
|
+
|
|
35
|
+
## Configuration
|
|
36
|
+
|
|
37
|
+
Les credentials viennent des arguments du constructeur, ou de l'environnement :
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
export BRIO_BASE_URL="https://async-api.sdid-app.cpin.numerique-interieur.com"
|
|
41
|
+
export BRIO_CLIENT_ID="mon_client"
|
|
42
|
+
export BRIO_CLIENT_SECRET="..."
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Un réglage manquant lève `BrioConfigError` **au constructeur** — pas une 401 au milieu d'un
|
|
46
|
+
traitement. Pour vérifier que les identifiants sont bons côté serveur :
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from brio_sdk import BrioClient
|
|
50
|
+
|
|
51
|
+
with BrioClient() as client:
|
|
52
|
+
me = client.whoami()
|
|
53
|
+
print(me.client_id, [a.service for a in me.authorizations])
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Quickstart (synchrone)
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from brio_sdk import BrioClient, Service
|
|
60
|
+
from brio_sdk.services import SplitDocumentBody, SplitDocumentResult
|
|
61
|
+
|
|
62
|
+
with BrioClient() as client:
|
|
63
|
+
upload = client.upload_file("dossier.pdf") # bascule API/S3 automatique
|
|
64
|
+
pending = client.submit_task(
|
|
65
|
+
Service.SPLIT_DOCUMENT,
|
|
66
|
+
SplitDocumentBody(file_id=upload.file_id), # ou simplement {"file_id": ...}
|
|
67
|
+
)
|
|
68
|
+
success = client.wait_for_result(Service.SPLIT_DOCUMENT, pending.data.task_id)
|
|
69
|
+
|
|
70
|
+
split = SplitDocumentResult.model_validate(success.result) # typage opt-in du résultat
|
|
71
|
+
for document in split.documents:
|
|
72
|
+
client.download_file(document.file_id, f"{document.page_start}.pdf")
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Quickstart (asynchrone)
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
import asyncio
|
|
79
|
+
|
|
80
|
+
from brio_sdk import AsyncBrioClient, Service
|
|
81
|
+
from brio_sdk.services import SplitDocumentBody, SplitDocumentResult
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
async def main() -> None:
|
|
85
|
+
async with AsyncBrioClient() as client:
|
|
86
|
+
upload = await client.upload_file("dossier.pdf")
|
|
87
|
+
pending = await client.submit_task(
|
|
88
|
+
Service.SPLIT_DOCUMENT,
|
|
89
|
+
SplitDocumentBody(file_id=upload.file_id),
|
|
90
|
+
)
|
|
91
|
+
success = await client.wait_for_result(Service.SPLIT_DOCUMENT, pending.data.task_id)
|
|
92
|
+
print(SplitDocumentResult.model_validate(success.result).documents)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
asyncio.run(main())
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Ce que le SDK prend en charge pour vous
|
|
99
|
+
|
|
100
|
+
| Sujet | Comportement |
|
|
101
|
+
|---|---|
|
|
102
|
+
| Auth | HTTP Basic injectée à chaque appel, jamais loguée |
|
|
103
|
+
| Polling | `wait_for_result` : intervalle doublé jusqu'à 30 s, délai max réglable, retour immédiat si la tâche est déjà finie |
|
|
104
|
+
| Réessais | 429 et 502/503/504 et erreurs de transport : backoff exponentiel + jitter, plafonné à 30 s, `Retry-After` prioritaire s'il est émis. `RetryConfig(max_attempts=1)` désactive tout |
|
|
105
|
+
| Upload | Bascule automatique : transit par l'API sous 25 Mo, URL pré-signée S3 au-delà (jusqu'à 500 Mo). Contenu streamé, pas de chargement en mémoire |
|
|
106
|
+
| Sécurité S3 | Les requêtes vers S3 partent sur un client HTTP dédié : l'en-tête `Authorization` de l'API ne fuite jamais chez le fournisseur de stockage |
|
|
107
|
+
| Typage | Modèles de réponse typés, union discriminée sur `data.status`, bodies typés par service |
|
|
108
|
+
|
|
109
|
+
## Erreurs : quoi attraper, quoi en faire
|
|
110
|
+
|
|
111
|
+
Toutes héritent de `BrioError`.
|
|
112
|
+
|
|
113
|
+
| Exception | Cause serveur | Action attendue |
|
|
114
|
+
|---|---|---|
|
|
115
|
+
| `BrioConfigError` | Configuration locale incomplète | Corriger les arguments ou les variables d'environnement |
|
|
116
|
+
| `AuthenticationError` | `AUTHENTICATION_REQUIRED` (401) | Vérifier `client_id` / `client_secret` |
|
|
117
|
+
| `ServiceForbidden` | `SERVICE_FORBIDDEN` (403) | Demander l'autorisation du client sur ce service |
|
|
118
|
+
| `ServiceNotFound` | `SERVICE_NOT_FOUND` (404) | Corriger le nom du service (voir l'enum `Service`) |
|
|
119
|
+
| `TaskNotFound` / `FileNotFound` | `TASK_NOT_FOUND` / `FILE_NOT_FOUND` (404) | Identifiant inconnu, ou appartenant à un autre client |
|
|
120
|
+
| `BodyValidationError` | `BODY_SCHEMA_INVALID`, `REQUEST_VALIDATION_ERROR`, `MALFORMED_REQUEST` | Lire `error.issues` : chaque entrée donne le champ fautif |
|
|
121
|
+
| `MissingMultipartField` | `MISSING_MULTIPART_FIELD` (422) | Champ absent du form-data d'upload |
|
|
122
|
+
| `TextTooLarge` | `TEXT_TOO_LARGE` (413) | Découper le contenu en entrée |
|
|
123
|
+
| `QuotaExceeded` | `SERVICE_QUOTA_EXCEEDED`, `CLIENT_SERVICE_QUOTA_EXCEEDED` (429) | Réessayer plus tard (le SDK a déjà réessayé) |
|
|
124
|
+
| `ServiceUnavailable` | `DEPENDENCIES_NOT_READY` (503) | Dépendance serveur indisponible, réessayer plus tard |
|
|
125
|
+
| `TaskFailed` | La tâche s'est terminée en échec | `error.message` porte le motif renvoyé par le service |
|
|
126
|
+
| `TaskTimeoutError` | Délai d'attente dépassé | La tâche **continue** côté serveur : relancer `wait_for_result` avec le même `task_id` |
|
|
127
|
+
| `BrioServerError` | Erreur non mappée, ou réponse hors contrat | Diagnostic : `error.response_payload` contient le corps brut |
|
|
128
|
+
| `BrioTransportError` | Aucune réponse (DNS, TCP, TLS, timeout) | Vérifier réseau et `base_url` |
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from brio_sdk import BodyValidationError, QuotaExceeded
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
client.submit_task(Service.CLASSIFY_DOCUMENT, {"file_id": file_id})
|
|
135
|
+
except BodyValidationError as error:
|
|
136
|
+
for issue in error.issues:
|
|
137
|
+
print(issue.loc, issue.msg)
|
|
138
|
+
except QuotaExceeded:
|
|
139
|
+
print("quota atteint, on réessaiera plus tard")
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`repr()` d'une exception n'expose jamais le corps de réponse ni un secret : elle peut être loguée
|
|
143
|
+
telle quelle.
|
|
144
|
+
|
|
145
|
+
## Services et bodies typés
|
|
146
|
+
|
|
147
|
+
`Service` est un `StrEnum` **ouvert** : une chaîne libre reste acceptée, parce que le catalogue est
|
|
148
|
+
piloté par la configuration serveur — un service plus récent que le SDK reste appelable.
|
|
149
|
+
|
|
150
|
+
| Service | Body typé | Résultat typé |
|
|
151
|
+
|---|---|---|
|
|
152
|
+
| `extract-text` | `ExtractTextBody` | `ExtractTextResult` |
|
|
153
|
+
| `classify-document` | `ClassifyDocumentBody` | `ClassifyDocumentResult` |
|
|
154
|
+
| `split-document` | `SplitDocumentBody` | `SplitDocumentResult` |
|
|
155
|
+
| `index-document` | `IndexDocumentBody` | `IndexDocumentResult` |
|
|
156
|
+
| `generation-render` | `GenerationRenderBody` / `GenerationRenderInlineBody` | `GenerationRenderResult` / `InlineRenderResult` |
|
|
157
|
+
| `extract-entities` | dict (méta-DSL récursif, non typé) | dict |
|
|
158
|
+
|
|
159
|
+
Les bodies sont en `extra="forbid"` : un champ mal orthographié est rejeté **localement**, avant
|
|
160
|
+
l'appel réseau. Les modèles de résultat sont en `extra="ignore"` et s'appliquent à la demande
|
|
161
|
+
(`Model.model_validate(success.result)`), ce qui isole le consommateur des évolutions de schéma
|
|
162
|
+
côté workers.
|
|
163
|
+
|
|
164
|
+
## Injection de dépendances
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
import httpx
|
|
168
|
+
|
|
169
|
+
from brio_sdk import BrioClient, RetryConfig
|
|
170
|
+
|
|
171
|
+
client = BrioClient(
|
|
172
|
+
retry=RetryConfig(max_attempts=5, backoff_cap_seconds=10.0),
|
|
173
|
+
http_client=httpx.Client(proxy="http://proxy.interne:3128"), # proxy, mTLS, timeouts maison
|
|
174
|
+
s3_http_client=httpx.Client(), # client distinct pour S3
|
|
175
|
+
)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
`clock` est également injectable (protocole `Clock` / `AsyncClock`) : les tests couvrent timeouts et
|
|
179
|
+
backoff sans attendre réellement.
|
|
180
|
+
|
|
181
|
+
## Compatibilité
|
|
182
|
+
|
|
183
|
+
Le SDK est ancré sur l'API **`/v1`** de l'async-api, pas sur un numéro de version serveur. Les
|
|
184
|
+
modèles de réponse ignorent les champs inconnus : un ajout côté serveur ne casse pas un
|
|
185
|
+
consommateur déjà déployé.
|
|
186
|
+
|
|
187
|
+
## Développement
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
uv sync
|
|
191
|
+
uv run ruff check .
|
|
192
|
+
uv run pytest
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Aucun test ne touche le réseau (`httpx.MockTransport`) ni ne dort (horloge factice).
|
brio_sdk-0.1.0/README.md
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# brio-sdk
|
|
2
|
+
|
|
3
|
+
Client Python des services IA **BRIO** : soumettre une tâche, attendre son résultat, envoyer et
|
|
4
|
+
récupérer des fichiers — sans réécrire l'auth, le polling ni le mapping d'erreurs.
|
|
5
|
+
|
|
6
|
+
Deux clients à **surface identique** : `BrioClient` (synchrone, pour les scripts) et
|
|
7
|
+
`AsyncBrioClient` (asynchrone, pour un service FastAPI ou un worker). Mêmes méthodes, mêmes
|
|
8
|
+
signatures, mêmes types de retour.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install brio-sdk
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Python ≥ 3.11.
|
|
15
|
+
|
|
16
|
+
## Configuration
|
|
17
|
+
|
|
18
|
+
Les credentials viennent des arguments du constructeur, ou de l'environnement :
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
export BRIO_BASE_URL="https://async-api.sdid-app.cpin.numerique-interieur.com"
|
|
22
|
+
export BRIO_CLIENT_ID="mon_client"
|
|
23
|
+
export BRIO_CLIENT_SECRET="..."
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Un réglage manquant lève `BrioConfigError` **au constructeur** — pas une 401 au milieu d'un
|
|
27
|
+
traitement. Pour vérifier que les identifiants sont bons côté serveur :
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from brio_sdk import BrioClient
|
|
31
|
+
|
|
32
|
+
with BrioClient() as client:
|
|
33
|
+
me = client.whoami()
|
|
34
|
+
print(me.client_id, [a.service for a in me.authorizations])
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quickstart (synchrone)
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from brio_sdk import BrioClient, Service
|
|
41
|
+
from brio_sdk.services import SplitDocumentBody, SplitDocumentResult
|
|
42
|
+
|
|
43
|
+
with BrioClient() as client:
|
|
44
|
+
upload = client.upload_file("dossier.pdf") # bascule API/S3 automatique
|
|
45
|
+
pending = client.submit_task(
|
|
46
|
+
Service.SPLIT_DOCUMENT,
|
|
47
|
+
SplitDocumentBody(file_id=upload.file_id), # ou simplement {"file_id": ...}
|
|
48
|
+
)
|
|
49
|
+
success = client.wait_for_result(Service.SPLIT_DOCUMENT, pending.data.task_id)
|
|
50
|
+
|
|
51
|
+
split = SplitDocumentResult.model_validate(success.result) # typage opt-in du résultat
|
|
52
|
+
for document in split.documents:
|
|
53
|
+
client.download_file(document.file_id, f"{document.page_start}.pdf")
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Quickstart (asynchrone)
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
import asyncio
|
|
60
|
+
|
|
61
|
+
from brio_sdk import AsyncBrioClient, Service
|
|
62
|
+
from brio_sdk.services import SplitDocumentBody, SplitDocumentResult
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
async def main() -> None:
|
|
66
|
+
async with AsyncBrioClient() as client:
|
|
67
|
+
upload = await client.upload_file("dossier.pdf")
|
|
68
|
+
pending = await client.submit_task(
|
|
69
|
+
Service.SPLIT_DOCUMENT,
|
|
70
|
+
SplitDocumentBody(file_id=upload.file_id),
|
|
71
|
+
)
|
|
72
|
+
success = await client.wait_for_result(Service.SPLIT_DOCUMENT, pending.data.task_id)
|
|
73
|
+
print(SplitDocumentResult.model_validate(success.result).documents)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
asyncio.run(main())
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Ce que le SDK prend en charge pour vous
|
|
80
|
+
|
|
81
|
+
| Sujet | Comportement |
|
|
82
|
+
|---|---|
|
|
83
|
+
| Auth | HTTP Basic injectée à chaque appel, jamais loguée |
|
|
84
|
+
| Polling | `wait_for_result` : intervalle doublé jusqu'à 30 s, délai max réglable, retour immédiat si la tâche est déjà finie |
|
|
85
|
+
| Réessais | 429 et 502/503/504 et erreurs de transport : backoff exponentiel + jitter, plafonné à 30 s, `Retry-After` prioritaire s'il est émis. `RetryConfig(max_attempts=1)` désactive tout |
|
|
86
|
+
| Upload | Bascule automatique : transit par l'API sous 25 Mo, URL pré-signée S3 au-delà (jusqu'à 500 Mo). Contenu streamé, pas de chargement en mémoire |
|
|
87
|
+
| Sécurité S3 | Les requêtes vers S3 partent sur un client HTTP dédié : l'en-tête `Authorization` de l'API ne fuite jamais chez le fournisseur de stockage |
|
|
88
|
+
| Typage | Modèles de réponse typés, union discriminée sur `data.status`, bodies typés par service |
|
|
89
|
+
|
|
90
|
+
## Erreurs : quoi attraper, quoi en faire
|
|
91
|
+
|
|
92
|
+
Toutes héritent de `BrioError`.
|
|
93
|
+
|
|
94
|
+
| Exception | Cause serveur | Action attendue |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| `BrioConfigError` | Configuration locale incomplète | Corriger les arguments ou les variables d'environnement |
|
|
97
|
+
| `AuthenticationError` | `AUTHENTICATION_REQUIRED` (401) | Vérifier `client_id` / `client_secret` |
|
|
98
|
+
| `ServiceForbidden` | `SERVICE_FORBIDDEN` (403) | Demander l'autorisation du client sur ce service |
|
|
99
|
+
| `ServiceNotFound` | `SERVICE_NOT_FOUND` (404) | Corriger le nom du service (voir l'enum `Service`) |
|
|
100
|
+
| `TaskNotFound` / `FileNotFound` | `TASK_NOT_FOUND` / `FILE_NOT_FOUND` (404) | Identifiant inconnu, ou appartenant à un autre client |
|
|
101
|
+
| `BodyValidationError` | `BODY_SCHEMA_INVALID`, `REQUEST_VALIDATION_ERROR`, `MALFORMED_REQUEST` | Lire `error.issues` : chaque entrée donne le champ fautif |
|
|
102
|
+
| `MissingMultipartField` | `MISSING_MULTIPART_FIELD` (422) | Champ absent du form-data d'upload |
|
|
103
|
+
| `TextTooLarge` | `TEXT_TOO_LARGE` (413) | Découper le contenu en entrée |
|
|
104
|
+
| `QuotaExceeded` | `SERVICE_QUOTA_EXCEEDED`, `CLIENT_SERVICE_QUOTA_EXCEEDED` (429) | Réessayer plus tard (le SDK a déjà réessayé) |
|
|
105
|
+
| `ServiceUnavailable` | `DEPENDENCIES_NOT_READY` (503) | Dépendance serveur indisponible, réessayer plus tard |
|
|
106
|
+
| `TaskFailed` | La tâche s'est terminée en échec | `error.message` porte le motif renvoyé par le service |
|
|
107
|
+
| `TaskTimeoutError` | Délai d'attente dépassé | La tâche **continue** côté serveur : relancer `wait_for_result` avec le même `task_id` |
|
|
108
|
+
| `BrioServerError` | Erreur non mappée, ou réponse hors contrat | Diagnostic : `error.response_payload` contient le corps brut |
|
|
109
|
+
| `BrioTransportError` | Aucune réponse (DNS, TCP, TLS, timeout) | Vérifier réseau et `base_url` |
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from brio_sdk import BodyValidationError, QuotaExceeded
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
client.submit_task(Service.CLASSIFY_DOCUMENT, {"file_id": file_id})
|
|
116
|
+
except BodyValidationError as error:
|
|
117
|
+
for issue in error.issues:
|
|
118
|
+
print(issue.loc, issue.msg)
|
|
119
|
+
except QuotaExceeded:
|
|
120
|
+
print("quota atteint, on réessaiera plus tard")
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`repr()` d'une exception n'expose jamais le corps de réponse ni un secret : elle peut être loguée
|
|
124
|
+
telle quelle.
|
|
125
|
+
|
|
126
|
+
## Services et bodies typés
|
|
127
|
+
|
|
128
|
+
`Service` est un `StrEnum` **ouvert** : une chaîne libre reste acceptée, parce que le catalogue est
|
|
129
|
+
piloté par la configuration serveur — un service plus récent que le SDK reste appelable.
|
|
130
|
+
|
|
131
|
+
| Service | Body typé | Résultat typé |
|
|
132
|
+
|---|---|---|
|
|
133
|
+
| `extract-text` | `ExtractTextBody` | `ExtractTextResult` |
|
|
134
|
+
| `classify-document` | `ClassifyDocumentBody` | `ClassifyDocumentResult` |
|
|
135
|
+
| `split-document` | `SplitDocumentBody` | `SplitDocumentResult` |
|
|
136
|
+
| `index-document` | `IndexDocumentBody` | `IndexDocumentResult` |
|
|
137
|
+
| `generation-render` | `GenerationRenderBody` / `GenerationRenderInlineBody` | `GenerationRenderResult` / `InlineRenderResult` |
|
|
138
|
+
| `extract-entities` | dict (méta-DSL récursif, non typé) | dict |
|
|
139
|
+
|
|
140
|
+
Les bodies sont en `extra="forbid"` : un champ mal orthographié est rejeté **localement**, avant
|
|
141
|
+
l'appel réseau. Les modèles de résultat sont en `extra="ignore"` et s'appliquent à la demande
|
|
142
|
+
(`Model.model_validate(success.result)`), ce qui isole le consommateur des évolutions de schéma
|
|
143
|
+
côté workers.
|
|
144
|
+
|
|
145
|
+
## Injection de dépendances
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
import httpx
|
|
149
|
+
|
|
150
|
+
from brio_sdk import BrioClient, RetryConfig
|
|
151
|
+
|
|
152
|
+
client = BrioClient(
|
|
153
|
+
retry=RetryConfig(max_attempts=5, backoff_cap_seconds=10.0),
|
|
154
|
+
http_client=httpx.Client(proxy="http://proxy.interne:3128"), # proxy, mTLS, timeouts maison
|
|
155
|
+
s3_http_client=httpx.Client(), # client distinct pour S3
|
|
156
|
+
)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
`clock` est également injectable (protocole `Clock` / `AsyncClock`) : les tests couvrent timeouts et
|
|
160
|
+
backoff sans attendre réellement.
|
|
161
|
+
|
|
162
|
+
## Compatibilité
|
|
163
|
+
|
|
164
|
+
Le SDK est ancré sur l'API **`/v1`** de l'async-api, pas sur un numéro de version serveur. Les
|
|
165
|
+
modèles de réponse ignorent les champs inconnus : un ajout côté serveur ne casse pas un
|
|
166
|
+
consommateur déjà déployé.
|
|
167
|
+
|
|
168
|
+
## Développement
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
uv sync
|
|
172
|
+
uv run ruff check .
|
|
173
|
+
uv run pytest
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Aucun test ne touche le réseau (`httpx.MockTransport`) ni ne dort (horloge factice).
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
{
|
|
2
|
+
"paths": {
|
|
3
|
+
"/storage/presigned-download": {
|
|
4
|
+
"post": {
|
|
5
|
+
"parameters": [],
|
|
6
|
+
"status_codes": [
|
|
7
|
+
"200",
|
|
8
|
+
"401",
|
|
9
|
+
"404",
|
|
10
|
+
"422",
|
|
11
|
+
"500"
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"/storage/presigned-upload": {
|
|
16
|
+
"post": {
|
|
17
|
+
"parameters": [],
|
|
18
|
+
"status_codes": [
|
|
19
|
+
"200",
|
|
20
|
+
"401",
|
|
21
|
+
"422",
|
|
22
|
+
"500"
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"/storage/upload": {
|
|
27
|
+
"post": {
|
|
28
|
+
"parameters": [],
|
|
29
|
+
"status_codes": [
|
|
30
|
+
"201",
|
|
31
|
+
"401",
|
|
32
|
+
"422",
|
|
33
|
+
"500"
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"/v1/me": {
|
|
38
|
+
"get": {
|
|
39
|
+
"parameters": [],
|
|
40
|
+
"status_codes": [
|
|
41
|
+
"200",
|
|
42
|
+
"401",
|
|
43
|
+
"500"
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"/v1/services/{service}/tasks": {
|
|
48
|
+
"post": {
|
|
49
|
+
"parameters": [
|
|
50
|
+
"path:service"
|
|
51
|
+
],
|
|
52
|
+
"status_codes": [
|
|
53
|
+
"201",
|
|
54
|
+
"400",
|
|
55
|
+
"401",
|
|
56
|
+
"403",
|
|
57
|
+
"404",
|
|
58
|
+
"413",
|
|
59
|
+
"422",
|
|
60
|
+
"429",
|
|
61
|
+
"500"
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"/v1/services/{service}/tasks/{task_id}": {
|
|
66
|
+
"get": {
|
|
67
|
+
"parameters": [
|
|
68
|
+
"path:service",
|
|
69
|
+
"path:task_id"
|
|
70
|
+
],
|
|
71
|
+
"status_codes": [
|
|
72
|
+
"200",
|
|
73
|
+
"401",
|
|
74
|
+
"403",
|
|
75
|
+
"404",
|
|
76
|
+
"422",
|
|
77
|
+
"500"
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"schemas": {
|
|
83
|
+
"Callback": {
|
|
84
|
+
"properties": {
|
|
85
|
+
"queue": "anyOf[string,null]",
|
|
86
|
+
"type": "anyOf[string,null]",
|
|
87
|
+
"url": "anyOf[string,null]"
|
|
88
|
+
},
|
|
89
|
+
"required": []
|
|
90
|
+
},
|
|
91
|
+
"ErrorResponse": {
|
|
92
|
+
"properties": {
|
|
93
|
+
"error": "ref:ErrorDetail",
|
|
94
|
+
"status": "string"
|
|
95
|
+
},
|
|
96
|
+
"required": [
|
|
97
|
+
"error"
|
|
98
|
+
]
|
|
99
|
+
},
|
|
100
|
+
"MeResponse": {
|
|
101
|
+
"properties": {
|
|
102
|
+
"authorizations": "array",
|
|
103
|
+
"client_id": "string"
|
|
104
|
+
},
|
|
105
|
+
"required": [
|
|
106
|
+
"authorizations",
|
|
107
|
+
"client_id"
|
|
108
|
+
]
|
|
109
|
+
},
|
|
110
|
+
"PresignedDownloadRequest": {
|
|
111
|
+
"properties": {
|
|
112
|
+
"file_id": "string"
|
|
113
|
+
},
|
|
114
|
+
"required": [
|
|
115
|
+
"file_id"
|
|
116
|
+
]
|
|
117
|
+
},
|
|
118
|
+
"PresignedDownloadResponse": {
|
|
119
|
+
"properties": {
|
|
120
|
+
"expires_in_seconds": "integer",
|
|
121
|
+
"file_id": "string",
|
|
122
|
+
"url": "string"
|
|
123
|
+
},
|
|
124
|
+
"required": [
|
|
125
|
+
"expires_in_seconds",
|
|
126
|
+
"file_id",
|
|
127
|
+
"url"
|
|
128
|
+
]
|
|
129
|
+
},
|
|
130
|
+
"PresignedUploadRequest": {
|
|
131
|
+
"properties": {
|
|
132
|
+
"filename": "string",
|
|
133
|
+
"mime_type": "string"
|
|
134
|
+
},
|
|
135
|
+
"required": [
|
|
136
|
+
"filename",
|
|
137
|
+
"mime_type"
|
|
138
|
+
]
|
|
139
|
+
},
|
|
140
|
+
"PresignedUploadResponse": {
|
|
141
|
+
"properties": {
|
|
142
|
+
"expires_in_seconds": "integer",
|
|
143
|
+
"fields": "object",
|
|
144
|
+
"file_id": "string",
|
|
145
|
+
"url": "string"
|
|
146
|
+
},
|
|
147
|
+
"required": [
|
|
148
|
+
"expires_in_seconds",
|
|
149
|
+
"fields",
|
|
150
|
+
"file_id",
|
|
151
|
+
"url"
|
|
152
|
+
]
|
|
153
|
+
},
|
|
154
|
+
"StorageUploadResponse": {
|
|
155
|
+
"properties": {
|
|
156
|
+
"file_id": "string"
|
|
157
|
+
},
|
|
158
|
+
"required": [
|
|
159
|
+
"file_id"
|
|
160
|
+
]
|
|
161
|
+
},
|
|
162
|
+
"TaskDataFailed": {
|
|
163
|
+
"properties": {
|
|
164
|
+
"end_date": "anyOf[string,null]",
|
|
165
|
+
"error_message": "anyOf[string,null]",
|
|
166
|
+
"start_date": "anyOf[string,null]",
|
|
167
|
+
"status": "ref:TaskStatus",
|
|
168
|
+
"submission_date": "anyOf[string,null]",
|
|
169
|
+
"task_id": "string"
|
|
170
|
+
},
|
|
171
|
+
"required": [
|
|
172
|
+
"error_message",
|
|
173
|
+
"task_id"
|
|
174
|
+
]
|
|
175
|
+
},
|
|
176
|
+
"TaskDataPending": {
|
|
177
|
+
"properties": {
|
|
178
|
+
"status": "ref:TaskStatus",
|
|
179
|
+
"submission_date": "anyOf[string,null]",
|
|
180
|
+
"task_id": "string",
|
|
181
|
+
"task_position": "anyOf[integer,null]"
|
|
182
|
+
},
|
|
183
|
+
"required": [
|
|
184
|
+
"task_id"
|
|
185
|
+
]
|
|
186
|
+
},
|
|
187
|
+
"TaskDataProgress": {
|
|
188
|
+
"properties": {
|
|
189
|
+
"progress": "anyOf[number,null]",
|
|
190
|
+
"start_date": "anyOf[string,null]",
|
|
191
|
+
"status": "ref:TaskStatus",
|
|
192
|
+
"submission_date": "anyOf[string,null]",
|
|
193
|
+
"task_id": "string"
|
|
194
|
+
},
|
|
195
|
+
"required": [
|
|
196
|
+
"task_id"
|
|
197
|
+
]
|
|
198
|
+
},
|
|
199
|
+
"TaskDataSuccess": {
|
|
200
|
+
"properties": {
|
|
201
|
+
"end_date": "anyOf[string,null]",
|
|
202
|
+
"result": "anyOf[unknown,null]",
|
|
203
|
+
"start_date": "anyOf[string,null]",
|
|
204
|
+
"status": "ref:TaskStatus",
|
|
205
|
+
"submission_date": "anyOf[string,null]",
|
|
206
|
+
"task_id": "string"
|
|
207
|
+
},
|
|
208
|
+
"required": [
|
|
209
|
+
"task_id"
|
|
210
|
+
]
|
|
211
|
+
},
|
|
212
|
+
"TaskRequest": {
|
|
213
|
+
"properties": {
|
|
214
|
+
"body": "object",
|
|
215
|
+
"callback": "anyOf[ref:Callback,null]"
|
|
216
|
+
},
|
|
217
|
+
"required": [
|
|
218
|
+
"body"
|
|
219
|
+
]
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "brio-sdk"
|
|
7
|
+
# Gérée par release-please (composant `brio-sdk` du monorepo) : le premier tag
|
|
8
|
+
# `brio-sdk-v0.1.0` publiera la première version sur PyPI. Ne pas éditer à la main.
|
|
9
|
+
version = "0.1.0"
|
|
10
|
+
description = "Client SDK (sync + async) des services IA BRIO — soumission de tâches, polling, fichiers."
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
license = { text = "MIT" }
|
|
13
|
+
requires-python = ">=3.11"
|
|
14
|
+
keywords = ["brio", "async-api", "sdk", "minint"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Typing :: Typed",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"httpx>=0.28",
|
|
25
|
+
"pydantic>=2.10",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Repository = "https://github.com/IA-Generative/async-api"
|
|
30
|
+
Documentation = "https://github.com/IA-Generative/async-api/tree/main/clients/python/brio-sdk"
|
|
31
|
+
|
|
32
|
+
[tool.hatch.build.targets.wheel]
|
|
33
|
+
packages = ["src/brio_sdk"]
|
|
34
|
+
|
|
35
|
+
[dependency-groups]
|
|
36
|
+
dev = [
|
|
37
|
+
"pytest>=8.4.2",
|
|
38
|
+
"pytest-asyncio>=0.24",
|
|
39
|
+
"ruff>=0.15.12",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
# Conventions alignées sur la racine du monorepo (cf. /pyproject.toml), à ceci près que
|
|
43
|
+
# la cible est py311 : plusieurs SI consommateurs ne sont pas en 3.13 (cf. #430).
|
|
44
|
+
[tool.ruff]
|
|
45
|
+
line-length = 120
|
|
46
|
+
indent-width = 4
|
|
47
|
+
target-version = "py311"
|
|
48
|
+
src = ["src", "tests"]
|
|
49
|
+
|
|
50
|
+
[tool.ruff.lint]
|
|
51
|
+
select = [
|
|
52
|
+
"E", "F", "ANN", "FBT", "COM", "B", "I", "UP", "S", "C4", "TID",
|
|
53
|
+
"SIM", "PL", "RUF", "ASYNC", "ICN", "PIE", "T20", "PYI", "PT",
|
|
54
|
+
"TC", "RET", "ARG", "PTH",
|
|
55
|
+
]
|
|
56
|
+
ignore = [
|
|
57
|
+
"S101", "B008", "PLR0913", "PLR2004", "RUF012",
|
|
58
|
+
# `Any` est assumé là où il l'est : payload d'erreur serveur, `result` d'une tâche,
|
|
59
|
+
# et passe-plat de kwargs vers httpx. Les typer plus serré serait faux.
|
|
60
|
+
"ANN401",
|
|
61
|
+
]
|
|
62
|
+
fixable = ["ALL"]
|
|
63
|
+
unfixable = []
|
|
64
|
+
|
|
65
|
+
[tool.ruff.lint.per-file-ignores]
|
|
66
|
+
"tests/*" = ["S", "ANN", "ARG", "PLR2004"]
|
|
67
|
+
"**/__init__.py" = ["F401"]
|
|
68
|
+
|
|
69
|
+
[tool.ruff.lint.isort]
|
|
70
|
+
known-first-party = ["brio_sdk"]
|
|
71
|
+
|
|
72
|
+
[tool.ruff.format]
|
|
73
|
+
quote-style = "double"
|
|
74
|
+
indent-style = "space"
|
|
75
|
+
|
|
76
|
+
[tool.pytest.ini_options]
|
|
77
|
+
asyncio_mode = "auto"
|
|
78
|
+
testpaths = ["tests"]
|