kalbio 0.2.1__tar.gz → 0.2.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: kalbio
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Python client for kaleidoscope.bio
5
5
  License: MIT
6
6
  Author: Ahmed Elnaiem
@@ -11,6 +11,9 @@ Classifier: Programming Language :: Python :: 3
11
11
  Classifier: Programming Language :: Python :: 3.10
12
12
  Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Programming Language :: Python :: 3.12
14
+ Provides-Extra: iap
15
+ Requires-Dist: cachetools (>=6.2.5,<7.0.0)
16
+ Requires-Dist: google-auth (>=2.0.0,<3.0.0) ; extra == "iap"
14
17
  Requires-Dist: pydantic (>=2.9.2,<3.0.0)
15
18
  Requires-Dist: requests (>=2.32.3,<3.0.0)
16
19
  Project-URL: Documentation, https://api.docs.kaleidoscope.bio
@@ -32,7 +35,6 @@ Kalbio is a Python Client library for building applications with the Kaleidoscop
32
35
  - **Intuitive Resource Management**: Clean, object-oriented interface for programs, activities, records, and more
33
36
  - **Advanced Search & Filtering**: Powerful query capabilities with type-safe filter operators
34
37
  - **File Upload Support**: Direct file upload to record fields with support for multiple file types
35
- - **Comprehensive Coverage**: Access all Kaleidoscope platform resources through a unified client
36
38
  - **Bulk Operations**: Import and export capabilities for efficient data management
37
39
 
38
40
  ## Requirements
@@ -147,13 +149,13 @@ with open("experiment_data.csv", "rb") as file:
147
149
  )
148
150
  ```
149
151
 
150
- ### Comprehensive Resource Management
152
+ ### Resource Management
151
153
 
152
- Kalbio provides services for all Kaleidoscope resources:
154
+ Kalbio provides services for the following Kaleidoscope resources:
153
155
 
154
156
  - **Programs**: Organize your work
155
157
  - **Activities**: Tasks, experiments, projects, etc.
156
- - **Records**: Structured data storage
158
+ - **Records**: Structured entity records
157
159
  - **Entity Types**: Define your data schemas
158
160
  - **Entity Fields**: Manage field definitions
159
161
  - **Labels**: Tag and categorize
@@ -170,7 +172,7 @@ Once you have a client instance, you have access to:
170
172
  | ---------------------- | ------------------------------------------- |
171
173
  | `client.programs` | Program management |
172
174
  | `client.activities` | Activities (tasks, experiments, projects) |
173
- | `client.records` | Record CRUD operations |
175
+ | `client.records` | Entity record CRUD operations |
174
176
  | `client.entity_types` | Entity type definitions |
175
177
  | `client.entity_fields` | Field definitions (key fields, data fields) |
176
178
  | `client.labels` | Label management |
@@ -12,7 +12,6 @@ Kalbio is a Python Client library for building applications with the Kaleidoscop
12
12
  - **Intuitive Resource Management**: Clean, object-oriented interface for programs, activities, records, and more
13
13
  - **Advanced Search & Filtering**: Powerful query capabilities with type-safe filter operators
14
14
  - **File Upload Support**: Direct file upload to record fields with support for multiple file types
15
- - **Comprehensive Coverage**: Access all Kaleidoscope platform resources through a unified client
16
15
  - **Bulk Operations**: Import and export capabilities for efficient data management
17
16
 
18
17
  ## Requirements
@@ -127,13 +126,13 @@ with open("experiment_data.csv", "rb") as file:
127
126
  )
128
127
  ```
129
128
 
130
- ### Comprehensive Resource Management
129
+ ### Resource Management
131
130
 
132
- Kalbio provides services for all Kaleidoscope resources:
131
+ Kalbio provides services for the following Kaleidoscope resources:
133
132
 
134
133
  - **Programs**: Organize your work
135
134
  - **Activities**: Tasks, experiments, projects, etc.
136
- - **Records**: Structured data storage
135
+ - **Records**: Structured entity records
137
136
  - **Entity Types**: Define your data schemas
