dataspace-sdk 0.4.2__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.
- dataspace_sdk/__init__.py +18 -0
- dataspace_sdk/__version__.py +3 -0
- dataspace_sdk/auth.py +470 -0
- dataspace_sdk/base.py +160 -0
- dataspace_sdk/client.py +206 -0
- dataspace_sdk/exceptions.py +36 -0
- dataspace_sdk/resources/__init__.py +8 -0
- dataspace_sdk/resources/aimodels.py +989 -0
- dataspace_sdk/resources/datasets.py +233 -0
- dataspace_sdk/resources/sectors.py +128 -0
- dataspace_sdk/resources/usecases.py +248 -0
- dataspace_sdk-0.4.2.dist-info/METADATA +551 -0
- dataspace_sdk-0.4.2.dist-info/RECORD +15 -0
- dataspace_sdk-0.4.2.dist-info/WHEEL +5 -0
- dataspace_sdk-0.4.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""DataSpace Python SDK for programmatic access to DataSpace resources."""
|
|
2
|
+
|
|
3
|
+
from dataspace_sdk.__version__ import __version__
|
|
4
|
+
from dataspace_sdk.client import DataSpaceClient
|
|
5
|
+
from dataspace_sdk.exceptions import (
|
|
6
|
+
DataSpaceAPIError,
|
|
7
|
+
DataSpaceAuthError,
|
|
8
|
+
DataSpaceNotFoundError,
|
|
9
|
+
DataSpaceValidationError,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"DataSpaceClient",
|
|
14
|
+
"DataSpaceAPIError",
|
|
15
|
+
"DataSpaceAuthError",
|
|
16
|
+
"DataSpaceNotFoundError",
|
|
17
|
+
"DataSpaceValidationError",
|
|
18
|
+
]
|
dataspace_sdk/auth.py
ADDED
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
"""Authentication module for DataSpace SDK."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import Any, Dict, Optional
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
from dataspace_sdk.exceptions import DataSpaceAuthError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AuthClient:
|
|
12
|
+
"""Handles authentication with DataSpace API."""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
base_url: str,
|
|
17
|
+
keycloak_url: Optional[str] = None,
|
|
18
|
+
keycloak_realm: Optional[str] = None,
|
|
19
|
+
keycloak_client_id: Optional[str] = None,
|
|
20
|
+
keycloak_client_secret: Optional[str] = None,
|
|
21
|
+
):
|
|
22
|
+
"""
|
|
23
|
+
Initialize the authentication client.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
base_url: Base URL of the DataSpace API
|
|
27
|
+
keycloak_url: Keycloak server URL (e.g., "https://opub-kc.civicdatalab.in")
|
|
28
|
+
keycloak_realm: Keycloak realm name (e.g., "DataSpace")
|
|
29
|
+
keycloak_client_id: Keycloak client ID (e.g., "dataspace")
|
|
30
|
+
keycloak_client_secret: Optional client secret for confidential clients
|
|
31
|
+
"""
|
|
32
|
+
self.base_url = base_url.rstrip("/")
|
|
33
|
+
self.keycloak_url = keycloak_url.rstrip("/") if keycloak_url else None
|
|
34
|
+
self.keycloak_realm = keycloak_realm
|
|
35
|
+
self.keycloak_client_id = keycloak_client_id
|
|
36
|
+
self.keycloak_client_secret = keycloak_client_secret
|
|
37
|
+
|
|
38
|
+
# Session state
|
|
39
|
+
self.access_token: Optional[str] = None
|
|
40
|
+
self.refresh_token: Optional[str] = None
|
|
41
|
+
self.keycloak_access_token: Optional[str] = None
|
|
42
|
+
self.keycloak_refresh_token: Optional[str] = None
|
|
43
|
+
self.token_expires_at: Optional[float] = None
|
|
44
|
+
self.user_info: Optional[Dict] = None
|
|
45
|
+
|
|
46
|
+
# Stored credentials for auto-relogin
|
|
47
|
+
self._username: Optional[str] = None
|
|
48
|
+
self._password: Optional[str] = None
|
|
49
|
+
|
|
50
|
+
def login(self, username: str, password: str) -> Dict[str, Any]:
|
|
51
|
+
"""
|
|
52
|
+
Login using username and password via Keycloak.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
username: User's username or email
|
|
56
|
+
password: User's password
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Dictionary containing user info and tokens
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
DataSpaceAuthError: If authentication fails
|
|
63
|
+
"""
|
|
64
|
+
if not all([self.keycloak_url, self.keycloak_realm, self.keycloak_client_id]):
|
|
65
|
+
raise DataSpaceAuthError(
|
|
66
|
+
"Keycloak configuration missing. Please provide keycloak_url, "
|
|
67
|
+
"keycloak_realm, and keycloak_client_id when initializing the client."
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Store credentials for auto-relogin
|
|
71
|
+
self._username = username
|
|
72
|
+
self._password = password
|
|
73
|
+
|
|
74
|
+
# Get Keycloak token
|
|
75
|
+
keycloak_token = self._get_keycloak_token(username, password)
|
|
76
|
+
|
|
77
|
+
# Login to DataSpace backend
|
|
78
|
+
return self._login_with_keycloak_token(keycloak_token)
|
|
79
|
+
|
|
80
|
+
def login_as_service_account(self) -> Dict[str, Any]:
|
|
81
|
+
"""
|
|
82
|
+
Login using client credentials (service account).
|
|
83
|
+
|
|
84
|
+
This method authenticates the client itself (not a user) using
|
|
85
|
+
the client_id and client_secret. Requires the Keycloak client
|
|
86
|
+
to have "Service Accounts Enabled".
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Dictionary containing user info and tokens
|
|
90
|
+
|
|
91
|
+
Raises:
|
|
92
|
+
DataSpaceAuthError: If authentication fails
|
|
93
|
+
"""
|
|
94
|
+
if not all(
|
|
95
|
+
[
|
|
96
|
+
self.keycloak_url,
|
|
97
|
+
self.keycloak_realm,
|
|
98
|
+
self.keycloak_client_id,
|
|
99
|
+
self.keycloak_client_secret,
|
|
100
|
+
]
|
|
101
|
+
):
|
|
102
|
+
raise DataSpaceAuthError(
|
|
103
|
+
"Service account authentication requires keycloak_url, "
|
|
104
|
+
"keycloak_realm, keycloak_client_id, and keycloak_client_secret."
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Get Keycloak token using client credentials
|
|
108
|
+
keycloak_token = self._get_service_account_token()
|
|
109
|
+
|
|
110
|
+
# Login to DataSpace backend
|
|
111
|
+
return self._login_with_keycloak_token(keycloak_token)
|
|
112
|
+
|
|
113
|
+
def _get_keycloak_token(self, username: str, password: str) -> str:
|
|
114
|
+
"""
|
|
115
|
+
Get Keycloak access token using username and password.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
username: User's username or email
|
|
119
|
+
password: User's password
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Keycloak access token
|
|
123
|
+
|
|
124
|
+
Raises:
|
|
125
|
+
DataSpaceAuthError: If authentication fails
|
|
126
|
+
"""
|
|
127
|
+
token_url = (
|
|
128
|
+
f"{self.keycloak_url}/auth/realms/{self.keycloak_realm}/"
|
|
129
|
+
f"protocol/openid-connect/token"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
data = {
|
|
133
|
+
"grant_type": "password",
|
|
134
|
+
"client_id": self.keycloak_client_id,
|
|
135
|
+
"username": username,
|
|
136
|
+
"password": password,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if self.keycloak_client_secret:
|
|
140
|
+
data["client_secret"] = self.keycloak_client_secret
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
response = requests.post(
|
|
144
|
+
token_url,
|
|
145
|
+
data=data,
|
|
146
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
if response.status_code == 200:
|
|
150
|
+
token_data = response.json()
|
|
151
|
+
self.keycloak_access_token = token_data.get("access_token")
|
|
152
|
+
self.keycloak_refresh_token = token_data.get("refresh_token")
|
|
153
|
+
|
|
154
|
+
# Calculate token expiration time
|
|
155
|
+
expires_in = token_data.get("expires_in", 300)
|
|
156
|
+
self.token_expires_at = time.time() + expires_in
|
|
157
|
+
|
|
158
|
+
if not self.keycloak_access_token:
|
|
159
|
+
raise DataSpaceAuthError("No access token in Keycloak response")
|
|
160
|
+
|
|
161
|
+
return self.keycloak_access_token
|
|
162
|
+
else:
|
|
163
|
+
error_data = response.json()
|
|
164
|
+
error_msg = error_data.get(
|
|
165
|
+
"error_description",
|
|
166
|
+
error_data.get("error", "Keycloak authentication failed"),
|
|
167
|
+
)
|
|
168
|
+
raise DataSpaceAuthError(
|
|
169
|
+
f"Keycloak login failed: {error_msg}",
|
|
170
|
+
status_code=response.status_code,
|
|
171
|
+
response=error_data,
|
|
172
|
+
)
|
|
173
|
+
except requests.RequestException as e:
|
|
174
|
+
raise DataSpaceAuthError(f"Network error during Keycloak authentication: {str(e)}")
|
|
175
|
+
|
|
176
|
+
def _get_service_account_token(self) -> str:
|
|
177
|
+
"""
|
|
178
|
+
Get Keycloak access token using client credentials (service account).
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Keycloak access token
|
|
182
|
+
|
|
183
|
+
Raises:
|
|
184
|
+
DataSpaceAuthError: If authentication fails
|
|
185
|
+
"""
|
|
186
|
+
token_url = (
|
|
187
|
+
f"{self.keycloak_url}/auth/realms/{self.keycloak_realm}/"
|
|
188
|
+
f"protocol/openid-connect/token"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
data = {
|
|
192
|
+
"grant_type": "client_credentials",
|
|
193
|
+
"client_id": self.keycloak_client_id,
|
|
194
|
+
"client_secret": self.keycloak_client_secret,
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
response = requests.post(
|
|
199
|
+
token_url,
|
|
200
|
+
data=data,
|
|
201
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
if response.status_code == 200:
|
|
205
|
+
token_data = response.json()
|
|
206
|
+
self.keycloak_access_token = token_data.get("access_token")
|
|
207
|
+
self.keycloak_refresh_token = token_data.get("refresh_token")
|
|
208
|
+
|
|
209
|
+
# Calculate token expiration time
|
|
210
|
+
expires_in = token_data.get("expires_in", 300)
|
|
211
|
+
self.token_expires_at = time.time() + expires_in
|
|
212
|
+
|
|
213
|
+
if not self.keycloak_access_token:
|
|
214
|
+
raise DataSpaceAuthError("No access token in Keycloak response")
|
|
215
|
+
|
|
216
|
+
return self.keycloak_access_token
|
|
217
|
+
else:
|
|
218
|
+
error_data = response.json()
|
|
219
|
+
error_msg = error_data.get(
|
|
220
|
+
"error_description",
|
|
221
|
+
error_data.get("error", "Service account authentication failed"),
|
|
222
|
+
)
|
|
223
|
+
raise DataSpaceAuthError(
|
|
224
|
+
f"Service account login failed: {error_msg}. "
|
|
225
|
+
f"Ensure 'Service Accounts Enabled' is ON in Keycloak client settings.",
|
|
226
|
+
status_code=response.status_code,
|
|
227
|
+
response=error_data,
|
|
228
|
+
)
|
|
229
|
+
except requests.RequestException as e:
|
|
230
|
+
raise DataSpaceAuthError(
|
|
231
|
+
f"Network error during service account authentication: {str(e)}"
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
def _refresh_keycloak_token(self) -> str:
|
|
235
|
+
"""
|
|
236
|
+
Refresh Keycloak access token using refresh token.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
New Keycloak access token
|
|
240
|
+
|
|
241
|
+
Raises:
|
|
242
|
+
DataSpaceAuthError: If token refresh fails
|
|
243
|
+
"""
|
|
244
|
+
if not self.keycloak_refresh_token:
|
|
245
|
+
# If no refresh token, try to relogin with stored credentials
|
|
246
|
+
if self._username and self._password:
|
|
247
|
+
return self._get_keycloak_token(self._username, self._password)
|
|
248
|
+
raise DataSpaceAuthError("No refresh token or credentials available")
|
|
249
|
+
|
|
250
|
+
token_url = (
|
|
251
|
+
f"{self.keycloak_url}/auth/realms/{self.keycloak_realm}/"
|
|
252
|
+
f"protocol/openid-connect/token"
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
data = {
|
|
256
|
+
"grant_type": "refresh_token",
|
|
257
|
+
"client_id": self.keycloak_client_id,
|
|
258
|
+
"refresh_token": self.keycloak_refresh_token,
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if self.keycloak_client_secret:
|
|
262
|
+
data["client_secret"] = self.keycloak_client_secret
|
|
263
|
+
|
|
264
|
+
try:
|
|
265
|
+
response = requests.post(
|
|
266
|
+
token_url,
|
|
267
|
+
data=data,
|
|
268
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
if response.status_code == 200:
|
|
272
|
+
token_data = response.json()
|
|
273
|
+
self.keycloak_access_token = token_data.get("access_token")
|
|
274
|
+
self.keycloak_refresh_token = token_data.get("refresh_token")
|
|
275
|
+
|
|
276
|
+
expires_in = token_data.get("expires_in", 300)
|
|
277
|
+
self.token_expires_at = time.time() + expires_in
|
|
278
|
+
|
|
279
|
+
if not self.keycloak_access_token:
|
|
280
|
+
raise DataSpaceAuthError("No access token in refresh response")
|
|
281
|
+
|
|
282
|
+
return self.keycloak_access_token
|
|
283
|
+
else:
|
|
284
|
+
# Refresh failed, try to relogin with stored credentials
|
|
285
|
+
if self._username and self._password:
|
|
286
|
+
return self._get_keycloak_token(self._username, self._password)
|
|
287
|
+
raise DataSpaceAuthError("Keycloak token refresh failed")
|
|
288
|
+
except requests.RequestException as e:
|
|
289
|
+
# Network error, try to relogin with stored credentials
|
|
290
|
+
if self._username and self._password:
|
|
291
|
+
return self._get_keycloak_token(self._username, self._password)
|
|
292
|
+
raise DataSpaceAuthError(f"Network error during token refresh: {str(e)}")
|
|
293
|
+
|
|
294
|
+
def _ensure_valid_keycloak_token(self) -> str:
|
|
295
|
+
"""
|
|
296
|
+
Ensure we have a valid Keycloak token, refreshing if necessary.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
Valid Keycloak access token
|
|
300
|
+
|
|
301
|
+
Raises:
|
|
302
|
+
DataSpaceAuthError: If unable to get valid token
|
|
303
|
+
"""
|
|
304
|
+
# Check if token is expired or about to expire (within 30 seconds)
|
|
305
|
+
if (
|
|
306
|
+
not self.keycloak_access_token
|
|
307
|
+
or not self.token_expires_at
|
|
308
|
+
or time.time() >= (self.token_expires_at - 30)
|
|
309
|
+
):
|
|
310
|
+
# Token expired or about to expire, refresh it
|
|
311
|
+
if self.keycloak_refresh_token or (self._username and self._password):
|
|
312
|
+
return self._refresh_keycloak_token()
|
|
313
|
+
raise DataSpaceAuthError("No valid token or credentials available")
|
|
314
|
+
|
|
315
|
+
return self.keycloak_access_token
|
|
316
|
+
|
|
317
|
+
def _login_with_keycloak_token(self, keycloak_token: str) -> Dict[str, Any]:
|
|
318
|
+
"""
|
|
319
|
+
Login using a Keycloak token.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
keycloak_token: Valid Keycloak access token
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
Dictionary containing user info and tokens
|
|
326
|
+
|
|
327
|
+
Raises:
|
|
328
|
+
DataSpaceAuthError: If authentication fails
|
|
329
|
+
"""
|
|
330
|
+
url = f"{self.base_url}/api/auth/keycloak/login/"
|
|
331
|
+
|
|
332
|
+
try:
|
|
333
|
+
response = requests.post(
|
|
334
|
+
url,
|
|
335
|
+
json={"token": keycloak_token},
|
|
336
|
+
headers={"Content-Type": "application/json"},
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
if response.status_code == 200:
|
|
340
|
+
data: Dict[str, Any] = response.json()
|
|
341
|
+
self.access_token = data.get("access")
|
|
342
|
+
self.refresh_token = data.get("refresh")
|
|
343
|
+
self.user_info = data.get("user")
|
|
344
|
+
return data
|
|
345
|
+
else:
|
|
346
|
+
error_msg = response.json().get("error", "Authentication failed")
|
|
347
|
+
raise DataSpaceAuthError(
|
|
348
|
+
error_msg,
|
|
349
|
+
status_code=response.status_code,
|
|
350
|
+
response=response.json(),
|
|
351
|
+
)
|
|
352
|
+
except requests.RequestException as e:
|
|
353
|
+
raise DataSpaceAuthError(f"Network error during authentication: {str(e)}")
|
|
354
|
+
|
|
355
|
+
def refresh_access_token(self) -> str:
|
|
356
|
+
"""
|
|
357
|
+
Refresh the access token using the refresh token.
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
New access token
|
|
361
|
+
|
|
362
|
+
Raises:
|
|
363
|
+
DataSpaceAuthError: If token refresh fails
|
|
364
|
+
"""
|
|
365
|
+
if not self.refresh_token:
|
|
366
|
+
raise DataSpaceAuthError("No refresh token available")
|
|
367
|
+
|
|
368
|
+
url = f"{self.base_url}/api/auth/token/refresh/"
|
|
369
|
+
|
|
370
|
+
try:
|
|
371
|
+
response = requests.post(
|
|
372
|
+
url,
|
|
373
|
+
json={"refresh": self.refresh_token},
|
|
374
|
+
headers={"Content-Type": "application/json"},
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
if response.status_code == 200:
|
|
378
|
+
data = response.json()
|
|
379
|
+
self.access_token = data.get("access")
|
|
380
|
+
if self.access_token is None:
|
|
381
|
+
raise DataSpaceAuthError("Token refresh returned no access token")
|
|
382
|
+
return self.access_token
|
|
383
|
+
else:
|
|
384
|
+
raise DataSpaceAuthError(
|
|
385
|
+
"Token refresh failed",
|
|
386
|
+
status_code=response.status_code,
|
|
387
|
+
response=response.json(),
|
|
388
|
+
)
|
|
389
|
+
except requests.RequestException as e:
|
|
390
|
+
raise DataSpaceAuthError(f"Network error during token refresh: {str(e)}")
|
|
391
|
+
|
|
392
|
+
def get_user_info(self) -> Dict[str, Any]:
|
|
393
|
+
"""
|
|
394
|
+
Get current user information.
|
|
395
|
+
|
|
396
|
+
Returns:
|
|
397
|
+
Dictionary containing user information
|
|
398
|
+
|
|
399
|
+
Raises:
|
|
400
|
+
DataSpaceAuthError: If request fails
|
|
401
|
+
"""
|
|
402
|
+
if not self.access_token:
|
|
403
|
+
raise DataSpaceAuthError("Not authenticated. Please login first.")
|
|
404
|
+
|
|
405
|
+
url = f"{self.base_url}/api/auth/user/info/"
|
|
406
|
+
|
|
407
|
+
try:
|
|
408
|
+
response = requests.get(
|
|
409
|
+
url,
|
|
410
|
+
headers=self._get_auth_headers(),
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
if response.status_code == 200:
|
|
414
|
+
user_info: Dict[str, Any] = response.json()
|
|
415
|
+
self.user_info = user_info
|
|
416
|
+
return self.user_info
|
|
417
|
+
else:
|
|
418
|
+
raise DataSpaceAuthError(
|
|
419
|
+
"Failed to get user info",
|
|
420
|
+
status_code=response.status_code,
|
|
421
|
+
response=response.json(),
|
|
422
|
+
)
|
|
423
|
+
except requests.RequestException as e:
|
|
424
|
+
raise DataSpaceAuthError(f"Network error getting user info: {str(e)}")
|
|
425
|
+
|
|
426
|
+
def _get_auth_headers(self) -> Dict[str, str]:
|
|
427
|
+
"""Get headers with authentication token."""
|
|
428
|
+
if not self.access_token:
|
|
429
|
+
return {}
|
|
430
|
+
return {"Authorization": f"Bearer {self.access_token}"}
|
|
431
|
+
|
|
432
|
+
def is_authenticated(self) -> bool:
|
|
433
|
+
"""Check if the client is authenticated."""
|
|
434
|
+
return self.access_token is not None
|
|
435
|
+
|
|
436
|
+
def ensure_authenticated(self) -> None:
|
|
437
|
+
"""
|
|
438
|
+
Ensure the client is authenticated, attempting auto-relogin if needed.
|
|
439
|
+
|
|
440
|
+
Raises:
|
|
441
|
+
DataSpaceAuthError: If unable to authenticate
|
|
442
|
+
"""
|
|
443
|
+
if not self.is_authenticated():
|
|
444
|
+
# Try to relogin with stored credentials
|
|
445
|
+
if self._username and self._password:
|
|
446
|
+
self.login(self._username, self._password)
|
|
447
|
+
else:
|
|
448
|
+
raise DataSpaceAuthError("Not authenticated. Please call login() first.")
|
|
449
|
+
|
|
450
|
+
def get_valid_token(self) -> str:
|
|
451
|
+
"""
|
|
452
|
+
Get a valid access token, refreshing if necessary.
|
|
453
|
+
|
|
454
|
+
Returns:
|
|
455
|
+
Valid access token
|
|
456
|
+
|
|
457
|
+
Raises:
|
|
458
|
+
DataSpaceAuthError: If unable to get valid token
|
|
459
|
+
"""
|
|
460
|
+
# First ensure we have a valid Keycloak token
|
|
461
|
+
if self.keycloak_url and self.keycloak_realm:
|
|
462
|
+
keycloak_token = self._ensure_valid_keycloak_token()
|
|
463
|
+
# Re-login to backend with fresh Keycloak token if needed
|
|
464
|
+
if not self.access_token:
|
|
465
|
+
self._login_with_keycloak_token(keycloak_token)
|
|
466
|
+
|
|
467
|
+
if not self.access_token:
|
|
468
|
+
raise DataSpaceAuthError("No access token available")
|
|
469
|
+
|
|
470
|
+
return self.access_token
|
dataspace_sdk/base.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Base client for making API requests."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
from dataspace_sdk.exceptions import (
|
|
8
|
+
DataSpaceAPIError,
|
|
9
|
+
DataSpaceAuthError,
|
|
10
|
+
DataSpaceNotFoundError,
|
|
11
|
+
DataSpaceValidationError,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BaseAPIClient:
|
|
16
|
+
"""Base client for making API requests to DataSpace."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, base_url: str, auth_client: Any = None):
|
|
19
|
+
"""
|
|
20
|
+
Initialize the base API client.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
base_url: Base URL of the DataSpace API
|
|
24
|
+
auth_client: Authentication client instance
|
|
25
|
+
"""
|
|
26
|
+
self.base_url = base_url.rstrip("/")
|
|
27
|
+
self.auth_client = auth_client
|
|
28
|
+
|
|
29
|
+
def _get_headers(self, additional_headers: Optional[Dict[str, str]] = None) -> Dict[str, str]:
|
|
30
|
+
"""
|
|
31
|
+
Get request headers including authentication.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
additional_headers: Additional headers to include
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Dictionary of headers
|
|
38
|
+
"""
|
|
39
|
+
headers = {"Content-Type": "application/json"}
|
|
40
|
+
|
|
41
|
+
if self.auth_client and self.auth_client.is_authenticated():
|
|
42
|
+
headers["Authorization"] = f"Bearer {self.auth_client.access_token}"
|
|
43
|
+
|
|
44
|
+
if additional_headers:
|
|
45
|
+
headers.update(additional_headers)
|
|
46
|
+
|
|
47
|
+
return headers
|
|
48
|
+
|
|
49
|
+
def _make_request(
|
|
50
|
+
self,
|
|
51
|
+
method: str,
|
|
52
|
+
endpoint: str,
|
|
53
|
+
params: Optional[Dict[str, Any]] = None,
|
|
54
|
+
data: Optional[Dict[str, Any]] = None,
|
|
55
|
+
json_data: Optional[Dict[str, Any]] = None,
|
|
56
|
+
headers: Optional[Dict[str, str]] = None,
|
|
57
|
+
) -> Dict[str, Any]:
|
|
58
|
+
"""
|
|
59
|
+
Make an HTTP request to the API.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
method: HTTP method (GET, POST, etc.)
|
|
63
|
+
endpoint: API endpoint
|
|
64
|
+
params: Query parameters
|
|
65
|
+
data: Form data
|
|
66
|
+
json_data: JSON data
|
|
67
|
+
headers: Additional headers
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Response data as dictionary
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
DataSpaceAPIError: For API errors
|
|
74
|
+
DataSpaceAuthError: For authentication errors
|
|
75
|
+
DataSpaceNotFoundError: For 404 errors
|
|
76
|
+
DataSpaceValidationError: For validation errors
|
|
77
|
+
"""
|
|
78
|
+
url = f"{self.base_url}{endpoint}"
|
|
79
|
+
request_headers = self._get_headers(headers)
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
response = requests.request(
|
|
83
|
+
method=method,
|
|
84
|
+
url=url,
|
|
85
|
+
params=params,
|
|
86
|
+
data=data,
|
|
87
|
+
json=json_data,
|
|
88
|
+
headers=request_headers,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Handle different status codes
|
|
92
|
+
if response.status_code == 200 or response.status_code == 201:
|
|
93
|
+
result: Dict[str, Any] = response.json() if response.content else {}
|
|
94
|
+
return result
|
|
95
|
+
elif response.status_code == 204:
|
|
96
|
+
return {}
|
|
97
|
+
elif response.status_code == 401:
|
|
98
|
+
raise DataSpaceAuthError(
|
|
99
|
+
"Authentication required or token expired",
|
|
100
|
+
status_code=response.status_code,
|
|
101
|
+
response=response.json() if response.content else {},
|
|
102
|
+
)
|
|
103
|
+
elif response.status_code == 404:
|
|
104
|
+
raise DataSpaceNotFoundError(
|
|
105
|
+
"Resource not found",
|
|
106
|
+
status_code=response.status_code,
|
|
107
|
+
response=response.json() if response.content else {},
|
|
108
|
+
)
|
|
109
|
+
elif response.status_code == 400:
|
|
110
|
+
raise DataSpaceValidationError(
|
|
111
|
+
"Validation error",
|
|
112
|
+
status_code=response.status_code,
|
|
113
|
+
response=response.json() if response.content else {},
|
|
114
|
+
)
|
|
115
|
+
else:
|
|
116
|
+
raise DataSpaceAPIError(
|
|
117
|
+
f"API request failed with status {response.status_code}",
|
|
118
|
+
status_code=response.status_code,
|
|
119
|
+
response=response.json() if response.content else {},
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
except requests.RequestException as e:
|
|
123
|
+
raise DataSpaceAPIError(f"Network error: {str(e)}")
|
|
124
|
+
|
|
125
|
+
def get(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
126
|
+
"""Make a GET request."""
|
|
127
|
+
return self._make_request("GET", endpoint, params=params)
|
|
128
|
+
|
|
129
|
+
def post(
|
|
130
|
+
self,
|
|
131
|
+
endpoint: str,
|
|
132
|
+
data: Optional[Dict[str, Any]] = None,
|
|
133
|
+
json_data: Optional[Dict[str, Any]] = None,
|
|
134
|
+
) -> Dict[str, Any]:
|
|
135
|
+
"""Make a POST request."""
|
|
136
|
+
return self._make_request("POST", endpoint, data=data, json_data=json_data)
|
|
137
|
+
|
|
138
|
+
def put(
|
|
139
|
+
self,
|
|
140
|
+
endpoint: str,
|
|
141
|
+
data: Optional[Dict[str, Any]] = None,
|
|
142
|
+
json_data: Optional[Dict[str, Any]] = None,
|
|
143
|
+
) -> Dict[str, Any]:
|
|
144
|
+
"""Make a PUT request."""
|
|
145
|
+
return self._make_request("PUT", endpoint, data=data, json_data=json_data)
|
|
146
|
+
|
|
147
|
+
def patch(
|
|
148
|
+
self,
|
|
149
|
+
endpoint: str,
|
|
150
|
+
data: Optional[Dict[str, Any]] = None,
|
|
151
|
+
json_data: Optional[Dict[str, Any]] = None,
|
|
152
|
+
) -> Dict[str, Any]:
|
|
153
|
+
"""Make a PATCH request."""
|
|
154
|
+
return self._make_request("PATCH", endpoint, data=data, json_data=json_data)
|
|
155
|
+
|
|
156
|
+
def delete(self, endpoint: str) -> Dict[str, Any]:
|
|
157
|
+
"""Make a DELETE request."""
|
|
158
|
+
response = self._make_request("DELETE", endpoint)
|
|
159
|
+
result: Dict[str, Any] = response if isinstance(response, dict) else {}
|
|
160
|
+
return result
|