Web API

This section provides the documentation to the web API implemented in InstrumentDB. You can get a view of the RESTful API calls using either the Swagger and Redoc pages, available at the addresses http://server/swagger/ and http://server/redoc/.

Format specifications

You can query the list of format specifications by issuing a GET command to the url http://server/api/format_specs/. To get information about one entity, append the full UUID and a slash to it: http://server/api/format_specs/ede55e77-0dd1-483e-a307-e6e67759a450/.

To create a new entity, issue a POST command and pass a JSON dictionary containing the following fields:

  • document_ref: a string uniquely identifying the document. The format of the string depends on the conventions used by the collaboration developing the instrument.

  • title: a string containing the title of the specification document.

  • doc_file: a file-like object containing the format specification document

  • doc_file_name: the file name to be suggested to the user when they want to download it

  • doc_mime_type: a string containing the MIME type of the specification document

  • file_mime_type: a string containing the MIME type of the file described by this specification document (optional).

Note the difference between doc_mime_type and file_mime_type: the former pertains to the document (DOCX, PDF, LaTeX, …), while the latter refers to the file described by the specification document. Both are used as hints to your browser: when you download the specification document or the data file, letting your webbrowser know the MIME type can enable more features, e.g., a preview within the “File save” dialog.

Here is an example, where we upload a Microsoft Word DOCX document describing how to interpret an Excel file with the details of some electronic board:

import requests as req

server = "http://127.0.0.1:8000/"

response = req.post(url=f"{server_url}/api/login",
    data={"username":"user1", "password": "passwd54321"}
)
assert response.ok
auth_header = {"Authorization": "Token " + response.json()["token"]}

response = req.post(
    server + "api/format_specs/",
    data={
        "document_ref": "DOC0001",
        "title": "Specification of the electronics",
        "doc_file_name": "DOC0001.docx",
        # The specification document is a MS Word file…
        "doc_mime_type": "application/vnd.openxmlformats-officedocument",
        # …which explains how to interpret the data file, created using MS Excel
        "file_mime_type": "application/vnd.openxmlformats-officedocument",
    },
    files={
        "doc_file": open("my_specification.docx", "rb"),
    }
    headers=auth_header,
)
assert response.ok
print(response.json())

Entities

You can query the list of entities by issuing a GET command to the url http://server/api/entities/. To get information about one entity, append the full UUID and a slash to it: http://server/api/entities/ede55e77-0dd1-483e-a307-e6e67759a450/.

To create a new entity, issue a POST command and pass a JSON dictionary containing the following fields:

  • name: a string

  • parent: the full URL to the parent entity; leave it out if the entity has no parent.

  • children: a list of full URLs for the child entities; you can leave it out.

  • quantities: a list of full URLs for each quantity belonging to this entity. Passing [] is ok.

Here is an example in Python, it creates a new entity and then deletes it immediately after:

import requests as req

server = "http://127.0.0.1:8000/"

response = req.post(url=f"{server_url}/api/login",
    data={"username":"user1", "password": "passwd54321"}
)
assert response.ok
auth_header = {"Authorization": "Token " + response.json()["token"]}

response = req.post(
    server + "api/entities/",
    data={
        "name": "my entity",
    },
    headers=auth_header,
)
assert response.ok
print(response.json())

# Output:
# {'uuid': 'f89e8597-7561-4170-bec3-9837b3f32d61',
#  'url': 'http://127.0.0.1:8000/f89e8597-7561-4170-bec3-9837b3f32d61/',
#  'name': 'my entity',
#  'parent': None,
#  'children': [],
#  'quantities': []}

# Now delete the entity that was created above
req.delete(response.json()["url"], headers=auth_header)

To alter an object, you can use the PATCH command. The following example creates an object and then modifies its name:

import requests as req

server = "http://127.0.0.1:8000/"
response = req.post(server + "api/entities/", data={
    "name": "my entity",
})
assert response.ok
url = response.json()["url"]

