Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add 'Client.list_sinks' API wrapper. #1601

Merged
merged 2 commits into from
Mar 12, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
36 changes: 36 additions & 0 deletions gcloud/logging/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,39 @@ def sink(self, name, filter_, destination):
:returns: Sink created with the current client.
"""
return Sink(name, filter_, destination, client=self)

def list_sinks(self, page_size=None, page_token=None):
"""List sinks for the project associated with this client.

See:
https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/projects.sinks/list

:type page_size: int
:param page_size: maximum number of sinks to return, If not passed,
defaults to a value set by the API.

:type page_token: string
:param page_token: opaque marker for the next "page" of sinks. If not
passed, the API will return the first page of
sinks.

:rtype: tuple, (list, str)
:returns: list of :class:`gcloud.logging.sink.Sink`, plus a
"next page token" string: if not None, indicates that
more sinks can be retrieved with another call (pass that
value as ``page_token``).
"""
params = {}

if page_size is not None:
params['pageSize'] = page_size

if page_token is not None:
params['pageToken'] = page_token

path = '/projects/%s/sinks' % (self.project,)
resp = self.connection.api_request(method='GET', path=path,
query_params=params)
sinks = [Sink.from_api_repr(resource, self)
for resource in resp.get('sinks', ())]
return sinks, resp.get('nextPageToken')
51 changes: 51 additions & 0 deletions gcloud/logging/sink.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,36 @@

"""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.

Expand Down Expand Up @@ -63,11 +90,35 @@ def path(self):
"""URL path for the sink's APIs"""
return '/%s' % (self.full_name)

@classmethod
def from_api_repr(cls, resource, client):
"""Factory: construct a sink given its API representation

:type resource: dict
:param resource: sink resource representation returned from the API

:type client: :class:`gcloud.pubsub.client.Client`
:param client: Client which holds credentials and project
configuration for the sink.

:rtype: :class:`gcloud.logging.sink.Sink`
:returns: Sink parsed from ``resource``.
:raises: :class:`ValueError` if ``client`` is not ``None`` and the
project from the resource does not agree with the project
from the client.
"""
sink_name = _sink_name_from_path(resource['name'], client.project)
filter_ = resource['filter']
destination = resource['destination']
return cls(sink_name, filter_, destination, client=client)

def _require_client(self, client):
"""Check client or verify over-ride.

:type client: :class:`gcloud.logging.client.Client` or ``NoneType``
:param client: the client to use. If not passed, falls back to the
``client`` stored on the current sink.

:rtype: :class:`gcloud.logging.client.Client`
:returns: The client passed in or the currently bound client.
"""
Expand Down
99 changes: 99 additions & 0 deletions gcloud/logging/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,105 @@ def test_sink(self):
self.assertTrue(sink.client is client)
self.assertEqual(sink.project, self.PROJECT)

def test_list_sinks_no_paging(self):
from gcloud.logging.sink import Sink
PROJECT = 'PROJECT'
CREDS = _Credentials()

CLIENT_OBJ = self._makeOne(project=PROJECT, credentials=CREDS)

SINK_NAME = 'sink_name'
FILTER = 'logName:syslog AND severity>=ERROR'
SINK_PATH = 'projects/%s/sinks/%s' % (PROJECT, SINK_NAME)

RETURNED = {
'sinks': [{
'name': SINK_PATH,
'filter': FILTER,
'destination': self.DESTINATION_URI,
}],
}
# Replace the connection on the client with one of our own.
CLIENT_OBJ.connection = _Connection(RETURNED)

# Execute request.
sinks, next_page_token = CLIENT_OBJ.list_sinks()
# Test values are correct.
self.assertEqual(len(sinks), 1)
sink = sinks[0]
self.assertTrue(isinstance(sink, Sink))
self.assertEqual(sink.name, SINK_NAME)
self.assertEqual(sink.filter_, FILTER)
self.assertEqual(sink.destination, self.DESTINATION_URI)
self.assertEqual(next_page_token, None)
self.assertEqual(len(CLIENT_OBJ.connection._requested), 1)
req = CLIENT_OBJ.connection._requested[0]
self.assertEqual(req['method'], 'GET')
self.assertEqual(req['path'], '/projects/%s/sinks' % (PROJECT,))
self.assertEqual(req['query_params'], {})

