From 6ed3d4c645670097d106f801cd22a5af48481ebb Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Tue, 15 Sep 2015 13:21:53 -0400 Subject: [PATCH 1/6] Add 'ManagedZone' class. --- gcloud/dns/__init__.py | 1 + gcloud/dns/test_zone.py | 315 ++++++++++++++++++++++++++++++++++++++++ gcloud/dns/zone.py | 246 +++++++++++++++++++++++++++++++ 3 files changed, 562 insertions(+) create mode 100644 gcloud/dns/test_zone.py create mode 100644 gcloud/dns/zone.py diff --git a/gcloud/dns/__init__.py b/gcloud/dns/__init__.py index 315d6f1a3fe8..19770d2f956d 100644 --- a/gcloud/dns/__init__.py +++ b/gcloud/dns/__init__.py @@ -21,6 +21,7 @@ from gcloud.dns.client import Client from gcloud.dns.connection import Connection +from gcloud.dns.zone import ManagedZone SCOPE = Connection.SCOPE diff --git a/gcloud/dns/test_zone.py b/gcloud/dns/test_zone.py new file mode 100644 index 000000000000..18fa9f657cf6 --- /dev/null +++ b/gcloud/dns/test_zone.py @@ -0,0 +1,315 @@ +# 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 TestManagedZone(unittest2.TestCase): + PROJECT = 'project' + ZONE_NAME = 'zone-name' + DESCRIPTION = 'ZONE DESCRIPTION' + DNS_NAME = 'test.example.com' + + def _getTargetClass(self): + from gcloud.dns.zone import ManagedZone + return ManagedZone + + def _makeOne(self, *args, **kw): + return self._getTargetClass()(*args, **kw) + + def _setUpConstants(self): + import datetime + import os + import time + from gcloud._helpers import UTC + + self.WHEN_TS = time.time() + self.WHEN = datetime.datetime.utcfromtimestamp(self.WHEN_TS).replace( + tzinfo=UTC) + self.ZONE_ID = os.getpid() + + def _makeResource(self): + self._setUpConstants() + return { + 'name': self.ZONE_NAME, + 'dnsName': self.DNS_NAME, + 'description': self.DESCRIPTION, + 'id': self.ZONE_ID, + 'creationTime': self.WHEN_TS * 1000, + 'nameServers': [ + 'ns-cloud1.googledomains.com', + 'ns-cloud2.googledomains.com', + ], + } + + def _verifyReadonlyResourceProperties(self, zone, resource): + + self.assertEqual(zone.zone_id, resource.get('id')) + + if 'creationTime' in resource: + self.assertEqual(zone.created, self.WHEN) + else: + self.assertEqual(zone.created, None) + + if 'nameServers' in resource: + self.assertEqual(zone.name_servers, resource['nameServers']) + else: + self.assertEqual(zone.name_servers, None) + + def _verifyResourceProperties(self, zone, resource): + + self._verifyReadonlyResourceProperties(zone, resource) + + self.assertEqual(zone.name, resource.get('name')) + self.assertEqual(zone.dns_name, resource.get('dnsName')) + self.assertEqual(zone.description, resource.get('description')) + self.assertEqual(zone.zone_id, resource.get('id')) + self.assertEqual(zone.name_server_set, resource.get('nameServerSet')) + + def test_ctor(self): + client = _Client(self.PROJECT) + zone = self._makeOne(self.ZONE_NAME, self.DNS_NAME, client) + self.assertEqual(zone.name, self.ZONE_NAME) + self.assertEqual(zone.dns_name, self.DNS_NAME) + self.assertTrue(zone._client is client) + self.assertEqual(zone.project, client.project) + self.assertEqual( + zone.path, + '/projects/%s/managedZones/%s' % (self.PROJECT, self.ZONE_NAME)) + + self.assertEqual(zone.zone_id, None) + self.assertEqual(zone.created, None) + + self.assertEqual(zone.description, None) + + def test_description_setter_bad_value(self): + client = _Client(self.PROJECT) + zone = self._makeOne(self.ZONE_NAME, self.DNS_NAME, client) + with self.assertRaises(ValueError): + zone.description = 12345 + + def test_description_setter(self): + client = _Client(self.PROJECT) + zone = self._makeOne(self.ZONE_NAME, self.DNS_NAME, client) + zone.description = 'DESCRIPTION' + self.assertEqual(zone.description, 'DESCRIPTION') + + def test_name_server_set_setter_bad_value(self): + client = _Client(self.PROJECT) + zone = self._makeOne(self.ZONE_NAME, self.DNS_NAME, client) + with self.assertRaises(ValueError): + zone.name_server_set = 12345 + + def test_name_server_set_setter(self): + client = _Client(self.PROJECT) + zone = self._makeOne(self.ZONE_NAME, self.DNS_NAME, client) + zone.name_server_set = 'NAME_SERVER_SET' + self.assertEqual(zone.name_server_set, 'NAME_SERVER_SET') + + def test_create_w_bound_client(self): + PATH = 'projects/%s/managedZones' % self.PROJECT + RESOURCE = self._makeResource() + conn = _Connection(RESOURCE) + client = _Client(project=self.PROJECT, connection=conn) + zone = self._makeOne(self.ZONE_NAME, self.DNS_NAME, client) + + zone.create() + + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + self.assertEqual(req['method'], 'POST') + self.assertEqual(req['path'], '/%s' % PATH) + SENT = { + 'name': self.ZONE_NAME, + 'dnsName': self.DNS_NAME, + } + self.assertEqual(req['data'], SENT) + self._verifyResourceProperties(zone, RESOURCE) + + def test_create_w_alternate_client(self): + PATH = 'projects/%s/managedZones' % self.PROJECT + USER_EMAIL = 'phred@example.com' + GROUP_EMAIL = 'group-name@lists.example.com' + DESCRIPTION = 'DESCRIPTION' + NAME_SERVER_SET = 'NAME_SERVER_SET' + RESOURCE = self._makeResource() + RESOURCE['nameServerSet'] = NAME_SERVER_SET + RESOURCE['description'] = DESCRIPTION + conn1 = _Connection() + client1 = _Client(project=self.PROJECT, connection=conn1) + conn2 = _Connection(RESOURCE) + client2 = _Client(project=self.PROJECT, connection=conn2) + zone = self._makeOne(self.ZONE_NAME, self.DNS_NAME, client1) + zone.name_server_set = NAME_SERVER_SET + zone.description = DESCRIPTION + + zone.create(client=client2) + + self.assertEqual(len(conn1._requested), 0) + self.assertEqual(len(conn2._requested), 1) + req = conn2._requested[0] + self.assertEqual(req['method'], 'POST') + self.assertEqual(req['path'], '/%s' % PATH) + SENT = { + 'name': self.ZONE_NAME, + 'dnsName': self.DNS_NAME, + 'nameServerSet': NAME_SERVER_SET, + 'description': DESCRIPTION, + } + self.assertEqual(req['data'], SENT) + self._verifyResourceProperties(zone, RESOURCE) + + def test_create_w_missing_output_properties(self): + # In the wild, the resource returned from 'zone.create' sometimes + # lacks 'creationTime' / 'lastModifiedTime' + PATH = 'projects/%s/managedZones' % (self.PROJECT,) + RESOURCE = self._makeResource() + del RESOURCE['creationTime'] + del RESOURCE['id'] + del RESOURCE['nameServers'] + self.WHEN = None + conn = _Connection(RESOURCE) + client = _Client(project=self.PROJECT, connection=conn) + zone = self._makeOne(self.ZONE_NAME, self.DNS_NAME, client) + + zone.create() + + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + self.assertEqual(req['method'], 'POST') + self.assertEqual(req['path'], '/%s' % PATH) + SENT = { + 'name': self.ZONE_NAME, + 'dnsName': self.DNS_NAME, + } + self.assertEqual(req['data'], SENT) + self._verifyResourceProperties(zone, RESOURCE) + + def test_exists_miss_w_bound_client(self): + PATH = 'projects/%s/managedZones/%s' % (self.PROJECT, self.ZONE_NAME) + conn = _Connection() + client = _Client(project=self.PROJECT, connection=conn) + zone = self._makeOne(self.ZONE_NAME, self.DNS_NAME, client) + + self.assertFalse(zone.exists()) + + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + self.assertEqual(req['method'], 'GET') + self.assertEqual(req['path'], '/%s' % PATH) + self.assertEqual(req['query_params'], {'fields': 'id'}) + + def test_exists_hit_w_alternate_client(self): + PATH = 'projects/%s/managedZones/%s' % (self.PROJECT, self.ZONE_NAME) + conn1 = _Connection() + client1 = _Client(project=self.PROJECT, connection=conn1) + conn2 = _Connection({}) + client2 = _Client(project=self.PROJECT, connection=conn2) + zone = self._makeOne(self.ZONE_NAME, self.DNS_NAME, client1) + + self.assertTrue(zone.exists(client=client2)) + + self.assertEqual(len(conn1._requested), 0) + self.assertEqual(len(conn2._requested), 1) + req = conn2._requested[0] + self.assertEqual(req['method'], 'GET') + self.assertEqual(req['path'], '/%s' % PATH) + self.assertEqual(req['query_params'], {'fields': 'id'}) + + def test_reload_w_bound_client(self): + PATH = 'projects/%s/managedZones/%s' % (self.PROJECT, self.ZONE_NAME) + RESOURCE = self._makeResource() + conn = _Connection(RESOURCE) + client = _Client(project=self.PROJECT, connection=conn) + zone = self._makeOne(self.ZONE_NAME, self.DNS_NAME, client) + + zone.reload() + + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + self.assertEqual(req['method'], 'GET') + self.assertEqual(req['path'], '/%s' % PATH) + self._verifyResourceProperties(zone, RESOURCE) + + def test_reload_w_alternate_client(self): + PATH = 'projects/%s/managedZones/%s' % (self.PROJECT, self.ZONE_NAME) + RESOURCE = self._makeResource() + conn1 = _Connection() + client1 = _Client(project=self.PROJECT, connection=conn1) + conn2 = _Connection(RESOURCE) + client2 = _Client(project=self.PROJECT, connection=conn2) + zone = self._makeOne(self.ZONE_NAME, self.DNS_NAME, client1) + + zone.reload(client=client2) + + self.assertEqual(len(conn1._requested), 0) + self.assertEqual(len(conn2._requested), 1) + req = conn2._requested[0] + self.assertEqual(req['method'], 'GET') + self.assertEqual(req['path'], '/%s' % PATH) + self._verifyResourceProperties(zone, RESOURCE) + + def test_delete_w_bound_client(self): + PATH = 'projects/%s/managedZones/%s' % (self.PROJECT, self.ZONE_NAME) + conn = _Connection({}) + client = _Client(project=self.PROJECT, connection=conn) + zone = self._makeOne(self.ZONE_NAME, self.DNS_NAME, client) + + zone.delete() + + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + self.assertEqual(req['method'], 'DELETE') + self.assertEqual(req['path'], '/%s' % PATH) + + def test_delete_w_alternate_client(self): + PATH = 'projects/%s/managedZones/%s' % (self.PROJECT, self.ZONE_NAME) + conn1 = _Connection() + client1 = _Client(project=self.PROJECT, connection=conn1) + conn2 = _Connection({}) + client2 = _Client(project=self.PROJECT, connection=conn2) + zone = self._makeOne(self.ZONE_NAME, self.DNS_NAME, client1) + + zone.delete(client=client2) + + self.assertEqual(len(conn1._requested), 0) + self.assertEqual(len(conn2._requested), 1) + req = conn2._requested[0] + self.assertEqual(req['method'], 'DELETE') + self.assertEqual(req['path'], '/%s' % PATH) + + +class _Client(object): + + def __init__(self, project='project', connection=None): + self.project = project + self.connection = connection + + +class _Connection(object): + + def __init__(self, *responses): + self._responses = responses + self._requested = [] + + def api_request(self, **kw): + from gcloud.exceptions import NotFound + self._requested.append(kw) + + try: + response, self._responses = self._responses[0], self._responses[1:] + except: + raise NotFound('miss') + else: + return response diff --git a/gcloud/dns/zone.py b/gcloud/dns/zone.py new file mode 100644 index 000000000000..19b6b53cebc6 --- /dev/null +++ b/gcloud/dns/zone.py @@ -0,0 +1,246 @@ +# 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. + +"""Define API ManagedZones.""" +import six + +from gcloud._helpers import _datetime_from_microseconds +from gcloud.exceptions import NotFound + + +class ManagedZone(object): + """ManagedZone are containers for DNS resource records. + + See: + https://cloud.google.com/dns/api/v1/managedZones + + :type name: string + :param name: the name of the zone + + :type dns_name: string + :param dns_name: the DNS name of the zone + + :type client: :class:`gcloud.dns.client.Client` + :param client: A client which holds credentials and project configuration + for the zone (which requires a project). + """ + + def __init__(self, name, dns_name, client): + self.name = name + self.dns_name = dns_name + self._client = client + self._properties = {} + + @property + def project(self): + """Project bound to the zone. + + :rtype: string + :returns: the project (derived from the client). + """ + return self._client.project + + @property + def path(self): + """URL path for the zone's APIs. + + :rtype: string + :returns: the path based on project and dataste name. + """ + return '/projects/%s/managedZones/%s' % (self.project, self.name) + + @property + def created(self): + """Datetime at which the zone was created. + + :rtype: ``datetime.datetime``, or ``NoneType`` + :returns: the creation time (None until set from the server). + """ + creation_time = self._properties.get('creationTime') + if creation_time is not None: + # creation_time will be in milliseconds. + return _datetime_from_microseconds(1000.0 * creation_time) + + @property + def name_servers(self): + """Datetime at which the zone was created. + + :rtype: list of strings, or ``NoneType``. + :returns: the assigned name servers (None until set from the server). + """ + return self._properties.get('nameServers') + + @property + def zone_id(self): + """ID for the zone resource. + + :rtype: string, or ``NoneType`` + :returns: the ID (None until set from the server). + """ + return self._properties.get('id') + + @property + def description(self): + """Description of the zone. + + :rtype: string, or ``NoneType`` + :returns: The description as set by the user, or None (the default). + """ + return self._properties.get('description') + + @description.setter + def description(self, value): + """Update description of the zone. + + :type value: string, or ``NoneType`` + :param value: new description + + :raises: ValueError for invalid value types. + """ + if not isinstance(value, six.string_types) and value is not None: + raise ValueError("Pass a string, or None") + self._properties['description'] = value + + @property + def name_server_set(self): + """Named set of DNS name servers that all host the same ManagedZones. + + Most users will leave this blank. + + See: + https://cloud.google.com/dns/api/v1/managedZones#nameServerSet + + :rtype: string, or ``NoneType`` + :returns: The name as set by the user, or None (the default). + """ + return self._properties.get('nameServerSet') + + @name_server_set.setter + def name_server_set(self, value): + """Update named set of DNS name servers. + + :type value: string, or ``NoneType`` + :param value: new title + + :raises: ValueError for invalid value types. + """ + if not isinstance(value, six.string_types) and value is not None: + raise ValueError("Pass a string, or None") + self._properties['nameServerSet'] = value + + def _require_client(self, client): + """Check client or verify over-ride. + + :type client: :class:`gcloud.dns.client.Client` or ``NoneType`` + :param client: the client to use. If not passed, falls back to the + ``client`` stored on the current zone. + + :rtype: :class:`gcloud.dns.client.Client` + :returns: The client passed in or the currently bound client. + """ + if client is None: + client = self._client + return client + + def _set_properties(self, api_response): + """Update properties from resource in body of ``api_response`` + + :type api_response: httplib2.Response + :param api_response: response returned from an API call + """ + self._properties.clear() + cleaned = api_response.copy() + if 'creationTime' in cleaned: + cleaned['creationTime'] = float(cleaned['creationTime']) + self._properties.update(cleaned) + + def _build_resource(self): + """Generate a resource for ``create`` or ``update``.""" + resource = { + 'name': self.name, + 'dnsName': self.dns_name, + } + + if self.description is not None: + resource['description'] = self.description + + if self.name_server_set is not None: + resource['nameServerSet'] = self.name_server_set + + return resource + + def create(self, client=None): + """API call: create the zone via a PUT request + + See: + https://cloud.google.com/dns/api/v1/managedZones/create + + :type client: :class:`gcloud.dns.client.Client` or ``NoneType`` + :param client: the client to use. If not passed, falls back to the + ``client`` stored on the current zone. + """ + client = self._require_client(client) + path = '/projects/%s/managedZones' % (self.project,) + api_response = client.connection.api_request( + method='POST', path=path, data=self._build_resource()) + self._set_properties(api_response) + + def exists(self, client=None): + """API call: test for the existence of the zone via a GET request + + See + https://cloud.google.com/dns/api/v1/managedZones/get + + :type client: :class:`gcloud.dns.client.Client` or ``NoneType`` + :param client: the client to use. If not passed, falls back to the + ``client`` stored on the current zone. + """ + client = self._require_client(client) + + try: + client.connection.api_request(method='GET', path=self.path, + query_params={'fields': 'id'}) + except NotFound: + return False + else: + return True + + def reload(self, client=None): + """API call: refresh zone properties via a GET request + + See + https://cloud.google.com/dns/api/v1/managedZones/get + + :type client: :class:`gcloud.dns.client.Client` or ``NoneType`` + :param client: the client to use. If not passed, falls back to the + ``client`` stored on the current zone. + """ + client = self._require_client(client) + + api_response = client.connection.api_request( + method='GET', path=self.path) + self._set_properties(api_response) + + def delete(self, client=None): + """API call: delete the zone via a DELETE request + + See: + https://cloud.google.com/dns/api/v1/managedZones/delete + + :type client: :class:`gcloud.dns.client.Client` or ``NoneType`` + :param client: the client to use. If not passed, falls back to the + ``client`` stored on the current zone. + """ + client = self._require_client(client) + client.connection.api_request(method='DELETE', path=self.path) From b62270c50b1d99f7967cd6e693f46893ef7696e0 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Tue, 15 Sep 2015 13:45:00 -0400 Subject: [PATCH 2/6] Add 'Client.list_zones'. --- gcloud/dns/client.py | 39 +++++++++++++++++- gcloud/dns/test_client.py | 87 +++++++++++++++++++++++++++++++++++++++ gcloud/dns/test_zone.py | 31 +++++++++++++- gcloud/dns/zone.py | 23 +++++++++++ 4 files changed, 177 insertions(+), 3 deletions(-) diff --git a/gcloud/dns/client.py b/gcloud/dns/client.py index 40b84acdaf1c..0e6c92ff80a7 100644 --- a/gcloud/dns/client.py +++ b/gcloud/dns/client.py @@ -17,6 +17,7 @@ from gcloud.client import JSONClient from gcloud.dns.connection import Connection +from gcloud.dns.zone import ManagedZone class Client(JSONClient): @@ -24,7 +25,7 @@ class Client(JSONClient): :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, + passed when creating a zone. If not passed, falls back to the default inferred from the environment. :type credentials: :class:`oauth2client.client.OAuth2Credentials` or @@ -56,3 +57,39 @@ def quotas(self): resp = self.connection.api_request(method='GET', path=path) return dict([(key, int(value)) for key, value in resp['quota'].items()]) + + def list_zones(self, max_results=None, page_token=None): + """List zones for the project associated with this client. + + See: + https://cloud.google.com/dns/api/v1/managedZones/list + + :type max_results: int + :param max_results: maximum number of zones 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 zones. If + not passed, the API will return the first page of + zones. + + :rtype: tuple, (list, str) + :returns: list of :class:`gcloud.dns.zone.ManagedZone`, plus a + "next page token" string: if the token is not None, + indicates that more zones can be retrieved with another + call (pass that value as ``page_token``). + """ + params = {} + + if max_results is not None: + params['maxResults'] = max_results + + if page_token is not None: + params['pageToken'] = page_token + + path = '/projects/%s/managedZones' % (self.project,) + resp = self.connection.api_request(method='GET', path=path, + query_params=params) + zones = [ManagedZone.from_api_repr(resource, self) + for resource in resp['managedZones']] + return zones, resp.get('nextPageToken') diff --git a/gcloud/dns/test_client.py b/gcloud/dns/test_client.py index fdba2c7cbf82..fab929c2d482 100644 --- a/gcloud/dns/test_client.py +++ b/gcloud/dns/test_client.py @@ -68,6 +68,93 @@ def test_quotas_defaults(self): self.assertEqual(req['method'], 'GET') self.assertEqual(req['path'], '/%s' % PATH) + def test_list_zones_defaults(self): + from gcloud.dns.zone import ManagedZone + PROJECT = 'PROJECT' + ID_1 = '123' + ZONE_1 = 'zone_one' + DNS_1 = 'one.example.com' + ID_2 = '234' + ZONE_2 = 'zone_two' + DNS_2 = 'two.example.com' + PATH = 'projects/%s/managedZones' % PROJECT + TOKEN = 'TOKEN' + DATA = { + 'nextPageToken': TOKEN, + 'managedZones': [ + {'kind': 'dns#managedZone', + 'id': ID_1, + 'name': ZONE_1, + 'dnsName': DNS_1}, + {'kind': 'dns#managedZone', + 'id': ID_2, + 'name': ZONE_2, + 'dnsName': DNS_2}, + ] + } + creds = _Credentials() + client = self._makeOne(PROJECT, creds) + conn = client.connection = _Connection(DATA) + + zones, token = client.list_zones() + + self.assertEqual(len(zones), len(DATA['managedZones'])) + for found, expected in zip(zones, DATA['managedZones']): + self.assertTrue(isinstance(found, ManagedZone)) + self.assertEqual(found.zone_id, expected['id']) + self.assertEqual(found.name, expected['name']) + self.assertEqual(found.dns_name, expected['dnsName']) + self.assertEqual(token, TOKEN) + + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + self.assertEqual(req['method'], 'GET') + self.assertEqual(req['path'], '/%s' % PATH) + + def test_list_zones_explicit(self): + from gcloud.dns.zone import ManagedZone + PROJECT = 'PROJECT' + ID_1 = '123' + ZONE_1 = 'zone_one' + DNS_1 = 'one.example.com' + ID_2 = '234' + ZONE_2 = 'zone_two' + DNS_2 = 'two.example.com' + PATH = 'projects/%s/managedZones' % PROJECT + TOKEN = 'TOKEN' + DATA = { + 'managedZones': [ + {'kind': 'dns#managedZone', + 'id': ID_1, + 'name': ZONE_1, + 'dnsName': DNS_1}, + {'kind': 'dns#managedZone', + 'id': ID_2, + 'name': ZONE_2, + 'dnsName': DNS_2}, + ] + } + creds = _Credentials() + client = self._makeOne(PROJECT, creds) + conn = client.connection = _Connection(DATA) + + zones, token = client.list_zones(max_results=3, page_token=TOKEN) + + self.assertEqual(len(zones), len(DATA['managedZones'])) + for found, expected in zip(zones, DATA['managedZones']): + self.assertTrue(isinstance(found, ManagedZone)) + self.assertEqual(found.zone_id, expected['id']) + self.assertEqual(found.name, expected['name']) + self.assertEqual(found.dns_name, expected['dnsName']) + self.assertEqual(token, None) + + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + self.assertEqual(req['method'], 'GET') + self.assertEqual(req['path'], '/%s' % PATH) + self.assertEqual(req['query_params'], + {'maxResults': 3, 'pageToken': TOKEN}) + class _Credentials(object): diff --git a/gcloud/dns/test_zone.py b/gcloud/dns/test_zone.py index 18fa9f657cf6..f35b124aebce 100644 --- a/gcloud/dns/test_zone.py +++ b/gcloud/dns/test_zone.py @@ -93,6 +93,35 @@ def test_ctor(self): self.assertEqual(zone.description, None) + def test_from_api_repr_missing_identity(self): + self._setUpConstants() + client = _Client(self.PROJECT) + RESOURCE = {} + klass = self._getTargetClass() + with self.assertRaises(KeyError): + klass.from_api_repr(RESOURCE, client=client) + + def test_from_api_repr_bare(self): + self._setUpConstants() + client = _Client(self.PROJECT) + RESOURCE = { + 'name': self.ZONE_NAME, + 'dnsName': self.DNS_NAME, + } + klass = self._getTargetClass() + zone = klass.from_api_repr(RESOURCE, client=client) + self.assertTrue(zone._client is client) + self._verifyResourceProperties(zone, RESOURCE) + + def test_from_api_repr_w_properties(self): + self._setUpConstants() + client = _Client(self.PROJECT) + RESOURCE = self._makeResource() + klass = self._getTargetClass() + zone = klass.from_api_repr(RESOURCE, client=client) + self.assertTrue(zone._client is client) + self._verifyResourceProperties(zone, RESOURCE) + def test_description_setter_bad_value(self): client = _Client(self.PROJECT) zone = self._makeOne(self.ZONE_NAME, self.DNS_NAME, client) @@ -139,8 +168,6 @@ def test_create_w_bound_client(self): def test_create_w_alternate_client(self): PATH = 'projects/%s/managedZones' % self.PROJECT - USER_EMAIL = 'phred@example.com' - GROUP_EMAIL = 'group-name@lists.example.com' DESCRIPTION = 'DESCRIPTION' NAME_SERVER_SET = 'NAME_SERVER_SET' RESOURCE = self._makeResource() diff --git a/gcloud/dns/zone.py b/gcloud/dns/zone.py index 19b6b53cebc6..2b69f1759f87 100644 --- a/gcloud/dns/zone.py +++ b/gcloud/dns/zone.py @@ -42,6 +42,29 @@ def __init__(self, name, dns_name, client): self._client = client self._properties = {} + @classmethod + def from_api_repr(cls, resource, client): + """Factory: construct a zone given its API representation + + :type resource: dict + :param resource: zone resource representation returned from the API + + :type client: :class:`gcloud.dns.client.Client` + :param client: Client which holds credentials and project + configuration for the zone. + + :rtype: :class:`gcloud.dns.zone.ManagedZone` + :returns: Zone parsed from ``resource``. + """ + name = resource.get('name') + dns_name = resource.get('dnsName') + if name is None or dns_name is None: + raise KeyError('Resource lacks required identity information:' + '["name"]["dnsName"]') + zone = cls(name, dns_name, client=client) + zone._set_properties(resource) + return zone + @property def project(self): """Project bound to the zone. From d5403dda0866f3cb653aec54577f276e8dd0210b Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Tue, 15 Sep 2015 13:54:02 -0400 Subject: [PATCH 3/6] Add 'Client.zone()' convenience factory. --- gcloud/dns/client.py | 14 ++++++++++++++ gcloud/dns/test_client.py | 13 +++++++++++++ 2 files changed, 27 insertions(+) diff --git a/gcloud/dns/client.py b/gcloud/dns/client.py index 0e6c92ff80a7..44db14a504d0 100644 --- a/gcloud/dns/client.py +++ b/gcloud/dns/client.py @@ -93,3 +93,17 @@ def list_zones(self, max_results=None, page_token=None): zones = [ManagedZone.from_api_repr(resource, self) for resource in resp['managedZones']] return zones, resp.get('nextPageToken') + + def zone(self, name, dns_name): + """Construct a zone bound to this client. + + :type name: string + :param name: Name of the zone. + + :type dns_name: string + :param dns_name: DNS name of the zone. + + :rtype: :class:`gcloud.dns.zone.ManagedZone` + :returns: a new ``ManagedZone`` instance + """ + return ManagedZone(name, dns_name, client=self) diff --git a/gcloud/dns/test_client.py b/gcloud/dns/test_client.py index fab929c2d482..295a5065fcc1 100644 --- a/gcloud/dns/test_client.py +++ b/gcloud/dns/test_client.py @@ -155,6 +155,19 @@ def test_list_zones_explicit(self): self.assertEqual(req['query_params'], {'maxResults': 3, 'pageToken': TOKEN}) + def test_zone(self): + from gcloud.dns.zone import ManagedZone + PROJECT = 'PROJECT' + ZONE_NAME = 'zone-name' + DNS_NAME = 'test.example.com' + creds = _Credentials() + client = self._makeOne(PROJECT, creds) + dataset = client.zone(ZONE_NAME, DNS_NAME) + self.assertTrue(isinstance(dataset, ManagedZone)) + self.assertEqual(dataset.name, ZONE_NAME) + self.assertEqual(dataset.dns_name, DNS_NAME) + self.assertTrue(dataset._client is client) + class _Credentials(object): From 4d8aec6e28167d5a8f996126d548dfab2d4431e2 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Tue, 15 Sep 2015 14:26:37 -0400 Subject: [PATCH 4/6] Nail down timestamp to avoid spurious rounding issues. --- gcloud/dns/test_zone.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gcloud/dns/test_zone.py b/gcloud/dns/test_zone.py index f35b124aebce..b19462053b5c 100644 --- a/gcloud/dns/test_zone.py +++ b/gcloud/dns/test_zone.py @@ -31,10 +31,9 @@ def _makeOne(self, *args, **kw): def _setUpConstants(self): import datetime import os - import time from gcloud._helpers import UTC - self.WHEN_TS = time.time() + self.WHEN_TS = 1437767599.006 self.WHEN = datetime.datetime.utcfromtimestamp(self.WHEN_TS).replace( tzinfo=UTC) self.ZONE_ID = os.getpid() From 83fc1e96eedd3de7fdea56154b9d529c8da20963 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Fri, 18 Sep 2015 12:15:49 -0400 Subject: [PATCH 5/6] Make zone ID deterministic. Addresses: https://github.com/GoogleCloudPlatform/gcloud-python/pull/1140#discussion_r39871789. --- gcloud/dns/test_zone.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gcloud/dns/test_zone.py b/gcloud/dns/test_zone.py index b19462053b5c..a96daa868ef7 100644 --- a/gcloud/dns/test_zone.py +++ b/gcloud/dns/test_zone.py @@ -30,13 +30,12 @@ def _makeOne(self, *args, **kw): def _setUpConstants(self): import datetime - import os from gcloud._helpers import UTC self.WHEN_TS = 1437767599.006 self.WHEN = datetime.datetime.utcfromtimestamp(self.WHEN_TS).replace( tzinfo=UTC) - self.ZONE_ID = os.getpid() + self.ZONE_ID = 12345 def _makeResource(self): self._setUpConstants() From 3f8f4c00c139af01f24a64fa14007c0c99a2d4c1 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Fri, 18 Sep 2015 12:17:05 -0400 Subject: [PATCH 6/6] Subject/verb agreement in number. Addreesses: https://github.com/GoogleCloudPlatform/gcloud-python/pull/1140#discussion_r39871893 --- gcloud/dns/zone.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gcloud/dns/zone.py b/gcloud/dns/zone.py index 2b69f1759f87..8a56fb13348a 100644 --- a/gcloud/dns/zone.py +++ b/gcloud/dns/zone.py @@ -20,7 +20,7 @@ class ManagedZone(object): - """ManagedZone are containers for DNS resource records. + """ManagedZones are containers for DNS resource records. See: https://cloud.google.com/dns/api/v1/managedZones