swiftapi-python 1.2.1__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,307 +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
- raise PolicyViolation(
115
- message=data.get("reason", "Action denied by policy"),
116
- action_type=data.get("action", {}).get("type"),
117
- denial_reason=data.get("reason"),
118
- )
119
-
120
- # Other errors
121
- if response.status_code >= 400:
122
- detail = data.get("detail", {})
123
- if isinstance(detail, dict):
124
- message = detail.get("message", str(detail))
125
- else:
126
- message = str(detail)
127
- raise SwiftAPIError(message, response.status_code, data)
128
-
129
- return data
130
-
131
- # =========================================================================
132
- # Core Endpoints
133
- # =========================================================================
134
-
135
- def verify(
136
- self,
137
- action_type: str,
138
- intent: str,
139
- params: Optional[Dict[str, Any]] = None,
140
- actor: str = "sdk",
141
- app_id: str = "swiftapi-python",
142
- ) -> Dict[str, Any]:
143
- """
144
- Submit an action for verification and get an execution attestation.
145
-
146
- Args:
147
- action_type: Type of action (e.g., "file_write", "api_call")
148
- intent: Human-readable description of intent
149
- params: Optional action parameters
150
- actor: Identifier for the requesting agent
151
- app_id: Application identifier
152
-
153
- Returns:
154
- Full verification response including execution_attestation
155
-
156
- Raises:
157
- PolicyViolation: If action is denied
158
- AuthenticationError: If key is invalid
159
- SwiftAPIError: For other errors
160
- """
161
- payload = {
162
- "action": {
163
- "type": action_type,
164
- "intent": intent,
165
- "params": params or {},
166
- },
167
- "actor": actor,
168
- "app_id": app_id,
169
- }
170
- return self._request("POST", "/verify", json=payload)
171
-
172
- def verify_attestation(self, attestation: Dict[str, Any]) -> Dict[str, Any]:
173
- """
174
- Verify an existing attestation with the server.
175
-
176
- Args:
177
- attestation: The execution_attestation to verify
178
-
179
- Returns:
180
- Verification result
181
- """
182
- return self._request("POST", "/attestation/verify", json=attestation)
183
-
184
- def revoke(self, jti: str, reason: str = "SDK revocation") -> Dict[str, Any]:
185
- """
186
- Revoke an attestation by JTI.
187
-
188
- Args:
189
- jti: The attestation's unique identifier
190
- reason: Reason for revocation
191
-
192
- Returns:
193
- Revocation confirmation
194
- """
195
- payload = {"jti": jti, "reason": reason}
196
- return self._request("POST", "/attestation/revoke", json=payload)
197
-
198
- def check_revocation(self, jti: str) -> bool:
199
- """
200
- Check if an attestation has been revoked.
201
-
202
- This is a fast check - use for real-time revocation verification.
203
-
204
- Args:
205
- jti: The attestation's unique identifier
206
-
207
- Returns:
208
- True if revoked, False if valid
209
- """
210
- try:
211
- result = self._request("GET", f"/attestation/revoked/{jti}")
212
- return result.get("revoked", False)
213
- except SwiftAPIError:
214
- # If endpoint fails, assume not revoked (fail-open for availability)
215
- return False
216
-
217
- def attest(
218
- self,
219
- action_type: str,
220
- action_data: Dict[str, Any],
221
- ) -> Dict[str, Any]:
222
- """
223
- Request an execution attestation from the authority.
224
-
225
- Calls /attest to get a signed attestation for the given action.
226
- The attestation can then be passed to gated endpoints like /chat/vibe.
227
-
228
- Args:
229
- action_type: Type of action (e.g., "chat_completion")
230
- action_data: Action payload to be attested
231
-
232
- Returns:
233
- Signed attestation dict with jti, signature, action_fingerprint, etc.
234
- """
235
- payload = {
236
- "action_type": action_type,
237
- "action_data": action_data,
238
- }
239
- return self._request("POST", "/attest", json=payload)
240
-
241
- # =========================================================================
242
- # Info Endpoints
243
- # =========================================================================
244
-
245
- def get_info(self) -> Dict[str, Any]:
246
- """Get API info and public key."""
247
- return self._request("GET", "/")
248
-
249
- def health(self) -> bool:
250
- """Check API health."""
251
- try:
252
- result = self._request("GET", "/health")
253
- return result.get("status") == "healthy"
254
- except Exception:
255
- return False
256
-
257
- def get_policies(self) -> List[Dict[str, Any]]:
258
- """Get active policies."""
259
- result = self._request("GET", "/policies")
260
- return result.get("policies", [])
261
-
262
- def get_scopes(self) -> List[str]:
263
- """Get available authority scopes."""
264
- result = self._request("GET", "/authority/scopes")
265
- return result.get("scopes", [])
266
-
267
- # =========================================================================
268
- # Key Management (Admin Only)
269
- # =========================================================================
270
-
271
- def list_keys(self) -> List[Dict[str, Any]]:
272
- """List authority keys (requires admin scope)."""
273
- result = self._request("GET", "/authority/keys")
274
- return result.get("keys", [])
275
-
276
- def create_key(
277
- self,
278
- name: str,
279
- scopes: List[str],
280
- ) -> Dict[str, Any]:
281
- """
282
- Create a new authority key (requires admin scope).
283
-
284
- Args:
285
- name: Key name
286
- scopes: List of scopes to grant
287
-
288
- Returns:
289
- New key details (including the key itself - shown only once!)
290
- """
291
- payload = {"name": name, "scopes": scopes}
292
- return self._request("POST", "/authority/keys", json=payload)
293
-
294
- def revoke_key(self, key_hash: str) -> Dict[str, Any]:
295
- """Revoke an authority key by hash."""
296
- return self._request("DELETE", f"/authority/keys/{key_hash}")
297
-
298
- # =========================================================================
299
- # Context Manager
300
- # =========================================================================
301
-
302
- def __enter__(self):
303
- return self
304
-
305
- def __exit__(self, exc_type, exc_val, exc_tb):
306
- self._session.close()
307
- 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