Repository: googleapis/google-cloud-python

Issue:
 path "mysink" did not match expected pattern
Looks like the Sink resource name is not the fully qualified version. Docs agree: https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/projects.sinks#resource-logsink. I don't see a good reason why it wouldn't be the full name. Once again, I will bug people but for now I think we should disable the validation.

```
 {u'filter': u'logName=projects/bill-stackdriver-experiment/logs/syslog AND severity>=ERROR', u'destination': u'storage.googleapis.com/bill-new-stackdriver', u'name': u'mysink', u'outputVersionFormat': u'V2'} 
Validating sink name mysink bill-stackdriver-experiment 
Traceback (most recent call last):
  File "export.py", line 102, in <module>
    main(args.project_id, args.destination_bucket)
  File "export.py", line 84, in main
    list_sinks(client)
  File "export.py", line 54, in list_sinks
    sinks = client.list_sinks()[0]
  File "/Users/waprin/.virtualenvs/docsamples10/lib/python2.7/site-packages/gcloud/logging/client.py", line 196, in list_sinks
    for resource in resp.get('sinks', ())]
  File "/Users/waprin/.virtualenvs/docsamples10/lib/python2.7/site-packages/gcloud/logging/sink.py", line 112, in from_api_repr
    sink_name = _sink_name_from_path(resource['name'], client.project)
  File "/Users/waprin/.virtualenvs/docsamples10/lib/python2.7/site-packages/gcloud/logging/sink.py", line 45, in _sink_name_from_path
    return _name_from_project_path(path, project, _SINK_TEMPLATE)
  File "/Users/waprin/.virtualenvs/docsamples10/lib/python2.7/site-packages/gcloud/_helpers.py", line 458, in _name_from_project_path
    path, template.pattern,))
ValueError: path "mysink" did not match expected pattern "
    projects/            # static prefix
    (?P<project>[^/]+)   # initial letter, wordchars + hyphen
    /sinks/              # static midfix
    (?P<name>[^/]+)      # initial letter, wordchars + allowed punc
```



Discussion:
Same idea as #1808?

Yep, except it will never get fixed because they don't consider it important enough to change the API for.

@waprin Thanks for the report! 61004b72 made a similar change for metrics:  we are missing the system test for `list_sinks` which would have surfaced this issue.


Resolving patch:
diff --git a/gcloud/logging/metric.py b/gcloud/logging/metric.py
--- a/gcloud/logging/metric.py
+++ b/gcloud/logging/metric.py
@@ -14,39 +14,9 @@
 
 """Define Logging API Metrics."""
 
-import re
-
-from gcloud._helpers import _name_from_project_path
 from gcloud.exceptions import NotFound
 
 
-_METRIC_TEMPLATE = re.compile(r"""
-    projects/            # static prefix
-    (?P<project>[^/]+)   # initial letter, wordchars + hyphen
-    /metrics/            # static midfix
-    (?P<name>[^/]+)      # initial letter, wordchars + allowed punc
-""", re.VERBOSE)
-
-
-def _metric_name_from_path(path, project):
-    """Validate a metric URI path and get the metric name.
-
-    :type path: string
-    :param path: URI path for a metric API request.
-
-    :type project: string
-    :param project: The project associated with the request. It is
-                    included for validation purposes.
-
-    :rtype: string
-    :returns: Metric name parsed from ``path``.
-    :raises: :class:`ValueError` if the ``path`` is ill-formed or if
-             the project from the ``path`` does not agree with the
-             ``project`` passed in.
-    """
-    return _name_from_project_path(path, project, _METRIC_TEMPLATE)
-
-
 class Metric(object):
     """Metrics represent named filters for log entries.
 
diff --git a/gcloud/logging/sink.py b/gcloud/logging/sink.py
--- a/gcloud/logging/sink.py
+++ b/gcloud/logging/sink.py
@@ -14,36 +14,9 @@
 
 """Define Logging API Sinks."""
 
-import re
-
-from gcloud._helpers import _name_from_project_path
 from gcloud.exceptions import NotFound
 
 
-_SINK_TEMPLATE = re.compile(r"""
-    projects/            # static prefix
-    (?P<project>[^/]+)   # initial letter, wordchars + hyphen
-    /sinks/              # static midfix
-    (?P<name>[^/]+)      # initial letter, wordchars + allowed punc
-""", re.VERBOSE)
-
-
-def _sink_name_from_path(path, project):
-    """Validate a sink URI path and get the sink name.
-    :type path: string
-    :param path: URI path for a sink API request.
-    :type project: string
-    :param project: The project associated with the request. It is
-                    included for validation purposes.
-    :rtype: string
-    :returns: Metric name parsed from ``path``.
-    :raises: :class:`ValueError` if the ``path`` is ill-formed or if
-             the project from the ``path`` does not agree with the
-             ``project`` passed in.
-    """
-    return _name_from_project_path(path, project, _SINK_TEMPLATE)
-
-
 class Sink(object):
     """Sinks represent filtered exports for log entries.
 
