From 3472e2298eb7c36a198a75544d490bdfcb1d121f Mon Sep 17 00:00:00 2001 From: Kevin Leyow Date: Wed, 2 Apr 2014 20:56:26 -0500 Subject: [PATCH 01/15] Fix #43 - Swapped project_name for project. --- gcloud/storage/__init__.py | 20 ++++++++++---------- gcloud/storage/acl.py | 2 +- gcloud/storage/bucket.py | 12 ++++++------ gcloud/storage/connection.py | 22 +++++++++++----------- gcloud/storage/demo/__init__.py | 6 +++--- gcloud/storage/key.py | 2 +- gcloud/storage/test_connection.py | 2 +- 7 files changed, 33 insertions(+), 33 deletions(-) diff --git a/gcloud/storage/__init__.py b/gcloud/storage/__init__.py index e53c3df71981..f2e0b51f252b 100644 --- a/gcloud/storage/__init__.py +++ b/gcloud/storage/__init__.py @@ -37,19 +37,19 @@ 'https://www.googleapis.com/auth/devstorage.read_write') -def get_connection(project_name, client_email, private_key_path): +def get_connection(project, client_email, private_key_path): """Shortcut method to establish a connection to Cloud Storage. Use this if you are going to access several buckets with the same set of credentials: >>> from gcloud import storage - >>> connection = storage.get_connection(project_name, email, key_path) + >>> connection = storage.get_connection(project, email, key_path) >>> bucket1 = connection.get_bucket('bucket1') >>> bucket2 = connection.get_bucket('bucket2') - :type project_name: string - :param project_name: The name of the project to connect to. + :type project: string + :param project: The name of the project to connect to. :type client_email: string :param client_email: The e-mail attached to the service account. @@ -68,15 +68,15 @@ def get_connection(project_name, client_email, private_key_path): credentials = Credentials.get_for_service_account( client_email, private_key_path, scope=SCOPE) - return Connection(project_name=project_name, credentials=credentials) + return Connection(project=project, credentials=credentials) -def get_bucket(bucket_name, project_name, client_email, private_key_path): +def get_bucket(bucket_name, project, client_email, private_key_path): """Shortcut method to establish a connection to a particular bucket. You'll generally use this as the first call to working with the API: >>> from gcloud import storage - >>> bucket = storage.get_bucket(project_name, bucket_name, email, key_path) + >>> bucket = storage.get_bucket(project, bucket_name, email, key_path) >>> # Now you can do things with the bucket. >>> bucket.exists('/path/to/file.txt') False @@ -85,8 +85,8 @@ def get_bucket(bucket_name, project_name, client_email, private_key_path): :param bucket_name: The id of the bucket you want to use. This is akin to a disk name on a file system. - :type project_name: string - :param project_name: The name of the project to connect to. + :type project: string + :param project: The name of the project to connect to. :type client_email: string :param client_email: The e-mail attached to the service account. @@ -100,5 +100,5 @@ def get_bucket(bucket_name, project_name, client_email, private_key_path): :returns: A bucket with a connection using the provided credentials. """ - connection = get_connection(project_name, client_email, private_key_path) + connection = get_connection(project, client_email, private_key_path) return connection.get_bucket(bucket_name) diff --git a/gcloud/storage/acl.py b/gcloud/storage/acl.py index 714beffc4a06..9ce9612f661d 100644 --- a/gcloud/storage/acl.py +++ b/gcloud/storage/acl.py @@ -8,7 +8,7 @@ :func:`gcloud.storage.bucket.Bucket.get_acl`:: >>> from gcloud import storage - >>> connection = storage.get_connection(project_name, email, key_path) + >>> connection = storage.get_connection(project, email, key_path) >>> bucket = connection.get_bucket(bucket_name) >>> acl = bucket.get_acl() diff --git a/gcloud/storage/bucket.py b/gcloud/storage/bucket.py index 6c3cfe07ff00..5c7ae9e9e684 100644 --- a/gcloud/storage/bucket.py +++ b/gcloud/storage/bucket.py @@ -62,7 +62,7 @@ def get_key(self, key): This will return None if the key doesn't exist:: >>> from gcloud import storage - >>> connection = storage.get_connection(project_name, email, key_path) + >>> connection = storage.get_connection(project, email, key_path) >>> bucket = connection.get_bucket('my-bucket') >>> print bucket.get_key('/path/to/key.txt') @@ -157,7 +157,7 @@ def delete_key(self, key): >>> from gcloud import storage >>> from gcloud.storage import exceptions - >>> connection = storage.get_connection(project_name, email, key_path) + >>> connection = storage.get_connection(project, email, key_path) >>> bucket = connection.get_bucket('my-bucket') >>> print bucket.get_all_keys() [] @@ -198,7 +198,7 @@ def upload_file(self, filename, key=None): For example:: >>> from gcloud import storage - >>> connection = storage.get_connection(project_name, email, key_path) + >>> connection = storage.get_connection(project, email, key_path) >>> bucket = connection.get_bucket('my-bucket') >>> bucket.upload_file('~/my-file.txt', 'remote-text-file.txt') >>> print bucket.get_all_keys() @@ -210,7 +210,7 @@ def upload_file(self, filename, key=None): (**not** the complete path):: >>> from gcloud import storage - >>> connection = storage.get_connection(project_name, email, key_path) + >>> connection = storage.get_connection(project, email, key_path) >>> bucket = connection.get_bucket('my-bucket') >>> bucket.upload_file('~/my-file.txt') >>> print bucket.get_all_keys() @@ -329,7 +329,7 @@ def configure_website(self, main_page_suffix=None, not_found_page=None): and a page to use when a key isn't found:: >>> from gcloud import storage - >>> connection = storage.get_connection(project_name, email, private_key_path) + >>> connection = storage.get_connection(project, email, private_key_path) >>> bucket = connection.get_bucket(bucket_name) >>> bucket.configure_website('index.html', '404.html') @@ -460,7 +460,7 @@ def clear_acl(self): to a bunch of coworkers:: >>> from gcloud import storage - >>> connection = storage.get_connection(project_name, email, private_key_path) + >>> connection = storage.get_connection(project, email, private_key_path) >>> bucket = connection.get_bucket(bucket_name) >>> acl = bucket.get_acl() >>> acl.user('coworker1@example.org').grant_read() diff --git a/gcloud/storage/connection.py b/gcloud/storage/connection.py index ec25a843f181..b626b3d76612 100644 --- a/gcloud/storage/connection.py +++ b/gcloud/storage/connection.py @@ -31,7 +31,7 @@ class Connection(connection.Connection): :class:`gcloud.storage.bucket.Bucket` objects:: >>> from gcloud import storage - >>> connection = storage.get_connection(project_name, email, key_path) + >>> connection = storage.get_connection(project, email, key_path) >>> bucket = connection.create_bucket('my-bucket-name') You can then delete this bucket:: @@ -67,15 +67,15 @@ class Connection(connection.Connection): API_ACCESS_ENDPOINT = 'https://storage.googleapis.com' - def __init__(self, project_name, *args, **kwargs): + def __init__(self, project, *args, **kwargs): """ - :type project_name: string - :param project_name: The project name to connect to. + :type project: string + :param project: The project name to connect to. """ super(Connection, self).__init__(*args, **kwargs) - self.project_name = project_name + self.project = project def __iter__(self): return iter(BucketIterator(connection=self)) @@ -115,7 +115,7 @@ def build_api_url(self, path, query_params=None, api_base_url=None, path=path) query_params = query_params or {} - query_params.update({'project': self.project_name}) + query_params.update({'project': self.project}) url += '?' + urllib.urlencode(query_params) return url @@ -242,7 +242,7 @@ def get_all_buckets(self, *args, **kwargs): so these two operations are identical:: >>> from gcloud import storage - >>> connection = storage.get_connection(project_name, email, key_path) + >>> connection = storage.get_connection(project, email, key_path) >>> for bucket in connection.get_all_buckets(): >>> print bucket >>> # ... is the same as ... @@ -269,7 +269,7 @@ def get_bucket(self, bucket_name, *args, **kwargs): >>> from gcloud import storage >>> from gcloud.storage import exceptions - >>> connection = storage.get_connection(project_name, email, key_path) + >>> connection = storage.get_connection(project, email, key_path) >>> try: >>> bucket = connection.get_bucket('my-bucket') >>> except exceptions.NotFoundError: @@ -296,7 +296,7 @@ def lookup(self, bucket_name): than catching an exception:: >>> from gcloud import storage - >>> connection = storage.get_connection(project_name, email, key_path) + >>> connection = storage.get_connection(project, email, key_path) >>> bucket = connection.get_bucket('doesnt-exist') >>> print bucket None @@ -323,7 +323,7 @@ def create_bucket(self, bucket, *args, **kwargs): For example:: >>> from gcloud import storage - >>> connection = storage.get_connection(project_name, client, key_path) + >>> connection = storage.get_connection(project, client, key_path) >>> bucket = connection.create_bucket('my-bucket') >>> print bucket @@ -347,7 +347,7 @@ def delete_bucket(self, bucket, *args, **kwargs): or to delete a bucket object:: >>> from gcloud import storage - >>> connection = storage.get_connection(project_name, email, key_path) + >>> connection = storage.get_connection(project, email, key_path) >>> connection.delete_bucket('my-bucket') True diff --git a/gcloud/storage/demo/__init__.py b/gcloud/storage/demo/__init__.py index 13d862564fc9..7aaa2dca6697 100644 --- a/gcloud/storage/demo/__init__.py +++ b/gcloud/storage/demo/__init__.py @@ -2,13 +2,13 @@ from gcloud import storage -__all__ = ['get_connection', 'CLIENT_EMAIL', 'PRIVATE_KEY_PATH', 'PROJECT_NAME'] +__all__ = ['get_connection', 'CLIENT_EMAIL', 'PRIVATE_KEY_PATH', 'PROJECT'] CLIENT_EMAIL = '606734090113-6ink7iugcv89da9sru7lii8bs3i0obqg@developer.gserviceaccount.com' PRIVATE_KEY_PATH = os.path.join(os.path.dirname(__file__), 'demo.key') -PROJECT_NAME = 'gcloud-storage-demo' +PROJECT = 'gcloud-storage-demo' def get_connection(): - return storage.get_connection(PROJECT_NAME, CLIENT_EMAIL, PRIVATE_KEY_PATH) + return storage.get_connection(PROJECT, CLIENT_EMAIL, PRIVATE_KEY_PATH) diff --git a/gcloud/storage/key.py b/gcloud/storage/key.py index 87348c261666..9b970bd12f1d 100644 --- a/gcloud/storage/key.py +++ b/gcloud/storage/key.py @@ -264,7 +264,7 @@ def set_contents_from_string(self, data, content_type='text/plain'): You can use this method to quickly set the value of a key:: >>> from gcloud import storage - >>> connection = storage.get_connection(project_name, email, key_path) + >>> connection = storage.get_connection(project, email, key_path) >>> bucket = connection.get_bucket(bucket_name) >>> key = bucket.new_key('my_text_file.txt') >>> key.set_contents_from_string('This is the contents of my file!') diff --git a/gcloud/storage/test_connection.py b/gcloud/storage/test_connection.py index d15c0e11484e..0a6923d9b27e 100644 --- a/gcloud/storage/test_connection.py +++ b/gcloud/storage/test_connection.py @@ -7,4 +7,4 @@ class TestConnection(unittest2.TestCase): def test_init(self): connection = Connection('project-name') - self.assertEqual('project-name', connection.project_name) + self.assertEqual('project-name', connection.project) From 86b886e169831e245406aa35596a2b547ab76c9d Mon Sep 17 00:00:00 2001 From: JJ Geewax Date: Tue, 22 Apr 2014 12:52:13 -0400 Subject: [PATCH 02/15] Fix #86 - Entity objects from a fetched query have a proper Dataset reference. --- gcloud/datastore/query.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gcloud/datastore/query.py b/gcloud/datastore/query.py index 46c34c1e0933..9b89c55ca4bc 100644 --- a/gcloud/datastore/query.py +++ b/gcloud/datastore/query.py @@ -244,4 +244,5 @@ def fetch(self, limit=None): entity_pbs = self.dataset().connection().run_query( query_pb=clone.to_protobuf(), dataset_id=self.dataset().id()) - return [Entity.from_protobuf(entity) for entity in entity_pbs] + return [Entity.from_protobuf(entity, dataset=self.dataset()) + for entity in entity_pbs] From 2cf60d4566c8486b7111c3e27f4d6407e191d833 Mon Sep 17 00:00:00 2001 From: JJ Geewax Date: Mon, 21 Apr 2014 14:07:47 -0400 Subject: [PATCH 03/15] Fix #80 - Added support for ancestor queries. --- gcloud/datastore/query.py | 67 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/gcloud/datastore/query.py b/gcloud/datastore/query.py index 46c34c1e0933..7b5b13e6a1ba 100644 --- a/gcloud/datastore/query.py +++ b/gcloud/datastore/query.py @@ -3,6 +3,7 @@ from gcloud.datastore import datastore_v1_pb2 as datastore_pb from gcloud.datastore import helpers from gcloud.datastore.entity import Entity +from gcloud.datastore.key import Key # TODO: Figure out how to properly handle namespaces. @@ -132,6 +133,72 @@ def filter(self, expression, value): setattr(property_filter.value, attr_name, pb_value) return clone + def ancestor(self, ancestor): + """Filter the query based on an ancestor. + + This will return a clone of the current :class:`Query` + filtered by the ancestor provided. + + For example:: + + >>> parent_key = Key.from_path('Person', '1') + >>> query = dataset.query('Person') + >>> filtered_query = query.ancestor(parent_key) + + If you don't have a :class:`gcloud.datastore.key.Key` but just + know the path, you can provide that as well:: + + >>> query = dataset.query('Person') + >>> filtered_query = query.ancestor(['Person', '1']) + + Each call to ``.ancestor()`` returns a cloned :class:`Query:, + however a query may only have one ancestor at a time. + + :type ancestor: :class:`gcloud.datastore.key.Key` or list + :param ancestor: Either a Key or a path of the form + ``['Kind', 'id or name', 'Kind', 'id or name', ...]``. + + :rtype: :class:`Query` + :returns: A Query filtered by the ancestor provided. + """ + + clone = self._clone() + + # If an ancestor filter already exists, remove it. + for i, filter in enumerate(clone._pb.filter.composite_filter.filter): + property_filter = filter.property_filter + if property_filter.operator == datastore_pb.PropertyFilter.HAS_ANCESTOR: + del clone._pb.filter.composite_filter.filter[i] + + # If we just deleted the last item, make sure to clear out the filter + # property all together. + if not clone._pb.filter.composite_filter.filter: + clone._pb.ClearField('filter') + + # If the ancestor is None, just return (we already removed the filter). + if not ancestor: + return clone + + # If a list was provided, turn it into a Key. + if isinstance(ancestor, list): + ancestor = Key.from_path(*ancestor) + + # If we don't have a Key value by now, something is wrong. + if not isinstance(ancestor, Key): + raise TypeError('Expected list or Key, got %s.' % type(ancestor)) + + # Get the composite filter and add a new property filter. + composite_filter = clone._pb.filter.composite_filter + composite_filter.operator = datastore_pb.CompositeFilter.AND + + # Filter on __key__ HAS_ANCESTOR == ancestor. + ancestor_filter = composite_filter.filter.add().property_filter + ancestor_filter.property.name = '__key__' + ancestor_filter.operator = datastore_pb.PropertyFilter.HAS_ANCESTOR + ancestor_filter.value.key_value.CopyFrom(ancestor.to_protobuf()) + + return clone + def kind(self, *kinds): """Get or set the Kind of the Query. From 6c999c020dca61b76452355c6775778113570ca8 Mon Sep 17 00:00:00 2001 From: Kevin Leyow Date: Fri, 25 Apr 2014 18:20:51 -0500 Subject: [PATCH 04/15] Fix #65 - Added force parameter to delete non empty buckets. Updated gcloud python docs to reflect forced bucket deletes. --- docs/storage-getting-started.rst | 5 +---- gcloud/storage/bucket.py | 12 ++++++++++-- gcloud/storage/connection.py | 11 ++++++++++- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/docs/storage-getting-started.rst b/docs/storage-getting-started.rst index 63e1266b5a2f..3cca86d0d5cd 100644 --- a/docs/storage-getting-started.rst +++ b/docs/storage-getting-started.rst @@ -217,10 +217,7 @@ otherwise you'll get an error. If you have a full bucket, you can delete it this way:: - >>> bucket = connection.get_bucket('my-bucket') - >>> for key in bucket: - ... key.delete() - >>> bucket.delete() + >>> bucket = connection.get_bucket('my-bucket', force=True) Listing available buckets ------------------------- diff --git a/gcloud/storage/bucket.py b/gcloud/storage/bucket.py index 6c3cfe07ff00..cc0d89bf76d3 100644 --- a/gcloud/storage/bucket.py +++ b/gcloud/storage/bucket.py @@ -130,7 +130,7 @@ def new_key(self, key): raise TypeError('Invalid key: %s' % key) - def delete(self): + def delete(self, force=False): """Delete this bucket. The bucket **must** be empty in order to delete it. @@ -139,12 +139,20 @@ def delete(self): If the bucket is not empty, this will raise an Exception. + If you want to delete a non-empty bucket you can pass + in a force parameter set to true. + This will iterate through the bucket's keys and delete the related objects, + before deleting the bucket. + + :type force: bool + :param full: If True, empties the bucket's objects then deletes it. + :raises: :class:`gcloud.storage.exceptions.NotFoundError` """ # TODO: Make sure the proper exceptions are raised. - return self.connection.delete_bucket(self.name) + return self.connection.delete_bucket(self.name, force=force) def delete_key(self, key): # TODO: Should we accept a 'silent' param here to not raise an exception? diff --git a/gcloud/storage/connection.py b/gcloud/storage/connection.py index ec25a843f181..9901fac34f04 100644 --- a/gcloud/storage/connection.py +++ b/gcloud/storage/connection.py @@ -340,7 +340,7 @@ def create_bucket(self, bucket, *args, **kwargs): data={'name': bucket.name}) return Bucket.from_dict(response, connection=self) - def delete_bucket(self, bucket, *args, **kwargs): + def delete_bucket(self, bucket, force=False, *args, **kwargs): """Delete a bucket. You can use this method to delete a bucket by name, @@ -369,12 +369,21 @@ def delete_bucket(self, bucket, *args, **kwargs): :type bucket: string or :class:`gcloud.storage.bucket.Bucket` :param bucket: The bucket name (or bucket object) to create. + :type force: bool + :param full: If True, empties the bucket's objects then deletes it. + :rtype: bool :returns: True if the bucket was deleted. :raises: :class:`gcloud.storage.exceptions.NotFoundError` """ bucket = self.new_bucket(bucket) + + # This force delete operation is slow. + if force: + for key in bucket: + key.delete() + response = self.api_request(method='DELETE', path=bucket.path) return True From 70069c5b80cb957611b7d210d85f094b0cf9da43 Mon Sep 17 00:00:00 2001 From: Burcu Dogan Date: Mon, 28 Apr 2014 12:28:21 -0600 Subject: [PATCH 05/15] gcloud-python is not a generic Google APIs client, fixing wording. --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 1cb740ee1122..aa382ee8abab 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -12,7 +12,7 @@ Google Cloud Python API .. warning:: This library is **still under construction** - and is **not** the official Google Python API client library. + and is **not** the official Google Cloud Python API client library. Getting started --------------- From f32f10e3d05be0bd693816ce9e5f9805d0150169 Mon Sep 17 00:00:00 2001 From: JJ Geewax Date: Fri, 2 May 2014 13:44:39 -0400 Subject: [PATCH 06/15] Fix #89 - Made README more user friendly. Focusing more on *users* of the library than *developers* of the library. --- README.rst | 57 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 16 deletions(-) diff --git a/README.rst b/README.rst index 1961c399df94..75649d93fe93 100644 --- a/README.rst +++ b/README.rst @@ -1,28 +1,33 @@ -Google Cloud -============ +Google Cloud Python Client +========================== -Official documentation ----------------------- +The goal of this project is to make it really simple and Pythonic +to use Google Cloud Platform services. -If you just want to **use** the library -(not contribute to it), -check out the official documentation: -http://GoogleCloudPlatform.github.io/gcloud-python/ +Quickstart +---------- -Incredibly quick demo ---------------------- +The library is ``pip``-installable:: -Start by cloning the repository:: + $ pip install gcloud + $ python -m gcloud.storage.demo # Runs the storage demo! - $ git clone git://github.com/GoogleCloudPlatform/gcloud-python.git - $ cd gcloud - $ python setup.py develop +Documentation +------------- + +- `gcloud docs (browse all services, quick-starts, walk-throughs) `_ +- `gcloud.datastore API docs `_ +- `gcloud.storage API docs `_ +- gcloud.bigquery API docs (*coming soon)* +- gcloud.compute API docs *(coming soon)* +- gcloud.dns API docs *(coming soon)* +- gcloud.sql API docs *(coming soon)* I'm getting weird errors... Can you help? ----------------------------------------- -Chances are you have some dependency problems, -if you're on Ubuntu, +Chances are you have some dependency problems... +If you're on Ubuntu, try installing the pre-compiled packages:: $ sudo apt-get install python-crypto python-openssl libffi-dev @@ -32,6 +37,7 @@ or try installing the development packages and then ``pip install`` the dependencies again:: $ sudo apt-get install python-dev libssl-dev libffi-dev + $ pip install gcloud How do I build the docs? ------------------------ @@ -50,4 +56,23 @@ Make sure you have ``nose`` installed and:: $ git clone git://github.com/GoogleCloudPlatform/gcloud-python.git $ pip install unittest2 nose + $ cd gcloud-python $ nosetests + +How can I contribute? +--------------------- + +Before we can accept any pull requests +we have to jump through a couple of legal hurdles, +primarily a Contributor License Agreement (CLA): + +- **If you are an individual writing original source code** + and you're sure you own the intellectual property, + then you'll need to sign an `individual CLA + `_. +- **If you work for a company that wants to allow you to contribute your work**, + then you'll need to sign a `corporate CLA + `_. + +You can sign these electronically (just scroll to the bottom). +After that, we'll be able to accept your pull requests. From f1c54916b22623d9c31abab75657ec593bb59b7c Mon Sep 17 00:00:00 2001 From: Ali Afshar Date: Thu, 8 May 2014 11:50:24 -0700 Subject: [PATCH 07/15] Added simplest CI build file. --- .travis.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000000..e69de29bb2d1 From f7150703727c0e4558efae29b66322722bf4c9db Mon Sep 17 00:00:00 2001 From: Ali Afshar Date: Thu, 8 May 2014 11:54:14 -0700 Subject: [PATCH 08/15] Added simplest CI build file. --- .travis.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.travis.yml b/.travis.yml index e69de29bb2d1..84dd8e950274 100644 --- a/.travis.yml +++ b/.travis.yml @@ -0,0 +1,8 @@ +language: python +python: + - "2.6" + - "2.7" +# command to install dependencies +install: "pip install ." +# command to run tests +script: nosetests From 6e196cff085732dd8540e92def72af8a7fe3faa3 Mon Sep 17 00:00:00 2001 From: Ali Afshar Date: Thu, 8 May 2014 12:52:59 -0700 Subject: [PATCH 09/15] Added simplest CI build file. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 84dd8e950274..f6da14e67f15 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,6 @@ python: - "2.6" - "2.7" # command to install dependencies -install: "pip install ." +install: "pip install . unittest2" # command to run tests script: nosetests From 21ec3c9e32186052fff88006de3a8c49a88b16cb Mon Sep 17 00:00:00 2001 From: Ali Afshar Date: Thu, 8 May 2014 13:00:40 -0700 Subject: [PATCH 10/15] Add build badge --- README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.rst b/README.rst index 75649d93fe93..80ec66e4585c 100644 --- a/README.rst +++ b/README.rst @@ -4,6 +4,8 @@ Google Cloud Python Client The goal of this project is to make it really simple and Pythonic to use Google Cloud Platform services. +[![Build Status](https://travis-ci.org/GoogleCloudPlatform/gcloud-python.svg?branch=master)](https://travis-ci.org/GoogleCloudPlatform/gcloud-python) + Quickstart ---------- From b22da8069f24e514f30acc990c1e4cd6a69774ad Mon Sep 17 00:00:00 2001 From: Ali Afshar Date: Thu, 8 May 2014 13:02:28 -0700 Subject: [PATCH 11/15] Update README.rst --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 80ec66e4585c..60f1fcf3121e 100644 --- a/README.rst +++ b/README.rst @@ -4,7 +4,7 @@ Google Cloud Python Client The goal of this project is to make it really simple and Pythonic to use Google Cloud Platform services. -[![Build Status](https://travis-ci.org/GoogleCloudPlatform/gcloud-python.svg?branch=master)](https://travis-ci.org/GoogleCloudPlatform/gcloud-python) +.. image:: https://travis-ci.org/GoogleCloudPlatform/gcloud-python.svg?branch=master :target: https://travis-ci.org/GoogleCloudPlatform/gcloud-python Quickstart ---------- From be94dffa9b873d6342c47b500404ded6fbdfbb8b Mon Sep 17 00:00:00 2001 From: Ali Afshar Date: Thu, 8 May 2014 13:03:40 -0700 Subject: [PATCH 12/15] Update README.rst --- README.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 60f1fcf3121e..760d2f08c6cd 100644 --- a/README.rst +++ b/README.rst @@ -4,7 +4,8 @@ Google Cloud Python Client The goal of this project is to make it really simple and Pythonic to use Google Cloud Platform services. -.. image:: https://travis-ci.org/GoogleCloudPlatform/gcloud-python.svg?branch=master :target: https://travis-ci.org/GoogleCloudPlatform/gcloud-python +.. image:: https://travis-ci.org/GoogleCloudPlatform/gcloud-python.svg?branch=master +:target: https://travis-ci.org/GoogleCloudPlatform/gcloud-python Quickstart ---------- From 87c3e73d7ae26ea0d3ae3a2d30b20ab7f343644f Mon Sep 17 00:00:00 2001 From: Ali Afshar Date: Thu, 8 May 2014 13:06:12 -0700 Subject: [PATCH 13/15] Update README.rst --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 760d2f08c6cd..85ef1dcd4d97 100644 --- a/README.rst +++ b/README.rst @@ -5,7 +5,7 @@ The goal of this project is to make it really simple and Pythonic to use Google Cloud Platform services. .. image:: https://travis-ci.org/GoogleCloudPlatform/gcloud-python.svg?branch=master -:target: https://travis-ci.org/GoogleCloudPlatform/gcloud-python + :target: https://travis-ci.org/GoogleCloudPlatform/gcloud-python Quickstart ---------- From 4dd8f6785d74abc8e5760033d35cfb7550e7f6b5 Mon Sep 17 00:00:00 2001 From: Ali Afshar Date: Mon, 19 May 2014 14:12:32 -0700 Subject: [PATCH 14/15] Don't assume that httplib2 responses contain any preset attributes, since they are looked up dynamically from a response headers dict. Fixes #93 --- gcloud/storage/exceptions.py | 2 +- gcloud/storage/test_connection.py | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/gcloud/storage/exceptions.py b/gcloud/storage/exceptions.py index 1d23a96cfdb8..dff01204bc95 100644 --- a/gcloud/storage/exceptions.py +++ b/gcloud/storage/exceptions.py @@ -14,7 +14,7 @@ def __init__(self, response, content): class NotFoundError(ConnectionError): def __init__(self, response, content): - self.message = 'GET %s returned a 404.' % (response.url) + self.message = 'Request returned a 404. Headers: %s' % (response) class StorageDataError(StorageError): diff --git a/gcloud/storage/test_connection.py b/gcloud/storage/test_connection.py index 0a6923d9b27e..cfb4ff60a6a9 100644 --- a/gcloud/storage/test_connection.py +++ b/gcloud/storage/test_connection.py @@ -1,10 +1,19 @@ import unittest2 from gcloud.storage.connection import Connection - +from gcloud.storage.exceptions import NotFoundError class TestConnection(unittest2.TestCase): def test_init(self): connection = Connection('project-name') self.assertEqual('project-name', connection.project) + + +class TestExceptions(unittest2.TestCase): + + def test_not_found_always_prints(self): + e = NotFoundError({}, None) + self.assertEqual('', str(e)) + + From c021a0d1a9bb57a5c8a5a56427ed6ed29aaea325 Mon Sep 17 00:00:00 2001 From: Kevin Leyow Date: Mon, 31 Mar 2014 21:31:27 -0500 Subject: [PATCH 15/15] Fix #77 - Initialized gcloud dns. Cleaned up dns. Addressed comments on dns. Added function to create changes. Changed project_id to project and addressed comments. Made some changes. Streamlined dns to reflect first draft of design and updated doc strings. Initialized more doc strings. Edited some doc strings. Changed project_name to project. Updated docstrings and fixed some errors. Fixed a bug involving changes. --- gcloud/connection.py | 84 ++++++++++++ gcloud/dns/__init__.py | 92 +++++++++++++ gcloud/dns/change.py | 21 +++ gcloud/dns/connection.py | 145 ++++++++++++++++++++ gcloud/dns/demo/__init__.py | 19 +++ gcloud/dns/demo/__main__.py | 5 + gcloud/dns/demo/demo.p12 | Bin 0 -> 1732 bytes gcloud/dns/demo/demo.py | 26 ++++ gcloud/dns/exceptions.py | 6 + gcloud/dns/record.py | 65 +++++++++ gcloud/dns/zone.py | 260 ++++++++++++++++++++++++++++++++++++ gcloud/exceptions.py | 18 +++ 12 files changed, 741 insertions(+) create mode 100644 gcloud/dns/__init__.py create mode 100644 gcloud/dns/change.py create mode 100644 gcloud/dns/connection.py create mode 100644 gcloud/dns/demo/__init__.py create mode 100644 gcloud/dns/demo/__main__.py create mode 100644 gcloud/dns/demo/demo.p12 create mode 100644 gcloud/dns/demo/demo.py create mode 100644 gcloud/dns/exceptions.py create mode 100644 gcloud/dns/record.py create mode 100644 gcloud/dns/zone.py create mode 100644 gcloud/exceptions.py diff --git a/gcloud/connection.py b/gcloud/connection.py index 35855e89b445..fb9773441f0f 100644 --- a/gcloud/connection.py +++ b/gcloud/connection.py @@ -1,4 +1,8 @@ import httplib2 +import json +import urllib + +from gcloud import exceptions class Connection(object): @@ -42,3 +46,83 @@ def http(self): self._http = self._credentials.authorize(self._http) return self._http + +class JsonConnection(Connection): + + API_BASE_URL = 'https://www.googleapis.com' + """The base of the API call URL.""" + + _EMPTY = object() + """A pointer to represent an empty value for default arguments.""" + + def __init__(self, project=None, *args, **kwargs): + + super(JsonConnection, self).__init__(*args, **kwargs) + + self.project = project + + def build_api_url(self, path, query_params=None, api_base_url=None, + api_version=None): + + url = self.API_URL_TEMPLATE.format( + api_base_url=(api_base_url or self.API_BASE_URL), + api_version=(api_version or self.API_VERSION), + path=path) + + query_params = query_params or {} + query_params.update({'project': self.project}) + url += '?' + urllib.urlencode(query_params) + + return url + + def make_request(self, method, url, data=None, content_type=None, + headers=None): + + headers = headers or {} + headers['Accept-Encoding'] = 'gzip' + + if data: + content_length = len(str(data)) + else: + content_length = 0 + + headers['Content-Length'] = content_length + + if content_type: + headers['Content-Type'] = content_type + + return self.http.request(uri=url, method=method, headers=headers, + body=data) + + def api_request(self, method, path=None, query_params=None, + data=None, content_type=None, + api_base_url=None, api_version=None, + expect_json=True): + + url = self.build_api_url(path=path, query_params=query_params, + api_base_url=api_base_url, + api_version=api_version) + + # Making the executive decision that any dictionary + # data will be sent properly as JSON. + if data and isinstance(data, dict): + data = json.dumps(data) + content_type = 'application/json' + + response, content = self.make_request( + method=method, url=url, data=data, content_type=content_type) + + # TODO: Add better error handling. + if response.status == 404: + raise exceptions.NotFoundError(response, content) + elif not 200 <= response.status < 300: + raise exceptions.ConnectionError(response, content) + + if content and expect_json: + # TODO: Better checking on this header for JSON. + content_type = response.get('content-type', '') + if not content_type.startswith('application/json'): + raise TypeError('Expected JSON, got %s' % content_type) + return json.loads(content) + + return content diff --git a/gcloud/dns/__init__.py b/gcloud/dns/__init__.py new file mode 100644 index 000000000000..83b11cfd4022 --- /dev/null +++ b/gcloud/dns/__init__.py @@ -0,0 +1,92 @@ +"""Shortcut methods for getting set up with Google Cloud DNS. + +You'll typically use these to get started with the API: + +>>> import gcloud.dns +>>> zone = gcloud.dns.get_zone('zone-name-here', + 'long-email@googleapis.com', + '/path/to/private.key') + +The main concepts with this API are: + +- :class:`gcloud.dns.connection.Connection` + which represents a connection between your machine + and the Cloud DNS API. + +- :class:`gcloud.dns.zone.Zone` + which represents a particular zone. +""" + + +__version__ = '0.1' + +# TODO: Allow specific scopes and authorization levels. +SCOPE = ('https://www.googleapis.com/auth/cloud-platform', + 'https://www.googleapis.com/auth/ndev.clouddns.readonly', + 'https://www.googleapis.com/auth/ndev.clouddns.readwrite') +"""The scope required for authenticating as a Cloud DNS consumer.""" + + +def get_connection(project, client_email, private_key_path): + """Shortcut method to establish a connection to Cloud DNS. + + Use this if you are going to access several zones + with the same set of credentials: + + >>> from gcloud import dns + >>> connection = dns.get_connection(project, email, key_path) + >>> zone1 = connection.get_zone('zone1') + >>> zone2 = connection.get_zone('zone2') + + :type project: string + :param project: The name of the project to connect to. + + :type client_email: string + :param client_email: The e-mail attached to the service account. + + :type private_key_path: string + :param private_key_path: The path to a private key file (this file was + given to you when you created the service + account). + + :rtype: :class:`gcloud.dns.connection.Connection` + :returns: A connection defined with the proper credentials. + """ + + from gcloud.credentials import Credentials + from gcloud.dns.connection import Connection + + credentials = Credentials.get_for_service_account( + client_email, private_key_path, scope=SCOPE) + return Connection(project=project, credentials=credentials) + + +def get_zone(zone, project, client_email, private_key_path): + """Shortcut method to establish a connection to a particular zone. + + You'll generally use this as the first call to working with the API: + + >>> from gcloud import dns + >>> zone = dns.get_zone(zone, project, email, key_path) + + :type zone: string + :param zone: The id of the zone you want to use. + This is akin to a disk name on a file system. + + :type project: string + :param project: The name of the project to connect to. + + :type client_email: string + :param client_email: The e-mail attached to the service account. + + :type private_key_path: string + :param private_key_path: The path to a private key file (this file was + given to you when you created the service + account). + + :rtype: :class:`gcloud.dns.zone.Zone` + :returns: A zone with a connection using the provided credentials. + """ + + connection = get_connection(project, client_email, private_key_path) + return connection.get_zone(zone) diff --git a/gcloud/dns/change.py b/gcloud/dns/change.py new file mode 100644 index 000000000000..243ce3b965a1 --- /dev/null +++ b/gcloud/dns/change.py @@ -0,0 +1,21 @@ +class Change(object): + """A class representing a Change on Cloud DNS. + + :type additions: list + :param name: A list of records slated to be added to a zone. + + :type deletions: list + :param data: A list of records slated to be deleted to a zone. + """ + + def __init__(self, additions=None, deletions=None): + self.additions = additions + self.deletions = deletions + + def to_dict(self): + """Format the change into a dict compatible with Cloud DNS. + + :rtype: dict + :returns: A Cloud DNS dict representation of a change. + """ + return {'additions': self.additions, 'deletions': self.deletions} diff --git a/gcloud/dns/connection.py b/gcloud/dns/connection.py new file mode 100644 index 000000000000..11bc608262f5 --- /dev/null +++ b/gcloud/dns/connection.py @@ -0,0 +1,145 @@ +from gcloud import connection +from gcloud.dns.record import Record +from gcloud.dns.zone import Zone + + +class Connection(connection.JsonConnection): + """A connection to Google Cloud DNS via the JSON REST API. + + See :class:`gcloud.connection.JsonConnection` for a full list of parameters. + :class:`Connection` differs only in needing a project name + (which you specify when creating a project in the Cloud Console). + """ + + API_VERSION = 'v1beta1' + """The version of the API, used in building the API call's URL.""" + + API_URL_TEMPLATE = ('{api_base_url}/dns/{api_version}/projects/{path}') + """A template used to craft the URL pointing toward a particular API call.""" + + _EMPTY = object() + """A pointer to represent an empty value for default arguments.""" + + def __init__(self, project=None, *args, **kwargs): + """ + :type project: string + :param project: The project name to connect to. + """ + + super(Connection, self).__init__(*args, **kwargs) + + self.project = project + + def new_zone(self, zone): + """Factory method for creating a new (unsaved) zone object. + + :type zone: string or :class:`gcloud.dns.zone.Zone` + :param zone: A name of a zone or an existing Zone object. + """ + + if isinstance(zone, Zone): + return zone + + # Support Python 2 and 3. + try: + string_type = basestring + except NameError: + string_type = str + + if isinstance(zone, string_type): + return Zone(connection=self, name=zone) + + def create_zone(self, zone, dns_name, description): + """Create a new zone. + + :type zone: string or :class:`gcloud.dns.zone.Zone` + :param zone: The zone name (or zone object) to create. + + :rtype: :class:`gcloud.dns.zone.Zone` + :returns: The newly created zone. + """ + + zone = self.new_zone(zone) + response = self.api_request(method='POST', path=zone.path, + data={'name': zone.name, 'dnsName': dns_name, + 'description': description}) + return Zone.from_dict(response, connection=self) + + def delete_zone(self, zone, force=False): + """Delete a zone. + + You can use this method to delete a zone by name, + or to delete a zone object:: + + >>> from gcloud import dns + >>> connection = dns.get_connection(project, email, key_path) + >>> connection.delete_zone('my-zone') + True + + You can also delete pass in the zone object:: + + >>> zone = connection.get_zone('other-zone') + >>> connection.delete_zone(zone) + True + + :type zone: string or :class:`gcloud.dns.zone.Zone` + :param zone: The zone name (or zone object) to create. + + :type force: bool + :param full: If True, deletes the zones's recordss then deletes it. + + :rtype: bool + :returns: True if the zone was deleted. + """ + + zone = self.new_zone(zone) + + if force: + rrsets = self.get_records(zone) + for rrset in rrsets['rrsets']: + record = Record.from_dict(rrset) + if record.type != 'NS' and record.type != 'SOA': + zone.remove_record(record) + zone.save() + + self.api_request(method='DELETE', path=zone.path + zone.name) + return True + + def get_zone(self, zone): + """Get a zone by name. + + :type zone: string + :param zone: The name of the zone to get. + + :rtype: :class:`gcloud.dns.zone.Zone` + :returns: The zone matching the name provided. + """ + + zone = self.new_zone(zone) + response = self.api_request(method='GET', path=zone.path) + return Zone.from_dict(response['managedZones'][0], connection=self) + + def get_records(self, zone): + """Get a list of resource records on a zone. + + :type zone: string or :class:`gcloud.dns.zone.Zone` + :param zone: The zone name (or zone object) to get records from. + """ + + zone = self.new_zone(zone) + return self.api_request(method='GET', path=zone.path + zone.name + + '/rrsets') + + def save_change(self, zone, change): + """Save a set of changes to a zone. + + :type zone: string or :class:`gcloud.dns.zone.Zone` + :param zone: The zone name (or zone object) to save to. + + :type change: dict + :param dict: A dict with the addition and deletion lists of records. + """ + + zone = self.new_zone(zone) + return self.api_request(method='POST', path=zone.path + zone.name + + '/changes', data=change) diff --git a/gcloud/dns/demo/__init__.py b/gcloud/dns/demo/__init__.py new file mode 100644 index 000000000000..6b050674353b --- /dev/null +++ b/gcloud/dns/demo/__init__.py @@ -0,0 +1,19 @@ +import os +from gcloud import dns + + +__all__ = ['get_connection', 'get_zone' 'CLIENT_EMAIL', 'PRIVATE_KEY_PATH', + 'PROJECT'] + + +CLIENT_EMAIL = '524635209885-rda26ks46309o10e0nc8rb7d33rn0hlm@developer.gserviceaccount.com' +PRIVATE_KEY_PATH = os.path.join(os.path.dirname(__file__), 'demo.p12') +PROJECT = 'gceremote' + + +def get_connection(): + return dns.get_connection(PROJECT, CLIENT_EMAIL, PRIVATE_KEY_PATH) + + +def get_zone(zone): + return dns.get_zone(zone, PROJECT, CLIENT_EMAIL, PRIVATE_KEY_PATH) diff --git a/gcloud/dns/demo/__main__.py b/gcloud/dns/demo/__main__.py new file mode 100644 index 000000000000..8074eae216b9 --- /dev/null +++ b/gcloud/dns/demo/__main__.py @@ -0,0 +1,5 @@ +from gcloud import demo +from gcloud import dns + + +demo.DemoRunner.from_module(dns).run() diff --git a/gcloud/dns/demo/demo.p12 b/gcloud/dns/demo/demo.p12 new file mode 100644 index 0000000000000000000000000000000000000000..0f5966647b38373e55d50a4d15b8a0ae2be6e6e0 GIT binary patch literal 1732 zcmY+Dc{tRGAIHCQ$|w!Skyc}jF;k94v*Rck$2y~5p#1nN94*C zn@Emdax2C(59x+EX&)}6y z8gr#)(HqXB$D5_C?k6;3kGtE*=m1D2{V-!=Xu+D_0;z~((egIK^#VJ3`d$2Hp@hcD zYRlB|ey!QnbOK%zn82DZR zcl~-u+gMp)I|0SpvKHfZ0zfqsqc&4QN>9fQV_)Cm5vx4h4*gqE^ww6X$;7(9s(7;# zrl4X&@0!))2mh+TY=&Yw94Wn2ykAbkDqCi-9lIh5@!G%~nn}&LyRDj>3y{g91 z_Yt0=4Jm1eA6{4x47Wueq3ND3^fJBtQJ%+e?+lJGou8k^;(~8b)Y?|w<+4~~kW^JK z`-)@^xH+(~xxFnawKvpP7Frb*U)Z{5LL(^S6TILk#E!B#5$D<~IA!joZ%f4f{o&Fm zeWgPvAP5Ks{DCmwA`k-j0`y%)5HbIh6!lS1c^7|%uev_T*oaIvHYAbBM5A3GNPm~4 z;H7Mc=B}j<1_8U{z&{D-Ux2{B0#az`@JMv?vPk`TQguahD5LT9`lYXc#InJ!`inE= z4Y%*J-bhbP9sat2wQvjfM+lB#o-0aL}ZyjcE$e#x^pL_lxb zBklDdW+4M1hqR$5<=JkR)>>+?&^TI-&gLuVOVSi%b*^Lj?i4>lC&5PRk9nzyP()<| zEFj|w&NlcB>cUTE=Z9mm`m4zCdpT2ZGgSWP`K0A%Sx+!-5A|*x-N&~|)1T>!cGB^q z&83Ye(rp&G-ziB|t&jNZgs+x2@CNzr%P^}wMFXbB*v!;Xx1xaP(Xp=d@mEnxOo#4J z_oN#}H))=q%El7eBj}n027jgK?zK72hp3~1+%ofDvc#9aOOWbWUnzR@n+6)OuzA9w z2`N$#OL!!7y2%HSv;9*F9SE9l?ECy?R`+97Ucm=Ps!rU0%$bYL0`JcfdxA%kzg*WL z#B|7W_+V7w`VR=rG676T1yAW>%SxEj)u!IxmI*mg%iYGs^vsx+tqV-y)nMhoZ2S+@*1|c-5@XWe!N2w?b;DY2Nzz{#4TQ#+%p{_o z+qfJ&uD`Z=2{!#`*wdzQVQ9K*cK8ZfHdo!p!t#uv$Iz|7NWx>=6W^4cp@^%m;gnad zzVJ{xba>9ZE-qYX9_fU}qJB18H8M?ZE$rqfAHIm+Mq|Er79wN(4PI(AKkGJ_??XHY z;w*nT=Cl+hFI{8T&9K2J$sixZC$Zl8%t_SpBE*yVOPz8t0ebDfs>pPg@AY8ftxr8b z)<5%eR`v6a${8*%3hSl~RV>VoQR(xF;})f`aHpsLy}dMEZpqrD{lwE^MqJZEQ+qTn z)kwv?PwCOU!~#4@gm(7%M}C1f@f=c=Ca0G3b+jt_T=@M$BTA1%J7mD^;fs5z*~UwV zbmB=OhA0h(Y0E;usxSbGI;i|!#mxKI&7nIYsr`}aBi|RBFrcWS`d%XEws}>|8OG?) OhMFv#RPFc`kv{<`xeb~C literal 0 HcmV?d00001 diff --git a/gcloud/dns/demo/demo.py b/gcloud/dns/demo/demo.py new file mode 100644 index 000000000000..e6eca9d89621 --- /dev/null +++ b/gcloud/dns/demo/demo.py @@ -0,0 +1,26 @@ +# Welcome to the gCloud DNS Demo! (hit enter) + +# We're going to walk through some of the basics..., +# Don't worry though. You don't need to do anything, just keep hitting enter... + +# Let's start by importing the demo module and getting a connection: +from gcloud.dns import demo +connection = demo.get_connection() + +# Lets create a zone. +zone = connection.create_zone('zone', 'zone.com.', 'My zone.') + +# Lets see what records the zone has... +print connection.get_records('zone') + +# Lets add a A record to the zone. +zone.add_a('zone.com.', ['1.1.1.1'], 9000) + +# Lets commit the changes of the zone with... +zone.save() + +# Lets see what records the zone has... +print connection.get_records('zone') + +# Finally lets clean up and delete our test zone. +zone.delete(force=True) diff --git a/gcloud/dns/exceptions.py b/gcloud/dns/exceptions.py new file mode 100644 index 000000000000..dff61f3cf740 --- /dev/null +++ b/gcloud/dns/exceptions.py @@ -0,0 +1,6 @@ +from gcloud.exceptions import Error +# TODO: Make these super useful. + + +class DNSError(Error): + pass diff --git a/gcloud/dns/record.py b/gcloud/dns/record.py new file mode 100644 index 000000000000..857bcb62f5aa --- /dev/null +++ b/gcloud/dns/record.py @@ -0,0 +1,65 @@ +class Record(object): + """A class representing a Resource Record Set on Cloud DNS. + + :type name: string + :param name: The name of the record, for example 'www.example.com.'. + + :type data: list + :param data: A list of the textual representation of Resource Records. + + :type ttl: int + :param ttl: The record's time to live. + + :type type: string + :param string: The type of DNS record. + """ + + def __init__(self, name=None, data=[], ttl=None, type=None): + self.name = name + self.data = data + self.ttl = ttl + self.type = type + + @classmethod + def from_dict(cls, record_dict): + """Construct a new record from a dictionary of data from Cloud DNS. + + :type record_dict: dict + :param record_dict: The dictionary of data to construct a record from. + + :rtype: :class:`Record` + :returns: A record constructed from the data provided. + """ + + return cls(name=record_dict['name'], data=record_dict['rrdatas'], + ttl=record_dict['ttl'], type=record_dict['type']) + + def __str__(self): + """Format the record when printed. + + :rtype: string + :returns: A formated record string. + """ + + record = ('{name} {ttl} IN {type} {data}') + return record.format(name=self.name, ttl=self.ttl, type=self.type, + data=self.data) + + def add_data(self, data): + """Add to the list of resource record data for the record. + + :type data: string + :param data: The textual representation of a resourse record. + """ + + self.data.append(data) + + def to_dict(self): + """Format the record into a dict compatible with Cloud DNS. + + :rtype: dict + :returns: A Cloud DNS dict representation of a record. + """ + + return {'name': self.name, 'rrdatas': self.data, 'ttl': self.ttl, + 'type': self.type} diff --git a/gcloud/dns/zone.py b/gcloud/dns/zone.py new file mode 100644 index 000000000000..bb9924da81a8 --- /dev/null +++ b/gcloud/dns/zone.py @@ -0,0 +1,260 @@ +from gcloud.dns.change import Change +from gcloud.dns.record import Record + + +class Zone(object): + """A class representing a Managed Zone on Cloud DNS. + + :type connection: :class:`gcloud.dns.connection.Connection` + :param connection: The connection to use when sending requests. + + :type creation_time: string + :param connection_time: Time that this zone was created on the server. + + :type description: string + :param data: A description of the zone. + + :type dns_name: string + :param data: The DNS name of the zone. + + :type id: unsigned long + :param data: Unique identifier defined by the server. + + :type kind: string + :param data: Identifies what kind of resource. + + :type name_servers: list + :param name_servers: List of virtual name servers of the zone. + """ + + def __init__(self, connection=None, creation_time=None, + description=None, dns_name=None, id=None, kind=None, name=None, + name_servers=None): + self.additions = [] + self.connection = connection + self.creation_time = creation_time + self.deletions = [] + self.description = description + self.dns_name = dns_name + self.id = id + self.kind = kind + self.name = name + self.name_servers = name_servers + + @classmethod + def from_dict(cls, zone_dict, connection=None): + """Construct a new zone from a dictionary of data from Cloud DNS. + + :type zone_dict: dict + :param zone_dict: The dictionary of data to construct a record from. + + :rtype: :class:`Zone` + :returns: A zone constructed from the data provided. + """ + + return cls(connection=connection, + creation_time=zone_dict['creationTime'], + description=zone_dict['description'], + dns_name=zone_dict['dnsName'], id=zone_dict['id'], + kind=zone_dict['kind'], name=zone_dict['name'], + name_servers=zone_dict['nameServers']) + + @property + def path(self): + """The URL path to this zone.""" + + if not self.connection.project: + raise ValueError('Cannot determine path without project name.') + + return self.connection.project + '/managedZones/' + + def delete(self, force=False): + """Delete this zone. + + The zone **must** be empty in order to delete it. + + If you want to delete a non-empty zone you can pass + in a force parameter set to true. + This will iterate through the zones's records and delete the related + records, before deleting the zone. + + :type force: bool + :param full: If True, deletes the zones's records then deletes it. + """ + + return self.connection.delete_zone(self.name, force=force) + + def save(self): + """Commit all the additions and deletions of records on this zone. + """ + + change = Change(additions=self.additions, deletions=self.deletions) + self.connection.save_change(self.name, change.to_dict()) + self.additions = [] + self.deletions = [] + return True + + def add_record(self, record): + """Add a record to the dict of records to be added to the zone. + + :type record: dict or :class:`Record` + :param record: A dict representation of a record to be added. + """ + + if isinstance(record, Record): + record = record.to_dict() + + if isinstance(record, dict): + self.additions.append(record) + + # Throw type error here. + + def remove_record(self, record): + """Add a record to the dict of records to be deleted to the zone. + + :type record: dict or :class:`Record` + :param record: A dict representation of a record to be deleted. + """ + + if isinstance(record, Record): + record = record.to_dict() + + if isinstance(record, dict): + self.deletions.append(record) + + # Throw type error here. + + def add_a(self, name, data, ttl): + """ Shortcut method to add a A record to be added to the zone. + :type name: string + :param name: The name of the record, for example 'www.example.com.'. + + :type data: list + :param data: A list of the textual representation of Resource Records. + + :type ttl: int + :param ttl: The record's time to live. + """ + + record = Record(name, data, ttl, 'A') + self.add_record(record) + + def add_aaaa(self, name, data, ttl): + """ Shortcut method to add a AAAA record to be added to the zone. + :type name: string + :param name: The name of the record, for example 'www.example.com.'. + + :type data: list + :param data: A list of the textual representation of Resource Records. + + :type ttl: int + :param ttl: The record's time to live. + """ + + record = Record(name, data, ttl, 'AAAA') + self.add_record(record) + + def add_cname(self, name, data, ttl): + """ Shortcut method to add a CNAME record to be added to the zone. + :type name: string + :param name: The name of the record, for example 'www.example.com.'. + + :type data: list + :param data: A list of the textual representation of Resource Records. + + :type ttl: int + :param ttl: The record's time to live. + """ + + record = Record(name, data, ttl, 'CNAME') + self.add_record(record) + + def add_mx(self, name, data, ttl): + """ Shortcut method to add a MX record to be added to the zone. + :type name: string + :param name: The name of the record, for example 'www.example.com.'. + + :type data: list + :param data: A list of the textual representation of Resource Records. + + :type ttl: int + :param ttl: The record's time to live. + """ + + record = Record(name, data, ttl, 'MX') + self.add_record(record) + + def add_ns(self, name, data, ttl): + """ Shortcut method to add a NS record to be added to the zone. + :type name: string + :param name: The name of the record, for example 'www.example.com.'. + + :type data: list + :param data: A list of the textual representation of Resource Records. + + :type ttl: int + :param ttl: The record's time to live. + """ + + record = Record(name, data, ttl, 'NS') + self.add_record(record) + + def add_ptr(self, name, data, ttl): + """ Shortcut method to add a PTR record to be added to the zone. + :type name: string + :param name: The name of the record, for example 'www.example.com.'. + + :type data: list + :param data: A list of the textual representation of Resource Records. + + :type ttl: int + :param ttl: The record's time to live. + """ + + record = Record(name, data, ttl, 'PTR') + self.add_record(record) + + def add_soa(self, name, data, ttl): + """ Shortcut method to add a SOA record to be added to the zone. + :type name: string + :param name: The name of the record, for example 'www.example.com.'. + + :type data: list + :param data: A list of the textual representation of Resource Records. + + :type ttl: int + :param ttl: The record's time to live. + """ + + record = Record(name, data, ttl, 'SOA') + self.add_record(record) + + def add_spf(self, name, data, ttl): + """ Shortcut method to add a SRV record to be added to the zone. + :type name: string + :param name: The name of the record, for example 'www.example.com.'. + + :type data: list + :param data: A list of the textual representation of Resource Records. + + :type ttl: int + :param ttl: The record's time to live. + """ + + record = Record(name, data, ttl, 'SRV') + self.add_record(record) + + def add_txt(self, name, data, ttl): + """ Shortcut method to add a TXT record to be added to the zone. + :type name: string + :param name: The name of the record, for example 'www.example.com.'. + + :type data: list + :param data: A list of the textual representation of Resource Records. + + :type ttl: int + :param ttl: The record's time to live. + """ + + record = Record(name, data, ttl, 'TXT') + self.add_record(record) diff --git a/gcloud/exceptions.py b/gcloud/exceptions.py new file mode 100644 index 000000000000..b05d5586307b --- /dev/null +++ b/gcloud/exceptions.py @@ -0,0 +1,18 @@ +# TODO: Make these super useful. + + +class Error(Exception): + pass + + +class ConnectionError(Error): + + def __init__(self, response, content): + message = str(response) + content + super(ConnectionError, self).__init__(message) + + +class NotFoundError(Error): + + def __init__(self, response, content): + self.message = 'GET %s returned a 404.' % (response.url)