communex.compat.storage
Data storage compatible with the classic commune
library.
1""" 2Data storage compatible with the *classic* `commune` library. 3""" 4 5# TODO: encryption 6 7import base64 8import hashlib 9import json 10import os.path 11import time 12from typing import Any 13 14from nacl.secret import SecretBox 15from nacl.utils import random 16 17from communex.errors import PasswordNotProvidedError 18from communex.util import ensure_parent_dir_exists 19 20# from cryptography.fernet import Fernet 21# from cryptography.hazmat.primitives import hashes 22# from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC 23 24 25COMMUNE_HOME = "~/.commune" 26""" 27COMMUNE_HOME 28 29 classic commune data storage home directory. 30""" 31 32 33def _derive_key(password: str): 34 # Derive a 256-bit key from the password using Blake2b 35 key = hashlib.blake2b(password.encode(), digest_size=32).digest() 36 return key 37 38 39def _encrypt_data(password: str, data: Any) -> str: 40 key = _derive_key(password) 41 box = SecretBox(key) 42 nonce = random(SecretBox.NONCE_SIZE) 43 raw = json.dumps(data).encode() 44 ciphertext = box.encrypt(raw, nonce).ciphertext 45 encrypted = nonce + ciphertext 46 decoded_data = base64.b64encode(encrypted).decode() 47 return decoded_data 48 49 50def _decrypt_data(password: str, data: str) -> Any: 51 key = _derive_key(password) 52 box = SecretBox(key) 53 encrypted = base64.b64decode(data.encode()) 54 nonce = encrypted[: SecretBox.NONCE_SIZE] 55 ciphertext = encrypted[SecretBox.NONCE_SIZE :] 56 raw = box.decrypt(ciphertext, nonce) 57 return json.loads(raw.decode()) 58 59 60def classic_load( 61 path: str, mode: str = "json", password: str | None = None 62) -> Any: 63 """ 64 Load data from commune data storage. 65 66 Args: 67 path: Data storage file path. 68 mode: Data storage mode. 69 70 Returns: 71 Data loaded from the data storage. 72 73 Todo: 74 * Other serialization modes support. Only json mode is supported now. 75 76 Raises: 77 NotImplementedError: See Todo. 78 AssertionError: Raised when the data is not in the classic format. 79 """ 80 if mode != "json": 81 raise NotImplementedError( 82 "Our commune data storage only supports json mode" 83 ) 84 85 full_path = os.path.expanduser(os.path.join(COMMUNE_HOME, path)) 86 with open(full_path, "r") as file: 87 body = json.load(file) 88 89 if body["encrypted"] and password is None: 90 raise PasswordNotProvidedError( 91 "Data is encrypted but no password provided" 92 ) 93 if body["encrypted"] and password is not None: 94 content = _decrypt_data(password, body["data"]) 95 else: 96 content = body["data"] 97 98 assert isinstance(body, dict) 99 assert isinstance(body["timestamp"], int) 100 assert isinstance(content, (dict, list, tuple, set, float, str, int)) 101 return content # type: ignore 102 103 104def classic_put( 105 path: str, value: Any, mode: str = "json", password: str | None = None 106): 107 """ 108 Put data into commune data storage. 109 110 Args: 111 path: Data storage path. 112 value: Data to store. 113 mode: Data storage mode. 114 encrypt: Whether to encrypt the data. 115 116 Todo: 117 * Other serialization modes support. Only json mode is supported now. 118 119 Raises: 120 NotImplementedError: See Todo. 121 TypeError: Raised when value is not a valid type. 122 FileExistsError: Raised when the file already exists. 123 """ 124 if mode != "json": 125 raise NotImplementedError( 126 "Our commune data storage only supports json mode" 127 ) 128 if not isinstance(value, (dict, list, tuple, set, float, str, int)): 129 raise TypeError( 130 f"Invalid type for commune data storage value: {type(value)}" 131 ) 132 133 timestamp = int(time.time()) 134 135 full_path = os.path.expanduser(os.path.join(COMMUNE_HOME, path)) 136 137 if os.path.exists(full_path): 138 raise FileExistsError( 139 f"Commune data storage file already exists: {full_path}" 140 ) 141 142 ensure_parent_dir_exists(full_path) 143 144 if password: 145 value = _encrypt_data(password, value) 146 encrypted = True 147 else: 148 encrypted = False 149 150 with open(full_path, "w") as file: 151 json.dump( 152 {"data": value, "encrypted": encrypted, "timestamp": timestamp}, 153 file, 154 indent=4, 155 )
COMMUNE_HOME =
'~/.commune'
COMMUNE_HOME
classic commune data storage home directory.
def
classic_load(path: str, mode: str = 'json', password: str | None = None) -> Any:
61def classic_load( 62 path: str, mode: str = "json", password: str | None = None 63) -> Any: 64 """ 65 Load data from commune data storage. 66 67 Args: 68 path: Data storage file path. 69 mode: Data storage mode. 70 71 Returns: 72 Data loaded from the data storage. 73 74 Todo: 75 * Other serialization modes support. Only json mode is supported now. 76 77 Raises: 78 NotImplementedError: See Todo. 79 AssertionError: Raised when the data is not in the classic format. 80 """ 81 if mode != "json": 82 raise NotImplementedError( 83 "Our commune data storage only supports json mode" 84 ) 85 86 full_path = os.path.expanduser(os.path.join(COMMUNE_HOME, path)) 87 with open(full_path, "r") as file: 88 body = json.load(file) 89 90 if body["encrypted"] and password is None: 91 raise PasswordNotProvidedError( 92 "Data is encrypted but no password provided" 93 ) 94 if body["encrypted"] and password is not None: 95 content = _decrypt_data(password, body["data"]) 96 else: 97 content = body["data"] 98 99 assert isinstance(body, dict) 100 assert isinstance(body["timestamp"], int) 101 assert isinstance(content, (dict, list, tuple, set, float, str, int)) 102 return content # type: ignore
Load data from commune data storage.
Arguments:
- path: Data storage file path.
- mode: Data storage mode.
Returns:
Data loaded from the data storage.
Todo:
- Other serialization modes support. Only json mode is supported now.
Raises:
- NotImplementedError: See Todo.
- AssertionError: Raised when the data is not in the classic format.
def
classic_put( path: str, value: Any, mode: str = 'json', password: str | None = None):
105def classic_put( 106 path: str, value: Any, mode: str = "json", password: str | None = None 107): 108 """ 109 Put data into commune data storage. 110 111 Args: 112 path: Data storage path. 113 value: Data to store. 114 mode: Data storage mode. 115 encrypt: Whether to encrypt the data. 116 117 Todo: 118 * Other serialization modes support. Only json mode is supported now. 119 120 Raises: 121 NotImplementedError: See Todo. 122 TypeError: Raised when value is not a valid type. 123 FileExistsError: Raised when the file already exists. 124 """ 125 if mode != "json": 126 raise NotImplementedError( 127 "Our commune data storage only supports json mode" 128 ) 129 if not isinstance(value, (dict, list, tuple, set, float, str, int)): 130 raise TypeError( 131 f"Invalid type for commune data storage value: {type(value)}" 132 ) 133 134 timestamp = int(time.time()) 135 136 full_path = os.path.expanduser(os.path.join(COMMUNE_HOME, path)) 137 138 if os.path.exists(full_path): 139 raise FileExistsError( 140 f"Commune data storage file already exists: {full_path}" 141 ) 142 143 ensure_parent_dir_exists(full_path) 144 145 if password: 146 value = _encrypt_data(password, value) 147 encrypted = True 148 else: 149 encrypted = False 150 151 with open(full_path, "w") as file: 152 json.dump( 153 {"data": value, "encrypted": encrypted, "timestamp": timestamp}, 154 file, 155 indent=4, 156 )
Put data into commune data storage.
Arguments:
- path: Data storage path.
- value: Data to store.
- mode: Data storage mode.
- encrypt: Whether to encrypt the data.
Todo:
- Other serialization modes support. Only json mode is supported now.
Raises:
- NotImplementedError: See Todo.
- TypeError: Raised when value is not a valid type.
- FileExistsError: Raised when the file already exists.