diff --git a/docs/explanation/plugin_system.md b/docs/explanation/plugin_system.md new file mode 100644 index 0000000000000000000000000000000000000000..d34f43db2ce1d60f43bd70a05b7ed82f551df983 --- /dev/null +++ b/docs/explanation/plugin_system.md @@ -0,0 +1,102 @@ +# NOMAD plugin system + +## Introduction + +NOMAD is used by many research communities with their specific data, workflows, and analysis tools. NOMAD plugins are key +to adopt NOMAD to these heterogeneous environments. +You can think of plugins as “add-ons†that provide additional capabilities. +Each plugin is a small independant software project that integrates with the core NOMAD and provides features without modifictions to the core NOMAD itself. +Some key advantages of using plugins: + +- **Modularity**: You can pick and choose which features or functions to add, rather than having everything baked into the core NOMAD. + +- **Customizability**: Users can add their own plugins to address specific use cases, without changing the official NOMAD software. + +- **Easy updates**: If a feature needs to be updated or improved, it can be done at the plugin level, without having to release a new NOMAD version. + +- **Collaboration**: Since plugins are independent, multiple developers can work on different features in parallel and with different release cycles without interfering with each other. + +## Architecture + +There are three core components to the plugin system: + +- **Distributions** define lists of plugins and their version. A distribution is a small + Git and Python project that maintains a list of plugin dependencies in its `pyproject.toml`. +- **Plugins** are Git and Python projects that contain one or many _entry points_. + We provide a [template repository](https://github.com/FAIRmat-NFDI/nomad-plugin-template) + for a quick start into plugin development. +- **Entry points** are individual contributions (e.g. parsers, schemas, or apps) + which are defined using a feature of Python called [_entry points_](https://setuptools.pypa.io/en/latest/userguide/entry_point.html). + +<figure markdown style="width: 100%"> + ``` mermaid + %%{init:{'flowchart':{'nodeSpacing': 25, 'subGraphTitleMargin': {'top': 5, 'bottom': 10}, 'padding': 10}}}%% + graph LR + subgraph NOMAD Distribution + subgraph NOMAD Plugin C + ro7(Entry point: Schema 1) + ro8(Entry point: Schema 2) + end + subgraph NOMAD Plugin B + ro4(Entry point: Schema) + ro5(Entry point: App 1) + ro6(Entry point: App 2) + end + subgraph NOMAD Plugin A + ro1(Entry point: Schema) + ro2(Entry point: Parser 1) + ro3(Entry point: Parser 2) + end + end + ``` + <figcaption>Relation between NOMAD distributions, plugins and entry points.</figcaption> +</figure> + +This architecture allows plugin developers to freely choose a suitable granularity for their use case: they may create a single plugin package that contains everything that e.g. a certain lab needs: schemas, parsers and apps. Alternatively they may also develop multiple plugins, each containing a single entry point. Reduction in package scope can help in developing different parts indepedently and also allows plugin users to choose only the parts that they need. + +## Entry point discovery + +Entry points are like pre-defined connectors or hooks that allow the main system to recognize and load the code from plugins without needing to hard-code them directly into the platform. This mechanism enables the automatic discovery of plugin code. Plugin entry points represent different types of customizations that can be added to a NOMAD installation. The following plugin entry point types are currently supported: + +- [Apps](../howto/plugins/apps.md) +- [Example uploads](../howto/plugins/example_uploads.md) +- [Normalizers](../howto/plugins/parsers.md) +- [Parsers](../howto/plugins/parsers.md) +- [Schema packages](../howto/plugins/schema_packages.md) + +## Loading plugins + +Entry points contain **configuration** (the entry point), but also a separate **resource** (the implementation). This split enables lazy-loading: the configuration can be loaded immediately, while the resource is loaded later when/if it is required. This can significantly improve startup times, as long as all time-consuming initializations are performed only when loading the resource. This split also helps to avoid cyclical imports between the plugin code and the `nomad-lab` package. The following diagram illustrates how NOMAD interacts with the entry points in a plugin: + +<figure markdown style="width: 100%"> + ``` mermaid + %%{init:{'sequence':{'mirrorActors': false}}}%% + sequenceDiagram + autonumber + rect + NOMAD->>Plugin: Request entry points matching the nomad.plugin group + Plugin-->>NOMAD: Return all entry point configurations + end + Note over NOMAD: Other tasks + rect + NOMAD->>Plugin: Request a specific entry point resource + opt + Plugin->>NOMAD: Request configuration overrides from nomad.yaml + NOMAD-->>Plugin: Return final configuration for entry point + end + Plugin-->>NOMAD: Return fully initialized entry point resource + end + ``` + <figcaption>NOMAD interaction with a plugin.</figcaption> +</figure> + +1. When NOMAD starts, it scans for plugin entry points defined under the `nomad.plugin` group in all of the Python packages that have been installed. +2. The plugin returns all entry points that it has registered in `pyproject.toml` under the `nomad.plugin` group. This only loads the configuration, but does not yet load the resource, i.e. main Python implementation. +3. When NOMAD needs to load the actual resource for an entry point (e.g. a parser), loads it by using the configuration instance. +4. When the resource is being loaded, the entry point may ask for any configuration overrides that may have been set in `nomad.yaml`. +5. NOMAD will return the final validated configuration that contains the default values and possible overrides. +6. The plugin loads and returns the resource using the final configuration. This typically involves creating an instance of a specific class, e.g. `Parser` in the case of parser entry points. + +## Learn how to write plugins + +You can learn more about plugin development in the [Get started with plugins](../howto/plugins/plugins.md) -page. diff --git a/docs/howto/plugins/plugins.md b/docs/howto/plugins/plugins.md index 41b23bae1ac1cf1893155600f99824ce5a2bdec6..3d70f6f1c1fc7a43acc9fa4a4c2ae0bb251a05d1 100644 --- a/docs/howto/plugins/plugins.md +++ b/docs/howto/plugins/plugins.md @@ -1,6 +1,6 @@ # Get started with plugins -The main way to customize a NOMAD installation is through the use of **plugins**. A NOMAD Plugin is a Git repository that contains a Python package that an administrator can install into a NOMAD deployment to add custom features. This page contains the basics of how to create, develop and publish a NOMAD Plugin. +The main way to customize a NOMAD installation is through the use of **plugins**. A NOMAD plugin is a Python package that an administrator can install into a NOMAD distribution to add custom features. This page contains shows you how to create, develop and publish a NOMAD plugin. For a high-level overview of the plugin mechanism, see the [NOMAD plugin system](../../explanation/plugin_system.md) -page. ## Plugin anatomy @@ -40,17 +40,9 @@ In the folder structure you can see that a single plugin can contain multiple ty ## Plugin entry points -Plugin entry points represent different types of customizations that can be added to a NOMAD installation. The following plugin entry point types are currently supported: +Plugin entry points represent different types of customizations that can be added to a NOMAD installation. Entry points contain **configuration**, but also a **resource**, which lives in a separate Python module. This split enables lazy-loading: the configuration can be loaded immediately, while the resource is loaded later when/if it is required. This can significantly improve startup times, as long as all time-consuming initializations are performed only when loading the resource. This split also helps to avoid cyclical imports between the plugin code and the `nomad-lab` package. - - [Apps](./apps.md) - - [Example uploads](./example_uploads.md) - - [Normalizers](./parsers.md) - - [Parsers](./parsers.md) - - [Schema packages](./schema_packages.md) - -Entry points contain **configuration**, but also a separate **resource**, which should live in a separate Python module. This split enables lazy-loading: the configuration can be loaded immediately, while the resource is loaded later when/if it is required. This can significantly improve startup times, as long as all time-consuming initializations are performed only when loading the resource. This split also helps to avoid cyclical imports between the plugin code and the `nomad-lab` package. - -For example the entry point instance for a parser is contained in `.../parsers/__init__.py` and it contains e.g. the name, version and any additional entry point-specific parameters that control its behaviour. The entry point has a `load` method than can be called lazily to return the resource, which is a `Parser` instance defined in `.../parsers/myparser.py`. +For example the entry point configuration for a parser is contained in `.../parsers/__init__.py` and it contains e.g. the name, version and any additional entry point-specific parameters that control its behaviour. The entry point has a `load` method than can be called lazily to return the resource, which is a `Parser` instance defined in `.../parsers/myparser.py`. In `pyproject.toml` you can expose plugin entry points for automatic discovery. E.g. to expose an app and a package, you would add the following to `pyproject.toml`: @@ -64,28 +56,11 @@ Here it is important to use the `nomad.plugin` group name in the `project.entry- You can read more about how to write different types of entry points in their dedicated documentation pages or learn more about the [Python entry point mechanism](https://setuptools.pypa.io/en/latest/userguide/entry_point.html). -### Controlling loading of plugin entry points - -By default, plugin entry points are automatically loaded, and as an administrator you only need to install the Python package. You can, however, control which entry points to load by explicitly including/excluding them in your `nomad.yaml`. For example, if a plugin has the following `pyproject.toml`: +### Plugin configuration -```toml -[project.entry-points.'nomad.plugin'] -myparser = "nomad_example.parsers:myparser" -``` +The plugin entry point configuration is an instance of a [`pydantic`](https://docs.pydantic.dev/1.10/) model. This base model may already contain entry point-specific fields (such as the file extensions that a parser plugin will match) but it is also possible to extend this model to define additional fields that control your plugin behaviour. -You could disable the parser entry point in your `nomad.yaml` with: - -```yaml -plugins: - entry_points: - exclude: ["nomad_plugin.parsers:myparser"] -``` - -### Extending and using the entry point - -The plugin entry point is an instance of a [`pydantic`](https://docs.pydantic.dev/1.10/) model. This base model may already contain entry point-specific fields (such as the file extensions that a parser plugin will match) but it is also possible to extend this model to define additional fields that control your plugin behaviour. - -To specify new configuration options, you can add new `pydantic` fields to the subclass. For example, if we wanted to add a new configuration option for a parser, we could do the following: +Here is an example of a new plugin entry point configuration class and instance for a parser, that has a new custom `parameter` configuration added as a `pydantic` `Field`: ```python from pydantic import Field @@ -94,9 +69,15 @@ from nomad.config.models.plugins import ParserEntryPoint class MyParserEntryPoint(ParserEntryPoint): parameter: int = Field(0, description='Config parameter for this parser.') + +myparser = MyParserEntryPoint( + name = 'MyParser', + description = 'My custom parser.', + mainfile_name_re = '.*\.myparser', +) ``` -where we have defined a new subclass of `ParserEntryPoint` and added a new configuration field `parameter`. The plugin users can then control these settings in their `nomad.yaml` using `plugins.entry_points.options`: +The plugin entry point behaviour can be controlled in `nomad.yaml` using `plugins.entry_points.options`: ```yaml plugins: @@ -108,7 +89,20 @@ plugins: Note that the model will also validate the values coming from `nomad.yaml`, and you should utilize the validation mechanisms of `pydantic` to provide users with helpful messages about invalid configuration. -In your code, you can then access the whole entry point by loading it with `config.get_plugin_entry_point`: +### Plugin resource + +The configuration class has a `load` method that returns the entry point resource. This is typically an instance of a class, e.g. `Parser` instance in the case of a parser entry point. Here is an example of a `load` method for a parser: + +```python +class MyParserEntryPoint(ParserEntryPoint): + + def load(self): + from nomad_example.parsers.myparser import MyParser + + return MyParser(**self.dict()) +``` + +Often when loading the resource, you will need access to the final entry point configuration defined in `nomad.yaml`. This way also any overrides to the plugin configuration are correctly taken into account. You can get the final configuration using the `get_plugin_entry_point` function and the plugin name as defined in `pyproject.toml` as an argument: ```python from nomad.config import config @@ -117,6 +111,23 @@ configuration = config.get_plugin_entry_point('nomad_example.parsers:myparser') print(f'The parser parameter is: {configuration.parameter}') ``` +### Controlling loading of plugin entry points + +By default, plugin entry points are automatically loaded, and as an administrator you only need to install the Python package. You can, however, control which entry points to load by explicitly including/excluding them in your `nomad.yaml`. For example, if a plugin has the following `pyproject.toml`: + +```toml +[project.entry-points.'nomad.plugin'] +myparser = "nomad_example.parsers:myparser" +``` + +You could disable the parser entry point in your `nomad.yaml` with: + +```yaml +plugins: + entry_points: + exclude: ["nomad_plugin.parsers:myparser"] +``` + ## Plugin development guidelines ### Linting and formatting diff --git a/mkdocs.yml b/mkdocs.yml index 04aede9dfcdccfb3b1f77e1523ffc29650868ae9..13bc1d39cc34960163f093714320c9c8814bfce9 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -3,103 +3,104 @@ site_description: | The documentation for NOMAD v1. site_author: The NOMAD Authors repo_url: https://github.com/nomad-coe/nomad -edit_uri: '' +edit_uri: "" nav: - Home: index.md - Tutorial: - - Navigating to NOMAD: tutorial/nomad_repo.md - - Uploading and publishing data: tutorial/upload_publish.md - - Exploring data: tutorial/explore.md - - Access data via API: tutorial/access_api.md - - Schemas and plugins: tutorial/custom.md - - Developing a NOMAD plugin: tutorial/develop_plugin.md + - Navigating to NOMAD: tutorial/nomad_repo.md + - Uploading and publishing data: tutorial/upload_publish.md + - Exploring data: tutorial/explore.md + - Access data via API: tutorial/access_api.md + - Schemas and plugins: tutorial/custom.md + - Developing a NOMAD plugin: tutorial/develop_plugin.md - How-to guides: - - Overview: howto/overview.md - - Manage and find data: - - Upload and publish data for supported formats: howto/manage/upload.md - - Use ELNs: howto/manage/eln.md - - Explore data: howto/manage/explore.md - - Use NORTH: howto/manage/north.md - - Programmatic use: - - Use the API: howto/programmatic/api.md # TODO separate into How-to and Explanation/Reference - - Download data: howto/programmatic/download.md - - Publish data using python: howto/programmatic/publish_python.md - - Install nomad-lab: howto/programmatic/pythonlib.md - - Access processed data: howto/programmatic/archive_query.md - - Run a parser: howto/programmatic/local_parsers.md - - NOMAD Oasis: - - Install an Oasis: howto/oasis/install.md - - Customize an Oasis: howto/oasis/customize.md - - Install plugins: howto/oasis/plugins_install.md - - Migrate Oasis versions: howto/oasis/migrate.md - - Perform admin tasks: howto/oasis/admin.md - - Plugins: - - Get started with plugins: howto/plugins/plugins.md - - Write an app: howto/plugins/apps.md - - Write an example upload: howto/plugins/example_uploads.md - - Write a normalizer: howto/plugins/normalizers.md - - Write a parser: howto/plugins/parsers.md - - Write a schema package: howto/plugins/schema_packages.md - - Customization: - - Write a YAML schema package: howto/customization/basics.md - - Define ELNs: howto/customization/elns.md - - Use base sections: howto/customization/base_sections.md - - Parse tabular data: howto/customization/tabular.md - - Define workflows: howto/customization/workflows.md - - Work with units: howto/customization/units.md - - Use HDF5 to handle large quantities: howto/customization/hdf5.md - - Development: - - Get started: howto/develop/setup.md - - Navigate the code: howto/develop/code.md - - Contribute: howto/develop/contrib.md - - Extend the search: howto/develop/search.md - - Migrate to autoformatted code: howto/develop/migrate-to-autoformatter.md - - Make a release: howto/develop/release.md + - Overview: howto/overview.md + - Manage and find data: + - Upload and publish data for supported formats: howto/manage/upload.md + - Use ELNs: howto/manage/eln.md + - Explore data: howto/manage/explore.md + - Use NORTH: howto/manage/north.md + - Programmatic use: + - Use the API: howto/programmatic/api.md # TODO separate into How-to and Explanation/Reference + - Download data: howto/programmatic/download.md + - Publish data using python: howto/programmatic/publish_python.md + - Install nomad-lab: howto/programmatic/pythonlib.md + - Access processed data: howto/programmatic/archive_query.md + - Run a parser: howto/programmatic/local_parsers.md + - NOMAD Oasis: + - Install an Oasis: howto/oasis/install.md + - Customize an Oasis: howto/oasis/customize.md + - Install plugins: howto/oasis/plugins_install.md + - Migrate Oasis versions: howto/oasis/migrate.md + - Perform admin tasks: howto/oasis/admin.md + - Plugins: + - Get started with plugins: howto/plugins/plugins.md + - Write an app: howto/plugins/apps.md + - Write an example upload: howto/plugins/example_uploads.md + - Write a normalizer: howto/plugins/normalizers.md + - Write a parser: howto/plugins/parsers.md + - Write a schema package: howto/plugins/schema_packages.md + - Customization: + - Write a YAML schema package: howto/customization/basics.md + - Define ELNs: howto/customization/elns.md + - Use base sections: howto/customization/base_sections.md + - Parse tabular data: howto/customization/tabular.md + - Define workflows: howto/customization/workflows.md + - Work with units: howto/customization/units.md + - Use HDF5 to handle large quantities: howto/customization/hdf5.md + - Development: + - Get started: howto/develop/setup.md + - Navigate the code: howto/develop/code.md + - Contribute: howto/develop/contrib.md + - Extend the search: howto/develop/search.md + - Migrate to autoformatted code: howto/develop/migrate-to-autoformatter.md + - Make a release: howto/develop/release.md - Domain-specific examples: - - Overview: examples/overview.md - - Computational data: - - Quick start: examples/computational_data/uploading.md - - Workflows: examples/computational_data/workflows.md - - MetaInfo: examples/computational_data/metainfo.md - - Schema plugins: examples/computational_data/schema_plugins.md - - Parser plugins: examples/computational_data/parser_plugins.md - - H5MD schema - Howto: examples/computational_data/h5md_howto.md - - H5MD schema - Explanation: examples/computational_data/h5md_expl.md - - H5MD - Reference: examples/computational_data/h5md_ref.md - - Experimental data: - - NeXus: - - NeXus semantic file format: examples/experiment_data/nexus.md - - Guide to parser library pynxtools: examples/experiment_data/pynxtools.md - - Electron microscopy: examples/experiment_data/em.md - - Photoemission spectroscopy: examples/experiment_data/mpes.md - - X-ray photoemission spectroscopy: examples/experiment_data/xps.md - - Optical spectroscopy: examples/experiment_data/opt.md - - Atom probe tomography: examples/experiment_data/apm.md - - Scanning tunneling spectroscopy: examples/experiment_data/sts.md - # - NOMAD-supported plugins: - # - Databases for specific research topics: + - Overview: examples/overview.md + - Computational data: + - Quick start: examples/computational_data/uploading.md + - Workflows: examples/computational_data/workflows.md + - MetaInfo: examples/computational_data/metainfo.md + - Schema plugins: examples/computational_data/schema_plugins.md + - Parser plugins: examples/computational_data/parser_plugins.md + - H5MD schema - Howto: examples/computational_data/h5md_howto.md + - H5MD schema - Explanation: examples/computational_data/h5md_expl.md + - H5MD - Reference: examples/computational_data/h5md_ref.md + - Experimental data: + - NeXus: + - NeXus semantic file format: examples/experiment_data/nexus.md + - Guide to parser library pynxtools: examples/experiment_data/pynxtools.md + - Electron microscopy: examples/experiment_data/em.md + - Photoemission spectroscopy: examples/experiment_data/mpes.md + - X-ray photoemission spectroscopy: examples/experiment_data/xps.md + - Optical spectroscopy: examples/experiment_data/opt.md + - Atom probe tomography: examples/experiment_data/apm.md + - Scanning tunneling spectroscopy: examples/experiment_data/sts.md + # - NOMAD-supported plugins: + # - Databases for specific research topics: - Explanation: - - From files to data: explanation/basics.md - - Data structure: explanation/data.md - - Processing: explanation/processing.md - - Architecture: explanation/architecture.md - - Federation and Oasis: explanation/oasis.md + - From files to data: explanation/basics.md + - Data structure: explanation/data.md + - Processing: explanation/processing.md + - Architecture: explanation/architecture.md + - Plugins: explanation/plugin_system.md + - Federation and Oasis: explanation/oasis.md - Reference: - - reference/config.md - - reference/annotations.md - - reference/cli.md - - reference/plugins.md - - reference/parsers.md - - reference/code_guidelines.md - - reference/glossary.md - - reference/tutorials.md + - reference/config.md + - reference/annotations.md + - reference/cli.md + - reference/plugins.md + - reference/parsers.md + - reference/code_guidelines.md + - reference/glossary.md + - reference/tutorials.md theme: name: material palette: - primary: '#2A4CDF' - accent: '#008A67' + primary: "#2A4CDF" + accent: "#008A67" font: - text: 'Titillium Web' + text: "Titillium Web" logo: assets/nomad-logo.png favicon: assets/favicon.png features: @@ -138,19 +139,19 @@ extra: homepage: https://nomad-lab.eu use_directory_urls: false plugins: - - search - - macros: - module_name: nomad/mkdocs - - redirects: - redirect_maps: - 'pythonlib.md': 'howto/programmatic/pythonlib.md' - 'oasis.md': 'howto/oasis/install.md' - 'develop/gitlab.md': 'howto/develop/contrib.md' - - glightbox - # - git-revision-date-localized - # TODO Fix error in pipeline when this plugin is included - # (with 'mkdocs-git-revision-date-localized-plugin==1.2.1' in pyproject.toml) - # see pipeline error here: https://gitlab.mpcdf.mpg.de/nomad-lab/nomad-FAIR/-/jobs/2473217 + - search + - macros: + module_name: nomad/mkdocs + - redirects: + redirect_maps: + "pythonlib.md": "howto/programmatic/pythonlib.md" + "oasis.md": "howto/oasis/install.md" + "develop/gitlab.md": "howto/develop/contrib.md" + - glightbox + # - git-revision-date-localized + # TODO Fix error in pipeline when this plugin is included + # (with 'mkdocs-git-revision-date-localized-plugin==1.2.1' in pyproject.toml) + # see pipeline error here: https://gitlab.mpcdf.mpg.de/nomad-lab/nomad-FAIR/-/jobs/2473217 extra_css: - stylesheets/extra.css extra_javascript: