xnatpy attempts to expose objects in XNAT as native feeling Python objects. The objects reflect the state of XNAT and changes to the objects automatically update the server.
To facilitate this xnatpy scans the server xnat.xsd and creates a Python class structure to mimic this is well as possible.
Current features:
Missing features (aka my TODO list):
import os
import random
First we need to set up an xnatpy session. The session scans the xnat.xsd, creates classes, logs in into XNAT, and keeps the connection alive using a hearbeat.
import xnat
print('Loaded xnatpy version {}'.format(xnat.__version__))
session = xnat.connect('https://central.xnat.org', user='xnatpydemo', password='demo2017')
This will create an XNAT session object, which is the base class for all operations with the XNAT server. It also create a keep alive thread that makes sure the session is polled every 14 minutes to keep the connection alive.
To save your login you set up a .netrc file with the correct information about the target host. A simple example of a .netrc file can be found at http://www.mavetju.org/unix/netrc.php.
It is possible to set the login information on connect without using a netrc file.
All the options of the connect function can be found in the docstring. You can turn of certificate verification for self signed certificates, change log options, disbale use of extension type.
help(xnat.connect)
The session is the main entry point for the module. It has some basic REST functions on which the rest of the package is built, these are the get, put, post, delete, head, download and upload methods. They are very similar to the requests package but contain helpers that build the total URL from a REST path and check the resulting HTML code and content somewhat.
help(session.get)
response = session.get('/data/projects/xnatpydemo/subjects')
print(response)
print('\n\n')
print(response.text)
print('\n\n')
print(session.get_json('/data/projects/xnatpydemo/subjects'))
xnatpy creates a class hierarchy based on the xnat.xsd and other schemas it can find on the XNAT server (including extension types). These classes mirror the behaviour described in the xsd as best as possible in Python. The idea is that it feels like you are manipulating python objects, while in fact you are working on the XNAT server. It completely abstract the REST interface away.
The entry points for this are the projects, subject and experiments properties in the session object:
sandbox = session.projects['xnatpydemo']
print(sandbox)
sandbox.description
new_description = 'Random_project_description_{}'.format(random.randint(0, 100))
print('Changing description to: {}'.format(new_description))
sandbox.description = new_description
sandbox.description
# Get a list of the subjects
sandbox.subjects
Note that the entries are in the form (CENTRAL_S01824, custom_label): <SubjectData CENTRAL_S01824>
. This does not mean that the key is (CENTRAL_S01824, custom_label)
, but that both the keys CENTRAL_S01824
and custom_label
can be used for lookup. The first key is always the XNAT internal id, the second key is defined as:
subject = sandbox.subjects['subject001']
print('Before change:')
print('Gender: {}'.format(subject.demographics.gender))
print('Initials: {}'.format(subject.initials))
# Change gender and initials. Flip them between male and female and JC and PI
subject.demographics.gender = 'female' if subject.demographics.gender == 'male' else 'male'
subject.initials = 'JC' if subject.initials == 'PI' else 'PI'
print('After change:')
print('Gender: {}'.format(subject.demographics.gender))
print('Initials: {}'.format(subject.initials))
There is some basic value checking before assignment are carried out. It uses the xsd directives when available. For example:
subject.demographics.gender = 'martian'
If you have a huge database and would like to skip (parts of) the listings, you can create an XNAT object straight from a REST path. It will automatically figure out the correct class from the XNAT response. This object then can be use just as any other xnatpy object:
subject = session.create_object('/data/projects/xnatpydemo/subjects/Case001')
print subject
print subject.experiments
You can see the mapping of classes created by xnatpy using the class lookup:
import pprint
pprint.pprint(session.XNAT_CLASS_LOOKUP)
print('Subject XSI: {}'.format(subject.__xsi_type__))
print('Subject xpath: {}'.format(subject.xpath))
print('Subject demographics XSI: {}'.format(subject.demographics.__xsi_type__))
print('Subject demographics XSI: {}'.format(subject.demographics.xpath))
In xnatpy custom variables are exposed as a simple mapping type that is very similar to a dictionary.
print(subject.fields)
subject.
# Add something
subject.fields['test_field'] = 42
print(subject.fields)
subject.fields['test_field']
Note that custom variables are always stored as string in the database. So the value is always casted to a string. Also the length of a value is limited because they are passed on the requested url.
The custom variables are by default not visible in the UI, there are special settings in the UI to make them appear. Defined variables in the UI that are not set, are just not appearing in the fields dictionary.
To avoid KeyError
, it would be best to use subject.fields.get('field_name')
which returns None when not available.
Downloading with xnatpy is fairly simple. Most objects that are downloadable have a .download
method. There is also a download_dir
method that downloads the zip and unpacks it to the target directory.
download_dir = os.path.expanduser('~/xnatpy_temp')
print('Using {} as download directory'.format(download_dir))
if not os.path.exists(download_dir):
os.makedirs(download_dir)
sandbox.subjects['Case001'].download_dir(download_dir)
xnatpy exposes the import service of xnat (documentation at https://xnat.readthedocs.io/en/latest/xnat.html#xnat.services.Services )
zip_file = os.path.join(download_dir, 'Case001.zip')
#sandbox.subjects['Case001'].experiments['Case001'].download(zip_file)
subject_name = "import_subject_{}".format(random.randint(0, 999))
print('Importing under new subject: {}'.format(subject_name))
session.services.import_(zip_file, destination='/prearchive', project='xnatpydemo', subject=subject_name)
xnatpy also offers an interface to the prearchive using the session.prearchive.session method, which list the PrearchiveSessions. These object can be used to inspect a session in the prearchive and to manipulate it (e.g. delete, move, archive)
More info can be found at https://xnat.readthedocs.io/en/latest/xnat.html#xnat.prearchive.PrearchiveSession
print("-- Before archive --")
print('prearchive:')
print session.prearchive.sessions()
print('project:')
print sandbox.subjects
# Create a new name for subject and experiment
pas = session.prearchive.sessions()[0]
nr = random.randint(0, 999)
subject_name = "archive_subject_{}".format(nr)
experiment_name = "archive_exp_{}".format(nr)
print('Using name: {} / {}'.format(subject_name, experiment_name))
# Archive subject under different name
pas.archive(subject=subject_name, experiment=experiment_name)
print("-- After archive --")
print('prearchive:')
print session.prearchive.sessions()
print('project:')
print sandbox.subjects.clearcache()
print sandbox.subjects
You can get some information about the users on the server using the REST API:
print(session.users['xnatpydemo'])
print(session.users['xnatpydemo'].first_name)
print(session.users['xnatpydemo'].email)
There is also an inspect module similar to that of pyxnat. It allows you to list datatypes and search fields defined on the server:
print('== Datatypes ==')
print(session.inspect.datatypes())
print('== Data fields ==')
print(session.inspect.datafields('xnat:subjectData'))
# Don't forget to disconnect to close cleanly and clean up temporary things!
session.disconnect()
It is also possible to use xnatpy in a context, which guarantees clean closure of the connections etc.
with xnat.connect('https://central.xnat.org', user='xnatpydemo', password='demo2017') as session:
print('Nosetests project description: {}'.format(session.projects['xnatpydemo'].description))
# Here the session will be closed properly, even if there were exceptions within the `with` context
Currently I am thinking on two different additions and how to implement that best.
This illustrates my current ideas, but these are not implemented yet. If you have an opinion about this, let me know!
or alternatively
with xnat.connect('https://central.xnat.org', user='xnatpydemo', password='demo2017') as session:
print session.classes.SubjectData.query().filter(session.classes.SubjectData.initials == 'JC').all()
But this does not: