From 8faa331be43ba514e2296717292a7130b30f0cb4 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Mon, 14 Sep 2015 15:42:56 -0400 Subject: [PATCH 1/3] Add DNS client / connection. --- gcloud/dns/__init__.py | 23 +++++++++ gcloud/dns/client.py | 58 +++++++++++++++++++++ gcloud/dns/connection.py | 34 +++++++++++++ gcloud/dns/test_client.py | 94 +++++++++++++++++++++++++++++++++++ gcloud/dns/test_connection.py | 47 ++++++++++++++++++ 5 files changed, 256 insertions(+) create mode 100644 gcloud/dns/__init__.py create mode 100644 gcloud/dns/client.py create mode 100644 gcloud/dns/connection.py create mode 100644 gcloud/dns/test_client.py create mode 100644 gcloud/dns/test_connection.py diff --git a/gcloud/dns/__init__.py b/gcloud/dns/__init__.py new file mode 100644 index 000000000000..a073a7b55a0d --- /dev/null +++ b/gcloud/dns/__init__.py @@ -0,0 +1,23 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Google Cloud DNS API wrapper. + +The main concepts with this API are: + +- :class:`gcloud.DNS.zone.ManagedZone` represents an collection of tables. +""" + +from gcloud.dns.client import Client +from gcloud.dns.connection import Connection diff --git a/gcloud/dns/client.py b/gcloud/dns/client.py new file mode 100644 index 000000000000..40b84acdaf1c --- /dev/null +++ b/gcloud/dns/client.py @@ -0,0 +1,58 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""gcloud dns client for interacting with API.""" + + +from gcloud.client import JSONClient +from gcloud.dns.connection import Connection + + +class Client(JSONClient): + """Client to bundle configuration needed for API requests. + + :type project: string + :param project: the project which the client acts on behalf of. Will be + passed when creating a dataset / job. If not passed, + falls back to the default inferred from the environment. + + :type credentials: :class:`oauth2client.client.OAuth2Credentials` or + :class:`NoneType` + :param credentials: The OAuth2 Credentials to use for the connection + owned by this client. If not passed (and if no ``http`` + object is passed), falls back to the default inferred + from the environment. + + :type http: :class:`httplib2.Http` or class that defines ``request()``. + :param http: An optional HTTP object to make requests. If not passed, an + ``http`` object is created that is bound to the + ``credentials`` for the current object. + """ + + _connection_class = Connection + + def quotas(self): + """Return DNS quots for the project associated with this client. + + See: + https://cloud.google.com/dns/api/v1/projects/get + + :rtype: mapping + :returns: keys for the mapping correspond to those of the ``quota`` + sub-mapping of the project resource. + """ + path = '/projects/%s' % (self.project,) + resp = self.connection.api_request(method='GET', path=path) + return dict([(key, int(value)) + for key, value in resp['quota'].items()]) diff --git a/gcloud/dns/connection.py b/gcloud/dns/connection.py new file mode 100644 index 000000000000..e997780f12ad --- /dev/null +++ b/gcloud/dns/connection.py @@ -0,0 +1,34 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Create / interact with gcloud dns connections.""" + +from gcloud import connection as base_connection + + +class Connection(base_connection.JSONConnection): + """A connection to Google Cloud DNS via the JSON REST API.""" + + API_BASE_URL = 'https://www.googleapis.com' + """The base of the API call URL.""" + + API_VERSION = 'v1' + """The version of the API, used in building the API call's URL.""" + + API_URL_TEMPLATE = '{api_base_url}/dns/{api_version}{path}' + """A template for the URL of a particular API call.""" + + SCOPE = ('https://www.googleapis.com/auth/ndev.clouddns.readwrite', + 'https://www.googleapis.com/auth/cloud-platform') + """The scopes required for authenticating as a Cloud DNS consumer.""" diff --git a/gcloud/dns/test_client.py b/gcloud/dns/test_client.py new file mode 100644 index 000000000000..fdba2c7cbf82 --- /dev/null +++ b/gcloud/dns/test_client.py @@ -0,0 +1,94 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest2 + + +class TestClient(unittest2.TestCase): + + def _getTargetClass(self): + from gcloud.dns.client import Client + return Client + + def _makeOne(self, *args, **kw): + return self._getTargetClass()(*args, **kw) + + def test_ctor(self): + from gcloud.dns.connection import Connection + PROJECT = 'PROJECT' + creds = _Credentials() + http = object() + client = self._makeOne(project=PROJECT, credentials=creds, http=http) + self.assertTrue(isinstance(client.connection, Connection)) + self.assertTrue(client.connection.credentials is creds) + self.assertTrue(client.connection.http is http) + + def test_quotas_defaults(self): + PROJECT = 'PROJECT' + PATH = 'projects/%s' % PROJECT + MANAGED_ZONES = 1234 + RRS_PER_RRSET = 23 + RRSETS_PER_ZONE = 345 + RRSET_ADDITIONS = 456 + RRSET_DELETIONS = 567 + TOTAL_SIZE = 67890 + DATA = { + 'quota': { + 'managedZones': str(MANAGED_ZONES), + 'resourceRecordsPerRrset': str(RRS_PER_RRSET), + 'rrsetsPerManagedZone': str(RRSETS_PER_ZONE), + 'rrsetAdditionsPerChange': str(RRSET_ADDITIONS), + 'rrsetDeletionsPerChange': str(RRSET_DELETIONS), + 'totalRrdataSizePerChange': str(TOTAL_SIZE), + } + } + CONVERTED = dict([(key, int(value)) + for key, value in DATA['quota'].items()]) + creds = _Credentials() + client = self._makeOne(PROJECT, creds) + conn = client.connection = _Connection(DATA) + + quotas = client.quotas() + + self.assertEqual(quotas, CONVERTED) + + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + self.assertEqual(req['method'], 'GET') + self.assertEqual(req['path'], '/%s' % PATH) + + +class _Credentials(object): + + _scopes = None + + @staticmethod + def create_scoped_required(): + return True + + def create_scoped(self, scope): + self._scopes = scope + return self + + +class _Connection(object): + + def __init__(self, *responses): + self._responses = responses + self._requested = [] + + def api_request(self, **kw): + self._requested.append(kw) + response, self._responses = self._responses[0], self._responses[1:] + return response diff --git a/gcloud/dns/test_connection.py b/gcloud/dns/test_connection.py new file mode 100644 index 000000000000..1a3f777399f5 --- /dev/null +++ b/gcloud/dns/test_connection.py @@ -0,0 +1,47 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest2 + + +class TestConnection(unittest2.TestCase): + + def _getTargetClass(self): + from gcloud.dns.connection import Connection + return Connection + + def _makeOne(self, *args, **kw): + return self._getTargetClass()(*args, **kw) + + def test_build_api_url_no_extra_query_params(self): + conn = self._makeOne() + URI = '/'.join([ + conn.API_BASE_URL, + 'dns', + conn.API_VERSION, + 'foo', + ]) + self.assertEqual(conn.build_api_url('/foo'), URI) + + def test_build_api_url_w_extra_query_params(self): + from six.moves.urllib.parse import parse_qsl + from six.moves.urllib.parse import urlsplit + conn = self._makeOne() + uri = conn.build_api_url('/foo', {'bar': 'baz'}) + scheme, netloc, path, qs, _ = urlsplit(uri) + self.assertEqual('%s://%s' % (scheme, netloc), conn.API_BASE_URL) + self.assertEqual(path, + '/'.join(['', 'dns', conn.API_VERSION, 'foo'])) + parms = dict(parse_qsl(qs)) + self.assertEqual(parms['bar'], 'baz') From b3407617c723d5bac579074262166ac6790be9d6 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Tue, 15 Sep 2015 12:36:33 -0400 Subject: [PATCH 2/3] Add top-level 'SCOPE' alias for DNS. --- gcloud/dns/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gcloud/dns/__init__.py b/gcloud/dns/__init__.py index a073a7b55a0d..315d6f1a3fe8 100644 --- a/gcloud/dns/__init__.py +++ b/gcloud/dns/__init__.py @@ -21,3 +21,6 @@ from gcloud.dns.client import Client from gcloud.dns.connection import Connection + + +SCOPE = Connection.SCOPE From 23eadfc9879c2134813e9b2dfb937caae55cdae6 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Fri, 18 Sep 2015 11:41:47 -0400 Subject: [PATCH 3/3] Narrow default scopes. Addresses: https://github.com/GoogleCloudPlatform/gcloud-python/pull/1138#discussion_r39804708 --- gcloud/dns/connection.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gcloud/dns/connection.py b/gcloud/dns/connection.py index e997780f12ad..e2b382fd9daa 100644 --- a/gcloud/dns/connection.py +++ b/gcloud/dns/connection.py @@ -29,6 +29,5 @@ class Connection(base_connection.JSONConnection): API_URL_TEMPLATE = '{api_base_url}/dns/{api_version}{path}' """A template for the URL of a particular API call.""" - SCOPE = ('https://www.googleapis.com/auth/ndev.clouddns.readwrite', - 'https://www.googleapis.com/auth/cloud-platform') + SCOPE = ('https://www.googleapis.com/auth/ndev.clouddns.readwrite',) """The scopes required for authenticating as a Cloud DNS consumer."""