pangea-sdk 6.3.0__py3-none-any.whl → 6.5.0b1__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.
pangea/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "6.3.0"
1
+ __version__ = "6.5.0beta1"
2
2
 
3
3
  from pangea.asyncio.request import PangeaRequestAsync
4
4
  from pangea.config import PangeaConfig
pangea/asyncio/request.py CHANGED
@@ -10,13 +10,13 @@ import asyncio
10
10
  import json
11
11
  import time
12
12
  from collections.abc import Iterable, Mapping
13
- from typing import Dict, List, Optional, Sequence, Tuple, Type, Union, cast
13
+ from typing import Dict, List, Optional, Sequence, Tuple, Type, Union, cast, overload
14
14
 
15
15
  import aiohttp
16
16
  from aiohttp import FormData
17
- from pydantic import BaseModel
17
+ from pydantic import BaseModel, TypeAdapter
18
18
  from pydantic_core import to_jsonable_python
19
- from typing_extensions import Any, TypeAlias, TypeVar, override
19
+ from typing_extensions import Any, Literal, TypeAlias, TypeVar, override
20
20
 
21
21
  import pangea.exceptions as pe
22
22
  from pangea.request import MultipartResponse, PangeaRequestBase
@@ -50,6 +50,24 @@ class PangeaRequestAsync(PangeaRequestBase):
50
50
  be set in PangeaConfig.
51
51
  """
52
52
 
53
+ async def delete(self, endpoint: str) -> None:
54
+ """
55
+ Makes a DELETE call to a Pangea endpoint.
56
+
57
+ Args:
58
+ endpoint: The Pangea API endpoint.
59
+ """
60
+
61
+ url = self._url(endpoint)
62
+
63
+ self.logger.debug(
64
+ json.dumps({"service": self.service, "action": "delete", "url": url}, default=default_encoder)
65
+ )
66
+
67
+ requests_response = await self._http_delete(url, headers=self._headers())
68
+ await self._check_http_errors(requests_response)
69
+
70
+ @overload
53
71
  async def post(
54
72
  self,
55
73
  endpoint: str,
@@ -58,18 +76,60 @@ class PangeaRequestAsync(PangeaRequestBase):
58
76
  files: Optional[List[Tuple]] = None,
59
77
  poll_result: bool = True,
60
78
  url: Optional[str] = None,
79
+ *,
80
+ pangea_response: Literal[True] = True,
61
81
  ) -> PangeaResponse[TResult]:
62
- """Makes the POST call to a Pangea Service endpoint.
82
+ """
83
+ Makes a POST call to a Pangea Service endpoint.
63
84
 
64
85
  Args:
65
- endpoint(str): The Pangea Service API endpoint.
66
- data(dict): The POST body payload object
86
+ endpoint: The Pangea Service API endpoint.
87
+ data: The POST body payload object
67
88
 
68
89
  Returns:
69
90
  PangeaResponse which contains the response in its entirety and
70
91
  various properties to retrieve individual fields
71
92
  """
72
93
 
94
+ @overload
95
+ async def post(
96
+ self,
97
+ endpoint: str,
98
+ result_class: Type[TResult],
99
+ data: str | BaseModel | Mapping[str, Any] | None = None,
100
+ files: Optional[List[Tuple]] = None,
101
+ poll_result: bool = True,
102
+ url: Optional[str] = None,
103
+ *,
104
+ pangea_response: Literal[False],
105
+ ) -> TResult:
106
+ """
107
+ Makes a POST call to a Pangea Service endpoint.
108
+
109
+ Args:
110
+ endpoint: The Pangea Service API endpoint.
111
+ data: The POST body payload object
112
+ """
113
+
114
+ async def post(
115
+ self,
116
+ endpoint: str,
117
+ result_class: Type[TResult],
118
+ data: str | BaseModel | Mapping[str, Any] | None = None,
119
+ files: Optional[List[Tuple]] = None,
120
+ poll_result: bool = True,
121
+ url: Optional[str] = None,
122
+ *,
123
+ pangea_response: bool = True,
124
+ ) -> PangeaResponse[TResult] | TResult:
125
+ """
126
+ Makes a POST call to a Pangea Service endpoint.
127
+
128
+ Args:
129
+ endpoint: The Pangea Service API endpoint.
130
+ data: The POST body payload object
131
+ """
132
+
73
133
  if isinstance(data, BaseModel):
