Repository: googleapis/google-cloud-python

Issue:
Raise warning/exception when user tries to `get_bucket(x)` if x includes forward slash
Hi there.

I made a silly mistake when trying to list blobs in a bucket, and have a proposal that could prevent users from repeating said mistake.

When initializing a `Bucket` object, I did so with a forward slash ("/") in the bucket name string. This produced a supposedly valid bucket, as seen here (actual bucket name changed for sake of demonstration):

```pycon
>>> from gcloud import storage
>>> c = storage.Client()
>>> b0 = c.get_bucket('valid-bucket')
>>> b0.exists()
True
>>> b1 = c.get_bucket('valid-bucket/')  # <-- A silly mistake
>>> b1.exists()
True
```

While both methods of initialization produce seemingly valid `Bucket` objects, initializing with a forward slash breaks further on, i.e:

```pycon
>>> len([x for x in b0.list_blobs()])
8112
>>> len([x for x in b1.list_blobs()])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in <listcomp>
  File "/usr/local/lib/python3.5/dist-packages/gcloud/iterator.py", line 79, in __iter__
    response = self.get_next_page_response()
  File "/usr/local/lib/python3.5/dist-packages/gcloud/iterator.py", line 115, in get_next_page_response
    method='GET', path=self.path, query_params=self.get_query_params())
  File "/usr/local/lib/python3.5/dist-packages/gcloud/connection.py", line 347, in api_request
    error_info=method + ' ' + url)
gcloud.exceptions.NotFound: 404 Not Found (GET https://www.googleapis.com/storage/v1/b/valid-bucket//o?projection=noAcl)
```

`gcloud` adds a second forward slash when constructing URLs internally, and this obviously breaks. I'd suggest raising a warning or exception when users try to initialize a bucket with a forward slash, or alternatively fixing it behind-the-scenes (simply dropping the slash).

Cheers,
Eilam


Discussion:
Thanks for reporting!

>Bucket names must start and end with a number or letter

https://cloud.google.com/storage/docs/naming#requirements

I think it could be good for us to raise an exception in `Bucket.__init__` if the name isn't valid.

Resolving patch:
diff --git a/storage/google/cloud/storage/_helpers.py b/storage/google/cloud/storage/_helpers.py
--- a/storage/google/cloud/storage/_helpers.py
+++ b/storage/google/cloud/storage/_helpers.py
@@ -21,6 +21,25 @@
 from hashlib import md5
 
 
+def _validate_name(name):
+    """Pre-flight ``Bucket`` name validation.
+
+    :type name: str or :data:`NoneType`
+    :param name: Proposed bucket name.
+
+    :rtype: str or :data:`NoneType`
+    :returns: ``name`` if valid.
+    """
+    if name is None:
+        return
+
+    # The first and las characters must be alphanumeric.
+    if not all([name[0].isalnum(), name[-1].isalnum()]):
+        raise ValueError(
+            'Bucket names must start and end with a number or letter.')
+    return name
+
+
 class _PropertyMixin(object):
     """Abstract mixin for cloud storage classes with associated propertties.
 
@@ -29,11 +48,12 @@ class _PropertyMixin(object):
       - path
 
     :type name: str
-    :param name: The name of the object.
+    :param name: The name of the object. Bucket names must start and end with a
+                 number or letter.
     """
 
     def __init__(self, name=None):
-        self.name = name
+        self.name = _validate_name(name)
         self._properties = {}
         self._changes = set()
 
diff --git a/storage/google/cloud/storage/bucket.py b/storage/google/cloud/storage/bucket.py
--- a/storage/google/cloud/storage/bucket.py
+++ b/storage/google/cloud/storage/bucket.py
@@ -81,7 +81,8 @@ class Bucket(_PropertyMixin):
                    for the bucket (which requires a project).
 
     :type name: str
-    :param name: The name of the bucket.
+    :param name: The name of the bucket. Bucket names must start and end with a
+                 number or letter.
     """
 
     _MAX_OBJECTS_FOR_ITERATION = 256


Test patch:
diff --git a/storage/unit_tests/test__helpers.py b/storage/unit_tests/test__helpers.py
--- a/storage/unit_tests/test__helpers.py
+++ b/storage/unit_tests/test__helpers.py
@@ -46,6 +46,19 @@ def test_client_is_abstract(self):
         mixin = self._make_one()
         self.assertRaises(NotImplementedError, lambda: mixin.client)
 
+    def test_bucket_name_value(self):
+        bucket_name = 'testing123'
+        mixin = self._make_one(name=bucket_name)
+        self.assertEqual(mixin.name, bucket_name)
+
+        bad_start_bucket_name = '/testing123'
+        with self.assertRaises(ValueError):
+            self._make_one(name=bad_start_bucket_name)
+
+        bad_end_bucket_name = 'testing123/'
+        with self.assertRaises(ValueError):
+            self._make_one(name=bad_end_bucket_name)
+
     def test_reload(self):
         connection = _Connection({'foo': 'Foo'})
         client = _Client(connection)