138
137
  - **Entity Fields**: Manage field definitions
139
138
  - **Labels**: Tag and categorize
@@ -150,7 +149,7 @@ Once you have a client instance, you have access to:
150
149
  | ---------------------- | ------------------------------------------- |
151
150
  | `client.programs` | Program management |
152
151
  | `client.activities` | Activities (tasks, experiments, projects) |
153
- | `client.records` | Record CRUD operations |
152
+ | `client.records` | Entity record CRUD operations |
154
153
  | `client.entity_types` | Entity type definitions |
155
154
  | `client.entity_fields` | Field definitions (key fields, data fields) |
156
155
  | `client.labels` | Label management |
@@ -0,0 +1,10 @@
1
+ from importlib.metadata import version, PackageNotFoundError
2
+
3
+ from kalbio.client import KaleidoscopeClient
4
+
5
+ try:
6
+ __version__ = version("kalbio")
7
+ except PackageNotFoundError:
8
+ __version__ = "unknown"
9
+
10
+ __all__ = ["KaleidoscopeClient"]
@@ -1,6 +1,6 @@
1
1
  """Activities Module for Kaleidoscope API Client.
2
2
 
3
- This module provides comprehensive functionality for managing activities (tasks, experiments,
3
+ This module provides functionality for managing activities (tasks, experiments,
4
4
  projects, stages, milestones, and design cycles) within the Kaleidoscope platform. It includes
5
5
  models for activities, activity definitions, and properties, as well as service classes for
6
6
  performing CRUD operations and managing activity workflows.
@@ -876,12 +876,11 @@ class ActivitiesService:
876
876
 
877
877
  try:
878
878
  payload = {
879
- "program_ids": program_ids,
880
879
  "title": title,
881
880
  "activity_type": activity_type,
882
881
  "definition_id": self._resolve_definition_id(activity_definition_id),
883
- "record_ids": [],
884
- "assigned_user_ids": assigned_user_ids,
882
+ "program_ids": program_ids if program_ids else [],
883
+ "assigned_user_ids": assigned_user_ids if assigned_user_ids else [],
885
884
  "start_date": start_date.isoformat() if start_date else None,
886
885
  "duration": duration,
887
886
  }
@@ -59,8 +59,8 @@ Any file retrieved by the `KaleidoscopeClient` must be of one the above types"""
59
59
  TIMEOUT_MAXIMUM = 10
60
60
  """Maximum timeout for API requests in seconds."""
61
61
 
62
- _api_key = os.getenv("KALEIDOSCOPE_API_CLIENT_ID")
63
- _api_secret = os.getenv("KALEIDOSCOPE_API_CLIENT_SECRET")
62
+ _env_client_id = os.getenv("KALEIDOSCOPE_API_CLIENT_ID")
63
+ _env_client_secret = os.getenv("KALEIDOSCOPE_API_CLIENT_SECRET")
64
64
 
65
65
 
66
66
  class _TokenResponse(BaseModel):
@@ -107,21 +107,34 @@ class KaleidoscopeClient:
107
107
  )
108
108
  # Use the client to interact with various services
109
109
  programs = client.activities.get_activities()
