enkryptai-sdk 1.0.23__py3-none-any.whl → 1.0.25__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.
enkryptai_sdk/red_team.py CHANGED
@@ -6,11 +6,14 @@ from .datasets import DatasetClient
6
6
  from .dto import (
7
7
  RedteamHealthResponse,
8
8
  RedTeamModelHealthConfig,
9
+ RedTeamModelHealthConfigV3,
9
10
  RedteamModelHealthResponse,
10
11
  RedTeamConfig,
11
12
  RedTeamConfigWithSavedModel,
12
13
  RedTeamCustomConfig,
13
14
  RedTeamCustomConfigWithSavedModel,
15
+ RedTeamCustomConfigV3,
16
+ RedTeamCustomConfigWithSavedModelV3,
14
17
  RedTeamResponse,
15
18
  RedTeamResultSummary,
16
19
  RedTeamResultDetails,
@@ -21,7 +24,8 @@ from .dto import (
21
24
  RedTeamRiskMitigationGuardrailsPolicyResponse,
22
25
  RedTeamRiskMitigationSystemPromptConfig,
23
26
  RedTeamRiskMitigationSystemPromptResponse,
24
- RedTeamFindingsResponse
27
+ RedTeamFindingsResponse,
28
+ RedTeamDownloadLinkResponse
25
29
  )
26
30
 
27
31
 
@@ -94,6 +98,40 @@ class RedTeamClient(BaseClient):
94
98
  return RedteamModelHealthResponse.from_dict(response)
95
99
  except Exception as e:
96
100
  raise RedTeamClientError(str(e))
101
+
102
+ def check_model_health_v3(self, config: RedTeamModelHealthConfigV3):
103
+ """
104
+ Get the health status of a model using V3 format with endpoint_configuration.
105
+
106
+ This method accepts endpoint_configuration (similar to add_custom_task) and
107
+ converts it internally to target_model_configuration format for backend compatibility.
108
+
109
+ Args:
110
+ config (RedTeamModelHealthConfigV3): Configuration object containing endpoint_configuration
111
+
112
+ Returns:
113
+ RedteamModelHealthResponse: Response from the API containing health status
114
+
115
+ Raises:
116
+ RedTeamClientError: If there's an error from the API
117
+ """
118
+ try:
119
+ config = RedTeamModelHealthConfigV3.from_dict(config)
120
+
121
+ # Convert endpoint_configuration to target_model_configuration
122
+ target_config = config.to_target_model_configuration()
123
+
124
+ # Create the payload in the format expected by the backend
125
+ payload = {
126
+ "target_model_configuration": target_config.to_dict()
127
+ }
128
+
129
+ response = self._request("POST", "/redteam/model-health", json=payload)
130
+ if response.get("error") not in [None, ""]:
131
+ raise RedTeamClientError(f"API Error: {str(response)}")
132
+ return RedteamModelHealthResponse.from_dict(response)
133
+ except Exception as e:
134
+ raise RedTeamClientError(str(e))
97
135
 
98
136
  def add_task(
99
137
  self,
@@ -200,6 +238,10 @@ class RedTeamClient(BaseClient):
200
238
  "redteam_test_configurations": test_configs,
201
239
  }
202
240
 
241
+ # Only add frameworks if provided and not empty
242
+ if config.frameworks:
243
+ payload["frameworks"] = config.frameworks
244
+
203
245
  if config.dataset_configuration:
204
246
  payload["dataset_configuration"] = DatasetClient.prepare_dataset_payload(
205
247
  config.dataset_configuration, True)
@@ -254,6 +296,10 @@ class RedTeamClient(BaseClient):
254
296
  "redteam_test_configurations": test_configs,
255
297
  }
256
298
 
299
+ # Only add frameworks if provided and not empty
300
+ if config.frameworks:
301
+ payload["frameworks"] = config.frameworks
302
+
257
303
  if config.dataset_configuration:
258
304
  payload["dataset_configuration"] = DatasetClient.prepare_dataset_payload(
259
305
  config.dataset_configuration, True)
