REST API#

AiiDA REST API 由两个主要类组成:

  • App,继承自 flask.Flask

  • AiidaApi,继承于 flask_restful.Api。该类定义了 REST API 提供的资源。

需要通过设置 api.app = appAiidaApi``(我们称之为  ``api)和 App``(我们称之为  ``app)的实例耦合起来。

扩展 REST API#

下面,我们将通过一个最小的示例来创建一个 API,通过添加一个端点 /new-endpoint 来扩展 AiiDA REST API。端点实现 GET 请求,检索最新创建的 Dict 节点,并返回其 id、ISO 8601 格式的 ctimeattributes

警告

REST API 目前是只读的,不支持在数据库中创建新数据或更改现有数据的端点。请参阅 this AiiDA enhancement proposal draft 了解这方面的努力。

为了实现这一目标,我们需要

  • 创建将绑定到新端点的 flask_restful.Resource 类。

  • 扩展 AiidaApi 类,以注册新的端点。

  • (可选)扩展 App 类以进行更多定制。

让我们先将以下代码放入文件 api.py

#!/usr/bin/env python
import click
from aiida import load_profile
from aiida.restapi import common
from aiida.restapi.api import AiidaApi, App
from aiida.restapi.run_api import run_api
from flask_restful import Resource


class NewResource(Resource):
    """Resource implementing a GET method returning id, ctime, and attributes of the latest created Dict."""

    def get(self):
        from aiida.orm import Dict, QueryBuilder

        query = QueryBuilder()
        query.append(Dict, project=['id', 'ctime', 'attributes'], tag='pdata')
        query.order_by({'pdata': {'ctime': 'desc'}})
        result = query.first()

        # Results are returned as a dictionary, datetime objects are serialized as ISO 8601
        return dict(id=result[0], ctime=result[1].isoformat(), attributes=result[2])


class NewApi(AiidaApi):
    def __init__(self, app=None, **kwargs):
        """This init serves to add new endpoints to the basic AiiDA Api"""
        super().__init__(app=app, **kwargs)

        self.add_resource(NewResource, '/new-endpoint/', strict_slashes=False)


# processing the options and running the app
CONFIG_DIR = common.__path__[0]


@click.command()
@click.option('-P', '--port', type=click.INT, default=5000, help='Port number')
@click.option('-H', '--hostname', default='127.0.0.1', help='Hostname')
@click.option(
    '-c',
    '--config-dir',
    'config',
    type=click.Path(exists=True),
    default=CONFIG_DIR,
    help='the path of the configuration directory',
)
@click.option('--debug', 'debug', is_flag=True, default=False, help='run app in debug mode')
@click.option(
    '--wsgi-profile',
    'wsgi_profile',
    is_flag=True,
    default=False,
    help='to use WSGI profiler middleware for finding bottlenecks in web application',
)
def newendpoint(**kwargs):
    """Runs the REST api"""
    # Invoke the runner
    run_api(App, NewApi, **kwargs)


# main program
if __name__ == '__main__':
    """
    Run the app with the provided options. For example:
    python example.py --hostname=127.0.0.2 --port=6000
    """

    load_profile()
    newendpoint()

现在,我们将一步一步地查看之前的代码。

首先是 imports。

from aiida.restapi.api import AiidaApi, App
from aiida.restapi.run_api import run_api
from flask_restful import Resource

首先,我们需要 import 扩展/使用基类: AiidaApiApp。为简单起见,建议使用 import 方法 run_api,因为它提供了配置 API、解析 command-line 参数以及将代表 API 和应用程序的两个类结合起来的接口。不过,您可以参考 flask_restful 的文档,通过其 built-in 方法配置和 hook-up API。

然后,我们定义一个代表附加资源的类:

class NewResource(Resource):
    """Resource implementing a GET method returning id, ctime, and attributes of the latest created Dict."""

    def get(self):
        from aiida.orm import Dict, QueryBuilder

        query = QueryBuilder()
        query.append(Dict, project=['id', 'ctime', 'attributes'], tag='pdata')
        query.order_by({'pdata': {'ctime': 'desc'}})
        result = query.first()

        # Results are returned as a dictionary, datetime objects are serialized as ISO 8601
        return dict(
            id=result[0],
            ctime=result[1].isoformat(),
            attributes=result[2]
        )

NewResource 包含一个方法 get。为该方法选择的名称不是任意的,而是由 Flask 固定的,它被调用来响应 HTTP GET 请求。换句话说,当 API 收到指向 URL new-endpoint 的 GET 请求时,函数 NewResource.get() 就会被调用。HTTP 响应是围绕这些函数返回的数据构建的。这些数据被打包成字典,由 Flask 序列化为 JSON 数据流。Flask 可以序列化所有 Python built-in 类型(如 intfloatstr 等),而对于自定义类型的序列化,我们让您参考 Flask documentation。Flask 文档也是自定义 HTTP 响应、构建自定义 URL(如接受参数)和更高级序列化问题等主题的主要信息来源。

当你需要处理错误时,可以考虑使用 aiida.restapi.common.exceptions 中已经定义的 AiiDA REST API-specific 异常。原因将在本节稍后部分阐明。

一旦定义了新资源,我们就必须通过分配一个(或多个)端点将其注册到 API 中。这是通过 add_resource() 方法在 NewApi__init__() 中完成的:

