diff --git a/README.md b/README.md index 3efad86..974c6e1 100644 --- a/README.md +++ b/README.md @@ -9,34 +9,27 @@ Documentation for the endpoint operations can be found [here](https://github.com This wrapper is available via pip: ``` -pip install irods-http-client +pip install irods-http ``` ## Usage To use the wrapper, follow the steps listed below. ```py -from irods_http_client import IRODSHTTPClient +import irods_http -# 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 = IRODSHTTPClient('http://:/irods-http-api/') +# Placeholder values needed for irods_http.authenticate() +url_base = "http://:/irods-http-api/" +username = "" +password = "" -# Most endpoint operations require a user to be authenticated in order to -# be executed. Authenticate with a username and password, and store the -# token received. -token = api.authenticate('', '') +# Create an IRODSHTTPSession to an iRODS HTTP API server +session = irods_http.authenticate(url_base, username, password) -# When calling authenticate for the first time on a new instance, the token -# will be automatically set. To change the token to use operations as a -# different user, use `setToken()`. -api.setToken(token) +# Use the session for all other operations +response = irods_http.collections.create(session, '//home//new_collection') -# Once a token is set, the rest of the operations can be used. -response = api.collections.create('//home//new_collection') - -# After executing the operation, the iRODS response data can be accessed like this. +# Check the resopnse for errors if response['status_code'] != 200: # Handle HTTP error. @@ -44,7 +37,7 @@ if response['data']['irods_response']['status_code'] < 0: # Handle iRODS error. ``` -The data returned by the wrapper will be in this format: +The response dict will have this format: ```py { 'status_code': , @@ -53,7 +46,8 @@ The data returned by the wrapper will be in this format: ``` where `status_code` is the HTTP status code from the response, and `data` is the result of the iRODS operation. -If there is data returned by the iRODS server, it will contain a dictionary called `irods_response`, which has an additional `status_code` indicating the result of the operation on the servers side, as well as any other expected data if the operation was successful. +`response['data']` will contain a dict named `irods_response`, which will contain the `status_code` returned by the iRODS Server as well as any other expected properties. + ```py { 'irods_response': { @@ -63,4 +57,6 @@ If there is data returned by the iRODS server, it will contain a dictionary call } ``` -More information regarding iRODS response data is available [here](https://github.com/irods/irods_client_http_api/blob/main/API.md). +When calling `data_objects.read()`, the `response['data']` will contain the raw bytes instead of a dict. + +More information regarding iRODS HTTP API response data is available [here](https://github.com/irods/irods_client_http_api/blob/main/API.md). diff --git a/irods_http/__init__.py b/irods_http/__init__.py new file mode 100644 index 0000000..c16a82b --- /dev/null +++ b/irods_http/__init__.py @@ -0,0 +1,31 @@ +"""iRODS HTTP client library for Python.""" + +from . import ( + collections, + data_objects, + queries, + resources, + rules, + tickets, + users_groups, + zones, +) +from .irods_http import ( + IRODSHTTPSession, + authenticate, + get_server_info, +) + +__all__ = [ + "IRODSHTTPSession", + "authenticate", + "collections", + "data_objects", + "get_server_info", + "queries", + "resources", + "rules", + "tickets", + "users_groups", + "zones", +] diff --git a/irods_http/collections.py b/irods_http/collections.py new file mode 100644 index 0000000..73178e5 --- /dev/null +++ b/irods_http/collections.py @@ -0,0 +1,307 @@ +"""Collection operations for iRODS HTTP API.""" + +import builtins +import json + +import requests + +from . import common +from .irods_http import IRODSHTTPSession # noqa: TC001 + + +def create(session: IRODSHTTPSession, lpath: str, create_intermediates: int = 0) -> dict: + """ + Create a new collection. + + Args: + session: IRODSHTTPSession object containing the base URL and authentication token. + 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.validate_not_none(session.token) + common.validate_instance(lpath, str) + common.validate_0_or_1(create_intermediates) + + data = { + "op": "create", + "lpath": lpath, + "create-intermediates": create_intermediates, + } + + r = requests.post(session.url_base + "/collections", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) + + +def remove(session: IRODSHTTPSession, lpath: str, recurse: int = 0, no_trash: int = 0) -> dict: + """ + Remove an existing collection. + + Args: + session: IRODSHTTPSession object containing the base URL and authentication token. + 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 permanently remove, 0 to move to trash. 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.validate_not_none(session.token) + common.validate_instance(lpath, str) + common.validate_0_or_1(recurse) + common.validate_0_or_1(no_trash) + + data = { + "op": "remove", + "lpath": lpath, + "recurse": recurse, + "no-trash": no_trash, + } + + r = requests.post(session.url_base + "/collections", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) + + +def stat(session: IRODSHTTPSession, lpath: str, ticket: str = "") -> dict: + """ + Give information about a collection. + + Args: + session: IRODSHTTPSession object containing the base URL and authentication token. + 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.validate_not_none(session.token) + common.validate_instance(lpath, str) + common.validate_instance(ticket, str) + + params = {"op": "stat", "lpath": lpath, "ticket": ticket} + + r = requests.get(session.url_base + "/collections", params=params, headers=session.get_headers) # noqa: S113 + return common.process_response(r) + + +def list(session: IRODSHTTPSession, lpath: str, recurse: int = 0, ticket: str = "") -> dict: # noqa: A001 + """ + Show the contents of a collection. + + Args: + session: IRODSHTTPSession object containing the base URL and authentication token. + 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.validate_not_none(session.token) + common.validate_instance(lpath, str) + common.validate_0_or_1(recurse) + common.validate_instance(ticket, str) + + params = {"op": "list", "lpath": lpath, "recurse": recurse, "ticket": ticket} + + r = requests.get(session.url_base + "/collections", params=params, headers=session.get_headers) # noqa: S113 + return common.process_response(r) + + +def set_permission( + session: IRODSHTTPSession, + lpath: str, + entity_name: str, + permission: str, + admin: int = 0, +) -> dict: + """ + Set the permission of a user for a given collection. + + Args: + session: IRODSHTTPSession object containing the base URL and authentication token. + 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.validate_not_none(session.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) + + data = { + "op": "set_permission", + "lpath": lpath, + "entity-name": entity_name, + "permission": permission, + "admin": admin, + } + + r = requests.post(session.url_base + "/collections", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) + + +def set_inheritance(session: IRODSHTTPSession, lpath: str, enable: int, admin: int = 0) -> dict: + """ + Set the inheritance for a collection. + + Args: + session: IRODSHTTPSession object containing the base URL and authentication token. + 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.validate_not_none(session.token) + common.validate_instance(lpath, str) + common.validate_0_or_1(enable) + common.validate_0_or_1(admin) + + data = { + "op": "set_inheritance", + "lpath": lpath, + "enable": enable, + "admin": admin, + } + + r = requests.post(session.url_base + "/collections", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) + + +def modify_permissions(session: IRODSHTTPSession, lpath: str, operations: dict, admin: int = 0) -> dict: + """ + Modify permissions for multiple users or groups for a collection. + + Args: + session: IRODSHTTPSession object containing the base URL and authentication token. + 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.validate_not_none(session.token) + common.validate_instance(lpath, str) + common.validate_instance(operations, builtins.list) + common.validate_instance(operations[0], dict) + common.validate_0_or_1(admin) + + data = { + "op": "modify_permissions", + "lpath": lpath, + "operations": json.dumps(operations), + "admin": admin, + } + + r = requests.post(session.url_base + "/collections", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) + + +def modify_metadata(session: IRODSHTTPSession, lpath: str, operations: dict, admin: int = 0) -> dict: + """ + Modify the metadata for a collection. + + Args: + session: IRODSHTTPSession object containing the base URL and authentication token. + 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.validate_not_none(session.token) + common.validate_instance(lpath, str) + common.validate_instance(operations, builtins.list) + common.validate_instance(operations[0], dict) + common.validate_0_or_1(admin) + + data = { + "op": "modify_metadata", + "lpath": lpath, + "operations": json.dumps(operations), + "admin": admin, + } + + r = requests.post(session.url_base + "/collections", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) + + +def rename(session: IRODSHTTPSession, old_lpath: str, new_lpath: str) -> dict: + """ + Rename or move a collection. + + Args: + session: IRODSHTTPSession object containing the base URL and authentication token. + 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.validate_not_none(session.token) + common.validate_instance(old_lpath, str) + common.validate_instance(new_lpath, str) + + data = {"op": "rename", "old-lpath": old_lpath, "new-lpath": new_lpath} + + r = requests.post(session.url_base + "/collections", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) + + +def touch(session: IRODSHTTPSession, lpath: str, seconds_since_epoch: int = -1, reference: str = "") -> dict: + """ + Update mtime for a collection. + + Args: + session: IRODSHTTPSession object containing the base URL and authentication token. + 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.validate_not_none(session.token) + common.validate_instance(lpath, str) + common.validate_gte_minus1(seconds_since_epoch) + common.validate_instance(reference, str) + + 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(session.url_base + "/collections", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) diff --git a/irods_http_client/common.py b/irods_http/common.py similarity index 77% rename from irods_http_client/common.py rename to irods_http/common.py index e24210a..dfefd11 100644 --- a/irods_http_client/common.py +++ b/irods_http/common.py @@ -16,18 +16,18 @@ def process_response(r): return {"status_code": r.status_code, "data": rdict} -def check_token(t): +def validate_not_none(x): """ - Verify that an authentication token is set. + Validate that a value is not None. Args: - t: The authentication token to check. + x: The value to validate. Raises: - RuntimeError: If the token is None. + ValueError: If x is None """ - if t is None: - raise RuntimeError("No token set. Use setToken() to set the auth token to be used.") + if x is None: + raise ValueError def validate_instance(x, expected_type): @@ -91,3 +91,17 @@ def validate_gte_minus1(x): validate_instance(x, int) if not x >= -1: raise ValueError(f"{x} must be >= 0, or flag value of -1") + + +def assert_success(cls, response): + """ + Validate HTTP and iRODS status codes are successes. + + Args: + cls: The unittest.TestCase class + response: The response from the iRODS HTTP API request + """ + # HTTP status code + cls.assertEqual(response["status_code"], 200) + # iRODS status code + cls.assertEqual(response["data"]["irods_response"]["status_code"], 0) diff --git a/irods_http/data_objects.py b/irods_http/data_objects.py new file mode 100644 index 0000000..26b71a9 --- /dev/null +++ b/irods_http/data_objects.py @@ -0,0 +1,843 @@ +"""Data object operations for iRODS HTTP API.""" + +import json + +import requests + +from . import common +from .irods_http import IRODSHTTPSession # noqa: TC001 + + +def touch( + session: IRODSHTTPSession, + lpath: str, + no_create: int = 0, + replica_number: int = -1, + leaf_resources: str = "", + seconds_since_epoch: int = -1, + reference: str = "", +) -> dict: + """ + Update mtime for an existing data object or create a new one. + + Args: + session: IRODSHTTPSession object containing base URL and authentication token. + 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. Defaults to 0. + replica_number: The replica number of the target replica. Defaults to -1. + leaf_resources: The resource holding an existing replica. If one does not exist, creates one. + Defaults to "". + seconds_since_epoch: The value to set mtime to, defaults to -1 as a flag. Defaults to -1. + reference: The absolute logical path of the data object to use as a reference for mtime. + Defaults 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.validate_not_none(session.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) + + 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(session.url_base + "/data-objects", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) + + +def remove(session: IRODSHTTPSession, lpath: str, catalog_only: int = 0, no_trash: int = 0, admin: int = 0) -> dict: + """ + Remove an existing data object. + + Args: + session: IRODSHTTPSession object containing base URL and authentication token. + 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.validate_not_none(session.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) + + data = { + "op": "remove", + "lpath": lpath, + "catalog-only": catalog_only, + "no-trash": no_trash, + "admin": admin, + } + + r = requests.post(session.url_base + "/data-objects", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) + + +def calculate_checksum( + session: IRODSHTTPSession, + lpath: str, + resource: str = "", + replica_number: int = -1, + force: int = 0, + all: int = 0, # noqa: A002 + admin: int = 0, +) -> dict: + """ + Calculate the checksum for a data object. + + Args: + session: IRODSHTTPSession object containing base URL and authentication token. + lpath: The absolute logical path of the data object to have its checksum calculated. + resource: The resource holding the existing replica. Defaults to "". + replica_number: The replica number of the target replica. Defaults to -1. + 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.validate_not_none(session.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) + + 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(session.url_base + "/data-objects", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) + + +def verify_checksum( + session: IRODSHTTPSession, + lpath: str, + resource: str = "", + replica_number: int = -1, + compute_checksums: int = 0, + admin: int = 0, +) -> dict: + """ + Verify the checksum for a data object. + + Args: + session: IRODSHTTPSession object containing base URL and authentication token. + lpath: The absolute logical path of the data object to have its checksum verified. + resource: The resource holding the existing replica. Defaults to "". + replica_number: The replica number of the target replica. Defaults to -1. + 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.validate_not_none(session.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) + + 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(session.url_base + "/data-objects", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) + + +def stat(session: IRODSHTTPSession, lpath: str, ticket: str = "") -> dict: + """ + Give information about a data object. + + Args: + session: IRODSHTTPSession object containing base URL and authentication token. + 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.validate_not_none(session.token) + common.validate_instance(lpath, str) + common.validate_instance(ticket, str) + + params = {"op": "stat", "lpath": lpath} + + if ticket != "": + params["ticket"] = ticket + + r = requests.get(session.url_base + "/data-objects", params=params, headers=session.get_headers) # noqa: S113 + return common.process_response(r) + + +def rename(session: IRODSHTTPSession, old_lpath: str, new_lpath: str) -> dict: + """ + Rename or move a data object. + + Args: + session: IRODSHTTPSession object containing base URL and authentication token. + 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.validate_not_none(session.token) + common.validate_instance(old_lpath, str) + common.validate_instance(new_lpath, str) + + data = {"op": "rename", "old-lpath": old_lpath, "new-lpath": new_lpath} + + r = requests.post(session.url_base + "/data-objects", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) + + +def copy( + session: IRODSHTTPSession, + src_lpath: str, + dst_lpath: str, + src_resource: str = "", + dst_resource: str = "", + overwrite: int = 0, +) -> dict: + """ + Copy a data object. + + Args: + session: IRODSHTTPSession object containing base URL and authentication token. + 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. Defaults to "". + dst_resource: The name of the destination resource. Defaults to "". + 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.validate_not_none(session.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) + + 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(session.url_base + "/data-objects", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) + + +def replicate( + session: IRODSHTTPSession, + lpath: str, + src_resource: str = "", + dst_resource: str = "", + admin: int = 0, +) -> dict: + """ + Replicates a data object from one resource to another. + + Args: + session: IRODSHTTPSession object containing base URL and authentication token. + lpath: The absolute logical path of the data object to be replicated. + src_resource: The name of the source resource. Defaults to "". + dst_resource: The name of the destination resource. Defaults to "". + 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.validate_not_none(session.token) + common.validate_instance(lpath, str) + common.validate_instance(src_resource, str) + common.validate_instance(dst_resource, str) + common.validate_0_or_1(admin) + + 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(session.url_base + "/data-objects", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) + + +def trim(session: IRODSHTTPSession, lpath: str, replica_number: int, catalog_only: int = 0, admin: int = 0) -> dict: + """ + Trims an existing replica or removes its catalog entry. + + Args: + session: IRODSHTTPSession object containing base URL and authentication token. + 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.validate_not_none(session.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) + + data = { + "op": "trim", + "lpath": lpath, + "replica-number": replica_number, + "catalog-only": catalog_only, + "admin": admin, + } + + r = requests.post(session.url_base + "/data-objects", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) + + +def register( + session: IRODSHTTPSession, + lpath: str, + ppath: str, + resource: str, + as_additional_replica: int = 0, + data_size: int = -1, + checksum: int = 0, +) -> dict: + """ + Register a data object/replica into the catalog. + + Args: + session: IRODSHTTPSession object containing base URL and authentication token. + 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. Defaults to -1. + checksum: Set to 1 to register with a checksum. 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.validate_not_none(session.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_0_or_1(checksum) + + data = { + "op": "register", + "lpath": lpath, + "ppath": ppath, + "resource": resource, + "as_additional_replica": as_additional_replica, + "checksum": checksum, + } + + if data_size != -1: + data["data-size"] = data_size + + r = requests.post(session.url_base + "/data-objects", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) + + +def read(session: IRODSHTTPSession, lpath: str, offset: int = 0, count: int = -1, ticket: str = "") -> dict: + """ + Read bytes from a data object. + + Args: + session: IRODSHTTPSession object containing base URL and authentication token. + 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. Defaults to -1. + 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.validate_not_none(session.token) + common.validate_instance(lpath, str) + common.validate_instance(offset, int) + common.validate_gte_minus1(count) + common.validate_instance(ticket, str) + + params = {"op": "read", "lpath": lpath, "offset": offset} + + if count != -1: + params["count"] = count + + if ticket != "": + params["ticket"] = ticket + + r = requests.get(session.url_base + "/data-objects", params=params, headers=session.get_headers) # noqa: S113 + # this is the only payload that is different from common.process_response() + return {'status_code': r.status_code, 'data': r.content} + + +def write( + session: IRODSHTTPSession, + bytes, # noqa: A002 + lpath: str = "", + resource: str = "", + offset: int = 0, + truncate: int = 1, + append: int = 0, + parallel_write_handle: str = "", + stream_index: int = -1, + ticket: str = "", +) -> dict: + """ + Write bytes to a data object. + + Args: + session: IRODSHTTPSession object containing base URL and authentication token. + bytes: The bytes to be written. + lpath: The absolute logical path of the data object to be written to. Defaults to "". + resource: The root resource to write to. Defaults 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. Defaults to "". + stream_index: The stream to use when writing in parallel. Defaults to -1. + 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. + + Raises: + TypeError: If bytes is not bytes or str. + ValueError: If bytes length is less than 0. + """ + common.validate_not_none(session.token) + if type(bytes) not in [bytes, str]: + raise TypeError("type(bytes) must be 'bytes' or 'str'") + 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) + common.validate_instance(ticket, str) + + 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 + + if ticket != "": + data["ticket"] = ticket + + r = requests.post(session.url_base + "/data-objects", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) + + +def parallel_write_init( + session: IRODSHTTPSession, + lpath: str, + stream_count: int, + truncate: int = 1, + append: int = 0, + ticket: str = "", +) -> dict: + """ + Initialize server-side state for parallel writing. + + Args: + session: IRODSHTTPSession object containing base URL and authentication token. + 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.validate_not_none(session.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) + + data = { + "op": "parallel_write_init", + "lpath": lpath, + "stream-count": stream_count, + "truncate": truncate, + "append": append, + } + + if ticket != "": + data["ticket"] = ticket + + r = requests.post(session.url_base + "/data-objects", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) + + +def parallel_write_shutdown(session: IRODSHTTPSession, parallel_write_handle: str) -> dict: + """ + Shuts down the parallel write state in the server. + + Args: + session: IRODSHTTPSession object containing base URL and authentication token. + 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.validate_not_none(session.token) + common.validate_instance(parallel_write_handle, str) + + data = { + "op": "parallel_write_shutdown", + "parallel-write-handle": parallel_write_handle, + } + + r = requests.post(session.url_base + "/data-objects", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) + + +def modify_metadata(session: IRODSHTTPSession, lpath: str, operations: list, admin: int = 0) -> dict: + """ + Modify the metadata for a data object. + + Args: + session: IRODSHTTPSession object containing base URL and authentication token. + 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.validate_not_none(session.token) + common.validate_instance(lpath, str) + common.validate_instance(operations, list) + common.validate_instance(operations[0], dict) + common.validate_0_or_1(admin) + + data = { + "op": "modify_metadata", + "lpath": lpath, + "operations": json.dumps(operations), + "admin": admin, + } + + r = requests.post(session.url_base + "/data-objects", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) + + +def set_permission(session: IRODSHTTPSession, lpath: str, entity_name: str, permission: str, admin: int = 0) -> dict: + """ + Set the permission of a user for a given data object. + + Args: + session: IRODSHTTPSession object containing base URL and authentication token. + 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.validate_not_none(session.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) + + data = { + "op": "set_permission", + "lpath": lpath, + "entity-name": entity_name, + "permission": permission, + "admin": admin, + } + + r = requests.post(session.url_base + "/data-objects", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) + + +def modify_permissions(session: IRODSHTTPSession, lpath: str, operations: list, admin: int = 0) -> dict: + """ + Modify permissions for multiple users or groups for a data object. + + Args: + session: IRODSHTTPSession object containing base URL and authentication token. + 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.validate_not_none(session.token) + common.validate_instance(lpath, str) + common.validate_instance(operations, list) + common.validate_instance(operations[0], dict) + common.validate_0_or_1(admin) + + data = { + "op": "modify_permissions", + "lpath": lpath, + "operations": json.dumps(operations), + "admin": admin, + } + + r = requests.post(session.url_base + "/data-objects", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) + + +def modify_replica( + session: IRODSHTTPSession, + lpath: str, + resource_hierarchy: str = "", + replica_number: int = -1, + new_data_checksum: str = "", + new_data_comments: str = "", + new_data_create_time: str = "", + new_data_expiry: str = "", + 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, +) -> dict: + """ + 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: + session: IRODSHTTPSession object containing base URL and authentication token. + lpath: The absolute logical path of the data object to have a replica modified. + resource_hierarchy: The hierarchy containing the resource to be modified. Defaults to "". + Mutually exclusive with replica_number. + replica_number: The number of the replica to be modified. Defaults to -1. Mutually exclusive with + resource_hierarchy. + new_data_checksum: The new checksum to be set. Defaults to "". + new_data_comments: The new comments to be set. Defaults to "". + new_data_create_time: The new create time to be set. Defaults to "". + new_data_expiry: The new expiry to be set. Defaults to "". + new_data_mode: The new mode to be set. Defaults to "". + new_data_modify_time: The new modify time to be set. Defaults to "". + new_data_path: The new path to be set. Defaults to "". + new_data_replica_number: The new replica number to be set. Defaults to -1. + new_data_replica_status: The new replica status to be set. Defaults to -1. + new_data_resource_id: The new resource id to be set. Defaults to -1. + new_data_size: The new size to be set. Defaults to -1. + new_data_status: The new data status to be set. Defaults to "". + new_data_type_name: The new type name to be set. Defaults to "". + new_data_version: The new version to be set. Defaults to -1. + + 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.validate_not_none(session.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_instance(new_data_create_time, str) + common.validate_instance(new_data_expiry, str) + 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) + + 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 != "": + data["new-data-create-time"] = new_data_create_time + no_params = False + + if new_data_expiry != "": + 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 != -1: + 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(session.url_base + "/data-objects", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) diff --git a/irods_http/irods_http.py b/irods_http/irods_http.py new file mode 100644 index 0000000..3517aca --- /dev/null +++ b/irods_http/irods_http.py @@ -0,0 +1,104 @@ +"""Main module for iRODS HTTP API interactions.""" + +import requests + +from irods_http import common + + +class IRODSHTTPSession: + """ + Encapsulates HTTP session details for iRODS HTTP API. + + This class binds together the base URL and authentication token that are + always used together in API calls. + + Attributes: + url_base: The base URL for the iRODS HTTP API. + token: The authentication token for the API. + """ + + def __init__(self, url_base: str, token: str): + """ + Initialize IRODSHTTPSession with URL and token. + + Args: + url_base: The base URL for the iRODS HTTP API. + token: The authentication token for the API. + """ + self.url_base = url_base + self.token = token + + self.get_headers = { + "Authorization": "Bearer " + self.token, + } + + self.post_headers = { + "Authorization": "Bearer " + self.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + +def authenticate(url_base: str, username: str, password: str) -> IRODSHTTPSession: + """ + Authenticate using basic authentication credentials. + + Makes a POST request to {url_base}/authenticate with HTTP basic auth + using the provided username and password. + + Args: + url_base: The base URL of the iRODS HTTP API server (e.g., "http://localhost:8080"). + username: The username for authentication. Must be a non-empty string. + password: The password for authentication. Must be a string. + + Returns: + An IRODSHTTPSession containing a token string that can be used for subsequent authenticated requests. + + Raises: + TypeError: If username or password are not strings. + ValueError: If username is empty. + RuntimeError: If authentication fails (non-2xx response status). + + Example: + >>> session = authenticate("http://localhost:8080", "user", "pass") + >>> print(session.token) + 'eae9c...' + """ + common.validate_instance(username, str) + common.validate_instance(password, str) + if not username: + raise ValueError("username cannot be empty") + + try: + r = requests.post(f"{url_base}/authenticate", auth=(username, password)) # noqa: S113 + + # Check for success status code (2xx) + if 200 <= r.status_code < 300: # noqa: PLR2004 + return IRODSHTTPSession(url_base, r.text) + + # Handle error status codes + error_msg = f"Authentication failed with status {r.status_code}" + if r.text: + error_msg += f": {r.text}" + raise RuntimeError(error_msg) + + except requests.exceptions.RequestException as e: + raise RuntimeError(f"Authentication request failed: {e!s}") from e + + +def get_server_info(session: IRODSHTTPSession): + """ + Get general information about the iRODS server. + + Args: + session: An IRODSHTTPSession instance. + + 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 " + session.token, + } + + r = requests.get(session.url_base + "/info", headers=headers) # noqa: S113 + return common.process_response(r) diff --git a/irods_http/queries.py b/irods_http/queries.py new file mode 100644 index 0000000..0dab7c8 --- /dev/null +++ b/irods_http/queries.py @@ -0,0 +1,163 @@ +"""Query operations for iRODS HTTP API.""" + +import requests + +from . import common +from .irods_http import IRODSHTTPSession # noqa: TC001 + + +def execute_genquery( + session: IRODSHTTPSession, + 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: + session: An IRODSHTTPSession instance. + 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.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) + + 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(session.url_base + "/query", headers=session.get_headers, params=params) # noqa: S113 + return common.process_response(r) + + +def execute_specific_query( + session: IRODSHTTPSession, + name: str, + args: str = "", + args_delimiter: str = ",", + offset: int = 0, + count: int = -1, +): + """ + Execute a specific query and returns the results. + + Args: + session: An IRODSHTTPSession instance. + 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.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) + + 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(session.url_base + "/query", headers=session.get_headers, params=params) # noqa: S113 + return common.process_response(r) + + +def add_specific_query(session: IRODSHTTPSession, name: str, sql: str): + """ + Add a SpecificQuery to the iRODS zone. + + Args: + session: An IRODSHTTPSession instance. + 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.validate_instance(name, str) + common.validate_instance(sql, str) + + data = {"op": "add_specific_query", "name": name, "sql": sql} + + r = requests.post(session.url_base + "/query", headers=session.get_headers, data=data) # noqa: S113 + return common.process_response(r) + + +def remove_specific_query(session: IRODSHTTPSession, name: str): + """ + Remove a SpecificQuery from the iRODS zone. + + Args: + session: An IRODSHTTPSession instance. + 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.validate_instance(name, str) + + data = {"op": "remove_specific_query", "name": name} + + r = requests.post(session.url_base + "/query", headers=session.get_headers, data=data) # noqa: S113 + return common.process_response(r) diff --git a/irods_http/resources.py b/irods_http/resources.py new file mode 100644 index 0000000..e2730c9 --- /dev/null +++ b/irods_http/resources.py @@ -0,0 +1,236 @@ +"""Resource operations for iRODS HTTP API.""" + +import json + +import requests + +from . import common +from .irods_http import IRODSHTTPSession # noqa: TC001 + + +def create(session: IRODSHTTPSession, name: str, type: str, host: str, vault_path: str, context: str): # noqa: A002 + """ + Create a new resource. + + Args: + session: An IRODSHTTPSession instance. + 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.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) + + 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(session.url_base + "/resources", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) + + +def remove(session: IRODSHTTPSession, name: str): + """ + Remove an existing resource. + + Args: + session: An IRODSHTTPSession instance. + 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.validate_instance(name, str) + + data = {"op": "remove", "name": name} + + r = requests.post(session.url_base + "/resources", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) + + +def modify(session: IRODSHTTPSession, name: str, property: str, value: str): # noqa: A002 + """ + Modify a property for a resource. + + Args: + session: An IRODSHTTPSession instance. + 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.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'") + + data = {"op": "modify", "name": name, "property": property, "value": value} + + r = requests.post(session.url_base + "/resources", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) + + +def add_child(session: IRODSHTTPSession, parent_name: str, child_name: str, context: str = ""): + """ + Create a parent-child relationship between two resources. + + Args: + session: An IRODSHTTPSession instance. + parent_name: The name of the parent resource. + child_name: The name of the child resource. + context: Additional information for the parent-child relationship. + + 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.validate_instance(parent_name, str) + common.validate_instance(child_name, str) + common.validate_instance(context, str) + + data = {"op": "add_child", "parent-name": parent_name, "child-name": child_name} + + if context != "": + data["context"] = context + + r = requests.post(session.url_base + "/resources", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) + + +def remove_child(session: IRODSHTTPSession, parent_name: str, child_name: str): + """ + Remove a parent-child relationship between two resources. + + Args: + session: An IRODSHTTPSession instance. + 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.validate_instance(parent_name, str) + common.validate_instance(child_name, str) + + data = { + "op": "remove_child", + "parent-name": parent_name, + "child-name": child_name, + } + + r = requests.post(session.url_base + "/resources", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) + + +def rebalance(session: IRODSHTTPSession, name: str): + """ + Rebalance a resource hierarchy. + + Args: + session: An IRODSHTTPSession instance. + 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.validate_instance(name, str) + + data = {"op": "rebalance", "name": name} + + r = requests.post(session.url_base + "/resources", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) + + +def stat(session: IRODSHTTPSession, name: str): + """ + Retrieve information for a resource. + + Args: + session: An IRODSHTTPSession instance. + 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.validate_instance(name, str) + + params = {"op": "stat", "name": name} + + r = requests.get(session.url_base + "/resources", headers=session.get_headers, params=params) # noqa: S113 + return common.process_response(r) + + +def modify_metadata(session: IRODSHTTPSession, name: str, operations: dict, admin: int = 0): + """ + Modify the metadata for a resource. + + Args: + session: An IRODSHTTPSession instance. + 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.validate_instance(name, str) + common.validate_instance(operations, list) + common.validate_instance(operations[0], dict) + common.validate_0_or_1(admin) + + data = { + "op": "modify_metadata", + "name": name, + "operations": json.dumps(operations), + "admin": admin, + } + + r = requests.post(session.url_base + "/resources", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) diff --git a/irods_http/rules.py b/irods_http/rules.py new file mode 100644 index 0000000..cf66ebc --- /dev/null +++ b/irods_http/rules.py @@ -0,0 +1,68 @@ +"""Rule operations for iRODS HTTP API.""" + +import requests + +from . import common +from .irods_http import IRODSHTTPSession # noqa: TC001 + + +def list_rule_engines(session: IRODSHTTPSession): + """ + List available rule engine plugin instances. + + Args: + session: An IRODSHTTPSession instance. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + params = {"op": "list_rule_engines"} + + r = requests.get(session.url_base + "/rules", params=params, headers=session.get_headers) # noqa: S113 + return common.process_response(r) + + +def execute(session: IRODSHTTPSession, rule_text: str, rep_instance: str = ""): + """ + Execute rule code. + + Args: + session: An IRODSHTTPSession instance. + 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.validate_instance(rule_text, str) + common.validate_instance(rep_instance, str) + + data = {"op": "execute", "rule-text": rule_text} + + if rep_instance != "": + data["rep-instance"] = rep_instance + + r = requests.post(session.url_base + "/rules", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) + + +def remove_delay_rule(session: IRODSHTTPSession, rule_id: int): + """ + Remove a delay rule from the catalog. + + Args: + session: An IRODSHTTPSession instance. + 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.validate_gte_zero(rule_id) + + data = {"op": "remove_delay_rule", "rule-id": rule_id} + + r = requests.post(session.url_base + "/rules", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) diff --git a/irods_http/tickets.py b/irods_http/tickets.py new file mode 100644 index 0000000..dded308 --- /dev/null +++ b/irods_http/tickets.py @@ -0,0 +1,93 @@ +"""Ticket operations for iRODS HTTP API.""" + +import requests + +from . import common +from .irods_http import IRODSHTTPSession # noqa: TC001 + + +def create( + session: IRODSHTTPSession, + lpath: str, + type: str = "read", # noqa: A002 + 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: + session: An IRODSHTTPSession instance. + 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.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) + + 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(session.url_base + "/tickets", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) + + +def remove(session: IRODSHTTPSession, name: str): + """ + Remove an existing ticket. + + Args: + session: An IRODSHTTPSession instance. + 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.validate_instance(name, str) + + data = {"op": "remove", "name": name} + + r = requests.post(session.url_base + "/tickets", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) diff --git a/irods_http/users_groups.py b/irods_http/users_groups.py new file mode 100644 index 0000000..5355a12 --- /dev/null +++ b/irods_http/users_groups.py @@ -0,0 +1,325 @@ +"""User and group operations for iRODS HTTP API.""" + +import json + +import requests + +from . import common +from .irods_http import IRODSHTTPSession # noqa: TC001 + + +def create_user(session: IRODSHTTPSession, name: str, zone: str, type: str = "rodsuser"): # noqa: A002 + """ + Create a new user. Requires rodsadmin or groupadmin privileges. + + Args: + session: An IRODSHTTPSession instance. + name: The name of the user to be created. + zone: The zone for the user to be created. + 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 type is not 'rodsuser', 'groupadmin', or 'rodsadmin'. + """ + common.validate_instance(name, str) + common.validate_instance(zone, str) + common.validate_instance(type, str) + if type not in ["rodsuser", "groupadmin", "rodsadmin"]: + raise ValueError("type must be set to rodsuser, groupadmin, or rodsadmin.") + + data = {"op": "create_user", "name": name, "zone": zone, "user-type": type} + + r = requests.post(session.url_base + "/users-groups", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) + + +def remove_user(session: IRODSHTTPSession, name: str, zone: str): + """ + Remove a user. Requires rodsadmin privileges. + + Args: + session: An IRODSHTTPSession instance. + 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.validate_instance(name, str) + common.validate_instance(zone, str) + + data = {"op": "remove_user", "name": name, "zone": zone} + + r = requests.post(session.url_base + "/users-groups", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) + + +def set_password(session: IRODSHTTPSession, name: str, zone: str, new_password: str = ""): + """ + Change a users password. Requires rodsadmin privileges. + + Args: + session: An IRODSHTTPSession instance. + 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.validate_instance(name, str) + common.validate_instance(zone, str) + common.validate_instance(new_password, str) + + data = { + "op": "set_password", + "name": name, + "zone": zone, + "new-password": new_password, + } + + r = requests.post(session.url_base + "/users-groups", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) + + +def set_user_type(session: IRODSHTTPSession, name: str, zone: str, type: str): # noqa: A002 + """ + Change a users type. Requires rodsadmin privileges. + + Args: + session: An IRODSHTTPSession instance. + name: The name of the user to have their type updated. + zone: The zone for the user to have their type updated. + 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.validate_instance(name, str) + common.validate_instance(zone, str) + common.validate_instance(type, str) + if type not in ["rodsuser", "groupadmin", "rodsadmin"]: + raise ValueError("type must be set to rodsuser, groupadmin, or rodsadmin.") + + data = { + "op": "set_user_type", + "name": name, + "zone": zone, + "new-user-type": type, + } + + r = requests.post(session.url_base + "/users-groups", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) + + +def create_group(session: IRODSHTTPSession, name: str): + """ + Create a new group. Requires rodsadmin or groupadmin privileges. + + Args: + session: An IRODSHTTPSession instance. + 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.validate_instance(name, str) + + data = {"op": "create_group", "name": name} + + r = requests.post(session.url_base + "/users-groups", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) + + +def remove_group(session: IRODSHTTPSession, name: str): + """ + Remove a group. Requires rodsadmin privileges. + + Args: + session: An IRODSHTTPSession instance. + 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.validate_instance(name, str) + + data = {"op": "remove_group", "name": name} + + r = requests.post(session.url_base + "/users-groups", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) + + +def add_to_group(session: IRODSHTTPSession, user: str, zone: str, group: str = ""): + """ + Add a user to a group. Requires rodsadmin or groupadmin privileges. + + Args: + session: An IRODSHTTPSession instance. + 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.validate_instance(user, str) + common.validate_instance(zone, str) + common.validate_instance(group, str) + + data = {"op": "add_to_group", "user": user, "zone": zone, "group": group} + + r = requests.post(session.url_base + "/users-groups", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) + + +def remove_from_group(session: IRODSHTTPSession, user: str, zone: str, group: str): + """ + Remove a user from a group. Requires rodsadmin or groupadmin privileges. + + Args: + session: An IRODSHTTPSession instance. + 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.validate_instance(user, str) + common.validate_instance(zone, str) + common.validate_instance(group, str) + + data = {"op": "remove_from_group", "user": user, "zone": zone, "group": group} + + r = requests.post(session.url_base + "/users-groups", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) + + +def users(session: IRODSHTTPSession): + """ + List all users in the zone. Requires rodsadmin privileges. + + Args: + session: An IRODSHTTPSession instance. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + params = {"op": "users"} + + r = requests.get(session.url_base + "/users-groups", headers=session.get_headers, params=params) # noqa: S113 + return common.process_response(r) + + +def groups(session: IRODSHTTPSession): + """ + List all groups in the zone. Requires rodsadmin privileges. + + Args: + session: An IRODSHTTPSession instance. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + params = {"op": "groups"} + + r = requests.get(session.url_base + "/users-groups", headers=session.get_headers, params=params) # noqa: S113 + return common.process_response(r) + + +def is_member_of_group(session: IRODSHTTPSession, group: str, user: str, zone: str): + """ + Return whether a user is a member of a group or not. + + Args: + session: An IRODSHTTPSession instance. + 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.validate_instance(group, str) + common.validate_instance(user, str) + common.validate_instance(zone, str) + + params = { + "op": "is_member_of_group", + "group": group, + "user": user, + "zone": zone, + } + + r = requests.get(session.url_base + "/users-groups", headers=session.post_headers, params=params) # noqa: S113 + return common.process_response(r) + + +def stat(session: IRODSHTTPSession, name: str, zone: str = ""): + """ + Return information about a user or group. + + Args: + session: An IRODSHTTPSession instance. + 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.validate_instance(name, str) + common.validate_instance(zone, str) + + params = {"op": "stat", "name": name} + + if zone != "": + params["zone"] = zone + + r = requests.get(session.url_base + "/users-groups", headers=session.get_headers, params=params) # noqa: S113 + return common.process_response(r) + + +def modify_metadata(session: IRODSHTTPSession, name: str, operations: list): + """ + Modify the metadata for a user or group. Requires rodsadmin privileges. + + Args: + session: An IRODSHTTPSession instance. + 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.validate_instance(name, str) + common.validate_instance(operations, list) + common.validate_instance(operations[0], dict) + + data = { + "op": "modify_metadata", + "name": name, + "operations": json.dumps(operations), + } + + r = requests.post(session.url_base + "/users-groups", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) diff --git a/irods_http/zones.py b/irods_http/zones.py new file mode 100644 index 0000000..c0fb9fb --- /dev/null +++ b/irods_http/zones.py @@ -0,0 +1,117 @@ +"""Zone operations for iRODS HTTP API.""" + +import requests + +from . import common +from .irods_http import IRODSHTTPSession # noqa: TC001 + + +def add(session: IRODSHTTPSession, name: str, connection_info: str = "", comment: str = ""): + """ + Add a remote zone to the local zone. Requires rodsadmin privileges. + + Args: + session: An IRODSHTTPSession instance. + 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.validate_instance(name, str) + common.validate_instance(connection_info, str) + common.validate_instance(comment, str) + + data = {"op": "add", "name": name} + + if connection_info != "": + data["connection-info"] = connection_info + if comment != "": + data["comment"] = comment + + r = requests.post(session.url_base + "/zones", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) + + +def remove(session: IRODSHTTPSession, name: str): + """ + Remove a remote zone from the local zone. Requires rodsadmin privileges. + + Args: + session: An IRODSHTTPSession instance. + 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.validate_instance(name, str) + + data = {"op": "remove", "name": name} + + r = requests.post(session.url_base + "/zones", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) + + +def modify(session: IRODSHTTPSession, name: str, property_: str, value: str): + """ + Modify properties of a remote zone. Requires rodsadmin privileges. + + Args: + session: An IRODSHTTPSession instance. + 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.validate_instance(name, str) + common.validate_instance(property_, str) + common.validate_instance(value, str) + + data = {"op": "modify", "name": name, "property": property_, "value": value} + + r = requests.post(session.url_base + "/zones", headers=session.post_headers, data=data) # noqa: S113 + return common.process_response(r) + + +def report(session: IRODSHTTPSession): + """ + Return information about the iRODS zone. Requires rodsadmin privileges. + + Args: + session: An IRODSHTTPSession instance. + + Returns: + A dict containing the HTTP status code and iRODS response. + The iRODS response is only valid if no error occurred during HTTP communication. + """ + params = {"op": "report"} + + r = requests.get(session.url_base + "/zones", headers=session.get_headers, params=params) # noqa: S113 + return common.process_response(r) + + +def stat(session: IRODSHTTPSession, name: str): + """ + Return information about a named iRODS zone. Requires rodsadmin privileges. + + Args: + session: An IRODSHTTPSession instance. + name: The name of 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.validate_instance(name, str) + + params = {"op": "stat", "name": name} + + r = requests.get(session.url_base + "/zones", headers=session.get_headers, params=params) # noqa: S113 + return common.process_response(r) diff --git a/irods_http_client/__init__.py b/irods_http_client/__init__.py deleted file mode 100644 index e69d90d..0000000 --- a/irods_http_client/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""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 deleted file mode 100644 index 7073bee..0000000 --- a/irods_http_client/collection_operations.py +++ /dev/null @@ -1,340 +0,0 @@ -"""Collection operations for iRODS HTTP API.""" - -import json - -import requests - -from . import common - - -class Collections: - """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/data_object_operations.py b/irods_http_client/data_object_operations.py deleted file mode 100644 index 4d99b97..0000000 --- a/irods_http_client/data_object_operations.py +++ /dev/null @@ -1,894 +0,0 @@ -"""Data object operations for iRODS HTTP API.""" - -import json - -import requests - -from . import common - - -class DataObjects: - """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/irods_http_client.py b/irods_http_client/irods_http_client.py deleted file mode 100644 index 10bda02..0000000 --- a/irods_http_client/irods_http_client.py +++ /dev/null @@ -1,106 +0,0 @@ -"""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 deleted file mode 100644 index 058f37d..0000000 --- a/irods_http_client/query_operations.py +++ /dev/null @@ -1,187 +0,0 @@ -"""Query operations for iRODS HTTP API.""" - -import requests - -from . import common - - -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 deleted file mode 100644 index 9243371..0000000 --- a/irods_http_client/resource_operations.py +++ /dev/null @@ -1,280 +0,0 @@ -"""Resource operations for iRODS HTTP API.""" - -import json - -import requests - -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 deleted file mode 100644 index 9f53e33..0000000 --- a/irods_http_client/rule_operations.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Rule operations for iRODS HTTP API.""" - -import requests - -from . import common - - -class Rules: - """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 deleted file mode 100644 index 16799af..0000000 --- a/irods_http_client/ticket_operations.py +++ /dev/null @@ -1,113 +0,0 @@ -"""Ticket operations for iRODS HTTP API.""" - -import requests - -from . import common - - -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 deleted file mode 100644 index f5812cf..0000000 --- a/irods_http_client/user_group_operations.py +++ /dev/null @@ -1,380 +0,0 @@ -"""User and group operations for iRODS HTTP API.""" - -import json - -import requests - -from . import common - - -class UsersGroups: - """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 deleted file mode 100644 index 087e385..0000000 --- a/irods_http_client/zone_operations.py +++ /dev/null @@ -1,145 +0,0 @@ -"""Zone operations for iRODS HTTP API.""" - -import requests - -from . import common - - -class Zones: - """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 index 5bc6f5e..afc5305 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,11 @@ [project] -name = "irods-http-client" +name = "irods-http" version = "0.1.0" authors = [ { name="iRODS Consortium", email="info@irods.org" }, ] description = "A Python wrapper for the iRODS HTTP API" +dependencies = ["requests"] readme = "README.md" requires-python = ">=3.9" license = "BSD-3-Clause" diff --git a/test/__init__.py b/test/__init__.py index d53f927..cf35192 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -1 +1 @@ -"""iRODS HTTP client test module.""" +"""iRODS HTTP client library test module.""" diff --git a/test/config.py b/test/config.py index 9178b39..2efc345 100644 --- a/test/config.py +++ b/test/config.py @@ -1,4 +1,4 @@ -"""Test configuration and settings for iRODS HTTP client tests.""" +"""Test configuration and settings for iRODS HTTP client library tests.""" import logging diff --git a/test/test_endpoint_operations.py b/test/test_endpoint_operations.py index 377579b..2349be1 100644 --- a/test/test_endpoint_operations.py +++ b/test/test_endpoint_operations.py @@ -2,7 +2,7 @@ Integration tests for iRODS HTTP API endpoint operations. Tests cover all major operation categories: collections, data objects, -resources, rules, queries, tickets, users/groups, and zones. +queries, resources, rules, tickets, users/groups, and zones. """ import concurrent.futures @@ -13,7 +13,19 @@ import config -from irods_http_client import IRODSHTTPClient +from irods_http import ( + authenticate, + collections, + common, + data_objects, + get_server_info, + queries, + resources, + rules, + tickets, + users_groups, + zones, +) def setup_class(cls, opts): @@ -62,8 +74,6 @@ def setup_class(cls, opts): 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"] @@ -73,36 +83,48 @@ def setup_class(cls, opts): cls.logger.debug("init_rodsadmin is False. Class setup complete.") return - # Authenticate as a rodsadmin and store the bearer token. + # Authenticate as a rodsadmin and store the session. cls.rodsadmin_username = config.test_config["rodsadmin"]["username"] try: - cls.rodsadmin_bearer_token = cls.api.authenticate( - cls.rodsadmin_username, config.test_config["rodsadmin"]["password"] + cls.rodsadmin_session = authenticate( + cls.url_base, 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. + # Authenticate as a rodsuser and store the session. 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( + users_groups.create_user(cls.rodsadmin_session, cls.rodsuser_username, cls.zone_name, "rodsuser") + users_groups.set_password( + cls.rodsadmin_session, 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"] + cls.rodsuser_session = authenticate( + cls.url_base, 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 + # Authenticate as the anonymous user and store the session. + cls.anonymous_username = "anonymous" + + try: + users_groups.create_user(cls.rodsadmin_session, cls.anonymous_username, cls.zone_name, "rodsuser") + cls.anonymous_session = authenticate(cls.url_base, cls.anonymous_username, "") + except RuntimeError: + cls._class_init_error = True + cls.logger.debug("Failed to authenticate as anonymous.") + return + cls.logger.debug("Class setup complete.") @@ -118,12 +140,13 @@ def tear_down_class(cls): if cls._class_init_error: return - cls.api.users_groups.remove_user(cls.rodsuser_username, cls.zone_name) + users_groups.remove_user(cls.rodsadmin_session, cls.rodsuser_username, cls.zone_name) + users_groups.remove_user(cls.rodsadmin_session, cls.anonymous_username, cls.zone_name) # Tests for library class LibraryTests(unittest.TestCase): - """Test library-level operations (info, get_token).""" + """Test library-level operations.""" @classmethod def setUpClass(cls): @@ -142,16 +165,18 @@ def setUp(self): # tests the info operation def test_info(self): """Test the info operation to retrieve server information.""" - self.api.info() + get_server_info(self.rodsadmin_session) - # 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 the validators + def test_validators(self): + """Test the validate functions in common.""" + self.assertRaises(ValueError, common.validate_not_none, None) + self.assertRaises(ValueError, common.validate_gte_zero, -1) + self.assertRaises(ValueError, common.validate_gte_minus1, -2) # Tests for collections operations -class CollectionsTests(unittest.TestCase): +class CollectionTests(unittest.TestCase): """Test iRODS collection operations.""" @classmethod @@ -171,271 +196,274 @@ def setUp(self): # tests the create operation def test_create(self): """Test collection creation operations and parameter validation.""" - self.api.set_token(self.rodsadmin_bearer_token) + try: + # test param checking + self.assertRaises(TypeError, collections.create, self.rodsadmin_session, 0, 0) + self.assertRaises( + TypeError, + collections.create, + self.rodsadmin_session, + f"/{self.zone_name}/home/{self.rodsadmin_username}", + "0", + ) + self.assertRaises( + ValueError, + collections.create, + self.rodsadmin_session, + f"/{self.zone_name}/home/{self.rodsadmin_username}", + 7, + ) - # 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 creating new collection + r = collections.create(self.rodsadmin_session, f"/{self.zone_name}/home/new") + common.assert_success(self, r) + self.assertTrue(r["data"]["created"]) - # 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 existing collection + r = collections.create(self.rodsadmin_session, f"/{self.zone_name}/home/new") + common.assert_success(self, r) + self.assertFalse(r["data"]["created"]) - # 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 invalid path + r = collections.create(self.rodsadmin_session, f"{self.zone_name}/home/new") + self.assertEqual(r["data"]["irods_response"]["status_code"], -358000) # OBJ_PATH_DOES_NOT_EXIST - # 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"]), - ) + # test create_intermediates + r = collections.create( + self.rodsadmin_session, f"/{self.zone_name}/home/test/folder", create_intermediates=0 + ) + self.assertEqual(r["data"]["irods_response"]["status_code"], -358000) # OBJ_PATH_DOES_NOT_EXIST + r = collections.create( + self.rodsadmin_session, f"/{self.zone_name}/home/test/folder", create_intermediates=1 + ) + common.assert_success(self, r) + self.assertTrue(r["data"]["created"]) + + finally: + # clean up test collections + collections.remove(self.rodsadmin_session, f"/{self.zone_name}/home/new") + collections.remove(self.rodsadmin_session, f"/{self.zone_name}/home/test/folder") + collections.remove(self.rodsadmin_session, f"/{self.zone_name}/home/test") # 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") + try: + # test param checking + self.assertRaises(TypeError, collections.remove, self.rodsadmin_session, 0, 0, 0) + self.assertRaises( + TypeError, + collections.remove, + self.rodsadmin_session, + f"/{self.zone_name}/home/{self.rodsadmin_username}", + "0", + 0, + ) + self.assertRaises( + ValueError, + collections.remove, + self.rodsadmin_session, + f"/{self.zone_name}/home/{self.rodsadmin_username}", + 5, + 0, + ) + self.assertRaises( + TypeError, + collections.remove, + self.rodsadmin_session, + f"/{self.zone_name}/home/{self.rodsadmin_username}", + 0, + "0", + ) + self.assertRaises( + ValueError, + collections.remove, + self.rodsadmin_session, + f"/{self.zone_name}/home/{self.rodsadmin_username}", + 0, + 5, + ) - # 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 + r = collections.create(self.rodsadmin_session, f"/{self.zone_name}/home/new") + common.assert_success(self, r) + self.assertTrue(r["data"]["created"]) + r = collections.remove(self.rodsadmin_session, f"/{self.zone_name}/home/new") + common.assert_success(self, r) + + # test invalid paths + r = collections.stat(self.rodsadmin_session, f"/{self.zone_name}/home/tensaitekinaaidorusama") + self.assertEqual("{'irods_response': {'status_code': -170000}}", str(r["data"])) + r = collections.stat(self.rodsadmin_session, f"/{self.zone_name}/home/aremonainainaikoremonainainai") + self.assertEqual("{'irods_response': {'status_code': -170000}}", str(r["data"])) + r = collections.stat(self.rodsadmin_session, f"/{self.zone_name}/home/binglebangledingledangle") + self.assertEqual("{'irods_response': {'status_code': -170000}}", str(r["data"])) + r = collections.stat(self.rodsadmin_session, f"{self.zone_name}/home/{self.rodsadmin_username}") + self.assertEqual("{'irods_response': {'status_code': -170000}}", str(r["data"])) + + # test recurse + r = collections.create( + self.rodsadmin_session, f"/{self.zone_name}/home/test/folder", create_intermediates=1 + ) + common.assert_success(self, r) + self.assertTrue(r["data"]["created"]) + r = collections.remove(self.rodsadmin_session, f"/{self.zone_name}/home/test", recurse=0) + self.assertEqual(r["data"]["irods_response"]["status_code"], -79000) # SYS_COLLECTION_NOT_EMPTY + r = collections.remove(self.rodsadmin_session, f"/{self.zone_name}/home/test", recurse=1) + common.assert_success(self, r) - # 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"])) + finally: + # clean up test collections + collections.remove(self.rodsadmin_session, f"/{self.zone_name}/home/new") + collections.remove(self.rodsadmin_session, f"/{self.zone_name}/home/test/folder") + collections.remove(self.rodsadmin_session, f"/{self.zone_name}/home/test") # 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") + try: + # test param checking + self.assertRaises(TypeError, collections.stat, self.rodsadmin_session, 0, "ticket") + self.assertRaises( + TypeError, + collections.stat, + self.rodsadmin_session, + f"/{self.zone_name}/home/{self.rodsadmin_username}", + 0, + ) - # 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 + r = collections.stat(self.rodsadmin_session, f"/{self.zone_name}/home/new") + self.assertEqual("{'irods_response': {'status_code': -170000}}", str(r["data"])) + r = collections.stat(self.rodsadmin_session, f"{self.zone_name}/home/new") + self.assertEqual("{'irods_response': {'status_code': -170000}}", str(r["data"])) - # 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 + r = collections.stat(self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}") + self.assertTrue(r["data"]["permissions"]) - # test valid path - response = self.api.collections.stat(f"/{self.zone_name}/home/{self.rodsadmin_username}") - self.assertTrue(response["data"]["permissions"]) + finally: + # clean up test collections + collections.remove(self.rodsadmin_session, f"/{self.zone_name}/home/new") # 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") + try: + # test param checking + self.assertRaises(TypeError, collections.list, self.rodsadmin_session, 0, "ticket") + self.assertRaises( + TypeError, + collections.list, + self.rodsadmin_session, + f"/{self.zone_name}/home/{self.rodsadmin_username}", + "0", + "ticket", + ) + self.assertRaises( + ValueError, + collections.list, + self.rodsadmin_session, + f"/{self.zone_name}/home/{self.rodsadmin_username}", + 5, + "ticket", + ) + self.assertRaises( + TypeError, + collections.list, + self.rodsadmin_session, + f"/{self.zone_name}/home/{self.rodsadmin_username}", + 0, + 0, + ) - # 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 + r = collections.list(self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}") + self.assertEqual("None", str(r["data"]["entries"])) - # 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 + collections.create(self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}/bosnia") + r = collections.list(self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}") + self.assertEqual( + f"/{self.zone_name}/home/{self.rodsadmin_username}/bosnia", + str(r["data"]["entries"][0]), + ) - # 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 + collections.create(self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}/albania") + collections.create(self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}/croatia") + r = collections.list(self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}") + self.assertEqual( + f"/{self.zone_name}/home/{self.rodsadmin_username}/albania", + str(r["data"]["entries"][0]), + ) + self.assertEqual( + f"/{self.zone_name}/home/{self.rodsadmin_username}/bosnia", + str(r["data"]["entries"][1]), + ) + self.assertEqual( + f"/{self.zone_name}/home/{self.rodsadmin_username}/croatia", + str(r["data"]["entries"][2]), + ) - # 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 + collections.create( + self.rodsadmin_session, + f"/{self.zone_name}/home/{self.rodsadmin_username}/croatia/zagreb", + ) + r = collections.list(self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}") + self.assertEqual( + f"/{self.zone_name}/home/{self.rodsadmin_username}/albania", + str(r["data"]["entries"][0]), + ) + self.assertEqual( + f"/{self.zone_name}/home/{self.rodsadmin_username}/bosnia", + str(r["data"]["entries"][1]), + ) + self.assertEqual( + f"/{self.zone_name}/home/{self.rodsadmin_username}/croatia", + str(r["data"]["entries"][2]), + ) + self.assertEqual(len(r["data"]["entries"]), 3) - # 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 + r = collections.list(self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}", recurse=1) + self.assertEqual( + f"/{self.zone_name}/home/{self.rodsadmin_username}/albania", + str(r["data"]["entries"][0]), + ) + self.assertEqual( + f"/{self.zone_name}/home/{self.rodsadmin_username}/bosnia", + str(r["data"]["entries"][1]), + ) + self.assertEqual( + f"/{self.zone_name}/home/{self.rodsadmin_username}/croatia", + str(r["data"]["entries"][2]), + ) + self.assertEqual( + f"/{self.zone_name}/home/{self.rodsadmin_username}/croatia/zagreb", + str(r["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]), - ) + finally: + # clean up test collections + collections.remove( + self.rodsadmin_session, + f"/{self.zone_name}/home/{self.rodsadmin_username}/croatia/zagreb", + ) + collections.remove(self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}/albania") + collections.remove(self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}/bosnia") + collections.remove(self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}/croatia") # 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, collections.set_permission, self.rodsadmin_session, 0, "jeb", "read", 0) self.assertRaises( TypeError, - self.api.collections.set_permission, + collections.set_permission, + self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}", 0, "read", @@ -443,15 +471,26 @@ def test_set_permission(self): ) self.assertRaises( TypeError, - self.api.collections.set_permission, + collections.set_permission, + self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}", "jeb", 0, 0, ) + self.assertRaises( + ValueError, + collections.set_permission, + self.rodsadmin_session, + f"/{self.zone_name}/home/{self.rodsadmin_username}", + "jeb", + "badperm", + 0, + ) self.assertRaises( TypeError, - self.api.collections.set_permission, + collections.set_permission, + self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}", "jeb", "read", @@ -459,268 +498,294 @@ def test_set_permission(self): ) self.assertRaises( ValueError, - self.api.collections.set_permission, + collections.set_permission, + self.rodsadmin_session, 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"])) + try: + # create new collection + r = collections.create(self.rodsadmin_session, f"/{self.zone_name}/home/setPerms") + common.assert_success(self, r) + + # test no permission + r = collections.stat(self.rodsuser_session, f"/{self.zone_name}/home/setPerms") + self.assertEqual(r["data"]["irods_response"]["status_code"], -170000) + + # test set permission + r = collections.set_permission( + self.rodsadmin_session, + f"/{self.zone_name}/home/setPerms", + self.rodsuser_username, + "read", + ) + common.assert_success(self, r) + + # test with permission + r = collections.stat(self.rodsadmin_session, f"/{self.zone_name}/home/setPerms") + self.assertTrue(r["data"]["permissions"]) + + # test set permission null + r = collections.set_permission( + self.rodsadmin_session, + f"/{self.zone_name}/home/setPerms", + self.rodsuser_username, + "null", + ) + common.assert_success(self, r) - # 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"])) + # test no permission + r = collections.stat(self.rodsuser_session, f"/{self.zone_name}/home/setPerms") + self.assertEqual("{'irods_response': {'status_code': -170000}}", str(r["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) + finally: + # remove the collection + collections.remove(self.rodsadmin_session, f"/{self.zone_name}/home/setPerms", recurse=1, no_trash=1) # tests the set inheritance operation def test_set_inheritance(self): """Test setting inheritance for collection permissions.""" - self.api.set_token(self.rodsadmin_bearer_token) + testcoll = f"/{self.zone_name}/home/{self.rodsadmin_username}/testcoll" - # 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, - ) + try: + collections.create(self.rodsadmin_session, testcoll) + + # test param checking + self.assertRaises(TypeError, collections.set_inheritance, self.rodsadmin_session, 0, 0, 0) + self.assertRaises( + TypeError, + collections.set_inheritance, + self.rodsadmin_session, + testcoll, + "0", + 0, + ) + self.assertRaises( + ValueError, + collections.set_inheritance, + self.rodsadmin_session, + testcoll, + 5, + 0, + ) + self.assertRaises( + TypeError, + collections.set_inheritance, + self.rodsadmin_session, + testcoll, + 0, + "0", + ) + self.assertRaises( + ValueError, + collections.set_inheritance, + self.rodsadmin_session, + testcoll, + 0, + 5, + ) + + # control + r = collections.stat(self.rodsadmin_session, testcoll) + self.assertFalse(r["data"]["inheritance_enabled"]) - # control - response = self.api.collections.stat(f"/{self.zone_name}/home/{self.rodsadmin_username}") - self.assertFalse(response["data"]["inheritance_enabled"]) + # test enabling inheritance + r = collections.set_inheritance(self.rodsadmin_session, testcoll, enable=1) + common.assert_success(self, r) - # 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"])) + # verify inheritance is enabled + r = collections.stat(self.rodsadmin_session, testcoll) + self.assertTrue(r["data"]["inheritance_enabled"]) - # 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 + r = collections.set_inheritance(self.rodsadmin_session, testcoll, enable=0) + common.assert_success(self, r) - # 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"])) + # verify inheritance is disabled + r = collections.stat(self.rodsadmin_session, testcoll) + self.assertFalse(r["data"]["inheritance_enabled"]) - # check if changed - response = self.api.collections.stat(f"/{self.zone_name}/home/{self.rodsadmin_username}") - self.assertFalse(response["data"]["inheritance_enabled"]) + finally: + collections.remove(self.rodsadmin_session, testcoll, recurse=1, no_trash=1) # test the modify permissions operation def test_modify_permissions(self): """Test modifying permissions on collections.""" - self.api.set_token(self.rodsadmin_bearer_token) + testcoll = f"/{self.zone_name}/home/modPerms" 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) + try: + # create new collection + r = collections.create(self.rodsadmin_session, testcoll) + common.assert_success(self, r) + + # test param checking + self.assertRaises(TypeError, collections.modify_permissions, self.rodsadmin_session, 0, ops_permissions, 0) + self.assertRaises( + TypeError, + collections.modify_permissions, + self.rodsadmin_session, + testcoll, + 5, + 0, + ) + self.assertRaises( + TypeError, + collections.modify_permissions, + self.rodsadmin_session, + testcoll, + ops_permissions, + "0", + ) + self.assertRaises( + ValueError, + collections.modify_permissions, + self.rodsadmin_session, + testcoll, + ops_permissions, + 5, + ) - # 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 no permissions + r = collections.stat(self.rodsuser_session, testcoll) + self.assertEqual("{'irods_response': {'status_code': -170000}}", str(r["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 set permissions + r = collections.modify_permissions(self.rodsadmin_session, testcoll, ops_permissions) + common.assert_success(self, r) - # 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 with permissions + r = collections.stat(self.rodsadmin_session, testcoll) + self.assertTrue(r["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 set permissions nuil + r = collections.modify_permissions(self.rodsadmin_session, testcoll, ops_permissions_null) + common.assert_success(self, r) - # 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"])) + # test without permissions + r = collections.stat(self.rodsuser_session, testcoll) + self.assertEqual("{'irods_response': {'status_code': -170000}}", str(r["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) + finally: + # remove the collection + collections.remove(self.rodsadmin_session, testcoll, recurse=1, no_trash=1) # test the modify metadata operation def test_modify_metadata(self): """Test modifying metadata on collections.""" - self.api.set_token(self.rodsadmin_bearer_token) + testcoll = f"/{self.zone_name}/home/{self.rodsadmin_username}/modify_metadata_test" 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, - ) + try: + collections.create(self.rodsadmin_session, testcoll) + + # test param checking + self.assertRaises(TypeError, collections.modify_metadata, self.rodsadmin_session, 0, ops_metadata, 0) + self.assertRaises( + TypeError, + collections.modify_metadata, + self.rodsadmin_session, + testcoll, + 5, + 0, + ) + self.assertRaises( + TypeError, + collections.modify_metadata, + self.rodsadmin_session, + testcoll, + ops_metadata, + "0", + ) + self.assertRaises( + ValueError, + collections.modify_metadata, + self.rodsadmin_session, + testcoll, + 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) + # test adding and removing metadata + r = collections.modify_metadata( + self.rodsadmin_session, + testcoll, + ops_metadata, + ) + common.assert_success(self, r) + r = collections.modify_metadata( + self.rodsadmin_session, + testcoll, + ops_metadata_remove, + ) + common.assert_success(self, r) + + finally: + collections.remove(self.rodsadmin_session, testcoll, no_trash=1) # 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") + testcolla = f"/{self.zone_name}/home/{self.rodsadmin_username}/test_rename_a" + testcollb = f"/{self.zone_name}/home/{self.rodsadmin_username}/test_rename_b" - # 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"]) + try: + collections.create(self.rodsadmin_session, testcolla) + + # test param checking + self.assertRaises( + TypeError, + collections.rename, + self.rodsadmin_session, + testcolla, + 0, + ) + self.assertRaises(TypeError, collections.rename, 0, testcolla) - # 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 renaming + r = collections.rename( + self.rodsadmin_session, + testcolla, + testcollb, + ) + common.assert_success(self, r) - # 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 presence + r = collections.stat(self.rodsadmin_session, testcolla) + self.assertEqual(r["data"]["irods_response"]["status_code"], -170000) + r = collections.stat(self.rodsadmin_session, testcollb) + common.assert_success(self, r) - # 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"])) + finally: + collections.remove(self.rodsadmin_session, testcolla, no_trash=1) + collections.remove(self.rodsadmin_session, testcollb, no_trash=1) # 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" + collections.touch( + self.rodsadmin_session, + f"/{self.zone_name}/home/{self.rodsadmin_username}", + reference=f"/{self.zone_name}/home", + ) + collections.touch( + self.rodsadmin_session, + f"/{self.zone_name}/home/{self.rodsadmin_username}", + seconds_since_epoch=9000, ) # Tests for data object operations -class DataObjectsTests(unittest.TestCase): +class DataObjectTests(unittest.TestCase): """Test iRODS data object operations.""" @classmethod @@ -737,10 +802,25 @@ 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) + def test_bad_write(self): + """Test writing non-bytes does not work.""" + # Exercise a bad write + f = f"/{self.zone_name}/home/{self.rodsuser_username}/bad_write.txt" + self.assertRaises(TypeError, data_objects.write, self.rodsuser_session, 4, f) + + def test_empty_write(self): + """Test writing an empty string works.""" + try: + # Exercise an empty write + f = f"/{self.zone_name}/home/{self.rodsuser_username}/empty_write.txt" + r = data_objects.write(self.rodsuser_session, "", f) + common.assert_success(self, r) + finally: + data_objects.remove(self.rodsuser_session, f, no_trash=1) + + def test_common_operations(self): + """Test common data object operations (write, read, copy, replicate, etc.).""" 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" @@ -748,74 +828,88 @@ def test_common_operations(self): try: # Create a unixfilesystem resource - r = self.api.resources.create( + r = resources.create( + self.rodsadmin_session, resc, "unixfilesystem", self.host, "/tmp/resource", # noqa: S108 "", ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) - 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) + r = data_objects.write(self.rodsuser_session, "These are the bytes being written to the object", f1) + common.assert_success(self, r) # 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')) + r = data_objects.read(self.rodsuser_session, f1, offset=6, count=13) + self.assertEqual(r["status_code"], 200) + self.assertNotIn('There ', r["data"].decode('utf-8')) + self.assertEqual('are the bytes', r["data"].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'}] + r = data_objects.modify_metadata( + self.rodsuser_session, + f1, + operations=[{'operation': 'add', 'attribute': 'a', 'value': 'v', 'units': 'u'}], ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) # 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) + r = data_objects.modify_replica(self.rodsadmin_session, f1, replica_number=0, new_data_comments="awesome") + common.assert_success(self, r) # Replicate the data object - r = self.api.data_objects.replicate( + r = data_objects.replicate( + self.rodsuser_session, f1, + src_resource="demoResc", dst_resource=resc, ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) # 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]}'" + r = queries.execute_genquery( + self.rodsuser_session, + f"select DATA_NAME, DATA_REPL_NUM where DATA_NAME = '{f1.rsplit('/', maxsplit=1)[-1]}'", ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) 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) + # Trim the data object + r = data_objects.trim(self.rodsuser_session, f1, replica_number=0) + common.assert_success(self, r) # Rename the data object - r = self.api.data_objects.rename(f1, f2) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + r = data_objects.rename(self.rodsuser_session, f1, f2) + common.assert_success(self, r) # Copy the data object - r = self.api.data_objects.copy(f2, f3) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + r = data_objects.copy(self.rodsuser_session, f2, f3) + common.assert_success(self, r) + + # Copy the data object again with parameters + r = data_objects.copy( + self.rodsuser_session, f2, f3, src_resource=resc, dst_resource="demoResc", overwrite=1 + ) + common.assert_success(self, r) + + # Exercise a bad permission + self.assertRaises(ValueError, data_objects.set_permission, self.rodsuser_session, f3, "rods", "bad") # Set permission on the object - r = self.api.data_objects.set_permission( + r = data_objects.set_permission( + self.rodsuser_session, f3, "rods", "read", ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) # Confirm that the permission has been set - r = self.api.data_objects.stat(f3) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + r = data_objects.stat(self.rodsuser_session, f3) + common.assert_success(self, r) self.assertIn( { "name": "rods", @@ -827,203 +921,453 @@ def test_common_operations(self): ) # 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) + r = data_objects.modify_permissions( + self.rodsuser_session, f3, operations=[{'entity_name': 'rods', 'acl': 'write'}] + ) + common.assert_success(self, r) finally: # Remove the data objects - r = self.api.data_objects.remove(f1, 0, 1) + r = data_objects.remove(self.rodsuser_session, f1, no_trash=1) - r = self.api.data_objects.remove(f2, 0, 1) + r = data_objects.remove(self.rodsuser_session, f2, no_trash=1) - r = self.api.data_objects.remove(f3, 0, 1) + r = data_objects.remove(self.rodsuser_session, f3, no_trash=1) # Remove the resource - self.api.set_token(self.rodsadmin_bearer_token) - r = self.api.resources.remove(resc) + r = resources.remove(self.rodsadmin_session, resc) + + def test_read_with_ticket(self): + """Test the read operation via anonymous ticket.""" + f = f"/{self.zone_name}/home/{self.rodsadmin_username}/anon-test1.txt" + + try: + # Create a data object + content = "hello anonymous" + r = data_objects.write(self.rodsadmin_session, content, f) + common.assert_success(self, r) + + # Create a ticket for read + r = tickets.create( + self.rodsadmin_session, + f, + "read", + ) + common.assert_success(self, r) + ticket_string = r["data"]["ticket"] + self.assertGreater(len(ticket_string), 0) + + # Stat with the ticket + r = data_objects.stat(self.anonymous_session, f, ticket=ticket_string) + common.assert_success(self, r) + + # Read the data object via anonymous ticket + r = data_objects.read(self.anonymous_session, f, ticket=ticket_string) + self.assertEqual(r["status_code"], 200) + self.assertEqual(r["data"], bytes(content, 'utf-8')) + + finally: + # Remove the data object + data_objects.remove(self.rodsadmin_session, f, no_trash=1) + + # Remove the ticket + tickets.remove(self.rodsadmin_session, ticket_string) + + def test_small_write_with_ticket(self): + """Test the small write operation via anonmymous ticket.""" + c = f"/{self.zone_name}/home/{self.rodsadmin_username}" + f = f"{c}/anon-test2.txt" + + try: + # Create a ticket for writing a small data object + r = tickets.create( + self.rodsadmin_session, + c, + "write", + write_data_object_count=4, + ) + common.assert_success(self, r) + ticket_string = r["data"]["ticket"] + self.assertGreater(len(ticket_string), 0) + + # Create a small data object via anonymous ticket + r = data_objects.write(self.anonymous_session, "writing", f, ticket=ticket_string) + common.assert_success(self, r) + + finally: + # Add own permission, for the removal + data_objects.set_permission(self.rodsadmin_session, f, "rods", "own", admin=1) + + # Remove the data object + data_objects.remove(self.rodsadmin_session, f, no_trash=1) + + # Remove the ticket + tickets.remove(self.rodsadmin_session, ticket_string) + + def test_large_write_with_ticket(self): + """Test the small write operation via anonmymous ticket.""" + c = f"/{self.zone_name}/home/{self.rodsadmin_username}" + f = f"{c}/anon-test3.txt" + + try: + # Create a ticket for writing a large data object + r = tickets.create( + self.rodsadmin_session, + c, + "write", + write_data_object_count=4, + ) + common.assert_success(self, r) + ticket_string = r["data"]["ticket"] + self.assertGreater(len(ticket_string), 0) + + # Open parallel write via anonymous ticket + r = data_objects.parallel_write_init(self.anonymous_session, f, stream_count=3, ticket=ticket_string) + common.assert_success(self, r) + handle = r["data"]["parallel_write_handle"] + + # Write to the data object using the parallel write handle + futures = [] + with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor: + for x in enumerate(["A", "B", "C"]): + count = 10 + futures.append( + executor.submit( + data_objects.write, + self.anonymous_session, + bytes=x[1] * count, + offset=x[0] * count, + stream_index=x[0], + parallel_write_handle=handle, + ) + ) + for future in concurrent.futures.as_completed(futures): + r = future.result() + common.assert_success(self, r) + + # Close parallel write + r = data_objects.parallel_write_shutdown(self.anonymous_session, handle) + common.assert_success(self, r) + + finally: + # Add own permission, for the removal + data_objects.set_permission(self.rodsadmin_session, f, "rods", "own", admin=1) + + # Remove the data object + data_objects.remove(self.rodsadmin_session, f, no_trash=1) + + # Remove the ticket + tickets.remove(self.rodsadmin_session, ticket_string) + + def test_modify_replica(self): + """Test modify replica options.""" + f = f"/{self.zone_name}/home/{self.rodsadmin_username}/modify-replica-test.txt" + + try: + # Create a data object + r = data_objects.write(self.rodsadmin_session, "some words", f) + common.assert_success(self, r) + + # Save the physical path + r = queries.execute_genquery( + self.rodsadmin_session, "SELECT DATA_PATH where DATA_NAME = 'modify-replica-test.txt'" + ) + common.assert_success(self, r) + phypath = r["data"]["rows"][0][0] + + # Save the resource id + r = queries.execute_genquery(self.rodsadmin_session, "SELECT RESC_ID where RESC_NAME = 'demoResc'") + common.assert_success(self, r) + rescid = int(r["data"]["rows"][0][0]) + + # Exercise modify replica error, incompatible params + self.assertRaises( + ValueError, + data_objects.modify_replica, + self.rodsadmin_session, + f, + replica_number=0, + resource_hierarchy="demoResc", + ) + + # Exercise modify replica error, no new data + self.assertRaises( + RuntimeError, + data_objects.modify_replica, + self.rodsadmin_session, + f, + ) + + # Modify the replica + r = data_objects.modify_replica( + self.rodsadmin_session, + f, + resource_hierarchy="demoResc", + new_data_checksum="not a real checksum", + new_data_create_time="1000", + new_data_expiry="3000", + new_data_mode="greatmode", + new_data_modify_time="2000", + new_data_path="/tmp/deleteme", # noqa: S108 + new_data_replica_number=5, + new_data_replica_status=0, + new_data_resource_id=rescid, + new_data_size=50, + new_data_status="warm", + new_data_type_name="html", + new_data_version=3, + ) + common.assert_success(self, r) + + # Restore the physical path so cleanup succeeds + r = data_objects.modify_replica( + self.rodsadmin_session, + f, + resource_hierarchy="demoResc", + new_data_path=phypath, + ) + common.assert_success(self, r) + + finally: + data_objects.remove(self.rodsadmin_session, f, no_trash=1) 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) + f = f"/{self.zone_name}/home/{self.rodsadmin_username}/file.txt" + resc = "newresource" - # 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) + try: + # Create a unixfilesystem resource. + r = resources.create( + self.rodsadmin_session, + resc, + "unixfilesystem", + self.host, + "/tmp/newresource", # noqa: S108 + "", + ) + common.assert_success(self, r) - # 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) + # Create a non-empty data object + r = data_objects.write( + self.rodsadmin_session, + "These are the bytes being written to the object", + f, + ) + common.assert_success(self, r) - # 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) + # Replicate the data object + r = data_objects.replicate( + self.rodsadmin_session, + f, + dst_resource=resc, + ) + common.assert_success(self, r) + + # Show that there are two replicas + r = queries.execute_genquery( + self.rodsadmin_session, "select DATA_NAME, DATA_REPL_NUM where DATA_NAME = 'file.txt'" + ) + common.assert_success(self, r) + 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", + r = data_objects.calculate_checksum( + self.rodsadmin_session, + f, + replica_number=0, + ) + common.assert_success(self, r) + + # Calculate a checksum for the second replica + r = data_objects.calculate_checksum( + self.rodsadmin_session, + f, + resource=resc, + ) + common.assert_success(self, r) + + # Verify checksum on first replica + r = data_objects.verify_checksum( + self.rodsadmin_session, + f, replica_number=0, ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) + + # Verify checksum on second replica + r = data_objects.verify_checksum( + self.rodsadmin_session, + f, + resource=resc, + ) + common.assert_success(self, r) - # 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 data object + data_objects.remove( + self.rodsadmin_session, + f, + catalog_only=0, + no_trash=1, + ) # Remove the resource - r = self.api.resources.remove("newresource") - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + resources.remove(self.rodsadmin_session, "newresource") def test_touch(self): """Test touch operation on data objects.""" - self.api.set_token(self.rodsadmin_bearer_token) + f = f"/{self.zone_name}/home/{self.rodsadmin_username}/new.txt" + + try: + # Test touching non existant data object with no_create + r = data_objects.touch(self.rodsadmin_session, f, no_create=1) + common.assert_success(self, r) - # 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 = data_objects.stat(self.rodsadmin_session, f) + self.assertEqual(r["data"]["irods_response"]["status_code"], -171000) - # 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 = data_objects.touch(self.rodsadmin_session, f, no_create=0) + common.assert_success(self, r) - # 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 = data_objects.stat(self.rodsadmin_session, f) + common.assert_success(self, r) - # 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 = data_objects.touch(self.rodsadmin_session, f, no_create=0) + common.assert_success(self, r) - # 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) + # Test parameter options + r = data_objects.touch(self.rodsadmin_session, f, seconds_since_epoch=5000) + common.assert_success(self, r) + r = data_objects.stat(self.rodsadmin_session, f) + self.assertEqual(r["data"]["modified_at"], 5000) - # 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) + r = data_objects.touch(self.rodsadmin_session, f, replica_number=0) + common.assert_success(self, r) + r = data_objects.touch(self.rodsadmin_session, f, leaf_resources="demoResc") + common.assert_success(self, r) + + r = data_objects.touch(self.rodsadmin_session, f, reference=f) + common.assert_success(self, r) + + finally: + # Remove the object + data_objects.remove(self.rodsadmin_session, f, no_trash=1) 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) + r = data_objects.stat(self.rodsadmin_session, filename) self.assertEqual(r["data"]["irods_response"]["status_code"], -171000) try: # Create a unixfilesystem resource. - r = self.api.resources.create( + r = resources.create( + self.rodsadmin_session, "register_resource", "unixfilesystem", self.host, "/tmp/register_resource", # noqa: S108 "", ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) + + # Create a non-empty data object. + content = "bytes in the server" + r = data_objects.write(self.rodsadmin_session, content, filename) + common.assert_success(self, r) + + # Query and save the physical path on the server. + r = queries.execute_genquery( + self.rodsadmin_session, "SELECT DATA_PATH where DATA_NAME = 'register-demo.txt'" + ) + common.assert_success(self, r) + phyfile = r["data"]["rows"][0][0] + + # Unregister the logical path to leave the physical file on the server. + r = data_objects.remove(self.rodsadmin_session, filename, catalog_only=1) + common.assert_success(self, r) - # Register the local file into the catalog as a new data object. + # Register the leftover 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( + r = data_objects.register( + self.rodsadmin_session, filename, - "/tmp/register-demo.txt", # noqa: S108 + phyfile, "register_resource", data_size=len(content), + checksum=1, ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) # 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'" + r = queries.execute_genquery( + self.rodsadmin_session, + "select DATA_NAME, DATA_PATH, DATA_CHECKSUM, RESC_NAME where DATA_NAME = 'register-demo.txt'", ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) 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") + self.assertEqual(r["data"]["rows"][0][1], phyfile) + self.assertNotEqual(r["data"]["rows"][0][2], "") + self.assertEqual(r["data"]["rows"][0][3], "register_resource") finally: # Unregister the data object - r = self.api.data_objects.remove(filename, 1) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + data_objects.remove(self.rodsadmin_session, filename, catalog_only=1) # Remove the resource - r = self.api.resources.remove("register_resource") - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + resources.remove(self.rodsadmin_session, "register_resource") 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) + f = f"/{self.zone_name}/home/{self.rodsadmin_username}/parallel-write.txt" # 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) + r = data_objects.parallel_write_init(self.rodsadmin_session, f, stream_count=3) + common.assert_success(self, r) 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"]): + for x 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], + data_objects.write, + self.rodsadmin_session, + bytes=x[1] * count, + offset=x[0] * count, + stream_index=x[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) + for future in concurrent.futures.as_completed(futures): + r = future.result() + common.assert_success(self, r) finally: # Close parallel write - r = self.api.data_objects.parallel_write_shutdown(handle) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + data_objects.parallel_write_shutdown(self.rodsadmin_session, handle) # Remove the object - r = self.api.data_objects.remove( - f"/{self.zone_name}/home/{self.rodsadmin_username}/parallel-write.txt", - 0, - 1, + data_objects.remove( + self.rodsadmin_session, + f, + catalog_only=0, + no_trash=1, ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Tests for resources operations -class ResourcesTests(unittest.TestCase): +class ResourceTests(unittest.TestCase): """Test iRODS resource operations.""" @classmethod @@ -1042,205 +1386,241 @@ def setUp(self): 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)]) + f = f"/{self.zone_name}/home/{self.rodsadmin_username}/test_object.txt" - # 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) + try: + # Create replication resource. + r = resources.create(self.rodsadmin_session, resc_repl, "replication", "", "", "") + common.assert_success(self, r) + + # Show the replication resource was created. + r = resources.stat(self.rodsadmin_session, resc_repl) + common.assert_success(self, r) + 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 = resources.create(self.rodsadmin_session, resc_name, "unixfilesystem", self.host, vault_path, "") + common.assert_success(self, r) + + # Add the unixfilesystem resource as a child of the replication resource. + r = resources.add_child(self.rodsadmin_session, resc_repl, resc_name) + common.assert_success(self, r) + + # Show that the resource was created and configured successfully. + r = resources.stat(self.rodsadmin_session, resc_name) + common.assert_success(self, r) + 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. + r = data_objects.write(self.rodsadmin_session, "These are the bytes to be written", f, resc_repl, offset=0) + common.assert_success(self, r) + + # Show there are two replicas under the replication resource hierarchy. + r = queries.execute_genquery( + self.rodsadmin_session, + f"select DATA_NAME, RESC_NAME where DATA_NAME = '{pathlib.Path(f).name}'", + ) + common.assert_success(self, r) + self.assertEqual(len(r["data"]["rows"]), 2) - # Launch rebalance - r = self.api.resources.rebalance(resc_repl) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + resc_tuple = (r["data"]["rows"][0][1], r["data"]["rows"][1][1]) + self.assertIn(resc_tuple, [(resc_ufs0, resc_ufs1), (resc_ufs1, resc_ufs0)]) - # Give the rebalance operation time to complete! - time.sleep(3) + # Trim a replica. + r = data_objects.trim(self.rodsadmin_session, f, replica_number=0) + common.assert_success(self, r) - # - # Clean-up - # + # Show there is only one replica under the replication resource hierarchy. + r = queries.execute_genquery( + self.rodsadmin_session, + f"select DATA_NAME, RESC_NAME where DATA_NAME = '{pathlib.Path(f).name}'", + ) + common.assert_success(self, r) + self.assertEqual(len(r["data"]["rows"]), 1) - # Remove the data object. - r = self.api.data_objects.remove(data_object, 0, 1) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + # Launch rebalance + r = resources.rebalance(self.rodsadmin_session, resc_repl) + common.assert_success(self, r) - # 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) + # Give the rebalance operation time to complete! + time.sleep(3) - # Remove ufs resource. - r = self.api.resources.remove(resc_name) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + # Show there are two replicas under the replication resource hierarchy. + r = queries.execute_genquery( + self.rodsadmin_session, + f"select DATA_NAME, RESC_NAME where DATA_NAME = '{pathlib.Path(f).name}'", + ) + common.assert_success(self, r) + self.assertEqual(len(r["data"]["rows"]), 2) - # 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) + finally: + # Remove the data object. + data_objects.remove(self.rodsadmin_session, f, catalog_only=0, no_trash=1) + + # Remove resources. + for resc_name in [resc_ufs0, resc_ufs1]: + with self.subTest(f"Detach and remove resource [{resc_name}] from [{resc_repl}]"): + # Detach and remove the ufs resource. + resources.remove_child(self.rodsadmin_session, resc_repl, resc_name) + resources.remove(self.rodsadmin_session, resc_name) + + # Remove replication resource. + resources.remove(self.rodsadmin_session, resc_repl) + + def test_modify_failures(self): + """Test modifying resources, poorly.""" + badresc = "badresc" + try: + # Create a unixfilesystem resource. + r = resources.create( + self.rodsadmin_session, + badresc, + "unixfilesystem", + self.host, + "/tmp/badresc_vault", # noqa: S108 + "", + ) + common.assert_success(self, r) - # Remove replication resource. - r = self.api.resources.remove(resc_repl) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + # Exercise bad modify property + self.assertRaises(ValueError, resources.modify, self.rodsadmin_session, badresc, "badoption", "2") - # 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) + # Exercise bad modify status + self.assertRaises(ValueError, resources.modify, self.rodsadmin_session, badresc, "status", "nope") - 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) + finally: + resources.remove(self.rodsadmin_session, badresc) - operations = [{"operation": "add", "attribute": "a1", "value": "v1", "units": "u1"}] + def test_add_child_context(self): + """Test adding child resource context.""" + resc = 'thechild' + try: + # Create a unixfilesystem resource. + r = resources.create( + self.rodsadmin_session, + resc, + "unixfilesystem", + self.host, + "/tmp/resc_vault", # noqa: S108 + "", + ) + common.assert_success(self, r) - # Add the metadata to the resource - r = self.api.resources.modify_metadata("metadata_demo", operations) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + # Exercise add child with context + r = resources.add_child(self.rodsadmin_session, "demoResc", resc, context="neat") + common.assert_success(self, r) - # 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") + # Confirm + r = resources.stat(self.rodsadmin_session, resc) + common.assert_success(self, r) + # TODO(irods_client_http_api#473): uncomment once parent_context is available + # self.assertEqual(r["data"]["info"]["parent_context"], "neat") - # Remove the metadata from the resource. - operations = [{"operation": "remove", "attribute": "a1", "value": "v1", "units": "u1"}] + finally: + resources.remove_child(self.rodsadmin_session, "demoResc", resc) + resources.remove(self.rodsadmin_session, resc) - r = self.api.resources.modify_metadata("metadata_demo", operations) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + def test_modify_metadata(self): + """Test modifying metadata on resources.""" + resc = "metadata_resc" - # 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) + try: + # Create a unixfilesystem resource. + r = resources.create( + self.rodsadmin_session, + resc, + "unixfilesystem", + self.host, + "/tmp/metadata_demo_vault", # noqa: S108 + "ignoreme", + ) + common.assert_success(self, r) + + # Add the metadata to the resource + operations = [{"operation": "add", "attribute": "a1", "value": "v1", "units": "u1"}] + r = resources.modify_metadata(self.rodsadmin_session, resc, operations) + common.assert_success(self, r) + + # Show that the metadata is on the resource + r = queries.execute_genquery( + self.rodsadmin_session, + "select RESC_NAME where META_RESC_ATTR_NAME = 'a1' and " + "META_RESC_ATTR_VALUE = 'v1' and META_RESC_ATTR_UNITS = 'u1'", + ) + common.assert_success(self, r) + self.assertEqual(r["data"]["rows"][0][0], resc) + + # Remove the metadata from the resource. + operations = [{"operation": "remove", "attribute": "a1", "value": "v1", "units": "u1"}] + r = resources.modify_metadata(self.rodsadmin_session, resc, operations) + common.assert_success(self, r) + + # Show that the metadata is no longer on the resource + r = queries.execute_genquery( + self.rodsadmin_session, + "select RESC_NAME where META_RESC_ATTR_NAME = 'a1' and " + "META_RESC_ATTR_VALUE = 'v1' and META_RESC_ATTR_UNITS = 'u1'", + ) + common.assert_success(self, r) + self.assertEqual(len(r["data"]["rows"]), 0) - # Remove the resource - r = self.api.resources.remove("metadata_demo") + finally: + # Remove the resource + resources.remove(self.rodsadmin_session, resc) 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: + # Create a new resource. + r = resources.create(self.rodsadmin_session, resource, "replication", "", "", "") + common.assert_success(self, r) + # The list of updates to apply in sequence. property_map = [ ("name", "test_modifying_resource_properties_renamed"), @@ -1260,23 +1640,23 @@ def test_modify_properties(self): 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) + r = resources.modify(self.rodsadmin_session, 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) + r = resources.stat(self.rodsadmin_session, resource) + common.assert_success(self, r) self.assertEqual(r["data"]["info"][p], v) finally: # Remove the resource - r = self.api.resources.remove(resource) + resources.remove(self.rodsadmin_session, resource) # Tests for rule operations -class RulesTests(unittest.TestCase): +class RuleTests(unittest.TestCase): """Test iRODS rule operations.""" @classmethod @@ -1296,22 +1676,21 @@ def setUp(self): 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) + r = rules.list_rule_engines(self.rodsadmin_session) + common.assert_success(self, r) 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!" + test_msg = "Hello from the test suite" # Execute rule text against the iRODS rule language. - r = self.api.rules.execute( + r = rules.execute( + self.rodsadmin_session, f'writeLine("stdout", "{test_msg}")', "irods_rule_engine_plugin-irods_rule_language-instance", ) - - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) self.assertEqual(r["data"]["stderr"], None) # The REP always appends a newline character to the result. While we could trim the result, @@ -1322,26 +1701,26 @@ 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)") + try: + # Schedule a delay rule to execute in the distant future. + r = rules.execute( + self.rodsadmin_session, + f'delay("{rep_instance}1h") ' + f'{{ writeLine("serverLog", "test suite"); }}', + rep_instance, + ) + common.assert_success(self, r) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) - self.assertEqual(len(r["data"]["rows"]), 1) + # 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 = queries.execute_genquery(self.rodsadmin_session, "select max(RULE_EXEC_ID)") + common.assert_success(self, r) + 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) + finally: + # Remove the delay rule. + rules.remove_delay_rule(self.rodsadmin_session, int(r["data"]["rows"][0][0])) # Tests for query operations @@ -1362,31 +1741,44 @@ def setUp(self): """Check that class initialization succeeded before each test.""" self.assertFalse(self._class_init_error, "Class initialization failed. Cannot continue.") + def test_bad_query_type(self): + """Test with a query type that does not exist.""" + self.assertRaises( + ValueError, queries.execute_genquery, self.rodsadmin_session, "SELECT ZONE_NAME", parser="bad" + ) + + def test_query_parameters(self): + """Test with queries that exercise the options.""" + # genquery + queries.execute_genquery(self.rodsadmin_session, "SELECT ZONE_NAME", count=1) + queries.execute_genquery(self.rodsadmin_session, "SELECT ZONE_NAME", zone=self.zone_name) + queries.execute_genquery(self.rodsadmin_session, "SELECT ZONE_NAME", parser="genquery2", sql_only=1) + + # specific query + queries.execute_specific_query(self.rodsadmin_session, "ls", count=1) + queries.execute_specific_query(self.rodsadmin_session, "lsl", args="ls") + 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) + r = queries.add_specific_query(self.rodsadmin_session, name=name, sql=sql) + common.assert_success(self, r) - # 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") + # Execute as rodsuser + r = queries.execute_specific_query(self.rodsuser_session, name=name) + common.assert_success(self, r) + self.assertEqual(r["data"]["rows"][0][0], "4") finally: - # Switch to rodsadmin and remove it - self.api.set_token(self.rodsadmin_bearer_token) - r = self.api.queries.remove_specific_query(name=name) + # Remove as rodsadmin + queries.remove_specific_query(self.rodsadmin_session, name=name) # Tests for tickets operations -class TicketsTests(unittest.TestCase): +class TicketTests(unittest.TestCase): """Test iRODS ticket operations.""" @classmethod @@ -1403,56 +1795,84 @@ 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" - ) + def test_create_failures(self): + """Test ticket create failure modes.""" + p = f"/{self.zone_name}/home/{self.rodsuser_username}" + + # bad type + self.assertRaises(ValueError, tickets.create, self.rodsadmin_session, p, type="bad") - 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) + # bad object count + self.assertRaises( + ValueError, + tickets.create, + self.rodsadmin_session, + p, + type="write", + write_data_object_count=-5, + ) - # Remove the ticket. - r = self.api.tickets.remove(ticket_string) + # bad byte count + self.assertRaises( + ValueError, + tickets.create, + self.rodsadmin_session, + p, + type="write", + write_byte_count=-2, + ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + def test_create_and_remove(self): + """Test creating and removing tickets.""" + try: + # Create a write ticket. + ticket_type = "write" + ticket_path = f"/{self.zone_name}/home/{self.rodsuser_username}" + ticket_use_count = 2000 + ticket_write_data_object_count = 4 + ticket_write_byte_count = 1000 + ticket_groups = "public" + ticket_hosts = self.host + r = tickets.create( + self.rodsuser_session, + ticket_path, + ticket_type, + use_count=ticket_use_count, + write_data_object_count=ticket_write_data_object_count, + write_byte_count=ticket_write_byte_count, + seconds_until_expiration=3600, + users="rods,jeb", + groups=ticket_groups, + hosts=ticket_hosts, + ) + common.assert_success(self, r) + 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 = queries.execute_genquery( + self.rodsadmin_session, + "select TICKET_STRING, TICKET_TYPE, TICKET_COLL_NAME, TICKET_USES_LIMIT, " + "TICKET_WRITE_FILE_LIMIT, TICKET_WRITE_BYTE_LIMIT, " + "TICKET_ALLOWED_USER_NAME, TICKET_ALLOWED_GROUP_NAME, TICKET_ALLOWED_HOST", + ) - # Show the ticket no longer exists. - r = self.api.queries.execute_genquery("select TICKET_STRING") + common.assert_success(self, r) + 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.assertEqual(r["data"]["rows"][0][4], str(ticket_write_data_object_count)) + self.assertEqual(r["data"]["rows"][0][5], str(ticket_write_byte_count)) + self.assertIn(r["data"]["rows"][0][6], ["rods", "jeb"]) + self.assertEqual(r["data"]["rows"][0][7], ticket_groups) + self.assertGreater(len(r["data"]["rows"][0][6]), 0) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) - self.assertEqual(len(r["data"]["rows"]), 0) + finally: + # Remove the ticket. + tickets.remove(self.rodsuser_session, ticket_string) # Tests for user operations @@ -1473,283 +1893,211 @@ 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" + def test_create_with_bad_type(self): + """Test user create with bad type.""" + self.assertRaises( + ValueError, users_groups.create_user, self.rodsadmin_session, "baduser", self.zone_name, type="bad" + ) - # Create a new user. - r = self.api.users_groups.create_user(new_username, self.zone_name, user_type) - self.assertEqual(r["status_code"], 200) + def test_set_to_bad_type(self): + """Test setting user type to bad value.""" + self.assertRaises( + ValueError, users_groups.set_user_type, self.rodsadmin_session, "baduser", self.zone_name, type="bad" + ) - # Stat the user. - r = self.api.users_groups.stat(new_username, self.zone_name) - self.assertEqual(r["status_code"], 200) + def test_bad_connection(self): + """Test authenticate with a bad hostname.""" + self.assertRaises(RuntimeError, authenticate, "example.org", "bad", "bad") - 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) + def test_empty_username(self): + """Test authenticate with an empty username.""" + self.assertRaises(ValueError, authenticate, self.url_base, "", "nope") - # Remove the user. - r = self.api.users_groups.remove_user(new_username, self.zone_name) - self.assertEqual(r["status_code"], 200) + def test_bad_password(self): + """Test authenticate with a bad password.""" + self.assertRaises(RuntimeError, authenticate, self.url_base, self.rodsadmin_username, "nope") 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) + try: + # Create a new user. + r = users_groups.create_user(self.rodsadmin_session, new_username, self.zone_name, user_type) + common.assert_success(self, r) - new_username = "test_user_groupadmin" - user_type = "groupadmin" + # Set a new password + new_password = "new_password" # noqa: S105 + r = users_groups.set_password(self.rodsadmin_session, new_username, self.zone_name, new_password) + common.assert_success(self, r) - # Create a new user. - r = self.api.users_groups.create_user(new_username, self.zone_name, user_type) - self.assertEqual(r["status_code"], 200) + # Try to get a token for the user + session = authenticate(self.url_base, new_username, new_password) + common.assert_success(self, r) + self.assertIsInstance(session.token, str) - # Stat the user. - r = self.api.users_groups.stat(new_username, self.zone_name) - self.assertEqual(r["status_code"], 200) + finally: + # Remove the user. + users_groups.remove_user(self.rodsadmin_session, new_username, self.zone_name) + + def test_create_stat_and_remove_threetypes(self): + """Test creation and removal of three types of users.""" + new_username = "testuser" + user_types = ["rodsadmin", "groupadmin", "rodsuser"] + + for t in user_types: + with self.subTest(f"Testing for [{t}]"): + try: + # Create a new user. + r = users_groups.create_user(self.rodsadmin_session, new_username, self.zone_name, t) + 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) + # Stat the user. + r = users_groups.stat(self.rodsadmin_session, 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"], t) - # Remove the user. - r = self.api.users_groups.remove_user(new_username, self.zone_name) - self.assertEqual(r["status_code"], 200) + finally: + # Remove the user. + users_groups.remove_user(self.rodsadmin_session, new_username, self.zone_name) 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) + try: + # Create a new group. + new_group = "test_group" + r = users_groups.create_group(self.rodsadmin_session, new_group) + self.assertEqual(r["status_code"], 200) + common.assert_success(self, r) - # Remove the user. - r = self.api.users_groups.remove_user(new_username, self.zone_name) - self.assertEqual(r["status_code"], 200) + # Stat the group. + r = users_groups.stat(self.rodsadmin_session, 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 = users_groups.create_user(self.rodsadmin_session, new_username, self.zone_name, user_type) + common.assert_success(self, r) + + # Add user to group. + r = users_groups.add_to_group(self.rodsadmin_session, new_username, self.zone_name, new_group) + common.assert_success(self, r) + + # Show that the user is a member of the group. + r = users_groups.is_member_of_group(self.rodsadmin_session, new_group, new_username, self.zone_name) + common.assert_success(self, r) + self.assertEqual(r["data"]["is_member"], True) - # 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) + finally: + # Remove user from group. + users_groups.remove_from_group(self.rodsadmin_session, new_username, self.zone_name, new_group) - # 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) + # Remove the user. + users_groups.remove_user(self.rodsadmin_session, new_username, self.zone_name) - stat_info = r["data"] - self.assertEqual(stat_info["irods_response"]["status_code"], 0) - self.assertEqual(stat_info["exists"], False) + # Remove group. + users_groups.remove_group(self.rodsadmin_session, new_group) 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) + try: + # Create a new user. + new_username = "test_user_rodsuser" + user_type = "rodsuser" + r = users_groups.create_user(self.rodsadmin_session, new_username, self.zone_name, user_type) + common.assert_success(self, r) + + # Show that a rodsadmin can change the type of the new user. + new_user_type = "groupadmin" + r = users_groups.set_user_type(self.rodsadmin_session, new_username, self.zone_name, new_user_type) + common.assert_success(self, r) + + # Show that a non-admin cannot change the type of the new user. + r = users_groups.set_user_type(self.rodsuser_session, new_user_type, self.zone_name, new_user_type) + self.assertEqual(r["status_code"], 200) + self.assertEqual(r["data"]["irods_response"]["status_code"], -13000) # SYS_NO_API_PRIV - # 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) + # Show that the user type matches the type set by the rodsadmin. + r = users_groups.stat(self.rodsuser_session, new_username, self.zone_name) + common.assert_success(self, r) + self.assertEqual(r["data"]["exists"], True) + self.assertEqual(r["data"]["local_unique_name"], f"{new_username}#{self.zone_name}") + self.assertEqual(r["data"]["type"], new_user_type) + + finally: + # Remove the user. + users_groups.remove_user(self.rodsadmin_session, new_username, self.zone_name) 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"]) + r = users_groups.users(self.rodsadmin_session) + common.assert_success(self, r) + self.assertIn({"name": self.rodsadmin_username, "zone": self.zone_name}, r["data"]["users"]) + self.assertIn({"name": self.rodsuser_username, "zone": self.zone_name}, r["data"]["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) + try: + # Create a new group. + new_group = "test_group" + r = users_groups.create_group(self.rodsadmin_session, new_group) + common.assert_success(self, r) + + # Get all groups. + r = users_groups.groups( + self.rodsadmin_session, + ) + common.assert_success(self, r) + self.assertIn("public", r["data"]["groups"]) + self.assertIn(new_group, r["data"]["groups"]) + + finally: + # Remove the new group. + users_groups.remove_group(self.rodsadmin_session, new_group) 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) + r = users_groups.modify_metadata(self.rodsadmin_session, username, ops) + common.assert_success(self, r) # Show the metadata exists on the user. - r = self.api.queries.execute_genquery( + r = queries.execute_genquery( + self.rodsadmin_session, "select USER_NAME where META_USER_ATTR_NAME = 'a1' and " - "META_USER_ATTR_VALUE = 'v1' and META_USER_ATTR_UNITS = 'u1'" + "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) + common.assert_success(self, r) + self.assertEqual(r["data"]["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) + r = users_groups.modify_metadata(self.rodsadmin_session, username, ops) + common.assert_success(self, r) # Show the metadata no longer exists on the user. - r = self.api.queries.execute_genquery( + r = queries.execute_genquery( + self.rodsadmin_session, "select USER_NAME where META_USER_ATTR_NAME = 'a1' and " - "META_USER_ATTR_VALUE = 'v1' and META_USER_ATTR_UNITS = 'u1'" + "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) + common.assert_success(self, r) + self.assertEqual(len(r["data"]["rows"]), 0) # Tests for zone operations @@ -1772,41 +2120,40 @@ def setUp(self): 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"]) + r = zones.report( + self.rodsadmin_session, + ) + common.assert_success(self, r) + self.assertIn("zones", r["data"]["zone_report"]) + self.assertGreaterEqual(len(r["data"]["zone_report"]["zones"]), 1) + self.assertIn("schema_version", r["data"]["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) + try: + zone_name = "other_zone" - # 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) + # Add a zone, only to remove it immediately + r = zones.add(self.rodsadmin_session, zone_name, connection_info="localhost:1250", comment="brief") + common.assert_success(self, r) - 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) + # Remove it + r = zones.remove(self.rodsadmin_session, zone_name) + common.assert_success(self, r) - 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"], "") + # Add a remote zone to the local zone. + # The new zone will not have any connection information or anything else. + r = zones.add(self.rodsadmin_session, zone_name) + common.assert_success(self, r) + + # Show the new zone exists by executing the stat operation on it. + r = zones.stat(self.rodsadmin_session, zone_name) + common.assert_success(self, r) + self.assertEqual(r["data"]["exists"], True) + self.assertEqual(r["data"]["info"]["name"], zone_name) + self.assertEqual(r["data"]["info"]["type"], "remote") + self.assertEqual(r["data"]["info"]["connection_info"], "") + self.assertEqual(r["data"]["info"]["comment"], "") # The properties to update. property_map = [ @@ -1818,27 +2165,22 @@ def test_adding_removing_and_modifying_zones(self): # 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) + r = zones.modify(self.rodsadmin_session, zone_name, p, v) + common.assert_success(self, r) # 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) + r = zones.stat(self.rodsadmin_session, zone_name) + common.assert_success(self, r) + self.assertEqual(r["data"]["exists"], True) + self.assertEqual(r["data"]["info"][p], v) finally: # Remove the remote zone. - r = self.api.zones.remove(zone_name) - self.assertEqual(r["status_code"], 200) + zones.remove(self.rodsadmin_session, zone_name) if __name__ == "__main__":