fraiseql-confiture 0.3.7__cp311-cp311-macosx_11_0_arm64.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.
Files changed (124) hide show
  1. confiture/__init__.py +48 -0
  2. confiture/_core.cpython-311-darwin.so +0 -0
  3. confiture/cli/__init__.py +0 -0
  4. confiture/cli/dry_run.py +116 -0
  5. confiture/cli/lint_formatter.py +193 -0
  6. confiture/cli/main.py +1893 -0
  7. confiture/config/__init__.py +0 -0
  8. confiture/config/environment.py +263 -0
  9. confiture/core/__init__.py +51 -0
  10. confiture/core/anonymization/__init__.py +0 -0
  11. confiture/core/anonymization/audit.py +485 -0
  12. confiture/core/anonymization/benchmarking.py +372 -0
  13. confiture/core/anonymization/breach_notification.py +652 -0
  14. confiture/core/anonymization/compliance.py +617 -0
  15. confiture/core/anonymization/composer.py +298 -0
  16. confiture/core/anonymization/data_subject_rights.py +669 -0
  17. confiture/core/anonymization/factory.py +319 -0
  18. confiture/core/anonymization/governance.py +737 -0
  19. confiture/core/anonymization/performance.py +1092 -0
  20. confiture/core/anonymization/profile.py +284 -0
  21. confiture/core/anonymization/registry.py +195 -0
  22. confiture/core/anonymization/security/kms_manager.py +547 -0
  23. confiture/core/anonymization/security/lineage.py +888 -0
  24. confiture/core/anonymization/security/token_store.py +686 -0
  25. confiture/core/anonymization/strategies/__init__.py +41 -0
  26. confiture/core/anonymization/strategies/address.py +359 -0
  27. confiture/core/anonymization/strategies/credit_card.py +374 -0
  28. confiture/core/anonymization/strategies/custom.py +161 -0
  29. confiture/core/anonymization/strategies/date.py +218 -0
  30. confiture/core/anonymization/strategies/differential_privacy.py +398 -0
  31. confiture/core/anonymization/strategies/email.py +141 -0
  32. confiture/core/anonymization/strategies/format_preserving_encryption.py +310 -0
  33. confiture/core/anonymization/strategies/hash.py +150 -0
  34. confiture/core/anonymization/strategies/ip_address.py +235 -0
  35. confiture/core/anonymization/strategies/masking_retention.py +252 -0
  36. confiture/core/anonymization/strategies/name.py +298 -0
  37. confiture/core/anonymization/strategies/phone.py +119 -0
  38. confiture/core/anonymization/strategies/preserve.py +85 -0
  39. confiture/core/anonymization/strategies/redact.py +101 -0
  40. confiture/core/anonymization/strategies/salted_hashing.py +322 -0
  41. confiture/core/anonymization/strategies/text_redaction.py +183 -0
  42. confiture/core/anonymization/strategies/tokenization.py +334 -0
  43. confiture/core/anonymization/strategy.py +241 -0
  44. confiture/core/anonymization/syncer_audit.py +357 -0
  45. confiture/core/blue_green.py +683 -0
  46. confiture/core/builder.py +500 -0
  47. confiture/core/checksum.py +358 -0
  48. confiture/core/connection.py +184 -0
  49. confiture/core/differ.py +522 -0
  50. confiture/core/drift.py +564 -0
  51. confiture/core/dry_run.py +182 -0
  52. confiture/core/health.py +313 -0
  53. confiture/core/hooks/__init__.py +87 -0
  54. confiture/core/hooks/base.py +232 -0
  55. confiture/core/hooks/context.py +146 -0
  56. confiture/core/hooks/execution_strategies.py +57 -0
  57. confiture/core/hooks/observability.py +220 -0
  58. confiture/core/hooks/phases.py +53 -0
  59. confiture/core/hooks/registry.py +295 -0
  60. confiture/core/large_tables.py +775 -0
  61. confiture/core/linting/__init__.py +70 -0
  62. confiture/core/linting/composer.py +192 -0
  63. confiture/core/linting/libraries/__init__.py +17 -0
  64. confiture/core/linting/libraries/gdpr.py +168 -0
  65. confiture/core/linting/libraries/general.py +184 -0
  66. confiture/core/linting/libraries/hipaa.py +144 -0
  67. confiture/core/linting/libraries/pci_dss.py +104 -0
  68. confiture/core/linting/libraries/sox.py +120 -0
  69. confiture/core/linting/schema_linter.py +491 -0
  70. confiture/core/linting/versioning.py +151 -0
  71. confiture/core/locking.py +389 -0
  72. confiture/core/migration_generator.py +298 -0
  73. confiture/core/migrator.py +882 -0
  74. confiture/core/observability/__init__.py +44 -0
  75. confiture/core/observability/audit.py +323 -0
  76. confiture/core/observability/logging.py +187 -0
  77. confiture/core/observability/metrics.py +174 -0
  78. confiture/core/observability/tracing.py +192 -0
  79. confiture/core/pg_version.py +418 -0
  80. confiture/core/pool.py +406 -0
  81. confiture/core/risk/__init__.py +39 -0
  82. confiture/core/risk/predictor.py +188 -0
  83. confiture/core/risk/scoring.py +248 -0
  84. confiture/core/rollback_generator.py +388 -0
  85. confiture/core/schema_analyzer.py +769 -0
  86. confiture/core/schema_to_schema.py +590 -0
  87. confiture/core/security/__init__.py +32 -0
  88. confiture/core/security/logging.py +201 -0
  89. confiture/core/security/validation.py +416 -0
  90. confiture/core/signals.py +371 -0
  91. confiture/core/syncer.py +540 -0
  92. confiture/exceptions.py +192 -0
  93. confiture/integrations/__init__.py +0 -0
  94. confiture/models/__init__.py +24 -0
  95. confiture/models/lint.py +193 -0
  96. confiture/models/migration.py +265 -0
  97. confiture/models/schema.py +203 -0
  98. confiture/models/sql_file_migration.py +225 -0
  99. confiture/scenarios/__init__.py +36 -0
  100. confiture/scenarios/compliance.py +586 -0
  101. confiture/scenarios/ecommerce.py +199 -0
  102. confiture/scenarios/financial.py +253 -0
  103. confiture/scenarios/healthcare.py +315 -0
  104. confiture/scenarios/multi_tenant.py +340 -0
  105. confiture/scenarios/saas.py +295 -0
  106. confiture/testing/FRAMEWORK_API.md +722 -0
  107. confiture/testing/__init__.py +100 -0
  108. confiture/testing/fixtures/__init__.py +11 -0
  109. confiture/testing/fixtures/data_validator.py +229 -0
  110. confiture/testing/fixtures/migration_runner.py +167 -0
  111. confiture/testing/fixtures/schema_snapshotter.py +352 -0
  112. confiture/testing/frameworks/__init__.py +10 -0
  113. confiture/testing/frameworks/mutation.py +587 -0
  114. confiture/testing/frameworks/performance.py +479 -0
  115. confiture/testing/loader.py +225 -0
  116. confiture/testing/pytest/__init__.py +38 -0
  117. confiture/testing/pytest_plugin.py +190 -0
  118. confiture/testing/sandbox.py +304 -0
  119. confiture/testing/utils/__init__.py +0 -0
  120. fraiseql_confiture-0.3.7.dist-info/METADATA +438 -0
  121. fraiseql_confiture-0.3.7.dist-info/RECORD +124 -0
  122. fraiseql_confiture-0.3.7.dist-info/WHEEL +4 -0
  123. fraiseql_confiture-0.3.7.dist-info/entry_points.txt +4 -0
  124. fraiseql_confiture-0.3.7.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,547 @@