@@ -281,6 +327,146 @@ class RedTeamClient(BaseClient):
281
327
  raise RedTeamClientError(f"API Error: {str(response)}")
282
328
  return RedTeamResponse.from_dict(response)
283
329
 
330
+ def add_custom_task_v3(
331
+ self,
332
+ config: RedTeamCustomConfigV3,
333
+ policy_name: str = None,
334
+ ):
335
+ """
336
+ Add a new custom red teaming task with v3 attack methods format.
337
+
338
+ V3 format supports nested attack methods:
339
+ {
340
+ "test_name": {
341
+ "sample_percentage": 50,
342
+ "attack_methods": {
343
+ "method_category": {
344
+ "method_name": {
345
+ "params": {}
346
+ }
347
+ }
348
+ }
349
+ }
350
+ }
351
+ """
352
+ headers = {
353
+ "Content-Type": "application/json",
354
+ }
355
+
356
+ if policy_name is not None:
357
+ headers["X-Enkrypt-Policy"] = policy_name
358
+
359
+ config = RedTeamCustomConfigV3.from_dict(config)
360
+ test_configs = config.redteam_test_configurations.to_dict()
361
+ # Remove None or empty test configurations
362
+ test_configs = {k: v for k, v in test_configs.items() if v is not None}
363
+
364
+ payload = {
365
+ "test_name": config.test_name,
366
+ "redteam_test_configurations": test_configs,
367
+ }
368
+
369
+ # Only add frameworks if provided and not empty
370
+ if config.frameworks:
371
+ payload["frameworks"] = config.frameworks
372
+
373
+ if config.dataset_configuration:
374
+ payload["dataset_configuration"] = DatasetClient.prepare_dataset_payload(
375
+ config.dataset_configuration, True)
376
+ else:
377
+ raise RedTeamClientError(
378
+ "Please provide a dataset configuration"
379
+ )
380
+
381
+ if config.endpoint_configuration:
382
+ payload["endpoint_configuration"] = ModelClient.prepare_model_payload(
383
+ config.endpoint_configuration, True)
384
+
385
+ response = self._request(
386
+ "POST",
387
+ "/redteam/v3/add-custom-task",
388
+ headers=headers,
389
+ json=payload,
390
+ )
391
+ if response.get("error"):
392
+ raise RedTeamClientError(f"API Error: {str(response)}")
393
+ return RedTeamResponse.from_dict(response)
394
+ else:
395
+ raise RedTeamClientError(
396
+ "Please provide a endpoint configuration"
397
+ )
398
+
399
+ def add_custom_task_with_saved_model_v3(
400
+ self,
401
+ config: RedTeamCustomConfigWithSavedModelV3,
402
+ model_saved_name: str,
403
+ model_version: str,
404
+ policy_name: str = None,
405
+ ):
406
+ """
407
+ Add a new red teaming custom task using a saved model with v3 attack methods format.
408
+
409
+ V3 format supports nested attack methods:
410
+ {
411
+ "test_name": {
412
+ "sample_percentage": 50,
413
+ "attack_methods": {
414
+ "method_category": {
415
+ "method_name": {
416
+ "params": {}
417
+ }
418
+ }
419
+ }
420
+ }
421
+ }
422
+ """
423
+ if not model_saved_name:
424
+ raise RedTeamClientError("Please provide a model_saved_name")
425
+
426
+ if not model_version:
427
+ raise RedTeamClientError("Please provide a model_version. Default is 'v1'")
428
+
429
+ config = RedTeamCustomConfigWithSavedModelV3.from_dict(config)
430
+ test_configs = config.redteam_test_configurations.to_dict()
431
+ # Remove None or empty test configurations
432
+ test_configs = {k: v for k, v in test_configs.items() if v is not None}
433
+
434
+ payload = {
435
+ "test_name": config.test_name,
436
+ "redteam_test_configurations": test_configs,
437
+ }
438
+
439
+ # Only add frameworks if provided and not empty
440
+ if config.frameworks:
441
+ payload["frameworks"] = config.frameworks
442
+
443
+ if config.dataset_configuration:
444
+ payload["dataset_configuration"] = DatasetClient.prepare_dataset_payload(
445
+ config.dataset_configuration, True)
446
+ else:
447
+ raise RedTeamClientError(
448
+ "Please provide a dataset configuration"
449
+ )
450
+
451
+ headers = {
452
+ "X-Enkrypt-Model": model_saved_name,
453
+ "X-Enkrypt-Model-Version": model_version,
454
+ "Content-Type": "application/json",
455
+ }
456
+
457
+ if policy_name is not None:
458
+ headers["X-Enkrypt-Policy"] = policy_name
459
+
460
+ response = self._request(
461
+ "POST",
462
+ "/redteam/v3/model/add-custom-task",
463
+ headers=headers,
464
+ json=payload,
465
+ )
466
+ if response.get("error"):
467
+ raise RedTeamClientError(f"API Error: {str(response)}")
468
+ return RedTeamResponse.from_dict(response)
469
+
284
470
  def status(self, task_id: str = None, test_name: str = None):
