Skip to content

Commit

Permalink
updates
Browse files Browse the repository at this point in the history
  • Loading branch information
brimoor committed Nov 19, 2024
1 parent 12e4270 commit 0ef0dc2
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 74 deletions.
99 changes: 57 additions & 42 deletions docs/source/plugins/developing_plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2432,73 +2432,88 @@ loaded only when the `brain_key` property is modified.

.. _panel-execution-store
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, such as cached data or
user preferences. See the
:class:`ExecutionStore <fiftyone.operators.store.ExecutionStore>` and
:class:`ExecutionStoreService <fiftyone.operators.store.ExecutionStoreService>`
classes for more information.
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:
from bson import ObjectId
from fiftyone.operators.store import ExecutionStore, ExecutionStoreService
STORE_NAME = "my_store_name"
GLOBAL_STORE_NAME = "my_global_store_name"
TTL_IN_SECONDS = 60
def on_load(ctx):
# Create a store instance, scoped to the ctx.dataset
store = ctx.store(STORE_NAME)
# Retrieve a store scoped to the current `ctx.dataset`
# The store is automatically created if necessary
store = ctx.store("my_store")
# Load a value from the 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_IN_SECONDS
store.set("my_data", {"key": "value"}, ttl=TTL_IN_SECONDS)
# Retrieve the stored data to verify
my_data = store.get("my_data")
print(my_data) # => {"key": "value"}
# 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
keys = store.list_keys()
print(keys) # => ["my_data", "another_existing_key"]
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
deleted = store.delete("my_data")
print(deleted) # => True
store.delete("my_key")
# Clear all data in the store
store.clear()
# Create a global store using the ExecutionStoreService
svc = ExecutionStoreService()
.. note::

# Set a key-value pair in the global store
svc.set(GLOBAL_STORE_NAME, "my_key", {"foo": "bar"}, ttl=TTL_IN_SECONDS)
Did you know? Any execution stores associated with a dataset are
automatically deleted when the dataset is deleted.

# Retrieve the key-value pair to verify
global_key_doc = svc.get(GLOBAL_STORE_NAME, "my_key")
print(global_key_doc.value) # => {"foo": "bar"}
print(global_key_doc.created) # => datetime.datetime
print(global_key_doc.expires) # => datetime.datetime
print(global_key_doc.ttl) # => 60
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:

# Delete a key from the global store
deleted_count = svc.delete(GLOBAL_STORE_NAME, "my_key")
.. code-block:: python
:linenos:
.. note::
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::

When using ``ctx.store`` in a panel, the store will be scoped to the
current ``ctx.dataset``. This means that the store will be deleted
when the dataset is deleted.
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
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(
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(
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
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]:
"""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,
)

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

0 comments on commit 0ef0dc2

Please sign in to comment.