diff --git a/effortless/configuration.py b/effortless/configuration.py index 045a6e8..a00b31a 100644 --- a/effortless/configuration.py +++ b/effortless/configuration.py @@ -8,51 +8,122 @@ class EffortlessConfig: This class holds various configuration options for an EffortlessDB instance. """ - def __init__(self, config: Dict[str, Any] = {}): + def __init__( + self, + *, + debug: bool = False, + required_fields: List[str] = [], + max_size: Optional[float] = None, + version: int = 1, + backup_path: Optional[str] = None, + backup_interval: int = 1, + encrypted: bool = False, + compressed: bool = False, + readonly: bool = False + ): """ Initialize an EffortlessConfig instance. Args: - config (Dict[str, Any], optional): A dictionary of configuration options. Defaults to an empty dict. - - Attributes: debug (bool): Enable debug mode. Defaults to False. - requires (List[str]): List of required fields for each entry. Defaults to an empty list. + required_fields (List[str]): List of required fields for each entry. Defaults to an empty list. max_size (Optional[int]): Maximum size of the database in MB. Defaults to None (no limit). - v (int): Version of the configuration. Always 1 for now. - backup (Optional[str]): Path to backup location. Defaults to None (no backup). + version (int): Version of the configuration. Always 1 for now. + backup_path (Optional[str]): Path to backup location. Defaults to None (no backup). backup_interval (int): Number of operations between backups. Defaults to 1. encrypted (bool): Whether the database should be encrypted. Defaults to False. compressed (bool): Whether the database should be compressed. Defaults to False. readonly (bool): Whether the database is in read-only mode. Defaults to False. """ - self.debug: bool = config.get("dbg", False) - self.requires: List[str] = config.get("rq", []) - self.max_size: Optional[int] = config.get("ms") - self.v: int = config.get("v", 1) - self.backup: Optional[str] = config.get("bp") - self.backup_interval: int = config.get("bpi", 1) - self.encrypted: bool = config.get("enc", False) - self.compressed: bool = config.get("cmp", False) - self.readonly: bool = config.get("ro", False) - - self._validate() - - def _validate(self) -> None: - """ - Validate the configuration values. + self.debug = debug + self.required_fields = required_fields + self.max_size = max_size + self.version = version + self.backup_path = backup_path + self.backup_interval = backup_interval + self.encrypted = encrypted + self.compressed = compressed + self.readonly = readonly - Raises: - ValueError: If any configuration value is invalid. - """ - if self.max_size is not None and self.max_size <= 0: + @property + def debug(self) -> bool: + return self._debug + + @debug.setter + def debug(self, value: bool) -> None: + self._debug = bool(value) + + @property + def required_fields(self) -> List[str]: + return self._required_fields + + @required_fields.setter + def required_fields(self, value: List[str]) -> None: + self._required_fields = list(value) + + @property + def max_size(self) -> Optional[float]: + return self._max_size + + @max_size.setter + def max_size(self, value: Optional[float]) -> None: + if value is not None and value <= 0: raise ValueError("max_size must be a positive integer") - if self.v != 1: + self._max_size = value + + @property + def version(self) -> int: + return self._version + + @version.setter + def version(self, value: int) -> None: + if value != 1: raise ValueError( "v1 is the only version of EffortlessDB currently available." ) - if self.backup_interval <= 0: + self._version = value + + @property + def backup_path(self) -> Optional[str]: + return self._backup_path + + @backup_path.setter + def backup_path(self, value: Optional[str]) -> None: + self._backup_path = value + + @property + def backup_interval(self) -> int: + return self._backup_interval + + @backup_interval.setter + def backup_interval(self, value: int) -> None: + if value <= 0: raise ValueError("Backup interval must be a positive integer") + self._backup_interval = value + + @property + def encrypted(self) -> bool: + return self._encrypted + + @encrypted.setter + def encrypted(self, value: bool) -> None: + self._encrypted = bool(value) + + @property + def compressed(self) -> bool: + return self._compressed + + @compressed.setter + def compressed(self, value: bool) -> None: + self._compressed = bool(value) + + @property + def readonly(self) -> bool: + return self._readonly + + @readonly.setter + def readonly(self, value: bool) -> None: + self._readonly = bool(value) def to_dict(self) -> Dict[str, Any]: """ @@ -62,17 +133,48 @@ def to_dict(self) -> Dict[str, Any]: Dict[str, Any]: A dictionary representation of the configuration. """ return { - "dbg": self.debug, - "rq": self.requires, - "ms": self.max_size, - "v": self.v, - "bp": self.backup, - "bpi": self.backup_interval, - "enc": self.encrypted, - "cmp": self.compressed, - "ro": self.readonly, + "debug": self.debug, + "required_fields": self.required_fields, + "max_size": self.max_size, + "version": self.version, + "backup_path": self.backup_path, + "backup_interval": self.backup_interval, + "encrypted": self.encrypted, + "compressed": self.compressed, + "readonly": self.readonly, } + @classmethod + def from_dict(cls, config_dict: Dict[str, Any]) -> "EffortlessConfig": + """ + Create an EffortlessConfig instance from a dictionary. + + Args: + config_dict (Dict[str, Any]): A dictionary containing configuration options. + + Returns: + EffortlessConfig: An instance of EffortlessConfig. + """ + # For backwards compatibility, map old keys to new keys + key_mapping = { + "dbg": "debug", + "rq": "required_fields", + "ms": "max_size", + "v": "version", + "bp": "backup_path", + "bpi": "backup_interval", + "enc": "encrypted", + "cmp": "compressed", + "ro": "readonly", + } + + # Create a new dictionary with updated keys + updated_dict = {} + for key, value in config_dict.items(): + updated_dict[key_mapping.get(key, key)] = value + + return cls(**updated_dict) + @staticmethod def default_headers() -> Dict[str, Any]: """ diff --git a/effortless/effortless.py b/effortless/effortless.py index 28c1149..dc2608f 100644 --- a/effortless/effortless.py +++ b/effortless/effortless.py @@ -42,8 +42,8 @@ def default_db(): Returns: dict: A dictionary with 'headers' (default configuration) and an empty 'content'. """ - ddb = EffortlessConfig.default_headers() - ddb["content"] = [] # Changed to an empty list + ddb: Dict[str, Any] = {"headers": EffortlessConfig().to_dict()} + ddb["content"] = [] return ddb def set_directory(self, directory: str) -> None: @@ -132,7 +132,7 @@ def _autoconfigure(self) -> None: This method is called internally during initialization and when the storage changes. """ data = self._read_db() - if "v" not in data["headers"]: + if "version" not in data["headers"]: self.config = EffortlessConfig() data["headers"] = self.config.to_dict() self._write_db(data) @@ -149,7 +149,7 @@ def _update_config(self): Note: This method is called internally after operations that might change the configuration. """ - self.config = EffortlessConfig(self._read_db()["headers"]) + self.config = EffortlessConfig.from_dict(self._read_db()["headers"]) def configure(self, new_config: EffortlessConfig) -> None: """ @@ -229,7 +229,7 @@ def add(self, entry: dict) -> None: if not isinstance(entry, dict): raise TypeError("Entry must be a dictionary") - for field in self.config.requires: + for field in self.config.required_fields: if field not in entry: raise ValueError( f"Field '{field}' is configured to be required in this database" @@ -302,10 +302,10 @@ def _read_db(self) -> Dict[str, Any]: headers = data["headers"] content = data["content"] - if headers.get("cmp"): + if headers.get("compressed"): content = self._decompress_data(content) - if headers.get("enc"): + if headers.get("encrypted"): content = self._decrypt_data( content if isinstance(content, str) else json.dumps(content) ) @@ -341,10 +341,10 @@ def _write_db(self, data: Dict[str, Any], write_in_readonly: bool = False) -> No headers = data["headers"] content = data["content"] - if headers.get("enc"): + if headers.get("encrypted"): content = self._encrypt_data(content) - if headers.get("cmp"): + if headers.get("compressed"): content = self._compress_data(json.dumps(content)) final_data = json.dumps( @@ -370,7 +370,10 @@ def _handle_backup(self) -> None: Backups are performed in a separate thread to avoid blocking the main operation. """ self._operation_count += 1 - if self.config.backup and self._operation_count >= self.config.backup_interval: + if ( + self.config.backup_path + and self._operation_count >= self.config.backup_interval + ): self._operation_count = 0 # If a backup thread is already running, we can stop it @@ -412,18 +415,17 @@ def _backup(self) -> bool: Note: If the backup fails, an error is logged but no exception is raised to the caller. """ - if self.config.backup: + if self.config.backup_path: try: - # Check if backup directory is valid - if not os.path.exists(self.config.backup) or not os.access( - self.config.backup, os.W_OK + if not os.path.exists(self.config.backup_path) or not os.access( + self.config.backup_path, os.W_OK ): raise IOError( - f"Backup directory {self.config.backup} is not writable or does not exist." + f"Backup directory {self.config.backup_path} is not writable or does not exist." ) backup_path = os.path.join( - self.config.backup, os.path.basename(self._storage_file) + self.config.backup_path, os.path.basename(self._storage_file) ) shutil.copy2(self._storage_file, backup_path) logger.debug(f"Database backed up to {backup_path}") diff --git a/tests/test_advanced_usage.py b/tests/test_advanced_usage.py index c09df6b..3e66dcb 100644 --- a/tests/test_advanced_usage.py +++ b/tests/test_advanced_usage.py @@ -162,7 +162,7 @@ def test_search_in_list(self): ) def test_encryption_and_compression(self): - self.db.configure(EffortlessConfig({"enc": True, "cmp": True})) + self.db.configure(EffortlessConfig(encrypted=True, compressed=True)) self.db.add({"test": "data"}) data = self.db._read_db() self.assertIsInstance( @@ -177,7 +177,7 @@ def test_encryption_and_compression(self): ) def test_encryption(self): - self.db.configure(EffortlessConfig({"enc": True})) + self.db.configure(EffortlessConfig(encrypted=True)) original_data = {"sensitive": "information"} self.db.add(original_data) @@ -198,7 +198,7 @@ def test_encryption(self): def test_backup(self): backup_dir = tempfile.mkdtemp() - self.db.configure(EffortlessConfig({"bp": backup_dir, "bpi": 1})) + self.db.configure(EffortlessConfig(backup_path=backup_dir, backup_interval=1)) self.db.add({"test": "backup"}) time.sleep(1) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 1235fb8..80243d6 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -18,10 +18,12 @@ def tearDown(self): def test_default_configuration(self): config = self.db.config self.assertFalse(config.debug, "Debug mode should be off by default") - self.assertEqual(config.requires, [], "No fields should be required by default") + self.assertEqual( + config.required_fields, [], "No fields should be required by default" + ) self.assertIsNone(config.max_size, "Max size should be None by default") - self.assertEqual(config.v, 1, "Version should be 1 by default") - self.assertIsNone(config.backup, "Backup path should be None by default") + self.assertEqual(config.version, 1, "Version should be 1 by default") + self.assertIsNone(config.backup_path, "Backup path should be None by default") self.assertEqual( config.backup_interval, 1, "Backup interval should be 1 by default" ) @@ -30,30 +32,32 @@ def test_default_configuration(self): self.assertFalse(config.readonly, "Read-only mode should be off by default") def test_configure_method(self): - new_config = { - "dbg": True, - "rq": ["name", "age"], - "ms": 100, - "bp": "/backup/path", - "bpi": 5, - "enc": False, - "cmp": False, - "ro": True, - } + new_config = EffortlessConfig( + debug=True, + required_fields=["name", "age"], + max_size=100, + backup_path="/backup/path", + backup_interval=5, + encrypted=False, + compressed=False, + readonly=True, + ) self.db.wipe() - self.db.configure(EffortlessConfig(new_config)) + self.db.configure(new_config) config = self.db.config self.assertTrue(config.debug, "Debug mode should be on after configuration") self.assertEqual( - config.requires, + config.required_fields, ["name", "age"], "Required fields should be set to name and age", ) self.assertEqual(config.max_size, 100, "Max size should be set to 100") - self.assertEqual(config.v, 1, "Version should remain 1") + self.assertEqual(config.version, 1, "Version should remain 1") self.assertEqual( - config.backup, "/backup/path", "Backup path should be set to /backup/path" + config.backup_path, + "/backup/path", + "Backup path should be set to /backup/path", ) self.assertEqual( config.backup_interval, 5, "Backup interval should be set to 5" @@ -70,7 +74,7 @@ def test_invalid_configuration(self): self.db.configure("invalid") # type: ignore def test_required_fields(self): - self.db.configure(EffortlessConfig({"rq": ["name"]})) + self.db.configure(EffortlessConfig(required_fields=["name"])) self.db.add({"name": "Alice", "age": 30}) # This should work with self.assertRaises( ValueError, @@ -80,7 +84,7 @@ def test_required_fields(self): def test_max_size_limit(self): self.db.wipe() - self.db.configure(EffortlessConfig({"ms": 0.001})) # Set max size to 1 KB + self.db.configure(EffortlessConfig(max_size=0.001)) # Set max size to 1 KB # This should work self.db.add({"small": "data"}) @@ -94,15 +98,17 @@ def test_max_size_limit(self): def test_readonly_mode(self): self.db = EffortlessDB() - self.db.configure(EffortlessConfig({"ro": True})) + self.db.configure(EffortlessConfig(readonly=True)) with self.assertRaises( ValueError, msg="Adding to a read-only database should raise ValueError" ): self.db.add({"name": "Alice"}) def test_configuration_persistence(self): - new_config = {"dbg": True, "rq": ["name"], "ms": 100, "v": 1} - self.db.configure(EffortlessConfig(new_config)) + new_config = EffortlessConfig( + debug=True, required_fields=["name"], max_size=100, version=1 + ) + self.db.configure(new_config) # Create a new instance with the same storage new_db = EffortlessDB() @@ -114,7 +120,7 @@ def test_configuration_persistence(self): config.debug, "Debug mode should persist across database instances" ) self.assertEqual( - config.requires, + config.required_fields, ["name"], "Required fields should persist across database instances", ) @@ -122,34 +128,36 @@ def test_configuration_persistence(self): config.max_size, 100, "Max size should persist across database instances" ) self.assertEqual( - config.v, 1, "Version should persist across database instances" + config.version, 1, "Version should persist across database instances" ) def test_invalid_configuration_values(self): with self.assertRaises( ValueError, msg="Negative max size should raise ValueError" ): - EffortlessConfig({"ms": -1}) + EffortlessConfig(max_size=-1) with self.assertRaises(ValueError, msg="Version 0 should raise ValueError"): - EffortlessConfig({"v": 0}) + EffortlessConfig(version=0) with self.assertRaises( ValueError, msg="Backup interval 0 should raise ValueError" ): - EffortlessConfig({"bpi": 0}) + EffortlessConfig(backup_interval=0) def test_backup_interval(self): # Configure the database with a backup path backup_path = tempfile.mkdtemp() # Create a temporary directory for backups - new_config = { - "dbg": True, - "bp": backup_path, # Set backup path - "bpi": 1, # Backup after every operation - } - self.db.configure(EffortlessConfig(new_config)) + new_config = EffortlessConfig( + debug=True, + backup_path=backup_path, # Set backup path + backup_interval=1, # Backup after every operation + ) + self.db.configure(new_config) # Assert that the backup path is properly configured self.assertEqual( - self.db.config.backup, backup_path, "Backup path should be set correctly" + self.db.config.backup_path, + backup_path, + "Backup path should be set correctly", ) # Add an entry to trigger a backup @@ -158,7 +166,7 @@ def test_backup_interval(self): backup_file = os.path.join(backup_path, "test_db.effortless") self.assertFalse( os.path.exists(backup_file), - "DB should not be backed up after 1 operation if bpi == 2.", + "DB should not be backed up after 1 operation if backup_interval == 2.", ) # Add another entry to trigger a backup again diff --git a/tests/test_docs.py b/tests/test_docs.py index 277c498..d3902cb 100644 --- a/tests/test_docs.py +++ b/tests/test_docs.py @@ -128,7 +128,7 @@ def test_advanced_usage(self): ) # Update configuration - db.configure(EffortlessConfig({"ro": True})) + db.configure(EffortlessConfig(readonly=True)) with self.assertRaises( Exception, msg="Adding to a read-only database should raise an exception" ): @@ -195,7 +195,7 @@ def test_safety_first(self): db.wipe(wipe_readonly=True) new_configuration = EffortlessConfig() - new_configuration.backup = self.test_dir + new_configuration.backup_path = self.test_dir db.configure(new_configuration) # Add some data @@ -205,7 +205,7 @@ def test_safety_first(self): db.finish_backup() self.assertEqual( - db.config.backup, + db.config.backup_path, self.test_dir, "Database backup directory should be set to the test directory", )