dominus-sdk-python 2.7.1__tar.gz → 2.8.0__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.
Files changed (43) hide show
  1. {dominus_sdk_python-2.7.1 → dominus_sdk_python-2.8.0}/PKG-INFO +1 -1
  2. {dominus_sdk_python-2.7.1 → dominus_sdk_python-2.8.0}/dominus/__init__.py +10 -1
  3. {dominus_sdk_python-2.7.1 → dominus_sdk_python-2.8.0}/dominus/helpers/core.py +255 -2
  4. {dominus_sdk_python-2.7.1 → dominus_sdk_python-2.8.0}/dominus/namespaces/ai.py +61 -43
  5. {dominus_sdk_python-2.7.1 → dominus_sdk_python-2.8.0}/dominus_sdk_python.egg-info/PKG-INFO +1 -1
  6. {dominus_sdk_python-2.7.1 → dominus_sdk_python-2.8.0}/pyproject.toml +1 -1
  7. {dominus_sdk_python-2.7.1 → dominus_sdk_python-2.8.0}/README.md +0 -0
  8. {dominus_sdk_python-2.7.1 → dominus_sdk_python-2.8.0}/dominus/config/__init__.py +0 -0
  9. {dominus_sdk_python-2.7.1 → dominus_sdk_python-2.8.0}/dominus/config/endpoints.py +0 -0
  10. {dominus_sdk_python-2.7.1 → dominus_sdk_python-2.8.0}/dominus/errors.py +0 -0
  11. {dominus_sdk_python-2.7.1 → dominus_sdk_python-2.8.0}/dominus/helpers/__init__.py +0 -0
  12. {dominus_sdk_python-2.7.1 → dominus_sdk_python-2.8.0}/dominus/helpers/auth.py +0 -0
  13. {dominus_sdk_python-2.7.1 → dominus_sdk_python-2.8.0}/dominus/helpers/cache.py +0 -0
  14. {dominus_sdk_python-2.7.1 → dominus_sdk_python-2.8.0}/dominus/helpers/crypto.py +0 -0
  15. {dominus_sdk_python-2.7.1 → dominus_sdk_python-2.8.0}/dominus/helpers/sse.py +0 -0
  16. {dominus_sdk_python-2.7.1 → dominus_sdk_python-2.8.0}/dominus/namespaces/__init__.py +0 -0
  17. {dominus_sdk_python-2.7.1 → dominus_sdk_python-2.8.0}/dominus/namespaces/admin.py +0 -0
  18. {dominus_sdk_python-2.7.1 → dominus_sdk_python-2.8.0}/dominus/namespaces/auth.py +0 -0
  19. {dominus_sdk_python-2.7.1 → dominus_sdk_python-2.8.0}/dominus/namespaces/courier.py +0 -0
  20. {dominus_sdk_python-2.7.1 → dominus_sdk_python-2.8.0}/dominus/namespaces/db.py +0 -0
  21. {dominus_sdk_python-2.7.1 → dominus_sdk_python-2.8.0}/dominus/namespaces/ddl.py +0 -0
  22. {dominus_sdk_python-2.7.1 → dominus_sdk_python-2.8.0}/dominus/namespaces/fastapi.py +0 -0
  23. {dominus_sdk_python-2.7.1 → dominus_sdk_python-2.8.0}/dominus/namespaces/files.py +0 -0
  24. {dominus_sdk_python-2.7.1 → dominus_sdk_python-2.8.0}/dominus/namespaces/health.py +0 -0
  25. {dominus_sdk_python-2.7.1 → dominus_sdk_python-2.8.0}/dominus/namespaces/logs.py +0 -0
  26. {dominus_sdk_python-2.7.1 → dominus_sdk_python-2.8.0}/dominus/namespaces/open.py +0 -0
  27. {dominus_sdk_python-2.7.1 → dominus_sdk_python-2.8.0}/dominus/namespaces/oracle/__init__.py +0 -0
  28. {dominus_sdk_python-2.7.1 → dominus_sdk_python-2.8.0}/dominus/namespaces/oracle/audio_capture.py +0 -0
  29. {dominus_sdk_python-2.7.1 → dominus_sdk_python-2.8.0}/dominus/namespaces/oracle/oracle_websocket.py +0 -0
  30. {dominus_sdk_python-2.7.1 → dominus_sdk_python-2.8.0}/dominus/namespaces/oracle/session.py +0 -0
  31. {dominus_sdk_python-2.7.1 → dominus_sdk_python-2.8.0}/dominus/namespaces/oracle/types.py +0 -0
  32. {dominus_sdk_python-2.7.1 → dominus_sdk_python-2.8.0}/dominus/namespaces/oracle/vad_gate.py +0 -0
  33. {dominus_sdk_python-2.7.1 → dominus_sdk_python-2.8.0}/dominus/namespaces/portal.py +0 -0
  34. {dominus_sdk_python-2.7.1 → dominus_sdk_python-2.8.0}/dominus/namespaces/redis.py +0 -0
  35. {dominus_sdk_python-2.7.1 → dominus_sdk_python-2.8.0}/dominus/namespaces/secrets.py +0 -0
  36. {dominus_sdk_python-2.7.1 → dominus_sdk_python-2.8.0}/dominus/namespaces/secure.py +0 -0
  37. {dominus_sdk_python-2.7.1 → dominus_sdk_python-2.8.0}/dominus/services/__init__.py +0 -0
  38. {dominus_sdk_python-2.7.1 → dominus_sdk_python-2.8.0}/dominus/start.py +0 -0
  39. {dominus_sdk_python-2.7.1 → dominus_sdk_python-2.8.0}/dominus_sdk_python.egg-info/SOURCES.txt +0 -0
  40. {dominus_sdk_python-2.7.1 → dominus_sdk_python-2.8.0}/dominus_sdk_python.egg-info/dependency_links.txt +0 -0
  41. {dominus_sdk_python-2.7.1 → dominus_sdk_python-2.8.0}/dominus_sdk_python.egg-info/requires.txt +0 -0
  42. {dominus_sdk_python-2.7.1 → dominus_sdk_python-2.8.0}/dominus_sdk_python.egg-info/top_level.txt +0 -0
  43. {dominus_sdk_python-2.7.1 → dominus_sdk_python-2.8.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dominus-sdk-python
3
- Version: 2.7.1
3
+ Version: 2.8.0
4
4
  Summary: Python SDK for the Dominus Orchestrator Platform
5
5
  Author-email: CareBridge Systems <dev@carebridge.io>
6
6
  License: Proprietary
@@ -126,6 +126,12 @@ from .helpers.cache import (
126
126
  exponential_backoff_with_jitter,
127
127
  )
128
128
 
129
+ # Export JWT verification utilities
130
+ from .helpers.core import (
131
+ verify_jwt_locally,
132
+ is_jwt_valid,
133
+ )
134
+
129
135
  # Export error classes
130
136
  from .errors import (
131
137
  DominusError,
@@ -140,7 +146,7 @@ from .errors import (
140
146
  TimeoutError as DominusTimeoutError,
141
147
  )
142
148
 
143
- __version__ = "2.7.1"
149
+ __version__ = "2.8.0"
144
150
  __all__ = [
145
151
  # Main SDK instance
146
152
  "dominus",
@@ -186,6 +192,9 @@ __all__ = [
186
192
  "DominusCache",
187
193
  "orchestrator_circuit_breaker",
188
194
  "exponential_backoff_with_jitter",
195
+ # JWT verification utilities
196
+ "verify_jwt_locally",
197
+ "is_jwt_valid",
189
198
  # Error classes
190
199
  "DominusError",
191
200
  "AuthenticationError",
@@ -41,10 +41,10 @@ def _b64url_decode(s: str) -> bytes:
41
41
  def _decode_jwt_payload(jwt: str) -> dict:
42
42
  """
43
43
  Decode JWT payload without verification (for exp checks only).
44
-
44
+
45
45
  Args:
46
46
  jwt: JWT token string
47
-
47
+
48
48
  Returns:
49
49
  Decoded payload dict
50
50
  """
@@ -59,6 +59,259 @@ def _decode_jwt_payload(jwt: str) -> dict:
59
59
  raise ValueError(f"Failed to decode JWT payload: {e}")
60
60
 
61
61
 
62
+ def _decode_jwt_header(jwt: str) -> dict:
63
+ """
64
+ Decode JWT header without verification.
65
+
66
+ Args:
67
+ jwt: JWT token string
68
+
69
+ Returns:
70
+ Decoded header dict (contains alg, kid, typ)
71
+ """
72
+ try:
73
+ parts = jwt.split('.')
74
+ if len(parts) != 3:
75
+ raise ValueError("Invalid JWT format")
76
+ header_b64url = parts[0]
77
+ header_bytes = _b64url_decode(header_b64url)
78
+ return json.loads(header_bytes.decode('utf-8'))
79
+ except Exception as e:
80
+ raise ValueError(f"Failed to decode JWT header: {e}")
81
+
82
+
83
+ # ============================================
84
+ # JWKS and Local JWT Verification
85
+ # ============================================
86
+
87
+ # JWKS cache
88
+ _cached_jwks: Optional[dict] = None
89
+ _jwks_cache_time: float = 0
90
+ _JWKS_CACHE_TTL = 3600 # 1 hour in seconds
91
+
92
+
93
+ async def _fetch_jwks() -> dict:
94
+ """
95
+ Fetch JWKS from gateway.
96
+
97
+ Returns:
98
+ JWKS dict with 'keys' array
99
+ """
100
+ global _cached_jwks, _jwks_cache_time
101
+
102
+ # Check cache first
103
+ if _cached_jwks and time.time() - _jwks_cache_time < _JWKS_CACHE_TTL:
104
+ return _cached_jwks
105
+
106
+ from ..config.endpoints import get_gateway_url
107
+ gateway_url = get_gateway_url()
108
+
109
+ try:
110
+ async with httpx.AsyncClient(timeout=10.0) as client:
111
+ response = await client.get(f"{gateway_url}/jwt/jwks")
112
+ response.raise_for_status()
113
+
114
+ # Response is base64 encoded
115
+ result = _b64_decode(response.text)
116
+
117
+ # Handle wrapped response or direct JWKS
118
+ if isinstance(result, dict):
119
+ if result.get("data", {}).get("keys"):
120
+ jwks = result["data"]
121
+ elif result.get("keys"):
122
+ jwks = result
123
+ else:
124
+ raise ValueError("Invalid JWKS response format")
125
+ else:
126
+ raise ValueError("Invalid JWKS response format")
127
+
128
+ _cached_jwks = jwks
129
+ _jwks_cache_time = time.time()
130
+ return jwks
131
+
132
+ except Exception as e:
133
+ # If fetch fails but we have cached JWKS, use it (stale is better than nothing)
134
+ if _cached_jwks:
135
+ return _cached_jwks
136
+ raise RuntimeError(f"Failed to fetch JWKS: {e}")
137
+
138
+
139
+ def _jwk_to_rsa_public_key(jwk: dict):
140
+ """
141
+ Convert JWK to RSA public key for verification.
142
+
143
+ Args:
144
+ jwk: JWK dict with n, e components
145
+
146
+ Returns:
147
+ RSA public key object
148
+ """
149
+ from cryptography.hazmat.primitives.asymmetric import rsa
150
+ from cryptography.hazmat.backends import default_backend
151
+
152
+ # Decode n and e from base64url
153
+ n_bytes = _b64url_decode(jwk["n"])
154
+ e_bytes = _b64url_decode(jwk["e"])
155
+
156
+ # Convert to integers
157
+ n = int.from_bytes(n_bytes, byteorder='big')
158
+ e = int.from_bytes(e_bytes, byteorder='big')
159
+
160
+ # Create RSA public numbers and derive public key
161
+ public_numbers = rsa.RSAPublicNumbers(e, n)
162
+ return public_numbers.public_key(default_backend())
163
+
164
+
165
+ async def verify_jwt_signature(jwt_token: str) -> dict:
166
+ """
167
+ Verify JWT signature locally using JWKS.
168
+
169
+ Args:
170
+ jwt_token: The JWT token to verify
171
+
172
+ Returns:
173
+ The decoded payload if valid
174
+
175
+ Raises:
176
+ RuntimeError: If verification fails
177
+ """
178
+ global _cached_jwks
179
+
180
+ from cryptography.hazmat.primitives import hashes
181
+ from cryptography.hazmat.primitives.asymmetric import padding
182
+ from cryptography.exceptions import InvalidSignature
183
+
184
+ parts = jwt_token.split('.')
185
+ if len(parts) != 3:
186
+ raise RuntimeError("Invalid JWT format")
187
+
188
+ header_b64, payload_b64, signature_b64 = parts
189
+
190
+ # Decode header to get kid
191
+ header = _decode_jwt_header(jwt_token)
192
+
193
+ # Verify algorithm
194
+ if header.get("alg") != "RS256":
195
+ raise RuntimeError(f"Unsupported JWT algorithm: {header.get('alg')}")
196
+
197
+ # Fetch JWKS and find the key
198
+ jwks = await _fetch_jwks()
199
+ kid = header.get("kid")
200
+
201
+ key = None
202
+ if kid:
203
+ for k in jwks.get("keys", []):
204
+ if k.get("kid") == kid:
205
+ key = k
206
+ break
207
+ else:
208
+ # Use first key if no kid
209
+ keys = jwks.get("keys", [])
210
+ if keys:
211
+ key = keys[0]
212
+
213
+ if not key:
214
+ # Key not found - refresh JWKS and try again
215
+ _cached_jwks = None
216
+ jwks = await _fetch_jwks()
217
+
218
+ if kid:
219
+ for k in jwks.get("keys", []):
220
+ if k.get("kid") == kid:
221
+ key = k
222
+ break
223
+ else:
224
+ keys = jwks.get("keys", [])
225
+ if keys:
226
+ key = keys[0]
227
+
228
+ if not key:
229
+ raise RuntimeError(f"JWT signing key not found: {kid}")
230
+
231
+ # Import the public key
232
+ public_key = _jwk_to_rsa_public_key(key)
233
+
234
+ # Verify signature
235
+ signature_input = f"{header_b64}.{payload_b64}".encode('utf-8')
236
+ signature = _b64url_decode(signature_b64)
237
+
238
+ try:
239
+ public_key.verify(
240
+ signature,
241
+ signature_input,
242
+ padding.PKCS1v15(),
243
+ hashes.SHA256()
244
+ )
245
+ except InvalidSignature:
246
+ raise RuntimeError("JWT signature verification failed")
247
+
248
+ # Decode and return payload
249
+ return _decode_jwt_payload(jwt_token)
250
+
251
+
252
+ async def verify_jwt_locally(jwt_token: str) -> dict:
253
+ """
254
+ Verify JWT locally: signature + claims.
255
+
256
+ Performs full local verification:
257
+ 1. Signature verification using JWKS
258
+ 2. Expiry check (exp claim)
259
+ 3. Not-before check (nbf claim if present)
260
+ 4. Issued-at sanity check (iat claim)
261
+
262
+ Args:
263
+ jwt_token: The JWT token to verify
264
+
265
+ Returns:
266
+ The decoded payload if valid
267
+
268
+ Raises:
269
+ RuntimeError: If verification fails
270
+ """
271
+ # Verify signature
272
+ payload = await verify_jwt_signature(jwt_token)
273
+
274
+ now = int(time.time())
275
+
276
+ # Check expiry
277
+ if payload.get("exp") and payload["exp"] < now:
278
+ raise RuntimeError("JWT has expired")
279
+
280
+ # Check not-before
281
+ if payload.get("nbf") and payload["nbf"] > now:
282
+ raise RuntimeError("JWT not yet valid")
283
+
284
+ # Sanity check issued-at (not more than 24 hours in the future)
285
+ if payload.get("iat") and payload["iat"] > now + 86400:
286
+ raise RuntimeError("JWT issued-at is in the future")
287
+
288
+ return payload
289
+
290
+
291
+ def is_jwt_valid(jwt_token: str) -> bool:
292
+ """
293
+ Quick local JWT validation (expiry only, no signature verification).
294
+ Use this for fast checks before making API calls.
295
+
296
+ Args:
297
+ jwt_token: The JWT token to check
298
+
299
+ Returns:
300
+ True if token appears valid (not expired), False otherwise
301
+ """
302
+ try:
303
+ payload = _decode_jwt_payload(jwt_token)
304
+ now = int(time.time())
305
+
306
+ # Check if expired (with 60 second buffer)
307
+ if payload.get("exp") and payload["exp"] < now - 60:
308
+ return False
309
+
310
+ return True
311
+ except Exception:
312
+ return False
313
+
314
+
62
315
  async def _get_service_jwt(psk_token: str, base_url: str) -> str:
63
316
  """
64
317
  Get service JWT by calling gateway /jwt/mint with PSK.
@@ -211,7 +211,11 @@ class RagSubNamespace(GatewayMixin):
211
211
  slug: str,
212
212
  identifier: str,
213
213
  content: str,
214
+ name: Optional[str] = None,
215
+ description: Optional[str] = None,
214
216
  category: Optional[str] = None,
217
+ subcategory: Optional[str] = None,
218
+ source_reference: Optional[Dict[str, Any]] = None,
215
219
  metadata: Optional[Dict[str, Any]] = None
216
220
  ) -> Dict[str, Any]:
217
221
  """
@@ -220,13 +224,25 @@ class RagSubNamespace(GatewayMixin):
220
224
  Args:
221
225
  slug: Corpus identifier
222
226
  identifier: Entry identifier (unique within corpus)
223
- content: Text content to embed
227
+ content: Text content to embed (stored as content_markdown)
228
+ name: Optional human-readable name
229
+ description: Optional brief description
224
230
  category: Optional category for filtering
231
+ subcategory: Optional subcategory
232
+ source_reference: Optional source attribution
225
233
  metadata: Optional metadata dict
226
234
  """
227
- body: Dict[str, Any] = {"content": content}
235
+ body: Dict[str, Any] = {"content_markdown": content}
236
+ if name:
237
+ body["name"] = name
238
+ if description:
239
+ body["description"] = description
228
240
  if category:
229
241
  body["category"] = category
242
+ if subcategory:
243
+ body["subcategory"] = subcategory
244
+ if source_reference:
245
+ body["source_reference"] = source_reference
230
246
  if metadata:
231
247
  body["metadata"] = metadata
232
248
 
@@ -260,11 +276,30 @@ class RagSubNamespace(GatewayMixin):
260
276
 
261
277
  Args:
262
278
  slug: Corpus identifier
263
- entries: List of {"identifier", "content", "category", "metadata"} dicts
279
+ entries: List of entry dicts with:
280
+ - identifier: string (required)
281
+ - content: string (required) - Will be sent as content_markdown
282
+ - name: string (optional)
283
+ - description: string (optional)
284
+ - category: string (optional)
285
+ - subcategory: string (optional)
286
+ - source_reference: dict (optional)
287
+ - metadata: dict (optional)
288
+
289
+ Returns:
290
+ Dict with "processed", "failed", "errors" counts
264
291
  """
292
+ # Transform 'content' to 'content_markdown' for API compatibility
293
+ transformed = []
294
+ for entry in entries:
295
+ e = dict(entry)
296
+ if "content" in e and "content_markdown" not in e:
297
+ e["content_markdown"] = e.pop("content")
298
+ transformed.append(e)
299
+
265
300
  return await self._api(
266
301
  endpoint=f"/api/rag/{slug}/bulk",
267
- body={"entries": entries}
302
+ body={"entries": transformed}
268
303
  )
269
304
 
270
305
  async def search(
@@ -390,16 +425,12 @@ class ArtifactsSubNamespace(GatewayMixin):
390
425
  async def get(
391
426
  self,
392
427
  artifact_id: str,
393
- conversation_id: Optional[str] = None
428
+ conversation_id: str
394
429
  ) -> Dict[str, Any]:
395
430
  """Get artifact by ID."""
396
- body: Dict[str, Any] = {"artifact_id": artifact_id}
397
- if conversation_id:
398
- body["conversation_id"] = conversation_id
399
-
400
431
  return await self._api(
401
- endpoint=f"/api/agent/artifacts/{artifact_id}",
402
- body=body
432
+ endpoint=f"/api/agent/artifacts/{artifact_id}?conversation_id={conversation_id}",
433
+ method="GET"
403
434
  )
404
435
 
405
436
  async def create(
@@ -407,27 +438,28 @@ class ArtifactsSubNamespace(GatewayMixin):
407
438
  name: str,
408
439
  content: str,
409
440
  conversation_id: str,
410
- artifact_type: str = "text",
441
+ artifact_type: str = "text/plain",
442
+ is_base64: bool = False,
411
443
  metadata: Optional[Dict[str, Any]] = None
412
444
  ) -> Dict[str, Any]:
413
445
  """
414
446
  Create a new artifact.
415
447
 
416
448
  Args:
417
- name: Artifact name/filename
418
- content: Artifact content (text or base64 for binary)
449
+ name: Artifact key/filename
450
+ content: Artifact content (text or base64-encoded for binary)
419
451
  conversation_id: Associated conversation ID
420
- artifact_type: Type of artifact (text, code, image, etc.)
421
- metadata: Optional metadata dict
452
+ artifact_type: MIME content type (e.g., "text/plain", "text/html", "image/png")
453
+ is_base64: If True, content is base64-encoded binary
454
+ metadata: Optional metadata dict (not stored, for SDK use)
422
455
  """
423
456
  body: Dict[str, Any] = {
424
- "name": name,
457
+ "key": name,
425
458
  "content": content,
426
459
  "conversation_id": conversation_id,
427
- "type": artifact_type
460
+ "content_type": artifact_type,
461
+ "is_base64": is_base64
428
462
  }
429
- if metadata:
430
- body["metadata"] = metadata
431
463
 
432
464
  return await self._api(
433
465
  endpoint="/api/agent/artifacts",
@@ -436,38 +468,24 @@ class ArtifactsSubNamespace(GatewayMixin):
436
468
 
437
469
  async def list(
438
470
  self,
439
- conversation_id: str,
440
- limit: int = 100,
441
- offset: int = 0
471
+ conversation_id: str
442
472
  ) -> List[Dict[str, Any]]:
443
473
  """List artifacts for a conversation."""
444
- body: Dict[str, Any] = {
445
- "conversation_id": conversation_id,
446
- "limit": limit,
447
- "offset": offset
448
- }
449
-
450
474
  result = await self._api(
451
- endpoint="/api/agent/artifacts",
452
- method="GET",
453
- body=body
475
+ endpoint=f"/api/agent/artifacts?conversation_id={conversation_id}",
476
+ method="GET"
454
477
  )
455
478
  return result.get("artifacts", result) if isinstance(result, dict) else result
456
479
 
457
480
  async def delete(
458
481
  self,
459
482
  artifact_id: str,
460
- conversation_id: Optional[str] = None
483
+ conversation_id: str
461
484
  ) -> Dict[str, Any]:
462
485
  """Delete an artifact."""
463
- body: Dict[str, Any] = {}
464
- if conversation_id:
465
- body["conversation_id"] = conversation_id
466
-
467
486
  return await self._api(
468
- endpoint=f"/api/agent/artifacts/{artifact_id}",
469
- method="DELETE",
470
- body=body if body else None
487
+ endpoint=f"/api/agent/artifacts/{artifact_id}?conversation_id={conversation_id}",
488
+ method="DELETE"
471
489
  )
472
490
 
473
491
 
@@ -900,10 +918,10 @@ class AiNamespace(GatewayMixin):
900
918
  List of history messages
901
919
  """
902
920
  result = await self._api(
903
- endpoint=f"/api/agent/history/{conversation_id}",
904
- body={"limit": limit}
921
+ endpoint=f"/api/agent/history/{conversation_id}?limit={limit}",
922
+ method="GET"
905
923
  )
906
- return result.get("history", result) if isinstance(result, dict) else result
924
+ return result.get("messages", result) if isinstance(result, dict) else result
907
925
 
908
926
  # ========================================
909
927
  # LLM Completions
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dominus-sdk-python
3
- Version: 2.7.1
3
+ Version: 2.8.0
4
4
  Summary: Python SDK for the Dominus Orchestrator Platform
5
5
  Author-email: CareBridge Systems <dev@carebridge.io>
6
6
  License: Proprietary
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dominus-sdk-python"
7
- version = "2.7.1"
7
+ version = "2.8.0"
8
8
  description = "Python SDK for the Dominus Orchestrator Platform"
9
9
  readme = "README.md"
10
10
  license = {text = "Proprietary"}