Repository: rucio/rucio

Issue:
Subscription client does not allow listing subscriptions by name only
Motivation
----------
`SubscriptionClient.list_subscriptions` takes both account and name as arguments, but the name is only added to the path if an account is specified. API/core functions already work with a name only, so it's just a matter of updating the client.


Modification
------------
Adapt `SubscriptionClient.list_subscriptions` to add the name to the path even when the account is not specified.




Discussion:

Resolving patch:
diff --git a/lib/rucio/client/subscriptionclient.py b/lib/rucio/client/subscriptionclient.py
--- a/lib/rucio/client/subscriptionclient.py
+++ b/lib/rucio/client/subscriptionclient.py
@@ -17,6 +17,7 @@
 # - Martin Barisits <martin.barisits@cern.ch>, 2013-2018
 # - Ralph Vigne <ralph.vigne@cern.ch>, 2015
 # - Vincent Garonne <vgaronne@gmail.com>, 2015-2018
+# - Eli Chadwick <eli.chadwick@stfc.ac.uk>, 2020
 #
 # PY3K COMPATIBLE
 
@@ -96,6 +97,8 @@ def list_subscriptions(self, name=None, account=None):
             path += '/%s' % (account)
             if name:
                 path += '/%s' % (name)
+        elif name:
+            path += '/Name/%s' % (name)
         else:
             path += '/'
         url = build_url(choice(self.list_hosts), path=path)
diff --git a/lib/rucio/web/rest/flaskapi/v1/subscription.py b/lib/rucio/web/rest/flaskapi/v1/subscription.py
--- a/lib/rucio/web/rest/flaskapi/v1/subscription.py
+++ b/lib/rucio/web/rest/flaskapi/v1/subscription.py
@@ -18,6 +18,7 @@
 # - Thomas Beermann <thomas.beermann@cern.ch>, 2014-2018
 # - Mario Lassnig <mario.lassnig@cern.ch>, 2018
 # - Hannes Hansen <hannes.jakob.hansen@cern.ch>, 2018-2019
+# - Eli Chadwick <eli.chadwick@stfc.ac.uk>, 2020
 #
 # PY3K COMPATIBLE
 
@@ -153,6 +154,35 @@ def post(self, account, name):
         return Response(subscription_id, status=201)
 
 
+class SubscriptionName(MethodView):
+
+    @check_accept_header_wrapper_flask(['application/x-json-stream'])
+    def get(self, name=None):
+        """
+        Retrieve a subscription by name.
+
+        .. :quickref: SubscriptionName; Get subscriptions by name.
+
+        :param name: The subscription name.
+        :resheader Content-Type: application/x-json-stream
+        :status 200: OK.
+        :status 401: Invalid Auth Token.
+        :status 404: Subscription Not Found.
+        :status 406: Not Acceptable.
+        :status 500: Internal Error.
+        :returns: Line separated list of dictionaries with subscription information.
+        """
+        try:
+            data = ""
+            for subscription in list_subscriptions(name=name):
+                data += dumps(subscription, cls=APIEncoder) + '\n'
+            return Response(data, content_type="application/x-json-stream")
+        except SubscriptionNotFound as error:
+            return generate_http_error_flask(404, 'SubscriptionNotFound', error.args[0])
+        except Exception as error:
+            return error, 500
+
+
 class Rules(MethodView):
 
     @check_accept_header_wrapper_flask(['application/x-json-stream'])
@@ -270,6 +300,8 @@ def get(self, subscription_id):
 bp.add_url_rule('/<account>/<name>', view_func=subscription_view, methods=['get', 'post', 'put'])
 bp.add_url_rule('/<account>', view_func=subscription_view, methods=['get', ])
 bp.add_url_rule('/', view_func=subscription_view, methods=['get', ])
+subscription_name_view = SubscriptionName.as_view('subscription_name')
+bp.add_url_rule('/Name/<name>', view_func=subscription_name_view, methods=['get', ])
 
 application = Flask(__name__)
 application.register_blueprint(bp)
diff --git a/lib/rucio/web/rest/webpy/v1/subscription.py b/lib/rucio/web/rest/webpy/v1/subscription.py
--- a/lib/rucio/web/rest/webpy/v1/subscription.py
+++ b/lib/rucio/web/rest/webpy/v1/subscription.py
@@ -18,6 +18,7 @@
 # - Thomas Beermann <thomas.beermann@cern.ch>, 2014
 # - Mario Lassnig <mario.lassnig@cern.ch>, 2018
 # - Hannes Hansen <hannes.jakob.hansen@cern.ch>, 2018-2019
+# - Eli Chadwick <eli.chadwick@stfc.ac.uk>, 2020
 #
 # PY3K COMPATIBLE
 
@@ -41,6 +42,7 @@
     '/(.*)/(.*)/Rules/States', 'States',
     '/(.*)/Rules/States', 'States',
     '/(.*)/(.*)/Rules', 'Rules',
