From a38d8cf97a93b49a7ab0a5f3a107173095b81f50 Mon Sep 17 00:00:00 2001 From: David Feltell Date: Fri, 17 Nov 2023 12:35:56 +0000 Subject: [PATCH 1/2] [Docs] Add entityTraits notebook Part of OpenAssetIO/OpenAssetIO#31. This notebook was originally used to sketch out the design of the `entityTraits` API method, elicit discussion, and come to a shared understanding of the feature as a team. Being an executable document, the notebook now functions as tested documentation and an e2e test suite for the feature, utilising the OpenAssetIO ecosystem beyond the core `entityTraits` API method (i.e. convenience signatures, BAL, `managementPolicy`). Signed-off-by: David Feltell --- examples/querying_entity_traits.ipynb | 743 ++++++++++++++++++ .../querying_entity_traits/bal_database.json | 74 ++ .../openassetio_config.toml | 5 + 3 files changed, 822 insertions(+) create mode 100644 examples/querying_entity_traits.ipynb create mode 100644 examples/resources/querying_entity_traits/bal_database.json create mode 100644 examples/resources/querying_entity_traits/openassetio_config.toml diff --git a/examples/querying_entity_traits.ipynb b/examples/querying_entity_traits.ipynb new file mode 100644 index 0000000..98bbd39 --- /dev/null +++ b/examples/querying_entity_traits.ipynb @@ -0,0 +1,743 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "07297a3d-048b-496b-adb0-8fdd67316021", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "# Hosts: Asset trait introspection\n" + ] + }, + { + "cell_type": "markdown", + "id": "60bd9de5-64c3-4f15-8824-ff1d94a894d7", + "metadata": { + "editable": true, + "jp-MarkdownHeadingCollapsed": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## Setup\n", + "\n", + "See \"Hello OpenAssetIO\" notebook for details on how to bootstrap OpenAssetIO. " + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "df703c44ad53b21c", + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-03-13T07:57:00.032409Z", + "start_time": "2024-03-13T07:56:59.995014Z" + } + }, + "outputs": [], + "source": [ + "from resources import helpers\n", + "\n", + "manager, context = helpers.bootstrap(\"resources/querying_entity_traits/openassetio_config.toml\")" + ] + }, + { + "cell_type": "markdown", + "id": "1a80594a-b171-49c5-8cfd-00e334759b1e", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## Getting started" + ] + }, + { + "cell_type": "markdown", + "id": "22277554-b477-4600-8c2a-90ee8d5c064e", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "In the following examples we're going to ask the manager about the traits associated with an asset.\n", + "\n", + "We've been given a URI by a colleague, which we need to turn into an `EntityReference` before we can use it to query the asset management system." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "82c3962a-27b5-4375-b4fa-dbd14dcfc93b", + "metadata": { + "ExecuteTime": { + "end_time": "2024-03-13T07:57:00.037270Z", + "start_time": "2024-03-13T07:57:00.034388Z" + } + }, + "outputs": [ + { + "data": { + "text/markdown": "> **Result:**\n> ``" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "logo_ref = manager.createEntityReference(\"bal:///project_artwork/logos/openassetio\")\n", + "\n", + "helpers.display_result(repr(logo_ref))" + ] + }, + { + "cell_type": "markdown", + "id": "ec69ae04-1895-4243-a949-71360175b173", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Now we have an entity reference for our logo, we can use the API to learn more about it." + ] + }, + { + "cell_type": "markdown", + "id": "d8eaf0a116f06949", + "metadata": { + "collapsed": false + }, + "source": [ + "## The `entityTraits` method\n", + "\n", + "The `entityTraits` methods allows a host to query the manager for the traits associated with a given entity reference. It has two access modes. `kRead` mode will return all the traits that the manager associates with an entity. `kWrite` mode returns the minimal set of traits required to publish to the given entity reference.\n", + "\n", + "The `entityTraits` method is a required method that managers _must_ implement, so there is no need to perform a `hasCapability` check before using it.\n", + "\n", + "The following examples illustrate usage of the `entityTraits` method." + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Available signatures" + ], + "metadata": { + "collapsed": false + }, + "id": "268c8216d24b459" + }, + { + "cell_type": "markdown", + "source": [ + "Like many OpenAssetIO API functions, there are multiple available signatures that provide a more convenient interface to the core batch-first callback-based signature:" + ], + "metadata": { + "collapsed": false + }, + "id": "35f1a92b885b17e" + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "c245fd7d715f87b1", + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-03-13T07:57:00.095337Z", + "start_time": "2024-03-13T07:57:00.038186Z" + } + }, + "outputs": [ + { + "data": { + "text/markdown": "> **Result:**\n> `{'openassetio-mediacreation:identity.DisplayName', 'openassetio-mediacreation:usage.Entity', 'openassetio-mediacreation:twoDimensional.Image', 'openassetio-mediacreation:lifecycle.Version', 'openassetio-mediacreation:content.LocatableContent', 'openassetio-mediacreation:timeDomain.FrameRanged'}`" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import operator\n", + "\n", + "from openassetio.hostApi import Manager\n", + "from openassetio.access import EntityTraitsAccess\n", + "\n", + "\n", + "# The core batch-first callback-based signature\n", + "callback_results = [None]\n", + "\n", + "manager.entityTraits(\n", + " [logo_ref], EntityTraitsAccess.kRead, context,\n", + " lambda idx, result: operator.setitem(callback_results, idx, result),\n", + " lambda idx, err: operator.setitem(callback_results, idx, err))\n", + "\n", + "[callback_result] = callback_results\n", + "\n", + "\n", + "# Singular, exception-throwing\n", + "singular_result = manager.entityTraits(logo_ref, EntityTraitsAccess.kRead, context)\n", + "\n", + "# Singular, success/error object return\n", + "singular_result_or_error = manager.entityTraits(\n", + " logo_ref, EntityTraitsAccess.kRead, context, Manager.BatchElementErrorPolicyTag.kVariant)\n", + "\n", + "# Batch, exception-throwing\n", + "[batch_result] = manager.entityTraits([logo_ref], EntityTraitsAccess.kRead, context)\n", + "\n", + "# Batch, success/error object return.\n", + "[batch_result_or_error] = manager.entityTraits(\n", + " [logo_ref], EntityTraitsAccess.kRead, context, Manager.BatchElementErrorPolicyTag.kVariant)\n", + "\n", + "\n", + "assert all(\n", + " result == callback_result for result in\n", + " (singular_result, singular_result_or_error, batch_result, batch_result_or_error))\n", + "\n", + "helpers.display_result(callback_result)" + ] + }, + { + "cell_type": "markdown", + "id": "4348da46-9467-4734-9f96-d84e26f7c11f", + "metadata": { + "editable": true, + "jp-MarkdownHeadingCollapsed": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## Querying the full set of traits associated with an entity" + ] + }, + { + "cell_type": "markdown", + "id": "5e32a587-5233-441e-9b80-1b2d6d3769fb", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "OpenAssetIO does not try to fully define entity types. What it does define are traits that an entity satisfies. An entity is theoretically fully specified by some (large) subset of traits in the universe of all possible traits. A manager understands some subset of that theoretical complete subset of traits. This is the \"trait set\" of the entity, with respect to a given manager.\n", + "\n", + "In order to find out what the trait set of an entity is, we can use the `entityTraits` API method." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "0fcf0214-2561-488a-8eee-0330ca27710d", + "metadata": { + "scrolled": true, + "ExecuteTime": { + "end_time": "2024-03-13T07:57:00.109500Z", + "start_time": "2024-03-13T07:57:00.096537Z" + } + }, + "outputs": [ + { + "data": { + "text/markdown": "> **Result:**\n> `{'openassetio-mediacreation:identity.DisplayName', 'openassetio-mediacreation:usage.Entity', 'openassetio-mediacreation:twoDimensional.Image', 'openassetio-mediacreation:lifecycle.Version', 'openassetio-mediacreation:content.LocatableContent', 'openassetio-mediacreation:timeDomain.FrameRanged'}`" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from openassetio.access import EntityTraitsAccess\n", + "\n", + "entity_trait_set = manager.entityTraits(logo_ref, EntityTraitsAccess.kRead, context)\n", + "\n", + "helpers.display_result(entity_trait_set)" + ] + }, + { + "cell_type": "markdown", + "id": "a6c4f0ff-0e5d-40b7-84d6-d46efa8cba7a", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "`EntityTraitAccess.kRead` tells the manager the reason why you want the trait set. In this case we want to use the result in a read context, e.g. `resolve`ing properties, grouping entities of the same type in a UI, or determining the type of entity after a drag-and-drop of an entity reference into the application.\n", + "\n", + "What happens when we `resolve` this trait set?" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "fbae28c950c33589", + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-03-13T07:57:00.124503Z", + "start_time": "2024-03-13T07:57:00.110943Z" + } + }, + "outputs": [ + { + "data": { + "text/markdown": "> **Result:**\n> `TraitsData({\"openassetio-mediacreation:identity.DisplayName\", \"openassetio-mediacreation:content.LocatableContent\", \"openassetio-mediacreation:lifecycle.Version\"})`" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from openassetio.access import ResolveAccess\n", + "\n", + "entity_data = manager.resolve(logo_ref, entity_trait_set, ResolveAccess.kRead, context)\n", + "\n", + "helpers.display_result(entity_data)" + ] + }, + { + "cell_type": "markdown", + "id": "609f599ef0708376", + "metadata": { + "collapsed": false + }, + "source": [ + "Note that not all traits have `resolve`able properties - many are simply used to aid classification (note the lack of `Entity` and `FrameRanged` traits). However, it is safe to `resolve` the full trait set - those that do not have properties are simply missing from the response.\n", + "\n", + "As an aside, we can filter the trait set through `managementPolicy`, if we wish to know in advance which traits any given manager is capable of providing data for" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "b5c27257bb1af339", + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-03-13T07:57:00.128772Z", + "start_time": "2024-03-13T07:57:00.125726Z" + } + }, + "outputs": [ + { + "data": { + "text/markdown": "> **Result:**\n> `TraitsData({\"openassetio-mediacreation:managementPolicy.Managed\", \"openassetio-mediacreation:lifecycle.Version\", \"openassetio-mediacreation:content.LocatableContent\", \"openassetio-mediacreation:identity.DisplayName\"})`" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from openassetio.access import PolicyAccess\n", + "\n", + "[policy_data] = manager.managementPolicy([entity_trait_set], PolicyAccess.kRead, context)\n", + "\n", + "helpers.display_result(policy_data)" + ] + }, + { + "cell_type": "markdown", + "id": "3dd1e760a2097051", + "metadata": { + "collapsed": false + }, + "source": [ + "Note how the trait set returned from `managementPolicy` (minus any `managementPolicy` traits) matches the trait set we successfully `resolve`d. For example, this is missing the `FrameRangedTrait`, despite it being included in the result of `entityTraits`, meaning that the `FrameRangedTrait` is a quality of the entity, but properties of it cannot be resolved (perhaps due to some technical limitation of the manager).\n", + "\n", + "In this way `managementPolicy` can be used to filter the trait set of an entity to only those traits that have `resolve`able properties. However, note that `mangementPolicy` does not take an entity reference argument, only trait set(s). The result of `managementPolicy` is therefore constant for any given manager, regardless of entity.\n", + "\n", + "In practice, a well-behaved host will cherry-pick only those traits the host needs to `resolve`. That trait set should then be passed to `managementPolicy` to determine what the manager can actually provide. This process is independent of `entityTraits`. That is, the host should know at application startup which traits it's going to need, and so query `managementPolicy` ahead of time and store the result for use later. See relevant documentation for `managementPolicy` for more information." + ] + }, + { + "cell_type": "markdown", + "id": "dbb66de3947d7b24", + "metadata": { + "collapsed": false + }, + "source": [ + "## Querying the required set of traits for publishing an entity\n", + "\n", + "By using the `kWrite` access mode of `entityTraits`, we can query the minimal trait set that _must_ be provided when publishing to a particular entity reference.\n", + "\n", + "In most applications it is expected that the user will provide (via previous interaction with the manager) an entity reference that is suitable for a particular use-case. With this assumption, the host should not need to include additional data that is irrelevant for the use-case when publishing to an entity reference. That is, the host should not need to `resolve` data for a given entity reference for the sole purpose of immediately giving the data back again to the manager. \n", + "\n", + "This corresponds to an important philosophy of OpenAssetIO: when the user provides an entity reference, the host should not make assumptions about its provenance. It may be a reference to an existing entity, or a reference to a container, or a placeholder reference to an entity that doesn't exist yet, etc.\n", + "\n", + "As such, `entityTraits` should rarely appear in publishing code. However, there are use cases where it is valuable." + ] + }, + { + "cell_type": "markdown", + "id": "1bfdacf358dad9fa", + "metadata": { + "collapsed": false + }, + "source": [ + "### An empty trait set is a valid response\n", + "\n", + "BAL has no restrictions on publishing to new entity references. So what happens if we just make up an entity reference, and try to get the trait set for it?" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "c771cf01dc76f671", + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-03-13T07:57:00.145198Z", + "start_time": "2024-03-13T07:57:00.129786Z" + } + }, + "outputs": [ + { + "data": { + "text/markdown": "> **Result:**\n> `set()`" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "new_entity_ref = manager.createEntityReference(\"bal:///some/new/ref\")\n", + "\n", + "entity_trait_set = manager.entityTraits(new_entity_ref, EntityTraitsAccess.kWrite, context)\n", + "\n", + "helpers.display_result(entity_trait_set)" + ] + }, + { + "cell_type": "markdown", + "id": "2985087ccc829642", + "metadata": { + "collapsed": false + }, + "source": [ + "...an empty trait set is returned.\n", + "\n", + "If the manager does not know the trait set, or it's not applicable, the returned trait set may be empty. For example, as illustrated here, the entity reference may point to a new entity with no type constraints. " + ] + }, + { + "cell_type": "markdown", + "id": "8103bbcc9d83049c", + "metadata": { + "collapsed": false + }, + "source": [ + "### Deciding whether an entity reference is appropriate\n", + "\n", + "Let's say we are in a widget used for publishing 3D models. The user provides an entity reference they want to publish to. \n", + "\n", + "Since we deal with 3D models, we expect this entity reference to support publishing a trait set of `{EntityTrait.kId, GeometryTrait.kId, LocatableContentTrait.kId}`. But the user enters a reference to an image, not a 3D model..." + ] + }, + { + "cell_type": "code", + "outputs": [ + { + "data": { + "text/markdown": "> **Result:**\n> `{'openassetio-mediacreation:twoDimensional.Image', 'openassetio-mediacreation:usage.Entity', 'openassetio-mediacreation:content.LocatableContent'}`" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": "> **Result:**\n> `Does our widget support this entity? False`" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from openassetio_mediacreation.traits.usage import EntityTrait\n", + "from openassetio_mediacreation.traits.content import LocatableContentTrait\n", + "from openassetio_mediacreation.traits.threeDimensional import GeometryTrait\n", + "\n", + "entity_trait_set = manager.entityTraits(logo_ref, EntityTraitsAccess.kWrite, context)\n", + "\n", + "helpers.display_result(entity_trait_set)\n", + "\n", + "# Check if the minimal trait set required for publishing is satisfied by\n", + "# the trait set we're planning to publish.\n", + "is_entity_supported = entity_trait_set <= {EntityTrait.kId, GeometryTrait.kId, LocatableContentTrait.kId}\n", + "\n", + "helpers.display_result(f\"Does our widget support this entity? {is_entity_supported}\")" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-03-13T07:57:00.160710Z", + "start_time": "2024-03-13T07:57:00.146143Z" + } + }, + "id": "c5cf5d230e7bc4d6", + "execution_count": 8 + }, + { + "cell_type": "markdown", + "id": "5a0d4c198b48e907", + "metadata": { + "collapsed": false + }, + "source": [ + "As expected, the trait set of the logo is not contained within our expected trait set, so our widget could flag an error to the user at this point.\n", + "\n", + "A host could skip this check and try to publish regardless, reporting any error (from `preflight` or `register`) to the user. The `preflight` method is intended, in part, as a validation step, so this workflow is not unreasonable. However, doing this would likely provide a worse UX, since the widget could not be pre-emptively disabled." + ] + }, + { + "cell_type": "markdown", + "id": "a28c7f370dfc7260", + "metadata": { + "collapsed": false + }, + "source": [ + "#### Aside: the minimal trait set required for publishing \n", + "\n", + "In the previous example we see that the resulting trait set is smaller than that for `kRead`. This is because it is the minimal set of traits that the new entity _must_ possess when publishing to this entity reference.\n", + "\n", + "Therefore, this trait set _must_ be imbued in the `TraitsData` given to `preflight` and `register` during the publishing process.\n", + "\n", + "When registering new data, the full trait set defines what 'kind' of entity is being published, regardless of the specifics of what actual data is provided.\n", + "\n", + "As a consequence, the minimal trait set _does not_ indicate whether a manager is capable of storing or dictating their properties (e.g. providing a file path to save to). See relevant documentation for `managementPolicy` for more information on how to determine that.\n", + "\n", + "Again, many of these traits will not have properties associated with them at all. These must still be passed to `preflight`/`register` so that the manager knows the kind of entity you are publishing.\n", + "\n", + "So what happens if we forget to include all these traits?" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "77a3047239a3c76a", + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-03-13T07:57:00.175612Z", + "start_time": "2024-03-13T07:57:00.161761Z" + } + }, + "outputs": [ + { + "data": { + "text/markdown": "> **Result:**\n> `BatchElementError(ErrorCode.kInvalidTraitSet, 'Publishing to this entity requires traits that are missing from the input')`" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from openassetio.trait import TraitsData\n", + "from openassetio.access import PublishingAccess\n", + "\n", + "data = TraitsData()\n", + "LocatableContentTrait(data).setLocation(\"file:///some/path\")\n", + "\n", + "entity_ref_or_error = manager.preflight(\n", + " logo_ref, data, PublishingAccess.kWrite, context, manager.BatchElementErrorPolicyTag.kVariant)\n", + "\n", + "helpers.display_result(entity_ref_or_error)" + ] + }, + { + "cell_type": "markdown", + "id": "6c0963e48e593185", + "metadata": { + "collapsed": false + }, + "source": [ + "We get an `InvalidTraitSet` error if the provided trait set is not compatible, or sufficiently complete, for publishing to the entity reference." + ] + }, + { + "cell_type": "markdown", + "id": "71f2b1f16e08b7ae", + "metadata": { + "collapsed": false + }, + "source": [ + "## Errors when querying the trait set\n", + "\n", + "### Entities that don't exist (yet)\n", + "\n", + "For `kRead` access, the entity must exist:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "32f76d73bd99a340", + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-03-13T07:57:00.190404Z", + "start_time": "2024-03-13T07:57:00.176719Z" + } + }, + "outputs": [ + { + "data": { + "text/markdown": "> **Result:**\n> `BatchElementError(ErrorCode.kEntityResolutionError, 'Entity 'project_artwork/logos/new' not found')`" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "future_ref = manager.createEntityReference(\"bal:///project_artwork/logos/new\")\n", + "\n", + "trait_set_or_error = manager.entityTraits(\n", + " future_ref, EntityTraitsAccess.kRead, context, manager.BatchElementErrorPolicyTag.kVariant)\n", + "\n", + "helpers.display_result(trait_set_or_error)" + ] + }, + { + "cell_type": "markdown", + "id": "73b6ce44255392eb", + "metadata": { + "collapsed": false + }, + "source": [ + "If the entity doesn't exist we get an `EntityResolutionError`.\n", + "\n", + "On the other hand, this entity reference is fine for `kWrite`:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "1debd3dfd60cb52d", + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-03-13T07:57:00.205379Z", + "start_time": "2024-03-13T07:57:00.191556Z" + } + }, + "outputs": [ + { + "data": { + "text/markdown": "> **Result:**\n> `set()`" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "trait_set_or_error = manager.entityTraits(\n", + " future_ref, EntityTraitsAccess.kWrite, context, manager.BatchElementErrorPolicyTag.kVariant)\n", + "\n", + "helpers.display_result(trait_set_or_error)" + ] + }, + { + "cell_type": "markdown", + "id": "50f192ae83209477", + "metadata": { + "collapsed": false + }, + "source": [ + "Here, once again, we get the minimal trait set required for publishing to this entity reference. Since the entity doesn't exist, BAL will allow any traits to be published to it, so the minimal trait set is empty." + ] + }, + { + "cell_type": "markdown", + "id": "4b1ebe5d69a64d8f", + "metadata": { + "collapsed": false + }, + "source": [ + "### Read-only entities\n", + "\n", + "Let's say that we have a reference to the current best/approved logo asset, and this entity is read-only to prevent artists overwriting it. What happens if we query `entityTraits` with a `kWrite` access mode, indicating we're planning to publish to it regardless?\n" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "ee61dbb2bd4b828f", + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-03-13T07:57:00.220216Z", + "start_time": "2024-03-13T07:57:00.206542Z" + } + }, + "outputs": [ + { + "data": { + "text/markdown": "> **Result:**\n> `BatchElementError(ErrorCode.kEntityAccessError, 'Entity 'project_artwork/logos/openassetio/approved' is inaccessible for write')`" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "approved_ref = manager.createEntityReference(\"bal:///project_artwork/logos/openassetio/approved\")\n", + "\n", + "trait_set_or_error = manager.entityTraits(\n", + " approved_ref, EntityTraitsAccess.kWrite, context, manager.BatchElementErrorPolicyTag.kVariant)\n", + "\n", + "helpers.display_result(trait_set_or_error)" + ] + }, + { + "cell_type": "markdown", + "id": "fff41a2190e269b6", + "metadata": { + "collapsed": false + }, + "source": [ + "Since the entity is read-only, we get an `EntityAccessError`." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/resources/querying_entity_traits/bal_database.json b/examples/resources/querying_entity_traits/bal_database.json new file mode 100644 index 0000000..2d49c91 --- /dev/null +++ b/examples/resources/querying_entity_traits/bal_database.json @@ -0,0 +1,74 @@ +{ + "managementPolicy": { + "read": { + "default": { + "openassetio-mediacreation:managementPolicy.Managed": {}, + "openassetio-mediacreation:content.LocatableContent": {}, + "openassetio-mediacreation:identity.DisplayName": {}, + "openassetio-mediacreation:lifecycle.Version": {} + } + }, + "write": { + "default": { + "openassetio-mediacreation:managementPolicy.Managed": {}, + "openassetio-mediacreation:content.LocatableContent": {}, + "openassetio-mediacreation:identity.DisplayName": {} + } + }, + "managerDriven": { + "default": { + "openassetio-mediacreation:managementPolicy.Managed": {}, + "openassetio-mediacreation:content.LocatableContent": {}, + "openassetio-mediacreation:identity.DisplayName": {} + } + } + }, + "entities": { + "project_artwork/logos/openassetio": { + "versions": [], + "overrideByAccess": { + "read": { + "traits": { + "openassetio-mediacreation:usage.Entity": {}, + "openassetio-mediacreation:twoDimensional.Image": {}, + "openassetio-mediacreation:identity.DisplayName": { + "name": "openassetio logo" + }, + "openassetio-mediacreation:content.LocatableContent": { + "location": "file:///mnt/shows/artwork/logos/openassetio.png" + }, + "openassetio-mediacreation:timeDomain.FrameRanged": {} + } + }, + "write": { + "traits": { + "openassetio-mediacreation:usage.Entity": {}, + "openassetio-mediacreation:twoDimensional.Image": {}, + "openassetio-mediacreation:content.LocatableContent": { + "location": "file:///mnt/shows/artwork/logos/openassetio.png" + } + } + } + } + }, + "project_artwork/logos/openassetio/approved": { + "versions": [], + "overrideByAccess": { + "read": { + "traits": { + "openassetio-mediacreation:usage.Entity": {}, + "openassetio-mediacreation:twoDimensional.Image": {}, + "openassetio-mediacreation:identity.DisplayName": { + "name": "openassetio logo" + }, + "openassetio-mediacreation:content.LocatableContent": { + "location": "file:///mnt/shows/artwork/logos/openassetio.png" + }, + "openassetio-mediacreation:timeDomain.FrameRanged": {} + } + }, + "write": {} + } + } + } +} diff --git a/examples/resources/querying_entity_traits/openassetio_config.toml b/examples/resources/querying_entity_traits/openassetio_config.toml new file mode 100644 index 0000000..083d491 --- /dev/null +++ b/examples/resources/querying_entity_traits/openassetio_config.toml @@ -0,0 +1,5 @@ +[manager] +identifier = "org.openassetio.examples.manager.bal" + +[manager.settings] +library_path = "${config_dir}/bal_database.json" From 0530d4b442248d437458b9385151bef85483b898 Mon Sep 17 00:00:00 2001 From: David Feltell Date: Wed, 6 Mar 2024 15:43:44 +0000 Subject: [PATCH 2/2] [Docs] Add generic republish notebook Part of OpenAssetIO/OpenAssetIO#31. This notebook came as an appendix to illustrating usage of `entityTraits` in the original design notebook for that API method (see previous commit). However, during review it became apparent that a generic resolve and republish workflow is a distinct use-case, and not specific to `entityTraits`. So it was moved to its own notebook. Signed-off-by: David Feltell --- examples/generic_republish.ipynb | 286 ++++++++++++++++++ .../generic_republish/bal_database.json | 74 +++++ .../generic_republish/openassetio_config.toml | 5 + 3 files changed, 365 insertions(+) create mode 100644 examples/generic_republish.ipynb create mode 100644 examples/resources/generic_republish/bal_database.json create mode 100644 examples/resources/generic_republish/openassetio_config.toml diff --git a/examples/generic_republish.ipynb b/examples/generic_republish.ipynb new file mode 100644 index 0000000..904fc76 --- /dev/null +++ b/examples/generic_republish.ipynb @@ -0,0 +1,286 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "07297a3d-048b-496b-adb0-8fdd67316021", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "# Hosts: Generic republishing\n", + "\n", + "\n", + "This notebook presents the rare case where a manager needs to republish entity data without any knowledge of the provenance of the entity. This may be useful for a generic asset introspection and modification tool. Primarily, this notebook is an exploration of advanced interplay between the entity introspection and publishing APIs." + ] + }, + { + "cell_type": "markdown", + "id": "60bd9de5-64c3-4f15-8824-ff1d94a894d7", + "metadata": { + "editable": true, + "jp-MarkdownHeadingCollapsed": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## Setup\n", + "\n", + "See \"Hello OpenAssetIO\" notebook for details on how to bootstrap OpenAssetIO. " + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "df703c44ad53b21c", + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-03-13T08:15:00.152634Z", + "start_time": "2024-03-13T08:15:00.141508Z" + } + }, + "outputs": [], + "source": [ + "from resources import helpers\n", + "\n", + "manager, context = helpers.bootstrap(\"resources/generic_republish/openassetio_config.toml\")" + ] + }, + { + "cell_type": "markdown", + "id": "1a80594a-b171-49c5-8cfd-00e334759b1e", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## Getting started" + ] + }, + { + "cell_type": "markdown", + "source": [ + "In the following examples we're going to ask the manager about the traits it supports with respect to an asset, and use that to determine a publishing workflow, without knowing anything about the entity.\n", + "\n", + "For these examples, we'll make use of the `project_artwork/logos/openassetio` entity in the asset management system. We must first request an `EntityReference` object representing this entity, for use in API queries:" + ], + "metadata": { + "collapsed": false + }, + "id": "22277554-b477-4600-8c2a-90ee8d5c064e" + }, + { + "cell_type": "code", + "outputs": [ + { + "data": { + "text/markdown": "> **Result:**\n> ``" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "logo_ref = manager.createEntityReference(\"bal:///project_artwork/logos/openassetio\")\n", + "\n", + "helpers.display_result(repr(logo_ref))" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-03-13T08:15:00.193600Z", + "start_time": "2024-03-13T08:15:00.188399Z" + } + }, + "id": "82c3962a-27b5-4375-b4fa-dbd14dcfc93b", + "execution_count": 9 + }, + { + "cell_type": "markdown", + "source": [ + "## Generic resolve and re-publish\n", + "\n", + "A rare use-case is for a tool that blindly resolves, updates, and re-publishes an entity, regardless what type of entity it is. \n", + "\n", + "Such generic re-publishing is discouraged and dangerous. For example, if \"approval\" status is blindly resolved and re-published, the manager may not know how it should handle this properly. A better approach to such a generic tool is to present the traits and their properties to the user to select before re-publishing.\n", + "\n", + "However, such workflows are possible and have their place in a pipeline. This can be accomplished by making use of `entityTraits`, alongside other introspection and publishing methods. The following subsections explore two possible workflows, illustrating how generic re-publishing might be achieved. " + ], + "metadata": { + "collapsed": false + }, + "id": "9c4274b9aa5f4965" + }, + { + "cell_type": "markdown", + "source": [ + "### Re-publishing a mutated entity\n", + "\n", + "The following presents an example of blindly updating the display name for any entity. Note that we make use of the fact that managers should silently ignore data that it cannot persist when publishing." + ], + "metadata": { + "collapsed": false + }, + "id": "92a3d36ca0b4a816" + }, + { + "cell_type": "code", + "outputs": [], + "source": [ + "from openassetio.access import EntityTraitsAccess, PolicyAccess, ResolveAccess, PublishingAccess\n", + "from openassetio_mediacreation.traits.identity import DisplayNameTrait\n", + "\n", + "# Get the complete trait set of the entity.\n", + "entity_trait_set = manager.entityTraits(logo_ref, EntityTraitsAccess.kRead, context)\n", + "\n", + "# Ensure the manager will accept a publish of this entity with an updated display name.\n", + "[policy_data] = manager.managementPolicy(\n", + " [entity_trait_set | {DisplayNameTrait.kId}], PolicyAccess.kWrite, context)\n", + "\n", + "if not DisplayNameTrait.kId in policy_data.traitSet():\n", + " raise Exception(\"Cannot update display name of this entity\")\n", + "\n", + "# Get all the properties of the given entity.\n", + "data_to_publish = manager.resolve(logo_ref, entity_trait_set, ResolveAccess.kRead, context)\n", + "\n", + "# Any traits without properties, or where the manager cannot provide them, will be missing from the data.\n", + "# We still need to imbue those traits, so that manager knows what kind of entity we are publishing.\n", + "data_to_publish.addTraits(entity_trait_set)\n", + "\n", + "# Create/update the name of the entity.\n", + "DisplayNameTrait(data_to_publish).setName(\"My New Name\")\n", + "\n", + "# Publish it. Any properties we `resolve`d that cannot be re-published will be silently ignored.\n", + "updated_ref = manager.register(logo_ref, data_to_publish, PublishingAccess.kWrite, context)" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-03-13T08:15:00.233940Z", + "start_time": "2024-03-13T08:15:00.195824Z" + } + }, + "id": "499f2040f821e2cd", + "execution_count": 10 + }, + { + "cell_type": "markdown", + "id": "b0f6750002fc94ee", + "metadata": { + "collapsed": false + }, + "source": [ + "### Re-publishing a minimal entity\n", + "\n", + "The following example dives deeper into the interaction between `entityTraits`, `managementPolicy` and `resolve` for a generic re-publisher. \n", + "\n", + "We want to re-publish a minimal entity (i.e. only the traits absolutely required for the given entity reference), with two new/updated traits. One of the traits, `BearerTokenTrait`, might not be supported, and the other trait, `LocatableContentTrait`, might have its properties dictated by the manager." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "beab2b9a9c071302", + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-03-13T08:15:00.292376Z", + "start_time": "2024-03-13T08:15:00.235128Z" + } + }, + "outputs": [], + "source": [ + "from openassetio_mediacreation.traits.auth import BearerTokenTrait\n", + "from openassetio_mediacreation.traits.content import LocatableContentTrait\n", + "\n", + "# The minimum set of traits required to publish to this entity reference.\n", + "minimum_trait_set = manager.entityTraits(logo_ref, EntityTraitsAccess.kWrite, context)\n", + "\n", + "# Whatever the minimum trait set is, we know we want to publish a location and auth token.\n", + "desired_trait_set = minimum_trait_set | {BearerTokenTrait.kId, LocatableContentTrait.kId}\n", + "\n", + "# Get the set of traits that have properties the manager can persist.\n", + "[policy_for_desired_traits] = manager.managementPolicy(\n", + " [desired_trait_set], PolicyAccess.kWrite, context)\n", + "\n", + "# Filter down the desired traits to only those that are supported.\n", + "trait_set_to_publish = desired_trait_set & policy_for_desired_traits.traitSet()\n", + "\n", + "# We want to keep (the minimum amount of) data from the previous version, except for the values we're going to\n", + "# provide.\n", + "trait_set_to_keep = trait_set_to_publish - {BearerTokenTrait.kId, LocatableContentTrait.kId}\n", + "\n", + "# Get the properties that we wish to keep from the current version.\n", + "data_to_publish = manager.resolve(logo_ref, trait_set_to_keep, ResolveAccess.kRead, context)\n", + "\n", + "# Any traits without properties, or where the manager cannot provide them, will be missing from the data.\n", + "# We still need to imbue those traits, so that manager knows what kind of entity we are publishing.\n", + "data_to_publish.addTraits(minimum_trait_set)\n", + "\n", + "# Get the manager's policy for dictating trait properties, i.e. which traits the manager can \"drive\" for us.\n", + "[policy_for_derived_traits] = manager.managementPolicy(\n", + " [trait_set_to_publish], PolicyAccess.kManagerDriven, context)\n", + "\n", + "# Check if the manager can derive a location for us.\n", + "if LocatableContentTrait.kId in policy_for_derived_traits.traitSet():\n", + " # Imbue an empty LocatableContentTrait, so that the manager is aware in `preflight` that we intend to publish\n", + " # this trait. We will ask the manager to fill in the value for us before calling `register`.\n", + " LocatableContentTrait.imbueTo(data_to_publish)\n", + "else:\n", + " # If the manager doesn't want to provide a location for entities of this type, use a default.\n", + " LocatableContentTrait(data_to_publish).setLocation(\"file:///tmp/file\")\n", + "\n", + "# Manager might not support BearerTokenTrait.\n", + "if BearerTokenTrait.kId in trait_set_to_publish:\n", + " # BearerTokenTrait is supported, so imbue and configure.\n", + " BearerTokenTrait(data_to_publish).setToken(\"==ZxErn43G\")\n", + "\n", + "# We can now successfully begin the publishing process.\n", + "working_ref = manager.preflight(logo_ref, data_to_publish, PublishingAccess.kWrite, context)\n", + "\n", + "# Check if the manager can provide a location to us.\n", + "if LocatableContentTrait.kId in policy_for_derived_traits.traitSet():\n", + " derived_data = manager.resolve(\n", + " working_ref, {LocatableContentTrait.kId}, ResolveAccess.kManagerDriven, context)\n", + "\n", + " LocatableContentTrait(data_to_publish).setLocation(\n", + " LocatableContentTrait(derived_data).getLocation())\n", + " \n", + "# [Do some work to write the new file...]\n", + "\n", + "# We can now finally publish\n", + "updated_ref = manager.register(logo_ref, data_to_publish, PublishingAccess.kWrite, context)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/resources/generic_republish/bal_database.json b/examples/resources/generic_republish/bal_database.json new file mode 100644 index 0000000..2d49c91 --- /dev/null +++ b/examples/resources/generic_republish/bal_database.json @@ -0,0 +1,74 @@ +{ + "managementPolicy": { + "read": { + "default": { + "openassetio-mediacreation:managementPolicy.Managed": {}, + "openassetio-mediacreation:content.LocatableContent": {}, + "openassetio-mediacreation:identity.DisplayName": {}, + "openassetio-mediacreation:lifecycle.Version": {} + } + }, + "write": { + "default": { + "openassetio-mediacreation:managementPolicy.Managed": {}, + "openassetio-mediacreation:content.LocatableContent": {}, + "openassetio-mediacreation:identity.DisplayName": {} + } + }, + "managerDriven": { + "default": { + "openassetio-mediacreation:managementPolicy.Managed": {}, + "openassetio-mediacreation:content.LocatableContent": {}, + "openassetio-mediacreation:identity.DisplayName": {} + } + } + }, + "entities": { + "project_artwork/logos/openassetio": { + "versions": [], + "overrideByAccess": { + "read": { + "traits": { + "openassetio-mediacreation:usage.Entity": {}, + "openassetio-mediacreation:twoDimensional.Image": {}, + "openassetio-mediacreation:identity.DisplayName": { + "name": "openassetio logo" + }, + "openassetio-mediacreation:content.LocatableContent": { + "location": "file:///mnt/shows/artwork/logos/openassetio.png" + }, + "openassetio-mediacreation:timeDomain.FrameRanged": {} + } + }, + "write": { + "traits": { + "openassetio-mediacreation:usage.Entity": {}, + "openassetio-mediacreation:twoDimensional.Image": {}, + "openassetio-mediacreation:content.LocatableContent": { + "location": "file:///mnt/shows/artwork/logos/openassetio.png" + } + } + } + } + }, + "project_artwork/logos/openassetio/approved": { + "versions": [], + "overrideByAccess": { + "read": { + "traits": { + "openassetio-mediacreation:usage.Entity": {}, + "openassetio-mediacreation:twoDimensional.Image": {}, + "openassetio-mediacreation:identity.DisplayName": { + "name": "openassetio logo" + }, + "openassetio-mediacreation:content.LocatableContent": { + "location": "file:///mnt/shows/artwork/logos/openassetio.png" + }, + "openassetio-mediacreation:timeDomain.FrameRanged": {} + } + }, + "write": {} + } + } + } +} diff --git a/examples/resources/generic_republish/openassetio_config.toml b/examples/resources/generic_republish/openassetio_config.toml new file mode 100644 index 0000000..083d491 --- /dev/null +++ b/examples/resources/generic_republish/openassetio_config.toml @@ -0,0 +1,5 @@ +[manager] +identifier = "org.openassetio.examples.manager.bal" + +[manager.settings] +library_path = "${config_dir}/bal_database.json"