Repository: googleapis/google-cloud-python

Issue:
Logging: sink reload throws exception if the sink has no filter
A KeyError exception for 'filter' is thrown when calling [sink].reload() if there is no filter. The API doesn't return a 'filter' key when retrieving the sink if its null.

```
Python 3.5.2
google-cloud-core (0.28.0)
google-cloud-logging (1.4.0)
```

## Simplified code example
```
logging_client = logging.Client(project_id)
sink = logging_client.sink(sink_name)
sink.reload()
```

## Raw API response for sink request (via gcloud)
```
 $ gcloud logging sinks list --project xxxx --log-http 
 =======================
 ==== request start ====
 uri: https://logging.googleapis.com/v2/projects/xxxx/sinks?alt=json
 method: GET
 [...]
 -- body start --
 {
   "sinks": [
     {
       "name": "xxxx",
       "destination": "storage.googleapis.com/xxxx",
       "outputVersionFormat": "V2",
       "writerIdentity": "serviceAccount:xxxx"
     }
   ]
 }
 
 -- body end --
 
 [...]
 NAME       DESTINATION                  FILTER
 xxxx       storage.googleapis.com/xxxx  (empty filter)
```
## Problematic code
/google/cloud/logging/sink.py Line 157
`self.filter_ = data['filter']`

## Traceback
```
Traceback (most recent call last):
  File "/[...]/python3.5/site-packages/xxxx", line 50, in _get_sink
    sink.reload()
  File "[...]/python3.5/site-packages/google/cloud/logging/sink.py", line 157, in reload
    self.filter_ = data['filter']
KeyError: 'filter'
```



Discussion:
@jceresini Thanks for the report!  I think maybe the `filter` argument used to be required on the back-end, and so we never saw this case.

Resolving patch:
diff --git a/logging/google/cloud/logging/sink.py b/logging/google/cloud/logging/sink.py
--- a/logging/google/cloud/logging/sink.py
+++ b/logging/google/cloud/logging/sink.py
@@ -27,9 +27,8 @@ class Sink(object):
     :param name: the name of the sink
 
     :type filter_: str
-    :param filter_: the advanced logs filter expression defining the entries
-                    exported by the sink.  If not passed, the instance should
-                    already exist, to be refreshed via :meth:`reload`.
+    :param filter_: (optional) the advanced logs filter expression defining
+                    the entries exported by the sink.
 
     :type destination: str
     :param destination: destination URI for the entries exported by the sink.
@@ -91,8 +90,8 @@ def from_api_repr(cls, resource, client):
                  from the client.
         """
         sink_name = resource['name']
-        filter_ = resource['filter']
         destination = resource['destination']
+        filter_ = resource.get('filter')
         return cls(sink_name, filter_, destination, client=client)
 
     def _require_client(self, client):
@@ -163,8 +162,8 @@ def reload(self, client=None):
         """
         client = self._require_client(client)
         data = client.sinks_api.sink_get(self.project, self.name)
-        self.filter_ = data['filter']
         self.destination = data['destination']
+        self.filter_ = data.get('filter')
 
     def update(self, client=None):
         """API call:  update sink configuration via a PUT request


Test patch:
diff --git a/logging/tests/unit/test_sink.py b/logging/tests/unit/test_sink.py
--- a/logging/tests/unit/test_sink.py
+++ b/logging/tests/unit/test_sink.py
@@ -62,13 +62,12 @@ def test_from_api_repr_minimal(self):
         FULL = 'projects/%s/sinks/%s' % (self.PROJECT, self.SINK_NAME)
         RESOURCE = {
             'name': self.SINK_NAME,
-            'filter': self.FILTER,
             'destination': self.DESTINATION_URI,
         }
         klass = self._get_target_class()
         sink = klass.from_api_repr(RESOURCE, client=client)
         self.assertEqual(sink.name, self.SINK_NAME)
-        self.assertEqual(sink.filter_, self.FILTER)
+        self.assertIsNone(sink.filter_)
         self.assertEqual(sink.destination, self.DESTINATION_URI)
         self.assertIs(sink._client, client)
         self.assertEqual(sink.project, self.PROJECT)
@@ -164,24 +163,20 @@ def test_exists_hit_w_alternate_client(self):
                          (self.PROJECT, self.SINK_NAME))
 
     def test_reload_w_bound_client(self):
-        NEW_FILTER = 'logName:syslog AND severity>=INFO'
         NEW_DESTINATION_URI = 'faux.googleapis.com/other'
         RESOURCE = {
             'name': self.SINK_NAME,
-            'filter': NEW_FILTER,
             'destination': NEW_DESTINATION_URI,
         }
         client = _Client(project=self.PROJECT)
         api = client.sinks_api = _DummySinksAPI()
         api._sink_get_response = RESOURCE
-        sink = self._make_one(self.SINK_NAME, self.FILTER,
-                              self.DESTINATION_URI,
-                              client=client)
+        sink = self._make_one(self.SINK_NAME, client=client)
 
         sink.reload()
 
-        self.assertEqual(sink.filter_, NEW_FILTER)
         self.assertEqual(sink.destination, NEW_DESTINATION_URI)
+        self.assertIsNone(sink.filter_)
         self.assertEqual(api._sink_get_called_with,
                          (self.PROJECT, self.SINK_NAME))
 
@@ -197,9 +192,7 @@ def test_reload_w_alternate_client(self):
         client2 = _Client(project=self.PROJECT)
         api = client2.sinks_api = _DummySinksAPI()
         api._sink_get_response = RESOURCE
-        sink = self._make_one(self.SINK_NAME, self.FILTER,
-                              self.DESTINATION_URI,
-                              client=client1)
+        sink = self._make_one(self.SINK_NAME, client=client1)
 
         sink.reload(client=client2)