Repository: TACC/agavepy

Issue:
Implement a method to list oauth clients subscriptions
Currently, there is no way to use AgavePy to list oauth clients api subscriptions.
TACC-Cloud requires this functionality.

Discussion:

Patch:
diff --git a/agavepy/agave.py b/agavepy/agave.py
index 3ade07f..fd0ad10 100644
--- a/agavepy/agave.py
+++ b/agavepy/agave.py
@@ -21,7 +21,7 @@ import requests
 
 from agavepy.tenants import tenant_list
 from agavepy.clients import (clients_create, clients_delete, clients_list, 
-    clients_subscribe)
+    clients_subscribe, clients_subscribtions)
 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, 
@@ -701,6 +701,22 @@ class Agave(object):
             api_name, api_version, api_provider)
 
 
+    def clients_subscribtions(self, client_name=None):
+        """ List oauth client subscriptions
+        """
+        # 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
+
+        # List subscriptions.
+        clients_subscribtions(self.username, client_name, self.api_server)
+
+
     def clients_list(self):
         """ List all oauth clients
         """
diff --git a/agavepy/clients/__init__.py b/agavepy/clients/__init__.py
index 7e2aedf..38e7b23 100644
--- a/agavepy/clients/__init__.py
+++ b/agavepy/clients/__init__.py
@@ -2,3 +2,4 @@ from .create import clients_create
 from .delete import clients_delete
 from .list import clients_list
 from .subscribe import clients_subscribe
+from .subscribtions import clients_subscribtions
diff --git a/agavepy/clients/subscribtions.py b/agavepy/clients/subscribtions.py
new file mode 100644
index 0000000..550a374
--- /dev/null
+++ b/agavepy/clients/subscribtions.py
@@ -0,0 +1,41 @@
+"""
+    subscriptions.py
+
+List oauth client subscriptions.
+"""
+import getpass
+import requests                                                                 
+from .exceptions import AgaveClientError                                        
+from ..utils import handle_bad_response_status_code
+
+
+
+def clients_subscribtions(username, client_name, tenant_url):
+    """ List oauth client subscriptions
+    """
+    # 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:
+        resp = requests.get(endpoint, auth=(username, passwd))
+        del passwd
+    except Exception as err:
+        del passwd
+        raise AgaveClientError(err)
+
+    # Handle bad status code.
+    handle_bad_response_status_code(resp)
+
+    # Print results.                                                            
+    print("{0:<16} {1:^8} {2:<20}".format("NAME", "VERSION", "PROVIDER"))
+    for api in resp.json().get("result", []):                                
+        print("{0:<16} {1:^8} {2:<20}".format(
+            api["apiName"], api["apiVersion"], api["apiProvider"]))
diff --git a/docs/docsite/authentication/clients.rst b/docs/docsite/authentication/clients.rst
index 96d8d85..ad6ab16 100644
--- a/docs/docsite/authentication/clients.rst
+++ b/docs/docsite/authentication/clients.rst
@@ -66,6 +66,8 @@ object will try to delete the oauth client in its current session.
 Subscribing to an API
 #####################
 
+To subscribe to a TACC api you need to know the ``api name``, the 
+``api version``, and the ``api provider``.
 If you want to subscribe an oauth client to a TACC api, then you can do so as
 follows:
 
@@ -73,5 +75,33 @@ follows:
 
     >>> 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.
+You can also specify the optional function argument ``client_name``. This
+should be a string and can be used to subscribe an oauth client not in the
+current session.
+
+
+List client subscriptions
+#########################
+
+To list client subscriptions you can use the ``clients_subscriptions`` method:
+
+.. code-block:: pycon
+
+    >>> ag.clients_subscribtions()
+    API password: 
+    NAME             VERSION  PROVIDER    
+    Apps                v2    admin    
+    Files               v2    admin   
+    Jobs                v2    admin   
+    Meta                v2    admin  
+    Monitors            v2    admin     
+    Notifications       v2    admin  
+    Postits             v2    admin   
+    Profiles            v2    admin   
+    Systems             v2    admin    
+    Transforms          v2    admin     
+    PublicKeys          v2    admin 
+
+Like ``clients_subscribe``, you can optionally specify the ``client_name``
+argument to list the subscriptions for another existing oauth client that you
+own.


Test patch:
diff --git a/tests/clients_test.py b/tests/clients_test.py
index 88aa14a..3eba507 100644
--- a/tests/clients_test.py
+++ b/tests/clients_test.py
@@ -24,7 +24,7 @@ from agavepy.agave import Agave
 # Sample successful responses from the agave api.
 sample_client_create_response = response_template_to_json("clients-create.json")
 sample_client_list_response = response_template_to_json("clients-list.json")