285
471
  """
286
472
  Get the status of a specific red teaming task.
@@ -394,7 +580,7 @@ class RedTeamClient(BaseClient):
394
580
  if test_name:
395
581
  headers["X-Enkrypt-Test-Name"] = test_name
396
582
 
397
- response = self._request("GET", "/redteam/results/summary", headers=headers)
583
+ response = self._request("GET", "/redteam/v3/results/summary", headers=headers)
398
584
  if response.get("error"):
399
585
  raise RedTeamClientError(f"API Error: {str(response)}")
400
586
  # print(f"Response: {response}")
@@ -427,7 +613,7 @@ class RedTeamClient(BaseClient):
427
613
  if test_name:
428
614
  headers["X-Enkrypt-Test-Name"] = test_name
429
615
 
430
- url = f"/redteam/v2/results/summary/{test_type}"
616
+ url = f"/redteam/v3/results/summary/{test_type}"
431
617
  response = self._request("GET", url, headers=headers)
432
618
  if response.get("error"):
433
619
  raise RedTeamClientError(f"API Error: {str(response)}")
@@ -457,7 +643,7 @@ class RedTeamClient(BaseClient):
457
643
  if test_name:
458
644
  headers["X-Enkrypt-Test-Name"] = test_name
459
645
 
460
- response = self._request("GET", "/redteam/v2/results/details", headers=headers)
646
+ response = self._request("GET", "/redteam/v3/results/details", headers=headers)
461
647
  if response.get("error"):
462
648
  raise RedTeamClientError(f"API Error: {str(response)}")
463
649
  return RedTeamResultDetails.from_dict(response)
@@ -488,9 +674,8 @@ class RedTeamClient(BaseClient):
488
674
  headers["X-Enkrypt-Task-ID"] = task_id
489
675
  if test_name:
490
676
  headers["X-Enkrypt-Test-Name"] = test_name
491
-
492
677
 
493
- url = f"/redteam/v2/results/details/{test_type}"
678
+ url = f"/redteam/v3/results/details/{test_type}"
494
679
  response = self._request("GET", url, headers=headers)
495
680
  if response.get("error"):
496
681
  raise RedTeamClientError(f"API Error: {str(response)}")
@@ -564,4 +749,35 @@ class RedTeamClient(BaseClient):
564
749
  return RedTeamFindingsResponse.from_dict(response)
565
750
  except Exception as e:
566
751
  raise RedTeamClientError(str(e))
752
+
753
+ def get_download_link(self, task_id: str = None, test_name: str = None):
754
+ """
755
+ Get a download link for red team test results.
756
+
757
+ Args:
758
+ task_id (str, optional): The ID of the task to get download link for
759
+ test_name (str, optional): The name of the test to get download link for
760
+
761
+ Returns:
762
+ RedTeamDownloadLinkResponse: Response containing download link and expiry information
763
+
764
+ Raises:
765
+ RedTeamClientError: If neither task_id nor test_name is provided, or if there's an error from the API
766
+ """
767
+ if not task_id and not test_name:
768
+ raise RedTeamClientError("Either task_id or test_name must be provided")
769
+
770
+ headers = {}
771
+ if task_id:
772
+ headers["X-Enkrypt-Task-ID"] = task_id
773
+ if test_name:
774
+ headers["X-Enkrypt-Test-Name"] = test_name
775
+
776
+ try:
777
+ response = self._request("GET", "/redteam/download-link", headers=headers)
778
+ if response.get("error"):
779
+ raise RedTeamClientError(f"API Error: {str(response)}")
780
+ return RedTeamDownloadLinkResponse.from_dict(response)
781
+ except Exception as e:
782
+ raise RedTeamClientError(str(e))
567
783
 
enkryptai_sdk/response.py CHANGED
@@ -1,3 +1,238 @@
1
+ import math
2
+ from typing import Dict, Any, List, Optional, Union
3
+
4
+
5
+ class PaginationInfo:
6
+ """
7
+ A class to handle pagination information and calculations.
8
+ """
9
+
10
+ def __init__(self, page: int = 1, per_page: int = 10, total_count: int = 0):
11
+ """
12
+ Initialize pagination information.
13
+
14
+ Args:
15
+ page (int): Current page number (1-based)
16
+ per_page (int): Number of items per page
17
+ total_count (int): Total number of items
18
+ """
19
+ self.page = max(1, page)
20
+ self.per_page = max(1, min(100, per_page)) # Ensure per_page is between 1 and 100
21
+ self.total_count = max(0, total_count)
22
+
23
+ @property
24
+ def total_pages(self) -> int:
25
+ """Calculate total number of pages."""
26
+ if self.total_count == 0:
27
+ return 0
28
+ return math.ceil(self.total_count / self.per_page)
29
+
30
+ @property
31
+ def has_next(self) -> bool:
32
+ """Check if there's a next page."""
33
+ return self.page < self.total_pages
34
+
35
+ @property
36
+ def has_previous(self) -> bool:
37
+ """Check if there's a previous page."""
38
+ return self.page > 1
39
+
40
+ @property
41
+ def offset(self) -> int:
42
+ """Calculate the offset for database queries."""
43
+ return (self.page - 1) * self.per_page
44
+
45
+ @property
46
+ def limit(self) -> int:
47
+ """Get the limit for database queries."""
48
+ return self.per_page
49
+
50
+ def to_dict(self) -> Dict[str, Any]:
51
+ """Convert pagination info to dictionary."""
52
+ return {
53
+ "page": self.page,
54
+ "per_page": self.per_page,
55
+ "total_count": self.total_count,
56
+ "total_pages": self.total_pages,
57
+ "has_next": self.has_next,
58
+ "has_previous": self.has_previous
59
+ }
60
+
61
+ @classmethod
62
+ def from_query_params(cls, query_params: Dict[str, Any], default_per_page: int = 10) -> "PaginationInfo":
63
+ """
64
+ Create PaginationInfo from query parameters.
65
+
66
+ Args:
67
+ query_params (Dict[str, Any]): Dictionary containing query parameters
68
+ default_per_page (int): Default items per page
69
+
70
+ Returns:
71
+ PaginationInfo: Pagination information object
72
+ """
73
+ try:
74
+ page = int(query_params.get("page", 1))
75
+ per_page = int(query_params.get("per_page", default_per_page))
76
+ except (ValueError, TypeError):
77
+ page = 1
78
+ per_page = default_per_page
79
+
80
+ # Validate pagination parameters
81
+ if page < 1:
82
+ page = 1
83
+ if per_page < 1 or per_page > 100:
84
+ per_page = min(max(1, per_page), 100)
85
+
86
+ return cls(page=page, per_page=per_page)
87
+
88
+ @classmethod
89
+ def validate_params(cls, page: Union[str, int], per_page: Union[str, int]) -> tuple[int, int]:
90
+ """
91
+ Validate pagination parameters and return validated values.
92
+
93
+ Args:
94
+ page: Page number (can be string or int)
95
+ per_page: Items per page (can be string or int)
96
+
97
+ Returns:
98
+ tuple[int, int]: Validated (page, per_page) values
99
+
100
+ Raises:
101
+ ValueError: If parameters are invalid
102
+ """
103
+ try:
104
+ page_num = int(page) if page is not None else 1
105
+ per_page_num = int(per_page) if per_page is not None else 10
106
+ except (ValueError, TypeError):
107
+ raise ValueError("Page and per_page must be valid integers")
108
+
109
+ if page_num < 1:
110
+ raise ValueError("Page must be >= 1")
111
+ if per_page_num < 1 or per_page_num > 100:
112
+ raise ValueError("Per_page must be between 1 and 100")
113
+
114
+ return page_num, per_page_num
115
+
116
+
117
+ class PaginatedResponse(dict):
118
+ """
119
+ A wrapper class for paginated API responses that provides pagination information
120
+ and maintains backward compatibility with dictionary access.
121
+ """
122
+
123
+ def __init__(self, data: List[Any], pagination: PaginationInfo, **kwargs):
124
+ """
125
+ Initialize the PaginatedResponse object.
126
+
127
+ Args:
128
+ data (List[Any]): List of items for the current page
129
+ pagination (PaginationInfo): Pagination information
130
+ **kwargs: Additional response data
131
+ """
132
+ response_data = {
133
+ "data": data,
134
+ "pagination": pagination.to_dict(),
135
+ **kwargs
136
+ }
137
+ super().__init__(response_data)
138
+ self._data = response_data
139
+ self._pagination = pagination
140
+
141
+ def get_data(self) -> List[Any]:
142
+ """Get the data items for the current page."""
143
+ return self._data.get("data", [])
144
+
145
+ def get_pagination(self) -> Dict[str, Any]:
146
+ """Get pagination information."""
147
+ return self._data.get("pagination", {})
148
+
149
+ def get_page(self) -> int:
150
+ """Get current page number."""
151
+ return self._pagination.page
152
+
153
+ def get_per_page(self) -> int:
154
+ """Get items per page."""
155
+ return self._pagination.per_page
156
+
157
+ def get_total_count(self) -> int:
158
+ """Get total number of items."""
159
+ return self._pagination.total_count
160
+
161
+ def get_total_pages(self) -> int:
162
+ """Get total number of pages."""
163
+ return self._pagination.total_pages
164
+
165
+ def has_next_page(self) -> bool:
166
+ """Check if there's a next page."""
167
+ return self._pagination.has_next
168
+
169
+ def has_previous_page(self) -> bool:
170
+ """Check if there's a previous page."""
171
+ return self._pagination.has_previous
172
+
173
+ def get_next_page_url(self, base_url: str, **query_params) -> Optional[str]:
174
+ """
175
+ Generate URL for the next page.
176
+
177
+ Args:
178
+ base_url (str): Base URL for the endpoint
179
+ **query_params: Additional query parameters to include
180
+
181
+ Returns:
182
+ Optional[str]: Next page URL or None if no next page
183
+ """
184
+ if not self.has_next_page():
185
+ return None
186
+
187
+ params = {**query_params, "page": self.get_page() + 1, "per_page": self.get_per_page()}
188
+ query_string = "&".join([f"{k}={v}" for k, v in params.items()])
189
+ return f"{base_url}?{query_string}"
190
+
191
+ def get_previous_page_url(self, base_url: str, **query_params) -> Optional[str]:
192
+ """
193
+ Generate URL for the previous page.
194
+
195
+ Args:
196
+ base_url (str): Base URL for the endpoint
197
+ **query_params: Additional query parameters to include
198
+
199
+ Returns:
200
+ Optional[str]: Previous page URL or None if no previous page
201
+ """
202
+ if not self.has_previous_page():
203
+ return None
204
+
205
+ params = {**query_params, "page": self.get_page() - 1, "per_page": self.get_per_page()}
206
+ query_string = "&".join([f"{k}={v}" for k, v in params.items()])
207
+ return f"{base_url}?{query_string}"
208
+
209
+ def get_page_urls(self, base_url: str, **query_params) -> Dict[str, Optional[str]]:
210
+ """
211
+ Generate URLs for all pagination actions.
212
+
213
+ Args:
214
+ base_url (str): Base URL for the endpoint
215
+ **query_params: Additional query parameters to include
216
+
217
+ Returns:
218
+ Dict[str, Optional[str]]: Dictionary with pagination URLs
219
+ """
220
+ return {
221
+ "first": f"{base_url}?{self._build_query_string(1, **query_params)}",
222
+ "previous": self.get_previous_page_url(base_url, **query_params),
223
+ "current": f"{base_url}?{self._build_query_string(self.get_page(), **query_params)}",
224
+ "next": self.get_next_page_url(base_url, **query_params),
225
+ "last": f"{base_url}?{self._build_query_string(self.get_total_pages(), **query_params)}" if self.get_total_pages() > 0 else None
226
+ }
227
+
228
+ def _build_query_string(self, page: int, **query_params) -> str:
229
+ """Build query string for a specific page."""
230
+ params = {**query_params, "page": page, "per_page": self.get_per_page()}
231
+ return "&".join([f"{k}={v}" for k, v in params.items()])
232
+
233
+ def __str__(self) -> str:
234
+ """String representation of the paginated response."""
235
+ return f"PaginatedResponse(page={self.get_page()}, per_page={self.get_per_page()}, total={self.get_total_count()}, items={len(self.get_data())})"
1
236
 