74
134
  data = data.model_dump(exclude_none=True)
75
135
 
@@ -109,9 +169,13 @@ class PangeaRequestAsync(PangeaRequestBase):
109
169
 
110
170
  await self._check_http_errors(requests_response)
111
171
 
172
+ if not pangea_response:
173
+ type_adapter = TypeAdapter(result_class)
174
+ return type_adapter.validate_python(await requests_response.json())
175
+
112
176
  if "multipart/form-data" in requests_response.headers.get("content-type", ""):
113
177
  multipart_response = await self._process_multipart_response(requests_response)
114
- pangea_response: PangeaResponse = PangeaResponse(
178
+ pangea_response_obj: PangeaResponse = PangeaResponse(
115
179
  requests_response,
116
180
  result_class=result_class,
117
181
  json=multipart_response.pangea_json,
@@ -124,49 +188,110 @@ class PangeaRequestAsync(PangeaRequestBase):
124
188
  json.dumps({"service": self.service, "action": "post", "url": url, "response": json_resp})
125
189
  )
126
190
 
127
- pangea_response = PangeaResponse(requests_response, result_class=result_class, json=json_resp)
191
+ pangea_response_obj = PangeaResponse(requests_response, result_class=result_class, json=json_resp)
128
192
  except aiohttp.ContentTypeError as e:
129
193
  raise pe.PangeaException(
130
194
  f"Failed to decode json response. {e}. Body: {await requests_response.text()}"
131
195
  ) from e
132
196
 
133
197
  if poll_result:
134
- pangea_response = await self._handle_queued_result(pangea_response)
198
+ pangea_response_obj = await self._handle_queued_result(pangea_response_obj)
135
199
 
136
- return self._check_response(pangea_response)
200
+ return self._check_response(pangea_response_obj)
137
201
 
138
- async def get(self, path: str, result_class: Type[TResult], check_response: bool = True) -> PangeaResponse[TResult]:
139
- """Makes the GET call to a Pangea Service endpoint.
202
+ @overload
203
+ async def get(
204
+ self,
205
+ path: str,
206
+ result_class: Type[TResult],
207
+ check_response: bool = True,
208
+ *,
209
+ params: (
210
+ Mapping[str | bytes | int | float, str | bytes | int | float | Iterable[str | bytes | int | float] | None]
211
+ | None
212
+ ) = None,
213
+ pangea_response: Literal[True] = True,
214
+ ) -> PangeaResponse[TResult]:
215
+ """
216
+ Makes the GET call to a Pangea Service endpoint.
140
217
 
141
218
  Args:
142
- endpoint(str): The Pangea Service API endpoint.
143
- path(str): Additional URL path
219
+ path: Additional URL path
220
+ params: Dictionary of querystring data to attach to the request
144
221
 
145
222
  Returns:
146
223
  PangeaResponse which contains the response in its entirety and
147
224
  various properties to retrieve individual fields
148
225
  """
149
226
 
227
+ @overload
228
+ async def get(
229
+ self,
230
+ path: str,
231
+ result_class: Type[TResult],
232
+ check_response: bool = True,
233
+ *,
234
+ params: (
235
+ Mapping[str | bytes | int | float, str | bytes | int | float | Iterable[str | bytes | int | float] | None]
236
+ | None
237
+ ) = None,
238
+ pangea_response: Literal[False] = False,
239
+ ) -> TResult:
240
+ """
241
+ Makes the GET call to a Pangea Service endpoint.
242
+
243
+ Args:
244
+ path: Additional URL path
245
+ params: Dictionary of querystring data to attach to the request
246
+ """
247
+
248
+ async def get(
249
+ self,
250
+ path: str,
251
+ result_class: Type[TResult],
252
+ check_response: bool = True,
253
+ *,
254
+ params: (
255
+ Mapping[str | bytes | int | float, str | bytes | int | float | Iterable[str | bytes | int | float] | None]
256
+ | None
257
+ ) = None,
258
+ pangea_response: bool = True,
259
+ ) -> PangeaResponse[TResult] | TResult:
260
+ """
261
+ Makes the GET call to a Pangea Service endpoint.
262
+
263
+ Args:
264
+ path: Additional URL path
265
+ params: Dictionary of querystring data to attach to the request
266
+ pangea_response: Whether or not the response body follows Pangea's
267
+ standard response schema
268
+ """
269
+
150
270
  url = self._url(path)
151
271
  self.logger.debug(json.dumps({"service": self.service, "action": "get", "url": url}))
152
272
 
153
- async with self.session.get(url, headers=self._headers()) as requests_response:
273
+ async with self.session.get(url, params=params, headers=self._headers()) as requests_response:
154
274
  await self._check_http_errors(requests_response)
155
- pangea_response = PangeaResponse(
275
+
276
+ if not pangea_response:
277
+ type_adapter = TypeAdapter(result_class)
278
+ return type_adapter.validate_python(await requests_response.json())
279
+
280
+ pangea_response_obj = PangeaResponse(
156
281
  requests_response, result_class=result_class, json=await requests_response.json()
157
282
  )
158
283
 
159
284
  self.logger.debug(
160
285
  json.dumps(
161
- {"service": self.service, "action": "get", "url": url, "response": pangea_response.json},
286
+ {"service": self.service, "action": "get", "url": url, "response": pangea_response_obj.json},
162
287
  default=default_encoder,
163
288
  )
164
289
  )
165
290
 
166
291
  if check_response is False:
167
- return pangea_response
292
+ return pangea_response_obj
168
293
 
169
- return self._check_response(pangea_response)
294
+ return self._check_response(pangea_response_obj)
170
295
 
171
296
  async def _check_http_errors(self, resp: aiohttp.ClientResponse):
172
297
  if resp.status == 503:
@@ -300,6 +425,14 @@ class PangeaRequestAsync(PangeaRequestBase):
300
425
  attached_files = await self._get_attached_files(multipart_reader)
301
426
  return MultipartResponse(pangea_json, attached_files) # type: ignore[arg-type]
302
427
 
428
+ async def _http_delete(
429
+ self,
430
+ url: str,
431
+ *,
432
+ headers: Mapping[str, str | bytes | None] = {},
433
+ ) -> aiohttp.ClientResponse:
434
+ return await self.session.delete(url, headers=headers)
435
+
303
436
  async def _http_post(
304
437
  self,
305
438
  url: str,
@@ -7,6 +7,7 @@ from .authz import AuthZAsync
7
7
  from .embargo import EmbargoAsync
8
8
  from .file_scan import FileScanAsync
9
9
  from .intel import DomainIntelAsync, FileIntelAsync, IpIntelAsync, UrlIntelAsync, UserIntelAsync
10
+ from .management import ManagementAsync
10
11
  from .prompt_guard import PromptGuardAsync
11
12
  from .redact import RedactAsync
12
13
  from .sanitize import SanitizeAsync
@@ -1,12 +1,29 @@
1
1
  from __future__ import annotations
2
2
 
3
- from collections.abc import Sequence
4
- from typing import overload
3
+ from collections.abc import Mapping, Sequence
4
+ from typing import Any, overload
5
+
6
+ from typing_extensions import Literal, TypeVar
5
7
 
6
8
  from pangea.asyncio.services.base import ServiceBaseAsync
7
9
  from pangea.config import PangeaConfig
8
10
  from pangea.response import PangeaResponse
9
- from pangea.services.ai_guard import LogFields, Message, Overrides, TextGuardResult
11
+ from pangea.services.ai_guard import (
12
+ AuditDataActivityConfig,
13
+ ConnectionsConfig,
14
+ ExtraInfo,
15
+ GuardResult,
16
+ LogFields,
17
+ Message,
18
+ Overrides,
19
+ RecipeConfig,
20
+ ServiceConfig,
21
+ ServiceConfigFilter,
22
+ ServiceConfigsPage,
23
+ TextGuardResult,
24
+ )
25
+
26
+ _T = TypeVar("_T")
10
27
 
11
28
 
12
29
  class AIGuardAsync(ServiceBaseAsync):
@@ -15,11 +32,9 @@ class AIGuardAsync(ServiceBaseAsync):
15
32
  Provides methods to interact with Pangea's AI Guard service.
16
33
 
17
34
  Examples:
18
- from pangea import PangeaConfig
19
35
  from pangea.asyncio.services import AIGuardAsync
20
36
 
21
- config = PangeaConfig(domain="aws.us.pangea.cloud")
22
- ai_guard = AIGuardAsync(token="pangea_token", config=config)
37
+ ai_guard = AIGuardAsync(token="pangea_token")
23
38
  """
24
39
 
25
40
  service_name = "ai-guard"
@@ -39,11 +54,9 @@ class AIGuardAsync(ServiceBaseAsync):
39
54
  config_id: Configuration ID.
40
55
 
41
56
  Examples:
42
- from pangea import PangeaConfig
43
57
  from pangea.asyncio.services import AIGuardAsync
44
58
 
45
- config = PangeaConfig(domain="aws.us.pangea.cloud")
46
- ai_guard = AIGuardAsync(token="pangea_token", config=config)
59
+ ai_guard = AIGuardAsync(token="pangea_token")
47
60
  """
48
61
 
49
62
  super().__init__(token, config, logger_name, config_id)
@@ -53,29 +66,31 @@ class AIGuardAsync(ServiceBaseAsync):
53
66
  self,
54
67
  text: str,
55
68
  *,
56
- recipe: str | None = None,
57
69
  debug: bool | None = None,
58
- overrides: Overrides | None = None,
59
70
  log_fields: LogFields | None = None,
71
+ overrides: Overrides | None = None,
72
+ recipe: str | None = None,
60
73
  ) -> PangeaResponse[TextGuardResult]:
61
74
  """
62
- Text Guard for scanning LLM inputs and outputs
75
+ Guard LLM input and output text
63
76
 
64
- Analyze and redact text to avoid manipulation of the model, addition of
65
- malicious content, and other undesirable data transfers.
77
+ Detect, remove, or block malicious content and intent in LLM inputs and
78
+ outputs to prevent model manipulation and data leakage.
66
79
 
67
80
  OperationId: ai_guard_post_v1_text_guard
68
81
 
69
82
  Args:
70
83
  text: Text to be scanned by AI Guard for PII, sensitive data,
71
84
  malicious content, and other data types defined by the
72
- configuration. Supports processing up to 10KB of text.
73
- recipe: Recipe key of a configuration of data types and settings
74
- defined in the Pangea User Console. It specifies the rules that
75
- are to be applied to the text, such as defang malicious URLs.
85
+ configuration. Supports processing up to 20 KiB of text.
76
86
  debug: Setting this value to true will provide a detailed analysis
77
87
  of the text data
78
88
  log_field: Additional fields to include in activity log
89
+ overrides: Overrides flags. Note: This parameter has no effect when
90
+ the request is made by AIDR
91
+ recipe: Recipe key of a configuration of data types and settings
92
+ defined in the Pangea User Console. It specifies the rules that
93
+ are to be applied to the text, such as defang malicious URLs.
79
94
 
80
95
  Examples:
81
96
  response = await ai_guard.guard_text("text")
@@ -92,24 +107,26 @@ class AIGuardAsync(ServiceBaseAsync):
92
107
  log_fields: LogFields | None = None,
93
108
  ) -> PangeaResponse[TextGuardResult]:
