diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 000000000000..f6da14e67f15
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,8 @@
+language: python
+python:
+ - "2.6"
+ - "2.7"
+# command to install dependencies
+install: "pip install . unittest2"
+# command to run tests
+script: nosetests
diff --git a/README.rst b/README.rst
index 1961c399df94..85ef1dcd4d97 100644
--- a/README.rst
+++ b/README.rst
@@ -1,28 +1,36 @@
-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/
+.. image:: https://travis-ci.org/GoogleCloudPlatform/gcloud-python.svg?branch=master
+ :target: https://travis-ci.org/GoogleCloudPlatform/gcloud-python
-Incredibly quick demo
----------------------
+Quickstart
+----------
-Start by cloning the repository::
+The library is ``pip``-installable::
- $ git clone git://github.com/GoogleCloudPlatform/gcloud-python.git
- $ cd gcloud
- $ python setup.py develop
+ $ pip install gcloud
+ $ python -m gcloud.storage.demo # Runs the storage demo!
+
+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 +40,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 +59,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.
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
---------------
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/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/datastore/query.py b/gcloud/datastore/query.py
index 46c34c1e0933..97b68a644b91 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.
@@ -244,4 +311,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]
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 000000000000..0f5966647b38
Binary files /dev/null and b/gcloud/dns/demo/demo.p12 differ
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)
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..13b5b91bcf83 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')
@@ -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?
@@ -157,7 +165,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 +206,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 +218,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 +337,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 +468,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..214d15824a3d 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
@@ -340,14 +340,14 @@ 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,
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
@@ -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
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/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/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..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_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))
+
+