infisicalsdk 1.0.3__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.
Potentially problematic release.
This version of infisicalsdk might be problematic. Click here for more details.
- infisical_sdk/__init__.py +3 -0
- infisical_sdk/api_types.py +127 -0
- infisical_sdk/client.py +345 -0
- infisical_sdk/infisical_requests.py +186 -0
- infisicalsdk-1.0.3.dist-info/METADATA +26 -0
- infisicalsdk-1.0.3.dist-info/RECORD +8 -0
- infisicalsdk-1.0.3.dist-info/WHEEL +5 -0
- infisicalsdk-1.0.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
from dataclasses import dataclass, field, fields
|
|
2
|
+
from typing import Optional, List, Any, Dict
|
|
3
|
+
from enum import Enum
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ApprovalStatus(str, Enum):
|
|
8
|
+
"""Enum for approval status"""
|
|
9
|
+
OPEN = "open"
|
|
10
|
+
APPROVED = "approved"
|
|
11
|
+
REJECTED = "rejected"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BaseModel:
|
|
15
|
+
"""Base class for all models"""
|
|
16
|
+
def to_dict(self) -> Dict:
|
|
17
|
+
"""Convert model to dictionary"""
|
|
18
|
+
result = {}
|
|
19
|
+
for key, value in self.__dict__.items():
|
|
20
|
+
if value is not None: # Skip None values
|
|
21
|
+
if isinstance(value, BaseModel):
|
|
22
|
+
result[key] = value.to_dict()
|
|
23
|
+
elif isinstance(value, list):
|
|
24
|
+
result[key] = [
|
|
25
|
+
item.to_dict() if isinstance(item, BaseModel) else item
|
|
26
|
+
for item in value
|
|
27
|
+
]
|
|
28
|
+
elif isinstance(value, Enum):
|
|
29
|
+
result[key] = value.value
|
|
30
|
+
else:
|
|
31
|
+
result[key] = value
|
|
32
|
+
return result
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def from_dict(cls, data: Dict) -> 'BaseModel':
|
|
36
|
+
"""Create model from dictionary"""
|
|
37
|
+
# Get only the fields that exist in the dataclass
|
|
38
|
+
valid_fields = {f.name for f in fields(cls)}
|
|
39
|
+
filtered_data = {k: v for k, v in data.items() if k in valid_fields}
|
|
40
|
+
return cls(**filtered_data)
|
|
41
|
+
|
|
42
|
+
def to_json(self) -> str:
|
|
43
|
+
"""Convert model to JSON string"""
|
|
44
|
+
return json.dumps(self.to_dict())
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def from_json(cls, json_str: str) -> 'BaseModel':
|
|
48
|
+
"""Create model from JSON string"""
|
|
49
|
+
data = json.loads(json_str)
|
|
50
|
+
return cls.from_dict(data)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class SecretTag(BaseModel):
|
|
55
|
+
"""Model for secret tags"""
|
|
56
|
+
id: str
|
|
57
|
+
slug: str
|
|
58
|
+
name: str
|
|
59
|
+
color: Optional[str] = None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class BaseSecret(BaseModel):
|
|
64
|
+
"""Infisical Secret"""
|
|
65
|
+
id: str
|
|
66
|
+
_id: str
|
|
67
|
+
workspace: str
|
|
68
|
+
environment: str
|
|
69
|
+
version: int
|
|
70
|
+
type: str
|
|
71
|
+
secretKey: str
|
|
72
|
+
secretValue: str
|
|
73
|
+
secretComment: str
|
|
74
|
+
createdAt: str
|
|
75
|
+
updatedAt: str
|
|
76
|
+
secretMetadata: Optional[Dict[str, Any]] = None
|
|
77
|
+
secretReminderNote: Optional[str] = None
|
|
78
|
+
secretReminderRepeatDays: Optional[int] = None
|
|
79
|
+
skipMultilineEncoding: Optional[bool] = False
|
|
80
|
+
metadata: Optional[Any] = None
|
|
81
|
+
secretPath: Optional[str] = None
|
|
82
|
+
tags: List[SecretTag] = field(default_factory=list)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass
|
|
86
|
+
class Import(BaseModel):
|
|
87
|
+
"""Model for imports section"""
|
|
88
|
+
secretPath: str
|
|
89
|
+
environment: str
|
|
90
|
+
folderId: Optional[str] = None
|
|
91
|
+
secrets: List[BaseSecret] = field(default_factory=list)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class ListSecretsResponse(BaseModel):
|
|
96
|
+
"""Complete response model for secrets API"""
|
|
97
|
+
secrets: List[BaseSecret]
|
|
98
|
+
imports: List[Import] = field(default_factory=list)
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def from_dict(cls, data: Dict) -> 'ListSecretsResponse':
|
|
102
|
+
"""Create model from dictionary with camelCase keys, handling nested objects"""
|
|
103
|
+
return cls(
|
|
104
|
+
secrets=[BaseSecret.from_dict(secret) for secret in data['secrets']],
|
|
105
|
+
imports=[Import.from_dict(imp) for imp in data.get('imports', [])]
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass
|
|
110
|
+
class SingleSecretResponse(BaseModel):
|
|
111
|
+
"""Response model for get secret API"""
|
|
112
|
+
secret: BaseSecret
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def from_dict(cls, data: Dict) -> 'ListSecretsResponse':
|
|
116
|
+
return cls(
|
|
117
|
+
secret=BaseSecret.from_dict(data['secret']),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass
|
|
122
|
+
class MachineIdentityLoginResponse(BaseModel):
|
|
123
|
+
"""Response model for machine identity login API"""
|
|
124
|
+
accessToken: str
|
|
125
|
+
expiresIn: int
|
|
126
|
+
accessTokenMaxTTL: int
|
|
127
|
+
tokenType: str
|
infisical_sdk/client.py
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
from typing import List, Union
|
|
4
|
+
import os
|
|
5
|
+
import datetime
|
|
6
|
+
from typing import Dict, Any
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
import boto3
|
|
10
|
+
from botocore.auth import SigV4Auth
|
|
11
|
+
from botocore.awsrequest import AWSRequest
|
|
12
|
+
from botocore.exceptions import NoCredentialsError
|
|
13
|
+
|
|
14
|
+
from .infisical_requests import InfisicalRequests
|
|
15
|
+
from .api_types import ListSecretsResponse, MachineIdentityLoginResponse
|
|
16
|
+
from .api_types import SingleSecretResponse, BaseSecret
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class InfisicalSDKClient:
|
|
20
|
+
def __init__(self, host: str, token: str = None):
|
|
21
|
+
self.host = host
|
|
22
|
+
self.access_token = token
|
|
23
|
+
|
|
24
|
+
self.api = InfisicalRequests(host=host, token=token)
|
|
25
|
+
|
|
26
|
+
self.auth = Auth(self)
|
|
27
|
+
self.secrets = V3RawSecrets(self)
|
|
28
|
+
|
|
29
|
+
def set_token(self, token: str):
|
|
30
|
+
"""
|
|
31
|
+
Set the access token for future requests.
|
|
32
|
+
"""
|
|
33
|
+
self.api.set_token(token)
|
|
34
|
+
self.access_token = token
|
|
35
|
+
|
|
36
|
+
def get_token(self):
|
|
37
|
+
"""
|
|
38
|
+
Set the access token for future requests.
|
|
39
|
+
"""
|
|
40
|
+
return self.access_token
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class UniversalAuth:
|
|
44
|
+
def __init__(self, client: InfisicalSDKClient):
|
|
45
|
+
self.client = client
|
|
46
|
+
|
|
47
|
+
def login(self, client_id: str, client_secret: str) -> MachineIdentityLoginResponse:
|
|
48
|
+
"""
|
|
49
|
+
Login with Universal Auth.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
client_id (str): Your Machine Identity Client ID.
|
|
53
|
+
client_secret (str): Your Machine Identity Client Secret.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Dict: A dictionary containing the access token and related information.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
requestBody = {
|
|
60
|
+
"clientId": client_id,
|
|
61
|
+
"clientSecret": client_secret
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
result = self.client.api.post(
|
|
65
|
+
path="/api/v1/auth/universal-auth/login",
|
|
66
|
+
json=requestBody,
|
|
67
|
+
model=MachineIdentityLoginResponse
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
self.client.set_token(result.data.accessToken)
|
|
71
|
+
|
|
72
|
+
return result.data
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class AWSAuth:
|
|
76
|
+
def __init__(self, client: InfisicalSDKClient) -> None:
|
|
77
|
+
self.client = client
|
|
78
|
+
|
|
79
|
+
def login(self, identity_id: str) -> MachineIdentityLoginResponse:
|
|
80
|
+
"""
|
|
81
|
+
Login with AWS Authentication.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
identity_id (str): Your Machine Identity ID that has AWS Auth configured.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Dict: A dictionary containing the access token and related information.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
identity_id = identity_id or os.getenv("INFISICAL_AWS_IAM_AUTH_IDENTITY_ID")
|
|
91
|
+
if not identity_id:
|
|
92
|
+
raise ValueError(
|
|
93
|
+
"Identity ID must be provided or set in the environment variable" +
|
|
94
|
+
"INFISICAL_AWS_IAM_AUTH_IDENTITY_ID."
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
aws_region = self.get_aws_region()
|
|
98
|
+
session = boto3.Session(region_name=aws_region)
|
|
99
|
+
|
|
100
|
+
credentials = self._get_aws_credentials(session)
|
|
101
|
+
|
|
102
|
+
iam_request_url = f"https://sts.{aws_region}.amazonaws.com/"
|
|
103
|
+
iam_request_body = "Action=GetCallerIdentity&Version=2011-06-15"
|
|
104
|
+
|
|
105
|
+
request_headers = self._prepare_aws_request(
|
|
106
|
+
iam_request_url,
|
|
107
|
+
iam_request_body,
|
|
108
|
+
credentials,
|
|
109
|
+
aws_region
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
requestBody = {
|
|
113
|
+
"identityId": identity_id,
|
|
114
|
+
"iamRequestBody": base64.b64encode(iam_request_body.encode()).decode(),
|
|
115
|
+
"iamRequestHeaders": base64.b64encode(json.dumps(request_headers).encode()).decode(),
|
|
116
|
+
"iamHttpRequestMethod": "POST"
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
result = self.client.api.post(
|
|
120
|
+
path="/api/v1/auth/aws-auth/login",
|
|
121
|
+
json=requestBody,
|
|
122
|
+
model=MachineIdentityLoginResponse
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
self.client.set_token(result.data.accessToken)
|
|
126
|
+
|
|
127
|
+
return result.data
|
|
128
|
+
|
|
129
|
+
def _get_aws_credentials(self, session: boto3.Session) -> Any:
|
|
130
|
+
try:
|
|
131
|
+
credentials = session.get_credentials()
|
|
132
|
+
if credentials is None:
|
|
133
|
+
raise NoCredentialsError("AWS credentials not found.")
|
|
134
|
+
return credentials.get_frozen_credentials()
|
|
135
|
+
except NoCredentialsError as e:
|
|
136
|
+
raise RuntimeError(f"AWS IAM Auth Login failed: {str(e)}")
|
|
137
|
+
|
|
138
|
+
def _prepare_aws_request(
|
|
139
|
+
self,
|
|
140
|
+
url: str,
|
|
141
|
+
body: str,
|
|
142
|
+
credentials: Any,
|
|
143
|
+
region: str) -> Dict[str, str]:
|
|
144
|
+
|
|
145
|
+
current_time = datetime.datetime.now(datetime.timezone.utc)
|
|
146
|
+
amz_date = current_time.strftime('%Y%m%dT%H%M%SZ')
|
|
147
|
+
|
|
148
|
+
request = AWSRequest(method="POST", url=url, data=body)
|
|
149
|
+
request.headers["X-Amz-Date"] = amz_date
|
|
150
|
+
request.headers["Host"] = f"sts.{region}.amazonaws.com"
|
|
151
|
+
request.headers["Content-Type"] = "application/x-www-form-urlencoded; charset=utf-8"
|
|
152
|
+
request.headers["Content-Length"] = str(len(body))
|
|
153
|
+
|
|
154
|
+
signer = SigV4Auth(credentials, "sts", region)
|
|
155
|
+
signer.add_auth(request)
|
|
156
|
+
|
|
157
|
+
return {k: v for k, v in request.headers.items() if k.lower() != "content-length"}
|
|
158
|
+
|
|
159
|
+
@staticmethod
|
|
160
|
+
def get_aws_region() -> str:
|
|
161
|
+
region = os.getenv("AWS_REGION") # Typically found in lambda runtime environment
|
|
162
|
+
if region:
|
|
163
|
+
return region
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
return AWSAuth._get_aws_ec2_identity_document_region()
|
|
167
|
+
except Exception as e:
|
|
168
|
+
raise Exception("Failed to retrieve AWS region") from e
|
|
169
|
+
|
|
170
|
+
@staticmethod
|
|
171
|
+
def _get_aws_ec2_identity_document_region(timeout: int = 5000) -> str:
|
|
172
|
+
session = requests.Session()
|
|
173
|
+
token_response = session.put(
|
|
174
|
+
"http://169.254.169.254/latest/api/token",
|
|
175
|
+
headers={"X-aws-ec2-metadata-token-ttl-seconds": "21600"},
|
|
176
|
+
timeout=timeout / 1000
|
|
177
|
+
)
|
|
178
|
+
token_response.raise_for_status()
|
|
179
|
+
metadata_token = token_response.text
|
|
180
|
+
|
|
181
|
+
identity_response = session.get(
|
|
182
|
+
"http://169.254.169.254/latest/dynamic/instance-identity/document",
|
|
183
|
+
headers={"X-aws-ec2-metadata-token": metadata_token, "Accept": "application/json"},
|
|
184
|
+
timeout=timeout / 1000
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
identity_response.raise_for_status()
|
|
188
|
+
return identity_response.json().get("region")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class Auth:
|
|
192
|
+
def __init__(self, client):
|
|
193
|
+
self.client = client
|
|
194
|
+
self.aws_auth = AWSAuth(client)
|
|
195
|
+
self.universal_auth = UniversalAuth(client)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class V3RawSecrets:
|
|
199
|
+
def __init__(self, client: InfisicalSDKClient) -> None:
|
|
200
|
+
self.client = client
|
|
201
|
+
|
|
202
|
+
def list_secrets(
|
|
203
|
+
self,
|
|
204
|
+
project_id: str,
|
|
205
|
+
environment_slug: str,
|
|
206
|
+
secret_path: str,
|
|
207
|
+
expand_secret_references: bool = True,
|
|
208
|
+
recursive: bool = False,
|
|
209
|
+
include_imports: bool = True,
|
|
210
|
+
tag_filters: List[str] = []) -> ListSecretsResponse:
|
|
211
|
+
|
|
212
|
+
params = {
|
|
213
|
+
"workspaceId": project_id,
|
|
214
|
+
"environment": environment_slug,
|
|
215
|
+
"secretPath": secret_path,
|
|
216
|
+
"expandSecretReferences": str(expand_secret_references).lower(),
|
|
217
|
+
"recursive": str(recursive).lower(),
|
|
218
|
+
"include_imports": str(include_imports).lower(),
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if tag_filters:
|
|
222
|
+
params["tag_slugs"] = ",".join(tag_filters)
|
|
223
|
+
|
|
224
|
+
result = self.client.api.get(
|
|
225
|
+
path="/api/v3/secrets/raw",
|
|
226
|
+
params=params,
|
|
227
|
+
model=ListSecretsResponse
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
return result.data
|
|
231
|
+
|
|
232
|
+
def get_secret_by_name(
|
|
233
|
+
self,
|
|
234
|
+
secret_name: str,
|
|
235
|
+
project_id: str,
|
|
236
|
+
environment_slug: str,
|
|
237
|
+
secret_path: str,
|
|
238
|
+
expand_secret_references: bool = True,
|
|
239
|
+
include_imports: bool = True,
|
|
240
|
+
version: str = None) -> BaseSecret:
|
|
241
|
+
|
|
242
|
+
params = {
|
|
243
|
+
"workspaceId": project_id,
|
|
244
|
+
"environment": environment_slug,
|
|
245
|
+
"secretPath": secret_path,
|
|
246
|
+
"expandSecretReferences": str(expand_secret_references).lower(),
|
|
247
|
+
"include_imports": str(include_imports).lower(),
|
|
248
|
+
"version": version
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
result = self.client.api.get(
|
|
252
|
+
path=f"/api/v3/secrets/raw/{secret_name}",
|
|
253
|
+
params=params,
|
|
254
|
+
model=SingleSecretResponse
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
return result.data.secret
|
|
258
|
+
|
|
259
|
+
def create_secret_by_name(
|
|
260
|
+
self,
|
|
261
|
+
secret_name: str,
|
|
262
|
+
project_id: str,
|
|
263
|
+
secret_path: str,
|
|
264
|
+
environment_slug: str,
|
|
265
|
+
secret_value: str = None,
|
|
266
|
+
secret_comment: str = None,
|
|
267
|
+
skip_multiline_encoding: bool = False,
|
|
268
|
+
secret_reminder_repeat_days: Union[float, int] = None,
|
|
269
|
+
secret_reminder_note: str = None) -> BaseSecret:
|
|
270
|
+
|
|
271
|
+
requestBody = {
|
|
272
|
+
"workspaceId": project_id,
|
|
273
|
+
"environment": environment_slug,
|
|
274
|
+
"secretPath": secret_path,
|
|
275
|
+
"secretValue": secret_value,
|
|
276
|
+
"secretComment": secret_comment,
|
|
277
|
+
"tagIds": None,
|
|
278
|
+
"skipMultilineEncoding": skip_multiline_encoding,
|
|
279
|
+
"type": "shared",
|
|
280
|
+
"secretReminderRepeatDays": secret_reminder_repeat_days,
|
|
281
|
+
"secretReminderNote": secret_reminder_note
|
|
282
|
+
}
|
|
283
|
+
result = self.client.api.post(
|
|
284
|
+
path=f"/api/v3/secrets/raw/{secret_name}",
|
|
285
|
+
json=requestBody,
|
|
286
|
+
model=SingleSecretResponse
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
return result.data.secret
|
|
290
|
+
|
|
291
|
+
def update_secret_by_name(
|
|
292
|
+
self,
|
|
293
|
+
current_secret_name: str,
|
|
294
|
+
project_id: str,
|
|
295
|
+
secret_path: str,
|
|
296
|
+
environment_slug: str,
|
|
297
|
+
secret_value: str = None,
|
|
298
|
+
secret_comment: str = None,
|
|
299
|
+
skip_multiline_encoding: bool = False,
|
|
300
|
+
secret_reminder_repeat_days: Union[float, int] = None,
|
|
301
|
+
secret_reminder_note: str = None,
|
|
302
|
+
new_secret_name: str = None) -> BaseSecret:
|
|
303
|
+
|
|
304
|
+
requestBody = {
|
|
305
|
+
"workspaceId": project_id,
|
|
306
|
+
"environment": environment_slug,
|
|
307
|
+
"secretPath": secret_path,
|
|
308
|
+
"secretValue": secret_value,
|
|
309
|
+
"secretComment": secret_comment,
|
|
310
|
+
"new_secret_name": new_secret_name,
|
|
311
|
+
"tagIds": None,
|
|
312
|
+
"skipMultilineEncoding": skip_multiline_encoding,
|
|
313
|
+
"type": "shared",
|
|
314
|
+
"secretReminderRepeatDays": secret_reminder_repeat_days,
|
|
315
|
+
"secretReminderNote": secret_reminder_note
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
result = self.client.api.patch(
|
|
319
|
+
path=f"/api/v3/secrets/raw/{current_secret_name}",
|
|
320
|
+
json=requestBody,
|
|
321
|
+
model=SingleSecretResponse
|
|
322
|
+
)
|
|
323
|
+
return result.data.secret
|
|
324
|
+
|
|
325
|
+
def delete_secret_by_name(
|
|
326
|
+
self,
|
|
327
|
+
secret_name: str,
|
|
328
|
+
project_id: str,
|
|
329
|
+
secret_path: str,
|
|
330
|
+
environment_slug: str) -> BaseSecret:
|
|
331
|
+
|
|
332
|
+
requestBody = {
|
|
333
|
+
"workspaceId": project_id,
|
|
334
|
+
"environment": environment_slug,
|
|
335
|
+
"secretPath": secret_path,
|
|
336
|
+
"type": "shared",
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
result = self.client.api.delete(
|
|
340
|
+
path=f"/api/v3/secrets/raw/{secret_name}",
|
|
341
|
+
json=requestBody,
|
|
342
|
+
model=SingleSecretResponse
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
return result.data.secret
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
from typing import Any, Dict, Generic, Optional, TypeVar
|
|
2
|
+
from urllib.parse import urljoin
|
|
3
|
+
import requests
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
T = TypeVar("T")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class InfisicalError(Exception):
|
|
10
|
+
"""Base exception for Infisical client errors"""
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class APIError(InfisicalError):
|
|
15
|
+
"""API-specific errors"""
|
|
16
|
+
def __init__(self, message: str, status_code: int, response: Dict[str, Any]):
|
|
17
|
+
self.status_code = status_code
|
|
18
|
+
self.response = response
|
|
19
|
+
super().__init__(f"{message} (Status: {status_code})")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class APIResponse(Generic[T]):
|
|
24
|
+
"""Generic API response wrapper"""
|
|
25
|
+
data: T
|
|
26
|
+
status_code: int
|
|
27
|
+
headers: Dict[str, str]
|
|
28
|
+
|
|
29
|
+
def to_dict(self) -> Dict:
|
|
30
|
+
"""Convert to dictionary with camelCase keys"""
|
|
31
|
+
return {
|
|
32
|
+
'data': self.data.to_dict() if hasattr(self.data, 'to_dict') else self.data,
|
|
33
|
+
'statusCode': self.status_code,
|
|
34
|
+
'headers': self.headers
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def from_dict(cls, data: Dict) -> 'APIResponse[T]':
|
|
39
|
+
"""Create from dictionary with camelCase keys"""
|
|
40
|
+
return cls(
|
|
41
|
+
data=data['data'],
|
|
42
|
+
status_code=data['statusCode'],
|
|
43
|
+
headers=data['headers']
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class InfisicalRequests:
|
|
48
|
+
def __init__(self, host: str, token: Optional[str] = None):
|
|
49
|
+
self.host = host.rstrip("/")
|
|
50
|
+
self.session = requests.Session()
|
|
51
|
+
|
|
52
|
+
# Set common headers
|
|
53
|
+
self.session.headers.update({
|
|
54
|
+
"Content-Type": "application/json",
|
|
55
|
+
"Accept": "application/json",
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
if token:
|
|
59
|
+
self.set_token(token)
|
|
60
|
+
|
|
61
|
+
def _build_url(self, path: str) -> str:
|
|
62
|
+
"""Construct full URL from path"""
|
|
63
|
+
return urljoin(self.host, path.lstrip("/"))
|
|
64
|
+
|
|
65
|
+
def set_token(self, token: str):
|
|
66
|
+
"""Set authorization token"""
|
|
67
|
+
self.session.headers["Authorization"] = f"Bearer {token}"
|
|
68
|
+
|
|
69
|
+
def _handle_response(self, response: requests.Response) -> Dict[str, Any]:
|
|
70
|
+
"""Handle API response and raise appropriate errors"""
|
|
71
|
+
try:
|
|
72
|
+
response.raise_for_status()
|
|
73
|
+
return response.json()
|
|
74
|
+
except requests.exceptions.HTTPError:
|
|
75
|
+
try:
|
|
76
|
+
error_data = response.json()
|
|
77
|
+
except ValueError:
|
|
78
|
+
error_data = {"message": response.text}
|
|
79
|
+
|
|
80
|
+
raise APIError(
|
|
81
|
+
message=error_data.get("message", "Unknown error"),
|
|
82
|
+
status_code=response.status_code,
|
|
83
|
+
response=error_data
|
|
84
|
+
)
|
|
85
|
+
except requests.exceptions.RequestException as e:
|
|
86
|
+
raise InfisicalError(f"Request failed: {str(e)}")
|
|
87
|
+
except ValueError:
|
|
88
|
+
raise InfisicalError("Invalid JSON response")
|
|
89
|
+
|
|
90
|
+
def get(
|
|
91
|
+
self,
|
|
92
|
+
path: str,
|
|
93
|
+
model: type[T],
|
|
94
|
+
params: Optional[Dict[str, Any]] = None
|
|
95
|
+
) -> APIResponse[T]:
|
|
96
|
+
|
|
97
|
+
"""
|
|
98
|
+
Make a GET request and parse response into given model
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
path: API endpoint path
|
|
102
|
+
model: model class to parse response into
|
|
103
|
+
params: Optional query parameters
|
|
104
|
+
"""
|
|
105
|
+
response = self.session.get(self._build_url(path), params=params)
|
|
106
|
+
data = self._handle_response(response)
|
|
107
|
+
|
|
108
|
+
parsed_data = model.from_dict(data) if hasattr(model, 'from_dict') else data
|
|
109
|
+
|
|
110
|
+
return APIResponse(
|
|
111
|
+
data=parsed_data,
|
|
112
|
+
status_code=response.status_code,
|
|
113
|
+
headers=dict(response.headers)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def post(
|
|
117
|
+
self,
|
|
118
|
+
path: str,
|
|
119
|
+
model: type[T],
|
|
120
|
+
json: Optional[Dict[str, Any]] = None
|
|
121
|
+
) -> APIResponse[T]:
|
|
122
|
+
|
|
123
|
+
"""Make a POST request with JSON data"""
|
|
124
|
+
|
|
125
|
+
if json is not None:
|
|
126
|
+
# Filter out None values
|
|
127
|
+
json = {k: v for k, v in json.items() if v is not None}
|
|
128
|
+
|
|
129
|
+
response = self.session.post(self._build_url(path), json=json)
|
|
130
|
+
data = self._handle_response(response)
|
|
131
|
+
|
|
132
|
+
parsed_data = model.from_dict(data) if hasattr(model, 'from_dict') else data
|
|
133
|
+
|
|
134
|
+
return APIResponse(
|
|
135
|
+
data=parsed_data,
|
|
136
|
+
status_code=response.status_code,
|
|
137
|
+
headers=dict(response.headers)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def patch(
|
|
141
|
+
self,
|
|
142
|
+
path: str,
|
|
143
|
+
model: type[T],
|
|
144
|
+
json: Optional[Dict[str, Any]] = None
|
|
145
|
+
) -> APIResponse[T]:
|
|
146
|
+
|
|
147
|
+
"""Make a PATCH request with JSON data"""
|
|
148
|
+
|
|
149
|
+
if json is not None:
|
|
150
|
+
# Filter out None values
|
|
151
|
+
json = {k: v for k, v in json.items() if v is not None}
|
|
152
|
+
|
|
153
|
+
response = self.session.patch(self._build_url(path), json=json)
|
|
154
|
+
data = self._handle_response(response)
|
|
155
|
+
|
|
156
|
+
parsed_data = model.from_dict(data) if hasattr(model, 'from_dict') else data
|
|
157
|
+
|
|
158
|
+
return APIResponse(
|
|
159
|
+
data=parsed_data,
|
|
160
|
+
status_code=response.status_code,
|
|
161
|
+
headers=dict(response.headers)
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
def delete(
|
|
165
|
+
self,
|
|
166
|
+
path: str,
|
|
167
|
+
model: type[T],
|
|
168
|
+
json: Optional[Dict[str, Any]] = None
|
|
169
|
+
) -> APIResponse[T]:
|
|
170
|
+
|
|
171
|
+
"""Make a PATCH request with JSON data"""
|
|
172
|
+
|
|
173
|
+
if json is not None:
|
|
174
|
+
# Filter out None values
|
|
175
|
+
json = {k: v for k, v in json.items() if v is not None}
|
|
176
|
+
|
|
177
|
+
response = self.session.delete(self._build_url(path), json=json)
|
|
178
|
+
data = self._handle_response(response)
|
|
179
|
+
|
|
180
|
+
parsed_data = model.from_dict(data) if hasattr(model, 'from_dict') else data
|
|
181
|
+
|
|
182
|
+
return APIResponse(
|
|
183
|
+
data=parsed_data,
|
|
184
|
+
status_code=response.status_code,
|
|
185
|
+
headers=dict(response.headers)
|
|
186
|
+
)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: infisicalsdk
|
|
3
|
+
Version: 1.0.3
|
|
4
|
+
Summary: Infisical API Client
|
|
5
|
+
Home-page: https://github.com/Infisical/python-sdk-official
|
|
6
|
+
Author: Infisical
|
|
7
|
+
Author-email: support@infisical.com
|
|
8
|
+
Keywords: Infisical,Infisical API,Infisical SDK
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: urllib3<2.1.0,>=1.25.3
|
|
11
|
+
Requires-Dist: python-dateutil
|
|
12
|
+
Requires-Dist: aenum
|
|
13
|
+
Requires-Dist: requests~=2.32
|
|
14
|
+
Requires-Dist: boto3~=1.35
|
|
15
|
+
Requires-Dist: botocore~=1.35
|
|
16
|
+
Dynamic: author
|
|
17
|
+
Dynamic: author-email
|
|
18
|
+
Dynamic: description
|
|
19
|
+
Dynamic: description-content-type
|
|
20
|
+
Dynamic: home-page
|
|
21
|
+
Dynamic: keywords
|
|
22
|
+
Dynamic: requires-dist
|
|
23
|
+
Dynamic: summary
|
|
24
|
+
|
|
25
|
+
Infisical SDK client for Python. To view documentation, please visit https://github.com/Infisical/python-sdk-official
|
|
26
|
+
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
infisical_sdk/__init__.py,sha256=UzssDXpMhK79mFBW4fpSea1bOVjoD_UILjvizFkLNz4,183
|
|
2
|
+
infisical_sdk/api_types.py,sha256=FjyDQ71pOYiyYA9oGAPsW5jW7LGe5L7yDuOvCr4UxwQ,3651
|
|
3
|
+
infisical_sdk/client.py,sha256=i1poZEz2tiGJLYZuRePfWONs7hQyP_A5QJJt1UeW8WQ,10936
|
|
4
|
+
infisical_sdk/infisical_requests.py,sha256=mROtB1fuF3E39fU9J6MiH0gQ-LhsK10W-zzEH0knePU,5655
|
|
5
|
+
infisicalsdk-1.0.3.dist-info/METADATA,sha256=LuJqI82C7DTQdeYLCpixwYmYF49y_RfdDlVrSFDBpW4,763
|
|
6
|
+
infisicalsdk-1.0.3.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
7
|
+
infisicalsdk-1.0.3.dist-info/top_level.txt,sha256=FvJjMGD1FvxwipO_qFajdH20yNV8n3lJ7G3DkQoPJNU,14
|
|
8
|
+
infisicalsdk-1.0.3.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
infisical_sdk
|