opensecureconf-client 2.3.0__py3-none-any.whl → 3.0.0__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.3.0.dist-info → opensecureconf_client-3.0.0.dist-info}/METADATA +386 -207
- opensecureconf_client-3.0.0.dist-info/RECORD +6 -0
- opensecureconf_client.py +236 -93
- opensecureconf_client-2.3.0.dist-info/RECORD +0 -6
- {opensecureconf_client-2.3.0.dist-info → opensecureconf_client-3.0.0.dist-info}/WHEEL +0 -0
- {opensecureconf_client-2.3.0.dist-info → opensecureconf_client-3.0.0.dist-info}/licenses/LICENSE +0 -0
- {opensecureconf_client-2.3.0.dist-info → opensecureconf_client-3.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
opensecureconf_client.py,sha256=pnMDS-UW5pr8xxJS6gaePIdn2vQX6INlLeRHU4PTjTY,34396
|
|
2
|
+
opensecureconf_client-3.0.0.dist-info/licenses/LICENSE,sha256=mvMdzinneV_-L01ddrHOBgbutNS8tjT1m7loT7VTWbI,1073
|
|
3
|
+
opensecureconf_client-3.0.0.dist-info/METADATA,sha256=SPhMNSD9P9BjwNoaH2067MimKgb1K5DBTaiOLyept1Q,42145
|
|
4
|
+
opensecureconf_client-3.0.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
5
|
+
opensecureconf_client-3.0.0.dist-info/top_level.txt,sha256=J7NP3hD92OUdqseJLlbzgPuG_ovqkURRyw7iBJJeDVE,22
|
|
6
|
+
opensecureconf_client-3.0.0.dist-info/RECORD,,
|
opensecureconf_client.py
CHANGED
|
@@ -13,6 +13,8 @@ Enhanced Features:
|
|
|
13
13
|
- Enhanced input validation
|
|
14
14
|
- Health check utilities
|
|
15
15
|
- Support for multiple value types (dict, str, int, bool, list)
|
|
16
|
+
- Multi-environment support (same key in different environments)
|
|
17
|
+
- Environment-based configuration isolation
|
|
16
18
|
"""
|
|
17
19
|
|
|
18
20
|
from typing import Any, Dict, List, Optional, Union
|
|
@@ -23,29 +25,37 @@ from requests.adapters import HTTPAdapter
|
|
|
23
25
|
from requests.exceptions import Timeout, RequestException
|
|
24
26
|
from urllib3.util.retry import Retry
|
|
25
27
|
|
|
28
|
+
|
|
26
29
|
# ============================================================================
|
|
27
30
|
# EXCEPTIONS
|
|
28
31
|
# ============================================================================
|
|
29
32
|
|
|
33
|
+
|
|
30
34
|
class OpenSecureConfError(Exception):
|
|
31
35
|
"""Base exception for OpenSecureConf client errors."""
|
|
32
36
|
|
|
37
|
+
|
|
33
38
|
class AuthenticationError(OpenSecureConfError):
|
|
34
39
|
"""Raised when authentication fails (invalid or missing user key)."""
|
|
35
40
|
|
|
41
|
+
|
|
36
42
|
class ConfigurationNotFoundError(OpenSecureConfError):
|
|
37
43
|
"""Raised when a requested configuration key does not exist."""
|
|
38
44
|
|
|
45
|
+
|
|
39
46
|
class ConfigurationExistsError(OpenSecureConfError):
|
|
40
47
|
"""Raised when attempting to create a configuration that already exists."""
|
|
41
48
|
|
|
49
|
+
|
|
42
50
|
class ClusterError(OpenSecureConfError):
|
|
43
51
|
"""Raised when cluster operations fail."""
|
|
44
52
|
|
|
53
|
+
|
|
45
54
|
# ============================================================================
|
|
46
55
|
# CLIENT
|
|
47
56
|
# ============================================================================
|
|
48
57
|
|
|
58
|
+
|
|
49
59
|
class OpenSecureConfClient:
|
|
50
60
|
"""
|
|
51
61
|
Enhanced client for interacting with the OpenSecureConf API.
|
|
@@ -68,14 +78,11 @@ class OpenSecureConfClient:
|
|
|
68
78
|
... enable_retry=True,
|
|
69
79
|
... log_level="INFO"
|
|
70
80
|
... )
|
|
71
|
-
>>> #
|
|
72
|
-
>>>
|
|
73
|
-
|
|
74
|
-
>>>
|
|
75
|
-
|
|
76
|
-
>>> config = client.create("max_connections", 100)
|
|
77
|
-
>>> # Bool value
|
|
78
|
-
>>> config = client.create("debug_mode", True)
|
|
81
|
+
>>> # Same key in different environments
|
|
82
|
+
>>> prod_config = client.create("database", {"host": "db.prod.com", "port": 5432},
|
|
83
|
+
... "production", "config")
|
|
84
|
+
>>> staging_config = client.create("database", {"host": "db.staging.com", "port": 5432},
|
|
85
|
+
... "staging", "config")
|
|
79
86
|
"""
|
|
80
87
|
|
|
81
88
|
def __init__(
|
|
@@ -350,14 +357,16 @@ class OpenSecureConfClient:
|
|
|
350
357
|
self,
|
|
351
358
|
key: str,
|
|
352
359
|
value: Union[Dict[str, Any], str, int, bool, list],
|
|
360
|
+
environment: str,
|
|
353
361
|
category: Optional[str] = None
|
|
354
362
|
) -> Dict[str, Any]:
|
|
355
363
|
"""
|
|
356
364
|
Create a new encrypted configuration entry.
|
|
357
365
|
|
|
358
366
|
Args:
|
|
359
|
-
key:
|
|
367
|
+
key: Configuration key (1-255 characters)
|
|
360
368
|
value: Configuration data (dict, string, int, bool, or list - will be encrypted)
|
|
369
|
+
environment: Environment identifier (REQUIRED, max 100 characters)
|
|
361
370
|
category: Optional category for grouping (max 100 characters)
|
|
362
371
|
|
|
363
372
|
Returns:
|
|
@@ -365,62 +374,80 @@ class OpenSecureConfClient:
|
|
|
365
374
|
- id: Configuration ID
|
|
366
375
|
- key: Configuration key
|
|
367
376
|
- value: Configuration value (decrypted)
|
|
377
|
+
- environment: Environment identifier
|
|
368
378
|
- category: Configuration category (if set)
|
|
369
379
|
|
|
370
380
|
Raises:
|
|
371
|
-
ConfigurationExistsError: If configuration key already exists
|
|
372
|
-
ValueError: If key is invalid
|
|
381
|
+
ConfigurationExistsError: If configuration (key, environment) already exists
|
|
382
|
+
ValueError: If key or environment is invalid
|
|
373
383
|
|
|
374
384
|
Example:
|
|
375
|
-
>>> #
|
|
376
|
-
>>>
|
|
385
|
+
>>> # Same key in different environments
|
|
386
|
+
>>> prod_config = client.create("database", {"host": "db.prod.com", "port": 5432},
|
|
387
|
+
... "production", "config")
|
|
388
|
+
>>> staging_config = client.create("database", {"host": "db.staging.com", "port": 5432},
|
|
389
|
+
... "staging", "config")
|
|
377
390
|
>>> # String value
|
|
378
|
-
>>>
|
|
391
|
+
>>> client.create("api_token", "secret-123", "production", "auth")
|
|
379
392
|
>>> # Integer value
|
|
380
|
-
>>>
|
|
393
|
+
>>> client.create("max_retries", 3, "production")
|
|
381
394
|
>>> # Boolean value
|
|
382
|
-
>>>
|
|
383
|
-
>>> # List value
|
|
384
|
-
>>> config = client.create("allowed_ips", ["192.168.1.1", "10.0.0.1"])
|
|
395
|
+
>>> client.create("debug", False, "development", "settings")
|
|
385
396
|
"""
|
|
386
397
|
# Enhanced validation
|
|
387
398
|
if not key or not isinstance(key, str):
|
|
388
399
|
raise ValueError("Key must be a non-empty string")
|
|
389
400
|
if len(key) > 255:
|
|
390
401
|
raise ValueError("Key must be between 1 and 255 characters")
|
|
402
|
+
if not environment or not isinstance(environment, str):
|
|
403
|
+
raise ValueError("Environment is required and must be a non-empty string")
|
|
404
|
+
if len(environment) > 100:
|
|
405
|
+
raise ValueError("Environment must be max 100 characters")
|
|
391
406
|
if category and len(category) > 100:
|
|
392
407
|
raise ValueError("Category must be max 100 characters")
|
|
393
408
|
|
|
394
|
-
payload = {
|
|
409
|
+
payload = {
|
|
410
|
+
"key": key,
|
|
411
|
+
"value": value,
|
|
412
|
+
"environment": environment,
|
|
413
|
+
"category": category
|
|
414
|
+
}
|
|
395
415
|
return self._make_request("POST", "/configs", json=payload)
|
|
396
416
|
|
|
397
|
-
def read(self, key: str) -> Dict[str, Any]:
|
|
417
|
+
def read(self, key: str, environment: str) -> Dict[str, Any]:
|
|
398
418
|
"""
|
|
399
|
-
Read and decrypt a configuration entry by key.
|
|
419
|
+
Read and decrypt a configuration entry by key and environment.
|
|
400
420
|
|
|
401
421
|
Args:
|
|
402
422
|
key: Configuration key to retrieve
|
|
423
|
+
environment: Environment identifier (REQUIRED)
|
|
403
424
|
|
|
404
425
|
Returns:
|
|
405
426
|
Dictionary containing the configuration with decrypted value
|
|
406
427
|
The value can be dict, str, int, bool, or list depending on what was stored
|
|
407
428
|
|
|
408
429
|
Raises:
|
|
409
|
-
ConfigurationNotFoundError: If configuration key does not exist
|
|
410
|
-
ValueError: If key is invalid
|
|
430
|
+
ConfigurationNotFoundError: If configuration (key, environment) does not exist
|
|
431
|
+
ValueError: If key or environment is invalid
|
|
411
432
|
|
|
412
433
|
Example:
|
|
413
|
-
>>>
|
|
414
|
-
>>>
|
|
434
|
+
>>> prod_config = client.read("database", "production")
|
|
435
|
+
>>> staging_config = client.read("database", "staging")
|
|
436
|
+
>>> print(prod_config["value"]) # Different from staging
|
|
437
|
+
>>> print(prod_config["environment"]) # "production"
|
|
415
438
|
"""
|
|
416
439
|
if not key or not isinstance(key, str):
|
|
417
440
|
raise ValueError("Key must be a non-empty string")
|
|
441
|
+
if not environment or not isinstance(environment, str):
|
|
442
|
+
raise ValueError("Environment is required and must be a non-empty string")
|
|
418
443
|
|
|
419
|
-
|
|
444
|
+
params = {"environment": environment}
|
|
445
|
+
return self._make_request("GET", f"/configs/{key}", params=params)
|
|
420
446
|
|
|
421
447
|
def update(
|
|
422
448
|
self,
|
|
423
449
|
key: str,
|
|
450
|
+
environment: str,
|
|
424
451
|
value: Union[Dict[str, Any], str, int, bool, list],
|
|
425
452
|
category: Optional[str] = None
|
|
426
453
|
) -> Dict[str, Any]:
|
|
@@ -429,6 +456,7 @@ class OpenSecureConfClient:
|
|
|
429
456
|
|
|
430
457
|
Args:
|
|
431
458
|
key: Configuration key to update
|
|
459
|
+
environment: Environment identifier (REQUIRED, cannot be changed)
|
|
432
460
|
value: New configuration data (dict, string, int, bool, or list - will be encrypted)
|
|
433
461
|
category: Optional new category
|
|
434
462
|
|
|
@@ -436,67 +464,86 @@ class OpenSecureConfClient:
|
|
|
436
464
|
Dictionary containing the updated configuration with decrypted value
|
|
437
465
|
|
|
438
466
|
Raises:
|
|
439
|
-
ConfigurationNotFoundError: If configuration key does not exist
|
|
440
|
-
ValueError: If key is invalid
|
|
467
|
+
ConfigurationNotFoundError: If configuration (key, environment) does not exist
|
|
468
|
+
ValueError: If key or environment is invalid
|
|
441
469
|
|
|
442
470
|
Example:
|
|
443
|
-
>>> # Update
|
|
444
|
-
>>> config = client.update("database",
|
|
445
|
-
|
|
446
|
-
>>>
|
|
447
|
-
>>>
|
|
448
|
-
>>> config = client.update("timeout", 60)
|
|
471
|
+
>>> # Update production config only
|
|
472
|
+
>>> config = client.update("database", "production",
|
|
473
|
+
... {"host": "db-new.prod.com", "port": 5432})
|
|
474
|
+
>>> # Update with string and category
|
|
475
|
+
>>> config = client.update("api_token", "staging", "new-token-456", "auth")
|
|
449
476
|
"""
|
|
450
477
|
if not key or not isinstance(key, str):
|
|
451
478
|
raise ValueError("Key must be a non-empty string")
|
|
479
|
+
if not environment or not isinstance(environment, str):
|
|
480
|
+
raise ValueError("Environment is required and must be a non-empty string")
|
|
452
481
|
if category and len(category) > 100:
|
|
453
482
|
raise ValueError("Category must be max 100 characters")
|
|
454
483
|
|
|
455
484
|
payload = {"value": value, "category": category}
|
|
456
|
-
|
|
485
|
+
params = {"environment": environment}
|
|
486
|
+
return self._make_request("PUT", f"/configs/{key}", json=payload, params=params)
|
|
457
487
|
|
|
458
|
-
def delete(self, key: str) -> Dict[str, str]:
|
|
488
|
+
def delete(self, key: str, environment: str) -> Dict[str, str]:
|
|
459
489
|
"""
|
|
460
|
-
Delete a configuration entry permanently.
|
|
490
|
+
Delete a configuration entry permanently from specific environment.
|
|
461
491
|
|
|
462
492
|
Args:
|
|
463
493
|
key: Configuration key to delete
|
|
494
|
+
environment: Environment identifier (REQUIRED)
|
|
464
495
|
|
|
465
496
|
Returns:
|
|
466
497
|
Dictionary with success message
|
|
467
498
|
|
|
468
499
|
Raises:
|
|
469
|
-
ConfigurationNotFoundError: If configuration key does not exist
|
|
470
|
-
ValueError: If key is invalid
|
|
500
|
+
ConfigurationNotFoundError: If configuration (key, environment) does not exist
|
|
501
|
+
ValueError: If key or environment is invalid
|
|
471
502
|
|
|
472
503
|
Example:
|
|
473
|
-
>>>
|
|
504
|
+
>>> # Delete from staging only
|
|
505
|
+
>>> result = client.delete("database", "staging")
|
|
506
|
+
>>> # Production and development environments remain untouched
|
|
474
507
|
>>> print(result["message"])
|
|
475
508
|
Configuration 'database' deleted successfully
|
|
476
509
|
"""
|
|
477
510
|
if not key or not isinstance(key, str):
|
|
478
511
|
raise ValueError("Key must be a non-empty string")
|
|
512
|
+
if not environment or not isinstance(environment, str):
|
|
513
|
+
raise ValueError("Environment is required and must be a non-empty string")
|
|
479
514
|
|
|
480
|
-
|
|
515
|
+
params = {"environment": environment}
|
|
516
|
+
return self._make_request("DELETE", f"/configs/{key}", params=params)
|
|
481
517
|
|
|
482
|
-
def list_all(self, category: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
518
|
+
def list_all(self, category: Optional[str] = None, environment: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
483
519
|
"""
|
|
484
|
-
List all configurations with optional category
|
|
520
|
+
List all configurations with optional category and environment filters.
|
|
485
521
|
All values are automatically decrypted.
|
|
486
522
|
|
|
487
523
|
Args:
|
|
488
524
|
category: Optional filter by category
|
|
525
|
+
environment: Optional filter by environment
|
|
489
526
|
|
|
490
527
|
Returns:
|
|
491
528
|
List of configuration dictionaries with decrypted values
|
|
492
529
|
Each value can be dict, str, int, bool, or list
|
|
493
530
|
|
|
494
531
|
Example:
|
|
495
|
-
>>>
|
|
496
|
-
>>>
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
532
|
+
>>> # List all
|
|
533
|
+
>>> configs = client.list_all()
|
|
534
|
+
>>> # Filter by environment
|
|
535
|
+
>>> prod_configs = client.list_all(environment="production")
|
|
536
|
+
>>> # Filter by category
|
|
537
|
+
>>> db_configs = client.list_all(category="database")
|
|
538
|
+
>>> # Filter by both
|
|
539
|
+
>>> configs = client.list_all(category="database", environment="production")
|
|
540
|
+
"""
|
|
541
|
+
params = {}
|
|
542
|
+
if category:
|
|
543
|
+
params["category"] = category
|
|
544
|
+
if environment:
|
|
545
|
+
params["environment"] = environment
|
|
546
|
+
|
|
500
547
|
return self._make_request("GET", "/configs", params=params)
|
|
501
548
|
|
|
502
549
|
# ========================================================================
|
|
@@ -512,7 +559,8 @@ class OpenSecureConfClient:
|
|
|
512
559
|
Create multiple configurations in batch.
|
|
513
560
|
|
|
514
561
|
Args:
|
|
515
|
-
configs: List of configuration dictionaries with 'key', 'value',
|
|
562
|
+
configs: List of configuration dictionaries with 'key', 'value',
|
|
563
|
+
'environment' (REQUIRED), and optional 'category'
|
|
516
564
|
Value can be dict, str, int, bool, or list
|
|
517
565
|
ignore_errors: If True, continue on errors and return partial results
|
|
518
566
|
|
|
@@ -520,14 +568,17 @@ class OpenSecureConfClient:
|
|
|
520
568
|
List of created configuration dictionaries
|
|
521
569
|
|
|
522
570
|
Raises:
|
|
523
|
-
ValueError: If configs format is invalid
|
|
571
|
+
ValueError: If configs format is invalid or environment is missing
|
|
524
572
|
OpenSecureConfError: If creation fails and ignore_errors is False
|
|
525
573
|
|
|
526
574
|
Example:
|
|
527
575
|
>>> configs = [
|
|
528
|
-
... {"key": "
|
|
529
|
-
...
|
|
530
|
-
... {"key": "
|
|
576
|
+
... {"key": "db", "value": {"host": "localhost"},
|
|
577
|
+
... "environment": "production", "category": "config"},
|
|
578
|
+
... {"key": "db", "value": {"host": "localhost"},
|
|
579
|
+
... "environment": "staging", "category": "config"},
|
|
580
|
+
... {"key": "token", "value": "secret-123",
|
|
581
|
+
... "environment": "production", "category": "auth"}
|
|
531
582
|
... ]
|
|
532
583
|
>>> results = client.bulk_create(configs)
|
|
533
584
|
>>> print(f"Created {len(results)} configurations")
|
|
@@ -543,19 +594,28 @@ class OpenSecureConfClient:
|
|
|
543
594
|
raise ValueError(f"Config at index {i} must be a dictionary")
|
|
544
595
|
if "key" not in config or "value" not in config:
|
|
545
596
|
raise ValueError(f"Config at index {i} missing required 'key' or 'value'")
|
|
597
|
+
if "environment" not in config:
|
|
598
|
+
raise ValueError(f"Config at index {i} missing required 'environment'")
|
|
546
599
|
|
|
547
600
|
try:
|
|
548
601
|
result = self.create(
|
|
549
602
|
key=config["key"],
|
|
550
603
|
value=config["value"],
|
|
604
|
+
environment=config["environment"],
|
|
551
605
|
category=config.get("category")
|
|
552
606
|
)
|
|
553
607
|
results.append(result)
|
|
554
|
-
self.logger.info(
|
|
608
|
+
self.logger.info(
|
|
609
|
+
f"Bulk create: created '{config['key']}' in '{config['environment']}'"
|
|
610
|
+
)
|
|
555
611
|
except Exception as e:
|
|
556
|
-
error_msg = f"Failed to create '{config['key']}': {str(e)}"
|
|
612
|
+
error_msg = f"Failed to create '{config['key']}' in '{config['environment']}': {str(e)}"
|
|
557
613
|
self.logger.error(error_msg)
|
|
558
|
-
errors.append({
|
|
614
|
+
errors.append({
|
|
615
|
+
"key": config["key"],
|
|
616
|
+
"environment": config["environment"],
|
|
617
|
+
"error": str(e)
|
|
618
|
+
})
|
|
559
619
|
if not ignore_errors:
|
|
560
620
|
raise OpenSecureConfError(error_msg) from e
|
|
561
621
|
|
|
@@ -566,41 +626,66 @@ class OpenSecureConfClient:
|
|
|
566
626
|
|
|
567
627
|
def bulk_read(
|
|
568
628
|
self,
|
|
569
|
-
|
|
629
|
+
items: List[Dict[str, str]],
|
|
570
630
|
ignore_errors: bool = False
|
|
571
631
|
) -> List[Dict[str, Any]]:
|
|
572
632
|
"""
|
|
573
633
|
Read multiple configurations in batch.
|
|
574
634
|
|
|
575
635
|
Args:
|
|
576
|
-
|
|
636
|
+
items: List of dictionaries with 'key' and 'environment' fields
|
|
577
637
|
ignore_errors: If True, skip missing keys and return partial results
|
|
578
638
|
|
|
579
639
|
Returns:
|
|
580
640
|
List of configuration dictionaries
|
|
581
641
|
|
|
642
|
+
Raises:
|
|
643
|
+
ValueError: If items format is invalid
|
|
644
|
+
|
|
582
645
|
Example:
|
|
583
|
-
>>>
|
|
646
|
+
>>> items = [
|
|
647
|
+
... {"key": "database", "environment": "production"},
|
|
648
|
+
... {"key": "database", "environment": "staging"},
|
|
649
|
+
... {"key": "api_token", "environment": "production"}
|
|
650
|
+
... ]
|
|
651
|
+
>>> configs = client.bulk_read(items)
|
|
584
652
|
>>> print(f"Retrieved {len(configs)} configurations")
|
|
585
653
|
"""
|
|
586
|
-
if not isinstance(
|
|
587
|
-
raise ValueError("
|
|
654
|
+
if not isinstance(items, list):
|
|
655
|
+
raise ValueError("items must be a list")
|
|
588
656
|
|
|
589
657
|
results = []
|
|
590
658
|
errors = []
|
|
591
659
|
|
|
592
|
-
for
|
|
660
|
+
for i, item in enumerate(items):
|
|
661
|
+
if not isinstance(item, dict):
|
|
662
|
+
raise ValueError(f"Item at index {i} must be a dictionary")
|
|
663
|
+
if "key" not in item or "environment" not in item:
|
|
664
|
+
raise ValueError(f"Item at index {i} missing required 'key' or 'environment'")
|
|
665
|
+
|
|
593
666
|
try:
|
|
594
|
-
result = self.read(key)
|
|
667
|
+
result = self.read(item["key"], item["environment"])
|
|
595
668
|
results.append(result)
|
|
596
669
|
except ConfigurationNotFoundError as e:
|
|
597
|
-
self.logger.warning(
|
|
598
|
-
|
|
670
|
+
self.logger.warning(
|
|
671
|
+
f"Bulk read: key '{item['key']}' not found in '{item['environment']}'"
|
|
672
|
+
)
|
|
673
|
+
errors.append({
|
|
674
|
+
"key": item["key"],
|
|
675
|
+
"environment": item["environment"],
|
|
676
|
+
"error": str(e)
|
|
677
|
+
})
|
|
599
678
|
if not ignore_errors:
|
|
600
679
|
raise
|
|
601
680
|
except Exception as e:
|
|
602
|
-
self.logger.error(
|
|
603
|
-
|
|
681
|
+
self.logger.error(
|
|
682
|
+
f"Bulk read: failed to read '{item['key']}' from '{item['environment']}': {str(e)}"
|
|
683
|
+
)
|
|
684
|
+
errors.append({
|
|
685
|
+
"key": item["key"],
|
|
686
|
+
"environment": item["environment"],
|
|
687
|
+
"error": str(e)
|
|
688
|
+
})
|
|
604
689
|
if not ignore_errors:
|
|
605
690
|
raise
|
|
606
691
|
|
|
@@ -608,37 +693,61 @@ class OpenSecureConfClient:
|
|
|
608
693
|
|
|
609
694
|
def bulk_delete(
|
|
610
695
|
self,
|
|
611
|
-
|
|
696
|
+
items: List[Dict[str, str]],
|
|
612
697
|
ignore_errors: bool = False
|
|
613
698
|
) -> Dict[str, Any]:
|
|
614
699
|
"""
|
|
615
700
|
Delete multiple configurations in batch.
|
|
616
701
|
|
|
617
702
|
Args:
|
|
618
|
-
|
|
703
|
+
items: List of dictionaries with 'key' and 'environment' fields
|
|
619
704
|
ignore_errors: If True, continue on errors
|
|
620
705
|
|
|
621
706
|
Returns:
|
|
622
|
-
Dictionary with summary: {
|
|
707
|
+
Dictionary with summary: {
|
|
708
|
+
"deleted": [{"key": "...", "environment": "..."}],
|
|
709
|
+
"failed": [{"key": "...", "environment": "...", "error": "..."}]
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
Raises:
|
|
713
|
+
ValueError: If items format is invalid
|
|
623
714
|
|
|
624
715
|
Example:
|
|
625
|
-
>>>
|
|
716
|
+
>>> items = [
|
|
717
|
+
... {"key": "temp1", "environment": "staging"},
|
|
718
|
+
... {"key": "temp2", "environment": "staging"},
|
|
719
|
+
... {"key": "temp3", "environment": "development"}
|
|
720
|
+
... ]
|
|
721
|
+
>>> result = client.bulk_delete(items)
|
|
626
722
|
>>> print(f"Deleted: {len(result['deleted'])}, Failed: {len(result['failed'])}")
|
|
627
723
|
"""
|
|
628
|
-
if not isinstance(
|
|
629
|
-
raise ValueError("
|
|
724
|
+
if not isinstance(items, list):
|
|
725
|
+
raise ValueError("items must be a list")
|
|
630
726
|
|
|
631
727
|
deleted = []
|
|
632
728
|
failed = []
|
|
633
729
|
|
|
634
|
-
for
|
|
730
|
+
for i, item in enumerate(items):
|
|
731
|
+
if not isinstance(item, dict):
|
|
732
|
+
raise ValueError(f"Item at index {i} must be a dictionary")
|
|
733
|
+
if "key" not in item or "environment" not in item:
|
|
734
|
+
raise ValueError(f"Item at index {i} missing required 'key' or 'environment'")
|
|
735
|
+
|
|
635
736
|
try:
|
|
636
|
-
self.delete(key)
|
|
637
|
-
deleted.append(key)
|
|
638
|
-
self.logger.info(
|
|
737
|
+
self.delete(item["key"], item["environment"])
|
|
738
|
+
deleted.append({"key": item["key"], "environment": item["environment"]})
|
|
739
|
+
self.logger.info(
|
|
740
|
+
f"Bulk delete: deleted '{item['key']}' from '{item['environment']}'"
|
|
741
|
+
)
|
|
639
742
|
except Exception as e:
|
|
640
|
-
self.logger.error(
|
|
641
|
-
|
|
743
|
+
self.logger.error(
|
|
744
|
+
f"Bulk delete: failed to delete '{item['key']}' from '{item['environment']}': {str(e)}"
|
|
745
|
+
)
|
|
746
|
+
failed.append({
|
|
747
|
+
"key": item["key"],
|
|
748
|
+
"environment": item["environment"],
|
|
749
|
+
"error": str(e)
|
|
750
|
+
})
|
|
642
751
|
if not ignore_errors:
|
|
643
752
|
raise
|
|
644
753
|
|
|
@@ -648,22 +757,25 @@ class OpenSecureConfClient:
|
|
|
648
757
|
# UTILITY METHODS
|
|
649
758
|
# ========================================================================
|
|
650
759
|
|
|
651
|
-
def exists(self, key: str) -> bool:
|
|
760
|
+
def exists(self, key: str, environment: str) -> bool:
|
|
652
761
|
"""
|
|
653
|
-
Check if a configuration key exists.
|
|
762
|
+
Check if a configuration key exists in specific environment.
|
|
654
763
|
|
|
655
764
|
Args:
|
|
656
765
|
key: Configuration key to check
|
|
766
|
+
environment: Environment identifier (REQUIRED)
|
|
657
767
|
|
|
658
768
|
Returns:
|
|
659
|
-
True if key exists, False otherwise
|
|
769
|
+
True if key exists in the specified environment, False otherwise
|
|
660
770
|
|
|
661
771
|
Example:
|
|
662
|
-
>>> if client.exists("database"):
|
|
663
|
-
... print("Configuration exists")
|
|
772
|
+
>>> if client.exists("database", "production"):
|
|
773
|
+
... print("Configuration exists in production")
|
|
774
|
+
>>> if not client.exists("database", "development"):
|
|
775
|
+
... print("Configuration does not exist in development")
|
|
664
776
|
"""
|
|
665
777
|
try:
|
|
666
|
-
self.read(key)
|
|
778
|
+
self.read(key, environment)
|
|
667
779
|
return True
|
|
668
780
|
except ConfigurationNotFoundError:
|
|
669
781
|
return False
|
|
@@ -671,6 +783,7 @@ class OpenSecureConfClient:
|
|
|
671
783
|
def get_or_default(
|
|
672
784
|
self,
|
|
673
785
|
key: str,
|
|
786
|
+
environment: str,
|
|
674
787
|
default: Union[Dict[str, Any], str, int, bool, list]
|
|
675
788
|
) -> Dict[str, Any]:
|
|
676
789
|
"""
|
|
@@ -678,6 +791,7 @@ class OpenSecureConfClient:
|
|
|
678
791
|
|
|
679
792
|
Args:
|
|
680
793
|
key: Configuration key to retrieve
|
|
794
|
+
environment: Environment identifier (REQUIRED)
|
|
681
795
|
default: Default value to return if key not found (any supported type)
|
|
682
796
|
|
|
683
797
|
Returns:
|
|
@@ -685,30 +799,39 @@ class OpenSecureConfClient:
|
|
|
685
799
|
|
|
686
800
|
Example:
|
|
687
801
|
>>> # Dict default
|
|
688
|
-
>>> config = client.get_or_default(
|
|
802
|
+
>>> config = client.get_or_default(
|
|
803
|
+
... "database", "production", {"host": "localhost", "port": 5432}
|
|
804
|
+
... )
|
|
689
805
|
>>> # String default
|
|
690
|
-
>>> config = client.get_or_default("token", "default-token")
|
|
806
|
+
>>> config = client.get_or_default("token", "staging", "default-token")
|
|
691
807
|
"""
|
|
692
808
|
try:
|
|
693
|
-
return self.read(key)
|
|
809
|
+
return self.read(key, environment)
|
|
694
810
|
except ConfigurationNotFoundError:
|
|
695
|
-
return {
|
|
811
|
+
return {
|
|
812
|
+
"key": key,
|
|
813
|
+
"value": default,
|
|
814
|
+
"environment": environment,
|
|
815
|
+
"category": None
|
|
816
|
+
}
|
|
696
817
|
|
|
697
|
-
def count(self, category: Optional[str] = None) -> int:
|
|
818
|
+
def count(self, category: Optional[str] = None, environment: Optional[str] = None) -> int:
|
|
698
819
|
"""
|
|
699
|
-
Count total configurations, optionally filtered by category.
|
|
820
|
+
Count total configurations, optionally filtered by category and/or environment.
|
|
700
821
|
|
|
701
822
|
Args:
|
|
702
823
|
category: Optional category filter
|
|
824
|
+
environment: Optional environment filter
|
|
703
825
|
|
|
704
826
|
Returns:
|
|
705
827
|
Number of configurations
|
|
706
828
|
|
|
707
829
|
Example:
|
|
708
830
|
>>> total = client.count()
|
|
709
|
-
>>> prod_count = client.count(
|
|
831
|
+
>>> prod_count = client.count(environment="production")
|
|
832
|
+
>>> db_prod_count = client.count(category="database", environment="production")
|
|
710
833
|
"""
|
|
711
|
-
configs = self.list_all(category=category)
|
|
834
|
+
configs = self.list_all(category=category, environment=environment)
|
|
712
835
|
return len(configs)
|
|
713
836
|
|
|
714
837
|
def list_categories(self) -> List[str]:
|
|
@@ -730,6 +853,26 @@ class OpenSecureConfClient:
|
|
|
730
853
|
categories.add(cat)
|
|
731
854
|
return sorted(list(categories))
|
|
732
855
|
|
|
856
|
+
def list_environments(self) -> List[str]:
|
|
857
|
+
"""
|
|
858
|
+
Get list of all unique environments.
|
|
859
|
+
|
|
860
|
+
Returns:
|
|
861
|
+
List of environment names
|
|
862
|
+
|
|
863
|
+
Example:
|
|
864
|
+
>>> environments = client.list_environments()
|
|
865
|
+
>>> print(f"Environments: {', '.join(environments)}")
|
|
866
|
+
Environments: development, production, staging
|
|
867
|
+
"""
|
|
868
|
+
configs = self.list_all()
|
|
869
|
+
environments = set()
|
|
870
|
+
for config in configs:
|
|
871
|
+
env = config.get("environment")
|
|
872
|
+
if env:
|
|
873
|
+
environments.add(env)
|
|
874
|
+
return sorted(list(environments))
|
|
875
|
+
|
|
733
876
|
# ========================================================================
|
|
734
877
|
# SESSION MANAGEMENT
|
|
735
878
|
# ========================================================================
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
opensecureconf_client.py,sha256=9BfhPk3C7hFUy0wEB8P2-Xn69Zfrr8v2fhQmk4nEwqQ,27630
|
|
2
|
-
opensecureconf_client-2.3.0.dist-info/licenses/LICENSE,sha256=mvMdzinneV_-L01ddrHOBgbutNS8tjT1m7loT7VTWbI,1073
|
|
3
|
-
opensecureconf_client-2.3.0.dist-info/METADATA,sha256=d5bjvHAT8Vb6VQDDRTpjmOSDRcMQNy26U4eInosp0HY,33865
|
|
4
|
-
opensecureconf_client-2.3.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
5
|
-
opensecureconf_client-2.3.0.dist-info/top_level.txt,sha256=J7NP3hD92OUdqseJLlbzgPuG_ovqkURRyw7iBJJeDVE,22
|
|
6
|
-
opensecureconf_client-2.3.0.dist-info/RECORD,,
|
|
File without changes
|
{opensecureconf_client-2.3.0.dist-info → opensecureconf_client-3.0.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{opensecureconf_client-2.3.0.dist-info → opensecureconf_client-3.0.0.dist-info}/top_level.txt
RENAMED
|
File without changes
|