diff --git a/.github/workflows/linter-ruff.yml b/.github/workflows/linter-ruff.yml new file mode 100644 index 0000000..d07e0f5 --- /dev/null +++ b/.github/workflows/linter-ruff.yml @@ -0,0 +1,11 @@ +name: linter-ruff + +on: pull_request + +defaults: + run: + shell: bash + +jobs: + ruff-lint: + uses: irods/irods_reusable_github_workflows/.github/workflows/linter-irods-ruff.yml@main diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4a9c4bb --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2024, The University of North Carolina at Chapel Hill +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index 02b4475..3efad86 100644 --- a/README.md +++ b/README.md @@ -4,21 +4,24 @@ This is a Python wrapper for the [iRODS HTTP API](https://github.com/irods/irods Documentation for the endpoint operations can be found [here](https://github.com/irods/irods_client_http_api/blob/main/API.md). -## Setup -**NOTICE:** This project is not yet available through pip. To use, clone the repository into the desired location. +## Install + +This wrapper is available via pip: + ``` -git clone https://github.com/irods/irods_client_http_python.git +pip install irods-http-client ``` + ## Usage To use the wrapper, follow the steps listed below. ```py -from irods_client import IrodsClient +from irods_http_client import IRODSHTTPClient # Create an instance of the wrapper with the base url of the iRODS server to # be accessed. , , and are placeholders, and need # to be replaced by appropriate values. -api = IrodsClient('http://:/irods-http-api/') +api = IRODSHTTPClient('http://:/irods-http-api/') # Most endpoint operations require a user to be authenticated in order to # be executed. Authenticate with a username and password, and store the diff --git a/irods_http_client/__init__.py b/irods_http_client/__init__.py index 05e0e52..e69d90d 100644 --- a/irods_http_client/__init__.py +++ b/irods_http_client/__init__.py @@ -1 +1,3 @@ -from .irodsHttpClient import IrodsHttpClient \ No newline at end of file +"""iRODS HTTP client library for Python.""" + +from .irods_http_client import IRODSHTTPClient as IRODSHTTPClient diff --git a/irods_http_client/collection_operations.py b/irods_http_client/collection_operations.py index 64fdaf2..7073bee 100644 --- a/irods_http_client/collection_operations.py +++ b/irods_http_client/collection_operations.py @@ -1,709 +1,340 @@ -import requests +"""Collection operations for iRODS HTTP API.""" + import json +import requests + +from . import common + + class Collections: - - def __init__(self, url_base: str): - """" - Initializes Collections with a base url. - Token is set to None initially, and updated when setToken() is called in irodsClient. - """ - self.url_base = url_base - self.token = None - - - def create(self, lpath: str, create_intermediates: int=0): - """ - Creates a new collection. - - Parameters - - lpath: The absolute logical path of the collection to be created. - - create_intermediates (optional): Set to 1 to create intermediates, otherwise set to 0. Defaults to 0. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(lpath, str)): - raise TypeError('lpath must be a string') - if (not isinstance(create_intermediates, int)): - raise TypeError('create_intermediates must be an int 1 or 0') - if ((not create_intermediates == 0) and (not create_intermediates == 1)): - raise ValueError('create_intermediates must be an int 1 or 0') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded', - } - - data = { - 'op': 'create', - 'lpath': lpath, - 'create-intermediates': create_intermediates - } - - r = requests.post(self.url_base + '/collections', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - - if rdict['irods_response']['status_code'] == 0 and rdict['created'] == False: - print('Failed to create collection: \'' + lpath + '\' already exists') - elif rdict['irods_response']['status_code']: - print('Failed to create collection \'' + lpath + '\': iRODS Status Code ' + str(rdict['irods_response']['status_code']) + ' - ' + str(rdict['irods_response']['status_message'])) - else: - print('Collection \'' + lpath + '\' created successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def remove(self, lpath: str, recurse: int=0, no_trash: int=0): - """ - Removes an existing collection. - - Parameters - - lpath: The absolute logical path of the collection to be removed. - - recurse (optional): Set to 1 to remove contents of the collection, otherwise set to 0. Defaults to 0. - - no_trash (optional): Set to 1 to move the collection to trash, 0 to permanently remove. Defaults to 0. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(lpath, str)): - raise TypeError('lpath must be a string') - if (not isinstance(recurse, int)): - raise TypeError('recurse must be an int 1 or 0') - if ((not recurse == 0) and (not recurse == 1)): - raise ValueError('recurse must be an int 1 or 0') - if (not isinstance(no_trash, int)): - raise TypeError('no_trash must be an int 1 or 0') - if ((not no_trash == 0) and (not no_trash == 1)): - raise ValueError('no_trash must be an int 1 or 0') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded', - } - - data = { - 'op': 'remove', - 'lpath': lpath, - 'recurse': recurse, - 'no-trash': no_trash - } - - r = requests.post(self.url_base + '/collections', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - - if rdict['irods_response']['status_code']: - print('Failed to remove collection \'' + lpath + '\': iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Collection \'' + lpath + '\' removed successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - elif (r.status_code / 100 == 4): #made redundant by updated else, can remove when housekeeping - rdict = r.json() - - print('Failed to remove collection \'' + lpath + '\': iRODS Status Code ' + str(rdict['irods_response']['status_code'])) - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def stat(self, lpath: str, ticket: str=''): - """ - Gives information about a collection. - - Parameters - - lpath: The absolute logical path of the collection being accessed. - - ticket (optional): Ticket to be enabled before the operation. Defaults to an empty string. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(lpath, str)): - raise TypeError('lpath must be a string') - if (not isinstance(ticket, str)): - raise TypeError('ticket must be a string') - - headers = { - 'Authorization': 'Bearer ' + self.token, - } - - params = { - 'op': 'stat', - 'lpath': lpath, - 'ticket': ticket - } - - r = requests.get(self.url_base + '/collections', params=params, headers=headers) - - if (r.status_code / 100 == 2): - rdict = r.json() - - if rdict['irods_response']['status_code']: - print('Failed to retrieve information for \'' + lpath + '\': iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Information for \'' + lpath + '\' retrieved successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def list(self, lpath: str, recurse: int=0, ticket: str=''): - """ - Shows the contents of a collection - - Parameters - - lpath: The absolute logical path of the collection to have its contents listed. - - recurse (optional): Set to 1 to list the contents of objects in the collection, otherwise set to 0. Defaults to 0. - - ticket (optional): Ticket to be enabled before the operation. Defaults to an empty string. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(lpath, str)): - raise TypeError('lpath must be a string') - if (not isinstance(recurse, int)): - raise TypeError('recurse must be an int 1 or 0') - if ((not recurse == 0) and (not recurse == 1)): - raise ValueError('recurse must be an int 1 or 0') - if (not isinstance(ticket, str)): - raise TypeError('ticket must be a string') - - headers = { - 'Authorization': 'Bearer ' + self.token, - } - - params = { - 'op': 'list', - 'lpath': lpath, - 'recurse': recurse, - 'ticket': ticket - } - - r = requests.get(self.url_base + '/collections', params=params, headers=headers) - - if (r.status_code / 100 == 2): - rdict = r.json() - - if rdict['irods_response']['status_code']: - print('Failed to retrieve list for \'' + lpath + '\': iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('List for \'' + lpath + '\' retrieved successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def set_permission(self, lpath: str, entity_name: str, permission: str, admin: int=0): - """ - Sets the permission of a user for a given collection. - - Parameters - - lpath: The absolute logical path of the collection to have a permission set. - - entity_name: The name of the user or group having its permission set. - - permission: The permission level being set. Either 'null', 'read', 'write', or 'own'. - - admin (optional): Set to 1 to run this operation as an admin, otherwise set to 0. Defaults to 0. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(lpath, str)): - raise TypeError('lpath must be a string') - if (not isinstance(entity_name, str)): - raise TypeError('entity_name must be a string') - if (not isinstance(permission, str)): - raise TypeError('permission must be a string (\'null\', \'read\', \'write\', or \'own\')') - if (not isinstance(admin, int)): - raise TypeError('admin must be an int 1 or 0') - if ((not admin == 0) and (not admin == 1)): - raise ValueError('admin must be an int 1 or 0') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded', - } - - data = { - 'op': 'set_permission', - 'lpath': lpath, - 'entity-name': entity_name, - 'permission': permission, - 'admin': admin - } - - r = requests.post(self.url_base + '/collections', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - - if rdict['irods_response']['status_code']: - print('Failed to set permission for \'' + lpath + '\': iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Permission for \'' + lpath + '\' set successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def set_inheritance(self, lpath: str, enable: int, admin: int=0): - """ - Sets the inheritance for a collection. - - Parameters - - lpath: The absolute logical path of the collection to have its inheritance set. - - enable: Set to 1 to enable inheritance, or 0 to disable. - - admin (optional): Set to 1 to run this operation as an admin, otherwise set to 0. Defaults to 0. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(lpath, str)): - raise TypeError('lpath must be a string') - if (not isinstance(enable, int)): - raise TypeError('enable must be an int 1 or 0') - if ((not enable == 0) and (not enable == 1)): - raise ValueError('enable must be an int 1 or 0') - if (not isinstance(admin, int)): - raise TypeError('admin must be an int 1 or 0') - if ((not admin == 0) and (not admin == 1)): - raise ValueError('admin must be an int 1 or 0') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded', - } - - data = { - 'op': 'set_inheritance', - 'lpath': lpath, - 'enable': enable, - 'admin': admin - } - - r = requests.post(self.url_base + '/collections', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - - operation = '' - if (enable == 1): - operation = 'enabled' - else: - operation = 'disabled' - - if rdict['irods_response']['status_code']: - print('Failed to set inheritance for \'' + lpath + '\': iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Inheritance for \'' + lpath + '\' ' + operation) - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def modify_permissions(self, lpath: str, operations: dict, admin: int=0): - """ - Modifies permissions for multiple users or groups for a collection. - - Parameters - - lpath: The absolute logical path of the collection to have its permissions modified. - - operations: Dictionary containing the operations to carry out. Should contain names and permissions for all operations. - - admin (optional): Set to 1 to run this operation as an admin, otherwise set to 0. Defaults to 0. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(lpath, str)): - raise TypeError('lpath must be a string') - if (not isinstance(operations, list)): - raise TypeError('operations must be a list of dictionaries') - if (not isinstance(operations[0], dict)): - raise TypeError('operations must be a list of dictionaries') - if (not isinstance(admin, int)): - raise TypeError('admin must be an int 1 or 0') - if ((not admin == 0) and (not admin == 1)): - raise ValueError('admin must be an int 1 or 0') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded', - } - - data = { - 'op': 'modify_permissions', - 'lpath': lpath, - 'operations': json.dumps(operations), - 'admin': admin - } - - r = requests.post(self.url_base + '/collections', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - - if rdict['irods_response']['status_code']: - print('Failed to modify permissions for \'' + lpath + '\': iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Permissions for \'' + lpath + '\' modified successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def modify_metadata(self, lpath: str, operations: dict, admin: int=0): - """ - Modifies the metadata for a collection. - - Parameters - - lpath: The absolute logical path of the collection to have its metadata modified. - - operations: Dictionary containing the operations to carry out. Should contain the operation, attribute, value, and optionally units. - - admin (optional): Set to 1 to run this operation as an admin, otherwise set to 0. Defaults to 0. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(lpath, str)): - raise TypeError('lpath must be a string') - if (not isinstance(operations, list)): - raise TypeError('operations must be a list of dictionaries') - if (not isinstance(operations[0], dict)): - raise TypeError('operations must be a list of dictionaries') - if (not isinstance(admin, int)): - raise TypeError('admin must be an int 1 or 0') - if ((not admin == 0) and (not admin == 1)): - raise ValueError('admin must be an int 1 or 0') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded', - } - - data = { - 'op': 'modify_metadata', - 'lpath': lpath, - 'operations': json.dumps(operations), - 'admin': admin - } - - r = requests.post(self.url_base + '/collections', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - - if rdict['irods_response']['status_code']: - print('Failed to modify metadata for \'' + lpath + '\': iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Metadata for \'' + lpath + '\' modified successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def rename(self, old_lpath: str, new_lpath: str): - """ - Renames or moves a collection - - Parameters - - old_lpath: The current absolute logical path of the collection. - - new_lpath: The absolute logical path of the destination for the collection. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(old_lpath, str)): - raise TypeError('old_lpath must be a string') - if (not isinstance(new_lpath, str)): - raise TypeError('new_lpath must be a string') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded', - } - - data = { - 'op': 'rename', - 'old-lpath': old_lpath, - 'new-lpath': new_lpath - } - - r = requests.post(self.url_base + '/collections', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - - if rdict['irods_response']['status_code']: - print('Failed to rename \'' + old_lpath + '\': iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('\'' + old_lpath + '\' renamed to \'' + new_lpath + '\'') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def touch(self, lpath, seconds_since_epoch=-1, reference=''): - """ - Updates mtime for a collection - - Parameters - - lpath: The absolute logical path of the collection being touched. - - seconds_since_epoch (optional): The value to set mtime to, defaults to -1 as a flag. - - reference (optional): The absolute logical path of the collection to use as a reference for mtime. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(lpath, str)): - raise TypeError('lpath must be a string') - if (not isinstance(seconds_since_epoch, int)): - raise TypeError('seconds_since_epoch must be an int') - if (not seconds_since_epoch >= -1): - raise ValueError('seconds_since_epoch must be greater than or equal to 0 or flag value -1') - if (not isinstance(reference, str)): - raise TypeError('reference must be a string') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded', - } - - data = { - 'op': 'touch', - 'lpath': lpath - } - - if (seconds_since_epoch != -1): - data['seconds-since-epoch'] = seconds_since_epoch - - if (reference != ''): - data['reference'] = reference - - r = requests.post(self.url_base + '/collections', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - - if rdict['irods_response']['status_code']: - print('Failed to update mtime for \'' + lpath + '\': iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('mtime for \'' + lpath + '\' updated successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) \ No newline at end of file + """Perform collection operations via iRODS HTTP API.""" + + def __init__(self, url_base: str): + """ + Initialize Collections with a base url. + + Token is set to None initially, and updated when setToken() is called in irodsClient. + """ + self.url_base = url_base + self.token = None + + def create(self, lpath: str, create_intermediates: int = 0): + """ + Create a new collection. + + Args: + lpath: The absolute logical path of the collection to be created. + create_intermediates: Set to 1 to create intermediates, otherwise set to 0. Defaults to 0. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(lpath, str) + common.validate_0_or_1(create_intermediates) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = { + "op": "create", + "lpath": lpath, + "create-intermediates": create_intermediates, + } + + r = requests.post(self.url_base + "/collections", headers=headers, data=data, timeout=30) + return common.process_response(r) + + def remove(self, lpath: str, recurse: int = 0, no_trash: int = 0): + """ + Remove an existing collection. + + Args: + lpath: The absolute logical path of the collection to be removed. + recurse: Set to 1 to remove contents of the collection, otherwise set to 0. Defaults to 0. + no_trash: Set to 1 to move the collection to trash, 0 to permanently remove. Defaults to 0. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(lpath, str) + common.validate_0_or_1(recurse) + common.validate_0_or_1(no_trash) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = { + "op": "remove", + "lpath": lpath, + "recurse": recurse, + "no-trash": no_trash, + } + + r = requests.post(self.url_base + "/collections", headers=headers, data=data, timeout=30) + return common.process_response(r) + + def stat(self, lpath: str, ticket: str = ""): + """ + Give information about a collection. + + Args: + lpath: The absolute logical path of the collection being accessed. + ticket: Ticket to be enabled before the operation. Defaults to an empty string. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(lpath, str) + common.validate_instance(ticket, str) + + headers = { + "Authorization": "Bearer " + self.token, + } + + params = {"op": "stat", "lpath": lpath, "ticket": ticket} + + r = requests.get(self.url_base + "/collections", params=params, headers=headers, timeout=30) + return common.process_response(r) + + def list(self, lpath: str, recurse: int = 0, ticket: str = ""): + """ + Show the contents of a collection. + + Args: + lpath: The absolute logical path of the collection to have its contents listed. + recurse: Set to 1 to list the contents of objects in the collection, + otherwise set to 0. Defaults to 0. + ticket: Ticket to be enabled before the operation. Defaults to an empty string. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(lpath, str) + common.validate_0_or_1(recurse) + common.validate_instance(ticket, str) + + headers = { + "Authorization": "Bearer " + self.token, + } + + params = {"op": "list", "lpath": lpath, "recurse": recurse, "ticket": ticket} + + r = requests.get(self.url_base + "/collections", params=params, headers=headers, timeout=30) + return common.process_response(r) + + def set_permission(self, lpath: str, entity_name: str, permission: str, admin: int = 0): + """ + Set the permission of a user for a given collection. + + Args: + lpath: The absolute logical path of the collection to have a permission set. + entity_name: The name of the user or group having its permission set. + permission: The permission level being set. Either 'null', 'read', 'write', or 'own'. + admin: Set to 1 to run this operation as an admin, otherwise set to 0. Defaults to 0. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + + Raises: + ValueError: If permission is not 'null', 'read', 'write', or 'own'. + """ + common.check_token(self.token) + common.validate_instance(lpath, str) + common.validate_instance(entity_name, str) + common.validate_instance(permission, str) + if permission not in ["null", "read", "write", "own"]: + raise ValueError("permission must be either 'null', 'read', 'write', or 'own'") + common.validate_0_or_1(admin) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = { + "op": "set_permission", + "lpath": lpath, + "entity-name": entity_name, + "permission": permission, + "admin": admin, + } + + r = requests.post(self.url_base + "/collections", headers=headers, data=data, timeout=30) + return common.process_response(r) + + def set_inheritance(self, lpath: str, enable: int, admin: int = 0): + """ + Set the inheritance for a collection. + + Args: + lpath: The absolute logical path of the collection to have its inheritance set. + enable: Set to 1 to enable inheritance, or 0 to disable. + admin: Set to 1 to run this operation as an admin, otherwise set to 0. Defaults to 0. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(lpath, str) + common.validate_0_or_1(enable) + common.validate_0_or_1(admin) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = { + "op": "set_inheritance", + "lpath": lpath, + "enable": enable, + "admin": admin, + } + + r = requests.post(self.url_base + "/collections", headers=headers, data=data, timeout=30) + return common.process_response(r) + + def modify_permissions(self, lpath: str, operations: dict, admin: int = 0): + """ + Modify permissions for multiple users or groups for a collection. + + Args: + lpath: The absolute logical path of the collection to have its permissions modified. + operations: Dictionary containing the operations to carry out. Should contain names + and permissions for all operations. + admin: Set to 1 to run this operation as an admin, otherwise set to 0. Defaults to 0. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(lpath, str) + common.validate_instance(operations, list) + common.validate_instance(operations[0], dict) + common.validate_0_or_1(admin) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = { + "op": "modify_permissions", + "lpath": lpath, + "operations": json.dumps(operations), + "admin": admin, + } + + r = requests.post(self.url_base + "/collections", headers=headers, data=data, timeout=30) + return common.process_response(r) + + def modify_metadata(self, lpath: str, operations: dict, admin: int = 0): + """ + Modify the metadata for a collection. + + Args: + lpath: The absolute logical path of the collection to have its metadata modified. + operations: Dictionary containing the operations to carry out. Should contain the + operation, attribute, value, and optionally units. + admin: Set to 1 to run this operation as an admin, otherwise set to 0. Defaults to 0. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(lpath, str) + common.validate_instance(operations, list) + common.validate_instance(operations[0], dict) + common.validate_0_or_1(admin) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = { + "op": "modify_metadata", + "lpath": lpath, + "operations": json.dumps(operations), + "admin": admin, + } + + r = requests.post(self.url_base + "/collections", headers=headers, data=data, timeout=30) + return common.process_response(r) + + def rename(self, old_lpath: str, new_lpath: str): + """ + Rename or move a collection. + + Args: + old_lpath: The current absolute logical path of the collection. + new_lpath: The absolute logical path of the destination for the collection. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(old_lpath, str) + common.validate_instance(new_lpath, str) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = {"op": "rename", "old-lpath": old_lpath, "new-lpath": new_lpath} + + r = requests.post(self.url_base + "/collections", headers=headers, data=data, timeout=30) + return common.process_response(r) + + def touch(self, lpath, seconds_since_epoch=-1, reference=""): + """ + Update mtime for a collection. + + Args: + lpath: The absolute logical path of the collection being touched. + seconds_since_epoch: The value to set mtime to, defaults to -1 as a flag. + reference: The absolute logical path of the collection to use as a reference for mtime. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(lpath, str) + common.validate_gte_minus1(seconds_since_epoch) + common.validate_instance(reference, str) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = {"op": "touch", "lpath": lpath} + + if seconds_since_epoch != -1: + data["seconds-since-epoch"] = seconds_since_epoch + + if reference != "": + data["reference"] = reference + + r = requests.post(self.url_base + "/collections", headers=headers, data=data, timeout=30) + return common.process_response(r) diff --git a/irods_http_client/common.py b/irods_http_client/common.py new file mode 100644 index 0000000..e24210a --- /dev/null +++ b/irods_http_client/common.py @@ -0,0 +1,93 @@ +"""Common utility functions for iRODS HTTP client operations.""" + + +def process_response(r): + """ + Process an HTTP response and return standardized response dict. + + Args: + r: The HTTP response object. + + Returns: + A dict with 'status_code' and 'data' keys containing the HTTP status code + and parsed JSON response body (or None if response is empty). + """ + rdict = r.json() if r.text != "" else None + return {"status_code": r.status_code, "data": rdict} + + +def check_token(t): + """ + Verify that an authentication token is set. + + Args: + t: The authentication token to check. + + Raises: + RuntimeError: If the token is None. + """ + if t is None: + raise RuntimeError("No token set. Use setToken() to set the auth token to be used.") + + +def validate_instance(x, expected_type): + """ + Validate that a value is an instance of the expected type. + + Args: + x: The value to validate. + expected_type: The expected type. + + Raises: + TypeError: If x is not an instance of expected_type. + """ + if not isinstance(x, expected_type): + raise TypeError + + +def validate_0_or_1(x): + """ + Validate that a value is either 0 or 1. + + Args: + x: The value to validate (must be an int). + + Raises: + TypeError: If x is not an integer. + ValueError: If x is not 0 or 1. + """ + validate_instance(x, int) + if x not in [0, 1]: + raise ValueError(f"{x} must be 0 or 1") + + +def validate_gte_zero(x): + """ + Validate that a value is greater than or equal to zero. + + Args: + x: The value to validate (must be an int). + + Raises: + TypeError: If x is not an integer. + ValueError: If x is less than 0. + """ + validate_instance(x, int) + if not x >= 0: + raise ValueError(f"{x} must be >= 0") + + +def validate_gte_minus1(x): + """ + Validate that a value is greater than or equal to -1. + + Args: + x: The value to validate (must be an int). + + Raises: + TypeError: If x is not an integer. + ValueError: If x is less than -1. + """ + validate_instance(x, int) + if not x >= -1: + raise ValueError(f"{x} must be >= 0, or flag value of -1") diff --git a/irods_http_client/data_object_operations.py b/irods_http_client/data_object_operations.py index be818fd..4d99b97 100644 --- a/irods_http_client/data_object_operations.py +++ b/irods_http_client/data_object_operations.py @@ -1,1511 +1,894 @@ -import requests +"""Data object operations for iRODS HTTP API.""" + import json +import requests + +from . import common + + class DataObjects: - def __init__(self, url_base: str): - """" - Initializes DataObjects with a base url. - Token is set to None initially, and updated when setToken() is called in irodsClient. - """ - self.url_base = url_base - self.token = None - - - def touch(self, lpath, no_create: int=0, replica_number: int=-1, leaf_resources: str='', seconds_since_epoch=-1, reference=''): - """ - Updates mtime for an existing data object or creates a new one - - Parameters - - lpath: The absolute logical path of the data object being touched. - - no_create (optional): Set to 1 to prevent creating a new object, otherwise set to 0. - - replica_number (optional): The replica number of the target replica. - - leaf_resources (optional): The resource holding an existing replica. If one does not exist, creates one. - - seconds_since_epoch (optional): The value to set mtime to, defaults to -1 as a flag. - - reference (optional): The absolute logical path of the data object to use as a reference for mtime. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(lpath, str)): - raise TypeError('lpath must be a string') - if (not isinstance(no_create, int)): - raise TypeError('no_create must be an int 1 or 0') - if ((not no_create == 0) and (not no_create == 1)): - raise ValueError('no_create must be an int 1 or 0') - if (not isinstance(replica_number, int)): - raise TypeError('replica_number must be an int') - if (not replica_number >= -1): - raise ValueError('replica_number must be greater than or equal to 0 or flag value -1') - if (not isinstance(leaf_resources, str)): - raise TypeError('leaf_resources must be a string') - if (not isinstance(seconds_since_epoch, int)): - raise TypeError('seconds_since_epoch must be an int') - if (not seconds_since_epoch >= -1): - raise ValueError('seconds_since_epoch must be greater than or equal to 0 or flag value -1') - if (not isinstance(reference, str)): - raise TypeError('reference must be a string') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded', - } - - data = { - 'op': 'touch', - 'lpath': lpath, - 'no-create': no_create - } - - if (seconds_since_epoch != -1): - data['seconds-since-epoch'] = seconds_since_epoch - - if (replica_number != -1): - data['replica-number'] = replica_number - - if (leaf_resources != ''): - data['leaf-resources'] = leaf_resources - - if (reference != ''): - data['reference'] = reference - - r = requests.post(self.url_base + '/data-objects', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - if rdict['irods_response']['status_code']: - print('Failed to touch data object \'' + lpath + '\': iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Data object \'' + lpath + '\' touched successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def remove(self, lpath: str, catalog_only: int=0, no_trash: int=0, admin: int=0): - """ - Removes an existing data object. - - Parameters - - lpath: The absolute logical path of the data object to be removed. - - catalog_only (optional): Set to 1 to remove only the catalog entry, otherwise set to 0. Defaults to 0. - - no_trash (optional): Set to 1 to move the data object to trash, 0 to permanently remove. Defaults to 0. - - admin (optional): Set to 1 to run this operation as an admin, otherwise set to 0. Defaults to 0. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(lpath, str)): - raise TypeError('lpath must be a string') - if (not isinstance(catalog_only, int)): - raise TypeError('catalog_only must be an int 1 or 0') - if ((not catalog_only == 0) and (not catalog_only == 1)): - raise ValueError('catalog_only must be an int 1 or 0') - if (not isinstance(no_trash, int)): - raise TypeError('no_trash must be an int 1 or 0') - if ((not no_trash == 0) and (not no_trash == 1)): - raise ValueError('no_trash must be an int 1 or 0') - if (not isinstance(admin, int)): - raise TypeError('admin must be an int 1 or 0') - if ((not admin == 0) and (not admin == 1)): - raise ValueError('admin must be an int 1 or 0') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded', - } - - data = { - 'op': 'remove', - 'lpath': lpath, - 'catalog-only': catalog_only, - 'no-trash': no_trash, - 'admin': admin - } - - r = requests.post(self.url_base + '/data-objects', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - if rdict['irods_response']['status_code']: - print('Failed to remove data object \'' + lpath + '\': iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Data object \'' + lpath + '\' removed successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - elif (r.status_code / 100 == 4): - rdict = r.json() - print('Failed to remove data object \'' + lpath + '\': iRODS Status Code ' + str(rdict['irods_response']['status_code'])) - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def calculate_checksum(self, lpath: str, resource: str='', replica_number: int=-1, force: int=0, all: int=0, admin: int=0): - """ - Calculates the checksum for a data object. - - Parameters - - lpath: The absolute logical path of the data object to have its checksum calculated. - - resource (optional): The resource holding the existing replica. - - replica_number (optional): The replica number of the target replica. - - force (optional): Set to 1 to replace the existing checksum, otherwise set to 0. Defaults to 0. - - all (optional): Set to 1 to calculate the checksum for all replicas, otherwise set to 0. Defaults to 0. - - admin (optional): Set to 1 to run this operation as an admin, otherwise set to 0. Defaults to 0. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(lpath, str)): - raise T('lpath must be a string') - if (not isinstance(resource, str)): - raise T('resource must be a string') - if (not isinstance(replica_number, int)): - raise T('replica_number must be an int') - if (not replica_number >= -1): - raise ValueError('replica number must be greater than or equal to 0 or flag value -1') - if (not isinstance(force, int)): - raise TypeError('force must be an int 1 or 0') - if ((not force == 0) and (not force == 1)): - raise ValueError('force must be an int 1 or 0') - if (not isinstance(all, int)): - raise TypeError('all must be an int 1 or 0') - if ((not all == 0) and (not all == 1)): - raise ValueError('all must be an int 1 or 0') - if (not isinstance(admin, int)): - raise TypeError('admin must be an int 1 or 0') - if ((not admin == 0) and (not admin == 1)): - raise ValueError('admin must be an int 1 or 0') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded', - } - - data = { - 'op': 'calculate_checksum', - 'lpath': lpath, - 'force': force, - 'all': all, - 'admin': admin - } - - if (resource != ''): - data['resource'] = resource - - if (replica_number != -1): - data['replica-number'] = replica_number - - - r = requests.post(self.url_base + '/data-objects', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - if rdict['irods_response']['status_code']: - print('Failed to calculate checksum for data object \'' + lpath + '\': iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Checksum for data object \'' + lpath + '\' calculated successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def verify_checksum(self, lpath: str, resource: str='', replica_number: int=-1, compute_checksums: int=0, admin: int=0): - """ - Verifies the checksum for a data object. - - Parameters - - lpath: The absolute logical path of the data object to have its checksum verified. - - resource (optional): The resource holding the existing replica. - - replica_number (optional): The replica number of the target replica. - - compute_checksums (optional): Set to 1 to skip checksum calculation, otherwise set to 0. Defaults to 0. - - admin (optional): Set to 1 to run this operation as an admin, otherwise set to 0. Defaults to 0. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(lpath, str)): - raise TypeError('lpath must be a string') - if (not isinstance(resource, str)): - raise TypeError('resource must be a string') - if (not isinstance(replica_number, int)): - raise TypeError('replica_number must be an int') - if (not replica_number >= -1): - raise ValueError('replica_number must be greater than or equal to 0 or flag value -1') - if (not isinstance(compute_checksums, int)): - raise TypeError('compute_checksums must be an int 1 or 0') - if ((not compute_checksums == 0) and (not compute_checksums == 1)): - raise ValueError('force must be an int 1 or 0') - if (not isinstance(admin, int)): - raise TypeError('admin must be an int 1 or 0') - if ((not admin == 0) and (not admin == 1)): - raise ValueError('admin must be an int 1 or 0') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded', - } - - data = { - 'op': 'calculate_checksum', - 'lpath': lpath, - 'compute-checksums': compute_checksums, - 'admin': admin - } - - if (resource != ''): - data['resource'] = resource - - if (replica_number != -1): - data['replica-number'] = replica_number - - - r = requests.post(self.url_base + '/data-objects', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - if rdict['irods_response']['status_code']: - print('Failed to verify checksum for data object \'' + lpath + '\': iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Checksum for data object \'' + lpath + '\' verified successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def stat(self, lpath: str, ticket: str=''): - """ - Gives information about a data object. - - Parameters - - lpath: The absolute logical path of the data object being accessed. - - ticket (optional): Ticket to be enabled before the operation. Defaults to an empty string. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(lpath, str)): - raise TypeError('lpath must be a string') - if (not isinstance(ticket, str)): - raise TypeError('ticket must be a string') - - headers = { - 'Authorization': 'Bearer ' + self.token, - } - - params = { - 'op': 'stat', - 'lpath': lpath, - 'ticket': ticket - } - - r = requests.get(self.url_base + '/data-objects', params=params, headers=headers) - - if (r.status_code / 100 == 2): - rdict = r.json() - if rdict['irods_response']['status_code']: - print('Failed to retrieve information for \'' + lpath + '\': iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Information for \'' + lpath + '\' retrieved successfully') - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def rename(self, old_lpath: str, new_lpath: str): - """ - Renames or moves a data object. - - Parameters - - old_lpath: The current absolute logical path of the data object. - - new_lpath: The absolute logical path of the destination for the data object. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(old_lpath, str)): - raise TypeError('old_lpath must be a string') - if (not isinstance(new_lpath, str)): - raise TypeError('new_lpath must be a string') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded', - } - - data = { - 'op': 'rename', - 'old-lpath': old_lpath, - 'new-lpath': new_lpath - } - - r = requests.post(self.url_base + '/data-objects', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - if rdict['irods_response']['status_code']: - print('Failed to rename \'' + old_lpath + '\': iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('\'' + old_lpath + '\' renamed to \'' + new_lpath + '\'') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def copy(self, src_lpath: str, dst_lpath: str, src_resource: str='', dst_resource: str='', overwrite: int=0): - """ - Copies a data object. - - Parameters - - src_lpath: The absolute logical path of the source data object. - - dst_lpath: The absolute logical path of the destination. - - src_resource: The absolute logical path of the source resource. - - dst_resource: The absolute logical path of the destination resource. - - overwrite: set to 1 to overwrite an existing objject, otherwise set to 0. Defaults to 0. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(src_lpath, str)): - raise TypeError('src_lpath must be a string') - if (not isinstance(dst_lpath, str)): - raise TypeError('dst_lpath must be a string') - if (not isinstance(src_resource, str)): - raise TypeError('src_resource must be a string') - if (not isinstance(dst_resource, str)): - raise TypeError('dst_lpath must be a string') - if (not isinstance(overwrite, int)): - raise TypeError('overwrite must be an int 1 or 0') - if ((not overwrite == 0) and (not overwrite == 1)): - raise ValueError('overwrite must be an int 1 or 0') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded', - } - - data = { - 'op': 'copy', - 'src-lpath': src_lpath, - 'dst-lpath': dst_lpath, - 'overwrite': overwrite - } - - if (src_resource != ''): - data['src-resource'] = src_resource - - if (dst_resource != ''): - data['dst-resource'] = dst_resource - - r = requests.post(self.url_base + '/data-objects', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - if rdict['irods_response']['status_code']: - print('Failed to copy \'' + src_lpath + '\': iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('\'' + src_lpath + '\' copied to \'' + dst_lpath + '\'') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def replicate(self, lpath: str, src_resource: str='', dst_resource: str='', admin: int=0): - """ - Replicates a data object from one resource to another. - - Parameters - - lpath: The absolute logical path of the data object to be replicated. - - src_resource: The absolute logical path of the source resource. - - dst_resource: The absolute logical path of the destination resource. - - admin (optional): Set to 1 to run this operation as an admin, otherwise set to 0. Defaults to 0. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(lpath, str)): - raise TypeError('lpath must be a string') - if (not isinstance(src_resource, str)): - raise TypeError('src_resource must be a string') - if (not isinstance(dst_resource, str)): - raise TypeError('dst_lpath must be a string') - if (not isinstance(admin, int)): - raise TypeError('admin must be an int 1 or 0') - if ((not admin == 0) and (not admin == 1)): - raise ValueError('admin must be an int 1 or 0') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded', - } - - data = { - 'op': 'replicate', - 'lpath': lpath, - 'admin': admin - } - - if (src_resource != ''): - data['src-resource'] = src_resource - - if (dst_resource != ''): - data['dst-resource'] = dst_resource - - print(data) - - r = requests.post(self.url_base + '/data-objects', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - if rdict['irods_response']['status_code']: - print('Failed to replicate \'' + lpath + '\': iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('\'' + lpath + '\' replicated from \'' + src_resource + '\' to \'' + dst_resource + '\'') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def trim(self, lpath: str, replica_number: int, catalog_only: int=0, admin: int=0): - """ - Trims an existing replica or removes its catalog entry. - - Parameters - - lpath: The absolute logical path of the data object to be trimmed. - - replica_number: The replica number of the target replica. - - catalog_only (optional): Set to 1 to remove only the catalog entry, otherwise set to 0. Defaults to 0. - - admin (optional): Set to 1 to run this operation as an admin, otherwise set to 0. Defaults to 0. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(lpath, str)): - raise T('lpath must be a string') - if (not isinstance(replica_number, int)): - raise T('replica_number must be an int') - if (not isinstance(catalog_only, int)): - raise TypeError('catalog_only must be an int 1 or 0') - if ((not catalog_only == 0) and (not catalog_only == 1)): - raise ValueError('catalog_only must be an int 1 or 0') - if (not isinstance(admin, int)): - raise TypeError('admin must be an int 1 or 0') - if ((not admin == 0) and (not admin == 1)): - raise ValueError('admin must be an int 1 or 0') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded', - } - - data = { - 'op': 'trim', - 'lpath': lpath, - 'replica-number': replica_number, - 'catalog-only': catalog_only, - 'admin': admin - } - - r = requests.post(self.url_base + '/data-objects', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - - if rdict['irods_response']['status_code']: - print('Failed to trim \'' + lpath + '\': iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Sucessfully trimmed \'' + lpath + '\'') - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def register(self, lpath: str, ppath: str, resource: str, as_additional_replica: int=0, data_size: int=-1, checksum: str=''): - """ - Registers a data object/replica into the catalog. - - Parameters - - lpath: The absolute logical path of the data object to be registered. - - ppath: The absolute physical path of the data object to be registered. - - resource: The resource that will own the replica. - - as_additional_replica (optional): Set to 1 to register as a replica of an existing object, otherwise set to 0. Defaults to 0. - - data_size (optional): The size of the replica in bytes. - - checksum (optional): The checksum to associate with the replica. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(lpath, str)): - raise TypeError('lpath must be a string') - if (not isinstance(ppath, str)): - raise TypeError('ppath must be a string') - if (not isinstance(resource, str)): - raise TypeError('resource must be a string') - if (not isinstance(as_additional_replica, int)): - raise TypeError('as_additional_replica must be an int 1 or 0') - if ((not as_additional_replica == 0) and (not as_additional_replica == 1)): - raise ValueError('as_additional_replica must be an int 1 or 0') - if (not isinstance(data_size, int)): - raise TypeError('data_size must be an int') - if (not data_size >= -1): - raise ValueError('data_size must be greater than or equal to 0 or flag value -1') - if (not isinstance(checksum, str)): - raise TypeError('checksum must be a string') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded', - } - - data = { - 'op': 'register', - 'lpath': lpath, - 'ppath': ppath, - 'resource': resource, - 'as_additional_replica': as_additional_replica - } - - if (data_size != -1): - data['data-size'] = data_size - - if (checksum != ''): - data['checksum'] = checksum - - r = requests.post(self.url_base + '/data-objects', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - - if rdict['irods_response']['status_code']: - print('Failed to register \'' + lpath + '\': iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Sucessfully registered \'' + lpath + '\'') - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def read(self, lpath: str, offset: int=0, count: int=-1, ticket: str=''): - """ - Reads bytes from a data object. - - Parameters - - lpath: The absolute logical path of the data object to be read from. - - offset (optional): The number of bytes to skip. Defaults to 0. - - count (optional): The number of bytes to read. - - ticket (optional): Ticket to be enabled before the operation. Defaults to an empty string. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(lpath, str)): - raise TypeError('lpath must be a string') - if (not isinstance(offset, int)): - raise TypeError('offset must be an int') - if (not isinstance(count, int)): - raise TypeError('count must be an int') - if (not count >= -1): - raise ValueError('count must be greater than or equal to 0 or flag value -1') - if (not isinstance(ticket, str)): - raise TypeError('ticket must be a string') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded', - } - - params = { - 'op': 'read', - 'lpath': lpath, - 'offset': offset - } - - if (count != -1): - params['count'] = count - - if (ticket != ''): - params['ticket'] = ticket - - r = requests.get(self.url_base + '/data-objects',params=params , headers=headers) - - if (r.status_code / 100 == 2): - print('Sucessfully read \'' + lpath + '\'') - return(r.text) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def write(self, bytes, lpath: str='', resource: str='', offset: int=0, truncate: int=1, append: int=0, parallel_write_handle: str='', stream_index: int=-1): - """ - Writes bytes to a data object. - - Parameters - - lpath: The absolute logical path of the data object to be written to. - - bytes: The bytes to be written. - - resource (optional): The root resource to write to. - - offset (optional): The number of bytes to skip. Defaults to 0. - - truncate (optional): Set to 1 to truncate the data object before writing, otherwise set to 0. Defaults to 1. - - append (optional): Set to 1 to append bytes to the data objectm otherwise set to 0. Defaults to 0. - - parallel_write_handle (optional): The handle to be used when writing in parallel. - - stream_index (optional): The stream to use when writing in parallel. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(lpath, str)): - raise TypeError('lpath must be a string') - if (not isinstance(resource, str)): - raise TypeError('resource must be a string') - if (not isinstance(offset, int)): - raise TypeError('offset must be an int') - if (not offset >= 0): - raise ValueError('offset must be greater than or equal to 0') - if (not isinstance(truncate, int)): - raise TypeError('truncate must be an int 1 or 0') - if ((not truncate == 0) and (not truncate == 1)): - raise ValueError('truncate must be an int 1 or 0') - if (not isinstance(append, int)): - raise TypeError('append must be an int 1 or 0') - if ((not append == 0) and (not append == 1)): - raise ValueError('append must be an int 1 or 0') - if (not isinstance(parallel_write_handle, str)): - raise TypeError('parallel_write_handle must be a string') - if (not isinstance(stream_index, int)): - raise TypeError('stream_index must be an int') - if (not stream_index >= -1): - raise ValueError('stream_index must be greater than or equal to 0 or flag value -1') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded', - } - - data = { - 'op': 'write', - 'offset': offset, - 'truncate': truncate, - 'append': append, - 'bytes': bytes - } - - if (parallel_write_handle != ''): - data['parallel-write-handle'] = parallel_write_handle - else: - data['lpath'] = lpath - - if (resource != ''): - data['resource'] = resource - - if (stream_index != -1): - data['stream-index'] = stream_index - - r = requests.post(self.url_base + '/data-objects', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - - if rdict['irods_response']['status_code']: - print('Failed to write to \'' + lpath + '\': iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Sucessfully wrote to \'' + lpath + '\'') - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def parallel_write_init(self, lpath: str, stream_count: int, truncate: int=1, append: int=0, ticket: str=''): - """ - Initializes server-side state for parallel writing. - - Parameters - - lpath: The absolute logical path of the data object to be initialized for parallel write. - - stream_count: THe number of streams to open. - - offset (optional): The number of bytes to skip. Defaults to 0. - - truncate (optional): Set to 1 to truncate the data object before writing, otherwise set to 0. Defaults to 1. - - append (optional): Set to 1 to append bytes to the data objectm otherwise set to 0. Defaults to 0. - - ticket (optional): Ticket to be enabled before the operation. Defaults to an empty string. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(lpath, str)): - raise TypeError('lpath must be a string') - if (not isinstance(stream_count, int)): - raise TypeError('stream_count must be an int') - if (not stream_count >= 0): - raise ValueError('stream_count must be greater than or equal to 0 or flag value -1') - if (not isinstance(truncate, int)): - raise TypeError('truncate must be an int 1 or 0') - if ((not truncate == 0) and (not truncate == 1)): - raise ValueError('truncate must be an int 1 or 0') - if (not isinstance(append, int)): - raise TypeError('append must be an int 1 or 0') - if ((not append == 0) and (not append == 1)): - raise ValueError('append must be an int 1 or 0') - if (not isinstance(ticket, str)): - raise TypeError('ticket must be a string') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded', - } - - data = { - 'op': 'parallel_write_init', - 'lpath': lpath, - 'stream-count': stream_count, - 'truncate': truncate, - 'append': append - } - - if (ticket != ''): - data['ticket'] = ticket - - r = requests.post(self.url_base + '/data-objects', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - - if rdict['irods_response']['status_code']: - print('Failed to open parallel write to \'' + lpath + '\': iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Sucessfully opened parallel write to \'' + lpath + '\'') - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def parallel_write_shutdown(self, parallel_write_handle: str): - """ - Shuts down the parallel write state in the server. - - Parameters - - parallel_write_handle: Handle obtained from parallel_write_init - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(parallel_write_handle, str)): - raise TypeError('parallel_write_handle must be a string') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded', - } - - data = { - 'op': 'parallel_write_shutdown', - 'parallel-write-handle': parallel_write_handle - } - - r = requests.post(self.url_base + '/data-objects', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - - if rdict['irods_response']['status_code']: - print('Failed to close parallel write: iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Sucessfully closed parallel write.') - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def modify_metadata(self, lpath: str, operations: list, admin: int=0): - """ - Modifies the metadata for a data object - - Parameters - - lpath: The absolute logical path of the data object to have its inheritance set. - - operations: Dictionary containing the operations to carry out. Should contain the operation, attribute, value, and optionally units. - - admin (optional): Set to 1 to run this operation as an admin, otherwise set to 0. Defaults to 0. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(lpath, str)): - raise TypeError('lpath must be a string') - if (not isinstance(operations, list)): - raise TypeError('operations must be a list of dictionaries') - if (not isinstance(operations[0], dict)): - raise TypeError('operations must be a list of dictionaries') - if (not isinstance(admin, int)): - raise TypeError('admin must be an int 1 or 0') - if ((not admin == 0) and (not admin == 1)): - raise ValueError('admin must be an int 1 or 0') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded', - } - - data = { - 'op': 'modify_metadata', - 'lpath': lpath, - 'operations': json.dumps(operations), - 'admin': admin - } - - r = requests.post(self.url_base + '/data-objects', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - - if rdict['irods_response']['status_code']: - print('Failed to modify metadata for \'' + lpath + '\': iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Metadata for \'' + lpath + '\' modified successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def set_permission(self, lpath: str, entity_name: str, permission: str, admin: int=0): - """ - Sets the permission of a user for a given data object - - Parameters - - lpath: The absolute logical path of the data object to have a permission set. - - entity_name: The name of the user or group having its permission set. - - permission: The permission level being set. Either 'null', 'read', 'write', or 'own'. - - admin (optional): Set to 1 to run this operation as an admin, otherwise set to 0. Defaults to 0. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(lpath, str)): - raise TypeError('lpath must be a string') - if (not isinstance(entity_name, str)): - raise TypeError('entity_name must be a string') - if (not isinstance(permission, str)): - raise TypeError('permission must be a string (\'null\', \'read\', \'write\', or \'own\')') - if (not isinstance(admin, int)): - raise TypeError('admin must be an int 1 or 0') - if ((not admin == 0) and (not admin == 1)): - raise ValueError('admin must be an int 1 or 0') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded', - } - - data = { - 'op': 'set_permission', - 'lpath': lpath, - 'entity-name': entity_name, - 'permission': permission, - 'admin': admin - } - - r = requests.post(self.url_base + '/data-objects', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - - if rdict['irods_response']['status_code']: - print('Failed to set permission for \'' + lpath + '\': iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Permission for \'' + lpath + '\' set successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def modify_permissions(self, lpath: str, operations: list, admin: int=0): - """ - Modifies permissions for multiple users or groups for a data object. - - Parameters - - lpath: The absolute logical path of the data object to have its permissions modified. - - operations: Dictionary containing the operations to carry out. Should contain names and permissions for all operations. - - admin (optional): Set to 1 to run this operation as an admin, otherwise set to 0. Defaults to 0. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(lpath, str)): - raise TypeError('lpath must be a string') - if (not isinstance(operations, list)): - raise TypeError('operations must be a list of dictionaries') - if (not isinstance(operations[0], dict)): - raise TypeError('operations must be a list of dictionaries') - if (not isinstance(admin, int)): - raise TypeError('admin must be an int 1 or 0') - if ((not admin == 0) and (not admin == 1)): - raise ValueError('admin must be an int 1 or 0') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded', - } - - data = { - 'op': 'modify_permissions', - 'lpath': lpath, - 'operations': json.dumps(operations), - 'admin': admin - } - - r = requests.post(self.url_base + '/data-objects', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - - if rdict['irods_response']['status_code']: - print('Failed to modify permissions for \'' + lpath + '\': iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Permissions for \'' + lpath + '\' modified successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def modify_replica(self, lpath: str, resource_hierarchy: str='', replica_number: int=-1, new_data_checksum: str='', - new_data_comments: str='', new_data_create_time: int=-1, new_data_expiry: int=-1, - new_data_mode: str='', new_data_modify_time: str='', new_data_path: str='', - new_data_replica_number: int=-1, new_data_replica_status: int=-1, new_data_resource_id: int=-1, - new_data_size: int=-1, new_data_status: str='', new_data_type_name: str='', new_data_version: int=-1): - """ - Modifies properties of a single replica. - - WARNING: - This operation requires rodsadmin level privileges and should only be used when there isn't a safer option. - Misuse can lead to catalog inconsistencies and unexpected behavior. - - Parameters - - lpath: The absolute logical path of the data object to have a replica modified. - - resource_hierarchy: The hierarchy containing the resource to be modified. Mutually exclusive with replica_number. - - replica_number: The number of the replica to be modified. mutually exclusive with resource_hierarchy. - - Note: - At least one of the following optional parameters must be passed in - - - new_data_checksum (optional): The new checksum to be set. - - new_data_comments (optional): The new comments to be set. - - new_data_create_time (optional): The new create time to be set. - - new_data_expiry (optional): The new expiry to be set. - - new_data_mode (optional): The new mode to be set. - - new_data_modify_time (optional): The new modify time to be set. - - new_data_path (optional): The new path to be set. - - new_data_replica_number (optional): The new replica number to be set. - - new_data_replica_status (optional): The new replica status to be set. - - new_data_resource_id (optional): The new resource id to be set - - new_data_size (optional): The new size to be set. - - new_data_status (optional): The new data status to be set. - - new_data_type_name (optional): The new type name to be set. - - new_data_version (optional): The new version to be set. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(lpath, str)): - raise TypeError('lpath must be a string') - if (not isinstance(resource_hierarchy, str)): - raise TypeError('resource_hierarchy must be a string') - if (not isinstance(replica_number, int)): - raise TypeError('replica_number must be an int') - if ((resource_hierarchy != '') and (replica_number != -1)): - raise ValueError('replica_hierarchy and replica_number are mutually exclusive') - if (not isinstance(new_data_checksum, str)): - raise TypeError('new_data_checksum must be a string') - if (not isinstance(new_data_comments, str)): - raise TypeError('new_data_comments must be a string') - if (not isinstance(new_data_create_time, int)): - raise TypeError('new_data_create_time must be an int') - if (not new_data_create_time >= -1): - raise ValueError('new_data_create_time must be greater than or equal to 0 or flag value -1') - if (not isinstance(new_data_expiry, int)): - raise TypeError('new_data_expiry must be an int') - if (not new_data_expiry >= -1): - raise ValueError('new_data_expiry must be greater than or equal to 0 or flag value -1') - if (not isinstance(new_data_mode, str)): - raise TypeError('new_data_mode must be a string') - if (not isinstance(new_data_modify_time, str)): - raise TypeError('new_data_modify_time must be a string') - if (not isinstance(new_data_path, str)): - raise TypeError('new_data_path must be a string') - if (not isinstance(new_data_replica_number, int)): - raise TypeError('new_data_replica_number must be an int') - if (not new_data_replica_number >= -1): - raise ValueError('new_data_replica_number must be greater than or equal to 0 or flag value -1') - if (not isinstance(new_data_replica_status, int)): - raise TypeError('new_data_replica_status must be an int') - if (not new_data_replica_status >= -1): - raise ValueError('new_data_replica_status must be greater than or equal to 0 or flag value -1') - if (not isinstance(new_data_resource_id, int)): - raise TypeError('new_data_resource_id must be an int') - if (not new_data_resource_id >= -1): - raise ValueError('new_data_resource_id must be greater than or equal to 0 or flag value -1') - if (not isinstance(new_data_size, int)): - raise TypeError('new_data_size must be an int') - if (not new_data_size >= -1): - raise ValueError('new_data_size must be greater than or equal to 0 or flag value -1') - if (not isinstance(new_data_status, str)): - raise TypeError('new_data_status must be a string') - if (not isinstance(new_data_type_name, str)): - raise TypeError('new_data_type_name must be a string') - if (not isinstance(new_data_version, int)): - raise TypeError('new_data_version must be an int') - if (not new_data_version >= -1): - raise ValueError('new_data_version must be greater than or equal to 0 or flag value -1') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded', - } - - data = { - 'op': 'modify_permissions', - 'lpath': lpath - } - - if (resource_hierarchy != ''): - data['resource-hierarchy'] = resource_hierarchy - - if (replica_number != -1): - data['replica-numbeer'] = replica_number - - # Boolean for checking if the user passed in any new_data parameters - no_params = True - - if (new_data_checksum != ''): - data['new-data-checksum'] = new_data_checksum - no_params = False - - if (new_data_comments != ''): - data['new-data-comments'] = new_data_comments - no_params = False - - if (new_data_create_time != -1): - data['new-data-create-time'] = new_data_create_time - no_params = False - - if (new_data_expiry != -1): - data['new-data-expiry'] = new_data_expiry - no_params = False - - if (new_data_mode != ''): - data['new-data-mode'] = new_data_mode - no_params = False - - if (new_data_modify_time != ''): - data['new-data-modify-time'] = new_data_modify_time - no_params = False - - if (new_data_path != ''): - data['new-data-path'] = new_data_path - no_params = False - - if (new_data_replica_number != -1): - data['new-data-replica-number'] = new_data_replica_number - no_params = False - - if (new_data_replica_status != -1): - data['new-data-replica-status'] = new_data_replica_status - no_params = False - - if (new_data_resource_id != -1): - data['new-data-resource-id'] = new_data_resource_id - no_params = False - - if (new_data_size != -1): - data['new-data-size'] = new_data_size - no_params = False - - if (new_data_status != ''): - data['new-data-status'] = new_data_status - no_params = False - - if (new_data_type_name != ''): - data['new-data-type-name'] = new_data_type_name - no_params = False - - if (new_data_version != ''): - data['new-data-version'] = new_data_version - no_params = False - - if (no_params): - raise RuntimeError('At least one new data parameter must be given.') - - r = requests.post(self.url_base + '/data-objects', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - - if rdict['irods_response']['status_code']: - print('Failed to modify \'' + lpath + '\': iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('\'' + lpath + '\' modified successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) \ No newline at end of file + """Perform data object operations via iRODS HTTP API.""" + + def __init__(self, url_base: str): + """ + Initialize DataObjects with a base url. + + Token is set to None initially, and updated when setToken() is called in irodsClient. + """ + self.url_base = url_base + self.token = None + + def touch( + self, + lpath, + no_create: int = 0, + replica_number: int = -1, + leaf_resources: str = "", + seconds_since_epoch=-1, + reference="", + ): + """ + Update mtime for an existing data object or create a new one. + + Args: + lpath: The absolute logical path of the data object being touched. + no_create: Set to 1 to prevent creating a new object, otherwise set to 0. + replica_number: The replica number of the target replica. + leaf_resources: The resource holding an existing replica. If one does not exist, creates one. + seconds_since_epoch: The value to set mtime to, defaults to -1 as a flag. + reference: The absolute logical path of the data object to use as a reference for mtime. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(lpath, str) + common.validate_0_or_1(no_create) + common.validate_gte_minus1(replica_number) + common.validate_instance(leaf_resources, str) + common.validate_gte_minus1(seconds_since_epoch) + common.validate_instance(reference, str) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = {"op": "touch", "lpath": lpath, "no-create": no_create} + + if seconds_since_epoch != -1: + data["seconds-since-epoch"] = seconds_since_epoch + + if replica_number != -1: + data["replica-number"] = replica_number + + if leaf_resources != "": + data["leaf-resources"] = leaf_resources + + if reference != "": + data["reference"] = reference + + r = requests.post(self.url_base + "/data-objects", headers=headers, data=data, timeout=30) + return common.process_response(r) + + def remove(self, lpath: str, catalog_only: int = 0, no_trash: int = 0, admin: int = 0): + """ + Remove an existing data object. + + Args: + lpath: The absolute logical path of the data object to be removed. + catalog_only: Set to 1 to remove only the catalog entry, otherwise set to 0. Defaults to 0. + no_trash: Set to 1 to move the data object to trash, 0 to permanently remove. Defaults to 0. + admin: Set to 1 to run this operation as an admin, otherwise set to 0. Defaults to 0. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(lpath, str) + common.validate_0_or_1(catalog_only) + common.validate_0_or_1(no_trash) + common.validate_0_or_1(admin) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = { + "op": "remove", + "lpath": lpath, + "catalog-only": catalog_only, + "no-trash": no_trash, + "admin": admin, + } + + r = requests.post(self.url_base + "/data-objects", headers=headers, data=data, timeout=30) + return common.process_response(r) + + def calculate_checksum( + self, + lpath: str, + resource: str = "", + replica_number: int = -1, + force: int = 0, + all_: int = 0, + admin: int = 0, + ): + """ + Calculate the checksum for a data object. + + Args: + lpath: The absolute logical path of the data object to have its checksum calculated. + resource: The resource holding the existing replica. + replica_number: The replica number of the target replica. + force: Set to 1 to replace the existing checksum, otherwise set to 0. Defaults to 0. + all_: Set to 1 to calculate the checksum for all replicas, otherwise set to 0. Defaults to 0. + admin: Set to 1 to run this operation as an admin, otherwise set to 0. Defaults to 0. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(lpath, str) + common.validate_instance(resource, str) + common.validate_gte_minus1(replica_number) + common.validate_0_or_1(force) + common.validate_0_or_1(all_) + common.validate_0_or_1(admin) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = { + "op": "calculate_checksum", + "lpath": lpath, + "force": force, + "all": all_, + "admin": admin, + } + + if resource != "": + data["resource"] = resource + + if replica_number != -1: + data["replica-number"] = replica_number + + r = requests.post(self.url_base + "/data-objects", headers=headers, data=data, timeout=30) + return common.process_response(r) + + def verify_checksum( + self, + lpath: str, + resource: str = "", + replica_number: int = -1, + compute_checksums: int = 0, + admin: int = 0, + ): + """ + Verify the checksum for a data object. + + Args: + lpath: The absolute logical path of the data object to have its checksum verified. + resource: The resource holding the existing replica. + replica_number: The replica number of the target replica. + compute_checksums: Set to 1 to skip checksum calculation, otherwise set to 0. Defaults to 0. + admin: Set to 1 to run this operation as an admin, otherwise set to 0. Defaults to 0. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(lpath, str) + common.validate_instance(resource, str) + common.validate_gte_minus1(replica_number) + common.validate_0_or_1(compute_checksums) + common.validate_0_or_1(admin) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = { + "op": "calculate_checksum", + "lpath": lpath, + "compute-checksums": compute_checksums, + "admin": admin, + } + + if resource != "": + data["resource"] = resource + + if replica_number != -1: + data["replica-number"] = replica_number + + r = requests.post(self.url_base + "/data-objects", headers=headers, data=data, timeout=30) + return common.process_response(r) + + def stat(self, lpath: str, ticket: str = ""): + """ + Give information about a data object. + + Args: + lpath: The absolute logical path of the data object being accessed. + ticket: Ticket to be enabled before the operation. Defaults to an empty string. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(lpath, str) + common.validate_instance(ticket, str) + + headers = { + "Authorization": "Bearer " + self.token, + } + + params = {"op": "stat", "lpath": lpath, "ticket": ticket} + + r = requests.get(self.url_base + "/data-objects", params=params, headers=headers, timeout=30) + return common.process_response(r) + + def rename(self, old_lpath: str, new_lpath: str): + """ + Rename or move a data object. + + Args: + old_lpath: The current absolute logical path of the data object. + new_lpath: The absolute logical path of the destination for the data object. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(old_lpath, str) + common.validate_instance(new_lpath, str) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = {"op": "rename", "old-lpath": old_lpath, "new-lpath": new_lpath} + + r = requests.post(self.url_base + "/data-objects", headers=headers, data=data, timeout=30) + return common.process_response(r) + + def copy( + self, + src_lpath: str, + dst_lpath: str, + src_resource: str = "", + dst_resource: str = "", + overwrite: int = 0, + ): + """ + Copy a data object. + + Args: + src_lpath: The absolute logical path of the source data object. + dst_lpath: The absolute logical path of the destination. + src_resource: The name of the source resource. + dst_resource: The name of the destination resource. + overwrite: Set to 1 to overwrite an existing objject, otherwise set to 0. Defaults to 0. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(src_lpath, str) + common.validate_instance(dst_lpath, str) + common.validate_instance(src_resource, str) + common.validate_instance(dst_resource, str) + common.validate_0_or_1(overwrite) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = { + "op": "copy", + "src-lpath": src_lpath, + "dst-lpath": dst_lpath, + "overwrite": overwrite, + } + + if src_resource != "": + data["src-resource"] = src_resource + + if dst_resource != "": + data["dst-resource"] = dst_resource + + r = requests.post(self.url_base + "/data-objects", headers=headers, data=data, timeout=30) + return common.process_response(r) + + def replicate(self, lpath: str, src_resource: str = "", dst_resource: str = "", admin: int = 0): + """ + Replicates a data object from one resource to another. + + Args: + lpath: The absolute logical path of the data object to be replicated. + src_resource: The name of the source resource. + dst_resource: The name of the destination resource. + admin: Set to 1 to run this operation as an admin, otherwise set to 0. Defaults to 0. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(lpath, str) + common.validate_instance(src_resource, str) + common.validate_instance(dst_resource, str) + common.validate_0_or_1(admin) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = {"op": "replicate", "lpath": lpath, "admin": admin} + + if src_resource != "": + data["src-resource"] = src_resource + + if dst_resource != "": + data["dst-resource"] = dst_resource + + r = requests.post(self.url_base + "/data-objects", headers=headers, data=data, timeout=30) + return common.process_response(r) + + def trim(self, lpath: str, replica_number: int, catalog_only: int = 0, admin: int = 0): + """ + Trims an existing replica or removes its catalog entry. + + Args: + lpath: The absolute logical path of the data object to be trimmed. + replica_number: The replica number of the target replica. + catalog_only: Set to 1 to remove only the catalog entry, otherwise set to 0. Defaults to 0. + admin: Set to 1 to run this operation as an admin, otherwise set to 0. Defaults to 0. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(lpath, str) + common.validate_instance(replica_number, int) + common.validate_0_or_1(catalog_only) + common.validate_0_or_1(admin) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = { + "op": "trim", + "lpath": lpath, + "replica-number": replica_number, + "catalog-only": catalog_only, + "admin": admin, + } + + r = requests.post(self.url_base + "/data-objects", headers=headers, data=data, timeout=30) + return common.process_response(r) + + def register( + self, + lpath: str, + ppath: str, + resource: str, + as_additional_replica: int = 0, + data_size: int = -1, + checksum: str = "", + ): + """ + Register a data object/replica into the catalog. + + Args: + lpath: The absolute logical path of the data object to be registered. + ppath: The absolute physical path of the data object to be registered. + resource: The resource that will own the replica. + as_additional_replica: Set to 1 to register as a replica of an existing + object, otherwise set to 0. Defaults to 0. + data_size: The size of the replica in bytes. + checksum: The checksum to associate with the replica. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(lpath, str) + common.validate_instance(ppath, str) + common.validate_instance(resource, str) + common.validate_0_or_1(as_additional_replica) + common.validate_gte_minus1(data_size) + common.validate_instance(checksum, str) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = { + "op": "register", + "lpath": lpath, + "ppath": ppath, + "resource": resource, + "as_additional_replica": as_additional_replica, + } + + if data_size != -1: + data["data-size"] = data_size + + if checksum != "": + data["checksum"] = checksum + + r = requests.post(self.url_base + "/data-objects", headers=headers, data=data, timeout=30) + return common.process_response(r) + + def read(self, lpath: str, offset: int = 0, count: int = -1, ticket: str = ""): + """ + Read bytes from a data object. + + Args: + lpath: The absolute logical path of the data object to be read from. + offset: The number of bytes to skip. Defaults to 0. + count: The number of bytes to read. + ticket: Ticket to be enabled before the operation. Defaults to an empty string. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(lpath, str) + common.validate_instance(offset, int) + common.validate_gte_minus1(count) + common.validate_instance(ticket, str) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + params = {"op": "read", "lpath": lpath, "offset": offset} + + if count != -1: + params["count"] = count + + if ticket != "": + params["ticket"] = ticket + + r = requests.get(self.url_base + "/data-objects", params=params, headers=headers, timeout=30) + # TODO(#45): confirm this is the format we want to return + # - this is the only payload that is different from common.process_response() + return {'status_code': r.status_code, 'data': {'irods_response': {'status_code': 0, 'bytes': r.content}}} + + def write( + self, + bytes_, + lpath: str = "", + resource: str = "", + offset: int = 0, + truncate: int = 1, + append: int = 0, + parallel_write_handle: str = "", + stream_index: int = -1, + ): + """ + Write bytes to a data object. + + Args: + bytes_: The bytes to be written. + lpath: The absolute logical path of the data object to be written to. + resource: The root resource to write to. + offset: The number of bytes to skip. Defaults to 0. + truncate: Set to 1 to truncate the data object before writing, otherwise set to 0. Defaults to 1. + append: Set to 1 to append bytes to the data objectm otherwise set to 0. Defaults to 0. + parallel_write_handle: The handle to be used when writing in parallel. + stream_index: The stream to use when writing in parallel. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + + Raises: + ValueError: If bytes length is less than 0. + """ + common.check_token(self.token) + # also need to validate that bytes_ is a proper type + if not len(bytes_) >= 0: + raise ValueError("bytes must be greater than or equal to 0") + common.validate_instance(lpath, str) + common.validate_instance(resource, str) + common.validate_gte_zero(offset) + common.validate_0_or_1(truncate) + common.validate_0_or_1(append) + common.validate_instance(parallel_write_handle, str) + common.validate_gte_minus1(stream_index) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = { + "op": "write", + "offset": offset, + "truncate": truncate, + "append": append, + "bytes": bytes_, + } + + if parallel_write_handle != "": + data["parallel-write-handle"] = parallel_write_handle + else: + data["lpath"] = lpath + + if resource != "": + data["resource"] = resource + + if stream_index != -1: + data["stream-index"] = stream_index + + r = requests.post(self.url_base + "/data-objects", headers=headers, data=data, timeout=30) + return common.process_response(r) + + def parallel_write_init( + self, + lpath: str, + stream_count: int, + truncate: int = 1, + append: int = 0, + ticket: str = "", + ): + """ + Initialize server-side state for parallel writing. + + Args: + lpath: The absolute logical path of the data object to be initialized for parallel write. + stream_count: The number of streams to open. + truncate: Set to 1 to truncate the data object before writing, otherwise set to 0. Defaults to 1. + append: Set to 1 to append bytes to the data objectm otherwise set to 0. Defaults to 0. + ticket: Ticket to be enabled before the operation. Defaults to an empty string. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(lpath, str) + common.validate_gte_zero(stream_count) + common.validate_0_or_1(truncate) + common.validate_0_or_1(append) + common.validate_instance(ticket, str) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = { + "op": "parallel_write_init", + "lpath": lpath, + "stream-count": stream_count, + "truncate": truncate, + "append": append, + } + + if ticket != "": + data["ticket"] = ticket + + r = requests.post(self.url_base + "/data-objects", headers=headers, data=data, timeout=30) + return common.process_response(r) + + def parallel_write_shutdown(self, parallel_write_handle: str): + """ + Shuts down the parallel write state in the server. + + Args: + parallel_write_handle: Handle obtained from parallel_write_init. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(parallel_write_handle, str) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = { + "op": "parallel_write_shutdown", + "parallel-write-handle": parallel_write_handle, + } + + r = requests.post(self.url_base + "/data-objects", headers=headers, data=data, timeout=30) + return common.process_response(r) + + def modify_metadata(self, lpath: str, operations: list, admin: int = 0): + """ + Modify the metadata for a data object. + + Args: + lpath: The absolute logical path of the data object to have its inheritance set. + operations: Dictionary containing the operations to carry out. Should contain the + operation, attribute, value, and optionally units. + admin: Set to 1 to run this operation as an admin, otherwise set to 0. Defaults to 0. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(lpath, str) + common.validate_instance(operations, list) + common.validate_instance(operations[0], dict) + common.validate_0_or_1(admin) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = { + "op": "modify_metadata", + "lpath": lpath, + "operations": json.dumps(operations), + "admin": admin, + } + + r = requests.post(self.url_base + "/data-objects", headers=headers, data=data, timeout=30) + return common.process_response(r) + + def set_permission(self, lpath: str, entity_name: str, permission: str, admin: int = 0): + """ + Set the permission of a user for a given data object. + + Args: + lpath: The absolute logical path of the data object to have a permission set. + entity_name: The name of the user or group having its permission set. + permission: The permission level being set. Either 'null', 'read', 'write', or 'own'. + admin: Set to 1 to run this operation as an admin, otherwise set to 0. Defaults to 0. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + + Raises: + ValueError: If permission is not 'null', 'read', 'write', or 'own'. + """ + common.check_token(self.token) + common.validate_instance(lpath, str) + common.validate_instance(entity_name, str) + common.validate_instance(permission, str) + if permission not in ["null", "read", "write", "own"]: + raise ValueError("permission must be either 'null', 'read', 'write', or 'own'") + common.validate_0_or_1(admin) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = { + "op": "set_permission", + "lpath": lpath, + "entity-name": entity_name, + "permission": permission, + "admin": admin, + } + + r = requests.post(self.url_base + "/data-objects", headers=headers, data=data, timeout=30) + return common.process_response(r) + + def modify_permissions(self, lpath: str, operations: list, admin: int = 0): + """ + Modify permissions for multiple users or groups for a data object. + + Args: + lpath: The absolute logical path of the data object to have its permissions modified. + operations: Dictionary containing the operations to carry out. Should contain names + and permissions for all operations. + admin: Set to 1 to run this operation as an admin, otherwise set to 0. Defaults to 0. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(lpath, str) + common.validate_instance(operations, list) + common.validate_instance(operations[0], dict) + common.validate_0_or_1(admin) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = { + "op": "modify_permissions", + "lpath": lpath, + "operations": json.dumps(operations), + "admin": admin, + } + + r = requests.post(self.url_base + "/data-objects", headers=headers, data=data, timeout=30) + return common.process_response(r) + + def modify_replica( + self, + lpath: str, + resource_hierarchy: str = "", + replica_number: int = -1, + new_data_checksum: str = "", + new_data_comments: str = "", + new_data_create_time: int = -1, + new_data_expiry: int = -1, + new_data_mode: str = "", + new_data_modify_time: str = "", + new_data_path: str = "", + new_data_replica_number: int = -1, + new_data_replica_status: int = -1, + new_data_resource_id: int = -1, + new_data_size: int = -1, + new_data_status: str = "", + new_data_type_name: str = "", + new_data_version: int = -1, + ): + """ + Modify properties of a single replica. + + Warning: + This operation requires rodsadmin level privileges and should only be used when there isn't a safer option. + Misuse can lead to catalog inconsistencies and unexpected behavior. + + Args: + lpath: The absolute logical path of the data object to have a replica modified. + resource_hierarchy: The hierarchy containing the resource to be modified. + Mutually exclusive with replica_number. + replica_number: The number of the replica to be modified. mutually exclusive with + resource_hierarchy. + new_data_checksum: The new checksum to be set. + new_data_comments: The new comments to be set. + new_data_create_time: The new create time to be set. + new_data_expiry: The new expiry to be set. + new_data_mode: The new mode to be set. + new_data_modify_time: The new modify time to be set. + new_data_path: The new path to be set. + new_data_replica_number: The new replica number to be set. + new_data_replica_status: The new replica status to be set. + new_data_resource_id: The new resource id to be set. + new_data_size: The new size to be set. + new_data_status: The new data status to be set. + new_data_type_name: The new type name to be set. + new_data_version: The new version to be set. + + Note: + At least one of the new_data parameters must be passed in. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + + Raises: + ValueError: If both resource_hierarchy and replica_number are provided. + RuntimeError: If no new_data parameters are provided. + """ + common.check_token(self.token) + common.validate_instance(lpath, str) + common.validate_instance(resource_hierarchy, str) + common.validate_instance(replica_number, int) + if (resource_hierarchy != "") and (replica_number != -1): + raise ValueError("replica_hierarchy and replica_number are mutually exclusive") + common.validate_instance(new_data_checksum, str) + common.validate_instance(new_data_comments, str) + common.validate_gte_minus1(new_data_create_time) + common.validate_gte_minus1(new_data_expiry) + common.validate_instance(new_data_mode, str) + common.validate_instance(new_data_modify_time, str) + common.validate_instance(new_data_path, str) + common.validate_gte_minus1(new_data_replica_number) + common.validate_gte_minus1(new_data_replica_status) + common.validate_gte_minus1(new_data_resource_id) + common.validate_gte_minus1(new_data_size) + common.validate_instance(new_data_status, str) + common.validate_instance(new_data_type_name, str) + common.validate_gte_minus1(new_data_version) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = {"op": "modify_replica", "lpath": lpath} + + if resource_hierarchy != "": + data["resource-hierarchy"] = resource_hierarchy + + if replica_number != -1: + data["replica-number"] = replica_number + + # Boolean for checking if the user passed in any new_data parameters + no_params = True + + if new_data_checksum != "": + data["new-data-checksum"] = new_data_checksum + no_params = False + + if new_data_comments != "": + data["new-data-comments"] = new_data_comments + no_params = False + + if new_data_create_time != -1: + data["new-data-create-time"] = new_data_create_time + no_params = False + + if new_data_expiry != -1: + data["new-data-expiry"] = new_data_expiry + no_params = False + + if new_data_mode != "": + data["new-data-mode"] = new_data_mode + no_params = False + + if new_data_modify_time != "": + data["new-data-modify-time"] = new_data_modify_time + no_params = False + + if new_data_path != "": + data["new-data-path"] = new_data_path + no_params = False + + if new_data_replica_number != -1: + data["new-data-replica-number"] = new_data_replica_number + no_params = False + + if new_data_replica_status != -1: + data["new-data-replica-status"] = new_data_replica_status + no_params = False + + if new_data_resource_id != -1: + data["new-data-resource-id"] = new_data_resource_id + no_params = False + + if new_data_size != -1: + data["new-data-size"] = new_data_size + no_params = False + + if new_data_status != "": + data["new-data-status"] = new_data_status + no_params = False + + if new_data_type_name != "": + data["new-data-type-name"] = new_data_type_name + no_params = False + + if new_data_version != "": + data["new-data-version"] = new_data_version + no_params = False + + if no_params: + raise RuntimeError("At least one new data parameter must be given.") + + r = requests.post(self.url_base + "/data-objects", headers=headers, data=data, timeout=30) + return common.process_response(r) diff --git a/irods_http_client/irodsHttpClient.py b/irods_http_client/irodsHttpClient.py deleted file mode 100644 index 35e841a..0000000 --- a/irods_http_client/irodsHttpClient.py +++ /dev/null @@ -1,123 +0,0 @@ -from irods_http_client.collection_operations import Collections -from irods_http_client.data_object_operations import DataObjects -from irods_http_client.query_operations import Queries -from irods_http_client.resource_operations import Resources -from irods_http_client.rule_operations import Rules -from irods_http_client.ticket_operations import Tickets -from irods_http_client.user_group_operations import UsersGroups -from irods_http_client.zone_operations import Zones -import requests - -class IrodsHttpClient: - def __init__(self, url_base: str): - """ Gets the base url from the user to initialize a client instance. """ - self.url_base = url_base - self.token = None - - self.collections = Collections(url_base) - self.data_objects = DataObjects(url_base) - self.queries = Queries(url_base) - self.resources = Resources(url_base) - self.rules = Rules(url_base) - self.tickets = Tickets(url_base) - self.users_groups = UsersGroups(url_base) - self.zones = Zones(url_base) - - - def authenticate(self, username: str='', password: str='', openid_token: str=''): - """ - Takes user credentials as parameters and attempts to authenticate and retrieve a token. - - Parameters - - username: The username of the user to be authenticated. - - password: The password of the user to be authenticated. - - Returns - - User token generated by the server. - """ - if (not isinstance(username, str)): - raise TypeError('username must be a string') - if (not isinstance(password, str)): - raise TypeError('password must be a string') - if (not isinstance(openid_token, str)): - raise TypeError('openid_token must be a string') - - if (openid_token != ''): #TODO: Add openid authentication - return('logged in with openid') - - r = requests.post(self.url_base + '/authenticate', auth=(username, password)) - - if (r.status_code / 100 == 2): - if (self.token == None): - self.setToken(r.text) - return(r.text) - else: - raise RuntimeError('Failed to authenticate: ' + str(r.status_code)) - - - def setToken(self, token: str): - """ - Sets the token to be used when making requests. - - Parameters - - token: The tokent to be set. - """ - if (not isinstance(token, str)): - raise TypeError('token must be a string') - self.token = token - - self.collections.token = token - self.data_objects.token = token - self.queries.token = token - self.resources.token = token - self.rules.token = token - self.tickets.token = token - self.users_groups.token = token - self.zones.token = token - - - def getToken(self): - """ Returns the authentication token currently in use """ - return(self.token) - - - def info(self): - """ - Gives general information about the iRODS server. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - - headers = { - 'Authorization': 'Bearer ' + self.token, - } - - r = requests.get(self.url_base + '/info', headers=headers) - - if (r.status_code / 100 == 2): - rdict = r.json() - - print('Server information for retrieved successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) \ No newline at end of file diff --git a/irods_http_client/irods_http_client.py b/irods_http_client/irods_http_client.py new file mode 100644 index 0000000..10bda02 --- /dev/null +++ b/irods_http_client/irods_http_client.py @@ -0,0 +1,106 @@ +"""Main client module for iRODS HTTP API interactions.""" + +import requests + +from irods_http_client import common +from irods_http_client.collection_operations import Collections +from irods_http_client.data_object_operations import DataObjects +from irods_http_client.query_operations import Queries +from irods_http_client.resource_operations import Resources +from irods_http_client.rule_operations import Rules +from irods_http_client.ticket_operations import Tickets +from irods_http_client.user_group_operations import UsersGroups +from irods_http_client.zone_operations import Zones + + +class IRODSHTTPClient: + """HTTP client for interacting with iRODS via REST API.""" + + def __init__(self, url_base: str): + """Get the base url from the user to initialize a client instance.""" + self.url_base = url_base + self.token = None + + self.collections = Collections(url_base) + self.data_objects = DataObjects(url_base) + self.queries = Queries(url_base) + self.resources = Resources(url_base) + self.rules = Rules(url_base) + self.tickets = Tickets(url_base) + self.users_groups = UsersGroups(url_base) + self.zones = Zones(url_base) + + def authenticate(self, username: str = "", password: str = ""): + """ + Take user credentials as parameters and attempts to authenticate and retrieve a token. + + Args: + username: The username of the user to be authenticated. + password: The password of the user to be authenticated. + + Returns: + User token generated by the server. + + Raises: + TypeError: If any parameter is not a string. + RuntimeError: If authentication fails. + """ + if not isinstance(username, str): + raise TypeError("username must be a string") + if not isinstance(password, str): + raise TypeError("password must be a string") + + r = requests.post(self.url_base + "/authenticate", auth=(username, password), timeout=30) + + if r.status_code / 100 == 2: # noqa: PLR2004 + if self.token is None: + self.set_token(r.text) + return r.text + raise RuntimeError("Failed to authenticate: " + str(r.status_code)) + + def set_token(self, token: str): + """ + Set the token to be used when making requests. + + Args: + token: The token to be set. + + Raises: + TypeError: If token is not a string. + """ + if not isinstance(token, str): + raise TypeError("token must be a string") + self.token = token + + self.collections.token = token + self.data_objects.token = token + self.queries.token = token + self.resources.token = token + self.rules.token = token + self.tickets.token = token + self.users_groups.token = token + self.zones.token = token + + def get_token(self): + """ + Return the authentication token currently in use. + + Returns: + The authentication token currently in use. + """ + return self.token + + def info(self): + """ + Give general information about the iRODS server. + + Returns + - A dict containing the HTTP status code and iRODS response. + - The iRODS response is only valid if no error occurred during HTTP communication. + """ + headers = { + "Authorization": "Bearer " + self.token, + } + + r = requests.get(self.url_base + "/info", headers=headers, timeout=30) + return common.process_response(r) diff --git a/irods_http_client/query_operations.py b/irods_http_client/query_operations.py index a14f127..058f37d 100644 --- a/irods_http_client/query_operations.py +++ b/irods_http_client/query_operations.py @@ -1,316 +1,187 @@ +"""Query operations for iRODS HTTP API.""" + import requests -import json -class Queries: +from . import common - def __init__(self, url_base: str): - """" - Initializes Queries with a base url. - Token is set to None initially, and updated when setToken() is called in irodsClient. - """ - self.url_base = url_base - self.token = None - - - def execute_genquery(self, query: str, offset: int=0, count: int=-1, case_sensitive: int=1, distinct: int=1, - parser: str='genquery1', sql_only: int=0, zone: str=''): - """ - Excecutes a GenQuery string and returns the results. - - Parameters - - query: The query being executed - - offset (optional): Number of rows to skip. Defaults to 0. - - count (optional): Number of rows to return. Default set by administrator. - - case_sensitive (optional): Set to 1 to execute a case sensitive query, otherwise set to 0. Defaults to 1. Only supported by GenQuery1. - - distinct (optional): Set to 1 to collapse duplicate rows, otherwise set to 0. Defaults to 1. Only supported by GenQuery 1 - - parser (optional): User either genquery1 or genquery2. Defaults to genquery1. - - sql_only (optional): Set to 1 to execute an SQL only query, otherwise set to 0. Defaults to 0. Only supported by GenQuery2. - - zone (optional): The zone name. Defaults ot the local zone. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(query, str)): - raise TypeError('query must be a string') - if ((not isinstance(offset, int))): - raise TypeError('offset must be an int') - if (not offset >= 0): - raise ValueError('offset must be greater than or equal to 0') - if ((not isinstance(count, int))): - raise TypeError('count must be an int') - if (not count >= -1): - raise ValueError('count must be greater than or equal to 0 or flag value -1') - if (not isinstance(case_sensitive, int)): - raise TypeError('case_sensitive must be an int 1 or 0') - if ((not case_sensitive == 0) and (not case_sensitive == 1)): - raise ValueError('case_sensitive must be an int 1 or 0') - if (not isinstance(distinct, int)): - raise TypeError('distinct must be an int 1 or 0') - if ((not distinct == 0) and (not distinct == 1)): - raise ValueError('distinct must be an int 1 or 0') - if (not isinstance(parser, str)): - raise TypeError('parser must be a string') - if ((not parser == 'genquery1') and (not parser == 'genquery2')): - raise ValueError('parser must be either \'genquery1\' or \'genquery2\'') - if (not isinstance(sql_only, int)): - raise TypeError('sql_only must be an int 1 or 0') - if ((not sql_only == 0) and (not sql_only == 1)): - raise ValueError('sql_only must be an int 1 or 0') - if (not isinstance(zone, str)): - raise TypeError('zone must be a string') - - headers = { - 'Authorization': 'Bearer ' + self.token, - } - - params = { - 'op': 'execute_genquery', - 'query': query, - 'offset': offset, - 'parser': parser - } - - if (count != -1): - params['count'] = count - - if (zone != ''): - params['zone'] = zone - - if (parser == 'genquery1'): - params['case-sensitive'] = case_sensitive - params['distinct'] = distinct - else: - params['sql-only'] = sql_only - - r = requests.get(self.url_base + '/query', headers=headers, params=params) - - if (r.status_code / 100 == 2): - rdict = r.json() - - if rdict['irods_response']['status_code']: - print('Failed to execute query: iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Query executed successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def execute_specific_query(self, name: str, args: str='', args_delimiter: str=',', offset: int=0, count: int=-1): - """ - Excecutes a specific query and returns the results. - - Parameters - - name: The name of the query to be executed - - args (optional): The arguments to be passed into the query - - args_delimiter (optional): The delimiter to be used to parse the args. Defaults to ','. - - offset (optional): Number of rows to skip. Defaults to 0. - - count (optional): Number of rows to return. Default set by administrator. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(name, str)): - raise TypeError('name must be a string') - if (not isinstance(args, str)): - raise TypeError('args must be a string') - if (not isinstance(args_delimiter, str)): - raise TypeError('args_delimiter must be a string') - if ((not isinstance(offset, int))): - raise TypeError('offset must be an int') - if (not offset >= 0): - raise ValueError('offset must be greater than or equal to 0') - if ((not isinstance(count, int))): - raise TypeError('count must be an int') - if (not count >= -1): - raise ValueError('count must be greater than or equal to 0 or flag value -1') - - headers = { - 'Authorization': 'Bearer ' + self.token, - } - - params = { - 'op': 'execute_specific_query', - 'name': name, - 'offset': offset, - 'args-delimiter': args_delimiter - } - - if (count != -1): - params['count'] = count - - if (args != ''): - params['args'] = args - - r = requests.get(self.url_base + '/query', headers=headers, params=params) - - if (r.status_code / 100 == 2): - rdict = r.json() - - if rdict['irods_response']['status_code']: - print('Failed to execute query: iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Query executed successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def add_specific_query(self, name: str, sql: str): - """ - Adds a SpecificQuery to the iRODS zone. - - Parameters - - name: The name of the query to be added. - - sql: The SQL attached to the query. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(name, str)): - raise TypeError('name must be a string') - if (not isinstance(sql, str)): - raise TypeError('sql must be a string') - - headers = { - 'Authorization': 'Bearer ' + self.token, - } - - data = { - 'op': 'add_specific_query', - 'name': name, - 'sql': sql - } - - r = requests.post(self.url_base + '/query', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - - if rdict['irods_response']['status_code']: - print('Failed to add query: iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Query added successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def remove_specific_query(self, name): - """ - Removes a SpecificQuery from the iRODS zone. - - Parameters - - name: The name of the SpecificQuery to be removed - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(name, str)): - raise TypeError('name must be a string') - - headers = { - 'Authorization': 'Bearer ' + self.token, - } - - data = { - 'op': 'remove_specific_query', - 'name': name - } - - r = requests.post(self.url_base + '/query', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - - if rdict['irods_response']['status_code']: - print('Failed to remove query: iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Query removed successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) \ No newline at end of file + +class Queries: + """Perform query operations via iRODS HTTP API.""" + + def __init__(self, url_base: str): + """ + Initialize Queries with a base url. + + Token is set to None initially, and updated when setToken() is called in irodsClient. + """ + self.url_base = url_base + self.token = None + + def execute_genquery( + self, + query: str, + offset: int = 0, + count: int = -1, + case_sensitive: int = 1, + distinct: int = 1, + parser: str = "genquery1", + sql_only: int = 0, + zone: str = "", + ): + """ + Execute a GenQuery string and returns the results. + + Args: + query: The query being executed. + offset: Number of rows to skip. Defaults to 0. + count: Number of rows to return. Default set by administrator. + case_sensitive: Set to 1 to execute a case sensitive query, otherwise + set to 0. Defaults to 1. Only supported by GenQuery1. + distinct: Set to 1 to collapse duplicate rows, otherwise set to 0. + Defaults to 1. Only supported by GenQuery 1. + parser: User either genquery1 or genquery2. Defaults to genquery1. + sql_only: Set to 1 to execute an SQL only query, otherwise set to 0. + Defaults to 0. Only supported by GenQuery2. + zone: The zone name. Defaults to the local zone. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + + Raises: + ValueError: If parser is not 'genquery1' or 'genquery2'. + """ + common.check_token(self.token) + common.validate_instance(query, str) + common.validate_gte_zero(offset) + common.validate_gte_minus1(count) + common.validate_0_or_1(case_sensitive) + common.validate_0_or_1(distinct) + common.validate_instance(parser, str) + if parser not in ["genquery1", "genquery2"]: + raise ValueError("parser must be either 'genquery1' or 'genquery2'") + common.validate_0_or_1(sql_only) + common.validate_instance(zone, str) + + headers = { + "Authorization": "Bearer " + self.token, + } + + params = { + "op": "execute_genquery", + "query": query, + "offset": offset, + "parser": parser, + } + + if count != -1: + params["count"] = count + + if zone != "": + params["zone"] = zone + + if parser == "genquery1": + params["case-sensitive"] = case_sensitive + params["distinct"] = distinct + else: + params["sql-only"] = sql_only + + r = requests.get(self.url_base + "/query", headers=headers, params=params, timeout=30) + return common.process_response(r) + + def execute_specific_query( + self, + name: str, + args: str = "", + args_delimiter: str = ",", + offset: int = 0, + count: int = -1, + ): + """ + Execute a specific query and returns the results. + + Args: + name: The name of the query to be executed. + args: The arguments to be passed into the query. + args_delimiter: The delimiter to be used to parse the args. Defaults to ','. + offset: Number of rows to skip. Defaults to 0. + count: Number of rows to return. Default set by administrator. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(name, str) + common.validate_instance(args, str) + common.validate_instance(args_delimiter, str) + common.validate_gte_zero(offset) + common.validate_gte_minus1(count) + + headers = { + "Authorization": "Bearer " + self.token, + } + + params = { + "op": "execute_specific_query", + "name": name, + "offset": offset, + "args-delimiter": args_delimiter, + } + + if count != -1: + params["count"] = count + + if args != "": + params["args"] = args + + r = requests.get(self.url_base + "/query", headers=headers, params=params, timeout=30) + return common.process_response(r) + + def add_specific_query(self, name: str, sql: str): + """ + Add a SpecificQuery to the iRODS zone. + + Args: + name: The name of the query to be added. + sql: The SQL attached to the query. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(name, str) + common.validate_instance(sql, str) + + headers = { + "Authorization": "Bearer " + self.token, + } + + data = {"op": "add_specific_query", "name": name, "sql": sql} + + r = requests.post(self.url_base + "/query", headers=headers, data=data, timeout=30) + return common.process_response(r) + + def remove_specific_query(self, name): + """ + Remove a SpecificQuery from the iRODS zone. + + Args: + name: The name of the SpecificQuery to be removed. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(name, str) + + headers = { + "Authorization": "Bearer " + self.token, + } + + data = {"op": "remove_specific_query", "name": name} + + r = requests.post(self.url_base + "/query", headers=headers, data=data, timeout=30) + return common.process_response(r) diff --git a/irods_http_client/resource_operations.py b/irods_http_client/resource_operations.py index d215d7a..9243371 100644 --- a/irods_http_client/resource_operations.py +++ b/irods_http_client/resource_operations.py @@ -1,561 +1,280 @@ -import requests +"""Resource operations for iRODS HTTP API.""" + import json -class Resources: +import requests - def __init__(self, url_base: str): - """" - Initializes DataObjects with a base url. - Token is set to None initially, and updated when setToken() is called in irodsClient. - """ - self.url_base = url_base - self.token = None - - - def create(self, name: str, type: str, host: str, vault_path: str, context: str): - """ - Creates a new resource. - - Parameters - - name: The name of the resource to be created. - - type: The type of the resource to be created. - - host: The host of the resource to be created. May or may not be required depending on the resource type. - - vault_path: Path to the storage vault for the resource. May or may not be required depending on the resource type. - - context: May or may not be required depending on the resource type. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(name, str)): - raise TypeError('name must be a string') - if (not isinstance(type, str)): - raise TypeError('type must be a string') - if (not isinstance(host, str)): - raise TypeError('host must be a string') - if (not isinstance(vault_path, str)): - raise TypeError('vault_path must be a string') - if (not isinstance(context, str)): - raise TypeError('context must be a string') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded', - } - - data = { - 'op': 'create', - 'name': name, - 'type': type - } - - if (host != ''): - data['host'] = host - - if (vault_path != ''): - data['vault-path'] = vault_path - - if (context != ''): - data['context'] = context - - r = requests.post(self.url_base + '/resources', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - - if rdict['irods_response']['status_code']: - print('Failed to create resource \'' + name + '\': iRODS Status Code ' + str(rdict['irods_response']['status_code']) + ' - ' + str(rdict['irods_response']['status_message'])) - else: - print('Resource \'' + name + '\' created successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def remove(self, name: str): - """ - Removes an existing resource. - - Parameters - - name: The name of the resource to be removed. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(name, str)): - raise TypeError('name must be a string') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded', - } - - data = { - 'op': 'remove', - 'name': name - } - - r = requests.post(self.url_base + '/resources', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - - if rdict['irods_response']['status_code']: - print('Failed to remove resource \'' + name + '\': iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Resource \'' + name + '\' removed successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - elif (r.status_code / 100 == 4): - print('Failed to remove resource \'' + name + '\'') - - return(r) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def modify(self, name: str, property: str, value: str): - """ - Modifies a property for a resource. - - Parameters - - name: The name of the resource to be modified. - - property: The property to be modified. - - value: The new value to be set. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(name, str)): - raise TypeError('name must be a string') - if (not isinstance(property, str)): - raise TypeError('property must be a string') - if property not in ['name', 'type', 'host', 'vault_path', 'context', 'status', 'free_space', 'comments', 'information']: - raise ValueError('Invalid property. Valid properties:\n - name\n - type\n - host\n - ''vault_path\n - context' + - '\n - status\n - free_space\n - comments\n - information') - if (not isinstance(value, str)): - raise TypeError('value must be a string') - if ((property == 'status') and (value != 'up') and (value != 'down')): - raise ValueError('status must be either \'up\' or \'down\'') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded', - } - - data = { - 'op': 'modify', - 'name': name, - 'property': property, - 'value': value - } - - r = requests.post(self.url_base + '/resources', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - - if rdict['irods_response']['status_code']: - print('Failed to modify property for \'' + name + '\': iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Property for \'' + name + '\' modified successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - elif (r.status_code / 100 == 4): - print('Failed to modify property for \'' + name + '\'') - - return(r) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def add_child(self, parent_name: str, child_name: str, context: str=''): - """ - Creates a parent-child relationship between two resources. - - Parameters - - parent_name: The name of the parent resource. - - child_name: The name of the child resource. - - context (optional): Additional information for the zone. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(parent_name, str)): - raise TypeError('parent_name must be a string') - if (not isinstance(child_name, str)): - raise TypeError('child_name must be a string') - if (not isinstance(context, str)): - raise TypeError('context must be a string') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded', - } - - data = { - 'op': 'add_child', - 'parent-name': parent_name, - 'child-name': child_name - } - - if (context != ''): - data['context'] = context - - r = requests.post(self.url_base + '/resources', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - - if rdict['irods_response']['status_code']: - print('Failed to add \'' + child_name + '\' as a child of \'' + parent_name + '\': iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Added \'' + child_name + '\' as a child of \'' + parent_name + '\' successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - elif (r.status_code / 100 == 4): - print('Failed to add \'' + child_name + '\' as a child of \'' + parent_name + '\'') - - return(r) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def remove_child(self, parent_name: str, child_name: str): - """ - Removes a parent-child relationship between two resources. - - Parameters - - parent_name: The name of the parent resource. - - child_name: The name of the child resource. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(parent_name, str)): - raise TypeError('parent_name must be a string') - if (not isinstance(child_name, str)): - raise TypeError('child_name must be a string') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded', - } - - data = { - 'op': 'remove_child', - 'parent-name': parent_name, - 'child-name': child_name - } - - r = requests.post(self.url_base + '/resources', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - - if rdict['irods_response']['status_code']: - print('Failed to remove \'' + child_name + '\' as a child of \'' + parent_name + '\': iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Removed \'' + child_name + '\' as a child of \'' + parent_name + '\' successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - elif (r.status_code / 100 == 4): - print('Failed to remove \'' + child_name + '\' as a child of \'' + parent_name + '\'') - - return(r) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def rebalance(self, name: str): - """ - Rebalances a resource hierarchy. - - Parameters - - name: The name of the resource to be rebalanced. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(name, str)): - raise TypeError('name must be a string') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded', - } - - data = { - 'op': 'rebalance', - 'name': name - } - - r = requests.post(self.url_base + '/resources', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - - if rdict['irods_response']['status_code']: - print('Failed to rebalance \'' + name + '\': iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('\'' + name + '\' rebalanced successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - elif (r.status_code / 100 == 4): - print('Failed to rebalance\'' + name + '\'') - - return(r) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def stat(self, name: str): - """ - Retrieves information for a resource. - - Parameters - - name: The name of the resource to be accessed. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(name, str)): - raise TypeError('name must be a string') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded', - } - - params = { - 'op': 'stat', - 'name': name - } - - r = requests.get(self.url_base + '/resources', headers=headers, params=params) - - if (r.status_code / 100 == 2): - rdict = r.json() - - if rdict['irods_response']['status_code']: - print('Failed to retrieve information for \'' + name + '\': iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Information for \'' + name + '\' retrieved successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - elif (r.status_code / 100 == 4): - print('Failed to retrieve information for \'' + name + '\'') - - return(r) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def modify_metadata(self, name: str, operations: dict, admin: int=0): - """ - Modifies the metadata for a resource. - - Parameters - - name: The absolute logical path of the resource to have its metadata modified. - - operations: Dictionary containing the operations to carry out. Should contain the operation, attribute, value, and optionally units. - - admin (optional): Set to 1 to run this operation as an admin, otherwise set to 0. Defaults to 0. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(name, str)): - raise TypeError('name must be a string') - if (not isinstance(operations, list)): - raise TypeError('operations must be a list of dictionaries') - if (not isinstance(operations[0], dict)): - raise TypeError('operations must be a list of dictionaries') - if (not isinstance(admin, int)): - raise TypeError('admin must be an int 1 or 0') - if ((not admin == 0) and (not admin == 1)): - raise ValueError('admin must be an int 1 or 0') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded', - } - - data = { - 'op': 'modify_metadata', - 'name': name, - 'operations': json.dumps(operations), - 'admin': admin - } - - r = requests.post(self.url_base + '/resources', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - - if rdict['irods_response']['status_code']: - print('Failed to modify metadata for \'' + name + '\': iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Metadata for \'' + name + '\' modified successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) \ No newline at end of file +from . import common + + +class Resources: + """Perform resource operations via iRODS HTTP API.""" + + def __init__(self, url_base: str): + """ + Initialize DataObjects with a base url. + + Token is set to None initially, and updated when setToken() is called in irodsClient. + """ + self.url_base = url_base + self.token = None + + def create(self, name: str, type_: str, host: str, vault_path: str, context: str): + """ + Create a new resource. + + Args: + name: The name of the resource to be created. + type_: The type of the resource to be created. + host: The host of the resource to be created. May or may not be required depending + on the resource type. + vault_path: Path to the storage vault for the resource. May or may not be required + depending on the resource type. + context: May or may not be required depending on the resource type. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(name, str) + common.validate_instance(type_, str) + common.validate_instance(host, str) + common.validate_instance(vault_path, str) + common.validate_instance(context, str) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = {"op": "create", "name": name, "type": type_} + + if host != "": + data["host"] = host + + if vault_path != "": + data["vault-path"] = vault_path + + if context != "": + data["context"] = context + + r = requests.post(self.url_base + "/resources", headers=headers, data=data, timeout=30) + return common.process_response(r) + + def remove(self, name: str): + """ + Remove an existing resource. + + Args: + name: The name of the resource to be removed. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(name, str) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = {"op": "remove", "name": name} + + r = requests.post(self.url_base + "/resources", headers=headers, data=data, timeout=30) + return common.process_response(r) + + def modify(self, name: str, property_: str, value: str): + """ + Modify a property for a resource. + + Args: + name: The name of the resource to be modified. + property_: The property to be modified. + value: The new value to be set. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + + Raises: + ValueError: If property_ is not a valid resource property. + """ + common.check_token(self.token) + common.validate_instance(name, str) + common.validate_instance(property_, str) + if property_ not in [ + "name", + "type", + "host", + "vault_path", + "context", + "status", + "free_space", + "comments", + "information", + ]: + raise ValueError( + "Invalid property. Valid properties:\n - name\n - type\n - host\n - " + "vault_path\n - context" + "\n - status\n - free_space\n - comments\n - information" + ) + common.validate_instance(value, str) + if (property_ == "status") and (value not in ["up", "down"]): + raise ValueError("status must be either 'up' or 'down'") + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = {"op": "modify", "name": name, "property": property_, "value": value} + + r = requests.post(self.url_base + "/resources", headers=headers, data=data, timeout=30) + return common.process_response(r) + + def add_child(self, parent_name: str, child_name: str, context: str = ""): + """ + Create a parent-child relationship between two resources. + + Args: + parent_name: The name of the parent resource. + child_name: The name of the child resource. + context: Additional information for the zone. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(parent_name, str) + common.validate_instance(child_name, str) + common.validate_instance(context, str) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = {"op": "add_child", "parent-name": parent_name, "child-name": child_name} + + if context != "": + data["context"] = context + + r = requests.post(self.url_base + "/resources", headers=headers, data=data, timeout=30) + return common.process_response(r) + + def remove_child(self, parent_name: str, child_name: str): + """ + Remove a parent-child relationship between two resources. + + Args: + parent_name: The name of the parent resource. + child_name: The name of the child resource. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(parent_name, str) + common.validate_instance(child_name, str) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = { + "op": "remove_child", + "parent-name": parent_name, + "child-name": child_name, + } + + r = requests.post(self.url_base + "/resources", headers=headers, data=data, timeout=30) + return common.process_response(r) + + def rebalance(self, name: str): + """ + Rebalance a resource hierarchy. + + Args: + name: The name of the resource to be rebalanced. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(name, str) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = {"op": "rebalance", "name": name} + + r = requests.post(self.url_base + "/resources", headers=headers, data=data, timeout=30) + return common.process_response(r) + + def stat(self, name: str): + """ + Retrieve information for a resource. + + Args: + name: The name of the resource to be accessed. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(name, str) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + params = {"op": "stat", "name": name} + + r = requests.get(self.url_base + "/resources", headers=headers, params=params, timeout=30) + return common.process_response(r) + + def modify_metadata(self, name: str, operations: dict, admin: int = 0): + """ + Modify the metadata for a resource. + + Args: + name: The absolute logical path of the resource to have its metadata modified. + operations: Dictionary containing the operations to carry out. Should contain the + operation, attribute, value, and optionally units. + admin: Set to 1 to run this operation as an admin, otherwise set to 0. Defaults to 0. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(name, str) + common.validate_instance(operations, list) + common.validate_instance(operations[0], dict) + common.validate_0_or_1(admin) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = { + "op": "modify_metadata", + "name": name, + "operations": json.dumps(operations), + "admin": admin, + } + + r = requests.post(self.url_base + "/resources", headers=headers, data=data, timeout=30) + return common.process_response(r) diff --git a/irods_http_client/rule_operations.py b/irods_http_client/rule_operations.py index fca88d6..9f53e33 100644 --- a/irods_http_client/rule_operations.py +++ b/irods_http_client/rule_operations.py @@ -1,185 +1,90 @@ +"""Rule operations for iRODS HTTP API.""" + import requests -import json + +from . import common + class Rules: - - def __init__(self, url_base: str): - """" - Initializes Rules with a base url. - Token is set to None initially, and updated when setToken() is called in irodsClient. - """ - self.url_base = url_base - self.token = None - - - def list_rule_engines(self): - """ - Lists available rule engine plugin instances. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - - headers = { - 'Authorization': 'Bearer ' + self.token, - } - - params = { - 'op': 'list_rule_engines' - } - - r = requests.get(self.url_base + '/rules', params=params, headers=headers) - - rdict = r.json() - - if (r.status_code / 100 == 2): - if rdict['irods_response']['status_code']: - print('Failed to retrieve rule engines list: iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Rule engine list retrieved successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def execute(self, rule_text: str, rep_instance: str=''): - """ - Executes rule code. - - Parameters - - rule_text: The rule code to execute. - - rep_instance (optional): The rule engine plugin to run the rule-text against. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(rule_text, str)): - raise TypeError('name must be a string') - if (not isinstance(rep_instance, str)): - raise TypeError('name must be a string') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded', - } - - data = { - 'op': 'execute', - 'rule-text': rule_text - } - - if (rep_instance != ''): - data['rep-instance'] = rep_instance - - r = requests.post(self.url_base + '/rules', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - - if rdict['irods_response']['status_code']: - print('Failed to remove execute rule: iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Rule executed successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def remove_delay_rule(self, rule_id: int): - """ - Removes a delay rule from the catalog. - - Parameters - - rule_id: The id of the delay rule to be removed. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(rule_id, int)): - raise TypeError('rule_id must be an int') - if (not rule_id >= 0): - raise ValueError('rule_id must be greater than or equal to 0') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded', - } - - data = { - 'op': 'remove_delay_rule', - 'rule-id': rule_id - } - - r = requests.post(self.url_base + '/rules', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - - if rdict['irods_response']['status_code']: - print('Failed to remove delay rule: iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Delay rule removed successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) \ No newline at end of file + """Perform rule operations via iRODS HTTP API.""" + + def __init__(self, url_base: str): + """ + Initialize Rules with a base url. + + Token is set to None initially, and updated when setToken() is called in irodsClient. + """ + self.url_base = url_base + self.token = None + + def list_rule_engines(self): + """ + List available rule engine plugin instances. + + Returns + - A dict containing the HTTP status code and iRODS response. + - The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + + headers = { + "Authorization": "Bearer " + self.token, + } + + params = {"op": "list_rule_engines"} + + r = requests.get(self.url_base + "/rules", params=params, headers=headers, timeout=30) + return common.process_response(r) + + def execute(self, rule_text: str, rep_instance: str = ""): + """ + Execute rule code. + + Args: + rule_text: The rule code to execute. + rep_instance: The rule engine plugin to run the rule-text against. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(rule_text, str) + common.validate_instance(rep_instance, str) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = {"op": "execute", "rule-text": rule_text} + + if rep_instance != "": + data["rep-instance"] = rep_instance + + r = requests.post(self.url_base + "/rules", headers=headers, data=data, timeout=30) + return common.process_response(r) + + def remove_delay_rule(self, rule_id: int): + """ + Remove a delay rule from the catalog. + + Args: + rule_id: The id of the delay rule to be removed. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_gte_zero(rule_id) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = {"op": "remove_delay_rule", "rule-id": rule_id} + + r = requests.post(self.url_base + "/rules", headers=headers, data=data, timeout=30) + return common.process_response(r) diff --git a/irods_http_client/ticket_operations.py b/irods_http_client/ticket_operations.py index 4d209f3..16799af 100644 --- a/irods_http_client/ticket_operations.py +++ b/irods_http_client/ticket_operations.py @@ -1,182 +1,113 @@ +"""Ticket operations for iRODS HTTP API.""" + import requests -import json -class Tickets: +from . import common - def __init__(self, url_base: str): - """" - Initializes Tickets with a base url. - Token is set to None initially, and updated when setToken() is called in irodsClient. - """ - self.url_base = url_base - self.token = None - - - def create(self, lpath: str, type: str='read', use_count: int=-1, write_data_object_count: int=-1, write_byte_count: int=-1, - seconds_until_expiration: int=-1, users: str='', groups: str='', hosts: str=''): - """ - Creates a new ticket for a collection or data object. - - Parameters - - lpath: Absolute logical path to a data object or collection. - - type (optional): Read or write. Defaults to read. - - use_count (optional): Number of times the ticket can be used. - - write_data_object_count (optional): Max number of writes that can be performed. - - write_byte_count (optional): Max number of bytes that can be written. - - seconds_until_expiration (optional): Number of seconds before the ticket expires. - - users (optional): Comma-delimited list of users allowed to use the ticket. - - groups (optional): Comma-delimited list of groups allowed to use the ticket. - - hosts (optional): Comma-delimited list of hosts allowed to use the ticket. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(lpath, str)): - raise TypeError('lpath must be an string') - if (not isinstance(type, str)): - raise TypeError('type must be a string') - if type not in ['read', 'write']: - raise ValueError('type must be either read or write') - if (not isinstance(use_count, int)): - raise TypeError('use_count must be an int') - if (not use_count >= -1): - raise ValueError('use_count must be greater than or equal to 0 or flag value -1') - if (not isinstance(write_data_object_count, int)): - raise TypeError('write_data_object_count must be an int') - if (not write_data_object_count >= -1): - raise ValueError('write_data_object_count must be greater than or equal to 0 or flag value -1') - if (not isinstance(write_byte_count, int)): - raise TypeError('write_byte_count must be an int') - if (not write_byte_count >= -1): - raise ValueError('write_byte_count must be greater than or equal to 0 or flag value -1') - if (not isinstance(seconds_until_expiration, int)): - raise TypeError('seconds_until_expiration must be an int') - if (not seconds_until_expiration >= -1): - raise ValueError('seconds_until_expiration must be greater than or equal to 0 or flag value -1') - if (not isinstance(users, str)): - raise TypeError('users must be an string') - if (not isinstance(groups, str)): - raise TypeError('groups must be an string') - if (not isinstance(hosts, str)): - raise TypeError('hosts must be an string') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded', - } - - data = { - 'op': 'create', - 'lpath': lpath, - 'type': type - } - - if (use_count != -1): - data['use-count'] = use_count - if (write_data_object_count != -1): - data['write-data-object-count'] = write_data_object_count - if (write_byte_count != -1): - data['write-byte-count'] = write_byte_count - if (seconds_until_expiration != -1): - data['seconds-until-expiration'] = seconds_until_expiration - if (users != ''): - data['users'] = users - if (groups != ''): - data['groups'] = groups - if (hosts != ''): - data['hosts'] = hosts - - print(data) - - r = requests.post(self.url_base + '/tickets', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - - print('Ticket generated successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def remove(self, name: str): - """ - Removes an existing ticket. - - Parameters - - name: The ticket to be removed. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(name, str)): - raise TypeError('name must be a string') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded', - } - - data = { - 'op': 'remove', - 'name': name - } - - r = requests.post(self.url_base + '/tickets', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - - if rdict['irods_response']['status_code']: - print('Failed to remove ticket \'' + name + '\': iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Ticket \'' + name + '\' removed successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - elif (r.status_code / 100 == 4): - print('Failed to remove ticket \'' + name + '\'') - - return(r) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRods Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) \ No newline at end of file + +class Tickets: + """Perform ticket operations via iRODS HTTP API.""" + + def __init__(self, url_base: str): + """ + Initialize Tickets with a base url. + + Token is set to None initially, and updated when setToken() is called in irodsClient. + """ + self.url_base = url_base + self.token = None + + def create( + self, + lpath: str, + type_: str = "read", + use_count: int = -1, + write_data_object_count: int = -1, + write_byte_count: int = -1, + seconds_until_expiration: int = -1, + users: str = "", + groups: str = "", + hosts: str = "", + ): + """ + Create a new ticket for a collection or data object. + + Args: + lpath: Absolute logical path to a data object or collection. + type_: Read or write. Defaults to read. + use_count: Number of times the ticket can be used. + write_data_object_count: Max number of writes that can be performed. + write_byte_count: Max number of bytes that can be written. + seconds_until_expiration: Number of seconds before the ticket expires. + users: Comma-delimited list of users allowed to use the ticket. + groups: Comma-delimited list of groups allowed to use the ticket. + hosts: Comma-delimited list of hosts allowed to use the ticket. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + + Raises: + ValueError: If type_ is not 'read' or 'write'. + """ + common.check_token(self.token) + common.validate_instance(lpath, str) + common.validate_instance(type_, str) + if type_ not in ["read", "write"]: + raise ValueError("type must be either read or write") + common.validate_gte_minus1(use_count) + common.validate_gte_minus1(write_data_object_count) + common.validate_gte_minus1(write_byte_count) + common.validate_gte_minus1(seconds_until_expiration) + common.validate_instance(users, str) + common.validate_instance(groups, str) + common.validate_instance(hosts, str) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = {"op": "create", "lpath": lpath, "type": type_} + + if use_count != -1: + data["use-count"] = use_count + if write_data_object_count != -1: + data["write-data-object-count"] = write_data_object_count + if write_byte_count != -1: + data["write-byte-count"] = write_byte_count + if seconds_until_expiration != -1: + data["seconds-until-expiration"] = seconds_until_expiration + if users != "": + data["users"] = users + if groups != "": + data["groups"] = groups + if hosts != "": + data["hosts"] = hosts + + r = requests.post(self.url_base + "/tickets", headers=headers, data=data, timeout=30) + return common.process_response(r) + + def remove(self, name: str): + """ + Remove an existing ticket. + + Args: + name: The ticket to be removed. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(name, str) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = {"op": "remove", "name": name} + + r = requests.post(self.url_base + "/tickets", headers=headers, data=data, timeout=30) + return common.process_response(r) diff --git a/irods_http_client/user_group_operations.py b/irods_http_client/user_group_operations.py index 73e06be..f5812cf 100644 --- a/irods_http_client/user_group_operations.py +++ b/irods_http_client/user_group_operations.py @@ -1,809 +1,380 @@ -import requests +"""User and group operations for iRODS HTTP API.""" + import json +import requests + +from . import common + + class UsersGroups: - def __init__(self, url_base: str): - """ - Initializes UsersGroups with a base url. - Token is set to None initially, and updated when setToken() is called in irodsClient. - """ - self.url_base = url_base - self.token = None - - - def create_user(self, name: str, zone: str, user_type: str='rodsuser'): - """ - Creates a new user. Requires rodsadmin or groupadmin privileges. - - Parameters - - name: The name of the user to be created. - - zone: The zone for the user to be created. - - user_type (optional): Can be rodsuser, groupadmin, or rodsadmin. Defaults to rodsuser. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(name, str)): - raise TypeError('name must be a string') - if (not isinstance(zone, str)): - raise TypeError('zone must be a string') - if (not isinstance(user_type, str)): - raise TypeError('user_type must be a string') - if user_type and user_type not in ['rodsuser', 'groupadmin', 'rodsadmin']: - raise ValueError('user_type must be set to rodsuser, groupadmin, or rodsadmin.') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded' - } - - data = { - 'op': 'create_user', - 'name': name, - 'zone': zone, - 'user-type': user_type - } - - r = requests.post(self.url_base + '/users-groups', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - if rdict['irods_response']['status_code']: - print('Failed to create user \'' + name + '\': iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('User \'' + name + '\' created successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRODS Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def remove_user(self, name: str, zone: str): - """ - Removes a user. Requires rodsadmin privileges. - - Parameters - - name: The name of the user to be removed. - - zone: The zone for the user to be removed. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(name, str)): - raise TypeError('name must be a string') - if (not isinstance(zone, str)): - raise TypeError('zone must be a string') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded' - } - - data = { - 'op': 'remove_user', - 'name': name, - 'zone': zone - } - - r = requests.post(self.url_base + '/users-groups', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - if rdict['irods_response']['status_code']: - print('Failed to remove user \'' + name + '\': iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('User \'' + name + '\' removed successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRODS Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def set_password(self, name: str, zone: str, new_password: str=''): - """ - Changes a users password. Requires rodsadmin privileges. - - Parameters - - name: The name of the user to have their password changed. - - zone: The zone for the user to have their password changed. - - new_password: The new password to set for the user. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(name, str)): - raise TypeError('name must be a string') - if (not isinstance(zone, str)): - raise TypeError('zone must be a string') - if (not isinstance(new_password, str)): - raise TypeError('new_password must be a string') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded' - } - - data = { - 'op': 'set_password', - 'name': name, - 'zone': zone, - 'new-password': new_password - } - - r = requests.post(self.url_base + '/users-groups', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - if rdict['irods_response']['status_code']: - print('Failed to change password for user \'' + name + '\': iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Password for user \'' + name + '\' changed successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRODS Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def set_user_type(self, name: str, zone: str, user_type: str): - """ - Changes a users type. Requires rodsadmin privileges. - - Parameters - - name: The name of the user to have their type updated. - - zone: The zone for the user to have their type updated. - - user_type: Can be rodsuser, groupadmin, or rodsadmin. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(name, str)): - raise TypeError('name must be a string') - if (not isinstance(zone, str)): - raise TypeError('zone must be a string') - if (not isinstance(user_type, str)): - raise TypeError('user_type must be a string') - if user_type and user_type not in ['rodsuser', 'groupadmin', 'rodsadmin']: - raise ValueError('user_type must be set to rodsuser, groupadmin, or rodsadmin.') - - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded' - } - - data = { - 'op': 'set_user_type', - 'name': name, - 'zone': zone, - 'new-user-type': user_type - } - - r = requests.post(self.url_base + '/users-groups', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - if rdict['irods_response']['status_code']: - print('Failed to change type for user \'' + name + '\': iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Type for user \'' + name + '\' changed successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRODS Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def create_group(self, name: str): - """ - Creates a new group. Requires rodsadmin or groupadmin privileges. - - Parameters - - name: The name of the group to be created. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(name, str)): - raise TypeError('name must be a string') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded' - } - - data = { - 'op': 'create_group', - 'name': name - } - - r = requests.post(self.url_base + '/users-groups', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - if rdict['irods_response']['status_code']: - print('Failed to create group \'' + name + '\': iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Group \'' + name + '\' created successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRODS Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def remove_group(self, name: str): - """ - Removes a group. Requires rodsadmin privileges. - - Parameters - - name: The name of the group to be removed. - - Parameters - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(name, str)): - raise TypeError('name must be a string') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded' - } - - data = { - 'op': 'remove_group', - 'name': name - } - - r = requests.post(self.url_base + '/users-groups', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - if rdict['irods_response']['status_code']: - print('Failed to remove group \'' + name + '\': iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Group \'' + name + '\' removed successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRODS Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def add_to_group(self, user: str, zone: str, group: str=''): - """ - Adds a user to a group. Requires rodsadmin or groupadmin privileges. - - Parameters - - user: The user to be added to the group. - - zone: The zone for the user to be added to the group. - - group: The group for the user to be added to. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(user, str)): - raise TypeError('user must be a string') - if (not isinstance(zone, str)): - raise TypeError('zone must be a string') - if (not isinstance(group, str)): - raise TypeError('group must be a string') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded' - } - - data = { - 'op': 'add_to_group', - 'user': user, - 'zone': zone, - 'group': group - } - - r = requests.post(self.url_base + '/users-groups', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - if rdict['irods_response']['status_code']: - print('Failed to add user \'' + user + '\' to group \'' + group + '\' : iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('User \'' + user + '\' added to group \'' + group + '\' successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRODS Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def remove_from_group(self, user: str, zone: str, group: str): - """ - Removes a user from a group. Requires rodsadmin or groupadmin privileges. - - Parameters - - user: The user to be removed from the group. - - zone: The zone for the user to be removed from the group. - - group: The group for the user to be removed from. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(user, str)): - raise TypeError('user must be a string') - if (not isinstance(zone, str)): - raise TypeError('zone must be a string') - if (not isinstance(group, str)): - raise TypeError('group must be a string') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded' - } - - data = { - 'op': 'remove_from_group', - 'user': user, - 'zone': zone, - 'group': group - } - - r = requests.post(self.url_base + '/users-groups', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - if rdict['irods_response']['status_code']: - print('Failed to remove user \'' + user + '\' from group \'' + group + '\' : iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('User \'' + user + '\' removed from group \'' + group + '\' successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRODS Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def users(self): - """ - Lists all users in the zone. Requires rodsadmin privileges. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - - headers = { - 'Authorization': 'Bearer ' + self.token - } - - params = { - 'op': 'users' - } - - r = requests.get(self.url_base + '/users-groups', headers=headers, params=params) - - if (r.status_code / 100 == 2): - rdict = r.json() - if rdict['irods_response']['status_code']: - print('Failed to retrieve user list : iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('User list retrieved successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRODS Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def groups(self): - """ - Lists all groups in the zone. Requires rodsadmin privileges. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - - headers = { - 'Authorization': 'Bearer ' + self.token, - } - - params = { - 'op': 'groups' - } - - r = requests.get(self.url_base + '/users-groups', headers=headers, params=params) - - if (r.status_code / 100 == 2): - rdict = r.json() - if rdict['irods_response']['status_code']: - print('Failed to retrieve group list : iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Group list retrieved successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRODS Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def is_member_of_group(self, group: str, user: str, zone: str): - """ - Returns whether a user is a member of a group or not. - - Parameters - group: The group being checked. - user: The user being checked. - zone: The zone for the user being checked. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(group, str)): - raise TypeError('group must be a string') - if (not isinstance(user, str)): - raise TypeError('user must be a string') - if (not isinstance(zone, str)): - raise TypeError('zone must be a string') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded' - } - - params = { - 'op': 'is_member_of_group', - 'group': group, - 'user': user, - 'zone': zone - } - - r = requests.get(self.url_base + '/users-groups', headers=headers, params=params) - - if (r.status_code / 100 == 2): - rdict = r.json() - if rdict['irods_response']['status_code']: - print('Failed to check membership in group \'' + group + '\' for user \'' + user + '\' : iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Membership in group \'' + group + '\' for user \'' + user + '\' checked successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRODS Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def stat(self, name: str, zone: str=''): - """ - Returns information about a user or group. - - Parameters - - name: The name of the user or group to be accessed. - - zone: The zone of the user to be accessed. Not required for groups. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(name, str)): - raise TypeError('name must be a string') - if (not isinstance(zone, str)): - raise TypeError('zone must be a string') - - headers = { - 'Authorization': 'Bearer ' + self.token - } - - params = { - 'op': 'stat', - 'name': name - } - - if (zone != ''): - params['zone'] = zone - - r = requests.get(self.url_base + '/users-groups', headers=headers, params=params) - - if (r.status_code / 100 == 2): - rdict = r.json() - if rdict['irods_response']['status_code']: - print('Failed to retrieve information for \'' + name + '\' : iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Information for \'' + name + '\' retrieved successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRODS Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def modify_metadata(self, name: str, operations: list): - """ - Modifies the metadata for a user or group. Requires rodsadmin privileges. - - Parameters - - name: The user or group to be modified. - - operations: The operations to be carried out. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(name, str)): - raise TypeError('name must be a string') - if (not isinstance(operations, list)): - raise TypeError('operations must be a list of dictionaries') - if (not isinstance(operations[0], dict)): - raise TypeError('operations must be a list of dictionaries') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded' - } - - data = { - 'op': 'modify_metadata', - 'name': name, - 'operations': json.dumps(operations) - } - - r = requests.post(self.url_base + '/users-groups', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - - if rdict['irods_response']['status_code']: - print('Failed to modify metadata for \'' + name + '\': iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Metadata for \'' + name + '\' modified successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRODS Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) + """Perform user and group operations via iRODS HTTP API.""" + + def __init__(self, url_base: str): + """ + Initialize UsersGroups with a base url. + + Token is set to None initially, and updated when setToken() is called in irodsClient. + """ + self.url_base = url_base + self.token = None + + def create_user(self, name: str, zone: str, user_type: str = "rodsuser"): + """ + Create a new user. Requires rodsadmin or groupadmin privileges. + + Args: + name: The name of the user to be created. + zone: The zone for the user to be created. + user_type: Can be rodsuser, groupadmin, or rodsadmin. Defaults to rodsuser. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + + Raises: + ValueError: If user_type is not 'rodsuser', 'groupadmin', or 'rodsadmin'. + """ + common.check_token(self.token) + common.validate_instance(name, str) + common.validate_instance(zone, str) + common.validate_instance(user_type, str) + if user_type not in ["rodsuser", "groupadmin", "rodsadmin"]: + raise ValueError("user_type must be set to rodsuser, groupadmin, or rodsadmin.") + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = {"op": "create_user", "name": name, "zone": zone, "user-type": user_type} + + r = requests.post(self.url_base + "/users-groups", headers=headers, data=data, timeout=30) + return common.process_response(r) + + def remove_user(self, name: str, zone: str): + """ + Remove a user. Requires rodsadmin privileges. + + Args: + name: The name of the user to be removed. + zone: The zone for the user to be removed. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(name, str) + common.validate_instance(zone, str) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = {"op": "remove_user", "name": name, "zone": zone} + + r = requests.post(self.url_base + "/users-groups", headers=headers, data=data, timeout=30) + return common.process_response(r) + + def set_password(self, name: str, zone: str, new_password: str = ""): + """ + Change a users password. Requires rodsadmin privileges. + + Args: + name: The name of the user to have their password changed. + zone: The zone for the user to have their password changed. + new_password: The new password to set for the user. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(name, str) + common.validate_instance(zone, str) + common.validate_instance(new_password, str) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = { + "op": "set_password", + "name": name, + "zone": zone, + "new-password": new_password, + } + + r = requests.post(self.url_base + "/users-groups", headers=headers, data=data, timeout=30) + return common.process_response(r) + + def set_user_type(self, name: str, zone: str, user_type: str): + """ + Change a users type. Requires rodsadmin privileges. + + Args: + name: The name of the user to have their type updated. + zone: The zone for the user to have their type updated. + user_type: Can be rodsuser, groupadmin, or rodsadmin. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + + Raises: + ValueError: If user_type is not 'rodsuser', 'groupadmin', or 'rodsadmin'. + """ + common.check_token(self.token) + common.validate_instance(name, str) + common.validate_instance(zone, str) + common.validate_instance(user_type, str) + if user_type not in ["rodsuser", "groupadmin", "rodsadmin"]: + raise ValueError("user_type must be set to rodsuser, groupadmin, or rodsadmin.") + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = { + "op": "set_user_type", + "name": name, + "zone": zone, + "new-user-type": user_type, + } + + r = requests.post(self.url_base + "/users-groups", headers=headers, data=data, timeout=30) + return common.process_response(r) + + def create_group(self, name: str): + """ + Create a new group. Requires rodsadmin or groupadmin privileges. + + Args: + name: The name of the group to be created. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(name, str) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = {"op": "create_group", "name": name} + + r = requests.post(self.url_base + "/users-groups", headers=headers, data=data, timeout=30) + return common.process_response(r) + + def remove_group(self, name: str): + """ + Remove a group. Requires rodsadmin privileges. + + Args: + name: The name of the group to be removed. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(name, str) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = {"op": "remove_group", "name": name} + + r = requests.post(self.url_base + "/users-groups", headers=headers, data=data, timeout=30) + return common.process_response(r) + + def add_to_group(self, user: str, zone: str, group: str = ""): + """ + Add a user to a group. Requires rodsadmin or groupadmin privileges. + + Args: + user: The user to be added to the group. + zone: The zone for the user to be added to the group. + group: The group for the user to be added to. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(user, str) + common.validate_instance(zone, str) + common.validate_instance(group, str) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = {"op": "add_to_group", "user": user, "zone": zone, "group": group} + + r = requests.post(self.url_base + "/users-groups", headers=headers, data=data, timeout=30) + return common.process_response(r) + + def remove_from_group(self, user: str, zone: str, group: str): + """ + Remove a user from a group. Requires rodsadmin or groupadmin privileges. + + Args: + user: The user to be removed from the group. + zone: The zone for the user to be removed from the group. + group: The group for the user to be removed from. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(user, str) + common.validate_instance(zone, str) + common.validate_instance(group, str) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = {"op": "remove_from_group", "user": user, "zone": zone, "group": group} + + r = requests.post(self.url_base + "/users-groups", headers=headers, data=data, timeout=30) + return common.process_response(r) + + def users(self): + """ + List all users in the zone. Requires rodsadmin privileges. + + Returns + - A dict containing the HTTP status code and iRODS response. + - The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + + headers = {"Authorization": "Bearer " + self.token} + + params = {"op": "users"} + + r = requests.get(self.url_base + "/users-groups", headers=headers, params=params, timeout=30) + return common.process_response(r) + + def groups(self): + """ + List all groups in the zone. Requires rodsadmin privileges. + + Returns + - A dict containing the HTTP status code and iRODS response. + - The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + + headers = { + "Authorization": "Bearer " + self.token, + } + + params = {"op": "groups"} + + r = requests.get(self.url_base + "/users-groups", headers=headers, params=params, timeout=30) + return common.process_response(r) + + def is_member_of_group(self, group: str, user: str, zone: str): + """ + Return whether a user is a member of a group or not. + + Args: + group: The group being checked. + user: The user being checked. + zone: The zone for the user being checked. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(group, str) + common.validate_instance(user, str) + common.validate_instance(zone, str) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + params = { + "op": "is_member_of_group", + "group": group, + "user": user, + "zone": zone, + } + + r = requests.get(self.url_base + "/users-groups", headers=headers, params=params, timeout=30) + return common.process_response(r) + + def stat(self, name: str, zone: str = ""): + """ + Return information about a user or group. + + Args: + name: The name of the user or group to be accessed. + zone: The zone of the user to be accessed. Not required for groups. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(name, str) + common.validate_instance(zone, str) + + headers = {"Authorization": "Bearer " + self.token} + + params = {"op": "stat", "name": name} + + if zone != "": + params["zone"] = zone + + r = requests.get(self.url_base + "/users-groups", headers=headers, params=params, timeout=30) + return common.process_response(r) + + def modify_metadata(self, name: str, operations: list): + """ + Modify the metadata for a user or group. Requires rodsadmin privileges. + + Args: + name: The user or group to be modified. + operations: The operations to be carried out. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(name, str) + common.validate_instance(operations, list) + common.validate_instance(operations[0], dict) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = { + "op": "modify_metadata", + "name": name, + "operations": json.dumps(operations), + } + + r = requests.post(self.url_base + "/users-groups", headers=headers, data=data, timeout=30) + return common.process_response(r) diff --git a/irods_http_client/zone_operations.py b/irods_http_client/zone_operations.py index 75a2b17..087e385 100644 --- a/irods_http_client/zone_operations.py +++ b/irods_http_client/zone_operations.py @@ -1,307 +1,145 @@ +"""Zone operations for iRODS HTTP API.""" + import requests -import json + +from . import common + class Zones: - def __init__(self, url_base: str): - """ - Initializes Zones with a base url. - Token is set to None initially, and updated when setToken() is called in irodsClient. - """ - self.url_base = url_base - self.token = None - - - def add(self, name: str, connection_info: str='', comment: str=''): - """ - Adds a remote zone to the local zone. Requires rodsadmin privileges. - - Parameters - - name: The name of the zone to be added. - - connection_info (optional): The host and port to connect to. If included, must be in the format : - - comment (optional): The comment to attach to the zone. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(name, str)): - raise TypeError('name must be a string') - if (not isinstance(connection_info, str)): - raise TypeError('connection_info must be a string') - if (not isinstance(comment, str)): - raise TypeError('comment must be a string') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded' - } - - data = { - 'op': 'add', - 'name': name - } - - if (connection_info != ''): - data['connection-info'] = connection_info - if (comment != ''): - data['comment'] = comment - - r = requests.post(self.url_base + '/zones', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - if rdict['irods_response']['status_code']: - print('Failed to add zone \'' + name + '\' : iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Zone \'' + name + '\' added successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRODS Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def remove(self, name: str): - """ - Removes a remote zone from the local zone. Requires rodsadmin privileges. - - Parameters - - name: The zone to be removed - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(name, str)): - raise TypeError('name must be a string') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded' - } - - data = { - 'op': 'remove', - 'name': name - } - - r = requests.post(self.url_base + '/zones', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - if rdict['irods_response']['status_code']: - print('Failed to remove zone \'' + name + '\' : iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Zone \'' + name + '\' removed successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRODS Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def modify(self, name: str, property: str, value: str): - """ - Modifies properties of a remote zone. Requires rodsadmin privileges. - - Parameters - - name: The name of the zone to be modified. - - property: The property to be modified. Can be set to 'name', 'connection_info', or 'comment'. - The value for 'connection_info' must be in the format :. - - value: The new value to be set. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(name, str)): - raise TypeError('name must be a string') - if (not isinstance(property, str)): - raise TypeError('property must be a string') - if (not isinstance(value, str)): - raise TypeError('value must be a string') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded' - } - - data = { - 'op': 'modify', - 'name': name, - 'property': property, - 'value': value - } - - r = requests.post(self.url_base + '/zones', headers=headers, data=data) - - if (r.status_code / 100 == 2): - rdict = r.json() - if rdict['irods_response']['status_code']: - print('Failed to modify zone \'' + name + '\' : iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Zone \'' + name + '\' modified successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRODS Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def report(self): - """ - Returns information about the iRODS zone. Requires rodsadmin privileges. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded' - } - - params = { - 'op': 'report' - } - - r = requests.get(self.url_base + '/zones', headers=headers, params=params) - - if (r.status_code / 100 == 2): - rdict = r.json() - if rdict['irods_response']['status_code']: - print('Failed to retrieve information for the iRODS zone : iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Information for the iRODS zone retrieved successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRODS Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - - - def stat(self, name: str): - """ - Returns information about a named iRODS zone. Requires rodsadmin privileges. - - Returns - - A dict containing the HTTP status code and iRODS response. - - The iRODS response is only valid if no error occurred during HTTP communication. - """ - if (self.token == None): - raise RuntimeError('No token set. Use setToken() to set the auth token to be used') - if (not isinstance(name, str)): - raise TypeError('name must be a string') - - headers = { - 'Authorization': 'Bearer ' + self.token, - 'Content-Type': 'application/x-www-form-urlencoded' - } - - params = { - 'op': 'stat', - 'name': name - } - - r = requests.get(self.url_base + '/zones', headers=headers, params=params) - - if (r.status_code / 100 == 2): - rdict = r.json() - if rdict['irods_response']['status_code']: - print('Failed to retrieve information for zone \'' + name + '\' : iRODS Status Code' + str(rdict['irods_response']['status_code'])) - else: - print('Information for zone \'' + name + '\' retrieved successfully') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) - else: - irods_err = '' - rdict = None - if (r.text != ''): - rdict = r.json() - irods_err = ': iRODS Status Code' + str(rdict['irods_response']) - print(f'Error <{r.status_code}>{irods_err}') - - return( - { - 'status_code': r.status_code, - 'data': rdict - } - ) \ No newline at end of file + """Perform zone operations via iRODS HTTP API.""" + + def __init__(self, url_base: str): + """ + Initialize Zones with a base url. + + Token is set to None initially, and updated when setToken() is called in irodsClient. + """ + self.url_base = url_base + self.token = None + + def add(self, name: str, connection_info: str = "", comment: str = ""): + """ + Add a remote zone to the local zone. Requires rodsadmin privileges. + + Args: + name: The name of the zone to be added. + connection_info: The host and port to connect to. If included, must be in the format :. + comment: The comment to attach to the zone. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(name, str) + common.validate_instance(connection_info, str) + common.validate_instance(comment, str) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = {"op": "add", "name": name} + + if connection_info != "": + data["connection-info"] = connection_info + if comment != "": + data["comment"] = comment + + r = requests.post(self.url_base + "/zones", headers=headers, data=data, timeout=30) + return common.process_response(r) + + def remove(self, name: str): + """ + Remove a remote zone from the local zone. Requires rodsadmin privileges. + + Args: + name: The zone to be removed. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(name, str) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = {"op": "remove", "name": name} + + r = requests.post(self.url_base + "/zones", headers=headers, data=data, timeout=30) + return common.process_response(r) + + def modify(self, name: str, property_: str, value: str): + """ + Modify properties of a remote zone. Requires rodsadmin privileges. + + Args: + name: The name of the zone to be modified. + property_: The property to be modified. Can be set to 'name', 'connection_info', or 'comment'. + The value for 'connection_info' must be in the format :. + value: The new value to be set. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(name, str) + common.validate_instance(property_, str) + common.validate_instance(value, str) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = {"op": "modify", "name": name, "property": property_, "value": value} + + r = requests.post(self.url_base + "/zones", headers=headers, data=data, timeout=30) + return common.process_response(r) + + def report(self): + """ + Return information about the iRODS zone. Requires rodsadmin privileges. + + Returns + - A dict containing the HTTP status code and iRODS response. + - The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + params = {"op": "report"} + + r = requests.get(self.url_base + "/zones", headers=headers, params=params, timeout=30) + return common.process_response(r) + + def stat(self, name: str): + """ + Return information about a named iRODS zone. Requires rodsadmin privileges. + + Returns + - A dict containing the HTTP status code and iRODS response. + - The iRODS response is only valid if no error occurred during HTTP communication. + """ + common.check_token(self.token) + common.validate_instance(name, str) + + headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + params = {"op": "stat", "name": name} + + r = requests.get(self.url_base + "/zones", headers=headers, params=params, timeout=30) + return common.process_response(r) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5bc6f5e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,503 @@ +[project] +name = "irods-http-client" +version = "0.1.0" +authors = [ + { name="iRODS Consortium", email="info@irods.org" }, +] +description = "A Python wrapper for the iRODS HTTP API" +readme = "README.md" +requires-python = ">=3.9" +license = "BSD-3-Clause" +classifiers = [ + "Programming Language :: Python :: 3", + "Operating System :: OS Independent", +] + +[project.urls] +Repository = "https://github.com/irods/irods_client_http_python" +Issues = "https://github.com/irods/irods_client_http_python/issues" + +[build-system] +requires = [ + "jsonschema", + "requests", + "setuptools >= 40.9.0" +] +build-backend = "setuptools.build_meta" + +[tool.ruff] +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "site-packages", + "venv", +] + +# Ensure this matches tool.ruff.lint.pycodestyle.max-doc-length +line-length = 120 +indent-width = 4 + +[tool.ruff.format] +quote-style = "preserve" +indent-style = "tab" +line-ending = "auto" + +# Enable reformatting of code snippets in docstrings. +docstring-code-format = true + +# Respect magic trailing commas. +skip-magic-trailing-comma = false + +[tool.ruff.lint] +# Enable preview rules, but require them to be explicitly enabled +preview = true +explicit-preview-rules = true +# Enable rules +select = [ + # eradicate + "ERA", + # flake8-2020 + "YTT", + # flake8-annotations, uncomment if we ever add type annotations + #"ANN", + # flake8-executable + "EXE", + # flake8-async + "ASYNC", + # flake8-blind-except + "BLE", + # flake8-bugbear + "B", + # flake8-builtins + "A", + # flake8-comprehensions + "C4", + # flake8-datetimez + "DTZ", + # flake8-executable + "EXE", + # flake8-future-annotations + "FA", + # flake8-implicit-str-concat + "ISC", + # flake8-import-conventions + "ICN", + # flake8-logging + "LOG", + # flake8-logging-format + "G", + # flake8-no-pep420 + "INP", + # flake8-pie + "PIE", + # flake8-pyi + "PYI", + # flake8-raise + "RSE", + # flake8-return + "RET", + # flake8-self + "SLF", + # flake8-simplify + "SIM", + # flake8-slots + "SLOT", + # flake8-tidy-imports + "TID", + # flake8-todos + "TD", + # flake8-type-checking + "TC", + # flake8-unused-arguments + "ARG", + # flake8-use-pathlib + "PTH", + # flynt + "FLY", + # isort + "I", + # pep8-naming + "N", + # Perflint + "PERF", + # pycodestyle + "E", "W", + # pydoclint + "DOC", + # pydocstyle, flake8-docstrings + "D", + # Pyflakes + "F", + # pygrep-hooks + "PGH", + # Pylint + "PL", + # pyupgrade + "UP", + # refurb + "FURB", + # Ruff-specific rules + "RUF", + # tryceratops + "TRY", + + ## flake8-bandit + ## We want a subset and it's easier to handle some here than in ignore + # S1??: general tests + "S1", + # S2??: misconfiguration + "S2", + # S301: suspicious pickle usage + "S301", + # S306: usage of insecure and deprecated mktemp + "S306", + # S307: usage of insecure eval + "S307", + # S311: usage of bad rand + "S311", + # S313-S319: usage of insecure XML parsing + "S313", "S314", "S315", "S316", "S317", "S318", "S319", + # S5??: cryptography + "S5", + # shell usage + "S602", "S604", "S605", + # S612: + ## flake8-commas + ## We only want one rule from this one + # COM818: trailing comman on bare tuple prohibited + "COM818", + ## flake8-quotes + ## We don't want most rules from this one, as quotes are mostly handled by the formatter + # Q004: Unnecessary escape on inner quote character + "Q004", + ## pydocstyle + ## Certain rules are disabled by tool.ruff.lint.pydocstyle.convention, but we still want some of them + # D213: Multi-line docstring summary should start at the second line + "D213", + # D214: Overindented section + "D214", + # D215: Overindented section underline + "D215", + # D404: First word of the docstring should not be "This" + "D404", + # D405: Section name should be properly capitalized + "D405", + # D406: Missing newline after section name + "D406", + # D409: Mismatched section underline length + "D409", + # D416: Missing colon after section name + "D416", + # D417: Missing parameter documentation + "D417", + + ### preview rules + ## pydoclint + # DOC102: undocumented parameter missing from signature + "DOC102", + # DOC201: undocumented return value + "DOC201", + # DOC202: docstring has returns section, but function/method does not return anything + "DOC202", + # DOC402: undocumented yield value + "DOC402", + # DOC403: docstring has yields section, but function/method does not yield anything + "DOC403", + # DOC501: undocumented raised exception + "DOC501", +] +# Disable rules +ignore = [ + # Not compatible with indent-style: tab + "D206", + + # false positives galore + "ERA001", + + ## flake8-datetimez + # DTZ003: datetime.datetime.utcnow() used + "DTZ003", + + ## flake8-tidy-imports + # TID252: Prefer absolute imports over relative imports + # This can introduce difficulties + "TID252", + + ## flake8-todos + # TD002: Missing author in TODO + "TD002", + + ## pycodestyle + # E402: Module level import not at top of file + # We often need to do some checking before importing stuff + "E402", + # Indentation/alignment/spacing stuff + # E128: continuation line under-indented for visual indent + # Don't hate me cuz I'm pretty. + # Uncomment if ruff ever supports this + #"E128", + # Most indentation handled by formatter + "E101", # indentation contains mixed spaces and tabs + "E111", # wrong indentation multiple + "E114", # wrong indentation multiple (for comments) + "E117", # over-indentation + "W191", # indentation includes tabs + + ## pydoclint + # DOC502: docstring specifies potential exception that is not actually raised in the fucntion/method body + # Does not account for exceptions from other sources + "DOC502", + + ## pylint + # PLC0415: import should be at top-level + # plenty of reasons to put it in a block + "PLC0415", + # PLC1901: comparison to empty string + # Does not account for types + "PLC1901", + # PLE0116: continue in finally block + # Supported since Python 3.8 + "PLE0116", + # Design complexity stuff + # These judgements should always be made by humans + #"PLC0302", # too many lines in module (uncomment if ruff ever supports this) + #"PLR0901", # too many class ancestors (uncomment if ruff ever supports this) + #"PLR0902", # too many instance attributes (uncomment if ruff ever supports this) + #"PLR0903", # too few public methods (uncomment if ruff ever supports this) + "PLR0904", # too many public methods + "PLR0911", # too many return statements + "PLR0912", # too many branches + "PLR0913", # too many arguments + "PLR0914", # too many locals + "PLR0915", # too many statements + "PLR0916", # too many boolean expressions in a single statement + "PLR0917", # too many positional arguments + "PLR1702", # too many nested blocks + + ## tryceratops + # TRY003: Avoid specifying long messages outside the exception class + # Way, way too many false positives + "TRY003", +] + +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +unfixable = [] + +# Don't whine about codes in noqa that ruff doesn't (yet) support +external = [ + # openstack's hacking flake8 plugin + "V", + # flake8-multiline-containers + "JS", + # flake8-typing-imports + "TYP" +] + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +# Allow suggesting `from __future__ import annotations` +future-annotations = true + +# TODO and TODO-like tags to recognize +task-tags = [ + 'FIXME', + 'TODO', + 'HACK', + 'KLUDGE', + 'BUG', + 'OPTIMIZE', + 'XXX', + 'JANK', + 'AWFUL', + 'BAD', + 'CRIME', + 'CRIMES', +] + +# Recognize `typing_extensions` imports +typing-extensions = true +# Speciy additional typing extension modules +typing-modules = [] + +[tool.ruff.lint.per-file-ignores] +"setup.py" = [ + # pydocstyle, flake8-docstrings + # Ignore missing module docstring in setup.py + "D100", +] +"irods/test/*_test.py" = [ + # flake8-bandit + # S106: Possible hardcoded password assigned to argument + # Ignore passwords in tests + "S106", + # pydocstyle, flake8-docstrings + # Ignore missing docstrings in tests + "D1", +] + +[tool.ruff.lint.flake8-annotations] +# Suppress ANN401 for dynamically typed *args and **kwargs arguments. +allow-star-arg-any = true +# Allow omission of __init__ return type hint if at least one argument is annotated +#mypy-init-return = true +# Suppress violations for dummy argument variables +#suppress-dummy-args = true + +[tool.ruff.lint.flake8-bugbear] +# function calls to allow as default arguments +extend-immutable-calls = [] + +[tool.ruff.lint.flake8-builtins] +#allowed-modules = [] +#ignorelist = [] + +[lint.flake8-implicit-str-concat] +# Allow multiline implicit string concatenation +allow-multiline = true + +[tool.ruff.lint.flake8-import-conventions] +# Disallow using the `from ... import ...` syntax for individual modules +banned-from = [] + +[tool.ruff.lint.flake8-import-conventions.extend-aliases] +# To enforce an aliasing convention, follow this pattern: +#"module1" = "alias1" +#"module2" = "alias2" + +[tool.ruff.lint.flake8-import-conventions.banned-aliases] +# To disallow specific aliases, follow this pattern: +#"module1" = ["alias1", "alias2"] +#"module2" = ["alias3", "alias4"] + +[tool.ruff.lint.flake8-self] +# names to ignore when flagging private member access +extend-ignore-names = ["test_*"] + +[tool.ruff.lint.flake8-tidy-imports] +# Disallow specific module-level imports (allow only in functions and blocks) +banned-module-level-imports = [] + +[tool.ruff.lint.flake8-tidy-imports.banned-api] +# Disallow specific modules from being imported at all +"optparse".msg = "Use argparse instead of optparse" +"getopt".msg = "Use argparse instead of getopt" +"distutils".msg = "distutils has been removed from the stdlib" +"imp".msg = "imp has been removed from the stdlib" +"pycrypto".msg = "pycrypto is no longer maintained; use pyca/cryptography instead" + +[tool.ruff.lint.flake8-type-checking] +# Exempt specific modules from needing to be moved into type-checking blocks +exempt-modules = ["typing"] +# Exempt classes that list any of the enumerated classes as a base class from needing to be moved +# into type-checking blocks +runtime-evaluated-base-classes = [] +# Exempt classes and functions decorated with any of the enumerated decorators from being moved +# into type-checking blocks +runtime-evaluated-decorators = [] +# Quote type annotations if doing so would allow an import to be moved into a type-checking block +#quote-annotations = true + +[tool.ruff.lint.flake8-unused-arguments] +# Do not ignore unused *args/**kwargs +ignore-variadic-names = false + +[tool.ruff.lint.isort] +# Order imports by type +order-by-type = true +# ordering of relative imports +relative-imports-order = "closest-to-furthest" +# force specific imports to the top of their section +force-to-top = [] +# Combine aliased imports +combine-as-imports = true +# Combine aliased imports as multi-line compount imports +force-wrap-aliases = true +# modules to separate into auxiliary block(s), in the order specified +forced-separate = [] +# modules to consider as part of the stdlib +extra-standard-library = [] +# modules to consider as being third-party +known-third-party = [ + # distro_distil is first-party, but is an iRODS-independent package + "distro_distil", +] +# modules to consider as first-party +known-first-party = [] +# modules to consider as being a local folder +known-local-folder = [] +# ensure certain imports are in all files +required-imports = [] +# single-line rule exceptions +single-line-exclusions = [] +# Do not fold mult-line imports if there is a trailing comma +split-on-trailing-comma = true +# tokesn to always recognize as CONSTANTs +constants = [ + #"GSI_AUTH_SCHEME", + #"PAM_AUTH_SCHEME", + #"DEFAULT_CONFIG_PATH" +] +# tokens to always recognise as variables +variables = [] + +[tool.ruff.lint.pep8-naming] +# treat methos with these decorators as class methods +classmethod-decorators = [] +# treat methos with these decorators as static methods +staticmethod-decorators = [] +# names to ignore when considering naming violations +extend-ignore-names = [] + +[tool.ruff.lint.pycodestyle] +# Ensure this matches toll.ruff.line-length +#max-doc-length = 120 + +[tool.ruff.lint.pydocstyle] +# Adhere to PEP257 +convention = "pep257" +# Ignore docstrings for functions/methods with these fully-qualified decorators +ignore-decorators = ["typing.overload"] +# Do not ignore undocumented *args/**kwargs +ignore-var-parameters = false +# Treat methods with these decorators as properties +property-decorators = [] + +[tool.ruff.lint.pyflakes] +# functions/classes to consider generic +extend-generics = [] + +[tool.ruff.lint.pylint] +# dunder method names to allow +allow-dunder-method-names = [] +# types to ignore when flagging magic values +allow-magic-value-types = [ + "str", + "bytes", + #"complex", + #"float", + #"int", +] diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..d53f927 --- /dev/null +++ b/test/__init__.py @@ -0,0 +1 @@ +"""iRODS HTTP client test module.""" diff --git a/test/config.py b/test/config.py index 8f5ec53..9178b39 100644 --- a/test/config.py +++ b/test/config.py @@ -1,83 +1,53 @@ +"""Test configuration and settings for iRODS HTTP client tests.""" + import logging -from jsonschema import validate +from jsonschema import validate test_config = { - 'log_level': logging.INFO, - - - 'host': 'localhost', - 'port': 9001, - 'url_base': '/irods-http-api/0.3.0', - - - 'rodsadmin': { - 'username': 'rods', - 'password': 'rods' - }, - - - 'rodsuser': { - 'username': 'jeb', - 'password': 'ding' - }, - - - 'irods_zone': 'tempZone', - 'irods_server_hostname': 'localhost' + "log_level": logging.INFO, + "host": "localhost", + "port": 9001, + "url_base": "/irods-http-api/0.6.0", + "rodsadmin": {"username": "rods", "password": "rods"}, + "rodsuser": {"username": "jeb", "password": "ding"}, + "irods_zone": "tempZone", + "irods_server_hostname": "localhost", } schema = { - '$schema': 'http://json-schema.org/draft-07/schema#', - '$id': 'https://schemas.irods.org/irods-http-api/test/0.3.0/test-schema.json', - 'type': 'object', - 'properties': { - 'host': { - 'type': 'string' - }, - 'port': { - 'type': 'number' - }, - 'url_base': { - 'type': 'string' - }, - 'rodsadmin': { - '$ref': '#/definitions/login' - }, - 'rodsuser': { - '$ref': '#/definitions/login' - }, - 'irods_zone': { - 'type': 'string' - }, - 'irods_server_hostname': { - 'type': 'string' - } - }, - 'required': [ - 'host', - 'port', - 'url_base', - 'rodsadmin', - 'rodsuser', - 'irods_zone', - 'irods_server_hostname' - ], - 'definitions': { - 'login': { - 'type': 'object', - 'properties': { - 'username': { - 'type': 'string' - }, - 'password': { - 'type': 'string' - } - }, - 'required': [ 'username', 'password' ] - } - } + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://schemas.irods.org/irods-http-api/test/0.6.0/test-schema.json", + "type": "object", + "properties": { + "host": {"type": "string"}, + "port": {"type": "number"}, + "url_base": {"type": "string"}, + "rodsadmin": {"$ref": "#/definitions/login"}, + "rodsuser": {"$ref": "#/definitions/login"}, + "irods_zone": {"type": "string"}, + "irods_server_hostname": {"type": "string"}, + }, + "required": [ + "host", + "port", + "url_base", + "rodsadmin", + "rodsuser", + "irods_zone", + "irods_server_hostname", + ], + "definitions": { + "login": { + "type": "object", + "properties": { + "username": {"type": "string"}, + "password": {"type": "string"}, + }, + "required": ["username", "password"], + } + }, } diff --git a/test/test_endpoint_operations.py b/test/test_endpoint_operations.py index eba538f..377579b 100644 --- a/test/test_endpoint_operations.py +++ b/test/test_endpoint_operations.py @@ -1,1439 +1,1845 @@ -import config -import unittest -from irods_http_client.irodsHttpClient import IrodsHttpClient -import concurrent.futures -import os -import time -import logging - - -def setup_class(cls, opts): - '''Initializes shared state needed by all test cases. - - - This function is designed to be called in setUpClass(). - - - Arguments: - cls -- The class to attach state to. - opts -- A dict containing options for controlling the behavior of the function. - ''' - +""" +Integration tests for iRODS HTTP API endpoint operations. - # Used as a signal for determining whether setUpClass() succeeded or not. - # If this results in being True, no tests should be allowed to run. - cls._class_init_error = False - cls._remove_rodsuser = False - - - # Initialize the class logger. - cls.logger = logging.getLogger(cls.__name__) - - - log_level = config.test_config.get('log_level', logging.INFO) - cls.logger.setLevel(log_level) - - - ch = logging.StreamHandler() - ch.setLevel(log_level) - ch.setFormatter(logging.Formatter(f'[%(asctime)s] [{cls.__name__}] [%(levelname)s] %(message)s')) - - - cls.logger.addHandler(ch) - - - # Initialize state. - - - if config.test_config.get('host', None) == None: - cls.logger.debug('Missing configuration property: host') - cls._class_init_error = True - return +Tests cover all major operation categories: collections, data objects, +resources, rules, queries, tickets, users/groups, and zones. +""" +import concurrent.futures +import logging +import pathlib +import time +import unittest - if config.test_config.get('port', None) == None: - cls.logger.debug('Missing configuration property: port') - cls._class_init_error = True - return +import config +from irods_http_client import IRODSHTTPClient - if config.test_config.get('url_base', None) == None: - cls.logger.debug('Missing configuration property: url_base') - cls._class_init_error = True - return +def setup_class(cls, opts): + """ + Initialize shared state needed by all test cases. + + This function is designed to be called in setUpClass(). + + Args: + cls: The class to attach state to. + opts: A dict containing options for controlling the behavior of the function. + """ + # Used as a signal for determining whether setUpClass() succeeded or not. + # If this results in being True, no tests should be allowed to run. + cls._class_init_error = False + + # Initialize the class logger. + cls.logger = logging.getLogger(cls.__name__) + + log_level = config.test_config.get("log_level", logging.INFO) + cls.logger.setLevel(log_level) + + ch = logging.StreamHandler() + ch.setLevel(log_level) + ch.setFormatter(logging.Formatter(f"[%(asctime)s] [{cls.__name__}] [%(levelname)s] %(message)s")) + + cls.logger.addHandler(ch) + + # Initialize state. + + if config.test_config.get("host", None) is None: + cls.logger.debug("Missing configuration property: host") + cls._class_init_error = True + return + + if config.test_config.get("port", None) is None: + cls.logger.debug("Missing configuration property: port") + cls._class_init_error = True + return + + if config.test_config.get("url_base", None) is None: + cls.logger.debug("Missing configuration property: url_base") + cls._class_init_error = True + return + + cls.url_base = f"http://{config.test_config['host']}:{config.test_config['port']}{config.test_config['url_base']}" + cls.url_endpoint = f'{cls.url_base}/{opts["endpoint_name"]}' + + cls.api = IRODSHTTPClient(cls.url_base) + + cls.zone_name = config.test_config["irods_zone"] + cls.host = config.test_config["irods_server_hostname"] + + # create_rodsuser cannot be honored if init_rodsadmin is set to False. + # Therefore, return immediately. + if not opts.get("init_rodsadmin", True): + cls.logger.debug("init_rodsadmin is False. Class setup complete.") + return + + # Authenticate as a rodsadmin and store the bearer token. + cls.rodsadmin_username = config.test_config["rodsadmin"]["username"] + + try: + cls.rodsadmin_bearer_token = cls.api.authenticate( + cls.rodsadmin_username, config.test_config["rodsadmin"]["password"] + ) + except RuntimeError: + cls._class_init_error = True + cls.logger.debug("Failed to authenticate as rodsadmin [%].", cls.rodsadmin_username) + return + + # Authenticate as a rodsuser and store the bearer token. + cls.rodsuser_username = config.test_config["rodsuser"]["username"] + + try: + cls.api.users_groups.create_user(cls.rodsuser_username, cls.zone_name, "rodsuser") + cls.api.users_groups.set_password( + cls.rodsuser_username, + cls.zone_name, + config.test_config["rodsuser"]["password"], + ) + cls.rodsuser_bearer_token = cls.api.authenticate( + cls.rodsuser_username, config.test_config["rodsuser"]["password"] + ) + except RuntimeError: + cls._class_init_error = True + cls.logger.debug("Failed to authenticate as rodsuser [%].", cls.rodsuser_username) + return + + cls.logger.debug("Class setup complete.") - cls.url_base = f"http://{config.test_config['host']}:{config.test_config['port']}{config.test_config['url_base']}" - cls.url_endpoint = f'{cls.url_base}/{opts["endpoint_name"]}' - cls.api = IrodsHttpClient(cls.url_base) +def tear_down_class(cls): + """ + Clean up shared state after test class execution. - cls.zone_name = config.test_config['irods_zone'] - cls.host = config.test_config['irods_server_hostname'] + Removes the rodsuser created during setup. + Args: + cls: The class to clean up state from. + """ + if cls._class_init_error: + return - # create_rodsuser cannot be honored if init_rodsadmin is set to False. - # Therefore, return immediately. - if not opts.get('init_rodsadmin', True): - cls.logger.debug('init_rodsadmin is False. Class setup complete.') - return + cls.api.users_groups.remove_user(cls.rodsuser_username, cls.zone_name) - # Authenticate as a rodsadmin and store the bearer token. - cls.rodsadmin_username = config.test_config['rodsadmin']['username'] +# Tests for library +class LibraryTests(unittest.TestCase): + """Test library-level operations (info, get_token).""" - try: - cls.rodsadmin_bearer_token = cls.api.authenticate(cls.rodsadmin_username, config.test_config['rodsadmin']['password']) - except RuntimeError: - cls._class_init_error = True - cls.logger.debug(f'Failed to authenticate as rodsadmin [{cls.rodsadmin_username}].') - return - - # Authenticate as a rodsuser and store the bearer token. - # Should be replaced once user operations are implemented - # Currently, the user specified in the config file must exist before running tests - cls.rodsuser_username = config.test_config['rodsuser']['username'] - print(cls.rodsuser_username) - print(config.test_config['rodsuser']['password']) - try: - cls.rodsuser_bearer_token = cls.api.authenticate(cls.rodsuser_username, config.test_config['rodsuser']['password']) - except RuntimeError: - cls._class_init_error = True - cls.logger.debug(f'Failed to authenticate as rodsuser [{cls.rodsuser_username}].') - return + @classmethod + def setUpClass(cls): + """Set up class-level resources for library tests.""" + setup_class(cls, {"endpoint_name": "collections"}) - cls.logger.debug('Class setup complete.') + @classmethod + def tearDownClass(cls): + """Tear down class-level resources.""" + tear_down_class(cls) + def setUp(self): + """Check that class initialization succeeded before each test.""" + self.assertFalse(self._class_init_error, "Class initialization failed. Cannot continue.") -def tear_down_class(cls): - if cls._class_init_error: - return + # tests the info operation + def test_info(self): + """Test the info operation to retrieve server information.""" + self.api.info() - if not cls._remove_rodsuser: - return + # tests the getToken operation + def test_get_token(self): + """Test the get_token operation to retrieve the current authentication token.""" + self.api.get_token() # Tests for collections operations -class collectionsTests(unittest.TestCase): - @classmethod - def setUpClass(cls): - setup_class(cls, {'endpoint_name': 'collections'}) - - @classmethod - def tearDownClass(cls): - tear_down_class(cls) - - def setUp(self): - self.assertFalse(self._class_init_error, 'Class initialization failed. Cannot continue.') - - #tests the create operation - def testCreate(self): - print(self.zone_name) - self.api.setToken(self.rodsadmin_bearer_token) - - #clean up test collections - self.api.collections.remove(f'/{self.zone_name}/home/new') - self.api.collections.remove(f'/{self.zone_name}/home/test/folder') - self.api.collections.remove(f'/{self.zone_name}/home/test') - - #test param checking - self.assertRaises(TypeError, self.api.collections.create, 0, 0) - self.assertRaises(TypeError, self.api.collections.create, f'/{self.zone_name}/home/{self.rodsadmin_username}', '0') - self.assertRaises(ValueError, self.api.collections.create, f'/{self.zone_name}/home/{self.rodsadmin_username}', 7) - - #test creating new collection - response = self.api.collections.create(f'/{self.zone_name}/home/new') - self.assertTrue(response['data']['created']) - self.assertEqual(response['data']['irods_response']['status_code'], 0) - - #test creating existing collection - response = self.api.collections.create(f'/{self.zone_name}/home/new') - self.assertFalse(response['data']['created']) - self.assertEqual(response['data']['irods_response']['status_code'], 0) - - #test invalid path - response = self.api.collections.create('{self.zone_name}/home/new') - self.assertEqual('{\'irods_response\': {\'status_code\': -358000, \'status_message\': \'path does not exist: OBJ_PATH_DOES_NOT_EXIST\'}}', str(response['data'])) - - #test create_intermediates - response = self.api.collections.create(f'/{self.zone_name}/home/test/folder', 0) - self.assertEqual('{\'irods_response\': {\'status_code\': -358000, \'status_message\': \'path does not exist: OBJ_PATH_DOES_NOT_EXIST\'}}', str(response['data'])) - response = self.api.collections.create(f'/{self.zone_name}/home/test/folder', 1) - self.assertEqual('{\'created\': True, \'irods_response\': {\'status_code\': 0}}', str(response['data'])) - - - #tests the remove operation - def testRemove(self): - self.api.setToken(self.rodsadmin_bearer_token) - - #clean up test collections - self.api.collections.remove(f'/{self.zone_name}/home/new') - self.api.collections.remove(f'/{self.zone_name}/home/test/folder') - self.api.collections.remove(f'/{self.zone_name}/home/test') - - #test param checking - self.assertRaises(TypeError, self.api.collections.remove, 0, 0, 0) - self.assertRaises(TypeError, self.api.collections.remove, f'/{self.zone_name}/home/{self.rodsadmin_username}', '0', 0) - self.assertRaises(ValueError, self.api.collections.remove, f'/{self.zone_name}/home/{self.rodsadmin_username}', 5, 0) - self.assertRaises(TypeError, self.api.collections.remove, f'/{self.zone_name}/home/{self.rodsadmin_username}', 0, '0') - self.assertRaises(ValueError, self.api.collections.remove, f'/{self.zone_name}/home/{self.rodsadmin_username}', 0, 5) - - #test removing collection - response = self.api.collections.create(f'/{self.zone_name}/home/new') - self.assertEqual('{\'created\': True, \'irods_response\': {\'status_code\': 0}}', str(response['data'])) - response = self.api.collections.remove(f'/{self.zone_name}/home/new') - self.assertEqual('{\'irods_response\': {\'status_code\': 0}}', str(response['data'])) - #test invalid paths - response = self.api.collections.stat(f'/{self.zone_name}/home/tensaitekinaaidorusama') - self.assertEqual('{\'irods_response\': {\'status_code\': -170000}}', str(response['data'])) - response = self.api.collections.stat(f'/{self.zone_name}/home/aremonainainaikoremonainainai') - self.assertEqual('{\'irods_response\': {\'status_code\': -170000}}', str(response['data'])) - response = self.api.collections.stat(f'/{self.zone_name}/home/binglebangledingledangle') - self.assertEqual('{\'irods_response\': {\'status_code\': -170000}}', str(response['data'])) - response = self.api.collections.stat(f'{self.zone_name}/home/{self.rodsadmin_username}') - self.assertEqual('{\'irods_response\': {\'status_code\': -170000}}', str(response['data'])) - - #test recurse - response = self.api.collections.create(f'/{self.zone_name}/home/test/folder', 1) - self.assertEqual('{\'created\': True, \'irods_response\': {\'status_code\': 0}}', str(response['data'])) - response = self.api.collections.remove(f'/{self.zone_name}/home/test', 0) - self.assertEqual('{\'irods_response\': {\'status_code\': -79000, \'status_message\': \'cannot remove non-empty collection: SYS_COLLECTION_NOT_EMPTY\'}}', str(response['data'])) - response = self.api.collections.remove(f'/{self.zone_name}/home/test', 1) - self.assertEqual('{\'irods_response\': {\'status_code\': 0}}', str(response['data'])) - - - #tests the stat operation - def testStat(self): - self.api.setToken(self.rodsadmin_bearer_token) - - #clean up test collections - self.api.collections.remove(f'/{self.zone_name}/home/new') - - #test param checking - self.assertRaises(TypeError, self.api.collections.stat, 0, 'ticket') - self.assertRaises(TypeError, self.api.collections.stat, f'/{self.zone_name}/home/{self.rodsadmin_username}', 0) - - #test invalid paths - response = self.api.collections.stat(f'/{self.zone_name}/home/new') - self.assertEqual('{\'irods_response\': {\'status_code\': -170000}}', str(response['data'])) - response = self.api.collections.stat('{self.zone_name}/home/new') - self.assertEqual('{\'irods_response\': {\'status_code\': -170000}}', str(response['data'])) - - #test valid path - response = self.api.collections.stat(f'/{self.zone_name}/home/{self.rodsadmin_username}') - self.assertTrue(response['data']['permissions']) - - - #tests the list operation - def testList(self): - self.api.setToken(self.rodsadmin_bearer_token) - - #clean up test collections - self.api.collections.remove(f'/{self.zone_name}/home/{self.rodsadmin_username}/croatia/zagreb') - self.api.collections.remove(f'/{self.zone_name}/home/{self.rodsadmin_username}/albania') - self.api.collections.remove(f'/{self.zone_name}/home/{self.rodsadmin_username}/bosnia') - self.api.collections.remove(f'/{self.zone_name}/home/{self.rodsadmin_username}/croatia') - - #test param checking - self.assertRaises(TypeError, self.api.collections.list, 0, 'ticket') - self.assertRaises(TypeError, self.api.collections.list, f'/{self.zone_name}/home/{self.rodsadmin_username}', '0', 'ticket') - self.assertRaises(ValueError, self.api.collections.list, f'/{self.zone_name}/home/{self.rodsadmin_username}', 5, 'ticket') - self.assertRaises(TypeError, self.api.collections.list, f'/{self.zone_name}/home/{self.rodsadmin_username}', 0, 0) - - #test empty collection - response = self.api.collections.list(f'/{self.zone_name}/home/{self.rodsadmin_username}') - self.assertEqual('None', str(response['data']['entries'])) - - #test collection with one item - self.api.collections.create(f'/{self.zone_name}/home/{self.rodsadmin_username}/bosnia') - response = self.api.collections.list(f'/{self.zone_name}/home/{self.rodsadmin_username}') - self.assertEqual(f'/{self.zone_name}/home/{self.rodsadmin_username}/bosnia', str(response['data']['entries'][0])) - - #test collection with multiple items - self.api.collections.create(f'/{self.zone_name}/home/{self.rodsadmin_username}/albania') - self.api.collections.create(f'/{self.zone_name}/home/{self.rodsadmin_username}/croatia') - response = self.api.collections.list(f'/{self.zone_name}/home/{self.rodsadmin_username}') - self.assertEqual(f'/{self.zone_name}/home/{self.rodsadmin_username}/albania', str(response['data']['entries'][0])) - self.assertEqual(f'/{self.zone_name}/home/{self.rodsadmin_username}/bosnia', str(response['data']['entries'][1])) - self.assertEqual(f'/{self.zone_name}/home/{self.rodsadmin_username}/croatia', str(response['data']['entries'][2])) - - #test without recursion - self.api.collections.create(f'/{self.zone_name}/home/{self.rodsadmin_username}/croatia/zagreb') - response = self.api.collections.list(f'/{self.zone_name}/home/{self.rodsadmin_username}') - self.assertEqual(f'/{self.zone_name}/home/{self.rodsadmin_username}/albania', str(response['data']['entries'][0])) - self.assertEqual(f'/{self.zone_name}/home/{self.rodsadmin_username}/bosnia', str(response['data']['entries'][1])) - self.assertEqual(f'/{self.zone_name}/home/{self.rodsadmin_username}/croatia', str(response['data']['entries'][2])) - self.assertEqual(len(response['data']['entries']), 3) - - #test with recursion - response = self.api.collections.list(f'/{self.zone_name}/home/{self.rodsadmin_username}', 1) - self.assertEqual(f'/{self.zone_name}/home/{self.rodsadmin_username}/albania', str(response['data']['entries'][0])) - self.assertEqual(f'/{self.zone_name}/home/{self.rodsadmin_username}/bosnia', str(response['data']['entries'][1])) - self.assertEqual(f'/{self.zone_name}/home/{self.rodsadmin_username}/croatia', str(response['data']['entries'][2])) - self.assertEqual(f'/{self.zone_name}/home/{self.rodsadmin_username}/croatia/zagreb', str(response['data']['entries'][3])) - - - #tests the set permission operation - def testSetPermission(self): - self.api.setToken(self.rodsadmin_bearer_token) - - #test param checking - self.assertRaises(TypeError, self.api.collections.set_permission, 0, 'jeb', 'read', 0) - self.assertRaises(TypeError, self.api.collections.set_permission, f'/{self.zone_name}/home/{self.rodsadmin_username}', 0, 'read', 0) - self.assertRaises(TypeError, self.api.collections.set_permission, f'/{self.zone_name}/home/{self.rodsadmin_username}', 'jeb', 0, 0) - self.assertRaises(TypeError, self.api.collections.set_permission, f'/{self.zone_name}/home/{self.rodsadmin_username}', 'jeb', 'read', '0') - self.assertRaises(ValueError, self.api.collections.set_permission, f'/{self.zone_name}/home/{self.rodsadmin_username}', 'jeb', 'read', 5) - - #create new collection - response = self.api.collections.create(f'/{self.zone_name}/home/setPerms') - self.assertEqual(response['data']['irods_response']['status_code'], 0) - - #test no permission - self.api.setToken(self.rodsuser_bearer_token) - response = self.api.collections.stat(f'/{self.zone_name}/home/setPerms') - self.assertEqual(response['data']['irods_response']['status_code'], -170000) - - #test set permission - self.api.setToken(self.rodsadmin_bearer_token) - response = self.api.collections.set_permission(f'/{self.zone_name}/home/setPerms', self.rodsuser_username, 'read') - self.assertEqual('{\'irods_response\': {\'status_code\': 0}}', str(response['data'])) - - #test with permission - self.api.setToken(self.rodsuser_bearer_token) - response = self.api.collections.stat(f'/{self.zone_name}/home/setPerms') - self.assertTrue(response['data']['permissions']) - - #test set permission null - self.api.setToken(self.rodsadmin_bearer_token) - response = self.api.collections.set_permission(f'/{self.zone_name}/home/setPerms', self.rodsuser_username, 'null') - self.assertEqual('{\'irods_response\': {\'status_code\': 0}}', str(response['data'])) - - #test no permission - self.api.setToken(self.rodsuser_bearer_token) - response = self.api.collections.stat(f'/{self.zone_name}/home/setPerms') - self.assertEqual('{\'irods_response\': {\'status_code\': -170000}}', str(response['data'])) - - #remove the collection - self.api.setToken(self.rodsadmin_bearer_token) - response = self.api.collections.remove(f'/{self.zone_name}/home/setPerms', 1, 1) - self.assertEqual(response['data']['irods_response']['status_code'], 0) - - - #tests the set inheritance operation - def testSetInheritance(self): - self.api.setToken(self.rodsadmin_bearer_token) - - #test param checking - self.assertRaises(TypeError, self.api.collections.set_inheritance, 0, 0, 0) - self.assertRaises(TypeError, self.api.collections.set_inheritance, f'/{self.zone_name}/home/{self.rodsadmin_username}', '0', 0) - self.assertRaises(ValueError, self.api.collections.set_inheritance, f'/{self.zone_name}/home/{self.rodsadmin_username}', 5, 0) - self.assertRaises(TypeError, self.api.collections.set_inheritance, f'/{self.zone_name}/home/{self.rodsadmin_username}', 0, '0') - self.assertRaises(ValueError, self.api.collections.set_inheritance, f'/{self.zone_name}/home/{self.rodsadmin_username}', 0, 5) - - #control - response = self.api.collections.stat(f'/{self.zone_name}/home/{self.rodsadmin_username}') - self.assertFalse(response['data']['inheritance_enabled']) - - #test enabling inheritance - response = self.api.collections.set_inheritance(f'/{self.zone_name}/home/{self.rodsadmin_username}', 1) - self.assertEqual('{\'irods_response\': {\'status_code\': 0}}', str(response['data'])) - - #check if changed - response = self.api.collections.stat(f'/{self.zone_name}/home/{self.rodsadmin_username}') - self.assertTrue(response['data']['inheritance_enabled']) - - #test disabling inheritance - response = self.api.collections.set_inheritance(f'/{self.zone_name}/home/{self.rodsadmin_username}', 0) - self.assertEqual('{\'irods_response\': {\'status_code\': 0}}', str(response['data'])) - - #check if changed - response = self.api.collections.stat(f'/{self.zone_name}/home/{self.rodsadmin_username}') - self.assertFalse(response['data']['inheritance_enabled']) - - - #test the modify permissions operation - def testModifyPermissions(self): - self.api.setToken(self.rodsadmin_bearer_token) - - ops_permissions = [ - { - 'entity_name': self.rodsuser_username, - 'acl': 'read' - } - ] - - ops_permissions_null = [ - { - 'entity_name': self.rodsuser_username, - 'acl': 'null' - } - ] - - #test param checking - self.assertRaises(TypeError, self.api.collections.modify_permissions, 0, ops_permissions, 0) - self.assertRaises(TypeError, self.api.collections.modify_permissions, f'/{self.zone_name}/home/{self.rodsadmin_username}', 5, 0) - self.assertRaises(TypeError, self.api.collections.modify_permissions, f'/{self.zone_name}/home/{self.rodsadmin_username}', ops_permissions, '0') - self.assertRaises(ValueError, self.api.collections.modify_permissions, f'/{self.zone_name}/home/{self.rodsadmin_username}', ops_permissions, 5) - - #create new collection - response = self.api.collections.create(f'/{self.zone_name}/home/modPerms') - self.assertEqual(response['data']['irods_response']['status_code'], 0) - - #test no permissions - self.api.setToken(self.rodsuser_bearer_token) - response = self.api.collections.stat(f'/{self.zone_name}/home/modPerms') - self.assertEqual('{\'irods_response\': {\'status_code\': -170000}}', str(response['data'])) - - #test set permissions - self.api.setToken(self.rodsadmin_bearer_token) - response = self.api.collections.modify_permissions(f'/{self.zone_name}/home/modPerms', ops_permissions) - self.assertEqual(response['data']['irods_response']['status_code'], 0) - - #test with permissions - self.api.setToken(self.rodsuser_bearer_token) - response = self.api.collections.stat(f'/{self.zone_name}/home/modPerms') - self.assertTrue(response['data']['permissions']) - - #test set permissions nuil - self.api.setToken(self.rodsadmin_bearer_token) - response = self.api.collections.modify_permissions(f'/{self.zone_name}/home/modPerms', ops_permissions_null) - self.assertEqual(response['data']['irods_response']['status_code'], 0) - - #test without permissions - self.api.setToken(self.rodsuser_bearer_token) - response = self.api.collections.stat(f'/{self.zone_name}/home/modPerms') - self.assertEqual('{\'irods_response\': {\'status_code\': -170000}}', str(response['data'])) - - #remove the collection - self.api.setToken(self.rodsadmin_bearer_token) - response = self.api.collections.remove(f'/{self.zone_name}/home/modPerms', 1, 1) - self.assertEqual(response['data']['irods_response']['status_code'], 0) - - - #test the modify metadata operation - def testModifyMetadata(self): - self.api.setToken(self.rodsadmin_bearer_token) - - ops_metadata = [ - { - 'operation': 'add', - 'attribute': 'eyeballs', - 'value': 'itchy' - } - ] - - ops_metadata_remove = [ - { - 'operation': 'remove', - 'attribute': 'eyeballs', - 'value': 'itchy' - } - ] - - #test param checking - self.assertRaises(TypeError, self.api.collections.modify_metadata, 0, ops_metadata, 0) - self.assertRaises(TypeError, self.api.collections.modify_metadata, f'/{self.zone_name}/home/{self.rodsadmin_username}', 5, 0) - self.assertRaises(TypeError, self.api.collections.modify_metadata, f'/{self.zone_name}/home/{self.rodsadmin_username}', ops_metadata, '0') - self.assertRaises(ValueError, self.api.collections.modify_metadata, f'/{self.zone_name}/home/{self.rodsadmin_username}', ops_metadata, 5) - - #test adding and removing metadata - response = self.api.collections.modify_metadata(f'/{self.zone_name}/home/{self.rodsadmin_username}', ops_metadata) - self.assertEqual(response['data']['irods_response']['status_code'], 0) - response = self.api.collections.modify_metadata(f'/{self.zone_name}/home/{self.rodsadmin_username}', ops_metadata_remove) - self.assertEqual(response['data']['irods_response']['status_code'], 0) - - - #tests the rename operation - def testRename(self): - self.api.setToken(self.rodsadmin_bearer_token) - - #test param checking - self.assertRaises(TypeError, self.api.collections.rename, f'/{self.zone_name}/home/{self.rodsadmin_username}', 0) - self.assertRaises(TypeError, self.api.collections.rename, 0, f'/{self.zone_name}/home/pods') - - #test before move - response = self.api.collections.stat(f'/{self.zone_name}/home/pods') - self.assertEqual('{\'irods_response\': {\'status_code\': -170000}}', str(response['data'])) - response = self.api.collections.stat(f'/{self.zone_name}/home/{self.rodsadmin_username}') - self.assertTrue(response['data']['permissions']) - - #test renaming - response = self.api.collections.rename(f'/{self.zone_name}/home/{self.rodsadmin_username}', f'/{self.zone_name}/home/pods') - self.assertEqual('{\'irods_response\': {\'status_code\': 0}}', str(response['data'])) - - #test before move - response = self.api.collections.stat(f'/{self.zone_name}/home/{self.rodsadmin_username}') - self.assertEqual('{\'irods_response\': {\'status_code\': -170000}}', str(response['data'])) - response = self.api.collections.stat(f'/{self.zone_name}/home/pods') - self.assertTrue(response['data']['permissions']) - - #test renaming - response = self.api.collections.rename(f'/{self.zone_name}/home/pods', f'/{self.zone_name}/home/{self.rodsadmin_username}') - self.assertEqual('{\'irods_response\': {\'status_code\': 0}}', str(response['data'])) - - - #tests the touch operation - def testTouch(self): - self.api.setToken(self.rodsadmin_bearer_token) - - self.assertTrue(True) - - -# Tests for data object operations -class dataObjectsTests(unittest.TestCase): - - @classmethod - def setUpClass(cls): - setup_class(cls, {'endpoint_name': 'data_objects'}) - - @classmethod - def tearDownClass(cls): - tear_down_class(cls) - - def setUp(self): - self.assertFalse(self._class_init_error, 'Class initialization failed. Cannot continue.') - - def testCommonOperations(self): - self.api.setToken(self.rodsadmin_bearer_token) - print(self.rodsadmin_bearer_token) - - try: - # Create a unixfilesystem resource. - r = self.api.resources.create('resource', 'unixfilesystem', self.host, '/tmp/resource', '') - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - self.api.setToken(self.rodsuser_bearer_token) - # Create a non-empty data object - print(self.api.collections.stat(f'/{self.zone_name}/home/{self.rodsuser_username}')) - r = self.api.data_objects.write('These are the bytes being written to the object', f'/{self.zone_name}/home/{self.rodsuser_username}/file.txt', 'resource') - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - # Replicate the data object - r = self.api.data_objects.replicate(f'/{self.zone_name}/home/{self.rodsuser_username}/file.txt', dst_resource='resource') - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - # Show that there are two replicas - # TODO: Implement once query operations are completed - - # Trim the first data object - r = self.api.data_objects.trim(f'/{self.zone_name}/home/{self.rodsuser_username}/file.txt', 0) - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - # Rename the data object - r = self.api.data_objects.rename(f'/{self.zone_name}/home/{self.rodsuser_username}/file.txt', f'/{self.zone_name}/home/{self.rodsuser_username}/newfile.txt') - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - # Copy the data object - r = self.api.data_objects.copy(f'/{self.zone_name}/home/{self.rodsuser_username}/newfile.txt', f'/{self.zone_name}/home/{self.rodsuser_username}/anotherfile.txt') - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - # Set permission on the object - r = self.api.data_objects.set_permission(f'/{self.zone_name}/home/{self.rodsuser_username}/anotherfile.txt', 'rods', 'read') - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - # Confirm that the permission has been set - r = self.api.data_objects.stat(f'/{self.zone_name}/home/{self.rodsuser_username}/anotherfile.txt') - self.assertEqual(r['data']['irods_response']['status_code'], 0) - self.assertIn({ - 'name': 'rods', - 'zone': self.zone_name, - 'type': 'rodsadmin', - 'perm': 'read_object' - }, r['data']['permissions']) - - finally: - # Remove the data objects - r = self.api.data_objects.remove(f'/{self.zone_name}/home/{self.rodsuser_username}/anotherfile.txt', 0, 1) - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - r = self.api.data_objects.remove(f'/{self.zone_name}/home/{self.rodsuser_username}/newfile.txt', 0, 1) - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - self.api.setToken(self.rodsadmin_bearer_token) - - # Remove the resource - r = self.api.resources.remove('resource') - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - - def testChecksums(self): - self.api.setToken(self.rodsadmin_bearer_token) - - # Create a unixfilesystem resource. - r = self.api.resources.create('newresource', 'unixfilesystem', self.host, '/tmp/newresource', '') - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - # Create a non-empty data object - r = self.api.data_objects.write('These are the bytes being written to the object', f'/{self.zone_name}/home/{self.rodsadmin_username}/file.txt', 'newresource') - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - # Replicate the data object - r = self.api.data_objects.replicate(f'/{self.zone_name}/home/{self.rodsadmin_username}/file.txt', dst_resource='newresource') - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - # Show that there are two replicas - # TODO: Implement once query operations are completed - - try: - # Calculate a checksum for the first replica - r = self.api.data_objects.calculate_checksum(f'/{self.zone_name}/home/{self.rodsadmin_username}/file.txt', replica_number=0) - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - # Verify checksum information across all replicas. - r = self.api.data_objects.verify_checksum(f'/{self.zone_name}/home/{self.rodsadmin_username}/file.txt') - print(r) - self.assertEqual(r['data']['irods_response']['status_code'], 0) - finally: - # Remove the data objects - r = self.api.data_objects.remove(f'/{self.zone_name}/home/{self.rodsadmin_username}/file.txt', 0, 1) - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - # Remove the resource - r = self.api.resources.remove('newresource') - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - - def testTouch(self): - self.api.setToken(self.rodsadmin_bearer_token) - - # Test touching non existant data object with no_create - r = self.api.data_objects.touch(f'/{self.zone_name}/home/{self.rodsadmin_username}/new.txt', 1) - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - # Show that the object has not been created - r = self.api.data_objects.stat(f'/{self.zone_name}/home/{self.rodsadmin_username}/new.txt') - self.assertEqual(r['data']['irods_response']['status_code'], -171000) - - # Test touching non existant object without no_create - r = self.api.data_objects.touch(f'/{self.zone_name}/home/{self.rodsadmin_username}/new.txt', 0) - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - # Show that the object has been created - r = self.api.data_objects.stat(f'/{self.zone_name}/home/{self.rodsadmin_username}/new.txt') - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - # Test touching existing object without no_create - r = self.api.data_objects.touch(f'/{self.zone_name}/home/{self.rodsadmin_username}/new.txt', 1) - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - # Remove the object - r = self.api.data_objects.remove(f'/{self.zone_name}/home/{self.rodsadmin_username}/new.txt') - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - - def testRegister(self): - self.api.setToken(self.rodsadmin_bearer_token) - - # Create a non-empty local file. - content = 'data' - with open('/tmp/register-demo.txt', 'w') as f: - f.write(content) - - # Show the data object we want to create via registration does not exist. - r = self.api.data_objects.stat(f'/{self.zone_name}/home/{self.rodsadmin_username}/register-demo.txt') - self.assertEqual(r['data']['irods_response']['status_code'], -171000) - - try: - # Create a unixfilesystem resource. - r = self.api.resources.create('register_resource', 'unixfilesystem', self.host, '/tmp/register_resource', '') - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - # Register the local file into the catalog as a new data object. - # We know we're registering a new data object because the "as-additional-replica" - # parameter isn't set to 1. - r = self.api.data_objects.register(f'/{self.zone_name}/home/{self.rodsadmin_username}/register-demo.txt', '/tmp/register-demo.txt', 'register_resource', data_size=len(content)) - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - # Show a new data object exists with the expected replica information. - # TODO: add when query operations are implemented - finally: - # Unregisterr the dataq object - r = self.api.data_objects.remove(f'/{self.zone_name}/home/{self.rodsadmin_username}/register-demo.txt', 1) - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - # Remove the resource - r = self.api.resources.remove('register_resource') - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - - def testParallelWrite(self): - self.api.setToken(self.rodsadmin_bearer_token) - self.api.data_objects.remove(f'/{self.zone_name}/home/{self.rodsadmin_username}/parallel-write.txt', 0, 1) - - # Open parallel write - r = self.api.data_objects.parallel_write_init(f'/{self.zone_name}/home/{self.rodsadmin_username}/parallel-write.txt', 3) - self.assertEqual(r['data']['irods_response']['status_code'], 0) - handle = r['data']['parallel_write_handle'] - - try: - # Write to the data object using the parallel write handle. - futures = [] - with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor: - for e in enumerate(['A', 'B', 'C']): - count = 10 - futures.append(executor.submit( - self.api.data_objects.write, bytes=e[1] * count, offset=e[0] * count, stream_index=e[0], parallel_write_handle=handle - )) - for f in concurrent.futures.as_completed(futures): - r = f.result() - self.assertEqual(r['data']['irods_response']['status_code'], 0) - finally: - # Close parallel write - r = self.api.data_objects.parallel_write_shutdown(handle) - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - # Remove the object - r = self.api.data_objects.remove(f'/{self.zone_name}/home/{self.rodsadmin_username}/parallel-write.txt', 0, 1) - self.assertEqual(r['data']['irods_response']['status_code'], 0) +class CollectionsTests(unittest.TestCase): + """Test iRODS collection operations.""" + + @classmethod + def setUpClass(cls): + """Set up class-level resources for collection tests.""" + setup_class(cls, {"endpoint_name": "collections"}) + + @classmethod + def tearDownClass(cls): + """Tear down class-level resources.""" + tear_down_class(cls) + + def setUp(self): + """Check that class initialization succeeded before each test.""" + self.assertFalse(self._class_init_error, "Class initialization failed. Cannot continue.") + + # tests the create operation + def test_create(self): + """Test collection creation operations and parameter validation.""" + self.api.set_token(self.rodsadmin_bearer_token) + + # clean up test collections + self.api.collections.remove(f"/{self.zone_name}/home/new") + self.api.collections.remove(f"/{self.zone_name}/home/test/folder") + self.api.collections.remove(f"/{self.zone_name}/home/test") + + # test param checking + self.assertRaises(TypeError, self.api.collections.create, 0, 0) + self.assertRaises( + TypeError, + self.api.collections.create, + f"/{self.zone_name}/home/{self.rodsadmin_username}", + "0", + ) + self.assertRaises( + ValueError, + self.api.collections.create, + f"/{self.zone_name}/home/{self.rodsadmin_username}", + 7, + ) + + # test creating new collection + response = self.api.collections.create(f"/{self.zone_name}/home/new") + self.assertTrue(response["data"]["created"]) + self.assertEqual(response["data"]["irods_response"]["status_code"], 0) + + # test creating existing collection + response = self.api.collections.create(f"/{self.zone_name}/home/new") + self.assertFalse(response["data"]["created"]) + self.assertEqual(response["data"]["irods_response"]["status_code"], 0) + + # test invalid path + response = self.api.collections.create(f"{self.zone_name}/home/new") + self.assertEqual( + "{'irods_response': {'status_code': -358000, " + "'status_message': 'path does not exist: OBJ_PATH_DOES_NOT_EXIST'}}", + str(response["data"]), + ) + + # test create_intermediates + response = self.api.collections.create(f"/{self.zone_name}/home/test/folder", 0) + self.assertEqual( + "{'irods_response': {'status_code': -358000, " + "'status_message': 'path does not exist: OBJ_PATH_DOES_NOT_EXIST'}}", + str(response["data"]), + ) + response = self.api.collections.create(f"/{self.zone_name}/home/test/folder", 1) + self.assertEqual( + "{'created': True, 'irods_response': {'status_code': 0}}", + str(response["data"]), + ) + + # tests the remove operation + def test_remove(self): + """Test collection removal operations and parameter validation.""" + self.api.set_token(self.rodsadmin_bearer_token) + + # clean up test collections + self.api.collections.remove(f"/{self.zone_name}/home/new") + self.api.collections.remove(f"/{self.zone_name}/home/test/folder") + self.api.collections.remove(f"/{self.zone_name}/home/test") + + # test param checking + self.assertRaises(TypeError, self.api.collections.remove, 0, 0, 0) + self.assertRaises( + TypeError, + self.api.collections.remove, + f"/{self.zone_name}/home/{self.rodsadmin_username}", + "0", + 0, + ) + self.assertRaises( + ValueError, + self.api.collections.remove, + f"/{self.zone_name}/home/{self.rodsadmin_username}", + 5, + 0, + ) + self.assertRaises( + TypeError, + self.api.collections.remove, + f"/{self.zone_name}/home/{self.rodsadmin_username}", + 0, + "0", + ) + self.assertRaises( + ValueError, + self.api.collections.remove, + f"/{self.zone_name}/home/{self.rodsadmin_username}", + 0, + 5, + ) + + # test removing collection + response = self.api.collections.create(f"/{self.zone_name}/home/new") + self.assertEqual( + "{'created': True, 'irods_response': {'status_code': 0}}", + str(response["data"]), + ) + response = self.api.collections.remove(f"/{self.zone_name}/home/new") + self.assertEqual("{'irods_response': {'status_code': 0}}", str(response["data"])) + # test invalid paths + response = self.api.collections.stat(f"/{self.zone_name}/home/tensaitekinaaidorusama") + self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) + response = self.api.collections.stat(f"/{self.zone_name}/home/aremonainainaikoremonainainai") + self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) + response = self.api.collections.stat(f"/{self.zone_name}/home/binglebangledingledangle") + self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) + response = self.api.collections.stat(f"{self.zone_name}/home/{self.rodsadmin_username}") + self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) + + # test recurse + response = self.api.collections.create(f"/{self.zone_name}/home/test/folder", 1) + self.assertEqual( + "{'created': True, 'irods_response': {'status_code': 0}}", + str(response["data"]), + ) + response = self.api.collections.remove(f"/{self.zone_name}/home/test", 0) + self.assertEqual( + "{'irods_response': {'status_code': -79000, " + "'status_message': 'cannot remove non-empty collection: " + "SYS_COLLECTION_NOT_EMPTY'}}", + str(response["data"]), + ) + response = self.api.collections.remove(f"/{self.zone_name}/home/test", 1) + self.assertEqual("{'irods_response': {'status_code': 0}}", str(response["data"])) + + # tests the stat operation + def test_stat(self): + """Test collection stat operation to retrieve metadata.""" + self.api.set_token(self.rodsadmin_bearer_token) + + # clean up test collections + self.api.collections.remove(f"/{self.zone_name}/home/new") + + # test param checking + self.assertRaises(TypeError, self.api.collections.stat, 0, "ticket") + self.assertRaises( + TypeError, + self.api.collections.stat, + f"/{self.zone_name}/home/{self.rodsadmin_username}", + 0, + ) + + # test invalid paths + response = self.api.collections.stat(f"/{self.zone_name}/home/new") + self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) + response = self.api.collections.stat(f"{self.zone_name}/home/new") + self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) + + # test valid path + response = self.api.collections.stat(f"/{self.zone_name}/home/{self.rodsadmin_username}") + self.assertTrue(response["data"]["permissions"]) + + # tests the list operation + def test_list(self): + """Test collection list operation to enumerate contents.""" + self.api.set_token(self.rodsadmin_bearer_token) + + # clean up test collections + self.api.collections.remove(f"/{self.zone_name}/home/{self.rodsadmin_username}/croatia/zagreb") + self.api.collections.remove(f"/{self.zone_name}/home/{self.rodsadmin_username}/albania") + self.api.collections.remove(f"/{self.zone_name}/home/{self.rodsadmin_username}/bosnia") + self.api.collections.remove(f"/{self.zone_name}/home/{self.rodsadmin_username}/croatia") + + # test param checking + self.assertRaises(TypeError, self.api.collections.list, 0, "ticket") + self.assertRaises( + TypeError, + self.api.collections.list, + f"/{self.zone_name}/home/{self.rodsadmin_username}", + "0", + "ticket", + ) + self.assertRaises( + ValueError, + self.api.collections.list, + f"/{self.zone_name}/home/{self.rodsadmin_username}", + 5, + "ticket", + ) + self.assertRaises( + TypeError, + self.api.collections.list, + f"/{self.zone_name}/home/{self.rodsadmin_username}", + 0, + 0, + ) + + # test empty collection + response = self.api.collections.list(f"/{self.zone_name}/home/{self.rodsadmin_username}") + self.assertEqual("None", str(response["data"]["entries"])) + + # test collection with one item + self.api.collections.create(f"/{self.zone_name}/home/{self.rodsadmin_username}/bosnia") + response = self.api.collections.list(f"/{self.zone_name}/home/{self.rodsadmin_username}") + self.assertEqual( + f"/{self.zone_name}/home/{self.rodsadmin_username}/bosnia", + str(response["data"]["entries"][0]), + ) + + # test collection with multiple items + self.api.collections.create(f"/{self.zone_name}/home/{self.rodsadmin_username}/albania") + self.api.collections.create(f"/{self.zone_name}/home/{self.rodsadmin_username}/croatia") + response = self.api.collections.list(f"/{self.zone_name}/home/{self.rodsadmin_username}") + self.assertEqual( + f"/{self.zone_name}/home/{self.rodsadmin_username}/albania", + str(response["data"]["entries"][0]), + ) + self.assertEqual( + f"/{self.zone_name}/home/{self.rodsadmin_username}/bosnia", + str(response["data"]["entries"][1]), + ) + self.assertEqual( + f"/{self.zone_name}/home/{self.rodsadmin_username}/croatia", + str(response["data"]["entries"][2]), + ) + + # test without recursion + self.api.collections.create(f"/{self.zone_name}/home/{self.rodsadmin_username}/croatia/zagreb") + response = self.api.collections.list(f"/{self.zone_name}/home/{self.rodsadmin_username}") + self.assertEqual( + f"/{self.zone_name}/home/{self.rodsadmin_username}/albania", + str(response["data"]["entries"][0]), + ) + self.assertEqual( + f"/{self.zone_name}/home/{self.rodsadmin_username}/bosnia", + str(response["data"]["entries"][1]), + ) + self.assertEqual( + f"/{self.zone_name}/home/{self.rodsadmin_username}/croatia", + str(response["data"]["entries"][2]), + ) + self.assertEqual(len(response["data"]["entries"]), 3) + + # test with recursion + response = self.api.collections.list(f"/{self.zone_name}/home/{self.rodsadmin_username}", 1) + self.assertEqual( + f"/{self.zone_name}/home/{self.rodsadmin_username}/albania", + str(response["data"]["entries"][0]), + ) + self.assertEqual( + f"/{self.zone_name}/home/{self.rodsadmin_username}/bosnia", + str(response["data"]["entries"][1]), + ) + self.assertEqual( + f"/{self.zone_name}/home/{self.rodsadmin_username}/croatia", + str(response["data"]["entries"][2]), + ) + self.assertEqual( + f"/{self.zone_name}/home/{self.rodsadmin_username}/croatia/zagreb", + str(response["data"]["entries"][3]), + ) + + # tests the set permission operation + def test_set_permission(self): + """Test setting permissions on collections.""" + self.api.set_token(self.rodsadmin_bearer_token) + + # test param checking + self.assertRaises(TypeError, self.api.collections.set_permission, 0, "jeb", "read", 0) + self.assertRaises( + TypeError, + self.api.collections.set_permission, + f"/{self.zone_name}/home/{self.rodsadmin_username}", + 0, + "read", + 0, + ) + self.assertRaises( + TypeError, + self.api.collections.set_permission, + f"/{self.zone_name}/home/{self.rodsadmin_username}", + "jeb", + 0, + 0, + ) + self.assertRaises( + TypeError, + self.api.collections.set_permission, + f"/{self.zone_name}/home/{self.rodsadmin_username}", + "jeb", + "read", + "0", + ) + self.assertRaises( + ValueError, + self.api.collections.set_permission, + f"/{self.zone_name}/home/{self.rodsadmin_username}", + "jeb", + "read", + 5, + ) + + # create new collection + response = self.api.collections.create(f"/{self.zone_name}/home/setPerms") + self.assertEqual(response["data"]["irods_response"]["status_code"], 0) + + # test no permission + self.api.set_token(self.rodsuser_bearer_token) + response = self.api.collections.stat(f"/{self.zone_name}/home/setPerms") + self.assertEqual(response["data"]["irods_response"]["status_code"], -170000) + + # test set permission + self.api.set_token(self.rodsadmin_bearer_token) + response = self.api.collections.set_permission( + f"/{self.zone_name}/home/setPerms", self.rodsuser_username, "read" + ) + self.assertEqual("{'irods_response': {'status_code': 0}}", str(response["data"])) + + # test with permission + self.api.set_token(self.rodsuser_bearer_token) + response = self.api.collections.stat(f"/{self.zone_name}/home/setPerms") + self.assertTrue(response["data"]["permissions"]) + + # test set permission null + self.api.set_token(self.rodsadmin_bearer_token) + response = self.api.collections.set_permission( + f"/{self.zone_name}/home/setPerms", self.rodsuser_username, "null" + ) + self.assertEqual("{'irods_response': {'status_code': 0}}", str(response["data"])) + + # test no permission + self.api.set_token(self.rodsuser_bearer_token) + response = self.api.collections.stat(f"/{self.zone_name}/home/setPerms") + self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) + + # remove the collection + self.api.set_token(self.rodsadmin_bearer_token) + response = self.api.collections.remove(f"/{self.zone_name}/home/setPerms", 1, 1) + self.assertEqual(response["data"]["irods_response"]["status_code"], 0) + + # tests the set inheritance operation + def test_set_inheritance(self): + """Test setting inheritance for collection permissions.""" + self.api.set_token(self.rodsadmin_bearer_token) + + # test param checking + self.assertRaises(TypeError, self.api.collections.set_inheritance, 0, 0, 0) + self.assertRaises( + TypeError, + self.api.collections.set_inheritance, + f"/{self.zone_name}/home/{self.rodsadmin_username}", + "0", + 0, + ) + self.assertRaises( + ValueError, + self.api.collections.set_inheritance, + f"/{self.zone_name}/home/{self.rodsadmin_username}", + 5, + 0, + ) + self.assertRaises( + TypeError, + self.api.collections.set_inheritance, + f"/{self.zone_name}/home/{self.rodsadmin_username}", + 0, + "0", + ) + self.assertRaises( + ValueError, + self.api.collections.set_inheritance, + f"/{self.zone_name}/home/{self.rodsadmin_username}", + 0, + 5, + ) + + # control + response = self.api.collections.stat(f"/{self.zone_name}/home/{self.rodsadmin_username}") + self.assertFalse(response["data"]["inheritance_enabled"]) + + # test enabling inheritance + response = self.api.collections.set_inheritance(f"/{self.zone_name}/home/{self.rodsadmin_username}", 1) + self.assertEqual("{'irods_response': {'status_code': 0}}", str(response["data"])) + + # check if changed + response = self.api.collections.stat(f"/{self.zone_name}/home/{self.rodsadmin_username}") + self.assertTrue(response["data"]["inheritance_enabled"]) + + # test disabling inheritance + response = self.api.collections.set_inheritance(f"/{self.zone_name}/home/{self.rodsadmin_username}", 0) + self.assertEqual("{'irods_response': {'status_code': 0}}", str(response["data"])) + + # check if changed + response = self.api.collections.stat(f"/{self.zone_name}/home/{self.rodsadmin_username}") + self.assertFalse(response["data"]["inheritance_enabled"]) + + # test the modify permissions operation + def test_modify_permissions(self): + """Test modifying permissions on collections.""" + self.api.set_token(self.rodsadmin_bearer_token) + + ops_permissions = [{"entity_name": self.rodsuser_username, "acl": "read"}] + + ops_permissions_null = [{"entity_name": self.rodsuser_username, "acl": "null"}] + + # test param checking + self.assertRaises(TypeError, self.api.collections.modify_permissions, 0, ops_permissions, 0) + self.assertRaises( + TypeError, + self.api.collections.modify_permissions, + f"/{self.zone_name}/home/{self.rodsadmin_username}", + 5, + 0, + ) + self.assertRaises( + TypeError, + self.api.collections.modify_permissions, + f"/{self.zone_name}/home/{self.rodsadmin_username}", + ops_permissions, + "0", + ) + self.assertRaises( + ValueError, + self.api.collections.modify_permissions, + f"/{self.zone_name}/home/{self.rodsadmin_username}", + ops_permissions, + 5, + ) + + # create new collection + response = self.api.collections.create(f"/{self.zone_name}/home/modPerms") + self.assertEqual(response["data"]["irods_response"]["status_code"], 0) + + # test no permissions + self.api.set_token(self.rodsuser_bearer_token) + response = self.api.collections.stat(f"/{self.zone_name}/home/modPerms") + self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) + + # test set permissions + self.api.set_token(self.rodsadmin_bearer_token) + response = self.api.collections.modify_permissions(f"/{self.zone_name}/home/modPerms", ops_permissions) + self.assertEqual(response["data"]["irods_response"]["status_code"], 0) + + # test with permissions + self.api.set_token(self.rodsuser_bearer_token) + response = self.api.collections.stat(f"/{self.zone_name}/home/modPerms") + self.assertTrue(response["data"]["permissions"]) + + # test set permissions nuil + self.api.set_token(self.rodsadmin_bearer_token) + response = self.api.collections.modify_permissions(f"/{self.zone_name}/home/modPerms", ops_permissions_null) + self.assertEqual(response["data"]["irods_response"]["status_code"], 0) + + # test without permissions + self.api.set_token(self.rodsuser_bearer_token) + response = self.api.collections.stat(f"/{self.zone_name}/home/modPerms") + self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) + + # remove the collection + self.api.set_token(self.rodsadmin_bearer_token) + response = self.api.collections.remove(f"/{self.zone_name}/home/modPerms", 1, 1) + self.assertEqual(response["data"]["irods_response"]["status_code"], 0) + + # test the modify metadata operation + def test_modify_metadata(self): + """Test modifying metadata on collections.""" + self.api.set_token(self.rodsadmin_bearer_token) + + ops_metadata = [{"operation": "add", "attribute": "eyeballs", "value": "itchy"}] + + ops_metadata_remove = [{"operation": "remove", "attribute": "eyeballs", "value": "itchy"}] + + # test param checking + self.assertRaises(TypeError, self.api.collections.modify_metadata, 0, ops_metadata, 0) + self.assertRaises( + TypeError, + self.api.collections.modify_metadata, + f"/{self.zone_name}/home/{self.rodsadmin_username}", + 5, + 0, + ) + self.assertRaises( + TypeError, + self.api.collections.modify_metadata, + f"/{self.zone_name}/home/{self.rodsadmin_username}", + ops_metadata, + "0", + ) + self.assertRaises( + ValueError, + self.api.collections.modify_metadata, + f"/{self.zone_name}/home/{self.rodsadmin_username}", + ops_metadata, + 5, + ) + + # test adding and removing metadata + response = self.api.collections.modify_metadata( + f"/{self.zone_name}/home/{self.rodsadmin_username}", ops_metadata + ) + self.assertEqual(response["data"]["irods_response"]["status_code"], 0) + response = self.api.collections.modify_metadata( + f"/{self.zone_name}/home/{self.rodsadmin_username}", ops_metadata_remove + ) + self.assertEqual(response["data"]["irods_response"]["status_code"], 0) + + # tests the rename operation + def test_rename(self): + """Test renaming collections.""" + self.api.set_token(self.rodsadmin_bearer_token) + + # test param checking + self.assertRaises( + TypeError, + self.api.collections.rename, + f"/{self.zone_name}/home/{self.rodsadmin_username}", + 0, + ) + self.assertRaises(TypeError, self.api.collections.rename, 0, f"/{self.zone_name}/home/pods") + + # test before move + response = self.api.collections.stat(f"/{self.zone_name}/home/pods") + self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) + response = self.api.collections.stat(f"/{self.zone_name}/home/{self.rodsadmin_username}") + self.assertTrue(response["data"]["permissions"]) + + # test renaming + response = self.api.collections.rename( + f"/{self.zone_name}/home/{self.rodsadmin_username}", + f"/{self.zone_name}/home/pods", + ) + self.assertEqual("{'irods_response': {'status_code': 0}}", str(response["data"])) + + # test before move + response = self.api.collections.stat(f"/{self.zone_name}/home/{self.rodsadmin_username}") + self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) + response = self.api.collections.stat(f"/{self.zone_name}/home/pods") + self.assertTrue(response["data"]["permissions"]) + + # test renaming + response = self.api.collections.rename( + f"/{self.zone_name}/home/pods", + f"/{self.zone_name}/home/{self.rodsadmin_username}", + ) + self.assertEqual("{'irods_response': {'status_code': 0}}", str(response["data"])) + + # tests the touch operation + def test_touch(self): + """Test touch operation to update collection timestamps.""" + self.api.set_token(self.rodsadmin_bearer_token) + self.api.collections.touch( + f"/{self.zone_name}/home/{self.rodsadmin_username}", reference=f"/{self.zone_name}/home" + ) +# Tests for data object operations +class DataObjectsTests(unittest.TestCase): + """Test iRODS data object operations.""" + + @classmethod + def setUpClass(cls): + """Set up class-level resources for data object tests.""" + setup_class(cls, {"endpoint_name": "data_objects"}) + + @classmethod + def tearDownClass(cls): + """Tear down class-level resources.""" + tear_down_class(cls) + + def setUp(self): + """Check that class initialization succeeded before each test.""" + self.assertFalse(self._class_init_error, "Class initialization failed. Cannot continue.") + + def test_common_operations(self): + """Test common data object operations (write, read, replicate, etc.).""" + self.api.set_token(self.rodsadmin_bearer_token) + + f1 = f"/{self.zone_name}/home/{self.rodsuser_username}/f1.txt" + f2 = f"/{self.zone_name}/home/{self.rodsuser_username}/f2.txt" + f3 = f"/{self.zone_name}/home/{self.rodsuser_username}/f3.txt" + resc = "resource" + + try: + # Create a unixfilesystem resource + r = self.api.resources.create( + resc, + "unixfilesystem", + self.host, + "/tmp/resource", # noqa: S108 + "", + ) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + self.api.set_token(self.rodsuser_bearer_token) + # Create a non-empty data object + r = self.api.data_objects.write("These are the bytes being written to the object", f1) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Read the data object + r = self.api.data_objects.read(f1, offset=6) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + self.assertIn('being written', r["data"]['irods_response']['bytes'].decode('utf-8')) + + # Add metadata to the data object + r = self.api.data_objects.modify_metadata( + f1, operations=[{'operation': 'add', 'attribute': 'a', 'value': 'v', 'units': 'u'}] + ) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Modify the replica + self.api.set_token(self.rodsadmin_bearer_token) + r = self.api.data_objects.modify_replica(f1, replica_number=0, new_data_comments="awesome") + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + self.api.set_token(self.rodsuser_bearer_token) + + # Replicate the data object + r = self.api.data_objects.replicate( + f1, + dst_resource=resc, + ) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Show that there are two replicas + r = self.api.queries.execute_genquery( + f"select DATA_NAME, DATA_REPL_NUM where DATA_NAME = '{f1.rsplit('/', maxsplit=1)[-1]}'" + ) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + self.assertEqual(len(r["data"]["rows"]), 2) + + # Trim the first data object + r = self.api.data_objects.trim(f1, 0) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Rename the data object + r = self.api.data_objects.rename(f1, f2) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Copy the data object + r = self.api.data_objects.copy(f2, f3) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Set permission on the object + r = self.api.data_objects.set_permission( + f3, + "rods", + "read", + ) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Confirm that the permission has been set + r = self.api.data_objects.stat(f3) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + self.assertIn( + { + "name": "rods", + "zone": self.zone_name, + "type": "rodsadmin", + "perm": "read_object", + }, + r["data"]["permissions"], + ) + + # Modify permission on the object + r = self.api.data_objects.modify_permissions(f3, operations=[{'entity_name': 'rods', 'acl': 'write'}]) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + finally: + # Remove the data objects + r = self.api.data_objects.remove(f1, 0, 1) + + r = self.api.data_objects.remove(f2, 0, 1) + + r = self.api.data_objects.remove(f3, 0, 1) + + # Remove the resource + self.api.set_token(self.rodsadmin_bearer_token) + r = self.api.resources.remove(resc) + + def test_checksums(self): + """Test checksum calculation and verification for data objects.""" + self.api.set_token(self.rodsadmin_bearer_token) + + # Create a unixfilesystem resource. + r = self.api.resources.create( + "newresource", + "unixfilesystem", + self.host, + "/tmp/newresource", # noqa: S108 + "", + ) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Create a non-empty data object + r = self.api.data_objects.write( + "These are the bytes being written to the object", + f"/{self.zone_name}/home/{self.rodsadmin_username}/file.txt", + ) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Replicate the data object + r = self.api.data_objects.replicate( + f"/{self.zone_name}/home/{self.rodsadmin_username}/file.txt", + dst_resource="newresource", + ) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Show that there are two replicas + r = self.api.queries.execute_genquery("select DATA_NAME, DATA_REPL_NUM where DATA_NAME = 'file.txt'") + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + self.assertEqual(len(r["data"]["rows"]), 2) + + try: + # Calculate a checksum for the first replica + r = self.api.data_objects.calculate_checksum( + f"/{self.zone_name}/home/{self.rodsadmin_username}/file.txt", + replica_number=0, + ) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Verify checksum information across all replicas. + r = self.api.data_objects.verify_checksum(f"/{self.zone_name}/home/{self.rodsadmin_username}/file.txt") + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + finally: + # Remove the data objects + r = self.api.data_objects.remove(f"/{self.zone_name}/home/{self.rodsadmin_username}/file.txt", 0, 1) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Remove the resource + r = self.api.resources.remove("newresource") + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + def test_touch(self): + """Test touch operation on data objects.""" + self.api.set_token(self.rodsadmin_bearer_token) + + # Test touching non existant data object with no_create + r = self.api.data_objects.touch(f"/{self.zone_name}/home/{self.rodsadmin_username}/new.txt", 1) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Show that the object has not been created + r = self.api.data_objects.stat(f"/{self.zone_name}/home/{self.rodsadmin_username}/new.txt") + self.assertEqual(r["data"]["irods_response"]["status_code"], -171000) + + # Test touching non existant object without no_create + r = self.api.data_objects.touch(f"/{self.zone_name}/home/{self.rodsadmin_username}/new.txt", 0) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Show that the object has been created + r = self.api.data_objects.stat(f"/{self.zone_name}/home/{self.rodsadmin_username}/new.txt") + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Test touching existing object without no_create + r = self.api.data_objects.touch(f"/{self.zone_name}/home/{self.rodsadmin_username}/new.txt", 1) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Remove the object + r = self.api.data_objects.remove(f"/{self.zone_name}/home/{self.rodsadmin_username}/new.txt") + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + def test_register(self): + """Test registering existing files as iRODS data objects.""" + self.api.set_token(self.rodsadmin_bearer_token) + + # Create a non-empty local file. + filename = f"/{self.zone_name}/home/{self.rodsadmin_username}/register-demo.txt" + content = "data" + + with pathlib.Path("/tmp/register-demo.txt").open("w") as f: # noqa: S108 + f.write(content) + + # Show the data object we want to create via registration does not exist. + r = self.api.data_objects.stat(filename) + self.assertEqual(r["data"]["irods_response"]["status_code"], -171000) + + try: + # Create a unixfilesystem resource. + r = self.api.resources.create( + "register_resource", + "unixfilesystem", + self.host, + "/tmp/register_resource", # noqa: S108 + "", + ) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Register the local file into the catalog as a new data object. + # We know we're registering a new data object because the "as-additional-replica" + # parameter isn't set to 1. + r = self.api.data_objects.register( + filename, + "/tmp/register-demo.txt", # noqa: S108 + "register_resource", + data_size=len(content), + ) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Show a new data object exists with the expected replica information. + r = self.api.queries.execute_genquery( + "select DATA_NAME, DATA_PATH, RESC_NAME where DATA_NAME = 'register-demo.txt'" + ) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + self.assertEqual(len(r["data"]["rows"]), 1) + self.assertEqual(r["data"]["rows"][0][1], "/tmp/register-demo.txt") # noqa: S108 + self.assertEqual(r["data"]["rows"][0][2], "register_resource") + + finally: + # Unregister the data object + r = self.api.data_objects.remove(filename, 1) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Remove the resource + r = self.api.resources.remove("register_resource") + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + def test_parallel_write(self): + """Test parallel writing to data objects.""" + self.api.set_token(self.rodsadmin_bearer_token) + self.api.data_objects.remove(f"/{self.zone_name}/home/{self.rodsadmin_username}/parallel-write.txt", 0, 1) + + # Open parallel write + r = self.api.data_objects.parallel_write_init( + f"/{self.zone_name}/home/{self.rodsadmin_username}/parallel-write.txt", 3 + ) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + handle = r["data"]["parallel_write_handle"] + + try: + # Write to the data object using the parallel write handle. + futures = [] + with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor: + for e in enumerate(["A", "B", "C"]): + count = 10 + futures.append( + executor.submit( + self.api.data_objects.write, + bytes_=e[1] * count, + offset=e[0] * count, + stream_index=e[0], + parallel_write_handle=handle, + ) + ) + for f in concurrent.futures.as_completed(futures): + r = f.result() + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + finally: + # Close parallel write + r = self.api.data_objects.parallel_write_shutdown(handle) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Remove the object + r = self.api.data_objects.remove( + f"/{self.zone_name}/home/{self.rodsadmin_username}/parallel-write.txt", + 0, + 1, + ) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Tests for resources operations -class resourcesTests(unittest.TestCase): - - @classmethod - def setUpClass(cls): - setup_class(cls, {'endpoint_name': 'resources'}) - - @classmethod - def tearDownClass(cls): - tear_down_class(cls) - - def setUp(self): - self.assertFalse(self._class_init_error, 'Class initialization failed. Cannot continue.') - - def testCommonOperations(self): - self.api.setToken(self.rodsadmin_bearer_token) - - #TEMPORARY pre-test cleanup - #test is currently not passing, so cleanup occusrs at the beginning to allow it to be run more than once in a row - self.api.resources.remove_child('test_repl', 'test_ufs0') - self.api.resources.remove_child('test_repl', 'test_ufs1') - self.api.resources.remove('test_ufs0') - self.api.resources.remove('test_ufs1') - self.api.resources.remove('test_repl') - - resc_repl = 'test_repl' - resc_ufs0 = 'test_ufs0' - resc_ufs1 = 'test_ufs1' - - # Create three resources (replication w/ two unixfilesystem resources). - r = self.api.resources.create(resc_repl, 'replication', '', '', '') - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - # Show the replication resource was created. - r = self.api.resources.stat(resc_repl) - self.assertEqual(r['data']['irods_response']['status_code'], 0) - self.assertEqual(r['data']['exists'], True) - self.assertIn('id', r['data']['info']) - self.assertEqual(r['data']['info']['name'], resc_repl) - self.assertEqual(r['data']['info']['type'], 'replication') - self.assertEqual(r['data']['info']['zone'], 'tempZone') - self.assertEqual(r['data']['info']['host'], 'EMPTY_RESC_HOST') - self.assertEqual(r['data']['info']['vault_path'], 'EMPTY_RESC_PATH') - self.assertIn('status', r['data']['info']) - self.assertIn('context', r['data']['info']) - self.assertIn('comments', r['data']['info']) - self.assertIn('information', r['data']['info']) - self.assertIn('free_space', r['data']['info']) - self.assertIn('free_space_last_modified', r['data']['info']) - self.assertEqual(r['data']['info']['parent_id'], '') - self.assertIn('created', r['data']['info']) - self.assertIn('last_modified', r['data']['info']) - self.assertIn('last_modified_millis', r['data']['info']) - - # Capture the replication resource's id. - # This resource is going to be the parent of the unixfilesystem resources. - # This value is needed to verify the relationship. - resc_repl_id = r['data']['info']['id'] - - for resc_name in [resc_ufs0, resc_ufs1]: - with self.subTest(f'Create and attach resource [{resc_name}] to [{resc_repl}]'): - vault_path = f'/tmp/{resc_name}_vault' - - # Create a unixfilesystem resource. - r = self.api.resources.create(resc_name, 'unixfilesystem', self.host, vault_path, '') - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - # Add the unixfilesystem resource as a child of the replication resource. - r = self.api.resources.add_child(resc_repl, resc_name) - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - # Show that the resource was created and configured successfully. - r = self.api.resources.stat(resc_name) - self.assertEqual(r['data']['irods_response']['status_code'], 0) - self.assertEqual(r['data']['exists'], True) - self.assertIn('id', r['data']['info']) - self.assertEqual(r['data']['info']['name'], resc_name) - self.assertEqual(r['data']['info']['type'], 'unixfilesystem') - self.assertEqual(r['data']['info']['zone'], self.zone_name) - self.assertEqual(r['data']['info']['host'], self.host) - self.assertEqual(r['data']['info']['vault_path'], vault_path) - self.assertIn('status', r['data']['info']) - self.assertIn('context', r['data']['info']) - self.assertIn('comments', r['data']['info']) - self.assertIn('information', r['data']['info']) - self.assertIn('free_space', r['data']['info']) - self.assertIn('free_space_last_modified', r['data']['info']) - self.assertEqual(r['data']['info']['parent_id'], resc_repl_id) - self.assertIn('created', r['data']['info']) - self.assertIn('last_modified', r['data']['info']) - - # Create a data object targeting the replication resource. - data_object = f'/{self.zone_name}/home/{self.rodsadmin_username}/resource_obj' - r = self.api.data_objects.write('These are the bytes to be written', data_object, resc_repl, 0) - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - # Show there are two replicas under the replication resource hierarchy. - r = self.api.queries.execute_genquery(f"select DATA_NAME, RESC_NAME where DATA_NAME = '{os.path.basename(data_object)}'") - self.assertEqual(r['data']['irods_response']['status_code'], 0) - self.assertEqual(len(r['data']['rows']), 2) - - resc_tuple = (r['data']['rows'][0][1], r['data']['rows'][1][1]) - self.assertIn(resc_tuple, [(resc_ufs0, resc_ufs1), (resc_ufs1, resc_ufs0)]) - - # Trim a replica. - r = self.api.data_objects.trim(data_object, 0) - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - # Show there is only one replica under the replication resource hierarchy. - r = self.api.queries.execute_genquery(f"select DATA_NAME, RESC_NAME where DATA_NAME = '{os.path.basename(data_object)}'") - self.assertEqual(r['data']['irods_response']['status_code'], 0) - self.assertEqual(len(r['data']['rows']), 1) - - # Launch rebalance - r = self.api.resources.rebalance(resc_repl) - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - # Give the rebalance operation time to complete! - time.sleep(3) - - # - # Clean-up - # - - # Remove the data object. - r = self.api.data_objects.remove(data_object, 0, 1) - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - # Remove resources. - for resc_name in [resc_ufs0, resc_ufs1]: - with self.subTest(f'Detach and remove resource [{resc_name}] from [{resc_repl}]'): - # Detach ufs resource from the replication resource. - r = self.api.resources.remove_child(resc_repl, resc_name) - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - # Remove ufs resource. - r = self.api.resources.remove(resc_name) - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - # Show that the resource no longer exists. - r = self.api.resources.stat(resc_name) - self.assertEqual(r['data']['irods_response']['status_code'], 0) - self.assertEqual(r['data']['exists'], False) - - # Remove replication resource. - r = self.api.resources.remove(resc_repl) - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - # Show that the resource no longer exists. - r = self.api.resources.stat(resc_repl) - self.assertEqual(r['data']['irods_response']['status_code'], 0) - self.assertEqual(r['data']['exists'], False) - - - def testModifyMetadata(self): - self.api.setToken(self.rodsadmin_bearer_token) - - # Create a unixfilesystem resource. - r = self.api.resources.create('metadata_demo', 'unixfilesystem', self.host, '/tmp/metadata_demo_vault', '') - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - operations = [ - { - 'operation': 'add', - 'attribute': 'a1', - 'value': 'v1', - 'units': 'u1' - } - ] - - # Add the metadata to the resource - r = self.api.resources.modify_metadata('metadata_demo', operations) - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - # Show that the metadata is on the resource - r = self.api.queries.execute_genquery("select RESC_NAME where META_RESC_ATTR_NAME = 'a1' and META_RESC_ATTR_VALUE = 'v1' and META_RESC_ATTR_UNITS = 'u1'") - self.assertEqual(r['data']['irods_response']['status_code'], 0) - self.assertEqual(r['data']['rows'][0][0], 'metadata_demo') - - # Remove the metadata from the resource. - operations = [ - { - 'operation': 'remove', - 'attribute': 'a1', - 'value': 'v1', - 'units': 'u1' - } - ] - - r = self.api.resources.modify_metadata('metadata_demo', operations) - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - # Show that the metadata is no longer on the resource - r = self.api.queries.execute_genquery("select RESC_NAME where META_RESC_ATTR_NAME = 'a1' and META_RESC_ATTR_VALUE = 'v1' and META_RESC_ATTR_UNITS = 'u1'") - self.assertEqual(r['data']['irods_response']['status_code'], 0) - self.assertEqual(len(r['data']['rows']), 0) - - # Remove the resource - r = self.api.resources.remove('metadata_demo') - - - def testModifyProperties(self): - self.api.setToken(self.rodsadmin_bearer_token) - - resource = 'properties_demo' - - # Create a new resource. - r = self.api.resources.create(resource, 'replication', '', '', '') - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - try: - # The list of updates to apply in sequence. - property_map = [ - ('name', 'test_modifying_resource_properties_renamed'), - ('type', 'passthru'), - ('host', 'example.org'), - ('vault_path', '/tmp/test_modifying_resource_properties_vault'), - ('status', 'down'), - ('status', 'up'), - ('comments', 'test_modifying_resource_properties_comments'), - ('information', 'test_modifying_resource_properties_information'), - ('free_space', 'test_modifying_resource_properties_free_space'), - ('context', 'test_modifying_resource_properties_context') - ] - - # Apply each update to the resource and verify that each one results - # in the expected results. - for p, v in property_map: - with self.subTest(f'Setting property [{p}] to value [{v}]'): - # Change a property of the resource. - r = self.api.resources.modify(resource, p, v) - - # Make sure to update the "resource" variable following a successful rename. - if 'name' == p: - resource = v - - # Show the property was modified. - r = self.api.resources.stat(resource) - self.assertEqual(r['data']['irods_response']['status_code'], 0) - self.assertEqual(r['data']['info'][p], v) - finally: - # Remove the resource - r = self.api.resources.remove(resource) +class ResourcesTests(unittest.TestCase): + """Test iRODS resource operations.""" + + @classmethod + def setUpClass(cls): + """Set up class-level resources for resource tests.""" + setup_class(cls, {"endpoint_name": "resources"}) + + @classmethod + def tearDownClass(cls): + """Tear down class-level resources.""" + tear_down_class(cls) + + def setUp(self): + """Check that class initialization succeeded before each test.""" + self.assertFalse(self._class_init_error, "Class initialization failed. Cannot continue.") + + def test_common_operations(self): + """Test common resource operations (create, list, stat, etc.).""" + self.api.set_token(self.rodsadmin_bearer_token) + + # TEMPORARY pre-test cleanup + # test is currently not passing, so cleanup occurs at the beginning to allow it + # to be run more than once in a row + self.api.resources.remove_child("test_repl", "test_ufs0") + self.api.resources.remove_child("test_repl", "test_ufs1") + self.api.resources.remove("test_ufs0") + self.api.resources.remove("test_ufs1") + self.api.resources.remove("test_repl") + + resc_repl = "test_repl" + resc_ufs0 = "test_ufs0" + resc_ufs1 = "test_ufs1" + + # Create three resources (replication w/ two unixfilesystem resources). + r = self.api.resources.create(resc_repl, "replication", "", "", "") + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Show the replication resource was created. + r = self.api.resources.stat(resc_repl) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + self.assertEqual(r["data"]["exists"], True) + self.assertIn("id", r["data"]["info"]) + self.assertEqual(r["data"]["info"]["name"], resc_repl) + self.assertEqual(r["data"]["info"]["type"], "replication") + self.assertEqual(r["data"]["info"]["zone"], "tempZone") + self.assertEqual(r["data"]["info"]["host"], "EMPTY_RESC_HOST") + self.assertEqual(r["data"]["info"]["vault_path"], "EMPTY_RESC_PATH") + self.assertIn("status", r["data"]["info"]) + self.assertIn("context", r["data"]["info"]) + self.assertIn("comments", r["data"]["info"]) + self.assertIn("information", r["data"]["info"]) + self.assertIn("free_space", r["data"]["info"]) + self.assertIn("free_space_last_modified", r["data"]["info"]) + self.assertEqual(r["data"]["info"]["parent_id"], "") + self.assertIn("created", r["data"]["info"]) + self.assertIn("last_modified", r["data"]["info"]) + self.assertIn("last_modified_millis", r["data"]["info"]) + + # Capture the replication resource's id. + # This resource is going to be the parent of the unixfilesystem resources. + # This value is needed to verify the relationship. + resc_repl_id = r["data"]["info"]["id"] + + for resc_name in [resc_ufs0, resc_ufs1]: + with self.subTest(f"Create and attach resource [{resc_name}] to [{resc_repl}]"): + vault_path = f"/tmp/{resc_name}_vault" # noqa: S108 + + # Create a unixfilesystem resource. + r = self.api.resources.create(resc_name, "unixfilesystem", self.host, vault_path, "") + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Add the unixfilesystem resource as a child of the replication resource. + r = self.api.resources.add_child(resc_repl, resc_name) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Show that the resource was created and configured successfully. + r = self.api.resources.stat(resc_name) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + self.assertEqual(r["data"]["exists"], True) + self.assertIn("id", r["data"]["info"]) + self.assertEqual(r["data"]["info"]["name"], resc_name) + self.assertEqual(r["data"]["info"]["type"], "unixfilesystem") + self.assertEqual(r["data"]["info"]["zone"], self.zone_name) + self.assertEqual(r["data"]["info"]["host"], self.host) + self.assertEqual(r["data"]["info"]["vault_path"], vault_path) + self.assertIn("status", r["data"]["info"]) + self.assertIn("context", r["data"]["info"]) + self.assertIn("comments", r["data"]["info"]) + self.assertIn("information", r["data"]["info"]) + self.assertIn("free_space", r["data"]["info"]) + self.assertIn("free_space_last_modified", r["data"]["info"]) + self.assertEqual(r["data"]["info"]["parent_id"], resc_repl_id) + self.assertIn("created", r["data"]["info"]) + self.assertIn("last_modified", r["data"]["info"]) + + # Create a data object targeting the replication resource. + data_object = f"/{self.zone_name}/home/{self.rodsadmin_username}/resource_obj" + r = self.api.data_objects.write("These are the bytes to be written", data_object, resc_repl, 0) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Show there are two replicas under the replication resource hierarchy. + r = self.api.queries.execute_genquery( + f"select DATA_NAME, RESC_NAME where DATA_NAME = '{pathlib.Path(data_object).name}'" + ) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + self.assertEqual(len(r["data"]["rows"]), 2) + + resc_tuple = (r["data"]["rows"][0][1], r["data"]["rows"][1][1]) + self.assertIn(resc_tuple, [(resc_ufs0, resc_ufs1), (resc_ufs1, resc_ufs0)]) + + # Trim a replica. + r = self.api.data_objects.trim(data_object, 0) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Show there is only one replica under the replication resource hierarchy. + r = self.api.queries.execute_genquery( + f"select DATA_NAME, RESC_NAME where DATA_NAME = '{pathlib.Path(data_object).name}'" + ) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + self.assertEqual(len(r["data"]["rows"]), 1) + + # Launch rebalance + r = self.api.resources.rebalance(resc_repl) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Give the rebalance operation time to complete! + time.sleep(3) + + # + # Clean-up + # + + # Remove the data object. + r = self.api.data_objects.remove(data_object, 0, 1) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Remove resources. + for resc_name in [resc_ufs0, resc_ufs1]: + with self.subTest(f"Detach and remove resource [{resc_name}] from [{resc_repl}]"): + # Detach ufs resource from the replication resource. + r = self.api.resources.remove_child(resc_repl, resc_name) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Remove ufs resource. + r = self.api.resources.remove(resc_name) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Show that the resource no longer exists. + r = self.api.resources.stat(resc_name) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + self.assertEqual(r["data"]["exists"], False) + + # Remove replication resource. + r = self.api.resources.remove(resc_repl) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Show that the resource no longer exists. + r = self.api.resources.stat(resc_repl) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + self.assertEqual(r["data"]["exists"], False) + + def test_modify_metadata(self): + """Test modifying metadata on resources.""" + self.api.set_token(self.rodsadmin_bearer_token) + + # Create a unixfilesystem resource. + r = self.api.resources.create( + "metadata_demo", + "unixfilesystem", + self.host, + "/tmp/metadata_demo_vault", # noqa: S108 + "", + ) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + operations = [{"operation": "add", "attribute": "a1", "value": "v1", "units": "u1"}] + + # Add the metadata to the resource + r = self.api.resources.modify_metadata("metadata_demo", operations) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Show that the metadata is on the resource + r = self.api.queries.execute_genquery( + "select RESC_NAME where META_RESC_ATTR_NAME = 'a1' and " + "META_RESC_ATTR_VALUE = 'v1' and META_RESC_ATTR_UNITS = 'u1'" + ) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + self.assertEqual(r["data"]["rows"][0][0], "metadata_demo") + + # Remove the metadata from the resource. + operations = [{"operation": "remove", "attribute": "a1", "value": "v1", "units": "u1"}] + + r = self.api.resources.modify_metadata("metadata_demo", operations) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Show that the metadata is no longer on the resource + r = self.api.queries.execute_genquery( + "select RESC_NAME where META_RESC_ATTR_NAME = 'a1' and " + "META_RESC_ATTR_VALUE = 'v1' and META_RESC_ATTR_UNITS = 'u1'" + ) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + self.assertEqual(len(r["data"]["rows"]), 0) + + # Remove the resource + r = self.api.resources.remove("metadata_demo") + + def test_modify_properties(self): + """Test modifying resource properties.""" + self.api.set_token(self.rodsadmin_bearer_token) + + resource = "properties_demo" + + # Create a new resource. + r = self.api.resources.create(resource, "replication", "", "", "") + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + try: + # The list of updates to apply in sequence. + property_map = [ + ("name", "test_modifying_resource_properties_renamed"), + ("type", "passthru"), + ("host", "example.org"), + ("vault_path", "/tmp/test_modifying_resource_properties_vault"), # noqa: S108 + ("status", "down"), + ("status", "up"), + ("comments", "test_modifying_resource_properties_comments"), + ("information", "test_modifying_resource_properties_information"), + ("free_space", "test_modifying_resource_properties_free_space"), + ("context", "test_modifying_resource_properties_context"), + ] + + # Apply each update to the resource and verify that each one results + # in the expected results. + for p, v in property_map: + with self.subTest(f"Setting property [{p}] to value [{v}]"): + # Change a property of the resource. + r = self.api.resources.modify(resource, p, v) + + # Make sure to update the "resource" variable following a successful rename. + if p == "name": + resource = v + + # Show the property was modified. + r = self.api.resources.stat(resource) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + self.assertEqual(r["data"]["info"][p], v) + finally: + # Remove the resource + r = self.api.resources.remove(resource) # Tests for rule operations -class rulesTests(unittest.TestCase): - - @classmethod - def setUpClass(cls): - setup_class(cls, {'endpoint_name': 'rules'}) - - @classmethod - def tearDownClass(cls): - tear_down_class(cls) - - def setUp(self): - self.assertFalse(self._class_init_error, 'Class initialization failed. Cannot continue.') - - def testList(self): - # Try listing rule engine plugins - r = self.api.rules.list_rule_engines() +class RulesTests(unittest.TestCase): + """Test iRODS rule operations.""" + + @classmethod + def setUpClass(cls): + """Set up class-level resources for rule tests.""" + setup_class(cls, {"endpoint_name": "rules"}) + + @classmethod + def tearDownClass(cls): + """Tear down class-level resources.""" + tear_down_class(cls) + + def setUp(self): + """Check that class initialization succeeded before each test.""" + self.assertFalse(self._class_init_error, "Class initialization failed. Cannot continue.") + + def test_list(self): + """Test listing rule engine plugins.""" + # Try listing rule engine plugins + r = self.api.rules.list_rule_engines() + + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + self.assertGreater(len(r["data"]["rule_engine_plugin_instances"]), 0) + + def test_execute_rule(self): + """Test executing iRODS rules.""" + test_msg = "This was run by the iRODS HTTP API test suite!" + + # Execute rule text against the iRODS rule language. + r = self.api.rules.execute( + f'writeLine("stdout", "{test_msg}")', + "irods_rule_engine_plugin-irods_rule_language-instance", + ) + + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + self.assertEqual(r["data"]["stderr"], None) + + # The REP always appends a newline character to the result. While we could trim the result, + # it is better to append a newline character to the expected result to guarantee things align. + self.assertEqual(r["data"]["stdout"], test_msg + "\n") + + def test_remove_delay_rule(self): + """Test removing delayed execution rules.""" + rep_instance = "irods_rule_engine_plugin-irods_rule_language-instance" + + # Schedule a delay rule to execute in the distant future. + r = self.api.rules.execute( + f'delay("{rep_instance}1h") ' + f'{{ writeLine("serverLog", "iRODS HTTP API"); }}', + rep_instance, + ) + + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Find the delay rule we just created. + # This query assumes the test suite is running on a system where no other delay + # rules are being created. + r = self.api.queries.execute_genquery("select max(RULE_EXEC_ID)") + + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + self.assertEqual(len(r["data"]["rows"]), 1) + + # Remove the delay rule. + r = self.api.rules.remove_delay_rule(int(r["data"]["rows"][0][0])) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + +# Tests for query operations +class QueryTests(unittest.TestCase): + """Test iRODS query operations.""" + + @classmethod + def setUpClass(cls): + """Set up class-level resources for query tests.""" + setup_class(cls, {"endpoint_name": "query"}) + + @classmethod + def tearDownClass(cls): + """Tear down class-level resources.""" + tear_down_class(cls) + + def setUp(self): + """Check that class initialization succeeded before each test.""" + self.assertFalse(self._class_init_error, "Class initialization failed. Cannot continue.") + + def test_create_execute_remove_specific_query(self): + """Test creating, executing, and removing specific queries.""" + try: + # As rodsadmin, create a specific query + self.api.set_token(self.rodsadmin_bearer_token) + + name = "get_users_count" + sql = "select count(*) from r_user_main" + r = self.api.queries.add_specific_query(name=name, sql=sql) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Switch to rodsuser and execute it + self.api.set_token(self.rodsuser_bearer_token) + r = self.api.queries.execute_specific_query(name=name) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + self.assertEqual(r["data"]["rows"][0][0], "3") - self.assertEqual(r['data']['irods_response']['status_code'], 0) - self.assertGreater(len(r['data']['rule_engine_plugin_instances']), 0) - - - def testExecuteRule(self): - test_msg = 'This was run by the iRODS HTTP API test suite!' - - # Execute rule text against the iRODS rule language. - r = self.api.rules.execute(f'writeLine("stdout", "{test_msg}")', 'irods_rule_engine_plugin-irods_rule_language-instance') - - self.assertEqual(r['data']['irods_response']['status_code'], 0) - self.assertEqual(r['data']['stderr'], None) - - # The REP always appends a newline character to the result. While we could trim the result, - # it is better to append a newline character to the expected result to guarantee things align. - self.assertEqual(r['data']['stdout'], test_msg + '\n') - - - def testRemoveDelayRule(self): - rep_instance = 'irods_rule_engine_plugin-irods_rule_language-instance' - - # Schedule a delay rule to execute in the distant future. - r = self.api.rules.execute(f'delay("{rep_instance}1h") {{ writeLine("serverLog", "iRODS HTTP API"); }}', rep_instance) - - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - # Find the delay rule we just created. - # This query assumes the test suite is running on a system where no other delay - # rules are being created. - r = self.api.queries.execute_genquery('select max(RULE_EXEC_ID)') - - self.assertEqual(r['data']['irods_response']['status_code'], 0) - self.assertEqual(len(r['data']['rows']), 1) - - print(r) - - # Remove the delay rule. - r = self.api.rules.remove_delay_rule(int(r['data']['rows'][0][0])) - print(r) - self.assertEqual(r['data']['irods_response']['status_code'], 0) + finally: + # Switch to rodsadmin and remove it + self.api.set_token(self.rodsadmin_bearer_token) + r = self.api.queries.remove_specific_query(name=name) # Tests for tickets operations -class ticketsTests(unittest.TestCase): - - @classmethod - def setUpClass(cls): - setup_class(cls, {'endpoint_name': 'tickets'}) - - @classmethod - def tearDownClass(cls): - tear_down_class(cls) - - def setUp(self): - self.assertFalse(self._class_init_error, 'Class initialization failed. Cannot continue.') - - def testCreateAndRemove(self): - self.api.setToken(self.rodsuser_bearer_token) - - # Create a write ticket. - ticket_type = 'write' - ticket_path = f'/{self.zone_name}/home/{self.rodsuser_username}' - ticket_use_count = 2000 - ticket_groups = 'public' - ticket_hosts = self.host - r = self.api.tickets.create(ticket_path, ticket_type, use_count=ticket_use_count, seconds_until_expiration=3600, users='rods,jeb', groups=ticket_groups, hosts=ticket_hosts) - print(r) - self.assertEqual(r['data']['irods_response']['status_code'], 0) - ticket_string = r['data']['ticket'] - self.assertGreater(len(ticket_string), 0) - - # Show the ticket exists and has the properties we defined during creation. - # We can use GenQuery for this, but it does seem better to provide a convenience operation - # for this. - r = self.api.queries.execute_genquery('select TICKET_STRING, TICKET_TYPE, TICKET_COLL_NAME, TICKET_USES_LIMIT, TICKET_ALLOWED_USER_NAME, TICKET_ALLOWED_GROUP_NAME, TICKET_ALLOWED_HOST') - - self.assertEqual(r['data']['irods_response']['status_code'], 0) - self.assertIn(ticket_string, r['data']['rows'][0]) - self.assertEqual(r['data']['rows'][0][1], ticket_type) - self.assertEqual(r['data']['rows'][0][2], ticket_path) - self.assertEqual(r['data']['rows'][0][3], str(ticket_use_count)) - self.assertIn(r['data']['rows'][0][4], ['rods', 'jeb']) - self.assertEqual(r['data']['rows'][0][5], ticket_groups) - self.assertGreater(len(r['data']['rows'][0][6]), 0) - - # Remove the ticket. - r = self.api.tickets.remove(ticket_string) - - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - # Show the ticket no longer exists. - r = self.api.queries.execute_genquery('select TICKET_STRING') - - self.assertEqual(r['data']['irods_response']['status_code'], 0) - self.assertEqual(len(r['data']['rows']), 0) +class TicketsTests(unittest.TestCase): + """Test iRODS ticket operations.""" + + @classmethod + def setUpClass(cls): + """Set up class-level resources for ticket tests.""" + setup_class(cls, {"endpoint_name": "tickets"}) + + @classmethod + def tearDownClass(cls): + """Tear down class-level resources.""" + tear_down_class(cls) + + def setUp(self): + """Check that class initialization succeeded before each test.""" + self.assertFalse(self._class_init_error, "Class initialization failed. Cannot continue.") + + def test_create_and_remove(self): + """Test creating and removing tickets.""" + self.api.set_token(self.rodsuser_bearer_token) + + # Create a write ticket. + ticket_type = "write" + ticket_path = f"/{self.zone_name}/home/{self.rodsuser_username}" + ticket_use_count = 2000 + ticket_groups = "public" + ticket_hosts = self.host + r = self.api.tickets.create( + ticket_path, + ticket_type, + use_count=ticket_use_count, + seconds_until_expiration=3600, + users="rods,jeb", + groups=ticket_groups, + hosts=ticket_hosts, + ) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + ticket_string = r["data"]["ticket"] + self.assertGreater(len(ticket_string), 0) + + # Show the ticket exists and has the properties we defined during creation. + # We can use GenQuery for this, but it does seem better to provide a convenience + # operation for this. + r = self.api.queries.execute_genquery( + "select TICKET_STRING, TICKET_TYPE, TICKET_COLL_NAME, TICKET_USES_LIMIT, " + "TICKET_ALLOWED_USER_NAME, TICKET_ALLOWED_GROUP_NAME, TICKET_ALLOWED_HOST" + ) + + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + self.assertIn(ticket_string, r["data"]["rows"][0]) + self.assertEqual(r["data"]["rows"][0][1], ticket_type) + self.assertEqual(r["data"]["rows"][0][2], ticket_path) + self.assertEqual(r["data"]["rows"][0][3], str(ticket_use_count)) + self.assertIn(r["data"]["rows"][0][4], ["rods", "jeb"]) + self.assertEqual(r["data"]["rows"][0][5], ticket_groups) + self.assertGreater(len(r["data"]["rows"][0][6]), 0) + + # Remove the ticket. + r = self.api.tickets.remove(ticket_string) + + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Show the ticket no longer exists. + r = self.api.queries.execute_genquery("select TICKET_STRING") + + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + self.assertEqual(len(r["data"]["rows"]), 0) # Tests for user operations -class userTests(unittest.TestCase): - @classmethod - def setUpClass(cls): - setup_class(cls, {'endpoint_name': 'users-groups'}) - - @classmethod - def tearDownClass(cls): - tear_down_class(cls) - - def setUp(self): - self.assertFalse(self._class_init_error, 'Class initialization failed. Cannot continue.') - - def test_create_stat_and_remove_rodsuser(self): - self.api.setToken(self.rodsadmin_bearer_token) - - new_username = 'test_user_rodsuser' - user_type = 'rodsuser' - - # Create a new user. - r = self.api.users_groups.create_user(new_username, self.zone_name, user_type) - self.assertEqual(r['status_code'], 200) - - # Stat the user. - r = self.api.users_groups.stat(new_username, self.zone_name) - self.assertEqual(r['status_code'], 200) - - stat_info = r['data'] - self.assertEqual(stat_info['irods_response']['status_code'], 0) - self.assertEqual(stat_info['exists'], True) - self.assertIn('id', stat_info) - self.assertEqual(stat_info['local_unique_name'], f'{new_username}#{self.zone_name}') - self.assertEqual(stat_info['type'], user_type) - - # Remove the user. - r = self.api.users_groups.remove_user(new_username, self.zone_name) - self.assertEqual(r['status_code'], 200) - - - def test_set_password(self): - self.api.setToken(self.rodsadmin_bearer_token) - - new_username = 'test_user_rodsuser' - user_type = 'rodsuser' - - # Create a new user. - r = self.api.users_groups.create_user(new_username, self.zone_name, user_type) - self.assertEqual(r['status_code'], 200) - - new_password = 'new_password' - # Set a new password - r = self.api.users_groups.set_password(new_username, self.zone_name, new_password) - self.assertEqual(r['status_code'], 200) - - # Try to get a token for the user - token = self.api.authenticate(new_username, new_password) - self.assertIsInstance(token, str) - - # Remove the user. - r = self.api.users_groups.remove_user(new_username, self.zone_name) - self.assertEqual(r['status_code'], 200) - - - def test_create_stat_and_remove_rodsadmin(self): - self.api.setToken(self.rodsadmin_bearer_token) - - new_username = 'test_user_rodsadmin' - user_type = 'rodsadmin' - headers = {'Authorization': 'Bearer ' + self.rodsadmin_bearer_token} - - # Create a new user. - r = self.api.users_groups.create_user(new_username, self.zone_name, user_type) - self.assertEqual(r['status_code'], 200) - - # Stat the user. - r = self.api.users_groups.stat(new_username, self.zone_name) - self.assertEqual(r['status_code'], 200) - - stat_info = r['data'] - self.assertEqual(stat_info['irods_response']['status_code'], 0) - self.assertEqual(stat_info['exists'], True) - self.assertIn('id', stat_info) - self.assertEqual(stat_info['local_unique_name'], f'{new_username}#{self.zone_name}') - self.assertEqual(stat_info['type'], user_type) - - # Remove the user. - r = self.api.users_groups.remove_user(new_username, self.zone_name) - self.assertEqual(r['status_code'], 200) - - - def test_create_stat_and_remove_groupadmin(self): - self.api.setToken(self.rodsadmin_bearer_token) - - new_username = 'test_user_groupadmin' - user_type = 'groupadmin' - - # Create a new user. - r = self.api.users_groups.create_user(new_username, self.zone_name, user_type) - self.assertEqual(r['status_code'], 200) - - # Stat the user. - r = self.api.users_groups.stat(new_username, self.zone_name) - self.assertEqual(r['status_code'], 200) - - stat_info = r['data'] - self.assertEqual(stat_info['irods_response']['status_code'], 0) - self.assertEqual(stat_info['exists'], True) - self.assertIn('id', stat_info) - self.assertEqual(stat_info['local_unique_name'], f'{new_username}#{self.zone_name}') - self.assertEqual(stat_info['type'], user_type) - - # Remove the user. - r = self.api.users_groups.remove_user(new_username, self.zone_name) - self.assertEqual(r['status_code'], 200) - - - def test_add_remove_user_to_and_from_group(self): - self.api.setToken(self.rodsadmin_bearer_token) - - # Create a new group. - new_group = 'test_group' - r = self.api.users_groups.create_group(new_group) - self.assertEqual(r['status_code'], 200) - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - # Stat the group. - r = self.api.users_groups.stat(new_group) - self.assertEqual(r['status_code'], 200) - - stat_info = r['data'] - self.assertEqual(stat_info['irods_response']['status_code'], 0) - self.assertEqual(stat_info['exists'], True) - self.assertIn('id', stat_info) - self.assertEqual(stat_info['type'], 'rodsgroup') - - # Create a new user. - new_username = 'test_user_rodsuser' - user_type = 'rodsuser' - r = self.api.users_groups.create_user(new_username, self.zone_name, user_type) - self.assertEqual(r['status_code'], 200) - - # Add user to group. - r = self.api.users_groups.add_to_group(new_username, self.zone_name, new_group) - self.assertEqual(r['status_code'], 200) - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - # Show that the user is a member of the group. - r = self.api.users_groups.is_member_of_group(new_group, new_username, self.zone_name) - self.assertEqual(r['status_code'], 200) - result = r['data'] - self.assertEqual(result['irods_response']['status_code'], 0) - self.assertEqual(result['is_member'], True) - - # Remove user from group. - data = {'op': 'remove_from_group', 'group': new_group, 'user': new_username, 'zone': self.zone_name} - r = self.api.users_groups.remove_from_group(new_username, self.zone_name, new_group) - - self.assertEqual(r['status_code'], 200) - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - # Remove the user. - r = self.api.users_groups.remove_user(new_username, self.zone_name) - self.assertEqual(r['status_code'], 200) - - # Remove group. - r = self.api.users_groups.remove_group(new_group) - self.assertEqual(r['status_code'], 200) - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - # Show that the group no longer exists. - params = {'op': 'stat', 'name': new_group} - r = self.api.users_groups.stat(new_group) - self.assertEqual(r['status_code'], 200) - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - stat_info = r['data'] - self.assertEqual(stat_info['irods_response']['status_code'], 0) - self.assertEqual(stat_info['exists'], False) - - - def test_only_a_rodsadmin_can_change_the_type_of_a_user(self): - self.api.setToken(self.rodsadmin_bearer_token) - - # Create a new user. - new_username = 'test_user_rodsuser' - user_type = 'rodsuser' - r = self.api.users_groups.create_user(new_username, self.zone_name, user_type) - self.assertEqual(r['status_code'], 200) - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - # Show that a rodsadmin can change the type of the new user. - new_user_type = 'groupadmin' - r = self.api.users_groups.set_user_type(new_username, self.zone_name, new_user_type) - self.assertEqual(r['status_code'], 200) - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - # Show that a non-admin cannot change the type of the new user. - self.api.setToken(self.rodsuser_bearer_token) - r = self.api.users_groups.set_user_type(new_user_type, self.zone_name, new_user_type) - self.assertEqual(r['status_code'], 200) - self.assertEqual(r['data']['irods_response']['status_code'], -13000) - - # Show that the user type matches the type set by the rodsadmin. - params = {'op': 'stat', 'name': new_username, 'zone': self.zone_name} - r = self.api.users_groups.stat(new_username, self.zone_name) - self.assertEqual(r['status_code'], 200) - - stat_info = r['data'] - self.assertEqual(stat_info['irods_response']['status_code'], 0) - self.assertEqual(stat_info['exists'], True) - self.assertEqual(stat_info['local_unique_name'], f'{new_username}#{self.zone_name}') - self.assertEqual(stat_info['type'], new_user_type) - - # Remove the user. - self.api.setToken(self.rodsadmin_bearer_token) - r = self.api.users_groups.remove_user(new_username, self.zone_name) - self.assertEqual(r['status_code'], 200) - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - - def test_listing_all_users_in_zone(self): - self.api.setToken(self.rodsuser_bearer_token) - - r = self.api.users_groups.users() - self.assertEqual(r['status_code'], 200) - result = r['data'] - self.assertEqual(result['irods_response']['status_code'], 0) - self.assertIn({'name': self.rodsadmin_username, 'zone': self.zone_name}, result['users']) - self.assertIn({'name': self.rodsuser_username, 'zone': self.zone_name}, result['users']) - - - def test_listing_all_groups_in_zone(self): - self.api.setToken(self.rodsadmin_bearer_token) - - # Create a new group. - new_group = 'test_group' - r = self.api.users_groups.create_group(new_group) - self.assertEqual(r['status_code'], 200) - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - self.api.setToken(self.rodsuser_bearer_token) - # Get all groups. - r = self.api.users_groups.groups() - self.assertEqual(r['status_code'], 200) - result = r['data'] - self.assertEqual(result['irods_response']['status_code'], 0) - self.assertIn('public', result['groups']) - self.assertIn(new_group, result['groups']) - - self.api.setToken(self.rodsadmin_bearer_token) - # Remove the new group. - r = self.api.users_groups.remove_group(new_group) - self.assertEqual(r['status_code'], 200) - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - - def test_modifying_metadata_atomically(self): - self.api.setToken(self.rodsadmin_bearer_token) - username = self.rodsuser_username - - # Add metadata to the user. - ops = [ - { - 'operation': 'add', - 'attribute': 'a1', - 'value': 'v1', - 'units': 'u1' - } - ] - r = self.api.users_groups.modify_metadata(username, ops) - self.assertEqual(r['status_code'], 200) - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - # Show the metadata exists on the user. - r = self.api.queries.execute_genquery("select USER_NAME where META_USER_ATTR_NAME = 'a1' and META_USER_ATTR_VALUE = 'v1' and META_USER_ATTR_UNITS = 'u1'") - self.assertEqual(r['status_code'], 200) - - result = r['data'] - self.assertEqual(result['irods_response']['status_code'], 0) - self.assertEqual(result['rows'][0][0], username) - - # Remove the metadata from the user. - ops = [ - { - 'operation': 'remove', - 'attribute': 'a1', - 'value': 'v1', - 'units': 'u1' - } - ] - r = self.api.users_groups.modify_metadata(username, ops) - self.assertEqual(r['status_code'], 200) - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - # Show the metadata no longer exists on the user. - r = self.api.queries.execute_genquery("select USER_NAME where META_USER_ATTR_NAME = 'a1' and META_USER_ATTR_VALUE = 'v1' and META_USER_ATTR_UNITS = 'u1'") - self.assertEqual(r['status_code'], 200) - - result = r['data'] - self.assertEqual(result['irods_response']['status_code'], 0) - self.assertEqual(len(result['rows']), 0) - +class UserTests(unittest.TestCase): + """Test iRODS user and group operations.""" + + @classmethod + def setUpClass(cls): + """Set up class-level resources for user tests.""" + setup_class(cls, {"endpoint_name": "users-groups"}) + + @classmethod + def tearDownClass(cls): + """Tear down class-level resources.""" + tear_down_class(cls) + + def setUp(self): + """Check that class initialization succeeded before each test.""" + self.assertFalse(self._class_init_error, "Class initialization failed. Cannot continue.") + + def test_create_stat_and_remove_rodsuser(self): + """Test creating, querying, and removing rodsuser users.""" + self.api.set_token(self.rodsadmin_bearer_token) + + new_username = "test_user_rodsuser" + user_type = "rodsuser" + + # Create a new user. + r = self.api.users_groups.create_user(new_username, self.zone_name, user_type) + self.assertEqual(r["status_code"], 200) + + # Stat the user. + r = self.api.users_groups.stat(new_username, self.zone_name) + self.assertEqual(r["status_code"], 200) + + stat_info = r["data"] + self.assertEqual(stat_info["irods_response"]["status_code"], 0) + self.assertEqual(stat_info["exists"], True) + self.assertIn("id", stat_info) + self.assertEqual(stat_info["local_unique_name"], f"{new_username}#{self.zone_name}") + self.assertEqual(stat_info["type"], user_type) + + # Remove the user. + r = self.api.users_groups.remove_user(new_username, self.zone_name) + self.assertEqual(r["status_code"], 200) + + def test_set_password(self): + """Test setting user passwords.""" + self.api.set_token(self.rodsadmin_bearer_token) + + new_username = "test_user_rodsuser" + user_type = "rodsuser" + + # Create a new user. + r = self.api.users_groups.create_user(new_username, self.zone_name, user_type) + self.assertEqual(r["status_code"], 200) + + new_password = "new_password" # noqa: S105 + # Set a new password + r = self.api.users_groups.set_password(new_username, self.zone_name, new_password) + self.assertEqual(r["status_code"], 200) + + # Try to get a token for the user + token = self.api.authenticate(new_username, new_password) + self.assertIsInstance(token, str) + + # Remove the user. + r = self.api.users_groups.remove_user(new_username, self.zone_name) + self.assertEqual(r["status_code"], 200) + + def test_create_stat_and_remove_rodsadmin(self): + """Test creating, querying, and removing rodsadmin users.""" + self.api.set_token(self.rodsadmin_bearer_token) + + new_username = "test_user_rodsadmin" + user_type = "rodsadmin" + + # Create a new user. + r = self.api.users_groups.create_user(new_username, self.zone_name, user_type) + self.assertEqual(r["status_code"], 200) + + # Stat the user. + r = self.api.users_groups.stat(new_username, self.zone_name) + self.assertEqual(r["status_code"], 200) + + stat_info = r["data"] + self.assertEqual(stat_info["irods_response"]["status_code"], 0) + self.assertEqual(stat_info["exists"], True) + self.assertIn("id", stat_info) + self.assertEqual(stat_info["local_unique_name"], f"{new_username}#{self.zone_name}") + self.assertEqual(stat_info["type"], user_type) + + # Remove the user. + r = self.api.users_groups.remove_user(new_username, self.zone_name) + self.assertEqual(r["status_code"], 200) + + def test_create_stat_and_remove_groupadmin(self): + """Test creating, querying, and removing groupadmin users.""" + self.api.set_token(self.rodsadmin_bearer_token) + + new_username = "test_user_groupadmin" + user_type = "groupadmin" + + # Create a new user. + r = self.api.users_groups.create_user(new_username, self.zone_name, user_type) + self.assertEqual(r["status_code"], 200) + + # Stat the user. + r = self.api.users_groups.stat(new_username, self.zone_name) + self.assertEqual(r["status_code"], 200) + + stat_info = r["data"] + self.assertEqual(stat_info["irods_response"]["status_code"], 0) + self.assertEqual(stat_info["exists"], True) + self.assertIn("id", stat_info) + self.assertEqual(stat_info["local_unique_name"], f"{new_username}#{self.zone_name}") + self.assertEqual(stat_info["type"], user_type) + + # Remove the user. + r = self.api.users_groups.remove_user(new_username, self.zone_name) + self.assertEqual(r["status_code"], 200) + + def test_add_remove_user_to_and_from_group(self): + """Test adding and removing users from groups.""" + self.api.set_token(self.rodsadmin_bearer_token) + + # Create a new group. + new_group = "test_group" + r = self.api.users_groups.create_group(new_group) + self.assertEqual(r["status_code"], 200) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Stat the group. + r = self.api.users_groups.stat(new_group) + self.assertEqual(r["status_code"], 200) + + stat_info = r["data"] + self.assertEqual(stat_info["irods_response"]["status_code"], 0) + self.assertEqual(stat_info["exists"], True) + self.assertIn("id", stat_info) + self.assertEqual(stat_info["type"], "rodsgroup") + + # Create a new user. + new_username = "test_user_rodsuser" + user_type = "rodsuser" + r = self.api.users_groups.create_user(new_username, self.zone_name, user_type) + self.assertEqual(r["status_code"], 200) + + # Add user to group. + r = self.api.users_groups.add_to_group(new_username, self.zone_name, new_group) + self.assertEqual(r["status_code"], 200) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Show that the user is a member of the group. + r = self.api.users_groups.is_member_of_group(new_group, new_username, self.zone_name) + self.assertEqual(r["status_code"], 200) + result = r["data"] + self.assertEqual(result["irods_response"]["status_code"], 0) + self.assertEqual(result["is_member"], True) + + # Remove user from group. + r = self.api.users_groups.remove_from_group(new_username, self.zone_name, new_group) + + self.assertEqual(r["status_code"], 200) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Remove the user. + r = self.api.users_groups.remove_user(new_username, self.zone_name) + self.assertEqual(r["status_code"], 200) + + # Remove group. + r = self.api.users_groups.remove_group(new_group) + self.assertEqual(r["status_code"], 200) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Show that the group no longer exists. + r = self.api.users_groups.stat(new_group) + self.assertEqual(r["status_code"], 200) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + stat_info = r["data"] + self.assertEqual(stat_info["irods_response"]["status_code"], 0) + self.assertEqual(stat_info["exists"], False) + + def test_only_a_rodsadmin_can_change_the_type_of_a_user(self): + """Test that only rodsadmin users can change user type.""" + self.api.set_token(self.rodsadmin_bearer_token) + + # Create a new user. + new_username = "test_user_rodsuser" + user_type = "rodsuser" + r = self.api.users_groups.create_user(new_username, self.zone_name, user_type) + self.assertEqual(r["status_code"], 200) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Show that a rodsadmin can change the type of the new user. + new_user_type = "groupadmin" + r = self.api.users_groups.set_user_type(new_username, self.zone_name, new_user_type) + self.assertEqual(r["status_code"], 200) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Show that a non-admin cannot change the type of the new user. + self.api.set_token(self.rodsuser_bearer_token) + r = self.api.users_groups.set_user_type(new_user_type, self.zone_name, new_user_type) + self.assertEqual(r["status_code"], 200) + self.assertEqual(r["data"]["irods_response"]["status_code"], -13000) + + # Show that the user type matches the type set by the rodsadmin. + r = self.api.users_groups.stat(new_username, self.zone_name) + self.assertEqual(r["status_code"], 200) + + stat_info = r["data"] + self.assertEqual(stat_info["irods_response"]["status_code"], 0) + self.assertEqual(stat_info["exists"], True) + self.assertEqual(stat_info["local_unique_name"], f"{new_username}#{self.zone_name}") + self.assertEqual(stat_info["type"], new_user_type) + + # Remove the user. + self.api.set_token(self.rodsadmin_bearer_token) + r = self.api.users_groups.remove_user(new_username, self.zone_name) + self.assertEqual(r["status_code"], 200) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + def test_listing_all_users_in_zone(self): + """Test listing all users in the zone.""" + self.api.set_token(self.rodsuser_bearer_token) + + r = self.api.users_groups.users() + self.assertEqual(r["status_code"], 200) + result = r["data"] + self.assertEqual(result["irods_response"]["status_code"], 0) + self.assertIn({"name": self.rodsadmin_username, "zone": self.zone_name}, result["users"]) + self.assertIn({"name": self.rodsuser_username, "zone": self.zone_name}, result["users"]) + + def test_listing_all_groups_in_zone(self): + """Test listing all groups in the zone.""" + self.api.set_token(self.rodsadmin_bearer_token) + + # Create a new group. + new_group = "test_group" + r = self.api.users_groups.create_group(new_group) + self.assertEqual(r["status_code"], 200) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + self.api.set_token(self.rodsuser_bearer_token) + # Get all groups. + r = self.api.users_groups.groups() + self.assertEqual(r["status_code"], 200) + result = r["data"] + self.assertEqual(result["irods_response"]["status_code"], 0) + self.assertIn("public", result["groups"]) + self.assertIn(new_group, result["groups"]) + + self.api.set_token(self.rodsadmin_bearer_token) + # Remove the new group. + r = self.api.users_groups.remove_group(new_group) + self.assertEqual(r["status_code"], 200) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + def test_modifying_metadata_atomically(self): + """Test atomically modifying user metadata.""" + self.api.set_token(self.rodsadmin_bearer_token) + username = self.rodsuser_username + + # Add metadata to the user. + ops = [{"operation": "add", "attribute": "a1", "value": "v1", "units": "u1"}] + r = self.api.users_groups.modify_metadata(username, ops) + self.assertEqual(r["status_code"], 200) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Show the metadata exists on the user. + r = self.api.queries.execute_genquery( + "select USER_NAME where META_USER_ATTR_NAME = 'a1' and " + "META_USER_ATTR_VALUE = 'v1' and META_USER_ATTR_UNITS = 'u1'" + ) + self.assertEqual(r["status_code"], 200) + + result = r["data"] + self.assertEqual(result["irods_response"]["status_code"], 0) + self.assertEqual(result["rows"][0][0], username) + + # Remove the metadata from the user. + ops = [{"operation": "remove", "attribute": "a1", "value": "v1", "units": "u1"}] + r = self.api.users_groups.modify_metadata(username, ops) + self.assertEqual(r["status_code"], 200) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Show the metadata no longer exists on the user. + r = self.api.queries.execute_genquery( + "select USER_NAME where META_USER_ATTR_NAME = 'a1' and " + "META_USER_ATTR_VALUE = 'v1' and META_USER_ATTR_UNITS = 'u1'" + ) + self.assertEqual(r["status_code"], 200) + + result = r["data"] + self.assertEqual(result["irods_response"]["status_code"], 0) + self.assertEqual(len(result["rows"]), 0) + # Tests for zone operations -class zoneTests(unittest.TestCase): - @classmethod - def setUpClass(cls): - setup_class(cls, {'endpoint_name': 'zones'}) - - @classmethod - def tearDownClass(cls): - tear_down_class(cls) - - def setUp(self): - self.assertFalse(self._class_init_error, 'Class initialization failed. Cannot continue.') - - - def test_report_operation(self): - self.api.setToken(self.rodsadmin_bearer_token) - r = self.api.zones.report() - self.assertEqual(r['status_code'], 200) - - result = r['data'] - self.assertEqual(result['irods_response']['status_code'], 0) - - zone_report = result['zone_report'] - self.assertIn('schema_version', zone_report) - self.assertIn('zones', zone_report) - self.assertGreaterEqual(len(zone_report['zones']), 1) - - - def test_adding_removing_and_modifying_zones(self): - self.api.setToken(self.rodsadmin_bearer_token) - - # Add a remote zone to the local zone. - # The new zone will not have any connection information or anything else. - zone_name = 'other_zone' - r = self.api.zones.add(zone_name) - self.assertEqual(r['status_code'], 200) - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - try: - # Show the new zone exists by executing the stat operation on it. - r = self.api.zones.stat(zone_name) - self.assertEqual(r['status_code'], 200) - - result = r['data'] - self.assertEqual(result['irods_response']['status_code'], 0) - self.assertEqual(result['exists'], True) - self.assertEqual(result['info']['name'], zone_name) - self.assertEqual(result['info']['type'], 'remote') - self.assertEqual(result['info']['connection_info'], '') - self.assertEqual(result['info']['comment'], '') - - # The properties to update. - property_map = [ - ('name', 'other_zone_renamed'), - ('connection_info', 'example.org:1247'), - ('comment', 'updated comment') - ] - - # Change the properties of the new zone. - for p, v in property_map: - with self.subTest(f'Setting property [{p}] to value [{v}]'): - r = self.api.zones.modify(zone_name, p, v) - self.assertEqual(r['status_code'], 200) - self.assertEqual(r['data']['irods_response']['status_code'], 0) - - # Capture the new name of the zone following its renaming. - if 'name' == p: - zone_name = v - - # Show the new zone was modified successfully. - r = self.api.zones.stat(zone_name) - self.assertEqual(r['status_code'], 200) - - result = r['data'] - self.assertEqual(result['irods_response']['status_code'], 0) - self.assertEqual(result['exists'], True) - self.assertEqual(result['info'][p], v) - - finally: - # Remove the remote zone. - r = self.api.zones.remove(zone_name) - self.assertEqual(r['status_code'], 200) - -if __name__ == '__main__': - unittest.main() \ No newline at end of file +class ZoneTests(unittest.TestCase): + """Test iRODS zone operations.""" + + @classmethod + def setUpClass(cls): + """Set up class-level resources for zone tests.""" + setup_class(cls, {"endpoint_name": "zones"}) + + @classmethod + def tearDownClass(cls): + """Tear down class-level resources.""" + tear_down_class(cls) + + def setUp(self): + """Check that class initialization succeeded before each test.""" + self.assertFalse(self._class_init_error, "Class initialization failed. Cannot continue.") + + def test_report_operation(self): + """Test the zone report operation.""" + self.api.set_token(self.rodsadmin_bearer_token) + r = self.api.zones.report() + self.assertEqual(r["status_code"], 200) + + result = r["data"] + self.assertEqual(result["irods_response"]["status_code"], 0) + + zone_report = result["zone_report"] + self.assertIn("zones", zone_report) + self.assertGreaterEqual(len(zone_report["zones"]), 1) + self.assertIn("schema_version", zone_report["zones"][0]["servers"][0]["server_config"]) + + def test_adding_removing_and_modifying_zones(self): + """Test adding, removing, and modifying zones.""" + self.api.set_token(self.rodsadmin_bearer_token) + + # Add a remote zone to the local zone. + # The new zone will not have any connection information or anything else. + zone_name = "other_zone" + r = self.api.zones.add(zone_name) + self.assertEqual(r["status_code"], 200) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + try: + # Show the new zone exists by executing the stat operation on it. + r = self.api.zones.stat(zone_name) + self.assertEqual(r["status_code"], 200) + + result = r["data"] + self.assertEqual(result["irods_response"]["status_code"], 0) + self.assertEqual(result["exists"], True) + self.assertEqual(result["info"]["name"], zone_name) + self.assertEqual(result["info"]["type"], "remote") + self.assertEqual(result["info"]["connection_info"], "") + self.assertEqual(result["info"]["comment"], "") + + # The properties to update. + property_map = [ + ("name", "other_zone_renamed"), + ("connection_info", "example.org:1247"), + ("comment", "updated comment"), + ] + + # Change the properties of the new zone. + for p, v in property_map: + with self.subTest(f"Setting property [{p}] to value [{v}]"): + r = self.api.zones.modify(zone_name, p, v) + self.assertEqual(r["status_code"], 200) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Capture the new name of the zone following its renaming. + if p == "name": + zone_name = v + + # Show the new zone was modified successfully. + r = self.api.zones.stat(zone_name) + self.assertEqual(r["status_code"], 200) + + result = r["data"] + self.assertEqual(result["irods_response"]["status_code"], 0) + self.assertEqual(result["exists"], True) + self.assertEqual(result["info"][p], v) + + finally: + # Remove the remote zone. + r = self.api.zones.remove(zone_name) + self.assertEqual(r["status_code"], 200) + + +if __name__ == "__main__": + unittest.main()