110
+
111
+ # For applications behind Google Cloud IAP:
112
+ client = KaleidoscopeClient(
113
+ client_id="your_api_client_id",
114
+ client_secret="your_api_client_secret",
115
+ iap_client_id="your_iap_client_id.apps.googleusercontent.com"
116
+ )
110
117
  ```
111
118
  """
112
119
 
113
120
  _client_id: str
114
121
  _client_secret: str
122
+ _iap_client_id: Optional[str]
115
123
 
116
- _refresh_token: str
117
- _access_token: str
118
- _refresh_before: datetime
124
+ _refresh_token: str = None
125
+ _access_token: str = None
126
+ _auth_refresh_before: Optional[datetime] = None
127
+
128
+ _iap_token: Optional[str] = None
129
+ _iap_refresh_before: Optional[datetime] = None
119
130
 
120
131
  def __init__(
121
132
  self,
122
- client_id: Optional[str] = _api_key,
123
- client_secret: Optional[str] = _api_secret,
133
+ client_id: Optional[str] = _env_client_id,
134
+ client_secret: Optional[str] = _env_client_secret,
124
135
  url: str = PROD_API_URL,
136
+ iap_client_id: Optional[str] = None,
137
+ verify_ssl: bool = True,
125
138
  ):
126
139
  """Initialize the Kaleidoscope API client.
127
140
 
@@ -133,6 +146,9 @@ class KaleidoscopeClient:
133
146
  client_secret (str): The API client secret for authentication.
134
147
  url (Optional[str]): The base URL for the API. Defaults to the production
135
148
  API URL if not provided.
149
+ iap_client_id (Optional[str]): The OAuth client ID for Google Cloud
150
+ Identity-Aware Proxy. If provided, the client will automatically
151
+ fetch and refresh IAP tokens. Requires the `google-auth` package.
136
152
 
137
153
  Example:
138
154
  ```python
@@ -141,6 +157,13 @@ class KaleidoscopeClient:
141
157
 
142
158
  # Or rely on environment variables KALEIDOSCOPE_API_CLIENT_ID/SECRET
143
159
  client = KaleidoscopeClient()
