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.
@@ -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
- >>> # Dict value
72
- >>> config = client.create("database", {"host": "localhost", "port": 5432})
73
- >>> # String value
74
- >>> config = client.create("api_token", "secret-token-123")
75
- >>> # Int value
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: Unique configuration key (1-255 characters)
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
- >>> # Dict value
376
- >>> config = client.create("database", {"host": "localhost", "port": 5432}, "prod")
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
- >>> config = client.create("api_token", "secret-token-123", "auth")
391
+ >>> client.create("api_token", "secret-123", "production", "auth")
379
392
  >>> # Integer value
380
- >>> config = client.create("max_retries", 3)
393
+ >>> client.create("max_retries", 3, "production")
381
394
  >>> # Boolean value
382
- >>> config = client.create("debug_enabled", False)
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 = {"key": key, "value": value, "category": category}
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
- >>> config = client.read("database")
414
- >>> print(config["value"]) # Could be any supported type
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
- return self._make_request("GET", f"/configs/{key}")
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 with dict
444
- >>> config = client.update("database", {"host": "db.example.com", "port": 5432})
445
- >>> # Update with string
446
- >>> config = client.update("api_token", "new-token-456")
447
- >>> # Update with int
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
- return self._make_request("PUT", f"/configs/{key}", json=payload)
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
- >>> result = client.delete("database")
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
- return self._make_request("DELETE", f"/configs/{key}")
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 filter.
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
- >>> configs = client.list_all(category="production")
496
- >>> for config in configs:
497
- ... print(f"{config['key']}: {config['value']}")
498
- """
499
- params = {"category": category} if category else {}
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', and optional 'category'
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": "db1", "value": {"host": "localhost"}, "category": "prod"},
529
- ... {"key": "token", "value": "secret-123", "category": "auth"},
530
- ... {"key": "retries", "value": 3, "category": "config"}
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(f"Bulk create: created '{config['key']}'")
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({"key": config["key"], "error": str(e)})
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
- keys: List[str],
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
- keys: List of configuration keys to retrieve
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
- >>> configs = client.bulk_read(["db1", "token", "retries"])
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(keys, list):
587
- raise ValueError("keys must be a list")
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 key in keys:
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(f"Bulk read: key '{key}' not found")
598
- errors.append({"key": key, "error": str(e)})
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(f"Bulk read: failed to read '{key}': {str(e)}")
603
- errors.append({"key": key, "error": str(e)})
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
- keys: List[str],
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
- keys: List of configuration keys to delete
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: {"deleted": [...], "failed": [...]}
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
- >>> result = client.bulk_delete(["temp1", "temp2", "temp3"])
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(keys, list):
629
- raise ValueError("keys must be a list")
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 key in keys:
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(f"Bulk delete: deleted '{key}'")
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(f"Bulk delete: failed to delete '{key}': {str(e)}")
641
- failed.append({"key": key, "error": str(e)})
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("database", {"host": "localhost", "port": 5432})
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 {"key": key, "value": default, "category": None}
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(category="production")
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,,