Repository: TACC/agavepy

Issue:
Implement a method to subscribe oauth clients to apis
Currently, there is no way to use `AgavePy` to subscribe oauth clients to services. 
`TACC-Cloud` requires this functionality.

Discussion:

Patch:
diff --git a/agavepy/agave.py b/agavepy/agave.py
index 6245c49..3ade07f 100644
--- a/agavepy/agave.py
+++ b/agavepy/agave.py
@@ -20,7 +20,8 @@ import dateutil.parser
 import requests
 
 from agavepy.tenants import tenant_list
-from agavepy.clients import clients_create, clients_delete, clients_list
+from agavepy.clients import (clients_create, clients_delete, clients_list, 
+    clients_subscribe)
 from agavepy.tokens import token_create, refresh_token
 from agavepy.utils import load_config, save_config
 from agavepy.files import (files_copy, files_delete, files_download, 
@@ -683,6 +684,23 @@ class Agave(object):
             self.api_key, self.api_secret = "", ""
 
 
+    def clients_subscribe(self, api_name, api_version, api_provider, client_name=None):
+        """ Subscribe the oauth client to an api
+        """
+        # Set username.
+        if self.username == "" or self.username is None:
+            self.username = input("API username: ")
+
+        # If client_name is not set, then delete the current client, if it
+        # exists.
+        if client_name is None:
+            client_name = self.client_name
+
+        # Subscribe client.
+        clients_subscribe(self.username, client_name, self.api_server, 
+            api_name, api_version, api_provider)
+
+
     def clients_list(self):
         """ List all oauth clients
         """
diff --git a/agavepy/clients/__init__.py b/agavepy/clients/__init__.py
index cfa3ed8..7e2aedf 100644
--- a/agavepy/clients/__init__.py
+++ b/agavepy/clients/__init__.py
@@ -1,3 +1,4 @@
 from .create import clients_create
 from .delete import clients_delete
 from .list import clients_list
+from .subscribe import clients_subscribe
diff --git a/agavepy/clients/subscribe.py b/agavepy/clients/subscribe.py
new file mode 100644
index 0000000..0e21f6d
--- /dev/null
+++ b/agavepy/clients/subscribe.py
@@ -0,0 +1,41 @@
+"""
+    subscribe.py
+
+Subscribe to TACC apis
+"""
+import getpass
+import requests                                                                 
+from .exceptions import AgaveClientError                                        
+from ..utils import handle_bad_response_status_code
+
+
+
+def clients_subscribe(username, client_name, tenant_url, 
+        api_name, api_version, api_provider):
+    """ Subscribe an oauth client to an api
+    """
+    # Set request endpoint.
+    endpoint = "{}/clients/v2/{}/subscriptions".format(tenant_url, client_name)
+
+    # Get user's password.
+    passwd = getpass.getpass(prompt="API password: ")
+
+    # Make sure client_name is valid.
+    if client_name == "" or client_name is None:
+        raise AgaveClientError("Error creating client: invalid client_name")
+
+    # Make request.
+    try:
+        data = {
+            "apiName": api_name,
+            "apiVersion": api_version,
+            "apiProvider": api_provider,
+        }
+        resp = requests.post(endpoint, data=data, auth=(username, passwd))
+        del passwd
+    except Exception as err:
+        del passwd
+        raise AgaveClientError(err)
+
+    # Handle bad status code.
+    handle_bad_response_status_code(resp)
diff --git a/docs/docsite/authentication/clients.rst b/docs/docsite/authentication/clients.rst
index b3b4402..96d8d85 100644
--- a/docs/docsite/authentication/clients.rst
+++ b/docs/docsite/authentication/clients.rst
@@ -61,3 +61,17 @@ If you want to delete an oauth client, you can do as such:
 
 If you don't pass a client name to ``clients_delete``, then the ``Agave``
 object will try to delete the oauth client in its current session.
+
+
+Subscribing to an API
+#####################
+
+If you want to subscribe an oauth client to a TACC api, then you can do so as
+follows:
+
+.. code-block:: pycon
+
+    >>> ag.clients_subscribe("PublicKeys", "v2", "admin", client_name="client")
+
+If you don't set the ``client_name`` argument then the ``ag`` object will try
+to use the oauth client stored in your current session.


Test patch:
diff --git a/tests/clients_test.py b/tests/clients_test.py
index 729f9ce..88aa14a 100644
--- a/tests/clients_test.py
+++ b/tests/clients_test.py
@@ -48,7 +48,7 @@ class MockServerClientEndpoints(BaseHTTPRequestHandler):
 
 
     def do_POST(self):
-        """ Mock agave client creation
+        """ Mock agave client creation and subscription
         """
         # Check that basic auth is used.
         authorization = self.headers.get("Authorization")
@@ -63,35 +63,37 @@ class MockServerClientEndpoints(BaseHTTPRequestHandler):
             headers = self.headers,
             environ={'REQUEST_METHOD': 'POST'})
 
-        # Check client name is set.
+        # Check parameters for client creation.
         client_name = form.getvalue("clientName", "")
-        if client_name == "":
-            self.send_response(400)
-            self.end_headers()
-            return
-
-        # Check client description is set.
         client_description = form.getvalue("description", "")
-        if client_description == "":
-            self.send_response(400)
-            self.end_headers()
-            return
+        client_tier = form.getvalue("tier", "")
+        creation = True
+        if client_name == "" or client_description == "" or client_tier == "":
+            creation = False
 
-        # CHeck client tier is set.
-        if form.getvalue("tier", "") == "":
-            self.send_response(400)
-            self.end_headers()
-            return
-
-        # Update response fields.
+        # Update response fields for client creation.
         sample_client_create_response["result"]["name"] = client_name
         sample_client_create_response["result"]["description"] = client_description
         sample_client_create_response["result"]["consumerKey"] = "some api key"
         sample_client_create_response["result"]["consumerSecret"] = "some secret"
 
-        self.send_response(200)
-        self.end_headers()
-        self.wfile.write(json.dumps(sample_client_create_response).encode())
+        # Check parameters for client subscription.
+        api_name = form.getvalue("apiName", "")
+        api_version = form.getvalue("apiVersion", "")
+        api_provider = form.getvalue("apiProvider", "")
+        subscription = True
+        if api_name == "" or api_version == "" or api_provider == "":
+            subscription = False
+
+        if creation or subscription:
+            self.send_response(200)
+            self.end_headers()
+            if creation:
+                self.wfile.write(json.dumps(sample_client_create_response).encode())
+        else:
+            self.send_response(400)
+            self.end_headers()
+        return
 
 
     def do_DELETE(self):
@@ -198,3 +200,25 @@ class TestMockServer(MockServer):
         out, err = capfd.readouterr()
         assert "DefaultApplication" in out
         assert "\"GET /clients/v2 HTTP/1.1\" 200" in err 
+
+
+    @patch("agavepy.agave.input")
+    @patch("agavepy.clients.subscribe.getpass.getpass")
+    def test_clients_subscribe(self, mock_input, mock_pass, capfd):
+        """ Test clients subscribe
+        """
+        # Patch username and password.
+        mock_input.return_value = "user"
+        mock_pass.return_value = "pass"
+
+        # Instantiate Agave object making reference to local mock server.
+        local_uri = "http://localhost:{port}".format(port=self.mock_server_port)
+        ag = Agave(api_server=local_uri)
+
+        # Subscribe a client.
+        ag.clients_subscribe("PublicKeys", "v2", "admin", client_name="client")
+
+        # Stdout should contain the putput from the command.
+        # Stderr will contain logs from the mock http server.
+        out, err = capfd.readouterr()
+        assert "200" in err