Skip to content

Commit

Permalink
actors_from_ids plugin hook and datasette.actors_from_ids() method (#…
Browse files Browse the repository at this point in the history
…2181)

* Prototype of actors_from_ids plugin hook, refs #2180
* datasette-remote-actors example plugin, refs #2180
  • Loading branch information
simonw committed Sep 8, 2023
1 parent c263704 commit b645174
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 0 deletions.
10 changes: 10 additions & 0 deletions datasette/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -819,6 +819,16 @@ async def _crumb_items(self, request, table=None, database=None):
)
return crumbs

async def actors_from_ids(
self, actor_ids: Iterable[Union[str, int]]
) -> Dict[Union[id, str], Dict]:
result = pm.hook.actors_from_ids(datasette=self, actor_ids=actor_ids)
if result is None:
# Do the default thing
return {actor_id: {"id": actor_id} for actor_id in actor_ids}
result = await await_me_maybe(result)
return result

async def permission_allowed(
self, actor, action, resource=None, default=DEFAULT_NOT_SET
):
Expand Down
5 changes: 5 additions & 0 deletions datasette/hookspecs.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ def actor_from_request(datasette, request):
"""Return an actor dictionary based on the incoming request"""


@hookspec(firstresult=True)
def actors_from_ids(datasette, actor_ids):
"""Returns a dictionary mapping those IDs to actor dictionaries"""


@hookspec
def filters_from_request(request, database, table, datasette):
"""
Expand Down
21 changes: 21 additions & 0 deletions docs/internals.rst
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,27 @@ await .render_template(template, context=None, request=None)

Renders a `Jinja template <https://jinja.palletsprojects.com/en/2.11.x/>`__ using Datasette's preconfigured instance of Jinja and returns the resulting string. The template will have access to Datasette's default template functions and any functions that have been made available by other plugins.

.. _datasette_actors_from_ids:

await .actors_from_ids(actor_ids)
---------------------------------

``actor_ids`` - list of strings or integers
A list of actor IDs to look up.

Returns a dictionary, where the keys are the IDs passed to it and the values are the corresponding actor dictionaries.

This method is mainly designed to be used with plugins. See the :ref:`plugin_hook_actors_from_ids` documentation for details.

If no plugins that implement that hook are installed, the default return value looks like this:

.. code-block:: json
{
"1": {"id": "1"},
"2": {"id": "2"}
}
.. _datasette_permission_allowed:

await .permission_allowed(actor, action, resource=None, default=...)
Expand Down
57 changes: 57 additions & 0 deletions docs/plugin_hooks.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1071,6 +1071,63 @@ Instead of returning a dictionary, this function can return an awaitable functio
Examples: `datasette-auth-tokens <https://datasette.io/plugins/datasette-auth-tokens>`_, `datasette-auth-passwords <https://datasette.io/plugins/datasette-auth-passwords>`_

.. _plugin_hook_actors_from_ids:

actors_from_ids(datasette, actor_ids)
-------------------------------------

``datasette`` - :ref:`internals_datasette`
You can use this to access plugin configuration options via ``datasette.plugin_config(your_plugin_name)``, or to execute SQL queries.

``actor_ids`` - list of strings or integers
The actor IDs to look up.

The hook must return a dictionary that maps the incoming actor IDs to their full dictionary representation.

Some plugins that implement social features may store the ID of the :ref:`actor <authentication_actor>` that performed an action - added a comment, bookmarked a table or similar - and then need a way to resolve those IDs into display-friendly actor dictionaries later on.

Unlike other plugin hooks, this only uses the first implementation of the hook to return a result. You can expect users to only have a single plugin installed that implements this hook.

If no plugin is installed, Datasette defaults to returning actors that are just ``{"id": actor_id}``.

The hook can return a dictionary or an awaitable function that then returns a dictionary.

This example implementation returns actors from a database table:

.. code-block:: python
from datasette import hookimpl
@hookimpl
def actors_from_ids(datasette, actor_ids):
db = datasette.get_database("actors")
async def inner():
sql = "select id, name from actors where id in ({})".format(
", ".join("?" for _ in actor_ids)
)
actors = {}
for row in (await db.execute(sql, actor_ids)).rows:
actor = dict(row)
actors[actor["id"]] = actor
return actors
return inner
The returned dictionary from this example looks like this:

.. code-block:: json
{
"1": {"id": "1", "name": "Tony"},
"2": {"id": "2", "name": "Tina"},
}
These IDs could be integers or strings, depending on how the actors used by the Datasette instance are configured.

Example: `datasette-remote-actors <https://github.com/datasette/datasette-remote-actors>`_

.. _plugin_hook_filters_from_request:

filters_from_request(request, database, table, datasette)
Expand Down
62 changes: 62 additions & 0 deletions tests/test_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -1215,3 +1215,65 @@ async def test_hook_register_permissions_allows_identical_duplicates():
await ds.invoke_startup()
# Check that ds.permissions has only one of each
assert len([p for p in ds.permissions.values() if p.abbr == "abbr1"]) == 1


@pytest.mark.asyncio
async def test_hook_actors_from_ids():
# Without the hook should return default {"id": id} list
ds = Datasette()
await ds.invoke_startup()
db = ds.add_memory_database("actors_from_ids")
await db.execute_write(
"create table actors (id text primary key, name text, age int)"
)
await db.execute_write(
"insert into actors (id, name, age) values ('3', 'Cate Blanchett', 52)"
)
await db.execute_write(
"insert into actors (id, name, age) values ('5', 'Rooney Mara', 36)"
)
await db.execute_write(
"insert into actors (id, name, age) values ('7', 'Sarah Paulson', 46)"
)
await db.execute_write(
"insert into actors (id, name, age) values ('9', 'Helena Bonham Carter', 55)"
)
table_names = await db.table_names()
assert table_names == ["actors"]
actors1 = await ds.actors_from_ids(["3", "5", "7"])
assert actors1 == {
"3": {"id": "3"},
"5": {"id": "5"},
"7": {"id": "7"},
}

class ActorsFromIdsPlugin:
__name__ = "ActorsFromIdsPlugin"

@hookimpl
def actors_from_ids(self, datasette, actor_ids):
db = datasette.get_database("actors_from_ids")

async def inner():
sql = "select id, name from actors where id in ({})".format(
", ".join("?" for _ in actor_ids)
)
actors = {}
result = await db.execute(sql, actor_ids)
for row in result.rows:
actor = dict(row)
actors[actor["id"]] = actor
return actors

return inner

try:
pm.register(ActorsFromIdsPlugin(), name="ActorsFromIdsPlugin")
actors2 = await ds.actors_from_ids(["3", "5", "7"])
assert actors2 == {
"3": {"id": "3", "name": "Cate Blanchett"},
"5": {"id": "5", "name": "Rooney Mara"},
"7": {"id": "7", "name": "Sarah Paulson"},
}
finally:
pm.unregister(name="ReturnNothingPlugin")

0 comments on commit b645174

Please sign in to comment.