diff --git a/examples/hybrid_plugin_system.ipynb b/examples/hybrid_plugin_system.ipynb new file mode 100644 index 0000000..489c0a4 --- /dev/null +++ b/examples/hybrid_plugin_system.ipynb @@ -0,0 +1,402 @@ +{ + "cells": [ + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "# Hosts: Using hybrid plugins\n", + "\n", + "This notebook illustrates usage of the Hybrid Plugin System. We combine a Python manager plugin and a C++ manager plugin, and dispatch to the appropriate plugin based on its capabilities and priority. \n", + "\n", + "The primary use-case for this feature is to allow performance critical functionality to be written in performant C++, whilst less performance critical functionality can be written in more flexible Python.\n", + "\n", + "The hybrid plugin system is not limited to this use-case, however. You are free to combine any number of plugins in any supported language (only C++ and Python are available in the core library at time of writing). The hybrid plugin system also provides a more convenient abstraction for working with multiple plugin systems in general.\n", + "\n", + "## How it works\n", + "\n", + "Given a set of manager plugins, to be composed by the hybrid plugin system:\n", + "\n", + "* All plugins must use the same unique identifier.\n", + "* Each plugin is discovered by a different child `ManagerImplementationFactoryInterface` instance (e.g. `PythonPluginSystemManagerImplementationFactory` or `CppPluginSystemManagerImplementationFactory`).\n", + "* All the OpenAssetIO _required_ capabilities (i.e. entityReferenceIdentification, managementPolicyQueries, entityTraitIntrospection) must be satisfied by at least one of the plugins.\n", + "* Priority order matches the order that `ManagerImplementationFactoryInterface` instances are provided (e.g. `CppPluginSystemManagerImplementationFactory` before `PythonPluginSystemManagerImplementationFactory`).\n", + "* API calls will be routed to the first plugin that advertises it has the appropriate capability.\n", + "\n", + "Note that if only one child factory locates a plugin with the desired identifier, then that plugin is used directly. In this way, host applications making use of the hybrid plugin system don't lose out on any functionality or performance. As such, the hybrid plugin system should be used by default by most hosts, even if they don't make use of all of its functionality." + ], + "id": "6ebc020b4c31f1b8" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## Example", + "id": "959ede09b6700a1e" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Preamble\n", + "\n", + "First lets get some boilerplate out of the way. See the \"Hello OpenAssetIO\" notebook for more details." + ], + "id": "5b0c4ae1f1b914e6" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-07-16T14:34:25.634357Z", + "start_time": "2024-07-16T14:34:25.606468Z" + } + }, + "cell_type": "code", + "source": [ + "import os\n", + "\n", + "\n", + "try:\n", + " import openassetio\n", + " import openassetio_mediacreation\n", + "except ImportError:\n", + " print(\n", + " \"This notebook requires the packages listed in `resources/requirements.txt` to be installed\")\n", + " raise\n", + "\n", + "from resources import helpers\n", + "\n", + "from openassetio.hostApi import HostInterface, ManagerFactory\n", + "from openassetio.log import ConsoleLogger, SeverityFilter\n", + "\n", + "\n", + "class NotebookHostInterface(HostInterface):\n", + " def identifier(self):\n", + " return \"org.jupyter.notebook\"\n", + "\n", + " def displayName(self):\n", + " return \"Jupyter Notebook\"\n", + "\n", + "\n", + "host_interface = NotebookHostInterface()\n", + "\n", + "logger = SeverityFilter(ConsoleLogger())" + ], + "id": "db2739c3f0a96d70", + "outputs": [], + "execution_count": 1 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "\n", + "### The two example plugins\n", + "\n", + "In order to illustrate the hybrid plugin system, we'll make use of two ready-made example plugins - the \"Basic Asset Libary\" (aka BAL, a pure Python manager/plugin) and the \"Simple C++ Manager\" (aka SimpleCppManager, a pure C++ manager/plugin). \n", + "\n", + "BAL should be installed into the Python environment of this notebook (see `resources/requirements.txt`), and so will be trivially discoverable by OpenAssetIO.\n", + "\n", + "SimpleCppManager is more complex, and must be built with a compiler toolchain compatible with the OpenAssetIO libraries in the Python environment of this notebook. See `resources/hybrid_plugin_system/SimpleCppManager/README.md` for more details. We assume it is installed into `resources/hybrid_plugin_system/SimpleCppManager`, and will be discovered by adding this location to the standard `OPENASSETIO_PLUGIN_PATH` environment variable." + ], + "id": "791f52d06eea9aea" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-07-16T14:34:25.638224Z", + "start_time": "2024-07-16T14:34:25.635698Z" + } + }, + "cell_type": "code", + "source": [ + "os.environ[\"OPENASSETIO_PLUGIN_PATH\"] = os.path.join(\n", + " \"resources\", \"hybrid_plugin_system\", \"SimpleCppManager\")" + ], + "id": "e3a94c5be66ad34e", + "outputs": [], + "execution_count": 2 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "\n", + "\n", + "These two plugins each advertise a different unique identifier, so will not work with the hybrid plugin system out of the box. Luckily, they are both designed to be used in testing situations and are fully configurable. We can control the identifier that they will advertise using environment variables:\n" + ], + "id": "e19f60f60b11d7f9" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-07-16T14:34:25.643984Z", + "start_time": "2024-07-16T14:34:25.639388Z" + } + }, + "cell_type": "code", + "source": [ + "os.environ[\"OPENASSETIO_BAL_IDENTIFIER\"] = \"org.openassetio.examples.manager.hybrid\"\n", + "os.environ[\"OPENASSETIO_SIMPLECPPMANAGER_IDENTIFIER\"] = \"org.openassetio.examples.manager.hybrid\"" + ], + "id": "6c95ee177f2a9028", + "outputs": [], + "execution_count": 3 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "Other configuration is provided by the configuration file `resources/hybrid_plugin_system/openassetio_config.toml`. Note that with hybrid plugins, the same configuration file is used for all the consistent plugins - this will be revisited later.\n", + "\n", + "Let's try to initialise the two managers separately and see what happens.\n" + ], + "id": "2332a406713d0e76" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-07-16T14:34:25.666995Z", + "start_time": "2024-07-16T14:34:25.645711Z" + } + }, + "cell_type": "code", + "source": [ + "from openassetio.errors import ConfigurationException\n", + "from openassetio.pluginSystem import (\n", + " CppPluginSystemManagerImplementationFactory, PythonPluginSystemManagerImplementationFactory)\n", + "\n", + "\n", + "cpp_factory = CppPluginSystemManagerImplementationFactory(logger)\n", + "\n", + "try:\n", + " _ = ManagerFactory.defaultManagerForInterface(\n", + " \"resources/hybrid_plugin_system/openassetio_config.toml\",\n", + " host_interface,\n", + " cpp_factory,\n", + " logger)\n", + "\n", + "except ConfigurationException as exc:\n", + " helpers.display_result(f\"C++ plugin error: {exc}\")\n", + "\n", + "python_factory = PythonPluginSystemManagerImplementationFactory(logger)\n", + "\n", + "try:\n", + " _ = ManagerFactory.defaultManagerForInterface(\n", + " \"resources/hybrid_plugin_system/openassetio_config.toml\",\n", + " host_interface,\n", + " python_factory,\n", + " logger)\n", + "\n", + "except ConfigurationException as exc:\n", + " helpers.display_result(f\"Python plugin error: {exc}\")\n" + ], + "id": "129439ba58f8e81f", + "outputs": [ + { + "data": { + "text/markdown": "> **Result:**\n> `C++ plugin error: Manager implementation for 'org.openassetio.examples.manager.hybrid' does not support the required capabilities: managementPolicyQueries`" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": "> **Result:**\n> `Python plugin error: Manager implementation for 'org.openassetio.examples.manager.hybrid' does not support the required capabilities: entityReferenceIdentification`" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "execution_count": 4 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Neither of them advertise all the required capabilities! At least, on their own...", + "id": "9328df2151a26971" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### The hybrid plugin system\n", + "\n", + "Given the two `ManagerImplementationFactoryInterface` instances (`cpp_factory` and `python_factory`), we can create a hybrid factory." + ], + "id": "5fd4a729153276f9" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-07-16T14:34:25.670851Z", + "start_time": "2024-07-16T14:34:25.667896Z" + } + }, + "cell_type": "code", + "source": [ + "from openassetio.pluginSystem import HybridPluginSystemManagerImplementationFactory\n", + "\n", + "\n", + "hybrid_factory = HybridPluginSystemManagerImplementationFactory(\n", + " [cpp_factory, python_factory], logger)\n", + "\n", + "manager = ManagerFactory.defaultManagerForInterface(\n", + " \"resources/hybrid_plugin_system/openassetio_config.toml\",\n", + " host_interface,\n", + " hybrid_factory,\n", + " logger)" + ], + "id": "87c3d7e701b095d9", + "outputs": [], + "execution_count": 5 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Success! Now lets retrieve some data.", + "id": "52d907bb3a19db77" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-07-16T14:34:25.677445Z", + "start_time": "2024-07-16T14:34:25.671716Z" + } + }, + "cell_type": "code", + "source": [ + "from openassetio.access import ResolveAccess\n", + "from openassetio_mediacreation.traits.identity import DisplayNameTrait\n", + "\n", + "\n", + "context = manager.createContext()\n", + "entity_ref = manager.createEntityReference(\"examplehybrid:///project_artwork/logos/openassetio\")\n", + "\n", + "trait_data = manager.resolve(entity_ref, {DisplayNameTrait.kId}, ResolveAccess.kRead, context)\n", + "\n", + "helpers.display_result(DisplayNameTrait(trait_data).getName())" + ], + "id": "35b7fc091048a457", + "outputs": [ + { + "data": { + "text/markdown": "> **Result:**\n> `The OpenAssetIO Logo`" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "execution_count": 6 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "We know that data came from the Simple C++ Manager plugin, since that is the only plugin that advertised the \"resolution\" capability.\n", + "\n", + "But what if both plugins advertise the same capability? In this case, the first plugin in the list supplied to the `HybridPluginSystemManagerImplementationFactory` constructor will be used. To illustrate, lets query the entity's traits." + ], + "id": "85d2a4a633cc9e82" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-07-16T14:34:25.682641Z", + "start_time": "2024-07-16T14:34:25.678333Z" + } + }, + "cell_type": "code", + "source": [ + "from openassetio.access import EntityTraitsAccess\n", + "\n", + "\n", + "trait_set = manager.entityTraits(entity_ref, EntityTraitsAccess.kRead, context)\n", + "\n", + "helpers.display_result(trait_set)" + ], + "id": "683fed98e47a97a", + "outputs": [ + { + "data": { + "text/markdown": "> **Result:**\n> `{'openassetio-mediacreation:twoDimensional.Image', 'openassetio-mediacreation:usage.Entity', 'openassetio-mediacreation:identity.DisplayName'}`" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "execution_count": 7 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Now lets try a different configuration of plugins - note that this time `python_factory` is ordered before `cpp_factory`.", + "id": "76fa30f9d2f31969" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-07-16T14:34:25.698766Z", + "start_time": "2024-07-16T14:34:25.683435Z" + } + }, + "cell_type": "code", + "source": [ + "alt_hybrid_factory = HybridPluginSystemManagerImplementationFactory(\n", + " [python_factory, cpp_factory], logger)\n", + "\n", + "alt_manager = ManagerFactory.defaultManagerForInterface(\n", + " \"resources/hybrid_plugin_system/openassetio_config.toml\",\n", + " host_interface,\n", + " alt_hybrid_factory,\n", + " logger)\n", + "\n", + "trait_set = alt_manager.entityTraits(entity_ref, EntityTraitsAccess.kRead, context)\n", + "\n", + "helpers.display_result(trait_set)" + ], + "id": "e8008d8183fb262d", + "outputs": [ + { + "data": { + "text/markdown": "> **Result:**\n> `{'openassetio-mediacreation:usage.Entity', 'openassetio-mediacreation:timeDomain.FrameRanged', 'openassetio-mediacreation:identity.DisplayName', 'openassetio-mediacreation:lifecycle.Version', 'openassetio-mediacreation:content.LocatableContent', 'openassetio-mediacreation:twoDimensional.Image'}`" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "execution_count": 8 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "The trait set of the entity is different! \n", + "\n", + "This is because both BAL and SimpleCppManager support the entityTraitIntrospection capability, and in the `alt_manager` case the Python child factory was provided first in the list passed to the hybrid factory constructor. The plugin loaded by the Python factory therefore gets priority for API calls, and the value returned from `entityTraits` by BAL is different than the value returned by SimpleCppManager.\n", + "\n", + "Note that in real-world usage, it is strongly recommended that constituent plugins return the same results when they support the same capability!" + ], + "id": "1fb66b7c6fd38071" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/resources/hybrid_plugin_system/SimpleCppManager/README.md b/examples/resources/hybrid_plugin_system/SimpleCppManager/README.md new file mode 100644 index 0000000..bdb479d --- /dev/null +++ b/examples/resources/hybrid_plugin_system/SimpleCppManager/README.md @@ -0,0 +1,10 @@ +# Simple C++ Manager + +This directory is where the hybrid_plugin_system.ipynb notebook will +look for the SimpleCppManager plugin. + +In order for that notebook to run, the SimpleCppManager plugin must be +built and installed here. + +See the [SimpleCppManager README](https://github.com/OpenAssetIO/OpenAssetIO/blob/main/examples/manager/SimpleCppManager/README.md) +for more details. \ No newline at end of file diff --git a/examples/resources/hybrid_plugin_system/bal_database.json b/examples/resources/hybrid_plugin_system/bal_database.json new file mode 100644 index 0000000..f3894d2 --- /dev/null +++ b/examples/resources/hybrid_plugin_system/bal_database.json @@ -0,0 +1,42 @@ +{ + "capabilities": ["managementPolicyQueries","entityTraitIntrospection"], + "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": [ + { + "traits": { + "openassetio-mediacreation:usage.Entity": {}, + "openassetio-mediacreation:twoDimensional.Image": {}, + "openassetio-mediacreation:identity.DisplayName": {}, + "openassetio-mediacreation:content.LocatableContent": {}, + "openassetio-mediacreation:timeDomain.FrameRanged": {} + } + } + ] + } + } +} diff --git a/examples/resources/hybrid_plugin_system/openassetio_config.toml b/examples/resources/hybrid_plugin_system/openassetio_config.toml new file mode 100644 index 0000000..98d7314 --- /dev/null +++ b/examples/resources/hybrid_plugin_system/openassetio_config.toml @@ -0,0 +1,44 @@ +[manager] +# Identifier advertised by BAL and SimpleCppManager, overridden using +# environment variables. +identifier = "org.openassetio.examples.manager.hybrid" + +[manager.settings] + +######################################################################## +# BAL settings + +# BAL JSON library containing its settings/database. +# +# In the JSON config file we: +# * Set the capabilities to not advertise entityReferenceIdentification, +# or resolution, since SimpleCppManager will satisfy those +# capabilities (see below). +# * Note that entityTraitIntrospection is advertised by both managers, +# so which manager is used for `entityTraits(...)` API calls depends +# on priority order. +library_path = "${config_dir}/bal_database.json" + +# Entity reference format should match. Overridden similarly for +# SimpleCppManager, below. +entity_reference_url_scheme = "examplehybrid" + +######################################################################## +# SimpleCppManager settings: + +# Entity reference format should match. Overridden similarly for BAL, +# above. +prefix = "examplehybrid:///" + +# SimpleCppManager does not advertise "managementPolicyQueries", which +# is a required capability for a manager. But when used as part of a +# hybrid plugin, the capability can be satisfied by the other +# constituent plugins(s). +capabilities = "entityReferenceIdentification,resolution,entityTraitIntrospection" + +# The database of entities, as regurgitated by SimpleCppManager. +read_traits = ''' +examplehybrid:///project_artwork/logos/openassetio,openassetio-mediacreation:usage.Entity +examplehybrid:///project_artwork/logos/openassetio,openassetio-mediacreation:twoDimensional.Image +examplehybrid:///project_artwork/logos/openassetio,openassetio-mediacreation:identity.DisplayName,name,The OpenAssetIO Logo +''' \ No newline at end of file