# This command changes "my entity" into "a better name"
req.patch(url, data={"name": "a better name"})

You can also access an entity deeply nested in the tree using the url http://server/tree/PATH. The PATH part is a nested string of entities separated by /, like for instance http://server/tree/instrument/electronic_board/board0. (Beware that InstrumentDB follows the HTTP protocol and returns a HTTP 302 FOUND signal, so your library of choice might need a further GET call to follow the alias. More advanced libraries do this automatically: this is the case of the requests library we are using in these examples.)

Quantities

You can query the list of quantities by issuing a GET command to the url http://server/api/quantities/. To get information about one entity, append the full UUID and a slash to it: http://server/api/quantities/ede55e77-0dd1-483e-a307-e6e67759a450/.

To create a new quantity, you must issue a POST command with a JSON record containing these keys:

  • name: a string

  • format_spec: the URL to a format specification object

  • parent_entity: the URL to an entity

  • data_files: a list of URLs for each data file. Passing [] is ok.

You can also access a quantity deeply nested in the tree of entities using a technique similar to the one described above for entities. If you are looking for a quantity named QUANTITY_NAME, buried in a deep branch of the tree of entities, you can use the url http://server/tree/PATH/QUANTITY_NAME/, where the PATH part is a nested string of entities separated by /. (Beware that InstrumentDB follows the HTTP protocol and returns a HTTP 302 FOUND signal, so your library of choice might need a further GET call to follow the alias. More advanced libraries do this automatically: this is the case of the requests library we are using in these examples.)

As an example, suppose that the tree of entities is the following:

instrument
|
+-- electronic_board
|
+-- telescope
    |
    +--- mirror1
    |
    +--- mirror2

You can retrieve the entity for mirror2 through the URL

http://server/tree/instrument/telescope/mirror2

Data files

You can query the list of data files by issuing a GET command to the url http://server/api/data_files/. To get information about one entity, append the full UUID and a slash to it: http://server/api/data_files/ede55e77-0dd1-483e-a307-e6e67759a450/.

To create a new data file, you must issue a POST command with a JSON record containing these keys:

  • name: a name to be used when the data file is going to be downloaded locally into an actual file.

  • upload_date: the date and time when the file was created. If not provided, the current date will be used.

  • file_data: a file-like object containing the contents of the file.

  • metadata: a JSON structure containing custom metadata associated with the data file.

  • quantity: the URL to the quantity that owns this data file.

  • spec_version: a custom string specifying which version of the specification document (associated with quantity) was used to produce this data file.

  • dependencies: a list of URLs to data files that have been used to produce this very data file (optional).

  • plot_mime_type: the MIME type of the plot associated with this data file (optional).

  • plot_file: a file-like object containing a visual representation of the data file.

  • comment: a string containing any comment (optional).

  • release_tags: a list of URLS to the releases that include this data file (optional).

Creating a POST command in Python with the requests library requires you to send the JSON and (optionally) the two files containing the data file itself and the plot. You can achieve this using both the data= and files= keywords when calling requests.post, like in the following example:

import requests as req

server_url = "http://127.0.0.1:8000"

response = req.post(url=f"{server_url}/api/login",
    data={"username":"user1", "password": "passwd54321"}
)
assert response.ok
auth_header = {"Authorization": "Token " + response.json()["token"]}

response = req.post(
    url=f"{server_url}/api/data_files/",
    data={
        "name": "My data file",
        "quantity": f"{server_url}/api/quantities/4a0c5e12-da9c-4c7a-923e-810a19974444/",
        "spec_version": "1.0",
        "metadata": "{}",
        "plot_mime_type": "image/png",  # THIS IS MANDATORY IF YOU INCLUDE "plot_file" BELOW!
    },
    files={
        "file_data": open("/local_storage/spreadsheet.xlsx", "rb"),
        "plot_file": open("/local_storage/summary_plot.png", "rb"),
    },
    headers=auth_header,
)

