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:
.. code-block:: text
instrument
|
+-- electronic_board
|
+-- telescope
|
+--- mirror1
|
+--- mirror2
You can retrieve the entity for ``mirror2`` through the URL
.. code-block:: text
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 :ref:`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:
.. code-block:: text
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
.. code-block:: text
http://server/releases/v2.03/instrument/telescope/mirror2
.. _releases:
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)