Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Documentation for the ExecutionStore #5144

Merged
merged 4 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions docs/source/plugins/developing_plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2430,6 +2430,91 @@ loaded only when the `brain_key` property is modified.
Panel data is never readable in Python; it is only implicitly used by
the types you define when they are rendered clientside.

.. _panel-execution-store

Execution store
---------------

Panels can store data in the execution store, which is a key-value store that
is persisted beyond the lifetime of the panel. This is useful for storing
information that should persist across panel instances and App sessions, such
as cached data, long-lived panel state, or user preferences.

You can create/retrieve execution stores scoped to the current ``ctx.dataset``
via :meth:`ctx.store <fiftyone.operators.executor.ExecutionContext.store>`:

.. code-block:: python
:linenos:

def on_load(ctx):
# Retrieve a store scoped to the current `ctx.dataset`
# The store is automatically created if necessary
store = ctx.store("my_store")

# Load a pre-existing value from the store
user_choice = store.get("user_choice")

# Store data with a TTL to ensure it is evicted after `ttl` seconds
store.set("my_key", {"foo": "bar"}, ttl=60)

# List all keys in the store
print(store.list_keys()) # ["user_choice", "my_key"]

# Retrieve data from the store
print(store.get("my_key")) # {"foo": "bar"}

# Retrieve metadata about a key
print(store.get_metadata("my_key"))
# {"created_at": ..., "updated_at": ..., "expires_at": ...}

# Delete a key from the store
store.delete("my_key")

# Clear all data in the store
store.clear()

.. note::

Did you know? Any execution stores associated with a dataset are
automatically deleted when the dataset is deleted.

For advanced use cases, it is also possible to create and use global stores
that are available to all datasets via the
:class:`ExecutionStore <fiftyone.operators.store.ExecutionStore>` class:

.. code-block:: python
:linenos:

from fiftyone.operators import ExecutionStore

# Retrieve a global store
# The store is automatically created if necessary
store = ExecutionStore.create("my_store")

# Store data with a TTL to ensure it is evicted after `ttl` seconds
store.set("my_key", {"foo": "bar"}, ttl=60)

# List all keys in the global store
print(store.list_keys()) # ["my_key"]

# Retrieve data from the global store
print(store.get("my_key")) # {"foo": "bar"}

# Retrieve metadata about a key
print(store.get_metadata("my_key"))
# {"created_at": ..., "updated_at": ..., "expires_at": ...}

# Delete a key from the global store
store.delete("my_key")

# Clear all data in the global store
store.clear()

.. warning::

Global stores have no automatic garbage collection, so take care when
creating and using global stores whose keys do not utilize TTLs.

.. _panel-saved-workspaces

Saved workspaces
Expand Down
32 changes: 9 additions & 23 deletions fiftyone/factory/repos/execution_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,33 +78,21 @@ def has_store(self, store_name) -> bool:

def list_stores(self) -> list[str]:
"""Lists the stores associated with the current context."""
result = self._collection.find(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Realized in testing that this method was broken

dict(key="__store__", dataset_id=self._dataset_id),
{"store_name": 1},
)
return [d["store_name"] for d in result]
pipeline = [
{"$match": {"dataset_id": self._dataset_id}},
{"$group": {"_id": "$store_name"}},
]
return [d["_id"] for d in self._collection.aggregate(pipeline)]

def count_stores(self) -> int:
"""Counts the stores associated with the current context."""
pipeline = [
{
"$match": {
"dataset_id": self._dataset_id,
}
},
{
"$group": {
"_id": {
"store_name": "$store_name",
"dataset_id": "$dataset_id",
}
}
},
{"$count": "total_stores"},
{"$match": {"dataset_id": self._dataset_id}},
{"$group": {"_id": "$store_name", "count": {"$count": {}}}},
]

result = list(self._collection.aggregate(pipeline))
return result[0]["total_stores"] if result else 0
return result[0]["count"] if result else 0

