From 0f3d4bbee72108c82907cc25683acdd4eed1ef7e Mon Sep 17 00:00:00 2001 From: David Feltell Date: Fri, 30 Aug 2024 18:58:55 +0100 Subject: [PATCH] [Docs] Add hybrid plugin system notebook Part of OpenAssetIO/OpenAssetIO#1202. Add a Jupyter Notebook illustrating the design and usage of the hybrid plugin system. Bump the minimum versions of OpenAssetIO and BAL in `requirements.txt` to support the required features for the notebook to run. Use a custom hybrid plugin created especially for the notebook, rather than attempting to sellotape existing managers together (e.g. BAL and SimpleCppManager). This allows the notebook to avoid potentially confusing readers with misleading text about configuring two completely independent plugins to work together. For the custom plugin sources, use linter config lifted from the main OpenAssetIO repo, with little modification. Signed-off-by: David Feltell --- .github/build_openassetio/action.yml | 17 +- .github/workflows/examples.yml | 56 ++- .github/workflows/test.yml | 29 +- examples/hybrid_plugin_system.ipynb | 457 ++++++++++++++++++ .../SimpleHybridManager/.clang-format | 5 + .../SimpleHybridManager/.clang-tidy | 75 +++ .../SimpleHybridManager/README.md | 19 + .../SimpleHybridManager/cmake-format.yaml | 44 ++ .../linter-requirements.txt | 10 + .../PyComponentOfSimpleHybridManager.py | 141 ++++++ .../SimpleHybridManager/pyproject.toml | 39 ++ .../SimpleHybridManager/src/CMakeLists.txt | 71 +++ .../src/CppComponentOfSimpleHybridManager.cpp | 130 +++++ .../openassetio_config.toml | 4 + examples/resources/requirements.txt | 4 +- 15 files changed, 1067 insertions(+), 34 deletions(-) create mode 100644 examples/hybrid_plugin_system.ipynb create mode 100755 examples/resources/hybrid_plugin_system/SimpleHybridManager/.clang-format create mode 100644 examples/resources/hybrid_plugin_system/SimpleHybridManager/.clang-tidy create mode 100644 examples/resources/hybrid_plugin_system/SimpleHybridManager/README.md create mode 100644 examples/resources/hybrid_plugin_system/SimpleHybridManager/cmake-format.yaml create mode 100644 examples/resources/hybrid_plugin_system/SimpleHybridManager/linter-requirements.txt create mode 100644 examples/resources/hybrid_plugin_system/SimpleHybridManager/plugin/PyComponentOfSimpleHybridManager.py create mode 100644 examples/resources/hybrid_plugin_system/SimpleHybridManager/pyproject.toml create mode 100644 examples/resources/hybrid_plugin_system/SimpleHybridManager/src/CMakeLists.txt create mode 100644 examples/resources/hybrid_plugin_system/SimpleHybridManager/src/CppComponentOfSimpleHybridManager.cpp create mode 100644 examples/resources/hybrid_plugin_system/openassetio_config.toml diff --git a/.github/build_openassetio/action.yml b/.github/build_openassetio/action.yml index 0e34aa6..68cd603 100644 --- a/.github/build_openassetio/action.yml +++ b/.github/build_openassetio/action.yml @@ -7,6 +7,10 @@ name: Build OpenAssetIO description: Builds OpenAssetIO and publishes an artifact +inputs: + install-prefix: + description: Where to install OpenAssetIO once built + required: true runs: using: "composite" steps: @@ -20,13 +24,6 @@ runs: - name: Build OpenAssetIO shell: bash run: | - cd openassetio-checkout - mkdir build - cmake -G Ninja -S . -B build - cmake --build build - cmake --install build - - uses: actions/upload-artifact@v3 - with: - name: OpenAssetIO Build - path: openassetio-checkout/build/dist - retention-days: 1 + cmake -G Ninja -S openassetio-checkout -B openassetio-checkout/build + cmake --build openassetio-checkout/build + cmake --install openassetio-checkout/build --prefix ${{ inputs.install-prefix }} diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 23777ca..6c6ee7e 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -18,6 +18,9 @@ jobs: matrix: os: ["windows-2022", "ubuntu-22.04", "macos-13"] python: ["3.10", "3.11"] + defaults: + run: + shell: bash steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 @@ -28,4 +31,55 @@ jobs: python -m pip install . python -m pip install -r examples/resources/requirements.txt - name: Test Notebooks - run: jupyter nbconvert --to html --execute examples/*.ipynb + # Execute all the notebooks apart from the Hybrid Plugin System, + # which requires a build of SimpleCppManager + run: > + find examples -maxdepth 1 -name "*.ipynb" + ! -name "hybrid_plugin_system.ipynb" + -exec jupyter nbconvert --to html --execute {} \; + + test-cpp-notebooks: + # A special job just for the Hybrid Plugin System because it needs a + # C++ build. + name: Test Hybrid Plugin System notebook + runs-on: ubuntu-latest + container: + image: ghcr.io/openassetio/openassetio-build + env: + SIMPLEHYBRIDMANAGER_SUBDIR: examples/resources/hybrid_plugin_system/SimpleHybridManager + steps: + - uses: actions/checkout@v3 + - name: Install Dependencies + run: | + python -m pip install . + python -m pip install -r examples/resources/requirements.txt + python -m pip install openassetio-traitgen + + - name: Build/install OpenAssetIO + uses: ./.github/build_openassetio + with: + install-prefix: ${{ env.SIMPLEHYBRIDMANAGER_SUBDIR }}/dependencies + + - name: Build/install MediaCreation + run: | + cmake -S . -B build -G Ninja + cmake --build build + cmake --install build --prefix $SIMPLEHYBRIDMANAGER_SUBDIR/dependencies + + - name: Build/install SimpleHybridManager + run: | + cmake -S src -B build -G Ninja + cmake --build build + cmake --install build --prefix plugin + env: + CMAKE_PREFIX_PATH: dependencies + # Since we're in a Docker container, `github.workspace` doesn't + # give the correct location, except, for some reason, when used + # in a `working-directory` option. See + # https://github.com/actions/runner/issues/2058#issuecomment-1541828550 + working-directory: ${{ github.workspace }}/${{ env.SIMPLEHYBRIDMANAGER_SUBDIR }} + + - name: Test notebook + run: jupyter nbconvert --to html --execute examples/hybrid_plugin_system.ipynb + + diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b971612..1127b7b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,16 +11,6 @@ concurrency: cancel-in-progress: true jobs: - build-openassetio: - name: Build OpenAssetIO - runs-on: ubuntu-latest - container: - image: ghcr.io/openassetio/openassetio-build - steps: - - uses: actions/checkout@v3 - - name: Build - uses: ./.github/build_openassetio - test-python: name: "${{ matrix.os }} python-${{ matrix.python }}" runs-on: ${{ matrix.os }} @@ -43,25 +33,22 @@ jobs: test-cpp: name: Test Cpp runs-on: ubuntu-latest - needs: build-openassetio container: - image: aswf/ci-base:2024 + image: ghcr.io/openassetio/openassetio-build steps: - uses: actions/checkout@v3 + - name: Build OpenAssetIO + uses: ./.github/build_openassetio + with: + install-prefix: openassetio + - name: Install Traitgen run: python -m pip install openassetio-traitgen==1.0.0a10 - - name: Get OpenAssetIO - uses: actions/download-artifact@v3 - with: - name: OpenAssetIO Build - path: ./openassetio-build - - name: Configure CMake build - run: > - cmake -S . -DCMAKE_PREFIX_PATH=`pwd`/openassetio-build -B build -G Ninja - --preset test + run: | + cmake -S . -DCMAKE_PREFIX_PATH=$(pwd)/openassetio -B build -G Ninja --preset test - name: Build tests run: cmake --build build diff --git a/examples/hybrid_plugin_system.ipynb b/examples/hybrid_plugin_system.ipynb new file mode 100644 index 0000000..281aa2b --- /dev/null +++ b/examples/hybrid_plugin_system.ipynb @@ -0,0 +1,457 @@ +{ + "cells": [ + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "# Hybrid Plugin System\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 also provides a more convenient abstraction for working with multiple plugin systems in general. It should be the default choice by most host applications (i.e. where a Python environment is available).\n", + "\n", + "## How it works\n", + "\n", + "The hybrid plugin system allows a manager to split its implementation between multiple languages. Under the hood, each language is loaded using its own plugin system (currently, C++ and Python are supported). It combines plugins that share the same identifier so they can be used by a host as if they were a single implementation. Calls are routed to one of the underlying implementations based on capabilities and the language priority set when the factory is made.\n", + "\n", + "This allows high call count methods such as `resolve` to be implemented in C++ to ensure maximum performance, whilst less frequently used methods such as `register` remain in more flexible Python.\n", + "\n", + "The following subsection dives into some more detail of how this works.\n", + "\n", + "### Details\n", + "\n", + "OpenAssetIO manager plugins must advertise a unique identifier. An OpenAssetIO plugin system (e.g. C++ or Python) maps unique identifiers to a plugin instance. A consequence of this is if multiple plugins advertise the same unique identifier, only one of those plugins can be chosen _by that plugin system_. However, if multiple plugin systems are in use,\n", + "then each plugin system has their own mapping of unique identifier to plugin instance. This means there _can_ be multiple plugins with the same identifier, as long as they are discovered by different plugin systems.\n", + "\n", + "This is the essence of how the hybrid plugin system discovers plugins. If two (or more) plugins from two (or more) different plugin systems advertise the same unique identifier, then we assume they are related and can be composed.\n", + "\n", + "The hybrid plugin system therefore wraps a list of child plugin systems, such that they present as a single plugin system to the host.\n", + "\n", + "Given that the hybrid plugin system has discovered two (or more) composable plugins, we then need a mechanism to dispatch API calls to the appropriate plugin. I.e. we need a way to choose which plugin is the best to use for a particular API call. This is where the `hasCapability` API method comes in.\n", + "\n", + "OpenAssetIO API methods are grouped under \"capabilities\", e.g. `\"resolution\"`, `\"publishing\"`, `\"relationshipQueries\"`, etc (these are stringified representations of the `Capability` enumeration). A manager plugin advertises which capabilities it supports by overriding the `hasCapability` method of the base `ManagerInterface` class.\n", + "\n", + "Therefore, we can dispatch an API call to the appropriate manager plugin by finding the plugin that advertises the associated capability for that API call.\n", + "\n", + "If multiple plugins advertise that they support the required capability, then which plugin to use is determined by the original order that the child plugin systems were provided to the hybrid plugin system. For example, if the hybrid plugin system was constructed with a list containing the C++ plugin system followed by the Python plugin system, and C++ and Python plugins have been discovered and composed, and both advertise that they support the required capability for a particular API call, then the C++ plugin will be chosen for that API call.\n", + "\n", + "All the OpenAssetIO _required_ capabilities (i.e. `\"entityReferenceIdentification\"`, `\"managementPolicyQueries\"`, `\"entityTraitIntrospection\"`) must be satisfied by at least one of the composed plugins.\n", + "\n", + "If only one child factory locates a plugin with the desired identifier, then that plugin is used directly (i.e. the plugin is not wrapped). In this way, host applications making use of the hybrid plugin system don't lose out on any functionality or performance. " + ], + "id": "6ebc020b4c31f1b8" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "#### A note on information sharing\n", + "\n", + "Having two plugins in completely different languages, which logically form a single plugin, raises the question of how data should be shared between them.\n", + "\n", + "OpenAssetIO has a mechanism to help with this via the `Context` object. A `Context` instance is passed to (almost) every API method. A well-behaved host will re-use the same `Context` for all requests in the same logical process (typically an application session). \n", + "\n", + "The `Context` object holds a `managerState` object, which can be used to communicate arbitrary information between the plugins. See the [API documentation](http://docs.openassetio.org/OpenAssetIO/stable_resolution.html#stable_resolution_manager_state) for more information.\n", + "\n", + "Populating the manager state in a way that can be read by both Python and C++ is left as an exercise to the reader. It's likely that the C++ plugin will require CPython as a dependency in order to translate between languages." + ], + "id": "25aa01663aa0e7c3" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## Example", + "id": "959ede09b6700a1e" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "### Preamble", + "id": "5b0c4ae1f1b914e6" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Reading through the following is not necessary to be able to understand how to use the hybrid plugin system, and can be safely skipped. Let's get the standard OpenAssetIO bootstrapping boilerplate out of the way. See the \"Hello OpenAssetIO\" notebook for more details.", + "id": "a80b0202bf806edb" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-09-25T10:51:56.201797Z", + "start_time": "2024-09-25T10:51:56.171156Z" + } + }, + "cell_type": "code", + "source": [ + "\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 LoggerInterface\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", + "class NullLogger(LoggerInterface):\n", + " def log(self, _severity, _message):\n", + " pass\n", + "\n", + "\n", + "host_interface = NotebookHostInterface()\n", + "\n", + "logger = NullLogger()" + ], + "id": "db2739c3f0a96d70", + "outputs": [], + "execution_count": 1 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "#### The example plugin(s)", + "id": "db3177717e0b2174" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "\n", + "In order to illustrate the hybrid plugin system, we'll make use of a super simple example hybrid plugin created just for this notebook, available in `resources/hybrid_plugin_system/SimpleHybridManager`.\n", + "\n", + "The Python plugin component is trivially available. However, the C++ plugin component 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/SimpleHybridManager/README.md` for more details. \n", + "\n", + "We assume both the C++ and Python plugin components are installed into `resources/hybrid_plugin_system/SimpleHybridManager/plugin`, and will be discovered by adding this location to the standard `OPENASSETIO_PLUGIN_PATH` environment variable." + ], + "id": "791f52d06eea9aea" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-09-25T10:51:56.209189Z", + "start_time": "2024-09-25T10:51:56.206563Z" + } + }, + "cell_type": "code", + "source": [ + "import os\n", + "\n", + "\n", + "os.environ[\"OPENASSETIO_PLUGIN_PATH\"] = os.path.join(\n", + " \"resources\", \"hybrid_plugin_system\", \"SimpleHybridManager\", \"plugin\")" + ], + "id": "e3a94c5be66ad34e", + "outputs": [], + "execution_count": 2 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "### Plugins designed for composition", + "id": "398b79c83304c214" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Let's try to initialise our C++ and Python example managers separately and see what happens.\n", + "id": "23f7dfb47f211322" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-09-25T10:51:56.339454Z", + "start_time": "2024-09-25T10:51:56.323959Z" + } + }, + "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", + " cpp_manager = 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", + "py_manager = ManagerFactory.defaultManagerForInterface(\n", + " \"resources/hybrid_plugin_system/openassetio_config.toml\",\n", + " host_interface,\n", + " python_factory,\n", + " logger)" + ], + "id": "129439ba58f8e81f", + "outputs": [ + { + "data": { + "text/markdown": "> **Result:**\n> `C++ plugin error: Manager implementation for 'org.openassetio.examples.simplehybridmanager' does not support the required capabilities: entityReferenceIdentification, managementPolicyQueries, entityTraitIntrospection`" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "execution_count": 3 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "So the C++ plugin system found a plugin, but it doesn't support any of the required capabilities.\n", + "\n", + "We had better luck with the Python plugin system. However, we're going to want to `resolve` an entity. Is the Python plugin capable of resolution?" + ], + "id": "9328df2151a26971" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-09-25T10:51:56.379374Z", + "start_time": "2024-09-25T10:51:56.375898Z" + } + }, + "cell_type": "code", + "source": [ + "can_resolve = py_manager.hasCapability(py_manager.Capability.kResolution)\n", + "\n", + "helpers.display_result(f\"Can resolve? {can_resolve}\")" + ], + "id": "27535337eda9f49e", + "outputs": [ + { + "data": { + "text/markdown": "> **Result:**\n> `Can resolve? False`" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "execution_count": 4 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "So no, the Python plugin does not support the `resolve` method, at least on its own...", + "id": "8b67117d2f42a87e" + }, + { + "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-09-25T10:51:56.431456Z", + "start_time": "2024-09-25T10:51:56.428572Z" + } + }, + "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! \n", + "\n", + "Notice how only a single config file (`openassetio_config.toml`) was provided. With hybrid plugins, the same configuration file is used for all the constituent plugins. In particular, any and all manager settings specified in the config file are passed to all plugins during initialisation.\n", + "\n", + "Is this combined hybrid plugin now capable of resolution?" + ], + "id": "4924fc6cb8e0620f" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-09-25T10:51:56.490738Z", + "start_time": "2024-09-25T10:51:56.487400Z" + } + }, + "cell_type": "code", + "source": [ + "can_resolve = manager.hasCapability(py_manager.Capability.kResolution)\n", + "\n", + "helpers.display_result(f\"Can resolve? {can_resolve}\")" + ], + "id": "2869873c40ecc805", + "outputs": [ + { + "data": { + "text/markdown": "> **Result:**\n> `Can resolve? True`" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "execution_count": 6 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "Again, success! The resolution capability from the C++ plugin has been combined with the capabilities of the Python plugin. \n", + "\n", + "Note that since the `resolve` method is implemented in the C++ plugin, the Python GIL will be released when calling this method, allowing Python threads to continue whilst the `resolve` call is processed. In particular, since many UIs are written in Python, allowing (multiple/batch) `resolve` calls to run in a separate thread, without holding the Python GIL, can prevent nasty UI lockups." + ], + "id": "52d907bb3a19db77" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "\n", + "Now lets retrieve an entity's trait set from the manager:" + ], + "id": "e12f1acc9f62324a" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-09-25T10:51:56.544062Z", + "start_time": "2024-09-25T10:51:56.540234Z" + } + }, + "cell_type": "code", + "source": [ + "from openassetio.access import EntityTraitsAccess\n", + "\n", + "\n", + "context = manager.createContext()\n", + "entity_ref = manager.createEntityReference(\"examplehybrid://example_entity\")\n", + "\n", + "trait_set = manager.entityTraits(entity_ref, EntityTraitsAccess.kRead, context)\n", + "\n", + "helpers.display_result(trait_set)\n" + ], + "id": "35b7fc091048a457", + "outputs": [ + { + "data": { + "text/markdown": "> **Result:**\n> `{'openassetio-mediacreation:usage.Entity', 'openassetio-mediacreation:content.LocatableContent'}`" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "execution_count": 7 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "So the entity has the `LocatableContent` trait. Let's `resolve` its location:\n", + "id": "85d2a4a633cc9e82" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-09-25T10:51:56.596321Z", + "start_time": "2024-09-25T10:51:56.592109Z" + } + }, + "cell_type": "code", + "source": [ + "from openassetio.access import ResolveAccess\n", + "from openassetio_mediacreation.traits.content import LocatableContentTrait\n", + "\n", + "\n", + "trait_data = manager.resolve(entity_ref, {LocatableContentTrait.kId}, ResolveAccess.kRead, context)\n", + "\n", + "url = LocatableContentTrait(trait_data).getLocation()\n", + "\n", + "helpers.display_result(url)\n" + ], + "id": "af6d8d7ec842c204", + "outputs": [ + { + "data": { + "text/markdown": "> **Result:**\n> `file:///some/path.exr`" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "execution_count": 8 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "Success! Pretty straightforward.\n", + " \n", + "For further reading, if you inspect the C++ implementation at `resources/hybrid_plugin_system/SimpleHybridManager/src/CppComponentOfSimpleHybridManager.cpp` you'll find no implementation of the `entityTraits` method, or indeed any non-trivial method, other than `resolve`. \n", + "\n", + "Similarly, if you inspect the Python implementation at `resources/hybrid_plugin_system/SimpleHybridManager/plugin/PyComponentOfSimpleHybridManager.py` you'll find no implementation of the `resolve` method. \n", + "\n", + "So the two plugins have been seamlessly combined into a single interface." + ], + "id": "be76095fb0d00046" + } + ], + "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/SimpleHybridManager/.clang-format b/examples/resources/hybrid_plugin_system/SimpleHybridManager/.clang-format new file mode 100755 index 0000000..5873ad1 --- /dev/null +++ b/examples/resources/hybrid_plugin_system/SimpleHybridManager/.clang-format @@ -0,0 +1,5 @@ +BasedOnStyle: Google +Language: Cpp +Standard: c++17 +ColumnLimit: 99 +IncludeBlocks: Preserve diff --git a/examples/resources/hybrid_plugin_system/SimpleHybridManager/.clang-tidy b/examples/resources/hybrid_plugin_system/SimpleHybridManager/.clang-tidy new file mode 100644 index 0000000..055b52f --- /dev/null +++ b/examples/resources/hybrid_plugin_system/SimpleHybridManager/.clang-tidy @@ -0,0 +1,75 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2022 Google LLC +# Copyright 2024 The Foundry Visionmongers Ltd + +# Modified from: https://raw.githubusercontent.com/googleapis/google-cloud-cpp/main/.clang-tidy +# See: https://releases.llvm.org/10.0.0/tools/clang/tools/extra/docs/clang-tidy/checks/list.html +--- +# Configure clang-tidy for this project. + +# Here is an explanation for why some of the checks are disabled: +# +# -modernize-use-trailing-return-type: clang-tidy recommends using +# `auto Foo() -> std::string { return ...; }`, we think the code is less +# readable in this form. +# +# -modernize-return-braced-init-list: We think removing typenames and using +# only braced-init can hurt readability. +# +# -modernize-avoid-c-arrays: We only use C arrays when they seem to be the +# right tool for the job, such as `char foo[] = "hello"`. In these cases, +# avoiding C arrays often makes the code less readable, and std::array is +# not a drop-in replacement because it doesn't deduce the size. +# +# -google-runtime-references: Allow usage of non-const references as +# function parameters. Otherwise we'd have to use pointers, which +# cpp core guidelines recommends against unless the parameter is +# nullable: https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#f60-prefer-t-over-t-when-no-argument-is-a-valid-option +# +# +Checks: > + -*, + bugprone-*, + google-*, + misc-*, + modernize-*, + performance-*, + portability-*, + readability-*, + -modernize-return-braced-init-list, + -modernize-use-trailing-return-type, + -modernize-avoid-c-arrays, + +# Turn all the warnings from the checks above into errors. +WarningsAsErrors: "*" +# Scan all (non-system) headers. +HeaderFilterRegex: '.*' +# Use .clang-format for fix suggestions. +FormatStyle: file + +CheckOptions: + - { key: readability-identifier-naming.NamespaceCase, value: camelBack } + - { key: readability-identifier-naming.ClassCase, value: CamelCase } + - { key: readability-identifier-naming.StructCase, value: CamelCase } + - { key: readability-identifier-naming.TemplateParameterCase, value: CamelCase } + - { key: readability-identifier-naming.FunctionCase, value: camelBack } + - { key: readability-identifier-naming.VariableCase, value: camelBack } + - { key: readability-identifier-naming.VariableIgnoredRegexp, value: "^_[0-9]+$"} + - { key: readability-identifier-naming.ClassMemberCase, value: camelBack } + - { key: readability-identifier-naming.PrivateMemberSuffix, value: _ } + - { key: readability-identifier-naming.ProtectedMemberSuffix, value: _ } + - { key: readability-identifier-naming.EnumConstantCase, value: CamelCase } + - { key: readability-identifier-naming.EnumConstantPrefix, value: k } + - { key: readability-identifier-naming.ConstexprVariableCase, value: CamelCase } + - { key: readability-identifier-naming.ConstexprVariablePrefix, value: k } + - { key: readability-identifier-naming.GlobalConstantCase, value: CamelCase } + - { key: readability-identifier-naming.GlobalConstantPrefix, value: k } + - { key: readability-identifier-naming.MemberConstantCase, value: CamelCase } + - { key: readability-identifier-naming.MemberConstantPrefix, value: k } + - { key: readability-identifier-naming.StaticConstantCase, value: CamelCase } + - { key: readability-identifier-naming.StaticConstantPrefix, value: k } + - { key: readability-implicit-bool-conversion.AllowIntegerConditions, value: 1 } + - { key: readability-implicit-bool-conversion.AllowPointerConditions, value: 1 } + # Allow structs where (all) member variables are public, even if + # the struct has member functions. + - { key: misc-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic, value: 1 } diff --git a/examples/resources/hybrid_plugin_system/SimpleHybridManager/README.md b/examples/resources/hybrid_plugin_system/SimpleHybridManager/README.md new file mode 100644 index 0000000..3683415 --- /dev/null +++ b/examples/resources/hybrid_plugin_system/SimpleHybridManager/README.md @@ -0,0 +1,19 @@ +# Simple Hybrid Manager + +This directory contains a very simple OpenAssetIO hybrid C++/Python +manager plugin. It is used in the _Hybrid Plugin System_ Jupyter +notebook. + +## C++ component + +The C++ component of the plugin is provided as sources under the `src` +directory, and so must be built before it can be used. + +It has OpenAssetIO and OpenAssetIO-MediaCreation as CMake dependencies, +so these projects need to be built and installed somewhere discoverable +by CMake. + +For the Jupyter Notebook to run, the resulting `.so`/`.dll` must be +placed in the `plugin` directory. This can be done using `cmake +--install` and setting `--install-prefix`/`--prefix` to the `plugin` +directory; or by simply copying the file from the build directory. \ No newline at end of file diff --git a/examples/resources/hybrid_plugin_system/SimpleHybridManager/cmake-format.yaml b/examples/resources/hybrid_plugin_system/SimpleHybridManager/cmake-format.yaml new file mode 100644 index 0000000..dd37e5f --- /dev/null +++ b/examples/resources/hybrid_plugin_system/SimpleHybridManager/cmake-format.yaml @@ -0,0 +1,44 @@ +_help_format: Options affecting formatting. +format: + _help_line_width: + - How wide to allow formatted cmake files + line_width: 99 + _help_tab_size: + - How many spaces to tab for indent + tab_size: 4 +_help_lint: Options affecting the linter +lint: + _help_disabled_codes: + - function( list of lint codes to disable + - C0113, Missing COMMENT in statement which allows it + disabled_codes: ["C0113"] + _help_function_pattern: + - regular expression pattern describing valid function names + function_pattern: "[0-9a-z_]+" + _help_macro_pattern: + - regular expression pattern describing valid macro names + macro_pattern: "[0-9a-z_]+" + _help_global_var_pattern: + - regular expression pattern describing valid names for + - variables with global (cache) scope + global_var_pattern: "[A-Z][0-9A-Z_]+" + _help_internal_var_pattern: + - regular expression pattern describing valid names for + - variables with global scope (but internal semantic) + internal_var_pattern: _[A-Z][0-9A-Z_]+ + _help_local_var_pattern: + - regular expression pattern describing valid names for + - variables with local scope + local_var_pattern: "[a-z][a-z0-9_]+" + _help_private_var_pattern: + - regular expression pattern describing valid names for + - private directory variables + private_var_pattern: _[0-9a-z_]+ + _help_public_var_pattern: + - regular expression pattern describing valid names for public + - directory variables + public_var_pattern: "[A-Z][0-9A-Z_]+" + _help_argument_var_pattern: + - regular expression pattern describing valid names for + - function/macro arguments and loop variables. + argument_var_pattern: "[a-z][a-z0-9_]+" diff --git a/examples/resources/hybrid_plugin_system/SimpleHybridManager/linter-requirements.txt b/examples/resources/hybrid_plugin_system/SimpleHybridManager/linter-requirements.txt new file mode 100644 index 0000000..84c2cf6 --- /dev/null +++ b/examples/resources/hybrid_plugin_system/SimpleHybridManager/linter-requirements.txt @@ -0,0 +1,10 @@ +# Python auto-formatter. +black==24.8.0 +# Python linter. +pylint==3.2.7 +# pflake8 - flake8 PEP8 linter with settings from pyproject.toml. +pyproject-flake8==7.0.0 +# CMake linter. +cmakelang==0.6.13 +# cmakelang dependency. +pyyaml==6.0.0 \ No newline at end of file diff --git a/examples/resources/hybrid_plugin_system/SimpleHybridManager/plugin/PyComponentOfSimpleHybridManager.py b/examples/resources/hybrid_plugin_system/SimpleHybridManager/plugin/PyComponentOfSimpleHybridManager.py new file mode 100644 index 0000000..070586d --- /dev/null +++ b/examples/resources/hybrid_plugin_system/SimpleHybridManager/plugin/PyComponentOfSimpleHybridManager.py @@ -0,0 +1,141 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright 2024 The Foundry Visionmongers Ltd +""" +A single-class module, providing the SimpleHybridManagerInterface class. +""" +# pylint: disable=unused-argument + +from openassetio.trait import TraitsData +from openassetio.errors import BatchElementError +from openassetio.access import PolicyAccess, EntityTraitsAccess +from openassetio.managerApi import ManagerInterface +from openassetio.pluginSystem import PythonPluginSystemManagerPlugin + +from openassetio_mediacreation.traits.content import LocatableContentTrait +from openassetio_mediacreation.traits.managementPolicy import ManagedTrait +from openassetio_mediacreation.traits.usage import EntityTrait + +# Unique ID of the plugin. Must match that advertised by the partner C++ +# plugin. +kPluginId = "org.openassetio.examples.simplehybridmanager" +# The one and only entity reference we support. +kTheEntityReference = "examplehybrid://example_entity" + + +class SimpleHybridManagerInterface(ManagerInterface): + """ + Python side of the hybrid plugin. + """ + + def identifier(self): + """ + Identifier must match the partner C++ plugin's identifier. + """ + return kPluginId + + def displayName(self): + """ + This display name will be used if the Python plugin system takes + precedence in the hybrid plugin system. + """ + return "Simple Hybrid Manager" + + def hasCapability(self, capability): + """ + This plugin supports only the minimal required set of + capabilities. It does not even support `resolve`. However, the + partner C++ plugin does support `resolve`. + """ + if capability in ( + ManagerInterface.Capability.kEntityReferenceIdentification, + ManagerInterface.Capability.kManagementPolicyQueries, + ManagerInterface.Capability.kEntityTraitIntrospection, + ): + return True + + return False + + def managementPolicy(self, traitSets, policyAccess, context, hostSession): + """ + Only support reading file paths (or URLs). + """ + policies = [TraitsData() for _ in traitSets] + if policyAccess != PolicyAccess.kRead: + # We only support read access. + return policies + + for trait_set, policy_data in zip(traitSets, policies): + if not {EntityTrait.kId, LocatableContentTrait.kId} <= trait_set: + # We only support file entities. + continue + ManagedTrait.imbueTo(policy_data) + + return policies + + def isEntityReferenceString(self, someString, hostSession): + """ + Both Python and C++ plugins should expect the same entity + reference format. + """ + return someString.startswith("examplehybrid://") + + def entityTraits( + self, + entityReferences, + entityTraitsAccess, + context, + _hostSession, + successCallback, + errorCallback, + ): + """ + This Python plugin provides introspection of entities to get + their trait set, whereas the values for the properties of the + traits are `resolve`d through the partner C++ plugin. + """ + + # Only support reading. + if entityTraitsAccess != EntityTraitsAccess.kRead: + result = BatchElementError( + BatchElementError.ErrorCode.kEntityAccessError, "Entities are read-only" + ) + for idx in range(len(entityReferences)): + errorCallback(idx, result) + return + + for idx, ref in enumerate(entityReferences): + if ref.toString() == kTheEntityReference: + successCallback(idx, {EntityTrait.kId, LocatableContentTrait.kId}) + else: + errorCallback( + idx, + BatchElementError( + BatchElementError.ErrorCode.kEntityResolutionError, + f"Entity '{ref.toString()}' not found", + ), + ) + + +class SimpleHybridManagerPlugin(PythonPluginSystemManagerPlugin): + """ + Entry point for the plugin. + """ + + @staticmethod + def identifier(): + """ + Identifier must match the partner C++ plugin's identifier. + """ + return kPluginId + + @classmethod + def interface(cls): + """ + Create the Python side of the hybrid plugin interface. + """ + return SimpleHybridManagerInterface() + + +# Public entry point that will be searched for by the plugin system. +# pylint: disable=invalid-name +openassetioPlugin = SimpleHybridManagerPlugin diff --git a/examples/resources/hybrid_plugin_system/SimpleHybridManager/pyproject.toml b/examples/resources/hybrid_plugin_system/SimpleHybridManager/pyproject.toml new file mode 100644 index 0000000..6bc0e87 --- /dev/null +++ b/examples/resources/hybrid_plugin_system/SimpleHybridManager/pyproject.toml @@ -0,0 +1,39 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2024 The Foundry Visionmongers Ltd + +[tool.pylint.messages_control] +disable = [ + "too-many-arguments", + "too-few-public-methods", +] + +# NB: This requires the use of pyproject-flake8 +[tool.flake8] +max-line-length = 99 +extend-ignore = "E266," + +[tool.pylint.format] +max-line-length = 99 + +[tool.pylint.basic] +argument-naming-style = "camelCase" +class-const-naming-style = "camelCase" +variable-naming-style = "snake_case" + +# Support both camelCase and PascalCase for modules +module-rgx = "_?([a-z]|[A-Z])+([A-Z][a-z0-9]*)*" +# camelCase doesn't include "__camelCase" or "test_camelCase" +attr-rgx = "_?_?[a-z0-9]+([A-Z][a-z0-9]*)*" +method-rgx = "(_?_|test_)?[a-z0-9]+([A-Z][a-z0-9]*)*" +function-rgx = "(_|test_)?[a-z0-9]+([A-Z][a-z0-9]*)*" +# C++ style constants, e.g. `kThing_SubThing`. +const-rgx = "k([A-Z0-9]+[a-z0-9]*)+_?([A-Z0-9]+[a-z0-9]*)*" +class-const-rgx = "k([A-Z0-9]+[a-z0-9]*)+_?([A-Z0-9]+[a-z0-9]*)*" + +[tool.pylint.similarities] +# Ignore imports when computing similarities. +ignore-imports = true + +[tool.black] +line-length = 99 +target-version = ["py311"] \ No newline at end of file diff --git a/examples/resources/hybrid_plugin_system/SimpleHybridManager/src/CMakeLists.txt b/examples/resources/hybrid_plugin_system/SimpleHybridManager/src/CMakeLists.txt new file mode 100644 index 0000000..2cdbf0b --- /dev/null +++ b/examples/resources/hybrid_plugin_system/SimpleHybridManager/src/CMakeLists.txt @@ -0,0 +1,71 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2024 The Foundry Visionmongers Ltd +cmake_minimum_required(VERSION 3.27) + +project(CppComponentOfSimpleHybridManager LANGUAGES CXX) + +set(_target_name ${PROJECT_NAME}) + +add_library(${_target_name} MODULE) + +install( + TARGETS ${_target_name} + EXPORT ${PROJECT_NAME}_EXPORTED_TARGETS + DESTINATION . +) + +#----------------------------------------------------------------------- +# Target properties. + +set_target_properties( + ${_target_name} + PROPERTIES + + # Ensure consistent C++17 standard. + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED YES + CXX_EXTENSIONS NO + + # Ensure non-exported symbols are hidden from the host application. + C_VISIBILITY_PRESET hidden + CXX_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN YES + + # Use a predictable name for the plugin binary. + OUTPUT_NAME ${PROJECT_NAME} + PREFIX "" + SOVERSION "" + VERSION "" +) + +#----------------------------------------------------------------------- +# Compiler warnings. + +if (CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + set(_project_warnings -Wall -Wextra -Wpedantic) +endif () + +target_compile_options(${_target_name} PRIVATE ${_project_warnings}) + +#----------------------------------------------------------------------- +# API export header. + +include(GenerateExportHeader) +generate_export_header( + ${_target_name} + EXPORT_FILE_NAME ${CMAKE_CURRENT_BINARY_DIR}/include/export.h + EXPORT_MACRO_NAME OPENASSETIO_EXAMPLE_SIMPLEHYBRIDMANAGER_EXPORT +) + +#----------------------------------------------------------------------- +# Target dependencies. + +target_sources(${_target_name} PRIVATE ${PROJECT_NAME}.cpp) + +# For generated API export header. +target_include_directories(${_target_name} PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/include) + +find_package(OpenAssetIO REQUIRED) +target_link_libraries(${_target_name} PRIVATE OpenAssetIO::openassetio-core) +find_package(OpenAssetIO-MediaCreation REQUIRED) +target_link_libraries(${_target_name} PRIVATE OpenAssetIO-MediaCreation::openassetio-mediacreation) diff --git a/examples/resources/hybrid_plugin_system/SimpleHybridManager/src/CppComponentOfSimpleHybridManager.cpp b/examples/resources/hybrid_plugin_system/SimpleHybridManager/src/CppComponentOfSimpleHybridManager.cpp new file mode 100644 index 0000000..7a46c03 --- /dev/null +++ b/examples/resources/hybrid_plugin_system/SimpleHybridManager/src/CppComponentOfSimpleHybridManager.cpp @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 The Foundry Visionmongers Ltd +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include + +namespace { +// Unique ID of the plugin. Must match that advertised by the partner +// Python plugin. +constexpr std::string_view kPluginId = "org.openassetio.examples.simplehybridmanager"; +// The one and only entity reference we support. These two lines +// represent our backend database. +constexpr std::string_view kTheEntityReference = "examplehybrid://example_entity"; +constexpr std::string_view kTheEntityPath = "file:///some/path.exr"; +} // namespace + +/** + * C++ side of Simple Hybrid Manager. + */ +struct SimpleHybridManagerInterface final : openassetio::managerApi::ManagerInterface { + /** + * Identifier must match the partner Python plugin's identifier. + */ + [[nodiscard]] openassetio::Identifier identifier() const override { + return openassetio::Identifier{kPluginId}; + } + + /** + * displayName has no base class implementation so must be + * implemented. + * + * This display name will be used if the C++ plugin system takes + * precedence in the hybrid plugin system. + */ + [[nodiscard]] openassetio::Str displayName() const override { return "Simple Hybrid Manager"; } + + /** + * The C++ side of this hybrid plugin is solely responsible for + * `resolve` and nothing else. + */ + [[nodiscard]] bool hasCapability(const Capability capability) override { + return capability == Capability::kResolution; + } + + /** + * Implementation of `resolve` in C++ - the only capability supported + * by this plugin. Other capabilities are handled by the partner + * Python plugin. + */ + void resolve(const openassetio::EntityReferences& entityReferences, + const openassetio::trait::TraitSet& traitSet, + const openassetio::access::ResolveAccess resolveAccess, + [[maybe_unused]] const openassetio::ContextConstPtr& context, + [[maybe_unused]] const openassetio::managerApi::HostSessionPtr& hostSession, + const ResolveSuccessCallback& successCallback, + const BatchElementErrorCallback& errorCallback) override { + using openassetio::EntityReference; + using openassetio::access::ResolveAccess; + using openassetio::errors::BatchElementError; + using openassetio::trait::TraitsData; + using openassetio::trait::TraitsDataPtr; + using openassetio_mediacreation::traits::content::LocatableContentTrait; + + // We only support read access. + if (resolveAccess != ResolveAccess::kRead) { + for (std::size_t idx = 0; idx < entityReferences.size(); ++idx) { + errorCallback(idx, BatchElementError{BatchElementError::ErrorCode::kEntityAccessError, + "Entity access is read-only"}); + } + return; + } + + // Loop each entity reference in the input batch. + for (std::size_t idx = 0; idx < entityReferences.size(); ++idx) { + // We only support one entity. + if (entityReferences[idx].toString() == kTheEntityReference) { + TraitsDataPtr traitsData = TraitsData::make(); + + // Populate the requested traits with their properties. We only + // support one trait. + if (traitSet.count(LocatableContentTrait::kId)) { + LocatableContentTrait{traitsData}.setLocation(openassetio::Str{kTheEntityPath}); + } + + successCallback(idx, std::move(traitsData)); + } else { + // If we can't find the entity reference in the database, then + // flag an error. + errorCallback(idx, BatchElementError{BatchElementError::ErrorCode::kEntityResolutionError, + "Entity not found"}); + } + } + } +}; + +/** + * Subclass of the CppPluginSystemManagerPlugin that can be used to + * construct instances of our simple ManagerInterface. + */ +struct SimpleHybridManagerPlugin final : openassetio::pluginSystem::CppPluginSystemManagerPlugin { + [[nodiscard]] openassetio::Identifier identifier() const override { + return openassetio::Identifier{kPluginId}; + } + openassetio::managerApi::ManagerInterfacePtr interface() override { + return std::make_shared(); + } +}; + +extern "C" { +/** + * External entry point that the OpenAssetIO plugin system will query. + */ +OPENASSETIO_EXAMPLE_SIMPLEHYBRIDMANAGER_EXPORT +openassetio::pluginSystem::PluginFactory openassetioPlugin() noexcept { + return []() noexcept -> openassetio::pluginSystem::CppPluginSystemPluginPtr { + return std::make_shared(); + }; +} +} 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..8d85bed --- /dev/null +++ b/examples/resources/hybrid_plugin_system/openassetio_config.toml @@ -0,0 +1,4 @@ +[manager] +# Identifier advertised by both the C++ and Python plugins that make up +# the hybrid manager SimpleHybridManager. +identifier = "org.openassetio.examples.simplehybridmanager" \ No newline at end of file diff --git a/examples/resources/requirements.txt b/examples/resources/requirements.txt index 4342f3a..fab8fa3 100644 --- a/examples/resources/requirements.txt +++ b/examples/resources/requirements.txt @@ -1,5 +1,5 @@ jupyter -openassetio>=v1.0.0b2rev0 -openassetio-manager-bal>=v1.0.0a14 +openassetio>=v1.0.0rc1.rev0 +openassetio-manager-bal>=v1.0.0b1.rev0 openassetio-mediacreation Pillow