Managing Bookmark Lifecycle
To manage the lifecycle of bookmarks—including creation, retrieval, updates, and soft-deletes—you use the BookmarkService. This service acts as a singleton facade that orchestrates operations across the repository, search index, and cache.
Accessing the Service
The BookmarkService is implemented as a singleton. You should instantiate it at the module level to share state across your application components.
from app.services.bookmark_service import BookmarkService
# Obtain the singleton instance
service = BookmarkService()
Creating a Bookmark
To create a bookmark, pass a dictionary containing at least a url and a title. The service performs validation and returns a tuple containing the created Bookmark object and an error message (if any).
data = {
"url": "https://example.com",
"title": "Example Domain",
"description": "A useful example site"
}
bookmark, error = service.create_bookmark(data)
if error:
# Handle validation errors (e.g., invalid URL or empty title)
print(f"Failed to create bookmark: {error}")
else:
print(f"Created bookmark with ID: {bookmark.id}")
When a bookmark is created, the service automatically:
- Validates the
urlandtitle. - Persists the entity via
BookmarkRepository. - Indexes the content in the
SearchIndex. - Invalidates any existing cache entry for that ID.
Retrieving Bookmarks
You can retrieve a single bookmark by its ID or fetch a paginated list with optional status filtering.
Single Retrieval (Cached)
The get_bookmark method checks the internal LRUCache before querying the repository.
bookmark_id = "some-uuid"
bookmark = service.get_bookmark(bookmark_id)
if not bookmark:
print("Bookmark not found")
Paginated Listing and Filtering
Use list_bookmarks to retrieve sets of bookmarks. You can filter by status using the strings "active", "archived", or "trashed".
# Get the first page of archived bookmarks
bookmarks, total_count = service.list_bookmarks(
page=1,
per_page=25,
status="archived"
)
print(f"Found {total_count} archived bookmarks.")
Updating and State Transitions
The service provides methods for partial updates and explicit state transitions like archiving or restoring.
Partial Updates
The update_bookmark method accepts a dictionary of fields to change. It automatically updates the updated_at timestamp and refreshes the search index.
updates = {"title": "New Improved Title"}
bookmark, error = service.update_bookmark(bookmark_id, updates)
if error:
print(f"Update failed: {error}")
Archiving and Restoring
State transitions are handled by specific methods that manage the underlying entity status and cache invalidation.
# Move to archive
archived_bookmark = service.archive_bookmark(bookmark_id)
# Restore to active status (from archive or trash)
restored_bookmark = service.restore_bookmark(bookmark_id)
Soft-Deleting Bookmarks
In this project, delete_bookmark performs a soft-delete. It moves the bookmark to the "trash" status rather than removing it from the database.
success = service.delete_bookmark(bookmark_id)
if not success:
print("Could not delete: Bookmark does not exist")
Troubleshooting
Shared State in Tests
Because BookmarkService is a singleton, state persists between test cases. If you are writing unit tests, use the internal _reset() method in your setup or teardown to ensure a clean environment.
def setup_function():
service = BookmarkService()
service._reset() # Reinitializes repo, cache, and search index
Validation Failures
The service does not raise exceptions for invalid data. Instead, it follows a Go-like return pattern: (Result, Error). Always check if the second element of the tuple is not None before proceeding with the result.
Tag Side Effects
When you delete a tag using service.delete_tag(tag_id), the service iterates through every bookmark containing that tag to remove the reference and invalidates their cache entries. This can be a heavy operation if many bookmarks share the same tag.