From b4f411f14c0d00fd0c3c91566131300d022ee1c9 Mon Sep 17 00:00:00 2001
From: Jon Wayne Parrott <jon.wayne.parrott@gmail.com>
Date: Fri, 25 Sep 2015 11:48:22 -0700
Subject: [PATCH] Bringing namespace samples up to standards

---
 appengine/multitenancy/README.md              |  59 ++----------
 appengine/multitenancy/__init__.py            |   0
 appengine/multitenancy/app.yaml               |  21 ++--
 .../multitenancy/{main.py => datastore.py}    |  55 ++++++-----
 appengine/multitenancy/datastore_test.py      |  40 ++++++++
 appengine/multitenancy/favicon.ico            | Bin 8348 -> 0 bytes
 appengine/multitenancy/memcache.py            |  53 ++++++++++
 appengine/multitenancy/memcache_test.py       |  40 ++++++++
 appengine/multitenancy/taskqueue.py           |  91 ++++++++++++++++++
 appengine/multitenancy/taskqueue_test.py      |  46 +++++++++
 tests/utils.py                                |  21 +++-
 11 files changed, 335 insertions(+), 91 deletions(-)
 create mode 100644 appengine/multitenancy/__init__.py
 rename appengine/multitenancy/{main.py => datastore.py} (50%)
 create mode 100644 appengine/multitenancy/datastore_test.py
 delete mode 100644 appengine/multitenancy/favicon.ico
 create mode 100644 appengine/multitenancy/memcache.py
 create mode 100644 appengine/multitenancy/memcache_test.py
 create mode 100644 appengine/multitenancy/taskqueue.py
 create mode 100644 appengine/multitenancy/taskqueue_test.py

diff --git a/appengine/multitenancy/README.md b/appengine/multitenancy/README.md
index c8787c136fe9..5ee7773df9b6 100644
--- a/appengine/multitenancy/README.md
+++ b/appengine/multitenancy/README.md
@@ -1,56 +1,13 @@
-## Multitenancy Using Namespaces Sample
+## Google App Engine Namespaces
 