94
109
  """
95
- Text Guard for scanning LLM inputs and outputs
110
+ Guard LLM input and output text
96
111
 
97
- Analyze and redact text to avoid manipulation of the model, addition of
98
- malicious content, and other undesirable data transfers.
112
+ Detect, remove, or block malicious content and intent in LLM inputs and
113
+ outputs to prevent model manipulation and data leakage.
99
114
 
100
115
  OperationId: ai_guard_post_v1_text_guard
101
116
 
102
117
  Args:
103
118
  messages: Structured messages data to be scanned by AI Guard for
104
119
  PII, sensitive data, malicious content, and other data types
105
- defined by the configuration. Supports processing up to 10KB of
106
- JSON text
107
- recipe: Recipe key of a configuration of data types and settings
108
- defined in the Pangea User Console. It specifies the rules that
109
- are to be applied to the text, such as defang malicious URLs.
120
+ defined by the configuration. Supports processing up to 20 KiB
121
+ of JSON text using Pangea message format.
110
122
  debug: Setting this value to true will provide a detailed analysis
111
123
  of the text data
112
124
  log_field: Additional fields to include in activity log
125
+ overrides: Overrides flags. Note: This parameter has no effect when
126
+ the request is made by AIDR
127
+ recipe: Recipe key of a configuration of data types and settings
128
+ defined in the Pangea User Console. It specifies the rules that
129
+ are to be applied to the text, such as defang malicious URLs.
113
130
 
114
131
  Examples:
115
132
  response = await ai_guard.guard_text(messages=[Message(role="user", content="hello world")])
@@ -126,10 +143,10 @@ class AIGuardAsync(ServiceBaseAsync):
126
143
  log_fields: LogFields | None = None,
127
144
  ) -> PangeaResponse[TextGuardResult]:
128
145
  """
