Skip to content

Commit

Permalink
1.1.1 [release]
Browse files Browse the repository at this point in the history
  • Loading branch information
bboonstra committed Oct 20, 2024
1 parent fc3fb47 commit 15d8782
Show file tree
Hide file tree
Showing 8 changed files with 679 additions and 137 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.1.1] - 2024-10-21

### Added

- Introduced new `passes` method for custom filtering functions
- Added `is_type` method for type checking in queries
- Expanded test suite to cover new `passes` and `is_type` functionalities
- Implements a blocking `finish_backup` method to databases to complete a backup before proceeding

## [1.1.0] - 2024-10-20

### Added
Expand Down
18 changes: 13 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ db.add({"Anything": "will not work"}) # Raises an error

## New Filtering Capabilities

Effortless 1.1.0 introduces powerful filtering capabilities using the `Field` class:
Effortless 1.1 introduces powerful filtering capabilities using the `Field` class:

- `equals`: Exact match
- `contains`: Check if a value is in a string or list
Expand All @@ -110,6 +110,8 @@ Effortless 1.1.0 introduces powerful filtering capabilities using the `Field` cl
- `matches_regex`: Regular expression matching
- `between_dates`: Date range filtering
- `fuzzy_match`: Approximate string matching
- `passes`: Apply a custom function to filter items
- `is_type`: Check the type of a field

You can combine these filters using `&` (AND) and `|` (OR) operators for complex queries.

Expand All @@ -120,13 +122,19 @@ result = db.filter(
)
```

For even more flexibility, you can use the `Query` class with a custom lambda function:
For even more flexibility, you can use the `passes` method with a custom function:

```python
from effortless import Query
def is_experienced(skills):
return len(skills) > 3