assert response.ok

uuid = response.json()["uuid"]
print("Data file created, UUID is ", uuid)

It is required that you specify plot_mime_type if you plan to pass plot_file like in the example above, because this will be used to determine how to show the image when browsing the database through the web interface.

If a data file is part of a release (see the section Releases below), you can access it using the url http://server/releases/RELEASE/PATH/QUANTITY, where RELEASE is the name of the release, PATH is the sequence of of entity names separated by /, and QUANTITY is the quantity which hosts the data file. For instance, if the tree of entities is the following:

instrument
|
+-- electronic_board
|
+-- telescope
    |
    +--- mirror1
    |
    +--- mirror2

and the quantity you are looking for is the CAD for mirror2 that is stored under the quantity design_cad, you can access the CAD that was saved in release v2.03 using the path

http://server/releases/v2.03/instrument/telescope/mirror2

Releases

You can query the list of releases by issuing a GET command to the url http://server/api/releases/. To get information about one release, append its name and a slash to it: http://server/api/releases/v0.28/. Finally, to download the JSON file for one release (without attachments!) append download/ to its URL: http://server/api/releases/v0.28/download/.

To create a new release, you must issue a POST command with a JSON record containing these keys:

  • tag: the name of the release. The only characters allowed here are letters, digits, the underscore and the dot.

  • rel_date: the date when the release was created. If not specified, the current date is used.

  • comment: a string containing any useful comment regarding this release (optional).

  • data_files: a list of URLs containing the data files.

To associate data files to releases, you can use one of the following approaches:

  1. Add data files to the release tag as soon as you create it;

  2. Add data files to the release tag after having created the release;

  3. Add releases to a data file.

Let’s see each of the three approaches. The first one is the simplest:

import requests as req

server = "http://127.0.0.1:8000/"

# Get authentication token (login)
response = req.post(url=f"{server_url}/api/login",
    data={"username":"user1", "password": "passwd54321"}
)
auth_header = {"Authorization": "Token " + response.json()["token"]}

# Name of the release we're going to create
release_name = "v0.10"

# These are the data files to be added to the release
data_files = [
    "http://127.0.0.1:8000/api/data_files/021d0dfa-e54a-44ca-abc8-ac1d01ed4c50/",
    "http://127.0.0.1:8000/api/data_files/791a310e-f950-4370-bcf0-bc49622847c9/",
    "http://127.0.0.1:8000/api/data_files/34c11186-2ce2-4805-9114-91ed460c6a95/",
]
# Create the release
response = requests.post(
    server + "api/releases/", data={
        "tag": release_name,
        "comment": "dummy release",
        "data_files": data_files,
    },
    headers=auth_header,
)

Let’s now consider the case where you did not pass the data_files key in the POST command above. (For instance, you were still building the list of data files.) Assuming that a release was already created, you can use PATCH commands to modify the release object, as shown in this snippet:

# We are re-using the "req" object got in the snippet above through
# the call to `requests.post`
release_info = response.json()

# This is the URL of the release we created
url = response.json()["url"]

# We are re-using "tag" and "comment" from the call to `request.post`
# above, but we might change them as well in this call, as the HTTP
# `patch` command overwrites everything.
requests.patch(
    url,
    data={
        "tag": release_info["tag"],
        "comment": release_info["comment"],
        "data_files": data_files,
    },
    headers=auth_header,
)

Alternatively, we can go through the opposite route and add the release tag to every data file in the list data_files. The following snippet is equivalent to the code above:

for cur_data_file_url in data_files:
    # Retrieve the current data file
    cur_data_file = req.get(cur_data_file_url, headers=auth_header).json()

    # Append the URL to the new release to the list of release tags
    cur_data_file["release_tags"].append(release_info["url"])

    # Modify the data file in the database
    req.patch(cur_data_file_url, data=cur_data_file, headers=auth_header)