-
+sample_client_subscription = response_template_to_json("clients-subscriptions.json")
 
 
 class MockServerClientEndpoints(BaseHTTPRequestHandler):
@@ -42,9 +42,14 @@ class MockServerClientEndpoints(BaseHTTPRequestHandler):
             self.end_headers()
             return
 
-        self.send_response(200)
-        self.end_headers()
-        self.wfile.write(json.dumps(sample_client_list_response).encode())
+        if "/subscriptions" in self.path:
+            self.send_response(200)
+            self.end_headers()
+            self.wfile.write(json.dumps(sample_client_subscription).encode())
+        else:
+            self.send_response(200)
+            self.end_headers()
+            self.wfile.write(json.dumps(sample_client_list_response).encode())
 
 
     def do_POST(self):
@@ -222,3 +227,27 @@ class TestMockServer(MockServer):
         # Stderr will contain logs from the mock http server.
         out, err = capfd.readouterr()
         assert "200" in err
+
+
+    @patch("agavepy.agave.input")
+    @patch("agavepy.clients.subscribtions.getpass.getpass")
+    def test_clients_subscribtions(self, mock_input, mock_pass, capfd):
+        """ Test clients subscribtions
+        """
+        # 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)
+        ag.client_name = "test"
+
+        # List subscriptions.
+        ag.clients_subscribtions()
+
+        # Stdout should contain the putput from the command.
+        # Stderr will contain logs from the mock http server.
+        out, err = capfd.readouterr()
+        assert "Apps" in out
+        assert "200" in err
diff --git a/tests/sample_responses/clients-subscriptions.json b/tests/sample_responses/clients-subscriptions.json
new file mode 100644
index 0000000..3cd58af
--- /dev/null
+++ b/tests/sample_responses/clients-subscriptions.json
@@ -0,0 +1,227 @@
+{
+  "status": "success",
+  "message": "Client subscriptions retrieved successfully.",
+  "version": "2.0.0-SNAPSHOT-rc3fad",
+  "result": [
+    {
+      "apiName": "Apps",
+      "tier": "Unlimited",
+      "apiStatus": "PUBLISHED",
+      "apiProvider": "admin",
+      "apiContext": "/apps/v2",
+      "apiVersion": "v2",
+      "_links": {
+        "self": {
+          "href": "https://api.tacc.utexas.edu/clients/v2/subs/subscriptions/"
+        },
+        "api": {
+          "href": "https://api.tacc.utexas.edu/apps/v2/"
+        },
+        "client": {
+          "href": "https://api.tacc.utexas.edu/clients/v2/subs"
+        }
+      },
+      "businessOwner": null
+    },
+    {
+      "apiName": "Files",
+      "tier": "Unlimited",
+      "apiStatus": "PUBLISHED",
+      "apiProvider": "admin",
+      "apiContext": "/files/v2",
+      "apiVersion": "v2",
+      "_links": {
+        "self": {
+          "href": "https://api.tacc.utexas.edu/clients/v2/subs/subscriptions/"
+        },
+        "api": {
+          "href": "https://api.tacc.utexas.edu/files/v2/"
+        },
+        "client": {
+          "href": "https://api.tacc.utexas.edu/clients/v2/subs"
+        }
+      },
+      "businessOwner": null
+    },
+    {
+      "apiName": "Jobs",
+      "tier": "Unlimited",
+      "apiStatus": "PUBLISHED",
+      "apiProvider": "admin",
+      "apiContext": "/jobs/v2",
+      "apiVersion": "v2",
+      "_links": {
+        "self": {
+          "href": "https://api.tacc.utexas.edu/clients/v2/subs/subscriptions/"
+        },
+        "api": {
+          "href": "https://api.tacc.utexas.edu/jobs/v2/"
+        },
+        "client": {
+          "href": "https://api.tacc.utexas.edu/clients/v2/subs"
+        }
+      },
+      "businessOwner": null
+    },
+    {
+      "apiName": "Meta",
+      "tier": "Unlimited",
+      "apiStatus": "PUBLISHED",
+      "apiProvider": "admin",
+      "apiContext": "/meta/v2",
+      "apiVersion": "v2",
+      "_links": {
+        "self": {
+          "href": "https://api.tacc.utexas.edu/clients/v2/subs/subscriptions/"
+        },
+        "api": {
+          "href": "https://api.tacc.utexas.edu/meta/v2/"
+        },
+        "client": {
+          "href": "https://api.tacc.utexas.edu/clients/v2/subs"
+        }
+      },
+      "businessOwner": null
+    },
+    {
+      "apiName": "Monitors",
+      "tier": "Unlimited",
+      "apiStatus": "PUBLISHED",
+      "apiProvider": "admin",
+      "apiContext": "/monitors/v2",
+      "apiVersion": "v2",
+      "_links": {
+        "self": {
+          "href": "https://api.tacc.utexas.edu/clients/v2/subs/subscriptions/"
+        },
+        "api": {
+          "href": "https://api.tacc.utexas.edu/monitors/v2/"
+        },
+        "client": {
+          "href": "https://api.tacc.utexas.edu/clients/v2/subs"
+        }
+      },
+      "businessOwner": null
+    },
+    {
+      "apiName": "Notifications",
+      "tier": "Unlimited",
+      "apiStatus": "PUBLISHED",
+      "apiProvider": "admin",
+      "apiContext": "/notifications/v2",
+      "apiVersion": "v2",
+      "_links": {
+        "self": {
+          "href": "https://api.tacc.utexas.edu/clients/v2/subs/subscriptions/"
+        },
+        "api": {
+          "href": "https://api.tacc.utexas.edu/notifications/v2/"
+        },
+        "client": {
+          "href": "https://api.tacc.utexas.edu/clients/v2/subs"
+        }
+      },
+      "businessOwner": null
+    },
+    {
+      "apiName": "Postits",
+      "tier": "Unlimited",
+      "apiStatus": "PUBLISHED",
+      "apiProvider": "admin",
+      "apiContext": "/postits/v2",
+      "apiVersion": "v2",
+      "_links": {
+        "self": {
+          "href": "https://api.tacc.utexas.edu/clients/v2/subs/subscriptions/"
+        },
+        "api": {
+          "href": "https://api.tacc.utexas.edu/postits/v2/"
+        },
+        "client": {
+          "href": "https://api.tacc.utexas.edu/clients/v2/subs"
+        }
+      },
+      "businessOwner": null
+    },
+    {
+      "apiName": "Profiles",
+      "tier": "Unlimited",
+      "apiStatus": "PUBLISHED",
+      "apiProvider": "admin",
+      "apiContext": "/profiles/v2",
+      "apiVersion": "v2",
+      "_links": {
+        "self": {
+          "href": "https://api.tacc.utexas.edu/clients/v2/subs/subscriptions/"
+        },
+        "api": {
+          "href": "https://api.tacc.utexas.edu/profiles/v2/"
+        },
+        "client": {
+          "href": "https://api.tacc.utexas.edu/clients/v2/subs"
+        }
+      },
+      "businessOwner": null
+    },
+    {
+      "apiName": "Systems",
+      "tier": "Unlimited",
+      "apiStatus": "PUBLISHED",
+      "apiProvider": "admin",
+      "apiContext": "/systems/v2",
+      "apiVersion": "v2",
+      "_links": {
+        "self": {
+          "href": "https://api.tacc.utexas.edu/clients/v2/subs/subscriptions/"
+        },
+        "api": {
+          "href": "https://api.tacc.utexas.edu/systems/v2/"
+        },
+        "client": {
+          "href": "https://api.tacc.utexas.edu/clients/v2/subs"
+        }
+      },
+      "businessOwner": null
+    },
+    {
+      "apiName": "Transforms",
+      "tier": "Unlimited",
+      "apiStatus": "PUBLISHED",
+      "apiProvider": "admin",
+      "apiContext": "/transforms/v2",
+      "apiVersion": "v2",
+      "_links": {
+        "self": {
+          "href": "https://api.tacc.utexas.edu/clients/v2/subs/subscriptions/"
+        },
+        "api": {
+          "href": "https://api.tacc.utexas.edu/transforms/v2/"
+        },
+        "client": {
+          "href": "https://api.tacc.utexas.edu/clients/v2/subs"
+        }
+      },
+      "businessOwner": null
+    },
+    {
+      "apiName": "PublicKeys",
+      "tier": "Unlimited",
+      "apiStatus": "PUBLISHED",
+      "apiProvider": "admin",
+      "apiContext": "/keys/v2",
+      "apiVersion": "v2",
+      "_links": {
+        "self": {
+          "href": "https://api.tacc.utexas.edu/clients/v2/subs/subscriptions/"
+        },
+        "api": {
+          "href": "https://api.tacc.utexas.edu/keys/v2/"
+        },
+        "client": {
+          "href": "https://api.tacc.utexas.edu/clients/v2/subs"
+        }
+      },
+      "businessOwner": null
+    }
+  ]
+}