From ef05f6a888660bb4c28517db64f1cee89255d7f6 Mon Sep 17 00:00:00 2001
From: k1o0 <k1o0@3tk.co>
Date: Tue, 8 Feb 2022 18:44:44 +0200
Subject: [PATCH] Update FAQ; version var; check min api version; lazy auth
 (#36)

---
 CHANGELOG.md             | 11 ++++++++++-
 docs/FAQ.md              | 31 +++++++++++++++++++++++++++++++
 one/__init__.py          |  1 +
 one/api.py               | 10 +++++++++-
 one/params.py            | 10 ++++++++--
 one/tests/test_one.py    |  7 +++++++
 one/tests/test_params.py | 11 ++++++++++-
 one/webclient.py         |  7 ++++---
 requirements.txt         |  1 +
 setup.py                 | 13 ++++++++++++-
 10 files changed, 93 insertions(+), 9 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 31c91c4..6fbf090 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 2b1ad4c..2686077 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 21b29ce..b493917 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 41550fa..2bd900f 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 1cb00c0..4661b27 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 b4e1f31..858ada9 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 0bcf1b0..b6b168a 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 84db033..800761c 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 d39688d..070babb 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 2626b28..3750d92 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",
-- 
GitLab