Skip to content

Commit

Permalink
Migrate configs to v2
Browse files Browse the repository at this point in the history
  • Loading branch information
bboonstra committed Oct 23, 2024
1 parent fdbd505 commit 75d7a8f
Show file tree
Hide file tree
Showing 5 changed files with 206 additions and 94 deletions.
176 changes: 139 additions & 37 deletions effortless/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
"""
Expand All @@ -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]:
"""
Expand Down
34 changes: 18 additions & 16 deletions effortless/effortless.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand All @@ -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:
"""
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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)
)
Expand Down Expand Up @@ -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(
Expand All @@ -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
Expand Down Expand Up @@ -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}")
Expand Down
6 changes: 3 additions & 3 deletions tests/test_advanced_usage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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)

Expand All @@ -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)
Expand Down
Loading

0 comments on commit 75d7a8f

Please sign in to comment.