Bookmark Fundamentals
The Bookmark entity is the central domain object in etchblok-test-api, representing a saved URL with associated metadata, tags, and lifecycle state. It is implemented as a dataclass in app/models/bookmark.py and provides a set of methods for state transitions and data manipulation.
Creating Bookmarks
You can instantiate a Bookmark directly using its constructor or by using the from_dict class method, which is the standard way etchblok-test-api processes incoming API payloads.
from app.models.bookmark import Bookmark
# Direct instantiation
bookmark = Bookmark(
url="https://example.com",
title="Example Domain",
description="A useful site for testing."
)
# From a dictionary (common in service layers)
data = {
"url": "https://github.com",
"title": "GitHub",
"tags": ["dev", "git"]
}
bookmark = Bookmark.from_dict(data)
When a Bookmark is created, several attributes are automatically initialized:
- ID: A 12-character hex string generated from a UUID (e.g.,
5f3e1a2b3c4d). - Timestamps:
created_atandupdated_atare set to the current UTC time. - Status: Defaults to
BookmarkStatus.ACTIVE.
Validation Gotcha
While the Bookmark class contains a name-mangled __validate_url method, it is not automatically invoked during initialization. In etchblok-test-api, validation is handled by the BookmarkService in app/services/bookmark_service.py using external validator functions before the model is even instantiated.
Lifecycle and Visibility
The visibility and state of a bookmark are governed by the BookmarkStatus enum. A bookmark can be in one of three states:
ACTIVE: The default state for new bookmarks.ARCHIVED: For bookmarks you want to keep but hide from the main view.TRASHED: A soft-deleted state.
You manage these states using the lifecycle methods provided by the Bookmark class. Each of these methods updates the status and calls the internal _touch() helper to refresh the updated_at timestamp.
# Move to archive
bookmark.archive() # status becomes BookmarkStatus.ARCHIVED
# Soft-delete
bookmark.trash() # status becomes BookmarkStatus.TRASHED
# Restore to active
bookmark.restore() # status becomes BookmarkStatus.ACTIVE
In the BookmarkService, these transitions are typically followed by a repository save and cache invalidation:
# Example from app/services/bookmark_service.py
def delete_bookmark(self, bookmark_id: str) -> bool:
bookmark = self._repo.get_bookmark(bookmark_id)
if not bookmark:
return False
bookmark.trash()
self._repo.save_bookmark(bookmark)
self._cache.invalidate(bookmark_id)
return True
Managing Tags and Metadata
Bookmarks support categorization through a list of tag IDs and extensibility via a metadata dictionary.
Tagging
The add_tag and remove_tag methods manage the tags list. They return a boolean indicating whether the operation actually modified the list (e.g., add_tag returns False if the tag is already present).
bookmark.add_tag("research") # Returns True
bookmark.add_tag("research") # Returns False (already exists)
bookmark.remove_tag("research") # Returns True
Metadata
The metadata attribute is a Dict[str, Any] intended for arbitrary key/value pairs. This allows etchblok-test-api to store additional information (like scraper results or UI preferences) without modifying the core schema.
Serialization for API Responses
To send bookmark data over the wire, use the to_dict() method. This method converts the internal state, including enums and datetimes, into a JSON-serializable dictionary.
# Serializing for a JSON response
json_data = bookmark.to_dict()
# Output structure:
# {
# "id": "...",
# "url": "https://...",
# "status": "active",
# "created_at": "2023-10-27T10:00:00.000000",
# ...
# }
The status enum is converted to its string value (e.g., "active"), and timestamps are converted to ISO 8601 strings using isoformat(). This ensures compatibility with the API layer in app/routes/bookmarks.py.