swiftapi-python 1.2.2__py3-none-any.whl → 1.3.0__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.
swiftapi/client.py CHANGED
@@ -1,309 +1,328 @@
1
- """
2
- SwiftAPI SDK - HTTP Client
3
-
4
- This module provides the HTTP client for communicating with SwiftAPI.
5
- """
6
-
7
- import requests
8
- from typing import Dict, Any, Optional, List
9
-
10
- from . import __version__
11
- from .exceptions import (
12
- SwiftAPIError,
13
- AuthenticationError,
14
- PolicyViolation,
15
- RateLimitError,
16
- NetworkError,
17
- )
18
-
19
- DEFAULT_BASE_URL = "https://swiftapi.ai"
20
- DEFAULT_TIMEOUT = 30
21
-
22
-
23
- class SwiftAPI:
24
- """
25
- SwiftAPI HTTP Client.
26
-
27
- Usage:
28
- api = SwiftAPI(key="swiftapi_live_...")
29
- result = api.verify("file_write", "Save config", {"path": "/etc/config"})
30
- """
31
-
32
- def __init__(
33
- self,
34
- key: str,
35
- base_url: str = DEFAULT_BASE_URL,
36
- timeout: int = DEFAULT_TIMEOUT,
37
- ):
38
- """
39
- Initialize SwiftAPI client.
40
-
41
- Args:
42
- key: SwiftAPI authority key (swiftapi_live_...)
43
- base_url: API base URL (default: https://swiftapi.ai)
44
- timeout: Request timeout in seconds
45
- """
46
- if not key:
47
- raise AuthenticationError("API key is required")
48
- if not key.startswith("swiftapi_live_"):
49
- raise AuthenticationError("Invalid key format: must start with 'swiftapi_live_'")
50
-
51
- self.key = key
52
- self.base_url = base_url.rstrip("/")
53
- self.timeout = timeout
54
- self._session = requests.Session()
55
- self._session.headers.update({
56
- "X-SwiftAPI-Authority": key,
57
- "Content-Type": "application/json",
58
- "User-Agent": f"swiftapi-python/{__version__}",
59
- })
60
-
61
- def _request(
62
- self,
63
- method: str,
64
- endpoint: str,
65
- json: Dict = None,
66
- params: Dict = None,
67
- ) -> Dict[str, Any]:
68
- """Make HTTP request to SwiftAPI."""
69
- url = f"{self.base_url}{endpoint}"
70
-
71
- try:
72
- response = self._session.request(
73
- method=method,
74
- url=url,
75
- json=json,
76
- params=params,
77
- timeout=self.timeout,
78
- )
79
- except requests.exceptions.ConnectionError as e:
80
- raise NetworkError(f"Failed to connect to SwiftAPI: {e}")
81
- except requests.exceptions.Timeout:
82
- raise NetworkError(f"Request timed out after {self.timeout}s")
83
- except requests.exceptions.RequestException as e:
84
- raise NetworkError(f"Request failed: {e}")
85
-
86
- # Handle response
87
- return self._handle_response(response)
88
-
89
- def _handle_response(self, response: requests.Response) -> Dict[str, Any]:
90
- """Handle API response and raise appropriate exceptions."""
91
- # Rate limit
92
- if response.status_code == 429:
93
- retry_after = response.headers.get("Retry-After")
94
- raise RateLimitError(int(retry_after) if retry_after else None)
95
-
96
- # Auth errors
97
- if response.status_code == 401:
98
- raise AuthenticationError("Invalid API key")
99
- if response.status_code == 403:
100
- try:
101
- data = response.json()
102
- raise AuthenticationError(data.get("detail", {}).get("message", "Forbidden"))
103
- except ValueError:
104
- raise AuthenticationError("Forbidden")
105
-
106
- # Parse JSON
107
- try:
108
- data = response.json()
109
- except ValueError:
110
- raise SwiftAPIError(f"Invalid JSON response: {response.text[:200]}")
111
-
112
- # Policy violations (denied actions)
113
- if response.status_code == 200 and data.get("approved") is False:
114
- reason = data.get("denial_reason") or data.get("reason", "Action denied by policy")
115
- action_type = data.get("action_type") or data.get("action", {}).get("type")
116
- raise PolicyViolation(
117
- message=reason,
118
- action_type=action_type,
119
- denial_reason=reason,
120
- )
121
-
122
- # Other errors
123
- if response.status_code >= 400:
124
- detail = data.get("detail", {})
125
- if isinstance(detail, dict):
126
- message = detail.get("message", str(detail))
127
- else:
128
- message = str(detail)
129
- raise SwiftAPIError(message, response.status_code, data)
130
-
131
- return data
132
-
133
- # =========================================================================
134
- # Core Endpoints
135
- # =========================================================================
136
-
137
- def verify(
138
- self,
139
- action_type: str,
140
- intent: str,
141
- params: Optional[Dict[str, Any]] = None,
142
- actor: str = "sdk",
143
- app_id: str = "swiftapi-python",
144
- ) -> Dict[str, Any]:
145
- """
146
- Submit an action for verification and get an execution attestation.
147
-
148
- Args:
149
- action_type: Type of action (e.g., "file_write", "api_call")
150
- intent: Human-readable description of intent
151
- params: Optional action parameters
152
- actor: Identifier for the requesting agent
153
- app_id: Application identifier
154
-
155
- Returns:
156
- Full verification response including execution_attestation
157
-
158
- Raises:
159
- PolicyViolation: If action is denied
160
- AuthenticationError: If key is invalid
161
- SwiftAPIError: For other errors
162
- """
163
- payload = {
164
- "action": {
165
- "type": action_type,
166
- "intent": intent,
167
- "params": params or {},
168
- },
169
- "actor": actor,
170
- "app_id": app_id,
171
- }
172
- return self._request("POST", "/verify", json=payload)
173
-
174
- def verify_attestation(self, attestation: Dict[str, Any]) -> Dict[str, Any]:
175
- """
176
- Verify an existing attestation with the server.
177
-
178
- Args:
179
- attestation: The execution_attestation to verify
180
-
181
- Returns:
182
- Verification result
183
- """
184
- return self._request("POST", "/attestation/verify", json=attestation)
185
-
186
- def revoke(self, jti: str, reason: str = "SDK revocation") -> Dict[str, Any]:
187
- """
188
- Revoke an attestation by JTI.
189
-
190
- Args:
191
- jti: The attestation's unique identifier
192
- reason: Reason for revocation
193
-
194
- Returns:
195
- Revocation confirmation
196
- """
197
- payload = {"jti": jti, "reason": reason}
198
- return self._request("POST", "/attestation/revoke", json=payload)
199
-
200
- def check_revocation(self, jti: str) -> bool:
201
- """
202
- Check if an attestation has been revoked.
203
-
204
- This is a fast check - use for real-time revocation verification.
205
-
206
- Args:
207
- jti: The attestation's unique identifier
208
-
209
- Returns:
210
- True if revoked, False if valid
211
- """
212
- try:
213
- result = self._request("GET", f"/attestation/revoked/{jti}")
214
- return result.get("revoked", False)
215
- except SwiftAPIError:
216
- # If endpoint fails, assume not revoked (fail-open for availability)
217
- return False
218
-
219
- def attest(
220
- self,
221
- action_type: str,
222
- action_data: Dict[str, Any],
223
- ) -> Dict[str, Any]:
224
- """
225
- Request an execution attestation from the authority.
226
-
227
- Calls /attest to get a signed attestation for the given action.
228
- The attestation can then be passed to gated endpoints like /chat/vibe.
229
-
230
- Args:
231
- action_type: Type of action (e.g., "chat_completion")
232
- action_data: Action payload to be attested
233
-
234
- Returns:
235
- Signed attestation dict with jti, signature, action_fingerprint, etc.
236
- """
237
- payload = {
238
- "action_type": action_type,
239
- "action_data": action_data,
240
- }
241
- return self._request("POST", "/attest", json=payload)
242
-
243
- # =========================================================================
244
- # Info Endpoints
245
- # =========================================================================
246
-
247
- def get_info(self) -> Dict[str, Any]:
248
- """Get API info and public key."""
249
- return self._request("GET", "/")
250
-
251
- def health(self) -> bool:
252
- """Check API health."""
253
- try:
254
- result = self._request("GET", "/health")
255
- return result.get("status") == "healthy"
256
- except Exception:
257
- return False
258
-
259
- def get_policies(self) -> List[Dict[str, Any]]:
260
- """Get active policies."""
261
- result = self._request("GET", "/policies")
262
- return result.get("policies", [])
263
-
264
- def get_scopes(self) -> List[str]:
265
- """Get available authority scopes."""
266
- result = self._request("GET", "/authority/scopes")
267
- return result.get("scopes", [])
268
-
269
- # =========================================================================
270
- # Key Management (Admin Only)
271
- # =========================================================================
272
-
273
- def list_keys(self) -> List[Dict[str, Any]]:
274
- """List authority keys (requires admin scope)."""
275
- result = self._request("GET", "/authority/keys")
276
- return result.get("keys", [])
277
-
278
- def create_key(
279
- self,
280
- name: str,
281
- scopes: List[str],
282
- ) -> Dict[str, Any]:
283
- """
284
- Create a new authority key (requires admin scope).
285
-
286
- Args:
287
- name: Key name
288
- scopes: List of scopes to grant
289
-
290
- Returns:
291
- New key details (including the key itself - shown only once!)
292
- """
293
- payload = {"name": name, "scopes": scopes}
294
- return self._request("POST", "/authority/keys", json=payload)
295
-
296
- def revoke_key(self, key_hash: str) -> Dict[str, Any]:
297
- """Revoke an authority key by hash."""
298
- return self._request("DELETE", f"/authority/keys/{key_hash}")
299
-
300
- # =========================================================================
301
- # Context Manager
302
- # =========================================================================
303
-
304
- def __enter__(self):
305
- return self
306
-
307
- def __exit__(self, exc_type, exc_val, exc_tb):
308
- self._session.close()
309
- return False
1
+ """
2
+ SwiftAPI SDK - HTTP Client
3
+
4
+ This module provides the HTTP client for communicating with SwiftAPI.
5
+ """
6
+
7
+ import requests
8
+ from typing import Dict, Any, Optional, List
9
+
10
+ from . import __version__
11
+ from .exceptions import (
12
+ SwiftAPIError,
13
+ AuthenticationError,
14
+ PolicyViolation,
15
+ RateLimitError,
16
+ NetworkError,
17
+ )
18
+
19
+ DEFAULT_BASE_URL = "https://swiftapi.ai"
20
+ DEFAULT_TIMEOUT = 30
21
+
22
+
23
+ class SwiftAPI:
24
+ """
25
+ SwiftAPI HTTP Client.
26
+
27
+ Usage:
28
+ api = SwiftAPI(key="swiftapi_live_...")
29
+ result = api.verify("file_write", "Save config", {"path": "/etc/config"})
30
+ """
31
+
32
+ def __init__(
33
+ self,
34
+ key: str,
35
+ base_url: str = DEFAULT_BASE_URL,
36
+ timeout: int = DEFAULT_TIMEOUT,
37
+ ):
38
+ """
39
+ Initialize SwiftAPI client.
40
+
41
+ Args:
42
+ key: SwiftAPI authority key (swiftapi_live_...)
43
+ base_url: API base URL (default: https://swiftapi.ai)
44
+ timeout: Request timeout in seconds
45
+ """
46
+ if not key:
47
+ raise AuthenticationError("API key is required")
48
+ if not key.startswith("swiftapi_live_"):
49
+ raise AuthenticationError("Invalid key format: must start with 'swiftapi_live_'")
50
+
51
+ from urllib.parse import urlparse
52
+ parsed = urlparse(base_url)
53
+ if parsed.scheme != "https" or not parsed.hostname or parsed.username or parsed.password or parsed.query or parsed.fragment:
54
+ raise SwiftAPIError("Authority URL must use HTTPS without embedded credentials")
55
+ if not isinstance(timeout, (int, float)) or not 0 < timeout <= 300:
56
+ raise SwiftAPIError("Timeout must be positive and at most 300 seconds")
57
+ self.key = key
58
+ self.base_url = base_url.rstrip("/")
59
+ self.timeout = timeout
60
+ self._session = requests.Session()
61
+ self._session.headers.update({
62
+ "X-SwiftAPI-Authority": key,
63
+ "Content-Type": "application/json",
64
+ "User-Agent": f"swiftapi-python/{__version__}",
65
+ })
66
+
67
+ def _request(
68
+ self,
69
+ method: str,
70
+ endpoint: str,
71
+ json: Dict = None,
72
+ params: Dict = None,
73
+ ) -> Dict[str, Any]:
74
+ """Make HTTP request to SwiftAPI."""
75
+ url = f"{self.base_url}{endpoint}"
76
+
77
+ try:
78
+ response = self._session.request(
79
+ method=method,
80
+ url=url,
81
+ json=json,
82
+ params=params,
83
+ timeout=self.timeout,
84
+ allow_redirects=False,
85
+ )
86
+ except requests.exceptions.ConnectionError:
87
+ raise NetworkError("Failed to connect to SwiftAPI") from None
88
+ except requests.exceptions.Timeout:
89
+ raise NetworkError(f"Request timed out after {self.timeout}s")
90
+ except requests.exceptions.RequestException:
91
+ raise NetworkError("SwiftAPI request failed") from None
92
+
93
+ # Handle response
94
+ return self._handle_response(response)
95
+
96
+ def _handle_response(self, response: requests.Response) -> Dict[str, Any]:
97
+ """Handle API response and raise appropriate exceptions."""
98
+ if 300 <= response.status_code < 400:
99
+ raise SwiftAPIError("Authority redirect refused", response.status_code)
100
+ # Rate limit
101
+ if response.status_code == 429:
102
+ retry_after = response.headers.get("Retry-After")
103
+ raise RateLimitError(int(retry_after) if retry_after and retry_after.isdigit() else None)
104
+
105
+ # Auth errors
106
+ if response.status_code == 401:
107
+ raise AuthenticationError("Invalid API key")
108
+ if response.status_code == 403:
109
+ try:
110
+ data = response.json()
111
+ detail = data.get("detail", {}) if isinstance(data, dict) else {}
112
+ raise AuthenticationError(detail.get("message", "Forbidden") if isinstance(detail, dict) else "Forbidden")
113
+ except ValueError:
114
+ raise AuthenticationError("Forbidden")
115
+
116
+ # Parse JSON
117
+ try:
118
+ data = response.json()
119
+ except ValueError:
120
+ raise SwiftAPIError("Invalid JSON response")
121
+
122
+ if not isinstance(data, dict):
123
+ raise SwiftAPIError("Expected JSON object response")
124
+
125
+ # Policy violations (denied actions)
126
+ if response.status_code == 200 and data.get("approved") is False:
127
+ reason = data.get("denial_reason") or data.get("reason", "Action denied by policy")
128
+ action_type = data.get("action_type") or data.get("action", {}).get("type")
129
+ raise PolicyViolation(
130
+ message=reason,
131
+ action_type=action_type,
132
+ denial_reason=reason,
133
+ )
134
+
135
+ # Other errors
136
+ if response.status_code >= 400:
137
+ detail = data.get("detail", {})
138
+ if isinstance(detail, dict):
139
+ message = detail.get("message", str(detail))
140
+ else:
141
+ message = str(detail)
142
+ raise SwiftAPIError(message, response.status_code, data)
143
+
144
+ return data
145
+
146
+ # =========================================================================
147
+ # Core Endpoints
148
+ # =========================================================================
149
+
150
+ def verify(
151
+ self,
152
+ action_type: str,
153
+ intent: str,
154
+ params: Optional[Dict[str, Any]] = None,
155
+ actor: str = "sdk",
156
+ app_id: str = "swiftapi-python",
157
+ ) -> Dict[str, Any]:
158
+ """
159
+ Submit an action for verification and get an execution attestation.
160
+
161
+ Args:
162
+ action_type: Type of action (e.g., "file_write", "api_call")
163
+ intent: Human-readable description of intent
164
+ params: Optional action parameters
165
+ actor: Identifier for the requesting agent
166
+ app_id: Application identifier
167
+
168
+ Returns:
169
+ Full verification response including execution_attestation
170
+
171
+ Raises:
172
+ PolicyViolation: If action is denied
173
+ AuthenticationError: If key is invalid
174
+ SwiftAPIError: For other errors
175
+ """
176
+ payload = {
177
+ "action": {
178
+ "type": action_type,
179
+ "intent": intent,
180
+ "params": params or {},
181
+ },
182
+ "actor": actor,
183
+ "app_id": app_id,
184
+ }
185
+ return self._request("POST", "/verify", json=payload)
186
+
187
+ def verify_attestation(self, attestation: Dict[str, Any]) -> Dict[str, Any]:
188
+ """
189
+ Verify an existing attestation with the server.
190
+
191
+ Args:
192
+ attestation: The execution_attestation to verify
193
+
194
+ Returns:
195
+ Verification result
196
+ """
197
+ return self._request("POST", "/attestation/verify", json=attestation)
198
+
199
+ def revoke(self, jti: str, reason: str = "SDK revocation") -> Dict[str, Any]:
200
+ """
201
+ Revoke an attestation by JTI.
202
+
203
+ Args:
204
+ jti: The attestation's unique identifier
205
+ reason: Reason for revocation
206
+
207
+ Returns:
208
+ Revocation confirmation
209
+ """
210
+ payload = {"jti": jti, "reason": reason}
211
+ return self._request("POST", "/attestation/revoke", json=payload)
212
+
213
+ def check_revocation(self, jti: str) -> bool:
214
+ """
215
+ Check if an attestation has been revoked.
216
+
217
+ This is a fast check - use for real-time revocation verification.
218
+
219
+ Args:
220
+ jti: The attestation's unique identifier
221
+
222
+ Returns:
223
+ True if revoked, False if valid
224
+
225
+ Raises:
226
+ SwiftAPIError: If revocation status cannot be established, including
227
+ network errors or malformed responses. Execution must not proceed.
228
+ """
229
+ result = self._request("GET", f"/attestation/revoked/{jti}")
230
+ if (
231
+ not isinstance(result, dict)
232
+ or result.get("jti") != jti
233
+ or type(result.get("revoked")) is not bool
234
+ ):
235
+ raise SwiftAPIError("Invalid revocation response")
236
+ return result["revoked"]
237
+
238
+ def attest(
239
+ self,
240
+ action_type: str,
241
+ action_data: Dict[str, Any],
242
+ ) -> Dict[str, Any]:
243
+ """
244
+ Request an execution attestation from the authority.
245
+
246
+ Calls /attest to get a signed attestation for the given action.
247
+ The attestation can then be passed to gated endpoints like /chat/vibe.
248
+
249
+ Args:
250
+ action_type: Type of action (e.g., "chat_completion")
251
+ action_data: Action payload to be attested
252
+
253
+ Returns:
254
+ Signed attestation dict with jti, signature, action_fingerprint, etc.
255
+ """
256
+ payload = {
257
+ "action_type": action_type,
258
+ "action_data": action_data,
259
+ }
260
+ return self._request("POST", "/attest", json=payload)
261
+
262
+ # =========================================================================
263
+ # Info Endpoints
264
+ # =========================================================================
265
+
266
+ def get_info(self) -> Dict[str, Any]:
267
+ """Get API info and public key."""
268
+ return self._request("GET", "/")
269
+
270
+ def health(self) -> bool:
271
+ """Check API health."""
272
+ try:
273
+ result = self._request("GET", "/health")
274
+ return result.get("status") == "healthy"
275
+ except Exception:
276
+ return False
277
+
278
+ def get_policies(self) -> List[Dict[str, Any]]:
279
+ """Get active policies."""
280
+ result = self._request("GET", "/policies")
281
+ return result.get("policies", [])
282
+
283
+ def get_scopes(self) -> List[str]:
284
+ """Get available authority scopes."""
285
+ result = self._request("GET", "/authority/scopes")
286
+ return result.get("scopes", [])
287
+
288
+ # =========================================================================
289
+ # Key Management (Admin Only)
290
+ # =========================================================================
291
+
292
+ def list_keys(self) -> List[Dict[str, Any]]:
293
+ """List authority keys (requires admin scope)."""
294
+ result = self._request("GET", "/authority/keys")
295
+ return result.get("keys", [])
296
+
297
+ def create_key(
298
+ self,
299
+ name: str,
300
+ scopes: List[str],
301
+ ) -> Dict[str, Any]:
302
+ """
303
+ Create a new authority key (requires admin scope).
304
+
305
+ Args:
306
+ name: Key name
307
+ scopes: List of scopes to grant
308
+
309
+ Returns:
310
+ New key details (including the key itself - shown only once!)
311
+ """
312
+ payload = {"name": name, "scopes": scopes}
313
+ return self._request("POST", "/authority/keys", json=payload)
314
+
315
+ def revoke_key(self, key_hash: str) -> Dict[str, Any]:
316
+ """Revoke an authority key by hash."""
317
+ return self._request("DELETE", f"/authority/keys/{key_hash}")
318
+
319
+ # =========================================================================
320
+ # Context Manager
321
+ # =========================================================================
322
+
323
+ def __enter__(self):
324
+ return self
325
+
326
+ def __exit__(self, exc_type, exc_val, exc_tb):
327
+ self._session.close()
328
+ return False