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.
Files changed (32) hide show
  1. infisicalsdk-1.0.15/LICENSE +11 -0
  2. {infisicalsdk-1.0.3 → infisicalsdk-1.0.15}/PKG-INFO +8 -6
  3. infisicalsdk-1.0.15/README.md +40 -0
  4. {infisicalsdk-1.0.3 → infisicalsdk-1.0.15}/infisical_sdk/__init__.py +1 -1
  5. infisicalsdk-1.0.15/infisical_sdk/api_types.py +397 -0
  6. infisicalsdk-1.0.15/infisical_sdk/client.py +66 -0
  7. {infisicalsdk-1.0.3 → infisicalsdk-1.0.15}/infisical_sdk/infisical_requests.py +71 -7
  8. infisicalsdk-1.0.15/infisical_sdk/resources/__init__.py +5 -0
  9. infisicalsdk-1.0.15/infisical_sdk/resources/auth.py +14 -0
  10. infisicalsdk-1.0.15/infisical_sdk/resources/auth_methods/__init__.py +4 -0
  11. infisicalsdk-1.0.15/infisical_sdk/resources/auth_methods/aws_auth.py +134 -0
  12. infisicalsdk-1.0.15/infisical_sdk/resources/auth_methods/oidc_auth.py +36 -0
  13. infisicalsdk-1.0.15/infisical_sdk/resources/auth_methods/token_auth.py +22 -0
  14. infisicalsdk-1.0.15/infisical_sdk/resources/auth_methods/universal_auth.py +35 -0
  15. infisicalsdk-1.0.15/infisical_sdk/resources/dynamic_secrets.py +335 -0
  16. infisicalsdk-1.0.15/infisical_sdk/resources/folders.py +75 -0
  17. infisicalsdk-1.0.15/infisical_sdk/resources/kms.py +177 -0
  18. infisicalsdk-1.0.15/infisical_sdk/resources/secrets.py +269 -0
  19. infisicalsdk-1.0.15/infisical_sdk/util/__init__.py +1 -0
  20. infisicalsdk-1.0.15/infisical_sdk/util/secrets_cache.py +192 -0
  21. {infisicalsdk-1.0.3 → infisicalsdk-1.0.15}/infisicalsdk.egg-info/PKG-INFO +8 -6
  22. infisicalsdk-1.0.15/infisicalsdk.egg-info/SOURCES.txt +27 -0
  23. {infisicalsdk-1.0.3 → infisicalsdk-1.0.15}/infisicalsdk.egg-info/requires.txt +0 -1
  24. {infisicalsdk-1.0.3 → infisicalsdk-1.0.15}/pyproject.toml +0 -1
  25. {infisicalsdk-1.0.3 → infisicalsdk-1.0.15}/setup.cfg +1 -1
  26. {infisicalsdk-1.0.3 → infisicalsdk-1.0.15}/setup.py +6 -6
  27. infisicalsdk-1.0.3/README.md +0 -204
  28. infisicalsdk-1.0.3/infisical_sdk/api_types.py +0 -127
  29. infisicalsdk-1.0.3/infisical_sdk/client.py +0 -345
  30. infisicalsdk-1.0.3/infisicalsdk.egg-info/SOURCES.txt +0 -13
  31. {infisicalsdk-1.0.3 → infisicalsdk-1.0.15}/infisicalsdk.egg-info/dependency_links.txt +0 -0
  32. {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.2
1
+ Metadata-Version: 2.4
2
2
  Name: infisicalsdk
3
- Version: 1.0.3
4
- Summary: Infisical API Client
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
- Requires-Dist: urllib3<2.1.0,>=1.25.3
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 client for Python. To view documentation, please visit https://github.com/Infisical/python-sdk-official
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
- from urllib.parse import urljoin
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 urljoin(self.host, path.lstrip("/"))
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: type[T],
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: type[T],
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: type[T],
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: type[T],
231
+ model: Type[T],
168
232
  json: Optional[Dict[str, Any]] = None
169
233
  ) -> APIResponse[T]:
170
234
 
@@ -0,0 +1,5 @@
1
+ from .secrets import V3RawSecrets
2
+ from .kms import KMS
3
+ from .auth import Auth
4
+ from .folders import V2Folders
5
+ from .dynamic_secrets import DynamicSecrets