class NewApi(AiidaApi):

    def __init__(self, app=None, **kwargs):
        """
        This init serves to add new endpoints to the basic AiiDA Api

        """
        super().__init__(app=app, **kwargs)

        self.add_resource(NewResource, '/new-endpoint/', strict_slashes=False)

按照我们的初衷,覆盖 __init__() 方法的主要目的(如果不是唯一目的的话)是向 API 注册新资源。事实上,__init__() 的一般形式是:

class NewApi(AiidaApi):

    def __init__(self, app=None, **kwargs):

        super())

        self.add_resource( ... )
        self.add_resource( ... )
        self.add_resource( ... )

        ...

在示例中,唯一的特征线是 self.add_resource(NewResource, '/new-endpoint/', strict_slashes=False)。总之,add_resource() 方法已在 Flask 中定义和记录。

最后,main 代码会配置并运行应用程序接口:

import aiida.restapi.common as common
from aiida import load_profile

CONFIG_DIR = common.__path__[0]

import click
@click.command()
@click.option('-P', '--port', type=click.INT, default=5000,
    help='Port number')
@click.option('-H', '--hostname', default='127.0.0.1',
    help='Hostname')
@click.option('-c','--config-dir','config',type=click.Path(exists=True), default=CONFIG_DIR,
    help='the path of the configuration directory')
@click.option('--debug', 'debug', is_flag=True, default=False,
    help='run app in debug mode')
@click.option('--wsgi-profile', 'wsgi_profile', is_flag=True, default=False,
    help='to use WSGI profiler middleware for finding bottlenecks in web application')

def newendpoint(**kwargs):
    """
    runs the REST api
    """
    # Invoke the runner
    run_api(App, NewApi, **kwargs)

# main program
if __name__ == '__main__':
    """
    Run the app with the provided options. For example:
    python api.py --host=127.0.0.2 --port=6000
    """

    load_profile()
    newendpoint()

click package 用于提供一个漂亮的命令行界面,以处理选项并处理传递给 newendpoint 函数的默认值。

方法 run_api() 完成了几个功能:它将 API 与 flask.Flask (即代表网络应用的 Flask 基本类)的实例耦合。这样,应用程序就配置好了,如果需要,还可以连接起来。

它需要输入

  • 代表应用程序接口和应用程序的类。

    我们强烈建议将继承自 flask.Flaskaiida.restapi.api.App 类传给 run_api(),因为它能正确处理 AiiDA RESTApi-specific 异常。

  • 位置参数,代表点击函数传递的 command-line 参数/选项。

    类型、默认值和帮助字符串可在 @click.option 定义中设置,并由命令行调用处理。

使用脚本前还有几件事要做:

  • 如果您想进一步自定义错误处理,可以从 App 的定义中获得灵感,创建派生类 NewApp(App)

  • 支持的命令行选项与 verdi restapi 相同。

    请使用 verdi restapi --help 查阅其完整文档。如果您想添加更多选项或修改现有选项,请从 run_api 中汲取灵感,创建自定义运行程序。

现在是运行 api.py 的时候了。在终端中键入

$ chmod +x api.py
$ ./api.py --port=6000
   * REST API running on http://127.0.0.1:6000/api/v4
   * Serving Flask app "aiida.restapi.run_api" (lazy loading)
   * Environment: production
     WARNING: This is a development server. Do not use it in a production deployment.
     Use a production WSGI server instead.
   * Debug mode: off
   * Running on http://127.0.0.1:6000/ (Press CTRL+C to quit)

让我们使用 curl 和 GET 方法来查询最新创建的 node:

curl http://127.0.0.2:6000/api/v4/new-endpoint/ -X GET

输出的形式(也仅是形式)应类似于

{
    "attributes": {
        "binding_energy_per_substructure_per_unit_area_units": "eV/ang^2",
        "binding_energy_per_substructure_per_unit_area": 0.0220032273047497
    },
    "ctime": "2017-04-05T16:01:06.227942+00:00",
    "id": 403504
}

而响应字典的实际值和属性字段的内部结构一般会有很大不同。

最后要说的是,在某些情况下,您可能不想使用内部 werkzeug-based 服务器。例如,您可能希望使用 wsgi 脚本通过 Apache 运行应用程序。在这种情况下,只需使用 configure_api 返回自定义对象 api

api = configure_api(App, MycloudApi, **kwargs)

通过 api.app 可以检索到 app。如 部署 REST API 服务器 所述,该代码片段成为 Apache 使用的 wsgi 文件的基本模块。此外,我们建议查阅 mod_wsgi 的文档。

备注

可选择将 catch_internal_server 变量创建为 False,以便让异常(包括 python 回溯)涌入 apache 错误日志。这在 app 仍处于开发阶段时尤其有用。

为多个配置文件提供服务#

单个 REST API 实例可以为 AiiDA 实例的所有配置文件提供数据。为了保持向后兼容性,新功能需要通过配置明确启用:

verdi config set rest_api.profile_switching true

重新启动 REST API 后,它将接受配置文件查询参数,例如

http://127.0.0.1:5000/api/v4/computers?profile=some-profile-name

如果指定的配置文件已加载,REST API 的功能与未启用配置文件切换时完全相同。如果指定了另一个配置文件,REST API 会在执行请求前首先切换配置文件。如果在请求中指定了配置文件参数,而 REST API 未启用配置文件切换功能,则会返回 400 响应。