+    '/Name/(.*)', 'SubscriptionName',
     '/(.*)/(.*)', 'Subscription',
     '/(.*)', 'Subscription',
     '/', 'Subscription',
@@ -163,6 +165,42 @@ def DELETE(self):
         raise BadRequest()
 
 
+class SubscriptionName:
+
+    @check_accept_header_wrapper(['application/x-json-stream'])
+    def GET(self, name):
+        """
+        Retrieve a subscription by name.
+
+        HTTP Success:
+            200 OK
+
+        HTTP Error:
+            404 Not Found
+            500 Internal Error
+            406 Not Acceptable
+
+        :param name: The subscription name.
+        """
+        header('Content-Type', 'application/x-json-stream')
+        try:
+            for subscription in list_subscriptions(name=name):
+                yield dumps(subscription, cls=APIEncoder) + '\n'
+        except SubscriptionNotFound as error:
+            raise generate_http_error(404, 'SubscriptionNotFound', error.args[0])
+        except Exception as error:
+            raise InternalError(error)
+
+    def PUT(self, name):
+        raise BadRequest()
+
+    def POST(self, name):
+        raise BadRequest()
+
+    def DELETE(self):
+        raise BadRequest()
+
+
 class Rules:
 
     @check_accept_header_wrapper(['application/x-json-stream'])


Test patch:
diff --git a/lib/rucio/tests/test_subscription.py b/lib/rucio/tests/test_subscription.py
--- a/lib/rucio/tests/test_subscription.py
+++ b/lib/rucio/tests/test_subscription.py
@@ -21,6 +21,7 @@
 # - Mario Lassnig <mario.lassnig@cern.ch>, 2018
 # - Andrew Lister <andrew.lister@stfc.ac.uk>, 2019
 # - Hannes Hansen <hannes.jakob.hansen@cern.ch>, 2019
+# - Eli Chadwick <eli.chadwick@stfc.ac.uk>, 2020
 
 from __future__ import print_function
 
@@ -35,13 +36,14 @@
 from rucio.common.exception import InvalidObject, SubscriptionNotFound, SubscriptionDuplicate
 from rucio.common.types import InternalAccount, InternalScope
 from rucio.common.utils import generate_uuid as uuid
+from rucio.core.account import add_account
 from rucio.core.account_limit import set_local_account_limit
 from rucio.core.did import add_did, set_new_dids
 from rucio.core.rse import add_rse
 from rucio.core.rule import add_rule
 from rucio.core.scope import add_scope
 from rucio.daemons.transmogrifier.transmogrifier import run
-from rucio.db.sqla.constants import DIDType
+from rucio.db.sqla.constants import AccountType, DIDType
 from rucio.web.rest.authentication import APP as auth_app
 from rucio.web.rest.subscription import APP as subs_app
 
@@ -333,6 +335,24 @@ def test_update_nonexisting_subscription(self):
         subscription_name = uuid()
         self.sub_client.update_subscription(name=subscription_name, filter={'project': ['toto', ]})
 
+    def test_create_and_list_subscription_by_account(self):
+        """ SUBSCRIPTION (CLIENT): Test retrieval of subscriptions for an account """
+        subscription_name = uuid()
+        account_name = uuid()[:10]
+        add_account(InternalAccount(account_name), AccountType.USER, 'rucio@email.com')
+        subid = self.sub_client.add_subscription(name=subscription_name, account=account_name, filter={'project': self.projects, 'datatype': ['AOD', ], 'excluded_pattern': self.pattern1, 'account': ['tier0', ]},
+                                                 replication_rules=[{'lifetime': 86400, 'rse_expression': 'MOCK|MOCK2', 'copies': 2, 'activity': 'Data Brokering'}], lifetime=100000, retroactive=0, dry_run=0, comments='Ni ! Ni!')
+        result = [sub['id'] for sub in self.sub_client.list_subscriptions(account=account_name)]
+        assert_equal(subid, result[0])
+
+    def test_create_and_list_subscription_by_name(self):
+        """ SUBSCRIPTION (CLIENT): Test retrieval of subscriptions for an account """
+        subscription_name = uuid()
+        subid = self.sub_client.add_subscription(name=subscription_name, account='root', filter={'project': self.projects, 'datatype': ['AOD', ], 'excluded_pattern': self.pattern1, 'account': ['tier0', ]},
+                                                 replication_rules=[{'lifetime': 86400, 'rse_expression': 'MOCK|MOCK2', 'copies': 2, 'activity': 'Data Brokering'}], lifetime=100000, retroactive=0, dry_run=0, comments='Ni ! Ni!')
+        result = [sub['id'] for sub in self.sub_client.list_subscriptions(name=subscription_name)]
+        assert_equal(subid, result[0])
+
     def test_run_transmogrifier(self):
         """ SUBSCRIPTION (DAEMON): Test the transmogrifier and the split_rule mode """
         tmp_scope = InternalScope('mock_' + uuid()[:8])