From b4f411f14c0d00fd0c3c91566131300d022ee1c9 Mon Sep 17 00:00:00 2001 From: Jon Wayne Parrott 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 - ``` - 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 23c553a2966ca4cecf146093f33d8114b4f1e368..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8348 zcmeI0du&tJ8Ng3Vwv|ewX4Hu@my&|vCD%DN@CLnxyptBT_%HjatoYr7%N3zGPed#@ckvA><;HWecDU4&2}*2(h%gm&WgCftWj zkQfW;&mXr@0S&(csd+cjaImbXSy=NC<10}z(efGwgi=;B$dt=HkKYCvp-#3KX+qIu zxx$>zcwk+N=c%1^J9)sO44lmS0##~wzU(43f>wW-p_YXwj>s>3{gHI*^oo1lmJ!X*bw{3UBAKRut zUh5Uy4_94IIkdxC^v{_g%FjuEI+f&;9KQ3i+nK7pU_RivFjP_DTl-&gP_qr$nD=M{ z@z;C3#y5Jsh77svGQ4u$ZX)t}=e52{#Xvk;4qIxNh86kR$OhCAie%&e`U{r{ACQZ@ z-)1#sItef{2L za&U;nm+_lo@lbR7vat&^EcECsH)uSnhlht@etsV4LE5B`j#GW*iur~}xpYlTtc;FC{nl|LK^miZ0y3WU>MIM zmc}94VzEFx9?#&?4l+h;ggzvOeCI$ob=`tBp*&EtirH2xU z4a#KB)IMa{_YdYrnxq>-DrrQ>rfpYe8@|Nc-oHnG*L(HR$}d4Eo2&X@o1~9l@%@W) zU{%rv$`tBAOHJH+?zcv7`!T~x;=<#7@4R5T^5$-%Pz-NAYt+5{d;>R+9W24y&`;ZkZqFGQ6_)<35Z_$5V#ga#=N9985-M0KR*fl@h4M0BxWvbYQr6t zULtYBAMJ%iU>x|?U8z_Zyv1jg_VcZ^kO)pd_)jk`_~2MHZmybbloW?l)lnMrb~TAX zV&%#e+VLvM4!%js+%B6}N!=udFlN5Jv;yQm0sdW({6ny+{{$_b`%pI&q3%#p3R?q( z%3@zpafPo4-GA}ErIfU@j?gpfdgeyI);;S-otz(GefQbXvG3J6hbwD_0*YOuqb1V8 zpQm{(oT|g$!tcw;Ck6l>=mo$z0J@0f0^U0{x?+mD8}QB{9yV7qQ;&$5^%*fb@qUB& zX(O<{D-aZ493K;1xH%!}9+}vt8Sshs4os9)d132glTa=lIJv}MJyU_Y!ZFl62j9@l zggg2y{y~c&Vm0cMa@}r@bbn?HR4Sa|66p;nlMl_6D%_%EjCNRq)NBvx!R!t`@zUoW zKV#O#EhZy4>~?VU+rfg@_W_4Kt~zS<|3JjVMcZ#exy;pDUypq|xjpD&0#H{BHgrvM zI=z9nnTN~NCX?mf@DOU;!82-#gXGsw5CSSf22lJ>u8duE&igGuZq4lVZf*LH2%QqOqkv@O{w`Y}q~yWm0EP zeSP~Hau49ZBU@&hWwH5YG0dnGVN1^iztQFh>rIvj5$iQ;q^nyuSuXo`U~{E8DpuIS zA{kR5oC9o=_>jhfRX@QTs1KRm{@F31 zFKP1!w?51@R^I~kA%H*B0W^sKnyVIsv`_2;=zbUGR9pOTqUhV{{^UH=RQ2@S?`uaQ zEy`)Gsd}1IEedW&4lAe0Sg5h`nQXqaZ}RyEzX{FT?hmF3>0_QQT1V~jI$wc&1@aZh ZS0G=3d706zJ{{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():