From a729cd43065e9b806c0bbf66c40faab2ee68bf4a Mon Sep 17 00:00:00 2001 From: Terrell Russell Date: Thu, 19 Feb 2026 11:39:57 -0500 Subject: [PATCH 01/29] [48] redesign to stateless free functions with session class - created HTTPSession class in common.py to encapsulate base URL and token - updated modules and methods to accept HTTPSession instead of separate parameters - updated test suite to use HTTPSession objects --- irods_http_client/__init__.py | 30 +- irods_http_client/collection_operations.py | 340 ------- irods_http_client/collections.py | 353 ++++++++ irods_http_client/common.py | 36 +- irods_http_client/data_object_operations.py | 894 ------------------- irods_http_client/data_objects.py | 925 ++++++++++++++++++++ irods_http_client/irods_http_client.py | 167 ++-- irods_http_client/queries.py | 178 ++++ irods_http_client/query_operations.py | 187 ---- irods_http_client/resource_operations.py | 280 ------ irods_http_client/resources.py | 275 ++++++ irods_http_client/rule_operations.py | 90 -- irods_http_client/rules.py | 81 ++ irods_http_client/ticket_operations.py | 113 --- irods_http_client/tickets.py | 102 +++ irods_http_client/user_group_operations.py | 380 -------- irods_http_client/users_groups.py | 382 ++++++++ irods_http_client/zone_operations.py | 145 --- irods_http_client/zones.py | 141 +++ test/test_endpoint_operations.py | 756 ++++++++-------- 20 files changed, 2970 insertions(+), 2885 deletions(-) delete mode 100644 irods_http_client/collection_operations.py create mode 100644 irods_http_client/collections.py delete mode 100644 irods_http_client/data_object_operations.py create mode 100644 irods_http_client/data_objects.py create mode 100644 irods_http_client/queries.py delete mode 100644 irods_http_client/query_operations.py delete mode 100644 irods_http_client/resource_operations.py create mode 100644 irods_http_client/resources.py delete mode 100644 irods_http_client/rule_operations.py create mode 100644 irods_http_client/rules.py delete mode 100644 irods_http_client/ticket_operations.py create mode 100644 irods_http_client/tickets.py delete mode 100644 irods_http_client/user_group_operations.py create mode 100644 irods_http_client/users_groups.py delete mode 100644 irods_http_client/zone_operations.py create mode 100644 irods_http_client/zones.py diff --git a/irods_http_client/__init__.py b/irods_http_client/__init__.py index e69d90d..2823d7b 100644 --- a/irods_http_client/__init__.py +++ b/irods_http_client/__init__.py @@ -1,3 +1,31 @@ """iRODS HTTP client library for Python.""" -from .irods_http_client import IRODSHTTPClient as IRODSHTTPClient +from . import ( + collections as collections, + data_objects as data_objects, + queries as queries, + resources as resources, + rules as rules, + tickets as tickets, + users_groups as users_groups, + zones as zones, +) +from .common import HTTPSession as HTTPSession +from .irods_http_client import ( + authenticate as authenticate, + get_server_info as get_server_info, +) + +__all__ = [ + "HTTPSession", + "authenticate", + "collections", + "data_objects", + "get_server_info", + "queries", + "resources", + "rules", + "tickets", + "users_groups", + "zones", +] 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/collections.py b/irods_http_client/collections.py new file mode 100644 index 0000000..fd17414 --- /dev/null +++ b/irods_http_client/collections.py @@ -0,0 +1,353 @@ +"""Collection operations for iRODS HTTP API.""" + +import json + +import requests + +from . import common + + +def create(session: common.HTTPSession, lpath: str, create_intermediates: int = 0) -> dict: + """ + Create a new collection. + + Args: + session: common.HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = { + "op": "create", + "lpath": lpath, + "create-intermediates": create_intermediates, + } + + r = requests.post(session.url_base + "/collections", headers=headers, data=data) # noqa: S113 + return common.process_response(r) + + +def remove(session: common.HTTPSession, lpath: str, recurse: int = 0, no_trash: int = 0) -> dict: + """ + Remove an existing collection. + + Args: + session: common.HTTPSession 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 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.validate_not_none(session.token) + common.validate_instance(lpath, str) + common.validate_0_or_1(recurse) + common.validate_0_or_1(no_trash) + + headers = { + "Authorization": "Bearer " + session.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = { + "op": "remove", + "lpath": lpath, + "recurse": recurse, + "no-trash": no_trash, + } + + r = requests.post(session.url_base + "/collections", headers=headers, data=data) # noqa: S113 + return common.process_response(r) + + +def stat(session: common.HTTPSession, lpath: str, ticket: str = "") -> dict: + """ + Give information about a collection. + + Args: + session: common.HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.token, + } + + params = {"op": "stat", "lpath": lpath, "ticket": ticket} + + r = requests.get(session.url_base + "/collections", params=params, headers=headers) # noqa: S113 + return common.process_response(r) + + +def list_collection(session: common.HTTPSession, lpath: str, recurse: int = 0, ticket: str = "") -> dict: + """ + Show the contents of a collection. + + Args: + session: common.HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.token, + } + + params = {"op": "list", "lpath": lpath, "recurse": recurse, "ticket": ticket} + + r = requests.get(session.url_base + "/collections", params=params, headers=headers) # noqa: S113 + return common.process_response(r) + + +def set_permission( + session: common.HTTPSession, + lpath: str, + entity_name: str, + permission: str, + admin: int = 0, +) -> dict: + """ + Set the permission of a user for a given collection. + + Args: + session: common.HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.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(session.url_base + "/collections", headers=headers, data=data) # noqa: S113 + return common.process_response(r) + + +def set_inheritance(session: common.HTTPSession, lpath: str, enable: int, admin: int = 0) -> dict: + """ + Set the inheritance for a collection. + + Args: + session: common.HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = { + "op": "set_inheritance", + "lpath": lpath, + "enable": enable, + "admin": admin, + } + + r = requests.post(session.url_base + "/collections", headers=headers, data=data) # noqa: S113 + return common.process_response(r) + + +def modify_permissions(session: common.HTTPSession, lpath: str, operations: dict, admin: int = 0) -> dict: + """ + Modify permissions for multiple users or groups for a collection. + + Args: + session: common.HTTPSession 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, list) + common.validate_instance(operations[0], dict) + common.validate_0_or_1(admin) + + headers = { + "Authorization": "Bearer " + session.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = { + "op": "modify_permissions", + "lpath": lpath, + "operations": json.dumps(operations), + "admin": admin, + } + + r = requests.post(session.url_base + "/collections", headers=headers, data=data) # noqa: S113 + return common.process_response(r) + + +def modify_metadata(session: common.HTTPSession, lpath: str, operations: dict, admin: int = 0) -> dict: + """ + Modify the metadata for a collection. + + Args: + session: common.HTTPSession 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, list) + common.validate_instance(operations[0], dict) + common.validate_0_or_1(admin) + + headers = { + "Authorization": "Bearer " + session.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = { + "op": "modify_metadata", + "lpath": lpath, + "operations": json.dumps(operations), + "admin": admin, + } + + r = requests.post(session.url_base + "/collections", headers=headers, data=data) # noqa: S113 + return common.process_response(r) + + +def rename(session: common.HTTPSession, old_lpath: str, new_lpath: str) -> dict: + """ + Rename or move a collection. + + Args: + session: common.HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = {"op": "rename", "old-lpath": old_lpath, "new-lpath": new_lpath} + + r = requests.post(session.url_base + "/collections", headers=headers, data=data) # noqa: S113 + return common.process_response(r) + + +def touch(session: common.HTTPSession, lpath: str, seconds_since_epoch: int = -1, reference: str = "") -> dict: + """ + Update mtime for a collection. + + Args: + session: common.HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.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(session.url_base + "/collections", headers=headers, data=data) # noqa: S113 + return common.process_response(r) diff --git a/irods_http_client/common.py b/irods_http_client/common.py index e24210a..4bade12 100644 --- a/irods_http_client/common.py +++ b/irods_http_client/common.py @@ -1,6 +1,30 @@ """Common utility functions for iRODS HTTP client operations.""" +class HTTPSession: + """ + 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 HTTPSession 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 + + def process_response(r): """ Process an HTTP response and return standardized response dict. @@ -16,18 +40,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): 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/data_objects.py b/irods_http_client/data_objects.py new file mode 100644 index 0000000..5c38843 --- /dev/null +++ b/irods_http_client/data_objects.py @@ -0,0 +1,925 @@ +"""Data object operations for iRODS HTTP API.""" + +import json + +import requests + +from . import common + + +def touch( + session: common.HTTPSession, + 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: common.HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.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(session.url_base + "/data-objects", headers=headers, data=data) # noqa: S113 + return common.process_response(r) + + +def remove(session: common.HTTPSession, lpath: str, catalog_only: int = 0, no_trash: int = 0, admin: int = 0) -> dict: + """ + Remove an existing data object. + + Args: + session: common.HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.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(session.url_base + "/data-objects", headers=headers, data=data) # noqa: S113 + return common.process_response(r) + + +def calculate_checksum( + session: common.HTTPSession, + lpath: str, + resource: str = "", + replica_number: int = -1, + force: int = 0, + all_: int = 0, + admin: int = 0, +) -> dict: + """ + Calculate the checksum for a data object. + + Args: + session: common.HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.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(session.url_base + "/data-objects", headers=headers, data=data) # noqa: S113 + return common.process_response(r) + + +def verify_checksum( + session: common.HTTPSession, + 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: common.HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.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(session.url_base + "/data-objects", headers=headers, data=data) # noqa: S113 + return common.process_response(r) + + +def stat(session: common.HTTPSession, lpath: str, ticket: str = "") -> dict: + """ + Give information about a data object. + + Args: + session: common.HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.token, + } + + params = {"op": "stat", "lpath": lpath, "ticket": ticket} + + r = requests.get(session.url_base + "/data-objects", params=params, headers=headers) # noqa: S113 + return common.process_response(r) + + +def rename(session: common.HTTPSession, old_lpath: str, new_lpath: str) -> dict: + """ + Rename or move a data object. + + Args: + session: common.HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = {"op": "rename", "old-lpath": old_lpath, "new-lpath": new_lpath} + + r = requests.post(session.url_base + "/data-objects", headers=headers, data=data) # noqa: S113 + return common.process_response(r) + + +def copy( + session: common.HTTPSession, + src_lpath: str, + dst_lpath: str, + src_resource: str = "", + dst_resource: str = "", + overwrite: int = 0, +) -> dict: + """ + Copy a data object. + + Args: + session: common.HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.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(session.url_base + "/data-objects", headers=headers, data=data) # noqa: S113 + return common.process_response(r) + + +def replicate( + session: common.HTTPSession, + lpath: str, + src_resource: str = "", + dst_resource: str = "", + admin: int = 0, +) -> dict: + """ + Replicates a data object from one resource to another. + + Args: + session: common.HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.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(session.url_base + "/data-objects", headers=headers, data=data) # noqa: S113 + return common.process_response(r) + + +def trim(session: common.HTTPSession, lpath: str, replica_number: int, catalog_only: int = 0, admin: int = 0) -> dict: + """ + Trims an existing replica or removes its catalog entry. + + Args: + session: common.HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.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(session.url_base + "/data-objects", headers=headers, data=data) # noqa: S113 + return common.process_response(r) + + +def register( + session: common.HTTPSession, + lpath: str, + ppath: str, + resource: str, + as_additional_replica: int = 0, + data_size: int = -1, + checksum: str = "", +) -> dict: + """ + Register a data object/replica into the catalog. + + Args: + session: common.HTTPSession 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: The checksum to associate with the replica. 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_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 " + session.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(session.url_base + "/data-objects", headers=headers, data=data) # noqa: S113 + return common.process_response(r) + + +def read(session: common.HTTPSession, lpath: str, offset: int = 0, count: int = -1, ticket: str = "") -> dict: + """ + Read bytes from a data object. + + Args: + session: common.HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.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(session.url_base + "/data-objects", params=params, headers=headers) # noqa: S113 + # 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( + session: common.HTTPSession, + bytes_, + lpath: str = "", + resource: str = "", + offset: int = 0, + truncate: int = 1, + append: int = 0, + parallel_write_handle: str = "", + stream_index: int = -1, +) -> dict: + """ + Write bytes to a data object. + + Args: + session: common.HTTPSession 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. + + 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.validate_not_none(session.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 " + session.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(session.url_base + "/data-objects", headers=headers, data=data) # noqa: S113 + return common.process_response(r) + + +def parallel_write_init( + session: common.HTTPSession, + lpath: str, + stream_count: int, + truncate: int = 1, + append: int = 0, + ticket: str = "", +) -> dict: + """ + Initialize server-side state for parallel writing. + + Args: + session: common.HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.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(session.url_base + "/data-objects", headers=headers, data=data) # noqa: S113 + return common.process_response(r) + + +def parallel_write_shutdown(session: common.HTTPSession, parallel_write_handle: str) -> dict: + """ + Shuts down the parallel write state in the server. + + Args: + session: common.HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = { + "op": "parallel_write_shutdown", + "parallel-write-handle": parallel_write_handle, + } + + r = requests.post(session.url_base + "/data-objects", headers=headers, data=data) # noqa: S113 + return common.process_response(r) + + +def modify_metadata(session: common.HTTPSession, lpath: str, operations: list, admin: int = 0) -> dict: + """ + Modify the metadata for a data object. + + Args: + session: common.HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = { + "op": "modify_metadata", + "lpath": lpath, + "operations": json.dumps(operations), + "admin": admin, + } + + r = requests.post(session.url_base + "/data-objects", headers=headers, data=data) # noqa: S113 + return common.process_response(r) + + +def set_permission(session: common.HTTPSession, lpath: str, entity_name: str, permission: str, admin: int = 0) -> dict: + """ + Set the permission of a user for a given data object. + + Args: + session: common.HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.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(session.url_base + "/data-objects", headers=headers, data=data) # noqa: S113 + return common.process_response(r) + + +def modify_permissions(session: common.HTTPSession, lpath: str, operations: list, admin: int = 0) -> dict: + """ + Modify permissions for multiple users or groups for a data object. + + Args: + session: common.HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = { + "op": "modify_permissions", + "lpath": lpath, + "operations": json.dumps(operations), + "admin": admin, + } + + r = requests.post(session.url_base + "/data-objects", headers=headers, data=data) # noqa: S113 + return common.process_response(r) + + +def modify_replica( + session: common.HTTPSession, + 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, +) -> 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: common.HTTPSession 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 -1. + new_data_expiry: The new expiry to be set. Defaults to -1. + 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_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 " + session.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(session.url_base + "/data-objects", headers=headers, data=data) # noqa: S113 + return common.process_response(r) diff --git a/irods_http_client/irods_http_client.py b/irods_http_client/irods_http_client.py index 10bda02..6123f77 100644 --- a/irods_http_client/irods_http_client.py +++ b/irods_http_client/irods_http_client.py @@ -3,104 +3,69 @@ 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) + + +def authenticate(url_base: str, username: str, password: str) -> str: + """ + 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: + 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 common.HTTPSession(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: common.HTTPSession): + """ + Get general information about the iRODS server. + + Args: + session: An HTTPSession 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_client/queries.py b/irods_http_client/queries.py new file mode 100644 index 0000000..b2d5699 --- /dev/null +++ b/irods_http_client/queries.py @@ -0,0 +1,178 @@ +"""Query operations for iRODS HTTP API.""" + +import requests + +from . import common + + +def execute_genquery( + session: common.HTTPSession, + 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 HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.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(session.url_base + "/query", headers=headers, params=params) # noqa: S113 + return common.process_response(r) + + +def execute_specific_query( + session: common.HTTPSession, + name: str, + args: str = "", + args_delimiter: str = ",", + offset: int = 0, + count: int = -1, +): + """ + Execute a specific query and returns the results. + + Args: + session: An HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.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(session.url_base + "/query", headers=headers, params=params) # noqa: S113 + return common.process_response(r) + + +def add_specific_query(session: common.HTTPSession, name: str, sql: str): + """ + Add a SpecificQuery to the iRODS zone. + + Args: + session: An HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.token, + } + + data = {"op": "add_specific_query", "name": name, "sql": sql} + + r = requests.post(session.url_base + "/query", headers=headers, data=data) # noqa: S113 + return common.process_response(r) + + +def remove_specific_query(session: common.HTTPSession, name: str): + """ + Remove a SpecificQuery from the iRODS zone. + + Args: + session: An HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.token, + } + + data = {"op": "remove_specific_query", "name": name} + + r = requests.post(session.url_base + "/query", headers=headers, data=data) # noqa: S113 + 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/resources.py b/irods_http_client/resources.py new file mode 100644 index 0000000..cd1c807 --- /dev/null +++ b/irods_http_client/resources.py @@ -0,0 +1,275 @@ +"""Resource operations for iRODS HTTP API.""" + +import json + +import requests + +from . import common + + +def create(session: common.HTTPSession, name: str, type_: str, host: str, vault_path: str, context: str): + """ + Create a new resource. + + Args: + session: An HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.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(session.url_base + "/resources", headers=headers, data=data) # noqa: S113 + return common.process_response(r) + + +def remove(session: common.HTTPSession, name: str): + """ + Remove an existing resource. + + Args: + session: An HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = {"op": "remove", "name": name} + + r = requests.post(session.url_base + "/resources", headers=headers, data=data) # noqa: S113 + return common.process_response(r) + + +def modify(session: common.HTTPSession, name: str, property_: str, value: str): + """ + Modify a property for a resource. + + Args: + session: An HTTPSession 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'") + + headers = { + "Authorization": "Bearer " + session.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = {"op": "modify", "name": name, "property": property_, "value": value} + + r = requests.post(session.url_base + "/resources", headers=headers, data=data) # noqa: S113 + return common.process_response(r) + + +def add_child(session: common.HTTPSession, parent_name: str, child_name: str, context: str = ""): + """ + Create a parent-child relationship between two resources. + + Args: + session: An HTTPSession instance. + 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.validate_instance(parent_name, str) + common.validate_instance(child_name, str) + common.validate_instance(context, str) + + headers = { + "Authorization": "Bearer " + session.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(session.url_base + "/resources", headers=headers, data=data) # noqa: S113 + return common.process_response(r) + + +def remove_child(session: common.HTTPSession, parent_name: str, child_name: str): + """ + Remove a parent-child relationship between two resources. + + Args: + session: An HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = { + "op": "remove_child", + "parent-name": parent_name, + "child-name": child_name, + } + + r = requests.post(session.url_base + "/resources", headers=headers, data=data) # noqa: S113 + return common.process_response(r) + + +def rebalance(session: common.HTTPSession, name: str): + """ + Rebalance a resource hierarchy. + + Args: + session: An HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = {"op": "rebalance", "name": name} + + r = requests.post(session.url_base + "/resources", headers=headers, data=data) # noqa: S113 + return common.process_response(r) + + +def stat(session: common.HTTPSession, name: str): + """ + Retrieve information for a resource. + + Args: + session: An HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + params = {"op": "stat", "name": name} + + r = requests.get(session.url_base + "/resources", headers=headers, params=params) # noqa: S113 + return common.process_response(r) + + +def modify_metadata(session: common.HTTPSession, name: str, operations: dict, admin: int = 0): + """ + Modify the metadata for a resource. + + Args: + session: An HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = { + "op": "modify_metadata", + "name": name, + "operations": json.dumps(operations), + "admin": admin, + } + + r = requests.post(session.url_base + "/resources", headers=headers, data=data) # noqa: S113 + 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/rules.py b/irods_http_client/rules.py new file mode 100644 index 0000000..5c46d21 --- /dev/null +++ b/irods_http_client/rules.py @@ -0,0 +1,81 @@ +"""Rule operations for iRODS HTTP API.""" + +import requests + +from . import common + + +def list_rule_engines(session: common.HTTPSession): + """ + List available rule engine plugin instances. + + Args: + session: An HTTPSession 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, + } + + params = {"op": "list_rule_engines"} + + r = requests.get(session.url_base + "/rules", params=params, headers=headers) # noqa: S113 + return common.process_response(r) + + +def execute(session: common.HTTPSession, rule_text: str, rep_instance: str = ""): + """ + Execute rule code. + + Args: + session: An HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.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(session.url_base + "/rules", headers=headers, data=data) # noqa: S113 + return common.process_response(r) + + +def remove_delay_rule(session: common.HTTPSession, rule_id: int): + """ + Remove a delay rule from the catalog. + + Args: + session: An HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = {"op": "remove_delay_rule", "rule-id": rule_id} + + r = requests.post(session.url_base + "/rules", headers=headers, data=data) # noqa: S113 + 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/tickets.py b/irods_http_client/tickets.py new file mode 100644 index 0000000..ba3b13c --- /dev/null +++ b/irods_http_client/tickets.py @@ -0,0 +1,102 @@ +"""Ticket operations for iRODS HTTP API.""" + +import requests + +from . import common + + +def create( + session: common.HTTPSession, + 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: + session: An HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.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(session.url_base + "/tickets", headers=headers, data=data) # noqa: S113 + return common.process_response(r) + + +def remove(session: common.HTTPSession, name: str): + """ + Remove an existing ticket. + + Args: + session: An HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = {"op": "remove", "name": name} + + r = requests.post(session.url_base + "/tickets", headers=headers, data=data) # noqa: S113 + 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/users_groups.py b/irods_http_client/users_groups.py new file mode 100644 index 0000000..d1d5e73 --- /dev/null +++ b/irods_http_client/users_groups.py @@ -0,0 +1,382 @@ +"""User and group operations for iRODS HTTP API.""" + +import json + +import requests + +from . import common + + +def create_user(session: common.HTTPSession, name: str, zone: str, user_type: str = "rodsuser"): + """ + Create a new user. Requires rodsadmin or groupadmin privileges. + + Args: + session: An HTTPSession instance. + 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.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 " + session.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = {"op": "create_user", "name": name, "zone": zone, "user-type": user_type} + + r = requests.post(session.url_base + "/users-groups", headers=headers, data=data) # noqa: S113 + return common.process_response(r) + + +def remove_user(session: common.HTTPSession, name: str, zone: str): + """ + Remove a user. Requires rodsadmin privileges. + + Args: + session: An HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = {"op": "remove_user", "name": name, "zone": zone} + + r = requests.post(session.url_base + "/users-groups", headers=headers, data=data) # noqa: S113 + return common.process_response(r) + + +def set_password(session: common.HTTPSession, name: str, zone: str, new_password: str = ""): + """ + Change a users password. Requires rodsadmin privileges. + + Args: + session: An HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = { + "op": "set_password", + "name": name, + "zone": zone, + "new-password": new_password, + } + + r = requests.post(session.url_base + "/users-groups", headers=headers, data=data) # noqa: S113 + return common.process_response(r) + + +def set_user_type(session: common.HTTPSession, name: str, zone: str, user_type: str): + """ + Change a users type. Requires rodsadmin privileges. + + Args: + session: An HTTPSession instance. + 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.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 " + session.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(session.url_base + "/users-groups", headers=headers, data=data) # noqa: S113 + return common.process_response(r) + + +def create_group(session: common.HTTPSession, name: str): + """ + Create a new group. Requires rodsadmin or groupadmin privileges. + + Args: + session: An HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = {"op": "create_group", "name": name} + + r = requests.post(session.url_base + "/users-groups", headers=headers, data=data) # noqa: S113 + return common.process_response(r) + + +def remove_group(session: common.HTTPSession, name: str): + """ + Remove a group. Requires rodsadmin privileges. + + Args: + session: An HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = {"op": "remove_group", "name": name} + + r = requests.post(session.url_base + "/users-groups", headers=headers, data=data) # noqa: S113 + return common.process_response(r) + + +def add_to_group(session: common.HTTPSession, user: str, zone: str, group: str = ""): + """ + Add a user to a group. Requires rodsadmin or groupadmin privileges. + + Args: + session: An HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = {"op": "add_to_group", "user": user, "zone": zone, "group": group} + + r = requests.post(session.url_base + "/users-groups", headers=headers, data=data) # noqa: S113 + return common.process_response(r) + + +def remove_from_group(session: common.HTTPSession, user: str, zone: str, group: str): + """ + Remove a user from a group. Requires rodsadmin or groupadmin privileges. + + Args: + session: An HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = {"op": "remove_from_group", "user": user, "zone": zone, "group": group} + + r = requests.post(session.url_base + "/users-groups", headers=headers, data=data) # noqa: S113 + return common.process_response(r) + + +def users(session: common.HTTPSession): + """ + List all users in the zone. Requires rodsadmin privileges. + + Args: + session: An HTTPSession 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} + + params = {"op": "users"} + + r = requests.get(session.url_base + "/users-groups", headers=headers, params=params) # noqa: S113 + return common.process_response(r) + + +def groups(session: common.HTTPSession): + """ + List all groups in the zone. Requires rodsadmin privileges. + + Args: + session: An HTTPSession 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, + } + + params = {"op": "groups"} + + r = requests.get(session.url_base + "/users-groups", headers=headers, params=params) # noqa: S113 + return common.process_response(r) + + +def is_member_of_group(session: common.HTTPSession, group: str, user: str, zone: str): + """ + Return whether a user is a member of a group or not. + + Args: + session: An HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + params = { + "op": "is_member_of_group", + "group": group, + "user": user, + "zone": zone, + } + + r = requests.get(session.url_base + "/users-groups", headers=headers, params=params) # noqa: S113 + return common.process_response(r) + + +def stat(session: common.HTTPSession, name: str, zone: str = ""): + """ + Return information about a user or group. + + Args: + session: An HTTPSession 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) + + headers = {"Authorization": "Bearer " + session.token} + + params = {"op": "stat", "name": name} + + if zone != "": + params["zone"] = zone + + r = requests.get(session.url_base + "/users-groups", headers=headers, params=params) # noqa: S113 + return common.process_response(r) + + +def modify_metadata(session: common.HTTPSession, name: str, operations: list): + """ + Modify the metadata for a user or group. Requires rodsadmin privileges. + + Args: + session: An HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = { + "op": "modify_metadata", + "name": name, + "operations": json.dumps(operations), + } + + r = requests.post(session.url_base + "/users-groups", headers=headers, data=data) # noqa: S113 + 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/irods_http_client/zones.py b/irods_http_client/zones.py new file mode 100644 index 0000000..acafb1f --- /dev/null +++ b/irods_http_client/zones.py @@ -0,0 +1,141 @@ +"""Zone operations for iRODS HTTP API.""" + +import requests + +from . import common + + +def add(session: common.HTTPSession, name: str, connection_info: str = "", comment: str = ""): + """ + Add a remote zone to the local zone. Requires rodsadmin privileges. + + Args: + session: An HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.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(session.url_base + "/zones", headers=headers, data=data) # noqa: S113 + return common.process_response(r) + + +def remove(session: common.HTTPSession, name: str): + """ + Remove a remote zone from the local zone. Requires rodsadmin privileges. + + Args: + session: An HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = {"op": "remove", "name": name} + + r = requests.post(session.url_base + "/zones", headers=headers, data=data) # noqa: S113 + return common.process_response(r) + + +def modify(session: common.HTTPSession, name: str, property_: str, value: str): + """ + Modify properties of a remote zone. Requires rodsadmin privileges. + + Args: + session: An HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + data = {"op": "modify", "name": name, "property": property_, "value": value} + + r = requests.post(session.url_base + "/zones", headers=headers, data=data) # noqa: S113 + return common.process_response(r) + + +def report(session: common.HTTPSession): + """ + Return information about the iRODS zone. Requires rodsadmin privileges. + + Args: + session: An HTTPSession 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, + "Content-Type": "application/x-www-form-urlencoded", + } + + params = {"op": "report"} + + r = requests.get(session.url_base + "/zones", headers=headers, params=params) # noqa: S113 + return common.process_response(r) + + +def stat(session: common.HTTPSession, name: str): + """ + Return information about a named iRODS zone. Requires rodsadmin privileges. + + Args: + session: An HTTPSession 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) + + headers = { + "Authorization": "Bearer " + session.token, + "Content-Type": "application/x-www-form-urlencoded", + } + + params = {"op": "stat", "name": name} + + r = requests.get(session.url_base + "/zones", headers=headers, params=params) # noqa: S113 + return common.process_response(r) diff --git a/test/test_endpoint_operations.py b/test/test_endpoint_operations.py index 377579b..d31f8b7 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_client 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,30 +83,31 @@ 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 @@ -118,12 +129,12 @@ 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) # Tests for library class LibraryTests(unittest.TestCase): - """Test library-level operations (info, get_token).""" + """Test library-level operations.""" @classmethod def setUpClass(cls): @@ -142,16 +153,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,40 +184,40 @@ 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) - # 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") + 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") # test param checking - self.assertRaises(TypeError, self.api.collections.create, 0, 0) + self.assertRaises(TypeError, collections.create, self.rodsadmin_session, 0, 0) self.assertRaises( TypeError, - self.api.collections.create, + collections.create, + self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}", "0", ) self.assertRaises( ValueError, - self.api.collections.create, + collections.create, + self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}", 7, ) # test creating new collection - response = self.api.collections.create(f"/{self.zone_name}/home/new") + response = collections.create(self.rodsadmin_session, 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") + response = collections.create(self.rodsadmin_session, 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") + response = collections.create(self.rodsadmin_session, f"{self.zone_name}/home/new") self.assertEqual( "{'irods_response': {'status_code': -358000, " "'status_message': 'path does not exist: OBJ_PATH_DOES_NOT_EXIST'}}", @@ -212,13 +225,13 @@ def test_create(self): ) # test create_intermediates - response = self.api.collections.create(f"/{self.zone_name}/home/test/folder", 0) + response = collections.create(self.rodsadmin_session, 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) + response = collections.create(self.rodsadmin_session, f"/{self.zone_name}/home/test/folder", 1) self.assertEqual( "{'created': True, 'irods_response': {'status_code': 0}}", str(response["data"]), @@ -227,156 +240,167 @@ def test_create(self): # 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") + 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") # test param checking - self.assertRaises(TypeError, self.api.collections.remove, 0, 0, 0) + self.assertRaises(TypeError, collections.remove, self.rodsadmin_session, 0, 0, 0) self.assertRaises( TypeError, - self.api.collections.remove, + collections.remove, + self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}", "0", 0, ) self.assertRaises( ValueError, - self.api.collections.remove, + collections.remove, + self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}", 5, 0, ) self.assertRaises( TypeError, - self.api.collections.remove, + collections.remove, + self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}", 0, "0", ) self.assertRaises( ValueError, - self.api.collections.remove, + collections.remove, + self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}", 0, 5, ) # test removing collection - response = self.api.collections.create(f"/{self.zone_name}/home/new") + response = collections.create(self.rodsadmin_session, 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") + response = collections.remove(self.rodsadmin_session, 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") + response = collections.stat(self.rodsadmin_session, 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") + response = collections.stat(self.rodsadmin_session, 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") + response = collections.stat(self.rodsadmin_session, 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}") + response = collections.stat(self.rodsadmin_session, 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) + response = collections.create(self.rodsadmin_session, 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) + response = collections.remove(self.rodsadmin_session, 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) + response = collections.remove(self.rodsadmin_session, f"/{self.zone_name}/home/test", 1) self.assertEqual("{'irods_response': {'status_code': 0}}", str(response["data"])) # tests the stat operation def test_stat(self): """Test collection stat operation to retrieve metadata.""" - self.api.set_token(self.rodsadmin_bearer_token) - # clean up test collections - self.api.collections.remove(f"/{self.zone_name}/home/new") + collections.remove(self.rodsadmin_session, f"/{self.zone_name}/home/new") # test param checking - self.assertRaises(TypeError, self.api.collections.stat, 0, "ticket") + self.assertRaises(TypeError, collections.stat, self.rodsadmin_session, 0, "ticket") self.assertRaises( TypeError, - self.api.collections.stat, + collections.stat, + self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}", 0, ) # test invalid paths - response = self.api.collections.stat(f"/{self.zone_name}/home/new") + response = collections.stat(self.rodsadmin_session, 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") + response = collections.stat(self.rodsadmin_session, f"{self.zone_name}/home/new") self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) # test valid path - response = self.api.collections.stat(f"/{self.zone_name}/home/{self.rodsadmin_username}") + response = collections.stat(self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}") self.assertTrue(response["data"]["permissions"]) # tests the list operation def test_list(self): """Test collection list operation to enumerate contents.""" - self.api.set_token(self.rodsadmin_bearer_token) - # clean up test collections - self.api.collections.remove(f"/{self.zone_name}/home/{self.rodsadmin_username}/croatia/zagreb") - self.api.collections.remove(f"/{self.zone_name}/home/{self.rodsadmin_username}/albania") - self.api.collections.remove(f"/{self.zone_name}/home/{self.rodsadmin_username}/bosnia") - self.api.collections.remove(f"/{self.zone_name}/home/{self.rodsadmin_username}/croatia") + 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") # test param checking - self.assertRaises(TypeError, self.api.collections.list, 0, "ticket") + self.assertRaises(TypeError, collections.list_collection, self.rodsadmin_session, 0, "ticket") self.assertRaises( TypeError, - self.api.collections.list, + collections.list_collection, + self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}", "0", "ticket", ) self.assertRaises( ValueError, - self.api.collections.list, + collections.list_collection, + self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}", 5, "ticket", ) self.assertRaises( TypeError, - self.api.collections.list, + collections.list_collection, + self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}", 0, 0, ) # test empty collection - response = self.api.collections.list(f"/{self.zone_name}/home/{self.rodsadmin_username}") + response = collections.list_collection( + self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}" + ) self.assertEqual("None", str(response["data"]["entries"])) # test collection with one item - self.api.collections.create(f"/{self.zone_name}/home/{self.rodsadmin_username}/bosnia") - response = self.api.collections.list(f"/{self.zone_name}/home/{self.rodsadmin_username}") + collections.create(self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}/bosnia") + response = collections.list_collection( + self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}" + ) self.assertEqual( f"/{self.zone_name}/home/{self.rodsadmin_username}/bosnia", str(response["data"]["entries"][0]), ) # test collection with multiple items - self.api.collections.create(f"/{self.zone_name}/home/{self.rodsadmin_username}/albania") - self.api.collections.create(f"/{self.zone_name}/home/{self.rodsadmin_username}/croatia") - response = self.api.collections.list(f"/{self.zone_name}/home/{self.rodsadmin_username}") + 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") + response = collections.list_collection( + self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}" + ) self.assertEqual( f"/{self.zone_name}/home/{self.rodsadmin_username}/albania", str(response["data"]["entries"][0]), @@ -391,8 +415,13 @@ def test_list(self): ) # 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}") + collections.create( + self.rodsadmin_session, + f"/{self.zone_name}/home/{self.rodsadmin_username}/croatia/zagreb", + ) + response = collections.list_collection( + self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}" + ) self.assertEqual( f"/{self.zone_name}/home/{self.rodsadmin_username}/albania", str(response["data"]["entries"][0]), @@ -408,7 +437,9 @@ def test_list(self): self.assertEqual(len(response["data"]["entries"]), 3) # test with recursion - response = self.api.collections.list(f"/{self.zone_name}/home/{self.rodsadmin_username}", 1) + response = collections.list_collection( + self.rodsadmin_session, 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]), @@ -429,13 +460,12 @@ def test_list(self): # 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 +473,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,7 +500,8 @@ 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", @@ -467,244 +509,255 @@ def test_set_permission(self): ) # create new collection - response = self.api.collections.create(f"/{self.zone_name}/home/setPerms") + response = collections.create(self.rodsadmin_session, 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") + response = collections.stat(self.rodsuser_session, 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" + response = collections.set_permission( + self.rodsadmin_session, + 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") + response = collections.stat(self.rodsadmin_session, 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" + response = collections.set_permission( + self.rodsadmin_session, + f"/{self.zone_name}/home/setPerms", + self.rodsuser_username, + "null", ) self.assertEqual("{'irods_response': {'status_code': 0}}", str(response["data"])) # test no permission - self.api.set_token(self.rodsuser_bearer_token) - response = self.api.collections.stat(f"/{self.zone_name}/home/setPerms") + response = collections.stat(self.rodsuser_session, f"/{self.zone_name}/home/setPerms") self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) # remove the collection - self.api.set_token(self.rodsadmin_bearer_token) - response = self.api.collections.remove(f"/{self.zone_name}/home/setPerms", 1, 1) + response = collections.remove(self.rodsadmin_session, f"/{self.zone_name}/home/setPerms", 1, 1) self.assertEqual(response["data"]["irods_response"]["status_code"], 0) # tests the set inheritance operation def test_set_inheritance(self): """Test setting inheritance for collection permissions.""" - self.api.set_token(self.rodsadmin_bearer_token) - # test param checking - self.assertRaises(TypeError, self.api.collections.set_inheritance, 0, 0, 0) + self.assertRaises(TypeError, collections.set_inheritance, self.rodsadmin_session, 0, 0, 0) self.assertRaises( TypeError, - self.api.collections.set_inheritance, + collections.set_inheritance, + self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}", "0", 0, ) self.assertRaises( ValueError, - self.api.collections.set_inheritance, + collections.set_inheritance, + self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}", 5, 0, ) self.assertRaises( TypeError, - self.api.collections.set_inheritance, + collections.set_inheritance, + self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}", 0, "0", ) self.assertRaises( ValueError, - self.api.collections.set_inheritance, + collections.set_inheritance, + self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}", 0, 5, ) # control - response = self.api.collections.stat(f"/{self.zone_name}/home/{self.rodsadmin_username}") + response = collections.stat(self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}") self.assertFalse(response["data"]["inheritance_enabled"]) # test enabling inheritance - response = self.api.collections.set_inheritance(f"/{self.zone_name}/home/{self.rodsadmin_username}", 1) + response = collections.set_inheritance( + self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}", 1 + ) self.assertEqual("{'irods_response': {'status_code': 0}}", str(response["data"])) # check if changed - response = self.api.collections.stat(f"/{self.zone_name}/home/{self.rodsadmin_username}") + response = collections.stat(self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}") self.assertTrue(response["data"]["inheritance_enabled"]) # test disabling inheritance - response = self.api.collections.set_inheritance(f"/{self.zone_name}/home/{self.rodsadmin_username}", 0) + response = collections.set_inheritance( + self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}", 0 + ) self.assertEqual("{'irods_response': {'status_code': 0}}", str(response["data"])) # check if changed - response = self.api.collections.stat(f"/{self.zone_name}/home/{self.rodsadmin_username}") + response = collections.stat(self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}") self.assertFalse(response["data"]["inheritance_enabled"]) # test the modify permissions operation def test_modify_permissions(self): """Test modifying permissions on collections.""" - self.api.set_token(self.rodsadmin_bearer_token) - ops_permissions = [{"entity_name": self.rodsuser_username, "acl": "read"}] ops_permissions_null = [{"entity_name": self.rodsuser_username, "acl": "null"}] # test param checking - self.assertRaises(TypeError, self.api.collections.modify_permissions, 0, ops_permissions, 0) + self.assertRaises(TypeError, collections.modify_permissions, self.rodsadmin_session, 0, ops_permissions, 0) self.assertRaises( TypeError, - self.api.collections.modify_permissions, + collections.modify_permissions, + self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}", 5, 0, ) self.assertRaises( TypeError, - self.api.collections.modify_permissions, + collections.modify_permissions, + self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}", ops_permissions, "0", ) self.assertRaises( ValueError, - self.api.collections.modify_permissions, + collections.modify_permissions, + self.rodsadmin_session, 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") + response = collections.create(self.rodsadmin_session, f"/{self.zone_name}/home/modPerms") self.assertEqual(response["data"]["irods_response"]["status_code"], 0) # test no permissions - self.api.set_token(self.rodsuser_bearer_token) - response = self.api.collections.stat(f"/{self.zone_name}/home/modPerms") + response = collections.stat(self.rodsuser_session, f"/{self.zone_name}/home/modPerms") self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) # test set permissions - self.api.set_token(self.rodsadmin_bearer_token) - response = self.api.collections.modify_permissions(f"/{self.zone_name}/home/modPerms", ops_permissions) + response = collections.modify_permissions( + self.rodsadmin_session, f"/{self.zone_name}/home/modPerms", ops_permissions + ) self.assertEqual(response["data"]["irods_response"]["status_code"], 0) # test with permissions - self.api.set_token(self.rodsuser_bearer_token) - response = self.api.collections.stat(f"/{self.zone_name}/home/modPerms") + response = collections.stat(self.rodsadmin_session, f"/{self.zone_name}/home/modPerms") self.assertTrue(response["data"]["permissions"]) # test set permissions nuil - self.api.set_token(self.rodsadmin_bearer_token) - response = self.api.collections.modify_permissions(f"/{self.zone_name}/home/modPerms", ops_permissions_null) + response = collections.modify_permissions( + self.rodsadmin_session, f"/{self.zone_name}/home/modPerms", ops_permissions_null + ) self.assertEqual(response["data"]["irods_response"]["status_code"], 0) # test without permissions - self.api.set_token(self.rodsuser_bearer_token) - response = self.api.collections.stat(f"/{self.zone_name}/home/modPerms") + response = collections.stat(self.rodsuser_session, f"/{self.zone_name}/home/modPerms") self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) # remove the collection - self.api.set_token(self.rodsadmin_bearer_token) - response = self.api.collections.remove(f"/{self.zone_name}/home/modPerms", 1, 1) + response = collections.remove(self.rodsadmin_session, f"/{self.zone_name}/home/modPerms", 1, 1) self.assertEqual(response["data"]["irods_response"]["status_code"], 0) # test the modify metadata operation def test_modify_metadata(self): """Test modifying metadata on collections.""" - self.api.set_token(self.rodsadmin_bearer_token) - ops_metadata = [{"operation": "add", "attribute": "eyeballs", "value": "itchy"}] ops_metadata_remove = [{"operation": "remove", "attribute": "eyeballs", "value": "itchy"}] # test param checking - self.assertRaises(TypeError, self.api.collections.modify_metadata, 0, ops_metadata, 0) + self.assertRaises(TypeError, collections.modify_metadata, self.rodsadmin_session, 0, ops_metadata, 0) self.assertRaises( TypeError, - self.api.collections.modify_metadata, + collections.modify_metadata, + self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}", 5, 0, ) self.assertRaises( TypeError, - self.api.collections.modify_metadata, + collections.modify_metadata, + self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}", ops_metadata, "0", ) self.assertRaises( ValueError, - self.api.collections.modify_metadata, + collections.modify_metadata, + self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}", ops_metadata, 5, ) # test adding and removing metadata - response = self.api.collections.modify_metadata( - f"/{self.zone_name}/home/{self.rodsadmin_username}", ops_metadata + response = collections.modify_metadata( + self.rodsadmin_session, + 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 + response = collections.modify_metadata( + self.rodsadmin_session, + f"/{self.zone_name}/home/{self.rodsadmin_username}", + ops_metadata_remove, ) self.assertEqual(response["data"]["irods_response"]["status_code"], 0) # tests the rename operation def test_rename(self): """Test renaming collections.""" - self.api.set_token(self.rodsadmin_bearer_token) - # test param checking self.assertRaises( TypeError, - self.api.collections.rename, + collections.rename, + self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}", 0, ) - self.assertRaises(TypeError, self.api.collections.rename, 0, f"/{self.zone_name}/home/pods") + self.assertRaises(TypeError, collections.rename, 0, f"/{self.zone_name}/home/pods") # test before move - response = self.api.collections.stat(f"/{self.zone_name}/home/pods") + response = collections.stat(self.rodsadmin_session, 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}") + response = collections.stat(self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}") self.assertTrue(response["data"]["permissions"]) # test renaming - response = self.api.collections.rename( + response = collections.rename( + self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}", f"/{self.zone_name}/home/pods", ) self.assertEqual("{'irods_response': {'status_code': 0}}", str(response["data"])) # test before move - response = self.api.collections.stat(f"/{self.zone_name}/home/{self.rodsadmin_username}") + response = collections.stat(self.rodsadmin_session, 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") + response = collections.stat(self.rodsadmin_session, f"/{self.zone_name}/home/pods") self.assertTrue(response["data"]["permissions"]) # test renaming - response = self.api.collections.rename( + response = collections.rename( + self.rodsadmin_session, f"/{self.zone_name}/home/pods", f"/{self.zone_name}/home/{self.rodsadmin_username}", ) @@ -713,14 +766,20 @@ def test_rename(self): # 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 @@ -739,8 +798,6 @@ def setUp(self): def test_common_operations(self): """Test common data object operations (write, read, replicate, etc.).""" - self.api.set_token(self.rodsadmin_bearer_token) - f1 = f"/{self.zone_name}/home/{self.rodsuser_username}/f1.txt" f2 = f"/{self.zone_name}/home/{self.rodsuser_username}/f2.txt" f3 = f"/{self.zone_name}/home/{self.rodsuser_username}/f3.txt" @@ -748,7 +805,8 @@ 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, @@ -756,57 +814,58 @@ def test_common_operations(self): "", ) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) - - self.api.set_token(self.rodsuser_bearer_token) # Create a non-empty data object - r = self.api.data_objects.write("These are the bytes being written to the object", f1) + r = data_objects.write(self.rodsuser_session, "These are the bytes being written to the object", f1) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Read the data object - r = self.api.data_objects.read(f1, offset=6) + r = data_objects.read(self.rodsuser_session, f1, offset=6) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) self.assertIn('being written', r["data"]['irods_response']['bytes'].decode('utf-8')) # Add metadata to the data object - r = self.api.data_objects.modify_metadata( - f1, operations=[{'operation': 'add', 'attribute': 'a', 'value': 'v', 'units': 'u'}] + 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) # 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") + r = data_objects.modify_replica(self.rodsadmin_session, f1, replica_number=0, new_data_comments="awesome") self.assertEqual(r["data"]["irods_response"]["status_code"], 0) - self.api.set_token(self.rodsuser_bearer_token) # Replicate the data object - r = self.api.data_objects.replicate( + r = data_objects.replicate( + self.rodsuser_session, f1, dst_resource=resc, ) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Show that there are two replicas - r = self.api.queries.execute_genquery( - f"select DATA_NAME, DATA_REPL_NUM where DATA_NAME = '{f1.rsplit('/', maxsplit=1)[-1]}'" + 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) self.assertEqual(len(r["data"]["rows"]), 2) # Trim the first data object - r = self.api.data_objects.trim(f1, 0) + r = data_objects.trim(self.rodsuser_session, f1, 0) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Rename the data object - r = self.api.data_objects.rename(f1, f2) + r = data_objects.rename(self.rodsuser_session, f1, f2) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Copy the data object - r = self.api.data_objects.copy(f2, f3) + r = data_objects.copy(self.rodsuser_session, f2, f3) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Set permission on the object - r = self.api.data_objects.set_permission( + r = data_objects.set_permission( + self.rodsuser_session, f3, "rods", "read", @@ -814,7 +873,7 @@ def test_common_operations(self): self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Confirm that the permission has been set - r = self.api.data_objects.stat(f3) + r = data_objects.stat(self.rodsuser_session, f3) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) self.assertIn( { @@ -827,27 +886,27 @@ def test_common_operations(self): ) # Modify permission on the object - r = self.api.data_objects.modify_permissions(f3, operations=[{'entity_name': 'rods', 'acl': 'write'}]) + r = data_objects.modify_permissions( + self.rodsuser_session, f3, operations=[{'entity_name': 'rods', 'acl': 'write'}] + ) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) finally: # Remove the data objects - r = self.api.data_objects.remove(f1, 0, 1) + r = data_objects.remove(self.rodsuser_session, f1, 0, 1) - r = self.api.data_objects.remove(f2, 0, 1) + r = data_objects.remove(self.rodsuser_session, f2, 0, 1) - r = self.api.data_objects.remove(f3, 0, 1) + r = data_objects.remove(self.rodsuser_session, f3, 0, 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_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( + r = resources.create( + self.rodsadmin_session, "newresource", "unixfilesystem", self.host, @@ -857,76 +916,84 @@ def test_checksums(self): self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Create a non-empty data object - r = self.api.data_objects.write( + r = data_objects.write( + self.rodsadmin_session, "These are the bytes being written to the object", f"/{self.zone_name}/home/{self.rodsadmin_username}/file.txt", ) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Replicate the data object - r = self.api.data_objects.replicate( + r = data_objects.replicate( + self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}/file.txt", dst_resource="newresource", ) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Show that there are two replicas - r = self.api.queries.execute_genquery("select DATA_NAME, DATA_REPL_NUM where DATA_NAME = 'file.txt'") + r = queries.execute_genquery( + self.rodsadmin_session, "select DATA_NAME, DATA_REPL_NUM where DATA_NAME = 'file.txt'" + ) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) self.assertEqual(len(r["data"]["rows"]), 2) try: # Calculate a checksum for the first replica - r = self.api.data_objects.calculate_checksum( + r = data_objects.calculate_checksum( + self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}/file.txt", replica_number=0, ) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Verify checksum information across all replicas. - r = self.api.data_objects.verify_checksum(f"/{self.zone_name}/home/{self.rodsadmin_username}/file.txt") + r = data_objects.verify_checksum( + self.rodsadmin_session, 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) + r = data_objects.remove( + self.rodsadmin_session, + f"/{self.zone_name}/home/{self.rodsadmin_username}/file.txt", + 0, + 1, + ) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Remove the resource - r = self.api.resources.remove("newresource") + r = resources.remove(self.rodsadmin_session, "newresource") self.assertEqual(r["data"]["irods_response"]["status_code"], 0) def test_touch(self): """Test touch operation on data objects.""" - self.api.set_token(self.rodsadmin_bearer_token) - # Test touching non existant data object with no_create - r = self.api.data_objects.touch(f"/{self.zone_name}/home/{self.rodsadmin_username}/new.txt", 1) + r = data_objects.touch(self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}/new.txt", 1) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Show that the object has not been created - r = self.api.data_objects.stat(f"/{self.zone_name}/home/{self.rodsadmin_username}/new.txt") + r = data_objects.stat(self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}/new.txt") self.assertEqual(r["data"]["irods_response"]["status_code"], -171000) # Test touching non existant object without no_create - r = self.api.data_objects.touch(f"/{self.zone_name}/home/{self.rodsadmin_username}/new.txt", 0) + r = data_objects.touch(self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}/new.txt", 0) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Show that the object has been created - r = self.api.data_objects.stat(f"/{self.zone_name}/home/{self.rodsadmin_username}/new.txt") + r = data_objects.stat(self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}/new.txt") self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Test touching existing object without no_create - r = self.api.data_objects.touch(f"/{self.zone_name}/home/{self.rodsadmin_username}/new.txt", 1) + r = data_objects.touch(self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}/new.txt", 1) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Remove the object - r = self.api.data_objects.remove(f"/{self.zone_name}/home/{self.rodsadmin_username}/new.txt") + r = data_objects.remove(self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}/new.txt") self.assertEqual(r["data"]["irods_response"]["status_code"], 0) def test_register(self): """Test registering existing files as iRODS data objects.""" - self.api.set_token(self.rodsadmin_bearer_token) - # Create a non-empty local file. filename = f"/{self.zone_name}/home/{self.rodsadmin_username}/register-demo.txt" content = "data" @@ -935,12 +1002,13 @@ def test_register(self): 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, @@ -952,7 +1020,8 @@ def test_register(self): # Register the local file into the catalog as a new data object. # We know we're registering a new data object because the "as-additional-replica" # parameter isn't set to 1. - r = self.api.data_objects.register( + r = data_objects.register( + self.rodsadmin_session, filename, "/tmp/register-demo.txt", # noqa: S108 "register_resource", @@ -961,8 +1030,9 @@ def test_register(self): self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Show a new data object exists with the expected replica information. - r = self.api.queries.execute_genquery( - "select DATA_NAME, DATA_PATH, RESC_NAME where DATA_NAME = 'register-demo.txt'" + r = queries.execute_genquery( + self.rodsadmin_session, + "select DATA_NAME, DATA_PATH, RESC_NAME where DATA_NAME = 'register-demo.txt'", ) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) self.assertEqual(len(r["data"]["rows"]), 1) @@ -971,21 +1041,27 @@ def test_register(self): finally: # Unregister the data object - r = self.api.data_objects.remove(filename, 1) + r = data_objects.remove(self.rodsadmin_session, filename, 1) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Remove the resource - r = self.api.resources.remove("register_resource") + r = resources.remove(self.rodsadmin_session, "register_resource") self.assertEqual(r["data"]["irods_response"]["status_code"], 0) def test_parallel_write(self): """Test parallel writing to data objects.""" - self.api.set_token(self.rodsadmin_bearer_token) - self.api.data_objects.remove(f"/{self.zone_name}/home/{self.rodsadmin_username}/parallel-write.txt", 0, 1) + data_objects.remove( + self.rodsadmin_session, + f"/{self.zone_name}/home/{self.rodsadmin_username}/parallel-write.txt", + 0, + 1, + ) # Open parallel write - r = self.api.data_objects.parallel_write_init( - f"/{self.zone_name}/home/{self.rodsadmin_username}/parallel-write.txt", 3 + r = data_objects.parallel_write_init( + self.rodsadmin_session, + f"/{self.zone_name}/home/{self.rodsadmin_username}/parallel-write.txt", + 3, ) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) handle = r["data"]["parallel_write_handle"] @@ -998,7 +1074,8 @@ def test_parallel_write(self): count = 10 futures.append( executor.submit( - self.api.data_objects.write, + data_objects.write, + self.rodsadmin_session, bytes_=e[1] * count, offset=e[0] * count, stream_index=e[0], @@ -1010,11 +1087,12 @@ def test_parallel_write(self): self.assertEqual(r["data"]["irods_response"]["status_code"], 0) finally: # Close parallel write - r = self.api.data_objects.parallel_write_shutdown(handle) + r = data_objects.parallel_write_shutdown(self.rodsadmin_session, handle) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Remove the object - r = self.api.data_objects.remove( + r = data_objects.remove( + self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}/parallel-write.txt", 0, 1, @@ -1023,7 +1101,7 @@ def test_parallel_write(self): # Tests for resources operations -class ResourcesTests(unittest.TestCase): +class ResourceTests(unittest.TestCase): """Test iRODS resource operations.""" @classmethod @@ -1042,27 +1120,25 @@ 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") + resources.remove_child(self.rodsadmin_session, "test_repl", "test_ufs0") + resources.remove_child(self.rodsadmin_session, "test_repl", "test_ufs1") + resources.remove(self.rodsadmin_session, "test_ufs0") + resources.remove(self.rodsadmin_session, "test_ufs1") + resources.remove(self.rodsadmin_session, "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", "", "", "") + r = resources.create(self.rodsadmin_session, 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) + r = resources.stat(self.rodsadmin_session, resc_repl) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) self.assertEqual(r["data"]["exists"], True) self.assertIn("id", r["data"]["info"]) @@ -1092,15 +1168,15 @@ def test_common_operations(self): 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, "") + r = resources.create(self.rodsadmin_session, 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) + r = resources.add_child(self.rodsadmin_session, 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) + r = resources.stat(self.rodsadmin_session, resc_name) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) self.assertEqual(r["data"]["exists"], True) self.assertIn("id", r["data"]["info"]) @@ -1121,12 +1197,13 @@ def test_common_operations(self): # 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) + r = data_objects.write(self.rodsadmin_session, "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}'" + r = queries.execute_genquery( + self.rodsadmin_session, + 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) @@ -1135,18 +1212,19 @@ def test_common_operations(self): self.assertIn(resc_tuple, [(resc_ufs0, resc_ufs1), (resc_ufs1, resc_ufs0)]) # Trim a replica. - r = self.api.data_objects.trim(data_object, 0) + r = data_objects.trim(self.rodsadmin_session, 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}'" + r = queries.execute_genquery( + self.rodsadmin_session, + f"select DATA_NAME, RESC_NAME where DATA_NAME = '{pathlib.Path(data_object).name}'", ) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) self.assertEqual(len(r["data"]["rows"]), 1) # Launch rebalance - r = self.api.resources.rebalance(resc_repl) + r = resources.rebalance(self.rodsadmin_session, resc_repl) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Give the rebalance operation time to complete! @@ -1157,40 +1235,39 @@ def test_common_operations(self): # # Remove the data object. - r = self.api.data_objects.remove(data_object, 0, 1) + r = data_objects.remove(self.rodsadmin_session, data_object, 0, 1) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Remove resources. for resc_name in [resc_ufs0, resc_ufs1]: with self.subTest(f"Detach and remove resource [{resc_name}] from [{resc_repl}]"): # Detach ufs resource from the replication resource. - r = self.api.resources.remove_child(resc_repl, resc_name) + r = resources.remove_child(self.rodsadmin_session, resc_repl, resc_name) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Remove ufs resource. - r = self.api.resources.remove(resc_name) + r = resources.remove(self.rodsadmin_session, resc_name) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Show that the resource no longer exists. - r = self.api.resources.stat(resc_name) + r = resources.stat(self.rodsadmin_session, resc_name) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) self.assertEqual(r["data"]["exists"], False) # Remove replication resource. - r = self.api.resources.remove(resc_repl) + r = resources.remove(self.rodsadmin_session, resc_repl) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Show that the resource no longer exists. - r = self.api.resources.stat(resc_repl) + r = resources.stat(self.rodsadmin_session, resc_repl) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) self.assertEqual(r["data"]["exists"], False) def test_modify_metadata(self): """Test modifying metadata on resources.""" - self.api.set_token(self.rodsadmin_bearer_token) - # Create a unixfilesystem resource. - r = self.api.resources.create( + r = resources.create( + self.rodsadmin_session, "metadata_demo", "unixfilesystem", self.host, @@ -1202,13 +1279,14 @@ def test_modify_metadata(self): operations = [{"operation": "add", "attribute": "a1", "value": "v1", "units": "u1"}] # Add the metadata to the resource - r = self.api.resources.modify_metadata("metadata_demo", operations) + r = resources.modify_metadata(self.rodsadmin_session, "metadata_demo", operations) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Show that the metadata is on the resource - r = self.api.queries.execute_genquery( + 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'" + "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") @@ -1216,28 +1294,27 @@ def test_modify_metadata(self): # Remove the metadata from the resource. operations = [{"operation": "remove", "attribute": "a1", "value": "v1", "units": "u1"}] - r = self.api.resources.modify_metadata("metadata_demo", operations) + r = resources.modify_metadata(self.rodsadmin_session, "metadata_demo", operations) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Show that the metadata is no longer on the resource - r = self.api.queries.execute_genquery( + 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'" + "META_RESC_ATTR_VALUE = 'v1' and META_RESC_ATTR_UNITS = 'u1'", ) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) self.assertEqual(len(r["data"]["rows"]), 0) # Remove the resource - r = self.api.resources.remove("metadata_demo") + r = resources.remove(self.rodsadmin_session, "metadata_demo") def test_modify_properties(self): """Test modifying resource properties.""" - self.api.set_token(self.rodsadmin_bearer_token) - resource = "properties_demo" # Create a new resource. - r = self.api.resources.create(resource, "replication", "", "", "") + r = resources.create(self.rodsadmin_session, resource, "replication", "", "", "") self.assertEqual(r["data"]["irods_response"]["status_code"], 0) try: @@ -1260,23 +1337,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) + r = resources.stat(self.rodsadmin_session, resource) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) self.assertEqual(r["data"]["info"][p], v) finally: # Remove the resource - r = self.api.resources.remove(resource) + r = resources.remove(self.rodsadmin_session, resource) # Tests for rule operations -class RulesTests(unittest.TestCase): +class RuleTests(unittest.TestCase): """Test iRODS rule operations.""" @classmethod @@ -1296,7 +1373,9 @@ def setUp(self): def test_list(self): """Test listing rule engine plugins.""" # Try listing rule engine plugins - r = self.api.rules.list_rule_engines() + r = rules.list_rule_engines( + self.rodsadmin_session, + ) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) self.assertGreater(len(r["data"]["rule_engine_plugin_instances"]), 0) @@ -1306,7 +1385,8 @@ def test_execute_rule(self): test_msg = "This was run by the iRODS HTTP API test suite!" # Execute rule text against the iRODS rule language. - r = self.api.rules.execute( + r = rules.execute( + self.rodsadmin_session, f'writeLine("stdout", "{test_msg}")', "irods_rule_engine_plugin-irods_rule_language-instance", ) @@ -1323,7 +1403,8 @@ def test_remove_delay_rule(self): rep_instance = "irods_rule_engine_plugin-irods_rule_language-instance" # Schedule a delay rule to execute in the distant future. - r = self.api.rules.execute( + r = rules.execute( + self.rodsadmin_session, f'delay("{rep_instance}1h") ' f'{{ writeLine("serverLog", "iRODS HTTP API"); }}', rep_instance, @@ -1334,13 +1415,13 @@ def test_remove_delay_rule(self): # 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)") + r = queries.execute_genquery(self.rodsadmin_session, "select max(RULE_EXEC_ID)") self.assertEqual(r["data"]["irods_response"]["status_code"], 0) self.assertEqual(len(r["data"]["rows"]), 1) # Remove the delay rule. - r = self.api.rules.remove_delay_rule(int(r["data"]["rows"][0][0])) + r = rules.remove_delay_rule(self.rodsadmin_session, int(r["data"]["rows"][0][0])) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) @@ -1366,27 +1447,24 @@ 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) + r = queries.add_specific_query(self.rodsadmin_session, name=name, sql=sql) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Switch to rodsuser and execute it - self.api.set_token(self.rodsuser_bearer_token) - r = self.api.queries.execute_specific_query(name=name) + r = queries.execute_specific_query(self.rodsuser_session, name=name) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) self.assertEqual(r["data"]["rows"][0][0], "3") finally: # Switch to rodsadmin and remove it - self.api.set_token(self.rodsadmin_bearer_token) - r = self.api.queries.remove_specific_query(name=name) + r = 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 @@ -1405,15 +1483,14 @@ def setUp(self): 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( + r = tickets.create( + self.rodsuser_session, ticket_path, ticket_type, use_count=ticket_use_count, @@ -1429,9 +1506,10 @@ def test_create_and_remove(self): # 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( + r = queries.execute_genquery( + self.rodsadmin_session, "select TICKET_STRING, TICKET_TYPE, TICKET_COLL_NAME, TICKET_USES_LIMIT, " - "TICKET_ALLOWED_USER_NAME, TICKET_ALLOWED_GROUP_NAME, TICKET_ALLOWED_HOST" + "TICKET_ALLOWED_USER_NAME, TICKET_ALLOWED_GROUP_NAME, TICKET_ALLOWED_HOST", ) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) @@ -1444,12 +1522,12 @@ def test_create_and_remove(self): self.assertGreater(len(r["data"]["rows"][0][6]), 0) # Remove the ticket. - r = self.api.tickets.remove(ticket_string) + r = tickets.remove(self.rodsuser_session, ticket_string) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Show the ticket no longer exists. - r = self.api.queries.execute_genquery("select TICKET_STRING") + r = queries.execute_genquery(self.rodsadmin_session, "select TICKET_STRING") self.assertEqual(r["data"]["irods_response"]["status_code"], 0) self.assertEqual(len(r["data"]["rows"]), 0) @@ -1475,17 +1553,15 @@ def setUp(self): def test_create_stat_and_remove_rodsuser(self): """Test creating, querying, and removing rodsuser users.""" - self.api.set_token(self.rodsadmin_bearer_token) - new_username = "test_user_rodsuser" user_type = "rodsuser" # Create a new user. - r = self.api.users_groups.create_user(new_username, self.zone_name, user_type) + r = users_groups.create_user(self.rodsadmin_session, 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) + r = users_groups.stat(self.rodsadmin_session, new_username, self.zone_name) self.assertEqual(r["status_code"], 200) stat_info = r["data"] @@ -1496,46 +1572,42 @@ def test_create_stat_and_remove_rodsuser(self): self.assertEqual(stat_info["type"], user_type) # Remove the user. - r = self.api.users_groups.remove_user(new_username, self.zone_name) + r = users_groups.remove_user(self.rodsadmin_session, new_username, self.zone_name) self.assertEqual(r["status_code"], 200) def test_set_password(self): """Test setting user passwords.""" - self.api.set_token(self.rodsadmin_bearer_token) - new_username = "test_user_rodsuser" user_type = "rodsuser" # Create a new user. - r = self.api.users_groups.create_user(new_username, self.zone_name, user_type) + r = users_groups.create_user(self.rodsadmin_session, 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) + r = users_groups.set_password(self.rodsadmin_session, 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) + session = authenticate(self.url_base, new_username, new_password) + self.assertIsInstance(session.token, str) # Remove the user. - r = self.api.users_groups.remove_user(new_username, self.zone_name) + r = users_groups.remove_user(self.rodsadmin_session, 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) + r = users_groups.create_user(self.rodsadmin_session, 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) + r = users_groups.stat(self.rodsadmin_session, new_username, self.zone_name) self.assertEqual(r["status_code"], 200) stat_info = r["data"] @@ -1546,22 +1618,20 @@ def test_create_stat_and_remove_rodsadmin(self): self.assertEqual(stat_info["type"], user_type) # Remove the user. - r = self.api.users_groups.remove_user(new_username, self.zone_name) + r = users_groups.remove_user(self.rodsadmin_session, new_username, self.zone_name) self.assertEqual(r["status_code"], 200) def test_create_stat_and_remove_groupadmin(self): """Test creating, querying, and removing groupadmin users.""" - self.api.set_token(self.rodsadmin_bearer_token) - new_username = "test_user_groupadmin" user_type = "groupadmin" # Create a new user. - r = self.api.users_groups.create_user(new_username, self.zone_name, user_type) + r = users_groups.create_user(self.rodsadmin_session, 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) + r = users_groups.stat(self.rodsadmin_session, new_username, self.zone_name) self.assertEqual(r["status_code"], 200) stat_info = r["data"] @@ -1572,21 +1642,19 @@ def test_create_stat_and_remove_groupadmin(self): self.assertEqual(stat_info["type"], user_type) # Remove the user. - r = self.api.users_groups.remove_user(new_username, self.zone_name) + r = users_groups.remove_user(self.rodsadmin_session, new_username, self.zone_name) self.assertEqual(r["status_code"], 200) def test_add_remove_user_to_and_from_group(self): """Test adding and removing users from groups.""" - self.api.set_token(self.rodsadmin_bearer_token) - # Create a new group. new_group = "test_group" - r = self.api.users_groups.create_group(new_group) + r = users_groups.create_group(self.rodsadmin_session, 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) + r = users_groups.stat(self.rodsadmin_session, new_group) self.assertEqual(r["status_code"], 200) stat_info = r["data"] @@ -1598,38 +1666,38 @@ def test_add_remove_user_to_and_from_group(self): # 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) + r = users_groups.create_user(self.rodsadmin_session, 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) + r = users_groups.add_to_group(self.rodsadmin_session, 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) + r = users_groups.is_member_of_group(self.rodsadmin_session, 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) + r = users_groups.remove_from_group(self.rodsadmin_session, new_username, self.zone_name, new_group) self.assertEqual(r["status_code"], 200) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Remove the user. - r = self.api.users_groups.remove_user(new_username, self.zone_name) + r = users_groups.remove_user(self.rodsadmin_session, new_username, self.zone_name) self.assertEqual(r["status_code"], 200) # Remove group. - r = self.api.users_groups.remove_group(new_group) + r = users_groups.remove_group(self.rodsadmin_session, new_group) self.assertEqual(r["status_code"], 200) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Show that the group no longer exists. - r = self.api.users_groups.stat(new_group) + r = users_groups.stat(self.rodsadmin_session, new_group) self.assertEqual(r["status_code"], 200) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) @@ -1639,29 +1707,26 @@ def test_add_remove_user_to_and_from_group(self): def test_only_a_rodsadmin_can_change_the_type_of_a_user(self): """Test that only rodsadmin users can change user type.""" - self.api.set_token(self.rodsadmin_bearer_token) - # Create a new user. new_username = "test_user_rodsuser" user_type = "rodsuser" - r = self.api.users_groups.create_user(new_username, self.zone_name, user_type) + r = users_groups.create_user(self.rodsadmin_session, 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) + r = users_groups.set_user_type(self.rodsadmin_session, 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) + 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) # Show that the user type matches the type set by the rodsadmin. - r = self.api.users_groups.stat(new_username, self.zone_name) + r = users_groups.stat(self.rodsuser_session, new_username, self.zone_name) self.assertEqual(r["status_code"], 200) stat_info = r["data"] @@ -1671,16 +1736,15 @@ def test_only_a_rodsadmin_can_change_the_type_of_a_user(self): 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) + r = users_groups.remove_user(self.rodsadmin_session, new_username, self.zone_name) self.assertEqual(r["status_code"], 200) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) def test_listing_all_users_in_zone(self): """Test listing all users in the zone.""" - self.api.set_token(self.rodsuser_bearer_token) - - r = self.api.users_groups.users() + r = users_groups.users( + self.rodsadmin_session, + ) self.assertEqual(r["status_code"], 200) result = r["data"] self.assertEqual(result["irods_response"]["status_code"], 0) @@ -1689,44 +1753,40 @@ def test_listing_all_users_in_zone(self): 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) + r = users_groups.create_group(self.rodsadmin_session, 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() + r = users_groups.groups( + self.rodsadmin_session, + ) 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) + r = users_groups.remove_group(self.rodsadmin_session, new_group) self.assertEqual(r["status_code"], 200) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) def test_modifying_metadata_atomically(self): """Test atomically modifying user metadata.""" - self.api.set_token(self.rodsadmin_bearer_token) username = self.rodsuser_username # Add metadata to the user. ops = [{"operation": "add", "attribute": "a1", "value": "v1", "units": "u1"}] - r = self.api.users_groups.modify_metadata(username, ops) + r = users_groups.modify_metadata(self.rodsadmin_session, username, ops) self.assertEqual(r["status_code"], 200) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Show the metadata exists on the user. - r = self.api.queries.execute_genquery( + 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) @@ -1736,14 +1796,15 @@ def test_modifying_metadata_atomically(self): # Remove the metadata from the user. ops = [{"operation": "remove", "attribute": "a1", "value": "v1", "units": "u1"}] - r = self.api.users_groups.modify_metadata(username, ops) + r = users_groups.modify_metadata(self.rodsadmin_session, username, ops) self.assertEqual(r["status_code"], 200) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Show the metadata no longer exists on the user. - r = self.api.queries.execute_genquery( + 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) @@ -1772,8 +1833,9 @@ 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() + r = zones.report( + self.rodsadmin_session, + ) self.assertEqual(r["status_code"], 200) result = r["data"] @@ -1786,18 +1848,16 @@ def test_report_operation(self): def test_adding_removing_and_modifying_zones(self): """Test adding, removing, and modifying zones.""" - self.api.set_token(self.rodsadmin_bearer_token) - # Add a remote zone to the local zone. # The new zone will not have any connection information or anything else. zone_name = "other_zone" - r = self.api.zones.add(zone_name) + r = zones.add(self.rodsadmin_session, zone_name) self.assertEqual(r["status_code"], 200) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) try: # Show the new zone exists by executing the stat operation on it. - r = self.api.zones.stat(zone_name) + r = zones.stat(self.rodsadmin_session, zone_name) self.assertEqual(r["status_code"], 200) result = r["data"] @@ -1818,7 +1878,7 @@ 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) + r = zones.modify(self.rodsadmin_session, zone_name, p, v) self.assertEqual(r["status_code"], 200) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) @@ -1827,7 +1887,7 @@ def test_adding_removing_and_modifying_zones(self): zone_name = v # Show the new zone was modified successfully. - r = self.api.zones.stat(zone_name) + r = zones.stat(self.rodsadmin_session, zone_name) self.assertEqual(r["status_code"], 200) result = r["data"] @@ -1837,7 +1897,7 @@ def test_adding_removing_and_modifying_zones(self): finally: # Remove the remote zone. - r = self.api.zones.remove(zone_name) + r = zones.remove(self.rodsadmin_session, zone_name) self.assertEqual(r["status_code"], 200) From 3d3a0bd0bf01e269b62d191cc8566ad8083d57e4 Mon Sep 17 00:00:00 2001 From: Terrell Russell Date: Sat, 21 Feb 2026 15:13:41 -0500 Subject: [PATCH 02/29] checksum flag is a bool --- irods_http_client/data_objects.py | 10 ++++------ test/test_endpoint_operations.py | 3 ++- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/irods_http_client/data_objects.py b/irods_http_client/data_objects.py index 5c38843..45e51b9 100644 --- a/irods_http_client/data_objects.py +++ b/irods_http_client/data_objects.py @@ -407,7 +407,7 @@ def register( resource: str, as_additional_replica: int = 0, data_size: int = -1, - checksum: str = "", + checksum: int = 0, ) -> dict: """ Register a data object/replica into the catalog. @@ -420,7 +420,7 @@ def register( 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: The checksum to associate with the replica. Defaults to "". + checksum: Set to 1 to register with a checksum. Defaults to 0. Returns: A dict containing the HTTP status code and iRODS response. @@ -432,7 +432,7 @@ def register( common.validate_instance(resource, str) common.validate_0_or_1(as_additional_replica) common.validate_gte_minus1(data_size) - common.validate_instance(checksum, str) + common.validate_0_or_1(checksum) headers = { "Authorization": "Bearer " + session.token, @@ -445,14 +445,12 @@ def register( "ppath": ppath, "resource": resource, "as_additional_replica": as_additional_replica, + "checksum": checksum, } if data_size != -1: data["data-size"] = data_size - if checksum != "": - data["checksum"] = checksum - r = requests.post(session.url_base + "/data-objects", headers=headers, data=data) # noqa: S113 return common.process_response(r) diff --git a/test/test_endpoint_operations.py b/test/test_endpoint_operations.py index d31f8b7..7c3dd37 100644 --- a/test/test_endpoint_operations.py +++ b/test/test_endpoint_operations.py @@ -1026,13 +1026,14 @@ def test_register(self): "/tmp/register-demo.txt", # noqa: S108 "register_resource", data_size=len(content), + checksum=1, ) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Show a new data object exists with the expected replica information. r = queries.execute_genquery( self.rodsadmin_session, - "select DATA_NAME, DATA_PATH, RESC_NAME where DATA_NAME = 'register-demo.txt'", + "select DATA_NAME, DATA_PATH, DATA_CHECKSUM, RESC_NAME where DATA_NAME = 'register-demo.txt'", ) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) self.assertEqual(len(r["data"]["rows"]), 1) From 83a3cf4232d03abd31ea7a6b674e9f33b317de0d Mon Sep 17 00:00:00 2001 From: Terrell Russell Date: Sat, 21 Feb 2026 15:16:00 -0500 Subject: [PATCH 03/29] [45] read just returns bytes as data --- irods_http_client/data_objects.py | 5 ++--- test/test_endpoint_operations.py | 8 +++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/irods_http_client/data_objects.py b/irods_http_client/data_objects.py index 45e51b9..cb84ca3 100644 --- a/irods_http_client/data_objects.py +++ b/irods_http_client/data_objects.py @@ -490,9 +490,8 @@ def read(session: common.HTTPSession, lpath: str, offset: int = 0, count: int = params["ticket"] = ticket r = requests.get(session.url_base + "/data-objects", params=params, headers=headers) # noqa: S113 - # 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}}} + # this is the only payload that is different from common.process_response() + return {'status_code': r.status_code, 'data': r.content} def write( diff --git a/test/test_endpoint_operations.py b/test/test_endpoint_operations.py index 7c3dd37..38cde3e 100644 --- a/test/test_endpoint_operations.py +++ b/test/test_endpoint_operations.py @@ -814,14 +814,16 @@ def test_common_operations(self): "", ) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + # Create a non-empty data object r = data_objects.write(self.rodsuser_session, "These are the bytes being written to the object", f1) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Read the data object - r = data_objects.read(self.rodsuser_session, 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 = data_objects.modify_metadata( From 8e09b4417d557d57ed6b907948eab5c5cd5e5073 Mon Sep 17 00:00:00 2001 From: Terrell Russell Date: Sat, 21 Feb 2026 15:18:10 -0500 Subject: [PATCH 04/29] times are strings - check http api itself --- irods_http_client/data_objects.py | 16 +++++----- test/test_endpoint_operations.py | 52 +++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 8 deletions(-) diff --git a/irods_http_client/data_objects.py b/irods_http_client/data_objects.py index cb84ca3..3540254 100644 --- a/irods_http_client/data_objects.py +++ b/irods_http_client/data_objects.py @@ -769,8 +769,8 @@ def modify_replica( 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_create_time: str = "", + new_data_expiry: str = "", new_data_mode: str = "", new_data_modify_time: str = "", new_data_path: str = "", @@ -798,8 +798,8 @@ def modify_replica( 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 -1. - new_data_expiry: The new expiry to be set. Defaults to -1. + 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 "". @@ -830,8 +830,8 @@ def modify_replica( 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_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) @@ -867,11 +867,11 @@ def modify_replica( data["new-data-comments"] = new_data_comments no_params = False - if new_data_create_time != -1: + if new_data_create_time != "": data["new-data-create-time"] = new_data_create_time no_params = False - if new_data_expiry != -1: + if new_data_expiry != "": data["new-data-expiry"] = new_data_expiry no_params = False diff --git a/test/test_endpoint_operations.py b/test/test_endpoint_operations.py index 38cde3e..d537661 100644 --- a/test/test_endpoint_operations.py +++ b/test/test_endpoint_operations.py @@ -904,6 +904,58 @@ def test_common_operations(self): # Remove the resource r = resources.remove(self.rodsadmin_session, resc) + 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) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Save the physical path + r = queries.execute_genquery(self.rodsadmin_session, "SELECT DATA_PATH where DATA_NAME = 'modify-replica-test.txt'") + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + phypath = r["data"]["rows"][0][0] + + # Save the resource id + r = queries.execute_genquery(self.rodsadmin_session, "SELECT RESC_ID where RESC_NAME = 'demoResc'") + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + rescid = int(r["data"]["rows"][0][0]) + + # 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", + ) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Restore the physical path so cleanup succeeds + r = data_objects.modify_replica( + self.rodsadmin_session, + f, + resource_hierarchy="demoResc", + new_data_path=phypath, + ) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + finally: + data_objects.remove(self.rodsadmin_session, f, no_trash=1) + def test_checksums(self): """Test checksum calculation and verification for data objects.""" # Create a unixfilesystem resource. From 473ec7ca602eec71e261621813bccfbc7294996b Mon Sep 17 00:00:00 2001 From: Terrell Russell Date: Sat, 21 Feb 2026 15:18:27 -0500 Subject: [PATCH 05/29] [47] increase coverage --- test/test_endpoint_operations.py | 248 ++++++++++++++++++++----------- 1 file changed, 160 insertions(+), 88 deletions(-) diff --git a/test/test_endpoint_operations.py b/test/test_endpoint_operations.py index d537661..d20d93c 100644 --- a/test/test_endpoint_operations.py +++ b/test/test_endpoint_operations.py @@ -797,7 +797,7 @@ def setUp(self): 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.).""" + """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" @@ -841,6 +841,7 @@ def test_common_operations(self): r = data_objects.replicate( self.rodsuser_session, f1, + src_resource="demoResc", dst_resource=resc, ) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) @@ -853,8 +854,8 @@ def test_common_operations(self): self.assertEqual(r["data"]["irods_response"]["status_code"], 0) self.assertEqual(len(r["data"]["rows"]), 2) - # Trim the first data object - r = data_objects.trim(self.rodsuser_session, f1, 0) + # Trim the data object + r = data_objects.trim(self.rodsuser_session, f1, replica_number=0) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Rename the data object @@ -865,6 +866,12 @@ def test_common_operations(self): r = data_objects.copy(self.rodsuser_session, f2, f3) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + # Copy the data object again with parameters + r = data_objects.copy(self.rodsuser_session, f2, f3, src_resource=resc, dst_resource="demoResc", overwrite=1) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Exercise a bad permission + self.assertRaises(ValueError, data_objects.set_permission, self.rodsuser_session, f3, "rods", "bad") # Set permission on the object r = data_objects.set_permission( self.rodsuser_session, @@ -969,18 +976,19 @@ def test_checksums(self): ) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + f = f"/{self.zone_name}/home/{self.rodsadmin_username}/file.txt" # Create a non-empty data object r = data_objects.write( self.rodsadmin_session, "These are the bytes being written to the object", - f"/{self.zone_name}/home/{self.rodsadmin_username}/file.txt", + f, ) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Replicate the data object r = data_objects.replicate( self.rodsadmin_session, - f"/{self.zone_name}/home/{self.rodsadmin_username}/file.txt", + f, dst_resource="newresource", ) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) @@ -996,64 +1004,96 @@ def test_checksums(self): # Calculate a checksum for the first replica r = data_objects.calculate_checksum( self.rodsadmin_session, - f"/{self.zone_name}/home/{self.rodsadmin_username}/file.txt", + f, replica_number=0, ) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) - # Verify checksum information across all replicas. + # Calculate a checksum for the second replica + r = data_objects.calculate_checksum( + self.rodsadmin_session, + f, + resource="newresource", + ) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Verify checksum on first replica r = data_objects.verify_checksum( - self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}/file.txt" + self.rodsadmin_session, + f, + replica_number=0, + ) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Verify checksum on second replica + r = data_objects.verify_checksum( + self.rodsadmin_session, + f, + resource="newresource", ) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + finally: # Remove the data objects - r = data_objects.remove( + data_objects.remove( self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}/file.txt", - 0, - 1, + catalog_only=0, + no_trash=1, ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Remove the resource - r = resources.remove(self.rodsadmin_session, "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.""" - # Test touching non existant data object with no_create - r = data_objects.touch(self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}/new.txt", 1) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + f = f"/{self.zone_name}/home/{self.rodsadmin_username}/new.txt" - # Show that the object has not been created - r = data_objects.stat(self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}/new.txt") - self.assertEqual(r["data"]["irods_response"]["status_code"], -171000) + try: + # Test touching non existant data object with no_create + r = data_objects.touch(self.rodsadmin_session, f, no_create=1) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) - # Test touching non existant object without no_create - r = data_objects.touch(self.rodsadmin_session, 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 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 been created - r = data_objects.stat(self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}/new.txt") - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + # Test touching non existant object without no_create + r = data_objects.touch(self.rodsadmin_session, f, no_create=0) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) - # Test touching existing object without no_create - r = data_objects.touch(self.rodsadmin_session, 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 been created + r = data_objects.stat(self.rodsadmin_session, f) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) - # Remove the object - r = data_objects.remove(self.rodsadmin_session, 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) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Test parameter options + r = data_objects.touch(self.rodsadmin_session, f, seconds_since_epoch=5000) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + r = data_objects.stat(self.rodsadmin_session, f) + self.assertEqual(r["data"]["modified_at"], 5000) + + r = data_objects.touch(self.rodsadmin_session, f, replica_number=0) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + r = data_objects.touch(self.rodsadmin_session, f, leaf_resources="demoResc") + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + r = data_objects.touch(self.rodsadmin_session, f, reference=f) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + + finally: + # Remove the object + r = data_objects.remove(self.rodsadmin_session, f) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) def test_register(self): """Test registering existing files as iRODS data objects.""" - # 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) + filename = f"/{self.zone_name}/home/{self.rodsadmin_username}/register-demo.txt" # Show the data object we want to create via registration does not exist. r = data_objects.stat(self.rodsadmin_session, filename) @@ -1071,13 +1111,27 @@ def test_register(self): ) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) - # Register the local file into the catalog as a new data object. + # Create a non-empty data object. + content = "bytes in the server" + r = data_objects.write(self.rodsadmin_session, content, filename) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # 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'") + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + 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, 1) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # 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 = data_objects.register( self.rodsadmin_session, filename, - "/tmp/register-demo.txt", # noqa: S108 + phyfile, "register_resource", data_size=len(content), checksum=1, @@ -1091,17 +1145,16 @@ def test_register(self): ) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) self.assertEqual(len(r["data"]["rows"]), 1) - self.assertEqual(r["data"]["rows"][0][1], "/tmp/register-demo.txt") # noqa: S108 - self.assertEqual(r["data"]["rows"][0][2], "register_resource") + 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 = data_objects.remove(self.rodsadmin_session, filename, 1) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + data_objects.remove(self.rodsadmin_session, filename, 1) # Remove the resource - r = resources.remove(self.rodsadmin_session, "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.""" @@ -1498,6 +1551,21 @@ 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: @@ -1538,54 +1606,50 @@ def setUp(self): def test_create_and_remove(self): """Test creating and removing tickets.""" - # 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 = tickets.create( - self.rodsuser_session, - 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 = queries.execute_genquery( - self.rodsadmin_session, - "select TICKET_STRING, TICKET_TYPE, TICKET_COLL_NAME, TICKET_USES_LIMIT, " - "TICKET_ALLOWED_USER_NAME, TICKET_ALLOWED_GROUP_NAME, TICKET_ALLOWED_HOST", - ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) - self.assertIn(ticket_string, r["data"]["rows"][0]) - self.assertEqual(r["data"]["rows"][0][1], ticket_type) - self.assertEqual(r["data"]["rows"][0][2], ticket_path) - self.assertEqual(r["data"]["rows"][0][3], str(ticket_use_count)) - self.assertIn(r["data"]["rows"][0][4], ["rods", "jeb"]) - self.assertEqual(r["data"]["rows"][0][5], ticket_groups) - self.assertGreater(len(r["data"]["rows"][0][6]), 0) + try: - # Remove the ticket. - r = tickets.remove(self.rodsuser_session, ticket_string) + # 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 = tickets.create( + self.rodsuser_session, + 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) - self.assertEqual(r["data"]["irods_response"]["status_code"], 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_ALLOWED_USER_NAME, TICKET_ALLOWED_GROUP_NAME, TICKET_ALLOWED_HOST", + ) - # Show the ticket no longer exists. - r = queries.execute_genquery(self.rodsadmin_session, "select TICKET_STRING") + 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) - 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 @@ -1630,6 +1694,14 @@ def test_create_stat_and_remove_rodsuser(self): r = users_groups.remove_user(self.rodsadmin_session, new_username, self.zone_name) self.assertEqual(r["status_code"], 200) + def test_empty_username(self): + """Test authenticate with an empty username""" + self.assertRaises(ValueError, authenticate, self.url_base, "", "nope") + + 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.""" new_username = "test_user_rodsuser" From 2e6204de901833fd363f5c9faeea97cf0c85322d Mon Sep 17 00:00:00 2001 From: Terrell Russell Date: Sat, 21 Feb 2026 15:20:13 -0500 Subject: [PATCH 06/29] ruff format --- test/test_endpoint_operations.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/test/test_endpoint_operations.py b/test/test_endpoint_operations.py index d20d93c..48895ae 100644 --- a/test/test_endpoint_operations.py +++ b/test/test_endpoint_operations.py @@ -867,7 +867,9 @@ def test_common_operations(self): self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Copy the data object again with parameters - r = data_objects.copy(self.rodsuser_session, f2, f3, src_resource=resc, dst_resource="demoResc", overwrite=1) + r = data_objects.copy( + self.rodsuser_session, f2, f3, src_resource=resc, dst_resource="demoResc", overwrite=1 + ) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Exercise a bad permission @@ -913,7 +915,6 @@ def test_common_operations(self): def test_modify_replica(self): """Test modify replica options.""" - f = f"/{self.zone_name}/home/{self.rodsadmin_username}/modify-replica-test.txt" try: @@ -922,7 +923,9 @@ def test_modify_replica(self): self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Save the physical path - r = queries.execute_genquery(self.rodsadmin_session, "SELECT DATA_PATH where DATA_NAME = 'modify-replica-test.txt'") + r = queries.execute_genquery( + self.rodsadmin_session, "SELECT DATA_PATH where DATA_NAME = 'modify-replica-test.txt'" + ) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) phypath = r["data"]["rows"][0][0] @@ -1084,7 +1087,6 @@ def test_touch(self): r = data_objects.touch(self.rodsadmin_session, f, reference=f) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) - finally: # Remove the object r = data_objects.remove(self.rodsadmin_session, f) @@ -1092,7 +1094,6 @@ def test_touch(self): def test_register(self): """Test registering existing files as iRODS data objects.""" - filename = f"/{self.zone_name}/home/{self.rodsadmin_username}/register-demo.txt" # Show the data object we want to create via registration does not exist. @@ -1111,13 +1112,15 @@ def test_register(self): ) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) - # Create a non-empty data object. + # Create a non-empty data object. content = "bytes in the server" r = data_objects.write(self.rodsadmin_session, content, filename) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # 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'") + r = queries.execute_genquery( + self.rodsadmin_session, "SELECT DATA_PATH where DATA_NAME = 'register-demo.txt'" + ) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) phyfile = r["data"]["rows"][0][0] @@ -1553,7 +1556,9 @@ def setUp(self): 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") + 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.""" @@ -1606,9 +1611,7 @@ def setUp(self): 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}" @@ -1695,11 +1698,11 @@ def test_create_stat_and_remove_rodsuser(self): self.assertEqual(r["status_code"], 200) def test_empty_username(self): - """Test authenticate with an empty username""" + """Test authenticate with an empty username.""" self.assertRaises(ValueError, authenticate, self.url_base, "", "nope") def test_bad_password(self): - """Test authenticate with a bad password""" + """Test authenticate with a bad password.""" self.assertRaises(RuntimeError, authenticate, self.url_base, self.rodsadmin_username, "nope") def test_set_password(self): From 7aa436e074559d1948d721e41b0c2c35acaed582 Mon Sep 17 00:00:00 2001 From: Terrell Russell Date: Sat, 21 Feb 2026 15:44:39 -0500 Subject: [PATCH 07/29] validate bytes type, with test --- irods_http_client/data_objects.py | 5 ++--- test/test_endpoint_operations.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/irods_http_client/data_objects.py b/irods_http_client/data_objects.py index 3540254..2de8325 100644 --- a/irods_http_client/data_objects.py +++ b/irods_http_client/data_objects.py @@ -527,9 +527,8 @@ def write( ValueError: If bytes length is less than 0. """ common.validate_not_none(session.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") + if type(bytes_) not in [bytes, str]: + raise TypeError(f"type(bytes_) must be 'bytes' or 'str'") common.validate_instance(lpath, str) common.validate_instance(resource, str) common.validate_gte_zero(offset) diff --git a/test/test_endpoint_operations.py b/test/test_endpoint_operations.py index 48895ae..18aca10 100644 --- a/test/test_endpoint_operations.py +++ b/test/test_endpoint_operations.py @@ -796,6 +796,21 @@ 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_write(self): + # 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): + 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) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + 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" From 0ed1ba127dfbac240face593087069f2c66c0b51 Mon Sep 17 00:00:00 2001 From: Terrell Russell Date: Sat, 21 Feb 2026 15:44:52 -0500 Subject: [PATCH 08/29] 47squash - increase coverage coverage, found bug in new_data_version check --- irods_http_client/data_objects.py | 5 +++-- test/test_endpoint_operations.py | 31 +++++++++++++++++++++++++------ 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/irods_http_client/data_objects.py b/irods_http_client/data_objects.py index 2de8325..9caf3f3 100644 --- a/irods_http_client/data_objects.py +++ b/irods_http_client/data_objects.py @@ -524,11 +524,12 @@ def write( 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(f"type(bytes_) must be 'bytes' or '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) @@ -910,7 +911,7 @@ def modify_replica( data["new-data-type-name"] = new_data_type_name no_params = False - if new_data_version != "": + if new_data_version != -1: data["new-data-version"] = new_data_version no_params = False diff --git a/test/test_endpoint_operations.py b/test/test_endpoint_operations.py index 18aca10..9d7ae51 100644 --- a/test/test_endpoint_operations.py +++ b/test/test_endpoint_operations.py @@ -797,11 +797,13 @@ def setUp(self): self.assertFalse(self._class_init_error, "Class initialization failed. Cannot continue.") 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" @@ -889,6 +891,7 @@ def test_common_operations(self): # Exercise a bad permission self.assertRaises(ValueError, data_objects.set_permission, self.rodsuser_session, f3, "rods", "bad") + # Set permission on the object r = data_objects.set_permission( self.rodsuser_session, @@ -949,6 +952,24 @@ def test_modify_replica(self): self.assertEqual(r["data"]["irods_response"]["status_code"], 0) 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, @@ -966,6 +987,7 @@ def test_modify_replica(self): new_data_size=50, new_data_status="warm", new_data_type_name="html", + new_data_version=3, ) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) @@ -1104,8 +1126,7 @@ def test_touch(self): finally: # Remove the object - r = data_objects.remove(self.rodsadmin_session, f) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + data_objects.remove(self.rodsadmin_session, f) def test_register(self): """Test registering existing files as iRODS data objects.""" @@ -1213,17 +1234,15 @@ def test_parallel_write(self): self.assertEqual(r["data"]["irods_response"]["status_code"], 0) finally: # Close parallel write - r = data_objects.parallel_write_shutdown(self.rodsadmin_session, handle) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + data_objects.parallel_write_shutdown(self.rodsadmin_session, handle) # Remove the object - r = data_objects.remove( + data_objects.remove( self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}/parallel-write.txt", 0, 1, ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Tests for resources operations From f11fc338cba02f5c7ec2e6b642db12e4b916148b Mon Sep 17 00:00:00 2001 From: Terrell Russell Date: Sat, 21 Feb 2026 16:32:16 -0500 Subject: [PATCH 09/29] coverage, found http api 473 coverage coverage coverage coverage --- irods_http_client/resources.py | 2 +- test/test_endpoint_operations.py | 131 ++++++++++++++++++++++++++++--- 2 files changed, 120 insertions(+), 13 deletions(-) diff --git a/irods_http_client/resources.py b/irods_http_client/resources.py index cd1c807..238fa3a 100644 --- a/irods_http_client/resources.py +++ b/irods_http_client/resources.py @@ -134,7 +134,7 @@ def add_child(session: common.HTTPSession, parent_name: str, child_name: str, co session: An HTTPSession instance. parent_name: The name of the parent resource. child_name: The name of the child resource. - context: Additional information for the zone. + context: Additional information for the parent-child relationship. Returns: A dict containing the HTTP status code and iRODS response. diff --git a/test/test_endpoint_operations.py b/test/test_endpoint_operations.py index 9d7ae51..5e820e2 100644 --- a/test/test_endpoint_operations.py +++ b/test/test_endpoint_operations.py @@ -1408,6 +1408,60 @@ def test_common_operations(self): self.assertEqual(r["data"]["irods_response"]["status_code"], 0) self.assertEqual(r["data"]["exists"], False) + 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 + "", + ) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Exercise bad modify property + self.assertRaises(ValueError, resources.modify, self.rodsadmin_session, badresc, "badoption", "2") + + # Exercise bad modify status + self.assertRaises(ValueError, resources.modify, self.rodsadmin_session, badresc, "status", "nope") + + finally: + resources.remove(self.rodsadmin_session, badresc) + + 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 + "", + ) + 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") + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Confirm + r = resources.stat(self.rodsadmin_session, resc) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + # Uncomment once parent_context is available + # https://github.com/irods/irods_client_http_api/issues/473 + # self.assertEqual(r["data"]["info"]["parent_context"], "neat") + + finally: + resources.remove_child(self.rodsadmin_session, "demoResc", resc) + resources.remove(self.rodsadmin_session, resc) + def test_modify_metadata(self): """Test modifying metadata on resources.""" # Create a unixfilesystem resource. @@ -1417,7 +1471,7 @@ def test_modify_metadata(self): "unixfilesystem", self.host, "/tmp/metadata_demo_vault", # noqa: S108 - "", + "ignoreme", ) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) @@ -1643,6 +1697,32 @@ 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_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") + + # bad object count + self.assertRaises( + ValueError, + tickets.create, + self.rodsadmin_session, + p, + type_="write", + write_data_object_count=-5, + ) + + # bad byte count + self.assertRaises( + ValueError, + tickets.create, + self.rodsadmin_session, + p, + type_="write", + write_byte_count=-2, + ) + def test_create_and_remove(self): """Test creating and removing tickets.""" try: @@ -1650,6 +1730,8 @@ def test_create_and_remove(self): 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( @@ -1657,6 +1739,8 @@ def test_create_and_remove(self): 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, @@ -1672,6 +1756,7 @@ def test_create_and_remove(self): 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", ) @@ -1680,8 +1765,10 @@ def test_create_and_remove(self): 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.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) finally: @@ -1707,6 +1794,18 @@ 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_with_bad_type(self): + """Test user create with bad type.""" + self.assertRaises( + ValueError, users_groups.create_user, self.rodsadmin_session, "baduser", self.zone_name, user_type="bad" + ) + + 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, user_type="bad" + ) + def test_create_stat_and_remove_rodsuser(self): """Test creating, querying, and removing rodsuser users.""" new_username = "test_user_rodsuser" @@ -2012,14 +2111,23 @@ def test_report_operation(self): def test_adding_removing_and_modifying_zones(self): """Test adding, removing, and modifying zones.""" - # 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 = zones.add(self.rodsadmin_session, zone_name) - self.assertEqual(r["status_code"], 200) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) - try: + zone_name = "other_zone" + + # Add a zone, only to remove it immediately + r = zones.add(self.rodsadmin_session, zone_name, connection_info="localhost:1250", comment="brief") + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Remove it + r = zones.remove(self.rodsadmin_session, zone_name) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # 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) + self.assertEqual(r["status_code"], 200) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + # Show the new zone exists by executing the stat operation on it. r = zones.stat(self.rodsadmin_session, zone_name) self.assertEqual(r["status_code"], 200) @@ -2061,8 +2169,7 @@ def test_adding_removing_and_modifying_zones(self): finally: # Remove the remote zone. - r = zones.remove(self.rodsadmin_session, zone_name) - self.assertEqual(r["status_code"], 200) + zones.remove(self.rodsadmin_session, zone_name) if __name__ == "__main__": From f6bf0710814df8e5b9244e822b277059096681ad Mon Sep 17 00:00:00 2001 From: Terrell Russell Date: Sun, 22 Feb 2026 10:36:30 -0500 Subject: [PATCH 10/29] move HTTPSession from common --- irods_http_client/__init__.py | 2 +- irods_http_client/collections.py | 41 ++++++++------- irods_http_client/common.py | 24 --------- irods_http_client/data_objects.py | 73 +++++++++++++------------- irods_http_client/irods_http_client.py | 28 +++++++++- irods_http_client/queries.py | 9 ++-- irods_http_client/resources.py | 17 +++--- irods_http_client/rules.py | 7 +-- irods_http_client/tickets.py | 5 +- irods_http_client/users_groups.py | 27 +++++----- irods_http_client/zones.py | 11 ++-- 11 files changed, 126 insertions(+), 118 deletions(-) diff --git a/irods_http_client/__init__.py b/irods_http_client/__init__.py index 2823d7b..40dc878 100644 --- a/irods_http_client/__init__.py +++ b/irods_http_client/__init__.py @@ -10,8 +10,8 @@ users_groups as users_groups, zones as zones, ) -from .common import HTTPSession as HTTPSession from .irods_http_client import ( + HTTPSession as HTTPSession, authenticate as authenticate, get_server_info as get_server_info, ) diff --git a/irods_http_client/collections.py b/irods_http_client/collections.py index fd17414..69b2f7f 100644 --- a/irods_http_client/collections.py +++ b/irods_http_client/collections.py @@ -5,14 +5,15 @@ import requests from . import common +from .irods_http_client import HTTPSession # noqa: TC001 -def create(session: common.HTTPSession, lpath: str, create_intermediates: int = 0) -> dict: +def create(session: HTTPSession, lpath: str, create_intermediates: int = 0) -> dict: """ Create a new collection. Args: - session: common.HTTPSession object containing the base URL and authentication token. + session: HTTPSession 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. @@ -39,12 +40,12 @@ def create(session: common.HTTPSession, lpath: str, create_intermediates: int = return common.process_response(r) -def remove(session: common.HTTPSession, lpath: str, recurse: int = 0, no_trash: int = 0) -> dict: +def remove(session: HTTPSession, lpath: str, recurse: int = 0, no_trash: int = 0) -> dict: """ Remove an existing collection. Args: - session: common.HTTPSession object containing the base URL and authentication token. + session: HTTPSession 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 move the collection to trash, 0 to permanently remove. Defaults to 0. @@ -74,12 +75,12 @@ def remove(session: common.HTTPSession, lpath: str, recurse: int = 0, no_trash: return common.process_response(r) -def stat(session: common.HTTPSession, lpath: str, ticket: str = "") -> dict: +def stat(session: HTTPSession, lpath: str, ticket: str = "") -> dict: """ Give information about a collection. Args: - session: common.HTTPSession object containing the base URL and authentication token. + session: HTTPSession 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. @@ -101,12 +102,12 @@ def stat(session: common.HTTPSession, lpath: str, ticket: str = "") -> dict: return common.process_response(r) -def list_collection(session: common.HTTPSession, lpath: str, recurse: int = 0, ticket: str = "") -> dict: +def list_collection(session: HTTPSession, lpath: str, recurse: int = 0, ticket: str = "") -> dict: """ Show the contents of a collection. Args: - session: common.HTTPSession object containing the base URL and authentication token. + session: HTTPSession 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. @@ -132,7 +133,7 @@ def list_collection(session: common.HTTPSession, lpath: str, recurse: int = 0, t def set_permission( - session: common.HTTPSession, + session: HTTPSession, lpath: str, entity_name: str, permission: str, @@ -142,7 +143,7 @@ def set_permission( Set the permission of a user for a given collection. Args: - session: common.HTTPSession object containing the base URL and authentication token. + session: HTTPSession 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'. @@ -180,12 +181,12 @@ def set_permission( return common.process_response(r) -def set_inheritance(session: common.HTTPSession, lpath: str, enable: int, admin: int = 0) -> dict: +def set_inheritance(session: HTTPSession, lpath: str, enable: int, admin: int = 0) -> dict: """ Set the inheritance for a collection. Args: - session: common.HTTPSession object containing the base URL and authentication token. + session: HTTPSession 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. @@ -215,12 +216,12 @@ def set_inheritance(session: common.HTTPSession, lpath: str, enable: int, admin: return common.process_response(r) -def modify_permissions(session: common.HTTPSession, lpath: str, operations: dict, admin: int = 0) -> dict: +def modify_permissions(session: HTTPSession, lpath: str, operations: dict, admin: int = 0) -> dict: """ Modify permissions for multiple users or groups for a collection. Args: - session: common.HTTPSession object containing the base URL and authentication token. + session: HTTPSession 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. @@ -252,12 +253,12 @@ def modify_permissions(session: common.HTTPSession, lpath: str, operations: dict return common.process_response(r) -def modify_metadata(session: common.HTTPSession, lpath: str, operations: dict, admin: int = 0) -> dict: +def modify_metadata(session: HTTPSession, lpath: str, operations: dict, admin: int = 0) -> dict: """ Modify the metadata for a collection. Args: - session: common.HTTPSession object containing the base URL and authentication token. + session: HTTPSession 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. @@ -289,12 +290,12 @@ def modify_metadata(session: common.HTTPSession, lpath: str, operations: dict, a return common.process_response(r) -def rename(session: common.HTTPSession, old_lpath: str, new_lpath: str) -> dict: +def rename(session: HTTPSession, old_lpath: str, new_lpath: str) -> dict: """ Rename or move a collection. Args: - session: common.HTTPSession object containing the base URL and authentication token. + session: HTTPSession 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. @@ -317,12 +318,12 @@ def rename(session: common.HTTPSession, old_lpath: str, new_lpath: str) -> dict: return common.process_response(r) -def touch(session: common.HTTPSession, lpath: str, seconds_since_epoch: int = -1, reference: str = "") -> dict: +def touch(session: HTTPSession, lpath: str, seconds_since_epoch: int = -1, reference: str = "") -> dict: """ Update mtime for a collection. Args: - session: common.HTTPSession object containing the base URL and authentication token. + session: HTTPSession 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. diff --git a/irods_http_client/common.py b/irods_http_client/common.py index 4bade12..e239d1a 100644 --- a/irods_http_client/common.py +++ b/irods_http_client/common.py @@ -1,30 +1,6 @@ """Common utility functions for iRODS HTTP client operations.""" -class HTTPSession: - """ - 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 HTTPSession 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 - - def process_response(r): """ Process an HTTP response and return standardized response dict. diff --git a/irods_http_client/data_objects.py b/irods_http_client/data_objects.py index 9caf3f3..6d8138f 100644 --- a/irods_http_client/data_objects.py +++ b/irods_http_client/data_objects.py @@ -5,10 +5,11 @@ import requests from . import common +from .irods_http_client import HTTPSession # noqa: TC001 def touch( - session: common.HTTPSession, + session: HTTPSession, lpath: str, no_create: int = 0, replica_number: int = -1, @@ -20,7 +21,7 @@ def touch( Update mtime for an existing data object or create a new one. Args: - session: common.HTTPSession object containing base URL and authentication token. + session: HTTPSession 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. @@ -65,12 +66,12 @@ def touch( return common.process_response(r) -def remove(session: common.HTTPSession, lpath: str, catalog_only: int = 0, no_trash: int = 0, admin: int = 0) -> dict: +def remove(session: HTTPSession, lpath: str, catalog_only: int = 0, no_trash: int = 0, admin: int = 0) -> dict: """ Remove an existing data object. Args: - session: common.HTTPSession object containing base URL and authentication token. + session: HTTPSession 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. @@ -104,7 +105,7 @@ def remove(session: common.HTTPSession, lpath: str, catalog_only: int = 0, no_tr def calculate_checksum( - session: common.HTTPSession, + session: HTTPSession, lpath: str, resource: str = "", replica_number: int = -1, @@ -116,7 +117,7 @@ def calculate_checksum( Calculate the checksum for a data object. Args: - session: common.HTTPSession object containing base URL and authentication token. + session: HTTPSession 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. @@ -160,7 +161,7 @@ def calculate_checksum( def verify_checksum( - session: common.HTTPSession, + session: HTTPSession, lpath: str, resource: str = "", replica_number: int = -1, @@ -171,7 +172,7 @@ def verify_checksum( Verify the checksum for a data object. Args: - session: common.HTTPSession object containing base URL and authentication token. + session: HTTPSession 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. @@ -211,12 +212,12 @@ def verify_checksum( return common.process_response(r) -def stat(session: common.HTTPSession, lpath: str, ticket: str = "") -> dict: +def stat(session: HTTPSession, lpath: str, ticket: str = "") -> dict: """ Give information about a data object. Args: - session: common.HTTPSession object containing base URL and authentication token. + session: HTTPSession 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. @@ -238,12 +239,12 @@ def stat(session: common.HTTPSession, lpath: str, ticket: str = "") -> dict: return common.process_response(r) -def rename(session: common.HTTPSession, old_lpath: str, new_lpath: str) -> dict: +def rename(session: HTTPSession, old_lpath: str, new_lpath: str) -> dict: """ Rename or move a data object. Args: - session: common.HTTPSession object containing base URL and authentication token. + session: HTTPSession 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. @@ -267,7 +268,7 @@ def rename(session: common.HTTPSession, old_lpath: str, new_lpath: str) -> dict: def copy( - session: common.HTTPSession, + session: HTTPSession, src_lpath: str, dst_lpath: str, src_resource: str = "", @@ -278,7 +279,7 @@ def copy( Copy a data object. Args: - session: common.HTTPSession object containing base URL and authentication token. + session: HTTPSession 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 "". @@ -319,7 +320,7 @@ def copy( def replicate( - session: common.HTTPSession, + session: HTTPSession, lpath: str, src_resource: str = "", dst_resource: str = "", @@ -329,7 +330,7 @@ def replicate( Replicates a data object from one resource to another. Args: - session: common.HTTPSession object containing base URL and authentication token. + session: HTTPSession 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 "". @@ -362,12 +363,12 @@ def replicate( return common.process_response(r) -def trim(session: common.HTTPSession, lpath: str, replica_number: int, catalog_only: int = 0, admin: int = 0) -> dict: +def trim(session: HTTPSession, lpath: str, replica_number: int, catalog_only: int = 0, admin: int = 0) -> dict: """ Trims an existing replica or removes its catalog entry. Args: - session: common.HTTPSession object containing base URL and authentication token. + session: HTTPSession 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. @@ -401,7 +402,7 @@ def trim(session: common.HTTPSession, lpath: str, replica_number: int, catalog_o def register( - session: common.HTTPSession, + session: HTTPSession, lpath: str, ppath: str, resource: str, @@ -413,7 +414,7 @@ def register( Register a data object/replica into the catalog. Args: - session: common.HTTPSession object containing base URL and authentication token. + session: HTTPSession 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. @@ -455,12 +456,12 @@ def register( return common.process_response(r) -def read(session: common.HTTPSession, lpath: str, offset: int = 0, count: int = -1, ticket: str = "") -> dict: +def read(session: HTTPSession, lpath: str, offset: int = 0, count: int = -1, ticket: str = "") -> dict: """ Read bytes from a data object. Args: - session: common.HTTPSession object containing base URL and authentication token. + session: HTTPSession 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. @@ -495,7 +496,7 @@ def read(session: common.HTTPSession, lpath: str, offset: int = 0, count: int = def write( - session: common.HTTPSession, + session: HTTPSession, bytes_, lpath: str = "", resource: str = "", @@ -509,7 +510,7 @@ def write( Write bytes to a data object. Args: - session: common.HTTPSession object containing base URL and authentication token. + session: HTTPSession 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 "". @@ -567,7 +568,7 @@ def write( def parallel_write_init( - session: common.HTTPSession, + session: HTTPSession, lpath: str, stream_count: int, truncate: int = 1, @@ -578,7 +579,7 @@ def parallel_write_init( Initialize server-side state for parallel writing. Args: - session: common.HTTPSession object containing base URL and authentication token. + session: HTTPSession 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. @@ -616,12 +617,12 @@ def parallel_write_init( return common.process_response(r) -def parallel_write_shutdown(session: common.HTTPSession, parallel_write_handle: str) -> dict: +def parallel_write_shutdown(session: HTTPSession, parallel_write_handle: str) -> dict: """ Shuts down the parallel write state in the server. Args: - session: common.HTTPSession object containing base URL and authentication token. + session: HTTPSession object containing base URL and authentication token. parallel_write_handle: Handle obtained from parallel_write_init. Returns: @@ -645,12 +646,12 @@ def parallel_write_shutdown(session: common.HTTPSession, parallel_write_handle: return common.process_response(r) -def modify_metadata(session: common.HTTPSession, lpath: str, operations: list, admin: int = 0) -> dict: +def modify_metadata(session: HTTPSession, lpath: str, operations: list, admin: int = 0) -> dict: """ Modify the metadata for a data object. Args: - session: common.HTTPSession object containing base URL and authentication token. + session: HTTPSession 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. @@ -682,12 +683,12 @@ def modify_metadata(session: common.HTTPSession, lpath: str, operations: list, a return common.process_response(r) -def set_permission(session: common.HTTPSession, lpath: str, entity_name: str, permission: str, admin: int = 0) -> dict: +def set_permission(session: HTTPSession, lpath: str, entity_name: str, permission: str, admin: int = 0) -> dict: """ Set the permission of a user for a given data object. Args: - session: common.HTTPSession object containing base URL and authentication token. + session: HTTPSession 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'. @@ -725,12 +726,12 @@ def set_permission(session: common.HTTPSession, lpath: str, entity_name: str, pe return common.process_response(r) -def modify_permissions(session: common.HTTPSession, lpath: str, operations: list, admin: int = 0) -> dict: +def modify_permissions(session: HTTPSession, lpath: str, operations: list, admin: int = 0) -> dict: """ Modify permissions for multiple users or groups for a data object. Args: - session: common.HTTPSession object containing base URL and authentication token. + session: HTTPSession 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. @@ -763,7 +764,7 @@ def modify_permissions(session: common.HTTPSession, lpath: str, operations: list def modify_replica( - session: common.HTTPSession, + session: HTTPSession, lpath: str, resource_hierarchy: str = "", replica_number: int = -1, @@ -790,7 +791,7 @@ def modify_replica( Misuse can lead to catalog inconsistencies and unexpected behavior. Args: - session: common.HTTPSession object containing base URL and authentication token. + session: HTTPSession 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. diff --git a/irods_http_client/irods_http_client.py b/irods_http_client/irods_http_client.py index 6123f77..3cb6299 100644 --- a/irods_http_client/irods_http_client.py +++ b/irods_http_client/irods_http_client.py @@ -5,6 +5,30 @@ from irods_http_client import common +class HTTPSession: + """ + 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 HTTPSession 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 + + def authenticate(url_base: str, username: str, password: str) -> str: """ Authenticate using basic authentication credentials. @@ -40,7 +64,7 @@ def authenticate(url_base: str, username: str, password: str) -> str: # Check for success status code (2xx) if 200 <= r.status_code < 300: # noqa: PLR2004 - return common.HTTPSession(url_base, r.text) + return HTTPSession(url_base, r.text) # Handle error status codes error_msg = f"Authentication failed with status {r.status_code}" @@ -52,7 +76,7 @@ def authenticate(url_base: str, username: str, password: str) -> str: raise RuntimeError(f"Authentication request failed: {e!s}") from e -def get_server_info(session: common.HTTPSession): +def get_server_info(session: HTTPSession): """ Get general information about the iRODS server. diff --git a/irods_http_client/queries.py b/irods_http_client/queries.py index b2d5699..6768bc2 100644 --- a/irods_http_client/queries.py +++ b/irods_http_client/queries.py @@ -3,10 +3,11 @@ import requests from . import common +from .irods_http_client import HTTPSession # noqa: TC001 def execute_genquery( - session: common.HTTPSession, + session: HTTPSession, query: str, offset: int = 0, count: int = -1, @@ -79,7 +80,7 @@ def execute_genquery( def execute_specific_query( - session: common.HTTPSession, + session: HTTPSession, name: str, args: str = "", args_delimiter: str = ",", @@ -128,7 +129,7 @@ def execute_specific_query( return common.process_response(r) -def add_specific_query(session: common.HTTPSession, name: str, sql: str): +def add_specific_query(session: HTTPSession, name: str, sql: str): """ Add a SpecificQuery to the iRODS zone. @@ -154,7 +155,7 @@ def add_specific_query(session: common.HTTPSession, name: str, sql: str): return common.process_response(r) -def remove_specific_query(session: common.HTTPSession, name: str): +def remove_specific_query(session: HTTPSession, name: str): """ Remove a SpecificQuery from the iRODS zone. diff --git a/irods_http_client/resources.py b/irods_http_client/resources.py index 238fa3a..f4f55f0 100644 --- a/irods_http_client/resources.py +++ b/irods_http_client/resources.py @@ -5,9 +5,10 @@ import requests from . import common +from .irods_http_client import HTTPSession # noqa: TC001 -def create(session: common.HTTPSession, name: str, type_: str, host: str, vault_path: str, context: str): +def create(session: HTTPSession, name: str, type_: str, host: str, vault_path: str, context: str): """ Create a new resource. @@ -51,7 +52,7 @@ def create(session: common.HTTPSession, name: str, type_: str, host: str, vault_ return common.process_response(r) -def remove(session: common.HTTPSession, name: str): +def remove(session: HTTPSession, name: str): """ Remove an existing resource. @@ -76,7 +77,7 @@ def remove(session: common.HTTPSession, name: str): return common.process_response(r) -def modify(session: common.HTTPSession, name: str, property_: str, value: str): +def modify(session: HTTPSession, name: str, property_: str, value: str): """ Modify a property for a resource. @@ -126,7 +127,7 @@ def modify(session: common.HTTPSession, name: str, property_: str, value: str): return common.process_response(r) -def add_child(session: common.HTTPSession, parent_name: str, child_name: str, context: str = ""): +def add_child(session: HTTPSession, parent_name: str, child_name: str, context: str = ""): """ Create a parent-child relationship between two resources. @@ -158,7 +159,7 @@ def add_child(session: common.HTTPSession, parent_name: str, child_name: str, co return common.process_response(r) -def remove_child(session: common.HTTPSession, parent_name: str, child_name: str): +def remove_child(session: HTTPSession, parent_name: str, child_name: str): """ Remove a parent-child relationship between two resources. @@ -189,7 +190,7 @@ def remove_child(session: common.HTTPSession, parent_name: str, child_name: str) return common.process_response(r) -def rebalance(session: common.HTTPSession, name: str): +def rebalance(session: HTTPSession, name: str): """ Rebalance a resource hierarchy. @@ -214,7 +215,7 @@ def rebalance(session: common.HTTPSession, name: str): return common.process_response(r) -def stat(session: common.HTTPSession, name: str): +def stat(session: HTTPSession, name: str): """ Retrieve information for a resource. @@ -239,7 +240,7 @@ def stat(session: common.HTTPSession, name: str): return common.process_response(r) -def modify_metadata(session: common.HTTPSession, name: str, operations: dict, admin: int = 0): +def modify_metadata(session: HTTPSession, name: str, operations: dict, admin: int = 0): """ Modify the metadata for a resource. diff --git a/irods_http_client/rules.py b/irods_http_client/rules.py index 5c46d21..a184296 100644 --- a/irods_http_client/rules.py +++ b/irods_http_client/rules.py @@ -3,9 +3,10 @@ import requests from . import common +from .irods_http_client import HTTPSession # noqa: TC001 -def list_rule_engines(session: common.HTTPSession): +def list_rule_engines(session: HTTPSession): """ List available rule engine plugin instances. @@ -26,7 +27,7 @@ def list_rule_engines(session: common.HTTPSession): return common.process_response(r) -def execute(session: common.HTTPSession, rule_text: str, rep_instance: str = ""): +def execute(session: HTTPSession, rule_text: str, rep_instance: str = ""): """ Execute rule code. @@ -56,7 +57,7 @@ def execute(session: common.HTTPSession, rule_text: str, rep_instance: str = "") return common.process_response(r) -def remove_delay_rule(session: common.HTTPSession, rule_id: int): +def remove_delay_rule(session: HTTPSession, rule_id: int): """ Remove a delay rule from the catalog. diff --git a/irods_http_client/tickets.py b/irods_http_client/tickets.py index ba3b13c..ab6d67a 100644 --- a/irods_http_client/tickets.py +++ b/irods_http_client/tickets.py @@ -3,10 +3,11 @@ import requests from . import common +from .irods_http_client import HTTPSession # noqa: TC001 def create( - session: common.HTTPSession, + session: HTTPSession, lpath: str, type_: str = "read", use_count: int = -1, @@ -77,7 +78,7 @@ def create( return common.process_response(r) -def remove(session: common.HTTPSession, name: str): +def remove(session: HTTPSession, name: str): """ Remove an existing ticket. diff --git a/irods_http_client/users_groups.py b/irods_http_client/users_groups.py index d1d5e73..cbf518f 100644 --- a/irods_http_client/users_groups.py +++ b/irods_http_client/users_groups.py @@ -5,9 +5,10 @@ import requests from . import common +from .irods_http_client import HTTPSession # noqa: TC001 -def create_user(session: common.HTTPSession, name: str, zone: str, user_type: str = "rodsuser"): +def create_user(session: HTTPSession, name: str, zone: str, user_type: str = "rodsuser"): """ Create a new user. Requires rodsadmin or groupadmin privileges. @@ -41,7 +42,7 @@ def create_user(session: common.HTTPSession, name: str, zone: str, user_type: st return common.process_response(r) -def remove_user(session: common.HTTPSession, name: str, zone: str): +def remove_user(session: HTTPSession, name: str, zone: str): """ Remove a user. Requires rodsadmin privileges. @@ -68,7 +69,7 @@ def remove_user(session: common.HTTPSession, name: str, zone: str): return common.process_response(r) -def set_password(session: common.HTTPSession, name: str, zone: str, new_password: str = ""): +def set_password(session: HTTPSession, name: str, zone: str, new_password: str = ""): """ Change a users password. Requires rodsadmin privileges. @@ -102,7 +103,7 @@ def set_password(session: common.HTTPSession, name: str, zone: str, new_password return common.process_response(r) -def set_user_type(session: common.HTTPSession, name: str, zone: str, user_type: str): +def set_user_type(session: HTTPSession, name: str, zone: str, user_type: str): """ Change a users type. Requires rodsadmin privileges. @@ -141,7 +142,7 @@ def set_user_type(session: common.HTTPSession, name: str, zone: str, user_type: return common.process_response(r) -def create_group(session: common.HTTPSession, name: str): +def create_group(session: HTTPSession, name: str): """ Create a new group. Requires rodsadmin or groupadmin privileges. @@ -166,7 +167,7 @@ def create_group(session: common.HTTPSession, name: str): return common.process_response(r) -def remove_group(session: common.HTTPSession, name: str): +def remove_group(session: HTTPSession, name: str): """ Remove a group. Requires rodsadmin privileges. @@ -191,7 +192,7 @@ def remove_group(session: common.HTTPSession, name: str): return common.process_response(r) -def add_to_group(session: common.HTTPSession, user: str, zone: str, group: str = ""): +def add_to_group(session: HTTPSession, user: str, zone: str, group: str = ""): """ Add a user to a group. Requires rodsadmin or groupadmin privileges. @@ -220,7 +221,7 @@ def add_to_group(session: common.HTTPSession, user: str, zone: str, group: str = return common.process_response(r) -def remove_from_group(session: common.HTTPSession, user: str, zone: str, group: str): +def remove_from_group(session: HTTPSession, user: str, zone: str, group: str): """ Remove a user from a group. Requires rodsadmin or groupadmin privileges. @@ -249,7 +250,7 @@ def remove_from_group(session: common.HTTPSession, user: str, zone: str, group: return common.process_response(r) -def users(session: common.HTTPSession): +def users(session: HTTPSession): """ List all users in the zone. Requires rodsadmin privileges. @@ -268,7 +269,7 @@ def users(session: common.HTTPSession): return common.process_response(r) -def groups(session: common.HTTPSession): +def groups(session: HTTPSession): """ List all groups in the zone. Requires rodsadmin privileges. @@ -289,7 +290,7 @@ def groups(session: common.HTTPSession): return common.process_response(r) -def is_member_of_group(session: common.HTTPSession, group: str, user: str, zone: str): +def is_member_of_group(session: HTTPSession, group: str, user: str, zone: str): """ Return whether a user is a member of a group or not. @@ -323,7 +324,7 @@ def is_member_of_group(session: common.HTTPSession, group: str, user: str, zone: return common.process_response(r) -def stat(session: common.HTTPSession, name: str, zone: str = ""): +def stat(session: HTTPSession, name: str, zone: str = ""): """ Return information about a user or group. @@ -350,7 +351,7 @@ def stat(session: common.HTTPSession, name: str, zone: str = ""): return common.process_response(r) -def modify_metadata(session: common.HTTPSession, name: str, operations: list): +def modify_metadata(session: HTTPSession, name: str, operations: list): """ Modify the metadata for a user or group. Requires rodsadmin privileges. diff --git a/irods_http_client/zones.py b/irods_http_client/zones.py index acafb1f..4d253a0 100644 --- a/irods_http_client/zones.py +++ b/irods_http_client/zones.py @@ -3,9 +3,10 @@ import requests from . import common +from .irods_http_client import HTTPSession # noqa: TC001 -def add(session: common.HTTPSession, name: str, connection_info: str = "", comment: str = ""): +def add(session: HTTPSession, name: str, connection_info: str = "", comment: str = ""): """ Add a remote zone to the local zone. Requires rodsadmin privileges. @@ -39,7 +40,7 @@ def add(session: common.HTTPSession, name: str, connection_info: str = "", comme return common.process_response(r) -def remove(session: common.HTTPSession, name: str): +def remove(session: HTTPSession, name: str): """ Remove a remote zone from the local zone. Requires rodsadmin privileges. @@ -64,7 +65,7 @@ def remove(session: common.HTTPSession, name: str): return common.process_response(r) -def modify(session: common.HTTPSession, name: str, property_: str, value: str): +def modify(session: HTTPSession, name: str, property_: str, value: str): """ Modify properties of a remote zone. Requires rodsadmin privileges. @@ -94,7 +95,7 @@ def modify(session: common.HTTPSession, name: str, property_: str, value: str): return common.process_response(r) -def report(session: common.HTTPSession): +def report(session: HTTPSession): """ Return information about the iRODS zone. Requires rodsadmin privileges. @@ -116,7 +117,7 @@ def report(session: common.HTTPSession): return common.process_response(r) -def stat(session: common.HTTPSession, name: str): +def stat(session: HTTPSession, name: str): """ Return information about a named iRODS zone. Requires rodsadmin privileges. From 748fb32c5094e7650a0fbe48f72895e9e3849107 Mon Sep 17 00:00:00 2001 From: Terrell Russell Date: Sun, 22 Feb 2026 11:51:00 -0500 Subject: [PATCH 11/29] [27] rename package and module to irods_http --- README.md | 38 +++++----- {irods_http_client => irods_http}/__init__.py | 6 +- .../collections.py | 42 +++++------ {irods_http_client => irods_http}/common.py | 0 .../data_objects.py | 74 +++++++++---------- .../irods_http.py | 14 ++-- {irods_http_client => irods_http}/queries.py | 18 ++--- .../resources.py | 34 ++++----- {irods_http_client => irods_http}/rules.py | 14 ++-- {irods_http_client => irods_http}/tickets.py | 10 +-- .../users_groups.py | 54 +++++++------- {irods_http_client => irods_http}/zones.py | 22 +++--- pyproject.toml | 3 +- test/__init__.py | 2 +- test/config.py | 2 +- test/test_endpoint_operations.py | 2 +- 16 files changed, 166 insertions(+), 169 deletions(-) rename {irods_http_client => irods_http}/__init__.py (84%) rename {irods_http_client => irods_http}/collections.py (84%) rename {irods_http_client => irods_http}/common.py (100%) rename {irods_http_client => irods_http}/data_objects.py (91%) rename irods_http_client/irods_http_client.py => irods_http/irods_http.py (89%) rename {irods_http_client => irods_http}/queries.py (91%) rename {irods_http_client => irods_http}/resources.py (87%) rename {irods_http_client => irods_http}/rules.py (83%) rename {irods_http_client => irods_http}/tickets.py (93%) rename {irods_http_client => irods_http}/users_groups.py (86%) rename {irods_http_client => irods_http}/zones.py (86%) diff --git a/README.md b/README.md index 3efad86..eaa2060 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 = session.collections.create('//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_client/__init__.py b/irods_http/__init__.py similarity index 84% rename from irods_http_client/__init__.py rename to irods_http/__init__.py index 40dc878..5b2a8ca 100644 --- a/irods_http_client/__init__.py +++ b/irods_http/__init__.py @@ -10,14 +10,14 @@ users_groups as users_groups, zones as zones, ) -from .irods_http_client import ( - HTTPSession as HTTPSession, +from .irods_http import ( + IRODSHTTPSession as IRODSHTTPSession, authenticate as authenticate, get_server_info as get_server_info, ) __all__ = [ - "HTTPSession", + "IRODSHTTPSession", "authenticate", "collections", "data_objects", diff --git a/irods_http_client/collections.py b/irods_http/collections.py similarity index 84% rename from irods_http_client/collections.py rename to irods_http/collections.py index 69b2f7f..be5ffce 100644 --- a/irods_http_client/collections.py +++ b/irods_http/collections.py @@ -5,15 +5,15 @@ import requests from . import common -from .irods_http_client import HTTPSession # noqa: TC001 +from .irods_http import IRODSHTTPSession # noqa: TC001 -def create(session: HTTPSession, lpath: str, create_intermediates: int = 0) -> dict: +def create(session: IRODSHTTPSession, lpath: str, create_intermediates: int = 0) -> dict: """ Create a new collection. Args: - session: HTTPSession object containing the base URL and authentication token. + 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. @@ -40,12 +40,12 @@ def create(session: HTTPSession, lpath: str, create_intermediates: int = 0) -> d return common.process_response(r) -def remove(session: HTTPSession, lpath: str, recurse: int = 0, no_trash: int = 0) -> dict: +def remove(session: IRODSHTTPSession, lpath: str, recurse: int = 0, no_trash: int = 0) -> dict: """ Remove an existing collection. Args: - session: HTTPSession object containing the base URL and authentication token. + 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 move the collection to trash, 0 to permanently remove. Defaults to 0. @@ -75,12 +75,12 @@ def remove(session: HTTPSession, lpath: str, recurse: int = 0, no_trash: int = 0 return common.process_response(r) -def stat(session: HTTPSession, lpath: str, ticket: str = "") -> dict: +def stat(session: IRODSHTTPSession, lpath: str, ticket: str = "") -> dict: """ Give information about a collection. Args: - session: HTTPSession object containing the base URL and authentication token. + 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. @@ -102,12 +102,12 @@ def stat(session: HTTPSession, lpath: str, ticket: str = "") -> dict: return common.process_response(r) -def list_collection(session: HTTPSession, lpath: str, recurse: int = 0, ticket: str = "") -> dict: +def list_collection(session: IRODSHTTPSession, lpath: str, recurse: int = 0, ticket: str = "") -> dict: """ Show the contents of a collection. Args: - session: HTTPSession object containing the base URL and authentication token. + 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. @@ -133,7 +133,7 @@ def list_collection(session: HTTPSession, lpath: str, recurse: int = 0, ticket: def set_permission( - session: HTTPSession, + session: IRODSHTTPSession, lpath: str, entity_name: str, permission: str, @@ -143,7 +143,7 @@ def set_permission( Set the permission of a user for a given collection. Args: - session: HTTPSession object containing the base URL and authentication token. + 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'. @@ -181,12 +181,12 @@ def set_permission( return common.process_response(r) -def set_inheritance(session: HTTPSession, lpath: str, enable: int, admin: int = 0) -> dict: +def set_inheritance(session: IRODSHTTPSession, lpath: str, enable: int, admin: int = 0) -> dict: """ Set the inheritance for a collection. Args: - session: HTTPSession object containing the base URL and authentication token. + 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. @@ -216,12 +216,12 @@ def set_inheritance(session: HTTPSession, lpath: str, enable: int, admin: int = return common.process_response(r) -def modify_permissions(session: HTTPSession, lpath: str, operations: dict, admin: int = 0) -> dict: +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: HTTPSession object containing the base URL and authentication token. + 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. @@ -253,12 +253,12 @@ def modify_permissions(session: HTTPSession, lpath: str, operations: dict, admin return common.process_response(r) -def modify_metadata(session: HTTPSession, lpath: str, operations: dict, admin: int = 0) -> dict: +def modify_metadata(session: IRODSHTTPSession, lpath: str, operations: dict, admin: int = 0) -> dict: """ Modify the metadata for a collection. Args: - session: HTTPSession object containing the base URL and authentication token. + 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. @@ -290,12 +290,12 @@ def modify_metadata(session: HTTPSession, lpath: str, operations: dict, admin: i return common.process_response(r) -def rename(session: HTTPSession, old_lpath: str, new_lpath: str) -> dict: +def rename(session: IRODSHTTPSession, old_lpath: str, new_lpath: str) -> dict: """ Rename or move a collection. Args: - session: HTTPSession object containing the base URL and authentication token. + 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. @@ -318,12 +318,12 @@ def rename(session: HTTPSession, old_lpath: str, new_lpath: str) -> dict: return common.process_response(r) -def touch(session: HTTPSession, lpath: str, seconds_since_epoch: int = -1, reference: str = "") -> dict: +def touch(session: IRODSHTTPSession, lpath: str, seconds_since_epoch: int = -1, reference: str = "") -> dict: """ Update mtime for a collection. Args: - session: HTTPSession object containing the base URL and authentication token. + 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. diff --git a/irods_http_client/common.py b/irods_http/common.py similarity index 100% rename from irods_http_client/common.py rename to irods_http/common.py diff --git a/irods_http_client/data_objects.py b/irods_http/data_objects.py similarity index 91% rename from irods_http_client/data_objects.py rename to irods_http/data_objects.py index 6d8138f..c82842e 100644 --- a/irods_http_client/data_objects.py +++ b/irods_http/data_objects.py @@ -5,11 +5,11 @@ import requests from . import common -from .irods_http_client import HTTPSession # noqa: TC001 +from .irods_http import IRODSHTTPSession # noqa: TC001 def touch( - session: HTTPSession, + session: IRODSHTTPSession, lpath: str, no_create: int = 0, replica_number: int = -1, @@ -21,7 +21,7 @@ def touch( Update mtime for an existing data object or create a new one. Args: - session: HTTPSession object containing base URL and authentication token. + 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. @@ -66,12 +66,12 @@ def touch( return common.process_response(r) -def remove(session: HTTPSession, lpath: str, catalog_only: int = 0, no_trash: int = 0, admin: int = 0) -> dict: +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: HTTPSession object containing base URL and authentication token. + 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. @@ -105,7 +105,7 @@ def remove(session: HTTPSession, lpath: str, catalog_only: int = 0, no_trash: in def calculate_checksum( - session: HTTPSession, + session: IRODSHTTPSession, lpath: str, resource: str = "", replica_number: int = -1, @@ -117,7 +117,7 @@ def calculate_checksum( Calculate the checksum for a data object. Args: - session: HTTPSession object containing base URL and authentication token. + 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. @@ -161,7 +161,7 @@ def calculate_checksum( def verify_checksum( - session: HTTPSession, + session: IRODSHTTPSession, lpath: str, resource: str = "", replica_number: int = -1, @@ -172,7 +172,7 @@ def verify_checksum( Verify the checksum for a data object. Args: - session: HTTPSession object containing base URL and authentication token. + 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. @@ -212,12 +212,12 @@ def verify_checksum( return common.process_response(r) -def stat(session: HTTPSession, lpath: str, ticket: str = "") -> dict: +def stat(session: IRODSHTTPSession, lpath: str, ticket: str = "") -> dict: """ Give information about a data object. Args: - session: HTTPSession object containing base URL and authentication token. + 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. @@ -239,12 +239,12 @@ def stat(session: HTTPSession, lpath: str, ticket: str = "") -> dict: return common.process_response(r) -def rename(session: HTTPSession, old_lpath: str, new_lpath: str) -> dict: +def rename(session: IRODSHTTPSession, old_lpath: str, new_lpath: str) -> dict: """ Rename or move a data object. Args: - session: HTTPSession object containing base URL and authentication token. + 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. @@ -268,7 +268,7 @@ def rename(session: HTTPSession, old_lpath: str, new_lpath: str) -> dict: def copy( - session: HTTPSession, + session: IRODSHTTPSession, src_lpath: str, dst_lpath: str, src_resource: str = "", @@ -279,7 +279,7 @@ def copy( Copy a data object. Args: - session: HTTPSession object containing base URL and authentication token. + 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 "". @@ -320,7 +320,7 @@ def copy( def replicate( - session: HTTPSession, + session: IRODSHTTPSession, lpath: str, src_resource: str = "", dst_resource: str = "", @@ -330,7 +330,7 @@ def replicate( Replicates a data object from one resource to another. Args: - session: HTTPSession object containing base URL and authentication token. + 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 "". @@ -363,12 +363,12 @@ def replicate( return common.process_response(r) -def trim(session: HTTPSession, lpath: str, replica_number: int, catalog_only: int = 0, admin: int = 0) -> dict: +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: HTTPSession object containing base URL and authentication token. + 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. @@ -402,7 +402,7 @@ def trim(session: HTTPSession, lpath: str, replica_number: int, catalog_only: in def register( - session: HTTPSession, + session: IRODSHTTPSession, lpath: str, ppath: str, resource: str, @@ -414,7 +414,7 @@ def register( Register a data object/replica into the catalog. Args: - session: HTTPSession object containing base URL and authentication token. + 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. @@ -456,12 +456,12 @@ def register( return common.process_response(r) -def read(session: HTTPSession, lpath: str, offset: int = 0, count: int = -1, ticket: str = "") -> dict: +def read(session: IRODSHTTPSession, lpath: str, offset: int = 0, count: int = -1, ticket: str = "") -> dict: """ Read bytes from a data object. Args: - session: HTTPSession object containing base URL and authentication token. + 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. @@ -496,7 +496,7 @@ def read(session: HTTPSession, lpath: str, offset: int = 0, count: int = -1, tic def write( - session: HTTPSession, + session: IRODSHTTPSession, bytes_, lpath: str = "", resource: str = "", @@ -510,7 +510,7 @@ def write( Write bytes to a data object. Args: - session: HTTPSession object containing base URL and authentication token. + 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 "". @@ -568,7 +568,7 @@ def write( def parallel_write_init( - session: HTTPSession, + session: IRODSHTTPSession, lpath: str, stream_count: int, truncate: int = 1, @@ -579,7 +579,7 @@ def parallel_write_init( Initialize server-side state for parallel writing. Args: - session: HTTPSession object containing base URL and authentication token. + 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. @@ -617,12 +617,12 @@ def parallel_write_init( return common.process_response(r) -def parallel_write_shutdown(session: HTTPSession, parallel_write_handle: str) -> dict: +def parallel_write_shutdown(session: IRODSHTTPSession, parallel_write_handle: str) -> dict: """ Shuts down the parallel write state in the server. Args: - session: HTTPSession object containing base URL and authentication token. + session: IRODSHTTPSession object containing base URL and authentication token. parallel_write_handle: Handle obtained from parallel_write_init. Returns: @@ -646,12 +646,12 @@ def parallel_write_shutdown(session: HTTPSession, parallel_write_handle: str) -> return common.process_response(r) -def modify_metadata(session: HTTPSession, lpath: str, operations: list, admin: int = 0) -> dict: +def modify_metadata(session: IRODSHTTPSession, lpath: str, operations: list, admin: int = 0) -> dict: """ Modify the metadata for a data object. Args: - session: HTTPSession object containing base URL and authentication token. + 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. @@ -683,12 +683,12 @@ def modify_metadata(session: HTTPSession, lpath: str, operations: list, admin: i return common.process_response(r) -def set_permission(session: HTTPSession, lpath: str, entity_name: str, permission: str, admin: int = 0) -> dict: +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: HTTPSession object containing base URL and authentication token. + 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'. @@ -726,12 +726,12 @@ def set_permission(session: HTTPSession, lpath: str, entity_name: str, permissio return common.process_response(r) -def modify_permissions(session: HTTPSession, lpath: str, operations: list, admin: int = 0) -> dict: +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: HTTPSession object containing base URL and authentication token. + 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. @@ -764,7 +764,7 @@ def modify_permissions(session: HTTPSession, lpath: str, operations: list, admin def modify_replica( - session: HTTPSession, + session: IRODSHTTPSession, lpath: str, resource_hierarchy: str = "", replica_number: int = -1, @@ -791,7 +791,7 @@ def modify_replica( Misuse can lead to catalog inconsistencies and unexpected behavior. Args: - session: HTTPSession object containing base URL and authentication token. + 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. diff --git a/irods_http_client/irods_http_client.py b/irods_http/irods_http.py similarity index 89% rename from irods_http_client/irods_http_client.py rename to irods_http/irods_http.py index 3cb6299..df3ba76 100644 --- a/irods_http_client/irods_http_client.py +++ b/irods_http/irods_http.py @@ -1,11 +1,11 @@ -"""Main client module for iRODS HTTP API interactions.""" +"""Main module for iRODS HTTP API interactions.""" import requests -from irods_http_client import common +from irods_http import common -class HTTPSession: +class IRODSHTTPSession: """ Encapsulates HTTP session details for iRODS HTTP API. @@ -19,7 +19,7 @@ class HTTPSession: def __init__(self, url_base: str, token: str): """ - Initialize HTTPSession with URL and token. + Initialize IRODSHTTPSession with URL and token. Args: url_base: The base URL for the iRODS HTTP API. @@ -64,7 +64,7 @@ def authenticate(url_base: str, username: str, password: str) -> str: # Check for success status code (2xx) if 200 <= r.status_code < 300: # noqa: PLR2004 - return HTTPSession(url_base, r.text) + return IRODSHTTPSession(url_base, r.text) # Handle error status codes error_msg = f"Authentication failed with status {r.status_code}" @@ -76,12 +76,12 @@ def authenticate(url_base: str, username: str, password: str) -> str: raise RuntimeError(f"Authentication request failed: {e!s}") from e -def get_server_info(session: HTTPSession): +def get_server_info(session: IRODSHTTPSession): """ Get general information about the iRODS server. Args: - session: An HTTPSession instance. + session: An IRODSHTTPSession instance. Returns: A dict containing the HTTP status code and iRODS response. diff --git a/irods_http_client/queries.py b/irods_http/queries.py similarity index 91% rename from irods_http_client/queries.py rename to irods_http/queries.py index 6768bc2..8e86adf 100644 --- a/irods_http_client/queries.py +++ b/irods_http/queries.py @@ -3,11 +3,11 @@ import requests from . import common -from .irods_http_client import HTTPSession # noqa: TC001 +from .irods_http import IRODSHTTPSession # noqa: TC001 def execute_genquery( - session: HTTPSession, + session: IRODSHTTPSession, query: str, offset: int = 0, count: int = -1, @@ -21,7 +21,7 @@ def execute_genquery( Execute a GenQuery string and returns the results. Args: - session: An HTTPSession instance. + 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. @@ -80,7 +80,7 @@ def execute_genquery( def execute_specific_query( - session: HTTPSession, + session: IRODSHTTPSession, name: str, args: str = "", args_delimiter: str = ",", @@ -91,7 +91,7 @@ def execute_specific_query( Execute a specific query and returns the results. Args: - session: An HTTPSession instance. + 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 ','. @@ -129,12 +129,12 @@ def execute_specific_query( return common.process_response(r) -def add_specific_query(session: HTTPSession, name: str, sql: str): +def add_specific_query(session: IRODSHTTPSession, name: str, sql: str): """ Add a SpecificQuery to the iRODS zone. Args: - session: An HTTPSession instance. + session: An IRODSHTTPSession instance. name: The name of the query to be added. sql: The SQL attached to the query. @@ -155,12 +155,12 @@ def add_specific_query(session: HTTPSession, name: str, sql: str): return common.process_response(r) -def remove_specific_query(session: HTTPSession, name: str): +def remove_specific_query(session: IRODSHTTPSession, name: str): """ Remove a SpecificQuery from the iRODS zone. Args: - session: An HTTPSession instance. + session: An IRODSHTTPSession instance. name: The name of the SpecificQuery to be removed. Returns: diff --git a/irods_http_client/resources.py b/irods_http/resources.py similarity index 87% rename from irods_http_client/resources.py rename to irods_http/resources.py index f4f55f0..429b004 100644 --- a/irods_http_client/resources.py +++ b/irods_http/resources.py @@ -5,15 +5,15 @@ import requests from . import common -from .irods_http_client import HTTPSession # noqa: TC001 +from .irods_http import IRODSHTTPSession # noqa: TC001 -def create(session: HTTPSession, name: str, type_: str, host: str, vault_path: str, context: str): +def create(session: IRODSHTTPSession, name: str, type_: str, host: str, vault_path: str, context: str): """ Create a new resource. Args: - session: An HTTPSession instance. + 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 @@ -52,12 +52,12 @@ def create(session: HTTPSession, name: str, type_: str, host: str, vault_path: s return common.process_response(r) -def remove(session: HTTPSession, name: str): +def remove(session: IRODSHTTPSession, name: str): """ Remove an existing resource. Args: - session: An HTTPSession instance. + session: An IRODSHTTPSession instance. name: The name of the resource to be removed. Returns: @@ -77,12 +77,12 @@ def remove(session: HTTPSession, name: str): return common.process_response(r) -def modify(session: HTTPSession, name: str, property_: str, value: str): +def modify(session: IRODSHTTPSession, name: str, property_: str, value: str): """ Modify a property for a resource. Args: - session: An HTTPSession instance. + 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. @@ -127,12 +127,12 @@ def modify(session: HTTPSession, name: str, property_: str, value: str): return common.process_response(r) -def add_child(session: HTTPSession, parent_name: str, child_name: str, context: str = ""): +def add_child(session: IRODSHTTPSession, parent_name: str, child_name: str, context: str = ""): """ Create a parent-child relationship between two resources. Args: - session: An HTTPSession instance. + 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. @@ -159,12 +159,12 @@ def add_child(session: HTTPSession, parent_name: str, child_name: str, context: return common.process_response(r) -def remove_child(session: HTTPSession, parent_name: str, child_name: str): +def remove_child(session: IRODSHTTPSession, parent_name: str, child_name: str): """ Remove a parent-child relationship between two resources. Args: - session: An HTTPSession instance. + session: An IRODSHTTPSession instance. parent_name: The name of the parent resource. child_name: The name of the child resource. @@ -190,12 +190,12 @@ def remove_child(session: HTTPSession, parent_name: str, child_name: str): return common.process_response(r) -def rebalance(session: HTTPSession, name: str): +def rebalance(session: IRODSHTTPSession, name: str): """ Rebalance a resource hierarchy. Args: - session: An HTTPSession instance. + session: An IRODSHTTPSession instance. name: The name of the resource to be rebalanced. Returns: @@ -215,12 +215,12 @@ def rebalance(session: HTTPSession, name: str): return common.process_response(r) -def stat(session: HTTPSession, name: str): +def stat(session: IRODSHTTPSession, name: str): """ Retrieve information for a resource. Args: - session: An HTTPSession instance. + session: An IRODSHTTPSession instance. name: The name of the resource to be accessed. Returns: @@ -240,12 +240,12 @@ def stat(session: HTTPSession, name: str): return common.process_response(r) -def modify_metadata(session: HTTPSession, name: str, operations: dict, admin: int = 0): +def modify_metadata(session: IRODSHTTPSession, name: str, operations: dict, admin: int = 0): """ Modify the metadata for a resource. Args: - session: An HTTPSession instance. + 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. diff --git a/irods_http_client/rules.py b/irods_http/rules.py similarity index 83% rename from irods_http_client/rules.py rename to irods_http/rules.py index a184296..f28d1a2 100644 --- a/irods_http_client/rules.py +++ b/irods_http/rules.py @@ -3,15 +3,15 @@ import requests from . import common -from .irods_http_client import HTTPSession # noqa: TC001 +from .irods_http import IRODSHTTPSession # noqa: TC001 -def list_rule_engines(session: HTTPSession): +def list_rule_engines(session: IRODSHTTPSession): """ List available rule engine plugin instances. Args: - session: An HTTPSession instance. + session: An IRODSHTTPSession instance. Returns: A dict containing the HTTP status code and iRODS response. @@ -27,12 +27,12 @@ def list_rule_engines(session: HTTPSession): return common.process_response(r) -def execute(session: HTTPSession, rule_text: str, rep_instance: str = ""): +def execute(session: IRODSHTTPSession, rule_text: str, rep_instance: str = ""): """ Execute rule code. Args: - session: An HTTPSession instance. + session: An IRODSHTTPSession instance. rule_text: The rule code to execute. rep_instance: The rule engine plugin to run the rule-text against. @@ -57,12 +57,12 @@ def execute(session: HTTPSession, rule_text: str, rep_instance: str = ""): return common.process_response(r) -def remove_delay_rule(session: HTTPSession, rule_id: int): +def remove_delay_rule(session: IRODSHTTPSession, rule_id: int): """ Remove a delay rule from the catalog. Args: - session: An HTTPSession instance. + session: An IRODSHTTPSession instance. rule_id: The id of the delay rule to be removed. Returns: diff --git a/irods_http_client/tickets.py b/irods_http/tickets.py similarity index 93% rename from irods_http_client/tickets.py rename to irods_http/tickets.py index ab6d67a..abfe43f 100644 --- a/irods_http_client/tickets.py +++ b/irods_http/tickets.py @@ -3,11 +3,11 @@ import requests from . import common -from .irods_http_client import HTTPSession # noqa: TC001 +from .irods_http import IRODSHTTPSession # noqa: TC001 def create( - session: HTTPSession, + session: IRODSHTTPSession, lpath: str, type_: str = "read", use_count: int = -1, @@ -22,7 +22,7 @@ def create( Create a new ticket for a collection or data object. Args: - session: An HTTPSession instance. + 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. @@ -78,12 +78,12 @@ def create( return common.process_response(r) -def remove(session: HTTPSession, name: str): +def remove(session: IRODSHTTPSession, name: str): """ Remove an existing ticket. Args: - session: An HTTPSession instance. + session: An IRODSHTTPSession instance. name: The ticket to be removed. Returns: diff --git a/irods_http_client/users_groups.py b/irods_http/users_groups.py similarity index 86% rename from irods_http_client/users_groups.py rename to irods_http/users_groups.py index cbf518f..b4394f3 100644 --- a/irods_http_client/users_groups.py +++ b/irods_http/users_groups.py @@ -5,15 +5,15 @@ import requests from . import common -from .irods_http_client import HTTPSession # noqa: TC001 +from .irods_http import IRODSHTTPSession # noqa: TC001 -def create_user(session: HTTPSession, name: str, zone: str, user_type: str = "rodsuser"): +def create_user(session: IRODSHTTPSession, name: str, zone: str, user_type: str = "rodsuser"): """ Create a new user. Requires rodsadmin or groupadmin privileges. Args: - session: An HTTPSession instance. + session: An IRODSHTTPSession instance. 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. @@ -42,12 +42,12 @@ def create_user(session: HTTPSession, name: str, zone: str, user_type: str = "ro return common.process_response(r) -def remove_user(session: HTTPSession, name: str, zone: str): +def remove_user(session: IRODSHTTPSession, name: str, zone: str): """ Remove a user. Requires rodsadmin privileges. Args: - session: An HTTPSession instance. + session: An IRODSHTTPSession instance. name: The name of the user to be removed. zone: The zone for the user to be removed. @@ -69,12 +69,12 @@ def remove_user(session: HTTPSession, name: str, zone: str): return common.process_response(r) -def set_password(session: HTTPSession, name: str, zone: str, new_password: str = ""): +def set_password(session: IRODSHTTPSession, name: str, zone: str, new_password: str = ""): """ Change a users password. Requires rodsadmin privileges. Args: - session: An HTTPSession instance. + 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. @@ -103,12 +103,12 @@ def set_password(session: HTTPSession, name: str, zone: str, new_password: str = return common.process_response(r) -def set_user_type(session: HTTPSession, name: str, zone: str, user_type: str): +def set_user_type(session: IRODSHTTPSession, name: str, zone: str, user_type: str): """ Change a users type. Requires rodsadmin privileges. Args: - session: An HTTPSession instance. + 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. user_type: Can be rodsuser, groupadmin, or rodsadmin. @@ -142,12 +142,12 @@ def set_user_type(session: HTTPSession, name: str, zone: str, user_type: str): return common.process_response(r) -def create_group(session: HTTPSession, name: str): +def create_group(session: IRODSHTTPSession, name: str): """ Create a new group. Requires rodsadmin or groupadmin privileges. Args: - session: An HTTPSession instance. + session: An IRODSHTTPSession instance. name: The name of the group to be created. Returns: @@ -167,12 +167,12 @@ def create_group(session: HTTPSession, name: str): return common.process_response(r) -def remove_group(session: HTTPSession, name: str): +def remove_group(session: IRODSHTTPSession, name: str): """ Remove a group. Requires rodsadmin privileges. Args: - session: An HTTPSession instance. + session: An IRODSHTTPSession instance. name: The name of the group to be removed. Returns: @@ -192,12 +192,12 @@ def remove_group(session: HTTPSession, name: str): return common.process_response(r) -def add_to_group(session: HTTPSession, user: str, zone: str, group: str = ""): +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 HTTPSession instance. + 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. @@ -221,12 +221,12 @@ def add_to_group(session: HTTPSession, user: str, zone: str, group: str = ""): return common.process_response(r) -def remove_from_group(session: HTTPSession, user: str, zone: str, group: str): +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 HTTPSession instance. + 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. @@ -250,12 +250,12 @@ def remove_from_group(session: HTTPSession, user: str, zone: str, group: str): return common.process_response(r) -def users(session: HTTPSession): +def users(session: IRODSHTTPSession): """ List all users in the zone. Requires rodsadmin privileges. Args: - session: An HTTPSession instance. + session: An IRODSHTTPSession instance. Returns: A dict containing the HTTP status code and iRODS response. @@ -269,12 +269,12 @@ def users(session: HTTPSession): return common.process_response(r) -def groups(session: HTTPSession): +def groups(session: IRODSHTTPSession): """ List all groups in the zone. Requires rodsadmin privileges. Args: - session: An HTTPSession instance. + session: An IRODSHTTPSession instance. Returns: A dict containing the HTTP status code and iRODS response. @@ -290,12 +290,12 @@ def groups(session: HTTPSession): return common.process_response(r) -def is_member_of_group(session: HTTPSession, group: str, user: str, zone: str): +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 HTTPSession instance. + session: An IRODSHTTPSession instance. group: The group being checked. user: The user being checked. zone: The zone for the user being checked. @@ -324,12 +324,12 @@ def is_member_of_group(session: HTTPSession, group: str, user: str, zone: str): return common.process_response(r) -def stat(session: HTTPSession, name: str, zone: str = ""): +def stat(session: IRODSHTTPSession, name: str, zone: str = ""): """ Return information about a user or group. Args: - session: An HTTPSession instance. + 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. @@ -351,12 +351,12 @@ def stat(session: HTTPSession, name: str, zone: str = ""): return common.process_response(r) -def modify_metadata(session: HTTPSession, name: str, operations: list): +def modify_metadata(session: IRODSHTTPSession, name: str, operations: list): """ Modify the metadata for a user or group. Requires rodsadmin privileges. Args: - session: An HTTPSession instance. + session: An IRODSHTTPSession instance. name: The user or group to be modified. operations: The operations to be carried out. diff --git a/irods_http_client/zones.py b/irods_http/zones.py similarity index 86% rename from irods_http_client/zones.py rename to irods_http/zones.py index 4d253a0..75131d8 100644 --- a/irods_http_client/zones.py +++ b/irods_http/zones.py @@ -3,15 +3,15 @@ import requests from . import common -from .irods_http_client import HTTPSession # noqa: TC001 +from .irods_http import IRODSHTTPSession # noqa: TC001 -def add(session: HTTPSession, name: str, connection_info: str = "", comment: str = ""): +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 HTTPSession instance. + 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. @@ -40,12 +40,12 @@ def add(session: HTTPSession, name: str, connection_info: str = "", comment: str return common.process_response(r) -def remove(session: HTTPSession, name: str): +def remove(session: IRODSHTTPSession, name: str): """ Remove a remote zone from the local zone. Requires rodsadmin privileges. Args: - session: An HTTPSession instance. + session: An IRODSHTTPSession instance. name: The zone to be removed. Returns: @@ -65,12 +65,12 @@ def remove(session: HTTPSession, name: str): return common.process_response(r) -def modify(session: HTTPSession, name: str, property_: str, value: str): +def modify(session: IRODSHTTPSession, name: str, property_: str, value: str): """ Modify properties of a remote zone. Requires rodsadmin privileges. Args: - session: An HTTPSession instance. + 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 :. @@ -95,12 +95,12 @@ def modify(session: HTTPSession, name: str, property_: str, value: str): return common.process_response(r) -def report(session: HTTPSession): +def report(session: IRODSHTTPSession): """ Return information about the iRODS zone. Requires rodsadmin privileges. Args: - session: An HTTPSession instance. + session: An IRODSHTTPSession instance. Returns: A dict containing the HTTP status code and iRODS response. @@ -117,12 +117,12 @@ def report(session: HTTPSession): return common.process_response(r) -def stat(session: HTTPSession, name: str): +def stat(session: IRODSHTTPSession, name: str): """ Return information about a named iRODS zone. Requires rodsadmin privileges. Args: - session: An HTTPSession instance. + session: An IRODSHTTPSession instance. name: The name of the zone. Returns: 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 5e820e2..f7e361e 100644 --- a/test/test_endpoint_operations.py +++ b/test/test_endpoint_operations.py @@ -13,7 +13,7 @@ import config -from irods_http_client import ( +from irods_http import ( authenticate, collections, common, From 0e36d99d688f14a04ed2dafc52506c0d575bcce1 Mon Sep 17 00:00:00 2001 From: Terrell Russell Date: Sun, 22 Feb 2026 12:21:34 -0500 Subject: [PATCH 12/29] tests clean themselves --- test/test_endpoint_operations.py | 676 ++++++++++++++++--------------- 1 file changed, 345 insertions(+), 331 deletions(-) diff --git a/test/test_endpoint_operations.py b/test/test_endpoint_operations.py index f7e361e..35b951e 100644 --- a/test/test_endpoint_operations.py +++ b/test/test_endpoint_operations.py @@ -184,278 +184,287 @@ def setUp(self): # tests the create operation def test_create(self): """Test collection creation operations and parameter validation.""" - # 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") - - # 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, - ) - - # test creating new collection - response = collections.create(self.rodsadmin_session, f"/{self.zone_name}/home/new") - self.assertTrue(response["data"]["created"]) - self.assertEqual(response["data"]["irods_response"]["status_code"], 0) + 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, + ) - # test creating existing collection - response = collections.create(self.rodsadmin_session, f"/{self.zone_name}/home/new") - self.assertFalse(response["data"]["created"]) - self.assertEqual(response["data"]["irods_response"]["status_code"], 0) + # test creating new collection + response = collections.create(self.rodsadmin_session, 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 = collections.create(self.rodsadmin_session, f"/{self.zone_name}/home/new") + self.assertFalse(response["data"]["created"]) + self.assertEqual(response["data"]["irods_response"]["status_code"], 0) + + # test invalid path + response = collections.create(self.rodsadmin_session, 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 - response = collections.create(self.rodsadmin_session, f"{self.zone_name}/home/new") - self.assertEqual( - "{'irods_response': {'status_code': -358000, " - "'status_message': 'path does not exist: OBJ_PATH_DOES_NOT_EXIST'}}", - str(response["data"]), - ) + # test create_intermediates + response = collections.create(self.rodsadmin_session, 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 = collections.create(self.rodsadmin_session, f"/{self.zone_name}/home/test/folder", 1) + self.assertEqual( + "{'created': True, 'irods_response': {'status_code': 0}}", + str(response["data"]), + ) - # test create_intermediates - response = collections.create(self.rodsadmin_session, 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 = collections.create(self.rodsadmin_session, f"/{self.zone_name}/home/test/folder", 1) - self.assertEqual( - "{'created': True, '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 remove operation def test_remove(self): """Test collection removal operations and parameter validation.""" - # 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") - - # 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, - ) + 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 removing collection - response = collections.create(self.rodsadmin_session, f"/{self.zone_name}/home/new") - self.assertEqual( - "{'created': True, 'irods_response': {'status_code': 0}}", - str(response["data"]), - ) - response = collections.remove(self.rodsadmin_session, f"/{self.zone_name}/home/new") - self.assertEqual("{'irods_response': {'status_code': 0}}", str(response["data"])) - # test invalid paths - response = collections.stat(self.rodsadmin_session, f"/{self.zone_name}/home/tensaitekinaaidorusama") - self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) - response = collections.stat(self.rodsadmin_session, f"/{self.zone_name}/home/aremonainainaikoremonainainai") - self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) - response = collections.stat(self.rodsadmin_session, f"/{self.zone_name}/home/binglebangledingledangle") - self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) - response = collections.stat(self.rodsadmin_session, f"{self.zone_name}/home/{self.rodsadmin_username}") - self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) + # test removing collection + response = collections.create(self.rodsadmin_session, f"/{self.zone_name}/home/new") + self.assertEqual( + "{'created': True, 'irods_response': {'status_code': 0}}", + str(response["data"]), + ) + response = collections.remove(self.rodsadmin_session, f"/{self.zone_name}/home/new") + self.assertEqual("{'irods_response': {'status_code': 0}}", str(response["data"])) + + # test invalid paths + response = collections.stat(self.rodsadmin_session, f"/{self.zone_name}/home/tensaitekinaaidorusama") + self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) + response = collections.stat(self.rodsadmin_session, f"/{self.zone_name}/home/aremonainainaikoremonainainai") + self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) + response = collections.stat(self.rodsadmin_session, f"/{self.zone_name}/home/binglebangledingledangle") + self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) + response = collections.stat(self.rodsadmin_session, f"{self.zone_name}/home/{self.rodsadmin_username}") + self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) + + # test recurse + response = collections.create(self.rodsadmin_session, f"/{self.zone_name}/home/test/folder", 1) + self.assertEqual( + "{'created': True, 'irods_response': {'status_code': 0}}", + str(response["data"]), + ) + response = collections.remove(self.rodsadmin_session, 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 = collections.remove(self.rodsadmin_session, f"/{self.zone_name}/home/test", 1) + self.assertEqual("{'irods_response': {'status_code': 0}}", str(response["data"])) - # test recurse - response = collections.create(self.rodsadmin_session, f"/{self.zone_name}/home/test/folder", 1) - self.assertEqual( - "{'created': True, 'irods_response': {'status_code': 0}}", - str(response["data"]), - ) - response = collections.remove(self.rodsadmin_session, 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 = collections.remove(self.rodsadmin_session, 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.""" - # clean up test collections - collections.remove(self.rodsadmin_session, 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, 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 invalid paths + response = collections.stat(self.rodsadmin_session, f"/{self.zone_name}/home/new") + self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) + response = collections.stat(self.rodsadmin_session, f"{self.zone_name}/home/new") + self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) - # test invalid paths - response = collections.stat(self.rodsadmin_session, f"/{self.zone_name}/home/new") - self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) - response = collections.stat(self.rodsadmin_session, f"{self.zone_name}/home/new") - self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) + # test valid path + response = collections.stat(self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}") + self.assertTrue(response["data"]["permissions"]) - # test valid path - response = collections.stat(self.rodsadmin_session, 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.""" - # 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") + try: + # test param checking + self.assertRaises(TypeError, collections.list_collection, self.rodsadmin_session, 0, "ticket") + self.assertRaises( + TypeError, + collections.list_collection, + self.rodsadmin_session, + f"/{self.zone_name}/home/{self.rodsadmin_username}", + "0", + "ticket", + ) + self.assertRaises( + ValueError, + collections.list_collection, + self.rodsadmin_session, + f"/{self.zone_name}/home/{self.rodsadmin_username}", + 5, + "ticket", + ) + self.assertRaises( + TypeError, + collections.list_collection, + self.rodsadmin_session, + f"/{self.zone_name}/home/{self.rodsadmin_username}", + 0, + 0, + ) - # test param checking - self.assertRaises(TypeError, collections.list_collection, self.rodsadmin_session, 0, "ticket") - self.assertRaises( - TypeError, - collections.list_collection, - self.rodsadmin_session, - f"/{self.zone_name}/home/{self.rodsadmin_username}", - "0", - "ticket", - ) - self.assertRaises( - ValueError, - collections.list_collection, - self.rodsadmin_session, - f"/{self.zone_name}/home/{self.rodsadmin_username}", - 5, - "ticket", - ) - self.assertRaises( - TypeError, - collections.list_collection, - self.rodsadmin_session, - f"/{self.zone_name}/home/{self.rodsadmin_username}", - 0, - 0, - ) + # test empty collection + response = collections.list_collection( + self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}" + ) + self.assertEqual("None", str(response["data"]["entries"])) - # test empty collection - response = collections.list_collection( - self.rodsadmin_session, 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") + response = collections.list_collection( + self.rodsadmin_session, 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 one item - collections.create(self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}/bosnia") - response = collections.list_collection( - self.rodsadmin_session, 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") + response = collections.list_collection( + self.rodsadmin_session, 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 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") - response = collections.list_collection( - self.rodsadmin_session, 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", + ) + response = collections.list_collection( + self.rodsadmin_session, 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 without recursion - collections.create( - self.rodsadmin_session, - f"/{self.zone_name}/home/{self.rodsadmin_username}/croatia/zagreb", - ) - response = collections.list_collection( - self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}" - ) - self.assertEqual( - f"/{self.zone_name}/home/{self.rodsadmin_username}/albania", - str(response["data"]["entries"][0]), - ) - self.assertEqual( - f"/{self.zone_name}/home/{self.rodsadmin_username}/bosnia", - str(response["data"]["entries"][1]), - ) - self.assertEqual( - f"/{self.zone_name}/home/{self.rodsadmin_username}/croatia", - str(response["data"]["entries"][2]), - ) - self.assertEqual(len(response["data"]["entries"]), 3) + # test with recursion + response = collections.list_collection( + self.rodsadmin_session, 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]), + ) - # test with recursion - response = collections.list_collection( - self.rodsadmin_session, 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): @@ -508,105 +517,110 @@ def test_set_permission(self): 5, ) - # create new collection - response = collections.create(self.rodsadmin_session, f"/{self.zone_name}/home/setPerms") - self.assertEqual(response["data"]["irods_response"]["status_code"], 0) + try: + # create new collection + response = collections.create(self.rodsadmin_session, f"/{self.zone_name}/home/setPerms") + self.assertEqual(response["data"]["irods_response"]["status_code"], 0) - # test no permission - response = collections.stat(self.rodsuser_session, f"/{self.zone_name}/home/setPerms") - self.assertEqual(response["data"]["irods_response"]["status_code"], -170000) + # test no permission + response = collections.stat(self.rodsuser_session, f"/{self.zone_name}/home/setPerms") + self.assertEqual(response["data"]["irods_response"]["status_code"], -170000) - # test set permission - response = collections.set_permission( - self.rodsadmin_session, - f"/{self.zone_name}/home/setPerms", - self.rodsuser_username, - "read", - ) - self.assertEqual("{'irods_response': {'status_code': 0}}", str(response["data"])) + # test set permission + response = collections.set_permission( + self.rodsadmin_session, + f"/{self.zone_name}/home/setPerms", + self.rodsuser_username, + "read", + ) + self.assertEqual("{'irods_response': {'status_code': 0}}", str(response["data"])) - # test with permission - response = collections.stat(self.rodsadmin_session, f"/{self.zone_name}/home/setPerms") - self.assertTrue(response["data"]["permissions"]) + # test with permission + response = collections.stat(self.rodsadmin_session, f"/{self.zone_name}/home/setPerms") + self.assertTrue(response["data"]["permissions"]) - # test set permission null - response = collections.set_permission( - self.rodsadmin_session, - f"/{self.zone_name}/home/setPerms", - self.rodsuser_username, - "null", - ) - self.assertEqual("{'irods_response': {'status_code': 0}}", str(response["data"])) + # test set permission null + response = collections.set_permission( + self.rodsadmin_session, + f"/{self.zone_name}/home/setPerms", + self.rodsuser_username, + "null", + ) + self.assertEqual("{'irods_response': {'status_code': 0}}", str(response["data"])) - # test no permission - response = collections.stat(self.rodsuser_session, f"/{self.zone_name}/home/setPerms") - self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) + # test no permission + response = collections.stat(self.rodsuser_session, f"/{self.zone_name}/home/setPerms") + self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) - # remove the collection - response = collections.remove(self.rodsadmin_session, 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", 1, 1) # tests the set inheritance operation def test_set_inheritance(self): """Test setting inheritance for collection permissions.""" - # test param checking - self.assertRaises(TypeError, collections.set_inheritance, self.rodsadmin_session, 0, 0, 0) - self.assertRaises( - TypeError, - collections.set_inheritance, - self.rodsadmin_session, - f"/{self.zone_name}/home/{self.rodsadmin_username}", - "0", - 0, - ) - self.assertRaises( - ValueError, - collections.set_inheritance, - self.rodsadmin_session, - f"/{self.zone_name}/home/{self.rodsadmin_username}", - 5, - 0, - ) - self.assertRaises( - TypeError, - collections.set_inheritance, - self.rodsadmin_session, - f"/{self.zone_name}/home/{self.rodsadmin_username}", - 0, - "0", - ) - self.assertRaises( - ValueError, - collections.set_inheritance, - self.rodsadmin_session, - f"/{self.zone_name}/home/{self.rodsadmin_username}", - 0, - 5, - ) + testcoll = f"/{self.zone_name}/home/{self.rodsadmin_username}/testcoll" - # control - response = collections.stat(self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}") - self.assertFalse(response["data"]["inheritance_enabled"]) + try: + collections.create(self.rodsadmin_session, testcoll) - # test enabling inheritance - response = collections.set_inheritance( - self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}", 1 - ) - self.assertEqual("{'irods_response': {'status_code': 0}}", str(response["data"])) + # 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, + ) - # check if changed - response = collections.stat(self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}") - self.assertTrue(response["data"]["inheritance_enabled"]) + # control + response = collections.stat(self.rodsadmin_session, testcoll) + self.assertFalse(response["data"]["inheritance_enabled"]) - # test disabling inheritance - response = collections.set_inheritance( - self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}", 0 - ) - self.assertEqual("{'irods_response': {'status_code': 0}}", str(response["data"])) + # test enabling inheritance + response = collections.set_inheritance(self.rodsadmin_session, testcoll, 1) + self.assertEqual("{'irods_response': {'status_code': 0}}", str(response["data"])) - # check if changed - response = collections.stat(self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}") - self.assertFalse(response["data"]["inheritance_enabled"]) + # verify inheritance is enabled + response = collections.stat(self.rodsadmin_session, testcoll) + self.assertTrue(response["data"]["inheritance_enabled"]) + + # test disabling inheritance + response = collections.set_inheritance(self.rodsadmin_session, testcoll, 0) + self.assertEqual("{'irods_response': {'status_code': 0}}", str(response["data"])) + + # verify inheritance is disabled + response = collections.stat(self.rodsadmin_session, testcoll) + self.assertFalse(response["data"]["inheritance_enabled"]) + + finally: + collections.remove(self.rodsadmin_session, testcoll) # test the modify permissions operation def test_modify_permissions(self): @@ -1126,7 +1140,7 @@ def test_touch(self): finally: # Remove the object - data_objects.remove(self.rodsadmin_session, f) + data_objects.remove(self.rodsadmin_session, f, no_trash=1) def test_register(self): """Test registering existing files as iRODS data objects.""" From 469292a913b5eef603451830641d6ce215095374 Mon Sep 17 00:00:00 2001 From: Terrell Russell Date: Sun, 22 Feb 2026 12:54:48 -0500 Subject: [PATCH 13/29] tests clean themselves --- irods_http/collections.py | 2 +- test/test_endpoint_operations.py | 249 ++++++++++++++++--------------- 2 files changed, 127 insertions(+), 124 deletions(-) diff --git a/irods_http/collections.py b/irods_http/collections.py index be5ffce..9cb8446 100644 --- a/irods_http/collections.py +++ b/irods_http/collections.py @@ -48,7 +48,7 @@ def remove(session: IRODSHTTPSession, lpath: str, recurse: int = 0, no_trash: in 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 move the collection to trash, 0 to permanently remove. 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. diff --git a/test/test_endpoint_operations.py b/test/test_endpoint_operations.py index 35b951e..856b11b 100644 --- a/test/test_endpoint_operations.py +++ b/test/test_endpoint_operations.py @@ -625,157 +625,160 @@ def test_set_inheritance(self): # test the modify permissions operation def test_modify_permissions(self): """Test modifying permissions on collections.""" + 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, collections.modify_permissions, self.rodsadmin_session, 0, ops_permissions, 0) - self.assertRaises( - TypeError, - collections.modify_permissions, - self.rodsadmin_session, - f"/{self.zone_name}/home/{self.rodsadmin_username}", - 5, - 0, - ) - self.assertRaises( - TypeError, - collections.modify_permissions, - self.rodsadmin_session, - f"/{self.zone_name}/home/{self.rodsadmin_username}", - ops_permissions, - "0", - ) - self.assertRaises( - ValueError, - collections.modify_permissions, - self.rodsadmin_session, - f"/{self.zone_name}/home/{self.rodsadmin_username}", - ops_permissions, - 5, - ) + try: + # create new collection + response = collections.create(self.rodsadmin_session, testcoll) + self.assertEqual(response["data"]["irods_response"]["status_code"], 0) - # create new collection - response = collections.create(self.rodsadmin_session, f"/{self.zone_name}/home/modPerms") - self.assertEqual(response["data"]["irods_response"]["status_code"], 0) + # 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 - response = collections.stat(self.rodsuser_session, f"/{self.zone_name}/home/modPerms") - self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) + # test no permissions + response = collections.stat(self.rodsuser_session, testcoll) + self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) - # test set permissions - response = collections.modify_permissions( - self.rodsadmin_session, f"/{self.zone_name}/home/modPerms", ops_permissions - ) - self.assertEqual(response["data"]["irods_response"]["status_code"], 0) + # test set permissions + response = collections.modify_permissions(self.rodsadmin_session, testcoll, ops_permissions) + self.assertEqual(response["data"]["irods_response"]["status_code"], 0) - # test with permissions - response = collections.stat(self.rodsadmin_session, f"/{self.zone_name}/home/modPerms") - self.assertTrue(response["data"]["permissions"]) + # test with permissions + response = collections.stat(self.rodsadmin_session, testcoll) + self.assertTrue(response["data"]["permissions"]) - # test set permissions nuil - response = collections.modify_permissions( - self.rodsadmin_session, f"/{self.zone_name}/home/modPerms", ops_permissions_null - ) - self.assertEqual(response["data"]["irods_response"]["status_code"], 0) + # test set permissions nuil + response = collections.modify_permissions(self.rodsadmin_session, testcoll, ops_permissions_null) + self.assertEqual(response["data"]["irods_response"]["status_code"], 0) - # test without permissions - response = collections.stat(self.rodsuser_session, f"/{self.zone_name}/home/modPerms") - self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) + # test without permissions + response = collections.stat(self.rodsuser_session, testcoll) + self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) - # remove the collection - response = collections.remove(self.rodsadmin_session, 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.""" + 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, collections.modify_metadata, self.rodsadmin_session, 0, ops_metadata, 0) - self.assertRaises( - TypeError, - collections.modify_metadata, - self.rodsadmin_session, - f"/{self.zone_name}/home/{self.rodsadmin_username}", - 5, - 0, - ) - self.assertRaises( - TypeError, - collections.modify_metadata, - self.rodsadmin_session, - f"/{self.zone_name}/home/{self.rodsadmin_username}", - ops_metadata, - "0", - ) - self.assertRaises( - ValueError, - collections.modify_metadata, - self.rodsadmin_session, - f"/{self.zone_name}/home/{self.rodsadmin_username}", - ops_metadata, - 5, - ) + try: + collections.create(self.rodsadmin_session, testcoll) - # test adding and removing metadata - response = collections.modify_metadata( - self.rodsadmin_session, - f"/{self.zone_name}/home/{self.rodsadmin_username}", - ops_metadata, - ) - self.assertEqual(response["data"]["irods_response"]["status_code"], 0) - response = collections.modify_metadata( - self.rodsadmin_session, - f"/{self.zone_name}/home/{self.rodsadmin_username}", - ops_metadata_remove, - ) - self.assertEqual(response["data"]["irods_response"]["status_code"], 0) + # 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 = collections.modify_metadata( + self.rodsadmin_session, + testcoll, + ops_metadata, + ) + self.assertEqual(response["data"]["irods_response"]["status_code"], 0) + response = collections.modify_metadata( + self.rodsadmin_session, + testcoll, + ops_metadata_remove, + ) + self.assertEqual(response["data"]["irods_response"]["status_code"], 0) + + finally: + collections.remove(self.rodsadmin_session, testcoll, no_trash=1) # tests the rename operation def test_rename(self): """Test renaming collections.""" - # test param checking - self.assertRaises( - TypeError, - collections.rename, - self.rodsadmin_session, - f"/{self.zone_name}/home/{self.rodsadmin_username}", - 0, - ) - self.assertRaises(TypeError, 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 = collections.stat(self.rodsadmin_session, f"/{self.zone_name}/home/pods") - self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) - response = collections.stat(self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}") - self.assertTrue(response["data"]["permissions"]) + try: + collections.create(self.rodsadmin_session, testcolla) - # test renaming - response = collections.rename( - self.rodsadmin_session, - 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 param checking + self.assertRaises( + TypeError, + collections.rename, + self.rodsadmin_session, + testcolla, + 0, + ) + self.assertRaises(TypeError, collections.rename, 0, testcolla) - # test before move - response = collections.stat(self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}") - self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) - response = collections.stat(self.rodsadmin_session, f"/{self.zone_name}/home/pods") - self.assertTrue(response["data"]["permissions"]) + # test renaming + response = collections.rename( + self.rodsadmin_session, + testcolla, + testcollb, + ) + self.assertEqual(response["data"]["irods_response"]["status_code"], 0) - # test renaming - response = collections.rename( - self.rodsadmin_session, - f"/{self.zone_name}/home/pods", - f"/{self.zone_name}/home/{self.rodsadmin_username}", - ) - self.assertEqual("{'irods_response': {'status_code': 0}}", str(response["data"])) + # test presence + response = collections.stat(self.rodsadmin_session, testcolla) + self.assertEqual(response["data"]["irods_response"]["status_code"], -170000) + response = collections.stat(self.rodsadmin_session, testcollb) + self.assertEqual(response["data"]["irods_response"]["status_code"], 0) + + 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): From 03cca3fc57742214dcfc9ec6298d97a522a87ea2 Mon Sep 17 00:00:00 2001 From: Terrell Russell Date: Sun, 22 Feb 2026 13:59:48 -0500 Subject: [PATCH 14/29] tests clean themselves --- test/test_endpoint_operations.py | 794 ++++++++++++++----------------- 1 file changed, 353 insertions(+), 441 deletions(-) diff --git a/test/test_endpoint_operations.py b/test/test_endpoint_operations.py index 856b11b..b8a754c 100644 --- a/test/test_endpoint_operations.py +++ b/test/test_endpoint_operations.py @@ -939,11 +939,11 @@ def test_common_operations(self): finally: # Remove the data objects - r = data_objects.remove(self.rodsuser_session, f1, 0, 1) + r = data_objects.remove(self.rodsuser_session, f1, no_trash=1) - r = data_objects.remove(self.rodsuser_session, f2, 0, 1) + r = data_objects.remove(self.rodsuser_session, f2, no_trash=1) - r = data_objects.remove(self.rodsuser_session, f3, 0, 1) + r = data_objects.remove(self.rodsuser_session, f3, no_trash=1) # Remove the resource r = resources.remove(self.rodsadmin_session, resc) @@ -1022,42 +1022,44 @@ def test_modify_replica(self): def test_checksums(self): """Test checksum calculation and verification for data objects.""" - # Create a unixfilesystem resource. - r = resources.create( - self.rodsadmin_session, - "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" - # Create a non-empty data object - r = data_objects.write( - self.rodsadmin_session, - "These are the bytes being written to the object", - f, - ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + resc = "newresource" - # Replicate the data object - r = data_objects.replicate( - self.rodsadmin_session, - f, - dst_resource="newresource", - ) - 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 + "", + ) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) - # Show that there are two replicas - r = queries.execute_genquery( - self.rodsadmin_session, "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) + # Create a non-empty data object + r = data_objects.write( + self.rodsadmin_session, + "These are the bytes being written to the object", + f, + ) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Replicate the data object + r = data_objects.replicate( + self.rodsadmin_session, + f, + dst_resource=resc, + ) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Show that there are two replicas + r = queries.execute_genquery( + self.rodsadmin_session, "select DATA_NAME, DATA_REPL_NUM where DATA_NAME = 'file.txt'" + ) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + self.assertEqual(len(r["data"]["rows"]), 2) - try: # Calculate a checksum for the first replica r = data_objects.calculate_checksum( self.rodsadmin_session, @@ -1070,7 +1072,7 @@ def test_checksums(self): r = data_objects.calculate_checksum( self.rodsadmin_session, f, - resource="newresource", + resource=resc, ) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) @@ -1086,15 +1088,15 @@ def test_checksums(self): r = data_objects.verify_checksum( self.rodsadmin_session, f, - resource="newresource", + resource=resc, ) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) finally: - # Remove the data objects + # Remove the data object data_objects.remove( self.rodsadmin_session, - f"/{self.zone_name}/home/{self.rodsadmin_username}/file.txt", + f, catalog_only=0, no_trash=1, ) @@ -1178,7 +1180,7 @@ def test_register(self): 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, 1) + r = data_objects.remove(self.rodsadmin_session, filename, catalog_only=1) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Register the leftover local file into the catalog as a new data object. @@ -1207,26 +1209,17 @@ def test_register(self): finally: # Unregister the data object - data_objects.remove(self.rodsadmin_session, filename, 1) + data_objects.remove(self.rodsadmin_session, filename, catalog_only=1) # Remove the resource resources.remove(self.rodsadmin_session, "register_resource") def test_parallel_write(self): """Test parallel writing to data objects.""" - data_objects.remove( - self.rodsadmin_session, - 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 = data_objects.parallel_write_init( - self.rodsadmin_session, - f"/{self.zone_name}/home/{self.rodsadmin_username}/parallel-write.txt", - 3, - ) + r = data_objects.parallel_write_init(self.rodsadmin_session, f, 3) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) handle = r["data"]["parallel_write_handle"] @@ -1234,20 +1227,20 @@ def test_parallel_write(self): # 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( data_objects.write, self.rodsadmin_session, - bytes_=e[1] * count, - offset=e[0] * count, - stream_index=e[0], + 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() + for future in concurrent.futures.as_completed(futures): + r = future.result() self.assertEqual(r["data"]["irods_response"]["status_code"], 0) finally: # Close parallel write @@ -1256,9 +1249,9 @@ def test_parallel_write(self): # Remove the object data_objects.remove( self.rodsadmin_session, - f"/{self.zone_name}/home/{self.rodsadmin_username}/parallel-write.txt", - 0, - 1, + f, + catalog_only=0, + no_trash=1, ) @@ -1282,148 +1275,130 @@ def setUp(self): def test_common_operations(self): """Test common resource operations (create, list, stat, etc.).""" - # 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 - resources.remove_child(self.rodsadmin_session, "test_repl", "test_ufs0") - resources.remove_child(self.rodsadmin_session, "test_repl", "test_ufs1") - resources.remove(self.rodsadmin_session, "test_ufs0") - resources.remove(self.rodsadmin_session, "test_ufs1") - resources.remove(self.rodsadmin_session, "test_repl") - resc_repl = "test_repl" resc_ufs0 = "test_ufs0" resc_ufs1 = "test_ufs1" - # Create three resources (replication w/ two unixfilesystem resources). - r = resources.create(self.rodsadmin_session, resc_repl, "replication", "", "", "") - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + f = f"/{self.zone_name}/home/{self.rodsadmin_username}/test_object.txt" - # Show the replication resource was created. - r = resources.stat(self.rodsadmin_session, 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 = resources.create(self.rodsadmin_session, 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 = resources.add_child(self.rodsadmin_session, resc_repl, resc_name) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) - - # Show that the resource was created and configured successfully. - r = resources.stat(self.rodsadmin_session, 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 = data_objects.write(self.rodsadmin_session, "These are the bytes to be written", data_object, resc_repl, 0) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + try: + # Create replication resource. + r = resources.create(self.rodsadmin_session, resc_repl, "replication", "", "", "") + 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(data_object).name}'", - ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) - self.assertEqual(len(r["data"]["rows"]), 2) + # Show the replication resource was created. + r = resources.stat(self.rodsadmin_session, 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 = resources.create(self.rodsadmin_session, resc_name, "unixfilesystem", self.host, vault_path, "") + 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)]) + # Add the unixfilesystem resource as a child of the replication resource. + r = resources.add_child(self.rodsadmin_session, resc_repl, resc_name) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) - # Trim a replica. - r = data_objects.trim(self.rodsadmin_session, data_object, 0) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + # Show that the resource was created and configured successfully. + r = resources.stat(self.rodsadmin_session, 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. + r = data_objects.write(self.rodsadmin_session, "These are the bytes to be written", f, resc_repl, 0) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) - # 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(data_object).name}'", - ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) - self.assertEqual(len(r["data"]["rows"]), 1) + # 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}'", + ) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + self.assertEqual(len(r["data"]["rows"]), 2) - # Launch rebalance - r = resources.rebalance(self.rodsadmin_session, 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, 0) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) - # - # 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}'", + ) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + self.assertEqual(len(r["data"]["rows"]), 1) - # Remove the data object. - r = data_objects.remove(self.rodsadmin_session, data_object, 0, 1) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + # Launch rebalance + r = resources.rebalance(self.rodsadmin_session, resc_repl) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) - # Remove resources. - for resc_name in [resc_ufs0, resc_ufs1]: - with self.subTest(f"Detach and remove resource [{resc_name}] from [{resc_repl}]"): - # Detach ufs resource from the replication resource. - r = resources.remove_child(self.rodsadmin_session, 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 = resources.remove(self.rodsadmin_session, 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}'", + ) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + self.assertEqual(len(r["data"]["rows"]), 2) - # Show that the resource no longer exists. - r = resources.stat(self.rodsadmin_session, 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 replication resource. - r = resources.remove(self.rodsadmin_session, resc_repl) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + # Remove resources. + for resc_name in [resc_ufs0, resc_ufs1]: + with self.subTest(f"Detach and remove resource [{resc_name}] from [{resc_repl}]"): + # Detach and remove the ufs resource. + resources.remove_child(self.rodsadmin_session, resc_repl, resc_name) + resources.remove(self.rodsadmin_session, resc_name) - # Show that the resource no longer exists. - r = resources.stat(self.rodsadmin_session, resc_repl) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) - self.assertEqual(r["data"]["exists"], False) + # Remove replication resource. + resources.remove(self.rodsadmin_session, resc_repl) def test_modify_failures(self): """Test modifying resources, poorly.""" @@ -1472,8 +1447,8 @@ def test_add_child_context(self): r = resources.stat(self.rodsadmin_session, resc) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Uncomment once parent_context is available - # https://github.com/irods/irods_client_http_api/issues/473 - # self.assertEqual(r["data"]["info"]["parent_context"], "neat") + # -- https://github.com/irods/irods_client_http_api/issues/473 + # self.assertEqual(r["data"]["info"]["parent_context"], "neat") finally: resources.remove_child(self.rodsadmin_session, "demoResc", resc) @@ -1481,59 +1456,61 @@ def test_add_child_context(self): def test_modify_metadata(self): """Test modifying metadata on resources.""" - # Create a unixfilesystem resource. - r = resources.create( - self.rodsadmin_session, - "metadata_demo", - "unixfilesystem", - self.host, - "/tmp/metadata_demo_vault", # noqa: S108 - "ignoreme", - ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) - - operations = [{"operation": "add", "attribute": "a1", "value": "v1", "units": "u1"}] + resc = "metadata_resc" - # Add the metadata to the resource - r = resources.modify_metadata(self.rodsadmin_session, "metadata_demo", operations) - 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/metadata_demo_vault", # noqa: S108 + "ignoreme", + ) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) - # 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'", - ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) - self.assertEqual(r["data"]["rows"][0][0], "metadata_demo") + # Add the metadata to the resource + operations = [{"operation": "add", "attribute": "a1", "value": "v1", "units": "u1"}] + r = resources.modify_metadata(self.rodsadmin_session, resc, operations) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) - # Remove the metadata from the resource. - operations = [{"operation": "remove", "attribute": "a1", "value": "v1", "units": "u1"}] + # 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'", + ) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + self.assertEqual(r["data"]["rows"][0][0], resc) - r = resources.modify_metadata(self.rodsadmin_session, "metadata_demo", operations) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + # Remove the metadata from the resource. + operations = [{"operation": "remove", "attribute": "a1", "value": "v1", "units": "u1"}] + r = resources.modify_metadata(self.rodsadmin_session, resc, operations) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) - # 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'", - ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) - self.assertEqual(len(r["data"]["rows"]), 0) + # 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'", + ) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + self.assertEqual(len(r["data"]["rows"]), 0) - # Remove the resource - r = resources.remove(self.rodsadmin_session, "metadata_demo") + finally: + # Remove the resource + resources.remove(self.rodsadmin_session, resc) def test_modify_properties(self): """Test modifying resource properties.""" resource = "properties_demo" - # Create a new resource. - r = resources.create(self.rodsadmin_session, resource, "replication", "", "", "") - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) - try: + # Create a new resource. + r = resources.create(self.rodsadmin_session, resource, "replication", "", "", "") + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + # The list of updates to apply in sequence. property_map = [ ("name", "test_modifying_resource_properties_renamed"), @@ -1565,7 +1542,7 @@ def test_modify_properties(self): self.assertEqual(r["data"]["info"][p], v) finally: # Remove the resource - r = resources.remove(self.rodsadmin_session, resource) + resources.remove(self.rodsadmin_session, resource) # Tests for rule operations @@ -1589,16 +1566,13 @@ def setUp(self): def test_list(self): """Test listing rule engine plugins.""" # Try listing rule engine plugins - r = rules.list_rule_engines( - self.rodsadmin_session, - ) - + r = rules.list_rule_engines(self.rodsadmin_session) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) self.assertGreater(len(r["data"]["rule_engine_plugin_instances"]), 0) def test_execute_rule(self): """Test executing iRODS rules.""" - test_msg = "This was run by the iRODS HTTP API test suite!" + test_msg = "Hello from the test suite" # Execute rule text against the iRODS rule language. r = rules.execute( @@ -1618,27 +1592,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 = rules.execute( - self.rodsadmin_session, - 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 = queries.execute_genquery(self.rodsadmin_session, "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, + ) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) - 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)") + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + self.assertEqual(len(r["data"]["rows"]), 1) - # Remove the delay rule. - r = rules.remove_delay_rule(self.rodsadmin_session, 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 @@ -1680,20 +1653,19 @@ def test_create_execute_remove_specific_query(self): """Test creating, executing, and removing specific queries.""" try: # As rodsadmin, create a specific query - name = "get_users_count" sql = "select count(*) from r_user_main" r = queries.add_specific_query(self.rodsadmin_session, name=name, sql=sql) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) - # Switch to rodsuser and execute it + # Execute as rodsuser r = queries.execute_specific_query(self.rodsuser_session, name=name) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) self.assertEqual(r["data"]["rows"][0][0], "3") finally: - # Switch to rodsadmin and remove it - r = queries.remove_specific_query(self.rodsadmin_session, name=name) + # Remove as rodsadmin + queries.remove_specific_query(self.rodsadmin_session, name=name) # Tests for tickets operations @@ -1717,6 +1689,7 @@ def setUp(self): 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") @@ -1823,30 +1796,6 @@ def test_set_to_bad_type(self): ValueError, users_groups.set_user_type, self.rodsadmin_session, "baduser", self.zone_name, user_type="bad" ) - def test_create_stat_and_remove_rodsuser(self): - """Test creating, querying, and removing rodsuser users.""" - new_username = "test_user_rodsuser" - user_type = "rodsuser" - - # Create a new user. - r = users_groups.create_user(self.rodsadmin_session, new_username, self.zone_name, user_type) - self.assertEqual(r["status_code"], 200) - - # 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"], user_type) - - # Remove the user. - r = users_groups.remove_user(self.rodsadmin_session, new_username, self.zone_name) - self.assertEqual(r["status_code"], 200) - def test_empty_username(self): """Test authenticate with an empty username.""" self.assertRaises(ValueError, authenticate, self.url_base, "", "nope") @@ -1860,171 +1809,133 @@ def test_set_password(self): new_username = "test_user_rodsuser" user_type = "rodsuser" - # Create a new user. - r = users_groups.create_user(self.rodsadmin_session, new_username, self.zone_name, user_type) - self.assertEqual(r["status_code"], 200) - - new_password = "new_password" # noqa: S105 - # Set a new password - r = users_groups.set_password(self.rodsadmin_session, new_username, self.zone_name, new_password) - self.assertEqual(r["status_code"], 200) - - # Try to get a token for the user - session = authenticate(self.url_base, new_username, new_password) - self.assertIsInstance(session.token, str) - - # Remove the user. - r = users_groups.remove_user(self.rodsadmin_session, 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.""" - new_username = "test_user_rodsadmin" - user_type = "rodsadmin" - - # Create a new user. - r = users_groups.create_user(self.rodsadmin_session, new_username, self.zone_name, user_type) - self.assertEqual(r["status_code"], 200) - - # 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"], user_type) - - # Remove the user. - r = users_groups.remove_user(self.rodsadmin_session, new_username, self.zone_name) - self.assertEqual(r["status_code"], 200) + try: + # Create a new user. + r = users_groups.create_user(self.rodsadmin_session, new_username, self.zone_name, user_type) + self.assertEqual(r["status_code"], 200) - def test_create_stat_and_remove_groupadmin(self): - """Test creating, querying, and removing groupadmin users.""" - 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) + self.assertEqual(r["status_code"], 200) - # Create a new user. - r = users_groups.create_user(self.rodsadmin_session, 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) + self.assertEqual(r["status_code"], 200) + self.assertIsInstance(session.token, str) - # Stat the user. - r = users_groups.stat(self.rodsadmin_session, 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 = users_groups.remove_user(self.rodsadmin_session, 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.""" - # Create a new group. - new_group = "test_group" - r = users_groups.create_group(self.rodsadmin_session, new_group) - self.assertEqual(r["status_code"], 200) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) - - # 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) - self.assertEqual(r["status_code"], 200) - - # Add user to group. - r = users_groups.add_to_group(self.rodsadmin_session, 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 = users_groups.is_member_of_group(self.rodsadmin_session, 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) + 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) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) - # Remove user from group. - r = users_groups.remove_from_group(self.rodsadmin_session, new_username, self.zone_name, new_group) + # 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) + self.assertEqual(r["status_code"], 200) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) - self.assertEqual(r["status_code"], 200) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + # Add user to group. + r = users_groups.add_to_group(self.rodsadmin_session, new_username, self.zone_name, new_group) + self.assertEqual(r["status_code"], 200) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) - # Remove the user. - r = users_groups.remove_user(self.rodsadmin_session, new_username, self.zone_name) - self.assertEqual(r["status_code"], 200) + # 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) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + self.assertEqual(r["data"]["is_member"], True) - # Remove group. - r = users_groups.remove_group(self.rodsadmin_session, 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 = users_groups.stat(self.rodsadmin_session, 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.""" - # 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) - 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 = users_groups.set_user_type(self.rodsadmin_session, new_username, self.zone_name, new_user_type) - self.assertEqual(r["status_code"], 200) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + 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) + 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. - 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) + # 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) + 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) - self.assertEqual(r["status_code"], 200) + # 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) - 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) + # Show that the user type matches the type set by the rodsadmin. + r = users_groups.stat(self.rodsuser_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.assertEqual(stat_info["local_unique_name"], f"{new_username}#{self.zone_name}") + self.assertEqual(stat_info["type"], new_user_type) - # Remove the user. - r = users_groups.remove_user(self.rodsadmin_session, new_username, self.zone_name) - self.assertEqual(r["status_code"], 200) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + 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.""" - r = users_groups.users( - self.rodsadmin_session, - ) + r = users_groups.users(self.rodsadmin_session) self.assertEqual(r["status_code"], 200) result = r["data"] self.assertEqual(result["irods_response"]["status_code"], 0) @@ -2033,24 +1944,26 @@ def test_listing_all_users_in_zone(self): def test_listing_all_groups_in_zone(self): """Test listing all groups in the zone.""" - # Create a new group. - new_group = "test_group" - r = users_groups.create_group(self.rodsadmin_session, new_group) - self.assertEqual(r["status_code"], 200) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) - # Get all groups. - r = users_groups.groups( - self.rodsadmin_session, - ) - 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"]) - # Remove the new group. - r = users_groups.remove_group(self.rodsadmin_session, 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) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Get all groups. + r = users_groups.groups( + self.rodsadmin_session, + ) + 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"]) + + 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.""" @@ -2087,7 +2000,6 @@ def test_modifying_metadata_atomically(self): "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) From 91de50d9f4f98be48d7927efc461f0628734a9ff Mon Sep 17 00:00:00 2001 From: Terrell Russell Date: Sun, 22 Feb 2026 14:03:52 -0500 Subject: [PATCH 15/29] update return type for authenticate() --- irods_http/irods_http.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/irods_http/irods_http.py b/irods_http/irods_http.py index df3ba76..6474ace 100644 --- a/irods_http/irods_http.py +++ b/irods_http/irods_http.py @@ -29,7 +29,7 @@ def __init__(self, url_base: str, token: str): self.token = token -def authenticate(url_base: str, username: str, password: str) -> str: +def authenticate(url_base: str, username: str, password: str) -> IRODSHTTPSession: """ Authenticate using basic authentication credentials. @@ -42,7 +42,7 @@ def authenticate(url_base: str, username: str, password: str) -> str: password: The password for authentication. Must be a string. Returns: - A token string that can be used for subsequent authenticated requests. + An IRODSHTTPSession containing a token string that can be used for subsequent authenticated requests. Raises: TypeError: If username or password are not strings. From 53fc6c21b977660433fd8b914bf784b278b903fd Mon Sep 17 00:00:00 2001 From: Terrell Russell Date: Mon, 23 Feb 2026 13:28:27 -0500 Subject: [PATCH 16/29] squash - triggers a requests.post() error --- test/test_endpoint_operations.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/test_endpoint_operations.py b/test/test_endpoint_operations.py index b8a754c..02895fc 100644 --- a/test/test_endpoint_operations.py +++ b/test/test_endpoint_operations.py @@ -1796,6 +1796,10 @@ def test_set_to_bad_type(self): ValueError, users_groups.set_user_type, self.rodsadmin_session, "baduser", self.zone_name, user_type="bad" ) + def test_bad_connection(self): + """Test authenticate with a bad hostname.""" + self.assertRaises(RuntimeError, authenticate, "example.org", "bad", "bad") + def test_empty_username(self): """Test authenticate with an empty username.""" self.assertRaises(ValueError, authenticate, self.url_base, "", "nope") From f0bd54de855550196a71361f116b0c95afcae42d Mon Sep 17 00:00:00 2001 From: Terrell Russell Date: Mon, 23 Feb 2026 22:31:24 -0500 Subject: [PATCH 17/29] write supports ticket --- irods_http/data_objects.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/irods_http/data_objects.py b/irods_http/data_objects.py index c82842e..f55e0ad 100644 --- a/irods_http/data_objects.py +++ b/irods_http/data_objects.py @@ -505,6 +505,7 @@ def write( append: int = 0, parallel_write_handle: str = "", stream_index: int = -1, + ticket: str = "", ) -> dict: """ Write bytes to a data object. @@ -519,6 +520,7 @@ def write( 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. @@ -538,6 +540,7 @@ def write( common.validate_0_or_1(append) common.validate_instance(parallel_write_handle, str) common.validate_gte_minus1(stream_index) + common.validate_instance(ticket, str) headers = { "Authorization": "Bearer " + session.token, @@ -563,6 +566,9 @@ def write( if stream_index != -1: data["stream-index"] = stream_index + if ticket != "": + data["ticket"] = ticket + r = requests.post(session.url_base + "/data-objects", headers=headers, data=data) # noqa: S113 return common.process_response(r) From 40a54e295ccd2422c5a441cecdd01b12c417edaa Mon Sep 17 00:00:00 2001 From: Terrell Russell Date: Mon, 23 Feb 2026 22:31:51 -0500 Subject: [PATCH 18/29] read, small write, large write with tickets --- test/test_endpoint_operations.py | 132 ++++++++++++++++++++++++++++++- 1 file changed, 131 insertions(+), 1 deletion(-) diff --git a/test/test_endpoint_operations.py b/test/test_endpoint_operations.py index 02895fc..f5367fc 100644 --- a/test/test_endpoint_operations.py +++ b/test/test_endpoint_operations.py @@ -114,6 +114,17 @@ def setup_class(cls, opts): 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.") @@ -130,6 +141,7 @@ def tear_down_class(cls): return 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 @@ -948,6 +960,124 @@ def test_common_operations(self): # Remove the resource 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) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Create a ticket for read + r = tickets.create( + self.rodsadmin_session, + f, + "read", + ) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + ticket_string = r["data"]["ticket"] + self.assertGreater(len(ticket_string), 0) + + # 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, + ) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + 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) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + 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, + ) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + 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, 3, ticket=ticket_string) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + 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() + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + # Close parallel write + r = data_objects.parallel_write_shutdown(self.anonymous_session, handle) + self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + + 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" @@ -1661,7 +1791,7 @@ def test_create_execute_remove_specific_query(self): # Execute as rodsuser r = queries.execute_specific_query(self.rodsuser_session, name=name) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) - self.assertEqual(r["data"]["rows"][0][0], "3") + self.assertEqual(r["data"]["rows"][0][0], "4") finally: # Remove as rodsadmin From 54dec67beb483875164c674ca3ebb9883607a6a7 Mon Sep 17 00:00:00 2001 From: Terrell Russell Date: Mon, 23 Feb 2026 22:57:54 -0500 Subject: [PATCH 19/29] squash with README update --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index eaa2060..974c6e1 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ password = "" session = irods_http.authenticate(url_base, username, password) # Use the session for all other operations -response = session.collections.create('//home//new_collection') +response = irods_http.collections.create(session, '//home//new_collection') # Check the resopnse for errors if response['status_code'] != 200: From 084ef79e0545df45539ce39bb026468858209cd1 Mon Sep 17 00:00:00 2001 From: Terrell Russell Date: Mon, 23 Feb 2026 23:01:25 -0500 Subject: [PATCH 20/29] squash - updated todo --- test/test_endpoint_operations.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/test_endpoint_operations.py b/test/test_endpoint_operations.py index f5367fc..957868b 100644 --- a/test/test_endpoint_operations.py +++ b/test/test_endpoint_operations.py @@ -1576,8 +1576,7 @@ def test_add_child_context(self): # Confirm r = resources.stat(self.rodsadmin_session, resc) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) - # Uncomment once parent_context is available - # -- https://github.com/irods/irods_client_http_api/issues/473 + # TODO(irods_client_http_api#473): uncomment once parent_context is available # self.assertEqual(r["data"]["info"]["parent_context"], "neat") finally: From b5eaba85102e270e0650ff318b8763f2198d84c8 Mon Sep 17 00:00:00 2001 From: Terrell Russell Date: Mon, 23 Feb 2026 23:01:42 -0500 Subject: [PATCH 21/29] squash - removed test/__init__.py --- test/__init__.py | 1 - 1 file changed, 1 deletion(-) delete mode 100644 test/__init__.py diff --git a/test/__init__.py b/test/__init__.py deleted file mode 100644 index cf35192..0000000 --- a/test/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""iRODS HTTP client library test module.""" From 8c2c3a99dea2cbaa1c26d43d52aa420d6ac0cbbc Mon Sep 17 00:00:00 2001 From: Terrell Russell Date: Mon, 23 Feb 2026 23:21:02 -0500 Subject: [PATCH 22/29] squash - use named parameters --- test/test_endpoint_operations.py | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/test/test_endpoint_operations.py b/test/test_endpoint_operations.py index 957868b..79deab9 100644 --- a/test/test_endpoint_operations.py +++ b/test/test_endpoint_operations.py @@ -233,13 +233,17 @@ def test_create(self): ) # test create_intermediates - response = collections.create(self.rodsadmin_session, f"/{self.zone_name}/home/test/folder", 0) + response = collections.create( + self.rodsadmin_session, f"/{self.zone_name}/home/test/folder", create_intermediates=0 + ) self.assertEqual( "{'irods_response': {'status_code': -358000, " "'status_message': 'path does not exist: OBJ_PATH_DOES_NOT_EXIST'}}", str(response["data"]), ) - response = collections.create(self.rodsadmin_session, f"/{self.zone_name}/home/test/folder", 1) + response = collections.create( + self.rodsadmin_session, f"/{self.zone_name}/home/test/folder", create_intermediates=1 + ) self.assertEqual( "{'created': True, 'irods_response': {'status_code': 0}}", str(response["data"]), @@ -310,19 +314,21 @@ def test_remove(self): self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) # test recurse - response = collections.create(self.rodsadmin_session, f"/{self.zone_name}/home/test/folder", 1) + response = collections.create( + self.rodsadmin_session, f"/{self.zone_name}/home/test/folder", create_intermediates=1 + ) self.assertEqual( "{'created': True, 'irods_response': {'status_code': 0}}", str(response["data"]), ) - response = collections.remove(self.rodsadmin_session, f"/{self.zone_name}/home/test", 0) + response = collections.remove(self.rodsadmin_session, f"/{self.zone_name}/home/test", recurse=0) self.assertEqual( "{'irods_response': {'status_code': -79000, " "'status_message': 'cannot remove non-empty collection: " "SYS_COLLECTION_NOT_EMPTY'}}", str(response["data"]), ) - response = collections.remove(self.rodsadmin_session, f"/{self.zone_name}/home/test", 1) + response = collections.remove(self.rodsadmin_session, f"/{self.zone_name}/home/test", recurse=1) self.assertEqual("{'irods_response': {'status_code': 0}}", str(response["data"])) finally: @@ -449,7 +455,7 @@ def test_list(self): # test with recursion response = collections.list_collection( - self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}", 1 + self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}", recurse=1 ) self.assertEqual( f"/{self.zone_name}/home/{self.rodsadmin_username}/albania", @@ -566,7 +572,7 @@ def test_set_permission(self): finally: # remove the collection - collections.remove(self.rodsadmin_session, f"/{self.zone_name}/home/setPerms", 1, 1) + 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): @@ -616,7 +622,7 @@ def test_set_inheritance(self): self.assertFalse(response["data"]["inheritance_enabled"]) # test enabling inheritance - response = collections.set_inheritance(self.rodsadmin_session, testcoll, 1) + response = collections.set_inheritance(self.rodsadmin_session, testcoll, enable=1) self.assertEqual("{'irods_response': {'status_code': 0}}", str(response["data"])) # verify inheritance is enabled @@ -624,7 +630,7 @@ def test_set_inheritance(self): self.assertTrue(response["data"]["inheritance_enabled"]) # test disabling inheritance - response = collections.set_inheritance(self.rodsadmin_session, testcoll, 0) + response = collections.set_inheritance(self.rodsadmin_session, testcoll, enable=0) self.assertEqual("{'irods_response': {'status_code': 0}}", str(response["data"])) # verify inheritance is disabled @@ -632,7 +638,7 @@ def test_set_inheritance(self): self.assertFalse(response["data"]["inheritance_enabled"]) finally: - collections.remove(self.rodsadmin_session, testcoll) + collections.remove(self.rodsadmin_session, testcoll, recurse=1, no_trash=1) # test the modify permissions operation def test_modify_permissions(self): @@ -1349,7 +1355,7 @@ def test_parallel_write(self): f = f"/{self.zone_name}/home/{self.rodsadmin_username}/parallel-write.txt" # Open parallel write - r = data_objects.parallel_write_init(self.rodsadmin_session, f, 3) + r = data_objects.parallel_write_init(self.rodsadmin_session, f, stream_count=3) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) handle = r["data"]["parallel_write_handle"] @@ -1475,7 +1481,7 @@ def test_common_operations(self): 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, 0) + r = data_objects.write(self.rodsadmin_session, "These are the bytes to be written", f, resc_repl, offset=0) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Show there are two replicas under the replication resource hierarchy. @@ -1490,7 +1496,7 @@ def test_common_operations(self): self.assertIn(resc_tuple, [(resc_ufs0, resc_ufs1), (resc_ufs1, resc_ufs0)]) # Trim a replica. - r = data_objects.trim(self.rodsadmin_session, f, 0) + r = data_objects.trim(self.rodsadmin_session, f, replica_number=0) self.assertEqual(r["data"]["irods_response"]["status_code"], 0) # Show there is only one replica under the replication resource hierarchy. From a2be019c8ff20d03d3e4f07b595f63014ac19bd1 Mon Sep 17 00:00:00 2001 From: Terrell Russell Date: Mon, 23 Feb 2026 23:34:14 -0500 Subject: [PATCH 23/29] squash - just check the error code --- test/test_endpoint_operations.py | 33 ++++++++------------------------ 1 file changed, 8 insertions(+), 25 deletions(-) diff --git a/test/test_endpoint_operations.py b/test/test_endpoint_operations.py index 79deab9..b78d9c0 100644 --- a/test/test_endpoint_operations.py +++ b/test/test_endpoint_operations.py @@ -226,28 +226,18 @@ def test_create(self): # test invalid path response = collections.create(self.rodsadmin_session, 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"]), - ) + self.assertEqual(response["data"]["irods_response"]["status_code"], -358000) # OBJ_PATH_DOES_NOT_EXIST # test create_intermediates response = collections.create( self.rodsadmin_session, f"/{self.zone_name}/home/test/folder", create_intermediates=0 ) - self.assertEqual( - "{'irods_response': {'status_code': -358000, " - "'status_message': 'path does not exist: OBJ_PATH_DOES_NOT_EXIST'}}", - str(response["data"]), - ) + self.assertEqual(response["data"]["irods_response"]["status_code"], -358000) # OBJ_PATH_DOES_NOT_EXIST response = collections.create( self.rodsadmin_session, f"/{self.zone_name}/home/test/folder", create_intermediates=1 ) - self.assertEqual( - "{'created': True, 'irods_response': {'status_code': 0}}", - str(response["data"]), - ) + self.assertEqual(response["data"]["irods_response"]["status_code"], 0) + self.assertTrue(response["data"]["created"]) finally: # clean up test collections @@ -317,19 +307,12 @@ def test_remove(self): response = collections.create( self.rodsadmin_session, f"/{self.zone_name}/home/test/folder", create_intermediates=1 ) - self.assertEqual( - "{'created': True, 'irods_response': {'status_code': 0}}", - str(response["data"]), - ) + self.assertEqual(response["data"]["irods_response"]["status_code"], 0) + self.assertTrue(response["data"]["created"]) response = collections.remove(self.rodsadmin_session, f"/{self.zone_name}/home/test", recurse=0) - self.assertEqual( - "{'irods_response': {'status_code': -79000, " - "'status_message': 'cannot remove non-empty collection: " - "SYS_COLLECTION_NOT_EMPTY'}}", - str(response["data"]), - ) + self.assertEqual(response["data"]["irods_response"]["status_code"], -79000) # SYS_COLLECTION_NOT_EMPTY response = collections.remove(self.rodsadmin_session, f"/{self.zone_name}/home/test", recurse=1) - self.assertEqual("{'irods_response': {'status_code': 0}}", str(response["data"])) + self.assertEqual(response["data"]["irods_response"]["status_code"], 0) finally: # clean up test collections From 450df612a8e14619a9d250f13b42cfa3526dcb70 Mon Sep 17 00:00:00 2001 From: Terrell Russell Date: Mon, 23 Feb 2026 23:59:23 -0500 Subject: [PATCH 24/29] squash - construct headers in the session --- irods_http/collections.py | 68 +++----------------- irods_http/data_objects.py | 125 ++++++------------------------------- irods_http/irods_http.py | 9 +++ irods_http/queries.py | 24 ++----- irods_http/resources.py | 56 +++-------------- irods_http/rules.py | 20 +----- irods_http/tickets.py | 14 +---- irods_http/users_groups.py | 84 ++++--------------------- irods_http/zones.py | 35 ++--------- 9 files changed, 72 insertions(+), 363 deletions(-) diff --git a/irods_http/collections.py b/irods_http/collections.py index 9cb8446..6876d4c 100644 --- a/irods_http/collections.py +++ b/irods_http/collections.py @@ -25,18 +25,13 @@ def create(session: IRODSHTTPSession, lpath: str, create_intermediates: int = 0) common.validate_instance(lpath, str) common.validate_0_or_1(create_intermediates) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = { "op": "create", "lpath": lpath, "create-intermediates": create_intermediates, } - r = requests.post(session.url_base + "/collections", headers=headers, data=data) # noqa: S113 + r = requests.post(session.url_base + "/collections", headers=session.post_headers, data=data) # noqa: S113 return common.process_response(r) @@ -59,11 +54,6 @@ def remove(session: IRODSHTTPSession, lpath: str, recurse: int = 0, no_trash: in common.validate_0_or_1(recurse) common.validate_0_or_1(no_trash) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = { "op": "remove", "lpath": lpath, @@ -71,7 +61,7 @@ def remove(session: IRODSHTTPSession, lpath: str, recurse: int = 0, no_trash: in "no-trash": no_trash, } - r = requests.post(session.url_base + "/collections", headers=headers, data=data) # noqa: S113 + r = requests.post(session.url_base + "/collections", headers=session.post_headers, data=data) # noqa: S113 return common.process_response(r) @@ -92,13 +82,9 @@ def stat(session: IRODSHTTPSession, lpath: str, ticket: str = "") -> dict: common.validate_instance(lpath, str) common.validate_instance(ticket, str) - headers = { - "Authorization": "Bearer " + session.token, - } - params = {"op": "stat", "lpath": lpath, "ticket": ticket} - r = requests.get(session.url_base + "/collections", params=params, headers=headers) # noqa: S113 + r = requests.get(session.url_base + "/collections", params=params, headers=session.get_headers) # noqa: S113 return common.process_response(r) @@ -122,13 +108,9 @@ def list_collection(session: IRODSHTTPSession, lpath: str, recurse: int = 0, tic common.validate_0_or_1(recurse) common.validate_instance(ticket, str) - headers = { - "Authorization": "Bearer " + session.token, - } - params = {"op": "list", "lpath": lpath, "recurse": recurse, "ticket": ticket} - r = requests.get(session.url_base + "/collections", params=params, headers=headers) # noqa: S113 + r = requests.get(session.url_base + "/collections", params=params, headers=session.get_headers) # noqa: S113 return common.process_response(r) @@ -164,11 +146,6 @@ def set_permission( raise ValueError("permission must be either 'null', 'read', 'write', or 'own'") common.validate_0_or_1(admin) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = { "op": "set_permission", "lpath": lpath, @@ -177,7 +154,7 @@ def set_permission( "admin": admin, } - r = requests.post(session.url_base + "/collections", headers=headers, data=data) # noqa: S113 + r = requests.post(session.url_base + "/collections", headers=session.post_headers, data=data) # noqa: S113 return common.process_response(r) @@ -200,11 +177,6 @@ def set_inheritance(session: IRODSHTTPSession, lpath: str, enable: int, admin: i common.validate_0_or_1(enable) common.validate_0_or_1(admin) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = { "op": "set_inheritance", "lpath": lpath, @@ -212,7 +184,7 @@ def set_inheritance(session: IRODSHTTPSession, lpath: str, enable: int, admin: i "admin": admin, } - r = requests.post(session.url_base + "/collections", headers=headers, data=data) # noqa: S113 + r = requests.post(session.url_base + "/collections", headers=session.post_headers, data=data) # noqa: S113 return common.process_response(r) @@ -237,11 +209,6 @@ def modify_permissions(session: IRODSHTTPSession, lpath: str, operations: dict, common.validate_instance(operations[0], dict) common.validate_0_or_1(admin) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = { "op": "modify_permissions", "lpath": lpath, @@ -249,7 +216,7 @@ def modify_permissions(session: IRODSHTTPSession, lpath: str, operations: dict, "admin": admin, } - r = requests.post(session.url_base + "/collections", headers=headers, data=data) # noqa: S113 + r = requests.post(session.url_base + "/collections", headers=session.post_headers, data=data) # noqa: S113 return common.process_response(r) @@ -274,11 +241,6 @@ def modify_metadata(session: IRODSHTTPSession, lpath: str, operations: dict, adm common.validate_instance(operations[0], dict) common.validate_0_or_1(admin) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = { "op": "modify_metadata", "lpath": lpath, @@ -286,7 +248,7 @@ def modify_metadata(session: IRODSHTTPSession, lpath: str, operations: dict, adm "admin": admin, } - r = requests.post(session.url_base + "/collections", headers=headers, data=data) # noqa: S113 + r = requests.post(session.url_base + "/collections", headers=session.post_headers, data=data) # noqa: S113 return common.process_response(r) @@ -307,14 +269,9 @@ def rename(session: IRODSHTTPSession, old_lpath: str, new_lpath: str) -> dict: common.validate_instance(old_lpath, str) common.validate_instance(new_lpath, str) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = {"op": "rename", "old-lpath": old_lpath, "new-lpath": new_lpath} - r = requests.post(session.url_base + "/collections", headers=headers, data=data) # noqa: S113 + r = requests.post(session.url_base + "/collections", headers=session.post_headers, data=data) # noqa: S113 return common.process_response(r) @@ -337,11 +294,6 @@ def touch(session: IRODSHTTPSession, lpath: str, seconds_since_epoch: int = -1, common.validate_gte_minus1(seconds_since_epoch) common.validate_instance(reference, str) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = {"op": "touch", "lpath": lpath} if seconds_since_epoch != -1: @@ -350,5 +302,5 @@ def touch(session: IRODSHTTPSession, lpath: str, seconds_since_epoch: int = -1, if reference != "": data["reference"] = reference - r = requests.post(session.url_base + "/collections", headers=headers, data=data) # noqa: S113 + 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/data_objects.py b/irods_http/data_objects.py index f55e0ad..b6b7a6b 100644 --- a/irods_http/data_objects.py +++ b/irods_http/data_objects.py @@ -43,11 +43,6 @@ def touch( common.validate_gte_minus1(seconds_since_epoch) common.validate_instance(reference, str) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = {"op": "touch", "lpath": lpath, "no-create": no_create} if seconds_since_epoch != -1: @@ -62,7 +57,7 @@ def touch( if reference != "": data["reference"] = reference - r = requests.post(session.url_base + "/data-objects", headers=headers, data=data) # noqa: S113 + r = requests.post(session.url_base + "/data-objects", headers=session.post_headers, data=data) # noqa: S113 return common.process_response(r) @@ -87,11 +82,6 @@ def remove(session: IRODSHTTPSession, lpath: str, catalog_only: int = 0, no_tras common.validate_0_or_1(no_trash) common.validate_0_or_1(admin) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = { "op": "remove", "lpath": lpath, @@ -100,7 +90,7 @@ def remove(session: IRODSHTTPSession, lpath: str, catalog_only: int = 0, no_tras "admin": admin, } - r = requests.post(session.url_base + "/data-objects", headers=headers, data=data) # noqa: S113 + r = requests.post(session.url_base + "/data-objects", headers=session.post_headers, data=data) # noqa: S113 return common.process_response(r) @@ -137,11 +127,6 @@ def calculate_checksum( common.validate_0_or_1(all_) common.validate_0_or_1(admin) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = { "op": "calculate_checksum", "lpath": lpath, @@ -156,7 +141,7 @@ def calculate_checksum( if replica_number != -1: data["replica-number"] = replica_number - r = requests.post(session.url_base + "/data-objects", headers=headers, data=data) # noqa: S113 + r = requests.post(session.url_base + "/data-objects", headers=session.post_headers, data=data) # noqa: S113 return common.process_response(r) @@ -190,11 +175,6 @@ def verify_checksum( common.validate_0_or_1(compute_checksums) common.validate_0_or_1(admin) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = { "op": "calculate_checksum", "lpath": lpath, @@ -208,7 +188,7 @@ def verify_checksum( if replica_number != -1: data["replica-number"] = replica_number - r = requests.post(session.url_base + "/data-objects", headers=headers, data=data) # noqa: S113 + r = requests.post(session.url_base + "/data-objects", headers=session.post_headers, data=data) # noqa: S113 return common.process_response(r) @@ -229,13 +209,9 @@ def stat(session: IRODSHTTPSession, lpath: str, ticket: str = "") -> dict: common.validate_instance(lpath, str) common.validate_instance(ticket, str) - headers = { - "Authorization": "Bearer " + session.token, - } - params = {"op": "stat", "lpath": lpath, "ticket": ticket} - r = requests.get(session.url_base + "/data-objects", params=params, headers=headers) # noqa: S113 + r = requests.get(session.url_base + "/data-objects", params=params, headers=session.get_headers) # noqa: S113 return common.process_response(r) @@ -256,14 +232,9 @@ def rename(session: IRODSHTTPSession, old_lpath: str, new_lpath: str) -> dict: common.validate_instance(old_lpath, str) common.validate_instance(new_lpath, str) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = {"op": "rename", "old-lpath": old_lpath, "new-lpath": new_lpath} - r = requests.post(session.url_base + "/data-objects", headers=headers, data=data) # noqa: S113 + r = requests.post(session.url_base + "/data-objects", headers=session.post_headers, data=data) # noqa: S113 return common.process_response(r) @@ -297,11 +268,6 @@ def copy( common.validate_instance(dst_resource, str) common.validate_0_or_1(overwrite) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = { "op": "copy", "src-lpath": src_lpath, @@ -315,7 +281,7 @@ def copy( if dst_resource != "": data["dst-resource"] = dst_resource - r = requests.post(session.url_base + "/data-objects", headers=headers, data=data) # noqa: S113 + r = requests.post(session.url_base + "/data-objects", headers=session.post_headers, data=data) # noqa: S113 return common.process_response(r) @@ -346,11 +312,6 @@ def replicate( common.validate_instance(dst_resource, str) common.validate_0_or_1(admin) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = {"op": "replicate", "lpath": lpath, "admin": admin} if src_resource != "": @@ -359,7 +320,7 @@ def replicate( if dst_resource != "": data["dst-resource"] = dst_resource - r = requests.post(session.url_base + "/data-objects", headers=headers, data=data) # noqa: S113 + r = requests.post(session.url_base + "/data-objects", headers=session.post_headers, data=data) # noqa: S113 return common.process_response(r) @@ -384,11 +345,6 @@ def trim(session: IRODSHTTPSession, lpath: str, replica_number: int, catalog_onl common.validate_0_or_1(catalog_only) common.validate_0_or_1(admin) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = { "op": "trim", "lpath": lpath, @@ -397,7 +353,7 @@ def trim(session: IRODSHTTPSession, lpath: str, replica_number: int, catalog_onl "admin": admin, } - r = requests.post(session.url_base + "/data-objects", headers=headers, data=data) # noqa: S113 + r = requests.post(session.url_base + "/data-objects", headers=session.post_headers, data=data) # noqa: S113 return common.process_response(r) @@ -435,11 +391,6 @@ def register( common.validate_gte_minus1(data_size) common.validate_0_or_1(checksum) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = { "op": "register", "lpath": lpath, @@ -452,7 +403,7 @@ def register( if data_size != -1: data["data-size"] = data_size - r = requests.post(session.url_base + "/data-objects", headers=headers, data=data) # noqa: S113 + r = requests.post(session.url_base + "/data-objects", headers=session.post_headers, data=data) # noqa: S113 return common.process_response(r) @@ -477,11 +428,6 @@ def read(session: IRODSHTTPSession, lpath: str, offset: int = 0, count: int = -1 common.validate_gte_minus1(count) common.validate_instance(ticket, str) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - params = {"op": "read", "lpath": lpath, "offset": offset} if count != -1: @@ -490,7 +436,7 @@ def read(session: IRODSHTTPSession, lpath: str, offset: int = 0, count: int = -1 if ticket != "": params["ticket"] = ticket - r = requests.get(session.url_base + "/data-objects", params=params, headers=headers) # noqa: S113 + 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} @@ -542,11 +488,6 @@ def write( common.validate_gte_minus1(stream_index) common.validate_instance(ticket, str) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = { "op": "write", "offset": offset, @@ -569,7 +510,7 @@ def write( if ticket != "": data["ticket"] = ticket - r = requests.post(session.url_base + "/data-objects", headers=headers, data=data) # noqa: S113 + r = requests.post(session.url_base + "/data-objects", headers=session.post_headers, data=data) # noqa: S113 return common.process_response(r) @@ -603,11 +544,6 @@ def parallel_write_init( common.validate_0_or_1(append) common.validate_instance(ticket, str) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = { "op": "parallel_write_init", "lpath": lpath, @@ -619,7 +555,7 @@ def parallel_write_init( if ticket != "": data["ticket"] = ticket - r = requests.post(session.url_base + "/data-objects", headers=headers, data=data) # noqa: S113 + r = requests.post(session.url_base + "/data-objects", headers=session.post_headers, data=data) # noqa: S113 return common.process_response(r) @@ -638,17 +574,12 @@ def parallel_write_shutdown(session: IRODSHTTPSession, parallel_write_handle: st common.validate_not_none(session.token) common.validate_instance(parallel_write_handle, str) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = { "op": "parallel_write_shutdown", "parallel-write-handle": parallel_write_handle, } - r = requests.post(session.url_base + "/data-objects", headers=headers, data=data) # noqa: S113 + r = requests.post(session.url_base + "/data-objects", headers=session.post_headers, data=data) # noqa: S113 return common.process_response(r) @@ -673,11 +604,6 @@ def modify_metadata(session: IRODSHTTPSession, lpath: str, operations: list, adm common.validate_instance(operations[0], dict) common.validate_0_or_1(admin) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = { "op": "modify_metadata", "lpath": lpath, @@ -685,7 +611,7 @@ def modify_metadata(session: IRODSHTTPSession, lpath: str, operations: list, adm "admin": admin, } - r = requests.post(session.url_base + "/data-objects", headers=headers, data=data) # noqa: S113 + r = requests.post(session.url_base + "/data-objects", headers=session.post_headers, data=data) # noqa: S113 return common.process_response(r) @@ -715,11 +641,6 @@ def set_permission(session: IRODSHTTPSession, lpath: str, entity_name: str, perm raise ValueError("permission must be either 'null', 'read', 'write', or 'own'") common.validate_0_or_1(admin) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = { "op": "set_permission", "lpath": lpath, @@ -728,7 +649,7 @@ def set_permission(session: IRODSHTTPSession, lpath: str, entity_name: str, perm "admin": admin, } - r = requests.post(session.url_base + "/data-objects", headers=headers, data=data) # noqa: S113 + r = requests.post(session.url_base + "/data-objects", headers=session.post_headers, data=data) # noqa: S113 return common.process_response(r) @@ -753,11 +674,6 @@ def modify_permissions(session: IRODSHTTPSession, lpath: str, operations: list, common.validate_instance(operations[0], dict) common.validate_0_or_1(admin) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = { "op": "modify_permissions", "lpath": lpath, @@ -765,7 +681,7 @@ def modify_permissions(session: IRODSHTTPSession, lpath: str, operations: list, "admin": admin, } - r = requests.post(session.url_base + "/data-objects", headers=headers, data=data) # noqa: S113 + r = requests.post(session.url_base + "/data-objects", headers=session.post_headers, data=data) # noqa: S113 return common.process_response(r) @@ -850,11 +766,6 @@ def modify_replica( common.validate_instance(new_data_type_name, str) common.validate_gte_minus1(new_data_version) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = {"op": "modify_replica", "lpath": lpath} if resource_hierarchy != "": @@ -925,5 +836,5 @@ def modify_replica( if no_params: raise RuntimeError("At least one new data parameter must be given.") - r = requests.post(session.url_base + "/data-objects", headers=headers, data=data) # noqa: S113 + 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 index 6474ace..3517aca 100644 --- a/irods_http/irods_http.py +++ b/irods_http/irods_http.py @@ -28,6 +28,15 @@ def __init__(self, url_base: str, token: str): 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: """ diff --git a/irods_http/queries.py b/irods_http/queries.py index 8e86adf..0dab7c8 100644 --- a/irods_http/queries.py +++ b/irods_http/queries.py @@ -52,10 +52,6 @@ def execute_genquery( common.validate_0_or_1(sql_only) common.validate_instance(zone, str) - headers = { - "Authorization": "Bearer " + session.token, - } - params = { "op": "execute_genquery", "query": query, @@ -75,7 +71,7 @@ def execute_genquery( else: params["sql-only"] = sql_only - r = requests.get(session.url_base + "/query", headers=headers, params=params) # noqa: S113 + r = requests.get(session.url_base + "/query", headers=session.get_headers, params=params) # noqa: S113 return common.process_response(r) @@ -108,10 +104,6 @@ def execute_specific_query( common.validate_gte_zero(offset) common.validate_gte_minus1(count) - headers = { - "Authorization": "Bearer " + session.token, - } - params = { "op": "execute_specific_query", "name": name, @@ -125,7 +117,7 @@ def execute_specific_query( if args != "": params["args"] = args - r = requests.get(session.url_base + "/query", headers=headers, params=params) # noqa: S113 + r = requests.get(session.url_base + "/query", headers=session.get_headers, params=params) # noqa: S113 return common.process_response(r) @@ -145,13 +137,9 @@ def add_specific_query(session: IRODSHTTPSession, name: str, sql: str): common.validate_instance(name, str) common.validate_instance(sql, str) - headers = { - "Authorization": "Bearer " + session.token, - } - data = {"op": "add_specific_query", "name": name, "sql": sql} - r = requests.post(session.url_base + "/query", headers=headers, data=data) # noqa: S113 + r = requests.post(session.url_base + "/query", headers=session.get_headers, data=data) # noqa: S113 return common.process_response(r) @@ -169,11 +157,7 @@ def remove_specific_query(session: IRODSHTTPSession, name: str): """ common.validate_instance(name, str) - headers = { - "Authorization": "Bearer " + session.token, - } - data = {"op": "remove_specific_query", "name": name} - r = requests.post(session.url_base + "/query", headers=headers, data=data) # noqa: S113 + 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 index 429b004..d23862c 100644 --- a/irods_http/resources.py +++ b/irods_http/resources.py @@ -32,11 +32,6 @@ def create(session: IRODSHTTPSession, name: str, type_: str, host: str, vault_pa common.validate_instance(vault_path, str) common.validate_instance(context, str) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = {"op": "create", "name": name, "type": type_} if host != "": @@ -48,7 +43,7 @@ def create(session: IRODSHTTPSession, name: str, type_: str, host: str, vault_pa if context != "": data["context"] = context - r = requests.post(session.url_base + "/resources", headers=headers, data=data) # noqa: S113 + r = requests.post(session.url_base + "/resources", headers=session.post_headers, data=data) # noqa: S113 return common.process_response(r) @@ -66,14 +61,9 @@ def remove(session: IRODSHTTPSession, name: str): """ common.validate_instance(name, str) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = {"op": "remove", "name": name} - r = requests.post(session.url_base + "/resources", headers=headers, data=data) # noqa: S113 + r = requests.post(session.url_base + "/resources", headers=session.post_headers, data=data) # noqa: S113 return common.process_response(r) @@ -116,14 +106,9 @@ def modify(session: IRODSHTTPSession, name: str, property_: str, value: str): if (property_ == "status") and (value not in ["up", "down"]): raise ValueError("status must be either 'up' or 'down'") - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = {"op": "modify", "name": name, "property": property_, "value": value} - r = requests.post(session.url_base + "/resources", headers=headers, data=data) # noqa: S113 + r = requests.post(session.url_base + "/resources", headers=session.post_headers, data=data) # noqa: S113 return common.process_response(r) @@ -145,17 +130,12 @@ def add_child(session: IRODSHTTPSession, parent_name: str, child_name: str, cont common.validate_instance(child_name, str) common.validate_instance(context, str) - headers = { - "Authorization": "Bearer " + session.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(session.url_base + "/resources", headers=headers, data=data) # noqa: S113 + r = requests.post(session.url_base + "/resources", headers=session.post_headers, data=data) # noqa: S113 return common.process_response(r) @@ -175,18 +155,13 @@ def remove_child(session: IRODSHTTPSession, parent_name: str, child_name: str): common.validate_instance(parent_name, str) common.validate_instance(child_name, str) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = { "op": "remove_child", "parent-name": parent_name, "child-name": child_name, } - r = requests.post(session.url_base + "/resources", headers=headers, data=data) # noqa: S113 + r = requests.post(session.url_base + "/resources", headers=session.post_headers, data=data) # noqa: S113 return common.process_response(r) @@ -204,14 +179,9 @@ def rebalance(session: IRODSHTTPSession, name: str): """ common.validate_instance(name, str) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = {"op": "rebalance", "name": name} - r = requests.post(session.url_base + "/resources", headers=headers, data=data) # noqa: S113 + r = requests.post(session.url_base + "/resources", headers=session.post_headers, data=data) # noqa: S113 return common.process_response(r) @@ -229,14 +199,9 @@ def stat(session: IRODSHTTPSession, name: str): """ common.validate_instance(name, str) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - params = {"op": "stat", "name": name} - r = requests.get(session.url_base + "/resources", headers=headers, params=params) # noqa: S113 + r = requests.get(session.url_base + "/resources", headers=session.get_headers, params=params) # noqa: S113 return common.process_response(r) @@ -260,11 +225,6 @@ def modify_metadata(session: IRODSHTTPSession, name: str, operations: dict, admi common.validate_instance(operations[0], dict) common.validate_0_or_1(admin) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = { "op": "modify_metadata", "name": name, @@ -272,5 +232,5 @@ def modify_metadata(session: IRODSHTTPSession, name: str, operations: dict, admi "admin": admin, } - r = requests.post(session.url_base + "/resources", headers=headers, data=data) # noqa: S113 + 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 index f28d1a2..cf66ebc 100644 --- a/irods_http/rules.py +++ b/irods_http/rules.py @@ -17,13 +17,9 @@ def list_rule_engines(session: IRODSHTTPSession): 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, - } - params = {"op": "list_rule_engines"} - r = requests.get(session.url_base + "/rules", params=params, headers=headers) # noqa: S113 + r = requests.get(session.url_base + "/rules", params=params, headers=session.get_headers) # noqa: S113 return common.process_response(r) @@ -43,17 +39,12 @@ def execute(session: IRODSHTTPSession, rule_text: str, rep_instance: str = ""): common.validate_instance(rule_text, str) common.validate_instance(rep_instance, str) - headers = { - "Authorization": "Bearer " + session.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(session.url_base + "/rules", headers=headers, data=data) # noqa: S113 + r = requests.post(session.url_base + "/rules", headers=session.post_headers, data=data) # noqa: S113 return common.process_response(r) @@ -71,12 +62,7 @@ def remove_delay_rule(session: IRODSHTTPSession, rule_id: int): """ common.validate_gte_zero(rule_id) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = {"op": "remove_delay_rule", "rule-id": rule_id} - r = requests.post(session.url_base + "/rules", headers=headers, data=data) # noqa: S113 + 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 index abfe43f..46bc989 100644 --- a/irods_http/tickets.py +++ b/irods_http/tickets.py @@ -52,11 +52,6 @@ def create( common.validate_instance(groups, str) common.validate_instance(hosts, str) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = {"op": "create", "lpath": lpath, "type": type_} if use_count != -1: @@ -74,7 +69,7 @@ def create( if hosts != "": data["hosts"] = hosts - r = requests.post(session.url_base + "/tickets", headers=headers, data=data) # noqa: S113 + r = requests.post(session.url_base + "/tickets", headers=session.post_headers, data=data) # noqa: S113 return common.process_response(r) @@ -92,12 +87,7 @@ def remove(session: IRODSHTTPSession, name: str): """ common.validate_instance(name, str) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = {"op": "remove", "name": name} - r = requests.post(session.url_base + "/tickets", headers=headers, data=data) # noqa: S113 + 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 index b4394f3..b45c7ce 100644 --- a/irods_http/users_groups.py +++ b/irods_http/users_groups.py @@ -31,14 +31,9 @@ def create_user(session: IRODSHTTPSession, name: str, zone: str, 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 " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = {"op": "create_user", "name": name, "zone": zone, "user-type": user_type} - r = requests.post(session.url_base + "/users-groups", headers=headers, data=data) # noqa: S113 + r = requests.post(session.url_base + "/users-groups", headers=session.post_headers, data=data) # noqa: S113 return common.process_response(r) @@ -58,14 +53,9 @@ def remove_user(session: IRODSHTTPSession, name: str, zone: str): common.validate_instance(name, str) common.validate_instance(zone, str) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = {"op": "remove_user", "name": name, "zone": zone} - r = requests.post(session.url_base + "/users-groups", headers=headers, data=data) # noqa: S113 + r = requests.post(session.url_base + "/users-groups", headers=session.post_headers, data=data) # noqa: S113 return common.process_response(r) @@ -87,11 +77,6 @@ def set_password(session: IRODSHTTPSession, name: str, zone: str, new_password: common.validate_instance(zone, str) common.validate_instance(new_password, str) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = { "op": "set_password", "name": name, @@ -99,7 +84,7 @@ def set_password(session: IRODSHTTPSession, name: str, zone: str, new_password: "new-password": new_password, } - r = requests.post(session.url_base + "/users-groups", headers=headers, data=data) # noqa: S113 + r = requests.post(session.url_base + "/users-groups", headers=session.post_headers, data=data) # noqa: S113 return common.process_response(r) @@ -126,11 +111,6 @@ def set_user_type(session: IRODSHTTPSession, name: str, zone: str, user_type: st if user_type not in ["rodsuser", "groupadmin", "rodsadmin"]: raise ValueError("user_type must be set to rodsuser, groupadmin, or rodsadmin.") - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = { "op": "set_user_type", "name": name, @@ -138,7 +118,7 @@ def set_user_type(session: IRODSHTTPSession, name: str, zone: str, user_type: st "new-user-type": user_type, } - r = requests.post(session.url_base + "/users-groups", headers=headers, data=data) # noqa: S113 + r = requests.post(session.url_base + "/users-groups", headers=session.post_headers, data=data) # noqa: S113 return common.process_response(r) @@ -156,14 +136,9 @@ def create_group(session: IRODSHTTPSession, name: str): """ common.validate_instance(name, str) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = {"op": "create_group", "name": name} - r = requests.post(session.url_base + "/users-groups", headers=headers, data=data) # noqa: S113 + r = requests.post(session.url_base + "/users-groups", headers=session.post_headers, data=data) # noqa: S113 return common.process_response(r) @@ -181,14 +156,9 @@ def remove_group(session: IRODSHTTPSession, name: str): """ common.validate_instance(name, str) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = {"op": "remove_group", "name": name} - r = requests.post(session.url_base + "/users-groups", headers=headers, data=data) # noqa: S113 + r = requests.post(session.url_base + "/users-groups", headers=session.post_headers, data=data) # noqa: S113 return common.process_response(r) @@ -210,14 +180,9 @@ def add_to_group(session: IRODSHTTPSession, user: str, zone: str, group: str = " common.validate_instance(zone, str) common.validate_instance(group, str) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = {"op": "add_to_group", "user": user, "zone": zone, "group": group} - r = requests.post(session.url_base + "/users-groups", headers=headers, data=data) # noqa: S113 + r = requests.post(session.url_base + "/users-groups", headers=session.post_headers, data=data) # noqa: S113 return common.process_response(r) @@ -239,14 +204,9 @@ def remove_from_group(session: IRODSHTTPSession, user: str, zone: str, group: st common.validate_instance(zone, str) common.validate_instance(group, str) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = {"op": "remove_from_group", "user": user, "zone": zone, "group": group} - r = requests.post(session.url_base + "/users-groups", headers=headers, data=data) # noqa: S113 + r = requests.post(session.url_base + "/users-groups", headers=session.post_headers, data=data) # noqa: S113 return common.process_response(r) @@ -261,11 +221,9 @@ def users(session: IRODSHTTPSession): 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} - params = {"op": "users"} - r = requests.get(session.url_base + "/users-groups", headers=headers, params=params) # noqa: S113 + r = requests.get(session.url_base + "/users-groups", headers=session.get_headers, params=params) # noqa: S113 return common.process_response(r) @@ -280,13 +238,9 @@ def groups(session: IRODSHTTPSession): 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, - } - params = {"op": "groups"} - r = requests.get(session.url_base + "/users-groups", headers=headers, params=params) # noqa: S113 + r = requests.get(session.url_base + "/users-groups", headers=session.get_headers, params=params) # noqa: S113 return common.process_response(r) @@ -308,11 +262,6 @@ def is_member_of_group(session: IRODSHTTPSession, group: str, user: str, zone: s common.validate_instance(user, str) common.validate_instance(zone, str) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - params = { "op": "is_member_of_group", "group": group, @@ -320,7 +269,7 @@ def is_member_of_group(session: IRODSHTTPSession, group: str, user: str, zone: s "zone": zone, } - r = requests.get(session.url_base + "/users-groups", headers=headers, params=params) # noqa: S113 + r = requests.get(session.url_base + "/users-groups", headers=session.post_headers, params=params) # noqa: S113 return common.process_response(r) @@ -340,14 +289,12 @@ def stat(session: IRODSHTTPSession, name: str, zone: str = ""): common.validate_instance(name, str) common.validate_instance(zone, str) - headers = {"Authorization": "Bearer " + session.token} - params = {"op": "stat", "name": name} if zone != "": params["zone"] = zone - r = requests.get(session.url_base + "/users-groups", headers=headers, params=params) # noqa: S113 + r = requests.get(session.url_base + "/users-groups", headers=session.get_headers, params=params) # noqa: S113 return common.process_response(r) @@ -368,16 +315,11 @@ def modify_metadata(session: IRODSHTTPSession, name: str, operations: list): common.validate_instance(operations, list) common.validate_instance(operations[0], dict) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = { "op": "modify_metadata", "name": name, "operations": json.dumps(operations), } - r = requests.post(session.url_base + "/users-groups", headers=headers, data=data) # noqa: S113 + 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 index 75131d8..c0fb9fb 100644 --- a/irods_http/zones.py +++ b/irods_http/zones.py @@ -24,11 +24,6 @@ def add(session: IRODSHTTPSession, name: str, connection_info: str = "", comment common.validate_instance(connection_info, str) common.validate_instance(comment, str) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = {"op": "add", "name": name} if connection_info != "": @@ -36,7 +31,7 @@ def add(session: IRODSHTTPSession, name: str, connection_info: str = "", comment if comment != "": data["comment"] = comment - r = requests.post(session.url_base + "/zones", headers=headers, data=data) # noqa: S113 + r = requests.post(session.url_base + "/zones", headers=session.post_headers, data=data) # noqa: S113 return common.process_response(r) @@ -54,14 +49,9 @@ def remove(session: IRODSHTTPSession, name: str): """ common.validate_instance(name, str) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = {"op": "remove", "name": name} - r = requests.post(session.url_base + "/zones", headers=headers, data=data) # noqa: S113 + r = requests.post(session.url_base + "/zones", headers=session.post_headers, data=data) # noqa: S113 return common.process_response(r) @@ -84,14 +74,9 @@ def modify(session: IRODSHTTPSession, name: str, property_: str, value: str): common.validate_instance(property_, str) common.validate_instance(value, str) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - data = {"op": "modify", "name": name, "property": property_, "value": value} - r = requests.post(session.url_base + "/zones", headers=headers, data=data) # noqa: S113 + r = requests.post(session.url_base + "/zones", headers=session.post_headers, data=data) # noqa: S113 return common.process_response(r) @@ -106,14 +91,9 @@ def report(session: IRODSHTTPSession): 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, - "Content-Type": "application/x-www-form-urlencoded", - } - params = {"op": "report"} - r = requests.get(session.url_base + "/zones", headers=headers, params=params) # noqa: S113 + r = requests.get(session.url_base + "/zones", headers=session.get_headers, params=params) # noqa: S113 return common.process_response(r) @@ -131,12 +111,7 @@ def stat(session: IRODSHTTPSession, name: str): """ common.validate_instance(name, str) - headers = { - "Authorization": "Bearer " + session.token, - "Content-Type": "application/x-www-form-urlencoded", - } - params = {"op": "stat", "name": name} - r = requests.get(session.url_base + "/zones", headers=headers, params=params) # noqa: S113 + r = requests.get(session.url_base + "/zones", headers=session.get_headers, params=params) # noqa: S113 return common.process_response(r) From a1314d43b5babf520407689fc0f4d845f778d210 Mon Sep 17 00:00:00 2001 From: Terrell Russell Date: Tue, 24 Feb 2026 00:16:23 -0500 Subject: [PATCH 25/29] use list, careful to avoid shadowing --- irods_http/collections.py | 7 ++++--- test/test_endpoint_operations.py | 26 +++++++++----------------- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/irods_http/collections.py b/irods_http/collections.py index 6876d4c..73178e5 100644 --- a/irods_http/collections.py +++ b/irods_http/collections.py @@ -1,5 +1,6 @@ """Collection operations for iRODS HTTP API.""" +import builtins import json import requests @@ -88,7 +89,7 @@ def stat(session: IRODSHTTPSession, lpath: str, ticket: str = "") -> dict: return common.process_response(r) -def list_collection(session: IRODSHTTPSession, lpath: str, recurse: int = 0, ticket: str = "") -> dict: +def list(session: IRODSHTTPSession, lpath: str, recurse: int = 0, ticket: str = "") -> dict: # noqa: A001 """ Show the contents of a collection. @@ -205,7 +206,7 @@ def modify_permissions(session: IRODSHTTPSession, lpath: str, operations: dict, """ common.validate_not_none(session.token) common.validate_instance(lpath, str) - common.validate_instance(operations, list) + common.validate_instance(operations, builtins.list) common.validate_instance(operations[0], dict) common.validate_0_or_1(admin) @@ -237,7 +238,7 @@ def modify_metadata(session: IRODSHTTPSession, lpath: str, operations: dict, adm """ common.validate_not_none(session.token) common.validate_instance(lpath, str) - common.validate_instance(operations, list) + common.validate_instance(operations, builtins.list) common.validate_instance(operations[0], dict) common.validate_0_or_1(admin) diff --git a/test/test_endpoint_operations.py b/test/test_endpoint_operations.py index b78d9c0..de831f3 100644 --- a/test/test_endpoint_operations.py +++ b/test/test_endpoint_operations.py @@ -353,10 +353,10 @@ def test_list(self): """Test collection list operation to enumerate contents.""" try: # test param checking - self.assertRaises(TypeError, collections.list_collection, self.rodsadmin_session, 0, "ticket") + self.assertRaises(TypeError, collections.list, self.rodsadmin_session, 0, "ticket") self.assertRaises( TypeError, - collections.list_collection, + collections.list, self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}", "0", @@ -364,7 +364,7 @@ def test_list(self): ) self.assertRaises( ValueError, - collections.list_collection, + collections.list, self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}", 5, @@ -372,7 +372,7 @@ def test_list(self): ) self.assertRaises( TypeError, - collections.list_collection, + collections.list, self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}", 0, @@ -380,16 +380,12 @@ def test_list(self): ) # test empty collection - response = collections.list_collection( - self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}" - ) + response = collections.list(self.rodsadmin_session, 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") - response = collections.list_collection( - self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}" - ) + response = 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(response["data"]["entries"][0]), @@ -398,9 +394,7 @@ def test_list(self): # 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") - response = collections.list_collection( - self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}" - ) + response = 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(response["data"]["entries"][0]), @@ -419,9 +413,7 @@ def test_list(self): self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}/croatia/zagreb", ) - response = collections.list_collection( - self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}" - ) + response = 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(response["data"]["entries"][0]), @@ -437,7 +429,7 @@ def test_list(self): self.assertEqual(len(response["data"]["entries"]), 3) # test with recursion - response = collections.list_collection( + response = collections.list( self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}", recurse=1 ) self.assertEqual( From b2a8aa49c6bad209929e23a59516a58d5daf18f0 Mon Sep 17 00:00:00 2001 From: Terrell Russell Date: Tue, 24 Feb 2026 14:46:01 -0500 Subject: [PATCH 26/29] check HTTP and iRODS status codes consistently --- irods_http/common.py | 14 + test/test_endpoint_operations.py | 488 ++++++++++++++----------------- 2 files changed, 240 insertions(+), 262 deletions(-) diff --git a/irods_http/common.py b/irods_http/common.py index e239d1a..dfefd11 100644 --- a/irods_http/common.py +++ b/irods_http/common.py @@ -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/test/test_endpoint_operations.py b/test/test_endpoint_operations.py index de831f3..5332c5f 100644 --- a/test/test_endpoint_operations.py +++ b/test/test_endpoint_operations.py @@ -215,29 +215,29 @@ def test_create(self): ) # test creating new collection - response = collections.create(self.rodsadmin_session, f"/{self.zone_name}/home/new") - self.assertTrue(response["data"]["created"]) - self.assertEqual(response["data"]["irods_response"]["status_code"], 0) + r = collections.create(self.rodsadmin_session, f"/{self.zone_name}/home/new") + common.assert_success(self, r) + self.assertTrue(r["data"]["created"]) # test creating existing collection - response = collections.create(self.rodsadmin_session, f"/{self.zone_name}/home/new") - self.assertFalse(response["data"]["created"]) - self.assertEqual(response["data"]["irods_response"]["status_code"], 0) + r = collections.create(self.rodsadmin_session, f"/{self.zone_name}/home/new") + common.assert_success(self, r) + self.assertFalse(r["data"]["created"]) # test invalid path - response = collections.create(self.rodsadmin_session, f"{self.zone_name}/home/new") - self.assertEqual(response["data"]["irods_response"]["status_code"], -358000) # OBJ_PATH_DOES_NOT_EXIST + 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 = collections.create( + r = collections.create( self.rodsadmin_session, f"/{self.zone_name}/home/test/folder", create_intermediates=0 ) - self.assertEqual(response["data"]["irods_response"]["status_code"], -358000) # OBJ_PATH_DOES_NOT_EXIST - response = collections.create( + 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 ) - self.assertEqual(response["data"]["irods_response"]["status_code"], 0) - self.assertTrue(response["data"]["created"]) + common.assert_success(self, r) + self.assertTrue(r["data"]["created"]) finally: # clean up test collections @@ -285,34 +285,32 @@ def test_remove(self): ) # test removing collection - response = collections.create(self.rodsadmin_session, f"/{self.zone_name}/home/new") - self.assertEqual( - "{'created': True, 'irods_response': {'status_code': 0}}", - str(response["data"]), - ) - response = collections.remove(self.rodsadmin_session, f"/{self.zone_name}/home/new") - self.assertEqual("{'irods_response': {'status_code': 0}}", str(response["data"])) + 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 - response = collections.stat(self.rodsadmin_session, f"/{self.zone_name}/home/tensaitekinaaidorusama") - self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) - response = collections.stat(self.rodsadmin_session, f"/{self.zone_name}/home/aremonainainaikoremonainainai") - self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) - response = collections.stat(self.rodsadmin_session, f"/{self.zone_name}/home/binglebangledingledangle") - self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) - response = collections.stat(self.rodsadmin_session, f"{self.zone_name}/home/{self.rodsadmin_username}") - self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) + 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 - response = collections.create( + r = collections.create( self.rodsadmin_session, f"/{self.zone_name}/home/test/folder", create_intermediates=1 ) - self.assertEqual(response["data"]["irods_response"]["status_code"], 0) - self.assertTrue(response["data"]["created"]) - response = collections.remove(self.rodsadmin_session, f"/{self.zone_name}/home/test", recurse=0) - self.assertEqual(response["data"]["irods_response"]["status_code"], -79000) # SYS_COLLECTION_NOT_EMPTY - response = collections.remove(self.rodsadmin_session, f"/{self.zone_name}/home/test", recurse=1) - self.assertEqual(response["data"]["irods_response"]["status_code"], 0) + 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) finally: # clean up test collections @@ -335,14 +333,14 @@ def test_stat(self): ) # test invalid paths - response = collections.stat(self.rodsadmin_session, f"/{self.zone_name}/home/new") - self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) - response = collections.stat(self.rodsadmin_session, f"{self.zone_name}/home/new") - self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) + 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 valid path - response = collections.stat(self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}") - self.assertTrue(response["data"]["permissions"]) + r = collections.stat(self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}") + self.assertTrue(r["data"]["permissions"]) finally: # clean up test collections @@ -380,32 +378,32 @@ def test_list(self): ) # test empty collection - response = collections.list(self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}") - self.assertEqual("None", str(response["data"]["entries"])) + r = collections.list(self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}") + self.assertEqual("None", str(r["data"]["entries"])) # test collection with one item collections.create(self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}/bosnia") - response = collections.list(self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}") + 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(response["data"]["entries"][0]), + str(r["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") - response = collections.list(self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}") + 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(response["data"]["entries"][0]), + str(r["data"]["entries"][0]), ) self.assertEqual( f"/{self.zone_name}/home/{self.rodsadmin_username}/bosnia", - str(response["data"]["entries"][1]), + str(r["data"]["entries"][1]), ) self.assertEqual( f"/{self.zone_name}/home/{self.rodsadmin_username}/croatia", - str(response["data"]["entries"][2]), + str(r["data"]["entries"][2]), ) # test without recursion @@ -413,40 +411,38 @@ def test_list(self): self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}/croatia/zagreb", ) - response = collections.list(self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}") + 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(response["data"]["entries"][0]), + str(r["data"]["entries"][0]), ) self.assertEqual( f"/{self.zone_name}/home/{self.rodsadmin_username}/bosnia", - str(response["data"]["entries"][1]), + str(r["data"]["entries"][1]), ) self.assertEqual( f"/{self.zone_name}/home/{self.rodsadmin_username}/croatia", - str(response["data"]["entries"][2]), + str(r["data"]["entries"][2]), ) - self.assertEqual(len(response["data"]["entries"]), 3) + self.assertEqual(len(r["data"]["entries"]), 3) # test with recursion - response = collections.list( - self.rodsadmin_session, f"/{self.zone_name}/home/{self.rodsadmin_username}", recurse=1 - ) + 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(response["data"]["entries"][0]), + str(r["data"]["entries"][0]), ) self.assertEqual( f"/{self.zone_name}/home/{self.rodsadmin_username}/bosnia", - str(response["data"]["entries"][1]), + str(r["data"]["entries"][1]), ) self.assertEqual( f"/{self.zone_name}/home/{self.rodsadmin_username}/croatia", - str(response["data"]["entries"][2]), + str(r["data"]["entries"][2]), ) self.assertEqual( f"/{self.zone_name}/home/{self.rodsadmin_username}/croatia/zagreb", - str(response["data"]["entries"][3]), + str(r["data"]["entries"][3]), ) finally: @@ -512,38 +508,38 @@ def test_set_permission(self): try: # create new collection - response = collections.create(self.rodsadmin_session, f"/{self.zone_name}/home/setPerms") - self.assertEqual(response["data"]["irods_response"]["status_code"], 0) + r = collections.create(self.rodsadmin_session, f"/{self.zone_name}/home/setPerms") + common.assert_success(self, r) # test no permission - response = collections.stat(self.rodsuser_session, f"/{self.zone_name}/home/setPerms") - self.assertEqual(response["data"]["irods_response"]["status_code"], -170000) + r = collections.stat(self.rodsuser_session, f"/{self.zone_name}/home/setPerms") + self.assertEqual(r["data"]["irods_response"]["status_code"], -170000) # test set permission - response = collections.set_permission( + r = collections.set_permission( self.rodsadmin_session, f"/{self.zone_name}/home/setPerms", self.rodsuser_username, "read", ) - self.assertEqual("{'irods_response': {'status_code': 0}}", str(response["data"])) + common.assert_success(self, r) # test with permission - response = collections.stat(self.rodsadmin_session, f"/{self.zone_name}/home/setPerms") - self.assertTrue(response["data"]["permissions"]) + r = collections.stat(self.rodsadmin_session, f"/{self.zone_name}/home/setPerms") + self.assertTrue(r["data"]["permissions"]) # test set permission null - response = collections.set_permission( + r = collections.set_permission( self.rodsadmin_session, f"/{self.zone_name}/home/setPerms", self.rodsuser_username, "null", ) - self.assertEqual("{'irods_response': {'status_code': 0}}", str(response["data"])) + common.assert_success(self, r) # test no permission - response = collections.stat(self.rodsuser_session, f"/{self.zone_name}/home/setPerms") - self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) + r = collections.stat(self.rodsuser_session, f"/{self.zone_name}/home/setPerms") + self.assertEqual("{'irods_response': {'status_code': -170000}}", str(r["data"])) finally: # remove the collection @@ -593,24 +589,24 @@ def test_set_inheritance(self): ) # control - response = collections.stat(self.rodsadmin_session, testcoll) - self.assertFalse(response["data"]["inheritance_enabled"]) + r = collections.stat(self.rodsadmin_session, testcoll) + self.assertFalse(r["data"]["inheritance_enabled"]) # test enabling inheritance - response = collections.set_inheritance(self.rodsadmin_session, testcoll, enable=1) - self.assertEqual("{'irods_response': {'status_code': 0}}", str(response["data"])) + r = collections.set_inheritance(self.rodsadmin_session, testcoll, enable=1) + common.assert_success(self, r) # verify inheritance is enabled - response = collections.stat(self.rodsadmin_session, testcoll) - self.assertTrue(response["data"]["inheritance_enabled"]) + r = collections.stat(self.rodsadmin_session, testcoll) + self.assertTrue(r["data"]["inheritance_enabled"]) # test disabling inheritance - response = collections.set_inheritance(self.rodsadmin_session, testcoll, enable=0) - self.assertEqual("{'irods_response': {'status_code': 0}}", str(response["data"])) + r = collections.set_inheritance(self.rodsadmin_session, testcoll, enable=0) + common.assert_success(self, r) # verify inheritance is disabled - response = collections.stat(self.rodsadmin_session, testcoll) - self.assertFalse(response["data"]["inheritance_enabled"]) + r = collections.stat(self.rodsadmin_session, testcoll) + self.assertFalse(r["data"]["inheritance_enabled"]) finally: collections.remove(self.rodsadmin_session, testcoll, recurse=1, no_trash=1) @@ -626,8 +622,8 @@ def test_modify_permissions(self): try: # create new collection - response = collections.create(self.rodsadmin_session, testcoll) - self.assertEqual(response["data"]["irods_response"]["status_code"], 0) + 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) @@ -657,24 +653,24 @@ def test_modify_permissions(self): ) # test no permissions - response = collections.stat(self.rodsuser_session, testcoll) - self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) + r = collections.stat(self.rodsuser_session, testcoll) + self.assertEqual("{'irods_response': {'status_code': -170000}}", str(r["data"])) # test set permissions - response = collections.modify_permissions(self.rodsadmin_session, testcoll, ops_permissions) - self.assertEqual(response["data"]["irods_response"]["status_code"], 0) + r = collections.modify_permissions(self.rodsadmin_session, testcoll, ops_permissions) + common.assert_success(self, r) # test with permissions - response = collections.stat(self.rodsadmin_session, testcoll) - self.assertTrue(response["data"]["permissions"]) + r = collections.stat(self.rodsadmin_session, testcoll) + self.assertTrue(r["data"]["permissions"]) # test set permissions nuil - response = collections.modify_permissions(self.rodsadmin_session, testcoll, ops_permissions_null) - self.assertEqual(response["data"]["irods_response"]["status_code"], 0) + r = collections.modify_permissions(self.rodsadmin_session, testcoll, ops_permissions_null) + common.assert_success(self, r) # test without permissions - response = collections.stat(self.rodsuser_session, testcoll) - self.assertEqual("{'irods_response': {'status_code': -170000}}", str(response["data"])) + r = collections.stat(self.rodsuser_session, testcoll) + self.assertEqual("{'irods_response': {'status_code': -170000}}", str(r["data"])) finally: # remove the collection @@ -720,18 +716,18 @@ def test_modify_metadata(self): ) # test adding and removing metadata - response = collections.modify_metadata( + r = collections.modify_metadata( self.rodsadmin_session, testcoll, ops_metadata, ) - self.assertEqual(response["data"]["irods_response"]["status_code"], 0) - response = collections.modify_metadata( + common.assert_success(self, r) + r = collections.modify_metadata( self.rodsadmin_session, testcoll, ops_metadata_remove, ) - self.assertEqual(response["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) finally: collections.remove(self.rodsadmin_session, testcoll, no_trash=1) @@ -756,18 +752,18 @@ def test_rename(self): self.assertRaises(TypeError, collections.rename, 0, testcolla) # test renaming - response = collections.rename( + r = collections.rename( self.rodsadmin_session, testcolla, testcollb, ) - self.assertEqual(response["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) # test presence - response = collections.stat(self.rodsadmin_session, testcolla) - self.assertEqual(response["data"]["irods_response"]["status_code"], -170000) - response = collections.stat(self.rodsadmin_session, testcollb) - self.assertEqual(response["data"]["irods_response"]["status_code"], 0) + 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) finally: collections.remove(self.rodsadmin_session, testcolla, no_trash=1) @@ -818,7 +814,7 @@ def test_empty_write(self): # Exercise an empty write f = f"/{self.zone_name}/home/{self.rodsuser_username}/empty_write.txt" r = data_objects.write(self.rodsuser_session, "", f) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) finally: data_objects.remove(self.rodsuser_session, f, no_trash=1) @@ -840,11 +836,11 @@ def test_common_operations(self): "/tmp/resource", # noqa: S108 "", ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) # Create a non-empty data object r = data_objects.write(self.rodsuser_session, "These are the bytes being written to the object", f1) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) # Read the data object r = data_objects.read(self.rodsuser_session, f1, offset=6, count=13) @@ -858,11 +854,11 @@ def test_common_operations(self): 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 r = data_objects.modify_replica(self.rodsadmin_session, f1, replica_number=0, new_data_comments="awesome") - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) # Replicate the data object r = data_objects.replicate( @@ -871,33 +867,33 @@ def test_common_operations(self): 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 = 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 data object r = data_objects.trim(self.rodsuser_session, f1, replica_number=0) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) # Rename the data object r = data_objects.rename(self.rodsuser_session, f1, f2) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) # Copy the data object r = data_objects.copy(self.rodsuser_session, f2, f3) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + 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 ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) # Exercise a bad permission self.assertRaises(ValueError, data_objects.set_permission, self.rodsuser_session, f3, "rods", "bad") @@ -909,11 +905,11 @@ def test_common_operations(self): "rods", "read", ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) # Confirm that the permission has been set r = data_objects.stat(self.rodsuser_session, f3) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) self.assertIn( { "name": "rods", @@ -928,7 +924,7 @@ def test_common_operations(self): r = data_objects.modify_permissions( self.rodsuser_session, f3, operations=[{'entity_name': 'rods', 'acl': 'write'}] ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) finally: # Remove the data objects @@ -949,7 +945,7 @@ def test_read_with_ticket(self): # Create a data object content = "hello anonymous" r = data_objects.write(self.rodsadmin_session, content, f) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) # Create a ticket for read r = tickets.create( @@ -957,7 +953,7 @@ def test_read_with_ticket(self): f, "read", ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) ticket_string = r["data"]["ticket"] self.assertGreater(len(ticket_string), 0) @@ -986,13 +982,13 @@ def test_small_write_with_ticket(self): "write", write_data_object_count=4, ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + 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) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) finally: # Add own permission, for the removal @@ -1017,13 +1013,13 @@ def test_large_write_with_ticket(self): "write", write_data_object_count=4, ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + 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, 3, ticket=ticket_string) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) handle = r["data"]["parallel_write_handle"] # Write to the data object using the parallel write handle @@ -1043,11 +1039,11 @@ def test_large_write_with_ticket(self): ) for future in concurrent.futures.as_completed(futures): r = future.result() - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) # Close parallel write r = data_objects.parallel_write_shutdown(self.anonymous_session, handle) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) finally: # Add own permission, for the removal @@ -1066,18 +1062,18 @@ def test_modify_replica(self): try: # Create a data object r = data_objects.write(self.rodsadmin_session, "some words", f) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + 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'" ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + 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'") - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) rescid = int(r["data"]["rows"][0][0]) # Exercise modify replica error, incompatible params @@ -1117,7 +1113,7 @@ def test_modify_replica(self): new_data_type_name="html", new_data_version=3, ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) # Restore the physical path so cleanup succeeds r = data_objects.modify_replica( @@ -1126,7 +1122,7 @@ def test_modify_replica(self): resource_hierarchy="demoResc", new_data_path=phypath, ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) finally: data_objects.remove(self.rodsadmin_session, f, no_trash=1) @@ -1146,7 +1142,7 @@ def test_checksums(self): "/tmp/newresource", # noqa: S108 "", ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) # Create a non-empty data object r = data_objects.write( @@ -1154,7 +1150,7 @@ def test_checksums(self): "These are the bytes being written to the object", f, ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) # Replicate the data object r = data_objects.replicate( @@ -1162,13 +1158,13 @@ def test_checksums(self): f, dst_resource=resc, ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + 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'" ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) self.assertEqual(len(r["data"]["rows"]), 2) # Calculate a checksum for the first replica @@ -1177,7 +1173,7 @@ def test_checksums(self): f, replica_number=0, ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) # Calculate a checksum for the second replica r = data_objects.calculate_checksum( @@ -1185,7 +1181,7 @@ def test_checksums(self): f, resource=resc, ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) # Verify checksum on first replica r = data_objects.verify_checksum( @@ -1193,7 +1189,7 @@ def test_checksums(self): 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( @@ -1201,7 +1197,7 @@ def test_checksums(self): f, resource=resc, ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) finally: # Remove the data object @@ -1222,7 +1218,7 @@ def test_touch(self): try: # Test touching non existant data object with no_create r = data_objects.touch(self.rodsadmin_session, f, no_create=1) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) # Show that the object has not been created r = data_objects.stat(self.rodsadmin_session, f) @@ -1230,29 +1226,29 @@ def test_touch(self): # Test touching non existant object without no_create r = data_objects.touch(self.rodsadmin_session, f, no_create=0) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) # Show that the object has been created r = data_objects.stat(self.rodsadmin_session, f) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) # Test touching existing object without no_create r = data_objects.touch(self.rodsadmin_session, f, no_create=0) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) # Test parameter options r = data_objects.touch(self.rodsadmin_session, f, seconds_since_epoch=5000) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) r = data_objects.stat(self.rodsadmin_session, f) self.assertEqual(r["data"]["modified_at"], 5000) r = data_objects.touch(self.rodsadmin_session, f, replica_number=0) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) r = data_objects.touch(self.rodsadmin_session, f, leaf_resources="demoResc") - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) r = data_objects.touch(self.rodsadmin_session, f, reference=f) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) finally: # Remove the object @@ -1276,23 +1272,23 @@ def test_register(self): "/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) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + 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'" ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + 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) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) # 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" @@ -1305,14 +1301,14 @@ def test_register(self): 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 = 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], phyfile) self.assertNotEqual(r["data"]["rows"][0][2], "") @@ -1331,7 +1327,7 @@ def test_parallel_write(self): # Open parallel write r = data_objects.parallel_write_init(self.rodsadmin_session, f, stream_count=3) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) handle = r["data"]["parallel_write_handle"] try: @@ -1352,7 +1348,7 @@ def test_parallel_write(self): ) for future in concurrent.futures.as_completed(futures): r = future.result() - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) finally: # Close parallel write data_objects.parallel_write_shutdown(self.rodsadmin_session, handle) @@ -1395,11 +1391,11 @@ def test_common_operations(self): try: # Create replication resource. r = resources.create(self.rodsadmin_session, resc_repl, "replication", "", "", "") - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) # Show the replication resource was created. r = resources.stat(self.rodsadmin_session, resc_repl) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + 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) @@ -1429,15 +1425,15 @@ def test_common_operations(self): # Create a unixfilesystem resource. r = resources.create(self.rodsadmin_session, resc_name, "unixfilesystem", self.host, vault_path, "") - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + 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) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) # Show that the resource was created and configured successfully. r = resources.stat(self.rodsadmin_session, resc_name) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + 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) @@ -1457,14 +1453,14 @@ def test_common_operations(self): # 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) - self.assertEqual(r["data"]["irods_response"]["status_code"], 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}'", ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) self.assertEqual(len(r["data"]["rows"]), 2) resc_tuple = (r["data"]["rows"][0][1], r["data"]["rows"][1][1]) @@ -1472,19 +1468,19 @@ def test_common_operations(self): # Trim a replica. r = data_objects.trim(self.rodsadmin_session, f, replica_number=0) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) # 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}'", ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) self.assertEqual(len(r["data"]["rows"]), 1) # Launch rebalance r = resources.rebalance(self.rodsadmin_session, resc_repl) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) # Give the rebalance operation time to complete! time.sleep(3) @@ -1494,7 +1490,7 @@ def test_common_operations(self): self.rodsadmin_session, f"select DATA_NAME, RESC_NAME where DATA_NAME = '{pathlib.Path(f).name}'", ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) self.assertEqual(len(r["data"]["rows"]), 2) finally: @@ -1524,7 +1520,7 @@ def test_modify_failures(self): "/tmp/badresc_vault", # noqa: S108 "", ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) # Exercise bad modify property self.assertRaises(ValueError, resources.modify, self.rodsadmin_session, badresc, "badoption", "2") @@ -1548,15 +1544,15 @@ def test_add_child_context(self): "/tmp/resc_vault", # noqa: S108 "", ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) # Exercise add child with context r = resources.add_child(self.rodsadmin_session, "demoResc", resc, context="neat") - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) # Confirm r = resources.stat(self.rodsadmin_session, resc) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + 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") @@ -1578,12 +1574,12 @@ def test_modify_metadata(self): "/tmp/metadata_demo_vault", # noqa: S108 "ignoreme", ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + 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) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) # Show that the metadata is on the resource r = queries.execute_genquery( @@ -1591,13 +1587,13 @@ def test_modify_metadata(self): "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) + 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) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) # Show that the metadata is no longer on the resource r = queries.execute_genquery( @@ -1605,7 +1601,7 @@ def test_modify_metadata(self): "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) + common.assert_success(self, r) self.assertEqual(len(r["data"]["rows"]), 0) finally: @@ -1619,7 +1615,7 @@ def test_modify_properties(self): try: # Create a new resource. r = resources.create(self.rodsadmin_session, resource, "replication", "", "", "") - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) # The list of updates to apply in sequence. property_map = [ @@ -1648,7 +1644,7 @@ def test_modify_properties(self): # Show the property was modified. r = resources.stat(self.rodsadmin_session, resource) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) self.assertEqual(r["data"]["info"][p], v) finally: # Remove the resource @@ -1677,7 +1673,7 @@ def test_list(self): """Test listing rule engine plugins.""" # Try listing rule engine plugins r = rules.list_rule_engines(self.rodsadmin_session) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) self.assertGreater(len(r["data"]["rule_engine_plugin_instances"]), 0) def test_execute_rule(self): @@ -1690,8 +1686,7 @@ def test_execute_rule(self): 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, @@ -1710,13 +1705,13 @@ def test_remove_delay_rule(self): f'{{ writeLine("serverLog", "test suite"); }}', rep_instance, ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) # 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)") - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) self.assertEqual(len(r["data"]["rows"]), 1) finally: @@ -1766,11 +1761,11 @@ def test_create_execute_remove_specific_query(self): name = "get_users_count" sql = "select count(*) from r_user_main" r = queries.add_specific_query(self.rodsadmin_session, name=name, sql=sql) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) # Execute as rodsuser r = queries.execute_specific_query(self.rodsuser_session, name=name) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) self.assertEqual(r["data"]["rows"][0][0], "4") finally: @@ -1846,7 +1841,7 @@ def test_create_and_remove(self): groups=ticket_groups, hosts=ticket_hosts, ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) ticket_string = r["data"]["ticket"] self.assertGreater(len(ticket_string), 0) @@ -1860,7 +1855,7 @@ def test_create_and_remove(self): "TICKET_ALLOWED_USER_NAME, TICKET_ALLOWED_GROUP_NAME, TICKET_ALLOWED_HOST", ) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + 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) @@ -1926,16 +1921,16 @@ def test_set_password(self): try: # Create a new user. r = users_groups.create_user(self.rodsadmin_session, new_username, self.zone_name, user_type) - self.assertEqual(r["status_code"], 200) + common.assert_success(self, r) # 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) - self.assertEqual(r["status_code"], 200) + common.assert_success(self, r) # Try to get a token for the user session = authenticate(self.url_base, new_username, new_password) - self.assertEqual(r["status_code"], 200) + common.assert_success(self, r) self.assertIsInstance(session.token, str) finally: @@ -1975,7 +1970,7 @@ def test_add_remove_user_to_and_from_group(self): new_group = "test_group" r = users_groups.create_group(self.rodsadmin_session, new_group) self.assertEqual(r["status_code"], 200) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) # Stat the group. r = users_groups.stat(self.rodsadmin_session, new_group) @@ -1990,17 +1985,15 @@ def test_add_remove_user_to_and_from_group(self): new_username = "test_user_rodsuser" user_type = "rodsuser" r = users_groups.create_user(self.rodsadmin_session, new_username, self.zone_name, user_type) - self.assertEqual(r["status_code"], 200) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + 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) - self.assertEqual(r["status_code"], 200) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + 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) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) self.assertEqual(r["data"]["is_member"], True) finally: @@ -2020,28 +2013,24 @@ def test_only_a_rodsadmin_can_change_the_type_of_a_user(self): new_username = "test_user_rodsuser" user_type = "rodsuser" r = users_groups.create_user(self.rodsadmin_session, new_username, self.zone_name, user_type) - self.assertEqual(r["status_code"], 200) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + 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) - self.assertEqual(r["status_code"], 200) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + 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) + self.assertEqual(r["data"]["irods_response"]["status_code"], -13000) # SYS_NO_API_PRIV # Show that the user type matches the type set by the rodsadmin. r = users_groups.stat(self.rodsuser_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.assertEqual(stat_info["local_unique_name"], f"{new_username}#{self.zone_name}") - self.assertEqual(stat_info["type"], new_user_type) + 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. @@ -2050,11 +2039,9 @@ def test_only_a_rodsadmin_can_change_the_type_of_a_user(self): def test_listing_all_users_in_zone(self): """Test listing all users in the zone.""" r = users_groups.users(self.rodsadmin_session) - 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"]) + 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.""" @@ -2062,18 +2049,15 @@ def test_listing_all_groups_in_zone(self): # Create a new group. new_group = "test_group" r = users_groups.create_group(self.rodsadmin_session, new_group) - self.assertEqual(r["status_code"], 200) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) # Get all groups. r = users_groups.groups( self.rodsadmin_session, ) - 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"]) + common.assert_success(self, r) + self.assertIn("public", r["data"]["groups"]) + self.assertIn(new_group, r["data"]["groups"]) finally: # Remove the new group. @@ -2086,8 +2070,7 @@ def test_modifying_metadata_atomically(self): # Add metadata to the user. ops = [{"operation": "add", "attribute": "a1", "value": "v1", "units": "u1"}] r = users_groups.modify_metadata(self.rodsadmin_session, username, ops) - self.assertEqual(r["status_code"], 200) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) # Show the metadata exists on the user. r = queries.execute_genquery( @@ -2095,17 +2078,13 @@ def test_modifying_metadata_atomically(self): "select USER_NAME where META_USER_ATTR_NAME = 'a1' and " "META_USER_ATTR_VALUE = 'v1' and META_USER_ATTR_UNITS = 'u1'", ) - self.assertEqual(r["status_code"], 200) - - result = r["data"] - self.assertEqual(result["irods_response"]["status_code"], 0) - self.assertEqual(result["rows"][0][0], username) + 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 = users_groups.modify_metadata(self.rodsadmin_session, username, ops) - self.assertEqual(r["status_code"], 200) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) # Show the metadata no longer exists on the user. r = queries.execute_genquery( @@ -2113,10 +2092,8 @@ def test_modifying_metadata_atomically(self): "select USER_NAME where META_USER_ATTR_NAME = 'a1' and " "META_USER_ATTR_VALUE = 'v1' and META_USER_ATTR_UNITS = 'u1'", ) - self.assertEqual(r["status_code"], 200) - result = r["data"] - self.assertEqual(result["irods_response"]["status_code"], 0) - self.assertEqual(len(result["rows"]), 0) + common.assert_success(self, r) + self.assertEqual(len(r["data"]["rows"]), 0) # Tests for zone operations @@ -2142,15 +2119,10 @@ def test_report_operation(self): r = zones.report( self.rodsadmin_session, ) - 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"]) + 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.""" @@ -2159,29 +2131,25 @@ def test_adding_removing_and_modifying_zones(self): # Add a zone, only to remove it immediately r = zones.add(self.rodsadmin_session, zone_name, connection_info="localhost:1250", comment="brief") - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) # Remove it r = zones.remove(self.rodsadmin_session, zone_name) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) # 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) - self.assertEqual(r["status_code"], 200) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + 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) - self.assertEqual(r["status_code"], 200) - - result = r["data"] - self.assertEqual(result["irods_response"]["status_code"], 0) - self.assertEqual(result["exists"], True) - self.assertEqual(result["info"]["name"], zone_name) - self.assertEqual(result["info"]["type"], "remote") - self.assertEqual(result["info"]["connection_info"], "") - self.assertEqual(result["info"]["comment"], "") + 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 = [ @@ -2194,8 +2162,7 @@ def test_adding_removing_and_modifying_zones(self): for p, v in property_map: with self.subTest(f"Setting property [{p}] to value [{v}]"): r = zones.modify(self.rodsadmin_session, zone_name, p, v) - self.assertEqual(r["status_code"], 200) - self.assertEqual(r["data"]["irods_response"]["status_code"], 0) + common.assert_success(self, r) # Capture the new name of the zone following its renaming. if p == "name": @@ -2203,12 +2170,9 @@ def test_adding_removing_and_modifying_zones(self): # Show the new zone was modified successfully. r = zones.stat(self.rodsadmin_session, 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) + common.assert_success(self, r) + self.assertEqual(r["data"]["exists"], True) + self.assertEqual(r["data"]["info"][p], v) finally: # Remove the remote zone. From 53eae7af74859987ce3fe5fc543b5c764c3e6c8c Mon Sep 17 00:00:00 2001 From: Terrell Russell Date: Tue, 24 Feb 2026 15:20:21 -0500 Subject: [PATCH 27/29] shadow python builtins, use noqa A002 --- irods_http/data_objects.py | 18 +++++++++--------- irods_http/resources.py | 22 +++++++++++----------- irods_http/tickets.py | 12 ++++++------ irods_http/users_groups.py | 26 +++++++++++++------------- test/test_endpoint_operations.py | 14 +++++++------- 5 files changed, 46 insertions(+), 46 deletions(-) diff --git a/irods_http/data_objects.py b/irods_http/data_objects.py index b6b7a6b..0755ea8 100644 --- a/irods_http/data_objects.py +++ b/irods_http/data_objects.py @@ -100,7 +100,7 @@ def calculate_checksum( resource: str = "", replica_number: int = -1, force: int = 0, - all_: int = 0, + all: int = 0, # noqa: A002 admin: int = 0, ) -> dict: """ @@ -112,7 +112,7 @@ def calculate_checksum( 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. + 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: @@ -124,14 +124,14 @@ def calculate_checksum( 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(all) common.validate_0_or_1(admin) data = { "op": "calculate_checksum", "lpath": lpath, "force": force, - "all": all_, + "all": all, "admin": admin, } @@ -443,7 +443,7 @@ def read(session: IRODSHTTPSession, lpath: str, offset: int = 0, count: int = -1 def write( session: IRODSHTTPSession, - bytes_, + bytes, # noqa: A002 lpath: str = "", resource: str = "", offset: int = 0, @@ -458,7 +458,7 @@ def write( Args: session: IRODSHTTPSession object containing base URL and authentication token. - bytes_: The bytes to be written. + 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. @@ -477,8 +477,8 @@ def write( 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'") + 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) @@ -493,7 +493,7 @@ def write( "offset": offset, "truncate": truncate, "append": append, - "bytes": bytes_, + "bytes": bytes, } if parallel_write_handle != "": diff --git a/irods_http/resources.py b/irods_http/resources.py index d23862c..e2730c9 100644 --- a/irods_http/resources.py +++ b/irods_http/resources.py @@ -8,14 +8,14 @@ from .irods_http import IRODSHTTPSession # noqa: TC001 -def create(session: IRODSHTTPSession, name: str, type_: str, host: str, vault_path: str, context: str): +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. + 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 @@ -27,12 +27,12 @@ def create(session: IRODSHTTPSession, name: str, type_: str, host: str, vault_pa 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(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_} + data = {"op": "create", "name": name, "type": type} if host != "": data["host"] = host @@ -67,14 +67,14 @@ def remove(session: IRODSHTTPSession, name: str): return common.process_response(r) -def modify(session: IRODSHTTPSession, name: str, property_: str, value: str): +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. + property: The property to be modified. value: The new value to be set. Returns: @@ -82,11 +82,11 @@ def modify(session: IRODSHTTPSession, name: str, property_: str, value: str): The iRODS response is only valid if no error occurred during HTTP communication. Raises: - ValueError: If property_ is not a valid resource property. + ValueError: If property is not a valid resource property. """ common.validate_instance(name, str) - common.validate_instance(property_, str) - if property_ not in [ + common.validate_instance(property, str) + if property not in [ "name", "type", "host", @@ -103,10 +103,10 @@ def modify(session: IRODSHTTPSession, name: str, property_: str, value: str): "\n - status\n - free_space\n - comments\n - information" ) common.validate_instance(value, str) - if (property_ == "status") and (value not in ["up", "down"]): + 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} + 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) diff --git a/irods_http/tickets.py b/irods_http/tickets.py index 46bc989..dded308 100644 --- a/irods_http/tickets.py +++ b/irods_http/tickets.py @@ -9,7 +9,7 @@ def create( session: IRODSHTTPSession, lpath: str, - type_: str = "read", + type: str = "read", # noqa: A002 use_count: int = -1, write_data_object_count: int = -1, write_byte_count: int = -1, @@ -24,7 +24,7 @@ def create( Args: session: An IRODSHTTPSession instance. lpath: Absolute logical path to a data object or collection. - type_: Read or write. Defaults to read. + 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. @@ -38,11 +38,11 @@ def create( The iRODS response is only valid if no error occurred during HTTP communication. Raises: - ValueError: If type_ is not 'read' or 'write'. + ValueError: If type is not 'read' or 'write'. """ common.validate_instance(lpath, str) - common.validate_instance(type_, str) - if type_ not in ["read", "write"]: + 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) @@ -52,7 +52,7 @@ def create( common.validate_instance(groups, str) common.validate_instance(hosts, str) - data = {"op": "create", "lpath": lpath, "type": type_} + data = {"op": "create", "lpath": lpath, "type": type} if use_count != -1: data["use-count"] = use_count diff --git a/irods_http/users_groups.py b/irods_http/users_groups.py index b45c7ce..5355a12 100644 --- a/irods_http/users_groups.py +++ b/irods_http/users_groups.py @@ -8,7 +8,7 @@ from .irods_http import IRODSHTTPSession # noqa: TC001 -def create_user(session: IRODSHTTPSession, name: str, zone: str, user_type: str = "rodsuser"): +def create_user(session: IRODSHTTPSession, name: str, zone: str, type: str = "rodsuser"): # noqa: A002 """ Create a new user. Requires rodsadmin or groupadmin privileges. @@ -16,22 +16,22 @@ def create_user(session: IRODSHTTPSession, name: str, zone: str, user_type: str session: An IRODSHTTPSession instance. 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. + 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'. + ValueError: If type is not 'rodsuser', 'groupadmin', or 'rodsadmin'. """ 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.") + 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": user_type} + 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) @@ -88,7 +88,7 @@ def set_password(session: IRODSHTTPSession, name: str, zone: str, new_password: return common.process_response(r) -def set_user_type(session: IRODSHTTPSession, name: str, zone: str, user_type: str): +def set_user_type(session: IRODSHTTPSession, name: str, zone: str, type: str): # noqa: A002 """ Change a users type. Requires rodsadmin privileges. @@ -96,7 +96,7 @@ def set_user_type(session: IRODSHTTPSession, name: str, zone: str, user_type: st 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. - user_type: Can be rodsuser, groupadmin, or rodsadmin. + type: Can be rodsuser, groupadmin, or rodsadmin. Returns: A dict containing the HTTP status code and iRODS response. @@ -107,15 +107,15 @@ def set_user_type(session: IRODSHTTPSession, name: str, zone: str, user_type: st """ 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.") + 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": user_type, + "new-user-type": type, } r = requests.post(session.url_base + "/users-groups", headers=session.post_headers, data=data) # noqa: S113 diff --git a/test/test_endpoint_operations.py b/test/test_endpoint_operations.py index 5332c5f..50afe58 100644 --- a/test/test_endpoint_operations.py +++ b/test/test_endpoint_operations.py @@ -1031,7 +1031,7 @@ def test_large_write_with_ticket(self): executor.submit( data_objects.write, self.anonymous_session, - bytes_=x[1] * count, + bytes=x[1] * count, offset=x[0] * count, stream_index=x[0], parallel_write_handle=handle, @@ -1340,7 +1340,7 @@ def test_parallel_write(self): executor.submit( data_objects.write, self.rodsadmin_session, - bytes_=x[1] * count, + bytes=x[1] * count, offset=x[0] * count, stream_index=x[0], parallel_write_handle=handle, @@ -1796,7 +1796,7 @@ def test_create_failures(self): p = f"/{self.zone_name}/home/{self.rodsuser_username}" # bad type - self.assertRaises(ValueError, tickets.create, self.rodsadmin_session, p, type_="bad") + self.assertRaises(ValueError, tickets.create, self.rodsadmin_session, p, type="bad") # bad object count self.assertRaises( @@ -1804,7 +1804,7 @@ def test_create_failures(self): tickets.create, self.rodsadmin_session, p, - type_="write", + type="write", write_data_object_count=-5, ) @@ -1814,7 +1814,7 @@ def test_create_failures(self): tickets.create, self.rodsadmin_session, p, - type_="write", + type="write", write_byte_count=-2, ) @@ -1892,13 +1892,13 @@ def setUp(self): 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, user_type="bad" + ValueError, users_groups.create_user, self.rodsadmin_session, "baduser", self.zone_name, type="bad" ) 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, user_type="bad" + ValueError, users_groups.set_user_type, self.rodsadmin_session, "baduser", self.zone_name, type="bad" ) def test_bad_connection(self): From 2aea7ce7c19087e009ba95102f3c762a08e9f48a Mon Sep 17 00:00:00 2001 From: Terrell Russell Date: Tue, 24 Feb 2026 15:21:43 -0500 Subject: [PATCH 28/29] Revert "squash - removed test/__init__.py" This reverts commit b5eaba85102e270e0650ff318b8763f2198d84c8. --- test/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 test/__init__.py diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..cf35192 --- /dev/null +++ b/test/__init__.py @@ -0,0 +1 @@ +"""iRODS HTTP client library test module.""" From 449404f6df5bad3456d811c62780d5d6f9e5cff4 Mon Sep 17 00:00:00 2001 From: Terrell Russell Date: Tue, 24 Feb 2026 15:26:44 -0500 Subject: [PATCH 29/29] remove unnecessary aliases --- irods_http/__init__.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/irods_http/__init__.py b/irods_http/__init__.py index 5b2a8ca..c16a82b 100644 --- a/irods_http/__init__.py +++ b/irods_http/__init__.py @@ -1,19 +1,19 @@ """iRODS HTTP client library for Python.""" from . import ( - collections as collections, - data_objects as data_objects, - queries as queries, - resources as resources, - rules as rules, - tickets as tickets, - users_groups as users_groups, - zones as zones, + collections, + data_objects, + queries, + resources, + rules, + tickets, + users_groups, + zones, ) from .irods_http import ( - IRODSHTTPSession as IRODSHTTPSession, - authenticate as authenticate, - get_server_info as get_server_info, + IRODSHTTPSession, + authenticate, + get_server_info, ) __all__ = [