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.
@@ -0,0 +1,9 @@
1
+ .venv/
2
+ dist/
3
+ build/
4
+ *.egg-info/
5
+ __pycache__/
6
+ .pytest_cache/
7
+ .ruff_cache/
8
+ .coverage
9
+ htmlcov/
@@ -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))
@@ -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).
@@ -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"]