2
237
 
3
238
  class GuardrailsResponse(dict):
@@ -100,7 +335,25 @@ class GuardrailsResponse(dict):
100
335
 
101
336
  return f"Response Status: {status}\n{violation_str}"
102
337
 
338
+ def get_pagination(self) -> Optional[Dict[str, Any]]:
339
+ """
340
+ Get pagination information if available.
341
+
342
+ Returns:
343
+ Optional[Dict[str, Any]]: Pagination data or None if not available
344
+ """
345
+ return self._data.get("pagination")
103
346
 
347
+ def is_paginated(self) -> bool:
348
+ """
349
+ Check if the response contains pagination information.
350
+
351
+ Returns:
352
+ bool: True if response is paginated, False otherwise
353
+ """
354
+ return "pagination" in self._data
355
+
356
+
104
357
  class PIIResponse(dict):
105
358
  """
106
359
  A wrapper class for Enkrypt AI PII API responses that provides additional functionality
@@ -132,4 +385,22 @@ class PIIResponse(dict):
132
385
  """
133
386
  return self._data.get("key", "")
134
387
 
388
+ def get_pagination(self) -> Optional[Dict[str, Any]]:
389
+ """
390
+ Get pagination information if available.
391
+
392
+ Returns:
393
+ Optional[Dict[str, Any]]: Pagination data or None if not available
394
+ """
395
+ return self._data.get("pagination")
396
+
397
+ def is_paginated(self) -> bool:
398
+ """
399
+ Check if the response contains pagination information.
400
+
401
+ Returns:
402
+ bool: True if response is paginated, False otherwise
403
+ """
404
+ return "pagination" in self._data
405
+
135
406
 
@@ -0,0 +1,29 @@
1
+ # Utils module for Enkrypt AI SDK
2
+
3
+ from .pagination import (
4
+ PaginationInfo,
5
+ PaginatedResponse,
6
+ parse_pagination_params,
7
+ build_pagination_url,
8
+ create_paginated_response,
9
+ validate_pagination_params,
10
+ get_pagination_metadata,
11
+ calculate_page_info,
12
+ create_pagination_links,
13
+ apply_pagination_to_list,
14
+ format_pagination_response
15
+ )
16
+
17
+ __all__ = [
18
+ "PaginationInfo",
19
+ "PaginatedResponse",
20
+ "parse_pagination_params",
21
+ "build_pagination_url",
22
+ "create_paginated_response",
23
+ "validate_pagination_params",
24
+ "get_pagination_metadata",
25
+ "calculate_page_info",
26
+ "create_pagination_links",
27
+ "apply_pagination_to_list",
28
+ "format_pagination_response"
29
+ ]