def delete_store(self, store_name) -> int:
"""Deletes the specified store."""
Expand Down Expand Up @@ -233,9 +221,7 @@ def has_store_global(self, store_name):
"""Determines whether a store with the given name exists across all
datasets and the global context.
"""
result = self._collection.find_one(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Realized in testing that this method was broken

dict(store_name=store_name, key="__store__"), {}
)
result = self._collection.find_one(dict(store_name=store_name), {})
return bool(result)

def list_stores_global(self) -> list[StoreDocument]:
Expand Down
1 change: 1 addition & 0 deletions fiftyone/operators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
)
from .utils import ProgressHandler, is_new
from .panel import Panel, PanelConfig
from .store import ExecutionStore
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By convention, I prefer that anything that we're publicly documenting for users to be accessible via the highest reasonable package level. For example in this case:

# happy
from fiftyone.operators import ExecutionStore

# sad; too low-level for end users
from fiftyone.operators.store import ExecutionStore

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add ExecutionStore to all to expose it properly.

The ExecutionStore is imported but not included in the __all__ list, which means it won't be exposed when users do from fiftyone.operators import *. Since this is being documented for users, it should be properly exposed.

Add this line before the __all__ definition:

from .store import ExecutionStore
from .categories import Categories

+ __all__ = ["ExecutionStore"]  # Add this before the dynamic __all__ computation
# This enables Sphinx refs to directly use paths imported here
__all__ = [k for k, v in globals().items() if not k.startswith("_")]

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Ruff

22-22: .store.ExecutionStore imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)

from .categories import Categories

# This enables Sphinx refs to directly use paths imported here
Expand Down
8 changes: 4 additions & 4 deletions fiftyone/operators/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from fiftyone.operators.operations import Operations
from fiftyone.operators.panel import PanelRef
from fiftyone.operators.registry import OperatorRegistry
from fiftyone.operators.store import ExecutionStore
import fiftyone.operators.types as types
from fiftyone.plugins.secrets import PluginSecretsResolver, SecretsDictionary
import fiftyone.server.view as fosv
Expand Down Expand Up @@ -876,17 +877,16 @@ def set_progress(self, progress=None, label=None):
self.log(f"Progress: {progress} - {label}")

def store(self, store_name):
"""
Create (if not previously created) and use a store with the specified name.
"""Retrieves the execution store with the given name.

The store is automatically created if necessary.

Args:
store_name: the name of the store

Returns:
a :class:`fiftyone.operators.store.ExecutionStore`
"""
from fiftyone.operators.store import ExecutionStore

dataset_id = self.dataset._doc.id
return ExecutionStore.create(store_name, dataset_id)

Expand Down
16 changes: 11 additions & 5 deletions fiftyone/operators/store/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
|
"""

from datetime import datetime
from typing import Any, Optional

from bson import ObjectId
Expand Down Expand Up @@ -101,19 +102,24 @@ def update_ttl(self, key: str, new_ttl: int) -> None:
"""
self._store_service.update_ttl(self.store_name, key, new_ttl)

def get_ttl(self, key: str) -> Optional[int]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Realized in testing that this method was broken

"""Retrieves the TTL for a specific key.
def get_metadata(self, key: str) -> Optional[datetime]:
"""Retrieves the metadata for the given key.

Args:
key: the key to get the TTL for
key: the key to check

Returns:
the TTL in seconds, or None if the key does not have a TTL
a dict of metadata about the key
"""
key_doc = self._store_service.get_key(self.store_name, key)
if key_doc is None:
return None
return key_doc.ttl

return dict(
created_at=key_doc.created_at,
updated_at=key_doc.updated_at,
expires_at=key_doc.expires_at,
)
brimoor marked this conversation as resolved.
Show resolved Hide resolved

def list_keys(self) -> list[str]:
"""Lists all keys in the store.
Expand Down
Loading