python-irodsclient
Advanced tools
| import unittest | ||
| import irods.client_configuration as cfg | ||
| # Test assignments on the negative and positive space of the | ||
| # client configuration. | ||
| class TestClientConfigurationAttributes(unittest.TestCase): | ||
| def test_configuration_writes_and_miswrites__issue_708(self): | ||
| # For caching configuration objects | ||
| configuration_level = {} | ||
| leaf_names = [] | ||
| for dotted_name, value, is_conf in cfg._var_items_as_generator(): # noqa: SLF001 | ||
| with self.subTest(dotted_name=dotted_name): | ||
| name_parts = dotted_name.split('.') | ||
| namespace = '.'.join(name_parts[:-1]) | ||
| attribute_name = name_parts[-1] | ||
| if isinstance(value, cfg.iRODSConfiguration): | ||
| # Store a parent object corresponding to a namespace. For any leaf value | ||
| # subsequently found in the top-down descent, to be sitting directly within | ||
| # that namespace, the "else is_conf" part of this if/else will run the core | ||
| # part of the test on the corresponding configuration setting. | ||
| configuration_level[dotted_name] = value | ||
| elif is_conf: | ||
| # A configuration setting was actually found (i.e. a "leaf" value within a dotted name.) | ||
| # Store the leaf name for proof positive of subtests actually run. | ||
| leaf_names.append(attribute_name) | ||
| # Test the positive space, i.e. the 'hit'. This simply tests that the | ||
| # setting may be written to without error: | ||
| setattr(configuration_level[namespace], attribute_name, value) | ||
| # Test the negative space, i.e. a deliberate 'miss'. This time we must fail; | ||
| # otherwise we'd get a silent miswrite in the form of a write to the incorrect attribute. | ||
| # (The new __slots__ members are there to prevent this): | ||
| with self.assertRaises(AttributeError): | ||
| setattr(configuration_level[namespace], attribute_name + '_1', value) | ||
| # These cases were identified as likely ones for possible failed writes via misspelling. | ||
| self.assertIn('irods_query_limit', leaf_names) | ||
| self.assertIn('xml_parser_default', leaf_names) |
| #! /usr/bin/env python | ||
| import os | ||
| import sys | ||
| import tempfile | ||
| import unittest | ||
| import textwrap | ||
| import json | ||
| import shutil | ||
| import ssl | ||
| import irods.test.helpers as helpers | ||
| from irods.connection import Connection | ||
| from irods.session import iRODSSession, NonAnonymousLoginWithoutPassword | ||
| from irods.rule import Rule | ||
| from irods.models import User | ||
| from socket import gethostname | ||
| from irods.password_obfuscation import encode as pw_encode | ||
| from irods.connection import PlainTextPAMPasswordError | ||
| from irods.access import iRODSAccess | ||
| import irods.exception as ex | ||
| import contextlib | ||
| import socket | ||
| from re import compile as regex | ||
| from typing import Dict, Optional | ||
| import gc | ||
| from irods.test.setup_ssl import create_ssl_dir | ||
| # | ||
| # Allow override to specify the PAM password in effect for the test rodsuser. | ||
| # | ||
| TEST_PAM_PW_OVERRIDE = os.environ.get("PYTHON_IRODSCLIENT_TEST_PAM_PW_OVERRIDE", "") | ||
| TEST_PAM_PW = TEST_PAM_PW_OVERRIDE or "test123" | ||
| TEST_IRODS_PW = "apass" | ||
| TEST_RODS_USER = "alissa" | ||
| from re import Pattern as regex_type | ||
| def json_file_update(fname, keys_to_delete=(), **kw): | ||
| with open(fname, "r") as f: | ||
| j = json.load(f) | ||
| j.update(**kw) | ||
| for k in keys_to_delete: | ||
| if k in j: | ||
| del j[k] | ||
| elif isinstance(k, regex_type): | ||
| jk = [i for i in j.keys() if k.search(i)] | ||
| for ky in jk: | ||
| del j[ky] | ||
| with open(fname, "w") as out: | ||
| json.dump(j, out, indent=4) | ||
| def env_dir_fullpath(authtype): | ||
| return os.path.join(os.environ["HOME"], ".irods." + authtype) | ||
| def json_env_fullpath(authtype): | ||
| return os.path.join(env_dir_fullpath(authtype), "irods_environment.json") | ||
| def secrets_fullpath(authtype): | ||
| return os.path.join(env_dir_fullpath(authtype), ".irodsA") | ||
| RODSADMIN_ENV_PATH = os.path.expanduser("~/.irods/irods_environment.json") | ||
| SERVER_ENV_SSL_SETTINGS = { | ||
| "irods_ssl_certificate_chain_file": "/etc/irods/ssl/irods.crt", | ||
| "irods_ssl_certificate_key_file": "/etc/irods/ssl/irods.key", | ||
| "irods_ssl_dh_params_file": "/etc/irods/ssl/dhparams.pem", | ||
| "irods_ssl_ca_certificate_file": "/etc/irods/ssl/irods.crt", | ||
| "irods_ssl_verify_server": "cert", | ||
| } | ||
| CLIENT_OPTIONS_FOR_SSL = { | ||
| "irods_client_server_policy": "CS_NEG_REQUIRE", | ||
| "irods_client_server_negotiation": "request_server_negotiation", | ||
| "irods_ssl_ca_certificate_file": "/etc/irods/ssl/irods.crt", | ||
| "irods_ssl_verify_server": "cert", | ||
| "irods_encryption_key_size": 16, | ||
| "irods_encryption_salt_size": 8, | ||
| "irods_encryption_num_hash_rounds": 16, | ||
| "irods_encryption_algorithm": "AES-256-CBC", | ||
| } | ||
| def client_env_keys_from_admin_env(user_name, auth_scheme=""): | ||
| cli_env = {} | ||
| with open(RODSADMIN_ENV_PATH) as f: | ||
| srv_env = json.load(f) | ||
| for k in ["irods_host", "irods_zone_name", "irods_port"]: | ||
| cli_env[k] = srv_env[k] | ||
| cli_env["irods_user_name"] = user_name | ||
| if auth_scheme: | ||
| cli_env["irods_authentication_scheme"] = auth_scheme | ||
| return cli_env | ||
| # For testing only! | ||
| # Note pam_password_in_plaintext* functions are for test only as they will allow transmitting passwords on a potentially | ||
| # interceptible, unencrypted channel. | ||
| @contextlib.contextmanager | ||
| def pam_password_in_plaintext_4_3(allow=True): | ||
| import irods.helpers | ||
| from irods.auth.pam_password import ENSURE_SSL_IS_ACTIVE | ||
| # We'll temporarily replace the original iRODSSession constructor with a new version which changes pam_password options | ||
| # to allow pam password SSL | ||
| old_init = iRODSSession.__init__ | ||
| def new_init(self, *arg, **kw): | ||
| old_init(self, *arg, **kw) | ||
| self.set_auth_option_for_scheme("pam_password", ENSURE_SSL_IS_ACTIVE, not (allow)) | ||
| with irods.helpers.temporarily_assign_attribute(iRODSSession, "__init__", new_init): | ||
| yield | ||
| @contextlib.contextmanager | ||
| def pam_password_in_plaintext_4_2(allow=True): | ||
| saved = bool(Connection.DISALLOWING_PAM_PLAINTEXT) | ||
| try: | ||
| Connection.DISALLOWING_PAM_PLAINTEXT = not (allow) | ||
| yield | ||
| finally: | ||
| Connection.DISALLOWING_PAM_PLAINTEXT = saved | ||
| @contextlib.contextmanager | ||
| def pam_password_in_plaintext(allow=True, nop=False): | ||
| if nop: | ||
| yield | ||
| return | ||
| with pam_password_in_plaintext_4_2(allow=allow): | ||
| with pam_password_in_plaintext_4_3(allow=allow): | ||
| yield | ||
| class TestLogins(unittest.TestCase): | ||
| """ | ||
| Ideally, these tests should move into CI, but that would require the server | ||
| (currently a different node than the client) to have SSL certs created and | ||
| enabled. | ||
| Until then, we require these tests to be run manually on a server node, | ||
| with: | ||
| python -m unittest "irods.test.login_auth_test[.XX[.YY]]' | ||
| Additionally: | ||
| 1. The PAM/SSL tests under the TestLogins class should be run on a | ||
| single-node iRODS system, by the service account user. This ensures | ||
| the /etc/irods directory is local and writable. | ||
| 2. ./setup_ssl.py (sets up SSL keys etc. in /etc/irods/ssl) should be run | ||
| first to create (or overwrite, if appropriate) the /etc/irods/ssl directory | ||
| and its contents. | ||
| 3. Must add & override configuration entries in /var/lib/irods/irods_environment | ||
| Per https://slides.com/irods/ugm2018-ssl-and-pam-configuration#/3/7 | ||
| """ | ||
| user_auth_envs = { | ||
| ".irods.pam": {"USER": TEST_RODS_USER, "PASSWORD": TEST_PAM_PW, "AUTH": "pam"}, | ||
| ".irods.native": { | ||
| "USER": TEST_RODS_USER, | ||
| "PASSWORD": TEST_IRODS_PW, | ||
| "AUTH": "native", | ||
| }, | ||
| } | ||
| env_save: Dict[str, Optional[str]] = {} | ||
| @contextlib.contextmanager | ||
| def setenv(self, var, newvalue): | ||
| try: | ||
| self.env_save[var] = os.environ.get(var, None) | ||
| os.environ[var] = newvalue | ||
| yield newvalue | ||
| finally: | ||
| oldvalue = self.env_save[var] | ||
| if oldvalue is None: | ||
| del os.environ[var] | ||
| else: | ||
| os.environ[var] = oldvalue | ||
| def create_env_dirs(self): | ||
| dirs = {} | ||
| retval = [] | ||
| # -- create environment configurations and secrets | ||
| with pam_password_in_plaintext(): | ||
| for dirname, lookup in self.user_auth_envs.items(): | ||
| if lookup["AUTH"] in ("pam", "pam_password"): | ||
| ses = iRODSSession( | ||
| host=gethostname(), | ||
| user=lookup["USER"], | ||
| zone="tempZone", | ||
| authentication_scheme=lookup["AUTH"], | ||
| password=lookup["PASSWORD"], | ||
| port=1247, | ||
| **( | ||
| {**SERVER_ENV_SSL_SETTINGS, **CLIENT_OPTIONS_FOR_SSL} | ||
| if self.admin.server_version >= (5,) | ||
| else {} | ||
| ), | ||
| ) | ||
| try: | ||
| pam_hashes = ses.pam_pw_negotiated | ||
| except AttributeError: | ||
| pam_hashes = [] | ||
| if not pam_hashes: | ||
| print("Warning ** PAM pw couldnt be generated") | ||
| break | ||
| scrambled_pw = pw_encode(pam_hashes[0]) | ||
| # elif lookup['AUTH'] == 'XXXXXX': # TODO: insert other authentication schemes here | ||
| elif lookup["AUTH"] in ("native", "", None): | ||
| scrambled_pw = pw_encode(lookup["PASSWORD"]) | ||
| cl_env = client_env_keys_from_admin_env(TEST_RODS_USER) | ||
| if lookup.get("AUTH", None) is not None: # - specify auth scheme only if given | ||
| cl_env["irods_authentication_scheme"] = lookup["AUTH"] | ||
| dirbase = os.path.join(os.environ["HOME"], dirname) | ||
| dirs[dirbase] = {"secrets": scrambled_pw, "client_environment": cl_env} | ||
| # -- create the environment directories and write into them the configurations just created | ||
| for absdir in dirs.keys(): | ||
| shutil.rmtree(absdir, ignore_errors=True) | ||
| os.mkdir(absdir) | ||
| with open(os.path.join(absdir, "irods_environment.json"), "w") as envfile: | ||
| envfile.write("{}") | ||
| json_file_update(envfile.name, **dirs[absdir]["client_environment"]) | ||
| with open(os.path.join(absdir, ".irodsA"), "w") as secrets_file: | ||
| secrets_file.write(dirs[absdir]["secrets"]) | ||
| os.chmod(secrets_file.name, 0o600) | ||
| retval = dirs.keys() | ||
| return retval | ||
| PAM_SCHEME_STRING = "pam" | ||
| @classmethod | ||
| def setUpClass(cls): | ||
| import irods.client_configuration as cfg | ||
| cls.admin = helpers.make_session() | ||
| if cls.admin.server_version >= (4, 3) and not cfg.legacy_auth.force_legacy_auth: | ||
| cls.PAM_SCHEME_STRING = cls.user_auth_envs[".irods.pam"]["AUTH"] = "pam_password" | ||
| @classmethod | ||
| def tearDownClass(cls): | ||
| cls.admin.cleanup() | ||
| def setUp(self): | ||
| super(TestLogins, self).setUp() | ||
| def tearDown(self): | ||
| for envdir in getattr(self, "envdirs", []): | ||
| shutil.rmtree(envdir, ignore_errors=True) | ||
| super(TestLogins, self).tearDown() | ||
| def validate_session(self, session, verbose=False, **options): | ||
| # - try to get the home collection | ||
| home_coll = "/{0.zone}/home/{0.username}".format(session) | ||
| self.assertTrue(session.collections.get(home_coll).path == home_coll) | ||
| if verbose: | ||
| print(home_coll) | ||
| # - check user is as expected | ||
| self.assertEqual(session.username, TEST_RODS_USER) | ||
| # - check socket type (normal vs SSL) against whether ssl requested | ||
| use_ssl = options.pop("ssl", None) | ||
| if use_ssl is not None: | ||
| my_connect = [s for s in (session.pool.active | session.pool.idle)][0] | ||
| self.assertEqual(bool(use_ssl), my_connect.socket.__class__ is ssl.SSLSocket) | ||
| @contextlib.contextmanager | ||
| def _setup_rodsuser_and_optional_pw(self, name, make_irods_pw=False): | ||
| try: | ||
| self.admin.users.create(name, "rodsuser") | ||
| if make_irods_pw: | ||
| self.admin.users.modify(name, "password", TEST_IRODS_PW) | ||
| yield | ||
| finally: | ||
| self.admin.users.remove(name) | ||
| def tst0(self, ssl_opt, auth_opt, env_opt, name=TEST_RODS_USER, make_irods_pw=False): | ||
| session = None | ||
| _auth_opt = auth_opt | ||
| if auth_opt in ("pam", "pam_password"): | ||
| auth_opt = self.PAM_SCHEME_STRING | ||
| with self._setup_rodsuser_and_optional_pw(name=name, make_irods_pw=make_irods_pw): | ||
| self.envdirs = self.create_env_dirs() | ||
| if not self.envdirs: | ||
| raise RuntimeError("Could not create one or more client environments") | ||
| auth_opt_explicit = "native" if _auth_opt == "" else _auth_opt | ||
| verbosity = False | ||
| # verbosity='' # -- debug - sanity check by printing out options applied | ||
| out = {"": ""} | ||
| if env_opt: | ||
| with ( | ||
| self.setenv("IRODS_ENVIRONMENT_FILE", json_env_fullpath(auth_opt_explicit)) as env_file, | ||
| self.setenv("IRODS_AUTHENTICATION_FILE", secrets_fullpath(auth_opt_explicit)), | ||
| ): | ||
| cli_env_extras = {} if not (ssl_opt) else dict(CLIENT_OPTIONS_FOR_SSL) | ||
| if auth_opt: | ||
| cli_env_extras.update(irods_authentication_scheme=auth_opt) | ||
| remove = [] | ||
| else: | ||
| remove = [regex("authentication_")] | ||
| with helpers.file_backed_up(env_file): | ||
| json_file_update(env_file, keys_to_delete=remove, **cli_env_extras) | ||
| with pam_password_in_plaintext(nop=ssl_opt): | ||
| session = iRODSSession(irods_env_file=env_file) | ||
| with open(env_file) as f: | ||
| out = json.load(f) | ||
| self.validate_session(session, verbose=verbosity, ssl=ssl_opt) | ||
| session.cleanup() | ||
| out["ARGS"] = "no" | ||
| else: | ||
| session_options = {} | ||
| if auth_opt: | ||
| session_options.update(authentication_scheme=auth_opt) | ||
| if ssl_opt: | ||
| SSL_cert = CLIENT_OPTIONS_FOR_SSL["irods_ssl_ca_certificate_file"] | ||
| session_options.update( | ||
| ssl_context=ssl.create_default_context( | ||
| purpose=ssl.Purpose.SERVER_AUTH, | ||
| capath=None, | ||
| cadata=None, | ||
| cafile=SSL_cert, | ||
| ), | ||
| **CLIENT_OPTIONS_FOR_SSL, | ||
| ) | ||
| lookup = self.user_auth_envs[".irods." + ("native" if not (_auth_opt) else _auth_opt)] | ||
| with pam_password_in_plaintext(nop=ssl_opt): | ||
| session = iRODSSession( | ||
| host=gethostname(), | ||
| user=lookup["USER"], | ||
| zone="tempZone", | ||
| password=lookup["PASSWORD"], | ||
| port=1247, | ||
| **session_options, | ||
| ) | ||
| out = session_options | ||
| self.validate_session(session, verbose=verbosity, ssl=ssl_opt) | ||
| session.cleanup() | ||
| out["ARGS"] = "yes" | ||
| if verbosity == "": | ||
| print("--- ssl:", ssl_opt, "/ auth:", repr(auth_opt), "/ env:", env_opt) | ||
| print( | ||
| "--- > ", | ||
| json.dumps({k: v for k, v in out.items() if k != "ssl_context"}, indent=4), | ||
| ) | ||
| print("---") | ||
| if session: | ||
| session.cleanup() | ||
| return session | ||
| # == test defaulting to 'native' | ||
| def test_01(self): | ||
| self.tst0(ssl_opt=True, auth_opt="", env_opt=False, make_irods_pw=True) | ||
| def test_02(self): | ||
| self.tst0(ssl_opt=False, auth_opt="", env_opt=False, make_irods_pw=True) | ||
| def test_03(self): | ||
| self.tst0(ssl_opt=True, auth_opt="", env_opt=True, make_irods_pw=True) | ||
| def test_04(self): | ||
| self.tst0(ssl_opt=False, auth_opt="", env_opt=True, make_irods_pw=True) | ||
| # == test explicit scheme 'native' | ||
| def test_1(self): | ||
| self.tst0(ssl_opt=True, auth_opt="native", env_opt=False, make_irods_pw=True) | ||
| def test_2(self): | ||
| self.tst0(ssl_opt=False, auth_opt="native", env_opt=False, make_irods_pw=True) | ||
| def test_3(self): | ||
| self.tst0(ssl_opt=True, auth_opt="native", env_opt=True, make_irods_pw=True) | ||
| def test_4(self): | ||
| self.tst0(ssl_opt=False, auth_opt="native", env_opt=True, make_irods_pw=True) | ||
| # == test explicit scheme 'pam' | ||
| def test_5(self): | ||
| self.tst0(ssl_opt=True, auth_opt="pam", env_opt=False) | ||
| def test_6(self): | ||
| if self.admin.server_version >= (5,): | ||
| self.skipTest("iRODS 5 does not permit sending the raw PAM password on an unencrypted connection.") | ||
| try: | ||
| session = self.tst0(ssl_opt=False, auth_opt="pam", env_opt=False) | ||
| except PlainTextPAMPasswordError: | ||
| pass | ||
| else: | ||
| # -- no exception raised (this is expected behavior in 4.3+ with the new authentication framework, | ||
| # but for 4.2 and previous, we expect the PlainTextPAMPasswordError to be raised. | ||
| if session.server_version_without_auth() < (4, 3): | ||
| self.fail("PlainTextPAMPasswordError should have been raised") | ||
| def test_7(self): | ||
| self.tst0(ssl_opt=True, auth_opt="pam", env_opt=True) | ||
| def test_8(self): | ||
| self.tst0(ssl_opt=False, auth_opt="pam", env_opt=True) | ||
| @unittest.skipUnless( | ||
| TEST_PAM_PW_OVERRIDE, | ||
| "Skipping unless pam password is overridden (e.g. to test special characters)", | ||
| ) | ||
| def test_escaped_pam_password_chars__362(self): | ||
| with self._setup_rodsuser_and_optional_pw(name=TEST_RODS_USER): | ||
| context = ssl._create_unverified_context( | ||
| purpose=ssl.Purpose.SERVER_AUTH, | ||
| capath=None, | ||
| cadata=None, | ||
| cafile=None, | ||
| ) | ||
| ssl_settings = { | ||
| "client_server_negotiation": "request_server_negotiation", | ||
| "client_server_policy": "CS_NEG_REQUIRE", | ||
| "encryption_algorithm": "AES-256-CBC", | ||
| "encryption_key_size": 32, | ||
| "encryption_num_hash_rounds": 16, | ||
| "encryption_salt_size": 8, | ||
| "ssl_ca_certificate_file": "/etc/irods/ssl/irods.crt", | ||
| "ssl_context": context, | ||
| } | ||
| irods_session = iRODSSession( | ||
| host=self.admin.host, | ||
| port=self.admin.port, | ||
| zone=self.admin.zone, | ||
| user=TEST_RODS_USER, | ||
| password=TEST_PAM_PW_OVERRIDE, | ||
| authentication_scheme="pam", | ||
| **ssl_settings, | ||
| ) | ||
| home_coll = "/{0.zone}/home/{0.username}".format(irods_session) | ||
| self.assertEqual(irods_session.collections.get(home_coll).path, home_coll) | ||
| class TestAnonymousUser(unittest.TestCase): | ||
| def setUp(self): | ||
| admin = self.admin = helpers.make_session() | ||
| user = self.user = admin.users.create("anonymous", "rodsuser", admin.zone) | ||
| self.home = "/{admin.zone}/home/{user.name}".format(**locals()) | ||
| admin.collections.create(self.home) | ||
| acl = iRODSAccess("own", self.home, user.name) | ||
| admin.acls.set(acl, admin=True) | ||
| self.env_file = os.path.expanduser("~/.irods.anon/irods_environment.json") | ||
| self.env_dir = os.path.dirname(self.env_file) | ||
| self.auth_file = os.path.expanduser("~/.irods.anon/.irodsA") | ||
| os.mkdir(os.path.dirname(self.env_file)) | ||
| json.dump( | ||
| { | ||
| "irods_host": admin.host, | ||
| "irods_port": admin.port, | ||
| "irods_user_name": user.name, | ||
| "irods_zone_name": admin.zone, | ||
| }, | ||
| open(self.env_file, "w"), | ||
| indent=4, | ||
| ) | ||
| def tearDown(self): | ||
| self.admin.collections.remove(self.home, recurse=True, force=True) | ||
| self.admin.users.remove(self.user.name) | ||
| shutil.rmtree(self.env_dir, ignore_errors=True) | ||
| def test_login_from_environment(self): | ||
| orig_env = os.environ.copy() | ||
| try: | ||
| os.environ["IRODS_ENVIRONMENT_FILE"] = self.env_file | ||
| os.environ["IRODS_AUTHENTICATION_FILE"] = self.auth_file | ||
| ses = helpers.make_session() | ||
| ses.collections.get(self.home) | ||
| finally: | ||
| os.environ.clear() | ||
| os.environ.update(orig_env) | ||
| class TestMiscellaneous(unittest.TestCase): | ||
| def test_nonanonymous_login_without_auth_file_fails__290(self): | ||
| ses = self.admin | ||
| if ses.users.get(ses.username).type != "rodsadmin": | ||
| self.skipTest("Only a rodsadmin may run this test.") | ||
| try: | ||
| ENV_DIR = tempfile.mkdtemp() | ||
| ses.users.create("bob", "rodsuser") | ||
| ses.users.modify("bob", "password", "bpass") | ||
| d = dict( | ||
| password="bpass", | ||
| user="bob", | ||
| host=ses.host, | ||
| port=ses.port, | ||
| zone=ses.zone, | ||
| ) | ||
| (bob_env, bob_auth) = helpers.make_environment_and_auth_files(ENV_DIR, **d) | ||
| login_options = { | ||
| "irods_env_file": bob_env, | ||
| "irods_authentication_file": bob_auth, | ||
| } | ||
| with helpers.make_session(**login_options) as s: | ||
| s.users.get("bob") | ||
| os.unlink(bob_auth) | ||
| # -- Check that we raise an appropriate exception pointing to the missing auth file path -- | ||
| with self.assertRaisesRegex(NonAnonymousLoginWithoutPassword, bob_auth): | ||
| with helpers.make_session(**login_options) as s: | ||
| s.users.get("bob") | ||
| finally: | ||
| try: | ||
| shutil.rmtree(ENV_DIR, ignore_errors=True) | ||
| ses.users.get("bob").remove() | ||
| except ex.UserDoesNotExist: | ||
| pass | ||
| def setUp(self): | ||
| admin = self.admin = helpers.make_session() | ||
| if admin.users.get(admin.username).type != "rodsadmin": | ||
| self.skipTest("need admin privilege") | ||
| admin.users.create("alice", "rodsuser") | ||
| def tearDown(self): | ||
| self.admin.users.remove("alice") | ||
| self.admin.cleanup() | ||
| def test_destruct_session_with_no_pool_315(self): | ||
| destruct_flag = [False] | ||
| class mySess(iRODSSession): | ||
| def __del__(self): | ||
| self.pool = None | ||
| super(mySess, self).__del__() # call parent destructor(s) - will raise | ||
| # an error before the #315 fix | ||
| destruct_flag[:] = [True] | ||
| admin = self.admin | ||
| admin.users.modify("alice", "password", "apass") | ||
| my_sess = mySess( | ||
| user="alice", | ||
| password="apass", | ||
| host=admin.host, | ||
| port=admin.port, | ||
| zone=admin.zone, | ||
| ) | ||
| my_sess.cleanup() | ||
| del my_sess | ||
| gc.collect() | ||
| self.assertEqual(destruct_flag, [True]) | ||
| def test_non_anon_native_login_omitting_password_fails_1__290(self): | ||
| # rodsuser with password unset | ||
| with self.assertRaises(ex.CAT_INVALID_USER): | ||
| self._non_anon_native_login_omitting_password_fails_N__290() | ||
| def test_non_anon_native_login_omitting_password_fails_2__290(self): | ||
| # rodsuser with a password set | ||
| self.admin.users.modify("alice", "password", "apass") | ||
| with self.assertRaises(ex.CAT_INVALID_AUTHENTICATION): | ||
| self._non_anon_native_login_omitting_password_fails_N__290() | ||
| def _non_anon_native_login_omitting_password_fails_N__290(self): | ||
| admin = self.admin | ||
| with iRODSSession(zone=admin.zone, port=admin.port, host=admin.host, user="alice") as alice: | ||
| alice.collections.get(helpers.home_collection(alice)) | ||
| class TestWithSSL(unittest.TestCase): | ||
| """ | ||
| The tests within this class should be run by an account other than the | ||
| service account. Otherwise there is risk of corrupting the server setup. | ||
| """ | ||
| def setUp(self): | ||
| if os.path.expanduser("~") == "/var/lib/irods": | ||
| self.skipTest("TestWithSSL may not be run by user irods") | ||
| if not os.path.exists("/etc/irods/ssl"): | ||
| self.skipTest("Running setup_ssl.py as irods user is prerequisite for this test.") | ||
| with helpers.make_session() as session: | ||
| if not session.host in ("localhost", socket.gethostname()): | ||
| self.skipTest("Test must be run co-resident with server") | ||
| def test_ssl_with_server_verify_set_to_none_281(self): | ||
| env_file = os.path.expanduser("~/.irods/irods_environment.json") | ||
| my_ssl_directory = "" | ||
| try: | ||
| with helpers.file_backed_up(env_file): | ||
| with open(env_file) as env_file_handle: | ||
| env = json.load(env_file_handle) | ||
| my_ssl_directory = tempfile.mkdtemp(dir=os.path.expanduser("~")) | ||
| # Elect for efficiency in DH param generation, eg. when setting up for testing. | ||
| create_ssl_dir(ssl_dir=my_ssl_directory, use_strong_primes_for_dh_generation=False) | ||
| settings_to_update = { | ||
| key: value.replace("/etc/irods/ssl", my_ssl_directory) | ||
| for key, value in env.items() | ||
| if type(value) is str and value.startswith("/etc/irods/ssl") | ||
| } | ||
| settings_to_update["irods_ssl_verify_server"] = "none" | ||
| env.update(settings_to_update) | ||
| with open(env_file, "w") as f: | ||
| json.dump(env, f) | ||
| with helpers.make_session() as session: | ||
| session.collections.get("/{session.zone}/home/{session.username}".format(**locals())) | ||
| finally: | ||
| if my_ssl_directory: | ||
| shutil.rmtree(my_ssl_directory) | ||
| if __name__ == "__main__": | ||
| # let the tests find the parent irods lib | ||
| sys.path.insert(0, os.path.abspath("../..")) | ||
| unittest.main() |
| #! /usr/bin/env python | ||
| import os | ||
| import sys | ||
| import tempfile | ||
| import unittest | ||
| import textwrap | ||
| import json | ||
| import shutil | ||
| import ssl | ||
| import irods.test.helpers as helpers | ||
| from irods.connection import Connection | ||
| from irods.session import iRODSSession, NonAnonymousLoginWithoutPassword | ||
| from irods.rule import Rule | ||
| from irods.models import User | ||
| from socket import gethostname | ||
| from irods.password_obfuscation import encode as pw_encode | ||
| from irods.connection import PlainTextPAMPasswordError | ||
| from irods.access import iRODSAccess | ||
| import irods.exception as ex | ||
| import contextlib | ||
| import socket | ||
| from re import compile as regex | ||
| from typing import Dict, Optional | ||
| import gc | ||
| from irods.test.setup_ssl import create_ssl_dir | ||
| # | ||
| # Allow override to specify the PAM password in effect for the test rodsuser. | ||
| # | ||
| TEST_PAM_PW_OVERRIDE = os.environ.get("PYTHON_IRODSCLIENT_TEST_PAM_PW_OVERRIDE", "") | ||
| TEST_PAM_PW = TEST_PAM_PW_OVERRIDE or "test123" | ||
| TEST_IRODS_PW = "apass" | ||
| TEST_RODS_USER = "alissa" | ||
| from re import Pattern as regex_type | ||
| def json_file_update(fname, keys_to_delete=(), **kw): | ||
| with open(fname, "r") as f: | ||
| j = json.load(f) | ||
| j.update(**kw) | ||
| for k in keys_to_delete: | ||
| if k in j: | ||
| del j[k] | ||
| elif isinstance(k, regex_type): | ||
| jk = [i for i in j.keys() if k.search(i)] | ||
| for ky in jk: | ||
| del j[ky] | ||
| with open(fname, "w") as out: | ||
| json.dump(j, out, indent=4) | ||
| def env_dir_fullpath(authtype): | ||
| return os.path.join(os.environ["HOME"], ".irods." + authtype) | ||
| def json_env_fullpath(authtype): | ||
| return os.path.join(env_dir_fullpath(authtype), "irods_environment.json") | ||
| def secrets_fullpath(authtype): | ||
| return os.path.join(env_dir_fullpath(authtype), ".irodsA") | ||
| RODSADMIN_ENV_PATH = os.path.expanduser("~/.irods/irods_environment.json") | ||
| SERVER_ENV_SSL_SETTINGS = { | ||
| "irods_ssl_certificate_chain_file": "/etc/irods/ssl/irods.crt", | ||
| "irods_ssl_certificate_key_file": "/etc/irods/ssl/irods.key", | ||
| "irods_ssl_dh_params_file": "/etc/irods/ssl/dhparams.pem", | ||
| "irods_ssl_ca_certificate_file": "/etc/irods/ssl/irods.crt", | ||
| "irods_ssl_verify_server": "cert", | ||
| } | ||
| CLIENT_OPTIONS_FOR_SSL = { | ||
| "irods_client_server_policy": "CS_NEG_REQUIRE", | ||
| "irods_client_server_negotiation": "request_server_negotiation", | ||
| "irods_ssl_ca_certificate_file": "/etc/irods/ssl/irods.crt", | ||
| "irods_ssl_verify_server": "cert", | ||
| "irods_encryption_key_size": 16, | ||
| "irods_encryption_salt_size": 8, | ||
| "irods_encryption_num_hash_rounds": 16, | ||
| "irods_encryption_algorithm": "AES-256-CBC", | ||
| } | ||
| def client_env_keys_from_admin_env(user_name, auth_scheme=""): | ||
| cli_env = {} | ||
| with open(RODSADMIN_ENV_PATH) as f: | ||
| srv_env = json.load(f) | ||
| for k in ["irods_host", "irods_zone_name", "irods_port"]: | ||
| cli_env[k] = srv_env[k] | ||
| cli_env["irods_user_name"] = user_name | ||
| if auth_scheme: | ||
| cli_env["irods_authentication_scheme"] = auth_scheme | ||
| return cli_env | ||
| # For testing only! | ||
| # Note pam_password_in_plaintext* functions are for test only as they will allow transmitting passwords on a potentially | ||
| # interceptible, unencrypted channel. | ||
| @contextlib.contextmanager | ||
| def pam_password_in_plaintext_4_3(allow=True): | ||
| import irods.helpers | ||
| from irods.auth.pam_password import ENSURE_SSL_IS_ACTIVE | ||
| # We'll temporarily replace the original iRODSSession constructor with a new version which changes pam_password options | ||
| # to allow pam password SSL | ||
| old_init = iRODSSession.__init__ | ||
| def new_init(self, *arg, **kw): | ||
| old_init(self, *arg, **kw) | ||
| self.set_auth_option_for_scheme("pam_password", ENSURE_SSL_IS_ACTIVE, not (allow)) | ||
| with irods.helpers.temporarily_assign_attribute(iRODSSession, "__init__", new_init): | ||
| yield | ||
| @contextlib.contextmanager | ||
| def pam_password_in_plaintext_4_2(allow=True): | ||
| saved = bool(Connection.DISALLOWING_PAM_PLAINTEXT) | ||
| try: | ||
| Connection.DISALLOWING_PAM_PLAINTEXT = not (allow) | ||
| yield | ||
| finally: | ||
| Connection.DISALLOWING_PAM_PLAINTEXT = saved | ||
| @contextlib.contextmanager | ||
| def pam_password_in_plaintext(allow=True, nop=False): | ||
| if nop: | ||
| yield | ||
| return | ||
| with pam_password_in_plaintext_4_2(allow=allow): | ||
| with pam_password_in_plaintext_4_3(allow=allow): | ||
| yield | ||
| class TestLogins(unittest.TestCase): | ||
| """ | ||
| Ideally, these tests should move into CI, but that would require the server | ||
| (currently a different node than the client) to have SSL certs created and | ||
| enabled. | ||
| Until then, we require these tests to be run manually on a server node, | ||
| with: | ||
| python -m unittest "irods.test.login_auth_test[.XX[.YY]]' | ||
| Additionally: | ||
| 1. The PAM/SSL tests under the TestLogins class should be run on a | ||
| single-node iRODS system, by the service account user. This ensures | ||
| the /etc/irods directory is local and writable. | ||
| 2. ./setup_ssl.py (sets up SSL keys etc. in /etc/irods/ssl) should be run | ||
| first to create (or overwrite, if appropriate) the /etc/irods/ssl directory | ||
| and its contents. | ||
| 3. Must add & override configuration entries in /var/lib/irods/irods_environment | ||
| Per https://slides.com/irods/ugm2018-ssl-and-pam-configuration#/3/7 | ||
| """ | ||
| user_auth_envs = { | ||
| ".irods.pam": {"USER": TEST_RODS_USER, "PASSWORD": TEST_PAM_PW, "AUTH": "pam"}, | ||
| ".irods.native": { | ||
| "USER": TEST_RODS_USER, | ||
| "PASSWORD": TEST_IRODS_PW, | ||
| "AUTH": "native", | ||
| }, | ||
| } | ||
| env_save: Dict[str, Optional[str]] = {} | ||
| @contextlib.contextmanager | ||
| def setenv(self, var, newvalue): | ||
| try: | ||
| self.env_save[var] = os.environ.get(var, None) | ||
| os.environ[var] = newvalue | ||
| yield newvalue | ||
| finally: | ||
| oldvalue = self.env_save[var] | ||
| if oldvalue is None: | ||
| del os.environ[var] | ||
| else: | ||
| os.environ[var] = oldvalue | ||
| def create_env_dirs(self): | ||
| dirs = {} | ||
| retval = [] | ||
| # -- create environment configurations and secrets | ||
| with pam_password_in_plaintext(): | ||
| for dirname, lookup in self.user_auth_envs.items(): | ||
| if lookup["AUTH"] in ("pam", "pam_password"): | ||
| ses = iRODSSession( | ||
| host=gethostname(), | ||
| user=lookup["USER"], | ||
| zone="tempZone", | ||
| authentication_scheme=lookup["AUTH"], | ||
| password=lookup["PASSWORD"], | ||
| port=1247, | ||
| **( | ||
| {**SERVER_ENV_SSL_SETTINGS, **CLIENT_OPTIONS_FOR_SSL} | ||
| if self.admin.server_version >= (5,) | ||
| else {} | ||
| ), | ||
| ) | ||
| try: | ||
| pam_hashes = ses.pam_pw_negotiated | ||
| except AttributeError: | ||
| pam_hashes = [] | ||
| if not pam_hashes: | ||
| print("Warning ** PAM pw couldnt be generated") | ||
| break | ||
| scrambled_pw = pw_encode(pam_hashes[0]) | ||
| # elif lookup['AUTH'] == 'XXXXXX': # TODO: insert other authentication schemes here | ||
| elif lookup["AUTH"] in ("native", "", None): | ||
| scrambled_pw = pw_encode(lookup["PASSWORD"]) | ||
| cl_env = client_env_keys_from_admin_env(TEST_RODS_USER) | ||
| if lookup.get("AUTH", None) is not None: # - specify auth scheme only if given | ||
| cl_env["irods_authentication_scheme"] = lookup["AUTH"] | ||
| dirbase = os.path.join(os.environ["HOME"], dirname) | ||
| dirs[dirbase] = {"secrets": scrambled_pw, "client_environment": cl_env} | ||
| # -- create the environment directories and write into them the configurations just created | ||
| for absdir in dirs.keys(): | ||
| shutil.rmtree(absdir, ignore_errors=True) | ||
| os.mkdir(absdir) | ||
| with open(os.path.join(absdir, "irods_environment.json"), "w") as envfile: | ||
| envfile.write("{}") | ||
| json_file_update(envfile.name, **dirs[absdir]["client_environment"]) | ||
| with open(os.path.join(absdir, ".irodsA"), "w") as secrets_file: | ||
| secrets_file.write(dirs[absdir]["secrets"]) | ||
| os.chmod(secrets_file.name, 0o600) | ||
| retval = dirs.keys() | ||
| return retval | ||
| PAM_SCHEME_STRING = "pam" | ||
| @classmethod | ||
| def setUpClass(cls): | ||
| import irods.client_configuration as cfg | ||
| cls.admin = helpers.make_session() | ||
| if cls.admin.server_version >= (4, 3) and not cfg.legacy_auth.force_legacy_auth: | ||
| cls.PAM_SCHEME_STRING = cls.user_auth_envs[".irods.pam"]["AUTH"] = "pam_password" | ||
| @classmethod | ||
| def tearDownClass(cls): | ||
| cls.admin.cleanup() | ||
| def setUp(self): | ||
| super(TestLogins, self).setUp() | ||
| def tearDown(self): | ||
| for envdir in getattr(self, "envdirs", []): | ||
| shutil.rmtree(envdir, ignore_errors=True) | ||
| super(TestLogins, self).tearDown() | ||
| def validate_session(self, session, verbose=False, **options): | ||
| # - try to get the home collection | ||
| home_coll = "/{0.zone}/home/{0.username}".format(session) | ||
| self.assertTrue(session.collections.get(home_coll).path == home_coll) | ||
| if verbose: | ||
| print(home_coll) | ||
| # - check user is as expected | ||
| self.assertEqual(session.username, TEST_RODS_USER) | ||
| # - check socket type (normal vs SSL) against whether ssl requested | ||
| use_ssl = options.pop("ssl", None) | ||
| if use_ssl is not None: | ||
| my_connect = [s for s in (session.pool.active | session.pool.idle)][0] | ||
| self.assertEqual(bool(use_ssl), my_connect.socket.__class__ is ssl.SSLSocket) | ||
| @contextlib.contextmanager | ||
| def _setup_rodsuser_and_optional_pw(self, name, make_irods_pw=False): | ||
| try: | ||
| self.admin.users.create(name, "rodsuser") | ||
| if make_irods_pw: | ||
| self.admin.users.modify(name, "password", TEST_IRODS_PW) | ||
| yield | ||
| finally: | ||
| self.admin.users.remove(name) | ||
| def tst0(self, ssl_opt, auth_opt, env_opt, name=TEST_RODS_USER, make_irods_pw=False): | ||
| session = None | ||
| _auth_opt = auth_opt | ||
| if auth_opt in ("pam", "pam_password"): | ||
| auth_opt = self.PAM_SCHEME_STRING | ||
| with self._setup_rodsuser_and_optional_pw(name=name, make_irods_pw=make_irods_pw): | ||
| self.envdirs = self.create_env_dirs() | ||
| if not self.envdirs: | ||
| raise RuntimeError("Could not create one or more client environments") | ||
| auth_opt_explicit = "native" if _auth_opt == "" else _auth_opt | ||
| verbosity = False | ||
| # verbosity='' # -- debug - sanity check by printing out options applied | ||
| out = {"": ""} | ||
| if env_opt: | ||
| with ( | ||
| self.setenv("IRODS_ENVIRONMENT_FILE", json_env_fullpath(auth_opt_explicit)) as env_file, | ||
| self.setenv("IRODS_AUTHENTICATION_FILE", secrets_fullpath(auth_opt_explicit)), | ||
| ): | ||
| cli_env_extras = {} if not (ssl_opt) else dict(CLIENT_OPTIONS_FOR_SSL) | ||
| if auth_opt: | ||
| cli_env_extras.update(irods_authentication_scheme=auth_opt) | ||
| remove = [] | ||
| else: | ||
| remove = [regex("authentication_")] | ||
| with helpers.file_backed_up(env_file): | ||
| json_file_update(env_file, keys_to_delete=remove, **cli_env_extras) | ||
| with pam_password_in_plaintext(nop=ssl_opt): | ||
| session = iRODSSession(irods_env_file=env_file) | ||
| with open(env_file) as f: | ||
| out = json.load(f) | ||
| self.validate_session(session, verbose=verbosity, ssl=ssl_opt) | ||
| session.cleanup() | ||
| out["ARGS"] = "no" | ||
| else: | ||
| session_options = {} | ||
| if auth_opt: | ||
| session_options.update(authentication_scheme=auth_opt) | ||
| if ssl_opt: | ||
| SSL_cert = CLIENT_OPTIONS_FOR_SSL["irods_ssl_ca_certificate_file"] | ||
| session_options.update( | ||
| ssl_context=ssl.create_default_context( | ||
| purpose=ssl.Purpose.SERVER_AUTH, | ||
| capath=None, | ||
| cadata=None, | ||
| cafile=SSL_cert, | ||
| ), | ||
| **CLIENT_OPTIONS_FOR_SSL, | ||
| ) | ||
| lookup = self.user_auth_envs[".irods." + ("native" if not (_auth_opt) else _auth_opt)] | ||
| with pam_password_in_plaintext(nop=ssl_opt): | ||
| session = iRODSSession( | ||
| host=gethostname(), | ||
| user=lookup["USER"], | ||
| zone="tempZone", | ||
| password=lookup["PASSWORD"], | ||
| port=1247, | ||
| **session_options, | ||
| ) | ||
| out = session_options | ||
| self.validate_session(session, verbose=verbosity, ssl=ssl_opt) | ||
| session.cleanup() | ||
| out["ARGS"] = "yes" | ||
| if verbosity == "": | ||
| print("--- ssl:", ssl_opt, "/ auth:", repr(auth_opt), "/ env:", env_opt) | ||
| print( | ||
| "--- > ", | ||
| json.dumps({k: v for k, v in out.items() if k != "ssl_context"}, indent=4), | ||
| ) | ||
| print("---") | ||
| if session: | ||
| session.cleanup() | ||
| return session | ||
| # == test defaulting to 'native' | ||
| def test_01(self): | ||
| self.tst0(ssl_opt=True, auth_opt="", env_opt=False, make_irods_pw=True) | ||
| def test_02(self): | ||
| self.tst0(ssl_opt=False, auth_opt="", env_opt=False, make_irods_pw=True) | ||
| def test_03(self): | ||
| self.tst0(ssl_opt=True, auth_opt="", env_opt=True, make_irods_pw=True) | ||
| def test_04(self): | ||
| self.tst0(ssl_opt=False, auth_opt="", env_opt=True, make_irods_pw=True) | ||
| # == test explicit scheme 'native' | ||
| def test_1(self): | ||
| self.tst0(ssl_opt=True, auth_opt="native", env_opt=False, make_irods_pw=True) | ||
| def test_2(self): | ||
| self.tst0(ssl_opt=False, auth_opt="native", env_opt=False, make_irods_pw=True) | ||
| def test_3(self): | ||
| self.tst0(ssl_opt=True, auth_opt="native", env_opt=True, make_irods_pw=True) | ||
| def test_4(self): | ||
| self.tst0(ssl_opt=False, auth_opt="native", env_opt=True, make_irods_pw=True) | ||
| # == test explicit scheme 'pam' | ||
| def test_5(self): | ||
| self.tst0(ssl_opt=True, auth_opt="pam", env_opt=False) | ||
| def test_6(self): | ||
| if self.admin.server_version >= (5,): | ||
| self.skipTest("iRODS 5 does not permit sending the raw PAM password on an unencrypted connection.") | ||
| try: | ||
| session = self.tst0(ssl_opt=False, auth_opt="pam", env_opt=False) | ||
| except PlainTextPAMPasswordError: | ||
| pass | ||
| else: | ||
| # -- no exception raised (this is expected behavior in 4.3+ with the new authentication framework, | ||
| # but for 4.2 and previous, we expect the PlainTextPAMPasswordError to be raised. | ||
| if session.server_version_without_auth() < (4, 3): | ||
| self.fail("PlainTextPAMPasswordError should have been raised") | ||
| def test_7(self): | ||
| self.tst0(ssl_opt=True, auth_opt="pam", env_opt=True) | ||
| def test_8(self): | ||
| self.tst0(ssl_opt=False, auth_opt="pam", env_opt=True) | ||
| @unittest.skipUnless( | ||
| TEST_PAM_PW_OVERRIDE, | ||
| "Skipping unless pam password is overridden (e.g. to test special characters)", | ||
| ) | ||
| def test_escaped_pam_password_chars__362(self): | ||
| with self._setup_rodsuser_and_optional_pw(name=TEST_RODS_USER): | ||
| context = ssl._create_unverified_context( | ||
| purpose=ssl.Purpose.SERVER_AUTH, | ||
| capath=None, | ||
| cadata=None, | ||
| cafile=None, | ||
| ) | ||
| ssl_settings = { | ||
| "client_server_negotiation": "request_server_negotiation", | ||
| "client_server_policy": "CS_NEG_REQUIRE", | ||
| "encryption_algorithm": "AES-256-CBC", | ||
| "encryption_key_size": 32, | ||
| "encryption_num_hash_rounds": 16, | ||
| "encryption_salt_size": 8, | ||
| "ssl_ca_certificate_file": "/etc/irods/ssl/irods.crt", | ||
| "ssl_context": context, | ||
| } | ||
| irods_session = iRODSSession( | ||
| host=self.admin.host, | ||
| port=self.admin.port, | ||
| zone=self.admin.zone, | ||
| user=TEST_RODS_USER, | ||
| password=TEST_PAM_PW_OVERRIDE, | ||
| authentication_scheme="pam", | ||
| **ssl_settings, | ||
| ) | ||
| home_coll = "/{0.zone}/home/{0.username}".format(irods_session) | ||
| self.assertEqual(irods_session.collections.get(home_coll).path, home_coll) | ||
| class TestAnonymousUser(unittest.TestCase): | ||
| def setUp(self): | ||
| admin = self.admin = helpers.make_session() | ||
| user = self.user = admin.users.create("anonymous", "rodsuser", admin.zone) | ||
| self.home = "/{admin.zone}/home/{user.name}".format(**locals()) | ||
| admin.collections.create(self.home) | ||
| acl = iRODSAccess("own", self.home, user.name) | ||
| admin.acls.set(acl, admin=True) | ||
| self.env_file = os.path.expanduser("~/.irods.anon/irods_environment.json") | ||
| self.env_dir = os.path.dirname(self.env_file) | ||
| self.auth_file = os.path.expanduser("~/.irods.anon/.irodsA") | ||
| os.mkdir(os.path.dirname(self.env_file)) | ||
| json.dump( | ||
| { | ||
| "irods_host": admin.host, | ||
| "irods_port": admin.port, | ||
| "irods_user_name": user.name, | ||
| "irods_zone_name": admin.zone, | ||
| }, | ||
| open(self.env_file, "w"), | ||
| indent=4, | ||
| ) | ||
| def tearDown(self): | ||
| self.admin.collections.remove(self.home, recurse=True, force=True) | ||
| self.admin.users.remove(self.user.name) | ||
| shutil.rmtree(self.env_dir, ignore_errors=True) | ||
| def test_login_from_environment(self): | ||
| orig_env = os.environ.copy() | ||
| try: | ||
| os.environ["IRODS_ENVIRONMENT_FILE"] = self.env_file | ||
| os.environ["IRODS_AUTHENTICATION_FILE"] = self.auth_file | ||
| ses = helpers.make_session() | ||
| ses.collections.get(self.home) | ||
| finally: | ||
| os.environ.clear() | ||
| os.environ.update(orig_env) | ||
| class TestMiscellaneous(unittest.TestCase): | ||
| def test_nonanonymous_login_without_auth_file_fails__290(self): | ||
| ses = self.admin | ||
| if ses.users.get(ses.username).type != "rodsadmin": | ||
| self.skipTest("Only a rodsadmin may run this test.") | ||
| try: | ||
| ENV_DIR = tempfile.mkdtemp() | ||
| ses.users.create("bob", "rodsuser") | ||
| ses.users.modify("bob", "password", "bpass") | ||
| d = dict( | ||
| password="bpass", | ||
| user="bob", | ||
| host=ses.host, | ||
| port=ses.port, | ||
| zone=ses.zone, | ||
| ) | ||
| (bob_env, bob_auth) = helpers.make_environment_and_auth_files(ENV_DIR, **d) | ||
| login_options = { | ||
| "irods_env_file": bob_env, | ||
| "irods_authentication_file": bob_auth, | ||
| } | ||
| with helpers.make_session(**login_options) as s: | ||
| s.users.get("bob") | ||
| os.unlink(bob_auth) | ||
| # -- Check that we raise an appropriate exception pointing to the missing auth file path -- | ||
| with self.assertRaisesRegex(NonAnonymousLoginWithoutPassword, bob_auth): | ||
| with helpers.make_session(**login_options) as s: | ||
| s.users.get("bob") | ||
| finally: | ||
| try: | ||
| shutil.rmtree(ENV_DIR, ignore_errors=True) | ||
| ses.users.get("bob").remove() | ||
| except ex.UserDoesNotExist: | ||
| pass | ||
| def setUp(self): | ||
| admin = self.admin = helpers.make_session() | ||
| if admin.users.get(admin.username).type != "rodsadmin": | ||
| self.skipTest("need admin privilege") | ||
| admin.users.create("alice", "rodsuser") | ||
| def tearDown(self): | ||
| self.admin.users.remove("alice") | ||
| self.admin.cleanup() | ||
| def test_destruct_session_with_no_pool_315(self): | ||
| destruct_flag = [False] | ||
| class mySess(iRODSSession): | ||
| def __del__(self): | ||
| self.pool = None | ||
| super(mySess, self).__del__() # call parent destructor(s) - will raise | ||
| # an error before the #315 fix | ||
| destruct_flag[:] = [True] | ||
| admin = self.admin | ||
| admin.users.modify("alice", "password", "apass") | ||
| my_sess = mySess( | ||
| user="alice", | ||
| password="apass", | ||
| host=admin.host, | ||
| port=admin.port, | ||
| zone=admin.zone, | ||
| ) | ||
| my_sess.cleanup() | ||
| del my_sess | ||
| gc.collect() | ||
| self.assertEqual(destruct_flag, [True]) | ||
| def test_non_anon_native_login_omitting_password_fails_1__290(self): | ||
| # rodsuser with password unset | ||
| with self.assertRaises(ex.CAT_INVALID_USER): | ||
| self._non_anon_native_login_omitting_password_fails_N__290() | ||
| def test_non_anon_native_login_omitting_password_fails_2__290(self): | ||
| # rodsuser with a password set | ||
| self.admin.users.modify("alice", "password", "apass") | ||
| with self.assertRaises(ex.CAT_INVALID_AUTHENTICATION): | ||
| self._non_anon_native_login_omitting_password_fails_N__290() | ||
| def _non_anon_native_login_omitting_password_fails_N__290(self): | ||
| admin = self.admin | ||
| with iRODSSession(zone=admin.zone, port=admin.port, host=admin.host, user="alice") as alice: | ||
| alice.collections.get(helpers.home_collection(alice)) | ||
| class TestWithSSL(unittest.TestCase): | ||
| """ | ||
| The tests within this class should be run by an account other than the | ||
| service account. Otherwise there is risk of corrupting the server setup. | ||
| """ | ||
| def setUp(self): | ||
| if os.path.expanduser("~") == "/var/lib/irods": | ||
| self.skipTest("TestWithSSL may not be run by user irods") | ||
| if not os.path.exists("/etc/irods/ssl"): | ||
| self.skipTest("Running setup_ssl.py as irods user is prerequisite for this test.") | ||
| with helpers.make_session() as session: | ||
| if not session.host in ("localhost", socket.gethostname()): | ||
| self.skipTest("Test must be run co-resident with server") | ||
| def test_ssl_with_server_verify_set_to_none_281(self): | ||
| env_file = os.path.expanduser("~/.irods/irods_environment.json") | ||
| my_ssl_directory = "" | ||
| try: | ||
| with helpers.file_backed_up(env_file): | ||
| with open(env_file) as env_file_handle: | ||
| env = json.load(env_file_handle) | ||
| my_ssl_directory = tempfile.mkdtemp(dir=os.path.expanduser("~")) | ||
| # Elect for efficiency in DH param generation, eg. when setting up for testing. | ||
| create_ssl_dir(ssl_dir=my_ssl_directory, use_strong_primes_for_dh_generation=False) | ||
| settings_to_update = { | ||
| key: value.replace("/etc/irods/ssl", my_ssl_directory) | ||
| for key, value in env.items() | ||
| if type(value) is str and value.startswith("/etc/irods/ssl") | ||
| } | ||
| settings_to_update["irods_ssl_verify_server"] = "none" | ||
| env.update(settings_to_update) | ||
| with open(env_file, "w") as f: | ||
| json.dump(env, f) | ||
| with helpers.make_session() as session: | ||
| session.collections.get("/{session.zone}/home/{session.username}".format(**locals())) | ||
| finally: | ||
| if my_ssl_directory: | ||
| shutil.rmtree(my_ssl_directory) | ||
| if __name__ == "__main__": | ||
| # let the tests find the parent irods lib | ||
| sys.path.insert(0, os.path.abspath("../..")) | ||
| unittest.main() |
| import os | ||
| import re | ||
| import signal | ||
| import subprocess | ||
| import sys | ||
| import tempfile | ||
| import irods.helpers | ||
| from irods.test import modules as test_modules | ||
| from irods.parallel import abort_parallel_transfers | ||
| OBJECT_SIZE = 4 * 1024**3 | ||
| OBJECT_NAME = "data_get_issue__722" | ||
| LOCAL_TEMPFILE_NAME = "data_object_for_issue_722.dat" | ||
| def test(test_case, signal_names=("SIGTERM", "SIGINT")): | ||
| """Creates a child process executing a long get() and ensures the process can be | ||
| terminated using SIGINT or SIGTERM. | ||
| """ | ||
| from .tools import wait_till_true | ||
| program = os.path.join(test_modules.__path__[0], os.path.basename(__file__)) | ||
| for signal_name in signal_names: | ||
| with test_case.subTest(f"Testing with signal {signal_name}"): | ||
| # Call into this same module as a command. This will initiate another Python process that | ||
| # performs a lengthy data object "get" operation (see the main body of the script, below.) | ||
| process = subprocess.Popen( | ||
| [sys.executable, program], | ||
| stderr=subprocess.PIPE, | ||
| stdout=subprocess.PIPE, | ||
| text=True, | ||
| ) | ||
| # Wait for download process to reach the point of spawning data transfer threads. In Python 3.9+ versions | ||
| # of the concurrent.futures module, these are nondaemon threads and will block the exit of the main thread | ||
| # unless measures are taken. | ||
| localfile = process.stdout.readline().strip() | ||
| # Use timeout of 10 minutes for test transfer, which should be more than enough. | ||
| test_case.assertTrue( | ||
| wait_till_true( | ||
| lambda: os.path.exists(localfile) and os.stat(localfile).st_size > OBJECT_SIZE // 2, | ||
| ), | ||
| "Parallel download from data_objects.get() probably experienced a fatal error before spawning auxiliary data transfer threads.", | ||
| ) | ||
| sig = getattr(signal, signal_name) | ||
| signal_offset_return_code = lambda s: 128 - s if s < 0 else s | ||
| signal_plus_128 = lambda sig: 128 + sig | ||
| # Interrupt the subprocess with the given signal. | ||
| process.send_signal(sig) | ||
| # Assert that this signal is what killed the subprocess, rather than a timed out process "wait" or a natural exit | ||
| # due to misproper or incomplete handling of the signal. | ||
| try: | ||
| translated_return_code = signal_offset_return_code(process.wait(timeout=15)) | ||
| test_case.assertIn( | ||
| translated_return_code, | ||
| [1, signal_plus_128(sig)], | ||
| f"Expected subprocess return code of {signal_plus_128(sig) = }; got {translated_return_code = }", | ||
| ) | ||
| except subprocess.TimeoutExpired: | ||
| test_case.fail( | ||
| "Subprocess timed out before terminating. " | ||
| "Non-daemon thread(s) probably prevented subprocess's main thread from exiting." | ||
| ) | ||
| # Assert that in the case of SIGINT, the process registered a KeyboardInterrupt. | ||
| if sig == signal.SIGINT: | ||
| test_case.assertTrue( | ||
| re.search("KeyboardInterrupt", process.stderr.read()), | ||
| "Did not find expected string 'KeyboardInterrupt' in log output.", | ||
| ) | ||
| if __name__ == "__main__": | ||
| # These lines are run only if the module is launched as a process. | ||
| session = irods.helpers.make_session() | ||
| hc = irods.helpers.home_collection(session) | ||
| TESTFILE_FILL = b"_" * (1024 * 1024) | ||
| object_path = f"{hc}/{OBJECT_NAME}" | ||
| # Create the object to be downloaded. | ||
| with session.data_objects.open(object_path, "w") as f: | ||
| for y in range(OBJECT_SIZE // len(TESTFILE_FILL)): | ||
| f.write(TESTFILE_FILL) | ||
| local_path = None | ||
| # Establish where (ie absolute path) to place the downloaded file, i.e. the get() target. | ||
| try: | ||
| with tempfile.NamedTemporaryFile(prefix="local_file_issue_722.dat", delete=True) as t: | ||
| local_path = t.name | ||
| # Tell the parent process the name of the local file, ie the result of the "get" from iRODS. | ||
| # That parent process is the unittest, which will use the filename to verify the threads are started | ||
| # and we're somewhere mid-transfer. | ||
| print(local_path) | ||
| sys.stdout.flush() | ||
| def handler(sig, *_): | ||
| abort_parallel_transfers() | ||
| if sig == signal.SIGTERM: | ||
| os._exit(128 + sig) | ||
| signal.signal(signal.SIGTERM, handler) | ||
| try: | ||
| # download the object | ||
| session.data_objects.get(object_path, local_path) | ||
| except KeyboardInterrupt: | ||
| abort_parallel_transfers() | ||
| raise | ||
| finally: | ||
| # Clean up, whether or not the download succeeded. | ||
| if local_path is not None and os.path.exists(local_path): | ||
| os.unlink(local_path) | ||
| if session.data_objects.exists(object_path): | ||
| session.data_objects.unlink(object_path, force=True) |
| import datetime | ||
| import os | ||
| import re | ||
| import signal | ||
| import subprocess | ||
| import sys | ||
| import irods.helpers | ||
| from irods.session import iRODSSession | ||
| from irods.test.helpers import unique_name | ||
| from irods.test import modules as test_modules | ||
| from irods.parallel import abort_parallel_transfers | ||
| OBJECT_SIZE = 4 * 1024**3 | ||
| LOCAL_TEMPFILE_NAME = "data_object_for_issue_722.dat" | ||
| def test(test_case, signal_names=("SIGTERM", "SIGINT")): | ||
| """Creates a child process executing a long put() and ensures the process can be terminated using SIGINT or SIGTERM.""" | ||
| from .tools import wait_till_true | ||
| program = os.path.join(test_modules.__path__[0], os.path.basename(__file__)) | ||
| session = getattr(test_case, "sess", None) or irods.helpers.make_session() | ||
| for signal_name in signal_names: | ||
| with test_case.subTest(f"Testing with signal {signal_name}"): | ||
| try: | ||
| # Call into this same module as a command. This will initiate another Python process that | ||
| # performs a lengthy data object "get" operation (see the main body of the script, below.) | ||
| process = subprocess.Popen( | ||
| # -k: Keep object around for replica status testing. | ||
| [sys.executable, program, "-k"], | ||
| stderr=subprocess.PIPE, | ||
| stdout=subprocess.PIPE, | ||
| text=True, | ||
| ) | ||
| # Wait for download process to reach the point of spawning data transfer threads. In Python 3.9+ versions | ||
| # of the concurrent.futures module, these are non-daemon threads and will block the exit of the main thread | ||
| # unless measures are taken. | ||
| logical_path = process.stdout.readline().strip() | ||
| # Use timeout of 10 minutes for test transfer, which should be more than enough. | ||
| test_case.assertTrue( | ||
| wait_till_true( | ||
| lambda: ( | ||
| session.data_objects.exists(logical_path) | ||
| and named_irods_data_object(session, logical_path, delete=False).data.size | ||
| > OBJECT_SIZE // 2 | ||
| ), | ||
| ), | ||
| "Parallel download from data_objects.put() probably experienced a fatal error before spawning auxiliary data transfer threads.", | ||
| ) | ||
| sig = getattr(signal, signal_name) | ||
| signal_offset_return_code = lambda s: 128 - s if s < 0 else s | ||
| signal_plus_128 = lambda sig: 128 + sig | ||
| # Interrupt the subprocess with the given signal. | ||
| process.send_signal(sig) | ||
| # Assert that this signal is what killed the subprocess, rather than a timed out process "wait" or a natural exit | ||
| # due to misproper or incomplete handling of the signal. | ||
| try: | ||
| translated_return_code = signal_offset_return_code(process.wait(timeout=15)) | ||
| test_case.assertIn( | ||
| translated_return_code, | ||
| [1, signal_plus_128(sig)], | ||
| f"Expected subprocess return code of {signal_plus_128(sig) = }; got {translated_return_code = }", | ||
| ) | ||
| except subprocess.TimeoutExpired: | ||
| test_case.fail( | ||
| "Subprocess timed out before terminating. " | ||
| "Non-daemon thread(s) probably prevented subprocess's main thread from exiting." | ||
| ) | ||
| # Assert that in the case of SIGINT, the process registered a KeyboardInterrupt. | ||
| if sig == signal.SIGINT: | ||
| test_case.assertTrue( | ||
| re.search("KeyboardInterrupt", process.stderr.read()), | ||
| "Did not find expected string 'KeyboardInterrupt' in log output.", | ||
| ) | ||
| # Assert that the status is left as not LOCKED. | ||
| test_case.assertTrue( | ||
| wait_till_true(lambda: int(session.data_objects.get(logical_path).replica_status) < 2) | ||
| ) | ||
| finally: | ||
| if logical_path and (d := irods.helpers.get_data_object(session, logical_path)): | ||
| d.unlink(force=True) | ||
| class named_irods_data_object: | ||
| def __init__(self, /, session: iRODSSession, path: str = "", delete: bool = True): | ||
| self.sess = session | ||
| self.delete = delete | ||
| if not path: | ||
| path = irods.helpers.home_collection(session) + "/" + unique_name(datetime.datetime.now()) | ||
| self.path = path | ||
| @property | ||
| def data(self): | ||
| return irods.helpers.get_data_object(self.sess, self.path) | ||
| def __del__(self): | ||
| if self.delete: | ||
| self.remove() | ||
| def remove(self): | ||
| if d := self.data: | ||
| d.unlink(force=True) | ||
| def create(self): | ||
| self.sess.data_objects.create(self.path) | ||
| return self | ||
| if __name__ == "__main__": | ||
| import getopt | ||
| opts, _ = getopt.getopt(sys.argv[1:], "k") | ||
| keep_data_object = "-k" in (_[0] for _ in opts) | ||
| # These lines are run only if the module is launched as a process. | ||
| test_session = irods.helpers.make_session() | ||
| hc = irods.helpers.home_collection(test_session) | ||
| TESTFILE_FILL = b"_" * (1024 * 1024) | ||
| object_path = named_irods_data_object(test_session, delete=True).create().path | ||
| local_path = object_path.split("/")[-1] | ||
| # Create the object to uploaded. | ||
| with open(local_path, "wb") as f: | ||
| for y in range(OBJECT_SIZE // len(TESTFILE_FILL)): | ||
| f.write(TESTFILE_FILL) | ||
| try: | ||
| # Tell the parent process the name of the data object logical path, the target of the "put" to iRODS. | ||
| # That parent process is the unittest, which will use the logical path to verify the threads are started | ||
| # and we're somewhere mid-transfer. | ||
| print(object_path) | ||
| sys.stdout.flush() | ||
| def handler(sig, *_): | ||
| abort_parallel_transfers() | ||
| if sig == signal.SIGTERM: | ||
| os._exit(128 + sig) | ||
| signal.signal(signal.SIGTERM, handler) | ||
| try: | ||
| # Upload the object | ||
| test_session.data_objects.put(local_path, object_path) | ||
| except KeyboardInterrupt: | ||
| abort_parallel_transfers() | ||
| raise | ||
| finally: | ||
| # Clean up, whether or not the upload succeeded. | ||
| if local_path is not None and os.path.exists(local_path): | ||
| os.unlink(local_path) | ||
| if not keep_data_object: | ||
| named_irods_data_object(test_session, path=object_path, delete=True) |
| import time | ||
| _clock_polling_interval = max(0.01, time.clock_getres(time.CLOCK_BOOTTIME)) | ||
| LARGE_TEST_TIMEOUT = 10 * 60.0 # ten minutes. | ||
| def wait_till_true(callback, timeout=LARGE_TEST_TIMEOUT, msg=""): | ||
| """Wait for test purposes until a condition becomes true , as determined by the | ||
| return value of the provided test function. | ||
| By default, we wait at most LARGE_TEST_TIMEOUT seconds for the callback function to return true, | ||
| and then quit or time out. Alternatively, a timeout of None translates as a request not to time out. | ||
| If the msg value passed in is a nonzero-length string, it can be used to raise a timeout exception; | ||
| otherwise timing out causes a normal exit, relaying as the return value the last value returned | ||
| from the test callback function. | ||
| """ | ||
| start_time = time.clock_gettime_ns(time.CLOCK_BOOTTIME) | ||
| while not (truth_value := callback()): | ||
| if timeout is not None and (time.clock_gettime_ns(time.CLOCK_BOOTTIME) - start_time) * 1e-9 > timeout: | ||
| if msg: | ||
| raise TimeoutError(msg) | ||
| else: | ||
| break | ||
| time.sleep(_clock_polling_interval) | ||
| return truth_value |
| import unittest | ||
| import os | ||
| import json | ||
| from irods.client_init import write_pam_interactive_irodsA_file | ||
| from unittest.mock import patch | ||
| from irods.auth import ClientAuthError, FORCE_PASSWORD_PROMPT | ||
| from irods.auth.pam_interactive import ( | ||
| _pam_interactive_ClientAuthState, | ||
| PERFORM_WAITING, | ||
| PERFORM_AUTHENTICATED, | ||
| PERFORM_NEXT, | ||
| ) | ||
| from irods.test.helpers import make_session | ||
| from irods.auth.pam_interactive import __NEXT_OPERATION__, __FLOW_COMPLETE__ | ||
| from irods.auth.pam_interactive import _auth_api_request | ||
| class PamInteractiveTest(unittest.TestCase): | ||
| def setUp(self): | ||
| # These tests assume the irods_environment.json file is set up correctly | ||
| # and that the iRODS user 'alice' exists in the 'tempZone' zone and has the password 'rods'. | ||
| # There should also be a linux user 'alice' with the same password. | ||
| self.sess = None | ||
| self.auth_client = _pam_interactive_ClientAuthState(None, None, scheme="pam_interactive") | ||
| self.env_file_path = os.path.expanduser("~/.irods/irods_environment.json") | ||
| self.auth_file_path = os.path.expanduser("~/.irods/.irodsA") | ||
| with open(self.env_file_path) as f: | ||
| env = json.load(f) | ||
| self.user = env.get("irods_user_name", "alice") | ||
| self.zone = env.get("irods_zone_name", "tempZone") | ||
| self.password = "rods" | ||
| def tearDown(self): | ||
| if self.sess: | ||
| self.sess.cleanup() | ||
| if os.path.exists(self.auth_file_path): | ||
| os.remove(self.auth_file_path) | ||
| def test_pam_interactive_login_basic(self): | ||
| with patch("getpass.getpass", return_value=self.password): | ||
| self.sess = make_session( | ||
| test_server_version=False, env_file=self.env_file_path, authentication_scheme="pam_interactive" | ||
| ) | ||
| # Creating a session does not trigger auth, so the home collection is accessed to trigger and confirm auth succeeded | ||
| home = self.sess.collections.get(f"/{self.sess.zone}/home/{self.sess.username}") | ||
| self.assertEqual(home.name, self.sess.username) | ||
| def test_pam_interactive_auth_file_creation(self): | ||
| with patch("getpass.getpass", return_value=self.password): | ||
| write_pam_interactive_irodsA_file(env_file=self.env_file_path) | ||
| self.assertTrue(os.path.exists(self.auth_file_path), ".irodsA file was not created") | ||
| with patch("getpass.getpass", return_value=self.password) as mock_getpass: | ||
| self.sess = make_session( | ||
| test_server_version=False, env_file=self.env_file_path, authentication_scheme="pam_interactive" | ||
| ) | ||
| # Creating a session does not trigger auth, so the home collection is accessed to trigger and confirm auth succeeded | ||
| home = self.sess.collections.get(f"/{self.sess.zone}/home/{self.sess.username}") | ||
| self.assertEqual(home.name, self.sess.username) | ||
| mock_getpass.assert_not_called() | ||
| def test_forced_interactive_flow(self): | ||
| with patch("getpass.getpass", return_value=self.password): | ||
| write_pam_interactive_irodsA_file(env_file=self.env_file_path) | ||
| self.assertTrue(os.path.exists(self.auth_file_path), ".irodsA file was not created") | ||
| with patch("getpass.getpass", return_value=self.password) as mock_getpass: | ||
| self.sess = make_session( | ||
| test_server_version=False, env_file=self.env_file_path, authentication_scheme="pam_interactive" | ||
| ) | ||
| self.sess.set_auth_option_for_scheme("pam_interactive", FORCE_PASSWORD_PROMPT, True) | ||
| # Creating a session does not trigger auth, so the home collection is accessed to trigger and confirm auth succeeded | ||
| home = self.sess.collections.get(f"/{self.sess.zone}/home/{self.sess.username}") | ||
| self.assertEqual(home.name, self.sess.username) | ||
| mock_getpass.assert_called_once() | ||
| def test_failed_login_incorrect_password(self): | ||
| with patch("getpass.getpass", return_value="wrong_password"): | ||
| with self.assertRaises(ClientAuthError): | ||
| self.sess = make_session( | ||
| test_server_version=False, env_file=self.env_file_path, authentication_scheme="pam_interactive" | ||
| ) | ||
| self.sess.collections.get(f"/{self.sess.zone}/home/{self.sess.username}") # trigger auth flow | ||
| with patch("getpass.getpass", return_value="wrong_password"): | ||
| with self.assertRaises(ClientAuthError): | ||
| write_pam_interactive_irodsA_file(env_file=self.env_file_path) | ||
| def test_get_default_value(self): | ||
| test_cases = [ | ||
| ("simple_path", {"msg": {"default_path": "/username"}, "pstate": {"username": "alice"}}, "alice"), | ||
| ("nested_path", {"msg": {"default_path": "/user/name"}, "pstate": {"user": {"name": "alice"}}}, "alice"), | ||
| ("path_does_not_exist", {"msg": {"default_path": "/user/username"}, "pstate": {"username": "alice"}}, ""), | ||
| ("non_string_value", {"msg": {"default_path": "/user/id"}, "pstate": {"user": {"id": 123}}}, "123"), | ||
| ("no_default_path", {"msg": {}, "pstate": {"username": "alice"}}, ""), | ||
| ] | ||
| for name, request, expected in test_cases: | ||
| with self.subTest(name=name): | ||
| self.assertEqual(self.auth_client._get_default_value(request), expected) | ||
| def test_patch_state(self): | ||
| test_cases = [ | ||
| ( | ||
| "add_op", | ||
| {"msg": {"patch": [{"op": "add", "path": "/username", "value": "alice"}]}, "pstate": {}}, | ||
| {"username": "alice"}, | ||
| True, | ||
| ), | ||
| ( | ||
| "replace_op", | ||
| { | ||
| "msg": {"patch": [{"op": "replace", "path": "/username", "value": "rods"}]}, | ||
| "pstate": {"username": "alice"}, | ||
| }, | ||
| {"username": "rods"}, | ||
| True, | ||
| ), | ||
| ( | ||
| "remove_op", | ||
| {"msg": {"patch": [{"op": "remove", "path": "/username"}]}, "pstate": {"username": "rods"}}, | ||
| {}, | ||
| True, | ||
| ), | ||
| ( | ||
| "add_resp_fallback", | ||
| {"msg": {"patch": [{"op": "add", "path": "/username"}]}, "pstate": {}, "resp": "alice"}, | ||
| {"username": "alice"}, | ||
| True, | ||
| ), | ||
| ( | ||
| "replace_resp_fallback", | ||
| { | ||
| "msg": {"patch": [{"op": "replace", "path": "/username"}]}, | ||
| "pstate": {"username": "rods"}, | ||
| "resp": "alice", | ||
| }, | ||
| {"username": "alice"}, | ||
| True, | ||
| ), | ||
| ( | ||
| "nested_add_operation", | ||
| {'msg': {'patch': [{'op': 'add', 'path': '/user/name', 'value': 'alice'}]}, 'pstate': {'user': {}}}, | ||
| {'user': {'name': 'alice'}}, | ||
| True, | ||
| ), | ||
| ( | ||
| "resp_fallback_empty", | ||
| {'msg': {'patch': [{'op': 'add', 'path': '/username'}]}, 'pstate': {}}, | ||
| {'username': ''}, | ||
| True, | ||
| ), | ||
| ( | ||
| "no_patch_ops", | ||
| {"msg": {}, "pstate": {"username": "alice"}, "pdirty": False}, | ||
| {"username": "alice"}, | ||
| False, | ||
| ), | ||
| ] | ||
| for name, request, expected_pstate, expected_pdirty in test_cases: | ||
| with self.subTest(name=name): | ||
| self.auth_client._patch_state(request) | ||
| self.assertEqual(request["pstate"], expected_pstate) | ||
| self.assertEqual(request["pdirty"], expected_pdirty) | ||
| def test_retrieve_entry(self): | ||
| test_cases = [ | ||
| ("surface_value", {"msg": {"retrieve": "/user"}, "pstate": {"user": "alice"}}, True, "alice"), | ||
| ( | ||
| "nested_value", | ||
| {"msg": {"retrieve": "/user/password"}, "pstate": {"user": {"password": "rods"}}}, | ||
| True, | ||
| "rods", | ||
| ), | ||
| ("empty_value", {"msg": {"retrieve": "/user"}, "pstate": {"user": ""}}, True, ""), | ||
| ("path_does_not_exist", {"msg": {"retrieve": "/missing"}, "pstate": {"user": "alice"}}, True, ""), | ||
| ("non_string_value", {'msg': {'retrieve': '/user/id'}, 'pstate': {'user': {'id': 456}}}, True, '456'), | ||
| ("no_retrieve_key", {"msg": {}, "pstate": {"user": "alice"}}, False, None), | ||
| ] | ||
| for name, request, expected_result, expected_resp in test_cases: | ||
| with self.subTest(name=name): | ||
| self.assertEqual(self.auth_client._retrieve_entry(request), expected_result) | ||
| if expected_result: | ||
| self.assertEqual(request["resp"], expected_resp) | ||
| @patch('irods.auth.pam_interactive._auth_api_request', return_value={"result": "ok"}) | ||
| @patch('sys.stderr.write') | ||
| def test_get_input(self, mock_stderr, mock_api_request): | ||
| test_cases = [ | ||
| ("non_password_input", False, 'sys.stdin.readline', "rods\n", {"msg": {"prompt": "Prompt:"}}, "rods"), | ||
| ( | ||
| "non_password_default", | ||
| False, | ||
| 'sys.stdin.readline', | ||
| "\n", | ||
| {"msg": {"prompt": "Prompt:", "default_path": "/password"}, "pstate": {"password": "rods"}}, | ||
| "rods", | ||
| ), | ||
| ("password_input", True, 'getpass.getpass', "rods", {"msg": {"prompt": "Password:"}}, "rods"), | ||
| ( | ||
| "password_default", | ||
| True, | ||
| 'getpass.getpass', | ||
| "", | ||
| {"msg": {"prompt": "Password:", "default_path": "/password"}, "pstate": {"password": "rods"}}, | ||
| "rods", | ||
| ), | ||
| ] | ||
| for name, is_password, mock_target, user_input, request, expected_resp in test_cases: | ||
| with ( | ||
| self.subTest(name=name), | ||
| patch(mock_target, return_value=user_input), | ||
| patch.object(self.auth_client, '_patch_state') as mock_patch, | ||
| ): | ||
| resp = self.auth_client._get_input(request, is_password=is_password) | ||
| self.assertEqual(request["resp"], expected_resp) | ||
| self.assertEqual(resp, {"result": "ok"}) | ||
| mock_patch.assert_called_once() | ||
| def test_pass_through_states(self): | ||
| with patch("irods.auth.pam_interactive._auth_api_request", return_value={"result": "ok"}): | ||
| request = {"msg": {"prompt": "Prompt:"}, "pstate": {}, "pdirty": False} | ||
| for state in [ | ||
| self.auth_client.next, | ||
| self.auth_client.running, | ||
| self.auth_client.ready, | ||
| self.auth_client.response, | ||
| ]: | ||
| with self.subTest(state=state.__name__): | ||
| resp = state(request) | ||
| self.assertEqual(resp, {"result": "ok"}) | ||
| def test_failure_states(self): | ||
| request = {"foo": "bar"} | ||
| with patch("irods.auth.pam_interactive._logger"): | ||
| for state in [self.auth_client.error, self.auth_client.timeout, self.auth_client.not_authenticated]: | ||
| with self.subTest(state=state.__name__): | ||
| resp = state(request) | ||
| self.assertEqual(resp[__NEXT_OPERATION__], __FLOW_COMPLETE__) | ||
| self.assertEqual(self.auth_client.loggedIn, 0) | ||
| @patch("sys.stdin.readline", return_value="ABC123\n") | ||
| def test_pam_interactive_mfa_flow(self, mock_stdin): | ||
| state = {"stage": "before_mfa", "step": 0} | ||
| def mock_server(conn, req): | ||
| # Switch from the real server to the mock server when the password step is completed | ||
| if state["stage"] == "before_mfa": | ||
| resp = _auth_api_request(conn, req) | ||
| if req.get(__NEXT_OPERATION__) == PERFORM_NEXT: # Indicates the password step is complete | ||
| state["stage"] = "mfa_mock" | ||
| return resp | ||
| # MFA simulation steps | ||
| if state["step"] == 0: | ||
| return { | ||
| __NEXT_OPERATION__: PERFORM_WAITING, | ||
| "pstate": {"Password: ": self.password, "verification_code": ""}, | ||
| "msg": { | ||
| "prompt": "Verification Code: ", | ||
| "default_path": "/verification_code", | ||
| "patch": [{"op": "add", "path": "/verification_code"}], | ||
| }, | ||
| "pdirty": True, | ||
| } | ||
| elif state["step"] == 1: | ||
| return { | ||
| __NEXT_OPERATION__: PERFORM_NEXT, | ||
| "pstate": {"Password: ": self.password, "verification_code": ""}, | ||
| "pdirty": True, | ||
| } | ||
| elif state["step"] == 2: | ||
| return { | ||
| __NEXT_OPERATION__: PERFORM_AUTHENTICATED, | ||
| "pstate": {"Password: ": self.password, "verification_code": "ABC123"}, | ||
| "pdirty": True, | ||
| "request_result": "temp_token", | ||
| } | ||
| state["step"] += 1 | ||
| with ( | ||
| patch("irods.auth.pam_interactive._auth_api_request", side_effect=mock_server), | ||
| patch("getpass.getpass", return_value=self.password), | ||
| patch("irods.auth.pam_interactive._authenticate_native") as mock_native, | ||
| ): | ||
| self.sess = make_session( | ||
| test_server_version=False, env_file=self.env_file_path, authentication_scheme="pam_interactive" | ||
| ) | ||
| self.sess.server_version # Trigger auth flow | ||
| mock_native.assert_called_once() | ||
| if __name__ == "__main__": | ||
| unittest.main() |
| #!/usr/bin/env python | ||
| import numbers | ||
| import os | ||
| import posix | ||
| import socket | ||
| import shutil | ||
| from subprocess import Popen, PIPE | ||
| import sys | ||
| IRODS_SSL_DIR = "/etc/irods/ssl" | ||
| SERVER_CERT_HOSTNAME = None | ||
| ext = "" | ||
| keep_old = False | ||
| def create_server_cert(process_output=sys.stdout, irods_key_path="irods.key", hostname=SERVER_CERT_HOSTNAME): | ||
| p = Popen( | ||
| "openssl req -new -x509 -key '{irods_key_path}' -out irods.crt{ext} -days 365 <<EOF{_sep_}" | ||
| "US{_sep_}North Carolina{_sep_}Chapel Hill{_sep_}UNC{_sep_}RENCI{_sep_}" | ||
| "{host}{_sep_}anon@mail.com{_sep_}EOF\n" | ||
| "".format(ext=ext, host=(hostname if hostname else socket.gethostname()), _sep_="\n", **locals()), | ||
| shell=True, | ||
| stdout=process_output, | ||
| stderr=process_output, | ||
| ) | ||
| p.wait() | ||
| return p.returncode | ||
| def create_ssl_dir(irods_key_path="irods.key", ssl_dir="", use_strong_primes_for_dh_generation=True): | ||
| ssl_dir = ssl_dir or IRODS_SSL_DIR | ||
| save_cwd = os.getcwd() | ||
| silent_run = {"shell": True, "stderr": PIPE, "stdout": PIPE} | ||
| try: | ||
| if not (os.path.exists(ssl_dir)): | ||
| os.mkdir(ssl_dir) | ||
| os.chdir(ssl_dir) | ||
| if not keep_old: | ||
| Popen( | ||
| "openssl genrsa -out '{irods_key_path}' 2048 && chmod 600 '{irods_key_path}'".format(**locals()), | ||
| **silent_run, | ||
| ).communicate() | ||
| with open("/dev/null", "wb") as dev_null: | ||
| if 0 == create_server_cert(process_output=dev_null, irods_key_path=irods_key_path): | ||
| if not keep_old: | ||
| # https://www.openssl.org/docs/man1.0.2/man1/dhparam.html#:~:text=DH%20parameter%20generation%20with%20the,that%20may%20be%20possible%20otherwise. | ||
| if use_strong_primes_for_dh_generation: | ||
| dhparam_generation_command = "openssl dhparam -2 -out dhparams.pem 2048" | ||
| else: | ||
| dhparam_generation_command = "openssl dhparam -dsaparam -out dhparams.pem 4096" | ||
| print("cmd=", dhparam_generation_command) | ||
| Popen(dhparam_generation_command, **silent_run).communicate() | ||
| return os.listdir(".") | ||
| finally: | ||
| os.chdir(save_cwd) | ||
| def test(options, args=()): | ||
| if args: | ||
| print("warning: non-option args are ignored", file=sys.stderr) | ||
| force = "-f" in options | ||
| affirm = "n" if (os.path.exists(IRODS_SSL_DIR) and not force) else "y" | ||
| if affirm == "n" and posix.isatty(sys.stdin.fileno()): | ||
| try: | ||
| input_ = raw_input | ||
| except NameError: | ||
| input_ = input | ||
| affirm = input_("This will overwrite directory '{}'. Proceed(Y/N)? ".format(IRODS_SSL_DIR)) | ||
| if affirm[:1].lower() == "y": | ||
| if not keep_old: | ||
| shutil.rmtree(IRODS_SSL_DIR, ignore_errors=True) | ||
| dh_strong_primes = "-q" not in options | ||
| wait_warning = " This may take a while." if dh_strong_primes else "" | ||
| print( | ||
| "Generating new '{}'.{}".format(IRODS_SSL_DIR, wait_warning), | ||
| file=sys.stderr, | ||
| ) | ||
| ssl_dir_files = create_ssl_dir(use_strong_primes_for_dh_generation=dh_strong_primes) | ||
| print("ssl_dir_files=", ssl_dir_files, file=sys.stderr) | ||
| def usage(exit_code=None): | ||
| print( | ||
| """Usage: {sys.argv[0]} [-f] [-h <hostname>] [-k] [-q] [-x <extension>] | ||
| -f Force replacement of the existing SSL directory (/etc/irods/ssl) with a new one, containing newly generated files. | ||
| -h In the generated certificate, use the given hostname rather than the value returned from socket.gethostname() | ||
| -k (Keep old secrets files.) Do not generate new key file or dhparams.pem file. | ||
| -q For testing; do a quick generation of a dhparams.pem file rather than waiting on system entropy to make it more secure. | ||
| -x Optional extra extension for appending to end of the filename for the generated certificate. | ||
| --help Print this help. | ||
| Any invalid option prints this help. | ||
| """.format(**globals()), | ||
| file=sys.stderr, | ||
| ) | ||
| if isinstance(exit_code, numbers.Integral): | ||
| exit(exit_code) | ||
| if __name__ == "__main__": | ||
| import getopt | ||
| try: | ||
| opt, arg_list = getopt.getopt(sys.argv[1:], "x:fh:kq", ["help"]) | ||
| except getopt.GetoptError: | ||
| usage(exit_code=1) | ||
| opt_lookup = dict(opt) | ||
| if "--help" in opt_lookup: | ||
| usage(exit_code=0) | ||
| ext = opt_lookup.get("-x", "") | ||
| if ext: | ||
| ext = "." + ext.lstrip(".") | ||
| keep_old = opt_lookup.get("-k") is not None | ||
| SERVER_CERT_HOSTNAME = opt_lookup.get("-h") | ||
| test(opt_lookup, arg_list) |
+522
| [project] | ||
| name = "python-irodsclient" | ||
| authors = [ | ||
| {name = "iRODS Consortium", email = "support@irods.org"}, | ||
| ] | ||
| description = "A Python API for iRODS" | ||
| readme = {file = "README.md", content-type = "text/markdown"} | ||
| requires-python = ">= 3.9" | ||
| keywords = ["irods"] | ||
| license = "BSD-3-Clause" | ||
| license-files = ["LICENSE.txt"] | ||
| classifiers = [ | ||
| "Development Status :: 5 - Production/Stable", | ||
| "Programming Language :: Python :: 3.9", | ||
| "Programming Language :: Python :: 3.10", | ||
| "Programming Language :: Python :: 3.11", | ||
| "Programming Language :: Python :: 3.12", | ||
| "Programming Language :: Python :: 3.13", | ||
| "Operating System :: POSIX :: Linux", | ||
| ] | ||
| dependencies = [ | ||
| "PrettyTable>=0.7.2", | ||
| "defusedxml", | ||
| "jsonpointer", | ||
| "jsonpatch", | ||
| ] | ||
| dynamic = ["version"] | ||
| [project.urls] | ||
| Repository = "https://github.com/irods/python-irodsclient" | ||
| Issues = "https://github.com/irods/python-irodsclient/issues" | ||
| [project.optional-dependencies] | ||
| tests = [ | ||
| "unittest-xml-reporting", # for xmlrunner | ||
| "types-defusedxml", # for type checking | ||
| "progressbar", # for type checking | ||
| "types-tqdm", # for type checking | ||
| ] | ||
| [build-system] | ||
| requires = ["setuptools"] | ||
| build-backend = "setuptools.build_meta" | ||
| [tool.setuptools] | ||
| py-modules = ["irods"] | ||
| script-files = ["irods/prc_write_irodsA.py"] | ||
| include-package-data = true | ||
| [tool.setuptools.packages.find] | ||
| exclude = ["test_harness*", "dist*"] | ||
| [tool.ruff] | ||
| exclude = [ | ||
| ".bzr", | ||
| ".direnv", | ||
| ".eggs", | ||
| ".git", | ||
| ".git-rewrite", | ||
| ".hg", | ||
| ".ipynb_checkpoints", | ||
| ".mypy_cache", | ||
| ".nox", | ||
| ".pants.d", | ||
| ".pyenv", | ||
| ".pytest_cache", | ||
| ".pytype", | ||
| ".ruff_cache", | ||
| ".svn", | ||
| ".tox", | ||
| ".venv", | ||
| ".vscode", | ||
| "__pypackages__", | ||
| "_build", | ||
| "buck-out", | ||
| "build", | ||
| "dist", | ||
| "node_modules", | ||
| "site-packages", | ||
| "venv", | ||
| ] | ||
| # Ensure this matches tool.ruff.lint.pycodestyle.max-doc-length | ||
| line-length = 120 | ||
| indent-width = 4 | ||
| [tool.ruff.format] | ||
| quote-style = "preserve" | ||
| indent-style = "space" | ||
| line-ending = "auto" | ||
| # Enable reformatting of code snippets in docstrings. | ||
| docstring-code-format = true | ||
| # Respect magic trailing commas. | ||
| skip-magic-trailing-comma = false | ||
| [tool.ruff.lint] | ||
| # Enable preview rules, but require them to be explicitly enabled | ||
| preview = true | ||
| explicit-preview-rules = true | ||
| # Enable rules | ||
| select = [ | ||
| # eradicate | ||
| "ERA", | ||
| # flake8-2020 | ||
| "YTT", | ||
| # flake8-annotations, uncomment if we ever add type annotations | ||
| #"ANN", | ||
| # flake8-executable | ||
| "EXE", | ||
| # flake8-async | ||
| "ASYNC", | ||
| # flake8-blind-except | ||
| "BLE", | ||
| # flake8-bugbear | ||
| "B", | ||
| # flake8-builtins | ||
| "A", | ||
| # flake8-comprehensions | ||
| "C4", | ||
| # flake8-datetimez | ||
| "DTZ", | ||
| # flake8-executable | ||
| "EXE", | ||
| # flake8-future-annotations | ||
| "FA", | ||
| # flake8-implicit-str-concat | ||
| "ISC", | ||
| # flake8-import-conventions | ||
| "ICN", | ||
| # flake8-logging | ||
| "LOG", | ||
| # flake8-logging-format | ||
| "G", | ||
| # flake8-no-pep420 | ||
| "INP", | ||
| # flake8-pie | ||
| "PIE", | ||
| # flake8-pyi | ||
| "PYI", | ||
| # flake8-raise | ||
| "RSE", | ||
| # flake8-return | ||
| "RET", | ||
| # flake8-self | ||
| "SLF", | ||
| # flake8-simplify | ||
| "SIM", | ||
| # flake8-slots | ||
| "SLOT", | ||
| # flake8-tidy-imports | ||
| "TID", | ||
| # flake8-todos | ||
| "TD", | ||
| # flake8-type-checking | ||
| "TC", | ||
| # flake8-unused-arguments | ||
| "ARG", | ||
| # flake8-use-pathlib | ||
| "PTH", | ||
| # flynt | ||
| "FLY", | ||
| # isort | ||
| "I", | ||
| # pep8-naming | ||
| "N", | ||
| # Perflint | ||
| "PERF", | ||
| # pycodestyle | ||
| "E", "W", | ||
| # pydoclint | ||
| "DOC", | ||
| # pydocstyle, flake8-docstrings | ||
| "D", | ||
| # Pyflakes | ||
| "F", | ||
| # pygrep-hooks | ||
| "PGH", | ||
| # Pylint | ||
| "PL", | ||
| # pyupgrade | ||
| "UP", | ||
| # refurb | ||
| "FURB", | ||
| # Ruff-specific rules | ||
| "RUF", | ||
| # tryceratops | ||
| "TRY", | ||
| ## flake8-bandit | ||
| ## We want a subset and it's easier to handle some here than in ignore | ||
| # S1??: general tests | ||
| "S1", | ||
| # S2??: misconfiguration | ||
| "S2", | ||
| # S301: suspicious pickle usage | ||
| "S301", | ||
| # S306: usage of insecure and deprecated mktemp | ||
| "S306", | ||
| # S307: usage of insecure eval | ||
| "S307", | ||
| # S311: usage of bad rand | ||
| "S311", | ||
| # S313-S319: usage of insecure XML parsing | ||
| "S313", "S314", "S315", "S316", "S317", "S318", "S319", | ||
| # S5??: cryptography | ||
| "S5", | ||
| # shell usage | ||
| "S602", "S604", "S605", | ||
| # S612: | ||
| ## flake8-commas | ||
| ## We only want one rule from this one | ||
| # COM818: trailing comman on bare tuple prohibited | ||
| "COM818", | ||
| ## flake8-quotes | ||
| ## We don't want most rules from this one, as quotes are mostly handled by the formatter | ||
| # Q004: Unnecessary escape on inner quote character | ||
| "Q004", | ||
| ## pydocstyle | ||
| ## Certain rules are disabled by tool.ruff.lint.pydocstyle.convention, but we still want some of them | ||
| # D213: Multi-line docstring summary should start at the second line | ||
| "D213", | ||
| # D214: Overindented section | ||
| "D214", | ||
| # D215: Overindented section underline | ||
| "D215", | ||
| # D404: First word of the docstring should not be "This" | ||
| "D404", | ||
| # D405: Section name should be properly capitalized | ||
| "D405", | ||
| # D406: Missing newline after section name | ||
| "D406", | ||
| # D409: Mismatched section underline length | ||
| "D409", | ||
| # D416: Missing colon after section name | ||
| "D416", | ||
| # D417: Missing parameter documentation | ||
| "D417", | ||
| ### preview rules | ||
| ## pydoclint | ||
| # DOC102: uocumented parameter missing from signature | ||
| "DOC102", | ||
| # DOC201: undocumented return value | ||
| "DOC201", | ||
| # DOC202: docstring has returns section, but function/method does not return anything | ||
| "DOC202", | ||
| # DOC402: undocumented yield value | ||
| "DOC402", | ||
| # DOC403: docstring has yields section, but function/method does not yield anything | ||
| "DOC403", | ||
| # DOC501: undocumented raised exception | ||
| "DOC501", | ||
| ] | ||
| # Disable rules | ||
| ignore = [ | ||
| ## flake8-datetimez | ||
| # DTZ003: datetime.datetime.utcnow() used | ||
| "DTZ003", | ||
| ## flake8-tidy-imports | ||
| # TID252: Prefer absolute imports over relative imports | ||
| # This can introduce difficulties | ||
| "TID252", | ||
| ## flake8-todos | ||
| # TD002: Missing author in TODO | ||
| "TD002", | ||
| ## pycodestyle | ||
| # E402: Module level import not at top of file | ||
| # We often need to do some checking before importing stuff | ||
| "E402", | ||
| # Indentation/alignment/spacing stuff | ||
| # E128: continuation line under-indented for visual indent | ||
| # Don't hate me cuz I'm pretty. | ||
| # Uncomment if ruff ever supports this | ||
| #"E128", | ||
| # Most indentation handled by formatter | ||
| "E101", # indentation contains mixed spaces and tabs | ||
| "E111", # wrong indentation multiple | ||
| "E114", # wrong indentation multiple (for comments) | ||
| "E117", # over-indentation | ||
| "W191", # indentation includes tabs | ||
| ## pydoclint | ||
| # DOC502: docstring specifies potential exception that is not actually raised in the fucntion/method body | ||
| # Does not account for exceptions from other sources | ||
| "DOC502", | ||
| ## pylint | ||
| # PLC0415: import should be at top-level | ||
| # plenty of reasons to put it in a block | ||
| "PLC0415", | ||
| # PLC1901: comparison to empty string | ||
| # Does not account for types | ||
| "PLC1901", | ||
| # PLE0116: continue in finally block | ||
| # Supported since Python 3.8 | ||
| "PLE0116", | ||
| # Design complexity stuff | ||
| # These judgements should always be made by humans | ||
| #"PLC0302", # too many lines in module (uncomment if ruff ever supports this) | ||
| #"PLR0901", # too many class ancestors (uncomment if ruff ever supports this) | ||
| #"PLR0902", # too many instance attributes (uncomment if ruff ever supports this) | ||
| #"PLR0903", # too few public methods (uncomment if ruff ever supports this) | ||
| "PLR0904", # too many public methods | ||
| "PLR0911", # too many return statements | ||
| "PLR0912", # too many branches | ||
| "PLR0913", # too many arguments | ||
| "PLR0914", # too many locals | ||
| "PLR0915", # too many statements | ||
| "PLR0916", # too many boolean expressions in a single statement | ||
| "PLR0917", # too many positional arguments | ||
| "PLR1702", # too many nested blocks | ||
| ## tryceratops | ||
| # TRY003: Avoid specifying long messages outside the exception class | ||
| # Way, way too many false positives | ||
| "TRY003", | ||
| ] | ||
| # Allow fix for all enabled rules (when `--fix`) is provided. | ||
| fixable = ["ALL"] | ||
| unfixable = [] | ||
| # Don't whine about codes in noqa that ruff doesn't (yet) support | ||
| external = [ | ||
| # openstack's hacking flake8 plugin | ||
| "V", | ||
| # flake8-multiline-containers | ||
| "JS", | ||
| # flake8-typing-imports | ||
| "TYP" | ||
| ] | ||
| # Allow unused variables when underscore-prefixed. | ||
| dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" | ||
| # Allow suggesting `from __future__ import annotations` | ||
| future-annotations = true | ||
| # TODO and TODO-like tags to recognize | ||
| task-tags = [ | ||
| 'FIXME', | ||
| 'TODO', | ||
| 'HACK', | ||
| 'KLUDGE', | ||
| 'BUG', | ||
| 'OPTIMIZE', | ||
| 'XXX', | ||
| 'JANK', | ||
| 'AWFUL', | ||
| 'BAD', | ||
| 'CRIME', | ||
| 'CRIMES', | ||
| ] | ||
| # Recognize `typing_extensions` imports | ||
| typing-extensions = true | ||
| # Speciy additional typing extension modules | ||
| typing-modules = [] | ||
| [tool.ruff.lint.per-file-ignores] | ||
| "setup.py" = [ | ||
| # pydocstyle, flake8-docstrings | ||
| # Ignore missing module docstring in setup.py | ||
| "D100", | ||
| ] | ||
| "irods/test/*_test.py" = [ | ||
| # flake8-bandit | ||
| # S106: Possible hardcoded password assigned to argument | ||
| # Ignore passwords in tests | ||
| "S106", | ||
| # pydocstyle, flake8-docstrings | ||
| # Ignore missing docstrings in tests | ||
| "D1", | ||
| ] | ||
| [tool.ruff.lint.flake8-annotations] | ||
| # Suppress ANN401 for dynamically typed *args and **kwargs arguments. | ||
| allow-star-arg-any = true | ||
| # Allow omission of __init__ return type hint if at least one argument is annotated | ||
| #mypy-init-return = true | ||
| # Suppress violations for dummy argument variables | ||
| #suppress-dummy-args = true | ||
| [tool.ruff.lint.flake8-bugbear] | ||
| # function calls to allow as default arguments | ||
| extend-immutable-calls = [] | ||
| [tool.ruff.lint.flake8-builtins] | ||
| #allowed-modules = [] | ||
| #ignorelist = [] | ||
| [lint.flake8-implicit-str-concat] | ||
| # Allow multiline implicit string concatenation | ||
| allow-multiline = true | ||
| [tool.ruff.lint.flake8-import-conventions] | ||
| # Disallow using the `from ... import ...` syntax for individual modules | ||
| banned-from = [] | ||
| [tool.ruff.lint.flake8-import-conventions.extend-aliases] | ||
| # To enforce an aliasing convention, follow this pattern: | ||
| #"module1" = "alias1" | ||
| #"module2" = "alias2" | ||
| [tool.ruff.lint.flake8-import-conventions.banned-aliases] | ||
| # To disallow specific aliases, follow this pattern: | ||
| #"module1" = ["alias1", "alias2"] | ||
| #"module2" = ["alias3", "alias4"] | ||
| [tool.ruff.lint.flake8-self] | ||
| # names to ignore when flagging private member access | ||
| extend-ignore-names = ["test_*"] | ||
| [tool.ruff.lint.flake8-tidy-imports] | ||
| # Disallow specific module-level imports (allow only in functions and blocks) | ||
| banned-module-level-imports = [] | ||
| [tool.ruff.lint.flake8-tidy-imports.banned-api] | ||
| # Disallow specific modules from being imported at all | ||
| "optparse".msg = "Use argparse instead of optparse" | ||
| "getopt".msg = "Use argparse instead of getopt" | ||
| "distutils".msg = "distutils has been removed from the stdlib" | ||
| "imp".msg = "imp has been removed from the stdlib" | ||
| "pycrypto".msg = "pycrypto is no longer maintained; use pyca/cryptography instead" | ||
| [tool.ruff.lint.flake8-type-checking] | ||
| # Exempt specific modules from needing to be moved into type-checking blocks | ||
| exempt-modules = ["typing"] | ||
| # Exempt classes that list any of the enumerated classes as a base class from needing to be moved | ||
| # into type-checking blocks | ||
| runtime-evaluated-base-classes = [] | ||
| # Exempt classes and functions decorated with any of the enumerated decorators from being moved | ||
| # into type-checking blocks | ||
| runtime-evaluated-decorators = [] | ||
| # Quote type annotations if doing so would allow an import to be moved into a type-checking block | ||
| #quote-annotations = true | ||
| [tool.ruff.lint.flake8-unused-arguments] | ||
| # Do not ignore unused *args/**kwargs | ||
| ignore-variadic-names = false | ||
| [tool.ruff.lint.isort] | ||
| # Order imports by type | ||
| order-by-type = true | ||
| # ordering of relative imports | ||
| relative-imports-order = "closest-to-furthest" | ||
| # force specific imports to the top of their section | ||
| force-to-top = [] | ||
| # Combine aliased imports | ||
| combine-as-imports = true | ||
| # Combine aliased imports as multi-line compount imports | ||
| force-wrap-aliases = true | ||
| # modules to separate into auxiliary block(s), in the order specified | ||
| forced-separate = [] | ||
| # modules to consider as part of the stdlib | ||
| extra-standard-library = [] | ||
| # modules to consider as being third-party | ||
| known-third-party = [ | ||
| # distro_distil is first-party, but is an iRODS-independent package | ||
| "distro_distil", | ||
| ] | ||
| # modules to consider as first-party | ||
| known-first-party = [] | ||
| # modules to consider as being a local folder | ||
| known-local-folder = [] | ||
| # ensure certain imports are in all files | ||
| required-imports = [] | ||
| # single-line rule exceptions | ||
| single-line-exclusions = [] | ||
| # Do not fold mult-line imports if there is a trailing comma | ||
| split-on-trailing-comma = true | ||
| # tokesn to always recognize as CONSTANTs | ||
| constants = [ | ||
| #"GSI_AUTH_SCHEME", | ||
| #"PAM_AUTH_SCHEME", | ||
| #"DEFAULT_CONFIG_PATH" | ||
| ] | ||
| # tokens to always recognise as variables | ||
| variables = [] | ||
| [tool.ruff.lint.pep8-naming] | ||
| # treat methos with these decorators as class methods | ||
| classmethod-decorators = [] | ||
| # treat methos with these decorators as static methods | ||
| staticmethod-decorators = [] | ||
| # names to ignore when considering naming violations | ||
| extend-ignore-names = [] | ||
| [tool.ruff.lint.pycodestyle] | ||
| # Ensure this matches toll.ruff.line-length | ||
| #max-doc-length = 120 | ||
| [tool.ruff.lint.pydocstyle] | ||
| # Adhere to PEP257 | ||
| convention = "pep257" | ||
| # Ignore docstrings for functions/methods with these fully-qualified decorators | ||
| ignore-decorators = ["typing.overload"] | ||
| # Do not ignore undocumented *args/**kwargs | ||
| ignore-var-parameters = false | ||
| # Treat methods with these decorators as properties | ||
| property-decorators = [] | ||
| [tool.ruff.lint.pyflakes] | ||
| # functions/classes to consider generic | ||
| extend-generics = [] | ||
| [tool.ruff.lint.pylint] | ||
| # dunder method names to allow | ||
| allow-dunder-method-names = [] | ||
| # types to ignore when flagging magic values | ||
| allow-magic-value-types = [ | ||
| "str", | ||
| "bytes", | ||
| #"complex", | ||
| #"float", | ||
| #"int", | ||
| ] |
| #!/home/tgr/repos/python-irodsclient/venv313/bin/python3.13 | ||
| import getopt | ||
| import textwrap | ||
| import sys | ||
| from typing import Callable, Dict | ||
| from irods.auth.pam_password import _get_pam_password_from_stdin as get_password | ||
| from irods.client_init import write_pam_irodsA_file, write_native_irodsA_file | ||
| if __name__ == "__main__": | ||
| extra_help = textwrap.dedent( | ||
| """ | ||
| This Python module also functions as a script to produce a "secrets" (i.e. encoded password) file. | ||
| Similar to iinit in this capacity, if the environment - and where applicable, the PAM | ||
| configuration for both system and user - is already set up in every other regard, this program | ||
| will generate the secrets file with appropriate permissions and in the normal location, usually: | ||
| ~/.irods/.irodsA | ||
| The user will be interactively prompted to enter their cleartext password. | ||
| """ | ||
| ) | ||
| vector : Dict[str, Callable] = {"pam_password": write_pam_irodsA_file, "native": write_native_irodsA_file} | ||
| opts, args = getopt.getopt(sys.argv[1:], "hi:", ["ttl=", "help"]) | ||
| optD = dict(opts) | ||
| help_selected = {*optD} & {"-h", "--help"} | ||
| if len(args) != 1 or help_selected: | ||
| print( | ||
| "{}\nUsage: {} [-i STREAM| -h | --help | --ttl HOURS] AUTH_SCHEME".format( | ||
| extra_help, sys.argv[0] | ||
| ) | ||
| ) | ||
| print(" Choices for AUTH_SCHEME are:") | ||
| for x in vector: | ||
| print(" {}".format(x)) | ||
| print( | ||
| " STREAM is the name of a file containing a password. Alternatively, a hyphen('-') is used to\n" | ||
| " indicate that the password may be read from stdin." | ||
| ) | ||
| sys.exit(0 if help_selected else 1) | ||
| scheme = args[0] | ||
| if scheme in vector: | ||
| options = {} | ||
| inp_stream = optD.get("-i", None) | ||
| if "--ttl" in optD: | ||
| options["ttl"] = optD["--ttl"] | ||
| if inp_stream is None or inp_stream == "-": | ||
| pw = get_password(sys.stdin, | ||
| prompt=f"Enter current password for scheme {scheme!r}: ",) | ||
| else: | ||
| pw = get_password(open(inp_stream, "r", encoding='utf-8'), | ||
| prompt=f"Enter current password for scheme {scheme!r}: ",) | ||
| vector[scheme](pw, **options) | ||
| else: | ||
| print("did not recognize authentication scheme argument", file=sys.stderr) |
+35
-0
@@ -14,2 +14,37 @@ # Changelog | ||
| ## [v3.3.0] - 2026-03-18 | ||
| This release includes improvements for listing tickets, clean cancellation of parallel transfers, storage of binary data in AVUs, and more. | ||
| ### Changed | ||
| - Migrate to pyproject.toml (#774, #783). | ||
| ### Removed | ||
| - Remove Jenkins test framework (#778). | ||
| ### Deprecated | ||
| - Deprecate `IRODS_VERSION` (#698). | ||
| ### Fixed | ||
| - Fix ability to store arbitrary binary data in an AVU (#707). | ||
| - Add `__slots__` member to prevent configuration misfires (#708). | ||
| - Preserve state when chaining calls on metadata manager (#709). | ||
| - Fix segfault and hung threads when signals abort a parallel transfer (#722). | ||
| - Fix handling of username in GeneralAdmin API for proper removal of remote user home collection (#763). | ||
| - Use named loggers (#771). | ||
| ### Added | ||
| - Add convenience functions for listing tickets (#120). | ||
| - Add automated testing via GitHub Actions (#502, #697). | ||
| - Add `_IRODS_VERSION` (#698). | ||
| - Add facilities for stopping parallel transfers in a clean manner (#722). | ||
| - Add configuration and GitHub Action workflows for code formatting and linting (#726). | ||
| - Allow MetadataManager options to be accessible as attributes (#795). | ||
| - Allow ticket instances to be populated using information from catalog queries (#801). | ||
| ## [v3.2.0] - 2025-08-27 | ||
@@ -16,0 +51,0 @@ |
@@ -33,5 +33,3 @@ import sys | ||
| default_irods_authentication_file = os.path.expanduser("~/.irods/.irodsA") | ||
| return os.environ.get( | ||
| "IRODS_AUTHENTICATION_FILE", default_irods_authentication_file | ||
| ) | ||
| return os.environ.get("IRODS_AUTHENTICATION_FILE", default_irods_authentication_file) | ||
@@ -113,4 +111,2 @@ | ||
| # and thus no settings file is auto-loaded. | ||
| client_configuration.autoload( | ||
| _file_to_load=os.environ.get(settings_path_environment_variable) | ||
| ) | ||
| client_configuration.autoload(_file_to_load=os.environ.get(settings_path_environment_variable)) |
+5
-18
@@ -23,3 +23,2 @@ import collections | ||
| class iRODSAccess(metaclass=_Access_LookupMeta): | ||
| @classmethod | ||
@@ -78,5 +77,3 @@ def to_int(cls, key): | ||
| strings = collections.OrderedDict( | ||
| (number, string) for string, number in codes.items() | ||
| ) | ||
| strings = collections.OrderedDict((number, string) for string, number in codes.items()) | ||
@@ -108,5 +105,3 @@ def __init__(self, access_name, path, user_name="", user_zone="", user_type=None): | ||
| def __hash__(self): | ||
| return hash( | ||
| (self.access_name, iRODSPath(self.path), self.user_name, self.user_zone) | ||
| ) | ||
| return hash((self.access_name, iRODSPath(self.path), self.user_name, self.user_zone)) | ||
@@ -122,7 +117,3 @@ def copy(self, decanonicalize=False): | ||
| }.get(self.access_name) | ||
| other.access_name = ( | ||
| replacement_string | ||
| if replacement_string is not None | ||
| else self.access_name | ||
| ) | ||
| other.access_name = replacement_string if replacement_string is not None else self.access_name | ||
| return other | ||
@@ -133,5 +124,3 @@ | ||
| access_name = self.access_name.replace(" ", "_") | ||
| user_type_hint = ( | ||
| "({user_type})" if object_dict.get("user_type") is not None else "" | ||
| ).format(**object_dict) | ||
| user_type_hint = ("({user_type})" if object_dict.get("user_type") is not None else "").format(**object_dict) | ||
| return f"<iRODSAccess {access_name} {self.path} {self.user_name}{user_type_hint} {self.user_zone}>" | ||
@@ -146,4 +135,2 @@ | ||
| ) | ||
| strings = collections.OrderedDict( | ||
| (number, string) for string, number in codes.items() | ||
| ) | ||
| strings = collections.OrderedDict((number, string) for string, number in codes.items()) |
+1
-2
@@ -5,3 +5,2 @@ from irods import derived_auth_filename | ||
| class iRODSAccount: | ||
| @property | ||
@@ -23,3 +22,3 @@ def derived_auth_file(self): | ||
| env_file="", | ||
| **kwargs | ||
| **kwargs, | ||
| ): | ||
@@ -26,0 +25,0 @@ |
+10
-16
@@ -20,2 +20,5 @@ import importlib | ||
| _logger = logging.getLogger(__name__) | ||
| class AuthStorage: | ||
@@ -145,5 +148,3 @@ """A class that facilitates flexible means of password storage. | ||
| message_body = JSON_Message(data, conn.server_version) | ||
| message = iRODSMessage( | ||
| "RODS_API_REQ", msg=message_body, int_info=api_number["AUTHENTICATION_APN"] | ||
| ) | ||
| message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["AUTHENTICATION_APN"]) | ||
| conn.send(message) | ||
@@ -164,3 +165,2 @@ response = conn.recv() | ||
| class authentication_base: | ||
| def __init__(self, connection, scheme): | ||
@@ -172,3 +172,3 @@ self.conn = connection | ||
| def call(self, next_operation, request): | ||
| logging.debug("next operation = %r", next_operation) | ||
| _logger.debug("next operation = %r", next_operation) | ||
| old_func = func = next_operation | ||
@@ -182,8 +182,6 @@ # One level of indirection should be sufficient to get a callable method. | ||
| resp = func(request) | ||
| logging.debug("resp = %r", resp) | ||
| _logger.debug("resp = %r", resp) | ||
| return resp | ||
| def authenticate_client( | ||
| self, next_operation="auth_client_start", initial_request=() | ||
| ): | ||
| def authenticate_client(self, next_operation="auth_client_start", initial_request=()): | ||
| if not isinstance(initial_request, dict): | ||
@@ -201,11 +199,7 @@ initial_request = dict(initial_request) | ||
| if next_operation is None: | ||
| raise ClientAuthError( | ||
| "next_operation key missing; cannot determine next operation" | ||
| ) | ||
| raise ClientAuthError("next_operation key missing; cannot determine next operation") | ||
| if next_operation in (__FLOW_COMPLETE__, ""): | ||
| raise ClientAuthError( | ||
| f"authentication flow stopped without success: scheme = {self.scheme}" | ||
| ) | ||
| raise ClientAuthError(f"authentication flow stopped without success: scheme = {self.scheme}") | ||
| to_send = resp | ||
| logging.debug("fully authenticated") | ||
| _logger.debug("fully authenticated") |
+5
-15
@@ -64,5 +64,3 @@ import base64 | ||
| # user_name and zone_name keys injected by authenticate_client() method | ||
| resp[__NEXT_OPERATION__] = ( | ||
| self.AUTH_CLIENT_AUTH_REQUEST | ||
| ) # native_auth_client_request | ||
| resp[__NEXT_OPERATION__] = self.AUTH_CLIENT_AUTH_REQUEST # native_auth_client_request | ||
| return resp | ||
@@ -80,5 +78,3 @@ | ||
| def native_auth_establish_context(self, request): | ||
| throw_if_request_message_is_missing_key( | ||
| request, ["user_name", "zone_name", "request_result"] | ||
| ) | ||
| throw_if_request_message_is_missing_key(request, ["user_name", "zone_name", "request_result"]) | ||
| request = request.copy() | ||
@@ -96,9 +92,5 @@ | ||
| challenge = request["request_result"].encode("utf-8") | ||
| self.conn._client_signature = "".join( | ||
| "{:02x}".format(c) for c in challenge[:16] | ||
| ) | ||
| self.conn._client_signature = "".join("{:02x}".format(c) for c in challenge[:16]) | ||
| padded_pwd = struct.pack( | ||
| "%ds" % MAX_PASSWORD_LENGTH, password.encode("utf-8").strip() | ||
| ) | ||
| padded_pwd = struct.pack("%ds" % MAX_PASSWORD_LENGTH, password.encode("utf-8").strip()) | ||
@@ -119,5 +111,3 @@ m = hashlib.md5() | ||
| def native_auth_client_response(self, request): | ||
| throw_if_request_message_is_missing_key( | ||
| request, ["user_name", "zone_name", "digest"] | ||
| ) | ||
| throw_if_request_message_is_missing_key(request, ["user_name", "zone_name", "digest"]) | ||
@@ -124,0 +114,0 @@ server_req = request.copy() |
@@ -10,3 +10,3 @@ from . import ( | ||
| STORE_PASSWORD_IN_MEMORY, | ||
| CLIENT_GET_REQUEST_RESULT | ||
| CLIENT_GET_REQUEST_RESULT, | ||
| ) | ||
@@ -21,3 +21,3 @@ from .native import _authenticate_native | ||
| # Constants defining the states and operations for the pam_interactive authentication flow | ||
| # Constants defining the states and operations for the pam_interactive authentication flow | ||
| AUTH_CLIENT_AUTH_REQUEST = "pam_auth_client_request" | ||
@@ -43,2 +43,3 @@ AUTH_CLIENT_AUTH_RESPONSE = "pam_auth_response" | ||
| def login(conn, **extra_opt): | ||
@@ -51,6 +52,5 @@ """The entry point for the pam_interactive authentication scheme.""" | ||
| auth_client_object = _pam_interactive_ClientAuthState(conn, depot, scheme=PAM_INTERACTIVE_SCHEME) | ||
| auth_client_object.authenticate_client( | ||
| initial_request=extra_opt | ||
| ) | ||
| auth_client_object.authenticate_client(initial_request=extra_opt) | ||
| class _pam_interactive_ClientAuthState(authentication_base): | ||
@@ -71,3 +71,3 @@ def __init__(self, conn, depot, *_, **_kw): | ||
| # The plugin is built on the authentication framework described here: | ||
| # https://github.com/irods-contrib/irods_working_group_authentication/tree/e83e84df8ea4a732e5de894fb28aae281c3b3d29/development | ||
| # https://github.com/irods-contrib/irods_working_group_authentication/tree/e83e84df8ea4a732e5de894fb28aae281c3b3d29/development | ||
@@ -131,3 +131,3 @@ resp["pstate"] = resp.get("pstate", {}) | ||
| for op in patch_ops: | ||
| if op.get("op") in ["add", "replace"] and "value" not in op: | ||
| if op.get("op") in ["add", "replace"] and "value" not in op: | ||
| op["value"] = resp | ||
@@ -275,2 +275,2 @@ | ||
| def not_authenticated(self, request): | ||
| return self._auth_failure(request, "Authentication failed possibly due to incorrect credentials.") | ||
| return self._auth_failure(request, "Authentication failed possibly due to incorrect credentials.") |
@@ -56,5 +56,3 @@ import getpass | ||
| _pam_password_ClientAuthState(conn, scheme=_scheme).authenticate_client( | ||
| initial_request=req | ||
| ) | ||
| _pam_password_ClientAuthState(conn, scheme=_scheme).authenticate_client(initial_request=req) | ||
@@ -64,11 +62,7 @@ _logger.debug("----------- %s (end)", _scheme) | ||
| def _get_pam_password_from_stdin( | ||
| file_like_object=None, prompt="Enter your current PAM password: " | ||
| ): | ||
| def _get_pam_password_from_stdin(file_like_object=None, prompt="Enter your current PAM password: "): | ||
| try: | ||
| if file_like_object: | ||
| if not getattr(file_like_object, "readline", None): | ||
| msg = ( | ||
| "The file_like_object, if provided, must have a 'readline' method." | ||
| ) | ||
| msg = "The file_like_object, if provided, must have a 'readline' method." | ||
| raise RuntimeError(msg) | ||
@@ -89,3 +83,2 @@ sys.stdin = file_like_object | ||
| class _pam_password_ClientAuthState(authentication_base): | ||
| # Client define | ||
@@ -106,5 +99,3 @@ AUTH_CLIENT_AUTH_REQUEST = "pam_password_auth_client_request" | ||
| # to the caller upon request. | ||
| self._list_for_request_result_return = request.pop( | ||
| CLIENT_GET_REQUEST_RESULT, False | ||
| ) | ||
| self._list_for_request_result_return = request.pop(CLIENT_GET_REQUEST_RESULT, False) | ||
@@ -128,5 +119,3 @@ ensure_ssl = request.pop(ENSURE_SSL_IS_ACTIVE, None) | ||
| # Like with the C++ plugin, we offer the user a chance to enter a password. | ||
| resp[AUTH_PASSWORD_KEY] = _get_pam_password_from_stdin( | ||
| file_like_object=password_input_obj | ||
| ) | ||
| resp[AUTH_PASSWORD_KEY] = _get_pam_password_from_stdin(file_like_object=password_input_obj) | ||
| else: | ||
@@ -133,0 +122,0 @@ # Password from .irodsA in environment. |
@@ -39,7 +39,3 @@ import ast | ||
| cls = type.__new__(meta, name, bases, attrs) | ||
| cls.writeable_properties = tuple( | ||
| k | ||
| for k, v in attrs.items() | ||
| if isinstance(v, property) and v.fset is not None | ||
| ) | ||
| cls.writeable_properties = tuple(k for k, v in attrs.items() if isinstance(v, property) and v.fset is not None) | ||
| return cls | ||
@@ -49,2 +45,4 @@ | ||
| class ConnectionsProperties(iRODSConfiguration, metaclass=iRODSConfigAliasMetaclass): | ||
| __slots__ = () | ||
| @property | ||
@@ -62,8 +60,16 @@ def xml_parser_default(self): | ||
| connections = ConnectionsProperties() | ||
| class ConfigurationError(BaseException): pass | ||
| class ConfigurationValueError(ValueError,ConfigurationError): pass | ||
| class ConfigurationError(BaseException): | ||
| pass | ||
| class ConfigurationValueError(ValueError, ConfigurationError): | ||
| pass | ||
| class Genquery1_Properties(iRODSConfiguration, metaclass=iRODSConfigAliasMetaclass): | ||
| __slots__ = () | ||
@@ -73,2 +79,3 @@ @property | ||
| import irods.query | ||
| return irods.query.IRODS_QUERY_LIMIT | ||
@@ -79,9 +86,13 @@ | ||
| import irods.query | ||
| requested = int(target_value) | ||
| if requested <= 0: | ||
| raise ConfigurationValueError(f'Error setting IRODS_QUERY_LIMIT to [{requested}]. Use positive values only.') | ||
| raise ConfigurationValueError( | ||
| f'Error setting IRODS_QUERY_LIMIT to [{requested}]. Use positive values only.' | ||
| ) | ||
| irods.query.IRODS_QUERY_LIMIT = requested | ||
| genquery1 = Genquery1_Properties() | ||
@@ -148,5 +159,3 @@ | ||
| def __init__(self): | ||
| self.time_to_live_in_hours = ( | ||
| 0 # -> We default to the server's TTL preference. | ||
| ) | ||
| self.time_to_live_in_hours = 0 # -> We default to the server's TTL preference. | ||
| self.password_for_auto_renew = "" | ||
@@ -178,7 +187,3 @@ self.store_password_to_environment = False | ||
| if isinstance(root, types.ModuleType): | ||
| return [ | ||
| ((i, v) + flag(False)) | ||
| for i, v in vars(root).items() | ||
| if isinstance(v, iRODSConfiguration) | ||
| ] | ||
| return [((i, v) + flag(False)) for i, v in vars(root).items() if isinstance(v, iRODSConfiguration)] | ||
| if isinstance(root, iRODSConfiguration): | ||
@@ -197,5 +202,3 @@ return [(i, getattr(root, i)) + flag(True) for i in _config_names(root)] | ||
| # yield from _var_items_as_generator(root = sub_node, dotted = dn) | ||
| for _dotted, _root, _is_config in _var_items_as_generator( | ||
| root=sub_node, dotted=dn | ||
| ): | ||
| for _dotted, _root, _is_config in _var_items_as_generator(root=sub_node, dotted=dn): | ||
| yield _dotted, _root, _is_config | ||
@@ -286,5 +289,3 @@ | ||
| def _load_config_line( | ||
| root, setting, value, return_old=None, eval_func=ast.literal_eval | ||
| ): | ||
| def _load_config_line(root, setting, value, return_old=None, eval_func=ast.literal_eval): | ||
| """Low-level utility function for loading a line of settings, with the option to return the old (displaced) value. | ||
@@ -319,7 +320,3 @@ | ||
| # If we get this far, there's a problem loading the configuration setting. Raise an exception or log it. | ||
| error_message = ( | ||
| "Bad setting: root = {root!r}, setting = {setting!r}, value = {value!r}".format( | ||
| **locals() | ||
| ) | ||
| ) | ||
| error_message = "Bad setting: root = {root!r}, setting = {setting!r}, value = {value!r}".format(**locals()) | ||
| if loadexc: | ||
@@ -459,7 +456,3 @@ error_message += " [{loadexc!r}]".format(**locals()) | ||
| def preserve_defaults(): | ||
| default_config_dict.update( | ||
| (k, copy.deepcopy(v)) | ||
| for k, v in globals().items() | ||
| if isinstance(v, iRODSConfiguration) | ||
| ) | ||
| default_config_dict.update((k, copy.deepcopy(v)) for k, v in globals().items() if isinstance(v, iRODSConfiguration)) | ||
@@ -483,5 +476,3 @@ | ||
| return { | ||
| _tuple.dotted: "__".join( | ||
| ["PYTHON_IRODSCLIENT_CONFIG"] + uppercase_and_dot_split(_tuple.dotted) | ||
| ) | ||
| _tuple.dotted: "__".join(["PYTHON_IRODSCLIENT_CONFIG"] + uppercase_and_dot_split(_tuple.dotted)) | ||
| for _tuple in _var_item_tuples_as_generator() | ||
@@ -488,0 +479,0 @@ if _tuple.is_config |
+17
-41
@@ -34,5 +34,3 @@ #!/usr/bin/env python3 | ||
| if not overwrite and os.path.exists(auth_file): | ||
| raise irodsA_already_exists( | ||
| f"Overwriting not enabled and {auth_file} already exists." | ||
| ) | ||
| raise irodsA_already_exists(f"Overwriting not enabled and {auth_file} already exists.") | ||
| with _open_file_for_protected_contents(auth_file, "w") as irodsA: | ||
@@ -65,9 +63,4 @@ irodsA.write(obf.encode(encode_input)) | ||
| ses = kw.pop("_session", None) or h.make_session(**kw) | ||
| if ( | ||
| ses._server_version(iRODSSession.GET_SERVER_VERSION_WITHOUT_AUTH) < (4, 3) | ||
| or cfg.legacy_auth.force_legacy_auth | ||
| ): | ||
| return write_pam_credentials_to_secrets_file( | ||
| password, overwrite=overwrite, ttl=ttl, _session=ses | ||
| ) | ||
| if ses._server_version(iRODSSession.GET_SERVER_VERSION_WITHOUT_AUTH) < (4, 3) or cfg.legacy_auth.force_legacy_auth: | ||
| return write_pam_credentials_to_secrets_file(password, overwrite=overwrite, ttl=ttl, _session=ses) | ||
@@ -79,15 +72,7 @@ auth_file = ses.pool.account.derived_auth_file | ||
| if ttl: | ||
| ses.set_auth_option_for_scheme( | ||
| "pam_password", irods.auth.pam_password.AUTH_TTL_KEY, ttl | ||
| ) | ||
| ses.set_auth_option_for_scheme( | ||
| "pam_password", irods.auth.FORCE_PASSWORD_PROMPT, io.StringIO(password) | ||
| ) | ||
| ses.set_auth_option_for_scheme( | ||
| "pam_password", irods.auth.STORE_PASSWORD_IN_MEMORY, True | ||
| ) | ||
| ses.set_auth_option_for_scheme("pam_password", irods.auth.pam_password.AUTH_TTL_KEY, ttl) | ||
| ses.set_auth_option_for_scheme("pam_password", irods.auth.FORCE_PASSWORD_PROMPT, io.StringIO(password)) | ||
| ses.set_auth_option_for_scheme("pam_password", irods.auth.STORE_PASSWORD_IN_MEMORY, True) | ||
| L = [] | ||
| ses.set_auth_option_for_scheme( | ||
| "pam_password", irods.auth.CLIENT_GET_REQUEST_RESULT, L | ||
| ) | ||
| ses.set_auth_option_for_scheme("pam_password", irods.auth.CLIENT_GET_REQUEST_RESULT, L) | ||
| with ses.pool.get_connection() as _: | ||
@@ -109,9 +94,7 @@ _write_encoded_auth_value(auth_file, L[0], overwrite) | ||
| to_encode = [] | ||
| with cfg.loadlines( | ||
| [ | ||
| dict(setting="legacy_auth.pam.password_for_auto_renew", value=None), | ||
| dict(setting="legacy_auth.pam.store_password_to_environment", value=False), | ||
| dict(setting="legacy_auth.pam.time_to_live_in_hours", value=ttl), | ||
| ] | ||
| ): | ||
| with cfg.loadlines([ | ||
| dict(setting="legacy_auth.pam.password_for_auto_renew", value=None), | ||
| dict(setting="legacy_auth.pam.store_password_to_environment", value=False), | ||
| dict(setting="legacy_auth.pam.time_to_live_in_hours", value=ttl), | ||
| ]): | ||
| to_encode = s.pam_pw_negotiated | ||
@@ -123,2 +106,3 @@ if not to_encode: | ||
| def write_pam_interactive_irodsA_file(overwrite=True, ttl="", **kw): | ||
@@ -135,21 +119,13 @@ """Write credentials to an .irodsA file for PAM interactive authentication.""" | ||
| ses.set_auth_option_for_scheme( | ||
| "pam_interactive", irods.auth.FORCE_PASSWORD_PROMPT, True | ||
| ) | ||
| ses.set_auth_option_for_scheme("pam_interactive", irods.auth.FORCE_PASSWORD_PROMPT, True) | ||
| if ttl: | ||
| ses.set_auth_option_for_scheme( | ||
| "pam_interactive", "time_to_live_in_hours", ttl | ||
| ) | ||
| ses.set_auth_option_for_scheme("pam_interactive", "time_to_live_in_hours", ttl) | ||
| ses.set_auth_option_for_scheme( | ||
| "pam_interactive", irods.auth.STORE_PASSWORD_IN_MEMORY, True | ||
| ) | ||
| ses.set_auth_option_for_scheme("pam_interactive", irods.auth.STORE_PASSWORD_IN_MEMORY, True) | ||
| L = [] | ||
| ses.set_auth_option_for_scheme( | ||
| "pam_interactive", irods.auth.CLIENT_GET_REQUEST_RESULT, L | ||
| ) | ||
| ses.set_auth_option_for_scheme("pam_interactive", irods.auth.CLIENT_GET_REQUEST_RESULT, L) | ||
| with ses.pool.get_connection() as _: | ||
| _write_encoded_auth_value(auth_file, L[0], overwrite) |
+4
-16
@@ -17,3 +17,2 @@ import itertools | ||
| class iRODSCollection: | ||
| class AbsolutePathRequired(Exception): | ||
@@ -49,5 +48,3 @@ """Exception raised by iRODSCollection.normalize_path. | ||
| if not self._meta: | ||
| self._meta = iRODSMetaCollection( | ||
| self.manager.sess.metadata, Collection, self.path | ||
| ) | ||
| self._meta = iRODSMetaCollection(self.manager.sess.metadata, Collection, self.path) | ||
| return self._meta | ||
@@ -57,10 +54,4 @@ | ||
| def subcollections(self): | ||
| query = self.manager.sess.query(Collection).filter( | ||
| Collection.parent_name == self.path | ||
| ) | ||
| return [ | ||
| iRODSCollection(self.manager, row) | ||
| for row in query | ||
| if row[Collection.name] != "/" | ||
| ] | ||
| query = self.manager.sess.query(Collection).filter(Collection.parent_name == self.path) | ||
| return [iRODSCollection(self.manager, row) for row in query if row[Collection.name] != "/"] | ||
@@ -72,6 +63,3 @@ @property | ||
| grouped = itertools.groupby(results, operator.itemgetter(DataObject.id)) | ||
| return [ | ||
| iRODSDataObject(self.manager.sess.data_objects, self, list(replicas)) | ||
| for _, replicas in grouped | ||
| ] | ||
| return [iRODSDataObject(self.manager.sess.data_objects, self, list(replicas)) for _, replicas in grouped] | ||
@@ -78,0 +66,0 @@ def remove(self, recurse=True, force=False, **options): |
+3
-12
| from datetime import datetime, timezone | ||
| from calendar import timegm | ||
| class Column_remover: | ||
@@ -8,4 +9,4 @@ def __init__(self, column): | ||
| class QueryKey: | ||
| def __init__(self, column_type): | ||
@@ -34,3 +35,2 @@ self.column_type = column_type | ||
| class Criterion: | ||
| def __init__(self, op, query_key, value): | ||
@@ -47,3 +47,2 @@ self.op = op | ||
| class In(Criterion): | ||
| def __init__(self, query_key, value): | ||
@@ -64,3 +63,2 @@ super(In, self).__init__("in", query_key, value) | ||
| class Like(Criterion): | ||
| def __init__(self, query_key, value): | ||
@@ -71,3 +69,2 @@ super(Like, self).__init__("like", query_key, value) | ||
| class NotLike(Criterion): | ||
| def __init__(self, query_key, value): | ||
@@ -78,3 +75,2 @@ super(NotLike, self).__init__("not like", query_key, value) | ||
| class Between(Criterion): | ||
| def __init__(self, query_key, value): | ||
@@ -93,3 +89,2 @@ super(Between, self).__init__("between", query_key, value) | ||
| class Column(QueryKey): | ||
| def __init__(self, column_type, icat_key, icat_id, min_version=(0, 0, 0)): | ||
@@ -124,4 +119,4 @@ self.icat_key = icat_key | ||
| class Keyword(QueryKey): | ||
| def __init__(self, column_type, icat_key): | ||
@@ -134,3 +129,2 @@ self.icat_key = icat_key | ||
| class ColumnType: | ||
| @staticmethod | ||
@@ -146,3 +140,2 @@ def to_python(string): | ||
| class Integer(ColumnType): | ||
| @staticmethod | ||
@@ -158,3 +151,2 @@ def to_python(string): | ||
| class String(ColumnType): | ||
| @staticmethod | ||
@@ -176,3 +168,2 @@ def to_python(string): | ||
| class DateTime(ColumnType): | ||
| @staticmethod | ||
@@ -179,0 +170,0 @@ def to_python(string): |
+25
-92
@@ -73,3 +73,2 @@ import socket | ||
| class Connection: | ||
| DISALLOWING_PAM_PLAINTEXT = True | ||
@@ -103,6 +102,3 @@ | ||
| if ( | ||
| self.server_version >= (4, 3, 0) | ||
| and not cfg.legacy_auth.force_legacy_auth | ||
| ): | ||
| if self.server_version >= (4, 3, 0) and not cfg.legacy_auth.force_legacy_auth: | ||
| import irods.auth | ||
@@ -139,6 +135,3 @@ | ||
| def server_version(self): | ||
| detected = tuple( | ||
| int(x) | ||
| for x in self._server_version.relVersion.replace("rods", "").split(".") | ||
| ) | ||
| detected = tuple(int(x) for x in self._server_version.relVersion.replace("rods", "").split(".")) | ||
| return safe_eval(os.environ.get("IRODS_SERVER_VERSION", "()")) or detected | ||
@@ -192,8 +185,3 @@ | ||
| try: | ||
| err_msg = ( | ||
| iRODSMessage(msg=msg.error) | ||
| .get_main_message(Error) | ||
| .RErrMsg_PI[0] | ||
| .msg | ||
| ) | ||
| err_msg = iRODSMessage(msg=msg.error).get_main_message(Error).RErrMsg_PI[0].msg | ||
| except TypeError: | ||
@@ -240,11 +228,5 @@ err_msg = None | ||
| CApath = getattr(irods_account, "ssl_ca_certificate_path", None) | ||
| verify = ( | ||
| ssl.CERT_NONE | ||
| if ((None is CAfile is CApath) or verify_server == "none") | ||
| else ssl.CERT_REQUIRED | ||
| ) | ||
| verify = ssl.CERT_NONE if ((None is CAfile is CApath) or verify_server == "none") else ssl.CERT_REQUIRED | ||
| # See https://stackoverflow.com/questions/30461969/disable-default-certificate-verification-in-python-2-7-9/49040695#49040695 | ||
| ctx = ssl.create_default_context( | ||
| ssl.Purpose.SERVER_AUTH, cafile=CAfile, capath=CApath | ||
| ) | ||
| ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=CAfile, capath=CApath) | ||
| # Note: check_hostname must be assigned prior to verify_mode property or Python library complains! | ||
@@ -269,5 +251,3 @@ ctx.check_hostname = verify_server == "hostname" and verify != ssl.CERT_NONE | ||
| # Wrap socket with context | ||
| wrapped_socket = context.wrap_socket( | ||
| self.socket, server_hostname=(host if context.check_hostname else None) | ||
| ) | ||
| wrapped_socket = context.wrap_socket(self.socket, server_hostname=(host if context.check_hostname else None)) | ||
@@ -281,5 +261,3 @@ # Initial SSL handshake | ||
| # Send header-only message with client side encryption settings | ||
| packed_header = iRODSMessage.pack_header( | ||
| algo, key_size, salt_size, hash_rounds, 0 | ||
| ) | ||
| packed_header = iRODSMessage.pack_header(algo, key_size, salt_size, hash_rounds, 0) | ||
| wrapped_socket.sendall(packed_header) | ||
@@ -302,6 +280,3 @@ | ||
| except socket.error: | ||
| raise NetworkException( | ||
| "Could not connect to specified host and port: " | ||
| + "{}:{}".format(*address) | ||
| ) | ||
| raise NetworkException("Could not connect to specified host and port: " + "{}:{}".format(*address)) | ||
@@ -318,3 +293,2 @@ self.socket = s | ||
| if not self.requires_cs_negotiation(): | ||
| if len(main_message.option) >= LONG_NAME_LEN: | ||
@@ -353,5 +327,3 @@ message = "Application name too long." | ||
| # Perform the negotiation | ||
| neg_result, status = perform_negotiation( | ||
| client_policy=client_policy, server_policy=server_policy | ||
| ) | ||
| neg_result, status = perform_negotiation(client_policy=client_policy, server_policy=server_policy) | ||
@@ -368,7 +340,3 @@ # Send negotiation result to server | ||
| self.disconnect() | ||
| raise NetworkException( | ||
| "Client-Server negotiation failure: {},{}".format( | ||
| client_policy, server_policy | ||
| ) | ||
| ) | ||
| raise NetworkException("Client-Server negotiation failure: {},{}".format(client_policy, server_policy)) | ||
@@ -391,7 +359,3 @@ # Server responds with version | ||
| try: | ||
| if ( | ||
| self.socket | ||
| and getattr(self, "_disconnected", False) == False | ||
| and self.socket.fileno() != -1 | ||
| ): | ||
| if self.socket and getattr(self, "_disconnected", False) == False and self.socket.fileno() != -1: | ||
| disconnect_msg = iRODSMessage(msg_type="RODS_DISCONNECT") | ||
@@ -443,5 +407,3 @@ self.send(disconnect_msg) | ||
| # CLIENT CONTEXT | ||
| self.client_ctx = gssapi.SecurityContext( | ||
| name=server_name, mech=gsi_mech, flags=[2, 4], usage="initiate" | ||
| ) | ||
| self.client_ctx = gssapi.SecurityContext(name=server_name, mech=gsi_mech, flags=[2, 4], usage="initiate") | ||
@@ -485,3 +447,2 @@ def send_gsi_token(self, server_token=None): | ||
| while not (self.client_ctx.complete): | ||
| server_token = self.receive_gsi_token() | ||
@@ -503,5 +464,3 @@ | ||
| # https://github.com/irods/irods/blob/master/lib/api/include/apiNumber.h#L158 | ||
| auth_req = iRODSMessage( | ||
| msg_type="RODS_API_REQ", msg=message_body, int_info=1201 | ||
| ) | ||
| auth_req = iRODSMessage(msg_type="RODS_API_REQ", msg=message_body, int_info=1201) | ||
| self.send(auth_req) | ||
@@ -553,12 +512,6 @@ # Getting the challenge message | ||
| inline_password = ( | ||
| self.account.authentication_scheme | ||
| == self.account._original_authentication_scheme | ||
| ) | ||
| inline_password = self.account.authentication_scheme == self.account._original_authentication_scheme | ||
| time_to_live_in_hours = cfg.legacy_auth.pam.time_to_live_in_hours | ||
| new_pam_password = self.account.password | ||
| if ( | ||
| not inline_password | ||
| and cfg.legacy_auth.pam.password_for_auto_renew is not None | ||
| ): | ||
| if not inline_password and cfg.legacy_auth.pam.password_for_auto_renew is not None: | ||
| # Login using PAM password from .irodsA | ||
@@ -583,5 +536,3 @@ try: | ||
| KVP_ESCAPED_CHARS = r"\;=" | ||
| kvp_escape = lambda s: "".join( | ||
| (rf"\{c}" if c in KVP_ESCAPED_CHARS else c) for c in s | ||
| ) | ||
| kvp_escape = lambda s: "".join((rf"\{c}" if c in KVP_ESCAPED_CHARS else c) for c in s) | ||
@@ -603,5 +554,3 @@ # Generate a new PAM password. | ||
| use_dedicated_pam_api = cfg.legacy_auth.pam.force_use_of_dedicated_pam_api or ( | ||
| len(ctx) >= MAX_NAME_LEN | ||
| ) | ||
| use_dedicated_pam_api = cfg.legacy_auth.pam.force_use_of_dedicated_pam_api or (len(ctx) >= MAX_NAME_LEN) | ||
@@ -617,9 +566,5 @@ if use_dedicated_pam_api: | ||
| api_name = ( | ||
| "PAM_AUTH_REQUEST_AN" if use_dedicated_pam_api else "AUTH_PLUG_REQ_AN" | ||
| ) | ||
| api_name = "PAM_AUTH_REQUEST_AN" if use_dedicated_pam_api else "AUTH_PLUG_REQ_AN" | ||
| auth_req = iRODSMessage( | ||
| msg_type="RODS_API_REQ", msg=message_body, int_info=api_number[api_name] | ||
| ) | ||
| auth_req = iRODSMessage(msg_type="RODS_API_REQ", msg=message_body, int_info=api_number[api_name]) | ||
@@ -635,5 +580,3 @@ self.send(auth_req) | ||
| Pam_Response_Class = ( | ||
| PamAuthRequestOut if use_dedicated_pam_api else AuthPluginOut | ||
| ) | ||
| Pam_Response_Class = PamAuthRequestOut if use_dedicated_pam_api else AuthPluginOut | ||
@@ -676,5 +619,3 @@ auth_out = output_message.get_main_message(Pam_Response_Class) | ||
| ) | ||
| message = iRODSMessage( | ||
| "RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_READ_AN"] | ||
| ) | ||
| message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_READ_AN"]) | ||
@@ -711,5 +652,3 @@ logger.debug(desc) | ||
| challenge = challenge.strip() | ||
| padded_pwd = struct.pack( | ||
| "%ds" % MAX_PASSWORD_LENGTH, password.encode("utf-8").strip() | ||
| ) | ||
| padded_pwd = struct.pack("%ds" % MAX_PASSWORD_LENGTH, password.encode("utf-8").strip()) | ||
@@ -765,5 +704,3 @@ m = hashlib.md5() | ||
| ) | ||
| message = iRODSMessage( | ||
| "RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_LSEEK_AN"] | ||
| ) | ||
| message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_LSEEK_AN"]) | ||
@@ -785,5 +722,3 @@ self.send(message) | ||
| ) | ||
| message = iRODSMessage( | ||
| "RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_CLOSE_AN"] | ||
| ) | ||
| message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_CLOSE_AN"]) | ||
@@ -794,5 +729,3 @@ self.send(message) | ||
| def temp_password(self): | ||
| request = iRODSMessage( | ||
| "RODS_API_REQ", msg=None, int_info=api_number["GET_TEMP_PASSWORD_AN"] | ||
| ) | ||
| request = iRODSMessage("RODS_API_REQ", msg=None, int_info=api_number["GET_TEMP_PASSWORD_AN"]) | ||
@@ -799,0 +732,0 @@ # Send and receive request |
+27
-39
@@ -31,3 +31,2 @@ import io | ||
| class iRODSReplica: | ||
| def __init__(self, number, status, resource_name, path, resc_hier, **kwargs): | ||
@@ -43,9 +42,6 @@ self.number = number | ||
| def __repr__(self): | ||
| return "<{}.{} {}>".format( | ||
| self.__class__.__module__, self.__class__.__name__, self.resource_name | ||
| ) | ||
| return "<{}.{} {}>".format(self.__class__.__module__, self.__class__.__name__, self.resource_name) | ||
| class iRODSDataObject: | ||
| def __init__(self, manager, parent=None, results=None): | ||
@@ -67,17 +63,21 @@ self.manager = manager | ||
| replica_args = [( | ||
| (r[DataObject.replica_number], | ||
| r[DataObject.replica_status], | ||
| r[DataObject.resource_name], | ||
| r[DataObject.path], | ||
| r[DataObject.resc_hier], | ||
| ) | ||
| ,dict( | ||
| checksum=r[DataObject.checksum], | ||
| size=r[DataObject.size], | ||
| comments=r[DataObject.comments], | ||
| create_time=r[DataObject.create_time], | ||
| modify_time=r[DataObject.modify_time], | ||
| replica_args = [ | ||
| ( | ||
| ( | ||
| r[DataObject.replica_number], | ||
| r[DataObject.replica_status], | ||
| r[DataObject.resource_name], | ||
| r[DataObject.path], | ||
| r[DataObject.resc_hier], | ||
| ), | ||
| dict( | ||
| checksum=r[DataObject.checksum], | ||
| size=r[DataObject.size], | ||
| comments=r[DataObject.comments], | ||
| create_time=r[DataObject.create_time], | ||
| modify_time=r[DataObject.modify_time], | ||
| ), | ||
| ) | ||
| ) for r in replicas] | ||
| for r in replicas | ||
| ] | ||
@@ -87,5 +87,5 @@ # Adjust for adding access_time in the iRODS 5 case. | ||
| if self.manager.sess.server_version >= (5,): | ||
| for n,r in enumerate(replicas): | ||
| for n, r in enumerate(replicas): | ||
| replica_args[n][1]['access_time'] = r[DataObject.access_time] | ||
| self.replicas = [iRODSReplica(*a,**k) for a,k in replica_args] | ||
| self.replicas = [iRODSReplica(*a, **k) for a, k in replica_args] | ||
@@ -100,11 +100,7 @@ self._meta = None | ||
| if not self._meta: | ||
| self._meta = iRODSMetaCollection( | ||
| self.manager.sess.metadata, DataObject, self.path | ||
| ) | ||
| self._meta = iRODSMetaCollection(self.manager.sess.metadata, DataObject, self.path) | ||
| return self._meta | ||
| def open(self, mode="r", finalize_on_close=True, **options): | ||
| return self.manager.open( | ||
| self.path, mode, finalize_on_close=finalize_on_close, **options | ||
| ) | ||
| return self.manager.open(self.path, mode, finalize_on_close=finalize_on_close, **options) | ||
@@ -161,5 +157,3 @@ def chksum(self, **options): | ||
| def replica_access_info(self): | ||
| message_body = JSON_Message( | ||
| {"fd": self.desc}, server_version=self.conn.server_version | ||
| ) | ||
| message_body = JSON_Message({"fd": self.desc}, server_version=self.conn.server_version) | ||
| message = iRODSMessage( | ||
@@ -183,14 +177,8 @@ "RODS_API_REQ", | ||
| replica_token = dobj_info.get("replica_token", "") | ||
| resc_hier = (dobj_info.get("data_object_info") or {}).get( | ||
| "resource_hierarchy", "" | ||
| ) | ||
| resc_hier = (dobj_info.get("data_object_info") or {}).get("resource_hierarchy", "") | ||
| return (replica_token, resc_hier) | ||
| def _close_replica(self): | ||
| server_version = ast.literal_eval( | ||
| os.environ.get("IRODS_VERSION_OVERRIDE", "()") | ||
| ) | ||
| if ( | ||
| server_version or self.conn.server_version | ||
| ) < IRODS_SERVER_WITH_CLOSE_REPLICA_API: | ||
| server_version = ast.literal_eval(os.environ.get("IRODS_VERSION_OVERRIDE", "()")) | ||
| if (server_version or self.conn.server_version) < IRODS_SERVER_WITH_CLOSE_REPLICA_API: | ||
| return False | ||
@@ -197,0 +185,0 @@ message_body = JSON_Message( |
+9
-25
@@ -92,5 +92,3 @@ # if you're copying these from the docs, you might find the following regex helpful: | ||
| codes: "Dict[int, iRODSException]" = {} | ||
| positive_code_error_message = ( | ||
| "For {name}, a positive code of {attrs[code]} was declared." | ||
| ) | ||
| positive_code_error_message = "For {name}, a positive code of {attrs[code]} was declared." | ||
@@ -100,5 +98,3 @@ def __init__(self, name, bases, attrs): | ||
| if attrs["code"] > 0: | ||
| print( | ||
| self.positive_code_error_message.format(**locals()), file=sys.stderr | ||
| ) | ||
| print(self.positive_code_error_message.format(**locals()), file=sys.stderr) | ||
| exit(1) | ||
@@ -122,5 +118,3 @@ iRODSExceptionMeta.codes[attrs["code"]] = self | ||
| try: | ||
| return self.__class__.__name__ + repr( | ||
| tuple([e, errno.errorcode[e], os.strerror(e)]) | ||
| ) | ||
| return self.__class__.__name__ + repr(tuple([e, errno.errorcode[e], os.strerror(e)])) | ||
| except: | ||
@@ -130,7 +124,5 @@ # The errno code is unrecognized, so fall through to default representation. | ||
| return self.__class__.__name__ + repr( | ||
| tuple( | ||
| [ | ||
| e, | ||
| ] | ||
| ) | ||
| tuple([ | ||
| e, | ||
| ]) | ||
| ) | ||
@@ -182,5 +174,3 @@ | ||
| negated = -abs(nominal[0]) | ||
| return ( | ||
| c if (negated <= -abs(THRESHOLD)) else negated | ||
| ) # produce a negative for nonzero integer input | ||
| return c if (negated <= -abs(THRESHOLD)) else negated # produce a negative for nonzero integer input | ||
@@ -200,5 +190,3 @@ | ||
| else: | ||
| message = "Supplied code {the_code!r} must be integer or string".format( | ||
| **locals() | ||
| ) | ||
| message = "Supplied code {the_code!r} must be integer or string".format(**locals()) | ||
| raise RuntimeError(message) | ||
@@ -213,7 +201,3 @@ finally: | ||
| cls = iRODSExceptionMeta.codes.get(rounded) | ||
| return ( | ||
| cls | ||
| if not name_only | ||
| else (cls.__name__ if cls is not None else "Unknown_iRODS_error") | ||
| ) | ||
| return cls if not name_only else (cls.__name__ if cls is not None else "Unknown_iRODS_error") | ||
@@ -220,0 +204,0 @@ |
@@ -20,5 +20,3 @@ import json | ||
| if not self._is_supported(): | ||
| raise OperationNotSupported( | ||
| "GenQuery2 is not supported by default on this iRODS version." | ||
| ) | ||
| raise OperationNotSupported("GenQuery2 is not supported by default on this iRODS version.") | ||
@@ -37,5 +35,3 @@ def execute(self, query, zone=None): | ||
| effective_zone = self.session.zone if zone is None else zone | ||
| return json.loads( | ||
| self._exec_genquery2("", effective_zone, column_mappings_flag=True) | ||
| ) | ||
| return json.loads(self._exec_genquery2("", effective_zone, column_mappings_flag=True)) | ||
@@ -48,5 +44,3 @@ def _exec_genquery2(self, query, zone, sql_flag=False, column_mappings_flag=False): | ||
| msg.column_mappings = 1 if column_mappings_flag else 0 | ||
| message = iRODSMessage( | ||
| "RODS_API_REQ", msg=msg, int_info=api_number["GENQUERY2_AN"] | ||
| ) | ||
| message = iRODSMessage("RODS_API_REQ", msg=msg, int_info=api_number["GENQUERY2_AN"]) | ||
| with self.session.pool.get_connection() as conn: | ||
@@ -53,0 +47,0 @@ conn.send(message) |
| import contextlib | ||
| import os | ||
| import sys | ||
| import irods.exception as ex | ||
| from irods import env_filename_from_keyword_args | ||
| import irods.exception as ex | ||
| from irods.message import ET, XML_Parser_Type, IRODS_VERSION | ||
| from irods.message import _IRODS_VERSION, ET, XML_Parser_Type | ||
| from irods.path import iRODSPath | ||
@@ -18,4 +19,4 @@ from irods.session import iRODSSession | ||
| class StopTestsException(Exception): | ||
| def __init__(self, *args, **kwargs): | ||
@@ -33,5 +34,3 @@ super().__init__(*args, **kwargs) | ||
| def _get_server_version_for_test(session, curtail_length): | ||
| return session._server_version(session.GET_SERVER_VERSION_WITHOUT_AUTH)[ | ||
| :curtail_length | ||
| ] | ||
| return session._server_version(session.GET_SERVER_VERSION_WITHOUT_AUTH)[:curtail_length] | ||
@@ -59,5 +58,8 @@ | ||
| session = iRODSSession(irods_env_file=env_file, **kwargs) | ||
| # irods.test.helpers version of this function sets test_server_version True by default, so | ||
| # that sessions generated for the test methods will abort on connecting with a server that | ||
| # is too recent. This is a way to ensure that tests don't fail due to a server mismatch. | ||
| if test_server_version: | ||
| connected_version = _get_server_version_for_test(session, curtail_length=3) | ||
| advertised_version = IRODS_VERSION[:3] | ||
| advertised_version = _IRODS_VERSION[:3] | ||
| if connected_version > advertised_version: | ||
@@ -106,5 +108,3 @@ msg = ( | ||
| @contextlib.contextmanager | ||
| def temporarily_assign_attribute( | ||
| target, attr, value, not_set_indicator=_unlikely_value() | ||
| ): | ||
| def temporarily_assign_attribute(target, attr, value, not_set_indicator=_unlikely_value()): | ||
| save = not_set_indicator | ||
@@ -153,3 +153,3 @@ try: | ||
| # Utility class and factory function for storing the original value of variables within the given namespace. | ||
| def create_value_cache(namespace:dict): | ||
| def create_value_cache(namespace: dict): | ||
| class CachedValues: | ||
@@ -161,4 +161,4 @@ __namespace = namespace | ||
| cached_value = cls.__namespace[name] | ||
| setattr(cls,name,property(lambda self: cached_value)) | ||
| setattr(cls, name, property(lambda self: cached_value)) | ||
| return CachedValues() |
+6
-18
@@ -98,13 +98,7 @@ """From rodsKeyWdDef.hpp""" | ||
| DRYRUN_KW = "dryrun" # do a dry run # | ||
| ACL_COLLECTION_KW = ( | ||
| "aclCollection" # the collection from which the ACL should be used # | ||
| ) | ||
| ACL_COLLECTION_KW = "aclCollection" # the collection from which the ACL should be used # | ||
| NO_CHK_COPY_LEN_KW = "noChkCopyLen" # Don't check the len when transfering # | ||
| TICKET_KW = "ticket" # for ticket-based-access # | ||
| PURGE_CACHE_KW = ( | ||
| "purgeCache" # purge the cache copy right after the operation JMC - backport 4537 | ||
| ) | ||
| EMPTY_BUNDLE_ONLY_KW = ( | ||
| "emptyBundleOnly" # delete emptyBundleOnly # # JMC - backport 4552 | ||
| ) | ||
| PURGE_CACHE_KW = "purgeCache" # purge the cache copy right after the operation JMC - backport 4537 | ||
| EMPTY_BUNDLE_ONLY_KW = "emptyBundleOnly" # delete emptyBundleOnly # # JMC - backport 4552 | ||
| GET_RESOURCE_INFO_OP_TYPE_KW = "getResourceInfoOpType" | ||
@@ -114,8 +108,4 @@ | ||
| # JMC - backport 4599 | ||
| LOCK_TYPE_KW = ( | ||
| "lockType" # valid values are READ_LOCK_TYPE, WRITE_LOCK_TYPE and UNLOCK_TYPE # | ||
| ) | ||
| LOCK_CMD_KW = ( | ||
| "lockCmd" # valid values are SET_LOCK_WAIT_CMD, SET_LOCK_CMD and GET_LOCK_CMD # | ||
| ) | ||
| LOCK_TYPE_KW = "lockType" # valid values are READ_LOCK_TYPE, WRITE_LOCK_TYPE and UNLOCK_TYPE # | ||
| LOCK_CMD_KW = "lockCmd" # valid values are SET_LOCK_WAIT_CMD, SET_LOCK_CMD and GET_LOCK_CMD # | ||
| LOCK_FD_KW = "lockFd" # Lock file desc for unlock # | ||
@@ -127,5 +117,3 @@ MAX_SUB_FILE_KW = "maxSubFile" # max number of files for tar file bundles # | ||
| # =-=-=-=-=-=-=- | ||
| MAX_SUB_FILE_KW = ( | ||
| "maxSubFile" # max number of files for tar file bundles # # JMC - backport 4771 | ||
| ) | ||
| MAX_SUB_FILE_KW = "maxSubFile" # max number of files for tar file bundles # # JMC - backport 4771 | ||
@@ -132,0 +120,0 @@ # OBJ_PATH_KW already defined # |
| class Manager: | ||
| __server_version = () | ||
@@ -4,0 +3,0 @@ |
@@ -7,9 +7,5 @@ from irods.api_number import api_number | ||
| with session.pool.get_connection() as conn: | ||
| message_body = JSON_Message( | ||
| {"logical_path": path, "options": options}, conn.server_version | ||
| ) | ||
| message = iRODSMessage( | ||
| "RODS_API_REQ", msg=message_body, int_info=api_number["TOUCH_APN"] | ||
| ) | ||
| message_body = JSON_Message({"logical_path": path, "options": options}, conn.server_version) | ||
| message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["TOUCH_APN"]) | ||
| conn.send(message) | ||
| response = conn.recv() |
@@ -34,25 +34,11 @@ from os.path import basename, dirname | ||
| raise | ||
| cond = ( | ||
| () | ||
| if not ids | ||
| else ( | ||
| (In(User.id, list(map(int, ids))),) | ||
| if len(ids) > 1 | ||
| else (User.id == int(ids[0]),) | ||
| ) | ||
| ) | ||
| return [ | ||
| iRODSUser(session.users, i) | ||
| for i in session.query(User.id, User.name, User.type, User.zone).filter(*cond) | ||
| ] | ||
| cond = () if not ids else ((In(User.id, list(map(int, ids))),) if len(ids) > 1 else (User.id == int(ids[0]),)) | ||
| return [iRODSUser(session.users, i) for i in session.query(User.id, User.name, User.type, User.zone).filter(*cond)] | ||
| class AccessManager(Manager): | ||
| def get(self, target, report_raw_acls=True, **kw): | ||
| if report_raw_acls: | ||
| return self.__get_raw( | ||
| target, **kw | ||
| ) # prefer a behavior consistent with 'ils -A` | ||
| return self.__get_raw(target, **kw) # prefer a behavior consistent with 'ils -A` | ||
@@ -74,7 +60,3 @@ # different query whether target is an object or a collection | ||
| results = ( | ||
| self.sess.query(user_type.name, user_type.zone, access_type.name) | ||
| .filter(*conditions) | ||
| ._all() | ||
| ) | ||
| results = self.sess.query(user_type.name, user_type.zone, access_type.name).filter(*conditions)._all() | ||
@@ -96,5 +78,3 @@ def get_usertype(row): | ||
| def coll_access_query(self, path): | ||
| return self.sess.query(Collection, CollectionAccess).filter( | ||
| Collection.name == path | ||
| ) | ||
| return self.sess.query(Collection, CollectionAccess).filter(Collection.name == path) | ||
@@ -104,5 +84,3 @@ def data_access_query(self, path): | ||
| dn = irods_basename(path) | ||
| return self.sess.query(DataObject, DataAccess).filter( | ||
| Collection.name == cn, DataObject.name == dn | ||
| ) | ||
| return self.sess.query(DataObject, DataAccess).filter(Collection.name == cn, DataObject.name == dn) | ||
@@ -138,5 +116,3 @@ def __get_raw(self, target, **kw): | ||
| extant_ids = set(u[User.id] for u in self.sess.query(User)) | ||
| rows = [ | ||
| r for r in query_func(target.path) if r[access_column.user_id] in extant_ids | ||
| ] | ||
| rows = [r for r in query_func(target.path) if r[access_column.user_id] in extant_ids] | ||
| userids = set(r[access_column.user_id] for r in rows) | ||
@@ -160,14 +136,12 @@ | ||
| acls = list( | ||
| { | ||
| iRODSAccess( | ||
| r[access_column.name], | ||
| target.path, | ||
| user_lookup[r[access_column.user_id]].name, | ||
| user_lookup[r[access_column.user_id]].zone, | ||
| user_lookup[r[access_column.user_id]].type, | ||
| ) | ||
| for r in rows | ||
| } | ||
| ) | ||
| acls = list({ | ||
| iRODSAccess( | ||
| r[access_column.name], | ||
| target.path, | ||
| user_lookup[r[access_column.user_id]].name, | ||
| user_lookup[r[access_column.user_id]].zone, | ||
| user_lookup[r[access_column.user_id]].type, | ||
| ) | ||
| for r in rows | ||
| }) | ||
| return acls | ||
@@ -174,0 +148,0 @@ |
@@ -19,3 +19,2 @@ from irods.models import Collection, DataObject | ||
| class CollectionManager(Manager): | ||
| def get(self, path): | ||
@@ -42,8 +41,4 @@ path = iRODSCollection.normalize_path(path) | ||
| message_body = CollectionRequest( | ||
| collName=path, KeyValPair_PI=StringStringMap(options) | ||
| ) | ||
| message = iRODSMessage( | ||
| "RODS_API_REQ", msg=message_body, int_info=api_number["COLL_CREATE_AN"] | ||
| ) | ||
| message_body = CollectionRequest(collName=path, KeyValPair_PI=StringStringMap(options)) | ||
| message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["COLL_CREATE_AN"]) | ||
| with self.sess.pool.get_connection() as conn: | ||
@@ -71,5 +66,3 @@ conn.send(message) | ||
| ) | ||
| message = iRODSMessage( | ||
| "RODS_API_REQ", msg=message_body, int_info=api_number["RM_COLL_AN"] | ||
| ) | ||
| message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["RM_COLL_AN"]) | ||
| with self.sess.pool.get_connection() as conn: | ||
@@ -126,5 +119,3 @@ conn.send(message) | ||
| message_body = ObjCopyRequest(srcDataObjInp_PI=src, destDataObjInp_PI=dest) | ||
| message = iRODSMessage( | ||
| "RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_RENAME_AN"] | ||
| ) | ||
| message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_RENAME_AN"]) | ||
@@ -149,5 +140,3 @@ with self.sess.pool.get_connection() as conn: | ||
| ) | ||
| message = iRODSMessage( | ||
| "RODS_API_REQ", msg=message_body, int_info=api_number["PHY_PATH_REG_AN"] | ||
| ) | ||
| message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["PHY_PATH_REG_AN"]) | ||
@@ -154,0 +143,0 @@ with self.sess.pool.get_connection() as conn: |
@@ -69,5 +69,3 @@ import ast | ||
| _update_types[:] = list( | ||
| (k, v) | ||
| for k, v in collections.OrderedDict([(type_, factory_)] + _update_types).items() | ||
| if v is not None | ||
| (k, v) for k, v in collections.OrderedDict([(type_, factory_)] + _update_types).items() if v is not None | ||
| ) | ||
@@ -107,5 +105,3 @@ | ||
| else: | ||
| logging_function( | ||
| "Could not derive an update function for: %r", object_ | ||
| ) | ||
| logging_function("Could not derive an update function for: %r", object_) | ||
| continue | ||
@@ -129,3 +125,2 @@ | ||
| class ManagedBufferedRandom(io.BufferedRandom): | ||
| def __init__(self, *a, **kwd): | ||
@@ -136,9 +131,13 @@ # Help ensure proper teardown sequence by storing a reference to the session, | ||
| super(ManagedBufferedRandom, self).__init__(*a, **kwd) | ||
| self.do_close = True | ||
| import irods.session | ||
| with irods.session._fds_lock: | ||
| irods.session._fds[self] = None | ||
| if irods.session._fds is not None: | ||
| irods.session._fds[self] = None | ||
| def __del__(self): | ||
| if not self.closed: | ||
| if self.do_close and not self.closed: | ||
| self.close() | ||
@@ -150,5 +149,3 @@ call___del__if_exists(super(ManagedBufferedRandom, self)) | ||
| DEFAULT_NUMBER_OF_THREADS = ( | ||
| 0 # Defaults for reasonable number of threads -- optimized to be | ||
| ) | ||
| DEFAULT_NUMBER_OF_THREADS = 0 # Defaults for reasonable number of threads -- optimized to be | ||
| # performant but allow no more worker threads than available CPUs. | ||
@@ -171,3 +168,2 @@ | ||
| class DataObjectManager(Manager): | ||
| READ_BUFFER_SIZE = 1024 * io.DEFAULT_BUFFER_SIZE | ||
@@ -225,5 +221,3 @@ WRITE_BUFFER_SIZE = 1024 * io.DEFAULT_BUFFER_SIZE | ||
| return obj_sz > MAXIMUM_SINGLE_THREADED_TRANSFER_SIZE | ||
| message = "obj_sz of {obj_sz!r} is neither an integer nor a seekable object".format( | ||
| **locals() | ||
| ) | ||
| message = "obj_sz of {obj_sz!r} is neither an integer nor a seekable object".format(**locals()) | ||
| raise RuntimeError(message) | ||
@@ -249,17 +243,19 @@ finally: | ||
| data_open_returned_values_ = {} | ||
| with self.open( | ||
| obj, "r", returned_values=data_open_returned_values_, **options | ||
| ) as o: | ||
| if self.should_parallelize_transfer( | ||
| num_threads, o, open_options=options.items() | ||
| ): | ||
| if not self.parallel_get( | ||
| (obj, o), | ||
| local_file, | ||
| num_threads=num_threads, | ||
| target_resource_name=options.get(kw.RESC_NAME_KW, ""), | ||
| data_open_returned_values=data_open_returned_values_, | ||
| updatables=updatables, | ||
| ): | ||
| raise RuntimeError("parallel get failed") | ||
| with self.open(obj, "r", returned_values=data_open_returned_values_, **options) as o: | ||
| if self.should_parallelize_transfer(num_threads, o, open_options=options.items()): | ||
| error = RuntimeError("parallel get failed") | ||
| try: | ||
| if not self.parallel_get( | ||
| (obj, o), | ||
| local_file, | ||
| num_threads=num_threads, | ||
| target_resource_name=options.get(kw.RESC_NAME_KW, ""), | ||
| data_open_returned_values=data_open_returned_values_, | ||
| updatables=updatables, | ||
| ): | ||
| raise error | ||
| except ex.iRODSException as e: | ||
| raise e | ||
| except BaseException as e: | ||
| raise error from e | ||
| else: | ||
@@ -271,10 +267,3 @@ with open(local_file, "wb") as f: | ||
| def get( | ||
| self, | ||
| path, | ||
| local_path=None, | ||
| num_threads=DEFAULT_NUMBER_OF_THREADS, | ||
| updatables=(), | ||
| **options | ||
| ): | ||
| def get(self, path, local_path=None, num_threads=DEFAULT_NUMBER_OF_THREADS, updatables=(), **options): | ||
| """ | ||
@@ -290,12 +279,7 @@ Get a reference to the data object at the specified `path'. | ||
| if local_path: | ||
| self._download( | ||
| path, | ||
| local_path, | ||
| num_threads=num_threads, | ||
| updatables=updatables, | ||
| **options | ||
| ) | ||
| self._download(path, local_path, num_threads=num_threads, updatables=updatables, **options) | ||
| query = ( | ||
| self.sess.query(DataObject) | ||
| self.sess | ||
| .query(DataObject) | ||
| .filter(DataObject.name == irods_basename(path)) | ||
@@ -307,5 +291,3 @@ .filter(DataObject.collection_id == parent.id) | ||
| if self.sess.ticket__: | ||
| query = query.filter( | ||
| Collection.id != 0 | ||
| ) # a no-op, but necessary because CAT_SQL_ERR results if the ticket | ||
| query = query.filter(Collection.id != 0) # a no-op, but necessary because CAT_SQL_ERR results if the ticket | ||
| # is for a DataObject and we don't explicitly join to Collection | ||
@@ -343,13 +325,9 @@ | ||
| updatables=(), | ||
| **options | ||
| **options, | ||
| ): | ||
| # Decide if a put option should be used and modify options accordingly. | ||
| self._resolve_force_put_option( | ||
| options, default_setting=client_config.data_objects.force_put_by_default | ||
| ) | ||
| self._resolve_force_put_option(options, default_setting=client_config.data_objects.force_put_by_default) | ||
| if self.sess.collections.exists(irods_path): | ||
| obj = iRODSCollection.normalize_path( | ||
| irods_path, os.path.basename(local_path) | ||
| ) | ||
| obj = iRODSCollection.normalize_path(irods_path, os.path.basename(local_path)) | ||
| else: | ||
@@ -363,18 +341,21 @@ obj = irods_path | ||
| sizelist = [] | ||
| if self.should_parallelize_transfer( | ||
| num_threads, f, measured_obj_size=sizelist, open_options=options | ||
| ): | ||
| if self.should_parallelize_transfer(num_threads, f, measured_obj_size=sizelist, open_options=options): | ||
| o = deferred_call(self.open, (obj, "w"), options) | ||
| f.close() | ||
| if not self.parallel_put( | ||
| local_path, | ||
| (obj, o), | ||
| total_bytes=sizelist[0], | ||
| num_threads=num_threads, | ||
| target_resource_name=options.get(kw.RESC_NAME_KW, "") | ||
| or options.get(kw.DEST_RESC_NAME_KW, ""), | ||
| open_options=options, | ||
| updatables=updatables, | ||
| ): | ||
| raise RuntimeError("parallel put failed") | ||
| error = RuntimeError("parallel put failed") | ||
| try: | ||
| if not self.parallel_put( | ||
| local_path, | ||
| (obj, o), | ||
| total_bytes=sizelist[0], | ||
| num_threads=num_threads, | ||
| target_resource_name=options.get(kw.RESC_NAME_KW, "") or options.get(kw.DEST_RESC_NAME_KW, ""), | ||
| open_options=options, | ||
| updatables=updatables, | ||
| ): | ||
| raise error | ||
| except ex.iRODSException as e: | ||
| raise e | ||
| except BaseException as e: | ||
| raise error from e | ||
| else: | ||
@@ -407,5 +388,3 @@ with self.open(obj, "w", **options) as o: | ||
| message_body = DataObjChksumRequest(path, **options) | ||
| message = iRODSMessage( | ||
| "RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_CHKSUM_AN"] | ||
| ) | ||
| message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_CHKSUM_AN"]) | ||
| checksum = "" | ||
@@ -421,8 +400,6 @@ msg_retn = [] | ||
| response = msg_retn[0] | ||
| logging.warning("Exception checksumming data object %r - %r", path, exc) | ||
| logger.warning("Exception checksumming data object %r - %r", path, exc) | ||
| if "response" in locals(): | ||
| try: | ||
| results = response.get_main_message( | ||
| DataObjChksumResponse, r_error=r_error_stack | ||
| ) | ||
| results = response.get_main_message(DataObjChksumResponse, r_error=r_error_stack) | ||
| checksum = results.myStr.strip() | ||
@@ -508,7 +485,3 @@ if checksum[0] in ( | ||
| def create( | ||
| self, | ||
| path, | ||
| resource=None, | ||
| force=client_config.getter("data_objects", "force_create_by_default"), | ||
| **options | ||
| self, path, resource=None, force=client_config.getter("data_objects", "force_create_by_default"), **options | ||
| ): | ||
@@ -546,5 +519,3 @@ """ | ||
| ) | ||
| message = iRODSMessage( | ||
| "RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_CREATE_AN"] | ||
| ) | ||
| message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_CREATE_AN"]) | ||
@@ -564,9 +535,7 @@ with self.sess.pool.get_connection() as conn: | ||
| _RESC_flags_for_open = frozenset( | ||
| ( | ||
| kw.RESC_NAME_KW, | ||
| kw.DEST_RESC_NAME_KW, # may be deprecated in the future | ||
| kw.RESC_HIER_STR_KW, | ||
| ) | ||
| ) | ||
| _RESC_flags_for_open = frozenset(( | ||
| kw.RESC_NAME_KW, | ||
| kw.DEST_RESC_NAME_KW, # may be deprecated in the future | ||
| kw.RESC_HIER_STR_KW, | ||
| )) | ||
@@ -585,3 +554,3 @@ def open( | ||
| allow_redirect=client_config.getter("data_objects", "allow_redirect"), | ||
| **options | ||
| **options, | ||
| ): | ||
@@ -681,11 +650,7 @@ _raw_fd_holder = options.get("_raw_fd_holder", []) | ||
| # Perform DATA_OBJ_OPEN call | ||
| message = iRODSMessage( | ||
| "RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_OPEN_AN"] | ||
| ) | ||
| message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_OPEN_AN"]) | ||
| conn.send(message) | ||
| desc = conn.recv().int_info | ||
| raw = iRODSDataObjectFileRaw( | ||
| conn, desc, finalize_on_close=finalize_on_close, **options | ||
| ) | ||
| raw = iRODSDataObjectFileRaw(conn, desc, finalize_on_close=finalize_on_close, **options) | ||
| raw.session = directed_sess | ||
@@ -715,5 +680,3 @@ | ||
| if self.sess.server_version < required_server_version: | ||
| raise ex.NotImplementedInIRODSServer( | ||
| "replica_truncate", required_server_version | ||
| ) | ||
| raise ex.NotImplementedInIRODSServer("replica_truncate", required_server_version) | ||
@@ -730,5 +693,3 @@ message_body = FileOpenRequest( | ||
| ) | ||
| message = iRODSMessage( | ||
| "RODS_API_REQ", msg=message_body, int_info=api_number["REPLICA_TRUNCATE_AN"] | ||
| ) | ||
| message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["REPLICA_TRUNCATE_AN"]) | ||
@@ -759,5 +720,3 @@ with self.sess.pool.get_connection() as conn: | ||
| ) | ||
| message = iRODSMessage( | ||
| "RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_TRIM_AN"] | ||
| ) | ||
| message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_TRIM_AN"]) | ||
@@ -787,5 +746,3 @@ with self.sess.pool.get_connection() as conn: | ||
| ) | ||
| message = iRODSMessage( | ||
| "RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_UNLINK_AN"] | ||
| ) | ||
| message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_UNLINK_AN"]) | ||
@@ -843,5 +800,3 @@ with self.sess.pool.get_connection() as conn: | ||
| message_body = ObjCopyRequest(srcDataObjInp_PI=src, destDataObjInp_PI=dest) | ||
| message = iRODSMessage( | ||
| "RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_RENAME_AN"] | ||
| ) | ||
| message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_RENAME_AN"]) | ||
@@ -882,5 +837,3 @@ with self.sess.pool.get_connection() as conn: | ||
| message_body = ObjCopyRequest(srcDataObjInp_PI=src, destDataObjInp_PI=dest) | ||
| message = iRODSMessage( | ||
| "RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_COPY_AN"] | ||
| ) | ||
| message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_COPY_AN"]) | ||
@@ -930,5 +883,3 @@ with self.sess.pool.get_connection() as conn: | ||
| ) | ||
| message = iRODSMessage( | ||
| "RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_REPL_AN"] | ||
| ) | ||
| message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_REPL_AN"]) | ||
@@ -952,5 +903,3 @@ with self.sess.pool.get_connection() as conn: | ||
| ) | ||
| message = iRODSMessage( | ||
| "RODS_API_REQ", msg=message_body, int_info=api_number["PHY_PATH_REG_AN"] | ||
| ) | ||
| message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["PHY_PATH_REG_AN"]) | ||
@@ -962,7 +911,3 @@ with self.sess.pool.get_connection() as conn: | ||
| def modDataObjMeta(self, data_obj_info, meta_dict, **options): | ||
| if ( | ||
| "rescHier" not in data_obj_info | ||
| and "rescName" not in data_obj_info | ||
| and "replNum" not in data_obj_info | ||
| ): | ||
| if "rescHier" not in data_obj_info and "rescName" not in data_obj_info and "replNum" not in data_obj_info: | ||
| meta_dict["all"] = "" | ||
@@ -1011,3 +956,3 @@ | ||
| if 'dataAccessTime' in DataObjInfo_class.__dict__: | ||
| fields["dataAccessTime"]="" | ||
| fields["dataAccessTime"] = "" | ||
@@ -1014,0 +959,0 @@ message_body = ModDataObjMeta_for_session(self.sess)( |
@@ -30,9 +30,17 @@ import logging | ||
| # This was necessarily made separate from the MetadataManager definition | ||
| # in order to avoid infinite recursion in iRODSMetaCollection.__getattr__ | ||
| _MetadataManager_opts_initializer = {'admin': False, 'timestamps': False, 'iRODSMeta_type': iRODSMeta, 'reload': True} | ||
| class MetadataManager(Manager): | ||
| def __init__(self, *_): | ||
| self._opts = _MetadataManager_opts_initializer.copy() | ||
| super().__init__(*_) | ||
| @property | ||
| def use_timestamps(self): | ||
| return getattr(self, "_use_ts", False) | ||
| return self._opts['timestamps'] | ||
| __kw : Dict[str, Any] = {} # default (empty) keywords | ||
| __kw: Dict[str, Any] = {} # default (empty) keywords | ||
@@ -44,8 +52,19 @@ def _updated_keywords(self, opts): | ||
| def __call__(self, admin=False, timestamps=False, **irods_kw_opt): | ||
| if admin: | ||
| irods_kw_opt.update([(kw.ADMIN_KW, "")]) | ||
| def get_api_keywords(self): | ||
| return self.__kw.copy() | ||
| def __call__(self, **flags): | ||
| # Make a new shallow copy of the manager object, but update options from parameter list. | ||
| new_self = copy.copy(self) | ||
| new_self._use_ts = timestamps | ||
| new_self.__kw = irods_kw_opt | ||
| new_self._opts = copy.copy(self._opts) | ||
| # Update the flags that do bookkeeping in the returned(new) manager object. | ||
| new_self._opts.update((key, val) for key, val in flags.items() if val is not None) | ||
| # Update the ADMIN_KW flag in the returned(new) object. | ||
| if new_self._opts.get('admin'): | ||
| self.__kw[kw.ADMIN_KW] = "" | ||
| else: | ||
| self.__kw.pop(kw.ADMIN_KW, None) | ||
| return new_self | ||
@@ -72,2 +91,5 @@ | ||
| def get(self, model_cls, path): | ||
| if not path: | ||
| # Short circuit. This should be of the same type as the object returned at the function's end. | ||
| return [] | ||
| resource_type = self._model_class_to_resource_type(model_cls) | ||
@@ -102,3 +124,3 @@ model = { | ||
| return [ | ||
| iRODSMeta( | ||
| self._opts['iRODSMeta_type'](None, None, None)._from_column_triple( | ||
| row[model.name], row[model.value], row[model.units], **meta_opts(row) | ||
@@ -113,13 +135,5 @@ ) | ||
| message_body = MetadataRequest( | ||
| "add", | ||
| "-" + resource_type, | ||
| path, | ||
| meta.name, | ||
| meta.value, | ||
| meta.units, | ||
| **self._updated_keywords(opts) | ||
| "add", "-" + resource_type, path, *meta._to_column_triple(), **self._updated_keywords(opts) | ||
| ) | ||
| request = iRODSMessage( | ||
| "RODS_API_REQ", msg=message_body, int_info=api_number["MOD_AVU_METADATA_AN"] | ||
| ) | ||
| request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["MOD_AVU_METADATA_AN"]) | ||
| with self.sess.pool.get_connection() as conn: | ||
@@ -133,13 +147,5 @@ conn.send(request) | ||
| message_body = MetadataRequest( | ||
| "rm", | ||
| "-" + resource_type, | ||
| path, | ||
| meta.name, | ||
| meta.value, | ||
| meta.units, | ||
| **self._updated_keywords(opts) | ||
| "rm", "-" + resource_type, path, *meta._to_column_triple(), **self._updated_keywords(opts) | ||
| ) | ||
| request = iRODSMessage( | ||
| "RODS_API_REQ", msg=message_body, int_info=api_number["MOD_AVU_METADATA_AN"] | ||
| ) | ||
| request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["MOD_AVU_METADATA_AN"]) | ||
| with self.sess.pool.get_connection() as conn: | ||
@@ -154,12 +160,5 @@ conn.send(request) | ||
| message_body = MetadataRequest( | ||
| "cp", | ||
| "-" + src_resource_type, | ||
| "-" + dest_resource_type, | ||
| src, | ||
| dest, | ||
| **self._updated_keywords(opts) | ||
| "cp", "-" + src_resource_type, "-" + dest_resource_type, src, dest, **self._updated_keywords(opts) | ||
| ) | ||
| request = iRODSMessage( | ||
| "RODS_API_REQ", msg=message_body, int_info=api_number["MOD_AVU_METADATA_AN"] | ||
| ) | ||
| request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["MOD_AVU_METADATA_AN"]) | ||
@@ -174,13 +173,5 @@ with self.sess.pool.get_connection() as conn: | ||
| message_body = MetadataRequest( | ||
| "set", | ||
| "-" + resource_type, | ||
| path, | ||
| meta.name, | ||
| meta.value, | ||
| meta.units, | ||
| **self._updated_keywords(opts) | ||
| "set", "-" + resource_type, path, *meta._to_column_triple(), **self._updated_keywords(opts) | ||
| ) | ||
| request = iRODSMessage( | ||
| "RODS_API_REQ", msg=message_body, int_info=api_number["MOD_AVU_METADATA_AN"] | ||
| ) | ||
| request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["MOD_AVU_METADATA_AN"]) | ||
| with self.sess.pool.get_connection() as conn: | ||
@@ -204,5 +195,3 @@ conn.send(request) | ||
| if not all(isinstance(op, AVUOperation) for op in avu_ops): | ||
| raise InvalidAtomicAVURequest( | ||
| "avu_ops must contain 1 or more AVUOperations" | ||
| ) | ||
| raise InvalidAtomicAVURequest("avu_ops must contain 1 or more AVUOperations") | ||
| request = { | ||
@@ -209,0 +198,0 @@ "admin_mode": True if kw.ADMIN_KW in self.__kw.keys() else False, |
@@ -14,9 +14,6 @@ from irods.models import Resource | ||
| class ResourceManager(Manager): | ||
| @staticmethod | ||
| def serialize(context): | ||
| if isinstance(context, dict): | ||
| return ";".join( | ||
| "{}={}".format(key, value) for (key, value) in list(context.items()) | ||
| ) | ||
| return ";".join("{}={}".format(key, value) for (key, value) in list(context.items())) | ||
| return context | ||
@@ -92,5 +89,3 @@ | ||
| message_body = GeneralAdminRequest("rm", "resource", name, mode) | ||
| request = iRODSMessage( | ||
| "RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"] | ||
| ) | ||
| request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"]) | ||
| with self.sess.pool.get_connection() as conn: | ||
@@ -106,5 +101,3 @@ conn.send(request) | ||
| with self.sess.pool.get_connection() as conn: | ||
| message_body = GeneralAdminRequest( | ||
| "modify", "resource", name, attribute, self.serialize(value) | ||
| ) | ||
| message_body = GeneralAdminRequest("modify", "resource", name, attribute, self.serialize(value)) | ||
@@ -130,5 +123,3 @@ request = iRODSMessage( | ||
| message_body = GeneralAdminRequest( | ||
| "add", "childtoresc", parent, child, context | ||
| ) | ||
| message_body = GeneralAdminRequest("add", "childtoresc", parent, child, context) | ||
@@ -135,0 +126,0 @@ request = iRODSMessage( |
@@ -30,3 +30,2 @@ import logging | ||
| class UserManager(Manager): | ||
| def _get_session(self): | ||
@@ -40,12 +39,39 @@ return self.sess | ||
| def set_quota(self, user_name, amount, resource="total"): | ||
| return _do_GeneralAdminRequest( | ||
| self._get_session, "set-quota", "user", user_name, resource, str(amount) | ||
| ) | ||
| return _do_GeneralAdminRequest(self._get_session, "set-quota", "user", user_name, resource, str(amount)) | ||
| def remove_quota(self, user_name, resource="total"): | ||
| return _do_GeneralAdminRequest( | ||
| self._get_session, "set-quota", "user", user_name, resource, "0" | ||
| ) | ||
| return _do_GeneralAdminRequest(self._get_session, "set-quota", "user", user_name, resource, "0") | ||
| @staticmethod | ||
| def _parse_user_and_zone(user_param, zone_param): | ||
| """ | ||
| Parse out user and zone components from the arguments given. | ||
| Args: | ||
| user_param: either a simple user name, or a combination of both | ||
| user and zone names joined with "#". | ||
| zone_param: a simple zone name, | ||
| Returns: | ||
| The resulting parsed user and zone. | ||
| Raises: | ||
| RuntimeError: in the case of formatting errors or conflicting zone names. | ||
| """ | ||
| if '#' in user_param: | ||
| u_parsed_user, u_parsed_zone = user_param.split('#', 1) | ||
| if not u_parsed_zone: | ||
| raise RuntimeError("The compound user#zone specification may not contain a zero-length zone") | ||
| if '#' in u_parsed_zone: | ||
| raise RuntimeError(f"{u_parsed_zone = } is wrongly formatted") | ||
| if zone_param and (u_parsed_zone != zone_param): | ||
| raise RuntimeError( | ||
| f"Two nonzero-length zone names ({u_parsed_zone}, {zone_param}) were given, but they do not agree." | ||
| ) | ||
| return u_parsed_user, u_parsed_zone | ||
| return user_param, zone_param | ||
| def get(self, user_name, user_zone=""): | ||
| user_name, user_zone = self._parse_user_and_zone(user_name, user_zone) | ||
| if not user_zone: | ||
@@ -62,11 +88,11 @@ user_zone = self.sess.zone | ||
| def create_remote(self, user_name:str, user_zone:str): | ||
| def create_remote(self, user_name: str, user_zone: str): | ||
| """ | ||
| Create an entry in the local catalog for a remote user. The user_type will be 'rodsuser'. | ||
| """ | ||
| if user_zone in (self.sess.zone,""): | ||
| if user_zone in (self.sess.zone, ""): | ||
| raise ValueError(f"Parameter [{user_zone = }] must be a remote zone.") | ||
| return self.create_with_password(user_name, password='', user_zone=user_zone) | ||
| def create_with_password(self, user_name:str, password:str, user_zone:str=""): | ||
| def create_with_password(self, user_name: str, password: str, user_zone: str = ""): | ||
| """This method can be used by a groupadmin to initialize the password field while creating the new user. | ||
@@ -85,13 +111,5 @@ (This is necessary since group administrators may not change the password of an existing user.) | ||
| user_name + ("" if not user_zone else f"#{user_zone}"), | ||
| ( | ||
| "" | ||
| if not password | ||
| else obf.obfuscate_new_password_with_key( | ||
| password, self.sess.pool.account.password | ||
| ) | ||
| ), | ||
| ("" if not password else obf.obfuscate_new_password_with_key(password, self.sess.pool.account.password)), | ||
| ) | ||
| request = iRODSMessage( | ||
| "RODS_API_REQ", msg=message_body, int_info=api_number["USER_ADMIN_AN"] | ||
| ) | ||
| request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["USER_ADMIN_AN"]) | ||
| with self.sess.pool.get_connection() as conn: | ||
@@ -107,7 +125,3 @@ conn.send(request) | ||
| "user", | ||
| ( | ||
| user_name | ||
| if not user_zone or user_zone == self.sess.zone | ||
| else f"{user_name}#{user_zone}" | ||
| ), | ||
| (user_name if not user_zone or user_zone == self.sess.zone else f"{user_name}#{user_zone}"), | ||
| user_type, | ||
@@ -117,5 +131,3 @@ user_zone, | ||
| ) | ||
| request = iRODSMessage( | ||
| "RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"] | ||
| ) | ||
| request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"]) | ||
| with self.sess.pool.get_connection() as conn: | ||
@@ -130,15 +142,14 @@ conn.send(request) | ||
| _object = self.get(user_name, user_zone) | ||
| if _object.type == "rodsgroup": # noqa: SIM108 | ||
| uz_args = (f"{_object.name}",) | ||
| else: | ||
| uz_args = (f"{_object.name}#{_object.zone}",) | ||
| message_body = GeneralAdminRequest( | ||
| "rm", | ||
| ( | ||
| "user" | ||
| if (_object.type != "rodsgroup" or self.sess.server_version < (4, 3, 2)) | ||
| else "group" | ||
| ), | ||
| user_name, | ||
| user_zone, | ||
| ("user" if (_object.type != "rodsgroup" or self.sess.server_version < (4, 3, 2)) else "group"), | ||
| *uz_args, | ||
| ) | ||
| request = iRODSMessage( | ||
| "RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"] | ||
| ) | ||
| request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"]) | ||
| with self.sess.pool.get_connection() as conn: | ||
@@ -151,5 +162,3 @@ conn.send(request) | ||
| with self.sess.pool.get_connection() as conn: | ||
| message_body = GetTempPasswordForOtherRequest( | ||
| targetUser=user_name, unused=None | ||
| ) | ||
| message_body = GetTempPasswordForOtherRequest(targetUser=user_name, unused=None) | ||
| request = iRODSMessage( | ||
@@ -191,5 +200,3 @@ "RODS_API_REQ", | ||
| def modify_password( | ||
| self, old_value, new_value, modify_irods_authentication_file=False | ||
| ): | ||
| def modify_password(self, old_value, new_value, modify_irods_authentication_file=False): | ||
| """ | ||
@@ -205,3 +212,2 @@ Change the password for the current user (in the manner of `ipasswd'). | ||
| with self.sess.pool.get_connection() as conn: | ||
| if ( | ||
@@ -214,12 +220,6 @@ old_value != self.sess.pool.account.password | ||
| hash_new_value = obf.obfuscate_new_password( | ||
| new_value, old_value, conn.client_signature | ||
| ) | ||
| hash_new_value = obf.obfuscate_new_password(new_value, old_value, conn.client_signature) | ||
| message_body = UserAdminRequest( | ||
| "userpw", self.sess.username, "password", hash_new_value | ||
| ) | ||
| request = iRODSMessage( | ||
| "RODS_API_REQ", msg=message_body, int_info=api_number["USER_ADMIN_AN"] | ||
| ) | ||
| message_body = UserAdminRequest("userpw", self.sess.username, "password", hash_new_value) | ||
| request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["USER_ADMIN_AN"]) | ||
@@ -260,9 +260,6 @@ conn.send(request) | ||
| with self.sess.pool.get_connection() as conn: | ||
| # if modifying password, new value needs obfuscating | ||
| if option == "password": | ||
| current_password = self.sess.pool.account.password | ||
| new_value = obf.obfuscate_new_password( | ||
| new_value, current_password, conn.client_signature | ||
| ) | ||
| new_value = obf.obfuscate_new_password(new_value, current_password, conn.client_signature) | ||
@@ -288,4 +285,5 @@ message_body = GeneralAdminRequest( | ||
| CREATE_GROUP__USER_TYPE__DEFAULT__API_CHANGE__VERSION_THRESHOLD = (4,3,4) | ||
| CREATE_GROUP__USER_TYPE__DEFAULT__API_CHANGE__VERSION_THRESHOLD = (4, 3, 4) | ||
| def get__group_create__user_type__default(session): | ||
@@ -296,4 +294,4 @@ if session.server_version < CREATE_GROUP__USER_TYPE__DEFAULT__API_CHANGE__VERSION_THRESHOLD: | ||
| class GroupManager(UserManager): | ||
| def get(self, name, user_zone=""): | ||
@@ -318,6 +316,3 @@ query = self.sess.query(Group).filter(Group.name == name) | ||
| sess = self.sess | ||
| if group_admin_flag or ( | ||
| group_admin_flag is not False | ||
| and sess.users.get(sess.username).type == "groupadmin" | ||
| ): | ||
| if group_admin_flag or (group_admin_flag is not False and sess.users.get(sess.username).type == "groupadmin"): | ||
| return (UserAdminRequest, "USER_ADMIN_AN") | ||
@@ -337,9 +332,9 @@ return (GeneralAdminRequest, "GENERAL_ADMIN_AN") | ||
| Input parameters: | ||
| ----------------- | ||
| name: This is the name to be given to the new group. | ||
| user_type: (deprecated parameter) This parameter should remain unused and will effectively resolve as "rodsgroup". | ||
| user_zone: (deprecated parameter) In iRODS 4.3+, do not use this parameter as groups may not be made for a remote zone. | ||
| auth_type: (deprecated parameter) This parameter should be left to default to "" as groups do not need authentication. | ||
| group_admin: If left to its default value of None, seamlessly allows a groupadmin to create new groups. | ||
| Input parameters: | ||
| ----------------- | ||
| name: This is the name to be given to the new group. | ||
| user_type: (deprecated parameter) This parameter should remain unused and will effectively resolve as "rodsgroup". | ||
| user_zone: (deprecated parameter) In iRODS 4.3+, do not use this parameter as groups may not be made for a remote zone. | ||
| auth_type: (deprecated parameter) This parameter should be left to default to "" as groups do not need authentication. | ||
| group_admin: If left to its default value of None, seamlessly allows a groupadmin to create new groups. | ||
| """ | ||
@@ -365,7 +360,12 @@ | ||
| "add", | ||
| ("user" if self.sess.server_version < CREATE_GROUP__USER_TYPE__DEFAULT__API_CHANGE__VERSION_THRESHOLD else "group"), | ||
| ( | ||
| "user" | ||
| if self.sess.server_version < CREATE_GROUP__USER_TYPE__DEFAULT__API_CHANGE__VERSION_THRESHOLD | ||
| else "group" | ||
| ), | ||
| name, | ||
| user_type, | ||
| "", | ||
| "") | ||
| "", | ||
| ) | ||
| request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_to_use) | ||
@@ -379,18 +379,10 @@ with self.sess.pool.get_connection() as conn: | ||
| def getmembers(self, name): | ||
| results = self.sess.query(User).filter( | ||
| User.type != "rodsgroup", Group.name == name | ||
| ) | ||
| results = self.sess.query(User).filter(User.type != "rodsgroup", Group.name == name) | ||
| return [iRODSUser(self, row) for row in results] | ||
| def addmember( | ||
| self, group_name, user_name, user_zone="", group_admin=None, **options | ||
| ): | ||
| def addmember(self, group_name, user_name, user_zone="", group_admin=None, **options): | ||
| (MessageClass, api_key) = self._api_info(group_admin) | ||
| message_body = MessageClass( | ||
| "modify", "group", group_name, "add", user_name, user_zone | ||
| ) | ||
| request = iRODSMessage( | ||
| "RODS_API_REQ", msg=message_body, int_info=api_number[api_key] | ||
| ) | ||
| message_body = MessageClass("modify", "group", group_name, "add", user_name, user_zone) | ||
| request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number[api_key]) | ||
| with self.sess.pool.get_connection() as conn: | ||
@@ -401,13 +393,7 @@ conn.send(request) | ||
| def removemember( | ||
| self, group_name, user_name, user_zone="", group_admin=None, **options | ||
| ): | ||
| def removemember(self, group_name, user_name, user_zone="", group_admin=None, **options): | ||
| (MessageClass, api_key) = self._api_info(group_admin) | ||
| message_body = MessageClass( | ||
| "modify", "group", group_name, "remove", user_name, user_zone | ||
| ) | ||
| request = iRODSMessage( | ||
| "RODS_API_REQ", msg=message_body, int_info=api_number[api_key] | ||
| ) | ||
| message_body = MessageClass("modify", "group", group_name, "remove", user_name, user_zone) | ||
| request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number[api_key]) | ||
| with self.sess.pool.get_connection() as conn: | ||
@@ -422,8 +408,4 @@ conn.send(request) | ||
| def set_quota(self, group_name, amount, resource="total"): | ||
| message_body = GeneralAdminRequest( | ||
| "set-quota", "group", group_name, resource, str(amount) | ||
| ) | ||
| request = iRODSMessage( | ||
| "RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"] | ||
| ) | ||
| message_body = GeneralAdminRequest("set-quota", "group", group_name, resource, str(amount)) | ||
| request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"]) | ||
| with self.sess.pool.get_connection() as conn: | ||
@@ -430,0 +412,0 @@ conn.send(request) |
@@ -14,3 +14,2 @@ import logging | ||
| class ZoneManager(Manager): | ||
| def get(self, zone_name): | ||
@@ -32,5 +31,3 @@ query = self.sess.query(Zone).filter(Zone.name == zone_name) | ||
| ) | ||
| request = iRODSMessage( | ||
| "RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"] | ||
| ) | ||
| request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"]) | ||
| with self.sess.pool.get_connection() as conn: | ||
@@ -44,5 +41,3 @@ conn.send(request) | ||
| message_body = GeneralAdminRequest("rm", "zone", zone_name) | ||
| request = iRODSMessage( | ||
| "RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"] | ||
| ) | ||
| request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"]) | ||
| with self.sess.pool.get_connection() as conn: | ||
@@ -49,0 +44,0 @@ conn.send(request) |
| """Define objects related to communication with iRODS server API endpoints.""" | ||
| import struct | ||
| import ast | ||
| import json | ||
| import logging | ||
| import os | ||
| import socket | ||
| import json | ||
| import irods.exception as ex | ||
| import struct | ||
| import threading | ||
| import xml.etree.ElementTree as ET_xml | ||
| from collections import namedtuple | ||
| from typing import Optional | ||
| import xml.etree.ElementTree as ET_xml | ||
| from warnings import warn | ||
| import defusedxml.ElementTree as ET_secure_xml | ||
| import irods.exception as ex | ||
| from . import quasixml as ET_quasi_xml | ||
| from ..api_number import api_number | ||
| from collections import namedtuple | ||
| import os | ||
| import ast | ||
| import threading | ||
| from .message import Message | ||
| from .property_types import ( | ||
| ArrayProperty, | ||
| BinaryProperty, | ||
| StringProperty, | ||
| IntegerProperty, | ||
| LongProperty, | ||
| ArrayProperty, | ||
| StringProperty, | ||
| SubmessageProperty, | ||
| ) | ||
| from ..api_number import api_number | ||
@@ -55,5 +59,3 @@ | ||
| # choices -- to their corresponding names as strings (e.g. XML_Parser_Type.STANDARD_XML is mapped to 'STANDARD_XML'): | ||
| PARSER_TYPE_STRINGS = { | ||
| v: k for k, v in XML_Parser_Type.__members__.items() if v.value != 0 | ||
| } | ||
| PARSER_TYPE_STRINGS = {v: k for k, v in XML_Parser_Type.__members__.items() if v.value != 0} | ||
@@ -80,8 +82,4 @@ # We maintain values on a per-thread basis of: | ||
| _Quasi_Xml_Server_Version = _qxml_server_version( | ||
| "PYTHON_IRODSCLIENT_QUASI_XML_SERVER_VERSION" | ||
| ) | ||
| if ( | ||
| _Quasi_Xml_Server_Version is None | ||
| ): # unspecified in environment yields empty tuple () | ||
| _Quasi_Xml_Server_Version = _qxml_server_version("PYTHON_IRODSCLIENT_QUASI_XML_SERVER_VERSION") | ||
| if _Quasi_Xml_Server_Version is None: # unspecified in environment yields empty tuple () | ||
| raise BadXMLSpec("Must properly specify a server version to use QUASI_XML") | ||
@@ -91,5 +89,3 @@ | ||
| _default_XML_env = os.environ.get( | ||
| "PYTHON_IRODSCLIENT_DEFAULT_XML", globals().get("_default_XML") | ||
| ) | ||
| _default_XML_env = os.environ.get("PYTHON_IRODSCLIENT_DEFAULT_XML", globals().get("_default_XML")) | ||
@@ -172,5 +168,3 @@ if not _default_XML_env: | ||
| _thrlocal.xml_type = ( | ||
| default_XML_parser() | ||
| if xml_type in (None, XML_Parser_Type(0)) | ||
| else XML_Parser_Type(xml_type) | ||
| default_XML_parser() if xml_type in (None, XML_Parser_Type(0)) else XML_Parser_Type(xml_type) | ||
| ) | ||
@@ -188,4 +182,18 @@ if isinstance(server_version, _TUPLE_LIKE_TYPES): | ||
| IRODS_VERSION = (5, 0, 1, "d") | ||
| # The symbol _IRODS_VERSION is for internal use in testing only. It indicates the current | ||
| # server version for which PRC has maintained compatibility. Attempting the unit tests with | ||
| # more recent servers will fail by design. | ||
| _IRODS_VERSION = (5, 0, 2, "d") | ||
| # This is the older, now deprecated, version of the above symbol. | ||
| _deprecated_names = {"IRODS_VERSION": _IRODS_VERSION} | ||
| def __getattr__(name): | ||
| if name in _deprecated_names: | ||
| warn(f"{name} is deprecated", DeprecationWarning, stacklevel=2) | ||
| return _deprecated_names[name] | ||
| raise AttributeError(f"module {__name__!r} has no attribute {name!r}") | ||
| UNICODE = str | ||
@@ -247,5 +255,3 @@ | ||
| retbuf_size = len(retbuf) if retbuf is not None else 0 | ||
| msg = "Read {} bytes from socket instead of expected {} bytes".format( | ||
| retbuf_size, size | ||
| ) | ||
| msg = "Read {} bytes from socket instead of expected {} bytes".format(retbuf_size, size) | ||
| raise socket.error(msg) | ||
@@ -293,3 +299,2 @@ | ||
| class iRODSMessage: | ||
| class ResponseNotParseable(Exception): | ||
@@ -432,5 +437,3 @@ """ | ||
| # pack header | ||
| packed_header = self.pack_header( | ||
| self.msg_type, len(main_msg), len(self.error), len(self.bs), self.int_info | ||
| ) | ||
| packed_header = self.pack_header(self.msg_type, len(main_msg), len(self.error), len(self.bs), self.int_info) | ||
@@ -441,5 +444,3 @@ return packed_header + main_msg + self.error + self.bs | ||
| msg = cls() | ||
| logger.debug( | ||
| "Attempt to parse server response [%r] as class [%r].", self.msg, cls | ||
| ) | ||
| logger.debug("Attempt to parse server response [%r] as class [%r].", self.msg, cls) | ||
| if self.error and isinstance(r_error, RErrorStack): | ||
@@ -452,7 +453,3 @@ r_error.fill(iRODSMessage(msg=self.error).get_main_message(Error)) | ||
| # through as usual for express reporting by instances of irods.connection.Connection . | ||
| message = ( | ||
| "Server response was {self.msg} while parsing as [{cls}]".format( | ||
| **locals() | ||
| ) | ||
| ) | ||
| message = "Server response was {self.msg} while parsing as [{cls}]".format(**locals()) | ||
| raise self.ResponseNotParseable(message) | ||
@@ -486,4 +483,4 @@ msg.unpack(ET().fromstring(self.msg)) | ||
| self.clientUser, self.clientRcatZone = client_user | ||
| self.relVersion = "rods{}.{}.{}".format(*IRODS_VERSION) | ||
| self.apiVersion = "{3}".format(*IRODS_VERSION) | ||
| self.relVersion = "rods{}.{}.{}".format(*_IRODS_VERSION) | ||
| self.apiVersion = "{3}".format(*_IRODS_VERSION) | ||
| self.option = application_name | ||
@@ -814,5 +811,3 @@ | ||
| elif i < 5 and not (arg): | ||
| error = Bad_AVU_Field( | ||
| "AVU %s (%r) is zero-length." % (field_name(i), arg) | ||
| ) | ||
| error = Bad_AVU_Field("AVU %s (%r) is zero-length." % (field_name(i), arg)) | ||
| if error is not None: | ||
@@ -881,5 +876,4 @@ raise error | ||
| class _admin_request_base(Message): | ||
| _name: Optional[str] = None | ||
| _name : Optional[str] = None | ||
| def __init__(self, *args): | ||
@@ -920,5 +914,3 @@ if self.__class__._name is None: | ||
| message_body = GeneralAdminRequest(*args) | ||
| request = iRODSMessage( | ||
| "RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"] | ||
| ) | ||
| request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"]) | ||
| with sess.pool.get_connection() as conn: | ||
@@ -1045,8 +1037,5 @@ conn.send(request) | ||
| if name == "type": | ||
| # unpack struct accordingly | ||
| message_class = globals()[unpacked_value] | ||
| self._values["inOutStruct"] = SubmessageProperty(message_class).unpack( | ||
| root.findall(unpacked_value) | ||
| ) | ||
| self._values["inOutStruct"] = SubmessageProperty(message_class).unpack(root.findall(unpacked_value)) | ||
@@ -1181,2 +1170,3 @@ | ||
| # -- A tuple-descended class which facilitates filling in a | ||
@@ -1200,10 +1190,3 @@ # quasi-RError stack from a JSON formatted list. | ||
| if isinstance(Err, (tuple, list)): | ||
| self[:] = [ | ||
| RError( | ||
| _Server_Status_Message( | ||
| msg=elem["message"], status=elem["error_code"] | ||
| ) | ||
| ) | ||
| for elem in Err | ||
| ] | ||
| self[:] = [RError(_Server_Status_Message(msg=elem["message"], status=elem["error_code"])) for elem in Err] | ||
| return | ||
@@ -1257,7 +1240,4 @@ | ||
| """Show both the message and iRODS error type (both integer and human-readable).""" | ||
| return ( | ||
| "{self.__class__.__name__}" | ||
| "<message = {self.message!r}, status = {self.status} {self.status_str}>".format( | ||
| **locals() | ||
| ) | ||
| return "{self.__class__.__name__}<message = {self.message!r}, status = {self.status} {self.status_str}>".format( | ||
| **locals() | ||
| ) | ||
@@ -1285,6 +1265,4 @@ | ||
| def empty_gen_query_out(cols): | ||
| sql_results = [ | ||
| GenQueryResponseColumn(attriInx=col.icat_id, value=[]) for col in cols | ||
| ] | ||
| sql_results = [GenQueryResponseColumn(attriInx=col.icat_id, value=[]) for col in cols] | ||
| gqo = GenQueryResponse(rowCnt=0, attriCnt=len(cols), SqlResult_PI=sql_results) | ||
| return gqo |
@@ -6,3 +6,2 @@ # http://askawizard.blogspot.com/2008/10/ordered-properties-python-saga-part-5.html | ||
| class MessageMetaclass(OrderedMetaclass): | ||
| def __init__(self, name, bases, attys): | ||
@@ -15,3 +14,2 @@ super(MessageMetaclass, self).__init__(name, bases, attys) | ||
| class Message(OrderedClass, metaclass=MessageMetaclass): | ||
| def __init__(self, *args, **kwargs): | ||
@@ -18,0 +16,0 @@ super(Message, self).__init__() |
@@ -7,4 +7,4 @@ # Ordered property classes stolen from Kris Kowal of Ask a Wizard | ||
| class OrderedProperty: | ||
| def __init__(self, *args, **kws): | ||
@@ -16,3 +16,2 @@ self._creation_counter = next_counter() | ||
| class OrderedMetaclass(type): | ||
| def __init__(self, name, bases, attys): | ||
@@ -26,4 +25,3 @@ super(OrderedMetaclass, self).__init__(name, bases, attys) | ||
| for name, value in base.__dict__.items() | ||
| if isinstance(value, OrderedProperty) | ||
| or isinstance(value, OrderedMetaclass) | ||
| if isinstance(value, OrderedProperty) or isinstance(value, OrderedMetaclass) | ||
| ), | ||
@@ -30,0 +28,0 @@ key=lambda property: property[1]._creation_counter, |
@@ -8,3 +8,2 @@ from base64 import b64encode, b64decode | ||
| class MessageProperty(OrderedProperty): | ||
| def __get__(self, obj, cls): | ||
@@ -38,3 +37,2 @@ return obj._values[self.name] | ||
| class IntegerProperty(MessageProperty): | ||
| def format(self, value): | ||
@@ -48,3 +46,2 @@ return str(value) | ||
| class LongProperty(MessageProperty): | ||
| def format(self, value): | ||
@@ -58,3 +55,2 @@ return str(value) | ||
| class BinaryProperty(MessageProperty): | ||
| def __init__(self, length=None): | ||
@@ -76,3 +72,2 @@ self.length = length | ||
| class StringProperty(MessageProperty): | ||
| def __init__(self, length=None): | ||
@@ -100,3 +95,2 @@ self.length = length | ||
| class ArrayProperty(MessageProperty): | ||
| def __init__(self, prop): | ||
@@ -115,3 +109,2 @@ self.prop = prop | ||
| class SubmessageProperty(MessageProperty): | ||
| def __init__(self, message_cls=None): | ||
@@ -118,0 +111,0 @@ self.message_cls = message_cls |
@@ -46,9 +46,5 @@ # A parser for the iRODS XML-like protocol. | ||
| if type(self.body) is list: | ||
| return "<{}>{}</{}>".format( | ||
| self.name, "".join(map(str, self.body)), self.name | ||
| ) | ||
| return "<{}>{}</{}>".format(self.name, "".join(map(str, self.body)), self.name) | ||
| else: | ||
| return "<{}>{}</{}>".format( | ||
| self.name, encode_entities(self.body), self.name | ||
| ) | ||
| return "<{}>{}</{}>".format(self.name, encode_entities(self.body), self.name) | ||
@@ -138,5 +134,3 @@ def __repr__(self): | ||
| if type(topen) is not TokenTagOpen: | ||
| raise QuasiXmlParseError( | ||
| "protocol error: data does not start with open tag" | ||
| ) | ||
| raise QuasiXmlParseError("protocol error: data does not start with open tag") | ||
@@ -156,10 +150,6 @@ children = [] | ||
| raise QuasiXmlParseError( | ||
| "protocol error: close tag <{}> does not match opening tag <{}>".format( | ||
| t.text, topen.text | ||
| ) | ||
| "protocol error: close tag <{}> does not match opening tag <{}>".format(t.text, topen.text) | ||
| ) | ||
| elif cdata is not None and len(children): | ||
| raise QuasiXmlParseError( | ||
| "protocol error: mixed cdata and child elements" | ||
| ) | ||
| raise QuasiXmlParseError("protocol error: mixed cdata and child elements") | ||
| return ( | ||
@@ -166,0 +156,0 @@ Element( |
+130
-24
@@ -0,12 +1,47 @@ | ||
| import base64 | ||
| import copy | ||
| class iRODSMeta: | ||
| def _to_column_triple(self): | ||
| return (self.name, self.forward_translate(self.value)) + ( | ||
| ('',) if not self.units else (self.forward_translate(self.units),) | ||
| ) | ||
| def _from_column_triple(self, name, value, units, **kw): | ||
| self.__low_level_init( | ||
| name, self.reverse_translate(value), units=None if not units else self.reverse_translate(units), **kw | ||
| ) | ||
| return self | ||
| reverse_translate = forward_translate = staticmethod(lambda _: _) | ||
| INIT_KW_ARGS = ['units', 'avu_id', 'create_time', 'modify_time'] | ||
| def __init__( | ||
| self, name, value, units=None, avu_id=None, create_time=None, modify_time=None | ||
| self, | ||
| name, | ||
| value, | ||
| /, | ||
| units=None, | ||
| *, | ||
| avu_id=None, | ||
| create_time=None, | ||
| modify_time=None, | ||
| ): | ||
| self.avu_id = avu_id | ||
| # Defer initialization for iRODSMeta(attribute,value,...) if neither attribute nor value is True under | ||
| # a 'bool' transformation. In so doing we streamline initialization for iRODSMeta (and any subclasses) | ||
| # for alternatively populating via _from_column_triple(...). | ||
| # This is the pathway for allowing user-defined encodings of the iRODSMeta (byte-)string AVU components. | ||
| if name or value: | ||
| # Note: calling locals() inside the dict comprehension would not access variables in this frame. | ||
| local_vars = locals() | ||
| kw = {name: local_vars.get(name) for name in self.INIT_KW_ARGS} | ||
| self.__low_level_init(name, value, **kw) | ||
| def __low_level_init(self, name, value, **kw): | ||
| self.name = name | ||
| self.value = value | ||
| self.units = units | ||
| self.create_time = create_time | ||
| self.modify_time = modify_time | ||
| for attr in self.INIT_KW_ARGS: | ||
| setattr(self, attr, kw.get(attr)) | ||
@@ -23,5 +58,20 @@ def __eq__(self, other): | ||
| def __repr__(self): | ||
| return "<iRODSMeta {avu_id} {name} {value} {units}>".format(**vars(self)) | ||
| return f"<{self.__class__.__name__} {self.avu_id} {self.name} {self.value} {self.units}>" | ||
| def __hash__(self): | ||
| return hash(tuple(self)) | ||
| class iRODSBinOrStringMeta(iRODSMeta): | ||
| @staticmethod | ||
| def reverse_translate(value): | ||
| """Translate an AVU field from its iRODS object-database form into the client representation of that field.""" | ||
| return value if value[0] != '\\' else base64.decodebytes(value[1:].encode('utf8')) | ||
| @staticmethod | ||
| def forward_translate(value): | ||
| """Translate an AVU field from the form it takes in the client, into an iRODS object-database compatible form.""" | ||
| return b'\\' + base64.encodebytes(value).strip() if isinstance(value, (bytes, bytearray)) else value | ||
| class BadAVUOperationKeyword(Exception): | ||
@@ -36,3 +86,2 @@ pass | ||
| class AVUOperation(dict): | ||
| @property | ||
@@ -58,5 +107,4 @@ def operation(self): | ||
| if not isinstance(avu_param, iRODSMeta): | ||
| error_msg = ( | ||
| "Nonconforming avu {!r} of type {}; must be an iRODSMeta." | ||
| "".format(avu_param, type(avu_param).__name__) | ||
| error_msg = "Nonconforming avu {!r} of type {}; must be an iRODSMeta.".format( | ||
| avu_param, type(avu_param).__name__ | ||
| ) | ||
@@ -67,7 +115,3 @@ raise BadAVUOperationValue(error_msg) | ||
| if operation not in ("add", "remove"): | ||
| error_msg = ( | ||
| "Nonconforming operation {!r}; must be 'add' or 'remove'.".format( | ||
| operation | ||
| ) | ||
| ) | ||
| error_msg = "Nonconforming operation {!r}; must be 'add' or 'remove'.".format(operation) | ||
| raise BadAVUOperationValue(error_msg) | ||
@@ -84,5 +128,3 @@ | ||
| if kw: | ||
| raise BadAVUOperationKeyword( | ||
| """Nonconforming keyword (s) {}.""".format(list(kw.keys())) | ||
| ) | ||
| raise BadAVUOperationKeyword("""Nonconforming keyword (s) {}.""".format(list(kw.keys()))) | ||
| for atr in ("operation", "avu"): | ||
@@ -92,10 +134,70 @@ setattr(self, atr, locals()[atr]) | ||
| import copy | ||
| class iRODSMetaCollection: | ||
| def __setattr__(self, name, value): | ||
| """ | ||
| Override __setattr__. | ||
| Protect the virtual, read-only attributes such as 'admin', 'timestamps', etc., | ||
| from being written or created as concrete attributes, which would interfere with | ||
| __getattr__'s intended operation for these cases. | ||
| class iRODSMetaCollection: | ||
| Args: | ||
| name: the name of the attribute to be written. | ||
| value: the value to be written to the attribute. | ||
| def __call__(self, admin=False, timestamps=False, **opts): | ||
| Raises: | ||
| AttributeError: on any attempt to write to these special attributes. | ||
| """ | ||
| from irods.manager.metadata_manager import _MetadataManager_opts_initializer | ||
| if name in _MetadataManager_opts_initializer: | ||
| msg = ( | ||
| f"""The "{name}" attribute is a special one, settable only via a """ | ||
| f"""call on the object. For example: admin_view = data_obj.metadata({name}=<value>)""" | ||
| ) | ||
| raise AttributeError(msg) | ||
| super().__setattr__(name, value) | ||
| def __getattr__(self, name): | ||
| """ | ||
| Override __getattr__. | ||
| Expose certain settable flags (e.g. "admin", "timestamps") as virtual, read-only | ||
| "attributes." The names of these special attributes appear as the keys of the | ||
| _MetadataManager_opts_initializer dictionary. | ||
| Args: | ||
| name: the name of the attribute to be fetched. | ||
| Returns: | ||
| the value of the named attribute. | ||
| Raises: | ||
| AttributeError: because this is the protocol for deferring to __getattr__'s | ||
| default behavior for the case in which none of the special attribute keys are | ||
| a match for 'name'. | ||
| """ | ||
| from irods.manager.metadata_manager import _MetadataManager_opts_initializer | ||
| # Separating _MetadataManager_opts_initializer from the MetadataManager class | ||
| # prevents the possibility of arbitrary access by copy.copy() to parts of | ||
| # our object's state before they have been initialized, as it is known to do | ||
| # by calling hasattr on the "__setstate__" attribute. The result of such | ||
| # unfettered access is infinite recursion. See: | ||
| # https://nedbatchelder.com/blog/201010/surprising_getattr_recursion | ||
| if name in _MetadataManager_opts_initializer: | ||
| return self._manager._opts[name] # noqa: SLF001 | ||
| raise AttributeError | ||
| def __call__(self, **opts): | ||
| """ | ||
| Optional parameters in **opts are: | ||
| admin (default: False): apply ADMIN_KW to future metadata operations. | ||
| timestamps (default: False): attach (ctime,mtime) timestamp attributes to AVUs received from iRODS. | ||
| """ | ||
| x = copy.copy(self) | ||
| x._manager = (x._manager)(admin, timestamps, **opts) | ||
| x._manager = (x._manager)(**opts) | ||
| x._reset_metadata() | ||
@@ -111,3 +213,7 @@ return x | ||
| def _reset_metadata(self): | ||
| self._meta = self._manager.get(self._model_cls, self._path) | ||
| m = self._manager | ||
| if not hasattr(self, "_meta"): | ||
| self._meta = m.get(None, "") | ||
| if m._opts.setdefault('reload', True): | ||
| self._meta = m.get(self._model_cls, self._path) | ||
@@ -139,3 +245,3 @@ def get_all(self, key): | ||
| raise ValueError("Must specify an iRODSMeta object or key, value, units)") | ||
| return args[0] if len(args) == 1 else iRODSMeta(*args) | ||
| return args[0] if len(args) == 1 else self._manager._opts['iRODSMeta_type'](*args) | ||
@@ -142,0 +248,0 @@ def apply_atomic_operations(self, *avu_ops): |
+4
-5
@@ -6,4 +6,4 @@ from typing import Dict, List, Tuple | ||
| class ModelBase(type): | ||
| column_items : List[Tuple[int, Column]] = [] | ||
| column_dict : Dict[int, Column] = {} | ||
| column_items: List[Tuple[int, Column]] = [] | ||
| column_dict: Dict[int, Column] = {} | ||
@@ -247,5 +247,4 @@ @classmethod | ||
| ## For now, use of these columns raises CAT_SQL_ERR in both PRC and iquest: (irods/irods#5929) | ||
| # create_time = Column(String, 'TICKET_CREATE_TIME', 2209) | ||
| # modify_time = Column(String, 'TICKET_MODIFY_TIME', 2210) | ||
| create_time = Column(DateTime, 'TICKET_CREATE_TIME', 2209, min_version=(4, 3, 0)) | ||
| modify_time = Column(DateTime, 'TICKET_MODIFY_TIME', 2210, min_version=(4, 3, 0)) | ||
@@ -252,0 +251,0 @@ class DataObject(Model): |
+199
-111
@@ -12,3 +12,4 @@ #!/usr/bin/env python | ||
| import multiprocessing | ||
| from typing import List, Union | ||
| from typing import List, Union, Any | ||
| import weakref | ||
@@ -20,3 +21,57 @@ from irods.data_object import iRODSDataObject | ||
| paths_active: weakref.WeakValueDictionary[str, "AsyncNotify"] = weakref.WeakValueDictionary() | ||
| transfer_managers: weakref.WeakKeyDictionary["_Multipart_close_manager", Any] = weakref.WeakKeyDictionary() | ||
| class FILTER_FUNCTIONS: | ||
| """The members of this class are free functions designed to be passed to | ||
| the "filter_function" parameter of the abort_parallel_transfers function. | ||
| """ | ||
| foreground = staticmethod(lambda item: isinstance(item[1], tuple)) | ||
| background = staticmethod(lambda item: not isinstance(item[1], tuple)) | ||
| def abort_parallel_transfers(dry_run=False, filter_function=None, transform=weakref.WeakKeyDictionary): | ||
| """ | ||
| If no explicit arguments are given, all ongoing parallel puts and gets are | ||
| cancelled as soon as possible. The corresponding threads are signalled to | ||
| exit by calling the quit() method on their corresponding transfer-manager | ||
| objects. | ||
| Setting dry_run=True results in no such cancellation being performed, | ||
| although a dict object will be computed for the return value containing, | ||
| as its keys, the transfer-manager objects that would have been so affected. | ||
| filter_function is usually left to its default value of None. By applying | ||
| a member of the FILTER_FUNCTIONS class, the caller may specify which | ||
| transfer-managers are affected and/or reflected in the return value: | ||
| FILTER_FUNCTIONS.foreground for example limits its scope to instances | ||
| of session.data_object.put() and session.data_object.get() that are | ||
| running synchronously. These calls block within the thread(s) that | ||
| are calling them. | ||
| FILTER_FUNCTIONS.background limits its scope to transfers started by | ||
| calls to io_main() which used Oper.NONBLOCKING to spawn the put or | ||
| get operation in the background. | ||
| transform defaults to a dictionary type with weak keys, since | ||
| allowing strong references to transfer-manager objects may artificially | ||
| increase lifetimes of threads and other objects unnecessarily and | ||
| complicate troubleshooting by altering library behavior. Consider using | ||
| transform=len if all that is desired is to check how many parallel | ||
| transfers exist in total, at the time. | ||
| """ | ||
| mgrs = dict(filter(filter_function, transfer_managers.items())) | ||
| if not dry_run: | ||
| for mgr, item in mgrs.items(): | ||
| if isinstance(item, tuple): | ||
| quit_func, args = item[:2] | ||
| quit_func(*args) | ||
| else: | ||
| mgr.quit() | ||
| return transform(mgrs) | ||
| logger = logging.getLogger(__name__) | ||
@@ -74,10 +129,6 @@ _nullh = logging.NullHandler() | ||
| if not callable(callback): | ||
| raise BadCallbackTarget( | ||
| '"callback" must be a callable accepting at least 1 argument' | ||
| ) | ||
| raise BadCallbackTarget('"callback" must be a callable accepting at least 1 argument') | ||
| self.done_callback = callback | ||
| def __init__( | ||
| self, futuresList, callback=None, progress_Queue=None, total=None, keep_=() | ||
| ): | ||
| def __init__(self, futuresList, callback=None, progress_Queue=None, total=None, keep_=()): | ||
| """AsyncNotify initialization (used internally to the io.parallel library). | ||
@@ -97,5 +148,7 @@ The casual user will only be concerned with the callback parameter, called when all threads | ||
| else: | ||
| self.__invoke_done_callback() | ||
| self.__invoke_futures_done_logic() | ||
| return | ||
| self.progress = [0, 0] | ||
| if (progress_Queue) and (total is not None): | ||
@@ -118,5 +171,3 @@ self.progress[1] = total | ||
| self._progress_fn = _progress | ||
| self._progress_thread = threading.Thread( | ||
| target=self._progress_fn, args=(progress_Queue, self) | ||
| ) | ||
| self._progress_thread = threading.Thread(target=self._progress_fn, args=(progress_Queue, self), daemon=True) | ||
| self._progress_thread.start() | ||
@@ -154,5 +205,3 @@ | ||
| def __call__( | ||
| self, future | ||
| ): # Our instance is called by each future (individual file part) when done. | ||
| def __call__(self, future): # Our instance is called by each future (individual file part) when done. | ||
| # When all futures are done, we invoke the configured callback. | ||
@@ -162,7 +211,8 @@ with self._lock: | ||
| if len(self._futures) == len(self._futures_done): | ||
| self.__invoke_done_callback() | ||
| # If a future returns None rather than an integer byte count, it has aborted the transfer. | ||
| self.__invoke_futures_done_logic(skip_user_callback=(None in self._futures_done.values())) | ||
| def __invoke_done_callback(self): | ||
| def __invoke_futures_done_logic(self, skip_user_callback=False): | ||
| try: | ||
| if callable(self.done_callback): | ||
| if not skip_user_callback and callable(self.done_callback): | ||
| self.done_callback(self) | ||
@@ -250,2 +300,8 @@ finally: | ||
| while True and bytecount < length: | ||
| if mgr._quit: | ||
| # Indicate by the return value that we are aborting (this part of) the data transfer. | ||
| # In the great majority of cases, this should be seen by the application as an overall | ||
| # abort of the PUT or GET of the requested object. | ||
| bytecount = None | ||
| break | ||
| buf = src.read(min(COPY_BUF_SIZE, length - bytecount)) | ||
@@ -285,3 +341,4 @@ buf_len = len(buf) | ||
| def __init__(self, initial_io_, exit_barrier_): | ||
| def __init__(self, initial_io_, exit_barrier_, executor=None): | ||
| self._quit = False | ||
| self.exit_barrier = exit_barrier_ | ||
@@ -291,3 +348,30 @@ self.initial_io = initial_io_ | ||
| self.aux = [] | ||
| self.futures = set() | ||
| self.executor = executor | ||
| def add_future(self, future): | ||
| self.futures.add(future) | ||
| @property | ||
| def active_futures(self): | ||
| return tuple(_ for _ in self.futures if not _.done()) | ||
| def shutdown(self): | ||
| if self.executor: | ||
| self.executor.shutdown(cancel_futures=True) | ||
| def quit(self): | ||
| from irods.session import _exclude_fds_from_auto_close | ||
| _exclude_fds_from_auto_close(self.aux + [self.initial_io]) | ||
| if not self._quit: | ||
| self._quit = True | ||
| # Disable barrier and abort threads. | ||
| self.exit_barrier.abort() | ||
| self.shutdown() | ||
| return self.active_futures | ||
| def __contains__(self, Io): | ||
@@ -317,4 +401,8 @@ with self.__lock: | ||
| is_initial = False | ||
| self.exit_barrier.wait() | ||
| if is_initial: | ||
| broken = False | ||
| try: | ||
| self.exit_barrier.wait() | ||
| except threading.BrokenBarrierError: | ||
| broken = True | ||
| if is_initial and not (broken or self._quit): | ||
| self.finalize() | ||
@@ -351,9 +439,5 @@ | ||
| return ( | ||
| _copy_part( | ||
| file_, objHandle, length, queueObject, thread_debug_id, mgr_, updatables | ||
| ) | ||
| _copy_part(file_, objHandle, length, queueObject, thread_debug_id, mgr_, updatables) | ||
| if Operation.isPut() | ||
| else _copy_part( | ||
| objHandle, file_, length, queueObject, thread_debug_id, mgr_, updatables | ||
| ) | ||
| else _copy_part(objHandle, file_, length, queueObject, thread_debug_id, mgr_, updatables) | ||
| ) | ||
@@ -363,11 +447,3 @@ | ||
| def _io_multipart_threaded( | ||
| operation_, | ||
| dataObj_and_IO, | ||
| replica_token, | ||
| hier_str, | ||
| session, | ||
| fname, | ||
| total_size, | ||
| num_threads, | ||
| **extra_options | ||
| operation_, dataObj_and_IO, replica_token, hier_str, session, fname, total_size, num_threads, **extra_options | ||
| ): | ||
@@ -391,10 +467,5 @@ """Called by _io_main. | ||
| ranges = [ | ||
| bytes_range_for_thread(i, num_threads, total_size, bytes_per_thread) | ||
| for i in range(num_threads) | ||
| ] | ||
| ranges = [bytes_range_for_thread(i, num_threads, total_size, bytes_per_thread) for i in range(num_threads)] | ||
| logger.info( | ||
| "num_threads = %s ; bytes_per_thread = %s", num_threads, bytes_per_thread | ||
| ) | ||
| logger.info("num_threads = %s ; bytes_per_thread = %s", num_threads, bytes_per_thread) | ||
@@ -410,7 +481,5 @@ queueLength = extra_options.get("queueLength", 0) | ||
| num_threads = min(num_threads, len(ranges)) | ||
| mgr = _Multipart_close_manager(Io, Barrier(num_threads)) | ||
| mgr = _Multipart_close_manager(Io, Barrier(num_threads), executor) | ||
| counter = 1 | ||
| gen_file_handle = lambda: open( | ||
| fname, Operation.disk_file_mode(initial_open=(counter == 1)) | ||
| ) | ||
| gen_file_handle = lambda: open(fname, Operation.disk_file_mode(initial_open=(counter == 1))) | ||
| File = gen_file_handle() | ||
@@ -423,46 +492,75 @@ | ||
| for byte_range in ranges: | ||
| if Io is None: | ||
| Io = session.data_objects.open( | ||
| Data_object.path, | ||
| Operation.data_object_mode(initial_open=False), | ||
| create=False, | ||
| finalize_on_close=False, | ||
| allow_redirect=False, | ||
| **{ | ||
| kw.NUM_THREADS_KW: str(num_threads), | ||
| kw.DATA_SIZE_KW: str(total_size), | ||
| kw.RESC_HIER_STR_KW: hier_str, | ||
| kw.REPLICA_TOKEN_KW: replica_token, | ||
| } | ||
| ) | ||
| mgr.add_io(Io) | ||
| logger.debug("target_host = %s", Io.raw.session.pool.account.host) | ||
| if File is None: | ||
| File = gen_file_handle() | ||
| futures.append( | ||
| executor.submit( | ||
| _io_part, | ||
| Io, | ||
| byte_range, | ||
| File, | ||
| Operation, | ||
| mgr, | ||
| thread_debug_id=str(counter), | ||
| **thread_opts | ||
| ) | ||
| ) | ||
| counter += 1 | ||
| Io = File = None | ||
| transfer_managers[mgr] = (_quit_current_transfer, [id(mgr)]) | ||
| if Operation.isNonBlocking(): | ||
| if queueLength: | ||
| return futures, queueObject, mgr | ||
| try: | ||
| thread_setup_error = None | ||
| for byte_range in ranges: | ||
| if Io is None: | ||
| Io = session.data_objects.open( | ||
| Data_object.path, | ||
| Operation.data_object_mode(initial_open=False), | ||
| create=False, | ||
| finalize_on_close=False, | ||
| allow_redirect=False, | ||
| **{ | ||
| kw.NUM_THREADS_KW: str(num_threads), | ||
| kw.DATA_SIZE_KW: str(total_size), | ||
| kw.RESC_HIER_STR_KW: hier_str, | ||
| kw.REPLICA_TOKEN_KW: replica_token, | ||
| }, | ||
| ) | ||
| mgr.add_io(Io) | ||
| logger.debug("target_host = %s", Io.raw.session.pool.account.host) | ||
| if File is None: | ||
| File = gen_file_handle() | ||
| try: | ||
| f = None | ||
| futures.append( | ||
| f := executor.submit( | ||
| _io_part, Io, byte_range, File, Operation, mgr, thread_debug_id=str(counter), **thread_opts | ||
| ) | ||
| ) | ||
| except RuntimeError as error: | ||
| # Executor was probably shut down before parallel transfer could be initiated. | ||
| thread_setup_error = error | ||
| break | ||
| else: | ||
| mgr.add_future(f) | ||
| counter += 1 | ||
| Io = File = None | ||
| if thread_setup_error: | ||
| raise thread_setup_error | ||
| bytes_transferred = 0 | ||
| if Operation.isNonBlocking(): | ||
| transfer_managers[mgr] = None | ||
| return (futures, mgr, queueObject) | ||
| else: | ||
| return futures | ||
| else: | ||
| bytecounts = [f.result() for f in futures] | ||
| return sum(bytecounts), total_size | ||
| # Enable user attempts to cancel the current synchronous transfer. | ||
| # At any given time, only one transfer manager key should map to a tuple object T. | ||
| # You should be able to quit all threads of the current transfer by calling T[0](*T[1]). | ||
| bytecounts = [future.result() for future in futures] | ||
| # If, rather than an integer byte-count, the "None" object was included as one of futures' return values, this | ||
| # is an indication that the PUT or GET operation should be marked as aborted, i.e. no bytes transferred. | ||
| if None not in bytecounts: | ||
| bytes_transferred = sum(bytecounts) | ||
| return (bytes_transferred, total_size) | ||
| except BaseException as e: | ||
| if isinstance(e, (SystemExit, KeyboardInterrupt, RuntimeError)): | ||
| mgr.quit() | ||
| raise | ||
| def _quit_current_transfer(obj_id): | ||
| l = [_ for _ in transfer_managers if id(_) == obj_id] | ||
| if l: | ||
| l[0].quit() | ||
| def io_main(session, Data, opr_, fname, R="", **kwopt): | ||
@@ -520,3 +618,3 @@ """ | ||
| returned_values=output_values, | ||
| **open_options | ||
| **open_options, | ||
| ) | ||
@@ -570,3 +668,3 @@ else: | ||
| num_threads=num_threads, | ||
| **{k: v for k, v in kwopt.items() if k in pass_thru_options} | ||
| **{k: v for k, v in kwopt.items() if k in pass_thru_options}, | ||
| ) | ||
@@ -579,15 +677,14 @@ | ||
| if Operation.isNonBlocking(): | ||
| (futures, mgr, chunk_notify_queue) = retval | ||
| if queueLength > 0: | ||
| (futures, chunk_notify_queue, mgr) = retval | ||
| else: | ||
| futures = retval | ||
| chunk_notify_queue = total_bytes = None | ||
| # For convenience, this information can help determine which data object mgr is tracking. | ||
| transfer_managers[mgr] = Data.path | ||
| return AsyncNotify( | ||
| paths_active[Data.path] = async_notify = AsyncNotify( | ||
| futures, # individual futures, one per transfer thread | ||
| progress_Queue=chunk_notify_queue, # for notifying the progress indicator thread | ||
| total=total_bytes, # total number of bytes for parallel transfer | ||
| keep_={"mgr": mgr}, | ||
| ) # an open raw i/o object needing to be persisted, if any | ||
| keep_={"mgr": mgr}, # objects needing to be persisted while futures are pending | ||
| ) | ||
| return async_notify | ||
| else: | ||
@@ -599,3 +696,2 @@ (_bytes_transferred, _bytes_total) = retval | ||
| if __name__ == "__main__": | ||
| import getopt | ||
@@ -620,5 +716,3 @@ import atexit | ||
| env_file = os.path.expanduser("~/.irods/irods_environment.json") | ||
| ssl_context = ssl.create_default_context( | ||
| purpose=ssl.Purpose.SERVER_AUTH, cafile=None, capath=None, cadata=None | ||
| ) | ||
| ssl_context = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH, cafile=None, capath=None, cadata=None) | ||
| ssl_settings = {"ssl_context": ssl_context} | ||
@@ -633,5 +727,3 @@ sess = iRODSSession(irods_env_file=env_file, **ssl_settings) | ||
| logFilename = opts.pop( | ||
| "-L", None | ||
| ) # '' for console, non-empty for filesystem destination | ||
| logFilename = opts.pop("-L", None) # '' for console, non-empty for filesystem destination | ||
| logLevel = logging.INFO if logFilename is None else logging.DEBUG | ||
@@ -663,13 +755,9 @@ logFilename = logFilename or opts.pop("-l", None) | ||
| print("waiting on completion...", file=sys.stderr) | ||
| ret.set_transfer_done_callback( | ||
| lambda r: print("Async transfer done for:", r, file=sys.stderr) | ||
| ) | ||
| done = ret.wait_until_transfer_done( | ||
| timeout=10.0 | ||
| ) # - or do other useful work here | ||
| ret.set_transfer_done_callback(lambda r: print("Async transfer done for:", r, file=sys.stderr)) | ||
| done = ret.wait_until_transfer_done(timeout=10.0) # - or do other useful work here | ||
| if done: | ||
| bytes_transferred = sum(ret.futures_done.values()) | ||
| bytes_transferred_total = sum(ret.futures_done.values()) | ||
| print( | ||
| "Asynch transfer complete. Total bytes transferred:", | ||
| bytes_transferred, | ||
| bytes_transferred_total, | ||
| file=sys.stderr, | ||
@@ -676,0 +764,0 @@ ) |
@@ -248,5 +248,3 @@ import hashlib | ||
| # insert the seq_index (which is NOT encoded): | ||
| encoded_string = chr(seq_index + ord("e")).join( | ||
| [encoded_string[:6], encoded_string[6:]] | ||
| ) | ||
| encoded_string = chr(seq_index + ord("e")).join([encoded_string[:6], encoded_string[6:]]) | ||
@@ -309,5 +307,3 @@ # aaaaand, append a null character. because we want to print | ||
| # the index of the target character in wheel | ||
| wheel_index = ( | ||
| wheel.index(c) - encoder_ring[encoder_ring_index % 61] - chain | ||
| ) % len(wheel) | ||
| wheel_index = (wheel.index(c) - encoder_ring[encoder_ring_index % 61] - chain) % len(wheel) | ||
| unscrambled_string += wheel[wheel_index] | ||
@@ -346,5 +342,3 @@ if block_chaining: | ||
| # the index of the target character in wheel | ||
| wheel_index = ( | ||
| wheel.index(c) + encoder_ring[encoder_ring_index % 61] + chain | ||
| ) % len(wheel) | ||
| wheel_index = (wheel.index(c) + encoder_ring[encoder_ring_index % 61] + chain) % len(wheel) | ||
| scrambled_string += wheel[wheel_index] | ||
@@ -363,5 +357,3 @@ if block_chaining: | ||
| to_scramble = ( | ||
| random.SystemRandom().choice(string.printable) + v2_prefix[1:10] + s[:150] | ||
| ) | ||
| to_scramble = random.SystemRandom().choice(string.printable) + v2_prefix[1:10] + s[:150] | ||
@@ -382,5 +374,3 @@ key = first_key[:90] + second_key[:100] | ||
| return scramble( | ||
| to_scramble, key=hashed_key, scramble_prefix="", block_chaining=True | ||
| ) | ||
| return scramble(to_scramble, key=hashed_key, scramble_prefix="", block_chaining=True) | ||
@@ -387,0 +377,0 @@ |
@@ -10,2 +10,5 @@ """A module providing tools for path normalization and manipulation.""" | ||
| _logger = logging.getLogger(__name__) | ||
| class iRODSPath(str): | ||
@@ -53,3 +56,3 @@ """A subclass of the Python string that normalizes iRODS logical paths.""" | ||
| if kw: | ||
| logging.warning("These iRODSPath options have no effect: %r", kw.items()) | ||
| _logger.warning("These iRODSPath options have no effect: %r", kw.items()) | ||
| normalized = _normalize_iRODS_logical_path(elem_list, absolute_) | ||
@@ -56,0 +59,0 @@ obj = str.__new__(cls, normalized) |
+4
-14
@@ -39,6 +39,3 @@ import contextlib | ||
| class Pool: | ||
| def __init__( | ||
| self, account, application_name="", connection_refresh_time=-1, session=None | ||
| ): | ||
| def __init__(self, account, application_name="", connection_refresh_time=-1, session=None): | ||
| """ | ||
@@ -57,7 +54,3 @@ Pool( account , application_name='' ) | ||
| self.connection_timeout = DEFAULT_CONNECTION_TIMEOUT | ||
| self.application_name = ( | ||
| os.environ.get("spOption", "") | ||
| or application_name | ||
| or DEFAULT_APPLICATION_NAME | ||
| ) | ||
| self.application_name = os.environ.get("spOption", "") or application_name or DEFAULT_APPLICATION_NAME | ||
| self._need_auth = True | ||
@@ -103,4 +96,3 @@ | ||
| self.refresh_connection | ||
| and (curr_time - conn.create_time).total_seconds() | ||
| > self.connection_refresh_time | ||
| and (curr_time - conn.create_time).total_seconds() > self.connection_refresh_time | ||
| ): | ||
@@ -122,5 +114,3 @@ logger.debug( | ||
| new_conn = True | ||
| logger.debug( | ||
| f"No connection found in idle set. Created a new connection with id: {id(conn)}" | ||
| ) | ||
| logger.debug(f"No connection found in idle set. Created a new connection with id: {id(conn)}") | ||
@@ -127,0 +117,0 @@ self.active.add(conn) |
@@ -25,3 +25,3 @@ #!/usr/bin/env python3 | ||
| vector : Dict[str, Callable] = {"pam_password": write_pam_irodsA_file, "native": write_native_irodsA_file} | ||
| vector: Dict[str, Callable] = {"pam_password": write_pam_irodsA_file, "native": write_native_irodsA_file} | ||
| opts, args = getopt.getopt(sys.argv[1:], "hi:", ["ttl=", "help"]) | ||
@@ -31,7 +31,3 @@ optD = dict(opts) | ||
| if len(args) != 1 or help_selected: | ||
| print( | ||
| "{}\nUsage: {} [-i STREAM| -h | --help | --ttl HOURS] AUTH_SCHEME".format( | ||
| extra_help, sys.argv[0] | ||
| ) | ||
| ) | ||
| print("{}\nUsage: {} [-i STREAM| -h | --help | --ttl HOURS] AUTH_SCHEME".format(extra_help, sys.argv[0])) | ||
| print(" Choices for AUTH_SCHEME are:") | ||
@@ -52,9 +48,13 @@ for x in vector: | ||
| if inp_stream is None or inp_stream == "-": | ||
| pw = get_password(sys.stdin, | ||
| prompt=f"Enter current password for scheme {scheme!r}: ",) | ||
| pw = get_password( | ||
| sys.stdin, | ||
| prompt=f"Enter current password for scheme {scheme!r}: ", | ||
| ) | ||
| else: | ||
| pw = get_password(open(inp_stream, "r", encoding='utf-8'), | ||
| prompt=f"Enter current password for scheme {scheme!r}: ",) | ||
| pw = get_password( | ||
| open(inp_stream, "r", encoding='utf-8'), | ||
| prompt=f"Enter current password for scheme {scheme!r}: ", | ||
| ) | ||
| vector[scheme](pw, **options) | ||
| else: | ||
| print("did not recognize authentication scheme argument", file=sys.stderr) |
+28
-54
@@ -39,4 +39,4 @@ from collections import OrderedDict | ||
| class Query: | ||
| def __init__(self, sess, *args, **kwargs): | ||
@@ -95,12 +95,5 @@ self.sess = sess | ||
| elif type(criterion.value) == list: | ||
| criterion.value = [ | ||
| str.upper(c) if type(c) == str else c for c in criterion.value | ||
| ] | ||
| criterion.value = [str.upper(c) if type(c) == str else c for c in criterion.value] | ||
| elif type(criterion.value) == tuple: | ||
| criterion.value = tuple( | ||
| [ | ||
| str.upper(c) if type(c) == str else c | ||
| for c in list(criterion.value) | ||
| ] | ||
| ) | ||
| criterion.value = tuple([str.upper(c) if type(c) == str else c for c in list(criterion.value)]) | ||
| new_q.criteria.append(criterion) | ||
@@ -166,5 +159,3 @@ | ||
| def _select_message(self): | ||
| dct = OrderedDict( | ||
| [(column.icat_id, value) for (column, value) in self.columns.items()] | ||
| ) | ||
| dct = OrderedDict([(column.icat_id, value) for (column, value) in self.columns.items()]) | ||
| return IntegerIntegerMap(dct) | ||
@@ -175,25 +166,21 @@ | ||
| def _conds_message(self): | ||
| dct = _OrderedMultiMapping( | ||
| [ | ||
| ( | ||
| criterion.query_key.icat_id, | ||
| criterion.op + " " + criterion.irods_value, | ||
| ) | ||
| for criterion in self.criteria | ||
| if isinstance(criterion.query_key, Column) | ||
| ] | ||
| ) | ||
| dct = _OrderedMultiMapping([ | ||
| ( | ||
| criterion.query_key.icat_id, | ||
| criterion.op + " " + criterion.irods_value, | ||
| ) | ||
| for criterion in self.criteria | ||
| if isinstance(criterion.query_key, Column) | ||
| ]) | ||
| return IntegerStringMap(dct) | ||
| def _kw_message(self): | ||
| dct = dict( | ||
| [ | ||
| ( | ||
| criterion.query_key.icat_key, | ||
| criterion.op + " " + criterion.irods_value, | ||
| ) | ||
| for criterion in self.criteria | ||
| if isinstance(criterion.query_key, Keyword) | ||
| ] | ||
| ) | ||
| dct = dict([ | ||
| ( | ||
| criterion.query_key.icat_key, | ||
| criterion.op + " " + criterion.irods_value, | ||
| ) | ||
| for criterion in self.criteria | ||
| if isinstance(criterion.query_key, Keyword) | ||
| ]) | ||
| for key in self._keywords: | ||
@@ -218,7 +205,4 @@ dct[key] = self._keywords[key] | ||
| with self.sess.pool.get_connection() as conn: | ||
| message_body = self._message() | ||
| message = iRODSMessage( | ||
| "RODS_API_REQ", msg=message_body, int_info=api_number["GEN_QUERY_AN"] | ||
| ) | ||
| message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["GEN_QUERY_AN"]) | ||
@@ -261,5 +245,3 @@ conn.send(message) | ||
| try: | ||
| result_set = self.continue_index( | ||
| result_set.continue_index | ||
| ).execute() | ||
| result_set = self.continue_index(result_set.continue_index).execute() | ||
| yield result_set | ||
@@ -306,3 +288,2 @@ except CAT_NO_ROWS_FOUND: | ||
| class SpecificQuery: | ||
| def __init__(self, sess, sql=None, alias=None, columns=None, args=None): | ||
@@ -323,8 +304,4 @@ if not sql and not alias: | ||
| message_body = GeneralAdminRequest( | ||
| "add", "specificQuery", self._sql, self._alias | ||
| ) | ||
| request = iRODSMessage( | ||
| "RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"] | ||
| ) | ||
| message_body = GeneralAdminRequest("add", "specificQuery", self._sql, self._alias) | ||
| request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"]) | ||
@@ -340,5 +317,3 @@ with self.session.pool.get_connection() as conn: | ||
| message_body = GeneralAdminRequest("rm", "specificQuery", target) | ||
| request = iRODSMessage( | ||
| "RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"] | ||
| ) | ||
| request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"]) | ||
@@ -367,8 +342,6 @@ with self.session.pool.get_connection() as conn: | ||
| KeyValPair_PI=conditions, | ||
| **sql_args | ||
| **sql_args, | ||
| ) | ||
| request = iRODSMessage( | ||
| "RODS_API_REQ", msg=message_body, int_info=api_number["SPECIFIC_QUERY_AN"] | ||
| ) | ||
| request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["SPECIFIC_QUERY_AN"]) | ||
@@ -405,3 +378,4 @@ with self.session.pool.get_connection() as conn: | ||
| import irods.helpers as helpers | ||
| cached_values = helpers.create_value_cache(globals()) | ||
| cached_values.make_entry('IRODS_QUERY_LIMIT') |
+5
-20
@@ -6,3 +6,2 @@ from irods.models import Resource | ||
| class iRODSResource: | ||
| def __init__(self, manager, result=None): | ||
@@ -57,7 +56,3 @@ self._hierarchy_string = "" | ||
| else: | ||
| self._parent_id = ( | ||
| sess.query(Resource) | ||
| .filter(Resource.name == self.parent) | ||
| .one()[Resource.id] | ||
| ) | ||
| self._parent_id = sess.query(Resource).filter(Resource.name == self.parent).one()[Resource.id] | ||
| return int(self._parent_id) | ||
@@ -74,7 +69,3 @@ | ||
| else: | ||
| self._parent_name = ( | ||
| sess.query(Resource) | ||
| .filter(Resource.id == self.parent) | ||
| .one()[Resource.name] | ||
| ) | ||
| self._parent_name = sess.query(Resource).filter(Resource.id == self.parent).one()[Resource.name] | ||
| return self._parent_name | ||
@@ -87,5 +78,3 @@ | ||
| if self._hierarchy_string == "": | ||
| self._hierarchy_string = ";".join( | ||
| r.name for r in self.hierarchy_as_list_of_resource_objects() | ||
| ) | ||
| self._hierarchy_string = ";".join(r.name for r in self.hierarchy_as_list_of_resource_objects()) | ||
| return self._hierarchy_string | ||
@@ -108,5 +97,3 @@ | ||
| if not self._meta: | ||
| self._meta = iRODSMetaCollection( | ||
| self.manager.sess.metadata, Resource, self.name | ||
| ) | ||
| self._meta = iRODSMetaCollection(self.manager.sess.metadata, Resource, self.name) | ||
| return self._meta | ||
@@ -137,5 +124,3 @@ | ||
| # query for children and cache results | ||
| query = session.query(Resource).filter( | ||
| Resource.parent == "{}".format(parent) | ||
| ) | ||
| query = session.query(Resource).filter(Resource.parent == "{}".format(parent)) | ||
| self._children = [self.__class__(self.manager, res) for res in query] | ||
@@ -142,0 +127,0 @@ |
+2
-8
@@ -6,3 +6,2 @@ from prettytable import PrettyTable | ||
| class ResultSet: | ||
| def __init__(self, raw): | ||
@@ -40,11 +39,7 @@ self.length = raw.rowCnt | ||
| _get_column_values = lambda self, index: [ | ||
| (col, col.value[index]) for col in self.cols | ||
| ] | ||
| _get_column_values = lambda self, index: [(col, col.value[index]) for col in self.cols] | ||
| def _format_row(self, index): | ||
| values = self._get_column_values(index) | ||
| return dict( | ||
| [self._format_attribute(col.attriInx, value) for col, value in values] | ||
| ) | ||
| return dict([self._format_attribute(col.attriInx, value) for col, value in values]) | ||
@@ -72,3 +67,2 @@ def __getitem__(self, index): | ||
| class SpecificQueryResultSet(ResultSet): | ||
| def __init__(self, raw, columns=None): | ||
@@ -75,0 +69,0 @@ self._query_columns = columns |
+9
-36
@@ -61,7 +61,3 @@ from irods.message import ( | ||
| else: | ||
| self.body = ( | ||
| "@external\n" + body | ||
| if irods_3_literal_style | ||
| else "@external rule { " + body + " }" | ||
| ) | ||
| self.body = "@external\n" + body if irods_3_literal_style else "@external rule { " + body + " }" | ||
@@ -114,11 +110,5 @@ # overwrite params and output if received arguments | ||
| with ( | ||
| io_open(rule_file, encoding=encoding) | ||
| if isinstance(rule_file, str) | ||
| else rule_file | ||
| ) as f: | ||
| with io_open(rule_file, encoding=encoding) if isinstance(rule_file, str) else rule_file as f: | ||
| # parse rule file line-by-line | ||
| for line in f: | ||
| # convert input line to Unicode if necessary | ||
@@ -130,3 +120,2 @@ if isinstance(line, bytes): | ||
| if line.strip().lower().startswith("input"): | ||
| input_header, input_line = line.split(None, 1) | ||
@@ -174,17 +163,9 @@ | ||
| inOutStruct = STR_PI(myStr=value) | ||
| param_array.append( | ||
| MsParam(label=label, type="STR_PI", inOutStruct=inOutStruct) | ||
| ) | ||
| param_array.append(MsParam(label=label, type="STR_PI", inOutStruct=inOutStruct)) | ||
| inpParamArray = MsParamArray( | ||
| paramLen=len(param_array), oprType=0, MsParam_PI=param_array | ||
| ) | ||
| inpParamArray = MsParamArray(paramLen=len(param_array), oprType=0, MsParam_PI=param_array) | ||
| # rule body | ||
| addr = RodsHostAddress(hostAddr="", rodsZone="", port=0, dummyInt=0) | ||
| condInput = StringStringMap( | ||
| {} | ||
| if self.instance_name is None | ||
| else {"instance_name": self.instance_name} | ||
| ) | ||
| condInput = StringStringMap({} if self.instance_name is None else {"instance_name": self.instance_name}) | ||
| message_body = RuleExecutionRequest( | ||
@@ -198,19 +179,11 @@ myRule=self.body, | ||
| request = iRODSMessage( | ||
| "RODS_API_REQ", msg=message_body, int_info=api_number["EXEC_MY_RULE_AN"] | ||
| ) | ||
| request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["EXEC_MY_RULE_AN"]) | ||
| with self.session.pool.get_connection() as conn: | ||
| conn.send(request) | ||
| response = conn.recv( | ||
| acceptable_errors=acceptable_errors, return_message=return_message | ||
| ) | ||
| response = conn.recv(acceptable_errors=acceptable_errors, return_message=return_message) | ||
| try: | ||
| out_param_array = response.get_main_message( | ||
| MsParamArray, r_error=r_error | ||
| ) | ||
| out_param_array = response.get_main_message(MsParamArray, r_error=r_error) | ||
| except iRODSMessage.ResponseNotParseable: | ||
| return ( | ||
| MsParamArray() | ||
| ) # Ergo, no useful return value - but the RError stack will be accessible | ||
| return MsParamArray() # Ergo, no useful return value - but the RError stack will be accessible | ||
| finally: | ||
@@ -217,0 +190,0 @@ if session_cleanup: |
+30
-51
@@ -5,2 +5,3 @@ import ast | ||
| import errno | ||
| from io import BufferedRandom | ||
| import json | ||
@@ -11,3 +12,5 @@ import logging | ||
| import threading | ||
| from typing import Iterable, Any, Optional | ||
| import weakref | ||
| import irods.auth | ||
@@ -34,3 +37,3 @@ from irods.query import Query | ||
| _fds = None | ||
| _fds: Optional[dict[BufferedRandom, Any]] = None | ||
| _fds_lock = threading.Lock() | ||
@@ -41,2 +44,14 @@ _sessions = None | ||
| def _exclude_fds_from_auto_close(descriptors: Iterable): | ||
| """Remove all descriptors from consideration for auto_close.""" | ||
| from irods.manager.data_object_manager import ManagedBufferedRandom | ||
| with _fds_lock: | ||
| fds: dict[BufferedRandom, Any] = _fds or {} | ||
| for fd in descriptors: | ||
| fds.pop(fd, None) | ||
| if isinstance(fd, ManagedBufferedRandom): | ||
| fd.do_close = False | ||
| def _cleanup_remaining_sessions(): | ||
@@ -53,5 +68,3 @@ for fd in list((_fds or {}).keys()): | ||
| with _sessions_lock: | ||
| at_client_exit._register( | ||
| at_client_exit.LibraryCleanupStage.DURING, _cleanup_remaining_sessions | ||
| ) | ||
| at_client_exit._register(at_client_exit.LibraryCleanupStage.DURING, _cleanup_remaining_sessions) | ||
@@ -80,3 +93,2 @@ | ||
| class iRODSSession: | ||
| def library_features(self): | ||
@@ -86,5 +98,3 @@ irods_version_needed = (4, 3, 1) | ||
| raise NotImplementedInIRODSServer("library_features", irods_version_needed) | ||
| message = iRODSMessage( | ||
| "RODS_API_REQ", int_info=api_number["GET_LIBRARY_FEATURES_AN"] | ||
| ) | ||
| message = iRODSMessage("RODS_API_REQ", int_info=api_number["GET_LIBRARY_FEATURES_AN"]) | ||
| with self.pool.get_connection() as conn: | ||
@@ -111,5 +121,3 @@ conn.send(message) | ||
| except AttributeError: | ||
| self.__access = ( | ||
| _iRODSAccess_pre_4_3_0 if self.server_version < (4, 3) else iRODSAccess | ||
| ) | ||
| self.__access = _iRODSAccess_pre_4_3_0 if self.server_version < (4, 3) else iRODSAccess | ||
| return self.__access | ||
@@ -124,5 +132,3 @@ | ||
| self._cached_connection_timeout = None | ||
| self.connection_timeout = kwargs.pop( | ||
| "connection_timeout", DEFAULT_CONNECTION_TIMEOUT | ||
| ) | ||
| self.connection_timeout = kwargs.pop("connection_timeout", DEFAULT_CONNECTION_TIMEOUT) | ||
| self.__configured = None | ||
@@ -145,7 +151,3 @@ if configure: | ||
| self.auth_options_by_scheme = { | ||
| "pam_password": { | ||
| irods.auth.CLIENT_GET_REQUEST_RESULT: (lambda sess, conn: []) | ||
| } | ||
| } | ||
| self.auth_options_by_scheme = {"pam_password": {irods.auth.CLIENT_GET_REQUEST_RESULT: (lambda sess, conn: [])}} | ||
@@ -275,7 +277,3 @@ if auto_cleanup: | ||
| if missing_file_path: | ||
| error_args += [ | ||
| "Authentication file not found at {!r}".format( | ||
| missing_file_path[0] | ||
| ) | ||
| ] | ||
| error_args += ["Authentication file not found at {!r}".format(missing_file_path[0])] | ||
| raise NonAnonymousLoginWithoutPassword(*error_args) | ||
@@ -292,7 +290,3 @@ | ||
| connection_refresh_time = self.get_connection_refresh_time(**kwargs) | ||
| logger.debug( | ||
| "In iRODSSession's configure(). connection_refresh_time set to {}".format( | ||
| connection_refresh_time | ||
| ) | ||
| ) | ||
| logger.debug("In iRODSSession's configure(). connection_refresh_time set to {}".format(connection_refresh_time)) | ||
| self.pool = Pool( | ||
@@ -354,5 +348,3 @@ account, | ||
| GET_SERVER_VERSION_WITHOUT_AUTH = staticmethod( | ||
| lambda s: s.server_version_without_auth() | ||
| ) | ||
| GET_SERVER_VERSION_WITHOUT_AUTH = staticmethod(lambda s: s.server_version_without_auth()) | ||
@@ -402,9 +394,4 @@ def _server_version(self, version_func=None): | ||
| self.pool.account.store_pw = box = [] | ||
| if ( | ||
| self.server_version_without_auth() >= (4, 3) | ||
| and not client_config.legacy_auth.force_legacy_auth | ||
| ): | ||
| old_setting = self.set_auth_option_for_scheme( | ||
| "pam_password", irods.auth.CLIENT_GET_REQUEST_RESULT, box | ||
| ) | ||
| if self.server_version_without_auth() >= (4, 3) and not client_config.legacy_auth.force_legacy_auth: | ||
| old_setting = self.set_auth_option_for_scheme("pam_password", irods.auth.CLIENT_GET_REQUEST_RESULT, box) | ||
| conn = self.pool.get_connection() | ||
@@ -417,5 +404,3 @@ pw = getattr(self.pool.account, "store_pw", []) | ||
| if old_setting is not _dummy: | ||
| self.set_auth_option_for_scheme( | ||
| "pam_password", irods.auth.CLIENT_GET_REQUEST_RESULT, old_setting | ||
| ) | ||
| self.set_auth_option_for_scheme("pam_password", irods.auth.CLIENT_GET_REQUEST_RESULT, old_setting) | ||
@@ -437,5 +422,3 @@ @property | ||
| if seconds == 0: | ||
| exc = ValueError( | ||
| "Setting an iRODS connection_timeout to 0 seconds would make it non-blocking." | ||
| ) | ||
| exc = ValueError("Setting an iRODS connection_timeout to 0 seconds would make it non-blocking.") | ||
| raise exc | ||
@@ -459,5 +442,3 @@ elif isinstance(seconds, Number): | ||
| else: | ||
| exc = ValueError( | ||
| "The iRODS connection_timeout must be assigned a positive int, positive float, or None." | ||
| ) | ||
| exc = ValueError("The iRODS connection_timeout must be assigned a positive int, positive float, or None.") | ||
| raise exc | ||
@@ -531,5 +512,3 @@ self._cached_connection_timeout = seconds | ||
| env_file_map = self.get_irods_env(env_file) | ||
| connection_refresh_time = int( | ||
| env_file_map.get("irods_connection_refresh_time", -1) | ||
| ) | ||
| connection_refresh_time = int(env_file_map.get("irods_connection_refresh_time", -1)) | ||
| if connection_refresh_time < 1: | ||
@@ -536,0 +515,0 @@ # Negative values are not allowed. |
@@ -19,3 +19,2 @@ #! /usr/bin/env python | ||
| class TestAccess(unittest.TestCase): | ||
| def setUp(self): | ||
@@ -25,5 +24,3 @@ self.sess = helpers.make_session() | ||
| # Create test collection | ||
| self.coll_path = "/{}/home/{}/test_dir".format( | ||
| self.sess.zone, self.sess.username | ||
| ) | ||
| self.coll_path = "/{}/home/{}/test_dir".format(self.sess.zone, self.sess.username) | ||
| self.coll = helpers.make_collection(self.sess, self.coll_path) | ||
@@ -86,5 +83,3 @@ VERSION_DEPENDENT_STRINGS = ( | ||
| repr(acl), | ||
| "<iRODSAccess own {path} {name}({type}) {zone}>".format( | ||
| path=path, **vars(user) | ||
| ), | ||
| "<iRODSAccess own {path} {name}({type}) {zone}>".format(path=path, **vars(user)), | ||
| ) | ||
@@ -113,5 +108,3 @@ | ||
| user = self.sess.users.create("bob", "rodsuser") | ||
| data = self.sess.data_objects.create( | ||
| "{}/obj_422".format(helpers.home_collection(self.sess)) | ||
| ) | ||
| data = self.sess.data_objects.create("{}/obj_422".format(helpers.home_collection(self.sess))) | ||
| permission_strings = self.sess.available_permissions.keys() | ||
@@ -165,16 +158,10 @@ for perm in permission_strings: | ||
| c[Collection.id] | ||
| for c in self.sess.query(Collection.id).filter( | ||
| Like(Collection.name, deepcoll.path + "%") | ||
| ) | ||
| for c in self.sess.query(Collection.id).filter(Like(Collection.name, deepcoll.path + "%")) | ||
| ] | ||
| D_rods = list( | ||
| self.sess.query(Collection.name, DataObject.name).filter( | ||
| In(DataObject.collection_id, coll_IDs) | ||
| ) | ||
| self.sess.query(Collection.name, DataObject.name).filter(In(DataObject.collection_id, coll_IDs)) | ||
| ) | ||
| self.assertEqual( | ||
| len(D_rods), OBJ_PER_LVL * DEPTH + 1 | ||
| ) # counts the 'older' objects plus one new object | ||
| self.assertEqual(len(D_rods), OBJ_PER_LVL * DEPTH + 1) # counts the 'older' objects plus one new object | ||
@@ -188,9 +175,4 @@ with iRODSSession( | ||
| ) as bob: | ||
| D = list(bob.query(Collection.name, DataObject.name).filter(In(DataObject.collection_id, coll_IDs))) | ||
| D = list( | ||
| bob.query(Collection.name, DataObject.name).filter( | ||
| In(DataObject.collection_id, coll_IDs) | ||
| ) | ||
| ) | ||
| # - bob should only see the new data object, but none existing before ACLs were applied | ||
@@ -208,6 +190,4 @@ | ||
| C = list(bob.query(Collection).filter(In(Collection.id, coll_IDs))) | ||
| self.assertEqual(len(C), 2) # query should return only the top-level and newly created collections | ||
| self.assertEqual( | ||
| len(C), 2 | ||
| ) # query should return only the top-level and newly created collections | ||
| self.assertEqual( | ||
| sorted([c[Collection.name] for c in C]), | ||
@@ -228,5 +208,3 @@ sorted([new_collection.path, deepcoll.path]), | ||
| test_coll_path = self.coll_path + "/test" | ||
| deepcoll = helpers.make_deep_collection( | ||
| self.sess, test_coll_path, depth=DEPTH, objects_per_level=2 | ||
| ) | ||
| deepcoll = helpers.make_deep_collection(self.sess, test_coll_path, depth=DEPTH, objects_per_level=2) | ||
| acl1 = iRODSAccess("inherit", deepcoll.path) | ||
@@ -236,5 +214,3 @@ self.sess.acls.set(acl1, recursive=recursionTruth) | ||
| iRODSCollection(self.sess.collections, _) | ||
| for _ in self.sess.query(Collection).filter( | ||
| Like(Collection.name, deepcoll.path + "/%") | ||
| ) | ||
| for _ in self.sess.query(Collection).filter(Like(Collection.name, deepcoll.path + "/%")) | ||
| ) | ||
@@ -247,5 +223,3 @@ | ||
| # assert lower level collections affected only for case when recursive = True | ||
| subcoll_truths = [ | ||
| (_.inheritance == recursionTruth) for _ in test_subcolls | ||
| ] | ||
| subcoll_truths = [(_.inheritance == recursionTruth) for _ in test_subcolls] | ||
| self.assertEqual(len(subcoll_truths), DEPTH - 1) | ||
@@ -356,4 +330,3 @@ self.assertTrue(all(subcoll_truths)) | ||
| for u in [ | ||
| iRODSUser(self.sess.users, row) | ||
| for row in self.sess.query(User).filter(In(User.id, ids_for_delete)) | ||
| iRODSUser(self.sess.users, row) for row in self.sess.query(User).filter(In(User.id, ids_for_delete)) | ||
| ]: | ||
@@ -384,7 +357,4 @@ u.remove() | ||
| for obj in self.coll_395, self.data: | ||
| # Add ACLs | ||
| for access in iRODSAccess("read", obj.path, "team"), iRODSAccess( | ||
| "write", obj.path, "alice" | ||
| ): | ||
| for access in iRODSAccess("read", obj.path, "team"), iRODSAccess("write", obj.path, "alice"): | ||
| ses.acls.set(access) | ||
@@ -397,10 +367,3 @@ | ||
| 1, | ||
| len( | ||
| [ | ||
| ac | ||
| for ac in ACLs | ||
| if (ac.access_name, ac.user_name) | ||
| == (self.mapping["write"], "alice") | ||
| ] | ||
| ), | ||
| len([ac for ac in ACLs if (ac.access_name, ac.user_name) == (self.mapping["write"], "alice")]), | ||
| ) | ||
@@ -425,5 +388,3 @@ | ||
| ) as bob: | ||
| self.assertTrue( | ||
| bob.data_objects.open(self.data.path, "r").read(), b"contents-395" | ||
| ) | ||
| self.assertTrue(bob.data_objects.open(self.data.path, "r").read(), b"contents-395") | ||
@@ -474,5 +435,3 @@ finally: | ||
| user_name = "testuser" | ||
| collection_path = "/".join( | ||
| [helpers.home_collection(self.sess), "give_read_access_to_this"] | ||
| ) | ||
| collection_path = "/".join([helpers.home_collection(self.sess), "give_read_access_to_this"]) | ||
@@ -499,5 +458,3 @@ try: | ||
| user_name = "testuser" | ||
| data_object_path = "/".join( | ||
| [helpers.home_collection(self.sess), "give_read_access_to_this"] | ||
| ) | ||
| data_object_path = "/".join([helpers.home_collection(self.sess), "give_read_access_to_this"]) | ||
@@ -524,5 +481,3 @@ try: | ||
| user_name = "testuser" | ||
| data_object_path = "/".join( | ||
| [helpers.home_collection(self.sess), "give_read_access_to_this"] | ||
| ) | ||
| data_object_path = "/".join([helpers.home_collection(self.sess), "give_read_access_to_this"]) | ||
@@ -553,7 +508,3 @@ try: | ||
| # TODO(#480): We cannot use the unittest.assertRaises context manager as this was introduced in python 3.1. | ||
| assertCall = getattr(self, "assertRaisesRegex", None) | ||
| if assertCall is None: | ||
| assertCall = self.assertRaisesRegexp | ||
| assertCall( | ||
| self.assertRaisesRegex( | ||
| TypeError, | ||
@@ -560,0 +511,0 @@ "'path' parameter must be of type 'str', 'irods.collection.iRODSCollection', " |
+68
-42
@@ -7,12 +7,14 @@ #! /usr/bin/env python | ||
| import unittest | ||
| from irods.models import User, Group | ||
| import irods.keywords as kw | ||
| from irods.column import Like | ||
| from irods.exception import ( | ||
| SYS_NO_API_PRIV, | ||
| ResourceDoesNotExist, | ||
| UserDoesNotExist, | ||
| ResourceDoesNotExist, | ||
| SYS_NO_API_PRIV, | ||
| ) | ||
| from irods.models import Collection, Group, User | ||
| from irods.resource import iRODSResource | ||
| from irods.session import iRODSSession | ||
| from irods.resource import iRODSResource | ||
| import irods.test.helpers as helpers | ||
| import irods.keywords as kw | ||
| from irods.test import helpers | ||
@@ -45,5 +47,3 @@ | ||
| self.assertEqual(user.zone, self.sess.zone) | ||
| self.assertEqual( | ||
| repr(user), "<iRODSUser {id} {name} {type} {zone}>".format(**vars(user)) | ||
| ) | ||
| self.assertEqual(repr(user), "<iRODSUser {id} {name} {type} {zone}>".format(**vars(user))) | ||
@@ -63,5 +63,3 @@ # delete user | ||
| # create user | ||
| user = self.sess.users.create( | ||
| self.new_user_name, self.new_user_type, self.sess.zone | ||
| ) | ||
| user = self.sess.users.create(self.new_user_name, self.new_user_type, self.sess.zone) | ||
@@ -83,3 +81,3 @@ # assertions | ||
| gpadmin_user = self.sess.users.create(self.new_user_name, "groupadmin") | ||
| gpadmin_user.modify("password", gpadmin_password:='my-gpadmin-passw') | ||
| gpadmin_user.modify("password", gpadmin_password := 'my-gpadmin-passw') | ||
| remote_zone = self.sess.zones.create('other_zone', 'remote') | ||
@@ -93,18 +91,18 @@ with iRODSSession( | ||
| ) as gpadmin: | ||
| for expected_exception,args,kwargs in ( | ||
| (None, ("newUser","newPassword"), {}), # no zone supplied | ||
| (None, ("newUser","newPassword"), {"user_zone":gpadmin.zone}), # local zone supplied | ||
| (ValueError, ("newUser","newPassword"), {"user_zone":remote_zone.name}), # remote zone supplied | ||
| (ValueError, ("newUser","newPassword"), {"user_zone":"fictionZone"}), # nonexistent zone supplied | ||
| (ValueError, ("newUser#tempZone","newPassword"), {}), | ||
| (ValueError, (f"newUser#{remote_zone.name}","newPassword"), {}), | ||
| (ValueError, ("newUser#fictionZone","newPassword"), {}), | ||
| (ValueError, (f"newUser#{gpadmin.zone}","newPassword"), {"user_zone":gpadmin.zone}), | ||
| (ValueError, (f"newUser#{gpadmin.zone}","newPassword"), {"user_zone":remote_zone.name}), | ||
| (ValueError, (f"newUser#{gpadmin.zone}","newPassword"), {"user_zone":"fictionZone"}), | ||
| for expected_exception, args, kwargs in ( | ||
| (None, ("newUser", "newPassword"), {}), # no zone supplied | ||
| (None, ("newUser", "newPassword"), {"user_zone": gpadmin.zone}), # local zone supplied | ||
| (ValueError, ("newUser", "newPassword"), {"user_zone": remote_zone.name}), # remote zone supplied | ||
| (ValueError, ("newUser", "newPassword"), {"user_zone": "fictionZone"}), # nonexistent zone supplied | ||
| (ValueError, ("newUser#tempZone", "newPassword"), {}), | ||
| (ValueError, (f"newUser#{remote_zone.name}", "newPassword"), {}), | ||
| (ValueError, ("newUser#fictionZone", "newPassword"), {}), | ||
| (ValueError, (f"newUser#{gpadmin.zone}", "newPassword"), {"user_zone": gpadmin.zone}), | ||
| (ValueError, (f"newUser#{gpadmin.zone}", "newPassword"), {"user_zone": remote_zone.name}), | ||
| (ValueError, (f"newUser#{gpadmin.zone}", "newPassword"), {"user_zone": "fictionZone"}), | ||
| ): | ||
| with self.subTest(args = args, kwargs = kwargs): | ||
| with self.subTest(args=args, kwargs=kwargs): | ||
| user = None | ||
| try: | ||
| test_function = lambda:gpadmin.users.create_with_password(*args, **kwargs) | ||
| test_function = lambda: gpadmin.users.create_with_password(*args, **kwargs) | ||
| if expected_exception: | ||
@@ -115,6 +113,10 @@ with self.assertRaises(expected_exception): | ||
| user = test_function() | ||
| self.assertEqual(user is None, expected_exception is not None, "In case of error, and only then, user should not exist.") | ||
| self.assertEqual( | ||
| user is None, | ||
| expected_exception is not None, | ||
| "In case of error, and only then, user should not exist.", | ||
| ) | ||
| finally: | ||
| if user: | ||
| self.sess.users.get(user.name,user.zone).remove() | ||
| self.sess.users.get(user.name, user.zone).remove() | ||
| finally: | ||
@@ -129,3 +131,3 @@ if remote_zone: | ||
| gpadmin_user = self.sess.users.create(self.new_user_name, "groupadmin") | ||
| gpadmin_user.modify("password", gpadmin_password:='my-gpadmin-passw') | ||
| gpadmin_user.modify("password", gpadmin_password := 'my-gpadmin-passw') | ||
| try: | ||
@@ -150,6 +152,9 @@ remote_zone = self.sess.zones.create('other_zone', 'remote') | ||
| # Add the remote test user to the group we created, then assert membership. | ||
| test_group.addmember(test_user.name, user_zone = remote_zone.name) | ||
| test_group.addmember(test_user.name, user_zone=remote_zone.name) | ||
| self.assertIn( | ||
| (test_user.name, test_user.zone), | ||
| [(_[User.name],_[User.zone]) for _ in self.sess.query(User,Group).filter(Group.name == test_group.name)] | ||
| [ | ||
| (_[User.name], _[User.zone]) | ||
| for _ in self.sess.query(User, Group).filter(Group.name == test_group.name) | ||
| ], | ||
| ) | ||
@@ -159,3 +164,3 @@ finally: | ||
| # Iterate through members from group (with groupadmin as the last), removing each one. | ||
| for member in sorted(test_group.members, key=lambda _:_.name == gpadmin_user.name): | ||
| for member in sorted(test_group.members, key=lambda _: _.name == gpadmin_user.name): | ||
| test_group.removemember(member.name, user_zone=member.zone) | ||
@@ -246,5 +251,3 @@ # Use rodadmin-enabled session to remove the group | ||
| # change type to rodsadmin | ||
| self.sess.users.modify( | ||
| "{}#{}".format(self.new_user_name, self.sess.zone), "type", "rodsadmin" | ||
| ) | ||
| self.sess.users.modify("{}#{}".format(self.new_user_name, self.sess.zone), "type", "rodsadmin") | ||
@@ -391,5 +394,3 @@ # check type again | ||
| resc_path = "/nobucket" | ||
| s3 = session.resources.create( | ||
| resc_name, resc_type, resc_host, resc_path, context | ||
| ) | ||
| s3 = session.resources.create(resc_name, resc_type, resc_host, resc_path, context) | ||
@@ -432,5 +433,3 @@ # verify context fields | ||
| # make new resource | ||
| self.sess.resources.create( | ||
| resc_name, resc_type, resc_host, resc_path, resource_class=resc_class | ||
| ) | ||
| self.sess.resources.create(resc_name, resc_type, resc_host, resc_path, resource_class=resc_class) | ||
@@ -497,3 +496,2 @@ # try invalid params | ||
| ) as session: | ||
| # do something that connects to the server | ||
@@ -547,3 +545,31 @@ session.users.get(username) | ||
| def test_deleting_remote_user_including_home_collection_and_trash_artifact__issue_763(self): | ||
| # Test and confirm that, when passing user and zone parameters separately in calls to | ||
| # remove remote users, that both /tempZone/home/user#zone and /tempZone/trash/home/user#zone | ||
| # are deleted. | ||
| remote_zone = remote_user = None | ||
| try: | ||
| remote_zone = (sess := self.sess).zones.create('other_zone', 'remote') | ||
| remote_user = sess.users.create(user_name='myuser', user_type='rodsuser', user_zone=remote_zone.name) | ||
| def get_collection_artifacts(): | ||
| return list( | ||
| sess.query(Collection).filter(Like(Collection.name, f'%/{remote_user.name}#{remote_zone.name}')) | ||
| ) | ||
| # Two collection artifacts should be present, with names: | ||
| # /<local_zone>/home/remote_user#remote_zone | ||
| # /<local_zone>/trash/home/remote_user#remote_zone | ||
| self.assertEqual(len(get_collection_artifacts()), 2) | ||
| remote_user.remove() | ||
| # The above-mentioned artifacts should have been deleted along with the remote user. | ||
| self.assertEqual(len(get_collection_artifacts()), 0) | ||
| finally: | ||
| if remote_zone: | ||
| remote_zone.remove() | ||
| if __name__ == "__main__": | ||
@@ -550,0 +576,0 @@ # let the tests find the parent irods lib |
@@ -13,7 +13,4 @@ import os | ||
| class TestCleanupFunctions(unittest.TestCase): | ||
| def test_execution_of_client_exit_functions_at_proper_time__issue_614(self): | ||
| helper_script = os.path.join( | ||
| test_modules.__path__[0], "test_client_exit_functions.py" | ||
| ) | ||
| helper_script = os.path.join(test_modules.__path__[0], "test_client_exit_functions.py") | ||
@@ -28,5 +25,3 @@ # Note: The enum.Enum subclass's __members__ is an ordered dictionary, i.e. key order is preserved: | ||
| p = subprocess.Popen( | ||
| [sys.executable, helper_script, *args], stdout=subprocess.PIPE | ||
| ) | ||
| p = subprocess.Popen([sys.executable, helper_script, *args], stdout=subprocess.PIPE) | ||
| script_output = p.communicate()[0].decode().strip() | ||
@@ -67,6 +62,4 @@ from irods.test.modules.test_client_exit_functions import ( | ||
| stdout_content, stderr_content = process.communicate() | ||
| self.assertEqual(len(list(re.finditer(rb"ZeroDivisionError.*\n", stderr_content))), 4) | ||
| self.assertEqual( | ||
| len(list(re.finditer(rb"ZeroDivisionError.*\n", stderr_content))), 4 | ||
| ) | ||
| self.assertEqual( | ||
| b",".join([m.group() for m in re.finditer(rb"(\w+)", stdout_content)]), | ||
@@ -73,0 +66,0 @@ b"before,during,after2,after1", |
@@ -11,3 +11,2 @@ #!/usr/bin/env python | ||
| class TestClientHints(unittest.TestCase): | ||
| def setUp(self): | ||
@@ -14,0 +13,0 @@ self.sess = helpers.make_session() |
@@ -22,3 +22,2 @@ #! /usr/bin/env python | ||
| class TestCollection(unittest.TestCase): | ||
| class WrongUserType(RuntimeError): | ||
@@ -31,5 +30,3 @@ pass | ||
| if adm.users.get(adm.username).type != "rodsadmin": | ||
| raise cls.WrongUserType( | ||
| "Must be an iRODS admin to run tests in class {0.__name__}".format(cls) | ||
| ) | ||
| raise cls.WrongUserType("Must be an iRODS admin to run tests in class {0.__name__}".format(cls)) | ||
| cls.logins = helpers.iRODSUserLogins(adm) | ||
@@ -49,5 +46,3 @@ cls.logins.create_user(RODSUSER, "abc123") | ||
| self.test_coll_path = "/{}/home/{}/test_dir".format( | ||
| self.sess.zone, self.sess.username | ||
| ) | ||
| self.test_coll_path = "/{}/home/{}/test_dir".format(self.sess.zone, self.sess.username) | ||
| self.test_coll = self.sess.collections.create(self.test_coll_path) | ||
@@ -299,5 +294,3 @@ | ||
| # confirm object count in collection | ||
| query = ( | ||
| self.sess.query().count(DataObject.id).filter(Collection.name == coll_path) | ||
| ) | ||
| query = self.sess.query().count(DataObject.id).filter(Collection.name == coll_path) | ||
| obj_count = next(query.get_results())[DataObject.id] | ||
@@ -335,5 +328,3 @@ self.assertEqual(file_count, int(obj_count)) | ||
| # confirm object count in collection | ||
| query = ( | ||
| self.sess.query().count(DataObject.id).filter(Collection.name == coll_path) | ||
| ) | ||
| query = self.sess.query().count(DataObject.id).filter(Collection.name == coll_path) | ||
| obj_count = next(query.get_results())[DataObject.id] | ||
@@ -358,5 +349,3 @@ self.assertEqual(file_count, int(obj_count)) | ||
| Home = helpers.home_collection(self.sess) | ||
| subcoll, dataobj = [ | ||
| unique_name(my_function_name(), time.time()) for x in range(2) | ||
| ] | ||
| subcoll, dataobj = [unique_name(my_function_name(), time.time()) for x in range(2)] | ||
| subcoll_fullpath = "{}/{}".format(Home, subcoll) | ||
@@ -377,5 +366,3 @@ subcoll_unnormalized = subcoll_fullpath + "/" | ||
| self.assertEqual( | ||
| self.sess.query(DataObject) | ||
| .filter(DataObject.name == dataobj) | ||
| .one()[DataObject.collection_id], | ||
| self.sess.query(DataObject).filter(DataObject.name == dataobj).one()[DataObject.collection_id], | ||
| c1.id, | ||
@@ -427,11 +414,7 @@ ) | ||
| new_mtime = 1400000000 | ||
| user_session.collections.touch( | ||
| home_collection_path, seconds_since_epoch=new_mtime | ||
| ) | ||
| user_session.collections.touch(home_collection_path, seconds_since_epoch=new_mtime) | ||
| # Compare mtimes for correctness. | ||
| collection = user_session.collections.get(home_collection_path) | ||
| self.assertEqual( | ||
| datetime.fromtimestamp(new_mtime, timezone.utc), collection.modify_time | ||
| ) | ||
| self.assertEqual(datetime.fromtimestamp(new_mtime, timezone.utc), collection.modify_time) | ||
| self.assertGreater(old_mtime, collection.modify_time) | ||
@@ -465,4 +448,6 @@ | ||
| # Create a data object. | ||
| data_object_path = "{home_collection}/test_touch_operation_does_not_work_when_given_a_data_object__525.txt".format( | ||
| **locals() | ||
| data_object_path = ( | ||
| "{home_collection}/test_touch_operation_does_not_work_when_given_a_data_object__525.txt".format( | ||
| **locals() | ||
| ) | ||
| ) | ||
@@ -485,5 +470,3 @@ self.assertFalse(user_session.data_objects.exists(data_object_path)) | ||
| home_collection = helpers.home_collection(user_session) | ||
| path = "{home_collection}/test_touch_operation_ignores_unsupported_options__525".format( | ||
| **locals() | ||
| ) | ||
| path = "{home_collection}/test_touch_operation_ignores_unsupported_options__525".format(**locals()) | ||
@@ -490,0 +473,0 @@ try: |
@@ -20,3 +20,2 @@ #! /usr/bin/env python | ||
| class TestConnections(unittest.TestCase): | ||
| def setUp(self): | ||
@@ -112,5 +111,3 @@ self.sess = helpers.make_session() | ||
| local_file.write(rand) | ||
| obj = sess.data_objects.put( | ||
| local_file.name, logical_path, return_data_object=True | ||
| ) | ||
| obj = sess.data_objects.put(local_file.name, logical_path, return_data_object=True) | ||
@@ -120,5 +117,3 @@ # Set a very short socket timeout and remove all pre-existing socket connections. | ||
| sess = ( | ||
| obj.manager.sess | ||
| ) # Because of client-redirect it is possible that self.sess and | ||
| sess = obj.manager.sess # Because of client-redirect it is possible that self.sess and | ||
| # obj.manager.sess do not refer to the same object. In any case, | ||
@@ -181,5 +176,3 @@ # it is the latter of the two iRODSSession objects that is | ||
| sess = helpers.make_session(connection_timeout=timeout) | ||
| self._assert_timeout_value_is_propagated_to_all_sockets__issue_569( | ||
| sess, timeout | ||
| ) | ||
| self._assert_timeout_value_is_propagated_to_all_sockets__issue_569(sess, timeout) | ||
@@ -190,5 +183,3 @@ def test_assigning_session_connection_timeout__issue_377(self): | ||
| sess.connection_timeout = timeout | ||
| self._assert_timeout_value_is_propagated_to_all_sockets__issue_569( | ||
| sess, timeout | ||
| ) | ||
| self._assert_timeout_value_is_propagated_to_all_sockets__issue_569(sess, timeout) | ||
@@ -241,5 +232,3 @@ def test_assigning_session_connection_timeout_to_invalid_values__issue_569(self): | ||
| self.assertEqual(old_timeout, sess.connection_timeout) | ||
| self._assert_timeout_value_is_propagated_to_all_sockets__issue_569( | ||
| sess, old_timeout | ||
| ) | ||
| self._assert_timeout_value_is_propagated_to_all_sockets__issue_569(sess, old_timeout) | ||
@@ -249,10 +238,6 @@ def test_legacy_auth_used_with_force_legacy_auth_configuration__issue_499(self): | ||
| with config.loadlines( | ||
| entries=[dict(setting="legacy_auth.force_legacy_auth", value=True)] | ||
| ): | ||
| with config.loadlines(entries=[dict(setting="legacy_auth.force_legacy_auth", value=True)]): | ||
| stream = io.StringIO() | ||
| logger = logging.getLogger("irods.connection") | ||
| with helpers.enableLogging( | ||
| logger, logging.StreamHandler, (stream,), level_=logging.INFO | ||
| ): | ||
| with helpers.enableLogging(logger, logging.StreamHandler, (stream,), level_=logging.INFO): | ||
| with temp_setter(logger, "propagate", False): | ||
@@ -259,0 +244,0 @@ helpers.make_session().collections.get("/") |
@@ -13,3 +13,2 @@ #! /usr/bin/env python | ||
| class TestException(unittest.TestCase): | ||
| def setUp(self): | ||
@@ -28,5 +27,3 @@ # open the session (per-test) | ||
| seed = helpers.my_function_name() + ":" + str(datetime.now()) | ||
| data = ( | ||
| helpers.home_collection(ses) + "/" + helpers.unique_name(seed, "data") | ||
| ) | ||
| data = helpers.home_collection(ses) + "/" + helpers.unique_name(seed, "data") | ||
| exc = None | ||
@@ -46,6 +43,4 @@ with helpers.create_simple_resc(self, vault_path="/home") as resc_name: | ||
| errno_repr = repr(errno_object) | ||
| self.assertRegexpMatches(errno_repr, r"\bErrno\b") | ||
| self.assertRegexpMatches( | ||
| errno_repr, """['"]{msg}['"]""".format(msg=os.strerror(errno.EACCES)) | ||
| ) | ||
| self.assertRegex(errno_repr, r"\bErrno\b") | ||
| self.assertRegex(errno_repr, """['"]{msg}['"]""".format(msg=os.strerror(errno.EACCES))) | ||
| self.assertIn(errno_repr, excep_repr) | ||
@@ -52,0 +47,0 @@ finally: |
@@ -11,3 +11,2 @@ #! /usr/bin/env python | ||
| class TestContinueQuery(unittest.TestCase): | ||
| @classmethod | ||
@@ -52,5 +51,3 @@ def setUpClass(cls): | ||
| # Query for all files in test collection | ||
| query = self.sess.query(DataObject.name, Collection.name).filter( | ||
| Collection.name == self.coll_path | ||
| ) | ||
| query = self.sess.query(DataObject.name, Collection.name).filter(Collection.name == self.coll_path) | ||
@@ -64,5 +61,3 @@ counter = 0 | ||
| # what we see | ||
| result_path = "{}/{}".format( | ||
| result[Collection.name], result[DataObject.name] | ||
| ) | ||
| result_path = "{}/{}".format(result[Collection.name], result[DataObject.name]) | ||
@@ -82,8 +77,3 @@ # compare | ||
| # Query should close after getting max_rows | ||
| results = ( | ||
| self.sess.query(DataObject.name, Collection.name) | ||
| .offset(offset) | ||
| .limit(max_rows) | ||
| .all() | ||
| ) | ||
| results = self.sess.query(DataObject.name, Collection.name).offset(offset).limit(max_rows).all() | ||
| self.assertEqual(len(results), max_rows) | ||
@@ -90,0 +80,0 @@ |
@@ -16,5 +16,3 @@ #! /usr/bin/env python | ||
| # Create test collection | ||
| self.coll_path = "/{}/home/{}/test_dir".format( | ||
| self.sess.zone, self.sess.username | ||
| ) | ||
| self.coll_path = "/{}/home/{}/test_dir".format(self.sess.zone, self.sess.username) | ||
| self.collection = self.sess.collections.create(self.coll_path) | ||
@@ -21,0 +19,0 @@ |
@@ -12,3 +12,2 @@ #! /usr/bin/env python | ||
| class TestForceCreate(unittest.TestCase): | ||
| def setUp(self): | ||
@@ -15,0 +14,0 @@ self.sess = helpers.make_session() |
@@ -9,3 +9,2 @@ import os | ||
| class TestGenQuery2(unittest.TestCase): | ||
| @classmethod | ||
@@ -17,12 +16,6 @@ def setUpClass(cls): | ||
| if cls.sess.server_version < (4, 3, 2): | ||
| raise unittest.SkipTest( | ||
| "GenQuery2 is not available by default in iRODS before v4.3.2." | ||
| ) | ||
| raise unittest.SkipTest("GenQuery2 is not available by default in iRODS before v4.3.2.") | ||
| cls.coll_path_a = "/{}/home/{}/test_query2_coll_a".format( | ||
| cls.sess.zone, cls.sess.username | ||
| ) | ||
| cls.coll_path_b = "/{}/home/{}/test_query2_coll_b".format( | ||
| cls.sess.zone, cls.sess.username | ||
| ) | ||
| cls.coll_path_a = "/{}/home/{}/test_query2_coll_a".format(cls.sess.zone, cls.sess.username) | ||
| cls.coll_path_b = "/{}/home/{}/test_query2_coll_b".format(cls.sess.zone, cls.sess.username) | ||
| cls.sess.collections.create(cls.coll_path_a) | ||
@@ -69,5 +62,3 @@ cls.sess.collections.create(cls.coll_path_b) | ||
| def test_select_or(self): | ||
| query = "SELECT COLL_NAME WHERE COLL_NAME = '{}' OR COLL_NAME = '{}'".format( | ||
| self.coll_path_a, self.coll_path_b | ||
| ) | ||
| query = "SELECT COLL_NAME WHERE COLL_NAME = '{}' OR COLL_NAME = '{}'".format(self.coll_path_a, self.coll_path_b) | ||
| q = self.sess.genquery2_object() | ||
@@ -82,6 +73,4 @@ query_result = q.execute(query) | ||
| def test_select_and(self): | ||
| query = ( | ||
| "SELECT COLL_NAME WHERE COLL_NAME LIKE '{}' AND COLL_NAME LIKE '{}'".format( | ||
| "%test_query2_coll%", "%query2_coll_a%" | ||
| ) | ||
| query = "SELECT COLL_NAME WHERE COLL_NAME LIKE '{}' AND COLL_NAME LIKE '{}'".format( | ||
| "%test_query2_coll%", "%query2_coll_a%" | ||
| ) | ||
@@ -88,0 +77,0 @@ q = self.sess.genquery2_object() |
+19
-43
| import base64 | ||
| import contextlib | ||
| import io | ||
| import datetime | ||
| import hashlib | ||
| import inspect | ||
| import io | ||
| import json | ||
@@ -11,6 +11,6 @@ import logging | ||
| import os | ||
| import random | ||
| import re | ||
| import shutil | ||
| import socket | ||
| import random | ||
| import re | ||
| import sys | ||
@@ -21,8 +21,9 @@ import tempfile | ||
| import irods.client_configuration as config | ||
| import irods.rule | ||
| from irods.helpers import ( | ||
| home_collection, | ||
| make_session as _irods_helpers_make_session) | ||
| from irods.message import iRODSMessage, IRODS_VERSION | ||
| make_session as _irods_helpers_make_session, | ||
| ) | ||
| from irods.message import iRODSMessage | ||
| from irods.password_obfuscation import encode | ||
| import irods.rule | ||
| from irods.session import iRODSSession | ||
@@ -53,5 +54,3 @@ | ||
| def create_user( | ||
| self, username, password=None, usertype="rodsuser", auto_remove=True | ||
| ): | ||
| def create_user(self, username, password=None, usertype="rodsuser", auto_remove=True): | ||
| u = self.admin.users.create(username, usertype) | ||
@@ -143,5 +142,3 @@ if password is not None: | ||
| if vault_path: | ||
| session.resources.create( | ||
| IRODS_REG_RESC, "unixfilesystem", session.host, vault_path | ||
| ) | ||
| session.resources.create(IRODS_REG_RESC, "unixfilesystem", session.host, vault_path) | ||
| Reg_Resc_Name = IRODS_REG_RESC | ||
@@ -160,5 +157,3 @@ return Reg_Resc_Name | ||
| with open(config, "w") as f1: | ||
| json.dump( | ||
| {recast(k): v for k, v in params.items() if k != "password"}, f1, indent=4 | ||
| ) | ||
| json.dump({recast(k): v for k, v in params.items() if k != "password"}, f1, indent=4) | ||
| auth = os.path.join(dir_, ".irodsA") | ||
@@ -176,5 +171,3 @@ with open(auth, "w") as f2: | ||
| if _irods_helpers_make_session.__doc__ is not None: | ||
| make_session.__doc__ = re.sub( | ||
| r"(test_server_version\s*)=\s*\w+", r"\1 = True", _irods_helpers_make_session.__doc__ | ||
| ) | ||
| make_session.__doc__ = re.sub(r"(test_server_version\s*)=\s*\w+", r"\1 = True", _irods_helpers_make_session.__doc__) | ||
@@ -224,5 +217,3 @@ | ||
| def make_deep_collection( | ||
| session, root_path, depth=10, objects_per_level=50, object_content=None | ||
| ): | ||
| def make_deep_collection(session, root_path, depth=10, objects_per_level=50, object_content=None): | ||
| # start at root path | ||
@@ -234,12 +225,7 @@ current_coll_path = root_path | ||
| # make list of object names | ||
| obj_names = [ | ||
| "obj" + str(i).zfill(len(str(objects_per_level))) | ||
| for i in range(objects_per_level) | ||
| ] | ||
| obj_names = ["obj" + str(i).zfill(len(str(objects_per_level))) for i in range(objects_per_level)] | ||
| # make subcollection and objects | ||
| if d == 0: | ||
| root_coll = make_collection( | ||
| session, current_coll_path, obj_names, object_content | ||
| ) | ||
| root_coll = make_collection(session, current_coll_path, obj_names, object_content) | ||
| else: | ||
@@ -249,5 +235,3 @@ make_collection(session, current_coll_path, obj_names, object_content) | ||
| # next level down | ||
| current_coll_path = os.path.join( | ||
| current_coll_path, "subcoll" + str(d).zfill(len(str(d))) | ||
| ) | ||
| current_coll_path = os.path.join(current_coll_path, "subcoll" + str(d).zfill(len(str(d)))) | ||
@@ -276,5 +260,3 @@ return root_coll | ||
| if not rescName: | ||
| rescName = "simple_resc_" + unique_name( | ||
| my_function_name() + "_simple_resc", datetime.datetime.now() | ||
| ) | ||
| rescName = "simple_resc_" + unique_name(my_function_name() + "_simple_resc", datetime.datetime.now()) | ||
| created = False | ||
@@ -329,5 +311,3 @@ try: | ||
| message_body = GeneralAdminRequest("rm", "unusedAVUs", "", "", "", "") | ||
| req = iRODSMessage( | ||
| "RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"] | ||
| ) | ||
| req = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"]) | ||
| with session.pool.get_connection() as conn: | ||
@@ -344,9 +324,5 @@ conn.send(req) | ||
| if _basename is None and require_that_file_exists: | ||
| err = RuntimeError( | ||
| "Attempted to back up a file which doesn't exist: %r" % (filename,) | ||
| ) | ||
| err = RuntimeError("Attempted to back up a file which doesn't exist: %r" % (filename,)) | ||
| raise err | ||
| with tempfile.NamedTemporaryFile( | ||
| prefix=("tmp" if not _basename else _basename) | ||
| ) as f: | ||
| with tempfile.NamedTemporaryFile(prefix=("tmp" if not _basename else _basename)) as f: | ||
| try: | ||
@@ -353,0 +329,0 @@ if _basename is not None: |
@@ -11,3 +11,2 @@ #! /usr/bin/env python | ||
| class TestLibraryFeatures(unittest.TestCase): | ||
| def setUp(self): | ||
@@ -14,0 +13,0 @@ self.sess = helpers.make_session() |
@@ -26,3 +26,3 @@ #! /usr/bin/env python | ||
| import gc | ||
| from irods.test.setupssl import create_ssl_dir | ||
| from irods.test.setup_ssl import create_ssl_dir | ||
@@ -119,5 +119,3 @@ # | ||
| old_init(self, *arg, **kw) | ||
| self.set_auth_option_for_scheme( | ||
| "pam_password", ENSURE_SSL_IS_ACTIVE, not (allow) | ||
| ) | ||
| self.set_auth_option_for_scheme("pam_password", ENSURE_SSL_IS_ACTIVE, not (allow)) | ||
@@ -165,3 +163,3 @@ with irods.helpers.temporarily_assign_attribute(iRODSSession, "__init__", new_init): | ||
| 2. ./setupssl.py (sets up SSL keys etc. in /etc/irods/ssl) should be run | ||
| 2. ./setup_ssl.py (sets up SSL keys etc. in /etc/irods/ssl) should be run | ||
| first to create (or overwrite, if appropriate) the /etc/irods/ssl directory | ||
@@ -184,3 +182,3 @@ and its contents. | ||
| env_save: Dict[str,Optional[str]] = {} | ||
| env_save: Dict[str, Optional[str]] = {} | ||
@@ -214,2 +212,7 @@ @contextlib.contextmanager | ||
| port=1247, | ||
| **( | ||
| {**SERVER_ENV_SSL_SETTINGS, **CLIENT_OPTIONS_FOR_SSL} | ||
| if self.admin.server_version >= (5,) | ||
| else {} | ||
| ), | ||
| ) | ||
@@ -228,5 +231,3 @@ try: | ||
| cl_env = client_env_keys_from_admin_env(TEST_RODS_USER) | ||
| if ( | ||
| lookup.get("AUTH", None) is not None | ||
| ): # - specify auth scheme only if given | ||
| if lookup.get("AUTH", None) is not None: # - specify auth scheme only if given | ||
| cl_env["irods_authentication_scheme"] = lookup["AUTH"] | ||
@@ -258,5 +259,3 @@ dirbase = os.path.join(os.environ["HOME"], dirname) | ||
| if cls.admin.server_version >= (4, 3) and not cfg.legacy_auth.force_legacy_auth: | ||
| cls.PAM_SCHEME_STRING = cls.user_auth_envs[".irods.pam"]["AUTH"] = ( | ||
| "pam_password" | ||
| ) | ||
| cls.PAM_SCHEME_STRING = cls.user_auth_envs[".irods.pam"]["AUTH"] = "pam_password" | ||
@@ -288,5 +287,3 @@ @classmethod | ||
| my_connect = [s for s in (session.pool.active | session.pool.idle)][0] | ||
| self.assertEqual( | ||
| bool(use_ssl), my_connect.socket.__class__ is ssl.SSLSocket | ||
| ) | ||
| self.assertEqual(bool(use_ssl), my_connect.socket.__class__ is ssl.SSLSocket) | ||
@@ -303,11 +300,8 @@ @contextlib.contextmanager | ||
| def tst0( | ||
| self, ssl_opt, auth_opt, env_opt, name=TEST_RODS_USER, make_irods_pw=False | ||
| ): | ||
| def tst0(self, ssl_opt, auth_opt, env_opt, name=TEST_RODS_USER, make_irods_pw=False): | ||
| session = None | ||
| _auth_opt = auth_opt | ||
| if auth_opt in ("pam", "pam_password"): | ||
| auth_opt = self.PAM_SCHEME_STRING | ||
| with self._setup_rodsuser_and_optional_pw( | ||
| name=name, make_irods_pw=make_irods_pw | ||
| ): | ||
| with self._setup_rodsuser_and_optional_pw(name=name, make_irods_pw=make_irods_pw): | ||
| self.envdirs = self.create_env_dirs() | ||
@@ -321,10 +315,7 @@ if not self.envdirs: | ||
| if env_opt: | ||
| with self.setenv( | ||
| "IRODS_ENVIRONMENT_FILE", json_env_fullpath(auth_opt_explicit) | ||
| ) as env_file, self.setenv( | ||
| "IRODS_AUTHENTICATION_FILE", secrets_fullpath(auth_opt_explicit) | ||
| with ( | ||
| self.setenv("IRODS_ENVIRONMENT_FILE", json_env_fullpath(auth_opt_explicit)) as env_file, | ||
| self.setenv("IRODS_AUTHENTICATION_FILE", secrets_fullpath(auth_opt_explicit)), | ||
| ): | ||
| cli_env_extras = ( | ||
| {} if not (ssl_opt) else dict(CLIENT_OPTIONS_FOR_SSL) | ||
| ) | ||
| cli_env_extras = {} if not (ssl_opt) else dict(CLIENT_OPTIONS_FOR_SSL) | ||
| if auth_opt: | ||
@@ -336,5 +327,3 @@ cli_env_extras.update(irods_authentication_scheme=auth_opt) | ||
| with helpers.file_backed_up(env_file): | ||
| json_file_update( | ||
| env_file, keys_to_delete=remove, **cli_env_extras | ||
| ) | ||
| json_file_update(env_file, keys_to_delete=remove, **cli_env_extras) | ||
| with pam_password_in_plaintext(nop=ssl_opt): | ||
@@ -344,5 +333,3 @@ session = iRODSSession(irods_env_file=env_file) | ||
| out = json.load(f) | ||
| self.validate_session( | ||
| session, verbose=verbosity, ssl=ssl_opt | ||
| ) | ||
| self.validate_session(session, verbose=verbosity, ssl=ssl_opt) | ||
| session.cleanup() | ||
@@ -363,7 +350,5 @@ out["ARGS"] = "no" | ||
| ), | ||
| **CLIENT_OPTIONS_FOR_SSL | ||
| **CLIENT_OPTIONS_FOR_SSL, | ||
| ) | ||
| lookup = self.user_auth_envs[ | ||
| ".irods." + ("native" if not (_auth_opt) else _auth_opt) | ||
| ] | ||
| lookup = self.user_auth_envs[".irods." + ("native" if not (_auth_opt) else _auth_opt)] | ||
| with pam_password_in_plaintext(nop=ssl_opt): | ||
@@ -376,3 +361,3 @@ session = iRODSSession( | ||
| port=1247, | ||
| **session_options | ||
| **session_options, | ||
| ) | ||
@@ -388,9 +373,9 @@ out = session_options | ||
| "--- > ", | ||
| json.dumps( | ||
| {k: v for k, v in out.items() if k != "ssl_context"}, indent=4 | ||
| ), | ||
| json.dumps({k: v for k, v in out.items() if k != "ssl_context"}, indent=4), | ||
| ) | ||
| print("---") | ||
| return session | ||
| if session: | ||
| session.cleanup() | ||
| return session | ||
@@ -431,4 +416,6 @@ # == test defaulting to 'native' | ||
| def test_6(self): | ||
| if self.admin.server_version >= (5,): | ||
| self.skipTest("iRODS 5 does not permit sending the raw PAM password on an unencrypted connection.") | ||
| try: | ||
| ses = self.tst0(ssl_opt=False, auth_opt="pam", env_opt=False) | ||
| session = self.tst0(ssl_opt=False, auth_opt="pam", env_opt=False) | ||
| except PlainTextPAMPasswordError: | ||
@@ -439,3 +426,3 @@ pass | ||
| # but for 4.2 and previous, we expect the PlainTextPAMPasswordError to be raised. | ||
| if ses.server_version_without_auth() < (4, 3): | ||
| if session.server_version_without_auth() < (4, 3): | ||
| self.fail("PlainTextPAMPasswordError should have been raised") | ||
@@ -478,3 +465,3 @@ | ||
| authentication_scheme="pam", | ||
| **ssl_settings | ||
| **ssl_settings, | ||
| ) | ||
@@ -486,3 +473,2 @@ home_coll = "/{0.zone}/home/{0.username}".format(irods_session) | ||
| class TestAnonymousUser(unittest.TestCase): | ||
| def setUp(self): | ||
@@ -531,3 +517,2 @@ admin = self.admin = helpers.make_session() | ||
| class TestMiscellaneous(unittest.TestCase): | ||
| def test_nonanonymous_login_without_auth_file_fails__290(self): | ||
@@ -557,3 +542,3 @@ ses = self.admin | ||
| # -- Check that we raise an appropriate exception pointing to the missing auth file path -- | ||
| with self.assertRaisesRegexp(NonAnonymousLoginWithoutPassword, bob_auth): | ||
| with self.assertRaisesRegex(NonAnonymousLoginWithoutPassword, bob_auth): | ||
| with helpers.make_session(**login_options) as s: | ||
@@ -617,5 +602,3 @@ s.users.get("bob") | ||
| admin = self.admin | ||
| with iRODSSession( | ||
| zone=admin.zone, port=admin.port, host=admin.host, user="alice" | ||
| ) as alice: | ||
| with iRODSSession(zone=admin.zone, port=admin.port, host=admin.host, user="alice") as alice: | ||
| alice.collections.get(helpers.home_collection(alice)) | ||
@@ -634,5 +617,3 @@ | ||
| if not os.path.exists("/etc/irods/ssl"): | ||
| self.skipTest( | ||
| "Running setupssl.py as irods user is prerequisite for this test." | ||
| ) | ||
| self.skipTest("Running setup_ssl.py as irods user is prerequisite for this test.") | ||
| with helpers.make_session() as session: | ||
@@ -651,5 +632,3 @@ if not session.host in ("localhost", socket.gethostname()): | ||
| # Elect for efficiency in DH param generation, eg. when setting up for testing. | ||
| create_ssl_dir( | ||
| ssl_dir=my_ssl_directory, use_strong_primes_for_dh_generation=False | ||
| ) | ||
| create_ssl_dir(ssl_dir=my_ssl_directory, use_strong_primes_for_dh_generation=False) | ||
| settings_to_update = { | ||
@@ -665,5 +644,3 @@ key: value.replace("/etc/irods/ssl", my_ssl_directory) | ||
| with helpers.make_session() as session: | ||
| session.collections.get( | ||
| "/{session.zone}/home/{session.username}".format(**locals()) | ||
| ) | ||
| session.collections.get("/{session.zone}/home/{session.username}".format(**locals())) | ||
| finally: | ||
@@ -670,0 +647,0 @@ if my_ssl_directory: |
@@ -28,3 +28,2 @@ #!/usr/bin/env python | ||
| class TestMessages(unittest.TestCase): | ||
| def test_startup_pack(self): | ||
@@ -216,3 +215,4 @@ sup = StartupPack(("rods", "tempZone"), ("rods", "tempZone")) | ||
| if __name__ == "__main__": | ||
| unittest.main() |
+137
-136
| #! /usr/bin/env python | ||
| # -*- coding: utf-8 -*- | ||
| import datetime | ||
| import os | ||
@@ -8,19 +9,20 @@ import re | ||
| import time | ||
| import datetime | ||
| import unittest | ||
| import irods.exception as ex | ||
| import irods.keywords as kw | ||
| from irods.column import Like, NotLike | ||
| from irods.manager.metadata_manager import InvalidAtomicAVURequest | ||
| from irods.message import Bad_AVU_Field | ||
| from irods.meta import ( | ||
| iRODSMeta, | ||
| AVUOperation, | ||
| BadAVUOperationKeyword, | ||
| BadAVUOperationValue, | ||
| BadAVUOperationKeyword, | ||
| iRODSBinOrStringMeta, | ||
| iRODSMeta, | ||
| ) | ||
| from irods.models import DataObject, Collection, Resource, CollectionMeta | ||
| import irods.test.helpers as helpers | ||
| import irods.keywords as kw | ||
| from irods.models import Collection, CollectionMeta, DataObject, ModelBase, Resource | ||
| from irods.path import iRODSPath | ||
| from irods.session import iRODSSession | ||
| from irods.message import Bad_AVU_Field | ||
| from irods.models import ModelBase | ||
| from irods.column import Like, NotLike | ||
| from irods.test import helpers | ||
@@ -68,3 +70,2 @@ | ||
| for v in (value, value_encoded): | ||
| # Establish invariant of exactly 2 AVUs attached to object. | ||
@@ -74,5 +75,3 @@ self.coll.metadata.remove_all() | ||
| self.coll.metadata["b"] = iRODSMeta("b", "<arbitrary>") | ||
| q = self.sess.query(CollectionMeta).filter( | ||
| Collection.name == self.coll_path | ||
| ) | ||
| q = self.sess.query(CollectionMeta).filter(Collection.name == self.coll_path) | ||
| self.assertEqual(len(list(q)), 2) | ||
@@ -93,9 +92,5 @@ | ||
| # Test query with operators == and != | ||
| q = self.sess.query(CollectionMeta).filter( | ||
| Collection.name == self.coll_path, CollectionMeta.value == v | ||
| ) | ||
| q = self.sess.query(CollectionMeta).filter(Collection.name == self.coll_path, CollectionMeta.value == v) | ||
| self.assertEqual(len(list(q)), 1) | ||
| q = self.sess.query(CollectionMeta).filter( | ||
| Collection.name == self.coll_path, CollectionMeta.value != v | ||
| ) | ||
| q = self.sess.query(CollectionMeta).filter(Collection.name == self.coll_path, CollectionMeta.value != v) | ||
| self.assertEqual(len(list(q)), 1) | ||
@@ -124,5 +119,3 @@ metadata.append(self.coll.metadata["a"]) | ||
| # test data | ||
| self.coll_path = "/{}/home/{}/test_dir".format( | ||
| self.sess.zone, self.sess.username | ||
| ) | ||
| self.coll_path = "/{}/home/{}/test_dir".format(self.sess.zone, self.sess.username) | ||
| self.obj_name = "test1" | ||
@@ -162,5 +155,3 @@ self.obj_path = "{coll_path}/{obj_name}".format(**vars(self)) | ||
| try: | ||
| obj.metadata.apply_atomic_operations( | ||
| AVUOperation(operation="add", avu=iRODSMeta("a", "b", "c")) | ||
| ) | ||
| obj.metadata.apply_atomic_operations(AVUOperation(operation="add", avu=iRODSMeta("a", "b", "c"))) | ||
| except ex.iRODSException as e: | ||
@@ -173,4 +164,6 @@ resp = e.server_msg.get_json_encoded_struct() | ||
| except Exception as e: | ||
| fail_message = "apply_atomic_operations on a nonexistent object raised an unexpected exception {e!r}".format( | ||
| **locals() | ||
| fail_message = ( | ||
| "apply_atomic_operations on a nonexistent object raised an unexpected exception {e!r}".format( | ||
| **locals() | ||
| ) | ||
| ) | ||
@@ -203,12 +196,5 @@ else: | ||
| for n, obj in enumerate((group, user, self.coll, self.obj)): | ||
| avus = [ | ||
| iRODSMeta("some_attribute", str(i), "some_units") | ||
| for i in range(n * 100, (n + 1) * 100) | ||
| ] | ||
| obj.metadata.apply_atomic_operations( | ||
| *[AVUOperation(operation="add", avu=avu_) for avu_ in avus] | ||
| ) | ||
| obj.metadata.apply_atomic_operations( | ||
| *[AVUOperation(operation="remove", avu=avu_) for avu_ in avus] | ||
| ) | ||
| avus = [iRODSMeta("some_attribute", str(i), "some_units") for i in range(n * 100, (n + 1) * 100)] | ||
| obj.metadata.apply_atomic_operations(*[AVUOperation(operation="add", avu=avu_) for avu_ in avus]) | ||
| obj.metadata.apply_atomic_operations(*[AVUOperation(operation="remove", avu=avu_) for avu_ in avus]) | ||
@@ -230,8 +216,4 @@ def test_atomic_metadata_operation_for_resource_244(self): | ||
| resc_meta = self.sess.metadata.get(Resource, resc.name) | ||
| avus_to_tuples = lambda avu_list: sorted( | ||
| [(i.name, i.value, i.units) for i in avu_list] | ||
| ) | ||
| self.assertEqual( | ||
| avus_to_tuples(resc_meta), avus_to_tuples([iRODSMeta(*resc_tuple)]) | ||
| ) | ||
| avus_to_tuples = lambda avu_list: sorted([(i.name, i.value, i.units) for i in avu_list]) | ||
| self.assertEqual(avus_to_tuples(resc_meta), avus_to_tuples([iRODSMeta(*resc_tuple)])) | ||
@@ -254,5 +236,3 @@ def test_atomic_metadata_operation_for_data_object_244(self): | ||
| ) | ||
| meta = self.sess.metadata.get( | ||
| DataObject, self.obj_path | ||
| ) # ... check integrity of change | ||
| meta = self.sess.metadata.get(DataObject, self.obj_path) # ... check integrity of change | ||
| self.assertEqual(sorted([AVU_Units_String(i) for i in meta]), ["", "units_244"]) | ||
@@ -274,5 +254,3 @@ | ||
| for n, item in enumerate(avus): | ||
| obj.metadata.apply_atomic_operations( | ||
| AVUOperation(operation="add", avu=item) | ||
| ) | ||
| obj.metadata.apply_atomic_operations(AVUOperation(operation="add", avu=item)) | ||
| self.assertEqual(len(obj.metadata.items()), n + 1) | ||
@@ -294,5 +272,3 @@ finally: | ||
| self.sess.resources.get(rescname).metadata.remove_all() | ||
| self.sess.metadata.set( | ||
| Resource, rescname, iRODSMeta("zero", "marginal", "cost") | ||
| ) | ||
| self.sess.metadata.set(Resource, rescname, iRODSMeta("zero", "marginal", "cost")) | ||
| self.sess.metadata.add(Resource, rescname, iRODSMeta("zero", "marginal")) | ||
@@ -310,8 +286,4 @@ self.sess.metadata.set(Resource, rescname, iRODSMeta("for", "ever", "after")) | ||
| # add metadata to test object | ||
| self.sess.metadata.add( | ||
| DataObject, self.obj_path, iRODSMeta(self.attr0, self.value0) | ||
| ) | ||
| self.sess.metadata.add( | ||
| DataObject, self.obj_path, iRODSMeta(self.attr1, self.value1, self.unit1) | ||
| ) | ||
| self.sess.metadata.add(DataObject, self.obj_path, iRODSMeta(self.attr0, self.value0)) | ||
| self.sess.metadata.add(DataObject, self.obj_path, iRODSMeta(self.attr1, self.value1, self.unit1)) | ||
@@ -347,5 +319,3 @@ # Throw in some unicode for good measure | ||
| with self.assertRaises(ValueError): | ||
| self.sess.metadata.add( | ||
| DataObject, self.obj_path, iRODSMeta("attr_with_empty_value", "") | ||
| ) | ||
| self.sess.metadata.add(DataObject, self.obj_path, iRODSMeta("attr_with_empty_value", "")) | ||
@@ -358,5 +328,3 @@ def test_copy_obj_meta(self): | ||
| # add metadata to test object | ||
| self.sess.metadata.add( | ||
| DataObject, self.obj_path, iRODSMeta(self.attr0, self.value0) | ||
| ) | ||
| self.sess.metadata.add(DataObject, self.obj_path, iRODSMeta(self.attr0, self.value0)) | ||
@@ -374,5 +342,3 @@ # copy metadata | ||
| # add metadata to test object | ||
| self.sess.metadata.add( | ||
| DataObject, self.obj_path, iRODSMeta(self.attr0, self.value0) | ||
| ) | ||
| self.sess.metadata.add(DataObject, self.obj_path, iRODSMeta(self.attr0, self.value0)) | ||
@@ -384,5 +350,3 @@ # check that metadata is there | ||
| # remove metadata from object | ||
| self.sess.metadata.remove( | ||
| DataObject, self.obj_path, iRODSMeta(self.attr0, self.value0) | ||
| ) | ||
| self.sess.metadata.remove(DataObject, self.obj_path, iRODSMeta(self.attr0, self.value0)) | ||
@@ -399,5 +363,3 @@ # check that metadata is gone | ||
| if adm.server_version <= (4, 2, 11): | ||
| self.skipTest( | ||
| "ADMIN_KW not valid for Metadata API in iRODS 4.2.11 and previous" | ||
| ) | ||
| self.skipTest("ADMIN_KW not valid for Metadata API in iRODS 4.2.11 and previous") | ||
@@ -416,5 +378,3 @@ # Create a rodsuser, and a session for that roduser. | ||
| # has the desired effect. | ||
| d = ses.data_objects.create( | ||
| "/{adm.zone}/home/{user.name}/testfile".format(**locals()) | ||
| ) | ||
| d = ses.data_objects.create("/{adm.zone}/home/{user.name}/testfile".format(**locals())) | ||
@@ -452,5 +412,3 @@ d.metadata.set("a", "aa", "1") | ||
| if adm.server_version <= (4, 2, 11): | ||
| self.skipTest( | ||
| "ADMIN_KW not valid for Metadata API in iRODS 4.2.11 and previous" | ||
| ) | ||
| self.skipTest("ADMIN_KW not valid for Metadata API in iRODS 4.2.11 and previous") | ||
@@ -476,5 +434,3 @@ try: | ||
| avu_item = iRODSMeta("issue_576", "dummy_value") | ||
| data_via_admin.metadata(admin=True).apply_atomic_operations( | ||
| AVUOperation(operation="add", avu=avu_item) | ||
| ) | ||
| data_via_admin.metadata(admin=True).apply_atomic_operations(AVUOperation(operation="add", avu=avu_item)) | ||
| self.assertIn(avu_item, data_via_admin.metadata.items()) | ||
@@ -492,5 +448,3 @@ finally: | ||
| # add metadata to test collection | ||
| self.sess.metadata.add( | ||
| Collection, self.coll_path, iRODSMeta(self.attr0, self.value0) | ||
| ) | ||
| self.sess.metadata.add(Collection, self.coll_path, iRODSMeta(self.attr0, self.value0)) | ||
@@ -505,5 +459,3 @@ # get collection metadata | ||
| # remove collection metadata | ||
| self.sess.metadata.remove( | ||
| Collection, self.coll_path, iRODSMeta(self.attr0, self.value0) | ||
| ) | ||
| self.sess.metadata.remove(Collection, self.coll_path, iRODSMeta(self.attr0, self.value0)) | ||
@@ -527,5 +479,3 @@ # check that metadata is gone | ||
| # add metadata to test object | ||
| meta = self.sess.metadata.add( | ||
| DataObject, test_obj_path, iRODSMeta(attribute, value, units) | ||
| ) | ||
| meta = self.sess.metadata.add(DataObject, test_obj_path, iRODSMeta(attribute, value, units)) | ||
@@ -557,5 +507,3 @@ # get metadata | ||
| # test AVUs | ||
| triplets = [ | ||
| ("test_attr" + str(i), "test_value", "test_units") for i in range(avu_count) | ||
| ] | ||
| triplets = [("test_attr" + str(i), "test_value", "test_units") for i in range(avu_count)] | ||
@@ -696,5 +644,3 @@ # get coll meta | ||
| data = self.sess.data_objects.create( | ||
| "{hc}/{index}_{edge_case_arg}_{method}_AZ__issue_547".format( | ||
| **locals() | ||
| ) | ||
| "{hc}/{index}_{edge_case_arg}_{method}_AZ__issue_547".format(**locals()) | ||
| ) | ||
@@ -712,5 +658,5 @@ to_delete.append(data) | ||
| args = ("an_attribute", 0) | ||
| with self.assertRaisesRegexp(Bad_AVU_Field, "incorrect type"): | ||
| with self.assertRaisesRegex(Bad_AVU_Field, "incorrect type"): | ||
| self.coll.metadata.set(*args) | ||
| with self.assertRaisesRegexp(Bad_AVU_Field, "incorrect type"): | ||
| with self.assertRaisesRegex(Bad_AVU_Field, "incorrect type"): | ||
| self.coll.metadata.add(*args) | ||
@@ -720,10 +666,8 @@ | ||
| args = ("an_attribute", "") | ||
| with self.assertRaisesRegexp(Bad_AVU_Field, "zero-length"): | ||
| with self.assertRaisesRegex(Bad_AVU_Field, "zero-length"): | ||
| self.coll.metadata.set(*args) | ||
| with self.assertRaisesRegexp(Bad_AVU_Field, "zero-length"): | ||
| with self.assertRaisesRegex(Bad_AVU_Field, "zero-length"): | ||
| self.coll.metadata.add(*args) | ||
| @unittest.skipUnless( | ||
| os.path.isfile(RODS_GENQUERY_INCLUDE_FILE_PATH), "need package irods-dev(el)" | ||
| ) | ||
| @unittest.skipUnless(os.path.isfile(RODS_GENQUERY_INCLUDE_FILE_PATH), "need package irods-dev(el)") | ||
| def test_that_all_column_mappings_are_uniquely_and_properly_defined__issue_643( | ||
@@ -741,22 +685,16 @@ self, | ||
| server_column_defs = sorted( | ||
| [ | ||
| (match.group("column_name"), int(match.group("column_value"))) | ||
| for match in ( | ||
| column_definitions_regex.match(line) for line in include_lines | ||
| ) | ||
| if match | ||
| ] | ||
| ) | ||
| server_column_defs = sorted([ | ||
| (match.group("column_name"), int(match.group("column_value"))) | ||
| for match in (column_definitions_regex.match(line) for line in include_lines) | ||
| if match | ||
| ]) | ||
| # Extract all GenQuery1 name-to-number mappings from PRC model class definitions (some omit 'COL_' prefix, so allow some flexibility there.) | ||
| prepend_col_prefix_if_needed = lambda s: ( | ||
| "COL_" + s if not s.startswith("COL_") else s | ||
| ) | ||
| prc_column_defs = sorted( | ||
| [ | ||
| (prepend_col_prefix_if_needed(i[1].icat_key), i[1].icat_id) | ||
| for i in ModelBase.column_items | ||
| ] | ||
| ) | ||
| prepend_col_prefix_if_needed = lambda s: "COL_" + s if not s.startswith("COL_") else s | ||
| current_server_version = self.sess.server_version | ||
| prc_column_defs = sorted([ | ||
| (prepend_col_prefix_if_needed(i[1].icat_key), i[1].icat_id) | ||
| for i in ModelBase.column_items | ||
| if current_server_version >= i[1].min_version | ||
| ]) | ||
@@ -774,5 +712,5 @@ sr = set(a for a, b in set(prc_column_defs) - set(server_column_defs)) | ||
| meta_units = str(time.time()) | ||
| execute_my_query = lambda: self.sess.query( | ||
| CollectionMeta, Collection.name | ||
| ).filter(Collection.name == test_coll.path, CollectionMeta.name == meta_key) | ||
| execute_my_query = lambda: self.sess.query(CollectionMeta, Collection.name).filter( | ||
| Collection.name == test_coll.path, CollectionMeta.name == meta_key | ||
| ) | ||
| # Make sure no iRODSMeta exists under the test key. | ||
@@ -811,14 +749,12 @@ del test_coll.metadata[meta_key] | ||
| 1, | ||
| len( | ||
| [ | ||
| _ | ||
| for _ in ( | ||
| session.query(CollectionMeta).filter( | ||
| Collection.name == hc, | ||
| CollectionMeta.name == attr_str, | ||
| CollectionMeta.value == string_value, | ||
| ) | ||
| len([ | ||
| _ | ||
| for _ in ( | ||
| session.query(CollectionMeta).filter( | ||
| Collection.name == hc, | ||
| CollectionMeta.name == attr_str, | ||
| CollectionMeta.value == string_value, | ||
| ) | ||
| ] | ||
| ), | ||
| ) | ||
| ]), | ||
| ) | ||
@@ -831,3 +767,68 @@ finally: | ||
| def test_binary_avu_fields__issue_707(self): | ||
| meta_coll = self.obj.metadata(iRODSMeta_type=iRODSBinOrStringMeta) | ||
| illegal_unicode_sequence = '\u1000'.encode()[:2] | ||
| avu_name = 'issue709' | ||
| meta_coll.set( | ||
| avu_name, (value := b'value_' + illegal_unicode_sequence), (units := b'units_' + illegal_unicode_sequence) | ||
| ) | ||
| self.assertEqual(meta_coll.get_one(avu_name), (avu_name, value, units)) | ||
| meta_coll.add(*(new_avu := iRODSMeta(avu_name, '\u1000', '\u1001'))) | ||
| relevant_avus = meta_coll.get_all(avu_name) | ||
| self.assertIn(new_avu, relevant_avus) | ||
| def test_cascading_changes_of_metadata_manager_options__issue_709(self): | ||
| d = None | ||
| try: | ||
| d = self.sess.data_objects.create(f'{self.coll.path}/issue_709_test_1') | ||
| m = d.metadata | ||
| self.assertEqual(m.admin, False) | ||
| m2 = m(admin=True) | ||
| self.assertEqual(m2.timestamps, False) | ||
| self.assertEqual(m2.admin, True) | ||
| m3 = m2(timestamps=True) | ||
| self.assertEqual(m3.timestamps, True) | ||
| self.assertEqual(m3.admin, True) | ||
| self.assertEqual(m3._manager.get_api_keywords().get(kw.ADMIN_KW), "") | ||
| m4 = m3(admin=False) | ||
| self.assertEqual(m4.admin, False) | ||
| self.assertEqual(m4._manager.get_api_keywords().get(kw.ADMIN_KW), None) | ||
| finally: | ||
| if d: | ||
| d.unlink(force=True) | ||
| def test_reload_can_be_deactivated__issue_768(self): | ||
| # Set an initial AVU | ||
| metacoll = self.obj.metadata | ||
| metacoll.set(item_1 := iRODSMeta('aa', 'bb', 'cc')) | ||
| # Initial defaults will always reload the AVU list from the server, so new AVU should be seen. | ||
| self.assertIn(item_1, metacoll.items()) | ||
| # Setting reload option to False will prevent reload of object AVUs, so an AVU just set should not be seen. | ||
| metacoll_2 = metacoll(reload=False) | ||
| metacoll_2.set(item_2 := iRODSMeta('xx', 'yy', 'zz')) | ||
| items = metacoll_2.items() | ||
| self.assertIn(item_1, items) | ||
| self.assertNotIn(item_2, items) | ||
| # Restore old setting. Check that both AVUs are seen as present. | ||
| items_reloaded = metacoll_2(reload=True).items() | ||
| self.assertIn(item_1, items_reloaded) | ||
| self.assertIn(item_2, items_reloaded) | ||
| def test_prevention_of_attribute_creation__issue_795(self): | ||
| data_path = iRODSPath(self.coll_path, helpers.unique_name(datetime.datetime.now())) # noqa: DTZ005 | ||
| data = self.sess.data_objects.create(data_path) | ||
| with self.assertRaises(AttributeError): | ||
| # This should cause an error since "admin" is considered as a read-only attribute; whereas | ||
| # data.metadata(admin = True) generates a cloned object but for the one change to "admin". | ||
| data.metadata.admin = True | ||
| if __name__ == "__main__": | ||
@@ -834,0 +835,0 @@ # let the tests find the parent irods lib |
@@ -45,7 +45,3 @@ # This helper module can double as a Python script, allowing us to run the below | ||
| # but by specifying a list/tuple of keys we can export only those specific locals by name. | ||
| return ( | ||
| L | ||
| if not isinstance(return_locals, (tuple, list)) | ||
| else [L[k] for k in return_locals] | ||
| ) | ||
| return L if not isinstance(return_locals, (tuple, list)) else [L[k] for k in return_locals] | ||
@@ -52,0 +48,0 @@ |
@@ -41,6 +41,4 @@ # Used in test of: | ||
| if __name__ == "__main__": | ||
| function_info = [ | ||
| [*get_stage_object_and_value_from_name(name), object_printer(name)] | ||
| for name in reversed(sys.argv[1:]) | ||
| [*get_stage_object_and_value_from_name(name), object_printer(name)] for name in reversed(sys.argv[1:]) | ||
| ] | ||
@@ -47,0 +45,0 @@ |
@@ -22,5 +22,3 @@ #! /usr/bin/env python | ||
| LOCALHOST_REGEX = re.compile( | ||
| r"""^(127(\.\d+){1,3}|[0:]+1|(.*-)?localhost(\.\w+)?)$""", re.IGNORECASE | ||
| ) | ||
| LOCALHOST_REGEX = re.compile(r"""^(127(\.\d+){1,3}|[0:]+1|(.*-)?localhost(\.\w+)?)$""", re.IGNORECASE) | ||
| USE_ONLY_LOCALHOST = False | ||
@@ -30,6 +28,5 @@ | ||
| class TestPool(unittest.TestCase): | ||
| config_extension = ".json" | ||
| test_extension = "" | ||
| preferred_parameters : Dict[str, Any] = {} | ||
| preferred_parameters: Dict[str, Any] = {} | ||
@@ -283,8 +280,4 @@ @classmethod | ||
| if self.sess.host != socket.gethostname() and not LOCALHOST_REGEX.match( | ||
| self.sess.host | ||
| ): | ||
| self.skipTest( | ||
| "local test only - client dot does not like the extra logging" | ||
| ) | ||
| if self.sess.host != socket.gethostname() and not LOCALHOST_REGEX.match(self.sess.host): | ||
| self.skipTest("local test only - client dot does not like the extra logging") | ||
@@ -356,5 +349,3 @@ # Set 'irods_connection_refresh_time' to '3' (in seconds) in | ||
| def test_get_connection_refresh_time_no_env_file_input_param(self): | ||
| connection_refresh_time = self.sess.get_connection_refresh_time( | ||
| first_name="Magic", last_name="Johnson" | ||
| ) | ||
| connection_refresh_time = self.sess.get_connection_refresh_time(first_name="Magic", last_name="Johnson") | ||
| self.assertEqual(connection_refresh_time, -1) | ||
@@ -398,5 +389,3 @@ | ||
| ) | ||
| connection_refresh_time = self.sess.get_connection_refresh_time( | ||
| irods_env_file=default_path | ||
| ) | ||
| connection_refresh_time = self.sess.get_connection_refresh_time(irods_env_file=default_path) | ||
| self.assertEqual(connection_refresh_time, 3) | ||
@@ -403,0 +392,0 @@ |
+128
-178
@@ -47,3 +47,2 @@ #! /usr/bin/env python | ||
| class TestQuery(unittest.TestCase): | ||
| Iterate_to_exhaust_statement_table = range(IRODS_STATEMENT_TABLE_SIZE + 1) | ||
@@ -71,5 +70,3 @@ | ||
| print( | ||
| "Could not remove resc {!r} due to: {} ".format( | ||
| cls.register_resc, e | ||
| ), | ||
| "Could not remove resc {!r} due to: {} ".format(cls.register_resc, e), | ||
| file=sys.stderr, | ||
@@ -82,5 +79,3 @@ ) | ||
| # test data | ||
| self.coll_path = "/{}/home/{}/test_dir".format( | ||
| self.sess.zone, self.sess.username | ||
| ) | ||
| self.coll_path = "/{}/home/{}/test_dir".format(self.sess.zone, self.sess.username) | ||
| self.obj_name = "test1" | ||
@@ -90,8 +85,4 @@ self.case_sensitive_obj_name1 = "caseSENSITIVEobject" | ||
| self.obj_path = "{coll_path}/{obj_name}".format(**vars(self)) | ||
| self.case_sensitive_obj_path1 = "{coll_path}/{case_sensitive_obj_name1}".format( | ||
| **vars(self) | ||
| ) | ||
| self.case_sensitive_obj_path2 = "{coll_path}/{case_sensitive_obj_name2}".format( | ||
| **vars(self) | ||
| ) | ||
| self.case_sensitive_obj_path1 = "{coll_path}/{case_sensitive_obj_name1}".format(**vars(self)) | ||
| self.case_sensitive_obj_path2 = "{coll_path}/{case_sensitive_obj_name2}".format(**vars(self)) | ||
@@ -139,7 +130,3 @@ # Create test collection and (empty) test object | ||
| result1 = ( | ||
| self.sess.query(DataObject.name) | ||
| .filter(DataObject.name == self.case_sensitive_obj_name1) | ||
| .all() | ||
| ) | ||
| result1 = self.sess.query(DataObject.name).filter(DataObject.name == self.case_sensitive_obj_name1).all() | ||
| self.assertTrue(result1.has_value(self.case_sensitive_obj_name1)) | ||
@@ -149,5 +136,3 @@ self.assertEqual(len(result1), 1) | ||
| result2 = ( | ||
| self.sess.query(DataObject.name) | ||
| .filter(DataObject.name == str.lower(self.case_sensitive_obj_name1)) | ||
| .all() | ||
| self.sess.query(DataObject.name).filter(DataObject.name == str.lower(self.case_sensitive_obj_name1)).all() | ||
| ) | ||
@@ -157,5 +142,3 @@ self.assertEqual(len(result2), 0) | ||
| result3 = ( | ||
| self.sess.query(DataObject.name) | ||
| .filter(DataObject.name == str.upper(self.case_sensitive_obj_name1)) | ||
| .all() | ||
| self.sess.query(DataObject.name).filter(DataObject.name == str.upper(self.case_sensitive_obj_name1)).all() | ||
| ) | ||
@@ -168,26 +151,15 @@ self.assertEqual(len(result3), 0) | ||
| result4 = ( | ||
| self.sess.query(DataObject.name) | ||
| .filter(Like(DataObject.name, search_expression)) | ||
| .all() | ||
| ) | ||
| result4 = self.sess.query(DataObject.name).filter(Like(DataObject.name, search_expression)).all() | ||
| self.assertTrue(result4.has_value(self.case_sensitive_obj_name1)) | ||
| self.assertEqual(len(result4), 1) | ||
| result5 = ( | ||
| self.sess.query(DataObject.name) | ||
| .filter(Like(DataObject.name, str.lower(search_expression))) | ||
| .all() | ||
| ) | ||
| result5 = self.sess.query(DataObject.name).filter(Like(DataObject.name, str.lower(search_expression))).all() | ||
| self.assertEqual(len(result5), 0) | ||
| result6 = ( | ||
| self.sess.query(DataObject.name) | ||
| .filter(Like(DataObject.name, str.upper(search_expression))) | ||
| .all() | ||
| ) | ||
| result6 = self.sess.query(DataObject.name).filter(Like(DataObject.name, str.upper(search_expression))).all() | ||
| self.assertEqual(len(result6), 0) | ||
| result7 = ( | ||
| self.sess.query(DataObject.name) | ||
| self.sess | ||
| .query(DataObject.name) | ||
| .filter(Collection.name == self.coll_path) | ||
@@ -201,3 +173,4 @@ .filter(NotLike(DataObject.name, search_expression)) | ||
| result8 = ( | ||
| self.sess.query(DataObject.name) | ||
| self.sess | ||
| .query(DataObject.name) | ||
| .filter(Collection.name == self.coll_path) | ||
@@ -212,3 +185,4 @@ .filter(NotLike(DataObject.name, str.lower(search_expression))) | ||
| result9 = ( | ||
| self.sess.query(DataObject.name) | ||
| self.sess | ||
| .query(DataObject.name) | ||
| .filter(Collection.name == self.coll_path) | ||
@@ -225,3 +199,4 @@ .filter(NotLike(DataObject.name, str.upper(search_expression))) | ||
| result10 = ( | ||
| self.sess.query(DataObject.name) | ||
| self.sess | ||
| .query(DataObject.name) | ||
| .filter(Collection.name == self.coll_path) | ||
@@ -235,3 +210,4 @@ .filter(In(DataObject.name, [self.case_sensitive_obj_name1])) | ||
| result11 = ( | ||
| self.sess.query(DataObject.name) | ||
| self.sess | ||
| .query(DataObject.name) | ||
| .filter(Collection.name == self.coll_path) | ||
@@ -244,3 +220,4 @@ .filter(In(DataObject.name, [str.lower(self.case_sensitive_obj_name1)])) | ||
| result12 = ( | ||
| self.sess.query(DataObject.name) | ||
| self.sess | ||
| .query(DataObject.name) | ||
| .filter(Collection.name == self.coll_path) | ||
@@ -283,3 +260,4 @@ .filter(In(DataObject.name, [str.upper(self.case_sensitive_obj_name1)])) | ||
| result1 = ( | ||
| self.sess.query(DataObject.name, case_sensitive=False) | ||
| self.sess | ||
| .query(DataObject.name, case_sensitive=False) | ||
| .filter(DataObject.name == self.case_sensitive_obj_name1) | ||
@@ -293,3 +271,4 @@ .all() | ||
| result2 = ( | ||
| self.sess.query(DataObject.name, case_sensitive=False) | ||
| self.sess | ||
| .query(DataObject.name, case_sensitive=False) | ||
| .filter(DataObject.name == str.lower(self.case_sensitive_obj_name1)) | ||
@@ -303,3 +282,4 @@ .all() | ||
| result3 = ( | ||
| self.sess.query(DataObject.name, case_sensitive=False) | ||
| self.sess | ||
| .query(DataObject.name, case_sensitive=False) | ||
| .filter(DataObject.name == str.upper(self.case_sensitive_obj_name1)) | ||
@@ -317,3 +297,4 @@ .all() | ||
| result4 = ( | ||
| self.sess.query(DataObject.name, case_sensitive=False) | ||
| self.sess | ||
| .query(DataObject.name, case_sensitive=False) | ||
| .filter(Like(DataObject.name, search_expression)) | ||
@@ -327,3 +308,4 @@ .all() | ||
| result5 = ( | ||
| self.sess.query(DataObject.name, case_sensitive=False) | ||
| self.sess | ||
| .query(DataObject.name, case_sensitive=False) | ||
| .filter(Like(DataObject.name, str.lower(search_expression))) | ||
@@ -337,3 +319,4 @@ .all() | ||
| result6 = ( | ||
| self.sess.query(DataObject.name, case_sensitive=False) | ||
| self.sess | ||
| .query(DataObject.name, case_sensitive=False) | ||
| .filter(Like(DataObject.name, str.upper(search_expression))) | ||
@@ -347,3 +330,4 @@ .all() | ||
| result7 = ( | ||
| self.sess.query(DataObject.name, case_sensitive=False) | ||
| self.sess | ||
| .query(DataObject.name, case_sensitive=False) | ||
| .filter(Collection.name == self.coll_path) | ||
@@ -358,3 +342,4 @@ .filter(NotLike(DataObject.name, search_expression)) | ||
| result8 = ( | ||
| self.sess.query(DataObject.name, case_sensitive=False) | ||
| self.sess | ||
| .query(DataObject.name, case_sensitive=False) | ||
| .filter(Collection.name == self.coll_path) | ||
@@ -369,3 +354,4 @@ .filter(NotLike(DataObject.name, str.lower(search_expression))) | ||
| result9 = ( | ||
| self.sess.query(DataObject.name, case_sensitive=False) | ||
| self.sess | ||
| .query(DataObject.name, case_sensitive=False) | ||
| .filter(Collection.name == self.coll_path) | ||
@@ -382,3 +368,4 @@ .filter(NotLike(DataObject.name, str.upper(search_expression))) | ||
| result10 = ( | ||
| self.sess.query(DataObject.name, case_sensitive=False) | ||
| self.sess | ||
| .query(DataObject.name, case_sensitive=False) | ||
| .filter(Collection.name == self.coll_path) | ||
@@ -393,3 +380,4 @@ .filter(In(DataObject.name, [self.case_sensitive_obj_name1])) | ||
| result11 = ( | ||
| self.sess.query(DataObject.name, case_sensitive=False) | ||
| self.sess | ||
| .query(DataObject.name, case_sensitive=False) | ||
| .filter(Collection.name == self.coll_path) | ||
@@ -404,3 +392,4 @@ .filter(In(DataObject.name, [str.lower(self.case_sensitive_obj_name1)])) | ||
| result12 = ( | ||
| self.sess.query(DataObject.name, case_sensitive=False) | ||
| self.sess | ||
| .query(DataObject.name, case_sensitive=False) | ||
| .filter(Collection.name == self.coll_path) | ||
@@ -417,3 +406,4 @@ .filter(In(DataObject.name, [str.upper(self.case_sensitive_obj_name1)])) | ||
| result13 = ( | ||
| self.sess.query(DataObject.name, case_sensitive=False) | ||
| self.sess | ||
| .query(DataObject.name, case_sensitive=False) | ||
| .filter(Collection.name == self.coll_path) | ||
@@ -436,3 +426,4 @@ .filter( | ||
| result14 = ( | ||
| self.sess.query(DataObject.name, case_sensitive=False) | ||
| self.sess | ||
| .query(DataObject.name, case_sensitive=False) | ||
| .filter(Collection.name == self.coll_path) | ||
@@ -455,3 +446,4 @@ .filter( | ||
| result15 = ( | ||
| self.sess.query(DataObject.name, case_sensitive=False) | ||
| self.sess | ||
| .query(DataObject.name, case_sensitive=False) | ||
| .filter(Collection.name == self.coll_path) | ||
@@ -477,3 +469,4 @@ .filter( | ||
| result16 = ( | ||
| self.sess.query(DataObject.name, case_sensitive=False) | ||
| self.sess | ||
| .query(DataObject.name, case_sensitive=False) | ||
| .filter(Like(DataObject.name, search_expression)) | ||
@@ -488,3 +481,4 @@ .filter(Collection.name == self.coll_path) | ||
| result17 = ( | ||
| self.sess.query(DataObject.name, case_sensitive=False) | ||
| self.sess | ||
| .query(DataObject.name, case_sensitive=False) | ||
| .filter(Like(DataObject.name, str.lower(search_expression))) | ||
@@ -499,3 +493,4 @@ .filter(Collection.name == str.lower(self.coll_path)) | ||
| result18 = ( | ||
| self.sess.query(DataObject.name, case_sensitive=False) | ||
| self.sess | ||
| .query(DataObject.name, case_sensitive=False) | ||
| .filter(Like(DataObject.name, str.upper(search_expression))) | ||
@@ -511,3 +506,4 @@ .filter(Collection.name == str.upper(self.coll_path)) | ||
| result19 = ( | ||
| self.sess.query(DataObject.name, case_sensitive=False) | ||
| self.sess | ||
| .query(DataObject.name, case_sensitive=False) | ||
| .filter(Like(DataObject.name, "THIS_SHOULD_NOT_MATCH")) | ||
@@ -601,19 +597,11 @@ .all() | ||
| test_collection_size = 8 | ||
| test_collection_path = "/{0}/home/{1}/testcoln_for_col_not_in_result".format( | ||
| self.sess.zone, self.sess.username | ||
| ) | ||
| test_collection_path = "/{0}/home/{1}/testcoln_for_col_not_in_result".format(self.sess.zone, self.sess.username) | ||
| c1 = c2 = None | ||
| try: | ||
| c1 = helpers.make_test_collection( | ||
| self.sess, test_collection_path + "1", obj_count=test_collection_size | ||
| ) | ||
| c2 = helpers.make_test_collection( | ||
| self.sess, test_collection_path + "2", obj_count=test_collection_size | ||
| ) | ||
| d12 = [ | ||
| sorted([d.id for d in c.data_objects]) | ||
| for c in sorted((c1, c2), key=lambda c: c.id) | ||
| ] | ||
| c1 = helpers.make_test_collection(self.sess, test_collection_path + "1", obj_count=test_collection_size) | ||
| c2 = helpers.make_test_collection(self.sess, test_collection_path + "2", obj_count=test_collection_size) | ||
| d12 = [sorted([d.id for d in c.data_objects]) for c in sorted((c1, c2), key=lambda c: c.id)] | ||
| query = ( | ||
| self.sess.query(DataObject) | ||
| self.sess | ||
| .query(DataObject) | ||
| .filter(Like(Collection.name, test_collection_path + "_")) | ||
@@ -623,7 +611,3 @@ .order_by(Collection.id) | ||
| q12 = list(map(lambda res: res[DataObject.id], query)) | ||
| self.assertTrue( | ||
| d12[0] + d12[1] | ||
| == sorted(q12[:test_collection_size]) | ||
| + sorted(q12[test_collection_size:]) | ||
| ) | ||
| self.assertTrue(d12[0] + d12[1] == sorted(q12[:test_collection_size]) + sorted(q12[test_collection_size:])) | ||
| finally: | ||
@@ -657,5 +641,3 @@ if c1: | ||
| for result in query: | ||
| res_str = "{} {}/{}".format( | ||
| result[Resource.name], result[Collection.name], result[DataObject.name] | ||
| ) | ||
| res_str = "{} {}/{}".format(result[Resource.name], result[Collection.name], result[DataObject.name]) | ||
| self.assertIn(session.zone, res_str) | ||
@@ -683,11 +665,7 @@ | ||
| for x in range(3, 9): | ||
| obj = helpers.make_object( | ||
| self.sess, file_path + "-{}".format(x) | ||
| ) # with metadata | ||
| obj = helpers.make_object(self.sess, file_path + "-{}".format(x)) # with metadata | ||
| objects.append(obj) | ||
| obj.metadata.add("A_meta", "1{}".format(x)) | ||
| obj.metadata.add("B_meta", "2{}".format(x)) | ||
| decoys.append( | ||
| helpers.make_object(self.sess, file_path + "-dummy{}".format(x)) | ||
| ) # without metadata | ||
| decoys.append(helpers.make_object(self.sess, file_path + "-dummy{}".format(x))) # without metadata | ||
| self.assertTrue(len(objects) > 0) | ||
@@ -697,3 +675,4 @@ | ||
| q = ( | ||
| self.sess.query(DataObject, DataObjectMeta) | ||
| self.sess | ||
| .query(DataObject, DataObjectMeta) | ||
| .filter(DataObjectMeta.name == "A_meta", DataObjectMeta.value < "20") | ||
@@ -705,8 +684,6 @@ .filter(DataObjectMeta.name == "B_meta", DataObjectMeta.value >= "20") | ||
| # -- test no-stomp of previous filter -- | ||
| self.assertTrue( | ||
| ("B_meta", "28") | ||
| in [(x.name, x.value) for x in objects[-1].metadata.items()] | ||
| ) | ||
| self.assertTrue(("B_meta", "28") in [(x.name, x.value) for x in objects[-1].metadata.items()]) | ||
| q = ( | ||
| self.sess.query(DataObject, DataObjectMeta) | ||
| self.sess | ||
| .query(DataObject, DataObjectMeta) | ||
| .filter(DataObjectMeta.name == "B_meta") | ||
@@ -722,3 +699,4 @@ .filter(DataObjectMeta.value < "28") | ||
| q = ( | ||
| self.sess.query(DataObject, DataObjectMeta) | ||
| self.sess | ||
| .query(DataObject, DataObjectMeta) | ||
| .filter(DataObjectMeta.name == "B_meta") | ||
@@ -739,5 +717,3 @@ .filter(DataObjectMeta.value == "28") | ||
| ) | ||
| testColl = helpers.make_test_collection( | ||
| self.sess, test_collection_path, obj_count=1 | ||
| ) | ||
| testColl = helpers.make_test_collection(self.sess, test_collection_path, obj_count=1) | ||
| testData = testColl.data_objects[0] | ||
@@ -769,12 +745,8 @@ testResc = self.sess.resources.get("demoResc") | ||
| for suffix, tblpair in tables.items(): | ||
| self.sess.query(*tblpair).filter( | ||
| tblpair[1].modify_time <= after | ||
| ).filter(tblpair[1].modify_time > before).filter( | ||
| tblpair[0].id == object_IDs[suffix] | ||
| ).one() | ||
| self.sess.query(*tblpair).filter( | ||
| tblpair[1].create_time <= after | ||
| ).filter(tblpair[1].create_time > before).filter( | ||
| tblpair[0].id == object_IDs[suffix] | ||
| ).one() | ||
| self.sess.query(*tblpair).filter(tblpair[1].modify_time <= after).filter( | ||
| tblpair[1].modify_time > before | ||
| ).filter(tblpair[0].id == object_IDs[suffix]).one() | ||
| self.sess.query(*tblpair).filter(tblpair[1].create_time <= after).filter( | ||
| tblpair[1].create_time > before | ||
| ).filter(tblpair[0].id == object_IDs[suffix]).one() | ||
| finally: | ||
@@ -789,3 +761,9 @@ for obj in objects.values(): | ||
| # Remove the column skips when irods/irods #8574 is resolved. | ||
| skipped_columns = {DataObject.map_id, Collection.map_id, DataObject.status, DataObject.type, DataObject.collection_id} | ||
| skipped_columns = { | ||
| DataObject.map_id, | ||
| Collection.map_id, | ||
| DataObject.status, | ||
| DataObject.type, | ||
| DataObject.collection_id, | ||
| } | ||
| collection = self.coll_path | ||
@@ -803,10 +781,7 @@ filename = "test_multiple_AVU_joins" | ||
| q = self.sess.query(Collection, DataObject, *{-col for col in skipped_columns}) | ||
| dummy_test = [ | ||
| d | ||
| for d in q | ||
| if d[DataObject.name][-1:] != "8" and d[DataObject.name][-7:-1] == "-dummy" | ||
| ] | ||
| dummy_test = [d for d in q if d[DataObject.name][-1:] != "8" and d[DataObject.name][-7:-1] == "-dummy"] | ||
| self.assertTrue(len(dummy_test) > 0) | ||
| q = ( | ||
| q.filter(Like(DataObject.name, "%-dummy_")) | ||
| q | ||
| .filter(Like(DataObject.name, "%-dummy_")) | ||
| .filter(Collection.name == collection) | ||
@@ -837,5 +812,3 @@ .filter(DataObject.name != (filename + "-dummy8")) | ||
| if not reg_info: | ||
| self.skipTest( | ||
| "server is non-localhost and no common path exists for object registration" | ||
| ) | ||
| self.skipTest("server is non-localhost and no common path exists for object registration") | ||
| (dir_, resc_option) = reg_info | ||
@@ -862,5 +835,3 @@ | ||
| (fd, encoded_test_file) = ( | ||
| tempfile.mkstemp( | ||
| dir=dir_.encode("utf-8"), prefix=filename_prefix.encode("utf-8") | ||
| ) | ||
| tempfile.mkstemp(dir=dir_.encode("utf-8"), prefix=filename_prefix.encode("utf-8")) | ||
| if sys.version_info >= (3, 5) | ||
@@ -875,12 +846,6 @@ else python34_unicode_mkstemp(dir=dir_, prefix=filename_prefix) | ||
| self.sess.data_objects.register(test_file, obj_path, **resc_option) | ||
| results = list( | ||
| self.sess.query(DataObject, Collection.name).filter( | ||
| DataObject.path == test_file | ||
| ) | ||
| ) | ||
| results = list(self.sess.query(DataObject, Collection.name).filter(DataObject.path == test_file)) | ||
| if results: | ||
| results = results[0] | ||
| result_logical_path = os.path.join( | ||
| results[Collection.name], results[DataObject.name] | ||
| ) | ||
| result_logical_path = os.path.join(results[Collection.name], results[DataObject.name]) | ||
| result_physical_path = results[DataObject.path] | ||
@@ -910,5 +875,3 @@ self.assertEqual(result_logical_path, obj_path) | ||
| if "/" not in coll_path: | ||
| coll_path = "/{}/home/{}/{}".format( | ||
| self.session.zone, self.session.username, coll_path | ||
| ) | ||
| coll_path = "/{}/home/{}/{}".format(self.session.zone, self.session.username, coll_path) | ||
| self.coll_path = coll_path | ||
@@ -927,9 +890,6 @@ self.num_objects = num_objects | ||
| if self.nAVUs > 0: | ||
| # - set the AVUs on the collection's objects: | ||
| for data_obj_path in map( | ||
| lambda d: d[Collection.name] + "/" + d[DataObject.name], | ||
| self.session.query(*q_params).filter( | ||
| Collection.name == self.test_collection.path | ||
| ), | ||
| self.session.query(*q_params).filter(Collection.name == self.test_collection.path), | ||
| ): | ||
@@ -944,5 +904,3 @@ data_obj = self.session.data_objects.get(data_obj_path) | ||
| # - The "with" statement receives, as context variable, a zero-arg function to build the query | ||
| return lambda: self.session.query(*q_params).filter( | ||
| Collection.name == self.test_collection.path | ||
| ) | ||
| return lambda: self.session.query(*q_params).filter(Collection.name == self.test_collection.path) | ||
@@ -965,6 +923,3 @@ def __exit__(self, *_): # - clean up after context block | ||
| with self.Issue_166_context( | ||
| self.sess, num_objects=self.More_than_one_batch | ||
| ) as buildQuery: | ||
| with self.Issue_166_context(self.sess, num_objects=self.More_than_one_batch) as buildQuery: | ||
| for dummy_i in self.Iterate_to_exhaust_statement_table: | ||
@@ -979,8 +934,4 @@ query = buildQuery() | ||
| with self.Issue_166_context( | ||
| self.sess, num_objects=self.More_than_one_batch | ||
| ) as buildQuery: | ||
| with self.Issue_166_context(self.sess, num_objects=self.More_than_one_batch) as buildQuery: | ||
| for dummy_i in self.Iterate_to_exhaust_statement_table: | ||
| for dummy_row in buildQuery(): | ||
@@ -994,3 +945,2 @@ break # single iteration | ||
| ) as buildQuery: | ||
| pages = [b for b in buildQuery().get_batches()] | ||
@@ -1002,3 +952,2 @@ self.assertTrue(len(pages) > 2 and len(pages[0]) < self.More_than_one_batch) | ||
| for _ in self.Iterate_to_exhaust_statement_table: | ||
| for batch in buildQuery().get_batches(): | ||
@@ -1018,11 +967,7 @@ to_compare.append(batch) | ||
| Set1 = {Compare_Key(dct) for dct in to_compare[1]} | ||
| self.assertTrue( | ||
| len(Set0 & Set1) == 0 | ||
| ) # assert intersection is null set | ||
| self.assertTrue(len(Set0 & Set1) == 0) # assert intersection is null set | ||
| def test_paging_get_results__166(self): | ||
| with self.Issue_166_context( | ||
| self.sess, num_objects=self.More_than_one_batch | ||
| ) as buildQuery: | ||
| with self.Issue_166_context(self.sess, num_objects=self.More_than_one_batch) as buildQuery: | ||
| batch_size = 0 | ||
@@ -1080,10 +1025,18 @@ for result_set in buildQuery().get_batches(): | ||
| def test_negating_columns_in_genquery1_results__issue_755(self): | ||
| columns_to_negate = {Collection.map_id, DataObject.map_id, DataObject.status, DataObject.type, DataObject.collection_id} | ||
| columns_to_negate = { | ||
| Collection.map_id, | ||
| DataObject.map_id, | ||
| DataObject.status, | ||
| DataObject.type, | ||
| DataObject.collection_id, | ||
| } | ||
| columns_to_negate_D = columns_to_negate & set(DataObject._columns) | ||
| columns_to_negate_C = columns_to_negate & set(Collection._columns) | ||
| cases = { (Collection, DataObject): columns_to_negate, | ||
| (Collection,): columns_to_negate_C, | ||
| (DataObject,): columns_to_negate_D, } | ||
| cases = { | ||
| (Collection, DataObject): columns_to_negate, | ||
| (Collection,): columns_to_negate_C, | ||
| (DataObject,): columns_to_negate_D, | ||
| } | ||
| for requested,intersect in cases.items(): | ||
| for requested, intersect in cases.items(): | ||
| q = self.sess.query(*requested, *{-col for col in columns_to_negate}).limit(1) | ||
@@ -1096,3 +1049,3 @@ row = list(q.all())[0] | ||
| # Remove the if/continue when irods/irods #8574 is resolved. | ||
| if self.sess.server_version > (5,0,0) and len(requested) > 1: | ||
| if self.sess.server_version > (5, 0, 0) and len(requested) > 1: | ||
| continue | ||
@@ -1119,10 +1072,12 @@ | ||
| # Test a query limit via configuration. | ||
| with config.loadlines( | ||
| entries=[dict(setting="genquery1.irods_query_limit", value=num_limited_results)] | ||
| ): | ||
| limited_results = list(self.sess.query(DataObject.id).filter(Like(DataObject.name, '%issue_712_obj%')).all()) | ||
| with config.loadlines(entries=[dict(setting="genquery1.irods_query_limit", value=num_limited_results)]): | ||
| limited_results = list( | ||
| self.sess.query(DataObject.id).filter(Like(DataObject.name, '%issue_712_obj%')).all() | ||
| ) | ||
| self.assertEqual(num_limited_results, len(limited_results)) | ||
| # Test the query limit is no longer in effect. | ||
| non_limited_results = list(self.sess.query(DataObject.id).filter(Like(DataObject.name, '%issue_712_obj%')).all()) | ||
| non_limited_results = list( | ||
| self.sess.query(DataObject.id).filter(Like(DataObject.name, '%issue_712_obj%')).all() | ||
| ) | ||
| self.assertEqual(num_total_objects, len(non_limited_results)) | ||
@@ -1135,3 +1090,2 @@ finally: | ||
| class TestSpecificQuery(unittest.TestCase): | ||
| def setUp(self): | ||
@@ -1148,5 +1102,3 @@ super(TestSpecificQuery, self).setUp() | ||
| test_collection_size = 3 * MAX_SQL_ROWS | ||
| test_collection_path = "/{0}/home/{1}/test_collection".format( | ||
| self.session.zone, self.session.username | ||
| ) | ||
| test_collection_path = "/{0}/home/{1}/test_collection".format(self.session.zone, self.session.username) | ||
| self.test_collection = helpers.make_test_collection( | ||
@@ -1186,5 +1138,3 @@ self.session, test_collection_path, obj_count=test_collection_size | ||
| test_collection_size = 3 * MAX_SQL_ROWS | ||
| test_collection_path = "/{0}/home/{1}/test_collection".format( | ||
| self.session.zone, self.session.username | ||
| ) | ||
| test_collection_path = "/{0}/home/{1}/test_collection".format(self.session.zone, self.session.username) | ||
| self.test_collection = helpers.make_test_collection( | ||
@@ -1191,0 +1141,0 @@ self.session, test_collection_path, obj_count=test_collection_size |
@@ -13,3 +13,2 @@ #! /usr/bin/env python | ||
| class TestResource(unittest.TestCase): | ||
| create_simple_resc_hierarchy = helpers.create_simple_resc_hierarchy | ||
@@ -29,5 +28,6 @@ create_simple_resc = helpers.create_simple_resc | ||
| # Create two (passthru + storage) hierarchies below the root: ie. pt0;leaf0 and pt1;leaf1 | ||
| with self.create_simple_resc_hierarchy( | ||
| pt + "_0", leaf + "_0" | ||
| ), self.create_simple_resc_hierarchy(pt + "_1", leaf + "_1"): | ||
| with ( | ||
| self.create_simple_resc_hierarchy(pt + "_0", leaf + "_0"), | ||
| self.create_simple_resc_hierarchy(pt + "_1", leaf + "_1"), | ||
| ): | ||
| try: | ||
@@ -47,12 +47,6 @@ # Adopt both passthru's as children under the main root (deferred) node. | ||
| hier_str += ";{}".format(resc.name) | ||
| self.assertEqual( | ||
| resc.parent_id, (None if n == 0 else parent_resc.id) | ||
| ) | ||
| self.assertEqual( | ||
| resc.parent_name, (None if n == 0 else parent_resc.name) | ||
| ) | ||
| self.assertEqual(resc.parent_id, (None if n == 0 else parent_resc.id)) | ||
| self.assertEqual(resc.parent_name, (None if n == 0 else parent_resc.name)) | ||
| self.assertEqual(resc.hierarchy_string, hier_str) | ||
| self.assertIs( | ||
| type(resc.hierarchy_string), str | ||
| ) # type of hierarchy field is string. | ||
| self.assertIs(type(resc.hierarchy_string), str) # type of hierarchy field is string. | ||
| if resc.parent is None: | ||
@@ -62,8 +56,4 @@ self.assertIs(resc.parent_id, None) | ||
| else: | ||
| self.assertIs( | ||
| type(resc.parent_id), int | ||
| ) # type of a non-null id field is integer. | ||
| self.assertIs( | ||
| type(resc.parent_name), str | ||
| ) # type of a non-null name field is string. | ||
| self.assertIs(type(resc.parent_id), int) # type of a non-null id field is integer. | ||
| self.assertIs(type(resc.parent_name), str) # type of a non-null name field is string. | ||
| parent_resc = resc | ||
@@ -93,6 +83,3 @@ finally: | ||
| self.sess.data_objects.put( | ||
| small_file, | ||
| "{home}/{small_file}".format(**locals()), | ||
| return_data_object=True, | ||
| **put_opts | ||
| small_file, "{home}/{small_file}".format(**locals()), return_data_object=True, **put_opts | ||
| ) | ||
@@ -107,6 +94,3 @@ ) | ||
| self.sess.data_objects.put( | ||
| large_file, | ||
| "{home}/{large_file}".format(**locals()), | ||
| return_data_object=True, | ||
| **put_opts | ||
| large_file, "{home}/{large_file}".format(**locals()), return_data_object=True, **put_opts | ||
| ) | ||
@@ -113,0 +97,0 @@ ) |
+12
-31
@@ -23,3 +23,3 @@ #! /usr/bin/env python | ||
| os.environ.get("PYTHON_RULE_ENGINE_INSTALLED", "*").lower()[:1] == "y", | ||
| "Test depends on server having Python-REP installed beyond the default options", | ||
| "Test depends on server having Python-REP installed (set PYTHON_RULE_ENGINE_INSTALLED=yes in environment).", | ||
| ) | ||
@@ -69,5 +69,3 @@ | ||
| INPUT *object="{object_path}",*name="{attr_name}",*value="{attr_value}" | ||
| OUTPUT ruleExecOut""".format( | ||
| **locals() | ||
| ) | ||
| OUTPUT ruleExecOut""".format(**locals()) | ||
| ) | ||
@@ -156,3 +154,2 @@ | ||
| for i in rule_instances_list: | ||
| if rule_dict: | ||
@@ -179,9 +176,3 @@ rule_to_call = rule_dict[i] | ||
| len(err_hash), | ||
| len( | ||
| [ | ||
| val | ||
| for val in err_hash.values() | ||
| if val[0].startswith("rule exec failed") | ||
| ] | ||
| ), | ||
| len([val for val in err_hash.values() if val[0].startswith("rule exec failed")]), | ||
| ) | ||
@@ -216,3 +207,3 @@ return err_hash | ||
| output="ruleExecOut", | ||
| **{key: val for key, val in kw.items() if key == "instance_name"} | ||
| **{key: val for key, val in kw.items() if key == "instance_name"}, | ||
| ) | ||
@@ -244,5 +235,3 @@ output = rule.execute() | ||
| writeLine("{stream_name}","*value") | ||
| """.format( | ||
| **locals() | ||
| ) | ||
| """.format(**locals()) | ||
| ) | ||
@@ -260,9 +249,3 @@ | ||
| myrule = Rule( | ||
| session, | ||
| body=rule_body, | ||
| params=input_params, | ||
| output=output_param, | ||
| **extra_options | ||
| ) | ||
| myrule = Rule(session, body=rule_body, params=input_params, output=output_param, **extra_options) | ||
| output = myrule.execute() | ||
@@ -367,5 +350,3 @@ | ||
| INPUT *some_string="{some_string}",*some_other_string="{some_other_string}",*err_string="{err_string}" | ||
| OUTPUT ruleExecOut""".format( | ||
| **locals() | ||
| ) | ||
| OUTPUT ruleExecOut""".format(**locals()) | ||
| ) | ||
@@ -431,3 +412,3 @@ | ||
| lines = self.lines_from_stdout_buf(output) | ||
| self.assertRegexpMatches(lines[0], r".*\[Hello world!\]") | ||
| self.assertRegex(lines[0], r".*\[Hello world!\]") | ||
@@ -454,4 +435,4 @@ def test_rulefile_in_file_like_object_2__336(self): | ||
| lines = self.lines_from_stdout_buf(output) | ||
| self.assertRegexpMatches(lines[0], r"\[STRING\]\[\]") | ||
| self.assertRegexpMatches(lines[1], r"\[STRING\]\[\]") | ||
| self.assertRegex(lines[0], r"\[STRING\]\[\]") | ||
| self.assertRegex(lines[1], r"\[STRING\]\[\]") | ||
@@ -465,4 +446,4 @@ r = Rule( | ||
| lines = self.lines_from_stdout_buf(output) | ||
| self.assertRegexpMatches(lines[0], r"\[INTEGER\]\[5\]") | ||
| self.assertRegexpMatches(lines[1], r"\[STRING\]\[A String\]") | ||
| self.assertRegex(lines[0], r"\[INTEGER\]\[5\]") | ||
| self.assertRegex(lines[1], r"\[STRING\]\[A String\]") | ||
@@ -469,0 +450,0 @@ |
+68
-13
@@ -9,3 +9,3 @@ #!/usr/bin/env python | ||
| import argparse | ||
| import os | ||
@@ -20,22 +20,77 @@ import sys | ||
| h = logging.StreamHandler() | ||
| f = logging.Formatter( | ||
| "%(asctime)s %(name)s-%(levelname)s [%(pathname)s %(lineno)d] %(message)s" | ||
| ) | ||
| f = logging.Formatter("%(asctime)s %(name)s-%(levelname)s [%(pathname)s %(lineno)d] %(message)s") | ||
| h.setFormatter(f) | ||
| logger.addHandler(h) | ||
| parser = argparse.ArgumentParser() | ||
| # Load all tests in the current directory and run them | ||
| def abs_path(initial_dir, levels_up=0): | ||
| directory = initial_dir | ||
| while levels_up > 0: | ||
| levels_up -= 1 | ||
| directory = os.path.join(directory, '..') | ||
| return os.path.abspath(directory) | ||
| # Load all tests in the current directory and run them. | ||
| if __name__ == "__main__": | ||
| # must set the path for the imported tests | ||
| sys.path.insert(0, os.path.abspath("../..")) | ||
| # Get path to script directory for test import and/or discovery. | ||
| script_dir = os.path.abspath(os.path.dirname(sys.argv[0])) | ||
| loader = TestLoader() | ||
| suite = TestSuite( | ||
| loader.discover(start_dir=".", pattern="*_test.py", top_level_dir=".") | ||
| # Must set the path for the imported tests. | ||
| sys.path.insert(0, abs_path(script_dir, levels_up=2)) | ||
| parser.add_argument('--tests', '-t', metavar='TESTS', dest='tests', nargs='+', help='List of tests to run.') | ||
| parser.add_argument( | ||
| '--environment_variable', | ||
| '-e', | ||
| metavar='ENVIRONMENT_VARIABLE', | ||
| dest='env_var', | ||
| type=str, | ||
| help='Name of environment variable name to scan for in reason strings when filtering skipped test names to be output.', | ||
| ) | ||
| result = xmlrunner.XMLTestRunner( | ||
| verbosity=2, output="/tmp/python-irodsclient/test-reports" | ||
| ).run(suite) | ||
| parser.add_argument( | ||
| '--output_tests_skipped', | ||
| '-s', | ||
| metavar='SKIPPED_TESTS_OUTPUT_FILENAME', | ||
| dest='skipped_tests_output_filename', | ||
| type=str, | ||
| help='Name of a file into which to write names of skipped tests.', | ||
| ) | ||
| parser.add_argument( | ||
| '--tests_file', | ||
| '-f', | ||
| metavar='TESTS_FILE', | ||
| dest='tests_file', | ||
| help='Name of a file containing a list of tests to run.', | ||
| ) | ||
| args = parser.parse_args() | ||
| if args.tests_file: | ||
| if args.tests: | ||
| print('Cannot specify both --tests and --tests_file', file=sys.stderr) | ||
| exit(2) | ||
| args.tests = filter(None, open(args.tests_file).read().split("\n")) | ||
| loader = TestLoader() | ||
| if args.tests: | ||
| suite = TestSuite(loader.loadTestsFromNames(args.tests)) | ||
| else: | ||
| suite = TestSuite(loader.discover(start_dir=script_dir, pattern='*_test.py', top_level_dir=script_dir)) | ||
| result = xmlrunner.XMLTestRunner(verbosity=2, output="/tmp/python-irodsclient/test-reports").run(suite) | ||
| if args.skipped_tests_output_filename: | ||
| with open(args.skipped_tests_output_filename, 'w') as skip_file: | ||
| do_output = lambda reason: (args.env_var in reason) if args.env_var else True | ||
| for testinfo, reason in result.skipped: | ||
| if do_output(reason): | ||
| print(testinfo.test_id, file=skip_file) | ||
| if result.wasSuccessful(): | ||
@@ -42,0 +97,0 @@ sys.exit(0) |
@@ -11,5 +11,3 @@ from irods.test.helpers import make_session, home_collection | ||
| connections = session.pool.active | session.pool.idle | ||
| is_SSL = len(connections) > 0 and all( | ||
| isinstance(conn.socket, ssl.SSLSocket) for conn in connections | ||
| ) | ||
| is_SSL = len(connections) > 0 and all(isinstance(conn.socket, ssl.SSLSocket) for conn in connections) | ||
| exit(0 if is_SSL else 1) |
@@ -51,3 +51,2 @@ #! /usr/bin/env python | ||
| ) as session: | ||
| # do something that connects to the server | ||
@@ -78,3 +77,2 @@ session.users.get(self.admin.username) | ||
| ) as session: | ||
| # do something that connects to the server | ||
@@ -81,0 +79,0 @@ session.users.get(self.new_user) |
| #! /usr/bin/env python | ||
| import calendar | ||
| import datetime | ||
| import os | ||
| import sys | ||
| import tempfile | ||
| import time | ||
| import unittest | ||
| import time | ||
| import calendar | ||
| import irods.test.helpers as helpers | ||
| import tempfile | ||
| from irods.session import iRODSSession | ||
| import irods.exception as ex | ||
| import irods.keywords as kw | ||
| from irods.ticket import Ticket | ||
| from irods.models import TicketQuery, DataObject, Collection | ||
| from irods.models import Collection, DataObject, TicketQuery | ||
| from irods.session import iRODSSession | ||
| from irods.test import helpers | ||
| from irods.ticket import Ticket, ticket_iterator | ||
| # As with most of the modules in this test suite, session objects created via | ||
@@ -26,5 +26,4 @@ # make_session() are implicitly agents of a rodsadmin unless otherwise indicated. | ||
| def gmtime_to_timestamp(gmt_struct): | ||
| return ( | ||
| "{0.tm_year:04d}-{0.tm_mon:02d}-{0.tm_mday:02d}." | ||
| "{0.tm_hour:02d}:{0.tm_min:02d}:{0.tm_sec:02d}".format(gmt_struct) | ||
| return "{0.tm_year:04d}-{0.tm_mon:02d}-{0.tm_mday:02d}.{0.tm_hour:02d}:{0.tm_min:02d}:{0.tm_sec:02d}".format( | ||
| gmt_struct | ||
| ) | ||
@@ -35,5 +34,3 @@ | ||
| my_userid = session.users.get(session.username).id | ||
| my_tickets = session.query(TicketQuery.Ticket).filter( | ||
| TicketQuery.Ticket.user_id == my_userid | ||
| ) | ||
| my_tickets = session.query(TicketQuery.Ticket).filter(TicketQuery.Ticket.user_id == my_userid) | ||
| for res in my_tickets: | ||
@@ -44,3 +41,2 @@ Ticket(session, result=res).delete() | ||
| class TestRodsUserTicketOps(unittest.TestCase): | ||
| def login(self, user): | ||
@@ -65,4 +61,3 @@ return iRODSSession( | ||
| return [ | ||
| "{}/{}".format(o[Collection.name], o[DataObject.name]) | ||
| for o in sess.query(Collection.name, DataObject.name) | ||
| "{}/{}".format(o[Collection.name], o[DataObject.name]) for o in sess.query(Collection.name, DataObject.name) | ||
| ] | ||
@@ -80,2 +75,4 @@ | ||
| self.skipTest("""Test runnable only by rodsadmin.""") | ||
| self.rods_admin_name = ses.username | ||
| self.host = ses.host | ||
@@ -135,6 +132,3 @@ self.port = ses.port | ||
| alice_home_path = self.irods_homedir(alice, path_only=True) | ||
| ticket_strings = [ | ||
| Ticket(alice).issue("read", alice_home_path).string | ||
| for _ in range(N_TICKETS) | ||
| ] | ||
| ticket_strings = [Ticket(alice).issue("read", alice_home_path).string for _ in range(N_TICKETS)] | ||
@@ -146,5 +140,3 @@ # As rodsadmin, use the ADMIN_KW flag to delete alice's tickets. | ||
| t[TicketQuery.Ticket.string] | ||
| for t in ses.query(TicketQuery.Ticket).filter( | ||
| TicketQuery.Owner.name == "alice" | ||
| ) | ||
| for t in ses.query(TicketQuery.Ticket).filter(TicketQuery.Owner.name == "alice") | ||
| ] | ||
@@ -156,5 +148,3 @@ self.assertEqual(len(alices_tickets), N_TICKETS) | ||
| t[TicketQuery.Ticket.string] | ||
| for t in ses.query(TicketQuery.Ticket).filter( | ||
| TicketQuery.Owner.name == "alice" | ||
| ) | ||
| for t in ses.query(TicketQuery.Ticket).filter(TicketQuery.Owner.name == "alice") | ||
| ] | ||
@@ -185,8 +175,4 @@ self.assertEqual(len(alices_tickets), 0) | ||
| ] | ||
| t1.modify( | ||
| "expire", later_ts | ||
| ) # - Specify expiry with the human readable timestamp. | ||
| t2.modify( | ||
| "expire", later_epoch | ||
| ) # - Specify expiry formatted as epoch seconds. | ||
| t1.modify("expire", later_ts) # - Specify expiry with the human readable timestamp. | ||
| t2.modify("expire", later_epoch) # - Specify expiry formatted as epoch seconds. | ||
@@ -202,7 +188,3 @@ # Check normal access succeeds prior to expiration | ||
| for ticket_string in tickets: | ||
| t = ( | ||
| ses.query(TicketQuery.Ticket) | ||
| .filter(TicketQuery.Ticket.string == ticket_string) | ||
| .one() | ||
| ) | ||
| t = ses.query(TicketQuery.Ticket).filter(TicketQuery.Ticket.string == ticket_string).one() | ||
| timestamps.append(t[TicketQuery.Ticket.expiry_ts]) | ||
@@ -219,5 +201,3 @@ self.assertEqual(len(timestamps), 2) | ||
| Expected_Exception = ( | ||
| ex.CAT_TICKET_EXPIRED | ||
| if ses.server_version >= (4, 2, 9) | ||
| else ex.SYS_FILE_DESC_OUT_OF_RANGE | ||
| ex.CAT_TICKET_EXPIRED if ses.server_version >= (4, 2, 9) else ex.SYS_FILE_DESC_OUT_OF_RANGE | ||
| ) | ||
@@ -227,10 +207,6 @@ | ||
| for ticket_string in tickets: | ||
| with self.login( | ||
| self.alice | ||
| ) as alice, tempfile.NamedTemporaryFile() as f: | ||
| with self.login(self.alice) as alice, tempfile.NamedTemporaryFile() as f: | ||
| Ticket(alice, ticket_string).supply() | ||
| with self.assertRaises(Expected_Exception): | ||
| alice.data_objects.get( | ||
| dobj.path, f.name, **{kw.FORCE_FLAG_KW: ""} | ||
| ) | ||
| alice.data_objects.get(dobj.path, f.name, **{kw.FORCE_FLAG_KW: ""}) | ||
@@ -258,6 +234,3 @@ finally: | ||
| # Create 'R' and 'W' in alice's home collection. | ||
| data_objs = [ | ||
| helpers.make_object(alice, home.path + "/" + name, content="abcxyz") | ||
| for name in ("R", "W") | ||
| ] | ||
| data_objs = [helpers.make_object(alice, home.path + "/" + name, content="abcxyz") for name in ("R", "W")] | ||
| tickets = { | ||
@@ -289,15 +262,11 @@ "R": Ticket(alice).issue("read", home.path + "/R"), | ||
| # Test upload was successful, by getting and confirming contents. | ||
| with self.login( | ||
| self.bob | ||
| ) as bob: # This check must be in a new session or we get CollectionDoesNotExist. - Possibly a new issue [ ] | ||
| with ( | ||
| self.login(self.bob) as bob | ||
| ): # This check must be in a new session or we get CollectionDoesNotExist. - Possibly a new issue [ ] | ||
| for name in ("R", "W"): | ||
| bob.cleanup() # clear out existing connections | ||
| Ticket(bob, tickets[name].string).supply() | ||
| bob.data_objects.get( | ||
| home.path + "/" + name, rw_names[name], **{kw.FORCE_FLAG_KW: ""} | ||
| ) | ||
| bob.data_objects.get(home.path + "/" + name, rw_names[name], **{kw.FORCE_FLAG_KW: ""}) | ||
| with open(rw_names[name], "r") as tmpread: | ||
| self.assertEqual( | ||
| tmpread.read(), "abcxyz" if name == "R" else "hello" | ||
| ) | ||
| self.assertEqual(tmpread.read(), "abcxyz" if name == "R" else "hello") | ||
| finally: | ||
@@ -324,6 +293,3 @@ for t in tickets.values(): | ||
| # Create 'x' and 'y' in alice's home collection | ||
| data_objs = [ | ||
| helpers.make_object(alice, home.path + "/" + name, content="abcxyz") | ||
| for name in ("x", "y") | ||
| ] | ||
| data_objs = [helpers.make_object(alice, home.path + "/" + name, content="abcxyz") for name in ("x", "y")] | ||
@@ -339,5 +305,3 @@ with self.login(self.bob) as bob: | ||
| tmpfiles += [tmpf] | ||
| bob.data_objects.get( | ||
| home.path + "/" + name, tmpf.name, **{kw.FORCE_FLAG_KW: ""} | ||
| ) | ||
| bob.data_objects.get(home.path + "/" + name, tmpf.name, **{kw.FORCE_FLAG_KW: ""}) | ||
| with open(tmpf.name, "r") as tmpread: | ||
@@ -359,5 +323,3 @@ self.assertEqual(tmpread.read(), "abcxyz") | ||
| tmpfiles += [tmpf] | ||
| bob.data_objects.get( | ||
| home.path + "/x", tmpf.name, **{kw.FORCE_FLAG_KW: ""} | ||
| ) | ||
| bob.data_objects.get(home.path + "/x", tmpf.name, **{kw.FORCE_FLAG_KW: ""}) | ||
| with open(tmpf.name, "r") as tmpread: | ||
@@ -378,5 +340,23 @@ self.assertEqual(tmpread.read(), "abcxyz") | ||
| def test_modify_time_and_create_time_attributes_in_tickets__issue_801(self): | ||
| # Specifically we are testing that 'modify_time' and 'create_time' attributes function as expected, | ||
| bobs_ticket = None | ||
| try: | ||
| with self.login(self.bob) as bob: | ||
| bobs_ticket = Ticket(bob).issue('write', helpers.home_collection(bob)) | ||
| time.sleep(4) | ||
| bobs_ticket.modify('add', 'user', self.rods_admin_name) | ||
| # Reload the ticket, this time with the full complement of attributes present. | ||
| bobs_ticket = next(ticket_iterator(bob, filter_args=[TicketQuery.Ticket.string == bobs_ticket.string])) | ||
| self.assertGreater(bobs_ticket.modify_time, bobs_ticket.create_time + datetime.timedelta(seconds=2)) | ||
| finally: | ||
| if bobs_ticket: | ||
| bobs_ticket.delete() | ||
| class TestTicketOps(unittest.TestCase): | ||
| def setUp(self): | ||
@@ -476,3 +456,21 @@ """Create objects for test""" | ||
| def test_ticket_iterator__issue_120(self): | ||
| ses = self.sess | ||
| t = None | ||
| try: | ||
| # t first assigned as a "utility" Ticket object | ||
| t = Ticket(ses).issue('read', helpers.home_collection(ses)) | ||
| # This time, t receives attributes from a query result: notably the id, which we use for the next test. | ||
| t = Ticket(ses, result=ses.query(TicketQuery.Ticket).filter(TicketQuery.Ticket.string == t.string).one()) | ||
| # Check an id attribute is present and listed in the results from list_tickets | ||
| self.assertIn(t.id, (ticket.id for ticket in ticket_iterator(ses))) | ||
| finally: | ||
| if t: | ||
| t.delete() | ||
| if __name__ == "__main__": | ||
@@ -479,0 +477,0 @@ # let the tests find the parent irods lib |
@@ -20,5 +20,3 @@ #! /usr/bin/env python | ||
| UNICODE_TEST_FILE = os.path.join( | ||
| os.path.dirname(os.path.abspath(__file__)), "unicode_sampler.xml" | ||
| ) | ||
| UNICODE_TEST_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "unicode_sampler.xml") | ||
@@ -63,8 +61,5 @@ | ||
| class TestUnicodeNames(unittest.TestCase): | ||
| def setUp(self): | ||
| self.sess = helpers.make_session() | ||
| self.coll_path = "/{}/home/{}/test_dir".format( | ||
| self.sess.zone, self.sess.username | ||
| ) | ||
| self.coll_path = "/{}/home/{}/test_dir".format(self.sess.zone, self.sess.username) | ||
@@ -98,5 +93,3 @@ # make list of unicode filenames, from file | ||
| # Query for all files in test collection | ||
| query = self.sess.query(DataObject.name, Collection.name).filter( | ||
| Collection.name == self.coll_path | ||
| ) | ||
| query = self.sess.query(DataObject.name, Collection.name).filter(Collection.name == self.coll_path) | ||
@@ -112,5 +105,3 @@ # Python2 compatibility note: In keeping with the principle of least surprise, we now ensure | ||
| # fyi | ||
| logger.info( | ||
| "{0}/{1}".format(result[Collection.name], result[DataObject.name]) | ||
| ) | ||
| logger.info("{0}/{1}".format(result[Collection.name], result[DataObject.name])) | ||
@@ -117,0 +108,0 @@ # remove from set |
@@ -50,3 +50,2 @@ #! /usr/bin/env python | ||
| class TestUserAndGroup(unittest.TestCase): | ||
| def setUp(self): | ||
@@ -116,5 +115,3 @@ self.sess = helpers.make_session() | ||
| ENV_DIR = tempfile.mkdtemp() | ||
| d = dict( | ||
| password=OLDPASS, user="alice", host=ses.host, port=ses.port, zone=ses.zone | ||
| ) | ||
| d = dict(password=OLDPASS, user="alice", host=ses.host, port=ses.port, zone=ses.zone) | ||
| (alice_env, alice_auth) = helpers.make_environment_and_auth_files(ENV_DIR, **d) | ||
@@ -128,5 +125,3 @@ try: | ||
| True, | ||
| lambda: helpers.make_session( | ||
| irods_env_file=alice_env, irods_authentication_file=alice_auth | ||
| ), | ||
| lambda: helpers.make_session(irods_env_file=alice_env, irods_authentication_file=alice_auth), | ||
| ), | ||
@@ -137,10 +132,6 @@ ]: | ||
| alice = alice_ses.users.get(alice_ses.username) | ||
| alice.modify_password( | ||
| OLDPASS, NEWPASS, modify_irods_authentication_file=modify_option | ||
| ) | ||
| alice.modify_password(OLDPASS, NEWPASS, modify_irods_authentication_file=modify_option) | ||
| d["password"] = NEWPASS | ||
| with iRODSSession(**d) as session: | ||
| self.do_something( | ||
| session | ||
| ) # can we still do stuff with the final value of the password? | ||
| self.do_something(session) # can we still do stuff with the final value of the password? | ||
| finally: | ||
@@ -160,5 +151,3 @@ shutil.rmtree(ENV_DIR) | ||
| # Test different combinations of new and old password lengths | ||
| tuples_of_old_and_new_password = [ | ||
| ("a" * x, "b" * y) for x in pw_lengths for y in pw_lengths | ||
| ] | ||
| tuples_of_old_and_new_password = [("a" * x, "b" * y) for x in pw_lengths for y in pw_lengths] | ||
| ses.users.create("alice", "rodsuser") | ||
@@ -261,12 +250,6 @@ | ||
| ) | ||
| (alice_env, alice_auth) = helpers.make_environment_and_auth_files( | ||
| ENV_DIR, **d | ||
| ) | ||
| (alice_env, alice_auth) = helpers.make_environment_and_auth_files(ENV_DIR, **d) | ||
| session_factories = [ | ||
| (lambda: iRODSSession(**d)), | ||
| ( | ||
| lambda: helpers.make_session( | ||
| irods_env_file=alice_env, irods_authentication_file=alice_auth | ||
| ) | ||
| ), | ||
| (lambda: helpers.make_session(irods_env_file=alice_env, irods_authentication_file=alice_auth)), | ||
| ] | ||
@@ -296,5 +279,3 @@ for factory in session_factories: | ||
| self.assertEqual(group.name, group_name) | ||
| self.assertEqual( | ||
| repr(group), "<iRODSGroup {0} {1}>".format(group.id, group_name) | ||
| ) | ||
| self.assertEqual(repr(group), "<iRODSGroup {0} {1}>".format(group.id, group_name)) | ||
@@ -401,12 +382,5 @@ # delete group | ||
| result = ( | ||
| self.sess.query(UserMeta, Group) | ||
| .filter(Group.name == group_name, UserMeta.name == "key") | ||
| .one() | ||
| ) | ||
| result = self.sess.query(UserMeta, Group).filter(Group.name == group_name, UserMeta.name == "key").one() | ||
| self.assertTrue( | ||
| [result[k] for k in (UserMeta.name, UserMeta.value, UserMeta.units)] | ||
| == triple | ||
| ) | ||
| self.assertTrue([result[k] for k in (UserMeta.name, UserMeta.value, UserMeta.units)] == triple) | ||
@@ -430,5 +404,3 @@ finally: | ||
| user.metadata["key0"] = iRODSMeta("key0", "value", "units") | ||
| sorted_triples = sorted( | ||
| [["key1", "value0", "units0"], ["key1", "value1", "units1"]] | ||
| ) | ||
| sorted_triples = sorted([["key1", "value0", "units0"], ["key1", "value1", "units1"]]) | ||
| for m in sorted_triples: | ||
@@ -438,21 +410,11 @@ user.metadata.add(iRODSMeta(*m)) | ||
| # general query gives the right results? | ||
| result_0 = ( | ||
| self.sess.query(UserMeta, User) | ||
| .filter(User.name == user_name, UserMeta.name == "key0") | ||
| .one() | ||
| ) | ||
| result_0 = self.sess.query(UserMeta, User).filter(User.name == user_name, UserMeta.name == "key0").one() | ||
| self.assertTrue( | ||
| [result_0[k] for k in (UserMeta.name, UserMeta.value, UserMeta.units)] | ||
| == ["key0", "value", "units"] | ||
| [result_0[k] for k in (UserMeta.name, UserMeta.value, UserMeta.units)] == ["key0", "value", "units"] | ||
| ) | ||
| results_1 = self.sess.query(UserMeta, User).filter( | ||
| User.name == user_name, UserMeta.name == "key1" | ||
| ) | ||
| results_1 = self.sess.query(UserMeta, User).filter(User.name == user_name, UserMeta.name == "key1") | ||
| retrieved_triples = [ | ||
| [res[k] for k in (UserMeta.name, UserMeta.value, UserMeta.units)] | ||
| for res in results_1 | ||
| ] | ||
| retrieved_triples = [[res[k] for k in (UserMeta.name, UserMeta.value, UserMeta.units)] for res in results_1] | ||
@@ -530,5 +492,3 @@ self.assertTrue(sorted_triples == sorted(retrieved_triples)) | ||
| # Generate a random password. | ||
| ga_password = helpers.unique_name( | ||
| helpers.my_function_name(), datetime.datetime.now() | ||
| )[:MAX_PASSWORD_LENGTH] | ||
| ga_password = helpers.unique_name(helpers.my_function_name(), datetime.datetime.now())[:MAX_PASSWORD_LENGTH] | ||
@@ -557,5 +517,3 @@ # Create a groupadmin user with that password, and a session object for logging in as that user. | ||
| alice.modify("password", "apass") | ||
| groupadmin, groupadmin_session = self.create_groupadmin_user_and_session( | ||
| GROUP_ADMIN | ||
| ) | ||
| groupadmin, groupadmin_session = self.create_groupadmin_user_and_session(GROUP_ADMIN) | ||
@@ -581,6 +539,3 @@ # As the groupadmin: | ||
| # Check that our members got removed. | ||
| self.assertFalse( | ||
| set(("alice", GROUP_ADMIN)) | ||
| & set(member.name for member in lab.members) | ||
| ) | ||
| self.assertFalse(set(("alice", GROUP_ADMIN)) & set(member.name for member in lab.members)) | ||
| finally: | ||
@@ -598,5 +553,3 @@ if groupadmin: | ||
| if admin.server_version < (4, 2, 12) or admin.server_version == (4, 3, 0): | ||
| self.skipTest( | ||
| "Password initialization is broken before iRODS 4.2.12, and in 4.3.0" | ||
| ) | ||
| self.skipTest("Password initialization is broken before iRODS 4.2.12, and in 4.3.0") | ||
| rodsuser = groupadmin = None | ||
@@ -607,5 +560,3 @@ rodsuser_name = "bob" | ||
| # Create a groupadmin. | ||
| groupadmin, groupadmin_session = self.create_groupadmin_user_and_session( | ||
| "groupadmin_428" | ||
| ) | ||
| groupadmin, groupadmin_session = self.create_groupadmin_user_and_session("groupadmin_428") | ||
@@ -615,5 +566,3 @@ # Use the groupadmin to create a new user initialized with a known password; then, test the | ||
| with groupadmin_session: | ||
| rodsuser = groupadmin_session.users.create_with_password( | ||
| rodsuser_name, rodsuser_password | ||
| ) | ||
| rodsuser = groupadmin_session.users.create_with_password(rodsuser_name, rodsuser_password) | ||
| with iRODSSession( | ||
@@ -626,5 +575,3 @@ user=rodsuser_name, | ||
| ) as rodsuser_session: | ||
| rodsuser_session.collections.get( | ||
| helpers.home_collection(rodsuser_session) | ||
| ) | ||
| rodsuser_session.collections.get(helpers.home_collection(rodsuser_session)) | ||
| finally: | ||
@@ -722,5 +669,3 @@ if groupadmin: | ||
| ) as user_sess: | ||
| test_object = user_sess.data_objects.create( | ||
| "/tempZone/home/public/bob_file_testing_group_quota" | ||
| ) | ||
| test_object = user_sess.data_objects.create("/tempZone/home/public/bob_file_testing_group_quota") | ||
| with test_object.open("w") as f: | ||
@@ -746,5 +691,3 @@ f.write(b"_" * my_object_size) | ||
| if self.sess.server_version >= (4, 3): | ||
| self.skipTest( | ||
| "iRODS servers 4.3.0 and higher have dropped user quotas in favor of group quotas." | ||
| ) | ||
| self.skipTest("iRODS servers 4.3.0 and higher have dropped user quotas in favor of group quotas.") | ||
| ses = self.sess | ||
@@ -763,5 +706,3 @@ test_object = None | ||
| ) as user_sess: | ||
| test_object = user_sess.data_objects.create( | ||
| "/tempZone/home/public/bobfile" | ||
| ) | ||
| test_object = user_sess.data_objects.create("/tempZone/home/public/bobfile") | ||
| with test_object.open("w") as f: | ||
@@ -786,5 +727,3 @@ f.write(b"_" * 1000) | ||
| with self.assertRaises(client_init.irodsA_already_exists): | ||
| client_init.write_native_credentials_to_secrets_file( | ||
| "somevalue", overwrite=False | ||
| ) | ||
| client_init.write_native_credentials_to_secrets_file("somevalue", overwrite=False) | ||
@@ -791,0 +730,0 @@ # Assert the auth file's contents haven't changed. |
@@ -16,3 +16,2 @@ #! /usr/bin/env python | ||
| class TestRemoteZone(unittest.TestCase): | ||
| def setUp(self): | ||
@@ -37,11 +36,7 @@ self.sess = helpers.make_session() | ||
| for result in session.query(Collection).filter( | ||
| Collection.owner_name == zBuser.name | ||
| and Collection.owner_zone == zBuser.zone | ||
| Collection.owner_name == zBuser.name and Collection.owner_zone == zBuser.zone | ||
| ) | ||
| ] | ||
| self.assertEqual( | ||
| [ | ||
| (u[User.name], u[User.zone]) | ||
| for u in session.query(User).filter(User.zone == A_ZONE_NAME) | ||
| ], | ||
| [(u[User.name], u[User.zone]) for u in session.query(User).filter(User.zone == A_ZONE_NAME)], | ||
| [(A_ZONE_USER, A_ZONE_NAME)], | ||
@@ -63,3 +58,3 @@ ) | ||
| zone = None | ||
| users= [] | ||
| users = [] | ||
| test_zone = "remote_zone" | ||
@@ -70,8 +65,4 @@ # TODO(#763): remove user name randomization. | ||
| zone = self.sess.zones.create(test_zone, "remote") | ||
| users.append( | ||
| self.sess.users.create(test_user, "rodsuser", user_zone=test_zone) | ||
| ) | ||
| users.append( | ||
| self.sess.users.create(test_user, "rodsuser", user_zone="") | ||
| ) | ||
| users.append(self.sess.users.create(test_user, "rodsuser", user_zone=test_zone)) | ||
| users.append(self.sess.users.create(test_user, "rodsuser", user_zone="")) | ||
| self.assertEqual(2, len(list(self.sess.query(User).filter(User.name == test_user)))) | ||
@@ -84,2 +75,3 @@ finally: | ||
| if __name__ == "__main__": | ||
@@ -86,0 +78,0 @@ # let the tests find the parent irods lib |
+62
-24
@@ -1,15 +0,14 @@ | ||
| from irods.api_number import api_number | ||
| from irods.message import iRODSMessage, TicketAdminRequest | ||
| from irods.models import TicketQuery | ||
| import calendar | ||
| import contextlib | ||
| import datetime | ||
| import random | ||
| import string | ||
| import logging | ||
| import datetime | ||
| import calendar | ||
| from typing import Any, Optional, Type, Union # noqa: UP035 | ||
| from irods.api_number import api_number | ||
| from irods.column import Column | ||
| from irods.message import TicketAdminRequest, iRODSMessage | ||
| from irods.models import TicketQuery | ||
| logger = logging.getLogger(__name__) | ||
| def get_epoch_seconds(utc_timestamp): | ||
@@ -31,16 +30,59 @@ epoch = None | ||
| def ticket_iterator(session, filter_args=()): | ||
| """ | ||
| Enumerate the Tickets visible to the user. | ||
| Args: | ||
| session: an iRODSSession object with which to perform a query. | ||
| filter_args: optional arguments for filtering the query. | ||
| Returns: | ||
| An iterator over a range of Ticket objects. | ||
| """ | ||
| return (Ticket(session, result=row) for row in session.query(TicketQuery.Ticket).filter(*filter_args)) | ||
| _COLUMN_KEY = Union[Column, Type[Column]] # noqa: UP006 | ||
| class Ticket: | ||
| def __init__(self, session, ticket="", result=None, allow_punctuation=False): | ||
| def __init__(self, session, ticket="", result: Optional[dict[_COLUMN_KEY, Any]] = None, allow_punctuation=False): # noqa: FA100 | ||
| """ | ||
| Initialize a Ticket object. If no 'result' or 'ticket' string is provided, then generate a new | ||
| Ticket string automatically. | ||
| Args: | ||
| session: an iRODSSession object through which API endpoints shall be called. | ||
| ticket: an optional ticket string, if a particular one is desired for ticket creation or deletion. | ||
| result: a row result from a query, containing at least the columns of irods.models.TicketQuery.Ticket. | ||
| allow_punctuation: True if punctuation characters are to be allowed in generating a Ticket string. | ||
| (By default, all characters will be digits or letters of the latin alphabet.) | ||
| Raises: | ||
| RuntimeError: if the given ticket parameter mismatches the result, or if result is of the wrong type. | ||
| """ | ||
| self._session = session | ||
| # Do an initial error and sanity check on result. | ||
| try: | ||
| if result is not None: | ||
| ticket = result[TicketQuery.Ticket.string] | ||
| except TypeError: | ||
| raise RuntimeError( | ||
| "If specified, 'result' parameter must be a TicketQuery.Ticket search result" | ||
| ) | ||
| self._ticket = ( | ||
| ticket if ticket else self._generate(allow_punctuation=allow_punctuation) | ||
| ) | ||
| _ticket = result[TicketQuery.Ticket.string] | ||
| except (TypeError, KeyError) as exc: | ||
| raise RuntimeError("If specified, 'result' parameter must be a TicketQuery.Ticket query result.") from exc | ||
| # Process query result if given, and set object attributes from it. | ||
| if result is not None: | ||
| if _ticket != ticket != "": | ||
| raise RuntimeError("A ticket name was specified but does not match the query result.") | ||
| ticket = _ticket | ||
| for attr, value in TicketQuery.Ticket.__dict__.items(): | ||
| if value is TicketQuery.Ticket.string: | ||
| continue | ||
| if not attr.startswith("_"): | ||
| # backward compatibility with older schema versions | ||
| with contextlib.suppress(KeyError): | ||
| setattr(self, attr, result[value]) | ||
| self._ticket = ticket if ticket else self._generate(allow_punctuation=allow_punctuation) | ||
| @property | ||
@@ -62,5 +104,3 @@ def session(self): | ||
| source_characters += string.punctuation | ||
| return "".join( | ||
| random.SystemRandom().choice(source_characters) for _ in range(length) | ||
| ) | ||
| return "".join(random.SystemRandom().choice(source_characters) for _ in range(length)) | ||
@@ -75,5 +115,3 @@ def _api_request(self, cmd_string, *args, **opts): | ||
| message_body = TicketAdminRequest(cmd_string, ticket_string, *args, **opts) | ||
| message = iRODSMessage( | ||
| "RODS_API_REQ", msg=message_body, int_info=api_number["TICKET_ADMIN_AN"] | ||
| ) | ||
| message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["TICKET_ADMIN_AN"]) | ||
| conn_.send(message) | ||
@@ -80,0 +118,0 @@ response = conn_.recv() |
+8
-26
@@ -13,3 +13,2 @@ from irods.models import User, Group, UserAuth | ||
| class iRODSUser: | ||
| def remove_quota(self, resource="total"): | ||
@@ -29,8 +28,4 @@ self.manager.remove_quota(self.name, resource=resource) | ||
| self.zone = result[User.zone] | ||
| self._comment = result.get( | ||
| User.comment, _Not_Defined | ||
| ) # these not needed in results for object ident, | ||
| self._info = result.get( | ||
| User.info, _Not_Defined | ||
| ) # so we fetch lazily via a property | ||
| self._comment = result.get(User.comment, _Not_Defined) # these not needed in results for object ident, | ||
| self._info = result.get(User.info, _Not_Defined) # so we fetch lazily via a property | ||
| self._meta = None | ||
@@ -41,5 +36,3 @@ | ||
| if self._comment == _Not_Defined: | ||
| query = self.manager.sess.query(User.id, User.comment).filter( | ||
| User.id == self.id | ||
| ) | ||
| query = self.manager.sess.query(User.id, User.comment).filter(User.id == self.id) | ||
| self._comment = query.one()[User.comment] | ||
@@ -51,5 +44,3 @@ return self._comment | ||
| if self._info == _Not_Defined: | ||
| query = self.manager.sess.query(User.id, User.info).filter( | ||
| User.id == self.id | ||
| ) | ||
| query = self.manager.sess.query(User.id, User.info).filter(User.id == self.id) | ||
| self._info = query.one()[User.info] | ||
@@ -60,5 +51,3 @@ return self._info | ||
| def dn(self): | ||
| query = self.manager.sess.query(UserAuth.user_dn).filter( | ||
| UserAuth.user_id == self.id | ||
| ) | ||
| query = self.manager.sess.query(UserAuth.user_dn).filter(UserAuth.user_id == self.id) | ||
| return [res[UserAuth.user_dn] for res in query] | ||
@@ -69,10 +58,6 @@ | ||
| if not self._meta: | ||
| self._meta = iRODSMetaCollection( | ||
| self.manager.sess.metadata, User, self.name | ||
| ) | ||
| self._meta = iRODSMetaCollection(self.manager.sess.metadata, User, self.name) | ||
| return self._meta | ||
| def modify_password( | ||
| self, old_value, new_value, modify_irods_authentication_file=False | ||
| ): | ||
| def modify_password(self, old_value, new_value, modify_irods_authentication_file=False): | ||
| self.manager.modify_password( | ||
@@ -98,3 +83,2 @@ old_value, | ||
| class iRODSGroup: | ||
| type = "rodsgroup" | ||
@@ -128,5 +112,3 @@ | ||
| if not self._meta: | ||
| self._meta = iRODSMetaCollection( | ||
| self.manager.sess.metadata, User, self.name | ||
| ) | ||
| self._meta = iRODSMetaCollection(self.manager.sess.metadata, User, self.name) | ||
| return self._meta | ||
@@ -133,0 +115,0 @@ |
+1
-1
| import os | ||
| __version__ = "3.2.0" | ||
| __version__ = "3.3.0" | ||
@@ -5,0 +5,0 @@ |
+0
-1
@@ -5,3 +5,2 @@ from irods.models import Zone | ||
| class iRODSZone: | ||
| def __init__(self, manager, result=None): | ||
@@ -8,0 +7,0 @@ """Construct an iRODSZone object.""" |
@@ -5,3 +5,3 @@ CHANGELOG.md | ||
| README.md | ||
| setup.cfg | ||
| pyproject.toml | ||
| setup.py | ||
@@ -67,2 +67,3 @@ irods/__init__.py | ||
| irods/test/cleanup_functions_test.py | ||
| irods/test/client_configuration_test.py | ||
| irods/test/client_hints_test.py | ||
@@ -79,6 +80,8 @@ irods/test/collection_test.py | ||
| irods/test/library_features_test.py | ||
| irods/test/login_auth_test_1.py | ||
| irods/test/login_auth_test_2.py | ||
| irods/test/login_auth_test_must_run_manually.py | ||
| irods/test/message_test.py | ||
| irods/test/meta_test.py | ||
| irods/test/pam_interactive_test.py | ||
| irods/test/pam_interactive_test_must_run_manually.py | ||
| irods/test/pool_test.py | ||
@@ -89,3 +92,3 @@ irods/test/query_test.py | ||
| irods/test/runner.py | ||
| irods/test/setupssl.py | ||
| irods/test/setup_ssl.py | ||
| irods/test/ssl_test_client.py | ||
@@ -103,3 +106,6 @@ irods/test/temp_password_test.py | ||
| irods/test/modules/test_saving_and_loading_of_settings__issue_471.py | ||
| irods/test/modules/test_signal_handling_in_multithread_get.py | ||
| irods/test/modules/test_signal_handling_in_multithread_put.py | ||
| irods/test/modules/test_xml_parser.py | ||
| irods/test/modules/tools.py | ||
| irods/test/test-data/irods_environment.json | ||
@@ -112,2 +118,3 @@ irods/test/test-data/irods_environment_negative_refresh_field.json | ||
| python_irodsclient.egg-info/requires.txt | ||
| python_irodsclient.egg-info/top_level.txt | ||
| python_irodsclient.egg-info/top_level.txt | ||
| venv313/bin/prc_write_irodsA.py |
@@ -0,1 +1,4 @@ | ||
| build | ||
| htmlcov | ||
| irods | ||
| venv313 |
+0
-3
@@ -1,4 +0,1 @@ | ||
| [metadata] | ||
| description_file = README.md | ||
| [egg_info] | ||
@@ -5,0 +2,0 @@ tag_build = |
+1
-41
@@ -1,5 +0,4 @@ | ||
| from setuptools import setup, find_packages | ||
| import codecs | ||
| import os | ||
| from setuptools import setup | ||
@@ -12,43 +11,4 @@ # Get package version | ||
| # Get description | ||
| with codecs.open("README.md", "r", "utf-8") as file: | ||
| long_description = file.read() | ||
| setup( | ||
| name="python-irodsclient", | ||
| version=version["__version__"], | ||
| author="iRODS Consortium", | ||
| author_email="support@irods.org", | ||
| description="A python API for iRODS", | ||
| long_description=long_description, | ||
| long_description_content_type="text/markdown", | ||
| license="BSD", | ||
| url="https://github.com/irods/python-irodsclient", | ||
| keywords="irods", | ||
| classifiers=[ | ||
| "License :: OSI Approved :: BSD License", | ||
| "Development Status :: 5 - Production/Stable", | ||
| "Programming Language :: Python :: 3.9", | ||
| "Programming Language :: Python :: 3.10", | ||
| "Programming Language :: Python :: 3.11", | ||
| "Programming Language :: Python :: 3.12", | ||
| "Programming Language :: Python :: 3.13", | ||
| "Operating System :: POSIX :: Linux", | ||
| ], | ||
| packages=find_packages(), | ||
| include_package_data=True, | ||
| install_requires=[ | ||
| "PrettyTable>=0.7.2", | ||
| "defusedxml", | ||
| "jsonpointer", | ||
| "jsonpatch", | ||
| ], | ||
| extras_require={"tests": ["unittest-xml-reporting", # for xmlrunner | ||
| "types-defusedxml", # for type checking | ||
| "progressbar", # for type checking | ||
| "types-tqdm"] # for type checking | ||
| }, | ||
| scripts=["irods/prc_write_irodsA.py"], | ||
| ) |
| import unittest | ||
| import os | ||
| import json | ||
| from irods.client_init import write_pam_interactive_irodsA_file | ||
| from unittest.mock import patch | ||
| from irods.auth import ClientAuthError, FORCE_PASSWORD_PROMPT | ||
| from irods.auth.pam_interactive import ( | ||
| _pam_interactive_ClientAuthState, | ||
| PERFORM_WAITING, | ||
| PERFORM_AUTHENTICATED, | ||
| PERFORM_NEXT, | ||
| ) | ||
| from irods.test.helpers import make_session | ||
| from irods.auth.pam_interactive import __NEXT_OPERATION__, __FLOW_COMPLETE__ | ||
| from irods.auth.pam_interactive import _auth_api_request | ||
| class PamInteractiveTest(unittest.TestCase): | ||
| def setUp(self): | ||
| # These tests assume the irods_environment.json file is set up correctly | ||
| # and that the iRODS user 'alice' exists in the 'tempZone' zone and has the password 'rods'. | ||
| # There should also be a linux user 'alice' with the same password. | ||
| self.sess = None | ||
| self.auth_client = _pam_interactive_ClientAuthState(None, None, scheme="pam_interactive") | ||
| self.env_file_path = os.path.expanduser("~/.irods/irods_environment.json") | ||
| self.auth_file_path = os.path.expanduser("~/.irods/.irodsA") | ||
| with open(self.env_file_path) as f: | ||
| env = json.load(f) | ||
| self.user = env.get("irods_user_name", "alice") | ||
| self.zone = env.get("irods_zone_name", "tempZone") | ||
| self.password = "rods" | ||
| def tearDown(self): | ||
| if self.sess: | ||
| self.sess.cleanup() | ||
| if os.path.exists(self.auth_file_path): | ||
| os.remove(self.auth_file_path) | ||
| def test_pam_interactive_login_basic(self): | ||
| with patch("getpass.getpass", return_value=self.password): | ||
| self.sess = make_session(test_server_version=False, env_file=self.env_file_path, authentication_scheme="pam_interactive") | ||
| # Creating a session does not trigger auth, so the home collection is accessed to trigger and confirm auth succeeded | ||
| home = self.sess.collections.get(f"/{self.sess.zone}/home/{self.sess.username}") | ||
| self.assertEqual(home.name, self.sess.username) | ||
| def test_pam_interactive_auth_file_creation(self): | ||
| with patch("getpass.getpass", return_value=self.password): | ||
| write_pam_interactive_irodsA_file(env_file=self.env_file_path) | ||
| self.assertTrue(os.path.exists(self.auth_file_path), ".irodsA file was not created") | ||
| with patch("getpass.getpass", return_value=self.password) as mock_getpass: | ||
| self.sess = make_session(test_server_version=False, env_file=self.env_file_path, authentication_scheme= "pam_interactive") | ||
| # Creating a session does not trigger auth, so the home collection is accessed to trigger and confirm auth succeeded | ||
| home = self.sess.collections.get(f"/{self.sess.zone}/home/{self.sess.username}") | ||
| self.assertEqual(home.name, self.sess.username) | ||
| mock_getpass.assert_not_called() | ||
| def test_forced_interactive_flow(self): | ||
| with patch("getpass.getpass", return_value=self.password): | ||
| write_pam_interactive_irodsA_file(env_file=self.env_file_path) | ||
| self.assertTrue(os.path.exists(self.auth_file_path), ".irodsA file was not created") | ||
| with patch("getpass.getpass", return_value=self.password) as mock_getpass: | ||
| self.sess = make_session(test_server_version=False, env_file=self.env_file_path, authentication_scheme="pam_interactive") | ||
| self.sess.set_auth_option_for_scheme("pam_interactive", FORCE_PASSWORD_PROMPT, True) | ||
| # Creating a session does not trigger auth, so the home collection is accessed to trigger and confirm auth succeeded | ||
| home = self.sess.collections.get(f"/{self.sess.zone}/home/{self.sess.username}") | ||
| self.assertEqual(home.name, self.sess.username) | ||
| mock_getpass.assert_called_once() | ||
| def test_failed_login_incorrect_password(self): | ||
| with patch("getpass.getpass", return_value="wrong_password"): | ||
| with self.assertRaises(ClientAuthError): | ||
| self.sess = make_session(test_server_version=False, env_file=self.env_file_path, authentication_scheme="pam_interactive") | ||
| self.sess.collections.get(f"/{self.sess.zone}/home/{self.sess.username}") # trigger auth flow | ||
| with patch("getpass.getpass", return_value="wrong_password"): | ||
| with self.assertRaises(ClientAuthError): | ||
| write_pam_interactive_irodsA_file(env_file=self.env_file_path) | ||
| def test_get_default_value(self): | ||
| test_cases = [ | ||
| ("simple_path", {"msg": {"default_path": "/username"}, "pstate": {"username": "alice"}}, "alice"), | ||
| ("nested_path", {"msg": {"default_path": "/user/name"}, "pstate": {"user": {"name": "alice"}}}, "alice"), | ||
| ("path_does_not_exist", {"msg": {"default_path": "/user/username"}, "pstate": {"username": "alice"}}, ""), | ||
| ("non_string_value", {"msg": {"default_path": "/user/id"}, "pstate": {"user": {"id": 123}}}, "123"), | ||
| ("no_default_path", {"msg": {}, "pstate": {"username": "alice"}}, ""), | ||
| ] | ||
| for name, request, expected in test_cases: | ||
| with self.subTest(name=name): | ||
| self.assertEqual(self.auth_client._get_default_value(request), expected) | ||
| def test_patch_state(self): | ||
| test_cases = [ | ||
| ("add_op", {"msg": {"patch": [{"op": "add", "path": "/username", "value": "alice"}]}, "pstate": {}}, {"username": "alice"}, True), | ||
| ("replace_op", {"msg": {"patch": [{"op": "replace", "path": "/username", "value": "rods"}]}, "pstate": {"username": "alice"}}, {"username": "rods"}, True), | ||
| ("remove_op", {"msg": {"patch": [{"op": "remove", "path": "/username"}]}, "pstate": {"username": "rods"}}, {}, True), | ||
| ("add_resp_fallback", {"msg": {"patch": [{"op": "add", "path": "/username"}]}, "pstate": {}, "resp": "alice"}, {"username": "alice"}, True), | ||
| ("replace_resp_fallback", {"msg": {"patch": [{"op": "replace", "path": "/username"}]}, "pstate": {"username": "rods"}, "resp": "alice"}, {"username": "alice"}, True), | ||
| ("nested_add_operation", {'msg': {'patch': [{'op': 'add', 'path': '/user/name', 'value': 'alice'}]}, 'pstate': {'user': {}}}, {'user': {'name': 'alice'}}, True), | ||
| ("resp_fallback_empty", {'msg': {'patch': [{'op': 'add', 'path': '/username'}]}, 'pstate': {}}, {'username': ''}, True), | ||
| ("no_patch_ops", {"msg": {}, "pstate": {"username": "alice"}, "pdirty": False}, {"username": "alice"}, False) | ||
| ] | ||
| for name, request, expected_pstate, expected_pdirty in test_cases: | ||
| with self.subTest(name=name): | ||
| self.auth_client._patch_state(request) | ||
| self.assertEqual(request["pstate"], expected_pstate) | ||
| self.assertEqual(request["pdirty"], expected_pdirty) | ||
| def test_retrieve_entry(self): | ||
| test_cases = [ | ||
| ("surface_value", {"msg": {"retrieve": "/user"}, "pstate": {"user": "alice"}}, True, "alice"), | ||
| ("nested_value", {"msg": {"retrieve": "/user/password"}, "pstate": {"user": {"password": "rods"}}}, True, "rods"), | ||
| ("empty_value", {"msg": {"retrieve": "/user"}, "pstate": {"user": ""}}, True, ""), | ||
| ("path_does_not_exist", {"msg": {"retrieve": "/missing"}, "pstate": {"user": "alice"}}, True, ""), | ||
| ("non_string_value", {'msg': {'retrieve': '/user/id'}, 'pstate': {'user': {'id': 456}}}, True, '456'), | ||
| ("no_retrieve_key", {"msg": {}, "pstate": {"user": "alice"}}, False, None) | ||
| ] | ||
| for name, request, expected_result, expected_resp in test_cases: | ||
| with self.subTest(name=name): | ||
| self.assertEqual(self.auth_client._retrieve_entry(request), expected_result) | ||
| if expected_result: | ||
| self.assertEqual(request["resp"], expected_resp) | ||
| @patch('irods.auth.pam_interactive._auth_api_request', return_value={"result": "ok"}) | ||
| @patch('sys.stderr.write') | ||
| def test_get_input(self, mock_stderr, mock_api_request): | ||
| test_cases = [ | ||
| ("non_password_input", False, 'sys.stdin.readline', "rods\n", {"msg": {"prompt": "Prompt:"}}, "rods"), | ||
| ("non_password_default", False, 'sys.stdin.readline', "\n", {"msg": {"prompt": "Prompt:", "default_path": "/password"}, "pstate": {"password": "rods"}}, "rods"), | ||
| ("password_input", True, 'getpass.getpass', "rods", {"msg": {"prompt": "Password:"}}, "rods"), | ||
| ("password_default", True, 'getpass.getpass', "", {"msg": {"prompt": "Password:", "default_path": "/password"}, "pstate": {"password": "rods"}}, "rods") | ||
| ] | ||
| for name, is_password, mock_target, user_input, request, expected_resp in test_cases: | ||
| with self.subTest(name=name), patch(mock_target, return_value=user_input), patch.object(self.auth_client, '_patch_state') as mock_patch: | ||
| resp = self.auth_client._get_input(request, is_password=is_password) | ||
| self.assertEqual(request["resp"], expected_resp) | ||
| self.assertEqual(resp, {"result": "ok"}) | ||
| mock_patch.assert_called_once() | ||
| def test_pass_through_states(self): | ||
| with patch("irods.auth.pam_interactive._auth_api_request", return_value={"result": "ok"}): | ||
| request = {"msg": {"prompt": "Prompt:"}, "pstate": {}, "pdirty": False} | ||
| for state in [self.auth_client.next, self.auth_client.running, self.auth_client.ready, self.auth_client.response]: | ||
| with self.subTest(state=state.__name__): | ||
| resp = state(request) | ||
| self.assertEqual(resp, {"result": "ok"}) | ||
| def test_failure_states(self): | ||
| request = {"foo": "bar"} | ||
| with patch("irods.auth.pam_interactive._logger"): | ||
| for state in [self.auth_client.error, self.auth_client.timeout, self.auth_client.not_authenticated]: | ||
| with self.subTest(state=state.__name__): | ||
| resp = state(request) | ||
| self.assertEqual(resp[__NEXT_OPERATION__], __FLOW_COMPLETE__) | ||
| self.assertEqual(self.auth_client.loggedIn, 0) | ||
| @patch("sys.stdin.readline", return_value="ABC123\n") | ||
| def test_pam_interactive_mfa_flow(self, mock_stdin): | ||
| state = {"stage": "before_mfa", "step": 0} | ||
| def mock_server(conn, req): | ||
| # Switch from the real server to the mock server when the password step is completed | ||
| if state["stage"] == "before_mfa": | ||
| resp = _auth_api_request(conn, req) | ||
| if req.get(__NEXT_OPERATION__) == PERFORM_NEXT: # Indicates the password step is complete | ||
| state["stage"] = "mfa_mock" | ||
| return resp | ||
| # MFA simulation steps | ||
| if state["step"] == 0: | ||
| return { | ||
| __NEXT_OPERATION__: PERFORM_WAITING, | ||
| "pstate": {"Password: ": self.password, "verification_code": ""}, | ||
| "msg": { | ||
| "prompt": "Verification Code: ", | ||
| "default_path": "/verification_code", | ||
| "patch": [{"op": "add", "path": "/verification_code"}], | ||
| }, | ||
| "pdirty": True, | ||
| } | ||
| elif state["step"] == 1: | ||
| return { | ||
| __NEXT_OPERATION__: PERFORM_NEXT, | ||
| "pstate": {"Password: ": self.password, "verification_code": ""}, | ||
| "pdirty": True, | ||
| } | ||
| elif state["step"] == 2: | ||
| return { | ||
| __NEXT_OPERATION__: PERFORM_AUTHENTICATED, | ||
| "pstate": {"Password: ": self.password, "verification_code": "ABC123"}, | ||
| "pdirty": True, | ||
| "request_result": "temp_token", | ||
| } | ||
| state["step"] += 1 | ||
| with patch("irods.auth.pam_interactive._auth_api_request", side_effect=mock_server), \ | ||
| patch("getpass.getpass", return_value=self.password), \ | ||
| patch("irods.auth.pam_interactive._authenticate_native") as mock_native: | ||
| self.sess = make_session(test_server_version=False, env_file=self.env_file_path, authentication_scheme="pam_interactive") | ||
| self.sess.server_version # Trigger auth flow | ||
| mock_native.assert_called_once() | ||
| if __name__ == "__main__": | ||
| unittest.main() |
| #!/usr/bin/env python | ||
| import numbers | ||
| import os | ||
| import posix | ||
| import socket | ||
| import shutil | ||
| from subprocess import Popen, PIPE | ||
| import sys | ||
| IRODS_SSL_DIR = "/etc/irods/ssl" | ||
| SERVER_CERT_HOSTNAME = None | ||
| ext = "" | ||
| keep_old = False | ||
| def create_server_cert( | ||
| process_output=sys.stdout, irods_key_path="irods.key", hostname=SERVER_CERT_HOSTNAME | ||
| ): | ||
| p = Popen( | ||
| "openssl req -new -x509 -key '{irods_key_path}' -out irods.crt{ext} -days 365 <<EOF{_sep_}" | ||
| "US{_sep_}North Carolina{_sep_}Chapel Hill{_sep_}UNC{_sep_}RENCI{_sep_}" | ||
| "{host}{_sep_}anon@mail.com{_sep_}EOF\n" | ||
| "".format( | ||
| ext=ext, | ||
| host=(hostname if hostname else socket.gethostname()), | ||
| _sep_="\n", | ||
| **locals() | ||
| ), | ||
| shell=True, | ||
| stdout=process_output, | ||
| stderr=process_output, | ||
| ) | ||
| p.wait() | ||
| return p.returncode | ||
| def create_ssl_dir( | ||
| irods_key_path="irods.key", ssl_dir="", use_strong_primes_for_dh_generation=True | ||
| ): | ||
| ssl_dir = ssl_dir or IRODS_SSL_DIR | ||
| save_cwd = os.getcwd() | ||
| silent_run = {"shell": True, "stderr": PIPE, "stdout": PIPE} | ||
| try: | ||
| if not (os.path.exists(ssl_dir)): | ||
| os.mkdir(ssl_dir) | ||
| os.chdir(ssl_dir) | ||
| if not keep_old: | ||
| Popen( | ||
| "openssl genrsa -out '{irods_key_path}' 2048 && chmod 600 '{irods_key_path}'".format( | ||
| **locals() | ||
| ), | ||
| **silent_run | ||
| ).communicate() | ||
| with open("/dev/null", "wb") as dev_null: | ||
| if 0 == create_server_cert( | ||
| process_output=dev_null, irods_key_path=irods_key_path | ||
| ): | ||
| if not keep_old: | ||
| # https://www.openssl.org/docs/man1.0.2/man1/dhparam.html#:~:text=DH%20parameter%20generation%20with%20the,that%20may%20be%20possible%20otherwise. | ||
| if use_strong_primes_for_dh_generation: | ||
| dhparam_generation_command = ( | ||
| "openssl dhparam -2 -out dhparams.pem" | ||
| ) | ||
| else: | ||
| dhparam_generation_command = ( | ||
| "openssl dhparam -dsaparam -out dhparams.pem 4096" | ||
| ) | ||
| print("cmd=", dhparam_generation_command) | ||
| Popen(dhparam_generation_command, **silent_run).communicate() | ||
| return os.listdir(".") | ||
| finally: | ||
| os.chdir(save_cwd) | ||
| def test(options, args=()): | ||
| if args: | ||
| print("warning: non-option args are ignored", file=sys.stderr) | ||
| force = "-f" in options | ||
| affirm = "n" if (os.path.exists(IRODS_SSL_DIR) and not force) else "y" | ||
| if affirm == "n" and posix.isatty(sys.stdin.fileno()): | ||
| try: | ||
| input_ = raw_input | ||
| except NameError: | ||
| input_ = input | ||
| affirm = input_( | ||
| "This will overwrite directory '{}'. Proceed(Y/N)? ".format(IRODS_SSL_DIR) | ||
| ) | ||
| if affirm[:1].lower() == "y": | ||
| if not keep_old: | ||
| shutil.rmtree(IRODS_SSL_DIR, ignore_errors=True) | ||
| dh_strong_primes = "-q" not in options | ||
| wait_warning = " This may take a while." if dh_strong_primes else "" | ||
| print( | ||
| "Generating new '{}'.{}".format(IRODS_SSL_DIR, wait_warning), | ||
| file=sys.stderr, | ||
| ) | ||
| ssl_dir_files = create_ssl_dir( | ||
| use_strong_primes_for_dh_generation=dh_strong_primes | ||
| ) | ||
| print("ssl_dir_files=", ssl_dir_files, file=sys.stderr) | ||
| def usage(exit_code=None): | ||
| print( | ||
| """Usage: {sys.argv[0]} [-f] [-h <hostname>] [-k] [-q] [-x <extension>] | ||
| -f Force replacement of the existing SSL directory (/etc/irods/ssl) with a new one, containing newly generated files. | ||
| -h In the generated certificate, use the given hostname rather than the value returned from socket.gethostname() | ||
| -k (Keep old secrets files.) Do not generate new key file or dhparams.pem file. | ||
| -q For testing; do a quick generation of a dhparams.pem file rather than waiting on system entropy to make it more secure. | ||
| -x Optional extra extension for appending to end of the filename for the generated certificate. | ||
| --help Print this help. | ||
| Any invalid option prints this help. | ||
| """.format( | ||
| **globals() | ||
| ), | ||
| file=sys.stderr, | ||
| ) | ||
| if isinstance(exit_code, numbers.Integral): | ||
| exit(exit_code) | ||
| if __name__ == "__main__": | ||
| import getopt | ||
| try: | ||
| opt, arg_list = getopt.getopt(sys.argv[1:], "x:fh:kq", ["help"]) | ||
| except getopt.GetoptError: | ||
| usage(exit_code=1) | ||
| opt_lookup = dict(opt) | ||
| if "--help" in opt_lookup: | ||
| usage(exit_code=0) | ||
| ext = opt_lookup.get("-x", "") | ||
| if ext: | ||
| ext = "." + ext.lstrip(".") | ||
| keep_old = opt_lookup.get("-k") is not None | ||
| SERVER_CERT_HOSTNAME = opt_lookup.get("-h") | ||
| test(opt_lookup, arg_list) |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
1316061
9.68%116
7.41%21191
2.37%