160
+
161
+ # For applications behind Google Cloud IAP
162
+ client = KaleidoscopeClient(
163
+ client_id="id",
164
+ client_secret="secret",
165
+ iap_client_id="your_iap_client_id.apps.googleusercontent.com"
166
+ )
144
167
  ```
145
168
  """
146
169
  if client_id is None:
@@ -183,9 +206,34 @@ class KaleidoscopeClient:
183
206
 
184
207
  self._client_id = client_id
185
208
  self._client_secret = client_secret
186
- self._get_auth_token()
209
+ self._iap_client_id = iap_client_id
210
+ self._verify_ssl = verify_ssl
211
+
212
+ def _refresh_iap_token(self):
213
+ """Fetch or refresh the IAP ID token.
214
+
215
+ Uses google-auth to obtain an ID token for the configured IAP client ID.
216
+ Supports both service account credentials and user credentials from
217
+ `gcloud auth application-default login`.
218
+
219
+ Raises:
220
+ ImportError: If google-auth is not installed.
221
+ google.auth.exceptions.DefaultCredentialsError: If no valid credentials found.
222
+ """
223
+ try:
224
+ from google.auth.transport.requests import Request
225
+ from google.oauth2 import id_token
226
+ except ImportError:
227
+ raise ImportError(
228
+ "The 'google-auth' package is required for IAP authentication. "
229
+ "Install it with: pip install google-auth or re-install kalbio with the iap option: "
230
+ "pip install kalbio[iap]"
231
+ )
232
+ self._iap_token = id_token.fetch_id_token(Request(), self._iap_client_id)
233
+ # IAP tokens typically expire in 1 hour; refresh 10 minutes early
234
+ self._iap_refresh_before = datetime.now() + timedelta(minutes=50)
187
235
 
188
- def _update_tokens(self, resp: _TokenResponse):
236
+ def _update_auth_tokens(self, resp: _TokenResponse):
189
237
  """Persist access and refresh tokens and compute the next refresh time.
190
238
 
191
239
  Args:
@@ -193,7 +241,7 @@ class KaleidoscopeClient:
193
241
  """
194
242
  self._access_token = resp.access_token
195
243
  self._refresh_token = resp.refresh_token
196
- self._last_refreshed_at = datetime.now() + timedelta(
244
+ self._auth_refresh_before = datetime.now() + timedelta(
197
245
  seconds=resp.expires_in - (60 * 10) # add a 10 minute buffer
198
246
  )
199
247
 
@@ -210,14 +258,16 @@ class KaleidoscopeClient:
210
258
  "client_id": self._client_id,
211
259
  "client_secret": self._client_secret,
212
260
  },
261
+ headers=self._get_iap_headers(),
213
262
  timeout=TIMEOUT_MAXIMUM,
263
+ verify=self._verify_ssl,
214
264
  )
215
265
  if auth_resp.status_code >= 400:
216
266
  raise RuntimeError(
217
267
  f"Could not connect to server with client_id {self._client_id}: {auth_resp.content}"
218
268
  )
219
269
 
220
- self._update_tokens(_TokenResponse.model_validate(auth_resp.json()))
270
+ self._update_auth_tokens(_TokenResponse.model_validate(auth_resp.json()))
221
271
 
222
272
  def _refresh_auth_token(self):
223
273
  """Refresh the access token using the stored refresh token.
@@ -234,27 +284,53 @@ class KaleidoscopeClient:
234
284
  "grant_type": "refresh_token",
235
285
  "refresh_token": self._refresh_token,
236
286
  },
287
+ headers=self._get_iap_headers(),
237
288
  timeout=TIMEOUT_MAXIMUM,
289
+ verify=self._verify_ssl,
238
290
  )
239
291
  if auth_resp.status_code >= 400:
240
292
  raise RuntimeError(f"Could not refresh access token: {auth_resp.content}")
241
293
 
242
- self._update_tokens(_TokenResponse.model_validate(auth_resp.json()))
294
+ self._update_auth_tokens(_TokenResponse.model_validate(auth_resp.json()))
295
+
296
+ def _get_iap_headers(self) -> dict:
297
+ """Build IAP headers, refreshing tokens if needed.
298
+
299
+ Returns:
300
+ dict: HTTP header for IAP (Proxy-Authorization)
301
+ """
302
+ headers = {}
303
+ if self._iap_client_id:
304
+ if (
305
+ self._iap_refresh_before is None
306
+ or datetime.now() > self._iap_refresh_before
307
+ ):
308
+ self._refresh_iap_token()
309
+ headers["Authorization"] = f"Bearer {self._iap_token}"
310
+
311
+ return headers
243
312
 
244
313
  def _get_headers(self) -> dict:
245
314
  """Build authorization headers, refreshing tokens if needed.
246
315
 
247
316
  Returns:
248
- dict: HTTP headers including `Authorization` and `Content-Type`.
317
+ dict: HTTP headers including `Authorization`, `Content-Type`,
318
+ and optionally `Proxy-Authorization` for IAP.
249
319
  """
250
- if datetime.now() > self._last_refreshed_at:
320
+ if (
321
+ self._auth_refresh_before is None
322
+ or datetime.now() > self._auth_refresh_before
323
+ ):
251
324
  self._refresh_auth_token()
252
325
 
253
- return {
326
+ headers = {
254
327
  "Content-Type": "application/json",
255
- "Authorization": f"Bearer {self._access_token}",
328
+ "X-Kal-Authorization": f"Bearer {self._access_token}",
329
+ **self._get_iap_headers(),
256
330
  }
257
331
 
332
+ return headers
333
+
258
334
  def _post(self, url: str, payload: dict) -> Any:
259
335
  """Send a POST request to the specified URL with the given payload.
260
336
 
@@ -277,6 +353,7 @@ class KaleidoscopeClient:
277
353
  data=json.dumps(payload),
278
354
  headers=self._get_headers(),
279
355
  timeout=TIMEOUT_MAXIMUM,
356
+ verify=self._verify_ssl,
280
357
  )
281
358
  if resp.status_code >= 400:
282
359
  print(f"POST {url} received {resp.status_code}: ", resp.content)
@@ -317,6 +394,7 @@ class KaleidoscopeClient:
317
394
  data=form_data,
318
395
  headers=self._get_headers(),
319
396
  timeout=TIMEOUT_MAXIMUM,
397
+ verify=self._verify_ssl,
320
398
  )
321
399
  if resp.status_code >= 400:
322
400
  print(f"POST {url} received {resp.status_code}: ", resp.content)
@@ -346,6 +424,7 @@ class KaleidoscopeClient:
346
424
  data=json.dumps(payload),
347
425
  headers=self._get_headers(),
348
426
  timeout=TIMEOUT_MAXIMUM,
427
+ verify=self._verify_ssl,
349
428
  )
350
429
  if resp.status_code >= 400:
351
430
  print(f"PUT {url} received {resp.status_code}: ", resp.content)
@@ -374,7 +453,12 @@ class KaleidoscopeClient:
374
453
  if params:
375
454
  url += "?" + urllib.parse.urlencode(params)
376
455
 
377
- resp = requests.get(url, headers=self._get_headers(), timeout=TIMEOUT_MAXIMUM)
456
+ resp = requests.get(
457
+ url,
458
+ headers=self._get_headers(),
459
+ timeout=TIMEOUT_MAXIMUM,
460
+ verify=self._verify_ssl,
461
+ )
378
462
  if resp.status_code >= 400:
379
463
  print(f"GET {url} received {resp.status_code}", resp.content)
380
464
  return None
@@ -412,7 +496,11 @@ class KaleidoscopeClient:
412
496
  url += "?" + urllib.parse.urlencode(params)
413
497
 
414
498
  resp = requests.get(
415
- url, headers=self._get_headers(), stream=True, timeout=TIMEOUT_MAXIMUM
499
+ url,
500
+ headers=self._get_headers(),
501
+ stream=True,
502
+ timeout=TIMEOUT_MAXIMUM,
503
+ verify=self._verify_ssl,
416
504
  )
417
505
  if resp.status_code >= 400:
418
506
  print(f"GET {url} received {resp.status_code}", resp.content)
@@ -452,7 +540,10 @@ class KaleidoscopeClient:
452
540
  url += "?" + urllib.parse.urlencode(params)
453
541
 
454
542
  resp = requests.delete(
455
- url, headers=self._get_headers(), timeout=TIMEOUT_MAXIMUM
543
+ url,
544
+ headers=self._get_headers(),
545
+ timeout=TIMEOUT_MAXIMUM,
546
+ verify=self._verify_ssl,
456
547
  )
457
548
  if resp.status_code >= 400:
458
549
  print(f"DELETE {url} received {resp.status_code}", resp.content)
@@ -1,10 +1,10 @@
1
- """Programs module for interacting with Kaleidoscope programs/experiments.
1
+ """Programs module for interacting with Kaleidoscope programs.
2
2
 
3
3
  The service allows users to retrieve all available programs in a workspace
4
4
  and filter programs by specific IDs.
5
5
 
6
6
  Classes:
7
- Program: Data model for a Kaleidoscope program/experiment.
7
+ Program: Data model for a Kaleidoscope program.
8
8
  ProgramsService: Service for managing program retrieval operations.
9
9
 
10
10
  Example:
@@ -64,12 +64,12 @@ class ProgramsService:
64
64
 
65
65
  @lru_cache
66
66
  def get_programs(self) -> List[Program]:
67
- """Retrieve all programs (experiments) available in the workspace.
67
+ """Retrieve all programs available in the workspace.
68
68
 
69
69
  This method caches its values.
70
70
 
71
71
  Returns:
72
- List[Program]: A list of Program objects representing the experiments in the workspace.
72
+ List[Program]: A list of Program objects in the workspace.
73
73
 
74
74
  Note:
75
75
  If an exception occurs during the API request, it logs the error,
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "kalbio"
3
- version = "0.2.1"
3
+ version = "0.2.2"
4
4
  description = "Python client for kaleidoscope.bio"
5
5
  authors = ["Ahmed Elnaiem <ahmed@kaleidoscope.bio>"]
6
6
  readme = "README.md"
@@ -10,6 +10,11 @@ license = "MIT"
10
10
  python = "^3.10"
11
11
  pydantic = "^2.9.2"
12
12
  requests = "^2.32.3"
13
+ cachetools = "^6.2.5"
14
+ google-auth = { version = "^2.0.0", optional = true }
15
+
16
+ [tool.poetry.extras]
17
+ iap = ["google-auth"]
13
18
 
14
19
  [tool.poetry.urls]
15
20
  Homepage = "https://github.com/kaleidoscope-tech/kalbio"
@@ -1 +0,0 @@
1
- __version__ = "0.2.0"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes