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 - ``` - 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 - ``` -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 . -# where 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('

Updated counters') - self.response.write(' to %s' % x) - self.response.write('

') + 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 23c553a2966c..000000000000 Binary files a/appengine/multitenancy/favicon.ico and /dev/null differ 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():