Using QCrBoxAPIClient to interact with QCrBox¶
In addition to the API endpoints available at http://127.0.0.1:11000/api (or whatever the the production URL is), we also offer a Python API client which simplifies the serialisation of requests/responses to/from the QCrBox API using Python dataclasses.
The API client can installed by adding it to your poetry
project, if you use poetry, using
poetry add git+https://github.com/QCrBox/QCrBoxAPIClient
. It can also be installed using pip
. The client only
requires a very minimal set of dependencies, which will be installed automatically.
In the following example, we will use the QCrBox API client to upload a dataset, invoke an interactive Olex 2 session and then end the session and delete the dataset which we uploaded.
NB: you will need to delay running the API calls to closing the interactive session and deleting the dataset if you wish to interact with Olex 2. Once the session is launched, and assuming you are running a development version of QCrBox locally on your machine, you can open the VNC session with the following host address:
http://127.0.0.1:12004/vnc.html?path=vnc&autoconnect=true&resize=remote&reconnect=true&show_dot=true
%pip install --upgrade pip > /dev/null 2>&1
Note: you may need to restart the kernel to use updated packages.
%pip install git+https://github.com/QCrBox/QCrBoxAPIClient@v0.2.2 2>&1 | tail -n 1
Requirement already satisfied: typing_extensions>=4.5 in /Users/saultyevil/srsg-projects/QCrBox/QCrBox/.venv/lib/python3.12/site-packages (from anyio->httpx<0.29.0,>=0.20.0->QCrBoxAPIClient==0.2.2) (4.13.2) Note: you may need to restart the kernel to use updated packages.
To use the API client, we need to instantiate a Client
object. This provides an interface to send requests and receive
responses to/from the QCrBox registry and is a required parameter to pass to each API endpoint function. In a
development situation, the base URL to provide to the client is http://127.0.0.1:11000
. If you are trying to connect
to a live version of QCrBox, please use the appropriate URL instead, e.g. http://demo1.qcrbox.org:11000
.
from qcrboxapiclient import Client
client = Client(base_url="http://127.0.0.1:11000")
First, we'll upload a dataset to the QCrBox registry. To do that, we'll need to read the file in as binary. To make life a little bit easier, we have created Python dataclass models which are used to represent the JSON requests to the QCrBox API.
To upload a file, we need create a CreateDatasetBody
object with the binary data we want to upload.
import io
from qcrboxapiclient.types import File
from qcrboxapiclient.models import CreateDatasetBody
import pathlib
test_file = pathlib.Path("../../pyqcrbox/robot_tests/test_data/robot_test_cif.cif").resolve()
with test_file.open("rb") as f:
file = File(io.BytesIO(f.read()), test_file.name)
upload_payload = CreateDatasetBody(file)
To access the API endpoints, multiple endpoint modules are defined for each category of API endpoint. Within these modules are further sub-modules for specific actions. Each sub-module has four functions for send requests to the API.
import qcrboxapiclient.api
help(qcrboxapiclient.api)
Help on package qcrboxapiclient.api in qcrboxapiclient: NAME qcrboxapiclient.api - Contains methods for accessing the API PACKAGE CONTENTS admin (package) applications (package) calculations (package) commands (package) data_files (package) datasets (package) interactive_sessions (package) FILE /Users/saultyevil/srsg-projects/QCrBox/QCrBox/.venv/lib/python3.12/site-packages/qcrboxapiclient/api/__init__.py
import qcrboxapiclient.api.datasets
help(qcrboxapiclient.api.datasets)
Help on package qcrboxapiclient.api.datasets in qcrboxapiclient.api: NAME qcrboxapiclient.api.datasets - Contains endpoint functions for accessing the API PACKAGE CONTENTS create_dataset delete_dataset_by_id download_dataset_by_id get_dataset_by_id list_datasets FILE /Users/saultyevil/srsg-projects/QCrBox/QCrBox/.venv/lib/python3.12/site-packages/qcrboxapiclient/api/datasets/__init__.py
To upload this to the registry, we use the create_dataset
endpoint module, which is accessible in
qcrboxapiclient.api.datasets
. Each module has a sync
and an async
function, as well as sync_detailed
and
async_detailed
. The difference between sync
and sync_detailed
is the type of object returned. With sync_detailed
a httpx.Response
is returned, including the status code and other details the httpx
package tracks. When you use
sync
instead, the response from the API is serialised into QCrBox response models defined in the API client.
For successful responses, the raw JSON returned from the API takes the form (this is an example of the applications API):
{
"status": "success",
"message": "Retrieve 5 applications.",
"payload": {
"applications": [
{ ... }
]
}
}
And an error response will usually look like this:
{
"status": "error",
"error": {
"code": 500,
"message": "There was an internal server error",
"details": "...."
}
}
In both cases, both of these responses are represented by the QCrBoxResponse
and QCrBoxErrorResponse
objects in the
API client.
from qcrboxapiclient.api.datasets import create_dataset
from qcrboxapiclient.models import QCrBoxErrorResponse
# Send a request to the API to add this file to the registry and create a dataset
response = create_dataset.sync(client=client, body=upload_payload)
# There are multiple ways to check if a request was a success
if not response:
raise Exception("No response from the API")
if isinstance(response, QCrBoxErrorResponse):
raise Exception("QCrBoxErrorResponse returned from API")
# The clearest way is to check that the status is "success"
if response.status != "success":
raise Exception("Failed to upload file")
else:
dataset_id = response.payload.datasets[0].qcrbox_dataset_id
data_file_id = response.payload.datasets[0].data_files[test_file.name].qcrbox_file_id
print(response)
QCrBoxResponseDatasetsResponse(status='success', message="Created dataset: 'qcrbox_ds_0x61f374eb8efe494386bc9d2e1c2c2364'", timestamp='2025-07-16T15:07:59.817498+00:00Z', payload=DatasetsResponse(datasets=[DatasetResponse(qcrbox_dataset_id='qcrbox_ds_0x61f374eb8efe494386bc9d2e1c2c2364', data_files=DatasetResponseDataFiles(additional_properties={'robot_test_cif.cif': DataFileMetadataResponse(qcrbox_file_id='qcrbox_df_0x9fdb547199a84dc48c8f1a990935cebc', filename='robot_test_cif.cif', filetype='cif', additional_properties={})}), additional_properties={})], additional_properties={}), additional_properties={})
Each QCrBoxResponse
has a payload attribute, which contains the data requested from the API. It's worth noting at this
stage that the create_dataset
API module returns reference IDs to datasets stored in the QCrBox registry data store.
To now create an interactive session for Olex2, we will use the interactive_sessions
API module.
To launch an interactive session, we need to tell the API which application we wish to launch and provide any arguments
required for that interactive sessions; usually a reference to a dataset or to a data file. These arguments are passed
to the API by using the CreateInteractiveSession
and CreateInteractiveSessionArguments
objects, where the latter is
used to define the keyword arguments passed to the command.
from qcrboxapiclient.api.interactive_sessions import create_interactive_session_with_arguments
from qcrboxapiclient.models import CreateInteractiveSession, CreateInteractiveSessionArguments
# The format of the arguments json/dict will change for each interactive session depending
# on how the application developer implemented it into QCrBox. Please check the application
# documentation to find the structure of arguments for the interactive session.
dict_arguments = {"input_file": {"data_file_id": data_file_id}}
arguments = CreateInteractiveSessionArguments.from_dict(dict_arguments)
# The CreativeInteractiveSession object requires the name and version of the application,
# as well as the arguments for the interactive session command
create_session = CreateInteractiveSession("olex2", "1.5-alpha", arguments)
# Send the request to the registry
response = create_interactive_session_with_arguments.sync(client=client, body=create_session)
# There are multiple ways to check if a request was a success
if not response:
raise Exception("No response from the API")
if isinstance(response, QCrBoxErrorResponse):
raise Exception("QCrBoxErrorResponse returned from API, failed to create interactive session")
# The clearest way is to check that the status is "success"
if response.status != "success":
raise Exception("Failed to create an interactive session")
else:
session_id = response.payload.interactive_session_id
print(response)
print("ID for interactive session:", session_id)
QCrBoxResponseInteractiveSessionIDResponse(status='success', message="Command invocation accepted: 'olex2'-'1.5-alpha'", timestamp='2025-07-16T15:07:59.877031+00:00Z', payload=InteractiveSessionIDResponse(interactive_session_id='qcrbox_calc_0x91c3b21b76b849a4a2484fcba45438c1', additional_properties={}), additional_properties={}) ID for interactive session: qcrbox_calc_0x91c3b21b76b849a4a2484fcba45438c1
To be able to start another session, we first need to close the one which we have open. In the same API module, there is an sub-module for the endpoint which closes interactive sessions.
import time
from qcrboxapiclient.api.interactive_sessions import close_interactive_session
# Sleep for 5 seconds to give the interactive session time to launch and start up
# properly
time.sleep(5)
# Call the API to close the session and return any output in the form of dataset ids
# In this case, there will be no output as we haven't interacted with the interactive
# session to create an output dataset
response = close_interactive_session.sync(client=client, id=session_id)
# There are multiple ways to check if a request was a success
if not response:
raise Exception("No response from the API")
if isinstance(response, QCrBoxErrorResponse):
raise Exception("QCrBoxErrorResponse returned from API, failed to close interactive session")
# The clearest way is to check that the status is "success"
if response.status != "success":
raise Exception("Failed to create an interactive session")
else:
output_dataset_id = response.payload.interactive_sessions[0].output_dataset_id
print(response)
print("Output dataset ID for interactive session:", output_dataset_id)
QCrBoxResponseInteractiveSessionClosedResponse(status='success', message="Closed interactive session: 'qcrbox_calc_0x91c3b21b76b849a4a2484fcba45438c1'", timestamp='2025-07-16T15:08:05.007906+00:00Z', payload=InteractiveSessionClosedResponse(interactive_sessions=[CloseInteractiveSessionResponseNATS(session_id='qcrbox_calc_0x91c3b21b76b849a4a2484fcba45438c1', status='successful', output_dataset_id='qcrbox_ds_0x3aef9e27d70d499db8b327b234b16476', error_msg='', additional_properties={})], additional_properties={}), additional_properties={}) Output dataset ID for interactive session: qcrbox_ds_0x3aef9e27d70d499db8b327b234b16476
Finally, we shall clean up after ourselves and delete the test dataset which was uploaded.
from qcrboxapiclient.api.datasets import create_dataset, delete_dataset_by_id
# For this API, we shouldn't respect a response as this calls a DELETE method. If the dataset
# was deleted successfully, the response will be `None`. If an error occurs, such as if an incorrect
# dataset ID is sent, then a QCrBoxErrorResponse is returned instead
response = delete_dataset_by_id.sync(id=dataset_id, client=client)
if isinstance(response, QCrBoxErrorResponse):
raise TypeError("Failed to delete test dataset")
print("Deleted test dataset:", dataset_id)
Deleted test dataset: qcrbox_ds_0x61f374eb8efe494386bc9d2e1c2c2364