Skip to main content

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_at and updated_at are 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.