Repository: sdss/sdss_access

Issue:
HttpAccess cannot download public file, but RsyncAccess can
**Description**
I can access public data products using `sdss_access.RsyncAccess`, but the same products cannot be downloaded using `sdss_access.HttpAccess`.

**Expected behaviour**
I don't think any authorization should be required (or even checked) for accessing public data products.

**Steps to recreate**
````python
from sdss_access import RsyncAccess, HttpAccess

kwds = dict(
    public=True, 
    release="DR16", 
    verbose=True
)
apstar_kwds = dict(    
    apred="r12",
    apstar="stars",
    telescope="apo25m",
    field="000+14",
    prefix="ap",
    obj="2M16544175-2148453",
)

# Works:
rsync = RsyncAccess(**kwds)
rsync.remote()
rsync.add("apStar", **apstar_kwds)
rsync.set_stream()

rsync.commit()

# Fails:
http = HttpAccess(**kwds)
http.remote() 
http.get("apStar", **apstar_kwds)
````

The stack trace reads:
````python
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-4-1abe27f23dc9> in <module>
     21 # Fails:
     22 http = HttpAccess(**kwds)
---> 23 http.remote()
     24 http.get("apStar", **apstar_kwds)

~/anaconda3/envs/astra/lib/python3.7/site-packages/sdss_access/sync/http.py in remote(self, remote_base, username, password)
     49             self.remote_base = remote_base
     50         self._remote = True
---> 51         self.set_auth(username=username, password=password)
     52         if self.auth.ready():
     53             passman = HTTPPasswordMgrWithDefaultRealm()

~/anaconda3/envs/astra/lib/python3.7/site-packages/sdss_access/sync/http.py in set_auth(self, username, password)
     31         self.auth.set_password(password)
     32         if not self.auth.ready():
---> 33             self.auth.load()
     34 
     35     def remote(self, remote_base=None, username=None, password=None):

~/anaconda3/envs/astra/lib/python3.7/site-packages/sdss_access/sync/auth.py in load(self)
     59 
     60     def load(self):
---> 61         if self.netloc and self.netrc:
     62             authenticators = self.netrc.authenticators(self.netloc)
     63             if authenticators and len(authenticators) == 3:

AttributeError: 'Auth' object has no attribute 'netrc'
````

