# NIST Public License - 2020
#
# This software was developed by employees of the National Institute of
# Standards and Technology (NIST), an agency of the Federal Government
# and is being made available as a public service. Pursuant to title 17
# United States Code Section 105, works of NIST employees are not subject
# to copyright protection in the United States. This software may be
# subject to foreign copyright. Permission in the United States and in
# foreign countries, to the extent that NIST may hold copyright, to use,
# copy, modify, create derivative works, and distribute this software and
# its documentation without fee is hereby granted on a non-exclusive basis,
# provided that this notice and disclaimer of warranty appears in all copies.
#
# THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND,
# EITHER EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED
# TO, ANY WARRANTY THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY
# IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE,
# AND FREEDOM FROM INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION
# WILL CONFORM TO THE SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE
# ERROR FREE. IN NO EVENT SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING,
# BUT NOT LIMITED TO, DIRECT, INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES,
# ARISING OUT OF, RESULTING FROM, OR IN ANY WAY CONNECTED WITH THIS SOFTWARE,
# WHETHER OR NOT BASED UPON WARRANTY, CONTRACT, TORT, OR OTHERWISE, WHETHER
# OR NOT INJURY WAS SUSTAINED BY PERSONS OR PROPERTY OR OTHERWISE, AND
# WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT OF THE RESULTS OF,
# OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER.
import argparse
import logging as _logging
import os as _os
import sys
from glob import glob as _glob
from urllib.parse import urljoin as _urljoin
import requests as _requests
from tqdm import tqdm as _tqdm
import nexusLIMS
from nexusLIMS.harvester.sharepoint_calendar import \
AuthenticationError as _authError
from nexusLIMS.utils import nexus_req as _nx_req
_logging.basicConfig()
_logger = _logging.getLogger(__name__)
_logger.setLevel(_logging.INFO)
CONFIG = nexusLIMS.get_config()
[docs]def get_workspace_id():
"""
Get the workspace ID that the user has access to (should be the Global
Public Workspace)
Returns
-------
workspace_id : str
The workspace ID
"""
# assuming there's only one workspace for this user (that is the public
# workspace)
_endpoint = _urljoin(CONFIG["cdcs_url"], 'rest/workspace/read_access')
_r = _nx_req(_endpoint, _requests.get, basic_auth=True)
if _r.status_code == 401:
raise _authError('Could not authenticate to CDCS. Are the '
'nexusLIMS_user and nexusLIMS_pass environment '
'variables set correctly?')
workspace_id = _r.json()[0]['id']
return workspace_id
[docs]def get_template_id():
"""
Get the template ID for the schema (so the record can be associated with it)
Returns
-------
template_id : str
The template ID
"""
# get the current template (XSD) id value:
_endpoint = _urljoin(CONFIG["cdcs_url"], 'rest/template-version-manager/global')
_r = _nx_req(_endpoint, _requests.get, basic_auth=True)
if _r.status_code == 401:
raise _authError('Could not authenticate to CDCS. Are the '
'nexusLIMS_user and nexusLIMS_pass environment '
'variables set correctly?')
if not _r.json():
raise ValueError("No template found in CDCS, is there any there?")
template_id = _r.json()[0]['current']
return template_id
[docs]def upload_record_content(xml_content, title):
"""
Upload a single XML record to the NexusLIMS CDCS instance.
Parameters
----------
xml_content : str
The actual content of an XML record (rather than a file)
title : str
The title to give to the record in CDCS
Returns
-------
post_r : :py:class:`~requests.Response`
The REST response returned from the CDCS instance after attempting
the upload
record_id : str
The id (on the server) of the record that was uploaded
"""
endpoint = _urljoin(CONFIG["cdcs_url"], 'rest/data/')
payload = {
'template': get_template_id(),
'title': title,
'xml_content': xml_content
}
post_r = _nx_req(endpoint, _requests.post, json=payload, basic_auth=True)
if post_r.status_code != 201:
# anything other than 201 status means something went wrong
_logger.error(f'Got error while uploading {title}:\n'
f'{post_r.text}')
return post_r
# assign this record to the public workspace
record_id = post_r.json()['id']
record_url = _urljoin(CONFIG["cdcs_url"],
f'data?id={record_id}')
wrk_endpoint = _urljoin(CONFIG["cdcs_url"],
f'rest/data/{record_id}/assign/'
f'{get_workspace_id()}')
r = _nx_req(wrk_endpoint, _requests.patch, basic_auth=True)
_logger.info(f'Record "{title}" available at {record_url}')
return post_r, record_id
[docs]def delete_record(record_id):
"""
Delete a Data record from the NexusLIMS CDCS instance via REST API
Parameters
----------
record_id : str
The id value (on the CDCS server) of the record to be deleted
Returns
-------
r : :py:class:`~requests.Response`
The REST response returned from the CDCS instance after attempting
the delete
"""
endpoint = _urljoin(CONFIG["cdcs_url"], f'rest/data/{record_id}')
r = _nx_req(endpoint, _requests.delete, basic_auth=True)
if r.status_code != 204:
# anything other than 204 status means something went wrong
_logger.error(f'Got error while deleting {record_id}:\n'
f'{r.text}')
return r
[docs]def upload_record_files(files_to_upload, progress=False):
"""
Upload a list of .xml files (or all .xml files in the current directory)
to the NexusLIMS CDCS instance using :py:meth:`upload_record_content`
Parameters
----------
files_to_upload : list or None
The list of .xml files to upload. If ``None``, all .xml files in the
current directory will be used instead.
progress : bool
Whether or not to show a progress bar for uploading
Returns
-------
files_uploaded : list of str
A list of the files that were successfully uploaded
record_ids : list of str
A list of the record id values (onthe server) that were uploaded
"""
if files_to_upload is None:
_logger.info('Using all .xml files in this directory')
files_to_upload = _glob('*.xml')
else:
_logger.info('Using .xml files from command line')
_logger.info(f'Found {len(files_to_upload)} files to upload\n')
if len(files_to_upload) == 0:
msg = 'No .xml files were found (please specify on the ' \
'command line, or run this script from a directory ' \
'containing one or more .xml files'
_logger.error(msg)
raise ValueError(msg)
files_uploaded = []
record_ids = []
for f in _tqdm(files_to_upload) if progress else files_to_upload:
with open(f, 'r') as xml_file:
xml_content = xml_file.read()
title = _os.path.basename(f)
r, record_id = upload_record_content(xml_content, title)
if r.status_code != 201:
_logger.warning(f'Could not upload {title}')
continue
else:
files_uploaded.append(f)
record_ids.append(record_id)
_logger.info(f'Successfully uploaded {len(files_uploaded)} of '
f'{len(files_to_upload)} files')
return files_uploaded, record_ids
if __name__ == '__main__': # pragma: no cover
parser = argparse.ArgumentParser(
description='Communicate with the Nexus CDCS instance')
parser.add_argument('--upload-records',
help='Upload .xml records to the the Nexus CDCS',
action='store_true')
parser.add_argument('xml',
nargs='*',
help='(used with --upload-records) '
'Files to upload (separated by space and '
'surrounded by quotes, if needed). If no files '
'are specified, all .xml files in the current '
'directory will be used instead.')
args = parser.parse_args()
if len(sys.argv) == 1:
parser.print_help()
if args.upload_records:
if len(sys.argv) == 2:
# no files were provided, so assume the user wanted to glob all
# .xml files in the current directory
upload_record_files(None)
elif len(sys.argv) > 2:
upload_record_files(args.xml)