@@ -107,7 +80,7 @@ def from_api_repr(cls, resource, client):
                  project from the resource does not agree with the project
                  from the client.
         """
-        sink_name = _sink_name_from_path(resource['name'], client.project)
+        sink_name = resource['name']
         filter_ = resource['filter']
         destination = resource['destination']
         return cls(sink_name, filter_, destination, client=client)


Test patch:
diff --git a/gcloud/logging/test_client.py b/gcloud/logging/test_client.py
--- a/gcloud/logging/test_client.py
+++ b/gcloud/logging/test_client.py
@@ -210,9 +210,8 @@ def test_list_sinks_no_paging(self):
         TOKEN = 'TOKEN'
         SINK_NAME = 'sink_name'
         FILTER = 'logName:syslog AND severity>=ERROR'
-        SINK_PATH = 'projects/%s/sinks/%s' % (PROJECT, SINK_NAME)
         SINKS = [{
-            'name': SINK_PATH,
+            'name': SINK_NAME,
             'filter': FILTER,
             'destination': self.DESTINATION_URI,
         }]
@@ -238,11 +237,10 @@ def test_list_sinks_with_paging(self):
         PROJECT = 'PROJECT'
         SINK_NAME = 'sink_name'
         FILTER = 'logName:syslog AND severity>=ERROR'
-        SINK_PATH = 'projects/%s/sinks/%s' % (PROJECT, SINK_NAME)
         TOKEN = 'TOKEN'
         PAGE_SIZE = 42
         SINKS = [{
-            'name': SINK_PATH,
+            'name': SINK_NAME,
             'filter': FILTER,
             'destination': self.DESTINATION_URI,
         }]
diff --git a/gcloud/logging/test_metric.py b/gcloud/logging/test_metric.py
--- a/gcloud/logging/test_metric.py
+++ b/gcloud/logging/test_metric.py
@@ -15,38 +15,6 @@
 import unittest2
 
 
-class Test__metric_name_from_path(unittest2.TestCase):
-
-    def _callFUT(self, path, project):
-        from gcloud.logging.metric import _metric_name_from_path
-        return _metric_name_from_path(path, project)
-
-    def test_invalid_path_length(self):
-        PATH = 'projects/foo'
-        PROJECT = None
-        self.assertRaises(ValueError, self._callFUT, PATH, PROJECT)
-
-    def test_invalid_path_format(self):
-        METRIC_NAME = 'METRIC_NAME'
-        PROJECT = 'PROJECT'
-        PATH = 'foo/%s/bar/%s' % (PROJECT, METRIC_NAME)
-        self.assertRaises(ValueError, self._callFUT, PATH, PROJECT)
-
-    def test_invalid_project(self):
-        METRIC_NAME = 'METRIC_NAME'
-        PROJECT1 = 'PROJECT1'
-        PROJECT2 = 'PROJECT2'
-        PATH = 'projects/%s/metrics/%s' % (PROJECT1, METRIC_NAME)
-        self.assertRaises(ValueError, self._callFUT, PATH, PROJECT2)
-
-    def test_valid_data(self):
-        METRIC_NAME = 'METRIC_NAME'
-        PROJECT = 'PROJECT'
-        PATH = 'projects/%s/metrics/%s' % (PROJECT, METRIC_NAME)
-        metric_name = self._callFUT(PATH, PROJECT)
-        self.assertEqual(metric_name, METRIC_NAME)
-
-
 class TestMetric(unittest2.TestCase):
 
     PROJECT = 'test-project'
diff --git a/gcloud/logging/test_sink.py b/gcloud/logging/test_sink.py
--- a/gcloud/logging/test_sink.py
+++ b/gcloud/logging/test_sink.py
@@ -15,38 +15,6 @@
 import unittest2
 
 
-class Test__sink_name_from_path(unittest2.TestCase):
-
-    def _callFUT(self, path, project):
-        from gcloud.logging.sink import _sink_name_from_path
-        return _sink_name_from_path(path, project)
-
-    def test_invalid_path_length(self):
-        PATH = 'projects/foo'
-        PROJECT = None
-        self.assertRaises(ValueError, self._callFUT, PATH, PROJECT)
-
-    def test_invalid_path_format(self):
-        SINK_NAME = 'SINK_NAME'
-        PROJECT = 'PROJECT'
-        PATH = 'foo/%s/bar/%s' % (PROJECT, SINK_NAME)
-        self.assertRaises(ValueError, self._callFUT, PATH, PROJECT)
-
-    def test_invalid_project(self):
-        SINK_NAME = 'SINK_NAME'
-        PROJECT1 = 'PROJECT1'
-        PROJECT2 = 'PROJECT2'
-        PATH = 'projects/%s/sinks/%s' % (PROJECT1, SINK_NAME)
-        self.assertRaises(ValueError, self._callFUT, PATH, PROJECT2)
-
-    def test_valid_data(self):
-        SINK_NAME = 'SINK_NAME'
-        PROJECT = 'PROJECT'
-        PATH = 'projects/%s/sinks/%s' % (PROJECT, SINK_NAME)
-        sink_name = self._callFUT(PATH, PROJECT)
-        self.assertEqual(sink_name, SINK_NAME)
-
-
 class TestSink(unittest2.TestCase):
 
     PROJECT = 'test-project'
@@ -78,7 +46,7 @@ def test_from_api_repr_minimal(self):
         client = _Client(project=self.PROJECT)
         FULL = 'projects/%s/sinks/%s' % (self.PROJECT, self.SINK_NAME)
         RESOURCE = {
-            'name': FULL,
+            'name': self.SINK_NAME,
             'filter': self.FILTER,
             'destination': self.DESTINATION_URI,
         }
@@ -95,7 +63,7 @@ def test_from_api_repr_w_description(self):
         client = _Client(project=self.PROJECT)
         FULL = 'projects/%s/sinks/%s' % (self.PROJECT, self.SINK_NAME)
         RESOURCE = {
-            'name': FULL,
+            'name': self.SINK_NAME,
             'filter': self.FILTER,
             'destination': self.DESTINATION_URI,
         }
@@ -108,20 +76,6 @@ def test_from_api_repr_w_description(self):
         self.assertEqual(sink.project, self.PROJECT)
         self.assertEqual(sink.full_name, FULL)
 
-    def test_from_api_repr_with_mismatched_project(self):
-        PROJECT1 = 'PROJECT1'
-        PROJECT2 = 'PROJECT2'
-        client = _Client(project=PROJECT1)
-        FULL = 'projects/%s/sinks/%s' % (PROJECT2, self.SINK_NAME)
-        RESOURCE = {
-            'name': FULL,
-            'filter': self.FILTER,
-            'destination': self.DESTINATION_URI,
-        }
-        klass = self._getTargetClass()
-        self.assertRaises(ValueError, klass.from_api_repr,
-                          RESOURCE, client=client)
-
     def test_create_w_bound_client(self):
         client = _Client(project=self.PROJECT)
         api = client.sinks_api = _DummySinksAPI()
diff --git a/system_tests/logging_.py b/system_tests/logging_.py
--- a/system_tests/logging_.py
+++ b/system_tests/logging_.py
@@ -277,6 +277,20 @@ def test_create_sink_bigquery_dataset(self):
         self.to_delete.append(sink)
         self.assertTrue(sink.exists())
 
+    def test_list_sinks(self):
+        uri = self._init_storage_bucket()
+        sink = Config.CLIENT.sink(DEFAULT_SINK_NAME, DEFAULT_FILTER, uri)
+        self.assertFalse(sink.exists())
+        before_sinks, _ = Config.CLIENT.list_sinks()
+        before_names = set(sink.name for sink in before_sinks)
+        sink.create()
+        self.to_delete.append(sink)
+        self.assertTrue(sink.exists())
+        after_sinks, _ = Config.CLIENT.list_sinks()
+        after_names = set(sink.name for sink in after_sinks)
+        self.assertEqual(after_names - before_names,
+                         set([DEFAULT_SINK_NAME]))
+
     def test_reload_sink(self):
         uri = self._init_bigquery_dataset()
         sink = Config.CLIENT.sink(DEFAULT_SINK_NAME, DEFAULT_FILTER, uri)