1
+ """KMS (Key Management Service) integration for encryption key management.
2
+
3
+ Provides multi-cloud support for key management:
4
+ - AWS KMS
5
+ - HashiCorp Vault
6
+ - Azure Key Vault
7
+
8
+ Enables secure key storage, rotation, and lifecycle management.
9
+ """
10
+
11
+ import logging
12
+ from abc import ABC, abstractmethod
13
+ from dataclasses import dataclass
14
+ from datetime import UTC, datetime
15
+ from enum import Enum
16
+ from typing import Any
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class KMSProvider(Enum):
22
+ """Supported KMS providers."""
23
+
24
+ AWS = "aws"
25
+ VAULT = "vault"
26
+ AZURE = "azure"
27
+ LOCAL = "local" # For testing only
28
+
29
+
30
+ @dataclass
31
+ class KeyMetadata:
32
+ """Metadata for an encryption key."""
33
+
34
+ key_id: str
35
+ provider: KMSProvider
36
+ algorithm: str
37
+ created_at: datetime
38
+ rotated_at: datetime | None = None
39
+ expires_at: datetime | None = None
40
+ version: int = 1
41
+ is_active: bool = True
42
+
43
+
44
+ class KMSClient(ABC):
45
+ """Abstract base class for KMS clients."""
46
+
47
+ @abstractmethod
48
+ def encrypt(self, plaintext: bytes, key_id: str) -> bytes:
49
+ """Encrypt plaintext using the specified key."""
50
+ pass
51
+
52
+ @abstractmethod
53
+ def decrypt(self, ciphertext: bytes, key_id: str | None = None) -> bytes:
54
+ """Decrypt ciphertext. Key ID can be embedded in ciphertext."""
55
+ pass
56
+
57
+ @abstractmethod
58
+ def rotate_key(self, key_id: str) -> str:
59
+ """Rotate a key and return the new key version."""
60
+ pass
61
+
62
+ @abstractmethod
63
+ def get_key_metadata(self, key_id: str) -> KeyMetadata:
64
+ """Get metadata for a key."""
65
+ pass
66
+
67
+
68
+ class AWSKMSClient(KMSClient):
69
+ """AWS KMS client implementation."""
70
+
71
+ def __init__(self, region: str = "us-east-1"):
72
+ """Initialize AWS KMS client.
73
+
74
+ Args:
75
+ region: AWS region (e.g., 'us-east-1')
76
+ """
77
+ self.region = region
78
+ self.provider = KMSProvider.AWS
79
+
80
+ try:
81
+ import boto3 # type: ignore[import-untyped]
82
+
83
+ self.client = boto3.client("kms", region_name=region)
84
+ except ImportError as e:
85
+ raise ImportError("boto3 is required for AWS KMS support") from e
86
+
87
+ def encrypt(self, plaintext: bytes, key_id: str) -> bytes:
88
+ """Encrypt plaintext using AWS KMS.
89
+
90
+ Args:
91
+ plaintext: Data to encrypt
92
+ key_id: AWS KMS key ID or ARN
93
+
94
+ Returns:
95
+ Encrypted data (ciphertext)
96
+
97
+ Raises:
98
+ Exception: If encryption fails
99
+ """
100
+ try:
101
+ response = self.client.encrypt(
102
+ KeyId=key_id,
103
+ Plaintext=plaintext,
104
+ )
105
+ return response["CiphertextBlob"]
106
+ except Exception as e:
107
+ logger.error(f"AWS KMS encryption failed: {e}")
108
+ raise
109
+
110
+ def decrypt(self, ciphertext: bytes, key_id: str | None = None) -> bytes: # noqa: ARG002
111
+ """Decrypt ciphertext using AWS KMS.
112
+
113
+ Args:
114
+ ciphertext: Encrypted data
115
+ _key_id: Not used (embedded in ciphertext by AWS)
116
+
117
+ Returns:
118
+ Decrypted plaintext
119
+
120
+ Raises:
121
+ Exception: If decryption fails
122
+ """
123
+ try:
124
+ response = self.client.decrypt(CiphertextBlob=ciphertext)
125
+ return response["Plaintext"]
126
+ except Exception as e:
127
+ logger.error(f"AWS KMS decryption failed: {e}")
128
+ raise
129
+
130
+ def rotate_key(self, key_id: str) -> str:
131
+ """Rotate an AWS KMS key.
132
+
133
+ Args:
134
+ key_id: AWS KMS key ID
135
+
136
+ Returns:
137
+ New key version
138
+
139
+ Raises:
140
+ Exception: If rotation fails
141
+ """
142
+ try:
143
+ # AWS KMS automatic key rotation (enable once)
144
+ response = self.client.describe_key(KeyId=key_id)
145
+ key_metadata = response["KeyMetadata"]
146
+
147
+ logger.info(f"Key rotation enabled for {key_id}")
148
+ # AWS handles rotation automatically
149
+ return f"{key_id}:v{key_metadata['KeyUsage']}"
150
+ except Exception as e:
151
+ logger.error(f"AWS KMS key rotation failed: {e}")
152
+ raise
153
+
154
+
155
+ class VaultKMSClient(KMSClient):
156
+ """HashiCorp Vault KMS client implementation."""
157
+
158
+ def __init__(self, vault_url: str, token: str, engine: str = "transit"):
159
+ """Initialize Vault KMS client.
160
+
161
+ Args:
162
+ vault_url: Vault server URL (e.g., 'http://localhost:8200')
163
+ token: Vault authentication token
164
+ engine: Transit engine path (default: 'transit')
165
+ """
166
+ self.vault_url = vault_url
167
+ self.token = token
168
+ self.engine = engine
169
+ self.provider = KMSProvider.VAULT
170
+
171
+ try:
172
+ import hvac # type: ignore[import-untyped]
173
+
174
+ self.client = hvac.Client(url=vault_url, token=token)
175
+ except ImportError as e:
176
+ raise ImportError("hvac is required for HashiCorp Vault support") from e
177
+
178
+ def encrypt(self, plaintext: bytes, key_id: str) -> bytes:
179
+ """Encrypt plaintext using Vault.
180
+
181
+ Args:
182
+ plaintext: Data to encrypt
183
+ key_id: Key name in Vault
184
+
185
+ Returns:
186
+ Encrypted data (ciphertext)
187
+
188
+ Raises:
189
+ Exception: If encryption fails
190
+ """
191
+ try:
192
+ # Vault expects base64 plaintext
193
+ import base64
194
+
195
+ plaintext_b64 = base64.b64encode(plaintext).decode("utf-8")
196
+
197
+ response = self.client.secrets.transit.encrypt_data(
198
+ name=key_id,
199
+ plaintext=plaintext_b64,
200
+ mount_point=self.engine,
201
+ )
202
+ return response["data"]["ciphertext"].encode()
203
+ except Exception as e:
204
+ logger.error(f"Vault encryption failed: {e}")
205
+ raise
206
+
207
+ def decrypt(self, ciphertext: bytes, key_id: str | None = None) -> bytes:
208
+ """Decrypt ciphertext using Vault.
209
+
210
+ Args:
211
+ ciphertext: Encrypted data
212
+ key_id: Key name in Vault (must be provided)
213
+
214
+ Returns:
215
+ Decrypted plaintext
216
+
217
+ Raises:
218
+ ValueError: If key_id not provided
219
+ Exception: If decryption fails
220
+ """
221
+ if not key_id:
222
+ raise ValueError("key_id must be provided for Vault decryption")
223
+
224
+ try:
225
+ import base64
226
+
227
+ ciphertext_str = ciphertext.decode() if isinstance(ciphertext, bytes) else ciphertext
228
+
229
+ response = self.client.secrets.transit.decrypt_data(
230
+ name=key_id,
231
+ ciphertext=ciphertext_str,
232
+ mount_point=self.engine,
233
+ )
234
+ plaintext_b64 = response["data"]["plaintext"]
235
+ return base64.b64decode(plaintext_b64)
236
+ except Exception as e:
237
+ logger.error(f"Vault decryption failed: {e}")
238
+ raise
239
+
240
+ def rotate_key(self, key_id: str) -> str:
241
+ """Rotate a Vault transit key.
242
+
243
+ Args:
244
+ key_id: Key name in Vault
245
+
246
+ Returns:
247
+ New key version
248
+
249
+ Raises:
250
+ Exception: If rotation fails
251
+ """
252
+ try:
253
+ self.client.secrets.transit.rotate_key(
254
+ name=key_id,
255
+ mount_point=self.engine,
256
+ )
257
+
258
+ # Get updated key metadata
259
+ metadata = self.client.secrets.transit.read_key(
260
+ name=key_id,
261
+ mount_point=self.engine,
262
+ )
263
+ new_version = metadata["data"]["latest_version"]
264
+
265
+ logger.info(f"Key {key_id} rotated to version {new_version}")
266
+ return f"{key_id}:v{new_version}"
267
+ except Exception as e:
268
+ logger.error(f"Vault key rotation failed: {e}")
269
+ raise
270
+
271
+
272
+ class AzureKMSClient(KMSClient):
273
+ """Azure Key Vault KMS client implementation."""
274
+
275
+ def __init__(self, vault_url: str, credential: Any):
276
+ """Initialize Azure Key Vault client.
277
+
278
+ Args:
279
+ vault_url: Azure Key Vault URL
280
+ credential: Azure credential object (e.g., DefaultAzureCredential)
281
+ """
282
+ self.vault_url = vault_url
283
+ self.credential = credential
284
+ self.provider = KMSProvider.AZURE
285
+
286
+ try:
287
+ from azure.keyvault.keys.crypto import ( # type: ignore[import-untyped]
288
+ CryptographyClient,
289
+ EncryptionAlgorithm,
290
+ )
291
+
292
+ self.CryptographyClient = CryptographyClient
293
+ self.EncryptionAlgorithm = EncryptionAlgorithm
294
+ except ImportError as e:
295
+ raise ImportError(
296
+ "azure-identity and azure-keyvault-keys are required for Azure support"
297
+ ) from e
298
+
299
+ def encrypt(self, plaintext: bytes, key_id: str) -> bytes:
300
+ """Encrypt plaintext using Azure Key Vault.
301
+
302
+ Args:
303
+ plaintext: Data to encrypt
304
+ key_id: Key name in Key Vault
305
+
306
+ Returns:
307
+ Encrypted data (ciphertext)
308
+
309
+ Raises:
310
+ Exception: If encryption fails
311
+ """
312
+ try:
313
+ key_url = f"{self.vault_url}/keys/{key_id}/latest"
314
+ crypto_client = self.CryptographyClient(key_url, credential=self.credential)
315
+
316
+ result = crypto_client.encrypt(
317
+ self.EncryptionAlgorithm.rsa_oaep,
318
+ plaintext,
319
+ )
320
+ return result.ciphertext
321
+ except Exception as e:
322
+ logger.error(f"Azure Key Vault encryption failed: {e}")
323
+ raise
324
+
325
+ def decrypt(self, ciphertext: bytes, key_id: str | None = None) -> bytes:
326
+ """Decrypt ciphertext using Azure Key Vault.
327
+
328
+ Args:
329
+ ciphertext: Encrypted data
330
+ key_id: Key name in Key Vault (must be provided)
331
+
332
+ Returns:
333
+ Decrypted plaintext
334
+
335
+ Raises:
336
+ ValueError: If key_id not provided
337
+ Exception: If decryption fails
338
+ """
339
+ if not key_id:
340
+ raise ValueError("key_id must be provided for Azure Key Vault decryption")
341
+
342
+ try:
343
+ key_url = f"{self.vault_url}/keys/{key_id}/latest"
344
+ crypto_client = self.CryptographyClient(key_url, credential=self.credential)
345
+
346
+ result = crypto_client.decrypt(
347
+ self.EncryptionAlgorithm.rsa_oaep,
348
+ ciphertext,
349
+ )
350
+ return result.plaintext
351
+ except Exception as e:
352
+ logger.error(f"Azure Key Vault decryption failed: {e}")
353
+ raise
354
+
355
+ def rotate_key(self, key_id: str) -> str:
356
+ """Rotate an Azure Key Vault key version.
357
+
358
+ Args:
359
+ key_id: Key name in Key Vault
360
+
361
+ Returns:
362
+ New key version
363
+
364
+ Raises:
365
+ Exception: If rotation fails
366
+ """
367
+ try:
368
+ from azure.keyvault.keys import KeyClient # type: ignore[import-untyped]
369
+
370
+ key_client = KeyClient(vault_url=self.vault_url, credential=self.credential)
371
+
372
+ # Get current key
373
+ key = key_client.get_key(key_id)
374
+ current_version = key.properties.version
375
+
376
+ logger.info(f"Key {key_id} current version: {current_version}")
377
+ # Note: Azure doesn't have automatic rotation like Vault
378
+ # Manual rotation requires re-keying data
379
+ return f"{key_id}:{current_version}"
380
+ except Exception as e:
381
+ logger.error(f"Azure Key Vault key info failed: {e}")
382
+ raise
383
+
384
+
385
+ class LocalKMSClient(KMSClient):
386
+ """Local KMS client for testing purposes.
387
+
388
+ WARNING: Only for testing. Never use in production.
389
+ """
390
+
391
+ def __init__(self, keys_dir: str | None = None):
392
+ """Initialize local KMS client.
393
+
394
+ Args:
395
+ keys_dir: Directory to store test keys (temporary)
396
+ """
397
+ import os
398
+
399
+ self.provider = KMSProvider.LOCAL
400
+ self.keys = {} # In-memory key storage
401
+ self.keys_dir = keys_dir or "/tmp/confiture_test_keys"
402
+
403
+ if not os.path.exists(self.keys_dir):
404
+ os.makedirs(self.keys_dir)
405
+
406
+ logger.warning("⚠️ LocalKMSClient is for TESTING ONLY. Never use in production.")
407
+
408
+ def encrypt(self, plaintext: bytes, key_id: str) -> bytes:
409
+ """Simple XOR encryption for testing.
410
+
411
+ Args:
412
+ plaintext: Data to encrypt
413
+ key_id: Key identifier
414
+
415
+ Returns:
416
+ Encrypted data
417
+ """
418
+ from cryptography.fernet import Fernet
419
+
420
+ if key_id not in self.keys:
421
+ # Generate key if not exists
422
+ self.keys[key_id] = Fernet.generate_key()
423
+
424
+ f = Fernet(self.keys[key_id])
425
+ return f.encrypt(plaintext)
426
+
427
+ def decrypt(self, ciphertext: bytes, key_id: str | None = None) -> bytes:
428
+ """Decrypt using Fernet.
429
+
430
+ Args:
431
+ ciphertext: Encrypted data
432
+ key_id: Key identifier (optional, can be embedded)
433
+
434
+ Returns:
435
+ Decrypted plaintext
436
+ """
437
+ from cryptography.fernet import Fernet
438
+
439
+ if not key_id:
440
+ # Try all keys
441
+ for _key_id, key in self.keys.items():
442
+ try:
443
+ f = Fernet(key)
444
+ return f.decrypt(ciphertext)
445
+ except Exception:
446
+ continue
447
+ raise ValueError("Could not decrypt with any available key")
448
+
449
+ if key_id not in self.keys:
450
+ raise ValueError(f"Key {key_id} not found")
451
+
452
+ f = Fernet(self.keys[key_id])
453
+ return f.decrypt(ciphertext)
454
+
455
+ def rotate_key(self, key_id: str) -> str:
456
+ """Rotate a test key.
457
+
458
+ Args:
459
+ key_id: Key identifier
460
+
461
+ Returns:
462
+ New version identifier
463
+ """
464
+ from cryptography.fernet import Fernet
465
+
466
+ self.keys[key_id] = Fernet.generate_key()
467
+ version = len([k for k in self.keys if k.startswith(key_id)])
468
+ return f"{key_id}:v{version}"
469
+
470
+ def get_key_metadata(self, key_id: str) -> KeyMetadata:
471
+ """Get metadata for a test key."""
472
+ return KeyMetadata(
473
+ key_id=key_id,
474
+ provider=KMSProvider.LOCAL,
475
+ algorithm="Fernet",
476
+ created_at=datetime.now(UTC),
477
+ version=1,
478
+ is_active=True,
479
+ )
480
+
481
+
482
+ class KMSFactory:
483
+ """Factory for creating KMS clients."""
484
+
485
+ _clients: dict[str, KMSClient] = {}
486
+
487
+ @staticmethod
488
+ def create(
489
+ provider: KMSProvider,
490
+ **config: Any,
491
+ ) -> KMSClient:
492
+ """Create a KMS client.
493
+
494
+ Args:
495
+ provider: KMS provider type
496
+ **config: Provider-specific configuration
497
+
498
+ Returns:
499
+ Configured KMS client
500
+
501
+ Raises:
502
+ ValueError: If provider not supported
503
+ """
504
+ if provider == KMSProvider.AWS:
505
+ return AWSKMSClient(
506
+ region=config.get("region", "us-east-1"),
507
+ )
508
+ elif provider == KMSProvider.VAULT:
509
+ return VaultKMSClient(
510
+ vault_url=config.get("vault_url"),
511
+ token=config.get("token"),
512
+ engine=config.get("engine", "transit"),
513
+ )
514
+ elif provider == KMSProvider.AZURE:
515
+ return AzureKMSClient(
516
+ vault_url=config.get("vault_url"),
517
+ credential=config.get("credential"),
518
+ )
519
+ elif provider == KMSProvider.LOCAL:
520
+ return LocalKMSClient(
521
+ keys_dir=config.get("keys_dir"),
522
+ )
523
+ else:
524
+ raise ValueError(f"Unsupported KMS provider: {provider}")
525
+
526
+ @staticmethod
527
+ def get_or_create(
528
+ provider: KMSProvider,
529
+ key: str | None = None,
530
+ **config: Any,
531
+ ) -> KMSClient:
532
+ """Get or create a cached KMS client.
533
+
534
+ Args:
535
+ provider: KMS provider type
536
+ key: Cache key (optional)
537
+ **config: Provider-specific configuration
538
+
539
+ Returns:
540
+ Configured KMS client (cached if available)
541
+ """
542
+ cache_key = key or str(provider)
543
+
544
+ if cache_key not in KMSFactory._clients:
545
+ KMSFactory._clients[cache_key] = KMSFactory.create(provider, **config)
546
+
547
+ return KMSFactory._clients[cache_key]