infisicalsdk 1.0.3__tar.gz → 1.0.15__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.
- infisicalsdk-1.0.15/LICENSE +11 -0
- {infisicalsdk-1.0.3 → infisicalsdk-1.0.15}/PKG-INFO +8 -6
- infisicalsdk-1.0.15/README.md +40 -0
- {infisicalsdk-1.0.3 → infisicalsdk-1.0.15}/infisical_sdk/__init__.py +1 -1
- infisicalsdk-1.0.15/infisical_sdk/api_types.py +397 -0
- infisicalsdk-1.0.15/infisical_sdk/client.py +66 -0
- {infisicalsdk-1.0.3 → infisicalsdk-1.0.15}/infisical_sdk/infisical_requests.py +71 -7
- infisicalsdk-1.0.15/infisical_sdk/resources/__init__.py +5 -0
- infisicalsdk-1.0.15/infisical_sdk/resources/auth.py +14 -0
- infisicalsdk-1.0.15/infisical_sdk/resources/auth_methods/__init__.py +4 -0
- infisicalsdk-1.0.15/infisical_sdk/resources/auth_methods/aws_auth.py +134 -0
- infisicalsdk-1.0.15/infisical_sdk/resources/auth_methods/oidc_auth.py +36 -0
- infisicalsdk-1.0.15/infisical_sdk/resources/auth_methods/token_auth.py +22 -0
- infisicalsdk-1.0.15/infisical_sdk/resources/auth_methods/universal_auth.py +35 -0
- infisicalsdk-1.0.15/infisical_sdk/resources/dynamic_secrets.py +335 -0
- infisicalsdk-1.0.15/infisical_sdk/resources/folders.py +75 -0
- infisicalsdk-1.0.15/infisical_sdk/resources/kms.py +177 -0
- infisicalsdk-1.0.15/infisical_sdk/resources/secrets.py +269 -0
- infisicalsdk-1.0.15/infisical_sdk/util/__init__.py +1 -0
- infisicalsdk-1.0.15/infisical_sdk/util/secrets_cache.py +192 -0
- {infisicalsdk-1.0.3 → infisicalsdk-1.0.15}/infisicalsdk.egg-info/PKG-INFO +8 -6
- infisicalsdk-1.0.15/infisicalsdk.egg-info/SOURCES.txt +27 -0
- {infisicalsdk-1.0.3 → infisicalsdk-1.0.15}/infisicalsdk.egg-info/requires.txt +0 -1
- {infisicalsdk-1.0.3 → infisicalsdk-1.0.15}/pyproject.toml +0 -1
- {infisicalsdk-1.0.3 → infisicalsdk-1.0.15}/setup.cfg +1 -1
- {infisicalsdk-1.0.3 → infisicalsdk-1.0.15}/setup.py +6 -6
- infisicalsdk-1.0.3/README.md +0 -204
- infisicalsdk-1.0.3/infisical_sdk/api_types.py +0 -127
- infisicalsdk-1.0.3/infisical_sdk/client.py +0 -345
- infisicalsdk-1.0.3/infisicalsdk.egg-info/SOURCES.txt +0 -13
- {infisicalsdk-1.0.3 → infisicalsdk-1.0.15}/infisicalsdk.egg-info/dependency_links.txt +0 -0
- {infisicalsdk-1.0.3 → infisicalsdk-1.0.15}/infisicalsdk.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Infisical
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
10
|
+
|
|
11
|
+
NOTE: This license pertains specifically to components of the codebase that do not possess an explicit license. Each distinct SDK incorporated within this software is governed by its individual licensing terms and conditions. The provisions outlined in this MIT license are applicable to those segments of the codebase not explicitly covered by their respective licenses.
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: infisicalsdk
|
|
3
|
-
Version: 1.0.
|
|
4
|
-
Summary: Infisical
|
|
3
|
+
Version: 1.0.15
|
|
4
|
+
Summary: Official Infisical SDK for Python (Latest)
|
|
5
5
|
Home-page: https://github.com/Infisical/python-sdk-official
|
|
6
6
|
Author: Infisical
|
|
7
7
|
Author-email: support@infisical.com
|
|
8
|
-
Keywords: Infisical,Infisical API,Infisical SDK
|
|
8
|
+
Keywords: Infisical,Infisical API,Infisical SDK,SDK,Secrets Management
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
|
-
|
|
10
|
+
License-File: LICENSE
|
|
11
11
|
Requires-Dist: python-dateutil
|
|
12
12
|
Requires-Dist: aenum
|
|
13
13
|
Requires-Dist: requests~=2.32
|
|
@@ -19,8 +19,10 @@ Dynamic: description
|
|
|
19
19
|
Dynamic: description-content-type
|
|
20
20
|
Dynamic: home-page
|
|
21
21
|
Dynamic: keywords
|
|
22
|
+
Dynamic: license-file
|
|
22
23
|
Dynamic: requires-dist
|
|
23
24
|
Dynamic: summary
|
|
24
25
|
|
|
25
|
-
Infisical SDK
|
|
26
|
+
The official Infisical SDK for Python.
|
|
27
|
+
Documentation can be found at https://github.com/Infisical/python-sdk-official
|
|
26
28
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<h1 align="center">
|
|
2
|
+
<img width="300" src="/img/logoname-white.svg#gh-dark-mode-only" alt="infisical">
|
|
3
|
+
</h1>
|
|
4
|
+
<p align="center">
|
|
5
|
+
<p align="center"><b>Infisical Python SDK</b></p>
|
|
6
|
+
<h4 align="center">
|
|
7
|
+
|
|
|
8
|
+
<a href="https://infisical.com/docs/sdks/languages/python">Documentation</a> |
|
|
9
|
+
<a href="https://www.infisical.com">Website</a> |
|
|
10
|
+
<a href="https://infisical.com/slack">Slack</a> |
|
|
11
|
+
</h4>
|
|
12
|
+
|
|
13
|
+
<h4 align="center">
|
|
14
|
+
<a href="https://github.com/Infisical/python-sdk-official/blob/main/LICENSE">
|
|
15
|
+
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="Infisical SDK's are released under the MIT license." />
|
|
16
|
+
</a>
|
|
17
|
+
<a href="https://infisical.com/slack">
|
|
18
|
+
<img src="https://img.shields.io/badge/chat-on%20Slack-blueviolet" alt="Slack community channel" />
|
|
19
|
+
</a>
|
|
20
|
+
<a href="https://twitter.com/infisical">
|
|
21
|
+
<img src="https://img.shields.io/twitter/follow/infisical?label=Follow" alt="Infisical Twitter" />
|
|
22
|
+
</a>
|
|
23
|
+
</h4>
|
|
24
|
+
|
|
25
|
+
## Introduction
|
|
26
|
+
|
|
27
|
+
**[Infisical](https://infisical.com)** is the open source secret management platform that teams use to centralize their secrets like API keys, database credentials, and configurations.
|
|
28
|
+
|
|
29
|
+
If you’re working with Python, the official Infisical Python SDK package is the easiest way to fetch and work with secrets for your application. You can read the documentation [here](https://infisical.com/docs/sdks/languages/python).
|
|
30
|
+
|
|
31
|
+
## Documentation
|
|
32
|
+
You can find the documentation for the Python SDK on our [SDK documentation page](https://infisical.com/docs/sdks/languages/python).
|
|
33
|
+
|
|
34
|
+
## Security
|
|
35
|
+
|
|
36
|
+
Please do not file GitHub issues or post on our public forum for security vulnerabilities, as they are public!
|
|
37
|
+
|
|
38
|
+
Infisical takes security issues very seriously. If you have any concerns about Infisical or believe you have uncovered a vulnerability, please get in touch via the e-mail address security@infisical.com. In the message, try to provide a description of the issue and ideally a way of reproducing it. The security team will get back to you as soon as possible.
|
|
39
|
+
|
|
40
|
+
Note that this security address should be used only for undisclosed vulnerabilities. Please report any security problems to us before disclosing it publicly.
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
from .client import InfisicalSDKClient # noqa
|
|
2
2
|
from .infisical_requests import InfisicalError # noqa
|
|
3
|
-
from .api_types import SingleSecretResponse, ListSecretsResponse, BaseSecret # noqa
|
|
3
|
+
from .api_types import SingleSecretResponse, ListSecretsResponse, BaseSecret, SymmetricEncryption, DynamicSecretProviders # noqa
|
|
@@ -0,0 +1,397 @@
|
|
|
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
|
+
secretValueHidden: Optional[bool] = False
|
|
78
|
+
secretReminderNote: Optional[str] = None
|
|
79
|
+
secretReminderRepeatDays: Optional[int] = None
|
|
80
|
+
skipMultilineEncoding: Optional[bool] = False
|
|
81
|
+
metadata: Optional[Any] = None
|
|
82
|
+
secretPath: Optional[str] = None
|
|
83
|
+
tags: List[SecretTag] = field(default_factory=list)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class Import(BaseModel):
|
|
88
|
+
"""Model for imports section"""
|
|
89
|
+
secretPath: str
|
|
90
|
+
environment: str
|
|
91
|
+
folderId: Optional[str] = None
|
|
92
|
+
secrets: List[BaseSecret] = field(default_factory=list)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class ListSecretsResponse(BaseModel):
|
|
97
|
+
"""Complete response model for secrets API"""
|
|
98
|
+
secrets: List[BaseSecret]
|
|
99
|
+
imports: List[Import] = field(default_factory=list)
|
|
100
|
+
|
|
101
|
+
@classmethod
|
|
102
|
+
def from_dict(cls, data: Dict) -> 'ListSecretsResponse':
|
|
103
|
+
"""Create model from dictionary with camelCase keys, handling nested objects"""
|
|
104
|
+
return cls(
|
|
105
|
+
secrets=[BaseSecret.from_dict(secret) for secret in data['secrets']],
|
|
106
|
+
imports=[Import.from_dict(imp) for imp in data.get('imports', [])]
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass
|
|
111
|
+
class SingleSecretResponse(BaseModel):
|
|
112
|
+
"""Response model for get secret API"""
|
|
113
|
+
secret: BaseSecret
|
|
114
|
+
|
|
115
|
+
@classmethod
|
|
116
|
+
def from_dict(cls, data: Dict) -> 'SingleSecretResponse':
|
|
117
|
+
return cls(
|
|
118
|
+
secret=BaseSecret.from_dict(data['secret']),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass
|
|
123
|
+
class MachineIdentityLoginResponse(BaseModel):
|
|
124
|
+
"""Response model for machine identity login API"""
|
|
125
|
+
accessToken: str
|
|
126
|
+
expiresIn: int
|
|
127
|
+
accessTokenMaxTTL: int
|
|
128
|
+
tokenType: str
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class SymmetricEncryption(str, Enum):
|
|
132
|
+
AES_GCM_256 = "aes-256-gcm"
|
|
133
|
+
AES_GCM_128 = "aes-128-gcm"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class OrderDirection(str, Enum):
|
|
137
|
+
ASC = "asc"
|
|
138
|
+
DESC = "desc"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class KmsKeysOrderBy(str, Enum):
|
|
142
|
+
NAME = "name"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@dataclass
|
|
146
|
+
class KmsKey(BaseModel):
|
|
147
|
+
"""Infisical KMS Key"""
|
|
148
|
+
id: str
|
|
149
|
+
description: str
|
|
150
|
+
isDisabled: bool
|
|
151
|
+
orgId: str
|
|
152
|
+
name: str
|
|
153
|
+
createdAt: str
|
|
154
|
+
updatedAt: str
|
|
155
|
+
projectId: str
|
|
156
|
+
version: int
|
|
157
|
+
encryptionAlgorithm: SymmetricEncryption
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@dataclass
|
|
161
|
+
class ListKmsKeysResponse(BaseModel):
|
|
162
|
+
"""Complete response model for Kms Keys API"""
|
|
163
|
+
keys: List[KmsKey]
|
|
164
|
+
totalCount: int
|
|
165
|
+
|
|
166
|
+
@classmethod
|
|
167
|
+
def from_dict(cls, data: Dict) -> 'ListKmsKeysResponse':
|
|
168
|
+
"""Create model from dictionary with camelCase keys, handling nested objects"""
|
|
169
|
+
return cls(
|
|
170
|
+
keys=[KmsKey.from_dict(key) for key in data['keys']],
|
|
171
|
+
totalCount=data['totalCount']
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@dataclass
|
|
176
|
+
class SingleKmsKeyResponse(BaseModel):
|
|
177
|
+
"""Response model for get/create/update/delete API"""
|
|
178
|
+
key: KmsKey
|
|
179
|
+
|
|
180
|
+
@classmethod
|
|
181
|
+
def from_dict(cls, data: Dict) -> 'SingleKmsKeyResponse':
|
|
182
|
+
return cls(
|
|
183
|
+
key=KmsKey.from_dict(data['key']),
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@dataclass
|
|
188
|
+
class KmsKeyEncryptDataResponse(BaseModel):
|
|
189
|
+
"""Response model for encrypt data API"""
|
|
190
|
+
ciphertext: str
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@dataclass
|
|
194
|
+
class KmsKeyDecryptDataResponse(BaseModel):
|
|
195
|
+
"""Response model for decrypt data API"""
|
|
196
|
+
plaintext: str
|
|
197
|
+
|
|
198
|
+
@dataclass
|
|
199
|
+
class CreateFolderResponseItem(BaseModel):
|
|
200
|
+
"""Folder model with path for create response"""
|
|
201
|
+
id: str
|
|
202
|
+
name: str
|
|
203
|
+
createdAt: str
|
|
204
|
+
updatedAt: str
|
|
205
|
+
envId: str
|
|
206
|
+
path: str
|
|
207
|
+
version: Optional[int] = 1
|
|
208
|
+
parentId: Optional[str] = None
|
|
209
|
+
isReserved: Optional[bool] = False
|
|
210
|
+
description: Optional[str] = None
|
|
211
|
+
lastSecretModified: Optional[str] = None
|
|
212
|
+
|
|
213
|
+
@dataclass
|
|
214
|
+
class CreateFolderResponse(BaseModel):
|
|
215
|
+
"""Response model for create folder API"""
|
|
216
|
+
folder: CreateFolderResponseItem
|
|
217
|
+
|
|
218
|
+
@classmethod
|
|
219
|
+
def from_dict(cls, data: Dict) -> 'CreateFolderResponse':
|
|
220
|
+
return cls(
|
|
221
|
+
folder=CreateFolderResponseItem.from_dict(data['folder']),
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@dataclass
|
|
226
|
+
class ListFoldersResponseItem(BaseModel):
|
|
227
|
+
"""Response model for list folders API"""
|
|
228
|
+
id: str
|
|
229
|
+
name: str
|
|
230
|
+
createdAt: str
|
|
231
|
+
updatedAt: str
|
|
232
|
+
envId: str
|
|
233
|
+
version: Optional[int] = 1
|
|
234
|
+
parentId: Optional[str] = None
|
|
235
|
+
isReserved: Optional[bool] = False
|
|
236
|
+
description: Optional[str] = None
|
|
237
|
+
lastSecretModified: Optional[str] = None
|
|
238
|
+
relativePath: Optional[str] = None
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@dataclass
|
|
242
|
+
class ListFoldersResponse(BaseModel):
|
|
243
|
+
"""Complete response model for folders API"""
|
|
244
|
+
folders: List[ListFoldersResponseItem]
|
|
245
|
+
|
|
246
|
+
@classmethod
|
|
247
|
+
def from_dict(cls, data: Dict) -> 'ListFoldersResponse':
|
|
248
|
+
"""Create model from dictionary with camelCase keys, handling nested objects"""
|
|
249
|
+
return cls(
|
|
250
|
+
folders=[ListFoldersResponseItem.from_dict(folder) for folder in data['folders']]
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@dataclass
|
|
255
|
+
class Environment(BaseModel):
|
|
256
|
+
"""Environment model"""
|
|
257
|
+
envId: str
|
|
258
|
+
envName: str
|
|
259
|
+
envSlug: str
|
|
260
|
+
|
|
261
|
+
@dataclass
|
|
262
|
+
class SingleFolderResponseItem(BaseModel):
|
|
263
|
+
"""Response model for get folder API"""
|
|
264
|
+
id: str
|
|
265
|
+
name: str
|
|
266
|
+
createdAt: str
|
|
267
|
+
updatedAt: str
|
|
268
|
+
envId: str
|
|
269
|
+
path: str
|
|
270
|
+
projectId: str
|
|
271
|
+
environment: Environment
|
|
272
|
+
version: Optional[int] = 1
|
|
273
|
+
parentId: Optional[str] = None
|
|
274
|
+
isReserved: Optional[bool] = False
|
|
275
|
+
description: Optional[str] = None
|
|
276
|
+
lastSecretModified: Optional[str] = None
|
|
277
|
+
|
|
278
|
+
@classmethod
|
|
279
|
+
def from_dict(cls, data: Dict) -> 'SingleFolderResponseItem':
|
|
280
|
+
"""Create model from dictionary with nested Environment"""
|
|
281
|
+
folder_data = data.copy()
|
|
282
|
+
folder_data['environment'] = Environment.from_dict(data['environment'])
|
|
283
|
+
|
|
284
|
+
return super().from_dict(folder_data)
|
|
285
|
+
|
|
286
|
+
@dataclass
|
|
287
|
+
class SingleFolderResponse(BaseModel):
|
|
288
|
+
"""Response model for get/create folder API"""
|
|
289
|
+
folder: SingleFolderResponseItem
|
|
290
|
+
|
|
291
|
+
@classmethod
|
|
292
|
+
def from_dict(cls, data: Dict) -> 'SingleFolderResponse':
|
|
293
|
+
return cls(
|
|
294
|
+
folder=SingleFolderResponseItem.from_dict(data['folder']),
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
class DynamicSecretProviders(str, Enum):
|
|
298
|
+
"""Enum for dynamic secret provider types"""
|
|
299
|
+
AWS_ELASTICACHE = "aws-elasticache"
|
|
300
|
+
AWS_IAM = "aws-iam"
|
|
301
|
+
AZURE_ENTRA_ID = "azure-entra-id"
|
|
302
|
+
AZURE_SQL_DATABASE = "azure-sql-database"
|
|
303
|
+
CASSANDRA = "cassandra"
|
|
304
|
+
COUCHBASE = "couchbase"
|
|
305
|
+
ELASTICSEARCH = "elastic-search"
|
|
306
|
+
GCP_IAM = "gcp-iam"
|
|
307
|
+
GITHUB = "github"
|
|
308
|
+
KUBERNETES = "kubernetes"
|
|
309
|
+
LDAP = "ldap"
|
|
310
|
+
MONGO_ATLAS = "mongo-db-atlas"
|
|
311
|
+
MONGODB = "mongo-db"
|
|
312
|
+
RABBITMQ = "rabbit-mq"
|
|
313
|
+
REDIS = "redis"
|
|
314
|
+
SAP_ASE = "sap-ase"
|
|
315
|
+
SAP_HANA = "sap-hana"
|
|
316
|
+
SNOWFLAKE = "snowflake"
|
|
317
|
+
SQL_DATABASE = "sql-database"
|
|
318
|
+
TOTP = "totp"
|
|
319
|
+
VERTICA = "vertica"
|
|
320
|
+
|
|
321
|
+
@dataclass
|
|
322
|
+
class DynamicSecret(BaseModel):
|
|
323
|
+
"""Infisical Dynamic Secret"""
|
|
324
|
+
id: str
|
|
325
|
+
name: str
|
|
326
|
+
version: int
|
|
327
|
+
type: str
|
|
328
|
+
folderId: str
|
|
329
|
+
createdAt: str
|
|
330
|
+
updatedAt: str
|
|
331
|
+
defaultTTL: Optional[str] = None
|
|
332
|
+
maxTTL: Optional[str] = None
|
|
333
|
+
status: Optional[str] = None
|
|
334
|
+
statusDetails: Optional[str] = None
|
|
335
|
+
usernameTemplate: Optional[str] = None
|
|
336
|
+
metadata: Optional[List[Dict[str, str]]] = field(default_factory=list)
|
|
337
|
+
inputs: Optional[Any] = None
|
|
338
|
+
|
|
339
|
+
@dataclass
|
|
340
|
+
class SingleDynamicSecretResponse(BaseModel):
|
|
341
|
+
"""Response model for get/create/update/delete dynamic secret API"""
|
|
342
|
+
dynamicSecret: DynamicSecret
|
|
343
|
+
|
|
344
|
+
@classmethod
|
|
345
|
+
def from_dict(cls, data: Dict) -> 'SingleDynamicSecretResponse':
|
|
346
|
+
return cls(
|
|
347
|
+
dynamicSecret=DynamicSecret.from_dict(data['dynamicSecret']),
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
@dataclass
|
|
351
|
+
class DynamicSecretLease(BaseModel):
|
|
352
|
+
"""Infisical Dynamic Secret Lease"""
|
|
353
|
+
id: str
|
|
354
|
+
expireAt: str
|
|
355
|
+
createdAt: str
|
|
356
|
+
updatedAt: str
|
|
357
|
+
version: int
|
|
358
|
+
dynamicSecretId: str
|
|
359
|
+
externalEntityId: str
|
|
360
|
+
status: Optional[str] = None
|
|
361
|
+
statusDetails: Optional[str] = None
|
|
362
|
+
dynamicSecret: Optional[DynamicSecret] = None
|
|
363
|
+
|
|
364
|
+
@classmethod
|
|
365
|
+
def from_dict(cls, data: Dict) -> 'DynamicSecretLease':
|
|
366
|
+
"""Create model from dictionary with nested DynamicSecret"""
|
|
367
|
+
lease_data = data.copy()
|
|
368
|
+
if 'dynamicSecret' in data and data['dynamicSecret'] is not None:
|
|
369
|
+
lease_data['dynamicSecret'] = DynamicSecret.from_dict(data['dynamicSecret'])
|
|
370
|
+
|
|
371
|
+
return super().from_dict(lease_data)
|
|
372
|
+
|
|
373
|
+
@dataclass
|
|
374
|
+
class CreateLeaseResponse(BaseModel):
|
|
375
|
+
"""Response model for create lease API - returns lease, dynamicSecret, and data"""
|
|
376
|
+
lease: DynamicSecretLease
|
|
377
|
+
dynamicSecret: DynamicSecret
|
|
378
|
+
data: Any
|
|
379
|
+
|
|
380
|
+
@classmethod
|
|
381
|
+
def from_dict(cls, data: Dict) -> 'CreateLeaseResponse':
|
|
382
|
+
return cls(
|
|
383
|
+
lease=DynamicSecretLease.from_dict(data['lease']),
|
|
384
|
+
dynamicSecret=DynamicSecret.from_dict(data['dynamicSecret']),
|
|
385
|
+
data=data.get('data', {}),
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
@dataclass
|
|
389
|
+
class SingleLeaseResponse(BaseModel):
|
|
390
|
+
"""Response model for get/delete/renew lease API - returns only lease"""
|
|
391
|
+
lease: DynamicSecretLease
|
|
392
|
+
|
|
393
|
+
@classmethod
|
|
394
|
+
def from_dict(cls, data: Dict) -> 'SingleLeaseResponse':
|
|
395
|
+
return cls(
|
|
396
|
+
lease=DynamicSecretLease.from_dict(data['lease']),
|
|
397
|
+
)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from .infisical_requests import InfisicalRequests
|
|
2
|
+
|
|
3
|
+
from infisical_sdk.resources import Auth
|
|
4
|
+
from infisical_sdk.resources import V3RawSecrets
|
|
5
|
+
from infisical_sdk.resources import KMS
|
|
6
|
+
from infisical_sdk.resources import V2Folders
|
|
7
|
+
from infisical_sdk.resources import DynamicSecrets
|
|
8
|
+
|
|
9
|
+
from infisical_sdk.util import SecretsCache
|
|
10
|
+
|
|
11
|
+
class InfisicalSDKClient:
|
|
12
|
+
def __init__(self, host: str, token: str = None, cache_ttl: int = 60):
|
|
13
|
+
"""
|
|
14
|
+
Initialize the Infisical SDK client.
|
|
15
|
+
|
|
16
|
+
:param str host: The host URL for your Infisical instance. Will default to `https://app.infisical.com` if not specified.
|
|
17
|
+
:param str token: The authentication token for the client. If not specified, you can use the `auth` methods to authenticate.
|
|
18
|
+
:param int cache_ttl: The time to live for the secrets cache. This is the number of seconds that secrets fetched from the API will be cached for. Set to `None` to disable caching. Defaults to `60` seconds.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
self.host = host
|
|
22
|
+
self.access_token = token
|
|
23
|
+
|
|
24
|
+
self.api = InfisicalRequests(host=host, token=token)
|
|
25
|
+
self.cache = SecretsCache(cache_ttl)
|
|
26
|
+
self.auth = Auth(self.api, self.set_token)
|
|
27
|
+
self.secrets = V3RawSecrets(self.api, self.cache)
|
|
28
|
+
self.kms = KMS(self.api)
|
|
29
|
+
self.folders = V2Folders(self.api)
|
|
30
|
+
self.dynamic_secrets = DynamicSecrets(self.api)
|
|
31
|
+
|
|
32
|
+
def set_token(self, token: str):
|
|
33
|
+
"""
|
|
34
|
+
Set the access token for future requests.
|
|
35
|
+
"""
|
|
36
|
+
self.api.set_token(token)
|
|
37
|
+
self.access_token = token
|
|
38
|
+
|
|
39
|
+
def get_token(self):
|
|
40
|
+
"""
|
|
41
|
+
Get the access token for future requests.
|
|
42
|
+
"""
|
|
43
|
+
return self.access_token
|
|
44
|
+
|
|
45
|
+
def close(self):
|
|
46
|
+
"""
|
|
47
|
+
Close the client and release resources.
|
|
48
|
+
|
|
49
|
+
This stops the background cache cleanup thread. You don't need to call
|
|
50
|
+
this if you're using the client as a context manager (with statement),
|
|
51
|
+
as cleanup happens automatically when exiting the context.
|
|
52
|
+
"""
|
|
53
|
+
if self.cache:
|
|
54
|
+
self.cache.close()
|
|
55
|
+
|
|
56
|
+
# These are automatically called if using the client as a context manager (on start)
|
|
57
|
+
# Example:
|
|
58
|
+
# with InfisicalSDKClient(...) as client:
|
|
59
|
+
# ...
|
|
60
|
+
def __enter__(self) -> "InfisicalSDKClient":
|
|
61
|
+
"""Support for context manager protocol."""
|
|
62
|
+
return self
|
|
63
|
+
|
|
64
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
65
|
+
"""Ensure cleanup when exiting context."""
|
|
66
|
+
self.close()
|
|
@@ -1,10 +1,34 @@
|
|
|
1
|
-
from typing import Any, Dict, Generic, Optional, TypeVar
|
|
2
|
-
|
|
1
|
+
from typing import Any, Dict, Generic, Optional, TypeVar, Type, Callable, List
|
|
2
|
+
import socket
|
|
3
3
|
import requests
|
|
4
|
+
import functools
|
|
4
5
|
from dataclasses import dataclass
|
|
6
|
+
import time
|
|
7
|
+
import random
|
|
5
8
|
|
|
6
9
|
T = TypeVar("T")
|
|
7
10
|
|
|
11
|
+
# List of network-related exceptions that should trigger retries
|
|
12
|
+
NETWORK_ERRORS = [
|
|
13
|
+
requests.exceptions.ConnectionError,
|
|
14
|
+
requests.exceptions.ChunkedEncodingError,
|
|
15
|
+
requests.exceptions.ReadTimeout,
|
|
16
|
+
requests.exceptions.ConnectTimeout,
|
|
17
|
+
socket.gaierror,
|
|
18
|
+
socket.timeout,
|
|
19
|
+
ConnectionResetError,
|
|
20
|
+
ConnectionRefusedError,
|
|
21
|
+
ConnectionError,
|
|
22
|
+
ConnectionAbortedError,
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
def join_url(base: str, path: str) -> str:
|
|
26
|
+
"""
|
|
27
|
+
Join base URL and path properly, handling slashes appropriately.
|
|
28
|
+
"""
|
|
29
|
+
if not base.endswith('/'):
|
|
30
|
+
base += '/'
|
|
31
|
+
return base + path.lstrip('/')
|
|
8
32
|
|
|
9
33
|
class InfisicalError(Exception):
|
|
10
34
|
"""Base exception for Infisical client errors"""
|
|
@@ -43,6 +67,42 @@ class APIResponse(Generic[T]):
|
|
|
43
67
|
headers=data['headers']
|
|
44
68
|
)
|
|
45
69
|
|
|
70
|
+
def with_retry(
|
|
71
|
+
max_retries: int = 3,
|
|
72
|
+
base_delay: float = 1.0,
|
|
73
|
+
network_errors: Optional[List[Type[Exception]]] = None
|
|
74
|
+
) -> Callable:
|
|
75
|
+
"""
|
|
76
|
+
Decorator to add retry logic with exponential backoff to requests methods.
|
|
77
|
+
"""
|
|
78
|
+
if network_errors is None:
|
|
79
|
+
network_errors = NETWORK_ERRORS
|
|
80
|
+
|
|
81
|
+
def decorator(func: Callable) -> Callable:
|
|
82
|
+
@functools.wraps(func)
|
|
83
|
+
def wrapper(*args, **kwargs):
|
|
84
|
+
retry_count = 0
|
|
85
|
+
|
|
86
|
+
while True:
|
|
87
|
+
try:
|
|
88
|
+
return func(*args, **kwargs)
|
|
89
|
+
except tuple(network_errors) as error:
|
|
90
|
+
retry_count += 1
|
|
91
|
+
if retry_count > max_retries:
|
|
92
|
+
raise
|
|
93
|
+
|
|
94
|
+
base_delay_with_backoff = base_delay * (2 ** (retry_count - 1))
|
|
95
|
+
|
|
96
|
+
# +/-20% jitter
|
|
97
|
+
jitter = random.uniform(-0.2, 0.2) * base_delay_with_backoff
|
|
98
|
+
delay = base_delay_with_backoff + jitter
|
|
99
|
+
|
|
100
|
+
time.sleep(delay)
|
|
101
|
+
|
|
102
|
+
return wrapper
|
|
103
|
+
|
|
104
|
+
return decorator
|
|
105
|
+
|
|
46
106
|
|
|
47
107
|
class InfisicalRequests:
|
|
48
108
|
def __init__(self, host: str, token: Optional[str] = None):
|
|
@@ -60,7 +120,7 @@ class InfisicalRequests:
|
|
|
60
120
|
|
|
61
121
|
def _build_url(self, path: str) -> str:
|
|
62
122
|
"""Construct full URL from path"""
|
|
63
|
-
return
|
|
123
|
+
return join_url(self.host, path.lstrip("/"))
|
|
64
124
|
|
|
65
125
|
def set_token(self, token: str):
|
|
66
126
|
"""Set authorization token"""
|
|
@@ -87,10 +147,11 @@ class InfisicalRequests:
|
|
|
87
147
|
except ValueError:
|
|
88
148
|
raise InfisicalError("Invalid JSON response")
|
|
89
149
|
|
|
150
|
+
@with_retry(max_retries=4, base_delay=1.0)
|
|
90
151
|
def get(
|
|
91
152
|
self,
|
|
92
153
|
path: str,
|
|
93
|
-
model:
|
|
154
|
+
model: Type[T],
|
|
94
155
|
params: Optional[Dict[str, Any]] = None
|
|
95
156
|
) -> APIResponse[T]:
|
|
96
157
|
|
|
@@ -113,10 +174,11 @@ class InfisicalRequests:
|
|
|
113
174
|
headers=dict(response.headers)
|
|
114
175
|
)
|
|
115
176
|
|
|
177
|
+
@with_retry(max_retries=4, base_delay=1.0)
|
|
116
178
|
def post(
|
|
117
179
|
self,
|
|
118
180
|
path: str,
|
|
119
|
-
model:
|
|
181
|
+
model: Type[T],
|
|
120
182
|
json: Optional[Dict[str, Any]] = None
|
|
121
183
|
) -> APIResponse[T]:
|
|
122
184
|
|
|
@@ -137,10 +199,11 @@ class InfisicalRequests:
|
|
|
137
199
|
headers=dict(response.headers)
|
|
138
200
|
)
|
|
139
201
|
|
|
202
|
+
@with_retry(max_retries=4, base_delay=1.0)
|
|
140
203
|
def patch(
|
|
141
204
|
self,
|
|
142
205
|
path: str,
|
|
143
|
-
model:
|
|
206
|
+
model: Type[T],
|
|
144
207
|
json: Optional[Dict[str, Any]] = None
|
|
145
208
|
) -> APIResponse[T]:
|
|
146
209
|
|
|
@@ -161,10 +224,11 @@ class InfisicalRequests:
|
|
|
161
224
|
headers=dict(response.headers)
|
|
162
225
|
)
|
|
163
226
|
|
|
227
|
+
@with_retry(max_retries=4, base_delay=1.0)
|
|
164
228
|
def delete(
|
|
165
229
|
self,
|
|
166
230
|
path: str,
|
|
167
|
-
model:
|
|
231
|
+
model: Type[T],
|
|
168
232
|
json: Optional[Dict[str, Any]] = None
|
|
169
233
|
) -> APIResponse[T]:
|
|
170
234
|
|