129
- Text Guard for scanning LLM inputs and outputs
146
+ Guard LLM input and output text
130
147
 
131
- Analyze and redact text to avoid manipulation of the model, addition of
132
- malicious content, and other undesirable data transfers.
148
+ Detect, remove, or block malicious content and intent in LLM inputs and
149
+ outputs to prevent model manipulation and data leakage.
133
150
 
134
151
  OperationId: ai_guard_post_v1_text_guard
135
152
 
@@ -141,12 +158,14 @@ class AIGuardAsync(ServiceBaseAsync):
141
158
  PII, sensitive data, malicious content, and other data types
142
159
  defined by the configuration. Supports processing up to 10KB of
143
160
  JSON text
144
- recipe: Recipe key of a configuration of data types and settings
145
- defined in the Pangea User Console. It specifies the rules that
146
- are to be applied to the text, such as defang malicious URLs.
147
161
  debug: Setting this value to true will provide a detailed analysis
148
162
  of the text data
149
163
  log_field: Additional fields to include in activity log
164
+ overrides: Overrides flags. Note: This parameter has no effect when
165
+ the request is made by AIDR
166
+ recipe: Recipe key of a configuration of data types and settings
167
+ defined in the Pangea User Console. It specifies the rules that
168
+ are to be applied to the text, such as defang malicious URLs.
150
169
 