def test_list_sinks_with_paging(self):
from gcloud.logging.sink import Sink
PROJECT = 'PROJECT'
CREDS = _Credentials()

CLIENT_OBJ = self._makeOne(project=PROJECT, credentials=CREDS)

SINK_NAME = 'sink_name'
FILTER = 'logName:syslog AND severity>=ERROR'
SINK_PATH = 'projects/%s/sinks/%s' % (PROJECT, SINK_NAME)
TOKEN1 = 'TOKEN1'
TOKEN2 = 'TOKEN2'
SIZE = 1
RETURNED = {
'sinks': [{
'name': SINK_PATH,
'filter': FILTER,
'destination': self.DESTINATION_URI,
}],
'nextPageToken': TOKEN2,
}
# Replace the connection on the client with one of our own.
CLIENT_OBJ.connection = _Connection(RETURNED)

# Execute request.
sinks, next_page_token = CLIENT_OBJ.list_sinks(SIZE, TOKEN1)
# Test values are correct.
self.assertEqual(len(sinks), 1)
sink = sinks[0]
self.assertTrue(isinstance(sink, Sink))
self.assertEqual(sink.name, SINK_NAME)
self.assertEqual(sink.filter_, FILTER)
self.assertEqual(sink.destination, self.DESTINATION_URI)
self.assertEqual(next_page_token, TOKEN2)
self.assertEqual(len(CLIENT_OBJ.connection._requested), 1)
req = CLIENT_OBJ.connection._requested[0]
self.assertEqual(req['method'], 'GET')
self.assertEqual(req['path'], '/projects/%s/sinks' % (PROJECT,))
self.assertEqual(req['query_params'],
{'pageSize': SIZE, 'pageToken': TOKEN1})

def test_list_sinks_missing_key(self):
PROJECT = 'PROJECT'
CREDS = _Credentials()

CLIENT_OBJ = self._makeOne(project=PROJECT, credentials=CREDS)

RETURNED = {}
# Replace the connection on the client with one of our own.
CLIENT_OBJ.connection = _Connection(RETURNED)

# Execute request.
sinks, next_page_token = CLIENT_OBJ.list_sinks()
# Test values are correct.
self.assertEqual(len(sinks), 0)
self.assertEqual(next_page_token, None)
self.assertEqual(len(CLIENT_OBJ.connection._requested), 1)
req = CLIENT_OBJ.connection._requested[0]
self.assertEqual(req['method'], 'GET')
self.assertEqual(req['path'], '/projects/%s/sinks' % PROJECT)
self.assertEqual(req['query_params'], {})


class _Credentials(object):

Expand Down
80 changes: 80 additions & 0 deletions gcloud/logging/test_sink.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,38 @@
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'
Expand Down Expand Up @@ -43,6 +75,54 @@ def test_ctor(self):
self.assertEqual(sink.full_name, FULL)
self.assertEqual(sink.path, '/%s' % (FULL,))

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,
'filter': self.FILTER,
'destination': self.DESTINATION_URI,
}
klass = self._getTargetClass()
sink = klass.from_api_repr(RESOURCE, client=CLIENT)
self.assertEqual(sink.name, self.SINK_NAME)
self.assertEqual(sink.filter_, self.FILTER)
self.assertEqual(sink.destination, self.DESTINATION_URI)
self.assertTrue(sink._client is CLIENT)
self.assertEqual(sink.project, self.PROJECT)
self.assertEqual(sink.full_name, FULL)

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,
'filter': self.FILTER,
'destination': self.DESTINATION_URI,
}
klass = self._getTargetClass()
sink = klass.from_api_repr(RESOURCE, client=CLIENT)
self.assertEqual(sink.name, self.SINK_NAME)
self.assertEqual(sink.filter_, self.FILTER)
self.assertEqual(sink.destination, self.DESTINATION_URI)
self.assertTrue(sink._client is CLIENT)
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):
FULL = 'projects/%s/sinks/%s' % (self.PROJECT, self.SINK_NAME)
RESOURCE = {
Expand Down