custom_query = Query(lambda item: len(item["name"]) > 5 and item["age"] % 2 == 0)
result = db.filter(custom_query)
result = db.filter(Field("skills").passes(is_experienced))
```

You can also check the type of a field:

```python
result = db.filter(Field("age").is_type(int))
```

These new filtering capabilities make Effortless more powerful while maintaining its simplicity and ease of use.
Expand Down
43 changes: 41 additions & 2 deletions effortless/effortless.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def __init__(self, db_name: str = "db"):
self.set_storage(db_name)
self._autoconfigure()
self._operation_count = 0
self._backup_thread = None

@staticmethod
def default_db():
Expand Down Expand Up @@ -374,9 +375,37 @@ def _handle_backup(self) -> None:
self._operation_count += 1
if self.config.backup and self._operation_count >= self.config.backup_interval:
self._operation_count = 0
threading.Thread(target=self._backup).start()

def _backup(self) -> None:
# If a backup thread is already running, we can stop it
if self._backup_thread and self._backup_thread.is_alive():
self._backup_thread.join(timeout=0) # Non-blocking join
if self._backup_thread.is_alive() and self.config.debug:
logger.debug("Previous backup thread is alive and not stopping")

# Start a new backup thread
self._backup_thread = threading.Thread(target=self._backup)
self._backup_thread.start()

def finish_backup(self, timeout: float = None) -> bool:
"""
Wait for any ongoing backup operation to complete.
This method blocks until the current backup thread (if any) has finished.
Args:
timeout (float, optional): Maximum time to wait for the backup to complete, in seconds.
If None, wait indefinitely. Defaults to None.
Returns:
bool: True if the backup completed (or there was no backup running),
False if the timeout was reached before the backup completed.
"""
if self._backup_thread and self._backup_thread.is_alive():
self._backup_thread.join(timeout)
return not self._backup_thread.is_alive()
return True

def _backup(self) -> bool:
"""
Perform a database backup.
Expand All @@ -388,13 +417,23 @@ def _backup(self) -> None:
"""
if self.config.backup:
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
):
raise IOError(
f"Backup directory {self.config.backup} is not writable or does not exist."
)

backup_path = os.path.join(
self.config.backup, os.path.basename(self._storage_file)
)
shutil.copy2(self._storage_file, backup_path)
logger.debug(f"Database backed up to {backup_path}")
return True # Indicate success
except IOError as e:
logger.error(f"Backup failed: {str(e)}")
return False # Indicate failure

def _compress_data(self, data: Dict[str, Any]) -> str:
"""
Expand Down
60 changes: 57 additions & 3 deletions effortless/search.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from abc import ABC, abstractmethod
from typing import Callable, Union, Tuple, List
from typing import Callable, Union, Tuple, List, Type
import re
from datetime import datetime
from difflib import SequenceMatcher
Expand Down Expand Up @@ -73,6 +73,7 @@ def __init__(
"""
if callable(condition_or_field):
self.condition = condition_or_field
self.field = None
else:
self.field = condition_or_field
self.condition = None
Expand Down Expand Up @@ -315,8 +316,19 @@ def between_dates(self, start_date, end_date):
TypeError: If start_date or end_date is not a datetime object.
ValueError: If start_date is after end_date.
"""
if not isinstance(start_date, datetime) or not isinstance(end_date, datetime):
raise TypeError("Start and end dates must be datetime objects")
def to_datetime(date):
if isinstance(date, str):
try:
return datetime.fromisoformat(date)
except ValueError:
raise ValueError(f"Invalid date format: {date}")
elif isinstance(date, datetime):
return date
else:
raise TypeError(f"Date must be a string or datetime object, not {type(date)}")

start_date = to_datetime(start_date)
end_date = to_datetime(end_date)
if start_date > end_date:
raise ValueError("Start date must be before or equal to end date")

Expand Down Expand Up @@ -359,6 +371,48 @@ def condition(item):
self.condition = lambda item: self._validate_field(item) and condition(item)
return self

def passes(self, func):
"""
Create a condition that checks if the field passes the given function.
Args:
func (callable): A function that takes the field value and returns a boolean.
Returns:
Query: This query object with the new condition.
"""

def condition(item):
field_value = self._get_nested_value(item, self.field)
try:
return func(field_value)
except Exception as e:
func_name = getattr(func, "__name__", "unnamed function")
raise ValueError(
f"Error checking condition '{func_name}': {str(e)}"
) from e

self.condition = lambda item: self._validate_field(item) and condition(item)
return self

def is_type(self, expected_type: Type):
"""
Create a condition that checks if the field is of the expected type.
Args:
expected_type (Type): The expected type of the field.
Returns:
Query: This query object with the new condition.
"""

def condition(item):
field_value = self._get_nested_value(item, self.field)
return isinstance(field_value, expected_type)

self.condition = lambda item: self._validate_field(item) and condition(item)
return self


class AndQuery(BaseQuery):
"""
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

setup(
name="Effortless",
version="1.1.0",
version="1.1.1",
packages=find_packages(),
description="Databases should be Effortless.",
long_description=open("README.md").read(),
Expand Down
39 changes: 37 additions & 2 deletions tests/test_configuration.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import unittest
import tempfile
import shutil
import os
from effortless import EffortlessDB, EffortlessConfig


Expand Down Expand Up @@ -65,10 +66,10 @@ 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

# This should work
self.db.add({"small": "data"})

# This should raise an error
large_data = {"large": "x" * 1000} # Approximately 1 KB
with self.assertRaises(ValueError):
Expand Down Expand Up @@ -103,6 +104,40 @@ def test_invalid_configuration_values(self):
with self.assertRaises(ValueError):
EffortlessConfig({"bpi": 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))

# Assert that the backup path is properly configured
self.assertEqual(self.db.config.backup, backup_path)

# Add an item to trigger a backup
self.db.add({"name": "Alice", "age": 30})

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.",
)

# Add another item to trigger a backup again
self.db.add({"name": "Bob", "age": 25})

# Check if the backup file still exists and has been updated
self.assertTrue(
os.path.exists(backup_file),
"Backup file should still exist after adding the second item.",
)

# Clean up the backup directory
shutil.rmtree(backup_path)


if __name__ == "__main__":
unittest.main()
Loading

0 comments on commit 15d8782

Please sign in to comment.