151
170
  Examples:
152
171
  response = await ai_guard.guard_text("text")
@@ -167,3 +186,157 @@ class AIGuardAsync(ServiceBaseAsync):
167
186
  "log_fields": log_fields,
168
187
  },
169
188
  )
189
+
190
+ async def guard(
191
+ self,
192
+ input: Mapping[str, Any],
193
+ *,
194
+ recipe: str | None = None,
195
+ debug: bool | None = None,
196
+ overrides: Overrides | None = None,
197
+ app_id: str | None = None,
198
+ actor_id: str | None = None,
199
+ llm_provider: str | None = None,
200
+ model: str | None = None,
201
+ model_version: str | None = None,
202
+ request_token_count: int | None = None,
203
+ response_token_count: int | None = None,
204
+ source_ip: str | None = None,
205
+ source_location: str | None = None,
206
+ tenant_id: str | None = None,
207
+ event_type: Literal["input", "output"] | None = None,
208
+ sensor_instance_id: str | None = None,
209
+ extra_info: ExtraInfo | None = None,
210
+ count_tokens: bool | None = None,
211
+ ) -> PangeaResponse[GuardResult]:
212
+ """
213
+ Guard LLM input and output
214
+
215
+ Analyze and redact content to avoid manipulation of the model, addition
216
+ of malicious content, and other undesirable data transfers.
217
+
218
+ OperationId: ai_guard_post_v1beta_guard
219
+
220
+ Args:
221
+ input: 'messages' (required) contains Prompt content and role array
222
+ in JSON format. The `content` is the multimodal text or image
223
+ input that will be analyzed. Additional properties such as
224
+ 'tools' may be provided for analysis.
225
+ recipe: Recipe key of a configuration of data types and settings defined in the Pangea User Console. It specifies the rules that are to be applied to the text, such as defang malicious URLs.
226
+ debug: Setting this value to true will provide a detailed analysis of the text data
227
+ app_name: Name of source application.
228
+ llm_provider: Underlying LLM. Example: 'OpenAI'.
229
+ model: Model used to perform the event. Example: 'gpt'.
230
+ model_version: Model version used to perform the event. Example: '3.5'.
231
+ request_token_count: Number of tokens in the request.
232
+ response_token_count: Number of tokens in the response.
233
+ source_ip: IP address of user or app or agent.
234
+ source_location: Location of user or app or agent.
235
+ tenant_id: For gateway-like integrations with multi-tenant support.
236
+ event_type: (AIDR) Event Type.
237
+ sensor_instance_id: (AIDR) sensor instance id.
238
+ extra_info: (AIDR) Logging schema.
239
+ count_tokens: Provide input and output token count.
240
+ """
241
+ return await self.request.post(
242
+ "v1beta/guard",
243
+ GuardResult,
244
+ data={
245
+ "input": input,
246
+ "recipe": recipe,
247
+ "debug": debug,
248
+ "overrides": overrides,
249
+ "app_id": app_id,
250
+ "actor_id": actor_id,
251
+ "llm_provider": llm_provider,
252
+ "model": model,
253
+ "model_version": model_version,
254
+ "request_token_count": request_token_count,
255
+ "response_token_count": response_token_count,
256
+ "source_ip": source_ip,
257
+ "source_location": source_location,
258
+ "tenant_id": tenant_id,
259
+ "event_type": event_type,
260
+ "sensor_instance_id": sensor_instance_id,
261
+ "extra_info": extra_info,
262
+ "count_tokens": count_tokens,
263
+ },
264
+ )
265
+
266
+ async def get_service_config(self, id: str) -> PangeaResponse[ServiceConfig]:
267
+ """
268
+ OperationId: ai_guard_post_v1beta_config
269
+ """
270
+ return await self.request.post("v1beta/config", data={"id": id}, result_class=ServiceConfig)
271
+
272
+ async def create_service_config(
273
+ self,
274
+ name: str,
275
+ *,
276
+ id: str | None = None,
277
+ audit_data_activity: AuditDataActivityConfig | None = None,
278
+ connections: ConnectionsConfig | None = None,
279
+ recipes: Mapping[str, RecipeConfig] | None = None,
280
+ ) -> PangeaResponse[ServiceConfig]:
281
+ """
282
+ OperationId: ai_guard_post_v1beta_config_create
283
+ """
284
+ return await self.request.post(
285
+ "v1beta/config/create",
286
+ data={
287
+ "name": name,
288
+ "id": id,
289
+ "audit_data_activity": audit_data_activity,
290
+ "connections": connections,
291
+ "recipes": recipes,
292
+ },
293
+ result_class=ServiceConfig,
294
+ )
295
+
296
+ async def update_service_config(
297
+ self,
298
+ id: str,
299
+ name: str,
300
+ *,
301
+ audit_data_activity: AuditDataActivityConfig | None = None,
302
+ connections: ConnectionsConfig | None = None,
303
+ recipes: Mapping[str, RecipeConfig] | None = None,
304
+ ) -> PangeaResponse[ServiceConfig]:
305
+ """
306
+ OperationId: ai_guard_post_v1beta_config_update
307
+ """
308
+ return await self.request.post(
309
+ "v1beta/config/update",
310
+ data={
311
+ "id": id,
312
+ "name": name,
313
+ "audit_data_activity": audit_data_activity,
314
+ "connections": connections,
315
+ "recipes": recipes,
316
+ },
317
+ result_class=ServiceConfig,
318
+ )
319
+
320
+ async def delete_service_config(self, id: str) -> PangeaResponse[ServiceConfig]:
321
+ """
322
+ OperationId: ai_guard_post_v1beta_config_delete
323
+ """
324
+ return await self.request.post("v1beta/config/delete", data={"id": id}, result_class=ServiceConfig)
325
+
326
+ async def list_service_configs(
327
+ self,
328
+ *,
329
+ filter: ServiceConfigFilter | None = None,
330
+ last: str | None = None,
331
+ order: Literal["asc", "desc"] | None = None,
332
+ order_by: Literal["id", "created_at", "updated_at"] | None = None,
333
+ size: int | None = None,
334
+ ) -> PangeaResponse[ServiceConfigsPage]:
335
+ """
336
+ OperationId: ai_guard_post_v1beta_config_list
337
+ """
338
+ return await self.request.post(
339
+ "v1beta/config/list",
340
+ data={"filter": filter, "last": last, "order": order, "order_by": order_by, "size": size},
341
+ result_class=ServiceConfigsPage,
342
+ )