opensecureconf-client 1.0.2__py3-none-any.whl → 2.0.2__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.
- opensecureconf_client-2.0.2.dist-info/METADATA +1289 -0
- opensecureconf_client-2.0.2.dist-info/RECORD +6 -0
- opensecureconf_client.py +477 -22
- opensecureconf_client-1.0.2.dist-info/METADATA +0 -229
- opensecureconf_client-1.0.2.dist-info/RECORD +0 -6
- {opensecureconf_client-1.0.2.dist-info → opensecureconf_client-2.0.2.dist-info}/WHEEL +0 -0
- {opensecureconf_client-1.0.2.dist-info → opensecureconf_client-2.0.2.dist-info}/licenses/LICENSE +0 -0
- {opensecureconf_client-1.0.2.dist-info → opensecureconf_client-2.0.2.dist-info}/top_level.txt +0 -0
opensecureconf_client.py
CHANGED
|
@@ -1,15 +1,31 @@
|
|
|
1
1
|
"""
|
|
2
|
-
OpenSecureConf Python Client
|
|
2
|
+
OpenSecureConf Python Client - Enhanced Edition
|
|
3
3
|
|
|
4
4
|
A Python client library for interacting with the OpenSecureConf API,
|
|
5
|
-
which provides encrypted configuration management with
|
|
5
|
+
which provides encrypted configuration management with clustering support.
|
|
6
|
+
|
|
7
|
+
Enhanced Features:
|
|
8
|
+
- Automatic retry logic with exponential backoff
|
|
9
|
+
- Cluster awareness (status, health)
|
|
10
|
+
- Connection pooling
|
|
11
|
+
- Structured logging
|
|
12
|
+
- Batch operations
|
|
13
|
+
- Enhanced input validation
|
|
14
|
+
- Health check utilities
|
|
6
15
|
"""
|
|
7
16
|
|
|
8
17
|
from typing import Any, Dict, List, Optional
|
|
9
|
-
|
|
18
|
+
import logging
|
|
19
|
+
import time
|
|
10
20
|
import requests
|
|
11
|
-
from requests.
|
|
21
|
+
from requests.adapters import HTTPAdapter
|
|
22
|
+
from requests.exceptions import Timeout, RequestException
|
|
23
|
+
from urllib3.util.retry import Retry
|
|
24
|
+
|
|
12
25
|
|
|
26
|
+
# ============================================================================
|
|
27
|
+
# EXCEPTIONS
|
|
28
|
+
# ============================================================================
|
|
13
29
|
|
|
14
30
|
class OpenSecureConfError(Exception):
|
|
15
31
|
"""Base exception for OpenSecureConf client errors."""
|
|
@@ -27,9 +43,17 @@ class ConfigurationExistsError(OpenSecureConfError):
|
|
|
27
43
|
"""Raised when attempting to create a configuration that already exists."""
|
|
28
44
|
|
|
29
45
|
|
|
46
|
+
class ClusterError(OpenSecureConfError):
|
|
47
|
+
"""Raised when cluster operations fail."""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ============================================================================
|
|
51
|
+
# CLIENT
|
|
52
|
+
# ============================================================================
|
|
53
|
+
|
|
30
54
|
class OpenSecureConfClient:
|
|
31
55
|
"""
|
|
32
|
-
|
|
56
|
+
Enhanced client for interacting with the OpenSecureConf API.
|
|
33
57
|
|
|
34
58
|
This client provides methods to create, read, update, delete, and list
|
|
35
59
|
encrypted configuration entries stored in an OpenSecureConf service.
|
|
@@ -39,12 +63,15 @@ class OpenSecureConfClient:
|
|
|
39
63
|
user_key (str): The encryption key used for authentication and encryption/decryption.
|
|
40
64
|
api_key (Optional[str]): Optional API key for additional authentication.
|
|
41
65
|
timeout (int): Request timeout in seconds.
|
|
66
|
+
logger (logging.Logger): Logger instance for debugging.
|
|
42
67
|
|
|
43
68
|
Example:
|
|
44
69
|
>>> client = OpenSecureConfClient(
|
|
45
70
|
... base_url="http://localhost:9000",
|
|
46
71
|
... user_key="my-secret-key-123",
|
|
47
|
-
... api_key="optional-api-key"
|
|
72
|
+
... api_key="optional-api-key",
|
|
73
|
+
... enable_retry=True,
|
|
74
|
+
... log_level="INFO"
|
|
48
75
|
... )
|
|
49
76
|
>>> config = client.create("database", {"host": "localhost", "port": 5432})
|
|
50
77
|
>>> print(config["value"])
|
|
@@ -57,10 +84,16 @@ class OpenSecureConfClient:
|
|
|
57
84
|
user_key: str,
|
|
58
85
|
api_key: Optional[str] = None,
|
|
59
86
|
timeout: int = 30,
|
|
60
|
-
verify_ssl: bool = True
|
|
87
|
+
verify_ssl: bool = True,
|
|
88
|
+
enable_retry: bool = True,
|
|
89
|
+
max_retries: int = 3,
|
|
90
|
+
backoff_factor: float = 1.0,
|
|
91
|
+
pool_connections: int = 10,
|
|
92
|
+
pool_maxsize: int = 20,
|
|
93
|
+
log_level: str = "WARNING"
|
|
61
94
|
):
|
|
62
95
|
"""
|
|
63
|
-
Initialize the OpenSecureConf client.
|
|
96
|
+
Initialize the OpenSecureConf client with enhanced features.
|
|
64
97
|
|
|
65
98
|
Args:
|
|
66
99
|
base_url: The base URL of the OpenSecureConf API (e.g., "http://localhost:9000")
|
|
@@ -68,35 +101,87 @@ class OpenSecureConfClient:
|
|
|
68
101
|
api_key: Optional API key for additional authentication
|
|
69
102
|
timeout: Request timeout in seconds (default: 30)
|
|
70
103
|
verify_ssl: Whether to verify SSL certificates (default: True)
|
|
104
|
+
enable_retry: Enable automatic retry with exponential backoff (default: True)
|
|
105
|
+
max_retries: Maximum number of retries for failed requests (default: 3)
|
|
106
|
+
backoff_factor: Backoff factor for retry delays (default: 1.0)
|
|
107
|
+
pool_connections: Number of connection pools (default: 10)
|
|
108
|
+
pool_maxsize: Maximum pool size (default: 20)
|
|
109
|
+
log_level: Logging level (default: WARNING)
|
|
71
110
|
|
|
72
111
|
Raises:
|
|
73
|
-
ValueError: If user_key is shorter than 8 characters
|
|
112
|
+
ValueError: If user_key is shorter than 8 characters or invalid parameters
|
|
74
113
|
"""
|
|
114
|
+
# Validation
|
|
75
115
|
if len(user_key) < 8:
|
|
76
116
|
raise ValueError("User key must be at least 8 characters long")
|
|
77
|
-
|
|
117
|
+
if not base_url:
|
|
118
|
+
raise ValueError("base_url cannot be empty")
|
|
119
|
+
if timeout <= 0:
|
|
120
|
+
raise ValueError("timeout must be positive")
|
|
121
|
+
if max_retries < 0:
|
|
122
|
+
raise ValueError("max_retries must be non-negative")
|
|
123
|
+
|
|
124
|
+
# Configuration
|
|
78
125
|
self.base_url = base_url.rstrip("/")
|
|
79
126
|
self.user_key = user_key
|
|
80
127
|
self.api_key = api_key
|
|
81
128
|
self.timeout = timeout
|
|
82
129
|
self.verify_ssl = verify_ssl
|
|
130
|
+
|
|
131
|
+
# Setup logging
|
|
132
|
+
self.logger = logging.getLogger(__name__)
|
|
133
|
+
self.logger.setLevel(getattr(logging, log_level.upper(), logging.WARNING))
|
|
134
|
+
if not self.logger.handlers:
|
|
135
|
+
handler = logging.StreamHandler()
|
|
136
|
+
formatter = logging.Formatter(
|
|
137
|
+
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
138
|
+
)
|
|
139
|
+
handler.setFormatter(formatter)
|
|
140
|
+
self.logger.addHandler(handler)
|
|
141
|
+
|
|
142
|
+
# Initialize session
|
|
83
143
|
self._session = requests.Session()
|
|
84
144
|
|
|
145
|
+
# Setup connection pooling
|
|
146
|
+
adapter = HTTPAdapter(
|
|
147
|
+
pool_connections=pool_connections,
|
|
148
|
+
pool_maxsize=pool_maxsize
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Setup retry strategy if enabled
|
|
152
|
+
if enable_retry:
|
|
153
|
+
retry_strategy = Retry(
|
|
154
|
+
total=max_retries,
|
|
155
|
+
backoff_factor=backoff_factor,
|
|
156
|
+
status_forcelist=[429, 500, 502, 503, 504],
|
|
157
|
+
allowed_methods=["HEAD", "GET", "PUT", "DELETE", "OPTIONS", "TRACE", "POST"]
|
|
158
|
+
)
|
|
159
|
+
adapter = HTTPAdapter(
|
|
160
|
+
max_retries=retry_strategy,
|
|
161
|
+
pool_connections=pool_connections,
|
|
162
|
+
pool_maxsize=pool_maxsize
|
|
163
|
+
)
|
|
164
|
+
self.logger.info(f"Retry enabled: max_retries={max_retries}, backoff_factor={backoff_factor}")
|
|
165
|
+
|
|
166
|
+
self._session.mount("http://", adapter)
|
|
167
|
+
self._session.mount("https://", adapter)
|
|
168
|
+
|
|
85
169
|
# Setup headers
|
|
86
170
|
headers = {
|
|
87
|
-
"x-user-key": self.user_key,
|
|
171
|
+
"x-user-key": self.user_key,
|
|
88
172
|
"Content-Type": "application/json"
|
|
89
173
|
}
|
|
90
174
|
|
|
91
|
-
# Add X-API-Key header if api_key is provided
|
|
92
175
|
if self.api_key:
|
|
93
176
|
headers["X-API-Key"] = self.api_key
|
|
94
177
|
|
|
95
178
|
self._session.headers.update(headers)
|
|
96
179
|
|
|
180
|
+
self.logger.info(f"Client initialized for {self.base_url}")
|
|
181
|
+
|
|
97
182
|
def _make_request(self, method: str, endpoint: str, **kwargs) -> Any:
|
|
98
183
|
"""
|
|
99
|
-
Make an HTTP request to the API with error handling.
|
|
184
|
+
Make an HTTP request to the API with error handling and logging.
|
|
100
185
|
|
|
101
186
|
Args:
|
|
102
187
|
method: HTTP method (GET, POST, PUT, DELETE)
|
|
@@ -117,38 +202,89 @@ class OpenSecureConfClient:
|
|
|
117
202
|
kwargs.setdefault("timeout", self.timeout)
|
|
118
203
|
kwargs.setdefault("verify", self.verify_ssl)
|
|
119
204
|
|
|
205
|
+
start_time = time.time()
|
|
206
|
+
self.logger.debug(f"{method} {url}")
|
|
207
|
+
|
|
120
208
|
try:
|
|
121
209
|
response = self._session.request(method, url, **kwargs)
|
|
210
|
+
duration = time.time() - start_time
|
|
122
211
|
|
|
212
|
+
self.logger.info(
|
|
213
|
+
f"{method} {endpoint} - Status: {response.status_code} - Duration: {duration:.3f}s"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# Handle error responses
|
|
123
217
|
if response.status_code == 401:
|
|
218
|
+
self.logger.error("Authentication failed: invalid or missing user key")
|
|
124
219
|
raise AuthenticationError(
|
|
125
220
|
"Authentication failed: invalid or missing user key"
|
|
126
221
|
)
|
|
222
|
+
|
|
223
|
+
if response.status_code == 403:
|
|
224
|
+
self.logger.error("Forbidden: invalid API key")
|
|
225
|
+
raise AuthenticationError("Forbidden: invalid API key")
|
|
226
|
+
|
|
127
227
|
if response.status_code == 404:
|
|
128
|
-
|
|
228
|
+
error_detail = response.json().get("detail", "Configuration not found")
|
|
229
|
+
self.logger.warning(f"Not found: {error_detail}")
|
|
230
|
+
raise ConfigurationNotFoundError(error_detail)
|
|
231
|
+
|
|
129
232
|
if response.status_code == 400:
|
|
130
233
|
error_detail = response.json().get("detail", "Bad request")
|
|
131
234
|
if "already exists" in error_detail.lower():
|
|
235
|
+
self.logger.warning(f"Configuration exists: {error_detail}")
|
|
132
236
|
raise ConfigurationExistsError(error_detail)
|
|
237
|
+
self.logger.error(f"Bad request: {error_detail}")
|
|
133
238
|
raise OpenSecureConfError(f"Bad request: {error_detail}")
|
|
239
|
+
|
|
134
240
|
if response.status_code >= 400:
|
|
135
241
|
error_detail = response.json().get("detail", "Unknown error")
|
|
242
|
+
self.logger.error(f"API error {response.status_code}: {error_detail}")
|
|
136
243
|
raise OpenSecureConfError(
|
|
137
244
|
f"API error ({response.status_code}): {error_detail}"
|
|
138
245
|
)
|
|
139
246
|
|
|
247
|
+
# Handle successful responses
|
|
140
248
|
if response.status_code == 204 or not response.content:
|
|
141
249
|
return None
|
|
142
250
|
|
|
143
251
|
return response.json()
|
|
144
252
|
|
|
145
253
|
except (ConnectionError, Timeout) as e:
|
|
254
|
+
self.logger.error(f"Connection error: {str(e)}")
|
|
146
255
|
raise ConnectionError(
|
|
147
256
|
f"Failed to connect to {self.base_url}: {str(e)}"
|
|
148
257
|
) from e
|
|
258
|
+
except RequestException as e:
|
|
259
|
+
self.logger.error(f"Request error: {str(e)}")
|
|
260
|
+
raise OpenSecureConfError(f"Request failed: {str(e)}") from e
|
|
149
261
|
except ValueError as e:
|
|
262
|
+
self.logger.error(f"Invalid JSON response: {str(e)}")
|
|
150
263
|
raise OpenSecureConfError(f"Invalid JSON response: {str(e)}") from e
|
|
151
264
|
|
|
265
|
+
# ========================================================================
|
|
266
|
+
# HEALTH & STATUS
|
|
267
|
+
# ========================================================================
|
|
268
|
+
|
|
269
|
+
def ping(self) -> bool:
|
|
270
|
+
"""
|
|
271
|
+
Check if the API server is reachable and responding.
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
True if server is healthy, False otherwise
|
|
275
|
+
|
|
276
|
+
Example:
|
|
277
|
+
>>> if client.ping():
|
|
278
|
+
... print("Server is healthy")
|
|
279
|
+
"""
|
|
280
|
+
try:
|
|
281
|
+
self.get_service_info()
|
|
282
|
+
self.logger.debug("Ping successful")
|
|
283
|
+
return True
|
|
284
|
+
except Exception as e:
|
|
285
|
+
self.logger.warning(f"Ping failed: {str(e)}")
|
|
286
|
+
return False
|
|
287
|
+
|
|
152
288
|
def get_service_info(self) -> Dict[str, Any]:
|
|
153
289
|
"""
|
|
154
290
|
Get information about the OpenSecureConf service.
|
|
@@ -159,12 +295,65 @@ class OpenSecureConfClient:
|
|
|
159
295
|
Example:
|
|
160
296
|
>>> info = client.get_service_info()
|
|
161
297
|
>>> print(info["version"])
|
|
162
|
-
1.0
|
|
298
|
+
2.1.0
|
|
163
299
|
"""
|
|
164
300
|
return self._make_request("GET", "/")
|
|
165
301
|
|
|
302
|
+
# ========================================================================
|
|
303
|
+
# CLUSTER OPERATIONS
|
|
304
|
+
# ========================================================================
|
|
305
|
+
|
|
306
|
+
def get_cluster_status(self) -> Dict[str, Any]:
|
|
307
|
+
"""
|
|
308
|
+
Get cluster status and node information.
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
Dictionary containing cluster status with fields:
|
|
312
|
+
- enabled: Whether clustering is enabled
|
|
313
|
+
- mode: Cluster mode (replica or federated)
|
|
314
|
+
- node_id: Current node identifier
|
|
315
|
+
- total_nodes: Total number of nodes in cluster
|
|
316
|
+
- healthy_nodes: Number of healthy nodes
|
|
317
|
+
|
|
318
|
+
Raises:
|
|
319
|
+
ClusterError: If cluster status cannot be retrieved
|
|
320
|
+
|
|
321
|
+
Example:
|
|
322
|
+
>>> status = client.get_cluster_status()
|
|
323
|
+
>>> print(f"Cluster mode: {status['mode']}")
|
|
324
|
+
>>> print(f"Healthy nodes: {status['healthy_nodes']}/{status['total_nodes']}")
|
|
325
|
+
"""
|
|
326
|
+
try:
|
|
327
|
+
return self._make_request("GET", "/cluster/status")
|
|
328
|
+
except OpenSecureConfError as e:
|
|
329
|
+
raise ClusterError(f"Failed to get cluster status: {str(e)}") from e
|
|
330
|
+
|
|
331
|
+
def get_cluster_health(self) -> Dict[str, Any]:
|
|
332
|
+
"""
|
|
333
|
+
Check cluster node health.
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
Dictionary containing health status
|
|
337
|
+
|
|
338
|
+
Example:
|
|
339
|
+
>>> health = client.get_cluster_health()
|
|
340
|
+
>>> print(health["status"])
|
|
341
|
+
healthy
|
|
342
|
+
"""
|
|
343
|
+
try:
|
|
344
|
+
return self._make_request("GET", "/cluster/health")
|
|
345
|
+
except OpenSecureConfError as e:
|
|
346
|
+
raise ClusterError(f"Failed to check cluster health: {str(e)}") from e
|
|
347
|
+
|
|
348
|
+
# ========================================================================
|
|
349
|
+
# CONFIGURATION CRUD OPERATIONS
|
|
350
|
+
# ========================================================================
|
|
351
|
+
|
|
166
352
|
def create(
|
|
167
|
-
self,
|
|
353
|
+
self,
|
|
354
|
+
key: str,
|
|
355
|
+
value: Dict[str, Any],
|
|
356
|
+
category: Optional[str] = None
|
|
168
357
|
) -> Dict[str, Any]:
|
|
169
358
|
"""
|
|
170
359
|
Create a new encrypted configuration entry.
|
|
@@ -183,7 +372,7 @@ class OpenSecureConfClient:
|
|
|
183
372
|
|
|
184
373
|
Raises:
|
|
185
374
|
ConfigurationExistsError: If configuration key already exists
|
|
186
|
-
ValueError: If key is invalid
|
|
375
|
+
ValueError: If key or value is invalid
|
|
187
376
|
|
|
188
377
|
Example:
|
|
189
378
|
>>> config = client.create(
|
|
@@ -192,11 +381,17 @@ class OpenSecureConfClient:
|
|
|
192
381
|
... category="production"
|
|
193
382
|
... )
|
|
194
383
|
"""
|
|
195
|
-
|
|
384
|
+
# Enhanced validation
|
|
385
|
+
if not key or not isinstance(key, str):
|
|
386
|
+
raise ValueError("Key must be a non-empty string")
|
|
387
|
+
if len(key) > 255:
|
|
196
388
|
raise ValueError("Key must be between 1 and 255 characters")
|
|
389
|
+
if not isinstance(value, dict):
|
|
390
|
+
raise ValueError("Value must be a dictionary")
|
|
391
|
+
if category and len(category) > 100:
|
|
392
|
+
raise ValueError("Category must be max 100 characters")
|
|
197
393
|
|
|
198
394
|
payload = {"key": key, "value": value, "category": category}
|
|
199
|
-
|
|
200
395
|
return self._make_request("POST", "/configs", json=payload)
|
|
201
396
|
|
|
202
397
|
def read(self, key: str) -> Dict[str, Any]:
|
|
@@ -211,16 +406,23 @@ class OpenSecureConfClient:
|
|
|
211
406
|
|
|
212
407
|
Raises:
|
|
213
408
|
ConfigurationNotFoundError: If configuration key does not exist
|
|
409
|
+
ValueError: If key is invalid
|
|
214
410
|
|
|
215
411
|
Example:
|
|
216
412
|
>>> config = client.read("database")
|
|
217
413
|
>>> print(config["value"]["host"])
|
|
218
414
|
localhost
|
|
219
415
|
"""
|
|
416
|
+
if not key or not isinstance(key, str):
|
|
417
|
+
raise ValueError("Key must be a non-empty string")
|
|
418
|
+
|
|
220
419
|
return self._make_request("GET", f"/configs/{key}")
|
|
221
420
|
|
|
222
421
|
def update(
|
|
223
|
-
self,
|
|
422
|
+
self,
|
|
423
|
+
key: str,
|
|
424
|
+
value: Dict[str, Any],
|
|
425
|
+
category: Optional[str] = None
|
|
224
426
|
) -> Dict[str, Any]:
|
|
225
427
|
"""
|
|
226
428
|
Update an existing configuration entry with new encrypted value.
|
|
@@ -235,6 +437,7 @@ class OpenSecureConfClient:
|
|
|
235
437
|
|
|
236
438
|
Raises:
|
|
237
439
|
ConfigurationNotFoundError: If configuration key does not exist
|
|
440
|
+
ValueError: If parameters are invalid
|
|
238
441
|
|
|
239
442
|
Example:
|
|
240
443
|
>>> config = client.update(
|
|
@@ -242,8 +445,14 @@ class OpenSecureConfClient:
|
|
|
242
445
|
... value={"host": "db.example.com", "port": 5432}
|
|
243
446
|
... )
|
|
244
447
|
"""
|
|
245
|
-
|
|
448
|
+
if not key or not isinstance(key, str):
|
|
449
|
+
raise ValueError("Key must be a non-empty string")
|
|
450
|
+
if not isinstance(value, dict):
|
|
451
|
+
raise ValueError("Value must be a dictionary")
|
|
452
|
+
if category and len(category) > 100:
|
|
453
|
+
raise ValueError("Category must be max 100 characters")
|
|
246
454
|
|
|
455
|
+
payload = {"value": value, "category": category}
|
|
247
456
|
return self._make_request("PUT", f"/configs/{key}", json=payload)
|
|
248
457
|
|
|
249
458
|
def delete(self, key: str) -> Dict[str, str]:
|
|
@@ -258,12 +467,16 @@ class OpenSecureConfClient:
|
|
|
258
467
|
|
|
259
468
|
Raises:
|
|
260
469
|
ConfigurationNotFoundError: If configuration key does not exist
|
|
470
|
+
ValueError: If key is invalid
|
|
261
471
|
|
|
262
472
|
Example:
|
|
263
473
|
>>> result = client.delete("database")
|
|
264
474
|
>>> print(result["message"])
|
|
265
475
|
Configuration 'database' deleted successfully
|
|
266
476
|
"""
|
|
477
|
+
if not key or not isinstance(key, str):
|
|
478
|
+
raise ValueError("Key must be a non-empty string")
|
|
479
|
+
|
|
267
480
|
return self._make_request("DELETE", f"/configs/{key}")
|
|
268
481
|
|
|
269
482
|
def list_all(self, category: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
@@ -285,16 +498,249 @@ class OpenSecureConfClient:
|
|
|
285
498
|
params = {"category": category} if category else {}
|
|
286
499
|
return self._make_request("GET", "/configs", params=params)
|
|
287
500
|
|
|
501
|
+
# ========================================================================
|
|
502
|
+
# BATCH OPERATIONS
|
|
503
|
+
# ========================================================================
|
|
504
|
+
|
|
505
|
+
def bulk_create(
|
|
506
|
+
self,
|
|
507
|
+
configs: List[Dict[str, Any]],
|
|
508
|
+
ignore_errors: bool = False
|
|
509
|
+
) -> List[Dict[str, Any]]:
|
|
510
|
+
"""
|
|
511
|
+
Create multiple configurations in batch.
|
|
512
|
+
|
|
513
|
+
Args:
|
|
514
|
+
configs: List of configuration dictionaries with 'key', 'value', and optional 'category'
|
|
515
|
+
ignore_errors: If True, continue on errors and return partial results
|
|
516
|
+
|
|
517
|
+
Returns:
|
|
518
|
+
List of created configuration dictionaries
|
|
519
|
+
|
|
520
|
+
Raises:
|
|
521
|
+
ValueError: If configs format is invalid
|
|
522
|
+
OpenSecureConfError: If creation fails and ignore_errors is False
|
|
523
|
+
|
|
524
|
+
Example:
|
|
525
|
+
>>> configs = [
|
|
526
|
+
... {"key": "db1", "value": {"host": "localhost"}, "category": "prod"},
|
|
527
|
+
... {"key": "db2", "value": {"host": "remote"}, "category": "prod"}
|
|
528
|
+
... ]
|
|
529
|
+
>>> results = client.bulk_create(configs)
|
|
530
|
+
>>> print(f"Created {len(results)} configurations")
|
|
531
|
+
"""
|
|
532
|
+
if not isinstance(configs, list):
|
|
533
|
+
raise ValueError("configs must be a list")
|
|
534
|
+
|
|
535
|
+
results = []
|
|
536
|
+
errors = []
|
|
537
|
+
|
|
538
|
+
for i, config in enumerate(configs):
|
|
539
|
+
if not isinstance(config, dict):
|
|
540
|
+
raise ValueError(f"Config at index {i} must be a dictionary")
|
|
541
|
+
if "key" not in config or "value" not in config:
|
|
542
|
+
raise ValueError(f"Config at index {i} missing required 'key' or 'value'")
|
|
543
|
+
|
|
544
|
+
try:
|
|
545
|
+
result = self.create(
|
|
546
|
+
key=config["key"],
|
|
547
|
+
value=config["value"],
|
|
548
|
+
category=config.get("category")
|
|
549
|
+
)
|
|
550
|
+
results.append(result)
|
|
551
|
+
self.logger.info(f"Bulk create: created '{config['key']}'")
|
|
552
|
+
except Exception as e:
|
|
553
|
+
error_msg = f"Failed to create '{config['key']}': {str(e)}"
|
|
554
|
+
self.logger.error(error_msg)
|
|
555
|
+
errors.append({"key": config["key"], "error": str(e)})
|
|
556
|
+
if not ignore_errors:
|
|
557
|
+
raise OpenSecureConfError(error_msg) from e
|
|
558
|
+
|
|
559
|
+
if errors:
|
|
560
|
+
self.logger.warning(f"Bulk create completed with {len(errors)} errors")
|
|
561
|
+
|
|
562
|
+
return results
|
|
563
|
+
|
|
564
|
+
def bulk_read(
|
|
565
|
+
self,
|
|
566
|
+
keys: List[str],
|
|
567
|
+
ignore_errors: bool = False
|
|
568
|
+
) -> List[Dict[str, Any]]:
|
|
569
|
+
"""
|
|
570
|
+
Read multiple configurations in batch.
|
|
571
|
+
|
|
572
|
+
Args:
|
|
573
|
+
keys: List of configuration keys to retrieve
|
|
574
|
+
ignore_errors: If True, skip missing keys and return partial results
|
|
575
|
+
|
|
576
|
+
Returns:
|
|
577
|
+
List of configuration dictionaries
|
|
578
|
+
|
|
579
|
+
Example:
|
|
580
|
+
>>> configs = client.bulk_read(["db1", "db2", "api"])
|
|
581
|
+
>>> print(f"Retrieved {len(configs)} configurations")
|
|
582
|
+
"""
|
|
583
|
+
if not isinstance(keys, list):
|
|
584
|
+
raise ValueError("keys must be a list")
|
|
585
|
+
|
|
586
|
+
results = []
|
|
587
|
+
errors = []
|
|
588
|
+
|
|
589
|
+
for key in keys:
|
|
590
|
+
try:
|
|
591
|
+
result = self.read(key)
|
|
592
|
+
results.append(result)
|
|
593
|
+
except ConfigurationNotFoundError as e:
|
|
594
|
+
self.logger.warning(f"Bulk read: key '{key}' not found")
|
|
595
|
+
errors.append({"key": key, "error": str(e)})
|
|
596
|
+
if not ignore_errors:
|
|
597
|
+
raise
|
|
598
|
+
except Exception as e:
|
|
599
|
+
self.logger.error(f"Bulk read: failed to read '{key}': {str(e)}")
|
|
600
|
+
errors.append({"key": key, "error": str(e)})
|
|
601
|
+
if not ignore_errors:
|
|
602
|
+
raise
|
|
603
|
+
|
|
604
|
+
return results
|
|
605
|
+
|
|
606
|
+
def bulk_delete(
|
|
607
|
+
self,
|
|
608
|
+
keys: List[str],
|
|
609
|
+
ignore_errors: bool = False
|
|
610
|
+
) -> Dict[str, Any]:
|
|
611
|
+
"""
|
|
612
|
+
Delete multiple configurations in batch.
|
|
613
|
+
|
|
614
|
+
Args:
|
|
615
|
+
keys: List of configuration keys to delete
|
|
616
|
+
ignore_errors: If True, continue on errors
|
|
617
|
+
|
|
618
|
+
Returns:
|
|
619
|
+
Dictionary with summary: {"deleted": [...], "failed": [...]}
|
|
620
|
+
|
|
621
|
+
Example:
|
|
622
|
+
>>> result = client.bulk_delete(["temp1", "temp2", "temp3"])
|
|
623
|
+
>>> print(f"Deleted: {len(result['deleted'])}, Failed: {len(result['failed'])}")
|
|
624
|
+
"""
|
|
625
|
+
if not isinstance(keys, list):
|
|
626
|
+
raise ValueError("keys must be a list")
|
|
627
|
+
|
|
628
|
+
deleted = []
|
|
629
|
+
failed = []
|
|
630
|
+
|
|
631
|
+
for key in keys:
|
|
632
|
+
try:
|
|
633
|
+
self.delete(key)
|
|
634
|
+
deleted.append(key)
|
|
635
|
+
self.logger.info(f"Bulk delete: deleted '{key}'")
|
|
636
|
+
except Exception as e:
|
|
637
|
+
self.logger.error(f"Bulk delete: failed to delete '{key}': {str(e)}")
|
|
638
|
+
failed.append({"key": key, "error": str(e)})
|
|
639
|
+
if not ignore_errors:
|
|
640
|
+
raise
|
|
641
|
+
|
|
642
|
+
return {"deleted": deleted, "failed": failed}
|
|
643
|
+
|
|
644
|
+
# ========================================================================
|
|
645
|
+
# UTILITY METHODS
|
|
646
|
+
# ========================================================================
|
|
647
|
+
|
|
648
|
+
def exists(self, key: str) -> bool:
|
|
649
|
+
"""
|
|
650
|
+
Check if a configuration key exists.
|
|
651
|
+
|
|
652
|
+
Args:
|
|
653
|
+
key: Configuration key to check
|
|
654
|
+
|
|
655
|
+
Returns:
|
|
656
|
+
True if key exists, False otherwise
|
|
657
|
+
|
|
658
|
+
Example:
|
|
659
|
+
>>> if client.exists("database"):
|
|
660
|
+
... print("Configuration exists")
|
|
661
|
+
"""
|
|
662
|
+
try:
|
|
663
|
+
self.read(key)
|
|
664
|
+
return True
|
|
665
|
+
except ConfigurationNotFoundError:
|
|
666
|
+
return False
|
|
667
|
+
|
|
668
|
+
def get_or_default(
|
|
669
|
+
self,
|
|
670
|
+
key: str,
|
|
671
|
+
default: Dict[str, Any]
|
|
672
|
+
) -> Dict[str, Any]:
|
|
673
|
+
"""
|
|
674
|
+
Get configuration value or return default if not found.
|
|
675
|
+
|
|
676
|
+
Args:
|
|
677
|
+
key: Configuration key to retrieve
|
|
678
|
+
default: Default value to return if key not found
|
|
679
|
+
|
|
680
|
+
Returns:
|
|
681
|
+
Configuration dictionary or default value
|
|
682
|
+
|
|
683
|
+
Example:
|
|
684
|
+
>>> config = client.get_or_default(
|
|
685
|
+
... "database",
|
|
686
|
+
... {"host": "localhost", "port": 5432}
|
|
687
|
+
... )
|
|
688
|
+
"""
|
|
689
|
+
try:
|
|
690
|
+
return self.read(key)
|
|
691
|
+
except ConfigurationNotFoundError:
|
|
692
|
+
return {"key": key, "value": default, "category": None}
|
|
693
|
+
|
|
694
|
+
def count(self, category: Optional[str] = None) -> int:
|
|
695
|
+
"""
|
|
696
|
+
Count total configurations, optionally filtered by category.
|
|
697
|
+
|
|
698
|
+
Args:
|
|
699
|
+
category: Optional category filter
|
|
700
|
+
|
|
701
|
+
Returns:
|
|
702
|
+
Number of configurations
|
|
703
|
+
|
|
704
|
+
Example:
|
|
705
|
+
>>> total = client.count()
|
|
706
|
+
>>> prod_count = client.count(category="production")
|
|
707
|
+
"""
|
|
708
|
+
configs = self.list_all(category=category)
|
|
709
|
+
return len(configs)
|
|
710
|
+
|
|
711
|
+
def list_categories(self) -> List[str]:
|
|
712
|
+
"""
|
|
713
|
+
Get list of all unique categories.
|
|
714
|
+
|
|
715
|
+
Returns:
|
|
716
|
+
List of category names
|
|
717
|
+
|
|
718
|
+
Example:
|
|
719
|
+
>>> categories = client.list_categories()
|
|
720
|
+
>>> print(f"Categories: {', '.join(categories)}")
|
|
721
|
+
"""
|
|
722
|
+
configs = self.list_all()
|
|
723
|
+
categories = set()
|
|
724
|
+
for config in configs:
|
|
725
|
+
cat = config.get("category")
|
|
726
|
+
if cat:
|
|
727
|
+
categories.add(cat)
|
|
728
|
+
return sorted(list(categories))
|
|
729
|
+
|
|
730
|
+
# ========================================================================
|
|
731
|
+
# SESSION MANAGEMENT
|
|
732
|
+
# ========================================================================
|
|
733
|
+
|
|
288
734
|
def close(self):
|
|
289
735
|
"""
|
|
290
736
|
Close the underlying HTTP session.
|
|
291
|
-
|
|
292
737
|
Should be called when the client is no longer needed to free resources.
|
|
293
738
|
|
|
294
739
|
Example:
|
|
295
740
|
>>> client.close()
|
|
296
741
|
"""
|
|
297
742
|
self._session.close()
|
|
743
|
+
self.logger.info("Client session closed")
|
|
298
744
|
|
|
299
745
|
def __enter__(self):
|
|
300
746
|
"""Context manager entry."""
|
|
@@ -304,6 +750,14 @@ class OpenSecureConfClient:
|
|
|
304
750
|
"""Context manager exit - automatically closes session."""
|
|
305
751
|
self.close()
|
|
306
752
|
|
|
753
|
+
def __repr__(self):
|
|
754
|
+
"""String representation of client."""
|
|
755
|
+
return f"OpenSecureConfClient(base_url='{self.base_url}')"
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
# ============================================================================
|
|
759
|
+
# EXPORTS
|
|
760
|
+
# ============================================================================
|
|
307
761
|
|
|
308
762
|
__all__ = [
|
|
309
763
|
"OpenSecureConfClient",
|
|
@@ -311,4 +765,5 @@ __all__ = [
|
|
|
311
765
|
"AuthenticationError",
|
|
312
766
|
"ConfigurationNotFoundError",
|
|
313
767
|
"ConfigurationExistsError",
|
|
768
|
+
"ClusterError",
|
|
314
769
|
]
|