hippius 0.1.6__py3-none-any.whl → 0.1.7__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.
hippius_sdk/config.py ADDED
@@ -0,0 +1,744 @@
1
+ """
2
+ Configuration management for Hippius SDK.
3
+
4
+ This module handles loading and saving configuration from the user's home directory,
5
+ specifically in ~/.hippius/config.
6
+ """
7
+
8
+ import os
9
+ import json
10
+ import base64
11
+ import hashlib
12
+ import getpass
13
+ from pathlib import Path
14
+ from typing import Dict, Any, Optional, List, Union, Tuple
15
+
16
+ # Define constants
17
+ CONFIG_DIR = os.path.expanduser("~/.hippius")
18
+ CONFIG_FILE = os.path.join(CONFIG_DIR, "config.json")
19
+ DEFAULT_CONFIG = {
20
+ "ipfs": {
21
+ "gateway": "https://ipfs.io",
22
+ "api_url": "https://relay-fr.hippius.network",
23
+ "local_ipfs": False,
24
+ },
25
+ "substrate": {
26
+ "url": "wss://rpc.hippius.network",
27
+ "seed_phrase": None,
28
+ "seed_phrase_encoded": False,
29
+ "seed_phrase_salt": None, # Salt for password-based encryption
30
+ "default_miners": [],
31
+ "active_account": None, # Name of the active account
32
+ "accounts": {}, # Dictionary of accounts with names as keys
33
+ },
34
+ "encryption": {
35
+ "encrypt_by_default": False,
36
+ "encryption_key": None,
37
+ },
38
+ "erasure_coding": {
39
+ "default_k": 3,
40
+ "default_m": 5,
41
+ "default_chunk_size": 1024 * 1024, # 1MB
42
+ },
43
+ "cli": {
44
+ "verbose": False,
45
+ "max_retries": 3,
46
+ },
47
+ }
48
+
49
+
50
+ def ensure_config_dir() -> None:
51
+ """Create configuration directory if it doesn't exist."""
52
+ if not os.path.exists(CONFIG_DIR):
53
+ try:
54
+ os.makedirs(CONFIG_DIR, exist_ok=True)
55
+ print(f"Created Hippius configuration directory: {CONFIG_DIR}")
56
+ except Exception as e:
57
+ print(f"Warning: Could not create configuration directory: {e}")
58
+
59
+
60
+ def load_config() -> Dict[str, Any]:
61
+ """
62
+ Load configuration from the config file.
63
+
64
+ If the file doesn't exist, create it with default values.
65
+
66
+ Returns:
67
+ Dict[str, Any]: The configuration dictionary
68
+ """
69
+ ensure_config_dir()
70
+
71
+ if not os.path.exists(CONFIG_FILE):
72
+ save_config(DEFAULT_CONFIG)
73
+ return DEFAULT_CONFIG.copy()
74
+
75
+ try:
76
+ with open(CONFIG_FILE, "r") as f:
77
+ config = json.load(f)
78
+
79
+ # Ensure all config sections exist (for backward compatibility)
80
+ for section, defaults in DEFAULT_CONFIG.items():
81
+ if section not in config:
82
+ config[section] = defaults
83
+
84
+ return config
85
+ except Exception as e:
86
+ print(f"Warning: Could not load configuration file: {e}")
87
+ print(f"Using default configuration")
88
+ return DEFAULT_CONFIG.copy()
89
+
90
+
91
+ def save_config(config: Dict[str, Any]) -> bool:
92
+ """
93
+ Save configuration to the config file.
94
+
95
+ Args:
96
+ config: The configuration dictionary to save
97
+
98
+ Returns:
99
+ bool: True if save was successful, False otherwise
100
+ """
101
+ ensure_config_dir()
102
+
103
+ try:
104
+ with open(CONFIG_FILE, "w") as f:
105
+ json.dump(config, f, indent=2)
106
+ return True
107
+ except Exception as e:
108
+ print(f"Warning: Could not save configuration file: {e}")
109
+ return False
110
+
111
+
112
+ def get_config_value(section: str, key: str, default: Any = None) -> Any:
113
+ """
114
+ Get a configuration value from a specific section.
115
+
116
+ Args:
117
+ section: The configuration section
118
+ key: The configuration key
119
+ default: Default value if not found
120
+
121
+ Returns:
122
+ Any: The configuration value or default
123
+ """
124
+ config = load_config()
125
+ return config.get(section, {}).get(key, default)
126
+
127
+
128
+ def set_config_value(section: str, key: str, value: Any) -> bool:
129
+ """
130
+ Set a configuration value in a specific section.
131
+
132
+ Args:
133
+ section: The configuration section
134
+ key: The configuration key
135
+ value: The value to set
136
+
137
+ Returns:
138
+ bool: True if save was successful, False otherwise
139
+ """
140
+ config = load_config()
141
+
142
+ if section not in config:
143
+ config[section] = {}
144
+
145
+ config[section][key] = value
146
+ return save_config(config)
147
+
148
+
149
+ def get_encryption_key() -> Optional[bytes]:
150
+ """
151
+ Get the encryption key from the configuration.
152
+
153
+ Returns:
154
+ Optional[bytes]: The encryption key or None if not set
155
+ """
156
+ key_str = get_config_value("encryption", "encryption_key")
157
+ if not key_str:
158
+ return None
159
+
160
+ try:
161
+ return base64.b64decode(key_str)
162
+ except Exception as e:
163
+ print(f"Warning: Could not decode encryption key from config: {e}")
164
+ return None
165
+
166
+
167
+ def set_encryption_key(key: Union[bytes, str]) -> bool:
168
+ """
169
+ Set the encryption key in the configuration.
170
+
171
+ Args:
172
+ key: The encryption key (bytes or base64-encoded string)
173
+
174
+ Returns:
175
+ bool: True if save was successful, False otherwise
176
+ """
177
+ # Convert bytes to base64 string if needed
178
+ if isinstance(key, bytes):
179
+ key = base64.b64encode(key).decode()
180
+
181
+ return set_config_value("encryption", "encryption_key", key)
182
+
183
+
184
+ def _derive_key_from_password(
185
+ password: str, salt: Optional[bytes] = None
186
+ ) -> Tuple[bytes, bytes]:
187
+ """
188
+ Derive an encryption key from a password using PBKDF2.
189
+
190
+ Args:
191
+ password: The user password
192
+ salt: Optional salt bytes. If None, a new random salt is generated
193
+
194
+ Returns:
195
+ Tuple[bytes, bytes]: (derived_key, salt)
196
+ """
197
+ # Import cryptography for PBKDF2
198
+ try:
199
+ from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
200
+ from cryptography.hazmat.primitives import hashes
201
+ import os
202
+ except ImportError:
203
+ raise ImportError(
204
+ "cryptography is required for password-based encryption. Install it with: pip install cryptography"
205
+ )
206
+
207
+ # Generate a salt if not provided
208
+ if salt is None:
209
+ salt = os.urandom(16)
210
+
211
+ # Create a PBKDF2HMAC instance
212
+ kdf = PBKDF2HMAC(
213
+ algorithm=hashes.SHA256(),
214
+ length=32, # 32 bytes (256 bits) key
215
+ salt=salt,
216
+ iterations=100000, # Recommended minimum by NIST
217
+ )
218
+
219
+ # Derive the key
220
+ key = kdf.derive(password.encode("utf-8"))
221
+
222
+ return key, salt
223
+
224
+
225
+ def encrypt_with_password(data: str, password: str) -> Tuple[str, str]:
226
+ """
227
+ Encrypt data using a password-derived key.
228
+
229
+ Args:
230
+ data: String data to encrypt
231
+ password: User password
232
+
233
+ Returns:
234
+ Tuple[str, str]: (base64_encrypted_data, base64_salt)
235
+ """
236
+ try:
237
+ # Derive key from password with a new salt
238
+ key, salt = _derive_key_from_password(password)
239
+
240
+ # Import NaCl for encryption
241
+ try:
242
+ import nacl.secret
243
+ import nacl.utils
244
+ except ImportError:
245
+ raise ValueError(
246
+ "PyNaCl is required for encryption. Install it with: pip install pynacl"
247
+ )
248
+
249
+ # Create a SecretBox with our derived key
250
+ box = nacl.secret.SecretBox(key)
251
+
252
+ # Encrypt the data
253
+ encrypted_data = box.encrypt(data.encode("utf-8"))
254
+
255
+ # Convert to base64 for storage
256
+ encoded_data = base64.b64encode(encrypted_data).decode("utf-8")
257
+ encoded_salt = base64.b64encode(salt).decode("utf-8")
258
+
259
+ return encoded_data, encoded_salt
260
+
261
+ except Exception as e:
262
+ raise ValueError(f"Error encrypting data with password: {e}")
263
+
264
+
265
+ def decrypt_with_password(encrypted_data: str, salt: str, password: str) -> str:
266
+ """
267
+ Decrypt data using a password-derived key.
268
+
269
+ Args:
270
+ encrypted_data: Base64-encoded encrypted data
271
+ salt: Base64-encoded salt
272
+ password: User password
273
+
274
+ Returns:
275
+ str: Decrypted data
276
+ """
277
+ try:
278
+ # Decode the encrypted data and salt
279
+ encrypted_bytes = base64.b64decode(encrypted_data)
280
+ salt_bytes = base64.b64decode(salt)
281
+
282
+ # Derive the key from the password and salt
283
+ key, _ = _derive_key_from_password(password, salt_bytes)
284
+
285
+ # Import NaCl for decryption
286
+ try:
287
+ import nacl.secret
288
+ import nacl.utils
289
+ except ImportError:
290
+ raise ValueError(
291
+ "PyNaCl is required for decryption. Install it with: pip install pynacl"
292
+ )
293
+
294
+ # Create a SecretBox with our derived key
295
+ box = nacl.secret.SecretBox(key)
296
+
297
+ # Decrypt the data
298
+ decrypted_data = box.decrypt(encrypted_bytes)
299
+
300
+ # Return the decrypted string
301
+ return decrypted_data.decode("utf-8")
302
+
303
+ except Exception as e:
304
+ raise ValueError(f"Error decrypting data with password: {e}")
305
+
306
+
307
+ def encrypt_seed_phrase(
308
+ seed_phrase: str, password: Optional[str] = None, account_name: Optional[str] = None
309
+ ) -> bool:
310
+ """
311
+ Encrypt the substrate seed phrase using password-based encryption.
312
+
313
+ Args:
314
+ seed_phrase: The plain text seed phrase to encrypt
315
+ password: Optional password (if None, will prompt)
316
+ account_name: Optional name for the account (if None, uses legacy mode or active account)
317
+
318
+ Returns:
319
+ bool: True if encryption and saving was successful, False otherwise
320
+ """
321
+ try:
322
+ # Get password from user if not provided
323
+ if password is None:
324
+ password = getpass.getpass("Enter password to encrypt seed phrase: ")
325
+ password_confirm = getpass.getpass("Confirm password: ")
326
+
327
+ if password != password_confirm:
328
+ raise ValueError("Passwords do not match")
329
+
330
+ # Encrypt the seed phrase
331
+ encrypted_data, salt = encrypt_with_password(seed_phrase, password)
332
+
333
+ # Get the SS58 address from the seed phrase
334
+ ss58_address = None
335
+ try:
336
+ from substrateinterface import Keypair
337
+
338
+ keypair = Keypair.create_from_mnemonic(seed_phrase)
339
+ ss58_address = keypair.ss58_address
340
+ except Exception as e:
341
+ print(f"Warning: Could not derive SS58 address: {e}")
342
+
343
+ config = load_config()
344
+
345
+ # Check if we're using the new multi-account system
346
+ if account_name is not None:
347
+ # Multi-account mode
348
+ if "accounts" not in config["substrate"]:
349
+ config["substrate"]["accounts"] = {}
350
+
351
+ # Store the account data
352
+ config["substrate"]["accounts"][account_name] = {
353
+ "seed_phrase": encrypted_data,
354
+ "seed_phrase_encoded": True,
355
+ "seed_phrase_salt": salt,
356
+ "ss58_address": ss58_address,
357
+ }
358
+
359
+ # Set as active account if no active account exists
360
+ if not config["substrate"].get("active_account"):
361
+ config["substrate"]["active_account"] = account_name
362
+
363
+ else:
364
+ # Legacy mode - single account
365
+ config["substrate"]["seed_phrase"] = encrypted_data
366
+ config["substrate"]["seed_phrase_encoded"] = True
367
+ config["substrate"]["seed_phrase_salt"] = salt
368
+ config["substrate"]["ss58_address"] = ss58_address
369
+
370
+ return save_config(config)
371
+
372
+ except Exception as e:
373
+ print(f"Error encrypting seed phrase: {e}")
374
+ return False
375
+
376
+
377
+ def decrypt_seed_phrase(
378
+ password: Optional[str] = None, account_name: Optional[str] = None
379
+ ) -> Optional[str]:
380
+ """
381
+ Decrypt the substrate seed phrase using password-based decryption.
382
+
383
+ Args:
384
+ password: Optional password (if None, will prompt)
385
+ account_name: Optional account name (if None, uses active account or legacy mode)
386
+
387
+ Returns:
388
+ Optional[str]: The decrypted seed phrase, or None if decryption failed
389
+ """
390
+ try:
391
+ config = load_config()
392
+
393
+ # Determine if we're using multi-account mode
394
+ if account_name is not None or config["substrate"].get("active_account"):
395
+ # Multi-account mode
396
+ name_to_use = account_name or config["substrate"].get("active_account")
397
+
398
+ if not name_to_use:
399
+ print("Error: No account specified and no active account")
400
+ return None
401
+
402
+ if name_to_use not in config["substrate"].get("accounts", {}):
403
+ print(f"Error: Account '{name_to_use}' not found")
404
+ return None
405
+
406
+ account_data = config["substrate"]["accounts"][name_to_use]
407
+ is_encoded = account_data.get("seed_phrase_encoded", False)
408
+
409
+ if not is_encoded:
410
+ return account_data.get("seed_phrase")
411
+
412
+ encrypted_data = account_data.get("seed_phrase")
413
+ salt = account_data.get("seed_phrase_salt")
414
+
415
+ else:
416
+ # Legacy mode - single account
417
+ is_encoded = config["substrate"].get("seed_phrase_encoded", False)
418
+
419
+ if not is_encoded:
420
+ return config["substrate"].get("seed_phrase")
421
+
422
+ encrypted_data = config["substrate"].get("seed_phrase")
423
+ salt = config["substrate"].get("seed_phrase_salt")
424
+
425
+ if not encrypted_data or not salt:
426
+ print("Error: No encrypted seed phrase found or missing salt")
427
+ return None
428
+
429
+ # Get password from user if not provided
430
+ if password is None:
431
+ password = getpass.getpass("Enter password to decrypt seed phrase: ")
432
+
433
+ # Decrypt the seed phrase
434
+ return decrypt_with_password(encrypted_data, salt, password)
435
+
436
+ except Exception as e:
437
+ print(f"Error decrypting seed phrase: {e}")
438
+ return None
439
+
440
+
441
+ def get_seed_phrase(
442
+ password: Optional[str] = None, account_name: Optional[str] = None
443
+ ) -> Optional[str]:
444
+ """
445
+ Get the substrate seed phrase from configuration, decrypting if necessary.
446
+
447
+ Args:
448
+ password: Optional password for decryption (if None and needed, will prompt)
449
+ account_name: Optional account name (if None, uses active account or legacy mode)
450
+
451
+ Returns:
452
+ Optional[str]: The seed phrase, or None if not available
453
+ """
454
+ config = load_config()
455
+
456
+ # Determine if we're using multi-account mode
457
+ if account_name is not None or config["substrate"].get("active_account"):
458
+ # Multi-account mode
459
+ name_to_use = account_name or config["substrate"].get("active_account")
460
+
461
+ if not name_to_use:
462
+ print("Error: No account specified and no active account")
463
+ return None
464
+
465
+ if name_to_use not in config["substrate"].get("accounts", {}):
466
+ print(f"Error: Account '{name_to_use}' not found")
467
+ return None
468
+
469
+ account_data = config["substrate"]["accounts"][name_to_use]
470
+ is_encoded = account_data.get("seed_phrase_encoded", False)
471
+
472
+ else:
473
+ # Legacy mode - single account
474
+ is_encoded = config["substrate"].get("seed_phrase_encoded", False)
475
+
476
+ if is_encoded:
477
+ # If encoded, decrypt it
478
+ return decrypt_seed_phrase(password, account_name)
479
+ else:
480
+ # If not encoded, just return the plain text seed phrase
481
+ if account_name is not None or config["substrate"].get("active_account"):
482
+ # Multi-account mode
483
+ name_to_use = account_name or config["substrate"].get("active_account")
484
+ return config["substrate"]["accounts"][name_to_use].get("seed_phrase")
485
+ else:
486
+ # Legacy mode
487
+ return config["substrate"].get("seed_phrase")
488
+
489
+
490
+ def set_seed_phrase(
491
+ seed_phrase: str,
492
+ encode: bool = False,
493
+ password: Optional[str] = None,
494
+ account_name: Optional[str] = None,
495
+ ) -> bool:
496
+ """
497
+ Set the substrate seed phrase in configuration, with optional encryption.
498
+
499
+ Args:
500
+ seed_phrase: The seed phrase to store
501
+ encode: Whether to encrypt the seed phrase (requires password)
502
+ password: Optional password for encryption (if None and encode=True, will prompt)
503
+ account_name: Optional name for the account (if None, uses legacy mode or active account)
504
+
505
+ Returns:
506
+ bool: True if saving was successful, False otherwise
507
+ """
508
+ if encode:
509
+ return encrypt_seed_phrase(seed_phrase, password, account_name)
510
+ else:
511
+ config = load_config()
512
+
513
+ # Get the SS58 address from the seed phrase
514
+ ss58_address = None
515
+ try:
516
+ from substrateinterface import Keypair
517
+
518
+ keypair = Keypair.create_from_mnemonic(seed_phrase)
519
+ ss58_address = keypair.ss58_address
520
+ except Exception as e:
521
+ print(f"Warning: Could not derive SS58 address: {e}")
522
+
523
+ # Determine if we're using multi-account mode
524
+ if account_name is not None:
525
+ # Multi-account mode
526
+ if "accounts" not in config["substrate"]:
527
+ config["substrate"]["accounts"] = {}
528
+
529
+ # Store the account data
530
+ config["substrate"]["accounts"][account_name] = {
531
+ "seed_phrase": seed_phrase,
532
+ "seed_phrase_encoded": False,
533
+ "seed_phrase_salt": None,
534
+ "ss58_address": ss58_address,
535
+ }
536
+
537
+ # Set as active account if no active account exists
538
+ if not config["substrate"].get("active_account"):
539
+ config["substrate"]["active_account"] = account_name
540
+
541
+ else:
542
+ # Legacy mode - single account
543
+ config["substrate"]["seed_phrase"] = seed_phrase
544
+ config["substrate"]["seed_phrase_encoded"] = False
545
+ config["substrate"]["seed_phrase_salt"] = None
546
+ config["substrate"]["ss58_address"] = ss58_address
547
+
548
+ return save_config(config)
549
+
550
+
551
+ def get_active_account() -> Optional[str]:
552
+ """
553
+ Get the name of the currently active account.
554
+
555
+ Returns:
556
+ Optional[str]: The name of the active account, or None if not set
557
+ """
558
+ return get_config_value("substrate", "active_account")
559
+
560
+
561
+ def set_active_account(account_name: str) -> bool:
562
+ """
563
+ Set the active account by name.
564
+
565
+ Args:
566
+ account_name: Name of the account to set as active
567
+
568
+ Returns:
569
+ bool: True if successful, False otherwise
570
+ """
571
+ config = load_config()
572
+
573
+ # Check if the account exists
574
+ if account_name not in config["substrate"].get("accounts", {}):
575
+ print(f"Error: Account '{account_name}' not found")
576
+ return False
577
+
578
+ # Set as active account
579
+ config["substrate"]["active_account"] = account_name
580
+ return save_config(config)
581
+
582
+
583
+ def list_accounts() -> Dict[str, Dict[str, Any]]:
584
+ """
585
+ Get a list of all stored accounts.
586
+
587
+ Returns:
588
+ Dict[str, Dict[str, Any]]: Dictionary of account names to account data
589
+ """
590
+ config = load_config()
591
+ accounts = config["substrate"].get("accounts", {})
592
+
593
+ # Mark the active account
594
+ active_account = config["substrate"].get("active_account")
595
+ if active_account and active_account in accounts:
596
+ accounts[active_account]["is_active"] = True
597
+
598
+ return accounts
599
+
600
+
601
+ def delete_account(account_name: str) -> bool:
602
+ """
603
+ Delete an account by name.
604
+
605
+ Args:
606
+ account_name: Name of the account to delete
607
+
608
+ Returns:
609
+ bool: True if successful, False otherwise
610
+ """
611
+ config = load_config()
612
+
613
+ # Check if the account exists
614
+ if account_name not in config["substrate"].get("accounts", {}):
615
+ print(f"Error: Account '{account_name}' not found")
616
+ return False
617
+
618
+ # Delete the account
619
+ del config["substrate"]["accounts"][account_name]
620
+
621
+ # Update active account if needed
622
+ if config["substrate"].get("active_account") == account_name:
623
+ if config["substrate"]["accounts"]:
624
+ # Set the first remaining account as active
625
+ config["substrate"]["active_account"] = next(
626
+ iter(config["substrate"]["accounts"])
627
+ )
628
+ else:
629
+ # No more accounts
630
+ config["substrate"]["active_account"] = None
631
+
632
+ return save_config(config)
633
+
634
+
635
+ def get_account_address(account_name: Optional[str] = None) -> Optional[str]:
636
+ """
637
+ Get the SS58 address for an account.
638
+
639
+ Args:
640
+ account_name: Optional name of the account (if None, uses active account or legacy mode)
641
+
642
+ Returns:
643
+ Optional[str]: The SS58 address, or None if not available
644
+ """
645
+ config = load_config()
646
+
647
+ # Determine if we're using multi-account mode
648
+ if account_name is not None or config["substrate"].get("active_account"):
649
+ # Multi-account mode
650
+ name_to_use = account_name or config["substrate"].get("active_account")
651
+
652
+ if not name_to_use:
653
+ print("Error: No account specified and no active account")
654
+ return None
655
+
656
+ if name_to_use not in config["substrate"].get("accounts", {}):
657
+ print(f"Error: Account '{name_to_use}' not found")
658
+ return None
659
+
660
+ return config["substrate"]["accounts"][name_to_use].get("ss58_address")
661
+ else:
662
+ # Legacy mode - single account
663
+ return config["substrate"].get("ss58_address")
664
+
665
+
666
+ def initialize_from_env() -> None:
667
+ """
668
+ Initialize configuration from environment variables.
669
+
670
+ This is useful for maintaining backward compatibility with .env files.
671
+ """
672
+ # Load dotenv first to get environment variables
673
+ try:
674
+ from dotenv import load_dotenv
675
+
676
+ load_dotenv()
677
+ except ImportError:
678
+ pass
679
+
680
+ config = load_config()
681
+ changed = False
682
+
683
+ # IPFS settings
684
+ if os.getenv("IPFS_GATEWAY"):
685
+ config["ipfs"]["gateway"] = os.getenv("IPFS_GATEWAY")
686
+ changed = True
687
+
688
+ if os.getenv("IPFS_API_URL"):
689
+ config["ipfs"]["api_url"] = os.getenv("IPFS_API_URL")
690
+ changed = True
691
+
692
+ # Substrate settings
693
+ if os.getenv("SUBSTRATE_URL"):
694
+ config["substrate"]["url"] = os.getenv("SUBSTRATE_URL")
695
+ changed = True
696
+
697
+ if os.getenv("SUBSTRATE_SEED_PHRASE"):
698
+ # Don't encrypt from env variables by default
699
+ config["substrate"]["seed_phrase"] = os.getenv("SUBSTRATE_SEED_PHRASE")
700
+ config["substrate"]["seed_phrase_encoded"] = False
701
+ config["substrate"]["seed_phrase_salt"] = None
702
+ changed = True
703
+
704
+ if os.getenv("SUBSTRATE_DEFAULT_MINERS"):
705
+ miners = os.getenv("SUBSTRATE_DEFAULT_MINERS").split(",")
706
+ config["substrate"]["default_miners"] = [m.strip() for m in miners if m.strip()]
707
+ changed = True
708
+
709
+ # Encryption settings
710
+ if os.getenv("HIPPIUS_ENCRYPTION_KEY"):
711
+ config["encryption"]["encryption_key"] = os.getenv("HIPPIUS_ENCRYPTION_KEY")
712
+ changed = True
713
+
714
+ if os.getenv("HIPPIUS_ENCRYPT_BY_DEFAULT"):
715
+ value = os.getenv("HIPPIUS_ENCRYPT_BY_DEFAULT").lower()
716
+ config["encryption"]["encrypt_by_default"] = value in ("true", "1", "yes")
717
+ changed = True
718
+
719
+ if changed:
720
+ save_config(config)
721
+
722
+
723
+ def get_all_config() -> Dict[str, Any]:
724
+ """
725
+ Get the complete configuration.
726
+
727
+ Returns:
728
+ Dict[str, Any]: The full configuration dictionary
729
+ """
730
+ return load_config()
731
+
732
+
733
+ def reset_config() -> bool:
734
+ """
735
+ Reset configuration to default values.
736
+
737
+ Returns:
738
+ bool: True if reset was successful, False otherwise
739
+ """
740
+ return save_config(DEFAULT_CONFIG.copy())
741
+
742
+
743
+ # Initialize configuration on module import
744
+ ensure_config_dir()