label-studio-sdk 1.0.10__py3-none-any.whl → 1.0.12__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 label-studio-sdk might be problematic. Click here for more details.
- label_studio_sdk/__init__.py +21 -1
- label_studio_sdk/_extensions/label_studio_tools/core/utils/json_schema.py +5 -0
- label_studio_sdk/base_client.py +8 -0
- label_studio_sdk/converter/converter.py +22 -0
- label_studio_sdk/converter/exports/brush_to_coco.py +332 -0
- label_studio_sdk/converter/main.py +14 -0
- label_studio_sdk/core/client_wrapper.py +34 -15
- label_studio_sdk/errors/__init__.py +3 -1
- label_studio_sdk/errors/not_found_error.py +9 -0
- label_studio_sdk/errors/unauthorized_error.py +9 -0
- label_studio_sdk/jwt_settings/__init__.py +2 -0
- label_studio_sdk/jwt_settings/client.py +259 -0
- label_studio_sdk/label_interface/control_tags.py +15 -2
- label_studio_sdk/label_interface/interface.py +80 -1
- label_studio_sdk/label_interface/object_tags.py +2 -2
- label_studio_sdk/projects/__init__.py +2 -1
- label_studio_sdk/projects/client.py +4 -0
- label_studio_sdk/projects/exports/client_ext.py +106 -40
- label_studio_sdk/projects/pauses/__init__.py +2 -0
- label_studio_sdk/projects/pauses/client.py +704 -0
- label_studio_sdk/projects/types/projects_update_response.py +10 -0
- label_studio_sdk/tokens/__init__.py +2 -0
- label_studio_sdk/tokens/client.py +610 -0
- label_studio_sdk/tokens/client_ext.py +94 -0
- label_studio_sdk/types/__init__.py +14 -0
- label_studio_sdk/types/access_token_response.py +22 -0
- label_studio_sdk/types/annotation.py +2 -1
- label_studio_sdk/types/annotation_completed_by.py +6 -0
- label_studio_sdk/types/api_token_response.py +32 -0
- label_studio_sdk/types/jwt_settings_response.py +32 -0
- label_studio_sdk/types/model_provider_connection_provider.py +1 -1
- label_studio_sdk/types/pause.py +34 -0
- label_studio_sdk/types/pause_paused_by.py +5 -0
- label_studio_sdk/types/project.py +10 -0
- label_studio_sdk/types/prompt_version_provider.py +1 -1
- label_studio_sdk/types/rotate_token_response.py +22 -0
- {label_studio_sdk-1.0.10.dist-info → label_studio_sdk-1.0.12.dist-info}/METADATA +3 -2
- {label_studio_sdk-1.0.10.dist-info → label_studio_sdk-1.0.12.dist-info}/RECORD +40 -23
- {label_studio_sdk-1.0.10.dist-info → label_studio_sdk-1.0.12.dist-info}/WHEEL +1 -1
- {label_studio_sdk-1.0.10.dist-info → label_studio_sdk-1.0.12.dist-info}/LICENSE +0 -0
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
# This file was auto-generated by Fern from our API Definition.
|
|
2
|
+
|
|
3
|
+
import typing
|
|
4
|
+
from ..core.client_wrapper import SyncClientWrapper
|
|
5
|
+
from ..core.request_options import RequestOptions
|
|
6
|
+
from ..errors.not_found_error import NotFoundError
|
|
7
|
+
from ..core.pydantic_utilities import parse_obj_as
|
|
8
|
+
from json.decoder import JSONDecodeError
|
|
9
|
+
from ..core.api_error import ApiError
|
|
10
|
+
from ..types.api_token_response import ApiTokenResponse
|
|
11
|
+
from ..types.access_token_response import AccessTokenResponse
|
|
12
|
+
from ..errors.unauthorized_error import UnauthorizedError
|
|
13
|
+
from ..types.rotate_token_response import RotateTokenResponse
|
|
14
|
+
from ..errors.bad_request_error import BadRequestError
|
|
15
|
+
from ..core.client_wrapper import AsyncClientWrapper
|
|
16
|
+
|
|
17
|
+
# this is used as the default value for optional parameters
|
|
18
|
+
OMIT = typing.cast(typing.Any, ...)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TokensClient:
|
|
22
|
+
def __init__(self, *, client_wrapper: SyncClientWrapper):
|
|
23
|
+
self._client_wrapper = client_wrapper
|
|
24
|
+
|
|
25
|
+
def blacklist(self, *, refresh: str, request_options: typing.Optional[RequestOptions] = None) -> None:
|
|
26
|
+
"""
|
|
27
|
+
Blacklist a refresh token to prevent its future use.
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
refresh : str
|
|
32
|
+
JWT refresh token
|
|
33
|
+
|
|
34
|
+
request_options : typing.Optional[RequestOptions]
|
|
35
|
+
Request-specific configuration.
|
|
36
|
+
|
|
37
|
+
Returns
|
|
38
|
+
-------
|
|
39
|
+
None
|
|
40
|
+
|
|
41
|
+
Examples
|
|
42
|
+
--------
|
|
43
|
+
from label_studio_sdk import LabelStudio
|
|
44
|
+
|
|
45
|
+
client = LabelStudio(
|
|
46
|
+
api_key="YOUR_API_KEY",
|
|
47
|
+
)
|
|
48
|
+
client.tokens.blacklist(
|
|
49
|
+
refresh="refresh",
|
|
50
|
+
)
|
|
51
|
+
"""
|
|
52
|
+
_response = self._client_wrapper.httpx_client.request(
|
|
53
|
+
"api/token/blacklist",
|
|
54
|
+
method="POST",
|
|
55
|
+
json={
|
|
56
|
+
"refresh": refresh,
|
|
57
|
+
},
|
|
58
|
+
headers={
|
|
59
|
+
"content-type": "application/json",
|
|
60
|
+
},
|
|
61
|
+
request_options=request_options,
|
|
62
|
+
omit=OMIT,
|
|
63
|
+
)
|
|
64
|
+
try:
|
|
65
|
+
if 200 <= _response.status_code < 300:
|
|
66
|
+
return
|
|
67
|
+
if _response.status_code == 404:
|
|
68
|
+
raise NotFoundError(
|
|
69
|
+
typing.cast(
|
|
70
|
+
typing.Optional[typing.Any],
|
|
71
|
+
parse_obj_as(
|
|
72
|
+
type_=typing.Optional[typing.Any], # type: ignore
|
|
73
|
+
object_=_response.json(),
|
|
74
|
+
),
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
_response_json = _response.json()
|
|
78
|
+
except JSONDecodeError:
|
|
79
|
+
raise ApiError(status_code=_response.status_code, body=_response.text)
|
|
80
|
+
raise ApiError(status_code=_response.status_code, body=_response_json)
|
|
81
|
+
|
|
82
|
+
def get(self, *, request_options: typing.Optional[RequestOptions] = None) -> typing.List[ApiTokenResponse]:
|
|
83
|
+
"""
|
|
84
|
+
List all API tokens for the current user.
|
|
85
|
+
|
|
86
|
+
Parameters
|
|
87
|
+
----------
|
|
88
|
+
request_options : typing.Optional[RequestOptions]
|
|
89
|
+
Request-specific configuration.
|
|
90
|
+
|
|
91
|
+
Returns
|
|
92
|
+
-------
|
|
93
|
+
typing.List[ApiTokenResponse]
|
|
94
|
+
List of API tokens retrieved successfully
|
|
95
|
+
|
|
96
|
+
Examples
|
|
97
|
+
--------
|
|
98
|
+
from label_studio_sdk import LabelStudio
|
|
99
|
+
|
|
100
|
+
client = LabelStudio(
|
|
101
|
+
api_key="YOUR_API_KEY",
|
|
102
|
+
)
|
|
103
|
+
client.tokens.get()
|
|
104
|
+
"""
|
|
105
|
+
_response = self._client_wrapper.httpx_client.request(
|
|
106
|
+
"api/token",
|
|
107
|
+
method="GET",
|
|
108
|
+
request_options=request_options,
|
|
109
|
+
)
|
|
110
|
+
try:
|
|
111
|
+
if 200 <= _response.status_code < 300:
|
|
112
|
+
return typing.cast(
|
|
113
|
+
typing.List[ApiTokenResponse],
|
|
114
|
+
parse_obj_as(
|
|
115
|
+
type_=typing.List[ApiTokenResponse], # type: ignore
|
|
116
|
+
object_=_response.json(),
|
|
117
|
+
),
|
|
118
|
+
)
|
|
119
|
+
_response_json = _response.json()
|
|
120
|
+
except JSONDecodeError:
|
|
121
|
+
raise ApiError(status_code=_response.status_code, body=_response.text)
|
|
122
|
+
raise ApiError(status_code=_response.status_code, body=_response_json)
|
|
123
|
+
|
|
124
|
+
def create(self, *, request_options: typing.Optional[RequestOptions] = None) -> ApiTokenResponse:
|
|
125
|
+
"""
|
|
126
|
+
Create a new API token for the current user.
|
|
127
|
+
|
|
128
|
+
Parameters
|
|
129
|
+
----------
|
|
130
|
+
request_options : typing.Optional[RequestOptions]
|
|
131
|
+
Request-specific configuration.
|
|
132
|
+
|
|
133
|
+
Returns
|
|
134
|
+
-------
|
|
135
|
+
ApiTokenResponse
|
|
136
|
+
Token created successfully
|
|
137
|
+
|
|
138
|
+
Examples
|
|
139
|
+
--------
|
|
140
|
+
from label_studio_sdk import LabelStudio
|
|
141
|
+
|
|
142
|
+
client = LabelStudio(
|
|
143
|
+
api_key="YOUR_API_KEY",
|
|
144
|
+
)
|
|
145
|
+
client.tokens.create()
|
|
146
|
+
"""
|
|
147
|
+
_response = self._client_wrapper.httpx_client.request(
|
|
148
|
+
"api/token",
|
|
149
|
+
method="POST",
|
|
150
|
+
request_options=request_options,
|
|
151
|
+
)
|
|
152
|
+
try:
|
|
153
|
+
if 200 <= _response.status_code < 300:
|
|
154
|
+
return typing.cast(
|
|
155
|
+
ApiTokenResponse,
|
|
156
|
+
parse_obj_as(
|
|
157
|
+
type_=ApiTokenResponse, # type: ignore
|
|
158
|
+
object_=_response.json(),
|
|
159
|
+
),
|
|
160
|
+
)
|
|
161
|
+
_response_json = _response.json()
|
|
162
|
+
except JSONDecodeError:
|
|
163
|
+
raise ApiError(status_code=_response.status_code, body=_response.text)
|
|
164
|
+
raise ApiError(status_code=_response.status_code, body=_response_json)
|
|
165
|
+
|
|
166
|
+
def refresh(self, *, refresh: str, request_options: typing.Optional[RequestOptions] = None) -> AccessTokenResponse:
|
|
167
|
+
"""
|
|
168
|
+
Get a new access token, using a refresh token.
|
|
169
|
+
|
|
170
|
+
Parameters
|
|
171
|
+
----------
|
|
172
|
+
refresh : str
|
|
173
|
+
JWT refresh token
|
|
174
|
+
|
|
175
|
+
request_options : typing.Optional[RequestOptions]
|
|
176
|
+
Request-specific configuration.
|
|
177
|
+
|
|
178
|
+
Returns
|
|
179
|
+
-------
|
|
180
|
+
AccessTokenResponse
|
|
181
|
+
New access token created successfully
|
|
182
|
+
|
|
183
|
+
Examples
|
|
184
|
+
--------
|
|
185
|
+
from label_studio_sdk import LabelStudio
|
|
186
|
+
|
|
187
|
+
client = LabelStudio(
|
|
188
|
+
api_key="YOUR_API_KEY",
|
|
189
|
+
)
|
|
190
|
+
client.tokens.refresh(
|
|
191
|
+
refresh="refresh",
|
|
192
|
+
)
|
|
193
|
+
"""
|
|
194
|
+
_response = self._client_wrapper.httpx_client.request(
|
|
195
|
+
"api/token/refresh",
|
|
196
|
+
method="POST",
|
|
197
|
+
json={
|
|
198
|
+
"refresh": refresh,
|
|
199
|
+
},
|
|
200
|
+
headers={
|
|
201
|
+
"content-type": "application/json",
|
|
202
|
+
},
|
|
203
|
+
request_options=request_options,
|
|
204
|
+
omit=OMIT,
|
|
205
|
+
)
|
|
206
|
+
try:
|
|
207
|
+
if 200 <= _response.status_code < 300:
|
|
208
|
+
return typing.cast(
|
|
209
|
+
AccessTokenResponse,
|
|
210
|
+
parse_obj_as(
|
|
211
|
+
type_=AccessTokenResponse, # type: ignore
|
|
212
|
+
object_=_response.json(),
|
|
213
|
+
),
|
|
214
|
+
)
|
|
215
|
+
if _response.status_code == 401:
|
|
216
|
+
raise UnauthorizedError(
|
|
217
|
+
typing.cast(
|
|
218
|
+
typing.Optional[typing.Any],
|
|
219
|
+
parse_obj_as(
|
|
220
|
+
type_=typing.Optional[typing.Any], # type: ignore
|
|
221
|
+
object_=_response.json(),
|
|
222
|
+
),
|
|
223
|
+
)
|
|
224
|
+
)
|
|
225
|
+
_response_json = _response.json()
|
|
226
|
+
except JSONDecodeError:
|
|
227
|
+
raise ApiError(status_code=_response.status_code, body=_response.text)
|
|
228
|
+
raise ApiError(status_code=_response.status_code, body=_response_json)
|
|
229
|
+
|
|
230
|
+
def rotate(self, *, refresh: str, request_options: typing.Optional[RequestOptions] = None) -> RotateTokenResponse:
|
|
231
|
+
"""
|
|
232
|
+
Blacklist existing refresh token, and get a new refresh token.
|
|
233
|
+
|
|
234
|
+
Parameters
|
|
235
|
+
----------
|
|
236
|
+
refresh : str
|
|
237
|
+
JWT refresh token
|
|
238
|
+
|
|
239
|
+
request_options : typing.Optional[RequestOptions]
|
|
240
|
+
Request-specific configuration.
|
|
241
|
+
|
|
242
|
+
Returns
|
|
243
|
+
-------
|
|
244
|
+
RotateTokenResponse
|
|
245
|
+
Refresh token successfully rotated
|
|
246
|
+
|
|
247
|
+
Examples
|
|
248
|
+
--------
|
|
249
|
+
from label_studio_sdk import LabelStudio
|
|
250
|
+
|
|
251
|
+
client = LabelStudio(
|
|
252
|
+
api_key="YOUR_API_KEY",
|
|
253
|
+
)
|
|
254
|
+
client.tokens.rotate(
|
|
255
|
+
refresh="refresh",
|
|
256
|
+
)
|
|
257
|
+
"""
|
|
258
|
+
_response = self._client_wrapper.httpx_client.request(
|
|
259
|
+
"api/token/rotate",
|
|
260
|
+
method="POST",
|
|
261
|
+
json={
|
|
262
|
+
"refresh": refresh,
|
|
263
|
+
},
|
|
264
|
+
headers={
|
|
265
|
+
"content-type": "application/json",
|
|
266
|
+
},
|
|
267
|
+
request_options=request_options,
|
|
268
|
+
omit=OMIT,
|
|
269
|
+
)
|
|
270
|
+
try:
|
|
271
|
+
if 200 <= _response.status_code < 300:
|
|
272
|
+
return typing.cast(
|
|
273
|
+
RotateTokenResponse,
|
|
274
|
+
parse_obj_as(
|
|
275
|
+
type_=RotateTokenResponse, # type: ignore
|
|
276
|
+
object_=_response.json(),
|
|
277
|
+
),
|
|
278
|
+
)
|
|
279
|
+
if _response.status_code == 400:
|
|
280
|
+
raise BadRequestError(
|
|
281
|
+
typing.cast(
|
|
282
|
+
typing.Optional[typing.Any],
|
|
283
|
+
parse_obj_as(
|
|
284
|
+
type_=typing.Optional[typing.Any], # type: ignore
|
|
285
|
+
object_=_response.json(),
|
|
286
|
+
),
|
|
287
|
+
)
|
|
288
|
+
)
|
|
289
|
+
_response_json = _response.json()
|
|
290
|
+
except JSONDecodeError:
|
|
291
|
+
raise ApiError(status_code=_response.status_code, body=_response.text)
|
|
292
|
+
raise ApiError(status_code=_response.status_code, body=_response_json)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class AsyncTokensClient:
|
|
296
|
+
def __init__(self, *, client_wrapper: AsyncClientWrapper):
|
|
297
|
+
self._client_wrapper = client_wrapper
|
|
298
|
+
|
|
299
|
+
async def blacklist(self, *, refresh: str, request_options: typing.Optional[RequestOptions] = None) -> None:
|
|
300
|
+
"""
|
|
301
|
+
Blacklist a refresh token to prevent its future use.
|
|
302
|
+
|
|
303
|
+
Parameters
|
|
304
|
+
----------
|
|
305
|
+
refresh : str
|
|
306
|
+
JWT refresh token
|
|
307
|
+
|
|
308
|
+
request_options : typing.Optional[RequestOptions]
|
|
309
|
+
Request-specific configuration.
|
|
310
|
+
|
|
311
|
+
Returns
|
|
312
|
+
-------
|
|
313
|
+
None
|
|
314
|
+
|
|
315
|
+
Examples
|
|
316
|
+
--------
|
|
317
|
+
import asyncio
|
|
318
|
+
|
|
319
|
+
from label_studio_sdk import AsyncLabelStudio
|
|
320
|
+
|
|
321
|
+
client = AsyncLabelStudio(
|
|
322
|
+
api_key="YOUR_API_KEY",
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
async def main() -> None:
|
|
327
|
+
await client.tokens.blacklist(
|
|
328
|
+
refresh="refresh",
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
asyncio.run(main())
|
|
333
|
+
"""
|
|
334
|
+
_response = await self._client_wrapper.httpx_client.request(
|
|
335
|
+
"api/token/blacklist",
|
|
336
|
+
method="POST",
|
|
337
|
+
json={
|
|
338
|
+
"refresh": refresh,
|
|
339
|
+
},
|
|
340
|
+
headers={
|
|
341
|
+
"content-type": "application/json",
|
|
342
|
+
},
|
|
343
|
+
request_options=request_options,
|
|
344
|
+
omit=OMIT,
|
|
345
|
+
)
|
|
346
|
+
try:
|
|
347
|
+
if 200 <= _response.status_code < 300:
|
|
348
|
+
return
|
|
349
|
+
if _response.status_code == 404:
|
|
350
|
+
raise NotFoundError(
|
|
351
|
+
typing.cast(
|
|
352
|
+
typing.Optional[typing.Any],
|
|
353
|
+
parse_obj_as(
|
|
354
|
+
type_=typing.Optional[typing.Any], # type: ignore
|
|
355
|
+
object_=_response.json(),
|
|
356
|
+
),
|
|
357
|
+
)
|
|
358
|
+
)
|
|
359
|
+
_response_json = _response.json()
|
|
360
|
+
except JSONDecodeError:
|
|
361
|
+
raise ApiError(status_code=_response.status_code, body=_response.text)
|
|
362
|
+
raise ApiError(status_code=_response.status_code, body=_response_json)
|
|
363
|
+
|
|
364
|
+
async def get(self, *, request_options: typing.Optional[RequestOptions] = None) -> typing.List[ApiTokenResponse]:
|
|
365
|
+
"""
|
|
366
|
+
List all API tokens for the current user.
|
|
367
|
+
|
|
368
|
+
Parameters
|
|
369
|
+
----------
|
|
370
|
+
request_options : typing.Optional[RequestOptions]
|
|
371
|
+
Request-specific configuration.
|
|
372
|
+
|
|
373
|
+
Returns
|
|
374
|
+
-------
|
|
375
|
+
typing.List[ApiTokenResponse]
|
|
376
|
+
List of API tokens retrieved successfully
|
|
377
|
+
|
|
378
|
+
Examples
|
|
379
|
+
--------
|
|
380
|
+
import asyncio
|
|
381
|
+
|
|
382
|
+
from label_studio_sdk import AsyncLabelStudio
|
|
383
|
+
|
|
384
|
+
client = AsyncLabelStudio(
|
|
385
|
+
api_key="YOUR_API_KEY",
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
async def main() -> None:
|
|
390
|
+
await client.tokens.get()
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
asyncio.run(main())
|
|
394
|
+
"""
|
|
395
|
+
_response = await self._client_wrapper.httpx_client.request(
|
|
396
|
+
"api/token",
|
|
397
|
+
method="GET",
|
|
398
|
+
request_options=request_options,
|
|
399
|
+
)
|
|
400
|
+
try:
|
|
401
|
+
if 200 <= _response.status_code < 300:
|
|
402
|
+
return typing.cast(
|
|
403
|
+
typing.List[ApiTokenResponse],
|
|
404
|
+
parse_obj_as(
|
|
405
|
+
type_=typing.List[ApiTokenResponse], # type: ignore
|
|
406
|
+
object_=_response.json(),
|
|
407
|
+
),
|
|
408
|
+
)
|
|
409
|
+
_response_json = _response.json()
|
|
410
|
+
except JSONDecodeError:
|
|
411
|
+
raise ApiError(status_code=_response.status_code, body=_response.text)
|
|
412
|
+
raise ApiError(status_code=_response.status_code, body=_response_json)
|
|
413
|
+
|
|
414
|
+
async def create(self, *, request_options: typing.Optional[RequestOptions] = None) -> ApiTokenResponse:
|
|
415
|
+
"""
|
|
416
|
+
Create a new API token for the current user.
|
|
417
|
+
|
|
418
|
+
Parameters
|
|
419
|
+
----------
|
|
420
|
+
request_options : typing.Optional[RequestOptions]
|
|
421
|
+
Request-specific configuration.
|
|
422
|
+
|
|
423
|
+
Returns
|
|
424
|
+
-------
|
|
425
|
+
ApiTokenResponse
|
|
426
|
+
Token created successfully
|
|
427
|
+
|
|
428
|
+
Examples
|
|
429
|
+
--------
|
|
430
|
+
import asyncio
|
|
431
|
+
|
|
432
|
+
from label_studio_sdk import AsyncLabelStudio
|
|
433
|
+
|
|
434
|
+
client = AsyncLabelStudio(
|
|
435
|
+
api_key="YOUR_API_KEY",
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
async def main() -> None:
|
|
440
|
+
await client.tokens.create()
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
asyncio.run(main())
|
|
444
|
+
"""
|
|
445
|
+
_response = await self._client_wrapper.httpx_client.request(
|
|
446
|
+
"api/token",
|
|
447
|
+
method="POST",
|
|
448
|
+
request_options=request_options,
|
|
449
|
+
)
|
|
450
|
+
try:
|
|
451
|
+
if 200 <= _response.status_code < 300:
|
|
452
|
+
return typing.cast(
|
|
453
|
+
ApiTokenResponse,
|
|
454
|
+
parse_obj_as(
|
|
455
|
+
type_=ApiTokenResponse, # type: ignore
|
|
456
|
+
object_=_response.json(),
|
|
457
|
+
),
|
|
458
|
+
)
|
|
459
|
+
_response_json = _response.json()
|
|
460
|
+
except JSONDecodeError:
|
|
461
|
+
raise ApiError(status_code=_response.status_code, body=_response.text)
|
|
462
|
+
raise ApiError(status_code=_response.status_code, body=_response_json)
|
|
463
|
+
|
|
464
|
+
async def refresh(
|
|
465
|
+
self, *, refresh: str, request_options: typing.Optional[RequestOptions] = None
|
|
466
|
+
) -> AccessTokenResponse:
|
|
467
|
+
"""
|
|
468
|
+
Get a new access token, using a refresh token.
|
|
469
|
+
|
|
470
|
+
Parameters
|
|
471
|
+
----------
|
|
472
|
+
refresh : str
|
|
473
|
+
JWT refresh token
|
|
474
|
+
|
|
475
|
+
request_options : typing.Optional[RequestOptions]
|
|
476
|
+
Request-specific configuration.
|
|
477
|
+
|
|
478
|
+
Returns
|
|
479
|
+
-------
|
|
480
|
+
AccessTokenResponse
|
|
481
|
+
New access token created successfully
|
|
482
|
+
|
|
483
|
+
Examples
|
|
484
|
+
--------
|
|
485
|
+
import asyncio
|
|
486
|
+
|
|
487
|
+
from label_studio_sdk import AsyncLabelStudio
|
|
488
|
+
|
|
489
|
+
client = AsyncLabelStudio(
|
|
490
|
+
api_key="YOUR_API_KEY",
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
async def main() -> None:
|
|
495
|
+
await client.tokens.refresh(
|
|
496
|
+
refresh="refresh",
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
asyncio.run(main())
|
|
501
|
+
"""
|
|
502
|
+
_response = await self._client_wrapper.httpx_client.request(
|
|
503
|
+
"api/token/refresh",
|
|
504
|
+
method="POST",
|
|
505
|
+
json={
|
|
506
|
+
"refresh": refresh,
|
|
507
|
+
},
|
|
508
|
+
headers={
|
|
509
|
+
"content-type": "application/json",
|
|
510
|
+
},
|
|
511
|
+
request_options=request_options,
|
|
512
|
+
omit=OMIT,
|
|
513
|
+
)
|
|
514
|
+
try:
|
|
515
|
+
if 200 <= _response.status_code < 300:
|
|
516
|
+
return typing.cast(
|
|
517
|
+
AccessTokenResponse,
|
|
518
|
+
parse_obj_as(
|
|
519
|
+
type_=AccessTokenResponse, # type: ignore
|
|
520
|
+
object_=_response.json(),
|
|
521
|
+
),
|
|
522
|
+
)
|
|
523
|
+
if _response.status_code == 401:
|
|
524
|
+
raise UnauthorizedError(
|
|
525
|
+
typing.cast(
|
|
526
|
+
typing.Optional[typing.Any],
|
|
527
|
+
parse_obj_as(
|
|
528
|
+
type_=typing.Optional[typing.Any], # type: ignore
|
|
529
|
+
object_=_response.json(),
|
|
530
|
+
),
|
|
531
|
+
)
|
|
532
|
+
)
|
|
533
|
+
_response_json = _response.json()
|
|
534
|
+
except JSONDecodeError:
|
|
535
|
+
raise ApiError(status_code=_response.status_code, body=_response.text)
|
|
536
|
+
raise ApiError(status_code=_response.status_code, body=_response_json)
|
|
537
|
+
|
|
538
|
+
async def rotate(
|
|
539
|
+
self, *, refresh: str, request_options: typing.Optional[RequestOptions] = None
|
|
540
|
+
) -> RotateTokenResponse:
|
|
541
|
+
"""
|
|
542
|
+
Blacklist existing refresh token, and get a new refresh token.
|
|
543
|
+
|
|
544
|
+
Parameters
|
|
545
|
+
----------
|
|
546
|
+
refresh : str
|
|
547
|
+
JWT refresh token
|
|
548
|
+
|
|
549
|
+
request_options : typing.Optional[RequestOptions]
|
|
550
|
+
Request-specific configuration.
|
|
551
|
+
|
|
552
|
+
Returns
|
|
553
|
+
-------
|
|
554
|
+
RotateTokenResponse
|
|
555
|
+
Refresh token successfully rotated
|
|
556
|
+
|
|
557
|
+
Examples
|
|
558
|
+
--------
|
|
559
|
+
import asyncio
|
|
560
|
+
|
|
561
|
+
from label_studio_sdk import AsyncLabelStudio
|
|
562
|
+
|
|
563
|
+
client = AsyncLabelStudio(
|
|
564
|
+
api_key="YOUR_API_KEY",
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
async def main() -> None:
|
|
569
|
+
await client.tokens.rotate(
|
|
570
|
+
refresh="refresh",
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
asyncio.run(main())
|
|
575
|
+
"""
|
|
576
|
+
_response = await self._client_wrapper.httpx_client.request(
|
|
577
|
+
"api/token/rotate",
|
|
578
|
+
method="POST",
|
|
579
|
+
json={
|
|
580
|
+
"refresh": refresh,
|
|
581
|
+
},
|
|
582
|
+
headers={
|
|
583
|
+
"content-type": "application/json",
|
|
584
|
+
},
|
|
585
|
+
request_options=request_options,
|
|
586
|
+
omit=OMIT,
|
|
587
|
+
)
|
|
588
|
+
try:
|
|
589
|
+
if 200 <= _response.status_code < 300:
|
|
590
|
+
return typing.cast(
|
|
591
|
+
RotateTokenResponse,
|
|
592
|
+
parse_obj_as(
|
|
593
|
+
type_=RotateTokenResponse, # type: ignore
|
|
594
|
+
object_=_response.json(),
|
|
595
|
+
),
|
|
596
|
+
)
|
|
597
|
+
if _response.status_code == 400:
|
|
598
|
+
raise BadRequestError(
|
|
599
|
+
typing.cast(
|
|
600
|
+
typing.Optional[typing.Any],
|
|
601
|
+
parse_obj_as(
|
|
602
|
+
type_=typing.Optional[typing.Any], # type: ignore
|
|
603
|
+
object_=_response.json(),
|
|
604
|
+
),
|
|
605
|
+
)
|
|
606
|
+
)
|
|
607
|
+
_response_json = _response.json()
|
|
608
|
+
except JSONDecodeError:
|
|
609
|
+
raise ApiError(status_code=_response.status_code, body=_response.text)
|
|
610
|
+
raise ApiError(status_code=_response.status_code, body=_response_json)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import typing
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
import jwt
|
|
7
|
+
|
|
8
|
+
from ..core.api_error import ApiError
|
|
9
|
+
from ..types.access_token_response import AccessTokenResponse
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TokensClientExt:
|
|
13
|
+
"""Client for managing authentication tokens."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, base_url: str, api_key: str):
|
|
16
|
+
self._base_url = base_url
|
|
17
|
+
self._api_key = api_key
|
|
18
|
+
self._use_legacy_token = not self._is_valid_jwt_token(api_key, raise_if_expired=True)
|
|
19
|
+
|
|
20
|
+
# cache state for access token when using jwt-based api_key
|
|
21
|
+
self._access_token: typing.Optional[str] = None
|
|
22
|
+
self._access_token_expiration: typing.Optional[datetime] = None
|
|
23
|
+
# Used to keep simultaneous refresh requests from spamming refresh endpoint
|
|
24
|
+
self._token_refresh_lock = threading.Lock()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _is_valid_jwt_token(self, token: str, raise_if_expired: bool = False) -> bool:
|
|
28
|
+
"""Check if a token is a valid JWT token by attempting to decode its header and check expiration."""
|
|
29
|
+
try:
|
|
30
|
+
decoded = jwt.decode(token, options={"verify_signature": False})
|
|
31
|
+
except jwt.InvalidTokenError:
|
|
32
|
+
# presumably a lagacy token
|
|
33
|
+
return False
|
|
34
|
+
expiration = decoded.get("exp")
|
|
35
|
+
if expiration is None:
|
|
36
|
+
raise ApiError(
|
|
37
|
+
status_code=401,
|
|
38
|
+
body={"detail": "API key does not have an expiration set, and is not valid. Please obtain a new refresh token."}
|
|
39
|
+
)
|
|
40
|
+
expiration_time = datetime.fromtimestamp(expiration, timezone.utc)
|
|
41
|
+
if expiration_time < datetime.now(timezone.utc):
|
|
42
|
+
if raise_if_expired:
|
|
43
|
+
raise ApiError(
|
|
44
|
+
status_code=401,
|
|
45
|
+
body={"detail": "API key has expired. Please obtain a new refresh token."}
|
|
46
|
+
)
|
|
47
|
+
else:
|
|
48
|
+
return False
|
|
49
|
+
return True
|
|
50
|
+
|
|
51
|
+
def _set_access_token(self, token: str) -> None:
|
|
52
|
+
"""Set the access token and cache its expiration time."""
|
|
53
|
+
try:
|
|
54
|
+
decoded = jwt.decode(token, options={"verify_signature": False})
|
|
55
|
+
expiration = decoded.get("exp")
|
|
56
|
+
if expiration is not None:
|
|
57
|
+
self._access_token_expiration = datetime.fromtimestamp(expiration, timezone.utc)
|
|
58
|
+
except jwt.InvalidTokenError:
|
|
59
|
+
pass
|
|
60
|
+
self._access_token = token
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def api_key(self) -> str:
|
|
64
|
+
"""Get the current access token, refreshing if necessary."""
|
|
65
|
+
# Legacy tokens: just return the API key directly
|
|
66
|
+
if self._use_legacy_token:
|
|
67
|
+
return self._api_key
|
|
68
|
+
|
|
69
|
+
# JWT tokens: handle refresh if needed
|
|
70
|
+
if (not self._access_token) or (not self._is_valid_jwt_token(self._access_token)):
|
|
71
|
+
with self._token_refresh_lock:
|
|
72
|
+
# Check again after acquiring lock, in case another invocation already refreshed
|
|
73
|
+
if (not self._access_token) or (not self._is_valid_jwt_token(self._access_token)):
|
|
74
|
+
token_response = self.refresh()
|
|
75
|
+
self._set_access_token(token_response.access)
|
|
76
|
+
|
|
77
|
+
return self._access_token
|
|
78
|
+
|
|
79
|
+
def refresh(self) -> AccessTokenResponse:
|
|
80
|
+
"""Refresh the access token and return the token response."""
|
|
81
|
+
# We don't do this often, just use a separate httpx client for simplicity here
|
|
82
|
+
# (avoids complicated state management and sync vs async handling)
|
|
83
|
+
with httpx.Client() as sync_client:
|
|
84
|
+
response = sync_client.request(
|
|
85
|
+
method="POST",
|
|
86
|
+
url=f"{self._base_url}/api/token/refresh/",
|
|
87
|
+
json={"refresh": self._api_key},
|
|
88
|
+
headers={"Content-Type": "application/json"},
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if response.status_code == 200:
|
|
92
|
+
return AccessTokenResponse.parse_obj(response.json())
|
|
93
|
+
else:
|
|
94
|
+
raise ApiError(status_code=response.status_code, body=response.json())
|