**Additional context**
The [documentation for `sdss_access.sync.auth.Auth.set_netrc`](https://sdss-access.readthedocs.io/en/latest/api.html?highlight=netrc#sdss_access.sync.auth.Auth.set_netrc) says to add a line to my `~/.netrc` file. I did that (using the correct password) and then a new exception occurs due to permissions. Admittedly the documentation for `set_netrc` *does* tell me to set the permissions correctly, but I would expect no authorization is required -- at all -- for public data.

````python
SDSS_ACCESS> Error NetrcParseError('~/.netrc access too permissive: access permissions must restrict access to only the owner')
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
~/anaconda3/envs/astra/lib/python3.7/io.py in <module>
     21 # Fails:
     22 http = HttpAccess(**kwds)
---> 23 http.remote()
     24 http.get("apStar", **apstar_kwds)

~/anaconda3/envs/astra/lib/python3.7/site-packages/sdss_access/sync/http.py in remote(self, remote_base, username, password)
     49             self.remote_base = remote_base
     50         self._remote = True
---> 51         self.set_auth(username=username, password=password)
     52         if self.auth.ready():
     53             passman = HTTPPasswordMgrWithDefaultRealm()

~/anaconda3/envs/astra/lib/python3.7/site-packages/sdss_access/sync/http.py in set_auth(self, username, password)
     31         self.auth.set_password(password)
     32         if not self.auth.ready():
---> 33             self.auth.load()
     34 
     35     def remote(self, remote_base=None, username=None, password=None):

~/anaconda3/envs/astra/lib/python3.7/site-packages/sdss_access/sync/auth.py in load(self)
     59 
     60     def load(self):
---> 61         if self.netloc and self.netrc:
     62             authenticators = self.netrc.authenticators(self.netloc)
     63             if authenticators and len(authenticators) == 3:

AttributeError: 'Auth' object has no attribute 'netrc'
````

I am using the the [bleeding edge](https://github.com/sdss/sdss_access/commit/f949ec106d25bc504f40753812d2c4c8e7fbf9ee) version of `sdss_access`.

Comments:

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 9632a9f..8636412 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -6,6 +6,13 @@ Change Log
 
 This document records the main changes to the sdss_access code.
 
+1.0.1 (unreleased)
+------------------
+
+- Bug Fix :issue:`16` - HttpAccess used in public mode checks for netrc file
+- Combines separate `set_auth` methods in `BaseAccess` and `HttpAccess` into a single `set_auth` available as `AuthMixin`
+- `Auth.set_netrc` now raises an `AccessError` on failure to find a value netrc file.
+
 1.0.0 (2020-05-07)
 ------------------
 
diff --git a/python/sdss_access/sync/__init__.py b/python/sdss_access/sync/__init__.py
index ddf2c1f..7519b10 100644
--- a/python/sdss_access/sync/__init__.py
+++ b/python/sdss_access/sync/__init__.py
@@ -1,4 +1,4 @@
-from .auth import Auth
+from .auth import Auth, AuthMixin
 from .cli import Cli
 from .stream import Stream
 from .http import HttpAccess
diff --git a/python/sdss_access/sync/auth.py b/python/sdss_access/sync/auth.py
index deb281c..91dfbbe 100644
--- a/python/sdss_access/sync/auth.py
+++ b/python/sdss_access/sync/auth.py
@@ -1,15 +1,19 @@
 from __future__ import absolute_import, division, print_function, unicode_literals
 from six.moves import input
 from sdss_access import is_posix
+from sdss_access.path import AccessError
 from os.path import join
 from os import environ
 
 
 class Auth(object):
+    ''' class for setting up SAS authenticaton for SDSS users '''
 
     def __init__(self, netloc=None, public=False, verbose=False):
         self.public = public
         self.verbose = verbose
+        self.username = None
+        self.password = None
         self.set_netloc(netloc=netloc)
         self.set_netrc()
 
@@ -24,52 +28,109 @@ class Auth(object):
             Windows: recommending _netrc following
             https://stackoverflow.com/questions/6031214/git-how-to-use-netrc-file-on-windows-to-save-user-and-password
         """
+        # set blank netrc
+        self.netrc = None
+
+        # if public do nothing
+        if self.public:
+            return
+
+        # try to get the netrc file
         try:
             from netrc import netrc
         except Exception as e:
             netrc = None
             if self.verbose:
-                print("SDSS_ACCESS> AUTH NETRC: %r" % e)
+                print("SDSS_ACCESS> AUTH NETRC: {0}".format(e))
+
         if netrc:
-            file = join(environ['HOME'], "_netrc") if not is_posix else None
+            netfile = join(environ['HOME'], "_netrc") if not is_posix else None
             try:
-                self.netrc = netrc(file) if not self.public else None
-            except Exception as e:
-                print("SDSS_ACCESS> Error %r" % e)
-        else:
-            self.netrc = None
+                self.netrc = netrc(netfile)
+            except FileNotFoundError as e:
+                raise AccessError("No netrc file found. Please create one. {0}".format(e))
 
     def set_netloc(self, netloc=None):
+        ''' sets the url domain location '''
         self.netloc = netloc
 
-    def set_username(self, username=None, inquire=False):
+    def set_username(self, username=None, inquire=None):
+        ''' sets the authentication username
+
+        Parameters:
+            username: str
+                The username for SDSS SAS access
+            inquire: bool
+                If True, prompts for input of username.
+        '''
         self.username = input("user [sdss]: ") or "sdss" if inquire else username
 
-    def set_password(self, password=None, inquire=False):
+    def set_password(self, password=None, inquire=None):
+        ''' sets the authentication password
+
+        Parameters:
+            password: str
+                The password for SDSS SAS access
+            inquire: bool
+                If True, prompts for input of password.
+        '''
         try:
             from getpass import getpass
             self.password = getpass("password: ") if inquire else password
         except Exception as e:
             if self.verbose:
-                print("SDSS_ACCESS> AUTH PASSWORD: %r" % e)
+                print("SDSS_ACCESS> AUTH PASSWORD: {0}".format(e))
             self.password = None
 
     def ready(self):
         return self.username and self.password
 
     def load(self):
+        ''' Sets the username and password from the local netrc file '''
         if self.netloc and self.netrc:
             authenticators = self.netrc.authenticators(self.netloc)
             if authenticators and len(authenticators) == 3:
                 self.set_username(authenticators[0])
                 self.set_password(authenticators[2])
                 if self.verbose:
-                    print("authentication for netloc=%r set for username=%r " % (self.netloc, self.username))
+                    print("authentication for netloc={0} set for username={1}".format(self.netloc, self.username))
             else:
                 if self.verbose:
-                    print("cannot find %r in ~/.netrc" % self.netloc)
+                    print("cannot find {0} in ~/.netrc".format(self.netloc))
                 self.set_username()
                 self.set_password()
         else:
             self.set_username()
             self.set_password()
+
+
+class AuthMixin(object):
+    ''' Mixin class to provide authentication method to other classes '''
+
+    def set_auth(self, username=None, password=None, inquire=True):
+        ''' Set the authentication
+
+        Parameters:
+            username: str
+                The username for SDSS SAS access
+            password: str
+                The password for SDSS SAS access
+            inquire: bool
+                If True, prompts for input of username/password.
+        '''
+        self.auth = Auth(public=self.public, netloc=self.netloc, verbose=self.verbose)
+        self.auth.set_username(username)
+        self.auth.set_password(password)
+
+        # if public then exit
+        if self.public:
+            return
+
+        # try to load the username and password
+        if not self.auth.ready():
+            self.auth.load()
+
+        # if still not ready then prompt for username and password
+        if not self.auth.ready():
+            self.auth.set_username(inquire=inquire)
+            self.auth.set_password(inquire=inquire)
diff --git a/python/sdss_access/sync/baseaccess.py b/python/sdss_access/sync/baseaccess.py
index f6f2878..06a7553 100644
--- a/python/sdss_access/sync/baseaccess.py
+++ b/python/sdss_access/sync/baseaccess.py
@@ -5,12 +5,12 @@ import abc
 import six
 from os.path import join, sep
 from sdss_access import SDSSPath
-from sdss_access.sync.auth import Auth
+from sdss_access.sync.auth import Auth, AuthMixin
 from sdss_access.sync.stream import Stream
 from sdss_access import is_posix, AccessError
 
 
-class BaseAccess(six.with_metaclass(abc.ABCMeta, SDSSPath)):
+class BaseAccess(six.with_metaclass(abc.ABCMeta, AuthMixin, SDSSPath)):
     """Class for providing Rsync or Curl access to SDSS SAS Paths
     """
     remote_scheme = None
@@ -116,18 +116,6 @@ class BaseAccess(six.with_metaclass(abc.ABCMeta, SDSSPath)):
         stream = Stream(stream_count=self.stream_count, verbose=self.verbose)
         return stream
 
-    def set_auth(self, username=None, password=None, inquire=True):
-        ''' set the authentication for accessing data '''
-        self.auth = Auth(public=self.public, netloc=self.netloc, verbose=self.verbose)
-        self.auth.set_username(username)
-        self.auth.set_password(password)
-        if not self.public:
-            if not self.auth.ready():
-                self.auth.load()
-            if not self.auth.ready():
-                self.auth.set_username(inquire=inquire)
-                self.auth.set_password(inquire=inquire)
-
     def reset(self):
         ''' Reset all streams '''
 
diff --git a/python/sdss_access/sync/http.py b/python/sdss_access/sync/http.py
index 5b3300a..3637607 100644
--- a/python/sdss_access/sync/http.py
+++ b/python/sdss_access/sync/http.py
@@ -12,26 +12,19 @@ except:
 from os import makedirs
 from os.path import isfile, exists, dirname
 from sdss_access import SDSSPath
-from sdss_access.sync.auth import Auth
+from sdss_access.sync.auth import Auth, AuthMixin
 
 
-class HttpAccess(SDSSPath):
+class HttpAccess(AuthMixin, SDSSPath):
     """Class for providing HTTP access via urllib.request (python3) or urllib2 (python2) to SDSS SAS Paths
     """
 
-    def __init__(self, verbose=False, public=False, release=None, label='sdss_http'):
+    def __init__(self, verbose=None, public=None, release=None, label='sdss_http'):
         super(HttpAccess, self).__init__(public=public, release=release, verbose=verbose)
         self.verbose = verbose
         self.label = label
         self._remote = False
 
-    def set_auth(self, username=None, password=None):
-        self.auth = Auth(netloc=self.netloc, verbose=self.verbose)
-        self.auth.set_username(username)
-        self.auth.set_password(password)
-        if not self.auth.ready():
-            self.auth.load()
-
     def remote(self, remote_base=None, username=None, password=None):
         """
         Configures remote access


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 8d90a5e..a08ccca 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -232,3 +232,10 @@ def gzcompress(filename):
             shutil.copyfileobj(f_in, f_out)
     os.remove(filename)
     yield
+
+
+@pytest.fixture()
+def monkeyhome(monkeypatch, tmp_path):
+    ''' monkeypatch the HOME directory '''
+    path = (tmp_path / 'tmp').mkdir()
+    monkeypatch.setenv("HOME", str(path))
diff --git a/tests/sync/test_auth.py b/tests/sync/test_auth.py
index a4375d9..4a301fb 100644
--- a/tests/sync/test_auth.py
+++ b/tests/sync/test_auth.py
@@ -9,18 +9,103 @@
 
 from __future__ import print_function, division, absolute_import
 
+import importlib
+import six.moves
 import pytest
 
-from sdss_access.sync.auth import Auth
+import sdss_access
+from sdss_access.sync.auth import Auth, AuthMixin
+from sdss_access.path import AccessError
 
 
-@pytest.mark.skip
-def test_set_username_interactively():
-    """If authentication via `.netrc` file fails, then test for interactive prompt.
+class TestAuth(object):
 
-    The interactive prompt raises an IOError (merged into OSError in Python 3.3), which this test
-    expects.
-    """
-    with pytest.raises((IOError, OSError)):
-        auth = Auth(netloc='data.sdss.org', public=False)
+    def test_nonetrc_fails(self, monkeyhome):
+        ''' test raise error when no netrc present '''
+        with pytest.raises(AccessError) as cm:
+            Auth()
+        assert 'No netrc file found. Please create one.' in str(cm.value)
+
+    def test_nonetrc_public_pass(self, monkeyhome):
+        ''' test public access does not fail when no netrc '''
+        auth = Auth(public=True)
+        assert auth.public is True
+        assert auth.ready() is None
+        assert auth.netrc is None
+
+    @pytest.mark.parametrize('netloc', [('data.sdss.org'), ('dtn01.sdss.org')])
+    def test_setnetloc(self, netloc):
+        ''' test setting a url domain location '''
+        auth = Auth(public=True, netloc=netloc)
+        assert auth.netloc == netloc
+
+    def test_auth_goodnetrc(self):
+        ''' test for a netrc loaded '''
+        auth = Auth()
+        assert auth.netrc is not None
+        assert 'data.sdss.org' in auth.netrc.hosts.keys()
+
+    @pytest.mark.parametrize('public', [(True), (False)])
+    def test_auth_defaultuser(self, public):
+        ''' test auth load default user '''
+        auth = Auth(netloc='data.sdss.org', public=public)
+        auth.load()
+        if public:
+            assert auth.username is None
+        else:
+            assert auth.username == auth.netrc.authenticators('data.sdss.org')[0]
+
+    def test_auth_setuserpass(self):
+        ''' test set userpass '''
+        auth = Auth(netloc='data.sdss.org')
+        assert auth.username is None
+        assert auth.password is None
+        auth.set_username('testuser')
+        auth.set_password('testpass')
+        assert auth.username == 'testuser'
+        assert auth.password == 'testpass'
+
+    @pytest.mark.parametrize('prompt, exp', [('blah', 'blah'), (None, 'sdss')])
+    def test_user_inquire(self, monkeypatch, prompt, exp):
+        ''' test username input prompt '''
+        # mock the six.moves.input
+        def mockinput(value):
+            return prompt
+
+        monkeypatch.setattr(six.moves, 'input', mockinput)
+        # reload the auth module
+        importlib.reload(sdss_access.sync.auth)
+        from sdss_access.sync.auth import Auth
+
+        # run the test
+        auth = Auth()
         auth.set_username(inquire=True)
+        assert auth.username == exp
+
+
+class TmpAccess(AuthMixin, object):
+    def __init__(self, public=None, netloc=None, verbose=None):
+        self.public = public
+        self.netloc = netloc
+        self.verbose = verbose
+
+
+class TestAuthMixin(object):
+
+    def test_set_auth(self):
+        ''' test the set_auth method '''
+        ta = TmpAccess(netloc='data.sdss.org')
+        ta.set_auth()
+        assert ta.netloc == 'data.sdss.org'
+        assert ta.auth is not None
+        assert ta.auth.netloc == 'data.sdss.org'
+        assert ta.auth.username == 'sdss'
+
+    def test_set_auth_public(self):
+        ''' test the public option '''
+        ta = TmpAccess(netloc='data.sdss.org', public=True)
+        ta.set_auth()
+        assert ta.auth is not None
+        assert ta.auth.public is True
+        assert ta.auth.username is None
+
diff --git a/tests/sync/test_http.py b/tests/sync/test_http.py
index 6e2798f..7818048 100644
--- a/tests/sync/test_http.py
+++ b/tests/sync/test_http.py
@@ -14,7 +14,7 @@
 from __future__ import print_function, division, absolute_import
 import os
 import pytest
-from sdss_access import tree
+from sdss_access import tree, AccessError
 from sdss_access.sync import HttpAccess
 
 
@@ -57,3 +57,18 @@ class TestHttp(object):
                  mangaid='1-42007', remote=True)
 
         assert os.path.exists(full)
+
+    def test_nonetrc_fails(self, monkeyhome):
+        ''' test raise error when no netrc present '''
+        with pytest.raises(AccessError) as cm:
+            http = HttpAccess()
+            http.remote()
+        assert 'No netrc file found. Please create one.' in str(cm.value)
+
+    def test_nonetrc_public_pass(self, monkeyhome):
+        ''' test public access does not fail when no netrc '''
+        http = HttpAccess(release='DR14')
+        http.remote()
+        assert http.public is True
+        assert http.auth.username is None
+        assert http.auth.ready() is None