From 983a645c9285ba65c7cf07fe6064c23e7e994c06 Mon Sep 17 00:00:00 2001 From: Sebastiaan Huber Date: Thu, 18 Jan 2024 17:43:03 +0100 Subject: [PATCH] Add AEP: Add a schema to ORM classes AiiDA's Python API provides an object relational mapper (ORM) that abstracts the various entities that can be stored inside the provenance graph and the relationships between them. In most use cases, users use this ORM directly in Python to construct new instances of entities and retrieve existing ones, in order to get access to their data and manipulate it. A current shortcoming of the ORM is that it is not possible to programmatically introspect the schema of each entity: that is to say, what data each entity stores. This makes it difficult for external applications to provide interfaces to create and or retrieve entity instances. It also makes it difficult to take the data outside of the Python environment since the data would have to be serialized. However, without a well-defined schema, doing this without an ad-hoc solution is practically impossible. Clear data schemas for all ORM entities would enable the creation of external applications to work with the data stored in AiiDA's provenance graph. A typical example use case would be a web API whose interface, to create and return ORM entities from an AiiDA profile, would be dynamically generated by programmatically introspecting the schema of all ORM entities stored within it. Currently, the interface has to be manually generated for each ORM entity, and data (de)serialization has to be implemented. --- 010_orm_schema/readme.md | 482 +++++++++++++++++++++ 010_orm_schema/screenshot_docs_fastapi.png | Bin 0 -> 45583 bytes README.md | 1 + _toc.yml | 1 + 4 files changed, 484 insertions(+) create mode 100644 010_orm_schema/readme.md create mode 100644 010_orm_schema/screenshot_docs_fastapi.png diff --git a/010_orm_schema/readme.md b/010_orm_schema/readme.md new file mode 100644 index 0000000..adbce9b --- /dev/null +++ b/010_orm_schema/readme.md @@ -0,0 +1,482 @@ +# AEP 010: Add a schema to ORM classes + +| AEP number | 010 | +|------------|-----------------------------------------------------------------| +| Title | Add a schema to ORM classes | +| Authors | [Sebastiaan P. Huber](mailto:mail@sphuber.net) (sphuber) | +| Champions | [Sebastiaan P. Huber](mailto:mail@sphuber.net) (sphuber) | +| Type | S - Standard | +| Created | 16-Jan-2024 | +| Status | submitted | + +## Table of contents + +* [Background](#background) +* [Design goals](#design-goals) +* [User interface](#user-interface) +* [Example use cases](#example-use-cases) +* [Implementation](#implementation) +* [Design discussion](#design-discussion) + * [Advantages](#advantages) + * [Disadvantages](#disadvantages) + * [Alternatives](#alternatives) + +## Background + +AiiDA's Python API provides an object relational mapper (ORM) that abstracts the various entities that can be stored inside the provenance graph and the relationships between them. +In most use cases, users use this ORM directly in Python to construct new instances of entities and retrieve existing ones, in order to get access to their data and manipulate it. +A current shortcoming of the ORM is that it is not possible to programmatically introspect the schema of each entity: that is to say, what data each entity stores. +This makes it difficult for external applications to provide interfaces to create and or retrieve entity instances. +It also makes it difficult to take the data outside of the Python environment since the data would have to be serialized. +However, without a well-defined schema, doing this without an ad-hoc solution is practically impossible. + +Clear data schemas for all ORM entities would enable the creation of external applications to work with the data stored in AiiDA's provenance graph. +A typical example use case would be a web API whose interface, to create and return ORM entities from an AiiDA profile, would be dynamically generated by programmatically introspecting the schema of all ORM entities stored within it. +Currently, the interface has to be manually generated for each ORM entity, and data (de)serialization has to be implemented. + +## Design goals + +The goal of this AEP is to provide a mechanism that allows an external application to programmatically determine the schema of all entities of AiiDA's ORM and automatically (de)serialize entity instances to and from other data formats, e.g., JSON. +The new functionality should or may satisfy the following requirements, in order of importance: + +* **Functionality**: ORM entities should define a schema of the data that they store. The schema should be introspectable programmatically and the interface should provide methods to (de)serialize instances from and to JSON. +* **Compatibility**: The existing functionality of the ORM should not be affected and all changes should be fully backwards compatible. The ORM is such a fundamental part of AiiDA's API that backwards incompatible changes are inacceptable. +* **Interface**: The interface should be intuitive and easy to use. The primary users are expected to be automated tools as users in interactive mode will probably continue using the existing Python API to create entity instances and retrieve their data. +* **Implementation**: The implementation should be such that the added functionality is either automatically inherited by ORM entity classes that can be customized in external packages, such as `Data` plugins, or the interface is simple enough for the plugins to modify and/or extend the schema. +* **Performance**: The performance of importing the ORM and existing functionality of the ORM should not be significantly impacted in a negative way. + +## User interface + +The user interface was designed with the following criteria in mind: + +* For each ORM entity it should be easy to get and inspect its schema programmatically +* It should be easy to construct an instance of an ORM entity from data serialized according to the schema +* It should be easy to serialize and ORM entity instance to a JSON-compatible format +* It should be easy for a `Data` plugin to extend/customize the schema + +The proposed implementation uses [`pydantic`](https://docs.pydantic.dev/latest/) to define the schema of ORM entities. +Each ORM entity class should define the `Model` class attribute, which is a subclass of `pydantic.BaseModel`. +As an example, let's take the base class `Entity`, which only defines a single property, the primary key `pk`. + +The model can be retrieved through `Entity.Model`, which in turn allows to use `pydantic`'s rich API to, for example, the model's fields: +```python +In [1]: from aiida.orm import Entity + +In [2]: Entity.Model.model_fields +Out[2]: { + 'pk': FieldInfo( + annotation=Union[int, NoneType], + required=False, + description='The primary key of the entity. Can be `None` if the entity is not yet stored.', + metadata=[{'priority': 0}, {'exclude_to_orm': True}] + ) +} +``` +It is also trivial to retrieve the schema in JSON format, which is ideal for web-based APIs: +```python +In [3]: Entity.Model.model_json_schema() +Out[3]: +{ + 'properties': { + 'pk': { + 'anyOf': [{'type': 'integer'}, {'type': 'null'}], + 'default': None, + 'description': 'The primary key of the entity. Can be `None` if the entity is not yet stored.', + 'title': 'Pk' + } + }, + 'title': 'Model', + 'type': 'object' +} +``` +An instance of an ORM entity can also easily be serialized to a JSON-compatible format through the `Entity.serialize()` method. +The `Entity.from_serialized` class method allows to reconstruct the instance from serialized data: +```python +In [4]: from aiida.orm import Int + +In [5]: node = Int(1) + +In [6]: serialized = node.serialize() + +In [7]: serialized +Out[7]: +{'pk': None, + 'uuid': '3f5b0b81-3e11-4cd2-976a-2c5d0c62d7ec', + 'node_type': 'data.int.Int.', + 'process_type': None, + 'repository_metadata': {}, + 'ctime': datetime.datetime(2024, 1, 18, 16, 9, 43, 388011, tzinfo=datetime.timezone(datetime.timedelta(seconds=3600), 'CET')), + 'mtime': None, + 'label': '', + 'description': '', + 'extras': {}, + 'computer': None, + 'user': 1, + 'source': None, + 'value': 1} + +In [8]: Int.from_serialized(**serialized) +Out[8]: +``` +Data plugins can easily extend the model of their base class, simply by adding a `Model` class attribute themselves and adding additional attributes: +```python +from aiida.orm import Data + +class PluginData(Data): + + class Model(Data.Model): + additional_attribute: str +``` +As long as the plugin's `Model` class attribute subclasses the `Model` classes of all bases of the plugin's base classes, all model attributes are automatically inherited and any custom attributes can be simply added. + +## Example use cases + +A typical use case for the new functionality is a REST API that exposes an AiiDA profile, allowing new entities to be created and fetched from the database. +The following example how, using [FastAPI](https://fastapi.tiangolo.com/), it is possible to create a REST API that allows to create an endpoint to create `Data` nodes and query for them by their pk with just a few lines of codes: +```python +from fastapi import FastAPI +from aiida import load_profile, orm + +load_profile() +app = FastAPI() + +@app.post('/data/') +async def create_data(model: orm.Data.Model): + data = orm.Data.from_model(model).store() + return data.to_model() + + +@app.get('/data/{pk}') +async def get_data(pk: int): + data = orm.QueryBuilder().append(orm.Data, filters={'id': pk}).one()[0] + return data.to_model() +``` +The FastAPI library is built itself on top of `pydantic` and therefore integrates neatly with the models defined by AiiDA's ORM entities. +The serialization and deserialization of ORM instances is now automatically provided through the `from_model` and `to_model` methods. +Since the schema of the entity model is defined by `pydantic`'s `BaseModel`, FastAPI can automatically generate complete documentation for the interface, as shown in the following screenshot: +![Screenshot of the automatically generated REST API documentation](screenshot_docs_fastapi.png) + +The following example demonstrates, using `curl`, how a new node can be created: +``` +$ curl -X POST 'http://127.0.0.1:8000/data/' -d '{"label": "some-label", "user": 1}' -H 'Content-Type: application/json' +{ + "pk": 1, + "uuid":"655ca12b-95a7-4a20-967f-8d634958637b", + "node_type":"data.Data.", + "process_type":null, + "repository_metadata":{}, + "ctime":"2024-01-18T16:32:55.046381+01:00", + "mtime":"2024-01-18T16:32:55.092325+01:00", + "label":"some-label", + "description":"", + "extras":{"_aiida_hash":"8a7969c09fc52f774764ea2be3e36d6a8394a7d8d756959ac41549e0010007d7"}, + "computer":null, + "user":1, + "source":null +} +``` +By passing the pk of the generated node, it can be retrieved from the GET end point: +``` +$ curl -X GET http://127.0.0.1:8000/data/1 +{ + "pk":1, + "uuid":"655ca12b-95a7-4a20-967f-8d634958637b", + "node_type":"data.Data.", + "process_type":null, + "repository_metadata":{}, + "ctime":"2024-01-18T16:32:55.046381+01:00", + "mtime":"2024-01-18T16:32:55.092325+01:00", + "label":"some-label", + "description":"", + "extras":{"_aiida_hash":"8a7969c09fc52f774764ea2be3e36d6a8394a7d8d756959ac41549e0010007d7"}, + "computer":null, + "user":1, + "source":null +} +``` + +Another typical use case, is the creation of a command line interface (CLI) to create new instances of ORM entities. +This is actually already implemented in `aiida-core` for the `AbstractCode` class, but currently it uses a custom ad-hoc format to define the entity's model. +This AEP would allow the functionality to simply reuse the `pydantic` model instead. + +## Implementation + +As mentioned in the user interface section, the proposed implementation uses [`pydantic`](https://docs.pydantic.dev/latest/) to define the schema of ORM entities. +Each ORM entity class should define the `Model` class attribute, which is a subclass of `pydantic.BaseModel`. +Below is a stripped down version of the definition of the `Model` class for the `Entity` base class: + +```python +import typing as t +import pydantic + +from aiida.common.pydantic import MetadataField + +class Entity(abc.ABC, Generic[BackendEntityType, CollectionType]): + """Base class for all ORM entities.""" + + class Model(BaseModel): + pk: Optional[int] = MetadataField( + None, + description='The primary key of the entity. Can be `None` if the entity is not yet stored.', + exclude_to_orm=True, + ) +``` + +The `Entity` class defines just a single property, namely the entity's primary key `pk`. +Entity properties are defined through attributes on the `Model` class, as is typical in usage of `pydantic`. +An important difference, however, is that instead of using `pydantic.fields.Field` to declare the attribute's value, one should use the`aiida.common.pydantic.MetadataField`. +This function forwards most arguments to the `pydantic.Field` callable, however, it defines a number of additional keyword arguments that are specific to AiiDA's implementation. +These values are stored in the field's metadata, which are used in other parts of the implementation to control the behavior of specific model fields: +```python +import typing as t +from pydantic import BaseModel, Field +from pydantic_core import PydanticUndefined +from aiida.orm import Entity + + +def MetadataField( + default: t.Any = PydanticUndefined, + *, + orm_class: 'Entity' | str | None = None, + orm_to_model: t.Callable[['Entity'], t.Any] | None = None, + model_to_orm: t.Callable[['BaseModel'], t.Any] | None = None, + exclude_to_orm: bool = False, + **kwargs, +): + """Return a :class:`pydantic.fields.Field` instance with additional metadata. + + .. code-block:: python + + class Model(BaseModel): + + attribute: MetadataField('default', orm_class='core.node') + + This is a utility function that constructs a ``Field`` instance with an easy interface to add additional metadata. + It is possible to add metadata using ``Annotated``:: + + class Model(BaseModel): + + attribute: Annotated[str, {'metadata': 'value'}] = Field(...) + + However, when requiring multiple metadata, this notation can make the model difficult to read. Since this utility + is only used to automatically build command line interfaces from the model definition, it is possible to restrict + which metadata are accepted. + + :param orm_class: The class, or entry point name thereof, to which the field should be converted. If this field is + defined, the value of this field should acccept an integer which will automatically be converted to an instance + of said ORM class using ``orm_class.collection.get(id={field_value})``. This is useful, for example, where a + field represents an instance of a different entity, such as an instance of ``User``. The serialized data would + store the ``pk`` of the user, but the ORM entity instance would receive the actual ``User`` instance with that + primary key. The same could be accomplished by manually specifying this conversion through ``model_to_orm``, but + since this is such a common use case, the ``orm_class`` attribute is specified as a shortcut. The entry point + name as an alternative to the target class itself is supported for cases where using the class itself would + cause a cyclic import. + :param orm_to_model: Optional callable to convert the value of a field from an ORM instance to a model instance. + :param model_to_orm: Optional callable to convert the value of a field from a model instance to an ORM instance. + :param exclude_to_orm: When set to ``True``, this field value will not be passed to the ORM entity constructor + through ``Entity.from_model``. + """ + field_info = Field(default, **kwargs) + + for key, value in ( + ('orm_class', orm_class), + ('orm_to_model', orm_to_model), + ('model_to_orm', model_to_orm), + ('exclude_to_orm', exclude_to_orm), + ): + if value is not None: + field_info.metadata.append({key: value}) + + return field_info + +``` +The custom arguments are added to the `metadata` property of the `FieldInfo` instance, which takes a list of dictionaries, one dictionary for each piece of metadata. +This makes for an awkward interface to retrieve a particular piece of metadata, since one would have to loop over the list until one find's the key of the metadata of interest. +As a solution, the following utility function is added to implement this once and make it easier to retrieve field metadata: +```python +def get_metadata(field_info, key: str, default: t.Any | None = None): + """Return a the metadata of the given field for a particular key. + + :param field_info: The field from which to retrieve the metadata. + :param key: The metadata name. + :param default: Optional default value to return in case the metadata is not defined on the field. + :returns: The metadata if defined, otherwise the default. + """ + for element in field_info.metadata: + if key in element: + return element[key] + return default +``` +Below is the implementation of the `Model` for the `Node` class, which directly subclasses `Entity`: +```python +class Node(Entity['BackendNode', NodeCollection], metaclass=AbstractNodeMeta): + + class Model(Entity.Model): + uuid: Optional[str] = MetadataField(None, description='The UUID of the node', exclude_to_orm=True) + node_type: Optional[str] = MetadataField(None, description='The type of the node', exclude_to_orm=True) + process_type: Optional[str] = MetadataField( + None, description='The process type of the node', exclude_to_orm=True + ) + repository_metadata: Optional[Dict[str, Any]] = MetadataField( + None, + description='Virtual hierarchy of the file repository.', + orm_to_model=lambda node: node.base.repository.metadata, + exclude_to_orm=True, + ) + ctime: Optional[datetime.datetime] = MetadataField( + None, description='The creation time of the node', exclude_to_orm=True + ) + mtime: Optional[datetime.datetime] = MetadataField( + None, description='The modification time of the node', exclude_to_orm=True + ) + label: Optional[str] = MetadataField(None, description='The node label') + description: Optional[str] = MetadataField(None, description='The node description') + extras: Optional[Dict[str, Any]] = MetadataField( + None, + description='The node extras', + orm_to_model=lambda node: node.base.extras.all, + ) + computer: Optional[int] = MetadataField( + None, + description='The PK of the computer', + orm_to_model=lambda node: node.computer.pk if node.computer else None, + orm_class=Computer, + ) + user: int = MetadataField( + description='The PK of the user who owns the node', + orm_to_model=lambda node: node.user.pk, + orm_class=User, + ) +``` +Here, three out of the four custom field keywords are used. +Below, the purpose of all four keywords are explained. + +* `exclude_to_orm`: +This boolean is set to `True` for those fields that should not be passed to the constructor of the ORM class when calling `Entity.from_model`. +The reason is that these fields are not actually accepted by the constructor. +This in turn is because these fields are rather determined by defaults defined on the database layer or the ORM itself, and therefore should not be defined by the user. +Examples are the `pk` and the `node_type`. +The former is automatically generated by the database and the `node_type` is determined by AiiDA itself based on the entity class. + +* `orm_to_model`: +The `Entity.to_model` implementation will determine the field values for the model instances by accessing it as an attribute on the ORM instance. +For example, for the `pk` field, the implementation assumes that the class has the `pk` property that returns the corresponding value. +The `orm_to_model` keyword allows to specify a custom callable that takes the ORM instance as the single argument and should return the value for the corresponding field. +For example, the `extras` field is not retrieved as `Node.extras` but as `Node.base.extras.all`. +Therefore, the `orm_to_model` for the `extras` field of the `Node` class is set to the lambda `lambda node: node.base.extras.all`. + +* `model_to_orm`: +Certain model fields may refer to other ORM entities, such as the `user` field of the `Node` class. +Since these entities are not directly serializable, they can be represented by their integer primary key instead. +The `Entity.from_model` call should then know how to convert this type to the entity's instance. +This can be defined using the optional `model_to_orm` keyword, which takes a callable that transforms the model field value in the corresponding ORM constructor argument. + +* `orm_class`: +This keyword essentially provides a shortcut for the `model_to_orm` argument. +In most cases where `model_to_orm` is required, the referent is an ORM entity itself. +Since these can be loaded from their pk, the typical `model_to_orm` callable would look something like `Entity.collection.get(id={field_value})`. +To prevent having to define this callable each time, the `orm_class` takes the target class and the implementation will automatically query the database to convert the pk to the entity instance. + +Below the actual implementation on the `Entity` base class that makes it possible to automatically convert between ORM instances, `pydantic` model instances and JSON serialized representations: +```python +class Entity(abc.ABC, Generic[BackendEntityType, CollectionType]): + """Base class for all ORM entities.""" + + def serialize(self) -> dict[str, Any]: + """Serialize the entity instance to JSON.""" + return self.to_model().model_dump() + + @classmethod + def from_serialized(cls, **kwargs: dict[str, Any]) -> 'Entity': + """Construct an entity instance from JSON serialized data.""" + return cls.from_model(cls.Model(**kwargs)) + + def to_model(self) -> Model: + """Return the entity instance as an instance of its model.""" + fields = {} + + for key, field in self.Model.model_fields.items(): + if orm_to_model := get_metadata(field, 'orm_to_model'): + fields[key] = orm_to_model(self) + else: + fields[key] = getattr(self, key) + + return self.Model(**fields) + + @classmethod + def from_model(cls, model: Model) -> 'Entity': + """Return an entity instance from an instance of its model.""" + fields = cls.model_to_orm_field_values(model) + return cls(**fields) + + @classmethod + def model_to_orm_fields(cls) -> dict[str, FieldInfo]: + return { + key: field for key, field in cls.Model.model_fields.items() if not get_metadata(field, 'exclude_to_orm') + } + + @classmethod + def model_to_orm_field_values(cls, model: Model) -> dict[str, Any]: + from aiida.plugins.factories import BaseFactory + + fields = {} + + for key, field in cls.model_to_orm_fields().items(): + field_value = getattr(model, key) + + if field_value is None: + continue + + if orm_class := get_metadata(field, 'orm_class'): + if isinstance(orm_class, str): + try: + orm_class = BaseFactory('aiida.orm', orm_class) + except EntryPointError as exception: + raise EntryPointError( + f'The `orm_class` of `{cls.__name__}.Model.{key} is invalid: {exception}' + ) from exception + try: + fields[key] = orm_class.collection.get(id=field_value) + except NotExistent as exception: + raise NotExistent(f'No `{orm_class}` found with pk={field_value}') from exception + elif model_to_orm := get_metadata(field, 'model_to_orm'): + fields[key] = model_to_orm(field_value) + else: + fields[key] = field_value + + return fields + +``` +Essentially, each ORM entity can be represented in three "formats": + +* AiiDA's ORM instance +* `pydantic.BaseModel` instance +* JSON serialized data + +The `to_model/from_model` convert from AiiDA's ORM to the pydantic model representation, and vice versa. +The `serialize/from_serialized` parallel pair, converts directly from AiiDA's ORM to a JSON representation, and vice versa. + + +## Design discussion + +The main design decision is the choice to use `pydantic` to define the entity models. + +### Advantages +The reason is that this package has become the de-facto standard in the Python ecosystem for defining data schemas. +As a result, it is battle-tested and has become very reliable. +With the recent release of v2, the performance has also been improved significantly with a lot of the data wrangling now being offloaded to Rust instead of in Python. +It also has widespread support in other tools that are often used in the Python community, such as FastAPI, as shown in the [example use cases](#example-use-cases) section. + +### Disadvantages +A potential disadvantage would be that `pydantic` would have to be added as a direct dependency. +However, `pydantic` is already a direct dependency of `aiida-core` so this AEP would not change anything in this regard. +A downside, or even shortcoming of the current user interface is the fact that the model adds an additional, somewhat independent representation layer. +That is to say, to use the validation of the model schema, one has to go explicitly through its constructor instead of constructing the ORM instance directly. +This is somewhat alleviated by the `Entity` base class implementing the `from_serialized` method, that still provides a relatively terse alternative. +The reason is that the ORM interface is such an integral part of AiiDA's API that cannot be broken, and integrating the new model schema more tightly into the ORM interface, would almost certainly be backwards incompatible, which is not acceptable for this part of the API. + +### Alternatives +An alternative would be to use [python-jsonschema](https://python-jsonschema.readthedocs.io/en/stable/). +While it would provide all the necessary functionality to implement the feature requirements, it would require more explicit implementation in `aiida-core` as well as by plugin developers. +Pydantic has a cleaner and simpler interface by relying on Python's type annotation system. +The `python-jsonschema` package also has less adoption in the Python community and so less support in other tools. diff --git a/010_orm_schema/screenshot_docs_fastapi.png b/010_orm_schema/screenshot_docs_fastapi.png new file mode 100644 index 0000000000000000000000000000000000000000..d60520c66a33439ab7a5c8c6d446b450844c2127 GIT binary patch literal 45583 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 literal 0 HcmV?d00001 diff --git a/README.md b/README.md index f46bf70..9694951 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Accepted AEPs can be easily browsed and read on the [online documentation](https | 007 | implemented | [Abstract and improve the file repository](007_improved_file_repository/readme.md) | | 008 | implemented | [Allow `CalcJob`s to be actively monitored and interrupted](008_calcjob_monitors/readme.md) | | 009 | implemented | [Improved Docker images](009_improved_docker_images/readme.md) | +| 010 | draft | [ORM schema](010_orm_schema/readme.md) | ## Submitting an AEP diff --git a/_toc.yml b/_toc.yml index 091cc2c..1ee6da1 100644 --- a/_toc.yml +++ b/_toc.yml @@ -12,3 +12,4 @@ subtrees: - file: 007_improved_file_repository/readme.md - file: 008_calcjob_monitors/readme.md - file: 009_improved_docker_images/readme.md + - file: 010_orm_schema/readme.md