-This is a sample app for Google App Engine that exercises the [namespace manager Python API](https://cloud.google.com/appengine/docs/python/multitenancy/multitenancy).
+This sample demonstrates how to use Google App Engine's [Namespace Manager API](https://cloud.google.com/appengine/docs/python/multitenancy/multitenancy) in Python.
 
-See our other [Google Cloud Platform github
-repos](https://github.com/GoogleCloudPlatform) for sample applications and
-scaffolding for other python frameworks and use cases.
+### Running the sample
 
-## Run Locally
-1. Install the [Google Cloud SDK](https://cloud.google.com/sdk/), including the [gcloud tool](https://cloud.google.com/sdk/gcloud/), and [gcloud app component](https://cloud.google.com/sdk/gcloud-app).
-2. Setup the gcloud tool.
+You can run the sample on your development server:
+        
+      $ dev_appserver.py .
 
-   ```
-   gcloud components update app
-   gcloud auth login
-   gcloud config set project <your-app-id>
-   ```
-   You don't need a valid app-id to run locally, but will need a valid id to deploy below.
-   
-1. Clone this repo.
+Or deploy the application:
 
-   ```
-   git clone https://github.com/GoogleCloudPlatform/appengine-multitenancy-python.git
-   ```
-1. Run this project locally from the command line.
-
-   ```
-   gcloud preview app run appengine-multitenancy-python/
-   ```
-
-1. Visit the application at [http://localhost:8080](http://localhost:8080).
-
-## Deploying
-
-1. Use the [Cloud Developer Console](https://console.developer.google.com)  to create a project/app id. (App id and project id are identical)
-2. Configure gcloud with your app id.
-
-   ```
-   gcloud config set project <your-app-id>
-   ```
-1. Use the [Admin Console](https://appengine.google.com) to view data, queues, and other App Engine specific administration tasks.
-1. Use gcloud to deploy your app.
-
-   ```
-   gcloud preview app deploy appengine-multitenancy-python/
-   ```
-
-1. Congratulations!  Your application is now live at your-app-id.appspot.com
-
-## Contributing changes
-
-* See [CONTRIBUTING.md](CONTRIBUTING.md)
-
-## Licensing
-
-* See [LICENSE](LICENSE)
+     $ appcfg.py update .
diff --git a/appengine/multitenancy/__init__.py b/appengine/multitenancy/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/appengine/multitenancy/app.yaml b/appengine/multitenancy/app.yaml
index 5227472ff0cf..47261e342688 100644
--- a/appengine/multitenancy/app.yaml
+++ b/appengine/multitenancy/app.yaml
@@ -1,22 +1,13 @@
-# This file specifies your Python application's runtime configuration
-# including URL routing, versions, static file uploads, etc. See
-# https://developers.google.com/appengine/docs/python/config/appconfig
-# for details.
-
 version: 1
 runtime: python27
 api_version: 1
 threadsafe: yes
 
-# Handlers define how to route requests to your application.
 handlers:
+- url: /datastore.*
+  script: datastore.app
+- url: /memcache.*
+  script: memcache.app
+- url: /task.*
+  script: taskqueue.app
 
-# This handler tells app engine how to route requests to a WSGI application.
-# The script value is in the format <path.to.module>.<wsgi_application>
-# where <wsgi_application> is a WSGI application object.
-- url: .*  # This regex directs all routes to main.app
-  script: main.app
-
-libraries:
-- name: webapp2
-  version: "2.5.2"
diff --git a/appengine/multitenancy/main.py b/appengine/multitenancy/datastore.py
similarity index 50%
rename from appengine/multitenancy/main.py
rename to appengine/multitenancy/datastore.py
index 8d3253b8bf27..c7d558cea1b8 100644
--- a/appengine/multitenancy/main.py
+++ b/appengine/multitenancy/datastore.py
@@ -12,6 +12,13 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+"""
+Sample App Engine application demonstrating how to use the Namespace Manager
+API with Datastore.
+
+For more information about App Engine, see README.md under /appengine.
+"""
+
 # [START all]
 from google.appengine.api import namespace_manager
 from google.appengine.ext import ndb
@@ -19,45 +26,45 @@
 
 
 class Counter(ndb.Model):
-    """Model for containing a count."""
     count = ndb.IntegerProperty()
 
 
+@ndb.transactional
 def update_counter(name):
     """Increment the named counter by 1."""
+    counter = Counter.get_by_id(name)
+    if counter is None:
+        counter = Counter(id=name, count=0)
 
-    def _update_counter(inner_name):
-        counter = Counter.get_by_id(inner_name)
-        if counter is None:
-            counter = Counter(id=inner_name)
-            counter.count = 0
-        counter.count += 1
-        counter.put()
+    counter.count += 1
+    counter.put()
 
-    # Update counter in a transaction.
-    ndb.transaction(lambda: _update_counter(name))
-    counter = Counter.get_by_id(name)
     return counter.count
 
 
-class SomeRequest(webapp2.RequestHandler):
-    """Perform synchronous requests to update counter."""
+class DatastoreCounterHandler(webapp2.RequestHandler):
+    """Increments counters in the global namespace as well as in whichever
+    namespace is specified by the request, which is arbitrarily named 'default'
+    if not specified."""
+
+    def get(self, namespace='default'):
+        global_count = update_counter('counter')
 
-    def get(self):
-        update_counter('SomeRequest')
-        # try/finally pattern to temporarily set the namespace.
         # Save the current namespace.
-        namespace = namespace_manager.get_namespace()
+        previous_namespace = namespace_manager.get_namespace()
         try:
-            namespace_manager.set_namespace('-global-')
-            x = update_counter('SomeRequest')
+            namespace_manager.set_namespace(namespace)
+            namespace_count = update_counter('counter')
         finally:
             # Restore the saved namespace.
-            namespace_manager.set_namespace(namespace)
-        self.response.write('<html><body><p>Updated counters')
-        self.response.write(' to %s' % x)
-        self.response.write('</p></body></html>')
+            namespace_manager.set_namespace(previous_namespace)
+
+        self.response.write('Global: {}, Namespace {}: {}'.format(
+            global_count, namespace, namespace_count))
 
 
-app = webapp2.WSGIApplication([('/', SomeRequest)], debug=True)
+app = webapp2.WSGIApplication([
+    (r'/datastore', DatastoreCounterHandler),
+    (r'/datastore/(.*)', DatastoreCounterHandler)
+], debug=True)
 # [END all]
diff --git a/appengine/multitenancy/datastore_test.py b/appengine/multitenancy/datastore_test.py
new file mode 100644
index 000000000000..f63f5e57a559
--- /dev/null
+++ b/appengine/multitenancy/datastore_test.py
@@ -0,0 +1,40 @@
+# Copyright 2015 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import tests
+import webtest
+
+from . import datastore
+
+
+class TestNamespaceDatastoreSample(tests.AppEngineTestbedCase):
+
+    def setUp(self):
+        super(TestNamespaceDatastoreSample, self).setUp()
+        self.app = webtest.TestApp(datastore.app)
+
+    def test_get(self):
+        response = self.app.get('/datastore')
+        self.assertEqual(response.status_int, 200)
+        self.assertTrue('Global: 1' in response.body)
+
+        response = self.app.get('/datastore/a')
+        self.assertEqual(response.status_int, 200)
+        self.assertTrue('Global: 2' in response.body)
+        self.assertTrue('a: 1' in response.body)
+
+        response = self.app.get('/datastore/b')
+        self.assertEqual(response.status_int, 200)
+        self.assertTrue('Global: 3' in response.body)
+        self.assertTrue('b: 1' in response.body)
diff --git a/appengine/multitenancy/favicon.ico b/appengine/multitenancy/favicon.ico
deleted file mode 100644
index 23c553a2966ca4cecf146093f33d8114b4f1e368..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 8348
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
ZS0G=3d<F6q$X6g=fqVt>706zJ{{SaIO|Jj|

diff --git a/appengine/multitenancy/memcache.py b/appengine/multitenancy/memcache.py
new file mode 100644
index 000000000000..3d120e7dd0e5
--- /dev/null
+++ b/appengine/multitenancy/memcache.py
@@ -0,0 +1,53 @@
+# Copyright 2015 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Sample App Engine application demonstrating how to use the Namespace Manager
+API with Memcache.
+
+For more information about App Engine, see README.md under /appengine.
+"""
+
+# [START all]
+from google.appengine.api import memcache
+from google.appengine.api import namespace_manager
+import webapp2
+
+
+class MemcacheCounterHandler(webapp2.RequestHandler):
+    """Increments counters in the global namespace as well as in whichever
+    namespace is specified by the request, which is arbitrarily named 'default'
+    if not specified."""
+
+    def get(self, namespace='default'):
+        global_count = memcache.incr('counter', initial_value=0)
+
+        # Save the current namespace.
+        previous_namespace = namespace_manager.get_namespace()
+        try:
+            namespace_manager.set_namespace(namespace)
+            namespace_count = memcache.incr('counter', initial_value=0)
+        finally:
+            # Restore the saved namespace.
+            namespace_manager.set_namespace(previous_namespace)
+
+        self.response.write('Global: {}, Namespace {}: {}'.format(
+            global_count, namespace, namespace_count))
+
+
+app = webapp2.WSGIApplication([
+    (r'/memcache', MemcacheCounterHandler),
+    (r'/memcache/(.*)', MemcacheCounterHandler)
+], debug=True)
+# [END all]
diff --git a/appengine/multitenancy/memcache_test.py b/appengine/multitenancy/memcache_test.py
new file mode 100644
index 000000000000..b37b1f268910
--- /dev/null
+++ b/appengine/multitenancy/memcache_test.py
@@ -0,0 +1,40 @@
+# Copyright 2015 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import tests
+import webtest
+
+from . import memcache
+
+
+class TestNamespaceMemcacheSample(tests.AppEngineTestbedCase):
+
+    def setUp(self):
+        super(TestNamespaceMemcacheSample, self).setUp()
+        self.app = webtest.TestApp(memcache.app)
+
+    def test_get(self):
+        response = self.app.get('/memcache')
+        self.assertEqual(response.status_int, 200)
+        self.assertTrue('Global: 1' in response.body)
+
+        response = self.app.get('/memcache/a')
+        self.assertEqual(response.status_int, 200)
+        self.assertTrue('Global: 2' in response.body)
+        self.assertTrue('a: 1' in response.body)
+
+        response = self.app.get('/memcache/b')
+        self.assertEqual(response.status_int, 200)
+        self.assertTrue('Global: 3' in response.body)
+        self.assertTrue('b: 1' in response.body)
diff --git a/appengine/multitenancy/taskqueue.py b/appengine/multitenancy/taskqueue.py
new file mode 100644
index 000000000000..313998ef2f27
--- /dev/null
+++ b/appengine/multitenancy/taskqueue.py
@@ -0,0 +1,91 @@
+# Copyright 2015 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Sample App Engine application demonstrating how to use the Namespace Manager
+API with Memcache.
+
+For more information about App Engine, see README.md under /appengine.
+"""
+
+# [START all]
+from google.appengine.api import namespace_manager
+from google.appengine.api import taskqueue
+from google.appengine.ext import ndb
+import webapp2
+
+
+class Counter(ndb.Model):
+    count = ndb.IntegerProperty()
+
+
+@ndb.transactional
+def update_counter(name):
+    """Increment the named counter by 1."""
+    counter = Counter.get_by_id(name)
+    if counter is None:
+        counter = Counter(id=name, count=0)
+
+    counter.count += 1
+    counter.put()
+
+    return counter.count
+
+
+def get_count(name):
+    counter = Counter.get_by_id(name)
+    if not counter:
+        return 0
+    return counter.count
+
+
+class DeferredCounterHandler(webapp2.RequestHandler):
+    def post(self):
+        name = self.request.get('counter_name')
+        update_counter(name)
+
+
+class TaskQueueCounterHandler(webapp2.RequestHandler):
+    """Queues two tasks to increment a counter in global namespace as well as
+    the namespace is specified by the request, which is arbitrarily named
+    'default' if not specified."""
+    def get(self, namespace='default'):
+        # Queue task to update global counter.
+        current_global_count = get_count('counter')
+        taskqueue.add(
+            url='/tasks/counter',
+            params={'counter_name': 'counter'})
+
+        # Queue task to update counter in specified namespace.
+        previous_namespace = namespace_manager.get_namespace()
+        try:
+            namespace_manager.set_namespace(namespace)
+            current_namespace_count = get_count('counter')
+            taskqueue.add(
+                url='/tasks/counter',
+                params={'counter_name': 'counter'})
+        finally:
+            namespace_manager.set_namespace(previous_namespace)
+
+        self.response.write(
+            'Counters will be updated asyncronously.'
+            'Current values: Global: {}, Namespace {}: {}'.format(
+                current_global_count, namespace, current_namespace_count))
+
+
+app = webapp2.WSGIApplication([
+    (r'/tasks/counter', DeferredCounterHandler),
+    (r'/taskqueue', TaskQueueCounterHandler),
+    (r'/taskqueue/(.*)', TaskQueueCounterHandler)
+], debug=True)
diff --git a/appengine/multitenancy/taskqueue_test.py b/appengine/multitenancy/taskqueue_test.py
new file mode 100644
index 000000000000..5fdf5c18835b
--- /dev/null
+++ b/appengine/multitenancy/taskqueue_test.py
@@ -0,0 +1,46 @@
+# Copyright 2015 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import tests
+import webtest
+
+from . import taskqueue
+
+
+class TestNamespaceTaskQueueSample(tests.AppEngineTestbedCase):
+
+    def setUp(self):
+        super(TestNamespaceTaskQueueSample, self).setUp()
+        self.app = webtest.TestApp(taskqueue.app)
+
+    def test_get(self):
+        response = self.app.get('/taskqueue')
+        self.assertEqual(response.status_int, 200)
+        self.assertTrue('Global: 0' in response.body)
+
+        self.runTasks()
+
+        response = self.app.get('/taskqueue')
+        self.assertEqual(response.status_int, 200)
+        self.assertTrue('Global: 1' in response.body)
+
+        response = self.app.get('/taskqueue/a')
+        self.assertEqual(response.status_int, 200)
+        self.assertTrue('a: 0' in response.body)
+
+        self.runTasks()
+
+        response = self.app.get('/taskqueue/a')
+        self.assertEqual(response.status_int, 200)
+        self.assertTrue('a: 1' in response.body)
diff --git a/tests/utils.py b/tests/utils.py
index 7c8064e58865..beba2bffa6aa 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -28,6 +28,7 @@
     APPENGINE_AVAILABLE = True
     from google.appengine.datastore import datastore_stub_util
     from google.appengine.ext import testbed
+    from google.appengine.api import namespace_manager
 except ImportError:
     APPENGINE_AVAILABLE = False
 
@@ -87,7 +88,9 @@ def setUp(self):
 
         # Setup remaining stubs.
         self.testbed.init_user_stub()
-        self.testbed.init_taskqueue_stub()
+        self.testbed.init_taskqueue_stub(root_path='tests/resources')
+        self.taskqueue_stub = self.testbed.get_stub(
+            testbed.TASKQUEUE_SERVICE_NAME)
 
     def tearDown(self):
         super(AppEngineTestbedCase, self).tearDown()
@@ -104,6 +107,22 @@ def loginUser(self, email='user@example.com', id='123', is_admin=False):
             user_is_admin='1' if is_admin else '0',
             overwrite=True)
 
+    def runTasks(self):
+        tasks = self.taskqueue_stub.get_filtered_tasks()
+        for task in tasks:
+            namespace = task.headers.get('X-AppEngine-Current-Namespace', '')
+            previous_namespace = namespace_manager.get_namespace()
+            try:
+                namespace_manager.set_namespace(namespace)
+                self.app.post(
+                    task.url,
+                    task.extract_params(),
+                    headers={
+                        k: v for k, v in task.headers.iteritems()
+                        if k.startswith('X-AppEngine')})
+            finally:
+                namespace_manager.set_namespace(previous_namespace)
+
 
 @contextlib.contextmanager
 def capture_stdout():