infisicalsdk 1.0.6__py3-none-any.whl → 1.0.8__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.

@@ -0,0 +1,134 @@
1
+ from botocore.auth import SigV4Auth
2
+ from botocore.awsrequest import AWSRequest
3
+ from botocore.exceptions import NoCredentialsError
4
+
5
+ from infisical_sdk.infisical_requests import InfisicalRequests
6
+ from infisical_sdk.api_types import MachineIdentityLoginResponse
7
+
8
+ from typing import Callable
9
+
10
+ import requests
11
+ import boto3
12
+ import base64
13
+ import json
14
+ import os
15
+ import datetime
16
+
17
+ from typing import Dict, Any
18
+
19
+
20
+ class AWSAuth:
21
+ def __init__(self, requests: InfisicalRequests, setToken: Callable[[str], None]) -> None:
22
+ self.requests = requests
23
+ self.setToken = setToken
24
+
25
+ def login(self, identity_id: str) -> MachineIdentityLoginResponse:
26
+ """
27
+ Login with AWS Authentication.
28
+
29
+ Args:
30
+ identity_id (str): Your Machine Identity ID that has AWS Auth configured.
31
+
32
+ Returns:
33
+ Dict: A dictionary containing the access token and related information.
34
+ """
35
+
36
+ identity_id = identity_id or os.getenv("INFISICAL_AWS_IAM_AUTH_IDENTITY_ID")
37
+ if not identity_id:
38
+ raise ValueError(
39
+ "Identity ID must be provided or set in the environment variable" +
40
+ "INFISICAL_AWS_IAM_AUTH_IDENTITY_ID."
41
+ )
42
+
43
+ aws_region = self.get_aws_region()
44
+ session = boto3.Session(region_name=aws_region)
45
+
46
+ credentials = self._get_aws_credentials(session)
47
+
48
+ iam_request_url = f"https://sts.{aws_region}.amazonaws.com/"
49
+ iam_request_body = "Action=GetCallerIdentity&Version=2011-06-15"
50
+
51
+ request_headers = self._prepare_aws_request(
52
+ iam_request_url,
53
+ iam_request_body,
54
+ credentials,
55
+ aws_region
56
+ )
57
+
58
+ requestBody = {
59
+ "identityId": identity_id,
60
+ "iamRequestBody": base64.b64encode(iam_request_body.encode()).decode(),
61
+ "iamRequestHeaders": base64.b64encode(json.dumps(request_headers).encode()).decode(),
62
+ "iamHttpRequestMethod": "POST"
63
+ }
64
+
65
+ result = self.requests.post(
66
+ path="/api/v1/auth/aws-auth/login",
67
+ json=requestBody,
68
+ model=MachineIdentityLoginResponse
69
+ )
70
+
71
+ self.setToken(result.data.accessToken)
72
+
73
+ return result.data
74
+
75
+ def _get_aws_credentials(self, session: boto3.Session) -> Any:
76
+ try:
77
+ credentials = session.get_credentials()
78
+ if credentials is None:
79
+ raise NoCredentialsError("AWS credentials not found.")
80
+ return credentials.get_frozen_credentials()
81
+ except NoCredentialsError as e:
82
+ raise RuntimeError(f"AWS IAM Auth Login failed: {str(e)}")
83
+
84
+ def _prepare_aws_request(
85
+ self,
86
+ url: str,
87
+ body: str,
88
+ credentials: Any,
89
+ region: str) -> Dict[str, str]:
90
+
91
+ current_time = datetime.datetime.now(datetime.timezone.utc)
92
+ amz_date = current_time.strftime('%Y%m%dT%H%M%SZ')
93
+
94
+ request = AWSRequest(method="POST", url=url, data=body)
95
+ request.headers["X-Amz-Date"] = amz_date
96
+ request.headers["Host"] = f"sts.{region}.amazonaws.com"
97
+ request.headers["Content-Type"] = "application/x-www-form-urlencoded; charset=utf-8"
98
+ request.headers["Content-Length"] = str(len(body))
99
+
100
+ signer = SigV4Auth(credentials, "sts", region)
101
+ signer.add_auth(request)
102
+
103
+ return {k: v for k, v in request.headers.items() if k.lower() != "content-length"}
104
+
105
+ @staticmethod
106
+ def get_aws_region() -> str:
107
+ region = os.getenv("AWS_REGION") # Typically found in lambda runtime environment
108
+ if region:
109
+ return region
110
+
111
+ try:
112
+ return AWSAuth._get_aws_ec2_identity_document_region()
113
+ except Exception as e:
114
+ raise Exception("Failed to retrieve AWS region") from e
115
+
116
+ @staticmethod
117
+ def _get_aws_ec2_identity_document_region(timeout: int = 5000) -> str:
118
+ session = requests.Session()
119
+ token_response = session.put(
120
+ "http://169.254.169.254/latest/api/token",
121
+ headers={"X-aws-ec2-metadata-token-ttl-seconds": "21600"},
122
+ timeout=timeout / 1000
123
+ )
124
+ token_response.raise_for_status()
125
+ metadata_token = token_response.text
126
+
127
+ identity_response = session.get(
128
+ "http://169.254.169.254/latest/dynamic/instance-identity/document",
129
+ headers={"X-aws-ec2-metadata-token": metadata_token, "Accept": "application/json"},
130
+ timeout=timeout / 1000
131
+ )
132
+
133
+ identity_response.raise_for_status()
134
+ return identity_response.json().get("region")
@@ -0,0 +1,35 @@
1
+ from infisical_sdk.api_types import MachineIdentityLoginResponse
2
+
3
+ from typing import Callable
4
+ from infisical_sdk.infisical_requests import InfisicalRequests
5
+ class UniversalAuth:
6
+ def __init__(self, requests: InfisicalRequests, setToken: Callable[[str], None]):
7
+ self.requests = requests
8
+ self.setToken = setToken
9
+
10
+ def login(self, client_id: str, client_secret: str) -> MachineIdentityLoginResponse:
11
+ """
12
+ Login with Universal Auth.
13
+
14
+ Args:
15
+ client_id (str): Your Machine Identity Client ID.
16
+ client_secret (str): Your Machine Identity Client Secret.
17
+
18
+ Returns:
19
+ Dict: A dictionary containing the access token and related information.
20
+ """
21
+
22
+ requestBody = {
23
+ "clientId": client_id,
24
+ "clientSecret": client_secret
25
+ }
26
+
27
+ result = self.requests.post(
28
+ path="/api/v1/auth/universal-auth/login",
29
+ json=requestBody,
30
+ model=MachineIdentityLoginResponse
31
+ )
32
+
33
+ self.setToken(result.data.accessToken)
34
+
35
+ return result.data
@@ -0,0 +1,177 @@
1
+ from infisical_sdk.api_types import SymmetricEncryption, KmsKeysOrderBy, OrderDirection
2
+ from infisical_sdk.api_types import ListKmsKeysResponse, SingleKmsKeyResponse
3
+ from infisical_sdk.api_types import KmsKey, KmsKeyEncryptDataResponse, KmsKeyDecryptDataResponse
4
+
5
+ from infisical_sdk.infisical_requests import InfisicalRequests
6
+
7
+
8
+ class KMS:
9
+ def __init__(self, requests: InfisicalRequests) -> None:
10
+ self.requests = requests
11
+
12
+ def list_keys(
13
+ self,
14
+ project_id: str,
15
+ offset: int = 0,
16
+ limit: int = 100,
17
+ order_by: KmsKeysOrderBy = KmsKeysOrderBy.NAME,
18
+ order_direction: OrderDirection = OrderDirection.ASC,
19
+ search: str = None) -> ListKmsKeysResponse:
20
+
21
+ params = {
22
+ "projectId": project_id,
23
+ "search": search,
24
+ "offset": offset,
25
+ "limit": limit,
26
+ "orderBy": order_by,
27
+ "orderDirection": order_direction,
28
+ }
29
+
30
+ result = self.requests.get(
31
+ path="/api/v1/kms/keys",
32
+ params=params,
33
+ model=ListKmsKeysResponse
34
+ )
35
+
36
+ return result.data
37
+
38
+ def get_key_by_id(
39
+ self,
40
+ key_id: str) -> KmsKey:
41
+
42
+ result = self.requests.get(
43
+ path=f"/api/v1/kms/keys/{key_id}",
44
+ model=SingleKmsKeyResponse
45
+ )
46
+
47
+ return result.data.key
48
+
49
+ def get_key_by_name(
50
+ self,
51
+ key_name: str,
52
+ project_id: str) -> KmsKey:
53
+
54
+ params = {
55
+ "projectId": project_id,
56
+ }
57
+
58
+ result = self.requests.get(
59
+ path=f"/api/v1/kms/keys/key-name/{key_name}",
60
+ params=params,
61
+ model=SingleKmsKeyResponse
62
+ )
63
+
64
+ return result.data.key
65
+
66
+ def create_key(
67
+ self,
68
+ name: str,
69
+ project_id: str,
70
+ encryption_algorithm: SymmetricEncryption,
71
+ description: str = None) -> KmsKey:
72
+
73
+ request_body = {
74
+ "name": name,
75
+ "projectId": project_id,
76
+ "encryptionAlgorithm": encryption_algorithm,
77
+ "description": description,
78
+ }
79
+
80
+ result = self.requests.post(
81
+ path="/api/v1/kms/keys",
82
+ json=request_body,
83
+ model=SingleKmsKeyResponse
84
+ )
85
+
86
+ return result.data.key
87
+
88
+ def update_key(
89
+ self,
90
+ key_id: str,
91
+ name: str = None,
92
+ is_disabled: bool = None,
93
+ description: str = None) -> KmsKey:
94
+
95
+ request_body = {
96
+ "name": name,
97
+ "isDisabled": is_disabled,
98
+ "description": description,
99
+ }
100
+
101
+ result = self.requests.patch(
102
+ path=f"/api/v1/kms/keys/{key_id}",
103
+ json=request_body,
104
+ model=SingleKmsKeyResponse
105
+ )
106
+
107
+ return result.data.key
108
+
109
+ def delete_key(
110
+ self,
111
+ key_id: str) -> KmsKey:
112
+
113
+ result = self.requests.delete(
114
+ path=f"/api/v1/kms/keys/{key_id}",
115
+ json={},
116
+ model=SingleKmsKeyResponse
117
+ )
118
+
119
+ return result.data.key
120
+
121
+ def encrypt_data(
122
+ self,
123
+ key_id: str,
124
+ base64EncodedPlaintext: str) -> str:
125
+ """
126
+ Encrypt data with the specified KMS key.
127
+
128
+ :param key_id: The ID of the key to decrypt the ciphertext with
129
+ :type key_id: str
130
+ :param base64EncodedPlaintext: The base64 encoded plaintext to encrypt
131
+ :type plaintext: str
132
+
133
+
134
+ :return: The encrypted base64 encoded plaintext (ciphertext)
135
+ :rtype: str
136
+ """
137
+
138
+ request_body = {
139
+ "plaintext": base64EncodedPlaintext
140
+ }
141
+
142
+ result = self.requests.post(
143
+ path=f"/api/v1/kms/keys/{key_id}/encrypt",
144
+ json=request_body,
145
+ model=KmsKeyEncryptDataResponse
146
+ )
147
+
148
+ return result.data.ciphertext
149
+
150
+ def decrypt_data(
151
+ self,
152
+ key_id: str,
153
+ ciphertext: str) -> str:
154
+ """
155
+ Decrypt data with the specified KMS key.
156
+
157
+ :param key_id: The ID of the key to decrypt the ciphertext with
158
+ :type key_id: str
159
+ :param ciphertext: The encrypted base64 plaintext to decrypt
160
+ :type ciphertext: str
161
+
162
+
163
+ :return: The base64 encoded plaintext
164
+ :rtype: str
165
+ """
166
+
167
+ request_body = {
168
+ "ciphertext": ciphertext
169
+ }
170
+
171
+ result = self.requests.post(
172
+ path=f"/api/v1/kms/keys/{key_id}/decrypt",
173
+ json=request_body,
174
+ model=KmsKeyDecryptDataResponse
175
+ )
176
+
177
+ return result.data.plaintext
@@ -0,0 +1,235 @@
1
+ from typing import List, Union
2
+
3
+ from infisical_sdk.infisical_requests import InfisicalRequests
4
+ from infisical_sdk.api_types import ListSecretsResponse, SingleSecretResponse, BaseSecret
5
+ from infisical_sdk.util import SecretsCache
6
+
7
+ CACHE_KEY_LIST_SECRETS = "cache-list-secrets"
8
+ CACHE_KEY_SINGLE_SECRET = "cache-single-secret"
9
+
10
+ class V3RawSecrets:
11
+ def __init__(self, requests: InfisicalRequests, cache: SecretsCache) -> None:
12
+ self.requests = requests
13
+ self.cache = cache
14
+
15
+ def list_secrets(
16
+ self,
17
+ project_id: str,
18
+ environment_slug: str,
19
+ secret_path: str,
20
+ expand_secret_references: bool = True,
21
+ view_secret_value: bool = True,
22
+ recursive: bool = False,
23
+ include_imports: bool = True,
24
+ tag_filters: List[str] = []) -> ListSecretsResponse:
25
+
26
+ params = {
27
+ "workspaceId": project_id,
28
+ "environment": environment_slug,
29
+ "secretPath": secret_path,
30
+ "viewSecretValue": str(view_secret_value).lower(),
31
+ "expandSecretReferences": str(expand_secret_references).lower(),
32
+ "recursive": str(recursive).lower(),
33
+ "include_imports": str(include_imports).lower(),
34
+ }
35
+
36
+ if tag_filters:
37
+ params["tagSlugs"] = ",".join(tag_filters)
38
+
39
+
40
+ cache_key = self.cache.compute_cache_key(CACHE_KEY_LIST_SECRETS, **params)
41
+ if self.cache.enabled:
42
+ cached_response = self.cache.get(cache_key)
43
+
44
+ if cached_response is not None and isinstance(cached_response, ListSecretsResponse):
45
+ return cached_response
46
+
47
+ result = self.requests.get(
48
+ path="/api/v3/secrets/raw",
49
+ params=params,
50
+ model=ListSecretsResponse
51
+ )
52
+
53
+ if self.cache.enabled:
54
+ self.cache.set(cache_key, result.data)
55
+
56
+ return result.data
57
+
58
+ def get_secret_by_name(
59
+ self,
60
+ secret_name: str,
61
+ project_id: str,
62
+ environment_slug: str,
63
+ secret_path: str,
64
+ expand_secret_references: bool = True,
65
+ include_imports: bool = True,
66
+ view_secret_value: bool = True,
67
+ version: str = None) -> BaseSecret:
68
+
69
+ params = {
70
+ "workspaceId": project_id,
71
+ "viewSecretValue": str(view_secret_value).lower(),
72
+ "environment": environment_slug,
73
+ "secretPath": secret_path,
74
+ "expandSecretReferences": str(expand_secret_references).lower(),
75
+ "include_imports": str(include_imports).lower(),
76
+ "version": version
77
+ }
78
+
79
+ cache_params = {
80
+ "project_id": project_id,
81
+ "environment_slug": environment_slug,
82
+ "secret_path": secret_path,
83
+ "secret_name": secret_name,
84
+ }
85
+
86
+ cache_key = self.cache.compute_cache_key(CACHE_KEY_SINGLE_SECRET, **cache_params)
87
+
88
+ if self.cache.enabled:
89
+ cached_response = self.cache.get(cache_key)
90
+
91
+ if cached_response is not None and isinstance(cached_response, BaseSecret):
92
+ return cached_response
93
+
94
+ result = self.requests.get(
95
+ path=f"/api/v3/secrets/raw/{secret_name}",
96
+ params=params,
97
+ model=SingleSecretResponse
98
+ )
99
+
100
+ if self.cache.enabled:
101
+ self.cache.set(cache_key, result.data.secret)
102
+
103
+ return result.data.secret
104
+
105
+ def create_secret_by_name(
106
+ self,
107
+ secret_name: str,
108
+ project_id: str,
109
+ secret_path: str,
110
+ environment_slug: str,
111
+ secret_value: str = None,
112
+ secret_comment: str = None,
113
+ skip_multiline_encoding: bool = False,
114
+ secret_reminder_repeat_days: Union[float, int] = None,
115
+ secret_reminder_note: str = None) -> BaseSecret:
116
+
117
+ requestBody = {
118
+ "workspaceId": project_id,
119
+ "environment": environment_slug,
120
+ "secretPath": secret_path,
121
+ "secretValue": secret_value,
122
+ "secretComment": secret_comment,
123
+ "tagIds": None,
124
+ "skipMultilineEncoding": skip_multiline_encoding,
125
+ "type": "shared",
126
+ "secretReminderRepeatDays": secret_reminder_repeat_days,
127
+ "secretReminderNote": secret_reminder_note
128
+ }
129
+ result = self.requests.post(
130
+ path=f"/api/v3/secrets/raw/{secret_name}",
131
+ json=requestBody,
132
+ model=SingleSecretResponse
133
+ )
134
+
135
+
136
+ if self.cache.enabled:
137
+ cache_params = {
138
+ "project_id": project_id,
139
+ "environment_slug": environment_slug,
140
+ "secret_path": secret_path,
141
+ "secret_name": secret_name,
142
+ }
143
+
144
+ cache_key = self.cache.compute_cache_key(CACHE_KEY_SINGLE_SECRET, **cache_params)
145
+ self.cache.set(cache_key, result.data.secret)
146
+
147
+ # Invalidates all list secret cache
148
+ self.cache.invalidate_operation(CACHE_KEY_LIST_SECRETS)
149
+
150
+ return result.data.secret
151
+
152
+ def update_secret_by_name(
153
+ self,
154
+ current_secret_name: str,
155
+ project_id: str,
156
+ secret_path: str,
157
+ environment_slug: str,
158
+ secret_value: str = None,
159
+ secret_comment: str = None,
160
+ skip_multiline_encoding: bool = False,
161
+ secret_reminder_repeat_days: Union[float, int] = None,
162
+ secret_reminder_note: str = None,
163
+ new_secret_name: str = None) -> BaseSecret:
164
+
165
+ requestBody = {
166
+ "workspaceId": project_id,
167
+ "environment": environment_slug,
168
+ "secretPath": secret_path,
169
+ "secretValue": secret_value,
170
+ "secretComment": secret_comment,
171
+ "newSecretName": new_secret_name,
172
+ "tagIds": None,
173
+ "skipMultilineEncoding": skip_multiline_encoding,
174
+ "type": "shared",
175
+ "secretReminderRepeatDays": secret_reminder_repeat_days,
176
+ "secretReminderNote": secret_reminder_note
177
+ }
178
+
179
+ result = self.requests.patch(
180
+ path=f"/api/v3/secrets/raw/{current_secret_name}",
181
+ json=requestBody,
182
+ model=SingleSecretResponse
183
+ )
184
+
185
+ if self.cache.enabled:
186
+ cache_params = {
187
+ "project_id": project_id,
188
+ "environment_slug": environment_slug,
189
+ "secret_path": secret_path,
190
+ "secret_name": current_secret_name,
191
+ }
192
+
193
+ cache_key = self.cache.compute_cache_key(CACHE_KEY_SINGLE_SECRET, **cache_params)
194
+ self.cache.unset(cache_key)
195
+
196
+ # Invalidates all list secret cache
197
+ self.cache.invalidate_operation(CACHE_KEY_LIST_SECRETS)
198
+
199
+ return result.data.secret
200
+
201
+ def delete_secret_by_name(
202
+ self,
203
+ secret_name: str,
204
+ project_id: str,
205
+ secret_path: str,
206
+ environment_slug: str) -> BaseSecret:
207
+
208
+ requestBody = {
209
+ "workspaceId": project_id,
210
+ "environment": environment_slug,
211
+ "secretPath": secret_path,
212
+ "type": "shared",
213
+ }
214
+
215
+ result = self.requests.delete(
216
+ path=f"/api/v3/secrets/raw/{secret_name}",
217
+ json=requestBody,
218
+ model=SingleSecretResponse
219
+ )
220
+
221
+ if self.cache.enabled:
222
+ cache_params = {
223
+ "project_id": project_id,
224
+ "environment_slug": environment_slug,
225
+ "secret_path": secret_path,
226
+ "secret_name": secret_name,
227
+ }
228
+
229
+ cache_key = self.cache.compute_cache_key(CACHE_KEY_SINGLE_SECRET, **cache_params)
230
+ self.cache.unset(cache_key)
231
+
232
+ # Invalidates all list secret cache
233
+ self.cache.invalidate_operation(CACHE_KEY_LIST_SECRETS)
234
+
235
+ return result.data.secret
@@ -0,0 +1 @@
1
+ from .secrets_cache import SecretsCache
@@ -0,0 +1,106 @@
1
+ from typing import Dict, Tuple, Any
2
+
3
+ from infisical_sdk.api_types import BaseSecret
4
+ import json
5
+ import time
6
+ import threading
7
+ from hashlib import sha256
8
+ import pickle
9
+
10
+ MAX_CACHE_SIZE = 1000
11
+
12
+ class SecretsCache:
13
+ def __init__(self, ttl_seconds: int = 60) -> None:
14
+ if ttl_seconds is None or ttl_seconds <= 0:
15
+ self.enabled = False
16
+ return
17
+
18
+ self.enabled = True
19
+ self.ttl = ttl_seconds
20
+ self.cleanup_interval = 60
21
+
22
+ self.cache: Dict[str, Tuple[bytes, float]] = {}
23
+
24
+ self.lock = threading.RLock()
25
+
26
+ self.stop_cleanup_thread = False
27
+ self.cleanup_thread = threading.Thread(target=self._cleanup_worker, daemon=True)
28
+ self.cleanup_thread.start()
29
+
30
+ def compute_cache_key(self, operation_name: str, **kwargs) -> str:
31
+ sorted_kwargs = sorted(kwargs.items())
32
+ json_str = json.dumps(sorted_kwargs)
33
+
34
+ return f"{operation_name}-{sha256(json_str.encode()).hexdigest()}"
35
+
36
+ def get(self, cache_key: str) -> Any:
37
+ if not self.enabled:
38
+ return None
39
+
40
+ with self.lock:
41
+ if cache_key in self.cache:
42
+ serialized_value, timestamp = self.cache[cache_key]
43
+ if time.time() - timestamp <= self.ttl:
44
+ return pickle.loads(serialized_value)
45
+ else:
46
+ self.cache.pop(cache_key, None)
47
+ return None
48
+ else:
49
+ return None
50
+
51
+
52
+ def set(self, cache_key: str, value: Any) -> None:
53
+ if not self.enabled:
54
+ return
55
+
56
+ with self.lock:
57
+ serialized_value = pickle.dumps(value)
58
+ self.cache[cache_key] = (serialized_value, time.time())
59
+
60
+ if len(self.cache) > MAX_CACHE_SIZE:
61
+ oldest_key = min(self.cache.keys(), key=lambda k: self.cache[k][1]) # oldest key based on timestamp
62
+ self.cache.pop(oldest_key)
63
+
64
+
65
+
66
+ def unset(self, cache_key: str) -> None:
67
+ if not self.enabled:
68
+ return
69
+
70
+ with self.lock:
71
+ self.cache.pop(cache_key, None)
72
+
73
+ def invalidate_operation(self, operation_name: str) -> None:
74
+ if not self.enabled:
75
+ return
76
+
77
+ with self.lock:
78
+ for key in list(self.cache.keys()):
79
+ if key.startswith(operation_name):
80
+ self.cache.pop(key, None)
81
+
82
+
83
+ def _cleanup_expired_items(self) -> None:
84
+ """Remove all expired items from the cache."""
85
+ current_time = time.time()
86
+ with self.lock:
87
+ expired_keys = [
88
+ key for key, (_, timestamp) in self.cache.items()
89
+ if current_time - timestamp > self.ttl
90
+ ]
91
+ for key in expired_keys:
92
+ self.cache.pop(key, None)
93
+
94
+ def _cleanup_worker(self) -> None:
95
+ """Background worker that periodically cleans up expired items."""
96
+ while not self.stop_cleanup_thread:
97
+ time.sleep(self.cleanup_interval)
98
+ self._cleanup_expired_items()
99
+
100
+ def __del__(self) -> None:
101
+ """Ensure thread is properly stopped when the object is garbage collected."""
102
+ self.stop_cleanup_thread = True
103
+ if self.enabled and self.cleanup_thread.is_alive():
104
+ self.cleanup_thread.join(timeout=1.0)
105
+
106
+
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: infisicalsdk
3
- Version: 1.0.6
3
+ Version: 1.0.8
4
4
  Summary: Infisical API Client
5
5
  Home-page: https://github.com/Infisical/python-sdk-official
6
6
  Author: Infisical