diff --git a/CHANGELOG.md b/CHANGELOG.md index 31c91c4db36b604ccedf899ba93d757ae630ffc2..6fbf09062d9d659e52022d139379a8138fda4857 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,22 @@ # Changelog ## [Latest](https://github.com/int-brain-lab/ONE/commits/main) [1.8.0] +### Added + +- added `from_df` method to one.alf.io.AlfBunch +- added `__version__` variable +- added check for remote cache minimum API version +- user prompted to verify settings correct in setup + ### Modified - datasets cache table expected to have index of (eid, id). NB: This changes the order of datasets returned by some functions - multithreading moved from One._download_datasets to one.webclient.http_download_file_list - cache_dir kwarg renamed to target_dir in one.webclient.http_download_file - 'table' attribute now split into columns and merged -- added `from_df` method to one.alf.io.AlfBunch +- when no username or password provided to constructor, AlyxClient init doesn't call authenticate +- 'stay_logged_in' kwarg removed from AlyxClient constructor; must manually call `authenticate` or remember to call `logout` +- user prompted whether to make url default in setup even if default already set ## [1.7.1] diff --git a/docs/FAQ.md b/docs/FAQ.md index 2b1ad4ced5cc0be2339747c845277ad00368f6de..268607765556bd1615a0f231405630aca2d6379a 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -56,3 +56,34 @@ called. To avoid this, run the following command instead: from one.api import OneAlyx new_one = OneAlyx.setup(base_url='https://alyx.example.com') ``` + +## How do I change my download (a.k.a. cache) directory? +For one-time changes, simply re-run the setup routine: +```python +from one.api import ONE +new_one = ONE().setup() # Re-run setup for default database +``` +When prompted ('Enter the location of the download cache') enter the absolute path of the new download location. + +## How do check who I'm logged in as? +```python +from one.api import ONE +one = ONE() +if not one.offline: + print(one.alyx.user) + print(one.alyx.base_url) +``` + +## How do I log out, or temporarily log in as someone else? +To log out: +```python +from one.api import ONE +one = ONE() + +one.alyx.logout() +``` + +To log in as someone else temporarily: +```python +one.alyx.authenticate(username='other_user', cache_token=False, force=True) +``` diff --git a/one/__init__.py b/one/__init__.py index 21b29cefa23ec6032ae96203f36ff2b65b0fe488..b4939177112ef32fe70b66f534b6c178c73dd29d 100644 --- a/one/__init__.py +++ b/one/__init__.py @@ -1 +1,2 @@ """The Open Neurophysiology Environment (ONE) API""" +__version__ = '1.8.0' diff --git a/one/api.py b/one/api.py index 41550fae86365e270a5193707b719e375bd69ebc..2bd900fd400016fdf11eb30a2c29a92438f4b55e 100644 --- a/one/api.py +++ b/one/api.py @@ -2,7 +2,6 @@ Things left to complete: - - TODO Add sig to ONE Light uuids. - TODO Save changes to cache. - TODO Fix update cache in AlyxONE - save parquet table. - TODO save parquet in update_filesystem. @@ -10,6 +9,7 @@ Things left to complete: import collections.abc import warnings import logging +import packaging.version from datetime import datetime, timedelta from functools import lru_cache, partial from inspect import unwrap @@ -1235,6 +1235,14 @@ class OneAlyx(One): try: # Determine whether a newer cache is available cache_info = self.alyx.get('cache/info', expires=True) + + # Check version compatibility + min_version = packaging.version.parse(cache_info.get('min_api_version', '0.0.0')) + if packaging.version.parse(one.__version__) < min_version: + warnings.warn(f'Newer cache tables require ONE version {min_version} or greater') + return + + # Check whether remote cache more recent remote_created = datetime.fromisoformat(cache_info['date_created']) local_created = cache_meta.get('created_time', None) if local_created and (remote_created - local_created) < timedelta(minutes=1): diff --git a/one/params.py b/one/params.py index 1cb00c03df08727181f634ed40a7faa1a5ede70d..4661b274e2482fbe53c63f17dfad4232569bb87f 100644 --- a/one/params.py +++ b/one/params.py @@ -161,9 +161,15 @@ def setup(client=None, silent=False, make_default=None): break cache_dir = input(prompt) or cache_dir # Prompt for another directory - if make_default is None and 'DEFAULT' not in cache_map.as_dict(): + if make_default is None: answer = input('Would you like to set this URL as the default one? [Y/n]') - make_default = True if not answer or answer[0].lower() == 'y' else False + make_default = (answer or 'y')[0].lower() == 'y' + + # Verify setup pars + answer = input('Are the above settings correct? [Y/n]') + if answer and answer.lower()[0] == 'n': + print('SETUP ABANDONED. Please re-run.') + return par_current else: par = par_current diff --git a/one/tests/test_one.py b/one/tests/test_one.py index b4e1f3114e875f7224ddbbc32e1ec0d75b489608..858ada952c0b83553c0eac89c862d612c5394046 100644 --- a/one/tests/test_one.py +++ b/one/tests/test_one.py @@ -936,6 +936,13 @@ class TestOneAlyx(unittest.TestCase): self.one._load_cache(clobber=True) self.assertEqual('local', self.one.mode) self.assertTrue('Failed to connect' in lg.output[-1]) + + cache_info = {'min_api_version': '200.0.0'} + # Check version verification + with mock.patch.object(self.one.alyx, 'get', return_value=cache_info),\ + self.assertWarns(UserWarning): + self.one._load_cache(clobber=True) + finally: # Restore properties self.one.mode = 'auto' self.one.alyx.silent = True diff --git a/one/tests/test_params.py b/one/tests/test_params.py index 0bcf1b0ada2ca16f9de6005cbeca0fc2409a3682..b6b168aa8f6516bf00b3fd39d8958da8158fc968 100644 --- a/one/tests/test_params.py +++ b/one/tests/test_params.py @@ -26,13 +26,16 @@ class TestParamSetup(unittest.TestCase): self.get_file_mock.start() self.addCleanup(self.get_file_mock.stop) - def _mock_input(self, prompt): + def _mock_input(self, prompt, **kwargs): """Stub function for builtins.input""" if prompt.lower().startswith('warning'): return 'n' elif 'url' in prompt.lower(): return self.url else: + for k, v in kwargs.items(): + if k in prompt: + return v return '' @mock.patch('one.params.getpass', return_value='mock_pwd') @@ -51,6 +54,12 @@ class TestParamSetup(unittest.TestCase): self.assertEqual('https://' + self.url, par.ALYX_URL) self.assertEqual('mock_pwd', par.HTTP_DATA_SERVER_PWD) + # Check verification prompt + resp_map = {'ALYX_LOGIN': 'mistake', 'settings correct?': 'N'} + with mock.patch('one.params.input', new=partial(self._mock_input, **resp_map)): + cache = one.params.setup() + self.assertNotEqual(cache.ALYX_LOGIN, 'mistake') + # Check that raises ValueError when bad URL provided self.url = 'ftp://' with self.assertRaises(ValueError), mock.patch('one.params.input', new=self._mock_input): diff --git a/one/webclient.py b/one/webclient.py index 84db03367f0b579fd4cdc6743378095920faef08..800761c2e78b8bcbe82a4228d5fc4e2825e3c9fc 100644 --- a/one/webclient.py +++ b/one/webclient.py @@ -471,7 +471,7 @@ class AlyxClient(): base_url = None def __init__(self, base_url=None, username=None, password=None, - cache_dir=None, silent=False, cache_rest='GET', stay_logged_in=True): + cache_dir=None, silent=False, cache_rest='GET'): """ Create a client instance that allows to GET and POST to the Alyx server. For One, constructor attempts to authenticate with credentials in params.py. @@ -498,10 +498,11 @@ class AlyxClient(): self._par = one.params.get(client=base_url, silent=self.silent) self.base_url = base_url or self._par.ALYX_URL self._par = self._par.set('CACHE_DIR', cache_dir or self._par.CACHE_DIR) - self.authenticate(username, password, cache_token=stay_logged_in) + if username or password: + self.authenticate(username, password) self._rest_schemes = None # the mixed accept application may cause errors sometimes, only necessary for the docs - self._headers['Accept'] = 'application/json' + self._headers = {**(self._headers or {}), 'Accept': 'application/json'} # REST cache parameters # The default length of time that cache file is valid for, # The default expiry is overridden by the `expires` kwarg. If False, the caching is diff --git a/requirements.txt b/requirements.txt index d39688db45e85029b6886435e49fa142f6f3fdaa..070babb2c35e3c0ef118a1e6e305d93876875232 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ pandas>=1.2.4 tqdm>=4.32.1 requests>=2.22.0 iblutil +packaging diff --git a/setup.py b/setup.py index 2626b285d412c107d8347bf60f4a424c0f1fa006..3750d929fcc76c277398ba11284201746ca11d34 100644 --- a/setup.py +++ b/setup.py @@ -22,9 +22,20 @@ with open('README.md', 'r') as f: with open('requirements.txt') as f: require = [x.strip() for x in f.readlines() if not x.startswith('git+')] + +def get_version(rel_path): + here = Path(__file__).parent.absolute() + with open(here.joinpath(rel_path), 'r') as fp: + for line in fp.read().splitlines(): + if line.startswith('__version__'): + delim = '"' if '"' in line else "'" + return line.split(delim)[1] + raise RuntimeError('Unable to find version string.') + + setup( name='ONE-api', - version='1.8.0', + version=get_version(Path('one', '__init__.py')), python_requires='>={}.{}'.format(*REQUIRED_PYTHON), description='Open Neurophysiology Environment', license="MIT",